commit 914fea506ec51856e33352990722190914e35ec6 Author: wehub-resource-sync Date: Mon Jul 13 13:30:30 2026 +0800 chore: import upstream snapshot with attribution diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..b9a948f --- /dev/null +++ b/.editorconfig @@ -0,0 +1,3 @@ +[*.sh] +indent_style = space +indent_size = 2 diff --git a/.gemini/config.yaml b/.gemini/config.yaml new file mode 100644 index 0000000..33ef390 --- /dev/null +++ b/.gemini/config.yaml @@ -0,0 +1,2 @@ +code_review: + comment_severity_threshold: LOW diff --git a/.gemini/styleguide.md b/.gemini/styleguide.md new file mode 100644 index 0000000..e182be5 --- /dev/null +++ b/.gemini/styleguide.md @@ -0,0 +1,153 @@ +# GoogleCloudPlatform/generative-ai Style Guide + +The current year is 2026. + +IMPORTANT: Agent Platform (full name Gemini Enterprise Agent Platform) was previously named "Vertex AI" and many web resources use the legacy branding. Make sure that all new content uses the new name. + +## Markdown Style (For `.md` files and `.ipynb` Markdown Cells) + +- Use single backticks ( ` ` ) to format inline code elements, such as variable names, function names, enum names, and brief code snippets. +- Add documentation links to the appropriate Agent Platform pages when describing product features. e.g. https://docs.cloud.google.com/gemini-enterprise-agent-platform/ + - Do not reference documentation from the Gemini Developer API, e.g. https://ai.google.dev/ unless there is not a suitable page in the Agent Platform documentation. + +The Author block in Notebooks and Markdown should be in a format like this: + +For one author: + +| Author | +| --- | +| [Firstname Lastname](https://github.com/username) | + +For multiple authors + +| Authors | +| --- | +| [Firstname Lastname](https://github.com/username) | +| [Firstname Lastname](https://github.com/username) | + +## Code Requirements + +- Don't include hard-coded Google Cloud project IDs, always use a placeholder like `[your-project-id]` + +**Correct** + +```py +PROJECT_ID = "[your-project-id]" +``` + +**Incorrect** + +```py +PROJECT_ID = "actual-projectid-1234" +``` + +- Use the `global` endpoint for all requests, unless a regional endpoint is specifically required for a model or feature. + +**Correct** + +```py +LOCATION = "global" +``` + +**Incorrect** + +```py +LOCATION = "us-central1" +``` + +- Don't restart the kernel or use `!pip`, use `%pip` when installing + +**Correct** + +```sh +%pip install +``` + +**Incorrect** + +```sh +!pip install +``` + +```sh +!pip3 install +``` + +```py +app = IPython.Application.instance() +app.kernel.do_shutdown(True) +``` + +## Golden Rule: Use the Correct and Current SDK + +Always use the **Google Gen AI SDK** (`google-genai`), which is the unified +standard library for all Gemini API requests (AI Studio/Gemini Developer API +and Vertex AI/Agent Platform) as of 2026. Do not use legacy libraries and SDKs. + +- **Library Name:** Google Gen AI SDK +- **Python Package:** `google-genai` +- **Legacy Library**: (`google-generativeai`) is deprecated. + +**Installation:** + +- **Incorrect:** `pip install google-generativeai` +- **Incorrect:** `pip install google-ai-generativelanguage` +- **Correct:** `pip install google-genai` + +**APIs and Usage:** + +- **Incorrect:** `import google.generativeai as genai`-> **Correct:** `from + google import genai` +- **Incorrect:** `from google.ai import generativelanguage_v1` -> + **Correct:** `from google import genai` +- **Incorrect:** `from google.generativeai` -> **Correct:** `from google + import genai` +- **Incorrect:** `from google.generativeai import types` -> **Correct:** `from + google.genai import types` +- **Incorrect:** `import google.generativeai as genai` -> **Correct:** `from + google import genai` +- **Incorrect:** `genai.configure(api_key=...)` -> **Correct:** `client = + genai.Client(api_key='...')` +- **Incorrect:** `model = genai.GenerativeModel(...)` +- **Incorrect:** `model.generate_content(...)` -> **Correct:** + `client.models.generate_content(...)` +- **Incorrect:** `response = model.generate_content(..., stream=True)` -> + **Correct:** `client.models.generate_content_stream(...)` +- **Incorrect:** `genai.GenerationConfig(...)` -> **Correct:** + `types.GenerateContentConfig(...)` +- **Incorrect:** `safety_settings={...}` -> **Correct:** Use `safety_settings` + inside a `GenerateContentConfig` object. +- **Incorrect:** `from google.api_core.exceptions import GoogleAPIError` -> + **Correct:** `from google.genai.errors import APIError` +- **Incorrect:** `types.ResponseModality.TEXT` + +## Models + +- By default, use the following models when using `google-genai`: + - **General Text & Multimodal Tasks:** `gemini-3.5-flash` + - **Coding and Complex Reasoning Tasks:** `gemini-3.1-pro-preview` + - **Low Latency & High Volume Tasks:** `gemini-3.1-flash-lite` + - **Fast Image Generation and Editing:** `gemini-3.1-flash-image` (aka Nano Banana 2) + - **High-Quality Image Generation and Editing:** `gemini-3-pro-image` (aka Nano Banana Pro) + - **High-Fidelity Video Generation:** or `veo-3.1-generate-001` + - **Advanced Video Editing Tasks:** `veo-3.1-generate-001` + - **Video Generation, Balance of speed and quality:** `veo-3.1-fast-generate-001` + - **Fast Video Generation:** `veo-3.1-lite-generate-001` + +- It is also acceptable to use following models if explicitly requested by the + user: + - **Gemini 2.5 Series**: `gemini-2.5-flash`, `gemini-2.5-pro`, `gemini-2.5-flash-lite` + - **Nano Banana:** `gemini-2.5-flash-image` + +- Do not use the following deprecated models (or their variants like + `gemini-1.5-flash-latest`): + - **Gemini 3 Pro Preview**: `gemini-3-pro-preview` (Replaced by `gemini-3.1-pro-preview`) + - **Gemini 3 Flash Preview**: `gemini-3-flash-preview` + - **Gemini 3.1 Flash Image Preview**: `gemini-3.1-flash-image-preview` + - **Gemini 3 Pro Image Preview**: `gemini-3-pro-image-preview` + - **Gemini 2.0 Series**: `gemini-2.0-flash`, `gemini-2.0-flash-lite` + - **Gemini 1.5 Series**: `gemini-1.5-flash`, `gemini-1.5-pro` + - **Prohibited:** `gemini-pro` + - **Veo 3:** `veo-3.0-generate-001`, `veo-3.0-fast-generate-001` + - **Veo 2:** `veo-2.0-generate-001` + - **Imagen:** `imagegeneration@00*`, `imagetext@001` `imagen-3.0-*`, `imagen-4.0-*` diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs new file mode 100644 index 0000000..af753fe --- /dev/null +++ b/.git-blame-ignore-revs @@ -0,0 +1,93 @@ +# Template taken from https://github.com/v8/v8/blob/master/.git-blame-ignore-revs. +# +# This file contains a list of git hashes of revisions to be ignored by git blame. These +# revisions are considered "unimportant" in that they are unlikely to be what you are +# interested in when blaming. Most of these will probably be commits related to linting +# and code formatting. +# +# Instructions: +# - Only large (generally automated) reformatting or renaming CLs should be +# added to this list. Do not put things here just because you feel they are +# trivial or unimportant. If in doubt, do not put it on this list. +# - Precede each revision with a comment containing the PR title and number. +# For bulk work over many commits, place all commits in a block with a single +# comment at the top describing the work done in those commits. +# - Only put full 40-character hashes on this list (not short hashes or any +# other revision reference). +# - Append to the bottom of the file (revisions should be in chronological order +# from oldest to newest). +# - Because you must use a hash, you need to append to this list in a follow-up +# PR to the actual reformatting PR that you are trying to ignore. +3495535bb8a42cd96bb65058396c464611bf6eb9 +dbca58d5b3fe8bcb3a18b0708692545d4e13c70f +6862d06d1bed3bfb73e4a15d6d22db70980fa345 +9ee2011ee29828b417b7ac9ac31c4a29643a16e7 +43626f81127ddba7a181df451455e103cebee3a5 +e32ff08d8f83a614f2b4206a15a5c8aa00adaa48 +d699c642fa8312e1a0bfc1ad2673bc17648a3c1a +cac56b435fc03002ce6896eec78dff6876bc9dc1 +a27d6c3304d0c8fc5978da93e7c4e7bfebd2eba3 +401df3220d24516eac92f5b4539a76cd7d757814 +33c6bcca6009219efbf05746b9e634c7d4d7dcba +08734ab993a4f5ac1a10561919b54c39ec3af04e +3f89a238eb0b0077ec82544c4fdc1a3fa781416c +a265634234d6c03c83dda1dd29484bdafbf98489 +dc699bc4cf3272a79a07ab93b0576d79afbe77de +9c40a06d35cdb95ddbbf78018d0d87a2a945a35e +418725cc5d10af3952179afa1ba9f9477c767ac2 +5a653d1d93f15995a306bfeb0c0c5d8f0b909242 +5742823bffccefce65087e6530649b4c9beddd67 +5742823bffccefce65087e6530649b4c9beddd67 +fc5d62431a71187ee6f92a8ccc492ca10e5dc2c3 +f35a605a4eaef3a8f0c65f598fe978294cf0e900 +5aec8dc940fa015601f1a9c727331291daffdc3d +ff92e99df5faf575245b635a1accd87e56245149 +c867107284b92cc641e6d274f8088b764b9c2fa2 +d6572fcc7d5d602a96f87fe1754fae9c15b058e8 +f5e47957685ffdc4fd4c01385bb11152d8829232 +bccaf22a73e3965cb02ed7ca3022ca859c53f2b8 +eeced39d16e4638ba6df67a6401e15ca376cd73b +7a5d55d1087fbf1617294281c0eebd212bf1e52c +6ddf4cdd146b928452950408a8c4c1324351681b +0c961fcb3c9cb7bfb5e1ab73acbb8fe7caebcbd4 +647a33fcaeb34ee456e4518b4ca443f82a92c501 +812b47dfb4c12a5621417a6da8cf5796b44e70d3 +a043e43746eea8aeca5dbfb17f2d496a2747b6a3 +aa073fe0b74e3813593476ff9a13496d24e2adab +030647d76d06d3a69a0f4b716ad1035de6808653 +2b29d1d9a7418fa941709179a1f284be91f3195f +94cace18604ef473e77e54a516b76ac263fae42b +cb2d315aab7438ec45db9928536e7c4c3802fc49 +824b32b10f7c3701de214784307b4abb22e5fb0f +b5c2d85557d877bc99bf18fdf549423dc54bb108 +8db09b75bdf1e2955f06fc77decfd06bfde13957 +f3fd6359f6424af47ba9d5f63145a69989e5c920 +221c0b03b29e4abf5a8d07daf2c47d7752a053a4 +12bea7f71729db4d19fa2eac5715d326cae39a22 +67f18b9c0b1beb1174bd55daa2069ab0f9b3e99d +344921c7c585c5e4c54863a58f59efabdbfe06f6 +84ab588c313ddfd08aa6288ed2eef5bf784b1a1a +8e28e0cfa5031892beaebecbe6308696964cc2e5 +6664dd984baa77f8358fa0ed8b57bde2b897e116 +7adb28cb68e34608e193bb47d123df7051714a46 +3e27e38b7eb154763d4c20d11d9c74a6dc08655a +abf108d802b8f0230b38553d0413153a25a01b97 +cf0063a41156c362f0f9823a016343daf46ab46b +b086ad4760a3437f0bf21a144b343e48bd0a015d +96f901d9b1f52a2c2dc467679354cfa78b9cec4a +cbc49cc90875655dcd26647835edfcb41c6b2caf +800d05b4a3a28fe23b971e702e76df0c41e6d308 +3f0974fed57315c6123864fa2334609f3332029a +afdcb95a39e574515a080cbe544fe35e7bf6d28b +21d4430e8d4b1e2cc2d65b643f88465c81cb76bd +b5dd69e1fc58a01c0c9bf07c4ed30aa132b77553 +a41f948147f9c16c9b1255038929ab1d1ac07def +4d099b05aafd58ab6f3bfb816fd312d8d2c7ffcd +6d74fa5d6d8a9a694951a8dac296c1cf314c9d5f +38a81c39a9f2afa674eb89d76bfe5e7671eacf6f +9b71a07da1a2824d108954d5155521ba465df1ef +076464885ddf63d57d91cac830ff83f774d5c877 +ca7ceb9f8358a5d79c31d2511f705bcc55c24232 +0538e91f7c69c34174c722ae1ed1552261c8b805 +6ba714a7cf1d8d460f5518e5bf220b76088aec09 +e8cdc7edfeb79312ebe8dcdcf4662ab64ee5bbc7 diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..29490a2 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,79 @@ +# Code owners file. +# This file controls who is tagged for review for any given pull request. +# +# For syntax help see: +# https://help.github.com/en/github/creating-cloning-and-archiving-repositories/about-code-owners#codeowners-syntax + +# @GoogleCloudPlatform/generative-ai-devrel are the default owners for changes in this repo + + +* @GoogleCloudPlatform/generative-ai-devrel +/generative-ai/embeddings @GoogleCloudPlatform/generative-ai-devrel @kazunori279 +/generative-ai/embeddings/vector-search-2-quickstart.ipynb @GoogleCloudPlatform/generative-ai-devrel @ericgribkoff +/generative-ai/gemini @GoogleCloudPlatform/generative-ai-devrel @polong-lin +/generative-ai/gemini/agent-engine @GoogleCloudPlatform/generative-ai-devrel @koverholt +/generative-ai/gemini/agent-engine/tutorial_ag2_on_agent_engine.ipynb @GoogleCloudPlatform/generative-ai-devrel @awaemmanuel +/generative-ai/gemini/agent-engine/tutorial_alloydb_rag_agent.ipynb @GoogleCloudPlatform/generative-ai-devrel @averikitsch +/generative-ai/gemini/agent-engine/tutorial_cloud_sql_pg_rag_agent.ipynb @GoogleCloudPlatform/generative-ai-devrel @averikitsch +/generative-ai/gemini/agents/genai-experience-concierge @GoogleCloudPlatform/generative-ai-devrel @pablofgaeta +/generative-ai/gemini/code-execution @GoogleCloudPlatform/generative-ai-devrel @koverholt +/generative-ai/gemini/evaluation @GoogleCloudPlatform/generative-ai-devrel @inardini @jsondai +/generative-ai/gemini/evaluation/evaluate_langchain_chains.ipynb @GoogleCloudPlatform/generative-ai-devrel @eliasecchig +/generative-ai/gemini/function-calling @GoogleCloudPlatform/generative-ai-devrel @koverholt +/generative-ai/gemini/getting-started @GoogleCloudPlatform/generative-ai-devrel @gericdong @polong-lin +/generative-ai/gemini/grounding @GoogleCloudPlatform/generative-ai-devrel @holtskinner +/generative-ai/gemini/mcp/intro_to_mcp.ipynb @GoogleCloudPlatform/generative-ai-devrel @wadave +/generative-ai/gemini/model-optimizer/intro_model_optimizer.ipynb @GoogleCloudPlatform/generative-ai-devrel @nardosalemu +/generative-ai/gemini/multimodal-dataset @GoogleCloudPlatform/generative-ai-devrel @fthoele @cleop-google @diskontinuum +/generative-ai/gemini/multimodal-live-api/websocket-demo-app/ @GoogleCloudPlatform/generative-ai-devrel @ZackAkil +/generative-ai/gemini/rag-engine @GoogleCloudPlatform/generative-ai-devrel @edtsoi430 +/generative-ai/gemini/rag-engine/rag_engine_cross_corpus_retrieval.ipynb @GoogleCloudPlatform/generative-ai-devrel @tiger-w-jin +/generative-ai/gemini/responsible-ai @GoogleCloudPlatform/generative-ai-devrel @iamthuya +/generative-ai/gemini/responsible-ai/gemini_safety_ratings.ipynb @GoogleCloudPlatform/generative-ai-devrel @ghchinoy +/generative-ai/gemini/sample-apps/finance-advisor-spanner/ @GoogleCloudPlatform/generative-ai-devrel @anirbanbagchi1979 +/generative-ai/gemini/sample-apps/genai-mlops-tune-and-eval @GoogleCloudPlatform/generative-ai-devrel @willisc7 +/generative-ai/gemini/sample-apps/genwealth/ @GoogleCloudPlatform/generative-ai-devrel @paulramsey +/generative-ai/gemini/sample-apps/photo-discovery @GoogleCloudPlatform/generative-ai-devrel @KhanhNwin @kazunori279 +/generative-ai/gemini/tuning/gemini_supervised_finetuning_text_classification.ipynb @GoogleCloudPlatform/generative-ai-devrel @eliasecchig @gabrielahrlr +/generative-ai/gemini/tuning/supervised_finetuning_using_gemini.ipynb @GoogleCloudPlatform/generative-ai-devrel @dmoonat +/generative-ai/gemini/use-cases/applying-llms-to-data/analyze-poster-images-in-bigquery/poster_image_analysis.ipynb @GoogleCloudPlatform/generative-ai-devrel @aliciawilliams +/generative-ai/gemini/use-cases/applying-llms-to-data/semantic-search-in-bigquery/stackoverflow_questions_semantic_search.ipynb @GoogleCloudPlatform/generative-ai-devrel @sethijaideep +/generative-ai/gemini/use-cases/applying-llms-to-data/using-gemini-with-bigquery-remote-functions @GoogleCloudPlatform/generative-ai-devrel @shanecglass +/generative-ai/gemini/use-cases/education @GoogleCloudPlatform/generative-ai-devrel @PicardParis +/generative-ai/gemini/use-cases/intro_multimodal_use_cases.ipynb @GoogleCloudPlatform/generative-ai-devrel @saeedaghabozorgi +/generative-ai/gemini/use-cases/multimodal-sentiment-analysis/intro_to_multimodal_sentiment_analysis.ipynb @GoogleCloudPlatform/generative-ai-devrel @Mansari +/generative-ai/gemini/use-cases/productivity/productivity_coaching_with_google_calendar_and_gmail.ipynb @GoogleCloudPlatform/generative-ai-devrel @Mansari +/generative-ai/gemini/use-cases/retail @GoogleCloudPlatform/generative-ai-devrel @iamthuya +/generative-ai/gemini/use-cases/retail/product_attributes_extraction.ipynb @GoogleCloudPlatform/generative-ai-devrel @tianli +/generative-ai/gemini/use-cases/retrieval-augmented-generation/rag_pipeline_terabyte_scale_with_bigframes.ipynb @GoogleCloudPlatform/generative-ai-devrel @eliasecchig @lspatarog +/generative-ai/gemini/use-cases/retrieval-augmented-generation/rag_qna_with_bq_and_featurestore.ipynb @GoogleCloudPlatform/generative-ai-devrel @eliasecchig @lspatarog +/generative-ai/gemini/use-cases/retrieval-augmented-generation/raw_with_bigquery.ipynb @GoogleCloudPlatform/generative-ai-devrel @jeffonelson +/generative-ai/gemini/use-cases/retrieval-augmented-generation/retail_warranty_claim_chatbot.ipynb @GoogleCloudPlatform/generative-ai-devrel @zthor5 +/generative-ai/genkit/generate-synthetic-database @GoogleCloudPlatform/generative-ai-devrel @aikozhaoz +/generative-ai/genkit/postcard-generator @GoogleCloudPlatform/generative-ai-devrel @mattsday +/generative-ai/open-models/fine-tuning @GoogleCloudPlatform/generative-ai-devrel @goudtho +/generative-ai/open-models/serving @GoogleCloudPlatform/generative-ai-devrel @polong-lin +/generative-ai/open-models/serving/cloud_run_ollama_gemma3_inference.ipynb @GoogleCloudPlatform/generative-ai-devrel @vladkol +/generative-ai/open-models/serving/cloud_run_vllm_gemma3_inference.ipynb @GoogleCloudPlatform/generative-ai-devrel @vladkol +/generative-ai/open-models/serving/vertex_ai_pytorch_inference_pllum_with_custom_handler.ipynb @GoogleCloudPlatform/generative-ai-devrel @rsamborski +/generative-ai/open-models/use-cases/cloud_run_ollama_gemma2_rag_qa.ipynb @GoogleCloudPlatform/generative-ai-devrel @eliasecchig +/generative-ai/search @GoogleCloudPlatform/generative-ai-devrel @holtskinner +/generative-ai/search/gemini-enterprise/group-licensing @GoogleCloudPlatform/generative-ai-devrel @williamsmt @googlecharles +/generative-ai/search/retrieval-augmented-generation/examples/contract_analysis.ipynb @GoogleCloudPlatform/generative-ai-devrel @guruvittal +/generative-ai/speech/getting-started @GoogleCloudPlatform/generative-ai-devrel @inardini +/generative-ai/speech/use-cases/storytelling @GoogleCloudPlatform/generative-ai-devrel @holtskinner +/generative-ai/vision/getting-started @GoogleCloudPlatform/generative-ai-devrel @iamthuya +/generative-ai/vision/gradio/gradio_image_generation_sdk.ipynb @GoogleCloudPlatform/generative-ai-devrel @jbrache +/generative-ai/vision/use-cases @GoogleCloudPlatform/generative-ai-devrel @iamthuya +/generative-ai/vision/use-cases/hey_llm @GoogleCloudPlatform/generative-ai-devrel @tushuhei +/generative-ai/vision/sample-apps/V-Start @GoogleCloudPlatform/generative-ai-devrel @WafaeBakkali +/generative-ai/open-models/serving/cloud_run_ollama_qwen3_inference.ipynb @GoogleCloudPlatform/generative-ai-devrel @vladkol +/generative-ai/open-models/get_started_with_model_garden_sdk.ipynb @GoogleCloudPlatform/generative-ai-devrel @inardini @lizzij +/generative-ai/open-models/use-cases/model_garden_litellm_inference.ipynb @GoogleCloudPlatform/generative-ai-devrel @lizzij +/generative-ai/llmevalkit @GoogleCloudPlatform/generative-ai-devrel @santoromike @lkatherine +/generative-ai/gemini/evaluation/evaluate_gemini_structured_output.ipynb @GoogleCloudPlatform/generative-ai-devrel @stevie-p +/generative-ai/agents/gke/agents_with_memory/get_started_with_memory_for_adk_in_gke.ipynb @GoogleCloudPlatform/generative-ai-devrel @vladkol +/generative-ai/agents/cloud_run/agents_with_memory/get_started_with_memory_for_adk_in_cloud_run.ipynb @GoogleCloudPlatform/generative-ai-devrel @vladkol +/generative-ai/agents/agent_engine/tutorial_bidi_stream_v2.ipynb @GoogleCloudPlatform/generative-ai-devrel @ericgribkoff +/generative-ai/gemini/sample-apps/gemini-live-telephony-app @GoogleCloudPlatform/generative-ai-devrel @KVishnuVardhanR +/generative-ai/search/auto-rag-eval @tanyagoogle diff --git a/.github/ISSUE_TEMPLATE/bug-report.yml b/.github/ISSUE_TEMPLATE/bug-report.yml new file mode 100644 index 0000000..3045bda --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug-report.yml @@ -0,0 +1,48 @@ +name: 🐞 Bug Report +description: File a bug report +title: "[Bug]: " +assignees: + - GoogleCloudPlatform/generative-ai-devrel +body: + - type: markdown + attributes: + value: | + Thanks for stopping by to let us know something could be better! + + **PLEASE READ**: + - This repository is for [Generative AI with Vertex AI on Google Cloud](https://cloud.google.com/vertex-ai/docs/generative-ai/learn/overview), not the [Google AI Gemini/PaLM APIs](https://ai.google.dev/) + - For issues with the Google AI Gemini API, refer to the [google-gemini](https://github.com/google-gemini) organization or the [Google Developers Community Discourse](https://discuss.ai.google.dev/). + - For issues with the Vertex AI Python SDK, please report in the [googleapis/python-aiplatform](https://github.com/googleapis/python-aiplatform/) repository. + - If you have a support contract with Google, please create an issue in the [support console](https://cloud.google.com/support/) instead of filing on GitHub. This will ensure a timely response. + - For issues with the Vertex AI API, please report them in [Google issue tracker](https://issuetracker.google.com/issues/new?component=1130925&template=1637248&pli=1). + - type: input + id: file-name + attributes: + label: File Name + description: Which notebook/code sample is the issue occurring in? + placeholder: ex. `gemini/getting-started/intro_gemini_python.ipynb` + validations: + required: true + - type: textarea + id: what-happened + attributes: + label: What happened? + description: Also tell us what you expected to happen and how to reproduce the issue. + placeholder: Tell us what you see! + value: "A bug happened!" + validations: + required: true + - type: textarea + id: logs + attributes: + label: Relevant log output + description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks. + render: shell + - type: checkboxes + id: terms + attributes: + label: Code of Conduct + description: By submitting this issue, you agree to follow our [Code of Conduct](https://github.com/GoogleCloudPlatform/generative-ai?tab=coc-ov-file) + options: + - label: I agree to follow this project's Code of Conduct + required: true diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..00e881e --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,23 @@ +blank_issues_enabled: false +contact_links: + - name: Google Gen AI Python SDK + url: https://github.com/googleapis/python-genai + about: For issues with the Google Gen AI Python SDK, please report in the googleapis/python-genai repository. + - name: Vertex AI Python SDK + url: https://github.com/googleapis/python-aiplatform + about: For issues with the Vertex AI Python SDK, please report in the googleapis/python-aiplatform repository. + - name: Google AI Gemini API + url: https://github.com/google-gemini + about: This repository is for Agent Platform on Google Cloud, not the Google AI Gemini/PaLM APIs. For issues with the Google AI Gemini API, report them in the appropriate repository in the google-gemini GitHub organization. + - name: Agent Platform API issue + url: https://issuetracker.google.com/issues/new?component=1130925&template=1637248&pli=1 + about: For issues with the Agent Platform API, please report them in Google issue tracker. + - name: Google Cloud Support + url: https://cloud.google.com/support + about: If you have a support contract with Google, please create a case in the support console instead of filing on GitHub. This will ensure a timely response. + - name: Stack Overflow - Google Cloud Collective + url: https://stackoverflow.com/collectives/google-cloud + about: A collective for developers who utilize Google Cloud's infrastructure and platform capabilities. This collective is organized and managed by the Stack Overflow community. + - name: Google Cloud Skills Boost (Qwiklabs) Support + url: https://support.google.com/qwiklabs/answer/10702448 + about: For issues with Qwiklabs, such as progress not updating, please contact Qwiklabs support. diff --git a/.github/ISSUE_TEMPLATE/feature-request.yml b/.github/ISSUE_TEMPLATE/feature-request.yml new file mode 100644 index 0000000..714a9ce --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature-request.yml @@ -0,0 +1,45 @@ +name: 💡 Feature Request +description: Suggest an idea for this repository +title: "[Feat]: " +assignees: + - GoogleCloudPlatform/generative-ai-devrel +body: + - type: markdown + attributes: + value: | + Thanks for stopping by to let us know something could be better! + **PLEASE READ**: + - This repository is for [Agent Platform on Google Cloud](https://docs.cloud.google.com/gemini-enterprise-agent-platform), not the [Google AI Gemini/PaLM APIs](https://ai.google.dev/) + - For issues with the Google AI Gemini API, refer to one of [these repositories](https://github.com/orgs/google/repositories?q=generative-ai) or the [Google Developers Community Discourse](https://discuss.ai.google.dev/). + - If you have a support contract with Google, please create an issue in the [support console](https://cloud.google.com/support/) instead of filing on GitHub. This will ensure a timely response. + - type: textarea + id: problem + attributes: + label: Is your feature request related to a problem? Please describe. + description: A clear and concise description of what the problem is. + placeholder: Ex. I'm always frustrated when [...] + - type: textarea + id: describe + attributes: + label: Describe the solution you'd like + description: A clear and concise description of what you want to happen. + validations: + required: true + - type: textarea + id: alternatives + attributes: + label: Describe alternatives you've considered + description: A clear and concise description of any alternative solutions or features you've considered. + - type: textarea + id: context + attributes: + label: Additional context + description: Add any other context or screenshots about the feature request here. + - type: checkboxes + id: terms + attributes: + label: Code of Conduct + description: By submitting this issue, you agree to follow our [Code of Conduct](https://github.com/GoogleCloudPlatform/generative-ai?tab=coc-ov-file) + options: + - label: I agree to follow this project's Code of Conduct + required: true diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..5216d0a --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,12 @@ +# Description + +Thank you for opening a Pull Request! +Before submitting your PR, there are a few things you can do to make sure it goes smoothly: + +- [ ] Follow the [`CONTRIBUTING` Guide](https://github.com/GoogleCloudPlatform/generative-ai/blob/main/CONTRIBUTING.md). +- [ ] Ensure the tests and linter pass (Run `./scripts/format.sh` from the repository root to format). +- If you are creating a new notebook/sample: + - [ ] You are listed as the author in your notebook or `README` file. + - [ ] Your account is listed in [`CODEOWNERS`](https://github.com/GoogleCloudPlatform/generative-ai/blob/main/.github/CODEOWNERS) for the file(s). + +Fixes # 🩕 diff --git a/.github/actions/spelling/README.md b/.github/actions/spelling/README.md new file mode 100644 index 0000000..c39c707 --- /dev/null +++ b/.github/actions/spelling/README.md @@ -0,0 +1,17 @@ +# check-spelling/check-spelling configuration + +| File | Purpose | Format | Info | +| -------------------------------------------------- | -------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- | +| [dictionary.txt](dictionary.txt) | Replacement dictionary (creating this file will override the default dictionary) | one word per line | [dictionary](https://github.com/check-spelling/check-spelling/wiki/Configuration#dictionary) | +| [allow.txt](allow.txt) | Add words to the dictionary | one word per line (only letters and `'`s allowed) | [allow](https://github.com/check-spelling/check-spelling/wiki/Configuration#allow) | +| [reject.txt](reject.txt) | Remove words from the dictionary (after allow) | grep pattern matching whole dictionary words | [reject](https://github.com/check-spelling/check-spelling/wiki/Configuration-Examples%3A-reject) | +| [excludes.txt](excludes.txt) | Files to ignore entirely | perl regular expression | [excludes](https://github.com/check-spelling/check-spelling/wiki/Configuration-Examples%3A-excludes) | +| [only.txt](only.txt) | Only check matching files (applied after excludes) | perl regular expression | [only](https://github.com/check-spelling/check-spelling/wiki/Configuration-Examples%3A-only) | +| [patterns.txt](patterns.txt) | Patterns to ignore from checked lines | perl regular expression (order matters, first match wins) | [patterns](https://github.com/check-spelling/check-spelling/wiki/Configuration-Examples%3A-patterns) | +| [candidate.patterns](candidate.patterns) | Patterns that might be worth adding to [patterns.txt](patterns.txt) | perl regular expression with optional comment block introductions (all matches will be suggested) | [candidates](https://github.com/check-spelling/check-spelling/wiki/Feature:-Suggest-patterns) | +| [line_forbidden.patterns](line_forbidden.patterns) | Patterns to flag in checked lines | perl regular expression (order matters, first match wins) | [patterns](https://github.com/check-spelling/check-spelling/wiki/Configuration-Examples%3A-patterns) | +| [advice.md](advice.md) | Supplement for GitHub comment when unrecognized words are found | GitHub Markdown | [advice](https://github.com/check-spelling/check-spelling/wiki/Configuration-Examples%3A-advice) | +| [block-delimiters.list](block-delimiters.list) | Define block begin/end markers to ignore lines of text | line with _literal string_ for **start** followed by line with _literal string_ for **end** | [block ignore](https://github.com/check-spelling/check-spelling/wiki/Feature%3A-Block-Ignore#status) | + +Note: you can replace any of these files with a directory by the same name (minus the suffix) +and then include multiple files inside that directory (with that suffix) to merge multiple files together. diff --git a/.github/actions/spelling/advice.md b/.github/actions/spelling/advice.md new file mode 100644 index 0000000..2c0631a --- /dev/null +++ b/.github/actions/spelling/advice.md @@ -0,0 +1,28 @@ + +
If the flagged items are :exploding_head: false positives + +If items relate to a ... + +- binary file (or some other file you wouldn't want to check at all). + + Please add a file path to the `excludes.txt` file matching the containing file. + + File paths are Perl 5 Regular Expressions - you can [test](https://www.regexplanet.com/advanced/perl/) yours before committing to verify it will match your files. + + `^` refers to the file's path from the root of the repository, so `^README\.md$` would exclude `README.md` (on whichever branch you're using). + +- well-formed pattern. + + If you can write a [pattern](https://github.com/check-spelling/check-spelling/wiki/Configuration-Examples:-patterns) that would match it, + try adding it to the `patterns.txt` file. + + Patterns are Perl 5 Regular Expressions - you can [test](https://www.regexplanet.com/advanced/perl/) yours before committing to verify it will match your lines. + + Note that patterns can't match multiline strings. + +
+ + + +:steam_locomotive: If you're seeing this message and your PR is from a branch that doesn't have check-spelling, +please merge to your PR's base branch to get the version configured for your repository. diff --git a/.github/actions/spelling/allow.txt b/.github/actions/spelling/allow.txt new file mode 100644 index 0000000..1da9e99 --- /dev/null +++ b/.github/actions/spelling/allow.txt @@ -0,0 +1,1848 @@ +aadd +Aadila +AALR +aaxis +ABG +Abhishek +acall +achat +Achird +AClient +acomplete +adamw +ADAS +adk +ADK +ADMA +admins +adr +aexecute +aextract +afrom +Afshaan +AFX +agains +agentcard +agentic +agg +AGG +aggfunc +ainit +ainvoke +aio +aip +AIP +AIphone +airbnb +airlume +aistudio +akka +Aktu +ALCS +ALDS +Alessia +algo +Alibaba +alisal +alloydb +Alok +ALPHAFOLD +ambiently +AMNOSH +amurensis +Anant +antiword +ANZ +AOT +apac +APAC +Aphone +APIENTRY +apikeys +apixaban +apk +appuser +appyaml +apredict +APSTUDIO +apy +APY +aquery +arange +Arborio +Arepa +aretrieve +argmax +arguana +arize +Arize +Arjen +Arrietty +Arsan +Artsakh +arun +arxiv +asdf +ASF +ASGI +Ashish +ASML +Aspeed +astype +atleast +atms +Atulan +auc +aujourd'hui +authtoken +autocal +autoflake +autogen +automerge +automl +autoptr +autopush +autorater +Autorater +autosxs +AUVs +avec +Avenir +aworkflow +axs +axvline +Ayra +Ayyappa +backstory +backticks +bagchi +Bakkali +bargap +barmode +barpolar +BARSAC +baxis +bayes +bbc +bbq +beir +Bengaluru +BENGALURU +bennett +Benno +bert +Bettes +betulus +Beyonce +bff +BFirst +bgr +BGR +bgswap +Bhagwat +Bharath +Biden +bigframes +bigquery +Bigtable +BIKBEAR +bikeshare +Bikeshare +biloba +BITCODE +bitcoin +Bitcoin +blogs +BLR +bluesky +bml +bnb +booktitle +boop +Borregas +Borussia +Bottas +boundings +boxplot +Boyz +bpa +bpd +bpm +Bqbl +bqdf +bqds +bqml +breakroom +briest +Broadoak +Brusdar +Brynner +btn +Btn +Buckleys +Bulmus +byoc +byod +BYOM +byor +BYOR +Byzance +cais +CALIPSO +campestre +CAMQAA +Canlaws +Caprese +carbonara +Carcante +cardio +Cardio +Cardizem +Casagrand +cashify +CASP +catus +caudatus +caxis +cbp +ccc +cce +cctv +CDIP +cdiv +CDNs +ceb +Ceccotti +Centros +ceo +cer +cfbundle +chatbots +CHECKOV +CHENNAI +chipset +chipsets +Chocolat +chromadb +Chrono +Chunyuan +cicd +cimg +ckpts +CLA +CLASSPATH +claude +clearbox +clf +clickable +CLIs +cloudrun +cloudveil +clsx +cmap +cmn +cnf +COCOAPODS +codebase +codebases +codefile +codehaus +codelab +codelabs +codenamed +coderag +codespell +Codicote +COINIT +colab +coldline +Coleoidea +Colm +coloraxis +colorbar +colorway +colurna +colvis +colwidth +CĂłmo +Connectez +CONOUT +constexpr +convo +convos +coolwarm +COPD +coq +cordata +coro +corpuses +COUNTIF +countplot +covs +Cowabunga +cpb +cpet +crewai +CRF +cristo +crm +CRM +Crossbody +crosstab +csa +cse +ctd +CTHH +CUAD +CUCUONAR +cupertino +curating +cusagt +cva +CVD +CWLEY +cyberpunk +cycleway +cycleways +cygpath +CZE +czf +D'orsay +Daniil +Darabont +darkgrid +dask +DataFrame +datname +dbadmin +dbln +dcg +dcl +DCR +ddbb +ddl +decord +dedup +Dedup +deepeval +DeepEval +deepseek +Demis +Demme +demouser +dente +Depatmint +Derpanis +descgen +deskmates +devkey +Dexin +dflags +dfs +DHH +diltiazem +dino +dioicus +direnv +distichum +Disturbia +diy +DKA +dlat +DLC +DLCs +dlon +dlp +Doaa +docai +DOCDB +docstore +docstores +doi +Donen +Doogler +dotprompt +Downscale +dpi +dpo +DPO +DQN +draig +Dreesen +drilldown +drinkware +dropbtn +dropdown +dropna +DRV +dshm +dsl +dsn +DSQ +dto +DTOs +dtype +dtypes +Duh +dumfries +dumpd +dunia +Duonebs +Durafast +Durmus +dvd +DVDs +dwmapi +DWMWA +Dzor +ecommerce +EDB +edgecolor +edgelist +edu +Edu +EDU +Edunov +eee +effi +EHR +EHsc +EIC +ein +Einhalt +EIP +ekg +Eleuther +elinks +Eliquis +Eliud +elous +ema +emb +embs +Embs +embvs +EMEA +EMFU +EMNLP +emojis +ename +encanta +endblock +endlocal +engi +ENU +Envane +envrc +envsubst +Enzo +epath +epoc +Eragon +Erinome +erlang +errlog +erty +ESG +Esin +ESPP +ESRB +etf +ETFs +etils +euchlora +eula +eur +evals +evalset +Evalset +evalsets +Evalsets +eventarc +Eventarc +EVs +evse +evt +expl +faa +facecolor +Fairall +faiss +FAISS +Fangming +FAQs +fastapi +fastmcp +Fastmcp +fastrtc +fda +fea +fect +Feitian +fewshot +ffi +ffprobe +FHIR +FIA +fibonacci +figheight +figsize +figwidth +FILEFLAGS +FILEOS +filestem +fillmode +fillna +Finvest +fiqa +firestore +firewalls +Fishburne +FITNESSS +fixmycar +flac +Flahs +Flatform +Flipkart +Flirble +Flirbles +floormat +FLX +fmeasure +FMWK +Folmer +fontane +fontdict +footwell +forbes +forno +fos +FPDF +FREDDIE +freedraw +freopen +Friedkin +Frodo +fromarray +frombytes +fromiter +frontline +Frontline +FRONTLINE +fsspec +FTPS +fts +Fudd +fulltext +fullurl +functiona +Fushimi +futur +Gabeira +Gacrux +gaeta +gameplay +Gameplay +gapic +Gatace +gauff +gbif +gboolean +gbq +gca +gce +gchar +gcloud +gcp +gcs +gcsfs +gcurl +gdk +gdkx +GDM +gdp +geboten +gemhall +genai +genkit +genmedia +GenTwo +genwealth +geocoded +GEOGPOINT +Geralt +GError +getconn +getdata +getexif +getparent +getpid +geval +GFD +gfile +GFile +Gfm +gguf +giallo +Giallo +Giancarlo +gidiyor +giftable +ginnala +Gisting +github +gitingest +gitlab +gitleaks +gke +GKE +Gleyber +glowin +gmaps +Gmb +GmbH +gms +gmtime +GObject +Godzilla +goldmine +Gombe +GOODALL +googleapi +googlecar +googler +goooooood +gozo +gpg +gpp +gpt +gpu +Grachev +gradio +gradlew +graphrag +Graterol +gridcolor +gridless +gridpoint +gridspec +groupadd +groupchat +grpcio +gshoe +gsm +GSM +gspread +gsutil +gtk +guanciale +Gundogan +gunicorn +gutenberg +Guterres +GWLP +hacer +hadolint +hahaha +Haichao +halluc +Hamamoto +Hamers +Hannaneh +Harvick +hashtag +hashtags +Haumea +Hawkins'll +Hayao +Hbf +HBox +HDFC +hdfs +HDFS +hdlr +hdone +hdtv +heatmap +heatmapgl +Herzing +hexsha +HHH +HHT +Hida +HIDPI +HIGHCPU +Hikaru +Hiromasa +Hisaki +historien +histplot +Hlc +Hmmm +HMO +hnsw +Hogwarts +Homesley +Hoodi +Horamavu +Hossein +hotpotqa +hovermode +hparams +HREDRAW +Hritik +HSA +HTH +HTMX +HTT +HTTH +https +httpx +Hubmann +HVDC +hwnd +hypercorn +Hyperdisk +HYSA +HZN +iap +iban +icd +ICDAR +ICICI +ICML +icudtl +idcg +idk +idks +idxs +IEA +ience +ifconfig +IFI +ifidx +iinfo +IIT +ILB +Ilkay +iloc +imagefont +imageno +imagetext +imdb +imencode +Img +importmap +imread +imshow +imwrite +inbox +indica +inermis +INFOPLIST +informati +inpaint +inpainted +inr +INR +IOMGR +ionate +Iosif +iostream +ipd +iphoneos +ipycanvas +ipykernel +ipynb +ipywebrtc +IRAs +isa +itable +itables +ITDMs +iterrows +itl +ITL +ivf +IVF +ivfflat +ixed +ized +J'aime +Jaival +japonicum +javac +JAVACMD +Jax +jdbc +JDK +jdoe +Jedi +Jeeg +jegadesh +jesieni +Jestem +jetbrains +Jhamtani +JHome +Jiacheng +Jianfeng +Jibo +Jincheng +Jincheol +jit +jiwer +Joji +Jolyon +Jostar +JPY +jsemer +jsonable +jsonify +jsonlines +jsonpath +jsonplus +Junzhe +jupyter +jvm +Kaelen +kaggle +Kaku +Kalai +Kalamang +kamada +Kamradt +Kanagawa +Katsura +Kaufmanns +Kawagoe +kawai +kde +Keanu +kendall +kenleejr +kephal +keras +Ketamine +keychain +KFBI +kfp +Khanh +khz +kickstart +Kipchoge +Kleiser +kmeans +KMEANS +knative +KNN +kok +Kongre +konnte +kotlin +Koyel +kpi +kpis +KPIs +Kraizt +KSA +kubectl +Kubeflow +Kvyat +Kwanzan +kyc +KYC +Kylian +L'avenir +l'harmonie +labelleft +labetalol +Ladhak +LAION +lakecolor +LANCZOS +landcolor +landuse +langgraph +Laomedeia +latte +lbl +LCEL +LEBRON +lecie +lego +lenght +lenzing +Levaquin +levelized +levelname +Leventer +lexer +lexers +liberties +lic +linalg +linecolor +linestyle +linkedin +linted +linting +litellm +livekit +LIVEKIT +Livewire +LLa +Llion +llm +llms +LLMs +LMPA +Lng +loftq +loghub +Loghub +logparser +logprobs +Logrus +lolcat +LOMs +Longhui +LOOKBACK +lookml +Lopyrev +loras +Lottry +louvain +lparam +LRESULT +lro +LROs +lru +lsb +LSTATUS +LSTM +LSum +LTRB +Lumet +Lumi +LUVBPTK +lxml +lycra +lyria +Maa +Maackia +Maarten +maas +macd +MACD +magicoder +magika +Mahindra +mahut +mai +Makemake +makeover +Malware +Mamah +Mandiri +Manh +manquez +Manyata +mapbox +marp +Masaru +maskmode +mathbf +mathvista +mavenrc +maxcold +Mbappe +mbp +mbsdk +mcp +MCP +MCQ +mdc +MDD +Meawad +mec +MEDIANCUT +mediapy +Mega +Megarobot +Meinl +Meirelles +Meltem +meme +Memegen +memes +Merhaba +mesop +Mesop +metadatas +metamath +METAMATH +metaverse +Meteo +metricx +Mewgler +MFU +mgrs +Mhatre +mic +MICOA +mics +Milito +millis +Mirai +miranda +Mitani +MLB +MLOPs +mlp +MMA +mmarco +mmlu +MMLU +MMO +mmol +mmr +Mnih +modelfile +Molaison +monty +Mookie +morty +Mosi +moviepy +mpb +mpe +mpegps +mpga +mpld +mpn +mrag +mrr +mrtydi +MSCHF +MSGSEND +msmarco +msr +MTL +multitool +mundo +Mvar +mvn +mvnw +mvp +MWh +mwouts +myaccount +mydb +mydomain +NADPH +Nagar +Nagasu +Najim +Nakoso +nanswer +Napisz +NARI +Narnia +nas +Navneet +nbconvert +nbfmt +nbformat +nbqa +nbviewer +NCCREATE +nce +ncols +nconst +ndarray +ndcg +NDCG +NDEBUG +nec +neering +nemri +neq +netif +newaxis +newaxisngram +nfcorpus +nfl +nginx +ngram +NGRAM +ngrams +NGRAMS +NHK +NICOLAS +nih +Nihonga +Niitsuma +nin +Nith +nlp +nmade +nmilitary +NMT +nmy +noabe +nobserved +nodularis +nohup +nokey +Nominatim +NOMINATIM +NOMINMAX +nonroot +norigin +notetaker +novnc +noxfile +NOZORDER +NPCs +NPO +nprint +npx +NREL +nrows +ntheory +nunique +nvidia +NVIDIA +NVL +NYT +oai +objc +ocr +ODb +oglevel +Ohashi +ohlcv +OHLCV +Ohtani +Okame +OlĂĄ +OLAP +oldboy +Oldboy +Olgivanna +ollama +Ollama +olleh +omn +Omnibox +Omnicase +OMR +onesie +onesies +Onone +OOD +OOM +Oort +OOTB +openai +openfda +opsz +Orbelians +Orus +osm +oss +osx +otlp +outdir +outro +overwiew +owlbot +oxml +padus +Pageable +pagemap +Pakeman +paleo +paligemma +palustris +Palya +pancetta +pandoc +pantarba +Paquete +paracord +Parag +parcoords +parfaite +parseable +passwort +pastiched +Pastra +payslip +paystub +PBA +pbar +pbxproj +pcs +PDEs +pdflatef +pdfminer +pdfs +PDFs +PEFT +PEGI +Pengyu +Persero +petabytes +pfas +pgadmin +PGHOST +PGPORT +PGUSER +pgvector +Phaidon +Phanfone +pharma +Pharma +phellos +Photoshop +Pichai +pietra +pii +pilates +Pilates +pincodes +Pistorius +pixmap +pkl +playlists +PLLu +pllum +plotly +PLOTLYENV +plpgsql +plt +pmb +podcast +podcasts +podes +podfile +podhelper +podman +Poirot +Polong +ponyo +Ponyo +Popken +popleft +Porat +postictal +powerups +PPO +prag +Pranav +praw +prcntg +PRD +PrĂ© +preds +pregel +produc +programar +projectid +proname +protobuf +pstotext +pth +PTx +pubmed +pubspec +Puram +putalpha +putdata +pvc +pyautogen +pyav +pybind +pydantic +pydrive +pydub +PYINK +pymupdf +pypdf +pyplot +pysftp +pytorch +pytrec +pyupgrade +pyvis +Pzk +Qantas +qas +QEDC +QLo +qna +QPM +qqq +qrel +qrels +quadrotor +qubit +qubits +quickbot +Quickbot +quicktime +quieres +quippy +qwen +Qwen +Qwiklabs +Rabbids +Raducanu +rag +ragas +RAGAS +ragdemos +rahhhrr +rai +Raikkonen +Rajpurkar +Ramblas +ramen +Ranurte +rapideval +raquin +rarian +rarians +ratelimit +rawbytes +rbc +rbf +rbm +recontext +Recontext +recrawl +reddit +redef +reidx +remai +rembg +reoffice +rephraser +REPOURL +repreve +requestz +reranked +reranker +reranking +Reranking +reranks +resil +Resona +retryable +ribeye +ricc +ringspun +Rivertown +Rivia +Rizzoli +RLHF +RMSE +RNN +RNNs +Robeco +Robo +Robotica +rooney +ROOTSPAN +Rosberg +ROUVs +rpet +rrf +RRF +rsc +rsi +RSI +rslora +rsp +RTN +rubra +rubrum +Ruchika +runjdwp +RUnlock +RUNPATH +Rupjit +RVL +RVR +RYDE +saaagesh +Sahel +SAOT +sargentii +Sasquatch +Sauron +saveddir +Sca +scann +scattergl +Schlenoff +Schulen +scidocs +scifact +scifi +scikit +Scorsese +SDKROOT +seabut +sealife +seatback +SEK +Selam +selectbox +selfie +selftext +selkirk +SEMCONV +seo +SEO +serde +serper +SERPER +serrata +serrulata +Sestero +setlocal +sft +Sha'carri +shalas +Shankman +Shaoxiong +Shar +sharegpt +Sharmila +Shawshank +Shazeer +SHEBA +shedd +Shenzhou +Shibuya +Shinjuku +Shklovsky +Shohei +shortdesc +showarrow +showlakes +showland +showor +showtime +showtimes +shrt +Shubham +Shyamalan +sid +sids +siglap +signings +simage +SIMONE +Simpsons +sinc +Singin +sittin +Skaffold +Sketchfab +skillsets +sklearn +sku +SKUs +slf +Smartbuy +Smaug +Smol +smolagent +SMSN +SNB +SNE +snowboard +socketio +Sono +sortant +sourced +sourcing +spamming +spearmanr +SPII +SPLADE +sqkm +SRE +srlimit +ssd +sse +ssh +ssml +ssn +ssr +SSRF +sss +SSZ +Statcast +stdcall +stext +STIX +Storrer +Stoynov +stp +Strappy +strdupv +streamlit +strfreev +stt +stuffie +subclip +SUBLANG +subviews +subword +suis +sunner +supima +suptitle +Surampudi +survei +Suzaka +sva +SVA +svh +svm +Sweetgum +SWP +sxs +sylvatica +SYMED +SYSROOT +Syunik +tabular +Tadao +Tafel +tagline +takeaways +Takeaways +TARG +taskfile +Taskfile +tastetest +tavily +Tbk +Tbl +TBY +TCEHY +telework +tema +tencel +Tencent +terabyte +Terabyte +terabytes +Terabytes +termcolor +terraform +Testables +Tetsuo +texting +textno +tfhub +tfidf +Tful +tfvars +tgi +TGI +tgz +Thallam +thelook +THH +THHH +thi +throug +THT +THTH +Tianli +Tianxiang +ticktext +tickvals +tiktoken +timechart +timecoded +timecodes +tion +titlebar +TLDR +tobytes +tok +TOKENLIST +tomentosa +Tongsheng +toolcall +toolsets +toolu +topbar +Topolino +toself +toset +tpot +TPOT +TPU +TPUs +tqdm +traceloop +Traceloop +treeah +Tribbiani +Tricyle +trimpath +tritan +TRK +trl +truncatum +Trung +tscore +tseslint +TSLA +TSMC +tsne +TSNE +tsv +ttext +ttfb +TTFB +ttft +TTFT +TTH +tts +TTT +tures +Tuteja +tval +twiml +typehints +ubuntu +udf +UDF +UDFs +Ufkes +uids +UIs +Ukiyo +ulaw +ulmoides +ultrawide +und +undst +unigram +Unimicron +unmarshal +unrtf +unsloth +Unsloth +upsamples +upsell +Upserting +urandom +Urs +Usain +usb +usd +usebackq +usecases +USERDATA +username +usernames +USK +Uszkoreit +Utik +uuids +UUV +uvb +uvicorn +uvx +UVX +vais +Valtteri +Vandamm +Vandoorne +vapo +VAPO +Vardhan +Vaswani +Vayots +VBG +vcodec +VCPU +vdb +vectoral +vectordb +veo +Verbinski +Verilog +versioned +Ves +Vesia +vesselin +Vettel +vframes +VFT +vibeo +vid +Vid +vidgen +VIDGEN +Vijay +Virat +viridis +Viru +Visualise +vllm +VLM +VLMs +VMAF +VMs +vnc +voiceover +Volodymyr +vos +VOS +votre +VQA +VRAM +VREDRAW +vrtx +VSC +vscode +VSJ +VSPM +vtotal +vtt +vulnz +Wafae +wafaeb +WAI +Wakatipu +waterjet +WAU +wcontext +wcslen +wdir +WDIR +weaviate +webcam +webclient +webfonts +webpage +webpages +webrtc +websites +WEBVTT +Wehn +Wehrlein +weightage +Weiyang +welcom +Welwyn +werden +wereld +werther +WFH +wght +whitegrid +wiersz +WIF +wiffle +wikipedia +wil +windspeed +winres +wip +wishlist +Witaj +Wnd +WNDCLASS +womens +Womens +wook +workarounds +worklets +wparam +WRatio +wscat +wscore +wscores +wstring +wurden +WXGA +wxyz +xanchor +xaxes +xaxis +xcassets +xcconfig +xcodeproj +xcol +xcscheme +xctest +xcvi +xdg +xelatex +XGA +xlabel +xlim +xmltodict +xsi +xsum +XSum +xtick +xticks +XXE +xxxxxxxx +xycoords +xytext +xzn +YAjjpd +yanchor +yaxes +yaxis +ycol +ydf +yedoensis +Ykb +ylabel +ylim +YMid +Yoshino +yougov +youtube +Youxi +yref +ytd +ytick +yticks +Yuxi +Yuxuan +Yuzuru +zakarid +Zakarids +zaxis +Zemeckis +zfq +Zhaohua +Zhenguo +Zhengying +Zijin +zimie +Zinnemann +Zizu +zmin +zola +Zom +Zosyn +Zscaler +Zuv diff --git a/.github/actions/spelling/block-delimiters.list b/.github/actions/spelling/block-delimiters.list new file mode 100644 index 0000000..4f21922 --- /dev/null +++ b/.github/actions/spelling/block-delimiters.list @@ -0,0 +1,15 @@ +# Public Keys +-----BEGIN PUBLIC KEY----- +-----END PUBLIC KEY----- + +# RSA Private Key +-----BEGIN RSA PRIVATE KEY----- +-----END RSA PRIVATE KEY----- + +# GPG Public Key +-----BEGIN PGP PUBLIC KEY BLOCK----- +-----END PGP PUBLIC KEY BLOCK----- + +# Certificates +-----BEGIN CERTIFICATE----- +-----END CERTIFICATE----- diff --git a/.github/actions/spelling/candidate.patterns b/.github/actions/spelling/candidate.patterns new file mode 100644 index 0000000..827c7ea --- /dev/null +++ b/.github/actions/spelling/candidate.patterns @@ -0,0 +1,675 @@ +# marker to ignore all code on line +^.*/\* #no-spell-check-line \*/.*$ +# marker to ignore all code on line +^.*\bno-spell-check(?:-line|)(?:\s.*|)$ + +# https://cspell.org/configuration/document-settings/ +# cspell inline +^.*\b[Cc][Ss][Pp][Ee][Ll]{2}:\s*[Dd][Ii][Ss][Aa][Bb][Ll][Ee]-[Ll][Ii][Nn][Ee]\b + +# patch hunk comments +^@@ -\d+(?:,\d+|) \+\d+(?:,\d+|) @@ .* +# git index header +index (?:[0-9a-z]{7,40},|)[0-9a-z]{7,40}\.\.[0-9a-z]{7,40} + +# file permissions +['"`\s][-bcdLlpsw](?:[-r][-w][-Ssx]){2}[-r][-w][-SsTtx]\+?['"`\s] + +# css url wrappings +\burl\([^)]+\) + +# cid urls +(['"])cid:.*?\g{-1} + +# data url in parens +\(data:(?:[^) ][^)]*?|)(?:[A-Z]{3,}|[A-Z][a-z]{2,}|[a-z]{3,})[^)]*\) +# data url in quotes +([`'"])data:(?:[^ `'"].*?|)(?:[A-Z]{3,}|[A-Z][a-z]{2,}|[a-z]{3,}).*\g{-1} +# data url +\bdata:[-a-zA-Z=;:/0-9+]*,\S* + +# https/http/file urls +(?:\b(?:https?|ftp|file)://)[-A-Za-z0-9+&@#/%?=~_|!:,.;]+[-A-Za-z0-9+&@#/%=~_|] + +# mailto urls +mailto:[-a-zA-Z=;:/?%&0-9+@._]{3,} + +# magnet urls +magnet:[?=:\w]+ + +# magnet urls +"magnet:[^"]+" + +# obs: +"obs:[^"]*" + +# The `\b` here means a break, it's the fancy way to handle urls, but it makes things harder to read +# In this examples content, I'm using a number of different ways to match things to show various approaches +# asciinema +\basciinema\.org/a/[0-9a-zA-Z]+ + +# asciinema v2 +^\[\d+\.\d+, "[io]", ".*"\]$ + +# apple +\bdeveloper\.apple\.com/[-\w?=/]+ +# Apple music +\bembed\.music\.apple\.com/fr/playlist/usr-share/[-\w.]+ + +# appveyor api +\bci\.appveyor\.com/api/projects/status/[0-9a-z]+ +# appveyor project +\bci\.appveyor\.com/project/(?:[^/\s"]*/){2}builds?/\d+/job/[0-9a-z]+ + +# Amazon + +# Amazon +\bamazon\.com/[-\w]+/(?:dp/[0-9A-Z]+|) +# AWS S3 +\b\w*\.s3[^.]*\.amazonaws\.com/[-\w/&#%_?:=]* +# AWS execute-api +\b[0-9a-z]{10}\.execute-api\.[-0-9a-z]+\.amazonaws\.com\b +# AWS ELB +\b\w+\.[-0-9a-z]+\.elb\.amazonaws\.com\b +# AWS SNS +\bsns\.[-0-9a-z]+.amazonaws\.com/[-\w/&#%_?:=]* +# AWS VPC +vpc-\w+ + +# While you could try to match `http://` and `https://` by using `s?` in `https?://`, sometimes there +# YouTube url +\b(?:(?:www\.|)youtube\.com|youtu.be)/(?:channel/|embed/|user/|playlist\?list=|watch\?v=|v/|)[-a-zA-Z0-9?&=_%]* +# YouTube music +\bmusic\.youtube\.com/youtubei/v1/browse(?:[?&]\w+=[-a-zA-Z0-9?&=_]*) +# YouTube tag +<\s*youtube\s+id=['"][-a-zA-Z0-9?_]*['"] +# YouTube image +\bimg\.youtube\.com/vi/[-a-zA-Z0-9?&=_]* +# Google Accounts +\baccounts.google.com/[-_/?=.:;+%&0-9a-zA-Z]* +# Google Analytics +\bgoogle-analytics\.com/collect.[-0-9a-zA-Z?%=&_.~]* +# Google APIs +\bgoogleapis\.(?:com|dev)/[a-z]+/(?:v\d+/|)[a-z]+/[-@:./?=\w+|&]+ +# Google Storage +\b[-a-zA-Z0-9.]*\bstorage\d*\.googleapis\.com(?:/\S*|) +# Google Calendar +\bcalendar\.google\.com/calendar(?:/u/\d+|)/embed\?src=[@./?=\w&%]+ +\w+\@group\.calendar\.google\.com\b +# Google DataStudio +\bdatastudio\.google\.com/(?:(?:c/|)u/\d+/|)(?:embed/|)(?:open|reporting|datasources|s)/[-0-9a-zA-Z]+(?:/page/[-0-9a-zA-Z]+|) +# The leading `/` here is as opposed to the `\b` above +# ... a short way to match `https://` or `http://` since most urls have one of those prefixes +# Google Docs +/docs\.google\.com/[a-z]+/(?:ccc\?key=\w+|(?:u/\d+|d/(?:e/|)[0-9a-zA-Z_-]+/)?(?:edit\?[-\w=#.]*|/\?[\w=&]*|)) +# Google Drive +\bdrive\.google\.com/(?:file/d/|open)[-0-9a-zA-Z_?=]* +# Google Groups +\bgroups\.google\.com(?:/[a-z]+/(?:#!|)[^/\s"]+)* +# Google Maps +\bmaps\.google\.com/maps\?[\w&;=]* +# Google themes +themes\.googleusercontent\.com/static/fonts/[^/\s"]+/v\d+/[^.]+. +# Google CDN +\bclients2\.google(?:usercontent|)\.com[-0-9a-zA-Z/.]* +# Goo.gl +/goo\.gl/[a-zA-Z0-9]+ +# Google Chrome Store +\bchrome\.google\.com/webstore/detail/[-\w]*(?:/\w*|) +# Google Books +\bgoogle\.(?:\w{2,4})/books(?:/\w+)*\?[-\w\d=&#.]* +# Google Fonts +\bfonts\.(?:googleapis|gstatic)\.com/[-/?=:;+&0-9a-zA-Z]* +# Google Forms +\bforms\.gle/\w+ +# Google Scholar +\bscholar\.google\.com/citations\?user=[A-Za-z0-9_]+ +# Google Colab Research Drive +\bcolab\.research\.google\.com/drive/[-0-9a-zA-Z_?=]* + +# GitHub SHAs (api) +\bapi.github\.com/repos(?:/[^/\s"]+){3}/[0-9a-f]+\b +# GitHub SHAs (markdown) +(?:\[`?[0-9a-f]+`?\]\(https:/|)/(?:www\.|)github\.com(?:/[^/\s"]+){2,}(?:/[^/\s")]+)(?:[0-9a-f]+(?:[-0-9a-zA-Z/#.]*|)\b|) +# GitHub SHAs +\bgithub\.com(?:/[^/\s"]+){2}[@#][0-9a-f]+\b +# GitHub SHA refs +\[([0-9a-f]+)\]\(https://(?:www\.|)github.com/[-\w]+/[-\w]+/commit/\g{-1}[0-9a-f]* +# GitHub wiki +\bgithub\.com/(?:[^/]+/){2}wiki/(?:(?:[^/]+/|)_history|[^/]+(?:/_compare|)/[0-9a-f.]{40,})\b +# githubusercontent +/[-a-z0-9]+\.githubusercontent\.com/[-a-zA-Z0-9?&=_\/.]* +# githubassets +\bgithubassets.com/[0-9a-f]+(?:[-/\w.]+) +# gist github +\bgist\.github\.com/[^/\s"]+/[0-9a-f]+ +# git.io +\bgit\.io/[0-9a-zA-Z]+ +# GitHub JSON +"node_id": "[-a-zA-Z=;:/0-9+_]*" +# Contributor +\[[^\]]+\]\(https://github\.com/[^/\s"]+/?\) +# GHSA +GHSA(?:-[0-9a-z]{4}){3} + +# GitHub actions +\buses:\s+[-\w.]+/[-\w./]+@[-\w.]+ + +# GitLab commit +\bgitlab\.[^/\s"]*/\S+/\S+/commit/[0-9a-f]{7,16}#[0-9a-f]{40}\b +# GitLab merge requests +\bgitlab\.[^/\s"]*/\S+/\S+/-/merge_requests/\d+/diffs#[0-9a-f]{40}\b +# GitLab uploads +\bgitlab\.[^/\s"]*/uploads/[-a-zA-Z=;:/0-9+]* +# GitLab commits +\bgitlab\.[^/\s"]*/(?:[^/\s"]+/){2}commits?/[0-9a-f]+\b + +# binance +accounts\.binance\.com/[a-z/]*oauth/authorize\?[-0-9a-zA-Z&%]* + +# bitbucket diff +\bapi\.bitbucket\.org/\d+\.\d+/repositories/(?:[^/\s"]+/){2}diff(?:stat|)(?:/[^/\s"]+){2}:[0-9a-f]+ +# bitbucket repositories commits +\bapi\.bitbucket\.org/\d+\.\d+/repositories/(?:[^/\s"]+/){2}commits?/[0-9a-f]+ +# bitbucket commits +\bbitbucket\.org/(?:[^/\s"]+/){2}commits?/[0-9a-f]+ + +# bit.ly +\bbit\.ly/\w+ + +# bitrise +\bapp\.bitrise\.io/app/[0-9a-f]*/[\w.?=&]* + +# bootstrapcdn.com +\bbootstrapcdn\.com/[-./\w]+ + +# cdn.cloudflare.com +\bcdnjs\.cloudflare\.com/[./\w]+ + +# circleci +\bcircleci\.com/gh(?:/[^/\s"]+){1,5}.[a-z]+\?[-0-9a-zA-Z=&]+ + +# gitter +\bgitter\.im(?:/[^/\s"]+){2}\?at=[0-9a-f]+ + +# gravatar +\bgravatar\.com/avatar/[0-9a-f]+ + +# ibm +[a-z.]*ibm\.com/[-_#=:%!?~.\\/\d\w]* + +# imgur +\bimgur\.com/[^.]+ + +# Internet Archive +\barchive\.org/web/\d+/(?:[-\w.?,'/\\+&%$#_:]*) + +# discord +/discord(?:app\.com|\.gg)/(?:invite/)?[a-zA-Z0-9]{7,} + +# Disqus +\bdisqus\.com/[-\w/%.()!?&=_]* + +# medium link +\blink\.medium\.com/[a-zA-Z0-9]+ +# medium +\bmedium\.com/@?[^/\s"]+/[-\w]+ + +# microsoft +\b(?:https?://|)(?:(?:download\.visualstudio|docs|msdn2?|research)\.microsoft|blogs\.msdn)\.com/[-_a-zA-Z0-9()=./%]* +# powerbi +\bapp\.powerbi\.com/reportEmbed/[^"' ]* +# vs devops +\bvisualstudio.com(?::443|)/[-\w/?=%&.]* +# microsoft store +\bmicrosoft\.com/store/apps/\w+ + +# mvnrepository.com +\bmvnrepository\.com/[-0-9a-z./]+ + +# now.sh +/[0-9a-z-.]+\.now\.sh\b + +# oracle +\bdocs\.oracle\.com/[-0-9a-zA-Z./_?#&=]* + +# chromatic.com +/\S+.chromatic.com\S*[")] + +# codacy +\bapi\.codacy\.com/project/badge/Grade/[0-9a-f]+ + +# compai +\bcompai\.pub/v1/png/[0-9a-f]+ + +# mailgun api +\.api\.mailgun\.net/v3/domains/[0-9a-z]+\.mailgun.org/messages/[0-9a-zA-Z=@]* +# mailgun +\b[0-9a-z]+.mailgun.org + +# /message-id/ +/message-id/[-\w@./%]+ + +# Reddit +\breddit\.com/r/[/\w_]* + +# requestb.in +\brequestb\.in/[0-9a-z]+ + +# sched +\b[a-z0-9]+\.sched\.com\b + +# Slack url +slack://[a-zA-Z0-9?&=]+ +# Slack +\bslack\.com/[-0-9a-zA-Z/_~?&=.]* +# Slack edge +\bslack-edge\.com/[-a-zA-Z0-9?&=%./]+ +# Slack images +\bslack-imgs\.com/[-a-zA-Z0-9?&=%.]+ + +# shields.io +\bshields\.io/[-\w/%?=&.:+;,]* + +# stackexchange -- https://stackexchange.com/feeds/sites +\b(?:askubuntu|serverfault|stack(?:exchange|overflow)|superuser).com/(?:questions/\w+/[-\w]+|a/) + +# Sentry +[0-9a-f]{32}\@o\d+\.ingest\.sentry\.io\b + +# Twitter markdown +\[@[^[/\]:]*?\]\(https://twitter.com/[^/\s"')]*(?:/status/\d+(?:\?[-_0-9a-zA-Z&=]*|)|)\) +# Twitter hashtag +\btwitter\.com/hashtag/[\w?_=&]* +# Twitter status +\btwitter\.com/[^/\s"')]*(?:/status/\d+(?:\?[-_0-9a-zA-Z&=]*|)|) +# Twitter profile images +\btwimg\.com/profile_images/[_\w./]* +# Twitter media +\btwimg\.com/media/[-_\w./?=]* +# Twitter link shortened +\bt\.co/\w+ + +# facebook +\bfburl\.com/[0-9a-z_]+ +# facebook CDN +\bfbcdn\.net/[\w/.,]* +# facebook watch +\bfb\.watch/[0-9A-Za-z]+ + +# dropbox +\bdropbox\.com/sh?/[^/\s"]+/[-0-9A-Za-z_.%?=&;]+ + +# ipfs protocol +ipfs://[0-9a-zA-Z]{3,} +# ipfs url +/ipfs/[0-9a-zA-Z]{3,} + +# w3 +\bw3\.org/[-0-9a-zA-Z/#.]+ + +# loom +\bloom\.com/embed/[0-9a-f]+ + +# regex101 +\bregex101\.com/r/[^/\s"]+/\d+ + +# figma +\bfigma\.com/file(?:/[0-9a-zA-Z]+/)+ + +# freecodecamp.org +\bfreecodecamp\.org/[-\w/.]+ + +# image.tmdb.org +\bimage\.tmdb\.org/[/\w.]+ + +# mermaid +\bmermaid\.ink/img/[-\w]+|\bmermaid-js\.github\.io/mermaid-live-editor/#/edit/[-\w]+ + +# Wikipedia +\ben\.wikipedia\.org/wiki/[-\w%.#]+ + +# gitweb +[^"\s]+/gitweb/\S+;h=[0-9a-f]+ + +# HyperKitty lists +/archives/list/[^@/]+@[^/\s"]*/message/[^/\s"]*/ + +# lists +/thread\.html/[^"\s]+ + +# list-management +\blist-manage\.com/subscribe(?:[?&](?:u|id)=[0-9a-f]+)+ + +# kubectl.kubernetes.io/last-applied-configuration +"kubectl.kubernetes.io/last-applied-configuration": ".*" + +# pgp +\bgnupg\.net/pks/lookup[?&=0-9a-zA-Z]* + +# Spotify +\bopen\.spotify\.com/embed/playlist/\w+ + +# Mastodon +\bmastodon\.[-a-z.]*/(?:media/|@)[?&=0-9a-zA-Z_]* + +# scastie +\bscastie\.scala-lang\.org/[^/]+/\w+ + +# images.unsplash.com +\bimages\.unsplash\.com/(?:(?:flagged|reserve)/|)[-\w./%?=%&.;]+ + +# pastebin +\bpastebin\.com/[\w/]+ + +# heroku +\b\w+\.heroku\.com/source/archive/\w+ + +# quip +\b\w+\.quip\.com/\w+(?:(?:#|/issues/)\w+)? + +# badgen.net +\bbadgen\.net/badge/[^")\]'\s]+ + +# statuspage.io +\w+\.statuspage\.io\b + +# media.giphy.com +\bmedia\.giphy\.com/media/[^/]+/[\w.?&=]+ + +# tinyurl +\btinyurl\.com/\w+ + +# codepen +\bcodepen\.io/[\w/]+ + +# registry.npmjs.org +\bregistry\.npmjs\.org/(?:@[^/"']+/|)[^/"']+/-/[-\w@.]+ + +# getopts +\bgetopts\s+(?:"[^"]+"|'[^']+') + +# ANSI color codes +(?:\\(?:u00|x)1[Bb]|\x1b|\\u\{1[Bb]\})\[\d+(?:;\d+|)m + +# URL escaped characters +%[0-9A-F][A-F](?=[A-Za-z]) +# lower URL escaped characters +%[0-9a-f][a-f](?=[a-z]{2,}) +# IPv6 +\b(?:[0-9a-fA-F]{0,4}:){3,7}[0-9a-fA-F]{0,4}\b +# c99 hex digits (not the full format, just one I've seen) +0x[0-9a-fA-F](?:\.[0-9a-fA-F]*|)[pP] +# Punycode +\bxn--[-0-9a-z]+ +# sha +sha\d+:[0-9]*[a-f]{3,}[0-9a-f]* +# sha-... -- uses a fancy capture +(\\?['"]|")[0-9a-f]{40,}\g{-1} +# hex runs +\b[0-9a-fA-F]{16,}\b +# hex in url queries +=[0-9a-fA-F]*?(?:[A-F]{3,}|[a-f]{3,})[0-9a-fA-F]*?& +# ssh +#(?:ssh-\S+|-nistp256) [-a-zA-Z=;:/0-9+]{12,} + +# PGP +\b(?:[0-9A-F]{4} ){9}[0-9A-F]{4}\b +# GPG keys +\b(?:[0-9A-F]{4} ){5}(?: [0-9A-F]{4}){5}\b +# Well known gpg keys +.well-known/openpgpkey/[\w./]+ + +# pki +-----BEGIN.*-----END + +# uuid: +\b[0-9a-fA-F]{8}-(?:[0-9a-fA-F]{4}-){3}[0-9a-fA-F]{12}\b +# hex digits including css/html color classes: +(?:[\\0][xX]|\\u|[uU]\+|#x?|%23)[0-9_a-fA-FgGrR]*?[a-fA-FgGrR]{2,}[0-9_a-fA-FgGrR]*(?:[uUlL]{0,3}|[iu]\d+)\b +# integrity +integrity=(['"])(?:\s*sha\d+-[-a-zA-Z=;:/0-9+]{40,})+\g{-1} + +# https://www.gnu.org/software/groff/manual/groff.html +# man troff content +\\f[BCIPR] +# '/" +\\\([ad]q + +# .desktop mime types +^MimeTypes?=.*$ +# .desktop localized entries +^[A-Z][a-z]+\[[a-z]+\]=.*$ +# Localized .desktop content +Name\[[^\]]+\]=.* + +# IServiceProvider / isAThing +(?:\b|_)(?:I|isA)(?=(?:[A-Z][a-z]{2,})+(?:[A-Z]|\b)) + +# crypt +(['"])\$2[ayb]\$.{56}\g{-1} + +# scrypt / argon +\$(?:scrypt|argon\d+[di]*)\$\S+ + +# go.sum +\bh1:\S+ + +# scala imports +^import (?:[\w.]|\{\w*?(?:,\s*(?:\w*|\*))+\})+ + +# scala modules +("[^"]+"\s*%%?\s*){2,3}"[^"]+" + +# Intel intrinsics +_mm_\w+ + +# Input to GitHub JSON +content: (['"])[-a-zA-Z=;:/0-9+]*=\g{-1} + +# This does not cover multiline strings, if your repository has them, +# you'll want to remove the `(?=.*?")` suffix. +# The `(?=.*?")` suffix should limit the false positives rate +# printf +#%(?:(?:(?:hh?|ll?|[jzt])?[diuoxn]|l?[cs]|L?[fega]|p)(?=[a-z]{2,})|(?:X|L?[FEGA]|p)(?=[a-zA-Z]{2,}))(?!%)(?=[_a-zA-Z]+(?!%)\b)(?=.*?['"]) + +# Alternative printf +# %s +#%(?:s(?=[a-z]{2,}))(?!%)(?=[_a-zA-Z]+(?!%)\b)(?=.*?['"]) + +# Python string prefix / binary prefix +# Note that there's a high false positive rate, remove the `?=` and search for the regex to see if the matches seem like reasonable strings +(?|m([|!/@#,;']).*?\g{-1}) + +# perl qr regex +(?|\(.*?\)|([|!/@#,;']).*?\g{-1}) + +# perl run +perl(?:\s+-[a-zA-Z]\w*)+ + +# C network byte conversions +#(?:\d|\bh)to(?!ken)(?=[a-z])|to(?=[adhiklpun]\() + +# Go regular expressions +regexp?\.MustCompile\(`[^`]*`\) + +# regex choice +\(\?:[^)]+\|[^)]+\) + +# proto +^\s*(\w+)\s\g{-1} = + +# sed regular expressions +sed 's/(?:[^/]*?[a-zA-Z]{3,}[^/]*?/){2} + +# node packages +(["'])@[^/'" ]+/[^/'" ]+\g{-1} + +# go install +go install(?:\s+[a-z]+\.[-@\w/.]+)+ + +# jetbrains schema https://youtrack.jetbrains.com/issue/RSRP-489571 +urn:shemas-jetbrains-com + +# kubernetes pod status lists +# https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle/#pod-phase +\w+(?:-\w+)+\s+\d+/\d+\s+(?:Running|Pending|Succeeded|Failed|Unknown)\s+ + +# kubectl - pods in CrashLoopBackOff +\w+-[0-9a-f]+-\w+\s+\d+/\d+\s+CrashLoopBackOff\s+ + +# kubernetes object suffix +-[0-9a-f]{10}-\w{5}\s + +# posthog secrets +([`'"])phc_[^"',]+\g{-1} + +# xcode + +# xcodeproject scenes +(?:Controller|destination|ID|id)="\w{3}-\w{2}-\w{3}" + +# xcode api botches +customObjectInstantitationMethod + +# msvc api botches +PrependWithABINamepsace + +# configure flags +.* \| --\w{2,}.*?(?=\w+\s\w+) + +# font awesome classes +\.fa-[-a-z0-9]+ + +# bearer auth +#(['"])[Bb]ear[e][r] .*?\g{-1} + +# bearer auth +\b[Bb]ear[e][r]:? [-a-zA-Z=;:/0-9+.]+ + +# basic auth +(['"])[Bb]asic [-a-zA-Z=;:/0-9+]{3,}\g{-1} + +# encoded-word +=\?[-a-zA-Z0-9"*%]+\?[BQ]\?[^?]{0,75}\?= + +# Time Zones +\b(?:Africa|Atlantic|America|Antarctica|Asia|Australia|Europe|Indian|Pacific)(?:/\w+)+ + +# linux kernel info +^(?:bugs|flags|Features)\s+:.* + +# systemd mode +systemd.*?running in system mode \([-+].*\)$ + +# Lorem +# Update Lorem based on your content (requires `ge` and `w` from https://github.com/jsoref/spelling; and `review` from https://github.com/check-spelling/check-spelling/wiki/Looking-for-items-locally ) +# grep '^[^#].*lorem' .github/actions/spelling/patterns.txt|perl -pne 's/.*i..\?://;s/\).*//' |tr '|' "\n"|sort -f |xargs -n1 ge|perl -pne 's/^[^:]*://'|sort -u|w|sed -e 's/ .*//'|w|review - +# Warning, while `(?i)` is very neat and fancy, if you have some binary files that aren't proper unicode, you might run into: +# ... Operation "substitution (s///)" returns its argument for non-Unicode code point 0x1C19AE (the code point will vary). +# ... You could manually change `(?i)X...` to use `[Xx]...` +# ... or you could add the files to your `excludes` file (a version after 0.0.19 should identify the file path) +(?:(?:\w|\s|[,.])*\b(?i)(?:amet|consectetur|cursus|dolor|eros|ipsum|lacus|libero|ligula|lorem|magna|neque|nulla|suscipit|tempus)\b(?:\w|\s|[,.])*) + +# Non-English +[a-zA-Z]*[Ă€ĂĂ‚ĂƒĂ„Ă…Ă†ÄŒĂ‡ĂˆĂ‰ĂŠĂ‹ĂŒĂĂŽĂĂĂ‘Ă’Ă“Ă”Ă•Ă–Ă˜Ă™ĂšĂ›ĂœĂĂŸĂ ĂĄĂąĂŁĂ€Ă„ĂŠÄĂ§ĂšĂ©ĂȘëÏíßïðñĂČóÎÔöÞĂčĂșĂ»ĂŒĂœĂżÄ€ÄĆĆ‚ĆƒĆ„Ć…Ć†Ć’Ć“ĆšĆ›Ć ĆĄĆœĆĆžĆœĆŸĆș][a-zA-Z]{3}[a-zA-ZĂ€ĂĂ‚ĂƒĂ„Ă…Ă†ÄŒĂ‡ĂˆĂ‰ĂŠĂ‹ĂŒĂĂŽĂĂĂ‘Ă’Ă“Ă”Ă•Ă–Ă˜Ă™ĂšĂ›ĂœĂĂŸĂ ĂĄĂąĂŁĂ€Ă„ĂŠÄĂ§ĂšĂ©ĂȘëÏíßïðñĂČóÎÔöÞĂčĂșĂ»ĂŒĂœĂżÄ€ÄĆĆ‚ĆƒĆ„Ć…Ć†Ć’Ć“ĆšĆ›Ć ĆĄĆœĆĆžĆœĆŸĆș]*|[a-zA-Z]{3,}[Ă€ĂĂ‚ĂƒĂ„Ă…Ă†ÄŒĂ‡ĂˆĂ‰ĂŠĂ‹ĂŒĂĂŽĂĂĂ‘Ă’Ă“Ă”Ă•Ă–Ă˜Ă™ĂšĂ›ĂœĂĂŸĂ ĂĄĂąĂŁĂ€Ă„ĂŠÄĂ§ĂšĂ©ĂȘëÏíßïðñĂČóÎÔöÞĂčĂșĂ»ĂŒĂœĂżÄ€ÄĆĆ‚ĆƒĆ„Ć…Ć†Ć’Ć“ĆšĆ›Ć ĆĄĆœĆĆžĆœĆŸĆș]|[Ă€ĂĂ‚ĂƒĂ„Ă…Ă†ÄŒĂ‡ĂˆĂ‰ĂŠĂ‹ĂŒĂĂŽĂĂĂ‘Ă’Ă“Ă”Ă•Ă–Ă˜Ă™ĂšĂ›ĂœĂĂŸĂ ĂĄĂąĂŁĂ€Ă„ĂŠÄĂ§ĂšĂ©ĂȘëÏíßïðñĂČóÎÔöÞĂčĂșĂ»ĂŒĂœĂżÄ€ÄĆĆ‚ĆƒĆ„Ć…Ć†Ć’Ć“ĆšĆ›Ć ĆĄĆœĆĆžĆœĆŸĆș][a-zA-Z]{3,} + +# highlighted letters +\[[A-Z]\][a-z]+ + +# French +# This corpus only had capital letters, but you probably want lowercase ones as well. +\b[LN]'+[a-z]{2,}\b + +# latex (check-spelling <= 0.0.21) +\\(?:n(?:ew|ormal|osub)|r(?:enew)|t(?:able(?:of|)|he|itle))(?=[a-z]+) + +# latex (check-spelling >= 0.0.22) +\\\w{2,}\{ + +# American Mathematical Society (AMS) / Doxygen +TeX/AMS + +# File extensions +\*\.[+\w]+, + +# eslint +"varsIgnorePattern": ".+" + +# Windows short paths +[/\\][^/\\]{5,6}~\d{1,2}[/\\] + +# cygwin paths +/cygdrive/[a-zA-Z]/(?:Program Files(?: \(.*?\)| ?)(?:/[-+.~\\/()\w ]+)*|[-+.~\\/()\w])+ + +# in check-spelling@v0.0.22+, printf markers aren't automatically consumed +# printf markers +(?v# +(?:(?<=[A-Z]{2})V|(?<=[a-z]{2}|[A-Z]{2})v)\d+(?:\b|(?=[a-zA-Z_])) + +# Compiler flags (Unix, Java/Scala) +# Use if you have things like `-Pdocker` and want to treat them as `docker` +#(?:^|[\t ,>"'`=(])-(?:(?:J-|)[DPWXY]|[Llf])(?=[A-Z]{2,}|[A-Z][a-z]|[a-z]{2,}) + +# Compiler flags (Windows / PowerShell) +# This is a subset of the more general compiler flags pattern. +# It avoids matching `-Path` to prevent it from being treated as `ath` +(?:^|[\t ,"'`=(])-(?:[DPL](?=[A-Z]{2,})|[WXYlf](?=[A-Z]{2,}|[A-Z][a-z]|[a-z]{2,})) + +# Compiler flags (linker) +,-B + +# libraries +\blib(?!rar(?:ies|y))(?=[a-z]) + +# WWNN/WWPN (NAA identifiers) +\b(?:0x)?10[0-9a-f]{14}\b|\b(?:0x|3)?[25][0-9a-f]{15}\b|\b(?:0x|3)?6[0-9a-f]{31}\b + +# iSCSI iqn (approximate regex) +\biqn\.[0-9]{4}-[0-9]{2}(?:[\.-][a-z][a-z0-9]*)*\b + +# curl arguments +\b(?:\\n|)curl(?:\.exe|)(?:\s+-[a-zA-Z]{1,2}\b)*(?:\s+-[a-zA-Z]{3,})(?:\s+-[a-zA-Z]+)* +# set arguments +\b(?:bash|sh|set)(?:\s+-[abefimouxE]{1,2})*\s+-[abefimouxE]{3,}(?:\s+-[abefimouxE]+)* +# tar arguments +\b(?:\\n|)g?tar(?:\.exe|)(?:(?:\s+--[-a-zA-Z]+|\s+-[a-zA-Z]+|\s[ABGJMOPRSUWZacdfh-pr-xz]+\b)(?:=[^ ]*|))+ +# tput arguments -- https://man7.org/linux/man-pages/man5/terminfo.5.html -- technically they can be more than 5 chars long... +\btput\s+(?:(?:-[SV]|-T\s*\w+)\s+)*\w{3,5}\b +# macOS temp folders +/var/folders/\w\w/[+\w]+/(?:T|-Caches-)/ +# github runner temp folders +/home/runner/work/_temp/[-_/a-z0-9]+ diff --git a/.github/actions/spelling/excludes.txt b/.github/actions/spelling/excludes.txt new file mode 100644 index 0000000..8b25848 --- /dev/null +++ b/.github/actions/spelling/excludes.txt @@ -0,0 +1,118 @@ +# See https://github.com/check-spelling/check-spelling/wiki/Configuration-Examples:-excludes +(?:^|/)(?i)COPYRIGHT +(?:^|/)(?i)LICEN[CS]E +(?:^|/)3rdparty/ +(?:^|/)go\.sum$ +(?:^|/)package(?:-lock|)\.json$ +(?:^|/)Pipfile$ +(?:^|/)pyproject.toml +(?:^|/)requirements(?:-dev|-doc|-test|)\.txt$ +(?:^|/)vendor/ +/CODEOWNERS$ +\.a$ +\.ai$ +\.all-contributorsrc$ +\.avi$ +\.bmp$ +\.bz2$ +\.cer$ +\.class$ +\.coveragerc$ +\.crl$ +\.crt$ +\.csr$ +\.dll$ +\.docx?$ +\.drawio$ +\.DS_Store$ +\.eot$ +\.eps$ +\.exe$ +\.gif$ +\.git-blame-ignore-revs$ +\.gitattributes$ +\.gitkeep$ +\.graffle$ +\.gz$ +\.icns$ +\.ico$ +\.jar$ +\.jks$ +\.jpe?g$ +\.key$ +\.lib$ +\.lock$ +\.map$ +\.min\.. +\.mo$ +\.mod$ +\.mp[34]$ +\.o$ +\.ocf$ +\.otf$ +\.p12$ +\.parquet$ +\.pdf$ +\.pem$ +\.pfx$ +\.png$ +\.psd$ +\.pyc$ +\.pylintrc$ +\.qm$ +\.s$ +\.sig$ +\.so$ +\.svgz?$ +\.sys$ +\.tar$ +\.tgz$ +\.tiff?$ +\.ttf$ +\.wav$ +\.webm$ +\.webp$ +\.woff2?$ +\.xcf$ +\.xlsx?$ +\.xpm$ +\.xz$ +\.zip$ +^\.github/actions/spelling/ +^\Q.github/workflows/spelling.yaml\E$ +^\Q.github/workflows/notebook_linter/run_linter.sh\E$ +^\Q.github/workflows/linter.yaml\E$ +^\Qgemini/use-cases/education/use_cases_for_education.ipynb\E$ +^\Qgemini/sample-apps/image-bash-jam/gemini-explain-image-italian.sh\E$ +^\Qgemini/sample-apps/image-bash-jam/tts.sh\E$ +^\Qgemini/use-cases/document-processing/sheet_music.ipynb\E$ +^\Qgemini/function-calling/use_case_company_news_and_insights.ipynb\E$ +^\Qgemini/getting-started/intro_gemini_1_5_pro.ipynb\E$ +^\Qgemini/getting-started/intro_gemini_pro_vision_python.ipynb\E$ +^\Qgemini/getting-started/intro_gemini_python.ipynb\E$ +^\Qgemini/agent-engine/tutorial_google_maps_agent.ipynb\E$ +^\Qgemini/sample-apps/image-bash-jam/images/.keep\E$ +^\Qgemini/sample-apps/image-bash-jam/README.md\E$ +^\Qgemini/sample-apps/photo-discovery/app/linux/CMakeLists.txt\E$ +^\Qgemini/sample-apps/photo-discovery/app/windows/CMakeLists.txt\E$ +^\Qgemini/use-cases/applying-llms-to-data/using-gemini-with-bigquery-remote-functions/src/sql/provision_text_sample_table.sql\E$ +^\Qgemini/use-cases/healthcare/react_gemini_healthcare_api.ipynb\E$ +^\Qgemini/use-cases/intro_multimodal_use_cases.ipynb\E$ +^\Qgemini/use-cases/retrieval-augmented-generation/intro_multimodal_rag.ipynb\E$ +^\Qgemini/use-cases/retail/product_attributes_extraction.ipynb\E$ +^\Qlanguage/use-cases/marketing-image-overlay/marketing_image_overlay.ipynb\E$ +^\Qsearch/bulk-question-answering/bulk_question_answering_output.tsv\E$ +^\Qvision/getting-started/image_editing_maskmode.ipynb\E$ +^\Qvision/getting-started/image_generation.ipynb\E$ +^\Qvision/getting-started/imagen4_image_generation.ipynb\E$ +^\Qvision/getting-started/visual_captioning.ipynb\E$ +^\Qvision/use-cases/creating_high_quality_visual_assets_with_gemini_and_imagen.ipynb\E$ +^\Qgemini/orchestration/llamaindex_workflows.ipynb\E$ +ignore$ +^\Qworkshops/ai-agents/ai_agents_for_engineers.ipynb\E$ +^\Q.github/workflows/issue_assigner/assign_issue.py\E$ +^\Qnoxfile.py\E$ +py\.typed$ +^\Qaudio/speech/use-cases/storytelling/macbeth_the_sitcom.json\E$ +.ruff.toml +^\Q.gemini/styleguide.md\E$ diff --git a/.github/actions/spelling/line_forbidden.patterns b/.github/actions/spelling/line_forbidden.patterns new file mode 100644 index 0000000..635b3be --- /dev/null +++ b/.github/actions/spelling/line_forbidden.patterns @@ -0,0 +1,323 @@ +# reject `m_data` as VxWorks defined it and that breaks things if it's used elsewhere +# see [fprime](https://github.com/nasa/fprime/commit/d589f0a25c59ea9a800d851ea84c2f5df02fb529) +# and [Qt](https://github.com/qtproject/qt-solutions/blame/fb7bc42bfcc578ff3fa3b9ca21a41e96eb37c1c7/qtscriptclassic/src/qscriptbuffer_p.h#L46) +#\bm_data\b + +# Were you debugging using a framework with `fit()`? +# If you have a framework that uses `it()` for testing and `fit()` for debugging a specific test, +# you might not want to check in code where you skip all the other tests. +#\bfit\( + +# Should be `HH:MM:SS` +\bHH:SS:MM\b + +# Should be `86400` (seconds in a standard day) +\b84600\b(?:.*\bday\b) + +# Should probably be `2006-01-02` (yyyy-mm-dd) +\b2006-01-02\b + +# Should probably be `YYYYMMDD` +\b[Yy]{4}[Dd]{2}[Mm]{2}(?!.*[Yy]{4}[Dd]{2}[Mm]{2}).*$ + +# Should be `anymore` +\bany more[,.] + +# Should be `cannot` (or `can't`) +# See https://www.grammarly.com/blog/cannot-or-can-not/ +# > Don't use `can not` when you mean `cannot`. The only time you're likely to see `can not` written as separate words is when the word `can` happens to precede some other phrase that happens to start with `not`. +# > `Can't` is a contraction of `cannot`, and it's best suited for informal writing. +# > In formal writing and where contractions are frowned upon, use `cannot`. +# > It is possible to write `can not`, but you generally find it only as part of some other construction, such as `not only . . . but also.` +# - if you encounter such a case, add a pattern for that case to patterns.txt. +\b[Cc]an not\b + +# Should be `GitHub` +(?v# +(?:(?<=[A-Z]{2})V|(?<=[a-z]{2}|[A-Z]{2})v)\d+(?:\b|(?=[a-zA-Z_])) + +# hit-count: 19 file-count: 10 +# Python string prefix / binary prefix +# Note that there's a high false positive rate, remove the `?=` and search for the regex to see if the matches seem like reasonable strings +(?"'`=(])-(?:(?:J-|)[DPWXY]|[Ll]|f(?!ix))(?=[A-Z]{2,}|[A-Z][a-z]|[a-z]{2,}) + +# hit-count: 7 file-count: 7 +# set arguments +\b(?:bash|sh|set)(?:\s+-[abefimouxE]{1,2})*\s+-[abefimouxE]{3,}(?:\s+-[abefimouxE]+)* + +# hit-count: 7 file-count: 5 +# hex runs +\b[0-9a-fA-F]{16,}\b + +# hit-count: 4 file-count: 2 +# kubernetes pod status lists +# https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle/#pod-phase +\w+(?:-\w+)+\s+\d+/\d+\s+(?:Running|Pending|Succeeded|Failed|Unknown)\s+ + +# hit-count: 1 file-count: 1 +# integrity +integrity=(['"])(?:\s*sha\d+-[-a-zA-Z=;:/0-9+]{40,})+\g{-1} + +# hit-count: 1 file-count: 1 +# curl arguments +\b(?:\\n|)curl(?:\.exe|)(?:\s+-[a-zA-Z]{1,2}\b)*(?:\s+-[a-zA-Z]{3,})(?:\s+-[a-zA-Z]+)* + +# pom.xml id fields +<\w+Id>[^<]+]*>|[^<]*)\s*$ + +# Autogenerated revert commit message +^This reverts commit [0-9a-f]{40}\.$ + +# ignore long runs of a single character: +\b([A-Za-z])\g{-1}{3,}\b + +# hit-count: 40 file-count: 8 +# Non-English +[a-zA-Z]*[Ă€ĂĂ‚ĂƒĂ„Ă…Ă†ÄŒĂ‡ĂˆĂ‰ĂŠĂ‹ĂŒĂĂŽĂĂĂ‘Ă’Ă“Ă”Ă•Ă–Ă˜Ă™ĂšĂ›ĂœĂĂŸĂ ĂĄĂąĂŁĂ€Ă„ĂŠÄĂ§ĂšĂ©ĂȘëÏíßïðñĂČóÎÔöÞĂčĂșĂ»ĂŒĂœĂżÄ€ÄĆĆ‚ĆƒĆ„Ć…Ć†Ć’Ć“ĆšĆ›Ć ĆĄĆœĆĆžĆœĆŸĆș][a-zA-Z]{3}[a-zA-ZĂ€ĂĂ‚ĂƒĂ„Ă…Ă†ÄŒĂ‡ĂˆĂ‰ĂŠĂ‹ĂŒĂĂŽĂĂĂ‘Ă’Ă“Ă”Ă•Ă–Ă˜Ă™ĂšĂ›ĂœĂĂŸĂ ĂĄĂąĂŁĂ€Ă„ĂŠÄĂ§ĂšĂ©ĂȘëÏíßïðñĂČóÎÔöÞĂčĂșĂ»ĂŒĂœĂżÄ€ÄĆĆ‚ĆƒĆ„Ć…Ć†Ć’Ć“ĆšĆ›Ć ĆĄĆœĆĆžĆœĆŸĆș]*|[a-zA-Z]{3,}[Ă€ĂĂ‚ĂƒĂ„Ă…Ă†ÄŒĂ‡ĂˆĂ‰ĂŠĂ‹ĂŒĂĂŽĂĂĂ‘Ă’Ă“Ă”Ă•Ă–Ă˜Ă™ĂšĂ›ĂœĂĂŸĂ ĂĄĂąĂŁĂ€Ă„ĂŠÄĂ§ĂšĂ©ĂȘëÏíßïðñĂČóÎÔöÞĂčĂșĂ»ĂŒĂœĂżÄ€ÄĆĆ‚ĆƒĆ„Ć…Ć†Ć’Ć“ĆšĆ›Ć ĆĄĆœĆĆžĆœĆŸĆș]|[Ă€ĂĂ‚ĂƒĂ„Ă…Ă†ÄŒĂ‡ĂˆĂ‰ĂŠĂ‹ĂŒĂĂŽĂĂĂ‘Ă’Ă“Ă”Ă•Ă–Ă˜Ă™ĂšĂ›ĂœĂĂŸĂ ĂĄĂąĂŁĂ€Ă„ĂŠÄĂ§ĂšĂ©ĂȘëÏíßïðñĂČóÎÔöÞĂčĂșĂ»ĂŒĂœĂżÄ€ÄĆĆ‚ĆƒĆ„Ć…Ć†Ć’Ć“ĆšĆ›Ć ĆĄĆœĆĆžĆœĆŸĆș][a-zA-Z]{3,} +(?i)thĂ©rĂšse + +# hit-count: 22 file-count: 1 +# ANSI color codes +(?:\\(?:u00|x)1[Bb]|\x1b|\\u\{1[Bb]\})\[\d+(?:;\d+|)m + +# hit-count: 15 file-count: 3 +# base64 encoded content +([`'"])[-a-zA-Z=;:/0-9+]{3,}=\g{-1} + +# hit-count: 11 file-count: 2 +# c99 hex digits (not the full format, just one I've seen) +0x[0-9a-fA-F](?:\.[0-9a-fA-F]*|)[pP] + +# hit-count: 10 file-count: 8 +# IServiceProvider / isAThing +(?:\b|_)(?:I|isA)(?=(?:[A-Z][a-z]{2,})+(?:[A-Z]|\b)) + +# hit-count: 6 file-count: 1 +# in check-spelling@v0.0.22+, printf markers aren't automatically consumed +# printf markers +(? int: + """Retrieves the issue number from GitHub event data.""" + # Load event data + with open(event_path, encoding="utf-8") as f: + event_data = json.load(f) + + # Determine the issue number based on the event + if "issue" in event_data: + return int(event_data["issue"]["number"]) + + raise ValueError("Unable to determine issue number from event data.") + + +def main() -> None: + """Gets filename mentioned in issue, finds author username in file, assigns issue to user.""" + # Get GitHub token and repository details + repo_name = os.getenv("GITHUB_REPOSITORY", "") + token = os.getenv("GITHUB_TOKEN") + issue_number = get_issue_number(os.getenv("GITHUB_EVENT_PATH", "")) + + g = Github(token) + repo = g.get_repo(repo_name) + issue = repo.get_issue(number=issue_number) + + # Regex to find any file with an extension + file_match = re.search(r"\b([\w-]+\.[\w]+)\b", issue.body, re.IGNORECASE) + + if not file_match: + print("No file found in issue.") + return + + file_name = file_match.group(1) + result = g.search_code(f"repo:{repo_name} filename:{file_name}") + + if result.totalCount == 0: + result = g.search_code(f"repo:{repo_name} {file_name}") + if result.totalCount == 0: + print(f"No files found for {file_name}") + return + + file_path = result[0].path + + # Get the commits for the file + commits = repo.get_commits(path=file_path) + + # Try to get the author of the first commit + if commits.totalCount > 0: + # The last commit in the list is the first commit + first_commit = commits[commits.totalCount - 1] + if first_commit.author: + username = first_commit.author.login + print( + f"Assigning {username} to Issue #{issue_number} for File {file_path} based on the first commit." + ) + issue.add_to_assignees(username) + return + + # If the file is a notebook and the first commit author wasn't found, + # check the notebook metadata as a fallback. + if file_name.endswith(".ipynb"): + print( + "Could not determine the first commit author, checking the notebook metadata." + ) + file_content_encoded = repo.get_contents(file_path).content + file_content = str(base64.b64decode(file_content_encoded))[:10000] + match = re.search( + r"Author.+https://github\.com/([^/\)]+)", file_content, flags=re.DOTALL + ) + + if match: + username = match.group(1) + print( + f"Assigning {username} to Issue #{issue_number} for File {file_path} based on notebook metadata." + ) + issue.add_to_assignees(username) + return + + print(f"No User Found for {file_name}") + + +if __name__ == "__main__": + main() diff --git a/.github/workflows/links.yaml b/.github/workflows/links.yaml new file mode 100644 index 0000000..ec4ed1a --- /dev/null +++ b/.github/workflows/links.yaml @@ -0,0 +1,25 @@ +name: Links + +on: + repository_dispatch: + workflow_dispatch: + schedule: + - cron: "00 18 * * *" + +jobs: + linkChecker: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + + - name: Link Checker + id: lychee + uses: lycheeverse/lychee-action@v2 + + - name: Create Issue From File + if: env.lychee_exit_code != 0 + uses: peter-evans/create-issue-from-file@v5 + with: + title: Link Checker Report + content-filepath: ./lychee/out.md + labels: report, automated issue diff --git a/.github/workflows/linter.yaml b/.github/workflows/linter.yaml new file mode 100644 index 0000000..319f1b0 --- /dev/null +++ b/.github/workflows/linter.yaml @@ -0,0 +1,66 @@ +################################# +################################# +## Super Linter GitHub Actions ## +################################# +################################# +name: Lint Code Base + +# +# Documentation: +# https://docs.github.com/en/actions/learn-github-actions/workflow-syntax-for-github-actions +# + +############################# +# Start the job on all push # +############################# +on: + pull_request: + branches: [main] + +############### +# Set the Job # +############### +jobs: + build: + # Name the Job + name: Lint Code Base + # Set the agent to run on + runs-on: ubuntu-latest + + ################## + # Load all steps # + ################## + steps: + ########################## + # Checkout the code base # + ########################## + - name: Checkout Code + uses: actions/checkout@v6 + with: + # Full git history is needed to get a proper list of changed files within `super-linter` + fetch-depth: 0 + + ################################ + # Run Linter against code base # + ################################ + - name: Lint Code Base + uses: super-linter/super-linter/slim@v7 + env: + DEFAULT_BRANCH: main + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + FILTER_REGEX_EXCLUDE: "^(.github/|gemini/sample-apps/finance-advisor-spanner/|gemini/sample-apps/quickbot/).*" + LOG_LEVEL: WARN + SHELLCHECK_OPTS: -e SC1091 -e 2086 + VALIDATE_ALL_CODEBASE: false + VALIDATE_PYTHON_PYINK: false + VALIDATE_PYTHON_ISORT: false + VALIDATE_PYTHON_BLACK: false + VALIDATE_CHECKOV: false + VALIDATE_JAVASCRIPT_STANDARD: false + VALIDATE_TYPESCRIPT_STANDARD: false + VALIDATE_JUPYTER_NBQA_FLAKE8: false + VALIDATE_JUPYTER_NBQA_PYLINT: false + VALIDATE_JUPYTER_NBQA_ISORT: false + VALIDATE_JUPYTER_NBQA_MYPY: false + VALIDATE_JUPYTER_NBQA_BLACK: false + VALIDATE_JSCPD: false diff --git a/.github/workflows/spelling.yaml b/.github/workflows/spelling.yaml new file mode 100644 index 0000000..c6bd924 --- /dev/null +++ b/.github/workflows/spelling.yaml @@ -0,0 +1,156 @@ +name: Check Spelling + +# Comment management is handled through a secondary job, for details see: +# https://github.com/check-spelling/check-spelling/wiki/Feature%3A-Restricted-Permissions +# +# `jobs.comment-push` runs when a push is made to a repository and the `jobs.spelling` job needs to make a comment +# (in odd cases, it might actually run just to collapse a comment, but that's fairly rare) +# it needs `contents: write` in order to add a comment. +# +# `jobs.comment-pr` runs when a pull_request is made to a repository and the `jobs.spelling` job needs to make a comment +# or collapse a comment (in the case where it had previously made a comment and now no longer needs to show a comment) +# it needs `pull-requests: write` in order to manipulate those comments. + +# Updating pull request branches is managed via comment handling. +# For details, see: https://github.com/check-spelling/check-spelling/wiki/Feature:-Update-expect-list +# +# These elements work together to make it happen: +# +# `on.issue_comment` +# This event listens to comments by users asking to update the metadata. +# +# `jobs.update` +# This job runs in response to an issue_comment and will push a new commit +# to update the spelling metadata. +# +# `with.experimental_apply_changes_via_bot` +# Tells the action to support and generate messages that enable it +# to make a commit to update the spelling metadata. +# +# `with.ssh_key` +# In order to trigger workflows when the commit is made, you can provide a +# secret (typically, a write-enabled github deploy key). +# +# For background, see: https://github.com/check-spelling/check-spelling/wiki/Feature:-Update-with-deploy-key + +# Sarif reporting +# +# Access to Sarif reports is generally restricted (by GitHub) to members of the repository. +# +# Requires enabling `security-events: write` +# and configuring the action with `use_sarif: 1` +# +# For information on the feature, see: https://github.com/check-spelling/check-spelling/wiki/Feature:-Sarif-output + +# Minimal workflow structure: +# +# on: +# push: +# ... +# pull_request_target: +# ... +# jobs: +# # you only want the spelling job, all others should be omitted +# spelling: +# # remove `security-events: write` and `use_sarif: 1` +# # remove `experimental_apply_changes_via_bot: 1` +# ... otherwise adjust the `with:` as you wish + +on: + pull_request: + branches: + - "**" + types: + - "opened" + - "reopened" + - "synchronize" + +jobs: + spelling: + name: Check Spelling + permissions: + contents: read + actions: read + security-events: write + outputs: + followup: ${{ steps.spelling.outputs.followup }} + runs-on: ubuntu-latest + if: ${{ contains(github.event_name, 'pull_request') || github.event_name == 'push' }} + concurrency: + group: spelling-${{ github.event.pull_request.number || github.ref }} + # note: If you use only_check_changed_files, you do not want cancel-in-progress + cancel-in-progress: false + steps: + - name: check-spelling + id: spelling + uses: check-spelling/check-spelling@main + with: + suppress_push_for_open_pull_request: ${{ github.actor != 'dependabot[bot]' && 1 }} + checkout: true + check_file_names: 1 + spell_check_this: check-spelling/spell-check-this@main + post_comment: 0 + use_magic_file: 1 + report-timing: 1 + warnings: bad-regex,binary-file,deprecated-feature,ignored-expect-variant,large-file,limited-references,no-newline-at-eof,noisy-file,non-alpha-in-dictionary,token-is-substring,unexpected-line-ending,whitespace-in-dictionary,minified-file,unsupported-configuration,no-files-to-check,unclosed-block-ignore-begin,unclosed-block-ignore-end + experimental_apply_changes_via_bot: 1 + dictionary_source_prefixes: '{"cspell": "https://raw.githubusercontent.com/streetsidesoftware/cspell-dicts/main/dictionaries/"}' + extra_dictionaries: | + cspell:aws/dict/aws.txt + cspell:bash/samples/bash-words.txt + cspell:companies/dict/companies.txt + cspell:css/dict/css.txt + cspell:data-science/dict/data-science-models.txt + cspell:data-science/dict/data-science.txt + cspell:data-science/dict/data-science-tools.txt + cspell:dart/src/dart.txt + cspell:de_DE/src/German_de_DE.dic + cspell:django/requirements.txt + cspell:django/dict/django.txt + cspell:docker/src/docker-words.txt + cspell:en_shared/dict/acronyms.txt + cspell:en_shared/dict/shared-additional-words.txt + cspell:en_GB/en_GB.trie + cspell:en_US/en_US.trie + cspell:filetypes/src/filetypes.txt + cspell:flutter/src/flutter.txt + cspell:fonts/dict/fonts.txt + cspell:fr_FR/fr-fr.trie + cspell:fullstack/dict/fullstack.txt + cspell:golang/dict/go.txt + cspell:google/dict/google.txt + cspell:html/dict/html.txt + cspell:it_IT/dict/it-it.trie + cspell:java/src/java.txt + cspell:k8s/dict/k8s.txt + cspell:mnemonics/dict/mnemonics.txt + cspell:monkeyc/src/monkeyc_keywords.txt + cspell:node/dict/node.txt + cspell:npm/dict/npm.txt + cspell:people-names/dict/people-names.txt + cspell:php/dict/php.txt + cspell:python/dict/python.txt + cspell:python/dict/python-common.txt + cspell:shell/dict/shell-all-words.txt + cspell:software-terms/dict/softwareTerms.txt + cspell:software-terms/dict/webServices.txt + cspell:sql/src/common-terms.txt + cspell:sql/src/sql.txt + cspell:sql/src/tsql.txt + cspell:svelte/dict/svelte.txt + cspell:terraform/dict/terraform.txt + cspell:typescript/dict/typescript.txt + check_extra_dictionaries: + cspell:cryptocurrencies/dict/cryptocurrencies.txt + cspell:gaming-terms/dict/gaming-terms.txt + cspell:latex/dict/latex.txt + cspell:public-licenses/src/additional-licenses.txt + cspell:public-licenses/src/generated/public-licenses.txt + ignore-pattern: "[^'a-zåéíóĂșĂ±Ă§ĂŒA-ZÁÉÍÓÚÑÇÜ]" + upper-pattern: "[A-ZÁÉÍÓÚÑÇÜ]" + lower-pattern: "[a-zåéíóĂșĂ±Ă§ĂŒ]" + not-lower-pattern: "[^a-zåéíóĂșĂ±Ă§ĂŒ]" + not-upper-or-lower-pattern: "[^A-ZÁÉÍÓÚÑÇÜa-zåéíóĂșĂ±Ă§ĂŒ]" + punctuation-pattern: "'" + only_check_changed_files: true + longest_word: "10" diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..705a351 --- /dev/null +++ b/.gitignore @@ -0,0 +1,191 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*.pyc +*$py.class +**/dist +/tmp +/out-tsc +/bazel-out + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +pip-wheel-metadata/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +.python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +Pipfile.lock +Pipfile + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +.venv* +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# ruff cache +.ruff_cache/ + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# macOS +.DS_Store + +# PyCharm +.idea + +# User-specific files +language/examples/prompt-design/train.csv +README-TOC*.md + +# Terraform +terraform.tfstate** +.terraform* +.Terraform* + +tmp* + +# Node +**/node_modules +npm-debug.log +yarn-error.log + +# IDEs and editors +.idea/ +.project +.classpath +.c9/ +*.launch +.settings/ +*.sublime-workspace + +# Visual Studio Code +.history/* + +# Miscellaneous +**/.angular/* +/.angular/cache +.sass-cache/ +/connect.lock +/coverage +/libpeerconnection.log +testem.log +/typings + +# System files +.DS_Store +Thumbs.db + +.agent/* +tools/llmevalkit/uv.lock +agent-development-answers.yml diff --git a/.gitleaksignore b/.gitleaksignore new file mode 100644 index 0000000..42c4621 --- /dev/null +++ b/.gitleaksignore @@ -0,0 +1,6 @@ +/github/workspace/gemini/use-cases/education/use_cases_for_education.ipynb:aws-access-token:2802 +/github/workspace/language/prompts/examples/chain_of_thought_react.ipynb:aws-access-token:308 +/github/workspace/vision/use-cases/creating_high_quality_visual_assets_with_gemini_and_imagen.ipynb:facebook-page-access-token:309 +/github/workspace/language/prompts/examples/chain_of_thought_react.ipynb:aws-access-token:310 +/github/workspace/gemini/prompts/examples/chain_of_thought_react.ipynb:aws-access-token:322 +/github/workspace/language/prompts/examples/chain_of_thought_react.ipynb:aws-access-token:297 diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..9c1044f --- /dev/null +++ b/.prettierrc @@ -0,0 +1,3 @@ +{ + "bracketSameLine": true +} diff --git a/.repo-metadata.json b/.repo-metadata.json new file mode 100644 index 0000000..22e0a59 --- /dev/null +++ b/.repo-metadata.json @@ -0,0 +1,8 @@ +{ + "default_version": "v1", + "name": "generative-ai", + "name_pretty": "Google Cloud Generative AI Samples", + "issue_tracker": "https://github.com/GoogleCloudPlatform/generative-ai/issues", + "language": "python", + "repo": "GoogleCloudPlatform/generative-ai" +} diff --git a/.ruff.toml b/.ruff.toml new file mode 100644 index 0000000..885191b --- /dev/null +++ b/.ruff.toml @@ -0,0 +1,133 @@ +line-length = 88 +indent-width = 4 # Google Style Guide §3.4: 4 spaces + +target-version = "py310" # Minimum Python version + +[lint] +ignore = [ + "COM812", # Trailing comma missing. + "FBT001", # Boolean positional arg in function definition + "FBT002", # Boolean default value in function definition + "D203", # 1 blank line required before class docstring (Google: 0) + "D213", # Multi-line docstring summary should start at the second line (Google: first line) + "D100", # Ignore Missing docstring in public module (often desired at top level __init__.py) + "D104", # Ignore Missing docstring in public package (often desired at top level __init__.py) + "D107", # Ignore Missing docstring in __init__ (use class docstring) + "TC006", # Ignore unquoted type expressions in typing.cast() calls. + "TD002", # Ignore Missing author in TODOs (often not required) + "TD003", # Ignore Missing issue link in TODOs (often not required/available) + "T201", # Ignore print presence + "RUF012", # Ignore Mutable class attributes should be annotated with `typing.ClassVar` + "E501", # Ignore line length (handled by Ruff's dynamic line length) + "UP007", # Do not upgrade `Optional[T]` to `T | None` (PEP 604 syntax) + "ANN002", + "ANN003", + "ANN401", + "TRY003", + "G004", + "TRY201", +] + +select = [ + "E", # pycodestyle errors (PEP 8) + "W", # pycodestyle warnings (PEP 8) + "F", # Pyflakes (logical errors, unused imports/variables) + "I", # isort (import sorting - Google Style §3.1.2) + "D", # pydocstyle (docstring conventions - Google Style §3.8) + "N", # pep8-naming (naming conventions - Google Style §3.16) + "UP", # pyupgrade (use modern Python syntax) + "ANN",# flake8-annotations (type hint usage/style - Google Style §2.22) + "A", # flake8-builtins (avoid shadowing builtins) + "B", # flake8-bugbear (potential logic errors & style issues - incl. mutable defaults B006, B008) + "C4", # flake8-comprehensions (unnecessary list/set/dict comprehensions) + "ISC",# flake8-implicit-str-concat (disallow implicit string concatenation across lines) + "T20",# flake8-print (discourage `print` - prefer logging) + "SIM",# flake8-simplify (simplify code, e.g., `if cond: return True else: return False`) + "PTH",# flake8-use-pathlib (use pathlib instead of os.path where possible) + "PL", # Pylint rules ported to Ruff (PLC, PLE, PLR, PLW) + "PIE",# flake8-pie (misc code improvements, e.g., no-unnecessary-pass) + "RUF",# Ruff-specific rules (e.g., RUF001-003 ambiguous unicode, RUF013 implicit optional) + "RET",# flake8-return (consistency in return statements) + "SLF",# flake8-self (check for private member access via `self`) + "TID",# flake8-tidy-imports (relative imports, banned imports - configure if needed) + "YTT",# flake8-boolean-trap (checks for boolean positional arguments, truthiness tests - Google Style §3.10) + "TD", # flake8-todos (check TODO format - Google Style §3.7) + "TCH",# flake8-type-checking (helps manage TYPE_CHECKING blocks and imports) + "PYI",# flake8-pyi (best practices for .pyi stub files, some rules are useful for .py too) + "S", # flake8-bandit (security issues) + "DTZ",# flake8-datetimez (timezone-aware datetimes) + "ERA",# flake8-eradicate (commented-out code) + "Q", # flake8-quotes (quote style consistency) + "RSE",# flake8-raise (modern raise statements) + "TRY",# tryceratops (exception handling best practices) + "PERF",# perflint (performance anti-patterns) + "BLE", + "T10", + "ICN", + "G", + "FIX", + "ASYNC", + "INP", +] + +exclude = [ + ".bzr", + ".direnv", + ".eggs", + ".git", + ".hg", + ".mypy_cache", + ".nox", + ".pants.d", + ".pytype", + ".ruff_cache", + ".svn", + ".tox", + ".venv", + "__pypackages__", + "_build", + "buck-out", + "build", + "dist", + "node_modules", + "venv", +] + +[lint.isort] +#force-sort-within-sections = true +#combine-as-imports = true +case-sensitive = true +#force-single-line = false +#known-first-party = [] +#known-third-party = [] +lines-after-imports = 1 +#lines-between-types = 1 +#no-lines-before = ["LOCALFOLDER"] +#required-imports = [] +#section-order = ["future", "standard-library", "third-party", "first-party", "local-folder"] + +[lint.pydocstyle] +convention = "google" +ignore-decorators = ["typing.overload", "abc.abstractmethod"] + +[lint.flake8-annotations] +mypy-init-return = true +allow-star-arg-any = false + +[lint.pep8-naming] +ignore-names = ["test_*", "setUp", "tearDown", "mock_*"] +classmethod-decorators = ["classmethod", "pydantic.validator", "pydantic.root_validator"] +staticmethod-decorators = ["staticmethod"] + +[lint.flake8-tidy-imports] +ban-relative-imports = "all" # Google generally prefers absolute imports (§3.1.2) + +[lint.flake8-quotes] +docstring-quotes = "double" +inline-quotes = "double" + +[format] +docstring-code-format = true +docstring-code-line-length = "dynamic" # Or set to 80 +quote-style = "double" +indent-style = "space" diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..9ad7f9a --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,23 @@ +{ + "python.testing.pytestArgs": [ + "tests" + ], + "python.testing.unittestEnabled": false, + "python.testing.pytestEnabled": true, + "editor.formatOnSave": true, + "[python]": { + "editor.defaultFormatter": "charliermarsh.ruff", + "editor.formatOnSave": true, + "editor.codeActionsOnSave": { + "source.organizeImports": "always", + "source.fixAll.ruff": "explicit" + } + }, + "ruff.importStrategy": "fromEnvironment", + "files.insertFinalNewline": true, + "files.trimFinalNewlines": true, + "files.trimTrailingWhitespace": true, + "editor.rulers": [ + 88 + ] +} diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..4d9e465 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,43 @@ +# Contributor Code of Conduct + +As contributors and maintainers of this project, +and in the interest of fostering an open and welcoming community, +we pledge to respect all people who contribute through reporting issues, +posting feature requests, updating documentation, +submitting pull requests or patches, and other activities. + +We are committed to making participation in this project +a harassment-free experience for everyone, +regardless of level of experience, gender, gender identity and expression, +sexual orientation, disability, personal appearance, +body size, race, ethnicity, age, religion, or nationality. + +Examples of unacceptable behavior by participants include: + +- The use of sexualized language or imagery +- Personal attacks +- Trolling or insulting/derogatory comments +- Public or private harassment +- Publishing other's private information, + such as physical or electronic + addresses, without explicit permission +- Other unethical or unprofessional conduct. + +Project maintainers have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions +that are not aligned to this Code of Conduct. +By adopting this Code of Conduct, +project maintainers commit themselves to fairly and consistently +applying these principles to every aspect of managing this project. +Project maintainers who do not follow or enforce the Code of Conduct +may be permanently removed from the project team. + +This code of conduct applies both within project spaces and in public spaces +when an individual is representing the project or its community. + +Instances of abusive, harassing, or otherwise unacceptable behavior +may be reported by opening an issue +or contacting one or more of the project maintainers. + +This Code of Conduct is adapted from the [Contributor Covenant](http://contributor-covenant.org), version 1.2.0, +available at [http://contributor-covenant.org/version/1/2/0/](http://contributor-covenant.org/version/1/2/0/) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..94617a6 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,84 @@ +# How to Contribute + +We'd love to accept your patches and contributions to this project. There are +just a few small guidelines you need to follow. + +## Contributor License Agreement + +Contributions to this project must be accompanied by a Contributor License +Agreement. You (or your employer) retain the copyright to your contribution; +this simply gives us permission to use and redistribute your contributions as +part of the project. Head over to to see +your current agreements on file or to sign a new one. + +You generally only need to submit a CLA once, so if you've already submitted one +(even if it was for a different project), you probably don't need to do it +again. + +## Notebook Template + +If you're creating a Jupyter Notebook, use [`notebook_template.ipynb`](notebook_template.ipynb) as a template. + +## Code Quality Checks + +All notebooks in this project are checked for formatting and style, to ensure a +consistent experience. To test notebooks prior to submitting a pull request, +you can follow these steps. + +From a command-line terminal (e.g. from Workbench or locally), +run this code block to format your code. +If the fixes can't be performed automatically, +then you will need to manually address them before submitting your PR. + +```shell +uv pip install autoflake ruff nbqa "nbformat>=5.10.4" "git+https://github.com/tensorflow/docs" +./scripts/format.sh +``` + +## Code Reviews + +All submissions, including submissions by project members, require review. We +use GitHub pull requests for this purpose. Consult +[GitHub Help](https://help.github.com/articles/about-pull-requests/) for more +information on using pull requests. + +## Community Guidelines + +This project follows [Google's Open Source Community Guidelines](https://opensource.google/conduct/). + +## Contributor Guide + +If you are new to contributing to open source, you can find helpful information in this contributor guide. + +You may follow these steps to contribute: + +1. **Fork the official repository.** This will create a copy of the official repository in your own account. +2. **Sync the branches.** This will ensure that your copy of the repository is up-to-date with the latest changes from the official repository. +3. **Work on your forked repository's feature branch.** This is where you will make your changes to the code. +4. **Commit your updates on your forked repository's feature branch.** This will save your changes to your copy of the repository. +5. **Submit a pull request to the official repository's main branch.** This will request that your changes be merged into the official repository. +6. **Resolve any linting errors.** This will ensure that your changes are formatted correctly. + - For errors generated by [check-spelling](https://github.com/check-spelling/check-spelling), go to the [Job Summary](https://github.com/GoogleCloudPlatform/generative-ai/actions/workflows/spelling.yaml) to read the errors. + - Fix any spelling errors that are found. + - Forbidden Patterns are defined as regular expressions, you can copy/paste them into many IDEs to find the instances. [Example for Visual Studio Code](https://medium.com/@nikhilbaxi3/visual-studio-code-secrets-of-regular-expression-search-71723c2ecbd2). + - Add false positives to [`.github/actions/spelling/allow.txt`](.github/actions/spelling/allow.txt). Be sure to check that it's actually spelled correctly! + +Here are some additional things to keep in mind during the process: + +- **Read the [Google's Open Source Community Guidelines](https://opensource.google/conduct/).** The contribution guidelines will provide you with more information about the project and how to contribute. +- **Test your changes.** Before you submit a pull request, make sure that your changes work as expected. +- **Be patient.** It may take some time for your pull request to be reviewed and merged. + +--- + +## For Google Employees + +Complete the following steps to register your GitHub account and be added as a contributor to this repository. + +1. Register your GitHub account at [go/GitHub](http://go/github) + +1. Once you have registered, go to [go/github-googlecloudplatform](http://go/github-googlecloudplatform) and request to join the GoogleCloudPlatform organization. Check the box "I need write access on a public repository". + +1. You'll receive an email to your GitHub registered email to approve the request to join. Approve it. + +1. Request to join this team [GoogleCloudPlatform/teams/generative-ai-samples-contributors](https://github.com/orgs/GoogleCloudPlatform/teams/generative-ai-samples-contributors/members) diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md new file mode 100644 index 0000000..e9382bb --- /dev/null +++ b/README.md @@ -0,0 +1,138 @@ +# Generative AI on Google Cloud + +> **[Gemini Enterprise Agent Platform](https://docs.cloud.google.com/gemini-enterprise-agent-platform)**, the latest evolution of Vertex AI, has been released! +> +> Check out the [`Google-Cloud-AI/agent-platform`](https://goo.gle/agent-platform-github) repository for a curated list of assets for agent building on Google Cloud. + + + +This repository contains notebooks, code samples, sample apps, and other resources that demonstrate how to use, develop and manage generative AI workflows using [Generative AI](https://cloud.google.com/ai/generative-ai) with [Agent Platform](https://docs.cloud.google.com/gemini-enterprise-agent-platform). + +## Intro Video + +[![What is Gemini Enterprise Agent Platform?](https://img.youtube.com/vi/j8qW5poBkEU/maxresdefault.jpg)](https://goo.gle/agent-platform-video) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Description
+ Gemini +
+ gemini/ +
+ Discover Gemini through starter notebooks, use cases, function calling, sample apps, and more. +
+ Search +
+ search/ +
Use this folder if you're interested in using Agent Search, a Google-managed solution to help you rapidly build search engines for websites and across enterprise data. (Formerly known as Enterprise Search on Generative AI App Builder).
+ RAG Grounding +
+ rag-grounding/ +
Use this folder for information on Retrieval Augmented Generation (RAG) and Grounding. This is an index of notebooks and samples across other directories focused on this topic.
+ Vision +
+ vision/ +
+ Use this folder if you're interested in building your own solutions from scratch using features from Imagen and Veo. +
+ Speech +
+ audio/ +
+ Use this folder if you're interested in building your own solutions from scratch using features from Chirp, a version of Google's Universal Speech Model (USM). +
+ Setup Env +
+ setup-env/ +
Instructions on how to set up Google Cloud, the Gen AI Python SDK, and notebook environments on Google Colab and Workbench.
+ Resources +
+ RESOURCES.md +
Learning resources (e.g. blogs, YouTube playlists) about Generative AI on Google Cloud.
+ + +## Related Repositories + +- ✹ [Agent Development Kit (ADK) Samples](https://github.com/google/adk-samples): This repository provides ready-to-use agents built on top of the Agent Development Kit, designed to accelerate your development process. These agents cover a range of common use cases and complexities, from simple conversational bots to complex multi-agent workflows. +- [🚀 Agent Starter Pack](https://github.com/GoogleCloudPlatform/agent-starter-pack) + - A collection of production-ready Generative AI Agent templates built for Google Cloud. + - It accelerates development by providing a holistic, production-ready solution, addressing common challenges (Deployment & Operations, Evaluation, Customization, Observability) in building and deploying Gen AI agents. +- [Gemini Cookbook](https://github.com/google-gemini/cookbook/) +- [genai-factory](https://github.com/googleCloudPlatform/genai-factory) - A collection of end-to-end infrastructure blueprints to deploy generative AI infrastructures in GCP, using IaC and following security best-practices. +- [Google Cloud Applied AI Engineering](https://github.com/GoogleCloudPlatform/applied-ai-engineering-samples) +- [Vertex AI GenMedia Creative Studio](https://github.com/GoogleCloudPlatform/vertex-ai-creative-studio) - Experience Google's generative media foundational models + custom workflows. +- [MCP Servers for GenMedia](https://goo.gle/vertex-genmedia-mcp) - Empower your agents with generative media tools. +- [Generative AI for Marketing using Google Cloud](https://github.com/GoogleCloudPlatform/genai-for-marketing) +- [Generative AI for Developer Productivity](https://github.com/GoogleCloudPlatform/genai-for-developers) +- Vertex AI Core + - [Vertex AI Samples](https://github.com/GoogleCloudPlatform/vertex-ai-samples) + - [MLOps with Vertex AI](https://github.com/GoogleCloudPlatform/mlops-with-vertex-ai) + - [Developing NLP solutions with T5X and Vertex AI](https://github.com/GoogleCloudPlatform/t5x-on-vertex-ai) + - [AlphaFold batch inference with Vertex AI Pipelines](https://github.com/GoogleCloudPlatform/vertex-ai-alphafold-inference-pipeline) + - [Serving Spark ML models using Vertex AI](https://github.com/GoogleCloudPlatform/vertex-ai-spark-ml-serving) + - [Sensitive Data Protection (Cloud DLP) for Vertex AI Generative Models (PaLM2)](https://github.com/GoogleCloudPlatform/Sensitive-Data-Protection-for-Vertex-AI-PaLM2) +- Conversational AI + - [Contact Center AI Samples](https://github.com/GoogleCloudPlatform/contact-center-ai-samples) + - [Reimagining Customer Experience 360](https://github.com/GoogleCloudPlatform/dialogflow-ccai-omnichannel) +- Document AI + - [Document AI Samples](https://github.com/GoogleCloudPlatform/document-ai-samples) +- Gemini in Google Cloud + - [Cymbal Superstore](https://github.com/GoogleCloudPlatform/cymbal-superstore) +- Cloud Databases + - [Gen AI Databases Retrieval App](https://github.com/GoogleCloudPlatform/genai-databases-retrieval-app) +- Other + - [ai-on-gke](https://github.com/GoogleCloudPlatform/ai-on-gke) + - [ai-infra-cluster-provisioning](https://github.com/GoogleCloudPlatform/ai-infra-cluster-provisioning) + - [solutions-genai-llm-workshop](https://github.com/GoogleCloudPlatform/solutions-genai-llm-workshop) + - [terraform-genai-doc-summarization](https://github.com/GoogleCloudPlatform/terraform-genai-doc-summarization) + - [terraform-genai-knowledge-base](https://github.com/GoogleCloudPlatform/terraform-genai-knowledge-base) + - [genai-product-catalog](https://github.com/GoogleCloudPlatform/genai-product-catalog) + - [solutionbuilder-terraform-genai-doc-summarization](https://github.com/GoogleCloudPlatform/solutionbuilder-terraform-genai-doc-summarization) + - [solutions-viai-edge-provisioning-configuration](https://github.com/GoogleCloudPlatform/solutions-viai-edge-provisioning-configuration) + - [mis-ai-accelerator](https://github.com/GoogleCloudPlatform/mis-ai-accelerator) + - [dataflow-opinion-analysis](https://github.com/GoogleCloudPlatform/dataflow-opinion-analysis) + - [genai-beyond-basics](https://github.com/meteatamel/genai-beyond-basics) + - [Gemini by Example](https://geminibyexample.com) + +## Contributing + +Contributions welcome! See the [Contributing Guide](https://github.com/GoogleCloudPlatform/generative-ai/blob/main/CONTRIBUTING.md). + +## Getting help + +Please use the [issues page](https://github.com/GoogleCloudPlatform/generative-ai/issues) to provide suggestions, feedback or submit a bug report. + +## Disclaimer + +This repository itself is not an officially supported Google product. The code in this repository is for demonstrative purposes only. diff --git a/README.wehub.md b/README.wehub.md new file mode 100644 index 0000000..286defb --- /dev/null +++ b/README.wehub.md @@ -0,0 +1,7 @@ +# WeHub 杄æșèŻŽæ˜Ž + +- 掟構éĄčç›źïŒš`GoogleCloudPlatform/generative-ai` +- ćŽŸć§‹ä»“ćș“https://github.com/GoogleCloudPlatform/generative-ai +- ćŻŒć…„æ–čćŒïŒšäžŠæžžé»˜èź€ćˆ†æ”Żçš„æœ€æ–°ćż«ç…§ +- ćŽŸäœœè€…ă€ç‰ˆæƒć’ŒèźžćŻèŻäżĄæŻä»„ćŽŸć§‹ä»“ćș“ćŠæœŹä»“ćș“ LICENSE äžș懆 +- æœŹæ–‡ä»¶ä»…ç”šäșŽèź°ćœ•杄æșïŒŒäžä»ŁèĄš WeHub æ˜ŻćŽŸéĄčç›źäœœè€… diff --git a/RESOURCES.md b/RESOURCES.md new file mode 100644 index 0000000..ecc621c --- /dev/null +++ b/RESOURCES.md @@ -0,0 +1,109 @@ +# Google Generative AI resources + +A collection of resources that helps everyone learn more about Google Generative AI offerings. + +Please submit additional resources via a PR. + +## Generative AI on Google Cloud + +- [Generative AI on Google Cloud](https://cloud.google.com/ai/generative-ai) +- [Overview of Generative AI](https://cloud.google.com/vertex-ai/generative-ai/docs/learn/overview) +- [Certifications and security controls](https://cloud.google.com/vertex-ai/generative-ai/docs/security-controls) +- [Responsible AI best practices](https://cloud.google.com/vertex-ai/generative-ai/docs/learn/responsible-ai) +- [Quotas and Limits](https://cloud.google.com/vertex-ai/generative-ai/docs/quotas) +- [Pricing](https://cloud.google.com/vertex-ai/pricing#generative_ai_models) + +## Products + +### Agent Platform + +- Use [Agent Platform](https://cloud.google.com/vertex-ai) to interact with, customize, and embed foundation models into your applications +- Access foundation models on [Model Garden](https://cloud.google.com/model-garden) +- Tune models via a simple UI on [Agent Studio](https://cloud.google.com/generative-ai-studio) +- Use [Agent Search](https://cloud.google.com/products/agent-builder), to build and deploy AI agents grounded in their data +- Google foundation models + - [Gemini API models](https://cloud.google.com/vertex-ai/generative-ai/docs/learn/models#gemini-models) + - [Imagen API models](https://cloud.google.com/vertex-ai/generative-ai/docs/learn/models#imagen-models) + - [MedLM API models](https://cloud.google.com/vertex-ai/generative-ai/docs/learn/models#medlm-models) + +### Gemini Models + +- [Gemini](https://cloud.google.com/vertex-ai/docs/generative-ai/multimodal/overview), a multimodal model from Google DeepMind, is capable of understanding virtually any input, combining different types of information, and generating almost any output. +- Prompt and test Gemini in [Agent Platform](https://console.cloud.google.com/freetrial/?redirectPath=%2Fvertex-ai%2Fgenerative%2Fmultimodal%2Fcreate%2Ftext) using text, images, video, or code. + +### Gemini for Google Cloud + +- [Gemini Code Assist](https://cloud.google.com/products/gemini/code-assist) offers AI-powered assistance to help developers build applications with higher velocity, quality, and security in popular code editors like VS Code and JetBrains, and on developer platforms like Firebase. +- [Gemini for Google Cloud](https://cloud.google.com/products/gemini) offerings assist users in working and coding more effectively, gaining deeper data insights, navigating security challenges, and more. + +## Get Started + +- Quickstart tutorial using [Agent Studio](https://cloud.google.com/vertex-ai/generative-ai/docs/start/quickstarts/quickstart) or the [Agent Platform API](https://cloud.google.com/vertex-ai/generative-ai/docs/start/quickstarts/quickstart-multimodal) +- Explore pretrained models in [Model Garden](https://cloud.google.com/vertex-ai/docs/start/explore-models) +- [Tune a Foundation model](https://cloud.google.com/vertex-ai/generative-ai/docs/models/tune-models) + +## Video Courses + +- [Real Terms for AI](https://goo.gle/AIwordsExplained) +- [AI Guide for Cloud Developers](https://www.youtube.com/playlist?list=PLIivdWyY5sqJio2yeg1dlfILOUO2FoFRx) +- [Intro to Gen AI - Playlist](https://www.youtube.com/playlist?list=PLBgogxgQVM9sl-KnKywVEhkb3QtLHU4OK) + - [Introduction to Generative AI](https://www.youtube.com/watch?v=cZaNf2rA30k&list=PLBgogxgQVM9sl-KnKywVEhkb3QtLHU4OK&index=1&pp=iAQB) + - [Introduction to Large Language Models](https://www.youtube.com/watch?v=RBzXsQHjptQ&list=PLBgogxgQVM9sl-KnKywVEhkb3QtLHU4OK&index=2&pp=iAQB) + - [Introduction to Responsible AI](https://www.youtube.com/watch?v=w_3L1Bf2P_g&list=PLBgogxgQVM9sl-KnKywVEhkb3QtLHU4OK&index=3&pp=iAQB) + - [Intro to Gen AI training course from Google Cloud](https://www.youtube.com/watch?v=9Eh7gsIH5h4&list=PLBgogxgQVM9sl-KnKywVEhkb3QtLHU4OK&index=4&pp=iAQB) +- [Gen AI for Developers](https://www.youtube.com/watch?v=JR9Gdo-_lx8&list=PLBgogxgQVM9s0i9oloJwjIG-zj6Svsm20) + - [Introduction to Image Generation](https://www.youtube.com/watch?v=JR9Gdo-_lx8&list=PLBgogxgQVM9s0i9oloJwjIG-zj6Svsm20&index=1&pp=iAQB) + - [Attention Mechanism: Overview](https://www.youtube.com/watch?v=8PmOaVYVeKY&list=PLBgogxgQVM9s0i9oloJwjIG-zj6Svsm20&index=2&pp=iAQB) + - [Encoder-Decoder Architecture: Overview](https://www.youtube.com/watch?v=671xny8iows&list=PLBgogxgQVM9s0i9oloJwjIG-zj6Svsm20&index=3&pp=iAQB) + - [Transformer Models and BERT Model: Overview](https://www.youtube.com/watch?v=hsp1OAcoLBY&list=PLBgogxgQVM9s0i9oloJwjIG-zj6Svsm20&index=4&pp=iAQB) + - [Create Image Captioning Models: Overview](https://www.youtube.com/watch?v=0BaIeMoFEoE&list=PLBgogxgQVM9s0i9oloJwjIG-zj6Svsm20&index=5&pp=iAQB) + - [Introduction to Agent Studio](https://www.youtube.com/watch?v=KWarqNq195M&list=PLBgogxgQVM9s0i9oloJwjIG-zj6Svsm20&index=6&pp=iAQB) + - [Vector Search and Embeddings](https://www.youtube.com/watch?v=YlAWtEAJl9g&list=PLBgogxgQVM9s0i9oloJwjIG-zj6Svsm20&index=7&pp=iAQB) +- [Gemini for Google Cloud - Playlist](https://www.youtube.com/playlist?list=PLBgogxgQVM9vMyRWTvDqxc-N5pYp-a98F) + - [Gemini for Application Developers](https://www.youtube.com/watch?v=WsXVGr0Q3C4&list=PLBgogxgQVM9vMyRWTvDqxc-N5pYp-a98F&index=1&pp=iAQB) + - [Gemini for Cloud Architects](https://www.youtube.com/watch?v=y-dlxWHtfhQ&list=PLBgogxgQVM9vMyRWTvDqxc-N5pYp-a98F&index=2&pp=iAQB) + - [Gemini for Data Scientists and Analysts](https://www.youtube.com/watch?v=0H7brO5JeCQ&list=PLBgogxgQVM9vMyRWTvDqxc-N5pYp-a98F&index=3&pp=iAQB) + - [Gemini for Network Engineers](https://www.youtube.com/watch?v=RHla4EEleCE&list=PLBgogxgQVM9vMyRWTvDqxc-N5pYp-a98F&index=4&pp=iAQB) + - [Gemini for Security Engineers](https://www.youtube.com/watch?v=08xeOzUUL-g&list=PLBgogxgQVM9vMyRWTvDqxc-N5pYp-a98F&index=5&pp=iAQB) + - [Gemini for Cloud DevOps Engineers](https://www.youtube.com/watch?v=zaVTJVwtyzI&list=PLBgogxgQVM9vMyRWTvDqxc-N5pYp-a98F&index=6&pp=iAQB) + - [Gemini end-to-end Software Development Lifecycle](https://www.youtube.com/watch?v=h41eoDraUzE&list=PLBgogxgQVM9vMyRWTvDqxc-N5pYp-a98F&index=7&pp=iAQB) + - [Technical training courses in Generative AI from Google Cloud](https://www.youtube.com/watch?v=5FIlXPSmUUc&list=PLBgogxgQVM9vMyRWTvDqxc-N5pYp-a98F&index=8&pp=iAQB) + +## Cloud Skills Boost + +- [Beginner: Introduction to Generative AI Learning Path](https://www.cloudskillsboost.google/paths/118): This learning path provides an overview of generative AI concepts, from the fundamentals of large language models to responsible AI principles. This learning path currently has several courses listed below: + + - [Introduction to Generative AI](https://www.cloudskillsboost.google/course_templates/536) + - [Introduction to Large Language Models](https://www.cloudskillsboost.google/course_templates/539) + - [Introduction to Responsible AI](https://www.cloudskillsboost.google/course_templates/554) + - [Prompt Design](https://www.cloudskillsboost.google/paths/118/course_templates/976) + - [Responsible AI: Applying AI Principles with Google Cloud](https://www.cloudskillsboost.google/paths/118/course_templates/388) + +- [Intermediate: Gemini for Google Cloud Learning Path](https://www.cloudskillsboost.google/paths/236): The Gemini for Google Cloud learning path provides examples of how Gemini can help make engineers of all types more efficient in their daily activities. + + - [Gemini for Application Developers](https://www.cloudskillsboost.google/course_templates/881) + - [Gemini for Cloud Architects](https://www.cloudskillsboost.google/paths/236/course_templates/878) + - [Gemini for Data Scientists and Analysts](https://www.cloudskillsboost.google/paths/236/course_templates/879) + - [Gemini for Network Engineers](https://www.cloudskillsboost.google/paths/236/course_templates/884) + - [Gemini for Security Engineers](https://www.cloudskillsboost.google/paths/236/course_templates/886) + - [Gemini for DevOps Engineers](https://www.cloudskillsboost.google/paths/236/course_templates/882) + - [Gemini for end-to-end SDLC](https://www.cloudskillsboost.google/course_templates/885) + - [Develop Gen AI Apps with Gemini and Streamlit](https://www.cloudskillsboost.google/paths/236/course_templates/978) + +- [Advanced: Generative AI for Developers Learning Path](https://www.cloudskillsboost.google/paths/183): A Generative AI Learning Path with a technical focus, built for App Developers, Machine Learning Engineers, and Data Scientists. Recommended prerequisite: Introduction to Generative AI learning path. + - [Introduction to Image Generation](https://www.cloudskillsboost.google/paths/183/course_templates/541) + - [Attention Mechanism](https://www.cloudskillsboost.google/paths/183/course_templates/537) + - [Encoder-Decoder Architecture](https://www.cloudskillsboost.google/paths/183/course_templates/543) + - [Transformer Models and BERT Model](https://www.cloudskillsboost.google/paths/183/course_templates/538) + - [Create Image Captioning Models](https://www.cloudskillsboost.google/paths/183/course_templates/542) + - [Introduction to Agent Studio](https://www.cloudskillsboost.google/paths/183/course_templates/552) + - [Vector Search and Embeddings](https://www.cloudskillsboost.google/paths/183/course_templates/939) + - [Inspect Rich Documents with Gemini Multimodality and Multimodal RAG](https://www.cloudskillsboost.google/paths/183/course_templates/981) + - [Responsible AI for Developers: Fairness and Bias](https://www.cloudskillsboost.google/paths/183/course_templates/985) + - [Responsible AI for Developers: Interpretability & Transparency](https://www.cloudskillsboost.google/paths/183/course_templates/989) + - [Machine Learning Operations (MLOps) for Generative AI](https://www.cloudskillsboost.google/paths/183/course_templates/927) + +## Google Cloud Playlists/Videos + +- [Generative AI - Google Cloud Next 2024](https://www.youtube.com/watch?v=PyM3Vt6s1GI&list=PLIivdWyY5sqLHERDVwseyPGka96mCiwpq) +- [AI and Machine Learning with Google Cloud](https://www.youtube.com/watch?v=V6YTS5aofqU&list=PLIivdWyY5sqJdmVMjLI8iCul14XkTRosn) diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..8b58ae9 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,7 @@ +# Security Policy + +To report a security issue, please use [g.co/vulnz](https://g.co/vulnz). + +The Google Security Team will respond within 5 working days of your report on g.co/vulnz. + +We use g.co/vulnz for our intake, and do coordination and disclosure here using GitHub Security Advisory to privately discuss and fix the issue. diff --git a/agents/README.md b/agents/README.md new file mode 100644 index 0000000..45e6f89 --- /dev/null +++ b/agents/README.md @@ -0,0 +1,31 @@ +# Agents + +This directory contains samples and resources for building AI agents with Google Cloud. + +## Subdirectories + +### adk + +The `adk` directory contains information about the Agent Development Kit (ADK). + +### agent_engine + +The `agent_engine` directory contains notebooks that demonstrate how to use the Agent Engine on Vertex AI. + +**Notebooks:** + +- [Get started with Vertex AI Memory Bank](agent_engine/memory_bank/get_started_with_memory_bank.ipynb): Learn how to use Vertex AI Memory Bank to build stateful, context-aware conversational AI agents. +- [A2A on Agent Engine](agent_engine/tutorial_a2a_on_agent_engine.ipynb): A tutorial on how to run a simple agent on Agent Engine. +- [Claude with ADK on Agent Engine](agent_engine/tutorial_claude_with_adk_on_agent_engine.ipynb): A tutorial on using the Claude model with ADK on Agent Engine. +- [Get Started with Code Execution](agent_engine/tutorial_get_started_with_code_execution.ipynb): A tutorial on how to get started with code execution on Agent Engine. +- [Get Started with Live API on Agent Engine](agent_engine/tutorial_get_started_with_live_api_on_agent_engine.ipynb): A tutorial on how to get started with a live API on Agent Engine. +- [MCP on Agent Engine](agent_engine/tutorial_mcp_on_agent_engine.ipynb): A tutorial on how to use the Multi-turn Conversation Platform (MCP) on Agent Engine. + +### gemini_data_analytics + +The `gemini_data_analytics` directory contains notebooks that demonstrate how to use Gemini for data analytics. + +**Notebooks:** + +- [Intro to Gemini Data Analytics (HTTP)](gemini_data_analytics/intro_gemini_data_analytics_http.ipynb): An introduction to using Gemini for data analytics with the HTTP API. +- [Intro to Gemini Data Analytics (SDK)](gemini_data_analytics/intro_gemini_data_analytics_sdk.ipynb): An introduction to using Gemini for data analytics with the Python SDK. diff --git a/agents/adk/README.md b/agents/adk/README.md new file mode 100644 index 0000000..d42cdb2 --- /dev/null +++ b/agents/adk/README.md @@ -0,0 +1,6 @@ +⚠ The Agent Development Kit (ADK) Sample Agents repository is available at [github.com/google/adk-samples](https://github.com/google/adk-samples). + +The repository contains sample agents for: + +- [Python](https://github.com/google/adk-samples/tree/main/python) +- [Java](https://github.com/google/adk-samples/tree/main/java) diff --git a/agents/adk/contract-compliance-pipeline/ARCHITECTURE.md b/agents/adk/contract-compliance-pipeline/ARCHITECTURE.md new file mode 100644 index 0000000..bae1bd4 --- /dev/null +++ b/agents/adk/contract-compliance-pipeline/ARCHITECTURE.md @@ -0,0 +1,157 @@ +# Architecture: Contract Compliance Multi-Agent Team + +This document describes the executable architecture in the repo today. The live demo is a local cross-language system: + +- Browser cockpit served by Python at `/live-compliance/` +- Python FastAPI service on `127.0.0.1:8000` +- Go A2A compliance service on `:8888` +- ADK `RemoteA2aAgent` handoff through the Go Agent Card and A2A JSON-RPC `SendMessage` + +The Go service is deterministic by design. It is not an LLM agent; it enforces policy thresholds that need repeatable audit behavior. + +## Runtime Flow + +```mermaid +flowchart LR + UI["Browser Cockpit
/live-compliance/"] + API["Python FastAPI
/api/compliance/upload"] + EXTRACT["Deterministic Extraction
tools.py"] + ADK["ADK RemoteA2aAgent
fast_api_app.py"] + CARD["Go Agent Card
/.well-known/agent.json"] + RPC["Go JSON-RPC
SendMessage"] + CHECK["Policy Checker
checker.go"] + ART["Case + HTML Artifacts
live_compliance.py"] + + UI -->|"sample text + policy + simulator state"| API + API --> EXTRACT + EXTRACT --> ADK + ADK -->|"GET Agent Card"| CARD + ADK -->|"POST JSON-RPC SendMessage"| RPC + RPC --> CHECK + CHECK -->|"passed / violations / timestamp"| RPC + RPC --> ADK + ADK --> API + API --> ART + ART --> UI +``` + +## Live Request Path + +1. The browser selects a bundled contract fixture from `sample-contracts/`. +2. The browser fetches fixture text from `/api/compliance/sample-contracts/{filename}`. +3. The browser posts the text, active policy values, and simulator settings to `/api/compliance/upload`. +4. Python extracts contract fields with deterministic parsing in `tools.py`. +5. Python classifies risk and builds an A2A data payload. +6. Python creates a focused `RemoteA2aAgent` in `fast_api_app.py`. +7. ADK resolves the Go Agent Card from `GO_AGENT_CARD_URL`. +8. ADK sends A2A JSON-RPC `SendMessage` to the Go service. +9. Go decodes the data part, applies deterministic policy checks, and returns a completed A2A task. +10. Python stores the case state, generates HTML artifacts, and returns the UI-visible payload. + +## A2A Payload Shape + +Python builds the UI-visible request envelope in `build_go_message_payload(...)`: + +```json +{ + "jsonrpc": "2.0", + "id": "case-{case_id}", + "method": "SendMessage", + "params": { + "metadata": { + "task_id": "{case_id}" + }, + "message": { + "messageId": "case-{case_id}-request", + "taskId": "{case_id}", + "role": "ROLE_USER", + "parts": [ + { + "data": { + "schema_version": "contract-compliance.a2a.v1", + "case_id": "{case_id}", + "contract": { + "contract_value": 250000.0, + "contractor_name": "ACME CLOUD SOLUTIONS", + "insurance_coverage": 2000000.0, + "liability_limit": "$1,000,000.00", + "term_length_years": 2, + "auto_renewal": false, + "has_termination_clause": true + }, + "policy": { + "max_contract_value": 500000.0, + "required_insurance_minimum": 1000000.0, + "max_term_years": 5, + "required_termination_clause": true, + "prohibited_clauses": ["unlimited liability", "auto-renewal > 3yr"] + } + }, + "mediaType": "application/json" + } + ] + } + } +} +``` + +Go returns a completed A2A task whose status message contains a data part: + +```json +{ + "passed": false, + "violations": [ + "Contract value $850000.00 exceeds company framework limit of $500000.00" + ], + "verdict_timestamp": "2026-06-03T00:00:00Z" +} +``` + +## State Outcomes + +The live cockpit maps Go results into three visible outcomes: + +| Outcome | Trigger | +|:---|:---| +| `APPROVED` | Go returns `passed: true`. | +| `REVIEW_READY` | Go returns `passed: false` with policy violations. | +| `MANUAL_REVIEW` | Go is unavailable or simulator mode is `Crashed (503)`. | + +The richer enum in `state_schema.py` still includes intermediate states used by the fuller ADK reference path, but the cockpit completes the healthy path in one API call. + +## Trust Boundaries + +Browser: + +- Chooses bundled sample contracts. +- Sends policy override values. +- Never calls the Go service directly. + +Python service: + +- Enforces file extension and 5MB upload limits. +- Rejects binary PDF uploads; bundled `.pdf` files are text fixtures. +- Resolves sample and artifact paths with basename and root-bound checks. +- Calls only the configured Go Agent Card URL. +- Fails closed to `MANUAL_REVIEW` when the Go handoff fails. + +Go service: + +- Serves an Agent Card at `/.well-known/agent.json`. +- Accepts JSON-RPC POST requests. +- Handles current `SendMessage` plus legacy `tasks/send` and `tasks/get`. +- Applies deterministic policy rules from `default_policy.json` or request policy overrides. + +## Key Files + +| File | Role | +|:---|:---| +| `python-extraction-agent/app/static/live-compliance/index.html` | Browser cockpit. | +| `python-extraction-agent/app/fast_api_app.py` | API routes, ADK handoff, case response. | +| `python-extraction-agent/app/tools.py` | Deterministic extraction and risk classification. | +| `python-extraction-agent/app/live_compliance.py` | Case state, events, artifact generation. | +| `python-extraction-agent/app/agent.py` | Fuller ADK `SequentialAgent` reference. | +| `go-compliance-agent/internal/agentcard/card.go` | Agent Card. | +| `go-compliance-agent/internal/handler/task_handler.go` | A2A JSON-RPC handler. | +| `go-compliance-agent/internal/compliance/checker.go` | Deterministic policy checker. | +| `go-compliance-agent/internal/policies/default_policy.json` | Default policy thresholds. | diff --git a/agents/adk/contract-compliance-pipeline/LICENSE b/agents/adk/contract-compliance-pipeline/LICENSE new file mode 100644 index 0000000..0714159 --- /dev/null +++ b/agents/adk/contract-compliance-pipeline/LICENSE @@ -0,0 +1,191 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to the Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by the Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding any notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + Copyright 2026 Google LLC + + 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 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/agents/adk/contract-compliance-pipeline/README.md b/agents/adk/contract-compliance-pipeline/README.md new file mode 100644 index 0000000..60fd18a --- /dev/null +++ b/agents/adk/contract-compliance-pipeline/README.md @@ -0,0 +1,206 @@ +# 📑 Contract Compliance Multi-Agent Team (ADK & A2A) + +**Authors** + +* [Shubham Saboo](https://github.com/Shubhamsaboo) +* [Eric Dong](https://github.com/gericdong) + +This Contract Compliance Engine demonstrates key principles for architecting cross-language, auditable compliance agents. The project contains a Python FastAPI service built with Google ADK for intake, deterministic extraction, and the `RemoteA2aAgent` handoff, plus a Go A2A compliance service that enforces policy rules with repeatable code. + +The live demo UI gives contract reviewers a polished cockpit for selecting sample vendor agreements, running compliance audits, watching the Python-to-Go A2A handoff, simulating remote-agent failure modes, and opening generated compliance artifacts outside the raw ADK console. + +Contract Compliance Engine live demo + +## Overview + +Most agent demos overuse LLMs for every step, including places where deterministic code is a better engineering choice. Contract compliance is one of those places. If the policy says vendor agreements cannot exceed `$500,000`, cannot run longer than `5` years, and must include an exit clause, those checks should be auditable and repeatable. + +This repository demonstrates that split of responsibility: + +- **Python FastAPI + ADK** handles intake, deterministic extraction, risk classification, session state, artifacts, and the focused `RemoteA2aAgent` handoff. +- **Go A2A compliance agent** exposes an Agent Card and validates extracted contract fields through JSON-RPC `SendMessage`. +- **Contract Compliance Engine UI** shows the full pipeline: contract selection, generated artifacts, live A2A payload, Go verdict, trace spans, simulator controls, and the active compliance policy summary. + +The core lesson: + +> LLMs are useful for ambiguity. Deterministic agents should enforce hard policy. + +The Go service is not an LLM agent. It is a deterministic policy agent exposed through an A2A-compatible interface. + +## System Architecture & Technology Stack + +```mermaid +flowchart TD + subgraph Client ["Client (Frontend)"] + UI["Cockpit Dashboard
(HTML5 / CSS3 / Vanilla JS)"] + end + + subgraph Python ["Orchestration Service (Python)"] + FastAPI["FastAPI App
(:8000)"] + ADK["Google ADK Coordinator
(RemoteA2aAgent)"] + Parser["Deterministic Parser
(Regular Expressions)"] + StateDB["Session DB
(SQLite / aiosqlite)"] + end + + subgraph Go ["Compliance Service (Go)"] + Server["Go HTTP Server
(:8888)"] + RPC["JSON-RPC 2.0 Handler"] + Checker["Deterministic Policy Engine"] + end + + UI -->|"Upload / Run request"| FastAPI + FastAPI --> Parser + FastAPI --> ADK + ADK -->|"A2A Handshake / RPC"| Server + Server --> RPC + RPC --> Checker + ADK -.->|"Persist checkpoints"| StateDB + Checker -->|"Task status verdict"| ADK + FastAPI -->|"Render HTML certificate"| UI +``` + +### Core Components & Technologies + +* **Frontend Dashboard (HTML5 / CSS3 / JavaScript)**: A rich, interactive user interface featuring code syntax highlighting, live state trace timelines, real-time payload visualizers, and an integrated policy overrides editor. +* **Orchestration Service (Python 3.13 / FastAPI / Google ADK)**: + * **FastAPI**: Serves the cockpit UI, manages upload end-points, and simulates service latencies/outages. + * **Google ADK (Agent Development Kit)**: Orchestrates the multi-agent execution pipeline and abstracts remote service communication via `RemoteA2aAgent`. + * **SQLite / aiosqlite**: Stores session logs, execution traces, and agent-state checkpoints. +* **Compliance Validator Service (Go / JSON-RPC 2.0)**: + * **Go Standard Library (`net/http`)**: High-performance HTTP server that exposes the A2A Agent Card config (`/.well-known/agent.json`). + * **JSON-RPC 2.0**: The protocol layer used for agent-to-agent communication (supporting `SendMessage`, `tasks/send`, and `tasks/get`). + * **Go Policy Checker**: Performs synchronous, auditable compliance validation against company thresholds. + +## Quick Start + +Terminal 1, start the Go A2A compliance agent: + +```bash +cd go-compliance-agent +go run cmd/server/main.go +``` + +Terminal 2, start the Python FastAPI cockpit: + +```bash +cd python-extraction-agent +uv sync +uv run uvicorn app.fast_api_app:app --host 127.0.0.1 --port 8000 +``` + +Open the live cockpit: + +```text +http://127.0.0.1:8000/live-compliance/ +``` + +This live cockpit path does **not** require a Gemini API key. The extraction fixtures and Go policy checks are deterministic so the demo stays reproducible. + +Docker Compose is also available: + +```bash +docker-compose up --build +``` + +## How To Use The Demo + +1. Open `/live-compliance/`. +2. Select one of the bundled vendor contracts. +3. Keep **A2A Simulator Mode** on `Healthy` for the normal path. +4. Click **Run Pipeline Audit**. +5. Watch **Agent Exchange** populate with the Python-to-Go `SendMessage` handoff. +6. Open the generated artifacts: + - legal parameters sheet + - Go compliance certificate + +The UI sends real contract text and active policy settings to Python. Python extracts deterministic contract facts, ADK performs the A2A handoff, and Go returns an auditable policy verdict. + +## Sample Outcomes + +| Contract | Expected Result | Why | +|:---|:---|:---| +| `standard-vendor-agreement.pdf` | Pass | Value, insurance, term, liability, renewal, and exit terms are within policy. | +| `high-risk-liability-contract.pdf` | Review | Unlimited liability language is prohibited by the Go policy checker. | +| `non-compliant-contract.pdf` | Review | Value, insurance, term, auto-renewal, liability, and exit-safety rules fail. | + +The files in `sample-contracts/` are plain-text fixtures with `.pdf` names. That keeps the demo inspectable without hiding behavior behind PDF parsing. + +## What Actually Runs + +```mermaid +flowchart LR + USER["End User
Contract Compliance Engine UI"] + PY["Python FastAPI (:8000)
intake, extraction, risk"] + ADK["ADK RemoteA2aAgent
Agent Card + SendMessage"] + GO["Go A2A Compliance Agent (:8888)
deterministic policy checks"] + OUT["Returned Case
verdict, trace, artifacts"] + + USER -->|"Select contract + run audit"| PY + PY -->|"Extract facts + attach active policy"| ADK + ADK -->|"GET /.well-known/agent.json
POST JSON-RPC SendMessage"| GO + GO -->|"Completed A2A Task
pass/review + violations"| ADK + ADK -->|"Go verdict"| PY + PY -->|"Case payload + artifact links"| OUT + OUT -->|"Render results"| USER +``` + +The executable cockpit uses `python-extraction-agent/app/fast_api_app.py` for the live browser path. `python-extraction-agent/app/agent.py` remains as a fuller ADK `SequentialAgent` reference for extract -> comply -> report. + +For the detailed topology and sequence diagram, see [ARCHITECTURE.md](./ARCHITECTURE.md). + +## Active Policy + +The live cockpit summarizes the active Go policy as: + +```text +Value Cap: $500k +Term Limit: 5 yrs +Insurance: $1M+ +Rules: Exit clause, no unlimited liability, no >3yr renewal +``` + +Those values are not decorative. The UI sends them as `custom_policies`; FastAPI includes them inside the A2A data part; Go uses them as the effective policy for that audit. + +## Key Files + +| File | Purpose | +|:---|:---| +| `python-extraction-agent/app/static/live-compliance/index.html` | Live Contract Compliance Engine UI. | +| `python-extraction-agent/app/fast_api_app.py` | FastAPI upload API, sample routes, live ADK `RemoteA2aAgent` handoff. | +| `python-extraction-agent/app/tools.py` | Deterministic extraction and risk classification. | +| `python-extraction-agent/app/live_compliance.py` | Case state, event stream, and generated HTML artifacts. | +| `go-compliance-agent/internal/agentcard/card.go` | Go Agent Card at `/.well-known/agent.json`. | +| `go-compliance-agent/internal/handler/task_handler.go` | JSON-RPC `SendMessage`, `tasks/send`, and `tasks/get` handlers. | +| `go-compliance-agent/internal/compliance/checker.go` | Deterministic policy checker. | +| `go-compliance-agent/internal/policies/default_policy.json` | Default compliance policy. | + +## Test Commands + +Run Python unit tests: + +```bash +cd python-extraction-agent +uv run pytest tests/unit -v +``` + +Run Go tests: + +```bash +cd go-compliance-agent +go test -v ./... +``` + +Manual smoke test: + +```bash +curl http://127.0.0.1:8888/.well-known/agent.json +``` + +Then run the live cockpit and confirm: + +- the Agent Exchange panel shows `SendMessage` +- the A2A payload includes `jsonrpc: "2.0"` +- the Go verdict appears in the UI +- the generated compliance certificate artifact renders + + diff --git a/agents/adk/contract-compliance-pipeline/TUTORIAL.md b/agents/adk/contract-compliance-pipeline/TUTORIAL.md new file mode 100644 index 0000000..7458976 --- /dev/null +++ b/agents/adk/contract-compliance-pipeline/TUTORIAL.md @@ -0,0 +1,180 @@ +# Tutorial: Run the Contract Compliance Multi-Agent Demo + +This tutorial walks through the executable demo in this repository. A Python FastAPI service handles contract intake, deterministic extraction, ADK `RemoteA2aAgent` handoff, case state, and artifacts. A Go A2A service enforces deterministic compliance rules and returns a JSON-RPC `SendMessage` task verdict. + +The live cockpit path does not require a Gemini API key. + +## 1. Start the Services + +Start the Go compliance agent: + +```bash +cd go-compliance-agent +go run cmd/server/main.go +``` + +The Go service exposes: + +```text +http://localhost:8888/.well-known/agent.json +http://localhost:8888/ +``` + +Start the Python cockpit: + +```bash +cd python-extraction-agent +uv sync +uv run uvicorn app.fast_api_app:app --host 127.0.0.1 --port 8000 +``` + +Open: + +```text +http://127.0.0.1:8000/live-compliance/ +``` + +## 2. Run the Happy Path + +1. Select `standard-vendor-agreement.pdf`. +2. Keep **A2A Simulator Mode** on `Healthy`. +3. Click **Run Pipeline Audit**. +4. Confirm the Agent Exchange panel shows: + - source: `python-extraction-agent` + - target: `Security Compliance Validator` + - method: `SendMessage` + - a task ID + - a JSON-RPC payload + - a passed Go verdict +5. Open the generated compliance certificate artifact. + +Expected result: the contract passes because value, term, insurance, liability, renewal, and exit terms are within policy. + +## 3. Run the Review Paths + +Test the bundled failure cases: + +| Contract | Expected Result | +|:---|:---| +| `high-risk-liability-contract.pdf` | Review because unlimited liability is prohibited. | +| `non-compliant-contract.pdf` | Review with multiple policy violations. | + +For `non-compliant-contract.pdf`, the UI should show review required with these violation categories: + +- value above `$500k` +- unlimited liability +- auto-renewal longer than 3 years +- insurance below `$1M` +- term longer than 5 years +- missing usable exit clause + +## 4. Simulate A2A Failure + +Use **A2A Simulator Mode** to test remote-agent failure behavior: + +- `Healthy`: normal Go service path. +- `Delayed`: waits before the Go call, bounded by the backend. +- `Crashed (503)`: simulates Go service failure and routes to manual review. + +Expected crashed result: Python fails closed to `MANUAL_REVIEW` and the certificate explains that manual legal review is required. + +## 5. What the Browser Sends + +The UI posts contract text and policy settings to: + +```text +POST /api/compliance/upload +``` + +Important form fields: + +| Field | Purpose | +|:---|:---| +| `file` | Text contract fixture, including `.pdf`-named text samples. | +| `custom_policies` | Active policy values sent to Go. | +| `simulated_latency` | Bounded delay for simulator testing. | +| `simulated_server_state` | `normal` or `crashed`. | + +The browser does not call the Go service directly. + +## 6. What Python Sends to Go + +`python-extraction-agent/app/fast_api_app.py` builds the A2A request in `build_go_message_payload(...)`. + +The live method is: + +```text +SendMessage +``` + +The payload contains: + +- `jsonrpc: "2.0"` +- `task_id` +- extracted contract fields +- active policy settings + +ADK sends the request through `RemoteA2aAgent`; it is not a manual browser-to-Go call. + +## 7. What Go Validates + +`go-compliance-agent/internal/compliance/checker.go` checks: + +- contract value <= `max_contract_value` +- insurance coverage >= `required_insurance_minimum` +- term length <= `max_term_years` +- termination clause present when required +- no prohibited unlimited liability clause +- no prohibited auto-renewal longer than 3 years + +Default thresholds live in: + +```text +go-compliance-agent/internal/policies/default_policy.json +``` + +## 8. Key Code To Read + +| File | Why it matters | +|:---|:---| +| `python-extraction-agent/app/fast_api_app.py` | API routes, request validation, ADK handoff, simulator handling. | +| `python-extraction-agent/app/tools.py` | Deterministic contract extraction and risk classification. | +| `python-extraction-agent/app/live_compliance.py` | Case state and generated HTML artifacts. | +| `python-extraction-agent/app/static/live-compliance/index.html` | Live cockpit UI and A2A payload display. | +| `go-compliance-agent/internal/agentcard/card.go` | Agent Card discovery response. | +| `go-compliance-agent/internal/handler/task_handler.go` | JSON-RPC `SendMessage` and legacy task handlers. | +| `go-compliance-agent/internal/compliance/checker.go` | Deterministic policy verdict. | + +## 9. Test the Demo + +Run Python tests: + +```bash +cd python-extraction-agent +uv run pytest tests/unit -v +``` + +Run Go tests: + +```bash +cd go-compliance-agent +go test -v ./... +``` + +Smoke-test the Agent Card: + +```bash +curl http://127.0.0.1:8888/.well-known/agent.json +``` + +From the repository root, smoke-test a non-compliant upload: + +```bash +curl -sS -X POST http://127.0.0.1:8000/api/compliance/upload \ + -F file=@sample-contracts/non-compliant-contract.pdf \ + -F simulated_latency=0 \ + -F simulated_server_state=normal \ + -F 'custom_policies={"max_contract_value":500000,"required_insurance_minimum":1000000,"max_term_years":5,"required_termination_clause":true,"prohibited_clauses":["unlimited liability","auto-renewal > 3yr"]}' +``` + +Expected result: response JSON contains `passed: false` and a list of Go policy violations. diff --git a/agents/adk/contract-compliance-pipeline/assets/architecture_diagram.png b/agents/adk/contract-compliance-pipeline/assets/architecture_diagram.png new file mode 100644 index 0000000..4ad8321 Binary files /dev/null and b/agents/adk/contract-compliance-pipeline/assets/architecture_diagram.png differ diff --git a/agents/adk/contract-compliance-pipeline/assets/live-compliance-cockpit-a2a.png b/agents/adk/contract-compliance-pipeline/assets/live-compliance-cockpit-a2a.png new file mode 100644 index 0000000..a846d4d Binary files /dev/null and b/agents/adk/contract-compliance-pipeline/assets/live-compliance-cockpit-a2a.png differ diff --git a/agents/adk/contract-compliance-pipeline/assets/live-compliance-cockpit.jpg b/agents/adk/contract-compliance-pipeline/assets/live-compliance-cockpit.jpg new file mode 100644 index 0000000..6cdf857 Binary files /dev/null and b/agents/adk/contract-compliance-pipeline/assets/live-compliance-cockpit.jpg differ diff --git a/agents/adk/contract-compliance-pipeline/assets/ui_mockup.png b/agents/adk/contract-compliance-pipeline/assets/ui_mockup.png new file mode 100644 index 0000000..a505ce8 Binary files /dev/null and b/agents/adk/contract-compliance-pipeline/assets/ui_mockup.png differ diff --git a/agents/adk/contract-compliance-pipeline/docker-compose.yml b/agents/adk/contract-compliance-pipeline/docker-compose.yml new file mode 100644 index 0000000..9432150 --- /dev/null +++ b/agents/adk/contract-compliance-pipeline/docker-compose.yml @@ -0,0 +1,53 @@ +# Copyright 2026 Google LLC +# +# 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 +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +services: + # Python intake, extraction, dashboard, and JSON-RPC handoff service + python-extraction-agent: + build: + context: ./python-extraction-agent + dockerfile: Dockerfile + ports: + - "8000:8000" + volumes: + - ./sample-contracts:/app/sample-contracts + environment: + - GOOGLE_CLOUD_PROJECT=${GOOGLE_CLOUD_PROJECT:-mock-gcp-project} + - GOOGLE_CLOUD_LOCATION=global + # Optional: kept for the ADK reference agent path; not required by the live cockpit. + - GOOGLE_API_KEY=${GOOGLE_API_KEY:-} + # A2A: Point to the Go agent's Agent Card URL via Docker networking + - GO_AGENT_CARD_URL=http://go-compliance-agent:8888/.well-known/agent.json + depends_on: + go-compliance-agent: + condition: service_started + networks: + - pipeline-net + + # Go Security Compliance Agent (A2A Server) + go-compliance-agent: + build: + context: ./go-compliance-agent + dockerfile: Dockerfile + ports: + - "8888:8888" + environment: + - HOST=0.0.0.0 + - AGENT_URL=http://go-compliance-agent:8888 + networks: + - pipeline-net + +networks: + pipeline-net: + driver: bridge diff --git a/agents/adk/contract-compliance-pipeline/go-compliance-agent/Dockerfile b/agents/adk/contract-compliance-pipeline/go-compliance-agent/Dockerfile new file mode 100644 index 0000000..c09eed2 --- /dev/null +++ b/agents/adk/contract-compliance-pipeline/go-compliance-agent/Dockerfile @@ -0,0 +1,41 @@ +# Copyright 2026 Google LLC +# +# 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 +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Stage 1: Build the Go validation engine binary +FROM golang:1.21-alpine AS builder + +WORKDIR /app + +# Copy modules files and cache packages download +COPY go.mod ./ +# Expose default config copies +RUN go mod download + +COPY . . + +RUN CGO_ENABLED=0 GOOS=linux go build -o compliance-server cmd/server/main.go + +# Stage 2: Bundle execution package inside minimal Alpine container +FROM alpine:latest + +RUN apk --no-cache add ca-certificates curl + +WORKDIR /root/ + +COPY --from=builder /app/compliance-server . +COPY --from=builder /app/internal/policies/default_policy.json internal/policies/default_policy.json + +EXPOSE 8888 + +CMD ["./compliance-server", "--port", "8888", "--policy", "internal/policies/default_policy.json"] diff --git a/agents/adk/contract-compliance-pipeline/go-compliance-agent/cmd/server/main.go b/agents/adk/contract-compliance-pipeline/go-compliance-agent/cmd/server/main.go new file mode 100644 index 0000000..b91b52f --- /dev/null +++ b/agents/adk/contract-compliance-pipeline/go-compliance-agent/cmd/server/main.go @@ -0,0 +1,87 @@ +package main + +import ( + "context" + "flag" + "fmt" + "net" + "net/http" + "os" + "os/signal" + "syscall" + "time" + + "go-compliance-agent/internal/agentcard" + "go-compliance-agent/internal/handler" +) + +func main() { + // Parse CLI parameters + port := flag.String("port", "8888", "Go A2A Service listener port") + policyPath := flag.String("policy", "internal/policies/default_policy.json", "Compliance rules JSON file path") + flag.Parse() + + fmt.Println("--- Go Compliance Agent (A2A Protocol) ---") + fmt.Printf("Bootstrapping validation policies from %s\n", *policyPath) + handler.InitPolicies(*policyPath) + + // API Routing definition — A2A uses a single JSON-RPC endpoint at root. + // The agent card endpoint must be registered first so it takes priority. + mux := http.NewServeMux() + mux.HandleFunc("/.well-known/agent.json", agentcard.Handler) + mux.HandleFunc("/", handler.HandleJSONRPC) + + // Global Middleware enabling CORS checks + corsHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Access-Control-Allow-Origin", "*") + w.Header().Set("Access-Control-Allow-Methods", "POST, GET, OPTIONS") + w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Accept") + + if r.Method == http.MethodOptions { + w.WriteHeader(http.StatusOK) + return + } + mux.ServeHTTP(w, r) + }) + + // Bind address — configurable via HOST env var, defaults to 0.0.0.0 for container compatibility + host := os.Getenv("HOST") + if host == "" { + host = "0.0.0.0" + } + bindAddr := net.JoinHostPort(host, *port) + + server := &http.Server{ + Addr: bindAddr, + Handler: corsHandler, + } + + // Graceful shutdown listeners + idleConnsClosed := make(chan struct{}) + go func() { + sigChan := make(chan os.Signal, 1) + signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM) + <-sigChan + + fmt.Println("\nReceived termination signal. Shutting down A2A service gracefully...") + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + if err := server.Shutdown(ctx); err != nil { + fmt.Printf("Error shutting down server: %v\n", err) + } + close(idleConnsClosed) + }() + + fmt.Printf("Agent Card: http://%s/.well-known/agent.json\n", bindAddr) + fmt.Printf("JSON-RPC: http://%s/\n", bindAddr) + fmt.Printf("A2A compliance validation engine listening on %s...\n", bindAddr) + + if err := server.ListenAndServe(); err != http.ErrServerClosed { + fmt.Printf("Error starting server: %v\n", err) + os.Exit(1) + } + + <-idleConnsClosed + fmt.Println("Security validation service exited safely.") +} diff --git a/agents/adk/contract-compliance-pipeline/go-compliance-agent/go.mod b/agents/adk/contract-compliance-pipeline/go-compliance-agent/go.mod new file mode 100644 index 0000000..b05bef1 --- /dev/null +++ b/agents/adk/contract-compliance-pipeline/go-compliance-agent/go.mod @@ -0,0 +1,3 @@ +module go-compliance-agent + +go 1.21 diff --git a/agents/adk/contract-compliance-pipeline/go-compliance-agent/internal/agentcard/card.go b/agents/adk/contract-compliance-pipeline/go-compliance-agent/internal/agentcard/card.go new file mode 100644 index 0000000..5540dbf --- /dev/null +++ b/agents/adk/contract-compliance-pipeline/go-compliance-agent/internal/agentcard/card.go @@ -0,0 +1,88 @@ +package agentcard + +import ( + "encoding/json" + "net/http" + "os" +) + +// AgentCard describes the agent's capabilities following the A2A protocol specification. +// Served at /.well-known/agent.json for discovery by other agents. +type AgentCard struct { + Name string `json:"name"` + Description string `json:"description"` + Version string `json:"version"` + SupportedInterfaces []AgentInterface `json:"supportedInterfaces"` + Capabilities Capabilities `json:"capabilities"` + DefaultInputModes []string `json:"defaultInputModes"` + DefaultOutputModes []string `json:"defaultOutputModes"` + Skills []Skill `json:"skills"` +} + +// AgentInterface specifies a transport mechanism and protocol version for agent communication. +type AgentInterface struct { + URL string `json:"url"` + ProtocolBinding string `json:"protocolBinding"` + ProtocolVersion string `json:"protocolVersion"` +} + +// Capabilities declares which optional A2A features the agent supports. +type Capabilities struct { + ExtendedAgentCard bool `json:"extendedAgentCard"` +} + +// Skill describes a specific capability the agent can perform. +type Skill struct { + ID string `json:"id"` + Name string `json:"name"` + Description string `json:"description"` + Tags []string `json:"tags"` + Examples []string `json:"examples"` +} + +// GetCard returns the preloaded Go Compliance service Agent Card configuration structure. +func GetCard() AgentCard { + // Allow the agent URL to be configured via environment variable for deployment flexibility. + agentURL := os.Getenv("AGENT_URL") + if agentURL == "" { + agentURL = "http://localhost:8888" + } + + return AgentCard{ + Name: "Security Compliance Validator", + Description: "Go-based validation engine that checks vendor contracts against corporate compliance policy rules. Accepts extracted contract fields and returns pass/fail verdict with specific violations.", + Version: "1.0.0", + SupportedInterfaces: []AgentInterface{ + { + URL: agentURL, + ProtocolBinding: "JSONRPC", + ProtocolVersion: "1.0", + }, + }, + Capabilities: Capabilities{ + ExtendedAgentCard: false, + }, + DefaultInputModes: []string{"application/json"}, + DefaultOutputModes: []string{"application/json"}, + Skills: []Skill{ + { + ID: "contract_compliance_check", + Name: "Contract Compliance Check", + Description: "Validates extracted contract fields against corporate policy rules. Returns pass/fail verdict with specific violations.", + Tags: []string{"compliance", "contract", "validation"}, + Examples: []string{ + "Check this contract for compliance violations", + "Validate vendor agreement terms against policy", + }, + }, + }, + } +} + +// Handler serves agent card configurations as JSON objects. +func Handler(w http.ResponseWriter, r *http.Request) { + card := GetCard() + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _ = json.NewEncoder(w).Encode(card) +} diff --git a/agents/adk/contract-compliance-pipeline/go-compliance-agent/internal/compliance/checker.go b/agents/adk/contract-compliance-pipeline/go-compliance-agent/internal/compliance/checker.go new file mode 100644 index 0000000..b66982d --- /dev/null +++ b/agents/adk/contract-compliance-pipeline/go-compliance-agent/internal/compliance/checker.go @@ -0,0 +1,107 @@ +package compliance + +import ( + "encoding/json" + "fmt" + "os" + "strings" + "time" +) + +// Policy represents compliance validation rules configuration. +type Policy struct { + MaxContractValue float64 `json:"max_contract_value"` + ProhibitedClauses []string `json:"prohibited_clauses"` + RequiredInsuranceMinimum float64 `json:"required_insurance_minimum"` + MaxTermYears int `json:"max_term_years"` + RequiredTerminationClause bool `json:"required_termination_clause"` +} + +// ContractDetails matches legal fields extracted by the Python subagent. +type ContractDetails struct { + ContractValue float64 `json:"contract_value"` + ContractorName string `json:"contractor_name"` + ClientName string `json:"client_name"` + StartDate string `json:"start_date"` + EndDate string `json:"end_date"` + LiabilityLimit string `json:"liability_limit"` + InsuranceCoverage float64 `json:"insurance_coverage"` + AutoRenewal bool `json:"auto_renewal"` + HasTerminationClause bool `json:"has_termination_clause"` + TermLengthYears int `json:"term_length_years"` +} + +// ComplianceResult holds safety pass audit verdicts and violation details. +type ComplianceResult struct { + Passed bool `json:"passed"` + Violations []string `json:"violations"` + VerdictTimestamp string `json:"verdict_timestamp"` +} + +// LoadPolicy reads legal policy metrics from local file paths. +func LoadPolicy(path string) (Policy, error) { + var policy Policy + bytes, err := os.ReadFile(path) + if err != nil { + return policy, fmt.Errorf("error reading policy file: %w", err) + } + err = json.Unmarshal(bytes, &policy) + if err != nil { + return policy, fmt.Errorf("error decoding policy JSON: %w", err) + } + return policy, nil +} + +// CheckCompliance evaluates contract parameters against standard corporate policies. +func CheckCompliance(details ContractDetails, policy Policy) ComplianceResult { + violations := make([]string, 0) + + // 1. Contract Value Audit + if details.ContractValue > policy.MaxContractValue { + violations = append(violations, fmt.Sprintf( + "Contract value $%.2f exceeds company framework limit of $%.2f", + details.ContractValue, policy.MaxContractValue, + )) + } + + // 2. Prohibited Clauses (Unlimited liability checks) + liabilityUpper := strings.ToLower(details.LiabilityLimit) + for _, clause := range policy.ProhibitedClauses { + if clause == "unlimited liability" { + if strings.Contains(liabilityUpper, "unlimited") || strings.Contains(liabilityUpper, "waived") { + violations = append(violations, "Unlimited contractor liability clauses are strictly prohibited by company policy") + } + } + if clause == "auto-renewal > 3yr" && details.AutoRenewal && details.TermLengthYears > 3 { + violations = append(violations, "Auto-renewal spans extending beyond 3 years are unauthorized without general counsel signatures") + } + } + + // 3. Liability Insurance Bounds Check + if details.InsuranceCoverage < policy.RequiredInsuranceMinimum { + violations = append(violations, fmt.Sprintf( + "Insurance coverage $%.2f is under the security minimum threshold of $%.2f", + details.InsuranceCoverage, policy.RequiredInsuranceMinimum, + )) + } + + // 4. Term Length Limits + if details.TermLengthYears > policy.MaxTermYears { + violations = append(violations, fmt.Sprintf( + "Engagement length of %d years exceeds framework limit bounds of %d years", + details.TermLengthYears, policy.MaxTermYears, + )) + } + + // 5. Written Exit Notice Audit + if policy.RequiredTerminationClause && !details.HasTerminationClause { + violations = append(violations, "Missing required written exit termination safety clauses") + } + + passed := len(violations) == 0 + return ComplianceResult{ + Passed: passed, + Violations: violations, + VerdictTimestamp: time.Now().UTC().Format(time.RFC3339), + } +} diff --git a/agents/adk/contract-compliance-pipeline/go-compliance-agent/internal/compliance/checker_test.go b/agents/adk/contract-compliance-pipeline/go-compliance-agent/internal/compliance/checker_test.go new file mode 100644 index 0000000..16462c5 --- /dev/null +++ b/agents/adk/contract-compliance-pipeline/go-compliance-agent/internal/compliance/checker_test.go @@ -0,0 +1,117 @@ +package compliance + +import ( + "strings" + "testing" +) + +func TestCheckCompliance_Pass(t *testing.T) { + policy := Policy{ + MaxContractValue: 500000.0, + ProhibitedClauses: []string{"unlimited liability", "auto-renewal > 3yr"}, + RequiredInsuranceMinimum: 1000000.0, + MaxTermYears: 5, + RequiredTerminationClause: true, + } + + // Acme Cloud Solutions values (Should Pass) + contract := ContractDetails{ + ContractValue: 250000.0, + ContractorName: "Acme Cloud Solutions", + ClientName: "GFD Platform Systems", + StartDate: "2026-06-01", + EndDate: "2028-06-01", + LiabilityLimit: "$1,000,000.00 standard cap", + InsuranceCoverage: 2000000.0, + AutoRenewal: false, + HasTerminationClause: true, + TermLengthYears: 2, + } + + result := CheckCompliance(contract, policy) + if !result.Passed { + t.Errorf("Expected compliance check to PASS, but got FAIL. Violations: %v", result.Violations) + } + if len(result.Violations) != 0 { + t.Errorf("Expected 0 violations, got %d: %v", len(result.Violations), result.Violations) + } +} + +func TestCheckCompliance_UnlimitedLiability(t *testing.T) { + policy := Policy{ + MaxContractValue: 500000.0, + ProhibitedClauses: []string{"unlimited liability"}, + RequiredInsuranceMinimum: 1000000.0, + MaxTermYears: 5, + RequiredTerminationClause: true, + } + + // Apex Data Systems (Should Fail due to unlimited liability) + contract := ContractDetails{ + ContractValue: 450000.0, + ContractorName: "Apex Data Systems", + ClientName: "GFD Platform Systems", + StartDate: "2026-06-01", + EndDate: "2027-06-01", + LiabilityLimit: "Contractor liability shall be entirely unlimited under all conditions", + InsuranceCoverage: 1500000.0, + AutoRenewal: false, + HasTerminationClause: true, + TermLengthYears: 1, + } + + result := CheckCompliance(contract, policy) + if result.Passed { + t.Errorf("Expected compliance check to FAIL, but got PASS.") + } + + hasLiabilityViolation := false + for _, v := range result.Violations { + if strings.Contains(v, "Unlimited contractor liability") { + hasLiabilityViolation = true + } + } + if !hasLiabilityViolation { + t.Errorf("Expected unlimited liability violation, but got none in: %v", result.Violations) + } +} + +func TestCheckCompliance_MultiViolations(t *testing.T) { + policy := Policy{ + MaxContractValue: 500000.0, + ProhibitedClauses: []string{"unlimited liability", "auto-renewal > 3yr"}, + RequiredInsuranceMinimum: 1000000.0, + MaxTermYears: 5, + RequiredTerminationClause: true, + } + + // Legacy Networks Corp — triggers all 6 violations: + // 1. Contract value exceeds limit + // 2. Unlimited liability (waived) + // 3. Auto-renewal > 3yr + // 4. Insurance below minimum + // 5. Term length exceeds max + // 6. Missing termination clause + contract := ContractDetails{ + ContractValue: 850000.0, + ContractorName: "Legacy Networks Corp", + ClientName: "GFD Platform Systems", + StartDate: "2026-06-01", + EndDate: "2032-06-01", + LiabilityLimit: "Contractor GFD liability limits are waived, resulting in unlimited GFD exposure", + InsuranceCoverage: 500000.0, + AutoRenewal: true, + HasTerminationClause: false, + TermLengthYears: 6, + } + + result := CheckCompliance(contract, policy) + if result.Passed { + t.Errorf("Expected compliance check to FAIL, but got PASS.") + } + + expectedViolations := 6 + if len(result.Violations) != expectedViolations { + t.Errorf("Expected %d violations, got %d: %v", expectedViolations, len(result.Violations), result.Violations) + } +} diff --git a/agents/adk/contract-compliance-pipeline/go-compliance-agent/internal/handler/task_handler.go b/agents/adk/contract-compliance-pipeline/go-compliance-agent/internal/handler/task_handler.go new file mode 100644 index 0000000..7736b94 --- /dev/null +++ b/agents/adk/contract-compliance-pipeline/go-compliance-agent/internal/handler/task_handler.go @@ -0,0 +1,473 @@ +package handler + +import ( + "bytes" + "encoding/json" + "fmt" + "net/http" + "sync" + "time" + + "go-compliance-agent/internal/compliance" +) + +// --- JSON-RPC 2.0 request/response structures per A2A protocol specification --- + +// JSONRPCRequest is the standard JSON-RPC 2.0 request envelope. +// The A2A protocol uses JSON-RPC as its wire format for all agent communication. +type JSONRPCRequest struct { + JSONRPC string `json:"jsonrpc"` + ID interface{} `json:"id"` + Method string `json:"method"` + Params json.RawMessage `json:"params"` +} + +// JSONRPCResponse is the standard JSON-RPC 2.0 response envelope. +type JSONRPCResponse struct { + JSONRPC string `json:"jsonrpc"` + ID interface{} `json:"id"` + Result interface{} `json:"result,omitempty"` + Error *JSONRPCError `json:"error,omitempty"` +} + +// JSONRPCError carries error details inside a JSON-RPC response. +type JSONRPCError struct { + Code int `json:"code"` + Message string `json:"message"` +} + +// --- A2A Task, Message, and Part structures --- + +// MessageSendParams contains the parameters for the current A2A message/send method. +type MessageSendParams struct { + Message Message `json:"message"` + Metadata map[string]interface{} `json:"metadata,omitempty"` +} + +// TaskSendParams contains the parameters for the legacy tasks/send JSON-RPC method. +type TaskSendParams struct { + ID string `json:"id"` + Message Message `json:"message"` +} + +// TaskGetParams contains the parameters for the tasks/get JSON-RPC method. +type TaskGetParams struct { + ID string `json:"id"` +} + +// Task represents an A2A task with its current status and any produced artifacts. +type Task struct { + ContextID string `json:"contextId"` + ID string `json:"id"` + Kind string `json:"kind,omitempty"` + Metadata map[string]interface{} `json:"metadata,omitempty"` + Status TaskStatus `json:"status"` + Artifacts []Artifact `json:"artifacts,omitempty"` + CreatedAt string `json:"createdAt,omitempty"` + LastModified string `json:"lastModified,omitempty"` +} + +// TaskStatus tracks the lifecycle state of a task. +type TaskStatus struct { + State string `json:"state"` + Message *Message `json:"message,omitempty"` +} + +// Message represents a conversational turn between user and agent, containing typed Parts. +type Message struct { + ContextID string `json:"contextId,omitempty"` + Kind string `json:"kind,omitempty"` + MessageID string `json:"messageId,omitempty"` + Metadata map[string]interface{} `json:"metadata,omitempty"` + Role string `json:"role"` + TaskID string `json:"taskId,omitempty"` + Parts []Part `json:"parts"` +} + +// Part is a polymorphic content unit — either text, raw bytes, URL, or structured data. +type Part struct { + Kind string `json:"kind,omitempty"` // legacy v0.3 + Type string `json:"type,omitempty"` // legacy v0.3 + Text string `json:"text,omitempty"` // v1.0 & v0.3 + URL string `json:"url,omitempty"` // v1.0 + Filename string `json:"filename,omitempty"` // v1.0 + MediaType string `json:"mediaType,omitempty"` // v1.0 + Raw string `json:"raw,omitempty"` // v1.0 + Data map[string]interface{} `json:"data,omitempty"` // v1.0 & v0.3 +} + +// Artifact is a named output produced by the agent during task execution. +type Artifact struct { + Name string `json:"name,omitempty"` + Parts []Part `json:"parts"` +} + +var ( + tasksMu sync.RWMutex + tasks = make(map[string]*Task) + policy compliance.Policy +) + +// InitPolicies configures the checker thresholds from targeted config paths. +func InitPolicies(policyPath string) { + loadedPolicy, err := compliance.LoadPolicy(policyPath) + if err != nil { + fmt.Printf("Warning: Failed to load policy file at %s: %v. Bootstrapping defaults.\n", policyPath, err) + policy = compliance.Policy{ + MaxContractValue: 500000.0, + ProhibitedClauses: []string{"unlimited liability", "auto-renewal > 3yr"}, + RequiredInsuranceMinimum: 1000000.0, + MaxTermYears: 5, + RequiredTerminationClause: true, + } + return + } + policy = loadedPolicy +} + +// HandleJSONRPC is the single entry point for all A2A JSON-RPC 2.0 requests. +// The A2A protocol routes all operations through JSON-RPC method dispatch +// rather than separate REST endpoints. +func HandleJSONRPC(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + + if r.Method != http.MethodPost { + writeJSONRPCError(w, nil, -32600, "Only POST method is accepted") + return + } + + // Decode the JSON-RPC envelope + var req JSONRPCRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + writeJSONRPCError(w, nil, -32700, "Parse error: invalid JSON") + return + } + + // Validate JSON-RPC version + if req.JSONRPC != "2.0" { + writeJSONRPCError(w, req.ID, -32600, "Invalid request: jsonrpc must be '2.0'") + return + } + + // Dispatch to the appropriate handler based on the A2A method name + switch req.Method { + case "message/send", "SendMessage": + result, rpcErr := handleMessageSend(req.Params) + if rpcErr != nil { + writeJSONRPCError(w, req.ID, rpcErr.Code, rpcErr.Message) + return + } + writeJSONRPCResult(w, req.ID, result) + + case "tasks/send": + result, rpcErr := handleTasksSend(req.Params) + if rpcErr != nil { + writeJSONRPCError(w, req.ID, rpcErr.Code, rpcErr.Message) + return + } + writeJSONRPCResult(w, req.ID, result) + + case "tasks/get", "GetTask": + result, rpcErr := handleTasksGet(req.Params) + if rpcErr != nil { + writeJSONRPCError(w, req.ID, rpcErr.Code, rpcErr.Message) + return + } + writeJSONRPCResult(w, req.ID, result) + + default: + writeJSONRPCError(w, req.ID, -32601, fmt.Sprintf("Method not found: %s", req.Method)) + } +} + +// handleMessageSend processes the current A2A message/send request shape. +// It accepts a Message containing a structured DataPart and returns a completed Task. +func handleMessageSend(params json.RawMessage) (*Task, *JSONRPCError) { + var sendParams MessageSendParams + if err := json.Unmarshal(params, &sendParams); err != nil { + return nil, &JSONRPCError{Code: -32602, Message: "Invalid params: " + err.Error()} + } + + taskID := sendParams.Message.TaskID + if taskID == "" { + taskID = stringValue(sendParams.Metadata, "task_id") + } + + return validateMessage(taskID, sendParams.Message, false) +} + +// handleTasksSend processes the legacy A2A tasks/send request shape. +// It is retained for older clients; the live demo uses message/send. +func handleTasksSend(params json.RawMessage) (*Task, *JSONRPCError) { + var sendParams TaskSendParams + if err := json.Unmarshal(params, &sendParams); err != nil { + return nil, &JSONRPCError{Code: -32602, Message: "Invalid params: " + err.Error()} + } + + if sendParams.ID == "" { + return nil, &JSONRPCError{Code: -32602, Message: "Missing required task id"} + } + + return validateMessage(sendParams.ID, sendParams.Message, true) +} + +func validateMessage(taskID string, message Message, includeLegacyType bool) (*Task, *JSONRPCError) { + contractPayload, rpcErr := extractContractPayload(message) + if rpcErr != nil { + return nil, rpcErr + } + + if taskID == "" { + taskID = stringValue(contractPayload, "case_id") + } + if taskID == "" { + taskID = fmt.Sprintf("task-%d", time.Now().UnixNano()) + } + + contextID := message.ContextID + if contextID == "" { + contextID = taskID + } + + detailsPayload, effectivePolicy, rpcErr := unpackContractPayload(contractPayload) + if rpcErr != nil { + return nil, rpcErr + } + + details, rpcErr := decodeContractDetails(detailsPayload) + if rpcErr != nil { + return nil, rpcErr + } + + // Run compliance checks synchronously — pure computation, no I/O wait needed. + result := compliance.CheckCompliance(details, effectivePolicy) + resultMap := complianceResultMap(result) + + task := completedTask(taskID, contextID, resultMap, includeLegacyType) + + // Store the task for later retrieval via tasks/get. + tasksMu.Lock() + tasks[taskID] = task + tasksMu.Unlock() + + return task, nil +} + +func extractContractPayload(message Message) (map[string]interface{}, *JSONRPCError) { + for _, part := range message.Parts { + if (part.Kind == "data" || part.Type == "data") && part.Data != nil { + return part.Data, nil + } + if part.Kind == "text" && part.Text != "" { + var data map[string]interface{} + if err := json.Unmarshal([]byte(part.Text), &data); err == nil { + return data, nil + } + } + } + return nil, &JSONRPCError{Code: -32602, Message: "No data part found in message"} +} + +func unpackContractPayload(payload map[string]interface{}) (map[string]interface{}, compliance.Policy, *JSONRPCError) { + effectivePolicy := policy + + if policyData, ok := payload["policy"]; ok { + parsedPolicy, rpcErr := decodePolicy(policyData) + if rpcErr != nil { + return nil, effectivePolicy, rpcErr + } + effectivePolicy = parsedPolicy + } + + if nested, ok := mapValue(payload, "contract"); ok { + return nested, effectivePolicy, nil + } + if nested, ok := mapValue(payload, "contract_details"); ok { + return nested, effectivePolicy, nil + } + + // Legacy clients send the contract fields directly in the DataPart. + direct := make(map[string]interface{}, len(payload)) + for key, value := range payload { + if key == "policy" || key == "risk_assessment" || key == "schema_version" || key == "case_id" { + continue + } + direct[key] = value + } + return direct, effectivePolicy, nil +} + +func decodePolicy(policyData interface{}) (compliance.Policy, *JSONRPCError) { + policyBytes, err := json.Marshal(policyData) + if err != nil { + return compliance.Policy{}, &JSONRPCError{Code: -32602, Message: "Invalid policy override"} + } + + var override compliance.Policy + if err := json.Unmarshal(policyBytes, &override); err != nil { + return compliance.Policy{}, &JSONRPCError{Code: -32602, Message: "Invalid policy override format: " + err.Error()} + } + return override, nil +} + +func decodeContractDetails(contractData map[string]interface{}) (compliance.ContractDetails, *JSONRPCError) { + if contractData == nil { + return compliance.ContractDetails{}, &JSONRPCError{Code: -32602, Message: "No data part found in message"} + } + + dataBytes, err := json.Marshal(contractData) + if err != nil { + return compliance.ContractDetails{}, &JSONRPCError{Code: -32603, Message: "Failed to marshal contract data"} + } + + var details compliance.ContractDetails + if err := json.Unmarshal(dataBytes, &details); err != nil { + return compliance.ContractDetails{}, &JSONRPCError{Code: -32602, Message: "Invalid contract data format: " + err.Error()} + } + return details, nil +} + +func complianceResultMap(result compliance.ComplianceResult) map[string]interface{} { + resultBytes, _ := json.Marshal(result) + var resultMap map[string]interface{} + _ = json.Unmarshal(resultBytes, &resultMap) + return resultMap +} + +func completedTask(taskID string, contextID string, resultMap map[string]interface{}, includeLegacyType bool) *Task { + resultPart := Part{ + Data: resultMap, + MediaType: "application/json", + } + if includeLegacyType { + resultPart.Kind = "data" + resultPart.Type = "data" + } + + agentMessage := Message{ + ContextID: contextID, + Kind: "message", + MessageID: fmt.Sprintf("msg-%s", taskID), + Role: "ROLE_AGENT", // "agent" -> "ROLE_AGENT" + TaskID: taskID, + Parts: []Part{resultPart}, + } + + now := time.Now().Format("2006-01-02T15:04:05.000Z") + return &Task{ + ContextID: contextID, + ID: taskID, + Kind: "task", + Status: TaskStatus{ + State: "TASK_STATE_COMPLETED", // "completed" -> "TASK_STATE_COMPLETED" + Message: &agentMessage, + }, + CreatedAt: now, + LastModified: now, + } +} + +func mapValue(data map[string]interface{}, key string) (map[string]interface{}, bool) { + value, ok := data[key] + if !ok { + return nil, false + } + typed, ok := value.(map[string]interface{}) + return typed, ok +} + +func stringValue(data map[string]interface{}, key string) string { + if data == nil { + return "" + } + value, ok := data[key] + if !ok { + return "" + } + typed, ok := value.(string) + if !ok { + return "" + } + return typed +} + +// handleTasksGet retrieves a previously submitted task by ID. +func handleTasksGet(params json.RawMessage) (*Task, *JSONRPCError) { + var getParams TaskGetParams + if err := json.Unmarshal(params, &getParams); err != nil { + return nil, &JSONRPCError{Code: -32602, Message: "Invalid params: " + err.Error()} + } + + tasksMu.RLock() + task, exists := tasks[getParams.ID] + tasksMu.RUnlock() + + if !exists { + return nil, &JSONRPCError{Code: -32602, Message: "Task not found: " + getParams.ID} + } + + return task, nil +} + +// writeJSONRPCResult sends a successful JSON-RPC 2.0 response. +func writeJSONRPCResult(w http.ResponseWriter, id interface{}, result interface{}) { + resp := JSONRPCResponse{ + JSONRPC: "2.0", + ID: id, + Result: result, + } + _ = json.NewEncoder(w).Encode(resp) +} + +// writeJSONRPCError sends a JSON-RPC 2.0 error response. +func writeJSONRPCError(w http.ResponseWriter, id interface{}, code int, message string) { + resp := JSONRPCResponse{ + JSONRPC: "2.0", + ID: id, + Error: &JSONRPCError{Code: code, Message: message}, + } + _ = json.NewEncoder(w).Encode(resp) +} + +// dispatchWebhook sends an A2A-formatted callback notification to the orchestrator. +func dispatchWebhook(url, taskID string, result compliance.ComplianceResult) { + // Convert result to a generic map for the A2A DataPart + resultBytes, _ := json.Marshal(result) + var resultMap map[string]interface{} + _ = json.Unmarshal(resultBytes, &resultMap) + + // Build an A2A-formatted webhook payload + payload := map[string]interface{}{ + "jsonrpc": "2.0", + "method": "tasks/pushNotification", + "params": map[string]interface{}{ + "id": taskID, + "status": map[string]interface{}{ + "state": "completed", + "message": map[string]interface{}{ + "role": "agent", + "parts": []map[string]interface{}{ + {"type": "data", "data": resultMap}, + }, + }, + }, + }, + } + bytesPayload, _ := json.Marshal(payload) + + fmt.Printf("A2A Webhook Dispatcher: Hitting callback endpoint %s\n", url) + req, err := http.NewRequest(http.MethodPost, url, bytes.NewBuffer(bytesPayload)) + if err != nil { + return + } + req.Header.Set("Content-Type", "application/json") + + client := &http.Client{Timeout: 5 * time.Second} + resp, err := client.Do(req) + if err != nil { + fmt.Printf("A2A Webhook Trigger Error: Connection failed: %v\n", err) + return + } + _ = resp.Body.Close() + fmt.Printf("A2A Webhook Trigger Callback: Received code %d from backend listener.\n", resp.StatusCode) +} diff --git a/agents/adk/contract-compliance-pipeline/go-compliance-agent/internal/handler/task_handler_test.go b/agents/adk/contract-compliance-pipeline/go-compliance-agent/internal/handler/task_handler_test.go new file mode 100644 index 0000000..ff5e548 --- /dev/null +++ b/agents/adk/contract-compliance-pipeline/go-compliance-agent/internal/handler/task_handler_test.go @@ -0,0 +1,168 @@ +package handler + +import ( + "bytes" + "encoding/json" + "net/http" + "net/http/httptest" + "testing" +) + +func TestHandleJSONRPCMessageSend(t *testing.T) { + InitPolicies("../policies/default_policy.json") + resetStoredTasks() + + payload := map[string]interface{}{ + "jsonrpc": "2.0", + "id": "rpc-1", + "method": "SendMessage", + "params": map[string]interface{}{ + "metadata": map[string]interface{}{"task_id": "case-123"}, + "message": map[string]interface{}{ + "kind": "message", + "messageId": "msg-1", + "role": "ROLE_USER", + "parts": []map[string]interface{}{ + { + "data": map[string]interface{}{ + "schema_version": "contract-compliance.a2a.v1", + "case_id": "case-123", + "contract": map[string]interface{}{ + "contract_value": 250000.0, + "contractor_name": "ACME CLOUD SOLUTIONS", + "client_name": "GFD PLATFORM SYSTEMS", + "start_date": "2026-06-01", + "end_date": "2028-06-01", + "liability_limit": "$1,000,000", + "insurance_coverage": 2000000.0, + "auto_renewal": false, + "has_termination_clause": true, + "term_length_years": 2, + }, + }, + "mediaType": "application/json", + }, + }, + }, + }, + } + + response := postJSONRPC(t, payload) + + if response.Error != nil { + t.Fatalf("unexpected JSON-RPC error: %+v", response.Error) + } + if response.Result.Kind != "task" { + t.Fatalf("expected A2A task result, got %q", response.Result.Kind) + } + if response.Result.ID != "case-123" { + t.Fatalf("expected task id case-123, got %q", response.Result.ID) + } + if response.Result.Status.State != "TASK_STATE_COMPLETED" { + t.Fatalf("expected completed task state TASK_STATE_COMPLETED, got %q", response.Result.Status.State) + } + + message := response.Result.Status.Message + if message == nil { + t.Fatal("expected status message") + } + if message.Role != "ROLE_AGENT" { + t.Fatalf("expected message role ROLE_AGENT, got %q", message.Role) + } + if len(message.Parts) != 1 || message.Parts[0].MediaType != "application/json" { + t.Fatalf("expected one A2A data part with mediaType application/json, got %+v", message.Parts) + } + if passed, ok := message.Parts[0].Data["passed"].(bool); !ok || !passed { + t.Fatalf("expected passed verdict, got %+v", message.Parts[0].Data["passed"]) + } +} + +func TestHandleJSONRPCLegacyTasksSendStillWorks(t *testing.T) { + InitPolicies("../policies/default_policy.json") + resetStoredTasks() + + payload := map[string]interface{}{ + "jsonrpc": "2.0", + "id": "rpc-legacy", + "method": "tasks/send", + "params": map[string]interface{}{ + "id": "legacy-task", + "message": map[string]interface{}{ + "role": "user", + "parts": []map[string]interface{}{ + { + "type": "data", + "data": map[string]interface{}{ + "contract_value": 900000.0, + "contractor_name": "Risky Vendor", + "client_name": "GFD PLATFORM SYSTEMS", + "start_date": "2026-06-01", + "end_date": "2032-06-01", + "liability_limit": "unlimited liability", + "insurance_coverage": 500000.0, + "auto_renewal": true, + "has_termination_clause": false, + "term_length_years": 6, + }, + }, + }, + }, + }, + } + + response := postJSONRPC(t, payload) + + if response.Error != nil { + t.Fatalf("unexpected JSON-RPC error: %+v", response.Error) + } + if response.Result.ID != "legacy-task" { + t.Fatalf("expected legacy task id, got %q", response.Result.ID) + } + + parts := response.Result.Status.Message.Parts + if len(parts) != 1 || parts[0].Type != "data" { + t.Fatalf("expected legacy type=data response part, got %+v", parts) + } + if passed, ok := parts[0].Data["passed"].(bool); !ok || passed { + t.Fatalf("expected failing verdict, got %+v", parts[0].Data["passed"]) + } +} + +func postJSONRPC(t *testing.T, payload map[string]interface{}) struct { + JSONRPC string `json:"jsonrpc"` + ID string `json:"id"` + Result Task `json:"result"` + Error *JSONRPCError `json:"error"` +} { + t.Helper() + + body, err := json.Marshal(payload) + if err != nil { + t.Fatalf("marshal payload: %v", err) + } + + request := httptest.NewRequest(http.MethodPost, "/", bytes.NewReader(body)) + recorder := httptest.NewRecorder() + HandleJSONRPC(recorder, request) + + if recorder.Code != http.StatusOK { + t.Fatalf("expected HTTP 200, got %d", recorder.Code) + } + + var response struct { + JSONRPC string `json:"jsonrpc"` + ID string `json:"id"` + Result Task `json:"result"` + Error *JSONRPCError `json:"error"` + } + if err := json.Unmarshal(recorder.Body.Bytes(), &response); err != nil { + t.Fatalf("unmarshal response: %v", err) + } + return response +} + +func resetStoredTasks() { + tasksMu.Lock() + defer tasksMu.Unlock() + tasks = make(map[string]*Task) +} diff --git a/agents/adk/contract-compliance-pipeline/go-compliance-agent/internal/policies/default_policy.json b/agents/adk/contract-compliance-pipeline/go-compliance-agent/internal/policies/default_policy.json new file mode 100644 index 0000000..d4bbc08 --- /dev/null +++ b/agents/adk/contract-compliance-pipeline/go-compliance-agent/internal/policies/default_policy.json @@ -0,0 +1,10 @@ +{ + "max_contract_value": 500000.0, + "prohibited_clauses": [ + "unlimited liability", + "auto-renewal > 3yr" + ], + "required_insurance_minimum": 1000000.0, + "max_term_years": 5, + "required_termination_clause": true +} diff --git a/agents/adk/contract-compliance-pipeline/python-extraction-agent/Dockerfile b/agents/adk/contract-compliance-pipeline/python-extraction-agent/Dockerfile new file mode 100644 index 0000000..1c02957 --- /dev/null +++ b/agents/adk/contract-compliance-pipeline/python-extraction-agent/Dockerfile @@ -0,0 +1,47 @@ +# Copyright 2026 Google LLC +# +# 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 +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +FROM python:3.11-slim + +# Install system dependencies +RUN apt-get update && apt-get install -y \ + curl \ + git \ + build-essential \ + && rm -rf /var/lib/apt/lists/* + +# Install UV +ADD https://astral.sh/uv/install.sh /install.sh +RUN chmod -R 755 /install.sh && /install.sh && rm /install.sh +ENV PATH="/root/.local/bin:${PATH}" + +WORKDIR /app + +# Copy project files +COPY pyproject.toml uv.lock* ./ +RUN uv venv .venv +ENV PATH="/app/.venv/bin:${PATH}" + +COPY . . + +# Sync dependencies after package sources are present +RUN uv pip install -e . + +EXPOSE 8000 + +ENV HOST=0.0.0.0 +ENV PORT=8000 + +# Runs standard FastAPI uvicorn listener +CMD ["uvicorn", "app.fast_api_app:app", "--host", "0.0.0.0", "--port", "8000"] diff --git a/agents/adk/contract-compliance-pipeline/python-extraction-agent/app/__init__.py b/agents/adk/contract-compliance-pipeline/python-extraction-agent/app/__init__.py new file mode 100644 index 0000000..6b9b094 --- /dev/null +++ b/agents/adk/contract-compliance-pipeline/python-extraction-agent/app/__init__.py @@ -0,0 +1 @@ +# Contract Compliance Pipeline - Python Extraction Service diff --git a/agents/adk/contract-compliance-pipeline/python-extraction-agent/app/agent.py b/agents/adk/contract-compliance-pipeline/python-extraction-agent/app/agent.py new file mode 100644 index 0000000..6217b93 --- /dev/null +++ b/agents/adk/contract-compliance-pipeline/python-extraction-agent/app/agent.py @@ -0,0 +1,202 @@ +# Copyright 2026 Google LLC +# +# 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 +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""ADK orchestration reference for the Contract Compliance Pipeline. + +The browser cockpit uses fast_api_app.py for the stable, observable demo path: +deterministic extraction plus a focused ADK RemoteA2aAgent handoff to Go. This +module keeps the fuller ADK agent hierarchy as a reference implementation for +the same cross-language design: + + SequentialAgent (coordinator) + ├── Agent: extractor_agent — Parses contracts, extracts key legal fields + ├── RemoteA2aAgent: compliance_agent — Sends fields to Go compliance service via A2A + └── Agent: report_agent — Generates final audit summary report + +Keep this distinction clear in docs and demos: fast_api_app.py runs the live +cockpit path with one focused RemoteA2aAgent call; this file shows the fuller +SequentialAgent architecture. +""" + +import os +import google.auth +from google.adk.agents import Agent, SequentialAgent +from google.adk.agents.remote_a2a_agent import RemoteA2aAgent +from google.adk.agents.callback_context import CallbackContext +from google.adk.apps import App +from google.adk.models import Gemini +from google.genai import types + +from app.state_schema import ComplianceStep +from app.tools import ( + read_contract_text, + save_extracted_fields, + classify_risk_level, + generate_summary_report, +) + +# Establish Google Cloud project details for Gemini API +try: + _, project_id = google.auth.default() + os.environ["GOOGLE_CLOUD_PROJECT"] = project_id +except Exception: + os.environ["GOOGLE_CLOUD_PROJECT"] = "mock-gcp-project" + +os.environ["GOOGLE_CLOUD_LOCATION"] = "global" +os.environ["GOOGLE_GENAI_USE_VERTEXAI"] = "True" + +# Go compliance agent endpoint (configurable via environment variable) +GO_AGENT_CARD_URL = os.environ.get( + "GO_AGENT_CARD_URL", + "http://localhost:8888/.well-known/agent.json" +) + + +async def initialize_compliance_state(callback_context: CallbackContext) -> None: + """Initialize pipeline state before the SequentialAgent begins execution. + + Sets up all state keys that sub-agents will read and write during the pipeline. + Using ToolContext.state for shared state is a core ADK pattern — all sub-agents + in a SequentialAgent share the same session state dictionary. + """ + state = callback_context.state + if "current_step" not in state: + state["current_step"] = ComplianceStep.INGESTED + if "contract_details" not in state: + state["contract_details"] = {} + if "risk_assessment" not in state: + state["risk_assessment"] = {} + if "compliance_verdict" not in state: + state["compliance_verdict"] = {} + if "trace_logs" not in state: + state["trace_logs"] = [] + if "final_report" not in state: + state["final_report"] = {} + + +# --------------------------------------------------------------------------- +# Sub-Agent 1: Extraction Agent +# --------------------------------------------------------------------------- +# Focused on one job: parse contract text and extract structured legal fields. +# Uses Gemini to understand natural language contract clauses and convert them +# to typed data (floats, dates, booleans). + +extractor_agent = Agent( + name="extractor_agent", + model=Gemini( + model="gemini-3.5-flash", + retry_options=types.HttpRetryOptions(attempts=3), + ), + instruction="""You are a Legal Data Extraction Agent. + Parse contract documents and extract key legal parameters. + + Current pipeline state: + - Step: {current_step} + - Contract Details: {contract_details} + + Instructions: + 1. If current_step is 'INGESTED', use 'read_contract_text' with the contract filename. + 2. Extract these fields from the text: + - contract_value (float, e.g., 250000.0) + - contractor_name (the vendor) + - client_name (the corporate entity) + - start_date (YYYY-MM-DD) + - end_date (YYYY-MM-DD) + - liability_limit (text description, e.g., '$1,000,000' or 'unlimited liability') + - insurance_coverage (float, minimum coverage amount) + - auto_renewal (boolean) + - has_termination_clause (boolean) + 3. Call 'save_extracted_fields' with all extracted parameters. + 4. Call 'classify_risk_level' to determine the risk tier. + 5. Hand control to the next agent. + """, + tools=[read_contract_text, save_extracted_fields, classify_risk_level], +) + + +# --------------------------------------------------------------------------- +# Sub-Agent 2: A2A Compliance Agent (Cross-Language Handoff) +# --------------------------------------------------------------------------- +# This is the key pattern: ADK's RemoteA2aAgent wraps a remote A2A-compliant service +# as a local sub-agent. ADK handles: +# - Agent Card discovery at the well-known URL +# - JSON-RPC 2.0 message submission (SendMessage) +# - Task response conversion back into ADK events +# - Message/Part serialization +# +# The Go compliance agent runs as a separate process (potentially on a different +# machine, written in a different language) but appears as just another sub-agent +# in the pipeline. + +compliance_agent = RemoteA2aAgent( + name="compliance_agent", + agent_card=GO_AGENT_CARD_URL, + description="Validates extracted contract fields against corporate compliance " + "policy rules via the Go-based Security Compliance Validator service.", +) + + +# --------------------------------------------------------------------------- +# Sub-Agent 3: Report Agent +# --------------------------------------------------------------------------- +# Generates the final audit summary report from the accumulated state. + +report_agent = Agent( + name="report_agent", + model=Gemini( + model="gemini-3.5-flash", + retry_options=types.HttpRetryOptions(attempts=3), + ), + instruction="""You are a Compliance Reporting Specialist. + Generate the final multi-agent compliance summary report. + + Current pipeline state: + - Step: {current_step} + - Compliance Verdict: {compliance_verdict} + - Final Report: {final_report} + + Instructions: + 1. Call the 'generate_summary_report' tool. + 2. Present the completed audit summary in clean Markdown: + - Case ID & Timestamps + - Contractor Name & Contract Value + - Risk Tier + - Compliance Verdict (APPROVED or REJECTED) + - Specific policy violations (if any) + 3. Hand control back to the coordinator. + """, + tools=[generate_summary_report], +) + + +# --------------------------------------------------------------------------- +# Coordinator: SequentialAgent +# --------------------------------------------------------------------------- +# Runs all three sub-agents in order: extract → comply → report. +# This is the "micro-agent" pattern — each agent has one job, a focused prompt, +# and a narrow tool set. Compare this to a "god agent" that would try to handle +# extraction, compliance checking, and reporting all in a single massive prompt. + +root_agent = SequentialAgent( + name="contract_compliance_coordinator", + description="Orchestrates contract parsing, A2A compliance validation, " + "and final reporting in sequence.", + sub_agents=[extractor_agent, compliance_agent, report_agent], + before_agent_callback=initialize_compliance_state, +) + +app = App( + root_agent=root_agent, + name="app", +) diff --git a/agents/adk/contract-compliance-pipeline/python-extraction-agent/app/app_utils/__init__.py b/agents/adk/contract-compliance-pipeline/python-extraction-agent/app/app_utils/__init__.py new file mode 100644 index 0000000..3cc6e2b --- /dev/null +++ b/agents/adk/contract-compliance-pipeline/python-extraction-agent/app/app_utils/__init__.py @@ -0,0 +1 @@ +# Shared utilities for telemetry and tracing context propagation diff --git a/agents/adk/contract-compliance-pipeline/python-extraction-agent/app/app_utils/telemetry.py b/agents/adk/contract-compliance-pipeline/python-extraction-agent/app/app_utils/telemetry.py new file mode 100644 index 0000000..85d59f0 --- /dev/null +++ b/agents/adk/contract-compliance-pipeline/python-extraction-agent/app/app_utils/telemetry.py @@ -0,0 +1,67 @@ +# Copyright 2026 Google LLC +# +# 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 +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os +import logging + +logger = logging.getLogger(__name__) + + +def setup_telemetry() -> None: + """Configures system OTel spans tracing options. + + Ties together native OpenTelemetry bindings exporting logs to Cloud Trace + if active GCP credentials exist. Falls back safely to standard dev formatters. + """ + logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' + ) + logger.info("Observability Engine: Tracing telemetry pipelines synchronized.") + + +def inject_trace_propagation_headers(target_headers: dict) -> dict: + """Injects W3C tracecontext propagation headers into outbound A2A network requests. + + Allows distributed tracing tracking metrics across Python/Go boundaries. + """ + # Simple mockup tracing transaction spans generator for dev + trace_id = os.urandom(16).hex() + span_id = os.urandom(8).hex() + + # Propagating traceparent header (W3C standard) + traceparent = f"00-{trace_id}-{span_id}-01" + target_headers["traceparent"] = traceparent + + logger.info(f"Injecting distributed context headers traceparent: {traceparent}") + return target_headers + + +def extract_trace_context(incoming_headers: dict) -> dict: + """Extracts propagation metrics context from incoming boundary headers.""" + traceparent = incoming_headers.get("traceparent") or incoming_headers.get("Traceparent") + if not traceparent: + return {} + + try: + parts = traceparent.split("-") + if len(parts) == 4: + return { + "trace_id": parts[1], + "parent_span_id": parts[2], + "flags": parts[3] + } + except Exception: + pass + return {} diff --git a/agents/adk/contract-compliance-pipeline/python-extraction-agent/app/fallback_handler.py b/agents/adk/contract-compliance-pipeline/python-extraction-agent/app/fallback_handler.py new file mode 100644 index 0000000..45c2664 --- /dev/null +++ b/agents/adk/contract-compliance-pipeline/python-extraction-agent/app/fallback_handler.py @@ -0,0 +1,205 @@ +# Copyright 2026 Google LLC +# +# 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 +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import asyncio +import json +import logging +import time +from google.adk.tools import ToolContext +from app.state_schema import ComplianceStep +# NOTE: This module is retained as a reference implementation showing the +# resilience pattern: timeout, retry, and fail-close to MANUAL_REVIEW. The live +# cockpit path calls the Go compliance agent explicitly from fast_api_app.py so +# the handoff is observable in the browser. agent.py keeps the ADK +# RemoteA2aAgent architecture reference. + + +async def invoke_a2a_compliance_check(tool_context): + """Placeholder for the legacy A2A compliance check. + + In the original implementation, this function was imported from + a2a_client.py which used hand-rolled HTTP calls to the Go agent. + The live cockpit now uses fast_api_app.invoke_go_compliance_service(). + """ + raise NotImplementedError( + "Legacy A2A client removed. Use fast_api_app.invoke_go_compliance_service()." + ) + + +logger = logging.getLogger(__name__) + + +def _log_structured_json(severity: str, message: str, **kwargs) -> None: + """Standardized structured JSON logger for GCP/Cloud Logging visibility. + + Rule 53 Enforcement: Logs diagnostic messages in parseable JSON structures, + ensuring sensitive session tokens are never printed. + """ + payload = { + "severity": severity, + "message": message, + "timestamp": time.strftime("%Y-%m-%dT%H:%M:%S%Z"), + **kwargs + } + logger.info(json.dumps(payload)) + + +async def invoke_resilient_compliance_check( + tool_context: ToolContext, + timeout_sec: float = 30.0, + max_retries: int = 3, + backoff_factor: float = 1.5, +) -> dict: + """Wraps A2A task audit dispatching in high-resilience triggers: + + 1. Handles 503 Crashed Server exceptions using exponential retry loops. + 2. Enforces a strict 30-second timeout boundary. + 3. Triggers safe fail-close path migrations (transitions step to MANUAL_REVIEW). + """ + state = tool_context.state + case_id = state.get("case_id", "local-case") + + # Check if a custom timeout is toggled in memory settings + simulated_timeout = state.get("simulated_timeout", timeout_sec) + + _log_structured_json( + severity="INFO", + message=f"Resilience Handler: Initializing compliance check for case {case_id}", + event="compliance_audit_start", + case_id=case_id, + timeout_boundary_sec=simulated_timeout + ) + + attempt = 0 + backoff_delay = 1.0 + + while attempt < max_retries: + attempt += 1 + try: + # Wrap the A2A submission and check in a strict timeout boundary + _log_structured_json( + severity="INFO", + message=f"Dispatching A2A audit request (Attempt {attempt} of {max_retries})", + event="a2a_dispatch_attempt", + case_id=case_id, + attempt=attempt + ) + + # Execute A2A client validation with timeout caps + res = await asyncio.wait_for( + invoke_a2a_compliance_check(tool_context), + timeout=simulated_timeout + ) + + # Check response status + if res.get("status") == "dormant_paused": + _log_structured_json( + severity="INFO", + message="External connection reports lag. Task submitted and pipeline entered dormant state.", + event="dormancy_entered", + case_id=case_id, + task_id=res.get("task_id") + ) + return res + + _log_structured_json( + severity="INFO", + message="A2A compliance validation audit finished successfully.", + event="compliance_audit_completed", + case_id=case_id + ) + return res + + except asyncio.TimeoutError: + # 30-second timeout reached + _log_structured_json( + severity="WARNING", + message=f"A2A connection timed out after {simulated_timeout} seconds limit.", + event="timeout_reached", + case_id=case_id, + timeout_limit_sec=simulated_timeout + ) + break # Move straight to MANUAL_REVIEW fallback + + except ConnectionError as e: + # 503 connection error or socket faults -> retry loop triggers + _log_structured_json( + severity="WARNING", + message=f"A2A connection failed on attempt {attempt}: {e!s}", + event="connection_fault", + case_id=case_id, + error=str(e) + ) + + if attempt >= max_retries: + _log_structured_json( + severity="ERROR", + message="Max A2A delivery retries exhausted. Triggering safety manual routing.", + event="retries_exhausted", + case_id=case_id + ) + break + + # Perform backoff + logger.info(f"Retrying compliance check after {backoff_delay} seconds delay...") + await asyncio.sleep(backoff_delay) + backoff_delay *= backoff_factor + + except Exception as e: + # Non-retryable errors — fail immediately + _log_structured_json( + severity="ERROR", + message=f"A2A audit failed with non-retryable error: {e!s}", + event="non_retryable_error", + case_id=case_id, + error=str(e) + ) + break + + # --- FALLBACK GATEWAY (Fail Safe Transition to MANUAL_REVIEW) --- + _log_structured_json( + severity="ALERT", + message="Resilience Gate triggered: Transitioning case to MANUAL_REVIEW checkpoint.", + event="fallback_recovery_triggered", + case_id=case_id + ) + + # Update persistent state metrics safely + state["current_step"] = ComplianceStep.MANUAL_REVIEW + state["pending_signals"] = [] + + fallback_verdict = { + "passed": False, + "violations": [ + "SYSTEM TIMEOUT: External compliance service failed to respond within 30-second threshold.", + "FAIL-SAFE ACTION: Document routed for legal manager manual verification." + ], + "verdict_timestamp": time.strftime("%Y-%m-%d %H:%M:%S %Z") + } + state["compliance_verdict"] = fallback_verdict + + # Append trace event logs + state["trace_logs"] = state.get("trace_logs", []) + state["trace_logs"].append({ + "span": "resilience_fallback_gate", + "service": "python-extraction-agent", + "duration_ms": 450, + "status": "manual_review_routed" + }) + + return { + "status": "fallback_manual_review", + "verdict": fallback_verdict, + "message": "Resilience safety check triggered. Case successfully routed for manual legal review." + } diff --git a/agents/adk/contract-compliance-pipeline/python-extraction-agent/app/fast_api_app.py b/agents/adk/contract-compliance-pipeline/python-extraction-agent/app/fast_api_app.py new file mode 100644 index 0000000..5d91310 --- /dev/null +++ b/agents/adk/contract-compliance-pipeline/python-extraction-agent/app/fast_api_app.py @@ -0,0 +1,574 @@ +# Copyright 2026 Google LLC +# +# 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 +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""FastAPI application for the Contract Compliance Pipeline. + +Provides REST API endpoints for contract upload, status checking, and +result retrieval. The live cockpit uses deterministic extraction and a real +ADK RemoteA2aAgent handoff to the Go compliance service so the demo remains +stable while still exercising the current A2A SendMessage protocol. +""" + +import json +import os +import secrets +import shutil +import logging +import uuid +import time +import asyncio +import urllib.error +import urllib.request +from pathlib import Path +from typing import Optional + +from fastapi import FastAPI, UploadFile, File, Form, HTTPException +from fastapi.responses import PlainTextResponse, RedirectResponse +from fastapi.staticfiles import StaticFiles +from fastapi.middleware.cors import CORSMiddleware + +from a2a.types import DataPart as A2ADataPart +from google.adk.a2a.agent.config import A2aRemoteAgentConfig, RequestInterceptor +from google.adk.a2a.converters.part_converter import ( + A2A_DATA_PART_END_TAG, + A2A_DATA_PART_START_TAG, + A2A_DATA_PART_TEXT_MIME_TYPE, +) +from google.adk.agents.remote_a2a_agent import RemoteA2aAgent +from google.adk.apps import App as ADKApp +from google.adk.cli.fast_api import get_fast_api_app +from google.adk.runners import Runner +from google.adk.sessions.database_session_service import DatabaseSessionService +from google.adk.sessions.in_memory_session_service import InMemorySessionService +from google.genai import types as genai_types + +from app.state_schema import ComplianceStep +from app.tools import ( + SANDBOX_DIR, + UPLOAD_DIR, + classify_contract_risk, + extract_contract_details_from_text, +) +from app.app_utils.telemetry import setup_telemetry +from app.live_compliance import ( + create_compliance_case, + get_case, + latest_case_payload, + sync_case_with_session_state, + case_payload, + artifact_response, + _event, +) + +setup_telemetry() +logger = logging.getLogger(__name__) + +GO_AGENT_CARD_URL = os.environ.get( + "GO_AGENT_CARD_URL", + "http://localhost:8888/.well-known/agent.json", +) +PROJECT_ROOT = Path(SANDBOX_DIR) +SAMPLE_CONTRACTS_DIR = PROJECT_ROOT / "sample-contracts" + + +def _go_jsonrpc_url() -> str: + if GO_AGENT_CARD_URL.endswith("/.well-known/agent.json"): + return GO_AGENT_CARD_URL.removesuffix("/.well-known/agent.json") + return GO_AGENT_CARD_URL.rstrip("/") + + +def build_contract_handoff_data(case_id: str, details: dict, policy: dict | None) -> dict: + data = { + "schema_version": "contract-compliance.a2a.v1", + "case_id": case_id, + "contract": details, + } + if policy: + data["policy"] = policy + return data + + +def build_go_message_payload(case_id: str, details: dict, policy: dict | None) -> dict: + return { + "jsonrpc": "2.0", + "id": f"case-{case_id}", + "method": "SendMessage", + "params": { + "metadata": {"task_id": case_id}, + "message": { + "messageId": f"case-{case_id}-request", + "taskId": case_id, + "role": "ROLE_USER", + "parts": [{ + "data": build_contract_handoff_data(case_id, details, policy), + "mediaType": "application/json" + }], + }, + }, + } + + +def extract_verdict_from_go_response(response: dict) -> dict: + result = response.get("result", response) + parts = ( + result + .get("status", {}) + .get("message", {}) + .get("parts", []) + ) + for part in parts: + if "data" in part: + return part["data"] + if part.get("kind") == "data" or part.get("type") == "data": + return part.get("data", {}) + raise RuntimeError("Go compliance service returned no verdict data") + + +def _a2a_data_part_as_genai_part(data: dict) -> genai_types.Part: + """Build a GenAI Part that ADK converts back into an A2A DataPart.""" + data_part = A2ADataPart(data=data) + return genai_types.Part( + inline_data=genai_types.Blob( + data=( + A2A_DATA_PART_START_TAG + + data_part.model_dump_json(by_alias=True, exclude_none=True).encode("utf-8") + + A2A_DATA_PART_END_TAG + ), + mime_type=A2A_DATA_PART_TEXT_MIME_TYPE, + ) + ) + + +def _http_json(method: str, url: str, payload: dict | None = None, timeout: float = 10.0) -> dict: + data = None if payload is None else json.dumps(payload).encode("utf-8") + request = urllib.request.Request( + url, + data=data, + headers={"Content-Type": "application/json", "Accept": "application/json"}, + method=method, + ) + with urllib.request.urlopen(request, timeout=timeout) as response: + return json.loads(response.read().decode("utf-8")) + + +async def invoke_go_compliance_service( + case_id: str, + details: dict, + policy: dict | None, + timeout: float = 10.0, +) -> dict: + """Calls the Go service through ADK RemoteA2aAgent and the A2A SDK.""" + agent_card = await asyncio.to_thread(_http_json, "GET", GO_AGENT_CARD_URL, None, timeout) + payload = build_go_message_payload(case_id, details, policy) + request_data = build_contract_handoff_data(case_id, details, policy) + + async def add_task_metadata(_ctx, a2a_message, parameters): + parameters.request_metadata = {"task_id": case_id} + return a2a_message, parameters + + remote_agent = RemoteA2aAgent( + name="compliance_agent", + agent_card=GO_AGENT_CARD_URL, + description="Validates extracted contract fields with the Go A2A compliance service.", + timeout=timeout, + config=A2aRemoteAgentConfig( + request_interceptors=[ + RequestInterceptor(before_request=add_task_metadata) + ] + ), + ) + session_service = InMemorySessionService() + adk_app_name = "contract_compliance_remote_handoff" + await session_service.create_session( + app_name=adk_app_name, + user_id="live-cockpit", + session_id=case_id, + state={}, + ) + runner = Runner( + app=ADKApp(name=adk_app_name, root_agent=remote_agent), + session_service=session_service, + ) + + response_task = None + request_message = None + error_message = None + try: + async for event in runner.run_async( + user_id="live-cockpit", + session_id=case_id, + new_message=genai_types.Content( + role="user", + parts=[_a2a_data_part_as_genai_part(request_data)], + ), + ): + metadata = event.custom_metadata or {} + request_message = metadata.get("a2a:request", request_message) + response_task = metadata.get("a2a:response", response_task) + if event.error_message: + error_message = event.error_message + finally: + await remote_agent.cleanup() + + if error_message: + raise RuntimeError(error_message) + if response_task is None: + raise RuntimeError("ADK RemoteA2aAgent returned no A2A task response") + + response = { + "jsonrpc": "2.0", + "id": payload["id"], + "result": response_task, + } + + return { + "agent_card": agent_card, + "request": payload, + "response": response, + "adk_request_message": request_message, + "adk_response_task": response_task, + "verdict": extract_verdict_from_go_response(response), + } + + +# --- Session Credentials --- +def get_session_secret() -> str: + """Generate or load a session secret key.""" + if os.getenv("SESSION_SECRET_KEY"): + return os.getenv("SESSION_SECRET_KEY") + + secret_file = os.path.join(os.path.dirname(os.path.abspath(__file__)), "session_secret.txt") + if os.path.exists(secret_file): + try: + with open(secret_file, "r", encoding="utf-8") as f: + return f.read().strip() + except Exception: + pass + + ephemeral = secrets.token_hex(32) + try: + with open(secret_file, "w", encoding="utf-8") as f: + f.write(ephemeral) + except Exception: + pass + return ephemeral + + +# --- App Configuration --- +AGENT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +session_service_uri = "sqlite+aiosqlite:///sessions.db" + +app: FastAPI = get_fast_api_app( + agents_dir=AGENT_DIR, + web=True, + session_service_uri=session_service_uri, + allow_origins=[ + "http://127.0.0.1:8000", + "http://localhost:8000", + "null", + ], +) + +app.title = "contract-compliance-pipeline" +app.description = "Multi-agent contract compliance pipeline using ADK and A2A protocol." + +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_methods=["*"], + allow_headers=["*"], +) + + +# --- Startup --- +@app.on_event("startup") +def boot_sandbox_bounds(): + """Initialize upload directory and preload sample contracts.""" + os.makedirs(UPLOAD_DIR, exist_ok=True) + # Preload sample contracts from project root + sample_dir = str(SAMPLE_CONTRACTS_DIR) + if os.path.exists(sample_dir): + for filename in os.listdir(sample_dir): + src = os.path.join(sample_dir, filename) + dst = os.path.join(UPLOAD_DIR, filename) + if os.path.isfile(src): + try: + shutil.copy2(src, dst) + except Exception as ex: + logger.warning(f"Preloading contract {filename} failed: {ex}") + logger.info(f"Upload directory ready: {UPLOAD_DIR}") + + +# --- ADK Session Storage --- +db_session_service = DatabaseSessionService(db_url=session_service_uri) + +# --- Static Files --- +static_folder = Path(__file__).parent / "static" / "live-compliance" +if static_folder.exists(): + app.mount( + "/live-compliance", + StaticFiles(directory=str(static_folder), html=True), + name="live-compliance", + ) + + +@app.get("/demo") +def demo_router() -> RedirectResponse: + return RedirectResponse(url="/live-compliance/") + + +# --- API Endpoints --- + +@app.get("/api/compliance/current") +def get_current_case_status() -> dict: + return latest_case_payload() + + +@app.get("/api/compliance/cases/{case_id}") +def get_compliance_case(case_id: str) -> dict: + case = get_case(case_id) + return {"active": True, "case": case_payload(case)} + + +@app.get("/api/compliance/cases/{case_id}/artifacts/{artifact_id}") +def get_compliance_artifact(case_id: str, artifact_id: str): + return artifact_response(case_id, artifact_id) + + +@app.get("/api/compliance/sample-contracts/{filename}") +def get_sample_contract(filename: str) -> PlainTextResponse: + """Serves bundled sample contract text for the browser-driven demo.""" + safe_filename = os.path.basename(filename) + target = (SAMPLE_CONTRACTS_DIR / safe_filename).resolve() + sample_root = SAMPLE_CONTRACTS_DIR.resolve() + if not str(target).startswith(str(sample_root) + os.path.sep): + raise HTTPException(status_code=403, detail="Invalid sample contract path.") + if not target.exists() or not target.is_file(): + raise HTTPException(status_code=404, detail="Sample contract not found.") + if target.suffix.lower() not in {".pdf", ".txt", ".md"}: + raise HTTPException(status_code=400, detail="Unsupported sample contract type.") + + return PlainTextResponse( + target.read_text(encoding="utf-8", errors="ignore"), + headers={"Cache-Control": "no-store"}, + ) + + +@app.post("/api/compliance/upload") +async def upload_contract_file( + file: UploadFile = File(...), + simulated_latency: float = Form(0.0), + simulated_server_state: str = Form("normal"), + custom_policies: Optional[str] = Form(None), +) -> dict: + """Upload a text contract fixture and run the compliance pipeline. + + Accepts text files and .pdf-named text fixtures, saves them securely, and kicks off the + multi-agent pipeline: extraction → A2A compliance check → reporting. + """ + filename = file.filename or "contract.pdf" + original_filename = os.path.basename(filename) + + # Verify extensions + ext = os.path.splitext(filename)[1].lower() + if ext not in [".pdf", ".txt", ".md"]: + raise HTTPException(status_code=400, detail="Only .pdf, .txt, and .md text fixtures are supported.") + + # Rename to UUID for security + case_id = str(uuid.uuid4()) + safe_filename = f"{case_id}{ext}" + os.makedirs(UPLOAD_DIR, exist_ok=True) + secure_target = os.path.join(UPLOAD_DIR, safe_filename) + + # Path traversal check + absolute_root = os.path.abspath(UPLOAD_DIR) + os.path.sep + absolute_target = os.path.abspath(secure_target) + if not absolute_target.startswith(absolute_root): + raise HTTPException(status_code=403, detail="Invalid upload path.") + + # Size limit (5MB) + content = await file.read() + if len(content) > 5 * 1024 * 1024: + raise HTTPException(status_code=400, detail="File size exceeds 5MB limit.") + if ext == ".pdf" and content.lstrip().startswith(b"%PDF"): + raise HTTPException( + status_code=400, + detail=( + "Binary PDF parsing is not included in this demo. " + "Use bundled text fixtures or upload .txt/.md contract text." + ), + ) + + # Save file + try: + with open(absolute_target, "wb") as f: + f.write(content) + except Exception as exc: + raise HTTPException(status_code=500, detail=f"Failed to save file: {exc!s}") + + # Create compliance case + case = await create_compliance_case(original_filename, db_session_service) + + content_text = content.decode("utf-8", errors="ignore") + details = extract_contract_details_from_text(original_filename, content_text) + risk = classify_contract_risk(details) + policy = None + if custom_policies: + try: + policy = json.loads(custom_policies) + except json.JSONDecodeError as exc: + raise HTTPException(status_code=400, detail=f"Invalid custom_policies JSON: {exc!s}") + + # Keep simulator values bounded so UI experiments cannot lock the API. + simulated_latency = max(0.0, min(float(simulated_latency), 5.0)) + simulated_server_state = simulated_server_state.lower().strip() + + trace_logs = [ + { + "span": "POST /api/compliance/upload", + "service": "python-extraction-agent", + "duration_ms": 140, + "status": "file_ingested", + }, + { + "span": "extract_contract_fields", + "service": "python-extraction-agent", + "duration_ms": 180, + "status": "fields_extracted", + }, + { + "span": "classify_risk_level", + "service": "python-extraction-agent", + "duration_ms": 25, + "status": risk["risk_tier"].lower(), + }, + ] + + _event(case, "agent", "Extractor completed", f"{details['contractor_name']} fields extracted and risk classified as {risk['risk_tier']}.") + + handoff = { + "status": "prepared", + "source_agent": "python-extraction-agent", + "target_agent": "Security Compliance Validator", + "remote_agent": "google.adk.agents.RemoteA2aAgent(compliance_agent)", + "agent_card_url": GO_AGENT_CARD_URL, + "jsonrpc_url": _go_jsonrpc_url(), + "method": "SendMessage", + "task_id": case.id, + "contract_details": details, + "risk_assessment": risk, + "policy_override": bool(policy), + "request": build_go_message_payload(case.id, details, policy), + } + + if simulated_latency: + await asyncio.sleep(simulated_latency) + + try: + if simulated_server_state == "crashed": + raise ConnectionError("Simulated Go compliance service 503 failure") + + handoff_result = await invoke_go_compliance_service( + case_id=case.id, + details=details, + policy=policy, + timeout=10.0, + ) + verdict = handoff_result["verdict"] + handoff.update( + { + "status": "completed", + "agent_card": handoff_result["agent_card"], + "request": handoff_result["request"], + "response": handoff_result["response"], + "adk_request_message": handoff_result["adk_request_message"], + "adk_response_task": handoff_result["adk_response_task"], + "verdict": verdict, + } + ) + current_step = ( + ComplianceStep.APPROVED + if verdict.get("passed", False) + else ComplianceStep.REVIEW_READY + ) + trace_logs.extend( + [ + { + "span": "GET /.well-known/agent.json", + "service": "go-compliance-agent", + "duration_ms": 45, + "status": "agent_card_discovered", + }, + { + "span": "ADK RemoteA2aAgent SendMessage", + "service": "go-compliance-agent", + "duration_ms": 120, + "status": "completed", + }, + { + "span": "go_validate_policy", + "service": "go-compliance-agent", + "duration_ms": 18, + "status": "passed" if verdict.get("passed", False) else "violations_found", + }, + ] + ) + except (ConnectionError, TimeoutError, urllib.error.URLError, RuntimeError) as exc: + logger.warning(f"Go compliance handoff failed for case {case.id}: {exc!s}") + current_step = ComplianceStep.MANUAL_REVIEW + verdict = { + "passed": False, + "violations": [ + "SYSTEM TIMEOUT: External compliance service failed to respond within 30-second threshold.", + "FAIL-SAFE ACTION: Document routed for legal manager manual verification.", + ], + "verdict_timestamp": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()), + } + handoff.update( + { + "status": "failed", + "error": str(exc), + "verdict": verdict, + } + ) + trace_logs.append( + { + "span": "resilience_fallback_gate", + "service": "python-extraction-agent", + "duration_ms": 450, + "status": "manual_review_routed", + } + ) + + session_state = { + "case_id": case.id, + "current_step": current_step, + "contract_filename": original_filename, + "contract_details": details, + "risk_assessment": risk, + "compliance_verdict": verdict, + "handoff": handoff, + "pending_signals": [], + "trace_logs": trace_logs, + "completion_time": time.strftime("%Y-%m-%d %H:%M:%S %Z"), + } + + sync_case_with_session_state(case, session_state) + return {"active": True, "case": case_payload(case)} + + +# --- Local Dev Server --- +if __name__ == "__main__": + import uvicorn + uvicorn.run(app, host="127.0.0.1", port=8000) diff --git a/agents/adk/contract-compliance-pipeline/python-extraction-agent/app/live_compliance.py b/agents/adk/contract-compliance-pipeline/python-extraction-agent/app/live_compliance.py new file mode 100644 index 0000000..85ef716 --- /dev/null +++ b/agents/adk/contract-compliance-pipeline/python-extraction-agent/app/live_compliance.py @@ -0,0 +1,682 @@ +# Copyright 2026 Google LLC +# +# 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 +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import html +import os +import time +import uuid +from dataclasses import dataclass, field +from pathlib import Path +from typing import Any +from fastapi import HTTPException +from fastapi.responses import HTMLResponse +from app.state_schema import ComplianceStep + +# Directories for artifact storage (relative to project root) +SANDBOX_ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) +ARTIFACTS_ROOT = os.path.join(SANDBOX_ROOT, "local_artifacts") + + +def _secure_artifact_dir(case_id: str) -> Path: + """Resolves target case directories securely and verifies bounds limit (Rule 8).""" + safe_case_id = os.path.basename(case_id) + target_dir = os.path.join(ARTIFACTS_ROOT, "compliance", safe_case_id) + + # Fully resolve absolute path boundaries + absolute_root = os.path.abspath(ARTIFACTS_ROOT) + os.path.sep + absolute_target = os.path.abspath(target_dir) + + if not absolute_target.startswith(absolute_root): + raise PermissionError("Access Denied: Path traversal detected on artifact write.") + + path_obj = Path(absolute_target) + path_obj.mkdir(parents=True, exist_ok=True) + return path_obj + + +@dataclass +class LiveComplianceCase: + id: str + session_id: str + user_id: str + filename: str + current_step: str = ComplianceStep.INGESTED + pending_signals: list[str] = field(default_factory=list) + status: str = "started" + adk_status: str = "session_created" + risk_tier: str = "MEDIUM" + passed: bool = False + handoff: dict[str, Any] = field(default_factory=dict) + events: list[dict[str, str]] = field(default_factory=list) + artifacts: list[dict[str, str]] = field(default_factory=list) + created_at: float = field(default_factory=time.time) + updated_at: float = field(default_factory=time.time) + + +CASES: dict[str, LiveComplianceCase] = {} +SESSION_TO_CASE: dict[str, str] = {} +LATEST_CASE_ID: str | None = None + + +def get_case(case_id: str) -> LiveComplianceCase: + case = CASES.get(case_id) + if not case: + raise HTTPException(status_code=404, detail="Compliance case not found") + return case + + +def _event(case: LiveComplianceCase, kind: str, title: str, detail: str) -> None: + case.events.insert(0, { + "kind": kind, + "title": title, + "detail": detail, + "time": time.strftime("%H:%M:%S"), + }) + case.updated_at = time.time() + + +def _artifact(case: LiveComplianceCase, artifact_id: str, title: str, filename: str) -> None: + href = f"/api/compliance/cases/{case.id}/artifacts/{artifact_id}" + existing = next((item for item in case.artifacts if item["id"] == artifact_id), None) + payload = { + "id": artifact_id, + "title": title, + "filename": filename, + "href": href, + "created_at": time.strftime("%H:%M:%S"), + } + if existing: + existing.update(payload) + else: + case.artifacts.insert(0, payload) + + +# --- HTML VISUAL REPORTS RENDERERS --- + +def _extracted_fields_html(case: LiveComplianceCase, details: dict, risk: dict) -> str: + factors_li = "".join([f"
  • {html.escape(f)}
  • " for f in risk.get("risk_factors", [])]) + if not factors_li: + factors_li = "
  • No significant legal risk factors identified.
  • " + + return f""" + + + + + Extraction Summary - Case {html.escape(case.id)} + + + +
    +
    +
    +

    Legal Ingestion Parameter Sheet

    +

    Extracted variables from raw document text by Extractor Specialist subagent.

    +
    +
    + Case: {html.escape(case.id[:8])}
    + Ingested: {html.escape(case.filename)} +
    +
    + +

    Core Parameters

    +
    +
    Contractor / Vendor{html.escape(details.get("contractor_name", "N/A"))}
    +
    Client Entity{html.escape(details.get("client_name", "N/A"))}
    +
    Total Contract Value${details.get("contract_value", 0.0):,.2f}
    +
    Contract Duration{details.get("term_length_years", 1)} year(s) ({html.escape(details.get("start_date", ""))} to {html.escape(details.get("end_date", ""))})
    +
    Liability Capping Limits{html.escape(details.get("liability_limit", "N/A"))}
    +
    Commercial Insurance Coverage${details.get("insurance_coverage", 0.0):,.2f}
    +
    Auto-Renewal Trigger{ "Yes (Auto-renewal enabled)" if details.get("auto_renewal") else "No (Term terminates cleanly)" }
    +
    Exit Notices Safety{ "Termination Safety Clause exists" if details.get("has_termination_clause") else "None (Missing standard exit notices!)" }
    +
    + +

    Specialist Risk Diagnostics

    +
    + Evaluated Risk Level Bounds + {html.escape(risk.get("risk_tier", "MEDIUM"))} Legal Risk +
    +

    Key Risk Indicators (KRIs)

    +
      + {factors_li} +
    +
    + +""" + + +def _compliance_cert_html(case: LiveComplianceCase, details: dict, verdict: dict) -> str: + passed = verdict.get("passed", False) + status_class = "approved" if passed else "flagged" + violations = [str(v) for v in verdict.get("violations", [])] + violation_count = len(violations) + if "SYSTEM TIMEOUT" in "".join(violations): + status_class = "manual" + + verdict_text = "PASSED COMPLIANCE" if passed else "FLAGGED FOR REVIEW" + if status_class == "manual": + verdict_text = "ROUTED FOR MANUAL REVIEW" + + verdict_caption = ( + "Zero policy threshold exceptions identified." + if passed + else f"{violation_count} policy exception{'s' if violation_count != 1 else ''} require legal review." + ) + if status_class == "manual": + verdict_caption = "Fail-close routing activated because the remote compliance service was unavailable." + + violations_li = "".join( + [ + ( + "
  • " + f"Exception {idx}" + f"{html.escape(v)}" + "
  • " + ) + for idx, v in enumerate(violations, 1) + ] + ) + if passed: + violations_li = "
  • ClearZero policy threshold exceptions identified.
  • " + elif not violations_li: + violations_li = "
  • ReviewGo returned a review verdict without enumerated policy exceptions.
  • " + + return f""" + + + + + Compliance Certificate - {html.escape(details.get("contractor_name", "Vendor"))} + + + +
    +
    +

    Contract Compliance Engine

    +

    A2A Audit Certificate

    +
    + +
    + {verdict_text} + {html.escape(verdict_caption)} +
    + +

    Validation Diagnostics Summary

    +

    + Contract compliance validation conducted by go-compliance-agent micro-service on a dedicated Go validation container running policy rules validation. +

    + +

    {violation_count}Policy Threshold Exception Log

    +
      + {violations_li} +
    + +
    +
    + Case transaction ID: {html.escape(case.id)}
    + A2A target container: go-compliance-agent:8888
    + Audit Timestamp: {html.escape(verdict.get("verdict_timestamp", ""))} +
    +
    + {f"
    Approved Safe
    " if passed else (f"
    Manual Review
    " if status_class == "manual" else "
    Review Required
    ")} +
    +
    +
    + +""" + + +# --- CORE CASE OPERATIONS --- + +async def create_compliance_case(filename: str, db_session_service) -> LiveComplianceCase: + """Scaffolds the active case data structure and registers persistent SQLite sessions.""" + global LATEST_CASE_ID + + case_id = str(uuid.uuid4()) + case = LiveComplianceCase( + id=case_id, + session_id=case_id, + user_id="ops_center", + filename=filename + ) + + # Register the session in the sqlite persistence engine (enables resume matching) + await db_session_service.create_session( + app_name="app", + user_id=case.user_id, + session_id=case.session_id, + state={ + "case_id": case.id, + "current_step": ComplianceStep.INGESTED, + "contract_filename": filename, + "contract_details": {}, + "risk_assessment": {}, + "compliance_verdict": {}, + "pending_signals": [] + } + ) + + _event(case, "system", "Pipeline Initialized", f"Compliance audit started for contract '{filename}'.") + _event(case, "agent", "Coordinator hydrated", "Contract extraction and Go compliance handoff ready.") + + CASES[case.id] = case + SESSION_TO_CASE[case.session_id] = case.id + LATEST_CASE_ID = case.id + return case + + +def save_artifact_file(case: LiveComplianceCase, artifact_id: str, title: str, filename: str, content: str) -> None: + """Saves generated visual HTML reports securely to sandboxed storage.""" + # Strip dangerous parameters to prevent directory traversals (Rule 8) + safe_filename = os.path.basename(filename) + + # Enforces boundary and writes + case_dir = _secure_artifact_dir(case.id) + target_path = case_dir / safe_filename + + target_path.write_text(content, encoding="utf-8") + _artifact(case, artifact_id, title, safe_filename) + + +def sync_case_with_session_state(case: LiveComplianceCase, state: dict) -> None: + """Synchronizes in-memory dashboard cases with ADK database state parameters.""" + if not state: + return + + case.current_step = state.get("current_step", case.current_step) + case.pending_signals = state.get("pending_signals", case.pending_signals) + + details = state.get("contract_details", {}) + risk = state.get("risk_assessment", {}) + verdict = state.get("compliance_verdict", {}) + handoff = state.get("handoff", {}) + if handoff: + case.handoff = handoff + + # Risk parameters mapping + if risk: + case.risk_tier = risk.get("risk_tier", case.risk_tier) + + # Verdict metrics mapping + if verdict: + case.passed = verdict.get("passed", False) + + # Synchronize tracking logs + if "trace_logs" in state: + # Avoid duplicate rendering in Cockpit dashboard + case.events = [e for e in case.events if e["kind"] != "trace"] + for log in state["trace_logs"]: + _event(case, "trace", f"Span completed: {log['span']}", f"Service: {log['service']} | Duration: {log['duration_ms']}ms | Status: {log['status']}") + + # Check transitions to write artifacts dynamically + if details and "parameters-sheet" not in [a["id"] for a in case.artifacts]: + # extraction artifact generated + html_param = _extracted_fields_html(case, details, risk) + save_artifact_file(case, "parameters-sheet", "Legal parameters sheet", "parameters_sheet.html", html_param) + _event(case, "agent", "Legal sheet generated", "A clean visual parameters sheet was compiled and attached to artifacts.") + + if verdict and "compliance-cert" not in [a["id"] for a in case.artifacts]: + # compliance verification card compiled + html_cert = _compliance_cert_html(case, details, verdict) + save_artifact_file(case, "compliance-cert", "Compliance A2A Certificate", "compliance_certificate.html", html_cert) + + passed_lbl = "APPROVED" if case.passed else "REJECTED (EXCEPTIONS)" + if "SYSTEM TIMEOUT" in "".join(verdict.get("violations", [])): + passed_lbl = "PENDING MANUAL REVIEW (TIMEOUT)" + + _event(case, "agent", "A2A certificate generated", f"Auditing verdict verified: {passed_lbl}.") + + # Sync visual status flags + if case.current_step == ComplianceStep.INGESTED: + case.status = "processing_extraction" + elif case.current_step == ComplianceStep.EXTRACTED: + case.status = "processing_risk" + elif case.current_step == ComplianceStep.COMPLIANCE_PENDING: + case.status = "waiting_a2a_task" + _event(case, "network", "A2A task pending", "Connection reports latency. State checkpoints saved for later completion.") + elif case.current_step == ComplianceStep.COMPLIANCE_COMPLETE: + case.status = "compiling_final_report" + elif case.current_step == ComplianceStep.MANUAL_REVIEW: + case.status = "manual_review_needed" + elif case.current_step == ComplianceStep.REVIEW_READY: + case.status = "review_completed_with_violations" + elif case.current_step == ComplianceStep.APPROVED: + case.status = "approved" + + case.updated_at = time.time() + + +def artifact_response(case_id: str, artifact_id: str) -> HTMLResponse: + """Serves case artifact documents dynamically, verifying bounds (Rule 8 & 120).""" + case = get_case(case_id) + artifact = next((item for item in case.artifacts if item["id"] == artifact_id), None) + if not artifact: + raise HTTPException(status_code=404, detail="Artifact reference not found") + + # Enforces boundary limits + safe_filename = os.path.basename(artifact["filename"]) + case_dir = _secure_artifact_dir(case.id) + target_path = case_dir / safe_filename + + if not target_path.exists(): + raise HTTPException(status_code=404, detail="Artifact physical report file missing") + + return HTMLResponse(target_path.read_text(encoding="utf-8")) + + +def case_payload(case: LiveComplianceCase) -> dict[str, Any]: + """Serializes the complete visual case payload for visual Cockpit frontend.""" + return { + "id": case.id, + "session_id": case.session_id, + "user_id": case.user_id, + "filename": case.filename, + "current_step": case.current_step, + "pending_signals": case.pending_signals, + "status": case.status, + "adk_status": case.adk_status, + "risk_tier": case.risk_tier, + "passed": case.passed, + "handoff": case.handoff, + "events": case.events, + "artifacts": case.artifacts, + "updated_at": case.updated_at, + } + + +def latest_case_payload() -> dict[str, Any]: + """Returns visual data on the latest audit case.""" + if not LATEST_CASE_ID or LATEST_CASE_ID not in CASES: + return {"active": False, "message": "No compliance cases processed yet."} + return {"active": True, "case": case_payload(CASES[LATEST_CASE_ID])} diff --git a/agents/adk/contract-compliance-pipeline/python-extraction-agent/app/state_schema.py b/agents/adk/contract-compliance-pipeline/python-extraction-agent/app/state_schema.py new file mode 100644 index 0000000..161bfee --- /dev/null +++ b/agents/adk/contract-compliance-pipeline/python-extraction-agent/app/state_schema.py @@ -0,0 +1,37 @@ +# Copyright 2026 Google LLC +# +# 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 +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from enum import Enum + + +class ComplianceStep(str, Enum): + """Pipeline state machine for contract compliance processing. + + Each contract moves through these states as it progresses through + the multi-agent pipeline. Using str+Enum ensures JSON serialization + compatibility with ADK's session state. + + State transitions: + INGESTED → EXTRACTED → COMPLIANCE_PENDING → COMPLIANCE_COMPLETE → APPROVED + → REVIEW_READY + → MANUAL_REVIEW (on timeout/failure) + """ + INGESTED = "INGESTED" # Contract uploaded, awaiting extraction + EXTRACTED = "EXTRACTED" # Fields extracted, ready for compliance check + COMPLIANCE_PENDING = "COMPLIANCE_PENDING" # A2A task sent to Go agent, awaiting result + COMPLIANCE_COMPLETE = "COMPLIANCE_COMPLETE" # Go agent returned compliance verdict + MANUAL_REVIEW = "MANUAL_REVIEW" # Timeout/error — routed for human review + REVIEW_READY = "REVIEW_READY" # Compliance failed — flagged for legal review + APPROVED = "APPROVED" # Compliance passed — contract approved + diff --git a/agents/adk/contract-compliance-pipeline/python-extraction-agent/app/static/live-compliance/index.html b/agents/adk/contract-compliance-pipeline/python-extraction-agent/app/static/live-compliance/index.html new file mode 100644 index 0000000..2c8c130 --- /dev/null +++ b/agents/adk/contract-compliance-pipeline/python-extraction-agent/app/static/live-compliance/index.html @@ -0,0 +1,2982 @@ + + + + + + Contract Compliance Engine - Multi-Agent A2A Panel + + + + + + +
    + + +
    +
    + +
    +

    Contract Compliance Engine

    +

    Building cross-language multi-agent systems using ADK and A2A

    +
    +
    +
    +
    +
    + Agent Server Online +
    +
    + 127.0.0.1:8000 +
    +
    +
    + + +
    +
    Pipeline Progress
    +
    + +
    +
    + Ingested +
    +
    + +
    +
    + Extracted +
    +
    + +
    +
    + A2A Handoff +
    +
    + +
    +
    + Audited +
    + +
    +
    + + +
    + + +
    +
    +
    + +

    Ops Portal Desk

    +
    + Idle +
    + +
    + Pick one of the pre-loaded vendor contracts from the corporate repository below to execute a real-time policy compliance audit. +
    + +
    + +
    +
    + standard-vendor-agreement.pdf + Acme Cloud Solutions | Value: $250k | Coverage: $2M +
    + +
    + +
    +
    + high-risk-liability-contract.pdf + Apex Data Systems | Value: $450k | Unlimited liability +
    + +
    + +
    +
    + non-compliant-contract.pdf + Legacy Networks Corp | Value: $850k | Value & term limit breach +
    + +
    + +
    + + + + +
    +
    + +

    Ambient ADK Event Ticker

    +
    +
    +
    +
    [System] Cockpit loaded. Session listening on 127.0.0.1:8000. Ready to dispatch.
    +
    + + +
    +
    + +

    Execution Spans Log

    +
    +
    +
    +
    +
    +
    + Framework initialized + SQLite persistence database loaded successfully. +
    +
    08:36:21
    +
    +
    +
    + + +
    +
    +
    + +

    Case Artifact Viewer

    +
    +
    +
    +
    + + +
    + + + +
    + +
    + Zero Auditing Runs Loaded +

    Upload or select a vendor contract above and trigger compliance validation to review extracted reports.

    +
    +
    +
    +
    + + +
    +
    +
    + +

    Agent Exchange

    +
    +
    + +
    + +
    +
    + +

    Live Agent Handoff

    +
    +
    +
    + Source + python-extraction-agent + Waiting for contract extraction. +
    +
    SendMessage
    +
    + Target + go-compliance-agent + Waiting for agent card discovery. +
    +
    +
    +
    + Agent Card + GET localhost:8888/.well-known/agent.json +
    +
    + Task ID + not dispatched +
    +
    +
    + + +
    +
    + +

    A2A Payload & Verdict

    +
    +
    Run an audit to see the structured A2A SendMessage payload that ADK sends to the Go agent.
    +
    No verdict returned yet.
    +
    +
    +
    + + +
    +
    +
    + +

    Run Configuration

    +
    +
    + +
    + +
    +
    +
    + +

    Distributed OTel Trace Spans

    +
    +
    +
    + +
    +
    + POST /api/compliance/upload + 0ms +
    +
    +
    + +
    +
    + extract_contract_fields + 0ms +
    +
    +
    + +
    +
    + GET /.well-known/agent.json + 0ms +
    +
    +
    + +
    +
    + ADK RemoteA2aAgent SendMessage + 0ms +
    +
    +
    + +
    +
    + go_validate_policy + 0ms +
    +
    +
    + + +
    +
    + + +
    +
    + A2A Simulator Mode + NORMAL +
    +
    + + + +
    +
    +
    + Simulated Network Delay + 1.5s +
    + +
    +
    + + +
    +
    +
    + +

    Active Compliance Policy

    +
    + Standard +
    + +
    +
    + Value Cap + $500k +
    +
    + Term Limit + 5 yrs +
    +
    + Insurance + $1M+ +
    +
    + +
    + Required Rule Set +
    + Exit clause + No unlimited liability + No >3yr renewal +
    +
    + + +
    +
    +
    + +
    + +
    + + + + + diff --git a/agents/adk/contract-compliance-pipeline/python-extraction-agent/app/tools.py b/agents/adk/contract-compliance-pipeline/python-extraction-agent/app/tools.py new file mode 100644 index 0000000..d96a5c4 --- /dev/null +++ b/agents/adk/contract-compliance-pipeline/python-extraction-agent/app/tools.py @@ -0,0 +1,415 @@ +# Copyright 2026 Google LLC +# +# 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 +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os +import re +import secrets +from pathlib import Path +from google.adk.tools import ToolContext +from app.state_schema import ComplianceStep + +# Bounded directories configuration for security path traversal check +# Local checkout root is two levels above python-extraction-agent/app. In the +# Docker image, only python-extraction-agent is copied, so fall back to /app. +_APP_DIR = Path(__file__).resolve().parent +_PYTHON_PROJECT_DIR = _APP_DIR.parent +_LOCAL_REPO_ROOT = _PYTHON_PROJECT_DIR.parent +SANDBOX_DIR = str( + _LOCAL_REPO_ROOT + if (_LOCAL_REPO_ROOT / "sample-contracts").exists() + else _PYTHON_PROJECT_DIR +) +UPLOAD_DIR = os.path.join(SANDBOX_DIR, "uploads") + + +def _secure_resolve_path(filename: str) -> str: + """Security Boundary Resolver (enforces path limits to prevent directory traversals). + + Rule 8 Enforcement: Sanitizes inputs and resolved target boundaries. + """ + # Active detection blocks (fail-close on traversal vectors) + if ".." in filename or "\\" in filename or (filename.startswith("/") and not filename.startswith(UPLOAD_DIR)): + raise PermissionError("Access Denied: Path traversal attack detected.") + + safe_filename = os.path.basename(filename) + target_path = os.path.join(UPLOAD_DIR, safe_filename) + + # Fully resolve absolute boundary paths + absolute_root = os.path.abspath(UPLOAD_DIR) + os.path.sep + absolute_target = os.path.abspath(target_path) + + if not absolute_target.startswith(absolute_root): + raise PermissionError("Access Denied: Path traversal attack detected.") + + return absolute_target + + +# Mock texts mapping for reliable local demo processing +MOCK_CONTRACT_TEXTS = { + "standard-vendor-agreement.pdf": """ + SOFTWARE SERVICES VENDOR AGREEMENT + + PARTIES: + This Agreement is entered into by and between ACME CLOUD SOLUTIONS ("Contractor") and GFD PLATFORM SYSTEMS ("Client"). + + VALUE & SERVICES: + Client agrees to pay Contractor a total consideration of $250,000.00 (Two Hundred and Fifty Thousand Dollars) for the performance of the cloud architectural services. + + TERM & COMMENCEMENT: + This Agreement shall commence on June 1, 2026, and shall continue in full force and effect until June 1, 2028 (a duration of exactly 2 years), unless terminated earlier in accordance with the terms herein. + + LIMITATION OF LIABILITY: + Except for breach of confidentiality obligations, each party's maximum aggregate liability to the other party under this Agreement shall be strictly capped and limited to $1,000,000.00 (One Million Dollars). Under no circumstances shall either party be liable for indirect or consequential damages. + + INSURANCE: + Contractor shall maintain Commercial General Liability insurance during the term of this agreement with a minimum coverage limit of $2,000,000.00. + + TERMINATION: + Either party may terminate this agreement upon 30 days prior written notice. + """, + + "high-risk-liability-contract.pdf": """ + ENTERPRISE CLOUD CONSULTING AGREEMENT + + PARTIES: + This Agreement is entered into by and between APEX DATA SYSTEMS ("Contractor") and GFD PLATFORM SYSTEMS ("Client"). + + VALUE & SERVICES: + Client agrees to pay Contractor a total fee of $450,000.00 for enterprise data engineering services. + + TERM & COMMENCEMENT: + This Agreement shall commence on June 1, 2026, and terminate on June 1, 2027 (1 year duration). + + LIMITATION OF LIABILITY: + LIABILITY LIMITS ARE EXPLICITLY WAIVED. CONTRACTOR LIABILITY SHALL BE UNLIMITED UNDER ALL CIRCUMSTANCES. GFD PLATFORM SYSTEMS ASSUMES FULL COMPENSATORY RESPONSIBILITY FOR ALL THIRD-PARTY CLAIMS ARISEN OUT OF PERFORMANCE ACTIONS. + + INSURANCE: + Contractor shall maintain standard professional liability insurance with minimum coverage of $1,500,000.00. + + TERMINATION: + Termination requires 30 days notice. + """, + + "non-compliant-contract.pdf": """ + LEGACY SYSTEMS INTEGRATION SERVICES CHARTER + + PARTIES: + This Agreement is entered into by and between LEGACY NETWORKS CORP ("Contractor") and GFD PLATFORM SYSTEMS ("Client"). + + VALUE & SERVICES: + Client agrees to pay Contractor a sum of $850,000.00 (Eight Hundred and Fifty Thousand Dollars) for legacy migration activities. + + TERM & COMMENCEMENT: + This Charter shall commence on June 1, 2026, and extend until June 1, 2032 (a total term of exactly 6 years). + + LIMITATION OF LIABILITY: + THE PARTIES EXPRESSLY AGREE THAT ALL CONTRACTOR RESPONSIBILITY LIMITS ARE INAPPLICABLE. CONTRACTOR'S TOTAL LIABILITY UNDER THIS CONTRACT SHALL BE ENTIRELY UNLIMITED. + + INSURANCE: + Contractor shall maintain general liability protection limits of $500,000.00. + + TERMINATION: + THIS AGREEMENT SHALL AUTOMATICALLY RENEW FOREVER WITH NO OPTION FOR TERMINATION OR WRITTEN EXIT NOTICES, EXCEPT ON TOTAL LIQUIDATION OF EITHER PARTY. + """ +} + +SAMPLE_CONTRACT_DETAILS = { + "standard-vendor-agreement.pdf": { + "contract_value": 250000.0, + "contractor_name": "ACME CLOUD SOLUTIONS", + "client_name": "GFD PLATFORM SYSTEMS", + "start_date": "2026-06-01", + "end_date": "2028-06-01", + "liability_limit": "$1,000,000.00", + "insurance_coverage": 2000000.0, + "auto_renewal": False, + "has_termination_clause": True, + "term_length_years": 2, + }, + "high-risk-liability-contract.pdf": { + "contract_value": 450000.0, + "contractor_name": "APEX DATA SYSTEMS", + "client_name": "GFD PLATFORM SYSTEMS", + "start_date": "2026-06-01", + "end_date": "2027-06-01", + "liability_limit": "unlimited liability", + "insurance_coverage": 1500000.0, + "auto_renewal": False, + "has_termination_clause": True, + "term_length_years": 1, + }, + "non-compliant-contract.pdf": { + "contract_value": 850000.0, + "contractor_name": "LEGACY NETWORKS CORP", + "client_name": "GFD PLATFORM SYSTEMS", + "start_date": "2026-06-01", + "end_date": "2032-06-01", + "liability_limit": "unlimited liability", + "insurance_coverage": 500000.0, + "auto_renewal": True, + "has_termination_clause": False, + "term_length_years": 6, + }, +} + +MONTHS = { + "january": "01", + "february": "02", + "march": "03", + "april": "04", + "may": "05", + "june": "06", + "july": "07", + "august": "08", + "september": "09", + "october": "10", + "november": "11", + "december": "12", +} + + +def _money_to_float(value: str) -> float: + return float(value.replace("$", "").replace(",", "")) + + +def _iso_date(month: str, day: str, year: str) -> str: + return f"{year}-{MONTHS[month.lower()]}-{int(day):02d}" + + +def extract_contract_details_from_text(filename: str, text: str) -> dict: + """Extracts deterministic contract fields from uploaded text for the cockpit demo.""" + sample_name = os.path.basename(filename) + normalized_text = text.strip() + if not normalized_text and sample_name in SAMPLE_CONTRACT_DETAILS: + return dict(SAMPLE_CONTRACT_DETAILS[sample_name]) + + amounts = re.findall(r"\$[\d,]+(?:\.\d{2})?", normalized_text) + dates = re.findall( + r"(January|February|March|April|May|June|July|August|September|October|November|December)\s+(\d{1,2}),\s+(\d{4})", + normalized_text, + flags=re.IGNORECASE, + ) + parties = re.search( + r"between\s+(.+?)\s+\(\"Contractor\"\)\s+and\s+(.+?)\s+\(\"Client\"\)", + normalized_text, + flags=re.IGNORECASE | re.DOTALL, + ) + duration = re.search(r"(\d+)\s+years?", normalized_text, flags=re.IGNORECASE) + + liability_section = "" + liability_match = re.search( + r"LIMITATION OF LIABILITY:\s*(.*?)(?:\n\s*\n|INSURANCE:)", + normalized_text, + flags=re.IGNORECASE | re.DOTALL, + ) + if liability_match: + liability_section = liability_match.group(1).strip() + + lower_text = normalized_text.lower() + termination_section = "" + termination_match = re.search( + r"TERMINATION:\s*(.*?)(?:\n\s*\n|$)", + normalized_text, + flags=re.IGNORECASE | re.DOTALL, + ) + if termination_match: + termination_section = termination_match.group(1).strip().lower() + + has_termination_clause = "terminate" in lower_text or "termination" in lower_text + blocked_termination_phrases = ( + "no option for termination", + "no option for termination or written exit", + "no termination", + "without termination", + ) + if any(phrase in termination_section for phrase in blocked_termination_phrases): + has_termination_clause = False + + return { + "contract_value": _money_to_float(amounts[0]) if amounts else 0.0, + "contractor_name": parties.group(1).strip() if parties else "Unknown Contractor", + "client_name": parties.group(2).strip() if parties else "Unknown Client", + "start_date": _iso_date(*dates[0]) if dates else "", + "end_date": _iso_date(*dates[1]) if len(dates) > 1 else "", + "liability_limit": liability_section or "unknown", + "insurance_coverage": _money_to_float(amounts[-1]) if len(amounts) > 1 else 0.0, + "auto_renewal": "automatically renew" in lower_text, + "has_termination_clause": has_termination_clause, + "term_length_years": int(duration.group(1)) if duration else 1, + } + + +def classify_contract_risk(details: dict) -> dict: + """Classifies legal risk from extracted details without requiring an ADK context.""" + liability = details.get("liability_limit", "").lower() + value = details.get("contract_value", 0.0) + term = details.get("term_length_years", 1) + risk_factors = [] + + if "unlimited" in liability or "none" in liability: + risk_factors.append("Unlimited contractor liability") + if value > 500000.0: + risk_factors.append("High financial obligation value (> $500k)") + if term > 5: + risk_factors.append("Extended engagement lifecycle duration (> 5 years)") + if not details.get("has_termination_clause", True): + risk_factors.append("Missing exit notice termination safety clauses") + + if len(risk_factors) >= 2 or "Unlimited contractor liability" in risk_factors: + risk_tier = "HIGH" + elif len(risk_factors) == 1: + risk_tier = "MEDIUM" + else: + risk_tier = "LOW" + + return {"risk_tier": risk_tier, "risk_factors": risk_factors} + + +def read_contract_text(file_path: str, tool_context: ToolContext) -> str: + """Reads contract file text content securely, verifying path boundaries. + + Args: + file_path: The filename or target path of the contract text fixture. + + Returns: + The raw text content extracted from the contract. + """ + try: + # Enforce sandbox isolation limits + secure_path = _secure_resolve_path(file_path) + filename = os.path.basename(secure_path) + + if os.path.exists(secure_path): + with open(secure_path, "r", encoding="utf-8", errors="ignore") as f: + return f.read() + + # Fallback registry keeps legacy tests and empty demo environments usable. + if filename in MOCK_CONTRACT_TEXTS: + return MOCK_CONTRACT_TEXTS[filename] + + return f"[Error: Target contract file '{filename}' was not found in uploads sandbox directory.]" + except Exception as e: + return f"[Error reading file: {e!s}]" + + +def save_extracted_fields( + contract_value: float, + contractor_name: str, + client_name: str, + start_date: str, + end_date: str, + liability_limit: str, + insurance_coverage: float, + auto_renewal: bool, + has_termination_clause: bool, + tool_context: ToolContext, +) -> dict: + """Saves extracted contract parameters directly to persistent ADK session state. + + Rule 24 & Masking Enforcement: Sensitive identifiers are secured. + """ + state = tool_context.state + + # Construct structured contract data profile + contract_data = { + "contract_value": contract_value, + "contractor_name": contractor_name, + "client_name": client_name, + "start_date": start_date, + "end_date": end_date, + "liability_limit": liability_limit, + "insurance_coverage": insurance_coverage, + "auto_renewal": auto_renewal, + "has_termination_clause": has_termination_clause, + } + + state["contract_details"] = contract_data + state["current_step"] = ComplianceStep.EXTRACTED + state["pending_signals"] = ["a2a_compliance_check"] + + # Calculate term length (approximate) + try: + start_year = int(start_date.split("-")[0]) + end_year = int(end_date.split("-")[0]) + term_years = max(1, end_year - start_year) + except Exception: + term_years = 1 + + contract_data["term_length_years"] = term_years + + return { + "status": "success", + "message": f"Contract parameters successfully extracted for {contractor_name}.", + "extracted_details": contract_data, + } + + +def classify_risk_level(tool_context: ToolContext) -> dict: + """Evaluates extracted clauses to determine initial legal risk tier. + + Returns: + A dictionary with risk classification verdict. + """ + state = tool_context.state + details = state.get("contract_details", {}) + + if not details: + return {"status": "error", "message": "No contract parameters extracted yet."} + + state["risk_assessment"] = classify_contract_risk(details) + + return { + "status": "success", + "risk_tier": state["risk_assessment"]["risk_tier"], + "risk_factors": state["risk_assessment"]["risk_factors"], + } + + +def generate_summary_report(tool_context: ToolContext) -> dict: + """Generates the final multi-agent compliance summary and HTML certificate. + + Writes safety report artifacts securely inside Sandbox target bounds. + """ + state = tool_context.state + case_id = state.get("case_id", "local-case") + details = state.get("contract_details", {}) + risk = state.get("risk_assessment", {}) + verdict = state.get("compliance_verdict", {}) + + if not details: + return {"status": "error", "message": "No data extracted."} + + # Set final steps + state["current_step"] = ComplianceStep.APPROVED if verdict.get("passed", False) else ComplianceStep.REVIEW_READY + state["pending_signals"] = [] + + report_data = { + "case_id": case_id, + "contractor": details.get("contractor_name", "Unknown Contractor"), + "client": details.get("client_name", "GFD Platform Systems"), + "value": details.get("contract_value", 0.0), + "term": f"{details.get('start_date')} to {details.get('end_date')} ({details.get('term_length_years')} yrs)", + "risk_tier": risk.get("risk_tier", "MEDIUM"), + "risk_factors": risk.get("risk_factors", []), + "passed": verdict.get("passed", False), + "violations": verdict.get("violations", []), + "timestamp": state.get("completion_time", "N/A"), + } + + state["final_report"] = report_data + return { + "status": "success", + "message": f"Legal audit report compiled successfully for case {case_id}.", + "report_profile": report_data, + } diff --git a/agents/adk/contract-compliance-pipeline/python-extraction-agent/pyproject.toml b/agents/adk/contract-compliance-pipeline/python-extraction-agent/pyproject.toml new file mode 100644 index 0000000..69f6820 --- /dev/null +++ b/agents/adk/contract-compliance-pipeline/python-extraction-agent/pyproject.toml @@ -0,0 +1,68 @@ +[project] +name = "contract-compliance-pipeline" +version = "0.1.0" +description = "Cross-language multi-agent system for contract compliance using ADK and A2A" +dependencies = [ + "google-adk>=1.15.0,<2.0.0", + "opentelemetry-instrumentation-google-genai>=0.1.0,<1.0.0", + "google-cloud-logging>=3.12.0,<4.0.0", + "google-cloud-aiplatform[agent-engines,evaluation]>=1.130.0", + "protobuf>=6.31.1,<7.0.0", + "greenlet>=3.0.0,<4.0.0", + "a2a-sdk>=0.3.4,<0.4", +] +requires-python = ">=3.11,<3.14" + +[dependency-groups] +dev = [ + "pytest>=8.3.4,<9.0.0", + "pytest-asyncio>=0.23.8,<1.0.0", + "nest-asyncio>=1.6.0,<2.0.0", +] + +[project.optional-dependencies] +eval = [ + "google-adk[eval]>=1.15.0,<2.0.0", +] +lint = [ + "ruff>=0.4.6,<1.0.0", + "ty>=0.0.1a0", + "codespell>=2.2.0,<3.0.0", +] + +[tool.ruff] +line-length = 88 +target-version = "py311" + +[tool.ruff.lint] +select = [ + "E", # pycodestyle + "F", # pyflakes + "W", # pycodestyle warnings + "I", # isort + "C", # flake8-comprehensions + "B", # flake8-bugbear + "UP", # pyupgrade + "RUF", # ruff specific rules +] +ignore = ["E501", "C901", "B006"] + +[tool.ruff.lint.isort] +known-first-party = ["app"] + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.pytest.ini_options] +pythonpath = "." +asyncio_default_fixture_loop_scope = "function" + +[tool.hatch.build.targets.wheel] +packages = ["app"] + +[tool.agents-cli] +name = "contract-compliance-pipeline" +description = "Cross-language multi-agent system for contract compliance using ADK and A2A" +base_template = "adk" +agent_directory = "app" diff --git a/agents/adk/contract-compliance-pipeline/python-extraction-agent/tests/eval/evalsets/a2a_mock_eval.json b/agents/adk/contract-compliance-pipeline/python-extraction-agent/tests/eval/evalsets/a2a_mock_eval.json new file mode 100644 index 0000000..006f59a --- /dev/null +++ b/agents/adk/contract-compliance-pipeline/python-extraction-agent/tests/eval/evalsets/a2a_mock_eval.json @@ -0,0 +1,132 @@ +{ + "description": "Mocked A2A response tests — validates Python agent handles every compliance outcome correctly", + "test_cases": [ + { + "name": "compliance_pass", + "description": "All contract terms are within policy limits. Agent should mark as APPROVED.", + "mocked_a2a_response": { + "jsonrpc": "2.0", + "id": "test-pass-001", + "result": { + "id": "task-pass-001", + "status": { + "state": "completed", + "message": { + "role": "agent", + "parts": [ + { + "type": "data", + "data": { + "passed": true, + "violations": [], + "verdict_timestamp": "2026-06-01T12:00:00Z" + } + } + ] + } + } + } + }, + "expected_pipeline_state": { + "current_step": "APPROVED", + "compliance_verdict": { + "passed": true, + "violations": [] + } + } + }, + { + "name": "compliance_fail_single_violation", + "description": "Contract has unlimited liability — should flag for review with violation details.", + "mocked_a2a_response": { + "jsonrpc": "2.0", + "id": "test-fail-001", + "result": { + "id": "task-fail-001", + "status": { + "state": "completed", + "message": { + "role": "agent", + "parts": [ + { + "type": "data", + "data": { + "passed": false, + "violations": [ + "Unlimited contractor liability clauses are strictly prohibited by company policy" + ], + "verdict_timestamp": "2026-06-01T12:01:00Z" + } + } + ] + } + } + } + }, + "expected_pipeline_state": { + "current_step": "REVIEW_READY", + "compliance_verdict": { + "passed": false, + "violations": [ + "Unlimited contractor liability clauses are strictly prohibited by company policy" + ] + } + } + }, + { + "name": "compliance_fail_multiple_violations", + "description": "Contract has 6 policy violations — should flag with full violation list.", + "mocked_a2a_response": { + "jsonrpc": "2.0", + "id": "test-fail-002", + "result": { + "id": "task-fail-002", + "status": { + "state": "completed", + "message": { + "role": "agent", + "parts": [ + { + "type": "data", + "data": { + "passed": false, + "violations": [ + "Contract value $850000.00 exceeds company framework limit of $500000.00", + "Unlimited contractor liability clauses are strictly prohibited by company policy", + "Auto-renewal spans extending beyond 3 years are unauthorized", + "Insurance coverage $500000.00 is under the security minimum threshold of $1000000.00", + "Engagement length of 6 years exceeds framework limit bounds of 5 years", + "Missing required written exit termination safety clauses" + ], + "verdict_timestamp": "2026-06-01T12:02:00Z" + } + } + ] + } + } + } + }, + "expected_pipeline_state": { + "current_step": "REVIEW_READY", + "compliance_verdict": { + "passed": false + } + } + }, + { + "name": "compliance_timeout", + "description": "Go compliance agent does not respond within 30 seconds — should fall back to MANUAL_REVIEW.", + "mocked_a2a_response": null, + "timeout_seconds": 30, + "expected_pipeline_state": { + "current_step": "MANUAL_REVIEW", + "compliance_verdict": { + "passed": false, + "violations": [ + "SYSTEM TIMEOUT: External compliance service failed to respond within 30-second threshold." + ] + } + } + } + ] +} diff --git a/agents/adk/contract-compliance-pipeline/python-extraction-agent/tests/eval/evalsets/extraction_eval.json b/agents/adk/contract-compliance-pipeline/python-extraction-agent/tests/eval/evalsets/extraction_eval.json new file mode 100644 index 0000000..e43fd8a --- /dev/null +++ b/agents/adk/contract-compliance-pipeline/python-extraction-agent/tests/eval/evalsets/extraction_eval.json @@ -0,0 +1,73 @@ +{ + "description": "Golden evaluation tests for contract field extraction accuracy", + "test_cases": [ + { + "name": "standard_vendor_agreement_extraction", + "input": { + "filename": "standard-vendor-agreement.pdf", + "user_message": "Audit contract file: 'standard-vendor-agreement.pdf'." + }, + "expected_fields": { + "contract_value": 250000.0, + "contractor_name": "ACME CLOUD SOLUTIONS", + "client_name": "GFD PLATFORM SYSTEMS", + "start_date": "2026-06-01", + "end_date": "2028-06-01", + "liability_limit": "$1,000,000", + "insurance_coverage": 2000000.0, + "auto_renewal": false, + "has_termination_clause": true, + "term_length_years": 2 + }, + "expected_risk_tier": "LOW", + "expected_risk_factors": [] + }, + { + "name": "high_risk_liability_extraction", + "input": { + "filename": "high-risk-liability-contract.pdf", + "user_message": "Audit contract file: 'high-risk-liability-contract.pdf'." + }, + "expected_fields": { + "contract_value": 450000.0, + "contractor_name": "APEX DATA SYSTEMS", + "client_name": "GFD PLATFORM SYSTEMS", + "start_date": "2026-06-01", + "end_date": "2027-06-01", + "liability_limit": "unlimited", + "insurance_coverage": 1500000.0, + "auto_renewal": false, + "has_termination_clause": true, + "term_length_years": 1 + }, + "expected_risk_tier": "HIGH", + "expected_risk_factors": ["Unlimited contractor liability"] + }, + { + "name": "non_compliant_contract_extraction", + "input": { + "filename": "non-compliant-contract.pdf", + "user_message": "Audit contract file: 'non-compliant-contract.pdf'." + }, + "expected_fields": { + "contract_value": 850000.0, + "contractor_name": "LEGACY NETWORKS CORP", + "client_name": "GFD PLATFORM SYSTEMS", + "start_date": "2026-06-01", + "end_date": "2032-06-01", + "liability_limit": "unlimited", + "insurance_coverage": 500000.0, + "auto_renewal": true, + "has_termination_clause": false, + "term_length_years": 6 + }, + "expected_risk_tier": "HIGH", + "expected_risk_factors": [ + "Unlimited contractor liability", + "High financial obligation value (> $500k)", + "Extended engagement lifecycle duration (> 5 years)", + "Missing exit notice termination safety clauses" + ] + } + ] +} diff --git a/agents/adk/contract-compliance-pipeline/python-extraction-agent/tests/unit/test_extraction.py b/agents/adk/contract-compliance-pipeline/python-extraction-agent/tests/unit/test_extraction.py new file mode 100644 index 0000000..e139cf2 --- /dev/null +++ b/agents/adk/contract-compliance-pipeline/python-extraction-agent/tests/unit/test_extraction.py @@ -0,0 +1,155 @@ +# Copyright 2026 Google LLC +# +# 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 +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import pytest +from pathlib import Path +from unittest.mock import MagicMock +from google.adk.tools import ToolContext +from app.state_schema import ComplianceStep +from app.tools import ( + _secure_resolve_path, + classify_contract_risk, + classify_risk_level, + extract_contract_details_from_text, + read_contract_text, + save_extracted_fields, +) + + +PROJECT_ROOT = Path(__file__).resolve().parents[3] + + +@pytest.fixture +def mock_tool_context(): + """Fixture initializing state mapping structures.""" + context = MagicMock(spec=ToolContext) + context.state = { + "case_id": "test-case-id", + "current_step": ComplianceStep.INGESTED, + "contract_details": {}, + "risk_assessment": {}, + "compliance_verdict": {}, + "pending_signals": [], + "trace_logs": [] + } + return context + + +def test_read_contract_text_mock_loading(mock_tool_context): + # Acme contract mock loading check + text = read_contract_text("standard-vendor-agreement.pdf", mock_tool_context) + assert "ACME CLOUD SOLUTIONS" in text + assert "GFD PLATFORM SYSTEMS" in text + assert "$250,000.00" in text + + +def test_extract_contract_details_from_physical_samples(): + sample_dir = PROJECT_ROOT / "sample-contracts" + + standard_text = (sample_dir / "standard-vendor-agreement.pdf").read_text() + standard = extract_contract_details_from_text( + "standard-vendor-agreement.pdf", + standard_text, + ) + assert standard["contract_value"] == 250000.0 + assert standard["contractor_name"] == "ACME CLOUD SOLUTIONS" + assert standard["insurance_coverage"] == 2000000.0 + assert standard["term_length_years"] == 2 + assert standard["has_termination_clause"] is True + assert classify_contract_risk(standard)["risk_tier"] == "LOW" + + non_compliant_text = (sample_dir / "non-compliant-contract.pdf").read_text() + non_compliant = extract_contract_details_from_text( + "non-compliant-contract.pdf", + non_compliant_text, + ) + assert non_compliant["contract_value"] == 850000.0 + assert non_compliant["contractor_name"] == "LEGACY NETWORKS CORP" + assert non_compliant["insurance_coverage"] == 500000.0 + assert non_compliant["auto_renewal"] is True + assert non_compliant["has_termination_clause"] is False + assert classify_contract_risk(non_compliant)["risk_tier"] == "HIGH" + + +def test_read_contract_text_path_traversal_denied(mock_tool_context): + # Verify core resolver raises correct PermissionError boundary violations (Rule 8) + with pytest.raises(PermissionError, match="Access Denied: Path traversal attack detected."): + _secure_resolve_path("../../../etc/passwd") + + # Verify client facing tool catches exception and returns safe diagnostic string + res = read_contract_text("../../../etc/passwd", mock_tool_context) + assert "[Error reading file: Access Denied: Path traversal attack detected.]" in res + + +def test_save_extracted_fields_state_mutations(mock_tool_context): + details = { + "contract_value": 300000.0, + "contractor_name": "Test Vendor Corp", + "client_name": "GFD Platform Systems", + "start_date": "2026-06-01", + "end_date": "2029-06-01", + "liability_limit": "$1,500,000 limits cap", + "insurance_coverage": 2000000.0, + "auto_renewal": False, + "has_termination_clause": True, + } + + res = save_extracted_fields( + contract_value=details["contract_value"], + contractor_name=details["contractor_name"], + client_name=details["client_name"], + start_date=details["start_date"], + end_date=details["end_date"], + liability_limit=details["liability_limit"], + insurance_coverage=details["insurance_coverage"], + auto_renewal=details["auto_renewal"], + has_termination_clause=details["has_termination_clause"], + tool_context=mock_tool_context + ) + + assert res["status"] == "success" + + # Assert persistent mutations mapping + state = mock_tool_context.state + assert state["current_step"] == ComplianceStep.EXTRACTED + assert state["contract_details"]["contractor_name"] == "Test Vendor Corp" + assert state["contract_details"]["term_length_years"] == 3 # (2029 - 2026) + + +def test_classify_risk_level_tiers(mock_tool_context): + state = mock_tool_context.state + + # 1. Low risk evaluation mapping + state["contract_details"] = { + "contract_value": 150000.0, + "contractor_name": "A", + "liability_limit": "$1M limited cap", + "term_length_years": 2, + "has_termination_clause": True, + } + classify_risk_level(mock_tool_context) + assert state["risk_assessment"]["risk_tier"] == "LOW" + assert len(state["risk_assessment"]["risk_factors"]) == 0 + + # 2. High risk evaluation matching unlimited liability + state["contract_details"] = { + "contract_value": 250000.0, + "contractor_name": "B", + "liability_limit": "Unlimited GFD Liability exposure", + "term_length_years": 3, + "has_termination_clause": True, + } + classify_risk_level(mock_tool_context) + assert state["risk_assessment"]["risk_tier"] == "HIGH" + assert "Unlimited contractor liability" in state["risk_assessment"]["risk_factors"] diff --git a/agents/adk/contract-compliance-pipeline/python-extraction-agent/tests/unit/test_fallback.py b/agents/adk/contract-compliance-pipeline/python-extraction-agent/tests/unit/test_fallback.py new file mode 100644 index 0000000..c5ad280 --- /dev/null +++ b/agents/adk/contract-compliance-pipeline/python-extraction-agent/tests/unit/test_fallback.py @@ -0,0 +1,106 @@ +# Copyright 2026 Google LLC +# +# 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 +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import pytest +import asyncio +import logging +from unittest.mock import MagicMock, patch +from google.adk.tools import ToolContext +from app.state_schema import ComplianceStep +from app.fallback_handler import invoke_resilient_compliance_check + +logger = logging.getLogger(__name__) + + +@pytest.fixture +def mock_tool_context(): + """Fixture initializing case checkpoints.""" + context = MagicMock(spec=ToolContext) + context.state = { + "case_id": "test-resilience-case", + "current_step": ComplianceStep.EXTRACTED, + "contract_details": { + "contract_value": 250000.0, + "contractor_name": "Standard Corp", + "liability_limit": "Capped liability limits", + "term_length_years": 2, + "insurance_coverage": 2000000.0, + "has_termination_clause": True, + }, + "risk_assessment": {"risk_tier": "LOW", "risk_factors": []}, + "compliance_verdict": {}, + "pending_signals": [], + "trace_logs": [] + } + return context + + +@pytest.mark.asyncio +async def test_invoke_resilient_compliance_check_success(mock_tool_context): + # Mock A2A checker to return immediate success verdict + mock_response = { + "status": "success", + "verdict": {"passed": True, "violations": []} + } + + with patch("app.fallback_handler.invoke_a2a_compliance_check", return_value=mock_response): + res = await invoke_resilient_compliance_check(mock_tool_context, timeout_sec=2.0) + assert res["status"] == "success" + assert res["verdict"]["passed"] is True + + +@pytest.mark.asyncio +async def test_invoke_resilient_compliance_check_connection_crashed_retry(mock_tool_context): + # Proves 503 Crashed Server retry behavior + # Mock failure on first attempt, then success on second + attempt_count = 0 + + async def mock_compliance_behavior(context): + nonlocal attempt_count + attempt_count += 1 + if attempt_count == 1: + raise ConnectionError("503 Server Crashed.") + return { + "status": "success", + "verdict": {"passed": True, "violations": []} + } + + with patch("app.fallback_handler.invoke_a2a_compliance_check", side_effect=mock_compliance_behavior): + res = await invoke_resilient_compliance_check(mock_tool_context, timeout_sec=5.0, max_retries=3, backoff_factor=0.1) + assert res["status"] == "success" + assert attempt_count == 2 # Should retry once and then succeed! + + +@pytest.mark.asyncio +async def test_invoke_resilient_compliance_check_timeout_fallback(mock_tool_context): + # Proves 30s Timeout Recovery Transition (enforces fail-close to MANUAL_REVIEW) + # Mock connection lag (sleeps beyond timeout boundaries limit) + async def mock_lagging_behavior(context): + await asyncio.sleep(5.0) # Sleeps 5 seconds + return {"status": "success"} + + with patch("app.fallback_handler.invoke_a2a_compliance_check", side_effect=mock_lagging_behavior): + # Set target timeout barrier to 1.0 second + res = await invoke_resilient_compliance_check(mock_tool_context, timeout_sec=1.0, max_retries=1) + + # Verify safety transitions + assert res["status"] == "fallback_manual_review" + + state = mock_tool_context.state + assert state["current_step"] == ComplianceStep.MANUAL_REVIEW + assert state["compliance_verdict"]["passed"] is False + assert "SYSTEM TIMEOUT" in state["compliance_verdict"]["violations"][0] + assert "manual_review" in [span["status"] for span in state["trace_logs"]][0] + + logger.info("Resilience verification test PASSED successfully.") diff --git a/agents/adk/contract-compliance-pipeline/python-extraction-agent/uv.lock b/agents/adk/contract-compliance-pipeline/python-extraction-agent/uv.lock new file mode 100644 index 0000000..c4d47fa --- /dev/null +++ b/agents/adk/contract-compliance-pipeline/python-extraction-agent/uv.lock @@ -0,0 +1,3487 @@ +version = 1 +revision = 3 +requires-python = ">=3.11, <3.14" +resolution-markers = [ + "python_full_version >= '3.13' and sys_platform == 'win32'", + "python_full_version >= '3.13' and sys_platform == 'emscripten'", + "python_full_version >= '3.13' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version < '3.13' and sys_platform == 'win32'", + "python_full_version < '3.13' and sys_platform == 'emscripten'", + "python_full_version < '3.13' and sys_platform != 'emscripten' and sys_platform != 'win32'", +] + +[[package]] +name = "a2a-sdk" +version = "0.3.26" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "google-api-core" }, + { name = "httpx" }, + { name = "httpx-sse" }, + { name = "protobuf" }, + { name = "pydantic" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/be/97/a6840e01795b182ce751ca165430d46459927cde9bfab838087cbb24aef7/a2a_sdk-0.3.26.tar.gz", hash = "sha256:44068e2d037afbb07ab899267439e9bc7eaa7ac2af94f1e8b239933c993ad52d", size = 274598, upload-time = "2026-04-09T15:21:13.902Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dd/d5/51f4ee1bf3b736add42a542d3c8a3fd3fa85f3d36c17972127defc46c26f/a2a_sdk-0.3.26-py3-none-any.whl", hash = "sha256:754e0573f6d33b225c1d8d51f640efa69cbbed7bdfb06ce9c3540ea9f58d4a91", size = 151016, upload-time = "2026-04-09T15:21:12.35Z" }, +] + +[[package]] +name = "absl-py" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/64/c7/8de93764ad66968d19329a7e0c147a2bb3c7054c554d4a119111b8f9440f/absl_py-2.4.0.tar.gz", hash = "sha256:8c6af82722b35cf71e0f4d1d47dcaebfff286e27110a99fc359349b247dfb5d4", size = 116543, upload-time = "2026-01-28T10:17:05.322Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/a6/907a406bb7d359e6a63f99c313846d9eec4f7e6f7437809e03aa00fa3074/absl_py-2.4.0-py3-none-any.whl", hash = "sha256:88476fd881ca8aab94ffa78b7b6c632a782ab3ba1cd19c9bd423abc4fb4cd28d", size = 135750, upload-time = "2026-01-28T10:17:04.19Z" }, +] + +[[package]] +name = "aiohappyeyeballs" +version = "2.6.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/33/c6/61a2d7b7572279226bb2e7f61d7a19ca7c90da0329c93fa0d560cbf288d8/aiohappyeyeballs-2.6.2.tar.gz", hash = "sha256:e202810ee718bd01fc6ef49e8ea53d023d5cb6b581076d7925aa499fa55dbe64", size = 22591, upload-time = "2026-05-20T15:12:24.631Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5f/fc/a7bf5b6e4e617b45f90f2d9d2a68519c249c81dd4fc2658c7a2a61c4f4b7/aiohappyeyeballs-2.6.2-py3-none-any.whl", hash = "sha256:4708045e2d7a6c6bdf8aafa8ed39649eaf926a4543b54560659129e3365953c4", size = 15062, upload-time = "2026-05-20T15:12:23.328Z" }, +] + +[[package]] +name = "aiohttp" +version = "3.13.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohappyeyeballs" }, + { name = "aiosignal" }, + { name = "attrs" }, + { name = "frozenlist" }, + { name = "multidict" }, + { name = "propcache" }, + { name = "yarl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/45/4a/064321452809dae953c1ed6e017504e72551a26b6f5708a5a80e4bf556ff/aiohttp-3.13.4.tar.gz", hash = "sha256:d97a6d09c66087890c2ab5d49069e1e570583f7ac0314ecf98294c1b6aaebd38", size = 7859748, upload-time = "2026-03-28T17:19:40.6Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d4/7e/cb94129302d78c46662b47f9897d642fd0b33bdfef4b73b20c6ced35aa4c/aiohttp-3.13.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8ea0c64d1bcbf201b285c2246c51a0c035ba3bbd306640007bc5844a3b4658c1", size = 760027, upload-time = "2026-03-28T17:15:33.022Z" }, + { url = "https://files.pythonhosted.org/packages/5e/cd/2db3c9397c3bd24216b203dd739945b04f8b87bb036c640da7ddb63c75ef/aiohttp-3.13.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6f742e1fa45c0ed522b00ede565e18f97e4cf8d1883a712ac42d0339dfb0cce7", size = 508325, upload-time = "2026-03-28T17:15:34.714Z" }, + { url = "https://files.pythonhosted.org/packages/36/a3/d28b2722ec13107f2e37a86b8a169897308bab6a3b9e071ecead9d67bd9b/aiohttp-3.13.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6dcfb50ee25b3b7a1222a9123be1f9f89e56e67636b561441f0b304e25aaef8f", size = 502402, upload-time = "2026-03-28T17:15:36.409Z" }, + { url = "https://files.pythonhosted.org/packages/fa/d6/acd47b5f17c4430e555590990a4746efbcb2079909bb865516892bf85f37/aiohttp-3.13.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3262386c4ff370849863ea93b9ea60fd59c6cf56bf8f93beac625cf4d677c04d", size = 1771224, upload-time = "2026-03-28T17:15:38.223Z" }, + { url = "https://files.pythonhosted.org/packages/98/af/af6e20113ba6a48fd1cd9e5832c4851e7613ef50c7619acdaee6ec5f1aff/aiohttp-3.13.4-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:473bb5aa4218dd254e9ae4834f20e31f5a0083064ac0136a01a62ddbae2eaa42", size = 1731530, upload-time = "2026-03-28T17:15:39.988Z" }, + { url = "https://files.pythonhosted.org/packages/81/16/78a2f5d9c124ad05d5ce59a9af94214b6466c3491a25fb70760e98e9f762/aiohttp-3.13.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e56423766399b4c77b965f6aaab6c9546617b8994a956821cc507d00b91d978c", size = 1827925, upload-time = "2026-03-28T17:15:41.944Z" }, + { url = "https://files.pythonhosted.org/packages/2a/1f/79acf0974ced805e0e70027389fccbb7d728e6f30fcac725fb1071e63075/aiohttp-3.13.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8af249343fafd5ad90366a16d230fc265cf1149f26075dc9fe93cfd7c7173942", size = 1923579, upload-time = "2026-03-28T17:15:44.071Z" }, + { url = "https://files.pythonhosted.org/packages/af/53/29f9e2054ea6900413f3b4c3eb9d8331f60678ec855f13ba8714c47fd48d/aiohttp-3.13.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bc0a5cf4f10ef5a2c94fdde488734b582a3a7a000b131263e27c9295bd682d9", size = 1767655, upload-time = "2026-03-28T17:15:45.911Z" }, + { url = "https://files.pythonhosted.org/packages/f3/57/462fe1d3da08109ba4aa8590e7aed57c059af2a7e80ec21f4bac5cfe1094/aiohttp-3.13.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5c7ff1028e3c9fc5123a865ce17df1cb6424d180c503b8517afbe89aa566e6be", size = 1630439, upload-time = "2026-03-28T17:15:48.11Z" }, + { url = "https://files.pythonhosted.org/packages/d7/4b/4813344aacdb8127263e3eec343d24e973421143826364fa9fc847f6283f/aiohttp-3.13.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ba5cf98b5dcb9bddd857da6713a503fa6d341043258ca823f0f5ab7ab4a94ee8", size = 1745557, upload-time = "2026-03-28T17:15:50.13Z" }, + { url = "https://files.pythonhosted.org/packages/d4/01/1ef1adae1454341ec50a789f03cfafe4c4ac9c003f6a64515ecd32fe4210/aiohttp-3.13.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:d85965d3ba21ee4999e83e992fecb86c4614d6920e40705501c0a1f80a583c12", size = 1741796, upload-time = "2026-03-28T17:15:52.351Z" }, + { url = "https://files.pythonhosted.org/packages/22/04/8cdd99af988d2aa6922714d957d21383c559835cbd43fbf5a47ddf2e0f05/aiohttp-3.13.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:49f0b18a9b05d79f6f37ddd567695943fcefb834ef480f17a4211987302b2dc7", size = 1805312, upload-time = "2026-03-28T17:15:54.407Z" }, + { url = "https://files.pythonhosted.org/packages/fb/7f/b48d5577338d4b25bbdbae35c75dbfd0493cb8886dc586fbfb2e90862239/aiohttp-3.13.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7f78cb080c86fbf765920e5f1ef35af3f24ec4314d6675d0a21eaf41f6f2679c", size = 1621751, upload-time = "2026-03-28T17:15:56.564Z" }, + { url = "https://files.pythonhosted.org/packages/bc/89/4eecad8c1858e6d0893c05929e22343e0ebe3aec29a8a399c65c3cc38311/aiohttp-3.13.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:67a3ec705534a614b68bbf1c70efa777a21c3da3895d1c44510a41f5a7ae0453", size = 1826073, upload-time = "2026-03-28T17:15:58.489Z" }, + { url = "https://files.pythonhosted.org/packages/f5/5c/9dc8293ed31b46c39c9c513ac7ca152b3c3d38e0ea111a530ad12001b827/aiohttp-3.13.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d6630ec917e85c5356b2295744c8a97d40f007f96a1c76bf1928dc2e27465393", size = 1760083, upload-time = "2026-03-28T17:16:00.677Z" }, + { url = "https://files.pythonhosted.org/packages/1e/19/8bbf6a4994205d96831f97b7d21a0feed120136e6267b5b22d229c6dc4dc/aiohttp-3.13.4-cp311-cp311-win32.whl", hash = "sha256:54049021bc626f53a5394c29e8c444f726ee5a14b6e89e0ad118315b1f90f5e3", size = 439690, upload-time = "2026-03-28T17:16:02.902Z" }, + { url = "https://files.pythonhosted.org/packages/0c/f5/ac409ecd1007528d15c3e8c3a57d34f334c70d76cfb7128a28cffdebd4c1/aiohttp-3.13.4-cp311-cp311-win_amd64.whl", hash = "sha256:c033f2bc964156030772d31cbf7e5defea181238ce1f87b9455b786de7d30145", size = 463824, upload-time = "2026-03-28T17:16:05.058Z" }, + { url = "https://files.pythonhosted.org/packages/1e/bd/ede278648914cabbabfdf95e436679b5d4156e417896a9b9f4587169e376/aiohttp-3.13.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ee62d4471ce86b108b19c3364db4b91180d13fe3510144872d6bad5401957360", size = 752158, upload-time = "2026-03-28T17:16:06.901Z" }, + { url = "https://files.pythonhosted.org/packages/90/de/581c053253c07b480b03785196ca5335e3c606a37dc73e95f6527f1591fe/aiohttp-3.13.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c0fd8f41b54b58636402eb493afd512c23580456f022c1ba2db0f810c959ed0d", size = 501037, upload-time = "2026-03-28T17:16:08.82Z" }, + { url = "https://files.pythonhosted.org/packages/fa/f9/a5ede193c08f13cc42c0a5b50d1e246ecee9115e4cf6e900d8dbd8fd6acb/aiohttp-3.13.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4baa48ce49efd82d6b1a0be12d6a36b35e5594d1dd42f8bfba96ea9f8678b88c", size = 501556, upload-time = "2026-03-28T17:16:10.63Z" }, + { url = "https://files.pythonhosted.org/packages/d6/10/88ff67cd48a6ec36335b63a640abe86135791544863e0cfe1f065d6cef7a/aiohttp-3.13.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d738ebab9f71ee652d9dbd0211057690022201b11197f9a7324fd4dba128aa97", size = 1757314, upload-time = "2026-03-28T17:16:12.498Z" }, + { url = "https://files.pythonhosted.org/packages/8b/15/fdb90a5cf5a1f52845c276e76298c75fbbcc0ac2b4a86551906d54529965/aiohttp-3.13.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0ce692c3468fa831af7dceed52edf51ac348cebfc8d3feb935927b63bd3e8576", size = 1731819, upload-time = "2026-03-28T17:16:14.558Z" }, + { url = "https://files.pythonhosted.org/packages/ec/df/28146785a007f7820416be05d4f28cc207493efd1e8c6c1068e9bdc29198/aiohttp-3.13.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8e08abcfe752a454d2cb89ff0c08f2d1ecd057ae3e8cc6d84638de853530ebab", size = 1793279, upload-time = "2026-03-28T17:16:16.594Z" }, + { url = "https://files.pythonhosted.org/packages/10/47/689c743abf62ea7a77774d5722f220e2c912a77d65d368b884d9779ef41b/aiohttp-3.13.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5977f701b3fff36367a11087f30ea73c212e686d41cd363c50c022d48b011d8d", size = 1891082, upload-time = "2026-03-28T17:16:18.71Z" }, + { url = "https://files.pythonhosted.org/packages/b0/b6/f7f4f318c7e58c23b761c9b13b9a3c9b394e0f9d5d76fbc6622fa98509f6/aiohttp-3.13.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:54203e10405c06f8b6020bd1e076ae0fe6c194adcee12a5a78af3ffa3c57025e", size = 1773938, upload-time = "2026-03-28T17:16:21.125Z" }, + { url = "https://files.pythonhosted.org/packages/aa/06/f207cb3121852c989586a6fc16ff854c4fcc8651b86c5d3bd1fc83057650/aiohttp-3.13.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:358a6af0145bc4dda037f13167bef3cce54b132087acc4c295c739d05d16b1c3", size = 1579548, upload-time = "2026-03-28T17:16:23.588Z" }, + { url = "https://files.pythonhosted.org/packages/6c/58/e1289661a32161e24c1fe479711d783067210d266842523752869cc1d9c2/aiohttp-3.13.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:898ea1850656d7d61832ef06aa9846ab3ddb1621b74f46de78fbc5e1a586ba83", size = 1714669, upload-time = "2026-03-28T17:16:25.713Z" }, + { url = "https://files.pythonhosted.org/packages/96/0a/3e86d039438a74a86e6a948a9119b22540bae037d6ba317a042ae3c22711/aiohttp-3.13.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:7bc30cceb710cf6a44e9617e43eebb6e3e43ad855a34da7b4b6a73537d8a6763", size = 1754175, upload-time = "2026-03-28T17:16:28.18Z" }, + { url = "https://files.pythonhosted.org/packages/f4/30/e717fc5df83133ba467a560b6d8ef20197037b4bb5d7075b90037de1018e/aiohttp-3.13.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4a31c0c587a8a038f19a4c7e60654a6c899c9de9174593a13e7cc6e15ff271f9", size = 1762049, upload-time = "2026-03-28T17:16:30.941Z" }, + { url = "https://files.pythonhosted.org/packages/e4/28/8f7a2d4492e336e40005151bdd94baf344880a4707573378579f833a64c1/aiohttp-3.13.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:2062f675f3fe6e06d6113eb74a157fb9df58953ffed0cdb4182554b116545758", size = 1570861, upload-time = "2026-03-28T17:16:32.953Z" }, + { url = "https://files.pythonhosted.org/packages/78/45/12e1a3d0645968b1c38de4b23fdf270b8637735ea057d4f84482ff918ad9/aiohttp-3.13.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:3d1ba8afb847ff80626d5e408c1fdc99f942acc877d0702fe137015903a220a9", size = 1790003, upload-time = "2026-03-28T17:16:35.468Z" }, + { url = "https://files.pythonhosted.org/packages/eb/0f/60374e18d590de16dcb39d6ff62f39c096c1b958e6f37727b5870026ea30/aiohttp-3.13.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b08149419994cdd4d5eecf7fd4bc5986b5a9380285bcd01ab4c0d6bfca47b79d", size = 1737289, upload-time = "2026-03-28T17:16:38.187Z" }, + { url = "https://files.pythonhosted.org/packages/02/bf/535e58d886cfbc40a8b0013c974afad24ef7632d645bca0b678b70033a60/aiohttp-3.13.4-cp312-cp312-win32.whl", hash = "sha256:fc432f6a2c4f720180959bc19aa37259651c1a4ed8af8afc84dd41c60f15f791", size = 434185, upload-time = "2026-03-28T17:16:40.735Z" }, + { url = "https://files.pythonhosted.org/packages/1e/1a/d92e3325134ebfff6f4069f270d3aac770d63320bd1fcd0eca023e74d9a8/aiohttp-3.13.4-cp312-cp312-win_amd64.whl", hash = "sha256:6148c9ae97a3e8bff9a1fc9c757fa164116f86c100468339730e717590a3fb77", size = 461285, upload-time = "2026-03-28T17:16:42.713Z" }, + { url = "https://files.pythonhosted.org/packages/e3/ac/892f4162df9b115b4758d615f32ec63d00f3084c705ff5526630887b9b42/aiohttp-3.13.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:63dd5e5b1e43b8fb1e91b79b7ceba1feba588b317d1edff385084fcc7a0a4538", size = 745744, upload-time = "2026-03-28T17:16:44.67Z" }, + { url = "https://files.pythonhosted.org/packages/97/a9/c5b87e4443a2f0ea88cb3000c93a8fdad1ee63bffc9ded8d8c8e0d66efc6/aiohttp-3.13.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:746ac3cc00b5baea424dacddea3ec2c2702f9590de27d837aa67004db1eebc6e", size = 498178, upload-time = "2026-03-28T17:16:46.766Z" }, + { url = "https://files.pythonhosted.org/packages/94/42/07e1b543a61250783650df13da8ddcdc0d0a5538b2bd15cef6e042aefc61/aiohttp-3.13.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bda8f16ea99d6a6705e5946732e48487a448be874e54a4f73d514660ff7c05d3", size = 498331, upload-time = "2026-03-28T17:16:48.9Z" }, + { url = "https://files.pythonhosted.org/packages/20/d6/492f46bf0328534124772d0cf58570acae5b286ea25006900650f69dae0e/aiohttp-3.13.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4b061e7b5f840391e3f64d0ddf672973e45c4cfff7a0feea425ea24e51530fc2", size = 1744414, upload-time = "2026-03-28T17:16:50.968Z" }, + { url = "https://files.pythonhosted.org/packages/e2/4d/e02627b2683f68051246215d2d62b2d2f249ff7a285e7a858dc47d6b6a14/aiohttp-3.13.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b252e8d5cd66184b570d0d010de742736e8a4fab22c58299772b0c5a466d4b21", size = 1719226, upload-time = "2026-03-28T17:16:53.173Z" }, + { url = "https://files.pythonhosted.org/packages/7b/6c/5d0a3394dd2b9f9aeba6e1b6065d0439e4b75d41f1fb09a3ec010b43552b/aiohttp-3.13.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:20af8aad61d1803ff11152a26146d8d81c266aa8c5aa9b4504432abb965c36a0", size = 1782110, upload-time = "2026-03-28T17:16:55.362Z" }, + { url = "https://files.pythonhosted.org/packages/0d/2d/c20791e3437700a7441a7edfb59731150322424f5aadf635602d1d326101/aiohttp-3.13.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:13a5cc924b59859ad2adb1478e31f410a7ed46e92a2a619d6d1dd1a63c1a855e", size = 1884809, upload-time = "2026-03-28T17:16:57.734Z" }, + { url = "https://files.pythonhosted.org/packages/c8/94/d99dbfbd1924a87ef643833932eb2a3d9e5eee87656efea7d78058539eff/aiohttp-3.13.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:534913dfb0a644d537aebb4123e7d466d94e3be5549205e6a31f72368980a81a", size = 1764938, upload-time = "2026-03-28T17:17:00.221Z" }, + { url = "https://files.pythonhosted.org/packages/49/61/3ce326a1538781deb89f6cf5e094e2029cd308ed1e21b2ba2278b08426f6/aiohttp-3.13.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:320e40192a2dcc1cf4b5576936e9652981ab596bf81eb309535db7e2f5b5672f", size = 1570697, upload-time = "2026-03-28T17:17:02.985Z" }, + { url = "https://files.pythonhosted.org/packages/b6/77/4ab5a546857bb3028fbaf34d6eea180267bdab022ee8b1168b1fcde4bfdd/aiohttp-3.13.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9e587fcfce2bcf06526a43cb705bdee21ac089096f2e271d75de9c339db3100c", size = 1702258, upload-time = "2026-03-28T17:17:05.28Z" }, + { url = "https://files.pythonhosted.org/packages/79/63/d8f29021e39bc5af8e5d5e9da1b07976fb9846487a784e11e4f4eeda4666/aiohttp-3.13.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:9eb9c2eea7278206b5c6c1441fdd9dc420c278ead3f3b2cc87f9b693698cc500", size = 1740287, upload-time = "2026-03-28T17:17:07.712Z" }, + { url = "https://files.pythonhosted.org/packages/55/3a/cbc6b3b124859a11bc8055d3682c26999b393531ef926754a3445b99dfef/aiohttp-3.13.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:29be00c51972b04bf9d5c8f2d7f7314f48f96070ca40a873a53056e652e805f7", size = 1753011, upload-time = "2026-03-28T17:17:10.053Z" }, + { url = "https://files.pythonhosted.org/packages/e0/30/836278675205d58c1368b21520eab9572457cf19afd23759216c04483048/aiohttp-3.13.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:90c06228a6c3a7c9f776fe4fc0b7ff647fffd3bed93779a6913c804ae00c1073", size = 1566359, upload-time = "2026-03-28T17:17:12.433Z" }, + { url = "https://files.pythonhosted.org/packages/50/b4/8032cc9b82d17e4277704ba30509eaccb39329dc18d6a35f05e424439e32/aiohttp-3.13.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:a533ec132f05fd9a1d959e7f34184cd7d5e8511584848dab85faefbaac573069", size = 1785537, upload-time = "2026-03-28T17:17:14.721Z" }, + { url = "https://files.pythonhosted.org/packages/17/7d/5873e98230bde59f493bf1f7c3e327486a4b5653fa401144704df5d00211/aiohttp-3.13.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1c946f10f413836f82ea4cfb90200d2a59578c549f00857e03111cf45ad01ca5", size = 1740752, upload-time = "2026-03-28T17:17:17.387Z" }, + { url = "https://files.pythonhosted.org/packages/7b/f2/13e46e0df051494d7d3c68b7f72d071f48c384c12716fc294f75d5b1a064/aiohttp-3.13.4-cp313-cp313-win32.whl", hash = "sha256:48708e2706106da6967eff5908c78ca3943f005ed6bcb75da2a7e4da94ef8c70", size = 433187, upload-time = "2026-03-28T17:17:19.523Z" }, + { url = "https://files.pythonhosted.org/packages/ea/c0/649856ee655a843c8f8664592cfccb73ac80ede6a8c8db33a25d810c12db/aiohttp-3.13.4-cp313-cp313-win_amd64.whl", hash = "sha256:74a2eb058da44fa3a877a49e2095b591d4913308bb424c418b77beb160c55ce3", size = 459778, upload-time = "2026-03-28T17:17:21.964Z" }, +] + +[[package]] +name = "aiosignal" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "frozenlist" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/61/62/06741b579156360248d1ec624842ad0edf697050bbaf7c3e46394e106ad1/aiosignal-1.4.0.tar.gz", hash = "sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7", size = 25007, upload-time = "2025-07-03T22:54:43.528Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl", hash = "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e", size = 7490, upload-time = "2025-07-03T22:54:42.156Z" }, +] + +[[package]] +name = "aiosqlite" +version = "0.22.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4e/8a/64761f4005f17809769d23e518d915db74e6310474e733e3593cfc854ef1/aiosqlite-0.22.1.tar.gz", hash = "sha256:043e0bd78d32888c0a9ca90fc788b38796843360c855a7262a532813133a0650", size = 14821, upload-time = "2025-12-23T19:25:43.997Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/00/b7/e3bf5133d697a08128598c8d0abc5e16377b51465a33756de24fa7dee953/aiosqlite-0.22.1-py3-none-any.whl", hash = "sha256:21c002eb13823fad740196c5a2e9d8e62f6243bd9e7e4a1f87fb5e44ecb4fceb", size = 17405, upload-time = "2025-12-23T19:25:42.139Z" }, +] + +[[package]] +name = "alembic" +version = "1.18.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mako" }, + { name = "sqlalchemy" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/94/13/8b084e0f2efb0275a1d534838844926f798bd766566b1375174e2448cd31/alembic-1.18.4.tar.gz", hash = "sha256:cb6e1fd84b6174ab8dbb2329f86d631ba9559dd78df550b57804d607672cedbc", size = 2056725, upload-time = "2026-02-10T16:00:47.195Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/29/6533c317b74f707ea28f8d633734dbda2119bbadfc61b2f3640ba835d0f7/alembic-1.18.4-py3-none-any.whl", hash = "sha256:a5ed4adcf6d8a4cb575f3d759f071b03cd6e5c7618eb796cb52497be25bfe19a", size = 263893, upload-time = "2026-02-10T16:00:49.997Z" }, +] + +[[package]] +name = "annotated-doc" +version = "0.0.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/57/ba/046ceea27344560984e26a590f90bc7f4a75b06701f653222458922b558c/annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4", size = 7288, upload-time = "2025-11-10T22:07:42.062Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320", size = 5303, upload-time = "2025-11-10T22:07:40.673Z" }, +] + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, +] + +[[package]] +name = "anyio" +version = "4.13.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/19/14/2c5dd9f512b66549ae92767a9c7b330ae88e1932ca57876909410251fe13/anyio-4.13.0.tar.gz", hash = "sha256:334b70e641fd2221c1505b3890c69882fe4a2df910cba14d97019b90b24439dc", size = 231622, upload-time = "2026-03-24T12:59:09.671Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/da/42/e921fccf5015463e32a3cf6ee7f980a6ed0f395ceeaa45060b61d86486c2/anyio-4.13.0-py3-none-any.whl", hash = "sha256:08b310f9e24a9594186fd75b4f73f4a4152069e3853f1ed8bfbf58369f4ad708", size = 114353, upload-time = "2026-03-24T12:59:08.246Z" }, +] + +[[package]] +name = "attrs" +version = "26.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9a/8e/82a0fe20a541c03148528be8cac2408564a6c9a0cc7e9171802bc1d26985/attrs-26.1.0.tar.gz", hash = "sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32", size = 952055, upload-time = "2026-03-19T14:22:25.026Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309", size = 67548, upload-time = "2026-03-19T14:22:23.645Z" }, +] + +[[package]] +name = "authlib" +version = "1.7.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, + { name = "joserfc" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/36/98/7d93f30d029643c0275dbc0bd6d5a6f670661ee6c9a94d93af7ab4887600/authlib-1.7.2.tar.gz", hash = "sha256:2cea25fefcd4e7173bdf1372c0afc265c8034b23a8cd5dcb6a9164b826c64231", size = 176511, upload-time = "2026-05-06T08:10:23.116Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/95/adcb68e20c34162e9135f370d6e31737719c2b6f94bc953fe7ed1f10fe21/authlib-1.7.2-py2.py3-none-any.whl", hash = "sha256:3e1faedc9d87e7d56a164eca3ccb6ace0d61b94abe83e92242f8dc8bba9b4a9f", size = 259548, upload-time = "2026-05-06T08:10:21.436Z" }, +] + +[[package]] +name = "certifi" +version = "2026.5.20" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/ce/ee2ecad540810a79593028e88299baeae54d346cc7a0d94b6199988b89b1/certifi-2026.5.20.tar.gz", hash = "sha256:69dea482ab64caa7b9f6aba1c6bf48bb6a5448d1c0f1b17ab42ad8c763a5344d", size = 135422, upload-time = "2026-05-20T11:46:50.073Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/59/8c/57e832b7af6d7c5abe66eb3fbe3a3a32f4d11ea23a1aa7131371035be991/certifi-2026.5.20-py3-none-any.whl", hash = "sha256:3c52e209ba0a4ad7aebe60436a4ab349c39e1e602e8c134221e546902ad25897", size = 134134, upload-time = "2026-05-20T11:46:48.578Z" }, +] + +[[package]] +name = "cffi" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser", marker = "implementation_name != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/4a/3dfd5f7850cbf0d06dc84ba9aa00db766b52ca38d8b86e3a38314d52498c/cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe", size = 184344, upload-time = "2025-09-08T23:22:26.456Z" }, + { url = "https://files.pythonhosted.org/packages/4f/8b/f0e4c441227ba756aafbe78f117485b25bb26b1c059d01f137fa6d14896b/cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c", size = 180560, upload-time = "2025-09-08T23:22:28.197Z" }, + { url = "https://files.pythonhosted.org/packages/b1/b7/1200d354378ef52ec227395d95c2576330fd22a869f7a70e88e1447eb234/cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92", size = 209613, upload-time = "2025-09-08T23:22:29.475Z" }, + { url = "https://files.pythonhosted.org/packages/b8/56/6033f5e86e8cc9bb629f0077ba71679508bdf54a9a5e112a3c0b91870332/cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93", size = 216476, upload-time = "2025-09-08T23:22:31.063Z" }, + { url = "https://files.pythonhosted.org/packages/dc/7f/55fecd70f7ece178db2f26128ec41430d8720f2d12ca97bf8f0a628207d5/cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5", size = 203374, upload-time = "2025-09-08T23:22:32.507Z" }, + { url = "https://files.pythonhosted.org/packages/84/ef/a7b77c8bdc0f77adc3b46888f1ad54be8f3b7821697a7b89126e829e676a/cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664", size = 202597, upload-time = "2025-09-08T23:22:34.132Z" }, + { url = "https://files.pythonhosted.org/packages/d7/91/500d892b2bf36529a75b77958edfcd5ad8e2ce4064ce2ecfeab2125d72d1/cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26", size = 215574, upload-time = "2025-09-08T23:22:35.443Z" }, + { url = "https://files.pythonhosted.org/packages/44/64/58f6255b62b101093d5df22dcb752596066c7e89dd725e0afaed242a61be/cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9", size = 218971, upload-time = "2025-09-08T23:22:36.805Z" }, + { url = "https://files.pythonhosted.org/packages/ab/49/fa72cebe2fd8a55fbe14956f9970fe8eb1ac59e5df042f603ef7c8ba0adc/cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414", size = 211972, upload-time = "2025-09-08T23:22:38.436Z" }, + { url = "https://files.pythonhosted.org/packages/0b/28/dd0967a76aab36731b6ebfe64dec4e981aff7e0608f60c2d46b46982607d/cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743", size = 217078, upload-time = "2025-09-08T23:22:39.776Z" }, + { url = "https://files.pythonhosted.org/packages/2b/c0/015b25184413d7ab0a410775fdb4a50fca20f5589b5dab1dbbfa3baad8ce/cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5", size = 172076, upload-time = "2025-09-08T23:22:40.95Z" }, + { url = "https://files.pythonhosted.org/packages/ae/8f/dc5531155e7070361eb1b7e4c1a9d896d0cb21c49f807a6c03fd63fc877e/cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5", size = 182820, upload-time = "2025-09-08T23:22:42.463Z" }, + { url = "https://files.pythonhosted.org/packages/95/5c/1b493356429f9aecfd56bc171285a4c4ac8697f76e9bbbbb105e537853a1/cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d", size = 177635, upload-time = "2025-09-08T23:22:43.623Z" }, + { url = "https://files.pythonhosted.org/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", size = 185271, upload-time = "2025-09-08T23:22:44.795Z" }, + { url = "https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", size = 181048, upload-time = "2025-09-08T23:22:45.938Z" }, + { url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529, upload-time = "2025-09-08T23:22:47.349Z" }, + { url = "https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", size = 220097, upload-time = "2025-09-08T23:22:48.677Z" }, + { url = "https://files.pythonhosted.org/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", size = 207983, upload-time = "2025-09-08T23:22:50.06Z" }, + { url = "https://files.pythonhosted.org/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", size = 206519, upload-time = "2025-09-08T23:22:51.364Z" }, + { url = "https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", size = 219572, upload-time = "2025-09-08T23:22:52.902Z" }, + { url = "https://files.pythonhosted.org/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", size = 222963, upload-time = "2025-09-08T23:22:54.518Z" }, + { url = "https://files.pythonhosted.org/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", size = 221361, upload-time = "2025-09-08T23:22:55.867Z" }, + { url = "https://files.pythonhosted.org/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", size = 172932, upload-time = "2025-09-08T23:22:57.188Z" }, + { url = "https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", size = 183557, upload-time = "2025-09-08T23:22:58.351Z" }, + { url = "https://files.pythonhosted.org/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", size = 177762, upload-time = "2025-09-08T23:22:59.668Z" }, + { url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230, upload-time = "2025-09-08T23:23:00.879Z" }, + { url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043, upload-time = "2025-09-08T23:23:02.231Z" }, + { url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" }, + { url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101, upload-time = "2025-09-08T23:23:04.792Z" }, + { url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948, upload-time = "2025-09-08T23:23:06.127Z" }, + { url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422, upload-time = "2025-09-08T23:23:07.753Z" }, + { url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload-time = "2025-09-08T23:23:09.648Z" }, + { url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload-time = "2025-09-08T23:23:10.928Z" }, + { url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload-time = "2025-09-08T23:23:12.42Z" }, + { url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" }, + { url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" }, + { url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/a1/67fe25fac3c7642725500a3f6cfe5821ad557c3abb11c9d20d12c7008d3e/charset_normalizer-3.4.7.tar.gz", hash = "sha256:ae89db9e5f98a11a4bf50407d4363e7b09b31e55bc117b4f7d80aab97ba009e5", size = 144271, upload-time = "2026-04-02T09:28:39.342Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/d7/b5b7020a0565c2e9fa8c09f4b5fa6232feb326b8c20081ccded47ea368fd/charset_normalizer-3.4.7-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7641bb8895e77f921102f72833904dcd9901df5d6d72a2ab8f31d04b7e51e4e7", size = 309705, upload-time = "2026-04-02T09:26:02.191Z" }, + { url = "https://files.pythonhosted.org/packages/5a/53/58c29116c340e5456724ecd2fff4196d236b98f3da97b404bc5e51ac3493/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:202389074300232baeb53ae2569a60901f7efadd4245cf3a3bf0617d60b439d7", size = 206419, upload-time = "2026-04-02T09:26:03.583Z" }, + { url = "https://files.pythonhosted.org/packages/b2/02/e8146dc6591a37a00e5144c63f29fb7c97a734ea8a111190783c0e60ab63/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:30b8d1d8c52a48c2c5690e152c169b673487a2a58de1ec7393196753063fcd5e", size = 227901, upload-time = "2026-04-02T09:26:04.738Z" }, + { url = "https://files.pythonhosted.org/packages/fb/73/77486c4cd58f1267bf17db420e930c9afa1b3be3fe8c8b8ebbebc9624359/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:532bc9bf33a68613fd7d65e4b1c71a6a38d7d42604ecf239c77392e9b4e8998c", size = 222742, upload-time = "2026-04-02T09:26:06.36Z" }, + { url = "https://files.pythonhosted.org/packages/a1/fa/f74eb381a7d94ded44739e9d94de18dc5edc9c17fb8c11f0a6890696c0a9/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2fe249cb4651fd12605b7288b24751d8bfd46d35f12a20b1ba33dea122e690df", size = 214061, upload-time = "2026-04-02T09:26:08.347Z" }, + { url = "https://files.pythonhosted.org/packages/dc/92/42bd3cefcf7687253fb86694b45f37b733c97f59af3724f356fa92b8c344/charset_normalizer-3.4.7-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:65bcd23054beab4d166035cabbc868a09c1a49d1efe458fe8e4361215df40265", size = 199239, upload-time = "2026-04-02T09:26:09.823Z" }, + { url = "https://files.pythonhosted.org/packages/4c/3d/069e7184e2aa3b3cddc700e3dd267413dc259854adc3380421c805c6a17d/charset_normalizer-3.4.7-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:08e721811161356f97b4059a9ba7bafb23ea5ee2255402c42881c214e173c6b4", size = 210173, upload-time = "2026-04-02T09:26:10.953Z" }, + { url = "https://files.pythonhosted.org/packages/62/51/9d56feb5f2e7074c46f93e0ebdbe61f0848ee246e2f0d89f8e20b89ebb8f/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e060d01aec0a910bdccb8be71faf34e7799ce36950f8294c8bf612cba65a2c9e", size = 209841, upload-time = "2026-04-02T09:26:12.142Z" }, + { url = "https://files.pythonhosted.org/packages/d2/59/893d8f99cc4c837dda1fe2f1139079703deb9f321aabcb032355de13b6c7/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:38c0109396c4cfc574d502df99742a45c72c08eff0a36158b6f04000043dbf38", size = 200304, upload-time = "2026-04-02T09:26:13.711Z" }, + { url = "https://files.pythonhosted.org/packages/7d/1d/ee6f3be3464247578d1ed5c46de545ccc3d3ff933695395c402c21fa6b77/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:1c2a768fdd44ee4a9339a9b0b130049139b8ce3c01d2ce09f67f5a68048d477c", size = 229455, upload-time = "2026-04-02T09:26:14.941Z" }, + { url = "https://files.pythonhosted.org/packages/54/bb/8fb0a946296ea96a488928bdce8ef99023998c48e4713af533e9bb98ef07/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:1a87ca9d5df6fe460483d9a5bbf2b18f620cbed41b432e2bddb686228282d10b", size = 210036, upload-time = "2026-04-02T09:26:16.478Z" }, + { url = "https://files.pythonhosted.org/packages/9a/bc/015b2387f913749f82afd4fcba07846d05b6d784dd16123cb66860e0237d/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:d635aab80466bc95771bb78d5370e74d36d1fe31467b6b29b8b57b2a3cd7d22c", size = 224739, upload-time = "2026-04-02T09:26:17.751Z" }, + { url = "https://files.pythonhosted.org/packages/17/ab/63133691f56baae417493cba6b7c641571a2130eb7bceba6773367ab9ec5/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ae196f021b5e7c78e918242d217db021ed2a6ace2bc6ae94c0fc596221c7f58d", size = 216277, upload-time = "2026-04-02T09:26:18.981Z" }, + { url = "https://files.pythonhosted.org/packages/06/6d/3be70e827977f20db77c12a97e6a9f973631a45b8d186c084527e53e77a4/charset_normalizer-3.4.7-cp311-cp311-win32.whl", hash = "sha256:adb2597b428735679446b46c8badf467b4ca5f5056aae4d51a19f9570301b1ad", size = 147819, upload-time = "2026-04-02T09:26:20.295Z" }, + { url = "https://files.pythonhosted.org/packages/20/d9/5f67790f06b735d7c7637171bbfd89882ad67201891b7275e51116ed8207/charset_normalizer-3.4.7-cp311-cp311-win_amd64.whl", hash = "sha256:8e385e4267ab76874ae30db04c627faaaf0b509e1ccc11a95b3fc3e83f855c00", size = 159281, upload-time = "2026-04-02T09:26:21.74Z" }, + { url = "https://files.pythonhosted.org/packages/ca/83/6413f36c5a34afead88ce6f66684d943d91f233d76dd083798f9602b75ae/charset_normalizer-3.4.7-cp311-cp311-win_arm64.whl", hash = "sha256:d4a48e5b3c2a489fae013b7589308a40146ee081f6f509e047e0e096084ceca1", size = 147843, upload-time = "2026-04-02T09:26:22.901Z" }, + { url = "https://files.pythonhosted.org/packages/0c/eb/4fc8d0a7110eb5fc9cc161723a34a8a6c200ce3b4fbf681bc86feee22308/charset_normalizer-3.4.7-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:eca9705049ad3c7345d574e3510665cb2cf844c2f2dcfe675332677f081cbd46", size = 311328, upload-time = "2026-04-02T09:26:24.331Z" }, + { url = "https://files.pythonhosted.org/packages/f8/e3/0fadc706008ac9d7b9b5be6dc767c05f9d3e5df51744ce4cc9605de7b9f4/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6178f72c5508bfc5fd446a5905e698c6212932f25bcdd4b47a757a50605a90e2", size = 208061, upload-time = "2026-04-02T09:26:25.568Z" }, + { url = "https://files.pythonhosted.org/packages/42/f0/3dd1045c47f4a4604df85ec18ad093912ae1344ac706993aff91d38773a2/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e1421b502d83040e6d7fb2fb18dff63957f720da3d77b2fbd3187ceb63755d7b", size = 229031, upload-time = "2026-04-02T09:26:26.865Z" }, + { url = "https://files.pythonhosted.org/packages/dc/67/675a46eb016118a2fbde5a277a5d15f4f69d5f3f5f338e5ee2f8948fcf43/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:edac0f1ab77644605be2cbba52e6b7f630731fc42b34cb0f634be1a6eface56a", size = 225239, upload-time = "2026-04-02T09:26:28.044Z" }, + { url = "https://files.pythonhosted.org/packages/4b/f8/d0118a2f5f23b02cd166fa385c60f9b0d4f9194f574e2b31cef350ad7223/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5649fd1c7bade02f320a462fdefd0b4bd3ce036065836d4f42e0de958038e116", size = 216589, upload-time = "2026-04-02T09:26:29.239Z" }, + { url = "https://files.pythonhosted.org/packages/b1/f1/6d2b0b261b6c4ceef0fcb0d17a01cc5bc53586c2d4796fa04b5c540bc13d/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:203104ed3e428044fd943bc4bf45fa73c0730391f9621e37fe39ecf477b128cb", size = 202733, upload-time = "2026-04-02T09:26:30.5Z" }, + { url = "https://files.pythonhosted.org/packages/6f/c0/7b1f943f7e87cc3db9626ba17807d042c38645f0a1d4415c7a14afb5591f/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:298930cec56029e05497a76988377cbd7457ba864beeea92ad7e844fe74cd1f1", size = 212652, upload-time = "2026-04-02T09:26:31.709Z" }, + { url = "https://files.pythonhosted.org/packages/38/dd/5a9ab159fe45c6e72079398f277b7d2b523e7f716acc489726115a910097/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:708838739abf24b2ceb208d0e22403dd018faeef86ddac04319a62ae884c4f15", size = 211229, upload-time = "2026-04-02T09:26:33.282Z" }, + { url = "https://files.pythonhosted.org/packages/d5/ff/531a1cad5ca855d1c1a8b69cb71abfd6d85c0291580146fda7c82857caa1/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:0f7eb884681e3938906ed0434f20c63046eacd0111c4ba96f27b76084cd679f5", size = 203552, upload-time = "2026-04-02T09:26:34.845Z" }, + { url = "https://files.pythonhosted.org/packages/c1/4c/a5fb52d528a8ca41f7598cb619409ece30a169fbdf9cdce592e53b46c3a6/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4dc1e73c36828f982bfe79fadf5919923f8a6f4df2860804db9a98c48824ce8d", size = 230806, upload-time = "2026-04-02T09:26:36.152Z" }, + { url = "https://files.pythonhosted.org/packages/59/7a/071feed8124111a32b316b33ae4de83d36923039ef8cf48120266844285b/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:aed52fea0513bac0ccde438c188c8a471c4e0f457c2dd20cdbf6ea7a450046c7", size = 212316, upload-time = "2026-04-02T09:26:37.672Z" }, + { url = "https://files.pythonhosted.org/packages/fd/35/f7dba3994312d7ba508e041eaac39a36b120f32d4c8662b8814dab876431/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:fea24543955a6a729c45a73fe90e08c743f0b3334bbf3201e6c4bc1b0c7fa464", size = 227274, upload-time = "2026-04-02T09:26:38.93Z" }, + { url = "https://files.pythonhosted.org/packages/8a/2d/a572df5c9204ab7688ec1edc895a73ebded3b023bb07364710b05dd1c9be/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:bb6d88045545b26da47aa879dd4a89a71d1dce0f0e549b1abcb31dfe4a8eac49", size = 218468, upload-time = "2026-04-02T09:26:40.17Z" }, + { url = "https://files.pythonhosted.org/packages/86/eb/890922a8b03a568ca2f336c36585a4713c55d4d67bf0f0c78924be6315ca/charset_normalizer-3.4.7-cp312-cp312-win32.whl", hash = "sha256:2257141f39fe65a3fdf38aeccae4b953e5f3b3324f4ff0daf9f15b8518666a2c", size = 148460, upload-time = "2026-04-02T09:26:41.416Z" }, + { url = "https://files.pythonhosted.org/packages/35/d9/0e7dffa06c5ab081f75b1b786f0aefc88365825dfcd0ac544bdb7b2b6853/charset_normalizer-3.4.7-cp312-cp312-win_amd64.whl", hash = "sha256:5ed6ab538499c8644b8a3e18debabcd7ce684f3fa91cf867521a7a0279cab2d6", size = 159330, upload-time = "2026-04-02T09:26:42.554Z" }, + { url = "https://files.pythonhosted.org/packages/9e/5d/481bcc2a7c88ea6b0878c299547843b2521ccbc40980cb406267088bc701/charset_normalizer-3.4.7-cp312-cp312-win_arm64.whl", hash = "sha256:56be790f86bfb2c98fb742ce566dfb4816e5a83384616ab59c49e0604d49c51d", size = 147828, upload-time = "2026-04-02T09:26:44.075Z" }, + { url = "https://files.pythonhosted.org/packages/c1/3b/66777e39d3ae1ddc77ee606be4ec6d8cbd4c801f65e5a1b6f2b11b8346dd/charset_normalizer-3.4.7-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f496c9c3cc02230093d8330875c4c3cdfc3b73612a5fd921c65d39cbcef08063", size = 309627, upload-time = "2026-04-02T09:26:45.198Z" }, + { url = "https://files.pythonhosted.org/packages/2e/4e/b7f84e617b4854ade48a1b7915c8ccfadeba444d2a18c291f696e37f0d3b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ea948db76d31190bf08bd371623927ee1339d5f2a0b4b1b4a4439a65298703c", size = 207008, upload-time = "2026-04-02T09:26:46.824Z" }, + { url = "https://files.pythonhosted.org/packages/c4/bb/ec73c0257c9e11b268f018f068f5d00aa0ef8c8b09f7753ebd5f2880e248/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a277ab8928b9f299723bc1a2dabb1265911b1a76341f90a510368ca44ad9ab66", size = 228303, upload-time = "2026-04-02T09:26:48.397Z" }, + { url = "https://files.pythonhosted.org/packages/85/fb/32d1f5033484494619f701e719429c69b766bfc4dbc61aa9e9c8c166528b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3bec022aec2c514d9cf199522a802bd007cd588ab17ab2525f20f9c34d067c18", size = 224282, upload-time = "2026-04-02T09:26:49.684Z" }, + { url = "https://files.pythonhosted.org/packages/fa/07/330e3a0dda4c404d6da83b327270906e9654a24f6c546dc886a0eb0ffb23/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e044c39e41b92c845bc815e5ae4230804e8e7bc29e399b0437d64222d92809dd", size = 215595, upload-time = "2026-04-02T09:26:50.915Z" }, + { url = "https://files.pythonhosted.org/packages/e3/7c/fc890655786e423f02556e0216d4b8c6bcb6bdfa890160dc66bf52dee468/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:f495a1652cf3fbab2eb0639776dad966c2fb874d79d87ca07f9d5f059b8bd215", size = 201986, upload-time = "2026-04-02T09:26:52.197Z" }, + { url = "https://files.pythonhosted.org/packages/d8/97/bfb18b3db2aed3b90cf54dc292ad79fdd5ad65c4eae454099475cbeadd0d/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e712b419df8ba5e42b226c510472b37bd57b38e897d3eca5e8cfd410a29fa859", size = 211711, upload-time = "2026-04-02T09:26:53.49Z" }, + { url = "https://files.pythonhosted.org/packages/6f/a5/a581c13798546a7fd557c82614a5c65a13df2157e9ad6373166d2a3e645d/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7804338df6fcc08105c7745f1502ba68d900f45fd770d5bdd5288ddccb8a42d8", size = 210036, upload-time = "2026-04-02T09:26:54.975Z" }, + { url = "https://files.pythonhosted.org/packages/8c/bf/b3ab5bcb478e4193d517644b0fb2bf5497fbceeaa7a1bc0f4d5b50953861/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:481551899c856c704d58119b5025793fa6730adda3571971af568f66d2424bb5", size = 202998, upload-time = "2026-04-02T09:26:56.303Z" }, + { url = "https://files.pythonhosted.org/packages/e7/4e/23efd79b65d314fa320ec6017b4b5834d5c12a58ba4610aa353af2e2f577/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f59099f9b66f0d7145115e6f80dd8b1d847176df89b234a5a6b3f00437aa0832", size = 230056, upload-time = "2026-04-02T09:26:57.554Z" }, + { url = "https://files.pythonhosted.org/packages/b9/9f/1e1941bc3f0e01df116e68dc37a55c4d249df5e6fa77f008841aef68264f/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:f59ad4c0e8f6bba240a9bb85504faa1ab438237199d4cce5f622761507b8f6a6", size = 211537, upload-time = "2026-04-02T09:26:58.843Z" }, + { url = "https://files.pythonhosted.org/packages/80/0f/088cbb3020d44428964a6c97fe1edfb1b9550396bf6d278330281e8b709c/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:3dedcc22d73ec993f42055eff4fcfed9318d1eeb9a6606c55892a26964964e48", size = 226176, upload-time = "2026-04-02T09:27:00.437Z" }, + { url = "https://files.pythonhosted.org/packages/6a/9f/130394f9bbe06f4f63e22641d32fc9b202b7e251c9aef4db044324dac493/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:64f02c6841d7d83f832cd97ccf8eb8a906d06eb95d5276069175c696b024b60a", size = 217723, upload-time = "2026-04-02T09:27:02.021Z" }, + { url = "https://files.pythonhosted.org/packages/73/55/c469897448a06e49f8fa03f6caae97074fde823f432a98f979cc42b90e69/charset_normalizer-3.4.7-cp313-cp313-win32.whl", hash = "sha256:4042d5c8f957e15221d423ba781e85d553722fc4113f523f2feb7b188cc34c5e", size = 148085, upload-time = "2026-04-02T09:27:03.192Z" }, + { url = "https://files.pythonhosted.org/packages/5d/78/1b74c5bbb3f99b77a1715c91b3e0b5bdb6fe302d95ace4f5b1bec37b0167/charset_normalizer-3.4.7-cp313-cp313-win_amd64.whl", hash = "sha256:3946fa46a0cf3e4c8cb1cc52f56bb536310d34f25f01ca9b6c16afa767dab110", size = 158819, upload-time = "2026-04-02T09:27:04.454Z" }, + { url = "https://files.pythonhosted.org/packages/68/86/46bd42279d323deb8687c4a5a811fd548cb7d1de10cf6535d099877a9a9f/charset_normalizer-3.4.7-cp313-cp313-win_arm64.whl", hash = "sha256:80d04837f55fc81da168b98de4f4b797ef007fc8a79ab71c6ec9bc4dd662b15b", size = 147915, upload-time = "2026-04-02T09:27:05.971Z" }, + { url = "https://files.pythonhosted.org/packages/db/8f/61959034484a4a7c527811f4721e75d02d653a35afb0b6054474d8185d4c/charset_normalizer-3.4.7-py3-none-any.whl", hash = "sha256:3dce51d0f5e7951f8bb4900c257dad282f49190fdbebecd4ba99bcc41fef404d", size = 61958, upload-time = "2026-04-02T09:28:37.794Z" }, +] + +[[package]] +name = "click" +version = "8.1.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593, upload-time = "2024-12-21T18:38:44.339Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188, upload-time = "2024-12-21T18:38:41.666Z" }, +] + +[[package]] +name = "cloudpickle" +version = "3.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/27/fb/576f067976d320f5f0114a8d9fa1215425441bb35627b1993e5afd8111e5/cloudpickle-3.1.2.tar.gz", hash = "sha256:7fda9eb655c9c230dab534f1983763de5835249750e85fbcef43aaa30a9a2414", size = 22330, upload-time = "2025-11-03T09:25:26.604Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/39/799be3f2f0f38cc727ee3b4f1445fe6d5e4133064ec2e4115069418a5bb6/cloudpickle-3.1.2-py3-none-any.whl", hash = "sha256:9acb47f6afd73f60dc1df93bb801b472f05ff42fa6c84167d25cb206be1fbf4a", size = 22228, upload-time = "2025-11-03T09:25:25.534Z" }, +] + +[[package]] +name = "codespell" +version = "2.4.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2d/9d/1d0903dff693160f893ca6abcabad545088e7a2ee0a6deae7c24e958be69/codespell-2.4.2.tar.gz", hash = "sha256:3c33be9ae34543807f088aeb4832dfad8cb2dae38da61cac0a7045dd376cfdf3", size = 352058, upload-time = "2026-03-05T18:10:42.936Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/42/a1/52fa05533e95fe45bcc09bcf8a503874b1c08f221a4e35608017e0938f55/codespell-2.4.2-py3-none-any.whl", hash = "sha256:97e0c1060cf46bd1d5db89a936c98db8c2b804e1fdd4b5c645e82a1ec6b1f886", size = 353715, upload-time = "2026-03-05T18:10:41.398Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "contract-compliance-pipeline" +version = "0.1.0" +source = { editable = "." } +dependencies = [ + { name = "a2a-sdk" }, + { name = "google-adk" }, + { name = "google-cloud-aiplatform", extra = ["agent-engines", "evaluation"] }, + { name = "google-cloud-logging" }, + { name = "greenlet" }, + { name = "opentelemetry-instrumentation-google-genai" }, + { name = "protobuf" }, +] + +[package.optional-dependencies] +eval = [ + { name = "google-adk", extra = ["eval"] }, +] +lint = [ + { name = "codespell" }, + { name = "ruff" }, + { name = "ty" }, +] + +[package.dev-dependencies] +dev = [ + { name = "nest-asyncio" }, + { name = "pytest" }, + { name = "pytest-asyncio" }, +] + +[package.metadata] +requires-dist = [ + { name = "a2a-sdk", specifier = ">=0.3.4,<0.4" }, + { name = "codespell", marker = "extra == 'lint'", specifier = ">=2.2.0,<3.0.0" }, + { name = "google-adk", specifier = ">=1.15.0,<2.0.0" }, + { name = "google-adk", extras = ["eval"], marker = "extra == 'eval'", specifier = ">=1.15.0,<2.0.0" }, + { name = "google-cloud-aiplatform", extras = ["agent-engines", "evaluation"], specifier = ">=1.130.0" }, + { name = "google-cloud-logging", specifier = ">=3.12.0,<4.0.0" }, + { name = "greenlet", specifier = ">=3.0.0,<4.0.0" }, + { name = "opentelemetry-instrumentation-google-genai", specifier = ">=0.1.0,<1.0.0" }, + { name = "protobuf", specifier = ">=6.31.1,<7.0.0" }, + { name = "ruff", marker = "extra == 'lint'", specifier = ">=0.4.6,<1.0.0" }, + { name = "ty", marker = "extra == 'lint'", specifier = ">=0.0.1a0" }, +] +provides-extras = ["eval", "lint"] + +[package.metadata.requires-dev] +dev = [ + { name = "nest-asyncio", specifier = ">=1.6.0,<2.0.0" }, + { name = "pytest", specifier = ">=8.3.4,<9.0.0" }, + { name = "pytest-asyncio", specifier = ">=0.23.8,<1.0.0" }, +] + +[[package]] +name = "cryptography" +version = "48.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9f/a9/db8f313fdcd85d767d4973515e1db101f9c71f95fced83233de224673757/cryptography-48.0.0.tar.gz", hash = "sha256:5c3932f4436d1cccb036cb0eaef46e6e2db91035166f1ad6505c3c9d5a635920", size = 832984, upload-time = "2026-05-04T22:59:38.133Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/3d/01f6dd9190170a5a241e0e98c2d04be3664a9e6f5b9b872cde63aff1c3dd/cryptography-48.0.0-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:0c558d2cdffd8f4bbb30fc7134c74d2ca9a476f830bb053074498fbc86f41ed6", size = 8001587, upload-time = "2026-05-04T22:57:36.803Z" }, + { url = "https://files.pythonhosted.org/packages/b2/6e/e90527eef33f309beb811cf7c982c3aeffcce8e3edb178baa4ca3ae4a6fa/cryptography-48.0.0-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f5333311663ea94f75dd408665686aaf426563556bb5283554a3539177e03b8c", size = 4690433, upload-time = "2026-05-04T22:57:40.373Z" }, + { url = "https://files.pythonhosted.org/packages/90/04/673510ed51ddff56575f306cf1617d80411ee76831ccd3097599140efdfe/cryptography-48.0.0-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7995ef305d7165c3f11ae07f2517e5a4f1d5c18da1376a0a9ed496336b69e5f3", size = 4710620, upload-time = "2026-05-04T22:57:42.935Z" }, + { url = "https://files.pythonhosted.org/packages/14/d5/e9c4ef932c8d800490c34d8bd589d64a31d5890e27ec9e9ad532be893294/cryptography-48.0.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:40ba1f85eaa6959837b1d51c9767e230e14612eea4ef110ee8854ada22da1bf5", size = 4696283, upload-time = "2026-05-04T22:57:45.294Z" }, + { url = "https://files.pythonhosted.org/packages/0c/29/174b9dfb60b12d59ecfc6cfa04bc88c21b42a54f01b8aae09bb6e51e4c7f/cryptography-48.0.0-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:369a6348999f94bbd53435c894377b20ab95f25a9065c283570e70150d8abc3c", size = 5296573, upload-time = "2026-05-04T22:57:47.933Z" }, + { url = "https://files.pythonhosted.org/packages/95/38/0d29a6fd7d0d1373f0c0c88a04ba20e359b257753ac497564cd660fc1d55/cryptography-48.0.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:a0e692c683f4df67815a2d258b324e66f4738bd7a96a218c826dce4f4bd05d8f", size = 4743677, upload-time = "2026-05-04T22:57:50.067Z" }, + { url = "https://files.pythonhosted.org/packages/30/be/eef653013d5c63b6a490529e0316f9ac14a37602965d4903efed1399f32b/cryptography-48.0.0-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:18349bbc56f4743c8b12dc32e2bccb2cf83ee8b69a3bba74ef8ae857e26b3d25", size = 4330808, upload-time = "2026-05-04T22:57:52.301Z" }, + { url = "https://files.pythonhosted.org/packages/84/9e/500463e87abb7a0a0f9f256ec21123ecde0a7b5541a15e840ea54551fd81/cryptography-48.0.0-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:7e8eac43dfca5c4cccc6dad9a80504436fca53bb9bc3100a2386d730fbe6b602", size = 4695941, upload-time = "2026-05-04T22:57:54.603Z" }, + { url = "https://files.pythonhosted.org/packages/e3/dc/7303087450c2ec9e7fbb750e17c2abfbc658f23cbd0e54009509b7cc4091/cryptography-48.0.0-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:9ccdac7d40688ecb5a3b4a604b8a88c8002e3442d6c60aead1db2a89a041560c", size = 5252579, upload-time = "2026-05-04T22:57:57.207Z" }, + { url = "https://files.pythonhosted.org/packages/d0/c0/7101d3b7215edcdc90c45da544961fd8ed2d6448f77577460fa75a8443f7/cryptography-48.0.0-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:bd72e68b06bb1e96913f97dd4901119bc17f39d4586a5adf2d3e47bc2b9d58b5", size = 4743326, upload-time = "2026-05-04T22:57:59.535Z" }, + { url = "https://files.pythonhosted.org/packages/ac/d8/5b833bad13016f562ab9d063d68199a4bd121d18458e439515601d3357ec/cryptography-48.0.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:59baa2cb386c4f0b9905bd6eb4c2a79a69a128408fd31d32ca4d7102d4156321", size = 4826672, upload-time = "2026-05-04T22:58:01.996Z" }, + { url = "https://files.pythonhosted.org/packages/98/e1/7074eb8bf3c135558c73fc2bcf0f5633f912e6fb87e868a55c454080ef09/cryptography-48.0.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:9249e3cd978541d665967ac2cb2787fd6a62bddf1e75b3e347a594d7dacf4f74", size = 4972574, upload-time = "2026-05-04T22:58:03.968Z" }, + { url = "https://files.pythonhosted.org/packages/04/70/e5a1b41d325f797f39427aa44ef8baf0be500065ab6d8e10369d850d4a4f/cryptography-48.0.0-cp311-abi3-win32.whl", hash = "sha256:9c459db21422be75e2809370b829a87eb37f74cd785fc4aa9ea1e5f43b47cda4", size = 3294868, upload-time = "2026-05-04T22:58:06.467Z" }, + { url = "https://files.pythonhosted.org/packages/f4/ac/8ac51b4a5fc5932eb7ee5c517ba7dc8cd834f0048962b6b352f00f41ebf9/cryptography-48.0.0-cp311-abi3-win_amd64.whl", hash = "sha256:5b012212e08b8dd5edc78ef54da83dd9892fd9105323b3993eff6bea65dc21d7", size = 3817107, upload-time = "2026-05-04T22:58:08.845Z" }, + { url = "https://files.pythonhosted.org/packages/f2/63/61d4a4e1c6b6bab6ce1e213cd36a24c415d90e76d78c5eb8577c5541d2e8/cryptography-48.0.0-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:58d00498e8933e4a194f3076aee1b4a97dfec1a6da444535755822fe5d8b0b86", size = 7983482, upload-time = "2026-05-04T22:58:43.769Z" }, + { url = "https://files.pythonhosted.org/packages/d5/ac/f5b5995b87770c693e2596559ffafe195b4033a57f14a82268a2842953f3/cryptography-48.0.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:614d0949f4790582d2cc25553abd09dd723025f0c0e7c67376a1d77196743d6e", size = 4683266, upload-time = "2026-05-04T22:58:46.064Z" }, + { url = "https://files.pythonhosted.org/packages/ec/c6/8b14f67e18338fbc4adb76f66c001f5c3610b3e2d1837f268f47a347dbbb/cryptography-48.0.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7ce4bfae76319a532a2dc68f82cc32f5676ee792a983187dac07183690e5c66f", size = 4696228, upload-time = "2026-05-04T22:58:48.22Z" }, + { url = "https://files.pythonhosted.org/packages/ea/73/f808fbae9514bd91b47875b003f13e284c8c6bdfd904b7944e803937eec1/cryptography-48.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:2eb992bbd4661238c5a397594c83f5b4dc2bc5b848c365c8f991b6780efcc5c7", size = 4689097, upload-time = "2026-05-04T22:58:50.9Z" }, + { url = "https://files.pythonhosted.org/packages/93/01/d86632d7d28db8ae83221995752eeb6639ffb374c2d22955648cf8d52797/cryptography-48.0.0-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:22a5cb272895dce158b2cacdfdc3debd299019659f42947dbdac6f32d68fe832", size = 5283582, upload-time = "2026-05-04T22:58:53.017Z" }, + { url = "https://files.pythonhosted.org/packages/02/e1/50edc7a50334807cc4791fc4a0ce7468b4a1416d9138eab358bfc9a3d70b/cryptography-48.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:2b4d59804e8408e2fea7d1fbaf218e5ec984325221db76e6a241a9abd6cdd95c", size = 4730479, upload-time = "2026-05-04T22:58:55.611Z" }, + { url = "https://files.pythonhosted.org/packages/6f/af/99a582b1b1641ff5911ac559beb45097cf79efd4ead4657f578ef1af2d47/cryptography-48.0.0-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:984a20b0f62a26f48a3396c72e4bc34c66e356d356bf370053066b3b6d54634a", size = 4326481, upload-time = "2026-05-04T22:58:57.607Z" }, + { url = "https://files.pythonhosted.org/packages/90/ee/89aa26a06ef0a7d7611788ffd571a7c50e368cc6a4d5eef8b4884e866edb/cryptography-48.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:5a5ed8fde7a1d09376ca0b40e68cd59c69fe23b1f9768bd5824f54681626032a", size = 4688713, upload-time = "2026-05-04T22:59:00.077Z" }, + { url = "https://files.pythonhosted.org/packages/70/ba/bcb1b0bb7a33d4c7c0c4d4c7874b4a62ae4f56113a5f4baefa362dfb1f0f/cryptography-48.0.0-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:8cd666227ef7af430aa5914a9910e0ddd703e75f039cef0825cd0da71b6b711a", size = 5238165, upload-time = "2026-05-04T22:59:02.317Z" }, + { url = "https://files.pythonhosted.org/packages/c9/70/ca4003b1ce5ca3dc3186ada51908c8a9b9ff7d5cab83cc0d43ee14ec144f/cryptography-48.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:9071196d81abc88b3516ac8cdfad32e2b66dd4a5393a8e68a961e9161ddc6239", size = 4729947, upload-time = "2026-05-04T22:59:05.255Z" }, + { url = "https://files.pythonhosted.org/packages/44/a0/4ec7cf774207905aef1a8d11c3750d5a1db805eb380ee4e16df317870128/cryptography-48.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1e2d54c8be6152856a36f0882ab231e70f8ec7f14e93cf87db8a2ed056bf160c", size = 4822059, upload-time = "2026-05-04T22:59:07.802Z" }, + { url = "https://files.pythonhosted.org/packages/1e/75/a2e55f99c16fcac7b5d6c1eb19ad8e00799854d6be5ca845f9259eae1681/cryptography-48.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a5da777e32ffed6f85a7b2b3f7c5cbc88c146bfcd0a1d7baf5fcc6c52ee35dd4", size = 4960575, upload-time = "2026-05-04T22:59:09.851Z" }, + { url = "https://files.pythonhosted.org/packages/b8/23/6e6f32143ab5d8b36ca848a502c4bcd477ae75b9e1677e3530d669062578/cryptography-48.0.0-cp39-abi3-win32.whl", hash = "sha256:77a2ccbbe917f6710e05ba9adaa25fb5075620bf3ea6fb751997875aff4ae4bd", size = 3279117, upload-time = "2026-05-04T22:59:12.019Z" }, + { url = "https://files.pythonhosted.org/packages/9d/9a/0fea98a70cf1749d41d738836f6349d97945f7c89433a259a6c2642eefeb/cryptography-48.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:16cd65b9330583e4619939b3a3843eec1e6e789744bb01e7c7e2e62e33c239c8", size = 3792100, upload-time = "2026-05-04T22:59:14.884Z" }, + { url = "https://files.pythonhosted.org/packages/be/d2/024b5e06be9d44cb021fb0e1a03d34d63989cf56a0fe62f3dfbab695b9b4/cryptography-48.0.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:84cf79f0dc8b36ac5da873481716e87aef31fcfa0444f9e1d8b4b2cece142855", size = 3950391, upload-time = "2026-05-04T22:59:17.415Z" }, + { url = "https://files.pythonhosted.org/packages/bc/17/3861e17c56fa0fd37491a14a8673fdb77c57fc5693cafe745ea8b06dba75/cryptography-48.0.0-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:fdfef35d751d510fcef5252703621574364fec16418c4a1e5e1055248401054b", size = 4637126, upload-time = "2026-05-04T22:59:20.197Z" }, + { url = "https://files.pythonhosted.org/packages/f0/0a/7e226dbff530f21480727eb764973a7bff2b912f8e15cd4f129e71b56d1d/cryptography-48.0.0-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:0890f502ddf7d9c6426129c3f49f5c0a39278ed7cd6322c8755ffca6ee675a13", size = 4667270, upload-time = "2026-05-04T22:59:22.647Z" }, + { url = "https://files.pythonhosted.org/packages/3b/f2/5a72274ca9f1b2a8b44a662ee0bf1b435909deb473d6f97bcd035bcdbc71/cryptography-48.0.0-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:ecde28a596bead48b0cfd2a1b4416c3d43074c2d785e3a398d7ec1fc4d0f7fbb", size = 4636797, upload-time = "2026-05-04T22:59:24.912Z" }, + { url = "https://files.pythonhosted.org/packages/b4/e1/48cedb2fe63626e91ded1edad159e2a4fb8b6906c4425eb7749673077ce7/cryptography-48.0.0-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:4defde8685ae324a9eb9d818717e93b4638ef67070ac9bc15b8ca85f63048355", size = 4666800, upload-time = "2026-05-04T22:59:27.474Z" }, + { url = "https://files.pythonhosted.org/packages/a2/ca/7e8365deec19afb2b2c7be7c1c0aa8f99633b54e90c570999acda93260fc/cryptography-48.0.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:db63bf618e5dea46c07de12e900fe1cdd2541e6dc9dbae772a70b7d4d4765f6a", size = 3739536, upload-time = "2026-05-04T22:59:29.61Z" }, +] + +[[package]] +name = "distro" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fc/f8/98eea607f65de6527f8a2e8885fc8015d3e6f5775df186e443e0964a11c3/distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed", size = 60722, upload-time = "2023-12-24T09:54:32.31Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277, upload-time = "2023-12-24T09:54:30.421Z" }, +] + +[[package]] +name = "docstring-parser" +version = "0.18.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/4d/f332313098c1de1b2d2ff91cf2674415cc7cddab2ca1b01ae29774bd5fdf/docstring_parser-0.18.0.tar.gz", hash = "sha256:292510982205c12b1248696f44959db3cdd1740237a968ea1e2e7a900eeb2015", size = 29341, upload-time = "2026-04-14T04:09:19.867Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/5f/ed01f9a3cdffbd5a008556fc7b2a08ddb1cc6ace7effa7340604b1d16699/docstring_parser-0.18.0-py3-none-any.whl", hash = "sha256:b3fcbed555c47d8479be0796ef7e19c2670d428d72e96da63f3a40122860374b", size = 22484, upload-time = "2026-04-14T04:09:18.638Z" }, +] + +[[package]] +name = "fastapi" +version = "0.136.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-doc" }, + { name = "pydantic" }, + { name = "starlette" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5d/45/c130091c2dfa061bbfe3150f2a5091ef1adf149f2a8d2ae769ecaf6e99a2/fastapi-0.136.1.tar.gz", hash = "sha256:7af665ad7acfa0a3baf8983d393b6b471b9da10ede59c60045f49fbc89a0fa7f", size = 397448, upload-time = "2026-04-23T16:49:44.046Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/ff/2e4eca3ade2c22fe1dea7043b8ee9dabe47753349eb1b56a202de8af6349/fastapi-0.136.1-py3-none-any.whl", hash = "sha256:a6e9d7eeada96c93a4d69cb03836b44fa34e2854accb7244a1ece36cd4781c3f", size = 117683, upload-time = "2026-04-23T16:49:42.437Z" }, +] + +[[package]] +name = "fastuuid" +version = "0.14.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/7d/d9daedf0f2ebcacd20d599928f8913e9d2aea1d56d2d355a93bfa2b611d7/fastuuid-0.14.0.tar.gz", hash = "sha256:178947fc2f995b38497a74172adee64fdeb8b7ec18f2a5934d037641ba265d26", size = 18232, upload-time = "2025-10-19T22:19:22.402Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/98/f3/12481bda4e5b6d3e698fbf525df4443cc7dce746f246b86b6fcb2fba1844/fastuuid-0.14.0-cp311-cp311-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:73946cb950c8caf65127d4e9a325e2b6be0442a224fd51ba3b6ac44e1912ce34", size = 516386, upload-time = "2025-10-19T22:42:40.176Z" }, + { url = "https://files.pythonhosted.org/packages/59/19/2fc58a1446e4d72b655648eb0879b04e88ed6fa70d474efcf550f640f6ec/fastuuid-0.14.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:12ac85024637586a5b69645e7ed986f7535106ed3013640a393a03e461740cb7", size = 264569, upload-time = "2025-10-19T22:25:50.977Z" }, + { url = "https://files.pythonhosted.org/packages/78/29/3c74756e5b02c40cfcc8b1d8b5bac4edbd532b55917a6bcc9113550e99d1/fastuuid-0.14.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:05a8dde1f395e0c9b4be515b7a521403d1e8349443e7641761af07c7ad1624b1", size = 254366, upload-time = "2025-10-19T22:29:49.166Z" }, + { url = "https://files.pythonhosted.org/packages/52/96/d761da3fccfa84f0f353ce6e3eb8b7f76b3aa21fd25e1b00a19f9c80a063/fastuuid-0.14.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:09378a05020e3e4883dfdab438926f31fea15fd17604908f3d39cbeb22a0b4dc", size = 278978, upload-time = "2025-10-19T22:35:41.306Z" }, + { url = "https://files.pythonhosted.org/packages/fc/c2/f84c90167cc7765cb82b3ff7808057608b21c14a38531845d933a4637307/fastuuid-0.14.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbb0c4b15d66b435d2538f3827f05e44e2baafcc003dd7d8472dc67807ab8fd8", size = 279692, upload-time = "2025-10-19T22:25:36.997Z" }, + { url = "https://files.pythonhosted.org/packages/af/7b/4bacd03897b88c12348e7bd77943bac32ccf80ff98100598fcff74f75f2e/fastuuid-0.14.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cd5a7f648d4365b41dbf0e38fe8da4884e57bed4e77c83598e076ac0c93995e7", size = 303384, upload-time = "2025-10-19T22:29:46.578Z" }, + { url = "https://files.pythonhosted.org/packages/c0/a2/584f2c29641df8bd810d00c1f21d408c12e9ad0c0dafdb8b7b29e5ddf787/fastuuid-0.14.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:c0a94245afae4d7af8c43b3159d5e3934c53f47140be0be624b96acd672ceb73", size = 460921, upload-time = "2025-10-19T22:36:42.006Z" }, + { url = "https://files.pythonhosted.org/packages/24/68/c6b77443bb7764c760e211002c8638c0c7cce11cb584927e723215ba1398/fastuuid-0.14.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:2b29e23c97e77c3a9514d70ce343571e469098ac7f5a269320a0f0b3e193ab36", size = 480575, upload-time = "2025-10-19T22:28:18.975Z" }, + { url = "https://files.pythonhosted.org/packages/5a/87/93f553111b33f9bb83145be12868c3c475bf8ea87c107063d01377cc0e8e/fastuuid-0.14.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:1e690d48f923c253f28151b3a6b4e335f2b06bf669c68a02665bc150b7839e94", size = 452317, upload-time = "2025-10-19T22:25:32.75Z" }, + { url = "https://files.pythonhosted.org/packages/9e/8c/a04d486ca55b5abb7eaa65b39df8d891b7b1635b22db2163734dc273579a/fastuuid-0.14.0-cp311-cp311-win32.whl", hash = "sha256:a6f46790d59ab38c6aa0e35c681c0484b50dc0acf9e2679c005d61e019313c24", size = 154804, upload-time = "2025-10-19T22:24:15.615Z" }, + { url = "https://files.pythonhosted.org/packages/9c/b2/2d40bf00820de94b9280366a122cbaa60090c8cf59e89ac3938cf5d75895/fastuuid-0.14.0-cp311-cp311-win_amd64.whl", hash = "sha256:e150eab56c95dc9e3fefc234a0eedb342fac433dacc273cd4d150a5b0871e1fa", size = 156099, upload-time = "2025-10-19T22:24:31.646Z" }, + { url = "https://files.pythonhosted.org/packages/02/a2/e78fcc5df65467f0d207661b7ef86c5b7ac62eea337c0c0fcedbeee6fb13/fastuuid-0.14.0-cp312-cp312-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:77e94728324b63660ebf8adb27055e92d2e4611645bf12ed9d88d30486471d0a", size = 510164, upload-time = "2025-10-19T22:31:45.635Z" }, + { url = "https://files.pythonhosted.org/packages/2b/b3/c846f933f22f581f558ee63f81f29fa924acd971ce903dab1a9b6701816e/fastuuid-0.14.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:caa1f14d2102cb8d353096bc6ef6c13b2c81f347e6ab9d6fbd48b9dea41c153d", size = 261837, upload-time = "2025-10-19T22:38:38.53Z" }, + { url = "https://files.pythonhosted.org/packages/54/ea/682551030f8c4fa9a769d9825570ad28c0c71e30cf34020b85c1f7ee7382/fastuuid-0.14.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d23ef06f9e67163be38cece704170486715b177f6baae338110983f99a72c070", size = 251370, upload-time = "2025-10-19T22:40:26.07Z" }, + { url = "https://files.pythonhosted.org/packages/14/dd/5927f0a523d8e6a76b70968e6004966ee7df30322f5fc9b6cdfb0276646a/fastuuid-0.14.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0c9ec605ace243b6dbe3bd27ebdd5d33b00d8d1d3f580b39fdd15cd96fd71796", size = 277766, upload-time = "2025-10-19T22:37:23.779Z" }, + { url = "https://files.pythonhosted.org/packages/16/6e/c0fb547eef61293153348f12e0f75a06abb322664b34a1573a7760501336/fastuuid-0.14.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:808527f2407f58a76c916d6aa15d58692a4a019fdf8d4c32ac7ff303b7d7af09", size = 278105, upload-time = "2025-10-19T22:26:56.821Z" }, + { url = "https://files.pythonhosted.org/packages/2d/b1/b9c75e03b768f61cf2e84ee193dc18601aeaf89a4684b20f2f0e9f52b62c/fastuuid-0.14.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2fb3c0d7fef6674bbeacdd6dbd386924a7b60b26de849266d1ff6602937675c8", size = 301564, upload-time = "2025-10-19T22:30:31.604Z" }, + { url = "https://files.pythonhosted.org/packages/fc/fa/f7395fdac07c7a54f18f801744573707321ca0cee082e638e36452355a9d/fastuuid-0.14.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ab3f5d36e4393e628a4df337c2c039069344db5f4b9d2a3c9cea48284f1dd741", size = 459659, upload-time = "2025-10-19T22:31:32.341Z" }, + { url = "https://files.pythonhosted.org/packages/66/49/c9fd06a4a0b1f0f048aacb6599e7d96e5d6bc6fa680ed0d46bf111929d1b/fastuuid-0.14.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:b9a0ca4f03b7e0b01425281ffd44e99d360e15c895f1907ca105854ed85e2057", size = 478430, upload-time = "2025-10-19T22:26:22.962Z" }, + { url = "https://files.pythonhosted.org/packages/be/9c/909e8c95b494e8e140e8be6165d5fc3f61fdc46198c1554df7b3e1764471/fastuuid-0.14.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:3acdf655684cc09e60fb7e4cf524e8f42ea760031945aa8086c7eae2eeeabeb8", size = 450894, upload-time = "2025-10-19T22:27:01.647Z" }, + { url = "https://files.pythonhosted.org/packages/90/eb/d29d17521976e673c55ef7f210d4cdd72091a9ec6755d0fd4710d9b3c871/fastuuid-0.14.0-cp312-cp312-win32.whl", hash = "sha256:9579618be6280700ae36ac42c3efd157049fe4dd40ca49b021280481c78c3176", size = 154374, upload-time = "2025-10-19T22:29:19.879Z" }, + { url = "https://files.pythonhosted.org/packages/cc/fc/f5c799a6ea6d877faec0472d0b27c079b47c86b1cdc577720a5386483b36/fastuuid-0.14.0-cp312-cp312-win_amd64.whl", hash = "sha256:d9e4332dc4ba054434a9594cbfaf7823b57993d7d8e7267831c3e059857cf397", size = 156550, upload-time = "2025-10-19T22:27:49.658Z" }, + { url = "https://files.pythonhosted.org/packages/a5/83/ae12dd39b9a39b55d7f90abb8971f1a5f3c321fd72d5aa83f90dc67fe9ed/fastuuid-0.14.0-cp313-cp313-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:77a09cb7427e7af74c594e409f7731a0cf887221de2f698e1ca0ebf0f3139021", size = 510720, upload-time = "2025-10-19T22:42:34.633Z" }, + { url = "https://files.pythonhosted.org/packages/53/b0/a4b03ff5d00f563cc7546b933c28cb3f2a07344b2aec5834e874f7d44143/fastuuid-0.14.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:9bd57289daf7b153bfa3e8013446aa144ce5e8c825e9e366d455155ede5ea2dc", size = 262024, upload-time = "2025-10-19T22:30:25.482Z" }, + { url = "https://files.pythonhosted.org/packages/9c/6d/64aee0a0f6a58eeabadd582e55d0d7d70258ffdd01d093b30c53d668303b/fastuuid-0.14.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ac60fc860cdf3c3f327374db87ab8e064c86566ca8c49d2e30df15eda1b0c2d5", size = 251679, upload-time = "2025-10-19T22:36:14.096Z" }, + { url = "https://files.pythonhosted.org/packages/60/f5/a7e9cda8369e4f7919d36552db9b2ae21db7915083bc6336f1b0082c8b2e/fastuuid-0.14.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ab32f74bd56565b186f036e33129da77db8be09178cd2f5206a5d4035fb2a23f", size = 277862, upload-time = "2025-10-19T22:36:23.302Z" }, + { url = "https://files.pythonhosted.org/packages/f0/d3/8ce11827c783affffd5bd4d6378b28eb6cc6d2ddf41474006b8d62e7448e/fastuuid-0.14.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:33e678459cf4addaedd9936bbb038e35b3f6b2061330fd8f2f6a1d80414c0f87", size = 278278, upload-time = "2025-10-19T22:29:43.809Z" }, + { url = "https://files.pythonhosted.org/packages/a2/51/680fb6352d0bbade04036da46264a8001f74b7484e2fd1f4da9e3db1c666/fastuuid-0.14.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1e3cc56742f76cd25ecb98e4b82a25f978ccffba02e4bdce8aba857b6d85d87b", size = 301788, upload-time = "2025-10-19T22:36:06.825Z" }, + { url = "https://files.pythonhosted.org/packages/fa/7c/2014b5785bd8ebdab04ec857635ebd84d5ee4950186a577db9eff0fb8ff6/fastuuid-0.14.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:cb9a030f609194b679e1660f7e32733b7a0f332d519c5d5a6a0a580991290022", size = 459819, upload-time = "2025-10-19T22:35:31.623Z" }, + { url = "https://files.pythonhosted.org/packages/01/d2/524d4ceeba9160e7a9bc2ea3e8f4ccf1ad78f3bde34090ca0c51f09a5e91/fastuuid-0.14.0-cp313-cp313-musllinux_1_1_i686.whl", hash = "sha256:09098762aad4f8da3a888eb9ae01c84430c907a297b97166b8abc07b640f2995", size = 478546, upload-time = "2025-10-19T22:26:03.023Z" }, + { url = "https://files.pythonhosted.org/packages/bc/17/354d04951ce114bf4afc78e27a18cfbd6ee319ab1829c2d5fb5e94063ac6/fastuuid-0.14.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:1383fff584fa249b16329a059c68ad45d030d5a4b70fb7c73a08d98fd53bcdab", size = 450921, upload-time = "2025-10-19T22:31:02.151Z" }, + { url = "https://files.pythonhosted.org/packages/fb/be/d7be8670151d16d88f15bb121c5b66cdb5ea6a0c2a362d0dcf30276ade53/fastuuid-0.14.0-cp313-cp313-win32.whl", hash = "sha256:a0809f8cc5731c066c909047f9a314d5f536c871a7a22e815cc4967c110ac9ad", size = 154559, upload-time = "2025-10-19T22:36:36.011Z" }, + { url = "https://files.pythonhosted.org/packages/22/1d/5573ef3624ceb7abf4a46073d3554e37191c868abc3aecd5289a72f9810a/fastuuid-0.14.0-cp313-cp313-win_amd64.whl", hash = "sha256:0df14e92e7ad3276327631c9e7cec09e32572ce82089c55cb1bb8df71cf394ed", size = 156539, upload-time = "2025-10-19T22:33:35.898Z" }, +] + +[[package]] +name = "filelock" +version = "3.29.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b5/fe/997687a931ab51049acce6fa1f23e8f01216374ea81374ddee763c493db5/filelock-3.29.0.tar.gz", hash = "sha256:69974355e960702e789734cb4871f884ea6fe50bd8404051a3530bc07809cf90", size = 57571, upload-time = "2026-04-19T15:39:10.068Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/47/dd9a212ef6e343a6857485ffe25bba537304f1913bdbed446a23f7f592e1/filelock-3.29.0-py3-none-any.whl", hash = "sha256:96f5f6344709aa1572bbf631c640e4ebeeb519e08da902c39a001882f30ac258", size = 39812, upload-time = "2026-04-19T15:39:08.752Z" }, +] + +[[package]] +name = "frozenlist" +version = "1.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2d/f5/c831fac6cc817d26fd54c7eaccd04ef7e0288806943f7cc5bbf69f3ac1f0/frozenlist-1.8.0.tar.gz", hash = "sha256:3ede829ed8d842f6cd48fc7081d7a41001a56f1f38603f9d49bf3020d59a31ad", size = 45875, upload-time = "2025-10-06T05:38:17.865Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bc/03/077f869d540370db12165c0aa51640a873fb661d8b315d1d4d67b284d7ac/frozenlist-1.8.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:09474e9831bc2b2199fad6da3c14c7b0fbdd377cce9d3d77131be28906cb7d84", size = 86912, upload-time = "2025-10-06T05:35:45.98Z" }, + { url = "https://files.pythonhosted.org/packages/df/b5/7610b6bd13e4ae77b96ba85abea1c8cb249683217ef09ac9e0ae93f25a91/frozenlist-1.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:17c883ab0ab67200b5f964d2b9ed6b00971917d5d8a92df149dc2c9779208ee9", size = 50046, upload-time = "2025-10-06T05:35:47.009Z" }, + { url = "https://files.pythonhosted.org/packages/6e/ef/0e8f1fe32f8a53dd26bdd1f9347efe0778b0fddf62789ea683f4cc7d787d/frozenlist-1.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fa47e444b8ba08fffd1c18e8cdb9a75db1b6a27f17507522834ad13ed5922b93", size = 50119, upload-time = "2025-10-06T05:35:48.38Z" }, + { url = "https://files.pythonhosted.org/packages/11/b1/71a477adc7c36e5fb628245dfbdea2166feae310757dea848d02bd0689fd/frozenlist-1.8.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2552f44204b744fba866e573be4c1f9048d6a324dfe14475103fd51613eb1d1f", size = 231067, upload-time = "2025-10-06T05:35:49.97Z" }, + { url = "https://files.pythonhosted.org/packages/45/7e/afe40eca3a2dc19b9904c0f5d7edfe82b5304cb831391edec0ac04af94c2/frozenlist-1.8.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:957e7c38f250991e48a9a73e6423db1bb9dd14e722a10f6b8bb8e16a0f55f695", size = 233160, upload-time = "2025-10-06T05:35:51.729Z" }, + { url = "https://files.pythonhosted.org/packages/a6/aa/7416eac95603ce428679d273255ffc7c998d4132cfae200103f164b108aa/frozenlist-1.8.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:8585e3bb2cdea02fc88ffa245069c36555557ad3609e83be0ec71f54fd4abb52", size = 228544, upload-time = "2025-10-06T05:35:53.246Z" }, + { url = "https://files.pythonhosted.org/packages/8b/3d/2a2d1f683d55ac7e3875e4263d28410063e738384d3adc294f5ff3d7105e/frozenlist-1.8.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:edee74874ce20a373d62dc28b0b18b93f645633c2943fd90ee9d898550770581", size = 243797, upload-time = "2025-10-06T05:35:54.497Z" }, + { url = "https://files.pythonhosted.org/packages/78/1e/2d5565b589e580c296d3bb54da08d206e797d941a83a6fdea42af23be79c/frozenlist-1.8.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c9a63152fe95756b85f31186bddf42e4c02c6321207fd6601a1c89ebac4fe567", size = 247923, upload-time = "2025-10-06T05:35:55.861Z" }, + { url = "https://files.pythonhosted.org/packages/aa/c3/65872fcf1d326a7f101ad4d86285c403c87be7d832b7470b77f6d2ed5ddc/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b6db2185db9be0a04fecf2f241c70b63b1a242e2805be291855078f2b404dd6b", size = 230886, upload-time = "2025-10-06T05:35:57.399Z" }, + { url = "https://files.pythonhosted.org/packages/a0/76/ac9ced601d62f6956f03cc794f9e04c81719509f85255abf96e2510f4265/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:f4be2e3d8bc8aabd566f8d5b8ba7ecc09249d74ba3c9ed52e54dc23a293f0b92", size = 245731, upload-time = "2025-10-06T05:35:58.563Z" }, + { url = "https://files.pythonhosted.org/packages/b9/49/ecccb5f2598daf0b4a1415497eba4c33c1e8ce07495eb07d2860c731b8d5/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:c8d1634419f39ea6f5c427ea2f90ca85126b54b50837f31497f3bf38266e853d", size = 241544, upload-time = "2025-10-06T05:35:59.719Z" }, + { url = "https://files.pythonhosted.org/packages/53/4b/ddf24113323c0bbcc54cb38c8b8916f1da7165e07b8e24a717b4a12cbf10/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:1a7fa382a4a223773ed64242dbe1c9c326ec09457e6b8428efb4118c685c3dfd", size = 241806, upload-time = "2025-10-06T05:36:00.959Z" }, + { url = "https://files.pythonhosted.org/packages/a7/fb/9b9a084d73c67175484ba2789a59f8eebebd0827d186a8102005ce41e1ba/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:11847b53d722050808926e785df837353bd4d75f1d494377e59b23594d834967", size = 229382, upload-time = "2025-10-06T05:36:02.22Z" }, + { url = "https://files.pythonhosted.org/packages/95/a3/c8fb25aac55bf5e12dae5c5aa6a98f85d436c1dc658f21c3ac73f9fa95e5/frozenlist-1.8.0-cp311-cp311-win32.whl", hash = "sha256:27c6e8077956cf73eadd514be8fb04d77fc946a7fe9f7fe167648b0b9085cc25", size = 39647, upload-time = "2025-10-06T05:36:03.409Z" }, + { url = "https://files.pythonhosted.org/packages/0a/f5/603d0d6a02cfd4c8f2a095a54672b3cf967ad688a60fb9faf04fc4887f65/frozenlist-1.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:ac913f8403b36a2c8610bbfd25b8013488533e71e62b4b4adce9c86c8cea905b", size = 44064, upload-time = "2025-10-06T05:36:04.368Z" }, + { url = "https://files.pythonhosted.org/packages/5d/16/c2c9ab44e181f043a86f9a8f84d5124b62dbcb3a02c0977ec72b9ac1d3e0/frozenlist-1.8.0-cp311-cp311-win_arm64.whl", hash = "sha256:d4d3214a0f8394edfa3e303136d0575eece0745ff2b47bd2cb2e66dd92d4351a", size = 39937, upload-time = "2025-10-06T05:36:05.669Z" }, + { url = "https://files.pythonhosted.org/packages/69/29/948b9aa87e75820a38650af445d2ef2b6b8a6fab1a23b6bb9e4ef0be2d59/frozenlist-1.8.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:78f7b9e5d6f2fdb88cdde9440dc147259b62b9d3b019924def9f6478be254ac1", size = 87782, upload-time = "2025-10-06T05:36:06.649Z" }, + { url = "https://files.pythonhosted.org/packages/64/80/4f6e318ee2a7c0750ed724fa33a4bdf1eacdc5a39a7a24e818a773cd91af/frozenlist-1.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:229bf37d2e4acdaf808fd3f06e854a4a7a3661e871b10dc1f8f1896a3b05f18b", size = 50594, upload-time = "2025-10-06T05:36:07.69Z" }, + { url = "https://files.pythonhosted.org/packages/2b/94/5c8a2b50a496b11dd519f4a24cb5496cf125681dd99e94c604ccdea9419a/frozenlist-1.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f833670942247a14eafbb675458b4e61c82e002a148f49e68257b79296e865c4", size = 50448, upload-time = "2025-10-06T05:36:08.78Z" }, + { url = "https://files.pythonhosted.org/packages/6a/bd/d91c5e39f490a49df14320f4e8c80161cfcce09f1e2cde1edd16a551abb3/frozenlist-1.8.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:494a5952b1c597ba44e0e78113a7266e656b9794eec897b19ead706bd7074383", size = 242411, upload-time = "2025-10-06T05:36:09.801Z" }, + { url = "https://files.pythonhosted.org/packages/8f/83/f61505a05109ef3293dfb1ff594d13d64a2324ac3482be2cedc2be818256/frozenlist-1.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96f423a119f4777a4a056b66ce11527366a8bb92f54e541ade21f2374433f6d4", size = 243014, upload-time = "2025-10-06T05:36:11.394Z" }, + { url = "https://files.pythonhosted.org/packages/d8/cb/cb6c7b0f7d4023ddda30cf56b8b17494eb3a79e3fda666bf735f63118b35/frozenlist-1.8.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3462dd9475af2025c31cc61be6652dfa25cbfb56cbbf52f4ccfe029f38decaf8", size = 234909, upload-time = "2025-10-06T05:36:12.598Z" }, + { url = "https://files.pythonhosted.org/packages/31/c5/cd7a1f3b8b34af009fb17d4123c5a778b44ae2804e3ad6b86204255f9ec5/frozenlist-1.8.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c4c800524c9cd9bac5166cd6f55285957fcfc907db323e193f2afcd4d9abd69b", size = 250049, upload-time = "2025-10-06T05:36:14.065Z" }, + { url = "https://files.pythonhosted.org/packages/c0/01/2f95d3b416c584a1e7f0e1d6d31998c4a795f7544069ee2e0962a4b60740/frozenlist-1.8.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d6a5df73acd3399d893dafc71663ad22534b5aa4f94e8a2fabfe856c3c1b6a52", size = 256485, upload-time = "2025-10-06T05:36:15.39Z" }, + { url = "https://files.pythonhosted.org/packages/ce/03/024bf7720b3abaebcff6d0793d73c154237b85bdf67b7ed55e5e9596dc9a/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:405e8fe955c2280ce66428b3ca55e12b3c4e9c336fb2103a4937e891c69a4a29", size = 237619, upload-time = "2025-10-06T05:36:16.558Z" }, + { url = "https://files.pythonhosted.org/packages/69/fa/f8abdfe7d76b731f5d8bd217827cf6764d4f1d9763407e42717b4bed50a0/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:908bd3f6439f2fef9e85031b59fd4f1297af54415fb60e4254a95f75b3cab3f3", size = 250320, upload-time = "2025-10-06T05:36:17.821Z" }, + { url = "https://files.pythonhosted.org/packages/f5/3c/b051329f718b463b22613e269ad72138cc256c540f78a6de89452803a47d/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:294e487f9ec720bd8ffcebc99d575f7eff3568a08a253d1ee1a0378754b74143", size = 246820, upload-time = "2025-10-06T05:36:19.046Z" }, + { url = "https://files.pythonhosted.org/packages/0f/ae/58282e8f98e444b3f4dd42448ff36fa38bef29e40d40f330b22e7108f565/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:74c51543498289c0c43656701be6b077f4b265868fa7f8a8859c197006efb608", size = 250518, upload-time = "2025-10-06T05:36:20.763Z" }, + { url = "https://files.pythonhosted.org/packages/8f/96/007e5944694d66123183845a106547a15944fbbb7154788cbf7272789536/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:776f352e8329135506a1d6bf16ac3f87bc25b28e765949282dcc627af36123aa", size = 239096, upload-time = "2025-10-06T05:36:22.129Z" }, + { url = "https://files.pythonhosted.org/packages/66/bb/852b9d6db2fa40be96f29c0d1205c306288f0684df8fd26ca1951d461a56/frozenlist-1.8.0-cp312-cp312-win32.whl", hash = "sha256:433403ae80709741ce34038da08511d4a77062aa924baf411ef73d1146e74faf", size = 39985, upload-time = "2025-10-06T05:36:23.661Z" }, + { url = "https://files.pythonhosted.org/packages/b8/af/38e51a553dd66eb064cdf193841f16f077585d4d28394c2fa6235cb41765/frozenlist-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:34187385b08f866104f0c0617404c8eb08165ab1272e884abc89c112e9c00746", size = 44591, upload-time = "2025-10-06T05:36:24.958Z" }, + { url = "https://files.pythonhosted.org/packages/a7/06/1dc65480ab147339fecc70797e9c2f69d9cea9cf38934ce08df070fdb9cb/frozenlist-1.8.0-cp312-cp312-win_arm64.whl", hash = "sha256:fe3c58d2f5db5fbd18c2987cba06d51b0529f52bc3a6cdc33d3f4eab725104bd", size = 40102, upload-time = "2025-10-06T05:36:26.333Z" }, + { url = "https://files.pythonhosted.org/packages/2d/40/0832c31a37d60f60ed79e9dfb5a92e1e2af4f40a16a29abcc7992af9edff/frozenlist-1.8.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8d92f1a84bb12d9e56f818b3a746f3efba93c1b63c8387a73dde655e1e42282a", size = 85717, upload-time = "2025-10-06T05:36:27.341Z" }, + { url = "https://files.pythonhosted.org/packages/30/ba/b0b3de23f40bc55a7057bd38434e25c34fa48e17f20ee273bbde5e0650f3/frozenlist-1.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:96153e77a591c8adc2ee805756c61f59fef4cf4073a9275ee86fe8cba41241f7", size = 49651, upload-time = "2025-10-06T05:36:28.855Z" }, + { url = "https://files.pythonhosted.org/packages/0c/ab/6e5080ee374f875296c4243c381bbdef97a9ac39c6e3ce1d5f7d42cb78d6/frozenlist-1.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f21f00a91358803399890ab167098c131ec2ddd5f8f5fd5fe9c9f2c6fcd91e40", size = 49417, upload-time = "2025-10-06T05:36:29.877Z" }, + { url = "https://files.pythonhosted.org/packages/d5/4e/e4691508f9477ce67da2015d8c00acd751e6287739123113a9fca6f1604e/frozenlist-1.8.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fb30f9626572a76dfe4293c7194a09fb1fe93ba94c7d4f720dfae3b646b45027", size = 234391, upload-time = "2025-10-06T05:36:31.301Z" }, + { url = "https://files.pythonhosted.org/packages/40/76/c202df58e3acdf12969a7895fd6f3bc016c642e6726aa63bd3025e0fc71c/frozenlist-1.8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eaa352d7047a31d87dafcacbabe89df0aa506abb5b1b85a2fb91bc3faa02d822", size = 233048, upload-time = "2025-10-06T05:36:32.531Z" }, + { url = "https://files.pythonhosted.org/packages/f9/c0/8746afb90f17b73ca5979c7a3958116e105ff796e718575175319b5bb4ce/frozenlist-1.8.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:03ae967b4e297f58f8c774c7eabcce57fe3c2434817d4385c50661845a058121", size = 226549, upload-time = "2025-10-06T05:36:33.706Z" }, + { url = "https://files.pythonhosted.org/packages/7e/eb/4c7eefc718ff72f9b6c4893291abaae5fbc0c82226a32dcd8ef4f7a5dbef/frozenlist-1.8.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f6292f1de555ffcc675941d65fffffb0a5bcd992905015f85d0592201793e0e5", size = 239833, upload-time = "2025-10-06T05:36:34.947Z" }, + { url = "https://files.pythonhosted.org/packages/c2/4e/e5c02187cf704224f8b21bee886f3d713ca379535f16893233b9d672ea71/frozenlist-1.8.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29548f9b5b5e3460ce7378144c3010363d8035cea44bc0bf02d57f5a685e084e", size = 245363, upload-time = "2025-10-06T05:36:36.534Z" }, + { url = "https://files.pythonhosted.org/packages/1f/96/cb85ec608464472e82ad37a17f844889c36100eed57bea094518bf270692/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ec3cc8c5d4084591b4237c0a272cc4f50a5b03396a47d9caaf76f5d7b38a4f11", size = 229314, upload-time = "2025-10-06T05:36:38.582Z" }, + { url = "https://files.pythonhosted.org/packages/5d/6f/4ae69c550e4cee66b57887daeebe006fe985917c01d0fff9caab9883f6d0/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:517279f58009d0b1f2e7c1b130b377a349405da3f7621ed6bfae50b10adf20c1", size = 243365, upload-time = "2025-10-06T05:36:40.152Z" }, + { url = "https://files.pythonhosted.org/packages/7a/58/afd56de246cf11780a40a2c28dc7cbabbf06337cc8ddb1c780a2d97e88d8/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:db1e72ede2d0d7ccb213f218df6a078a9c09a7de257c2fe8fcef16d5925230b1", size = 237763, upload-time = "2025-10-06T05:36:41.355Z" }, + { url = "https://files.pythonhosted.org/packages/cb/36/cdfaf6ed42e2644740d4a10452d8e97fa1c062e2a8006e4b09f1b5fd7d63/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b4dec9482a65c54a5044486847b8a66bf10c9cb4926d42927ec4e8fd5db7fed8", size = 240110, upload-time = "2025-10-06T05:36:42.716Z" }, + { url = "https://files.pythonhosted.org/packages/03/a8/9ea226fbefad669f11b52e864c55f0bd57d3c8d7eb07e9f2e9a0b39502e1/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:21900c48ae04d13d416f0e1e0c4d81f7931f73a9dfa0b7a8746fb2fe7dd970ed", size = 233717, upload-time = "2025-10-06T05:36:44.251Z" }, + { url = "https://files.pythonhosted.org/packages/1e/0b/1b5531611e83ba7d13ccc9988967ea1b51186af64c42b7a7af465dcc9568/frozenlist-1.8.0-cp313-cp313-win32.whl", hash = "sha256:8b7b94a067d1c504ee0b16def57ad5738701e4ba10cec90529f13fa03c833496", size = 39628, upload-time = "2025-10-06T05:36:45.423Z" }, + { url = "https://files.pythonhosted.org/packages/d8/cf/174c91dbc9cc49bc7b7aab74d8b734e974d1faa8f191c74af9b7e80848e6/frozenlist-1.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:878be833caa6a3821caf85eb39c5ba92d28e85df26d57afb06b35b2efd937231", size = 43882, upload-time = "2025-10-06T05:36:46.796Z" }, + { url = "https://files.pythonhosted.org/packages/c1/17/502cd212cbfa96eb1388614fe39a3fc9ab87dbbe042b66f97acb57474834/frozenlist-1.8.0-cp313-cp313-win_arm64.whl", hash = "sha256:44389d135b3ff43ba8cc89ff7f51f5a0bb6b63d829c8300f79a2fe4fe61bcc62", size = 39676, upload-time = "2025-10-06T05:36:47.8Z" }, + { url = "https://files.pythonhosted.org/packages/d2/5c/3bbfaa920dfab09e76946a5d2833a7cbdf7b9b4a91c714666ac4855b88b4/frozenlist-1.8.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:e25ac20a2ef37e91c1b39938b591457666a0fa835c7783c3a8f33ea42870db94", size = 89235, upload-time = "2025-10-06T05:36:48.78Z" }, + { url = "https://files.pythonhosted.org/packages/d2/d6/f03961ef72166cec1687e84e8925838442b615bd0b8854b54923ce5b7b8a/frozenlist-1.8.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:07cdca25a91a4386d2e76ad992916a85038a9b97561bf7a3fd12d5d9ce31870c", size = 50742, upload-time = "2025-10-06T05:36:49.837Z" }, + { url = "https://files.pythonhosted.org/packages/1e/bb/a6d12b7ba4c3337667d0e421f7181c82dda448ce4e7ad7ecd249a16fa806/frozenlist-1.8.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4e0c11f2cc6717e0a741f84a527c52616140741cd812a50422f83dc31749fb52", size = 51725, upload-time = "2025-10-06T05:36:50.851Z" }, + { url = "https://files.pythonhosted.org/packages/bc/71/d1fed0ffe2c2ccd70b43714c6cab0f4188f09f8a67a7914a6b46ee30f274/frozenlist-1.8.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b3210649ee28062ea6099cfda39e147fa1bc039583c8ee4481cb7811e2448c51", size = 284533, upload-time = "2025-10-06T05:36:51.898Z" }, + { url = "https://files.pythonhosted.org/packages/c9/1f/fb1685a7b009d89f9bf78a42d94461bc06581f6e718c39344754a5d9bada/frozenlist-1.8.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:581ef5194c48035a7de2aefc72ac6539823bb71508189e5de01d60c9dcd5fa65", size = 292506, upload-time = "2025-10-06T05:36:53.101Z" }, + { url = "https://files.pythonhosted.org/packages/e6/3b/b991fe1612703f7e0d05c0cf734c1b77aaf7c7d321df4572e8d36e7048c8/frozenlist-1.8.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3ef2d026f16a2b1866e1d86fc4e1291e1ed8a387b2c333809419a2f8b3a77b82", size = 274161, upload-time = "2025-10-06T05:36:54.309Z" }, + { url = "https://files.pythonhosted.org/packages/ca/ec/c5c618767bcdf66e88945ec0157d7f6c4a1322f1473392319b7a2501ded7/frozenlist-1.8.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5500ef82073f599ac84d888e3a8c1f77ac831183244bfd7f11eaa0289fb30714", size = 294676, upload-time = "2025-10-06T05:36:55.566Z" }, + { url = "https://files.pythonhosted.org/packages/7c/ce/3934758637d8f8a88d11f0585d6495ef54b2044ed6ec84492a91fa3b27aa/frozenlist-1.8.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:50066c3997d0091c411a66e710f4e11752251e6d2d73d70d8d5d4c76442a199d", size = 300638, upload-time = "2025-10-06T05:36:56.758Z" }, + { url = "https://files.pythonhosted.org/packages/fc/4f/a7e4d0d467298f42de4b41cbc7ddaf19d3cfeabaf9ff97c20c6c7ee409f9/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:5c1c8e78426e59b3f8005e9b19f6ff46e5845895adbde20ece9218319eca6506", size = 283067, upload-time = "2025-10-06T05:36:57.965Z" }, + { url = "https://files.pythonhosted.org/packages/dc/48/c7b163063d55a83772b268e6d1affb960771b0e203b632cfe09522d67ea5/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:eefdba20de0d938cec6a89bd4d70f346a03108a19b9df4248d3cf0d88f1b0f51", size = 292101, upload-time = "2025-10-06T05:36:59.237Z" }, + { url = "https://files.pythonhosted.org/packages/9f/d0/2366d3c4ecdc2fd391e0afa6e11500bfba0ea772764d631bbf82f0136c9d/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:cf253e0e1c3ceb4aaff6df637ce033ff6535fb8c70a764a8f46aafd3d6ab798e", size = 289901, upload-time = "2025-10-06T05:37:00.811Z" }, + { url = "https://files.pythonhosted.org/packages/b8/94/daff920e82c1b70e3618a2ac39fbc01ae3e2ff6124e80739ce5d71c9b920/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:032efa2674356903cd0261c4317a561a6850f3ac864a63fc1583147fb05a79b0", size = 289395, upload-time = "2025-10-06T05:37:02.115Z" }, + { url = "https://files.pythonhosted.org/packages/e3/20/bba307ab4235a09fdcd3cc5508dbabd17c4634a1af4b96e0f69bfe551ebd/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6da155091429aeba16851ecb10a9104a108bcd32f6c1642867eadaee401c1c41", size = 283659, upload-time = "2025-10-06T05:37:03.711Z" }, + { url = "https://files.pythonhosted.org/packages/fd/00/04ca1c3a7a124b6de4f8a9a17cc2fcad138b4608e7a3fc5877804b8715d7/frozenlist-1.8.0-cp313-cp313t-win32.whl", hash = "sha256:0f96534f8bfebc1a394209427d0f8a63d343c9779cda6fc25e8e121b5fd8555b", size = 43492, upload-time = "2025-10-06T05:37:04.915Z" }, + { url = "https://files.pythonhosted.org/packages/59/5e/c69f733a86a94ab10f68e496dc6b7e8bc078ebb415281d5698313e3af3a1/frozenlist-1.8.0-cp313-cp313t-win_amd64.whl", hash = "sha256:5d63a068f978fc69421fb0e6eb91a9603187527c86b7cd3f534a5b77a592b888", size = 48034, upload-time = "2025-10-06T05:37:06.343Z" }, + { url = "https://files.pythonhosted.org/packages/16/6c/be9d79775d8abe79b05fa6d23da99ad6e7763a1d080fbae7290b286093fd/frozenlist-1.8.0-cp313-cp313t-win_arm64.whl", hash = "sha256:bf0a7e10b077bf5fb9380ad3ae8ce20ef919a6ad93b4552896419ac7e1d8e042", size = 41749, upload-time = "2025-10-06T05:37:07.431Z" }, + { url = "https://files.pythonhosted.org/packages/9a/9a/e35b4a917281c0b8419d4207f4334c8e8c5dbf4f3f5f9ada73958d937dcc/frozenlist-1.8.0-py3-none-any.whl", hash = "sha256:0c18a16eab41e82c295618a77502e17b195883241c563b00f0aa5106fc4eaa0d", size = 13409, upload-time = "2025-10-06T05:38:16.721Z" }, +] + +[[package]] +name = "fsspec" +version = "2026.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d5/8d/1c51c094345df128ca4a990d633fe1a0ff28726c9e6b3c41ba65087bba1d/fsspec-2026.4.0.tar.gz", hash = "sha256:301d8ac70ae90ef3ad05dcf94d6c3754a097f9b5fe4667d2787aa359ec7df7e4", size = 312760, upload-time = "2026-04-29T20:42:38.635Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d5/0c/043d5e551459da400957a1395e0febbf771446ff34291afcbe3d8be2a279/fsspec-2026.4.0-py3-none-any.whl", hash = "sha256:11ef7bb35dab8a394fde6e608221d5cf3e8499401c249bebaeaad760a1a8dec2", size = 203402, upload-time = "2026-04-29T20:42:36.842Z" }, +] + +[[package]] +name = "gepa" +version = "0.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e4/62/10f5a8f24c075e3b64f952be73ba8e15f0055584bbcdf9ce48d754a36679/gepa-0.1.1.tar.gz", hash = "sha256:643fda01c23de4c9f01306e01305dd69facc29bcb34ad59e4cd07e6621d34aa1", size = 272251, upload-time = "2026-03-16T10:17:53.131Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/b7/8c72dedbb950d88a6f64588fcbc590d2a21e2b9f19b36aa6c5016c54ec75/gepa-0.1.1-py3-none-any.whl", hash = "sha256:71ead7c591eafcc727b83509cdc4182f20264800a6ddf8520d61419daeb47466", size = 244246, upload-time = "2026-03-16T10:17:51.922Z" }, +] + +[[package]] +name = "google-adk" +version = "1.34.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiosqlite" }, + { name = "anyio" }, + { name = "authlib" }, + { name = "click" }, + { name = "fastapi" }, + { name = "google-api-python-client" }, + { name = "google-auth", extra = ["pyopenssl"] }, + { name = "google-cloud-aiplatform", extra = ["agent-engines"] }, + { name = "google-cloud-bigquery" }, + { name = "google-cloud-bigquery-storage" }, + { name = "google-cloud-bigtable" }, + { name = "google-cloud-dataplex" }, + { name = "google-cloud-discoveryengine" }, + { name = "google-cloud-pubsub" }, + { name = "google-cloud-secret-manager" }, + { name = "google-cloud-spanner" }, + { name = "google-cloud-speech" }, + { name = "google-cloud-storage" }, + { name = "google-genai" }, + { name = "graphviz" }, + { name = "httpx" }, + { name = "jsonschema" }, + { name = "mcp" }, + { name = "opentelemetry-api" }, + { name = "opentelemetry-exporter-gcp-logging" }, + { name = "opentelemetry-exporter-gcp-monitoring" }, + { name = "opentelemetry-exporter-gcp-trace" }, + { name = "opentelemetry-exporter-otlp-proto-http" }, + { name = "opentelemetry-resourcedetector-gcp" }, + { name = "opentelemetry-sdk" }, + { name = "pyarrow" }, + { name = "pydantic" }, + { name = "python-dateutil" }, + { name = "python-dotenv" }, + { name = "pyyaml" }, + { name = "requests" }, + { name = "sqlalchemy" }, + { name = "sqlalchemy-spanner" }, + { name = "starlette" }, + { name = "tenacity" }, + { name = "typing-extensions" }, + { name = "tzlocal" }, + { name = "uvicorn" }, + { name = "watchdog" }, + { name = "websockets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4c/7b/8979da99b14f6b8418a9936d0f6290da5c7c0a75be8fc422ffb5a9f116eb/google_adk-1.34.1.tar.gz", hash = "sha256:ddc521a218e20eb12edad0974babfca7bc2fe79b70669dcb7d5ebd55d9adba90", size = 2431664, upload-time = "2026-05-22T18:43:32.935Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3d/63/04ecc46a09bdb609064b231e42d99cbadd3d332e9c3cab395b73b9c8b1a0/google_adk-1.34.1-py3-none-any.whl", hash = "sha256:60d3a8169888f0b47c5de73628fe14665bf914273f53a643e731f8b79dbc4390", size = 2876291, upload-time = "2026-05-22T18:43:34.61Z" }, +] + +[package.optional-dependencies] +eval = [ + { name = "gepa" }, + { name = "google-cloud-aiplatform", extra = ["evaluation"] }, + { name = "jinja2" }, + { name = "pandas" }, + { name = "rouge-score" }, + { name = "tabulate" }, +] + +[[package]] +name = "google-api-core" +version = "2.30.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "google-auth" }, + { name = "googleapis-common-protos" }, + { name = "proto-plus" }, + { name = "protobuf" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/16/ce/502a57fb0ec752026d24df1280b162294b22a0afb98a326084f9a979138b/google_api_core-2.30.3.tar.gz", hash = "sha256:e601a37f148585319b26db36e219df68c5d07b6382cff2d580e83404e44d641b", size = 177001, upload-time = "2026-04-10T00:41:28.035Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/03/15/e56f351cf6ef1cfea58e6ac226a7318ed1deb2218c4b3cc9bd9e4b786c5a/google_api_core-2.30.3-py3-none-any.whl", hash = "sha256:a85761ba72c444dad5d611c2220633480b2b6be2521eca69cca2dbb3ffd6bfe8", size = 173274, upload-time = "2026-04-09T22:57:16.198Z" }, +] + +[package.optional-dependencies] +grpc = [ + { name = "grpcio" }, + { name = "grpcio-status" }, +] + +[[package]] +name = "google-api-python-client" +version = "2.196.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "google-api-core" }, + { name = "google-auth" }, + { name = "google-auth-httplib2" }, + { name = "httplib2" }, + { name = "uritemplate" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6d/f3/34ef8aca7909675fe327f96c1ed927f0520e7acf68af19157e96acc05e76/google_api_python_client-2.196.0.tar.gz", hash = "sha256:9f335d38f6caaa2747bcf64335ed1a9a19047d53e86538eda6a1b17d37f1743d", size = 14628129, upload-time = "2026-05-06T23:47:35.655Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/99/c7/1817b4edf966d5afcac1c0781ca36d621bc0cb58104c4e7c2a475ab185f7/google_api_python_client-2.196.0-py3-none-any.whl", hash = "sha256:2591e9b47dcb17e4e62a09370aaee3bcf323af8f28ccecdabcd0a42a23ca4db5", size = 15206663, upload-time = "2026-05-06T23:47:32.886Z" }, +] + +[[package]] +name = "google-auth" +version = "2.53.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, + { name = "pyasn1-modules" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c6/ad/ff781329bbbdc0974a098d996e89c9e1f7024262f9e3eec442fbb9ad1ac6/google_auth-2.53.0.tar.gz", hash = "sha256:e7e6aa16f6bee7b2b264830fd04f08087a1d5a836df516251a5d15327b246c9c", size = 335844, upload-time = "2026-05-15T20:53:07.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4a/c9/db44165ba7c581268c6d46017ef63339110378305062830104fc7fa144cb/google_auth-2.53.0-py3-none-any.whl", hash = "sha256:6e7449917c599b35126a99ec268ec6880301f2fea41dce198fe8fd83ff642b68", size = 246071, upload-time = "2026-05-15T20:53:05.609Z" }, +] + +[package.optional-dependencies] +pyopenssl = [ + { name = "pyopenssl" }, +] +requests = [ + { name = "requests" }, +] + +[[package]] +name = "google-auth-httplib2" +version = "0.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "google-auth" }, + { name = "httplib2" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1c/b3/f192c8bc7e41e0ebdbd95afcae4783417a34b6a6af62d22daf22c3fd38fc/google_auth_httplib2-0.4.0.tar.gz", hash = "sha256:d5b030a204b7a4b4d553ba9ca701b62481ee2b74419325580be70f7d85ffed35", size = 11161, upload-time = "2026-05-07T08:03:46.878Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/97/be/954c35a62b9e31de66b0a43c225c9b6bb9e0f98d6b1dc110a2308e3644f5/google_auth_httplib2-0.4.0-py3-none-any.whl", hash = "sha256:8e55cfafa3358cba85f6cad4a886138e88e158d71e7e5c9ee5936a5c1507fb91", size = 9529, upload-time = "2026-05-07T08:02:12.375Z" }, +] + +[[package]] +name = "google-cloud-aiplatform" +version = "1.153.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "docstring-parser" }, + { name = "google-api-core", extra = ["grpc"] }, + { name = "google-auth" }, + { name = "google-cloud-bigquery" }, + { name = "google-cloud-resource-manager" }, + { name = "google-cloud-storage" }, + { name = "google-genai" }, + { name = "packaging" }, + { name = "proto-plus" }, + { name = "protobuf" }, + { name = "pydantic" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d5/97/1779e66ab845550bc602364311ea093ba156cb805a1c31b7c4d6f25b5863/google_cloud_aiplatform-1.153.1.tar.gz", hash = "sha256:445b6c683d5c630f174d81ae1f69f7da9e27e4d4ec5b70c5fe96de5c1247cfbc", size = 11011349, upload-time = "2026-05-15T06:34:14.851Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/16/01/8a1900e7a742ed480e6037ac4f6541466cb981d81bd4cbd34a9d46204ea1/google_cloud_aiplatform-1.153.1-py2.py3-none-any.whl", hash = "sha256:033fa1595a7e8ed1d97066e261e630f38fbc60e10c98c6487cf228fe9c7ec151", size = 9170782, upload-time = "2026-05-15T06:34:10.887Z" }, +] + +[package.optional-dependencies] +agent-engines = [ + { name = "aiohttp" }, + { name = "cloudpickle" }, + { name = "google-cloud-iam" }, + { name = "google-cloud-logging" }, + { name = "google-cloud-trace" }, + { name = "opentelemetry-exporter-gcp-logging" }, + { name = "opentelemetry-exporter-gcp-trace" }, + { name = "opentelemetry-exporter-otlp-proto-http" }, + { name = "opentelemetry-sdk" }, + { name = "packaging" }, + { name = "pydantic" }, + { name = "typing-extensions" }, +] +evaluation = [ + { name = "jsonschema" }, + { name = "litellm" }, + { name = "pandas" }, + { name = "pyyaml" }, + { name = "ruamel-yaml" }, + { name = "scikit-learn" }, + { name = "tqdm" }, +] + +[[package]] +name = "google-cloud-appengine-logging" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "google-api-core", extra = ["grpc"] }, + { name = "google-auth" }, + { name = "grpcio" }, + { name = "proto-plus" }, + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bc/02/800897064ca6f1a26835cdf23939c4b93e38a30f3fb5c7cec7c01ae2edc2/google_cloud_appengine_logging-1.9.0.tar.gz", hash = "sha256:ff397f0bbc1485f979ab45767c38e0f676c9598c97c384f7412216e6ea22f805", size = 17963, upload-time = "2026-03-30T22:51:33.556Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/56/4a/304d42664ab2afbe7be39559c9eb3f81dd06e7ac9284f9f36f726f15939d/google_cloud_appengine_logging-1.9.0-py3-none-any.whl", hash = "sha256:bbf3a7e4dc171678f7f481259d1f68c3ae7d337530f1f2361f8a0b214dbcfe36", size = 18333, upload-time = "2026-03-30T22:49:39.045Z" }, +] + +[[package]] +name = "google-cloud-audit-log" +version = "0.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "googleapis-common-protos" }, + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/be/9f/3aedb3ce1d58c58ec7dd06b3964836eabfd17a16a95b60c8f609c0afff7f/google_cloud_audit_log-0.5.0.tar.gz", hash = "sha256:3b32d5e77db634c46fbd6c5e01f5bda836f420dfbb21d730501c75e9fab4e4a4", size = 44670, upload-time = "2026-03-30T22:50:42.295Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/40/79fa535b6e3321d5e07b2a9ab4bb63860d3fea12230c765837881348003c/google_cloud_audit_log-0.5.0-py3-none-any.whl", hash = "sha256:3f4632f25bf67446fa9085c52868f3cb42fb1afbab9489ba8978e30991afc79f", size = 44862, upload-time = "2026-03-30T22:47:57.533Z" }, +] + +[[package]] +name = "google-cloud-bigquery" +version = "3.41.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "google-api-core", extra = ["grpc"] }, + { name = "google-auth" }, + { name = "google-cloud-core" }, + { name = "google-resumable-media" }, + { name = "packaging" }, + { name = "python-dateutil" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ce/13/6515c7aab55a4a0cf708ffd309fb9af5bab54c13e32dc22c5acd6497193c/google_cloud_bigquery-3.41.0.tar.gz", hash = "sha256:2217e488b47ed576360c9b2cc07d59d883a54b83167c0ef37f915c26b01a06fe", size = 513434, upload-time = "2026-03-30T22:50:55.347Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/40/33/1d3902efadef9194566d499d61507e1f038454e0b55499d2d7f8ab2a4fee/google_cloud_bigquery-3.41.0-py3-none-any.whl", hash = "sha256:2a5b5a737b401cbd824a6e5eac7554100b878668d908e6548836b5d8aaa4dcaa", size = 262343, upload-time = "2026-03-30T22:48:45.444Z" }, +] + +[[package]] +name = "google-cloud-bigquery-storage" +version = "2.38.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "google-api-core", extra = ["grpc"] }, + { name = "google-auth" }, + { name = "grpcio" }, + { name = "proto-plus" }, + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9c/b9/7f4195887e19d4ee2cc0c13f739d8c10f0802537a61503727d282a458f4e/google_cloud_bigquery_storage-2.38.0.tar.gz", hash = "sha256:bc703ab31c8c7dc9d0a281ff5109ba7461b3a6dc517f6acca1a823124085ab0d", size = 310588, upload-time = "2026-05-07T08:03:54.054Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/d1/382d6b1ec5fe91d79499390712875918bb7709a20726eccfadb8f835d422/google_cloud_bigquery_storage-2.38.0-py3-none-any.whl", hash = "sha256:313e605c51e6c36046cbeccff4a98aa0f728add8f757962bab2266838136d538", size = 306663, upload-time = "2026-05-07T08:02:22.593Z" }, +] + +[[package]] +name = "google-cloud-bigtable" +version = "2.38.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "google-api-core", extra = ["grpc"] }, + { name = "google-auth" }, + { name = "google-cloud-core" }, + { name = "google-crc32c" }, + { name = "grpc-google-iam-v1" }, + { name = "grpcio" }, + { name = "proto-plus" }, + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f7/2c/a62b2108459518914d75b8455dd69bac838d6bf276fe902320f5f16cf9cb/google_cloud_bigtable-2.38.0.tar.gz", hash = "sha256:0ad24f0106c2eb0f38e278b1641052e65882a4da0141d1f9ad78ea691724aaa3", size = 800955, upload-time = "2026-05-07T19:32:53.737Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/46/9d/9c0a81aa9cf6c058b02d3be194d70bcd7e4bd82f631c8110560c3908dbc4/google_cloud_bigtable-2.38.0-py3-none-any.whl", hash = "sha256:9f6a4bdbefb34d0420f41c574d9805d8a63d080d10be5a176205e3b322c122a1", size = 556168, upload-time = "2026-05-07T19:32:51.48Z" }, +] + +[[package]] +name = "google-cloud-core" +version = "2.6.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "google-api-core" }, + { name = "google-auth" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a8/dd/1eef226e470369b26824a505c34482c0b493bc35fe8e0c6b003b5feca21a/google_cloud_core-2.6.0.tar.gz", hash = "sha256:e76149739f90fac1fc6757c09f47eaccb3145b54adbd7759b0f7c4b235f46c83", size = 36001, upload-time = "2026-05-07T08:04:04.124Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/84/4a/98da8930ab109c73d9a5d13782a9ebb81ea8c111f6d534a567b71d23e52b/google_cloud_core-2.6.0-py3-none-any.whl", hash = "sha256:6d63ac8e5eca6d9e4319d0a1e2265fadcd7f1049904378caecfa01cf52dd869e", size = 29390, upload-time = "2026-05-07T08:02:34.672Z" }, +] + +[[package]] +name = "google-cloud-dataplex" +version = "2.19.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "google-api-core", extra = ["grpc"] }, + { name = "google-auth" }, + { name = "grpc-google-iam-v1" }, + { name = "grpcio" }, + { name = "proto-plus" }, + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d9/fb/0ff732c12b394b13c13a094f15f25b0a8752c00aad03c29e1d24d2e60ec5/google_cloud_dataplex-2.19.0.tar.gz", hash = "sha256:81a637f2474bd5391ed102311ac6b90d6c066fae1c29837baa8dd36328db0b05", size = 882730, upload-time = "2026-05-07T08:04:07.278Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/74/67/b880c7c96f961ef945f992e45eaeb727a6a54586eb5e0d28f2677958d413/google_cloud_dataplex-2.19.0-py3-none-any.whl", hash = "sha256:356204387ea954710519946dbca224a16f36fb81bd651ccb92ef2bcdbb12422e", size = 676716, upload-time = "2026-05-07T08:02:39.806Z" }, +] + +[[package]] +name = "google-cloud-discoveryengine" +version = "0.13.12" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "google-api-core", extra = ["grpc"] }, + { name = "google-auth" }, + { name = "proto-plus" }, + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8f/cd/b33bbc4b096d937abee5ebfad3908b2bdc65acd1582191aa33beaa2b70a5/google_cloud_discoveryengine-0.13.12.tar.gz", hash = "sha256:d6b9f8fadd8ad0d2f4438231c5eb7772a317e9f59cafbcbadc19b5d54c609419", size = 3582382, upload-time = "2025-09-22T16:51:14.052Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/93/70/607f6011648f603d35e60a16c34aee68a0b39510e4268d4859f3268684f9/google_cloud_discoveryengine-0.13.12-py3-none-any.whl", hash = "sha256:295f8c6df3fb26b90fb82c2cd6fbcf4b477661addcb19a94eea16463a5c4e041", size = 3337248, upload-time = "2025-09-22T16:50:57.375Z" }, +] + +[[package]] +name = "google-cloud-iam" +version = "2.23.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "google-api-core", extra = ["grpc"] }, + { name = "google-auth" }, + { name = "grpc-google-iam-v1" }, + { name = "grpcio" }, + { name = "proto-plus" }, + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0d/5f/128a1462354e0f8f0b7baff34b5a1a4e5cd7aee100d8db0eb39843b43d1d/google_cloud_iam-2.23.0.tar.gz", hash = "sha256:49246f6221026d381cff4f8d804daf1bb6416153f2504bf5ef54d4af2450b828", size = 561685, upload-time = "2026-05-07T08:04:16.253Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/ee/470f0c337a235b12c6a880df25809b8b11b33986510d66450cb5ef540a83/google_cloud_iam-2.23.0-py3-none-any.whl", hash = "sha256:a123ac45080a5c1735218a6b3db4c6e6ea12a1cdc86feec1c30ad1ede6c91fc6", size = 515952, upload-time = "2026-05-07T08:02:48.144Z" }, +] + +[[package]] +name = "google-cloud-logging" +version = "3.15.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "google-api-core", extra = ["grpc"] }, + { name = "google-auth" }, + { name = "google-cloud-appengine-logging" }, + { name = "google-cloud-audit-log" }, + { name = "google-cloud-core" }, + { name = "grpc-google-iam-v1" }, + { name = "grpcio" }, + { name = "opentelemetry-api" }, + { name = "proto-plus" }, + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/99/06/253e9795a5877f35183a7175977ca47a17255fe0c8487155f48b86c83f3e/google_cloud_logging-3.15.0.tar.gz", hash = "sha256:72168a1e98bbfc27c75f0b8f630a7f5d786065f3f1f7e9e53d2d787a03693a4a", size = 294881, upload-time = "2026-03-26T22:18:36.947Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/86/0c/fc1a0c57f95d21559ed13e381d9024e9ee9d521489707573fd10af856545/google_cloud_logging-3.15.0-py3-none-any.whl", hash = "sha256:7dcc67434c4e7181510c133d5ac8fd4ce60c23fa4158661f67e54bf440c32450", size = 234212, upload-time = "2026-03-26T22:15:16.404Z" }, +] + +[[package]] +name = "google-cloud-monitoring" +version = "2.30.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "google-api-core", extra = ["grpc"] }, + { name = "google-auth" }, + { name = "grpcio" }, + { name = "proto-plus" }, + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/3f/7bc306ebb006114f58fb9143aec91e1b014a11577350d8bbd6bbc38389f9/google_cloud_monitoring-2.30.0.tar.gz", hash = "sha256:a9530aa9aa246c490810dfa7be32d67e8340d19108acc99cbc02d1ed494fba76", size = 407108, upload-time = "2026-03-26T22:17:10.365Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ad/c8/666c21c470b9d6fd62ac9ee74dc265419975228f9b16f8ad72ec22e8d98b/google_cloud_monitoring-2.30.0-py3-none-any.whl", hash = "sha256:2729f3b88a4798b7757b1d9d31b6cb562bb3544e8173765e4e5cd44d8685b1ed", size = 391367, upload-time = "2026-03-26T22:15:04.088Z" }, +] + +[[package]] +name = "google-cloud-pubsub" +version = "2.38.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "google-api-core", extra = ["grpc"] }, + { name = "google-auth" }, + { name = "grpc-google-iam-v1" }, + { name = "grpcio" }, + { name = "grpcio-status" }, + { name = "opentelemetry-api" }, + { name = "opentelemetry-sdk" }, + { name = "proto-plus" }, + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a9/a5/0c7b27493ad6e744d175999e93bc51141ae70b23184d0bdbcb13fc9a4b29/google_cloud_pubsub-2.38.0.tar.gz", hash = "sha256:9212309f8d6cfaefb577bca52492b13464b56e584505408685d63e69346c56cf", size = 402783, upload-time = "2026-05-07T08:04:29.024Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/ea/d000ce0663c8989651f80b02e2ea8bb700d29140ade580f275b4ec4c9687/google_cloud_pubsub-2.38.0-py3-none-any.whl", hash = "sha256:fed2c40cfb77d58f6dced563a8146a8c34319c7dfbbb4d045b6c9c101e043db9", size = 324839, upload-time = "2026-05-07T08:03:04.923Z" }, +] + +[[package]] +name = "google-cloud-resource-manager" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "google-api-core", extra = ["grpc"] }, + { name = "google-auth" }, + { name = "grpc-google-iam-v1" }, + { name = "grpcio" }, + { name = "proto-plus" }, + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b2/1a/13060cabf553d52d151d2afc26b39561e82853380d499dd525a0d422d9f0/google_cloud_resource_manager-1.17.0.tar.gz", hash = "sha256:0f486b62e2c58ff992a3a50fa0f4a96eef7750aa6c971bb373398ccb91828660", size = 464971, upload-time = "2026-03-26T22:17:29.204Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3d/f7/661d7a9023e877a226b5683429c3662f75a29ef45cb1464cf39adb689218/google_cloud_resource_manager-1.17.0-py3-none-any.whl", hash = "sha256:e479baf4b014a57f298e01b8279e3290b032e3476d69c8e5e1427af8f82739a5", size = 404403, upload-time = "2026-03-26T22:15:26.57Z" }, +] + +[[package]] +name = "google-cloud-secret-manager" +version = "2.28.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "google-api-core", extra = ["grpc"] }, + { name = "google-auth" }, + { name = "grpc-google-iam-v1" }, + { name = "grpcio" }, + { name = "proto-plus" }, + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/58/a3/c0d4535d841b65421a043e32b3a4acc66431da8e0f99534204112322ec04/google_cloud_secret_manager-2.28.0.tar.gz", hash = "sha256:5763f42a449c27597fb42d40fa93e2c56b02dbceba0c41da202fb97e810cb278", size = 279947, upload-time = "2026-05-07T08:04:34.515Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ba/ee/b69997ea7dfa1e23b6d6e872022727e16d1286ccce6c8ae526b0fe5f0ca5/google_cloud_secret_manager-2.28.0-py3-none-any.whl", hash = "sha256:15bcb9d7d3442d842ebac94e2d6e428a11d79cf1aeb91a005a0576f257643317", size = 225578, upload-time = "2026-05-07T08:03:11.54Z" }, +] + +[[package]] +name = "google-cloud-spanner" +version = "3.66.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "google-api-core", extra = ["grpc"] }, + { name = "google-cloud-core" }, + { name = "google-cloud-monitoring" }, + { name = "grpc-google-iam-v1" }, + { name = "grpc-interceptor" }, + { name = "mmh3" }, + { name = "opentelemetry-api" }, + { name = "opentelemetry-resourcedetector-gcp" }, + { name = "opentelemetry-sdk" }, + { name = "opentelemetry-semantic-conventions" }, + { name = "proto-plus" }, + { name = "protobuf" }, + { name = "sqlparse" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c3/5d/b628fb39ff6b62ed301ede5107487d48da39826597eee5b735aea2595bcb/google_cloud_spanner-3.66.0.tar.gz", hash = "sha256:a5de352c9cce75ba1b1b2e767816b14c6d147815c7450dbf42aae34773fad3f5", size = 889920, upload-time = "2026-05-07T08:04:35.788Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/61/4e/5111be2281faad007d7f85b1bce6e1ae68a10946e302000470835f81cf72/google_cloud_spanner-3.66.0-py3-none-any.whl", hash = "sha256:a5ec48576022fc064d7217ec5011ce1f159eb8eac669de0acd8a4497c205d872", size = 613881, upload-time = "2026-05-07T08:03:13.118Z" }, +] + +[[package]] +name = "google-cloud-speech" +version = "2.39.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "google-api-core", extra = ["grpc"] }, + { name = "google-auth" }, + { name = "grpcio" }, + { name = "proto-plus" }, + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2b/5f/db75d3d05380ca3cdafcb1ab1575668de17634b6f7a3ba85582ae20c5f7e/google_cloud_speech-2.39.0.tar.gz", hash = "sha256:be712b5bd3294211a1a67f73fb4f5e772694eb5605aaafbc5ba838d4359a3161", size = 406000, upload-time = "2026-05-07T08:04:37.415Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/35/bf/933f43de2575c946961f9e4b03685da1f9e7058e4bf17c94a26b18d95da9/google_cloud_speech-2.39.0-py3-none-any.whl", hash = "sha256:ad2297b50a6aa10da76b65a9500e7990b7fd0cb295b415aae1f86b9e1ffae7d8", size = 345989, upload-time = "2026-05-07T08:03:15.235Z" }, +] + +[[package]] +name = "google-cloud-storage" +version = "3.10.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "google-api-core" }, + { name = "google-auth" }, + { name = "google-cloud-core" }, + { name = "google-crc32c" }, + { name = "google-resumable-media" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4c/47/205eb8e9a1739b5345843e5a425775cbdc472cc38e7eda082ba5b8d02450/google_cloud_storage-3.10.1.tar.gz", hash = "sha256:97db9aa4460727982040edd2bd13ff3d5e2260b5331ad22895802da1fc2a5286", size = 17309950, upload-time = "2026-03-23T09:35:23.409Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ad/ff/ca9ab2417fa913d75aae38bf40bf856bb2749a604b2e0f701b37cfcd23cc/google_cloud_storage-3.10.1-py3-none-any.whl", hash = "sha256:a72f656759b7b99bda700f901adcb3425a828d4a29f911bc26b3ea79c5b1217f", size = 324453, upload-time = "2026-03-23T09:35:21.368Z" }, +] + +[[package]] +name = "google-cloud-trace" +version = "1.19.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "google-api-core", extra = ["grpc"] }, + { name = "google-auth" }, + { name = "grpcio" }, + { name = "proto-plus" }, + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/89/7b/c2a5848c4722373c92b500b65e6308ad89ca0c7c01054e0d948c58c107f2/google_cloud_trace-1.19.0.tar.gz", hash = "sha256:58293c6efcee6c74bb854ff01b008823bef66845c14f15ffa5209d545098a65d", size = 103875, upload-time = "2026-03-26T22:18:18.123Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a4/91/0090acafa7d2caf1bf0d7222d42935e118164a539f9f9a00a814afa63fa1/google_cloud_trace-1.19.0-py3-none-any.whl", hash = "sha256:59604c4c775c40af31b367df6bada0af34518cc35ac8cfedecd43898a120c51d", size = 108454, upload-time = "2026-03-26T22:14:32.631Z" }, +] + +[[package]] +name = "google-crc32c" +version = "1.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/03/41/4b9c02f99e4c5fb477122cd5437403b552873f014616ac1d19ac8221a58d/google_crc32c-1.8.0.tar.gz", hash = "sha256:a428e25fb7691024de47fecfbff7ff957214da51eddded0da0ae0e0f03a2cf79", size = 14192, upload-time = "2025-12-16T00:35:25.142Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5d/ef/21ccfaab3d5078d41efe8612e0ed0bfc9ce22475de074162a91a25f7980d/google_crc32c-1.8.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:014a7e68d623e9a4222d663931febc3033c5c7c9730785727de2a81f87d5bab8", size = 31298, upload-time = "2025-12-16T00:20:32.241Z" }, + { url = "https://files.pythonhosted.org/packages/c5/b8/f8413d3f4b676136e965e764ceedec904fe38ae8de0cdc52a12d8eb1096e/google_crc32c-1.8.0-cp311-cp311-macosx_12_0_x86_64.whl", hash = "sha256:86cfc00fe45a0ac7359e5214a1704e51a99e757d0272554874f419f79838c5f7", size = 30872, upload-time = "2025-12-16T00:33:58.785Z" }, + { url = "https://files.pythonhosted.org/packages/f6/fd/33aa4ec62b290477181c55bb1c9302c9698c58c0ce9a6ab4874abc8b0d60/google_crc32c-1.8.0-cp311-cp311-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:19b40d637a54cb71e0829179f6cb41835f0fbd9e8eb60552152a8b52c36cbe15", size = 33243, upload-time = "2025-12-16T00:40:21.46Z" }, + { url = "https://files.pythonhosted.org/packages/71/03/4820b3bd99c9653d1a5210cb32f9ba4da9681619b4d35b6a052432df4773/google_crc32c-1.8.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:17446feb05abddc187e5441a45971b8394ea4c1b6efd88ab0af393fd9e0a156a", size = 33608, upload-time = "2025-12-16T00:40:22.204Z" }, + { url = "https://files.pythonhosted.org/packages/7c/43/acf61476a11437bf9733fb2f70599b1ced11ec7ed9ea760fdd9a77d0c619/google_crc32c-1.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:71734788a88f551fbd6a97be9668a0020698e07b2bf5b3aa26a36c10cdfb27b2", size = 34439, upload-time = "2025-12-16T00:35:20.458Z" }, + { url = "https://files.pythonhosted.org/packages/e9/5f/7307325b1198b59324c0fa9807cafb551afb65e831699f2ce211ad5c8240/google_crc32c-1.8.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:4b8286b659c1335172e39563ab0a768b8015e88e08329fa5321f774275fc3113", size = 31300, upload-time = "2025-12-16T00:21:56.723Z" }, + { url = "https://files.pythonhosted.org/packages/21/8e/58c0d5d86e2220e6a37befe7e6a94dd2f6006044b1a33edf1ff6d9f7e319/google_crc32c-1.8.0-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:2a3dc3318507de089c5384cc74d54318401410f82aa65b2d9cdde9d297aca7cb", size = 30867, upload-time = "2025-12-16T00:38:31.302Z" }, + { url = "https://files.pythonhosted.org/packages/ce/a9/a780cc66f86335a6019f557a8aaca8fbb970728f0efd2430d15ff1beae0e/google_crc32c-1.8.0-cp312-cp312-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:14f87e04d613dfa218d6135e81b78272c3b904e2a7053b841481b38a7d901411", size = 33364, upload-time = "2025-12-16T00:40:22.96Z" }, + { url = "https://files.pythonhosted.org/packages/21/3f/3457ea803db0198c9aaca2dd373750972ce28a26f00544b6b85088811939/google_crc32c-1.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cb5c869c2923d56cb0c8e6bcdd73c009c36ae39b652dbe46a05eb4ef0ad01454", size = 33740, upload-time = "2025-12-16T00:40:23.96Z" }, + { url = "https://files.pythonhosted.org/packages/df/c0/87c2073e0c72515bb8733d4eef7b21548e8d189f094b5dad20b0ecaf64f6/google_crc32c-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:3cc0c8912038065eafa603b238abf252e204accab2a704c63b9e14837a854962", size = 34437, upload-time = "2025-12-16T00:35:21.395Z" }, + { url = "https://files.pythonhosted.org/packages/d1/db/000f15b41724589b0e7bc24bc7a8967898d8d3bc8caf64c513d91ef1f6c0/google_crc32c-1.8.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:3ebb04528e83b2634857f43f9bb8ef5b2bbe7f10f140daeb01b58f972d04736b", size = 31297, upload-time = "2025-12-16T00:23:20.709Z" }, + { url = "https://files.pythonhosted.org/packages/d7/0d/8ebed0c39c53a7e838e2a486da8abb0e52de135f1b376ae2f0b160eb4c1a/google_crc32c-1.8.0-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:450dc98429d3e33ed2926fc99ee81001928d63460f8538f21a5d6060912a8e27", size = 30867, upload-time = "2025-12-16T00:43:14.628Z" }, + { url = "https://files.pythonhosted.org/packages/ce/42/b468aec74a0354b34c8cbf748db20d6e350a68a2b0912e128cabee49806c/google_crc32c-1.8.0-cp313-cp313-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:3b9776774b24ba76831609ffbabce8cdf6fa2bd5e9df37b594221c7e333a81fa", size = 33344, upload-time = "2025-12-16T00:40:24.742Z" }, + { url = "https://files.pythonhosted.org/packages/1c/e8/b33784d6fc77fb5062a8a7854e43e1e618b87d5ddf610a88025e4de6226e/google_crc32c-1.8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:89c17d53d75562edfff86679244830599ee0a48efc216200691de8b02ab6b2b8", size = 33694, upload-time = "2025-12-16T00:40:25.505Z" }, + { url = "https://files.pythonhosted.org/packages/92/b1/d3cbd4d988afb3d8e4db94ca953df429ed6db7282ed0e700d25e6c7bfc8d/google_crc32c-1.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:57a50a9035b75643996fbf224d6661e386c7162d1dfdab9bc4ca790947d1007f", size = 34435, upload-time = "2025-12-16T00:35:22.107Z" }, + { url = "https://files.pythonhosted.org/packages/52/c5/c171e4d8c44fec1422d801a6d2e5d7ddabd733eeda505c79730ee9607f07/google_crc32c-1.8.0-pp311-pypy311_pp73-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:87fa445064e7db928226b2e6f0d5304ab4cd0339e664a4e9a25029f384d9bb93", size = 28615, upload-time = "2025-12-16T00:40:29.298Z" }, + { url = "https://files.pythonhosted.org/packages/9c/97/7d75fe37a7a6ed171a2cf17117177e7aab7e6e0d115858741b41e9dd4254/google_crc32c-1.8.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f639065ea2042d5c034bf258a9f085eaa7af0cd250667c0635a3118e8f92c69c", size = 28800, upload-time = "2025-12-16T00:40:30.322Z" }, +] + +[[package]] +name = "google-genai" +version = "1.75.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "distro" }, + { name = "google-auth", extra = ["requests"] }, + { name = "httpx" }, + { name = "pydantic" }, + { name = "requests" }, + { name = "sniffio" }, + { name = "tenacity" }, + { name = "typing-extensions" }, + { name = "websockets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9d/59/3ed61240ef20b3ae6ed54e82c6f8b6d1f194947bc6679679dd6cdb037594/google_genai-1.75.0.tar.gz", hash = "sha256:56bac3991b311c93f980c0a2abcd287b672146905df1fbd71c92ed633d5a07cf", size = 539039, upload-time = "2026-05-04T22:48:54.857Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2d/b6/552d40e96da22921eb1fead7c14b00b5b5473a20e45959488660fab35ee2/google_genai-1.75.0-py3-none-any.whl", hash = "sha256:8dc4c096e7d6288c3087f6893f582fe52468932464781edb8193bd92b9fefb2c", size = 793726, upload-time = "2026-05-04T22:48:53.033Z" }, +] + +[[package]] +name = "google-resumable-media" +version = "2.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "google-crc32c" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/00/4b/0b235beccc310d0a48adbc7246b719d173cca6c88c572dfa4b090e39143c/google_resumable_media-2.9.0.tar.gz", hash = "sha256:f7cfb224846a9dd444d125115dfbe8ef02a2b893e78f087762fe716a255a734b", size = 2164534, upload-time = "2026-05-07T08:04:44.236Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/07/73/3518e63deb1667c5409a4579e28daf5e84479a87a72c547e0487f7883dcd/google_resumable_media-2.9.0-py3-none-any.whl", hash = "sha256:c8901e88e389af8bed64d9696c74d8bad961865eb2236e13e0bfca9bb0a65ca3", size = 81507, upload-time = "2026-05-07T08:03:23.809Z" }, +] + +[[package]] +name = "googleapis-common-protos" +version = "1.75.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b5/c8/f439cffde755cffa462bfbb156278fa6f9d09119719af9814b858fd4f81f/googleapis_common_protos-1.75.0.tar.gz", hash = "sha256:53a062ff3c32552fbd62c11fe23768b78e4ddf0494d5e5fd97d3f4689c75fbbd", size = 151035, upload-time = "2026-05-07T08:04:49.423Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/c8/e2645aa8ed02fd4c7a2f59d68783b65b1f3cbdfe39a6308e156509d1fee8/googleapis_common_protos-1.75.0-py3-none-any.whl", hash = "sha256:961ed60399c457ceb0ee8f285a84c870aabc9c6a832b9d37bb281b5bebde43ed", size = 300631, upload-time = "2026-05-07T08:03:30.345Z" }, +] + +[package.optional-dependencies] +grpc = [ + { name = "grpcio" }, +] + +[[package]] +name = "graphviz" +version = "0.21" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f8/b3/3ac91e9be6b761a4b30d66ff165e54439dcd48b83f4e20d644867215f6ca/graphviz-0.21.tar.gz", hash = "sha256:20743e7183be82aaaa8ad6c93f8893c923bd6658a04c32ee115edb3c8a835f78", size = 200434, upload-time = "2025-06-15T09:35:05.824Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/91/4c/e0ce1ef95d4000ebc1c11801f9b944fa5910ecc15b5e351865763d8657f8/graphviz-0.21-py3-none-any.whl", hash = "sha256:54f33de9f4f911d7e84e4191749cac8cc5653f815b06738c54db9a15ab8b1e42", size = 47300, upload-time = "2025-06-15T09:35:04.433Z" }, +] + +[[package]] +name = "greenlet" +version = "3.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6d/6e/802acd792aebb2256fbbee8cacf2727faaeb6f240ac11008f09eae4414bc/greenlet-3.5.1.tar.gz", hash = "sha256:5a56aeb7d5d9cc4b3a735efb5095bd4b4f6f0e4f93e5ca876d0e2315137b7829", size = 197356, upload-time = "2026-05-20T15:05:03.917Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/42/3c/ff890b466eaba2b0f5e6bdfff025f8c75f41b8ffdc3dbc3d24ad261e764a/greenlet-3.5.1-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:73f78f9b9f0a5c06e5c946ba1e8e36f5114923b6be109ee618c54f079c3ea14f", size = 284764, upload-time = "2026-05-20T13:09:10.204Z" }, + { url = "https://files.pythonhosted.org/packages/81/0e/5e5457be3d256918f6a4756f073548a3f0190836e2cc94aa6d0d617a940b/greenlet-3.5.1-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a0cbed8bb44e23c5b199f888f4e4ce096b45ad9f25ff74a7ad0213875e936bb2", size = 603479, upload-time = "2026-05-20T14:00:04.757Z" }, + { url = "https://files.pythonhosted.org/packages/6d/e1/f89a21d58d308298e6f275f13a1b472ed96c680b601a371b08be6a725989/greenlet-3.5.1-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a203a8bd0acb0701653d3bbb26e404854a68674139ed5cbb778830f42b09bb33", size = 615495, upload-time = "2026-05-20T14:05:40.87Z" }, + { url = "https://files.pythonhosted.org/packages/2c/f2/8fd452fd81adb9ec79c8275c1375702ab0fd6bee4952da12eaa09b9508d8/greenlet-3.5.1-cp311-cp311-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6ebeb75c81211f5c702576cf81f315e77e23cfdb2c7c6fcb9dd143e6de35c360", size = 623515, upload-time = "2026-05-20T14:09:07.853Z" }, + { url = "https://files.pythonhosted.org/packages/75/de/af6cef182862d2ccd6975440d21c9058a77c3f9b469abf94e322dfd2e0e3/greenlet-3.5.1-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8a271fcd66c74615cda6a964fda3f304267a12e50a084472218a39bb0376f563", size = 614754, upload-time = "2026-05-20T13:14:24.947Z" }, + { url = "https://files.pythonhosted.org/packages/ec/bc/c318aa9f3ffc77320fddcee3d892be957b42e2ff947198d9450b004f3a38/greenlet-3.5.1-cp311-cp311-manylinux_2_39_riscv64.whl", hash = "sha256:017a544f0385d441e88714160d089d6900ef46c9eff9d99b6715a5ef2d127747", size = 418439, upload-time = "2026-05-20T14:01:38.446Z" }, + { url = "https://files.pythonhosted.org/packages/1a/c6/50e520283a9f19388a7326b05f9e8637e566003475eacaadad04f558c68d/greenlet-3.5.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ded7b068c7c31c1a8657d4fd42d886b3e051ae29f88b80c5ff9d502257b0f071", size = 1574097, upload-time = "2026-05-20T14:02:24.003Z" }, + { url = "https://files.pythonhosted.org/packages/21/1c/13abd1f4860d987fa5e1170a01930d6e6cd40d328de487a3c9fdaff0ffd0/greenlet-3.5.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d0932b81d72f552ded9d810d00021b64d89f2195a91ce115b893f943b7a4ab3c", size = 1641058, upload-time = "2026-05-20T13:14:31.83Z" }, + { url = "https://files.pythonhosted.org/packages/f5/56/5f332b7705545eac2dc01b4e9254d24a793f2656d55d5cc6b94ee59d22ae/greenlet-3.5.1-cp311-cp311-win_amd64.whl", hash = "sha256:88e300d136eac057b2397aa1cfd7328b4c87c7eb66a09c7bc6a1292234db474e", size = 238089, upload-time = "2026-05-20T13:14:03.229Z" }, + { url = "https://files.pythonhosted.org/packages/d9/a9/a3c2fa886c5b94863fb0e61b3bc14610b7aa94cf4f17f8741b11708305fc/greenlet-3.5.1-cp311-cp311-win_arm64.whl", hash = "sha256:cc6ab7e555c8a112ad3a76e368e86e12a2754bcae1652a5602e133ec7b635523", size = 234989, upload-time = "2026-05-20T13:08:27.715Z" }, + { url = "https://files.pythonhosted.org/packages/c4/37/4549f149c9797c21b32c2683c33522af22522099de128b2406672526d005/greenlet-3.5.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:fa4f98af3a528f0c3fd592a26df7f376f93329c8f4d987f6bb979057af8bf5e2", size = 286220, upload-time = "2026-05-20T13:07:28.463Z" }, + { url = "https://files.pythonhosted.org/packages/38/ff/a4f436709716965eaab9f36ea7b906c8a927fbe32fb1372a2071d964f6b1/greenlet-3.5.1-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ffea73584b216150eab159b6d12348fb253e68757974de1e2c40d8a318ac89ed", size = 601585, upload-time = "2026-05-20T14:00:06.141Z" }, + { url = "https://files.pythonhosted.org/packages/65/ad/54bc3fcee3ad368a61b19b67d88117f7a8c29727bf71fffdeda81fbd946e/greenlet-3.5.1-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1072b4f9edcc1e192d9283a66a3e68d6b84c561de33a83d7858beb9ba1effe10", size = 614215, upload-time = "2026-05-20T14:05:42.675Z" }, + { url = "https://files.pythonhosted.org/packages/7c/6c/de5b1b388cd2d9fbdfeab324863daba37d54e6e233ddbefd70b385a8c591/greenlet-3.5.1-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:89101bfd5011e069be974903cb3a4e4523845e4ece2d62dcd8d358933c0ef249", size = 620094, upload-time = "2026-05-20T14:09:09.18Z" }, + { url = "https://files.pythonhosted.org/packages/40/69/b91cda0647df839483201545913514c2827ebea5e5ccdf931842763bc127/greenlet-3.5.1-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:add5217d68b31130f0beca584d7fef4878327d2e31642b66618a14eef312b63b", size = 611358, upload-time = "2026-05-20T13:14:26.37Z" }, + { url = "https://files.pythonhosted.org/packages/4a/43/1204baffab8a6476464795a7ccf394a3248d4f22c9f87173a15b36b6d971/greenlet-3.5.1-cp312-cp312-manylinux_2_39_riscv64.whl", hash = "sha256:e6cd99ea59dd5d89f0c956606571d79bfe6f68c9eb7f4a4083a41a7f1587edee", size = 422782, upload-time = "2026-05-20T14:01:39.597Z" }, + { url = "https://files.pythonhosted.org/packages/59/90/3cf77e080350cd02fa307bb2abf05df48f4482c240275bbd2c203ba8bb1c/greenlet-3.5.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a5ea42a752d47a145eae922b605cd1634665ac3d5ec1e72402d5048e8d60d207", size = 1570475, upload-time = "2026-05-20T14:02:25.29Z" }, + { url = "https://files.pythonhosted.org/packages/65/2c/18cece62045e74598c3c393f70dce4a63f56222015ba29a5d4eeb04f764c/greenlet-3.5.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c5551170cf4f5ff5623e9af81323751979fee2c731e2287b61f73cd27257b823", size = 1635625, upload-time = "2026-05-20T13:14:34.027Z" }, + { url = "https://files.pythonhosted.org/packages/30/f5/310d104ddf41eb5a70f4c268d22508dfb0c3c8e86fec152be34d0d2ed819/greenlet-3.5.1-cp312-cp312-win_amd64.whl", hash = "sha256:3c8bb982ad117d29478ef8f5533e97df21f1e2befd17a299257b0c96d1371c0b", size = 238791, upload-time = "2026-05-20T13:10:39.018Z" }, + { url = "https://files.pythonhosted.org/packages/62/90/ceca11f504cd23a8047a3dea31919adc48df9b626dd0c13f0d858734fdfd/greenlet-3.5.1-cp312-cp312-win_arm64.whl", hash = "sha256:80eb4b04dadc4e67df3fae179a32c4706a3f495bc7f22fc8a81115d5f5512188", size = 235580, upload-time = "2026-05-20T13:08:45.056Z" }, + { url = "https://files.pythonhosted.org/packages/27/69/7f7e5372d998b81001899b1c0823c957aa413ba0f2662e65821611cc31e4/greenlet-3.5.1-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:51518ff74664078fc51bffcc6fc529b0df5ae58da192691cee765d45ce944a2b", size = 285060, upload-time = "2026-05-20T13:08:51.899Z" }, + { url = "https://files.pythonhosted.org/packages/b1/bf/387f9b6b865fd2ae0d0be09e0004827295a01b71be76ed350dd1e28a91a4/greenlet-3.5.1-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ffdb3c0bb002c99cd8f298957e046c3dbf6006b5b7cdf11a4e19194624a0a0a", size = 604370, upload-time = "2026-05-20T14:00:07.492Z" }, + { url = "https://files.pythonhosted.org/packages/32/f5/169ce3d4e4c67291bd18f8cbe0299c9f3e45102c7f1fb3c14780c93e4532/greenlet-3.5.1-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7715a5a2c3378ba602c3a440558261e13a820bb53a82693aacd7b7f6d964e283", size = 616987, upload-time = "2026-05-20T14:05:44.237Z" }, + { url = "https://files.pythonhosted.org/packages/19/ba/c24110c55dffa55aa6e1d98b45310da33801aeba7686ff0190fe5d46fd32/greenlet-3.5.1-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d40a890035c0058cadbdc4af7569800fd28a0e527a0fdbb7b5f9418f176846ce", size = 622911, upload-time = "2026-05-20T14:09:10.598Z" }, + { url = "https://files.pythonhosted.org/packages/ee/e5/7f2e41d5273be07e77560d61ea4e56485b4d6c316d2a84518c62d1364061/greenlet-3.5.1-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dc71ff466927a201b08305acac451ebe1aedfcea002f62f1f2f2ac2ac1e6a135", size = 613911, upload-time = "2026-05-20T13:14:27.539Z" }, + { url = "https://files.pythonhosted.org/packages/ec/7b/d20db2e8a5ad6c038702f3179b136f93f0a3d1a21a0c0777f3e470cdf4b2/greenlet-3.5.1-cp313-cp313-manylinux_2_39_riscv64.whl", hash = "sha256:67821bb03e4e98664490edb787ff6af501194c29bbee0f5c1dfdcf1dc3d9d436", size = 425228, upload-time = "2026-05-20T14:01:40.837Z" }, + { url = "https://files.pythonhosted.org/packages/c5/a4/fbdc67579b73615a1f91615e814303cc71e06128f7baaba87be79b8fb90c/greenlet-3.5.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cd443683db272ebaaca03af98c0b063ab30db70ea8a31a1559f35e3f7b744ccd", size = 1570689, upload-time = "2026-05-20T14:02:27.225Z" }, + { url = "https://files.pythonhosted.org/packages/e6/b4/77abbe35078be39718a46cd49caf16bceb35662f97a34101dca28aa98e47/greenlet-3.5.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:089fff7a6ce8d9316d1f65ebc00273a56be258c1725b32b94de90a3a979557e1", size = 1635602, upload-time = "2026-05-20T13:14:36.344Z" }, + { url = "https://files.pythonhosted.org/packages/37/f7/129f27ca700845b8ee8ca88ce7f43435a1239c2eddb7677fc938822762cf/greenlet-3.5.1-cp313-cp313-win_amd64.whl", hash = "sha256:110a1ca7b49b014b097f6078272c3f4ed31af45b254de5228b79adba879f6af9", size = 238683, upload-time = "2026-05-20T13:11:50.57Z" }, + { url = "https://files.pythonhosted.org/packages/6d/5c/a485a36e87df8d8fd0632ee01511244f5156a20ed3746cc6599340326395/greenlet-3.5.1-cp313-cp313-win_arm64.whl", hash = "sha256:f16ba1efc0715b680a18b8123d90dad887c6112ae3555b4b5c32c149540c6b4e", size = 235499, upload-time = "2026-05-20T13:12:42.028Z" }, +] + +[[package]] +name = "grpc-google-iam-v1" +version = "0.14.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "googleapis-common-protos", extra = ["grpc"] }, + { name = "grpcio" }, + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/44/4f/d098419ad0bfc06c9ce440575f05aa22d8973b6c276e86ac7890093d3c37/grpc_google_iam_v1-0.14.4.tar.gz", hash = "sha256:392b3796947ed6334e61171d9ab06bf7eb357f554e5fc7556ad7aab6d0e17038", size = 23706, upload-time = "2026-04-01T01:57:49.813Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/89/22/c2dd50c09bf679bd38173656cd4402d2511e563b33bc88f90009cf50613c/grpc_google_iam_v1-0.14.4-py3-none-any.whl", hash = "sha256:412facc320fcbd94034b4df3d557662051d4d8adfa86e0ddb4dca70a3f739964", size = 32675, upload-time = "2026-04-01T01:57:47.69Z" }, +] + +[[package]] +name = "grpc-interceptor" +version = "0.15.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "grpcio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9f/28/57449d5567adf4c1d3e216aaca545913fbc21a915f2da6790d6734aac76e/grpc-interceptor-0.15.4.tar.gz", hash = "sha256:1f45c0bcb58b6f332f37c637632247c9b02bc6af0fdceb7ba7ce8d2ebbfb0926", size = 19322, upload-time = "2023-11-16T02:05:42.459Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/15/ac/8d53f230a7443401ce81791ec50a3b0e54924bf615ad287654fa4a2f5cdc/grpc_interceptor-0.15.4-py3-none-any.whl", hash = "sha256:0035f33228693ed3767ee49d937bac424318db173fef4d2d0170b3215f254d9d", size = 20848, upload-time = "2023-11-16T02:05:40.913Z" }, +] + +[[package]] +name = "grpcio" +version = "1.80.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b7/48/af6173dbca4454f4637a4678b67f52ca7e0c1ed7d5894d89d434fecede05/grpcio-1.80.0.tar.gz", hash = "sha256:29aca15edd0688c22ba01d7cc01cb000d72b2033f4a3c72a81a19b56fd143257", size = 12978905, upload-time = "2026-03-30T08:49:10.502Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5d/db/1d56e5f5823257b291962d6c0ce106146c6447f405b60b234c4f222a7cde/grpcio-1.80.0-cp311-cp311-linux_armv7l.whl", hash = "sha256:dfab85db094068ff42e2a3563f60ab3dddcc9d6488a35abf0132daec13209c8a", size = 6055009, upload-time = "2026-03-30T08:46:46.265Z" }, + { url = "https://files.pythonhosted.org/packages/6e/18/c83f3cad64c5ca63bca7e91e5e46b0d026afc5af9d0a9972472ceba294b3/grpcio-1.80.0-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:5c07e82e822e1161354e32da2662f741a4944ea955f9f580ec8fb409dd6f6060", size = 12035295, upload-time = "2026-03-30T08:46:49.099Z" }, + { url = "https://files.pythonhosted.org/packages/0f/8e/e14966b435be2dda99fbe89db9525ea436edc79780431a1c2875a3582644/grpcio-1.80.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ba0915d51fd4ced2db5ff719f84e270afe0e2d4c45a7bdb1e8d036e4502928c2", size = 6610297, upload-time = "2026-03-30T08:46:52.123Z" }, + { url = "https://files.pythonhosted.org/packages/cc/26/d5eb38f42ce0e3fdc8174ea4d52036ef8d58cc4426cb800f2610f625dd75/grpcio-1.80.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:3cb8130ba457d2aa09fa6b7c3ed6b6e4e6a2685fce63cb803d479576c4d80e21", size = 7300208, upload-time = "2026-03-30T08:46:54.859Z" }, + { url = "https://files.pythonhosted.org/packages/25/51/bd267c989f85a17a5b3eea65a6feb4ff672af41ca614e5a0279cc0ea381c/grpcio-1.80.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:09e5e478b3d14afd23f12e49e8b44c8684ac3c5f08561c43a5b9691c54d136ab", size = 6813442, upload-time = "2026-03-30T08:46:57.056Z" }, + { url = "https://files.pythonhosted.org/packages/9e/d9/d80eef735b19e9169e30164bbf889b46f9df9127598a83d174eb13a48b26/grpcio-1.80.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:00168469238b022500e486c1c33916acf2f2a9b2c022202cf8a1885d2e3073c1", size = 7414743, upload-time = "2026-03-30T08:46:59.682Z" }, + { url = "https://files.pythonhosted.org/packages/de/f2/567f5bd5054398ed6b0509b9a30900376dcf2786bd936812098808b49d8d/grpcio-1.80.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8502122a3cc1714038e39a0b071acb1207ca7844208d5ea0d091317555ee7106", size = 8426046, upload-time = "2026-03-30T08:47:02.474Z" }, + { url = "https://files.pythonhosted.org/packages/62/29/73ef0141b4732ff5eacd68430ff2512a65c004696997f70476a83e548e7e/grpcio-1.80.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ce1794f4ea6cc3ca29463f42d665c32ba1b964b48958a66497917fe9069f26e6", size = 7851641, upload-time = "2026-03-30T08:47:05.462Z" }, + { url = "https://files.pythonhosted.org/packages/46/69/abbfa360eb229a8623bab5f5a4f8105e445bd38ce81a89514ba55d281ad0/grpcio-1.80.0-cp311-cp311-win32.whl", hash = "sha256:51b4a7189b0bef2aa30adce3c78f09c83526cf3dddb24c6a96555e3b97340440", size = 4154368, upload-time = "2026-03-30T08:47:08.027Z" }, + { url = "https://files.pythonhosted.org/packages/6f/d4/ae92206d01183b08613e846076115f5ac5991bae358d2a749fa864da5699/grpcio-1.80.0-cp311-cp311-win_amd64.whl", hash = "sha256:02e64bb0bb2da14d947a49e6f120a75e947250aebe65f9629b62bb1f5c14e6e9", size = 4894235, upload-time = "2026-03-30T08:47:10.839Z" }, + { url = "https://files.pythonhosted.org/packages/5c/e8/a2b749265eb3415abc94f2e619bbd9e9707bebdda787e61c593004ec927a/grpcio-1.80.0-cp312-cp312-linux_armv7l.whl", hash = "sha256:c624cc9f1008361014378c9d776de7182b11fe8b2e5a81bc69f23a295f2a1ad0", size = 6015616, upload-time = "2026-03-30T08:47:13.428Z" }, + { url = "https://files.pythonhosted.org/packages/3e/97/b1282161a15d699d1e90c360df18d19165a045ce1c343c7f313f5e8a0b77/grpcio-1.80.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:f49eddcac43c3bf350c0385366a58f36bed8cc2c0ec35ef7b74b49e56552c0c2", size = 12014204, upload-time = "2026-03-30T08:47:15.873Z" }, + { url = "https://files.pythonhosted.org/packages/6e/5e/d319c6e997b50c155ac5a8cb12f5173d5b42677510e886d250d50264949d/grpcio-1.80.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d334591df610ab94714048e0d5b4f3dd5ad1bee74dfec11eee344220077a79de", size = 6563866, upload-time = "2026-03-30T08:47:18.588Z" }, + { url = "https://files.pythonhosted.org/packages/ae/f6/fdd975a2cb4d78eb67769a7b3b3830970bfa2e919f1decf724ae4445f42c/grpcio-1.80.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:0cb517eb1d0d0aaf1d87af7cc5b801d686557c1d88b2619f5e31fab3c2315921", size = 7273060, upload-time = "2026-03-30T08:47:21.113Z" }, + { url = "https://files.pythonhosted.org/packages/db/f0/a3deb5feba60d9538a962913e37bd2e69a195f1c3376a3dd44fe0427e996/grpcio-1.80.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4e78c4ac0d97dc2e569b2f4bcbbb447491167cb358d1a389fc4af71ab6f70411", size = 6782121, upload-time = "2026-03-30T08:47:23.827Z" }, + { url = "https://files.pythonhosted.org/packages/ca/84/36c6dcfddc093e108141f757c407902a05085e0c328007cb090d56646cdf/grpcio-1.80.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2ed770b4c06984f3b47eb0517b1c69ad0b84ef3f40128f51448433be904634cd", size = 7383811, upload-time = "2026-03-30T08:47:26.517Z" }, + { url = "https://files.pythonhosted.org/packages/7c/ef/f3a77e3dc5b471a0ec86c564c98d6adfa3510d38f8ee99010410858d591e/grpcio-1.80.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:256507e2f524092f1473071a05e65a5b10d84b82e3ff24c5b571513cfaa61e2f", size = 8393860, upload-time = "2026-03-30T08:47:29.439Z" }, + { url = "https://files.pythonhosted.org/packages/9b/8d/9d4d27ed7f33d109c50d6b5ce578a9914aa68edab75d65869a17e630a8d1/grpcio-1.80.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:9a6284a5d907c37db53350645567c522be314bac859a64a7a5ca63b77bb7958f", size = 7830132, upload-time = "2026-03-30T08:47:33.254Z" }, + { url = "https://files.pythonhosted.org/packages/14/e4/9990b41c6d7a44e1e9dee8ac11d7a9802ba1378b40d77468a7761d1ad288/grpcio-1.80.0-cp312-cp312-win32.whl", hash = "sha256:c71309cfce2f22be26aa4a847357c502db6c621f1a49825ae98aa0907595b193", size = 4140904, upload-time = "2026-03-30T08:47:35.319Z" }, + { url = "https://files.pythonhosted.org/packages/2f/2c/296f6138caca1f4b92a31ace4ae1b87dab692fc16a7a3417af3bb3c805bf/grpcio-1.80.0-cp312-cp312-win_amd64.whl", hash = "sha256:9fe648599c0e37594c4809d81a9e77bd138cc82eb8baa71b6a86af65426723ff", size = 4880944, upload-time = "2026-03-30T08:47:37.831Z" }, + { url = "https://files.pythonhosted.org/packages/2f/3a/7c3c25789e3f069e581dc342e03613c5b1cb012c4e8c7d9d5cf960a75856/grpcio-1.80.0-cp313-cp313-linux_armv7l.whl", hash = "sha256:e9e408fc016dffd20661f0126c53d8a31c2821b5c13c5d67a0f5ed5de93319ad", size = 6017243, upload-time = "2026-03-30T08:47:40.075Z" }, + { url = "https://files.pythonhosted.org/packages/04/19/21a9806eb8240e174fd1ab0cd5b9aa948bb0e05c2f2f55f9d5d7405e6d08/grpcio-1.80.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:92d787312e613754d4d8b9ca6d3297e69994a7912a32fa38c4c4e01c272974b0", size = 12010840, upload-time = "2026-03-30T08:47:43.11Z" }, + { url = "https://files.pythonhosted.org/packages/18/3a/23347d35f76f639e807fb7a36fad3068aed100996849a33809591f26eca6/grpcio-1.80.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8ac393b58aa16991a2f1144ec578084d544038c12242da3a215966b512904d0f", size = 6567644, upload-time = "2026-03-30T08:47:46.806Z" }, + { url = "https://files.pythonhosted.org/packages/ff/40/96e07ecb604a6a67ae6ab151e3e35b132875d98bc68ec65f3e5ab3e781d7/grpcio-1.80.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:68e5851ac4b9afe07e7f84483803ad167852570d65326b34d54ca560bfa53fb6", size = 7277830, upload-time = "2026-03-30T08:47:49.643Z" }, + { url = "https://files.pythonhosted.org/packages/9b/e2/da1506ecea1f34a5e365964644b35edef53803052b763ca214ba3870c856/grpcio-1.80.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:873ff5d17d68992ef6605330127425d2fc4e77e612fa3c3e0ed4e668685e3140", size = 6783216, upload-time = "2026-03-30T08:47:52.817Z" }, + { url = "https://files.pythonhosted.org/packages/44/83/3b20ff58d0c3b7f6caaa3af9a4174d4023701df40a3f39f7f1c8e7c48f9d/grpcio-1.80.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2bea16af2750fd0a899bf1abd9022244418b55d1f37da2202249ba4ba673838d", size = 7385866, upload-time = "2026-03-30T08:47:55.687Z" }, + { url = "https://files.pythonhosted.org/packages/47/45/55c507599c5520416de5eefecc927d6a0d7af55e91cfffb2e410607e5744/grpcio-1.80.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ba0db34f7e1d803a878284cd70e4c63cb6ae2510ba51937bf8f45ba997cefcf7", size = 8391602, upload-time = "2026-03-30T08:47:58.303Z" }, + { url = "https://files.pythonhosted.org/packages/10/bb/dd06f4c24c01db9cf11341b547d0a016b2c90ed7dbbb086a5710df7dd1d7/grpcio-1.80.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8eb613f02d34721f1acf3626dfdb3545bd3c8505b0e52bf8b5710a28d02e8aa7", size = 7826752, upload-time = "2026-03-30T08:48:01.311Z" }, + { url = "https://files.pythonhosted.org/packages/f9/1e/9d67992ba23371fd63d4527096eb8c6b76d74d52b500df992a3343fd7251/grpcio-1.80.0-cp313-cp313-win32.whl", hash = "sha256:93b6f823810720912fd131f561f91f5fed0fda372b6b7028a2681b8194d5d294", size = 4142310, upload-time = "2026-03-30T08:48:04.594Z" }, + { url = "https://files.pythonhosted.org/packages/cf/e6/283326a27da9e2c3038bc93eeea36fb118ce0b2d03922a9cda6688f53c5b/grpcio-1.80.0-cp313-cp313-win_amd64.whl", hash = "sha256:e172cf795a3ba5246d3529e4d34c53db70e888fa582a8ffebd2e6e48bc0cba50", size = 4882833, upload-time = "2026-03-30T08:48:07.363Z" }, +] + +[[package]] +name = "grpcio-status" +version = "1.80.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "googleapis-common-protos" }, + { name = "grpcio" }, + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/ed/105f619bdd00cb47a49aa2feea6232ea2bbb04199d52a22cc6a7d603b5cb/grpcio_status-1.80.0.tar.gz", hash = "sha256:df73802a4c89a3ea88aa2aff971e886fccce162bc2e6511408b3d67a144381cd", size = 13901, upload-time = "2026-03-30T08:54:34.784Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/80/58cd2dfc19a07d022abe44bde7c365627f6c7cb6f692ada6c65ca437d09a/grpcio_status-1.80.0-py3-none-any.whl", hash = "sha256:4b56990363af50dbf2c2ebb80f1967185c07d87aa25aa2bea45ddb75fc181dbe", size = 14638, upload-time = "2026-03-30T08:54:01.569Z" }, +] + +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + +[[package]] +name = "hf-xet" +version = "1.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/74/d8/5c06fc76461418326a7decf8367480c35be11a41fd938633929c60a9ec6b/hf_xet-1.5.0.tar.gz", hash = "sha256:e0fb0a34d9f406eed88233e829a67ec016bec5af19e480eac65a233ea289a948", size = 837196, upload-time = "2026-05-06T06:18:15.583Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/68/9b/6912c99070915a4f28119e3c5b52a9abd1eec0ad5cb293b8c967a0c6f5a2/hf_xet-1.5.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:7d70fe2ce97b9db73b9c9b9c81fe3693640aec83416a966c446afea54acfae3c", size = 4023383, upload-time = "2026-05-06T06:17:53.947Z" }, + { url = "https://files.pythonhosted.org/packages/0f/6d/9563cfde59b5d8128a9c7ec972a087f4c782e4f7bac5a85234edfd5d5e49/hf_xet-1.5.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:73a0dae8c71de3b0633a45c73f4a4a5ed09e94b43441d82981a781d4f12baa42", size = 3792751, upload-time = "2026-05-06T06:17:51.791Z" }, + { url = "https://files.pythonhosted.org/packages/07/a5/ed5a0cf35b49a0571af5a8f53416dad1877a718c021c9937c3a53cb45781/hf_xet-1.5.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a60290ec57e9b71767fba7c3645ddafdd0759974b540441510c629c6db6db24a", size = 4456058, upload-time = "2026-05-06T06:17:40.735Z" }, + { url = "https://files.pythonhosted.org/packages/60/fb/3ae8bf2a7a37a4197d0195d7247fd25b3952e15cb8a599e285dfaa6f52b3/hf_xet-1.5.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:e5de0f6deada0dada870bb376a11bcd1f08abf3a968a6d118f33e72d1b1eb480", size = 4250783, upload-time = "2026-05-06T06:17:38.412Z" }, + { url = "https://files.pythonhosted.org/packages/a2/9b/8bae40d4d91525085137196e84eb0ed49cf65b5e96e5c3ecdadd8bd0fac2/hf_xet-1.5.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c799d49f1a5544a0ef7591c0ee75e0d6b93d6f56dc7a4979f59f7518d2872216", size = 4445594, upload-time = "2026-05-06T06:18:04.219Z" }, + { url = "https://files.pythonhosted.org/packages/13/59/c74efbbd4e8728172b2cc72a2bc014d2947a4b7bdced932fbd3f5da1a4e5/hf_xet-1.5.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2baea1b0b989e5c152fe81425f7745ddc8901280ba3d97c98d8cdece7b706c60", size = 4663995, upload-time = "2026-05-06T06:18:06.1Z" }, + { url = "https://files.pythonhosted.org/packages/73/32/8e1e0410af64cda9b139d1dcebdc993a8ff9c8c7c0e2696ae356d75ccc0d/hf_xet-1.5.0-cp313-cp313t-win_amd64.whl", hash = "sha256:526345b3ed45f374f6317349df489167606736c876241ba984105afe7fd4839d", size = 3966608, upload-time = "2026-05-06T06:18:19.74Z" }, + { url = "https://files.pythonhosted.org/packages/fc/34/a8febc8f4edbea8b3e21b02ebc8b628679b84ba7e45cde624a7736b51500/hf_xet-1.5.0-cp313-cp313t-win_arm64.whl", hash = "sha256:786d28e2eb8315d5035544b9d137b4a842d600c434bb91bf7d0d953cce906ad4", size = 3796946, upload-time = "2026-05-06T06:18:17.568Z" }, + { url = "https://files.pythonhosted.org/packages/3d/fb/69ff198a82cae7eb1a69fb84d93b3a3e4816564d76817fe541ddc96874eb/hf_xet-1.5.0-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:dad0dc84e941b8ba3c860659fe1fdc35c049d47cce293f003287757e971a8f56", size = 4030814, upload-time = "2026-05-06T06:17:57.933Z" }, + { url = "https://files.pythonhosted.org/packages/9b/ff/edcc2b40162bef3ff78e14ab637e5f3b89243d6aee72f5949d3bb6a5af83/hf_xet-1.5.0-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:fd6e5a9b0fdac4ed03ed45ef79254a655b1aaab514a02202617fbf643f5fdf7a", size = 3798444, upload-time = "2026-05-06T06:17:55.79Z" }, + { url = "https://files.pythonhosted.org/packages/49/4d/103f76b04310e5e57656696cc184690d20c466af0bca3ca88f8c8ea5d4f3/hf_xet-1.5.0-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3531b1823a0e6d77d80f9ed15ca0e00f0d115094f8ac033d5cae88f4564cc949", size = 4465986, upload-time = "2026-05-06T06:17:44.886Z" }, + { url = "https://files.pythonhosted.org/packages/c4/a2/546f47f464737b3edbab6f8ddb57f2599b93d2cbb66f06abb475ccb48651/hf_xet-1.5.0-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:9a0ee58cd18d5ea799f7ed11290bbccbe56bdd8b1d97ca74b9cc49a3945d7a3b", size = 4259865, upload-time = "2026-05-06T06:17:42.639Z" }, + { url = "https://files.pythonhosted.org/packages/95/7f/1be593c1f28613be2e196473481cd81bfc5910795e30a34e8f744f6cac4f/hf_xet-1.5.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1e60df5a42e9bed8628b6416af2cba4cba57ae9f02de226a06b020d98e1aab18", size = 4459835, upload-time = "2026-05-06T06:18:08.026Z" }, + { url = "https://files.pythonhosted.org/packages/aa/b2/703569fc881f3284487e68cda7b42179978480da3c438042a6bbbb4a671c/hf_xet-1.5.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:4b35549ce62601b84da4ff9b24d970032ace3d4430f52d91bcbb26c901d6c690", size = 4672414, upload-time = "2026-05-06T06:18:09.864Z" }, + { url = "https://files.pythonhosted.org/packages/af/37/1b6def445c567286b50aa3b33828158e135b1be44938dde59f11382a500c/hf_xet-1.5.0-cp37-abi3-win_amd64.whl", hash = "sha256:2806c7c17b4d23f8d88f7c4814f838c3b6150773fe339c20af23e1cfaf2797e4", size = 3977238, upload-time = "2026-05-06T06:18:23.621Z" }, + { url = "https://files.pythonhosted.org/packages/62/94/3b66b148778ee100dcfd69c2ca22b57b41b44d3063ceec934f209e9184ce/hf_xet-1.5.0-cp37-abi3-win_arm64.whl", hash = "sha256:b6c9df403040248c76d808d3e047d64db2d923bae593eb244c41e425cf6cd7be", size = 3806916, upload-time = "2026-05-06T06:18:21.7Z" }, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, +] + +[[package]] +name = "httplib2" +version = "0.31.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyparsing" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c1/1f/e86365613582c027dda5ddb64e1010e57a3d53e99ab8a72093fa13d565ec/httplib2-0.31.2.tar.gz", hash = "sha256:385e0869d7397484f4eab426197a4c020b606edd43372492337c0b4010ae5d24", size = 250800, upload-time = "2026-01-23T11:04:44.165Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2f/90/fd509079dfcab01102c0fdd87f3a9506894bc70afcf9e9785ef6b2b3aff6/httplib2-0.31.2-py3-none-any.whl", hash = "sha256:dbf0c2fa3862acf3c55c078ea9c0bc4481d7dc5117cae71be9514912cf9f8349", size = 91099, upload-time = "2026-01-23T11:04:42.78Z" }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, +] + +[[package]] +name = "httpx-sse" +version = "0.4.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0f/4c/751061ffa58615a32c31b2d82e8482be8dd4a89154f003147acee90f2be9/httpx_sse-0.4.3.tar.gz", hash = "sha256:9b1ed0127459a66014aec3c56bebd93da3c1bc8bb6618c8082039a44889a755d", size = 15943, upload-time = "2025-10-10T21:48:22.271Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/fd/6668e5aec43ab844de6fc74927e155a3b37bf40d7c3790e49fc0406b6578/httpx_sse-0.4.3-py3-none-any.whl", hash = "sha256:0ac1c9fe3c0afad2e0ebb25a934a59f4c7823b60792691f779fad2c5568830fc", size = 8960, upload-time = "2025-10-10T21:48:21.158Z" }, +] + +[[package]] +name = "huggingface-hub" +version = "1.16.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "filelock" }, + { name = "fsspec" }, + { name = "hf-xet", marker = "platform_machine == 'AMD64' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'arm64' or platform_machine == 'x86_64'" }, + { name = "httpx" }, + { name = "packaging" }, + { name = "pyyaml" }, + { name = "tqdm" }, + { name = "typer" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/48/0f/ed994dbade67a54407c28cab96ef845e0e6d25500be56aca6394f8bfc9dd/huggingface_hub-1.16.1.tar.gz", hash = "sha256:7f1dc4c5ec21aed69be630ad0c3378616be16f3de1a47b141c0e812965d9c832", size = 792534, upload-time = "2026-05-21T18:40:00.908Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/79/621a7dbb80c70974f73a597275351ebe03ce5bc65cb5f8f4acb5859252bc/huggingface_hub-1.16.1-py3-none-any.whl", hash = "sha256:64340de934b9ce37857ef85a82de72f5629e8a270f9119eabb12bf495eb53c22", size = 668176, upload-time = "2026-05-21T18:39:58.596Z" }, +] + +[[package]] +name = "idna" +version = "3.16" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1a/88/bcf9709822fe69d02c2a6a77956c98ce6ea8ca8767a9aadcedc7eb6a2390/idna-3.16.tar.gz", hash = "sha256:d7a6da03db833450fca25d2358ac9ff06cd624577a4aea3a596d5c0f77b8e03d", size = 203770, upload-time = "2026-05-22T00:16:18.781Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/16/70255075a9859a0e3adb789b68ceb0e210dec03934245fd98d248226572f/idna-3.16-py3-none-any.whl", hash = "sha256:cc246e3a3f89580c3a951b5ad298ca4638078b2cdd4f115654332b5c26daded5", size = 74165, upload-time = "2026-05-22T00:16:16.698Z" }, +] + +[[package]] +name = "importlib-metadata" +version = "8.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "zipp" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cd/12/33e59336dca5be0c398a7482335911a33aa0e20776128f038019f1a95f1b/importlib_metadata-8.5.0.tar.gz", hash = "sha256:71522656f0abace1d072b9e5481a48f07c138e00f079c38c8f883823f9c26bd7", size = 55304, upload-time = "2024-09-11T14:56:08.937Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/d9/a1e041c5e7caa9a05c925f4bdbdfb7f006d1f74996af53467bc394c97be7/importlib_metadata-8.5.0-py3-none-any.whl", hash = "sha256:45e54197d28b7a7f1559e60b95e7c567032b602131fbd588f1497f47880aa68b", size = 26514, upload-time = "2024-09-11T14:56:07.019Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, +] + +[[package]] +name = "jiter" +version = "0.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/66/b5/55f06bb281d92fb3cc86d14e1def2bd908bb77693183e7cb1f5a3c388b0c/jiter-0.15.0.tar.gz", hash = "sha256:4251acc80e2b7c9b7b8823456ea0fceeb0734dac2df7636d3c711b38476b5a76", size = 166640, upload-time = "2026-05-19T10:09:48.361Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e4/13/daa722f5765c393576f466378f9dfd29d77c9bed939e0688f96afa3601ea/jiter-0.15.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:0f862193b8696249d22ec433e85fd2ab0ad9596bc3e45e6c0bc55e8aeba97be2", size = 310899, upload-time = "2026-05-19T10:07:12.89Z" }, + { url = "https://files.pythonhosted.org/packages/7f/82/2d2551829b082f4b6d82b9f939b031fb808a10aab1ec0664f82e150bb9a2/jiter-0.15.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1303d4d68a9b051ea90502402063ecf3807da00ad2affa19ca1ae3b90b3c5f67", size = 314963, upload-time = "2026-05-19T10:07:14.539Z" }, + { url = "https://files.pythonhosted.org/packages/2a/0a/8b1a51466f7fe9f31dbe4bc7e0ca848674f9825e0f737b929b97e8c60aa7/jiter-0.15.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:392b8ab019e5502d08aff85c6272209c24bc2cbe706ea82a56368f524236614a", size = 341730, upload-time = "2026-05-19T10:07:15.869Z" }, + { url = "https://files.pythonhosted.org/packages/f6/2a/e71dea19822e2e404e83992a08c1d6b9b617bb944f28c9c2fbd85d02c91e/jiter-0.15.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:773b6eb282ce11ee19f05f6b2d4404fa308e5bbd353b0b80a0262caad6db2cd7", size = 366214, upload-time = "2026-05-19T10:07:17.259Z" }, + { url = "https://files.pythonhosted.org/packages/c4/59/97e1fa539d124a509a00ab7f669289d1c1d236ecabf12948a18f16c91082/jiter-0.15.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8d2c0c44d569ce0f2850f5c926f8caeb5f245fbc84475aeb36efccc2103e6dbd", size = 459527, upload-time = "2026-05-19T10:07:18.741Z" }, + { url = "https://files.pythonhosted.org/packages/d1/7a/4a68d331aef8cf2e2393c14a3aacb635c62aa86071b0229899fb5baaa907/jiter-0.15.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:032396229564bca02440396bd327710719f724f5e7b7e9f7a8eb3faa4a2c2281", size = 375451, upload-time = "2026-05-19T10:07:20.208Z" }, + { url = "https://files.pythonhosted.org/packages/7b/7e/1c445c2b6f0e30a274dc8082e0c3c7825411cce80d726bccd697c98cc8d3/jiter-0.15.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3d37768fce7f88dd2a8c6091f2325dea27d30d30d5c6e7a1c0f0af77723b708", size = 349428, upload-time = "2026-05-19T10:07:22.372Z" }, + { url = "https://files.pythonhosted.org/packages/00/94/e20d38984fc17a636371bffd2ae0f698124fdc8e75ef969cd2da6ba7cea7/jiter-0.15.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:2c9cb907439d20bd0c7d7565ca01ee52234203208433749bae5b516907526928", size = 355405, upload-time = "2026-05-19T10:07:23.916Z" }, + { url = "https://files.pythonhosted.org/packages/94/fa/4d09f814779d0ea80a28ed8e4c6662ec9a4a8ecef0ac52190ebac6262d14/jiter-0.15.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9100ddbec09741cc66feb0fc6773f8bdbd0e3c345689368f260082ff85dcc0cd", size = 393688, upload-time = "2026-05-19T10:07:25.854Z" }, + { url = "https://files.pythonhosted.org/packages/54/9d/8eb5d4fb8bf7e93a75964a5da71a75c67c864baf7fa3f98598187b3c7e57/jiter-0.15.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ae1b0d82ac2d987f9ea512b1c9adfcc71a28de3dea3a6039b54d76cffda9901e", size = 520853, upload-time = "2026-05-19T10:07:27.303Z" }, + { url = "https://files.pythonhosted.org/packages/e7/2c/5e07874e59e623a943a0acf1552a80d05b70f31b402287a8fc6d7ec634c7/jiter-0.15.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:8020c99ec13a7db2b6f96cbe82ef4721c88b426a4892f27478044af0284615ef", size = 551016, upload-time = "2026-05-19T10:07:28.846Z" }, + { url = "https://files.pythonhosted.org/packages/22/ed/d2d34422143474cadc15b60d482b1c35683dbc5c63c24346ddd0df09bcaf/jiter-0.15.0-cp311-cp311-win32.whl", hash = "sha256:42bfb257930800cf43e7c62c832402c704ab60797c992faf88d20e903eac8f32", size = 209518, upload-time = "2026-05-19T10:07:30.431Z" }, + { url = "https://files.pythonhosted.org/packages/1d/7d/52778b930e5cc3e52a37d950b1c10494244308b4329b25a0ff0d88303a81/jiter-0.15.0-cp311-cp311-win_amd64.whl", hash = "sha256:860a74063284a2ae9bfedd694f299cc2c68e2696c5f3d440cc9d18bb81b9dd04", size = 200565, upload-time = "2026-05-19T10:07:32.125Z" }, + { url = "https://files.pythonhosted.org/packages/3b/4f/d9b4067feb69b3fa6eb0488e1b59e2ad5b463fe39f59e527eab2aca00bb0/jiter-0.15.0-cp311-cp311-win_arm64.whl", hash = "sha256:37a10c377ce3a4a85f4a67f28b7afe093154cde77eaf248a72e856aa08b4d865", size = 195488, upload-time = "2026-05-19T10:07:33.846Z" }, + { url = "https://files.pythonhosted.org/packages/44/53/4f6bddbcde3c71e56d0aa1337ec95950f3d27dd4153e25aadf0feac71751/jiter-0.15.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:0e90a1c315a0226ec822d973817967f9223b7701546c8c2a7913e7ab0926294d", size = 308793, upload-time = "2026-05-19T10:07:35.25Z" }, + { url = "https://files.pythonhosted.org/packages/01/84/c01099b59a285a1ebba64ae93f62bfa036675340fd1b0045ae65890a0442/jiter-0.15.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8c9004af7c8d67cce7f1aae1026fb55607f4aa600710d08ede3a3ce4aeefe7e0", size = 309570, upload-time = "2026-05-19T10:07:36.919Z" }, + { url = "https://files.pythonhosted.org/packages/58/64/8fb7f9d45bb98190355454cd04dad8d8f27223d6bd52f83af07f637168a6/jiter-0.15.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c210f8b35dc6f30aafd4b4365ca89b9d1189f21ab49b8e68fa6322a847aef138", size = 336783, upload-time = "2026-05-19T10:07:38.694Z" }, + { url = "https://files.pythonhosted.org/packages/c3/b6/f5739011d009b3a30f6a53c5240979030ba29ae46a8c67e3a15759f7c37d/jiter-0.15.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5f30bae8bc1c2d613e28e5af3e8cceb09b742f1c8a8a5f839fb67afaffc03b61", size = 363555, upload-time = "2026-05-19T10:07:40.832Z" }, + { url = "https://files.pythonhosted.org/packages/e5/12/98a9d9f766665e8a3b6252454e17cb0c464606a28cf2fa09399b003345fa/jiter-0.15.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c60e71b6d10cfc284c9bf36bd885e8d44c46f688ce50aa91b5edd90181dea687", size = 452255, upload-time = "2026-05-19T10:07:42.62Z" }, + { url = "https://files.pythonhosted.org/packages/e8/d5/60f972840f79c5e7544fce567c56f1e4e50468f996baba3e78d823dd62a6/jiter-0.15.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ab068bce62a45aa3e7367eceaffb5dde60b7eb853be8dece45132e3d0ff4879", size = 373559, upload-time = "2026-05-19T10:07:44.201Z" }, + { url = "https://files.pythonhosted.org/packages/ee/cf/d46ef1234ba335aabc2f013210db8e0821a22f5e644a2e9449df199ecc23/jiter-0.15.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa248c9eb220197d363f688818dac2fd4b2f0cd7d843ca7105d652034823427d", size = 346055, upload-time = "2026-05-19T10:07:46.005Z" }, + { url = "https://files.pythonhosted.org/packages/f0/63/4d2749d8d54d230bad9b3a6b0d00cc28c6ff6b2fdffc26a8ccf76cc5a974/jiter-0.15.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:2a77aadd57cac1682e4401a72724d2796d89a4ba129b1a5812aa94ee480826eb", size = 351406, upload-time = "2026-05-19T10:07:47.855Z" }, + { url = "https://files.pythonhosted.org/packages/d9/b9/9965b990035d8773328e0a8c8b457a87bf2b19f6c4126d9d99296be5d16a/jiter-0.15.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2ae901f3a55bfafdde31d289590fa25e3245735a2b1e8c7cc15871710a002871", size = 389357, upload-time = "2026-05-19T10:07:49.665Z" }, + { url = "https://files.pythonhosted.org/packages/2d/55/9ddf903deda1413e87fed792f416b7123daee5b8efbad6a202a7421c36a5/jiter-0.15.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:f0b271b462769543716f92d3a4f90527df6ef5ed05ee95ec4137f513e21e1b77", size = 517263, upload-time = "2026-05-19T10:07:51.537Z" }, + { url = "https://files.pythonhosted.org/packages/e8/76/a0c40ad064d3a20a4fde231e35d56e9a01ce82164278180e82d5daf85469/jiter-0.15.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2fb6a5d26af81fc0f00f9360a891e05cf755e149bba391c4d563adc54812973d", size = 548646, upload-time = "2026-05-19T10:07:53.196Z" }, + { url = "https://files.pythonhosted.org/packages/23/4f/eca9b954942916ba2f453891b8593ab444cd872396fe66a3936616f236f3/jiter-0.15.0-cp312-cp312-win32.whl", hash = "sha256:c2f6bb8b5216ab9e7873bc08b5d7bef2b8abbb578a3069bf1cd14a45d71d771d", size = 206427, upload-time = "2026-05-19T10:07:55.307Z" }, + { url = "https://files.pythonhosted.org/packages/95/bf/8ead82a87495149542748e828d153fd232a512a22c83b02c4815c1a9c7d8/jiter-0.15.0-cp312-cp312-win_amd64.whl", hash = "sha256:40b2c7e92c44a84d748d21706c68dc6ff8161d80b59c99d774721a0d2317d7c7", size = 197300, upload-time = "2026-05-19T10:07:56.651Z" }, + { url = "https://files.pythonhosted.org/packages/f4/e4/9b8a78fb2d894471bc344e37f1949bdd784bd914d031dba0ba3a40c71dd7/jiter-0.15.0-cp312-cp312-win_arm64.whl", hash = "sha256:cc0bc345cf2df9d1c00ac443f50d543c1ccfa8b0422cb85b1ab70d681c0b255b", size = 192702, upload-time = "2026-05-19T10:07:58.307Z" }, + { url = "https://files.pythonhosted.org/packages/e5/f4/f708c900ecee41b2025ef8413d5351e5649eb2125c506f6720cc69b06f5c/jiter-0.15.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1c11465f97e2abf45a014b83b730222f8f1c5335e802c7055a67d50de6f1f4e3", size = 307829, upload-time = "2026-05-19T10:07:59.704Z" }, + { url = "https://files.pythonhosted.org/packages/86/59/db537c0949e83668c38481d426b9f2fd5ab758c4ee53a811dd0a510626a0/jiter-0.15.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d1e7b1776f0797956c509e123d0952d10d293a9492dea9f288ab9570ec01d1a5", size = 308445, upload-time = "2026-05-19T10:08:01.184Z" }, + { url = "https://files.pythonhosted.org/packages/37/38/ea0e13b18c30ef951da0d47d39e7fa9edb82a93a62990ffbd7cea9b622d4/jiter-0.15.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:351a341c2105aa430b7047e30f1bf7975f6313b00165d3fc07be2edaf741f279", size = 336181, upload-time = "2026-05-19T10:08:02.688Z" }, + { url = "https://files.pythonhosted.org/packages/58/fc/2303901b16c4ba05865588990a420c0b4156270b44379c20931544a1d962/jiter-0.15.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4ab395feec8d249ec4044e228e98a7033f043426a265df439dc3698823f0a4e4", size = 362985, upload-time = "2026-05-19T10:08:04.394Z" }, + { url = "https://files.pythonhosted.org/packages/5b/6f/11bace093c52e7d4d26c8e606ccd7ae8c972189622469ec0d9e28161e28b/jiter-0.15.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a2a438005b6f22d0273413484d6094d7c2c5d10ec1b3a3bf128e0d1d3ba53258", size = 453292, upload-time = "2026-05-19T10:08:05.967Z" }, + { url = "https://files.pythonhosted.org/packages/22/db/987f2f086ca4d7a6582eb4ccd513f9b26b42d9e4243a087609a3137a8fc7/jiter-0.15.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f18f85e4218d1b40f000f42a92239a7a61a902cd42c65e6c360dbd17dcb20894", size = 373501, upload-time = "2026-05-19T10:08:07.857Z" }, + { url = "https://files.pythonhosted.org/packages/8f/7c/89fbcabb2739b7a5b8dc959a1b6c5761f6484f5fed3486854b3c789bb1de/jiter-0.15.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d1aa62e277fc1cbd80e6deacae6f4d983b41b3d7728e0645c5d741a6149bba45", size = 344683, upload-time = "2026-05-19T10:08:09.431Z" }, + { url = "https://files.pythonhosted.org/packages/30/6f/6cca7692e7dddfec6d8d76c54dc97f2af2a41df4ac0674b999df1f09a5f3/jiter-0.15.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:6550fa135c7deb8ead6af49ed7ff648532ea8334a1447fe34a36315ef79c5c29", size = 350892, upload-time = "2026-05-19T10:08:11.352Z" }, + { url = "https://files.pythonhosted.org/packages/39/14/0338d6190cb8e6d22e677ab1d4eabd4117f67cca70c54cd04b82ff64e068/jiter-0.15.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:066f8f33f18b2419cd8213b2436fa7fbc9c499f315971cfa3ce1f9820c001b1b", size = 388723, upload-time = "2026-05-19T10:08:12.912Z" }, + { url = "https://files.pythonhosted.org/packages/90/31/cc19f4a1bdb6afb09ce6a2f2615aa8d44d994eba0d8e6105ed1af920e736/jiter-0.15.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:75e8a04e91432dde9f1838373cf93d23726c79d3e908d319acf0e796f85592e7", size = 516648, upload-time = "2026-05-19T10:08:14.808Z" }, + { url = "https://files.pythonhosted.org/packages/49/9f/833c541512cd091b63c10c0381973dfe11bc7a503a818c16384417e0c81e/jiter-0.15.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:a97261f1fccb8e50ecd2890a96e46efdc3f57c80a197324c6777827231eca712", size = 547382, upload-time = "2026-05-19T10:08:16.927Z" }, + { url = "https://files.pythonhosted.org/packages/d2/11/e7b70e91f90bc4477e8eee9e8a5f7cf3cb41b4525d6394dc98a714eb8f7f/jiter-0.15.0-cp313-cp313-win32.whl", hash = "sha256:c77496cb10bd7549690fbbab3e5ec05857b83e49276f4a9423a766ddd2afcd4c", size = 205845, upload-time = "2026-05-19T10:08:18.401Z" }, + { url = "https://files.pythonhosted.org/packages/4b/23/5c20d9ad6f02c493e4023e5d2d09e1c1f15fe2753c9102c544aff068a88e/jiter-0.15.0-cp313-cp313-win_amd64.whl", hash = "sha256:b15741f501469009ae0ae90b7147958a664a7dede40aa7ff174a8a4645f546d0", size = 196842, upload-time = "2026-05-19T10:08:20.131Z" }, + { url = "https://files.pythonhosted.org/packages/6b/11/1eb400ef248e8c925fd883fbe325daf5e42cd1b0d308539dd332bd4f7ffc/jiter-0.15.0-cp313-cp313-win_arm64.whl", hash = "sha256:5d6a60072b44c3c2b797a7ddcbcbbf2b34ea3cfd4721580fbfd2a09d9d9b84ba", size = 192212, upload-time = "2026-05-19T10:08:21.807Z" }, + { url = "https://files.pythonhosted.org/packages/8a/60/2fd8d7c79da8acf9b7b277c7616847773779356b92acfc9bb158452174da/jiter-0.15.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:ef1fd24d9413f6209e00d3d5a453e67acfe004a25cc6c8e8484faed4311ab9e8", size = 315065, upload-time = "2026-05-19T10:08:23.218Z" }, + { url = "https://files.pythonhosted.org/packages/46/f4/008fb7d65e8ac2abf00811651a661e025c4ba80bbc6f378450384ddd3aed/jiter-0.15.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:144f8e72cb53dab146347b91cceac01f5481237f2b93b4a339a1ee8f8878b67c", size = 339444, upload-time = "2026-05-19T10:08:24.701Z" }, + { url = "https://files.pythonhosted.org/packages/00/55/90b0c7b9c6896c0f2a591dd36d36b71d22e09674bfef178fa03ba3f81499/jiter-0.15.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:553fcac2ef2cb990877f9fc0833b8b629a3e6a5670b6b5fd58219b41a653ddc4", size = 347779, upload-time = "2026-05-19T10:08:26.408Z" }, + { url = "https://files.pythonhosted.org/packages/51/6b/69666cec5000fd57734c118437394516c749ae8dbeea9fb66d6fef9c4775/jiter-0.15.0-cp313-cp313t-win_amd64.whl", hash = "sha256:774f93f65031856bf14ad9f59bdcab8b8cad501e5ceabd51ba3525f76937a25b", size = 200395, upload-time = "2026-05-19T10:08:28.055Z" }, + { url = "https://files.pythonhosted.org/packages/39/04/a6aa62cd27e8149b0d28df5561f10f6cceaf7935a9ccf3f1c5a05f9a0cd8/jiter-0.15.0-cp313-cp313t-win_arm64.whl", hash = "sha256:f1e1754960f38ec40613a07e5e372df67acb3b890fb383b6fb3de3e49ddbf3c7", size = 190516, upload-time = "2026-05-19T10:08:29.35Z" }, + { url = "https://files.pythonhosted.org/packages/65/43/1fc62172aa98b50a7de9a25554060db510f85c89cfbed0dfe13e1907a139/jiter-0.15.0-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:411fa4dfa5a7ae3d11491027ffb9beadec3996010a986862db70d91abba1c750", size = 305585, upload-time = "2026-05-19T10:09:35.995Z" }, + { url = "https://files.pythonhosted.org/packages/e8/c4/dd58fcd9e2df83666e5c1c1347bef58ce919cd8efc3ffa38aeea62ce493b/jiter-0.15.0-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:2b0074e2f56eb2dacca1689760fd2852a068f85a0547a157b82cb4cafeb6768b", size = 306936, upload-time = "2026-05-19T10:09:37.435Z" }, + { url = "https://files.pythonhosted.org/packages/39/86/b695e16f1180c07f43ea98e73ecd21cf63fa2e1b0c1103739013784d11ae/jiter-0.15.0-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:913d02d29c9606643418d9ccfc3b72492ab25a6bf7889934e09a3490f8d3438b", size = 342453, upload-time = "2026-05-19T10:09:39.294Z" }, + { url = "https://files.pythonhosted.org/packages/34/56/55d76614af37fe3f22a3347d1e410d2a15da581997cb2da499a625000bb5/jiter-0.15.0-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b15d3ec9b0449c40e85319bdb4caa8b77ab526e74f5532ed94bec15e2f66822c", size = 345606, upload-time = "2026-05-19T10:09:40.727Z" }, + { url = "https://files.pythonhosted.org/packages/73/38/505941b2b092fd5bbbd60a52a880db1173f1690ae6751bed3af1c9ddcb4e/jiter-0.15.0-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:631f13a3d04e97d4e083993b10f4b99530e3a10d953e2eb5e196b7dc7f812ce0", size = 303769, upload-time = "2026-05-19T10:09:42.203Z" }, + { url = "https://files.pythonhosted.org/packages/e7/95/a06692b29e77473f286e1ec1f426d3ca44d7b5843be8ad21d7a5f3fcdcc0/jiter-0.15.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:b6c0ffae686c39bf3737be60793783267628783ea42545632c10b291105aee45", size = 305128, upload-time = "2026-05-19T10:09:43.657Z" }, + { url = "https://files.pythonhosted.org/packages/23/85/7270d7ad41d6061a25b950c6bf91d638bd9aacb113200a8c8d57a055fd67/jiter-0.15.0-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d54fb5b31dea401a41af3f8a7d2512e9b6a6a005491e6166c7e4ffab9639a9c", size = 340459, upload-time = "2026-05-19T10:09:45.452Z" }, + { url = "https://files.pythonhosted.org/packages/c8/8d/302cb2057b7513327b4d575cff6b1d066ee6431a5357fc3f8867cd684406/jiter-0.15.0-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:54d5d6090cdc1b7c9e780dfb04949a990adb1e301a2fc0bbcee7de4638d33f9a", size = 344469, upload-time = "2026-05-19T10:09:46.864Z" }, +] + +[[package]] +name = "joblib" +version = "1.5.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/41/f2/d34e8b3a08a9cc79a50b2208a93dce981fe615b64d5a4d4abee421d898df/joblib-1.5.3.tar.gz", hash = "sha256:8561a3269e6801106863fd0d6d84bb737be9e7631e33aaed3fb9ce5953688da3", size = 331603, upload-time = "2025-12-15T08:41:46.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7b/91/984aca2ec129e2757d1e4e3c81c3fcda9d0f85b74670a094cc443d9ee949/joblib-1.5.3-py3-none-any.whl", hash = "sha256:5fc3c5039fc5ca8c0276333a188bbd59d6b7ab37fe6632daa76bc7f9ec18e713", size = 309071, upload-time = "2025-12-15T08:41:44.973Z" }, +] + +[[package]] +name = "joserfc" +version = "1.6.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3b/dc/5f768c2e391e9afabe5d18e3221346deb5fb6338565f1ccc9e7c6d7befdd/joserfc-1.6.5.tar.gz", hash = "sha256:1482a7db78fb4602e44ed89e51b599d052e091288c7c532c5b694e20149dec48", size = 231881, upload-time = "2026-05-06T04:58:13.408Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/3b/ad1cb22e75c963b1f07c8a2329bf47227ce7e4361df5eb2fb101b2ce33ef/joserfc-1.6.5-py3-none-any.whl", hash = "sha256:e9878a0f8243fe7b95e11fdda81374ca9f7a689e302751579d3dfdeec559675e", size = 70464, upload-time = "2026-05-06T04:58:11.668Z" }, +] + +[[package]] +name = "jsonschema" +version = "4.23.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "jsonschema-specifications" }, + { name = "referencing" }, + { name = "rpds-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/38/2e/03362ee4034a4c917f697890ccd4aec0800ccf9ded7f511971c75451deec/jsonschema-4.23.0.tar.gz", hash = "sha256:d71497fef26351a33265337fa77ffeb82423f3ea21283cd9467bb03999266bc4", size = 325778, upload-time = "2024-07-08T18:40:05.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/4a/4f9dbeb84e8850557c02365a0eee0649abe5eb1d84af92a25731c6c0f922/jsonschema-4.23.0-py3-none-any.whl", hash = "sha256:fbadb6f8b144a8f8cf9f0b89ba94501d143e50411a1278633f56a7acf7fd5566", size = 88462, upload-time = "2024-07-08T18:40:00.165Z" }, +] + +[[package]] +name = "jsonschema-specifications" +version = "2025.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "referencing" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/19/74/a633ee74eb36c44aa6d1095e7cc5569bebf04342ee146178e2d36600708b/jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d", size = 32855, upload-time = "2025-09-08T01:34:59.186Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437, upload-time = "2025-09-08T01:34:57.871Z" }, +] + +[[package]] +name = "litellm" +version = "1.83.14" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohttp" }, + { name = "click" }, + { name = "fastuuid" }, + { name = "httpx" }, + { name = "importlib-metadata" }, + { name = "jinja2" }, + { name = "jsonschema" }, + { name = "openai" }, + { name = "pydantic" }, + { name = "python-dotenv" }, + { name = "tiktoken" }, + { name = "tokenizers" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8d/7c/c095649380adc96c8630273c1768c2ad1e74aa2ee1dd8dd05d218a60569f/litellm-1.83.14.tar.gz", hash = "sha256:24aef9b47cdc424c833e32f3727f411741c690832cd1fe4405e0077144fe09c9", size = 14836599, upload-time = "2026-04-26T03:16:10.176Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/5c/1b5691575420135e90578543b2bf219497caa33cfd0af64cb38f30288450/litellm-1.83.14-py3-none-any.whl", hash = "sha256:92b11ba2a32cf80707ddf388d18526696c7999a21b418c5e3b6eda1243d2cfdb", size = 16457054, upload-time = "2026-04-26T03:16:05.72Z" }, +] + +[[package]] +name = "mako" +version = "1.3.12" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/00/62/791b31e69ae182791ec67f04850f2f062716bbd205483d63a215f3e062d3/mako-1.3.12.tar.gz", hash = "sha256:9f778e93289bd410bb35daadeb4fc66d95a746f0b75777b942088b7fd7af550a", size = 400219, upload-time = "2026-04-28T19:01:08.512Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bc/b1/a0ec7a5a9db730a08daef1fdfb8090435b82465abbf758a596f0ea88727e/mako-1.3.12-py3-none-any.whl", hash = "sha256:8f61569480282dbf557145ce441e4ba888be453c30989f879f0d652e39f53ea9", size = 78521, upload-time = "2026-04-28T19:01:10.393Z" }, +] + +[[package]] +name = "markdown-it-py" +version = "4.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/ff/7841249c247aa650a76b9ee4bbaeae59370dc8bfd2f6c01f3630c35eb134/markdown_it_py-4.2.0.tar.gz", hash = "sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49", size = 82454, upload-time = "2026-05-07T12:08:28.36Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl", hash = "sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a", size = 91687, upload-time = "2026-05-07T12:08:27.182Z" }, +] + +[[package]] +name = "markupsafe" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/08/db/fefacb2136439fc8dd20e797950e749aa1f4997ed584c62cfb8ef7c2be0e/markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad", size = 11631, upload-time = "2025-09-27T18:36:18.185Z" }, + { url = "https://files.pythonhosted.org/packages/e1/2e/5898933336b61975ce9dc04decbc0a7f2fee78c30353c5efba7f2d6ff27a/markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a", size = 12058, upload-time = "2025-09-27T18:36:19.444Z" }, + { url = "https://files.pythonhosted.org/packages/1d/09/adf2df3699d87d1d8184038df46a9c80d78c0148492323f4693df54e17bb/markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50", size = 24287, upload-time = "2025-09-27T18:36:20.768Z" }, + { url = "https://files.pythonhosted.org/packages/30/ac/0273f6fcb5f42e314c6d8cd99effae6a5354604d461b8d392b5ec9530a54/markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf", size = 22940, upload-time = "2025-09-27T18:36:22.249Z" }, + { url = "https://files.pythonhosted.org/packages/19/ae/31c1be199ef767124c042c6c3e904da327a2f7f0cd63a0337e1eca2967a8/markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f", size = 21887, upload-time = "2025-09-27T18:36:23.535Z" }, + { url = "https://files.pythonhosted.org/packages/b2/76/7edcab99d5349a4532a459e1fe64f0b0467a3365056ae550d3bcf3f79e1e/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a", size = 23692, upload-time = "2025-09-27T18:36:24.823Z" }, + { url = "https://files.pythonhosted.org/packages/a4/28/6e74cdd26d7514849143d69f0bf2399f929c37dc2b31e6829fd2045b2765/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115", size = 21471, upload-time = "2025-09-27T18:36:25.95Z" }, + { url = "https://files.pythonhosted.org/packages/62/7e/a145f36a5c2945673e590850a6f8014318d5577ed7e5920a4b3448e0865d/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a", size = 22923, upload-time = "2025-09-27T18:36:27.109Z" }, + { url = "https://files.pythonhosted.org/packages/0f/62/d9c46a7f5c9adbeeeda52f5b8d802e1094e9717705a645efc71b0913a0a8/markupsafe-3.0.3-cp311-cp311-win32.whl", hash = "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19", size = 14572, upload-time = "2025-09-27T18:36:28.045Z" }, + { url = "https://files.pythonhosted.org/packages/83/8a/4414c03d3f891739326e1783338e48fb49781cc915b2e0ee052aa490d586/markupsafe-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01", size = 15077, upload-time = "2025-09-27T18:36:29.025Z" }, + { url = "https://files.pythonhosted.org/packages/35/73/893072b42e6862f319b5207adc9ae06070f095b358655f077f69a35601f0/markupsafe-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c", size = 13876, upload-time = "2025-09-27T18:36:29.954Z" }, + { url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" }, + { url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" }, + { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" }, + { url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" }, + { url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" }, + { url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" }, + { url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" }, + { url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" }, + { url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" }, + { url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" }, + { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" }, + { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" }, + { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" }, + { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" }, + { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" }, + { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" }, + { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" }, + { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" }, + { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" }, + { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" }, + { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" }, + { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" }, + { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" }, + { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" }, + { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" }, + { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" }, + { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" }, + { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" }, + { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" }, + { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" }, + { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" }, +] + +[[package]] +name = "mcp" +version = "1.27.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "httpx" }, + { name = "httpx-sse" }, + { name = "jsonschema" }, + { name = "pydantic" }, + { name = "pydantic-settings" }, + { name = "pyjwt", extra = ["crypto"] }, + { name = "python-multipart" }, + { name = "pywin32", marker = "sys_platform == 'win32'" }, + { name = "sse-starlette" }, + { name = "starlette" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, + { name = "uvicorn", marker = "sys_platform != 'emscripten'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/38/83/d1efe7c2980d8a3afa476f4e3d42d53dd54c0ab94c27bee5d755b45c8b73/mcp-1.27.1.tar.gz", hash = "sha256:0f47e1820f8f8f941466b39749eb1d1839a04caddca2bc60e9d46e8a99914924", size = 608458, upload-time = "2026-05-08T16:50:12.601Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fd/73/42d9596facebdb533b7f0b86c1b0364ef350d1f8ba78b1052e8a58b48b65/mcp-1.27.1-py3-none-any.whl", hash = "sha256:1af3c4203b329430fde7a87b4fcb6392a041f5cb851fd68fc674016ab4e7c06f", size = 216260, upload-time = "2026-05-08T16:50:10.547Z" }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, +] + +[[package]] +name = "mmh3" +version = "5.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/91/1a/edb23803a168f070ded7a3014c6d706f63b90c84ccc024f89d794a3b7a6d/mmh3-5.2.1.tar.gz", hash = "sha256:bbea5b775f0ac84945191fb83f845a6fd9a21a03ea7f2e187defac7e401616ad", size = 33775, upload-time = "2026-03-05T15:55:57.716Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/65/d7/3312a59df3c1cdd783f4cf0c4ee8e9decff9c5466937182e4cc7dbbfe6c5/mmh3-5.2.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:dae0f0bd7d30c0ad61b9a504e8e272cb8391eed3f1587edf933f4f6b33437450", size = 56082, upload-time = "2026-03-05T15:53:59.702Z" }, + { url = "https://files.pythonhosted.org/packages/61/96/6f617baa098ca0d2989bfec6d28b5719532cd8d8848782662f5b755f657f/mmh3-5.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9aeaf53eaa075dd63e81512522fd180097312fb2c9f476333309184285c49ce0", size = 40458, upload-time = "2026-03-05T15:54:01.548Z" }, + { url = "https://files.pythonhosted.org/packages/c1/b4/9cd284bd6062d711e13d26c04d4778ab3f690c1c38a4563e3c767ec8802e/mmh3-5.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0634581290e6714c068f4aa24020acf7880927d1f0084fa753d9799ae9610082", size = 40079, upload-time = "2026-03-05T15:54:02.743Z" }, + { url = "https://files.pythonhosted.org/packages/f6/09/a806334ce1d3d50bf782b95fcee8b3648e1e170327d4bb7b4bad2ad7d956/mmh3-5.2.1-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e080c0637aea036f35507e803a4778f119a9b436617694ae1c5c366805f1e997", size = 97242, upload-time = "2026-03-05T15:54:04.536Z" }, + { url = "https://files.pythonhosted.org/packages/ee/93/723e317dd9e041c4dc4566a2eb53b01ad94de31750e0b834f1643905e97c/mmh3-5.2.1-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:db0562c5f71d18596dcd45e854cf2eeba27d7543e1a3acdafb7eef728f7fe85d", size = 103082, upload-time = "2026-03-05T15:54:06.387Z" }, + { url = "https://files.pythonhosted.org/packages/61/b5/f96121e69cc48696075071531cf574f112e1ffd08059f4bffb41210e6fc5/mmh3-5.2.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1d9f9a3ce559a5267014b04b82956993270f63ec91765e13e9fd73daf2d2738e", size = 106054, upload-time = "2026-03-05T15:54:07.506Z" }, + { url = "https://files.pythonhosted.org/packages/82/49/192b987ec48d0b2aecf8ac285a9b11fbc00030f6b9c694664ae923458dde/mmh3-5.2.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:960b1b3efa39872ac8b6cc3a556edd6fb90ed74f08c9c45e028f1005b26aa55d", size = 112910, upload-time = "2026-03-05T15:54:09.403Z" }, + { url = "https://files.pythonhosted.org/packages/cf/a1/03e91fd334ed0144b83343a76eb11f17434cd08f746401488cfeafb2d241/mmh3-5.2.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d30b650595fdbe32366b94cb14f30bb2b625e512bd4e1df00611f99dc5c27fd4", size = 120551, upload-time = "2026-03-05T15:54:10.587Z" }, + { url = "https://files.pythonhosted.org/packages/93/b9/b89a71d2ff35c3a764d1c066c7313fc62c7cc48fa48a4b3b0304a4a0146f/mmh3-5.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:82f3802bfc4751f420d591c5c864de538b71cea117fce67e4595c2afede08a15", size = 99096, upload-time = "2026-03-05T15:54:11.76Z" }, + { url = "https://files.pythonhosted.org/packages/36/b5/613772c1c6ed5f7b63df55eb131e887cc43720fec392777b95a79d34e640/mmh3-5.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:915e7a2418f10bd1151b1953df06d896db9783c9cfdb9a8ee1f9b3a4331ab503", size = 98524, upload-time = "2026-03-05T15:54:13.122Z" }, + { url = "https://files.pythonhosted.org/packages/5e/0e/1524566fe8eaf871e4f7bc44095929fcd2620488f402822d848df19d679c/mmh3-5.2.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:fc78739b5ec6e4fb02301984a3d442a91406e7700efbe305071e7fd1c78278f2", size = 106239, upload-time = "2026-03-05T15:54:14.601Z" }, + { url = "https://files.pythonhosted.org/packages/04/94/21adfa7d90a7a697137ad6de33eeff6445420ca55e433a5d4919c79bc3b5/mmh3-5.2.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:41aac7002a749f08727cb91babff1daf8deac317c0b1f317adc69be0e6c375d1", size = 109797, upload-time = "2026-03-05T15:54:15.819Z" }, + { url = "https://files.pythonhosted.org/packages/b5/e6/1aacc3a219e1aa62fa65669995d4a3562b35be5200ec03680c7e4bec9676/mmh3-5.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9d8089d853c7963a8ce87fff93e2a67075c0bc08684a08ea6ad13577c38ffc38", size = 97228, upload-time = "2026-03-05T15:54:16.992Z" }, + { url = "https://files.pythonhosted.org/packages/f1/b9/5e4cca8dcccf298add0a27f3c357bc8cf8baf821d35cdc6165e4bd5a48b0/mmh3-5.2.1-cp311-cp311-win32.whl", hash = "sha256:baeb47635cb33375dee4924cd93d7f5dcaa786c740b08423b0209b824a1ee728", size = 40751, upload-time = "2026-03-05T15:54:18.714Z" }, + { url = "https://files.pythonhosted.org/packages/72/fc/5b11d49247f499bcda591171e9cf3b6ee422b19e70aa2cef2e0ae65ca3b9/mmh3-5.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:1e4ecee40ba19e6975e1120829796770325841c2f153c0e9aecca927194c6a2a", size = 41517, upload-time = "2026-03-05T15:54:19.764Z" }, + { url = "https://files.pythonhosted.org/packages/8a/5f/2a511ee8a1c2a527c77726d5231685b72312c5a1a1b7639ad66a9652aa84/mmh3-5.2.1-cp311-cp311-win_arm64.whl", hash = "sha256:c302245fd6c33d96bd169c7ccf2513c20f4c1e417c07ce9dce107c8bc3f8411f", size = 39287, upload-time = "2026-03-05T15:54:20.904Z" }, + { url = "https://files.pythonhosted.org/packages/92/94/bc5c3b573b40a328c4d141c20e399039ada95e5e2a661df3425c5165fd84/mmh3-5.2.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0cc21533878e5586b80d74c281d7f8da7932bc8ace50b8d5f6dbf7e3935f63f1", size = 56087, upload-time = "2026-03-05T15:54:21.92Z" }, + { url = "https://files.pythonhosted.org/packages/f6/80/64a02cc3e95c3af0aaa2590849d9ed24a9f14bb93537addde688e039b7c3/mmh3-5.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4eda76074cfca2787c8cf1bec603eaebdddd8b061ad5502f85cddae998d54f00", size = 40500, upload-time = "2026-03-05T15:54:22.953Z" }, + { url = "https://files.pythonhosted.org/packages/8b/72/e6d6602ce18adf4ddcd0e48f2e13590cc92a536199e52109f46f259d3c46/mmh3-5.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:eee884572b06bbe8a2b54f424dbd996139442cf83c76478e1ec162512e0dd2c7", size = 40034, upload-time = "2026-03-05T15:54:23.943Z" }, + { url = "https://files.pythonhosted.org/packages/59/c2/bf4537a8e58e21886ef16477041238cab5095c836496e19fafc34b7445d2/mmh3-5.2.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0d0b7e803191db5f714d264044e06189c8ccd3219e936cc184f07106bd17fd7b", size = 97292, upload-time = "2026-03-05T15:54:25.335Z" }, + { url = "https://files.pythonhosted.org/packages/e5/e2/51ed62063b44d10b06d975ac87af287729eeb5e3ed9772f7584a17983e90/mmh3-5.2.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8e6c219e375f6341d0959af814296372d265a8ca1af63825f65e2e87c618f006", size = 103274, upload-time = "2026-03-05T15:54:26.44Z" }, + { url = "https://files.pythonhosted.org/packages/75/ce/12a7524dca59eec92e5b31fdb13ede1e98eda277cf2b786cf73bfbc24e81/mmh3-5.2.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:26fb5b9c3946bf7f1daed7b37e0c03898a6f062149127570f8ede346390a0825", size = 106158, upload-time = "2026-03-05T15:54:28.578Z" }, + { url = "https://files.pythonhosted.org/packages/86/1f/d3ba6dd322d01ab5d44c46c8f0c38ab6bbbf9b5e20e666dfc05bf4a23604/mmh3-5.2.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3c38d142c706201db5b2345166eeef1e7740e3e2422b470b8ba5c8727a9b4c7a", size = 113005, upload-time = "2026-03-05T15:54:29.767Z" }, + { url = "https://files.pythonhosted.org/packages/b6/a9/15d6b6f913294ea41b44d901741298e3718e1cb89ee626b3694625826a43/mmh3-5.2.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:50885073e2909251d4718634a191c49ae5f527e5e1736d738e365c3e8be8f22b", size = 120744, upload-time = "2026-03-05T15:54:30.931Z" }, + { url = "https://files.pythonhosted.org/packages/76/b3/70b73923fd0284c439860ff5c871b20210dfdbe9a6b9dd0ee6496d77f174/mmh3-5.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b3f99e1756fc48ad507b95e5d86f2fb21b3d495012ff13e6592ebac14033f166", size = 99111, upload-time = "2026-03-05T15:54:32.353Z" }, + { url = "https://files.pythonhosted.org/packages/dd/38/99f7f75cd27d10d8b899a1caafb9d531f3903e4d54d572220e3d8ac35e89/mmh3-5.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:62815d2c67f2dd1be76a253d88af4e1da19aeaa1820146dec52cf8bee2958b16", size = 98623, upload-time = "2026-03-05T15:54:33.801Z" }, + { url = "https://files.pythonhosted.org/packages/fd/68/6e292c0853e204c44d2f03ea5f090be3317a0e2d9417ecb62c9eb27687df/mmh3-5.2.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8f767ba0911602ddef289404e33835a61168314ebd3c729833db2ed685824211", size = 106437, upload-time = "2026-03-05T15:54:35.177Z" }, + { url = "https://files.pythonhosted.org/packages/dd/c6/fedd7284c459cfb58721d461fcf5607a4c1f5d9ab195d113d51d10164d16/mmh3-5.2.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:67e41a497bac88cc1de96eeba56eeb933c39d54bc227352f8455aa87c4ca4000", size = 110002, upload-time = "2026-03-05T15:54:36.673Z" }, + { url = "https://files.pythonhosted.org/packages/3b/ac/ca8e0c19a34f5b71390171d2ff0b9f7f187550d66801a731bb68925126a4/mmh3-5.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3d74a03fb57757ece25aa4b3c1c60157a1cece37a020542785f942e2f827eed5", size = 97507, upload-time = "2026-03-05T15:54:37.804Z" }, + { url = "https://files.pythonhosted.org/packages/df/94/6ebb9094cfc7ac5e7950776b9d13a66bb4a34f83814f32ba2abc9494fc68/mmh3-5.2.1-cp312-cp312-win32.whl", hash = "sha256:7374d6e3ef72afe49697ecd683f3da12f4fc06af2d75433d0580c6746d2fa025", size = 40773, upload-time = "2026-03-05T15:54:40.077Z" }, + { url = "https://files.pythonhosted.org/packages/5b/3c/cd3527198cf159495966551c84a5f36805a10ac17b294f41f67b83f6a4d6/mmh3-5.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:3a9fed49c6ce4ed7e73f13182760c65c816da006debe67f37635580dfb0fae00", size = 41560, upload-time = "2026-03-05T15:54:41.148Z" }, + { url = "https://files.pythonhosted.org/packages/15/96/6fe5ebd0f970a076e3ed5512871ce7569447b962e96c125528a2f9724470/mmh3-5.2.1-cp312-cp312-win_arm64.whl", hash = "sha256:bbfcb95d9a744e6e2827dfc66ad10e1020e0cac255eb7f85652832d5a264c2fc", size = 39313, upload-time = "2026-03-05T15:54:42.171Z" }, + { url = "https://files.pythonhosted.org/packages/25/a5/9daa0508a1569a54130f6198d5462a92deda870043624aa3ea72721aa765/mmh3-5.2.1-cp313-cp313-android_21_arm64_v8a.whl", hash = "sha256:723b2681ed4cc07d3401bbea9c201ad4f2a4ca6ba8cddaff6789f715dd2b391e", size = 40832, upload-time = "2026-03-05T15:54:43.212Z" }, + { url = "https://files.pythonhosted.org/packages/0a/6b/3230c6d80c1f4b766dedf280a92c2241e99f87c1504ff74205ec8cebe451/mmh3-5.2.1-cp313-cp313-android_21_x86_64.whl", hash = "sha256:3619473a0e0d329fd4aec8075628f8f616be2da41605300696206d6f36920c3d", size = 41964, upload-time = "2026-03-05T15:54:44.204Z" }, + { url = "https://files.pythonhosted.org/packages/62/fb/648bfddb74a872004b6ee751551bfdda783fe6d70d2e9723bad84dbe5311/mmh3-5.2.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:e48d4dbe0f88e53081da605ae68644e5182752803bbc2beb228cca7f1c4454d6", size = 39114, upload-time = "2026-03-05T15:54:45.205Z" }, + { url = "https://files.pythonhosted.org/packages/95/c2/ab7901f87af438468b496728d11264cb397b3574d41506e71b92128e0373/mmh3-5.2.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:a482ac121de6973897c92c2f31defc6bafb11c83825109275cffce54bb64933f", size = 39819, upload-time = "2026-03-05T15:54:46.509Z" }, + { url = "https://files.pythonhosted.org/packages/2f/ed/6f88dda0df67de1612f2e130ffea34cf84aaee5bff5b0aff4dbff2babe34/mmh3-5.2.1-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:17fbb47f0885ace8327ce1235d0416dc86a211dcd8cc1e703f41523be32cfec8", size = 40330, upload-time = "2026-03-05T15:54:47.864Z" }, + { url = "https://files.pythonhosted.org/packages/3d/66/7516d23f53cdf90f43fce24ab80c28f45e6851d78b46bef8c02084edf583/mmh3-5.2.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:d51fde50a77f81330523562e3c2734ffdca9c4c9e9d355478117905e1cfe16c6", size = 56078, upload-time = "2026-03-05T15:54:48.9Z" }, + { url = "https://files.pythonhosted.org/packages/bc/34/4d152fdf4a91a132cb226b671f11c6b796eada9ab78080fb5ce1e95adaab/mmh3-5.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:19bbd3b841174ae6ed588536ab5e1b1fe83d046e668602c20266547298d939a9", size = 40498, upload-time = "2026-03-05T15:54:49.942Z" }, + { url = "https://files.pythonhosted.org/packages/d4/4c/8e3af1b6d85a299767ec97bd923f12b06267089c1472c27c1696870d1175/mmh3-5.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:be77c402d5e882b6fbacfd90823f13da8e0a69658405a39a569c6b58fdb17b03", size = 40033, upload-time = "2026-03-05T15:54:50.994Z" }, + { url = "https://files.pythonhosted.org/packages/8b/f2/966ea560e32578d453c9e9db53d602cbb1d0da27317e232afa7c38ceba11/mmh3-5.2.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:fd96476f04db5ceba1cfa0f21228f67c1f7402296f0e73fee3513aa680ad237b", size = 97320, upload-time = "2026-03-05T15:54:52.072Z" }, + { url = "https://files.pythonhosted.org/packages/bb/0d/2c5f9893b38aeb6b034d1a44ecd55a010148054f6a516abe53b5e4057297/mmh3-5.2.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:707151644085dd0f20fe4f4b573d28e5130c4aaa5f587e95b60989c5926653b5", size = 103299, upload-time = "2026-03-05T15:54:53.569Z" }, + { url = "https://files.pythonhosted.org/packages/1c/fc/2ebaef4a4d4376f89761274dc274035ffd96006ab496b4ee5af9b08f21a9/mmh3-5.2.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3737303ca9ea0f7cb83028781148fcda4f1dac7821db0c47672971dabcf63593", size = 106222, upload-time = "2026-03-05T15:54:55.092Z" }, + { url = "https://files.pythonhosted.org/packages/57/09/ea7ffe126d0ba0406622602a2d05e1e1a6841cc92fc322eb576c95b27fad/mmh3-5.2.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2778fed822d7db23ac5008b181441af0c869455b2e7d001f4019636ac31b6fe4", size = 113048, upload-time = "2026-03-05T15:54:56.305Z" }, + { url = "https://files.pythonhosted.org/packages/85/57/9447032edf93a64aa9bef4d9aa596400b1756f40411890f77a284f6293ca/mmh3-5.2.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d57dea657357230cc780e13920d7fa7db059d58fe721c80020f94476da4ca0a1", size = 120742, upload-time = "2026-03-05T15:54:57.453Z" }, + { url = "https://files.pythonhosted.org/packages/53/82/a86cc87cc88c92e9e1a598fee509f0409435b57879a6129bf3b3e40513c7/mmh3-5.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:169e0d178cb59314456ab30772429a802b25d13227088085b0d49b9fe1533104", size = 99132, upload-time = "2026-03-05T15:54:58.583Z" }, + { url = "https://files.pythonhosted.org/packages/54/f7/6b16eb1b40ee89bb740698735574536bc20d6cdafc65ae702ea235578e05/mmh3-5.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:7e4e1f580033335c6f76d1e0d6b56baf009d1a64d6a4816347e4271ba951f46d", size = 98686, upload-time = "2026-03-05T15:55:00.078Z" }, + { url = "https://files.pythonhosted.org/packages/e8/88/a601e9f32ad1410f438a6d0544298ea621f989bd34a0731a7190f7dec799/mmh3-5.2.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:2bd9f19f7f1fcebd74e830f4af0f28adad4975d40d80620be19ffb2b2af56c9f", size = 106479, upload-time = "2026-03-05T15:55:01.532Z" }, + { url = "https://files.pythonhosted.org/packages/d6/5c/ce29ae3dfc4feec4007a437a1b7435fb9507532a25147602cd5b52be86db/mmh3-5.2.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:c88653877aeb514c089d1b3d473451677b8b9a6d1497dbddf1ae7934518b06d2", size = 110030, upload-time = "2026-03-05T15:55:02.934Z" }, + { url = "https://files.pythonhosted.org/packages/13/30/ae444ef2ff87c805d525da4fa63d27cda4fe8a48e77003a036b8461cfd5c/mmh3-5.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fceef7fe67c81e1585198215e42ad3fdba3a25644beda8fbdaf85f4d7b93175a", size = 97536, upload-time = "2026-03-05T15:55:04.135Z" }, + { url = "https://files.pythonhosted.org/packages/4b/f9/dc3787ee5c813cc27fe79f45ad4500d9b5437f23a7402435cc34e07c7718/mmh3-5.2.1-cp313-cp313-win32.whl", hash = "sha256:54b64fb2433bc71488e7a449603bf8bd31fbcf9cb56fbe1eb6d459e90b86c37b", size = 40769, upload-time = "2026-03-05T15:55:05.277Z" }, + { url = "https://files.pythonhosted.org/packages/43/67/850e0b5a1e97799822ebfc4ca0e8c6ece3ed8baf7dcdf64de817dfdda2ca/mmh3-5.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:cae6383181f1e345317742d2ddd88f9e7d2682fa4c9432e3a74e47d92dce0229", size = 41563, upload-time = "2026-03-05T15:55:06.283Z" }, + { url = "https://files.pythonhosted.org/packages/c0/cc/98c90b28e1da5458e19fbfaf4adb5289208d3bfccd45dd14eab216a2f0bb/mmh3-5.2.1-cp313-cp313-win_arm64.whl", hash = "sha256:022aa1a528604e6c83d0a7705fdef0b5355d897a9e0fa3a8d26709ceaa06965d", size = 39310, upload-time = "2026-03-05T15:55:07.323Z" }, +] + +[[package]] +name = "multidict" +version = "6.7.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1a/c2/c2d94cbe6ac1753f3fc980da97b3d930efe1da3af3c9f5125354436c073d/multidict-6.7.1.tar.gz", hash = "sha256:ec6652a1bee61c53a3e5776b6049172c53b6aaba34f18c9ad04f82712bac623d", size = 102010, upload-time = "2026-01-26T02:46:45.979Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ce/f1/a90635c4f88fb913fbf4ce660b83b7445b7a02615bda034b2f8eb38fd597/multidict-6.7.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7ff981b266af91d7b4b3793ca3382e53229088d193a85dfad6f5f4c27fc73e5d", size = 76626, upload-time = "2026-01-26T02:43:26.485Z" }, + { url = "https://files.pythonhosted.org/packages/a6/9b/267e64eaf6fc637a15b35f5de31a566634a2740f97d8d094a69d34f524a4/multidict-6.7.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:844c5bca0b5444adb44a623fb0a1310c2f4cd41f402126bb269cd44c9b3f3e1e", size = 44706, upload-time = "2026-01-26T02:43:27.607Z" }, + { url = "https://files.pythonhosted.org/packages/dd/a4/d45caf2b97b035c57267791ecfaafbd59c68212004b3842830954bb4b02e/multidict-6.7.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f2a0a924d4c2e9afcd7ec64f9de35fcd96915149b2216e1cb2c10a56df483855", size = 44356, upload-time = "2026-01-26T02:43:28.661Z" }, + { url = "https://files.pythonhosted.org/packages/fd/d2/0a36c8473f0cbaeadd5db6c8b72d15bbceeec275807772bfcd059bef487d/multidict-6.7.1-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:8be1802715a8e892c784c0197c2ace276ea52702a0ede98b6310c8f255a5afb3", size = 244355, upload-time = "2026-01-26T02:43:31.165Z" }, + { url = "https://files.pythonhosted.org/packages/5d/16/8c65be997fd7dd311b7d39c7b6e71a0cb449bad093761481eccbbe4b42a2/multidict-6.7.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2e2d2ed645ea29f31c4c7ea1552fcfd7cb7ba656e1eafd4134a6620c9f5fdd9e", size = 246433, upload-time = "2026-01-26T02:43:32.581Z" }, + { url = "https://files.pythonhosted.org/packages/01/fb/4dbd7e848d2799c6a026ec88ad39cf2b8416aa167fcc903baa55ecaa045c/multidict-6.7.1-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:95922cee9a778659e91db6497596435777bd25ed116701a4c034f8e46544955a", size = 225376, upload-time = "2026-01-26T02:43:34.417Z" }, + { url = "https://files.pythonhosted.org/packages/b6/8a/4a3a6341eac3830f6053062f8fbc9a9e54407c80755b3f05bc427295c2d0/multidict-6.7.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6b83cabdc375ffaaa15edd97eb7c0c672ad788e2687004990074d7d6c9b140c8", size = 257365, upload-time = "2026-01-26T02:43:35.741Z" }, + { url = "https://files.pythonhosted.org/packages/f7/a2/dd575a69c1aa206e12d27d0770cdf9b92434b48a9ef0cd0d1afdecaa93c4/multidict-6.7.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:38fb49540705369bab8484db0689d86c0a33a0a9f2c1b197f506b71b4b6c19b0", size = 254747, upload-time = "2026-01-26T02:43:36.976Z" }, + { url = "https://files.pythonhosted.org/packages/5a/56/21b27c560c13822ed93133f08aa6372c53a8e067f11fbed37b4adcdac922/multidict-6.7.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:439cbebd499f92e9aa6793016a8acaa161dfa749ae86d20960189f5398a19144", size = 246293, upload-time = "2026-01-26T02:43:38.258Z" }, + { url = "https://files.pythonhosted.org/packages/5a/a4/23466059dc3854763423d0ad6c0f3683a379d97673b1b89ec33826e46728/multidict-6.7.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6d3bc717b6fe763b8be3f2bee2701d3c8eb1b2a8ae9f60910f1b2860c82b6c49", size = 242962, upload-time = "2026-01-26T02:43:40.034Z" }, + { url = "https://files.pythonhosted.org/packages/1f/67/51dd754a3524d685958001e8fa20a0f5f90a6a856e0a9dcabff69be3dbb7/multidict-6.7.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:619e5a1ac57986dbfec9f0b301d865dddf763696435e2962f6d9cf2fdff2bb71", size = 237360, upload-time = "2026-01-26T02:43:41.752Z" }, + { url = "https://files.pythonhosted.org/packages/64/3f/036dfc8c174934d4b55d86ff4f978e558b0e585cef70cfc1ad01adc6bf18/multidict-6.7.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:0b38ebffd9be37c1170d33bc0f36f4f262e0a09bc1aac1c34c7aa51a7293f0b3", size = 245940, upload-time = "2026-01-26T02:43:43.042Z" }, + { url = "https://files.pythonhosted.org/packages/3d/20/6214d3c105928ebc353a1c644a6ef1408bc5794fcb4f170bb524a3c16311/multidict-6.7.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:10ae39c9cfe6adedcdb764f5e8411d4a92b055e35573a2eaa88d3323289ef93c", size = 253502, upload-time = "2026-01-26T02:43:44.371Z" }, + { url = "https://files.pythonhosted.org/packages/b1/e2/c653bc4ae1be70a0f836b82172d643fcf1dade042ba2676ab08ec08bff0f/multidict-6.7.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:25167cc263257660290fba06b9318d2026e3c910be240a146e1f66dd114af2b0", size = 247065, upload-time = "2026-01-26T02:43:45.745Z" }, + { url = "https://files.pythonhosted.org/packages/c8/11/a854b4154cd3bd8b1fd375e8a8ca9d73be37610c361543d56f764109509b/multidict-6.7.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:128441d052254f42989ef98b7b6a6ecb1e6f708aa962c7984235316db59f50fa", size = 241870, upload-time = "2026-01-26T02:43:47.054Z" }, + { url = "https://files.pythonhosted.org/packages/13/bf/9676c0392309b5fdae322333d22a829715b570edb9baa8016a517b55b558/multidict-6.7.1-cp311-cp311-win32.whl", hash = "sha256:d62b7f64ffde3b99d06b707a280db04fb3855b55f5a06df387236051d0668f4a", size = 41302, upload-time = "2026-01-26T02:43:48.753Z" }, + { url = "https://files.pythonhosted.org/packages/c9/68/f16a3a8ba6f7b6dc92a1f19669c0810bd2c43fc5a02da13b1cbf8e253845/multidict-6.7.1-cp311-cp311-win_amd64.whl", hash = "sha256:bdbf9f3b332abd0cdb306e7c2113818ab1e922dc84b8f8fd06ec89ed2a19ab8b", size = 45981, upload-time = "2026-01-26T02:43:49.921Z" }, + { url = "https://files.pythonhosted.org/packages/ac/ad/9dd5305253fa00cd3c7555dbef69d5bf4133debc53b87ab8d6a44d411665/multidict-6.7.1-cp311-cp311-win_arm64.whl", hash = "sha256:b8c990b037d2fff2f4e33d3f21b9b531c5745b33a49a7d6dbe7a177266af44f6", size = 43159, upload-time = "2026-01-26T02:43:51.635Z" }, + { url = "https://files.pythonhosted.org/packages/8d/9c/f20e0e2cf80e4b2e4b1c365bf5fe104ee633c751a724246262db8f1a0b13/multidict-6.7.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a90f75c956e32891a4eda3639ce6dd86e87105271f43d43442a3aedf3cddf172", size = 76893, upload-time = "2026-01-26T02:43:52.754Z" }, + { url = "https://files.pythonhosted.org/packages/fe/cf/18ef143a81610136d3da8193da9d80bfe1cb548a1e2d1c775f26b23d024a/multidict-6.7.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3fccb473e87eaa1382689053e4a4618e7ba7b9b9b8d6adf2027ee474597128cd", size = 45456, upload-time = "2026-01-26T02:43:53.893Z" }, + { url = "https://files.pythonhosted.org/packages/a9/65/1caac9d4cd32e8433908683446eebc953e82d22b03d10d41a5f0fefe991b/multidict-6.7.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b0fa96985700739c4c7853a43c0b3e169360d6855780021bfc6d0f1ce7c123e7", size = 43872, upload-time = "2026-01-26T02:43:55.041Z" }, + { url = "https://files.pythonhosted.org/packages/cf/3b/d6bd75dc4f3ff7c73766e04e705b00ed6dbbaccf670d9e05a12b006f5a21/multidict-6.7.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cb2a55f408c3043e42b40cc8eecd575afa27b7e0b956dfb190de0f8499a57a53", size = 251018, upload-time = "2026-01-26T02:43:56.198Z" }, + { url = "https://files.pythonhosted.org/packages/fd/80/c959c5933adedb9ac15152e4067c702a808ea183a8b64cf8f31af8ad3155/multidict-6.7.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eb0ce7b2a32d09892b3dd6cc44877a0d02a33241fafca5f25c8b6b62374f8b75", size = 258883, upload-time = "2026-01-26T02:43:57.499Z" }, + { url = "https://files.pythonhosted.org/packages/86/85/7ed40adafea3d4f1c8b916e3b5cc3a8e07dfcdcb9cd72800f4ed3ca1b387/multidict-6.7.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c3a32d23520ee37bf327d1e1a656fec76a2edd5c038bf43eddfa0572ec49c60b", size = 242413, upload-time = "2026-01-26T02:43:58.755Z" }, + { url = "https://files.pythonhosted.org/packages/d2/57/b8565ff533e48595503c785f8361ff9a4fde4d67de25c207cd0ba3befd03/multidict-6.7.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9c90fed18bffc0189ba814749fdcc102b536e83a9f738a9003e569acd540a733", size = 268404, upload-time = "2026-01-26T02:44:00.216Z" }, + { url = "https://files.pythonhosted.org/packages/e0/50/9810c5c29350f7258180dfdcb2e52783a0632862eb334c4896ac717cebcb/multidict-6.7.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:da62917e6076f512daccfbbde27f46fed1c98fee202f0559adec8ee0de67f71a", size = 269456, upload-time = "2026-01-26T02:44:02.202Z" }, + { url = "https://files.pythonhosted.org/packages/f3/8d/5e5be3ced1d12966fefb5c4ea3b2a5b480afcea36406559442c6e31d4a48/multidict-6.7.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bfde23ef6ed9db7eaee6c37dcec08524cb43903c60b285b172b6c094711b3961", size = 256322, upload-time = "2026-01-26T02:44:03.56Z" }, + { url = "https://files.pythonhosted.org/packages/31/6e/d8a26d81ac166a5592782d208dd90dfdc0a7a218adaa52b45a672b46c122/multidict-6.7.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3758692429e4e32f1ba0df23219cd0b4fc0a52f476726fff9337d1a57676a582", size = 253955, upload-time = "2026-01-26T02:44:04.845Z" }, + { url = "https://files.pythonhosted.org/packages/59/4c/7c672c8aad41534ba619bcd4ade7a0dc87ed6b8b5c06149b85d3dd03f0cd/multidict-6.7.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:398c1478926eca669f2fd6a5856b6de9c0acf23a2cb59a14c0ba5844fa38077e", size = 251254, upload-time = "2026-01-26T02:44:06.133Z" }, + { url = "https://files.pythonhosted.org/packages/7b/bd/84c24de512cbafbdbc39439f74e967f19570ce7924e3007174a29c348916/multidict-6.7.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:c102791b1c4f3ab36ce4101154549105a53dc828f016356b3e3bcae2e3a039d3", size = 252059, upload-time = "2026-01-26T02:44:07.518Z" }, + { url = "https://files.pythonhosted.org/packages/fa/ba/f5449385510825b73d01c2d4087bf6d2fccc20a2d42ac34df93191d3dd03/multidict-6.7.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:a088b62bd733e2ad12c50dad01b7d0166c30287c166e137433d3b410add807a6", size = 263588, upload-time = "2026-01-26T02:44:09.382Z" }, + { url = "https://files.pythonhosted.org/packages/d7/11/afc7c677f68f75c84a69fe37184f0f82fce13ce4b92f49f3db280b7e92b3/multidict-6.7.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:3d51ff4785d58d3f6c91bdbffcb5e1f7ddfda557727043aa20d20ec4f65e324a", size = 259642, upload-time = "2026-01-26T02:44:10.73Z" }, + { url = "https://files.pythonhosted.org/packages/2b/17/ebb9644da78c4ab36403739e0e6e0e30ebb135b9caf3440825001a0bddcb/multidict-6.7.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fc5907494fccf3e7d3f94f95c91d6336b092b5fc83811720fae5e2765890dfba", size = 251377, upload-time = "2026-01-26T02:44:12.042Z" }, + { url = "https://files.pythonhosted.org/packages/ca/a4/840f5b97339e27846c46307f2530a2805d9d537d8b8bd416af031cad7fa0/multidict-6.7.1-cp312-cp312-win32.whl", hash = "sha256:28ca5ce2fd9716631133d0e9a9b9a745ad7f60bac2bccafb56aa380fc0b6c511", size = 41887, upload-time = "2026-01-26T02:44:14.245Z" }, + { url = "https://files.pythonhosted.org/packages/80/31/0b2517913687895f5904325c2069d6a3b78f66cc641a86a2baf75a05dcbb/multidict-6.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcee94dfbd638784645b066074b338bc9cc155d4b4bffa4adce1615c5a426c19", size = 46053, upload-time = "2026-01-26T02:44:15.371Z" }, + { url = "https://files.pythonhosted.org/packages/0c/5b/aba28e4ee4006ae4c7df8d327d31025d760ffa992ea23812a601d226e682/multidict-6.7.1-cp312-cp312-win_arm64.whl", hash = "sha256:ba0a9fb644d0c1a2194cf7ffb043bd852cea63a57f66fbd33959f7dae18517bf", size = 43307, upload-time = "2026-01-26T02:44:16.852Z" }, + { url = "https://files.pythonhosted.org/packages/f2/22/929c141d6c0dba87d3e1d38fbdf1ba8baba86b7776469f2bc2d3227a1e67/multidict-6.7.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:2b41f5fed0ed563624f1c17630cb9941cf2309d4df00e494b551b5f3e3d67a23", size = 76174, upload-time = "2026-01-26T02:44:18.509Z" }, + { url = "https://files.pythonhosted.org/packages/c7/75/bc704ae15fee974f8fccd871305e254754167dce5f9e42d88a2def741a1d/multidict-6.7.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84e61e3af5463c19b67ced91f6c634effb89ef8bfc5ca0267f954451ed4bb6a2", size = 45116, upload-time = "2026-01-26T02:44:19.745Z" }, + { url = "https://files.pythonhosted.org/packages/79/76/55cd7186f498ed080a18440c9013011eb548f77ae1b297206d030eb1180a/multidict-6.7.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:935434b9853c7c112eee7ac891bc4cb86455aa631269ae35442cb316790c1445", size = 43524, upload-time = "2026-01-26T02:44:21.571Z" }, + { url = "https://files.pythonhosted.org/packages/e9/3c/414842ef8d5a1628d68edee29ba0e5bcf235dbfb3ccd3ea303a7fe8c72ff/multidict-6.7.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:432feb25a1cb67fe82a9680b4d65fb542e4635cb3166cd9c01560651ad60f177", size = 249368, upload-time = "2026-01-26T02:44:22.803Z" }, + { url = "https://files.pythonhosted.org/packages/f6/32/befed7f74c458b4a525e60519fe8d87eef72bb1e99924fa2b0f9d97a221e/multidict-6.7.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e82d14e3c948952a1a85503817e038cba5905a3352de76b9a465075d072fba23", size = 256952, upload-time = "2026-01-26T02:44:24.306Z" }, + { url = "https://files.pythonhosted.org/packages/03/d6/c878a44ba877f366630c860fdf74bfb203c33778f12b6ac274936853c451/multidict-6.7.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4cfb48c6ea66c83bcaaf7e4dfa7ec1b6bbcf751b7db85a328902796dfde4c060", size = 240317, upload-time = "2026-01-26T02:44:25.772Z" }, + { url = "https://files.pythonhosted.org/packages/68/49/57421b4d7ad2e9e60e25922b08ceb37e077b90444bde6ead629095327a6f/multidict-6.7.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1d540e51b7e8e170174555edecddbd5538105443754539193e3e1061864d444d", size = 267132, upload-time = "2026-01-26T02:44:27.648Z" }, + { url = "https://files.pythonhosted.org/packages/b7/fe/ec0edd52ddbcea2a2e89e174f0206444a61440b40f39704e64dc807a70bd/multidict-6.7.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:273d23f4b40f3dce4d6c8a821c741a86dec62cded82e1175ba3d99be128147ed", size = 268140, upload-time = "2026-01-26T02:44:29.588Z" }, + { url = "https://files.pythonhosted.org/packages/b0/73/6e1b01cbeb458807aa0831742232dbdd1fa92bfa33f52a3f176b4ff3dc11/multidict-6.7.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d624335fd4fa1c08a53f8b4be7676ebde19cd092b3895c421045ca87895b429", size = 254277, upload-time = "2026-01-26T02:44:30.902Z" }, + { url = "https://files.pythonhosted.org/packages/6a/b2/5fb8c124d7561a4974c342bc8c778b471ebbeb3cc17df696f034a7e9afe7/multidict-6.7.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:12fad252f8b267cc75b66e8fc51b3079604e8d43a75428ffe193cd9e2195dfd6", size = 252291, upload-time = "2026-01-26T02:44:32.31Z" }, + { url = "https://files.pythonhosted.org/packages/5a/96/51d4e4e06bcce92577fcd488e22600bd38e4fd59c20cb49434d054903bd2/multidict-6.7.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:03ede2a6ffbe8ef936b92cb4529f27f42be7f56afcdab5ab739cd5f27fb1cbf9", size = 250156, upload-time = "2026-01-26T02:44:33.734Z" }, + { url = "https://files.pythonhosted.org/packages/db/6b/420e173eec5fba721a50e2a9f89eda89d9c98fded1124f8d5c675f7a0c0f/multidict-6.7.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:90efbcf47dbe33dcf643a1e400d67d59abeac5db07dc3f27d6bdeae497a2198c", size = 249742, upload-time = "2026-01-26T02:44:35.222Z" }, + { url = "https://files.pythonhosted.org/packages/44/a3/ec5b5bd98f306bc2aa297b8c6f11a46714a56b1e6ef5ebda50a4f5d7c5fb/multidict-6.7.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5c4b9bfc148f5a91be9244d6264c53035c8a0dcd2f51f1c3c6e30e30ebaa1c84", size = 262221, upload-time = "2026-01-26T02:44:36.604Z" }, + { url = "https://files.pythonhosted.org/packages/cd/f7/e8c0d0da0cd1e28d10e624604e1a36bcc3353aaebdfdc3a43c72bc683a12/multidict-6.7.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:401c5a650f3add2472d1d288c26deebc540f99e2fb83e9525007a74cd2116f1d", size = 258664, upload-time = "2026-01-26T02:44:38.008Z" }, + { url = "https://files.pythonhosted.org/packages/52/da/151a44e8016dd33feed44f730bd856a66257c1ee7aed4f44b649fb7edeb3/multidict-6.7.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:97891f3b1b3ffbded884e2916cacf3c6fc87b66bb0dde46f7357404750559f33", size = 249490, upload-time = "2026-01-26T02:44:39.386Z" }, + { url = "https://files.pythonhosted.org/packages/87/af/a3b86bf9630b732897f6fc3f4c4714b90aa4361983ccbdcd6c0339b21b0c/multidict-6.7.1-cp313-cp313-win32.whl", hash = "sha256:e1c5988359516095535c4301af38d8a8838534158f649c05dd1050222321bcb3", size = 41695, upload-time = "2026-01-26T02:44:41.318Z" }, + { url = "https://files.pythonhosted.org/packages/b2/35/e994121b0e90e46134673422dd564623f93304614f5d11886b1b3e06f503/multidict-6.7.1-cp313-cp313-win_amd64.whl", hash = "sha256:960c83bf01a95b12b08fd54324a4eb1d5b52c88932b5cba5d6e712bb3ed12eb5", size = 45884, upload-time = "2026-01-26T02:44:42.488Z" }, + { url = "https://files.pythonhosted.org/packages/ca/61/42d3e5dbf661242a69c97ea363f2d7b46c567da8eadef8890022be6e2ab0/multidict-6.7.1-cp313-cp313-win_arm64.whl", hash = "sha256:563fe25c678aaba333d5399408f5ec3c383ca5b663e7f774dd179a520b8144df", size = 43122, upload-time = "2026-01-26T02:44:43.664Z" }, + { url = "https://files.pythonhosted.org/packages/6d/b3/e6b21c6c4f314bb956016b0b3ef2162590a529b84cb831c257519e7fde44/multidict-6.7.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:c76c4bec1538375dad9d452d246ca5368ad6e1c9039dadcf007ae59c70619ea1", size = 83175, upload-time = "2026-01-26T02:44:44.894Z" }, + { url = "https://files.pythonhosted.org/packages/fb/76/23ecd2abfe0957b234f6c960f4ade497f55f2c16aeb684d4ecdbf1c95791/multidict-6.7.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:57b46b24b5d5ebcc978da4ec23a819a9402b4228b8a90d9c656422b4bdd8a963", size = 48460, upload-time = "2026-01-26T02:44:46.106Z" }, + { url = "https://files.pythonhosted.org/packages/c4/57/a0ed92b23f3a042c36bc4227b72b97eca803f5f1801c1ab77c8a212d455e/multidict-6.7.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e954b24433c768ce78ab7929e84ccf3422e46deb45a4dc9f93438f8217fa2d34", size = 46930, upload-time = "2026-01-26T02:44:47.278Z" }, + { url = "https://files.pythonhosted.org/packages/b5/66/02ec7ace29162e447f6382c495dc95826bf931d3818799bbef11e8f7df1a/multidict-6.7.1-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3bd231490fa7217cc832528e1cd8752a96f0125ddd2b5749390f7c3ec8721b65", size = 242582, upload-time = "2026-01-26T02:44:48.604Z" }, + { url = "https://files.pythonhosted.org/packages/58/18/64f5a795e7677670e872673aca234162514696274597b3708b2c0d276cce/multidict-6.7.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:253282d70d67885a15c8a7716f3a73edf2d635793ceda8173b9ecc21f2fb8292", size = 250031, upload-time = "2026-01-26T02:44:50.544Z" }, + { url = "https://files.pythonhosted.org/packages/c8/ed/e192291dbbe51a8290c5686f482084d31bcd9d09af24f63358c3d42fd284/multidict-6.7.1-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0b4c48648d7649c9335cf1927a8b87fa692de3dcb15faa676c6a6f1f1aabda43", size = 228596, upload-time = "2026-01-26T02:44:51.951Z" }, + { url = "https://files.pythonhosted.org/packages/1e/7e/3562a15a60cf747397e7f2180b0a11dc0c38d9175a650e75fa1b4d325e15/multidict-6.7.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:98bc624954ec4d2c7cb074b8eefc2b5d0ce7d482e410df446414355d158fe4ca", size = 257492, upload-time = "2026-01-26T02:44:53.902Z" }, + { url = "https://files.pythonhosted.org/packages/24/02/7d0f9eae92b5249bb50ac1595b295f10e263dd0078ebb55115c31e0eaccd/multidict-6.7.1-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1b99af4d9eec0b49927b4402bcbb58dea89d3e0db8806a4086117019939ad3dd", size = 255899, upload-time = "2026-01-26T02:44:55.316Z" }, + { url = "https://files.pythonhosted.org/packages/00/e3/9b60ed9e23e64c73a5cde95269ef1330678e9c6e34dd4eb6b431b85b5a10/multidict-6.7.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6aac4f16b472d5b7dc6f66a0d49dd57b0e0902090be16594dc9ebfd3d17c47e7", size = 247970, upload-time = "2026-01-26T02:44:56.783Z" }, + { url = "https://files.pythonhosted.org/packages/3e/06/538e58a63ed5cfb0bd4517e346b91da32fde409d839720f664e9a4ae4f9d/multidict-6.7.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:21f830fe223215dffd51f538e78c172ed7c7f60c9b96a2bf05c4848ad49921c3", size = 245060, upload-time = "2026-01-26T02:44:58.195Z" }, + { url = "https://files.pythonhosted.org/packages/b2/2f/d743a3045a97c895d401e9bd29aaa09b94f5cbdf1bd561609e5a6c431c70/multidict-6.7.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:f5dd81c45b05518b9aa4da4aa74e1c93d715efa234fd3e8a179df611cc85e5f4", size = 235888, upload-time = "2026-01-26T02:44:59.57Z" }, + { url = "https://files.pythonhosted.org/packages/38/83/5a325cac191ab28b63c52f14f1131f3b0a55ba3b9aa65a6d0bf2a9b921a0/multidict-6.7.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:eb304767bca2bb92fb9c5bd33cedc95baee5bb5f6c88e63706533a1c06ad08c8", size = 243554, upload-time = "2026-01-26T02:45:01.054Z" }, + { url = "https://files.pythonhosted.org/packages/20/1f/9d2327086bd15da2725ef6aae624208e2ef828ed99892b17f60c344e57ed/multidict-6.7.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:c9035dde0f916702850ef66460bc4239d89d08df4d02023a5926e7446724212c", size = 252341, upload-time = "2026-01-26T02:45:02.484Z" }, + { url = "https://files.pythonhosted.org/packages/e8/2c/2a1aa0280cf579d0f6eed8ee5211c4f1730bd7e06c636ba2ee6aafda302e/multidict-6.7.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:af959b9beeb66c822380f222f0e0a1889331597e81f1ded7f374f3ecb0fd6c52", size = 246391, upload-time = "2026-01-26T02:45:03.862Z" }, + { url = "https://files.pythonhosted.org/packages/e5/03/7ca022ffc36c5a3f6e03b179a5ceb829be9da5783e6fe395f347c0794680/multidict-6.7.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:41f2952231456154ee479651491e94118229844dd7226541788be783be2b5108", size = 243422, upload-time = "2026-01-26T02:45:05.296Z" }, + { url = "https://files.pythonhosted.org/packages/dc/1d/b31650eab6c5778aceed46ba735bd97f7c7d2f54b319fa916c0f96e7805b/multidict-6.7.1-cp313-cp313t-win32.whl", hash = "sha256:df9f19c28adcb40b6aae30bbaa1478c389efd50c28d541d76760199fc1037c32", size = 47770, upload-time = "2026-01-26T02:45:06.754Z" }, + { url = "https://files.pythonhosted.org/packages/ac/5b/2d2d1d522e51285bd61b1e20df8f47ae1a9d80839db0b24ea783b3832832/multidict-6.7.1-cp313-cp313t-win_amd64.whl", hash = "sha256:d54ecf9f301853f2c5e802da559604b3e95bb7a3b01a9c295c6ee591b9882de8", size = 53109, upload-time = "2026-01-26T02:45:08.044Z" }, + { url = "https://files.pythonhosted.org/packages/3d/a3/cc409ba012c83ca024a308516703cf339bdc4b696195644a7215a5164a24/multidict-6.7.1-cp313-cp313t-win_arm64.whl", hash = "sha256:5a37ca18e360377cfda1d62f5f382ff41f2b8c4ccb329ed974cc2e1643440118", size = 45573, upload-time = "2026-01-26T02:45:09.349Z" }, + { url = "https://files.pythonhosted.org/packages/81/08/7036c080d7117f28a4af526d794aab6a84463126db031b007717c1a6676e/multidict-6.7.1-py3-none-any.whl", hash = "sha256:55d97cc6dae627efa6a6e548885712d4864b81110ac76fa4e534c03819fa4a56", size = 12319, upload-time = "2026-01-26T02:46:44.004Z" }, +] + +[[package]] +name = "nest-asyncio" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/83/f8/51569ac65d696c8ecbee95938f89d4abf00f47d58d48f6fbabfe8f0baefe/nest_asyncio-1.6.0.tar.gz", hash = "sha256:6f172d5449aca15afd6c646851f4e31e02c598d553a667e38cafa997cfec55fe", size = 7418, upload-time = "2024-01-21T14:25:19.227Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/c4/c2971a3ba4c6103a3d10c4b0f24f461ddc027f0f09763220cf35ca1401b3/nest_asyncio-1.6.0-py3-none-any.whl", hash = "sha256:87af6efd6b5e897c81050477ef65c62e2b2f35d51703cae01aff2905b1852e1c", size = 5195, upload-time = "2024-01-21T14:25:17.223Z" }, +] + +[[package]] +name = "nltk" +version = "3.9.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "joblib" }, + { name = "regex" }, + { name = "tqdm" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/74/a1/b3b4adf15585a5bc4c357adde150c01ebeeb642173ded4d871e89468767c/nltk-3.9.4.tar.gz", hash = "sha256:ed03bc098a40481310320808b2db712d95d13ca65b27372f8a403949c8b523d0", size = 2946864, upload-time = "2026-03-24T06:13:40.641Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9d/91/04e965f8e717ba0ab4bdca5c112deeab11c9e750d94c4d4602f050295d39/nltk-3.9.4-py3-none-any.whl", hash = "sha256:f2fa301c3a12718ce4a0e9305c5675299da5ad9e26068218b69d692fda84828f", size = 1552087, upload-time = "2026-03-24T06:13:38.47Z" }, +] + +[[package]] +name = "numpy" +version = "2.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d0/ad/fed0499ce6a338d2a03ebae59cd15093910c8875328855781952abf6c2fe/numpy-2.4.6.tar.gz", hash = "sha256:f3a3570c4a2a16746ac2c31a7c7c7b0c186b95ce902e33db6f28094ed7387dda", size = 20735807, upload-time = "2026-05-18T23:37:14.07Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/49/ec46835a70be8fa6446c495126ac84fdb28cb2558e1620ffb87a10c8b64c/numpy-2.4.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0280e0356c0829a18d9de1cb7eee50ec22ca639878d7240307ca0943d73cd2c4", size = 16969194, upload-time = "2026-05-18T23:33:13.503Z" }, + { url = "https://files.pythonhosted.org/packages/0e/0d/f5957185c0ee2f3e12f78715aa9e3b353fd83633316c8532b38faa37e3f6/numpy-2.4.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:110f8b71aacb688ec69062bb7f6938a0f8acb01b7c1c4beb453c65b6d234584d", size = 14964111, upload-time = "2026-05-18T23:33:17.795Z" }, + { url = "https://files.pythonhosted.org/packages/ad/40/40a40ee0ddf7ceb782c49af278894b686e586d65d8c1889c8b5da01a3d7d/numpy-2.4.6-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:4cfe66903cc32a9921a6733d96b19bb6abf310397581bbad89c228f5abaf0ee8", size = 5469159, upload-time = "2026-05-18T23:33:20.654Z" }, + { url = "https://files.pythonhosted.org/packages/63/13/f9a8046535cb21deae82f8d03de9617e08882d274fad2539630761888228/numpy-2.4.6-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:8155154c7c691289fe18f510b5d4657c68c67989f293f0535a91360392ff6538", size = 6798936, upload-time = "2026-05-18T23:33:22.987Z" }, + { url = "https://files.pythonhosted.org/packages/33/a8/6fa8c1a345a8c85dbb21932c447bee07c30a2c2a3f31e369c0a84b300147/numpy-2.4.6-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ab0a9c4ffb1a6d95ef519fe4247dba8eb6b18ad93999f76b7f657039acabd47", size = 15966692, upload-time = "2026-05-18T23:33:26.62Z" }, + { url = "https://files.pythonhosted.org/packages/02/03/74fe2a4cb3817d94d86402f2506554130a2f01414e299b5a843e5a8a957f/numpy-2.4.6-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:89cd468399cfd2504718f0ba50e410dca55a170b61a02ad92bb18c8a65186e93", size = 16918164, upload-time = "2026-05-18T23:33:29.955Z" }, + { url = "https://files.pythonhosted.org/packages/c5/80/3615be3313f7e7696609bc194b9f0101da809df79e859bdb84e0cd043f46/numpy-2.4.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c2d37ab77531417474168eb79d6d80b14f821a966818505d03013d0833edb7a8", size = 17322877, upload-time = "2026-05-18T23:33:34.724Z" }, + { url = "https://files.pythonhosted.org/packages/ca/ac/a691e0fe2675e370d0e08ff905adc49a1c8830e8cae03efe4477e92cd55d/numpy-2.4.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f407cb6b8e9d6d8c626bc73c945db1706035af8fd632295547bf1c9e46d092d6", size = 18651487, upload-time = "2026-05-18T23:33:38.217Z" }, + { url = "https://files.pythonhosted.org/packages/15/a7/9bc1cd626d7bf6869bfedf27b91b6ab5dd607758bf8e959d6fa80c6a59cb/numpy-2.4.6-cp311-cp311-win32.whl", hash = "sha256:ddea102b48f9e339f3948bf22040944184627a30fdf7f858667673b9c5f033c8", size = 6233945, upload-time = "2026-05-18T23:33:41.331Z" }, + { url = "https://files.pythonhosted.org/packages/c5/31/7fc6239c12bce7e931463251cca4426c465e1876ba3cc785402ef4dd8f4e/numpy-2.4.6-cp311-cp311-win_amd64.whl", hash = "sha256:1e254a00cdf42b1e4d5b3d68d33af63268d41340d8885df2ab6470f2e1500147", size = 12608406, upload-time = "2026-05-18T23:33:44.131Z" }, + { url = "https://files.pythonhosted.org/packages/27/83/140f85a466595a16382996a1bf06b2b54bcd597488921b0c9daaeeda72af/numpy-2.4.6-cp311-cp311-win_arm64.whl", hash = "sha256:ed9749eef4cbd126da3dc1d6bcb3a57f5eb7ac6a6484146bdbf743f552dfc577", size = 10479528, upload-time = "2026-05-18T23:33:50.725Z" }, + { url = "https://files.pythonhosted.org/packages/95/2a/3d7b5ac8aac24feaf9ad7ed58f45b0bbc06d37e4338ae84c9f2298b570f9/numpy-2.4.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:001fbb8e08d942dd57599e781f2472269ee7f2755fae407b4f67b2f0b17da3f1", size = 16689119, upload-time = "2026-05-18T23:33:54.065Z" }, + { url = "https://files.pythonhosted.org/packages/ea/12/92c4c131527599e8288d6918e888d88726f84d805d784b771f32408aeaef/numpy-2.4.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ebfb099f8dcf083deef3ac1ca4c1503f387cf76296fcb3816b66f5ecb5f54fdb", size = 14699246, upload-time = "2026-05-18T23:33:57.621Z" }, + { url = "https://files.pythonhosted.org/packages/ad/fe/c0a6b7b2ca128a8fb228575147073b660656734b8ebe4d76c8fd748dcc79/numpy-2.4.6-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:3213d622a0283a39a93d188f3cf72b26862df52fbb4ca3697f51705016523d41", size = 5204410, upload-time = "2026-05-18T23:34:00.302Z" }, + { url = "https://files.pythonhosted.org/packages/f3/d4/9770d14ba719432bb90a421bfd443872ed0f70f7264b64bec12ea363d5fd/numpy-2.4.6-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:357cc07a6d7b0b182ff02249616a03742827ebb1277546b5c7cd7f7620a45698", size = 6551240, upload-time = "2026-05-18T23:34:02.852Z" }, + { url = "https://files.pythonhosted.org/packages/c9/c6/50a46a6205feba2343f1d6d17438107c5dc491ed1c736e6ea68689fd906b/numpy-2.4.6-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f9fb9157b4ce2971008323afe46053787b526ef624fea915b261468a8421a0f", size = 15671012, upload-time = "2026-05-18T23:34:05.485Z" }, + { url = "https://files.pythonhosted.org/packages/99/60/14115e6364fa676c5397c2ad3004e527e9aa487abf5d0706ec81bbd08529/numpy-2.4.6-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:90f9849678c75fe7afa2d348ac842c168b0a4d3d61919687216dfc547976d853", size = 16645538, upload-time = "2026-05-18T23:34:09.265Z" }, + { url = "https://files.pythonhosted.org/packages/ae/c5/693cbe59e57db94d2231fa519ca3978dc9e19da5a8f088588f5c6e947ff2/numpy-2.4.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c1a2af6c6ef86344a6b0db6b97834208bf598db514f2b155042439b62605601a", size = 17020706, upload-time = "2026-05-18T23:34:13.053Z" }, + { url = "https://files.pythonhosted.org/packages/ef/fc/85b7c4eff9b4966ade25c2273cf7e7012e92366c032058653934b37de044/numpy-2.4.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e5805d5a22fd19c8ccff10a9561f9df94436b0545619ea579db2d3c35294bce2", size = 18368541, upload-time = "2026-05-18T23:34:17.024Z" }, + { url = "https://files.pythonhosted.org/packages/f6/81/e1b27545deedce7f4a0b348618c6b62d74e36a4dc9ccd42f3eb2f85eee32/numpy-2.4.6-cp312-cp312-win32.whl", hash = "sha256:e3eeb0aabd6bd5ce64faae67e9935203a6991b4bc2a485a767fbafb2c5125f45", size = 5962825, upload-time = "2026-05-18T23:34:20.3Z" }, + { url = "https://files.pythonhosted.org/packages/ab/ca/feab00bd44aa5fe1ad2c18f08b4d3bb92e26484b0b1d1443897809ed528c/numpy-2.4.6-cp312-cp312-win_amd64.whl", hash = "sha256:d8e8286dd7cea7895157318d1b91cdacac64c479f3cbc8dce548331728484751", size = 12321687, upload-time = "2026-05-18T23:34:23.095Z" }, + { url = "https://files.pythonhosted.org/packages/63/cf/5a6d34850a39d1093558564f77ee8e8e0bee5061151b8f05a55711001ec7/numpy-2.4.6-cp312-cp312-win_arm64.whl", hash = "sha256:4081eb135ac24158bd51cdfbef16f1c64df7063b1143f24731387137c092bec8", size = 10221482, upload-time = "2026-05-18T23:34:25.876Z" }, + { url = "https://files.pythonhosted.org/packages/fb/82/bdab26d7438c6791ca31b7c024ca37c1eab8b726ba236129005cd4a06e45/numpy-2.4.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:511dbaf848decaaaf4b4ca48032619fb3138710c4bf7da7617765edad1ef96b0", size = 16684648, upload-time = "2026-05-18T23:34:29.41Z" }, + { url = "https://files.pythonhosted.org/packages/1b/30/a80189bcc7f5e4258b3fbc3968d909d1756f54d023299ecc39ad6fdb9ef8/numpy-2.4.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bf162abab1c1a736333192707cef898e735a5ca00f38f27eeedf44b39d9e85eb", size = 14693902, upload-time = "2026-05-18T23:34:33.013Z" }, + { url = "https://files.pythonhosted.org/packages/97/12/70b5d0d7c15e1ebb8a6a84a8caa1d19e181d84fb58bb6d70aca29099dec1/numpy-2.4.6-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:043191bfa8eab18c776647b62723ac9dddece59743b13f49b2016094129c2b3f", size = 5198992, upload-time = "2026-05-18T23:34:36.132Z" }, + { url = "https://files.pythonhosted.org/packages/ba/8c/ebd2a8f8a83541f8d38cc5667e8c2b69cecfd30da6e45693e8158857d44b/numpy-2.4.6-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:6180d8b35af935aed8ece3a85e0a43f87393ae0ac87c8d2c8bd2c993f7270ef3", size = 6546944, upload-time = "2026-05-18T23:34:38.484Z" }, + { url = "https://files.pythonhosted.org/packages/bb/c5/7b863a97a91671a0338f4253bd3b5a3d3852f0692dae91711c9f4a10e787/numpy-2.4.6-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:72fbe16c6fac95aedf5937fa873445cec2110be35d8a4e9433d7501fd98dae6b", size = 15669392, upload-time = "2026-05-18T23:34:41.257Z" }, + { url = "https://files.pythonhosted.org/packages/a5/9d/3584b9984ca4c047aea75214ce1a4c4c73d849bd71b604264b7f5653f8a8/numpy-2.4.6-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a7830bab239b79cda9c08c2da014761cafb48da6150e1da17ac06283f43b6089", size = 16633220, upload-time = "2026-05-18T23:34:45.075Z" }, + { url = "https://files.pythonhosted.org/packages/05/ae/7c67fba23bd98caec7c99261f3a16072ade14813486b0282cb29846de832/numpy-2.4.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ef4aea96ce4d3b074422cb4f2f64e216bf9e213004bb58ecfdf50ea02ea8eb9a", size = 17020800, upload-time = "2026-05-18T23:34:49.065Z" }, + { url = "https://files.pythonhosted.org/packages/d9/5d/3b6725cb31d983c5e66916f5d36f6d7e5521129e4c4404d64f918292a5b6/numpy-2.4.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:dfa20cc6ca228e6b155b11da03825975ce66aea520985dbbddf0f2a5a495c605", size = 18357600, upload-time = "2026-05-18T23:34:52.709Z" }, + { url = "https://files.pythonhosted.org/packages/f7/da/2ccc6c2fe8898dee01d90c75c5f5f914a23daf99e3e0f59516a08760c8b5/numpy-2.4.6-cp313-cp313-win32.whl", hash = "sha256:56b39e5e0622a09a25bf5baf62f4bcf0cb8a41ae6e2819cf49bbc5a74c083f91", size = 5961134, upload-time = "2026-05-18T23:34:55.618Z" }, + { url = "https://files.pythonhosted.org/packages/b5/cd/9cc4dc876fb065d5c220aae4d5e14826b2715331bb7618ce1fb07a679d99/numpy-2.4.6-cp313-cp313-win_amd64.whl", hash = "sha256:c4fc99836233ea196540b17ab0983aff60ed07941751930f5f4d05bc3b3b7359", size = 12318598, upload-time = "2026-05-18T23:34:58.928Z" }, + { url = "https://files.pythonhosted.org/packages/39/1e/c0bcba1f8694116485fe28fd1be698c278fcda4141c5b0e53a2aed8b12a8/numpy-2.4.6-cp313-cp313-win_arm64.whl", hash = "sha256:a7c711e21628b52034bb5ab8d1bce291f752fcc5e92accc615778acee1ff4778", size = 10222272, upload-time = "2026-05-18T23:35:02.167Z" }, + { url = "https://files.pythonhosted.org/packages/63/6d/cc5619247c8f4204e507f5883528372e4ac4bb189e579fb859a12e480b1f/numpy-2.4.6-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:112b06a867b235ef466ed3508ddf0238050df9c727cafb5301ac385b899189a1", size = 14821197, upload-time = "2026-05-18T23:35:05.468Z" }, + { url = "https://files.pythonhosted.org/packages/00/58/f1c39161c87d9e9bed660f1ed4bafc0e403d5ec9650b6dd77aead07d489b/numpy-2.4.6-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:eaf7fa2de5c0be8ae6ff8e9bea2ccd725e980541244521d8d4b5f3354a27babe", size = 5326287, upload-time = "2026-05-18T23:35:08.693Z" }, + { url = "https://files.pythonhosted.org/packages/af/57/3917ab0fd97f271a8694513581b8a36c655f111c446852c302f04ccdb6fc/numpy-2.4.6-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:7265a2f3d436e54ef9f2b52b5c937e6be778781bd97a590319d7348f1c1ca997", size = 6646763, upload-time = "2026-05-18T23:35:11.459Z" }, + { url = "https://files.pythonhosted.org/packages/eb/0f/037e64c494b67581ae18193d770adef354c41f3f2c8ebf865602d949bf8f/numpy-2.4.6-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f74a575920ab21fe304421a3fc28793d82e299cae9eccb37084e9fc7f3617c20", size = 15728070, upload-time = "2026-05-18T23:35:14.79Z" }, + { url = "https://files.pythonhosted.org/packages/21/a6/5d2bae9c9542eb4df16dc9c46dc79c186e9bad53805dfa5399a6023c6db0/numpy-2.4.6-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ede83e07a75dd06bc501566c1eca2afc0d61677c1472ac9ad93fdee6e638a48d", size = 16681752, upload-time = "2026-05-18T23:35:18.836Z" }, + { url = "https://files.pythonhosted.org/packages/92/14/23d1dfb410ae362cd59ce53e936b1513d545eb40db3949ced632e19a459e/numpy-2.4.6-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:68bb27509ac1b9a3443094260f6326150663b06abe40b73a2f81160623da5b67", size = 17086024, upload-time = "2026-05-18T23:35:22.52Z" }, + { url = "https://files.pythonhosted.org/packages/4b/6e/23595a2c642cdf3bc567877064bdd7f91c8b0038a4453cf2daf7248eafe9/numpy-2.4.6-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:a0df0043bdb289bde1f62da130d20df23d58b45429f752bc7a8fc5325a225ecd", size = 18403398, upload-time = "2026-05-18T23:35:26.398Z" }, + { url = "https://files.pythonhosted.org/packages/8a/90/0ac3bc947217e66dec77e7cbc6a1979d1af70b6461b82f620d3bccd5e4c8/numpy-2.4.6-cp313-cp313t-win32.whl", hash = "sha256:29a287e0cf63ff528da061de6b9f64a4618da591ca1046aafc54062e40ca7eab", size = 6084971, upload-time = "2026-05-18T23:35:29.387Z" }, + { url = "https://files.pythonhosted.org/packages/77/71/5673e351671a1d2bd6063b91b44f70c0affea7d1516fa7a6572941ba4aa1/numpy-2.4.6-cp313-cp313t-win_amd64.whl", hash = "sha256:25c692919ac5a01f170a3bfcd62d745b24fd095c353d50812637d6fcab442e75", size = 12458532, upload-time = "2026-05-18T23:35:32.175Z" }, + { url = "https://files.pythonhosted.org/packages/3f/88/19d3503c5046e688f049274b27a3ef3d771152fa80d3ba3d01a3dff61abe/numpy-2.4.6-cp313-cp313t-win_arm64.whl", hash = "sha256:1e978ec1e8bd0e0e4de6bb75de9d30cbb74db6b6a2bb727618613703ca0167dd", size = 10291881, upload-time = "2026-05-18T23:35:35.465Z" }, + { url = "https://files.pythonhosted.org/packages/de/12/b422cc84439adc0d00de605bf4a308890ae5c26f2c71fbd73e5d08fbb0dd/numpy-2.4.6-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:55cced7c52e981362f708ad635198e97a752dfba412cc03c23bbf3bd8d5cd662", size = 16847511, upload-time = "2026-05-18T23:36:50.673Z" }, + { url = "https://files.pythonhosted.org/packages/44/53/f481bef68011740f8849418d82db07230e825013f31f4eef5ba5b805316a/numpy-2.4.6-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d6da64deb6b8ed903e7560180a92f2d804ee1ba5eeb849ac2748b8c1aba1f6d7", size = 14889064, upload-time = "2026-05-18T23:36:53.879Z" }, + { url = "https://files.pythonhosted.org/packages/7f/57/42ed575c10ced8af951d426bc4e1f8aff16fd851db33f067036215a7f860/numpy-2.4.6-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:68a5124b13fa6cc2086764a20005d30bc0548146f7f5322f02fce212ca14317f", size = 5394157, upload-time = "2026-05-18T23:36:57.194Z" }, + { url = "https://files.pythonhosted.org/packages/6a/ef/f66cc724fcc36c1e364c67f51ae9146090b8b584f27d58b97fdae3edd737/numpy-2.4.6-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:948424b06129ce883307e8cff868c31396d8dc7630a59c61d70d98dbe70f222c", size = 6708728, upload-time = "2026-05-18T23:36:59.575Z" }, + { url = "https://files.pythonhosted.org/packages/1a/9c/c531f2293b91265d8b48e9b329f54fdd7ffae73cb4134ea10cca4237e9cc/numpy-2.4.6-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5dbbdb29840ca3d91ee0fece42fc29278886d908280bfec0a5846c6f901a3eb0", size = 15798374, upload-time = "2026-05-18T23:37:02.674Z" }, + { url = "https://files.pythonhosted.org/packages/1a/b0/413077f6b1153ed3cba361401c6783bbad6114804a000cc22eb71c13e190/numpy-2.4.6-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8ad03c0965fb3c692200e74d458ca28c1dbb4ce96f9a479a8aa041ad5fabca02", size = 16747286, upload-time = "2026-05-18T23:37:06.327Z" }, + { url = "https://files.pythonhosted.org/packages/15/ce/e5ec180bc41812edcd8daeb8639d205622c0e8c02259d8ab25a0201b3c2a/numpy-2.4.6-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:2803abfebfc990042cd494d8ce2d5f82e9d847af6d35ec486923aa19dbad5e73", size = 12504263, upload-time = "2026-05-18T23:37:09.715Z" }, +] + +[[package]] +name = "openai" +version = "2.24.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "distro" }, + { name = "httpx" }, + { name = "jiter" }, + { name = "pydantic" }, + { name = "sniffio" }, + { name = "tqdm" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/13/17e87641b89b74552ed408a92b231283786523edddc95f3545809fab673c/openai-2.24.0.tar.gz", hash = "sha256:1e5769f540dbd01cb33bc4716a23e67b9d695161a734aff9c5f925e2bf99a673", size = 658717, upload-time = "2026-02-24T20:02:07.958Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c9/30/844dc675ee6902579b8eef01ed23917cc9319a1c9c0c14ec6e39340c96d0/openai-2.24.0-py3-none-any.whl", hash = "sha256:fed30480d7d6c884303287bde864980a4b137b60553ffbcf9ab4a233b7a73d94", size = 1120122, upload-time = "2026-02-24T20:02:05.669Z" }, +] + +[[package]] +name = "opentelemetry-api" +version = "1.41.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "importlib-metadata" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fa/fc/b7564cbef36601aef0d6c9bc01f7badb64be8e862c2e1c3c5c3b43b53e4f/opentelemetry_api-1.41.1.tar.gz", hash = "sha256:0ad1814d73b875f84494387dae86ce0b12c68556331ce6ce8fe789197c949621", size = 71416, upload-time = "2026-04-24T13:15:38.262Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/29/59/3e7118ed140f76b0982ba4321bdaed1997a0473f9720de2d10788a577033/opentelemetry_api-1.41.1-py3-none-any.whl", hash = "sha256:a22df900e75c76dc08440710e51f52f1aa6b451b429298896023e60db5b3139f", size = 69007, upload-time = "2026-04-24T13:15:15.662Z" }, +] + +[[package]] +name = "opentelemetry-exporter-gcp-logging" +version = "1.12.0a0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "google-cloud-logging" }, + { name = "opentelemetry-api" }, + { name = "opentelemetry-resourcedetector-gcp" }, + { name = "opentelemetry-sdk" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/77/e4/95ecebaa1c5134adaa0d0374028b25e3b3c5c08535d29a66d39d372a3d11/opentelemetry_exporter_gcp_logging-1.12.0a0.tar.gz", hash = "sha256:586529dbbcae5e22b880f7c121fde3f0fe8ae997aba1bad53f13c20eeb27cb3a", size = 22521, upload-time = "2026-04-28T20:59:40.237Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/55/93/3a0a9a62db0b90029a8160774e791044c0566aa94d5160ce7bbce8abf242/opentelemetry_exporter_gcp_logging-1.12.0a0-py3-none-any.whl", hash = "sha256:2aca9b01b3248c2fa95d38d01aa71aca8e22f640c44dba36ca6b883930762971", size = 14207, upload-time = "2026-04-28T20:59:35.109Z" }, +] + +[[package]] +name = "opentelemetry-exporter-gcp-monitoring" +version = "1.12.0a0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "google-cloud-monitoring" }, + { name = "opentelemetry-api" }, + { name = "opentelemetry-resourcedetector-gcp" }, + { name = "opentelemetry-sdk" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ff/5d/f82b2858d00be6f91b917dc67ccf71688fa822448b2d26ace69b809f5835/opentelemetry_exporter_gcp_monitoring-1.12.0a0.tar.gz", hash = "sha256:2b285078cddd4af78a363a55b5478e89f7df6f15bba9139d3f484099e534df4c", size = 20839, upload-time = "2026-04-28T20:59:40.982Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ae/b5/1623886d049095bb5abcec0cd67a0e40c00ff1672a25f82ed9867f88c1e7/opentelemetry_exporter_gcp_monitoring-1.12.0a0-py3-none-any.whl", hash = "sha256:1a7daf8c9350d55010fa33d2c2f646655a03a81d0d8073a2ae0e066791d6177d", size = 13608, upload-time = "2026-04-28T20:59:36.315Z" }, +] + +[[package]] +name = "opentelemetry-exporter-gcp-trace" +version = "1.12.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "google-cloud-trace" }, + { name = "opentelemetry-api" }, + { name = "opentelemetry-resourcedetector-gcp" }, + { name = "opentelemetry-sdk" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bf/55/32922e72d88421505383dfdba9c1ee6ad67253f94f2358f6e9dbc4ac3749/opentelemetry_exporter_gcp_trace-1.12.0.tar.gz", hash = "sha256:18c6e56fe123eed020d5005fdd819b196d64f651545bce1ca7e2e2cbaf9d343b", size = 18779, upload-time = "2026-04-28T20:59:41.974Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8c/68/c60e79992918eecb6de167e782c86946fdd5492bb163fe320f1a18959c3d/opentelemetry_exporter_gcp_trace-1.12.0-py3-none-any.whl", hash = "sha256:1538dab654bcb25e757ed34c94f27a2e30d90dc7deb3630f8d46d1111fcb3bad", size = 14013, upload-time = "2026-04-28T20:59:37.518Z" }, +] + +[[package]] +name = "opentelemetry-exporter-otlp-proto-common" +version = "1.41.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-proto" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ae/fa/f9e3bd3c4d692b3ce9a2880a167d1f79681a1bea11f00d5bf76adc03e6ea/opentelemetry_exporter_otlp_proto_common-1.41.1.tar.gz", hash = "sha256:0e253156ea9c36b0bd3d2440c5c9ba7dd1f3fb64ba7a08fc85fbac536b56e1fb", size = 20409, upload-time = "2026-04-24T13:15:40.924Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/29/48/bce76d3ea772b609757e9bc844e02ab408a6446609bf74fb562062ba6b71/opentelemetry_exporter_otlp_proto_common-1.41.1-py3-none-any.whl", hash = "sha256:10da74dad6a49344b9b7b21b6182e3060373a235fde1528616d5f01f92e66aa9", size = 18366, upload-time = "2026-04-24T13:15:18.917Z" }, +] + +[[package]] +name = "opentelemetry-exporter-otlp-proto-http" +version = "1.41.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "googleapis-common-protos" }, + { name = "opentelemetry-api" }, + { name = "opentelemetry-exporter-otlp-proto-common" }, + { name = "opentelemetry-proto" }, + { name = "opentelemetry-sdk" }, + { name = "requests" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/33/5b/9d3c7f70cca10136ba82a81e738dee626c8e7fc61c6887ea9a58bf34c606/opentelemetry_exporter_otlp_proto_http-1.41.1.tar.gz", hash = "sha256:4747a9604c8550ab38c6fd6180e2fcb80de3267060bef2c306bad3cb443302bc", size = 24139, upload-time = "2026-04-24T13:15:42.977Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ba/4d/ef07ff2fc630849f2080ae0ae73a61f67257905b7ac79066640bfa0c5739/opentelemetry_exporter_otlp_proto_http-1.41.1-py3-none-any.whl", hash = "sha256:1a21e8f49c7a946d935551e90947d6c3eb39236723c6624401da0f33d68edcb4", size = 22673, upload-time = "2026-04-24T13:15:21.313Z" }, +] + +[[package]] +name = "opentelemetry-instrumentation" +version = "0.62b1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-semantic-conventions" }, + { name = "packaging" }, + { name = "wrapt" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/52/cb/0523b92c112a6cc70be43724343dc45225d3af134419844d7879a07755d4/opentelemetry_instrumentation-0.62b1.tar.gz", hash = "sha256:90e92a905ba4f84db06ac3aec96701df6c079b2d66e9379f8739f0a1bdcc7f45", size = 34043, upload-time = "2026-04-24T13:22:31.997Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4d/0f/45adbaea1f81b847cffdcee4f4b5f89297e42facf7fac78c7aaac4c38e75/opentelemetry_instrumentation-0.62b1-py3-none-any.whl", hash = "sha256:976fc6e640f2006599e97429c949e622c108d0c17c2059347d1e6c93c707f257", size = 34163, upload-time = "2026-04-24T13:21:31.722Z" }, +] + +[[package]] +name = "opentelemetry-instrumentation-google-genai" +version = "0.7b1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-instrumentation" }, + { name = "opentelemetry-semantic-conventions" }, + { name = "opentelemetry-util-genai" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5a/ca/da1a8d598542a3bd343859efc6e6e89b74b5189887b8f7ef447267e5e9ed/opentelemetry_instrumentation_google_genai-0.7b1.tar.gz", hash = "sha256:dc81249eb8a01184e2b3e0eb207864158e7147e3307b018dc9625b23973457a5", size = 52078, upload-time = "2026-05-19T01:46:07.689Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/52/59/6c89d9c1c72ea321b2e3f6557fa58add59c61d0ba483d90955a7d9af4ec3/opentelemetry_instrumentation_google_genai-0.7b1-py3-none-any.whl", hash = "sha256:b7b3cda48efc9b9550dee76e4437a2f5e3b6877abffc4a6c336966eceb00f8b8", size = 31414, upload-time = "2026-05-19T01:46:06.393Z" }, +] + +[[package]] +name = "opentelemetry-proto" +version = "1.41.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/99/e8/633c6d8a9c8840338b105907e55c32d3da1983abab5e52f899f72a82c3d1/opentelemetry_proto-1.41.1.tar.gz", hash = "sha256:4b9d2eb631237ea43b80e16c073af438554e32bc7e9e3f8ca4a9582f900020e5", size = 45670, upload-time = "2026-04-24T13:15:49.768Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e4/1e/5cd77035e3e82070e2265a63a760f715aacd3cb16dddc7efee913f297fcc/opentelemetry_proto-1.41.1-py3-none-any.whl", hash = "sha256:0496713b804d127a4147e32849fbaf5683fac8ee98550e8e7679cd706c289720", size = 72076, upload-time = "2026-04-24T13:15:32.542Z" }, +] + +[[package]] +name = "opentelemetry-resourcedetector-gcp" +version = "1.12.0a0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-sdk" }, + { name = "requests" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/21/ae/b62c5e986c9c7f908a15682ea173bcfcdc00403c0c85243ccbd30eca7fc2/opentelemetry_resourcedetector_gcp-1.12.0a0.tar.gz", hash = "sha256:d5e3f78283a272eb92547e00bbeff45b7332a34ae791a70ab4eba81af9bc3baf", size = 18797, upload-time = "2026-04-28T20:59:43.195Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/84/9db2999adbc41505af3e6717e8d958746778cbfc9e07ed9c670bf9d1e6db/opentelemetry_resourcedetector_gcp-1.12.0a0-py3-none-any.whl", hash = "sha256:e803688d14e2969fe816077be81f7b034368314d485863f12ce49daba7c81919", size = 18798, upload-time = "2026-04-28T20:59:39.257Z" }, +] + +[[package]] +name = "opentelemetry-sdk" +version = "1.41.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-semantic-conventions" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/58/d0/54ee30dab82fb0acda23d144502771ff76ef8728459c83c3e89ef9fb1825/opentelemetry_sdk-1.41.1.tar.gz", hash = "sha256:724b615e1215b5aeacda0abb8a6a8922c9a1853068948bd0bd225a56d0c792e6", size = 230180, upload-time = "2026-04-24T13:15:50.991Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b4/e7/a1420b698aad018e1cf60fdbaaccbe49021fb415e2a0d81c242f4c518f54/opentelemetry_sdk-1.41.1-py3-none-any.whl", hash = "sha256:edee379c126c1bce952b0c812b48fe8ff35b30df0eecf17e98afa4d598b7d85d", size = 180213, upload-time = "2026-04-24T13:15:33.767Z" }, +] + +[[package]] +name = "opentelemetry-semantic-conventions" +version = "0.62b1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9e/de/911ac9e309052aca1b20b2d5549d3db45d1011e1a610e552c6ccdd1b64f8/opentelemetry_semantic_conventions-0.62b1.tar.gz", hash = "sha256:c5cc6e04a7f8c7cdd30be2ed81499fa4e75bfbd52c9cb70d40af1f9cd3619802", size = 145750, upload-time = "2026-04-24T13:15:52.236Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/eb/a6/83dc2ab6fa397ee66fba04fe2e74bdf7be3b3870005359ceb7689103c058/opentelemetry_semantic_conventions-0.62b1-py3-none-any.whl", hash = "sha256:cf506938103d331fbb78eded0d9788095f7fd59016f2bda813c3324e5a74a93c", size = 231620, upload-time = "2026-04-24T13:15:35.454Z" }, +] + +[[package]] +name = "opentelemetry-util-genai" +version = "0.3b0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-instrumentation" }, + { name = "opentelemetry-semantic-conventions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a2/d8/4dd2fb622d26ec45b10ef63eb87fd512f5d7467c7bd35ce390629bd6dff8/opentelemetry_util_genai-0.3b0.tar.gz", hash = "sha256:83e127789a9ad615b8ca65f05fc36955a67ce257b06142bfd46159a3b7ed73d3", size = 31800, upload-time = "2026-02-20T16:16:14.807Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/e5/fada54909e445d7b4007f8b96221d571999efeab9446f3127cc1cebe5e07/opentelemetry_util_genai-0.3b0-py3-none-any.whl", hash = "sha256:ebc2b01bcb891ddc7218452470d189d3321cd742653299ff8e7de45debcfb986", size = 28426, upload-time = "2026-02-20T16:16:12.027Z" }, +] + +[[package]] +name = "packaging" +version = "26.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", size = 228134, upload-time = "2026-04-24T20:15:23.917Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" }, +] + +[[package]] +name = "pandas" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "python-dateutil" }, + { name = "tzdata", marker = "sys_platform == 'emscripten' or sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f8/87/4341c6252d1c47b08768c3d25ac487362bf403f0313ddae4a2a26c9b1b4c/pandas-3.0.3.tar.gz", hash = "sha256:696a4a00a2a2a35d4e5deb3fc946641b96c944f02230e4f76137fe35d806c4fc", size = 4651414, upload-time = "2026-05-11T18:54:29.21Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/42/16/b5c76b838fd9bf6ce84d3a53346b8874ec05c5f0040d75ef2c320100cd2a/pandas-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:455f6f8139d4282188f526868dbc3c828470e88a3d9d59a891bd46a455f21b98", size = 10338495, upload-time = "2026-05-11T18:52:11.558Z" }, + { url = "https://files.pythonhosted.org/packages/5a/b0/a4ffc4ae74d2d822200dcc46898987d8eb6032d1e2b219cae39da6f5cbcc/pandas-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4e15135e2ee5df1063313e2425ceef8ac0f4ae775893815b0923651b806a5639", size = 9938250, upload-time = "2026-05-11T18:52:17.005Z" }, + { url = "https://files.pythonhosted.org/packages/2e/b2/3323601a52caee42c019e370090ca4544b241437240ca04f786cce82b0cf/pandas-3.0.3-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:05f1f1752b8533ea03f7f39a9c15b1a058d067bb48f4748948e7a8691e0510f2", size = 10770558, upload-time = "2026-05-11T18:52:19.865Z" }, + { url = "https://files.pythonhosted.org/packages/32/f1/bbecd2f867b97abebe0f9b53d750f862251b40337e061b36676ded3d920f/pandas-3.0.3-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8a1e45c80cceb3b4a21bc5939d52e8cbd8d9b7305309219d59e9754d9ce09e27", size = 11274611, upload-time = "2026-05-11T18:52:22.622Z" }, + { url = "https://files.pythonhosted.org/packages/7f/4f/eafabf2d5fae5adf143b4d18d3706c5efdc368a7c4eb1ee8a3eddabbd0f6/pandas-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:14da8316da4d0c5a77618425996bfb1248ca87fc2c1486e6fde4652bd18b5824", size = 11784670, upload-time = "2026-05-11T18:52:25.4Z" }, + { url = "https://files.pythonhosted.org/packages/49/44/1eb20389301b57b19cc099a1c2f662501f72f08a65f912d05822613c1532/pandas-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a55066a0505dae0ba2b50a46637db34b46f9094c65c5d4800794ef6335010938", size = 12353708, upload-time = "2026-05-11T18:52:28.139Z" }, + { url = "https://files.pythonhosted.org/packages/eb/62/c321f13b5ba1819fc8dca456c7fce578da2dcfecff1abbf0eaddf8406c0f/pandas-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:6674ab18ad8c57802867264b00e15e7bb904700cdd9046e3b2fa1fce237439ea", size = 9907609, upload-time = "2026-05-11T18:52:30.982Z" }, + { url = "https://files.pythonhosted.org/packages/53/85/1b7f563ebc6357c27233a02a96b589bcce1fa9c6eb89fb4f0e56421d277e/pandas-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:5cc09a68b3120e0f54870dede8287a7bb1fa463907e4fcec1ea77cab6179bf7a", size = 9165596, upload-time = "2026-05-11T18:52:33.334Z" }, + { url = "https://files.pythonhosted.org/packages/24/f1/392f8c5bfc16f66a0d2d41561c01627c228fe7ed2a0d056ef11315042570/pandas-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fed2ff7fd9779120e388e285fc029bd5cf9490cdd2e4166a9ee22c0e49a9ab09", size = 10357846, upload-time = "2026-05-11T18:52:36.143Z" }, + { url = "https://files.pythonhosted.org/packages/cf/3d/b16412745651e855f357e5e66930248688378853a6e2698a214e331fba1f/pandas-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b168fc218fd80a6cbdbdbc1a97ddc7889ed057d7eb45f50d866ceab5f39904c4", size = 9899550, upload-time = "2026-05-11T18:52:38.976Z" }, + { url = "https://files.pythonhosted.org/packages/31/a8/fa2535168fffcedf67f4f6de28d2dd903a747ca7c8ea6989451aaeb3a92f/pandas-3.0.3-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0383c72c75cdcca61a9e116e611143902dbfd08bff356829c2f6d1cf40a9ca8c", size = 10412965, upload-time = "2026-05-11T18:52:41.915Z" }, + { url = "https://files.pythonhosted.org/packages/65/b6/09b01cdbc15224e2850365192d17b7bdebb8bdbd8780ed221fcdf0d9a515/pandas-3.0.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6dc0b3fd2169c9157deed50b4d519553a3655c8c6a96027136d654592be973a9", size = 10894600, upload-time = "2026-05-11T18:52:45.02Z" }, + { url = "https://files.pythonhosted.org/packages/c9/a4/2eb28f2fccb4ced4a2c79ab2a5dee9ade1ebf44922ebad6fea158c9f95d4/pandas-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7e65d5407dc0b394f509699650e4a2ec01c0514f21850f453fa60f3be79a5dbf", size = 11422824, upload-time = "2026-05-11T18:52:48.058Z" }, + { url = "https://files.pythonhosted.org/packages/f8/45/830bb57f533a4604b355e07edcb8ea18cf88b5f94e5fca92f27052d7c597/pandas-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f8894dc474d648fe7b6ff0ca9b0bd73950d19952bc1a6534540762c5d79d305c", size = 11950889, upload-time = "2026-05-11T18:52:50.905Z" }, + { url = "https://files.pythonhosted.org/packages/b9/c5/fc1b368f303087d20e8c9bf3d6ceb186263cfac0ade735cd938538bea839/pandas-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:c7be265b62cef88e253a941e4698604973736dcfe242fdb5198f0f7bc473cdcc", size = 9755463, upload-time = "2026-05-11T18:52:53.386Z" }, + { url = "https://files.pythonhosted.org/packages/86/bd/fda8f9705b1b09c6ebe14bfc0fa0e4ec8584d54ea673628f157ff55131af/pandas-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:557409bc4178e70ee8d9ddb494798e51ebf6ea59330f6be22c51bab2a7db6c49", size = 9066158, upload-time = "2026-05-11T18:52:56.038Z" }, + { url = "https://files.pythonhosted.org/packages/c5/90/62d8302883c44308c477e222c3daf7c813a34c8e96985882fbd53d964352/pandas-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:67b3b64c11910cfa29f4e94a14d3bff9ee693b6fc76055e7cad549cee0aec5fa", size = 10331071, upload-time = "2026-05-11T18:52:58.838Z" }, + { url = "https://files.pythonhosted.org/packages/7f/ae/6a6493c783a101f165e4356953ba3c74d6f77f0042fa7d753da9dfbb640c/pandas-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:39436b377d56d2a2e52d0395bdbee171f01068e99af5250509aceeb929f765c7", size = 9875690, upload-time = "2026-05-11T18:53:01.431Z" }, + { url = "https://files.pythonhosted.org/packages/62/7c/5df8e9f56c69a2769fbe9382a5ef8f2658c007e376434e1e2cbb57ad895f/pandas-3.0.3-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d4be06d68f9ddcfc645b87534911da79a8fbffc7573c80e0edcf42a5020624d8", size = 10381634, upload-time = "2026-05-11T18:53:04.393Z" }, + { url = "https://files.pythonhosted.org/packages/99/68/1237369725aa617bb358263d535803e3053fdbc593513ec5ed9c9896b5b6/pandas-3.0.3-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a4eeb6830daf35a71cc09649bd823e2b542dac246cdee9614c6e4bd65028cd6a", size = 10891243, upload-time = "2026-05-11T18:53:07.643Z" }, + { url = "https://files.pythonhosted.org/packages/25/93/77d108e8af7222b4a503ebde0e30215b1c2e4f8e53a526431890f22d5586/pandas-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1928e07221f82db493cd4af1e23c1bfca524a19a4699887975bff68f49a72bfb", size = 11388659, upload-time = "2026-05-11T18:53:10.634Z" }, + { url = "https://files.pythonhosted.org/packages/d0/bd/eff5b4399f332ac386c853f6cd2bd3fa2ca0061b9f36ecd9c4d7c4265649/pandas-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51b1fe551acb77dac643c6fda86084d8d446c10fe64b06a9cc29c4cc8540e7f2", size = 11942880, upload-time = "2026-05-11T18:53:13.536Z" }, + { url = "https://files.pythonhosted.org/packages/2c/20/559ace4200982c3887d0b86bfd0d856a2143ef8ddab63cc07934951a964c/pandas-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:a82d532a3351d435432cd913edbccaf8b8e01d4dd0e5ced5a8d2e8ecd94c7e44", size = 9757091, upload-time = "2026-05-11T18:53:16.306Z" }, + { url = "https://files.pythonhosted.org/packages/3a/66/69055a09fe200f29f922a3eeec4804611900b95f52d932ece3393c3c0c19/pandas-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:275c14e0fce14a2ec20eee474aecd305478ea3c1e6f6a9d8fe219a165542717e", size = 9057282, upload-time = "2026-05-11T18:53:18.768Z" }, + { url = "https://files.pythonhosted.org/packages/57/0e/efe801b0e6811e8e650cd21b7f2608e30f08a7067e2bf6e8752b0d56ee3c/pandas-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:46997386d528eb40376ecd6b033cf4a8a1e5282580f68f43de875b78cba2199d", size = 10767016, upload-time = "2026-05-11T18:53:21.227Z" }, + { url = "https://files.pythonhosted.org/packages/ea/dc/eb55135a1d5f0f0519f28da1f609a206d2cad1f9c35c32d51e38dd7261ae/pandas-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:261e308dfb22448384b7580cf719d2f998fe2966c92893c3e77d14008af1f066", size = 10420210, upload-time = "2026-05-11T18:53:23.982Z" }, + { url = "https://files.pythonhosted.org/packages/c6/3e/b1d5d955ce33ffecb407465a60bc32769d74fcf68224b7ae67ae11d4dea4/pandas-3.0.3-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dd1a5d1def6a46002e964510bdc67c368aa0951df5d1d9f8365336f5a1f490cd", size = 10336126, upload-time = "2026-05-11T18:53:26.731Z" }, + { url = "https://files.pythonhosted.org/packages/f5/76/a01261711ab60a22d71b862f0de20e4c504bf80457270ad8cb42110f6abc/pandas-3.0.3-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d72828c20c6d6e83e1e22a6a3b47b326b71664112fa9705dcbccfd7a39b62085", size = 10728051, upload-time = "2026-05-11T18:53:29.125Z" }, + { url = "https://files.pythonhosted.org/packages/e9/21/ea191195e587b18cf682e97f433f81b2d0fbe341380e80a3e0d6e4403c8e/pandas-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:d26cbe1fcfc12e8fd900e2454163e466b2d3af84f7c75481df7683ffc073d870", size = 11350796, upload-time = "2026-05-11T18:53:32.056Z" }, + { url = "https://files.pythonhosted.org/packages/64/69/f0eaaf54939f0e8c6768fd06be9af2cef9b36048b96dfb9e1b2c685a807e/pandas-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:3e91cec1879ada0624fc3dc9953c5cbd60208e59c0db28f540c5d6d47502422f", size = 11799741, upload-time = "2026-05-11T18:53:34.985Z" }, + { url = "https://files.pythonhosted.org/packages/45/a4/865e0e510cae5fc2194de4db28be638952de942571ba9125934fd9c01d47/pandas-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:08d789b41f87e0905880e293cedf6197ce71fe67cc081358b1e148a491b9bd13", size = 10499958, upload-time = "2026-05-11T18:53:37.857Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "propcache" +version = "0.5.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ec/44/c87281c333769159c50594f22610f77398a47ccbfbbf23074e744e86f87c/propcache-0.5.2.tar.gz", hash = "sha256:01c4fc7480cd0598bb4b57022df55b9ca296da7fc5a8760bd8451a7e63a7d427", size = 50208, upload-time = "2026-05-08T21:02:12.199Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/f1/8a8cc1c2c7e7934ab77e0163414f736fadbc0f5e8dd9673b952355ac175b/propcache-0.5.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:74b70780220e2dd89175ca24b81b68b67c83db499ae611e7f2313cb329801c78", size = 90744, upload-time = "2026-05-08T20:59:45.799Z" }, + { url = "https://files.pythonhosted.org/packages/c2/f4/651b1225e976bd1a2ba5cfba0c29d096581c2636b437e3a9a7ab6276270a/propcache-0.5.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a4840ab0ae0216d952f4b53dc6d0b992bfc2bedbfe360bdd9b548bc184c08959", size = 52033, upload-time = "2026-05-08T20:59:47.408Z" }, + { url = "https://files.pythonhosted.org/packages/15/a8/8ede85d6aa1f79fc7dc2f8fd2c8d65920b8272c3892903c8a1affde48cfb/propcache-0.5.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c6844ba6364fb12f403928a82cfd295ab103a2b315c77c747b2dbe4a41894ea7", size = 52754, upload-time = "2026-05-08T20:59:49.202Z" }, + { url = "https://files.pythonhosted.org/packages/7d/fe/b3551b41bbc2f5b5bb088fc6920567cd43101253e68fbaa261339eb96fe1/propcache-0.5.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2293949b855ce597f2826452d17c2d545fb5622379c4ea6fdf525e9b8e8a2511", size = 57573, upload-time = "2026-05-08T20:59:50.778Z" }, + { url = "https://files.pythonhosted.org/packages/83/27/ab851ebd1b7172e3e161f5f8d39e315d54a91bea246f01f4d872d3376aef/propcache-0.5.2-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0fd59b5af35f74da48d905dcbad55449ba13be91823cb05a9bd590bbf5b61660", size = 60645, upload-time = "2026-05-08T20:59:52.227Z" }, + { url = "https://files.pythonhosted.org/packages/95/7d/466b3d18022e9897cbda9c735c493c5bd747d7a4c6f5ea1480b4cec434b6/propcache-0.5.2-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29f9309a2e42b0d273be006fdb4be2d6c39a47f6f57d8fb1cf9f81481df81b66", size = 61563, upload-time = "2026-05-08T20:59:53.866Z" }, + { url = "https://files.pythonhosted.org/packages/27/1b/16ab7f2cf2041da2f60d156ba64c2484eadf9168075b4ff43c3ef60045af/propcache-0.5.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5aaa2b923c1944ac8febd6609cb373540a5563e7cbcb0fd770f75dace2eb817b", size = 58888, upload-time = "2026-05-08T20:59:55.457Z" }, + { url = "https://files.pythonhosted.org/packages/0a/67/bb777ffd907633563bf35fd859c4ce97b0512c32f4633cf5d1eb7c33512b/propcache-0.5.2-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:66ea454f095ddf5b6b14f56c064c0941c4788be11e18d2464cf643bf7203ff67", size = 59253, upload-time = "2026-05-08T20:59:57.075Z" }, + { url = "https://files.pythonhosted.org/packages/b9/42/64f8d90b73fd9cdc1499b48057ff6d9cd2a98a25734c9bb62ecf07e87061/propcache-0.5.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:95f1e3f4760d404b13c9976c0229b2b49a3c8e2c62a9ce92efdd2b11ada75e3f", size = 57558, upload-time = "2026-05-08T20:59:58.602Z" }, + { url = "https://files.pythonhosted.org/packages/eb/02/dba5bc03c9041f2092ea55a449caf5dfe68352c6654511b29ba0654ddb69/propcache-0.5.2-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:85341b12b9d55bad0bded24cac341bb34289469e03a11f3f583ea1cc1db0326c", size = 55007, upload-time = "2026-05-08T20:59:59.837Z" }, + { url = "https://files.pythonhosted.org/packages/14/c0/43f649c7aa2a77a3b100d84e9dea3a483120ecb608bfe36ce49eaff517fe/propcache-0.5.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:26a4dca084132874e639895c3135dfad5eb20bae209f62d1aeb31b03e601c3c0", size = 60355, upload-time = "2026-05-08T21:00:01.144Z" }, + { url = "https://files.pythonhosted.org/packages/83/c0/435dafd27f1cb4a495381dae60e25883ccfe4020bb72818e8184c1678092/propcache-0.5.2-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:3b199b9b2b3d6a7edf3183ba8a9a137a22b97f7df525feb5ae1eccf026d2a9c6", size = 59057, upload-time = "2026-05-08T21:00:02.401Z" }, + { url = "https://files.pythonhosted.org/packages/53/ae/6e292df9135d659944e96cb3389258e4a663e5b2b5f6c217ef0ddc8d2f73/propcache-0.5.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:e59bc9e66329185b93dab73f210f1a37f81cb40f321501db8017c9aea15dba27", size = 61938, upload-time = "2026-05-08T21:00:03.638Z" }, + { url = "https://files.pythonhosted.org/packages/0b/42/314ebc50d8159055411fd6b0bda322ff510e4b1f7d2e4927940ad0f6af20/propcache-0.5.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:552ffadf6ad409844bc5919c42a0a83d88314cedddaea0e41e80a8b8fffe881f", size = 59731, upload-time = "2026-05-08T21:00:04.881Z" }, + { url = "https://files.pythonhosted.org/packages/b8/9b/2da6dee38871c3c8772fabc2758325a5c9077d6d18c597737dc04dd884cd/propcache-0.5.2-cp311-cp311-win32.whl", hash = "sha256:cd416c1de191973c52ff1a12a57446bfc7642797b282d7caf2162d7d1b8aa9a0", size = 38966, upload-time = "2026-05-08T21:00:06.511Z" }, + { url = "https://files.pythonhosted.org/packages/42/4e/f17363fb58c0afe05b067361cb6d86ed2d29de6506779a27547c4d183075/propcache-0.5.2-cp311-cp311-win_amd64.whl", hash = "sha256:44e488ef40dbb452700b2b1f8188934121f6648f52c295055662d2191959ff82", size = 42135, upload-time = "2026-05-08T21:00:08.088Z" }, + { url = "https://files.pythonhosted.org/packages/c6/eb/6af6685077d22e8b33358d3c548e3282706a0b3cd85044ffba4e5dd08e3b/propcache-0.5.2-cp311-cp311-win_arm64.whl", hash = "sha256:54adaa85a22078d1e306304a40984dc5be99d599bf3dc0a24dc98f7daeab89ab", size = 38381, upload-time = "2026-05-08T21:00:09.692Z" }, + { url = "https://files.pythonhosted.org/packages/4a/cb/e27bc2b2737a0bb49962b275efa051e8f1c35a936df7d5139b6b658b7dc9/propcache-0.5.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:806719138ecd720339a12410fb9614ac9b2b2d3a5fdf8235d56981c36f4039ba", size = 95887, upload-time = "2026-05-08T21:00:11.277Z" }, + { url = "https://files.pythonhosted.org/packages/e6/13/b8ae04c59392f8d11c6cd9fb4011d1dc7c86b81225c770280300e259ffe1/propcache-0.5.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:db2b80ea58eab4f86b2beec3cc8b39e8ff9276ac20e96b7cce43c8ae84cd6b5a", size = 54654, upload-time = "2026-05-08T21:00:12.604Z" }, + { url = "https://files.pythonhosted.org/packages/2c/7d/49777a3e20b55863d4794384a38acd460c04157b0a00f8602b0d508b8431/propcache-0.5.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e5cbfac9f61484f7e9f3597775500cd3ebe8274e9b050c38f9525c77c97520bf", size = 55190, upload-time = "2026-05-08T21:00:13.935Z" }, + { url = "https://files.pythonhosted.org/packages/44/c7/085d0cd63062e84044e3f05797749c3f8e3938ff3aeb0eb2f69d43fafc91/propcache-0.5.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5dbc581d2814337da56222fab8dc5f161cd798a434e49bac27930aaef798e144", size = 59995, upload-time = "2026-05-08T21:00:15.526Z" }, + { url = "https://files.pythonhosted.org/packages/9c/42/32cf8e3009e92b2645cf1e944f701e8ea4e924dffde1ee26db860bcbf7e4/propcache-0.5.2-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:857187f381f88c8e2fa2fe56ab94879d011b883d5a2ee5a1b60a8cd2a06846d9", size = 63422, upload-time = "2026-05-08T21:00:16.824Z" }, + { url = "https://files.pythonhosted.org/packages/9e/1b/f112433f99fc979431b87a39ef169e3f8df070d99a72792c56d6937ac48b/propcache-0.5.2-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:178b4a2cdaac1818e2bf1c5a99b94383fa73ea5382e032a48dec07dc5668dc42", size = 64342, upload-time = "2026-05-08T21:00:18.362Z" }, + { url = "https://files.pythonhosted.org/packages/14/15/5574111ae50dd6e879456888c0eadd4c5a869959775854e18e18a6b345f3/propcache-0.5.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6f328175a2cde1f0ff2c4ed8ce968b9dcfb55f3a7153f39e2957ed994da13476", size = 61639, upload-time = "2026-05-08T21:00:19.692Z" }, + { url = "https://files.pythonhosted.org/packages/cc/da/4d775080b1490c0ae604acda868bd71aabe3a89ed16f2aa4339eb8a283e7/propcache-0.5.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5671d09a36b06d0fd4a3da0fccbcae360e9b1570924171a15e9e0997f0249fba", size = 61588, upload-time = "2026-05-08T21:00:21.155Z" }, + { url = "https://files.pythonhosted.org/packages/04/ac/f076982cbe2195ee9cf32de5a1e46951d9fb399fc207f390562dd0fd8fb2/propcache-0.5.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:80168e2ebe4d3ec6599d10ad8f520304ae1cad9b6c5a95372aef1b66b7bfb53a", size = 60029, upload-time = "2026-05-08T21:00:22.713Z" }, + { url = "https://files.pythonhosted.org/packages/70/60/189be62e0dd898dce3b331e1b8c7a543cd3a405ac0c81fe8ee8a9d5d77e1/propcache-0.5.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:45f11346f884bc47444f6e6647131055844134c3175b629f84952e2b5cd62b64", size = 56774, upload-time = "2026-05-08T21:00:24.001Z" }, + { url = "https://files.pythonhosted.org/packages/ea/9e/93377b9c7939c1ffae98f878dee955efadfd638078bc86dbc21f9d52f651/propcache-0.5.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8e778ebd44ef4f66ed60a0416b06b489687db264a9c0b3620362f26489492913", size = 63532, upload-time = "2026-05-08T21:00:25.545Z" }, + { url = "https://files.pythonhosted.org/packages/14/f9/590ef6cfb9b8028d516d287812ece32bb0bc5f11fbb9c8bf6b2e6313fec8/propcache-0.5.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:c0cb9ed24c8964e172768d455a38254c2dd8a552905729ce006cad3d3dda59b1", size = 61592, upload-time = "2026-05-08T21:00:27.186Z" }, + { url = "https://files.pythonhosted.org/packages/b4/5e/70958b3034c297a630bba2f17ca7abc2d5f39a803ad7e370ab79d1ecd022/propcache-0.5.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:1d1ad32d9d4355e2be65574fd0bfd3677e7066b009cd5b9b2dee8aa6a6393b33", size = 64788, upload-time = "2026-05-08T21:00:28.8Z" }, + { url = "https://files.pythonhosted.org/packages/12/fd/77fe5936d8c3086ca9048f7f415f122ed82e53884a9ec193646b42deef06/propcache-0.5.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c80f4ba3e8f00189165999a742ee526ebeccedf6c3f7beb0c7df821e9772435a", size = 62514, upload-time = "2026-05-08T21:00:30.098Z" }, + { url = "https://files.pythonhosted.org/packages/cf/74/66bd798b5b3be70aa1b391f5cc9d6a0a5532d7fd3b19ec0b213e72e6ad9d/propcache-0.5.2-cp312-cp312-win32.whl", hash = "sha256:8c7972d8f193740d9175f0998ab38717e6cd322d5935c5b0fef8c0d323fd9031", size = 39018, upload-time = "2026-05-08T21:00:31.622Z" }, + { url = "https://files.pythonhosted.org/packages/61/7c/5c0d34aa3024694d6dcb9271cdbdd08c4e47c1c0ad95ec7e7bc74cdea145/propcache-0.5.2-cp312-cp312-win_amd64.whl", hash = "sha256:d9ee8826a7d47863a08ac44e1a5f611a462eefc3a194b492da242128bec75b42", size = 42322, upload-time = "2026-05-08T21:00:32.918Z" }, + { url = "https://files.pythonhosted.org/packages/4d/91/875812f1a3feb20ceba818ef39fbe4d92f1081e04ac815c822496d0d038b/propcache-0.5.2-cp312-cp312-win_arm64.whl", hash = "sha256:2800a4a8ead6b28cccd1ec54b59346f0def7922ee1c7598e8499c733cfbb7c84", size = 38172, upload-time = "2026-05-08T21:00:35.124Z" }, + { url = "https://files.pythonhosted.org/packages/c5/09/f049e45385503fe67db75a6b6186a7b9f0c3930366dc960522c312a825b1/propcache-0.5.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:099aaf4b4d1a02265b92a977edf00b5c4f63b3b17ac6de39b0d637c9cac0188a", size = 94457, upload-time = "2026-05-08T21:00:36.355Z" }, + { url = "https://files.pythonhosted.org/packages/6b/65/83d1d05655baf63113731bd5a1008435e14f8d1e5a06cbe4ec5b23ad7a31/propcache-0.5.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:68ce1c44c7a813a7f71ea04315a8c7b330b63db99d059a797a4651bb6f69f117", size = 53835, upload-time = "2026-05-08T21:00:38.072Z" }, + { url = "https://files.pythonhosted.org/packages/a9/12/a6ba6482bb5ea3260c000c9b20881c95fa11c6b30173715668259f844ed7/propcache-0.5.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:fc299c129490f55f254cd90be0deca4764e36e9a7c08b4aa588479a3bbed3098", size = 54545, upload-time = "2026-05-08T21:00:39.319Z" }, + { url = "https://files.pythonhosted.org/packages/a9/19/7fa086f5764c59ec8a8e157cd93aa8497acc00aba9dcdec56bfffb32602d/propcache-0.5.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a6ae2198be502c10f09b2516e7b5d019816924bc3183a43ce792a7bd6625e6f4", size = 59886, upload-time = "2026-05-08T21:00:40.621Z" }, + { url = "https://files.pythonhosted.org/packages/a1/e4/5d7663dc8235956c8f5281698a3af1d351d8820341ddd890f59d9a9127f2/propcache-0.5.2-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6041d31504dc1779d700e1edcfb08eea334b357620b06681a4eabb57a74e574e", size = 63261, upload-time = "2026-05-08T21:00:41.775Z" }, + { url = "https://files.pythonhosted.org/packages/4a/4a/15a03adee24d6350da4292caeac44c34c033d2afe5e87eb370f38854560f/propcache-0.5.2-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f7eabc04151c78a9f4d5bbb5f1faf571e4defeb4b585e0fe95b60ff2dbe4d3d7", size = 64184, upload-time = "2026-05-08T21:00:43.018Z" }, + { url = "https://files.pythonhosted.org/packages/8b/c6/979176efdaa3d239e36d503d5af63a0a773b36662ed8f52e5b6a6d9fd40e/propcache-0.5.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4db0ba63d693afd40d249bd93f842b5f144f8fcbb83de05660373bcf30517b1d", size = 61534, upload-time = "2026-05-08T21:00:44.507Z" }, + { url = "https://files.pythonhosted.org/packages/c8/22/63e8cd1bae4c2d2be6493b6b7d10566ddafad88137cfbc99964a1119853c/propcache-0.5.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1dbcf7675229b35d31abb6547d8ebc8c27a830ac3f9a794edff6254873ec7c0a", size = 61500, upload-time = "2026-05-08T21:00:45.796Z" }, + { url = "https://files.pythonhosted.org/packages/60/5a/28e5d9acbac1cc9ccb67045e8c1b943aa8d79fdf39c93bd73cacd68008ea/propcache-0.5.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d310c013aad2c72f1c3f2f8dd3279d460a858c551f97aeb8c63e4693cca7b4d2", size = 59994, upload-time = "2026-05-08T21:00:47.093Z" }, + { url = "https://files.pythonhosted.org/packages/f3/40/db650677f554a95b9c01a7c9d93d629e93a15562f5deb4573c9ee136fed2/propcache-0.5.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:06187263ddad280d05b4d8a8b3bb7d164cbebd469236544a42e6d9b28ac6a4fa", size = 56884, upload-time = "2026-05-08T21:00:48.376Z" }, + { url = "https://files.pythonhosted.org/packages/80/45/70b39b89516ff8b96bf732fa6fded8cef20f293cb1508690101c3c07ec51/propcache-0.5.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3115559b8effafd63b142ea5ed53d63a16ea6469cbc63dce4ee194b42db5d853", size = 63464, upload-time = "2026-05-08T21:00:49.954Z" }, + { url = "https://files.pythonhosted.org/packages/f9/e2/fa59d3a89eac5534293124af4f1d0d0ada091ce4a0ab4610ce03fd2bdd8d/propcache-0.5.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c60462af8e6dc30c35407c7237ea908d777b22862bbee27bc4699c0d8bcdc45a", size = 61588, upload-time = "2026-05-08T21:00:51.281Z" }, + { url = "https://files.pythonhosted.org/packages/0b/97/efb547a55c4bc7381cfb202d6a2239ac621045277bc1ea5dfd3a7f0516c0/propcache-0.5.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:40314bca9ac559716fe374094fc81c11dcc34b64fd6c585360f5775690505704", size = 64667, upload-time = "2026-05-08T21:00:52.602Z" }, + { url = "https://files.pythonhosted.org/packages/92/56/f5c7d9b4b7595d5127da38974d791b2153f3d1eae6c674af3583ace92ad3/propcache-0.5.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:cfa21e036ce1e1db2be04ba3b85d2df1bb1702fa01932d984c5464c665228ff4", size = 62463, upload-time = "2026-05-08T21:00:54.303Z" }, + { url = "https://files.pythonhosted.org/packages/bd/3b/484a3a65fc9f9f60c41dcd17b428bace5389544e2c680994534a20755066/propcache-0.5.2-cp313-cp313-win32.whl", hash = "sha256:f156a3529f38063b6dbaf356e15602a7f95f8055b1295a438433a6386f10463d", size = 38621, upload-time = "2026-05-08T21:00:55.808Z" }, + { url = "https://files.pythonhosted.org/packages/1c/fd/3f0f10dba4dabad3bf53102be007abf55481067952bde0fdddff439e7c61/propcache-0.5.2-cp313-cp313-win_amd64.whl", hash = "sha256:dfed59d0a5aeb01e242e66ff0300bc4a265a7c05f612d30016f0b60b1017d757", size = 41649, upload-time = "2026-05-08T21:00:57.061Z" }, + { url = "https://files.pythonhosted.org/packages/90/ec/6ce619cc32bb500a482f811f9cd509368b4e58e638d13f2c68f370d6b475/propcache-0.5.2-cp313-cp313-win_arm64.whl", hash = "sha256:ba338430e87ceb9c8f0cf754de38a9860560261e56c00376debd628698a7364f", size = 37636, upload-time = "2026-05-08T21:00:58.646Z" }, + { url = "https://files.pythonhosted.org/packages/1b/82/c1d268bbbf2ef981c5bf0fbbe746db617c66e3bcefe431a1aa8943fbe23a/propcache-0.5.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:a592f5f3da71c8691c788c13cb6734b6d17663d2e1cb8caddf0673d01ef8847d", size = 98872, upload-time = "2026-05-08T21:00:59.889Z" }, + { url = "https://files.pythonhosted.org/packages/f4/d4/52c871e73e864e6b34c0e2d58ac1ec5ccd149497ddc7ad2137ae98323a35/propcache-0.5.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:6a997d0489e9668a384fcfd5061b857aa5361de73191cac204d04b889cfbbafa", size = 56257, upload-time = "2026-05-08T21:01:01.195Z" }, + { url = "https://files.pythonhosted.org/packages/67/f0/9b90ca2a210b3d09bcfcd96ecd0f55545c091535abce2a45de2775cfd357/propcache-0.5.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:10734b5484ea113152ee25a91dccedf81631791805d2c9ccb054958e51842c94", size = 56696, upload-time = "2026-05-08T21:01:02.941Z" }, + { url = "https://files.pythonhosted.org/packages/9d/0e/6e9d4ba07c8e56e21ddec1e75f12148142b21ca83a51871babce095334f4/propcache-0.5.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cafca7e56c12bb02ae16d283742bef25a61122e9dab2b5b3f2ccbe589ce32164", size = 62378, upload-time = "2026-05-08T21:01:04.475Z" }, + { url = "https://files.pythonhosted.org/packages/65/19/c10badaa463dde8a27ce884f8ee2ec37e6035b7c9f5ff0c8f74f06f08dac/propcache-0.5.2-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f064f8d2b59177878b7615df1735cd8fe3462ed6be8c7b217d17a276489c2b7f", size = 65283, upload-time = "2026-05-08T21:01:05.959Z" }, + { url = "https://files.pythonhosted.org/packages/b0/b6/93bea99ca80e19cef6512a8580e5b7857bbe09422d9daa7fd4ef5723306c/propcache-0.5.2-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f78abfa8dfc32376fd1aacf597b2f2fbbe0ea751419aee718af5d4f82537ef8c", size = 66616, upload-time = "2026-05-08T21:01:07.228Z" }, + { url = "https://files.pythonhosted.org/packages/83/e4/5c7462e50625f051f37fb38b8224f7639f667184bbd34424ec83819bb1b7/propcache-0.5.2-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f7467da8a9822bf1a55336f877340c5bcbd3c482afc43a99771169f74a26dedc", size = 63773, upload-time = "2026-05-08T21:01:08.514Z" }, + { url = "https://files.pythonhosted.org/packages/ca/b6/99238894047b13c823be25027e736626cd414a52a5e30d2c3347c2733529/propcache-0.5.2-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a6ddc6ac9e25de626c1f129c1b467d7ecd33ce2237d3fd0c4e429feef0a7ee1f", size = 63664, upload-time = "2026-05-08T21:01:09.874Z" }, + { url = "https://files.pythonhosted.org/packages/85/1e/a3a1a63116a2b8edb415a8bb9a6f0c34bd03830b1e18e8ce2904e1dc1cf4/propcache-0.5.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2f22cbbac9e26a8e864c0985ff1268d5d939d53d9d9411a9824279097e03a2cb", size = 62643, upload-time = "2026-05-08T21:01:11.132Z" }, + { url = "https://files.pythonhosted.org/packages/e4/03/893cf147de2fc6543c5eaa07ad833170e7e2a2385725bbebe8c0503723bb/propcache-0.5.2-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:fc76378c62a0f04d0cd82fbb1a2cd2d7e28fcb40d5873f28a6c44e388aaa2751", size = 59595, upload-time = "2026-05-08T21:01:12.387Z" }, + { url = "https://files.pythonhosted.org/packages/86/3b/04c1a2e12c57766568ba75ba72b3bf2042818d4c1425fab6fc07155c7cff/propcache-0.5.2-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:acd2c8edba48e31e58a363b8cf4e5c7db3b04b3f9e371f601df30d9b0d244836", size = 65711, upload-time = "2026-05-08T21:01:13.676Z" }, + { url = "https://files.pythonhosted.org/packages/1c/34/80f8d0099f8d6bacc4de1624c85672681c8cd1149ca2da0e38fd120b817f/propcache-0.5.2-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:452b5065457eb9991ec5eb38ff41d6cd4c991c9ac7c531c4d5849ae473a9a13f", size = 64247, upload-time = "2026-05-08T21:01:14.936Z" }, + { url = "https://files.pythonhosted.org/packages/f3/1a/8b08f3a5f1037e9e370c55883ceeeee0f6dd0416fb2d2d67b8bfc91f2a79/propcache-0.5.2-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:3430bb2bfe1331885c427745a751e774ee679fd4344f80b97bf879815fe8fa55", size = 67102, upload-time = "2026-05-08T21:01:16.281Z" }, + { url = "https://files.pythonhosted.org/packages/34/68/8bdb7bb7756d76e005490649d10e4a8369e610c74d619f71e1aedf889e9c/propcache-0.5.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:cef6cea3922890dd6c9654971001fa797b526c16ab5e1e46c05fd6f877be7568", size = 64964, upload-time = "2026-05-08T21:01:17.57Z" }, + { url = "https://files.pythonhosted.org/packages/0a/aa/50fb0b5d3968b61a510926ff8b8465f1d6e976b3ab74496d7a4b9fc42515/propcache-0.5.2-cp313-cp313t-win32.whl", hash = "sha256:72d61e16dd78228b58c5d47be830ff3da7e5f139abdf0aef9d86cde1c5cf2191", size = 42546, upload-time = "2026-05-08T21:01:18.946Z" }, + { url = "https://files.pythonhosted.org/packages/ae/4c/0ddbae64321bd4a95bcbfc19307238016b5b1fee645c84626c8d539e5b74/propcache-0.5.2-cp313-cp313t-win_amd64.whl", hash = "sha256:0958834041a0166d343b8d2cedcd8bcbaeb4fdbe0cf08320c5379f143c3be6e7", size = 46330, upload-time = "2026-05-08T21:01:20.162Z" }, + { url = "https://files.pythonhosted.org/packages/00/d9/9cddc8efb78d8af264c5ec9f6d10b62f57c515feda8d321595f56010fb23/propcache-0.5.2-cp313-cp313t-win_arm64.whl", hash = "sha256:6de8bd93ddde9b992cf2b2e0d796d501a19026b5b9fd87356d7d0779531a8d96", size = 40521, upload-time = "2026-05-08T21:01:21.399Z" }, + { url = "https://files.pythonhosted.org/packages/3a/ed/1cdcab6ba3d6ab7feca11fc14f0eeea80755bb53ef4e892079f31b10a25f/propcache-0.5.2-py3-none-any.whl", hash = "sha256:be1ddfcbb376e3de5d2e2db1d58d6d67463e6b4f9f040c000de8e300295465fe", size = 14036, upload-time = "2026-05-08T21:02:10.673Z" }, +] + +[[package]] +name = "proto-plus" +version = "1.28.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c9/56/e647b0c675392d2da368da7b6f158f7368b18542fd6f7d7400a2f39de000/proto_plus-1.28.0.tar.gz", hash = "sha256:38e5696342835b08fc116f30a25665b29531cda9d5d5643e9b81fc312385abd9", size = 57221, upload-time = "2026-05-07T08:04:50.811Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7c/20/b122d4626976acb81132036d2ad1bb35a1a8775fceb837ec30964622516a/proto_plus-1.28.0-py3-none-any.whl", hash = "sha256:a630604310899e73c59ec302e5765c058d412b2f090b9c79c8822589f14955b8", size = 50410, upload-time = "2026-05-07T08:03:31.962Z" }, +] + +[[package]] +name = "protobuf" +version = "6.33.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/66/70/e908e9c5e52ef7c3a6c7902c9dfbb34c7e29c25d2f81ade3856445fd5c94/protobuf-6.33.6.tar.gz", hash = "sha256:a6768d25248312c297558af96a9f9c929e8c4cee0659cb07e780731095f38135", size = 444531, upload-time = "2026-03-18T19:05:00.988Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fc/9f/2f509339e89cfa6f6a4c4ff50438db9ca488dec341f7e454adad60150b00/protobuf-6.33.6-cp310-abi3-win32.whl", hash = "sha256:7d29d9b65f8afef196f8334e80d6bc1d5d4adedb449971fefd3723824e6e77d3", size = 425739, upload-time = "2026-03-18T19:04:48.373Z" }, + { url = "https://files.pythonhosted.org/packages/76/5d/683efcd4798e0030c1bab27374fd13a89f7c2515fb1f3123efdfaa5eab57/protobuf-6.33.6-cp310-abi3-win_amd64.whl", hash = "sha256:0cd27b587afca21b7cfa59a74dcbd48a50f0a6400cfb59391340ad729d91d326", size = 437089, upload-time = "2026-03-18T19:04:50.381Z" }, + { url = "https://files.pythonhosted.org/packages/5c/01/a3c3ed5cd186f39e7880f8303cc51385a198a81469d53d0fdecf1f64d929/protobuf-6.33.6-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:9720e6961b251bde64edfdab7d500725a2af5280f3f4c87e57c0208376aa8c3a", size = 427737, upload-time = "2026-03-18T19:04:51.866Z" }, + { url = "https://files.pythonhosted.org/packages/ee/90/b3c01fdec7d2f627b3a6884243ba328c1217ed2d978def5c12dc50d328a3/protobuf-6.33.6-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:e2afbae9b8e1825e3529f88d514754e094278bb95eadc0e199751cdd9a2e82a2", size = 324610, upload-time = "2026-03-18T19:04:53.096Z" }, + { url = "https://files.pythonhosted.org/packages/9b/ca/25afc144934014700c52e05103c2421997482d561f3101ff352e1292fb81/protobuf-6.33.6-cp39-abi3-manylinux2014_s390x.whl", hash = "sha256:c96c37eec15086b79762ed265d59ab204dabc53056e3443e702d2681f4b39ce3", size = 339381, upload-time = "2026-03-18T19:04:54.616Z" }, + { url = "https://files.pythonhosted.org/packages/16/92/d1e32e3e0d894fe00b15ce28ad4944ab692713f2e7f0a99787405e43533a/protobuf-6.33.6-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:e9db7e292e0ab79dd108d7f1a94fe31601ce1ee3f7b79e0692043423020b0593", size = 323436, upload-time = "2026-03-18T19:04:55.768Z" }, + { url = "https://files.pythonhosted.org/packages/c4/72/02445137af02769918a93807b2b7890047c32bfb9f90371cbc12688819eb/protobuf-6.33.6-py3-none-any.whl", hash = "sha256:77179e006c476e69bf8e8ce866640091ec42e1beb80b213c3900006ecfba6901", size = 170656, upload-time = "2026-03-18T19:04:59.826Z" }, +] + +[[package]] +name = "pyarrow" +version = "24.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/91/13/13e1069b351bdc3881266e11147ffccf687505dbb0ea74036237f5d454a5/pyarrow-24.0.0.tar.gz", hash = "sha256:85fe721a14dd823aca09127acbb06c3ca723efbd436c004f16bca601b04dcc83", size = 1180261, upload-time = "2026-04-21T10:51:25.837Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/c9/a47ab7ece0d86cbe6678418a0fbd1ac4bb493b9184a3891dfa0e7f287ae0/pyarrow-24.0.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:b0e131f880cda8d04e076cee175a46fc0e8bc8b65c99c6c09dff6669335fde74", size = 35068898, upload-time = "2026-04-21T10:46:36.599Z" }, + { url = "https://files.pythonhosted.org/packages/d1/bc/8db86617a9a58008acf8913d6fed68ea2a46acb6de928db28d724c891a68/pyarrow-24.0.0-cp311-cp311-macosx_12_0_x86_64.whl", hash = "sha256:1b2fe7f9a5566401a0ef2571f197eb92358925c1f0c8dba305d6e43ea0871bb3", size = 36679915, upload-time = "2026-04-21T10:46:42.602Z" }, + { url = "https://files.pythonhosted.org/packages/eb/8e/fb178720400ef69db251eb4a9c3ccf4af269bc1feb5055529b8fc87170d1/pyarrow-24.0.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:0b3537c00fb8d384f15ac1e79b6eb6db04a16514c8c1d22e59a9b95c8ba42868", size = 45697931, upload-time = "2026-04-21T10:46:48.403Z" }, + { url = "https://files.pythonhosted.org/packages/f3/27/99c42abe8e21b44f4917f62631f3aa31404882a2c41d8a4cd5c110e13d52/pyarrow-24.0.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:14e31a3c9e35f1ab6356c6378f6f72830e6d2d5f1791df3774a7b097d18a6a1e", size = 48837449, upload-time = "2026-04-21T10:46:55.329Z" }, + { url = "https://files.pythonhosted.org/packages/36/b6/333749e2666e9032891125bf9c691146e92901bece62030ac1430e2e7c88/pyarrow-24.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b7d9a514e73bc42711e6a35aaccf3587c520024fe0a25d830a1a8a27c15f4f57", size = 49395949, upload-time = "2026-04-21T10:47:01.869Z" }, + { url = "https://files.pythonhosted.org/packages/17/25/c5201706a2dd374e8ba6ee3fd7a8c89fb7ffc16eed5217a91fd2bd7f7626/pyarrow-24.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b196eb3f931862af3fa84c2a253514d859c08e0d8fe020e07be12e75a5a9780c", size = 51912986, upload-time = "2026-04-21T10:47:09.872Z" }, + { url = "https://files.pythonhosted.org/packages/f8/d2/4d1bbba65320b21a49678d6fbdc6ff7c649251359fdcfc03568c4136231d/pyarrow-24.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:35405aecb474e683fb36af650618fd5340ee5471fc65a21b36076a18bbc6c981", size = 27255371, upload-time = "2026-04-21T10:47:15.943Z" }, + { url = "https://files.pythonhosted.org/packages/b4/a9/9686d9f07837f91f775e8932659192e02c74f9d8920524b480b85212cc68/pyarrow-24.0.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:6233c9ed9ab9d1db47de57d9753256d9dcffbf42db341576099f0fd9f6bf4810", size = 34981559, upload-time = "2026-04-21T10:47:22.17Z" }, + { url = "https://files.pythonhosted.org/packages/80/b6/0ddf0e9b6ead3474ab087ae598c76b031fc45532bf6a63f3a553440fb258/pyarrow-24.0.0-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:f7616236ec1bc2b15bfdec22a71ab38851c86f8f05ff64f379e1278cf20c634a", size = 36663654, upload-time = "2026-04-21T10:47:28.315Z" }, + { url = "https://files.pythonhosted.org/packages/7c/3b/926382efe8ce27ba729071d3566ade6dfb86bdf112f366000196b2f5780a/pyarrow-24.0.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:1617043b99bd33e5318ae18eb2919af09c71322ef1ca46566cdafc6e6712fb66", size = 45679394, upload-time = "2026-04-21T10:47:34.821Z" }, + { url = "https://files.pythonhosted.org/packages/b3/7a/829f7d9dfd37c207206081d6dad474d81dde29952401f07f2ba507814818/pyarrow-24.0.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:6165461f55ef6314f026de6638d661188e3455d3ec49834556a0ebbdbace18bb", size = 48863122, upload-time = "2026-04-21T10:47:42.056Z" }, + { url = "https://files.pythonhosted.org/packages/5f/e8/f88ce625fe8babaae64e8db2d417c7653adb3019b08aae85c5ed787dc816/pyarrow-24.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3b13dedfe76a0ad2d1d859b0811b53827a4e9d93a0bcb05cf59333ab4980cc7e", size = 49376032, upload-time = "2026-04-21T10:47:48.967Z" }, + { url = "https://files.pythonhosted.org/packages/36/7a/82c363caa145fff88fb475da50d3bf52bb024f61917be5424c3392eaf878/pyarrow-24.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:25ea65d868eb04015cd18e6df2fbe98f07e5bda2abefabcb88fce39a947716f6", size = 51929490, upload-time = "2026-04-21T10:47:55.981Z" }, + { url = "https://files.pythonhosted.org/packages/66/1c/e3e72c8014ad2743ca64a701652c733cc5cbcee15c0463a32a8c55518d9e/pyarrow-24.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:295f0a7f2e242dabd513737cf076007dc5b2d59237e3eca37b05c0c6446f3826", size = 27355660, upload-time = "2026-04-21T10:48:01.718Z" }, + { url = "https://files.pythonhosted.org/packages/6f/d3/a1abf004482026ddc17f4503db227787fa3cfe41ec5091ff20e4fea55e57/pyarrow-24.0.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:02b001b3ed4723caa44f6cd1af2d5c86aa2cf9971dacc2ffa55b21237713dfba", size = 34976759, upload-time = "2026-04-21T10:48:07.258Z" }, + { url = "https://files.pythonhosted.org/packages/4f/4a/34f0a36d28a2dd32225301b79daad44e243dc1a2bb77d43b60749be255c4/pyarrow-24.0.0-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:04920d6a71aabd08a0417709efce97d45ea8e6fb733d9ca9ecffb13c67839f68", size = 36658471, upload-time = "2026-04-21T10:48:13.347Z" }, + { url = "https://files.pythonhosted.org/packages/1f/78/543b94712ae8bb1a6023bcc1acf1a740fbff8286747c289cd9468fced2a5/pyarrow-24.0.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:a964266397740257f16f7bb2e4f08a0c81454004beab8ff59dd531b73610e9f2", size = 45675981, upload-time = "2026-04-21T10:48:20.201Z" }, + { url = "https://files.pythonhosted.org/packages/84/9f/8fb7c222b100d314137fa40ec050de56cd8c6d957d1cfff685ce72f15b17/pyarrow-24.0.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:6f066b179d68c413374294bc1735f68475457c933258df594443bb9d88ddc2a0", size = 48859172, upload-time = "2026-04-21T10:48:27.541Z" }, + { url = "https://files.pythonhosted.org/packages/a7/d3/1ea72538e6c8b3b475ed78d1049a2c518e655761ea50fe1171fc855fcab7/pyarrow-24.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1183baeb14c5f587b1ec52831e665718ce632caab84b7cd6b85fd44f96114495", size = 49385733, upload-time = "2026-04-21T10:48:34.7Z" }, + { url = "https://files.pythonhosted.org/packages/c3/be/c3d8b06a1ba35f2260f8e1f771abbee7d5e345c0937aab90675706b1690a/pyarrow-24.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:806f24b4085453c197a5078218d1ee08783ebbba271badd153d1ae22a3ee804f", size = 51934335, upload-time = "2026-04-21T10:48:42.099Z" }, + { url = "https://files.pythonhosted.org/packages/9c/62/89e07a1e7329d2cde3e3c6994ba0839a24977a2beda8be6005ea3d860b99/pyarrow-24.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:e4505fc6583f7b05ab854934896bcac8253b04ac1171a77dfb73efef92076d91", size = 27271748, upload-time = "2026-04-21T10:49:42.532Z" }, + { url = "https://files.pythonhosted.org/packages/17/1a/cff3a59f80b5b1658549d46611b67163f65e0664431c076ad728bf9d5af4/pyarrow-24.0.0-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:1a4e45017efbf115032e4475ee876d525e0e36c742214fbe405332480ecd6275", size = 35238554, upload-time = "2026-04-21T10:48:48.526Z" }, + { url = "https://files.pythonhosted.org/packages/a8/99/cce0f42a327bfef2c420fb6078a3eb834826e5d6697bf3009fe11d2ad051/pyarrow-24.0.0-cp313-cp313t-macosx_12_0_x86_64.whl", hash = "sha256:7986f1fa71cee060ad00758bcc79d3a93bab8559bf978fab9e53472a2e25a17b", size = 36782301, upload-time = "2026-04-21T10:48:55.181Z" }, + { url = "https://files.pythonhosted.org/packages/2a/66/8e560d5ff6793ca29aca213c53eec0dd482dd46cb93b2819e5aab52e4252/pyarrow-24.0.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:d3e0b61e8efb24ed38898e5cdc5fffa9124be480008d401a1f8071500494ae42", size = 45721929, upload-time = "2026-04-21T10:49:03.676Z" }, + { url = "https://files.pythonhosted.org/packages/27/0c/a26e25505d030716e078d9f16eb74973cbf0b33b672884e9f9da1c83b871/pyarrow-24.0.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:55a3bc1e3df3b5567b7d27ef551b2283f0c68a5e86f1cd56abc569da4f31335b", size = 48825365, upload-time = "2026-04-21T10:49:11.714Z" }, + { url = "https://files.pythonhosted.org/packages/5f/eb/771f9ecb0c65e73fe9dccdd1717901b9594f08c4515d000c7c62df573811/pyarrow-24.0.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:641f795b361874ac9da5294f8f443dfdbee355cf2bd9e3b8d97aaac2306b9b37", size = 49451819, upload-time = "2026-04-21T10:49:21.474Z" }, + { url = "https://files.pythonhosted.org/packages/48/da/61ae89a88732f5a785646f3ec6125dbb640fa98a540eb2b9889caa561403/pyarrow-24.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8adc8e6ce5fccf5dc707046ae4914fd537def529709cc0d285d37a7f9cd442ca", size = 51909252, upload-time = "2026-04-21T10:49:31.164Z" }, + { url = "https://files.pythonhosted.org/packages/cb/1a/8dd5cafab7b66573fa91c03d06d213356ad4edd71813aa75e08ce2b3a844/pyarrow-24.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:9b18371ad2f44044b81a8d23bc2d8a9b6a6226dca775e8e16cfee640473d6c5d", size = 27388127, upload-time = "2026-04-21T10:49:37.334Z" }, +] + +[[package]] +name = "pyasn1" +version = "0.6.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5c/5f/6583902b6f79b399c9c40674ac384fd9cd77805f9e6205075f828ef11fb2/pyasn1-0.6.3.tar.gz", hash = "sha256:697a8ecd6d98891189184ca1fa05d1bb00e2f84b5977c481452050549c8a72cf", size = 148685, upload-time = "2026-03-17T01:06:53.382Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5d/a0/7d793dce3fa811fe047d6ae2431c672364b462850c6235ae306c0efd025f/pyasn1-0.6.3-py3-none-any.whl", hash = "sha256:a80184d120f0864a52a073acc6fc642847d0be408e7c7252f31390c0f4eadcde", size = 83997, upload-time = "2026-03-17T01:06:52.036Z" }, +] + +[[package]] +name = "pyasn1-modules" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyasn1" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e9/e6/78ebbb10a8c8e4b61a59249394a4a594c1a7af95593dc933a349c8d00964/pyasn1_modules-0.4.2.tar.gz", hash = "sha256:677091de870a80aae844b1ca6134f54652fa2c8c5a52aa396440ac3106e941e6", size = 307892, upload-time = "2025-03-28T02:41:22.17Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/47/8d/d529b5d697919ba8c11ad626e835d4039be708a35b0d22de83a269a6682c/pyasn1_modules-0.4.2-py3-none-any.whl", hash = "sha256:29253a9207ce32b64c3ac6600edc75368f98473906e8fd1043bd6b5b1de2c14a", size = 181259, upload-time = "2025-03-28T02:41:19.028Z" }, +] + +[[package]] +name = "pycparser" +version = "3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" }, +] + +[[package]] +name = "pydantic" +version = "2.12.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/69/44/36f1a6e523abc58ae5f928898e4aca2e0ea509b5aa6f6f392a5d882be928/pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49", size = 821591, upload-time = "2025-11-26T15:11:46.471Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d", size = 463580, upload-time = "2025-11-26T15:11:44.605Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.41.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952, upload-time = "2025-11-04T13:43:49.098Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e8/72/74a989dd9f2084b3d9530b0915fdda64ac48831c30dbf7c72a41a5232db8/pydantic_core-2.41.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a3a52f6156e73e7ccb0f8cced536adccb7042be67cb45f9562e12b319c119da6", size = 2105873, upload-time = "2025-11-04T13:39:31.373Z" }, + { url = "https://files.pythonhosted.org/packages/12/44/37e403fd9455708b3b942949e1d7febc02167662bf1a7da5b78ee1ea2842/pydantic_core-2.41.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7f3bf998340c6d4b0c9a2f02d6a400e51f123b59565d74dc60d252ce888c260b", size = 1899826, upload-time = "2025-11-04T13:39:32.897Z" }, + { url = "https://files.pythonhosted.org/packages/33/7f/1d5cab3ccf44c1935a359d51a8a2a9e1a654b744b5e7f80d41b88d501eec/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:378bec5c66998815d224c9ca994f1e14c0c21cb95d2f52b6021cc0b2a58f2a5a", size = 1917869, upload-time = "2025-11-04T13:39:34.469Z" }, + { url = "https://files.pythonhosted.org/packages/6e/6a/30d94a9674a7fe4f4744052ed6c5e083424510be1e93da5bc47569d11810/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b576130c69225432866fe2f4a469a85a54ade141d96fd396dffcf607b558f8", size = 2063890, upload-time = "2025-11-04T13:39:36.053Z" }, + { url = "https://files.pythonhosted.org/packages/50/be/76e5d46203fcb2750e542f32e6c371ffa9b8ad17364cf94bb0818dbfb50c/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6cb58b9c66f7e4179a2d5e0f849c48eff5c1fca560994d6eb6543abf955a149e", size = 2229740, upload-time = "2025-11-04T13:39:37.753Z" }, + { url = "https://files.pythonhosted.org/packages/d3/ee/fed784df0144793489f87db310a6bbf8118d7b630ed07aa180d6067e653a/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88942d3a3dff3afc8288c21e565e476fc278902ae4d6d134f1eeda118cc830b1", size = 2350021, upload-time = "2025-11-04T13:39:40.94Z" }, + { url = "https://files.pythonhosted.org/packages/c8/be/8fed28dd0a180dca19e72c233cbf58efa36df055e5b9d90d64fd1740b828/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f31d95a179f8d64d90f6831d71fa93290893a33148d890ba15de25642c5d075b", size = 2066378, upload-time = "2025-11-04T13:39:42.523Z" }, + { url = "https://files.pythonhosted.org/packages/b0/3b/698cf8ae1d536a010e05121b4958b1257f0b5522085e335360e53a6b1c8b/pydantic_core-2.41.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c1df3d34aced70add6f867a8cf413e299177e0c22660cc767218373d0779487b", size = 2175761, upload-time = "2025-11-04T13:39:44.553Z" }, + { url = "https://files.pythonhosted.org/packages/b8/ba/15d537423939553116dea94ce02f9c31be0fa9d0b806d427e0308ec17145/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4009935984bd36bd2c774e13f9a09563ce8de4abaa7226f5108262fa3e637284", size = 2146303, upload-time = "2025-11-04T13:39:46.238Z" }, + { url = "https://files.pythonhosted.org/packages/58/7f/0de669bf37d206723795f9c90c82966726a2ab06c336deba4735b55af431/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:34a64bc3441dc1213096a20fe27e8e128bd3ff89921706e83c0b1ac971276594", size = 2340355, upload-time = "2025-11-04T13:39:48.002Z" }, + { url = "https://files.pythonhosted.org/packages/e5/de/e7482c435b83d7e3c3ee5ee4451f6e8973cff0eb6007d2872ce6383f6398/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c9e19dd6e28fdcaa5a1de679aec4141f691023916427ef9bae8584f9c2fb3b0e", size = 2319875, upload-time = "2025-11-04T13:39:49.705Z" }, + { url = "https://files.pythonhosted.org/packages/fe/e6/8c9e81bb6dd7560e33b9053351c29f30c8194b72f2d6932888581f503482/pydantic_core-2.41.5-cp311-cp311-win32.whl", hash = "sha256:2c010c6ded393148374c0f6f0bf89d206bf3217f201faa0635dcd56bd1520f6b", size = 1987549, upload-time = "2025-11-04T13:39:51.842Z" }, + { url = "https://files.pythonhosted.org/packages/11/66/f14d1d978ea94d1bc21fc98fcf570f9542fe55bfcc40269d4e1a21c19bf7/pydantic_core-2.41.5-cp311-cp311-win_amd64.whl", hash = "sha256:76ee27c6e9c7f16f47db7a94157112a2f3a00e958bc626e2f4ee8bec5c328fbe", size = 2011305, upload-time = "2025-11-04T13:39:53.485Z" }, + { url = "https://files.pythonhosted.org/packages/56/d8/0e271434e8efd03186c5386671328154ee349ff0354d83c74f5caaf096ed/pydantic_core-2.41.5-cp311-cp311-win_arm64.whl", hash = "sha256:4bc36bbc0b7584de96561184ad7f012478987882ebf9f9c389b23f432ea3d90f", size = 1972902, upload-time = "2025-11-04T13:39:56.488Z" }, + { url = "https://files.pythonhosted.org/packages/5f/5d/5f6c63eebb5afee93bcaae4ce9a898f3373ca23df3ccaef086d0233a35a7/pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7", size = 2110990, upload-time = "2025-11-04T13:39:58.079Z" }, + { url = "https://files.pythonhosted.org/packages/aa/32/9c2e8ccb57c01111e0fd091f236c7b371c1bccea0fa85247ac55b1e2b6b6/pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0", size = 1896003, upload-time = "2025-11-04T13:39:59.956Z" }, + { url = "https://files.pythonhosted.org/packages/68/b8/a01b53cb0e59139fbc9e4fda3e9724ede8de279097179be4ff31f1abb65a/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69", size = 1919200, upload-time = "2025-11-04T13:40:02.241Z" }, + { url = "https://files.pythonhosted.org/packages/38/de/8c36b5198a29bdaade07b5985e80a233a5ac27137846f3bc2d3b40a47360/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75", size = 2052578, upload-time = "2025-11-04T13:40:04.401Z" }, + { url = "https://files.pythonhosted.org/packages/00/b5/0e8e4b5b081eac6cb3dbb7e60a65907549a1ce035a724368c330112adfdd/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05", size = 2208504, upload-time = "2025-11-04T13:40:06.072Z" }, + { url = "https://files.pythonhosted.org/packages/77/56/87a61aad59c7c5b9dc8caad5a41a5545cba3810c3e828708b3d7404f6cef/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc", size = 2335816, upload-time = "2025-11-04T13:40:07.835Z" }, + { url = "https://files.pythonhosted.org/packages/0d/76/941cc9f73529988688a665a5c0ecff1112b3d95ab48f81db5f7606f522d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c", size = 2075366, upload-time = "2025-11-04T13:40:09.804Z" }, + { url = "https://files.pythonhosted.org/packages/d3/43/ebef01f69baa07a482844faaa0a591bad1ef129253ffd0cdaa9d8a7f72d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5", size = 2171698, upload-time = "2025-11-04T13:40:12.004Z" }, + { url = "https://files.pythonhosted.org/packages/b1/87/41f3202e4193e3bacfc2c065fab7706ebe81af46a83d3e27605029c1f5a6/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c", size = 2132603, upload-time = "2025-11-04T13:40:13.868Z" }, + { url = "https://files.pythonhosted.org/packages/49/7d/4c00df99cb12070b6bccdef4a195255e6020a550d572768d92cc54dba91a/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294", size = 2329591, upload-time = "2025-11-04T13:40:15.672Z" }, + { url = "https://files.pythonhosted.org/packages/cc/6a/ebf4b1d65d458f3cda6a7335d141305dfa19bdc61140a884d165a8a1bbc7/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1", size = 2319068, upload-time = "2025-11-04T13:40:17.532Z" }, + { url = "https://files.pythonhosted.org/packages/49/3b/774f2b5cd4192d5ab75870ce4381fd89cf218af999515baf07e7206753f0/pydantic_core-2.41.5-cp312-cp312-win32.whl", hash = "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d", size = 1985908, upload-time = "2025-11-04T13:40:19.309Z" }, + { url = "https://files.pythonhosted.org/packages/86/45/00173a033c801cacf67c190fef088789394feaf88a98a7035b0e40d53dc9/pydantic_core-2.41.5-cp312-cp312-win_amd64.whl", hash = "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815", size = 2020145, upload-time = "2025-11-04T13:40:21.548Z" }, + { url = "https://files.pythonhosted.org/packages/f9/22/91fbc821fa6d261b376a3f73809f907cec5ca6025642c463d3488aad22fb/pydantic_core-2.41.5-cp312-cp312-win_arm64.whl", hash = "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3", size = 1976179, upload-time = "2025-11-04T13:40:23.393Z" }, + { url = "https://files.pythonhosted.org/packages/87/06/8806241ff1f70d9939f9af039c6c35f2360cf16e93c2ca76f184e76b1564/pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9", size = 2120403, upload-time = "2025-11-04T13:40:25.248Z" }, + { url = "https://files.pythonhosted.org/packages/94/02/abfa0e0bda67faa65fef1c84971c7e45928e108fe24333c81f3bfe35d5f5/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34", size = 1896206, upload-time = "2025-11-04T13:40:27.099Z" }, + { url = "https://files.pythonhosted.org/packages/15/df/a4c740c0943e93e6500f9eb23f4ca7ec9bf71b19e608ae5b579678c8d02f/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0", size = 1919307, upload-time = "2025-11-04T13:40:29.806Z" }, + { url = "https://files.pythonhosted.org/packages/9a/e3/6324802931ae1d123528988e0e86587c2072ac2e5394b4bc2bc34b61ff6e/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33", size = 2063258, upload-time = "2025-11-04T13:40:33.544Z" }, + { url = "https://files.pythonhosted.org/packages/c9/d4/2230d7151d4957dd79c3044ea26346c148c98fbf0ee6ebd41056f2d62ab5/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e", size = 2214917, upload-time = "2025-11-04T13:40:35.479Z" }, + { url = "https://files.pythonhosted.org/packages/e6/9f/eaac5df17a3672fef0081b6c1bb0b82b33ee89aa5cec0d7b05f52fd4a1fa/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2", size = 2332186, upload-time = "2025-11-04T13:40:37.436Z" }, + { url = "https://files.pythonhosted.org/packages/cf/4e/35a80cae583a37cf15604b44240e45c05e04e86f9cfd766623149297e971/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586", size = 2073164, upload-time = "2025-11-04T13:40:40.289Z" }, + { url = "https://files.pythonhosted.org/packages/bf/e3/f6e262673c6140dd3305d144d032f7bd5f7497d3871c1428521f19f9efa2/pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d", size = 2179146, upload-time = "2025-11-04T13:40:42.809Z" }, + { url = "https://files.pythonhosted.org/packages/75/c7/20bd7fc05f0c6ea2056a4565c6f36f8968c0924f19b7d97bbfea55780e73/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740", size = 2137788, upload-time = "2025-11-04T13:40:44.752Z" }, + { url = "https://files.pythonhosted.org/packages/3a/8d/34318ef985c45196e004bc46c6eab2eda437e744c124ef0dbe1ff2c9d06b/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e", size = 2340133, upload-time = "2025-11-04T13:40:46.66Z" }, + { url = "https://files.pythonhosted.org/packages/9c/59/013626bf8c78a5a5d9350d12e7697d3d4de951a75565496abd40ccd46bee/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858", size = 2324852, upload-time = "2025-11-04T13:40:48.575Z" }, + { url = "https://files.pythonhosted.org/packages/1a/d9/c248c103856f807ef70c18a4f986693a46a8ffe1602e5d361485da502d20/pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36", size = 1994679, upload-time = "2025-11-04T13:40:50.619Z" }, + { url = "https://files.pythonhosted.org/packages/9e/8b/341991b158ddab181cff136acd2552c9f35bd30380422a639c0671e99a91/pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11", size = 2019766, upload-time = "2025-11-04T13:40:52.631Z" }, + { url = "https://files.pythonhosted.org/packages/73/7d/f2f9db34af103bea3e09735bb40b021788a5e834c81eedb541991badf8f5/pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd", size = 1981005, upload-time = "2025-11-04T13:40:54.734Z" }, + { url = "https://files.pythonhosted.org/packages/11/72/90fda5ee3b97e51c494938a4a44c3a35a9c96c19bba12372fb9c634d6f57/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:b96d5f26b05d03cc60f11a7761a5ded1741da411e7fe0909e27a5e6a0cb7b034", size = 2115441, upload-time = "2025-11-04T13:42:39.557Z" }, + { url = "https://files.pythonhosted.org/packages/1f/53/8942f884fa33f50794f119012dc6a1a02ac43a56407adaac20463df8e98f/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:634e8609e89ceecea15e2d61bc9ac3718caaaa71963717bf3c8f38bfde64242c", size = 1930291, upload-time = "2025-11-04T13:42:42.169Z" }, + { url = "https://files.pythonhosted.org/packages/79/c8/ecb9ed9cd942bce09fc888ee960b52654fbdbede4ba6c2d6e0d3b1d8b49c/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93e8740d7503eb008aa2df04d3b9735f845d43ae845e6dcd2be0b55a2da43cd2", size = 1948632, upload-time = "2025-11-04T13:42:44.564Z" }, + { url = "https://files.pythonhosted.org/packages/2e/1b/687711069de7efa6af934e74f601e2a4307365e8fdc404703afc453eab26/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f15489ba13d61f670dcc96772e733aad1a6f9c429cc27574c6cdaed82d0146ad", size = 2138905, upload-time = "2025-11-04T13:42:47.156Z" }, + { url = "https://files.pythonhosted.org/packages/09/32/59b0c7e63e277fa7911c2fc70ccfb45ce4b98991e7ef37110663437005af/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd", size = 2110495, upload-time = "2025-11-04T13:42:49.689Z" }, + { url = "https://files.pythonhosted.org/packages/aa/81/05e400037eaf55ad400bcd318c05bb345b57e708887f07ddb2d20e3f0e98/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc", size = 1915388, upload-time = "2025-11-04T13:42:52.215Z" }, + { url = "https://files.pythonhosted.org/packages/6e/0d/e3549b2399f71d56476b77dbf3cf8937cec5cd70536bdc0e374a421d0599/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56", size = 1942879, upload-time = "2025-11-04T13:42:56.483Z" }, + { url = "https://files.pythonhosted.org/packages/f7/07/34573da085946b6a313d7c42f82f16e8920bfd730665de2d11c0c37a74b5/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b", size = 2139017, upload-time = "2025-11-04T13:42:59.471Z" }, + { url = "https://files.pythonhosted.org/packages/5f/9b/1b3f0e9f9305839d7e84912f9e8bfbd191ed1b1ef48083609f0dabde978c/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b2379fa7ed44ddecb5bfe4e48577d752db9fc10be00a6b7446e9663ba143de26", size = 2101980, upload-time = "2025-11-04T13:43:25.97Z" }, + { url = "https://files.pythonhosted.org/packages/a4/ed/d71fefcb4263df0da6a85b5d8a7508360f2f2e9b3bf5814be9c8bccdccc1/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:266fb4cbf5e3cbd0b53669a6d1b039c45e3ce651fd5442eff4d07c2cc8d66808", size = 1923865, upload-time = "2025-11-04T13:43:28.763Z" }, + { url = "https://files.pythonhosted.org/packages/ce/3a/626b38db460d675f873e4444b4bb030453bbe7b4ba55df821d026a0493c4/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58133647260ea01e4d0500089a8c4f07bd7aa6ce109682b1426394988d8aaacc", size = 2134256, upload-time = "2025-11-04T13:43:31.71Z" }, + { url = "https://files.pythonhosted.org/packages/83/d9/8412d7f06f616bbc053d30cb4e5f76786af3221462ad5eee1f202021eb4e/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:287dad91cfb551c363dc62899a80e9e14da1f0e2b6ebde82c806612ca2a13ef1", size = 2174762, upload-time = "2025-11-04T13:43:34.744Z" }, + { url = "https://files.pythonhosted.org/packages/55/4c/162d906b8e3ba3a99354e20faa1b49a85206c47de97a639510a0e673f5da/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:03b77d184b9eb40240ae9fd676ca364ce1085f203e1b1256f8ab9984dca80a84", size = 2143141, upload-time = "2025-11-04T13:43:37.701Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f2/f11dd73284122713f5f89fc940f370d035fa8e1e078d446b3313955157fe/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:a668ce24de96165bb239160b3d854943128f4334822900534f2fe947930e5770", size = 2330317, upload-time = "2025-11-04T13:43:40.406Z" }, + { url = "https://files.pythonhosted.org/packages/88/9d/b06ca6acfe4abb296110fb1273a4d848a0bfb2ff65f3ee92127b3244e16b/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f14f8f046c14563f8eb3f45f499cc658ab8d10072961e07225e507adb700e93f", size = 2316992, upload-time = "2025-11-04T13:43:43.602Z" }, + { url = "https://files.pythonhosted.org/packages/36/c7/cfc8e811f061c841d7990b0201912c3556bfeb99cdcb7ed24adc8d6f8704/pydantic_core-2.41.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56121965f7a4dc965bff783d70b907ddf3d57f6eba29b6d2e5dabfaf07799c51", size = 2145302, upload-time = "2025-11-04T13:43:46.64Z" }, +] + +[[package]] +name = "pydantic-settings" +version = "2.14.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "python-dotenv" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/07/60/1d1e59c9c90d54591469ada7d268251f71c24bdb765f1a8a832cee8c6653/pydantic_settings-2.14.1.tar.gz", hash = "sha256:e874d3bec7e787b0c9958277956ed9b4dd5de6a80e162188fdaff7c5e26fd5fa", size = 235551, upload-time = "2026-05-08T13:40:06.542Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ae/8d/f1af3832f5e6eb13ba94ee809e72b8ecb5eef226d27ee0bef7d963d943c7/pydantic_settings-2.14.1-py3-none-any.whl", hash = "sha256:6e3c7edfd8277687cdc598f56e5cff0e9bfff0910a3749deaa8d4401c3a2b9de", size = 60964, upload-time = "2026-05-08T13:40:04.958Z" }, +] + +[[package]] +name = "pygments" +version = "2.20.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, +] + +[[package]] +name = "pyjwt" +version = "2.13.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3b/81/58d0ac84e1ef3a3843791d6954d94c0b33d526c75eeb1efbce9d0a4c4077/pyjwt-2.13.0.tar.gz", hash = "sha256:41571c89ca91598c79e8ef18a2d07367d4810fbbd6f637794879baf1b7703423", size = 107515, upload-time = "2026-05-21T19:54:36.618Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a3/5e/ecf12fdb62546d64385c158514e9b2b671f7832108ef2ecd2020ce0af2d1/pyjwt-2.13.0-py3-none-any.whl", hash = "sha256:66adcc2aff09b3f1bbd95fc1e1577df8ac8723c978552fd43304c8a290ac5728", size = 31274, upload-time = "2026-05-21T19:54:35.362Z" }, +] + +[package.optional-dependencies] +crypto = [ + { name = "cryptography" }, +] + +[[package]] +name = "pyopenssl" +version = "26.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1a/51/27a5ad5f939d08f690a326ef9582cda7140555180db71695f6fb747d6a36/pyopenssl-26.2.0.tar.gz", hash = "sha256:8c6fcecd1183a7fc897548dfe388b0cdb7f37e018200d8409cf33959dbe35387", size = 182195, upload-time = "2026-05-04T23:06:09.72Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/73/b8/a0e2790ae249d6f38c9f66de7a211621a7ab2650217bcd04e1262f578a56/pyopenssl-26.2.0-py3-none-any.whl", hash = "sha256:4f9d971bc5298b8bc1fab282803da04bf000c755d4ad9d99b52de2569ca19a70", size = 55823, upload-time = "2026-05-04T23:06:08.395Z" }, +] + +[[package]] +name = "pyparsing" +version = "3.3.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/91/9c6ee907786a473bf81c5f53cf703ba0957b23ab84c264080fb5a450416f/pyparsing-3.3.2.tar.gz", hash = "sha256:c777f4d763f140633dcb6d8a3eda953bf7a214dc4eff598413c070bcdc117cbc", size = 6851574, upload-time = "2026-01-21T03:57:59.36Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl", hash = "sha256:850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d", size = 122781, upload-time = "2026-01-21T03:57:55.912Z" }, +] + +[[package]] +name = "pytest" +version = "8.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a3/5c/00a0e072241553e1a7496d638deababa67c5058571567b92a7eaa258397c/pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", size = 1519618, upload-time = "2025-09-04T14:34:22.711Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79", size = 365750, upload-time = "2025-09-04T14:34:20.226Z" }, +] + +[[package]] +name = "pytest-asyncio" +version = "0.26.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8e/c4/453c52c659521066969523e87d85d54139bbd17b78f09532fb8eb8cdb58e/pytest_asyncio-0.26.0.tar.gz", hash = "sha256:c4df2a697648241ff39e7f0e4a73050b03f123f760673956cf0d72a4990e312f", size = 54156, upload-time = "2025-03-25T06:22:28.883Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/7f/338843f449ace853647ace35870874f69a764d251872ed1b4de9f234822c/pytest_asyncio-0.26.0-py3-none-any.whl", hash = "sha256:7b51ed894f4fbea1340262bdae5135797ebbe21d8638978e35d31c6d19f72fb0", size = 19694, upload-time = "2025-03-25T06:22:27.807Z" }, +] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, +] + +[[package]] +name = "python-dotenv" +version = "1.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/82/ed/0301aeeac3e5353ef3d94b6ec08bbcabd04a72018415dcb29e588514bba8/python_dotenv-1.2.2.tar.gz", hash = "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3", size = 50135, upload-time = "2026-03-01T16:00:26.196Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a", size = 22101, upload-time = "2026-03-01T16:00:25.09Z" }, +] + +[[package]] +name = "python-multipart" +version = "0.0.29" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4e/fe/70bd71a6738b09a0bdf6480ca6436b167469ca4578b2a0efbe390b4b0e70/python_multipart-0.0.29.tar.gz", hash = "sha256:643e93849196645e2dbdd81a0f8829a23123ad7f797a84a364c6fb3563f18904", size = 45678, upload-time = "2026-05-17T17:29:47.654Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8f/cb/769cfc37177252872a45a71f3fbdde9d51b471a3f3c14bfe95dde3407386/python_multipart-0.0.29-py3-none-any.whl", hash = "sha256:2ddcc971cef266225f54f552d8fa10bcfbb1f14446caec199060daac59ff2d69", size = 29640, upload-time = "2026-05-17T17:29:45.69Z" }, +] + +[[package]] +name = "pywin32" +version = "311" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7c/af/449a6a91e5d6db51420875c54f6aff7c97a86a3b13a0b4f1a5c13b988de3/pywin32-311-cp311-cp311-win32.whl", hash = "sha256:184eb5e436dea364dcd3d2316d577d625c0351bf237c4e9a5fabbcfa5a58b151", size = 8697031, upload-time = "2025-07-14T20:13:13.266Z" }, + { url = "https://files.pythonhosted.org/packages/51/8f/9bb81dd5bb77d22243d33c8397f09377056d5c687aa6d4042bea7fbf8364/pywin32-311-cp311-cp311-win_amd64.whl", hash = "sha256:3ce80b34b22b17ccbd937a6e78e7225d80c52f5ab9940fe0506a1a16f3dab503", size = 9508308, upload-time = "2025-07-14T20:13:15.147Z" }, + { url = "https://files.pythonhosted.org/packages/44/7b/9c2ab54f74a138c491aba1b1cd0795ba61f144c711daea84a88b63dc0f6c/pywin32-311-cp311-cp311-win_arm64.whl", hash = "sha256:a733f1388e1a842abb67ffa8e7aad0e70ac519e09b0f6a784e65a136ec7cefd2", size = 8703930, upload-time = "2025-07-14T20:13:16.945Z" }, + { url = "https://files.pythonhosted.org/packages/e7/ab/01ea1943d4eba0f850c3c61e78e8dd59757ff815ff3ccd0a84de5f541f42/pywin32-311-cp312-cp312-win32.whl", hash = "sha256:750ec6e621af2b948540032557b10a2d43b0cee2ae9758c54154d711cc852d31", size = 8706543, upload-time = "2025-07-14T20:13:20.765Z" }, + { url = "https://files.pythonhosted.org/packages/d1/a8/a0e8d07d4d051ec7502cd58b291ec98dcc0c3fff027caad0470b72cfcc2f/pywin32-311-cp312-cp312-win_amd64.whl", hash = "sha256:b8c095edad5c211ff31c05223658e71bf7116daa0ecf3ad85f3201ea3190d067", size = 9495040, upload-time = "2025-07-14T20:13:22.543Z" }, + { url = "https://files.pythonhosted.org/packages/ba/3a/2ae996277b4b50f17d61f0603efd8253cb2d79cc7ae159468007b586396d/pywin32-311-cp312-cp312-win_arm64.whl", hash = "sha256:e286f46a9a39c4a18b319c28f59b61de793654af2f395c102b4f819e584b5852", size = 8710102, upload-time = "2025-07-14T20:13:24.682Z" }, + { url = "https://files.pythonhosted.org/packages/a5/be/3fd5de0979fcb3994bfee0d65ed8ca9506a8a1260651b86174f6a86f52b3/pywin32-311-cp313-cp313-win32.whl", hash = "sha256:f95ba5a847cba10dd8c4d8fefa9f2a6cf283b8b88ed6178fa8a6c1ab16054d0d", size = 8705700, upload-time = "2025-07-14T20:13:26.471Z" }, + { url = "https://files.pythonhosted.org/packages/e3/28/e0a1909523c6890208295a29e05c2adb2126364e289826c0a8bc7297bd5c/pywin32-311-cp313-cp313-win_amd64.whl", hash = "sha256:718a38f7e5b058e76aee1c56ddd06908116d35147e133427e59a3983f703a20d", size = 9494700, upload-time = "2025-07-14T20:13:28.243Z" }, + { url = "https://files.pythonhosted.org/packages/04/bf/90339ac0f55726dce7d794e6d79a18a91265bdf3aa70b6b9ca52f35e022a/pywin32-311-cp313-cp313-win_arm64.whl", hash = "sha256:7b4075d959648406202d92a2310cb990fea19b535c7f4a78d3f5e10b926eeb8a", size = 8709318, upload-time = "2025-07-14T20:13:30.348Z" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" }, + { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" }, + { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" }, + { url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" }, + { url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" }, + { url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" }, + { url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" }, + { url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" }, + { url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" }, + { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, + { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, + { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, + { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, + { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, + { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, + { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, + { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, + { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, + { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, + { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, + { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, + { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, + { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, + { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, + { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, + { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, + { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, +] + +[[package]] +name = "referencing" +version = "0.37.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "rpds-py" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/22/f5/df4e9027acead3ecc63e50fe1e36aca1523e1719559c499951bb4b53188f/referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8", size = 78036, upload-time = "2025-10-13T15:30:48.871Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231", size = 26766, upload-time = "2025-10-13T15:30:47.625Z" }, +] + +[[package]] +name = "regex" +version = "2026.5.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/dc/0e/49aee608ad09480e7fd276898c99ec6192985fa331abe4eb3a986094490b/regex-2026.5.9.tar.gz", hash = "sha256:a8234aa23ec39894bfe4a3f1b85616a7032481964a13ac6fc9f10de4f6fca270", size = 416074, upload-time = "2026-05-09T23:15:19.37Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/dc/c1f2df4027e82fc54b5a473e4b250f5139faca49a0fbe29a48668d228f34/regex-2026.5.9-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ccf5249114cc3e772ecdd88a98a86eca0fd74c61ce32a94743758c083fc05d48", size = 489445, upload-time = "2026-05-09T23:12:06.111Z" }, + { url = "https://files.pythonhosted.org/packages/03/d2/59f01110660081cce9c0bc30ebd0b5ee250dacf658e3248ed92f01e0e8ee/regex-2026.5.9-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:46f1326ca6e65b0879d23ca302c0f2415aad42ff0309b9c818e7949fe19a41d8", size = 291271, upload-time = "2026-05-09T23:12:07.731Z" }, + { url = "https://files.pythonhosted.org/packages/58/b6/14b2c84ff90ddb370c81d27503f4a0fcf071496416f4855f6cc8c5d81c35/regex-2026.5.9-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ef31cbfe458e21c6122ba8150ff060e0c7789ed0d26eb423f25472584920b555", size = 289212, upload-time = "2026-05-09T23:12:09.266Z" }, + { url = "https://files.pythonhosted.org/packages/03/d0/4db86529117320de0c84afd90e70bb47434625875e34fcef9d8c127c5b16/regex-2026.5.9-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:992604d02e6d9c6d786c24a706a71ecffe1020fc1ef264044474cd81fa2c3919", size = 792310, upload-time = "2026-05-09T23:12:11.416Z" }, + { url = "https://files.pythonhosted.org/packages/07/78/fe4800cd322f862ecffd2d553409b20d80650e5ed71b9d178f853d020b82/regex-2026.5.9-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c9411dd64ca95477225734a93dfc8583b51916b8d5942f99d6cac21e09965451", size = 861721, upload-time = "2026-05-09T23:12:13.681Z" }, + { url = "https://files.pythonhosted.org/packages/b5/d0/b3618a895dd8feb897c61bb2954edd265e1767d82a01d53065d5871127a3/regex-2026.5.9-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3dd4a3ff360dfb836fecdb93a4598f9d6e2ac81e3e397125145c6221bf58cf4c", size = 906460, upload-time = "2026-05-09T23:12:15.443Z" }, + { url = "https://files.pythonhosted.org/packages/33/6f/1481597e859ef19508b345eec4afd1416ed6e6b459c75a64026ef193aecf/regex-2026.5.9-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2a661a7d270a61f7cf460caee8b9fa2d5ef9e5c681234bcb9e0fe14f488e7dfc", size = 799843, upload-time = "2026-05-09T23:12:16.892Z" }, + { url = "https://files.pythonhosted.org/packages/73/59/955734c803f59108deccba3597ae440c76b62a652733c0006e6243758420/regex-2026.5.9-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f079e50a0d3cc3cd5091fa9ff45869a2e6b2cd35895731edafb0327901a8d86d", size = 773610, upload-time = "2026-05-09T23:12:19.127Z" }, + { url = "https://files.pythonhosted.org/packages/68/8f/70c04a236d651c81881dac42ef8538bddda6121434509d0a22d9e601503b/regex-2026.5.9-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4ebe8f0b5ec5a5024dc4a4c59f444c4e9afc5f2abdbb8962065b75d27fb971f9", size = 781645, upload-time = "2026-05-09T23:12:20.806Z" }, + { url = "https://files.pythonhosted.org/packages/1d/96/05c7434d88185e5d27fe54aeb74df86bd77cd79f52f0b4eae54faa8fea70/regex-2026.5.9-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:97cf3bc1b7d7d2306772ec07366c80d9df00ff79e79cea32898883a646d2fae2", size = 854473, upload-time = "2026-05-09T23:12:22.465Z" }, + { url = "https://files.pythonhosted.org/packages/4e/c1/6e3d8202d981f3117004bf341ee74893ba4ba8a9fbaf4b94615846550a08/regex-2026.5.9-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:0f9eede6a5cbdc02d4978090186390936e1776a7d1359b21e41014c609880bcf", size = 763311, upload-time = "2026-05-09T23:12:24.351Z" }, + { url = "https://files.pythonhosted.org/packages/93/c7/e7737f1526b3fb32bd4c337fd6c71c3ebb5c8296fc34d11197e0955d2e35/regex-2026.5.9-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:01f0f5f55f4b64dacec85dc116d3c05fd23ad3ff037bbc73a2085775953c2611", size = 844593, upload-time = "2026-05-09T23:12:26.341Z" }, + { url = "https://files.pythonhosted.org/packages/a5/27/0daffb1a535bb39f422c3d200f4ab023c71110ad66a32b366bee708baba0/regex-2026.5.9-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1268eddd8486dc561d08eee1156e40aa3a8fe10f4bdec8fa653b455fcbffd12c", size = 789167, upload-time = "2026-05-09T23:12:27.975Z" }, + { url = "https://files.pythonhosted.org/packages/ce/fc/294fe4fac4f2ed67207b17471815870c1c45b3a489e08e0ac96daea16ef6/regex-2026.5.9-cp311-cp311-win32.whl", hash = "sha256:8676474c07469d6f33dd1085ca2cd45f65785f32518f2b20e36d9953ca07f994", size = 266249, upload-time = "2026-05-09T23:12:30.141Z" }, + { url = "https://files.pythonhosted.org/packages/d0/b0/8dce459f6245bcf8f6e9f23ac9569f1a0f15c131cc0745e82b43226204cf/regex-2026.5.9-cp311-cp311-win_amd64.whl", hash = "sha256:246de9d60aa3f8538b519834dd95cbf276ea263d6a7bd5a3666dc3fa0230505b", size = 278423, upload-time = "2026-05-09T23:12:31.676Z" }, + { url = "https://files.pythonhosted.org/packages/db/8d/f9aeff6ad63a3ef720386f2907e6d34a35a510a6e498ebad28b0fb3f6ab6/regex-2026.5.9-cp311-cp311-win_arm64.whl", hash = "sha256:d726ca3f0d76969bf1e8e477d160d3d666bbf999f6860bd314889e5345782046", size = 270420, upload-time = "2026-05-09T23:12:33.194Z" }, + { url = "https://files.pythonhosted.org/packages/50/9b/6550044bc44e17c84d312c031c2ec42fbdb6a4ec4e29093be3a172d08772/regex-2026.5.9-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:57eeeb05db7979413dec5438f2db21d7ecbba787cde7a711df1a6f6df672aa06", size = 490451, upload-time = "2026-05-09T23:12:34.72Z" }, + { url = "https://files.pythonhosted.org/packages/1e/95/fc7ba4303b5a0f92446a12ee6778ef2c6c799233f5060042a31bf390cfe9/regex-2026.5.9-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:398c521292f4c7fb807001dcd54694d3a1fcafc179a36ad9cc56f98df85930b6", size = 292112, upload-time = "2026-05-09T23:12:36.285Z" }, + { url = "https://files.pythonhosted.org/packages/54/4b/ee27938d1b2c443e89a9a10e00d2d19aa5ee300cd3d61140644e93bb083e/regex-2026.5.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f7a7c26137296beba7784de6eba69c6a93a63ccebc385e4962fe67e267a91225", size = 289599, upload-time = "2026-05-09T23:12:38.089Z" }, + { url = "https://files.pythonhosted.org/packages/d8/dd/ba103dc19614e25f3880800ca67ce093d6e21b325d72b8383c7bf906e9fa/regex-2026.5.9-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6441cc660d76107934a09c22167200839a0e89604a6297f78a974e66e931d2c0", size = 796732, upload-time = "2026-05-09T23:12:40.062Z" }, + { url = "https://files.pythonhosted.org/packages/cf/e7/f035b4fd858b050b0080bf302968dc0f59ba34e391872d54936758e6844e/regex-2026.5.9-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:91328f1c23d47595ca3ef0a7557fa129c5a23404b775c770697d2f35b33e0107", size = 865440, upload-time = "2026-05-09T23:12:42.059Z" }, + { url = "https://files.pythonhosted.org/packages/0a/51/8cd301ecc899aea28124357f729f4272f44de7806fc7ca02490bfbe253e8/regex-2026.5.9-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:93a7860539414dddaefba2b40f8771765ae17949d4c7182b876ce429e11a8309", size = 912329, upload-time = "2026-05-09T23:12:44.373Z" }, + { url = "https://files.pythonhosted.org/packages/cc/1e/3fbe2fa1e8cebd62f3bb7d3321cff1640aca2e240b51d9bd624aad949260/regex-2026.5.9-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dd2810d22146b6d838acc5ec15602cb6b47920aa4e33015df3868eedfd20bab8", size = 801239, upload-time = "2026-05-09T23:12:46.268Z" }, + { url = "https://files.pythonhosted.org/packages/17/2f/6f6008682bf2cf98040a0d3153a8e557b6ab728d7713d045cee4ce544ab8/regex-2026.5.9-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:daff2bdbaf1d23e52fdff7c0b7bc2048b68f978df6a4d107ac981f94caef2e66", size = 777054, upload-time = "2026-05-09T23:12:48.051Z" }, + { url = "https://files.pythonhosted.org/packages/19/2b/eee0d20a6842ba04df4b8847a920b57ef56853f14ef85405473e586b605a/regex-2026.5.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4eeb011098fcb77af513dcef521a3dbecbf8849b1e38940759d293b7a93f5026", size = 785098, upload-time = "2026-05-09T23:12:49.851Z" }, + { url = "https://files.pythonhosted.org/packages/4a/98/6fc1e6410feefb92159edaed5041992bfe390e8d26c721865434acbca558/regex-2026.5.9-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:ea9c8ecfa1b73c73b626534d6626e5340d429630943672b8480724f44e84b962", size = 860095, upload-time = "2026-05-09T23:12:51.666Z" }, + { url = "https://files.pythonhosted.org/packages/18/a3/bd855e0f2cb1a978ecf6fa6bb69632dd9c3f6ea3b81cde62fde14c9daec7/regex-2026.5.9-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:cd2846168eb9ee3c513902bc8225409cb1caab31d04728b145171fa1625d9621", size = 765762, upload-time = "2026-05-09T23:12:53.413Z" }, + { url = "https://files.pythonhosted.org/packages/dc/66/0ae8c092e60b14c79d24f8e0b7f0aea5bfbffdcab00b5483d13404d3c3a5/regex-2026.5.9-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:39617fb0cde9c0e6306dc70e3bfc096f3da793219879f7ae7aa341a69fbdcf6d", size = 852100, upload-time = "2026-05-09T23:12:55.256Z" }, + { url = "https://files.pythonhosted.org/packages/21/de/8dfde60fc1b21c946a893ba273403b72617edb261370cb1087099a83f088/regex-2026.5.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fd03c4f0e33280d15cae17159b899245d6b7c53d21def19b263b39655061f5ce", size = 789479, upload-time = "2026-05-09T23:12:57.573Z" }, + { url = "https://files.pythonhosted.org/packages/c3/1c/bdcc98f9a4af4fdd166c74941174619ccff4726d3ce32faa8e9a2ecd38dd/regex-2026.5.9-cp312-cp312-win32.whl", hash = "sha256:164eba9b755ea6f244b0d881196fbc1fac09714e9782c9e2732b813142033c8e", size = 266699, upload-time = "2026-05-09T23:12:59.14Z" }, + { url = "https://files.pythonhosted.org/packages/78/87/240d36864f9e48ace85f72e79ced97ceb7f27ce87739a947dcb834b4e6bc/regex-2026.5.9-cp312-cp312-win_amd64.whl", hash = "sha256:86f40a5d6444db30a125c9c9177e6b25dad981cbc37451fd838f145e6edac92e", size = 277783, upload-time = "2026-05-09T23:13:00.789Z" }, + { url = "https://files.pythonhosted.org/packages/4f/b5/7b30f312b0669dff5beebe5b0989dc2d1a312b1a44fab852199c387a5b96/regex-2026.5.9-cp312-cp312-win_arm64.whl", hash = "sha256:96f5f58b54a063d7ea9dca08e1cf57bfe10499c4d579ee672da284f57f5f0070", size = 270513, upload-time = "2026-05-09T23:13:02.426Z" }, + { url = "https://files.pythonhosted.org/packages/aa/da/797e91ecec6f84135da778ddce78c20e0af5d2a15c26f87a81bc3eadb6db/regex-2026.5.9-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:d626b84406444b165fc0ba981604edea39f0588ff1f92baa23fe50799ea9afdb", size = 490303, upload-time = "2026-05-09T23:13:04.382Z" }, + { url = "https://files.pythonhosted.org/packages/44/da/bf30abaaa737b58f4a4b8c4a03659e02fd92092c822e0197ed9e0daab917/regex-2026.5.9-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d7bdc0ab8f3dd7e1b4f9ab88634e13374669db86bb3c72e8292f07ae313f539f", size = 292019, upload-time = "2026-05-09T23:13:06.022Z" }, + { url = "https://files.pythonhosted.org/packages/2d/e7/d0eaf5713828417b9e5648cf81fa9bacd4961f6ab98c380c2034f8716e35/regex-2026.5.9-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a8820737949116ffff55fe18f9fc644530063ba6ebfcb8314239416e78f1347c", size = 289468, upload-time = "2026-05-09T23:13:08.214Z" }, + { url = "https://files.pythonhosted.org/packages/d3/9b/b3fdd62b003baa1a9b593cd8c8699c9651c2e80cc21a5c715707983c42d7/regex-2026.5.9-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa0fbdbac82cb3e4450d0ccde7d7a35607f4cb2dd9fba4b8b69bfaf8c9fa6aed", size = 796749, upload-time = "2026-05-09T23:13:10.573Z" }, + { url = "https://files.pythonhosted.org/packages/d4/30/66ab84588765f5b4b271a9ca09ef7ce2b87caa95176ec3d2ad65d7bc4902/regex-2026.5.9-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:57e8915c7986aa33d25e4d3629cef711cd2863f2961b10409f0c04cb8b7d9020", size = 865445, upload-time = "2026-05-09T23:13:12.523Z" }, + { url = "https://files.pythonhosted.org/packages/1a/89/f05169e8588aac365f35ffc7f3bc3184f095ef4cfded7cfaa3c7fd5dbd89/regex-2026.5.9-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:508f56a89ba9cb26e4168cbc37dbd60a28d82430a9e18ad1d25fe0883c314ca2", size = 912322, upload-time = "2026-05-09T23:13:14.281Z" }, + { url = "https://files.pythonhosted.org/packages/30/e1/c93444052cf41581f3c884ab3fb5823daf0992f11cd4388d4275ca610558/regex-2026.5.9-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b6d189041f15691cfa2b6c4290448ec221244d225b3f5fe9e7771b34ffcdf6e2", size = 801269, upload-time = "2026-05-09T23:13:16.569Z" }, + { url = "https://files.pythonhosted.org/packages/50/fe/0cf96b882f540e62e8b9956599798203d599c44cf4c77917ca27400ff69b/regex-2026.5.9-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e82db382b44d0111b22601c509c89f64434816c9e0eef9d1989cda8cc6ff1c04", size = 777085, upload-time = "2026-05-09T23:13:18.675Z" }, + { url = "https://files.pythonhosted.org/packages/23/5c/d78d4924e7fc875557b9e9b768423925fdfaac5549d06da7810019a9bd26/regex-2026.5.9-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2acfb48634f64996b57f90f39afa692ff362162722581921fe92239a59960f3c", size = 785153, upload-time = "2026-05-09T23:13:20.525Z" }, + { url = "https://files.pythonhosted.org/packages/bf/e0/5214774090e7b4524dcea3e3c4aa74141d43043f8beb49c1599db1c8b53a/regex-2026.5.9-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:d29eebfc9525db68cad3c97eedd7f754fa265aa5cd0cf4f863b2421e1b48fc9f", size = 860164, upload-time = "2026-05-09T23:13:22.263Z" }, + { url = "https://files.pythonhosted.org/packages/6e/e1/4a57a83350319b1271f0d7a249b8672513ed928b237a741631270de6caea/regex-2026.5.9-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:debb893095e944091c16e641a6e33c1b0f4cb61ab945ec5afbf53ce7068834d8", size = 765731, upload-time = "2026-05-09T23:13:24.277Z" }, + { url = "https://files.pythonhosted.org/packages/12/f4/499e74a20c156fc75836ee04a72a38d1a063978f600937f9760467beb1b0/regex-2026.5.9-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:d659eee77986549c9ea45b861c7567e44d6287c3dc9a4565478853f7b9fe2ff6", size = 852062, upload-time = "2026-05-09T23:13:26.125Z" }, + { url = "https://files.pythonhosted.org/packages/5b/92/7eebc0d0a01e78629695f342ba17e0deaff8fb45e79cc0d7b98287da6e3e/regex-2026.5.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2efa205e6d98b24d1f3ab395c11aa15cdf10935bca283d0285e0499c284fba21", size = 789577, upload-time = "2026-05-09T23:13:27.814Z" }, + { url = "https://files.pythonhosted.org/packages/05/a4/018e71f7d2ad48c1ebe6d3ae0026f9b7cb4802fd15c7cc02fdf724355102/regex-2026.5.9-cp313-cp313-win32.whl", hash = "sha256:f3844f134e834076677dd369976e9f5068679fcb8e50102fdf6b7ac96a3ec127", size = 266691, upload-time = "2026-05-09T23:13:29.549Z" }, + { url = "https://files.pythonhosted.org/packages/e6/1d/861a93719fb9ee7dbfc3761b3797b7a3e112a5d42c6129459d2d741be9b5/regex-2026.5.9-cp313-cp313-win_amd64.whl", hash = "sha256:3527bb4942d2c14552155406cdedd906567456821848aed1cb4933a391bf5eca", size = 277747, upload-time = "2026-05-09T23:13:31.859Z" }, + { url = "https://files.pythonhosted.org/packages/d9/c6/0a2436ae4da1ba76e51cb98943c6838a9a721faa40ebe2dce07694ae34e3/regex-2026.5.9-cp313-cp313-win_arm64.whl", hash = "sha256:56a33f191f17d8c417f99945ebdc1e691d3af9605d86ec68c7e54a57e3e17af6", size = 270500, upload-time = "2026-05-09T23:13:33.525Z" }, + { url = "https://files.pythonhosted.org/packages/e8/e9/d21346f7b60ed58789371358ed66b09d00f832e1bd7c06e55d9da5679882/regex-2026.5.9-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:01f28d868834624c934b8d2e0aa1c8341337e37831f4a012f18a5afcba4cbaf3", size = 494172, upload-time = "2026-05-09T23:13:35.935Z" }, + { url = "https://files.pythonhosted.org/packages/c4/43/fd1177a2032037c681baecdb3422ee4e1424aec4e4f470ef47793d325274/regex-2026.5.9-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:48036f6374aaa79eb3b754ec29c61d1c6b1606749d705a13f8854fa2539671f6", size = 293952, upload-time = "2026-05-09T23:13:38.307Z" }, + { url = "https://files.pythonhosted.org/packages/f2/7d/9fbf919768368d3f8a4f6c692cf2aa61e482b2b81ec6a298ace4cbf02480/regex-2026.5.9-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b96350aa424e79d4fd6b567b344dcbe2b2d6bfc48dfe7717587e1fa6d43da6ff", size = 292314, upload-time = "2026-05-09T23:13:40.353Z" }, + { url = "https://files.pythonhosted.org/packages/e2/6c/e41bfeecb589716843e7c4df09ba46ff2a42961457afece19059d85caeef/regex-2026.5.9-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8f3af7a4903c5c04a11a196a5aa75cdd7dd3f8508132f9fb3259d9f5908e3b88", size = 811681, upload-time = "2026-05-09T23:13:42.543Z" }, + { url = "https://files.pythonhosted.org/packages/87/83/a5c1c525fba0aa656e88ad0face0b1829788ef4c2fb6b26df58aa1151b84/regex-2026.5.9-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7e87577720152d2caae19fe2baaf1f8d5ca12091e9e229f03915c37d1e4b9178", size = 871135, upload-time = "2026-05-09T23:13:44.326Z" }, + { url = "https://files.pythonhosted.org/packages/18/d4/80882e799e440dd878b0979cbebf8fa4d54624a332c83037c7a701649e3f/regex-2026.5.9-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c8b9b9d294cfea3cd19c718ade7cc93492b2c4991abd9a68d0b3477ae6d8e100", size = 917265, upload-time = "2026-05-09T23:13:47.295Z" }, + { url = "https://files.pythonhosted.org/packages/ae/ff/8db60211e2286e396aad7dc7725356c502bff0901ea05bd6cdc2e1a042b9/regex-2026.5.9-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:728d8bfd28a8845c8b6bc5dc7ce010453d206396786c0765c2740cb65f37791e", size = 816311, upload-time = "2026-05-09T23:13:49.885Z" }, + { url = "https://files.pythonhosted.org/packages/4c/47/742ef579c61730f8d268e5cf1f9ce0e37e2ea041ad0f5644724f2378e463/regex-2026.5.9-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7e30b874d341fac767d7df5a0870540541c2c054b80cfaac116e8d367a8a7ff2", size = 785498, upload-time = "2026-05-09T23:13:52.25Z" }, + { url = "https://files.pythonhosted.org/packages/7f/ab/cb0999802dcb0fb95b1ab005e8d4163d8afdd67efc2cb6b6630ac13f8cb1/regex-2026.5.9-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:fd190e88a895a8901325fad284a3f74ea52b1da8525b76cc811fa9b1edf0ce2b", size = 801348, upload-time = "2026-05-09T23:13:54.127Z" }, + { url = "https://files.pythonhosted.org/packages/7d/62/8ca59a24c55bc34d166eefaf3717bd77772f329fdbf984d86581e0a3571c/regex-2026.5.9-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:8e76e8161ad00694cfce6767d5dea860c6391ac5b83e5c3a39661e696f11fc7e", size = 866493, upload-time = "2026-05-09T23:13:56.067Z" }, + { url = "https://files.pythonhosted.org/packages/8d/3d/30f2ae62cef3278bb5bb821f467277a55fb73f01032cf85997e15e8289a8/regex-2026.5.9-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:ddda5340e6c01a293027dd46232fa79eaff1b48058ce7a98f572b6445b088041", size = 772811, upload-time = "2026-05-09T23:13:57.867Z" }, + { url = "https://files.pythonhosted.org/packages/d8/ae/7d2089bcd78ad0c0161bc684339df50032acb438a7bd3305e7ddb1193cec/regex-2026.5.9-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:205109e96b3cf5adf8f4cd62bedde9487feb282b9497a3535451e5a24cd706a0", size = 856584, upload-time = "2026-05-09T23:13:59.679Z" }, + { url = "https://files.pythonhosted.org/packages/a9/29/92ff47f75990131ea4f24ba17819e5a9d141e10819807e09addd73409af6/regex-2026.5.9-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dfbe4579b9f08036aa7d101d1835437a20783574ac66327e6b29b4018a138081", size = 803453, upload-time = "2026-05-09T23:14:01.978Z" }, + { url = "https://files.pythonhosted.org/packages/04/99/eff29f1037dcab36702c9ee5d6858cf1ce2336ea8ea2987f64245b99ea5e/regex-2026.5.9-cp313-cp313t-win32.whl", hash = "sha256:ed2c9e8068b614c574d8d30e543d617cf5379b0535d46f97ef00e904745a08b5", size = 269951, upload-time = "2026-05-09T23:14:03.661Z" }, + { url = "https://files.pythonhosted.org/packages/0e/9d/8870b8981d27b22cda77bb26a5ac7ebfa9c7d9e0dea195a834a82380e748/regex-2026.5.9-cp313-cp313t-win_amd64.whl", hash = "sha256:b46b0f094dc1d3b90356c85a0bd2c9bafc4a6a190b9d6f8ddd5a033b6e088ed4", size = 281240, upload-time = "2026-05-09T23:14:05.56Z" }, + { url = "https://files.pythonhosted.org/packages/72/b1/3379415e8f135c13ac551353397cc4fe97b4978f3cac73c5fcbcded548b8/regex-2026.5.9-cp313-cp313t-win_arm64.whl", hash = "sha256:872acc074bd29ffc9913ecdfedf6ea77502312ca44a4aa0d3779089c6069d8de", size = 272383, upload-time = "2026-05-09T23:14:07.843Z" }, +] + +[[package]] +name = "requests" +version = "2.34.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ac/c3/e2a2b89f2d3e2179abd6d00ebd70bff6273f37fb3e0cc209f48b39d00cbf/requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed", size = 142856, upload-time = "2026-05-14T19:25:27.735Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0", size = 73075, upload-time = "2026-05-14T19:25:26.443Z" }, +] + +[[package]] +name = "rich" +version = "15.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c0/8f/0722ca900cc807c13a6a0c696dacf35430f72e0ec571c4275d2371fca3e9/rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36", size = 230680, upload-time = "2026-04-12T08:24:00.75Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb", size = 310654, upload-time = "2026-04-12T08:24:02.83Z" }, +] + +[[package]] +name = "rouge-score" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "absl-py" }, + { name = "nltk" }, + { name = "numpy" }, + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e2/c5/9136736c37022a6ad27fea38f3111eb8f02fe75d067f9a985cc358653102/rouge_score-0.1.2.tar.gz", hash = "sha256:c7d4da2683e68c9abf0135ef915d63a46643666f848e558a1b9f7ead17ff0f04", size = 17400, upload-time = "2022-07-22T22:46:22.909Z" } + +[[package]] +name = "rpds-py" +version = "0.30.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/20/af/3f2f423103f1113b36230496629986e0ef7e199d2aa8392452b484b38ced/rpds_py-0.30.0.tar.gz", hash = "sha256:dd8ff7cf90014af0c0f787eea34794ebf6415242ee1d6fa91eaba725cc441e84", size = 69469, upload-time = "2025-11-30T20:24:38.837Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4d/6e/f964e88b3d2abee2a82c1ac8366da848fce1c6d834dc2132c3fda3970290/rpds_py-0.30.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a2bffea6a4ca9f01b3f8e548302470306689684e61602aa3d141e34da06cf425", size = 370157, upload-time = "2025-11-30T20:21:53.789Z" }, + { url = "https://files.pythonhosted.org/packages/94/ba/24e5ebb7c1c82e74c4e4f33b2112a5573ddc703915b13a073737b59b86e0/rpds_py-0.30.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dc4f992dfe1e2bc3ebc7444f6c7051b4bc13cd8e33e43511e8ffd13bf407010d", size = 359676, upload-time = "2025-11-30T20:21:55.475Z" }, + { url = "https://files.pythonhosted.org/packages/84/86/04dbba1b087227747d64d80c3b74df946b986c57af0a9f0c98726d4d7a3b/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:422c3cb9856d80b09d30d2eb255d0754b23e090034e1deb4083f8004bd0761e4", size = 389938, upload-time = "2025-11-30T20:21:57.079Z" }, + { url = "https://files.pythonhosted.org/packages/42/bb/1463f0b1722b7f45431bdd468301991d1328b16cffe0b1c2918eba2c4eee/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:07ae8a593e1c3c6b82ca3292efbe73c30b61332fd612e05abee07c79359f292f", size = 402932, upload-time = "2025-11-30T20:21:58.47Z" }, + { url = "https://files.pythonhosted.org/packages/99/ee/2520700a5c1f2d76631f948b0736cdf9b0acb25abd0ca8e889b5c62ac2e3/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:12f90dd7557b6bd57f40abe7747e81e0c0b119bef015ea7726e69fe550e394a4", size = 525830, upload-time = "2025-11-30T20:21:59.699Z" }, + { url = "https://files.pythonhosted.org/packages/e0/ad/bd0331f740f5705cc555a5e17fdf334671262160270962e69a2bdef3bf76/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:99b47d6ad9a6da00bec6aabe5a6279ecd3c06a329d4aa4771034a21e335c3a97", size = 412033, upload-time = "2025-11-30T20:22:00.991Z" }, + { url = "https://files.pythonhosted.org/packages/f8/1e/372195d326549bb51f0ba0f2ecb9874579906b97e08880e7a65c3bef1a99/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:33f559f3104504506a44bb666b93a33f5d33133765b0c216a5bf2f1e1503af89", size = 390828, upload-time = "2025-11-30T20:22:02.723Z" }, + { url = "https://files.pythonhosted.org/packages/ab/2b/d88bb33294e3e0c76bc8f351a3721212713629ffca1700fa94979cb3eae8/rpds_py-0.30.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:946fe926af6e44f3697abbc305ea168c2c31d3e3ef1058cf68f379bf0335a78d", size = 404683, upload-time = "2025-11-30T20:22:04.367Z" }, + { url = "https://files.pythonhosted.org/packages/50/32/c759a8d42bcb5289c1fac697cd92f6fe01a018dd937e62ae77e0e7f15702/rpds_py-0.30.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:495aeca4b93d465efde585977365187149e75383ad2684f81519f504f5c13038", size = 421583, upload-time = "2025-11-30T20:22:05.814Z" }, + { url = "https://files.pythonhosted.org/packages/2b/81/e729761dbd55ddf5d84ec4ff1f47857f4374b0f19bdabfcf929164da3e24/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9a0ca5da0386dee0655b4ccdf46119df60e0f10da268d04fe7cc87886872ba7", size = 572496, upload-time = "2025-11-30T20:22:07.713Z" }, + { url = "https://files.pythonhosted.org/packages/14/f6/69066a924c3557c9c30baa6ec3a0aa07526305684c6f86c696b08860726c/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8d6d1cc13664ec13c1b84241204ff3b12f9bb82464b8ad6e7a5d3486975c2eed", size = 598669, upload-time = "2025-11-30T20:22:09.312Z" }, + { url = "https://files.pythonhosted.org/packages/5f/48/905896b1eb8a05630d20333d1d8ffd162394127b74ce0b0784ae04498d32/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3896fa1be39912cf0757753826bc8bdc8ca331a28a7c4ae46b7a21280b06bb85", size = 561011, upload-time = "2025-11-30T20:22:11.309Z" }, + { url = "https://files.pythonhosted.org/packages/22/16/cd3027c7e279d22e5eb431dd3c0fbc677bed58797fe7581e148f3f68818b/rpds_py-0.30.0-cp311-cp311-win32.whl", hash = "sha256:55f66022632205940f1827effeff17c4fa7ae1953d2b74a8581baaefb7d16f8c", size = 221406, upload-time = "2025-11-30T20:22:13.101Z" }, + { url = "https://files.pythonhosted.org/packages/fa/5b/e7b7aa136f28462b344e652ee010d4de26ee9fd16f1bfd5811f5153ccf89/rpds_py-0.30.0-cp311-cp311-win_amd64.whl", hash = "sha256:a51033ff701fca756439d641c0ad09a41d9242fa69121c7d8769604a0a629825", size = 236024, upload-time = "2025-11-30T20:22:14.853Z" }, + { url = "https://files.pythonhosted.org/packages/14/a6/364bba985e4c13658edb156640608f2c9e1d3ea3c81b27aa9d889fff0e31/rpds_py-0.30.0-cp311-cp311-win_arm64.whl", hash = "sha256:47b0ef6231c58f506ef0b74d44e330405caa8428e770fec25329ed2cb971a229", size = 229069, upload-time = "2025-11-30T20:22:16.577Z" }, + { url = "https://files.pythonhosted.org/packages/03/e7/98a2f4ac921d82f33e03f3835f5bf3a4a40aa1bfdc57975e74a97b2b4bdd/rpds_py-0.30.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a161f20d9a43006833cd7068375a94d035714d73a172b681d8881820600abfad", size = 375086, upload-time = "2025-11-30T20:22:17.93Z" }, + { url = "https://files.pythonhosted.org/packages/4d/a1/bca7fd3d452b272e13335db8d6b0b3ecde0f90ad6f16f3328c6fb150c889/rpds_py-0.30.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6abc8880d9d036ecaafe709079969f56e876fcf107f7a8e9920ba6d5a3878d05", size = 359053, upload-time = "2025-11-30T20:22:19.297Z" }, + { url = "https://files.pythonhosted.org/packages/65/1c/ae157e83a6357eceff62ba7e52113e3ec4834a84cfe07fa4b0757a7d105f/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca28829ae5f5d569bb62a79512c842a03a12576375d5ece7d2cadf8abe96ec28", size = 390763, upload-time = "2025-11-30T20:22:21.661Z" }, + { url = "https://files.pythonhosted.org/packages/d4/36/eb2eb8515e2ad24c0bd43c3ee9cd74c33f7ca6430755ccdb240fd3144c44/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a1010ed9524c73b94d15919ca4d41d8780980e1765babf85f9a2f90d247153dd", size = 408951, upload-time = "2025-11-30T20:22:23.408Z" }, + { url = "https://files.pythonhosted.org/packages/d6/65/ad8dc1784a331fabbd740ef6f71ce2198c7ed0890dab595adb9ea2d775a1/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8d1736cfb49381ba528cd5baa46f82fdc65c06e843dab24dd70b63d09121b3f", size = 514622, upload-time = "2025-11-30T20:22:25.16Z" }, + { url = "https://files.pythonhosted.org/packages/63/8e/0cfa7ae158e15e143fe03993b5bcd743a59f541f5952e1546b1ac1b5fd45/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d948b135c4693daff7bc2dcfc4ec57237a29bd37e60c2fabf5aff2bbacf3e2f1", size = 414492, upload-time = "2025-11-30T20:22:26.505Z" }, + { url = "https://files.pythonhosted.org/packages/60/1b/6f8f29f3f995c7ffdde46a626ddccd7c63aefc0efae881dc13b6e5d5bb16/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47f236970bccb2233267d89173d3ad2703cd36a0e2a6e92d0560d333871a3d23", size = 394080, upload-time = "2025-11-30T20:22:27.934Z" }, + { url = "https://files.pythonhosted.org/packages/6d/d5/a266341051a7a3ca2f4b750a3aa4abc986378431fc2da508c5034d081b70/rpds_py-0.30.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:2e6ecb5a5bcacf59c3f912155044479af1d0b6681280048b338b28e364aca1f6", size = 408680, upload-time = "2025-11-30T20:22:29.341Z" }, + { url = "https://files.pythonhosted.org/packages/10/3b/71b725851df9ab7a7a4e33cf36d241933da66040d195a84781f49c50490c/rpds_py-0.30.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a8fa71a2e078c527c3e9dc9fc5a98c9db40bcc8a92b4e8858e36d329f8684b51", size = 423589, upload-time = "2025-11-30T20:22:31.469Z" }, + { url = "https://files.pythonhosted.org/packages/00/2b/e59e58c544dc9bd8bd8384ecdb8ea91f6727f0e37a7131baeff8d6f51661/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:73c67f2db7bc334e518d097c6d1e6fed021bbc9b7d678d6cc433478365d1d5f5", size = 573289, upload-time = "2025-11-30T20:22:32.997Z" }, + { url = "https://files.pythonhosted.org/packages/da/3e/a18e6f5b460893172a7d6a680e86d3b6bc87a54c1f0b03446a3c8c7b588f/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5ba103fb455be00f3b1c2076c9d4264bfcb037c976167a6047ed82f23153f02e", size = 599737, upload-time = "2025-11-30T20:22:34.419Z" }, + { url = "https://files.pythonhosted.org/packages/5c/e2/714694e4b87b85a18e2c243614974413c60aa107fd815b8cbc42b873d1d7/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7cee9c752c0364588353e627da8a7e808a66873672bcb5f52890c33fd965b394", size = 563120, upload-time = "2025-11-30T20:22:35.903Z" }, + { url = "https://files.pythonhosted.org/packages/6f/ab/d5d5e3bcedb0a77f4f613706b750e50a5a3ba1c15ccd3665ecc636c968fd/rpds_py-0.30.0-cp312-cp312-win32.whl", hash = "sha256:1ab5b83dbcf55acc8b08fc62b796ef672c457b17dbd7820a11d6c52c06839bdf", size = 223782, upload-time = "2025-11-30T20:22:37.271Z" }, + { url = "https://files.pythonhosted.org/packages/39/3b/f786af9957306fdc38a74cef405b7b93180f481fb48453a114bb6465744a/rpds_py-0.30.0-cp312-cp312-win_amd64.whl", hash = "sha256:a090322ca841abd453d43456ac34db46e8b05fd9b3b4ac0c78bcde8b089f959b", size = 240463, upload-time = "2025-11-30T20:22:39.021Z" }, + { url = "https://files.pythonhosted.org/packages/f3/d2/b91dc748126c1559042cfe41990deb92c4ee3e2b415f6b5234969ffaf0cc/rpds_py-0.30.0-cp312-cp312-win_arm64.whl", hash = "sha256:669b1805bd639dd2989b281be2cfd951c6121b65e729d9b843e9639ef1fd555e", size = 230868, upload-time = "2025-11-30T20:22:40.493Z" }, + { url = "https://files.pythonhosted.org/packages/ed/dc/d61221eb88ff410de3c49143407f6f3147acf2538c86f2ab7ce65ae7d5f9/rpds_py-0.30.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:f83424d738204d9770830d35290ff3273fbb02b41f919870479fab14b9d303b2", size = 374887, upload-time = "2025-11-30T20:22:41.812Z" }, + { url = "https://files.pythonhosted.org/packages/fd/32/55fb50ae104061dbc564ef15cc43c013dc4a9f4527a1f4d99baddf56fe5f/rpds_py-0.30.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e7536cd91353c5273434b4e003cbda89034d67e7710eab8761fd918ec6c69cf8", size = 358904, upload-time = "2025-11-30T20:22:43.479Z" }, + { url = "https://files.pythonhosted.org/packages/58/70/faed8186300e3b9bdd138d0273109784eea2396c68458ed580f885dfe7ad/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2771c6c15973347f50fece41fc447c054b7ac2ae0502388ce3b6738cd366e3d4", size = 389945, upload-time = "2025-11-30T20:22:44.819Z" }, + { url = "https://files.pythonhosted.org/packages/bd/a8/073cac3ed2c6387df38f71296d002ab43496a96b92c823e76f46b8af0543/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0a59119fc6e3f460315fe9d08149f8102aa322299deaa5cab5b40092345c2136", size = 407783, upload-time = "2025-11-30T20:22:46.103Z" }, + { url = "https://files.pythonhosted.org/packages/77/57/5999eb8c58671f1c11eba084115e77a8899d6e694d2a18f69f0ba471ec8b/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:76fec018282b4ead0364022e3c54b60bf368b9d926877957a8624b58419169b7", size = 515021, upload-time = "2025-11-30T20:22:47.458Z" }, + { url = "https://files.pythonhosted.org/packages/e0/af/5ab4833eadc36c0a8ed2bc5c0de0493c04f6c06de223170bd0798ff98ced/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:692bef75a5525db97318e8cd061542b5a79812d711ea03dbc1f6f8dbb0c5f0d2", size = 414589, upload-time = "2025-11-30T20:22:48.872Z" }, + { url = "https://files.pythonhosted.org/packages/b7/de/f7192e12b21b9e9a68a6d0f249b4af3fdcdff8418be0767a627564afa1f1/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9027da1ce107104c50c81383cae773ef5c24d296dd11c99e2629dbd7967a20c6", size = 394025, upload-time = "2025-11-30T20:22:50.196Z" }, + { url = "https://files.pythonhosted.org/packages/91/c4/fc70cd0249496493500e7cc2de87504f5aa6509de1e88623431fec76d4b6/rpds_py-0.30.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:9cf69cdda1f5968a30a359aba2f7f9aa648a9ce4b580d6826437f2b291cfc86e", size = 408895, upload-time = "2025-11-30T20:22:51.87Z" }, + { url = "https://files.pythonhosted.org/packages/58/95/d9275b05ab96556fefff73a385813eb66032e4c99f411d0795372d9abcea/rpds_py-0.30.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a4796a717bf12b9da9d3ad002519a86063dcac8988b030e405704ef7d74d2d9d", size = 422799, upload-time = "2025-11-30T20:22:53.341Z" }, + { url = "https://files.pythonhosted.org/packages/06/c1/3088fc04b6624eb12a57eb814f0d4997a44b0d208d6cace713033ff1a6ba/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5d4c2aa7c50ad4728a094ebd5eb46c452e9cb7edbfdb18f9e1221f597a73e1e7", size = 572731, upload-time = "2025-11-30T20:22:54.778Z" }, + { url = "https://files.pythonhosted.org/packages/d8/42/c612a833183b39774e8ac8fecae81263a68b9583ee343db33ab571a7ce55/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ba81a9203d07805435eb06f536d95a266c21e5b2dfbf6517748ca40c98d19e31", size = 599027, upload-time = "2025-11-30T20:22:56.212Z" }, + { url = "https://files.pythonhosted.org/packages/5f/60/525a50f45b01d70005403ae0e25f43c0384369ad24ffe46e8d9068b50086/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:945dccface01af02675628334f7cf49c2af4c1c904748efc5cf7bbdf0b579f95", size = 563020, upload-time = "2025-11-30T20:22:58.2Z" }, + { url = "https://files.pythonhosted.org/packages/0b/5d/47c4655e9bcd5ca907148535c10e7d489044243cc9941c16ed7cd53be91d/rpds_py-0.30.0-cp313-cp313-win32.whl", hash = "sha256:b40fb160a2db369a194cb27943582b38f79fc4887291417685f3ad693c5a1d5d", size = 223139, upload-time = "2025-11-30T20:23:00.209Z" }, + { url = "https://files.pythonhosted.org/packages/f2/e1/485132437d20aa4d3e1d8b3fb5a5e65aa8139f1e097080c2a8443201742c/rpds_py-0.30.0-cp313-cp313-win_amd64.whl", hash = "sha256:806f36b1b605e2d6a72716f321f20036b9489d29c51c91f4dd29a3e3afb73b15", size = 240224, upload-time = "2025-11-30T20:23:02.008Z" }, + { url = "https://files.pythonhosted.org/packages/24/95/ffd128ed1146a153d928617b0ef673960130be0009c77d8fbf0abe306713/rpds_py-0.30.0-cp313-cp313-win_arm64.whl", hash = "sha256:d96c2086587c7c30d44f31f42eae4eac89b60dabbac18c7669be3700f13c3ce1", size = 230645, upload-time = "2025-11-30T20:23:03.43Z" }, + { url = "https://files.pythonhosted.org/packages/ff/1b/b10de890a0def2a319a2626334a7f0ae388215eb60914dbac8a3bae54435/rpds_py-0.30.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:eb0b93f2e5c2189ee831ee43f156ed34e2a89a78a66b98cadad955972548be5a", size = 364443, upload-time = "2025-11-30T20:23:04.878Z" }, + { url = "https://files.pythonhosted.org/packages/0d/bf/27e39f5971dc4f305a4fb9c672ca06f290f7c4e261c568f3dea16a410d47/rpds_py-0.30.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:922e10f31f303c7c920da8981051ff6d8c1a56207dbdf330d9047f6d30b70e5e", size = 353375, upload-time = "2025-11-30T20:23:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/40/58/442ada3bba6e8e6615fc00483135c14a7538d2ffac30e2d933ccf6852232/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdc62c8286ba9bf7f47befdcea13ea0e26bf294bda99758fd90535cbaf408000", size = 383850, upload-time = "2025-11-30T20:23:07.825Z" }, + { url = "https://files.pythonhosted.org/packages/14/14/f59b0127409a33c6ef6f5c1ebd5ad8e32d7861c9c7adfa9a624fc3889f6c/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:47f9a91efc418b54fb8190a6b4aa7813a23fb79c51f4bb84e418f5476c38b8db", size = 392812, upload-time = "2025-11-30T20:23:09.228Z" }, + { url = "https://files.pythonhosted.org/packages/b3/66/e0be3e162ac299b3a22527e8913767d869e6cc75c46bd844aa43fb81ab62/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1f3587eb9b17f3789ad50824084fa6f81921bbf9a795826570bda82cb3ed91f2", size = 517841, upload-time = "2025-11-30T20:23:11.186Z" }, + { url = "https://files.pythonhosted.org/packages/3d/55/fa3b9cf31d0c963ecf1ba777f7cf4b2a2c976795ac430d24a1f43d25a6ba/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:39c02563fc592411c2c61d26b6c5fe1e51eaa44a75aa2c8735ca88b0d9599daa", size = 408149, upload-time = "2025-11-30T20:23:12.864Z" }, + { url = "https://files.pythonhosted.org/packages/60/ca/780cf3b1a32b18c0f05c441958d3758f02544f1d613abf9488cd78876378/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51a1234d8febafdfd33a42d97da7a43f5dcb120c1060e352a3fbc0c6d36e2083", size = 383843, upload-time = "2025-11-30T20:23:14.638Z" }, + { url = "https://files.pythonhosted.org/packages/82/86/d5f2e04f2aa6247c613da0c1dd87fcd08fa17107e858193566048a1e2f0a/rpds_py-0.30.0-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:eb2c4071ab598733724c08221091e8d80e89064cd472819285a9ab0f24bcedb9", size = 396507, upload-time = "2025-11-30T20:23:16.105Z" }, + { url = "https://files.pythonhosted.org/packages/4b/9a/453255d2f769fe44e07ea9785c8347edaf867f7026872e76c1ad9f7bed92/rpds_py-0.30.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6bdfdb946967d816e6adf9a3d8201bfad269c67efe6cefd7093ef959683c8de0", size = 414949, upload-time = "2025-11-30T20:23:17.539Z" }, + { url = "https://files.pythonhosted.org/packages/a3/31/622a86cdc0c45d6df0e9ccb6becdba5074735e7033c20e401a6d9d0e2ca0/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c77afbd5f5250bf27bf516c7c4a016813eb2d3e116139aed0096940c5982da94", size = 565790, upload-time = "2025-11-30T20:23:19.029Z" }, + { url = "https://files.pythonhosted.org/packages/1c/5d/15bbf0fb4a3f58a3b1c67855ec1efcc4ceaef4e86644665fff03e1b66d8d/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:61046904275472a76c8c90c9ccee9013d70a6d0f73eecefd38c1ae7c39045a08", size = 590217, upload-time = "2025-11-30T20:23:20.885Z" }, + { url = "https://files.pythonhosted.org/packages/6d/61/21b8c41f68e60c8cc3b2e25644f0e3681926020f11d06ab0b78e3c6bbff1/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4c5f36a861bc4b7da6516dbdf302c55313afa09b81931e8280361a4f6c9a2d27", size = 555806, upload-time = "2025-11-30T20:23:22.488Z" }, + { url = "https://files.pythonhosted.org/packages/f9/39/7e067bb06c31de48de3eb200f9fc7c58982a4d3db44b07e73963e10d3be9/rpds_py-0.30.0-cp313-cp313t-win32.whl", hash = "sha256:3d4a69de7a3e50ffc214ae16d79d8fbb0922972da0356dcf4d0fdca2878559c6", size = 211341, upload-time = "2025-11-30T20:23:24.449Z" }, + { url = "https://files.pythonhosted.org/packages/0a/4d/222ef0b46443cf4cf46764d9c630f3fe4abaa7245be9417e56e9f52b8f65/rpds_py-0.30.0-cp313-cp313t-win_amd64.whl", hash = "sha256:f14fc5df50a716f7ece6a80b6c78bb35ea2ca47c499e422aa4463455dd96d56d", size = 225768, upload-time = "2025-11-30T20:23:25.908Z" }, + { url = "https://files.pythonhosted.org/packages/69/71/3f34339ee70521864411f8b6992e7ab13ac30d8e4e3309e07c7361767d91/rpds_py-0.30.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c2262bdba0ad4fc6fb5545660673925c2d2a5d9e2e0fb603aad545427be0fc58", size = 372292, upload-time = "2025-11-30T20:24:16.537Z" }, + { url = "https://files.pythonhosted.org/packages/57/09/f183df9b8f2d66720d2ef71075c59f7e1b336bec7ee4c48f0a2b06857653/rpds_py-0.30.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:ee6af14263f25eedc3bb918a3c04245106a42dfd4f5c2285ea6f997b1fc3f89a", size = 362128, upload-time = "2025-11-30T20:24:18.086Z" }, + { url = "https://files.pythonhosted.org/packages/7a/68/5c2594e937253457342e078f0cc1ded3dd7b2ad59afdbf2d354869110a02/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3adbb8179ce342d235c31ab8ec511e66c73faa27a47e076ccc92421add53e2bb", size = 391542, upload-time = "2025-11-30T20:24:20.092Z" }, + { url = "https://files.pythonhosted.org/packages/49/5c/31ef1afd70b4b4fbdb2800249f34c57c64beb687495b10aec0365f53dfc4/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:250fa00e9543ac9b97ac258bd37367ff5256666122c2d0f2bc97577c60a1818c", size = 404004, upload-time = "2025-11-30T20:24:22.231Z" }, + { url = "https://files.pythonhosted.org/packages/e3/63/0cfbea38d05756f3440ce6534d51a491d26176ac045e2707adc99bb6e60a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9854cf4f488b3d57b9aaeb105f06d78e5529d3145b1e4a41750167e8c213c6d3", size = 527063, upload-time = "2025-11-30T20:24:24.302Z" }, + { url = "https://files.pythonhosted.org/packages/42/e6/01e1f72a2456678b0f618fc9a1a13f882061690893c192fcad9f2926553a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:993914b8e560023bc0a8bf742c5f303551992dcb85e247b1e5c7f4a7d145bda5", size = 413099, upload-time = "2025-11-30T20:24:25.916Z" }, + { url = "https://files.pythonhosted.org/packages/b8/25/8df56677f209003dcbb180765520c544525e3ef21ea72279c98b9aa7c7fb/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58edca431fb9b29950807e301826586e5bbf24163677732429770a697ffe6738", size = 392177, upload-time = "2025-11-30T20:24:27.834Z" }, + { url = "https://files.pythonhosted.org/packages/4a/b4/0a771378c5f16f8115f796d1f437950158679bcd2a7c68cf251cfb00ed5b/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:dea5b552272a944763b34394d04577cf0f9bd013207bc32323b5a89a53cf9c2f", size = 406015, upload-time = "2025-11-30T20:24:29.457Z" }, + { url = "https://files.pythonhosted.org/packages/36/d8/456dbba0af75049dc6f63ff295a2f92766b9d521fa00de67a2bd6427d57a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ba3af48635eb83d03f6c9735dfb21785303e73d22ad03d489e88adae6eab8877", size = 423736, upload-time = "2025-11-30T20:24:31.22Z" }, + { url = "https://files.pythonhosted.org/packages/13/64/b4d76f227d5c45a7e0b796c674fd81b0a6c4fbd48dc29271857d8219571c/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:dff13836529b921e22f15cb099751209a60009731a68519630a24d61f0b1b30a", size = 573981, upload-time = "2025-11-30T20:24:32.934Z" }, + { url = "https://files.pythonhosted.org/packages/20/91/092bacadeda3edf92bf743cc96a7be133e13a39cdbfd7b5082e7ab638406/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:1b151685b23929ab7beec71080a8889d4d6d9fa9a983d213f07121205d48e2c4", size = 599782, upload-time = "2025-11-30T20:24:35.169Z" }, + { url = "https://files.pythonhosted.org/packages/d1/b7/b95708304cd49b7b6f82fdd039f1748b66ec2b21d6a45180910802f1abf1/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:ac37f9f516c51e5753f27dfdef11a88330f04de2d564be3991384b2f3535d02e", size = 562191, upload-time = "2025-11-30T20:24:36.853Z" }, +] + +[[package]] +name = "ruamel-yaml" +version = "0.19.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c7/3b/ebda527b56beb90cb7652cb1c7e4f91f48649fbcd8d2eb2fb6e77cd3329b/ruamel_yaml-0.19.1.tar.gz", hash = "sha256:53eb66cd27849eff968ebf8f0bf61f46cdac2da1d1f3576dd4ccee9b25c31993", size = 142709, upload-time = "2026-01-02T16:50:31.84Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b8/0c/51f6841f1d84f404f92463fc2b1ba0da357ca1e3db6b7fbda26956c3b82a/ruamel_yaml-0.19.1-py3-none-any.whl", hash = "sha256:27592957fedf6e0b62f281e96effd28043345e0e66001f97683aa9a40c667c93", size = 118102, upload-time = "2026-01-02T16:50:29.201Z" }, +] + +[[package]] +name = "ruff" +version = "0.15.14" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/dc/8a/8bce2894573e9dae6ff4d77fe34ad727d79b9e6238ad288c5638990d90f6/ruff-0.15.14.tar.gz", hash = "sha256:48e866b165be4a9bdbf310f7d3c9a07edef2fe8cd63ffeb4e00bb590506ebf9f", size = 4700910, upload-time = "2026-05-21T14:34:55.177Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b9/c8/74a92c6ff9fcfb4f1f947126d3ebee8389276e161ecc85de5bda7cda51bd/ruff-0.15.14-py3-none-linux_armv6l.whl", hash = "sha256:8dd2db9416e487c8d4b01fa7056bb02c4d05969d4f8d17a08c229c2f4ff3c108", size = 10739177, upload-time = "2026-05-21T14:34:37.332Z" }, + { url = "https://files.pythonhosted.org/packages/45/91/254a35c20acc38a7223c9d2d594af12e794432464f2cdeb52af1dc4a892d/ruff-0.15.14-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:be4ff55af755bd71a00ab3dc6bd7ffc467bd76e0df6881e286c2e3d23e8fb43b", size = 11144969, upload-time = "2026-05-21T14:34:43.978Z" }, + { url = "https://files.pythonhosted.org/packages/56/9e/d13e40f83b8d0a94430e6778ce1d94a43b38cf2efe63278bdd2b4c65abbf/ruff-0.15.14-py3-none-macosx_11_0_arm64.whl", hash = "sha256:48d5909d7d06276ce7dde6d32bfa4b0d4cb2651145cd8ee4b440722cbc77832f", size = 10478207, upload-time = "2026-05-21T14:34:48.378Z" }, + { url = "https://files.pythonhosted.org/packages/8d/f1/b15a7839fa4f332f8acec78e20564f26bb2d866e3d21710b877fd0263000/ruff-0.15.14-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca8cbfa94c4f90984a67561978602746d4cd27103568f745fa90eee3f0d4107d", size = 10818459, upload-time = "2026-05-21T14:34:22.318Z" }, + { url = "https://files.pythonhosted.org/packages/45/33/53d651177f84f94b400a0e27f8824eeada3dddc9d5ee8aeb048f4352a520/ruff-0.15.14-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9a6bbc0333f1ab053423bcbf6226477d266ca7cec7738c4c8e3f55647803f3c4", size = 10541800, upload-time = "2026-05-21T14:34:20.209Z" }, + { url = "https://files.pythonhosted.org/packages/b8/a6/868f87e0bf9786ed24b5d0d0ad8676b8a94fd1912f42cddf9cfc7857818a/ruff-0.15.14-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8a24a4f7605d7003a6674d4387651effd939dead3fddd0f36561eb77a9a2e542", size = 11342149, upload-time = "2026-05-21T14:34:46.365Z" }, + { url = "https://files.pythonhosted.org/packages/a7/8b/38cd5c19faffdcc05a408d2b78edccc69492ab9720eadb49ea15ef80d768/ruff-0.15.14-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:049b5326e53ed80978f2fc041a280603f69dd6b0c95464342a2bb4572d9d9e2f", size = 12212563, upload-time = "2026-05-21T14:34:28.579Z" }, + { url = "https://files.pythonhosted.org/packages/3e/4d/a3c5b874a556d5731e3e657aaf04311bb76f0a5c3ec220ed43051be6b64b/ruff-0.15.14-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d4ed42e6696c8dfa5f06728e6441993901f548eb92d73bc472cb5a38d1395fbf", size = 11493299, upload-time = "2026-05-21T14:34:41.836Z" }, + { url = "https://files.pythonhosted.org/packages/1e/c0/56472c251d09858a53e51efbd485b09e1995d8731668b76d52e5dd6ee0f1/ruff-0.15.14-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:715c543cf450c4888251f91c52f1942a800541d9bddd7ac060aa4e6b77ae7cba", size = 11455931, upload-time = "2026-05-21T14:34:57.276Z" }, + { url = "https://files.pythonhosted.org/packages/2c/4a/e2e7b4d8dbf233d4eace59c75bc3435fa6d8bd3bae82d351d4e4300c0fd1/ruff-0.15.14-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:72ebab6013ec887d439d8b7593737a0a4ffb06d45d209d4e4bf2e92813082d3f", size = 11400794, upload-time = "2026-05-21T14:34:39.773Z" }, + { url = "https://files.pythonhosted.org/packages/97/c7/83c0539fe34c3e09136204d1e75d6052492364e0b3cb05e9465423f567d7/ruff-0.15.14-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:49072d36abdbe97a8dd7f480afe9c675699c0c495d4c84076e2c1203c4550581", size = 10804759, upload-time = "2026-05-21T14:34:31.045Z" }, + { url = "https://files.pythonhosted.org/packages/86/a6/18f2bfc095a2ab4a78745644e428205532ce6653a5d0fa8501572891534d/ruff-0.15.14-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:958522aee105068640c2c2ceae08f413ae44d922f52a1374ac13d6a96032fc93", size = 10539517, upload-time = "2026-05-21T14:34:53.064Z" }, + { url = "https://files.pythonhosted.org/packages/54/3a/5a8b3b69c654d4e4bf1d246ac5b49cbcdac6eaab6905925f8915f31e3b80/ruff-0.15.14-py3-none-musllinux_1_2_i686.whl", hash = "sha256:f3707da619a143a2e8830e2abab8224478d69ace2d28cb6c20543ae97c36bf61", size = 11065169, upload-time = "2026-05-21T14:34:24.484Z" }, + { url = "https://files.pythonhosted.org/packages/ed/c5/8864e4e7925b836ea354b31d57641ec03830564e281a8b6f061f8c3e0ec1/ruff-0.15.14-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:bb01d645694e3ec0102105d07ef2d53703970407d59c04e59d3ba0b7a1d53553", size = 11560214, upload-time = "2026-05-21T14:34:50.975Z" }, + { url = "https://files.pythonhosted.org/packages/36/38/012bf76752e1f89ed50b77b99532d90f3a3e287bc7918e1fc0948ac866ac/ruff-0.15.14-py3-none-win32.whl", hash = "sha256:6d0c1ad2a0ab718d39b6d8fd2217981ce4d625cd96a720095f798fb47d8b13e6", size = 10805548, upload-time = "2026-05-21T14:34:33.453Z" }, + { url = "https://files.pythonhosted.org/packages/d1/b7/4ea2c170f10ad760fff2a5250beb18897719dc8b52b53a24cddbb9dd3f19/ruff-0.15.14-py3-none-win_amd64.whl", hash = "sha256:802342981e056db3851a7836e5b070f8f15f67d4a685ae2a6160939d364b2902", size = 11939523, upload-time = "2026-05-21T14:34:18.077Z" }, + { url = "https://files.pythonhosted.org/packages/62/d5/bc97ff895ec35cf3925d4bd60f3b39d822f377a446906ec9bcc87405e59b/ruff-0.15.14-py3-none-win_arm64.whl", hash = "sha256:ff47b90a9ef6a40c9e2f3b479c1fb78531adf055b94c1eba0a7ba04b31951826", size = 11208607, upload-time = "2026-05-21T14:34:26.525Z" }, +] + +[[package]] +name = "scikit-learn" +version = "1.8.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "joblib" }, + { name = "numpy" }, + { name = "scipy" }, + { name = "threadpoolctl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0e/d4/40988bf3b8e34feec1d0e6a051446b1f66225f8529b9309becaeef62b6c4/scikit_learn-1.8.0.tar.gz", hash = "sha256:9bccbb3b40e3de10351f8f5068e105d0f4083b1a65fa07b6634fbc401a6287fd", size = 7335585, upload-time = "2025-12-10T07:08:53.618Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c9/92/53ea2181da8ac6bf27170191028aee7251f8f841f8d3edbfdcaf2008fde9/scikit_learn-1.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:146b4d36f800c013d267b29168813f7a03a43ecd2895d04861f1240b564421da", size = 8595835, upload-time = "2025-12-10T07:07:39.385Z" }, + { url = "https://files.pythonhosted.org/packages/01/18/d154dc1638803adf987910cdd07097d9c526663a55666a97c124d09fb96a/scikit_learn-1.8.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:f984ca4b14914e6b4094c5d52a32ea16b49832c03bd17a110f004db3c223e8e1", size = 8080381, upload-time = "2025-12-10T07:07:41.93Z" }, + { url = "https://files.pythonhosted.org/packages/8a/44/226142fcb7b7101e64fdee5f49dbe6288d4c7af8abf593237b70fca080a4/scikit_learn-1.8.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5e30adb87f0cc81c7690a84f7932dd66be5bac57cfe16b91cb9151683a4a2d3b", size = 8799632, upload-time = "2025-12-10T07:07:43.899Z" }, + { url = "https://files.pythonhosted.org/packages/36/4d/4a67f30778a45d542bbea5db2dbfa1e9e100bf9ba64aefe34215ba9f11f6/scikit_learn-1.8.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ada8121bcb4dac28d930febc791a69f7cb1673c8495e5eee274190b73a4559c1", size = 9103788, upload-time = "2025-12-10T07:07:45.982Z" }, + { url = "https://files.pythonhosted.org/packages/89/3c/45c352094cfa60050bcbb967b1faf246b22e93cb459f2f907b600f2ceda5/scikit_learn-1.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:c57b1b610bd1f40ba43970e11ce62821c2e6569e4d74023db19c6b26f246cb3b", size = 8081706, upload-time = "2025-12-10T07:07:48.111Z" }, + { url = "https://files.pythonhosted.org/packages/3d/46/5416595bb395757f754feb20c3d776553a386b661658fb21b7c814e89efe/scikit_learn-1.8.0-cp311-cp311-win_arm64.whl", hash = "sha256:2838551e011a64e3053ad7618dda9310175f7515f1742fa2d756f7c874c05961", size = 7688451, upload-time = "2025-12-10T07:07:49.873Z" }, + { url = "https://files.pythonhosted.org/packages/90/74/e6a7cc4b820e95cc38cf36cd74d5aa2b42e8ffc2d21fe5a9a9c45c1c7630/scikit_learn-1.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5fb63362b5a7ddab88e52b6dbb47dac3fd7dafeee740dc6c8d8a446ddedade8e", size = 8548242, upload-time = "2025-12-10T07:07:51.568Z" }, + { url = "https://files.pythonhosted.org/packages/49/d8/9be608c6024d021041c7f0b3928d4749a706f4e2c3832bbede4fb4f58c95/scikit_learn-1.8.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:5025ce924beccb28298246e589c691fe1b8c1c96507e6d27d12c5fadd85bfd76", size = 8079075, upload-time = "2025-12-10T07:07:53.697Z" }, + { url = "https://files.pythonhosted.org/packages/dd/47/f187b4636ff80cc63f21cd40b7b2d177134acaa10f6bb73746130ee8c2e5/scikit_learn-1.8.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4496bb2cf7a43ce1a2d7524a79e40bc5da45cf598dbf9545b7e8316ccba47bb4", size = 8660492, upload-time = "2025-12-10T07:07:55.574Z" }, + { url = "https://files.pythonhosted.org/packages/97/74/b7a304feb2b49df9fafa9382d4d09061a96ee9a9449a7cbea7988dda0828/scikit_learn-1.8.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a0bcfe4d0d14aec44921545fd2af2338c7471de9cb701f1da4c9d85906ab847a", size = 8931904, upload-time = "2025-12-10T07:07:57.666Z" }, + { url = "https://files.pythonhosted.org/packages/9f/c4/0ab22726a04ede56f689476b760f98f8f46607caecff993017ac1b64aa5d/scikit_learn-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:35c007dedb2ffe38fe3ee7d201ebac4a2deccd2408e8621d53067733e3c74809", size = 8019359, upload-time = "2025-12-10T07:07:59.838Z" }, + { url = "https://files.pythonhosted.org/packages/24/90/344a67811cfd561d7335c1b96ca21455e7e472d281c3c279c4d3f2300236/scikit_learn-1.8.0-cp312-cp312-win_arm64.whl", hash = "sha256:8c497fff237d7b4e07e9ef1a640887fa4fb765647f86fbe00f969ff6280ce2bb", size = 7641898, upload-time = "2025-12-10T07:08:01.36Z" }, + { url = "https://files.pythonhosted.org/packages/03/aa/e22e0768512ce9255eba34775be2e85c2048da73da1193e841707f8f039c/scikit_learn-1.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0d6ae97234d5d7079dc0040990a6f7aeb97cb7fa7e8945f1999a429b23569e0a", size = 8513770, upload-time = "2025-12-10T07:08:03.251Z" }, + { url = "https://files.pythonhosted.org/packages/58/37/31b83b2594105f61a381fc74ca19e8780ee923be2d496fcd8d2e1147bd99/scikit_learn-1.8.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:edec98c5e7c128328124a029bceb09eda2d526997780fef8d65e9a69eead963e", size = 8044458, upload-time = "2025-12-10T07:08:05.336Z" }, + { url = "https://files.pythonhosted.org/packages/2d/5a/3f1caed8765f33eabb723596666da4ebbf43d11e96550fb18bdec42b467b/scikit_learn-1.8.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:74b66d8689d52ed04c271e1329f0c61635bcaf5b926db9b12d58914cdc01fe57", size = 8610341, upload-time = "2025-12-10T07:08:07.732Z" }, + { url = "https://files.pythonhosted.org/packages/38/cf/06896db3f71c75902a8e9943b444a56e727418f6b4b4a90c98c934f51ed4/scikit_learn-1.8.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8fdf95767f989b0cfedb85f7ed8ca215d4be728031f56ff5a519ee1e3276dc2e", size = 8900022, upload-time = "2025-12-10T07:08:09.862Z" }, + { url = "https://files.pythonhosted.org/packages/1c/f9/9b7563caf3ec8873e17a31401858efab6b39a882daf6c1bfa88879c0aa11/scikit_learn-1.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:2de443b9373b3b615aec1bb57f9baa6bb3a9bd093f1269ba95c17d870422b271", size = 7989409, upload-time = "2025-12-10T07:08:12.028Z" }, + { url = "https://files.pythonhosted.org/packages/49/bd/1f4001503650e72c4f6009ac0c4413cb17d2d601cef6f71c0453da2732fc/scikit_learn-1.8.0-cp313-cp313-win_arm64.whl", hash = "sha256:eddde82a035681427cbedded4e6eff5e57fa59216c2e3e90b10b19ab1d0a65c3", size = 7619760, upload-time = "2025-12-10T07:08:13.688Z" }, + { url = "https://files.pythonhosted.org/packages/d2/7d/a630359fc9dcc95496588c8d8e3245cc8fd81980251079bc09c70d41d951/scikit_learn-1.8.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:7cc267b6108f0a1499a734167282c00c4ebf61328566b55ef262d48e9849c735", size = 8826045, upload-time = "2025-12-10T07:08:15.215Z" }, + { url = "https://files.pythonhosted.org/packages/cc/56/a0c86f6930cfcd1c7054a2bc417e26960bb88d32444fe7f71d5c2cfae891/scikit_learn-1.8.0-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:fe1c011a640a9f0791146011dfd3c7d9669785f9fed2b2a5f9e207536cf5c2fd", size = 8420324, upload-time = "2025-12-10T07:08:17.561Z" }, + { url = "https://files.pythonhosted.org/packages/46/1e/05962ea1cebc1cf3876667ecb14c283ef755bf409993c5946ade3b77e303/scikit_learn-1.8.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:72358cce49465d140cc4e7792015bb1f0296a9742d5622c67e31399b75468b9e", size = 8680651, upload-time = "2025-12-10T07:08:19.952Z" }, + { url = "https://files.pythonhosted.org/packages/fe/56/a85473cd75f200c9759e3a5f0bcab2d116c92a8a02ee08ccd73b870f8bb4/scikit_learn-1.8.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:80832434a6cc114f5219211eec13dcbc16c2bac0e31ef64c6d346cde3cf054cb", size = 8925045, upload-time = "2025-12-10T07:08:22.11Z" }, + { url = "https://files.pythonhosted.org/packages/cc/b7/64d8cfa896c64435ae57f4917a548d7ac7a44762ff9802f75a79b77cb633/scikit_learn-1.8.0-cp313-cp313t-win_amd64.whl", hash = "sha256:ee787491dbfe082d9c3013f01f5991658b0f38aa8177e4cd4bf434c58f551702", size = 8507994, upload-time = "2025-12-10T07:08:23.943Z" }, + { url = "https://files.pythonhosted.org/packages/5e/37/e192ea709551799379958b4c4771ec507347027bb7c942662c7fbeba31cb/scikit_learn-1.8.0-cp313-cp313t-win_arm64.whl", hash = "sha256:bf97c10a3f5a7543f9b88cbf488d33d175e9146115a451ae34568597ba33dcde", size = 7869518, upload-time = "2025-12-10T07:08:25.71Z" }, +] + +[[package]] +name = "scipy" +version = "1.17.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7a/97/5a3609c4f8d58b039179648e62dd220f89864f56f7357f5d4f45c29eb2cc/scipy-1.17.1.tar.gz", hash = "sha256:95d8e012d8cb8816c226aef832200b1d45109ed4464303e997c5b13122b297c0", size = 30573822, upload-time = "2026-02-23T00:26:24.851Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/75/b4ce781849931fef6fd529afa6b63711d5a733065722d0c3e2724af9e40a/scipy-1.17.1-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:1f95b894f13729334fb990162e911c9e5dc1ab390c58aa6cbecb389c5b5e28ec", size = 31613675, upload-time = "2026-02-23T00:16:00.13Z" }, + { url = "https://files.pythonhosted.org/packages/f7/58/bccc2861b305abdd1b8663d6130c0b3d7cc22e8d86663edbc8401bfd40d4/scipy-1.17.1-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:e18f12c6b0bc5a592ed23d3f7b891f68fd7f8241d69b7883769eb5d5dfb52696", size = 28162057, upload-time = "2026-02-23T00:16:09.456Z" }, + { url = "https://files.pythonhosted.org/packages/6d/ee/18146b7757ed4976276b9c9819108adbc73c5aad636e5353e20746b73069/scipy-1.17.1-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:a3472cfbca0a54177d0faa68f697d8ba4c80bbdc19908c3465556d9f7efce9ee", size = 20334032, upload-time = "2026-02-23T00:16:17.358Z" }, + { url = "https://files.pythonhosted.org/packages/ec/e6/cef1cf3557f0c54954198554a10016b6a03b2ec9e22a4e1df734936bd99c/scipy-1.17.1-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:766e0dc5a616d026a3a1cffa379af959671729083882f50307e18175797b3dfd", size = 22709533, upload-time = "2026-02-23T00:16:25.791Z" }, + { url = "https://files.pythonhosted.org/packages/4d/60/8804678875fc59362b0fb759ab3ecce1f09c10a735680318ac30da8cd76b/scipy-1.17.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:744b2bf3640d907b79f3fd7874efe432d1cf171ee721243e350f55234b4cec4c", size = 33062057, upload-time = "2026-02-23T00:16:36.931Z" }, + { url = "https://files.pythonhosted.org/packages/09/7d/af933f0f6e0767995b4e2d705a0665e454d1c19402aa7e895de3951ebb04/scipy-1.17.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:43af8d1f3bea642559019edfe64e9b11192a8978efbd1539d7bc2aaa23d92de4", size = 35349300, upload-time = "2026-02-23T00:16:49.108Z" }, + { url = "https://files.pythonhosted.org/packages/b4/3d/7ccbbdcbb54c8fdc20d3b6930137c782a163fa626f0aef920349873421ba/scipy-1.17.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:cd96a1898c0a47be4520327e01f874acfd61fb48a9420f8aa9f6483412ffa444", size = 35127333, upload-time = "2026-02-23T00:17:01.293Z" }, + { url = "https://files.pythonhosted.org/packages/e8/19/f926cb11c42b15ba08e3a71e376d816ac08614f769b4f47e06c3580c836a/scipy-1.17.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4eb6c25dd62ee8d5edf68a8e1c171dd71c292fdae95d8aeb3dd7d7de4c364082", size = 37741314, upload-time = "2026-02-23T00:17:12.576Z" }, + { url = "https://files.pythonhosted.org/packages/95/da/0d1df507cf574b3f224ccc3d45244c9a1d732c81dcb26b1e8a766ae271a8/scipy-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:d30e57c72013c2a4fe441c2fcb8e77b14e152ad48b5464858e07e2ad9fbfceff", size = 36607512, upload-time = "2026-02-23T00:17:23.424Z" }, + { url = "https://files.pythonhosted.org/packages/68/7f/bdd79ceaad24b671543ffe0ef61ed8e659440eb683b66f033454dcee90eb/scipy-1.17.1-cp311-cp311-win_arm64.whl", hash = "sha256:9ecb4efb1cd6e8c4afea0daa91a87fbddbce1b99d2895d151596716c0b2e859d", size = 24599248, upload-time = "2026-02-23T00:17:34.561Z" }, + { url = "https://files.pythonhosted.org/packages/35/48/b992b488d6f299dbe3f11a20b24d3dda3d46f1a635ede1c46b5b17a7b163/scipy-1.17.1-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:35c3a56d2ef83efc372eaec584314bd0ef2e2f0d2adb21c55e6ad5b344c0dcb8", size = 31610954, upload-time = "2026-02-23T00:17:49.855Z" }, + { url = "https://files.pythonhosted.org/packages/b2/02/cf107b01494c19dc100f1d0b7ac3cc08666e96ba2d64db7626066cee895e/scipy-1.17.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:fcb310ddb270a06114bb64bbe53c94926b943f5b7f0842194d585c65eb4edd76", size = 28172662, upload-time = "2026-02-23T00:18:01.64Z" }, + { url = "https://files.pythonhosted.org/packages/cf/a9/599c28631bad314d219cf9ffd40e985b24d603fc8a2f4ccc5ae8419a535b/scipy-1.17.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:cc90d2e9c7e5c7f1a482c9875007c095c3194b1cfedca3c2f3291cdc2bc7c086", size = 20344366, upload-time = "2026-02-23T00:18:12.015Z" }, + { url = "https://files.pythonhosted.org/packages/35/f5/906eda513271c8deb5af284e5ef0206d17a96239af79f9fa0aebfe0e36b4/scipy-1.17.1-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:c80be5ede8f3f8eded4eff73cc99a25c388ce98e555b17d31da05287015ffa5b", size = 22704017, upload-time = "2026-02-23T00:18:21.502Z" }, + { url = "https://files.pythonhosted.org/packages/da/34/16f10e3042d2f1d6b66e0428308ab52224b6a23049cb2f5c1756f713815f/scipy-1.17.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e19ebea31758fac5893a2ac360fedd00116cbb7628e650842a6691ba7ca28a21", size = 32927842, upload-time = "2026-02-23T00:18:35.367Z" }, + { url = "https://files.pythonhosted.org/packages/01/8e/1e35281b8ab6d5d72ebe9911edcdffa3f36b04ed9d51dec6dd140396e220/scipy-1.17.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:02ae3b274fde71c5e92ac4d54bc06c42d80e399fec704383dcd99b301df37458", size = 35235890, upload-time = "2026-02-23T00:18:49.188Z" }, + { url = "https://files.pythonhosted.org/packages/c5/5c/9d7f4c88bea6e0d5a4f1bc0506a53a00e9fcb198de372bfe4d3652cef482/scipy-1.17.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8a604bae87c6195d8b1045eddece0514d041604b14f2727bbc2b3020172045eb", size = 35003557, upload-time = "2026-02-23T00:18:54.74Z" }, + { url = "https://files.pythonhosted.org/packages/65/94/7698add8f276dbab7a9de9fb6b0e02fc13ee61d51c7c3f85ac28b65e1239/scipy-1.17.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f590cd684941912d10becc07325a3eeb77886fe981415660d9265c4c418d0bea", size = 37625856, upload-time = "2026-02-23T00:19:00.307Z" }, + { url = "https://files.pythonhosted.org/packages/a2/84/dc08d77fbf3d87d3ee27f6a0c6dcce1de5829a64f2eae85a0ecc1f0daa73/scipy-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:41b71f4a3a4cab9d366cd9065b288efc4d4f3c0b37a91a8e0947fb5bd7f31d87", size = 36549682, upload-time = "2026-02-23T00:19:07.67Z" }, + { url = "https://files.pythonhosted.org/packages/bc/98/fe9ae9ffb3b54b62559f52dedaebe204b408db8109a8c66fdd04869e6424/scipy-1.17.1-cp312-cp312-win_arm64.whl", hash = "sha256:f4115102802df98b2b0db3cce5cb9b92572633a1197c77b7553e5203f284a5b3", size = 24547340, upload-time = "2026-02-23T00:19:12.024Z" }, + { url = "https://files.pythonhosted.org/packages/76/27/07ee1b57b65e92645f219b37148a7e7928b82e2b5dbeccecb4dff7c64f0b/scipy-1.17.1-cp313-cp313-macosx_10_14_x86_64.whl", hash = "sha256:5e3c5c011904115f88a39308379c17f91546f77c1667cea98739fe0fccea804c", size = 31590199, upload-time = "2026-02-23T00:19:17.192Z" }, + { url = "https://files.pythonhosted.org/packages/ec/ae/db19f8ab842e9b724bf5dbb7db29302a91f1e55bc4d04b1025d6d605a2c5/scipy-1.17.1-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:6fac755ca3d2c3edcb22f479fceaa241704111414831ddd3bc6056e18516892f", size = 28154001, upload-time = "2026-02-23T00:19:22.241Z" }, + { url = "https://files.pythonhosted.org/packages/5b/58/3ce96251560107b381cbd6e8413c483bbb1228a6b919fa8652b0d4090e7f/scipy-1.17.1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:7ff200bf9d24f2e4d5dc6ee8c3ac64d739d3a89e2326ba68aaf6c4a2b838fd7d", size = 20325719, upload-time = "2026-02-23T00:19:26.329Z" }, + { url = "https://files.pythonhosted.org/packages/b2/83/15087d945e0e4d48ce2377498abf5ad171ae013232ae31d06f336e64c999/scipy-1.17.1-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:4b400bdc6f79fa02a4d86640310dde87a21fba0c979efff5248908c6f15fad1b", size = 22683595, upload-time = "2026-02-23T00:19:30.304Z" }, + { url = "https://files.pythonhosted.org/packages/b4/e0/e58fbde4a1a594c8be8114eb4aac1a55bcd6587047efc18a61eb1f5c0d30/scipy-1.17.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2b64ca7d4aee0102a97f3ba22124052b4bd2152522355073580bf4845e2550b6", size = 32896429, upload-time = "2026-02-23T00:19:35.536Z" }, + { url = "https://files.pythonhosted.org/packages/f5/5f/f17563f28ff03c7b6799c50d01d5d856a1d55f2676f537ca8d28c7f627cd/scipy-1.17.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:581b2264fc0aa555f3f435a5944da7504ea3a065d7029ad60e7c3d1ae09c5464", size = 35203952, upload-time = "2026-02-23T00:19:42.259Z" }, + { url = "https://files.pythonhosted.org/packages/8d/a5/9afd17de24f657fdfe4df9a3f1ea049b39aef7c06000c13db1530d81ccca/scipy-1.17.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:beeda3d4ae615106d7094f7e7cef6218392e4465cc95d25f900bebabfded0950", size = 34979063, upload-time = "2026-02-23T00:19:47.547Z" }, + { url = "https://files.pythonhosted.org/packages/8b/13/88b1d2384b424bf7c924f2038c1c409f8d88bb2a8d49d097861dd64a57b2/scipy-1.17.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6609bc224e9568f65064cfa72edc0f24ee6655b47575954ec6339534b2798369", size = 37598449, upload-time = "2026-02-23T00:19:53.238Z" }, + { url = "https://files.pythonhosted.org/packages/35/e5/d6d0e51fc888f692a35134336866341c08655d92614f492c6860dc45bb2c/scipy-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:37425bc9175607b0268f493d79a292c39f9d001a357bebb6b88fdfaff13f6448", size = 36510943, upload-time = "2026-02-23T00:20:50.89Z" }, + { url = "https://files.pythonhosted.org/packages/2a/fd/3be73c564e2a01e690e19cc618811540ba5354c67c8680dce3281123fb79/scipy-1.17.1-cp313-cp313-win_arm64.whl", hash = "sha256:5cf36e801231b6a2059bf354720274b7558746f3b1a4efb43fcf557ccd484a87", size = 24545621, upload-time = "2026-02-23T00:20:55.871Z" }, + { url = "https://files.pythonhosted.org/packages/6f/6b/17787db8b8114933a66f9dcc479a8272e4b4da75fe03b0c282f7b0ade8cd/scipy-1.17.1-cp313-cp313t-macosx_10_14_x86_64.whl", hash = "sha256:d59c30000a16d8edc7e64152e30220bfbd724c9bbb08368c054e24c651314f0a", size = 31936708, upload-time = "2026-02-23T00:19:58.694Z" }, + { url = "https://files.pythonhosted.org/packages/38/2e/524405c2b6392765ab1e2b722a41d5da33dc5c7b7278184a8ad29b6cb206/scipy-1.17.1-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:010f4333c96c9bb1a4516269e33cb5917b08ef2166d5556ca2fd9f082a9e6ea0", size = 28570135, upload-time = "2026-02-23T00:20:03.934Z" }, + { url = "https://files.pythonhosted.org/packages/fd/c3/5bd7199f4ea8556c0c8e39f04ccb014ac37d1468e6cfa6a95c6b3562b76e/scipy-1.17.1-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:2ceb2d3e01c5f1d83c4189737a42d9cb2fc38a6eeed225e7515eef71ad301dce", size = 20741977, upload-time = "2026-02-23T00:20:07.935Z" }, + { url = "https://files.pythonhosted.org/packages/d9/b8/8ccd9b766ad14c78386599708eb745f6b44f08400a5fd0ade7cf89b6fc93/scipy-1.17.1-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:844e165636711ef41f80b4103ed234181646b98a53c8f05da12ca5ca289134f6", size = 23029601, upload-time = "2026-02-23T00:20:12.161Z" }, + { url = "https://files.pythonhosted.org/packages/6d/a0/3cb6f4d2fb3e17428ad2880333cac878909ad1a89f678527b5328b93c1d4/scipy-1.17.1-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:158dd96d2207e21c966063e1635b1063cd7787b627b6f07305315dd73d9c679e", size = 33019667, upload-time = "2026-02-23T00:20:17.208Z" }, + { url = "https://files.pythonhosted.org/packages/f3/c3/2d834a5ac7bf3a0c806ad1508efc02dda3c8c61472a56132d7894c312dea/scipy-1.17.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:74cbb80d93260fe2ffa334efa24cb8f2f0f622a9b9febf8b483c0b865bfb3475", size = 35264159, upload-time = "2026-02-23T00:20:23.087Z" }, + { url = "https://files.pythonhosted.org/packages/4d/77/d3ed4becfdbd217c52062fafe35a72388d1bd82c2d0ba5ca19d6fcc93e11/scipy-1.17.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:dbc12c9f3d185f5c737d801da555fb74b3dcfa1a50b66a1a93e09190f41fab50", size = 35102771, upload-time = "2026-02-23T00:20:28.636Z" }, + { url = "https://files.pythonhosted.org/packages/bd/12/d19da97efde68ca1ee5538bb261d5d2c062f0c055575128f11a2730e3ac1/scipy-1.17.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:94055a11dfebe37c656e70317e1996dc197e1a15bbcc351bcdd4610e128fe1ca", size = 37665910, upload-time = "2026-02-23T00:20:34.743Z" }, + { url = "https://files.pythonhosted.org/packages/06/1c/1172a88d507a4baaf72c5a09bb6c018fe2ae0ab622e5830b703a46cc9e44/scipy-1.17.1-cp313-cp313t-win_amd64.whl", hash = "sha256:e30bdeaa5deed6bc27b4cc490823cd0347d7dae09119b8803ae576ea0ce52e4c", size = 36562980, upload-time = "2026-02-23T00:20:40.575Z" }, + { url = "https://files.pythonhosted.org/packages/70/b0/eb757336e5a76dfa7911f63252e3b7d1de00935d7705cf772db5b45ec238/scipy-1.17.1-cp313-cp313t-win_arm64.whl", hash = "sha256:a720477885a9d2411f94a93d16f9d89bad0f28ca23c3f8daa521e2dcc3f44d49", size = 24856543, upload-time = "2026-02-23T00:20:45.313Z" }, +] + +[[package]] +name = "shellingham" +version = "1.5.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" }, +] + +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, +] + +[[package]] +name = "sniffio" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" }, +] + +[[package]] +name = "sqlalchemy" +version = "2.0.49" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "greenlet", marker = "platform_machine == 'AMD64' or platform_machine == 'WIN32' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'ppc64le' or platform_machine == 'win32' or platform_machine == 'x86_64'" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/09/45/461788f35e0364a8da7bda51a1fe1b09762d0c32f12f63727998d85a873b/sqlalchemy-2.0.49.tar.gz", hash = "sha256:d15950a57a210e36dd4cec1aac22787e2a4d57ba9318233e2ef8b2daf9ff2d5f", size = 9898221, upload-time = "2026-04-03T16:38:11.704Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/60/b5/e3617cc67420f8f403efebd7b043128f94775e57e5b84e7255203390ceae/sqlalchemy-2.0.49-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c5070135e1b7409c4161133aa525419b0062088ed77c92b1da95366ec5cbebbe", size = 2159126, upload-time = "2026-04-03T16:50:13.242Z" }, + { url = "https://files.pythonhosted.org/packages/20/9b/91ca80403b17cd389622a642699e5f6564096b698e7cdcbcbb6409898bc4/sqlalchemy-2.0.49-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9ac7a3e245fd0310fd31495eb61af772e637bdf7d88ee81e7f10a3f271bff014", size = 3315509, upload-time = "2026-04-03T16:54:49.332Z" }, + { url = "https://files.pythonhosted.org/packages/b1/61/0722511d98c54de95acb327824cb759e8653789af2b1944ab1cc69d32565/sqlalchemy-2.0.49-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4d4e5a0ceba319942fa6b585cf82539288a61e314ef006c1209f734551ab9536", size = 3315014, upload-time = "2026-04-03T16:56:56.376Z" }, + { url = "https://files.pythonhosted.org/packages/46/55/d514a653ffeb4cebf4b54c47bec32ee28ad89d39fafba16eeed1d81dccd5/sqlalchemy-2.0.49-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3ddcb27fb39171de36e207600116ac9dfd4ae46f86c82a9bf3934043e80ebb88", size = 3267388, upload-time = "2026-04-03T16:54:51.272Z" }, + { url = "https://files.pythonhosted.org/packages/2f/16/0dcc56cb6d3335c1671a2258f5d2cb8267c9a2260e27fde53cbfb1b3540a/sqlalchemy-2.0.49-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:32fe6a41ad97302db2931f05bb91abbcc65b5ce4c675cd44b972428dd2947700", size = 3289602, upload-time = "2026-04-03T16:56:57.63Z" }, + { url = "https://files.pythonhosted.org/packages/51/6c/f8ab6fb04470a133cd80608db40aa292e6bae5f162c3a3d4ab19544a67af/sqlalchemy-2.0.49-cp311-cp311-win32.whl", hash = "sha256:46d51518d53edfbe0563662c96954dc8fcace9832332b914375f45a99b77cc9a", size = 2119044, upload-time = "2026-04-03T17:00:53.455Z" }, + { url = "https://files.pythonhosted.org/packages/c4/59/55a6d627d04b6ebb290693681d7683c7da001eddf90b60cfcc41ee907978/sqlalchemy-2.0.49-cp311-cp311-win_amd64.whl", hash = "sha256:951d4a210744813be63019f3df343bf233b7432aadf0db54c75802247330d3af", size = 2143642, upload-time = "2026-04-03T17:00:54.769Z" }, + { url = "https://files.pythonhosted.org/packages/49/b3/2de412451330756aaaa72d27131db6dde23995efe62c941184e15242a5fa/sqlalchemy-2.0.49-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4bbccb45260e4ff1b7db0be80a9025bb1e6698bdb808b83fff0000f7a90b2c0b", size = 2157681, upload-time = "2026-04-03T16:53:07.132Z" }, + { url = "https://files.pythonhosted.org/packages/50/84/b2a56e2105bd11ebf9f0b93abddd748e1a78d592819099359aa98134a8bf/sqlalchemy-2.0.49-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fb37f15714ec2652d574f021d479e78cd4eb9d04396dca36568fdfffb3487982", size = 3338976, upload-time = "2026-04-03T17:07:40Z" }, + { url = "https://files.pythonhosted.org/packages/2c/fa/65fcae2ed62f84ab72cf89536c7c3217a156e71a2c111b1305ab6f0690e2/sqlalchemy-2.0.49-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3bb9ec6436a820a4c006aad1ac351f12de2f2dbdaad171692ee457a02429b672", size = 3351937, upload-time = "2026-04-03T17:12:23.374Z" }, + { url = "https://files.pythonhosted.org/packages/f8/2f/6fd118563572a7fe475925742eb6b3443b2250e346a0cc27d8d408e73773/sqlalchemy-2.0.49-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8d6efc136f44a7e8bc8088507eaabbb8c2b55b3dbb63fe102c690da0ddebe55e", size = 3281646, upload-time = "2026-04-03T17:07:41.949Z" }, + { url = "https://files.pythonhosted.org/packages/c5/d7/410f4a007c65275b9cf82354adb4bb8ba587b176d0a6ee99caa16fe638f8/sqlalchemy-2.0.49-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e06e617e3d4fd9e51d385dfe45b077a41e9d1b033a7702551e3278ac597dc750", size = 3316695, upload-time = "2026-04-03T17:12:25.642Z" }, + { url = "https://files.pythonhosted.org/packages/d9/95/81f594aa60ded13273a844539041ccf1e66c5a7bed0a8e27810a3b52d522/sqlalchemy-2.0.49-cp312-cp312-win32.whl", hash = "sha256:83101a6930332b87653886c01d1ee7e294b1fe46a07dd9a2d2b4f91bcc88eec0", size = 2117483, upload-time = "2026-04-03T17:05:40.896Z" }, + { url = "https://files.pythonhosted.org/packages/47/9e/fd90114059175cac64e4fafa9bf3ac20584384d66de40793ae2e2f26f3bb/sqlalchemy-2.0.49-cp312-cp312-win_amd64.whl", hash = "sha256:618a308215b6cececb6240b9abde545e3acdabac7ae3e1d4e666896bf5ba44b4", size = 2144494, upload-time = "2026-04-03T17:05:42.282Z" }, + { url = "https://files.pythonhosted.org/packages/ae/81/81755f50eb2478eaf2049728491d4ea4f416c1eb013338682173259efa09/sqlalchemy-2.0.49-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:df2d441bacf97022e81ad047e1597552eb3f83ca8a8f1a1fdd43cd7fe3898120", size = 2154547, upload-time = "2026-04-03T16:53:08.64Z" }, + { url = "https://files.pythonhosted.org/packages/a2/bc/3494270da80811d08bcfa247404292428c4fe16294932bce5593f215cad9/sqlalchemy-2.0.49-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8e20e511dc15265fb433571391ba313e10dd8ea7e509d51686a51313b4ac01a2", size = 3280782, upload-time = "2026-04-03T17:07:43.508Z" }, + { url = "https://files.pythonhosted.org/packages/cd/f5/038741f5e747a5f6ea3e72487211579d8cbea5eb9827a9cbd61d0108c4bd/sqlalchemy-2.0.49-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:47604cb2159f8bbd5a1ab48a714557156320f20871ee64d550d8bf2683d980d3", size = 3297156, upload-time = "2026-04-03T17:12:27.697Z" }, + { url = "https://files.pythonhosted.org/packages/88/50/a6af0ff9dc954b43a65ca9b5367334e45d99684c90a3d3413fc19a02d43c/sqlalchemy-2.0.49-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:22d8798819f86720bc646ab015baff5ea4c971d68121cb36e2ebc2ee43ead2b7", size = 3228832, upload-time = "2026-04-03T17:07:45.38Z" }, + { url = "https://files.pythonhosted.org/packages/bc/d1/5f6bdad8de0bf546fc74370939621396515e0cdb9067402d6ba1b8afbe9a/sqlalchemy-2.0.49-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9b1c058c171b739e7c330760044803099c7fff11511e3ab3573e5327116a9c33", size = 3267000, upload-time = "2026-04-03T17:12:29.657Z" }, + { url = "https://files.pythonhosted.org/packages/f7/30/ad62227b4a9819a5e1c6abff77c0f614fa7c9326e5a3bdbee90f7139382b/sqlalchemy-2.0.49-cp313-cp313-win32.whl", hash = "sha256:a143af2ea6672f2af3f44ed8f9cd020e9cc34c56f0e8db12019d5d9ecf41cb3b", size = 2115641, upload-time = "2026-04-03T17:05:43.989Z" }, + { url = "https://files.pythonhosted.org/packages/17/3a/7215b1b7d6d49dc9a87211be44562077f5f04f9bb5a59552c1c8e2d98173/sqlalchemy-2.0.49-cp313-cp313-win_amd64.whl", hash = "sha256:12b04d1db2663b421fe072d638a138460a51d5a862403295671c4f3987fb9148", size = 2141498, upload-time = "2026-04-03T17:05:45.7Z" }, + { url = "https://files.pythonhosted.org/packages/28/4b/52a0cb2687a9cd1648252bb257be5a1ba2c2ded20ba695c65756a55a15a4/sqlalchemy-2.0.49-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:24bd94bb301ec672d8f0623eba9226cc90d775d25a0c92b5f8e4965d7f3a1518", size = 3560807, upload-time = "2026-04-03T16:58:31.666Z" }, + { url = "https://files.pythonhosted.org/packages/8c/d8/fda95459204877eed0458550d6c7c64c98cc50c2d8d618026737de9ed41a/sqlalchemy-2.0.49-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a51d3db74ba489266ef55c7a4534eb0b8db9a326553df481c11e5d7660c8364d", size = 3527481, upload-time = "2026-04-03T17:06:00.155Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0a/2aac8b78ac6487240cf7afef8f203ca783e8796002dc0cf65c4ee99ff8bb/sqlalchemy-2.0.49-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:55250fe61d6ebfd6934a272ee16ef1244e0f16b7af6cd18ab5b1fc9f08631db0", size = 3468565, upload-time = "2026-04-03T16:58:33.414Z" }, + { url = "https://files.pythonhosted.org/packages/a5/3d/ce71cfa82c50a373fd2148b3c870be05027155ce791dc9a5dcf439790b8b/sqlalchemy-2.0.49-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:46796877b47034b559a593d7e4b549aba151dae73f9e78212a3478161c12ab08", size = 3477769, upload-time = "2026-04-03T17:06:02.787Z" }, + { url = "https://files.pythonhosted.org/packages/d5/e8/0a9f5c1f7c6f9ca480319bf57c2d7423f08d31445974167a27d14483c948/sqlalchemy-2.0.49-cp313-cp313t-win32.whl", hash = "sha256:9c4969a86e41454f2858256c39bdfb966a20961e9b58bf8749b65abf447e9a8d", size = 2143319, upload-time = "2026-04-03T17:02:04.328Z" }, + { url = "https://files.pythonhosted.org/packages/0e/51/fb5240729fbec73006e137c4f7a7918ffd583ab08921e6ff81a999d6517a/sqlalchemy-2.0.49-cp313-cp313t-win_amd64.whl", hash = "sha256:b9870d15ef00e4d0559ae10ee5bc71b654d1f20076dbe8bc7ed19b4c0625ceba", size = 2175104, upload-time = "2026-04-03T17:02:05.989Z" }, + { url = "https://files.pythonhosted.org/packages/e5/30/8519fdde58a7bdf155b714359791ad1dc018b47d60269d5d160d311fdc36/sqlalchemy-2.0.49-py3-none-any.whl", hash = "sha256:ec44cfa7ef1a728e88ad41674de50f6db8cfdb3e2af84af86e0041aaf02d43d0", size = 1942158, upload-time = "2026-04-03T16:53:44.135Z" }, +] + +[[package]] +name = "sqlalchemy-spanner" +version = "1.18.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "alembic" }, + { name = "google-cloud-spanner" }, + { name = "sqlalchemy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2c/85/d439100c0518b5e3a339c9fcdbaa7436523955d6f676cf28c21e757cf61e/sqlalchemy_spanner-1.18.0.tar.gz", hash = "sha256:faabdf74797399e1c4d60500fd55012b1d21a5a697ebafc3d5156d0f2d3b3993", size = 82833, upload-time = "2026-05-07T08:04:52.897Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/db/26/aa7d506d10d859a3fc333595ed21acc774f4ff9d6b5abb4e11aed5a02c2e/sqlalchemy_spanner-1.18.0-py3-none-any.whl", hash = "sha256:626ff607d35f1fb0031ceabe887ed42578dfd9e0e134084e803c0ecc02353f3e", size = 31924, upload-time = "2026-05-07T08:03:35.49Z" }, +] + +[[package]] +name = "sqlparse" +version = "0.5.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/90/76/437d71068094df0726366574cf3432a4ed754217b436eb7429415cf2d480/sqlparse-0.5.5.tar.gz", hash = "sha256:e20d4a9b0b8585fdf63b10d30066c7c94c5d7a7ec47c889a2d83a3caa93ff28e", size = 120815, upload-time = "2025-12-19T07:17:45.073Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/4b/359f28a903c13438ef59ebeee215fb25da53066db67b305c125f1c6d2a25/sqlparse-0.5.5-py3-none-any.whl", hash = "sha256:12a08b3bf3eec877c519589833aed092e2444e68240a3577e8e26148acc7b1ba", size = 46138, upload-time = "2025-12-19T07:17:46.573Z" }, +] + +[[package]] +name = "sse-starlette" +version = "3.4.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "starlette" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f7/2b/58abc2d1fd397e7dde08e947e05c884d8ef2f78d5e2588c17a12d42d6994/sse_starlette-3.4.4.tar.gz", hash = "sha256:07e0fa0460138baf25cdd5fb28683472c3995dc1642225191b3832d62526bcb0", size = 31819, upload-time = "2026-05-12T17:37:17.019Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/67/805710444ea8cc75fbf70b920ed431a560c4bf9c57f7d5a3117213189399/sse_starlette-3.4.4-py3-none-any.whl", hash = "sha256:3f4dd50d8aed2771a091f3a83000323fc3844541c16b4fe585ae2420cc6df973", size = 16514, upload-time = "2026-05-12T17:37:15.601Z" }, +] + +[[package]] +name = "starlette" +version = "0.52.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c4/68/79977123bb7be889ad680d79a40f339082c1978b5cfcf62c2d8d196873ac/starlette-0.52.1.tar.gz", hash = "sha256:834edd1b0a23167694292e94f597773bc3f89f362be6effee198165a35d62933", size = 2653702, upload-time = "2026-01-18T13:34:11.062Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/0d/13d1d239a25cbfb19e740db83143e95c772a1fe10202dda4b76792b114dd/starlette-0.52.1-py3-none-any.whl", hash = "sha256:0029d43eb3d273bc4f83a08720b4912ea4b071087a3b48db01b7c839f7954d74", size = 74272, upload-time = "2026-01-18T13:34:09.188Z" }, +] + +[[package]] +name = "tabulate" +version = "0.10.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/46/58/8c37dea7bbf769b20d58e7ace7e5edfe65b849442b00ffcdd56be88697c6/tabulate-0.10.0.tar.gz", hash = "sha256:e2cfde8f79420f6deeffdeda9aaec3b6bc5abce947655d17ac662b126e48a60d", size = 91754, upload-time = "2026-03-04T18:55:34.402Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/99/55/db07de81b5c630da5cbf5c7df646580ca26dfaefa593667fc6f2fe016d2e/tabulate-0.10.0-py3-none-any.whl", hash = "sha256:f0b0622e567335c8fabaaa659f1b33bcb6ddfe2e496071b743aa113f8774f2d3", size = 39814, upload-time = "2026-03-04T18:55:31.284Z" }, +] + +[[package]] +name = "tenacity" +version = "9.1.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/47/c6/ee486fd809e357697ee8a44d3d69222b344920433d3b6666ccd9b374630c/tenacity-9.1.4.tar.gz", hash = "sha256:adb31d4c263f2bd041081ab33b498309a57c77f9acf2db65aadf0898179cf93a", size = 49413, upload-time = "2026-02-07T10:45:33.841Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d7/c1/eb8f9debc45d3b7918a32ab756658a0904732f75e555402972246b0b8e71/tenacity-9.1.4-py3-none-any.whl", hash = "sha256:6095a360c919085f28c6527de529e76a06ad89b23659fa881ae0649b867a9d55", size = 28926, upload-time = "2026-02-07T10:45:32.24Z" }, +] + +[[package]] +name = "threadpoolctl" +version = "3.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b7/4d/08c89e34946fce2aec4fbb45c9016efd5f4d7f24af8e5d93296e935631d8/threadpoolctl-3.6.0.tar.gz", hash = "sha256:8ab8b4aa3491d812b623328249fab5302a68d2d71745c8a4c719a2fcaba9f44e", size = 21274, upload-time = "2025-03-13T13:49:23.031Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/d5/f9a850d79b0851d1d4ef6456097579a9005b31fea68726a4ae5f2d82ddd9/threadpoolctl-3.6.0-py3-none-any.whl", hash = "sha256:43a0b8fd5a2928500110039e43a5eed8480b918967083ea48dc3ab9f13c4a7fb", size = 18638, upload-time = "2025-03-13T13:49:21.846Z" }, +] + +[[package]] +name = "tiktoken" +version = "0.12.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "regex" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7d/ab/4d017d0f76ec3171d469d80fc03dfbb4e48a4bcaddaa831b31d526f05edc/tiktoken-0.12.0.tar.gz", hash = "sha256:b18ba7ee2b093863978fcb14f74b3707cdc8d4d4d3836853ce7ec60772139931", size = 37806, upload-time = "2025-10-06T20:22:45.419Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/46/21ea696b21f1d6d1efec8639c204bdf20fde8bafb351e1355c72c5d7de52/tiktoken-0.12.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:6e227c7f96925003487c33b1b32265fad2fbcec2b7cf4817afb76d416f40f6bb", size = 1051565, upload-time = "2025-10-06T20:21:44.566Z" }, + { url = "https://files.pythonhosted.org/packages/c9/d9/35c5d2d9e22bb2a5f74ba48266fb56c63d76ae6f66e02feb628671c0283e/tiktoken-0.12.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c06cf0fcc24c2cb2adb5e185c7082a82cba29c17575e828518c2f11a01f445aa", size = 995284, upload-time = "2025-10-06T20:21:45.622Z" }, + { url = "https://files.pythonhosted.org/packages/01/84/961106c37b8e49b9fdcf33fe007bb3a8fdcc380c528b20cc7fbba80578b8/tiktoken-0.12.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:f18f249b041851954217e9fd8e5c00b024ab2315ffda5ed77665a05fa91f42dc", size = 1129201, upload-time = "2025-10-06T20:21:47.074Z" }, + { url = "https://files.pythonhosted.org/packages/6a/d0/3d9275198e067f8b65076a68894bb52fd253875f3644f0a321a720277b8a/tiktoken-0.12.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:47a5bc270b8c3db00bb46ece01ef34ad050e364b51d406b6f9730b64ac28eded", size = 1152444, upload-time = "2025-10-06T20:21:48.139Z" }, + { url = "https://files.pythonhosted.org/packages/78/db/a58e09687c1698a7c592e1038e01c206569b86a0377828d51635561f8ebf/tiktoken-0.12.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:508fa71810c0efdcd1b898fda574889ee62852989f7c1667414736bcb2b9a4bd", size = 1195080, upload-time = "2025-10-06T20:21:49.246Z" }, + { url = "https://files.pythonhosted.org/packages/9e/1b/a9e4d2bf91d515c0f74afc526fd773a812232dd6cda33ebea7f531202325/tiktoken-0.12.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a1af81a6c44f008cba48494089dd98cccb8b313f55e961a52f5b222d1e507967", size = 1255240, upload-time = "2025-10-06T20:21:50.274Z" }, + { url = "https://files.pythonhosted.org/packages/9d/15/963819345f1b1fb0809070a79e9dd96938d4ca41297367d471733e79c76c/tiktoken-0.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:3e68e3e593637b53e56f7237be560f7a394451cb8c11079755e80ae64b9e6def", size = 879422, upload-time = "2025-10-06T20:21:51.734Z" }, + { url = "https://files.pythonhosted.org/packages/a4/85/be65d39d6b647c79800fd9d29241d081d4eeb06271f383bb87200d74cf76/tiktoken-0.12.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b97f74aca0d78a1ff21b8cd9e9925714c15a9236d6ceacf5c7327c117e6e21e8", size = 1050728, upload-time = "2025-10-06T20:21:52.756Z" }, + { url = "https://files.pythonhosted.org/packages/4a/42/6573e9129bc55c9bf7300b3a35bef2c6b9117018acca0dc760ac2d93dffe/tiktoken-0.12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2b90f5ad190a4bb7c3eb30c5fa32e1e182ca1ca79f05e49b448438c3e225a49b", size = 994049, upload-time = "2025-10-06T20:21:53.782Z" }, + { url = "https://files.pythonhosted.org/packages/66/c5/ed88504d2f4a5fd6856990b230b56d85a777feab84e6129af0822f5d0f70/tiktoken-0.12.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:65b26c7a780e2139e73acc193e5c63ac754021f160df919add909c1492c0fb37", size = 1129008, upload-time = "2025-10-06T20:21:54.832Z" }, + { url = "https://files.pythonhosted.org/packages/f4/90/3dae6cc5436137ebd38944d396b5849e167896fc2073da643a49f372dc4f/tiktoken-0.12.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:edde1ec917dfd21c1f2f8046b86348b0f54a2c0547f68149d8600859598769ad", size = 1152665, upload-time = "2025-10-06T20:21:56.129Z" }, + { url = "https://files.pythonhosted.org/packages/a3/fe/26df24ce53ffde419a42f5f53d755b995c9318908288c17ec3f3448313a3/tiktoken-0.12.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:35a2f8ddd3824608b3d650a000c1ef71f730d0c56486845705a8248da00f9fe5", size = 1194230, upload-time = "2025-10-06T20:21:57.546Z" }, + { url = "https://files.pythonhosted.org/packages/20/cc/b064cae1a0e9fac84b0d2c46b89f4e57051a5f41324e385d10225a984c24/tiktoken-0.12.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:83d16643edb7fa2c99eff2ab7733508aae1eebb03d5dfc46f5565862810f24e3", size = 1254688, upload-time = "2025-10-06T20:21:58.619Z" }, + { url = "https://files.pythonhosted.org/packages/81/10/b8523105c590c5b8349f2587e2fdfe51a69544bd5a76295fc20f2374f470/tiktoken-0.12.0-cp312-cp312-win_amd64.whl", hash = "sha256:ffc5288f34a8bc02e1ea7047b8d041104791d2ddbf42d1e5fa07822cbffe16bd", size = 878694, upload-time = "2025-10-06T20:21:59.876Z" }, + { url = "https://files.pythonhosted.org/packages/00/61/441588ee21e6b5cdf59d6870f86beb9789e532ee9718c251b391b70c68d6/tiktoken-0.12.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:775c2c55de2310cc1bc9a3ad8826761cbdc87770e586fd7b6da7d4589e13dab3", size = 1050802, upload-time = "2025-10-06T20:22:00.96Z" }, + { url = "https://files.pythonhosted.org/packages/1f/05/dcf94486d5c5c8d34496abe271ac76c5b785507c8eae71b3708f1ad9b45a/tiktoken-0.12.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a01b12f69052fbe4b080a2cfb867c4de12c704b56178edf1d1d7b273561db160", size = 993995, upload-time = "2025-10-06T20:22:02.788Z" }, + { url = "https://files.pythonhosted.org/packages/a0/70/5163fe5359b943f8db9946b62f19be2305de8c3d78a16f629d4165e2f40e/tiktoken-0.12.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:01d99484dc93b129cd0964f9d34eee953f2737301f18b3c7257bf368d7615baa", size = 1128948, upload-time = "2025-10-06T20:22:03.814Z" }, + { url = "https://files.pythonhosted.org/packages/0c/da/c028aa0babf77315e1cef357d4d768800c5f8a6de04d0eac0f377cb619fa/tiktoken-0.12.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:4a1a4fcd021f022bfc81904a911d3df0f6543b9e7627b51411da75ff2fe7a1be", size = 1151986, upload-time = "2025-10-06T20:22:05.173Z" }, + { url = "https://files.pythonhosted.org/packages/a0/5a/886b108b766aa53e295f7216b509be95eb7d60b166049ce2c58416b25f2a/tiktoken-0.12.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:981a81e39812d57031efdc9ec59fa32b2a5a5524d20d4776574c4b4bd2e9014a", size = 1194222, upload-time = "2025-10-06T20:22:06.265Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f8/4db272048397636ac7a078d22773dd2795b1becee7bc4922fe6207288d57/tiktoken-0.12.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9baf52f84a3f42eef3ff4e754a0db79a13a27921b457ca9832cf944c6be4f8f3", size = 1255097, upload-time = "2025-10-06T20:22:07.403Z" }, + { url = "https://files.pythonhosted.org/packages/8e/32/45d02e2e0ea2be3a9ed22afc47d93741247e75018aac967b713b2941f8ea/tiktoken-0.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:b8a0cd0c789a61f31bf44851defbd609e8dd1e2c8589c614cc1060940ef1f697", size = 879117, upload-time = "2025-10-06T20:22:08.418Z" }, + { url = "https://files.pythonhosted.org/packages/ce/76/994fc868f88e016e6d05b0da5ac24582a14c47893f4474c3e9744283f1d5/tiktoken-0.12.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:d5f89ea5680066b68bcb797ae85219c72916c922ef0fcdd3480c7d2315ffff16", size = 1050309, upload-time = "2025-10-06T20:22:10.939Z" }, + { url = "https://files.pythonhosted.org/packages/f6/b8/57ef1456504c43a849821920d582a738a461b76a047f352f18c0b26c6516/tiktoken-0.12.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b4e7ed1c6a7a8a60a3230965bdedba8cc58f68926b835e519341413370e0399a", size = 993712, upload-time = "2025-10-06T20:22:12.115Z" }, + { url = "https://files.pythonhosted.org/packages/72/90/13da56f664286ffbae9dbcfadcc625439142675845baa62715e49b87b68b/tiktoken-0.12.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:fc530a28591a2d74bce821d10b418b26a094bf33839e69042a6e86ddb7a7fb27", size = 1128725, upload-time = "2025-10-06T20:22:13.541Z" }, + { url = "https://files.pythonhosted.org/packages/05/df/4f80030d44682235bdaecd7346c90f67ae87ec8f3df4a3442cb53834f7e4/tiktoken-0.12.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:06a9f4f49884139013b138920a4c393aa6556b2f8f536345f11819389c703ebb", size = 1151875, upload-time = "2025-10-06T20:22:14.559Z" }, + { url = "https://files.pythonhosted.org/packages/22/1f/ae535223a8c4ef4c0c1192e3f9b82da660be9eb66b9279e95c99288e9dab/tiktoken-0.12.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:04f0e6a985d95913cabc96a741c5ffec525a2c72e9df086ff17ebe35985c800e", size = 1194451, upload-time = "2025-10-06T20:22:15.545Z" }, + { url = "https://files.pythonhosted.org/packages/78/a7/f8ead382fce0243cb625c4f266e66c27f65ae65ee9e77f59ea1653b6d730/tiktoken-0.12.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:0ee8f9ae00c41770b5f9b0bb1235474768884ae157de3beb5439ca0fd70f3e25", size = 1253794, upload-time = "2025-10-06T20:22:16.624Z" }, + { url = "https://files.pythonhosted.org/packages/93/e0/6cc82a562bc6365785a3ff0af27a2a092d57c47d7a81d9e2295d8c36f011/tiktoken-0.12.0-cp313-cp313t-win_amd64.whl", hash = "sha256:dc2dd125a62cb2b3d858484d6c614d136b5b848976794edfb63688d539b8b93f", size = 878777, upload-time = "2025-10-06T20:22:18.036Z" }, +] + +[[package]] +name = "tokenizers" +version = "0.22.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "huggingface-hub" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/73/6f/f80cfef4a312e1fb34baf7d85c72d4411afde10978d4657f8cdd811d3ccc/tokenizers-0.22.2.tar.gz", hash = "sha256:473b83b915e547aa366d1eee11806deaf419e17be16310ac0a14077f1e28f917", size = 372115, upload-time = "2026-01-05T10:45:15.988Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/92/97/5dbfabf04c7e348e655e907ed27913e03db0923abb5dfdd120d7b25630e1/tokenizers-0.22.2-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:544dd704ae7238755d790de45ba8da072e9af3eea688f698b137915ae959281c", size = 3100275, upload-time = "2026-01-05T10:41:02.158Z" }, + { url = "https://files.pythonhosted.org/packages/2e/47/174dca0502ef88b28f1c9e06b73ce33500eedfac7a7692108aec220464e7/tokenizers-0.22.2-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:1e418a55456beedca4621dbab65a318981467a2b188e982a23e117f115ce5001", size = 2981472, upload-time = "2026-01-05T10:41:00.276Z" }, + { url = "https://files.pythonhosted.org/packages/d6/84/7990e799f1309a8b87af6b948f31edaa12a3ed22d11b352eaf4f4b2e5753/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2249487018adec45d6e3554c71d46eb39fa8ea67156c640f7513eb26f318cec7", size = 3290736, upload-time = "2026-01-05T10:40:32.165Z" }, + { url = "https://files.pythonhosted.org/packages/78/59/09d0d9ba94dcd5f4f1368d4858d24546b4bdc0231c2354aa31d6199f0399/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:25b85325d0815e86e0bac263506dd114578953b7b53d7de09a6485e4a160a7dd", size = 3168835, upload-time = "2026-01-05T10:40:38.847Z" }, + { url = "https://files.pythonhosted.org/packages/47/50/b3ebb4243e7160bda8d34b731e54dd8ab8b133e50775872e7a434e524c28/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bfb88f22a209ff7b40a576d5324bf8286b519d7358663db21d6246fb17eea2d5", size = 3521673, upload-time = "2026-01-05T10:40:56.614Z" }, + { url = "https://files.pythonhosted.org/packages/e0/fa/89f4cb9e08df770b57adb96f8cbb7e22695a4cb6c2bd5f0c4f0ebcf33b66/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1c774b1276f71e1ef716e5486f21e76333464f47bece56bbd554485982a9e03e", size = 3724818, upload-time = "2026-01-05T10:40:44.507Z" }, + { url = "https://files.pythonhosted.org/packages/64/04/ca2363f0bfbe3b3d36e95bf67e56a4c88c8e3362b658e616d1ac185d47f2/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:df6c4265b289083bf710dff49bc51ef252f9d5be33a45ee2bed151114a56207b", size = 3379195, upload-time = "2026-01-05T10:40:51.139Z" }, + { url = "https://files.pythonhosted.org/packages/2e/76/932be4b50ef6ccedf9d3c6639b056a967a86258c6d9200643f01269211ca/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:369cc9fc8cc10cb24143873a0d95438bb8ee257bb80c71989e3ee290e8d72c67", size = 3274982, upload-time = "2026-01-05T10:40:58.331Z" }, + { url = "https://files.pythonhosted.org/packages/1d/28/5f9f5a4cc211b69e89420980e483831bcc29dade307955cc9dc858a40f01/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:29c30b83d8dcd061078b05ae0cb94d3c710555fbb44861139f9f83dcca3dc3e4", size = 9478245, upload-time = "2026-01-05T10:41:04.053Z" }, + { url = "https://files.pythonhosted.org/packages/6c/fb/66e2da4704d6aadebf8cb39f1d6d1957df667ab24cff2326b77cda0dcb85/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:37ae80a28c1d3265bb1f22464c856bd23c02a05bb211e56d0c5301a435be6c1a", size = 9560069, upload-time = "2026-01-05T10:45:10.673Z" }, + { url = "https://files.pythonhosted.org/packages/16/04/fed398b05caa87ce9b1a1bb5166645e38196081b225059a6edaff6440fac/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:791135ee325f2336f498590eb2f11dc5c295232f288e75c99a36c5dbce63088a", size = 9899263, upload-time = "2026-01-05T10:45:12.559Z" }, + { url = "https://files.pythonhosted.org/packages/05/a1/d62dfe7376beaaf1394917e0f8e93ee5f67fea8fcf4107501db35996586b/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:38337540fbbddff8e999d59970f3c6f35a82de10053206a7562f1ea02d046fa5", size = 10033429, upload-time = "2026-01-05T10:45:14.333Z" }, + { url = "https://files.pythonhosted.org/packages/fd/18/a545c4ea42af3df6effd7d13d250ba77a0a86fb20393143bbb9a92e434d4/tokenizers-0.22.2-cp39-abi3-win32.whl", hash = "sha256:a6bf3f88c554a2b653af81f3204491c818ae2ac6fbc09e76ef4773351292bc92", size = 2502363, upload-time = "2026-01-05T10:45:20.593Z" }, + { url = "https://files.pythonhosted.org/packages/65/71/0670843133a43d43070abeb1949abfdef12a86d490bea9cd9e18e37c5ff7/tokenizers-0.22.2-cp39-abi3-win_amd64.whl", hash = "sha256:c9ea31edff2968b44a88f97d784c2f16dc0729b8b143ed004699ebca91f05c48", size = 2747786, upload-time = "2026-01-05T10:45:18.411Z" }, + { url = "https://files.pythonhosted.org/packages/72/f4/0de46cfa12cdcbcd464cc59fde36912af405696f687e53a091fb432f694c/tokenizers-0.22.2-cp39-abi3-win_arm64.whl", hash = "sha256:9ce725d22864a1e965217204946f830c37876eee3b2ba6fc6255e8e903d5fcbc", size = 2612133, upload-time = "2026-01-05T10:45:17.232Z" }, +] + +[[package]] +name = "tqdm" +version = "4.67.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/09/a9/6ba95a270c6f1fbcd8dac228323f2777d886cb206987444e4bce66338dd4/tqdm-4.67.3.tar.gz", hash = "sha256:7d825f03f89244ef73f1d4ce193cb1774a8179fd96f31d7e1dcde62092b960bb", size = 169598, upload-time = "2026-02-03T17:35:53.048Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/16/e1/3079a9ff9b8e11b846c6ac5c8b5bfb7ff225eee721825310c91b3b50304f/tqdm-4.67.3-py3-none-any.whl", hash = "sha256:ee1e4c0e59148062281c49d80b25b67771a127c85fc9676d3be5f243206826bf", size = 78374, upload-time = "2026-02-03T17:35:50.982Z" }, +] + +[[package]] +name = "ty" +version = "0.0.38" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/33/3b/45be6b37d5060d6917bf7f1f234c00d360fc5f8b7486f8a96af640e25661/ty-0.0.38.tar.gz", hash = "sha256:fbc8d47f7630457669ab41e333dc093897fdb7ead1ffc94dcf8f30b5d39aa56d", size = 5681218, upload-time = "2026-05-20T00:15:32.781Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/43/ea9b4e57d6a266670dbe34858e92f6093ca054ad1b48f1c82580a72340fb/ty-0.0.38-py3-none-linux_armv6l.whl", hash = "sha256:3501dcf44ca03f813f9cb4fabfdf601adc0ac1337c411405b470530679e37a45", size = 11289326, upload-time = "2026-05-20T00:14:52.371Z" }, + { url = "https://files.pythonhosted.org/packages/5d/ff/24e2f623a1c6b5f5ccf8bf82fccd937033c6a7dba57a4028c7f41270fa4a/ty-0.0.38-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b34b4094b76252c3e8c90762cdd5e8a9f1101534484745ff4b480f71eb38ac2e", size = 11063047, upload-time = "2026-05-20T00:14:42.832Z" }, + { url = "https://files.pythonhosted.org/packages/e9/41/4f0d910f0acbd20b358eda80a5cd6a8361d27ff5b8e87ab559d3f69f125e/ty-0.0.38-py3-none-macosx_11_0_arm64.whl", hash = "sha256:c518ad33a877677365baab2e21d82cf59ffee789203a15a143f5179ee5a1d3f8", size = 10494436, upload-time = "2026-05-20T00:15:24.425Z" }, + { url = "https://files.pythonhosted.org/packages/69/d8/da06833422082aa98b169a391f9197e2d73865e96c90b6979ac886b890a2/ty-0.0.38-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9238494722303eccddc6a27eb647948b694eecd6b974910d13b9e6cd46bbeb6a", size = 11000992, upload-time = "2026-05-20T00:14:58.368Z" }, + { url = "https://files.pythonhosted.org/packages/16/f7/e1172197fb827e6410ca3eb0dc68ef2789f3c70683696f2a0ce5c90764fd/ty-0.0.38-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:31d91d7336c5d51bf822ac0df512f300584ca4dcca041fc6a6d7df03a8ddbb31", size = 11058583, upload-time = "2026-05-20T00:15:11.314Z" }, + { url = "https://files.pythonhosted.org/packages/5b/61/7fbaf0c05981e006a8804287819c574dff90a6bf8e96efad7226be0700aa/ty-0.0.38-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:65165879814993450710b9349791e4898c65e36b1e14eec554884c06a2f20ff1", size = 11531036, upload-time = "2026-05-20T00:15:14.62Z" }, + { url = "https://files.pythonhosted.org/packages/49/e3/47c0c64e401d50f925df3e52479d4e7626754b2a9e38201d142fdacd6252/ty-0.0.38-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6d61868b8d1c4033bf8088191de953fed245c2f9e1bb9d2d53e5699170b0924c", size = 12129991, upload-time = "2026-05-20T00:14:39.475Z" }, + { url = "https://files.pythonhosted.org/packages/90/99/2f452d02901bcd7f1b109cf5b848727ce37f372c3406143aa52d1305d40e/ty-0.0.38-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8f9a9175548c98dbff7707865738c07c2b1f8e07a09b8c68101baebb5dac59a4", size = 11756167, upload-time = "2026-05-20T00:15:27.526Z" }, + { url = "https://files.pythonhosted.org/packages/dd/0c/c7e14d111c813e1a20b82e944f1c997c4631a2bb710eaa64fb6b26835e13/ty-0.0.38-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:375d3a964c6b4aea2e9237fdb5eb9ed03dc43088986a94209a28a4ea3b62001c", size = 11637099, upload-time = "2026-05-20T00:15:21.261Z" }, + { url = "https://files.pythonhosted.org/packages/37/de/ab02659dd1ed62898db7db4d37f9937c80854dd45e95093fa0fe10328d82/ty-0.0.38-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:cdfd547782c45267aa0b52abad31bd406bf4768c264532ef9e2360cd3c6ce048", size = 11813583, upload-time = "2026-05-20T00:14:45.875Z" }, + { url = "https://files.pythonhosted.org/packages/7e/57/bd1b5ebf4e71a4295484afac0202df1740b0807762b86744b1bef4534984/ty-0.0.38-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:858bc675b75626470abe4e6c3b3934b853642b04f2ac4d7139fcefea3b48b213", size = 10975405, upload-time = "2026-05-20T00:15:30.354Z" }, + { url = "https://files.pythonhosted.org/packages/e7/55/0305c78711bbd23922cf291996a08ef9544f4179da98e9a75c14e608f379/ty-0.0.38-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:54be4f00432870da42cd74fe145a3362fd248e22d032c74bd807cb45bf068f94", size = 11097551, upload-time = "2026-05-20T00:14:55.179Z" }, + { url = "https://files.pythonhosted.org/packages/7c/4f/7effe7f9a6ac9719eb7234172c01739c5f888bb47f9acc2ea8da1f4afed3/ty-0.0.38-py3-none-musllinux_1_2_i686.whl", hash = "sha256:494af66a76a86dbf16a3003d3b63b03484aa4c7489dfe11f3ee5413b98b22d60", size = 11214391, upload-time = "2026-05-20T00:15:18.094Z" }, + { url = "https://files.pythonhosted.org/packages/75/cd/d9fdfec3a74a6ad0209fa5e7113ae29d4f457d0651cfbb813b4c6563e0d4/ty-0.0.38-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:3d92527c4be78a5ce6d32e8bb0aa2a6988d4076eddf1294e56fdaf06d1a98e7e", size = 11730871, upload-time = "2026-05-20T00:14:49.219Z" }, + { url = "https://files.pythonhosted.org/packages/0e/4a/beefade12d109b4f7793d61b04b4478b1ad4d1465a719e7ff55b2d42461a/ty-0.0.38-py3-none-win32.whl", hash = "sha256:36fc5dd5dc09207ff3004b1560a79a3fb8d12456daeec914a7b802a918da654c", size = 10548583, upload-time = "2026-05-20T00:15:07.892Z" }, + { url = "https://files.pythonhosted.org/packages/15/64/941b205e2e46cc2297c245c64aa7691410b7454fa4d07a6cb3cf59487833/ty-0.0.38-py3-none-win_amd64.whl", hash = "sha256:eef0a8956ba14514076b1a963d13eb32986d9ebad7f0527b3cc01cb68bf35147", size = 11650542, upload-time = "2026-05-20T00:15:01.441Z" }, + { url = "https://files.pythonhosted.org/packages/59/02/c1c4f9ec4b94d95190636fa13f79c32f65165fbe3a0503882d4df164d2ac/ty-0.0.38-py3-none-win_arm64.whl", hash = "sha256:79abfc8658a026c30b1c955613437dab3ef4b12feca56a3e6df50903cc39e07f", size = 11010307, upload-time = "2026-05-20T00:15:04.567Z" }, +] + +[[package]] +name = "typer" +version = "0.23.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-doc" }, + { name = "click" }, + { name = "rich" }, + { name = "shellingham" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fd/07/b822e1b307d40e263e8253d2384cf98c51aa2368cc7ba9a07e523a1d964b/typer-0.23.1.tar.gz", hash = "sha256:2070374e4d31c83e7b61362fd859aa683576432fd5b026b060ad6b4cd3b86134", size = 120047, upload-time = "2026-02-13T10:04:30.984Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d5/91/9b286ab899c008c2cb05e8be99814807e7fbbd33f0c0c960470826e5ac82/typer-0.23.1-py3-none-any.whl", hash = "sha256:3291ad0d3c701cbf522012faccfbb29352ff16ad262db2139e6b01f15781f14e", size = 56813, upload-time = "2026-02-13T10:04:32.008Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, +] + +[[package]] +name = "tzdata" +version = "2026.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ba/19/1b9b0e29f30c6d35cb345486df41110984ea67ae69dddbc0e8a100999493/tzdata-2026.2.tar.gz", hash = "sha256:9173fde7d80d9018e02a662e168e5a2d04f87c41ea174b139fbef642eda62d10", size = 198254, upload-time = "2026-04-24T15:22:08.651Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ce/e4/dccd7f47c4b64213ac01ef921a1337ee6e30e8c6466046018326977efd95/tzdata-2026.2-py2.py3-none-any.whl", hash = "sha256:bbe9af844f658da81a5f95019480da3a89415801f6cc966806612cc7169bffe7", size = 349321, upload-time = "2026-04-24T15:22:05.876Z" }, +] + +[[package]] +name = "tzlocal" +version = "5.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "tzdata", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8b/2e/c14812d3d4d9cd1773c6be938f89e5735a1f11a9f184ac3639b93cef35d5/tzlocal-5.3.1.tar.gz", hash = "sha256:cceffc7edecefea1f595541dbd6e990cb1ea3d19bf01b2809f362a03dd7921fd", size = 30761, upload-time = "2025-03-05T21:17:41.549Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/14/e2a54fabd4f08cd7af1c07030603c3356b74da07f7cc056e600436edfa17/tzlocal-5.3.1-py3-none-any.whl", hash = "sha256:eb1a66c3ef5847adf7a834f1be0800581b683b5608e74f86ecbcef8ab91bb85d", size = 18026, upload-time = "2025-03-05T21:17:39.857Z" }, +] + +[[package]] +name = "uritemplate" +version = "4.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/98/60/f174043244c5306c9988380d2cb10009f91563fc4b31293d27e17201af56/uritemplate-4.2.0.tar.gz", hash = "sha256:480c2ed180878955863323eea31b0ede668795de182617fef9c6ca09e6ec9d0e", size = 33267, upload-time = "2025-06-02T15:12:06.318Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a9/99/3ae339466c9183ea5b8ae87b34c0b897eda475d2aec2307cae60e5cd4f29/uritemplate-4.2.0-py3-none-any.whl", hash = "sha256:962201ba1c4edcab02e60f9a0d3821e82dfc5d2d6662a21abd533879bdb8a686", size = 11488, upload-time = "2025-06-02T15:12:03.405Z" }, +] + +[[package]] +name = "urllib3" +version = "2.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" }, +] + +[[package]] +name = "uvicorn" +version = "0.47.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f6/b1/8e7077a8641086aea449e1b5752a570f1b5906c64e0a33cd6d93b63a066b/uvicorn-0.47.0.tar.gz", hash = "sha256:7c9a0ea1a9414106bbab7324609c162d8fa0cdcdcb703060987269d77c7bb533", size = 90582, upload-time = "2026-05-14T18:16:54.455Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/15/41/ac2dfdbc1f60c7af4f994c7a335cfa7040c01642b605d65f611cecc2a1e4/uvicorn-0.47.0-py3-none-any.whl", hash = "sha256:2c5715bc12d1892d84752049f400cd1c3cb018514967fdfeb97640443a6a9432", size = 71301, upload-time = "2026-05-14T18:16:51.762Z" }, +] + +[[package]] +name = "watchdog" +version = "6.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/db/7d/7f3d619e951c88ed75c6037b246ddcf2d322812ee8ea189be89511721d54/watchdog-6.0.0.tar.gz", hash = "sha256:9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282", size = 131220, upload-time = "2024-11-01T14:07:13.037Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/24/d9be5cd6642a6aa68352ded4b4b10fb0d7889cb7f45814fb92cecd35f101/watchdog-6.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6eb11feb5a0d452ee41f824e271ca311a09e250441c262ca2fd7ebcf2461a06c", size = 96393, upload-time = "2024-11-01T14:06:31.756Z" }, + { url = "https://files.pythonhosted.org/packages/63/7a/6013b0d8dbc56adca7fdd4f0beed381c59f6752341b12fa0886fa7afc78b/watchdog-6.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ef810fbf7b781a5a593894e4f439773830bdecb885e6880d957d5b9382a960d2", size = 88392, upload-time = "2024-11-01T14:06:32.99Z" }, + { url = "https://files.pythonhosted.org/packages/d1/40/b75381494851556de56281e053700e46bff5b37bf4c7267e858640af5a7f/watchdog-6.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:afd0fe1b2270917c5e23c2a65ce50c2a4abb63daafb0d419fde368e272a76b7c", size = 89019, upload-time = "2024-11-01T14:06:34.963Z" }, + { url = "https://files.pythonhosted.org/packages/39/ea/3930d07dafc9e286ed356a679aa02d777c06e9bfd1164fa7c19c288a5483/watchdog-6.0.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bdd4e6f14b8b18c334febb9c4425a878a2ac20efd1e0b231978e7b150f92a948", size = 96471, upload-time = "2024-11-01T14:06:37.745Z" }, + { url = "https://files.pythonhosted.org/packages/12/87/48361531f70b1f87928b045df868a9fd4e253d9ae087fa4cf3f7113be363/watchdog-6.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c7c15dda13c4eb00d6fb6fc508b3c0ed88b9d5d374056b239c4ad1611125c860", size = 88449, upload-time = "2024-11-01T14:06:39.748Z" }, + { url = "https://files.pythonhosted.org/packages/5b/7e/8f322f5e600812e6f9a31b75d242631068ca8f4ef0582dd3ae6e72daecc8/watchdog-6.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6f10cb2d5902447c7d0da897e2c6768bca89174d0c6e1e30abec5421af97a5b0", size = 89054, upload-time = "2024-11-01T14:06:41.009Z" }, + { url = "https://files.pythonhosted.org/packages/68/98/b0345cabdce2041a01293ba483333582891a3bd5769b08eceb0d406056ef/watchdog-6.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:490ab2ef84f11129844c23fb14ecf30ef3d8a6abafd3754a6f75ca1e6654136c", size = 96480, upload-time = "2024-11-01T14:06:42.952Z" }, + { url = "https://files.pythonhosted.org/packages/85/83/cdf13902c626b28eedef7ec4f10745c52aad8a8fe7eb04ed7b1f111ca20e/watchdog-6.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:76aae96b00ae814b181bb25b1b98076d5fc84e8a53cd8885a318b42b6d3a5134", size = 88451, upload-time = "2024-11-01T14:06:45.084Z" }, + { url = "https://files.pythonhosted.org/packages/fe/c4/225c87bae08c8b9ec99030cd48ae9c4eca050a59bf5c2255853e18c87b50/watchdog-6.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a175f755fc2279e0b7312c0035d52e27211a5bc39719dd529625b1930917345b", size = 89057, upload-time = "2024-11-01T14:06:47.324Z" }, + { url = "https://files.pythonhosted.org/packages/a9/c7/ca4bf3e518cb57a686b2feb4f55a1892fd9a3dd13f470fca14e00f80ea36/watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13", size = 79079, upload-time = "2024-11-01T14:06:59.472Z" }, + { url = "https://files.pythonhosted.org/packages/5c/51/d46dc9332f9a647593c947b4b88e2381c8dfc0942d15b8edc0310fa4abb1/watchdog-6.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:9041567ee8953024c83343288ccc458fd0a2d811d6a0fd68c4c22609e3490379", size = 79078, upload-time = "2024-11-01T14:07:01.431Z" }, + { url = "https://files.pythonhosted.org/packages/d4/57/04edbf5e169cd318d5f07b4766fee38e825d64b6913ca157ca32d1a42267/watchdog-6.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:82dc3e3143c7e38ec49d61af98d6558288c415eac98486a5c581726e0737c00e", size = 79076, upload-time = "2024-11-01T14:07:02.568Z" }, + { url = "https://files.pythonhosted.org/packages/ab/cc/da8422b300e13cb187d2203f20b9253e91058aaf7db65b74142013478e66/watchdog-6.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:212ac9b8bf1161dc91bd09c048048a95ca3a4c4f5e5d4a7d1b1a7d5752a7f96f", size = 79077, upload-time = "2024-11-01T14:07:03.893Z" }, + { url = "https://files.pythonhosted.org/packages/2c/3b/b8964e04ae1a025c44ba8e4291f86e97fac443bca31de8bd98d3263d2fcf/watchdog-6.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:e3df4cbb9a450c6d49318f6d14f4bbc80d763fa587ba46ec86f99f9e6876bb26", size = 79078, upload-time = "2024-11-01T14:07:05.189Z" }, + { url = "https://files.pythonhosted.org/packages/62/ae/a696eb424bedff7407801c257d4b1afda455fe40821a2be430e173660e81/watchdog-6.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:2cce7cfc2008eb51feb6aab51251fd79b85d9894e98ba847408f662b3395ca3c", size = 79077, upload-time = "2024-11-01T14:07:06.376Z" }, + { url = "https://files.pythonhosted.org/packages/b5/e8/dbf020b4d98251a9860752a094d09a65e1b436ad181faf929983f697048f/watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:20ffe5b202af80ab4266dcd3e91aae72bf2da48c0d33bdb15c66658e685e94e2", size = 79078, upload-time = "2024-11-01T14:07:07.547Z" }, + { url = "https://files.pythonhosted.org/packages/07/f6/d0e5b343768e8bcb4cda79f0f2f55051bf26177ecd5651f84c07567461cf/watchdog-6.0.0-py3-none-win32.whl", hash = "sha256:07df1fdd701c5d4c8e55ef6cf55b8f0120fe1aef7ef39a1c6fc6bc2e606d517a", size = 79065, upload-time = "2024-11-01T14:07:09.525Z" }, + { url = "https://files.pythonhosted.org/packages/db/d9/c495884c6e548fce18a8f40568ff120bc3a4b7b99813081c8ac0c936fa64/watchdog-6.0.0-py3-none-win_amd64.whl", hash = "sha256:cbafb470cf848d93b5d013e2ecb245d4aa1c8fd0504e863ccefa32445359d680", size = 79070, upload-time = "2024-11-01T14:07:10.686Z" }, + { url = "https://files.pythonhosted.org/packages/33/e8/e40370e6d74ddba47f002a32919d91310d6074130fe4e17dabcafc15cbf1/watchdog-6.0.0-py3-none-win_ia64.whl", hash = "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f", size = 79067, upload-time = "2024-11-01T14:07:11.845Z" }, +] + +[[package]] +name = "websockets" +version = "15.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/21/e6/26d09fab466b7ca9c7737474c52be4f76a40301b08362eb2dbc19dcc16c1/websockets-15.0.1.tar.gz", hash = "sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee", size = 177016, upload-time = "2025-03-05T20:03:41.606Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9f/32/18fcd5919c293a398db67443acd33fde142f283853076049824fc58e6f75/websockets-15.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:823c248b690b2fd9303ba00c4f66cd5e2d8c3ba4aa968b2779be9532a4dad431", size = 175423, upload-time = "2025-03-05T20:01:56.276Z" }, + { url = "https://files.pythonhosted.org/packages/76/70/ba1ad96b07869275ef42e2ce21f07a5b0148936688c2baf7e4a1f60d5058/websockets-15.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678999709e68425ae2593acf2e3ebcbcf2e69885a5ee78f9eb80e6e371f1bf57", size = 173082, upload-time = "2025-03-05T20:01:57.563Z" }, + { url = "https://files.pythonhosted.org/packages/86/f2/10b55821dd40eb696ce4704a87d57774696f9451108cff0d2824c97e0f97/websockets-15.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d50fd1ee42388dcfb2b3676132c78116490976f1300da28eb629272d5d93e905", size = 173330, upload-time = "2025-03-05T20:01:59.063Z" }, + { url = "https://files.pythonhosted.org/packages/a5/90/1c37ae8b8a113d3daf1065222b6af61cc44102da95388ac0018fcb7d93d9/websockets-15.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d99e5546bf73dbad5bf3547174cd6cb8ba7273062a23808ffea025ecb1cf8562", size = 182878, upload-time = "2025-03-05T20:02:00.305Z" }, + { url = "https://files.pythonhosted.org/packages/8e/8d/96e8e288b2a41dffafb78e8904ea7367ee4f891dafc2ab8d87e2124cb3d3/websockets-15.0.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:66dd88c918e3287efc22409d426c8f729688d89a0c587c88971a0faa2c2f3792", size = 181883, upload-time = "2025-03-05T20:02:03.148Z" }, + { url = "https://files.pythonhosted.org/packages/93/1f/5d6dbf551766308f6f50f8baf8e9860be6182911e8106da7a7f73785f4c4/websockets-15.0.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8dd8327c795b3e3f219760fa603dcae1dcc148172290a8ab15158cf85a953413", size = 182252, upload-time = "2025-03-05T20:02:05.29Z" }, + { url = "https://files.pythonhosted.org/packages/d4/78/2d4fed9123e6620cbf1706c0de8a1632e1a28e7774d94346d7de1bba2ca3/websockets-15.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8fdc51055e6ff4adeb88d58a11042ec9a5eae317a0a53d12c062c8a8865909e8", size = 182521, upload-time = "2025-03-05T20:02:07.458Z" }, + { url = "https://files.pythonhosted.org/packages/e7/3b/66d4c1b444dd1a9823c4a81f50231b921bab54eee2f69e70319b4e21f1ca/websockets-15.0.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:693f0192126df6c2327cce3baa7c06f2a117575e32ab2308f7f8216c29d9e2e3", size = 181958, upload-time = "2025-03-05T20:02:09.842Z" }, + { url = "https://files.pythonhosted.org/packages/08/ff/e9eed2ee5fed6f76fdd6032ca5cd38c57ca9661430bb3d5fb2872dc8703c/websockets-15.0.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:54479983bd5fb469c38f2f5c7e3a24f9a4e70594cd68cd1fa6b9340dadaff7cf", size = 181918, upload-time = "2025-03-05T20:02:11.968Z" }, + { url = "https://files.pythonhosted.org/packages/d8/75/994634a49b7e12532be6a42103597b71098fd25900f7437d6055ed39930a/websockets-15.0.1-cp311-cp311-win32.whl", hash = "sha256:16b6c1b3e57799b9d38427dda63edcbe4926352c47cf88588c0be4ace18dac85", size = 176388, upload-time = "2025-03-05T20:02:13.32Z" }, + { url = "https://files.pythonhosted.org/packages/98/93/e36c73f78400a65f5e236cd376713c34182e6663f6889cd45a4a04d8f203/websockets-15.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:27ccee0071a0e75d22cb35849b1db43f2ecd3e161041ac1ee9d2352ddf72f065", size = 176828, upload-time = "2025-03-05T20:02:14.585Z" }, + { url = "https://files.pythonhosted.org/packages/51/6b/4545a0d843594f5d0771e86463606a3988b5a09ca5123136f8a76580dd63/websockets-15.0.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:3e90baa811a5d73f3ca0bcbf32064d663ed81318ab225ee4f427ad4e26e5aff3", size = 175437, upload-time = "2025-03-05T20:02:16.706Z" }, + { url = "https://files.pythonhosted.org/packages/f4/71/809a0f5f6a06522af902e0f2ea2757f71ead94610010cf570ab5c98e99ed/websockets-15.0.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:592f1a9fe869c778694f0aa806ba0374e97648ab57936f092fd9d87f8bc03665", size = 173096, upload-time = "2025-03-05T20:02:18.832Z" }, + { url = "https://files.pythonhosted.org/packages/3d/69/1a681dd6f02180916f116894181eab8b2e25b31e484c5d0eae637ec01f7c/websockets-15.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0701bc3cfcb9164d04a14b149fd74be7347a530ad3bbf15ab2c678a2cd3dd9a2", size = 173332, upload-time = "2025-03-05T20:02:20.187Z" }, + { url = "https://files.pythonhosted.org/packages/a6/02/0073b3952f5bce97eafbb35757f8d0d54812b6174ed8dd952aa08429bcc3/websockets-15.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8b56bdcdb4505c8078cb6c7157d9811a85790f2f2b3632c7d1462ab5783d215", size = 183152, upload-time = "2025-03-05T20:02:22.286Z" }, + { url = "https://files.pythonhosted.org/packages/74/45/c205c8480eafd114b428284840da0b1be9ffd0e4f87338dc95dc6ff961a1/websockets-15.0.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0af68c55afbd5f07986df82831c7bff04846928ea8d1fd7f30052638788bc9b5", size = 182096, upload-time = "2025-03-05T20:02:24.368Z" }, + { url = "https://files.pythonhosted.org/packages/14/8f/aa61f528fba38578ec553c145857a181384c72b98156f858ca5c8e82d9d3/websockets-15.0.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64dee438fed052b52e4f98f76c5790513235efaa1ef7f3f2192c392cd7c91b65", size = 182523, upload-time = "2025-03-05T20:02:25.669Z" }, + { url = "https://files.pythonhosted.org/packages/ec/6d/0267396610add5bc0d0d3e77f546d4cd287200804fe02323797de77dbce9/websockets-15.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d5f6b181bb38171a8ad1d6aa58a67a6aa9d4b38d0f8c5f496b9e42561dfc62fe", size = 182790, upload-time = "2025-03-05T20:02:26.99Z" }, + { url = "https://files.pythonhosted.org/packages/02/05/c68c5adbf679cf610ae2f74a9b871ae84564462955d991178f95a1ddb7dd/websockets-15.0.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5d54b09eba2bada6011aea5375542a157637b91029687eb4fdb2dab11059c1b4", size = 182165, upload-time = "2025-03-05T20:02:30.291Z" }, + { url = "https://files.pythonhosted.org/packages/29/93/bb672df7b2f5faac89761cb5fa34f5cec45a4026c383a4b5761c6cea5c16/websockets-15.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3be571a8b5afed347da347bfcf27ba12b069d9d7f42cb8c7028b5e98bbb12597", size = 182160, upload-time = "2025-03-05T20:02:31.634Z" }, + { url = "https://files.pythonhosted.org/packages/ff/83/de1f7709376dc3ca9b7eeb4b9a07b4526b14876b6d372a4dc62312bebee0/websockets-15.0.1-cp312-cp312-win32.whl", hash = "sha256:c338ffa0520bdb12fbc527265235639fb76e7bc7faafbb93f6ba80d9c06578a9", size = 176395, upload-time = "2025-03-05T20:02:33.017Z" }, + { url = "https://files.pythonhosted.org/packages/7d/71/abf2ebc3bbfa40f391ce1428c7168fb20582d0ff57019b69ea20fa698043/websockets-15.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcd5cf9e305d7b8338754470cf69cf81f420459dbae8a3b40cee57417f4614a7", size = 176841, upload-time = "2025-03-05T20:02:34.498Z" }, + { url = "https://files.pythonhosted.org/packages/cb/9f/51f0cf64471a9d2b4d0fc6c534f323b664e7095640c34562f5182e5a7195/websockets-15.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ee443ef070bb3b6ed74514f5efaa37a252af57c90eb33b956d35c8e9c10a1931", size = 175440, upload-time = "2025-03-05T20:02:36.695Z" }, + { url = "https://files.pythonhosted.org/packages/8a/05/aa116ec9943c718905997412c5989f7ed671bc0188ee2ba89520e8765d7b/websockets-15.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5a939de6b7b4e18ca683218320fc67ea886038265fd1ed30173f5ce3f8e85675", size = 173098, upload-time = "2025-03-05T20:02:37.985Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0b/33cef55ff24f2d92924923c99926dcce78e7bd922d649467f0eda8368923/websockets-15.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:746ee8dba912cd6fc889a8147168991d50ed70447bf18bcda7039f7d2e3d9151", size = 173329, upload-time = "2025-03-05T20:02:39.298Z" }, + { url = "https://files.pythonhosted.org/packages/31/1d/063b25dcc01faa8fada1469bdf769de3768b7044eac9d41f734fd7b6ad6d/websockets-15.0.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:595b6c3969023ecf9041b2936ac3827e4623bfa3ccf007575f04c5a6aa318c22", size = 183111, upload-time = "2025-03-05T20:02:40.595Z" }, + { url = "https://files.pythonhosted.org/packages/93/53/9a87ee494a51bf63e4ec9241c1ccc4f7c2f45fff85d5bde2ff74fcb68b9e/websockets-15.0.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c714d2fc58b5ca3e285461a4cc0c9a66bd0e24c5da9911e30158286c9b5be7f", size = 182054, upload-time = "2025-03-05T20:02:41.926Z" }, + { url = "https://files.pythonhosted.org/packages/ff/b2/83a6ddf56cdcbad4e3d841fcc55d6ba7d19aeb89c50f24dd7e859ec0805f/websockets-15.0.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f3c1e2ab208db911594ae5b4f79addeb3501604a165019dd221c0bdcabe4db8", size = 182496, upload-time = "2025-03-05T20:02:43.304Z" }, + { url = "https://files.pythonhosted.org/packages/98/41/e7038944ed0abf34c45aa4635ba28136f06052e08fc2168520bb8b25149f/websockets-15.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:229cf1d3ca6c1804400b0a9790dc66528e08a6a1feec0d5040e8b9eb14422375", size = 182829, upload-time = "2025-03-05T20:02:48.812Z" }, + { url = "https://files.pythonhosted.org/packages/e0/17/de15b6158680c7623c6ef0db361da965ab25d813ae54fcfeae2e5b9ef910/websockets-15.0.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:756c56e867a90fb00177d530dca4b097dd753cde348448a1012ed6c5131f8b7d", size = 182217, upload-time = "2025-03-05T20:02:50.14Z" }, + { url = "https://files.pythonhosted.org/packages/33/2b/1f168cb6041853eef0362fb9554c3824367c5560cbdaad89ac40f8c2edfc/websockets-15.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:558d023b3df0bffe50a04e710bc87742de35060580a293c2a984299ed83bc4e4", size = 182195, upload-time = "2025-03-05T20:02:51.561Z" }, + { url = "https://files.pythonhosted.org/packages/86/eb/20b6cdf273913d0ad05a6a14aed4b9a85591c18a987a3d47f20fa13dcc47/websockets-15.0.1-cp313-cp313-win32.whl", hash = "sha256:ba9e56e8ceeeedb2e080147ba85ffcd5cd0711b89576b83784d8605a7df455fa", size = 176393, upload-time = "2025-03-05T20:02:53.814Z" }, + { url = "https://files.pythonhosted.org/packages/1b/6c/c65773d6cab416a64d191d6ee8a8b1c68a09970ea6909d16965d26bfed1e/websockets-15.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:e09473f095a819042ecb2ab9465aee615bd9c2028e4ef7d933600a8401c79561", size = 176837, upload-time = "2025-03-05T20:02:55.237Z" }, + { url = "https://files.pythonhosted.org/packages/fa/a8/5b41e0da817d64113292ab1f8247140aac61cbf6cfd085d6a0fa77f4984f/websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f", size = 169743, upload-time = "2025-03-05T20:03:39.41Z" }, +] + +[[package]] +name = "wrapt" +version = "2.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2d/9f/06263fcd8ad6c405f05a3905fd7a84dd3176eb5ad46e44bccc0cd16348bb/wrapt-2.2.1.tar.gz", hash = "sha256:6744f504375775d7609c82c8d3d94af1c9a6f05586984536905908ba905277b9", size = 127620, upload-time = "2026-05-22T14:49:43.056Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5f/ac/4370bde262c0e633e6c4f0e56d55095710024cf9a5cecc20c59a10de483c/wrapt-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:dd57607acc85678925940bd5df0385ff8332083a32fa8d7a43f8767f4997263c", size = 80321, upload-time = "2026-05-22T14:47:43.996Z" }, + { url = "https://files.pythonhosted.org/packages/eb/79/b8ff3a61e71babf58a8cf4c0d63358e8bad383e15bf7f35e62d2f6b6e4a4/wrapt-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1ae574d65c9fa8e86f64f6a7c2668f9fcd507b183e0e577619f504b883cb0a6c", size = 81216, upload-time = "2026-05-22T14:47:45.243Z" }, + { url = "https://files.pythonhosted.org/packages/6e/fd/c0cac1f77c9c4f6fe58a920ca632ce379bb8be928720e11e8d73de28a5e9/wrapt-2.2.1-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9a04c28c10ba7fd12842b109d2edb0678872a2fe65277ca4ff06a0d61edee245", size = 159208, upload-time = "2026-05-22T14:47:47.176Z" }, + { url = "https://files.pythonhosted.org/packages/d9/4f/744132a7b2fbefa6b81118ec5942eca5fc2e9a129f9055a0c5e46885a549/wrapt-2.2.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3e2f02472a1cbbf3884b365714a810b5947134a95ad6952b554cb8cce9d492b0", size = 160322, upload-time = "2026-05-22T14:47:49.04Z" }, + { url = "https://files.pythonhosted.org/packages/d6/95/b7cd9a22a06cf93e6482904ee6afc956248983553593fd1009296d1b3b31/wrapt-2.2.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ac2745950b2bff80219c15ebf2fa9d8427eba7e249739f97e55c9d169e47e9e1", size = 153243, upload-time = "2026-05-22T14:47:50.386Z" }, + { url = "https://files.pythonhosted.org/packages/4c/4a/eb79423192015f46f0db2872e7e04a3dde8d359b83411e8959e7c9287eaa/wrapt-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:67a97e5b6c457f0cd3cfc19ebb2d84463e60c3ece754cc831e4281a3ca29bb18", size = 159231, upload-time = "2026-05-22T14:47:51.753Z" }, + { url = "https://files.pythonhosted.org/packages/ec/dc/435015b58ce33c6fc4104158fa91ddb0e809ab03a5751fb7465d1d461456/wrapt-2.2.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:c803a3d331796255af51ba2c79ed0ac8275865b516c09e61f248d1e7aff31ce9", size = 152351, upload-time = "2026-05-22T14:47:53.214Z" }, + { url = "https://files.pythonhosted.org/packages/77/ac/5d203f98df8fd136b95c5227139aea02d34505e18baf812d0c005df61963/wrapt-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9b984d1eb252145d6302c1dbd5e87fc6d404d45531447c84eadec04bf1fcb027", size = 158347, upload-time = "2026-05-22T14:47:54.982Z" }, + { url = "https://files.pythonhosted.org/packages/52/2f/a92427dbdc74e54c1674abbed27e61b2cb5e7a94441b8c1270c70671d928/wrapt-2.2.1-cp311-cp311-win32.whl", hash = "sha256:8a983a603a18c8708f024f7f6991b2e66159219abbf894634c5056243c55f3cd", size = 77562, upload-time = "2026-05-22T14:47:56.275Z" }, + { url = "https://files.pythonhosted.org/packages/c8/56/987b9c13b3e1c1a3c6de71284076f996b79caec90e75a87c044a40c23db9/wrapt-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:9c210a6994b21aa9b29e81c8d11560e8fdab54c117e9cff37870d0a27bde1343", size = 80616, upload-time = "2026-05-22T14:47:57.854Z" }, + { url = "https://files.pythonhosted.org/packages/7e/25/d01f560888d99d94a959c85533de349ce68d71ace3f2591d6ea8f632cfed/wrapt-2.2.1-cp311-cp311-win_arm64.whl", hash = "sha256:401229e9d63ca09f9b8891ecf83798d26c11bbb445d11ed9f1836b6d4585b38a", size = 79025, upload-time = "2026-05-22T14:47:59.089Z" }, + { url = "https://files.pythonhosted.org/packages/89/0c/bfae7b9401583b6d05938cd16dedc43857d96da2f8a3d50d78cc515bf6ff/wrapt-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3ffad790d9d11d8ecf9f17c4bb671a5b4089e4d8b575c46c5129597f41f836b0", size = 81021, upload-time = "2026-05-22T14:48:00.313Z" }, + { url = "https://files.pythonhosted.org/packages/26/58/80f6a6599f933f4caecc1cb3ee88a04faf81e8b9bddbd6109c688dd63e0f/wrapt-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:628f5220c7a904d5fc78f7075c8d7871433eb6d035c94728a22fdf85f193d2a8", size = 81692, upload-time = "2026-05-22T14:48:01.49Z" }, + { url = "https://files.pythonhosted.org/packages/17/93/fb357cc7847c58a8ae790be718903afa81a28d23e642c843dc4129e8a0b2/wrapt-2.2.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:61acce4257a9883669703c525447c5b4c392edf0f987ae77ec32668440158f0e", size = 169364, upload-time = "2026-05-22T14:48:02.791Z" }, + { url = "https://files.pythonhosted.org/packages/aa/0b/76b601ee309a8bd556af0eecb184394c20b3c49aa9c8e085aa1ffacc2568/wrapt-2.2.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:727ab4244622cd6ad2390f322642090c877d2e83a608d2653a7643ae5368d926", size = 171079, upload-time = "2026-05-22T14:48:04.22Z" }, + { url = "https://files.pythonhosted.org/packages/cd/87/ee3f32d5658e3e26d3e0e457922b47a36dd3bfbdfee7f97bb3e802344a66/wrapt-2.2.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:03df9ebed4c73ab93fa8c07e3d41d818dfca1852b15731a3de59457b27814624", size = 160205, upload-time = "2026-05-22T14:48:05.553Z" }, + { url = "https://files.pythonhosted.org/packages/b1/d0/ae2fd64277a67f5d7bffcf2d05eea1e476263fb2a072baf0b0129ab85984/wrapt-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0d9ff006f420b2ec8296aa56ade43ea7da3e997e85769f0aafc5e0661aacb710", size = 168922, upload-time = "2026-05-22T14:48:07.132Z" }, + { url = "https://files.pythonhosted.org/packages/b1/f3/2d541a060c5bbafb9400bca4917e4d78bfd1f239f404782c86831a8f6b29/wrapt-2.2.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:844c858fc3bb7eacc0ba8efa904935d16aac6a4470948ad1e7e55c9f5a2a665f", size = 158388, upload-time = "2026-05-22T14:48:08.629Z" }, + { url = "https://files.pythonhosted.org/packages/1d/68/8d92c8800c57e93cb116ae9e9d6cbafc34fade5ee9f9107b6f203fb4dc35/wrapt-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:87bacdaf225117a342a20d9c03438d701c02112f6e3f351ce9b7f32354f14797", size = 167682, upload-time = "2026-05-22T14:48:10.042Z" }, + { url = "https://files.pythonhosted.org/packages/30/72/83ea3790ea352439442349388e29ff07b76e0686265f9088bbb505d1608d/wrapt-2.2.1-cp312-cp312-win32.whl", hash = "sha256:2f8c90c8afde51969487be4e1343ae049b268854877d415c2510baf833775052", size = 77857, upload-time = "2026-05-22T14:48:11.782Z" }, + { url = "https://files.pythonhosted.org/packages/ef/cb/99450668dd3502d62a54a1c8aa56e44f34cb8c1261b381cfe2e7926c3b75/wrapt-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:6ce32763ac31ce94fe9aada947e479b1975012bff166da409b4b9e4e376cf7e5", size = 80825, upload-time = "2026-05-22T14:48:13.046Z" }, + { url = "https://files.pythonhosted.org/packages/e6/3a/87512881be64e743f9ee4c66f4cbe8e884974bef2a5989af71f999653ac7/wrapt-2.2.1-cp312-cp312-win_arm64.whl", hash = "sha256:8d1b4d0e0c2119587a31f5c029abd547e0c81d93b89d394566fe1588659eb579", size = 79087, upload-time = "2026-05-22T14:48:14.323Z" }, + { url = "https://files.pythonhosted.org/packages/88/d1/a1b08f8f4fac8cbb156fa51cf64ee2c7f7f74f9875ba3cf70b3c58368694/wrapt-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d2beb1c7cab10603aecdc42f8edd6ff013f9a32e4543474e38e6b77ce9975aeb", size = 80831, upload-time = "2026-05-22T14:48:15.598Z" }, + { url = "https://files.pythonhosted.org/packages/54/ce/57890814991446a845e09b3445ce8b694f27eb0577004f2c2a36a9772ed4/wrapt-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e0cb7e4dd71f4c32e5e84843cd3c4cd65dda034314004bbe1d7f99af2426ab80", size = 81375, upload-time = "2026-05-22T14:48:17.071Z" }, + { url = "https://files.pythonhosted.org/packages/38/65/08d7a6c76ac4493bdb668205ee9c1de1bd5daca61717c3e9aa49b4c01499/wrapt-2.2.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:95821352042722cd9f1108874579a47989d0a7e12a37d87d2fc4af20fd99ab8a", size = 167417, upload-time = "2026-05-22T14:48:18.303Z" }, + { url = "https://files.pythonhosted.org/packages/62/ce/f1ccbee7a1bfe5cdc6b3da6bab4b45713d628b9294da32a39f563d648140/wrapt-2.2.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:abd621552ede77c4c69be7fac44ba911225b0c812b6ba604e5964cf98085b474", size = 166948, upload-time = "2026-05-22T14:48:19.768Z" }, + { url = "https://files.pythonhosted.org/packages/86/2a/f85d48d1cd4869aee6704028d257d740a47c1c467b457ce396b4b5b55d07/wrapt-2.2.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e3677c7146ce694874941ba82b57092cc4875445aadf29d72807351023105143", size = 158148, upload-time = "2026-05-22T14:48:21.96Z" }, + { url = "https://files.pythonhosted.org/packages/fe/5c/93939ad11d4a12358ab1aab219a2ef5efa5612e0db6b9fc65af8af1a891b/wrapt-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9a5934eaea872e17936b5f45501eba5ab0bce9a74122e172b663d7c28c459c4a", size = 165905, upload-time = "2026-05-22T14:48:23.373Z" }, + { url = "https://files.pythonhosted.org/packages/e0/22/b8c2aa89862ff58605934d7abf4b70e6a5a1c33df96656f49035ccdf1c8a/wrapt-2.2.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:f5b9daf6b629fce418e0cc3dd0436eac045188fa35deadb7a7f3941d5b8203f9", size = 156712, upload-time = "2026-05-22T14:48:24.767Z" }, + { url = "https://files.pythonhosted.org/packages/5d/78/bf00a7b02239c12bb02ddcc3c0b971bfcc36e578c5a44f1ccfef5b458545/wrapt-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f53ac9f3ef573326d009ed809beff4efcac6451931c2b8132586da4b9e53ff31", size = 166560, upload-time = "2026-05-22T14:48:26.83Z" }, + { url = "https://files.pythonhosted.org/packages/fe/93/6390ca9c5b787683cef588d04f57c8d41b9a2323b5597a65f18638c90ef2/wrapt-2.2.1-cp313-cp313-win32.whl", hash = "sha256:1ffa9cfd4bdb581539951b14ae661ff20ed0c3599b3e911a131ee0ec5ac11337", size = 77817, upload-time = "2026-05-22T14:48:28.221Z" }, + { url = "https://files.pythonhosted.org/packages/97/73/ce10f0e71c0cfaa1a65faadb8efd4852028b3bb9ba28932b8889df769d38/wrapt-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:368eac1e20fd0bb03dd3cc42bf9887154c3861b60989389ccb5fac032617d215", size = 80736, upload-time = "2026-05-22T14:48:30.139Z" }, + { url = "https://files.pythonhosted.org/packages/c7/4c/89f4a6818fafbbd840330e4fa3873073e1bfc166133a64cac7f8fde7a5e3/wrapt-2.2.1-cp313-cp313-win_arm64.whl", hash = "sha256:c754dafdf5aaf0b401b644a90a30046929a0dd1a536e0ff0ec959a59155d9c7f", size = 79099, upload-time = "2026-05-22T14:48:31.405Z" }, + { url = "https://files.pythonhosted.org/packages/bf/f2/9a8741c46f8c208ac0a45b25ba170bcb4fb72a2781d5fb97dbd7b6be73cb/wrapt-2.2.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:ed928d0fda15fc0adc8d13305c8b3c0f2fba5b0669950c9e6d019d9162a3b3e8", size = 82802, upload-time = "2026-05-22T14:48:33.307Z" }, + { url = "https://files.pythonhosted.org/packages/9c/0d/e9c855716a3705eef1416456bdf062b60620726fdc59428ff670fc3c60dc/wrapt-2.2.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:fafb4e739e43544d12cb4abd1605fd4683b6ca6a9ad682b7fd8f4d21973eafa8", size = 83329, upload-time = "2026-05-22T14:48:34.593Z" }, + { url = "https://files.pythonhosted.org/packages/3b/d6/a88f1c13112b7831adac75cea65d8310e0d696d570c8961844c90a57b865/wrapt-2.2.1-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:74d6a0c31472fe5d814917266b9f46495d7c61ed890af08b468acea92fb89a8d", size = 202937, upload-time = "2026-05-22T14:48:35.859Z" }, + { url = "https://files.pythonhosted.org/packages/42/65/e29d54aef06a4d898a5b8a25589a0b3769bde454f922fad8f6f89fbfb650/wrapt-2.2.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ab5be648d5a0b86b7438864f8df3c705a65cef35a2fd3e5561e3e203167e0f27", size = 209997, upload-time = "2026-05-22T14:48:38.153Z" }, + { url = "https://files.pythonhosted.org/packages/2a/91/e4454263516cf0e12640912fbca9a83654e424f0a6ddb79f5cd7ce14bf33/wrapt-2.2.1-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9d8f204c8e3a8bf9ece17e0a83d137fd807440977f8a5e762d59306795011440", size = 194856, upload-time = "2026-05-22T14:48:39.69Z" }, + { url = "https://files.pythonhosted.org/packages/de/d0/fe0ee202286afdf4a7f77dd29f195703145764d572aec209c5086e57d924/wrapt-2.2.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:d047f6498c973874ba08ac3f97c69a2c4b2211c8de6f4c205f75cb1c9522596e", size = 205654, upload-time = "2026-05-22T14:48:43.456Z" }, + { url = "https://files.pythonhosted.org/packages/23/b6/87d860dfc6460c246af70b1fd5c8b76df77571b42a493459423ded94fd7d/wrapt-2.2.1-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:7a4fdb9326aab4a5a477a1640e5ad786a8495901009d7e7b038371edd23a9d2b", size = 192206, upload-time = "2026-05-22T14:48:44.858Z" }, + { url = "https://files.pythonhosted.org/packages/df/46/3eea8cde077d985f239a38c0257087b8064fd9ee9b1a99e282d2c86da4ef/wrapt-2.2.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c8cc5094b08abeae52da9c73c8a32003623be691a5193df2f4e3eac3d557c394", size = 198428, upload-time = "2026-05-22T14:48:46.319Z" }, + { url = "https://files.pythonhosted.org/packages/18/dc/b927ee9c7fc67adc3a5658f246a0d275425eb840ba36e7b702e70f18bde8/wrapt-2.2.1-cp313-cp313t-win32.whl", hash = "sha256:9907a4402ab6db12b7077a0ea5d7a4d028ecb22c8eee2b53527080d347cd1562", size = 79448, upload-time = "2026-05-22T14:48:47.901Z" }, + { url = "https://files.pythonhosted.org/packages/ec/b3/fd30b473fe498c70e6b9a5f328b8d3fbaf1b8c3c481465f59724bba8eb70/wrapt-2.2.1-cp313-cp313t-win_amd64.whl", hash = "sha256:5590d63f5243251641cf543009b4c9314a79d0598fdb8a8e4cfc918494536c53", size = 83021, upload-time = "2026-05-22T14:48:49.201Z" }, + { url = "https://files.pythonhosted.org/packages/ee/f3/96c39153a8737a6e9aa85adef254ac4195bea3f2d24efc60472ccc3c9e2e/wrapt-2.2.1-cp313-cp313t-win_arm64.whl", hash = "sha256:c318a64b53d97b841d7b5e637517e50a27be64bc695128422953d4b21710954e", size = 80295, upload-time = "2026-05-22T14:48:50.479Z" }, + { url = "https://files.pythonhosted.org/packages/53/46/29ac9daf11a86c22a8c38cd9236c62928ccae83f7ceb06bd3b0467cf9d05/wrapt-2.2.1-py3-none-any.whl", hash = "sha256:3aafea2975caef8ca49400640dde02cc7426e798f24870ed01f490bc3cffd32f", size = 61000, upload-time = "2026-05-22T14:49:41.593Z" }, +] + +[[package]] +name = "yarl" +version = "1.24.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "multidict" }, + { name = "propcache" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/79/12/1e8f37460ea0f7eb59c221fdaf0ed75e7ac43e97f8093b9c6f411df50a78/yarl-1.24.2.tar.gz", hash = "sha256:9ac374123c6fd7abf64d1fec93962b0bd4ee2c19751755a762a72dd96c0378f8", size = 210798, upload-time = "2026-05-19T21:31:05.599Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c5/c5/1ce244152ff2839645e7cae92f90e7bafcb2c52bea7ff586ac714f14f5df/yarl-1.24.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:36348bebb147b83818b9d7e673ea4debc75970afc6ffdc7e3975ad05ce5a58c1", size = 128971, upload-time = "2026-05-19T21:28:20.543Z" }, + { url = "https://files.pythonhosted.org/packages/87/5a/00f36967203ed89cb3acd2c8ed526cc3fed9418eb70ce128160a911c8499/yarl-1.24.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1a97e42c8a2233f2f279ecadd9e4a037bcb5d813b78435e8eedd4db5a9e9708c", size = 91507, upload-time = "2026-05-19T21:28:22.556Z" }, + { url = "https://files.pythonhosted.org/packages/31/d0/1fb0c1cd27288f39f6974da4318c32768d72c9890984541fdf1e2e32a51d/yarl-1.24.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8d027d56f1035e339d1001ac33eceab5b2ec8e42e449787bb75e289fb9a5cd1d", size = 91343, upload-time = "2026-05-19T21:28:24.092Z" }, + { url = "https://files.pythonhosted.org/packages/03/ce/d4a646508bed2f8dec6435b40166fe9308dd191262033d3f307b2bbcaecd/yarl-1.24.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a6377060e7927187a42b7eb202090cbe2b34933a4eeaf90e3bd9e33432e5cae", size = 105704, upload-time = "2026-05-19T21:28:25.872Z" }, + { url = "https://files.pythonhosted.org/packages/4b/07/b3278e82d8bc41485bcf6d856cd0433262593de615b1d3dc43bd3f5bead4/yarl-1.24.2-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:17076578bce0049a5ce57d14ad1bded391b68a3b213e9b81b0097b090244999a", size = 97281, upload-time = "2026-05-19T21:28:27.352Z" }, + { url = "https://files.pythonhosted.org/packages/17/5b/4cee6e7c92e487bebe7afc797da0aa54a248ab4e776a68fe369ec29665a5/yarl-1.24.2-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:50713f1d4d6be6375bb178bb43d140ee1acb8abe589cd723320b7925a275be1e", size = 114020, upload-time = "2026-05-19T21:28:29.458Z" }, + { url = "https://files.pythonhosted.org/packages/5c/82/111076571545a7d4f9cca3fbd5c6f40615af58642be09f12328f48022468/yarl-1.24.2-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:34263e2fa8fb5bb63a0d97706cda38edbad62fddb58c7f12d6acbc092812aa50", size = 111450, upload-time = "2026-05-19T21:28:31.262Z" }, + { url = "https://files.pythonhosted.org/packages/b6/ec/08f671f69a444d704aeecebf92af659b67b97a869942411d0a578b08c334/yarl-1.24.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:49016d82f032b1bd1e10b01078a7d29ae71bf468eeae0ea22df8bab691e60003", size = 106384, upload-time = "2026-05-19T21:28:32.856Z" }, + { url = "https://files.pythonhosted.org/packages/e5/86/ce41e7a7a199340b2330d52b60f25c4074b6636dd0e60b1a80d31a9db042/yarl-1.24.2-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3f6d2c216318f8f32038ca3f72501ba08536f0fd18a36e858836b121b2deed9f", size = 106153, upload-time = "2026-05-19T21:28:35.222Z" }, + { url = "https://files.pythonhosted.org/packages/c4/5d/31be8a729531ab3e55ac3e7e5c800be8c89ea98947f418b2f6ea259fb6ee/yarl-1.24.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:08d3a33218e0c64393e7610284e770409a9c31c429b078bcb24096ed0a783b8f", size = 105322, upload-time = "2026-05-19T21:28:36.642Z" }, + { url = "https://files.pythonhosted.org/packages/47/9b/b57afb22b386ae87ac9940f09878b98d8c333f89113e6fc96fcf4ca9eb64/yarl-1.24.2-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:5d699376c4ca3cba49bbfae3a05b5b70ded572937171ce1e0b8d87118e2ba294", size = 99057, upload-time = "2026-05-19T21:28:38.386Z" }, + { url = "https://files.pythonhosted.org/packages/a3/4f/06348c27c8389256c313e8a57d796808fc0264c915dd5e7cfd3c0e314dc7/yarl-1.24.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:a1cab588b4fa14bea2e55ebea27478adfb05372f47573738e1acc4a36c0b05d2", size = 113502, upload-time = "2026-05-19T21:28:40.091Z" }, + { url = "https://files.pythonhosted.org/packages/5f/1c/284f307b298e4a17b7943b07d9d7ecc4151537f8d137ba51f3bb6c31ca20/yarl-1.24.2-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:ec87ccc31bd21db7ad009d8572c127c1000f268517618a4cc09adba3c2a7f21c", size = 105253, upload-time = "2026-05-19T21:28:41.987Z" }, + { url = "https://files.pythonhosted.org/packages/c8/bf/0de123bec8619e45c80cbded9085f61b5b4a9eddb8abe6d25d28ee1ec866/yarl-1.24.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:d1dd47a22843b212baa8d74f37796815d43bd046b42a0f41e9da433386c3136b", size = 111345, upload-time = "2026-05-19T21:28:43.93Z" }, + { url = "https://files.pythonhosted.org/packages/90/af/0248eb065e51129d2a9b2436cd1b5c772c19a6b04e5b6a186955671e3319/yarl-1.24.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7b54b9c67c2b06bd7b9a77253d242124b9c95d2c02def5a1144001ee547dd9d5", size = 106558, upload-time = "2026-05-19T21:28:45.806Z" }, + { url = "https://files.pythonhosted.org/packages/21/3c/f960d7a65ef97d8ba9b424fb5128796a4bc710fc6df2ddbbd7dfdc3bbd20/yarl-1.24.2-cp311-cp311-win_amd64.whl", hash = "sha256:f8fdbcff8b2c7c9284e60c196f693588598ddcee31e11c18e14949ce44519d45", size = 92808, upload-time = "2026-05-19T21:28:48.465Z" }, + { url = "https://files.pythonhosted.org/packages/03/1a/49fb03750e4de4d2284cd5b885a383133c34eef45bd59631b2bb8b7e81e8/yarl-1.24.2-cp311-cp311-win_arm64.whl", hash = "sha256:b32c37a7a337e90822c45797bf3d79d60875cfcccd3ecc80e9f453d87026c122", size = 87610, upload-time = "2026-05-19T21:28:50.07Z" }, + { url = "https://files.pythonhosted.org/packages/f0/da/866bcb01076ba49d2b42b309867bed3826421f1c479655eb7a607b44f20b/yarl-1.24.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:b975866c184564c827e0877380f0dae57dcca7e52782128381b72feff6dfceb8", size = 129957, upload-time = "2026-05-19T21:28:51.695Z" }, + { url = "https://files.pythonhosted.org/packages/bf/1d/fcefb70922ea2268a8971d8e5874d9a8218644200fb8465f1dcad55e6851/yarl-1.24.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3b075301a2836a0e297b1b658cb6d6135df535d62efefdd60366bd589c2c82f2", size = 92164, upload-time = "2026-05-19T21:28:53.242Z" }, + { url = "https://files.pythonhosted.org/packages/29/b6/170e2b8d4e3bc30e6bfdcca53556537f5bf595e938632dfcb059311f3ff6/yarl-1.24.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8ae44649b00947634ab0dab2a374a638f52923a6e67083f2c156cd5cbd1a881d", size = 91688, upload-time = "2026-05-19T21:28:54.865Z" }, + { url = "https://files.pythonhosted.org/packages/fe/a5/c9f655d5553ea0b99fdac9d6a99ad3f9b3e73b8e5758bb46f58c9831f74c/yarl-1.24.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:507cc19f0b45454e2d6dcd62ff7d062b9f77a2812404e62dbdaec05b50faa035", size = 102902, upload-time = "2026-05-19T21:28:56.963Z" }, + { url = "https://files.pythonhosted.org/packages/5d/bc/6b9664d815d79af4ee553337f9d606c56bbf269186ada9172de45f1b5f60/yarl-1.24.2-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c4c17bad5a530912d2111825d3f05e89bab2dd376aaa8cbc77e449e6db63e576", size = 97931, upload-time = "2026-05-19T21:28:58.56Z" }, + { url = "https://files.pythonhosted.org/packages/98/ec/32ba48acae30fecd60928f5791188b80a9d6ee3840507ffda29fecd37b71/yarl-1.24.2-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f5f0cbb112838a4a293985b6ed73948a547dadcc1ba6d2089938e7abdedceef8", size = 111030, upload-time = "2026-05-19T21:29:00.148Z" }, + { url = "https://files.pythonhosted.org/packages/82/5a/6f4cd081e5f4934d2ae3a8ef4abe3afacc010d26f0035ee91b35cd7d7c37/yarl-1.24.2-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5ec8356b8a6afcf81fc7aeeef13b1ff7a49dec00f313394bbb9e83830d32ccd7", size = 110392, upload-time = "2026-05-19T21:29:02.155Z" }, + { url = "https://files.pythonhosted.org/packages/7a/da/323a01c349bd5fb01bb6652e314d9bb218cee630a736bdb810ad50e4013f/yarl-1.24.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7e7ebcdef69dec6c6451e616f32b622a6d4a2e92b445c992f7c8e5274a6bbc4c", size = 105612, upload-time = "2026-05-19T21:29:04.247Z" }, + { url = "https://files.pythonhosted.org/packages/7c/80/264ab684f181e1a876389374519ff05d10248725535ae2ac4e8ac4e563d6/yarl-1.24.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:47a55d6cf6db2f401017a9e96e5288844e5051911fb4e0c8311a3980f5e59a7d", size = 104487, upload-time = "2026-05-19T21:29:06.491Z" }, + { url = "https://files.pythonhosted.org/packages/41/07/efabe5df87e96d7ad5959760b888344be48cd6884db127b407c6b5503adc/yarl-1.24.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3065657c80a2321225e804048597ad55658a7e76b32d6f5ee4074d04c50401db", size = 102333, upload-time = "2026-05-19T21:29:08.267Z" }, + { url = "https://files.pythonhosted.org/packages/44/0c/bcf7c42603e1009295f586d8890f2ba032c8b53310e815adf0a202c73d9f/yarl-1.24.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:cb84b80d88e19ede158619b80813968713d8d008b0e2497a576e6a0557d50712", size = 99025, upload-time = "2026-05-19T21:29:10.682Z" }, + { url = "https://files.pythonhosted.org/packages/4f/82/84482ab1a57a0f21a08afe6a7004c61d741f8f2ecc3b05c321577c612164/yarl-1.24.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:990de4f680b1c217e77ff0d6aa0029f9eb79889c11fb3e9a3942c7eba29c1996", size = 110507, upload-time = "2026-05-19T21:29:12.954Z" }, + { url = "https://files.pythonhosted.org/packages/c4/8d/a546ba1dfe1b0f290e05fef145cd07614c0f15df1a707195e512d1e39d1d/yarl-1.24.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:abb8ec0323b80161e3802da3150ef660b41d0e9be2048b76a363d93eee992c2b", size = 103719, upload-time = "2026-05-19T21:29:14.893Z" }, + { url = "https://files.pythonhosted.org/packages/1a/b6/267f2a09213138473adfce6b8a6e17791d7fee70bd4d9003218e4dec58b0/yarl-1.24.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:e7977781f83638a4c73e0f88425563d70173e0dfd90ac006a45c65036293ee3c", size = 110438, upload-time = "2026-05-19T21:29:16.485Z" }, + { url = "https://files.pythonhosted.org/packages/48/2d/1c8d89c7c5f9cad9fb2902445d94e2ab1d7aa35de029afbb8ae95c42d00f/yarl-1.24.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e30dd55825dc554ec5b66a94953b8eda8745926514c5089dfcacecb9c99b5bd1", size = 105719, upload-time = "2026-05-19T21:29:18.367Z" }, + { url = "https://files.pythonhosted.org/packages/a7/25/722e3b93bd687009afb2d59a35e13d30ddd8f80571445bb0c4e4ce26ec66/yarl-1.24.2-cp312-cp312-win_amd64.whl", hash = "sha256:7dafe10c12ddd4d120d528c4b5599c953bd7b12845347d507b95451195bb6cad", size = 92901, upload-time = "2026-05-19T21:29:20.014Z" }, + { url = "https://files.pythonhosted.org/packages/39/47/4486ccfb674c04854a1ef8aa77868b6a6f765feaf69633409d7ca4f02cb8/yarl-1.24.2-cp312-cp312-win_arm64.whl", hash = "sha256:044a09d8401fcf8681977faef6d286b8ade1e2d2e9dceda175d1cfa5ca496f30", size = 87229, upload-time = "2026-05-19T21:29:22.1Z" }, + { url = "https://files.pythonhosted.org/packages/82/62/fcf0ce677f17e5c471c06311dd25964be38a4c586993632910d2e75278bc/yarl-1.24.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:491ac9141decf49ee8030199e1ee251cdff0e131f25678817ff6aa5f837a3536", size = 128978, upload-time = "2026-05-19T21:29:23.83Z" }, + { url = "https://files.pythonhosted.org/packages/d3/58/8e63299bb71ed61a834121d9d3fe6c9fcf2a6a5d09754ff4f20f2d20baf5/yarl-1.24.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e89418f65eda18f99030386305bd44d7d504e328a7945db1ead514fbe03a0607", size = 91733, upload-time = "2026-05-19T21:29:25.375Z" }, + { url = "https://files.pythonhosted.org/packages/c1/24/16748d5dab6daec8b0ed81ccec639a1cded0f18dcc62a4f696b4fe366c37/yarl-1.24.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cdfcce633b4a4bb8281913c57fcafd4b5933fbc19111a5e3930bbd299d6102f1", size = 91113, upload-time = "2026-05-19T21:29:26.928Z" }, + { url = "https://files.pythonhosted.org/packages/1b/66/b63fff7b71211e866624b21432d5943cbb633eb0c2872d9ee3070648f22c/yarl-1.24.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:863297ddede92ee49024e9a9b11ecb59f310ca85b60d8537f56bed9bbb5b1986", size = 103899, upload-time = "2026-05-19T21:29:28.842Z" }, + { url = "https://files.pythonhosted.org/packages/9d/ac/ba1974b8533909636f7733fe86cf677e3619527c3c2fa913e0ea89c48757/yarl-1.24.2-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:374423f70754a2c96942ede36a29d37dc6b0cb8f92f8d009ddf3ed78d3da5488", size = 97862, upload-time = "2026-05-19T21:29:31.086Z" }, + { url = "https://files.pythonhosted.org/packages/1b/a5/123ac993b5c2ba6f554a140305620cb8f150fa543711bbc49be3ec0a65a4/yarl-1.24.2-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:33a29b5d00ccbf3219bb3e351d7875739c19481e030779f48cc46a7a71681a9b", size = 111060, upload-time = "2026-05-19T21:29:32.657Z" }, + { url = "https://files.pythonhosted.org/packages/23/37/c472d3af3509688392134a88a825276770a187f1daa4de3f6dc0a327a751/yarl-1.24.2-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a9532c57211730c515341af11fef6e9b61d157487272a096d0c04da445642592", size = 110613, upload-time = "2026-05-19T21:29:34.379Z" }, + { url = "https://files.pythonhosted.org/packages/df/88/09c28dad91e662ccfaa1b78f1c57badde74fc9d0b23e74aef644750ecd73/yarl-1.24.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:91e72cf093fd833483a97ee648e0c053c7c629f51ff4a0e7edd84f806b0c5617", size = 107012, upload-time = "2026-05-19T21:29:36.216Z" }, + { url = "https://files.pythonhosted.org/packages/07/ab/9d4f69d571a94f4d112fa7e2e007200f5a54d319f58c82ac7b7baa61f5c6/yarl-1.24.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b3177bc0a768ef3bacceb4f272632990b7bea352f1b2f1eee9d6d6ff16516f92", size = 105887, upload-time = "2026-05-19T21:29:38.746Z" }, + { url = "https://files.pythonhosted.org/packages/8e/9a/000b2b66c0d772a499fc531d21dab92dfeb73b640a12eed6ba89f49bb2d0/yarl-1.24.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e196952aacaf3b232e265ff02980b64d483dc0972bd49bcb061171ff22ac203a", size = 103620, upload-time = "2026-05-19T21:29:40.368Z" }, + { url = "https://files.pythonhosted.org/packages/41/7c/7c1050f73450fbdaa3f0c72017059f00ce5e13366692f3dba25275a1083d/yarl-1.24.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:204e7a61ce99919c0de1bf904ab5d7aa188a129ea8f690a8f76cfb6e2844dc44", size = 100599, upload-time = "2026-05-19T21:29:42.66Z" }, + { url = "https://files.pythonhosted.org/packages/ec/b1/29e5756b3926705f5f6089bd5b9f50a56eaac550da6e260bf713ead44d04/yarl-1.24.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4b156914620f0b9d78dc1adb3751141daee561cfec796088abb89ed49d220f1a", size = 110604, upload-time = "2026-05-19T21:29:44.632Z" }, + { url = "https://files.pythonhosted.org/packages/a3/4b/8415bc96e9b150cde942fbac9a8182985e58f40ce5c54c34ed015407d3ee/yarl-1.24.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:8372a2b976cf70654b2be6619ab6068acabb35f724c0fda7b277fbf53d66a5cf", size = 105161, upload-time = "2026-05-19T21:29:46.755Z" }, + { url = "https://files.pythonhosted.org/packages/8b/d4/cde059abfa229553b7298a2eadde2752e723d50aeedaef86ce59da2718ee/yarl-1.24.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:f9a1e9b622ca284143aab5d885848686dcd85453bb1ca9abcdb7503e64dc0056", size = 110619, upload-time = "2026-05-19T21:29:48.972Z" }, + { url = "https://files.pythonhosted.org/packages/e7/2c/d6a6c9a61549f7b6c7e6dc6937d195bcf069582b47b7200dcd0e7b256acf/yarl-1.24.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:810e19b685c8c3c5862f6a38160a1f4e4c0916c9390024ec347b6157a45a0992", size = 107362, upload-time = "2026-05-19T21:29:51Z" }, + { url = "https://files.pythonhosted.org/packages/92/dd/3ae5fe417e9d1c353a548553326eb9935e76b6b727161563b424cc296df3/yarl-1.24.2-cp313-cp313-win_amd64.whl", hash = "sha256:7d37fb7c38f2b6edab0f845c4f85148d4c44204f52bc127021bd2bc9fdbf1656", size = 92667, upload-time = "2026-05-19T21:29:52.743Z" }, + { url = "https://files.pythonhosted.org/packages/10/cc/a7beb239f78f27fca1b053c8e8595e4179c02e62249b4687ec218c370c50/yarl-1.24.2-cp313-cp313-win_arm64.whl", hash = "sha256:1e831894be7c2954240e49791fa4b50c05a0dc881de2552cfe3ffd8631c7f461", size = 87069, upload-time = "2026-05-19T21:29:54.442Z" }, + { url = "https://files.pythonhosted.org/packages/fd/4d/4b880086bd0d3e034d25647be1d830afc3e3f610e98c4ab3490af6b1b6d5/yarl-1.24.2-py3-none-any.whl", hash = "sha256:2783d9226db8797636cd6896e4de81feed252d1db72265686c9558d97a4d94b9", size = 53576, upload-time = "2026-05-19T21:31:03.909Z" }, +] + +[[package]] +name = "zipp" +version = "4.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b9/d8/eab98a517c14134c0b2eb4e2387bc5f457334293ec5d2dd3857ec2966802/zipp-4.1.0.tar.gz", hash = "sha256:4cb57381f544315db7688e976e922a2b18cdb513d21cc194eb42232ba2a3e602", size = 26214, upload-time = "2026-05-18T20:08:57.967Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3a/13/547360d81e6d88d58492968ffda9f9542854f11310ee556fef14260cc886/zipp-4.1.0-py3-none-any.whl", hash = "sha256:25ad4e16390cd314347dd8f1de67a2ac538ae658ed4ab9db16029c07c188e97f", size = 10238, upload-time = "2026-05-18T20:08:57.045Z" }, +] diff --git a/agents/adk/contract-compliance-pipeline/sample-contracts/high-risk-liability-contract.pdf b/agents/adk/contract-compliance-pipeline/sample-contracts/high-risk-liability-contract.pdf new file mode 100644 index 0000000..7869fed --- /dev/null +++ b/agents/adk/contract-compliance-pipeline/sample-contracts/high-risk-liability-contract.pdf @@ -0,0 +1,19 @@ +ENTERPRISE CLOUD CONSULTING AGREEMENT + +PARTIES: +This Agreement is entered into by and between APEX DATA SYSTEMS ("Contractor") and GFD PLATFORM SYSTEMS ("Client"). + +VALUE & SERVICES: +Client agrees to pay Contractor a total fee of $450,000.00 for enterprise data engineering services. + +TERM & COMMENCEMENT: +This Agreement shall commence on June 1, 2026, and terminate on June 1, 2027 (1 year duration). + +LIMITATION OF LIABILITY: +LIABILITY LIMITS ARE EXPLICITLY WAIVED. CONTRACTOR LIABILITY SHALL BE UNLIMITED UNDER ALL CIRCUMSTANCES. GFD PLATFORM SYSTEMS ASSUMES FULL COMPENSATORY RESPONSIBILITY FOR ALL THIRD-PARTY CLAIMS ARISEN OUT OF PERFORMANCE ACTIONS. + +INSURANCE: +Contractor shall maintain standard professional liability insurance with minimum coverage of $1,500,000.00. + +TERMINATION: +Termination requires 30 days notice. diff --git a/agents/adk/contract-compliance-pipeline/sample-contracts/non-compliant-contract.pdf b/agents/adk/contract-compliance-pipeline/sample-contracts/non-compliant-contract.pdf new file mode 100644 index 0000000..8e5e564 --- /dev/null +++ b/agents/adk/contract-compliance-pipeline/sample-contracts/non-compliant-contract.pdf @@ -0,0 +1,19 @@ +LEGACY SYSTEMS INTEGRATION SERVICES CHARTER + +PARTIES: +This Agreement is entered into by and between LEGACY NETWORKS CORP ("Contractor") and GFD PLATFORM SYSTEMS ("Client"). + +VALUE & SERVICES: +Client agrees to pay Contractor a sum of $850,000.00 (Eight Hundred and Fifty Thousand Dollars) for legacy migration activities. + +TERM & COMMENCEMENT: +This Charter shall commence on June 1, 2026, and extend until June 1, 2032 (a total term of exactly 6 years). + +LIMITATION OF LIABILITY: +THE PARTIES EXPRESSLY AGREE THAT ALL CONTRACTOR RESPONSIBILITY LIMITS ARE INAPPLICABLE. CONTRACTOR'S TOTAL LIABILITY UNDER THIS CONTRACT SHALL BE ENTIRELY UNLIMITED. + +INSURANCE: +Contractor shall maintain general liability protection limits of $500,000.00. + +TERMINATION: +THIS AGREEMENT SHALL AUTOMATICALLY RENEW FOREVER WITH NO OPTION FOR TERMINATION OR WRITTEN EXIT NOTICES, EXCEPT ON TOTAL LIQUIDATION OF EITHER PARTY. diff --git a/agents/adk/contract-compliance-pipeline/sample-contracts/standard-vendor-agreement.pdf b/agents/adk/contract-compliance-pipeline/sample-contracts/standard-vendor-agreement.pdf new file mode 100644 index 0000000..0a324ec --- /dev/null +++ b/agents/adk/contract-compliance-pipeline/sample-contracts/standard-vendor-agreement.pdf @@ -0,0 +1,19 @@ +SOFTWARE SERVICES VENDOR AGREEMENT + +PARTIES: +This Agreement is entered into by and between ACME CLOUD SOLUTIONS ("Contractor") and GFD PLATFORM SYSTEMS ("Client"). + +VALUE & SERVICES: +Client agrees to pay Contractor a total consideration of $250,000.00 (Two Hundred and Fifty Thousand Dollars) for the performance of the cloud architectural services. + +TERM & COMMENCEMENT: +This Agreement shall commence on June 1, 2026, and shall continue in full force and effect until June 1, 2028 (a duration of exactly 2 years), unless terminated earlier in accordance with the terms herein. + +LIMITATION OF LIABILITY: +Except for breach of confidentiality obligations, each party's maximum aggregate liability to the other party under this Agreement shall be strictly capped and limited to $1,000,000.00 (One Million Dollars). Under no circumstances shall either party be liable for indirect or consequential damages. + +INSURANCE: +Contractor shall maintain Commercial General Liability insurance during the term of this agreement with a minimum coverage limit of $2,000,000.00. + +TERMINATION: +Either party may terminate this agreement upon 30 days prior written notice. diff --git a/agents/adk/new-hire-onboarding/README.md b/agents/adk/new-hire-onboarding/README.md new file mode 100644 index 0000000..adeee6e --- /dev/null +++ b/agents/adk/new-hire-onboarding/README.md @@ -0,0 +1,250 @@ +# 👔 Long Running AI Agent Team for New Hire Onboarding + +This **HR New Hire Onboarding Coordinator Agent** demonstrates key principles for architecting agents to handle time-agnostic, multi-day workflows. The project contains a ReAct agent powered by [Gemini Flash-Lite](app/agent.py#L67) and built on top of the Google Agent Development Kit (ADK). The agent guides new hires through a structured onboarding state machine and includes a polished live demo UI for showing pause/resume behavior outside the raw ADK web console. + +![Live onboarding app overview](assets/live-onboarding-overview.png) + +--- + +## 📖 Overview + +Most chatbot tutorials rely on dumping massive conversation history JSON blobs into a vector database to "remember" past turns. However, over multi-day workflows, this unstructured approach introduces severe prompt context pollution, high token costs, and reasoning hallucinations. + +To build reliable background agents, we design a **durable, lightweight agent memory schema**. By grounding the [Onboarding Coordinator Agent](app/agent.py#L65) in a strict enum-based state machine, we guarantee the agent maintains its logical reasoning chain across weeks of delay, without relying on raw chat logs. The coordinator coordinates human-in-the-loop task progression and automated system provisioning, ensuring that every onboarding step is completed in order and preventing checkpoints from being bypassed. + +--- + +## đŸ–„ïž Live Onboarding Demo + +The repository now includes a production-style React demo served by FastAPI at: + +```bash +http://127.0.0.1:8000/live-onboarding/ +``` + +The demo is intentionally built as a two-sided onboarding cockpit: + +- **HR command center:** shows backend case state, ADK activity, state-machine progress, local artifacts, and event history. +- **Employee portal:** lets the employee inspect the generated packet, sign it, confirm laptop delivery, and view the resulting artifacts. + +The important demo behavior is honest: + +- The UI does **not** optimistically advance the workflow. +- Clicking **Sign packet** waits for the backend ADK resume turn to complete before the signed packet/state appears. +- Clicking **Confirm laptop delivered** waits for the hardware ADK resume turn to complete. +- After hardware completion, the employee portal shows the **Hardware delivery receipt** first, then automatically transitions to the **Day One schedule**. +- Local HTML artifacts are real files generated by the FastAPI backend and served through `/api/live-onboarding/cases/{case_id}/artifacts/{artifact_id}`. + +This keeps the app demo-friendly while still proving the long-running ADK pattern: the agent pauses at external events, resumes from webhook-like actions, and only moves forward when the backend state has actually changed. + +### Demo Flow + +1. Start a fresh case. +2. Review the unsigned onboarding packet. +3. Click **Sign packet**. +4. Wait for the ADK signature resume to finish. +5. Review the signed packet. +6. Click **Confirm laptop delivered**. +7. Wait for the ADK hardware resume to finish. +8. See the hardware receipt, then the Day One schedule. + +### Run the Live Demo Locally + +Authenticate with Google Cloud Application Default Credentials first: + +```bash +gcloud auth application-default login +gcloud config set project +``` + +Install and run the FastAPI app: + +```bash +uv sync +uv run uvicorn app.fast_api_app:app --host 127.0.0.1 --port 8000 +``` + +Then open: + +```bash +http://127.0.0.1:8000/live-onboarding/ +``` + +If you change the React frontend, rebuild the static assets: + +```bash +cd frontend/live-onboarding +npm install +npm run build +``` + +The build writes directly into `app/static/live-onboarding`, which is what FastAPI serves. + +--- + +## 🔄 Core Design Patterns: Architecting for Time + +To transition from building stateless chatbots to reliable background agents, this repository demonstrates three architectural paradigm shifts: + +| Stateless Chatbot Pattern | Durable Background Agent Pattern (This Repo) | Why It Matters | +| :--- | :--- | :--- | +| **Stateless memory** (dumps raw JSON logs or chat histories into vector DBs). | **Durable memory schema** (grounded enums serialized to SQLite/Cloud SQL). | Eliminates prompt context pollution, token bloat, and reasoning hallucinations over multi-week wait times. | +| **Active polling / blocked threads** (keeps loops running or actively polls APIs). | **Event-driven dormancy gates** (dormant scale-to-zero wait state resumed via webhooks). | Conserves compute resources; agent sleep state persists durably at rest until external events trigger wake-up. | +| **Monolithic single-agent** (all tools stuffed into one system instruction). | **Multi-agent delegation** (HR coordinator delegates IT setups to specialized subagents). | Decouples complex workflows, keeps prompts targeted, and preserves the logical reasoning chain. | + +--- + +### 🔄 Onboarding State Machine & Idle Time Pause Gates + +This coordinator does not run in a single thread or block execution. It employs **durable dormancy gates** during long periods of **"Idle Time"**—specifically when waiting for signatures or shipping delivery carrier callbacks. + +```mermaid +stateDiagram-v2 + [*] --> START : Initialize details + START --> WELCOME_SENT : send_welcome_packet() + + state "WELCOME_SENT" as WELCOME_SENT + note right of WELCOME_SENT + ⏳ IDLE TIME PAUSE (Days) + Waiting for local signature webhook callback. + Container scales down; state persisted. + end note + + WELCOME_SENT --> DOCUMENTS_SIGNED : Webhook Resume Callback + DOCUMENTS_SIGNED --> IT_PROVISIONED : provision_software_accounts() + + state "IT_PROVISIONED" as IT_PROVISIONED + note right of IT_PROVISIONED + ⏳ IDLE TIME PAUSE (Days) + Waiting for carrier package delivery webhook. + Container scales down; state persisted. + end note + + IT_PROVISIONED --> HARDWARE_DELIVERED : Webhook Resume Callback + HARDWARE_DELIVERED --> COMPLETED : send_day_one_schedule() + COMPLETED --> [*] : Onboarding Completed +``` + +--- + +## đŸ—ïž Project Structure + +A clear understanding of the workspace layout and files: + +``` +new-hire-onboarding/ +├── app/ # Core Agent Application +│ ├── agent.py # Main agent logic, model config, & system prompt +│ ├── tools.py # Automated tools (emails, IT provisioning, hardware tracking) +│ ├── state_schema.py # OnboardingStep enum definition +│ ├── resume_handler.py # Callback webhooks to transition states (signatures, delivery) +│ ├── live_onboarding.py # Live demo case state, local artifact generation, and API helpers +│ ├── fast_api_app.py # FastAPI server exposing agent and custom endpoints +│ ├── agent_runtime_app.py # Dedicated Reasoning Engine App wrapper for Agent Runtime +│ ├── static/live-onboarding/ # Built React app served by FastAPI +│ └── app_utils/ # Shared utilities (telemetry, feedback typing) +├── frontend/live-onboarding/ # React + Vite source for the live onboarding UI +├── tests/ # Test Suites +│ ├── unit/ # Basic unit and logic tests +│ ├── integration/ # Local stream and E2E FastAPI server tests +│ └── eval/ # Performance evaluations using Golden Sets +├── pyproject.toml # Poetry/uv project description & dependencies +└── GEMINI.md # AI-assisted development instruction playbook +``` + +### Key Symbols and Modules +* **State Definitions:** Defined in [OnboardingStep](app/state_schema.py#L15) +* **Agent Instantiation:** Created in [root_agent](app/agent.py#L65) using the `gemini-3.1-flash-lite` model. +* **FastAPI App Setup:** Configured in [fast_api_app.py](app/fast_api_app.py) +* **Webhook Resume Flow:** Implemented in [OnboardingResumeHandler](app/resume_handler.py#L24) +* **Live Demo State and Artifacts:** Implemented in [live_onboarding.py](app/live_onboarding.py) + +--- + +## 🚀 Features and Steps + +1. **`START`** + * **Action:** Collects new hire's full name, personal email, and official start date. + * **Tool:** Calls [send_welcome_packet](app/tools.py#L18). +2. **`WELCOME_SENT`** + * **Action:** Pauses for signature. Webhook callback triggers document signing verification. + * **Transition Handler:** [receive_signed_documents_callback](app/resume_handler.py#L29). +3. **`DOCUMENTS_SIGNED`** + * **Action:** Collects desired corporate username prefix. + * **Tool:** Calls [provision_software_accounts](app/tools.py#L44) to set up corporate email and Slack. +4. **`IT_PROVISIONED`** + * **Action:** Collects a laptop hardware tracking ID (e.g., `HW-12345`). + * **Tool/Callback:** Calls [check_hardware_delivery](app/tools.py#L72) or invokes [receive_hardware_delivery_callback](app/resume_handler.py#L50). +5. **`HARDWARE_DELIVERED`** + * **Action:** Finishes hardware step and generates personalized start itinerary. + * **Tool:** Calls [send_day_one_schedule](app/tools.py#L102). +6. **`COMPLETED`** + * **Action:** Onboarding is finished! Prints the completed agenda and congratulates the new employee. + +--- + +## ⚙ Prerequisites + +Ensure you have the following installed: +- **uv**: Python package manager — [Install UV](https://docs.astral.sh/uv/getting-started/installation/) +- **agents-cli**(https://google.github.io/agents-cli/): The official command-line interface for the **Gemini Enterprise Agent Platform**. It manages project scaffolding, local interactive playgrounds (`agents-cli playground`), golden evaluations, and reasoning engine deployments. Install it globally using: + ```bash + uv tool install google-agents-cli + ``` +- **Google Cloud SDK**: Authenticated to Google Cloud — [Install Gcloud](https://cloud.google.com/sdk/docs/install) + +--- + +## đŸ› ïž Local Development Commands + +| Command | Purpose | +| :--- | :--- | +| `agents-cli install` | Installs the project dependencies via `uv` | +| `agents-cli playground` | Runs the agent in an interactive local chat sandbox | +| `uv run uvicorn app.fast_api_app:app --host 127.0.0.1 --port 8000` | Runs the FastAPI app and live onboarding UI | +| `cd frontend/live-onboarding && npm run build` | Builds the React UI into `app/static/live-onboarding` | +| `agents-cli lint` | Validates code structure and checks styling formatting | +| `uv run pytest tests/unit` | Runs deterministic live onboarding state and artifact tests | +| `uv run pytest tests/integration` | Runs streaming integration tests & E2E fastapi server validations | +| `.venv/bin/adk eval ./app ` | Runs direct local python evaluation runner bypassing system package registries | + +--- + +## 📊 Evaluation & Validation Loop + +The agent's state machine transitions are validated using formal golden evaluation sets: + +- **Evaluation Set Config:** [eval_config.json](tests/eval/eval_config.json) +- **Golden Standard Cases:** [onboarding_eval.json](tests/eval/evalsets/onboarding_eval.json) +- **Golden Idle-Time Delay Cases:** [idle_time_delay_eval.json](tests/eval/evalsets/idle_time_delay_eval.json) + +Run evaluation metrics locally (using the direct virtualenv runner to avoid credential/conflict overrides): +```bash +.venv/bin/adk eval ./app tests/eval/evalsets/idle_time_delay_eval.json --config_file_path tests/eval/eval_config.json +``` + +--- + +## ☁ Infrastructure & Deployment + +1. **Setup Google Cloud Project Config:** + ```bash + gcloud config set project + ``` +2. **Deploy to Agent Runtime:** + This project's deployment target is scaffolded and pre-configured for **Agent Runtime** (Agent Engines): + ```bash + agents-cli deploy + ``` + Agent Runtime automatically hosts the server, manages persistent sessions out-of-the-box, and natively integrates trace spans with **Cloud Trace** for real-time ambient monitoring. +3. **Enhance Project Infrastructure:** + To add CI/CD runners (GitHub Actions/Cloud Build) or adapt configurations: + ```bash + agents-cli scaffold enhance + ``` + +--- + +## 📡 Observability + +This agent includes built-in telemetry pre-configured in [telemetry.py](app/app_utils/telemetry.py). It leverages OpenTelemetry to export trace spans, API logs, and model execution metadata directly to **Cloud Trace**, **Cloud Logging**, and **BigQuery**. diff --git a/agents/adk/new-hire-onboarding/TUTORIAL.md b/agents/adk/new-hire-onboarding/TUTORIAL.md new file mode 100644 index 0000000..ac5c5f6 --- /dev/null +++ b/agents/adk/new-hire-onboarding/TUTORIAL.md @@ -0,0 +1,202 @@ +# 🎓 Tutorial: Building & Deploying Long-running AI Agents with Agent Development Kit and Agent Platform + +This tutorial details a profound paradigm shift in generative AI application development: **moving away from stateless chatbots toward building reliable, time-agnostic background processes that can run for weeks.** + +Stateless chat applications are excellent for short, immediate Q&A turns. However, real-world enterprise workflows (like HR onboarding, supply chain tracking, or procurement) are dominated by **"idle time"**—periods where the agent must pause execution for days or weeks, waiting for external human actions (like signing a contract) or real-world events (like hardware package deliveries). + +This tutorial is styled as an **Iterative Coding Agent Playbook**. You will build a **New Hire Onboarding Coordinator Agent** by feeding high-level, intent-driven prompts incrementally to a coding agent. + +--- + +## 🔄 Conceptual Paradigm: Architecting for Time + +When designing long-running background agents, we must shift our thinking across three axes: + +1. **From Stateless to Durable**: Instead of keeping conversation history in volatile memory, the agent's state must be durably serialized to a persistent database (e.g. SQLite or Cloud SQL) so it survives system restarts and idle periods. +2. **From Active Polling to Event-Driven Resumption**: Instead of keeping a thread blocked or actively polling APIs for weeks, the agent must enter a dormant "paused" state. It is awakened programmatically only when an external webhook event triggers a resume event. +3. **From Single-Agent Monoliths to Multi-Agent Delegation**: Long-running processes involve disparate tasks. Rather than forcing a single coordinator to hold all tool definitions, we delegate specialized sub-workflows (like IT provisioning) to isolated subagents, handing control back when finished. + +--- + +## đŸ—ïž Step-by-Step Build Playbook + +Before we begin, ensure you have the **Agents CLI** (`agents-cli`) installed. This is the official command-line interface for the **Gemini Enterprise Agent Platform**, providing standard commands to scaffold projects, run local playgrounds, perform golden evaluations, and deploy reasoning engines. + +Install it globally using the `uv` tool manager: +```bash +uv tool install google-agents-cli +``` + +Follow these 6 structured phases to build the time-agnostic onboarding coordinator using iterative agent prompts. + +--- + +### Phase 1: The Foundation Scaffold + +We begin by bootstrapping our project structure and preparing it for persistent execution rather than simple chat. + +> ### đŸ€– Coding Agent Prompt 1: The Foundation Scaffold +> +> "Scaffold a new ADK agent project named 'onboarding-agent' in prototype mode. This is a long-running background process that should survive infrastructure restarts, so please ensure persistent session and memory bank settings are wired up from the start." + +#### Key Scaffold Outcomes: +- A standard ADK project structure is initialized. +- Volatile session parameters are cleared to prepare for database storage. + +--- + +### Phase 2: Grounding with a Strict State Machine + +Most chatbot tutorials rely on dumping massive conversation history JSON blobs into a vector database to "remember" past turns. However, over multi-day workflows, this unstructured approach introduces severe prompt context pollution, high token costs, and reasoning hallucinations. + +To build reliable background agents, we design a **durable, lightweight agent memory schema**. By grounding the coordinator in a strict enum-based state machine, we guarantee the agent maintains its logical reasoning chain across weeks of delay, without relying on raw chat logs. + +> ### đŸ€– Coding Agent Prompt 2: Grounding with a State Schema +> +> "We need to implement a strict state machine to ground our onboarding coordinator. Define an enum schema with steps: `START`, `WELCOME_SENT`, `DOCUMENTS_SIGNED`, `IT_PROVISIONED`, `HARDWARE_DELIVERED`, and `COMPLETED`, to track and enforce sequential task completion." + +#### Resulting Code ([app/state_schema.py](app/state_schema.py)): +```python +from enum import Enum + +class OnboardingStep(str, Enum): + START = "START" + WELCOME_SENT = "WELCOME_SENT" + DOCUMENTS_SIGNED = "DOCUMENTS_SIGNED" + IT_PROVISIONED = "IT_PROVISIONED" + HARDWARE_DELIVERED = "HARDWARE_DELIVERED" + COMPLETED = "COMPLETED" +``` + +--- + +### Phase 3: Multi-Agent IT Provisioning Delegation + +Rather than stuffing all IT and HR tools into a single coordinator agent, we instantiate a specialized subagent responsible solely for software account setups. + +> ### đŸ€– Coding Agent Prompt 3: Multi-Agent Delegation Setup +> +> "Rather than having our main onboarding coordinator invoke provisioning tools directly, let's delegate IT setup. Add a specialized subagent 'it_agent' to handle software account provisioning, register it under the main coordinator, and update the coordinator's instructions to delegate to it when documents are signed." + +#### Resulting Code Segment ([app/agent.py](app/agent.py)): +```python +from google.adk.agents import Agent +from app.tools import provision_software_accounts + +it_agent = Agent( + name="it_agent", + instructions="""You are the IT Provisioning Assistant. + Your sole responsibility is to provision corporate accounts. + When asked, collect the desired username prefix and call the 'provision_software_accounts' tool. + Once complete, hand control back to the coordinator immediately.""", + tools=[provision_software_accounts], + model="gemini-3.5-flash" +) +``` + +--- + +### Phase 4: Durable Database-Backed Session Storage + +In a serverless containerized environment (such as Cloud Run), containers frequently cold-start, scale down to zero when idle, or restart unexpectedly. Furthermore, external API tools might hit transient rate limits. If our agent's state lives in volatile container memory, all in-flight onboarding runs are permanently lost during these events. + +To build resilient multi-day background processes, we implement a strict **Checkpoint-and-Resume loop**. By writing our agent's session checkpoints to a persistent database (such as SQLite locally or Cloud SQL in production), the agent can survive container restarts, and failed operations can trigger safe retries without losing or corrupting the onboarding state. + +> ### đŸ€– Coding Agent Prompt 4: Persistent SQL Session Setup +> +> "To allow our onboarding sessions to survive long delays and server restarts, transition our session storage from volatile in-memory to persistent SQLite storage (sqlite+aiosqlite:///sessions.db) in our FastAPI application." + +#### Resulting Code Segment ([app/fast_api_app.py](app/fast_api_app.py)): +```python +from google.adk.sessions import DatabaseSessionService + +# Durable SQLite persistence +session_service_uri = "sqlite+aiosqlite:///sessions.db" +db_session_service = DatabaseSessionService(db_url=session_service_uri) + +app: FastAPI = get_fast_api_app( + agents_dir=AGENT_DIR, + web=True, + session_service_uri=session_service_uri, + otel_to_cloud=True, +) +``` + +--- + +### Phase 5: Event-Driven Resumption & Structured JSON Logging + +The most challenging aspect of long-running workflows is managing **"idle time"**—the long gaps when the agent goes dormant to wait for human approvals or slow physical actions (like document signatures or shipping packages). + +We handle this by **serializing the agent's working memory** at the moment of the pause, storing both the full conversation thread and our custom step variables in SQLite. When the external signature event triggers the webhook, the resume handler **hydrates the working memory** seamlessly. This ensures the LLM restores its complete context and picks up the logical reasoning chain *exactly* where it left off, without any memory dropouts or hallucinated deviations. + +> ### đŸ€– Coding Agent Prompt 5: Webhooks & Ambient Wake-up Setup +> +> "Expose webhook endpoints for contract signature and hardware delivery events on our FastAPI server. Create a resume handler that automatically hydrates the matching session state, transitions the onboarding step, and wakes up the agent programmatically using `runner.run_async` to execute the next step ambiently. Include structured JSON logging for all key webhook events." + +#### Resulting Webhook Code Segment ([app/fast_api_app.py](app/fast_api_app.py)): +```python +@app.post("/webhooks/document_signed") +async def trigger_document_signed_webhook(payload: WebhookPayload): + await resume_handler.receive_signed_documents_callback( + user_id=payload.user_id, + session_id=payload.session_id + ) + return {"status": "success", "message": "Document signature processed, agent resumed."} +``` + +#### Resulting Resume Handler Segment ([app/resume_handler.py](app/resume_handler.py)): +```python +async for event in self.runner.run_async( + user_id=user_id, + session_id=session_id, + new_message=types.Content( + role="user", + parts=[types.Part.from_text("Resume onboarding: Contract has been signed.")] + ) +): + self._log_structured( + severity="INFO", + message=f"Wake-up execution event: {event}", + event="runner_event", + session_id=session_id + ) +``` + +--- + +### Phase 6: Simulating week-long "Idle Time" delays (Evals) + +How do we test an onboarding flow that spans days? We write multi-turn Golden simulation tests that mock delays and webhook triggers. + +> ### đŸ€– Coding Agent Prompt 6: Golden Case Delay Simulation +> +> "Use the agents-cli evaluation skills to generate an eval set for our onboarding workflow. Create test trajectories that specifically simulate 'idle time'. I need a test case that mocks a 48-hour delay for IT hardware provisioning, and verifies that the agent resumes and successfully routes the final schedule without dropping the new hire's original context." + +#### Verifying Evaluations: +Execute the direct ADK virtualenv script to run your delay simulations locally bypassing transient packaging registry issues: +```bash +.venv/bin/adk eval ./app tests/eval/evalsets/idle_time_delay_eval.json --config_file_path tests/eval/eval_config.json +``` + +--- + +## 🚀 Production Deployment Setup + +When evaluations are green, elevate your project target to **Gemini Enterprise Agent Platform (Agent Runtime)**: + +> ### đŸ€– Coding Agent Prompt 7: Managed Platform Scaffolding +> +> "We are ready to deploy. Use `agents-cli scaffold enhance` to target Gemini Enterprise Agent Runtime (Agent Engine). Scaffold the Reasoning Engine app wrapper, and ensure Cloud Trace integration is wired up so we can monitor our pause-and-resume latencies in production." + +This command: +- Prepares the project to run on Google Cloud Agent Runtime. +- Natively handles session persistence directly in the cloud. +- Enables **Cloud Trace** out-of-the-box by default. + +--- + +## 🎯 Summary of Key Learnings +- **Chatbots are a subset**: Background processes are the future of enterprise AI automation. +- **Grounded state machines**: Enums and persistent databases protect your coordinator from losing tracks during days of inactivity. +- **Mocking time**: ADK evaluations are powerful tools to simulate multi-day delays instantly in continuous integration pipelines. diff --git a/agents/adk/new-hire-onboarding/app/__init__.py b/agents/adk/new-hire-onboarding/app/__init__.py new file mode 100644 index 0000000..d972509 --- /dev/null +++ b/agents/adk/new-hire-onboarding/app/__init__.py @@ -0,0 +1,17 @@ +# Copyright 2026 Google LLC +# +# 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 +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from .agent import app + +__all__ = ["app"] diff --git a/agents/adk/new-hire-onboarding/app/agent.py b/agents/adk/new-hire-onboarding/app/agent.py new file mode 100644 index 0000000..5b31504 --- /dev/null +++ b/agents/adk/new-hire-onboarding/app/agent.py @@ -0,0 +1,100 @@ +# Copyright 2026 Google LLC +# +# 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 +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os + +import google.auth +from google.adk.agents import Agent +from google.adk.agents.callback_context import CallbackContext +from google.adk.apps import App +from google.adk.models import Gemini +from google.genai import types + +from app.state_schema import OnboardingStep +from app.tools import ( + check_hardware_delivery, + provision_software_accounts, + send_day_one_schedule, + send_welcome_packet, +) + +_, project_id = google.auth.default() +os.environ["GOOGLE_CLOUD_PROJECT"] = project_id +os.environ["GOOGLE_CLOUD_LOCATION"] = "global" +os.environ["GOOGLE_GENAI_USE_ENTERPRISE"] = "True" + + +async def initialize_onboarding_state(callback_context: CallbackContext) -> None: + """Ensures all onboarding state machine keys are initialized to prevent errors.""" + state = callback_context.state + if "current_step" not in state: + state["current_step"] = OnboardingStep.START + if "new_hire_details" not in state: + state["new_hire_details"] = {} + if "pending_signals" not in state: + state["pending_signals"] = [] + + +instruction = """You are an HR Onboarding Coordinator Agent. Your goal is to safely guide the onboarding process of new hires through a sequence of checkpoint steps. + +Current Step: {current_step} +New Hire Details: {new_hire_details} +Pending Signals: {pending_signals} + +Follow this state machine flow exactly: +1. If current_step is 'START': Ask for the new hire's name, email, and start date. Once provided, invoke the 'send_welcome_packet' tool. +2. If current_step is 'WELCOME_SENT': Inform the user that you are currently in a "idle-time" pause waiting for the employee to sign documents. Do not call other tools. +3. If current_step is 'DOCUMENTS_SIGNED': Delegate the IT accounts provisioning to the 'it_agent' subagent. Do not call tools directly for provisioning accounts; transfer execution to 'it_agent'. +4. If current_step is 'IT_PROVISIONED': Ask for the hardware tracking ID (e.g. HW-12345) to check laptop shipping status. Once provided, invoke 'check_hardware_delivery'. +5. If current_step is 'HARDWARE_DELIVERED': Invoke the 'send_day_one_schedule' tool using the new hire's corporate email. +6. If current_step is 'COMPLETED': State that onboarding is fully complete, congratulate the user, and list the day-one schedule. + +Always stay grounded in your tools and current state. Do not skip steps or invent details. +""" + +it_agent = Agent( + name="it_agent", + model=Gemini( + model="gemini-3.1-flash-lite", + retry_options=types.HttpRetryOptions(attempts=3), + ), + instruction="""You are an IT Provisioning Agent. Your goal is to provision corporate software accounts (email, Slack) for the new hire. + +Current Step: {current_step} +New Hire Details: {new_hire_details} + +Follow these instructions: +1. Prompt the user/coordinator for the desired corporate username prefix if they haven't provided one. +2. Once provided, invoke the 'provision_software_accounts' tool. +3. After accounts are provisioned, inform the user that provisioning is complete and that control is being returned. Transfer execution back to the parent coordinator agent. +""", + tools=[provision_software_accounts], +) + +root_agent = Agent( + name="hr_onboarding_coordinator", + model=Gemini( + model="gemini-3.1-flash-lite", + retry_options=types.HttpRetryOptions(attempts=3), + ), + instruction=instruction, + tools=[send_welcome_packet, check_hardware_delivery, send_day_one_schedule], + sub_agents=[it_agent], + before_agent_callback=initialize_onboarding_state, +) + +app = App( + root_agent=root_agent, + name="app", +) diff --git a/agents/adk/new-hire-onboarding/app/agent_runtime_app.py b/agents/adk/new-hire-onboarding/app/agent_runtime_app.py new file mode 100644 index 0000000..debddc5 --- /dev/null +++ b/agents/adk/new-hire-onboarding/app/agent_runtime_app.py @@ -0,0 +1,65 @@ +# Copyright 2026 Google LLC +# +# 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 +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import logging +import os +from typing import Any + +import vertexai +from dotenv import load_dotenv +from google.adk.artifacts import GcsArtifactService, InMemoryArtifactService +from google.cloud import logging as google_cloud_logging +from vertexai.agent_engines.templates.adk import AdkApp + +from app.agent import app as adk_app +from app.app_utils.telemetry import setup_telemetry +from app.app_utils.typing import Feedback + +# Load environment variables from .env file at runtime +load_dotenv() + + +class AgentEngineApp(AdkApp): + def set_up(self) -> None: + """Initialize the agent engine app with logging and telemetry.""" + vertexai.init() + setup_telemetry() + super().set_up() + logging.basicConfig(level=logging.INFO) + logging_client = google_cloud_logging.Client() + self.logger = logging_client.logger(__name__) + if gemini_location: + os.environ["GOOGLE_CLOUD_LOCATION"] = gemini_location + + def register_feedback(self, feedback: dict[str, Any]) -> None: + """Collect and log feedback.""" + feedback_obj = Feedback.model_validate(feedback) + self.logger.log_struct(feedback_obj.model_dump(), severity="INFO") + + def register_operations(self) -> dict[str, list[str]]: + """Registers the operations of the Agent.""" + operations = super().register_operations() + operations[""] = [*operations.get("", []), "register_feedback"] + return operations + + +gemini_location = os.environ.get("GOOGLE_CLOUD_LOCATION") +logs_bucket_name = os.environ.get("LOGS_BUCKET_NAME") +agent_runtime = AgentEngineApp( + app=adk_app, + artifact_service_builder=lambda: ( + GcsArtifactService(bucket_name=logs_bucket_name) + if logs_bucket_name + else InMemoryArtifactService() + ), +) diff --git a/agents/adk/new-hire-onboarding/app/app_utils/telemetry.py b/agents/adk/new-hire-onboarding/app/app_utils/telemetry.py new file mode 100644 index 0000000..318e46b --- /dev/null +++ b/agents/adk/new-hire-onboarding/app/app_utils/telemetry.py @@ -0,0 +1,52 @@ +# Copyright 2026 Google LLC +# +# 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 +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import logging +import os + + +def setup_telemetry() -> str | None: + """Configure OpenTelemetry and Gen AI telemetry with GCS upload.""" + os.environ.setdefault("GOOGLE_CLOUD_AGENT_ENGINE_ENABLE_TELEMETRY", "true") + + bucket = os.environ.get("LOGS_BUCKET_NAME") + capture_content = os.environ.get( + "OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT", "false" + ) + if bucket and capture_content != "false": + logging.info( + "Prompt-response logging enabled - mode: NO_CONTENT (metadata only, no prompts/responses)" + ) + os.environ["OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT"] = "NO_CONTENT" + os.environ.setdefault("OTEL_INSTRUMENTATION_GENAI_UPLOAD_FORMAT", "jsonl") + os.environ.setdefault("OTEL_INSTRUMENTATION_GENAI_COMPLETION_HOOK", "upload") + os.environ.setdefault( + "OTEL_SEMCONV_STABILITY_OPT_IN", "gen_ai_latest_experimental" + ) + commit_sha = os.environ.get("COMMIT_SHA", "dev") + os.environ.setdefault( + "OTEL_RESOURCE_ATTRIBUTES", + f"service.namespace=new-hire-onboarding,service.version={commit_sha}", + ) + path = os.environ.get("GENAI_TELEMETRY_PATH", "completions") + os.environ.setdefault( + "OTEL_INSTRUMENTATION_GENAI_UPLOAD_BASE_PATH", + f"gs://{bucket}/{path}", + ) + else: + logging.info( + "Prompt-response logging disabled (set LOGS_BUCKET_NAME=gs://your-bucket and OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT=NO_CONTENT to enable)" + ) + + return bucket diff --git a/agents/adk/new-hire-onboarding/app/app_utils/typing.py b/agents/adk/new-hire-onboarding/app/app_utils/typing.py new file mode 100644 index 0000000..8dd4954 --- /dev/null +++ b/agents/adk/new-hire-onboarding/app/app_utils/typing.py @@ -0,0 +1,33 @@ +# Copyright 2026 Google LLC +# +# 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 +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import uuid +from typing import ( + Literal, +) + +from pydantic import ( + BaseModel, + Field, +) + + +class Feedback(BaseModel): + """Represents feedback for a conversation.""" + + score: int | float + text: str | None = "" + log_type: Literal["feedback"] = "feedback" + service_name: Literal["new-hire-onboarding"] = "new-hire-onboarding" + user_id: str = Field(default_factory=lambda: str(uuid.uuid4())) + session_id: str = Field(default_factory=lambda: str(uuid.uuid4())) diff --git a/agents/adk/new-hire-onboarding/app/fast_api_app.py b/agents/adk/new-hire-onboarding/app/fast_api_app.py new file mode 100644 index 0000000..c6d9f40 --- /dev/null +++ b/agents/adk/new-hire-onboarding/app/fast_api_app.py @@ -0,0 +1,184 @@ +# Copyright 2026 Google LLC +# +# 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 +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os + +from fastapi import FastAPI +from fastapi.responses import RedirectResponse +from fastapi.staticfiles import StaticFiles +from google.adk.cli.fast_api import get_fast_api_app +from google.adk.runners import Runner +from google.adk.sessions.database_session_service import DatabaseSessionService +from google.cloud import logging as google_cloud_logging +from pydantic import BaseModel + +from app.agent import app as agent_app +from app.app_utils.telemetry import setup_telemetry +from app.app_utils.typing import Feedback +from app.live_onboarding import ( + artifact_response, + case_payload, + create_live_case, + get_case, + latest_case_payload, + mark_document_signed, + mark_hardware_delivered, + static_root, +) +from app.resume_handler import OnboardingResumeHandler + +setup_telemetry() +logging_client = google_cloud_logging.Client() +logger = logging_client.logger(__name__) +allow_origins = ( + os.getenv("ALLOW_ORIGINS", "").split(",") if os.getenv("ALLOW_ORIGINS") else None +) + +# Artifact bucket for ADK (created by Terraform, passed via env var) +logs_bucket_name = os.environ.get("LOGS_BUCKET_NAME") + +AGENT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +# Persistent SQLite session configuration +session_service_uri = "sqlite+aiosqlite:///sessions.db" + +artifact_service_uri = f"gs://{logs_bucket_name}" if logs_bucket_name else None + +app: FastAPI = get_fast_api_app( + agents_dir=AGENT_DIR, + web=True, + artifact_service_uri=artifact_service_uri, + allow_origins=allow_origins, + session_service_uri=session_service_uri, + otel_to_cloud=True, +) +app.title = "new-hire-onboarding" +app.description = "API for interacting with the Agent new-hire-onboarding" + +db_session_service = DatabaseSessionService(db_url=session_service_uri) +webhook_runner = Runner(app=agent_app, session_service=db_session_service) +resume_handler = OnboardingResumeHandler(runner=webhook_runner) + +app.mount( + "/live-onboarding", + StaticFiles(directory=static_root() / "live-onboarding", html=True), + name="live-onboarding", +) + + +@app.get("/demo") +def demo_index() -> RedirectResponse: + return RedirectResponse(url="/live-onboarding/") + + +@app.post("/api/live-onboarding/start") +async def start_live_onboarding() -> dict: + case = await create_live_case(db_session_service) + return { + "active": True, + "case": case_payload(get_case(case.id)), + } + + +@app.get("/api/live-onboarding/cases/{case_id}") +def get_live_onboarding_case(case_id: str) -> dict: + return { + "active": True, + "case": case_payload(get_case(case_id)), + } + + +@app.post("/api/live-onboarding/cases/{case_id}/sign") +async def sign_live_onboarding_packet(case_id: str) -> dict: + case = get_case(case_id) + if case.document_signed: + return {"active": True, "case": case_payload(case)} + await mark_document_signed(case, resume_handler) + return {"active": True, "case": case_payload(case)} + + +@app.post("/api/live-onboarding/cases/{case_id}/deliver-hardware") +async def confirm_live_hardware_delivery(case_id: str) -> dict: + case = get_case(case_id) + if case.hardware_delivered: + return {"active": True, "case": case_payload(case)} + await mark_hardware_delivered(case, resume_handler) + return {"active": True, "case": case_payload(case)} + + +@app.get("/api/live-onboarding/cases/{case_id}/artifacts/{artifact_id}") +def get_live_onboarding_artifact(case_id: str, artifact_id: str): + return artifact_response(case_id, artifact_id) + + +@app.get("/api/live-onboarding/current") +def get_current_live_onboarding_case() -> dict: + return latest_case_payload() + + +class WebhookPayload(BaseModel): + user_id: str + session_id: str + + +class HardwareWebhookPayload(WebhookPayload): + tracking_id: str + + +@app.post("/webhooks/document_signed") +async def trigger_document_signed_webhook(payload: WebhookPayload) -> dict[str, str]: + """Webhook called when employee signs their contract. Wakes up the onboarding agent.""" + await resume_handler.receive_signed_documents_callback( + user_id=payload.user_id, session_id=payload.session_id + ) + return { + "status": "success", + "message": "Document signature processed, agent resumed.", + } + + +@app.post("/webhooks/hardware_delivered") +async def trigger_hardware_delivered_webhook( + payload: HardwareWebhookPayload, +) -> dict[str, str]: + """Webhook called when carrier confirms delivery of the laptop. Wakes up the onboarding agent.""" + await resume_handler.receive_hardware_delivery_callback( + user_id=payload.user_id, + session_id=payload.session_id, + tracking_id=payload.tracking_id, + ) + return { + "status": "success", + "message": "Hardware delivery processed, agent resumed.", + } + + +@app.post("/feedback") +def collect_feedback(feedback: Feedback) -> dict[str, str]: + """Collect and log feedback. + + Args: + feedback: The feedback data to log + + Returns: + Success message + """ + logger.log_struct(feedback.model_dump(), severity="INFO") + return {"status": "success"} + + +# Main execution +if __name__ == "__main__": + import uvicorn + + uvicorn.run(app, host="0.0.0.0", port=8000) diff --git a/agents/adk/new-hire-onboarding/app/live_onboarding.py b/agents/adk/new-hire-onboarding/app/live_onboarding.py new file mode 100644 index 0000000..b26375e --- /dev/null +++ b/agents/adk/new-hire-onboarding/app/live_onboarding.py @@ -0,0 +1,684 @@ +import html +import time +import uuid +from dataclasses import dataclass, field +from pathlib import Path +from typing import Any + +from fastapi import HTTPException +from fastapi.responses import HTMLResponse + +from app.state_schema import OnboardingStep + + +@dataclass +class LiveOnboardingCase: + id: str + session_id: str + user_id: str + employee: dict[str, str] + current_step: str = OnboardingStep.WELCOME_SENT + pending_signals: list[str] = field(default_factory=lambda: ["document_signed"]) + status: str = "waiting_for_employee_signature" + document_signed: bool = False + hardware_delivered: bool = False + adk_status: str = "session_ready" + events: list[dict[str, str]] = field(default_factory=list) + artifacts: list[dict[str, str]] = field(default_factory=list) + created_at: float = field(default_factory=time.time) + updated_at: float = field(default_factory=time.time) + + +CASES: dict[str, LiveOnboardingCase] = {} +SESSION_TO_CASE: dict[str, str] = {} +LATEST_CASE_ID: str | None = None + + +def static_root() -> Path: + return Path(__file__).parent / "static" + + +def artifact_root() -> Path: + path = Path(__file__).resolve().parents[1] / "local_artifacts" / "onboarding" + path.mkdir(parents=True, exist_ok=True) + return path + + +def _employee() -> dict[str, str]: + return { + "name": "Olivia Bennett", + "email": "olivia.bennett@example.com", + "start_date": "2026-06-01", + "role": "Product Manager", + "team": "Platform Systems", + "manager": "Avery Stone", + "corporate_email": "olivia.bennett@example.com", + "tracking_id": "HW-55443", + "photo_url": "/live-onboarding/olivia-bennett.jpg", + } + + +def _case_dir(case_id: str) -> Path: + path = artifact_root() / case_id + path.mkdir(parents=True, exist_ok=True) + return path + + +def _event(case: LiveOnboardingCase, kind: str, title: str, detail: str) -> None: + case.events.insert( + 0, + { + "kind": kind, + "title": title, + "detail": detail, + "time": time.strftime("%H:%M:%S"), + }, + ) + case.updated_at = time.time() + + +def _artifact( + case: LiveOnboardingCase, artifact_id: str, title: str, kind: str, filename: str +) -> None: + href = f"/api/live-onboarding/cases/{case.id}/artifacts/{artifact_id}" + existing = next( + (item for item in case.artifacts if item["id"] == artifact_id), None + ) + payload = { + "id": artifact_id, + "title": title, + "kind": kind, + "filename": filename, + "href": href, + "created_at": time.strftime("%H:%M:%S"), + } + if existing: + existing.update(payload) + else: + case.artifacts.insert(0, payload) + + +def _packet_html(case: LiveOnboardingCase, signed: bool = False) -> str: + employee = case.employee + signature = ( + html.escape(employee["name"]) if signed else "Pending employee signature" + ) + signed_at = time.strftime("%Y-%m-%d %H:%M:%S %Z") if signed else "" + signature_class = "signed" if signed else "pending" + return f""" + + + + + Onboarding Packet - {html.escape(employee["name"])} + + + +
    +
    +
    +

    New Hire Onboarding Packet

    +

    This local packet is generated for the demo workflow. It is not a legal contract or e-signature integration.

    +
    +
    + Packet ID: {html.escape(case.id)}
    + Session ID: {html.escape(case.session_id)}
    + Generated locally +
    +
    + +
    +
    Employee{html.escape(employee["name"])}
    +
    Email{html.escape(employee["email"])}
    +
    Role{html.escape(employee["role"])}
    +
    Start date{html.escape(employee["start_date"])}
    +
    Team{html.escape(employee["team"])}
    +
    Manager{html.escape(employee["manager"])}
    +
    + +

    Welcome

    +

    + Welcome to Platform Systems. This packet confirms the onboarding details that the HR + onboarding coordinator will use to prepare access, equipment, and the Day One schedule. +

    + +

    Employee acknowledgements

    +
      +
    • I confirm that my onboarding profile is accurate.
    • +
    • I acknowledge that IT access and hardware delivery depend on completing this packet.
    • +
    • I understand this demo stores a local signed artifact for inspection by HR.
    • +
    + +
    + Signature + {signature} + {"Signed locally at " + html.escape(signed_at) + "" if signed else ""} +
    +
    + +""" + + +def _schedule_html(case: LiveOnboardingCase) -> str: + employee = case.employee + return f""" + + + + + Day One Schedule + + + +
    +

    Day One Schedule for {html.escape(employee["name"])}

    +

    Sent to {html.escape(employee["corporate_email"])} after hardware delivery was confirmed.

    +
      +
    1. 09:00Welcome and IT login setup
    2. +
    3. 10:00Meet {html.escape(employee["manager"])} and the team
    4. +
    5. 11:30Platform Systems overview
    6. +
    7. 14:00Security and device walkthrough
    8. +
    +
    + +""" + + +def _hardware_receipt_html(case: LiveOnboardingCase) -> str: + employee = case.employee + return f""" + + + + + Hardware Delivery Receipt + + + +
    +

    Hardware Delivery Receipt

    +

    Employee-confirmed local receipt generated by the onboarding demo.

    +
    + Confirmed delivery +

    {html.escape(employee["tracking_id"])} delivered to {html.escape(employee["name"])}.

    +
    +
    + +""" + + +def _write_artifact(case: LiveOnboardingCase, filename: str, body: str) -> None: + (_case_dir(case.id) / filename).write_text(body, encoding="utf-8") + + +def case_payload(case: LiveOnboardingCase) -> dict[str, Any]: + return { + "id": case.id, + "session_id": case.session_id, + "user_id": case.user_id, + "employee": case.employee, + "current_step": case.current_step, + "pending_signals": case.pending_signals, + "status": case.status, + "document_signed": case.document_signed, + "hardware_delivered": case.hardware_delivered, + "adk_status": case.adk_status, + "events": case.events, + "artifacts": case.artifacts, + "updated_at": case.updated_at, + } + + +async def create_live_case(session_service) -> LiveOnboardingCase: + global LATEST_CASE_ID + + employee = _employee() + case_id = str(uuid.uuid4()) + case = LiveOnboardingCase( + id=case_id, + session_id=case_id, + user_id="employee", + employee=employee, + ) + await session_service.create_session( + app_name="app", + user_id=case.user_id, + session_id=case.session_id, + state={ + "current_step": OnboardingStep.WELCOME_SENT, + "new_hire_details": { + "name": employee["name"], + "email": employee["email"], + "start_date": employee["start_date"], + }, + "pending_signals": ["document_signed"], + }, + ) + _write_artifact(case, "welcome_packet.html", _packet_html(case)) + _artifact( + case, + "welcome-packet", + "Unsigned onboarding packet", + "html", + "welcome_packet.html", + ) + _event( + case, + "agent", + "Welcome packet generated", + "A real local HTML onboarding packet was generated and attached to this case.", + ) + _event( + case, + "state", + "Agent is waiting", + "ADK session is parked at WELCOME_SENT until the employee signs the packet.", + ) + CASES[case.id] = case + SESSION_TO_CASE[case.session_id] = case.id + LATEST_CASE_ID = case.id + return case + + +async def mark_document_signed(case: LiveOnboardingCase, resume_handler) -> None: + case.document_signed = True + case.status = "waking_after_signature" + case.current_step = OnboardingStep.DOCUMENTS_SIGNED + case.pending_signals = [] + _write_artifact( + case, "signed_onboarding_packet.html", _packet_html(case, signed=True) + ) + _artifact( + case, + "signed-packet", + "Signed onboarding packet", + "html", + "signed_onboarding_packet.html", + ) + _event( + case, + "employee", + f"{case.employee['name']} signed the packet", + "The employee clicked Sign Packet. A signed local artifact was stored.", + ) + _event( + case, + "webhook", + "document_signed webhook fired", + "The app called the same ADK resume handler used by /webhooks/document_signed.", + ) + try: + await resume_handler.receive_signed_documents_callback( + user_id=case.user_id, session_id=case.session_id + ) + case.adk_status = "document_resume_completed" + case.current_step = OnboardingStep.IT_PROVISIONED + case.pending_signals = ["hardware_delivered"] + case.status = "waiting_for_hardware_delivery" + _event( + case, + "agent", + "ADK resume completed", + "The document signature wake turn completed. HR can now wait for hardware delivery.", + ) + except Exception as exc: + case.adk_status = "document_resume_failed" + case.status = "document_signed_adk_resume_failed" + _event(case, "error", "ADK resume failed", str(exc)) + finally: + case.updated_at = time.time() + + +async def mark_hardware_delivered(case: LiveOnboardingCase, resume_handler) -> None: + case.hardware_delivered = True + case.status = "waking_after_hardware_delivery" + case.current_step = OnboardingStep.HARDWARE_DELIVERED + case.pending_signals = [] + _write_artifact( + case, + "hardware_delivery_receipt.html", + _hardware_receipt_html(case), + ) + _artifact( + case, + "hardware-receipt", + "Hardware delivery receipt", + "html", + "hardware_delivery_receipt.html", + ) + _event( + case, + "employee", + f"{case.employee['name']} confirmed laptop delivery", + f"Employee confirmed receipt of tracking ID {case.employee['tracking_id']}.", + ) + _event( + case, + "webhook", + "hardware_delivered webhook fired", + "The app called the same ADK resume handler used by /webhooks/hardware_delivered.", + ) + try: + await resume_handler.receive_hardware_delivery_callback( + user_id=case.user_id, + session_id=case.session_id, + tracking_id=case.employee["tracking_id"], + ) + case.adk_status = "hardware_resume_completed" + case.current_step = OnboardingStep.COMPLETED + case.status = "completed" + _write_artifact(case, "day_one_schedule.html", _schedule_html(case)) + _artifact( + case, + "day-one-schedule", + "Day One schedule", + "html", + "day_one_schedule.html", + ) + _event( + case, + "agent", + "Onboarding completed", + "The hardware wake turn completed and a local Day One schedule artifact was stored.", + ) + except Exception as exc: + case.adk_status = "hardware_resume_failed" + case.status = "hardware_delivered_adk_resume_failed" + _event(case, "error", "ADK resume failed", str(exc)) + finally: + case.updated_at = time.time() + + +def case_for_session(session_id: str) -> LiveOnboardingCase | None: + case_id = SESSION_TO_CASE.get(session_id) + if not case_id: + return None + return CASES.get(case_id) + + +def get_case(case_id: str) -> LiveOnboardingCase: + case = CASES.get(case_id) + if not case: + raise HTTPException(status_code=404, detail="Onboarding case not found") + return case + + +def artifact_response(case_id: str, artifact_id: str) -> HTMLResponse: + case = get_case(case_id) + artifact = next( + (item for item in case.artifacts if item["id"] == artifact_id), None + ) + if not artifact: + raise HTTPException(status_code=404, detail="Artifact not found") + path = _case_dir(case.id) / artifact["filename"] + if not path.exists(): + raise HTTPException(status_code=404, detail="Artifact file not found") + return HTMLResponse(path.read_text(encoding="utf-8")) + + +def empty_case_payload() -> dict[str, Any]: + return {"active": False, "message": "No live onboarding case has been started."} + + +def latest_case_payload() -> dict[str, Any]: + if not LATEST_CASE_ID or LATEST_CASE_ID not in CASES: + return empty_case_payload() + return {"active": True, "case": case_payload(CASES[LATEST_CASE_ID])} diff --git a/agents/adk/new-hire-onboarding/app/resume_handler.py b/agents/adk/new-hire-onboarding/app/resume_handler.py new file mode 100644 index 0000000..dbe87e3 --- /dev/null +++ b/agents/adk/new-hire-onboarding/app/resume_handler.py @@ -0,0 +1,173 @@ +# Copyright 2026 Google LLC +# +# 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 +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import json +import logging + +from google.adk.runners import Runner +from google.genai import types + +from app.state_schema import OnboardingStep + +logger = logging.getLogger(__name__) + + +class OnboardingResumeHandler: + def __init__(self, runner: Runner): + """Initializes the resume handler with the active ADK Runner.""" + self.runner = runner + + def _log_structured(self, severity: str, message: str, **kwargs) -> None: + """Helper to output formatted JSON logs that Cloud Logging can parse natively.""" + payload = {"severity": severity, "message": message, **kwargs} + logger.info(json.dumps(payload)) + + async def receive_signed_documents_callback( + self, user_id: str, session_id: str + ) -> None: + """Simulates an external webhook notifying that the employee signed onboarding documents. + + Hydrates the existing session, transitions the checkpoint to DOCUMENTS_SIGNED, and resumes. + """ + self._log_structured( + severity="INFO", + message=f"Received document signature notification for session {session_id}", + event="webhook_received", + webhook_type="document_signed", + session_id=session_id, + user_id=user_id, + ) + + try: + self._log_structured( + severity="INFO", + message=f"State machine transitioned to {OnboardingStep.DOCUMENTS_SIGNED}", + event="state_transition", + session_id=session_id, + user_id=user_id, + new_step=OnboardingStep.DOCUMENTS_SIGNED, + ) + + # Trigger runner wake-up and run execution ambiently + async for event in self.runner.run_async( + user_id=user_id, + session_id=session_id, + new_message=types.Content( + role="user", + parts=[ + types.Part.from_text( + text="Resume onboarding: Contract has been signed." + ) + ], + ), + state_delta={ + "current_step": OnboardingStep.DOCUMENTS_SIGNED, + "pending_signals": [], + }, + ): + self._log_structured( + severity="INFO", + message=f"Wake-up execution event: {event}", + event="runner_event", + session_id=session_id, + user_id=user_id, + ) + + self._log_structured( + severity="INFO", + message="Ambient document signature execution turn completed successfully", + event="runner_turn_success", + session_id=session_id, + user_id=user_id, + ) + except Exception as e: + self._log_structured( + severity="ERROR", + message=f"Ambient document signature execution turn failed: {e!s}", + event="runner_turn_failure", + session_id=session_id, + user_id=user_id, + error=str(e), + ) + raise + + async def receive_hardware_delivery_callback( + self, user_id: str, session_id: str, tracking_id: str + ) -> None: + """Simulates a carrier callback confirming laptop package delivery at the employee's house. + + Hydrates the session, transitions the checkpoint to HARDWARE_DELIVERED, and resumes. + """ + self._log_structured( + severity="INFO", + message=f"Received hardware package delivery webhook for tracking ID {tracking_id}", + event="webhook_received", + webhook_type="hardware_delivered", + session_id=session_id, + user_id=user_id, + tracking_id=tracking_id, + ) + + try: + self._log_structured( + severity="INFO", + message=f"State machine transitioned to {OnboardingStep.HARDWARE_DELIVERED}", + event="state_transition", + session_id=session_id, + user_id=user_id, + new_step=OnboardingStep.HARDWARE_DELIVERED, + ) + + # Trigger runner wake-up and run execution ambiently + async for event in self.runner.run_async( + user_id=user_id, + session_id=session_id, + new_message=types.Content( + role="user", + parts=[ + types.Part.from_text( + text=f"Resume onboarding: Hardware delivered with tracking ID {tracking_id}." + ) + ], + ), + state_delta={ + "current_step": OnboardingStep.HARDWARE_DELIVERED, + "pending_signals": [], + }, + ): + self._log_structured( + severity="INFO", + message=f"Wake-up execution event: {event}", + event="runner_event", + session_id=session_id, + user_id=user_id, + ) + + self._log_structured( + severity="INFO", + message="Ambient hardware delivery execution turn completed successfully", + event="runner_turn_success", + session_id=session_id, + user_id=user_id, + ) + except Exception as e: + self._log_structured( + severity="ERROR", + message=f"Ambient hardware delivery execution turn failed: {e!s}", + event="runner_turn_failure", + session_id=session_id, + user_id=user_id, + error=str(e), + ) + raise diff --git a/agents/adk/new-hire-onboarding/app/state_schema.py b/agents/adk/new-hire-onboarding/app/state_schema.py new file mode 100644 index 0000000..d412a40 --- /dev/null +++ b/agents/adk/new-hire-onboarding/app/state_schema.py @@ -0,0 +1,22 @@ +# Copyright 2026 Google LLC +# +# 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 +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +class OnboardingStep: + START = "START" + WELCOME_SENT = "WELCOME_SENT" + DOCUMENTS_SIGNED = "DOCUMENTS_SIGNED" + IT_PROVISIONED = "IT_PROVISIONED" + HARDWARE_DELIVERED = "HARDWARE_DELIVERED" + COMPLETED = "COMPLETED" diff --git a/agents/adk/new-hire-onboarding/app/static/live-onboarding/assets/index-DgUrFQNs.css b/agents/adk/new-hire-onboarding/app/static/live-onboarding/assets/index-DgUrFQNs.css new file mode 100644 index 0000000..8fe536b --- /dev/null +++ b/agents/adk/new-hire-onboarding/app/static/live-onboarding/assets/index-DgUrFQNs.css @@ -0,0 +1 @@ +:root{color-scheme:dark;--colors-primary: #f54e00;--colors-primary-active: #d04200;--colors-canvas: #12110e;--colors-canvas-soft: #181713;--colors-surface-card: #1f1e19;--colors-surface-strong: #302e27;--colors-hairline: #343229;--colors-hairline-soft: #2a2822;--colors-hairline-strong: #514e43;--colors-ink: #f4f1e8;--colors-body: #c8c2b6;--colors-muted: #989184;--colors-muted-soft: #706a60;--colors-on-primary: #ffffff;--colors-timeline-thinking: #b87558;--colors-timeline-grep: #6f9f74;--colors-timeline-read: #6f8fbb;--colors-timeline-edit: #9271b6;--colors-timeline-done: #d39542;--colors-semantic-success: #58b991;--colors-semantic-error: #f06383;--rounded-sm: 6px;--rounded-md: 8px;--rounded-lg: 12px;--font-sans: Inter, system-ui, "Helvetica Neue", Helvetica, Arial, sans-serif;--font-mono: "JetBrains Mono", "SFMono-Regular", "Roboto Mono", Consolas, monospace}*{box-sizing:border-box}html{min-width:320px;background:var(--colors-canvas)}body{margin:0;color:var(--colors-ink);font-family:var(--font-sans);letter-spacing:0;overflow-x:hidden}button,iframe{font:inherit}button{border:0}.app-shell{width:min(1200px,calc(100% - 32px));min-height:100vh;margin:0 auto;padding:16px 0 80px;overflow-x:hidden}.topbar,.brand-lockup,.top-actions,.hero-strip,.hero-stats,.state-band,.state-band-steps,.workspace,.panel-header,.panel-title,.employee-card,.section-heading,.artifact-card,.event-row,.document-toolbar,.portal-grid,.action-card,.case-summary div,.connection-pill,.secondary-button,.primary-button{display:flex}.topbar{align-items:center;justify-content:space-between;gap:24px;min-height:64px;margin-bottom:32px;border-bottom:1px solid var(--colors-hairline)}.brand-lockup{align-items:center;min-width:0;max-width:100%;width:100%;gap:12px}.brand-lockup>div:last-child,.panel-title>div,.document-toolbar>div{flex:1;min-width:0;max-width:100%}.brand-mark{display:grid;flex:0 0 auto;width:32px;height:32px;place-items:center;border-radius:var(--rounded-md);color:var(--colors-primary);background:transparent}h1,h2,h3,h4,p{margin:0}h1{color:var(--colors-ink);font-size:18px;font-weight:600;line-height:1.4}.brand-lockup p,.panel-header p,.event-row p,.artifact-card small,.document-toolbar p,.action-card p,.case-summary p,.employee-card p,.metric span,.tiny-label,.step-row p{color:var(--colors-body);font-size:14px;font-weight:400;line-height:1.5;overflow-wrap:anywhere}.top-actions{align-items:center;justify-content:flex-end;flex-wrap:wrap;gap:8px}.connection-pill,.secondary-button,.primary-button{align-items:center;justify-content:center;gap:8px;min-height:40px;border-radius:var(--rounded-md);padding:10px 18px;font-size:14px;font-weight:500;line-height:1;white-space:nowrap}.connection-pill{border:1px solid var(--colors-hairline-strong);color:var(--colors-body);background:var(--colors-surface-card)}.pulse{display:block;width:7px;height:7px;border-radius:999px}.pulse.online{background:var(--colors-semantic-success)}.pulse.idle{background:var(--colors-timeline-done)}.secondary-button,.primary-button,.green-button,.amber-button{cursor:pointer;transition:background .14s ease,border-color .14s ease,opacity .14s ease}.secondary-button{border:1px solid var(--colors-hairline-strong);color:var(--colors-ink);background:var(--colors-surface-card)}.primary-button{color:var(--colors-on-primary);background:var(--colors-primary)}.primary-button:active,.green-button:active,.amber-button:active{background:var(--colors-primary-active)}button:disabled{cursor:not-allowed;opacity:.56}.error-banner{margin-bottom:16px;padding:12px 16px;border:1px solid rgba(207,45,86,.34);border-radius:var(--rounded-md);color:var(--colors-semantic-error);background:var(--colors-surface-card);font-size:14px}.hero-strip{align-items:flex-end;justify-content:space-between;gap:32px;margin-bottom:24px;padding-bottom:28px;border-bottom:1px solid var(--colors-hairline)}.tiny-label{display:block;margin-bottom:8px;color:var(--colors-muted);font-size:11px;font-weight:600;letter-spacing:.88px;line-height:1.4;text-transform:uppercase}.hero-strip h2{width:100%;max-width:560px;color:var(--colors-ink);font-size:clamp(32px,4.5vw,56px);font-weight:400;letter-spacing:-1.12px;line-height:1.05;overflow-wrap:anywhere}.hero-stats{align-items:stretch;justify-content:flex-end;flex-wrap:wrap;gap:12px;max-width:610px}.metric{min-width:138px;padding:12px 16px;border:1px solid var(--colors-hairline);border-radius:var(--rounded-lg);background:var(--colors-surface-card)}.metric strong{display:block;margin-top:6px;color:var(--colors-ink);font-family:var(--font-mono);font-size:13px;font-weight:400;line-height:1.5;overflow-wrap:anywhere}.state-band{align-items:center;justify-content:space-between;gap:18px;margin-bottom:24px;padding:16px 18px;border:1px solid var(--colors-hairline);border-radius:var(--rounded-lg);background:var(--colors-surface-card)}.state-band strong{display:block;color:var(--colors-ink);font-size:22px;font-weight:400;letter-spacing:-.11px;line-height:1.3}.state-band p{margin-top:4px;color:var(--colors-body);font-size:14px;line-height:1.5}.state-band-steps{align-items:center;justify-content:flex-end;flex-wrap:wrap;gap:8px;flex:0 0 auto}.state-chip{display:inline-flex;align-items:center;justify-content:center;min-height:30px;gap:7px;padding:7px 10px;border:1px solid var(--colors-hairline);border-radius:9999px;color:var(--colors-body);background:var(--colors-canvas-soft);font-size:12px;font-weight:500;line-height:1;white-space:nowrap}.state-chip.active{border-color:var(--colors-primary);color:var(--colors-on-primary);background:var(--colors-primary)}.workspace{position:relative;display:grid;grid-template-columns:minmax(360px,410px) minmax(0,1fr);align-items:start;gap:24px}.hr-panel,.employee-panel{min-width:0;border:1px solid var(--colors-hairline);border-radius:var(--rounded-lg);background:var(--colors-surface-card)}.hr-panel{padding:24px}.employee-panel{flex:1;padding:24px}.panel-header{align-items:flex-start;justify-content:space-between;gap:16px;margin-bottom:20px}.panel-title{align-items:flex-start;min-width:0;gap:12px}.panel-title h3{color:var(--colors-ink);font-size:26px;font-weight:400;letter-spacing:-.325px;line-height:1.25}.panel-icon,.action-icon{display:grid;flex:0 0 auto;width:32px;height:32px;place-items:center;border:1px solid var(--colors-hairline);border-radius:var(--rounded-md);color:var(--colors-ink);background:var(--colors-canvas-soft)}.employee-state{min-height:28px;padding:4px 10px;border-radius:9999px;color:var(--colors-ink);background:var(--colors-surface-strong);font-size:11px;font-weight:600;letter-spacing:.88px;line-height:1.4;text-transform:uppercase;white-space:nowrap}.employee-card{display:grid;grid-template-columns:auto minmax(0,1fr) auto;align-items:center;gap:16px;margin-bottom:24px;padding:16px;border:1px solid var(--colors-hairline);border-radius:var(--rounded-lg);background:var(--colors-canvas-soft)}.avatar{flex:0 0 auto;width:48px;height:48px;display:block;border-radius:9999px;border:1px solid var(--colors-hairline-strong);background:var(--colors-surface-card);object-fit:cover}.employee-info{min-width:0}.employee-card h4{color:var(--colors-ink);font-size:18px;font-weight:600;line-height:1.4}.employee-meta{margin-left:auto;text-align:right;white-space:nowrap}.employee-meta span{display:block;color:var(--colors-muted);font-size:11px;font-weight:600;letter-spacing:.88px;text-transform:uppercase}.employee-meta strong{color:var(--colors-ink);font-family:var(--font-mono);font-size:13px;font-weight:400}.progress-section,.artifact-section,.event-section,.agent-ticker{margin-top:24px}.section-heading{align-items:center;justify-content:space-between;margin-bottom:12px}.section-heading h4{color:var(--colors-ink);font-size:18px;font-weight:600;line-height:1.4}.section-heading span{color:var(--colors-muted);font-family:var(--font-mono);font-size:13px}.step-stack,.artifact-list,.event-list{display:grid;gap:8px}.step-row{display:grid;grid-template-columns:auto minmax(0,1fr) auto auto;align-items:center;gap:10px;padding:12px;border:1px solid var(--colors-hairline);border-radius:var(--rounded-md);background:var(--colors-canvas-soft)}.step-row.active{border-color:var(--colors-hairline-strong);background:var(--colors-surface-card)}.step-row.done{background:var(--colors-canvas-soft)}.step-row.locked{color:var(--colors-muted)}.step-icon{display:grid;width:30px;height:30px;place-items:center;border:1px solid var(--colors-hairline);border-radius:var(--rounded-sm);color:var(--colors-ink);background:var(--colors-surface-card)}.timeline-pill{justify-self:end;min-width:74px;padding:4px 10px;border-radius:9999px;color:var(--colors-ink);font-size:11px;font-weight:600;letter-spacing:.88px;line-height:1.4;text-align:center;text-transform:uppercase}.timeline-pill.thinking{background:var(--colors-timeline-thinking)}.timeline-pill.grep{background:var(--colors-timeline-grep)}.timeline-pill.read{background:var(--colors-timeline-read)}.timeline-pill.edit{background:var(--colors-timeline-edit)}.timeline-pill.done{color:#16130f;background:var(--colors-timeline-done)}.ticker-rows{display:grid;gap:8px}.ticker-row{display:grid;grid-template-columns:auto minmax(0,1fr);align-items:center;gap:10px;min-height:38px;padding:8px 10px;border:1px solid var(--colors-hairline);border-radius:var(--rounded-md);background:var(--colors-canvas-soft)}.ticker-row strong{color:var(--colors-ink);font-size:13px;font-weight:500;line-height:1.35}.ticker-row .timeline-pill{min-width:78px;justify-self:start}.artifact-card{align-items:center;width:100%;min-height:62px;gap:12px;padding:12px;border:1px solid var(--colors-hairline);border-radius:var(--rounded-md);color:var(--colors-ink);background:var(--colors-surface-card);text-align:left}.artifact-thumb{position:relative;display:grid;flex:0 0 48px;width:48px;height:58px;place-items:center;align-content:center;gap:4px;border:1px solid var(--colors-hairline-strong);border-radius:6px;color:var(--colors-ink);background:var(--colors-canvas-soft)}.artifact-thumb:before{position:absolute;top:0;right:0;width:13px;height:13px;border-bottom:1px solid var(--colors-hairline-strong);border-left:1px solid var(--colors-hairline-strong);background:var(--colors-surface-card);content:""}.artifact-thumb strong{color:var(--colors-body);font-family:var(--font-mono);font-size:9px;font-weight:400;line-height:1;text-transform:uppercase}.artifact-card span{min-width:0;flex:1}.artifact-card .artifact-thumb{min-width:48px;flex:0 0 48px}.artifact-card strong,.artifact-card small{display:block}.artifact-card strong,.step-row strong,.action-card h4,.case-summary strong{color:var(--colors-ink);font-size:16px;font-weight:600;line-height:1.4}.artifact-card .artifact-thumb strong{color:var(--colors-body);font-family:var(--font-mono);font-size:9px;font-weight:400;line-height:1;text-transform:uppercase}.artifact-card.selected{border-color:var(--colors-primary);background:var(--colors-canvas-soft)}.empty-box{display:flex;align-items:center;gap:10px;padding:12px;border:1px solid var(--colors-hairline);border-radius:var(--rounded-md);color:var(--colors-body);background:var(--colors-canvas-soft);font-size:14px}.event-list{max-height:420px;overflow:auto;padding-right:2px}.event-row{align-items:flex-start;gap:10px;padding:12px;border:1px solid var(--colors-hairline);border-radius:var(--rounded-md);background:var(--colors-canvas-soft)}.event-row code{display:block;margin-bottom:4px;color:var(--colors-body);font-family:var(--font-mono);font-size:13px;font-weight:400;line-height:1.5}.event-row strong{display:block;margin-bottom:4px;color:var(--colors-ink);font-size:16px;font-weight:600;line-height:1.4}.event-dot{width:7px;height:7px;margin-top:7px;border-radius:999px;background:var(--colors-primary)}.event-dot.employee{background:var(--colors-semantic-success)}.event-dot.webhook{background:var(--colors-timeline-done)}.event-dot.error{background:var(--colors-semantic-error)}.portal-grid{display:grid;grid-template-columns:minmax(0,1fr) minmax(248px,276px);align-items:start;gap:16px}.document-stage{flex:1;min-width:0}.document-toolbar{align-items:flex-start;justify-content:space-between;gap:16px;margin-bottom:12px;padding:16px;border:1px solid var(--colors-hairline);border-radius:var(--rounded-lg);background:var(--colors-canvas-soft)}.document-toolbar h3{color:var(--colors-ink);max-width:420px;font-size:clamp(22px,3vw,26px);font-weight:400;letter-spacing:-.325px;line-height:1.25}.open-link{display:inline-flex;align-items:center;justify-content:center;min-height:40px;padding:10px 18px;border:1px solid var(--colors-hairline-strong);border-radius:var(--rounded-md);color:var(--colors-ink);background:var(--colors-surface-card);font-size:14px;font-weight:500;line-height:1;text-decoration:none;white-space:nowrap}.document-frame-wrap{position:relative;height:min(68vh,620px);min-height:520px;border:1px solid var(--colors-hairline);border-radius:var(--rounded-lg);overflow:hidden;background:var(--colors-surface-card)}.document-frame-wrap iframe{width:100%;height:100%;border:0;background:var(--colors-surface-card)}.document-placeholder{display:grid;height:100%;min-height:520px;place-items:center;align-content:center;gap:8px;padding:24px;color:var(--colors-body);text-align:center}.document-placeholder strong{color:var(--colors-ink);font-size:22px;font-weight:400;letter-spacing:-.11px;line-height:1.3}.action-dock{flex:0 0 292px;display:grid;align-content:start;gap:12px}.action-card,.case-summary{border:1px solid var(--colors-hairline);border-radius:var(--rounded-lg);background:var(--colors-surface-card)}.action-card{flex-direction:column;gap:12px;padding:16px}.action-card.complete{background:var(--colors-canvas-soft)}.action-icon.amber{color:var(--colors-ink);background:var(--colors-canvas-soft)}.green-button,.amber-button{min-height:40px;border-radius:var(--rounded-md);color:var(--colors-on-primary);background:var(--colors-primary);font-size:14px;font-weight:500;line-height:1}.action-card.complete .green-button,.action-card.complete .amber-button{color:var(--colors-body);background:var(--colors-surface-strong)}.case-summary{padding:16px}.case-summary div{align-items:center;gap:8px;margin-bottom:10px;color:var(--colors-muted);font-size:11px;font-weight:600;letter-spacing:.88px;line-height:1.4;text-transform:uppercase}.case-summary p{margin-top:4px;font-family:var(--font-mono);font-size:13px;overflow-wrap:anywhere}@media(max-width:1180px){.workspace{grid-template-columns:1fr}.portal-grid{grid-template-columns:minmax(0,1fr)}.action-dock{grid-template-columns:repeat(2,minmax(0,1fr))}}@media(max-width:760px){.app-shell{width:min(100% - 18px,1200px);padding-top:8px;padding-bottom:48px}.topbar,.hero-strip,.state-band,.document-toolbar{align-items:stretch;flex-direction:column}.topbar{min-height:auto;padding-bottom:16px}.brand-lockup p,.panel-header p{max-width:280px}.top-actions,.hero-stats,.state-band-steps{justify-content:stretch}.connection-pill,.secondary-button,.primary-button,.metric,.state-chip,.open-link{width:100%}.hero-strip h2{max-width:10ch;font-size:29px;letter-spacing:-.64px;line-height:1.12}.hr-panel,.employee-panel{padding:16px}.employee-card{grid-template-columns:auto minmax(0,1fr);align-items:flex-start}.employee-meta{grid-column:2;margin-left:0;text-align:left}.step-row{grid-template-columns:auto minmax(0,1fr)}.timeline-pill{justify-self:start}.action-dock{grid-template-columns:1fr}.document-frame-wrap,.document-placeholder{min-height:520px}.document-frame-wrap iframe{height:520px}} diff --git a/agents/adk/new-hire-onboarding/app/static/live-onboarding/assets/index-GxbLCVag.js b/agents/adk/new-hire-onboarding/app/static/live-onboarding/assets/index-GxbLCVag.js new file mode 100644 index 0000000..01f2dd7 --- /dev/null +++ b/agents/adk/new-hire-onboarding/app/static/live-onboarding/assets/index-GxbLCVag.js @@ -0,0 +1,192 @@ +/** + * @license + * Copyright 2026 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +(function () { const _ = document.createElement("link").relList; if (_ && _.supports && _.supports("modulepreload")) return; for (const q of document.querySelectorAll('link[rel="modulepreload"]')) y(q); new MutationObserver(q => { for (const G of q) if (G.type === "childList") for (const P of G.addedNodes) P.tagName === "LINK" && P.rel === "modulepreload" && y(P) }).observe(document, { childList: !0, subtree: !0 }); function N(q) { const G = {}; return q.integrity && (G.integrity = q.integrity), q.referrerPolicy && (G.referrerPolicy = q.referrerPolicy), q.crossOrigin === "use-credentials" ? G.credentials = "include" : q.crossOrigin === "anonymous" ? G.credentials = "omit" : G.credentials = "same-origin", G } function y(q) { if (q.ep) return; q.ep = !0; const G = N(q); fetch(q.href, G) } })(); function iy(f) { return f && f.__esModule && Object.prototype.hasOwnProperty.call(f, "default") ? f.default : f } var hf = { exports: {} }, pu = {};/** + * @license React + * react-jsx-runtime.production.js + * + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */var zh; function cy() { if (zh) return pu; zh = 1; var f = Symbol.for("react.transitional.element"), _ = Symbol.for("react.fragment"); function N(y, q, G) { var P = null; if (G !== void 0 && (P = "" + G), q.key !== void 0 && (P = "" + q.key), "key" in q) { G = {}; for (var vl in q) vl !== "key" && (G[vl] = q[vl]) } else G = q; return q = G.ref, { $$typeof: f, type: y, key: P, ref: q !== void 0 ? q : null, props: G } } return pu.Fragment = _, pu.jsx = N, pu.jsxs = N, pu } var Eh; function fy() { return Eh || (Eh = 1, hf.exports = cy()), hf.exports } var v = fy(), of = { exports: {} }, X = {};/** + * @license React + * react.production.js + * + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */var Th; function sy() { if (Th) return X; Th = 1; var f = Symbol.for("react.transitional.element"), _ = Symbol.for("react.portal"), N = Symbol.for("react.fragment"), y = Symbol.for("react.strict_mode"), q = Symbol.for("react.profiler"), G = Symbol.for("react.consumer"), P = Symbol.for("react.context"), vl = Symbol.for("react.forward_ref"), R = Symbol.for("react.suspense"), T = Symbol.for("react.memo"), V = Symbol.for("react.lazy"), C = Symbol.for("react.activity"), ul = Symbol.iterator; function Nl(h) { return h === null || typeof h != "object" ? null : (h = ul && h[ul] || h["@@iterator"], typeof h == "function" ? h : null) } var Gl = { isMounted: function () { return !1 }, enqueueForceUpdate: function () { }, enqueueReplaceState: function () { }, enqueueSetState: function () { } }, Hl = Object.assign, zt = {}; function Zl(h, A, O) { this.props = h, this.context = A, this.refs = zt, this.updater = O || Gl } Zl.prototype.isReactComponent = {}, Zl.prototype.setState = function (h, A) { if (typeof h != "object" && typeof h != "function" && h != null) throw Error("takes an object of state variables to update or a function which returns an object of state variables."); this.updater.enqueueSetState(this, h, A, "setState") }, Zl.prototype.forceUpdate = function (h) { this.updater.enqueueForceUpdate(this, h, "forceUpdate") }; function Mt() { } Mt.prototype = Zl.prototype; function _l(h, A, O) { this.props = h, this.context = A, this.refs = zt, this.updater = O || Gl } var Kl = _l.prototype = new Mt; Kl.constructor = _l, Hl(Kl, Zl.prototype), Kl.isPureReactComponent = !0; var dt = Array.isArray; function Xl() { } var j = { H: null, A: null, T: null, S: null }, sl = Object.prototype.hasOwnProperty; function Rl(h, A, O) { var x = O.ref; return { $$typeof: f, type: h, key: A, ref: x !== void 0 ? x : null, props: O } } function Ft(h, A) { return Rl(h.type, A, h.props) } function Ot(h) { return typeof h == "object" && h !== null && h.$$typeof === f } function Jl(h) { var A = { "=": "=0", ":": "=2" }; return "$" + h.replace(/[=:]/g, function (O) { return A[O] }) } var Te = /\/+/g; function Ht(h, A) { return typeof h == "object" && h !== null && h.key != null ? Jl("" + h.key) : A.toString(36) } function Et(h) { switch (h.status) { case "fulfilled": return h.value; case "rejected": throw h.reason; default: switch (typeof h.status == "string" ? h.then(Xl, Xl) : (h.status = "pending", h.then(function (A) { h.status === "pending" && (h.status = "fulfilled", h.value = A) }, function (A) { h.status === "pending" && (h.status = "rejected", h.reason = A) })), h.status) { case "fulfilled": return h.value; case "rejected": throw h.reason } }throw h } function p(h, A, O, x, Q) { var K = typeof h; (K === "undefined" || K === "boolean") && (h = null); var tl = !1; if (h === null) tl = !0; else switch (K) { case "bigint": case "string": case "number": tl = !0; break; case "object": switch (h.$$typeof) { case f: case _: tl = !0; break; case V: return tl = h._init, p(tl(h._payload), A, O, x, Q) } }if (tl) return Q = Q(h), tl = x === "" ? "." + Ht(h, 0) : x, dt(Q) ? (O = "", tl != null && (O = tl.replace(Te, "$&/") + "/"), p(Q, A, O, "", function (Oa) { return Oa })) : Q != null && (Ot(Q) && (Q = Ft(Q, O + (Q.key == null || h && h.key === Q.key ? "" : ("" + Q.key).replace(Te, "$&/") + "/") + tl)), A.push(Q)), 1; tl = 0; var Ll = x === "" ? "." : x + ":"; if (dt(h)) for (var Sl = 0; Sl < h.length; Sl++)x = h[Sl], K = Ll + Ht(x, Sl), tl += p(x, A, O, K, Q); else if (Sl = Nl(h), typeof Sl == "function") for (h = Sl.call(h), Sl = 0; !(x = h.next()).done;)x = x.value, K = Ll + Ht(x, Sl++), tl += p(x, A, O, K, Q); else if (K === "object") { if (typeof h.then == "function") return p(Et(h), A, O, x, Q); throw A = String(h), Error("Objects are not valid as a React child (found: " + (A === "[object Object]" ? "object with keys {" + Object.keys(h).join(", ") + "}" : A) + "). If you meant to render a collection of children, use an array instead.") } return tl } function M(h, A, O) { if (h == null) return h; var x = [], Q = 0; return p(h, x, "", "", function (K) { return A.call(O, K, Q++) }), x } function Y(h) { if (h._status === -1) { var A = h._result; A = A(), A.then(function (O) { (h._status === 0 || h._status === -1) && (h._status = 1, h._result = O) }, function (O) { (h._status === 0 || h._status === -1) && (h._status = 2, h._result = O) }), h._status === -1 && (h._status = 0, h._result = A) } if (h._status === 1) return h._result.default; throw h._result } var nl = typeof reportError == "function" ? reportError : function (h) { if (typeof window == "object" && typeof window.ErrorEvent == "function") { var A = new window.ErrorEvent("error", { bubbles: !0, cancelable: !0, message: typeof h == "object" && h !== null && typeof h.message == "string" ? String(h.message) : String(h), error: h }); if (!window.dispatchEvent(A)) return } else if (typeof process == "object" && typeof process.emit == "function") { process.emit("uncaughtException", h); return } console.error(h) }, dl = { map: M, forEach: function (h, A, O) { M(h, function () { A.apply(this, arguments) }, O) }, count: function (h) { var A = 0; return M(h, function () { A++ }), A }, toArray: function (h) { return M(h, function (A) { return A }) || [] }, only: function (h) { if (!Ot(h)) throw Error("React.Children.only expected to receive a single React element child."); return h } }; return X.Activity = C, X.Children = dl, X.Component = Zl, X.Fragment = N, X.Profiler = q, X.PureComponent = _l, X.StrictMode = y, X.Suspense = R, X.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE = j, X.__COMPILER_RUNTIME = { __proto__: null, c: function (h) { return j.H.useMemoCache(h) } }, X.cache = function (h) { return function () { return h.apply(null, arguments) } }, X.cacheSignal = function () { return null }, X.cloneElement = function (h, A, O) { if (h == null) throw Error("The argument must be a React element, but you passed " + h + "."); var x = Hl({}, h.props), Q = h.key; if (A != null) for (K in A.key !== void 0 && (Q = "" + A.key), A) !sl.call(A, K) || K === "key" || K === "__self" || K === "__source" || K === "ref" && A.ref === void 0 || (x[K] = A[K]); var K = arguments.length - 2; if (K === 1) x.children = O; else if (1 < K) { for (var tl = Array(K), Ll = 0; Ll < K; Ll++)tl[Ll] = arguments[Ll + 2]; x.children = tl } return Rl(h.type, Q, x) }, X.createContext = function (h) { return h = { $$typeof: P, _currentValue: h, _currentValue2: h, _threadCount: 0, Provider: null, Consumer: null }, h.Provider = h, h.Consumer = { $$typeof: G, _context: h }, h }, X.createElement = function (h, A, O) { var x, Q = {}, K = null; if (A != null) for (x in A.key !== void 0 && (K = "" + A.key), A) sl.call(A, x) && x !== "key" && x !== "__self" && x !== "__source" && (Q[x] = A[x]); var tl = arguments.length - 2; if (tl === 1) Q.children = O; else if (1 < tl) { for (var Ll = Array(tl), Sl = 0; Sl < tl; Sl++)Ll[Sl] = arguments[Sl + 2]; Q.children = Ll } if (h && h.defaultProps) for (x in tl = h.defaultProps, tl) Q[x] === void 0 && (Q[x] = tl[x]); return Rl(h, K, Q) }, X.createRef = function () { return { current: null } }, X.forwardRef = function (h) { return { $$typeof: vl, render: h } }, X.isValidElement = Ot, X.lazy = function (h) { return { $$typeof: V, _payload: { _status: -1, _result: h }, _init: Y } }, X.memo = function (h, A) { return { $$typeof: T, type: h, compare: A === void 0 ? null : A } }, X.startTransition = function (h) { var A = j.T, O = {}; j.T = O; try { var x = h(), Q = j.S; Q !== null && Q(O, x), typeof x == "object" && x !== null && typeof x.then == "function" && x.then(Xl, nl) } catch (K) { nl(K) } finally { A !== null && O.types !== null && (A.types = O.types), j.T = A } }, X.unstable_useCacheRefresh = function () { return j.H.useCacheRefresh() }, X.use = function (h) { return j.H.use(h) }, X.useActionState = function (h, A, O) { return j.H.useActionState(h, A, O) }, X.useCallback = function (h, A) { return j.H.useCallback(h, A) }, X.useContext = function (h) { return j.H.useContext(h) }, X.useDebugValue = function () { }, X.useDeferredValue = function (h, A) { return j.H.useDeferredValue(h, A) }, X.useEffect = function (h, A) { return j.H.useEffect(h, A) }, X.useEffectEvent = function (h) { return j.H.useEffectEvent(h) }, X.useId = function () { return j.H.useId() }, X.useImperativeHandle = function (h, A, O) { return j.H.useImperativeHandle(h, A, O) }, X.useInsertionEffect = function (h, A) { return j.H.useInsertionEffect(h, A) }, X.useLayoutEffect = function (h, A) { return j.H.useLayoutEffect(h, A) }, X.useMemo = function (h, A) { return j.H.useMemo(h, A) }, X.useOptimistic = function (h, A) { return j.H.useOptimistic(h, A) }, X.useReducer = function (h, A, O) { return j.H.useReducer(h, A, O) }, X.useRef = function (h) { return j.H.useRef(h) }, X.useState = function (h) { return j.H.useState(h) }, X.useSyncExternalStore = function (h, A, O) { return j.H.useSyncExternalStore(h, A, O) }, X.useTransition = function () { return j.H.useTransition() }, X.version = "19.2.6", X } var Ah; function Sf() { return Ah || (Ah = 1, of.exports = sy()), of.exports } var xl = Sf(); const dy = iy(xl); var yf = { exports: {} }, zu = {}, mf = { exports: {} }, vf = {};/** + * @license React + * scheduler.production.js + * + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */var _h; function hy() { return _h || (_h = 1, (function (f) { function _(p, M) { var Y = p.length; p.push(M); l: for (; 0 < Y;) { var nl = Y - 1 >>> 1, dl = p[nl]; if (0 < q(dl, M)) p[nl] = M, p[Y] = dl, Y = nl; else break l } } function N(p) { return p.length === 0 ? null : p[0] } function y(p) { if (p.length === 0) return null; var M = p[0], Y = p.pop(); if (Y !== M) { p[0] = Y; l: for (var nl = 0, dl = p.length, h = dl >>> 1; nl < h;) { var A = 2 * (nl + 1) - 1, O = p[A], x = A + 1, Q = p[x]; if (0 > q(O, Y)) x < dl && 0 > q(Q, O) ? (p[nl] = Q, p[x] = Y, nl = x) : (p[nl] = O, p[A] = Y, nl = A); else if (x < dl && 0 > q(Q, Y)) p[nl] = Q, p[x] = Y, nl = x; else break l } } return M } function q(p, M) { var Y = p.sortIndex - M.sortIndex; return Y !== 0 ? Y : p.id - M.id } if (f.unstable_now = void 0, typeof performance == "object" && typeof performance.now == "function") { var G = performance; f.unstable_now = function () { return G.now() } } else { var P = Date, vl = P.now(); f.unstable_now = function () { return P.now() - vl } } var R = [], T = [], V = 1, C = null, ul = 3, Nl = !1, Gl = !1, Hl = !1, zt = !1, Zl = typeof setTimeout == "function" ? setTimeout : null, Mt = typeof clearTimeout == "function" ? clearTimeout : null, _l = typeof setImmediate < "u" ? setImmediate : null; function Kl(p) { for (var M = N(T); M !== null;) { if (M.callback === null) y(T); else if (M.startTime <= p) y(T), M.sortIndex = M.expirationTime, _(R, M); else break; M = N(T) } } function dt(p) { if (Hl = !1, Kl(p), !Gl) if (N(R) !== null) Gl = !0, Xl || (Xl = !0, Jl()); else { var M = N(T); M !== null && Et(dt, M.startTime - p) } } var Xl = !1, j = -1, sl = 5, Rl = -1; function Ft() { return zt ? !0 : !(f.unstable_now() - Rl < sl) } function Ot() { if (zt = !1, Xl) { var p = f.unstable_now(); Rl = p; var M = !0; try { l: { Gl = !1, Hl && (Hl = !1, Mt(j), j = -1), Nl = !0; var Y = ul; try { t: { for (Kl(p), C = N(R); C !== null && !(C.expirationTime > p && Ft());) { var nl = C.callback; if (typeof nl == "function") { C.callback = null, ul = C.priorityLevel; var dl = nl(C.expirationTime <= p); if (p = f.unstable_now(), typeof dl == "function") { C.callback = dl, Kl(p), M = !0; break t } C === N(R) && y(R), Kl(p) } else y(R); C = N(R) } if (C !== null) M = !0; else { var h = N(T); h !== null && Et(dt, h.startTime - p), M = !1 } } break l } finally { C = null, ul = Y, Nl = !1 } M = void 0 } } finally { M ? Jl() : Xl = !1 } } } var Jl; if (typeof _l == "function") Jl = function () { _l(Ot) }; else if (typeof MessageChannel < "u") { var Te = new MessageChannel, Ht = Te.port2; Te.port1.onmessage = Ot, Jl = function () { Ht.postMessage(null) } } else Jl = function () { Zl(Ot, 0) }; function Et(p, M) { j = Zl(function () { p(f.unstable_now()) }, M) } f.unstable_IdlePriority = 5, f.unstable_ImmediatePriority = 1, f.unstable_LowPriority = 4, f.unstable_NormalPriority = 3, f.unstable_Profiling = null, f.unstable_UserBlockingPriority = 2, f.unstable_cancelCallback = function (p) { p.callback = null }, f.unstable_forceFrameRate = function (p) { 0 > p || 125 < p ? console.error("forceFrameRate takes a positive int between 0 and 125, forcing frame rates higher than 125 fps is not supported") : sl = 0 < p ? Math.floor(1e3 / p) : 5 }, f.unstable_getCurrentPriorityLevel = function () { return ul }, f.unstable_next = function (p) { switch (ul) { case 1: case 2: case 3: var M = 3; break; default: M = ul }var Y = ul; ul = M; try { return p() } finally { ul = Y } }, f.unstable_requestPaint = function () { zt = !0 }, f.unstable_runWithPriority = function (p, M) { switch (p) { case 1: case 2: case 3: case 4: case 5: break; default: p = 3 }var Y = ul; ul = p; try { return M() } finally { ul = Y } }, f.unstable_scheduleCallback = function (p, M, Y) { var nl = f.unstable_now(); switch (typeof Y == "object" && Y !== null ? (Y = Y.delay, Y = typeof Y == "number" && 0 < Y ? nl + Y : nl) : Y = nl, p) { case 1: var dl = -1; break; case 2: dl = 250; break; case 5: dl = 1073741823; break; case 4: dl = 1e4; break; default: dl = 5e3 }return dl = Y + dl, p = { id: V++, callback: M, priorityLevel: p, startTime: Y, expirationTime: dl, sortIndex: -1 }, Y > nl ? (p.sortIndex = Y, _(T, p), N(R) === null && p === N(T) && (Hl ? (Mt(j), j = -1) : Hl = !0, Et(dt, Y - nl))) : (p.sortIndex = dl, _(R, p), Gl || Nl || (Gl = !0, Xl || (Xl = !0, Jl()))), p }, f.unstable_shouldYield = Ft, f.unstable_wrapCallback = function (p) { var M = ul; return function () { var Y = ul; ul = M; try { return p.apply(this, arguments) } finally { ul = Y } } } })(vf)), vf } var Mh; function oy() { return Mh || (Mh = 1, mf.exports = hy()), mf.exports } var rf = { exports: {} }, Ql = {};/** + * @license React + * react-dom.production.js + * + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */var Oh; function yy() { if (Oh) return Ql; Oh = 1; var f = Sf(); function _(R) { var T = "https://react.dev/errors/" + R; if (1 < arguments.length) { T += "?args[]=" + encodeURIComponent(arguments[1]); for (var V = 2; V < arguments.length; V++)T += "&args[]=" + encodeURIComponent(arguments[V]) } return "Minified React error #" + R + "; visit " + T + " for the full message or use the non-minified dev environment for full errors and additional helpful warnings." } function N() { } var y = { d: { f: N, r: function () { throw Error(_(522)) }, D: N, C: N, L: N, m: N, X: N, S: N, M: N }, p: 0, findDOMNode: null }, q = Symbol.for("react.portal"); function G(R, T, V) { var C = 3 < arguments.length && arguments[3] !== void 0 ? arguments[3] : null; return { $$typeof: q, key: C == null ? null : "" + C, children: R, containerInfo: T, implementation: V } } var P = f.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE; function vl(R, T) { if (R === "font") return ""; if (typeof T == "string") return T === "use-credentials" ? T : "" } return Ql.__DOM_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE = y, Ql.createPortal = function (R, T) { var V = 2 < arguments.length && arguments[2] !== void 0 ? arguments[2] : null; if (!T || T.nodeType !== 1 && T.nodeType !== 9 && T.nodeType !== 11) throw Error(_(299)); return G(R, T, null, V) }, Ql.flushSync = function (R) { var T = P.T, V = y.p; try { if (P.T = null, y.p = 2, R) return R() } finally { P.T = T, y.p = V, y.d.f() } }, Ql.preconnect = function (R, T) { typeof R == "string" && (T ? (T = T.crossOrigin, T = typeof T == "string" ? T === "use-credentials" ? T : "" : void 0) : T = null, y.d.C(R, T)) }, Ql.prefetchDNS = function (R) { typeof R == "string" && y.d.D(R) }, Ql.preinit = function (R, T) { if (typeof R == "string" && T && typeof T.as == "string") { var V = T.as, C = vl(V, T.crossOrigin), ul = typeof T.integrity == "string" ? T.integrity : void 0, Nl = typeof T.fetchPriority == "string" ? T.fetchPriority : void 0; V === "style" ? y.d.S(R, typeof T.precedence == "string" ? T.precedence : void 0, { crossOrigin: C, integrity: ul, fetchPriority: Nl }) : V === "script" && y.d.X(R, { crossOrigin: C, integrity: ul, fetchPriority: Nl, nonce: typeof T.nonce == "string" ? T.nonce : void 0 }) } }, Ql.preinitModule = function (R, T) { if (typeof R == "string") if (typeof T == "object" && T !== null) { if (T.as == null || T.as === "script") { var V = vl(T.as, T.crossOrigin); y.d.M(R, { crossOrigin: V, integrity: typeof T.integrity == "string" ? T.integrity : void 0, nonce: typeof T.nonce == "string" ? T.nonce : void 0 }) } } else T == null && y.d.M(R) }, Ql.preload = function (R, T) { if (typeof R == "string" && typeof T == "object" && T !== null && typeof T.as == "string") { var V = T.as, C = vl(V, T.crossOrigin); y.d.L(R, V, { crossOrigin: C, integrity: typeof T.integrity == "string" ? T.integrity : void 0, nonce: typeof T.nonce == "string" ? T.nonce : void 0, type: typeof T.type == "string" ? T.type : void 0, fetchPriority: typeof T.fetchPriority == "string" ? T.fetchPriority : void 0, referrerPolicy: typeof T.referrerPolicy == "string" ? T.referrerPolicy : void 0, imageSrcSet: typeof T.imageSrcSet == "string" ? T.imageSrcSet : void 0, imageSizes: typeof T.imageSizes == "string" ? T.imageSizes : void 0, media: typeof T.media == "string" ? T.media : void 0 }) } }, Ql.preloadModule = function (R, T) { if (typeof R == "string") if (T) { var V = vl(T.as, T.crossOrigin); y.d.m(R, { as: typeof T.as == "string" && T.as !== "script" ? T.as : void 0, crossOrigin: V, integrity: typeof T.integrity == "string" ? T.integrity : void 0 }) } else y.d.m(R) }, Ql.requestFormReset = function (R) { y.d.r(R) }, Ql.unstable_batchedUpdates = function (R, T) { return R(T) }, Ql.useFormState = function (R, T, V) { return P.H.useFormState(R, T, V) }, Ql.useFormStatus = function () { return P.H.useHostTransitionStatus() }, Ql.version = "19.2.6", Ql } var Nh; function my() { if (Nh) return rf.exports; Nh = 1; function f() { if (!(typeof __REACT_DEVTOOLS_GLOBAL_HOOK__ > "u" || typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE != "function")) try { __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(f) } catch (_) { console.error(_) } } return f(), rf.exports = yy(), rf.exports }/** + * @license React + * react-dom-client.production.js + * + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */var jh; function vy() { + if (jh) return zu; jh = 1; var f = oy(), _ = Sf(), N = my(); function y(l) { var t = "https://react.dev/errors/" + l; if (1 < arguments.length) { t += "?args[]=" + encodeURIComponent(arguments[1]); for (var e = 2; e < arguments.length; e++)t += "&args[]=" + encodeURIComponent(arguments[e]) } return "Minified React error #" + l + "; visit " + t + " for the full message or use the non-minified dev environment for full errors and additional helpful warnings." } function q(l) { return !(!l || l.nodeType !== 1 && l.nodeType !== 9 && l.nodeType !== 11) } function G(l) { var t = l, e = l; if (l.alternate) for (; t.return;)t = t.return; else { l = t; do t = l, (t.flags & 4098) !== 0 && (e = t.return), l = t.return; while (l) } return t.tag === 3 ? e : null } function P(l) { if (l.tag === 13) { var t = l.memoizedState; if (t === null && (l = l.alternate, l !== null && (t = l.memoizedState)), t !== null) return t.dehydrated } return null } function vl(l) { if (l.tag === 31) { var t = l.memoizedState; if (t === null && (l = l.alternate, l !== null && (t = l.memoizedState)), t !== null) return t.dehydrated } return null } function R(l) { if (G(l) !== l) throw Error(y(188)) } function T(l) { var t = l.alternate; if (!t) { if (t = G(l), t === null) throw Error(y(188)); return t !== l ? null : l } for (var e = l, a = t; ;) { var u = e.return; if (u === null) break; var n = u.alternate; if (n === null) { if (a = u.return, a !== null) { e = a; continue } break } if (u.child === n.child) { for (n = u.child; n;) { if (n === e) return R(u), l; if (n === a) return R(u), t; n = n.sibling } throw Error(y(188)) } if (e.return !== a.return) e = u, a = n; else { for (var i = !1, c = u.child; c;) { if (c === e) { i = !0, e = u, a = n; break } if (c === a) { i = !0, a = u, e = n; break } c = c.sibling } if (!i) { for (c = n.child; c;) { if (c === e) { i = !0, e = n, a = u; break } if (c === a) { i = !0, a = n, e = u; break } c = c.sibling } if (!i) throw Error(y(189)) } } if (e.alternate !== a) throw Error(y(190)) } if (e.tag !== 3) throw Error(y(188)); return e.stateNode.current === e ? l : t } function V(l) { var t = l.tag; if (t === 5 || t === 26 || t === 27 || t === 6) return l; for (l = l.child; l !== null;) { if (t = V(l), t !== null) return t; l = l.sibling } return null } var C = Object.assign, ul = Symbol.for("react.element"), Nl = Symbol.for("react.transitional.element"), Gl = Symbol.for("react.portal"), Hl = Symbol.for("react.fragment"), zt = Symbol.for("react.strict_mode"), Zl = Symbol.for("react.profiler"), Mt = Symbol.for("react.consumer"), _l = Symbol.for("react.context"), Kl = Symbol.for("react.forward_ref"), dt = Symbol.for("react.suspense"), Xl = Symbol.for("react.suspense_list"), j = Symbol.for("react.memo"), sl = Symbol.for("react.lazy"), Rl = Symbol.for("react.activity"), Ft = Symbol.for("react.memo_cache_sentinel"), Ot = Symbol.iterator; function Jl(l) { return l === null || typeof l != "object" ? null : (l = Ot && l[Ot] || l["@@iterator"], typeof l == "function" ? l : null) } var Te = Symbol.for("react.client.reference"); function Ht(l) { if (l == null) return null; if (typeof l == "function") return l.$$typeof === Te ? null : l.displayName || l.name || null; if (typeof l == "string") return l; switch (l) { case Hl: return "Fragment"; case Zl: return "Profiler"; case zt: return "StrictMode"; case dt: return "Suspense"; case Xl: return "SuspenseList"; case Rl: return "Activity" }if (typeof l == "object") switch (l.$$typeof) { case Gl: return "Portal"; case _l: return l.displayName || "Context"; case Mt: return (l._context.displayName || "Context") + ".Consumer"; case Kl: var t = l.render; return l = l.displayName, l || (l = t.displayName || t.name || "", l = l !== "" ? "ForwardRef(" + l + ")" : "ForwardRef"), l; case j: return t = l.displayName || null, t !== null ? t : Ht(l.type) || "Memo"; case sl: t = l._payload, l = l._init; try { return Ht(l(t)) } catch { } }return null } var Et = Array.isArray, p = _.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE, M = N.__DOM_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE, Y = { pending: !1, data: null, method: null, action: null }, nl = [], dl = -1; function h(l) { return { current: l } } function A(l) { 0 > dl || (l.current = nl[dl], nl[dl] = null, dl--) } function O(l, t) { dl++, nl[dl] = l.current, l.current = t } var x = h(null), Q = h(null), K = h(null), tl = h(null); function Ll(l, t) { switch (O(K, t), O(Q, l), O(x, null), t.nodeType) { case 9: case 11: l = (l = t.documentElement) && (l = l.namespaceURI) ? V0(l) : 0; break; default: if (l = t.tagName, t = t.namespaceURI) t = V0(t), l = K0(t, l); else switch (l) { case "svg": l = 1; break; case "math": l = 2; break; default: l = 0 } }A(x), O(x, l) } function Sl() { A(x), A(Q), A(K) } function Oa(l) { l.memoizedState !== null && O(tl, l); var t = x.current, e = K0(t, l.type); t !== e && (O(Q, l), O(x, e)) } function Au(l) { Q.current === l && (A(x), A(Q)), tl.current === l && (A(tl), ru._currentValue = Y) } var Jn, pf; function Ae(l) { + if (Jn === void 0) try { throw Error() } catch (e) { + var t = e.stack.trim().match(/\n( *(at )?)/); Jn = t && t[1] || "", pf = -1 < e.stack.indexOf(` + at`) ? " ()" : -1 < e.stack.indexOf("@") ? "@unknown:0:0" : "" + } return ` +`+ Jn + l + pf + } var wn = !1; function kn(l, t) { + if (!l || wn) return ""; wn = !0; var e = Error.prepareStackTrace; Error.prepareStackTrace = void 0; try { + var a = { DetermineComponentFrameRoot: function () { try { if (t) { var E = function () { throw Error() }; if (Object.defineProperty(E.prototype, "props", { set: function () { throw Error() } }), typeof Reflect == "object" && Reflect.construct) { try { Reflect.construct(E, []) } catch (S) { var g = S } Reflect.construct(l, [], E) } else { try { E.call() } catch (S) { g = S } l.call(E.prototype) } } else { try { throw Error() } catch (S) { g = S } (E = l()) && typeof E.catch == "function" && E.catch(function () { }) } } catch (S) { if (S && g && typeof S.stack == "string") return [S.stack, g.stack] } return [null, null] } }; a.DetermineComponentFrameRoot.displayName = "DetermineComponentFrameRoot"; var u = Object.getOwnPropertyDescriptor(a.DetermineComponentFrameRoot, "name"); u && u.configurable && Object.defineProperty(a.DetermineComponentFrameRoot, "name", { value: "DetermineComponentFrameRoot" }); var n = a.DetermineComponentFrameRoot(), i = n[0], c = n[1]; if (i && c) { + var s = i.split(` +`), r = c.split(` +`); for (u = a = 0; a < s.length && !s[a].includes("DetermineComponentFrameRoot");)a++; for (; u < r.length && !r[u].includes("DetermineComponentFrameRoot");)u++; if (a === s.length || u === r.length) for (a = s.length - 1, u = r.length - 1; 1 <= a && 0 <= u && s[a] !== r[u];)u--; for (; 1 <= a && 0 <= u; a--, u--)if (s[a] !== r[u]) { + if (a !== 1 || u !== 1) do if (a--, u--, 0 > u || s[a] !== r[u]) { + var b = ` +`+ s[a].replace(" at new ", " at "); return l.displayName && b.includes("") && (b = b.replace("", l.displayName)), b + } while (1 <= a && 0 <= u); break + } + } + } finally { wn = !1, Error.prepareStackTrace = e } return (e = l ? l.displayName || l.name : "") ? Ae(e) : "" + } function qh(l, t) { switch (l.tag) { case 26: case 27: case 5: return Ae(l.type); case 16: return Ae("Lazy"); case 13: return l.child !== t && t !== null ? Ae("Suspense Fallback") : Ae("Suspense"); case 19: return Ae("SuspenseList"); case 0: case 15: return kn(l.type, !1); case 11: return kn(l.type.render, !1); case 1: return kn(l.type, !0); case 31: return Ae("Activity"); default: return "" } } function zf(l) { + try { var t = "", e = null; do t += qh(l, e), e = l, l = l.return; while (l); return t } catch (a) { + return ` +Error generating stack: `+ a.message + ` +`+ a.stack + } + } var Wn = Object.prototype.hasOwnProperty, $n = f.unstable_scheduleCallback, Fn = f.unstable_cancelCallback, Bh = f.unstable_shouldYield, Yh = f.unstable_requestPaint, lt = f.unstable_now, Gh = f.unstable_getCurrentPriorityLevel, Ef = f.unstable_ImmediatePriority, Tf = f.unstable_UserBlockingPriority, _u = f.unstable_NormalPriority, Xh = f.unstable_LowPriority, Af = f.unstable_IdlePriority, Qh = f.log, Zh = f.unstable_setDisableYieldValue, Na = null, tt = null; function It(l) { if (typeof Qh == "function" && Zh(l), tt && typeof tt.setStrictMode == "function") try { tt.setStrictMode(Na, l) } catch { } } var et = Math.clz32 ? Math.clz32 : Kh, Lh = Math.log, Vh = Math.LN2; function Kh(l) { return l >>>= 0, l === 0 ? 32 : 31 - (Lh(l) / Vh | 0) | 0 } var Mu = 256, Ou = 262144, Nu = 4194304; function _e(l) { var t = l & 42; if (t !== 0) return t; switch (l & -l) { case 1: return 1; case 2: return 2; case 4: return 4; case 8: return 8; case 16: return 16; case 32: return 32; case 64: return 64; case 128: return 128; case 256: case 512: case 1024: case 2048: case 4096: case 8192: case 16384: case 32768: case 65536: case 131072: return l & 261888; case 262144: case 524288: case 1048576: case 2097152: return l & 3932160; case 4194304: case 8388608: case 16777216: case 33554432: return l & 62914560; case 67108864: return 67108864; case 134217728: return 134217728; case 268435456: return 268435456; case 536870912: return 536870912; case 1073741824: return 0; default: return l } } function ju(l, t, e) { var a = l.pendingLanes; if (a === 0) return 0; var u = 0, n = l.suspendedLanes, i = l.pingedLanes; l = l.warmLanes; var c = a & 134217727; return c !== 0 ? (a = c & ~n, a !== 0 ? u = _e(a) : (i &= c, i !== 0 ? u = _e(i) : e || (e = c & ~l, e !== 0 && (u = _e(e))))) : (c = a & ~n, c !== 0 ? u = _e(c) : i !== 0 ? u = _e(i) : e || (e = a & ~l, e !== 0 && (u = _e(e)))), u === 0 ? 0 : t !== 0 && t !== u && (t & n) === 0 && (n = u & -u, e = t & -t, n >= e || n === 32 && (e & 4194048) !== 0) ? t : u } function ja(l, t) { return (l.pendingLanes & ~(l.suspendedLanes & ~l.pingedLanes) & t) === 0 } function Jh(l, t) { switch (l) { case 1: case 2: case 4: case 8: case 64: return t + 250; case 16: case 32: case 128: case 256: case 512: case 1024: case 2048: case 4096: case 8192: case 16384: case 32768: case 65536: case 131072: case 262144: case 524288: case 1048576: case 2097152: return t + 5e3; case 4194304: case 8388608: case 16777216: case 33554432: return -1; case 67108864: case 134217728: case 268435456: case 536870912: case 1073741824: return -1; default: return -1 } } function _f() { var l = Nu; return Nu <<= 1, (Nu & 62914560) === 0 && (Nu = 4194304), l } function In(l) { for (var t = [], e = 0; 31 > e; e++)t.push(l); return t } function Ua(l, t) { l.pendingLanes |= t, t !== 268435456 && (l.suspendedLanes = 0, l.pingedLanes = 0, l.warmLanes = 0) } function wh(l, t, e, a, u, n) { var i = l.pendingLanes; l.pendingLanes = e, l.suspendedLanes = 0, l.pingedLanes = 0, l.warmLanes = 0, l.expiredLanes &= e, l.entangledLanes &= e, l.errorRecoveryDisabledLanes &= e, l.shellSuspendCounter = 0; var c = l.entanglements, s = l.expirationTimes, r = l.hiddenUpdates; for (e = i & ~e; 0 < e;) { var b = 31 - et(e), E = 1 << b; c[b] = 0, s[b] = -1; var g = r[b]; if (g !== null) for (r[b] = null, b = 0; b < g.length; b++) { var S = g[b]; S !== null && (S.lane &= -536870913) } e &= ~E } a !== 0 && Mf(l, a, 0), n !== 0 && u === 0 && l.tag !== 0 && (l.suspendedLanes |= n & ~(i & ~t)) } function Mf(l, t, e) { l.pendingLanes |= t, l.suspendedLanes &= ~t; var a = 31 - et(t); l.entangledLanes |= t, l.entanglements[a] = l.entanglements[a] | 1073741824 | e & 261930 } function Of(l, t) { var e = l.entangledLanes |= t; for (l = l.entanglements; e;) { var a = 31 - et(e), u = 1 << a; u & t | l[a] & t && (l[a] |= t), e &= ~u } } function Nf(l, t) { var e = t & -t; return e = (e & 42) !== 0 ? 1 : Pn(e), (e & (l.suspendedLanes | t)) !== 0 ? 0 : e } function Pn(l) { switch (l) { case 2: l = 1; break; case 8: l = 4; break; case 32: l = 16; break; case 256: case 512: case 1024: case 2048: case 4096: case 8192: case 16384: case 32768: case 65536: case 131072: case 262144: case 524288: case 1048576: case 2097152: case 4194304: case 8388608: case 16777216: case 33554432: l = 128; break; case 268435456: l = 134217728; break; default: l = 0 }return l } function li(l) { return l &= -l, 2 < l ? 8 < l ? (l & 134217727) !== 0 ? 32 : 268435456 : 8 : 2 } function jf() { var l = M.p; return l !== 0 ? l : (l = window.event, l === void 0 ? 32 : mh(l.type)) } function Uf(l, t) { var e = M.p; try { return M.p = l, t() } finally { M.p = e } } var Pt = Math.random().toString(36).slice(2), Cl = "__reactFiber$" + Pt, wl = "__reactProps$" + Pt, Le = "__reactContainer$" + Pt, ti = "__reactEvents$" + Pt, kh = "__reactListeners$" + Pt, Wh = "__reactHandles$" + Pt, xf = "__reactResources$" + Pt, xa = "__reactMarker$" + Pt; function ei(l) { delete l[Cl], delete l[wl], delete l[ti], delete l[kh], delete l[Wh] } function Ve(l) { var t = l[Cl]; if (t) return t; for (var e = l.parentNode; e;) { if (t = e[Le] || e[Cl]) { if (e = t.alternate, t.child !== null || e !== null && e.child !== null) for (l = I0(l); l !== null;) { if (e = l[Cl]) return e; l = I0(l) } return t } l = e, e = l.parentNode } return null } function Ke(l) { if (l = l[Cl] || l[Le]) { var t = l.tag; if (t === 5 || t === 6 || t === 13 || t === 31 || t === 26 || t === 27 || t === 3) return l } return null } function Ha(l) { var t = l.tag; if (t === 5 || t === 26 || t === 27 || t === 6) return l.stateNode; throw Error(y(33)) } function Je(l) { var t = l[xf]; return t || (t = l[xf] = { hoistableStyles: new Map, hoistableScripts: new Map }), t } function jl(l) { l[xa] = !0 } var Hf = new Set, Rf = {}; function Me(l, t) { we(l, t), we(l + "Capture", t) } function we(l, t) { for (Rf[l] = t, l = 0; l < t.length; l++)Hf.add(t[l]) } var $h = RegExp("^[:A-Z_a-z\\u00C0-\\u00D6\\u00D8-\\u00F6\\u00F8-\\u02FF\\u0370-\\u037D\\u037F-\\u1FFF\\u200C-\\u200D\\u2070-\\u218F\\u2C00-\\u2FEF\\u3001-\\uD7FF\\uF900-\\uFDCF\\uFDF0-\\uFFFD][:A-Z_a-z\\u00C0-\\u00D6\\u00D8-\\u00F6\\u00F8-\\u02FF\\u0370-\\u037D\\u037F-\\u1FFF\\u200C-\\u200D\\u2070-\\u218F\\u2C00-\\u2FEF\\u3001-\\uD7FF\\uF900-\\uFDCF\\uFDF0-\\uFFFD\\-.0-9\\u00B7\\u0300-\\u036F\\u203F-\\u2040]*$"), Cf = {}, Df = {}; function Fh(l) { return Wn.call(Df, l) ? !0 : Wn.call(Cf, l) ? !1 : $h.test(l) ? Df[l] = !0 : (Cf[l] = !0, !1) } function Uu(l, t, e) { if (Fh(t)) if (e === null) l.removeAttribute(t); else { switch (typeof e) { case "undefined": case "function": case "symbol": l.removeAttribute(t); return; case "boolean": var a = t.toLowerCase().slice(0, 5); if (a !== "data-" && a !== "aria-") { l.removeAttribute(t); return } }l.setAttribute(t, "" + e) } } function xu(l, t, e) { if (e === null) l.removeAttribute(t); else { switch (typeof e) { case "undefined": case "function": case "symbol": case "boolean": l.removeAttribute(t); return }l.setAttribute(t, "" + e) } } function Rt(l, t, e, a) { if (a === null) l.removeAttribute(e); else { switch (typeof a) { case "undefined": case "function": case "symbol": case "boolean": l.removeAttribute(e); return }l.setAttributeNS(t, e, "" + a) } } function ht(l) { switch (typeof l) { case "bigint": case "boolean": case "number": case "string": case "undefined": return l; case "object": return l; default: return "" } } function qf(l) { var t = l.type; return (l = l.nodeName) && l.toLowerCase() === "input" && (t === "checkbox" || t === "radio") } function Ih(l, t, e) { var a = Object.getOwnPropertyDescriptor(l.constructor.prototype, t); if (!l.hasOwnProperty(t) && typeof a < "u" && typeof a.get == "function" && typeof a.set == "function") { var u = a.get, n = a.set; return Object.defineProperty(l, t, { configurable: !0, get: function () { return u.call(this) }, set: function (i) { e = "" + i, n.call(this, i) } }), Object.defineProperty(l, t, { enumerable: a.enumerable }), { getValue: function () { return e }, setValue: function (i) { e = "" + i }, stopTracking: function () { l._valueTracker = null, delete l[t] } } } } function ai(l) { if (!l._valueTracker) { var t = qf(l) ? "checked" : "value"; l._valueTracker = Ih(l, t, "" + l[t]) } } function Bf(l) { if (!l) return !1; var t = l._valueTracker; if (!t) return !0; var e = t.getValue(), a = ""; return l && (a = qf(l) ? l.checked ? "true" : "false" : l.value), l = a, l !== e ? (t.setValue(l), !0) : !1 } function Hu(l) { if (l = l || (typeof document < "u" ? document : void 0), typeof l > "u") return null; try { return l.activeElement || l.body } catch { return l.body } } var Ph = /[\n"\\]/g; function ot(l) { return l.replace(Ph, function (t) { return "\\" + t.charCodeAt(0).toString(16) + " " }) } function ui(l, t, e, a, u, n, i, c) { l.name = "", i != null && typeof i != "function" && typeof i != "symbol" && typeof i != "boolean" ? l.type = i : l.removeAttribute("type"), t != null ? i === "number" ? (t === 0 && l.value === "" || l.value != t) && (l.value = "" + ht(t)) : l.value !== "" + ht(t) && (l.value = "" + ht(t)) : i !== "submit" && i !== "reset" || l.removeAttribute("value"), t != null ? ni(l, i, ht(t)) : e != null ? ni(l, i, ht(e)) : a != null && l.removeAttribute("value"), u == null && n != null && (l.defaultChecked = !!n), u != null && (l.checked = u && typeof u != "function" && typeof u != "symbol"), c != null && typeof c != "function" && typeof c != "symbol" && typeof c != "boolean" ? l.name = "" + ht(c) : l.removeAttribute("name") } function Yf(l, t, e, a, u, n, i, c) { if (n != null && typeof n != "function" && typeof n != "symbol" && typeof n != "boolean" && (l.type = n), t != null || e != null) { if (!(n !== "submit" && n !== "reset" || t != null)) { ai(l); return } e = e != null ? "" + ht(e) : "", t = t != null ? "" + ht(t) : e, c || t === l.value || (l.value = t), l.defaultValue = t } a = a ?? u, a = typeof a != "function" && typeof a != "symbol" && !!a, l.checked = c ? l.checked : !!a, l.defaultChecked = !!a, i != null && typeof i != "function" && typeof i != "symbol" && typeof i != "boolean" && (l.name = i), ai(l) } function ni(l, t, e) { t === "number" && Hu(l.ownerDocument) === l || l.defaultValue === "" + e || (l.defaultValue = "" + e) } function ke(l, t, e, a) { if (l = l.options, t) { t = {}; for (var u = 0; u < e.length; u++)t["$" + e[u]] = !0; for (e = 0; e < l.length; e++)u = t.hasOwnProperty("$" + l[e].value), l[e].selected !== u && (l[e].selected = u), u && a && (l[e].defaultSelected = !0) } else { for (e = "" + ht(e), t = null, u = 0; u < l.length; u++) { if (l[u].value === e) { l[u].selected = !0, a && (l[u].defaultSelected = !0); return } t !== null || l[u].disabled || (t = l[u]) } t !== null && (t.selected = !0) } } function Gf(l, t, e) { if (t != null && (t = "" + ht(t), t !== l.value && (l.value = t), e == null)) { l.defaultValue !== t && (l.defaultValue = t); return } l.defaultValue = e != null ? "" + ht(e) : "" } function Xf(l, t, e, a) { if (t == null) { if (a != null) { if (e != null) throw Error(y(92)); if (Et(a)) { if (1 < a.length) throw Error(y(93)); a = a[0] } e = a } e == null && (e = ""), t = e } e = ht(t), l.defaultValue = e, a = l.textContent, a === e && a !== "" && a !== null && (l.value = a), ai(l) } function We(l, t) { if (t) { var e = l.firstChild; if (e && e === l.lastChild && e.nodeType === 3) { e.nodeValue = t; return } } l.textContent = t } var lo = new Set("animationIterationCount aspectRatio borderImageOutset borderImageSlice borderImageWidth boxFlex boxFlexGroup boxOrdinalGroup columnCount columns flex flexGrow flexPositive flexShrink flexNegative flexOrder gridArea gridRow gridRowEnd gridRowSpan gridRowStart gridColumn gridColumnEnd gridColumnSpan gridColumnStart fontWeight lineClamp lineHeight opacity order orphans scale tabSize widows zIndex zoom fillOpacity floodOpacity stopOpacity strokeDasharray strokeDashoffset strokeMiterlimit strokeOpacity strokeWidth MozAnimationIterationCount MozBoxFlex MozBoxFlexGroup MozLineClamp msAnimationIterationCount msFlex msZoom msFlexGrow msFlexNegative msFlexOrder msFlexPositive msFlexShrink msGridColumn msGridColumnSpan msGridRow msGridRowSpan WebkitAnimationIterationCount WebkitBoxFlex WebKitBoxFlexGroup WebkitBoxOrdinalGroup WebkitColumnCount WebkitColumns WebkitFlex WebkitFlexGrow WebkitFlexPositive WebkitFlexShrink WebkitLineClamp".split(" ")); function Qf(l, t, e) { var a = t.indexOf("--") === 0; e == null || typeof e == "boolean" || e === "" ? a ? l.setProperty(t, "") : t === "float" ? l.cssFloat = "" : l[t] = "" : a ? l.setProperty(t, e) : typeof e != "number" || e === 0 || lo.has(t) ? t === "float" ? l.cssFloat = e : l[t] = ("" + e).trim() : l[t] = e + "px" } function Zf(l, t, e) { if (t != null && typeof t != "object") throw Error(y(62)); if (l = l.style, e != null) { for (var a in e) !e.hasOwnProperty(a) || t != null && t.hasOwnProperty(a) || (a.indexOf("--") === 0 ? l.setProperty(a, "") : a === "float" ? l.cssFloat = "" : l[a] = ""); for (var u in t) a = t[u], t.hasOwnProperty(u) && e[u] !== a && Qf(l, u, a) } else for (var n in t) t.hasOwnProperty(n) && Qf(l, n, t[n]) } function ii(l) { if (l.indexOf("-") === -1) return !1; switch (l) { case "annotation-xml": case "color-profile": case "font-face": case "font-face-src": case "font-face-uri": case "font-face-format": case "font-face-name": case "missing-glyph": return !1; default: return !0 } } var to = new Map([["acceptCharset", "accept-charset"], ["htmlFor", "for"], ["httpEquiv", "http-equiv"], ["crossOrigin", "crossorigin"], ["accentHeight", "accent-height"], ["alignmentBaseline", "alignment-baseline"], ["arabicForm", "arabic-form"], ["baselineShift", "baseline-shift"], ["capHeight", "cap-height"], ["clipPath", "clip-path"], ["clipRule", "clip-rule"], ["colorInterpolation", "color-interpolation"], ["colorInterpolationFilters", "color-interpolation-filters"], ["colorProfile", "color-profile"], ["colorRendering", "color-rendering"], ["dominantBaseline", "dominant-baseline"], ["enableBackground", "enable-background"], ["fillOpacity", "fill-opacity"], ["fillRule", "fill-rule"], ["floodColor", "flood-color"], ["floodOpacity", "flood-opacity"], ["fontFamily", "font-family"], ["fontSize", "font-size"], ["fontSizeAdjust", "font-size-adjust"], ["fontStretch", "font-stretch"], ["fontStyle", "font-style"], ["fontVariant", "font-variant"], ["fontWeight", "font-weight"], ["glyphName", "glyph-name"], ["glyphOrientationHorizontal", "glyph-orientation-horizontal"], ["glyphOrientationVertical", "glyph-orientation-vertical"], ["horizAdvX", "horiz-adv-x"], ["horizOriginX", "horiz-origin-x"], ["imageRendering", "image-rendering"], ["letterSpacing", "letter-spacing"], ["lightingColor", "lighting-color"], ["markerEnd", "marker-end"], ["markerMid", "marker-mid"], ["markerStart", "marker-start"], ["overlinePosition", "overline-position"], ["overlineThickness", "overline-thickness"], ["paintOrder", "paint-order"], ["panose-1", "panose-1"], ["pointerEvents", "pointer-events"], ["renderingIntent", "rendering-intent"], ["shapeRendering", "shape-rendering"], ["stopColor", "stop-color"], ["stopOpacity", "stop-opacity"], ["strikethroughPosition", "strikethrough-position"], ["strikethroughThickness", "strikethrough-thickness"], ["strokeDasharray", "stroke-dasharray"], ["strokeDashoffset", "stroke-dashoffset"], ["strokeLinecap", "stroke-linecap"], ["strokeLinejoin", "stroke-linejoin"], ["strokeMiterlimit", "stroke-miterlimit"], ["strokeOpacity", "stroke-opacity"], ["strokeWidth", "stroke-width"], ["textAnchor", "text-anchor"], ["textDecoration", "text-decoration"], ["textRendering", "text-rendering"], ["transformOrigin", "transform-origin"], ["underlinePosition", "underline-position"], ["underlineThickness", "underline-thickness"], ["unicodeBidi", "unicode-bidi"], ["unicodeRange", "unicode-range"], ["unitsPerEm", "units-per-em"], ["vAlphabetic", "v-alphabetic"], ["vHanging", "v-hanging"], ["vIdeographic", "v-ideographic"], ["vMathematical", "v-mathematical"], ["vectorEffect", "vector-effect"], ["vertAdvY", "vert-adv-y"], ["vertOriginX", "vert-origin-x"], ["vertOriginY", "vert-origin-y"], ["wordSpacing", "word-spacing"], ["writingMode", "writing-mode"], ["xmlnsXlink", "xmlns:xlink"], ["xHeight", "x-height"]]), eo = /^[\u0000-\u001F ]*j[\r\n\t]*a[\r\n\t]*v[\r\n\t]*a[\r\n\t]*s[\r\n\t]*c[\r\n\t]*r[\r\n\t]*i[\r\n\t]*p[\r\n\t]*t[\r\n\t]*:/i; function Ru(l) { return eo.test("" + l) ? "javascript:throw new Error('React has blocked a javascript: URL as a security precaution.')" : l } function Ct() { } var ci = null; function fi(l) { return l = l.target || l.srcElement || window, l.correspondingUseElement && (l = l.correspondingUseElement), l.nodeType === 3 ? l.parentNode : l } var $e = null, Fe = null; function Lf(l) { var t = Ke(l); if (t && (l = t.stateNode)) { var e = l[wl] || null; l: switch (l = t.stateNode, t.type) { case "input": if (ui(l, e.value, e.defaultValue, e.defaultValue, e.checked, e.defaultChecked, e.type, e.name), t = e.name, e.type === "radio" && t != null) { for (e = l; e.parentNode;)e = e.parentNode; for (e = e.querySelectorAll('input[name="' + ot("" + t) + '"][type="radio"]'), t = 0; t < e.length; t++) { var a = e[t]; if (a !== l && a.form === l.form) { var u = a[wl] || null; if (!u) throw Error(y(90)); ui(a, u.value, u.defaultValue, u.defaultValue, u.checked, u.defaultChecked, u.type, u.name) } } for (t = 0; t < e.length; t++)a = e[t], a.form === l.form && Bf(a) } break l; case "textarea": Gf(l, e.value, e.defaultValue); break l; case "select": t = e.value, t != null && ke(l, !!e.multiple, t, !1) } } } var si = !1; function Vf(l, t, e) { if (si) return l(t, e); si = !0; try { var a = l(t); return a } finally { if (si = !1, ($e !== null || Fe !== null) && (zn(), $e && (t = $e, l = Fe, Fe = $e = null, Lf(t), l))) for (t = 0; t < l.length; t++)Lf(l[t]) } } function Ra(l, t) { var e = l.stateNode; if (e === null) return null; var a = e[wl] || null; if (a === null) return null; e = a[t]; l: switch (t) { case "onClick": case "onClickCapture": case "onDoubleClick": case "onDoubleClickCapture": case "onMouseDown": case "onMouseDownCapture": case "onMouseMove": case "onMouseMoveCapture": case "onMouseUp": case "onMouseUpCapture": case "onMouseEnter": (a = !a.disabled) || (l = l.type, a = !(l === "button" || l === "input" || l === "select" || l === "textarea")), l = !a; break l; default: l = !1 }if (l) return null; if (e && typeof e != "function") throw Error(y(231, t, typeof e)); return e } var Dt = !(typeof window > "u" || typeof window.document > "u" || typeof window.document.createElement > "u"), di = !1; if (Dt) try { var Ca = {}; Object.defineProperty(Ca, "passive", { get: function () { di = !0 } }), window.addEventListener("test", Ca, Ca), window.removeEventListener("test", Ca, Ca) } catch { di = !1 } var le = null, hi = null, Cu = null; function Kf() { if (Cu) return Cu; var l, t = hi, e = t.length, a, u = "value" in le ? le.value : le.textContent, n = u.length; for (l = 0; l < e && t[l] === u[l]; l++); var i = e - l; for (a = 1; a <= i && t[e - a] === u[n - a]; a++); return Cu = u.slice(l, 1 < a ? 1 - a : void 0) } function Du(l) { var t = l.keyCode; return "charCode" in l ? (l = l.charCode, l === 0 && t === 13 && (l = 13)) : l = t, l === 10 && (l = 13), 32 <= l || l === 13 ? l : 0 } function qu() { return !0 } function Jf() { return !1 } function kl(l) { function t(e, a, u, n, i) { this._reactName = e, this._targetInst = u, this.type = a, this.nativeEvent = n, this.target = i, this.currentTarget = null; for (var c in l) l.hasOwnProperty(c) && (e = l[c], this[c] = e ? e(n) : n[c]); return this.isDefaultPrevented = (n.defaultPrevented != null ? n.defaultPrevented : n.returnValue === !1) ? qu : Jf, this.isPropagationStopped = Jf, this } return C(t.prototype, { preventDefault: function () { this.defaultPrevented = !0; var e = this.nativeEvent; e && (e.preventDefault ? e.preventDefault() : typeof e.returnValue != "unknown" && (e.returnValue = !1), this.isDefaultPrevented = qu) }, stopPropagation: function () { var e = this.nativeEvent; e && (e.stopPropagation ? e.stopPropagation() : typeof e.cancelBubble != "unknown" && (e.cancelBubble = !0), this.isPropagationStopped = qu) }, persist: function () { }, isPersistent: qu }), t } var Oe = { eventPhase: 0, bubbles: 0, cancelable: 0, timeStamp: function (l) { return l.timeStamp || Date.now() }, defaultPrevented: 0, isTrusted: 0 }, Bu = kl(Oe), Da = C({}, Oe, { view: 0, detail: 0 }), ao = kl(Da), oi, yi, qa, Yu = C({}, Da, { screenX: 0, screenY: 0, clientX: 0, clientY: 0, pageX: 0, pageY: 0, ctrlKey: 0, shiftKey: 0, altKey: 0, metaKey: 0, getModifierState: vi, button: 0, buttons: 0, relatedTarget: function (l) { return l.relatedTarget === void 0 ? l.fromElement === l.srcElement ? l.toElement : l.fromElement : l.relatedTarget }, movementX: function (l) { return "movementX" in l ? l.movementX : (l !== qa && (qa && l.type === "mousemove" ? (oi = l.screenX - qa.screenX, yi = l.screenY - qa.screenY) : yi = oi = 0, qa = l), oi) }, movementY: function (l) { return "movementY" in l ? l.movementY : yi } }), wf = kl(Yu), uo = C({}, Yu, { dataTransfer: 0 }), no = kl(uo), io = C({}, Da, { relatedTarget: 0 }), mi = kl(io), co = C({}, Oe, { animationName: 0, elapsedTime: 0, pseudoElement: 0 }), fo = kl(co), so = C({}, Oe, { clipboardData: function (l) { return "clipboardData" in l ? l.clipboardData : window.clipboardData } }), ho = kl(so), oo = C({}, Oe, { data: 0 }), kf = kl(oo), yo = { Esc: "Escape", Spacebar: " ", Left: "ArrowLeft", Up: "ArrowUp", Right: "ArrowRight", Down: "ArrowDown", Del: "Delete", Win: "OS", Menu: "ContextMenu", Apps: "ContextMenu", Scroll: "ScrollLock", MozPrintableKey: "Unidentified" }, mo = { 8: "Backspace", 9: "Tab", 12: "Clear", 13: "Enter", 16: "Shift", 17: "Control", 18: "Alt", 19: "Pause", 20: "CapsLock", 27: "Escape", 32: " ", 33: "PageUp", 34: "PageDown", 35: "End", 36: "Home", 37: "ArrowLeft", 38: "ArrowUp", 39: "ArrowRight", 40: "ArrowDown", 45: "Insert", 46: "Delete", 112: "F1", 113: "F2", 114: "F3", 115: "F4", 116: "F5", 117: "F6", 118: "F7", 119: "F8", 120: "F9", 121: "F10", 122: "F11", 123: "F12", 144: "NumLock", 145: "ScrollLock", 224: "Meta" }, vo = { Alt: "altKey", Control: "ctrlKey", Meta: "metaKey", Shift: "shiftKey" }; function ro(l) { var t = this.nativeEvent; return t.getModifierState ? t.getModifierState(l) : (l = vo[l]) ? !!t[l] : !1 } function vi() { return ro } var go = C({}, Da, { key: function (l) { if (l.key) { var t = yo[l.key] || l.key; if (t !== "Unidentified") return t } return l.type === "keypress" ? (l = Du(l), l === 13 ? "Enter" : String.fromCharCode(l)) : l.type === "keydown" || l.type === "keyup" ? mo[l.keyCode] || "Unidentified" : "" }, code: 0, location: 0, ctrlKey: 0, shiftKey: 0, altKey: 0, metaKey: 0, repeat: 0, locale: 0, getModifierState: vi, charCode: function (l) { return l.type === "keypress" ? Du(l) : 0 }, keyCode: function (l) { return l.type === "keydown" || l.type === "keyup" ? l.keyCode : 0 }, which: function (l) { return l.type === "keypress" ? Du(l) : l.type === "keydown" || l.type === "keyup" ? l.keyCode : 0 } }), So = kl(go), bo = C({}, Yu, { pointerId: 0, width: 0, height: 0, pressure: 0, tangentialPressure: 0, tiltX: 0, tiltY: 0, twist: 0, pointerType: 0, isPrimary: 0 }), Wf = kl(bo), po = C({}, Da, { touches: 0, targetTouches: 0, changedTouches: 0, altKey: 0, metaKey: 0, ctrlKey: 0, shiftKey: 0, getModifierState: vi }), zo = kl(po), Eo = C({}, Oe, { propertyName: 0, elapsedTime: 0, pseudoElement: 0 }), To = kl(Eo), Ao = C({}, Yu, { deltaX: function (l) { return "deltaX" in l ? l.deltaX : "wheelDeltaX" in l ? -l.wheelDeltaX : 0 }, deltaY: function (l) { return "deltaY" in l ? l.deltaY : "wheelDeltaY" in l ? -l.wheelDeltaY : "wheelDelta" in l ? -l.wheelDelta : 0 }, deltaZ: 0, deltaMode: 0 }), _o = kl(Ao), Mo = C({}, Oe, { newState: 0, oldState: 0 }), Oo = kl(Mo), No = [9, 13, 27, 32], ri = Dt && "CompositionEvent" in window, Ba = null; Dt && "documentMode" in document && (Ba = document.documentMode); var jo = Dt && "TextEvent" in window && !Ba, $f = Dt && (!ri || Ba && 8 < Ba && 11 >= Ba), Ff = " ", If = !1; function Pf(l, t) { switch (l) { case "keyup": return No.indexOf(t.keyCode) !== -1; case "keydown": return t.keyCode !== 229; case "keypress": case "mousedown": case "focusout": return !0; default: return !1 } } function ls(l) { return l = l.detail, typeof l == "object" && "data" in l ? l.data : null } var Ie = !1; function Uo(l, t) { switch (l) { case "compositionend": return ls(t); case "keypress": return t.which !== 32 ? null : (If = !0, Ff); case "textInput": return l = t.data, l === Ff && If ? null : l; default: return null } } function xo(l, t) { if (Ie) return l === "compositionend" || !ri && Pf(l, t) ? (l = Kf(), Cu = hi = le = null, Ie = !1, l) : null; switch (l) { case "paste": return null; case "keypress": if (!(t.ctrlKey || t.altKey || t.metaKey) || t.ctrlKey && t.altKey) { if (t.char && 1 < t.char.length) return t.char; if (t.which) return String.fromCharCode(t.which) } return null; case "compositionend": return $f && t.locale !== "ko" ? null : t.data; default: return null } } var Ho = { color: !0, date: !0, datetime: !0, "datetime-local": !0, email: !0, month: !0, number: !0, password: !0, range: !0, search: !0, tel: !0, text: !0, time: !0, url: !0, week: !0 }; function ts(l) { var t = l && l.nodeName && l.nodeName.toLowerCase(); return t === "input" ? !!Ho[l.type] : t === "textarea" } function es(l, t, e, a) { $e ? Fe ? Fe.push(a) : Fe = [a] : $e = a, t = Nn(t, "onChange"), 0 < t.length && (e = new Bu("onChange", "change", null, e, a), l.push({ event: e, listeners: t })) } var Ya = null, Ga = null; function Ro(l) { Y0(l, 0) } function Gu(l) { var t = Ha(l); if (Bf(t)) return l } function as(l, t) { if (l === "change") return t } var us = !1; if (Dt) { var gi; if (Dt) { var Si = "oninput" in document; if (!Si) { var ns = document.createElement("div"); ns.setAttribute("oninput", "return;"), Si = typeof ns.oninput == "function" } gi = Si } else gi = !1; us = gi && (!document.documentMode || 9 < document.documentMode) } function is() { Ya && (Ya.detachEvent("onpropertychange", cs), Ga = Ya = null) } function cs(l) { if (l.propertyName === "value" && Gu(Ga)) { var t = []; es(t, Ga, l, fi(l)), Vf(Ro, t) } } function Co(l, t, e) { l === "focusin" ? (is(), Ya = t, Ga = e, Ya.attachEvent("onpropertychange", cs)) : l === "focusout" && is() } function Do(l) { if (l === "selectionchange" || l === "keyup" || l === "keydown") return Gu(Ga) } function qo(l, t) { if (l === "click") return Gu(t) } function Bo(l, t) { if (l === "input" || l === "change") return Gu(t) } function Yo(l, t) { return l === t && (l !== 0 || 1 / l === 1 / t) || l !== l && t !== t } var at = typeof Object.is == "function" ? Object.is : Yo; function Xa(l, t) { if (at(l, t)) return !0; if (typeof l != "object" || l === null || typeof t != "object" || t === null) return !1; var e = Object.keys(l), a = Object.keys(t); if (e.length !== a.length) return !1; for (a = 0; a < e.length; a++) { var u = e[a]; if (!Wn.call(t, u) || !at(l[u], t[u])) return !1 } return !0 } function fs(l) { for (; l && l.firstChild;)l = l.firstChild; return l } function ss(l, t) { var e = fs(l); l = 0; for (var a; e;) { if (e.nodeType === 3) { if (a = l + e.textContent.length, l <= t && a >= t) return { node: e, offset: t - l }; l = a } l: { for (; e;) { if (e.nextSibling) { e = e.nextSibling; break l } e = e.parentNode } e = void 0 } e = fs(e) } } function ds(l, t) { return l && t ? l === t ? !0 : l && l.nodeType === 3 ? !1 : t && t.nodeType === 3 ? ds(l, t.parentNode) : "contains" in l ? l.contains(t) : l.compareDocumentPosition ? !!(l.compareDocumentPosition(t) & 16) : !1 : !1 } function hs(l) { l = l != null && l.ownerDocument != null && l.ownerDocument.defaultView != null ? l.ownerDocument.defaultView : window; for (var t = Hu(l.document); t instanceof l.HTMLIFrameElement;) { try { var e = typeof t.contentWindow.location.href == "string" } catch { e = !1 } if (e) l = t.contentWindow; else break; t = Hu(l.document) } return t } function bi(l) { var t = l && l.nodeName && l.nodeName.toLowerCase(); return t && (t === "input" && (l.type === "text" || l.type === "search" || l.type === "tel" || l.type === "url" || l.type === "password") || t === "textarea" || l.contentEditable === "true") } var Go = Dt && "documentMode" in document && 11 >= document.documentMode, Pe = null, pi = null, Qa = null, zi = !1; function os(l, t, e) { var a = e.window === e ? e.document : e.nodeType === 9 ? e : e.ownerDocument; zi || Pe == null || Pe !== Hu(a) || (a = Pe, "selectionStart" in a && bi(a) ? a = { start: a.selectionStart, end: a.selectionEnd } : (a = (a.ownerDocument && a.ownerDocument.defaultView || window).getSelection(), a = { anchorNode: a.anchorNode, anchorOffset: a.anchorOffset, focusNode: a.focusNode, focusOffset: a.focusOffset }), Qa && Xa(Qa, a) || (Qa = a, a = Nn(pi, "onSelect"), 0 < a.length && (t = new Bu("onSelect", "select", null, t, e), l.push({ event: t, listeners: a }), t.target = Pe))) } function Ne(l, t) { var e = {}; return e[l.toLowerCase()] = t.toLowerCase(), e["Webkit" + l] = "webkit" + t, e["Moz" + l] = "moz" + t, e } var la = { animationend: Ne("Animation", "AnimationEnd"), animationiteration: Ne("Animation", "AnimationIteration"), animationstart: Ne("Animation", "AnimationStart"), transitionrun: Ne("Transition", "TransitionRun"), transitionstart: Ne("Transition", "TransitionStart"), transitioncancel: Ne("Transition", "TransitionCancel"), transitionend: Ne("Transition", "TransitionEnd") }, Ei = {}, ys = {}; Dt && (ys = document.createElement("div").style, "AnimationEvent" in window || (delete la.animationend.animation, delete la.animationiteration.animation, delete la.animationstart.animation), "TransitionEvent" in window || delete la.transitionend.transition); function je(l) { if (Ei[l]) return Ei[l]; if (!la[l]) return l; var t = la[l], e; for (e in t) if (t.hasOwnProperty(e) && e in ys) return Ei[l] = t[e]; return l } var ms = je("animationend"), vs = je("animationiteration"), rs = je("animationstart"), Xo = je("transitionrun"), Qo = je("transitionstart"), Zo = je("transitioncancel"), gs = je("transitionend"), Ss = new Map, Ti = "abort auxClick beforeToggle cancel canPlay canPlayThrough click close contextMenu copy cut drag dragEnd dragEnter dragExit dragLeave dragOver dragStart drop durationChange emptied encrypted ended error gotPointerCapture input invalid keyDown keyPress keyUp load loadedData loadedMetadata loadStart lostPointerCapture mouseDown mouseMove mouseOut mouseOver mouseUp paste pause play playing pointerCancel pointerDown pointerMove pointerOut pointerOver pointerUp progress rateChange reset resize seeked seeking stalled submit suspend timeUpdate touchCancel touchEnd touchStart volumeChange scroll toggle touchMove waiting wheel".split(" "); Ti.push("scrollEnd"); function Tt(l, t) { Ss.set(l, t), Me(t, [l]) } var Xu = typeof reportError == "function" ? reportError : function (l) { if (typeof window == "object" && typeof window.ErrorEvent == "function") { var t = new window.ErrorEvent("error", { bubbles: !0, cancelable: !0, message: typeof l == "object" && l !== null && typeof l.message == "string" ? String(l.message) : String(l), error: l }); if (!window.dispatchEvent(t)) return } else if (typeof process == "object" && typeof process.emit == "function") { process.emit("uncaughtException", l); return } console.error(l) }, yt = [], ta = 0, Ai = 0; function Qu() { for (var l = ta, t = Ai = ta = 0; t < l;) { var e = yt[t]; yt[t++] = null; var a = yt[t]; yt[t++] = null; var u = yt[t]; yt[t++] = null; var n = yt[t]; if (yt[t++] = null, a !== null && u !== null) { var i = a.pending; i === null ? u.next = u : (u.next = i.next, i.next = u), a.pending = u } n !== 0 && bs(e, u, n) } } function Zu(l, t, e, a) { yt[ta++] = l, yt[ta++] = t, yt[ta++] = e, yt[ta++] = a, Ai |= a, l.lanes |= a, l = l.alternate, l !== null && (l.lanes |= a) } function _i(l, t, e, a) { return Zu(l, t, e, a), Lu(l) } function Ue(l, t) { return Zu(l, null, null, t), Lu(l) } function bs(l, t, e) { l.lanes |= e; var a = l.alternate; a !== null && (a.lanes |= e); for (var u = !1, n = l.return; n !== null;)n.childLanes |= e, a = n.alternate, a !== null && (a.childLanes |= e), n.tag === 22 && (l = n.stateNode, l === null || l._visibility & 1 || (u = !0)), l = n, n = n.return; return l.tag === 3 ? (n = l.stateNode, u && t !== null && (u = 31 - et(e), l = n.hiddenUpdates, a = l[u], a === null ? l[u] = [t] : a.push(t), t.lane = e | 536870912), n) : null } function Lu(l) { if (50 < su) throw su = 0, Cc = null, Error(y(185)); for (var t = l.return; t !== null;)l = t, t = l.return; return l.tag === 3 ? l.stateNode : null } var ea = {}; function Lo(l, t, e, a) { this.tag = l, this.key = e, this.sibling = this.child = this.return = this.stateNode = this.type = this.elementType = null, this.index = 0, this.refCleanup = this.ref = null, this.pendingProps = t, this.dependencies = this.memoizedState = this.updateQueue = this.memoizedProps = null, this.mode = a, this.subtreeFlags = this.flags = 0, this.deletions = null, this.childLanes = this.lanes = 0, this.alternate = null } function ut(l, t, e, a) { return new Lo(l, t, e, a) } function Mi(l) { return l = l.prototype, !(!l || !l.isReactComponent) } function qt(l, t) { var e = l.alternate; return e === null ? (e = ut(l.tag, t, l.key, l.mode), e.elementType = l.elementType, e.type = l.type, e.stateNode = l.stateNode, e.alternate = l, l.alternate = e) : (e.pendingProps = t, e.type = l.type, e.flags = 0, e.subtreeFlags = 0, e.deletions = null), e.flags = l.flags & 65011712, e.childLanes = l.childLanes, e.lanes = l.lanes, e.child = l.child, e.memoizedProps = l.memoizedProps, e.memoizedState = l.memoizedState, e.updateQueue = l.updateQueue, t = l.dependencies, e.dependencies = t === null ? null : { lanes: t.lanes, firstContext: t.firstContext }, e.sibling = l.sibling, e.index = l.index, e.ref = l.ref, e.refCleanup = l.refCleanup, e } function ps(l, t) { l.flags &= 65011714; var e = l.alternate; return e === null ? (l.childLanes = 0, l.lanes = t, l.child = null, l.subtreeFlags = 0, l.memoizedProps = null, l.memoizedState = null, l.updateQueue = null, l.dependencies = null, l.stateNode = null) : (l.childLanes = e.childLanes, l.lanes = e.lanes, l.child = e.child, l.subtreeFlags = 0, l.deletions = null, l.memoizedProps = e.memoizedProps, l.memoizedState = e.memoizedState, l.updateQueue = e.updateQueue, l.type = e.type, t = e.dependencies, l.dependencies = t === null ? null : { lanes: t.lanes, firstContext: t.firstContext }), l } function Vu(l, t, e, a, u, n) { var i = 0; if (a = l, typeof l == "function") Mi(l) && (i = 1); else if (typeof l == "string") i = k1(l, e, x.current) ? 26 : l === "html" || l === "head" || l === "body" ? 27 : 5; else l: switch (l) { case Rl: return l = ut(31, e, t, u), l.elementType = Rl, l.lanes = n, l; case Hl: return xe(e.children, u, n, t); case zt: i = 8, u |= 24; break; case Zl: return l = ut(12, e, t, u | 2), l.elementType = Zl, l.lanes = n, l; case dt: return l = ut(13, e, t, u), l.elementType = dt, l.lanes = n, l; case Xl: return l = ut(19, e, t, u), l.elementType = Xl, l.lanes = n, l; default: if (typeof l == "object" && l !== null) switch (l.$$typeof) { case _l: i = 10; break l; case Mt: i = 9; break l; case Kl: i = 11; break l; case j: i = 14; break l; case sl: i = 16, a = null; break l }i = 29, e = Error(y(130, l === null ? "null" : typeof l, "")), a = null }return t = ut(i, e, t, u), t.elementType = l, t.type = a, t.lanes = n, t } function xe(l, t, e, a) { return l = ut(7, l, a, t), l.lanes = e, l } function Oi(l, t, e) { return l = ut(6, l, null, t), l.lanes = e, l } function zs(l) { var t = ut(18, null, null, 0); return t.stateNode = l, t } function Ni(l, t, e) { return t = ut(4, l.children !== null ? l.children : [], l.key, t), t.lanes = e, t.stateNode = { containerInfo: l.containerInfo, pendingChildren: null, implementation: l.implementation }, t } var Es = new WeakMap; function mt(l, t) { if (typeof l == "object" && l !== null) { var e = Es.get(l); return e !== void 0 ? e : (t = { value: l, source: t, stack: zf(t) }, Es.set(l, t), t) } return { value: l, source: t, stack: zf(t) } } var aa = [], ua = 0, Ku = null, Za = 0, vt = [], rt = 0, te = null, Nt = 1, jt = ""; function Bt(l, t) { aa[ua++] = Za, aa[ua++] = Ku, Ku = l, Za = t } function Ts(l, t, e) { vt[rt++] = Nt, vt[rt++] = jt, vt[rt++] = te, te = l; var a = Nt; l = jt; var u = 32 - et(a) - 1; a &= ~(1 << u), e += 1; var n = 32 - et(t) + u; if (30 < n) { var i = u - u % 5; n = (a & (1 << i) - 1).toString(32), a >>= i, u -= i, Nt = 1 << 32 - et(t) + u | e << u | a, jt = n + l } else Nt = 1 << n | e << u | a, jt = l } function ji(l) { l.return !== null && (Bt(l, 1), Ts(l, 1, 0)) } function Ui(l) { for (; l === Ku;)Ku = aa[--ua], aa[ua] = null, Za = aa[--ua], aa[ua] = null; for (; l === te;)te = vt[--rt], vt[rt] = null, jt = vt[--rt], vt[rt] = null, Nt = vt[--rt], vt[rt] = null } function As(l, t) { vt[rt++] = Nt, vt[rt++] = jt, vt[rt++] = te, Nt = t.id, jt = t.overflow, te = l } var Dl = null, ol = null, $ = !1, ee = null, gt = !1, xi = Error(y(519)); function ae(l) { var t = Error(y(418, 1 < arguments.length && arguments[1] !== void 0 && arguments[1] ? "text" : "HTML", "")); throw La(mt(t, l)), xi } function _s(l) { var t = l.stateNode, e = l.type, a = l.memoizedProps; switch (t[Cl] = l, t[wl] = a, e) { case "dialog": w("cancel", t), w("close", t); break; case "iframe": case "object": case "embed": w("load", t); break; case "video": case "audio": for (e = 0; e < hu.length; e++)w(hu[e], t); break; case "source": w("error", t); break; case "img": case "image": case "link": w("error", t), w("load", t); break; case "details": w("toggle", t); break; case "input": w("invalid", t), Yf(t, a.value, a.defaultValue, a.checked, a.defaultChecked, a.type, a.name, !0); break; case "select": w("invalid", t); break; case "textarea": w("invalid", t), Xf(t, a.value, a.defaultValue, a.children) }e = a.children, typeof e != "string" && typeof e != "number" && typeof e != "bigint" || t.textContent === "" + e || a.suppressHydrationWarning === !0 || Z0(t.textContent, e) ? (a.popover != null && (w("beforetoggle", t), w("toggle", t)), a.onScroll != null && w("scroll", t), a.onScrollEnd != null && w("scrollend", t), a.onClick != null && (t.onclick = Ct), t = !0) : t = !1, t || ae(l, !0) } function Ms(l) { for (Dl = l.return; Dl;)switch (Dl.tag) { case 5: case 31: case 13: gt = !1; return; case 27: case 3: gt = !0; return; default: Dl = Dl.return } } function na(l) { if (l !== Dl) return !1; if (!$) return Ms(l), $ = !0, !1; var t = l.tag, e; if ((e = t !== 3 && t !== 27) && ((e = t === 5) && (e = l.type, e = !(e !== "form" && e !== "button") || Wc(l.type, l.memoizedProps)), e = !e), e && ol && ae(l), Ms(l), t === 13) { if (l = l.memoizedState, l = l !== null ? l.dehydrated : null, !l) throw Error(y(317)); ol = F0(l) } else if (t === 31) { if (l = l.memoizedState, l = l !== null ? l.dehydrated : null, !l) throw Error(y(317)); ol = F0(l) } else t === 27 ? (t = ol, ge(l.type) ? (l = lf, lf = null, ol = l) : ol = t) : ol = Dl ? bt(l.stateNode.nextSibling) : null; return !0 } function He() { ol = Dl = null, $ = !1 } function Hi() { var l = ee; return l !== null && (Il === null ? Il = l : Il.push.apply(Il, l), ee = null), l } function La(l) { ee === null ? ee = [l] : ee.push(l) } var Ri = h(null), Re = null, Yt = null; function ue(l, t, e) { O(Ri, t._currentValue), t._currentValue = e } function Gt(l) { l._currentValue = Ri.current, A(Ri) } function Ci(l, t, e) { for (; l !== null;) { var a = l.alternate; if ((l.childLanes & t) !== t ? (l.childLanes |= t, a !== null && (a.childLanes |= t)) : a !== null && (a.childLanes & t) !== t && (a.childLanes |= t), l === e) break; l = l.return } } function Di(l, t, e, a) { var u = l.child; for (u !== null && (u.return = l); u !== null;) { var n = u.dependencies; if (n !== null) { var i = u.child; n = n.firstContext; l: for (; n !== null;) { var c = n; n = u; for (var s = 0; s < t.length; s++)if (c.context === t[s]) { n.lanes |= e, c = n.alternate, c !== null && (c.lanes |= e), Ci(n.return, e, l), a || (i = null); break l } n = c.next } } else if (u.tag === 18) { if (i = u.return, i === null) throw Error(y(341)); i.lanes |= e, n = i.alternate, n !== null && (n.lanes |= e), Ci(i, e, l), i = null } else i = u.child; if (i !== null) i.return = u; else for (i = u; i !== null;) { if (i === l) { i = null; break } if (u = i.sibling, u !== null) { u.return = i.return, i = u; break } i = i.return } u = i } } function ia(l, t, e, a) { l = null; for (var u = t, n = !1; u !== null;) { if (!n) { if ((u.flags & 524288) !== 0) n = !0; else if ((u.flags & 262144) !== 0) break } if (u.tag === 10) { var i = u.alternate; if (i === null) throw Error(y(387)); if (i = i.memoizedProps, i !== null) { var c = u.type; at(u.pendingProps.value, i.value) || (l !== null ? l.push(c) : l = [c]) } } else if (u === tl.current) { if (i = u.alternate, i === null) throw Error(y(387)); i.memoizedState.memoizedState !== u.memoizedState.memoizedState && (l !== null ? l.push(ru) : l = [ru]) } u = u.return } l !== null && Di(t, l, e, a), t.flags |= 262144 } function Ju(l) { for (l = l.firstContext; l !== null;) { if (!at(l.context._currentValue, l.memoizedValue)) return !0; l = l.next } return !1 } function Ce(l) { Re = l, Yt = null, l = l.dependencies, l !== null && (l.firstContext = null) } function ql(l) { return Os(Re, l) } function wu(l, t) { return Re === null && Ce(l), Os(l, t) } function Os(l, t) { var e = t._currentValue; if (t = { context: t, memoizedValue: e, next: null }, Yt === null) { if (l === null) throw Error(y(308)); Yt = t, l.dependencies = { lanes: 0, firstContext: t }, l.flags |= 524288 } else Yt = Yt.next = t; return e } var Vo = typeof AbortController < "u" ? AbortController : function () { var l = [], t = this.signal = { aborted: !1, addEventListener: function (e, a) { l.push(a) } }; this.abort = function () { t.aborted = !0, l.forEach(function (e) { return e() }) } }, Ko = f.unstable_scheduleCallback, Jo = f.unstable_NormalPriority, zl = { $$typeof: _l, Consumer: null, Provider: null, _currentValue: null, _currentValue2: null, _threadCount: 0 }; function qi() { return { controller: new Vo, data: new Map, refCount: 0 } } function Va(l) { l.refCount--, l.refCount === 0 && Ko(Jo, function () { l.controller.abort() }) } var Ka = null, Bi = 0, ca = 0, fa = null; function wo(l, t) { if (Ka === null) { var e = Ka = []; Bi = 0, ca = Xc(), fa = { status: "pending", value: void 0, then: function (a) { e.push(a) } } } return Bi++, t.then(Ns, Ns), t } function Ns() { if (--Bi === 0 && Ka !== null) { fa !== null && (fa.status = "fulfilled"); var l = Ka; Ka = null, ca = 0, fa = null; for (var t = 0; t < l.length; t++)(0, l[t])() } } function ko(l, t) { var e = [], a = { status: "pending", value: null, reason: null, then: function (u) { e.push(u) } }; return l.then(function () { a.status = "fulfilled", a.value = t; for (var u = 0; u < e.length; u++)(0, e[u])(t) }, function (u) { for (a.status = "rejected", a.reason = u, u = 0; u < e.length; u++)(0, e[u])(void 0) }), a } var js = p.S; p.S = function (l, t) { o0 = lt(), typeof t == "object" && t !== null && typeof t.then == "function" && wo(l, t), js !== null && js(l, t) }; var De = h(null); function Yi() { var l = De.current; return l !== null ? l : hl.pooledCache } function ku(l, t) { t === null ? O(De, De.current) : O(De, t.pool) } function Us() { var l = Yi(); return l === null ? null : { parent: zl._currentValue, pool: l } } var sa = Error(y(460)), Gi = Error(y(474)), Wu = Error(y(542)), $u = { then: function () { } }; function xs(l) { return l = l.status, l === "fulfilled" || l === "rejected" } function Hs(l, t, e) { switch (e = l[e], e === void 0 ? l.push(t) : e !== t && (t.then(Ct, Ct), t = e), t.status) { case "fulfilled": return t.value; case "rejected": throw l = t.reason, Cs(l), l; default: if (typeof t.status == "string") t.then(Ct, Ct); else { if (l = hl, l !== null && 100 < l.shellSuspendCounter) throw Error(y(482)); l = t, l.status = "pending", l.then(function (a) { if (t.status === "pending") { var u = t; u.status = "fulfilled", u.value = a } }, function (a) { if (t.status === "pending") { var u = t; u.status = "rejected", u.reason = a } }) } switch (t.status) { case "fulfilled": return t.value; case "rejected": throw l = t.reason, Cs(l), l }throw Be = t, sa } } function qe(l) { try { var t = l._init; return t(l._payload) } catch (e) { throw e !== null && typeof e == "object" && typeof e.then == "function" ? (Be = e, sa) : e } } var Be = null; function Rs() { if (Be === null) throw Error(y(459)); var l = Be; return Be = null, l } function Cs(l) { if (l === sa || l === Wu) throw Error(y(483)) } var da = null, Ja = 0; function Fu(l) { var t = Ja; return Ja += 1, da === null && (da = []), Hs(da, l, t) } function wa(l, t) { t = t.props.ref, l.ref = t !== void 0 ? t : null } function Iu(l, t) { throw t.$$typeof === ul ? Error(y(525)) : (l = Object.prototype.toString.call(t), Error(y(31, l === "[object Object]" ? "object with keys {" + Object.keys(t).join(", ") + "}" : l))) } function Ds(l) { function t(o, d) { if (l) { var m = o.deletions; m === null ? (o.deletions = [d], o.flags |= 16) : m.push(d) } } function e(o, d) { if (!l) return null; for (; d !== null;)t(o, d), d = d.sibling; return null } function a(o) { for (var d = new Map; o !== null;)o.key !== null ? d.set(o.key, o) : d.set(o.index, o), o = o.sibling; return d } function u(o, d) { return o = qt(o, d), o.index = 0, o.sibling = null, o } function n(o, d, m) { return o.index = m, l ? (m = o.alternate, m !== null ? (m = m.index, m < d ? (o.flags |= 67108866, d) : m) : (o.flags |= 67108866, d)) : (o.flags |= 1048576, d) } function i(o) { return l && o.alternate === null && (o.flags |= 67108866), o } function c(o, d, m, z) { return d === null || d.tag !== 6 ? (d = Oi(m, o.mode, z), d.return = o, d) : (d = u(d, m), d.return = o, d) } function s(o, d, m, z) { var D = m.type; return D === Hl ? b(o, d, m.props.children, z, m.key) : d !== null && (d.elementType === D || typeof D == "object" && D !== null && D.$$typeof === sl && qe(D) === d.type) ? (d = u(d, m.props), wa(d, m), d.return = o, d) : (d = Vu(m.type, m.key, m.props, null, o.mode, z), wa(d, m), d.return = o, d) } function r(o, d, m, z) { return d === null || d.tag !== 4 || d.stateNode.containerInfo !== m.containerInfo || d.stateNode.implementation !== m.implementation ? (d = Ni(m, o.mode, z), d.return = o, d) : (d = u(d, m.children || []), d.return = o, d) } function b(o, d, m, z, D) { return d === null || d.tag !== 7 ? (d = xe(m, o.mode, z, D), d.return = o, d) : (d = u(d, m), d.return = o, d) } function E(o, d, m) { if (typeof d == "string" && d !== "" || typeof d == "number" || typeof d == "bigint") return d = Oi("" + d, o.mode, m), d.return = o, d; if (typeof d == "object" && d !== null) { switch (d.$$typeof) { case Nl: return m = Vu(d.type, d.key, d.props, null, o.mode, m), wa(m, d), m.return = o, m; case Gl: return d = Ni(d, o.mode, m), d.return = o, d; case sl: return d = qe(d), E(o, d, m) }if (Et(d) || Jl(d)) return d = xe(d, o.mode, m, null), d.return = o, d; if (typeof d.then == "function") return E(o, Fu(d), m); if (d.$$typeof === _l) return E(o, wu(o, d), m); Iu(o, d) } return null } function g(o, d, m, z) { var D = d !== null ? d.key : null; if (typeof m == "string" && m !== "" || typeof m == "number" || typeof m == "bigint") return D !== null ? null : c(o, d, "" + m, z); if (typeof m == "object" && m !== null) { switch (m.$$typeof) { case Nl: return m.key === D ? s(o, d, m, z) : null; case Gl: return m.key === D ? r(o, d, m, z) : null; case sl: return m = qe(m), g(o, d, m, z) }if (Et(m) || Jl(m)) return D !== null ? null : b(o, d, m, z, null); if (typeof m.then == "function") return g(o, d, Fu(m), z); if (m.$$typeof === _l) return g(o, d, wu(o, m), z); Iu(o, m) } return null } function S(o, d, m, z, D) { if (typeof z == "string" && z !== "" || typeof z == "number" || typeof z == "bigint") return o = o.get(m) || null, c(d, o, "" + z, D); if (typeof z == "object" && z !== null) { switch (z.$$typeof) { case Nl: return o = o.get(z.key === null ? m : z.key) || null, s(d, o, z, D); case Gl: return o = o.get(z.key === null ? m : z.key) || null, r(d, o, z, D); case sl: return z = qe(z), S(o, d, m, z, D) }if (Et(z) || Jl(z)) return o = o.get(m) || null, b(d, o, z, D, null); if (typeof z.then == "function") return S(o, d, m, Fu(z), D); if (z.$$typeof === _l) return S(o, d, m, wu(d, z), D); Iu(d, z) } return null } function U(o, d, m, z) { for (var D = null, F = null, H = d, L = d = 0, W = null; H !== null && L < m.length; L++) { H.index > L ? (W = H, H = null) : W = H.sibling; var I = g(o, H, m[L], z); if (I === null) { H === null && (H = W); break } l && H && I.alternate === null && t(o, H), d = n(I, d, L), F === null ? D = I : F.sibling = I, F = I, H = W } if (L === m.length) return e(o, H), $ && Bt(o, L), D; if (H === null) { for (; L < m.length; L++)H = E(o, m[L], z), H !== null && (d = n(H, d, L), F === null ? D = H : F.sibling = H, F = H); return $ && Bt(o, L), D } for (H = a(H); L < m.length; L++)W = S(H, o, L, m[L], z), W !== null && (l && W.alternate !== null && H.delete(W.key === null ? L : W.key), d = n(W, d, L), F === null ? D = W : F.sibling = W, F = W); return l && H.forEach(function (Ee) { return t(o, Ee) }), $ && Bt(o, L), D } function B(o, d, m, z) { if (m == null) throw Error(y(151)); for (var D = null, F = null, H = d, L = d = 0, W = null, I = m.next(); H !== null && !I.done; L++, I = m.next()) { H.index > L ? (W = H, H = null) : W = H.sibling; var Ee = g(o, H, I.value, z); if (Ee === null) { H === null && (H = W); break } l && H && Ee.alternate === null && t(o, H), d = n(Ee, d, L), F === null ? D = Ee : F.sibling = Ee, F = Ee, H = W } if (I.done) return e(o, H), $ && Bt(o, L), D; if (H === null) { for (; !I.done; L++, I = m.next())I = E(o, I.value, z), I !== null && (d = n(I, d, L), F === null ? D = I : F.sibling = I, F = I); return $ && Bt(o, L), D } for (H = a(H); !I.done; L++, I = m.next())I = S(H, o, L, I.value, z), I !== null && (l && I.alternate !== null && H.delete(I.key === null ? L : I.key), d = n(I, d, L), F === null ? D = I : F.sibling = I, F = I); return l && H.forEach(function (ny) { return t(o, ny) }), $ && Bt(o, L), D } function fl(o, d, m, z) { if (typeof m == "object" && m !== null && m.type === Hl && m.key === null && (m = m.props.children), typeof m == "object" && m !== null) { switch (m.$$typeof) { case Nl: l: { for (var D = m.key; d !== null;) { if (d.key === D) { if (D = m.type, D === Hl) { if (d.tag === 7) { e(o, d.sibling), z = u(d, m.props.children), z.return = o, o = z; break l } } else if (d.elementType === D || typeof D == "object" && D !== null && D.$$typeof === sl && qe(D) === d.type) { e(o, d.sibling), z = u(d, m.props), wa(z, m), z.return = o, o = z; break l } e(o, d); break } else t(o, d); d = d.sibling } m.type === Hl ? (z = xe(m.props.children, o.mode, z, m.key), z.return = o, o = z) : (z = Vu(m.type, m.key, m.props, null, o.mode, z), wa(z, m), z.return = o, o = z) } return i(o); case Gl: l: { for (D = m.key; d !== null;) { if (d.key === D) if (d.tag === 4 && d.stateNode.containerInfo === m.containerInfo && d.stateNode.implementation === m.implementation) { e(o, d.sibling), z = u(d, m.children || []), z.return = o, o = z; break l } else { e(o, d); break } else t(o, d); d = d.sibling } z = Ni(m, o.mode, z), z.return = o, o = z } return i(o); case sl: return m = qe(m), fl(o, d, m, z) }if (Et(m)) return U(o, d, m, z); if (Jl(m)) { if (D = Jl(m), typeof D != "function") throw Error(y(150)); return m = D.call(m), B(o, d, m, z) } if (typeof m.then == "function") return fl(o, d, Fu(m), z); if (m.$$typeof === _l) return fl(o, d, wu(o, m), z); Iu(o, m) } return typeof m == "string" && m !== "" || typeof m == "number" || typeof m == "bigint" ? (m = "" + m, d !== null && d.tag === 6 ? (e(o, d.sibling), z = u(d, m), z.return = o, o = z) : (e(o, d), z = Oi(m, o.mode, z), z.return = o, o = z), i(o)) : e(o, d) } return function (o, d, m, z) { try { Ja = 0; var D = fl(o, d, m, z); return da = null, D } catch (H) { if (H === sa || H === Wu) throw H; var F = ut(29, H, null, o.mode); return F.lanes = z, F.return = o, F } finally { } } } var Ye = Ds(!0), qs = Ds(!1), ne = !1; function Xi(l) { l.updateQueue = { baseState: l.memoizedState, firstBaseUpdate: null, lastBaseUpdate: null, shared: { pending: null, lanes: 0, hiddenCallbacks: null }, callbacks: null } } function Qi(l, t) { l = l.updateQueue, t.updateQueue === l && (t.updateQueue = { baseState: l.baseState, firstBaseUpdate: l.firstBaseUpdate, lastBaseUpdate: l.lastBaseUpdate, shared: l.shared, callbacks: null }) } function ie(l) { return { lane: l, tag: 0, payload: null, callback: null, next: null } } function ce(l, t, e) { var a = l.updateQueue; if (a === null) return null; if (a = a.shared, (ll & 2) !== 0) { var u = a.pending; return u === null ? t.next = t : (t.next = u.next, u.next = t), a.pending = t, t = Lu(l), bs(l, null, e), t } return Zu(l, a, t, e), Lu(l) } function ka(l, t, e) { if (t = t.updateQueue, t !== null && (t = t.shared, (e & 4194048) !== 0)) { var a = t.lanes; a &= l.pendingLanes, e |= a, t.lanes = e, Of(l, e) } } function Zi(l, t) { var e = l.updateQueue, a = l.alternate; if (a !== null && (a = a.updateQueue, e === a)) { var u = null, n = null; if (e = e.firstBaseUpdate, e !== null) { do { var i = { lane: e.lane, tag: e.tag, payload: e.payload, callback: null, next: null }; n === null ? u = n = i : n = n.next = i, e = e.next } while (e !== null); n === null ? u = n = t : n = n.next = t } else u = n = t; e = { baseState: a.baseState, firstBaseUpdate: u, lastBaseUpdate: n, shared: a.shared, callbacks: a.callbacks }, l.updateQueue = e; return } l = e.lastBaseUpdate, l === null ? e.firstBaseUpdate = t : l.next = t, e.lastBaseUpdate = t } var Li = !1; function Wa() { if (Li) { var l = fa; if (l !== null) throw l } } function $a(l, t, e, a) { Li = !1; var u = l.updateQueue; ne = !1; var n = u.firstBaseUpdate, i = u.lastBaseUpdate, c = u.shared.pending; if (c !== null) { u.shared.pending = null; var s = c, r = s.next; s.next = null, i === null ? n = r : i.next = r, i = s; var b = l.alternate; b !== null && (b = b.updateQueue, c = b.lastBaseUpdate, c !== i && (c === null ? b.firstBaseUpdate = r : c.next = r, b.lastBaseUpdate = s)) } if (n !== null) { var E = u.baseState; i = 0, b = r = s = null, c = n; do { var g = c.lane & -536870913, S = g !== c.lane; if (S ? (k & g) === g : (a & g) === g) { g !== 0 && g === ca && (Li = !0), b !== null && (b = b.next = { lane: 0, tag: c.tag, payload: c.payload, callback: null, next: null }); l: { var U = l, B = c; g = t; var fl = e; switch (B.tag) { case 1: if (U = B.payload, typeof U == "function") { E = U.call(fl, E, g); break l } E = U; break l; case 3: U.flags = U.flags & -65537 | 128; case 0: if (U = B.payload, g = typeof U == "function" ? U.call(fl, E, g) : U, g == null) break l; E = C({}, E, g); break l; case 2: ne = !0 } } g = c.callback, g !== null && (l.flags |= 64, S && (l.flags |= 8192), S = u.callbacks, S === null ? u.callbacks = [g] : S.push(g)) } else S = { lane: g, tag: c.tag, payload: c.payload, callback: c.callback, next: null }, b === null ? (r = b = S, s = E) : b = b.next = S, i |= g; if (c = c.next, c === null) { if (c = u.shared.pending, c === null) break; S = c, c = S.next, S.next = null, u.lastBaseUpdate = S, u.shared.pending = null } } while (!0); b === null && (s = E), u.baseState = s, u.firstBaseUpdate = r, u.lastBaseUpdate = b, n === null && (u.shared.lanes = 0), oe |= i, l.lanes = i, l.memoizedState = E } } function Bs(l, t) { if (typeof l != "function") throw Error(y(191, l)); l.call(t) } function Ys(l, t) { var e = l.callbacks; if (e !== null) for (l.callbacks = null, l = 0; l < e.length; l++)Bs(e[l], t) } var ha = h(null), Pu = h(0); function Gs(l, t) { l = kt, O(Pu, l), O(ha, t), kt = l | t.baseLanes } function Vi() { O(Pu, kt), O(ha, ha.current) } function Ki() { kt = Pu.current, A(ha), A(Pu) } var nt = h(null), St = null; function fe(l) { var t = l.alternate; O(bl, bl.current & 1), O(nt, l), St === null && (t === null || ha.current !== null || t.memoizedState !== null) && (St = l) } function Ji(l) { O(bl, bl.current), O(nt, l), St === null && (St = l) } function Xs(l) { l.tag === 22 ? (O(bl, bl.current), O(nt, l), St === null && (St = l)) : se() } function se() { O(bl, bl.current), O(nt, nt.current) } function it(l) { A(nt), St === l && (St = null), A(bl) } var bl = h(0); function ln(l) { for (var t = l; t !== null;) { if (t.tag === 13) { var e = t.memoizedState; if (e !== null && (e = e.dehydrated, e === null || Ic(e) || Pc(e))) return t } else if (t.tag === 19 && (t.memoizedProps.revealOrder === "forwards" || t.memoizedProps.revealOrder === "backwards" || t.memoizedProps.revealOrder === "unstable_legacy-backwards" || t.memoizedProps.revealOrder === "together")) { if ((t.flags & 128) !== 0) return t } else if (t.child !== null) { t.child.return = t, t = t.child; continue } if (t === l) break; for (; t.sibling === null;) { if (t.return === null || t.return === l) return null; t = t.return } t.sibling.return = t.return, t = t.sibling } return null } var Xt = 0, Z = null, il = null, El = null, tn = !1, oa = !1, Ge = !1, en = 0, Fa = 0, ya = null, Wo = 0; function rl() { throw Error(y(321)) } function wi(l, t) { if (t === null) return !1; for (var e = 0; e < t.length && e < l.length; e++)if (!at(l[e], t[e])) return !1; return !0 } function ki(l, t, e, a, u, n) { return Xt = n, Z = t, t.memoizedState = null, t.updateQueue = null, t.lanes = 0, p.H = l === null || l.memoizedState === null ? Td : sc, Ge = !1, n = e(a, u), Ge = !1, oa && (n = Zs(t, e, a, u)), Qs(l), n } function Qs(l) { p.H = lu; var t = il !== null && il.next !== null; if (Xt = 0, El = il = Z = null, tn = !1, Fa = 0, ya = null, t) throw Error(y(300)); l === null || Tl || (l = l.dependencies, l !== null && Ju(l) && (Tl = !0)) } function Zs(l, t, e, a) { Z = l; var u = 0; do { if (oa && (ya = null), Fa = 0, oa = !1, 25 <= u) throw Error(y(301)); if (u += 1, El = il = null, l.updateQueue != null) { var n = l.updateQueue; n.lastEffect = null, n.events = null, n.stores = null, n.memoCache != null && (n.memoCache.index = 0) } p.H = Ad, n = t(e, a) } while (oa); return n } function $o() { var l = p.H, t = l.useState()[0]; return t = typeof t.then == "function" ? Ia(t) : t, l = l.useState()[0], (il !== null ? il.memoizedState : null) !== l && (Z.flags |= 1024), t } function Wi() { var l = en !== 0; return en = 0, l } function $i(l, t, e) { t.updateQueue = l.updateQueue, t.flags &= -2053, l.lanes &= ~e } function Fi(l) { if (tn) { for (l = l.memoizedState; l !== null;) { var t = l.queue; t !== null && (t.pending = null), l = l.next } tn = !1 } Xt = 0, El = il = Z = null, oa = !1, Fa = en = 0, ya = null } function Vl() { var l = { memoizedState: null, baseState: null, baseQueue: null, queue: null, next: null }; return El === null ? Z.memoizedState = El = l : El = El.next = l, El } function pl() { if (il === null) { var l = Z.alternate; l = l !== null ? l.memoizedState : null } else l = il.next; var t = El === null ? Z.memoizedState : El.next; if (t !== null) El = t, il = l; else { if (l === null) throw Z.alternate === null ? Error(y(467)) : Error(y(310)); il = l, l = { memoizedState: il.memoizedState, baseState: il.baseState, baseQueue: il.baseQueue, queue: il.queue, next: null }, El === null ? Z.memoizedState = El = l : El = El.next = l } return El } function an() { return { lastEffect: null, events: null, stores: null, memoCache: null } } function Ia(l) { var t = Fa; return Fa += 1, ya === null && (ya = []), l = Hs(ya, l, t), t = Z, (El === null ? t.memoizedState : El.next) === null && (t = t.alternate, p.H = t === null || t.memoizedState === null ? Td : sc), l } function un(l) { if (l !== null && typeof l == "object") { if (typeof l.then == "function") return Ia(l); if (l.$$typeof === _l) return ql(l) } throw Error(y(438, String(l))) } function Ii(l) { var t = null, e = Z.updateQueue; if (e !== null && (t = e.memoCache), t == null) { var a = Z.alternate; a !== null && (a = a.updateQueue, a !== null && (a = a.memoCache, a != null && (t = { data: a.data.map(function (u) { return u.slice() }), index: 0 }))) } if (t == null && (t = { data: [], index: 0 }), e === null && (e = an(), Z.updateQueue = e), e.memoCache = t, e = t.data[t.index], e === void 0) for (e = t.data[t.index] = Array(l), a = 0; a < l; a++)e[a] = Ft; return t.index++, e } function Qt(l, t) { return typeof t == "function" ? t(l) : t } function nn(l) { var t = pl(); return Pi(t, il, l) } function Pi(l, t, e) { var a = l.queue; if (a === null) throw Error(y(311)); a.lastRenderedReducer = e; var u = l.baseQueue, n = a.pending; if (n !== null) { if (u !== null) { var i = u.next; u.next = n.next, n.next = i } t.baseQueue = u = n, a.pending = null } if (n = l.baseState, u === null) l.memoizedState = n; else { t = u.next; var c = i = null, s = null, r = t, b = !1; do { var E = r.lane & -536870913; if (E !== r.lane ? (k & E) === E : (Xt & E) === E) { var g = r.revertLane; if (g === 0) s !== null && (s = s.next = { lane: 0, revertLane: 0, gesture: null, action: r.action, hasEagerState: r.hasEagerState, eagerState: r.eagerState, next: null }), E === ca && (b = !0); else if ((Xt & g) === g) { r = r.next, g === ca && (b = !0); continue } else E = { lane: 0, revertLane: r.revertLane, gesture: null, action: r.action, hasEagerState: r.hasEagerState, eagerState: r.eagerState, next: null }, s === null ? (c = s = E, i = n) : s = s.next = E, Z.lanes |= g, oe |= g; E = r.action, Ge && e(n, E), n = r.hasEagerState ? r.eagerState : e(n, E) } else g = { lane: E, revertLane: r.revertLane, gesture: r.gesture, action: r.action, hasEagerState: r.hasEagerState, eagerState: r.eagerState, next: null }, s === null ? (c = s = g, i = n) : s = s.next = g, Z.lanes |= E, oe |= E; r = r.next } while (r !== null && r !== t); if (s === null ? i = n : s.next = c, !at(n, l.memoizedState) && (Tl = !0, b && (e = fa, e !== null))) throw e; l.memoizedState = n, l.baseState = i, l.baseQueue = s, a.lastRenderedState = n } return u === null && (a.lanes = 0), [l.memoizedState, a.dispatch] } function lc(l) { var t = pl(), e = t.queue; if (e === null) throw Error(y(311)); e.lastRenderedReducer = l; var a = e.dispatch, u = e.pending, n = t.memoizedState; if (u !== null) { e.pending = null; var i = u = u.next; do n = l(n, i.action), i = i.next; while (i !== u); at(n, t.memoizedState) || (Tl = !0), t.memoizedState = n, t.baseQueue === null && (t.baseState = n), e.lastRenderedState = n } return [n, a] } function Ls(l, t, e) { var a = Z, u = pl(), n = $; if (n) { if (e === void 0) throw Error(y(407)); e = e() } else e = t(); var i = !at((il || u).memoizedState, e); if (i && (u.memoizedState = e, Tl = !0), u = u.queue, ac(Js.bind(null, a, u, l), [l]), u.getSnapshot !== t || i || El !== null && El.memoizedState.tag & 1) { if (a.flags |= 2048, ma(9, { destroy: void 0 }, Ks.bind(null, a, u, e, t), null), hl === null) throw Error(y(349)); n || (Xt & 127) !== 0 || Vs(a, t, e) } return e } function Vs(l, t, e) { l.flags |= 16384, l = { getSnapshot: t, value: e }, t = Z.updateQueue, t === null ? (t = an(), Z.updateQueue = t, t.stores = [l]) : (e = t.stores, e === null ? t.stores = [l] : e.push(l)) } function Ks(l, t, e, a) { t.value = e, t.getSnapshot = a, ws(t) && ks(l) } function Js(l, t, e) { return e(function () { ws(t) && ks(l) }) } function ws(l) { var t = l.getSnapshot; l = l.value; try { var e = t(); return !at(l, e) } catch { return !0 } } function ks(l) { var t = Ue(l, 2); t !== null && Pl(t, l, 2) } function tc(l) { var t = Vl(); if (typeof l == "function") { var e = l; if (l = e(), Ge) { It(!0); try { e() } finally { It(!1) } } } return t.memoizedState = t.baseState = l, t.queue = { pending: null, lanes: 0, dispatch: null, lastRenderedReducer: Qt, lastRenderedState: l }, t } function Ws(l, t, e, a) { return l.baseState = e, Pi(l, il, typeof a == "function" ? a : Qt) } function Fo(l, t, e, a, u) { if (sn(l)) throw Error(y(485)); if (l = t.action, l !== null) { var n = { payload: u, action: l, next: null, isTransition: !0, status: "pending", value: null, reason: null, listeners: [], then: function (i) { n.listeners.push(i) } }; p.T !== null ? e(!0) : n.isTransition = !1, a(n), e = t.pending, e === null ? (n.next = t.pending = n, $s(t, n)) : (n.next = e.next, t.pending = e.next = n) } } function $s(l, t) { var e = t.action, a = t.payload, u = l.state; if (t.isTransition) { var n = p.T, i = {}; p.T = i; try { var c = e(u, a), s = p.S; s !== null && s(i, c), Fs(l, t, c) } catch (r) { ec(l, t, r) } finally { n !== null && i.types !== null && (n.types = i.types), p.T = n } } else try { n = e(u, a), Fs(l, t, n) } catch (r) { ec(l, t, r) } } function Fs(l, t, e) { e !== null && typeof e == "object" && typeof e.then == "function" ? e.then(function (a) { Is(l, t, a) }, function (a) { return ec(l, t, a) }) : Is(l, t, e) } function Is(l, t, e) { t.status = "fulfilled", t.value = e, Ps(t), l.state = e, t = l.pending, t !== null && (e = t.next, e === t ? l.pending = null : (e = e.next, t.next = e, $s(l, e))) } function ec(l, t, e) { var a = l.pending; if (l.pending = null, a !== null) { a = a.next; do t.status = "rejected", t.reason = e, Ps(t), t = t.next; while (t !== a) } l.action = null } function Ps(l) { l = l.listeners; for (var t = 0; t < l.length; t++)(0, l[t])() } function ld(l, t) { return t } function td(l, t) { if ($) { var e = hl.formState; if (e !== null) { l: { var a = Z; if ($) { if (ol) { t: { for (var u = ol, n = gt; u.nodeType !== 8;) { if (!n) { u = null; break t } if (u = bt(u.nextSibling), u === null) { u = null; break t } } n = u.data, u = n === "F!" || n === "F" ? u : null } if (u) { ol = bt(u.nextSibling), a = u.data === "F!"; break l } } ae(a) } a = !1 } a && (t = e[0]) } } return e = Vl(), e.memoizedState = e.baseState = t, a = { pending: null, lanes: 0, dispatch: null, lastRenderedReducer: ld, lastRenderedState: t }, e.queue = a, e = pd.bind(null, Z, a), a.dispatch = e, a = tc(!1), n = fc.bind(null, Z, !1, a.queue), a = Vl(), u = { state: t, dispatch: null, action: l, pending: null }, a.queue = u, e = Fo.bind(null, Z, u, n, e), u.dispatch = e, a.memoizedState = l, [t, e, !1] } function ed(l) { var t = pl(); return ad(t, il, l) } function ad(l, t, e) { if (t = Pi(l, t, ld)[0], l = nn(Qt)[0], typeof t == "object" && t !== null && typeof t.then == "function") try { var a = Ia(t) } catch (i) { throw i === sa ? Wu : i } else a = t; t = pl(); var u = t.queue, n = u.dispatch; return e !== t.memoizedState && (Z.flags |= 2048, ma(9, { destroy: void 0 }, Io.bind(null, u, e), null)), [a, n, l] } function Io(l, t) { l.action = t } function ud(l) { var t = pl(), e = il; if (e !== null) return ad(t, e, l); pl(), t = t.memoizedState, e = pl(); var a = e.queue.dispatch; return e.memoizedState = l, [t, a, !1] } function ma(l, t, e, a) { return l = { tag: l, create: e, deps: a, inst: t, next: null }, t = Z.updateQueue, t === null && (t = an(), Z.updateQueue = t), e = t.lastEffect, e === null ? t.lastEffect = l.next = l : (a = e.next, e.next = l, l.next = a, t.lastEffect = l), l } function nd() { return pl().memoizedState } function cn(l, t, e, a) { var u = Vl(); Z.flags |= l, u.memoizedState = ma(1 | t, { destroy: void 0 }, e, a === void 0 ? null : a) } function fn(l, t, e, a) { var u = pl(); a = a === void 0 ? null : a; var n = u.memoizedState.inst; il !== null && a !== null && wi(a, il.memoizedState.deps) ? u.memoizedState = ma(t, n, e, a) : (Z.flags |= l, u.memoizedState = ma(1 | t, n, e, a)) } function id(l, t) { cn(8390656, 8, l, t) } function ac(l, t) { fn(2048, 8, l, t) } function Po(l) { Z.flags |= 4; var t = Z.updateQueue; if (t === null) t = an(), Z.updateQueue = t, t.events = [l]; else { var e = t.events; e === null ? t.events = [l] : e.push(l) } } function cd(l) { var t = pl().memoizedState; return Po({ ref: t, nextImpl: l }), function () { if ((ll & 2) !== 0) throw Error(y(440)); return t.impl.apply(void 0, arguments) } } function fd(l, t) { return fn(4, 2, l, t) } function sd(l, t) { return fn(4, 4, l, t) } function dd(l, t) { if (typeof t == "function") { l = l(); var e = t(l); return function () { typeof e == "function" ? e() : t(null) } } if (t != null) return l = l(), t.current = l, function () { t.current = null } } function hd(l, t, e) { e = e != null ? e.concat([l]) : null, fn(4, 4, dd.bind(null, t, l), e) } function uc() { } function od(l, t) { var e = pl(); t = t === void 0 ? null : t; var a = e.memoizedState; return t !== null && wi(t, a[1]) ? a[0] : (e.memoizedState = [l, t], l) } function yd(l, t) { var e = pl(); t = t === void 0 ? null : t; var a = e.memoizedState; if (t !== null && wi(t, a[1])) return a[0]; if (a = l(), Ge) { It(!0); try { l() } finally { It(!1) } } return e.memoizedState = [a, t], a } function nc(l, t, e) { return e === void 0 || (Xt & 1073741824) !== 0 && (k & 261930) === 0 ? l.memoizedState = t : (l.memoizedState = e, l = m0(), Z.lanes |= l, oe |= l, e) } function md(l, t, e, a) { return at(e, t) ? e : ha.current !== null ? (l = nc(l, e, a), at(l, t) || (Tl = !0), l) : (Xt & 42) === 0 || (Xt & 1073741824) !== 0 && (k & 261930) === 0 ? (Tl = !0, l.memoizedState = e) : (l = m0(), Z.lanes |= l, oe |= l, t) } function vd(l, t, e, a, u) { var n = M.p; M.p = n !== 0 && 8 > n ? n : 8; var i = p.T, c = {}; p.T = c, fc(l, !1, t, e); try { var s = u(), r = p.S; if (r !== null && r(c, s), s !== null && typeof s == "object" && typeof s.then == "function") { var b = ko(s, a); Pa(l, t, b, st(l)) } else Pa(l, t, a, st(l)) } catch (E) { Pa(l, t, { then: function () { }, status: "rejected", reason: E }, st()) } finally { M.p = n, i !== null && c.types !== null && (i.types = c.types), p.T = i } } function l1() { } function ic(l, t, e, a) { if (l.tag !== 5) throw Error(y(476)); var u = rd(l).queue; vd(l, u, t, Y, e === null ? l1 : function () { return gd(l), e(a) }) } function rd(l) { var t = l.memoizedState; if (t !== null) return t; t = { memoizedState: Y, baseState: Y, baseQueue: null, queue: { pending: null, lanes: 0, dispatch: null, lastRenderedReducer: Qt, lastRenderedState: Y }, next: null }; var e = {}; return t.next = { memoizedState: e, baseState: e, baseQueue: null, queue: { pending: null, lanes: 0, dispatch: null, lastRenderedReducer: Qt, lastRenderedState: e }, next: null }, l.memoizedState = t, l = l.alternate, l !== null && (l.memoizedState = t), t } function gd(l) { var t = rd(l); t.next === null && (t = l.alternate.memoizedState), Pa(l, t.next.queue, {}, st()) } function cc() { return ql(ru) } function Sd() { return pl().memoizedState } function bd() { return pl().memoizedState } function t1(l) { for (var t = l.return; t !== null;) { switch (t.tag) { case 24: case 3: var e = st(); l = ie(e); var a = ce(t, l, e); a !== null && (Pl(a, t, e), ka(a, t, e)), t = { cache: qi() }, l.payload = t; return }t = t.return } } function e1(l, t, e) { var a = st(); e = { lane: a, revertLane: 0, gesture: null, action: e, hasEagerState: !1, eagerState: null, next: null }, sn(l) ? zd(t, e) : (e = _i(l, t, e, a), e !== null && (Pl(e, l, a), Ed(e, t, a))) } function pd(l, t, e) { var a = st(); Pa(l, t, e, a) } function Pa(l, t, e, a) { var u = { lane: a, revertLane: 0, gesture: null, action: e, hasEagerState: !1, eagerState: null, next: null }; if (sn(l)) zd(t, u); else { var n = l.alternate; if (l.lanes === 0 && (n === null || n.lanes === 0) && (n = t.lastRenderedReducer, n !== null)) try { var i = t.lastRenderedState, c = n(i, e); if (u.hasEagerState = !0, u.eagerState = c, at(c, i)) return Zu(l, t, u, 0), hl === null && Qu(), !1 } catch { } finally { } if (e = _i(l, t, u, a), e !== null) return Pl(e, l, a), Ed(e, t, a), !0 } return !1 } function fc(l, t, e, a) { if (a = { lane: 2, revertLane: Xc(), gesture: null, action: a, hasEagerState: !1, eagerState: null, next: null }, sn(l)) { if (t) throw Error(y(479)) } else t = _i(l, e, a, 2), t !== null && Pl(t, l, 2) } function sn(l) { var t = l.alternate; return l === Z || t !== null && t === Z } function zd(l, t) { oa = tn = !0; var e = l.pending; e === null ? t.next = t : (t.next = e.next, e.next = t), l.pending = t } function Ed(l, t, e) { if ((e & 4194048) !== 0) { var a = t.lanes; a &= l.pendingLanes, e |= a, t.lanes = e, Of(l, e) } } var lu = { readContext: ql, use: un, useCallback: rl, useContext: rl, useEffect: rl, useImperativeHandle: rl, useLayoutEffect: rl, useInsertionEffect: rl, useMemo: rl, useReducer: rl, useRef: rl, useState: rl, useDebugValue: rl, useDeferredValue: rl, useTransition: rl, useSyncExternalStore: rl, useId: rl, useHostTransitionStatus: rl, useFormState: rl, useActionState: rl, useOptimistic: rl, useMemoCache: rl, useCacheRefresh: rl }; lu.useEffectEvent = rl; var Td = { readContext: ql, use: un, useCallback: function (l, t) { return Vl().memoizedState = [l, t === void 0 ? null : t], l }, useContext: ql, useEffect: id, useImperativeHandle: function (l, t, e) { e = e != null ? e.concat([l]) : null, cn(4194308, 4, dd.bind(null, t, l), e) }, useLayoutEffect: function (l, t) { return cn(4194308, 4, l, t) }, useInsertionEffect: function (l, t) { cn(4, 2, l, t) }, useMemo: function (l, t) { var e = Vl(); t = t === void 0 ? null : t; var a = l(); if (Ge) { It(!0); try { l() } finally { It(!1) } } return e.memoizedState = [a, t], a }, useReducer: function (l, t, e) { var a = Vl(); if (e !== void 0) { var u = e(t); if (Ge) { It(!0); try { e(t) } finally { It(!1) } } } else u = t; return a.memoizedState = a.baseState = u, l = { pending: null, lanes: 0, dispatch: null, lastRenderedReducer: l, lastRenderedState: u }, a.queue = l, l = l.dispatch = e1.bind(null, Z, l), [a.memoizedState, l] }, useRef: function (l) { var t = Vl(); return l = { current: l }, t.memoizedState = l }, useState: function (l) { l = tc(l); var t = l.queue, e = pd.bind(null, Z, t); return t.dispatch = e, [l.memoizedState, e] }, useDebugValue: uc, useDeferredValue: function (l, t) { var e = Vl(); return nc(e, l, t) }, useTransition: function () { var l = tc(!1); return l = vd.bind(null, Z, l.queue, !0, !1), Vl().memoizedState = l, [!1, l] }, useSyncExternalStore: function (l, t, e) { var a = Z, u = Vl(); if ($) { if (e === void 0) throw Error(y(407)); e = e() } else { if (e = t(), hl === null) throw Error(y(349)); (k & 127) !== 0 || Vs(a, t, e) } u.memoizedState = e; var n = { value: e, getSnapshot: t }; return u.queue = n, id(Js.bind(null, a, n, l), [l]), a.flags |= 2048, ma(9, { destroy: void 0 }, Ks.bind(null, a, n, e, t), null), e }, useId: function () { var l = Vl(), t = hl.identifierPrefix; if ($) { var e = jt, a = Nt; e = (a & ~(1 << 32 - et(a) - 1)).toString(32) + e, t = "_" + t + "R_" + e, e = en++, 0 < e && (t += "H" + e.toString(32)), t += "_" } else e = Wo++, t = "_" + t + "r_" + e.toString(32) + "_"; return l.memoizedState = t }, useHostTransitionStatus: cc, useFormState: td, useActionState: td, useOptimistic: function (l) { var t = Vl(); t.memoizedState = t.baseState = l; var e = { pending: null, lanes: 0, dispatch: null, lastRenderedReducer: null, lastRenderedState: null }; return t.queue = e, t = fc.bind(null, Z, !0, e), e.dispatch = t, [l, t] }, useMemoCache: Ii, useCacheRefresh: function () { return Vl().memoizedState = t1.bind(null, Z) }, useEffectEvent: function (l) { var t = Vl(), e = { impl: l }; return t.memoizedState = e, function () { if ((ll & 2) !== 0) throw Error(y(440)); return e.impl.apply(void 0, arguments) } } }, sc = { readContext: ql, use: un, useCallback: od, useContext: ql, useEffect: ac, useImperativeHandle: hd, useInsertionEffect: fd, useLayoutEffect: sd, useMemo: yd, useReducer: nn, useRef: nd, useState: function () { return nn(Qt) }, useDebugValue: uc, useDeferredValue: function (l, t) { var e = pl(); return md(e, il.memoizedState, l, t) }, useTransition: function () { var l = nn(Qt)[0], t = pl().memoizedState; return [typeof l == "boolean" ? l : Ia(l), t] }, useSyncExternalStore: Ls, useId: Sd, useHostTransitionStatus: cc, useFormState: ed, useActionState: ed, useOptimistic: function (l, t) { var e = pl(); return Ws(e, il, l, t) }, useMemoCache: Ii, useCacheRefresh: bd }; sc.useEffectEvent = cd; var Ad = { readContext: ql, use: un, useCallback: od, useContext: ql, useEffect: ac, useImperativeHandle: hd, useInsertionEffect: fd, useLayoutEffect: sd, useMemo: yd, useReducer: lc, useRef: nd, useState: function () { return lc(Qt) }, useDebugValue: uc, useDeferredValue: function (l, t) { var e = pl(); return il === null ? nc(e, l, t) : md(e, il.memoizedState, l, t) }, useTransition: function () { var l = lc(Qt)[0], t = pl().memoizedState; return [typeof l == "boolean" ? l : Ia(l), t] }, useSyncExternalStore: Ls, useId: Sd, useHostTransitionStatus: cc, useFormState: ud, useActionState: ud, useOptimistic: function (l, t) { var e = pl(); return il !== null ? Ws(e, il, l, t) : (e.baseState = l, [l, e.queue.dispatch]) }, useMemoCache: Ii, useCacheRefresh: bd }; Ad.useEffectEvent = cd; function dc(l, t, e, a) { t = l.memoizedState, e = e(a, t), e = e == null ? t : C({}, t, e), l.memoizedState = e, l.lanes === 0 && (l.updateQueue.baseState = e) } var hc = { enqueueSetState: function (l, t, e) { l = l._reactInternals; var a = st(), u = ie(a); u.payload = t, e != null && (u.callback = e), t = ce(l, u, a), t !== null && (Pl(t, l, a), ka(t, l, a)) }, enqueueReplaceState: function (l, t, e) { l = l._reactInternals; var a = st(), u = ie(a); u.tag = 1, u.payload = t, e != null && (u.callback = e), t = ce(l, u, a), t !== null && (Pl(t, l, a), ka(t, l, a)) }, enqueueForceUpdate: function (l, t) { l = l._reactInternals; var e = st(), a = ie(e); a.tag = 2, t != null && (a.callback = t), t = ce(l, a, e), t !== null && (Pl(t, l, e), ka(t, l, e)) } }; function _d(l, t, e, a, u, n, i) { return l = l.stateNode, typeof l.shouldComponentUpdate == "function" ? l.shouldComponentUpdate(a, n, i) : t.prototype && t.prototype.isPureReactComponent ? !Xa(e, a) || !Xa(u, n) : !0 } function Md(l, t, e, a) { l = t.state, typeof t.componentWillReceiveProps == "function" && t.componentWillReceiveProps(e, a), typeof t.UNSAFE_componentWillReceiveProps == "function" && t.UNSAFE_componentWillReceiveProps(e, a), t.state !== l && hc.enqueueReplaceState(t, t.state, null) } function Xe(l, t) { var e = t; if ("ref" in t) { e = {}; for (var a in t) a !== "ref" && (e[a] = t[a]) } if (l = l.defaultProps) { e === t && (e = C({}, e)); for (var u in l) e[u] === void 0 && (e[u] = l[u]) } return e } function Od(l) { Xu(l) } function Nd(l) { console.error(l) } function jd(l) { Xu(l) } function dn(l, t) { try { var e = l.onUncaughtError; e(t.value, { componentStack: t.stack }) } catch (a) { setTimeout(function () { throw a }) } } function Ud(l, t, e) { try { var a = l.onCaughtError; a(e.value, { componentStack: e.stack, errorBoundary: t.tag === 1 ? t.stateNode : null }) } catch (u) { setTimeout(function () { throw u }) } } function oc(l, t, e) { return e = ie(e), e.tag = 3, e.payload = { element: null }, e.callback = function () { dn(l, t) }, e } function xd(l) { return l = ie(l), l.tag = 3, l } function Hd(l, t, e, a) { var u = e.type.getDerivedStateFromError; if (typeof u == "function") { var n = a.value; l.payload = function () { return u(n) }, l.callback = function () { Ud(t, e, a) } } var i = e.stateNode; i !== null && typeof i.componentDidCatch == "function" && (l.callback = function () { Ud(t, e, a), typeof u != "function" && (ye === null ? ye = new Set([this]) : ye.add(this)); var c = a.stack; this.componentDidCatch(a.value, { componentStack: c !== null ? c : "" }) }) } function a1(l, t, e, a, u) { if (e.flags |= 32768, a !== null && typeof a == "object" && typeof a.then == "function") { if (t = e.alternate, t !== null && ia(t, e, u, !0), e = nt.current, e !== null) { switch (e.tag) { case 31: case 13: return St === null ? En() : e.alternate === null && gl === 0 && (gl = 3), e.flags &= -257, e.flags |= 65536, e.lanes = u, a === $u ? e.flags |= 16384 : (t = e.updateQueue, t === null ? e.updateQueue = new Set([a]) : t.add(a), Bc(l, a, u)), !1; case 22: return e.flags |= 65536, a === $u ? e.flags |= 16384 : (t = e.updateQueue, t === null ? (t = { transitions: null, markerInstances: null, retryQueue: new Set([a]) }, e.updateQueue = t) : (e = t.retryQueue, e === null ? t.retryQueue = new Set([a]) : e.add(a)), Bc(l, a, u)), !1 }throw Error(y(435, e.tag)) } return Bc(l, a, u), En(), !1 } if ($) return t = nt.current, t !== null ? ((t.flags & 65536) === 0 && (t.flags |= 256), t.flags |= 65536, t.lanes = u, a !== xi && (l = Error(y(422), { cause: a }), La(mt(l, e)))) : (a !== xi && (t = Error(y(423), { cause: a }), La(mt(t, e))), l = l.current.alternate, l.flags |= 65536, u &= -u, l.lanes |= u, a = mt(a, e), u = oc(l.stateNode, a, u), Zi(l, u), gl !== 4 && (gl = 2)), !1; var n = Error(y(520), { cause: a }); if (n = mt(n, e), fu === null ? fu = [n] : fu.push(n), gl !== 4 && (gl = 2), t === null) return !0; a = mt(a, e), e = t; do { switch (e.tag) { case 3: return e.flags |= 65536, l = u & -u, e.lanes |= l, l = oc(e.stateNode, a, l), Zi(e, l), !1; case 1: if (t = e.type, n = e.stateNode, (e.flags & 128) === 0 && (typeof t.getDerivedStateFromError == "function" || n !== null && typeof n.componentDidCatch == "function" && (ye === null || !ye.has(n)))) return e.flags |= 65536, u &= -u, e.lanes |= u, u = xd(u), Hd(u, l, e, a), Zi(e, u), !1 }e = e.return } while (e !== null); return !1 } var yc = Error(y(461)), Tl = !1; function Bl(l, t, e, a) { t.child = l === null ? qs(t, null, e, a) : Ye(t, l.child, e, a) } function Rd(l, t, e, a, u) { e = e.render; var n = t.ref; if ("ref" in a) { var i = {}; for (var c in a) c !== "ref" && (i[c] = a[c]) } else i = a; return Ce(t), a = ki(l, t, e, i, n, u), c = Wi(), l !== null && !Tl ? ($i(l, t, u), Zt(l, t, u)) : ($ && c && ji(t), t.flags |= 1, Bl(l, t, a, u), t.child) } function Cd(l, t, e, a, u) { if (l === null) { var n = e.type; return typeof n == "function" && !Mi(n) && n.defaultProps === void 0 && e.compare === null ? (t.tag = 15, t.type = n, Dd(l, t, n, a, u)) : (l = Vu(e.type, null, a, t, t.mode, u), l.ref = t.ref, l.return = t, t.child = l) } if (n = l.child, !zc(l, u)) { var i = n.memoizedProps; if (e = e.compare, e = e !== null ? e : Xa, e(i, a) && l.ref === t.ref) return Zt(l, t, u) } return t.flags |= 1, l = qt(n, a), l.ref = t.ref, l.return = t, t.child = l } function Dd(l, t, e, a, u) { if (l !== null) { var n = l.memoizedProps; if (Xa(n, a) && l.ref === t.ref) if (Tl = !1, t.pendingProps = a = n, zc(l, u)) (l.flags & 131072) !== 0 && (Tl = !0); else return t.lanes = l.lanes, Zt(l, t, u) } return mc(l, t, e, a, u) } function qd(l, t, e, a) { var u = a.children, n = l !== null ? l.memoizedState : null; if (l === null && t.stateNode === null && (t.stateNode = { _visibility: 1, _pendingMarkers: null, _retryCache: null, _transitions: null }), a.mode === "hidden") { if ((t.flags & 128) !== 0) { if (n = n !== null ? n.baseLanes | e : e, l !== null) { for (a = t.child = l.child, u = 0; a !== null;)u = u | a.lanes | a.childLanes, a = a.sibling; a = u & ~n } else a = 0, t.child = null; return Bd(l, t, n, e, a) } if ((e & 536870912) !== 0) t.memoizedState = { baseLanes: 0, cachePool: null }, l !== null && ku(t, n !== null ? n.cachePool : null), n !== null ? Gs(t, n) : Vi(), Xs(t); else return a = t.lanes = 536870912, Bd(l, t, n !== null ? n.baseLanes | e : e, e, a) } else n !== null ? (ku(t, n.cachePool), Gs(t, n), se(), t.memoizedState = null) : (l !== null && ku(t, null), Vi(), se()); return Bl(l, t, u, e), t.child } function tu(l, t) { return l !== null && l.tag === 22 || t.stateNode !== null || (t.stateNode = { _visibility: 1, _pendingMarkers: null, _retryCache: null, _transitions: null }), t.sibling } function Bd(l, t, e, a, u) { var n = Yi(); return n = n === null ? null : { parent: zl._currentValue, pool: n }, t.memoizedState = { baseLanes: e, cachePool: n }, l !== null && ku(t, null), Vi(), Xs(t), l !== null && ia(l, t, a, !0), t.childLanes = u, null } function hn(l, t) { return t = yn({ mode: t.mode, children: t.children }, l.mode), t.ref = l.ref, l.child = t, t.return = l, t } function Yd(l, t, e) { return Ye(t, l.child, null, e), l = hn(t, t.pendingProps), l.flags |= 2, it(t), t.memoizedState = null, l } function u1(l, t, e) { var a = t.pendingProps, u = (t.flags & 128) !== 0; if (t.flags &= -129, l === null) { if ($) { if (a.mode === "hidden") return l = hn(t, a), t.lanes = 536870912, tu(null, l); if (Ji(t), (l = ol) ? (l = $0(l, gt), l = l !== null && l.data === "&" ? l : null, l !== null && (t.memoizedState = { dehydrated: l, treeContext: te !== null ? { id: Nt, overflow: jt } : null, retryLane: 536870912, hydrationErrors: null }, e = zs(l), e.return = t, t.child = e, Dl = t, ol = null)) : l = null, l === null) throw ae(t); return t.lanes = 536870912, null } return hn(t, a) } var n = l.memoizedState; if (n !== null) { var i = n.dehydrated; if (Ji(t), u) if (t.flags & 256) t.flags &= -257, t = Yd(l, t, e); else if (t.memoizedState !== null) t.child = l.child, t.flags |= 128, t = null; else throw Error(y(558)); else if (Tl || ia(l, t, e, !1), u = (e & l.childLanes) !== 0, Tl || u) { if (a = hl, a !== null && (i = Nf(a, e), i !== 0 && i !== n.retryLane)) throw n.retryLane = i, Ue(l, i), Pl(a, l, i), yc; En(), t = Yd(l, t, e) } else l = n.treeContext, ol = bt(i.nextSibling), Dl = t, $ = !0, ee = null, gt = !1, l !== null && As(t, l), t = hn(t, a), t.flags |= 4096; return t } return l = qt(l.child, { mode: a.mode, children: a.children }), l.ref = t.ref, t.child = l, l.return = t, l } function on(l, t) { var e = t.ref; if (e === null) l !== null && l.ref !== null && (t.flags |= 4194816); else { if (typeof e != "function" && typeof e != "object") throw Error(y(284)); (l === null || l.ref !== e) && (t.flags |= 4194816) } } function mc(l, t, e, a, u) { return Ce(t), e = ki(l, t, e, a, void 0, u), a = Wi(), l !== null && !Tl ? ($i(l, t, u), Zt(l, t, u)) : ($ && a && ji(t), t.flags |= 1, Bl(l, t, e, u), t.child) } function Gd(l, t, e, a, u, n) { return Ce(t), t.updateQueue = null, e = Zs(t, a, e, u), Qs(l), a = Wi(), l !== null && !Tl ? ($i(l, t, n), Zt(l, t, n)) : ($ && a && ji(t), t.flags |= 1, Bl(l, t, e, n), t.child) } function Xd(l, t, e, a, u) { if (Ce(t), t.stateNode === null) { var n = ea, i = e.contextType; typeof i == "object" && i !== null && (n = ql(i)), n = new e(a, n), t.memoizedState = n.state !== null && n.state !== void 0 ? n.state : null, n.updater = hc, t.stateNode = n, n._reactInternals = t, n = t.stateNode, n.props = a, n.state = t.memoizedState, n.refs = {}, Xi(t), i = e.contextType, n.context = typeof i == "object" && i !== null ? ql(i) : ea, n.state = t.memoizedState, i = e.getDerivedStateFromProps, typeof i == "function" && (dc(t, e, i, a), n.state = t.memoizedState), typeof e.getDerivedStateFromProps == "function" || typeof n.getSnapshotBeforeUpdate == "function" || typeof n.UNSAFE_componentWillMount != "function" && typeof n.componentWillMount != "function" || (i = n.state, typeof n.componentWillMount == "function" && n.componentWillMount(), typeof n.UNSAFE_componentWillMount == "function" && n.UNSAFE_componentWillMount(), i !== n.state && hc.enqueueReplaceState(n, n.state, null), $a(t, a, n, u), Wa(), n.state = t.memoizedState), typeof n.componentDidMount == "function" && (t.flags |= 4194308), a = !0 } else if (l === null) { n = t.stateNode; var c = t.memoizedProps, s = Xe(e, c); n.props = s; var r = n.context, b = e.contextType; i = ea, typeof b == "object" && b !== null && (i = ql(b)); var E = e.getDerivedStateFromProps; b = typeof E == "function" || typeof n.getSnapshotBeforeUpdate == "function", c = t.pendingProps !== c, b || typeof n.UNSAFE_componentWillReceiveProps != "function" && typeof n.componentWillReceiveProps != "function" || (c || r !== i) && Md(t, n, a, i), ne = !1; var g = t.memoizedState; n.state = g, $a(t, a, n, u), Wa(), r = t.memoizedState, c || g !== r || ne ? (typeof E == "function" && (dc(t, e, E, a), r = t.memoizedState), (s = ne || _d(t, e, s, a, g, r, i)) ? (b || typeof n.UNSAFE_componentWillMount != "function" && typeof n.componentWillMount != "function" || (typeof n.componentWillMount == "function" && n.componentWillMount(), typeof n.UNSAFE_componentWillMount == "function" && n.UNSAFE_componentWillMount()), typeof n.componentDidMount == "function" && (t.flags |= 4194308)) : (typeof n.componentDidMount == "function" && (t.flags |= 4194308), t.memoizedProps = a, t.memoizedState = r), n.props = a, n.state = r, n.context = i, a = s) : (typeof n.componentDidMount == "function" && (t.flags |= 4194308), a = !1) } else { n = t.stateNode, Qi(l, t), i = t.memoizedProps, b = Xe(e, i), n.props = b, E = t.pendingProps, g = n.context, r = e.contextType, s = ea, typeof r == "object" && r !== null && (s = ql(r)), c = e.getDerivedStateFromProps, (r = typeof c == "function" || typeof n.getSnapshotBeforeUpdate == "function") || typeof n.UNSAFE_componentWillReceiveProps != "function" && typeof n.componentWillReceiveProps != "function" || (i !== E || g !== s) && Md(t, n, a, s), ne = !1, g = t.memoizedState, n.state = g, $a(t, a, n, u), Wa(); var S = t.memoizedState; i !== E || g !== S || ne || l !== null && l.dependencies !== null && Ju(l.dependencies) ? (typeof c == "function" && (dc(t, e, c, a), S = t.memoizedState), (b = ne || _d(t, e, b, a, g, S, s) || l !== null && l.dependencies !== null && Ju(l.dependencies)) ? (r || typeof n.UNSAFE_componentWillUpdate != "function" && typeof n.componentWillUpdate != "function" || (typeof n.componentWillUpdate == "function" && n.componentWillUpdate(a, S, s), typeof n.UNSAFE_componentWillUpdate == "function" && n.UNSAFE_componentWillUpdate(a, S, s)), typeof n.componentDidUpdate == "function" && (t.flags |= 4), typeof n.getSnapshotBeforeUpdate == "function" && (t.flags |= 1024)) : (typeof n.componentDidUpdate != "function" || i === l.memoizedProps && g === l.memoizedState || (t.flags |= 4), typeof n.getSnapshotBeforeUpdate != "function" || i === l.memoizedProps && g === l.memoizedState || (t.flags |= 1024), t.memoizedProps = a, t.memoizedState = S), n.props = a, n.state = S, n.context = s, a = b) : (typeof n.componentDidUpdate != "function" || i === l.memoizedProps && g === l.memoizedState || (t.flags |= 4), typeof n.getSnapshotBeforeUpdate != "function" || i === l.memoizedProps && g === l.memoizedState || (t.flags |= 1024), a = !1) } return n = a, on(l, t), a = (t.flags & 128) !== 0, n || a ? (n = t.stateNode, e = a && typeof e.getDerivedStateFromError != "function" ? null : n.render(), t.flags |= 1, l !== null && a ? (t.child = Ye(t, l.child, null, u), t.child = Ye(t, null, e, u)) : Bl(l, t, e, u), t.memoizedState = n.state, l = t.child) : l = Zt(l, t, u), l } function Qd(l, t, e, a) { return He(), t.flags |= 256, Bl(l, t, e, a), t.child } var vc = { dehydrated: null, treeContext: null, retryLane: 0, hydrationErrors: null }; function rc(l) { return { baseLanes: l, cachePool: Us() } } function gc(l, t, e) { return l = l !== null ? l.childLanes & ~e : 0, t && (l |= ft), l } function Zd(l, t, e) { var a = t.pendingProps, u = !1, n = (t.flags & 128) !== 0, i; if ((i = n) || (i = l !== null && l.memoizedState === null ? !1 : (bl.current & 2) !== 0), i && (u = !0, t.flags &= -129), i = (t.flags & 32) !== 0, t.flags &= -33, l === null) { if ($) { if (u ? fe(t) : se(), (l = ol) ? (l = $0(l, gt), l = l !== null && l.data !== "&" ? l : null, l !== null && (t.memoizedState = { dehydrated: l, treeContext: te !== null ? { id: Nt, overflow: jt } : null, retryLane: 536870912, hydrationErrors: null }, e = zs(l), e.return = t, t.child = e, Dl = t, ol = null)) : l = null, l === null) throw ae(t); return Pc(l) ? t.lanes = 32 : t.lanes = 536870912, null } var c = a.children; return a = a.fallback, u ? (se(), u = t.mode, c = yn({ mode: "hidden", children: c }, u), a = xe(a, u, e, null), c.return = t, a.return = t, c.sibling = a, t.child = c, a = t.child, a.memoizedState = rc(e), a.childLanes = gc(l, i, e), t.memoizedState = vc, tu(null, a)) : (fe(t), Sc(t, c)) } var s = l.memoizedState; if (s !== null && (c = s.dehydrated, c !== null)) { if (n) t.flags & 256 ? (fe(t), t.flags &= -257, t = bc(l, t, e)) : t.memoizedState !== null ? (se(), t.child = l.child, t.flags |= 128, t = null) : (se(), c = a.fallback, u = t.mode, a = yn({ mode: "visible", children: a.children }, u), c = xe(c, u, e, null), c.flags |= 2, a.return = t, c.return = t, a.sibling = c, t.child = a, Ye(t, l.child, null, e), a = t.child, a.memoizedState = rc(e), a.childLanes = gc(l, i, e), t.memoizedState = vc, t = tu(null, a)); else if (fe(t), Pc(c)) { if (i = c.nextSibling && c.nextSibling.dataset, i) var r = i.dgst; i = r, a = Error(y(419)), a.stack = "", a.digest = i, La({ value: a, source: null, stack: null }), t = bc(l, t, e) } else if (Tl || ia(l, t, e, !1), i = (e & l.childLanes) !== 0, Tl || i) { if (i = hl, i !== null && (a = Nf(i, e), a !== 0 && a !== s.retryLane)) throw s.retryLane = a, Ue(l, a), Pl(i, l, a), yc; Ic(c) || En(), t = bc(l, t, e) } else Ic(c) ? (t.flags |= 192, t.child = l.child, t = null) : (l = s.treeContext, ol = bt(c.nextSibling), Dl = t, $ = !0, ee = null, gt = !1, l !== null && As(t, l), t = Sc(t, a.children), t.flags |= 4096); return t } return u ? (se(), c = a.fallback, u = t.mode, s = l.child, r = s.sibling, a = qt(s, { mode: "hidden", children: a.children }), a.subtreeFlags = s.subtreeFlags & 65011712, r !== null ? c = qt(r, c) : (c = xe(c, u, e, null), c.flags |= 2), c.return = t, a.return = t, a.sibling = c, t.child = a, tu(null, a), a = t.child, c = l.child.memoizedState, c === null ? c = rc(e) : (u = c.cachePool, u !== null ? (s = zl._currentValue, u = u.parent !== s ? { parent: s, pool: s } : u) : u = Us(), c = { baseLanes: c.baseLanes | e, cachePool: u }), a.memoizedState = c, a.childLanes = gc(l, i, e), t.memoizedState = vc, tu(l.child, a)) : (fe(t), e = l.child, l = e.sibling, e = qt(e, { mode: "visible", children: a.children }), e.return = t, e.sibling = null, l !== null && (i = t.deletions, i === null ? (t.deletions = [l], t.flags |= 16) : i.push(l)), t.child = e, t.memoizedState = null, e) } function Sc(l, t) { return t = yn({ mode: "visible", children: t }, l.mode), t.return = l, l.child = t } function yn(l, t) { return l = ut(22, l, null, t), l.lanes = 0, l } function bc(l, t, e) { return Ye(t, l.child, null, e), l = Sc(t, t.pendingProps.children), l.flags |= 2, t.memoizedState = null, l } function Ld(l, t, e) { l.lanes |= t; var a = l.alternate; a !== null && (a.lanes |= t), Ci(l.return, t, e) } function pc(l, t, e, a, u, n) { var i = l.memoizedState; i === null ? l.memoizedState = { isBackwards: t, rendering: null, renderingStartTime: 0, last: a, tail: e, tailMode: u, treeForkCount: n } : (i.isBackwards = t, i.rendering = null, i.renderingStartTime = 0, i.last = a, i.tail = e, i.tailMode = u, i.treeForkCount = n) } function Vd(l, t, e) { var a = t.pendingProps, u = a.revealOrder, n = a.tail; a = a.children; var i = bl.current, c = (i & 2) !== 0; if (c ? (i = i & 1 | 2, t.flags |= 128) : i &= 1, O(bl, i), Bl(l, t, a, e), a = $ ? Za : 0, !c && l !== null && (l.flags & 128) !== 0) l: for (l = t.child; l !== null;) { if (l.tag === 13) l.memoizedState !== null && Ld(l, e, t); else if (l.tag === 19) Ld(l, e, t); else if (l.child !== null) { l.child.return = l, l = l.child; continue } if (l === t) break l; for (; l.sibling === null;) { if (l.return === null || l.return === t) break l; l = l.return } l.sibling.return = l.return, l = l.sibling } switch (u) { case "forwards": for (e = t.child, u = null; e !== null;)l = e.alternate, l !== null && ln(l) === null && (u = e), e = e.sibling; e = u, e === null ? (u = t.child, t.child = null) : (u = e.sibling, e.sibling = null), pc(t, !1, u, e, n, a); break; case "backwards": case "unstable_legacy-backwards": for (e = null, u = t.child, t.child = null; u !== null;) { if (l = u.alternate, l !== null && ln(l) === null) { t.child = u; break } l = u.sibling, u.sibling = e, e = u, u = l } pc(t, !0, e, null, n, a); break; case "together": pc(t, !1, null, null, void 0, a); break; default: t.memoizedState = null }return t.child } function Zt(l, t, e) { if (l !== null && (t.dependencies = l.dependencies), oe |= t.lanes, (e & t.childLanes) === 0) if (l !== null) { if (ia(l, t, e, !1), (e & t.childLanes) === 0) return null } else return null; if (l !== null && t.child !== l.child) throw Error(y(153)); if (t.child !== null) { for (l = t.child, e = qt(l, l.pendingProps), t.child = e, e.return = t; l.sibling !== null;)l = l.sibling, e = e.sibling = qt(l, l.pendingProps), e.return = t; e.sibling = null } return t.child } function zc(l, t) { return (l.lanes & t) !== 0 ? !0 : (l = l.dependencies, !!(l !== null && Ju(l))) } function n1(l, t, e) { switch (t.tag) { case 3: Ll(t, t.stateNode.containerInfo), ue(t, zl, l.memoizedState.cache), He(); break; case 27: case 5: Oa(t); break; case 4: Ll(t, t.stateNode.containerInfo); break; case 10: ue(t, t.type, t.memoizedProps.value); break; case 31: if (t.memoizedState !== null) return t.flags |= 128, Ji(t), null; break; case 13: var a = t.memoizedState; if (a !== null) return a.dehydrated !== null ? (fe(t), t.flags |= 128, null) : (e & t.child.childLanes) !== 0 ? Zd(l, t, e) : (fe(t), l = Zt(l, t, e), l !== null ? l.sibling : null); fe(t); break; case 19: var u = (l.flags & 128) !== 0; if (a = (e & t.childLanes) !== 0, a || (ia(l, t, e, !1), a = (e & t.childLanes) !== 0), u) { if (a) return Vd(l, t, e); t.flags |= 128 } if (u = t.memoizedState, u !== null && (u.rendering = null, u.tail = null, u.lastEffect = null), O(bl, bl.current), a) break; return null; case 22: return t.lanes = 0, qd(l, t, e, t.pendingProps); case 24: ue(t, zl, l.memoizedState.cache) }return Zt(l, t, e) } function Kd(l, t, e) { if (l !== null) if (l.memoizedProps !== t.pendingProps) Tl = !0; else { if (!zc(l, e) && (t.flags & 128) === 0) return Tl = !1, n1(l, t, e); Tl = (l.flags & 131072) !== 0 } else Tl = !1, $ && (t.flags & 1048576) !== 0 && Ts(t, Za, t.index); switch (t.lanes = 0, t.tag) { case 16: l: { var a = t.pendingProps; if (l = qe(t.elementType), t.type = l, typeof l == "function") Mi(l) ? (a = Xe(l, a), t.tag = 1, t = Xd(null, t, l, a, e)) : (t.tag = 0, t = mc(null, t, l, a, e)); else { if (l != null) { var u = l.$$typeof; if (u === Kl) { t.tag = 11, t = Rd(null, t, l, a, e); break l } else if (u === j) { t.tag = 14, t = Cd(null, t, l, a, e); break l } } throw t = Ht(l) || l, Error(y(306, t, "")) } } return t; case 0: return mc(l, t, t.type, t.pendingProps, e); case 1: return a = t.type, u = Xe(a, t.pendingProps), Xd(l, t, a, u, e); case 3: l: { if (Ll(t, t.stateNode.containerInfo), l === null) throw Error(y(387)); a = t.pendingProps; var n = t.memoizedState; u = n.element, Qi(l, t), $a(t, a, null, e); var i = t.memoizedState; if (a = i.cache, ue(t, zl, a), a !== n.cache && Di(t, [zl], e, !0), Wa(), a = i.element, n.isDehydrated) if (n = { element: a, isDehydrated: !1, cache: i.cache }, t.updateQueue.baseState = n, t.memoizedState = n, t.flags & 256) { t = Qd(l, t, a, e); break l } else if (a !== u) { u = mt(Error(y(424)), t), La(u), t = Qd(l, t, a, e); break l } else { switch (l = t.stateNode.containerInfo, l.nodeType) { case 9: l = l.body; break; default: l = l.nodeName === "HTML" ? l.ownerDocument.body : l }for (ol = bt(l.firstChild), Dl = t, $ = !0, ee = null, gt = !0, e = qs(t, null, a, e), t.child = e; e;)e.flags = e.flags & -3 | 4096, e = e.sibling } else { if (He(), a === u) { t = Zt(l, t, e); break l } Bl(l, t, a, e) } t = t.child } return t; case 26: return on(l, t), l === null ? (e = eh(t.type, null, t.pendingProps, null)) ? t.memoizedState = e : $ || (e = t.type, l = t.pendingProps, a = jn(K.current).createElement(e), a[Cl] = t, a[wl] = l, Yl(a, e, l), jl(a), t.stateNode = a) : t.memoizedState = eh(t.type, l.memoizedProps, t.pendingProps, l.memoizedState), null; case 27: return Oa(t), l === null && $ && (a = t.stateNode = P0(t.type, t.pendingProps, K.current), Dl = t, gt = !0, u = ol, ge(t.type) ? (lf = u, ol = bt(a.firstChild)) : ol = u), Bl(l, t, t.pendingProps.children, e), on(l, t), l === null && (t.flags |= 4194304), t.child; case 5: return l === null && $ && ((u = a = ol) && (a = D1(a, t.type, t.pendingProps, gt), a !== null ? (t.stateNode = a, Dl = t, ol = bt(a.firstChild), gt = !1, u = !0) : u = !1), u || ae(t)), Oa(t), u = t.type, n = t.pendingProps, i = l !== null ? l.memoizedProps : null, a = n.children, Wc(u, n) ? a = null : i !== null && Wc(u, i) && (t.flags |= 32), t.memoizedState !== null && (u = ki(l, t, $o, null, null, e), ru._currentValue = u), on(l, t), Bl(l, t, a, e), t.child; case 6: return l === null && $ && ((l = e = ol) && (e = q1(e, t.pendingProps, gt), e !== null ? (t.stateNode = e, Dl = t, ol = null, l = !0) : l = !1), l || ae(t)), null; case 13: return Zd(l, t, e); case 4: return Ll(t, t.stateNode.containerInfo), a = t.pendingProps, l === null ? t.child = Ye(t, null, a, e) : Bl(l, t, a, e), t.child; case 11: return Rd(l, t, t.type, t.pendingProps, e); case 7: return Bl(l, t, t.pendingProps, e), t.child; case 8: return Bl(l, t, t.pendingProps.children, e), t.child; case 12: return Bl(l, t, t.pendingProps.children, e), t.child; case 10: return a = t.pendingProps, ue(t, t.type, a.value), Bl(l, t, a.children, e), t.child; case 9: return u = t.type._context, a = t.pendingProps.children, Ce(t), u = ql(u), a = a(u), t.flags |= 1, Bl(l, t, a, e), t.child; case 14: return Cd(l, t, t.type, t.pendingProps, e); case 15: return Dd(l, t, t.type, t.pendingProps, e); case 19: return Vd(l, t, e); case 31: return u1(l, t, e); case 22: return qd(l, t, e, t.pendingProps); case 24: return Ce(t), a = ql(zl), l === null ? (u = Yi(), u === null && (u = hl, n = qi(), u.pooledCache = n, n.refCount++, n !== null && (u.pooledCacheLanes |= e), u = n), t.memoizedState = { parent: a, cache: u }, Xi(t), ue(t, zl, u)) : ((l.lanes & e) !== 0 && (Qi(l, t), $a(t, null, null, e), Wa()), u = l.memoizedState, n = t.memoizedState, u.parent !== a ? (u = { parent: a, cache: a }, t.memoizedState = u, t.lanes === 0 && (t.memoizedState = t.updateQueue.baseState = u), ue(t, zl, a)) : (a = n.cache, ue(t, zl, a), a !== u.cache && Di(t, [zl], e, !0))), Bl(l, t, t.pendingProps.children, e), t.child; case 29: throw t.pendingProps }throw Error(y(156, t.tag)) } function Lt(l) { l.flags |= 4 } function Ec(l, t, e, a, u) { if ((t = (l.mode & 32) !== 0) && (t = !1), t) { if (l.flags |= 16777216, (u & 335544128) === u) if (l.stateNode.complete) l.flags |= 8192; else if (S0()) l.flags |= 8192; else throw Be = $u, Gi } else l.flags &= -16777217 } function Jd(l, t) { if (t.type !== "stylesheet" || (t.state.loading & 4) !== 0) l.flags &= -16777217; else if (l.flags |= 16777216, !ch(t)) if (S0()) l.flags |= 8192; else throw Be = $u, Gi } function mn(l, t) { t !== null && (l.flags |= 4), l.flags & 16384 && (t = l.tag !== 22 ? _f() : 536870912, l.lanes |= t, Sa |= t) } function eu(l, t) { if (!$) switch (l.tailMode) { case "hidden": t = l.tail; for (var e = null; t !== null;)t.alternate !== null && (e = t), t = t.sibling; e === null ? l.tail = null : e.sibling = null; break; case "collapsed": e = l.tail; for (var a = null; e !== null;)e.alternate !== null && (a = e), e = e.sibling; a === null ? t || l.tail === null ? l.tail = null : l.tail.sibling = null : a.sibling = null } } function yl(l) { var t = l.alternate !== null && l.alternate.child === l.child, e = 0, a = 0; if (t) for (var u = l.child; u !== null;)e |= u.lanes | u.childLanes, a |= u.subtreeFlags & 65011712, a |= u.flags & 65011712, u.return = l, u = u.sibling; else for (u = l.child; u !== null;)e |= u.lanes | u.childLanes, a |= u.subtreeFlags, a |= u.flags, u.return = l, u = u.sibling; return l.subtreeFlags |= a, l.childLanes = e, t } function i1(l, t, e) { var a = t.pendingProps; switch (Ui(t), t.tag) { case 16: case 15: case 0: case 11: case 7: case 8: case 12: case 9: case 14: return yl(t), null; case 1: return yl(t), null; case 3: return e = t.stateNode, a = null, l !== null && (a = l.memoizedState.cache), t.memoizedState.cache !== a && (t.flags |= 2048), Gt(zl), Sl(), e.pendingContext && (e.context = e.pendingContext, e.pendingContext = null), (l === null || l.child === null) && (na(t) ? Lt(t) : l === null || l.memoizedState.isDehydrated && (t.flags & 256) === 0 || (t.flags |= 1024, Hi())), yl(t), null; case 26: var u = t.type, n = t.memoizedState; return l === null ? (Lt(t), n !== null ? (yl(t), Jd(t, n)) : (yl(t), Ec(t, u, null, a, e))) : n ? n !== l.memoizedState ? (Lt(t), yl(t), Jd(t, n)) : (yl(t), t.flags &= -16777217) : (l = l.memoizedProps, l !== a && Lt(t), yl(t), Ec(t, u, l, a, e)), null; case 27: if (Au(t), e = K.current, u = t.type, l !== null && t.stateNode != null) l.memoizedProps !== a && Lt(t); else { if (!a) { if (t.stateNode === null) throw Error(y(166)); return yl(t), null } l = x.current, na(t) ? _s(t) : (l = P0(u, a, e), t.stateNode = l, Lt(t)) } return yl(t), null; case 5: if (Au(t), u = t.type, l !== null && t.stateNode != null) l.memoizedProps !== a && Lt(t); else { if (!a) { if (t.stateNode === null) throw Error(y(166)); return yl(t), null } if (n = x.current, na(t)) _s(t); else { var i = jn(K.current); switch (n) { case 1: n = i.createElementNS("http://www.w3.org/2000/svg", u); break; case 2: n = i.createElementNS("http://www.w3.org/1998/Math/MathML", u); break; default: switch (u) { case "svg": n = i.createElementNS("http://www.w3.org/2000/svg", u); break; case "math": n = i.createElementNS("http://www.w3.org/1998/Math/MathML", u); break; case "script": n = i.createElement("div"), n.innerHTML = " + + + +
    + + diff --git a/agents/adk/new-hire-onboarding/app/static/live-onboarding/olivia-bennett.jpg b/agents/adk/new-hire-onboarding/app/static/live-onboarding/olivia-bennett.jpg new file mode 100644 index 0000000..395cd41 Binary files /dev/null and b/agents/adk/new-hire-onboarding/app/static/live-onboarding/olivia-bennett.jpg differ diff --git a/agents/adk/new-hire-onboarding/app/tools.py b/agents/adk/new-hire-onboarding/app/tools.py new file mode 100644 index 0000000..7aaad1b --- /dev/null +++ b/agents/adk/new-hire-onboarding/app/tools.py @@ -0,0 +1,133 @@ +# Copyright 2026 Google LLC +# +# 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 +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from google.adk.tools import ToolContext + +from app.state_schema import OnboardingStep + + +def send_welcome_packet( + name: str, email: str, start_date: str, tool_context: ToolContext +) -> dict: + """Sends the initial welcome packet and document signature links to the new hire. + + Args: + name: Full name of the new hire. + email: Email address of the new hire. + start_date: The employee's official start date (YYYY-MM-DD). + + Returns: + A dictionary containing delivery status and links to sign. + """ + state = tool_context.state + state["new_hire_details"] = { + "name": name, + "email": email, + "start_date": start_date, + } + state["current_step"] = OnboardingStep.WELCOME_SENT + state["pending_signals"] = ["document_signed"] + + return { + "status": "success", + "message": f"Welcome packet sent to {name} ({email}). Documents pending signature.", + "signature_link": f"https://onboarding.example.com/sign?email={email}", + } + + +def provision_software_accounts(username: str, tool_context: ToolContext) -> dict: + """Provisions corporate software accounts (email, Slack) for the new hire. + + Args: + username: Desired corporate username prefix. + + Returns: + A dictionary containing the generated credentials and status. + """ + state = tool_context.state + email = f"{username}@example.com" + + state["current_step"] = OnboardingStep.IT_PROVISIONED + state["new_hire_details"]["corporate_email"] = email + + if "document_signed" in state.get("pending_signals", []): + state["pending_signals"].remove("document_signed") + + state["pending_signals"].append("hardware_delivered") + + return { + "status": "success", + "corporate_email": email, + "slack_user": f"@{username}", + "temporary_password": "TempPassword2026!", + } + + +def check_hardware_delivery(tracking_id: str, tool_context: ToolContext) -> dict: + """Queries the shipping carrier API to check delivery status of the new hire's laptop. + + Args: + tracking_id: The shipment tracking number (e.g., HW-12345). + + Returns: + A dictionary containing shipping status. + """ + state = tool_context.state + + if tracking_id.startswith("HW-"): + state["current_step"] = OnboardingStep.HARDWARE_DELIVERED + + if "hardware_delivered" in state.get("pending_signals", []): + state["pending_signals"].remove("hardware_delivered") + + return { + "status": "delivered", + "tracking_id": tracking_id, + "carrier": "FedEx", + "signed_by": state.get("new_hire_details", {}).get("name", "Resident"), + } + + return { + "status": "in_transit", + "tracking_id": tracking_id, + "message": "Package is in transit to employee residence.", + } + + +def send_day_one_schedule(email: str, tool_context: ToolContext) -> dict: + """Sends the personalized Day One onboarding itinerary to the employee's corporate email. + + Args: + email: The corporate email address of the employee. + + Returns: + A dictionary confirming completion status. + """ + state = tool_context.state + state["current_step"] = OnboardingStep.COMPLETED + state["pending_signals"] = [] + + name = state.get("new_hire_details", {}).get("name", "New Hire") + + return { + "status": "success", + "message": f"Personalized Day One schedule sent successfully to {name} at {email}.", + "itinerary": [ + "09:00 AM - Welcome & IT Login Setup", + "10:00 AM - Meet the Manager & Team Intro", + "11:30 AM - Platform Architecture Overview", + "01:00 PM - Lunch break", + "02:00 PM - ADK workflow walkthrough", + ], + } diff --git a/agents/adk/new-hire-onboarding/assets/live-onboarding-overview.png b/agents/adk/new-hire-onboarding/assets/live-onboarding-overview.png new file mode 100644 index 0000000..ef6282c Binary files /dev/null and b/agents/adk/new-hire-onboarding/assets/live-onboarding-overview.png differ diff --git a/agents/adk/new-hire-onboarding/frontend/live-onboarding/index.html b/agents/adk/new-hire-onboarding/frontend/live-onboarding/index.html new file mode 100644 index 0000000..4d01e47 --- /dev/null +++ b/agents/adk/new-hire-onboarding/frontend/live-onboarding/index.html @@ -0,0 +1,12 @@ + + + + + + Live Onboarding Desk + + +
    + + + diff --git a/agents/adk/new-hire-onboarding/frontend/live-onboarding/package.json b/agents/adk/new-hire-onboarding/frontend/live-onboarding/package.json new file mode 100644 index 0000000..d825335 --- /dev/null +++ b/agents/adk/new-hire-onboarding/frontend/live-onboarding/package.json @@ -0,0 +1,24 @@ +{ + "name": "live-onboarding-desk", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "dev": "vite --host 127.0.0.1 --port 5173", + "build": "vite build", + "preview": "vite preview --host 127.0.0.1 --port 5173", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "lucide-react": "^0.468.0", + "react": "^19.0.0", + "react-dom": "^19.0.0" + }, + "devDependencies": { + "@types/react": "^19.2.14", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^5.0.4", + "typescript": "^5.7.2", + "vite": "^6.0.5" + } +} diff --git a/agents/adk/new-hire-onboarding/frontend/live-onboarding/public/olivia-bennett.jpg b/agents/adk/new-hire-onboarding/frontend/live-onboarding/public/olivia-bennett.jpg new file mode 100644 index 0000000..395cd41 Binary files /dev/null and b/agents/adk/new-hire-onboarding/frontend/live-onboarding/public/olivia-bennett.jpg differ diff --git a/agents/adk/new-hire-onboarding/frontend/live-onboarding/src/App.tsx b/agents/adk/new-hire-onboarding/frontend/live-onboarding/src/App.tsx new file mode 100644 index 0000000..8139248 --- /dev/null +++ b/agents/adk/new-hire-onboarding/frontend/live-onboarding/src/App.tsx @@ -0,0 +1,777 @@ +import { + Activity, + BadgeCheck, + Box, + CalendarDays, + CheckCircle2, + ChevronRight, + ClipboardSignature, + FileCheck2, + FileText, + Laptop, + Mail, + PackageCheck, + Play, + RefreshCw, + Send, + ShieldCheck, + Sparkles, + UserRound, + Workflow, +} from "lucide-react"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { + confirmHardware, + getCase, + getCurrentCase, + signPacket, + startCase, +} from "./api"; +import type { Artifact, CaseEvent, LiveCase } from "./types"; + +type BusyAction = "start" | "sign" | "hardware" | "refresh" | null; + +const steps = [ + { + id: "WELCOME_SENT", + title: "Packet sent", + detail: "Local document generated", + icon: Mail, + pill: "thinking", + pillLabel: "Thinking", + }, + { + id: "DOCUMENTS_SIGNED", + title: "Signature", + detail: "Employee signs locally", + icon: ClipboardSignature, + pill: "read", + pillLabel: "Reading", + }, + { + id: "IT_PROVISIONED", + title: "IT ready", + detail: "ADK wake completes", + icon: Laptop, + pill: "edit", + pillLabel: "Editing", + }, + { + id: "HARDWARE_DELIVERED", + title: "Laptop", + detail: "Delivery confirmed", + icon: PackageCheck, + pill: "grep", + pillLabel: "Grepping", + }, + { + id: "COMPLETED", + title: "Day One", + detail: "Schedule artifact stored", + icon: CalendarDays, + pill: "done", + pillLabel: "Done", + }, +]; + +const artifactCopy: Record = { + "welcome-packet": "Employee-facing packet before signature.", + "signed-packet": "Signed local artifact saved after employee action.", + "hardware-receipt": "Receipt generated from the employee delivery confirmation.", + "day-one-schedule": "Final itinerary after the second ADK wake turn.", +}; + +const artifactThumbCopy: Record = { + "welcome-packet": "Packet", + "signed-packet": "Signed", + "hardware-receipt": "Receipt", + "day-one-schedule": "Brief", +}; + +function stepIndex(step: string | undefined) { + return Math.max(0, steps.findIndex((item) => item.id === step)); +} + +function statusLabel(caseData: LiveCase | null) { + if (!caseData) return "Backend not started"; + if (caseData.status === "completed") return "Day One ready"; + if (caseData.status.includes("waking")) return "ADK wake turn running"; + if (!caseData.document_signed) return "Waiting for employee signature"; + if (!caseData.hardware_delivered) return "Waiting for laptop delivery"; + return "Processing"; +} + +function waitingOn(caseData: LiveCase | null) { + if (!caseData) return "Create case"; + if (caseData.pending_signals.length) return caseData.pending_signals.join(", "); + if (caseData.status === "completed") return "Nothing"; + if (caseData.status.includes("waking")) return "Runner completion"; + return "Employee action"; +} + +function isWakeRunning(caseData: LiveCase | null) { + return Boolean(caseData?.status.includes("waking")); +} + +function getSelectedArtifact(caseData: LiveCase | null, selectedId: string | null) { + if (!caseData?.artifacts.length) return null; + return ( + caseData.artifacts.find((artifact) => artifact.id === selectedId) ?? + caseData.artifacts[0] + ); +} + +function hasArtifact(caseData: LiveCase, artifactId: string) { + return caseData.artifacts.some((artifact) => artifact.id === artifactId); +} + +function nextAction(caseData: LiveCase | null) { + const firstName = employeeFirstName(caseData); + if (!caseData) return `Start the HR case to generate ${firstName}'s packet.`; + if (!caseData.document_signed) return `Review the packet and sign it as ${firstName}.`; + if (!caseData.hardware_delivered) return `Confirm that ${firstName} received the laptop.`; + return "Open the Day One schedule artifact."; +} + +function employeeFirstName(caseData: LiveCase | null) { + return caseData?.employee.name.split(" ")[0] ?? "Olivia"; +} + +function formatStep(step: string) { + return step.replaceAll("_", " "); +} + +function nextSignalLabel(caseData: LiveCase | null) { + if (!caseData) return "Ready to start"; + if (caseData.pending_signals.length) return caseData.pending_signals.join(", "); + if (caseData.status === "completed") return "Complete"; + if (caseData.status.includes("waking")) return "ADK runner"; + return "Employee action"; +} + +function activeStateDetail(caseData: LiveCase | null) { + if (!caseData) return "Create a case to generate the first local packet."; + if (caseData.status.includes("waking")) return "Webhook received; ADK is resuming the paused run."; + if (!caseData.document_signed) return "Paused until the employee signs the generated packet."; + if (!caseData.hardware_delivered) return "Paused until the employee confirms laptop delivery."; + return "All required artifacts have been generated and stored locally."; +} + +function agentTickerRows(caseData: LiveCase | null) { + const waking = isWakeRunning(caseData); + return [ + { + label: waking ? "ADK wake turn running" : "Waiting for backend signal", + pill: waking ? "grep" : "thinking", + }, + { + label: caseData?.document_signed ? "Signed packet confirmed" : "Packet unsigned", + pill: caseData?.document_signed ? "done" : "read", + }, + { + label: caseData?.hardware_delivered ? "Delivery confirmed" : "Hardware pending", + pill: caseData?.hardware_delivered ? "done" : "edit", + }, + ]; +} + +function App() { + const [caseData, setCaseData] = useState(null); + const [selectedArtifactId, setSelectedArtifactId] = useState(null); + const [busyAction, setBusyAction] = useState(null); + const [error, setError] = useState(null); + const completedCaseSequenceStarted = useRef(null); + const completedArtifactTimer = useRef(null); + + const selectedArtifact = useMemo( + () => getSelectedArtifact(caseData, selectedArtifactId), + [caseData, selectedArtifactId], + ); + + const applyCase = useCallback((nextCase: LiveCase) => { + setCaseData(nextCase); + setSelectedArtifactId((current) => { + if ( + nextCase.status === "completed" && + hasArtifact(nextCase, "hardware-receipt") && + hasArtifact(nextCase, "day-one-schedule") && + completedCaseSequenceStarted.current !== nextCase.id + ) { + return "hardware-receipt"; + } + if (current && nextCase.artifacts.some((artifact) => artifact.id === current)) { + return current; + } + return nextCase.artifacts[0]?.id ?? null; + }); + }, []); + + const refreshCase = useCallback(async () => { + if (!caseData?.id) return; + const payload = await getCase(caseData.id); + if (payload.active) applyCase(payload.case); + }, [applyCase, caseData?.id]); + + useEffect(() => { + let cancelled = false; + + getCurrentCase() + .then((payload) => { + if (!cancelled && payload.active) applyCase(payload.case); + }) + .catch((err: Error) => { + if (!cancelled) setError(err.message); + }); + + return () => { + cancelled = true; + }; + }, [applyCase]); + + useEffect(() => { + if (!caseData?.id || caseData.status === "completed") return undefined; + const timer = window.setInterval(() => { + refreshCase().catch((err: Error) => setError(err.message)); + }, 1200); + return () => window.clearInterval(timer); + }, [caseData?.id, caseData?.status, refreshCase]); + + useEffect(() => { + if ( + !caseData || + caseData.status !== "completed" || + completedCaseSequenceStarted.current === caseData.id || + !hasArtifact(caseData, "hardware-receipt") || + !hasArtifact(caseData, "day-one-schedule") + ) { + return; + } + + completedCaseSequenceStarted.current = caseData.id; + setSelectedArtifactId("hardware-receipt"); + if (completedArtifactTimer.current) { + window.clearTimeout(completedArtifactTimer.current); + } + completedArtifactTimer.current = window.setTimeout(() => { + setSelectedArtifactId((current) => + current === "hardware-receipt" ? "day-one-schedule" : current, + ); + }, 3200); + + return () => { + if (completedArtifactTimer.current) { + window.clearTimeout(completedArtifactTimer.current); + completedArtifactTimer.current = null; + } + }; + }, [caseData]); + + async function runAction(action: BusyAction, task: () => Promise) { + setBusyAction(action); + setError(null); + try { + await task(); + } catch (err) { + setError(err instanceof Error ? err.message : String(err)); + } finally { + setBusyAction(null); + } + } + + const handleStart = () => + runAction("start", async () => { + completedCaseSequenceStarted.current = null; + if (completedArtifactTimer.current) { + window.clearTimeout(completedArtifactTimer.current); + completedArtifactTimer.current = null; + } + const payload = await startCase(); + if (payload.active) applyCase(payload.case); + }); + + const handleSign = () => + runAction("sign", async () => { + if (!caseData) return; + const payload = await signPacket(caseData.id); + if (payload.active) { + applyCase(payload.case); + setSelectedArtifactId("signed-packet"); + } + }); + + const handleHardware = () => + runAction("hardware", async () => { + if (!caseData) return; + const payload = await confirmHardware(caseData.id); + if (payload.active) { + applyCase(payload.case); + } + }); + + const handleRefresh = () => + runAction("refresh", async () => { + await refreshCase(); + }); + + const completedCount = caseData ? stepIndex(caseData.current_step) + 1 : 0; + const waking = isWakeRunning(caseData); + const canSign = Boolean(caseData && !caseData.document_signed && !waking); + const canConfirmHardware = Boolean( + caseData?.document_signed && + !caseData.hardware_delivered && + caseData.pending_signals.includes("hardware_delivered") && + !waking, + ); + const isBusy = busyAction !== null; + + return ( +
    +
    +
    +
    +
    +
    +

    Live Onboarding Desk

    +

    Long-running ADK agents that can pause and resume

    +
    +
    + +
    +
    + + {caseData ? "Backend case live" : "Ready to create case"} +
    + + +
    +
    + + {error ?
    {error}
    : null} + +
    +
    + Current workflow +

    {statusLabel(caseData)}

    +
    +
    + + + + +
    +
    + + + +
    + + +
    +
    +
    +
    + ); +} + +function ActiveStateBand({ caseData }: { caseData: LiveCase | null }) { + const waking = isWakeRunning(caseData); + + return ( +
    +
    + Active case state + {statusLabel(caseData)} +

    {activeStateDetail(caseData)}

    +
    +
    + + + {caseData?.adk_status ?? "No ADK run yet"} +
    +
    + ); +} + +function Metric({ label, value }: { label: string; value: string }) { + return ( +
    + {label} + {value} +
    + ); +} + +function PanelHeader({ + title, + subtitle, + icon, + trailing, +}: { + title: string; + subtitle: string; + icon: React.ReactNode; + trailing?: React.ReactNode; +}) { + return ( +
    +
    + {icon} +
    +

    {title}

    +

    {subtitle}

    +
    +
    + {trailing} +
    + ); +} + +function EmployeeCard({ caseData }: { caseData: LiveCase | null }) { + const employee = caseData?.employee; + const photoUrl = employee?.photo_url ?? "/live-onboarding/olivia-bennett.jpg"; + + return ( +
    + {employee?.name +
    +

    {employee?.name ?? "Olivia Bennett"}

    +

    + {employee?.role ?? "Product Manager"} · {employee?.team ?? "Platform Systems"} +

    +
    +
    + Start + {employee?.start_date ?? "2026-06-01"} +
    +
    + ); +} + +function AgentTicker({ caseData }: { caseData: LiveCase | null }) { + const rows = agentTickerRows(caseData); + + return ( +
    +
    +

    Agent activity

    + {isWakeRunning(caseData) ? "running" : "idle"} +
    +
    + {rows.map((row) => ( +
    + {row.pill} + {row.label} +
    + ))} +
    +
    + ); +} + +function ProgressRail({ + currentStep, + completedCount, +}: { + currentStep?: string; + completedCount: number; +}) { + const currentIndex = stepIndex(currentStep); + + return ( +
    +
    +

    Agent path

    + {completedCount} / {steps.length} +
    +
    + {steps.map((step, index) => { + const Icon = step.icon; + const state = + index < currentIndex || currentStep === "COMPLETED" + ? "done" + : index === currentIndex + ? "active" + : "locked"; + + return ( +
    + + +
    + {step.title} +

    {step.detail}

    +
    + {step.pillLabel} + {state === "done" ?
    + ); + })} +
    +
    + ); +} + +function ArtifactList({ + artifacts, + selectedId, + onSelect, +}: { + artifacts: Artifact[]; + selectedId: string | null; + onSelect: (id: string) => void; +}) { + return ( +
    +
    +

    Local artifacts

    + {artifacts.length} +
    +
    + {artifacts.length ? ( + artifacts.map((artifact) => ( + + )) + ) : ( +
    +
    + )} +
    +
    + ); +} + +function EventLog({ events }: { events: CaseEvent[] }) { + return ( +
    +
    +

    Live activity

    + {events.length} +
    +
    + {events.length ? ( + events.slice(0, 8).map((event, index) => ( +
    + +
    + {event.time} · {event.kind} + {event.title} +

    {event.detail}

    +
    +
    + )) + ) : ( +
    +
    + )} +
    +
    + ); +} + +function DocumentStage({ artifact }: { artifact: Artifact | null }) { + return ( +
    +
    +
    + Selected artifact +

    {artifact?.title ?? "No packet yet"}

    +

    + {artifact + ? `${artifact.filename} is served by FastAPI from the local artifact store.` + : "Start Olivia's onboarding to generate the first packet."} +

    +
    + {artifact ? ( + + Open artifact + + ) : null} +
    + +
    + {artifact ? ( + +
    diff --git a/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/app/components/main/search-results/search-results.component.scss b/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/app/components/main/search-results/search-results.component.scss new file mode 100644 index 0000000..2dec662 --- /dev/null +++ b/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/app/components/main/search-results/search-results.component.scss @@ -0,0 +1,296 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +.overview-container { + display: flex; + justify-content: center; + padding-top: 2%; + padding-right: 1.5%; + + .overview-sub-container { + width: 66%; + background: linear-gradient(92.1deg, #9ebcedb2 19.16%, rgba(163, 136, 245, 0.651) 79%, rgba(234, 68, 53, 0.493) 135.24%); + border-radius: 12px; + .overview-text { + font-family: 'Roboto'; + font-style: normal; + font-weight: 500; + font-size: 20px; + line-height: 24px; + color: #5C5F61; + padding: 2%; + } + .overview-content { + + font-family: 'Roboto'; + font-style: normal; + font-weight: 400; + font-size: 19px; + line-height: 30px; + color: #161A1D; + padding-left: 2%; + padding-right: 2%; + padding-bottom: 2%; + + } + + .overview-button { + button { + font-family: 'Roboto'; + font-style: normal; + font-weight: 400; + font-size: 20px; + line-height: 24px; + color: #4285F4; + padding: 40px; + } + } + } + +} + +.header-container { + font-family: 'Roboto'; + font-style: normal; + font-weight: 400; + font-size: 16px; + line-height: 19px; + color: #5F6368; + display: flex; + justify-content: flex-start; + width: 66%; +} + +.search-result:hover { + border: 1px solid #DFE0E2; + border-radius: 10px; +} + +.search-result-parent-container{ + display: flex; + flex-direction: column; + width: 66%; + gap: 15px; + padding-bottom: 5%; +} + +.search-result-row-container { + display: flex; + flex-direction: row; + gap: 15px; +} + +.search-result { + padding: 1%; + padding-top: 2%; + cursor: pointer; + border: 1px solid transparent; + width: 25%; + + .title-container { + display: flex; + flex-direction: row; + font-family: 'Roboto'; + font-style: normal; + font-weight: 500; + font-size: 16px; + line-height: 31px; + color: #161A1D; + display: flex; + gap: 15px; + + .link-container { + font-size: 14px !important; + font-weight: 400 !important; + color: #676c6f !important; + } + } + + .description-container { + font-family: 'Roboto'; + font-style: normal; + font-weight: 400; + font-size: 16px; + line-height: 24px; + color: #161A1D; + opacity: 0.8; + padding: 1%; + padding-top: 4%; + } + + .overview-button { + padding-top: 2%; + button { + font-family: 'Roboto'; + font-style: normal; + font-weight: 400; + font-size: 16px; + line-height: 24px; + color: #4285F4; + } + } + +} + +.padding-top { + padding-top: 0; +} + +.close-container { + display: flex; + justify-content: end; +} + +.pdf-display{ + display: flex; + justify-content: center; +} + +.overview { + z-index: 1000; +} + +/* card-list.component.scss */ +.card-list { + display: flex; + gap: 20px; /* Adjust spacing between cards */ +} + +.card { + border: 1px solid #eee; + border-radius: 8px; + flex: 0 0 auto; /* Prevent cards from stretching */ + box-shadow: 2px 2px 5px rgba(0,0,0,0.1); + padding: 16px; + transition: transform 0.2s; + + &:hover { + transform: scale(1.02); + box-shadow: 3px 3px 7px rgba(0,0,0,0.15); + cursor: pointer; + } + + .card-content { + display: flex; + flex-direction: column; + height: 100%; + } + + .card-header { + display: flex; + align-items: center; + margin-bottom: 10px; + + .material-symbols-outlined { + font-size: 1.2rem; + margin-right: 5px; + &.bookmark { + margin-left: auto; // Push bookmark to right + cursor: pointer; + } + } + + .card-title { + font-weight: 500; + width: 250px; + } + } + + .card-description { + flex-grow: 1; // Allow description to take up available space + overflow: hidden; + text-overflow: ellipsis; + display: -webkit-box; + -webkit-line-clamp: 10; /* Number of lines to show */ + -webkit-box-orient: vertical; + width: 250px; + } + + .card-footer { + display: flex; + align-items: center; + margin-top: 10px; + + .material-symbols-outlined { + font-size: 1.2rem; + margin-right: 5px; + } + } +} + +.documents-container { + margin-top: 2rem; + display: flex; + justify-content: space-between; + align-items: center; + max-width: 1000px; +} + +.navigation { + display: flex; + justify-content: center; + margin-top: 20px; + + .nav-button { + border: none; + background: none; + cursor: pointer; + transition: background-color 0.2s; + + &:hover { + background-color: #f0f0f0; + } + + &:disabled { + opacity: 0.5; + cursor: default; + } + + .material-symbols-outlined { + font-size: 1.5rem; + } + } +} + +.document-viewer { + background: rgb(255, 255, 255); + border-left: 1px solid var(--md-sys-color-outline); + border-top: 1px solid var(--md-sys-color-outline); + display: block; + height: 75%; + position: fixed; + right: 0px; + bottom: 0px; + width: 35%; + z-index: 9999; + + .document-viewer-header { + -webkit-box-pack: justify; + -webkit-box-align: center; + -webkit-box-flex: 1; + align-items: center; + border-bottom: 1px solid var(--md-sys-color-outline); + display: flex; + flex: 1 1 0%; + justify-content: space-between; + padding: 8px; + } + + .document-viewer-content { + width: 100%; + height: 100%; + } +} diff --git a/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/app/components/main/search-results/search-results.component.spec.ts b/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/app/components/main/search-results/search-results.component.spec.ts new file mode 100644 index 0000000..1c8bf77 --- /dev/null +++ b/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/app/components/main/search-results/search-results.component.spec.ts @@ -0,0 +1,38 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {ComponentFixture, TestBed} from '@angular/core/testing'; + +import {SearchResultsComponent} from './search-results.component'; + +describe('SearchResultsComponent', () => { + let component: SearchResultsComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [SearchResultsComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(SearchResultsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/app/components/main/search-results/search-results.component.ts b/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/app/components/main/search-results/search-results.component.ts new file mode 100644 index 0000000..ae1cb78 --- /dev/null +++ b/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/app/components/main/search-results/search-results.component.ts @@ -0,0 +1,139 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {Component, OnDestroy, ViewChild, TemplateRef} from '@angular/core'; +import {SearchService} from 'src/app/services/search.service'; +import {ReplaySubject} from 'rxjs'; +import {UserService} from 'src/app/services/user/user.service'; +import {ActivatedRoute, Router} from '@angular/router'; +import {search_image_type, PDF, image_name} from 'src/environments/constant'; +import { + DomSanitizer, + SafeResourceUrl, + SafeUrl, +} from '@angular/platform-browser'; +import {MatDialog} from '@angular/material/dialog'; + +@Component({ + selector: 'app-search-results', + templateUrl: './search-results.component.html', + styleUrls: ['./search-results.component.scss'], +}) +export class SearchResultsComponent implements OnDestroy { + @ViewChild('preview', {static: true}) + previewRef!: TemplateRef<{}>; + summary = ''; + private readonly destroyed = new ReplaySubject(1); + serachResult: any = []; + documents: any = []; + images: any = []; + pdf = PDF; + imageName = image_name; + documentURL: SafeResourceUrl | undefined; + openPreviewDocument: any; + currentPage = 0; + pageSize = 3; + selectedDocument: any; + safeUrl: SafeUrl | undefined; + + constructor( + private router: Router, + private route: ActivatedRoute, + private service: SearchService, + private userService: UserService, + private dialog: MatDialog, + private sanitizer: DomSanitizer + ) { + const query = this.route.snapshot.queryParamMap.get('q'); + + this.service.search(query!).subscribe({ + next: (searchRespone: any) => { + this.summary = searchRespone.summary; + this.serachResult = searchRespone.results; + this.serachResult.forEach((element: any) => { + this.documents.push(element); + if (search_image_type.includes(element.link.split('.')[1])) { + this.images.push(element); + } + }); + console.log(this.documents, this.images); + this.userService.hideLoading(); + }, + error: () => { + this.userService.hideLoading(); + }, + }); + } + + searchTerm(term: string) { + this.router.navigate(['/search'], {queryParams: {q: term}}); + + this.service.search(term).subscribe({ + next: (searchRespone: any) => { + this.serachResult = searchRespone.results; + this.summary = searchRespone.summary; + this.userService.hideLoading(); + }, + error: () => { + this.userService.hideLoading(); + }, + }); + } + + openNewWindow(link: string) { + window.open(link, '_blank'); + } + + previewDocument(event: any, document: any) { + event.stopPropagation(); + if (document.link.endsWith('.pdf') || document.link.endsWith('.docx')) { + this.selectedDocument = document; + this.safeUrl = this.sanitizer.bypassSecurityTrustResourceUrl( + this.selectedDocument.link + ); + } + } + + closePreview() { + this.selectedDocument = undefined; + } + + ngOnDestroy() { + this.destroyed.next(); + this.destroyed.complete(); + } + + get pagedDocuments() { + const startIndex = this.currentPage * this.pageSize; + return this.documents.slice(startIndex, startIndex + this.pageSize); + } + + get totalPages() { + return Math.ceil(this.documents.length / this.pageSize); + } + + nextPage() { + if (this.currentPage < this.totalPages - 1) { + this.currentPage++; + } + } + + prevPage() { + if (this.currentPage > 0) { + this.currentPage--; + } + } +} diff --git a/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/app/components/search-application/manage-search-application/manage-search-application.component.html b/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/app/components/search-application/manage-search-application/manage-search-application.component.html new file mode 100644 index 0000000..51a5baa --- /dev/null +++ b/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/app/components/search-application/manage-search-application/manage-search-application.component.html @@ -0,0 +1,74 @@ + + +
    + +
    +
    +
    +
    +
    + Manage Agent Builder Config +
    +
    +
    +
    + + Choose an Engine + + {{ + engine.name + }} + + +
    + + + +
    +
    +
    +
    +
    diff --git a/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/app/components/search-application/manage-search-application/manage-search-application.component.scss b/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/app/components/search-application/manage-search-application/manage-search-application.component.scss new file mode 100644 index 0000000..3b7067f --- /dev/null +++ b/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/app/components/search-application/manage-search-application/manage-search-application.component.scss @@ -0,0 +1,571 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +.root-container { + display: flex; + width: 100%; + justify-content: center; + + .max-width-container { + width: 100%; + max-width: 1280px; + } +} + +.action-button-container { + padding: 0; +} + +.table-container { + justify-content: center; + display: block; + flex-direction: row; + padding: 5.5% 3%; + padding-bottom: 5%; + + .row { + display: flex; + flex-direction: row; + + .mat-mdc-form-field { + width: 100% !important; + } + + } + +} + +.amber { + color: #fbc645; +} + +.red { + color: red; +} + +.blue { + color: #4285F4; +} + +::ng-deep .mat-checkbox-checked .mat-checkbox-background, +.mat-checkbox-indeterminate .mat-checkbox-background { + background-color: #4285F4 !important; +} + +// overwrite the ripple overlay on hover and click +::ng-deep .mat-checkbox:not(.mat-checkbox-disabled) .mat-checkbox-ripple .mat-ripple-element { + background-color: #4285F4 !important; +} + +::ng-deep .mdc-checkbox__ripple { + background-color: #4285F4 !important; +} + +::ng-deep .mat-mdc-checkbox .mdc-checkbox .mdc-checkbox__native-control:enabled:checked~.mdc-checkbox__background { + background-color: #4285F4 !important; + border-color: #4285F4 !important; +} + +::ng-deep .mat-mdc-checkbox .mdc-checkbox .mdc-checkbox__native-control:enabled:indeterminate~.mdc-checkbox__background { + background-color: #4285F4 !important; + border-color: #4285F4 !important; + +} + +::ng-deep .mat-mdc-slide-toggle.mat-mdc-slide-toggle-checked:not(.mat-disabled) .mdc-switch__shadow { + background-color: #4285F4; + /*replace with your color*/ +} + +::ng-deep .mat-mdc-slide-toggle.mat-mdc-slide-toggle-checked:not(.mat-disabled) .mdc-switch__track::after { + background-color: #4285F4 !important; + /*replace with your color*/ +} + +.buttons-container { + display: flex; + flex-direction: row; + justify-content: flex-start; + width: 100%; + padding-top: 1%; + padding-bottom: 1%; + padding-left: 3%; +} + +.saved-intents-container { + padding-top: 2%; + padding-bottom: 2%; +} + +.heading-container { + width: 73%; + align-content: center; + flex-wrap: wrap; + display: flex; + flex-direction: row; + + span { + font-family: 'Google Sans', sans-serif !important; + font-style: normal; + font-weight: 400; + font-size: 20px; + line-height: 14px; + color: #161A1D; + } +} + +.button-child-container { + display: flex; + flex-direction: row; + gap: 20px; + width: 24%; + justify-content: center; + + button { + border-radius: 30px; + font-family: 'Google Sans', sans-serif !important; + font-style: normal; + font-weight: 500; + font-size: 14px; + line-height: 23px; + letter-spacing: unset; + box-shadow: none; + } + + .bulk-delete-button { + color: #f70808; + border: 1px solid #f70808; + } + + .create-notification-button { + color: #fff; + background: #4285F4; + } +} + +.form-field { + width: 100%; +} + +.dialog-toggle-button-container { + font-family: 'Google Sans', sans-serif !important; + font-style: normal; + width: 100%; + display: flex; + flex-direction: row; + gap: 10px; + padding-bottom: 4%; + + button { + height: 45px; + border: none; + } + + .error { + background: #e9867e33; + width: 28%; + } + + .warning { + background: rgb(241 226 183 / 44%); + width: 28%; + } + + .info { + background: #F6F9FE; + width: 35%; + } + + .errorOutline { + border: 1px solid red; + } + + .warningOutline { + border: 1px solid #fbc645; + } + + .infoOutline { + border: 1px solid #4285F4; + } +} + +.delete-confirmation-text { + font-family: 'Google Sans', sans-serif !important; + font-style: normal; + width: 100%; + display: flex; + justify-content: center; + font-size: 20px; + color: #161A1D; +} + +.dialog-delete-button { + border-radius: 30px; + font-family: 'Google Sans', sans-serif !important; + font-style: normal; + font-weight: 500; + font-size: 14px; + line-height: 23px; + letter-spacing: unset; + box-shadow: none; + color: #f70808; + border: 1px solid #f70808; +} + +.disableDiv { + pointer-events: none; +} + +.spinner-container { + width: 100%; + justify-content: center; + display: flex; + flex-direction: row; + text-align: center; + position: absolute; + top: 50%; +} + +.spinner-dialog-container { + width: 100%; + justify-content: flex-end; + display: flex; + flex-direction: row; + padding-right: 5%; + padding-bottom: 4%; +} + +.text-column { + width: 30% !important; + word-break: break-word; +} + +.comment-detail-container { + display: flex; + flex-direction: column; + gap: 20px; +} + +.details-container { + font-weight: 600; + border-radius: 5px; + border: 1px solid #9e9e9e; + text-align: start; + padding: 3%; + gap: 14px; + display: flex; + flex-direction: column; + + .question-details-container { + background: white; + font-weight: 400; + border-radius: 12px; + } +} + +.chip-color { + background: aliceblue !important; + border: 1px solid #4285f4; +} + +.action-column { + width: 8%; +} + +::-webkit-scrollbar { + width: 9px; +} + +::-webkit-scrollbar-track { + background: transparent; +} + +::-webkit-scrollbar-thumb { + background-color: rgba(155, 155, 155, 0.5); + border-radius: 20px; + border: transparent; +} + +.search-question-field { + width: 100%; + + mat-icon { + color: #363e45; + cursor: pointer; + } +} + +::ng-deep .search-question-field .mat-mdc-text-field-wrapper.mdc-text-field--outlined .mat-mdc-form-field-infix { + padding-top: 10px !important; + padding-bottom: 10px !important; +} + +::ng-deep .search-question-field .mdc-text-field__input { + height: 34px !important; +} + +.input-field-container { + display: flex; + flex-direction: row; + gap: 2%; + align-items: center; +} + +.submit-button { + // width: 8% !important; + background: #4285F4 !important; + border: 1px solid #4285F4; + box-sizing: border-box; + // border: 1px solid #e0e0e0 !important; + color: #fff !important; + position: relative; + user-select: none; + cursor: pointer; + outline: none; + -webkit-tap-highlight-color: transparent; + font-family: 'Google Sans', sans-serif !important; + font-size: 14px; + font-weight: 500; + white-space: nowrap; + text-decoration: none; + vertical-align: baseline; + text-align: center; + margin: 0; + min-width: 64px; + line-height: 36px; + padding: 20px 46px; + border-radius: 48px !important; + overflow: visible; + transform: translate3d(0, 0, 0); + transition: background 400ms cubic-bezier(0.25, 0.8, 0.25, 1), box-shadow 280ms cubic-bezier(0.4, 0, 0.2, 1); +} + +.reset-button { + background: white !important; + box-sizing: border-box !important; + border: 1px solid #4285F4 !important; + color: #4285F4 !important; + cursor: pointer; + outline: none; + font-family: 'Google Sans', sans-serif !important; + font-size: 14px; + font-weight: 500; + margin: 0; + min-width: 130px; + line-height: 36px; + padding: 0px 25px; + height: 100%; + border-radius: 48px !important; + display: flex; + justify-content: center; + align-content: center; + align-items: center; + flex-direction: row; +} + +.inactive-button { + + background: #ffffff !important; + box-sizing: border-box !important; + border: 1px solid rgba(43, 41, 40, 0.502) !important; + color: rgba(43, 41, 40, 0.93) !important; + cursor: pointer; + outline: none; + font-family: "Google Sans", sans-serif !important; + font-size: 14px; + font-weight: 500; + margin: 0; + min-width: 64px; + line-height: 36px; + padding: 0px 45px; + height: 60%; + margin-left: 2rem; + border-radius: 48px !important; + display: flex; + justify-content: center; + flex-direction: row; + flex-wrap: wrap; + align-content: center; + align-items: center; +} + +.active-button { + + background: rgb(226 249 225) !important; + box-sizing: border-box !important; + border: 1px solid rgb(59 153 29 / 84%) !important; + color: rgb(17 193 56 / 93%) !important; + cursor: pointer; + outline: none; + font-family: "Google Sans", sans-serif !important; + font-size: 14px; + font-weight: 500; + margin: 0; + min-width: 64px; + line-height: 36px; + padding: 0px 45px; + height: 60%; + margin-left: 2rem; + border-radius: 48px !important; + display: flex; + justify-content: center; + flex-direction: column; + flex-wrap: wrap; + align-content: center; + align-items: center; +} + +.intent-field { + width: 50%; + + .my-icon { + margin-right: 0.5rem; + color: #005cbb; + border-radius: 2rem; + padding: 1rem; + } +} + +.intent-heading-container { + font-style: normal; + font-weight: 400; + font-size: 16px; + line-height: 23px; + color: #363e45; + padding-top: 2%; + padding-bottom: 1%; +} + +.internal-spinner-container { + height: 31px +} + +pre { + width: 100%; + color: white; + padding: 2%; + font-family: monospace; + text-wrap: pretty; + display: flex; + flex-direction: row; +} + + +::-webkit-scrollbar-track { + -webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3); + border-radius: 10px; + background-color: #F5F5F5; +} + +::-webkit-scrollbar { + width: 12px; + background-color: #F5F5F5; +} + +::-webkit-scrollbar-thumb { + border-radius: 10px; + -webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, .3); + background-color: #D62929; +} + +.intent-chunk { + display: flex; + flex-direction: row; + width: 90%; +} + +.copy-button-container { + width: 10%; + display: flex; + justify-content: flex-end; +} + +.configure-title { + display: flex; + align-content: center; + flex-wrap: wrap; + width: 50%; +} + +.intent-buttons-container { + display: flex; + gap: 2%; + justify-content: flex-end; + padding-top: 1%; + padding-bottom: 1%; +} + +.image-container { + justify-content: center; + display: flex; + width: 90%; + + img { + height: 47%; + width: 47%; + } + } + + .delete-confirmation-text { + font-family: 'Google Sans', sans-serif !important; + font-style: normal; + width: 100%; + display: flex; + justify-content: center; + font-size: 20px; + color: #161A1D; + } + + .dialog-actions-container { + padding-bottom: 2%; + justify-content: center; + display: flex; + flex-direction: row; + text-align: end; + width: 100%; + padding-right: 2%; + + .discard-button { + color: rgba(92, 95, 97, 0.5); + border: 1px solid rgb(92 95 97 / 28%); + border-radius: 40px; + box-shadow: none; + letter-spacing: inherit; + } + + .save-button { + background: #4285F4; + color: #fff; + border-radius: 40px; + box-shadow: none; + letter-spacing: inherit; + } + + .dialog-delete-button { + border-radius: 30px; + font-family: 'Google Sans', sans-serif !important; + font-style: normal; + font-weight: 500; + font-size: 14px; + line-height: 23px; + letter-spacing: unset; + box-shadow: none; + color: #f70808; + border: 1px solid #f70808; + } + } + + diff --git a/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/app/components/search-application/manage-search-application/manage-search-application.component.spec.ts b/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/app/components/search-application/manage-search-application/manage-search-application.component.spec.ts new file mode 100644 index 0000000..bf04b68 --- /dev/null +++ b/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/app/components/search-application/manage-search-application/manage-search-application.component.spec.ts @@ -0,0 +1,38 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {ComponentFixture, TestBed} from '@angular/core/testing'; + +import {ManageSearchApplicationComponent} from './manage-search-application.component'; + +describe('ManageAgentBuilderComponent', () => { + let component: ManageSearchApplicationComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ManageSearchApplicationComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(ManageSearchApplicationComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/app/components/search-application/manage-search-application/manage-search-application.component.ts b/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/app/components/search-application/manage-search-application/manage-search-application.component.ts new file mode 100644 index 0000000..731716f --- /dev/null +++ b/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/app/components/search-application/manage-search-application/manage-search-application.component.ts @@ -0,0 +1,107 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {Component, OnDestroy} from '@angular/core'; +import {SearchApplicationService} from 'src/app/services/search_application.service'; +import {ReplaySubject} from 'rxjs'; +import {takeUntil} from 'rxjs/operators'; +import {UserService} from 'src/app/services/user/user.service'; +import {Router} from '@angular/router'; +import {Engine} from 'src/app/models/engine.model'; +import {EnginesService} from 'src/app/services/engines.service'; + +@Component({ + selector: 'app-manage-search-application', + templateUrl: './manage-search-application.component.html', + styleUrls: ['./manage-search-application.component.scss'], +}) +export class ManageSearchApplicationComponent implements OnDestroy { + selectedEngine: Engine | undefined; + engines: Engine[] = []; + editMode = false; + savedEngineID = ''; + private readonly destroyed = new ReplaySubject(1); + + constructor( + private readonly searchApplicationService: SearchApplicationService, + private readonly userService: UserService, + private readonly router: Router, + private enginesService: EnginesService + ) { + this.enginesService + .getAll() + .subscribe(response => (this.engines = response)); + this.disableForm(); + this.getConfigData(); + } + + disableForm() { + this.editMode = false; + } + + enableForm() { + this.editMode = true; + } + + getConfigData() { + this.userService.showLoading(); + this.searchApplicationService + .get() + .pipe(takeUntil(this.destroyed)) + .subscribe({ + next: response => { + this.savedEngineID = response.engine_id; + this.selectedEngine = this.engines.filter( + e => e.engine_id === response.engine_id + )[0]; + this.userService.hideLoading(); + }, + error: () => { + this.userService.hideLoading(); + }, + }); + } + + saveForm() { + this.userService.showLoading(); + const searchApplication = { + engine_id: this.selectedEngine?.engine_id || "", + region: this.selectedEngine?.region || "", + }; + + this.searchApplicationService + .update(this.savedEngineID, searchApplication) + .subscribe({ + next: () => { + this.savedEngineID = this.selectedEngine?.engine_id || ""; + this.userService.hideLoading(); + this.disableForm(); + }, + error: () => { + this.userService.hideLoading(); + }, + }); + } + + navigateToMain() { + this.router.navigateByUrl('/'); + } + + ngOnDestroy(): void { + this.destroyed.next(); + this.destroyed.complete(); + } +} diff --git a/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/app/components/search-application/search-application-form/search-application-form.component.html b/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/app/components/search-application/search-application-form/search-application-form.component.html new file mode 100644 index 0000000..f47c004 --- /dev/null +++ b/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/app/components/search-application/search-application-form/search-application-form.component.html @@ -0,0 +1,52 @@ + + +
    +
    + Configure Search Application +
    + +
    + + Choose an Engine + + {{engine.name}} + + +
    + +
    + +
    + +
    +
    +
    diff --git a/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/app/components/search-application/search-application-form/search-application-form.component.scss b/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/app/components/search-application/search-application-form/search-application-form.component.scss new file mode 100644 index 0000000..df1b17b --- /dev/null +++ b/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/app/components/search-application/search-application-form/search-application-form.component.scss @@ -0,0 +1,106 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +.configure-title { + display: flex; + align-content: center; + flex-wrap: wrap; + width: 100%; + justify-content: center; + margin-top: -26px; + padding-bottom: 5%; + font-family: 'Roboto'; + font-style: normal; + font-weight: 400; + font-size: 24px; + line-height: 24px; + color: #161A1D; +} + +.form-field { + width: 70%; + + .my-icon { + margin-right: 0.5rem; + color: #005cbb; + border-radius: 2rem; + padding: 1rem; + } + } + + .button-container { + justify-content: center; + display: flex; + gap: 10px; + margin-bottom: -30px; + padding-top: 3%; + + .submit-button { + width: 7%; + background: #4285F4; + box-sizing: border-box; + border: 1px solid #e0e0e0; + color: #fff; + position: relative; + user-select: none; + cursor: pointer; + outline: none; + -webkit-tap-highlight-color: transparent; + display: inline-block; + font-family: 'Google Sans', sans-serif !important; + font-size: 14px; + font-weight: 500; + white-space: nowrap; + text-decoration: none; + vertical-align: baseline; + text-align: center; + margin: 0; + min-width: 64px; + padding: 0 16px; + border-radius: 35px; + overflow: visible; + transform: translate3d(0, 0, 0); + transition: background 400ms cubic-bezier(0.25, 0.8, 0.25, 1), box-shadow 280ms cubic-bezier(0.4, 0, 0.2, 1); + } + + .cancel-button { + width: 7%; + background: white; + box-sizing: border-box; + border: 1px solid #4285F4; + color: #4285F4; + position: relative; + user-select: none; + cursor: pointer; + outline: none; + -webkit-tap-highlight-color: transparent; + display: inline-block; + font-family: 'Google Sans', sans-serif !important; + font-size: 14px; + font-weight: 500; + white-space: nowrap; + text-decoration: none; + vertical-align: baseline; + text-align: center; + margin: 0; + min-width: 64px; + padding: 0 16px; + border-radius: 35px; + overflow: visible; + transform: translate3d(0, 0, 0); + transition: background 400ms cubic-bezier(0.25, 0.8, 0.25, 1), box-shadow 280ms cubic-bezier(0.4, 0, 0.2, 1); + } + } diff --git a/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/app/components/search-application/search-application-form/search-application-form.component.spec.ts b/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/app/components/search-application/search-application-form/search-application-form.component.spec.ts new file mode 100644 index 0000000..96b03fa --- /dev/null +++ b/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/app/components/search-application/search-application-form/search-application-form.component.spec.ts @@ -0,0 +1,38 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {ComponentFixture, TestBed} from '@angular/core/testing'; + +import {SearchApplicationFormComponent} from './search-application-form.component'; + +describe('AddAgentBuilderComponent', () => { + let component: SearchApplicationFormComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [SearchApplicationFormComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(SearchApplicationFormComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/app/components/search-application/search-application-form/search-application-form.component.ts b/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/app/components/search-application/search-application-form/search-application-form.component.ts new file mode 100644 index 0000000..ddb73b3 --- /dev/null +++ b/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/app/components/search-application/search-application-form/search-application-form.component.ts @@ -0,0 +1,66 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {Component} from '@angular/core'; +import {MatDialogRef} from '@angular/material/dialog'; +import {Router} from '@angular/router'; +import {Engine} from 'src/app/models/engine.model'; +import {EnginesService} from 'src/app/services/engines.service'; +import {SearchApplicationService} from 'src/app/services/search_application.service'; +import {UserService} from 'src/app/services/user/user.service'; + +@Component({ + selector: 'app-search-application-form', + templateUrl: './search-application-form.component.html', + styleUrls: ['./search-application-form.component.scss'], +}) +export class SearchApplicationFormComponent { + showSpinner = false; + selectedEngine: Engine | undefined; + engines: Engine[] = []; + + constructor( + private dialogRef: MatDialogRef, + private readonly router: Router, + private readonly searchApplicationService: SearchApplicationService, + private userService: UserService, + private enginesService: EnginesService + ) { + this.enginesService + .getAll() + .subscribe(response => (this.engines = response)); + } + + saveForm() { + if (this.selectedEngine) { + const searchApplication = { + engine_id: this.selectedEngine.engine_id, + region: this.selectedEngine.region, + }; + this.userService.showLoading(); + this.searchApplicationService.create(searchApplication).subscribe({ + next: () => { + this.userService.hideLoading(); + this.dialogRef.close()!; + this.router.navigateByUrl('/'); + }, + error: () => { + this.userService.hideLoading(); + }, + }); + } + } +} diff --git a/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/app/components/toast-message/toast-message.component.html b/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/app/components/toast-message/toast-message.component.html new file mode 100644 index 0000000..a83d217 --- /dev/null +++ b/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/app/components/toast-message/toast-message.component.html @@ -0,0 +1,26 @@ + + +
    + icon inside toast message + {{ text }} + cross icon inside toast message +
    diff --git a/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/app/components/toast-message/toast-message.component.scss b/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/app/components/toast-message/toast-message.component.scss new file mode 100644 index 0000000..f29e152 --- /dev/null +++ b/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/app/components/toast-message/toast-message.component.scss @@ -0,0 +1,37 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +.toast-content { + color: #ffffff; + font-family: "Google Sans", sans-serif !important; +} + +.close-toast { + margin-left: auto; + align-self: baseline; + cursor: pointer; +} + +.red-toast { + background-color: #ed0c0c !important; +} +.green-toast { + background-color: #06865e !important; +} + +.mat-mdc-snack-bar-container .mdc-snackbar__surface { + background-color: transparent !important; +} diff --git a/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/app/components/toast-message/toast-message.component.spec.ts b/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/app/components/toast-message/toast-message.component.spec.ts new file mode 100644 index 0000000..d0d405a --- /dev/null +++ b/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/app/components/toast-message/toast-message.component.spec.ts @@ -0,0 +1,38 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {ComponentFixture, TestBed} from '@angular/core/testing'; + +import {ToastMessageComponent} from './toast-message.component'; + +describe('ToastMessageComponent', () => { + let component: ToastMessageComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ToastMessageComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(ToastMessageComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/app/components/toast-message/toast-message.component.ts b/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/app/components/toast-message/toast-message.component.ts new file mode 100644 index 0000000..ebac36d --- /dev/null +++ b/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/app/components/toast-message/toast-message.component.ts @@ -0,0 +1,40 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {Component, Inject, ViewEncapsulation} from '@angular/core'; +import {MatSnackBar, MAT_SNACK_BAR_DATA} from '@angular/material/snack-bar'; + +@Component({ + selector: 'app-toast-message', + templateUrl: './toast-message.component.html', + styleUrls: ['./toast-message.component.scss'], + encapsulation: ViewEncapsulation.None, +}) +export class ToastMessageComponent { + text: string; + icon: string; + constructor( + private _snackBar: MatSnackBar, + @Inject(MAT_SNACK_BAR_DATA) public snackBarData: any + ) { + this.text = snackBarData.text; + this.icon = snackBarData.icon; + } + + closeToast() { + this._snackBar.dismiss(); + } +} diff --git a/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/app/models/config.model.ts b/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/app/models/config.model.ts new file mode 100644 index 0000000..fda930d --- /dev/null +++ b/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/app/models/config.model.ts @@ -0,0 +1,20 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export type config = { + name: string; + url: string; +}; diff --git a/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/app/models/engine.model.ts b/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/app/models/engine.model.ts new file mode 100644 index 0000000..b7f7de8 --- /dev/null +++ b/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/app/models/engine.model.ts @@ -0,0 +1,21 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export type Engine = { + name: string; + engine_id: string; + region: string; +}; diff --git a/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/app/models/search.model.ts b/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/app/models/search.model.ts new file mode 100644 index 0000000..e281135 --- /dev/null +++ b/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/app/models/search.model.ts @@ -0,0 +1,52 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export type SearchRequest = { + term: string; +}; + +export type SearchResponse = { + summary: any; + results: SearchResult[]; + totalSize: number; +}; + +export type SearchResult = { + document: Document; +}; + +export type Document = { + derivedStructData: DocumentData; +}; + +export type DocumentData = { + title: string; + link: string; + snippets: Snippet[]; + pagemap: PageMap; +}; + +export type Snippet = { + snippet: string; +}; + +export type PageMap = { + cse_image: ImagesData[]; +}; + +export type ImagesData = { + src: string; +}; diff --git a/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/app/models/search_application.model.ts b/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/app/models/search_application.model.ts new file mode 100644 index 0000000..6e6b273 --- /dev/null +++ b/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/app/models/search_application.model.ts @@ -0,0 +1,20 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export type SearchApplication = { + engine_id: string; + region: string; +}; diff --git a/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/app/pipes/truncate.pipe.spec.ts b/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/app/pipes/truncate.pipe.spec.ts new file mode 100644 index 0000000..82e65c6 --- /dev/null +++ b/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/app/pipes/truncate.pipe.spec.ts @@ -0,0 +1,24 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {TruncatePipe} from './truncate.pipe'; + +describe('TruncatePipe', () => { + it('create an instance', () => { + const pipe = new TruncatePipe(); + expect(pipe).toBeTruthy(); + }); +}); diff --git a/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/app/pipes/truncate.pipe.ts b/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/app/pipes/truncate.pipe.ts new file mode 100644 index 0000000..ae06a3b --- /dev/null +++ b/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/app/pipes/truncate.pipe.ts @@ -0,0 +1,26 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {Pipe, PipeTransform} from '@angular/core'; + +@Pipe({ + name: 'truncate', +}) +export class TruncatePipe implements PipeTransform { + transform(value: string, limit = 300, trail = '...'): string { + return value.length > limit ? value.substring(0, limit) + trail : value; + } +} diff --git a/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/app/services/engines.service.spec.ts b/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/app/services/engines.service.spec.ts new file mode 100644 index 0000000..dd94b99 --- /dev/null +++ b/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/app/services/engines.service.spec.ts @@ -0,0 +1,32 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {TestBed} from '@angular/core/testing'; + +import {EnginesService} from './engines.service'; + +describe('EnginesService', () => { + let service: EnginesService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(EnginesService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/app/services/engines.service.ts b/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/app/services/engines.service.ts new file mode 100644 index 0000000..7a17897 --- /dev/null +++ b/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/app/services/engines.service.ts @@ -0,0 +1,36 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {map, Observable} from 'rxjs'; +import {HttpClient} from '@angular/common/http'; +import {Injectable} from '@angular/core'; +import {Engine} from '../models/engine.model'; +import {environment} from 'src/environments/environment'; + +const ENGINES_URL = `${environment.backendURL}/search/engines`; + +@Injectable({ + providedIn: 'root', +}) +export class EnginesService { + constructor(private http: HttpClient) {} + + getAll(): Observable { + return this.http + .get(ENGINES_URL) + .pipe(map(response => response as Engine[])); + } +} diff --git a/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/app/services/login/auth.guard.spec.ts b/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/app/services/login/auth.guard.spec.ts new file mode 100644 index 0000000..8284e7a --- /dev/null +++ b/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/app/services/login/auth.guard.spec.ts @@ -0,0 +1,32 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {TestBed} from '@angular/core/testing'; + +import {AuthGuard} from './auth.guard'; + +describe('AuthGuard', () => { + let guard: AuthGuard; + + beforeEach(() => { + TestBed.configureTestingModule({}); + guard = TestBed.inject(AuthGuard); + }); + + it('should be created', () => { + expect(guard).toBeTruthy(); + }); +}); diff --git a/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/app/services/login/auth.guard.ts b/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/app/services/login/auth.guard.ts new file mode 100644 index 0000000..fd95abd --- /dev/null +++ b/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/app/services/login/auth.guard.ts @@ -0,0 +1,54 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {Injectable} from '@angular/core'; +import { + ActivatedRouteSnapshot, + CanActivate, + Router, + RouterStateSnapshot, + UrlTree, +} from '@angular/router'; +import {Observable} from 'rxjs'; +import {AuthService} from './auth.service'; +import {environment} from 'src/environments/environment'; + +const LOGIN_ROUTE = '/login'; + +@Injectable({ + providedIn: 'root', +}) +export class AuthGuard implements CanActivate { + constructor( + private auth: AuthService, + private router: Router + ) {} + + canActivate( + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot + ): + | Observable + | Promise + | boolean + | UrlTree { + if (environment.requiredLogin === 'True' && !this.auth.isLoggedIn()) { + this.router.navigate([LOGIN_ROUTE]); + return false; + } + return true; + } +} diff --git a/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/app/services/login/auth.service.spec.ts b/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/app/services/login/auth.service.spec.ts new file mode 100644 index 0000000..f08aa1a --- /dev/null +++ b/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/app/services/login/auth.service.spec.ts @@ -0,0 +1,32 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {TestBed} from '@angular/core/testing'; + +import {AuthService} from './auth.service'; + +describe('AuthService', () => { + let service: AuthService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(AuthService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/app/services/login/auth.service.ts b/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/app/services/login/auth.service.ts new file mode 100644 index 0000000..325d42e --- /dev/null +++ b/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/app/services/login/auth.service.ts @@ -0,0 +1,57 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {Injectable} from '@angular/core'; +import {Router} from '@angular/router'; +import {environment} from 'src/environments/environment'; + +const USER_TOKEN_KEY = 'gpau_id'; +const USER_DETAILS = 'USER_DETAILS'; +const LOGIN_ROUTE = '/login'; + +@Injectable({ + providedIn: 'root', +}) +export class AuthService { + private requiredLogin: boolean = environment.requiredLogin === 'True'; + + constructor(private router: Router) {} + + saveUserSession(token: string) { + localStorage.setItem(USER_TOKEN_KEY, token); + } + + logout(route: string = LOGIN_ROUTE) { + localStorage.removeItem(USER_TOKEN_KEY); + localStorage.removeItem(USER_DETAILS); + localStorage.removeItem('showTooltip'); + this.router.navigateByUrl(route); + } + + isLoggedIn() { + const isLoggedIn = localStorage.getItem(USER_TOKEN_KEY) !== null; + if (!isLoggedIn && this.router.url !== LOGIN_ROUTE) { + this.router.navigate([LOGIN_ROUTE]); + } + return isLoggedIn; + } + + isUserLoggedIn() { + if (!this.requiredLogin) return true; + const isLoggedIn = localStorage.getItem(USER_TOKEN_KEY) !== null; + return isLoggedIn; + } +} diff --git a/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/app/services/search.service.spec.ts b/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/app/services/search.service.spec.ts new file mode 100644 index 0000000..b024e3b --- /dev/null +++ b/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/app/services/search.service.spec.ts @@ -0,0 +1,32 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {TestBed} from '@angular/core/testing'; + +import {SearchService} from './search.service'; + +describe('SearchService', () => { + let service: SearchService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(SearchService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/app/services/search.service.ts b/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/app/services/search.service.ts new file mode 100644 index 0000000..1495b60 --- /dev/null +++ b/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/app/services/search.service.ts @@ -0,0 +1,37 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {HttpClient} from '@angular/common/http'; +import {Injectable} from '@angular/core'; +import {environment} from 'src/environments/environment'; +import {map} from 'rxjs/operators'; +import {SearchRequest, SearchResponse} from '../models/search.model'; + +const searchURL = `${environment.backendURL}/search`; + +@Injectable({ + providedIn: 'root', +}) +export class SearchService { + constructor(private http: HttpClient) {} + + search(term: string) { + const request: SearchRequest = {term: term}; + return this.http + .post(searchURL, request) + .pipe(map(response => response as SearchResponse)); + } +} diff --git a/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/app/services/search_application.service.spec.ts b/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/app/services/search_application.service.spec.ts new file mode 100644 index 0000000..866afb3 --- /dev/null +++ b/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/app/services/search_application.service.spec.ts @@ -0,0 +1,32 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {TestBed} from '@angular/core/testing'; + +import {SearchApplicationService} from './search_application.service'; + +describe('SearchApplicationService', () => { + let service: SearchApplicationService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(SearchApplicationService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/app/services/search_application.service.ts b/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/app/services/search_application.service.ts new file mode 100644 index 0000000..1e419fb --- /dev/null +++ b/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/app/services/search_application.service.ts @@ -0,0 +1,45 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {Injectable} from '@angular/core'; +import {HttpClient} from '@angular/common/http'; +import {environment} from 'src/environments/environment'; +import {map} from 'rxjs/operators'; +import {Observable} from 'rxjs'; +import {SearchApplication} from '../models/search_application.model'; + +const configURL = `${environment.backendURL}/search/application`; + +@Injectable({ + providedIn: 'root', +}) +export class SearchApplicationService { + constructor(private readonly http: HttpClient) {} + + get(): Observable { + return this.http + .get(configURL) + .pipe(map(response => response as SearchApplication)); + } + + create(searchApplication: SearchApplication) { + return this.http.post(configURL, searchApplication); + } + + update(engine_id: string, searchApplication: SearchApplication) { + return this.http.put(`${configURL}/${engine_id}`, searchApplication); + } +} diff --git a/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/app/services/speech-to-text.ts b/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/app/services/speech-to-text.ts new file mode 100644 index 0000000..16b1469 --- /dev/null +++ b/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/app/services/speech-to-text.ts @@ -0,0 +1,34 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {Injectable} from '@angular/core'; +import {HttpClient} from '@angular/common/http'; +import {environment} from 'src/environments/environment'; + +const audioChatUrl = `${environment.backendURL}/audio_chat`; + +@Injectable({ + providedIn: 'root', +}) +export class SpeechToTextService { + constructor(private http: HttpClient) {} + + async transcribeAudio(audioBlob: Blob) { + const formData = new FormData(); + formData.append('audio_file', audioBlob, 'audio.wav'); + return this.http.post(audioChatUrl, formData); + } +} diff --git a/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/app/services/user/session.service.spec.ts b/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/app/services/user/session.service.spec.ts new file mode 100644 index 0000000..ff45965 --- /dev/null +++ b/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/app/services/user/session.service.spec.ts @@ -0,0 +1,32 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {TestBed} from '@angular/core/testing'; + +import {SessionService} from './session.service'; + +describe('SessionService', () => { + let service: SessionService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(SessionService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/app/services/user/session.service.ts b/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/app/services/user/session.service.ts new file mode 100644 index 0000000..4a51a17 --- /dev/null +++ b/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/app/services/user/session.service.ts @@ -0,0 +1,35 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {Injectable} from '@angular/core'; +import {v4 as uuid} from 'uuid'; + +const SESSION_KEY = 'pasid'; + +@Injectable({ + providedIn: 'root', +}) +export class SessionService { + constructor() {} + + getSession(): string | null { + return sessionStorage.getItem(SESSION_KEY); + } + + createSession() { + sessionStorage.setItem(SESSION_KEY, uuid()); + } +} diff --git a/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/app/services/user/user.service.ts b/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/app/services/user/user.service.ts new file mode 100644 index 0000000..cda7580 --- /dev/null +++ b/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/app/services/user/user.service.ts @@ -0,0 +1,54 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {Injectable} from '@angular/core'; +import {BehaviorSubject} from 'rxjs'; + +type UserStored = { + uid?: string; + name?: string; + email?: string; + photoURL?: string; + displayName?: string; + domain?: string; +}; + +@Injectable({ + providedIn: 'root', +}) +export class UserService { + readonly loadingSubject = new BehaviorSubject(false); + + constructor() {} + + showLoading() { + this.loadingSubject.next(true); + } + + hideLoading() { + this.loadingSubject.next(false); + } + + getUserDetails(): UserStored { + if (localStorage.getItem('USER_DETAILS') !== null) { + const userObj = localStorage.getItem('USER_DETAILS'); + return JSON.parse(userObj || '{}'); + } else { + const userDetails: UserStored = {}; + return userDetails; + } + } +} diff --git a/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/assets/.gitkeep b/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/assets/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/assets/gcloud-logo.svg b/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/assets/gcloud-logo.svg new file mode 100644 index 0000000..5150056 --- /dev/null +++ b/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/assets/gcloud-logo.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/assets/images/SocialHeaderTemplate8.png b/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/assets/images/SocialHeaderTemplate8.png new file mode 100644 index 0000000..88f4306 Binary files /dev/null and b/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/assets/images/SocialHeaderTemplate8.png differ diff --git a/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/assets/images/answernotfound.png b/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/assets/images/answernotfound.png new file mode 100644 index 0000000..874c87d Binary files /dev/null and b/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/assets/images/answernotfound.png differ diff --git a/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/assets/images/attached.svg b/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/assets/images/attached.svg new file mode 100644 index 0000000..ba09784 --- /dev/null +++ b/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/assets/images/attached.svg @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/assets/images/available.svg b/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/assets/images/available.svg new file mode 100644 index 0000000..5a460d0 --- /dev/null +++ b/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/assets/images/available.svg @@ -0,0 +1,4 @@ + + + + diff --git a/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/assets/images/badge-banner.svg b/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/assets/images/badge-banner.svg new file mode 100644 index 0000000..a38efec --- /dev/null +++ b/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/assets/images/badge-banner.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/assets/images/badge.svg b/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/assets/images/badge.svg new file mode 100644 index 0000000..3fc6b94 --- /dev/null +++ b/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/assets/images/badge.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/assets/images/books.png b/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/assets/images/books.png new file mode 100644 index 0000000..94feabc Binary files /dev/null and b/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/assets/images/books.png differ diff --git a/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/assets/images/bot_logo.svg b/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/assets/images/bot_logo.svg new file mode 100644 index 0000000..4be6c2f --- /dev/null +++ b/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/assets/images/bot_logo.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/assets/images/cancel-toast.svg b/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/assets/images/cancel-toast.svg new file mode 100644 index 0000000..9db4780 --- /dev/null +++ b/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/assets/images/cancel-toast.svg @@ -0,0 +1,4 @@ + + + + diff --git a/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/assets/images/chatIcon.svg b/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/assets/images/chatIcon.svg new file mode 100644 index 0000000..3303847 --- /dev/null +++ b/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/assets/images/chatIcon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/assets/images/chat_background.png b/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/assets/images/chat_background.png new file mode 100644 index 0000000..701ec81 Binary files /dev/null and b/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/assets/images/chat_background.png differ diff --git a/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/assets/images/cross-in-circle-white copy.svg b/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/assets/images/cross-in-circle-white copy.svg new file mode 100644 index 0000000..96a0fe3 --- /dev/null +++ b/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/assets/images/cross-in-circle-white copy.svg @@ -0,0 +1,3 @@ + + + diff --git a/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/assets/images/cross-in-circle-white.svg b/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/assets/images/cross-in-circle-white.svg new file mode 100644 index 0000000..96a0fe3 --- /dev/null +++ b/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/assets/images/cross-in-circle-white.svg @@ -0,0 +1,3 @@ + + + diff --git a/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/assets/images/cross-in-circle.svg b/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/assets/images/cross-in-circle.svg new file mode 100644 index 0000000..c5c50ea --- /dev/null +++ b/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/assets/images/cross-in-circle.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/assets/images/delete_notification_dialog_image.png b/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/assets/images/delete_notification_dialog_image.png new file mode 100644 index 0000000..5afbf5c Binary files /dev/null and b/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/assets/images/delete_notification_dialog_image.png differ diff --git a/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/assets/images/doc.png b/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/assets/images/doc.png new file mode 100644 index 0000000..0b21898 Binary files /dev/null and b/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/assets/images/doc.png differ diff --git a/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/assets/images/error_img.svg b/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/assets/images/error_img.svg new file mode 100644 index 0000000..ac20193 --- /dev/null +++ b/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/assets/images/error_img.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/assets/images/feedback-container.svg b/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/assets/images/feedback-container.svg new file mode 100644 index 0000000..7b0f481 --- /dev/null +++ b/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/assets/images/feedback-container.svg @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/assets/images/footer.png b/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/assets/images/footer.png new file mode 100644 index 0000000..eda0b3b Binary files /dev/null and b/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/assets/images/footer.png differ diff --git a/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/assets/images/gcloudlight1.png b/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/assets/images/gcloudlight1.png new file mode 100644 index 0000000..5ddb77c Binary files /dev/null and b/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/assets/images/gcloudlight1.png differ diff --git a/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/assets/images/gnosis-logo.svg b/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/assets/images/gnosis-logo.svg new file mode 100644 index 0000000..17d0b7f --- /dev/null +++ b/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/assets/images/gnosis-logo.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/assets/images/gsd-icon.png b/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/assets/images/gsd-icon.png new file mode 100644 index 0000000..abeacf9 Binary files /dev/null and b/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/assets/images/gsd-icon.png differ diff --git a/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/assets/images/home_bottom.png b/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/assets/images/home_bottom.png new file mode 100644 index 0000000..78d1c92 Binary files /dev/null and b/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/assets/images/home_bottom.png differ diff --git a/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/assets/images/img.png b/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/assets/images/img.png new file mode 100644 index 0000000..53bc884 Binary files /dev/null and b/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/assets/images/img.png differ diff --git a/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/assets/images/left_loader_images/Left/1.png b/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/assets/images/left_loader_images/Left/1.png new file mode 100644 index 0000000..d930ffb Binary files /dev/null and b/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/assets/images/left_loader_images/Left/1.png differ diff --git a/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/assets/images/left_loader_images/Left/2.png b/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/assets/images/left_loader_images/Left/2.png new file mode 100644 index 0000000..ac32f6a Binary files /dev/null and b/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/assets/images/left_loader_images/Left/2.png differ diff --git a/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/assets/images/left_loader_images/Left/3.png b/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/assets/images/left_loader_images/Left/3.png new file mode 100644 index 0000000..cee4db0 Binary files /dev/null and b/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/assets/images/left_loader_images/Left/3.png differ diff --git a/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/assets/images/left_loader_images/Left/4.png b/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/assets/images/left_loader_images/Left/4.png new file mode 100644 index 0000000..77a6d94 Binary files /dev/null and b/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/assets/images/left_loader_images/Left/4.png differ diff --git a/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/assets/images/left_loader_images/Left/5.png b/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/assets/images/left_loader_images/Left/5.png new file mode 100644 index 0000000..deba49b Binary files /dev/null and b/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/assets/images/left_loader_images/Left/5.png differ diff --git a/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/assets/images/left_loader_images/Left/6.png b/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/assets/images/left_loader_images/Left/6.png new file mode 100644 index 0000000..61e1988 Binary files /dev/null and b/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/assets/images/left_loader_images/Left/6.png differ diff --git a/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/assets/images/login-globe.png b/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/assets/images/login-globe.png new file mode 100644 index 0000000..f64021b Binary files /dev/null and b/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/assets/images/login-globe.png differ diff --git a/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/assets/images/login_left.svg b/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/assets/images/login_left.svg new file mode 100644 index 0000000..397f17f --- /dev/null +++ b/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/assets/images/login_left.svg @@ -0,0 +1,101 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/assets/images/new-pdc-logo.svg b/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/assets/images/new-pdc-logo.svg new file mode 100644 index 0000000..c267930 --- /dev/null +++ b/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/assets/images/new-pdc-logo.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/assets/images/noPic.svg b/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/assets/images/noPic.svg new file mode 100644 index 0000000..0f9c09d --- /dev/null +++ b/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/assets/images/noPic.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/assets/images/pdf.png b/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/assets/images/pdf.png new file mode 100644 index 0000000..a8c471f Binary files /dev/null and b/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/assets/images/pdf.png differ diff --git a/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/assets/images/phoneIcon.png b/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/assets/images/phoneIcon.png new file mode 100644 index 0000000..a0af511 Binary files /dev/null and b/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/assets/images/phoneIcon.png differ diff --git a/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/assets/images/right_loader_images/Right/1.png b/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/assets/images/right_loader_images/Right/1.png new file mode 100644 index 0000000..c571db1 Binary files /dev/null and b/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/assets/images/right_loader_images/Right/1.png differ diff --git a/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/assets/images/right_loader_images/Right/2.png b/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/assets/images/right_loader_images/Right/2.png new file mode 100644 index 0000000..d6a1da0 Binary files /dev/null and b/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/assets/images/right_loader_images/Right/2.png differ diff --git a/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/assets/images/right_loader_images/Right/3.png b/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/assets/images/right_loader_images/Right/3.png new file mode 100644 index 0000000..042ba23 Binary files /dev/null and b/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/assets/images/right_loader_images/Right/3.png differ diff --git a/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/assets/images/right_loader_images/Right/4.png b/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/assets/images/right_loader_images/Right/4.png new file mode 100644 index 0000000..96c067a Binary files /dev/null and b/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/assets/images/right_loader_images/Right/4.png differ diff --git a/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/assets/images/right_loader_images/Right/5.png b/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/assets/images/right_loader_images/Right/5.png new file mode 100644 index 0000000..3a15579 Binary files /dev/null and b/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/assets/images/right_loader_images/Right/5.png differ diff --git a/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/assets/images/right_loader_images/Right/6.png b/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/assets/images/right_loader_images/Right/6.png new file mode 100644 index 0000000..7de6698 Binary files /dev/null and b/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/assets/images/right_loader_images/Right/6.png differ diff --git a/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/assets/images/searchIcon.png b/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/assets/images/searchIcon.png new file mode 100644 index 0000000..7b95f3c Binary files /dev/null and b/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/assets/images/searchIcon.png differ diff --git a/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/assets/images/searchIcon.svg b/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/assets/images/searchIcon.svg new file mode 100644 index 0000000..b82fe2f --- /dev/null +++ b/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/assets/images/searchIcon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/assets/images/support-page-banner.svg b/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/assets/images/support-page-banner.svg new file mode 100644 index 0000000..0fa6347 --- /dev/null +++ b/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/assets/images/support-page-banner.svg @@ -0,0 +1,134 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/assets/images/tick-with-circle.svg b/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/assets/images/tick-with-circle.svg new file mode 100644 index 0000000..8335e7c --- /dev/null +++ b/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/assets/images/tick-with-circle.svg @@ -0,0 +1,4 @@ + + + + diff --git a/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/assets/images/unnamed.jpeg b/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/assets/images/unnamed.jpeg new file mode 100644 index 0000000..b33f75d Binary files /dev/null and b/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/assets/images/unnamed.jpeg differ diff --git a/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/assets/images/user-logo.svg b/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/assets/images/user-logo.svg new file mode 100644 index 0000000..8135e28 --- /dev/null +++ b/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/assets/images/user-logo.svg @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/assets/images/voiceIcon.svg b/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/assets/images/voiceIcon.svg new file mode 100644 index 0000000..9c3e1d3 --- /dev/null +++ b/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/assets/images/voiceIcon.svg @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/assets/images/warning-yellow.png b/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/assets/images/warning-yellow.png new file mode 100644 index 0000000..e94f265 Binary files /dev/null and b/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/assets/images/warning-yellow.png differ diff --git a/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/environments/constant.ts b/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/environments/constant.ts new file mode 100644 index 0000000..8edf1a8 --- /dev/null +++ b/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/environments/constant.ts @@ -0,0 +1,25 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export const search_document_type = ['pdf', 'docx', 'doc']; + +export const search_image_type = ['jpg', 'jpeg', 'png']; + +export const image_name = new Map() + .set('doc', 'assets/images/doc.png') + .set('pdf', 'assets/images/pdf.png'); + +export const PDF = 'pdf'; diff --git a/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/environments/environment.ts b/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/environments/environment.ts new file mode 100644 index 0000000..3b04dba --- /dev/null +++ b/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/environments/environment.ts @@ -0,0 +1,31 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export const environment = { + firebase: { + apiKey: '', + authDomain: '', + projectId: '', + storageBucket: '', + messagingSenderId: '', + appId: '', + measurementId: '', + }, + requiredLogin: 'False', + backendURL: 'http://localhost:8080/api', + chatbotName: 'My New Agent', + environment: 'development', +}; diff --git a/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/favicon.ico b/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/favicon.ico new file mode 100644 index 0000000..997406a Binary files /dev/null and b/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/favicon.ico differ diff --git a/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/index.html b/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/index.html new file mode 100644 index 0000000..d1b75a6 --- /dev/null +++ b/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/index.html @@ -0,0 +1,40 @@ + + + + + + + + Quick Bot + + + + + + + + + + + + + + + + diff --git a/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/main.ts b/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/main.ts new file mode 100644 index 0000000..0da5f95 --- /dev/null +++ b/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/main.ts @@ -0,0 +1,23 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {platformBrowserDynamic} from '@angular/platform-browser-dynamic'; + +import {AppModule} from './app/app.module'; + +platformBrowserDynamic() + .bootstrapModule(AppModule) + .catch(err => console.error(err)); diff --git a/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/styles.scss b/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/styles.scss new file mode 100644 index 0000000..50cb6b6 --- /dev/null +++ b/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/src/styles.scss @@ -0,0 +1,76 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* You can add global styles to this file, and also import other style files */ + +html, body { height: 100%;margin: 0; font-family: 'Google Sans', sans-serif !important; } + +.material-symbols-rounded { +font-variation-settings: +'FILL', +'wght' 600, +'GRAD' 0, +'opsz' 48 +} + +.tooltip-list .mdc-tooltip__surface { + white-space: pre; + max-width: unset !important; +} + +.mat-mdc-slide-toggle.mat-accent { + --mdc-switch-selected-focus-state-layer-color: #4285F4; + --mdc-switch-selected-handle-color: #4285F4; + --mdc-switch-selected-hover-state-layer-color: #4285F4; + --mdc-switch-selected-pressed-state-layer-color: #4285F4; + --mdc-switch-selected-focus-handle-color: #3f74e0; + --mdc-switch-selected-hover-handle-color: #3f74e0; + --mdc-switch-selected-pressed-handle-color: #3f74e0; + --mdc-switch-selected-focus-track-color: rgba(66, 133, 244, 0.3); + --mdc-switch-selected-hover-track-color: rgba(66, 133, 244, 0.3); + --mdc-switch-selected-pressed-track-color: rgba(66, 133, 244, 0.3); + --mdc-switch-selected-track-color: rgba(66, 133, 244, 0.3); +} + +.arrow-top { + box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.1) !important; + border-radius: 20px !important; +} + +.mat-mdc-checkbox.mat-accent { + --mdc-checkbox-selected-checkmark-color: #fff; + --mdc-checkbox-selected-focus-icon-color: #4285F4; + --mdc-checkbox-selected-hover-icon-color: #4285F4; + --mdc-checkbox-selected-icon-color: #4285F4; + --mdc-checkbox-selected-pressed-icon-color: #4285F4; + --mdc-checkbox-unselected-focus-icon-color: #212121; + --mdc-checkbox-unselected-hover-icon-color: #212121; + --mdc-checkbox-disabled-selected-icon-color: rgba(0, 0, 0, 0.38); + --mdc-checkbox-disabled-unselected-icon-color: rgba(0, 0, 0, 0.38); + --mdc-checkbox-unselected-icon-color: rgba(0, 0, 0, 0.54); + --mdc-checkbox-unselected-pressed-icon-color: rgba(0, 0, 0, 0.54); +} + +$tour-skip-link-color: red; +$tour-text-color: purple; +$tour-next-text-color: yellow; +$tour-zindex: 100; +$tour-orb-color: cyan; +$tour-next-button-color: green; +$tour-next-button-hover: black; +$tour-back-button-color: white; +$tour-shadow-color: gray; +$tour-step-color: aliceblue; diff --git a/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/tsconfig.app.json b/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/tsconfig.app.json new file mode 100644 index 0000000..374cc9d --- /dev/null +++ b/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/tsconfig.app.json @@ -0,0 +1,14 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "./out-tsc/app", + "types": [] + }, + "files": [ + "src/main.ts" + ], + "include": [ + "src/**/*.d.ts" + ] +} diff --git a/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/tsconfig.json b/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/tsconfig.json new file mode 100644 index 0000000..fc49b39 --- /dev/null +++ b/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/tsconfig.json @@ -0,0 +1,34 @@ +{ + "extends": "./node_modules/gts/tsconfig-google.json", + "compileOnSave": false, + "compilerOptions": { + "ignoreDeprecations": "5.0", + "baseUrl": "./", + "outDir": "./dist/out-tsc", + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "downlevelIteration": true, + "experimentalDecorators": true, + "moduleResolution": "node", + "importHelpers": true, + "target": "ES2022", + "module": "ES2022", + "useDefineForClassFields": false, + "lib": [ + "ES2022", + "dom" + ], + }, + "angularCompilerOptions": { + "enableI18nLegacyMessageIdFormat": false, + "strictInjectionParameters": true, + "strictInputAccessModifiers": true, + "strictTemplates": true, + "suppressImplicitAnyIndexErrors": true, + "strictPropertyInitialization": false, + }, + "include": [ + "src/**/*.ts", + "test/**/*.ts" + ] +} diff --git a/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/tsconfig.spec.json b/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/tsconfig.spec.json new file mode 100644 index 0000000..be7e9da --- /dev/null +++ b/gemini/sample-apps/quickbot/document-search-using-agent-builder/frontend/tsconfig.spec.json @@ -0,0 +1,14 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "./out-tsc/spec", + "types": [ + "jasmine" + ] + }, + "include": [ + "src/**/*.spec.ts", + "src/**/*.d.ts" + ] +} diff --git a/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/.dockerignore b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/.dockerignore new file mode 100644 index 0000000..d472d1d --- /dev/null +++ b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/.dockerignore @@ -0,0 +1,12 @@ +node_modules +npm-debug.log +tests +coverage +Dockerfile +.dockerignore +.env +*.md +.git +.vscode +.gitignore +Makefile \ No newline at end of file diff --git a/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/README.md b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/README.md new file mode 100644 index 0000000..2d5bead --- /dev/null +++ b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/README.md @@ -0,0 +1,275 @@ +# Quickbot - Background Changer using Imagen3 + +Quickbot Background Changer Template is an innovative application built with Google Cloud's Imagen3 model on Vertex AI. It provides a user-friendly frontend interface and a robust backend API to seamlessly alter or generate image backgrounds based on user input. + +## Overview + +This project allows users to modify or replace image backgrounds, leveraging the capabilities of Google Cloud's Imagen3 model on Vertex AI. It's designed with a decoupled frontend and backend architecture, suitable for scalable deployments. + +## Demo +Watch the Background Changer Template transform images: + +![Background Changer Demo](./assets/quickbot-background-changer-photo.png) + +## Prerequisites + +Before you begin, ensure you have the following installed: + +* **Docker and Docker Compose v2:** Essential for the containerized deployment. + * Verify your Docker Compose version with `docker compose version`. If you have an older `docker-compose` (with a hyphen), you might need to upgrade to use `docker compose` in the commands. +* **Google Cloud SDK (`gcloud` CLI):** Required for manual backend setup, authentication, and interaction with Google Cloud services (like Vertex AI for Imagen3). +* **Python 3.x:** For backend development (if not using Docker). +* **Node.js and npm (or yarn):** For frontend development (if not using Docker). + +## Getting Started + +You have two main options to get the application running: + +### Option 1: Using Docker Compose (Recommended for Quick Start) + +This is the **simplest way to get the entire application (frontend and backend) up and running!** You just need to run `docker compose up` after initial setup. See the next steps: + +1. **Ensure Docker and Docker Compose v2 are installed and running.** + +2. **Authenticate with Google Cloud for Vertex AI Imagen Access:** + For the application (specifically the backend running in Docker) to interact with Google Cloud's Imagen3 on Vertex AI, you need to provide Google Cloud credentials to the Docker container. The recommended way for local development is using Application Default Credentials (ADC). + Run the following commands in your local terminal: + ```bash + gcloud auth application-default login + gcloud config set project + gcloud auth application-default set-quota-project + + # Verify your configuration + gcloud auth list + gcloud config list project + ``` + This will create or update ADC on your local machine. The `docker-compose.yml` file is typically configured to mount these local credentials into the backend container, allowing it to authenticate. + > **Windows Users:** The path to ADC might differ on Windows. You may need to adjust the commented-out volume mount paths for `gcloud` credentials in the `docker-compose.yml` file to ensure the backend container can access them. + > **Note:** Ensure the Vertex AI API is enabled in your Google Cloud project. + +3. **Build Docker Images:** + Build the Docker images for the frontend and backend services: + ```bash + docker compose build + ``` + The backend will be configured using environment variables (see "Environment Variables" section) to connect to Google Cloud services, including Imagen3 on Vertex AI. + +4. **Run the application:** + After building the images, start the services: + ```bash + docker compose up + ``` + The frontend should typically be available at `http://localhost:4200` (or as configured) and the backend API at `http://localhost:8080`. + +### Option 2: Manual Setup (for Development and Customization) + +Follow these steps if you prefer to run the frontend and backend services manually on your local machine. + +**A. Backend Setup** + +1. **Navigate to the `backend/` directory.** + ```bash + cd backend + ``` + +2. **Create a virtual environment and install dependencies:** + ```bash + # Check if you are already in an environment + pip -V + + # If not, create and activate (for Linux/macOS) + python3 -m venv .venv + source .venv/bin/activate + + # Install requirements + pip3 install -r requirements.txt + ``` + > **VS Code Tip:** If VS Code doesn't recognize your virtual environment, press `Ctrl + Shift + P` (or `Cmd + Shift + P` on Mac), type "Python: Select Interpreter", choose "Enter interpreter path...", and then find and select `.venv/bin/python` inside your `backend` directory. + +3. **Setup Google Cloud (`gcloud`) credentials:** + Ensure you're authenticated and your project is configured correctly. The Vertex AI API should be enabled in your project. + ```bash + gcloud auth login # Login with your user account + gcloud config set project + + # For services using Application Default Credentials (ADC) locally + gcloud auth application-default login + # Optionally, set a quota project for ADC if not inherited + gcloud auth application-default set-quota-project + + # Verify configuration + gcloud auth list + gcloud config list project + ``` + +4. **Configure Environment Variables:** + Backend configuration is managed via environment variables. Create a `.local.env` file in the `backend/` directory (you can copy from `.local.env.example` if one exists). This file should be added to `.gitignore`. + + * **For Mac/Windows (or zsh console on Linux):** + Source the variables directly (from the `backend/` directory): + ```bash + . ./.local.env + ``` + * **For Linux (bash):** + Open `backend/.venv/bin/activate` and append the `export` commands from your `backend/.local.env` file after the `PATH` export section. For example: + ```sh + # ... existing activate script content ... + _OLD_VIRTUAL_PATH="$PATH" + PATH="$VIRTUAL_ENV/bin:$PATH" + export PATH + + # Quickbot env variables (copied from .local.env) + export ENVIRONMENT="development" + export FRONTEND_URL="http://localhost:4200" + export GCP_PROJECT_ID="" + export VERTEX_AI_LOCATION="us-central1" # Example location + export IMAGEN_MODEL_ID="imagegeneration@006" # Example model + # ... other necessary variables ... + ``` + Verify the variables are set by running `env` in your activated terminal. + +5. **Run the setup script (if applicable):** + This script might perform initial configurations if needed by the backend. + ```bash + # from the backend/ directory + python3 setup.py + ``` + +6. **Run the backend application:** + ```bash + # from the backend/ directory + uvicorn main:app --reload --port 8080 + ``` + +**B. Frontend Setup** + +(These instructions assume a typical TypeScript/Angular frontend. Adjust as necessary based on your `frontend/README.md`.) + +1. **Navigate to the `frontend/` directory.** + ```bash + cd frontend + ``` +2. **Install dependencies:** + ```bash + npm install + ``` +3. **Environment Variables (if applicable):** + The frontend might require its own environment configuration (e.g., via a `.env` file or Angular's `environment.ts` files for API endpoints). Check the `frontend/` directory or its `README.md` for specific instructions. +4. **Run the frontend application:** + ```bash + npm start + # Or, for many Angular projects: + # ng serve + ``` + The application will typically be available at `http://localhost:4200`. + +## Project Structure (highlighting important parts) +```text +text-to-imagen-using-imagen3/ +├── backend/ # Python backend (FastAPI/Uvicorn) +│ ├── .venv/ # Python virtual environment (gitignored) +│ ├── .local.env # Local environment variables (gitignored) +│ ├── main.py # Main application file +│ ├── requirements.txt # Backend dependencies +│ ├── setup.py # Backend setup script +│ └── README.md # Backend-specific instructions +├── frontend/ # TypeScript frontend (Angular) +│ ├── node_modules/ # Node.js dependencies (gitignored) +│ ├── src/ # Frontend source code +│ ├── package.json # Frontend dependencies and scripts +│ ├── tsconfig.json # TypeScript configuration +│ └── README.md # Frontend-specific instructions +├── docker-compose.yml # Docker Compose configuration for all services +└── README.md # This file: Root project README +``` + +## Environment Variables + +Configuration for both frontend and backend is primarily managed through environment variables. + +* **Backend:** + * When running manually, backend environment variables are typically defined in `backend/.local.env`. + * When running with Docker, these variables are usually passed into the backend container via the `docker-compose.yml` file (often referencing a `.env` file at the root or `backend/` directory). + * Key variables include: + * `GCLOUD_PROJECT`: Your Google Cloud Project ID. + * `ENVIRONMENT`: Application environment (e.g., `development`, `production`). + * `FRONTEND_URL`: URL of the frontend application (e.g., `http://localhost:4200`). + * Consult `backend/README.md` or `backend/.local.env.example` for a complete list. + +* **Frontend:** + * Frontend environment variables (e.g., API endpoint URLs) are usually managed within the frontend's build system (e.g., Angular's `environment.ts` files or a `.env` file in the `frontend/` directory). + * Consult `frontend/README.md` for specific details. + +## Code Styling & Commit Guidelines + +To maintain code quality and consistency across the project: + +* **TypeScript (Frontend):** We follow the Angular Coding Style Guide by leveraging Google's TypeScript Style Guide using `gts`. This includes a formatter, linter, and automatic code fixer. +* **Python (Backend):** We adhere to the Google Python Style Guide, using tools like `pylint` and `black` for linting and formatting. +* **Commit Messages:** We suggest following Angular's Commit Message Guidelines to create clear and descriptive commit messages. + +### Frontend (TypeScript with `gts`) + +(Assumes setup within the `frontend/` directory) + +1. **Initialize `gts` (if not already done in the project):** + Navigate to `frontend/` and run: + ```bash + npx gts init + ``` + This will set up `gts` and create necessary configuration files (like `tsconfig.json`). Ensure your `tsconfig.json` (or a related `gts` config file like `.gtsrc`) includes an extension for `gts` defaults, typically: + ```json + { + "extends": "./node_modules/gts/tsconfig-google.json" + // ... other configurations + } + ``` +2. **Check for linting issues:** + (This assumes a `lint` script is defined in `frontend/package.json`, e.g., `"lint": "gts lint"`) + ```bash + # from frontend/ directory + npm run lint + ``` +3. **Fix linting issues automatically (where possible):** + (This assumes a `fix` script is defined in `frontend/package.json`, e.g., `"fix": "gts fix"`) + ```bash + # from frontend/ directory + npm run fix + ``` + +### Backend (Python with `pylint` and `black`) + +(Assumes setup within the `backend/` directory and its virtual environment activated) + +1. **Ensure Dependencies are Installed:** + Add `pylint` and `black` to your `backend/requirements.txt` file if not already present: + ``` + pylint + black + ``` + Then install them within your virtual environment: + ```bash + # from backend/ directory, with .venv activated + pip install pylint black + # or pip install -r requirements.txt + ``` +2. **Configure `pylint`:** + It's recommended to have a `.pylintrc` file in your `backend/` directory to configure `pylint` rules. You can generate one if it doesn't exist: + ```bash + # from backend/ directory + pylint --generate-rcfile > .pylintrc + ``` + Customize this file according to your project's needs and the Google Python Style Guide. +3. **Check for linting issues with `pylint`:** + Navigate to the `backend/` directory and run: + ```bash + # from backend/ directory + pylint . + # Or specify modules/packages: pylint your_module_name + ``` +4. **Format code with `black`:** + To automatically format all Python files in the `backend/` directory and its subdirectories: + ```bash + # from backend/ directory + python -m black . --line-length=80 + ``` diff --git a/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/assets/quickbot-background-changer-photo.png b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/assets/quickbot-background-changer-photo.png new file mode 100644 index 0000000..72b01a8 Binary files /dev/null and b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/assets/quickbot-background-changer-photo.png differ diff --git a/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/backend/.gcloudignore b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/backend/.gcloudignore new file mode 100644 index 0000000..606a07e --- /dev/null +++ b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/backend/.gcloudignore @@ -0,0 +1,20 @@ +# This file specifies files that are *not* uploaded to Google Cloud +# using gcloud. It follows the same syntax as .gitignore, with the addition of +# "#!include" directives (which insert the entries of the given .gitignore-style +# file at that point). +# +# For more information, run: +# $ gcloud topic gcloudignore +# +.gcloudignore +# If you would like to upload your .git directory, .gitignore file or files +# from your .gitignore file, remove the corresponding line +# below: +.git +.gitignore + +# Python pycache: +__pycache__/ +# Ignored by the build system +/setup.cfg +my_env \ No newline at end of file diff --git a/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/backend/.gitignore b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/backend/.gitignore new file mode 100644 index 0000000..83bb4d6 --- /dev/null +++ b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/backend/.gitignore @@ -0,0 +1,135 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# VSCode +.vscode/* +!.vscode/settings.json +!.vscode/extensions.json + +# Logs +*/output +*.info diff --git a/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/backend/Dockerfile b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/backend/Dockerfile new file mode 100644 index 0000000..e796690 --- /dev/null +++ b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/backend/Dockerfile @@ -0,0 +1,27 @@ +# Copyright 2025 Google LLC +# +# 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 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +FROM python:3.11-alpine + +COPY requirements.txt /tmp +RUN pip install -r /tmp/requirements.txt + +WORKDIR /app +COPY . /app/ + +ENV ENVIRONMENT="development" +ENV FRONTEND_URL="http://localhost:4200" + +EXPOSE 8080 +ENTRYPOINT ["gunicorn", "main:app", "--workers=4", "--worker-class=uvicorn.workers.UvicornWorker", "--timeout=36000", "--bind=0.0.0.0:8080"] \ No newline at end of file diff --git a/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/backend/README.md b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/backend/README.md new file mode 100644 index 0000000..32a31a4 --- /dev/null +++ b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/backend/README.md @@ -0,0 +1,133 @@ +# QuickBot App Backend | image-background-changer-using-imagen3 + +[![linting: pylint](https://img.shields.io/badge/linting-pylint-yellowgreen)](https://github.com/pylint-dev/pylint) +[![Code Style: Google](https://img.shields.io/badge/code%20style-google-blueviolet.svg)](https://github.com/google/gts) + +QuickBot App is a set of templates that can be deployed out of the box into Cloud Run and work independently. Each one can be run independently connected to the user default google cloud auth credentials, and based on the complexity of each template, may require to deploy more or less resources into our Google Cloud Project. +The architecture always follows the following structure: a folder for the frontend which consists in an Angular app, and a backend folder which consists of a FastAPI Python app. + + +## Setting up +### 1. Create virtualenv inside the backend folder and install dependencies +Create a virtual environment on the root of the application, activate it and install the requirements +``` +# check if you are already in the env +pip -V + +# if not then +python3 -m venv .venv +source .venv/bin/activate +pip3 install -r requirements.txt +``` + +> **IMPORTANT!** VS Code may not recognize your env, in that case type "ctrl + shift + P", then select "Python: Select Interpreter" and then select "Enter interpreter path..." and then select your .venv python interpreter, in this case .backend/.venv/bin/python + + +### 2. Setup gcloud credentials +``` +gcloud auth list +gcloud config list + +gcloud auth login +gcloud config set project +gcloud auth application-default set-quota-project + +gcloud auth list +gcloud config list +``` + +### 3. Add environment variables + +#### If you have Mac or Windows (or if you are using zsh console on Linux) +``` +. ./local.env +``` + +#### If you have Linux +Open the file .venv/bin/activate and paste the env variables from `.local.env` after the PATH export, like this: +``` +... + +_OLD_VIRTUAL_PATH="$PATH" +PATH="$VIRTUAL_ENV/bin:$PATH" +export PATH + +# Quickbot env variables +export ENVIRONMENT=development +export FRONTEND_URL=http://localhost:4200 + +... +``` + +Check that the env variables has been taken into account, running: +``` +env +``` +You should see the new env variables set there + + +### 4. Run the application +Finally run using uvicorn +``` +uvicorn main:app --reload --port 8080 +``` + +## Code Styling & Commit Guidelines + +To maintain code quality and consistency: + +* **TypeScript (Frontend):** We follow [Angular Coding Style Guide](https://angular.dev/style-guide) by leveraging the use of [Google's TypeScript Style Guide](https://github.com/google/gts) using `gts`. This includes a formatter, linter, and automatic code fixer. +* **Python (Backend):** We adhere to the [Google Python Style Guide](https://google.github.io/styleguide/pyguide.html), using tools like `pylint` and `black` for linting and formatting. +* **Commit Messages:** We suggest following [Angular's Commit Message Guidelines](https://github.com/angular/angular/blob/main/contributing-docs/commit-message-guidelines.md) to create clear and descriptive commit messages. + +#### Frontend (TypeScript with `gts`) + +1. **Initialize `gts` (if not already done in the project):** + Navigate to the `frontend/` directory and run: + ```bash + npx gts init + ``` + This will set up `gts` and create necessary configuration files (like `tsconfig.json`). Ensure your `tsconfig.json` (or a related gts config file like `.gtsrc`) includes an extension for `gts` defaults, typically: + ```json + { + "extends": "./node_modules/gts/tsconfig-google.json", + // ... other configurations + } + ``` +2. **Check for linting issues:** + ```bash + npm run lint + ``` + (This assumes a `lint` script is defined in `package.json`, e.g., `"lint": "gts lint"`) +3. **Fix linting issues automatically (where possible):** + ```bash + npm run fix + ``` + (This assumes a `fix` script is defined in `package.json`, e.g., `"fix": "gts fix"`) + +#### Backend (Python with `pylint` and `black`) + +1. **Ensure Dependencies are Installed:** + Add `pylint` and `black` to your `backend/requirements.txt` file: + ``` + pylint + black + ``` + Then install them within your virtual environment: + ```bash + pip install pylint black + # or pip install -r requirements.txt + ``` +2. **Configure `pylint`:** + It's recommended to have a `.pylintrc` file in your `backend/` directory to configure `pylint` rules. You might need to copy a standard one or generate one (`pylint --generate-rcfile > .pylintrc`). +3. **Check for linting issues with `pylint`:** + Navigate to the `backend/` directory and run: + ```bash + pylint . + ``` + (Or specify modules/packages: `pylint your_module_name`) +4. **Format code with `black`:** + To automatically format all Python files in the current directory and subdirectories: + ```bash + python -m black . --line-length=80 + ``` diff --git a/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/backend/__init__.py b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/backend/__init__.py new file mode 100644 index 0000000..36a1e8d --- /dev/null +++ b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/backend/__init__.py @@ -0,0 +1,14 @@ +# Copyright 2025 Google LLC +# +# 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 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + diff --git a/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/backend/local.env b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/backend/local.env new file mode 100644 index 0000000..af26a11 --- /dev/null +++ b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/backend/local.env @@ -0,0 +1,2 @@ +export ENVIRONMENT=development +export FRONTEND_URL=http://localhost:4200 diff --git a/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/backend/main.py b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/backend/main.py new file mode 100644 index 0000000..4efb486 --- /dev/null +++ b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/backend/main.py @@ -0,0 +1,96 @@ +# Copyright 2025 Google LLC +# +# 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 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from os import getenv + +from fastapi import FastAPI, File, UploadFile +from fastapi.middleware.cors import CORSMiddleware +from google.cloud import speech + +from src.controller.search import router as search_router + +app = FastAPI() + + +def configure_cors(app): + """Configures CORS middleware based on the environment.""" + environment = getenv("ENVIRONMENT") + allowed_origins = [] + + if environment == "production": + frontend_url = getenv("FRONTEND_URL") + if not frontend_url: + raise ValueError( + "FRONTEND_URL environment variable not set in production" + ) + allowed_origins.append(frontend_url) + elif environment == "development": + allowed_origins.append("*") # Allow all origins in development + else: + raise ValueError( + f"Invalid ENVIRONMENT: {environment}. Must be 'production' or 'development'" + ) + + app.add_middleware( + CORSMiddleware, + allow_origins=allowed_origins, + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], + ) + + +# Create a route to handle GET requests on root +@app.get("/") +async def root(): + return "You are calling Quick Bot Backend" + + +# Create a route to handle GET requests on /version +@app.get("/api/version") +def version(): + return "v0.0.1" + + +@app.post("/api/audio_chat") +async def audio_chat(audio_file: UploadFile = File(...)): + client = speech.SpeechClient() + audio_content = await audio_file.read() + audio = speech.RecognitionAudio(content=audio_content) + config = speech.RecognitionConfig( + language_code="en-US", + sample_rate_hertz=48000, + model="default", + audio_channel_count=1, + enable_word_confidence=True, + enable_word_time_offsets=True, + ) + + operation = client.long_running_recognize(config=config, audio=audio) + + print("Waiting for operation to complete...") + response = operation.result(timeout=90) + print(response) + + text = "" + for result in response.results: + print(f"Transcript: {result.alternatives[0].transcript}") + text = result.alternatives[0].transcript + + return text, 200 + + +configure_cors(app) + +app.include_router(search_router) diff --git a/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/backend/pylintrc b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/backend/pylintrc new file mode 100644 index 0000000..cd99157 --- /dev/null +++ b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/backend/pylintrc @@ -0,0 +1,399 @@ +# This Pylint rcfile contains a best-effort configuration to uphold the +# best-practices and style described in the Google Python style guide: +# https://google.github.io/styleguide/pyguide.html +# +# Its canonical open-source location is: +# https://google.github.io/styleguide/pylintrc + +[MAIN] + +# Files or directories to be skipped. They should be base names, not paths. +ignore=third_party + +# Files or directories matching the regex patterns are skipped. The regex +# matches against base names, not paths. +ignore-patterns= + +# Pickle collected data for later comparisons. +persistent=no + +# List of plugins (as comma separated values of python modules names) to load, +# usually to register additional checkers. +load-plugins= + +# Use multiple processes to speed up Pylint. +jobs=4 + +# Allow loading of arbitrary C extensions. Extensions are imported into the +# active Python interpreter and may run arbitrary code. +unsafe-load-any-extension=no + + +[MESSAGES CONTROL] + +# Only show warnings with the listed confidence levels. Leave empty to show +# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED +confidence= + +# Enable the message, report, category or checker with the given id(s). You can +# either give multiple identifier separated by comma (,) or put this option +# multiple time (only on the command line, not in the configuration file where +# it should appear only once). See also the "--disable" option for examples. +#enable= + +# Disable the message, report, category or checker with the given id(s). You +# can either give multiple identifiers separated by comma (,) or put this +# option multiple times (only on the command line, not in the configuration +# file where it should appear only once).You can also use "--disable=all" to +# disable everything first and then reenable specific checks. For example, if +# you want to run only the similarities checker, you can use "--disable=all +# --enable=similarities". If you want to run only the classes checker, but have +# no Warning level messages displayed, use"--disable=all --enable=classes +# --disable=W" +disable=R, + abstract-method, + apply-builtin, + arguments-differ, + attribute-defined-outside-init, + backtick, + bad-option-value, + basestring-builtin, + buffer-builtin, + c-extension-no-member, + consider-using-enumerate, + cmp-builtin, + cmp-method, + coerce-builtin, + coerce-method, + delslice-method, + div-method, + eq-without-hash, + execfile-builtin, + file-builtin, + filter-builtin-not-iterating, + fixme, + getslice-method, + global-statement, + hex-method, + idiv-method, + implicit-str-concat, + import-error, + import-self, + import-star-module-level, + input-builtin, + intern-builtin, + invalid-str-codec, + locally-disabled, + long-builtin, + long-suffix, + map-builtin-not-iterating, + misplaced-comparison-constant, + missing-function-docstring, + metaclass-assignment, + next-method-called, + next-method-defined, + no-absolute-import, + no-init, # added + no-member, + no-name-in-module, + no-self-use, + nonzero-method, + oct-method, + old-division, + old-ne-operator, + old-octal-literal, + old-raise-syntax, + parameter-unpacking, + print-statement, + raising-string, + range-builtin-not-iterating, + raw_input-builtin, + rdiv-method, + reduce-builtin, + relative-import, + reload-builtin, + round-builtin, + setslice-method, + signature-differs, + standarderror-builtin, + suppressed-message, + sys-max-int, + trailing-newlines, + unichr-builtin, + unicode-builtin, + unnecessary-pass, + unpacking-in-except, + useless-else-on-loop, + useless-suppression, + using-cmp-argument, + wrong-import-order, + xrange-builtin, + zip-builtin-not-iterating, + + +[REPORTS] + +# Set the output format. Available formats are text, parseable, colorized, msvs +# (visual studio) and html. You can also give a reporter class, eg +# mypackage.mymodule.MyReporterClass. +output-format=text + +# Tells whether to display a full report or only the messages +reports=no + +# Python expression which should return a note less than 10 (10 is the highest +# note). You have access to the variables errors warning, statement which +# respectively contain the number of errors / warnings messages and the total +# number of statements analyzed. This is used by the global evaluation report +# (RP0004). +evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) + +# Template used to display messages. This is a python new-style format string +# used to format the message information. See doc for all details +#msg-template= + + +[BASIC] + +# Good variable names which should always be accepted, separated by a comma +good-names=main,_ + +# Bad variable names which should always be refused, separated by a comma +bad-names= + +# Colon-delimited sets of names that determine each other's naming style when +# the name regexes allow several styles. +name-group= + +# Include a hint for the correct naming format with invalid-name +include-naming-hint=no + +# List of decorators that produce properties, such as abc.abstractproperty. Add +# to this list to register other decorators that produce valid properties. +property-classes=abc.abstractproperty,cached_property.cached_property,cached_property.threaded_cached_property,cached_property.cached_property_with_ttl,cached_property.threaded_cached_property_with_ttl + +# Regular expression matching correct function names +function-rgx=^(?:(?PsetUp|tearDown|setUpModule|tearDownModule)|(?P_?[A-Z][a-zA-Z0-9]*)|(?P_?[a-z][a-z0-9_]*))$ + +# Regular expression matching correct variable names +variable-rgx=^[a-z][a-z0-9_]*$ + +# Regular expression matching correct constant names +const-rgx=^(_?[A-Z][A-Z0-9_]*|__[a-z0-9_]+__|_?[a-z][a-z0-9_]*)$ + +# Regular expression matching correct attribute names +attr-rgx=^_{0,2}[a-z][a-z0-9_]*$ + +# Regular expression matching correct argument names +argument-rgx=^[a-z][a-z0-9_]*$ + +# Regular expression matching correct class attribute names +class-attribute-rgx=^(_?[A-Z][A-Z0-9_]*|__[a-z0-9_]+__|_?[a-z][a-z0-9_]*)$ + +# Regular expression matching correct inline iteration names +inlinevar-rgx=^[a-z][a-z0-9_]*$ + +# Regular expression matching correct class names +class-rgx=^_?[A-Z][a-zA-Z0-9]*$ + +# Regular expression matching correct module names +module-rgx=^(_?[a-z][a-z0-9_]*|__init__)$ + +# Regular expression matching correct method names +method-rgx=(?x)^(?:(?P_[a-z0-9_]+__|runTest|setUp|tearDown|setUpTestCase|tearDownTestCase|setupSelf|tearDownClass|setUpClass|(test|assert)_*[A-Z0-9][a-zA-Z0-9_]*|next)|(?P_{0,2}[A-Z][a-zA-Z0-9_]*)|(?P_{0,2}[a-z][a-z0-9_]*))$ + +# Regular expression which should only match function or class names that do +# not require a docstring. +no-docstring-rgx=(__.*__|main|test.*|.*test|.*Test)$ + +# Minimum line length for functions/classes that require docstrings, shorter +# ones are exempt. +docstring-min-length=12 + + +[TYPECHECK] + +# List of decorators that produce context managers, such as +# contextlib.contextmanager. Add to this list to register other decorators that +# produce valid context managers. +contextmanager-decorators=contextlib.contextmanager,contextlib2.contextmanager + +# List of module names for which member attributes should not be checked +# (useful for modules/projects where namespaces are manipulated during runtime +# and thus existing member attributes cannot be deduced by static analysis. It +# supports qualified module names, as well as Unix pattern matching. +ignored-modules= + +# List of class names for which member attributes should not be checked (useful +# for classes with dynamically set attributes). This supports the use of +# qualified names. +ignored-classes=optparse.Values,thread._local,_thread._local + +# List of members which are set dynamically and missed by pylint inference +# system, and so shouldn't trigger E1101 when accessed. Python regular +# expressions are accepted. +generated-members= + + +[FORMAT] + +# Maximum number of characters on a single line. +max-line-length=80 + +# TODO(https://github.com/pylint-dev/pylint/issues/3352): Direct pylint to exempt +# lines made too long by directives to pytype. + +# Regexp for a line that is allowed to be longer than the limit. +ignore-long-lines=(?x)( + ^\s*(\#\ )??$| + ^\s*(from\s+\S+\s+)?import\s+.+$) + +# Allow the body of an if to be on the same line as the test if there is no +# else. +single-line-if-stmt=yes + +# Maximum number of lines in a module +max-module-lines=99999 + +# String used as indentation unit. The internal Google style guide mandates 2 +# spaces. Google's externaly-published style guide says 4, consistent with +# PEP 8. Here, we use 2 spaces, for conformity with many open-sourced Google +# projects (like TensorFlow). +indent-string=' ' + +# Number of spaces of indent required inside a hanging or continued line. +indent-after-paren=4 + +# Expected format of line ending, e.g. empty (any line ending), LF or CRLF. +expected-line-ending-format= + + +[MISCELLANEOUS] + +# List of note tags to take in consideration, separated by a comma. +notes=TODO + + +[STRING] + +# This flag controls whether inconsistent-quotes generates a warning when the +# character used as a quote delimiter is used inconsistently within a module. +check-quote-consistency=yes + + +[VARIABLES] + +# Tells whether we should check for unused import in __init__ files. +init-import=no + +# A regular expression matching the name of dummy variables (i.e. expectedly +# not used). +dummy-variables-rgx=^\*{0,2}(_$|unused_|dummy_) + +# List of additional names supposed to be defined in builtins. Remember that +# you should avoid to define new builtins when possible. +additional-builtins= + +# List of strings which can identify a callback function by name. A callback +# name must start or end with one of those strings. +callbacks=cb_,_cb + +# List of qualified module names which can have objects that can redefine +# builtins. +redefining-builtins-modules=six,six.moves,past.builtins,future.builtins,functools + + +[LOGGING] + +# Logging modules to check that the string format arguments are in logging +# function parameter format +logging-modules=logging,absl.logging,tensorflow.io.logging + + +[SIMILARITIES] + +# Minimum lines number of a similarity. +min-similarity-lines=4 + +# Ignore comments when computing similarities. +ignore-comments=yes + +# Ignore docstrings when computing similarities. +ignore-docstrings=yes + +# Ignore imports when computing similarities. +ignore-imports=no + + +[SPELLING] + +# Spelling dictionary name. Available dictionaries: none. To make it working +# install python-enchant package. +spelling-dict= + +# List of comma separated words that should not be checked. +spelling-ignore-words= + +# A path to a file that contains private dictionary; one word per line. +spelling-private-dict-file= + +# Tells whether to store unknown words to indicated private dictionary in +# --spelling-private-dict-file option instead of raising a message. +spelling-store-unknown-words=no + + +[IMPORTS] + +# Deprecated modules which should not be used, separated by a comma +deprecated-modules=regsub, + TERMIOS, + Bastion, + rexec, + sets + +# Create a graph of every (i.e. internal and external) dependencies in the +# given file (report RP0402 must not be disabled) +import-graph= + +# Create a graph of external dependencies in the given file (report RP0402 must +# not be disabled) +ext-import-graph= + +# Create a graph of internal dependencies in the given file (report RP0402 must +# not be disabled) +int-import-graph= + +# Force import order to recognize a module as part of the standard +# compatibility libraries. +known-standard-library= + +# Force import order to recognize a module as part of a third party library. +known-third-party=enchant, absl + +# Analyse import fallback blocks. This can be used to support both Python 2 and +# 3 compatible code, which means that the block might have code that exists +# only in one or another interpreter, leading to false positives when analysed. +analyse-fallback-blocks=no + + +[CLASSES] + +# List of method names used to declare (i.e. assign) instance attributes. +defining-attr-methods=__init__, + __new__, + setUp + +# List of member names, which should be excluded from the protected access +# warning. +exclude-protected=_asdict, + _fields, + _replace, + _source, + _make + +# List of valid names for the first argument in a class method. +valid-classmethod-first-arg=cls, + class_ + +# List of valid names for the first argument in a metaclass class method. +valid-metaclass-classmethod-first-arg=mcs diff --git a/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/backend/pyproject.toml b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/backend/pyproject.toml new file mode 100644 index 0000000..7ced637 --- /dev/null +++ b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/backend/pyproject.toml @@ -0,0 +1,5 @@ +[tool.coverage.run] +source = ["livecov"] + +[tool.pytest.ini_options] +addopts = "--cov --cov-report=lcov:lcov.info --cov-report=term" diff --git a/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/backend/pytest.ini b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/backend/pytest.ini new file mode 100644 index 0000000..2a3f054 --- /dev/null +++ b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/backend/pytest.ini @@ -0,0 +1,12 @@ +[pytest] +minversion = 7.0 + +# Directories to look for the test files +testpaths = tests + +# Directories to look for the source code +pythonpath = . + +python_files = test_*.py *_tests.py + +log_cli_level = DEBUG diff --git a/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/backend/requirements.txt b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/backend/requirements.txt new file mode 100644 index 0000000..73ac724 --- /dev/null +++ b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/backend/requirements.txt @@ -0,0 +1,26 @@ +gunicorn==20.1.0; python_version > '3.0' +gunicorn==19.10.0; python_version < '3.0' +uvicorn~=0.17.0 +fastapi~=0.111.1 +fastapi-utilities==0.2.0 +cloudpickle==2.2.1 +grpcio==1.66.2 +urllib3==1.26.16 +typing-inspect==0.9.0 +typing_extensions==4.12.2 +pydantic==2.9.2 +requests==2.31.0 +Pillow==11.2.1 + +google-cloud-aiplatform==1.69.0 +google-cloud-bigquery==3.26.0 +google-cloud-tasks==2.16.5 +google-cloud-logging==3.11.2 +google-cloud-speech==2.27.0 +google-cloud-discoveryengine==0.13.4 +google-genai==0.8.0 +pylint +black +pytest +pytest-cov +pytest-watch diff --git a/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/backend/src/__init__.py b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/backend/src/__init__.py new file mode 100644 index 0000000..36a1e8d --- /dev/null +++ b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/backend/src/__init__.py @@ -0,0 +1,14 @@ +# Copyright 2025 Google LLC +# +# 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 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + diff --git a/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/backend/src/controller/__init__.py b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/backend/src/controller/__init__.py new file mode 100644 index 0000000..36a1e8d --- /dev/null +++ b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/backend/src/controller/__init__.py @@ -0,0 +1,14 @@ +# Copyright 2025 Google LLC +# +# 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 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + diff --git a/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/backend/src/controller/search.py b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/backend/src/controller/search.py new file mode 100644 index 0000000..98a2337 --- /dev/null +++ b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/backend/src/controller/search.py @@ -0,0 +1,93 @@ +# Copyright 2025 Google LLC +# +# 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 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from typing import Annotated, Optional +from fastapi import APIRouter, HTTPException, status as Status + +from src.model.search import ( + CreateSearchRequest, + GenerationModelOptionalLiteral, + SearchResponse, +) +from src.service.search import ImagenSearchService +from fastapi import Form, File, UploadFile + +router = APIRouter( + prefix="/api/search", + tags=["searches"], + responses={404: {"description": "Not found"}}, +) + +ALLOWED_IMAGE_TYPES = ["image/jpg", "image/jpeg", "image/png", "image/webp"] + + +@router.post("", response_model=SearchResponse) +async def search( + userImage: Annotated[UploadFile, File()], + term: Annotated[Optional[str], Form(min_length=10, max_length=200)], + generationModel: Annotated[ + Optional[GenerationModelOptionalLiteral], + Form(description="Model used for image edition"), + ], + numberOfImages: Annotated[ + Optional[int], + Form(ge=1, le=4, description="Number of images to generate"), + ], + maskDistilation: Annotated[ + Optional[float], + Form( + ge=0, + le=1, + description="Dilation percentage of the mask provided. Float between 0 and 1.", + ), + ], +): + try: + if userImage.content_type not in ALLOWED_IMAGE_TYPES: + raise HTTPException( + status_code=Status.HTTP_400_BAD_REQUEST, + detail=f"Invalid image type. Allowed types are: {', '.join(ALLOWED_IMAGE_TYPES)}", + ) + createSearchRequest = CreateSearchRequest.model_validate( + { + "term": term, + "generation_model": generationModel, + "number_of_images": numberOfImages, + "user_image": userImage.file.read(), + "user_image_mime_type": userImage.content_type, + "mask_distilation": maskDistilation, + } + ) + + service = ImagenSearchService() + return await service.generate_images(createSearchRequest) + except HTTPException as http_exception: + raise HTTPException( + status_code=Status.HTTP_400_BAD_REQUEST, + detail=str(http_exception), + ) + except ValueError as value_error: + raise HTTPException( + status_code=Status.HTTP_400_BAD_REQUEST, + detail=str(value_error), + ) + except Exception as e: + raise HTTPException( + status_code=( + e.code + if hasattr(e, "code") + else Status.HTTP_500_INTERNAL_SERVER_ERROR + ), + detail=str(e.message) if hasattr(e, "message") else str(e), + ) diff --git a/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/backend/src/model/__init__.py b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/backend/src/model/__init__.py new file mode 100644 index 0000000..36a1e8d --- /dev/null +++ b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/backend/src/model/__init__.py @@ -0,0 +1,14 @@ +# Copyright 2025 Google LLC +# +# 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 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + diff --git a/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/backend/src/model/http_status.py b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/backend/src/model/http_status.py new file mode 100644 index 0000000..ca1c1c3 --- /dev/null +++ b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/backend/src/model/http_status.py @@ -0,0 +1,25 @@ +# Copyright 2025 Google LLC +# +# 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 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from fastapi import HTTPException + + +class ResourceAlreadyExists(HTTPException): + def __init__(self, detail="Resource already exists"): + super().__init__(status_code=400, detail=detail) + + +class BadRequest(HTTPException): + def __init__(self, detail="Resource already exists"): + super().__init__(status_code=400, detail=detail) diff --git a/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/backend/src/model/search.py b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/backend/src/model/search.py new file mode 100644 index 0000000..c1dc417 --- /dev/null +++ b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/backend/src/model/search.py @@ -0,0 +1,67 @@ +# Copyright 2025 Google LLC +# +# 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 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from typing import List, Literal, Optional, Union + +from pydantic import BaseModel, ConfigDict, Field +from pydantic.alias_generators import to_camel + + +# Create a Literal type from the list of valid models +GenerationModelOptionalLiteral = Union[ + Literal["imagen-3.0-capability-001"], Literal["imagegeneration@006"] +] + + +class CreateSearchRequest(BaseModel): + term: str = Field(description="Prompt term to be passed to the model") + generation_model: Optional[GenerationModelOptionalLiteral] = Field( + default="imagen-3.0-capability-001", + description="Model used for image edition", + ) + number_of_images: Optional[int] = Field( + 4, description="Number of images to generate" + ) + user_image: bytes + user_image_mime_type: str + mask_distilation: Optional[float] = Field( + 0.005, + description="Dilation percentage of the mask provided. Float between 0 and 1.", + ) + + +class BaseSchema(BaseModel): + model_config = ConfigDict( + alias_generator=to_camel, + populate_by_name=True, + from_attributes=True, + ) + + +class CustomImageResult(BaseSchema): + gcs_uri: Optional[str] + mime_type: str + encoded_image: str + + +class ImageGenerationResult(BaseSchema): + enhanced_prompt: Optional[str] + rai_filtered_reason: Optional[str] + image: CustomImageResult + + +class SearchResponse(BaseSchema): + gemini_results: List[ImageGenerationResult] + imagen_entire_img_results: List[ImageGenerationResult] + imagen_background_img_results: List[ImageGenerationResult] diff --git a/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/backend/src/service/__init__.py b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/backend/src/service/__init__.py new file mode 100644 index 0000000..36a1e8d --- /dev/null +++ b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/backend/src/service/__init__.py @@ -0,0 +1,14 @@ +# Copyright 2025 Google LLC +# +# 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 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + diff --git a/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/backend/src/service/search.py b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/backend/src/service/search.py new file mode 100644 index 0000000..3ed2ca6 --- /dev/null +++ b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/backend/src/service/search.py @@ -0,0 +1,380 @@ +# Copyright 2025 Google LLC +# +# 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 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import asyncio # Added for parallel execution +import base64 +from typing import List, Optional +from io import BytesIO + +import google.auth +from google import genai +from google.genai import types +from google.genai.types import ( + RawReferenceImage, + EditImageConfig, + MaskReferenceImage, + MaskReferenceConfig, + Image as GenaiImage, # Keep this alias for Imagen 3 needs this + Blob as GenaiBlob, # Added for type checking in Gemini response +) +from PIL import Image as PilImage # Import PIL Image and alias, Gemini uses it + + +from src.model.search import ( + CreateSearchRequest, + CustomImageResult, + ImageGenerationResult, + SearchResponse, +) + + +class ImagenSearchService: + # Helper function for Imagen3 edit_image (entire image) + @staticmethod + async def _generate_imagen_entire_image_task( + client: genai.Client, + model_name: str, # Changed from 'model' to 'model_name' for clarity + prompt: str, + raw_reference_image: RawReferenceImage, + number_of_images: int, + ) -> types.EditImageResponse: + def blocking_call() -> types.EditImageResponse: + return client.models.edit_image( + model=model_name, + prompt=prompt, + reference_images=[raw_reference_image], + config=EditImageConfig( + edit_mode="EDIT_MODE_DEFAULT", + number_of_images=number_of_images, + safety_filter_level="BLOCK_MEDIUM_AND_ABOVE", + person_generation="DONT_ALLOW", + ), + ) + + return await asyncio.to_thread(blocking_call) + + # Helper function for Imagen3 edit_image (background swap) + @staticmethod + async def _generate_imagen_background_task( + client: genai.Client, + model_name: str, # Changed from 'model' to 'model_name' for clarity + prompt: str, + raw_reference_image: RawReferenceImage, + mask_ref_image: MaskReferenceImage, + number_of_images: int, + ) -> types.EditImageResponse: + def blocking_call() -> types.EditImageResponse: + return client.models.edit_image( + model=model_name, + prompt=prompt, + reference_images=[raw_reference_image, mask_ref_image], + config=EditImageConfig( + edit_mode="EDIT_MODE_BGSWAP", + number_of_images=number_of_images, + safety_filter_level="BLOCK_ONLY_HIGH", + person_generation="DONT_ALLOW", + ), + ) + + return await asyncio.to_thread(blocking_call) + + @staticmethod + async def _generate_single_gemini_image_task( + client: genai.Client, + gemini_model_name: str, + prompt: str, + user_image_bytes: bytes, # Changed to accept bytes + ) -> ImageGenerationResult: + try: + # Create PIL Image object inside the task + pil_image_obj = PilImage.open(BytesIO(user_image_bytes)) + except Exception as e: + print(f"Error opening image for Gemini task: {e}") + return ImageGenerationResult( + enhanced_prompt=prompt, + rai_filtered_reason=f"Image Processing Error: {str(e)}", + image=None, + ) + + def blocking_call() -> types.GenerateContentResponse: + return client.models.generate_content( + model=gemini_model_name, + contents=[prompt, pil_image_obj], + config=types.GenerateContentConfig( + response_modalities=["TEXT", "IMAGE"] + ), + ) + + try: + gemini_response_object = await asyncio.to_thread(blocking_call) + + if ( + gemini_response_object.prompt_feedback + and gemini_response_object.prompt_feedback.blocked + ): + print( + f"Gemini prompt blocked: {gemini_response_object.prompt_feedback.block_reason}" + ) + return ImageGenerationResult( + enhanced_prompt=prompt, + rai_filtered_reason=str( + gemini_response_object.prompt_feedback.block_reason.name + ), + image=None, + ) + + if not gemini_response_object.candidates: + print("DEBUG: Gemini response had no candidates.") + return ImageGenerationResult( + enhanced_prompt=prompt, + rai_filtered_reason="No candidates in response", + image=None, + ) + + for candidate in gemini_response_object.candidates: + if not (candidate.content and candidate.content.parts): + print( + "DEBUG: Gemini response candidate had no content parts." + ) + continue + + image_part_data: Optional[GenaiBlob] = None + enhanced_prompt_text = prompt + + text_parts = [ + p.text + for p in candidate.content.parts + if p.text is not None + ] + if text_parts: + enhanced_prompt_text = text_parts[0] + + for part in candidate.content.parts: + if ( + part.inline_data + and isinstance(part.inline_data, GenaiBlob) + and part.inline_data.mime_type.startswith("image/") + ): + image_part_data = part.inline_data + break + + if image_part_data: + mime_type = image_part_data.mime_type + image_bytes_data = image_part_data.data + encoded_image = base64.b64encode(image_bytes_data).decode( + "utf-8" + ) + + return ImageGenerationResult( + enhanced_prompt=enhanced_prompt_text, + rai_filtered_reason=( + str(candidate.finish_reason.name) + if candidate.finish_reason + else None + ), + image=CustomImageResult( + gcs_uri=None, + mime_type=mime_type, + encoded_image=encoded_image, + ), + ) + else: + print( + f"DEBUG: Gemini candidate did not yield a usable image. Finish reason: {candidate.finish_reason}" + ) + + return ImageGenerationResult( + enhanced_prompt=prompt, + rai_filtered_reason="No image data in response candidates", + image=None, + ) + + except Exception as e: + # Catch specific exceptions from the SDK if possible, or broader ones + print( + f"Error during Gemini API call for prompt '{prompt[:50]}...': {e}" + ) + return ImageGenerationResult( + enhanced_prompt=prompt, + rai_filtered_reason=f"API Error: {str(e)}", + image=None, + ) + + async def generate_images( + self, searchRequest: CreateSearchRequest + ) -> SearchResponse: + _, PROJECT_ID = google.auth.default() + LOCATION = "us-central1" + client = genai.Client( + vertexai=True, project=PROJECT_ID, location=LOCATION + ) + + prompt = f"""{searchRequest.term}""" + + # Validate user_image bytes early, but PIL object creation will be per-task for Gemini + try: + # Quick check if image is somewhat valid, without keeping it open + PilImage.open(BytesIO(searchRequest.user_image)).close() + except Exception as e: + print(f"ERROR: User image bytes are invalid: {e}") + raise ValueError(f"Invalid user image file: {e}") + + original_image_genai = GenaiImage(image_bytes=searchRequest.user_image) + # Use string IDs for reference_id as expected by the API + raw_reference_image = RawReferenceImage( + reference_image=original_image_genai, + reference_id=0, + ) + + mask_ref_image_for_bg_swap = MaskReferenceImage( + reference_id=1, + reference_image=None, + config=MaskReferenceConfig( + mask_mode="MASK_MODE_BACKGROUND", + segmentation_classes=[1], + mask_dilation=searchRequest.mask_distilation, + ), + ) + + imagen_model_to_use = searchRequest.generation_model + num_images_to_generate = searchRequest.number_of_images + + imagen_entire_task = self._generate_imagen_entire_image_task( + client, + imagen_model_to_use, + prompt, + raw_reference_image, + num_images_to_generate, + ) + + imagen_background_task = self._generate_imagen_background_task( + client, + imagen_model_to_use, + prompt, + raw_reference_image, + mask_ref_image_for_bg_swap, + num_images_to_generate, + ) + + gemini_model_name = "gemini-2.0-flash-preview-image-generation" + gemini_tasks = [ + self._generate_single_gemini_image_task( + client, gemini_model_name, prompt, searchRequest.user_image + ) + for _ in range(num_images_to_generate) + ] + + async def gather_gemini_results_async(): + results = await asyncio.gather( + *gemini_tasks, return_exceptions=True + ) + processed_results = [] + for i, res in enumerate(results): + if isinstance(res, ImageGenerationResult): + processed_results.append(res) + elif isinstance(res, Exception): + print(f"Gemini task {i+1} failed: {res}") + processed_results.append( + ImageGenerationResult( + enhanced_prompt=prompt, + rai_filtered_reason=f"Gemini Task Error: {str(res)}", + image=None, + ) + ) + else: + print( + f"Gemini task {i+1} returned unexpected type: {type(res)}" + ) + processed_results.append( + ImageGenerationResult( + enhanced_prompt=prompt, + rai_filtered_reason="Gemini Task Internal Error: Unexpected Result Type", + image=None, + ) + ) + return processed_results + + all_task_results = await asyncio.gather( + imagen_entire_task, + imagen_background_task, + gather_gemini_results_async(), + return_exceptions=True, + ) + + response_images_entire_image: List[ImageGenerationResult] = [] + if isinstance(all_task_results[0], types.EditImageResponse): + images_entire_raw = all_task_results[0] + if images_entire_raw.generated_images: + response_images_entire_image = [ + ImageGenerationResult( + enhanced_prompt=img.enhanced_prompt, + rai_filtered_reason=img.rai_filtered_reason, + image=CustomImageResult( + gcs_uri=img.image.gcs_uri, + encoded_image=base64.b64encode( + img.image.image_bytes + ).decode("utf-8"), + mime_type=img.image.mime_type, + ), + ) + for img in images_entire_raw.generated_images + ] + elif isinstance(all_task_results[0], Exception): + print( + f"Imagen entire image generation failed: {all_task_results[0]}" + ) + + response_images_just_background: List[ImageGenerationResult] = [] + if isinstance(all_task_results[1], types.EditImageResponse): + images_background_raw = all_task_results[1] + if images_background_raw.generated_images: + response_images_just_background = [ + ImageGenerationResult( + enhanced_prompt=img.enhanced_prompt, + rai_filtered_reason=img.rai_filtered_reason, + image=CustomImageResult( + gcs_uri=img.image.gcs_uri, + encoded_image=base64.b64encode( + img.image.image_bytes + ).decode("utf-8"), + mime_type=img.image.mime_type, + ), + ) + for img in images_background_raw.generated_images + ] + elif isinstance(all_task_results[1], Exception): + print( + f"Imagen background swap generation failed: {all_task_results[1]}" + ) + + gemini_generated_results_list: List[ImageGenerationResult] = [] + if isinstance(all_task_results[2], list): + gemini_generated_results_list = all_task_results[2] + elif isinstance(all_task_results[2], Exception): + print( + f"Gathering Gemini results task itself failed: {all_task_results[2]}" + ) + + valid_gemini_results = [ + res + for res in gemini_generated_results_list + if res.image is not None + ] + response_gemini_results = valid_gemini_results[:num_images_to_generate] + + return SearchResponse( + gemini_results=response_gemini_results, + imagen_entire_img_results=response_images_entire_image, + imagen_background_img_results=response_images_just_background, + ) diff --git a/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/backend/tests/test_search.py b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/backend/tests/test_search.py new file mode 100644 index 0000000..37399e1 --- /dev/null +++ b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/backend/tests/test_search.py @@ -0,0 +1,165 @@ +# Copyright 2025 Google LLC +# +# 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 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Tests for the search controller and service.""" + +import base64 +from io import BytesIO +from unittest.mock import MagicMock + +import pytest +from fastapi.testclient import TestClient +from google.genai import types + +from src.controller.search import router +from src.model.search import ( + CreateSearchRequest, + ImageGenerationResult, + CustomImageResult, +) +from src.service.search import ImagenSearchService + +# Create a test client for the FastAPI app +client = TestClient(router) + + +@pytest.fixture(scope="function", name="mock_genai_client") +def fixture_mock_genai_client(): + """Provides a mock google.genai Client.""" + mock_client = MagicMock() + mock_response = types.EditImageResponse() + + # Create mock generated images with base64 encoded placeholder image data + mock_image_data = types.Image( + gcs_uri="gs://mock_bucket/mock_image.png", + image_bytes=b"mock_image_bytes", # Must be bytes + mime_type="image/png", + ) + mock_generated_image = types.GeneratedImage( + enhanced_prompt="Mock enhanced prompt", + rai_filtered_reason=None, + image=mock_image_data, + ) + mock_response.generated_images = [ + mock_generated_image, + mock_generated_image, + mock_generated_image, + mock_generated_image, + ] + + mock_client.models.edit_image.return_value = mock_response + return mock_client + + +@pytest.fixture(scope="function", name="mock_imagen_search_service") +def fixture_mock_imagen_search_service(mock_genai_client): + """Provides a mock ImagenSearchService with a mock genai client.""" + service = ImagenSearchService() + service.client = mock_genai_client # Inject the mock client + return service + + +class TestSearchController: + """Tests for the /api/search endpoint.""" + + def test_search_endpoint(self, monkeypatch, mock_imagen_search_service): + # Mock the ImagenSearchService to avoid actual API calls + # Mock the google.auth.default to avoid authentication issues + with monkeypatch.context() as m: # use a context for clarity + mock_client_class = MagicMock( + return_value=mock_imagen_search_service.client + ) + m.setattr( + "src.controller.search.ImagenSearchService", + lambda: mock_imagen_search_service, + ) + m.setattr( + "src.service.search.google.auth.default", + lambda: (None, "test_project_id"), + ) + m.setattr( + "src.service.search.google.genai.Client", mock_client_class + ) + + search_term = "a cute cat wearing a hat" + image_content = b"fake_png_bytes_for_testing" + user_image = ("test_image.png", BytesIO(image_content), "image/png") + response = client.post( + "/api/search", + data={ + "term": search_term, + "numberOfImages": 4, + "maskDistilation": 0.005, + "generationModel": "imagen-3.0-capability-001", + }, + files={"userImage": user_image}, + ) + + assert response.status_code == 200 + data = response.json() + assert len(data) == 8 + + for image_data in data: + assert image_data["enhancedPrompt"] == "Mock enhanced prompt" + assert ( + image_data["image"]["gcsUri"] + == "gs://mock_bucket/mock_image.png" + ) + assert image_data["image"]["mimeType"] == "image/png" + assert image_data["image"]["encodedImage"] == base64.b64encode( + b"mock_image_bytes" + ).decode("utf-8") + + +class TestImagenSearchService: + """Tests for the ImagenSearchService class.""" + + def test_imagen_search_service( + self, monkeypatch, mock_imagen_search_service + ): + + # Mock the google.auth.default to avoid authentication issues + with monkeypatch.context() as m: # use a context for clarity + mock_client_class = MagicMock( + return_value=mock_imagen_search_service.client + ) + m.setattr( + "src.service.search.google.auth.default", + lambda: (None, "test_project_id"), + ) + m.setattr( + "src.service.search.google.genai.Client", mock_client_class + ) + + search_request = CreateSearchRequest( + term="a dog playing fetch", + user_image=b"fake_user_image_bytes", + number_of_images=2, + mask_distilation=0.1, + generation_model="imagegeneration@006", + ) + results = mock_imagen_search_service.generate_images(search_request) + + assert isinstance(results, list) + assert len(results) == 8 + assert all( + isinstance(result, ImageGenerationResult) for result in results + ) + mock_client_class.assert_called_once() + + for result in results: + assert isinstance(result.image, CustomImageResult) + assert result.image.encoded_image == base64.b64encode( + b"mock_image_bytes" + ).decode("utf-8") diff --git a/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/docker-compose.yml b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/docker-compose.yml new file mode 100644 index 0000000..86e08fd --- /dev/null +++ b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/docker-compose.yml @@ -0,0 +1,48 @@ +# Copyright 2025 Google LLC +# +# 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 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +services: + backend: + build: + context: ./backend + dockerfile: Dockerfile + container_name: quickbot-image-background-changer-backend + ports: + - "8080:8080" + volumes: + - ./backend:/app + # Mount the gcloud ADC directory. Replace with the correct path for your OS if different. :ro - read-only for better security + - ~/.config/gcloud/:/root/.config/gcloud:ro # Linux/macOS example: + # - %APPDATA%/gcloud:/root/.config/gcloud:ro # Windows example: + environment: + ENVIRONMENT: development + FRONTEND_URL: http://localhost:4200 + GCLOUD_PROJECT: my-project-id + GOOGLE_APPLICATION_CREDENTIALS: "/root/.config/gcloud/application_default_credentials.json" + restart: unless-stopped + + frontend: + build: + context: ./frontend + dockerfile: Dockerfile + container_name: quickbot-image-background-changer-frontend + ports: + - "4200:8080" + volumes: + - ./frontend:/app + # Use an anonymous volume for node_modules to prevent host node_modules + # from interfering and potentially speed up builds/rebuilds + - /app/node_modules + # To change the env variables in the frontend, change the environment.ts file + restart: unless-stopped \ No newline at end of file diff --git a/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/.dockerignore b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/.dockerignore new file mode 100644 index 0000000..7109f56 --- /dev/null +++ b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/.dockerignore @@ -0,0 +1,4 @@ +.angular +.vscode +dist +node_modules \ No newline at end of file diff --git a/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/.editorconfig b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/.editorconfig new file mode 100644 index 0000000..79fe802 --- /dev/null +++ b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/.editorconfig @@ -0,0 +1,8 @@ +root = true + +[*] +indent_style = space +indent_size = 2 +end_of_line = lf +charset = utf-8 +insert_final_newline = true diff --git a/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/.eslintignore b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/.eslintignore new file mode 100644 index 0000000..edd9d60 --- /dev/null +++ b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/.eslintignore @@ -0,0 +1,2 @@ +build/ +dist/ diff --git a/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/.eslintrc.json b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/.eslintrc.json new file mode 100644 index 0000000..f95bb33 --- /dev/null +++ b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/.eslintrc.json @@ -0,0 +1,3 @@ +{ + "extends": "./node_modules/gts/" +} diff --git a/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/.gitignore b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/.gitignore new file mode 100644 index 0000000..0711527 --- /dev/null +++ b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/.gitignore @@ -0,0 +1,42 @@ +# See http://help.github.com/ignore-files/ for more about ignoring files. + +# Compiled output +/dist +/tmp +/out-tsc +/bazel-out + +# Node +/node_modules +npm-debug.log +yarn-error.log + +# IDEs and editors +.idea/ +.project +.classpath +.c9/ +*.launch +.settings/ +*.sublime-workspace + +# Visual Studio Code +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +.history/* + +# Miscellaneous +/.angular/cache +.sass-cache/ +/connect.lock +/coverage +/libpeerconnection.log +testem.log +/typings + +# System files +.DS_Store +Thumbs.db diff --git a/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/.prettierrc.js b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/.prettierrc.js new file mode 100644 index 0000000..535df79 --- /dev/null +++ b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/.prettierrc.js @@ -0,0 +1,33 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +module.exports = { + ...require('gts/.prettierrc.json'), + "overrides": [ + { + "files": "*.html", + "options": { + "parser": "angular" + } + }, + { + "files": "*.scss", + "options": { + "parser": "scss" + } + } + ] +} diff --git a/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/Dockerfile b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/Dockerfile new file mode 100644 index 0000000..e2b6e1c --- /dev/null +++ b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/Dockerfile @@ -0,0 +1,27 @@ +# Copyright 2025 Google LLC +# +# 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 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +FROM node:18.17.1-alpine AS builder + +WORKDIR /app +COPY . /app/ + +RUN npm ci +RUN npm run build:prod + +FROM nginx:alpine +COPY --from=builder /app/dist/quick-bot-app-frontend /usr/share/nginx/html +COPY --from=builder /app/nginx.conf /etc/nginx/conf.d/ + +EXPOSE 8080 \ No newline at end of file diff --git a/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/README.md b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/README.md new file mode 100644 index 0000000..f169230 --- /dev/null +++ b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/README.md @@ -0,0 +1,94 @@ +# QuickBot App Frontend | image-background-changer-using-imagen3 + +[![linting: pylint](https://img.shields.io/badge/linting-pylint-yellowgreen)](https://github.com/pylint-dev/pylint) +[![Code Style: Google](https://img.shields.io/badge/code%20style-google-blueviolet.svg)](https://github.com/google/gts) + +QuickBot App is a set of templates that can be deployed out of the box into Cloud Run and work independently. Each one can be run independently connected to the user default google cloud auth credentials, and based on the complexity of each template, may require to deploy more or less resources into our Google Cloud Project. +The architecture always follows the following structure: a folder for the frontend which consists in an Angular app, and a backend folder which consists of a FastAPI Python app. + +This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 15.1.3. + +## Development server + +Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The application will automatically reload if you change any of the source files. + +## Code scaffolding + +Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`. + +## Build + +Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. + +## Running unit tests + +Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io). + +## Running end-to-end tests + +Run `ng e2e` to execute the end-to-end tests via a platform of your choice. To use this command, you need to first add a package that implements end-to-end testing capabilities. + +## Further help + +To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.io/cli) page. + + +## Code Styling & Commit Guidelines + +To maintain code quality and consistency: + +* **TypeScript (Frontend):** We follow [Angular Coding Style Guide](https://angular.dev/style-guide) by leveraging the use of [Google's TypeScript Style Guide](https://github.com/google/gts) using `gts`. This includes a formatter, linter, and automatic code fixer. +* **Python (Backend):** We adhere to the [Google Python Style Guide](https://google.github.io/styleguide/pyguide.html), using tools like `pylint` and `black` for linting and formatting. +* **Commit Messages:** We suggest following [Angular's Commit Message Guidelines](https://github.com/angular/angular/blob/main/contributing-docs/commit-message-guidelines.md) to create clear and descriptive commit messages. + +#### Frontend (TypeScript with `gts`) + +1. **Initialize `gts` (if not already done in the project):** + Navigate to the `frontend/` directory and run: + ```bash + npx gts init + ``` + This will set up `gts` and create necessary configuration files (like `tsconfig.json`). Ensure your `tsconfig.json` (or a related gts config file like `.gtsrc`) includes an extension for `gts` defaults, typically: + ```json + { + "extends": "./node_modules/gts/tsconfig-google.json", + // ... other configurations + } + ``` +2. **Check for linting issues:** + ```bash + npm run lint + ``` + (This assumes a `lint` script is defined in `package.json`, e.g., `"lint": "gts lint"`) +3. **Fix linting issues automatically (where possible):** + ```bash + npm run fix + ``` + (This assumes a `fix` script is defined in `package.json`, e.g., `"fix": "gts fix"`) + +#### Backend (Python with `pylint` and `black`) + +1. **Ensure Dependencies are Installed:** + Add `pylint` and `black` to your `backend/requirements.txt` file: + ``` + pylint + black + ``` + Then install them within your virtual environment: + ```bash + pip install pylint black + # or pip install -r requirements.txt + ``` +2. **Configure `pylint`:** + It's recommended to have a `.pylintrc` file in your `backend/` directory to configure `pylint` rules. You might need to copy a standard one or generate one (`pylint --generate-rcfile > .pylintrc`). +3. **Check for linting issues with `pylint`:** + Navigate to the `backend/` directory and run: + ```bash + pylint . + ``` + (Or specify modules/packages: `pylint your_module_name`) +4. **Format code with `black`:** + To automatically format all Python files in the current directory and subdirectories: + ```bash + python -m black . --line-length=80 + ``` diff --git a/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/angular.json b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/angular.json new file mode 100644 index 0000000..15d3e38 --- /dev/null +++ b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/angular.json @@ -0,0 +1,123 @@ +{ + "$schema": "./node_modules/@angular/cli/lib/config/schema.json", + "version": 1, + "newProjectRoot": "projects", + "projects": { + "quick-bot-app-frontend": { + "projectType": "application", + "schematics": { + "@schematics/angular:component": { + "style": "scss" + } + }, + "root": "", + "sourceRoot": "src", + "prefix": "app", + "architect": { + "build": { + "builder": "@angular-devkit/build-angular:browser", + "options": { + "outputPath": "dist/quick-bot-app-frontend", + "index": "src/index.html", + "main": "src/main.ts", + "polyfills": [ + "zone.js" + ], + "tsConfig": "tsconfig.app.json", + "inlineStyleLanguage": "scss", + "assets": [ + "src/favicon.ico", + "src/assets" + ], + "styles": [ + "@angular/material/prebuilt-themes/indigo-pink.css", + "src/styles.scss", + "node_modules/prismjs/themes/prism-okaidia.css" + ], + "scripts": [ + "node_modules/marked/marked.min.js", + "node_modules/prismjs/prism.js", + "node_modules/prismjs/components/prism-csharp.min.js", + "node_modules/prismjs/components/prism-css.min.js", + "node_modules/clipboard/dist/clipboard.min.js" + ] + }, + "configurations": { + "production": { + "budgets": [ + { + "type": "initial", + "maximumWarning": "4mb", + "maximumError": "10mb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "20kb", + "maximumError": "50kb" + } + ], + "outputHashing": "all" + }, + "development": { + "buildOptimizer": false, + "optimization": false, + "vendorChunk": true, + "extractLicenses": false, + "sourceMap": true, + "namedChunks": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "builder": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "browserTarget": "quick-bot-app-frontend:build:production" + }, + "development": { + "browserTarget": "quick-bot-app-frontend:build:development" + } + }, + "defaultConfiguration": "development" + }, + "extract-i18n": { + "builder": "@angular-devkit/build-angular:extract-i18n", + "options": { + "browserTarget": "quick-bot-app-frontend:build" + } + }, + "test": { + "builder": "@angular-devkit/build-angular:karma", + "options": { + "polyfills": [ + "zone.js", + "zone.js/testing" + ], + "tsConfig": "tsconfig.spec.json", + "inlineStyleLanguage": "scss", + "assets": [ + "src/favicon.ico", + "src/assets" + ], + "styles": [ + "@angular/material/prebuilt-themes/indigo-pink.css", + "src/styles.scss", + "node_modules/prismjs/themes/prism-okaidia.css" + ], + "scripts": [ + "node_modules/marked/marked.min.js", + "node_modules/prismjs/prism.js", + "node_modules/prismjs/components/prism-csharp.min.js", + "node_modules/prismjs/components/prism-css.min.js", + "node_modules/clipboard/dist/clipboard.min.js" + ] + } + } + } + } + }, + "cli": { + "analytics": false + } +} diff --git a/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/nginx.conf b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/nginx.conf new file mode 100644 index 0000000..6c8cce4 --- /dev/null +++ b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/nginx.conf @@ -0,0 +1,11 @@ +server { + listen 8080; + + root /usr/share/nginx/html; + index index.html index.htm; + include /etc/nginx/mime.types; + + location / { + try_files $uri $uri/ /index.html; + } +} \ No newline at end of file diff --git a/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/package-lock.json b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/package-lock.json new file mode 100644 index 0000000..97e2cd3 --- /dev/null +++ b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/package-lock.json @@ -0,0 +1,25849 @@ +{ + "name": "quick-bot-app-frontend", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "quick-bot-app-frontend", + "version": "0.0.0", + "hasInstallScript": true, + "license": "ISC", + "dependencies": { + "@angular/animations": "^15.1.0", + "@angular/cdk": "^15.2.9", + "@angular/common": "^15.1.0", + "@angular/compiler": "^15.1.0", + "@angular/core": "^15.1.0", + "@angular/fire": "7.5", + "@angular/flex-layout": "^15.0.0-beta.42", + "@angular/forms": "^15.1.0", + "@angular/material": "^15.2.9", + "@angular/platform-browser": "^15.1.0", + "@angular/platform-browser-dynamic": "^15.1.0", + "@angular/router": "^15.1.0", + "@fortawesome/fontawesome-free": "^6.6.0", + "@ionic/angular": "^7.5.4", + "@ng-idle/core": "^14.0.0", + "clipboard": "^2.0.11", + "ionic-emoji-rating": "^1.0.15", + "marked": "^4.3.0", + "moment": "^2.30.1", + "ng2-pdf-viewer": "^7.0.2", + "ngx-markdown": "^15.1.2", + "prismjs": "^1.29.0", + "rxfire": "6.0.3", + "rxjs": "~7.8.0", + "tslib": "^2.3.0", + "uuid": "^9.0.1", + "zone.js": "~0.12.0" + }, + "devDependencies": { + "@angular-devkit/build-angular": "^15.1.3", + "@angular/cli": "~15.1.3", + "@angular/compiler-cli": "^15.1.0", + "@types/jasmine": "~4.3.0", + "@types/marked": "^4.3.0", + "@types/node": "20.12.7", + "@types/uuid": "^9.0.4", + "firebase-tools": "^11.30.0", + "gts": "^5.3.1", + "jasmine-core": "~4.5.0", + "karma": "~6.4.0", + "karma-chrome-launcher": "~3.1.0", + "karma-coverage": "~2.2.0", + "karma-jasmine": "~5.1.0", + "karma-jasmine-html-reporter": "~2.0.0", + "prettier": "^3.5.1", + "tailwindcss": "^3.4.17", + "typescript": "~4.9.4" + } + }, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.0.tgz", + "integrity": "sha512-qRmjj8nj9qmLTQXXmaR1cck3UXSRMPrbsLJAasZpF+t3riI71BXed5ebIOYwQntykeZuhjsdweEc9BxH5Jc26w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jridgewell/gen-mapping": "^0.1.0", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@angular-devkit/architect": { + "version": "0.1502.11", + "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1502.11.tgz", + "integrity": "sha512-+hkG5UjIaKMRdo6SFLNQs+Cv7yAVeN8ijfDwI2z/mp7/otowuSEy+H3Tii195jfJ8TQ+y1B7svnx2D6O7oOYbQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "15.2.11", + "rxjs": "6.6.7" + }, + "engines": { + "node": "^14.20.0 || ^16.13.0 || >=18.10.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@angular-devkit/architect/node_modules/rxjs": { + "version": "6.6.7", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", + "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^1.9.0" + }, + "engines": { + "npm": ">=2.0.0" + } + }, + "node_modules/@angular-devkit/architect/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true, + "license": "0BSD" + }, + "node_modules/@angular-devkit/build-angular": { + "version": "15.2.11", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-angular/-/build-angular-15.2.11.tgz", + "integrity": "sha512-MnpVCJdk5jHuK7CH/cTcRT0JQkkKkRTEV3WTyOUhTm0O3PlKwvTM6/Sner+zyuhKyw5VFBBMypHh59aTUDEZ1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "2.2.0", + "@angular-devkit/architect": "0.1502.11", + "@angular-devkit/build-webpack": "0.1502.11", + "@angular-devkit/core": "15.2.11", + "@babel/core": "7.20.12", + "@babel/generator": "7.20.14", + "@babel/helper-annotate-as-pure": "7.18.6", + "@babel/helper-split-export-declaration": "7.18.6", + "@babel/plugin-proposal-async-generator-functions": "7.20.7", + "@babel/plugin-transform-async-to-generator": "7.20.7", + "@babel/plugin-transform-runtime": "7.19.6", + "@babel/preset-env": "7.20.2", + "@babel/runtime": "7.20.13", + "@babel/template": "7.20.7", + "@discoveryjs/json-ext": "0.5.7", + "@ngtools/webpack": "15.2.11", + "ansi-colors": "4.1.3", + "autoprefixer": "10.4.13", + "babel-loader": "9.1.2", + "babel-plugin-istanbul": "6.1.1", + "browserslist": "4.21.5", + "cacache": "17.0.4", + "chokidar": "3.5.3", + "copy-webpack-plugin": "11.0.0", + "critters": "0.0.16", + "css-loader": "6.7.3", + "esbuild-wasm": "0.17.8", + "glob": "8.1.0", + "https-proxy-agent": "5.0.1", + "inquirer": "8.2.4", + "jsonc-parser": "3.2.0", + "karma-source-map-support": "1.4.0", + "less": "4.1.3", + "less-loader": "11.1.0", + "license-webpack-plugin": "4.0.2", + "loader-utils": "3.2.1", + "magic-string": "0.29.0", + "mini-css-extract-plugin": "2.7.2", + "open": "8.4.1", + "ora": "5.4.1", + "parse5-html-rewriting-stream": "7.0.0", + "piscina": "3.2.0", + "postcss": "8.4.31", + "postcss-loader": "7.0.2", + "resolve-url-loader": "5.0.0", + "rxjs": "6.6.7", + "sass": "1.58.1", + "sass-loader": "13.2.0", + "semver": "7.5.3", + "source-map-loader": "4.0.1", + "source-map-support": "0.5.21", + "terser": "5.16.3", + "text-table": "0.2.0", + "tree-kill": "1.2.2", + "tslib": "2.5.0", + "webpack": "5.76.1", + "webpack-dev-middleware": "6.1.2", + "webpack-dev-server": "4.11.1", + "webpack-merge": "5.8.0", + "webpack-subresource-integrity": "5.1.0" + }, + "engines": { + "node": "^14.20.0 || ^16.13.0 || >=18.10.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "optionalDependencies": { + "esbuild": "0.17.8" + }, + "peerDependencies": { + "@angular/compiler-cli": "^15.0.0", + "@angular/localize": "^15.0.0", + "@angular/platform-server": "^15.0.0", + "@angular/service-worker": "^15.0.0", + "karma": "^6.3.0", + "ng-packagr": "^15.0.0", + "protractor": "^7.0.0", + "tailwindcss": "^2.0.0 || ^3.0.0", + "typescript": ">=4.8.2 <5.0" + }, + "peerDependenciesMeta": { + "@angular/localize": { + "optional": true + }, + "@angular/platform-server": { + "optional": true + }, + "@angular/service-worker": { + "optional": true + }, + "karma": { + "optional": true + }, + "ng-packagr": { + "optional": true + }, + "protractor": { + "optional": true + }, + "tailwindcss": { + "optional": true + } + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/@ngtools/webpack": { + "version": "15.2.11", + "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-15.2.11.tgz", + "integrity": "sha512-yqp+FziuJ+wIVij4eTqfhuiTPNaG1PU8ukeGOdqkVH4nQMlmzs9UldXy1iYC/6swzn6XO/pkqisU3m/jxemMzA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.20.0 || ^16.13.0 || >=18.10.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "@angular/compiler-cli": "^15.0.0", + "typescript": ">=4.8.2 <5.0", + "webpack": "^5.54.0" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/rxjs": { + "version": "6.6.7", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", + "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^1.9.0" + }, + "engines": { + "npm": ">=2.0.0" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/rxjs/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true, + "license": "0BSD" + }, + "node_modules/@angular-devkit/build-angular/node_modules/tslib": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz", + "integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==", + "dev": true, + "license": "0BSD" + }, + "node_modules/@angular-devkit/build-webpack": { + "version": "0.1502.11", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.1502.11.tgz", + "integrity": "sha512-OTONIRp770Jfems4+cULmtoeSzjnpx5UjV2EazojnhRXXBSJMWRMPvwD2QvQl9UO/6eOV3d2mgmP2xOZgc/D6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/architect": "0.1502.11", + "rxjs": "6.6.7" + }, + "engines": { + "node": "^14.20.0 || ^16.13.0 || >=18.10.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "webpack": "^5.30.0", + "webpack-dev-server": "^4.0.0" + } + }, + "node_modules/@angular-devkit/build-webpack/node_modules/rxjs": { + "version": "6.6.7", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", + "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^1.9.0" + }, + "engines": { + "npm": ">=2.0.0" + } + }, + "node_modules/@angular-devkit/build-webpack/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true, + "license": "0BSD" + }, + "node_modules/@angular-devkit/core": { + "version": "15.2.11", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-15.2.11.tgz", + "integrity": "sha512-zd6QelJ8pOPvz6TsehR0JqixjDjzgEOkKywBJBuwNXY+Nw3MJGayJeWS0UgC+Gk+LoTkpI21RoyaYELkAmD/tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "8.12.0", + "ajv-formats": "2.1.1", + "jsonc-parser": "3.2.0", + "rxjs": "6.6.7", + "source-map": "0.7.4" + }, + "engines": { + "node": "^14.20.0 || ^16.13.0 || >=18.10.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "chokidar": "^3.5.2" + }, + "peerDependenciesMeta": { + "chokidar": { + "optional": true + } + } + }, + "node_modules/@angular-devkit/core/node_modules/rxjs": { + "version": "6.6.7", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", + "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^1.9.0" + }, + "engines": { + "npm": ">=2.0.0" + } + }, + "node_modules/@angular-devkit/core/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true, + "license": "0BSD" + }, + "node_modules/@angular-devkit/schematics": { + "version": "15.1.6", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-15.1.6.tgz", + "integrity": "sha512-cwmJFpS43zrdlmfwfHIxG/Nzg5rzFdtKrHx64ZXxNFm6JdyK2JTs/qrHUwv1FYWAcqhdiHn+00jYklMmvsvPOA==", + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "15.1.6", + "jsonc-parser": "3.2.0", + "magic-string": "0.27.0", + "ora": "5.4.1", + "rxjs": "6.6.7" + }, + "engines": { + "node": "^14.20.0 || ^16.13.0 || >=18.10.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@angular-devkit/schematics/node_modules/@angular-devkit/core": { + "version": "15.1.6", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-15.1.6.tgz", + "integrity": "sha512-jGgxyRjecVf6lEyqDxz7ltMEndNPxIg720pk6r40fgsu0dU8w9vjJSJe7k0XdJiXVRcN6wZa/J5nO/xcwWVIsA==", + "license": "MIT", + "dependencies": { + "ajv": "8.12.0", + "ajv-formats": "2.1.1", + "jsonc-parser": "3.2.0", + "rxjs": "6.6.7", + "source-map": "0.7.4" + }, + "engines": { + "node": "^14.20.0 || ^16.13.0 || >=18.10.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "chokidar": "^3.5.2" + }, + "peerDependenciesMeta": { + "chokidar": { + "optional": true + } + } + }, + "node_modules/@angular-devkit/schematics/node_modules/magic-string": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.27.0.tgz", + "integrity": "sha512-8UnnX2PeRAPZuN12svgR9j7M1uWMovg/CEnIwIG0LFkXSJJe4PdfUGiTGl8V9bsBHFUtfVINcSyYxd7q+kx9fA==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.13" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@angular-devkit/schematics/node_modules/rxjs": { + "version": "6.6.7", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", + "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^1.9.0" + }, + "engines": { + "npm": ">=2.0.0" + } + }, + "node_modules/@angular-devkit/schematics/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "license": "0BSD" + }, + "node_modules/@angular/animations": { + "version": "15.2.10", + "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-15.2.10.tgz", + "integrity": "sha512-yxfN8qQpMaukRU5LjFkJBmy85rqrOp86tYVCsf+hmPEFRiXBMUj6xYLeCMcpk3Mt1JtnWGBR34ivGx+7bNeAow==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^14.20.0 || ^16.13.0 || >=18.10.0" + }, + "peerDependencies": { + "@angular/core": "15.2.10" + } + }, + "node_modules/@angular/cdk": { + "version": "15.2.9", + "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-15.2.9.tgz", + "integrity": "sha512-koaM07N1AIQ5oHU27l0/FoQSSoYAwlAYwVZ4Di3bYrJsTBNCN2Xsby7wI8gZxdepMnV4Fe9si382BDBov+oO4Q==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "optionalDependencies": { + "parse5": "^7.1.2" + }, + "peerDependencies": { + "@angular/common": "^15.0.0 || ^16.0.0", + "@angular/core": "^15.0.0 || ^16.0.0", + "rxjs": "^6.5.3 || ^7.4.0" + } + }, + "node_modules/@angular/cli": { + "version": "15.1.6", + "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-15.1.6.tgz", + "integrity": "sha512-GmC9jZK2ipUWj0dlfTI5oEYia4y1fLfO3AtAKU5CylNYrGyB+DRytKY8Bx6Fs4kaNBY8V8YnyLi7E/78gziMdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/architect": "0.1501.6", + "@angular-devkit/core": "15.1.6", + "@angular-devkit/schematics": "15.1.6", + "@schematics/angular": "15.1.6", + "@yarnpkg/lockfile": "1.1.0", + "ansi-colors": "4.1.3", + "ini": "3.0.1", + "inquirer": "8.2.4", + "jsonc-parser": "3.2.0", + "npm-package-arg": "10.1.0", + "npm-pick-manifest": "8.0.1", + "open": "8.4.0", + "ora": "5.4.1", + "pacote": "15.0.8", + "resolve": "1.22.1", + "semver": "7.3.8", + "symbol-observable": "4.0.0", + "yargs": "17.6.2" + }, + "bin": { + "ng": "bin/ng.js" + }, + "engines": { + "node": "^14.20.0 || ^16.13.0 || >=18.10.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@angular/cli/node_modules/@angular-devkit/architect": { + "version": "0.1501.6", + "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1501.6.tgz", + "integrity": "sha512-u07zZFlfrg0Qn4mu5M9Nz0pH2Yd2028XF/73980PsZMxwkSm4diF08v4bHk3UyR7yPT7phwvt4znj6ryZhx1gw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "15.1.6", + "rxjs": "6.6.7" + }, + "engines": { + "node": "^14.20.0 || ^16.13.0 || >=18.10.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@angular/cli/node_modules/@angular-devkit/core": { + "version": "15.1.6", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-15.1.6.tgz", + "integrity": "sha512-jGgxyRjecVf6lEyqDxz7ltMEndNPxIg720pk6r40fgsu0dU8w9vjJSJe7k0XdJiXVRcN6wZa/J5nO/xcwWVIsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "8.12.0", + "ajv-formats": "2.1.1", + "jsonc-parser": "3.2.0", + "rxjs": "6.6.7", + "source-map": "0.7.4" + }, + "engines": { + "node": "^14.20.0 || ^16.13.0 || >=18.10.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "chokidar": "^3.5.2" + }, + "peerDependenciesMeta": { + "chokidar": { + "optional": true + } + } + }, + "node_modules/@angular/cli/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@angular/cli/node_modules/open": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/open/-/open-8.4.0.tgz", + "integrity": "sha512-XgFPPM+B28FtCCgSb9I+s9szOC1vZRSwgWsRUA5ylIxRTgKozqjOCrVOqGsYABPYK5qnfqClxZTFBa8PKt2v6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-lazy-prop": "^2.0.0", + "is-docker": "^2.1.1", + "is-wsl": "^2.2.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@angular/cli/node_modules/rxjs": { + "version": "6.6.7", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", + "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^1.9.0" + }, + "engines": { + "npm": ">=2.0.0" + } + }, + "node_modules/@angular/cli/node_modules/semver": { + "version": "7.3.8", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", + "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", + "dev": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@angular/cli/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true, + "license": "0BSD" + }, + "node_modules/@angular/cli/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/@angular/common": { + "version": "15.2.10", + "resolved": "https://registry.npmjs.org/@angular/common/-/common-15.2.10.tgz", + "integrity": "sha512-jdBn3fctkqoNrJn9VLsUHpcCEhCxWSczdsR+BBbD6T0oLl6vMrAVNjPwfBejnlgfWN1KoRU9kgOYsMxa5apIWQ==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^14.20.0 || ^16.13.0 || >=18.10.0" + }, + "peerDependencies": { + "@angular/core": "15.2.10", + "rxjs": "^6.5.3 || ^7.4.0" + } + }, + "node_modules/@angular/compiler": { + "version": "15.2.10", + "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-15.2.10.tgz", + "integrity": "sha512-M0XkeU0O73UlJZwDvOyp8/apetz9UKj78eTFDseMYJDLcxe6MpkbkxqpsGZnKYDj7LIep8PmCAKEkhtenE82zw==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^14.20.0 || ^16.13.0 || >=18.10.0" + }, + "peerDependencies": { + "@angular/core": "15.2.10" + }, + "peerDependenciesMeta": { + "@angular/core": { + "optional": true + } + } + }, + "node_modules/@angular/compiler-cli": { + "version": "15.2.10", + "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-15.2.10.tgz", + "integrity": "sha512-mCFIxrs60XicKfA2o42hA7LrQvhybi9BQveWuZn/2iIEOXx7R62Iemz8E21pLWftAZHGxEW3NECfBrY1d3gVmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "7.19.3", + "@jridgewell/sourcemap-codec": "^1.4.14", + "chokidar": "^3.0.0", + "convert-source-map": "^1.5.1", + "dependency-graph": "^0.11.0", + "magic-string": "^0.27.0", + "reflect-metadata": "^0.1.2", + "semver": "^7.0.0", + "tslib": "^2.3.0", + "yargs": "^17.2.1" + }, + "bin": { + "ng-xi18n": "bundles/src/bin/ng_xi18n.js", + "ngc": "bundles/src/bin/ngc.js", + "ngcc": "bundles/ngcc/main-ngcc.js" + }, + "engines": { + "node": "^14.20.0 || ^16.13.0 || >=18.10.0" + }, + "peerDependencies": { + "@angular/compiler": "15.2.10", + "typescript": ">=4.8.2 <5.0" + } + }, + "node_modules/@angular/compiler-cli/node_modules/@babel/core": { + "version": "7.19.3", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.19.3.tgz", + "integrity": "sha512-WneDJxdsjEvyKtXKsaBGbDeiyOjR5vYq4HcShxnIbG0qixpoHjI3MqeZM9NDvsojNCEBItQE4juOo/bU6e72gQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.1.0", + "@babel/code-frame": "^7.18.6", + "@babel/generator": "^7.19.3", + "@babel/helper-compilation-targets": "^7.19.3", + "@babel/helper-module-transforms": "^7.19.0", + "@babel/helpers": "^7.19.0", + "@babel/parser": "^7.19.3", + "@babel/template": "^7.18.10", + "@babel/traverse": "^7.19.3", + "@babel/types": "^7.19.3", + "convert-source-map": "^1.7.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.1", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@angular/compiler-cli/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/@angular/compiler-cli/node_modules/magic-string": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.27.0.tgz", + "integrity": "sha512-8UnnX2PeRAPZuN12svgR9j7M1uWMovg/CEnIwIG0LFkXSJJe4PdfUGiTGl8V9bsBHFUtfVINcSyYxd7q+kx9fA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.13" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@angular/core": { + "version": "15.2.10", + "resolved": "https://registry.npmjs.org/@angular/core/-/core-15.2.10.tgz", + "integrity": "sha512-meGGidnitQJGDxYd9/LrqYiVlId+vGaLoiLgJdKBz+o2ZO6OmXQGuNw2VBqf17/Cc0/UjzrOY7+kILNFKkk/WQ==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^14.20.0 || ^16.13.0 || >=18.10.0" + }, + "peerDependencies": { + "rxjs": "^6.5.3 || ^7.4.0", + "zone.js": "~0.11.4 || ~0.12.0 || ~0.13.0" + } + }, + "node_modules/@angular/fire": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/@angular/fire/-/fire-7.5.0.tgz", + "integrity": "sha512-M/JJ5SKnpfNZFLKl0Awjn3r2NhURy2Mx+E8EzSDWSg1sHD7IThrynZYPUSZR+cd3KGsGoEacIP2x8Ra+0a+CVg==", + "license": "MIT", + "dependencies": { + "@angular-devkit/schematics": "^12.0.0 || ^13.0.0 || ^14.0.0 || ^15.0.0", + "@schematics/angular": "^12.0.0 || ^13.0.0 || ^14.0.0 || ^15.0.0", + "file-loader": "^6.2.0", + "firebase": "^9.8.0", + "fs-extra": "^8.0.1", + "fuzzy": "^0.1.3", + "inquirer": "^8.1.1", + "inquirer-autocomplete-prompt": "^1.0.1", + "jsonc-parser": "^3.0.0", + "node-fetch": "^2.6.1", + "open": "^8.0.0", + "ora": "^5.3.0", + "rxfire": "^6.0.0", + "semver": "^7.1.3", + "triple-beam": "^1.3.0", + "tslib": "^2.0.0", + "winston": "^3.0.0" + }, + "peerDependencies": { + "@angular/common": "^12.0.0 || ^13.0.0 || ^14.0.0 || ^15.0.0", + "@angular/core": "^12.0.0 || ^13.0.0 || ^14.0.0 || ^15.0.0", + "@angular/platform-browser": "^12.0.0 || ^13.0.0 || ^14.0.0 || ^15.0.0", + "@angular/platform-browser-dynamic": "^12.0.0 || ^13.0.0 || ^14.0.0 || ^15.0.0", + "firebase-tools": "^9.9.0 || ^10.0.0 || ^11.0.0", + "rxjs": "~6.6.0 || ^7.0.0" + }, + "peerDependenciesMeta": { + "firebase-tools": { + "optional": true + } + } + }, + "node_modules/@angular/flex-layout": { + "version": "15.0.0-beta.42", + "resolved": "https://registry.npmjs.org/@angular/flex-layout/-/flex-layout-15.0.0-beta.42.tgz", + "integrity": "sha512-cTAPVMMxnyIFwpZwdq0PL5mdP9Qh+R8MB7ZBezVaN3Rz2fRrkagzKpLvPX3TFzepXrvHBdpKsU4b8u+NxEC/6g==", + "deprecated": "This package has been deprecated. Please see https://blog.angular.io/modern-css-in-angular-layouts-4a259dca9127", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "peerDependencies": { + "@angular/cdk": ">=15.0.0", + "@angular/common": ">=15.0.2", + "@angular/core": ">=15.0.2", + "@angular/platform-browser": ">=15.0.2", + "rxjs": "^6.5.3 || ^7.4.0" + } + }, + "node_modules/@angular/forms": { + "version": "15.2.10", + "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-15.2.10.tgz", + "integrity": "sha512-NIntGsNcN6o8L1txsbWXOf6f3K/CUBizdKsxsYVYGJIXEW5qU6UnWmfAZffNNXsT/XvbgUCjgDwT0cAwcqZPuQ==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^14.20.0 || ^16.13.0 || >=18.10.0" + }, + "peerDependencies": { + "@angular/common": "15.2.10", + "@angular/core": "15.2.10", + "@angular/platform-browser": "15.2.10", + "rxjs": "^6.5.3 || ^7.4.0" + } + }, + "node_modules/@angular/material": { + "version": "15.2.9", + "resolved": "https://registry.npmjs.org/@angular/material/-/material-15.2.9.tgz", + "integrity": "sha512-emuFF/7+91Jq+6kVCl3FiVoFLtAZoh+woFQWNuK8nhx0HmD4ckLFI8d9a6ERYR3zRuKhq5deSRE2kYsfpjrrsQ==", + "license": "MIT", + "dependencies": { + "@material/animation": "15.0.0-canary.684e33d25.0", + "@material/auto-init": "15.0.0-canary.684e33d25.0", + "@material/banner": "15.0.0-canary.684e33d25.0", + "@material/base": "15.0.0-canary.684e33d25.0", + "@material/button": "15.0.0-canary.684e33d25.0", + "@material/card": "15.0.0-canary.684e33d25.0", + "@material/checkbox": "15.0.0-canary.684e33d25.0", + "@material/chips": "15.0.0-canary.684e33d25.0", + "@material/circular-progress": "15.0.0-canary.684e33d25.0", + "@material/data-table": "15.0.0-canary.684e33d25.0", + "@material/density": "15.0.0-canary.684e33d25.0", + "@material/dialog": "15.0.0-canary.684e33d25.0", + "@material/dom": "15.0.0-canary.684e33d25.0", + "@material/drawer": "15.0.0-canary.684e33d25.0", + "@material/elevation": "15.0.0-canary.684e33d25.0", + "@material/fab": "15.0.0-canary.684e33d25.0", + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "@material/floating-label": "15.0.0-canary.684e33d25.0", + "@material/form-field": "15.0.0-canary.684e33d25.0", + "@material/icon-button": "15.0.0-canary.684e33d25.0", + "@material/image-list": "15.0.0-canary.684e33d25.0", + "@material/layout-grid": "15.0.0-canary.684e33d25.0", + "@material/line-ripple": "15.0.0-canary.684e33d25.0", + "@material/linear-progress": "15.0.0-canary.684e33d25.0", + "@material/list": "15.0.0-canary.684e33d25.0", + "@material/menu": "15.0.0-canary.684e33d25.0", + "@material/menu-surface": "15.0.0-canary.684e33d25.0", + "@material/notched-outline": "15.0.0-canary.684e33d25.0", + "@material/radio": "15.0.0-canary.684e33d25.0", + "@material/ripple": "15.0.0-canary.684e33d25.0", + "@material/rtl": "15.0.0-canary.684e33d25.0", + "@material/segmented-button": "15.0.0-canary.684e33d25.0", + "@material/select": "15.0.0-canary.684e33d25.0", + "@material/shape": "15.0.0-canary.684e33d25.0", + "@material/slider": "15.0.0-canary.684e33d25.0", + "@material/snackbar": "15.0.0-canary.684e33d25.0", + "@material/switch": "15.0.0-canary.684e33d25.0", + "@material/tab": "15.0.0-canary.684e33d25.0", + "@material/tab-bar": "15.0.0-canary.684e33d25.0", + "@material/tab-indicator": "15.0.0-canary.684e33d25.0", + "@material/tab-scroller": "15.0.0-canary.684e33d25.0", + "@material/textfield": "15.0.0-canary.684e33d25.0", + "@material/theme": "15.0.0-canary.684e33d25.0", + "@material/tooltip": "15.0.0-canary.684e33d25.0", + "@material/top-app-bar": "15.0.0-canary.684e33d25.0", + "@material/touch-target": "15.0.0-canary.684e33d25.0", + "@material/typography": "15.0.0-canary.684e33d25.0", + "tslib": "^2.3.0" + }, + "peerDependencies": { + "@angular/animations": "^15.0.0 || ^16.0.0", + "@angular/cdk": "15.2.9", + "@angular/common": "^15.0.0 || ^16.0.0", + "@angular/core": "^15.0.0 || ^16.0.0", + "@angular/forms": "^15.0.0 || ^16.0.0", + "@angular/platform-browser": "^15.0.0 || ^16.0.0", + "rxjs": "^6.5.3 || ^7.4.0" + } + }, + "node_modules/@angular/platform-browser": { + "version": "15.2.10", + "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-15.2.10.tgz", + "integrity": "sha512-9tbgVGSJqwfrOzT8aA/kWBLNhJSQ9gUg0CJxwFBSJm8VkBUJrszoBlDsnSvlxx8/W2ejNULKHFTXeUzq0O/+RQ==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^14.20.0 || ^16.13.0 || >=18.10.0" + }, + "peerDependencies": { + "@angular/animations": "15.2.10", + "@angular/common": "15.2.10", + "@angular/core": "15.2.10" + }, + "peerDependenciesMeta": { + "@angular/animations": { + "optional": true + } + } + }, + "node_modules/@angular/platform-browser-dynamic": { + "version": "15.2.10", + "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-15.2.10.tgz", + "integrity": "sha512-JHP6W+FX715Qv7DhqvfZLuBZXSDJrboiQsR06gUAgDSjAUyhbqmpVg/2YOtgeWpPkzNDtXdPU2PhcRdIv5J3Yg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^14.20.0 || ^16.13.0 || >=18.10.0" + }, + "peerDependencies": { + "@angular/common": "15.2.10", + "@angular/compiler": "15.2.10", + "@angular/core": "15.2.10", + "@angular/platform-browser": "15.2.10" + } + }, + "node_modules/@angular/router": { + "version": "15.2.10", + "resolved": "https://registry.npmjs.org/@angular/router/-/router-15.2.10.tgz", + "integrity": "sha512-LmuqEg0iIXSw7bli6HKJ19cbxP91v37GtRwbGKswyLihqzTgvjBYpvcfMnB5FRQ5LWkTwq5JclkX03dZw290Yg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^14.20.0 || ^16.13.0 || >=18.10.0" + }, + "peerDependencies": { + "@angular/common": "15.2.10", + "@angular/core": "15.2.10", + "@angular/platform-browser": "15.2.10", + "rxjs": "^6.5.3 || ^7.4.0" + } + }, + "node_modules/@assemblyscript/loader": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@assemblyscript/loader/-/loader-0.10.1.tgz", + "integrity": "sha512-H71nDOOL8Y7kWRLqf6Sums+01Q5msqBW2KhDUTemh1tvY04eSkSXrK0uj/4mmY0Xr16/3zyZmsrxN7CKuRbNRg==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/@babel/code-frame": { + "version": "7.26.2", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", + "integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.25.9", + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.26.5", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.26.5.tgz", + "integrity": "sha512-XvcZi1KWf88RVbF9wn8MN6tYFloU5qX8KjuF3E1PVBmJ9eypXfs4GRiJwLuTZL0iSnJUKn1BFPa5BPZZJyFzPg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.20.12", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.20.12.tgz", + "integrity": "sha512-XsMfHovsUYHFMdrIHkZphTN/2Hzzi78R08NuHfDBehym2VsPDL6Zn/JAD/JQdnRvbSsbQc4mVaU1m6JgtTEElg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.1.0", + "@babel/code-frame": "^7.18.6", + "@babel/generator": "^7.20.7", + "@babel/helper-compilation-targets": "^7.20.7", + "@babel/helper-module-transforms": "^7.20.11", + "@babel/helpers": "^7.20.7", + "@babel/parser": "^7.20.7", + "@babel/template": "^7.20.7", + "@babel/traverse": "^7.20.12", + "@babel/types": "^7.20.7", + "convert-source-map": "^1.7.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.2", + "semver": "^6.3.0" + }, + "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.20.14", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.20.14.tgz", + "integrity": "sha512-AEmuXHdcD3A52HHXxaTmYlb8q/xMEhoRP67B3T4Oq7lbmSoqroMZzjnGj3+i1io3pdnF8iBYVu4Ilj+c4hBxYg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.20.7", + "@jridgewell/gen-mapping": "^0.3.2", + "jsesc": "^2.5.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/generator/node_modules/@jridgewell/gen-mapping": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", + "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/helper-annotate-as-pure": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.18.6.tgz", + "integrity": "sha512-duORpUiYrEpzKIop6iNbjnwKLAKnJ47csTyRACyEmWj0QdUrm5aqNJGHSSEQSUAvNW0ojX0dOmK9dZduvkfeXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.26.5", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.26.5.tgz", + "integrity": "sha512-IXuyn5EkouFJscIDuFF5EsiSolseme1s0CZB+QxVugqJLYmKdxI1VfIBOst0SUu4rnk2Z7kqTwmoO1lp3HIfnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.26.5", + "@babel/helper-validator-option": "^7.25.9", + "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/browserslist": { + "version": "4.24.4", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.4.tgz", + "integrity": "sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==", + "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": { + "caniuse-lite": "^1.0.30001688", + "electron-to-chromium": "^1.5.73", + "node-releases": "^2.0.19", + "update-browserslist-db": "^1.1.1" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "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-create-class-features-plugin": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.25.9.tgz", + "integrity": "sha512-UTZQMvt0d/rSz6KI+qdu7GQze5TIajwTS++GUozlw8VBJDEOAqSXwm1WvmYEZwqdqSGQshRocPDqrt4HBZB3fQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.25.9", + "@babel/helper-member-expression-to-functions": "^7.25.9", + "@babel/helper-optimise-call-expression": "^7.25.9", + "@babel/helper-replace-supers": "^7.25.9", + "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9", + "@babel/traverse": "^7.25.9", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-create-class-features-plugin/node_modules/@babel/helper-annotate-as-pure": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.25.9.tgz", + "integrity": "sha512-gv7320KBUFJz1RnylIg5WWYPRXKZ884AGkYpgpWW02TH66Dl+HaC1t1CKd0z3R4b6hdYEcmrNZHUmfCP+1u3/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-create-class-features-plugin/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-create-regexp-features-plugin": { + "version": "7.26.3", + "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.26.3.tgz", + "integrity": "sha512-G7ZRb40uUgdKOQqPLjfD12ZmGA54PzqDFUv2BKImnC9QIfGhIHKvVML0oN8IUiDq4iRqpq74ABpvOaerfWdong==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.25.9", + "regexpu-core": "^6.2.0", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-create-regexp-features-plugin/node_modules/@babel/helper-annotate-as-pure": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.25.9.tgz", + "integrity": "sha512-gv7320KBUFJz1RnylIg5WWYPRXKZ884AGkYpgpWW02TH66Dl+HaC1t1CKd0z3R4b6hdYEcmrNZHUmfCP+1u3/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-create-regexp-features-plugin/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-define-polyfill-provider": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.3.3.tgz", + "integrity": "sha512-z5aQKU4IzbqCC1XH0nAqfsFLMVSo22SBKUc0BxGrLkolTdPTructy0ToNnlO2zA4j9Q/7pjMZf0DSY+DSTYzww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-compilation-targets": "^7.17.7", + "@babel/helper-plugin-utils": "^7.16.7", + "debug": "^4.1.1", + "lodash.debounce": "^4.0.8", + "resolve": "^1.14.2", + "semver": "^6.1.2" + }, + "peerDependencies": { + "@babel/core": "^7.4.0-0" + } + }, + "node_modules/@babel/helper-define-polyfill-provider/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-environment-visitor": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.24.7.tgz", + "integrity": "sha512-DoiN84+4Gnd0ncbBOM9AZENV4a5ZiL39HYMyZJGZ/AZEykHYdJw0wW3kdcsh9/Kn+BRXHLkkklZ51ecPKmI1CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-member-expression-to-functions": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.25.9.tgz", + "integrity": "sha512-wbfdZ9w5vk0C0oyHqAJbc62+vet5prjj01jjJ8sKn3j9h3MQQlflEdXYvuqRWjHnM12coDEqiC1IRCi0U/EKwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.25.9", + "@babel/types": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.25.9.tgz", + "integrity": "sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.25.9", + "@babel/types": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.26.0.tgz", + "integrity": "sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.25.9", + "@babel/helper-validator-identifier": "^7.25.9", + "@babel/traverse": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-optimise-call-expression": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.25.9.tgz", + "integrity": "sha512-FIpuNaz5ow8VyrYcnXQTDRGvV6tTjkNtCK/RYNDXGSLlUD6cBuQTSw43CShGxjvfBTfcUA/r6UhUCbtYqkhcuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.26.5", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.26.5.tgz", + "integrity": "sha512-RS+jZcRdZdRFzMyr+wcsaqOmld1/EqTghfaBGQQd/WnRdzdlvSZ//kF7U8VQTxf1ynZ4cjUcYgjVGx13ewNPMg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-remap-async-to-generator": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.25.9.tgz", + "integrity": "sha512-IZtukuUeBbhgOcaW2s06OXTzVNJR0ybm4W5xC1opWFFJMZbwRj5LCk+ByYH7WdZPZTt8KnFwA8pvjN2yqcPlgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.25.9", + "@babel/helper-wrap-function": "^7.25.9", + "@babel/traverse": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-remap-async-to-generator/node_modules/@babel/helper-annotate-as-pure": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.25.9.tgz", + "integrity": "sha512-gv7320KBUFJz1RnylIg5WWYPRXKZ884AGkYpgpWW02TH66Dl+HaC1t1CKd0z3R4b6hdYEcmrNZHUmfCP+1u3/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-replace-supers": { + "version": "7.26.5", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.26.5.tgz", + "integrity": "sha512-bJ6iIVdYX1YooY2X7w1q6VITt+LnUILtNk7zT78ykuwStx8BauCzxvFqFaHjOpW1bVnSUM1PN1f0p5P21wHxvg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-member-expression-to-functions": "^7.25.9", + "@babel/helper-optimise-call-expression": "^7.25.9", + "@babel/traverse": "^7.26.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-skip-transparent-expression-wrappers": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.25.9.tgz", + "integrity": "sha512-K4Du3BFa3gvyhzgPcntrkDgZzQaq6uozzcpGbOO1OEJaI+EJdqWIMTLgFgQf6lrfiDFo5FU+BxKepI9RmZqahA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.25.9", + "@babel/types": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-split-export-declaration": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.18.6.tgz", + "integrity": "sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", + "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", + "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.25.9.tgz", + "integrity": "sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-wrap-function": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.25.9.tgz", + "integrity": "sha512-ETzz9UTjQSTmw39GboatdymDq4XIQbR8ySgVrylRhPOFpsd+JrKHIuF0de7GCWmem+T4uC5z7EZguod7Wj4A4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.25.9", + "@babel/traverse": "^7.25.9", + "@babel/types": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-wrap-function/node_modules/@babel/template": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.9.tgz", + "integrity": "sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.25.9", + "@babel/parser": "^7.25.9", + "@babel/types": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.26.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.7.tgz", + "integrity": "sha512-8NHiL98vsi0mbPQmYAGWwfcFaOy4j2HY49fXJCfuDcdE7fMIsH9a7GdaeXpIBsbT7307WU8KCMp5pUVDNL4f9A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.25.9", + "@babel/types": "^7.26.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers/node_modules/@babel/template": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.9.tgz", + "integrity": "sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.25.9", + "@babel/parser": "^7.25.9", + "@babel/types": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.26.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.7.tgz", + "integrity": "sha512-kEvgGGgEjRUutvdVvZhbn/BxVt+5VSpwXz1j3WYXQbXDo8KzFOPNG2GQbdAiNq8g6wn1yKk7C/qrke03a84V+w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.26.7" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.25.9.tgz", + "integrity": "sha512-2qUwwfAFpJLZqxd02YW9btUCZHl+RFvdDkNfZwaIJrvB8Tesjsk8pEQkTvGwZXLqXUx/2oyY3ySRhm6HOXuCug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.25.9.tgz", + "integrity": "sha512-6xWgLZTJXwilVjlnV7ospI3xi+sl8lN8rXXbBD6vYn3UYDlGsag8wrZkKcSI8G6KgqKP7vNFaDgeDnfAABq61g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9", + "@babel/plugin-transform-optional-chaining": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.13.0" + } + }, + "node_modules/@babel/plugin-proposal-async-generator-functions": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.20.7.tgz", + "integrity": "sha512-xMbiLsn/8RK7Wq7VeVytytS2L6qE69bXPB10YCmMdDZbKF4okCqY74pI/jJQ/8U0b/F6NrT2+14b8/P9/3AMGA==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-async-generator-functions instead.", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-environment-visitor": "^7.18.9", + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/helper-remap-async-to-generator": "^7.18.9", + "@babel/plugin-syntax-async-generators": "^7.8.4" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-class-properties": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.18.6.tgz", + "integrity": "sha512-cumfXOF0+nzZrrN8Rf0t7M+tF6sZc7vhQwYQck9q1/5w2OExlD+b4v4RpMJFaV1Z7WcDRgO6FqvxqxGlwo+RHQ==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-class-properties instead.", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-class-static-block": { + "version": "7.21.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-static-block/-/plugin-proposal-class-static-block-7.21.0.tgz", + "integrity": "sha512-XP5G9MWNUskFuP30IfFSEFB0Z6HzLIUcjYM4bYOPHXl7eiJ9HFv8tWj6TXTN5QODiEhDZAeI4hLok2iHFFV4hw==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-class-static-block instead.", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.21.0", + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/plugin-syntax-class-static-block": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.12.0" + } + }, + "node_modules/@babel/plugin-proposal-dynamic-import": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.18.6.tgz", + "integrity": "sha512-1auuwmK+Rz13SJj36R+jqFPMJWyKEDd7lLSdOj4oJK0UTgGueSAtkrCvz9ewmgyU/P941Rv2fQwZJN8s6QruXw==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-dynamic-import instead.", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6", + "@babel/plugin-syntax-dynamic-import": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-export-namespace-from": { + "version": "7.18.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-export-namespace-from/-/plugin-proposal-export-namespace-from-7.18.9.tgz", + "integrity": "sha512-k1NtHyOMvlDDFeb9G5PhUXuGj8m/wiwojgQVEhJ/fsVsMCpLyOP4h0uGEjYJKrRI+EVPlb5Jk+Gt9P97lOGwtA==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-export-namespace-from instead.", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.9", + "@babel/plugin-syntax-export-namespace-from": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-json-strings": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.18.6.tgz", + "integrity": "sha512-lr1peyn9kOdbYc0xr0OdHTZ5FMqS6Di+H0Fz2I/JwMzGmzJETNeOFq2pBySw6X/KFL5EWDjlJuMsUGRFb8fQgQ==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-json-strings instead.", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6", + "@babel/plugin-syntax-json-strings": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-logical-assignment-operators": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-logical-assignment-operators/-/plugin-proposal-logical-assignment-operators-7.20.7.tgz", + "integrity": "sha512-y7C7cZgpMIjWlKE5T7eJwp+tnRYM89HmRvWM5EQuB5BoHEONjmQ8lSNmBUwOyy/GFRsohJED51YBF79hE1djug==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-logical-assignment-operators instead.", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-nullish-coalescing-operator": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.18.6.tgz", + "integrity": "sha512-wQxQzxYeJqHcfppzBDnm1yAY0jSRkUXR2z8RePZYrKwMKgMlE8+Z6LUno+bd6LvbGh8Gltvy74+9pIYkr+XkKA==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-nullish-coalescing-operator instead.", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-numeric-separator": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.18.6.tgz", + "integrity": "sha512-ozlZFogPqoLm8WBr5Z8UckIoE4YQ5KESVcNudyXOR8uqIkliTEgJ3RoketfG6pmzLdeZF0H/wjE9/cCEitBl7Q==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-numeric-separator instead.", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6", + "@babel/plugin-syntax-numeric-separator": "^7.10.4" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-object-rest-spread": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.20.7.tgz", + "integrity": "sha512-d2S98yCiLxDVmBmE8UjGcfPvNEUbA1U5q5WxaWFUGRzJSVAZqm5W6MbPct0jxnegUZ0niLeNX+IOzEs7wYg9Dg==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-object-rest-spread instead.", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.20.5", + "@babel/helper-compilation-targets": "^7.20.7", + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-transform-parameters": "^7.20.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-optional-catch-binding": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.18.6.tgz", + "integrity": "sha512-Q40HEhs9DJQyaZfUjjn6vE8Cv4GmMHCYuMGIWUnlxH6400VGxOuwWsPt4FxXxJkC/5eOzgn0z21M9gMT4MOhbw==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-optional-catch-binding instead.", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-optional-chaining": { + "version": "7.21.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.21.0.tgz", + "integrity": "sha512-p4zeefM72gpmEe2fkUr/OnOXpWEf8nAgk7ZYVqqfFiyIG7oFfVZcCrU64hWn5xp4tQ9LkV4bTIa5rD0KANpKNA==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-optional-chaining instead.", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/helper-skip-transparent-expression-wrappers": "^7.20.0", + "@babel/plugin-syntax-optional-chaining": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-private-methods": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-methods/-/plugin-proposal-private-methods-7.18.6.tgz", + "integrity": "sha512-nutsvktDItsNn4rpGItSNV2sz1XwS+nfU0Rg8aCx3W3NOKVzdMjJRu0O5OkgDp3ZGICSTbgRpxZoWsxoKRvbeA==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-private-methods instead.", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-private-property-in-object": { + "version": "7.21.11", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.11.tgz", + "integrity": "sha512-0QZ8qP/3RLDVBwBFoWAwCtgcDZJVwA5LUJRZU8x2YFfKNuFq161wK3cuGrALu5yiPu+vzwTAg/sMWVNeWeNyaw==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-private-property-in-object instead.", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.18.6", + "@babel/helper-create-class-features-plugin": "^7.21.0", + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-unicode-property-regex": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.18.6.tgz", + "integrity": "sha512-2BShG/d5yoZyXZfVePH91urL5wTG6ASZU9M4o03lKK8u8UW1y08OMttBSOADTcJrnPMpvDXRG3G8fyLh4ovs8w==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-unicode-property-regex instead.", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-dynamic-import": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz", + "integrity": "sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-export-namespace-from": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz", + "integrity": "sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.3" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-assertions": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.26.0.tgz", + "integrity": "sha512-QCWT5Hh830hK5EQa7XzuqIkQU9tT/whqbDz7kuaZMHFl1inRRg7JnuAEOQ0Ur0QUl0NufCk1msK2BeY79Aj/eg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-arrow-functions": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.25.9.tgz", + "integrity": "sha512-6jmooXYIwn9ca5/RylZADJ+EnSxVUS5sjeJ9UPk6RWRzXCmOJCy6dqItPJFpw2cuCangPK4OYr5uhGKcmrm5Qg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-async-to-generator": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.20.7.tgz", + "integrity": "sha512-Uo5gwHPT9vgnSXQxqGtpdufUiWp96gk7yiP4Mp5bm1QMkEmLXBO7PAGYbKoJ6DhAwiNkcHFBol/x5zZZkL/t0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.18.6", + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/helper-remap-async-to-generator": "^7.18.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-block-scoped-functions": { + "version": "7.26.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.26.5.tgz", + "integrity": "sha512-chuTSY+hq09+/f5lMj8ZSYgCFpppV2CbYrhNFJ1BFoXpiWPnnAb7R0MqrafCpN8E1+YRrtM1MXZHJdIx8B6rMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.26.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-block-scoping": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.25.9.tgz", + "integrity": "sha512-1F05O7AYjymAtqbsFETboN1NvBdcnzMerO+zlMyJBEz6WkMdejvGWw9p05iTSjC85RLlBseHHQpYaM4gzJkBGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-classes": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.25.9.tgz", + "integrity": "sha512-mD8APIXmseE7oZvZgGABDyM34GUmK45Um2TXiBUt7PnuAxrgoSVf123qUzPxEr/+/BHrRn5NMZCdE2m/1F8DGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.25.9", + "@babel/helper-compilation-targets": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/helper-replace-supers": "^7.25.9", + "@babel/traverse": "^7.25.9", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-classes/node_modules/@babel/helper-annotate-as-pure": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.25.9.tgz", + "integrity": "sha512-gv7320KBUFJz1RnylIg5WWYPRXKZ884AGkYpgpWW02TH66Dl+HaC1t1CKd0z3R4b6hdYEcmrNZHUmfCP+1u3/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/plugin-transform-computed-properties": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.25.9.tgz", + "integrity": "sha512-HnBegGqXZR12xbcTHlJ9HGxw1OniltT26J5YpfruGqtUHlz/xKf/G2ak9e+t0rVqrjXa9WOhvYPz1ERfMj23AA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/template": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-computed-properties/node_modules/@babel/template": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.9.tgz", + "integrity": "sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.25.9", + "@babel/parser": "^7.25.9", + "@babel/types": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/plugin-transform-destructuring": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.25.9.tgz", + "integrity": "sha512-WkCGb/3ZxXepmMiX101nnGiU+1CAdut8oHyEOHxkKuS1qKpU2SMXE2uSvfz8PBuLd49V6LEsbtyPhWC7fnkgvQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-dotall-regex": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.25.9.tgz", + "integrity": "sha512-t7ZQ7g5trIgSRYhI9pIJtRl64KHotutUJsh4Eze5l7olJv+mRSg4/MmbZ0tv1eeqRbdvo/+trvJD/Oc5DmW2cA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-duplicate-keys": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.25.9.tgz", + "integrity": "sha512-LZxhJ6dvBb/f3x8xwWIuyiAHy56nrRG3PeYTpBkkzkYRRQ6tJLu68lEF5VIqMUZiAV7a8+Tb78nEoMCMcqjXBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-exponentiation-operator": { + "version": "7.26.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.26.3.tgz", + "integrity": "sha512-7CAHcQ58z2chuXPWblnn1K6rLDnDWieghSOEmqQsrBenH0P9InCUtOJYD89pvngljmZlJcz3fcmgYsXFNGa1ZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-for-of": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.25.9.tgz", + "integrity": "sha512-LqHxduHoaGELJl2uhImHwRQudhCM50pT46rIBNvtT/Oql3nqiS3wOwP+5ten7NpYSXrrVLgtZU3DZmPtWZo16A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-function-name": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.25.9.tgz", + "integrity": "sha512-8lP+Yxjv14Vc5MuWBpJsoUCd3hD6V9DgBon2FVYL4jJgbnVQ9fTgYmonchzZJOVNgzEgbxp4OwAf6xz6M/14XA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-compilation-targets": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/traverse": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-literals": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.25.9.tgz", + "integrity": "sha512-9N7+2lFziW8W9pBl2TzaNht3+pgMIRP74zizeCSrtnSKVdUl8mAjjOP2OOVQAfZ881P2cNjDj1uAMEdeD50nuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-member-expression-literals": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.25.9.tgz", + "integrity": "sha512-PYazBVfofCQkkMzh2P6IdIUaCEWni3iYEerAsRWuVd8+jlM1S9S9cz1dF9hIzyoZ8IA3+OwVYIp9v9e+GbgZhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-amd": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.25.9.tgz", + "integrity": "sha512-g5T11tnI36jVClQlMlt4qKDLlWnG5pP9CSM4GhdRciTNMRgkfpo5cR6b4rGIOYPgRRuFAvwjPQ/Yk+ql4dyhbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-commonjs": { + "version": "7.26.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.26.3.tgz", + "integrity": "sha512-MgR55l4q9KddUDITEzEFYn5ZsGDXMSsU9E+kh7fjRXTIC3RHqfCo8RPRbyReYJh44HQ/yomFkqbOFohXvDCiIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.26.0", + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-systemjs": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.25.9.tgz", + "integrity": "sha512-hyss7iIlH/zLHaehT+xwiymtPOpsiwIIRlCAOwBB04ta5Tt+lNItADdlXw3jAWZ96VJ2jlhl/c+PNIQPKNfvcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/helper-validator-identifier": "^7.25.9", + "@babel/traverse": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-umd": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.25.9.tgz", + "integrity": "sha512-bS9MVObUgE7ww36HEfwe6g9WakQ0KF07mQF74uuXdkoziUPfKyu/nIm663kz//e5O1nPInPFx36z7WJmJ4yNEw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-named-capturing-groups-regex": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.25.9.tgz", + "integrity": "sha512-oqB6WHdKTGl3q/ItQhpLSnWWOpjUJLsOCLVyeFgeTktkBSCiurvPOsyt93gibI9CmuKvTUEtWmG5VhZD+5T/KA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-new-target": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.25.9.tgz", + "integrity": "sha512-U/3p8X1yCSoKyUj2eOBIx3FOn6pElFOKvAAGf8HTtItuPyB+ZeOqfn+mvTtg9ZlOAjsPdK3ayQEjqHjU/yLeVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-object-super": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.25.9.tgz", + "integrity": "sha512-Kj/Gh+Rw2RNLbCK1VAWj2U48yxxqL2x0k10nPtSdRa0O2xnHXalD0s+o1A6a0W43gJ00ANo38jxkQreckOzv5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/helper-replace-supers": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-optional-chaining": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.25.9.tgz", + "integrity": "sha512-6AvV0FsLULbpnXeBjrY4dmWF8F7gf8QnvTEoO/wX/5xm/xE1Xo8oPuD3MPS+KS9f9XBEAWN7X1aWr4z9HdOr7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-parameters": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.25.9.tgz", + "integrity": "sha512-wzz6MKwpnshBAiRmn4jR8LYz/g8Ksg0o80XmwZDlordjwEk9SxBzTWC7F5ef1jhbrbOW2DJ5J6ayRukrJmnr0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-property-literals": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.25.9.tgz", + "integrity": "sha512-IvIUeV5KrS/VPavfSM/Iu+RE6llrHrYIKY1yfCzyO/lMXHQ+p7uGhonmGVisv6tSBSVgWzMBohTcvkC9vQcQFA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-regenerator": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.25.9.tgz", + "integrity": "sha512-vwDcDNsgMPDGP0nMqzahDWE5/MLcX8sv96+wfX7as7LoF/kr97Bo/7fI00lXY4wUXYfVmwIIyG80fGZ1uvt2qg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9", + "regenerator-transform": "^0.15.2" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-reserved-words": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.25.9.tgz", + "integrity": "sha512-7DL7DKYjn5Su++4RXu8puKZm2XBPHyjWLUidaPEkCUBbE7IPcsrkRHggAOOKydH1dASWdcUBxrkOGNxUv5P3Jg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-runtime": { + "version": "7.19.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.19.6.tgz", + "integrity": "sha512-PRH37lz4JU156lYFW1p8OxE5i7d6Sl/zV58ooyr+q1J1lnQPyg5tIiXlIwNVhJaY4W3TmOtdc8jqdXQcB1v5Yw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.18.6", + "@babel/helper-plugin-utils": "^7.19.0", + "babel-plugin-polyfill-corejs2": "^0.3.3", + "babel-plugin-polyfill-corejs3": "^0.6.0", + "babel-plugin-polyfill-regenerator": "^0.4.1", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-runtime/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/plugin-transform-shorthand-properties": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.25.9.tgz", + "integrity": "sha512-MUv6t0FhO5qHnS/W8XCbHmiRWOphNufpE1IVxhK5kuN3Td9FT1x4rx4K42s3RYdMXCXpfWkGSbCSd0Z64xA7Ng==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-spread": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.25.9.tgz", + "integrity": "sha512-oNknIB0TbURU5pqJFVbOOFspVlrpVwo2H1+HUIsVDvp5VauGGDP1ZEvO8Nn5xyMEs3dakajOxlmkNW7kNgSm6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-sticky-regex": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.25.9.tgz", + "integrity": "sha512-WqBUSgeVwucYDP9U/xNRQam7xV8W5Zf+6Eo7T2SRVUFlhRiMNFdFz58u0KZmCVVqs2i7SHgpRnAhzRNmKfi2uA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-template-literals": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.25.9.tgz", + "integrity": "sha512-o97AE4syN71M/lxrCtQByzphAdlYluKPDBzDVzMmfCobUjjhAryZV0AIpRPrxN0eAkxXO6ZLEScmt+PNhj2OTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-typeof-symbol": { + "version": "7.26.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.26.7.tgz", + "integrity": "sha512-jfoTXXZTgGg36BmhqT3cAYK5qkmqvJpvNrPhaK/52Vgjhw4Rq29s9UqpWWV0D6yuRmgiFH/BUVlkl96zJWqnaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.26.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-escapes": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.25.9.tgz", + "integrity": "sha512-s5EDrE6bW97LtxOcGj1Khcx5AaXwiMmi4toFWRDP9/y0Woo6pXC+iyPu/KuhKtfSrNFd7jJB+/fkOtZy6aIC6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-regex": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.25.9.tgz", + "integrity": "sha512-yoxstj7Rg9dlNn9UQxzk4fcNivwv4nUYz7fYXBaKxvw/lnmPuOm/ikoELygbYq68Bls3D/D+NBPHiLwZdZZ4HA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-env": { + "version": "7.20.2", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.20.2.tgz", + "integrity": "sha512-1G0efQEWR1EHkKvKHqbG+IN/QdgwfByUpM5V5QroDzGV2t3S/WXNQd693cHiHTlCFMpr9B6FkPFXDA2lQcKoDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.20.1", + "@babel/helper-compilation-targets": "^7.20.0", + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/helper-validator-option": "^7.18.6", + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.18.6", + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.18.9", + "@babel/plugin-proposal-async-generator-functions": "^7.20.1", + "@babel/plugin-proposal-class-properties": "^7.18.6", + "@babel/plugin-proposal-class-static-block": "^7.18.6", + "@babel/plugin-proposal-dynamic-import": "^7.18.6", + "@babel/plugin-proposal-export-namespace-from": "^7.18.9", + "@babel/plugin-proposal-json-strings": "^7.18.6", + "@babel/plugin-proposal-logical-assignment-operators": "^7.18.9", + "@babel/plugin-proposal-nullish-coalescing-operator": "^7.18.6", + "@babel/plugin-proposal-numeric-separator": "^7.18.6", + "@babel/plugin-proposal-object-rest-spread": "^7.20.2", + "@babel/plugin-proposal-optional-catch-binding": "^7.18.6", + "@babel/plugin-proposal-optional-chaining": "^7.18.9", + "@babel/plugin-proposal-private-methods": "^7.18.6", + "@babel/plugin-proposal-private-property-in-object": "^7.18.6", + "@babel/plugin-proposal-unicode-property-regex": "^7.18.6", + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-dynamic-import": "^7.8.3", + "@babel/plugin-syntax-export-namespace-from": "^7.8.3", + "@babel/plugin-syntax-import-assertions": "^7.20.0", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5", + "@babel/plugin-transform-arrow-functions": "^7.18.6", + "@babel/plugin-transform-async-to-generator": "^7.18.6", + "@babel/plugin-transform-block-scoped-functions": "^7.18.6", + "@babel/plugin-transform-block-scoping": "^7.20.2", + "@babel/plugin-transform-classes": "^7.20.2", + "@babel/plugin-transform-computed-properties": "^7.18.9", + "@babel/plugin-transform-destructuring": "^7.20.2", + "@babel/plugin-transform-dotall-regex": "^7.18.6", + "@babel/plugin-transform-duplicate-keys": "^7.18.9", + "@babel/plugin-transform-exponentiation-operator": "^7.18.6", + "@babel/plugin-transform-for-of": "^7.18.8", + "@babel/plugin-transform-function-name": "^7.18.9", + "@babel/plugin-transform-literals": "^7.18.9", + "@babel/plugin-transform-member-expression-literals": "^7.18.6", + "@babel/plugin-transform-modules-amd": "^7.19.6", + "@babel/plugin-transform-modules-commonjs": "^7.19.6", + "@babel/plugin-transform-modules-systemjs": "^7.19.6", + "@babel/plugin-transform-modules-umd": "^7.18.6", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.19.1", + "@babel/plugin-transform-new-target": "^7.18.6", + "@babel/plugin-transform-object-super": "^7.18.6", + "@babel/plugin-transform-parameters": "^7.20.1", + "@babel/plugin-transform-property-literals": "^7.18.6", + "@babel/plugin-transform-regenerator": "^7.18.6", + "@babel/plugin-transform-reserved-words": "^7.18.6", + "@babel/plugin-transform-shorthand-properties": "^7.18.6", + "@babel/plugin-transform-spread": "^7.19.0", + "@babel/plugin-transform-sticky-regex": "^7.18.6", + "@babel/plugin-transform-template-literals": "^7.18.9", + "@babel/plugin-transform-typeof-symbol": "^7.18.9", + "@babel/plugin-transform-unicode-escapes": "^7.18.10", + "@babel/plugin-transform-unicode-regex": "^7.18.6", + "@babel/preset-modules": "^0.1.5", + "@babel/types": "^7.20.2", + "babel-plugin-polyfill-corejs2": "^0.3.3", + "babel-plugin-polyfill-corejs3": "^0.6.0", + "babel-plugin-polyfill-regenerator": "^0.4.1", + "core-js-compat": "^3.25.1", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-env/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/preset-modules": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.6.tgz", + "integrity": "sha512-ID2yj6K/4lKfhuU3+EX4UvNbIt7eACFbHmNUjzA+ep+B5971CknnA/9DEWKbRokfbbtblxxxXFJJrH47UEAMVg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/plugin-proposal-unicode-property-regex": "^7.4.4", + "@babel/plugin-transform-dotall-regex": "^7.4.4", + "@babel/types": "^7.4.4", + "esutils": "^2.0.2" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.20.13", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.20.13.tgz", + "integrity": "sha512-gt3PKXs0DBoL9xCvOIIZ2NEqAGZqHjAnmVbfQtB620V0uReIQutpel14KcneZuer7UioY8ALKZ7iocavvzTNFA==", + "dev": true, + "license": "MIT", + "dependencies": { + "regenerator-runtime": "^0.13.11" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.20.7.tgz", + "integrity": "sha512-8SegXApWe6VoNw0r9JHpSteLKTpTiLZ4rMlGIm9JQ18KiCtyQiAMEazujAHrUS5flrcqYZa75ukev3P6QmUwUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.18.6", + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.26.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.26.7.tgz", + "integrity": "sha512-1x1sgeyRLC3r5fQOM0/xtQKsYjyxmFjaOrLJNtZ81inNjyJHGIolTULPiSc/2qe1/qfpFLisLQYFnnZl7QoedA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.26.2", + "@babel/generator": "^7.26.5", + "@babel/parser": "^7.26.7", + "@babel/template": "^7.25.9", + "@babel/types": "^7.26.7", + "debug": "^4.3.1", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse/node_modules/@babel/generator": { + "version": "7.26.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.5.tgz", + "integrity": "sha512-2caSP6fN9I7HOe6nqhtft7V4g7/V/gfDsC3Ag4W7kEzzvRGKqiv0pu0HogPiZ3KaVSoNDhUws6IJjDjpfmYIXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.26.5", + "@babel/types": "^7.26.5", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse/node_modules/@babel/template": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.9.tgz", + "integrity": "sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.25.9", + "@babel/parser": "^7.25.9", + "@babel/types": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse/node_modules/@jridgewell/gen-mapping": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", + "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/traverse/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/@babel/types": { + "version": "7.26.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.7.tgz", + "integrity": "sha512-t8kDRGrKXyp6+tjUh7hw2RLyclsW4TRoRvRHtSyAX9Bb5ldlFh+90YAYY6awRXrlB4G5G2izNeGySpATlFzmOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.25.9", + "@babel/helper-validator-identifier": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@braintree/sanitize-url": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/@braintree/sanitize-url/-/sanitize-url-6.0.4.tgz", + "integrity": "sha512-s3jaWicZd0pkP0jf5ysyHUI/RE7MHos6qlToFcGWXVp+ykHOy77OUMrfbgJ9it2C5bow7OIQwYYaHjk9XlBQ2A==", + "license": "MIT", + "optional": true + }, + "node_modules/@colors/colors": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", + "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/@dabh/diagnostics": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.3.tgz", + "integrity": "sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==", + "license": "MIT", + "dependencies": { + "colorspace": "1.1.x", + "enabled": "2.0.x", + "kuler": "^2.0.0" + } + }, + "node_modules/@discoveryjs/json-ext": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz", + "integrity": "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.17.8", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.17.8.tgz", + "integrity": "sha512-0/rb91GYKhrtbeglJXOhAv9RuYimgI8h623TplY2X+vA4EXnk3Zj1fXZreJ0J3OJJu1bwmb0W7g+2cT/d8/l/w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.17.8", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.17.8.tgz", + "integrity": "sha512-oa/N5j6v1svZQs7EIRPqR8f+Bf8g6HBDjD/xHC02radE/NjKHK7oQmtmLxPs1iVwYyvE+Kolo6lbpfEQ9xnhxQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.17.8", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.17.8.tgz", + "integrity": "sha512-bTliMLqD7pTOoPg4zZkXqCDuzIUguEWLpeqkNfC41ODBHwoUgZ2w5JBeYimv4oP6TDVocoYmEhZrCLQTrH89bg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.17.8", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.17.8.tgz", + "integrity": "sha512-ghAbV3ia2zybEefXRRm7+lx8J/rnupZT0gp9CaGy/3iolEXkJ6LYRq4IpQVI9zR97ID80KJVoUlo3LSeA/sMAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.17.8", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.17.8.tgz", + "integrity": "sha512-n5WOpyvZ9TIdv2V1K3/iIkkJeKmUpKaCTdun9buhGRWfH//osmUjlv4Z5mmWdPWind/VGcVxTHtLfLCOohsOXw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.17.8", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.17.8.tgz", + "integrity": "sha512-a/SATTaOhPIPFWvHZDoZYgxaZRVHn0/LX1fHLGfZ6C13JqFUZ3K6SMD6/HCtwOQ8HnsNaEeokdiDSFLuizqv5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.17.8", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.17.8.tgz", + "integrity": "sha512-xpFJb08dfXr5+rZc4E+ooZmayBW6R3q59daCpKZ/cDU96/kvDM+vkYzNeTJCGd8rtO6fHWMq5Rcv/1cY6p6/0Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.17.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.17.8.tgz", + "integrity": "sha512-6Ij8gfuGszcEwZpi5jQIJCVIACLS8Tz2chnEBfYjlmMzVsfqBP1iGmHQPp7JSnZg5xxK9tjCc+pJ2WtAmPRFVA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.17.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.17.8.tgz", + "integrity": "sha512-v3iwDQuDljLTxpsqQDl3fl/yihjPAyOguxuloON9kFHYwopeJEf1BkDXODzYyXEI19gisEsQlG1bM65YqKSIww==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.17.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.17.8.tgz", + "integrity": "sha512-8svILYKhE5XetuFk/B6raFYIyIqydQi+GngEXJgdPdI7OMKUbSd7uzR02wSY4kb53xBrClLkhH4Xs8P61Q2BaA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.17.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.17.8.tgz", + "integrity": "sha512-B6FyMeRJeV0NpyEOYlm5qtQfxbdlgmiGdD+QsipzKfFky0K5HW5Td6dyK3L3ypu1eY4kOmo7wW0o94SBqlqBSA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.17.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.17.8.tgz", + "integrity": "sha512-CCb67RKahNobjm/eeEqeD/oJfJlrWyw29fgiyB6vcgyq97YAf3gCOuP6qMShYSPXgnlZe/i4a8WFHBw6N8bYAA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.17.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.17.8.tgz", + "integrity": "sha512-bytLJOi55y55+mGSdgwZ5qBm0K9WOCh0rx+vavVPx+gqLLhxtSFU0XbeYy/dsAAD6xECGEv4IQeFILaSS2auXw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.17.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.17.8.tgz", + "integrity": "sha512-2YpRyQJmKVBEHSBLa8kBAtbhucaclb6ex4wchfY0Tj3Kg39kpjeJ9vhRU7x4mUpq8ISLXRXH1L0dBYjAeqzZAw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.17.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.17.8.tgz", + "integrity": "sha512-QgbNY/V3IFXvNf11SS6exkpVcX0LJcob+0RWCgV9OiDAmVElnxciHIisoSix9uzYzScPmS6dJFbZULdSAEkQVw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.17.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.17.8.tgz", + "integrity": "sha512-mM/9S0SbAFDBc4OPoyP6SEOo5324LpUxdpeIUUSrSTOfhHU9hEfqRngmKgqILqwx/0DVJBzeNW7HmLEWp9vcOA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.17.8", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.17.8.tgz", + "integrity": "sha512-eKUYcWaWTaYr9zbj8GertdVtlt1DTS1gNBWov+iQfWuWyuu59YN6gSEJvFzC5ESJ4kMcKR0uqWThKUn5o8We6Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.17.8", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.17.8.tgz", + "integrity": "sha512-Vc9J4dXOboDyMXKD0eCeW0SIeEzr8K9oTHJU+Ci1mZc5njPfhKAqkRt3B/fUNU7dP+mRyralPu8QUkiaQn7iIg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.17.8", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.17.8.tgz", + "integrity": "sha512-0xvOTNuPXI7ft1LYUgiaXtpCEjp90RuBBYovdd2lqAFxje4sEucurg30M1WIm03+3jxByd3mfo+VUmPtRSVuOw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.17.8", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.17.8.tgz", + "integrity": "sha512-G0JQwUI5WdEFEnYNKzklxtBheCPkuDdu1YrtRrjuQv30WsYbkkoixKxLLv8qhJmNI+ATEWquZe/N0d0rpr55Mg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.17.8", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.17.8.tgz", + "integrity": "sha512-Fqy63515xl20OHGFykjJsMnoIWS+38fqfg88ClvPXyDbLtgXal2DTlhb1TfTX34qWi3u4I7Cq563QcHpqgLx8w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.17.8", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.17.8.tgz", + "integrity": "sha512-1iuezdyDNngPnz8rLRDO2C/ZZ/emJLb72OsZeqQ6gL6Avko/XCXZw+NuxBSNhBAP13Hie418V7VMt9et1FMvpg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.1.tgz", + "integrity": "sha512-s3O3waFUrMV8P/XaF/+ZTp1X9XBZW1a4B97ZnjQF2KYWaFD2A8KyFBsrsfSjEmjn3RGWAIuvlneuZm3CUK3jbA==", + "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.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", + "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", + "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/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "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/@eslint/eslintrc/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/@eslint/eslintrc/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/eslintrc/node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/@eslint/eslintrc/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/@eslint/eslintrc/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/eslintrc/node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz", + "integrity": "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@firebase/analytics": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@firebase/analytics/-/analytics-0.10.0.tgz", + "integrity": "sha512-Locv8gAqx0e+GX/0SI3dzmBY5e9kjVDtD+3zCFLJ0tH2hJwuCAiL+5WkHuxKj92rqQj/rvkBUCfA1ewlX2hehg==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.6.4", + "@firebase/installations": "0.6.4", + "@firebase/logger": "0.4.0", + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/analytics-compat": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/@firebase/analytics-compat/-/analytics-compat-0.2.6.tgz", + "integrity": "sha512-4MqpVLFkGK7NJf/5wPEEP7ePBJatwYpyjgJ+wQHQGHfzaCDgntOnl9rL2vbVGGKCnRqWtZDIWhctB86UWXaX2Q==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/analytics": "0.10.0", + "@firebase/analytics-types": "0.8.0", + "@firebase/component": "0.6.4", + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/analytics-types": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@firebase/analytics-types/-/analytics-types-0.8.0.tgz", + "integrity": "sha512-iRP+QKI2+oz3UAh4nPEq14CsEjrjD6a5+fuypjScisAh9kXKFvdJOZJDwk7kikLvWVLGEs9+kIUS4LPQV7VZVw==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/app": { + "version": "0.9.13", + "resolved": "https://registry.npmjs.org/@firebase/app/-/app-0.9.13.tgz", + "integrity": "sha512-GfiI1JxJ7ecluEmDjPzseRXk/PX31hS7+tjgBopL7XjB2hLUdR+0FTMXy2Q3/hXezypDvU6or7gVFizDESrkXw==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.6.4", + "@firebase/logger": "0.4.0", + "@firebase/util": "1.9.3", + "idb": "7.1.1", + "tslib": "^2.1.0" + } + }, + "node_modules/@firebase/app-check": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@firebase/app-check/-/app-check-0.8.0.tgz", + "integrity": "sha512-dRDnhkcaC2FspMiRK/Vbp+PfsOAEP6ZElGm9iGFJ9fDqHoPs0HOPn7dwpJ51lCFi1+2/7n5pRPGhqF/F03I97g==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.6.4", + "@firebase/logger": "0.4.0", + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/app-check-compat": { + "version": "0.3.7", + "resolved": "https://registry.npmjs.org/@firebase/app-check-compat/-/app-check-compat-0.3.7.tgz", + "integrity": "sha512-cW682AxsyP1G+Z0/P7pO/WT2CzYlNxoNe5QejVarW2o5ZxeWSSPAiVEwpEpQR/bUlUmdeWThYTMvBWaopdBsqw==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/app-check": "0.8.0", + "@firebase/app-check-types": "0.5.0", + "@firebase/component": "0.6.4", + "@firebase/logger": "0.4.0", + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/app-check-interop-types": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@firebase/app-check-interop-types/-/app-check-interop-types-0.3.0.tgz", + "integrity": "sha512-xAxHPZPIgFXnI+vb4sbBjZcde7ZluzPPaSK7Lx3/nmuVk4TjZvnL8ONnkd4ERQKL8WePQySU+pRcWkh8rDf5Sg==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/app-check-types": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@firebase/app-check-types/-/app-check-types-0.5.0.tgz", + "integrity": "sha512-uwSUj32Mlubybw7tedRzR24RP8M8JUVR3NPiMk3/Z4bCmgEKTlQBwMXrehDAZ2wF+TsBq0SN1c6ema71U/JPyQ==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/app-compat": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/@firebase/app-compat/-/app-compat-0.2.13.tgz", + "integrity": "sha512-j6ANZaWjeVy5zg6X7uiqh6lM6o3n3LD1+/SJFNs9V781xyryyZWXe+tmnWNWPkP086QfJoNkWN9pMQRqSG4vMg==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/app": "0.9.13", + "@firebase/component": "0.6.4", + "@firebase/logger": "0.4.0", + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" + } + }, + "node_modules/@firebase/app-types": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@firebase/app-types/-/app-types-0.9.0.tgz", + "integrity": "sha512-AeweANOIo0Mb8GiYm3xhTEBVCmPwTYAu9Hcd2qSkLuga/6+j9b1Jskl5bpiSQWy9eJ/j5pavxj6eYogmnuzm+Q==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/auth": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/@firebase/auth/-/auth-0.23.2.tgz", + "integrity": "sha512-dM9iJ0R6tI1JczuGSxXmQbXAgtYie0K4WvKcuyuSTCu9V8eEDiz4tfa1sO3txsfvwg7nOY3AjoCyMYEdqZ8hdg==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.6.4", + "@firebase/logger": "0.4.0", + "@firebase/util": "1.9.3", + "node-fetch": "2.6.7", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/auth-compat": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@firebase/auth-compat/-/auth-compat-0.4.2.tgz", + "integrity": "sha512-Q30e77DWXFmXEt5dg5JbqEDpjw9y3/PcP9LslDPR7fARmAOTIY9MM6HXzm9KC+dlrKH/+p6l8g9ifJiam9mc4A==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/auth": "0.23.2", + "@firebase/auth-types": "0.12.0", + "@firebase/component": "0.6.4", + "@firebase/util": "1.9.3", + "node-fetch": "2.6.7", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/auth-compat/node_modules/node-fetch": { + "version": "2.6.7", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", + "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/@firebase/auth-interop-types": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@firebase/auth-interop-types/-/auth-interop-types-0.2.1.tgz", + "integrity": "sha512-VOaGzKp65MY6P5FI84TfYKBXEPi6LmOCSMMzys6o2BN2LOsqy7pCuZCup7NYnfbk5OkkQKzvIfHOzTm0UDpkyg==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/auth-types": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@firebase/auth-types/-/auth-types-0.12.0.tgz", + "integrity": "sha512-pPwaZt+SPOshK8xNoiQlK5XIrS97kFYc3Rc7xmy373QsOJ9MmqXxLaYssP5Kcds4wd2qK//amx/c+A8O2fVeZA==", + "license": "Apache-2.0", + "peerDependencies": { + "@firebase/app-types": "0.x", + "@firebase/util": "1.x" + } + }, + "node_modules/@firebase/auth/node_modules/node-fetch": { + "version": "2.6.7", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", + "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/@firebase/component": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.6.4.tgz", + "integrity": "sha512-rLMyrXuO9jcAUCaQXCMjCMUsWrba5fzHlNK24xz5j2W6A/SRmK8mZJ/hn7V0fViLbxC0lPMtrK1eYzk6Fg03jA==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" + } + }, + "node_modules/@firebase/database": { + "version": "0.14.4", + "resolved": "https://registry.npmjs.org/@firebase/database/-/database-0.14.4.tgz", + "integrity": "sha512-+Ea/IKGwh42jwdjCyzTmeZeLM3oy1h0mFPsTy6OqCWzcu/KFqRAr5Tt1HRCOBlNOdbh84JPZC47WLU18n2VbxQ==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/auth-interop-types": "0.2.1", + "@firebase/component": "0.6.4", + "@firebase/logger": "0.4.0", + "@firebase/util": "1.9.3", + "faye-websocket": "0.11.4", + "tslib": "^2.1.0" + } + }, + "node_modules/@firebase/database-compat": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@firebase/database-compat/-/database-compat-0.3.4.tgz", + "integrity": "sha512-kuAW+l+sLMUKBThnvxvUZ+Q1ZrF/vFJ58iUY9kAcbX48U03nVzIF6Tmkf0p3WVQwMqiXguSgtOPIB6ZCeF+5Gg==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.6.4", + "@firebase/database": "0.14.4", + "@firebase/database-types": "0.10.4", + "@firebase/logger": "0.4.0", + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" + } + }, + "node_modules/@firebase/database-types": { + "version": "0.10.4", + "resolved": "https://registry.npmjs.org/@firebase/database-types/-/database-types-0.10.4.tgz", + "integrity": "sha512-dPySn0vJ/89ZeBac70T+2tWWPiJXWbmRygYv0smT5TfE3hDrQ09eKMF3Y+vMlTdrMWq7mUdYW5REWPSGH4kAZQ==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/app-types": "0.9.0", + "@firebase/util": "1.9.3" + } + }, + "node_modules/@firebase/firestore": { + "version": "3.13.0", + "resolved": "https://registry.npmjs.org/@firebase/firestore/-/firestore-3.13.0.tgz", + "integrity": "sha512-NwcnU+madJXQ4fbLkGx1bWvL612IJN/qO6bZ6dlPmyf7QRyu5azUosijdAN675r+bOOJxMtP1Bv981bHBXAbUg==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.6.4", + "@firebase/logger": "0.4.0", + "@firebase/util": "1.9.3", + "@firebase/webchannel-wrapper": "0.10.1", + "@grpc/grpc-js": "~1.7.0", + "@grpc/proto-loader": "^0.6.13", + "node-fetch": "2.6.7", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=10.10.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/firestore-compat": { + "version": "0.3.12", + "resolved": "https://registry.npmjs.org/@firebase/firestore-compat/-/firestore-compat-0.3.12.tgz", + "integrity": "sha512-mazuNGAx5Kt9Nph0pm6ULJFp/+j7GSsx+Ncw1GrnKl+ft1CQ4q2LcUssXnjqkX2Ry0fNGqUzC1mfIUrk9bYtjQ==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.6.4", + "@firebase/firestore": "3.13.0", + "@firebase/firestore-types": "2.5.1", + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/firestore-types": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@firebase/firestore-types/-/firestore-types-2.5.1.tgz", + "integrity": "sha512-xG0CA6EMfYo8YeUxC8FeDzf6W3FX1cLlcAGBYV6Cku12sZRI81oWcu61RSKM66K6kUENP+78Qm8mvroBcm1whw==", + "license": "Apache-2.0", + "peerDependencies": { + "@firebase/app-types": "0.x", + "@firebase/util": "1.x" + } + }, + "node_modules/@firebase/firestore/node_modules/node-fetch": { + "version": "2.6.7", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", + "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/@firebase/functions": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@firebase/functions/-/functions-0.10.0.tgz", + "integrity": "sha512-2U+fMNxTYhtwSpkkR6WbBcuNMOVaI7MaH3cZ6UAeNfj7AgEwHwMIFLPpC13YNZhno219F0lfxzTAA0N62ndWzA==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/app-check-interop-types": "0.3.0", + "@firebase/auth-interop-types": "0.2.1", + "@firebase/component": "0.6.4", + "@firebase/messaging-interop-types": "0.2.0", + "@firebase/util": "1.9.3", + "node-fetch": "2.6.7", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/functions-compat": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@firebase/functions-compat/-/functions-compat-0.3.5.tgz", + "integrity": "sha512-uD4jwgwVqdWf6uc3NRKF8cSZ0JwGqSlyhPgackyUPe+GAtnERpS4+Vr66g0b3Gge0ezG4iyHo/EXW/Hjx7QhHw==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.6.4", + "@firebase/functions": "0.10.0", + "@firebase/functions-types": "0.6.0", + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/functions-types": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@firebase/functions-types/-/functions-types-0.6.0.tgz", + "integrity": "sha512-hfEw5VJtgWXIRf92ImLkgENqpL6IWpYaXVYiRkFY1jJ9+6tIhWM7IzzwbevwIIud/jaxKVdRzD7QBWfPmkwCYw==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/functions/node_modules/node-fetch": { + "version": "2.6.7", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", + "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/@firebase/installations": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/@firebase/installations/-/installations-0.6.4.tgz", + "integrity": "sha512-u5y88rtsp7NYkCHC3ElbFBrPtieUybZluXyzl7+4BsIz4sqb4vSAuwHEUgCgCeaQhvsnxDEU6icly8U9zsJigA==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.6.4", + "@firebase/util": "1.9.3", + "idb": "7.0.1", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/installations-compat": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@firebase/installations-compat/-/installations-compat-0.2.4.tgz", + "integrity": "sha512-LI9dYjp0aT9Njkn9U4JRrDqQ6KXeAmFbRC0E7jI7+hxl5YmRWysq5qgQl22hcWpTk+cm3es66d/apoDU/A9n6Q==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.6.4", + "@firebase/installations": "0.6.4", + "@firebase/installations-types": "0.5.0", + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/installations-types": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@firebase/installations-types/-/installations-types-0.5.0.tgz", + "integrity": "sha512-9DP+RGfzoI2jH7gY4SlzqvZ+hr7gYzPODrbzVD82Y12kScZ6ZpRg/i3j6rleto8vTFC8n6Len4560FnV1w2IRg==", + "license": "Apache-2.0", + "peerDependencies": { + "@firebase/app-types": "0.x" + } + }, + "node_modules/@firebase/installations/node_modules/idb": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/idb/-/idb-7.0.1.tgz", + "integrity": "sha512-UUxlE7vGWK5RfB/fDwEGgRf84DY/ieqNha6msMV99UsEMQhJ1RwbCd8AYBj3QMgnE3VZnfQvm4oKVCJTYlqIgg==", + "license": "ISC" + }, + "node_modules/@firebase/logger": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@firebase/logger/-/logger-0.4.0.tgz", + "integrity": "sha512-eRKSeykumZ5+cJPdxxJRgAC3G5NknY2GwEbKfymdnXtnT0Ucm4pspfR6GT4MUQEDuJwRVbVcSx85kgJulMoFFA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@firebase/messaging": { + "version": "0.12.4", + "resolved": "https://registry.npmjs.org/@firebase/messaging/-/messaging-0.12.4.tgz", + "integrity": "sha512-6JLZct6zUaex4g7HI3QbzeUrg9xcnmDAPTWpkoMpd/GoSVWH98zDoWXMGrcvHeCAIsLpFMe4MPoZkJbrPhaASw==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.6.4", + "@firebase/installations": "0.6.4", + "@firebase/messaging-interop-types": "0.2.0", + "@firebase/util": "1.9.3", + "idb": "7.0.1", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/messaging-compat": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@firebase/messaging-compat/-/messaging-compat-0.2.4.tgz", + "integrity": "sha512-lyFjeUhIsPRYDPNIkYX1LcZMpoVbBWXX4rPl7c/rqc7G+EUea7IEtSt4MxTvh6fDfPuzLn7+FZADfscC+tNMfg==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.6.4", + "@firebase/messaging": "0.12.4", + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/messaging-interop-types": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@firebase/messaging-interop-types/-/messaging-interop-types-0.2.0.tgz", + "integrity": "sha512-ujA8dcRuVeBixGR9CtegfpU4YmZf3Lt7QYkcj693FFannwNuZgfAYaTmbJ40dtjB81SAu6tbFPL9YLNT15KmOQ==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/messaging/node_modules/idb": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/idb/-/idb-7.0.1.tgz", + "integrity": "sha512-UUxlE7vGWK5RfB/fDwEGgRf84DY/ieqNha6msMV99UsEMQhJ1RwbCd8AYBj3QMgnE3VZnfQvm4oKVCJTYlqIgg==", + "license": "ISC" + }, + "node_modules/@firebase/performance": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/@firebase/performance/-/performance-0.6.4.tgz", + "integrity": "sha512-HfTn/bd8mfy/61vEqaBelNiNnvAbUtME2S25A67Nb34zVuCSCRIX4SseXY6zBnOFj3oLisaEqhVcJmVPAej67g==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.6.4", + "@firebase/installations": "0.6.4", + "@firebase/logger": "0.4.0", + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/performance-compat": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@firebase/performance-compat/-/performance-compat-0.2.4.tgz", + "integrity": "sha512-nnHUb8uP9G8islzcld/k6Bg5RhX62VpbAb/Anj7IXs/hp32Eb2LqFPZK4sy3pKkBUO5wcrlRWQa6wKOxqlUqsg==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.6.4", + "@firebase/logger": "0.4.0", + "@firebase/performance": "0.6.4", + "@firebase/performance-types": "0.2.0", + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/performance-types": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@firebase/performance-types/-/performance-types-0.2.0.tgz", + "integrity": "sha512-kYrbr8e/CYr1KLrLYZZt2noNnf+pRwDq2KK9Au9jHrBMnb0/C9X9yWSXmZkFt4UIdsQknBq8uBB7fsybZdOBTA==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/remote-config": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/@firebase/remote-config/-/remote-config-0.4.4.tgz", + "integrity": "sha512-x1ioTHGX8ZwDSTOVp8PBLv2/wfwKzb4pxi0gFezS5GCJwbLlloUH4YYZHHS83IPxnua8b6l0IXUaWd0RgbWwzQ==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.6.4", + "@firebase/installations": "0.6.4", + "@firebase/logger": "0.4.0", + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/remote-config-compat": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@firebase/remote-config-compat/-/remote-config-compat-0.2.4.tgz", + "integrity": "sha512-FKiki53jZirrDFkBHglB3C07j5wBpitAaj8kLME6g8Mx+aq7u9P7qfmuSRytiOItADhWUj7O1JIv7n9q87SuwA==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.6.4", + "@firebase/logger": "0.4.0", + "@firebase/remote-config": "0.4.4", + "@firebase/remote-config-types": "0.3.0", + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/remote-config-types": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@firebase/remote-config-types/-/remote-config-types-0.3.0.tgz", + "integrity": "sha512-RtEH4vdcbXZuZWRZbIRmQVBNsE7VDQpet2qFvq6vwKLBIQRQR5Kh58M4ok3A3US8Sr3rubYnaGqZSurCwI8uMA==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/storage": { + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/@firebase/storage/-/storage-0.11.2.tgz", + "integrity": "sha512-CtvoFaBI4hGXlXbaCHf8humajkbXhs39Nbh6MbNxtwJiCqxPy9iH3D3CCfXAvP0QvAAwmJUTK3+z9a++Kc4nkA==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.6.4", + "@firebase/util": "1.9.3", + "node-fetch": "2.6.7", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/storage-compat": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@firebase/storage-compat/-/storage-compat-0.3.2.tgz", + "integrity": "sha512-wvsXlLa9DVOMQJckbDNhXKKxRNNewyUhhbXev3t8kSgoCotd1v3MmqhKKz93ePhDnhHnDs7bYHy+Qa8dRY6BXw==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.6.4", + "@firebase/storage": "0.11.2", + "@firebase/storage-types": "0.8.0", + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/storage-types": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@firebase/storage-types/-/storage-types-0.8.0.tgz", + "integrity": "sha512-isRHcGrTs9kITJC0AVehHfpraWFui39MPaU7Eo8QfWlqW7YPymBmRgjDrlOgFdURh6Cdeg07zmkLP5tzTKRSpg==", + "license": "Apache-2.0", + "peerDependencies": { + "@firebase/app-types": "0.x", + "@firebase/util": "1.x" + } + }, + "node_modules/@firebase/storage/node_modules/node-fetch": { + "version": "2.6.7", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", + "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/@firebase/util": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/@firebase/util/-/util-1.9.3.tgz", + "integrity": "sha512-DY02CRhOZwpzO36fHpuVysz6JZrscPiBXD0fXp6qSrL9oNOx5KWICKdR95C0lSITzxp0TZosVyHqzatE8JbcjA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@firebase/webchannel-wrapper": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@firebase/webchannel-wrapper/-/webchannel-wrapper-0.10.1.tgz", + "integrity": "sha512-Dq5rYfEpdeel0bLVN+nfD1VWmzCkK+pJbSjIawGE+RY4+NIJqhbUDDQjvV0NUK84fMfwxvtFoCtEe70HfZjFcw==", + "license": "Apache-2.0" + }, + "node_modules/@fortawesome/fontawesome-free": { + "version": "6.7.2", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-6.7.2.tgz", + "integrity": "sha512-JUOtgFW6k9u4Y+xeIaEiLr3+cjoUPiAuLXoyKOJSia6Duzb7pq+A76P9ZdPDoAoxHdHzq6gE9/jKBGXlZT8FbA==", + "license": "(CC-BY-4.0 AND OFL-1.1 AND MIT)", + "engines": { + "node": ">=6" + } + }, + "node_modules/@gar/promisify": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz", + "integrity": "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@grpc/grpc-js": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.7.3.tgz", + "integrity": "sha512-H9l79u4kJ2PVSxUNA08HMYAnUBLj9v6KjYQ7SQ71hOZcEXhShE/y5iQCesP8+6/Ik/7i2O0a10bPquIcYfufog==", + "license": "Apache-2.0", + "dependencies": { + "@grpc/proto-loader": "^0.7.0", + "@types/node": ">=12.12.47" + }, + "engines": { + "node": "^8.13.0 || >=10.10.0" + } + }, + "node_modules/@grpc/grpc-js/node_modules/@grpc/proto-loader": { + "version": "0.7.13", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.13.tgz", + "integrity": "sha512-AiXO/bfe9bmxBjxxtYxFAXGZvMaN5s8kO+jBHAJCON8rJoB5YS/D6X7ZNc6XQkuHNmyl4CYaMI1fJ/Gn27RGGw==", + "license": "Apache-2.0", + "dependencies": { + "lodash.camelcase": "^4.3.0", + "long": "^5.0.0", + "protobufjs": "^7.2.5", + "yargs": "^17.7.2" + }, + "bin": { + "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@grpc/grpc-js/node_modules/long": { + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/long/-/long-5.2.4.tgz", + "integrity": "sha512-qtzLbJE8hq7VabR3mISmVGtoXP8KGc2Z/AT8OuqlYD7JTR3oqrgwdjnk07wpj1twXxYmgDXgoKVWUG/fReSzHg==", + "license": "Apache-2.0" + }, + "node_modules/@grpc/grpc-js/node_modules/protobufjs": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.4.0.tgz", + "integrity": "sha512-mRUWCc3KUU4w1jU8sGxICXH/gNS94DvI1gxqDvBzhj1JpcsimQkYiOJfwsPUykUI5ZaspFbSgmBLER8IrQ3tqw==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/node": ">=13.7.0", + "long": "^5.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@grpc/grpc-js/node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@grpc/proto-loader": { + "version": "0.6.13", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.6.13.tgz", + "integrity": "sha512-FjxPYDRTn6Ec3V0arm1FtSpmP6V50wuph2yILpyvTKzjc76oDdoihXqM1DzOW5ubvCC8GivfCnNtfaRE8myJ7g==", + "license": "Apache-2.0", + "dependencies": { + "@types/long": "^4.0.1", + "lodash.camelcase": "^4.3.0", + "long": "^4.0.0", + "protobufjs": "^6.11.3", + "yargs": "^16.2.0" + }, + "bin": { + "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@grpc/proto-loader/node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/@grpc/proto-loader/node_modules/yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "license": "MIT", + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@grpc/proto-loader/node_modules/yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.11.14", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", + "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==", + "deprecated": "Use @eslint/config-array instead", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanwhocodes/object-schema": "^2.0.2", + "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.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "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.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "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/@ionic/angular": { + "version": "7.8.6", + "resolved": "https://registry.npmjs.org/@ionic/angular/-/angular-7.8.6.tgz", + "integrity": "sha512-3Qe53hXpyjtx6fFcxt/NTAlauIawsGmCZJPauV5sAnSKVuX8C82C1zMAZTeJt6m2dnd71wythc98BXUXsx/UxQ==", + "license": "MIT", + "dependencies": { + "@ionic/core": "7.8.6", + "ionicons": "^7.0.0", + "jsonc-parser": "^3.0.0", + "tslib": "^2.3.0" + }, + "peerDependencies": { + "@angular/core": ">=14.0.0", + "@angular/forms": ">=14.0.0", + "@angular/router": ">=14.0.0", + "rxjs": ">=7.5.0", + "zone.js": ">=0.11.0" + } + }, + "node_modules/@ionic/core": { + "version": "7.8.6", + "resolved": "https://registry.npmjs.org/@ionic/core/-/core-7.8.6.tgz", + "integrity": "sha512-HAYZdEmeJgOdo2kDlZkcCGHb+zs/vjU6iv4skbVBL7y+OnSv/oC2u83Yee8S3/aY0YAxkyBgu7hLTYH13Zc2Aw==", + "license": "MIT", + "dependencies": { + "@stencil/core": "^4.12.2", + "ionicons": "^7.2.2", + "tslib": "^2.1.0" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": 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/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": 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/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": 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/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.1.1.tgz", + "integrity": "sha512-sQXCasFk+U8lWYEe66WxRDOE9PjVz4vSM51fTu3Hw+ClTpUSQb718772vH3pyS5pShp6lvQM7SxgIDXXXmOX7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/set-array": "^1.0.0", + "@jridgewell/sourcemap-codec": "^1.4.10" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.6.tgz", + "integrity": "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, + "node_modules/@jridgewell/source-map/node_modules/@jridgewell/gen-mapping": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", + "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", + "license": "MIT", + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@leichtgewicht/ip-codec": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz", + "integrity": "sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@material/animation": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/animation/-/animation-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-5osi1z4JQIXcklPALbH/zTfOm2pDzHt9Fxm7ZyURy250xIZj6QjULRzPTnzOhC2ropfix9ra2Cfggbf0dcRbEQ==", + "license": "MIT", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@material/auto-init": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/auto-init/-/auto-init-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-OigQTmrVzkcGvxNjOaIe5oItTFPgrO9xLewvharDI6m6yvO1z7OBnkcW+sFN6ggLNYNxd0O1u9v64vMsmeDABQ==", + "license": "MIT", + "dependencies": { + "@material/base": "15.0.0-canary.684e33d25.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/banner": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/banner/-/banner-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-PqtGp3KWzdu58rWv/DIvSfe38m5YKOBbAAbBinSvgadBb/da+IE1t5F7YPNKE1T5lJsQBGVUYx6QBIeXm+aI/A==", + "license": "MIT", + "dependencies": { + "@material/base": "15.0.0-canary.684e33d25.0", + "@material/button": "15.0.0-canary.684e33d25.0", + "@material/dom": "15.0.0-canary.684e33d25.0", + "@material/elevation": "15.0.0-canary.684e33d25.0", + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "@material/ripple": "15.0.0-canary.684e33d25.0", + "@material/rtl": "15.0.0-canary.684e33d25.0", + "@material/shape": "15.0.0-canary.684e33d25.0", + "@material/theme": "15.0.0-canary.684e33d25.0", + "@material/tokens": "15.0.0-canary.684e33d25.0", + "@material/typography": "15.0.0-canary.684e33d25.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/base": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/base/-/base-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-oOaqb/SfjWwTKsdJUZmeh/Qrs41nIJI0N+zELsxnvbGjSIN1ZMAKYZFPMahqvC68OJ6+5CvJM8PoTNs5l+B8IQ==", + "license": "MIT", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@material/button": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/button/-/button-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-Nkekk4edeX+ObVOa7UlwavaHdmckPV5wU4SAJf3iA3R61cmz+KsgAgpzfcwv5WfNhIlc2nLu8QYEecpHdo9d/w==", + "license": "MIT", + "dependencies": { + "@material/density": "15.0.0-canary.684e33d25.0", + "@material/dom": "15.0.0-canary.684e33d25.0", + "@material/elevation": "15.0.0-canary.684e33d25.0", + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "@material/focus-ring": "15.0.0-canary.684e33d25.0", + "@material/ripple": "15.0.0-canary.684e33d25.0", + "@material/rtl": "15.0.0-canary.684e33d25.0", + "@material/shape": "15.0.0-canary.684e33d25.0", + "@material/theme": "15.0.0-canary.684e33d25.0", + "@material/tokens": "15.0.0-canary.684e33d25.0", + "@material/touch-target": "15.0.0-canary.684e33d25.0", + "@material/typography": "15.0.0-canary.684e33d25.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/card": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/card/-/card-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-xhyB7XX5KkEiCEqwSPkl58ZGYL6xFdnY62zimyBXJRG/Eaa0Swj3kW20hVCpt4f7c9Zmp8Se27rg8vnKmhvO3g==", + "license": "MIT", + "dependencies": { + "@material/dom": "15.0.0-canary.684e33d25.0", + "@material/elevation": "15.0.0-canary.684e33d25.0", + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "@material/ripple": "15.0.0-canary.684e33d25.0", + "@material/rtl": "15.0.0-canary.684e33d25.0", + "@material/shape": "15.0.0-canary.684e33d25.0", + "@material/theme": "15.0.0-canary.684e33d25.0", + "@material/tokens": "15.0.0-canary.684e33d25.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/checkbox": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/checkbox/-/checkbox-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-NFpM3TS924PmVsk2KQLNU95OYCf8ZwYgzeqfnAexU0bEfjUJXINBun2Go0AaeOUMjuvWUe+byjrXgv8SFYbMUA==", + "license": "MIT", + "dependencies": { + "@material/animation": "15.0.0-canary.684e33d25.0", + "@material/base": "15.0.0-canary.684e33d25.0", + "@material/density": "15.0.0-canary.684e33d25.0", + "@material/dom": "15.0.0-canary.684e33d25.0", + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "@material/focus-ring": "15.0.0-canary.684e33d25.0", + "@material/ripple": "15.0.0-canary.684e33d25.0", + "@material/theme": "15.0.0-canary.684e33d25.0", + "@material/touch-target": "15.0.0-canary.684e33d25.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/chips": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/chips/-/chips-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-z4ajQ4NnsAQ/Si9tZ4xmxzjj2Qb+vW++4QjCjjjwAGIZbCe0xglAnMh2t66XLJUxt7RoKZuZVEO7ZqcFZpvJFQ==", + "license": "MIT", + "dependencies": { + "@material/animation": "15.0.0-canary.684e33d25.0", + "@material/base": "15.0.0-canary.684e33d25.0", + "@material/checkbox": "15.0.0-canary.684e33d25.0", + "@material/density": "15.0.0-canary.684e33d25.0", + "@material/dom": "15.0.0-canary.684e33d25.0", + "@material/elevation": "15.0.0-canary.684e33d25.0", + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "@material/focus-ring": "15.0.0-canary.684e33d25.0", + "@material/ripple": "15.0.0-canary.684e33d25.0", + "@material/rtl": "15.0.0-canary.684e33d25.0", + "@material/shape": "15.0.0-canary.684e33d25.0", + "@material/theme": "15.0.0-canary.684e33d25.0", + "@material/tokens": "15.0.0-canary.684e33d25.0", + "@material/touch-target": "15.0.0-canary.684e33d25.0", + "@material/typography": "15.0.0-canary.684e33d25.0", + "safevalues": "^0.3.4", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/circular-progress": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/circular-progress/-/circular-progress-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-G6qD0nGNtEUwWnAMJuA9INYFpZoKtx7KFjBaPF4Ol2YLHtmShALNAYyn54TMAK8AZ2IpW08PXjGS7Ye88vrdEQ==", + "license": "MIT", + "dependencies": { + "@material/animation": "15.0.0-canary.684e33d25.0", + "@material/base": "15.0.0-canary.684e33d25.0", + "@material/dom": "15.0.0-canary.684e33d25.0", + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "@material/progress-indicator": "15.0.0-canary.684e33d25.0", + "@material/rtl": "15.0.0-canary.684e33d25.0", + "@material/theme": "15.0.0-canary.684e33d25.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/data-table": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/data-table/-/data-table-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-+wDw1DDDFfAsKAMzs84f/5GCjux39zjNfW8tL4wFbkWNwewmQrG9zaQMJhBpVOtLCrM8Gj6SOgOANqgqoCjvGg==", + "license": "MIT", + "dependencies": { + "@material/animation": "15.0.0-canary.684e33d25.0", + "@material/base": "15.0.0-canary.684e33d25.0", + "@material/checkbox": "15.0.0-canary.684e33d25.0", + "@material/density": "15.0.0-canary.684e33d25.0", + "@material/dom": "15.0.0-canary.684e33d25.0", + "@material/elevation": "15.0.0-canary.684e33d25.0", + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "@material/icon-button": "15.0.0-canary.684e33d25.0", + "@material/linear-progress": "15.0.0-canary.684e33d25.0", + "@material/list": "15.0.0-canary.684e33d25.0", + "@material/menu": "15.0.0-canary.684e33d25.0", + "@material/rtl": "15.0.0-canary.684e33d25.0", + "@material/select": "15.0.0-canary.684e33d25.0", + "@material/shape": "15.0.0-canary.684e33d25.0", + "@material/theme": "15.0.0-canary.684e33d25.0", + "@material/tokens": "15.0.0-canary.684e33d25.0", + "@material/touch-target": "15.0.0-canary.684e33d25.0", + "@material/typography": "15.0.0-canary.684e33d25.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/density": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/density/-/density-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-661yEVRMGrlq6S6WuSbPRO+ZwpdUOg2glCc7y96doM6itSLOa3UEAldjOLfsYZVB74GnKCiuDp//QmfoRyYTfA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@material/dialog": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/dialog/-/dialog-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-szn0dHnfeQTSOC6SSRSGAzX6Tnx+4NnSMUwNkXm+3bwjds8ZVK26+DXwLrP5f3ID5F1K5sFsRf2INo5/TNTHyQ==", + "license": "MIT", + "dependencies": { + "@material/animation": "15.0.0-canary.684e33d25.0", + "@material/base": "15.0.0-canary.684e33d25.0", + "@material/button": "15.0.0-canary.684e33d25.0", + "@material/dom": "15.0.0-canary.684e33d25.0", + "@material/elevation": "15.0.0-canary.684e33d25.0", + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "@material/icon-button": "15.0.0-canary.684e33d25.0", + "@material/ripple": "15.0.0-canary.684e33d25.0", + "@material/rtl": "15.0.0-canary.684e33d25.0", + "@material/shape": "15.0.0-canary.684e33d25.0", + "@material/theme": "15.0.0-canary.684e33d25.0", + "@material/tokens": "15.0.0-canary.684e33d25.0", + "@material/touch-target": "15.0.0-canary.684e33d25.0", + "@material/typography": "15.0.0-canary.684e33d25.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/dom": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/dom/-/dom-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-7pEJLYov+tGgfuD8mZxoVU6rWtPI8ppjTAhz+F27Hz9FG0JETMWTKpDPBXLnKvX7vhIxL83GvZ9geNHCe8Hfog==", + "license": "MIT", + "dependencies": { + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "@material/rtl": "15.0.0-canary.684e33d25.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/drawer": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/drawer/-/drawer-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-/KMckLf1PYU/H3PXnS4e0aFl03qG3JlSv4LGgX6juJufcONqGTl/m63EMO/L/eUy6H1CRrXmVDjik/jzHLyDhg==", + "license": "MIT", + "dependencies": { + "@material/animation": "15.0.0-canary.684e33d25.0", + "@material/base": "15.0.0-canary.684e33d25.0", + "@material/dom": "15.0.0-canary.684e33d25.0", + "@material/elevation": "15.0.0-canary.684e33d25.0", + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "@material/list": "15.0.0-canary.684e33d25.0", + "@material/ripple": "15.0.0-canary.684e33d25.0", + "@material/rtl": "15.0.0-canary.684e33d25.0", + "@material/shape": "15.0.0-canary.684e33d25.0", + "@material/theme": "15.0.0-canary.684e33d25.0", + "@material/typography": "15.0.0-canary.684e33d25.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/elevation": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/elevation/-/elevation-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-WDF8SsRtq3rXUbVVbd9K4DUijIPH0bUFSOreVYxudpuxAfTlDS5+aeS1EK9UIBFYLuba4u5wVT2tDv6e1RTfrQ==", + "license": "MIT", + "dependencies": { + "@material/animation": "15.0.0-canary.684e33d25.0", + "@material/base": "15.0.0-canary.684e33d25.0", + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "@material/rtl": "15.0.0-canary.684e33d25.0", + "@material/theme": "15.0.0-canary.684e33d25.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/fab": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/fab/-/fab-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-KCu87rWOKEAe9vZcAm6K8XazYSWPNjMG+OhrbPjHW6bCO7as1YCgtmkBkhff7csY/rFmcVpIy884xtUfLmSudQ==", + "license": "MIT", + "dependencies": { + "@material/animation": "15.0.0-canary.684e33d25.0", + "@material/dom": "15.0.0-canary.684e33d25.0", + "@material/elevation": "15.0.0-canary.684e33d25.0", + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "@material/focus-ring": "15.0.0-canary.684e33d25.0", + "@material/ripple": "15.0.0-canary.684e33d25.0", + "@material/rtl": "15.0.0-canary.684e33d25.0", + "@material/shape": "15.0.0-canary.684e33d25.0", + "@material/theme": "15.0.0-canary.684e33d25.0", + "@material/tokens": "15.0.0-canary.684e33d25.0", + "@material/touch-target": "15.0.0-canary.684e33d25.0", + "@material/typography": "15.0.0-canary.684e33d25.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/feature-targeting": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/feature-targeting/-/feature-targeting-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-HyH1erNTSjS63sigNSUMaCd0nJhTNdDFeC+myrxwtDaQm+uYJ8troCNtQM3g6mx0XATNtX5aTOoPmrM6yVVi1A==", + "license": "MIT", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@material/floating-label": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/floating-label/-/floating-label-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-f7TPp6bKpGvV3sYYiZHSGlrixXKkXXITW3Esp7KB9jRq42c0H82novmdwvY0eTef4ootmA2JEysr78KQfHBUPg==", + "license": "MIT", + "dependencies": { + "@material/animation": "15.0.0-canary.684e33d25.0", + "@material/base": "15.0.0-canary.684e33d25.0", + "@material/dom": "15.0.0-canary.684e33d25.0", + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "@material/rtl": "15.0.0-canary.684e33d25.0", + "@material/theme": "15.0.0-canary.684e33d25.0", + "@material/typography": "15.0.0-canary.684e33d25.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/focus-ring": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/focus-ring/-/focus-ring-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-ikw2RVUfgzXChpWIzPH1VzRvTjYb5ZKj4H+CZf7jqPUXMstFOZg90Bp7ARLZHqYiyNMuUq3zUTHozS6iHorSqg==", + "license": "MIT", + "dependencies": { + "@material/dom": "15.0.0-canary.684e33d25.0", + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "@material/rtl": "15.0.0-canary.684e33d25.0" + } + }, + "node_modules/@material/form-field": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/form-field/-/form-field-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-vpF9N/uq5no/7+8GAbEH0868FhOuBgxAWRr1Sfb+jthKfBr8OS/wPU/AHzZHdHdAm7PQynbeOXfDsX2dI//PDA==", + "license": "MIT", + "dependencies": { + "@material/base": "15.0.0-canary.684e33d25.0", + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "@material/ripple": "15.0.0-canary.684e33d25.0", + "@material/rtl": "15.0.0-canary.684e33d25.0", + "@material/theme": "15.0.0-canary.684e33d25.0", + "@material/typography": "15.0.0-canary.684e33d25.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/icon-button": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/icon-button/-/icon-button-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-wMI+XGzmIN/o2ePBKg2hLyx7H4pXCRAyyIKMQS1FMp1UKa2tYmiHVX/V8skhKwCqxg3i6Ls/LxMjfPxTR18WvQ==", + "license": "MIT", + "dependencies": { + "@material/base": "15.0.0-canary.684e33d25.0", + "@material/density": "15.0.0-canary.684e33d25.0", + "@material/dom": "15.0.0-canary.684e33d25.0", + "@material/elevation": "15.0.0-canary.684e33d25.0", + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "@material/focus-ring": "15.0.0-canary.684e33d25.0", + "@material/ripple": "15.0.0-canary.684e33d25.0", + "@material/rtl": "15.0.0-canary.684e33d25.0", + "@material/theme": "15.0.0-canary.684e33d25.0", + "@material/touch-target": "15.0.0-canary.684e33d25.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/image-list": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/image-list/-/image-list-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-Ol+uaHYBe5R/cgzlfh5ONnMVX0wO6fV74JMUcQCQlxP6lXau/edARo4tkRc7A7UJUkU3VRv0EpEjLoCRNUPGaA==", + "license": "MIT", + "dependencies": { + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "@material/shape": "15.0.0-canary.684e33d25.0", + "@material/theme": "15.0.0-canary.684e33d25.0", + "@material/typography": "15.0.0-canary.684e33d25.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/layout-grid": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/layout-grid/-/layout-grid-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-ALXE1mqFNb/RB2lVRQ3/r1Aufw2mFZnOjRE+boYDVepmAG/xWyPCyaGoavELJF5l4GAb0tXi8wA/8HeGbLOpuA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@material/line-ripple": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/line-ripple/-/line-ripple-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-7hRx8C/e9i0P6pgQpNOMfTwSS2r1fwEvBL72QDVGLtLuoKKwsjjgP6Z0Jat/GeHJe87u9LQvGBoD4upt+of/HA==", + "license": "MIT", + "dependencies": { + "@material/animation": "15.0.0-canary.684e33d25.0", + "@material/base": "15.0.0-canary.684e33d25.0", + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "@material/theme": "15.0.0-canary.684e33d25.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/linear-progress": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/linear-progress/-/linear-progress-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-iJclt7mKmcMk6pqD7ocXKfCWZhqBoODp7N593jYlxVpTJuEz2wiVAjZUDn/YGj/Uz3CRH+2YFfOiLr9pwWjhDg==", + "license": "MIT", + "dependencies": { + "@material/animation": "15.0.0-canary.684e33d25.0", + "@material/base": "15.0.0-canary.684e33d25.0", + "@material/dom": "15.0.0-canary.684e33d25.0", + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "@material/progress-indicator": "15.0.0-canary.684e33d25.0", + "@material/rtl": "15.0.0-canary.684e33d25.0", + "@material/theme": "15.0.0-canary.684e33d25.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/list": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/list/-/list-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-rQ+FCSdzmwTcT00IYE0uRV3CS4oGSccKFl9hkcF+aHFW61L7ORh/SCGUDPrEfQFrFkMn5f8qroVJjpUAMXBz4g==", + "license": "MIT", + "dependencies": { + "@material/base": "15.0.0-canary.684e33d25.0", + "@material/density": "15.0.0-canary.684e33d25.0", + "@material/dom": "15.0.0-canary.684e33d25.0", + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "@material/ripple": "15.0.0-canary.684e33d25.0", + "@material/rtl": "15.0.0-canary.684e33d25.0", + "@material/shape": "15.0.0-canary.684e33d25.0", + "@material/theme": "15.0.0-canary.684e33d25.0", + "@material/tokens": "15.0.0-canary.684e33d25.0", + "@material/typography": "15.0.0-canary.684e33d25.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/menu": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/menu/-/menu-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-r7wzDLSGSI9629/mfpvsMzkVxpmV75kcD3IrW0Pcu6/Bv/1xi0EvjcUXzNJJoQlwN4Zj35Ymz/PCjZkIDIz68Q==", + "license": "MIT", + "dependencies": { + "@material/base": "15.0.0-canary.684e33d25.0", + "@material/dom": "15.0.0-canary.684e33d25.0", + "@material/elevation": "15.0.0-canary.684e33d25.0", + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "@material/list": "15.0.0-canary.684e33d25.0", + "@material/menu-surface": "15.0.0-canary.684e33d25.0", + "@material/ripple": "15.0.0-canary.684e33d25.0", + "@material/rtl": "15.0.0-canary.684e33d25.0", + "@material/shape": "15.0.0-canary.684e33d25.0", + "@material/theme": "15.0.0-canary.684e33d25.0", + "@material/tokens": "15.0.0-canary.684e33d25.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/menu-surface": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/menu-surface/-/menu-surface-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-RVO5GAYcfWPaKwxsF/NhUAmrYXQCQBKvRQW0TIlbmAJz6lcFeTs6YZqF3u1C7qrL3ZQGz+sur/7ywj6QU0oMow==", + "license": "MIT", + "dependencies": { + "@material/animation": "15.0.0-canary.684e33d25.0", + "@material/base": "15.0.0-canary.684e33d25.0", + "@material/elevation": "15.0.0-canary.684e33d25.0", + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "@material/rtl": "15.0.0-canary.684e33d25.0", + "@material/shape": "15.0.0-canary.684e33d25.0", + "@material/theme": "15.0.0-canary.684e33d25.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/notched-outline": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/notched-outline/-/notched-outline-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-9YHcBkvJLPVYzkHcWoTpBZAFrEd+j1hjhGxLhh0LuNrZe8VroUkZD1TTnUAPHRG3os6EqEWWaKb0RN+aPIF2yQ==", + "license": "MIT", + "dependencies": { + "@material/base": "15.0.0-canary.684e33d25.0", + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "@material/floating-label": "15.0.0-canary.684e33d25.0", + "@material/rtl": "15.0.0-canary.684e33d25.0", + "@material/shape": "15.0.0-canary.684e33d25.0", + "@material/theme": "15.0.0-canary.684e33d25.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/progress-indicator": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/progress-indicator/-/progress-indicator-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-c0icji4faeNWUoqGENGC7Hav0Puxh0RwXIDVizffaUxKIGbajpIp5+4Zop73fK/xFLGMB/npg7TbP+aCGjQ3fw==", + "license": "MIT", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@material/radio": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/radio/-/radio-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-U3Eh8sNUA8trDla1Bq8Bo02foxYvtoewaKeF8A8tAju81XZ4jRiftfOsOWZDZEHCVbbCB2QwvutvFlnay5n+Aw==", + "license": "MIT", + "dependencies": { + "@material/animation": "15.0.0-canary.684e33d25.0", + "@material/base": "15.0.0-canary.684e33d25.0", + "@material/density": "15.0.0-canary.684e33d25.0", + "@material/dom": "15.0.0-canary.684e33d25.0", + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "@material/focus-ring": "15.0.0-canary.684e33d25.0", + "@material/ripple": "15.0.0-canary.684e33d25.0", + "@material/theme": "15.0.0-canary.684e33d25.0", + "@material/touch-target": "15.0.0-canary.684e33d25.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/ripple": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/ripple/-/ripple-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-RyePu7SjIm/OuyyEieZ/gxiPYkNZOZHeid72WRcN9ofdlljj2pifcdPvcfZA+v/DMS33xo5GjG2L/Qj6ClWrKw==", + "license": "MIT", + "dependencies": { + "@material/animation": "15.0.0-canary.684e33d25.0", + "@material/base": "15.0.0-canary.684e33d25.0", + "@material/dom": "15.0.0-canary.684e33d25.0", + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "@material/rtl": "15.0.0-canary.684e33d25.0", + "@material/theme": "15.0.0-canary.684e33d25.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/rtl": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/rtl/-/rtl-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-NqdJl8Ayupp1Th+vCNCpVQHbUFOuF7TCte9LD1norTIBUF/QizIxWby2W5uUEiPbnh5j9PmE1CJtfLwKun3pcw==", + "license": "MIT", + "dependencies": { + "@material/theme": "15.0.0-canary.684e33d25.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/segmented-button": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/segmented-button/-/segmented-button-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-bEGgg8vgXNLyukyV8HRjFMuQ6t6nm5LQ4Pgm22um61Yc8qyi0BOqV41OR4SVdUrUqZxh1aVD+p+4NN03+LfQXw==", + "license": "MIT", + "dependencies": { + "@material/base": "15.0.0-canary.684e33d25.0", + "@material/elevation": "15.0.0-canary.684e33d25.0", + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "@material/ripple": "15.0.0-canary.684e33d25.0", + "@material/theme": "15.0.0-canary.684e33d25.0", + "@material/touch-target": "15.0.0-canary.684e33d25.0", + "@material/typography": "15.0.0-canary.684e33d25.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/select": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/select/-/select-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-kf178/2TeEinTv0mgmSBcmmExQ2h7a7dtR1E3WuqQgisJ/R6+zVLMkC2CnfIyzxYX2vkuUTG0ue3Reh/6XiqSg==", + "license": "MIT", + "dependencies": { + "@material/animation": "15.0.0-canary.684e33d25.0", + "@material/base": "15.0.0-canary.684e33d25.0", + "@material/density": "15.0.0-canary.684e33d25.0", + "@material/dom": "15.0.0-canary.684e33d25.0", + "@material/elevation": "15.0.0-canary.684e33d25.0", + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "@material/floating-label": "15.0.0-canary.684e33d25.0", + "@material/line-ripple": "15.0.0-canary.684e33d25.0", + "@material/list": "15.0.0-canary.684e33d25.0", + "@material/menu": "15.0.0-canary.684e33d25.0", + "@material/menu-surface": "15.0.0-canary.684e33d25.0", + "@material/notched-outline": "15.0.0-canary.684e33d25.0", + "@material/ripple": "15.0.0-canary.684e33d25.0", + "@material/rtl": "15.0.0-canary.684e33d25.0", + "@material/shape": "15.0.0-canary.684e33d25.0", + "@material/theme": "15.0.0-canary.684e33d25.0", + "@material/tokens": "15.0.0-canary.684e33d25.0", + "@material/typography": "15.0.0-canary.684e33d25.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/shape": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/shape/-/shape-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-aEelpaTFmpnCji3TUGP9bVCS/bRVjUmLTHBPZtuu1gOrUVVtJ6kYOg73dZNJF+XOoNL2yOX/LRcKwsop29tptA==", + "license": "MIT", + "dependencies": { + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "@material/rtl": "15.0.0-canary.684e33d25.0", + "@material/theme": "15.0.0-canary.684e33d25.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/slider": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/slider/-/slider-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-WVyK+2pSNSZmj07M2K/a3TADoQ9FBCndfNC/vE7/wGIg4dddJJK5KvQ+yruf9R2cSzTL/S1sZ5WpyyeM8E9HTw==", + "license": "MIT", + "dependencies": { + "@material/animation": "15.0.0-canary.684e33d25.0", + "@material/base": "15.0.0-canary.684e33d25.0", + "@material/dom": "15.0.0-canary.684e33d25.0", + "@material/elevation": "15.0.0-canary.684e33d25.0", + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "@material/ripple": "15.0.0-canary.684e33d25.0", + "@material/rtl": "15.0.0-canary.684e33d25.0", + "@material/theme": "15.0.0-canary.684e33d25.0", + "@material/tokens": "15.0.0-canary.684e33d25.0", + "@material/typography": "15.0.0-canary.684e33d25.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/snackbar": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/snackbar/-/snackbar-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-itO+DCkOannZzR1/cCHcqAm7ifhuFvXmDItNoA8qLEcAyJDJJRkhpwj3XQ01yuo9gBFcSctp7Txt7e+Hncm/Jg==", + "license": "MIT", + "dependencies": { + "@material/animation": "15.0.0-canary.684e33d25.0", + "@material/base": "15.0.0-canary.684e33d25.0", + "@material/button": "15.0.0-canary.684e33d25.0", + "@material/dom": "15.0.0-canary.684e33d25.0", + "@material/elevation": "15.0.0-canary.684e33d25.0", + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "@material/icon-button": "15.0.0-canary.684e33d25.0", + "@material/ripple": "15.0.0-canary.684e33d25.0", + "@material/rtl": "15.0.0-canary.684e33d25.0", + "@material/shape": "15.0.0-canary.684e33d25.0", + "@material/theme": "15.0.0-canary.684e33d25.0", + "@material/tokens": "15.0.0-canary.684e33d25.0", + "@material/typography": "15.0.0-canary.684e33d25.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/switch": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/switch/-/switch-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-Jxi0gl92yvvZZsAPxvVHzXx2ga+T/djMow98jvEczmpUorWnAhgiCr9CsSSRoosahWyRB8NLZOxUQrACxvffjw==", + "license": "MIT", + "dependencies": { + "@material/animation": "15.0.0-canary.684e33d25.0", + "@material/base": "15.0.0-canary.684e33d25.0", + "@material/density": "15.0.0-canary.684e33d25.0", + "@material/dom": "15.0.0-canary.684e33d25.0", + "@material/elevation": "15.0.0-canary.684e33d25.0", + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "@material/focus-ring": "15.0.0-canary.684e33d25.0", + "@material/ripple": "15.0.0-canary.684e33d25.0", + "@material/rtl": "15.0.0-canary.684e33d25.0", + "@material/shape": "15.0.0-canary.684e33d25.0", + "@material/theme": "15.0.0-canary.684e33d25.0", + "@material/tokens": "15.0.0-canary.684e33d25.0", + "safevalues": "^0.3.4", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/tab": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/tab/-/tab-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-WQL3wj9syHNcfe8KbgGGUcA34M8C/xZ+n0Fkkh8Kk6puVwaU+xqUNihsxPY6YzKpmh4PZ4oJaBdiN8zvFT1zqQ==", + "license": "MIT", + "dependencies": { + "@material/base": "15.0.0-canary.684e33d25.0", + "@material/elevation": "15.0.0-canary.684e33d25.0", + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "@material/focus-ring": "15.0.0-canary.684e33d25.0", + "@material/ripple": "15.0.0-canary.684e33d25.0", + "@material/rtl": "15.0.0-canary.684e33d25.0", + "@material/tab-indicator": "15.0.0-canary.684e33d25.0", + "@material/theme": "15.0.0-canary.684e33d25.0", + "@material/tokens": "15.0.0-canary.684e33d25.0", + "@material/typography": "15.0.0-canary.684e33d25.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/tab-bar": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/tab-bar/-/tab-bar-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-SW/cMaDsIGGkM1ag3A7GJRlmr8eXmObWsvitQJzh6Azr5zzZtSI+GQygkMesAEE1gbpqOVN8d40rh3H7VVIAcA==", + "license": "MIT", + "dependencies": { + "@material/animation": "15.0.0-canary.684e33d25.0", + "@material/base": "15.0.0-canary.684e33d25.0", + "@material/density": "15.0.0-canary.684e33d25.0", + "@material/elevation": "15.0.0-canary.684e33d25.0", + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "@material/tab": "15.0.0-canary.684e33d25.0", + "@material/tab-indicator": "15.0.0-canary.684e33d25.0", + "@material/tab-scroller": "15.0.0-canary.684e33d25.0", + "@material/theme": "15.0.0-canary.684e33d25.0", + "@material/tokens": "15.0.0-canary.684e33d25.0", + "@material/typography": "15.0.0-canary.684e33d25.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/tab-indicator": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/tab-indicator/-/tab-indicator-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-kKICqSPqOlaf0lzaFFCmuOqPXJC+cK48Qmsc+m5o6fJhkmuZRCYpIwB2JeP+uZSOq/bTH+SrPtCtnVlgWg6ksA==", + "license": "MIT", + "dependencies": { + "@material/animation": "15.0.0-canary.684e33d25.0", + "@material/base": "15.0.0-canary.684e33d25.0", + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "@material/theme": "15.0.0-canary.684e33d25.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/tab-scroller": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/tab-scroller/-/tab-scroller-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-H6EU/TSiK/M2DyyORX5GEtXD9rKYxTMHC2VxsNWARPMFJGzgeW2ugYkFv+rKI1/c0bs0CJ4e+qFnOlBsQXZvyQ==", + "license": "MIT", + "dependencies": { + "@material/animation": "15.0.0-canary.684e33d25.0", + "@material/base": "15.0.0-canary.684e33d25.0", + "@material/dom": "15.0.0-canary.684e33d25.0", + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "@material/tab": "15.0.0-canary.684e33d25.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/textfield": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/textfield/-/textfield-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-OvgpDXjvpyJTtAWskO69IDybFvDNzr9w2PN/Fk7yFm+uNVupaWz1Ew8lZ4gGslaTNSVmh2XcsvmzxcLINSiiNg==", + "license": "MIT", + "dependencies": { + "@material/animation": "15.0.0-canary.684e33d25.0", + "@material/base": "15.0.0-canary.684e33d25.0", + "@material/density": "15.0.0-canary.684e33d25.0", + "@material/dom": "15.0.0-canary.684e33d25.0", + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "@material/floating-label": "15.0.0-canary.684e33d25.0", + "@material/line-ripple": "15.0.0-canary.684e33d25.0", + "@material/notched-outline": "15.0.0-canary.684e33d25.0", + "@material/ripple": "15.0.0-canary.684e33d25.0", + "@material/rtl": "15.0.0-canary.684e33d25.0", + "@material/shape": "15.0.0-canary.684e33d25.0", + "@material/theme": "15.0.0-canary.684e33d25.0", + "@material/tokens": "15.0.0-canary.684e33d25.0", + "@material/typography": "15.0.0-canary.684e33d25.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/theme": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/theme/-/theme-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-AZxaXXAvRKzAi20RlMxzt2U5UmkCWyv7DMWEBXsxtG5Tk54mi1HsbVUp3fxDPTlmL7Pq8p1/DESg/o7TgRCVlw==", + "license": "MIT", + "dependencies": { + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/tokens": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/tokens/-/tokens-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-wVwbQOTCXDPKYPdHQHLr026y36MMFelID1CmbfRk6mSol4O8yE9U0fXcShfRDW8Qo5E3X31w9c2A6T3neJY7wQ==", + "license": "MIT", + "dependencies": { + "@material/elevation": "15.0.0-canary.684e33d25.0" + } + }, + "node_modules/@material/tooltip": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/tooltip/-/tooltip-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-dtm26QjxyQdinc8btgz6yys07b7bUW4FZgNF2EBPeGrICrPg7jf+JEvDziz5g8VMaTBQLOQRSCGy0MKuRlOjLw==", + "license": "MIT", + "dependencies": { + "@material/animation": "15.0.0-canary.684e33d25.0", + "@material/base": "15.0.0-canary.684e33d25.0", + "@material/button": "15.0.0-canary.684e33d25.0", + "@material/dom": "15.0.0-canary.684e33d25.0", + "@material/elevation": "15.0.0-canary.684e33d25.0", + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "@material/rtl": "15.0.0-canary.684e33d25.0", + "@material/shape": "15.0.0-canary.684e33d25.0", + "@material/theme": "15.0.0-canary.684e33d25.0", + "@material/tokens": "15.0.0-canary.684e33d25.0", + "@material/typography": "15.0.0-canary.684e33d25.0", + "safevalues": "^0.3.4", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/top-app-bar": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/top-app-bar/-/top-app-bar-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-1M+oupUxflfW7u81P1XlxoLZB8bLzwtpKofIfDNRbEsiKhlLTERJR3Yak3BGE9xakNMysAaBHlkb5MrN5bNPFw==", + "license": "MIT", + "dependencies": { + "@material/animation": "15.0.0-canary.684e33d25.0", + "@material/base": "15.0.0-canary.684e33d25.0", + "@material/elevation": "15.0.0-canary.684e33d25.0", + "@material/ripple": "15.0.0-canary.684e33d25.0", + "@material/rtl": "15.0.0-canary.684e33d25.0", + "@material/shape": "15.0.0-canary.684e33d25.0", + "@material/theme": "15.0.0-canary.684e33d25.0", + "@material/typography": "15.0.0-canary.684e33d25.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/touch-target": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/touch-target/-/touch-target-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-zdE69Slg8+T7sTn1OwqZ6H7WBYac9mxJ/JlJqfTqthzIjZRcCxBSYymQJcDHjsrPnUojOtr9U4Tpm5YZ96TEkQ==", + "license": "MIT", + "dependencies": { + "@material/base": "15.0.0-canary.684e33d25.0", + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "@material/rtl": "15.0.0-canary.684e33d25.0", + "@material/theme": "15.0.0-canary.684e33d25.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/typography": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/typography/-/typography-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-aVnvgMwcfNa/K4wujzpKDIxjGl2hbkEL+m+OKDSQqWYjKcP9QrbzCXJruJBqxrBoPRHLbqo47k5f9uT8raSgjw==", + "license": "MIT", + "dependencies": { + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "@material/theme": "15.0.0-canary.684e33d25.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@ng-idle/core": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/@ng-idle/core/-/core-14.0.0.tgz", + "integrity": "sha512-DqseA+F6LXm/Hr4D8zQhDBliuAU06t9BOUpcdSUKQWdDEKo3mlovRxz7DMuXyzg4OBQ0saQpE3MeMqakIEHUHw==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.0.0" + }, + "peerDependencies": { + "@angular/common": ">=9.0.0", + "@angular/core": ">=9.0.0" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@npmcli/fs": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-3.1.1.tgz", + "integrity": "sha512-q9CRWjpHCMIh5sVyefoD1cA7PkvILqCZsnSOEUUivORLjxCO/Irmue2DprETiNgEqktDBZaM1Bi+jrarx1XdCg==", + "dev": true, + "license": "ISC", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/git": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@npmcli/git/-/git-4.1.0.tgz", + "integrity": "sha512-9hwoB3gStVfa0N31ymBmrX+GuDGdVA/QWShZVqE0HK2Af+7QGGrCTbZia/SW0ImUTjTne7SP91qxDmtXvDHRPQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/promise-spawn": "^6.0.0", + "lru-cache": "^7.4.4", + "npm-pick-manifest": "^8.0.0", + "proc-log": "^3.0.0", + "promise-inflight": "^1.0.1", + "promise-retry": "^2.0.1", + "semver": "^7.3.5", + "which": "^3.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/git/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/@npmcli/git/node_modules/which": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/which/-/which-3.0.1.tgz", + "integrity": "sha512-XA1b62dzQzLfaEOSQFTCOd5KFf/1VSzZo7/7TUjnya6u0vGGKzU96UQBZTAThCb2j4/xjBAyii1OhRLJEivHvg==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/installed-package-contents": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@npmcli/installed-package-contents/-/installed-package-contents-2.1.0.tgz", + "integrity": "sha512-c8UuGLeZpm69BryRykLuKRyKFZYJsZSCT4aVY5ds4omyZqJ172ApzgfKJ5eV/r3HgLdUYgFVe54KSFVjKoe27w==", + "dev": true, + "license": "ISC", + "dependencies": { + "npm-bundled": "^3.0.0", + "npm-normalize-package-bin": "^3.0.0" + }, + "bin": { + "installed-package-contents": "bin/index.js" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/move-file": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@npmcli/move-file/-/move-file-2.0.1.tgz", + "integrity": "sha512-mJd2Z5TjYWq/ttPLLGqArdtnC74J6bOzg4rMDnN+p1xTacZ2yPRCk2y0oSWQtygLR9YVQXgOcONrwtnk3JupxQ==", + "deprecated": "This functionality has been moved to @npmcli/fs", + "dev": true, + "license": "MIT", + "dependencies": { + "mkdirp": "^1.0.4", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/@npmcli/move-file/node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true, + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@npmcli/node-gyp": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/node-gyp/-/node-gyp-3.0.0.tgz", + "integrity": "sha512-gp8pRXC2oOxu0DUE1/M3bYtb1b3/DbJ5aM113+XJBgfXdussRAsX0YOrOhdd8WvnAR6auDBvJomGAkLKA5ydxA==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/promise-spawn": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@npmcli/promise-spawn/-/promise-spawn-6.0.2.tgz", + "integrity": "sha512-gGq0NJkIGSwdbUt4yhdF8ZrmkGKVz9vAdVzpOfnom+V8PLSmSOVhZwbNvZZS1EYcJN5hzzKBxmmVVAInM6HQLg==", + "dev": true, + "license": "ISC", + "dependencies": { + "which": "^3.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/promise-spawn/node_modules/which": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/which/-/which-3.0.1.tgz", + "integrity": "sha512-XA1b62dzQzLfaEOSQFTCOd5KFf/1VSzZo7/7TUjnya6u0vGGKzU96UQBZTAThCb2j4/xjBAyii1OhRLJEivHvg==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/run-script": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@npmcli/run-script/-/run-script-6.0.2.tgz", + "integrity": "sha512-NCcr1uQo1k5U+SYlnIrbAh3cxy+OQT1VtqiAbxdymSlptbzBb62AjH2xXgjNCoP073hoa1CfCAcwoZ8k96C4nA==", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/node-gyp": "^3.0.0", + "@npmcli/promise-spawn": "^6.0.0", + "node-gyp": "^9.0.0", + "read-package-json-fast": "^3.0.0", + "which": "^3.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/run-script/node_modules/which": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/which/-/which-3.0.1.tgz", + "integrity": "sha512-XA1b62dzQzLfaEOSQFTCOd5KFf/1VSzZo7/7TUjnya6u0vGGKzU96UQBZTAThCb2j4/xjBAyii1OhRLJEivHvg==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@pkgr/core": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.1.1.tgz", + "integrity": "sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/unts" + } + }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", + "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1", + "@protobufjs/inquire": "^1.1.0" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/inquire": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", + "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", + "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==", + "license": "BSD-3-Clause" + }, + "node_modules/@schematics/angular": { + "version": "15.1.6", + "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-15.1.6.tgz", + "integrity": "sha512-y2kIQ1wJL0wR6v/LM5+PFJUivrYtdaIJVRdOXLLWl0AB5aLwObiWgLzAuBsbGm/9//WPPhw9PglS5EFFxTBDzg==", + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "15.1.6", + "@angular-devkit/schematics": "15.1.6", + "jsonc-parser": "3.2.0" + }, + "engines": { + "node": "^14.20.0 || ^16.13.0 || >=18.10.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@schematics/angular/node_modules/@angular-devkit/core": { + "version": "15.1.6", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-15.1.6.tgz", + "integrity": "sha512-jGgxyRjecVf6lEyqDxz7ltMEndNPxIg720pk6r40fgsu0dU8w9vjJSJe7k0XdJiXVRcN6wZa/J5nO/xcwWVIsA==", + "license": "MIT", + "dependencies": { + "ajv": "8.12.0", + "ajv-formats": "2.1.1", + "jsonc-parser": "3.2.0", + "rxjs": "6.6.7", + "source-map": "0.7.4" + }, + "engines": { + "node": "^14.20.0 || ^16.13.0 || >=18.10.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "chokidar": "^3.5.2" + }, + "peerDependenciesMeta": { + "chokidar": { + "optional": true + } + } + }, + "node_modules/@schematics/angular/node_modules/rxjs": { + "version": "6.6.7", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", + "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^1.9.0" + }, + "engines": { + "npm": ">=2.0.0" + } + }, + "node_modules/@schematics/angular/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "license": "0BSD" + }, + "node_modules/@socket.io/component-emitter": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", + "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@stencil/core": { + "version": "4.25.1", + "resolved": "https://registry.npmjs.org/@stencil/core/-/core-4.25.1.tgz", + "integrity": "sha512-SJhAAN6nHj7l2n2F7H6NoOUFhXC5tYXSvOpAaNFi5As6c2jOvaDOvAH+om4OKmw8U2kHI9yihEqpZrtY1J20dQ==", + "license": "MIT", + "bin": { + "stencil": "bin/stencil" + }, + "engines": { + "node": ">=16.0.0", + "npm": ">=7.10.0" + } + }, + "node_modules/@tootallnate/once": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", + "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/@types/body-parser": { + "version": "1.19.5", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", + "integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/bonjour": { + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@types/bonjour/-/bonjour-3.5.13.tgz", + "integrity": "sha512-z9fJ5Im06zvUL548KvYNecEVlA7cVDkGUi6kZusb04mpyEFKCIZJvloCcmpmLaIahDpOQGHaHmG6imtPMmPXGQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/connect-history-api-fallback": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.5.4.tgz", + "integrity": "sha512-n6Cr2xS1h4uAulPRdlw6Jl6s1oG8KrVilPN2yUITEs+K48EzMJJ3W1xy8K5eWuFvjp3R74AOIGSmp2UfBJ8HFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express-serve-static-core": "*", + "@types/node": "*" + } + }, + "node_modules/@types/cors": { + "version": "2.8.17", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz", + "integrity": "sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/eslint": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", + "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", + "license": "MIT", + "dependencies": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "node_modules/@types/eslint-scope": { + "version": "3.7.7", + "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", + "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", + "license": "MIT", + "dependencies": { + "@types/eslint": "*", + "@types/estree": "*" + } + }, + "node_modules/@types/estree": { + "version": "0.0.51", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.51.tgz", + "integrity": "sha512-CuPgU6f3eT/XgKKPqKd/gLZV1Xmvf1a2R5POBOGQa6uv82xpls89HU5zKeVoyR8XzHd1RGNOlQlvUe3CFkjWNQ==", + "license": "MIT" + }, + "node_modules/@types/express": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz", + "integrity": "sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.33", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.6.tgz", + "integrity": "sha512-3xhRnjJPkULekpSzgtoNYYcTWgEZkp4myc+Saevii5JPnHNvHMRlBSHDbs7Bh1iPPoVTERHEZXyhyLbMEsExsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/express/node_modules/@types/express-serve-static-core": { + "version": "4.19.6", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.6.tgz", + "integrity": "sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/http-errors": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", + "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/http-proxy": { + "version": "1.17.15", + "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.15.tgz", + "integrity": "sha512-25g5atgiVNTIv0LBDTg1H74Hvayx0ajtJPLLcYE3whFv75J0pWNtOBzaXJQgDTmrX1bx5U9YC2w/n65BN1HwRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/jasmine": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/@types/jasmine/-/jasmine-4.3.6.tgz", + "integrity": "sha512-3N0FpQTeiWjm+Oo1WUYWguUS7E6JLceiGTriFrG8k5PU7zRLJCzLcWURU3wjMbZGS//a2/LgjsnO3QxIlwxt9g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "license": "MIT" + }, + "node_modules/@types/long": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz", + "integrity": "sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==", + "license": "MIT" + }, + "node_modules/@types/marked": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@types/marked/-/marked-4.3.2.tgz", + "integrity": "sha512-a79Yc3TOk6dGdituy8hmTTJXjOkZ7zsFYV10L337ttq/rec8lRMDBpV7fL3uLx6TgbFCa5DU/h8FmIBQPSbU0w==", + "license": "MIT" + }, + "node_modules/@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/minimist": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-hov8bUuiLiyFPGyFPE1lwWhmzYbirOXQNNo40+y3zow8aFVTeyn3VWL0VFFfdNddA8S4Vf0Tc062rzyNr7Paag==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "20.12.7", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.7.tgz", + "integrity": "sha512-wq0cICSkRLVaf3UGLMGItu/PtdY7oaXaI/RVU+xliKVOtRna3PRY57ZDfztpDL0n11vfymMUnXv8QwYCO7L1wg==", + "license": "MIT", + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/@types/node-forge": { + "version": "1.3.11", + "resolved": "https://registry.npmjs.org/@types/node-forge/-/node-forge-1.3.11.tgz", + "integrity": "sha512-FQx220y22OKNTqaByeBGqHWYz4cl94tpcxeFdvBo3wjG6XPBuZ0BNgNZRV5J5TFmmcsJ4IzsLkmGRiQbnYsBEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/normalize-package-data": { + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz", + "integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/parse-json": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", + "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/qs": { + "version": "6.9.18", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.18.tgz", + "integrity": "sha512-kK7dgTYDyGqS+e2Q4aK9X3D7q234CIZ1Bv0q/7Z5IwRDoADNU81xXJK/YVyLbLTZCoIwUoDoffFeF+p/eIklAA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/semver": { + "version": "7.5.8", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.8.tgz", + "integrity": "sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/send": { + "version": "0.17.4", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", + "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/serve-index": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@types/serve-index/-/serve-index-1.9.4.tgz", + "integrity": "sha512-qLpGZ/c2fhSs5gnYsQxtDEq3Oy8SXPClIXkW5ghvAvsNuVSA8k+gCONcUCS/UjLEYvYps+e8uBtfgXgvhwfNug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.7", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.7.tgz", + "integrity": "sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "*" + } + }, + "node_modules/@types/sockjs": { + "version": "0.3.36", + "resolved": "https://registry.npmjs.org/@types/sockjs/-/sockjs-0.3.36.tgz", + "integrity": "sha512-MK9V6NzAS1+Ud7JV9lJLFqW85VbC9dq3LmwZCuBe4wBDgKC0Kj/jd8Xl+nSviU+Qc3+m7umHHyHg//2KSa0a0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/triple-beam": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.5.tgz", + "integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==", + "license": "MIT" + }, + "node_modules/@types/uuid": { + "version": "9.0.8", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.8.tgz", + "integrity": "sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/ws": { + "version": "8.5.14", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.14.tgz", + "integrity": "sha512-bd/YFLW+URhBzMXurx7lWByOu+xzU9+kb3RboOteXYDfW+tr+JZa99OyNmPINEGB/ahzKrEuc8rcv4gnpJmxTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.62.0.tgz", + "integrity": "sha512-TiZzBSJja/LbhNPvk6yc0JrX9XqhQ0hdh6M2svYfsHGejaKFIAGd9MQ+ERIMzLGlN/kZoYIgdxFV0PuljTKXag==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.4.0", + "@typescript-eslint/scope-manager": "5.62.0", + "@typescript-eslint/type-utils": "5.62.0", + "@typescript-eslint/utils": "5.62.0", + "debug": "^4.3.4", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "natural-compare-lite": "^1.4.0", + "semver": "^7.3.7", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^5.0.0", + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.62.0.tgz", + "integrity": "sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/scope-manager": "5.62.0", + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/typescript-estree": "5.62.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.62.0.tgz", + "integrity": "sha512-VXuvVvZeQCQb5Zgf4HAxc04q5j+WrNAtNh9OwCsCgpKqESMTu3tF/jhZ3xG6T4NZwWl65Bg8KuS2uEvhSfLl0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/visitor-keys": "5.62.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.62.0.tgz", + "integrity": "sha512-xsSQreu+VnfbqQpW5vnCJdq1Z3Q0U31qiWmRhr98ONQmcp/yhiPJFPq8MXiJVLiksmOKSjIldZzkebzHuCGzew==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/typescript-estree": "5.62.0", + "@typescript-eslint/utils": "5.62.0", + "debug": "^4.3.4", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/types": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.62.0.tgz", + "integrity": "sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.62.0.tgz", + "integrity": "sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/visitor-keys": "5.62.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "semver": "^7.3.7", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/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/@typescript-eslint/typescript-estree/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/@typescript-eslint/utils": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.62.0.tgz", + "integrity": "sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@types/json-schema": "^7.0.9", + "@types/semver": "^7.3.12", + "@typescript-eslint/scope-manager": "5.62.0", + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/typescript-estree": "5.62.0", + "eslint-scope": "^5.1.1", + "semver": "^7.3.7" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.62.0.tgz", + "integrity": "sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "5.62.0", + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", + "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", + "dev": true, + "license": "ISC" + }, + "node_modules/@webassemblyjs/ast": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.1.tgz", + "integrity": "sha512-ukBh14qFLjxTQNTXocdyksN5QdM28S1CxHt2rdskFyL+xFV7VremuBLVbmCePj+URalXBENx/9Lm7lnhihtCSw==", + "license": "MIT", + "dependencies": { + "@webassemblyjs/helper-numbers": "1.11.1", + "@webassemblyjs/helper-wasm-bytecode": "1.11.1" + } + }, + "node_modules/@webassemblyjs/floating-point-hex-parser": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.1.tgz", + "integrity": "sha512-iGRfyc5Bq+NnNuX8b5hwBrRjzf0ocrJPI6GWFodBFzmFnyvrQ83SHKhmilCU/8Jv67i4GJZBMhEzltxzcNagtQ==", + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-api-error": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.1.tgz", + "integrity": "sha512-RlhS8CBCXfRUR/cwo2ho9bkheSXG0+NwooXcc3PAILALf2QLdFyj7KGsKRbVc95hZnhnERon4kW/D3SZpp6Tcg==", + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-buffer": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.1.tgz", + "integrity": "sha512-gwikF65aDNeeXa8JxXa2BAk+REjSyhrNC9ZwdT0f8jc4dQQeDQ7G4m0f2QCLPJiMTTO6wfDmRmj/pW0PsUvIcA==", + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-numbers": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.1.tgz", + "integrity": "sha512-vDkbxiB8zfnPdNK9Rajcey5C0w+QJugEglN0of+kmO8l7lDb77AnlKYQF7aarZuCrv+l0UvqL+68gSDr3k9LPQ==", + "license": "MIT", + "dependencies": { + "@webassemblyjs/floating-point-hex-parser": "1.11.1", + "@webassemblyjs/helper-api-error": "1.11.1", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/helper-wasm-bytecode": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.1.tgz", + "integrity": "sha512-PvpoOGiJwXeTrSf/qfudJhwlvDQxFgelbMqtq52WWiXC6Xgg1IREdngmPN3bs4RoO83PnL/nFrxucXj1+BX62Q==", + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-wasm-section": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.1.tgz", + "integrity": "sha512-10P9No29rYX1j7F3EVPX3JvGPQPae+AomuSTPiF9eBQeChHI6iqjMIwR9JmOJXwpnn/oVGDk7I5IlskuMwU/pg==", + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.11.1", + "@webassemblyjs/helper-buffer": "1.11.1", + "@webassemblyjs/helper-wasm-bytecode": "1.11.1", + "@webassemblyjs/wasm-gen": "1.11.1" + } + }, + "node_modules/@webassemblyjs/ieee754": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.11.1.tgz", + "integrity": "sha512-hJ87QIPtAMKbFq6CGTkZYJivEwZDbQUgYd3qKSadTNOhVY7p+gfP6Sr0lLRVTaG1JjFj+r3YchoqRYxNH3M0GQ==", + "license": "MIT", + "dependencies": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "node_modules/@webassemblyjs/leb128": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.11.1.tgz", + "integrity": "sha512-BJ2P0hNZ0u+Th1YZXJpzW6miwqQUGcIHT1G/sf72gLVD9DZ5AdYTqPNbHZh6K1M5VmKvFXwGSWZADz+qBWxeRw==", + "license": "Apache-2.0", + "dependencies": { + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/utf8": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.11.1.tgz", + "integrity": "sha512-9kqcxAEdMhiwQkHpkNiorZzqpGrodQQ2IGrHHxCy+Ozng0ofyMA0lTqiLkVs1uzTRejX+/O0EOT7KxqVPuXosQ==", + "license": "MIT" + }, + "node_modules/@webassemblyjs/wasm-edit": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.1.tgz", + "integrity": "sha512-g+RsupUC1aTHfR8CDgnsVRVZFJqdkFHpsHMfJuWQzWU3tvnLC07UqHICfP+4XyL2tnr1amvl1Sdp06TnYCmVkA==", + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.11.1", + "@webassemblyjs/helper-buffer": "1.11.1", + "@webassemblyjs/helper-wasm-bytecode": "1.11.1", + "@webassemblyjs/helper-wasm-section": "1.11.1", + "@webassemblyjs/wasm-gen": "1.11.1", + "@webassemblyjs/wasm-opt": "1.11.1", + "@webassemblyjs/wasm-parser": "1.11.1", + "@webassemblyjs/wast-printer": "1.11.1" + } + }, + "node_modules/@webassemblyjs/wasm-gen": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.1.tgz", + "integrity": "sha512-F7QqKXwwNlMmsulj6+O7r4mmtAlCWfO/0HdgOxSklZfQcDu0TpLiD1mRt/zF25Bk59FIjEuGAIyn5ei4yMfLhA==", + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.11.1", + "@webassemblyjs/helper-wasm-bytecode": "1.11.1", + "@webassemblyjs/ieee754": "1.11.1", + "@webassemblyjs/leb128": "1.11.1", + "@webassemblyjs/utf8": "1.11.1" + } + }, + "node_modules/@webassemblyjs/wasm-opt": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.1.tgz", + "integrity": "sha512-VqnkNqnZlU5EB64pp1l7hdm3hmQw7Vgqa0KF/KCNO9sIpI6Fk6brDEiX+iCOYrvMuBWDws0NkTOxYEb85XQHHw==", + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.11.1", + "@webassemblyjs/helper-buffer": "1.11.1", + "@webassemblyjs/wasm-gen": "1.11.1", + "@webassemblyjs/wasm-parser": "1.11.1" + } + }, + "node_modules/@webassemblyjs/wasm-parser": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.1.tgz", + "integrity": "sha512-rrBujw+dJu32gYB7/Lup6UhdkPx9S9SnobZzRVL7VcBH9Bt9bCBLEuX/YXOOtBsOZ4NQrRykKhffRWHvigQvOA==", + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.11.1", + "@webassemblyjs/helper-api-error": "1.11.1", + "@webassemblyjs/helper-wasm-bytecode": "1.11.1", + "@webassemblyjs/ieee754": "1.11.1", + "@webassemblyjs/leb128": "1.11.1", + "@webassemblyjs/utf8": "1.11.1" + } + }, + "node_modules/@webassemblyjs/wast-printer": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.11.1.tgz", + "integrity": "sha512-IQboUWM4eKzWW+N/jij2sRatKMh99QEelo3Eb2q0qXkvPRISAj8Qxtmw5itwqK+TTkBuUIE45AxYPToqPtL5gg==", + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.11.1", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@xtuc/ieee754": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", + "license": "BSD-3-Clause" + }, + "node_modules/@xtuc/long": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", + "license": "Apache-2.0" + }, + "node_modules/@yarnpkg/lockfile": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz", + "integrity": "sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/abab": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", + "integrity": "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==", + "deprecated": "Use your platform's native atob() and btoa() methods instead", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "dev": true, + "license": "ISC" + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/accepts/node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.14.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", + "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-import-assertions": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.9.0.tgz", + "integrity": "sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA==", + "deprecated": "package has been renamed to acorn-import-attributes", + "license": "MIT", + "peerDependencies": { + "acorn": "^8" + } + }, + "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/adjust-sourcemap-loader": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/adjust-sourcemap-loader/-/adjust-sourcemap-loader-4.0.0.tgz", + "integrity": "sha512-OXwN5b9pCUXNQHJpwwD2qP40byEmSgzj8B4ydSN0uMNYWiFmJ6x6KwUllMmfk8Rwu/HJDFR7U8ubsWBoN0Xp0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "loader-utils": "^2.0.0", + "regex-parser": "^2.2.11" + }, + "engines": { + "node": ">=8.9" + } + }, + "node_modules/adjust-sourcemap-loader/node_modules/loader-utils": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", + "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" + }, + "engines": { + "node": ">=8.9.0" + } + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/agentkeepalive": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz", + "integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "humanize-ms": "^1.2.1" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/aggregate-error": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", + "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ajv": { + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", + "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/ansi-colors": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "license": "MIT", + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-html-community": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/ansi-html-community/-/ansi-html-community-0.0.8.tgz", + "integrity": "sha512-1APHAyr3+PCamwNw3bXCPp4HFLONZt/yIH0sZp0/469KWNTEy+qN5jQ3GVX6DMZ1UXAi34yVwtTeaG/HpBuuzw==", + "dev": true, + "engines": [ + "node >= 0.8.0" + ], + "license": "Apache-2.0", + "bin": { + "ansi-html": "bin/ansi-html" + } + }, + "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==", + "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==", + "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==", + "dev": true, + "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==", + "devOptional": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/aproba": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", + "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/are-we-there-yet": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-3.0.1.tgz", + "integrity": "sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg==", + "deprecated": "This package is no longer supported.", + "dev": true, + "license": "ISC", + "dependencies": { + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "dev": true, + "license": "MIT" + }, + "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/arrify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", + "integrity": "sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "license": "MIT" + }, + "node_modules/autoprefixer": { + "version": "10.4.13", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.13.tgz", + "integrity": "sha512-49vKpMqcZYsJjwotvt4+h/BCjJVnhGwcLpDt5xkcaOG3eLrG/HUYLagrihYsQ+qrIBgIzX1Rw7a6L8I/ZA1Atg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + } + ], + "license": "MIT", + "dependencies": { + "browserslist": "^4.21.4", + "caniuse-lite": "^1.0.30001426", + "fraction.js": "^4.2.0", + "normalize-range": "^0.1.2", + "picocolors": "^1.0.0", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/babel-loader": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-9.1.2.tgz", + "integrity": "sha512-mN14niXW43tddohGl8HPu5yfQq70iUThvFL/4QzESA7GcZoC0eVOhvWdQ8+3UlSjaDE9MVtsW9mxDY07W7VpVA==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-cache-dir": "^3.3.2", + "schema-utils": "^4.0.0" + }, + "engines": { + "node": ">= 14.15.0" + }, + "peerDependencies": { + "@babel/core": "^7.12.0", + "webpack": ">=5" + } + }, + "node_modules/babel-plugin-istanbul": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", + "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-instrument": "^5.0.4", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-polyfill-corejs2": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.3.3.tgz", + "integrity": "sha512-8hOdmFYFSZhqg2C/JgLUQ+t52o5nirNwaWM2B9LWteozwIvM14VSwdsCAUET10qT+kmySAlseadmfeeSWFCy+Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.17.7", + "@babel/helper-define-polyfill-provider": "^0.3.3", + "semver": "^6.1.1" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/babel-plugin-polyfill-corejs2/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-plugin-polyfill-corejs3": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.6.0.tgz", + "integrity": "sha512-+eHqR6OPcBhJOGgsIar7xoAB1GcSwVUA3XjAd7HJNzOXT4wv6/H7KIdA/Nc60cvUlDbKApmqNvD1B1bzOt4nyA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.3.3", + "core-js-compat": "^3.25.1" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/babel-plugin-polyfill-regenerator": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.4.1.tgz", + "integrity": "sha512-NtQGmyQDXjQqQ+IzRkBVwEOz9lQ4zxAQZgoAYEtU9dJjnl1Oc98qnN7jcp+bE7O7aYzVpavXE3/VKXNzUbh7aw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.3.3" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "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/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "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/base64id": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", + "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^4.5.0 || >= 5.9" + } + }, + "node_modules/batch": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz", + "integrity": "sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw==", + "dev": true, + "license": "MIT" + }, + "node_modules/big.js": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", + "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "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==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/body-parser": { + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", + "dev": true, + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.13.0", + "raw-body": "2.5.2", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/body-parser/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/body-parser/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "license": "MIT" + }, + "node_modules/bonjour-service": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/bonjour-service/-/bonjour-service-1.3.0.tgz", + "integrity": "sha512-3YuAUiSkWykd+2Azjgyxei8OWf8thdn8AITIog2M4UICzoqfjlqr64WIjEXZllf/W6vK1goqleSR6brGomxQqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "multicast-dns": "^7.2.5" + } + }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "dev": true, + "license": "ISC" + }, + "node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.21.5", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.5.tgz", + "integrity": "sha512-tUkiguQGW7S3IhB7N+c2MV/HZPSCPAAiYBZXLsBhFB/PCy6ZKKsZrmBayHV9fdGV/ARIfJ14NkxKzRDjvp7L6w==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + } + ], + "license": "MIT", + "dependencies": { + "caniuse-lite": "^1.0.30001449", + "electron-to-chromium": "^1.4.284", + "node-releases": "^2.0.8", + "update-browserslist-db": "^1.0.10" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "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": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "license": "MIT" + }, + "node_modules/builtins": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/builtins/-/builtins-5.1.0.tgz", + "integrity": "sha512-SW9lzGTLvWTP1AY8xeAMZimqDrIaSdLQUcVr9DMef51niJ022Ri87SwRRKYm4A6iHfkPaiVUu/Duw2Wc4J7kKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.0.0" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/cacache": { + "version": "17.0.4", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-17.0.4.tgz", + "integrity": "sha512-Z/nL3gU+zTUjz5pCA5vVjYM8pmaw2kxM7JEiE0fv3w77Wj+sFbi70CrBruUWH0uNcEdvLDixFpgA2JM4F4DBjA==", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/fs": "^3.1.0", + "fs-minipass": "^3.0.0", + "glob": "^8.0.1", + "lru-cache": "^7.7.1", + "minipass": "^4.0.0", + "minipass-collect": "^1.0.2", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "p-map": "^4.0.0", + "promise-inflight": "^1.0.1", + "ssri": "^10.0.0", + "tar": "^6.1.11", + "unique-filename": "^3.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/cacache/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.1.tgz", + "integrity": "sha512-BhYE+WDaywFg2TBWYNXAE+8B1ATnThNBqXHP5nQu0jWJdVvY2hvkpyB3qOmtmDePiS5/BDQ8wASEWGMWRG148g==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.3.tgz", + "integrity": "sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/camelcase-keys": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-6.2.2.tgz", + "integrity": "sha512-YrwaA0vEKazPBkn0ipTiMpSajYDSe+KjQfrjhcBMxJt/znbvlHd8Pw/Vamaz5EB4Wfhs3SUR3Z9mwRu/P3s3Yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "camelcase": "^5.3.1", + "map-obj": "^4.0.0", + "quick-lru": "^4.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001696", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001696.tgz", + "integrity": "sha512-pDCPkvzfa39ehJtJ+OwGT/2yvT2SbjfHhiIW2LWOAcMQ7BzwxT/XuyUp4OTOd0XFWA6BKw0JalnBHgSi5DGJBQ==", + "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/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "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/chardet": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", + "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", + "license": "MIT" + }, + "node_modules/chokidar": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "devOptional": true, + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ], + "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" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/chrome-trace-event": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", + "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", + "license": "MIT", + "engines": { + "node": ">=6.0" + } + }, + "node_modules/clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "license": "MIT", + "dependencies": { + "restore-cursor": "^3.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cli-spinners": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", + "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-width": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-3.0.0.tgz", + "integrity": "sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==", + "license": "ISC", + "engines": { + "node": ">= 10" + } + }, + "node_modules/clipboard": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/clipboard/-/clipboard-2.0.11.tgz", + "integrity": "sha512-C+0bbOqkezLIsmWSvlsXS0Q0bmkugu7jcfMIACB+RDEntIzQIkdr148we28AfSloQLRdZlYL/QYyrq05j/3Faw==", + "license": "MIT", + "dependencies": { + "good-listener": "^1.2.2", + "select": "^1.1.2", + "tiny-emitter": "^2.0.0" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/clone": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/clone-deep": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", + "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-plain-object": "^2.0.4", + "kind-of": "^6.0.2", + "shallow-clone": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/color": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/color/-/color-3.2.1.tgz", + "integrity": "sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==", + "license": "MIT", + "dependencies": { + "color-convert": "^1.9.3", + "color-string": "^1.6.0" + } + }, + "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==", + "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==", + "license": "MIT" + }, + "node_modules/color-string": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", + "license": "MIT", + "dependencies": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, + "node_modules/color-support": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", + "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", + "dev": true, + "license": "ISC", + "bin": { + "color-support": "bin.js" + } + }, + "node_modules/color/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "license": "MIT", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/color/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "license": "MIT" + }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/colorspace": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/colorspace/-/colorspace-1.1.4.tgz", + "integrity": "sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w==", + "license": "MIT", + "dependencies": { + "color": "^3.1.3", + "text-hex": "1.0.x" + } + }, + "node_modules/commander": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 12" + } + }, + "node_modules/commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", + "dev": true, + "license": "MIT" + }, + "node_modules/compressible": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", + "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": ">= 1.43.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/compression": { + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.7.5.tgz", + "integrity": "sha512-bQJ0YRck5ak3LgtnpKkiabX5pNF7tMUh1BSy2ZBOTh0Dim0BUu6aPPwByIns6/A5Prh8PufSPerMDUklpzes2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "compressible": "~2.0.18", + "debug": "2.6.9", + "negotiator": "~0.6.4", + "on-headers": "~1.0.2", + "safe-buffer": "5.2.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/compression/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/compression/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "license": "MIT" + }, + "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/connect": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/connect/-/connect-3.7.0.tgz", + "integrity": "sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "finalhandler": "1.1.2", + "parseurl": "~1.3.3", + "utils-merge": "1.0.1" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/connect-history-api-fallback": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-2.0.0.tgz", + "integrity": "sha512-U73+6lQFmfiNPrYbXqr6kZ1i1wiRqXnp2nhMsINseWXO8lDau0LGEffJ8kQi4EjLZympVgRdvqjAgiZ1tgzDDA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/connect/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/connect/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "license": "MIT" + }, + "node_modules/console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/copy-anything": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-2.0.6.tgz", + "integrity": "sha512-1j20GZTsvKNkc4BY3NpMOM8tt///wY3FpIzozTOFO2ffuZcV61nojHXVKIy3WM+7ADCy5FVhdZYHYDdgTU0yJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-what": "^3.14.1" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, + "node_modules/copy-webpack-plugin": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-11.0.0.tgz", + "integrity": "sha512-fX2MWpamkW0hZxMEg0+mYnA40LTosOSa5TqZ9GYIBzyJa9C3QUaMPSE2xAi/buNr8u89SfD9wHSQVBzrRa/SOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-glob": "^3.2.11", + "glob-parent": "^6.0.1", + "globby": "^13.1.1", + "normalize-path": "^3.0.0", + "schema-utils": "^4.0.0", + "serialize-javascript": "^6.0.0" + }, + "engines": { + "node": ">= 14.15.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + } + }, + "node_modules/copy-webpack-plugin/node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/core-js-compat": { + "version": "3.40.0", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.40.0.tgz", + "integrity": "sha512-0XEDpr5y5mijvw8Lbc6E5AkjrHfp7eEoPlu36SWeAbcL8fn1G1ANe8DBlo2XoNN89oVpxWwOjYIPVzR4ZvsKCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "browserslist": "^4.24.3" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "node_modules/core-js-compat/node_modules/browserslist": { + "version": "4.24.4", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.4.tgz", + "integrity": "sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==", + "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": { + "caniuse-lite": "^1.0.30001688", + "electron-to-chromium": "^1.5.73", + "node-releases": "^2.0.19", + "update-browserslist-db": "^1.1.1" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/cose-base": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/cose-base/-/cose-base-1.0.3.tgz", + "integrity": "sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg==", + "license": "MIT", + "optional": true, + "dependencies": { + "layout-base": "^1.0.0" + } + }, + "node_modules/cosmiconfig": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", + "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.2.1", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.10.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/critters": { + "version": "0.0.16", + "resolved": "https://registry.npmjs.org/critters/-/critters-0.0.16.tgz", + "integrity": "sha512-JwjgmO6i3y6RWtLYmXwO5jMd+maZt8Tnfu7VVISmEWyQqfLpB8soBswf8/2bu6SBXxtKA68Al3c+qIG1ApT68A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "chalk": "^4.1.0", + "css-select": "^4.2.0", + "parse5": "^6.0.1", + "parse5-htmlparser2-tree-adapter": "^6.0.1", + "postcss": "^8.3.7", + "pretty-bytes": "^5.3.0" + } + }, + "node_modules/critters/node_modules/parse5": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz", + "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/cross-spawn/node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/css-loader": { + "version": "6.7.3", + "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-6.7.3.tgz", + "integrity": "sha512-qhOH1KlBMnZP8FzRO6YCH9UHXQhVMcEGLyNdb7Hv2cpcmJbW0YrddO+tG1ab5nT41KpHIYGsbeHqxB9xPu1pKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "icss-utils": "^5.1.0", + "postcss": "^8.4.19", + "postcss-modules-extract-imports": "^3.0.0", + "postcss-modules-local-by-default": "^4.0.0", + "postcss-modules-scope": "^3.0.0", + "postcss-modules-values": "^4.0.0", + "postcss-value-parser": "^4.2.0", + "semver": "^7.3.8" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" + } + }, + "node_modules/css-select": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-4.3.0.tgz", + "integrity": "sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.0.1", + "domhandler": "^4.3.1", + "domutils": "^2.8.0", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-what": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", + "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "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==", + "dev": true, + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/custom-event": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/custom-event/-/custom-event-1.0.1.tgz", + "integrity": "sha512-GAj5FOq0Hd+RsCGVJxZuKaIDXDf3h6GQoNEjFgbLLI/trgtavwUbSnZ5pVfg27DVCaWjIohryS0JFwIJyT2cMg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cytoscape": { + "version": "3.31.0", + "resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.31.0.tgz", + "integrity": "sha512-zDGn1K/tfZwEnoGOcHc0H4XazqAAXAuDpcYw9mUnUjATjqljyCNGJv8uEvbvxGaGHaVshxMecyl6oc6uKzRfbw==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/cytoscape-cose-bilkent": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cytoscape-cose-bilkent/-/cytoscape-cose-bilkent-4.1.0.tgz", + "integrity": "sha512-wgQlVIUJF13Quxiv5e1gstZ08rnZj2XaLHGoFMYXz7SkNfCDOOteKBE6SYRfA9WxxI/iBc3ajfDoc6hb/MRAHQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "cose-base": "^1.0.0" + }, + "peerDependencies": { + "cytoscape": "^3.2.0" + } + }, + "node_modules/cytoscape-fcose": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cytoscape-fcose/-/cytoscape-fcose-2.2.0.tgz", + "integrity": "sha512-ki1/VuRIHFCzxWNrsshHYPs6L7TvLu3DL+TyIGEsRcvVERmxokbf5Gdk7mFxZnTdiGtnA4cfSmjZJMviqSuZrQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "cose-base": "^2.2.0" + }, + "peerDependencies": { + "cytoscape": "^3.2.0" + } + }, + "node_modules/cytoscape-fcose/node_modules/cose-base": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cose-base/-/cose-base-2.2.0.tgz", + "integrity": "sha512-AzlgcsCbUMymkADOJtQm3wO9S3ltPfYOFD5033keQn9NJzIbtnZj+UdBJe7DYml/8TdbtHJW3j58SOnKhWY/5g==", + "license": "MIT", + "optional": true, + "dependencies": { + "layout-base": "^2.0.0" + } + }, + "node_modules/cytoscape-fcose/node_modules/layout-base": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/layout-base/-/layout-base-2.0.1.tgz", + "integrity": "sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg==", + "license": "MIT", + "optional": true + }, + "node_modules/d3": { + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/d3/-/d3-7.9.0.tgz", + "integrity": "sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==", + "license": "ISC", + "optional": true, + "dependencies": { + "d3-array": "3", + "d3-axis": "3", + "d3-brush": "3", + "d3-chord": "3", + "d3-color": "3", + "d3-contour": "4", + "d3-delaunay": "6", + "d3-dispatch": "3", + "d3-drag": "3", + "d3-dsv": "3", + "d3-ease": "3", + "d3-fetch": "3", + "d3-force": "3", + "d3-format": "3", + "d3-geo": "3", + "d3-hierarchy": "3", + "d3-interpolate": "3", + "d3-path": "3", + "d3-polygon": "3", + "d3-quadtree": "3", + "d3-random": "3", + "d3-scale": "4", + "d3-scale-chromatic": "3", + "d3-selection": "3", + "d3-shape": "3", + "d3-time": "3", + "d3-time-format": "4", + "d3-timer": "3", + "d3-transition": "3", + "d3-zoom": "3" + }, + "engines": { + "node": ">=12" + } + }, + "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", + "optional": true, + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-axis": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-3.0.0.tgz", + "integrity": "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==", + "license": "ISC", + "optional": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-brush": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-3.0.0.tgz", + "integrity": "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==", + "license": "ISC", + "optional": true, + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "3", + "d3-transition": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-chord": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-chord/-/d3-chord-3.0.1.tgz", + "integrity": "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==", + "license": "ISC", + "optional": true, + "dependencies": { + "d3-path": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "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", + "optional": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-contour": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-contour/-/d3-contour-4.0.2.tgz", + "integrity": "sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==", + "license": "ISC", + "optional": true, + "dependencies": { + "d3-array": "^3.2.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-delaunay": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==", + "license": "ISC", + "optional": true, + "dependencies": { + "delaunator": "5" + }, + "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", + "optional": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-drag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", + "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", + "license": "ISC", + "optional": true, + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-selection": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dsv": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-3.0.1.tgz", + "integrity": "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==", + "license": "ISC", + "optional": true, + "dependencies": { + "commander": "7", + "iconv-lite": "0.6", + "rw": "1" + }, + "bin": { + "csv2json": "bin/dsv2json.js", + "csv2tsv": "bin/dsv2dsv.js", + "dsv2dsv": "bin/dsv2dsv.js", + "dsv2json": "bin/dsv2json.js", + "json2csv": "bin/json2dsv.js", + "json2dsv": "bin/json2dsv.js", + "json2tsv": "bin/json2dsv.js", + "tsv2csv": "bin/dsv2dsv.js", + "tsv2json": "bin/dsv2json.js" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dsv/node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/d3-dsv/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "optional": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", + "optional": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-fetch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-fetch/-/d3-fetch-3.0.1.tgz", + "integrity": "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==", + "license": "ISC", + "optional": true, + "dependencies": { + "d3-dsv": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-force": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-3.0.0.tgz", + "integrity": "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==", + "license": "ISC", + "optional": true, + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-quadtree": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz", + "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==", + "license": "ISC", + "optional": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-geo": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-3.1.1.tgz", + "integrity": "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==", + "license": "ISC", + "optional": true, + "dependencies": { + "d3-array": "2.5.0 - 3" + }, + "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", + "optional": true, + "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", + "optional": true, + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "license": "ISC", + "optional": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-polygon": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-3.0.1.tgz", + "integrity": "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==", + "license": "ISC", + "optional": true, + "engines": { + "node": ">=12" + } + }, + "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", + "optional": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-random": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-random/-/d3-random-3.0.1.tgz", + "integrity": "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==", + "license": "ISC", + "optional": true, + "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", + "optional": true, + "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-scale-chromatic": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", + "integrity": "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==", + "license": "ISC", + "optional": true, + "dependencies": { + "d3-color": "1 - 3", + "d3-interpolate": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-selection": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", + "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", + "license": "ISC", + "optional": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "license": "ISC", + "optional": true, + "dependencies": { + "d3-path": "^3.1.0" + }, + "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", + "optional": true, + "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", + "optional": true, + "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", + "optional": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-transition": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", + "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", + "license": "ISC", + "optional": true, + "dependencies": { + "d3-color": "1 - 3", + "d3-dispatch": "1 - 3", + "d3-ease": "1 - 3", + "d3-interpolate": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "d3-selection": "2 - 3" + } + }, + "node_modules/d3-zoom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz", + "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", + "license": "ISC", + "optional": true, + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "2 - 3", + "d3-transition": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/dagre-d3-es": { + "version": "7.0.9", + "resolved": "https://registry.npmjs.org/dagre-d3-es/-/dagre-d3-es-7.0.9.tgz", + "integrity": "sha512-rYR4QfVmy+sR44IBDvVtcAmOReGBvRCWDpO2QjYwqgh9yijw6eSHBqaPG/LIOEy7aBsniLvtMW6pg19qJhq60w==", + "license": "MIT", + "optional": true, + "dependencies": { + "d3": "^7.8.2", + "lodash-es": "^4.17.21" + } + }, + "node_modules/date-format": { + "version": "4.0.14", + "resolved": "https://registry.npmjs.org/date-format/-/date-format-4.0.14.tgz", + "integrity": "sha512-39BOQLs9ZjKh0/patS9nrT8wc3ioX3/eA/zgbKNopnF2wCqJEoxywwwElATYvRsXdnOxA/OQeQoFZ3rFjVajhg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/dayjs": { + "version": "1.11.13", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz", + "integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==", + "license": "MIT", + "optional": true + }, + "node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/decamelize-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/decamelize-keys/-/decamelize-keys-1.1.1.tgz", + "integrity": "sha512-WiPxgEirIV0/eIOMcnFBA3/IJZAZqKnwAwWyvvdi4lsr1WCN22nhdf/3db3DoZcUjTV2SqfzIwNyp6y2xs3nmg==", + "dev": true, + "license": "MIT", + "dependencies": { + "decamelize": "^1.1.0", + "map-obj": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/decamelize-keys/node_modules/map-obj": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz", + "integrity": "sha512-7N/q3lyZ+LVCp7PzuxrJr4KMbBE2hW7BT7YNia330OFxIf4d3r5zVpicP2650l7CPN6RM9zOJRl3NGpqSiw3Eg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "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/default-gateway": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/default-gateway/-/default-gateway-6.0.3.tgz", + "integrity": "sha512-fwSOJsbbNzZ/CUFpqFBqYfYNLj1NbMPm8MMCIzHjC83iSJRBEGmDUxU+WP661BaBQImeC2yHwXtz+P/O9o+XEg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "execa": "^5.0.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/defaults": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", + "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==", + "license": "MIT", + "dependencies": { + "clone": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/define-lazy-prop": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", + "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/delaunator": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.0.1.tgz", + "integrity": "sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==", + "license": "ISC", + "optional": true, + "dependencies": { + "robust-predicates": "^3.0.2" + } + }, + "node_modules/delegate": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/delegate/-/delegate-3.2.0.tgz", + "integrity": "sha512-IofjkYBZaZivn0V8nnsMJGBr4jVLxHDheKSW88PyxS5QC4Vo9ZbZVvhzlSxY87fVq3STR6r+4cGepyHkcWOQSw==", + "license": "MIT" + }, + "node_modules/delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/dependency-graph": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/dependency-graph/-/dependency-graph-0.11.0.tgz", + "integrity": "sha512-JeMq7fEshyepOWDfcfHK06N3MhyPhz++vtqWhMT5O9A3K42rdsEDpfdVqjaqaAhsw6a+ZqeDvQVtD0hFHQWrzg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/detect-node": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", + "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", + "dev": true, + "license": "MIT" + }, + "node_modules/di": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/di/-/di-0.0.1.tgz", + "integrity": "sha512-uJaamHkagcZtHPqCIHZxnFrXlunQXgBOsZSUOWwFw31QJCAbyTBoHMW75YOTur5ZNx8pIeAKgf6GWIgaqqiLhA==", + "dev": true, + "license": "MIT" + }, + "node_modules/didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "dev": true, + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/dns-packet": { + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-5.6.1.tgz", + "integrity": "sha512-l4gcSouhcgIKRvyy99RNVOgxXiicE+2jZoNmaNmZ6JXiGajBOJAesk1OBlJuM5k2c+eudGdLxDqXuPCKIj6kpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@leichtgewicht/ip-codec": "^2.0.1" + }, + "engines": { + "node": ">=6" + } + }, + "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/dom-serialize": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/dom-serialize/-/dom-serialize-2.2.1.tgz", + "integrity": "sha512-Yra4DbvoW7/Z6LBN560ZwXMjoNOSAN2wRsKFGc4iBeso+mpIA6qj1vfdf9HpMaKAqG6wXTy+1SYEzmNpKXOSsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "custom-event": "~1.0.0", + "ent": "~2.2.0", + "extend": "^3.0.0", + "void-elements": "^2.0.0" + } + }, + "node_modules/dom-serializer": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz", + "integrity": "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==", + "dev": true, + "license": "MIT", + "dependencies": { + "domelementtype": "^2.0.1", + "domhandler": "^4.2.0", + "entities": "^2.0.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, + "node_modules/domhandler": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz", + "integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.2.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/dompurify": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.4.3.tgz", + "integrity": "sha512-q6QaLcakcRjebxjg8/+NP+h0rPfatOgOzc46Fst9VAA3jF2ApfKBNKMzdP4DYTqtUMXSCd5pRS/8Po/OmoCHZQ==", + "license": "(MPL-2.0 OR Apache-2.0)", + "optional": true + }, + "node_modules/domutils": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", + "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^1.0.1", + "domelementtype": "^2.2.0", + "domhandler": "^4.2.0" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "dev": true, + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.90", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.90.tgz", + "integrity": "sha512-C3PN4aydfW91Natdyd449Kw+BzhLmof6tzy5W1pFC5SpQxVXT+oyiyOG9AgYYSN9OdA/ik3YkCrpwqI8ug5Tug==", + "license": "ISC" + }, + "node_modules/elkjs": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/elkjs/-/elkjs-0.8.2.tgz", + "integrity": "sha512-L6uRgvZTH+4OF5NE/MBbzQx/WYpru1xCBE9respNj6qznEewGUIfhzmm7horWWxbNO2M0WckQypGctR8lH79xQ==", + "license": "EPL-2.0", + "optional": true + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/emoji-toolkit": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/emoji-toolkit/-/emoji-toolkit-7.0.1.tgz", + "integrity": "sha512-l5aJyAhpC5s4mDuoVuqt4SzVjwIsIvakPh4ZGJJE4KWuWFCEHaXacQFkStVdD9zbRR+/BbRXob7u99o0lQFr8A==", + "license": "MIT", + "optional": true + }, + "node_modules/emojis-list": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", + "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/enabled": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz", + "integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/encoding": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", + "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", + "license": "MIT", + "optional": true, + "dependencies": { + "iconv-lite": "^0.6.2" + } + }, + "node_modules/encoding/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "optional": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/engine.io": { + "version": "6.6.4", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.4.tgz", + "integrity": "sha512-ZCkIjSYNDyGn0R6ewHDtXgns/Zre/NT6Agvq1/WobF7JXgFff4SeDroKiCO3fNJreU9YG429Sc81o4w5ok/W5g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/cors": "^2.8.12", + "@types/node": ">=10.0.0", + "accepts": "~1.3.4", + "base64id": "2.0.0", + "cookie": "~0.7.2", + "cors": "~2.8.5", + "debug": "~4.3.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.17.1" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/engine.io-parser": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", + "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/engine.io/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/enhanced-resolve": { + "version": "5.18.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.0.tgz", + "integrity": "sha512-0/r0MySGYG8YqlayBZ6MuCfECmHFdJ5qyPh8s8wa5Hnm6SaFLSK1VYCbj+NKp090Nm1caZhD+QTnmxO7esYGyQ==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/ent": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/ent/-/ent-2.2.2.tgz", + "integrity": "sha512-kKvD1tO6BM+oK9HzCPpUdRb4vKFQY/FPTFmurMvh6LlN68VMrdj77w8yp51/kDbpkFOS9J8w5W6zIzgM2H8/hw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "punycode": "^1.4.1", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/entities": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", + "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", + "dev": true, + "license": "BSD-2-Clause", + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/err-code": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", + "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", + "dev": true, + "license": "MIT" + }, + "node_modules/errno": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.8.tgz", + "integrity": "sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "prr": "~1.0.1" + }, + "bin": { + "errno": "cli.js" + } + }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-module-lexer": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-0.9.3.tgz", + "integrity": "sha512-1HQ2M2sPtxwnvOvT1ZClHyQDiggdNjURWpY2we6aMKCQiUVxTmVs2UYPLIrD84sS+kMdUwfBSylbJPwNnBrnHQ==", + "license": "MIT" + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.17.8", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.17.8.tgz", + "integrity": "sha512-g24ybC3fWhZddZK6R3uD2iF/RIPnRpwJAqLov6ouX3hMbY4+tKolP0VMF3zuIYCaXun+yHwS5IPQ91N2BT191g==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/android-arm": "0.17.8", + "@esbuild/android-arm64": "0.17.8", + "@esbuild/android-x64": "0.17.8", + "@esbuild/darwin-arm64": "0.17.8", + "@esbuild/darwin-x64": "0.17.8", + "@esbuild/freebsd-arm64": "0.17.8", + "@esbuild/freebsd-x64": "0.17.8", + "@esbuild/linux-arm": "0.17.8", + "@esbuild/linux-arm64": "0.17.8", + "@esbuild/linux-ia32": "0.17.8", + "@esbuild/linux-loong64": "0.17.8", + "@esbuild/linux-mips64el": "0.17.8", + "@esbuild/linux-ppc64": "0.17.8", + "@esbuild/linux-riscv64": "0.17.8", + "@esbuild/linux-s390x": "0.17.8", + "@esbuild/linux-x64": "0.17.8", + "@esbuild/netbsd-x64": "0.17.8", + "@esbuild/openbsd-x64": "0.17.8", + "@esbuild/sunos-x64": "0.17.8", + "@esbuild/win32-arm64": "0.17.8", + "@esbuild/win32-ia32": "0.17.8", + "@esbuild/win32-x64": "0.17.8" + } + }, + "node_modules/esbuild-wasm": { + "version": "0.17.8", + "resolved": "https://registry.npmjs.org/esbuild-wasm/-/esbuild-wasm-0.17.8.tgz", + "integrity": "sha512-zCmpxv95E0FuCmvdw1K836UHnj4EdiQnFfjTby35y3LAjRPtXMj3sbHDRHjbD8Mqg5lTwq3knacr/1qIFU51CQ==", + "dev": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "dev": true, + "license": "MIT" + }, + "node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/eslint": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz", + "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==", + "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.0", + "@humanwhocodes/config-array": "^0.11.14", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-config-prettier": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz", + "integrity": "sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==", + "dev": true, + "license": "MIT", + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-plugin-es": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-es/-/eslint-plugin-es-4.1.0.tgz", + "integrity": "sha512-GILhQTnjYE2WorX5Jyi5i4dz5ALWxBIdQECVQavL6s7cI76IZTDWleTHkxz/QT3kvcs2QlGHvKLYsSlPOlPXnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-utils": "^2.0.0", + "regexpp": "^3.0.0" + }, + "engines": { + "node": ">=8.10.0" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + }, + "peerDependencies": { + "eslint": ">=4.19.1" + } + }, + "node_modules/eslint-plugin-es/node_modules/eslint-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.1.0.tgz", + "integrity": "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^1.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + } + }, + "node_modules/eslint-plugin-es/node_modules/eslint-visitor-keys": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", + "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=4" + } + }, + "node_modules/eslint-plugin-n": { + "version": "15.7.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-n/-/eslint-plugin-n-15.7.0.tgz", + "integrity": "sha512-jDex9s7D/Qial8AGVIHq4W7NswpUD5DPDL2RH8Lzd9EloWUuvUkHfv4FRLMipH5q2UtyurorBkPeNi1wVWNh3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "builtins": "^5.0.1", + "eslint-plugin-es": "^4.1.0", + "eslint-utils": "^3.0.0", + "ignore": "^5.1.1", + "is-core-module": "^2.11.0", + "minimatch": "^3.1.2", + "resolve": "^1.22.1", + "semver": "^7.3.8" + }, + "engines": { + "node": ">=12.22.0" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-plugin-n/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint-plugin-n/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/eslint-plugin-prettier": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.1.3.tgz", + "integrity": "sha512-C9GCVAs4Eq7ZC/XFQHITLiHJxQngdtraXaM+LoUFoFp/lHNl2Zn8f3WQbe9HvTBBQ9YnKFB0/2Ajdqwo5D1EAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "prettier-linter-helpers": "^1.0.0", + "synckit": "^0.8.6" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint-plugin-prettier" + }, + "peerDependencies": { + "@types/eslint": ">=8.0.0", + "eslint": ">=8.0.0", + "eslint-config-prettier": "*", + "prettier": ">=3.0.0" + }, + "peerDependenciesMeta": { + "@types/eslint": { + "optional": true + }, + "eslint-config-prettier": { + "optional": true + } + } + }, + "node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/eslint-utils": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz", + "integrity": "sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^2.0.0" + }, + "engines": { + "node": "^10.0.0 || ^12.0.0 || >= 14.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + }, + "peerDependencies": { + "eslint": ">=5" + } + }, + "node_modules/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", + "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10" + } + }, + "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/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "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/eslint/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/eslint/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/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/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/eslint/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/eslint/node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/eslint/node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/eslint/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/eslint/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/eslint/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/eslint/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/eslint/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/eslint/node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "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/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esquery/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/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esrecurse/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "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/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/eventemitter-asyncresource": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/eventemitter-asyncresource/-/eventemitter-asyncresource-1.0.0.tgz", + "integrity": "sha512-39F7TBIV0G7gTelxwbEqnwhp90eqCPON1k0NwNfwhgKn4Co4ybUbj2pECcXT0B3ztRKZ7Pw1JujUUgmQJHcVAQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "dev": true, + "license": "MIT" + }, + "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/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/exponential-backoff": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.1.tgz", + "integrity": "sha512-dX7e/LHVJ6W3DE1MHWi9S1EYzDESENfLrYohG2G++ovZrYOkm4Knwa0mc1cn84xJOR4KEU0WSchhLbd0UklbHw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/express": { + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", + "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.3", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.7.1", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.3.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.12", + "proxy-addr": "~2.0.7", + "qs": "6.13.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.19.0", + "serve-static": "1.16.2", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express/node_modules/cookie": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/express/node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/express/node_modules/finalhandler": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/express/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "license": "MIT" + }, + "node_modules/express/node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "dev": true, + "license": "MIT" + }, + "node_modules/external-editor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", + "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", + "license": "MIT", + "dependencies": { + "chardet": "^0.7.0", + "iconv-lite": "^0.4.24", + "tmp": "^0.0.33" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-diff": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz", + "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", + "dev": true, + "license": "Apache-2.0" + }, + "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==", + "dev": true, + "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-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==", + "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.19.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.0.tgz", + "integrity": "sha512-7SFSRCNjBQIZH/xZR3iy5iQYR8aGBE0h3VG6/cwlbrpdciNYBMotQav8c1XI3HjHH+NikUpP53nPdlZSdWmFzA==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/faye-websocket": { + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz", + "integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==", + "license": "Apache-2.0", + "dependencies": { + "websocket-driver": ">=0.5.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/fecha": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz", + "integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==", + "license": "MIT" + }, + "node_modules/figures": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", + "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^1.0.5" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/file-loader": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/file-loader/-/file-loader-6.2.0.tgz", + "integrity": "sha512-qo3glqyTa61Ytg4u73GultjHGjdRyig3tG6lPtyX/jOEJvHif9uB0/OCI2Kif6ctF3caQTW2G5gym21oAsI4pw==", + "license": "MIT", + "dependencies": { + "loader-utils": "^2.0.0", + "schema-utils": "^3.0.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^4.0.0 || ^5.0.0" + } + }, + "node_modules/file-loader/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "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/file-loader/node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "license": "MIT", + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "node_modules/file-loader/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==", + "license": "MIT" + }, + "node_modules/file-loader/node_modules/loader-utils": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", + "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", + "license": "MIT", + "dependencies": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" + }, + "engines": { + "node": ">=8.9.0" + } + }, + "node_modules/file-loader/node_modules/schema-utils": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "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==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", + "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "statuses": "~1.5.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/finalhandler/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/finalhandler/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "license": "MIT" + }, + "node_modules/finalhandler/node_modules/on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==", + "dev": true, + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/find-cache-dir": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.2.tgz", + "integrity": "sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==", + "dev": true, + "license": "MIT", + "dependencies": { + "commondir": "^1.0.1", + "make-dir": "^3.0.2", + "pkg-dir": "^4.1.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/avajs/find-cache-dir?sponsor=1" + } + }, + "node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase": { + "version": "9.23.0", + "resolved": "https://registry.npmjs.org/firebase/-/firebase-9.23.0.tgz", + "integrity": "sha512-/4lUVY0lUvBDIaeY1q6dUYhS8Sd18Qb9CgWkPZICUo9IXpJNCEagfNZXBBFCkMTTN5L5gx2Hjr27y21a9NzUcA==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/analytics": "0.10.0", + "@firebase/analytics-compat": "0.2.6", + "@firebase/app": "0.9.13", + "@firebase/app-check": "0.8.0", + "@firebase/app-check-compat": "0.3.7", + "@firebase/app-compat": "0.2.13", + "@firebase/app-types": "0.9.0", + "@firebase/auth": "0.23.2", + "@firebase/auth-compat": "0.4.2", + "@firebase/database": "0.14.4", + "@firebase/database-compat": "0.3.4", + "@firebase/firestore": "3.13.0", + "@firebase/firestore-compat": "0.3.12", + "@firebase/functions": "0.10.0", + "@firebase/functions-compat": "0.3.5", + "@firebase/installations": "0.6.4", + "@firebase/installations-compat": "0.2.4", + "@firebase/messaging": "0.12.4", + "@firebase/messaging-compat": "0.2.4", + "@firebase/performance": "0.6.4", + "@firebase/performance-compat": "0.2.4", + "@firebase/remote-config": "0.4.4", + "@firebase/remote-config-compat": "0.2.4", + "@firebase/storage": "0.11.2", + "@firebase/storage-compat": "0.3.2", + "@firebase/util": "1.9.3" + } + }, + "node_modules/firebase-tools": { + "version": "11.30.0", + "resolved": "https://registry.npmjs.org/firebase-tools/-/firebase-tools-11.30.0.tgz", + "integrity": "sha512-rNMhtsFKy35AZqfy0obY2Kgsfmg8N1duEx2UDT14NsMHB7xt2Z7cUd92bkAoQAfD0jAOd7LhRT/7E6J5yB+kGw==", + "devOptional": true, + "hasShrinkwrap": true, + "license": "MIT", + "dependencies": { + "@google-cloud/pubsub": "^3.0.1", + "abort-controller": "^3.0.0", + "ajv": "^6.12.6", + "archiver": "^5.0.0", + "async-lock": "1.3.2", + "body-parser": "^1.19.0", + "chokidar": "^3.0.2", + "cjson": "^0.3.1", + "cli-table": "0.3.11", + "colorette": "^2.0.19", + "commander": "^4.0.1", + "configstore": "^5.0.1", + "cors": "^2.8.5", + "cross-env": "^5.1.3", + "cross-spawn": "^7.0.3", + "csv-parse": "^5.0.4", + "exegesis": "^4.1.0", + "exegesis-express": "^4.0.0", + "express": "^4.16.4", + "filesize": "^6.1.0", + "form-data": "^4.0.0", + "fs-extra": "^10.1.0", + "glob": "^7.1.2", + "google-auth-library": "^7.11.0", + "inquirer": "^8.2.0", + "js-yaml": "^3.13.1", + "jsonwebtoken": "^9.0.0", + "leven": "^3.1.0", + "libsodium-wrappers": "^0.7.10", + "lodash": "^4.17.21", + "marked": "^4.0.14", + "marked-terminal": "^5.1.1", + "mime": "^2.5.2", + "minimatch": "^3.0.4", + "morgan": "^1.10.0", + "node-fetch": "^2.6.7", + "open": "^6.3.0", + "ora": "^5.4.1", + "p-limit": "^3.0.1", + "portfinder": "^1.0.32", + "progress": "^2.0.3", + "proxy-agent": "^5.0.0", + "request": "^2.87.0", + "retry": "^0.13.1", + "rimraf": "^3.0.0", + "semver": "^5.7.1", + "stream-chain": "^2.2.4", + "stream-json": "^1.7.3", + "strip-ansi": "^6.0.1", + "superstatic": "^9.0.3", + "tar": "^6.1.11", + "tcp-port-used": "^1.0.2", + "tmp": "^0.2.1", + "triple-beam": "^1.3.0", + "universal-analytics": "^0.5.3", + "update-notifier-cjs": "^5.1.6", + "uuid": "^8.3.2", + "winston": "^3.0.0", + "winston-transport": "^4.4.0", + "ws": "^7.2.3" + }, + "bin": { + "firebase": "lib/bin/firebase.js" + }, + "engines": { + "node": "^14.18.0 || >=16.4.0" + } + }, + "node_modules/firebase-tools/node_modules/@apidevtools/json-schema-ref-parser": { + "version": "9.0.7", + "resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-9.0.7.tgz", + "integrity": "sha512-QdwOGF1+eeyFh+17v2Tz626WX0nucd1iKOm6JUTUvCZdbolblCOOQCxGrQPY0f7jEhn36PiAWqZnsC2r5vmUWg==", + "devOptional": true, + "dependencies": { + "@jsdevtools/ono": "^7.1.3", + "call-me-maybe": "^1.0.1", + "js-yaml": "^3.13.1" + } + }, + "node_modules/firebase-tools/node_modules/@babel/parser": { + "version": "7.21.4", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.21.4.tgz", + "integrity": "sha512-alVJj7k7zIxqBZ7BTRhz0IqJFxW1VJbm6N8JbcYhQ186df9ZBPbZBmWSqAMXwHGsCJdYks7z/voa3ibiS5bCIw==", + "devOptional": true, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/firebase-tools/node_modules/@colors/colors": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", + "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==", + "optional": true, + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/firebase-tools/node_modules/@gar/promisify": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz", + "integrity": "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==", + "optional": true + }, + "node_modules/firebase-tools/node_modules/@google-cloud/precise-date": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@google-cloud/precise-date/-/precise-date-2.0.3.tgz", + "integrity": "sha512-+SDJ3ZvGkF7hzo6BGa8ZqeK3F6Z4+S+KviC9oOK+XCs3tfMyJCh/4j93XIWINgMMDIh9BgEvlw4306VxlXIlYA==", + "devOptional": true, + "engines": { + "node": ">=10.4.0" + } + }, + "node_modules/firebase-tools/node_modules/@google-cloud/projectify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@google-cloud/projectify/-/projectify-2.0.1.tgz", + "integrity": "sha512-ZDG38U/Yy6Zr21LaR3BTiiLtpJl6RkPS/JwoRT453G+6Q1DhlV0waNf8Lfu+YVYGIIxgKnLayJRfYlFJfiI8iQ==", + "devOptional": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/firebase-tools/node_modules/@google-cloud/promisify": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@google-cloud/promisify/-/promisify-2.0.3.tgz", + "integrity": "sha512-d4VSA86eL/AFTe5xtyZX+ePUjE8dIFu2T8zmdeNBSa5/kNgXPCx/o/wbFNHAGLJdGnk1vddRuMESD9HbOC8irw==", + "devOptional": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/firebase-tools/node_modules/@google-cloud/pubsub": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@google-cloud/pubsub/-/pubsub-3.0.1.tgz", + "integrity": "sha512-dznNbRd/Y8J0C0xvdvCPi3B1msK/dj/Nya+NQZ2doUOLT6eoa261tBwk9umOQs5L5GKcdlqQKbBjrNjDYVbzQA==", + "devOptional": true, + "dependencies": { + "@google-cloud/paginator": "^4.0.0", + "@google-cloud/precise-date": "^2.0.0", + "@google-cloud/projectify": "^2.0.0", + "@google-cloud/promisify": "^2.0.0", + "@opentelemetry/api": "^1.0.0", + "@opentelemetry/semantic-conventions": "^1.0.0", + "@types/duplexify": "^3.6.0", + "@types/long": "^4.0.0", + "arrify": "^2.0.0", + "extend": "^3.0.2", + "google-auth-library": "^8.0.2", + "google-gax": "^3.0.1", + "is-stream-ended": "^0.1.4", + "lodash.snakecase": "^4.1.1", + "p-defer": "^3.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/firebase-tools/node_modules/@google-cloud/pubsub/node_modules/@google-cloud/paginator": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@google-cloud/paginator/-/paginator-4.0.0.tgz", + "integrity": "sha512-wNmCZl+2G2DmgT/VlF+AROf80SoaC/CwS8trwmjNaq26VRNK8yPbU5F/Vy+R9oDAGKWQU2k8+Op5H4kFJVXFaQ==", + "devOptional": true, + "dependencies": { + "arrify": "^2.0.0", + "extend": "^3.0.2" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/firebase-tools/node_modules/@google-cloud/pubsub/node_modules/gaxios": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-5.0.0.tgz", + "integrity": "sha512-VD/yc5ln6XU8Ch1hyYY6kRMBE0Yc2np3fPyeJeYHhrPs1i8rgnsApPMWyrugkl7LLoSqpOJVBWlQIa87OAvt8Q==", + "devOptional": true, + "dependencies": { + "abort-controller": "^3.0.0", + "extend": "^3.0.2", + "https-proxy-agent": "^5.0.0", + "is-stream": "^2.0.0", + "node-fetch": "^2.6.7" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/firebase-tools/node_modules/@google-cloud/pubsub/node_modules/gcp-metadata": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-5.0.0.tgz", + "integrity": "sha512-gfwuX3yA3nNsHSWUL4KG90UulNiq922Ukj3wLTrcnX33BB7PwB1o0ubR8KVvXu9nJH+P5w1j2SQSNNqto+H0DA==", + "devOptional": true, + "dependencies": { + "gaxios": "^5.0.0", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/firebase-tools/node_modules/@google-cloud/pubsub/node_modules/google-auth-library": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-8.0.2.tgz", + "integrity": "sha512-HoG+nWFAThLovKpvcbYzxgn+nBJPTfAwtq0GxPN821nOO+21+8oP7MoEHfd1sbDulUFFGfcjJr2CnJ4YssHcyg==", + "devOptional": true, + "dependencies": { + "arrify": "^2.0.0", + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "fast-text-encoding": "^1.0.0", + "gaxios": "^5.0.0", + "gcp-metadata": "^5.0.0", + "gtoken": "^5.3.2", + "jws": "^4.0.0", + "lru-cache": "^6.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/firebase-tools/node_modules/@google-cloud/pubsub/node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "devOptional": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/firebase-tools/node_modules/@google-cloud/pubsub/node_modules/jwa": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz", + "integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==", + "devOptional": true, + "dependencies": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/firebase-tools/node_modules/@google-cloud/pubsub/node_modules/jws": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", + "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", + "devOptional": true, + "dependencies": { + "jwa": "^2.0.0", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/firebase-tools/node_modules/@grpc/grpc-js": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.7.3.tgz", + "integrity": "sha512-H9l79u4kJ2PVSxUNA08HMYAnUBLj9v6KjYQ7SQ71hOZcEXhShE/y5iQCesP8+6/Ik/7i2O0a10bPquIcYfufog==", + "devOptional": true, + "dependencies": { + "@grpc/proto-loader": "^0.7.0", + "@types/node": ">=12.12.47" + }, + "engines": { + "node": "^8.13.0 || >=10.10.0" + } + }, + "node_modules/firebase-tools/node_modules/@grpc/grpc-js/node_modules/@grpc/proto-loader": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.4.tgz", + "integrity": "sha512-MnWjkGwqQ3W8fx94/c1CwqLsNmHHv2t0CFn+9++6+cDphC1lolpg9M2OU0iebIjK//pBNX9e94ho+gjx6vz39w==", + "devOptional": true, + "dependencies": { + "@types/long": "^4.0.1", + "lodash.camelcase": "^4.3.0", + "long": "^4.0.0", + "protobufjs": "^7.0.0", + "yargs": "^16.2.0" + }, + "bin": { + "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/firebase-tools/node_modules/@grpc/grpc-js/node_modules/protobufjs": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.1.2.tgz", + "integrity": "sha512-4ZPTPkXCdel3+L81yw3dG6+Kq3umdWKh7Dc7GW/CpNk4SX3hK58iPCWeCyhVTDrbkNeKrYNZ7EojM5WDaEWTLQ==", + "devOptional": true, + "hasInstallScript": true, + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/node": ">=13.7.0", + "long": "^5.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/firebase-tools/node_modules/@grpc/grpc-js/node_modules/protobufjs/node_modules/long": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/long/-/long-5.2.1.tgz", + "integrity": "sha512-GKSNGeNAtw8IryjjkhZxuKB3JzlcLTwjtiQCHKvqQet81I93kXslhDQruGI/QsddO83mcDToBVy7GqGS/zYf/A==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/@jsdevtools/ono": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz", + "integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/@jsdoc/salty": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@jsdoc/salty/-/salty-0.2.3.tgz", + "integrity": "sha512-bbtCxCkxcnWhi50I+4Lj6mdz9w3pOXOgEQrID8TCZ/DF51fW7M9GCQW2y45SpBDdHd1Eirm1X/Cf6CkAAe8HPg==", + "devOptional": true, + "dependencies": { + "lodash": "^4.17.21" + }, + "engines": { + "node": ">=v12.0.0" + } + }, + "node_modules/firebase-tools/node_modules/@npmcli/fs": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-2.1.2.tgz", + "integrity": "sha512-yOJKRvohFOaLqipNtwYB9WugyZKhC/DZC4VYPmpaCzDBrA8YpK3qHZ8/HGscMnE4GqbkLNuVcCnxkeQEdGt6LQ==", + "optional": true, + "dependencies": { + "@gar/promisify": "^1.1.3", + "semver": "^7.3.5" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/firebase-tools/node_modules/@npmcli/fs/node_modules/semver": { + "version": "7.3.7", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz", + "integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==", + "optional": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/firebase-tools/node_modules/@npmcli/move-file": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@npmcli/move-file/-/move-file-2.0.1.tgz", + "integrity": "sha512-mJd2Z5TjYWq/ttPLLGqArdtnC74J6bOzg4rMDnN+p1xTacZ2yPRCk2y0oSWQtygLR9YVQXgOcONrwtnk3JupxQ==", + "optional": true, + "dependencies": { + "mkdirp": "^1.0.4", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/firebase-tools/node_modules/@npmcli/move-file/node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "optional": true, + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/firebase-tools/node_modules/@opentelemetry/api": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.0.4.tgz", + "integrity": "sha512-BuJuXRSJNQ3QoKA6GWWDyuLpOUck+9hAXNMCnrloc1aWVoy6Xq6t9PUV08aBZ4Lutqq2LEHM486bpZqoViScog==", + "devOptional": true, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/firebase-tools/node_modules/@opentelemetry/semantic-conventions": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.2.0.tgz", + "integrity": "sha512-BNKB9fiYVghALJzCuWO3eNYfdTExPVK4ykrtmfNfy0A6UWYhOYjGMXifUmkunDJNL8ju9tBobo8jF0WR9zGy1Q==", + "devOptional": true, + "engines": { + "node": ">=8.12.0" + } + }, + "node_modules/firebase-tools/node_modules/@pnpm/network.ca-file": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@pnpm/network.ca-file/-/network.ca-file-1.0.2.tgz", + "integrity": "sha512-YcPQ8a0jwYU9bTdJDpXjMi7Brhkr1mXsXrUJvjqM2mQDgkRiz8jFaQGOdaLxgjtUfQgZhKy/O3cG/YwmgKaxLA==", + "devOptional": true, + "dependencies": { + "graceful-fs": "4.2.10" + }, + "engines": { + "node": ">=12.22.0" + } + }, + "node_modules/firebase-tools/node_modules/@pnpm/npm-conf": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@pnpm/npm-conf/-/npm-conf-1.0.5.tgz", + "integrity": "sha512-hD8ml183638O3R6/Txrh0L8VzGOrFXgRtRDG4qQC4tONdZ5Z1M+tlUUDUvrjYdmK6G+JTBTeaCLMna11cXzi8A==", + "devOptional": true, + "dependencies": { + "@pnpm/network.ca-file": "^1.0.1", + "config-chain": "^1.1.11" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/firebase-tools/node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha1-m4sMxmPWaafY9vXQiToU00jzD78=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/@protobufjs/codegen": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/@protobufjs/eventemitter": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha1-NVy8mLr61ZePntCV85diHx0Ga3A=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/@protobufjs/fetch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", + "integrity": "sha1-upn7WYYUr2VwDBYZ/wbUVLDYTEU=", + "devOptional": true, + "dependencies": { + "@protobufjs/aspromise": "^1.1.1", + "@protobufjs/inquire": "^1.1.0" + } + }, + "node_modules/firebase-tools/node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha1-Xp4avctz/Ap8uLKR33jIy9l7h9E=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/@protobufjs/inquire": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", + "integrity": "sha1-/yAOPnzyQp4tyvwRQIKOjMY48Ik=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha1-bMKyDFya1q0NzP0hynZz2Nf79o0=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha1-Cf0V8tbTq/qbZbw2ZQbWrXhG/1Q=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/@protobufjs/utf8": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", + "integrity": "sha1-p3c2C1s5oaLlEG+OhY8v0tBgxXA=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/@tootallnate/once": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", + "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==", + "devOptional": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/firebase-tools/node_modules/@types/duplexify": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/@types/duplexify/-/duplexify-3.6.0.tgz", + "integrity": "sha512-5zOA53RUlzN74bvrSGwjudssD9F3a797sDZQkiYpUOxW+WHaXTCPz4/d5Dgi6FKnOqZ2CpaTo0DhgIfsXAOE/A==", + "devOptional": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/firebase-tools/node_modules/@types/linkify-it": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-3.0.2.tgz", + "integrity": "sha512-HZQYqbiFVWufzCwexrvh694SOim8z2d+xJl5UNamcvQFejLY/2YUtzXHYi3cHdI7PMlS8ejH2slRAOJQ32aNbA==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/@types/long": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.1.tgz", + "integrity": "sha512-5tXH6Bx/kNGd3MgffdmP4dy2Z+G4eaXw0SE81Tq3BNadtnMR5/ySMzX4SLEzHJzSmPNn4HIdpQsBvXMUykr58w==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/@types/markdown-it": { + "version": "12.2.3", + "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-12.2.3.tgz", + "integrity": "sha512-GKMHFfv3458yYy+v/N8gjufHO6MSZKCOXpZc5GXIWWy8uldwfmPn98vp81gZ5f9SVw8YYBctgfJ22a2d7AOMeQ==", + "devOptional": true, + "dependencies": { + "@types/linkify-it": "*", + "@types/mdurl": "*" + } + }, + "node_modules/firebase-tools/node_modules/@types/mdurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-1.0.2.tgz", + "integrity": "sha512-eC4U9MlIcu2q0KQmXszyn5Akca/0jrQmwDRgpAMJai7qBWq4amIQhZyNau4VYGtCeALvW1/NtjzJJ567aZxfKA==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/@types/node": { + "version": "14.18.36", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.36.tgz", + "integrity": "sha512-FXKWbsJ6a1hIrRxv+FoukuHnGTgEzKYGi7kilfMae96AL9UNkPFNWJEEYWzdRI9ooIkbr4AKldyuSTLql06vLQ==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "optional": true + }, + "node_modules/firebase-tools/node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "devOptional": true, + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, + "node_modules/firebase-tools/node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "devOptional": true, + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/firebase-tools/node_modules/acorn": { + "version": "8.8.2", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz", + "integrity": "sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==", + "devOptional": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/firebase-tools/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==", + "devOptional": true, + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/firebase-tools/node_modules/acorn-walk": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz", + "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==", + "devOptional": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/firebase-tools/node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "devOptional": true, + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/firebase-tools/node_modules/agent-base/node_modules/debug": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "devOptional": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/firebase-tools/node_modules/agent-base/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/agentkeepalive": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.2.1.tgz", + "integrity": "sha512-Zn4cw2NEqd+9fiSVWMscnjyQ1a8Yfoc5oBajLeo5w+YBHgDUcEBY2hS4YpTz6iN5f/2zQiktcuM6tS8x1p9dpA==", + "optional": true, + "dependencies": { + "debug": "^4.1.0", + "depd": "^1.1.2", + "humanize-ms": "^1.2.1" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/firebase-tools/node_modules/agentkeepalive/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "optional": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/firebase-tools/node_modules/agentkeepalive/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "optional": true + }, + "node_modules/firebase-tools/node_modules/aggregate-error": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", + "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "optional": true, + "dependencies": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "devOptional": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/firebase-tools/node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "devOptional": true, + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/firebase-tools/node_modules/ajv-formats/node_modules/ajv": { + "version": "8.8.2", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.8.2.tgz", + "integrity": "sha512-x9VuX+R/jcFj1DHo/fCp99esgGDWiHENrKxaCENuCxpoMCmAt/COCGVDwA7kleEpEzJjDnvh3yGoOuLu0Dtllw==", + "devOptional": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/firebase-tools/node_modules/ajv-formats/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/ansi-align": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.1.tgz", + "integrity": "sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==", + "devOptional": true, + "dependencies": { + "string-width": "^4.1.0" + } + }, + "node_modules/firebase-tools/node_modules/ansi-escapes": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-5.0.0.tgz", + "integrity": "sha512-5GFMVX8HqE/TB+FuBJGuO5XG0WrsA6ptUqoODaT/n9mmUaZFkqnBueB4leqGBCmrUHnCnC4PCZTCd0E7QQ83bA==", + "devOptional": true, + "dependencies": { + "type-fest": "^1.0.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/firebase-tools/node_modules/ansi-escapes/node_modules/type-fest": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-1.4.0.tgz", + "integrity": "sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==", + "devOptional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/firebase-tools/node_modules/ansicolors": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/ansicolors/-/ansicolors-0.3.2.tgz", + "integrity": "sha1-ZlWX3oap/+Oqm/vmyuXG6kJrSXk=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/anymatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", + "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==", + "devOptional": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/firebase-tools/node_modules/aproba": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", + "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==", + "optional": true + }, + "node_modules/firebase-tools/node_modules/archiver": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/archiver/-/archiver-5.3.1.tgz", + "integrity": "sha512-8KyabkmbYrH+9ibcTScQ1xCJC/CGcugdVIwB+53f5sZziXgwUh3iXlAlANMxcZyDEfTHMe6+Z5FofV8nopXP7w==", + "devOptional": true, + "dependencies": { + "archiver-utils": "^2.1.0", + "async": "^3.2.3", + "buffer-crc32": "^0.2.1", + "readable-stream": "^3.6.0", + "readdir-glob": "^1.0.0", + "tar-stream": "^2.2.0", + "zip-stream": "^4.1.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/firebase-tools/node_modules/archiver-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-2.1.0.tgz", + "integrity": "sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw==", + "devOptional": true, + "dependencies": { + "glob": "^7.1.4", + "graceful-fs": "^4.2.0", + "lazystream": "^1.0.0", + "lodash.defaults": "^4.2.0", + "lodash.difference": "^4.5.0", + "lodash.flatten": "^4.4.0", + "lodash.isplainobject": "^4.0.6", + "lodash.union": "^4.6.0", + "normalize-path": "^3.0.0", + "readable-stream": "^2.0.0" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/firebase-tools/node_modules/archiver-utils/node_modules/readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "devOptional": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/firebase-tools/node_modules/archiver/node_modules/async": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.3.tgz", + "integrity": "sha512-spZRyzKL5l5BZQrr/6m/SqFdBN0q3OCI0f9rjfBzCMBIP4p75P620rR3gTmaksNOhmzgdxcaxdNfMy6anrbM0g==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/are-we-there-yet": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-3.0.1.tgz", + "integrity": "sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg==", + "optional": true, + "dependencies": { + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/firebase-tools/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "devOptional": true, + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/firebase-tools/node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/arrify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-2.0.1.tgz", + "integrity": "sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==", + "devOptional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/as-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/as-array/-/as-array-2.0.0.tgz", + "integrity": "sha1-TwSAXYf4/OjlEbwhCPjl46KH1Uc=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/asn1": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", + "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", + "devOptional": true, + "dependencies": { + "safer-buffer": "~2.1.0" + } + }, + "node_modules/firebase-tools/node_modules/assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", + "devOptional": true, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/firebase-tools/node_modules/ast-types": { + "version": "0.13.4", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz", + "integrity": "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==", + "devOptional": true, + "dependencies": { + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/firebase-tools/node_modules/ast-types/node_modules/tslib": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz", + "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/async": { + "version": "2.6.4", + "resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz", + "integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==", + "devOptional": true, + "dependencies": { + "lodash": "^4.17.14" + } + }, + "node_modules/firebase-tools/node_modules/async-lock": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/async-lock/-/async-lock-1.3.2.tgz", + "integrity": "sha512-phnXdS3RP7PPcmP6NWWzWMU0sLTeyvtZCxBPpZdkYE3seGLKSQZs9FrmVO/qwypq98FUtWWUEYxziLkdGk5nnA==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/aws-sign2": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", + "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=", + "devOptional": true, + "engines": { + "node": "*" + } + }, + "node_modules/firebase-tools/node_modules/aws4": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz", + "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "devOptional": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/firebase-tools/node_modules/basic-auth": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", + "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==", + "devOptional": true, + "dependencies": { + "safe-buffer": "5.1.2" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/firebase-tools/node_modules/basic-auth-connect": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/basic-auth-connect/-/basic-auth-connect-1.0.0.tgz", + "integrity": "sha1-/bC0OWLKe0BFanwrtI/hc9otISI=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", + "devOptional": true, + "dependencies": { + "tweetnacl": "^0.14.3" + } + }, + "node_modules/firebase-tools/node_modules/bignumber.js": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.0.2.tgz", + "integrity": "sha512-GAcQvbpsM0pUb0zw1EI0KhQEZ+lRwR5fYaAp3vPOYuP7aDvGy6cVN6XHLauvF8SOga2y0dcLcjt3iQDTSEliyw==", + "devOptional": true, + "engines": { + "node": "*" + } + }, + "node_modules/firebase-tools/node_modules/binary-extensions": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.0.0.tgz", + "integrity": "sha512-Phlt0plgpIIBOGTT/ehfFnbNlfsDEiqmzE2KRXoX1bLIlir4X/MR+zSyBEkL05ffWgnRSf/DXv+WrUAVr93/ow==", + "devOptional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "devOptional": true, + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/firebase-tools/node_modules/body-parser": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", + "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==", + "devOptional": true, + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.4", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.11.0", + "raw-body": "2.5.1", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/firebase-tools/node_modules/body-parser/node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "devOptional": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/firebase-tools/node_modules/body-parser/node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "devOptional": true, + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/firebase-tools/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "devOptional": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/firebase-tools/node_modules/braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "devOptional": true, + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "devOptional": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/firebase-tools/node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI=", + "devOptional": true, + "engines": { + "node": "*" + } + }, + "node_modules/firebase-tools/node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "devOptional": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/firebase-tools/node_modules/cacache": { + "version": "16.1.3", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-16.1.3.tgz", + "integrity": "sha512-/+Emcj9DAXxX4cwlLmRI9c166RuL3w30zp4R7Joiv2cQTtTtA+jeuCAjH3ZlGnYS3tKENSrKhAzVVP9GVyzeYQ==", + "optional": true, + "dependencies": { + "@npmcli/fs": "^2.1.0", + "@npmcli/move-file": "^2.0.0", + "chownr": "^2.0.0", + "fs-minipass": "^2.1.0", + "glob": "^8.0.1", + "infer-owner": "^1.0.4", + "lru-cache": "^7.7.1", + "minipass": "^3.1.6", + "minipass-collect": "^1.0.2", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "mkdirp": "^1.0.4", + "p-map": "^4.0.0", + "promise-inflight": "^1.0.1", + "rimraf": "^3.0.2", + "ssri": "^9.0.0", + "tar": "^6.1.11", + "unique-filename": "^2.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/firebase-tools/node_modules/cacache/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "optional": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/firebase-tools/node_modules/cacache/node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "optional": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/firebase-tools/node_modules/cacache/node_modules/glob": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.0.3.tgz", + "integrity": "sha512-ull455NHSHI/Y1FqGaaYFaLGkNMMJbavMrEGFXG/PGrg6y7sutWHUHrz6gy6WEBH6akM1M414dWKCNs+IhKdiQ==", + "optional": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/firebase-tools/node_modules/cacache/node_modules/lru-cache": { + "version": "7.14.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.14.0.tgz", + "integrity": "sha512-EIRtP1GrSJny0dqb50QXRUNBxHJhcpxHC++M5tD7RYbvLLn5KVWKsbyswSSqDuU15UFi3bgTQIY8nhDMeF6aDQ==", + "optional": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/firebase-tools/node_modules/cacache/node_modules/minimatch": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.0.tgz", + "integrity": "sha512-9TPBGGak4nHfGZsPBohm9AWg6NoT7QTCehS3BIJABslyZbzxfV78QM2Y6+i741OPZIafFAaiiEMh5OyIrJPgtg==", + "optional": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/firebase-tools/node_modules/cacache/node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "optional": true, + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/firebase-tools/node_modules/cacache/node_modules/p-map": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", + "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", + "optional": true, + "dependencies": { + "aggregate-error": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/firebase-tools/node_modules/call-bind": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", + "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "devOptional": true, + "dependencies": { + "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/firebase-tools/node_modules/call-me-maybe": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.1.tgz", + "integrity": "sha1-JtII6onje1y95gJQoV8DHBak1ms=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/cardinal": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/cardinal/-/cardinal-2.1.1.tgz", + "integrity": "sha1-fMEFXYItISlU0HsIXeolHMe8VQU=", + "devOptional": true, + "dependencies": { + "ansicolors": "~0.3.2", + "redeyed": "~2.1.0" + }, + "bin": { + "cdl": "bin/cdl.js" + } + }, + "node_modules/firebase-tools/node_modules/caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/catharsis": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/catharsis/-/catharsis-0.9.0.tgz", + "integrity": "sha512-prMTQVpcns/tzFgFVkVp6ak6RykZyWb3gu8ckUpd6YkTlacOd3DXGJjIpD4Q6zJirizvaiAjSSHlOsA+6sNh2A==", + "devOptional": true, + "dependencies": { + "lodash": "^4.17.15" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/firebase-tools/node_modules/chardet": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", + "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/chokidar": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "devOptional": true, + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ], + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/firebase-tools/node_modules/ci-info": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz", + "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/cjson": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/cjson/-/cjson-0.3.3.tgz", + "integrity": "sha1-qS2ceG5b+bkwgGMp7gXV0yYbSvo=", + "devOptional": true, + "dependencies": { + "json-parse-helpfulerror": "^1.0.3" + }, + "engines": { + "node": ">= 0.3.0" + } + }, + "node_modules/firebase-tools/node_modules/clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", + "optional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/firebase-tools/node_modules/cli-boxes": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-2.2.1.tgz", + "integrity": "sha512-y4coMcylgSCdVinjiDBuR8PCC2bLjyGTwEmPb9NHR/QaNU6EUOXcTY/s6VjGMD6ENSEaeQYHCY0GNGS5jfMwPw==", + "devOptional": true, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/firebase-tools/node_modules/cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "devOptional": true, + "dependencies": { + "restore-cursor": "^3.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/cli-spinners": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.6.1.tgz", + "integrity": "sha512-x/5fWmGMnbKQAaNwN+UZlV79qBLM9JFnJuJ03gIi5whrob0xV0ofNVHy9DhwGdsMJQc2OKv0oGmLzvaqvAVv+g==", + "devOptional": true, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/firebase-tools/node_modules/cli-table": { + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/cli-table/-/cli-table-0.3.11.tgz", + "integrity": "sha512-IqLQi4lO0nIB4tcdTpN4LCB9FI3uqrJZK7RC515EnhZ6qBaglkIgICb1wjeAqpdoOabm1+SuQtkXIPdYC93jhQ==", + "devOptional": true, + "dependencies": { + "colors": "1.0.3" + }, + "engines": { + "node": ">= 0.2.0" + } + }, + "node_modules/firebase-tools/node_modules/cli-table3": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.2.tgz", + "integrity": "sha512-QyavHCaIC80cMivimWu4aWHilIpiDpfm3hGmqAmXVL1UsnbLuBSMd21hTX6VY4ZSDSM73ESLeF8TOYId3rBTbw==", + "devOptional": true, + "dependencies": { + "string-width": "^4.2.0" + }, + "engines": { + "node": "10.* || >= 12.*" + }, + "optionalDependencies": { + "@colors/colors": "1.5.0" + } + }, + "node_modules/firebase-tools/node_modules/cli-width": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-3.0.0.tgz", + "integrity": "sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==", + "devOptional": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/firebase-tools/node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "devOptional": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/firebase-tools/node_modules/clone": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha1-2jCcwmPfFZlMaIypAheco8fNfH4=", + "devOptional": true, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/firebase-tools/node_modules/color": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/color/-/color-3.0.0.tgz", + "integrity": "sha512-jCpd5+s0s0t7p3pHQKpnJ0TpQKKdleP71LWcA0aqiljpiuAkOSUFN/dyH8ZwF0hRmFlrIuRhufds1QyEP9EB+w==", + "devOptional": true, + "dependencies": { + "color-convert": "^1.9.1", + "color-string": "^1.5.2" + } + }, + "node_modules/firebase-tools/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "devOptional": true, + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/firebase-tools/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/color-string": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.5.5.tgz", + "integrity": "sha512-jgIoum0OfQfq9Whcfc2z/VhCNcmQjWbey6qBX0vqt7YICflUmBCh9E9CiQD5GSJ+Uehixm3NUwHVhqUAWRivZg==", + "devOptional": true, + "dependencies": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, + "node_modules/firebase-tools/node_modules/color-support": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", + "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", + "optional": true, + "bin": { + "color-support": "bin.js" + } + }, + "node_modules/firebase-tools/node_modules/colorette": { + "version": "2.0.19", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.19.tgz", + "integrity": "sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/colornames": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/colornames/-/colornames-1.1.1.tgz", + "integrity": "sha1-+IiQMGhcfE/54qVZ9Qd+t2qBb5Y=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/colors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.0.3.tgz", + "integrity": "sha1-BDP0TYCWgP3rYO0mDxsMJi6CpAs=", + "devOptional": true, + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/firebase-tools/node_modules/colorspace": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/colorspace/-/colorspace-1.1.2.tgz", + "integrity": "sha512-vt+OoIP2d76xLhjwbBaucYlNSpPsrJWPlBTtwCpQKIu6/CSMutyzX93O/Do0qzpH3YoHEes8YEFXyZ797rEhzQ==", + "devOptional": true, + "dependencies": { + "color": "3.0.x", + "text-hex": "1.0.x" + } + }, + "node_modules/firebase-tools/node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "devOptional": true, + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/firebase-tools/node_modules/commander": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.0.1.tgz", + "integrity": "sha512-IPF4ouhCP+qdlcmCedhxX4xiGBPyigb8v5NeUp+0LyhwLgxMqyp3S0vl7TAPfS/hiP7FC3caI/PB9lTmP8r1NA==", + "devOptional": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/firebase-tools/node_modules/compress-commons": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-4.1.1.tgz", + "integrity": "sha512-QLdDLCKNV2dtoTorqgxngQCMA+gWXkM/Nwu7FpeBhk/RdkzimqC3jueb/FDmaZeXh+uby1jkBqE3xArsLBE5wQ==", + "devOptional": true, + "dependencies": { + "buffer-crc32": "^0.2.13", + "crc32-stream": "^4.0.2", + "normalize-path": "^3.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/firebase-tools/node_modules/compressible": { + "version": "2.0.17", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.17.tgz", + "integrity": "sha512-BGHeLCK1GV7j1bSmQQAi26X+GgWcTjLr/0tzSvMCl3LH1w1IJ4PFSPoV5316b30cneTziC+B1a+3OjoSUcQYmw==", + "devOptional": true, + "dependencies": { + "mime-db": ">= 1.40.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/firebase-tools/node_modules/compression": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.7.4.tgz", + "integrity": "sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==", + "devOptional": true, + "dependencies": { + "accepts": "~1.3.5", + "bytes": "3.0.0", + "compressible": "~2.0.16", + "debug": "2.6.9", + "on-headers": "~1.0.2", + "safe-buffer": "5.1.2", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/firebase-tools/node_modules/compression/node_modules/bytes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", + "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=", + "devOptional": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/firebase-tools/node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/config-chain": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.13.tgz", + "integrity": "sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==", + "devOptional": true, + "dependencies": { + "ini": "^1.3.4", + "proto-list": "~1.2.1" + } + }, + "node_modules/firebase-tools/node_modules/configstore": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/configstore/-/configstore-5.0.1.tgz", + "integrity": "sha512-aMKprgk5YhBNyH25hj8wGt2+D52Sw1DRRIzqBwLp2Ya9mFmY8KPvvtvmna8SxVR9JMZ4kzMD68N22vlaRpkeFA==", + "devOptional": true, + "dependencies": { + "dot-prop": "^5.2.0", + "graceful-fs": "^4.1.2", + "make-dir": "^3.0.0", + "unique-string": "^2.0.0", + "write-file-atomic": "^3.0.0", + "xdg-basedir": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/configstore/node_modules/crypto-random-string": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz", + "integrity": "sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==", + "devOptional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/configstore/node_modules/dot-prop": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.2.0.tgz", + "integrity": "sha512-uEUyaDKoSQ1M4Oq8l45hSE26SnTxL6snNnqvK/VWx5wJhmff5z0FUVJDKDanor/6w3kzE3i7XZOk+7wC0EXr1A==", + "devOptional": true, + "dependencies": { + "is-obj": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/configstore/node_modules/unique-string": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-2.0.0.tgz", + "integrity": "sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==", + "devOptional": true, + "dependencies": { + "crypto-random-string": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/connect": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/connect/-/connect-3.7.0.tgz", + "integrity": "sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ==", + "devOptional": true, + "dependencies": { + "debug": "2.6.9", + "finalhandler": "1.1.2", + "parseurl": "~1.3.3", + "utils-merge": "1.0.1" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/firebase-tools/node_modules/console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==", + "optional": true + }, + "node_modules/firebase-tools/node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "devOptional": true, + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/firebase-tools/node_modules/content-disposition/node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "devOptional": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/firebase-tools/node_modules/content-type": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", + "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==", + "devOptional": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/firebase-tools/node_modules/cookie": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", + "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", + "devOptional": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/firebase-tools/node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "devOptional": true, + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/firebase-tools/node_modules/crc-32": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", + "devOptional": true, + "bin": { + "crc32": "bin/crc32.njs" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/firebase-tools/node_modules/crc32-stream": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-4.0.2.tgz", + "integrity": "sha512-DxFZ/Hk473b/muq1VJ///PMNLj0ZMnzye9thBpmjpJKCc5eMgB95aK8zCGrGfQ90cWo561Te6HK9D+j4KPdM6w==", + "devOptional": true, + "dependencies": { + "crc-32": "^1.2.0", + "readable-stream": "^3.4.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/firebase-tools/node_modules/cross-env": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-5.2.0.tgz", + "integrity": "sha512-jtdNFfFW1hB7sMhr/H6rW1Z45LFqyI431m3qU6bFXcQ3Eh7LtBuG3h74o7ohHZ3crrRkkqHlo4jYHFPcjroANg==", + "devOptional": true, + "dependencies": { + "cross-spawn": "^6.0.5", + "is-windows": "^1.0.0" + }, + "bin": { + "cross-env": "dist/bin/cross-env.js", + "cross-env-shell": "dist/bin/cross-env-shell.js" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/firebase-tools/node_modules/cross-env/node_modules/cross-spawn": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", + "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", + "devOptional": true, + "dependencies": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + }, + "engines": { + "node": ">=4.8" + } + }, + "node_modules/firebase-tools/node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "devOptional": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/firebase-tools/node_modules/cross-spawn/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==", + "devOptional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/cross-spawn/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==", + "devOptional": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/cross-spawn/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==", + "devOptional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/cross-spawn/node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "devOptional": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/firebase-tools/node_modules/csv-parse": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/csv-parse/-/csv-parse-5.0.4.tgz", + "integrity": "sha512-5AIdl8l6n3iYQYxan5djB5eKDa+vBnhfWZtRpJTcrETWfVLYN0WSj3L9RwvgYt+psoO77juUr8TG8qpfGZifVQ==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", + "devOptional": true, + "dependencies": { + "assert-plus": "^1.0.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/firebase-tools/node_modules/data-uri-to-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-3.0.1.tgz", + "integrity": "sha512-WboRycPNsVw3B3TL559F7kuBUM4d8CgMEvk6xEJlOp7OBPjt6G7z8WMWlD2rOFZLk6OYfFIUGsCOWzcQH9K2og==", + "devOptional": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/firebase-tools/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "devOptional": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/firebase-tools/node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "devOptional": true, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/firebase-tools/node_modules/deep-freeze": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/deep-freeze/-/deep-freeze-0.0.1.tgz", + "integrity": "sha1-OgsABd4YZygZ39OM0x+RF5yJPoQ=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/deep-is": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", + "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/defaults": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.3.tgz", + "integrity": "sha1-xlYFHpgX2f8I7YgUd/P+QBnz730=", + "devOptional": true, + "dependencies": { + "clone": "^1.0.2" + } + }, + "node_modules/firebase-tools/node_modules/degenerator": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-3.0.1.tgz", + "integrity": "sha512-LFsIFEeLPlKvAKXu7j3ssIG6RT0TbI7/GhsqrI0DnHASEQjXQ0LUSYcjJteGgRGmZbl1TnMSxpNQIAiJ7Du5TQ==", + "devOptional": true, + "dependencies": { + "ast-types": "^0.13.2", + "escodegen": "^1.8.1", + "esprima": "^4.0.0", + "vm2": "^3.9.3" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/firebase-tools/node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", + "devOptional": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/firebase-tools/node_modules/delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", + "optional": true + }, + "node_modules/firebase-tools/node_modules/depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=", + "optional": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/firebase-tools/node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "devOptional": true, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/firebase-tools/node_modules/diagnostics": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/diagnostics/-/diagnostics-1.1.1.tgz", + "integrity": "sha512-8wn1PmdunLJ9Tqbx+Fx/ZEuHfJf4NKSN2ZBj7SJC/OWRWha843+WsTjqMe1B5E3p28jqBlp+mJ2fPVxPyNgYKQ==", + "devOptional": true, + "dependencies": { + "colorspace": "1.1.x", + "enabled": "1.0.x", + "kuler": "1.0.x" + } + }, + "node_modules/firebase-tools/node_modules/duplexify": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.1.tgz", + "integrity": "sha512-DY3xVEmVHTv1wSzKNbwoU6nVjzI369Y6sPoqfYr0/xlx3IdX2n94xIszTcjPO8W8ZIv0Wb0PXNcjuZyT4wiICA==", + "devOptional": true, + "dependencies": { + "end-of-stream": "^1.4.1", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1", + "stream-shift": "^1.0.0" + } + }, + "node_modules/firebase-tools/node_modules/ecc-jsbn": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", + "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", + "devOptional": true, + "dependencies": { + "jsbn": "~0.1.0", + "safer-buffer": "^2.1.0" + } + }, + "node_modules/firebase-tools/node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "devOptional": true, + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/firebase-tools/node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/enabled": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/enabled/-/enabled-1.0.2.tgz", + "integrity": "sha1-ll9lE9LC0cX0ZStkouM5ZGf8L5M=", + "devOptional": true, + "dependencies": { + "env-variable": "0.0.x" + } + }, + "node_modules/firebase-tools/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=", + "devOptional": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/firebase-tools/node_modules/encoding": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", + "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", + "optional": true, + "dependencies": { + "iconv-lite": "^0.6.2" + } + }, + "node_modules/firebase-tools/node_modules/encoding/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "optional": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/firebase-tools/node_modules/end-of-stream": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.1.tgz", + "integrity": "sha512-1MkrZNvWTKCaigbn+W15elq2BB/L22nqrSY5DKlo3X6+vclJm8Bb5djXJBmEX6fS3+zCh/F4VBK5Z2KxJt4s2Q==", + "devOptional": true, + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/firebase-tools/node_modules/entities": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.1.0.tgz", + "integrity": "sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w==", + "devOptional": true, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/firebase-tools/node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "optional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/firebase-tools/node_modules/env-variable": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/env-variable/-/env-variable-0.0.6.tgz", + "integrity": "sha512-bHz59NlBbtS0NhftmR8+ExBEekE7br0e01jw+kk0NDro7TtZzBYZ5ScGPs3OmwnpyfHTHOtr1Y6uedCdrIldtg==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/err-code": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", + "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", + "optional": true + }, + "node_modules/firebase-tools/node_modules/escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "devOptional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/firebase-tools/node_modules/escape-goat": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/escape-goat/-/escape-goat-2.1.1.tgz", + "integrity": "sha512-8/uIhbG12Csjy2JEW7D9pHbreaVaS/OpN3ycnyvElTdwM5n6GY6W6e2IPemfvGZeUMqZ9A/3GqIZMgKnBhAw/Q==", + "devOptional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "devOptional": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/firebase-tools/node_modules/escodegen": { + "version": "1.14.3", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.14.3.tgz", + "integrity": "sha512-qFcX0XJkdg+PB3xjZZG/wKSuT1PnQWx57+TVSjIMmILd2yC/6ByYElPwJnslDsuWuSAp4AwJGumarAAmJch5Kw==", + "devOptional": true, + "dependencies": { + "esprima": "^4.0.1", + "estraverse": "^4.2.0", + "esutils": "^2.0.2", + "optionator": "^0.8.1" + }, + "bin": { + "escodegen": "bin/escodegen.js", + "esgenerate": "bin/esgenerate.js" + }, + "engines": { + "node": ">=4.0" + }, + "optionalDependencies": { + "source-map": "~0.6.1" + } + }, + "node_modules/firebase-tools/node_modules/eslint-visitor-keys": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz", + "integrity": "sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA==", + "devOptional": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/firebase-tools/node_modules/espree": { + "version": "9.4.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.4.1.tgz", + "integrity": "sha512-XwctdmTO6SIvCzd9810yyNzIrOrqNYV9Koizx4C/mRhf9uq0o4yHoCEU/670pOxOL/MSraektvSAji79kX90Vg==", + "devOptional": true, + "dependencies": { + "acorn": "^8.8.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/firebase-tools/node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "devOptional": true, + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/firebase-tools/node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "devOptional": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/firebase-tools/node_modules/esutils": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz", + "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=", + "devOptional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/firebase-tools/node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "devOptional": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/firebase-tools/node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "devOptional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/firebase-tools/node_modules/events-listener": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/events-listener/-/events-listener-1.1.0.tgz", + "integrity": "sha512-Kd3EgYfODHueq6GzVfs/VUolh2EgJsS8hkO3KpnDrxVjU3eq63eXM2ujXkhPP+OkeUOhL8CxdfZbQXzryb5C4g==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/exegesis": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/exegesis/-/exegesis-4.1.0.tgz", + "integrity": "sha512-iqc55n+hmv8d1KYNMjq7bCcp4u74oRY6MBcj6Vsux7Wd4mRvlgahKqrBTyLIWwscNjEF3qvPmeJ0RPTj8ORMNg==", + "devOptional": true, + "dependencies": { + "@apidevtools/json-schema-ref-parser": "^9.0.3", + "ajv": "^8.3.0", + "ajv-formats": "^2.1.0", + "body-parser": "^1.18.3", + "content-type": "^1.0.4", + "deep-freeze": "0.0.1", + "events-listener": "^1.1.0", + "glob": "^7.1.3", + "json-ptr": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "lodash": "^4.17.11", + "openapi3-ts": "^2.0.1", + "promise-breaker": "^5.0.0", + "pump": "^3.0.0", + "qs": "^6.6.0", + "raw-body": "^2.3.3", + "semver": "^7.0.0" + }, + "engines": { + "node": ">=6.0.0", + "npm": ">5.0.0" + } + }, + "node_modules/firebase-tools/node_modules/exegesis-express": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/exegesis-express/-/exegesis-express-4.0.0.tgz", + "integrity": "sha512-V2hqwTtYRj0bj43K4MCtm0caD97YWkqOUHFMRCBW5L1x9IjyqOEc7Xa4oQjjiFbeFOSQzzwPV+BzXsQjSz08fw==", + "devOptional": true, + "dependencies": { + "exegesis": "^4.1.0" + }, + "engines": { + "node": ">=6.0.0", + "npm": ">5.0.0" + } + }, + "node_modules/firebase-tools/node_modules/exegesis/node_modules/ajv": { + "version": "8.8.2", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.8.2.tgz", + "integrity": "sha512-x9VuX+R/jcFj1DHo/fCp99esgGDWiHENrKxaCENuCxpoMCmAt/COCGVDwA7kleEpEzJjDnvh3yGoOuLu0Dtllw==", + "devOptional": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/firebase-tools/node_modules/exegesis/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/exegesis/node_modules/semver": { + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", + "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", + "devOptional": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/firebase-tools/node_modules/express": { + "version": "4.18.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz", + "integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==", + "devOptional": true, + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.1", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.5.0", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.2.0", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.7", + "qs": "6.11.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.18.0", + "serve-static": "1.15.0", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/firebase-tools/node_modules/express/node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "devOptional": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/firebase-tools/node_modules/express/node_modules/finalhandler": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", + "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", + "devOptional": true, + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/firebase-tools/node_modules/express/node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "devOptional": true, + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/firebase-tools/node_modules/express/node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "devOptional": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/firebase-tools/node_modules/express/node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "devOptional": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/firebase-tools/node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/external-editor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", + "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", + "devOptional": true, + "dependencies": { + "chardet": "^0.7.0", + "iconv-lite": "^0.4.24", + "tmp": "^0.0.33" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/firebase-tools/node_modules/external-editor/node_modules/tmp": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "devOptional": true, + "dependencies": { + "os-tmpdir": "~1.0.2" + }, + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/firebase-tools/node_modules/extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=", + "devOptional": true, + "engines": [ + "node >=0.6.0" + ] + }, + "node_modules/firebase-tools/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==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/fast-json-stable-stringify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", + "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/fast-safe-stringify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", + "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/fast-text-encoding": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/fast-text-encoding/-/fast-text-encoding-1.0.3.tgz", + "integrity": "sha512-dtm4QZH9nZtcDt8qJiOH9fcQd1NAgi+K1O2DbE6GG1PPCK/BWfOH3idCTRQ4ImXRUOyopDEgDEnVEE7Y/2Wrig==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/fast-url-parser": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/fast-url-parser/-/fast-url-parser-1.1.3.tgz", + "integrity": "sha1-9K8+qfNNiicc9YrSs3WfQx8LMY0=", + "devOptional": true, + "dependencies": { + "punycode": "^1.3.2" + } + }, + "node_modules/firebase-tools/node_modules/fast-url-parser/node_modules/punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/fecha": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fecha/-/fecha-2.3.3.tgz", + "integrity": "sha512-lUGBnIamTAwk4znq5BcqsDaxSmZ9nDVJaij6NvRt/Tg4R69gERA+otPKbS86ROw9nxVMw2/mp1fnaiWqbs6Sdg==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/figures": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", + "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", + "devOptional": true, + "dependencies": { + "escape-string-regexp": "^1.0.5" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/firebase-tools/node_modules/file-uri-to-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-2.0.0.tgz", + "integrity": "sha512-hjPFI8oE/2iQPVe4gbrJ73Pp+Xfub2+WI2LlXDbsaJBwT5wuMh35WNWVYYTpnz895shtwfyutMFLFywpQAFdLg==", + "devOptional": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/firebase-tools/node_modules/filesize": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/filesize/-/filesize-6.1.0.tgz", + "integrity": "sha512-LpCHtPQ3sFx67z+uh2HnSyWSLLu5Jxo21795uRDuar/EOuYWXib5EmPaGIBuSnRqH2IODiKA2k5re/K9OnN/Yg==", + "devOptional": true, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/firebase-tools/node_modules/fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "devOptional": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/finalhandler": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", + "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", + "devOptional": true, + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "statuses": "~1.5.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/firebase-tools/node_modules/forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=", + "devOptional": true, + "engines": { + "node": "*" + } + }, + "node_modules/firebase-tools/node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "devOptional": true, + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/firebase-tools/node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "devOptional": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/firebase-tools/node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=", + "devOptional": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/firebase-tools/node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "devOptional": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/firebase-tools/node_modules/fs-extra/node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "devOptional": true, + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/firebase-tools/node_modules/fs-extra/node_modules/universalify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", + "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", + "devOptional": true, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/firebase-tools/node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "devOptional": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/firebase-tools/node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/firebase-tools/node_modules/ftp": { + "version": "0.3.10", + "resolved": "https://registry.npmjs.org/ftp/-/ftp-0.3.10.tgz", + "integrity": "sha1-kZfYYa2BQvPmPVqDv+TFn3MwiF0=", + "devOptional": true, + "dependencies": { + "readable-stream": "1.1.x", + "xregexp": "2.0.0" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/firebase-tools/node_modules/ftp/node_modules/isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/ftp/node_modules/readable-stream": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", + "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", + "devOptional": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + }, + "node_modules/firebase-tools/node_modules/ftp/node_modules/string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/gauge": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-4.0.4.tgz", + "integrity": "sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg==", + "optional": true, + "dependencies": { + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.3", + "console-control-strings": "^1.1.0", + "has-unicode": "^2.0.1", + "signal-exit": "^3.0.7", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.5" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/firebase-tools/node_modules/gaxios": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-4.1.0.tgz", + "integrity": "sha512-vb0to8xzGnA2qcgywAjtshOKKVDf2eQhJoiL6fHhgW5tVN7wNk7egnYIO9zotfn3lQ3De1VPdf7V5/BWfCtCmg==", + "devOptional": true, + "dependencies": { + "abort-controller": "^3.0.0", + "extend": "^3.0.2", + "https-proxy-agent": "^5.0.0", + "is-stream": "^2.0.0", + "node-fetch": "^2.3.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/firebase-tools/node_modules/gaxios/node_modules/is-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.0.tgz", + "integrity": "sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw==", + "devOptional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/gcp-metadata": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-4.2.0.tgz", + "integrity": "sha512-vQZD57cQkqIA6YPGXM/zc+PIZfNRFdukWGsGZ5+LcJzesi5xp6Gn7a02wRJi4eXPyArNMIYpPET4QMxGqtlk6Q==", + "devOptional": true, + "dependencies": { + "gaxios": "^3.0.0", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/firebase-tools/node_modules/gcp-metadata/node_modules/gaxios": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-3.2.0.tgz", + "integrity": "sha512-+6WPeVzPvOshftpxJwRi2Ozez80tn/hdtOUag7+gajDHRJvAblKxTFSSMPtr2hmnLy7p0mvYz0rMXLBl8pSO7Q==", + "devOptional": true, + "dependencies": { + "abort-controller": "^3.0.0", + "extend": "^3.0.2", + "https-proxy-agent": "^5.0.0", + "is-stream": "^2.0.0", + "node-fetch": "^2.3.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/firebase-tools/node_modules/gcp-metadata/node_modules/is-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.0.tgz", + "integrity": "sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw==", + "devOptional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "devOptional": true, + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/firebase-tools/node_modules/get-intrinsic": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.3.tgz", + "integrity": "sha512-QJVz1Tj7MS099PevUG5jvnt9tSkXN8K14dxQlikJuPt4uD9hHAHjLyLBiLR5zELelBdD9QNRAXZzsJx0WaDL9A==", + "devOptional": true, + "dependencies": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/firebase-tools/node_modules/get-uri": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-3.0.2.tgz", + "integrity": "sha512-+5s0SJbGoyiJTZZ2JTpFPLMPSch72KEqGOTvQsBqg0RBWvwhWUSYZFAtz3TPW0GXJuLBJPts1E241iHg+VRfhg==", + "devOptional": true, + "dependencies": { + "@tootallnate/once": "1", + "data-uri-to-buffer": "3", + "debug": "4", + "file-uri-to-path": "2", + "fs-extra": "^8.1.0", + "ftp": "^0.3.10" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/firebase-tools/node_modules/get-uri/node_modules/debug": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", + "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", + "devOptional": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/firebase-tools/node_modules/get-uri/node_modules/fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "devOptional": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/firebase-tools/node_modules/get-uri/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", + "devOptional": true, + "dependencies": { + "assert-plus": "^1.0.0" + } + }, + "node_modules/firebase-tools/node_modules/glob": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", + "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", + "devOptional": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/firebase-tools/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==", + "devOptional": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/firebase-tools/node_modules/glob-slash": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/glob-slash/-/glob-slash-1.0.0.tgz", + "integrity": "sha1-/lLvpDMjP3Si/mTHq7m8hIICq5U=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/glob-slasher": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/glob-slasher/-/glob-slasher-1.0.1.tgz", + "integrity": "sha1-dHoOW7IiZC7hDT4FRD4QlJPLD44=", + "devOptional": true, + "dependencies": { + "glob-slash": "^1.0.0", + "lodash.isobject": "^2.4.1", + "toxic": "^1.0.0" + } + }, + "node_modules/firebase-tools/node_modules/global-dirs": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-3.0.1.tgz", + "integrity": "sha512-NBcGGFbBA9s1VzD41QXDG+3++t9Mn5t1FpLdhESY6oKY4gYTFpX4wO3sqGUa0Srjtbfj3szX0RnemmrVRUdULA==", + "devOptional": true, + "dependencies": { + "ini": "2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/firebase-tools/node_modules/global-dirs/node_modules/ini": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ini/-/ini-2.0.0.tgz", + "integrity": "sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA==", + "devOptional": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/firebase-tools/node_modules/google-auth-library": { + "version": "7.14.1", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-7.14.1.tgz", + "integrity": "sha512-5Rk7iLNDFhFeBYc3s8l1CqzbEBcdhwR193RlD4vSNFajIcINKI8W8P0JLmBpwymHqqWbX34pJDQu39cSy/6RsA==", + "devOptional": true, + "dependencies": { + "arrify": "^2.0.0", + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "fast-text-encoding": "^1.0.0", + "gaxios": "^4.0.0", + "gcp-metadata": "^4.2.0", + "gtoken": "^5.0.4", + "jws": "^4.0.0", + "lru-cache": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/firebase-tools/node_modules/google-auth-library/node_modules/jwa": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz", + "integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==", + "devOptional": true, + "dependencies": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/firebase-tools/node_modules/google-auth-library/node_modules/jws": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", + "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", + "devOptional": true, + "dependencies": { + "jwa": "^2.0.0", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/firebase-tools/node_modules/google-gax": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/google-gax/-/google-gax-3.5.3.tgz", + "integrity": "sha512-caItkifbcPDVf5xW2j6xLXmuX9bh2dheJN9AzMuQj6VvWacUVV5d0BkjL+Ia8sgX9VvXtbqjf4NKK3nTgV9UUQ==", + "devOptional": true, + "dependencies": { + "@grpc/grpc-js": "~1.7.0", + "@grpc/proto-loader": "^0.7.0", + "@types/long": "^4.0.0", + "abort-controller": "^3.0.0", + "duplexify": "^4.0.0", + "fast-text-encoding": "^1.0.3", + "google-auth-library": "^8.0.2", + "is-stream-ended": "^0.1.4", + "node-fetch": "^2.6.1", + "object-hash": "^3.0.0", + "proto3-json-serializer": "^1.0.0", + "protobufjs": "7.1.2", + "protobufjs-cli": "1.1.0", + "retry-request": "^5.0.0" + }, + "bin": { + "compileProtos": "build/tools/compileProtos.js", + "minifyProtoJson": "build/tools/minify.js" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/firebase-tools/node_modules/google-gax/node_modules/@grpc/proto-loader": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.4.tgz", + "integrity": "sha512-MnWjkGwqQ3W8fx94/c1CwqLsNmHHv2t0CFn+9++6+cDphC1lolpg9M2OU0iebIjK//pBNX9e94ho+gjx6vz39w==", + "devOptional": true, + "dependencies": { + "@types/long": "^4.0.1", + "lodash.camelcase": "^4.3.0", + "long": "^4.0.0", + "protobufjs": "^7.0.0", + "yargs": "^16.2.0" + }, + "bin": { + "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/firebase-tools/node_modules/google-gax/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==", + "devOptional": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/firebase-tools/node_modules/google-gax/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "devOptional": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/firebase-tools/node_modules/google-gax/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "devOptional": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/firebase-tools/node_modules/google-gax/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==", + "devOptional": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/firebase-tools/node_modules/google-gax/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==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/google-gax/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "devOptional": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/firebase-tools/node_modules/google-gax/node_modules/gaxios": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-5.0.2.tgz", + "integrity": "sha512-TjtV2AJOZoMQqRYoy5eM8cCQogYwazWNYLQ72QB0kwa6vHHruYkGmhhyrlzbmgNHK1dNnuP2WSH81urfzyN2Og==", + "devOptional": true, + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^5.0.0", + "is-stream": "^2.0.0", + "node-fetch": "^2.6.7" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/firebase-tools/node_modules/google-gax/node_modules/gcp-metadata": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-5.2.0.tgz", + "integrity": "sha512-aFhhvvNycky2QyhG+dcfEdHBF0FRbYcf39s6WNHUDysKSrbJ5vuFbjydxBcmewtXeV248GP8dWT3ByPNxsyHCw==", + "devOptional": true, + "dependencies": { + "gaxios": "^5.0.0", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/firebase-tools/node_modules/google-gax/node_modules/glob": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "devOptional": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/firebase-tools/node_modules/google-gax/node_modules/google-auth-library": { + "version": "8.7.0", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-8.7.0.tgz", + "integrity": "sha512-1M0NG5VDIvJZEnstHbRdckLZESoJwguinwN8Dhae0j2ZKIQFIV63zxm6Fo6nM4xkgqUr2bbMtV5Dgo+Hy6oo0Q==", + "devOptional": true, + "dependencies": { + "arrify": "^2.0.0", + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "fast-text-encoding": "^1.0.0", + "gaxios": "^5.0.0", + "gcp-metadata": "^5.0.0", + "gtoken": "^6.1.0", + "jws": "^4.0.0", + "lru-cache": "^6.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/firebase-tools/node_modules/google-gax/node_modules/google-p12-pem": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/google-p12-pem/-/google-p12-pem-4.0.1.tgz", + "integrity": "sha512-WPkN4yGtz05WZ5EhtlxNDWPhC4JIic6G8ePitwUWy4l+XPVYec+a0j0Ts47PDtW59y3RwAhUd9/h9ZZ63px6RQ==", + "devOptional": true, + "dependencies": { + "node-forge": "^1.3.1" + }, + "bin": { + "gp12-pem": "build/src/bin/gp12-pem.js" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/firebase-tools/node_modules/google-gax/node_modules/gtoken": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-6.1.2.tgz", + "integrity": "sha512-4ccGpzz7YAr7lxrT2neugmXQ3hP9ho2gcaityLVkiUecAiwiy60Ii8gRbZeOsXV19fYaRjgBSshs8kXw+NKCPQ==", + "devOptional": true, + "dependencies": { + "gaxios": "^5.0.1", + "google-p12-pem": "^4.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/firebase-tools/node_modules/google-gax/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==", + "devOptional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/google-gax/node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "devOptional": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/firebase-tools/node_modules/google-gax/node_modules/jwa": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz", + "integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==", + "devOptional": true, + "dependencies": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/firebase-tools/node_modules/google-gax/node_modules/jws": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", + "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", + "devOptional": true, + "dependencies": { + "jwa": "^2.0.0", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/firebase-tools/node_modules/google-gax/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "devOptional": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/firebase-tools/node_modules/google-gax/node_modules/protobufjs": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.1.2.tgz", + "integrity": "sha512-4ZPTPkXCdel3+L81yw3dG6+Kq3umdWKh7Dc7GW/CpNk4SX3hK58iPCWeCyhVTDrbkNeKrYNZ7EojM5WDaEWTLQ==", + "devOptional": true, + "hasInstallScript": true, + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/node": ">=13.7.0", + "long": "^5.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/firebase-tools/node_modules/google-gax/node_modules/protobufjs-cli": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/protobufjs-cli/-/protobufjs-cli-1.1.0.tgz", + "integrity": "sha512-VXMQn+z3yG2WbN2E+mx5vcyIHF7yJSg2jqyqfxcZLWNOSTqUzSSgAE5vu04/JEpwxTI04JGyrZRDHC36wr04uw==", + "devOptional": true, + "dependencies": { + "chalk": "^4.0.0", + "escodegen": "^1.13.0", + "espree": "^9.0.0", + "estraverse": "^5.1.0", + "glob": "^8.0.0", + "jsdoc": "^4.0.0", + "minimist": "^1.2.0", + "semver": "^7.1.2", + "tmp": "^0.2.1", + "uglify-js": "^3.7.7" + }, + "bin": { + "pbjs": "bin/pbjs", + "pbts": "bin/pbts" + }, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "protobufjs": "^7.0.0" + } + }, + "node_modules/firebase-tools/node_modules/google-gax/node_modules/protobufjs/node_modules/long": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/long/-/long-5.2.1.tgz", + "integrity": "sha512-GKSNGeNAtw8IryjjkhZxuKB3JzlcLTwjtiQCHKvqQet81I93kXslhDQruGI/QsddO83mcDToBVy7GqGS/zYf/A==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/google-gax/node_modules/semver": { + "version": "7.3.8", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", + "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", + "devOptional": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/firebase-tools/node_modules/google-gax/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==", + "devOptional": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/google-p12-pem": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/google-p12-pem/-/google-p12-pem-3.1.3.tgz", + "integrity": "sha512-MC0jISvzymxePDVembypNefkAQp+DRP7dBE+zNUPaIjEspIlYg0++OrsNr248V9tPbz6iqtZ7rX1hxWA5B8qBQ==", + "devOptional": true, + "dependencies": { + "node-forge": "^1.0.0" + }, + "bin": { + "gp12-pem": "build/src/bin/gp12-pem.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/firebase-tools/node_modules/graceful-fs": { + "version": "4.2.10", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", + "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/gtoken": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-5.3.2.tgz", + "integrity": "sha512-gkvEKREW7dXWF8NV8pVrKfW7WqReAmjjkMBh6lNCCGOM4ucS0r0YyXXl0r/9Yj8wcW/32ISkfc8h5mPTDbtifQ==", + "devOptional": true, + "dependencies": { + "gaxios": "^4.0.0", + "google-p12-pem": "^3.1.3", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/firebase-tools/node_modules/gtoken/node_modules/jwa": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz", + "integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==", + "devOptional": true, + "dependencies": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/firebase-tools/node_modules/gtoken/node_modules/jws": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", + "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", + "devOptional": true, + "dependencies": { + "jwa": "^2.0.0", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/firebase-tools/node_modules/har-schema": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", + "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=", + "devOptional": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/firebase-tools/node_modules/har-validator": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.3.tgz", + "integrity": "sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==", + "deprecated": "this library is no longer supported", + "devOptional": true, + "dependencies": { + "ajv": "^6.5.5", + "har-schema": "^2.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/firebase-tools/node_modules/has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "devOptional": true, + "dependencies": { + "function-bind": "^1.1.1" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/firebase-tools/node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "devOptional": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/firebase-tools/node_modules/has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==", + "optional": true + }, + "node_modules/firebase-tools/node_modules/has-yarn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/has-yarn/-/has-yarn-2.1.0.tgz", + "integrity": "sha512-UqBRqi4ju7T+TqGNdqAO0PaSVGsDGJUBQvk9eUWNGRY1CFGDzYhLWoM7JQEemnlvVcv/YEmc2wNW8BC24EnUsw==", + "devOptional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/http-cache-semantics": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", + "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==", + "optional": true + }, + "node_modules/firebase-tools/node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "devOptional": true, + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/firebase-tools/node_modules/http-errors/node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "devOptional": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/firebase-tools/node_modules/http-errors/node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "devOptional": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/firebase-tools/node_modules/http-proxy-agent": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz", + "integrity": "sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==", + "devOptional": true, + "dependencies": { + "@tootallnate/once": "1", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/firebase-tools/node_modules/http-proxy-agent/node_modules/debug": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "devOptional": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/firebase-tools/node_modules/http-proxy-agent/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/http-signature": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", + "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", + "devOptional": true, + "dependencies": { + "assert-plus": "^1.0.0", + "jsprim": "^1.2.2", + "sshpk": "^1.7.0" + }, + "engines": { + "node": ">=0.8", + "npm": ">=1.3.7" + } + }, + "node_modules/firebase-tools/node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "devOptional": true, + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/firebase-tools/node_modules/https-proxy-agent/node_modules/debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "deprecated": "Debug versions >=3.2.0 <3.2.7 || >=4 <4.3.1 have a low-severity ReDos regression when used in a Node.js environment. It is recommended you upgrade to 3.2.7 or 4.3.1. (https://github.com/visionmedia/debug/issues/797)", + "devOptional": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/firebase-tools/node_modules/https-proxy-agent/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/humanize-ms": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", + "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", + "optional": true, + "dependencies": { + "ms": "^2.0.0" + } + }, + "node_modules/firebase-tools/node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "devOptional": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/firebase-tools/node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "devOptional": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/firebase-tools/node_modules/import-lazy": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/import-lazy/-/import-lazy-2.1.0.tgz", + "integrity": "sha1-BWmOPUXIjo1+nZLLBYTnfwlvPkM=", + "devOptional": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/firebase-tools/node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", + "devOptional": true, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/firebase-tools/node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/infer-owner": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/infer-owner/-/infer-owner-1.0.4.tgz", + "integrity": "sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==", + "optional": true + }, + "node_modules/firebase-tools/node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "devOptional": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/firebase-tools/node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/inquirer": { + "version": "8.2.5", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-8.2.5.tgz", + "integrity": "sha512-QAgPDQMEgrDssk1XiwwHoOGYF9BAbUcc1+j+FhEvaOt8/cKRqyLn0U5qA6F74fGhTMGxf92pOvPBeh29jQJDTQ==", + "devOptional": true, + "dependencies": { + "ansi-escapes": "^4.2.1", + "chalk": "^4.1.1", + "cli-cursor": "^3.1.0", + "cli-width": "^3.0.0", + "external-editor": "^3.0.3", + "figures": "^3.0.0", + "lodash": "^4.17.21", + "mute-stream": "0.0.8", + "ora": "^5.4.1", + "run-async": "^2.4.0", + "rxjs": "^7.5.5", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0", + "through": "^2.3.6", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/firebase-tools/node_modules/inquirer/node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "devOptional": true, + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/firebase-tools/node_modules/inquirer/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "devOptional": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/firebase-tools/node_modules/inquirer/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "devOptional": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/firebase-tools/node_modules/inquirer/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "devOptional": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/firebase-tools/node_modules/inquirer/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/inquirer/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "devOptional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/inquirer/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "devOptional": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/inquirer/node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "devOptional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/firebase-tools/node_modules/install-artifact-from-github": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/install-artifact-from-github/-/install-artifact-from-github-1.3.1.tgz", + "integrity": "sha512-3l3Bymg2eKDsN5wQuMfgGEj2x6l5MCAv0zPL6rxHESufFVlEAKW/6oY9F1aGgvY/EgWm5+eWGRjINveL4X7Hgg==", + "optional": true, + "bin": { + "install-from-cache": "bin/install-from-cache.js", + "save-to-github-cache": "bin/save-to-github-cache.js" + } + }, + "node_modules/firebase-tools/node_modules/ip": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.5.tgz", + "integrity": "sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/ip-regex": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-4.3.0.tgz", + "integrity": "sha512-B9ZWJxHHOHUhUjCPrMpLD4xEq35bUTClHM1S6CBU5ixQnkZmwipwgc96vAd7AAGM9TGHvJR+Uss+/Ak6UphK+Q==", + "devOptional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "devOptional": true, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/firebase-tools/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==", + "devOptional": true, + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/is-ci": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-2.0.0.tgz", + "integrity": "sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w==", + "devOptional": true, + "dependencies": { + "ci-info": "^2.0.0" + }, + "bin": { + "is-ci": "bin.js" + } + }, + "node_modules/firebase-tools/node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", + "devOptional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/firebase-tools/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "devOptional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/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==", + "devOptional": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/firebase-tools/node_modules/is-installed-globally": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.4.0.tgz", + "integrity": "sha512-iwGqO3J21aaSkC7jWnHP/difazwS7SFeIqxv6wEtLU8Y5KlzFTjyqcSIT0d8s4+dDhKytsk9PJZ2BkS5eZwQRQ==", + "devOptional": true, + "dependencies": { + "global-dirs": "^3.0.0", + "is-path-inside": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/firebase-tools/node_modules/is-interactive": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", + "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", + "devOptional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/is-lambda": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-lambda/-/is-lambda-1.0.1.tgz", + "integrity": "sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==", + "optional": true + }, + "node_modules/firebase-tools/node_modules/is-npm": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-npm/-/is-npm-5.0.0.tgz", + "integrity": "sha512-WW/rQLOazUq+ST/bCAVBp/2oMERWLsR7OrKyt052dNDk4DHcDE0/7QSXITlmi+VBcV13DfIbysG3tZJm5RfdBA==", + "devOptional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/firebase-tools/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==", + "devOptional": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/firebase-tools/node_modules/is-obj": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", + "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==", + "devOptional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/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==", + "devOptional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=", + "devOptional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/firebase-tools/node_modules/is-stream-ended": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-stream-ended/-/is-stream-ended-0.1.4.tgz", + "integrity": "sha512-xj0XPvmr7bQFTvirqnFr50o0hQIh6ZItDqloxt5aJrR4NQsYeSsyFQERYGCAzfindAcnKjINnwEEgLx4IqVzQw==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "devOptional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/firebase-tools/node_modules/is-url": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/is-url/-/is-url-1.2.4.tgz", + "integrity": "sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/is-windows": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", + "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", + "devOptional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/firebase-tools/node_modules/is-wsl": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-1.1.0.tgz", + "integrity": "sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0=", + "devOptional": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/firebase-tools/node_modules/is-yarn-global": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/is-yarn-global/-/is-yarn-global-0.3.0.tgz", + "integrity": "sha512-VjSeb/lHmkoyd8ryPVIKvOCn4D1koMqY+vqyjjUfc3xyKtP4dYOxM44sZrnqQSzSds3xyOrUTLTC9LVCVgLngw==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/is2": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/is2/-/is2-2.0.7.tgz", + "integrity": "sha512-4vBQoURAXC6hnLFxD4VW7uc04XiwTTl/8ydYJxKvPwkWQrSjInkuM5VZVg6BGr1/natq69zDuvO9lGpLClJqvA==", + "devOptional": true, + "dependencies": { + "deep-is": "^0.1.3", + "ip-regex": "^4.1.0", + "is-url": "^1.2.4" + }, + "engines": { + "node": ">=v0.10.0" + } + }, + "node_modules/firebase-tools/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/isomorphic-fetch": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-3.0.0.tgz", + "integrity": "sha512-qvUtwJ3j6qwsF3jLxkZ72qCgjMysPzDfeV240JHiGZsANBYd+EEuu35v7dfrJ9Up0Ak07D7GGSkGhCHTqg/5wA==", + "devOptional": true, + "dependencies": { + "node-fetch": "^2.6.1", + "whatwg-fetch": "^3.4.1" + } + }, + "node_modules/firebase-tools/node_modules/isomorphic-fetch/node_modules/whatwg-fetch": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.2.tgz", + "integrity": "sha512-bJlen0FcuU/0EMLrdbJ7zOnW6ITZLrZMIarMUVmdKtsGvZna8vxKYaexICWPfZ8qwf9fzNq+UEIZrnSaApt6RA==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/jju": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/jju/-/jju-1.4.0.tgz", + "integrity": "sha1-o6vicYryQaKykE+EpiWXDzia4yo=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/join-path": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/join-path/-/join-path-1.1.1.tgz", + "integrity": "sha1-EFNaEm0ky9Zff/zfFe8uYxB2tQU=", + "devOptional": true, + "dependencies": { + "as-array": "^2.0.0", + "url-join": "0.0.1", + "valid-url": "^1" + } + }, + "node_modules/firebase-tools/node_modules/js-yaml": { + "version": "3.13.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz", + "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==", + "devOptional": true, + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/firebase-tools/node_modules/js2xmlparser": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/js2xmlparser/-/js2xmlparser-4.0.2.tgz", + "integrity": "sha512-6n4D8gLlLf1n5mNLQPRfViYzu9RATblzPEtm1SthMX1Pjao0r9YI9nw7ZIfRxQMERS87mcswrg+r/OYrPRX6jA==", + "devOptional": true, + "dependencies": { + "xmlcreate": "^2.0.4" + } + }, + "node_modules/firebase-tools/node_modules/jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/jsdoc": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsdoc/-/jsdoc-4.0.0.tgz", + "integrity": "sha512-tzTgkklbWKrlaQL2+e3NNgLcZu3NaK2vsHRx7tyHQ+H5jcB9Gx0txSd2eJWlMC/xU1+7LQu4s58Ry0RkuaEQVg==", + "devOptional": true, + "dependencies": { + "@babel/parser": "^7.9.4", + "@jsdoc/salty": "^0.2.1", + "@types/markdown-it": "^12.2.3", + "bluebird": "^3.7.2", + "catharsis": "^0.9.0", + "escape-string-regexp": "^2.0.0", + "js2xmlparser": "^4.0.2", + "klaw": "^3.0.0", + "markdown-it": "^12.3.2", + "markdown-it-anchor": "^8.4.1", + "marked": "^4.0.10", + "mkdirp": "^1.0.4", + "requizzle": "^0.2.3", + "strip-json-comments": "^3.1.0", + "underscore": "~1.13.2" + }, + "bin": { + "jsdoc": "jsdoc.js" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/firebase-tools/node_modules/jsdoc/node_modules/bluebird": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/jsdoc/node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "devOptional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/jsdoc/node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "devOptional": true, + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/firebase-tools/node_modules/jsdoc/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==", + "devOptional": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/firebase-tools/node_modules/json-bigint": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", + "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", + "devOptional": true, + "dependencies": { + "bignumber.js": "^9.0.0" + } + }, + "node_modules/firebase-tools/node_modules/json-parse-helpfulerror": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/json-parse-helpfulerror/-/json-parse-helpfulerror-1.0.3.tgz", + "integrity": "sha1-E/FM4C7tTpgSl7ZOueO5MuLdE9w=", + "devOptional": true, + "dependencies": { + "jju": "^1.1.0" + } + }, + "node_modules/firebase-tools/node_modules/json-ptr": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-ptr/-/json-ptr-3.0.1.tgz", + "integrity": "sha512-hrZ4tElT8huJUH3OwOK+d7F8PRqw09QnGM3Mm3GmqKWDyCCPCG8lGHxXOwQAj0VOxzLirOds07Kz10B5F8M8EA==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/json-schema": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", + "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==", + "devOptional": true + }, + "node_modules/firebase-tools/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==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", + "devOptional": true, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/firebase-tools/node_modules/jsonwebtoken": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.0.tgz", + "integrity": "sha512-tuGfYXxkQGDPnLJ7SibiQgVgeDgfbPq2k2ICcbgqW8WxWLBAxKQM/ZCu/IT8SOSwmaYl4dpTFCW5xZv7YbbWUw==", + "devOptional": true, + "dependencies": { + "jws": "^3.2.2", + "lodash": "^4.17.21", + "ms": "^2.1.1", + "semver": "^7.3.8" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/firebase-tools/node_modules/jsonwebtoken/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==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/jsonwebtoken/node_modules/semver": { + "version": "7.3.8", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", + "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", + "devOptional": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/firebase-tools/node_modules/jsprim": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.2.tgz", + "integrity": "sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw==", + "devOptional": true, + "dependencies": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.4.0", + "verror": "1.10.0" + }, + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/firebase-tools/node_modules/jwa": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", + "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", + "devOptional": true, + "dependencies": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/firebase-tools/node_modules/jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "devOptional": true, + "dependencies": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/firebase-tools/node_modules/klaw": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/klaw/-/klaw-3.0.0.tgz", + "integrity": "sha512-0Fo5oir+O9jnXu5EefYbVK+mHMBeEVEy2cmctR1O1NECcCkPRreJKrS6Qt/j3KC2C148Dfo9i3pCmCMsdqGr0g==", + "devOptional": true, + "dependencies": { + "graceful-fs": "^4.1.9" + } + }, + "node_modules/firebase-tools/node_modules/kuler": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/kuler/-/kuler-1.0.1.tgz", + "integrity": "sha512-J9nVUucG1p/skKul6DU3PUZrhs0LPulNaeUOox0IyXDi8S4CztTHs1gQphhuZmzXG7VOQSf6NJfKuzteQLv9gQ==", + "devOptional": true, + "dependencies": { + "colornames": "^1.1.1" + } + }, + "node_modules/firebase-tools/node_modules/lazystream": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz", + "integrity": "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==", + "devOptional": true, + "dependencies": { + "readable-stream": "^2.0.5" + }, + "engines": { + "node": ">= 0.6.3" + } + }, + "node_modules/firebase-tools/node_modules/lazystream/node_modules/readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "devOptional": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/firebase-tools/node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "devOptional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/firebase-tools/node_modules/levn": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", + "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", + "devOptional": true, + "dependencies": { + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/firebase-tools/node_modules/libsodium": { + "version": "0.7.10", + "resolved": "https://registry.npmjs.org/libsodium/-/libsodium-0.7.10.tgz", + "integrity": "sha512-eY+z7hDrDKxkAK+QKZVNv92A5KYkxfvIshtBJkmg5TSiCnYqZP3i9OO9whE79Pwgm4jGaoHgkM4ao/b9Cyu4zQ==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/libsodium-wrappers": { + "version": "0.7.10", + "resolved": "https://registry.npmjs.org/libsodium-wrappers/-/libsodium-wrappers-0.7.10.tgz", + "integrity": "sha512-pO3F1Q9NPLB/MWIhehim42b/Fwb30JNScCNh8TcQ/kIc+qGLQch8ag8wb0keK3EP5kbGakk1H8Wwo7v+36rNQg==", + "devOptional": true, + "dependencies": { + "libsodium": "^0.7.0" + } + }, + "node_modules/firebase-tools/node_modules/linkify-it": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-3.0.3.tgz", + "integrity": "sha512-ynTsyrFSdE5oZ/O9GEf00kPngmOfVwazR5GKDq6EYfhlpFug3J2zybX56a2PRRpc9P+FuSoGNAwjlbDs9jJBPQ==", + "devOptional": true, + "dependencies": { + "uc.micro": "^1.0.1" + } + }, + "node_modules/firebase-tools/node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/lodash._objecttypes": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/lodash._objecttypes/-/lodash._objecttypes-2.4.1.tgz", + "integrity": "sha1-fAt/admKH3ZSn4kLDNsbTf7BHBE=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha1-soqmKIorn8ZRA1x3EfZathkDMaY=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/lodash.defaults": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", + "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/lodash.difference": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.difference/-/lodash.difference-4.5.0.tgz", + "integrity": "sha512-dS2j+W26TQ7taQBGN8Lbbq04ssV3emRw4NY58WErlTO29pIqS0HmoT5aJ9+TUQ1N3G+JOZSji4eugsWwGp9yPA==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/lodash.flatten": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz", + "integrity": "sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/lodash.isobject": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/lodash.isobject/-/lodash.isobject-2.4.1.tgz", + "integrity": "sha1-Wi5H/mmVPx7mMafrof5k0tBlWPU=", + "devOptional": true, + "dependencies": { + "lodash._objecttypes": "~2.4.1" + } + }, + "node_modules/firebase-tools/node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/lodash.snakecase": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.snakecase/-/lodash.snakecase-4.1.1.tgz", + "integrity": "sha1-OdcUo1NXFHg3rv1ktdy7Fr7Nj40=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/lodash.union": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.union/-/lodash.union-4.6.0.tgz", + "integrity": "sha512-c4pB2CdGrGdjMKYLA+XiRDO7Y0PRQbm/Gzg8qMj+QH+pFVAoTp5sBpO0odL3FjoPCGjK96p6qsP+yQoiLoOBcw==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "devOptional": true, + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/firebase-tools/node_modules/log-symbols/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "devOptional": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/firebase-tools/node_modules/log-symbols/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "devOptional": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/firebase-tools/node_modules/log-symbols/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "devOptional": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/firebase-tools/node_modules/log-symbols/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/log-symbols/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "devOptional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/log-symbols/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "devOptional": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/logform": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/logform/-/logform-2.1.2.tgz", + "integrity": "sha512-+lZh4OpERDBLqjiwDLpAWNQu6KMjnlXH2ByZwCuSqVPJletw0kTWJf5CgSNAUKn1KUkv3m2cUz/LK8zyEy7wzQ==", + "devOptional": true, + "dependencies": { + "colors": "^1.2.1", + "fast-safe-stringify": "^2.0.4", + "fecha": "^2.3.3", + "ms": "^2.1.1", + "triple-beam": "^1.3.0" + } + }, + "node_modules/firebase-tools/node_modules/logform/node_modules/colors": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz", + "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==", + "devOptional": true, + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/firebase-tools/node_modules/logform/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/long": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", + "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "devOptional": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/firebase-tools/node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "devOptional": true, + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/firebase-tools/node_modules/make-dir/node_modules/semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "devOptional": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/firebase-tools/node_modules/make-fetch-happen": { + "version": "10.2.1", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-10.2.1.tgz", + "integrity": "sha512-NgOPbRiaQM10DYXvN3/hhGVI2M5MtITFryzBGxHM5p4wnFxsVCbxkrBrDsk+EZ5OB4jEOT7AjDxtdF+KVEFT7w==", + "optional": true, + "dependencies": { + "agentkeepalive": "^4.2.1", + "cacache": "^16.1.0", + "http-cache-semantics": "^4.1.0", + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.0", + "is-lambda": "^1.0.1", + "lru-cache": "^7.7.1", + "minipass": "^3.1.6", + "minipass-collect": "^1.0.2", + "minipass-fetch": "^2.0.3", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^0.6.3", + "promise-retry": "^2.0.1", + "socks-proxy-agent": "^7.0.0", + "ssri": "^9.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/firebase-tools/node_modules/make-fetch-happen/node_modules/@tootallnate/once": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", + "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", + "optional": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/firebase-tools/node_modules/make-fetch-happen/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "optional": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/firebase-tools/node_modules/make-fetch-happen/node_modules/http-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", + "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", + "optional": true, + "dependencies": { + "@tootallnate/once": "2", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/firebase-tools/node_modules/make-fetch-happen/node_modules/lru-cache": { + "version": "7.14.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.14.0.tgz", + "integrity": "sha512-EIRtP1GrSJny0dqb50QXRUNBxHJhcpxHC++M5tD7RYbvLLn5KVWKsbyswSSqDuU15UFi3bgTQIY8nhDMeF6aDQ==", + "optional": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/firebase-tools/node_modules/make-fetch-happen/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "optional": true + }, + "node_modules/firebase-tools/node_modules/make-fetch-happen/node_modules/socks-proxy-agent": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-7.0.0.tgz", + "integrity": "sha512-Fgl0YPZ902wEsAyiQ+idGd1A7rSFx/ayC1CQVMw5P+EQx2V0SgpGtf6OKFhVjPflPUl9YMmEOnmfjCdMUsygww==", + "optional": true, + "dependencies": { + "agent-base": "^6.0.2", + "debug": "^4.3.3", + "socks": "^2.6.2" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/firebase-tools/node_modules/markdown-it": { + "version": "12.3.2", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-12.3.2.tgz", + "integrity": "sha512-TchMembfxfNVpHkbtriWltGWc+m3xszaRD0CZup7GFFhzIgQqxIfn3eGj1yZpfuflzPvfkt611B2Q/Bsk1YnGg==", + "devOptional": true, + "dependencies": { + "argparse": "^2.0.1", + "entities": "~2.1.0", + "linkify-it": "^3.0.1", + "mdurl": "^1.0.1", + "uc.micro": "^1.0.5" + }, + "bin": { + "markdown-it": "bin/markdown-it.js" + } + }, + "node_modules/firebase-tools/node_modules/markdown-it-anchor": { + "version": "8.6.6", + "resolved": "https://registry.npmjs.org/markdown-it-anchor/-/markdown-it-anchor-8.6.6.tgz", + "integrity": "sha512-jRW30YGywD2ESXDc+l17AiritL0uVaSnWsb26f+68qaW9zgbIIr1f4v2Nsvc0+s0Z2N3uX6t/yAw7BwCQ1wMsA==", + "devOptional": true, + "peerDependencies": { + "@types/markdown-it": "*", + "markdown-it": "*" + } + }, + "node_modules/firebase-tools/node_modules/markdown-it/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==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/marked": { + "version": "4.2.12", + "resolved": "https://registry.npmjs.org/marked/-/marked-4.2.12.tgz", + "integrity": "sha512-yr8hSKa3Fv4D3jdZmtMMPghgVt6TWbk86WQaWhDloQjRSQhMMYCAro7jP7VDJrjjdV8pxVxMssXS8B8Y5DZ5aw==", + "devOptional": true, + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 12" + } + }, + "node_modules/firebase-tools/node_modules/marked-terminal": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/marked-terminal/-/marked-terminal-5.1.1.tgz", + "integrity": "sha512-+cKTOx9P4l7HwINYhzbrBSyzgxO2HaHKGZGuB1orZsMIgXYaJyfidT81VXRdpelW/PcHEWxywscePVgI/oUF6g==", + "devOptional": true, + "dependencies": { + "ansi-escapes": "^5.0.0", + "cardinal": "^2.1.1", + "chalk": "^5.0.0", + "cli-table3": "^0.6.1", + "node-emoji": "^1.11.0", + "supports-hyperlinks": "^2.2.0" + }, + "engines": { + "node": ">=14.13.1 || >=16.0.0" + }, + "peerDependencies": { + "marked": "^1.0.0 || ^2.0.0 || ^3.0.0 || ^4.0.0" + } + }, + "node_modules/firebase-tools/node_modules/marked-terminal/node_modules/chalk": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.0.1.tgz", + "integrity": "sha512-Fo07WOYGqMfCWHOzSXOt2CxDbC6skS/jO9ynEcmpANMoPrD+W1r1K6Vx7iNm+AQmETU1Xr2t+n8nzkV9t6xh3w==", + "devOptional": true, + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/firebase-tools/node_modules/mdurl": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz", + "integrity": "sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=", + "devOptional": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/firebase-tools/node_modules/merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=", + "devOptional": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/firebase-tools/node_modules/mime": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.5.2.tgz", + "integrity": "sha512-tqkh47FzKeCPD2PUiPB6pkbMzsCasjxAfC62/Wap5qrUWcb+sFasXUC5I3gYM5iBM8v/Qpn4UK0x+j0iHyFPDg==", + "devOptional": true, + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/firebase-tools/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "devOptional": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/firebase-tools/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "devOptional": true, + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/firebase-tools/node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "devOptional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/firebase-tools/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "devOptional": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/firebase-tools/node_modules/minimist": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", + "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/minipass": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.1.6.tgz", + "integrity": "sha512-rty5kpw9/z8SX9dmxblFA6edItUmwJgMeYDZRrwlIVN27i8gysGbznJwUggw2V/FVqFSDdWy040ZPS811DYAqQ==", + "devOptional": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/minipass-collect": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-1.0.2.tgz", + "integrity": "sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==", + "optional": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/firebase-tools/node_modules/minipass-fetch": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-2.1.2.tgz", + "integrity": "sha512-LT49Zi2/WMROHYoqGgdlQIZh8mLPZmOrN2NdJjMXxYe4nkN6FUyuPuOAOedNJDrx0IRGg9+4guZewtp8hE6TxA==", + "optional": true, + "dependencies": { + "minipass": "^3.1.6", + "minipass-sized": "^1.0.3", + "minizlib": "^2.1.2" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + }, + "optionalDependencies": { + "encoding": "^0.1.13" + } + }, + "node_modules/firebase-tools/node_modules/minipass-flush": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz", + "integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==", + "optional": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/firebase-tools/node_modules/minipass-pipeline": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", + "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", + "optional": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/minipass-sized": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz", + "integrity": "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==", + "optional": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "devOptional": true, + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/firebase-tools/node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "devOptional": true, + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/firebase-tools/node_modules/morgan": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.0.tgz", + "integrity": "sha512-AbegBVI4sh6El+1gNwvD5YIck7nSA36weD7xvIxG4in80j/UoK8AEGaWnnz8v1GxonMCltmlNs5ZKbGvl9b1XQ==", + "devOptional": true, + "dependencies": { + "basic-auth": "~2.0.1", + "debug": "2.6.9", + "depd": "~2.0.0", + "on-finished": "~2.3.0", + "on-headers": "~1.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/firebase-tools/node_modules/morgan/node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "devOptional": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/firebase-tools/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/mute-stream": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", + "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/nan": { + "version": "2.16.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.16.0.tgz", + "integrity": "sha512-UdAqHyFngu7TfQKsCBgAA6pWDkT8MAO7d0jyOecVhN5354xbLqdn8mV9Tat9gepAupm0bt2DbeaSC8vS52MuFA==", + "optional": true + }, + "node_modules/firebase-tools/node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "devOptional": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/firebase-tools/node_modules/netmask": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.0.2.tgz", + "integrity": "sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==", + "devOptional": true, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/firebase-tools/node_modules/nice-try": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", + "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/node-emoji": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-1.11.0.tgz", + "integrity": "sha512-wo2DpQkQp7Sjm2A0cq+sN7EHKO6Sl0ctXeBdFZrL9T9+UywORbufTcTZxom8YqpLQt/FqNMUkOpkZrJVYSKD3A==", + "devOptional": true, + "dependencies": { + "lodash": "^4.17.21" + } + }, + "node_modules/firebase-tools/node_modules/node-fetch": { + "version": "2.6.7", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", + "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", + "devOptional": true, + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/firebase-tools/node_modules/node-forge": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", + "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==", + "devOptional": true, + "engines": { + "node": ">= 6.13.0" + } + }, + "node_modules/firebase-tools/node_modules/node-gyp": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-9.1.0.tgz", + "integrity": "sha512-HkmN0ZpQJU7FLbJauJTHkHlSVAXlNGDAzH/VYFZGDOnFyn/Na3GlNJfkudmufOdS6/jNFhy88ObzL7ERz9es1g==", + "optional": true, + "dependencies": { + "env-paths": "^2.2.0", + "glob": "^7.1.4", + "graceful-fs": "^4.2.6", + "make-fetch-happen": "^10.0.3", + "nopt": "^5.0.0", + "npmlog": "^6.0.0", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "tar": "^6.1.2", + "which": "^2.0.2" + }, + "bin": { + "node-gyp": "bin/node-gyp.js" + }, + "engines": { + "node": "^12.22 || ^14.13 || >=16" + } + }, + "node_modules/firebase-tools/node_modules/node-gyp/node_modules/semver": { + "version": "7.3.7", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz", + "integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==", + "optional": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/firebase-tools/node_modules/node-gyp/node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "optional": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/firebase-tools/node_modules/nopt": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", + "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", + "optional": true, + "dependencies": { + "abbrev": "1" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/firebase-tools/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==", + "devOptional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/firebase-tools/node_modules/npmlog": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-6.0.2.tgz", + "integrity": "sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg==", + "optional": true, + "dependencies": { + "are-we-there-yet": "^3.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^4.0.3", + "set-blocking": "^2.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/firebase-tools/node_modules/oauth-sign": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", + "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", + "devOptional": true, + "engines": { + "node": "*" + } + }, + "node_modules/firebase-tools/node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", + "devOptional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/firebase-tools/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==", + "devOptional": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/firebase-tools/node_modules/object-inspect": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.2.tgz", + "integrity": "sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==", + "devOptional": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/firebase-tools/node_modules/on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", + "devOptional": true, + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/firebase-tools/node_modules/on-headers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", + "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", + "devOptional": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/firebase-tools/node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "devOptional": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/firebase-tools/node_modules/one-time": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/one-time/-/one-time-0.0.4.tgz", + "integrity": "sha1-+M33eISCb+Tf+T46nMN7HkSAdC4=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "devOptional": true, + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/firebase-tools/node_modules/open": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/open/-/open-6.4.0.tgz", + "integrity": "sha512-IFenVPgF70fSm1keSd2iDBIDIBZkroLeuffXq+wKTzTJlBpesFWojV9lb8mzOfaAzM1sr7HQHuO0vtV0zYekGg==", + "devOptional": true, + "dependencies": { + "is-wsl": "^1.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/openapi3-ts": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/openapi3-ts/-/openapi3-ts-2.0.1.tgz", + "integrity": "sha512-v6X3iwddhi276siej96jHGIqTx3wzVfMTmpGJEQDt7GPI7pI6sywItURLzpEci21SBRpPN/aOWSF5mVfFVNmcg==", + "devOptional": true, + "dependencies": { + "yaml": "^1.10.0" + } + }, + "node_modules/firebase-tools/node_modules/optionator": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", + "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", + "devOptional": true, + "dependencies": { + "deep-is": "~0.1.3", + "fast-levenshtein": "~2.0.6", + "levn": "~0.3.0", + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2", + "word-wrap": "~1.2.3" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/firebase-tools/node_modules/ora": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", + "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", + "devOptional": true, + "dependencies": { + "bl": "^4.1.0", + "chalk": "^4.1.0", + "cli-cursor": "^3.1.0", + "cli-spinners": "^2.5.0", + "is-interactive": "^1.0.0", + "is-unicode-supported": "^0.1.0", + "log-symbols": "^4.1.0", + "strip-ansi": "^6.0.0", + "wcwidth": "^1.0.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/firebase-tools/node_modules/ora/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "devOptional": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/firebase-tools/node_modules/ora/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "devOptional": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/firebase-tools/node_modules/ora/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "devOptional": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/firebase-tools/node_modules/ora/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/ora/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "devOptional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/ora/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "devOptional": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", + "devOptional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/firebase-tools/node_modules/p-defer": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-defer/-/p-defer-3.0.0.tgz", + "integrity": "sha512-ugZxsxmtTln604yeYd29EGrNhazN2lywetzpKhfmQjW/VJmhpDmWbiX+h0zL8V91R0UXkhb3KtPmyq9PZw3aYw==", + "devOptional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/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==", + "devOptional": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/firebase-tools/node_modules/pac-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-5.0.0.tgz", + "integrity": "sha512-CcFG3ZtnxO8McDigozwE3AqAw15zDvGH+OjXO4kzf7IkEKkQ4gxQ+3sdF50WmhQ4P/bVusXcqNE2S3XrNURwzQ==", + "devOptional": true, + "dependencies": { + "@tootallnate/once": "1", + "agent-base": "6", + "debug": "4", + "get-uri": "3", + "http-proxy-agent": "^4.0.1", + "https-proxy-agent": "5", + "pac-resolver": "^5.0.0", + "raw-body": "^2.2.0", + "socks-proxy-agent": "5" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/firebase-tools/node_modules/pac-proxy-agent/node_modules/debug": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", + "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", + "devOptional": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/firebase-tools/node_modules/pac-proxy-agent/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/pac-resolver": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-5.0.0.tgz", + "integrity": "sha512-H+/A6KitiHNNW+bxBKREk2MCGSxljfqRX76NjummWEYIat7ldVXRU3dhRIE3iXZ0nvGBk6smv3nntxKkzRL8NA==", + "devOptional": true, + "dependencies": { + "degenerator": "^3.0.1", + "ip": "^1.1.5", + "netmask": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/firebase-tools/node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "devOptional": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/firebase-tools/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": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "devOptional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/firebase-tools/node_modules/path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=", + "devOptional": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/firebase-tools/node_modules/path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "devOptional": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/firebase-tools/node_modules/portfinder": { + "version": "1.0.32", + "resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.32.tgz", + "integrity": "sha512-on2ZJVVDXRADWE6jnQaX0ioEylzgBpQk8r55NE4wjXW1ZxO+BgDlY6DXwj20i0V8eB4SenDQ00WEaxfiIQPcxg==", + "devOptional": true, + "dependencies": { + "async": "^2.6.4", + "debug": "^3.2.7", + "mkdirp": "^0.5.6" + }, + "engines": { + "node": ">= 0.12.0" + } + }, + "node_modules/firebase-tools/node_modules/portfinder/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "devOptional": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/firebase-tools/node_modules/portfinder/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==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/prelude-ls": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", + "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=", + "devOptional": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/firebase-tools/node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "devOptional": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/firebase-tools/node_modules/promise-breaker": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/promise-breaker/-/promise-breaker-5.0.0.tgz", + "integrity": "sha512-mgsWQuG4kJ1dtO6e/QlNDLFtMkMzzecsC69aI5hlLEjGHFNpHrvGhFi4LiK5jg2SMQj74/diH+wZliL9LpGsyA==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/promise-inflight": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", + "integrity": "sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==", + "optional": true + }, + "node_modules/firebase-tools/node_modules/promise-retry": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", + "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", + "optional": true, + "dependencies": { + "err-code": "^2.0.2", + "retry": "^0.12.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/firebase-tools/node_modules/promise-retry/node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "optional": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/firebase-tools/node_modules/proto-list": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", + "integrity": "sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/proto3-json-serializer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proto3-json-serializer/-/proto3-json-serializer-1.1.0.tgz", + "integrity": "sha512-SjXwUWe/vANGs/mJJTbw5++7U67nwsymg7qsoPtw6GiXqw3kUy8ByojrlEdVE2efxAdKreX8WkDafxvYW95ZQg==", + "devOptional": true, + "dependencies": { + "protobufjs": "^7.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/firebase-tools/node_modules/proto3-json-serializer/node_modules/long": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/long/-/long-5.2.1.tgz", + "integrity": "sha512-GKSNGeNAtw8IryjjkhZxuKB3JzlcLTwjtiQCHKvqQet81I93kXslhDQruGI/QsddO83mcDToBVy7GqGS/zYf/A==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/proto3-json-serializer/node_modules/protobufjs": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.1.2.tgz", + "integrity": "sha512-4ZPTPkXCdel3+L81yw3dG6+Kq3umdWKh7Dc7GW/CpNk4SX3hK58iPCWeCyhVTDrbkNeKrYNZ7EojM5WDaEWTLQ==", + "devOptional": true, + "hasInstallScript": true, + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/node": ">=13.7.0", + "long": "^5.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/firebase-tools/node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "devOptional": true, + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/firebase-tools/node_modules/proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-5.0.0.tgz", + "integrity": "sha512-gkH7BkvLVkSfX9Dk27W6TyNOWWZWRilRfk1XxGNWOYJ2TuedAv1yFpCaU9QSBmBe716XOTNpYNOzhysyw8xn7g==", + "devOptional": true, + "dependencies": { + "agent-base": "^6.0.0", + "debug": "4", + "http-proxy-agent": "^4.0.0", + "https-proxy-agent": "^5.0.0", + "lru-cache": "^5.1.1", + "pac-proxy-agent": "^5.0.0", + "proxy-from-env": "^1.0.0", + "socks-proxy-agent": "^5.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/firebase-tools/node_modules/proxy-agent/node_modules/debug": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", + "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", + "devOptional": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/firebase-tools/node_modules/proxy-agent/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==", + "devOptional": true, + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/firebase-tools/node_modules/proxy-agent/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/proxy-agent/node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/psl": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz", + "integrity": "sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "devOptional": true, + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/firebase-tools/node_modules/punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", + "devOptional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/firebase-tools/node_modules/pupa": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/pupa/-/pupa-2.1.1.tgz", + "integrity": "sha512-l1jNAspIBSFqbT+y+5FosojNpVpF94nlI+wDUpqP9enwOTfHx9f0gh5nB96vl+6yTpsJsypeNrwfzPrKuHB41A==", + "devOptional": true, + "dependencies": { + "escape-goat": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/qs": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", + "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "devOptional": true, + "dependencies": { + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/firebase-tools/node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "devOptional": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/firebase-tools/node_modules/raw-body": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", + "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", + "devOptional": true, + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/firebase-tools/node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "devOptional": true, + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/firebase-tools/node_modules/re2": { + "version": "1.17.7", + "resolved": "https://registry.npmjs.org/re2/-/re2-1.17.7.tgz", + "integrity": "sha512-X8GSuiBoVWwcjuppqSjsIkRxNUKDdjhkO9SBekQbZ2ksqWUReCy7DQPWOVpoTnpdtdz5PIpTTxTFzvJv5UMfjA==", + "hasInstallScript": true, + "optional": true, + "dependencies": { + "install-artifact-from-github": "^1.3.1", + "nan": "^2.16.0", + "node-gyp": "^9.0.0" + } + }, + "node_modules/firebase-tools/node_modules/readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "devOptional": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/firebase-tools/node_modules/readdir-glob": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/readdir-glob/-/readdir-glob-1.1.1.tgz", + "integrity": "sha512-91/k1EzZwDx6HbERR+zucygRFfiPl2zkIYZtv3Jjr6Mn7SkKcVct8aVO+sSRiGMc6fLf72du3d92/uY63YPdEA==", + "devOptional": true, + "dependencies": { + "minimatch": "^3.0.4" + } + }, + "node_modules/firebase-tools/node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "devOptional": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/firebase-tools/node_modules/redeyed": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/redeyed/-/redeyed-2.1.1.tgz", + "integrity": "sha1-iYS1gV2ZyyIEacme7v/jiRPmzAs=", + "devOptional": true, + "dependencies": { + "esprima": "~4.0.0" + } + }, + "node_modules/firebase-tools/node_modules/registry-url": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/registry-url/-/registry-url-5.1.0.tgz", + "integrity": "sha512-8acYXXTI0AkQv6RAOjE3vOaIXZkT9wo4LOFbBKYQEEnnMNBpKqdUrI6S4NT0KPIo/WVvJ5tE/X5LF/TQUf0ekw==", + "devOptional": true, + "dependencies": { + "rc": "^1.2.8" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/request": { + "version": "2.88.2", + "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", + "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==", + "deprecated": "request has been deprecated, see https://github.com/request/request/issues/3142", + "devOptional": true, + "dependencies": { + "aws-sign2": "~0.7.0", + "aws4": "^1.8.0", + "caseless": "~0.12.0", + "combined-stream": "~1.0.6", + "extend": "~3.0.2", + "forever-agent": "~0.6.1", + "form-data": "~2.3.2", + "har-validator": "~5.1.3", + "http-signature": "~1.2.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.19", + "oauth-sign": "~0.9.0", + "performance-now": "^2.1.0", + "qs": "~6.5.2", + "safe-buffer": "^5.1.2", + "tough-cookie": "~2.5.0", + "tunnel-agent": "^0.6.0", + "uuid": "^3.3.2" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/firebase-tools/node_modules/request/node_modules/form-data": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", + "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", + "devOptional": true, + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 0.12" + } + }, + "node_modules/firebase-tools/node_modules/request/node_modules/qs": { + "version": "6.5.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.3.tgz", + "integrity": "sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==", + "devOptional": true, + "engines": { + "node": ">=0.6" + } + }, + "node_modules/firebase-tools/node_modules/request/node_modules/uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", + "deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.", + "devOptional": true, + "bin": { + "uuid": "bin/uuid" + } + }, + "node_modules/firebase-tools/node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", + "devOptional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/firebase-tools/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==", + "devOptional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/firebase-tools/node_modules/requizzle": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/requizzle/-/requizzle-0.2.4.tgz", + "integrity": "sha512-JRrFk1D4OQ4SqovXOgdav+K8EAhSB/LJZqCz8tbX0KObcdeM15Ss59ozWMBWmmINMagCwmqn4ZNryUGpBsl6Jw==", + "devOptional": true, + "dependencies": { + "lodash": "^4.17.21" + } + }, + "node_modules/firebase-tools/node_modules/restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "devOptional": true, + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/retry": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "devOptional": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/firebase-tools/node_modules/retry-request": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/retry-request/-/retry-request-5.0.2.tgz", + "integrity": "sha512-wfI3pk7EE80lCIXprqh7ym48IHYdwmAAzESdbU8Q9l7pnRCk9LEhpbOTNKjz6FARLm/Bl5m+4F0ABxOkYUujSQ==", + "devOptional": true, + "dependencies": { + "debug": "^4.1.1", + "extend": "^3.0.2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/firebase-tools/node_modules/retry-request/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "devOptional": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/firebase-tools/node_modules/retry-request/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "devOptional": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/firebase-tools/node_modules/router": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/router/-/router-1.3.5.tgz", + "integrity": "sha512-kozCJZUhuSJ5VcLhSb3F8fsmGXy+8HaDbKCAerR1G6tq3mnMZFMuSohbFvGv1c5oMFipijDjRZuuN/Sq5nMf3g==", + "devOptional": true, + "dependencies": { + "array-flatten": "3.0.0", + "debug": "2.6.9", + "methods": "~1.1.2", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.7", + "setprototypeof": "1.2.0", + "utils-merge": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/firebase-tools/node_modules/router/node_modules/array-flatten": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-3.0.0.tgz", + "integrity": "sha512-zPMVc3ZYlGLNk4mpK1NzP2wg0ml9t7fUgDsayR5Y5rSzxQilzR9FGu/EH2jQOcKSAeAfWeylyW8juy3OkWRvNA==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/run-async": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", + "integrity": "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==", + "devOptional": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/firebase-tools/node_modules/rxjs": { + "version": "7.5.6", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.5.6.tgz", + "integrity": "sha512-dnyv2/YsXhnm461G+R/Pe5bWP41Nm6LBXEYWI6eiFP4fiwx6WRI/CD0zbdVAudd9xwLEF2IDcKXLHit0FYjUzw==", + "devOptional": true, + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/firebase-tools/node_modules/rxjs/node_modules/tslib": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz", + "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "devOptional": true, + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/firebase-tools/node_modules/semver-diff": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/semver-diff/-/semver-diff-3.1.1.tgz", + "integrity": "sha512-GX0Ix/CJcHyB8c4ykpHGIAvLyOwOobtM/8d+TQkAd81/bEjgPHrfba41Vpesr7jX/t8Uh+R3EX9eAS5be+jQYg==", + "devOptional": true, + "dependencies": { + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/semver-diff/node_modules/semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "devOptional": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/firebase-tools/node_modules/send": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", + "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "devOptional": true, + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/firebase-tools/node_modules/send/node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "devOptional": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/firebase-tools/node_modules/send/node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "devOptional": true, + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/firebase-tools/node_modules/send/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==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/send/node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "devOptional": true, + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/firebase-tools/node_modules/send/node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "devOptional": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/firebase-tools/node_modules/serve-static": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", + "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", + "devOptional": true, + "dependencies": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.18.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/firebase-tools/node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", + "optional": true + }, + "node_modules/firebase-tools/node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", + "devOptional": true, + "dependencies": { + "shebang-regex": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/firebase-tools/node_modules/shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", + "devOptional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/firebase-tools/node_modules/side-channel": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", + "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "devOptional": true, + "dependencies": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/firebase-tools/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/simple-swizzle": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", + "integrity": "sha1-pNprY1/8zMoz9w0Xy5JZLeleVXo=", + "devOptional": true, + "dependencies": { + "is-arrayish": "^0.3.1" + } + }, + "node_modules/firebase-tools/node_modules/simple-swizzle/node_modules/is-arrayish": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", + "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "devOptional": true, + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/firebase-tools/node_modules/socks": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.7.0.tgz", + "integrity": "sha512-scnOe9y4VuiNUULJN72GrM26BNOjVsfPXI+j+98PkyEfsIXroa5ofyjT+FzGvn/xHs73U2JtoBYAVx9Hl4quSA==", + "devOptional": true, + "dependencies": { + "ip": "^2.0.0", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.13.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/firebase-tools/node_modules/socks-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-5.0.1.tgz", + "integrity": "sha512-vZdmnjb9a2Tz6WEQVIurybSwElwPxMZaIc7PzqbJTrezcKNznv6giT7J7tZDZ1BojVaa1jvO/UiUdhDVB0ACoQ==", + "devOptional": true, + "dependencies": { + "agent-base": "^6.0.2", + "debug": "4", + "socks": "^2.3.3" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/firebase-tools/node_modules/socks-proxy-agent/node_modules/debug": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", + "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", + "devOptional": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/firebase-tools/node_modules/socks-proxy-agent/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/socks/node_modules/ip": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ip/-/ip-2.0.0.tgz", + "integrity": "sha512-WKa+XuLG1A1R0UWhl2+1XQSi+fZWMsYKffMZTTYsiZaUD8k2yDAj5atimTUD2TZkyCkNEeYE5NhFZmupOGtjYQ==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/firebase-tools/node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/sshpk": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz", + "integrity": "sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==", + "devOptional": true, + "dependencies": { + "asn1": "~0.2.3", + "assert-plus": "^1.0.0", + "bcrypt-pbkdf": "^1.0.0", + "dashdash": "^1.12.0", + "ecc-jsbn": "~0.1.1", + "getpass": "^0.1.1", + "jsbn": "~0.1.0", + "safer-buffer": "^2.0.2", + "tweetnacl": "~0.14.0" + }, + "bin": { + "sshpk-conv": "bin/sshpk-conv", + "sshpk-sign": "bin/sshpk-sign", + "sshpk-verify": "bin/sshpk-verify" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/firebase-tools/node_modules/ssri": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-9.0.1.tgz", + "integrity": "sha512-o57Wcn66jMQvfHG1FlYbWeZWW/dHZhJXjpIcTfXldXEk5nz5lStPo3mK0OJQfGR3RbZUlbISexbljkJzuEj/8Q==", + "optional": true, + "dependencies": { + "minipass": "^3.1.1" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/firebase-tools/node_modules/stack-trace": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", + "integrity": "sha1-VHxws0fo0ytOEI6hoqFZ5f3eGcA=", + "devOptional": true, + "engines": { + "node": "*" + } + }, + "node_modules/firebase-tools/node_modules/statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=", + "devOptional": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/firebase-tools/node_modules/stream-chain": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/stream-chain/-/stream-chain-2.2.4.tgz", + "integrity": "sha512-9lsl3YM53V5N/I1C2uJtc3Kavyi3kNYN83VkKb/bMWRk7D9imiFyUPYa0PoZbLohSVOX1mYE9YsmwObZUsth6Q==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/stream-json": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/stream-json/-/stream-json-1.7.3.tgz", + "integrity": "sha512-Y6dXn9KKWSwxOqnvHGcdZy1PK+J+7alBwHCeU3W9oRqm4ilLRA0XSPmd1tWwhg7tv9EIxJTMWh7KF15tYelKJg==", + "devOptional": true, + "dependencies": { + "stream-chain": "^2.2.4" + } + }, + "node_modules/firebase-tools/node_modules/stream-shift": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.1.tgz", + "integrity": "sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "devOptional": true, + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/firebase-tools/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "devOptional": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/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==", + "devOptional": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/strip-ansi/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==", + "devOptional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", + "devOptional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/firebase-tools/node_modules/superstatic": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/superstatic/-/superstatic-9.0.3.tgz", + "integrity": "sha512-e/tmW0bsnQ/33ivK6y3CapJT0Ovy4pk/ohNPGhIAGU2oasoNLRQ1cv6enua09NU9w6Y0H/fBu07cjzuiWvLXxw==", + "devOptional": true, + "dependencies": { + "basic-auth-connect": "^1.0.0", + "commander": "^10.0.0", + "compression": "^1.7.0", + "connect": "^3.7.0", + "destroy": "^1.0.4", + "fast-url-parser": "^1.1.3", + "glob-slasher": "^1.0.1", + "is-url": "^1.2.2", + "join-path": "^1.1.1", + "lodash": "^4.17.19", + "mime-types": "^2.1.35", + "minimatch": "^6.1.6", + "morgan": "^1.8.2", + "on-finished": "^2.2.0", + "on-headers": "^1.0.0", + "path-to-regexp": "^1.8.0", + "router": "^1.3.1", + "update-notifier-cjs": "^5.1.6" + }, + "bin": { + "superstatic": "lib/bin/server.js" + }, + "engines": { + "node": "^14.18.0 || >=16.4.0" + }, + "optionalDependencies": { + "re2": "^1.17.7" + } + }, + "node_modules/firebase-tools/node_modules/superstatic/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "devOptional": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/firebase-tools/node_modules/superstatic/node_modules/commander": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.0.tgz", + "integrity": "sha512-zS5PnTI22FIRM6ylNW8G4Ap0IEOyk62fhLSD0+uHRT9McRCLGpkVNvao4bjimpK/GShynyQkFFxHhwMcETmduA==", + "devOptional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/firebase-tools/node_modules/superstatic/node_modules/isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/superstatic/node_modules/minimatch": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-6.2.0.tgz", + "integrity": "sha512-sauLxniAmvnhhRjFwPNnJKaPFYyddAgbYdeUpHULtCT/GhzdCx/MDNy+Y40lBxTQUrMzDE8e0S43Z5uqfO0REg==", + "devOptional": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/firebase-tools/node_modules/superstatic/node_modules/path-to-regexp": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz", + "integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==", + "devOptional": true, + "dependencies": { + "isarray": "0.0.1" + } + }, + "node_modules/firebase-tools/node_modules/supports-hyperlinks": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-2.2.0.tgz", + "integrity": "sha512-6sXEzV5+I5j8Bmq9/vUphGRM/RJNT9SCURJLjwfOg51heRtguGWDzcaBlgAzKhQa0EVNpPEKzQuBwZ8S8WaCeQ==", + "devOptional": true, + "dependencies": { + "has-flag": "^4.0.0", + "supports-color": "^7.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/supports-hyperlinks/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==", + "devOptional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/supports-hyperlinks/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==", + "devOptional": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/tar": { + "version": "6.1.11", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.1.11.tgz", + "integrity": "sha512-an/KZQzQUkZCkuoAA64hM92X0Urb6VpRhAFllDzz44U2mcD5scmT3zBc4VgVpkugF580+DQn8eAFSyoQt0tznA==", + "devOptional": true, + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^3.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/firebase-tools/node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "devOptional": true, + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/firebase-tools/node_modules/tar/node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "devOptional": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/firebase-tools/node_modules/tar/node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "devOptional": true, + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/firebase-tools/node_modules/tcp-port-used": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/tcp-port-used/-/tcp-port-used-1.0.2.tgz", + "integrity": "sha512-l7ar8lLUD3XS1V2lfoJlCBaeoaWo/2xfYt81hM7VlvR4RrMVFqfmzfhLVk40hAb368uitje5gPtBRL1m/DGvLA==", + "devOptional": true, + "dependencies": { + "debug": "4.3.1", + "is2": "^2.0.6" + } + }, + "node_modules/firebase-tools/node_modules/tcp-port-used/node_modules/debug": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "devOptional": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/firebase-tools/node_modules/tcp-port-used/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/text-hex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", + "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/tmp": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz", + "integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==", + "devOptional": true, + "dependencies": { + "rimraf": "^3.0.0" + }, + "engines": { + "node": ">=8.17.0" + } + }, + "node_modules/firebase-tools/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==", + "devOptional": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/firebase-tools/node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "devOptional": true, + "engines": { + "node": ">=0.6" + } + }, + "node_modules/firebase-tools/node_modules/tough-cookie": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", + "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", + "devOptional": true, + "dependencies": { + "psl": "^1.1.28", + "punycode": "^2.1.1" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/firebase-tools/node_modules/toxic": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toxic/-/toxic-1.0.1.tgz", + "integrity": "sha512-WI3rIGdcaKULYg7KVoB0zcjikqvcYYvcuT6D89bFPz2rVR0Rl0PK6x8/X62rtdLtBKIE985NzVf/auTtGegIIg==", + "devOptional": true, + "dependencies": { + "lodash": "^4.17.10" + } + }, + "node_modules/firebase-tools/node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/triple-beam": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.3.0.tgz", + "integrity": "sha512-XrHUvV5HpdLmIj4uVMxHggLbFSZYIn7HEWsqePZcI50pco+MPqJ50wMGY794X7AOOhxOBAjbkqfAbEe/QMp2Lw==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", + "devOptional": true, + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/firebase-tools/node_modules/tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/type-check": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", + "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", + "devOptional": true, + "dependencies": { + "prelude-ls": "~1.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/firebase-tools/node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "devOptional": true, + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/firebase-tools/node_modules/typedarray-to-buffer": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", + "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", + "devOptional": true, + "dependencies": { + "is-typedarray": "^1.0.0" + } + }, + "node_modules/firebase-tools/node_modules/uc.micro": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz", + "integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/uglify-js": { + "version": "3.17.4", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.17.4.tgz", + "integrity": "sha512-T9q82TJI9e/C1TAxYvfb16xO120tMVFZrGA3f9/P4424DNu6ypK103y0GPFVa17yotwSyZW5iYXgjYHkGrJW/g==", + "devOptional": true, + "bin": { + "uglifyjs": "bin/uglifyjs" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/firebase-tools/node_modules/underscore": { + "version": "1.13.6", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.6.tgz", + "integrity": "sha512-+A5Sja4HP1M08MaXya7p5LvjuM7K6q/2EaC0+iovj/wOcMsTzMvDFbasi/oSapiwOlt252IqsKqPjCl7huKS0A==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/unique-filename": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-2.0.1.tgz", + "integrity": "sha512-ODWHtkkdx3IAR+veKxFV+VBkUMcN+FaqzUUd7IZzt+0zhDZFPFxhlqwPF3YQvMHx1TD0tdgYl+kuPnJ8E6ql7A==", + "optional": true, + "dependencies": { + "unique-slug": "^3.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/firebase-tools/node_modules/unique-slug": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-3.0.0.tgz", + "integrity": "sha512-8EyMynh679x/0gqE9fT9oilG+qEt+ibFyqjuVTsZn1+CMxH+XLlpvr2UZx4nVcCwTpx81nICr2JQFkM+HPLq4w==", + "optional": true, + "dependencies": { + "imurmurhash": "^0.1.4" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/firebase-tools/node_modules/universal-analytics": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/universal-analytics/-/universal-analytics-0.5.3.tgz", + "integrity": "sha512-HXSMyIcf2XTvwZ6ZZQLfxfViRm/yTGoRgDeTbojtq6rezeyKB0sTBcKH2fhddnteAHRcHiKgr/ACpbgjGOC6RQ==", + "devOptional": true, + "dependencies": { + "debug": "^4.3.1", + "uuid": "^8.0.0" + }, + "engines": { + "node": ">=12.18.2" + } + }, + "node_modules/firebase-tools/node_modules/universal-analytics/node_modules/debug": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", + "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", + "devOptional": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/firebase-tools/node_modules/universal-analytics/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "devOptional": true, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/firebase-tools/node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=", + "devOptional": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/firebase-tools/node_modules/update-notifier-cjs": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/update-notifier-cjs/-/update-notifier-cjs-5.1.6.tgz", + "integrity": "sha512-wgxdSBWv3x/YpMzsWz5G4p4ec7JWD0HCl8W6bmNB6E5Gwo+1ym5oN4hiXpLf0mPySVEJEIsYlkshnplkg2OP9A==", + "devOptional": true, + "dependencies": { + "boxen": "^5.0.0", + "chalk": "^4.1.0", + "configstore": "^5.0.1", + "has-yarn": "^2.1.0", + "import-lazy": "^2.1.0", + "is-ci": "^2.0.0", + "is-installed-globally": "^0.4.0", + "is-npm": "^5.0.0", + "is-yarn-global": "^0.3.0", + "isomorphic-fetch": "^3.0.0", + "pupa": "^2.1.1", + "registry-auth-token": "^5.0.1", + "registry-url": "^5.1.0", + "semver": "^7.3.7", + "semver-diff": "^3.1.1", + "xdg-basedir": "^4.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/firebase-tools/node_modules/update-notifier-cjs/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==", + "devOptional": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/firebase-tools/node_modules/update-notifier-cjs/node_modules/boxen": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/boxen/-/boxen-5.1.2.tgz", + "integrity": "sha512-9gYgQKXx+1nP8mP7CzFyaUARhg7D3n1dF/FnErWmu9l6JvGpNUN278h0aSb+QjoiKSWG+iZ3uHrcqk0qrY9RQQ==", + "devOptional": true, + "dependencies": { + "ansi-align": "^3.0.0", + "camelcase": "^6.2.0", + "chalk": "^4.1.0", + "cli-boxes": "^2.2.1", + "string-width": "^4.2.2", + "type-fest": "^0.20.2", + "widest-line": "^3.1.0", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/firebase-tools/node_modules/update-notifier-cjs/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "devOptional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/firebase-tools/node_modules/update-notifier-cjs/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "devOptional": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/firebase-tools/node_modules/update-notifier-cjs/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==", + "devOptional": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/firebase-tools/node_modules/update-notifier-cjs/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==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/update-notifier-cjs/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==", + "devOptional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/update-notifier-cjs/node_modules/registry-auth-token": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-5.0.1.tgz", + "integrity": "sha512-UfxVOj8seK1yaIOiieV4FIP01vfBDLsY0H9sQzi9EbbUdJiuuBjJgLa1DpImXMNPnVkBD4eVxTEXcrZA6kfpJA==", + "devOptional": true, + "dependencies": { + "@pnpm/npm-conf": "^1.0.4" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/firebase-tools/node_modules/update-notifier-cjs/node_modules/semver": { + "version": "7.3.8", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", + "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", + "devOptional": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/firebase-tools/node_modules/update-notifier-cjs/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==", + "devOptional": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/update-notifier-cjs/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==", + "devOptional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/firebase-tools/node_modules/uri-js": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", + "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", + "devOptional": true, + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/firebase-tools/node_modules/url-join": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/url-join/-/url-join-0.0.1.tgz", + "integrity": "sha1-HbSK1CLTQCRpqH99l73r/k+x48g=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=", + "devOptional": true, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/firebase-tools/node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "devOptional": true, + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/firebase-tools/node_modules/valid-url": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/valid-url/-/valid-url-1.0.9.tgz", + "integrity": "sha1-HBRHm0DxOXp1eC8RXkCGRHQzogA=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=", + "devOptional": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/firebase-tools/node_modules/verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", + "devOptional": true, + "engines": [ + "node >=0.6.0" + ], + "dependencies": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + } + }, + "node_modules/firebase-tools/node_modules/vm2": { + "version": "3.9.17", + "resolved": "https://registry.npmjs.org/vm2/-/vm2-3.9.17.tgz", + "devOptional": true, + "dependencies": { + "acorn": "^8.7.0", + "acorn-walk": "^8.2.0" + }, + "bin": { + "vm2": "bin/vm2" + }, + "engines": { + "node": ">=6.0" + } + }, + "node_modules/firebase-tools/node_modules/wcwidth": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", + "integrity": "sha1-8LDc+RW8X/FSivrbLA4XtTLaL+g=", + "devOptional": true, + "dependencies": { + "defaults": "^1.0.3" + } + }, + "node_modules/firebase-tools/node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha1-lmRU6HZUYuN2RNNib2dCzotwll0=", + "devOptional": true, + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/firebase-tools/node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "devOptional": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, + "node_modules/firebase-tools/node_modules/wide-align": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", + "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", + "optional": true, + "dependencies": { + "string-width": "^1.0.2 || 2 || 3 || 4" + } + }, + "node_modules/firebase-tools/node_modules/widest-line": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-3.1.0.tgz", + "integrity": "sha512-NsmoXalsWVDMGupxZ5R08ka9flZjjiLvHVAWYOKtiKM8ujtZWr9cRffak+uSE48+Ob8ObalXpwyeUiyDD6QFgg==", + "devOptional": true, + "dependencies": { + "string-width": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/winston": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/winston/-/winston-3.2.1.tgz", + "integrity": "sha512-zU6vgnS9dAWCEKg/QYigd6cgMVVNwyTzKs81XZtTFuRwJOcDdBg7AU0mXVyNbs7O5RH2zdv+BdNZUlx7mXPuOw==", + "devOptional": true, + "dependencies": { + "async": "^2.6.1", + "diagnostics": "^1.1.1", + "is-stream": "^1.1.0", + "logform": "^2.1.1", + "one-time": "0.0.4", + "readable-stream": "^3.1.1", + "stack-trace": "0.0.x", + "triple-beam": "^1.3.0", + "winston-transport": "^4.3.0" + }, + "engines": { + "node": ">= 6.4.0" + } + }, + "node_modules/firebase-tools/node_modules/winston-transport": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.4.0.tgz", + "integrity": "sha512-Lc7/p3GtqtqPBYYtS6KCN3c77/2QCev51DvcJKbkFPQNoj1sinkGwLGFDxkXY9J6p9+EPnYs+D90uwbnaiURTw==", + "devOptional": true, + "dependencies": { + "readable-stream": "^2.3.7", + "triple-beam": "^1.2.0" + }, + "engines": { + "node": ">= 6.4.0" + } + }, + "node_modules/firebase-tools/node_modules/winston-transport/node_modules/readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "devOptional": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/firebase-tools/node_modules/word-wrap": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", + "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", + "devOptional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/firebase-tools/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "devOptional": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/firebase-tools/node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "devOptional": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/firebase-tools/node_modules/wrap-ansi/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "devOptional": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/firebase-tools/node_modules/wrap-ansi/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/write-file-atomic": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz", + "integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==", + "devOptional": true, + "dependencies": { + "imurmurhash": "^0.1.4", + "is-typedarray": "^1.0.0", + "signal-exit": "^3.0.2", + "typedarray-to-buffer": "^3.1.5" + } + }, + "node_modules/firebase-tools/node_modules/ws": { + "version": "7.4.6", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.6.tgz", + "integrity": "sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A==", + "devOptional": true, + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/firebase-tools/node_modules/xdg-basedir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-4.0.0.tgz", + "integrity": "sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q==", + "devOptional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/xmlcreate": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/xmlcreate/-/xmlcreate-2.0.4.tgz", + "integrity": "sha512-nquOebG4sngPmGPICTS5EnxqhKbCmz5Ox5hsszI2T6U5qdrJizBc+0ilYSEjTSzU0yZcmvppztXe/5Al5fUwdg==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/xregexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/xregexp/-/xregexp-2.0.0.tgz", + "integrity": "sha1-UqY+VsoLhKfzpfPWGHLxJq16WUM=", + "devOptional": true, + "engines": { + "node": "*" + } + }, + "node_modules/firebase-tools/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "devOptional": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/firebase-tools/node_modules/yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "devOptional": true, + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/firebase-tools/node_modules/yargs-parser": { + "version": "20.2.4", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz", + "integrity": "sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==", + "devOptional": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/firebase-tools/node_modules/yargs/node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "devOptional": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/firebase-tools/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==", + "devOptional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/firebase-tools/node_modules/zip-stream": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-4.1.0.tgz", + "integrity": "sha512-zshzwQW7gG7hjpBlgeQP9RuyPGNxvJdzR8SUM3QhxCnLjWN2E7j3dOvpeDcQoETfHx0urRS7EtmVToql7YpU4A==", + "devOptional": true, + "dependencies": { + "archiver-utils": "^2.1.0", + "compress-commons": "^4.1.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">= 10" + } + }, + "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.3.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.2.tgz", + "integrity": "sha512-AiwGJM8YcNOaobumgtng+6NHuOqC3A7MixFeDafM3X9cIUM+xUXoS5Vfgf+OihAYe20fxqNM9yPBXJzRtZ/4eA==", + "dev": true, + "license": "ISC" + }, + "node_modules/fn.name": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz", + "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==", + "license": "MIT" + }, + "node_modules/follow-redirects": { + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/foreground-child": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", + "integrity": "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.0", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/foreground-child/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fraction.js": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", + "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "patreon", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/fs-minipass": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-3.0.3.tgz", + "integrity": "sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/fs-minipass/node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/fs-monkey": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.0.6.tgz", + "integrity": "sha512-b1FMfwetIKymC0eioW7mTywihSQE4oLzQn1dB6rZB5fx/3NpNEdAWeCSMB+60/AeT0TCXsxzAlcYVEFCTAksWg==", + "dev": true, + "license": "Unlicense" + }, + "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==", + "dev": true, + "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==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/fuzzy": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/fuzzy/-/fuzzy-0.1.3.tgz", + "integrity": "sha512-/gZffu4ykarLrCiP3Ygsa86UAo1E5vEVlvTrpkKywXSbP9Xhln3oSp9QSV57gEq3JFFpGJ4GZ+5zdEp3FcUh4w==", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/gauge": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-4.0.4.tgz", + "integrity": "sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg==", + "deprecated": "This package is no longer supported.", + "dev": true, + "license": "ISC", + "dependencies": { + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.3", + "console-control-strings": "^1.1.0", + "has-unicode": "^2.0.1", + "signal-exit": "^3.0.7", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.5" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "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-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.7.tgz", + "integrity": "sha512-VW6Pxhsrk0KAOqs3WEd0klDiF/+V7gQOpAvY1jVU/LHmaD/kQO4523aiJuikX/QAKYiW6x8Jh+RJej1almdtCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "function-bind": "^1.1.2", + "get-proto": "^1.0.0", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "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==", + "devOptional": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "license": "BSD-2-Clause" + }, + "node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/globby": { + "version": "13.2.2", + "resolved": "https://registry.npmjs.org/globby/-/globby-13.2.2.tgz", + "integrity": "sha512-Y1zNGV+pzQdh7H39l9zgB4PJqjRNqydvdYCDG4HFXM4XuvSaQQlEc91IU1yALL8gUTDomgBAfz3XJdmUS+oo0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "dir-glob": "^3.0.1", + "fast-glob": "^3.3.0", + "ignore": "^5.2.4", + "merge2": "^1.4.1", + "slash": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/good-listener": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/good-listener/-/good-listener-1.2.2.tgz", + "integrity": "sha512-goW1b+d9q/HIwbVYZzZ6SsTr4IgE+WA44A0GmPIQstuOrgsFcT7VEJ48nmr9GaRtNu0XTKacFLGnBPAM6Afouw==", + "license": "MIT", + "dependencies": { + "delegate": "^3.1.2" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, + "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/gts": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/gts/-/gts-5.3.1.tgz", + "integrity": "sha512-P9F+krJkGOkisUX+P9pfUas1Xy+U+CxBFZT62uInkJbgvZpnW1ug/pIcMJJmLOthMq1J88lpQUGhXDC9UTvVcw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@typescript-eslint/eslint-plugin": "5.62.0", + "@typescript-eslint/parser": "5.62.0", + "chalk": "^4.1.2", + "eslint": "8.57.0", + "eslint-config-prettier": "9.1.0", + "eslint-plugin-n": "15.7.0", + "eslint-plugin-prettier": "5.1.3", + "execa": "^5.0.0", + "inquirer": "^7.3.3", + "json5": "^2.1.3", + "meow": "^9.0.0", + "ncp": "^2.0.0", + "prettier": "3.2.5", + "rimraf": "3.0.2", + "write-file-atomic": "^4.0.0" + }, + "bin": { + "gts": "build/src/cli.js" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "typescript": ">=3" + } + }, + "node_modules/gts/node_modules/inquirer": { + "version": "7.3.3", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-7.3.3.tgz", + "integrity": "sha512-JG3eIAj5V9CwcGvuOmoo6LB9kbAYT8HXffUl6memuszlwDC/qvFAJw49XJ5NROSFNPxp3iQg1GqkFhaY/CR0IA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-escapes": "^4.2.1", + "chalk": "^4.1.0", + "cli-cursor": "^3.1.0", + "cli-width": "^3.0.0", + "external-editor": "^3.0.3", + "figures": "^3.0.0", + "lodash": "^4.17.19", + "mute-stream": "0.0.8", + "run-async": "^2.4.0", + "rxjs": "^6.6.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0", + "through": "^2.3.6" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/gts/node_modules/prettier": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.5.tgz", + "integrity": "sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/gts/node_modules/rxjs": { + "version": "6.6.7", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", + "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^1.9.0" + }, + "engines": { + "npm": ">=2.0.0" + } + }, + "node_modules/gts/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true, + "license": "0BSD" + }, + "node_modules/handle-thing": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz", + "integrity": "sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg==", + "dev": true, + "license": "MIT" + }, + "node_modules/hard-rejection": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/hard-rejection/-/hard-rejection-2.1.0.tgz", + "integrity": "sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "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==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hdr-histogram-js": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/hdr-histogram-js/-/hdr-histogram-js-2.0.3.tgz", + "integrity": "sha512-Hkn78wwzWHNCp2uarhzQ2SGFLU3JY8SBDDd3TAABK4fc30wm+MuPOrg5QVFVfkKOQd6Bfz3ukJEI+q9sXEkK1g==", + "dev": true, + "license": "BSD", + "dependencies": { + "@assemblyscript/loader": "^0.10.1", + "base64-js": "^1.2.0", + "pako": "^1.0.3" + } + }, + "node_modules/hdr-histogram-percentiles-obj": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hdr-histogram-percentiles-obj/-/hdr-histogram-percentiles-obj-3.0.0.tgz", + "integrity": "sha512-7kIufnBqdsBGcSZLPJwqHT3yhk1QTsSlFsVD3kx5ixH/AlgBs9yM1q6DPhXZ8f8gtdqgh7N7/5btRLpQsS2gHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/hosted-git-info": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-6.1.3.tgz", + "integrity": "sha512-HVJyzUrLIL1c0QmviVh5E8VGyUS7xCFPS6yydaVd1UegW+ibV/CohqTH9MkOLDp5o+rb82DMo77PTuc9F/8GKw==", + "dev": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^7.5.1" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/hosted-git-info/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/hpack.js": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz", + "integrity": "sha512-zJxVehUdMGIKsRaNt7apO2Gqp0BdqW5yaiGHXXmbpvxgBYVZnAql+BJb4RO5ad2MgpbZKn5G6nMnegrH1FcNYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.1", + "obuf": "^1.0.0", + "readable-stream": "^2.0.1", + "wbuf": "^1.1.0" + } + }, + "node_modules/hpack.js/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/hpack.js/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/hpack.js/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/html-entities": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.5.2.tgz", + "integrity": "sha512-K//PSRMQk4FZ78Kyau+mZurHn3FH0Vwr+H36eE0rPbeYkRRi9YxceYPhuN60UwWorxyKHhqoAJl2OFKa4BVtaA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/mdevils" + }, + { + "type": "patreon", + "url": "https://patreon.com/mdevils" + } + ], + "license": "MIT" + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, + "node_modules/http-cache-semantics": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", + "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/http-deceiver": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz", + "integrity": "sha512-LmpOGxTfbpgtGVxJrj5k7asXHCgNZp5nLfp+hWc8QQRqtb7fUy6kRY3BO1h9ddF6yIPYUARgxGOwB42DnxIaNw==", + "dev": true, + "license": "MIT" + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/http-errors/node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/http-parser-js": { + "version": "0.5.9", + "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.9.tgz", + "integrity": "sha512-n1XsPy3rXVxlqxVioEWdC+0+M+SQw0DpJynwtOPo1X+ZlvdzTLtDBIJJlDQTnwZIFJrZSzSGmIOUdP8tu+SgLw==", + "license": "MIT" + }, + "node_modules/http-proxy": { + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", + "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eventemitter3": "^4.0.0", + "follow-redirects": "^1.0.0", + "requires-port": "^1.0.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/http-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", + "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tootallnate/once": "2", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/http-proxy-middleware": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.7.tgz", + "integrity": "sha512-fgVY8AV7qU7z/MmXJ/rxwbrtQH4jBQ9m7kp3llF0liB7glmFeVZFBepQb32T3y8n8k2+AEYuMPCpinYW+/CuRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-proxy": "^1.17.8", + "http-proxy": "^1.18.1", + "is-glob": "^4.0.1", + "is-plain-obj": "^3.0.0", + "micromatch": "^4.0.2" + }, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "@types/express": "^4.17.13" + }, + "peerDependenciesMeta": { + "@types/express": { + "optional": true + } + } + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/humanize-ms": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", + "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.0.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/icss-utils": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz", + "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/idb": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/idb/-/idb-7.1.1.tgz", + "integrity": "sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ==", + "license": "ISC" + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "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": "BSD-3-Clause" + }, + "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/ignore-walk": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-6.0.5.tgz", + "integrity": "sha512-VuuG0wCnjhnylG1ABXT3dAuIpTNDs/G8jlpmwXY03fXoXy/8ZK8/T+hMzt8L4WnrLCJgdybqgPagnF/f97cg3A==", + "dev": true, + "license": "ISC", + "dependencies": { + "minimatch": "^9.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/ignore-walk/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/image-size": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/image-size/-/image-size-0.5.5.tgz", + "integrity": "sha512-6TDAlDPZxUFCv+fuOkIoXT/V/f3Qbq8e37p+YOiYrUv3v9cc3/6x78VdfPgFVaB9dZYeLUfKgHRebpkm/oP2VQ==", + "dev": true, + "license": "MIT", + "optional": true, + "bin": { + "image-size": "bin/image-size.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/immutable": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.7.tgz", + "integrity": "sha512-1hqclzwYwjRDFLjcFxOM5AYkkG0rpFPpr1RLPMEuGczoS7YA8gLhy8SWXYRAA/XwfEHpfo3cw5JGioS32fnMRw==", + "dev": true, + "license": "MIT" + }, + "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/import-fresh/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/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/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/infer-owner": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/infer-owner/-/infer-owner-1.0.4.tgz", + "integrity": "sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==", + "dev": true, + "license": "ISC" + }, + "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==", + "license": "ISC" + }, + "node_modules/ini": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ini/-/ini-3.0.1.tgz", + "integrity": "sha512-it4HyVAUTKBc6m8e1iXWvXSTdndF7HbdN713+kvLrymxTaU4AUBWrJ4vEooP+V7fexnVD3LKcBshjGGPefSMUQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/inquirer": { + "version": "8.2.4", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-8.2.4.tgz", + "integrity": "sha512-nn4F01dxU8VeKfq192IjLsxu0/OmMZ4Lg3xKAns148rCaXP6ntAoEkVYZThWjwON8AlzdZZi6oqnhNbxUG9hVg==", + "license": "MIT", + "dependencies": { + "ansi-escapes": "^4.2.1", + "chalk": "^4.1.1", + "cli-cursor": "^3.1.0", + "cli-width": "^3.0.0", + "external-editor": "^3.0.3", + "figures": "^3.0.0", + "lodash": "^4.17.21", + "mute-stream": "0.0.8", + "ora": "^5.4.1", + "run-async": "^2.4.0", + "rxjs": "^7.5.5", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0", + "through": "^2.3.6", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/inquirer-autocomplete-prompt": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/inquirer-autocomplete-prompt/-/inquirer-autocomplete-prompt-1.4.0.tgz", + "integrity": "sha512-qHgHyJmbULt4hI+kCmwX92MnSxDs/Yhdt4wPA30qnoa01OF6uTXV8yvH4hKXgdaTNmkZ9D01MHjqKYEuJN+ONw==", + "license": "ISC", + "dependencies": { + "ansi-escapes": "^4.3.1", + "chalk": "^4.0.0", + "figures": "^3.2.0", + "run-async": "^2.4.0", + "rxjs": "^6.6.2" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "inquirer": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/inquirer-autocomplete-prompt/node_modules/rxjs": { + "version": "6.6.7", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", + "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^1.9.0" + }, + "engines": { + "npm": ">=2.0.0" + } + }, + "node_modules/inquirer-autocomplete-prompt/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "license": "0BSD" + }, + "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", + "optional": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/ionic-emoji-rating": { + "version": "1.0.15", + "resolved": "https://registry.npmjs.org/ionic-emoji-rating/-/ionic-emoji-rating-1.0.15.tgz", + "integrity": "sha512-ooNInojKERSgfoMZOAWvLm0MAARmAh1ibGr32x9S98J1jv2RC9DdaXOBWf/Xx9ZfJmvUAgZKjufcriszGcuZFA==", + "license": "MIT" + }, + "node_modules/ionicons": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/ionicons/-/ionicons-7.4.0.tgz", + "integrity": "sha512-ZK94MMqgzMCPPMhmk8Ouu6goyVHFIlw/ACP6oe3FrikcI0N7CX0xcwVaEbUc0G/v3W0shI93vo+9ve/KpvcNhQ==", + "license": "MIT", + "dependencies": { + "@stencil/core": "^4.0.3" + } + }, + "node_modules/ip-address": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz", + "integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "jsbn": "1.1.0", + "sprintf-js": "^1.1.3" + }, + "engines": { + "node": ">= 12" + } + }, + "node_modules/ip-address/node_modules/sprintf-js": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/ipaddr.js": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.2.0.tgz", + "integrity": "sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true, + "license": "MIT" + }, + "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==", + "devOptional": true, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "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==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-interactive": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", + "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-lambda": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-lambda/-/is-lambda-1.0.1.tgz", + "integrity": "sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==", + "dev": true, + "license": "MIT" + }, + "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==", + "devOptional": true, + "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": "3.0.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-3.0.0.tgz", + "integrity": "sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dev": true, + "license": "MIT", + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-what": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/is-what/-/is-what-3.14.1.tgz", + "integrity": "sha512-sNxgpk9793nzSs7bA6JQJGeIuRBQhAaNGG77kzYQgMkrID+lS6SlK07K5LaptscDlSaIgH+GPFzf+d75FVxozA==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "license": "MIT", + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/isbinaryfile": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-4.0.10.tgz", + "integrity": "sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/gjtorikian/" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", + "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument/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/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report/node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/istanbul-reports": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", + "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": 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/jasmine-core": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-4.5.0.tgz", + "integrity": "sha512-9PMzyvhtocxb3aXJVOPqBDswdgyAeSB81QnLop4npOpbqnheaTEwPc9ZloQeVswugPManznQBjD8kWDTjlnHuw==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-worker": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", + "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "license": "MIT", + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "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==", + "dev": true, + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsbn": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz", + "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "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==", + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonc-parser": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.0.tgz", + "integrity": "sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==", + "license": "MIT" + }, + "node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "license": "MIT", + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/jsonparse": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz", + "integrity": "sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==", + "dev": true, + "engines": [ + "node >= 0.2.0" + ], + "license": "MIT" + }, + "node_modules/karma": { + "version": "6.4.4", + "resolved": "https://registry.npmjs.org/karma/-/karma-6.4.4.tgz", + "integrity": "sha512-LrtUxbdvt1gOpo3gxG+VAJlJAEMhbWlM4YrFQgql98FwF7+K8K12LYO4hnDdUkNjeztYrOXEMqgTajSWgmtI/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@colors/colors": "1.5.0", + "body-parser": "^1.19.0", + "braces": "^3.0.2", + "chokidar": "^3.5.1", + "connect": "^3.7.0", + "di": "^0.0.1", + "dom-serialize": "^2.2.1", + "glob": "^7.1.7", + "graceful-fs": "^4.2.6", + "http-proxy": "^1.18.1", + "isbinaryfile": "^4.0.8", + "lodash": "^4.17.21", + "log4js": "^6.4.1", + "mime": "^2.5.2", + "minimatch": "^3.0.4", + "mkdirp": "^0.5.5", + "qjobs": "^1.2.0", + "range-parser": "^1.2.1", + "rimraf": "^3.0.2", + "socket.io": "^4.7.2", + "source-map": "^0.6.1", + "tmp": "^0.2.1", + "ua-parser-js": "^0.7.30", + "yargs": "^16.1.1" + }, + "bin": { + "karma": "bin/karma" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/karma-chrome-launcher": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/karma-chrome-launcher/-/karma-chrome-launcher-3.1.1.tgz", + "integrity": "sha512-hsIglcq1vtboGPAN+DGCISCFOxW+ZVnIqhDQcCMqqCp+4dmJ0Qpq5QAjkbA0X2L9Mi6OBkHi2Srrbmm7pUKkzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "which": "^1.2.1" + } + }, + "node_modules/karma-coverage": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/karma-coverage/-/karma-coverage-2.2.1.tgz", + "integrity": "sha512-yj7hbequkQP2qOSb20GuNSIyE//PgJWHwC2IydLE6XRtsnaflv+/OSGNssPjobYUlhVVagy99TQpqUt3vAUG7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "istanbul-lib-coverage": "^3.2.0", + "istanbul-lib-instrument": "^5.1.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.1", + "istanbul-reports": "^3.0.5", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/karma-coverage/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/karma-coverage/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/karma-jasmine": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/karma-jasmine/-/karma-jasmine-5.1.0.tgz", + "integrity": "sha512-i/zQLFrfEpRyQoJF9fsCdTMOF5c2dK7C7OmsuKg2D0YSsuZSfQDiLuaiktbuio6F2wiCsZSnSnieIQ0ant/uzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "jasmine-core": "^4.1.0" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "karma": "^6.0.0" + } + }, + "node_modules/karma-jasmine-html-reporter": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/karma-jasmine-html-reporter/-/karma-jasmine-html-reporter-2.0.0.tgz", + "integrity": "sha512-SB8HNNiazAHXM1vGEzf8/tSyEhkfxuDdhYdPBX2Mwgzt0OuF2gicApQ+uvXLID/gXyJQgvrM9+1/2SxZFUUDIA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "jasmine-core": "^4.0.0", + "karma": "^6.0.0", + "karma-jasmine": "^5.0.0" + } + }, + "node_modules/karma-source-map-support": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/karma-source-map-support/-/karma-source-map-support-1.4.0.tgz", + "integrity": "sha512-RsBECncGO17KAoJCYXjv+ckIz+Ii9NCi+9enk+rq6XC81ezYkb4/RHE6CTXdA7IOJqoF3wcaLfVG0CPmE5ca6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "source-map-support": "^0.5.5" + } + }, + "node_modules/karma/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/karma/node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/karma/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "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/karma/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/karma/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/karma/node_modules/tmp": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.3.tgz", + "integrity": "sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.14" + } + }, + "node_modules/karma/node_modules/yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/karma/node_modules/yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/katex": { + "version": "0.16.21", + "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.21.tgz", + "integrity": "sha512-XvqR7FgOHtWupfMiigNzmh+MgUVmDGU2kXZm899ZkPfcuoPuFxyHmXsgATDpFZDAXCI8tvinaVcDo8PIIJSo4A==", + "funding": [ + "https://opencollective.com/katex", + "https://github.com/sponsors/katex" + ], + "license": "MIT", + "optional": true, + "dependencies": { + "commander": "^8.3.0" + }, + "bin": { + "katex": "cli.js" + } + }, + "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/khroma": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/khroma/-/khroma-2.1.0.tgz", + "integrity": "sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw==", + "optional": true + }, + "node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/klona": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/klona/-/klona-2.0.6.tgz", + "integrity": "sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/kuler": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz", + "integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==", + "license": "MIT" + }, + "node_modules/layout-base": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/layout-base/-/layout-base-1.0.2.tgz", + "integrity": "sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg==", + "license": "MIT", + "optional": true + }, + "node_modules/less": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/less/-/less-4.1.3.tgz", + "integrity": "sha512-w16Xk/Ta9Hhyei0Gpz9m7VS8F28nieJaL/VyShID7cYvP6IL5oHeL6p4TXSDJqZE/lNv0oJ2pGVjJsRkfwm5FA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "copy-anything": "^2.0.1", + "parse-node-version": "^1.0.1", + "tslib": "^2.3.0" + }, + "bin": { + "lessc": "bin/lessc" + }, + "engines": { + "node": ">=6" + }, + "optionalDependencies": { + "errno": "^0.1.1", + "graceful-fs": "^4.1.2", + "image-size": "~0.5.0", + "make-dir": "^2.1.0", + "mime": "^1.4.1", + "needle": "^3.1.0", + "source-map": "~0.6.0" + } + }, + "node_modules/less-loader": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/less-loader/-/less-loader-11.1.0.tgz", + "integrity": "sha512-C+uDBV7kS7W5fJlUjq5mPBeBVhYpTIm5gB09APT9o3n/ILeaXVsiSFTbZpTJCJwQ/Crczfn3DmfQFwxYusWFug==", + "dev": true, + "license": "MIT", + "dependencies": { + "klona": "^2.0.4" + }, + "engines": { + "node": ">= 14.15.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "less": "^3.5.0 || ^4.0.0", + "webpack": "^5.0.0" + } + }, + "node_modules/less/node_modules/make-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", + "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "pify": "^4.0.1", + "semver": "^5.6.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/less/node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true, + "license": "MIT", + "optional": true, + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/less/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "license": "ISC", + "optional": true, + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/less/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "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/license-webpack-plugin": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/license-webpack-plugin/-/license-webpack-plugin-4.0.2.tgz", + "integrity": "sha512-771TFWFD70G1wLTC4oU2Cw4qvtmNrIw+wRvBtn+okgHl7slJVi7zfNcdmqDL72BojM30VNJ2UHylr1o77U37Jw==", + "dev": true, + "license": "ISC", + "dependencies": { + "webpack-sources": "^3.0.0" + }, + "peerDependenciesMeta": { + "webpack": { + "optional": true + }, + "webpack-sources": { + "optional": true + } + } + }, + "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==", + "dev": true, + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/loader-runner": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", + "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==", + "license": "MIT", + "engines": { + "node": ">=6.11.5" + } + }, + "node_modules/loader-utils": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-3.2.1.tgz", + "integrity": "sha512-ZvFw1KWS3GVyYBYb7qkmRM/WwL2TQQBxgCK62rlvm4WpVQ23Nb4tYjApUlfjrEGvOs7KHEsmyUn75OHZrJMWPw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12.13.0" + } + }, + "node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "license": "MIT" + }, + "node_modules/lodash-es": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", + "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==", + "license": "MIT", + "optional": true + }, + "node_modules/lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", + "license": "MIT" + }, + "node_modules/lodash.debounce": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", + "dev": true, + "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/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log4js": { + "version": "6.9.1", + "resolved": "https://registry.npmjs.org/log4js/-/log4js-6.9.1.tgz", + "integrity": "sha512-1somDdy9sChrr9/f4UlzhdaGfDR2c/SaD2a4T7qEkG4jTS57/B3qmnjLYePwQ8cqWnUHZI0iAKxMBpCZICiZ2g==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "date-format": "^4.0.14", + "debug": "^4.3.4", + "flatted": "^3.2.7", + "rfdc": "^1.3.0", + "streamroller": "^3.1.5" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/logform": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/logform/-/logform-2.7.0.tgz", + "integrity": "sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ==", + "license": "MIT", + "dependencies": { + "@colors/colors": "1.6.0", + "@types/triple-beam": "^1.3.2", + "fecha": "^4.2.0", + "ms": "^2.1.1", + "safe-stable-stringify": "^2.3.1", + "triple-beam": "^1.3.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/logform/node_modules/@colors/colors": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.6.0.tgz", + "integrity": "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==", + "license": "MIT", + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/long": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", + "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==", + "license": "Apache-2.0" + }, + "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/magic-string": { + "version": "0.29.0", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.29.0.tgz", + "integrity": "sha512-WcfidHrDjMY+eLjlU+8OvwREqHwpgCeKVBUpQ3OhYYuvfaYCUgcbuBzappNzZvg/v8onU3oQj+BYpkOJe9Iw4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.13" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/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/make-fetch-happen": { + "version": "10.2.1", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-10.2.1.tgz", + "integrity": "sha512-NgOPbRiaQM10DYXvN3/hhGVI2M5MtITFryzBGxHM5p4wnFxsVCbxkrBrDsk+EZ5OB4jEOT7AjDxtdF+KVEFT7w==", + "dev": true, + "license": "ISC", + "dependencies": { + "agentkeepalive": "^4.2.1", + "cacache": "^16.1.0", + "http-cache-semantics": "^4.1.0", + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.0", + "is-lambda": "^1.0.1", + "lru-cache": "^7.7.1", + "minipass": "^3.1.6", + "minipass-collect": "^1.0.2", + "minipass-fetch": "^2.0.3", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^0.6.3", + "promise-retry": "^2.0.1", + "socks-proxy-agent": "^7.0.0", + "ssri": "^9.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/make-fetch-happen/node_modules/@npmcli/fs": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-2.1.2.tgz", + "integrity": "sha512-yOJKRvohFOaLqipNtwYB9WugyZKhC/DZC4VYPmpaCzDBrA8YpK3qHZ8/HGscMnE4GqbkLNuVcCnxkeQEdGt6LQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "@gar/promisify": "^1.1.3", + "semver": "^7.3.5" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/make-fetch-happen/node_modules/cacache": { + "version": "16.1.3", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-16.1.3.tgz", + "integrity": "sha512-/+Emcj9DAXxX4cwlLmRI9c166RuL3w30zp4R7Joiv2cQTtTtA+jeuCAjH3ZlGnYS3tKENSrKhAzVVP9GVyzeYQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/fs": "^2.1.0", + "@npmcli/move-file": "^2.0.0", + "chownr": "^2.0.0", + "fs-minipass": "^2.1.0", + "glob": "^8.0.1", + "infer-owner": "^1.0.4", + "lru-cache": "^7.7.1", + "minipass": "^3.1.6", + "minipass-collect": "^1.0.2", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "mkdirp": "^1.0.4", + "p-map": "^4.0.0", + "promise-inflight": "^1.0.1", + "rimraf": "^3.0.2", + "ssri": "^9.0.0", + "tar": "^6.1.11", + "unique-filename": "^2.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/make-fetch-happen/node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/make-fetch-happen/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/make-fetch-happen/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/make-fetch-happen/node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true, + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/make-fetch-happen/node_modules/ssri": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-9.0.1.tgz", + "integrity": "sha512-o57Wcn66jMQvfHG1FlYbWeZWW/dHZhJXjpIcTfXldXEk5nz5lStPo3mK0OJQfGR3RbZUlbISexbljkJzuEj/8Q==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.1.1" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/make-fetch-happen/node_modules/unique-filename": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-2.0.1.tgz", + "integrity": "sha512-ODWHtkkdx3IAR+veKxFV+VBkUMcN+FaqzUUd7IZzt+0zhDZFPFxhlqwPF3YQvMHx1TD0tdgYl+kuPnJ8E6ql7A==", + "dev": true, + "license": "ISC", + "dependencies": { + "unique-slug": "^3.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/make-fetch-happen/node_modules/unique-slug": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-3.0.0.tgz", + "integrity": "sha512-8EyMynh679x/0gqE9fT9oilG+qEt+ibFyqjuVTsZn1+CMxH+XLlpvr2UZx4nVcCwTpx81nICr2JQFkM+HPLq4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/make-fetch-happen/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/map-obj": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-4.3.0.tgz", + "integrity": "sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/marked": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/marked/-/marked-4.3.0.tgz", + "integrity": "sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A==", + "license": "MIT", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 12" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/memfs": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/memfs/-/memfs-3.5.3.tgz", + "integrity": "sha512-UERzLsxzllchadvbPs5aolHh65ISpKpM+ccLbOJ8/vvpBKmAWf+la7dXFy7Mr0ySHbdHrFv5kGFCUHHe6GFEmw==", + "dev": true, + "license": "Unlicense", + "dependencies": { + "fs-monkey": "^1.0.4" + }, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/meow": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/meow/-/meow-9.0.0.tgz", + "integrity": "sha512-+obSblOQmRhcyBt62furQqRAQpNyWXo8BuQ5bN7dG8wmwQ+vwHKp/rCFD4CrTP8CsDQD1sjoZ94K417XEUk8IQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/minimist": "^1.2.0", + "camelcase-keys": "^6.2.2", + "decamelize": "^1.2.0", + "decamelize-keys": "^1.1.0", + "hard-rejection": "^2.1.0", + "minimist-options": "4.1.0", + "normalize-package-data": "^3.0.0", + "read-pkg-up": "^7.0.1", + "redent": "^3.0.0", + "trim-newlines": "^3.0.0", + "type-fest": "^0.18.0", + "yargs-parser": "^20.2.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/meow/node_modules/hosted-git-info": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz", + "integrity": "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==", + "dev": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/meow/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/meow/node_modules/normalize-package-data": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-3.0.3.tgz", + "integrity": "sha512-p2W1sgqij3zMMyRC067Dg16bfzVH+w7hyegmpIvZ4JNjqtGOVAIvLmjBx3yP7YTe9vKJgkoNOPjwQGogDoMXFA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "hosted-git-info": "^4.0.1", + "is-core-module": "^2.5.0", + "semver": "^7.3.4", + "validate-npm-package-license": "^3.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/meow/node_modules/type-fest": { + "version": "0.18.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.18.1.tgz", + "integrity": "sha512-OIAYXk8+ISY+qTOwkHtKqzAuxchoMiD9Udx+FSGQDuiRR+PJKJHc2NJAXlbhkGwTt/4/nKZxELY1w3ReWOL8mw==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/meow/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/meow/node_modules/yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "license": "MIT" + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/mermaid": { + "version": "9.4.3", + "resolved": "https://registry.npmjs.org/mermaid/-/mermaid-9.4.3.tgz", + "integrity": "sha512-TLkQEtqhRSuEHSE34lh5bCa94KATCyluAXmFnNI2PRZwOpXFeqiJWwZl+d2CcemE1RS6QbbueSSq9QIg8Uxcyw==", + "license": "MIT", + "optional": true, + "dependencies": { + "@braintree/sanitize-url": "^6.0.0", + "cytoscape": "^3.23.0", + "cytoscape-cose-bilkent": "^4.1.0", + "cytoscape-fcose": "^2.1.0", + "d3": "^7.4.0", + "dagre-d3-es": "7.0.9", + "dayjs": "^1.11.7", + "dompurify": "2.4.3", + "elkjs": "^0.8.2", + "khroma": "^2.0.0", + "lodash-es": "^4.17.21", + "non-layered-tidy-tree-layout": "^2.0.2", + "stylis": "^4.1.2", + "ts-dedent": "^2.2.0", + "uuid": "^9.0.0", + "web-worker": "^1.2.0" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/mini-css-extract-plugin": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.7.2.tgz", + "integrity": "sha512-EdlUizq13o0Pd+uCp+WO/JpkLvHRVGt97RqfeGhXqAcorYo1ypJSpkV+WDT0vY/kmh/p7wRdJNJtuyK540PXDw==", + "dev": true, + "license": "MIT", + "dependencies": { + "schema-utils": "^4.0.0" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" + } + }, + "node_modules/minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", + "dev": true, + "license": "ISC" + }, + "node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minimist-options": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/minimist-options/-/minimist-options-4.1.0.tgz", + "integrity": "sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "arrify": "^1.0.1", + "is-plain-obj": "^1.1.0", + "kind-of": "^6.0.3" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/minimist-options/node_modules/is-plain-obj": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", + "integrity": "sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/minipass": { + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-4.2.8.tgz", + "integrity": "sha512-fNzuVyifolSLFL4NzpF+wEF4qrgqaaKX0haXPQEdQ7NKAN+WecoKMHV09YcuL/DHxrUsYQOK3MiuDf7Ip2OXfQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-collect": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-1.0.2.tgz", + "integrity": "sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minipass-collect/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-collect/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/minipass-fetch": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-2.1.2.tgz", + "integrity": "sha512-LT49Zi2/WMROHYoqGgdlQIZh8mLPZmOrN2NdJjMXxYe4nkN6FUyuPuOAOedNJDrx0IRGg9+4guZewtp8hE6TxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^3.1.6", + "minipass-sized": "^1.0.3", + "minizlib": "^2.1.2" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + }, + "optionalDependencies": { + "encoding": "^0.1.13" + } + }, + "node_modules/minipass-fetch/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-fetch/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/minipass-flush": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz", + "integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minipass-flush/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-flush/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/minipass-json-stream": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/minipass-json-stream/-/minipass-json-stream-1.0.2.tgz", + "integrity": "sha512-myxeeTm57lYs8pH2nxPzmEEg8DGIgW+9mv6D4JZD2pa81I/OBjeU7PtICXV6c9eRGTA5JMDsuIPUZRCyBMYNhg==", + "dev": true, + "license": "MIT", + "dependencies": { + "jsonparse": "^1.3.1", + "minipass": "^3.0.0" + } + }, + "node_modules/minipass-json-stream/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-json-stream/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/minipass-pipeline": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", + "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-pipeline/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-pipeline/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/minipass-sized": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz", + "integrity": "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-sized/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-sized/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minizlib/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minizlib/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/moment": { + "version": "2.30.1", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", + "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/multicast-dns": { + "version": "7.2.5", + "resolved": "https://registry.npmjs.org/multicast-dns/-/multicast-dns-7.2.5.tgz", + "integrity": "sha512-2eznPJP8z2BFLX50tf0LuODrpINqP1RVIm/CObbTcBRITQgmC/TjcREF1NeTBzIcR5XO/ukWo+YHOjBbFwIupg==", + "dev": true, + "license": "MIT", + "dependencies": { + "dns-packet": "^5.2.2", + "thunky": "^1.0.2" + }, + "bin": { + "multicast-dns": "cli.js" + } + }, + "node_modules/mute-stream": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", + "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", + "license": "ISC" + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.8", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz", + "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==", + "dev": true, + "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/natural-compare-lite": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz", + "integrity": "sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==", + "dev": true, + "license": "MIT" + }, + "node_modules/ncp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ncp/-/ncp-2.0.0.tgz", + "integrity": "sha512-zIdGUrPRFTUELUvr3Gmc7KZ2Sw/h1PiVM0Af/oHB6zgnV1ikqSfRk+TOufi79aHYCW3NiOXmr1BP5nWbzojLaA==", + "dev": true, + "license": "MIT", + "bin": { + "ncp": "bin/ncp" + } + }, + "node_modules/needle": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/needle/-/needle-3.3.1.tgz", + "integrity": "sha512-6k0YULvhpw+RoLNiQCRKOl09Rv1dPLr8hHnVjHqdolKwDrdNyk+Hmrthi4lIGPPz3r39dLx0hsF5s40sZ3Us4Q==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "iconv-lite": "^0.6.3", + "sax": "^1.2.4" + }, + "bin": { + "needle": "bin/needle" + }, + "engines": { + "node": ">= 4.4.x" + } + }, + "node_modules/needle/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/negotiator": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", + "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "license": "MIT" + }, + "node_modules/ng2-pdf-viewer": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/ng2-pdf-viewer/-/ng2-pdf-viewer-7.0.2.tgz", + "integrity": "sha512-yAd8OddmPCnkERv3IWFyZ1GxkMx+n2fc8NQQtWCjQbhr1sKjrP0TQH41AmgudSzTtvWJw9z784V+TKIaWA0eKw==", + "license": "MIT", + "dependencies": { + "pdfjs-dist": "~2.11.338", + "tslib": "^2.0.0" + }, + "peerDependencies": { + "pdfjs-dist": "~2.11.338" + } + }, + "node_modules/ngx-markdown": { + "version": "15.1.2", + "resolved": "https://registry.npmjs.org/ngx-markdown/-/ngx-markdown-15.1.2.tgz", + "integrity": "sha512-mAUORpUnHCV4tnxEHV4oS5YEdIaolUclulCblUrvAEU3AEND8MMTxlwHujqVC2M398/aKH0SBSrjLzDbMUJCoQ==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "optionalDependencies": { + "clipboard": "^2.0.11", + "emoji-toolkit": "^7.0.0", + "katex": "^0.16.0", + "mermaid": "^9.1.2", + "prismjs": "^1.28.0" + }, + "peerDependencies": { + "@angular/common": "^15.0.0", + "@angular/core": "^15.0.0", + "@angular/platform-browser": "^15.0.0", + "@types/marked": "^4.0.3", + "marked": "^4.0.17", + "rxjs": "^6.5.3 || ^7.4.0", + "zone.js": "~0.11.4 || ~0.12.0 || ~0.13.0" + } + }, + "node_modules/nice-napi": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/nice-napi/-/nice-napi-1.0.2.tgz", + "integrity": "sha512-px/KnJAJZf5RuBGcfD+Sp2pAKq0ytz8j+1NehvgIGFkvtvFrDM3T8E4x/JJODXK9WZow8RRGrbA9QQ3hs+pDhA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "!win32" + ], + "dependencies": { + "node-addon-api": "^3.0.0", + "node-gyp-build": "^4.2.2" + } + }, + "node_modules/node-addon-api": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-3.2.1.tgz", + "integrity": "sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-forge": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", + "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==", + "dev": true, + "license": "(BSD-3-Clause OR GPL-2.0)", + "engines": { + "node": ">= 6.13.0" + } + }, + "node_modules/node-gyp": { + "version": "9.4.1", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-9.4.1.tgz", + "integrity": "sha512-OQkWKbjQKbGkMf/xqI1jjy3oCTgMKJac58G2+bjZb3fza6gW2YrCSdMQYaoTb70crvE//Gngr4f0AgVHmqHvBQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "env-paths": "^2.2.0", + "exponential-backoff": "^3.1.1", + "glob": "^7.1.4", + "graceful-fs": "^4.2.6", + "make-fetch-happen": "^10.0.3", + "nopt": "^6.0.0", + "npmlog": "^6.0.0", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "tar": "^6.1.2", + "which": "^2.0.2" + }, + "bin": { + "node-gyp": "bin/node-gyp.js" + }, + "engines": { + "node": "^12.13 || ^14.13 || >=16" + } + }, + "node_modules/node-gyp-build": { + "version": "4.8.4", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", + "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==", + "dev": true, + "license": "MIT", + "optional": true, + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, + "node_modules/node-gyp/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/node-gyp/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "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/node-gyp/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/node-gyp/node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/node-releases": { + "version": "2.0.19", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", + "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", + "license": "MIT" + }, + "node_modules/non-layered-tidy-tree-layout": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/non-layered-tidy-tree-layout/-/non-layered-tidy-tree-layout-2.0.2.tgz", + "integrity": "sha512-gkXMxRzUH+PB0ax9dUN0yYF0S25BqeAYqhgMaLUFmpXLEk7Fcu8f4emJuOAY0V8kjDICxROIKsTAKsV/v355xw==", + "license": "MIT", + "optional": true + }, + "node_modules/nopt": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-6.0.0.tgz", + "integrity": "sha512-ZwLpbTgdhuZUnZzjd7nb1ZV+4DoiC6/sfiVKok72ym/4Tlf+DFdlHYmT2JPmcNNWV6Pi3SDf1kT+A4r9RTuT9g==", + "dev": true, + "license": "ISC", + "dependencies": { + "abbrev": "^1.0.0" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/normalize-package-data": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-5.0.0.tgz", + "integrity": "sha512-h9iPVIfrVZ9wVYQnxFgtw1ugSvGEMOlyPWWtm8BMJhnwyEL/FLbYbTY3V3PpjI/BUK67n9PEWDu6eHzu1fB15Q==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "hosted-git-info": "^6.0.0", + "is-core-module": "^2.8.1", + "semver": "^7.3.5", + "validate-npm-package-license": "^3.0.4" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "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==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-range": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-bundled": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-3.0.1.tgz", + "integrity": "sha512-+AvaheE/ww1JEwRHOrn4WHNzOxGtVp+adrg2AeZS/7KuxGUYFuBta98wYpfHBbJp6Tg6j1NKSEVHNcfZzJHQwQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "npm-normalize-package-bin": "^3.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm-install-checks": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/npm-install-checks/-/npm-install-checks-6.3.0.tgz", + "integrity": "sha512-W29RiK/xtpCGqn6f3ixfRYGk+zRyr+Ew9F2E20BfXxT5/euLdA/Nm7fO7OeTGuAmTs30cpgInyJ0cYe708YTZw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "semver": "^7.1.1" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm-normalize-package-bin": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-3.0.1.tgz", + "integrity": "sha512-dMxCf+zZ+3zeQZXKxmyuCKlIDPGuv8EF940xbkC4kQVDTtqoh6rJFO+JTKSA6/Rwi0getWmtuy4Itup0AMcaDQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm-package-arg": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-10.1.0.tgz", + "integrity": "sha512-uFyyCEmgBfZTtrKk/5xDfHp6+MdrqGotX/VoOyEEl3mBwiEE5FlBaePanazJSVMPT7vKepcjYBY2ztg9A3yPIA==", + "dev": true, + "license": "ISC", + "dependencies": { + "hosted-git-info": "^6.0.0", + "proc-log": "^3.0.0", + "semver": "^7.3.5", + "validate-npm-package-name": "^5.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm-packlist": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-7.0.4.tgz", + "integrity": "sha512-d6RGEuRrNS5/N84iglPivjaJPxhDbZmlbTwTDX2IbcRHG5bZCdtysYMhwiPvcF4GisXHGn7xsxv+GQ7T/02M5Q==", + "dev": true, + "license": "ISC", + "dependencies": { + "ignore-walk": "^6.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm-pick-manifest": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/npm-pick-manifest/-/npm-pick-manifest-8.0.1.tgz", + "integrity": "sha512-mRtvlBjTsJvfCCdmPtiu2bdlx8d/KXtF7yNXNWe7G0Z36qWA9Ny5zXsI2PfBZEv7SXgoxTmNaTzGSbbzDZChoA==", + "dev": true, + "license": "ISC", + "dependencies": { + "npm-install-checks": "^6.0.0", + "npm-normalize-package-bin": "^3.0.0", + "npm-package-arg": "^10.0.0", + "semver": "^7.3.5" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm-registry-fetch": { + "version": "14.0.5", + "resolved": "https://registry.npmjs.org/npm-registry-fetch/-/npm-registry-fetch-14.0.5.tgz", + "integrity": "sha512-kIDMIo4aBm6xg7jOttupWZamsZRkAqMqwqqbVXnUqstY5+tapvv6bkH/qMR76jdgV+YljEUCyWx3hRYMrJiAgA==", + "dev": true, + "license": "ISC", + "dependencies": { + "make-fetch-happen": "^11.0.0", + "minipass": "^5.0.0", + "minipass-fetch": "^3.0.0", + "minipass-json-stream": "^1.0.1", + "minizlib": "^2.1.2", + "npm-package-arg": "^10.0.0", + "proc-log": "^3.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm-registry-fetch/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/npm-registry-fetch/node_modules/make-fetch-happen": { + "version": "11.1.1", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-11.1.1.tgz", + "integrity": "sha512-rLWS7GCSTcEujjVBs2YqG7Y4643u8ucvCJeSRqiLYhesrDuzeuFIk37xREzAsfQaqzl8b9rNCE4m6J8tvX4Q8w==", + "dev": true, + "license": "ISC", + "dependencies": { + "agentkeepalive": "^4.2.1", + "cacache": "^17.0.0", + "http-cache-semantics": "^4.1.1", + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.0", + "is-lambda": "^1.0.1", + "lru-cache": "^7.7.1", + "minipass": "^5.0.0", + "minipass-fetch": "^3.0.0", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^0.6.3", + "promise-retry": "^2.0.1", + "socks-proxy-agent": "^7.0.0", + "ssri": "^10.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm-registry-fetch/node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=8" + } + }, + "node_modules/npm-registry-fetch/node_modules/minipass-fetch": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-3.0.5.tgz", + "integrity": "sha512-2N8elDQAtSnFV0Dk7gt15KHsS0Fyz6CbYZ360h0WTYV1Ty46li3rAXVOQj1THMNLdmrD9Vt5pBPtWtVkpwGBqg==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^7.0.3", + "minipass-sized": "^1.0.3", + "minizlib": "^2.1.2" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + }, + "optionalDependencies": { + "encoding": "^0.1.13" + } + }, + "node_modules/npm-registry-fetch/node_modules/minipass-fetch/node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npmlog": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-6.0.2.tgz", + "integrity": "sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg==", + "deprecated": "This package is no longer supported.", + "dev": true, + "license": "ISC", + "dependencies": { + "are-we-there-yet": "^3.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^4.0.3", + "set-blocking": "^2.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "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==", + "dev": true, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/object-inspect": { + "version": "1.13.3", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.3.tgz", + "integrity": "sha512-kDCGIbxkDSXE3euJZZXzc6to7fCrKHNI/hSRQnRuQ+BWjFNzZwiFF8fj/6o2t2G9/jTj8PSIYTfCLelLZEeRpA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/obuf": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz", + "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==", + "dev": true, + "license": "MIT" + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/on-headers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", + "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "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/one-time": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/one-time/-/one-time-1.0.0.tgz", + "integrity": "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==", + "license": "MIT", + "dependencies": { + "fn.name": "1.x.x" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/open": { + "version": "8.4.1", + "resolved": "https://registry.npmjs.org/open/-/open-8.4.1.tgz", + "integrity": "sha512-/4b7qZNhv6Uhd7jjnREh1NjnPxlTq+XNWPG88Ydkj5AILcA5m3ajvcg57pB24EQjKv0dK62XnDqk9c/hkIG5Kg==", + "license": "MIT", + "dependencies": { + "define-lazy-prop": "^2.0.0", + "is-docker": "^2.1.1", + "is-wsl": "^2.2.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "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/ora": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", + "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", + "license": "MIT", + "dependencies": { + "bl": "^4.1.0", + "chalk": "^4.1.0", + "cli-cursor": "^3.1.0", + "cli-spinners": "^2.5.0", + "is-interactive": "^1.0.0", + "is-unicode-supported": "^0.1.0", + "log-symbols": "^4.1.0", + "strip-ansi": "^6.0.0", + "wcwidth": "^1.0.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-map": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", + "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "aggregate-error": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-retry": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz", + "integrity": "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/retry": "0.12.0", + "retry": "^0.13.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-retry/node_modules/retry": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, + "node_modules/pacote": { + "version": "15.0.8", + "resolved": "https://registry.npmjs.org/pacote/-/pacote-15.0.8.tgz", + "integrity": "sha512-UlcumB/XS6xyyIMwg/WwMAyUmga+RivB5KgkRwA1hZNtrx+0Bt41KxHCvg1kr0pZ/ZeD8qjhW4fph6VaYRCbLw==", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/git": "^4.0.0", + "@npmcli/installed-package-contents": "^2.0.1", + "@npmcli/promise-spawn": "^6.0.1", + "@npmcli/run-script": "^6.0.0", + "cacache": "^17.0.0", + "fs-minipass": "^3.0.0", + "minipass": "^4.0.0", + "npm-package-arg": "^10.0.0", + "npm-packlist": "^7.0.0", + "npm-pick-manifest": "^8.0.0", + "npm-registry-fetch": "^14.0.0", + "proc-log": "^3.0.0", + "promise-retry": "^2.0.1", + "read-package-json": "^6.0.0", + "read-package-json-fast": "^3.0.0", + "ssri": "^10.0.0", + "tar": "^6.1.11" + }, + "bin": { + "pacote": "lib/bin.js" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "dev": true, + "license": "(MIT AND Zlib)" + }, + "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-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parse-node-version": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parse-node-version/-/parse-node-version-1.0.1.tgz", + "integrity": "sha512-3YHlOa/JgH6Mnpr05jP9eDG254US9ek25LyIxZlDItp2iJtwyaXQb57lBYLdT3MowkUFYEV2XXNAYIPlESvJlA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/parse5": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.2.1.tgz", + "integrity": "sha512-BuBYQYlv1ckiPdQi/ohiivi9Sagc9JG+Ozs0r7b/0iK3sKmrb0b9FdWdBbOdx6hBCM/F9Ir82ofnBhtZOjCRPQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "entities": "^4.5.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-html-rewriting-stream": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/parse5-html-rewriting-stream/-/parse5-html-rewriting-stream-7.0.0.tgz", + "integrity": "sha512-mazCyGWkmCRWDI15Zp+UiCqMp/0dgEmkZRvhlsqqKYr4SsVm/TvnSpD9fCvqCA2zoWJcfRym846ejWBBHRiYEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^4.3.0", + "parse5": "^7.0.0", + "parse5-sax-parser": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-html-rewriting-stream/node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/parse5-htmlparser2-tree-adapter": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-6.0.1.tgz", + "integrity": "sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "parse5": "^6.0.1" + } + }, + "node_modules/parse5-htmlparser2-tree-adapter/node_modules/parse5": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz", + "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==", + "dev": true, + "license": "MIT" + }, + "node_modules/parse5-sax-parser": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/parse5-sax-parser/-/parse5-sax-parser-7.0.0.tgz", + "integrity": "sha512-5A+v2SNsq8T6/mG3ahcz8ZtQ0OUFTatxPbeidoMB7tkJSGDY3tdfl4MHovtLQHkEn5CGxijNWRQHhRQ6IRpXKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5/node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "devOptional": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "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==", + "dev": true, + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": 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/path-scurry/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/path-scurry/node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "dev": true, + "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/pdfjs-dist": { + "version": "2.11.338", + "resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-2.11.338.tgz", + "integrity": "sha512-Ti5VTB0VvSdtTtc7TG71ghMx0SEuNcEs4ghVuZxW0p6OqLjMc0xekZV1B+MmlxEG2Du2e5jgazucWIG/SXTcdA==", + "license": "Apache-2.0", + "peerDependencies": { + "worker-loader": "^3.0.8" + }, + "peerDependenciesMeta": { + "worker-loader": { + "optional": true + } + } + }, + "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.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=6" + } + }, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/piscina": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/piscina/-/piscina-3.2.0.tgz", + "integrity": "sha512-yn/jMdHRw+q2ZJhFhyqsmANcbF6V2QwmD84c6xRau+QpQOmtrBCoRGdvTfeuFDYXB5W2m6MfLkjkvQa9lUSmIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eventemitter-asyncresource": "^1.0.0", + "hdr-histogram-js": "^2.0.1", + "hdr-histogram-percentiles-obj": "^3.0.0" + }, + "optionalDependencies": { + "nice-napi": "^1.0.2" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/postcss": { + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.6", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "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==", + "dev": true, + "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.0.1", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz", + "integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==", + "dev": true, + "license": "MIT", + "dependencies": { + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": "^8.4.21" + } + }, + "node_modules/postcss-load-config": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz", + "integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "lilconfig": "^3.0.0", + "yaml": "^2.3.4" + }, + "engines": { + "node": ">= 14" + }, + "peerDependencies": { + "postcss": ">=8.0.9", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "postcss": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/postcss-load-config/node_modules/yaml": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.0.tgz", + "integrity": "sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ==", + "dev": true, + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + } + }, + "node_modules/postcss-loader": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/postcss-loader/-/postcss-loader-7.0.2.tgz", + "integrity": "sha512-fUJzV/QH7NXUAqV8dWJ9Lg4aTkDCezpTS5HgJ2DvqznexTbSTxgi/dTECvTZ15BwKTtk8G/bqI/QTu2HPd3ZCg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cosmiconfig": "^7.0.0", + "klona": "^2.0.5", + "semver": "^7.3.8" + }, + "engines": { + "node": ">= 14.15.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "postcss": "^7.0.0 || ^8.0.1", + "webpack": "^5.0.0" + } + }, + "node_modules/postcss-modules-extract-imports": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.1.0.tgz", + "integrity": "sha512-k3kNe0aNFQDAZGbin48pL2VNidTF0w4/eASDsxlyspobzU3wZQLOGj7L9gfRe0Jo9/4uud09DsjFNH7winGv8Q==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-local-by-default": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.2.0.tgz", + "integrity": "sha512-5kcJm/zk+GJDSfw+V/42fJ5fhjL5YbFDl8nVdXkJPLLW+Vf9mTD5Xe0wqIaDnLuL2U6cDNpTr+UQ+v2HWIBhzw==", + "dev": true, + "license": "MIT", + "dependencies": { + "icss-utils": "^5.0.0", + "postcss-selector-parser": "^7.0.0", + "postcss-value-parser": "^4.1.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-scope": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.2.1.tgz", + "integrity": "sha512-m9jZstCVaqGjTAuny8MdgE88scJnCiQSlSrOWcTQgM2t32UBe+MUmFSO5t7VMSfAf/FJKImAxBav8ooCHJXCJA==", + "dev": true, + "license": "ISC", + "dependencies": { + "postcss-selector-parser": "^7.0.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-values": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz", + "integrity": "sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "icss-utils": "^5.0.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "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==", + "dev": true, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-selector-parser": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.0.0.tgz", + "integrity": "sha512-9RbEr1Y7FFfptd/1eEdntyjMwLeghW1bHX9GWjXo19vx4ytPQhANltvVxDggzJl7mnWM+dX28kb6cyS/4iQjlQ==", + "dev": true, + "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==", + "dev": true, + "license": "MIT" + }, + "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/prettier": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.1.tgz", + "integrity": "sha512-hPpFQvHwL3Qv5AdRvBFMhnKo4tYxp0ReXiPn2bxkiohEX6mBeBwEpBSQTkD458RaaDKQMYSp4hX4UtfUTA5wDw==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prettier-linter-helpers": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz", + "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-diff": "^1.1.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/pretty-bytes": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz", + "integrity": "sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/prismjs": { + "version": "1.29.0", + "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.29.0.tgz", + "integrity": "sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/proc-log": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-3.0.0.tgz", + "integrity": "sha512-++Vn7NS4Xf9NacaU9Xq3URUuqZETPsf8L4j5/ckhaRYsfPeRyzGw+iDjFhV/Jr3uNmTvvddEJFWh5R1gRgUH8A==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true, + "license": "MIT" + }, + "node_modules/promise-inflight": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", + "integrity": "sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/promise-retry": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", + "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "err-code": "^2.0.2", + "retry": "^0.12.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/protobufjs": { + "version": "6.11.4", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.11.4.tgz", + "integrity": "sha512-5kQWPaJHi1WoCpjTGszzQ32PG2F4+wRY6BmAT4Vfw56Q2FZ4YZzK20xUYQH4YkfehY1e6QSICrJquM6xXZNcrw==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/long": "^4.0.1", + "@types/node": ">=13.7.0", + "long": "^4.0.0" + }, + "bin": { + "pbjs": "bin/pbjs", + "pbts": "bin/pbts" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/proxy-addr/node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/prr": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", + "integrity": "sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/qjobs": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/qjobs/-/qjobs-1.2.0.tgz", + "integrity": "sha512-8YOJEHtxpySA3fFDyCRxA+UUV+fA+rTWnuWvylOK/NCjhY+b4ocCtmu8TtsWb+mYeU+GCHf/S66KZF/AsteKHg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.9" + } + }, + "node_modules/qs": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/quick-lru": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-4.0.1.tgz", + "integrity": "sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "dev": true, + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/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==", + "dev": true, + "license": "MIT", + "dependencies": { + "pify": "^2.3.0" + } + }, + "node_modules/read-cache/node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/read-package-json": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/read-package-json/-/read-package-json-6.0.4.tgz", + "integrity": "sha512-AEtWXYfopBj2z5N5PbkAOeNHRPUg5q+Nen7QLxV8M2zJq1ym6/lCz3fYNTCXe19puu2d06jfHhrP7v/S2PtMMw==", + "deprecated": "This package is no longer supported. Please use @npmcli/package-json instead.", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^10.2.2", + "json-parse-even-better-errors": "^3.0.0", + "normalize-package-data": "^5.0.0", + "npm-normalize-package-bin": "^3.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/read-package-json-fast": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/read-package-json-fast/-/read-package-json-fast-3.0.2.tgz", + "integrity": "sha512-0J+Msgym3vrLOUB3hzQCuZHII0xkNGCtz/HJH9xZshwv9DbDwkw1KaE3gx/e2J5rpEY5rtOy6cyhKOPrkP7FZw==", + "dev": true, + "license": "ISC", + "dependencies": { + "json-parse-even-better-errors": "^3.0.0", + "npm-normalize-package-bin": "^3.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/read-package-json-fast/node_modules/json-parse-even-better-errors": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-3.0.2.tgz", + "integrity": "sha512-fi0NG4bPjCHunUJffmLd0gxssIgkNmArMvis4iNah6Owg1MCJjWhEcDLmsK6iGkJq3tHwbDkTlce70/tmXN4cQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/read-package-json/node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": 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/read-package-json/node_modules/json-parse-even-better-errors": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-3.0.2.tgz", + "integrity": "sha512-fi0NG4bPjCHunUJffmLd0gxssIgkNmArMvis4iNah6Owg1MCJjWhEcDLmsK6iGkJq3tHwbDkTlce70/tmXN4cQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/read-package-json/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/read-package-json/node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/read-pkg": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz", + "integrity": "sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/normalize-package-data": "^2.4.0", + "normalize-package-data": "^2.5.0", + "parse-json": "^5.0.0", + "type-fest": "^0.6.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/read-pkg-up": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-7.0.1.tgz", + "integrity": "sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^4.1.0", + "read-pkg": "^5.2.0", + "type-fest": "^0.8.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-pkg-up/node_modules/type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=8" + } + }, + "node_modules/read-pkg/node_modules/hosted-git-info": { + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", + "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", + "dev": true, + "license": "ISC" + }, + "node_modules/read-pkg/node_modules/normalize-package-data": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", + "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "hosted-git-info": "^2.1.4", + "resolve": "^1.10.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + } + }, + "node_modules/read-pkg/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/read-pkg/node_modules/type-fest": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.6.0.tgz", + "integrity": "sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=8" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/reflect-metadata": { + "version": "0.1.14", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.14.tgz", + "integrity": "sha512-ZhYeb6nRaXCfhnndflDK8qI6ZQ/YcWZCISRAWICW9XYqMUwjZM9Z0DveWX/ABN01oxSHwVxKQmxeYZSsm0jh5A==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/regenerate": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", + "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==", + "dev": true, + "license": "MIT" + }, + "node_modules/regenerate-unicode-properties": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.2.0.tgz", + "integrity": "sha512-DqHn3DwbmmPVzeKj9woBadqmXxLvQoQIwu7nopMc72ztvxVmVk2SBhSnx67zuye5TP+lJsb/TBQsjLKhnDf3MA==", + "dev": true, + "license": "MIT", + "dependencies": { + "regenerate": "^1.4.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/regenerator-runtime": { + "version": "0.13.11", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", + "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==", + "dev": true, + "license": "MIT" + }, + "node_modules/regenerator-transform": { + "version": "0.15.2", + "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.15.2.tgz", + "integrity": "sha512-hfMp2BoF0qOk3uc5V20ALGDS2ddjQaLrdl7xrGXvAIow7qeWRM2VA2HuCHkUKk9slq3VwEwLNK3DFBqDfPGYtg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.8.4" + } + }, + "node_modules/regex-parser": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/regex-parser/-/regex-parser-2.3.0.tgz", + "integrity": "sha512-TVILVSz2jY5D47F4mA4MppkBrafEaiUWJO/TcZHEIuI13AqoZMkK1WMA4Om1YkYbTx+9Ki1/tSUXbceyr9saRg==", + "dev": true, + "license": "MIT" + }, + "node_modules/regexpp": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz", + "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + } + }, + "node_modules/regexpu-core": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-6.2.0.tgz", + "integrity": "sha512-H66BPQMrv+V16t8xtmq+UC0CBpiTBA60V8ibS1QVReIp8T1z8hwFxqcGzm9K6lgsN7sB5edVH8a+ze6Fqm4weA==", + "dev": true, + "license": "MIT", + "dependencies": { + "regenerate": "^1.4.2", + "regenerate-unicode-properties": "^10.2.0", + "regjsgen": "^0.8.0", + "regjsparser": "^0.12.0", + "unicode-match-property-ecmascript": "^2.0.0", + "unicode-match-property-value-ecmascript": "^2.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/regjsgen": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.8.0.tgz", + "integrity": "sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/regjsparser": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.12.0.tgz", + "integrity": "sha512-cnE+y8bz4NhMjISKbgeVJtqNbtf5QpjZP+Bslo+UqkIt9QPnX9q095eiRRASJG1/tz6dlNr6Z5NsBiWYokp6EQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "jsesc": "~3.0.2" + }, + "bin": { + "regjsparser": "bin/parser" + } + }, + "node_modules/regjsparser/node_modules/jsesc": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.0.2.tgz", + "integrity": "sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "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/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/resolve": { + "version": "1.22.1", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz", + "integrity": "sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.9.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-url-loader": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-url-loader/-/resolve-url-loader-5.0.0.tgz", + "integrity": "sha512-uZtduh8/8srhBoMx//5bwqjQ+rfYOUq8zC9NrMUGtjBiGTtFJM42s58/36+hTqeqINcnYe08Nj3LkK9lW4N8Xg==", + "dev": true, + "license": "MIT", + "dependencies": { + "adjust-sourcemap-loader": "^4.0.0", + "convert-source-map": "^1.7.0", + "loader-utils": "^2.0.0", + "postcss": "^8.2.14", + "source-map": "0.6.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/resolve-url-loader/node_modules/loader-utils": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", + "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" + }, + "engines": { + "node": ">=8.9.0" + } + }, + "node_modules/resolve-url-loader/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "license": "MIT", + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "dev": true, + "license": "MIT" + }, + "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/rimraf/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/rimraf/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "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/rimraf/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/robust-predicates": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.2.tgz", + "integrity": "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==", + "license": "Unlicense", + "optional": true + }, + "node_modules/run-async": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", + "integrity": "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==", + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/rw": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz", + "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==", + "license": "BSD-3-Clause", + "optional": true + }, + "node_modules/rxfire": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/rxfire/-/rxfire-6.0.3.tgz", + "integrity": "sha512-77nkyffHh7jgfi1YA/N9RI+kWxYpgKk6GRML1lyersvaqbJt4hkvWwk1rWib9Rb5Lr5mT+Ha45lu7nM79sJCZA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^1.9.0 || ~2.1.0" + }, + "peerDependencies": { + "firebase": "^9.0.0", + "rxjs": "^6.0.0 || ^7.0.0" + } + }, + "node_modules/rxfire/node_modules/tslib": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.1.0.tgz", + "integrity": "sha512-hcVC3wYEziELGGmEEXue7D75zbwIIVUMWAVbHItGPx0ziyXxrOMQx4rQEVEV45Ut/1IotuEvwqPopzIOkDMf0A==", + "license": "0BSD" + }, + "node_modules/rxjs": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-stable-stringify": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/safevalues": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/safevalues/-/safevalues-0.3.4.tgz", + "integrity": "sha512-LRneZZRXNgjzwG4bDQdOTSbze3fHm1EAKN/8bePxnlEZiBmkYEDggaHbuvHI9/hoqHbGfsEA7tWS9GhYHZBBsw==", + "license": "Apache-2.0" + }, + "node_modules/sass": { + "version": "1.58.1", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.58.1.tgz", + "integrity": "sha512-bnINi6nPXbP1XNRaranMFEBZWUfdW/AF16Ql5+ypRxfTvCRTTKrLsMIakyDcayUt2t/RZotmL4kgJwNH5xO+bg==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": ">=3.0.0 <4.0.0", + "immutable": "^4.0.0", + "source-map-js": ">=0.6.2 <2.0.0" + }, + "bin": { + "sass": "sass.js" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/sass-loader": { + "version": "13.2.0", + "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-13.2.0.tgz", + "integrity": "sha512-JWEp48djQA4nbZxmgC02/Wh0eroSUutulROUusYJO9P9zltRbNN80JCBHqRGzjd4cmZCa/r88xgfkjGD0TXsHg==", + "dev": true, + "license": "MIT", + "dependencies": { + "klona": "^2.0.4", + "neo-async": "^2.6.2" + }, + "engines": { + "node": ">= 14.15.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "fibers": ">= 3.1.0", + "node-sass": "^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0", + "sass": "^1.3.0", + "sass-embedded": "*", + "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "fibers": { + "optional": true + }, + "node-sass": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + } + } + }, + "node_modules/sax": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz", + "integrity": "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==", + "dev": true, + "license": "ISC", + "optional": true + }, + "node_modules/schema-utils": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.0.tgz", + "integrity": "sha512-Gf9qqc58SpCA/xdziiHz35F4GNIWYWZrEshUc/G/r5BnLph6xpKuLeoJoQuj5WfBIx/eQLf+hmVPYHaxJu7V2g==", + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/select": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/select/-/select-1.1.2.tgz", + "integrity": "sha512-OwpTSOfy6xSs1+pwcNrv0RBMOzI39Lp3qQKUTPVVPRjCdNa5JH/oPRiqsesIskK8TVgmRiHwO4KXlV2Li9dANA==", + "license": "MIT" + }, + "node_modules/select-hose": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz", + "integrity": "sha512-mEugaLK+YfkijB4fx0e6kImuJdCIt2LxCRcbEYPqRGCs4F2ogyfZU5IAZRdjCP8JPq2AtdNoC/Dux63d9Kiryg==", + "dev": true, + "license": "MIT" + }, + "node_modules/selfsigned": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-2.4.1.tgz", + "integrity": "sha512-th5B4L2U+eGLq1TVh7zNRGBapioSORUeymIydxgFpwww9d2qyKvtuPU2jJuHvYAwwqi2Y596QBL3eEqcPEYL8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node-forge": "^1.3.0", + "node-forge": "^1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/semver": { + "version": "7.5.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.3.tgz", + "integrity": "sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ==", + "license": "ISC", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/semver/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/semver/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC" + }, + "node_modules/send": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/send/node_modules/debug/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "license": "MIT" + }, + "node_modules/send/node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/send/node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/serialize-javascript": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", + "license": "BSD-3-Clause", + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/serve-index": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/serve-index/-/serve-index-1.9.1.tgz", + "integrity": "sha512-pXHfKNP4qujrtteMrSBb0rc8HJ9Ms/GrXwcUtUtD5s4ewDJI8bT3Cz2zTVRMKtri49pLx2e0Ya8ziP5Ya2pZZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "accepts": "~1.3.4", + "batch": "0.6.1", + "debug": "2.6.9", + "escape-html": "~1.0.3", + "http-errors": "~1.6.2", + "mime-types": "~2.1.17", + "parseurl": "~1.3.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/serve-index/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/serve-index/node_modules/depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/serve-index/node_modules/http-errors": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", + "integrity": "sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.0", + "statuses": ">= 1.4.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/serve-index/node_modules/inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==", + "dev": true, + "license": "ISC" + }, + "node_modules/serve-index/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "license": "MIT" + }, + "node_modules/serve-index/node_modules/setprototypeof": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", + "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/serve-static": { + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.19.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/serve-static/node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "dev": true, + "license": "ISC" + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "dev": true, + "license": "ISC" + }, + "node_modules/shallow-clone": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", + "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", + "dev": true, + "license": "MIT", + "dependencies": { + "kind-of": "^6.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "license": "ISC" + }, + "node_modules/simple-swizzle": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", + "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.3.1" + } + }, + "node_modules/simple-swizzle/node_modules/is-arrayish": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", + "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==", + "license": "MIT" + }, + "node_modules/slash": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-4.0.0.tgz", + "integrity": "sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socket.io": { + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.1.tgz", + "integrity": "sha512-oZ7iUCxph8WYRHHcjBEc9unw3adt5CmSNlppj/5Q4k2RIrhl8Z5yY2Xr4j9zj0+wzVZ0bxmYoGSzKJnRl6A4yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "accepts": "~1.3.4", + "base64id": "~2.0.0", + "cors": "~2.8.5", + "debug": "~4.3.2", + "engine.io": "~6.6.0", + "socket.io-adapter": "~2.5.2", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/socket.io-adapter": { + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.5.tgz", + "integrity": "sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "~4.3.4", + "ws": "~8.17.1" + } + }, + "node_modules/socket.io-adapter/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io-parser": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", + "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==", + "dev": true, + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-parser/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/sockjs": { + "version": "0.3.24", + "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.24.tgz", + "integrity": "sha512-GJgLTZ7vYb/JtPSSZ10hsOYIvEYsjbNU+zPdIHcUaWVNUEPivzxku31865sSSud0Da0W4lEeOPlmw93zLQchuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "faye-websocket": "^0.11.3", + "uuid": "^8.3.2", + "websocket-driver": "^0.7.4" + } + }, + "node_modules/sockjs/node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "dev": true, + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/socks": { + "version": "2.8.3", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.3.tgz", + "integrity": "sha512-l5x7VUUWbjVFbafGLxPWkYsHIhEvmF85tbIeFZWc8ZPtoMyybuEhL7Jye/ooC4/d48FgOjSJXgsF/AJPYCW8Zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "ip-address": "^9.0.5", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-7.0.0.tgz", + "integrity": "sha512-Fgl0YPZ902wEsAyiQ+idGd1A7rSFx/ayC1CQVMw5P+EQx2V0SgpGtf6OKFhVjPflPUl9YMmEOnmfjCdMUsygww==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^6.0.2", + "debug": "^4.3.3", + "socks": "^2.6.2" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/source-map": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", + "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", + "license": "BSD-3-Clause", + "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==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-loader": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/source-map-loader/-/source-map-loader-4.0.1.tgz", + "integrity": "sha512-oqXpzDIByKONVY8g1NUPOTQhe0UTU5bWUl32GSkqK2LjJj0HmwTMVKxcUip0RgAYhY1mqgOxjbQM48a0mmeNfA==", + "dev": true, + "license": "MIT", + "dependencies": { + "abab": "^2.0.6", + "iconv-lite": "^0.6.3", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": ">= 14.15.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.72.1" + } + }, + "node_modules/source-map-loader/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/source-map-support/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/spdx-correct": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", + "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-exceptions": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", + "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==", + "dev": true, + "license": "CC-BY-3.0" + }, + "node_modules/spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-license-ids": { + "version": "3.0.21", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.21.tgz", + "integrity": "sha512-Bvg/8F5XephndSK3JffaRqdT+gyhfqIPwDHpX80tJrF8QQRYMo8sNMeaZ2Dp5+jhwKnUmIOyFFQfHRkjJm5nXg==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/spdy": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/spdy/-/spdy-4.0.2.tgz", + "integrity": "sha512-r46gZQZQV+Kl9oItvl1JZZqJKGr+oEkB08A6BzkiR7593/7IbtuncXHd2YoYeTsG4157ZssMu9KYvUHLcjcDoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.0", + "handle-thing": "^2.0.0", + "http-deceiver": "^1.2.7", + "select-hose": "^2.0.0", + "spdy-transport": "^3.0.0" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/spdy-transport": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/spdy-transport/-/spdy-transport-3.0.0.tgz", + "integrity": "sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.0", + "detect-node": "^2.0.4", + "hpack.js": "^2.1.6", + "obuf": "^1.1.2", + "readable-stream": "^3.0.6", + "wbuf": "^1.7.3" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/ssri": { + "version": "10.0.6", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-10.0.6.tgz", + "integrity": "sha512-MGrFH9Z4NP9Iyhqn16sDtBpRRNJ0Y2hNa6D65h736fVSaPCHr4DM4sWUNvVaSuC+0OBGhwsrydQwmgfg5LncqQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/ssri/node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/stack-trace": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", + "integrity": "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/streamroller": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/streamroller/-/streamroller-3.1.5.tgz", + "integrity": "sha512-KFxaM7XT+irxvdqSP1LGLgNWbYN7ay5owZ3r/8t77p+EtSUAfUgtl7be3xtqtOmGUl9K9YPO2ca8133RlTjvKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "date-format": "^4.0.14", + "debug": "^4.3.4", + "fs-extra": "^8.1.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "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/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "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-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "min-indent": "^1.0.0" + }, + "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/stylis": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.5.tgz", + "integrity": "sha512-K7npNOKGRYuhAFFzkzMGfxFDpN6gDwf8hcMiE+uveTVbBgm93HrNP3ZDUpKqzZ4pG7TP6fmb+EMAQPjq9FqqvA==", + "license": "MIT", + "optional": true + }, + "node_modules/sucrase": { + "version": "3.35.0", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz", + "integrity": "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "glob": "^10.3.10", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/sucrase/node_modules/@jridgewell/gen-mapping": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", + "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/sucrase/node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/sucrase/node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": 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/sucrase/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/sucrase/node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "license": "ISC", + "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==", + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/symbol-observable": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-4.0.0.tgz", + "integrity": "sha512-b19dMThMV4HVFynSAM1++gBHAbk2Tc/osgLIBZMKsyqh34jb2e8Os7T6ZW/Bt3pJFdBTd2JwAnAAEQV7rSNvcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/synckit": { + "version": "0.8.8", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.8.8.tgz", + "integrity": "sha512-HwOKAP7Wc5aRGYdKH+dw0PRRpbO841v2DENBtjnR5HFWoiNByAl7vrx3p0G/rCyYXQsrxqtX48TImFtPcIHSpQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@pkgr/core": "^0.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/unts" + } + }, + "node_modules/tailwindcss": { + "version": "3.4.17", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz", + "integrity": "sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==", + "dev": true, + "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.6", + "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", + "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/node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "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/tailwindcss/node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/tailwindcss/node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/tailwindcss/node_modules/postcss": { + "version": "8.5.3", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz", + "integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.8", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/tailwindcss/node_modules/resolve": { + "version": "1.22.10", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", + "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.0", + "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/tapable": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", + "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/tar": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "dev": true, + "license": "ISC", + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/tar/node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/tar/node_modules/fs-minipass/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tar/node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=8" + } + }, + "node_modules/tar/node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true, + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/tar/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/terser": { + "version": "5.16.3", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.16.3.tgz", + "integrity": "sha512-v8wWLaS/xt3nE9dgKEWhNUFP6q4kngO5B8eYFUuebsu7Dw/UNAnpUod6UHo04jSSkv8TzKHjZDSd7EXdDQAl8Q==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@jridgewell/source-map": "^0.3.2", + "acorn": "^8.5.0", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser-webpack-plugin": { + "version": "5.3.11", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.11.tgz", + "integrity": "sha512-RVCsMfuD0+cTt3EwX8hSl2Ks56EbFHWmhluwcqoPKtBnfjiT6olaq7PRIRfhyU8nnC2MrnDrBLfrD/RGE+cVXQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "jest-worker": "^27.4.5", + "schema-utils": "^4.3.0", + "serialize-javascript": "^6.0.2", + "terser": "^5.31.1" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "uglify-js": { + "optional": true + } + } + }, + "node_modules/terser-webpack-plugin/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "license": "MIT" + }, + "node_modules/terser-webpack-plugin/node_modules/terser": { + "version": "5.37.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.37.0.tgz", + "integrity": "sha512-B8wRRkmre4ERucLM/uXx4MOV5cbnOlVAqUst+1+iLKPI0dOgFO28f84ptoQt9HEI537PMzfYa/d+GEPKTRXmYA==", + "license": "BSD-2-Clause", + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.8.2", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/test-exclude/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/test-exclude/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "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/test-exclude/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/text-hex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", + "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==", + "license": "MIT" + }, + "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==", + "dev": true, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", + "license": "MIT" + }, + "node_modules/thunky": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz", + "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tiny-emitter": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/tiny-emitter/-/tiny-emitter-2.1.0.tgz", + "integrity": "sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q==", + "license": "MIT" + }, + "node_modules/tmp": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "license": "MIT", + "dependencies": { + "os-tmpdir": "~1.0.2" + }, + "engines": { + "node": ">=0.6.0" + } + }, + "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==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "license": "MIT", + "bin": { + "tree-kill": "cli.js" + } + }, + "node_modules/trim-newlines": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-3.0.1.tgz", + "integrity": "sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/triple-beam": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.4.1.tgz", + "integrity": "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==", + "license": "MIT", + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/ts-dedent": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/ts-dedent/-/ts-dedent-2.2.0.tgz", + "integrity": "sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=6.10" + } + }, + "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==", + "dev": true, + "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/tsutils": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", + "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^1.8.1" + }, + "engines": { + "node": ">= 6" + }, + "peerDependencies": { + "typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta" + } + }, + "node_modules/tsutils/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true, + "license": "0BSD" + }, + "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.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typed-assert": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/typed-assert/-/typed-assert-1.0.9.tgz", + "integrity": "sha512-KNNZtayBCtmnNmbo5mG47p1XsCyrx6iVqomjcZnec/1Y5GGARaxPs6r49RnSPeUP3YjNYiU9sQHAtY4BBvnZwg==", + "dev": true, + "license": "MIT" + }, + "node_modules/typescript": { + "version": "4.9.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", + "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + }, + "node_modules/ua-parser-js": { + "version": "0.7.40", + "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.40.tgz", + "integrity": "sha512-us1E3K+3jJppDBa3Tl0L3MOJiGhe1C6P0+nIvQAFYbxlMAx0h81eOwLmU57xgqToduDDPx3y5QsdjPfDu+FgOQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/ua-parser-js" + }, + { + "type": "paypal", + "url": "https://paypal.me/faisalman" + }, + { + "type": "github", + "url": "https://github.com/sponsors/faisalman" + } + ], + "license": "MIT", + "bin": { + "ua-parser-js": "script/cli.js" + }, + "engines": { + "node": "*" + } + }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "license": "MIT" + }, + "node_modules/unicode-canonical-property-names-ecmascript": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.1.tgz", + "integrity": "sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-ecmascript": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz", + "integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "unicode-canonical-property-names-ecmascript": "^2.0.0", + "unicode-property-aliases-ecmascript": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-value-ecmascript": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.2.0.tgz", + "integrity": "sha512-4IehN3V/+kkr5YeSSDDQG8QLqO26XpL2XP3GQtqwlT/QYSECAwFztxVHjlbh0+gjJ3XmNLS0zDsbgs9jWKExLg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-property-aliases-ecmascript": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz", + "integrity": "sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/unique-filename": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-3.0.0.tgz", + "integrity": "sha512-afXhuC55wkAmZ0P18QsVE6kp8JaxrEokN2HGIoIVv2ijHQd419H0+6EigAFcIzXeMIkcIkNBpB3L/DXB3cTS/g==", + "dev": true, + "license": "ISC", + "dependencies": { + "unique-slug": "^4.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/unique-slug": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-4.0.0.tgz", + "integrity": "sha512-WrcA6AyEfqDX5bWige/4NQfPZMtASNVxdmWR76WESYQVAACSgWcR6e9i0mofqqBxYFtL4oAxPIptY73/0YE1DQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "license": "MIT", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.2.tgz", + "integrity": "sha512-PPypAm5qvlD7XMZC3BujecnaOxwhrtoFR+Dqkk5Aa/6DssiH0ibKoketaj9w8LP7Bont1rYeoV5plxD7RTEPRg==", + "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==", + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/uri-js/node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "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/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "node_modules/validate-npm-package-name": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-5.0.1.tgz", + "integrity": "sha512-OljLrQ9SQdOUqTaQxqL5dEfZWrXExyyWsozYlAWFawPVNuD83igl7uJD2RTkNMbniIYgt8l81eCJGIdQF7avLQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/void-elements": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-2.0.1.tgz", + "integrity": "sha512-qZKX4RnBzH2ugr8Lxa7x+0V6XD9Sb/ouARtiasEQCHB1EVU4NXtmHsDDrx1dO4ne5fc3J6EW05BP1Dl0z0iung==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/watchpack": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.2.tgz", + "integrity": "sha512-TnbFSbcOCcDgjZ4piURLCbJ3nJhznVh9kw6F6iokjiFPl8ONxe9A6nMDVXDiNbrSfLILs6vB07F7wLBrwPYzJw==", + "license": "MIT", + "dependencies": { + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/wbuf": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/wbuf/-/wbuf-1.7.3.tgz", + "integrity": "sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimalistic-assert": "^1.0.0" + } + }, + "node_modules/wcwidth": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", + "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==", + "license": "MIT", + "dependencies": { + "defaults": "^1.0.3" + } + }, + "node_modules/web-worker": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/web-worker/-/web-worker-1.5.0.tgz", + "integrity": "sha512-RiMReJrTAiA+mBjGONMnjVDP2u3p9R1vkcGz6gDIrOMT3oGuYwX2WRMYI9ipkphSuE5XKEhydbhNEJh4NY9mlw==", + "license": "Apache-2.0", + "optional": true + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/webpack": { + "version": "5.76.1", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.76.1.tgz", + "integrity": "sha512-4+YIK4Abzv8172/SGqObnUjaIHjLEuUasz9EwQj/9xmPPkYJy2Mh03Q/lJfSD3YLzbxy5FeTq5Uw0323Oh6SJQ==", + "license": "MIT", + "dependencies": { + "@types/eslint-scope": "^3.7.3", + "@types/estree": "^0.0.51", + "@webassemblyjs/ast": "1.11.1", + "@webassemblyjs/wasm-edit": "1.11.1", + "@webassemblyjs/wasm-parser": "1.11.1", + "acorn": "^8.7.1", + "acorn-import-assertions": "^1.7.6", + "browserslist": "^4.14.5", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.10.0", + "es-module-lexer": "^0.9.0", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.2.9", + "json-parse-even-better-errors": "^2.3.1", + "loader-runner": "^4.2.0", + "mime-types": "^2.1.27", + "neo-async": "^2.6.2", + "schema-utils": "^3.1.0", + "tapable": "^2.1.1", + "terser-webpack-plugin": "^5.1.3", + "watchpack": "^2.4.0", + "webpack-sources": "^3.2.3" + }, + "bin": { + "webpack": "bin/webpack.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-dev-middleware": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-6.1.2.tgz", + "integrity": "sha512-Wu+EHmX326YPYUpQLKmKbTyZZJIB8/n6R09pTmB03kJmnMsVPTo9COzHZFr01txwaCAuZvfBJE4ZCHRcKs5JaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "colorette": "^2.0.10", + "memfs": "^3.4.12", + "mime-types": "^2.1.31", + "range-parser": "^1.2.1", + "schema-utils": "^4.0.0" + }, + "engines": { + "node": ">= 14.15.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "webpack": { + "optional": true + } + } + }, + "node_modules/webpack-dev-server": { + "version": "4.11.1", + "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-4.11.1.tgz", + "integrity": "sha512-lILVz9tAUy1zGFwieuaQtYiadImb5M3d+H+L1zDYalYoDl0cksAB1UNyuE5MMWJrG6zR1tXkCP2fitl7yoUJiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/bonjour": "^3.5.9", + "@types/connect-history-api-fallback": "^1.3.5", + "@types/express": "^4.17.13", + "@types/serve-index": "^1.9.1", + "@types/serve-static": "^1.13.10", + "@types/sockjs": "^0.3.33", + "@types/ws": "^8.5.1", + "ansi-html-community": "^0.0.8", + "bonjour-service": "^1.0.11", + "chokidar": "^3.5.3", + "colorette": "^2.0.10", + "compression": "^1.7.4", + "connect-history-api-fallback": "^2.0.0", + "default-gateway": "^6.0.3", + "express": "^4.17.3", + "graceful-fs": "^4.2.6", + "html-entities": "^2.3.2", + "http-proxy-middleware": "^2.0.3", + "ipaddr.js": "^2.0.1", + "open": "^8.0.9", + "p-retry": "^4.5.0", + "rimraf": "^3.0.2", + "schema-utils": "^4.0.0", + "selfsigned": "^2.1.1", + "serve-index": "^1.9.1", + "sockjs": "^0.3.24", + "spdy": "^4.0.2", + "webpack-dev-middleware": "^5.3.1", + "ws": "^8.4.2" + }, + "bin": { + "webpack-dev-server": "bin/webpack-dev-server.js" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^4.37.0 || ^5.0.0" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-dev-server/node_modules/webpack-dev-middleware": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-5.3.4.tgz", + "integrity": "sha512-BVdTqhhs+0IfoeAf7EoH5WE+exCmqGerHfDM0IL096Px60Tq2Mn9MAbnaGUe6HiMa41KMCYF19gyzZmBcq/o4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "colorette": "^2.0.10", + "memfs": "^3.4.3", + "mime-types": "^2.1.31", + "range-parser": "^1.2.1", + "schema-utils": "^4.0.0" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^4.0.0 || ^5.0.0" + } + }, + "node_modules/webpack-merge": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.8.0.tgz", + "integrity": "sha512-/SaI7xY0831XwP6kzuwhKWVKDP9t1QY1h65lAFLbZqMPIuYcD9QAW4u9STIbU9kaJbPBB/geU/gLr1wDjOhQ+Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "clone-deep": "^4.0.1", + "wildcard": "^2.0.0" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/webpack-sources": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz", + "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==", + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webpack-subresource-integrity": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/webpack-subresource-integrity/-/webpack-subresource-integrity-5.1.0.tgz", + "integrity": "sha512-sacXoX+xd8r4WKsy9MvH/q/vBtEHr86cpImXwyg74pFIpERKt6FmB8cXpeuh0ZLgclOlHI4Wcll7+R5L02xk9Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "typed-assert": "^1.0.8" + }, + "engines": { + "node": ">= 12" + }, + "peerDependencies": { + "html-webpack-plugin": ">= 5.0.0-beta.1 < 6", + "webpack": "^5.12.0" + }, + "peerDependenciesMeta": { + "html-webpack-plugin": { + "optional": true + } + } + }, + "node_modules/webpack/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "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/webpack/node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "license": "MIT", + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "node_modules/webpack/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==", + "license": "MIT" + }, + "node_modules/webpack/node_modules/schema-utils": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/websocket-driver": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", + "integrity": "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==", + "license": "Apache-2.0", + "dependencies": { + "http-parser-js": ">=0.5.1", + "safe-buffer": ">=5.1.0", + "websocket-extensions": ">=0.1.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/websocket-extensions": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz", + "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, + "node_modules/wide-align": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", + "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^1.0.2 || 2 || 3 || 4" + } + }, + "node_modules/wildcard": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.1.tgz", + "integrity": "sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/winston": { + "version": "3.17.0", + "resolved": "https://registry.npmjs.org/winston/-/winston-3.17.0.tgz", + "integrity": "sha512-DLiFIXYC5fMPxaRg832S6F5mJYvePtmO5G9v9IgUFPhXm9/GkXarH/TUrBAVzhTCzAj9anE/+GjrgXp/54nOgw==", + "license": "MIT", + "dependencies": { + "@colors/colors": "^1.6.0", + "@dabh/diagnostics": "^2.0.2", + "async": "^3.2.3", + "is-stream": "^2.0.0", + "logform": "^2.7.0", + "one-time": "^1.0.0", + "readable-stream": "^3.4.0", + "safe-stable-stringify": "^2.3.1", + "stack-trace": "0.0.x", + "triple-beam": "^1.3.0", + "winston-transport": "^4.9.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/winston-transport": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.9.0.tgz", + "integrity": "sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A==", + "license": "MIT", + "dependencies": { + "logform": "^2.7.0", + "readable-stream": "^3.6.2", + "triple-beam": "^1.3.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/winston/node_modules/@colors/colors": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.6.0.tgz", + "integrity": "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==", + "license": "MIT", + "engines": { + "node": ">=0.1.90" + } + }, + "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/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "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/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/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/write-file-atomic": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", + "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.7" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/ws": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 6" + } + }, + "node_modules/yargs": { + "version": "17.6.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.6.2.tgz", + "integrity": "sha512-1/9UrdHjDZc0eOU0HxOHoS78C69UD3JRMvzlJ7S79S2nTaWRA/whGCTV8o9e/N/1Va9YIV7Q4sOxD8VV4pCWOw==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zone.js": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.12.0.tgz", + "integrity": "sha512-XtC+I5dXU14HrzidAKBNMqneIVUykLEAA1x+v4KVrd6AUPWlwYORF8KgsVqvgdHiKZ4BkxxjvYi/ksEixTPR0Q==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + } + } + } +} diff --git a/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/package.json b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/package.json new file mode 100644 index 0000000..a3c94a4 --- /dev/null +++ b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/package.json @@ -0,0 +1,79 @@ +{ + "name": "quick-bot-app-frontend", + "version": "0.0.0", + "scripts": { + "ng": "ng", + "start": "ng serve", + "build:prod": "ng build --configuration production --aot --output-hashing=all", + "build:dev": "ng build --configuration development", + "build:staging": "ng build --configuration staging", + "build": "ng build", + "watch": "ng build --watch --configuration development", + "test": "ng test", + "postinstall": "ngcc", + "post-build": "node ./build/post-build.js", + "lint": "gts lint", + "clean": "gts clean", + "compile": "tsc", + "fix": "gts fix", + "prepare": "npm run compile", + "pretest": "npm run compile", + "posttest": "npm run lint", + "format": "prettier --write \"src/**/*.{ts,html,scss,json}\"", + "check-format": "prettier --check \"src/**/*.{ts,html,scss,json}\"" + }, + "private": true, + "dependencies": { + "@angular/animations": "^15.1.0", + "@angular/cdk": "^15.2.9", + "@angular/common": "^15.1.0", + "@angular/compiler": "^15.1.0", + "@angular/core": "^15.1.0", + "@angular/fire": "7.5", + "@angular/flex-layout": "^15.0.0-beta.42", + "@angular/forms": "^15.1.0", + "@angular/material": "^15.2.9", + "@angular/platform-browser": "^15.1.0", + "@angular/platform-browser-dynamic": "^15.1.0", + "@angular/router": "^15.1.0", + "@fortawesome/fontawesome-free": "^6.6.0", + "@ionic/angular": "^7.5.4", + "@ng-idle/core": "^14.0.0", + "clipboard": "^2.0.11", + "ionic-emoji-rating": "^1.0.15", + "marked": "^4.3.0", + "moment": "^2.30.1", + "ng2-pdf-viewer": "^7.0.2", + "ngx-markdown": "^15.1.2", + "prismjs": "^1.29.0", + "rxfire": "6.0.3", + "rxjs": "~7.8.0", + "tslib": "^2.3.0", + "uuid": "^9.0.1", + "zone.js": "~0.12.0" + }, + "devDependencies": { + "@angular-devkit/build-angular": "^15.1.3", + "@angular/cli": "~15.1.3", + "@angular/compiler-cli": "^15.1.0", + "@types/jasmine": "~4.3.0", + "@types/marked": "^4.3.0", + "@types/node": "20.12.7", + "@types/uuid": "^9.0.4", + "firebase-tools": "^11.30.0", + "gts": "^5.3.1", + "jasmine-core": "~4.5.0", + "karma": "~6.4.0", + "karma-chrome-launcher": "~3.1.0", + "karma-coverage": "~2.2.0", + "karma-jasmine": "~5.1.0", + "karma-jasmine-html-reporter": "~2.0.0", + "prettier": "^3.5.1", + "tailwindcss": "^3.4.17", + "typescript": "~4.9.4" + }, + "description": "This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 15.1.3.", + "main": "index.js", + "author": "", + "license": "ISC" +} diff --git a/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/postcss.config.js b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/postcss.config.js new file mode 100644 index 0000000..30a0865 --- /dev/null +++ b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/postcss.config.js @@ -0,0 +1,22 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +module.exports = { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; diff --git a/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/app/app-routing.module.ts b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/app/app-routing.module.ts new file mode 100644 index 0000000..9a379d6 --- /dev/null +++ b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/app/app-routing.module.ts @@ -0,0 +1,34 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {NgModule} from '@angular/core'; +import {RouterModule, Routes} from '@angular/router'; +import {MainComponent} from './components/main/main.component'; +import {LoginComponent} from './components/login/login.component'; +import {AuthGuard} from './services/login/auth.guard'; +import {SearchResultsComponent} from './components/main/search-results/search-results.component'; + +const routes: Routes = [ + {path: '', component: MainComponent, canActivate: [AuthGuard]}, + {path: 'login', component: LoginComponent}, + {path: 'search', component: SearchResultsComponent, canActivate: [AuthGuard]}, +]; + +@NgModule({ + imports: [RouterModule.forRoot(routes, {onSameUrlNavigation: 'reload'})], + exports: [RouterModule], +}) +export class AppRoutingModule {} diff --git a/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/app/app.component.html b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/app/app.component.html new file mode 100644 index 0000000..be3bd01 --- /dev/null +++ b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/app/app.component.html @@ -0,0 +1,34 @@ + + + + +
    + +
    + + diff --git a/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/app/app.component.scss b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/app/app.component.scss new file mode 100644 index 0000000..6bc6422 --- /dev/null +++ b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/app/app.component.scss @@ -0,0 +1,142 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +::ng-deep .tour-step { + background: white; +} + +::ng-deep .tour-top { + margin: -1%; +} + +::ng-deep .tour-left { + margin-left: -1%; +} + +::ng-deep .tour-top-right { + margin-top: -1%; +} + +::ng-deep .tour-bottom-right { + margin-top: 1%; +} + +::ng-deep .tour-bottom-left { + margin-top: 1%; +} + +::ng-deep .tour-bottom { + margin: 1%; +} + +::ng-deep ngx-guided-tour .tour-content { + font-size: 17px !important; + padding-bottom: 20px !important; +} + +::ng-deep ngx-guided-tour .tour-title { + font-size: 22px !important; + padding-bottom: 0 !important; +} + +::ng-deep ngx-guided-tour .tour-step .tour-buttons .skip-button { + font-weight: 500 !important; + font-size: 17px !important; + line-height: 1.7 !important; +} + +::ng-deep ngx-guided-tour .tour-step { + border-radius: 20px !important; +} + +::ng-deep ngx-guided-tour .tour-step .tour-buttons .back-button { + font-weight: 500 !important; + font-size: 17px !important; + line-height: 1.7 !important; +} + +::ng-deep ngx-guided-tour .tour-step .tour-buttons .next-button { + background: #4285f4; + padding-left: 16px !important; + padding-right: 16px !important; + padding-top: 6px !important; + padding-bottom: 6px !important; + font-size: 16px !important; + color: white; + font-weight: 500; + border-radius: 15px !important; +} + +::ng-deep ngx-guided-tour .tour-step.tour-top .tour-block { + margin-bottom: 0 !important; +} + +::ng-deep ngx-guided-tour .tour-step.tour-bottom-left .tour-block { + margin-top: 0 !important; +} + +::ng-deep pre { + background: #485f84 !important; +} + +div { + font-family: 'Google Sans', sans-serif !important; +} + +.video-embed-player { + position: fixed; + top: 20vh; + left: -50vw; + z-index: -10000; + overflow: hidden; + width: 200vw; +} + +// Small devices (landscape phones, 576px and up) +@media (min-width: 576px) { +} + +// Medium devices (tablets, 768px and up) +@media (min-width: 768px) { + .video-embed-player { + top: 20vh; + width: 150vw; + left: -25vw; + } +} + +// Large devices (desktops, 992px and up) +@media (min-width: 992px) { + .video-embed-player { + top: 10vh; + } +} + +// X-Large devices (large desktops, 1200px and up) +@media (min-width: 1200px) { + .video-embed-player { + left: 0; + width: 100vw; + } +} + +// XX-Large devices (larger desktops, 1400px and up) +@media (min-width: 1400px) { + .video-embed-player { + top: -10vw; + width: 110vw; + } +} diff --git a/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/app/app.component.spec.ts b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/app/app.component.spec.ts new file mode 100644 index 0000000..9ff3178 --- /dev/null +++ b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/app/app.component.spec.ts @@ -0,0 +1,49 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {TestBed} from '@angular/core/testing'; +import {RouterTestingModule} from '@angular/router/testing'; +import {AppComponent} from './app.component'; + +describe('AppComponent', () => { + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [RouterTestingModule], + declarations: [AppComponent], + }).compileComponents(); + }); + + it('should create the app', () => { + const fixture = TestBed.createComponent(AppComponent); + const app = fixture.componentInstance; + expect(app).toBeTruthy(); + }); + + it("should have as title 'pac-assist-ui'", () => { + const fixture = TestBed.createComponent(AppComponent); + const app = fixture.componentInstance; + expect(app.title).toEqual('pac-assist-ui'); + }); + + it('should render title', () => { + const fixture = TestBed.createComponent(AppComponent); + fixture.detectChanges(); + const compiled = fixture.nativeElement as HTMLElement; + expect(compiled.querySelector('.content span')?.textContent).toContain( + 'pac-assist-ui app is running!' + ); + }); +}); diff --git a/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/app/app.component.ts b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/app/app.component.ts new file mode 100644 index 0000000..a05a5ba --- /dev/null +++ b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/app/app.component.ts @@ -0,0 +1,67 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {Component} from '@angular/core'; +import {Router, NavigationEnd, Event as NavigationEvent} from '@angular/router'; +import {UserService} from './services/user/user.service'; +import {AuthService} from './services/login/auth.service'; + +@Component({ + selector: 'app-root', + templateUrl: './app.component.html', + styleUrls: ['./app.component.scss'], +}) +export class AppComponent { + title = 'pac-assist-ui'; + showHeader = true; + userInfo: any; + + showLoading = false; + showVideo = true; + + constructor( + private router: Router, + private userService: UserService, + public authService: AuthService + ) { + this.router.events.subscribe((event: NavigationEvent) => { + if (event instanceof NavigationEnd) { + if (event.url === '/') this.showVideo = true; + + if ( + event.url === '/login' || + event.url === '/login/e2e' || + (event.url.includes('login') && event.url.includes('email')) || + (event.url.includes('login') && event.url.includes('tos')) || + event.url.includes('reset-password') || + event.url.includes('support-ticket') + ) { + this.showHeader = false; + } else { + this.userInfo = this.userService.getUserDetails(); + this.showHeader = true; + } + } + }); + } + + ngOnInit(): void { + this.userService.loadingSubject.subscribe(loadingValue => { + this.showLoading = loadingValue; + this.showVideo = loadingValue; + }); + } +} diff --git a/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/app/app.module.ts b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/app/app.module.ts new file mode 100644 index 0000000..196fa43 --- /dev/null +++ b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/app/app.module.ts @@ -0,0 +1,165 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {NgModule} from '@angular/core'; +import {BrowserModule} from '@angular/platform-browser'; +import {AppRoutingModule} from './app-routing.module'; +import {AppComponent} from './app.component'; +import {HeaderComponent} from './components/header/header.component'; +import {FooterComponent} from './components/footer/footer.component'; +import {MainComponent} from './components/main/main.component'; +import {BrowserAnimationsModule} from '@angular/platform-browser/animations'; +import {MatIconModule} from '@angular/material/icon'; +import {MatAutocompleteModule} from '@angular/material/autocomplete'; +import {MatButtonModule} from '@angular/material/button'; +import {MatToolbarModule} from '@angular/material/toolbar'; +import {HttpClientModule} from '@angular/common/http'; +import {MatInputModule} from '@angular/material/input'; +import {NgFor, PathLocationStrategy} from '@angular/common'; +import {MatSelectModule} from '@angular/material/select'; +import {MatFormFieldModule} from '@angular/material/form-field'; +import {FormsModule, ReactiveFormsModule} from '@angular/forms'; +import {MatGridListModule} from '@angular/material/grid-list'; +import {MatCardModule} from '@angular/material/card'; +import {MatProgressSpinnerModule} from '@angular/material/progress-spinner'; +import {MatTooltipModule} from '@angular/material/tooltip'; +import {LoginComponent} from './components/login/login.component'; +import {initializeApp, provideFirebaseApp} from '@angular/fire/app'; +import { + provideAnalytics, + getAnalytics, + ScreenTrackingService, + UserTrackingService, +} from '@angular/fire/analytics'; +import {environment} from '../environments/environment'; +import {provideAuth, getAuth} from '@angular/fire/auth'; +import {MatMenuModule} from '@angular/material/menu'; +import {MatDividerModule} from '@angular/material/divider'; +import {MatChipsModule} from '@angular/material/chips'; +import {LocationStrategy} from '@angular/common'; +import {MarkdownModule} from 'ngx-markdown'; +import {MatDialogModule} from '@angular/material/dialog'; +import {CdkAccordionModule} from '@angular/cdk/accordion'; +import {MatExpansionModule} from '@angular/material/expansion'; +import {IonicRatingModule} from 'ionic-emoji-rating'; +import {MatListModule} from '@angular/material/list'; +import {MatTabsModule} from '@angular/material/tabs'; +import {MatTableModule} from '@angular/material/table'; +import {MatCheckboxModule} from '@angular/material/checkbox'; +import {MatSlideToggleModule} from '@angular/material/slide-toggle'; +import {MatButtonToggleModule} from '@angular/material/button-toggle'; +import {MatSnackBarModule} from '@angular/material/snack-bar'; +import {MatSortModule} from '@angular/material/sort'; +import {MatPaginatorModule} from '@angular/material/paginator'; +import {MatSidenavModule} from '@angular/material/sidenav'; +import {NgIdleModule} from '@ng-idle/core'; +import {ClipboardModule} from '@angular/cdk/clipboard'; +import {MatProgressBarModule} from '@angular/material/progress-bar'; +import {PdfViewerModule} from 'ng2-pdf-viewer'; + +import 'prismjs'; +import 'prismjs/components/prism-typescript.min.js'; +import 'prismjs/plugins/line-numbers/prism-line-numbers.js'; +import 'prismjs/plugins/line-highlight/prism-line-highlight.js'; +import {FlexLayoutModule} from '@angular/flex-layout'; +import {MatSliderModule} from '@angular/material/slider'; +import {MatStepperModule} from '@angular/material/stepper'; +import {ChatInputComponent} from './components/main/chat-input/chat-input.component'; +import {SearchResultsComponent} from './components/main/search-results/search-results.component'; +import {ToastMessageComponent} from './components/toast-message/toast-message.component'; +import {TruncatePipe} from './pipes/truncate.pipe'; +import {BackgroundChangerInputComponent} from './components/background-changer-input/background-changer-input.component'; + +@NgModule({ + declarations: [ + AppComponent, + HeaderComponent, + FooterComponent, + MainComponent, + LoginComponent, + ChatInputComponent, + SearchResultsComponent, + ToastMessageComponent, + TruncatePipe, + BackgroundChangerInputComponent, + ], + imports: [ + BrowserModule, + AppRoutingModule, + BrowserAnimationsModule, + MatSnackBarModule, + MatPaginatorModule, + MatSortModule, + MatSliderModule, + MatToolbarModule, + MatButtonModule, + MatIconModule, + HttpClientModule, + FormsModule, + MatFormFieldModule, + MatSelectModule, + NgFor, + MatInputModule, + MatGridListModule, + MatCardModule, + MatTooltipModule, + MatProgressSpinnerModule, + MatMenuModule, + MatDividerModule, + MatChipsModule, + ReactiveFormsModule, + MatMenuModule, + MatDialogModule, + CdkAccordionModule, + IonicRatingModule, + MarkdownModule.forRoot(), + MatExpansionModule, + MatListModule, + MatTabsModule, + MatCheckboxModule, + MatTableModule, + MatSlideToggleModule, + MatButtonToggleModule, + MatSidenavModule, + MatAutocompleteModule, + environment.requiredLogin === 'True' + ? [ + provideFirebaseApp(() => initializeApp(environment.firebase)), + provideAuth(() => getAuth()), + ] + : [], + environment.requiredLogin === 'True' + ? [provideAnalytics(() => getAnalytics())] + : [], + FlexLayoutModule, + NgIdleModule.forRoot(), + ClipboardModule, + MatStepperModule, + MatProgressBarModule, + PdfViewerModule, + ], + providers: [ + {provide: LocationStrategy, useClass: PathLocationStrategy}, + environment.requiredLogin === 'True' + ? [ + ScreenTrackingService, // Automatically track screen views + UserTrackingService, // Automatically track user interactions + ] + : [], + ], + bootstrap: [AppComponent], +}) +export class AppModule {} diff --git a/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/app/components/background-changer-input/background-changer-input.component.html b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/app/components/background-changer-input/background-changer-input.component.html new file mode 100644 index 0000000..d9550ba --- /dev/null +++ b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/app/components/background-changer-input/background-changer-input.component.html @@ -0,0 +1,51 @@ + + + + +
    + + + + {{ selectedFile.name }} + + No image selected +
    + +
    + Image Preview +
    +
    + + + +
    diff --git a/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/app/components/background-changer-input/background-changer-input.component.scss b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/app/components/background-changer-input/background-changer-input.component.scss new file mode 100644 index 0000000..16d001f --- /dev/null +++ b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/app/components/background-changer-input/background-changer-input.component.scss @@ -0,0 +1,80 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +.file-input-wrapper { + margin-bottom: 20px; + display: flex; + flex-direction: column; + align-items: center; + gap: 10px; + + .placeholder { + color: rgba(0, 0, 0, 0.54); // Material hint text color + } + + .file-name { + font-style: italic; + color: rgba(0, 0, 0, 0.87); + } +} + +.image-preview { + margin-top: 20px; + border: 1px solid #ccc; + border-radius: 4px; + padding: 5px; + max-width: 100%; + display: flex; + justify-content: center; + + img { + max-width: 100%; + max-height: 300px; // Adjust as needed + display: block; + } +} + +.card-input-wrapper { + // min-width: 40vw; + width: 80%; + min-height: 10rem; + display: flex; + justify-content: center; + flex-wrap: wrap; + flex-direction: column; + align-items: center; + text-align: center; + border: 4px dashed lightgrey; + margin: 2rem; + padding: 1rem; + border-radius: 4px; + cursor: pointer; + transition: border 1s; + text-decoration: none; + + &:hover { + border-color: rgba(66, 133, 244, 0.95); + } +} + +.centered-mat-card { + display: flex; + flex-direction: column; + align-items: center; + align-self: center; + justify-self: center; + width: 40vw; +} diff --git a/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/app/components/background-changer-input/background-changer-input.component.spec.ts b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/app/components/background-changer-input/background-changer-input.component.spec.ts new file mode 100644 index 0000000..6d89124 --- /dev/null +++ b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/app/components/background-changer-input/background-changer-input.component.spec.ts @@ -0,0 +1,38 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {ComponentFixture, TestBed} from '@angular/core/testing'; + +import {BackgroundChangerInputComponent} from './background-changer-input.component'; + +describe('BackgroundChangerComponent', () => { + let component: BackgroundChangerInputComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [BackgroundChangerInputComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(BackgroundChangerInputComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/app/components/background-changer-input/background-changer-input.component.ts b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/app/components/background-changer-input/background-changer-input.component.ts new file mode 100644 index 0000000..608f408 --- /dev/null +++ b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/app/components/background-changer-input/background-changer-input.component.ts @@ -0,0 +1,62 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + Component, + ViewChild, + ElementRef, + Output, + EventEmitter, +} from '@angular/core'; + +@Component({ + selector: 'app-background-changer-input', + templateUrl: './background-changer-input.component.html', + styleUrls: ['./background-changer-input.component.scss'], +}) +export class BackgroundChangerInputComponent { + @ViewChild('fileInput') fileInput!: ElementRef; + imageUrl: string | ArrayBuffer | null = null; + selectedFile: File | null = null; + + @Output() emitSearch: EventEmitter = new EventEmitter(); + + constructor() {} + + onFileSelected(event: any): void { + const file: File = event.target.files[0]; + + if (file) { + this.selectedFile = file; + const reader = new FileReader(); + reader.onload = (e: any) => { + this.imageUrl = e.target.result; + }; + reader.readAsDataURL(file); + } else { + this.imageUrl = null; + this.selectedFile = null; + } + } + + onSubmit(): void { + if (this.selectedFile) { + this.emitSearch.emit(this.selectedFile); // Emit the file + } else { + console.log('No image selected to submit.'); + } + } +} diff --git a/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/app/components/footer/footer.component.html b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/app/components/footer/footer.component.html new file mode 100644 index 0000000..e649379 --- /dev/null +++ b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/app/components/footer/footer.component.html @@ -0,0 +1,25 @@ + + +

    + +

    + +

    diff --git a/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/app/components/footer/footer.component.scss b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/app/components/footer/footer.component.scss new file mode 100644 index 0000000..b405613 --- /dev/null +++ b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/app/components/footer/footer.component.scss @@ -0,0 +1,76 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +.container { + padding-left: 4rem; + padding-right: 4rem; +} + +.footer { + position: fixed; + bottom: 0; +} + +.genAI { + background: linear-gradient( + 86.95deg, + #34a853 -65.34%, + #fbbc05 -12.75%, + #ea4335 25.15%, + #1a73e8 103.88% + ); + -webkit-background-clip: text; + -moz-background-clip: text; + background-clip: text; + color: transparent; +} + +:host ::ng-deep .mat-toolbar.mat-primary { + background-color: transparent; + color: #504747; + box-shadow: none; + justify-content: center; +} + +.line-spacer { + flex: 1 1 auto; +} + +.links-weight { + margin-left: 10px; + margin-right: 10px; + font-size: 14px; + font-weight: 400; + cursor: pointer; +} + +.links-bold { + margin-right: 10px; + color: #5f6368; + font-family: 'Google Sans', sans-serif !important; + .blue { + color: #4285f4; + } + .red { + color: #ea4335; + } + .yellow { + color: #fbbc04; + } + .green { + color: #34a853; + } +} diff --git a/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/app/components/footer/footer.component.spec.ts b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/app/components/footer/footer.component.spec.ts new file mode 100644 index 0000000..420323c --- /dev/null +++ b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/app/components/footer/footer.component.spec.ts @@ -0,0 +1,38 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {ComponentFixture, TestBed} from '@angular/core/testing'; + +import {FooterComponent} from './footer.component'; + +describe('FooterComponent', () => { + let component: FooterComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [FooterComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(FooterComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/app/components/footer/footer.component.ts b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/app/components/footer/footer.component.ts new file mode 100644 index 0000000..a38bb6c --- /dev/null +++ b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/app/components/footer/footer.component.ts @@ -0,0 +1,24 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {Component} from '@angular/core'; + +@Component({ + selector: 'app-footer', + templateUrl: './footer.component.html', + styleUrls: ['./footer.component.scss'], +}) +export class FooterComponent {} diff --git a/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/app/components/header/header.component.html b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/app/components/header/header.component.html new file mode 100644 index 0000000..ba7cc36 --- /dev/null +++ b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/app/components/header/header.component.html @@ -0,0 +1,105 @@ + + + + diff --git a/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/app/components/header/header.component.scss b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/app/components/header/header.component.scss new file mode 100644 index 0000000..f2913bd --- /dev/null +++ b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/app/components/header/header.component.scss @@ -0,0 +1,643 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +.container { + margin: auto; +} + +.logo-text { + font-family: + Google Sans, + Helvetica Neue, + sans-serif; + font-style: normal; + font-size: 22px; + line-height: 128.7%; + letter-spacing: normal; + color: #2d2c2cbf; +} + +.gradient-text { + font-family: + Google Sans, + Helvetica Neue, + sans-serif; + font-style: normal; + font-size: 30px; + line-height: 128.7%; + letter-spacing: normal; + background: linear-gradient( + 89.9deg, + rgba(66, 133, 244, 0.95) 21.17%, + #a488f5 44.34%, + rgba(234, 67, 53, 0.88) 81.26% + ); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; + text-fill-color: transparent; +} + +.links-pointer { + cursor: pointer; +} + +.logo-icon { + height: 35px; + width: 125px; + cursor: pointer; +} + +// .profile-container { +// margin-left: 1%; +// font-family: 'Google Sans', sans-serif !important; + +// .circle { +// display: flex; +// justify-content: center; +// width: 40px; +// height: 40px; +// border: 5px solid rgb(255, 255, 255); +// border-radius: 60%; +// overflow: hidden; +// } + +// .circle img { +// width: 100%; +// height: 100%; +// object-fit: cover; +// } +// } + +.card-inside-right { + display: flex; + flex-direction: column; + align-items: flex-start; + color: #3e4245; + font-size: 1rem; + font-weight: 400; + font-family: 'Google Sans', sans-serif !important; + border-radius: 5px; + letter-spacing: 0.4px; + + .container-circle { + width: 100%; + height: 100%; + object-fit: cover; + border-radius: 50px; + padding-top: 4%; + padding-bottom: 4%; + } + + .name { + display: flex; + align-items: center; + font-size: 1rem; + color: #3e4245; + font-weight: 500; + padding: 1.3em 0.4em; + flex-direction: column; + gap: 10px; + background: #dde3ea !important; + width: -webkit-fill-available; + .sub-title { + color: #3e4245; + } + + margin: auto; + } + + .company { + display: flex; + align-items: center; + padding: 0.3em 1em; + width: -webkit-fill-available; + + .logo { + padding-right: 2%; + color: #5f6368; + } + + .text { + min-width: 100px; + + .sub-title { + font-size: 0.85rem; + margin-bottom: 10px; + color: #3e4245; + } + } + + margin-bottom: 10px; + } + + .logout { + background: #dde3ea; + border-radius: 20px; + font-size: 1rem; + font-weight: 500; + font-family: 'Google Sans', sans-serif !important; + margin-bottom: 1em; + cursor: pointer; + width: -webkit-fill-available; + padding: 0.3em 1em; + + .logout-container { + display: flex; + align-items: center; + + .logo { + padding-right: 4%; + color: #5f6368; + } + + .text { + .sub-title { + color: #3e4245; + font-size: 0.85rem; + font-family: 'Google Sans', sans-serif !important; + } + } + } + } +} + +//------------css of card end-------------------// + +.image { + width: 35px; + height: 35px; +} + +.fixed-top { + z-index: 1000; + position: relative; +} + +.top-nav { + height: 60px; + background-color: transparent; + + .parent-container { + display: flex; + justify-content: space-between; + align-items: center; + padding: 1rem 3rem; + + .left-heading { + display: flex; + align-items: center; + cursor: pointer; + } + + .logo-icon { + height: 4.2em; + width: 4em; + cursor: pointer; + background-image: url('../../../assets/images/new-pdc-logo.svg'); + background-size: 443px; + background-repeat: round; + border-radius: 28px; + margin-right: 1em; + } + + .user-icon { + height: 50px; + width: 50px; + cursor: pointer; + border-radius: 28px; + } + + .right-menu-items { + display: flex; + align-items: center; + + .profile-container { + margin-left: 1%; + font-family: 'Google Sans', sans-serif !important; + + .circle { + display: flex; + justify-content: center; + width: 35px; + height: 35px; + border-radius: 60%; + overflow: hidden; + } + + .circle img { + width: 100%; + height: 100%; + object-fit: cover; + } + } + } + } +} + +.icon-color { + color: #5f6368; + font-size: 24px; +} + +::ng-deep .quick-link.mat-mdc-menu-panel.mat-mdc-menu-panel { + width: 420px !important; + display: flex; + flex-direction: column; + background: #f0f4f9; + + .mat-mdc-menu-content { + padding: 0.5rem; + } + + .ql-container { + width: 100%; + display: flex; + flex-direction: column; + + .ql-child-container { + display: flex; + flex-direction: row; + text-wrap: nowrap; + justify-content: space-between; + align-items: flex-end; + } + + a, + a:active { + text-decoration: none; + } + + .align-link-content { + display: flex; + flex-direction: row; + align-items: baseline; + gap: 10px; + width: 100% !important; + box-shadow: 0px 4px 4px -6px #b9b8b8 !important; + } + + .no-box-shadow { + box-shadow: none !important; + } + + .icon-container { + display: flex; + alignment-baseline: after-edge; + + mat-icon { + color: #161a1d !important; + } + } + } + + .ql-header { + margin: 1rem; + margin-left: 1.7rem; + } + + .avatar { + width: 40px; + height: 40px; //<--use the size you choose + border-radius: 100%; + text-align: center; + } + + div.link-label { + color: #161a1d; + font-size: 16px; + font-style: normal; + font-weight: 400; + line-height: 24px; + letter-spacing: 0.4px; + height: 50px; + margin: 0px 5px; + width: 57%; + } + + .avatar span { + line-height: 40px; + font-size: 16px; + } + + .link-button { + margin: 1rem; + height: 41px; + width: 100% !important; + } +} + +.notification-banner-container { + z-index: 999; + position: relative; + width: inherit; + display: flex; + flex-direction: row; + height: 8%; + background: #f6f9fe; + align-content: center; + flex-wrap: wrap; + padding-left: 1%; + gap: 1%; + margin-bottom: 0.2%; + + .amber { + color: #fbc645; + } + + .red { + color: red; + } + + .blue { + color: #4285f4; + } +} + +.red-border { + border-left: 5px solid red; +} + +.amber-border { + border-left: 5px solid #fbc645; +} + +.blue-border { + border-left: 5px solid #4285f4; +} + +.notification-text { + display: flex; + flex-direction: row; + align-items: center; + gap: 15px; + width: 70%; +} + +.notification-buttons-container { + width: 26%; + justify-content: flex-end; + display: flex; + flex-direction: row; + gap: 15px; + + button { + font-family: 'Google Sans', sans-serif !important; + font-style: normal; + font-weight: 500; + line-height: 23px; + font-size: 14px; + } + + .dismiss-button { + letter-spacing: 0.4px; + color: #4285f4; + } + + .learn-more-button { + color: #fff !important; + background: #4285f4 !important; + border-radius: 30px; + letter-spacing: unset; + box-shadow: none; + } +} + +::ng-deep .profile-menu.mat-mdc-menu-panel.mat-mdc-menu-panel { + max-width: 320px !important; + width: 300px !important; +} + +::ng-deep .profile-menu .mat-mdc-menu-content { + padding: 0% !important; +} + +.action-item-container { + width: -webkit-fill-available; + padding-inline: 5%; + padding-top: 2%; + padding-bottom: 2%; +} + +.logout-button { + background: white !important; + border: 1px solid #4285f4 !important; +} + +.logout-button-text { + color: #4285f4 !important; +} + +.app-version-chip { + font-size: 9px; + border-radius: -2px; + padding-inline: 8px; + line-height: 16px; + border-radius: 11px; + margin-left: 8px; + color: #474c55; + background: + linear-gradient( + 92.1deg, + #bdd1f3 19.16%, + rgba(199, 183, 245, 0.9) 79%, + rgba(235, 165, 158, 0.9) 135.24% + ), + linear-gradient( + 272.04deg, + rgba(158, 188, 237, 0.7) 14.93%, + rgba(158, 188, 237, 0) 104.51% + ) !important; +} + +.bypassLoginLogo { + background: aliceblue; + color: cornflowerblue; + align-items: center; + cursor: pointer; +} + +.bypassMenuContainer { + display: flex; + justify-content: center; + background: aliceblue; + width: 42% !important; + height: 40% !important; + border-radius: 65px !important; +} + +.bypassMenuLogo { + font-size: 6rem; + height: 6rem; + width: 6rem; +} + +.chat-footer { + min-height: 60px; + display: flex; + border: 1px solid #dde6f6; + box-shadow: 0px 4px 18px rgba(195, 209, 226, 0.25); + border-radius: 30px; + width: 66%; + margin-top: 25px; + // box-shadow: 5px 4px 24px rgba(66, 133, 244, 0.15); + + .chat-textarea { + flex-grow: 1; + display: flex; + align-items: center; + + input { + width: 100%; + height: 45px; + border: 0px; + border-radius: 4px; + resize: none; + outline: none; + padding: 14px; + font-family: 'Google Sans', sans-serif !important; + box-sizing: border-box; + -ms-overflow-style: none; + scrollbar-width: none; + font-size: 0.95rem; + line-height: 1.1rem; + color: #534646; + margin-left: 20px; + } + + input::-webkit-scrollbar { + display: none; + } + } + + .micIcon { + display: flex; + align-items: center; + color: #1f1f1f; + margin-top: 2px; + padding-right: 1%; + + img { + max-width: 40%; + cursor: pointer; + } + } +} + +.blue-dot { + /* Ellipse 1226 */ + content: ''; + width: 8px; + height: 8px; + left: 1503px; + top: 445px; + display: flow-root; + border-radius: 15px; + + /* Blue */ + background: #4285f4; + animation: blue-transform 2s infinite; +} + +.green-dot { + content: ''; + width: 8px; + height: 8px; + left: 1522px; + top: 445px; + display: flow-root; + border-radius: 15px; + + /* Green */ + background: #0f9d58; + animation: green-transform 2s infinite; +} + +.yellow-dot { + content: ''; + width: 8px; + height: 8px; + left: 1541px; + top: 445px; + display: flow-root; + border-radius: 15px; + + /* Yellow */ + background: #f4b400; + animation: yellow-transform 2s infinite; +} + +.recording-container { + display: flex; + flex-direction: row; + gap: 10px; + cursor: pointer; + align-items: baseline; +} + +@keyframes blue-transform { + 0% { + transform: translateY(-14px); + } + + 33% { + transform: translateY(0); + } + + 66% { + transform: translateY(0); + } + + 100% { + transform: translateY(-14px); + } +} + +@keyframes green-transform { + 0% { + transform: translateY(0); + } + + 33% { + transform: translateY(-14px); + } + + 66% { + transform: translateY(0); + } + + 100% { + transform: translateY(0); + } +} + +@keyframes yellow-transform { + 0% { + transform: translateY(0); + } + + 33% { + transform: translateY(0); + } + + 66% { + transform: translateY(-14px); + } + + 100% { + transform: translateY(0); + } +} diff --git a/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/app/components/header/header.component.spec.ts b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/app/components/header/header.component.spec.ts new file mode 100644 index 0000000..90d999b --- /dev/null +++ b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/app/components/header/header.component.spec.ts @@ -0,0 +1,38 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {ComponentFixture, TestBed} from '@angular/core/testing'; + +import {HeaderComponent} from './header.component'; + +describe('HeaderComponent', () => { + let component: HeaderComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [HeaderComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(HeaderComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/app/components/header/header.component.ts b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/app/components/header/header.component.ts new file mode 100644 index 0000000..31849c9 --- /dev/null +++ b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/app/components/header/header.component.ts @@ -0,0 +1,83 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {Component, EventEmitter, Output} from '@angular/core'; +import {DomSanitizer} from '@angular/platform-browser'; +import {MatIconRegistry} from '@angular/material/icon'; +import {UserService} from 'src/app/services/user/user.service'; +import {AuthService} from '../../services/login/auth.service'; +import {environment} from 'src/environments/environment'; +import {Router} from '@angular/router'; + +const GOOGLE_CLOUD_ICON = ` + + + + + + + + +`; + +@Component({ + selector: 'app-header', + templateUrl: './header.component.html', + styleUrls: ['./header.component.scss'], +}) +export class HeaderComponent { + headerTitle: string = environment.chatbotName; + requiredLogin: string = environment.requiredLogin; + + showLoading = false; + + @Output() emitSearch: EventEmitter = new EventEmitter(); + + constructor( + iconRegistry: MatIconRegistry, + sanitizer: DomSanitizer, + public _UserService: UserService, + public authService: AuthService, + private router: Router + ) { + iconRegistry.addSvgIconLiteral( + 'google-cloud-icon', + sanitizer.bypassSecurityTrustHtml(GOOGLE_CLOUD_ICON) + ); + } + + ngOnInit(): void { + this._UserService.loadingSubject.subscribe( + loadingValue => (this.showLoading = loadingValue) + ); + } + + isSearchRoute(): boolean { + return this.router.url.startsWith('/search'); + } + + searchTerm(term: string) { + this.emitSearch.emit(term); + } + + goToManageConfig() { + this.router.navigateByUrl('/manage-config'); + } + + logout() { + this.authService.logout(); + } +} diff --git a/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/app/components/login/login.component.html b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/app/components/login/login.component.html new file mode 100644 index 0000000..ab76003 --- /dev/null +++ b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/app/components/login/login.component.html @@ -0,0 +1,63 @@ + + + diff --git a/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/app/components/login/login.component.scss b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/app/components/login/login.component.scss new file mode 100644 index 0000000..bf919c7 --- /dev/null +++ b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/app/components/login/login.component.scss @@ -0,0 +1,569 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +.login-container { + display: flex; + min-height: 100vh; + max-height: 100%; + .spinner { + display: flex; + justify-content: center; + margin: 1em; + } + + .linear-text { + background: linear-gradient( + 89.9deg, + rgba(66, 133, 244, 0.95) 21.17%, + #a488f5 44.34%, + rgba(234, 67, 53, 0.88) 81.26% + ); + -webkit-background-clip: text; + -moz-background-clip: text; + background-clip: text; + color: transparent; + } + + .heading { + font-family: 'Google Sans', sans-serif !important; + font-size: 32px; + font-style: normal; + font-weight: 500; + line-height: 150.7%; + color: white; + } + + .left-container { + background-image: + url('../../../assets/images/colibri_image_fx.jpg'), + url('../../../assets/images/crane_image_fx.jpg'); + background-repeat: no-repeat, repeat; + background-size: contain, contain; + flex: 0 1 55%; + min-height: 100%; + max-width: 60%; + align-items: center; + padding: 0.5em; + overflow: auto; + display: flex; + justify-content: center; + + .login-app-text-sub { + color: #161a1d; + font-size: 1rem; + font-style: normal; + font-weight: 400; + line-height: 150.7%; + } + + .login-app-text { + // margin-bottom: 2rem; + line-height: 1.5em; + } + + .expand-button { + --mdc-fab-container-color: #4285f41a; + --mdc-fab-icon-color: #000000; + border: 1px solid #4285f4; + --mat-mdc-fab-color: #4285f4; + height: 2.2rem; + font-size: 0.8rem; + font-weight: 400; + box-shadow: none; + } + + .expand-less-button { + --mdc-fab-container-color: rgba(92, 95, 97, 0.1); + --mdc-fab-icon-color: #000000; + border: 1px solid rgba(92, 95, 97, 0.5); + --mat-mdc-fab-color: rgba(92, 95, 97, 0.5); + margin-bottom: 1em; + height: 2.2rem; + font-size: 0.8rem; + font-weight: 400; + box-shadow: none; + } + } + + .right-container { + background-image: url('../../../assets/images/hot_air_baloon_image_fx.jpg'); + background-repeat: repeat; + background-size: cover; + flex: 0 1 45%; + min-height: 100%; + max-width: 80%; + align-items: flex-start; + display: flex; + justify-content: center; + overflow-y: auto; + scrollbar-width: thin; + + .login-card { + backdrop-filter: blur(10px); + flex-direction: column; + display: flex; + max-width: 100%; + padding: 3%; + margin: auto; + text-align: center; + + .logo-icon { + height: 145px; + width: 100%; + // margin-bottom: 2rem; + } + + .login-button { + background: #4285f4; + box-sizing: border-box; + border: 1px solid #e0e0e0; + color: #fff; + position: relative; + user-select: none; + cursor: pointer; + outline: none; + -webkit-tap-highlight-color: transparent; + display: inline-block; + font-family: 'Google Sans', sans-serif !important; + font-size: 14px; + font-weight: 500; + white-space: nowrap; + text-decoration: none; + vertical-align: baseline; + text-align: center; + margin: 0; + min-width: 100px; + line-height: 36px; + padding: 0 16px; + border-radius: 4px; + overflow: visible; + transform: translate3d(0, 0, 0); + transition: + background 400ms cubic-bezier(0.25, 0.8, 0.25, 1), + box-shadow 280ms cubic-bezier(0.4, 0, 0.2, 1); + } + + .register-text { + display: flex; + flex-direction: column; + align-items: flex-start; + margin: 1em 0em; + font-weight: 400; + color: #161a1d; + } + + .register-button { + background: white; + box-sizing: border-box; + border: 1px solid #4285f4; + color: #4285f4; + position: relative; + user-select: none; + cursor: pointer; + outline: none; + -webkit-tap-highlight-color: transparent; + display: inline-block; + font-family: 'Google Sans', sans-serif !important; + font-size: 14px; + font-weight: 500; + white-space: nowrap; + text-decoration: none; + vertical-align: baseline; + text-align: center; + margin: 0; + min-width: 64px; + line-height: 36px; + padding: 0 16px; + border-radius: 4px; + overflow: visible; + transform: translate3d(0, 0, 0); + transition: + background 400ms cubic-bezier(0.25, 0.8, 0.25, 1), + box-shadow 280ms cubic-bezier(0.4, 0, 0.2, 1); + } + } + } + + .slide { + width: 100%; + min-height: 15rem; + border-radius: 10px; + background-size: cover; + background-position: center; + font-size: 1rem; + font-style: normal; + font-weight: 400; + line-height: 200.7%; + padding-left: 0px; + + .feature-heading { + font-weight: 700; + } + } + + .slider { + position: relative; + height: 100%; + } + + .dotsContainer { + display: flex; + justify-content: center; + align-items: center; + margin-top: 2em; + } + + .dot { + margin: 0 3px; + cursor: pointer; + font-size: 18px; + color: #d0d3d7; + } + + .active { + color: #1a73e8; + background-color: #d0d3d7; + padding: 2px; + border-radius: 50%; + font-size: 12px; + } + + .material-symbols-outlined { + font-variation-settings: + 'FILL' 1, + 'wght' 400, + 'GRAD' 0, + 'opsz' 18; + } + + h6 { + position: relative; + z-index: 1; + overflow: hidden; + text-align: center; + font-size: 1.2em; + font-style: normal; + font-weight: 500; + line-height: 200.7%; + font-family: 'Google Sans', sans-serif !important; + } + + h6:before, + h6:after { + position: absolute; + top: 51%; + overflow: hidden; + width: 50%; + height: 1px; + content: '\a0'; + background: linear-gradient( + 86.95deg, + #34a853 -65.34%, + #fbbc05 -12.75%, + #ea4335 25.15%, + #1a73e8 103.88% + ); + } + + h6:before { + margin-left: -50%; + text-align: right; + } +} + +.login-buttons-container { + padding-inline: 4%; + gap: 12px; + display: flex; + flex-direction: column; + background: #f7f9ff; + padding-top: 4%; + padding-bottom: 4%; + border-radius: 5px; +} + +.back-button { + color: #5c5f61 !important; + font-weight: 400 !important; +} + +.help-container { + padding-top: 2%; + text-align: center; +} + +.blue { + color: #4285f4; + cursor: pointer; +} + +mat-form-field { + width: 100%; +} + +.user-name-container { + display: flex; + flex-direction: row; + gap: 3%; +} + +.error-message { + font-weight: 500; + font-size: 0.8rem; + letter-spacing: -0.005rem; + color: #ed0c0c; + + .mat-mdc-form-field-subscript-wrapper { + margin-top: -10px; + } +} + +.forgot-password-container { + display: flex; + flex-direction: column; + text-align: center; + padding: 4%; +} + +.forgot-password-icon-container { + width: 100%; + height: 100%; + + mat-icon { + font-size: 105px; + height: 100%; + width: 100%; + padding: 1%; + padding-top: 7%; + color: #3467bb; + } +} + +.forgot-password-heading-container { + font-size: 29px; + font-weight: 600; + font-family: 'Google Sans', sans-serif !important; + line-height: 2.5em; +} + +.forgot-password-subtext-container { + display: flex; + flex-direction: column; + text-align: center; + font-size: 19px; + gap: 10px; + line-height: 1.5em; + + mat-icon { + color: #4385f4; + } +} + +.logo-icon { + height: 170px; + width: 100%; +} + +.login-button { + width: 100%; + background: #4285f4; + box-sizing: border-box; + border: 1px solid #e0e0e0; + color: #fff; + position: relative; + user-select: none; + cursor: pointer; + outline: none; + -webkit-tap-highlight-color: transparent; + display: inline-block; + font-family: 'Google Sans', sans-serif !important; + font-size: 14px; + font-weight: 500; + white-space: nowrap; + text-decoration: none; + vertical-align: baseline; + text-align: center; + margin: 0; + min-width: 64px; + line-height: 44px; + padding: 0 16px; + border-radius: 4px; + overflow: visible; + transform: translate3d(0, 0, 0); + transition: + background 400ms cubic-bezier(0.25, 0.8, 0.25, 1), + box-shadow 280ms cubic-bezier(0.4, 0, 0.2, 1); +} + +.register-text { + display: flex; + flex-direction: column; + align-items: flex-start; + margin-bottom: 0.3em; + font-weight: 400; + color: #5c5f61; +} + +.register-button { + width: 100%; + background: white; + box-sizing: border-box; + border: 1px solid #4285f4; + color: #4285f4; + position: relative; + user-select: none; + cursor: pointer; + outline: none; + -webkit-tap-highlight-color: transparent; + display: inline-block; + font-family: 'Google Sans', sans-serif !important; + font-size: 14px; + font-weight: 500; + white-space: nowrap; + text-decoration: none; + vertical-align: baseline; + text-align: center; + margin: 0; + min-width: 64px; + line-height: 44px; + padding: 0 16px; + border-radius: 4px; + overflow: visible; + transform: translate3d(0, 0, 0); + transition: + background 400ms cubic-bezier(0.25, 0.8, 0.25, 1), + box-shadow 280ms cubic-bezier(0.4, 0, 0.2, 1); +} + +.send-email-button { + background: #40bd40 !important; + font-weight: 600 !important; + font-size: 16px !important; +} + +.close-button { + float: right !important; +} + +.forgot-password-link-container { + width: 100%; + text-align: end; + display: flex; + flex-direction: column; + padding-bottom: 4%; + z-index: 100; + position: relative; +} + +.button-css { + display: flex; + justify-content: center; + + .ok-button { + justify-content: center; + display: flex; + width: 25%; + background: #4285f4; + box-sizing: border-box; + border: 1px solid #e0e0e0; + color: #fff; + position: relative; + user-select: none; + cursor: pointer; + outline: none; + -webkit-tap-highlight-color: transparent; + font-family: 'Google Sans', sans-serif !important; + font-size: 14px; + font-weight: 500; + white-space: nowrap; + text-decoration: none; + vertical-align: baseline; + text-align: center; + margin: 0; + min-width: 64px; + line-height: 44px; + padding: 0 16px; + border-radius: 4px; + overflow: visible; + transform: translate3d(0, 0, 0); + transition: + background 400ms cubic-bezier(0.25, 0.8, 0.25, 1), + box-shadow 280ms cubic-bezier(0.4, 0, 0.2, 1); + } +} + +.icon-css { + font-size: 40px; + color: #f3f30aed; +} + +.text-css { + font-size: 15px; + line-height: 1.45em; + font-family: 'Google Sans', sans-serif !important; + color: #645a5a; +} + +.logo-container { + display: flex; + flex-direction: row; + justify-content: center; + text-align: center; + mat-icon { + height: 20% !important; + width: 100% !important; + font-size: 70px !important; + color: #4285f4 !important; + } +} + +.heading-span { + display: flex; + flex-direction: row; + gap: 8px; + align-items: center; + padding: 2%; + width: 100%; + justify-content: center; + font-size: 18px; + font-weight: 700; + padding-left: 15%; + color: #4285f4 !important; +} + +.heading-container { + display: flex; + flex-direction: row; + justify-content: center; + background: aliceblue; +} + +.sub-parent-container { + display: flex; + flex-direction: column; + text-align: center; + gap: 15px; + font-style: normal; + line-height: normal; + font-family: 'Google Sans', sans-serif !important; +} + +.info { + padding-bottom: 10px !important; + small { + color: #a8a8b5; + } +} diff --git a/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/app/components/login/login.component.spec.ts b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/app/components/login/login.component.spec.ts new file mode 100644 index 0000000..cb6b0f3 --- /dev/null +++ b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/app/components/login/login.component.spec.ts @@ -0,0 +1,38 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {ComponentFixture, TestBed} from '@angular/core/testing'; + +import {LoginComponent} from './login.component'; + +describe('LoginComponent', () => { + let component: LoginComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [LoginComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(LoginComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/app/components/login/login.component.ts b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/app/components/login/login.component.ts new file mode 100644 index 0000000..37b3e71 --- /dev/null +++ b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/app/components/login/login.component.ts @@ -0,0 +1,92 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {Component, NgZone, inject} from '@angular/core'; +import {Auth, signInWithPopup, GoogleAuthProvider} from '@angular/fire/auth'; +import {Router} from '@angular/router'; +import {AuthService} from 'src/app/services/login/auth.service'; +import {MatSnackBar} from '@angular/material/snack-bar'; +import {environment} from 'src/environments/environment'; +import {ToastMessageComponent} from '../toast-message/toast-message.component'; + +const HOME_ROUTE = '/'; + +interface LooseObject { + [key: string]: any; +} + +@Component({ + selector: 'app-login', + templateUrl: './login.component.html', + styleUrls: ['./login.component.scss'], +}) +export class LoginComponent { + private readonly auth: Auth = inject(Auth); + private readonly provider: GoogleAuthProvider = new GoogleAuthProvider(); + loader = false; + chatbotName: string = environment.chatbotName; + + constructor( + private authService: AuthService, + private router: Router, + public ngZone: NgZone, + private _snackBar: MatSnackBar + ) { + this.provider.setCustomParameters({ + prompt: 'select_account', + }); + } + + loginWithGoogle() { + this.loader = true; + signInWithPopup(this.auth, this.provider) + .then((result: any) => { + const user = result.user.toJSON(); + this.ngZone.run(() => { + this.authService.saveUserSession(user.stsTokenManager.accessToken); + this.redirect(user); + }); + }) + .catch(error => { + this.loader = false; + if (error.message !== 'Firebase: Error (auth/popup-closed-by-user).') { + this._snackBar.openFromComponent(ToastMessageComponent, { + panelClass: ['red-toast'], + verticalPosition: 'top', + horizontalPosition: 'right', + duration: 5000, + data: { + text: 'Error with SignIn. Please try again later !!!', + icon: 'cross-in-circle-white', + }, + }); + } + console.error(`Error: ${error}`); + }); + } + + redirect(user: any) { + const userDetails: LooseObject = {}; + userDetails['name'] = user.displayName; + userDetails['email'] = user.email; + userDetails['photoURL'] = user.photoURL; + userDetails['domain'] = user.domain; + (userDetails['uid'] = user.uid), + localStorage.setItem('USER_DETAILS', JSON.stringify(userDetails)); + this.loader = false; + this.router.navigate([HOME_ROUTE]); + } +} diff --git a/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/app/components/main/chat-input/chat-input.component.html b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/app/components/main/chat-input/chat-input.component.html new file mode 100644 index 0000000..cd2a76a --- /dev/null +++ b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/app/components/main/chat-input/chat-input.component.html @@ -0,0 +1,61 @@ + + + diff --git a/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/app/components/main/chat-input/chat-input.component.scss b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/app/components/main/chat-input/chat-input.component.scss new file mode 100644 index 0000000..31c7fd6 --- /dev/null +++ b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/app/components/main/chat-input/chat-input.component.scss @@ -0,0 +1,212 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +.chat-footer { + min-height: 60px; + display: flex; + border: 1px solid #dde6f6; + box-shadow: 0px 4px 18px rgba(195, 209, 226, 0.25); + border-radius: 30px; + width: 90vw; + backdrop-filter: blur(10px); + + .chat-textarea { + flex-grow: 1; + display: flex; + align-items: center; + + input { + width: 100%; + height: 45px; + border: 0px; + border-radius: 20px; + resize: none; + outline: none; + padding: 14px; + font-family: 'Google Sans', sans-serif !important; + box-sizing: border-box; + -ms-overflow-style: none; + scrollbar-width: none; + font-size: 0.95rem; + line-height: 1.1rem; + color: #534646; + margin-left: 20px; + } + + input::-webkit-scrollbar { + display: none; + } + } + + .micIcon { + display: flex; + align-items: center; + color: #1f1f1f; + margin-top: 2px; + padding-right: 1%; + + img { + max-width: 40%; + cursor: pointer; + } + } +} + +.recording-container { + display: flex; + flex-direction: row; + gap: 10px; + cursor: pointer; + align-items: baseline; +} + +.blue-dot { + /* Ellipse 1226 */ + content: ''; + width: 8px; + height: 8px; + left: 1503px; + top: 445px; + display: flow-root; + border-radius: 15px; + + /* Blue */ + background: #4285f4; + animation: blue-transform 2s infinite; +} + +.green-dot { + content: ''; + width: 8px; + height: 8px; + left: 1522px; + top: 445px; + display: flow-root; + border-radius: 15px; + + /* Green */ + background: #0f9d58; + animation: green-transform 2s infinite; +} + +.yellow-dot { + content: ''; + width: 8px; + height: 8px; + left: 1541px; + top: 445px; + display: flow-root; + border-radius: 15px; + + /* Yellow */ + background: #f4b400; + animation: yellow-transform 2s infinite; +} + +.file-input-wrapper { + margin-bottom: 20px; + display: flex; + flex-direction: column; + align-items: center; + gap: 10px; + + .placeholder { + color: rgba(0, 0, 0, 0.54); // Material hint text color + } + + .file-name { + font-style: italic; + color: rgba(0, 0, 0, 0.87); + } +} + +.image-preview { + margin-top: 20px; + border: 1px solid #ccc; + border-radius: 4px; + padding: 5px; + max-width: 100%; + display: flex; + justify-content: center; + + img { + max-width: 100%; + max-height: 300px; // Adjust as needed + display: block; + } +} + +.card-input-wrapper { + // min-width: 40vw; + width: 80%; + min-height: 10rem; + display: flex; + justify-content: center; + flex-wrap: wrap; + flex-direction: column; + align-items: center; + text-align: center; + border: 4px dashed lightgrey; + margin: 2rem; + padding: 1rem; + border-radius: 4px; + cursor: pointer; + transition: border 1s; + text-decoration: none; + + &:hover { + border-color: rgba(66, 133, 244, 0.95); + } +} + +.centered-mat-card { + display: flex; + flex-direction: column; + align-items: center; + align-self: center; + justify-self: center; + width: 40vw; +} + + +// Small devices (landscape phones, 576px and up) +@media (min-width: 576px) { +} + +// Medium devices (tablets, 768px and up) +@media (min-width: 768px) { + .chat-footer { + min-width: 720px; + } +} + +// Large devices (desktops, 992px and up) +@media (min-width: 992px) { + .chat-footer { + width: 60vw; + } +} + +// X-Large devices (large desktops, 1200px and up) +@media (min-width: 1200px) { +} + +// XX-Large devices (larger desktops, 1400px and up) +@media (min-width: 1400px) { + .chat-footer { + width: 50vw; + } +} diff --git a/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/app/components/main/chat-input/chat-input.component.spec.ts b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/app/components/main/chat-input/chat-input.component.spec.ts new file mode 100644 index 0000000..158ea34 --- /dev/null +++ b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/app/components/main/chat-input/chat-input.component.spec.ts @@ -0,0 +1,38 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {ComponentFixture, TestBed} from '@angular/core/testing'; + +import {ChatInputComponent} from './chat-input.component'; + +describe('ChatInputComponent', () => { + let component: ChatInputComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ChatInputComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(ChatInputComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/app/components/main/chat-input/chat-input.component.ts b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/app/components/main/chat-input/chat-input.component.ts new file mode 100644 index 0000000..771a3fd --- /dev/null +++ b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/app/components/main/chat-input/chat-input.component.ts @@ -0,0 +1,89 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {Component, EventEmitter, Output} from '@angular/core'; +import {ActivatedRoute} from '@angular/router'; +import {SpeechToTextService} from 'src/app/services/speech-to-text'; + +@Component({ + selector: 'app-chat-input', + templateUrl: './chat-input.component.html', + styleUrls: ['./chat-input.component.scss'], +}) +export class ChatInputComponent { + isRecording = false; + transcribedText = ''; + mediaRecorder: MediaRecorder | undefined; + audioChunks: Blob[] = []; + + term = ''; + @Output() emitSearch: EventEmitter = new EventEmitter(); + + constructor( + private speechToTextService: SpeechToTextService, + private route: ActivatedRoute + ) { + const query = this.route.snapshot.queryParamMap.get('q'); + if (query) { + this.term = query; + } + } + + ngOnInit() { + navigator.mediaDevices + .getUserMedia({audio: true}) + .then(stream => this.setupMediaRecorder(stream)) + .catch(err => { + console.error(err); + }); + } + + searchTerm() { + this.emitSearch.emit(this.term); + } + + setupMediaRecorder(stream: MediaStream) { + this.mediaRecorder = new MediaRecorder(stream); + this.mediaRecorder.ondataavailable = event => + this.audioChunks.push(event.data); + this.mediaRecorder.onstop = () => this.sendAudioToGCP(); + } + + startRecording() { + this.isRecording = true; + this.audioChunks = []; + if (this.mediaRecorder) this.mediaRecorder.start(); + } + + stopRecording() { + this.isRecording = false; + if (this.mediaRecorder) this.mediaRecorder.stop(); + } + + async sendAudioToGCP() { + const audioBlob = new Blob(this.audioChunks); + (await this.speechToTextService.transcribeAudio(audioBlob)).subscribe( + (response: any) => { + this.term = response[0]; + this.searchTerm(); + }, + (error: any) => { + // Handle errors + console.error('Error transcribing audio:', error); + } + ); + } +} diff --git a/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/app/components/main/main.component.html b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/app/components/main/main.component.html new file mode 100644 index 0000000..5335586 --- /dev/null +++ b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/app/components/main/main.component.html @@ -0,0 +1,31 @@ + + + +
    +
    +
    +
    +
    + Hey, {{ savedUser.name ? savedUser.name!.split(' ')[0] + '!' : '' }} +
    +
    Upload your photo and start now!
    +
    +
    +
    + + +
    diff --git a/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/app/components/main/main.component.scss b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/app/components/main/main.component.scss new file mode 100644 index 0000000..dd537b5 --- /dev/null +++ b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/app/components/main/main.component.scss @@ -0,0 +1,1110 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@import url('https://fonts.googleapis.com/icon?family=Material+Icons|Material+Icons+Outlined'); + +:host + ::ng-deep + mat-form-feild + .mat-form-field-appearance-outline + .mat-form-field-outline-start { + color: red; + border-radius: 50px !important; + font-size: 10px !important; +} + +:host + ::ng-deep + .mdc-text-field--outlined + .mdc-notched-outline + .mdc-notched-outline__leading { + border-radius: 28px 0 0 28px !important; + min-width: 28px !important; + border-color: #babfc4 !important; +} + +:host + ::ng-deep + .mdc-text-field--outlined + .mdc-notched-outline + .mdc-notched-outline__trailing { + border-radius: 0 28px 28px 0 !important; + border-color: #babfc4 !important; +} + +:host ::ng-deep .mat-form-field-outline-end { + border-radius: 0 28px 28px 0 !important; +} + +:host ::ng-deep .mat-mdc-card { + box-shadow: + 0px 2px 1px -1px rgb(217 220 224), + 0px 1px 1px 0px rgba(0, 0, 0, 0.14), + 0px 1px 3px 0px rgb(229 232 234) !important; +} + +:host ::ng-deep .mat-mdc-standard-chip.mat-primary.mat-mdc-chip-selected, +.mat-mdc-standard-chip.mat-primary.mat-mdc-chip-highlighted { + --mdc-chip-elevated-container-color: #edf3fe; + --mdc-chip-elevated-disabled-container-color: #e0e0e0; + --mdc-chip-label-text-color: #4285f4; + --mdc-chip-disabled-label-text-color: #4285f4; + --mdc-chip-with-icon-icon-color: #4285f4; + --mdc-chip-with-icon-disabled-icon-color: #4285f4; + --mdc-chip-with-trailing-icon-disabled-trailing-icon-color: #4285f4; + --mdc-chip-with-trailing-icon-trailing-icon-color: #4285f4; + --mdc-chip-with-icon-selected-icon-color: #4285f4; +} + +.parent-container { + display: flex; + max-height: calc(100vh - 70px); + height: 90vh; + font-family: 'Google Sans', sans-serif !important; + overflow-y: auto; + flex-direction: column; + justify-content: center; + + .img-globe { + // position:absolute; + width: 66%; + } +} + +.wrapping-container { + width: 100%; +} + +.main-div { + font-family: 'Google Sans', sans-serif !important; +} + +.text-control { + width: 32%; +} + +h1 { + line-height: 50px; + width: 34%; +} + +h2 { + font-family: 'Google Sans', sans-serif !important; +} + +.header { + -webkit-background-clip: text; + -webkit-text-fill-color: #48b975; + background-clip: text; + font-size: 3rem; + text-align: center; + font-weight: bold; + font-weight: 700; + -webkit-font-smoothing: antialiased; + height: 100%; + font-family: 'Google Sans', sans-serif; +} + +.send-button { + color: #babfc4; +} + +.logo-icon { + height: 120px; + width: 400px; + margin-bottom: 90px; + margin-left: -20px; +} + +.user-info { + display: flex; + justify-content: center; +} + +.user-info-column { + display: column; +} + +.card-outer-div { + .card-inside-left { + .circle { + width: 100px; + height: 100px; + border: 5px solid rgb(255, 255, 255); + border-radius: 50%; + overflow: hidden; + box-shadow: + 0px 2px 1px -1px rgba(0, 0, 0, 0.2), + 0px 1px 1px 0px rgba(0, 0, 0, 0.14), + 0px 1px 3px 0px rgba(0, 0, 0, 0.12); + } + + .circle img { + width: 100%; + height: 100%; + object-fit: cover; + } + } + + .card-inside-right { + width: 80%; + display: flex; + flex-direction: column; + align-items: center; + color: #747474; + font-size: 0.8rem; + font-weight: 500; + + .name { + .title { + font: 1rem; + } + + .sub-title { + font-size: 0.9rem; + margin-left: 2%; + } + } + + .company { + display: flex; + align-items: center; + + .logo { + padding-right: 1%; + } + + .text { + width: 100%; + + .sub-title { + margin-left: 2%; + } + } + } + } +} + +.mat-mdc-standard-chip { + --mdc-chip-label-text-color: #4285f4; + --mdc-chip-elevated-container-color: #fff; + border: 1px solid #4285f4 !important; + font-family: 'Google Sans', sans-serif !important; + font-size: 15px !important; + font-weight: 500 !important; + padding-left: 12px !important; + padding-right: 12px !important; +} + +.sub-heading { + font-family: 'Google Sans', sans-serif; + color: #161a1d; + font-size: 30px; + line-height: 40px; + margin-top: -20px; + text-align: left; + z-index: 999; +} + +.genAI { + //background-image: linear-gradient(to left, #4285F4, #DB4437, #F4B400, #0F9D58); + // background-image: linear-gradient(to left, #34A853, #FBBC05, #EA4335, #1A73E8); + background: linear-gradient( + 86.95deg, + #34a853 -65.34%, + #fbbc05 -12.75%, + #ea4335 25.15%, + #1a73e8 103.88% + ); + -webkit-background-clip: text; + -moz-background-clip: text; + background-clip: text; + color: transparent; +} + +.margin-div { + margin-top: -20px; +} + +.chat-footer-outer { + display: flex; + align-items: center; + width: 100%; + justify-content: center; + + .chat-footer { + display: flex; + border: 1px solid #dde6f6; + box-shadow: 0px 4px 18px rgba(195, 209, 226, 0.25); + border-radius: 20px; + width: 80%; + backdrop-filter: blur(10px); + + .chat-textarea { + flex-grow: 1; + display: flex; + align-items: center; + + input { + width: 100%; + height: 45px; + border: 0px; + border-radius: 4px; + resize: none; + outline: none; + padding: 14px; + font-family: 'Google Sans', sans-serif !important; + box-sizing: border-box; + -ms-overflow-style: none; + scrollbar-width: none; + font-size: 0.95rem; + line-height: 1.1rem; + color: #534646; + margin-left: 20px; + } + + input::-webkit-scrollbar { + display: none; + } + } + + .micIcon { + display: flex; + align-items: center; + color: #1f1f1f; + margin-top: 2px; + padding-right: 1%; + + img { + max-width: 40%; + cursor: pointer; + } + } + } +} + +.text-gnosis { + color: var(--White, #fff); + font-size: 16px; + font-style: normal; + font-weight: 400; + line-height: 24px; + /* 150% */ + letter-spacing: 0.4px; +} + +.chip-cancel-button { + color: #002dff; +} + +.categories-div { + .mat-mdc-standard-chip { + --mdc-chip-label-text-color: #0f9d58; + --mdc-chip-elevated-container-color: #fff; + border: 1px solid #0f9d58 !important; + font-family: 'Google Sans', sans-serif !important; + // font-weight: 600; + } +} + +.intent-chip { + .mat-mdc-standard-chip { + --mdc-chip-label-text-color: #4285f4; + --mdc-chip-elevated-container-color: #fff; + border: 1px solid #4285f4 !important; + font-family: 'Google Sans', sans-serif !important; + // font-weight: 600; + } + + .chip-cancel-button { + color: #002dff; + } +} + +.category-chip { + .mat-mdc-standard-chip { + --mdc-chip-label-text-color: #0f9d58; + --mdc-chip-elevated-container-color: #fff; + border: 1px solid #0f9d58 !important; + font-family: 'Google Sans', sans-serif !important; + // font-weight: 600; + } + + .chip-cancel-button { + color: #007d06; + } +} + +.questions-div { + flex-direction: column; + margin-top: 2rem; + margin-bottom: 5rem; + + .intent-card { + padding: 0px; + } + + .intent-heading { + color: #787978; + font-size: 16px; + font-style: normal; + font-weight: 400; + line-height: normal; + font-family: 'Google Sans', sans-serif !important; + width: 66%; + } + + .intent-container { + border-radius: 8px; + background-color: rgba(246, 249, 254, 1); + width: 66%; + box-shadow: none !important; + flex-shrink: 0; + + .selected-question { + // margin: 0.5rem 0rem; + &:hover { + background-color: rgba(66, 133, 244, 0.1); + } + } + + .question-item { + display: flex; + justify-content: space-between; + font-family: 'Google Sans', sans-serif !important; + color: #787978; + font-size: 1em; + line-height: 24px; + } + + .question-link { + overflow: hidden; + text-overflow: ellipsis; + } + } +} + +.intent-chip-textarea ::ng-deep .mdc-evolution-chip-set__chips { + align-items: center !important; +} + +.float-button { + position: fixed; + right: 3em; + bottom: 6em; + --mdc-fab-container-color: #4285f4; +} + +.name-container { + font-size: 3.5rem; + font-weight: 400; + line-height: 4rem; + font-family: + Google Sans, + Helvetica Neue, + sans-serif; + letter-spacing: -0.03em; + background: linear-gradient( + 89.9deg, + rgba(66, 133, 244, 0.95) 0.17%, + #a488f5 7.34%, + rgba(234, 67, 53, 0.88) 24.26% + ); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; + text-fill-color: transparent; + animation: effect 2s linear; + display: flex; + flex-direction: row; + gap: 1%; + align-items: center; + justify-content: center; +} + +@keyframes effect { + 0% { + background: linear-gradient( + 89.9deg, + rgba(240, 216, 214, 0.88) 0.17%, + #ebcaec 7.34%, + rgba(194, 214, 247, 0.95) 24.26% + ); + -webkit-background-clip: text; + } + + 10% { + background: linear-gradient( + 89.9deg, + rgba(227, 237, 253, 0.95) 0.17%, + #e5ddfd 7.34%, + rgba(186, 210, 248, 0.95) 24.26% + ); + -webkit-background-clip: text; + } + + 20% { + background: linear-gradient( + 89.9deg, + rgba(218, 231, 253, 0.95) 0.17%, + #dcd1fc 7.34%, + rgba(172, 202, 252, 0.95) 24.26% + ); + -webkit-background-clip: text; + } + + 30% { + background: linear-gradient( + 89.9deg, + rgba(199, 219, 252, 0.95) 0.17%, + #d0c2fa 7.34%, + rgba(161, 196, 252, 0.95) 24.26% + ); + -webkit-background-clip: text; + } + + 40% { + background: linear-gradient( + 89.9deg, + rgba(169, 201, 253, 0.95) 0.17%, + #cdbcff 7.34%, + rgba(149, 188, 250, 0.95) 24.26% + ); + -webkit-background-clip: text; + } + + 50% { + background: linear-gradient( + 89.9deg, + rgba(200, 220, 253, 0.95) 0.17%, + #bba5fc 7.34%, + rgba(203, 221, 250, 0.95) 24.26% + ); + -webkit-background-clip: text; + } + + 60% { + background: linear-gradient( + 89.9deg, + rgba(227, 237, 253, 0.95) 0.17%, + #e5ddfd 7.34%, + rgba(186, 210, 248, 0.95) 24.26% + ); + -webkit-background-clip: text; + } + + 70% { + background: linear-gradient( + 89.9deg, + rgba(183, 210, 253, 0.95) 0.17%, + #e5ddfd 7.34%, + rgba(248, 217, 215, 0.88) 24.26% + ); + -webkit-background-clip: text; + } + + 80% { + background: linear-gradient( + 89.9deg, + rgba(122, 172, 252, 0.95) 0.17%, + #e5ddfd 7.34%, + rgba(247, 182, 176, 0.88) 24.26% + ); + -webkit-background-clip: text; + } + + 90% { + background: linear-gradient( + 89.9deg, + rgba(94, 148, 236, 0.95) 0.17%, + #b49ff1 7.34%, + rgba(240, 121, 111, 0.88) 24.26% + ); + -webkit-background-clip: text; + } + + 100% { + background: linear-gradient( + 89.9deg, + rgba(66, 133, 244, 0.95) 0.17%, + #a488f5 7.34%, + rgba(234, 67, 53, 0.88) 24.26% + ); + -webkit-background-clip: text; + } +} + +.helper-container { + white-space: nowrap; + overflow: hidden; + font-size: 1.5rem; + font-weight: 400; + line-height: 4rem; + font-family: + Google Sans, + Helvetica Neue, + sans-serif; + letter-spacing: -0.03em; + color: #8f9391; + padding-bottom: 3rem; + opacity: 0.5; + animation: + typing 2s steps(22), + blink 0.5s step-end infinite alternate; + justify-content: center; + display: flex; +} + +@keyframes typing { + from { + width: 0; + } +} + +@keyframes blink { + 50% { + border-color: transparent; + } +} + +.quick-bot-introduction-container { + width: 100%; + display: flex; + flex-direction: row; + justify-content: center; + + .introduction-sub-container { + width: 66%; + justify-content: center; + text-align: center; + display: flex; + flex-direction: column; + padding-top: 2%; + padding-bottom: 2%; + background: + radial-gradient(white, #fafcff) padding-box, + linear-gradient( + 89.86deg, + #34a853 -40.93%, + #fbbc05 4.92%, + #ea4335 37.97%, + #1a73e8 106.62% + ) + border-box; + border-radius: 24px; + border: 1px solid transparent; + } + + .logo-image-container { + img { + height: 110px; + width: 110px; + } + } + + .logo-text-heading { + font-style: normal; + font-weight: 500; + font-size: 28px; + line-height: 202%; + /* or 65px */ + text-align: center; + + background: linear-gradient( + 89.86deg, + #34a853 -40.93%, + #fbbc05 4.92%, + #ea4335 37.97%, + #1a73e8 106.62% + ); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; + text-fill-color: transparent; + } + + .logo-text-subheading { + font-style: normal; + font-weight: 500; + font-size: 21px; + line-height: 202%; + /* or 48px */ + text-align: center; + } + + .blue { + color: #4285f4; + } +} + +.helpButton { + position: fixed !important; + bottom: 45px !important; + right: 20px !important; + background-color: #4285f4 !important; + color: white !important; + text-decoration: none !important; + border-radius: 60px !important; + height: 41px !important; + display: inline-flex !important; + align-items: center !important; + overflow: hidden !important; + width: auto !important; + max-width: 41px !important; + -webkit-transition: max-width 0.5s !important; + transition: max-width 0.5s !important; + z-index: 1000 !important; + + &:hover { + max-width: 300px !important; + } + + .text { + white-space: nowrap !important; + padding-right: 15px !important; + font-size: 15px !important; + font-weight: 600 !important; + } +} + +::placeholder { + color: #646566; +} + +.intent-container-box { + box-sizing: border-box; + width: 239px; + height: 239px; + border-radius: 14px; + transition-delay: 2s; + transition: width 8s; + padding: 1%; + cursor: pointer; +} + +.selected-intent-box { + box-sizing: border-box; + height: 239px; + border-radius: 14px; + transition-delay: 2s; + transition: width 8s; + cursor: pointer; + width: max-content !important; + max-width: 100%; + display: flex; + flex-direction: column; + gap: 1%; + padding: 1%; + background: + linear-gradient( + 92.1deg, + #bdd1f3 19.16%, + rgb(199 183 245 / 90%) 79%, + rgb(235 165 158 / 90%) 135.24% + ), + linear-gradient( + 272.04deg, + rgba(158, 188, 237, 0.7) 14.93%, + rgba(158, 188, 237, 0) 104.51% + ) !important; +} + +.cards-outer-container { + width: 65%; + overflow: auto; + scrollbar-width: none; +} + +.cards-outer-container:after { + z-index: -1; + content: ''; + width: 10%; + opacity: 0.5 !important; +} + +.cards-container-all-categories { + width: max-content; + display: flex; + overflow-x: auto; + gap: 1%; + max-width: 160%; + min-width: 100%; + scrollbar-width: none; +} + +.cards-container-few-categories { + width: max-content; + display: flex; + overflow-x: auto; + gap: 1%; + max-width: 160%; + min-width: 100%; + scrollbar-width: none; + justify-content: center; + padding-inline: 5%; +} + +.intent-container-box:hover { + background: #dde3ea !important; +} + +.card-detail-container { + width: 100%; + min-width: 250px; + display: flex; + flex-direction: row; +} + +.selected-intent-suggested-question { + gap: 4%; + display: flex !important; + flex-direction: column; +} + +.suggested-question-chip-row { + margin-top: 0.5rem; + + .suggested-question-chip { + display: flex; + flex-direction: row; + margin: 0.5rem; + padding: 0.5rem; + width: 100%; + + .suggested-question-icon { + padding-right: 0.25rem; + } + + .suggested-question-text { + margin-left: 0.25rem; + + width: 100%; + } + } +} + +.close-button-container { + display: none !important; +} + +.expand-close-button-container { + display: block !important; + + .close-button { + margin-top: -10px !important; + } +} + +.card-heading { + padding-top: 2.5%; + font-size: 17px; + color: #1f1f1f; + width: 90%; +} + +.card-content { + font-size: 13px; + color: #646566; + padding-top: 4%; + white-space: break-spaces; + text-wrap: balance; + padding-right: 3%; + text-align: left; + width: 85%; +} + +::ng-deep .mat-mdc-snack-bar-container { + &.green-snackbar { + --mdc-snackbar-container-color: #0f9d58; + --mat-mdc-snack-bar-button-color: #fff; + --mdc-snackbar-supporting-text-color: #fff; + } + + &.red-snackbar { + --mdc-snackbar-container-color: #e9103f; + --mat-mdc-snack-bar-button-color: #fff; + --mdc-snackbar-supporting-text-color: #fff; + } +} + +::ng-deep .mat-mdc-dialog-surface { + justify-content: center !important; + display: flex !important; + flex-direction: column !important; + text-align: center !important; + background: #ffffff; + border-radius: 20px !important; + box-sizing: border-box; + box-shadow: none !important; + + .badge-prompt-parent-container { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + text-align: center; + + .badge-greetings-container { + width: 100%; + display: flex; + flex-direction: row; + justify-content: center; + + img { + width: 45%; + } + } + + .badge-text-container { + font-style: normal; + font-weight: 400; + font-size: 17px; + line-height: 45px; + display: flex; + align-items: center; + color: rgba(92, 95, 97, 0.7); + } + } +} + +.blue-dot { + /* Ellipse 1226 */ + content: ''; + width: 8px; + height: 8px; + left: 1503px; + top: 445px; + display: flow-root; + border-radius: 15px; + + /* Blue */ + background: #4285f4; + animation: blue-transform 2s infinite; +} + +.green-dot { + content: ''; + width: 8px; + height: 8px; + left: 1522px; + top: 445px; + display: flow-root; + border-radius: 15px; + + /* Green */ + background: #0f9d58; + animation: green-transform 2s infinite; +} + +.yellow-dot { + content: ''; + width: 8px; + height: 8px; + left: 1541px; + top: 445px; + display: flow-root; + border-radius: 15px; + + /* Yellow */ + background: #f4b400; + animation: yellow-transform 2s infinite; +} + +.recording-container { + display: flex; + flex-direction: row; + gap: 10px; + cursor: pointer; + align-items: baseline; +} + +@keyframes blue-transform { + 0% { + transform: translateY(-14px); + } + + 33% { + transform: translateY(0); + } + + 66% { + transform: translateY(0); + } + + 100% { + transform: translateY(-14px); + } +} + +@keyframes green-transform { + 0% { + transform: translateY(0); + } + + 33% { + transform: translateY(-14px); + } + + 66% { + transform: translateY(0); + } + + 100% { + transform: translateY(0); + } +} + +@keyframes yellow-transform { + 0% { + transform: translateY(0); + } + + 33% { + transform: translateY(0); + } + + 66% { + transform: translateY(-14px); + } + + 100% { + transform: translateY(0); + } +} + +.tooltip-container { + display: flex; + width: 100%; + justify-content: center; + align-items: baseline; + padding-top: 2%; + padding-bottom: 2%; + .tooltip-sub-container { + width: 66%; + display: flex; + align-items: baseline; + justify-content: center; + } +} + +.tooltip-icon-container { + display: flex; + flex-direction: row; + // align-self: flex-end; + gap: 10px; + flex-wrap: nowrap; + text-wrap: nowrap; + align-self: center; + color: #5f6368; + + .tooltip-label { + font-family: 'Roboto'; + font-style: normal; + font-weight: 500; + font-size: 16px; + line-height: 20px; + text-align: center; + } + + mat-icon { + font-size: 22px !important; + } +} + +.tooltip-text-container { + flex-wrap: wrap; + word-wrap: break-word; + justify-content: flex-start; + display: flex; + color: #5f6368; + .tooltip-text { + font-family: 'Roboto'; + font-style: normal; + font-weight: 400; + font-size: 16px; + line-height: 20px; + } +} + +.dismiss-container { + font-family: 'Roboto'; + font-style: normal; + font-weight: 500; + font-size: 14px; + line-height: 20px; + /* identical to box height, or 143% */ + text-align: center; + + /* Blue */ + color: #4285f4; + padding-left: 1%; + cursor: pointer; +} + +.dismiss-container:hover { + text-decoration: underline; +} + +.journey-chips { + max-height: 300px; + overflow-y: auto; +} + +::ng-deep .mat-mdc-standard-chip:hover { + background: linear-gradient( + 92.1deg, + #9ebced 19.16%, + rgba(164, 136, 245, 0.9) 79% + ) !important; + --mdc-chip-label-text-color: #fff; +} + +.suggested-question-container { + width: 60%; +} + +// Small devices (landscape phones, 576px and up) +@media (min-width: 576px) { + .helper-container { + font-size: 2rem; + } +} + +// Medium devices (tablets, 768px and up) +@media (min-width: 768px) { + .chat-footer-outer { + .chat-footer { + min-height: 60px; + width: 66%; + } + } + + .helper-container { + font-size: 3.5rem; + } +} + +// Large devices (desktops, 992px and up) +@media (min-width: 992px) { +} + +// X-Large devices (large desktops, 1200px and up) +@media (min-width: 1200px) { +} + +// XX-Large devices (larger desktops, 1400px and up) +@media (min-width: 1400px) { +} diff --git a/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/app/components/main/main.component.spec.ts b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/app/components/main/main.component.spec.ts new file mode 100644 index 0000000..66e7fd9 --- /dev/null +++ b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/app/components/main/main.component.spec.ts @@ -0,0 +1,38 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {ComponentFixture, TestBed} from '@angular/core/testing'; + +import {MainComponent} from './main.component'; + +describe('MainComponent', () => { + let component: MainComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [MainComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(MainComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/app/components/main/main.component.ts b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/app/components/main/main.component.ts new file mode 100644 index 0000000..87fc1da --- /dev/null +++ b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/app/components/main/main.component.ts @@ -0,0 +1,63 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {Component, OnDestroy} from '@angular/core'; +import {UserService} from 'src/app/services/user/user.service'; +import {Router} from '@angular/router'; +import {MatDialog} from '@angular/material/dialog'; +import {ReplaySubject} from 'rxjs'; +import {SearchResponse} from 'src/app/models/search.model'; +import {ImageService} from 'src/app/services/image/image.service'; +@Component({ + selector: 'app-main', + templateUrl: './main.component.html', + styleUrls: ['./main.component.scss'], +}) +export class MainComponent implements OnDestroy { + private readonly destroyed = new ReplaySubject(1); + term = ''; + showResults = false; + searchResults: SearchResponse = { + summary: undefined, + results: [], + totalSize: 0, + }; + savedUser; + + constructor( + public userService: UserService, + private router: Router, + public dialog: MatDialog, + private imageService: ImageService + ) { + this.savedUser = userService.getUserDetails(); + } + + goToResults(file: File) { + this.imageService.setImage(file); // Store the file in the service + this.router.navigate(['/search'], { + queryParams: { + q: 'Change the background to be minimalist, elegant, with a soft neutral color palette and subtle shadows, conveying a sense of sophistication and high quality.', + filename: file.name, + }, + }); + } + + ngOnDestroy() { + this.destroyed.next(); + this.destroyed.complete(); + } +} diff --git a/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/app/components/main/search-results/search-results.component.html b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/app/components/main/search-results/search-results.component.html new file mode 100644 index 0000000..c975103 --- /dev/null +++ b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/app/components/main/search-results/search-results.component.html @@ -0,0 +1,181 @@ + + + + +
    + +
    + Image Background Changer +

    + Prompt: {{ selectedResult?.enhancedPrompt }} +

    +

    + {{ summary }} +

    +
    + + +
    + + +
    + Selected Edited Image + + Image Placeholder + +
    + No images were generated for this query. +
    + +
    + Source: {{ selectedResult?.source }} +
    +
    + + +
    + + +
    +

    Imagen 3 Edited Entire Image

    +
    +
    +
    + Edited Entire Image Thumbnail +
    +
    + No "entire image" edits. +
    +
    +
    +
    + + +
    +

    Imagen 3 Edited Background Only

    +
    +
    +
    + Edited Background Thumbnail +
    +
    + No "background only" edits. +
    +
    +
    +
    + + +
    +

    Gemini 2.0 Image

    +
    +
    +
    + Gemini Image Thumbnail +
    +
    + No Gemini results. +
    +
    +
    +
    +
    +
    + + + +
    +
    +

    Configuration

    + + + + AI Model + + + {{ model.viewValue }} + + + + + +
    +
    + Number of Results + {{selectedNumberOfResults}} +
    + + + +
    + + +
    +
    + Mask Dilation + {{ selectedMaskDistilation | number:'1.0-4' }} +
    + (Only for background editing) + + + +
    + +
    + +
    + +
    +
    +
    + + +
    + No results yet +

    Describe the Background Change

    +

    Enter a prompt for the new background you envision for your image.

    +
    +
    +
    diff --git a/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/app/components/main/search-results/search-results.component.scss b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/app/components/main/search-results/search-results.component.scss new file mode 100644 index 0000000..fc48104 --- /dev/null +++ b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/app/components/main/search-results/search-results.component.scss @@ -0,0 +1,49 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +.w-24.h-24.cursor-pointer { + transition: border-color 0.2s ease-in-out; +} + +.bg-custom-gray { + // background-color: $custom-gray-color; // Using a variable + background-color: #f0f2f5; // Or a direct value, adjust as needed +} + +// Custom scrollbar styling for thumbnail areas +.custom-scrollbar { + &::-webkit-scrollbar { + width: 8px; // Width of the scrollbar + height: 8px; // Height of the scrollbar (for horizontal) + } + + &::-webkit-scrollbar-track { + background: #f1f1f1; // Color of the tracking area + border-radius: 10px; + } + + &::-webkit-scrollbar-thumb { + background: #c1c1c1; // Color of the scroll thumb + border-radius: 10px; + + &:hover { + background: #a8a8a8; // Color of the scroll thumb on hover + } + } + + // For Firefox + scrollbar-width: thin; + scrollbar-color: #c1c1c1 #f1f1f1; +} \ No newline at end of file diff --git a/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/app/components/main/search-results/search-results.component.spec.ts b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/app/components/main/search-results/search-results.component.spec.ts new file mode 100644 index 0000000..1c8bf77 --- /dev/null +++ b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/app/components/main/search-results/search-results.component.spec.ts @@ -0,0 +1,38 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {ComponentFixture, TestBed} from '@angular/core/testing'; + +import {SearchResultsComponent} from './search-results.component'; + +describe('SearchResultsComponent', () => { + let component: SearchResultsComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [SearchResultsComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(SearchResultsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/app/components/main/search-results/search-results.component.ts b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/app/components/main/search-results/search-results.component.ts new file mode 100644 index 0000000..0a47eb7 --- /dev/null +++ b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/app/components/main/search-results/search-results.component.ts @@ -0,0 +1,270 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {Component, OnDestroy, ViewChild, TemplateRef} from '@angular/core'; +import {SearchService} from 'src/app/services/search.service'; +import {ReplaySubject} from 'rxjs'; +import {UserService} from 'src/app/services/user/user.service'; +import {ActivatedRoute, Router} from '@angular/router'; +import {GeneratedImage} from 'src/app/models/generated-image.model'; +import {CombinedBackgroundChangerResults, SearchRequest} from 'src/app/models/search.model'; +import {MatSnackBar} from '@angular/material/snack-bar'; +import {ToastMessageComponent} from '../../toast-message/toast-message.component'; + +interface Imagen3Model { + value: string; + viewValue: string; +} + +@Component({ + selector: 'app-search-results', + templateUrl: './search-results.component.html', + styleUrls: ['./search-results.component.scss'], +}) +export class SearchResultsComponent implements OnDestroy { + @ViewChild('preview', {static: true}) + previewRef!: TemplateRef<{}>; + + summary = ''; + private readonly destroyed = new ReplaySubject(1); + isLoading = false; + + geminiDocuments: GeneratedImage[] = []; + imagenEntireImageDocuments: GeneratedImage[] = []; + imagenBackgroundDocuments: GeneratedImage[] = []; + + showDefaultDocuments = false; + defaultPlaceholderImageUrl = 'assets/images/placeholder_image.png'; + selectedResult: GeneratedImage | undefined; + currentSearchTerm = ''; + + imagen3ModelsList: Imagen3Model[] = [ + { + value: 'imagen-3.0-capability-001', + viewValue: 'Imagen 3: imagen-3.0-capability-001', + }, + ]; + selectedModel = this.imagen3ModelsList[0].value; + selectedNumberOfResults = 2; + selectedMaskDistilation = 0.005; + + searchRequest: SearchRequest = { + term: '', + model: this.selectedModel, + numberOfResults: this.selectedNumberOfResults, + maskDistilation: this.selectedMaskDistilation, + }; + + constructor( + private router: Router, + private route: ActivatedRoute, + private service: SearchService, + private userService: UserService, + private _snackBar: MatSnackBar + ) { + const query = this.route.snapshot.queryParamMap.get('q'); + this.userService.showLoading(); + + if (!query) { + this.selectedResult = undefined; + this.showDefaultDocuments = true; + this.userService.hideLoading(); + return; + } + + this.currentSearchTerm = query; + const initialSearchRequest: SearchRequest = { + term: query, + model: this.selectedModel, + numberOfResults: this.selectedNumberOfResults, + maskDistilation: this.selectedMaskDistilation, + }; + this.searchTerm(initialSearchRequest); + } + + searchTerm({ + term, + model, + numberOfResults, + maskDistilation, + }: { + term?: string | undefined; + model?: string | undefined; + numberOfResults?: number | undefined; + maskDistilation?: number | undefined; + }) { + this.isLoading = true; + this.userService.showLoading(); + + this.searchRequest.term = term || this.searchRequest.term; + this.searchRequest.model = model || this.selectedModel; + this.searchRequest.numberOfResults = numberOfResults || this.selectedNumberOfResults; + this.searchRequest.maskDistilation = + maskDistilation || this.selectedMaskDistilation; + this.currentSearchTerm = this.searchRequest.term; + + // Reset previous results + this.geminiDocuments = []; + this.imagenEntireImageDocuments = []; + this.imagenBackgroundDocuments = []; + this.selectedResult = undefined; + this.summary = ''; + this.showDefaultDocuments = false; + + this.service.search(this.searchRequest).subscribe({ + next: (searchResponse: CombinedBackgroundChangerResults) => { + this.processSearchResults(searchResponse); + this.isLoading = false; + this.userService.hideLoading(); + }, + error: error => { + this.handleSearchError(error); + }, + }); + } + + private processSearchResults(searchResponse: CombinedBackgroundChangerResults) { + this.geminiDocuments = (searchResponse.geminiResults || []).map(img => ({ + ...img, + source: 'Gemini', + })); + this.imagenEntireImageDocuments = ( + searchResponse.imagenEntireImgResults || [] + ).map(img => ({ + ...img, + source: 'Imagen 3 - Entire Mode', + })); + this.imagenBackgroundDocuments = ( + searchResponse.imagenBackgroundImgResults || [] + ).map(img => ({ + ...img, + source: 'Imagen 3 - Background Mode', + })); + + const hasGeminiResults = this.geminiDocuments.length > 0; + const hasImagenEntireResults = this.imagenEntireImageDocuments.length > 0; + const hasImagenBackgroundResults = + this.imagenBackgroundDocuments.length > 0; + + if ( + hasGeminiResults || + hasImagenEntireResults || + hasImagenBackgroundResults + ) { + this.showDefaultDocuments = false; + // Select the first available image as the default selectedResult + if (hasImagenBackgroundResults) { + this.selectedResult = this.imagenBackgroundDocuments[0]; + } else if (hasImagenEntireResults) { + this.selectedResult = this.imagenEntireImageDocuments[0]; + } else if (hasGeminiResults) { + this.selectedResult = this.geminiDocuments[0]; + } + this.summary = + this.selectedResult?.enhancedPrompt || + 'Image generation results displayed.'; + } else { + this.showDefaultDocuments = true; + this.selectedResult = undefined; + this.summary = 'No images were generated for your prompt.'; + } + } + + private handleSearchError(error: any) { + console.error('Search error:', error); + this.geminiDocuments = []; + this.imagenEntireImageDocuments = []; + this.imagenBackgroundDocuments = []; + this.selectedResult = undefined; + this.showDefaultDocuments = true; + this.summary = 'An error occurred while generating images.'; + this.isLoading = false; + this.userService.hideLoading(); + this.showErrorSnackBar(error); + } + + showErrorSnackBar(error: any): void { + let errorMessage = 'Error sending request. Please try again later!'; + let triedToGeneratePersons = false; + + if (error?.error?.detail) { + if (typeof error.error.detail === 'string') { + if ( + error.error.detail.includes( + "The image you want to edit contains content that has been blocked because you selected the 'Don't allow' option for Person Generation." + ) + ) { + triedToGeneratePersons = true; + errorMessage = + 'The image you want to edit contains content that has been blocked because there are persons in it. See the safety settings documentation for more details.'; + } else { + errorMessage = error.error.detail; + } + } else if (Array.isArray(error.error.detail) && error.error.detail[0]?.msg) { + errorMessage = `${error.error.detail[0].msg} - ${error.error.detail[0].loc?.join(', ')}`; + } + } else if (error?.message) { + errorMessage = error.message; + } + + + this._snackBar.openFromComponent(ToastMessageComponent, { + panelClass: ['red-toast'], + verticalPosition: 'top', + horizontalPosition: 'right', + duration: 10000, + data: { + text: errorMessage, + icon: 'cross-in-circle-white', + }, + }); + if (triedToGeneratePersons) this.goToHomePage(); + } + + // goToResults(term: string) { + // if (term) { + // this.router.navigate(['/search'], {queryParams: {q: term}}); + // } + // } + + changeImageSelection(result: GeneratedImage) { + this.selectedResult = result; + this.summary = result.enhancedPrompt || ''; + } + + changeImagen3Model(model: Imagen3Model) { + this.selectedModel = model.value; + } + + onNumberOfResultsChange(event: Event) { + const value = Number((event.target as HTMLInputElement).value); + this.selectedNumberOfResults = value; + } + + onSliderChange(event: Event) { + const value = Number((event.target as HTMLInputElement).value); + this.selectedMaskDistilation = value; + } + + goToHomePage() { + this.router.navigate(['/']); + } + + ngOnDestroy() { + this.destroyed.next(); + this.destroyed.complete(); + } +} diff --git a/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/app/components/toast-message/toast-message.component.html b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/app/components/toast-message/toast-message.component.html new file mode 100644 index 0000000..a83d217 --- /dev/null +++ b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/app/components/toast-message/toast-message.component.html @@ -0,0 +1,26 @@ + + +
    + icon inside toast message + {{ text }} + cross icon inside toast message +
    diff --git a/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/app/components/toast-message/toast-message.component.scss b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/app/components/toast-message/toast-message.component.scss new file mode 100644 index 0000000..0d8177f --- /dev/null +++ b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/app/components/toast-message/toast-message.component.scss @@ -0,0 +1,37 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +.toast-content { + color: #ffffff; + font-family: 'Google Sans', sans-serif !important; +} + +.close-toast { + margin-left: auto; + align-self: baseline; + cursor: pointer; +} + +.red-toast { + background-color: #ed0c0c !important; +} +.green-toast { + background-color: #06865e !important; +} + +.mat-mdc-snack-bar-container .mdc-snackbar__surface { + background-color: transparent !important; +} diff --git a/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/app/components/toast-message/toast-message.component.spec.ts b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/app/components/toast-message/toast-message.component.spec.ts new file mode 100644 index 0000000..d0d405a --- /dev/null +++ b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/app/components/toast-message/toast-message.component.spec.ts @@ -0,0 +1,38 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {ComponentFixture, TestBed} from '@angular/core/testing'; + +import {ToastMessageComponent} from './toast-message.component'; + +describe('ToastMessageComponent', () => { + let component: ToastMessageComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ToastMessageComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(ToastMessageComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/app/components/toast-message/toast-message.component.ts b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/app/components/toast-message/toast-message.component.ts new file mode 100644 index 0000000..ebac36d --- /dev/null +++ b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/app/components/toast-message/toast-message.component.ts @@ -0,0 +1,40 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {Component, Inject, ViewEncapsulation} from '@angular/core'; +import {MatSnackBar, MAT_SNACK_BAR_DATA} from '@angular/material/snack-bar'; + +@Component({ + selector: 'app-toast-message', + templateUrl: './toast-message.component.html', + styleUrls: ['./toast-message.component.scss'], + encapsulation: ViewEncapsulation.None, +}) +export class ToastMessageComponent { + text: string; + icon: string; + constructor( + private _snackBar: MatSnackBar, + @Inject(MAT_SNACK_BAR_DATA) public snackBarData: any + ) { + this.text = snackBarData.text; + this.icon = snackBarData.icon; + } + + closeToast() { + this._snackBar.dismiss(); + } +} diff --git a/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/app/models/config.model.ts b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/app/models/config.model.ts new file mode 100644 index 0000000..fda930d --- /dev/null +++ b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/app/models/config.model.ts @@ -0,0 +1,20 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export type config = { + name: string; + url: string; +}; diff --git a/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/app/models/generated-image.model.ts b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/app/models/generated-image.model.ts new file mode 100644 index 0000000..b5413bf --- /dev/null +++ b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/app/models/generated-image.model.ts @@ -0,0 +1,29 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +type Image = { + gcsUri?: string; + imageBytes?: ArrayBuffer; + encodedImage?: string; + mimeType?: string; +}; + +export type GeneratedImage = { + image?: Image; + raiFilteredReason?: string; + enhancedPrompt?: string; + source?: string; +}; diff --git a/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/app/models/search.model.ts b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/app/models/search.model.ts new file mode 100644 index 0000000..4190d24 --- /dev/null +++ b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/app/models/search.model.ts @@ -0,0 +1,63 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { GeneratedImage } from "./generated-image.model"; + +export type SearchRequest = { + term: string; + model: string; + numberOfResults: number; + maskDistilation: number; +}; + +export type SearchResponse = { + summary: any; + results: SearchResult[]; + totalSize: number; +}; + +export interface CombinedBackgroundChangerResults { + geminiResults?: GeneratedImage[]; + imagenEntireImgResults?: GeneratedImage[]; + imagenBackgroundImgResults?: GeneratedImage[]; +} + +export type SearchResult = { + document: Document; +}; + +export type Document = { + derivedStructData: DocumentData; +}; + +export type DocumentData = { + title: string; + link: string; + snippets: Snippet[]; + pagemap: PageMap; +}; + +export type Snippet = { + snippet: string; +}; + +export type PageMap = { + cse_image: ImagesData[]; +}; + +export type ImagesData = { + src: string; +}; diff --git a/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/app/pipes/truncate.pipe.spec.ts b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/app/pipes/truncate.pipe.spec.ts new file mode 100644 index 0000000..82e65c6 --- /dev/null +++ b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/app/pipes/truncate.pipe.spec.ts @@ -0,0 +1,24 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {TruncatePipe} from './truncate.pipe'; + +describe('TruncatePipe', () => { + it('create an instance', () => { + const pipe = new TruncatePipe(); + expect(pipe).toBeTruthy(); + }); +}); diff --git a/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/app/pipes/truncate.pipe.ts b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/app/pipes/truncate.pipe.ts new file mode 100644 index 0000000..ae06a3b --- /dev/null +++ b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/app/pipes/truncate.pipe.ts @@ -0,0 +1,26 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {Pipe, PipeTransform} from '@angular/core'; + +@Pipe({ + name: 'truncate', +}) +export class TruncatePipe implements PipeTransform { + transform(value: string, limit = 300, trail = '...'): string { + return value.length > limit ? value.substring(0, limit) + trail : value; + } +} diff --git a/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/app/services/image/image.service.ts b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/app/services/image/image.service.ts new file mode 100644 index 0000000..85d2149 --- /dev/null +++ b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/app/services/image/image.service.ts @@ -0,0 +1,34 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {Injectable} from '@angular/core'; + +@Injectable({ + providedIn: 'root', +}) +export class ImageService { + private image: File | null = null; + + constructor() {} + + setImage(image: File) { + this.image = image; + } + + getImage(): File | null { + return this.image; + } +} diff --git a/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/app/services/login/auth.guard.spec.ts b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/app/services/login/auth.guard.spec.ts new file mode 100644 index 0000000..8284e7a --- /dev/null +++ b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/app/services/login/auth.guard.spec.ts @@ -0,0 +1,32 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {TestBed} from '@angular/core/testing'; + +import {AuthGuard} from './auth.guard'; + +describe('AuthGuard', () => { + let guard: AuthGuard; + + beforeEach(() => { + TestBed.configureTestingModule({}); + guard = TestBed.inject(AuthGuard); + }); + + it('should be created', () => { + expect(guard).toBeTruthy(); + }); +}); diff --git a/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/app/services/login/auth.guard.ts b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/app/services/login/auth.guard.ts new file mode 100644 index 0000000..fd95abd --- /dev/null +++ b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/app/services/login/auth.guard.ts @@ -0,0 +1,54 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {Injectable} from '@angular/core'; +import { + ActivatedRouteSnapshot, + CanActivate, + Router, + RouterStateSnapshot, + UrlTree, +} from '@angular/router'; +import {Observable} from 'rxjs'; +import {AuthService} from './auth.service'; +import {environment} from 'src/environments/environment'; + +const LOGIN_ROUTE = '/login'; + +@Injectable({ + providedIn: 'root', +}) +export class AuthGuard implements CanActivate { + constructor( + private auth: AuthService, + private router: Router + ) {} + + canActivate( + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot + ): + | Observable + | Promise + | boolean + | UrlTree { + if (environment.requiredLogin === 'True' && !this.auth.isLoggedIn()) { + this.router.navigate([LOGIN_ROUTE]); + return false; + } + return true; + } +} diff --git a/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/app/services/login/auth.service.spec.ts b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/app/services/login/auth.service.spec.ts new file mode 100644 index 0000000..f08aa1a --- /dev/null +++ b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/app/services/login/auth.service.spec.ts @@ -0,0 +1,32 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {TestBed} from '@angular/core/testing'; + +import {AuthService} from './auth.service'; + +describe('AuthService', () => { + let service: AuthService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(AuthService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/app/services/login/auth.service.ts b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/app/services/login/auth.service.ts new file mode 100644 index 0000000..325d42e --- /dev/null +++ b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/app/services/login/auth.service.ts @@ -0,0 +1,57 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {Injectable} from '@angular/core'; +import {Router} from '@angular/router'; +import {environment} from 'src/environments/environment'; + +const USER_TOKEN_KEY = 'gpau_id'; +const USER_DETAILS = 'USER_DETAILS'; +const LOGIN_ROUTE = '/login'; + +@Injectable({ + providedIn: 'root', +}) +export class AuthService { + private requiredLogin: boolean = environment.requiredLogin === 'True'; + + constructor(private router: Router) {} + + saveUserSession(token: string) { + localStorage.setItem(USER_TOKEN_KEY, token); + } + + logout(route: string = LOGIN_ROUTE) { + localStorage.removeItem(USER_TOKEN_KEY); + localStorage.removeItem(USER_DETAILS); + localStorage.removeItem('showTooltip'); + this.router.navigateByUrl(route); + } + + isLoggedIn() { + const isLoggedIn = localStorage.getItem(USER_TOKEN_KEY) !== null; + if (!isLoggedIn && this.router.url !== LOGIN_ROUTE) { + this.router.navigate([LOGIN_ROUTE]); + } + return isLoggedIn; + } + + isUserLoggedIn() { + if (!this.requiredLogin) return true; + const isLoggedIn = localStorage.getItem(USER_TOKEN_KEY) !== null; + return isLoggedIn; + } +} diff --git a/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/app/services/search.service.spec.ts b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/app/services/search.service.spec.ts new file mode 100644 index 0000000..b024e3b --- /dev/null +++ b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/app/services/search.service.spec.ts @@ -0,0 +1,32 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {TestBed} from '@angular/core/testing'; + +import {SearchService} from './search.service'; + +describe('SearchService', () => { + let service: SearchService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(SearchService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/app/services/search.service.ts b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/app/services/search.service.ts new file mode 100644 index 0000000..eadf501 --- /dev/null +++ b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/app/services/search.service.ts @@ -0,0 +1,65 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {HttpClient} from '@angular/common/http'; +import {Injectable} from '@angular/core'; +import {environment} from 'src/environments/environment'; +import {map} from 'rxjs/operators'; +import {CombinedBackgroundChangerResults, SearchRequest} from '../models/search.model'; +import {GeneratedImage} from '../models/generated-image.model'; +import {ImageService} from './image/image.service'; +import {Router} from '@angular/router'; +import {Observable, throwError} from 'rxjs'; + +const searchURL = `${environment.backendURL}/search`; + +@Injectable({ + providedIn: 'root', +}) +export class SearchService { + constructor( + private http: HttpClient, + private imageService: ImageService, + private router: Router + ) {} + + search(searchRequest: SearchRequest): Observable { + const userImage = this.imageService.getImage(); + + if (!userImage) { + // Redirect to homepage and alert that you need to upload an image first + this.router.navigate(['/']); + alert('Please upload an image first.'); + return throwError( + () => new Error('No image and no search term provided.') + ); + } + + const formData = new FormData(); + formData.append('term', searchRequest.term); + formData.append('generationModel', searchRequest.model); + formData.append('numberOfImages', searchRequest.numberOfResults.toString()); + formData.append( + 'maskDistilation', + searchRequest.maskDistilation.toString() + ); + formData.append('userImage', userImage, userImage.name); + + return this.http + .post(searchURL, formData) + .pipe(map(response => response as CombinedBackgroundChangerResults)); + } +} diff --git a/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/app/services/speech-to-text.ts b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/app/services/speech-to-text.ts new file mode 100644 index 0000000..16b1469 --- /dev/null +++ b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/app/services/speech-to-text.ts @@ -0,0 +1,34 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {Injectable} from '@angular/core'; +import {HttpClient} from '@angular/common/http'; +import {environment} from 'src/environments/environment'; + +const audioChatUrl = `${environment.backendURL}/audio_chat`; + +@Injectable({ + providedIn: 'root', +}) +export class SpeechToTextService { + constructor(private http: HttpClient) {} + + async transcribeAudio(audioBlob: Blob) { + const formData = new FormData(); + formData.append('audio_file', audioBlob, 'audio.wav'); + return this.http.post(audioChatUrl, formData); + } +} diff --git a/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/app/services/user/session.service.spec.ts b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/app/services/user/session.service.spec.ts new file mode 100644 index 0000000..ff45965 --- /dev/null +++ b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/app/services/user/session.service.spec.ts @@ -0,0 +1,32 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {TestBed} from '@angular/core/testing'; + +import {SessionService} from './session.service'; + +describe('SessionService', () => { + let service: SessionService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(SessionService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/app/services/user/session.service.ts b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/app/services/user/session.service.ts new file mode 100644 index 0000000..4a51a17 --- /dev/null +++ b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/app/services/user/session.service.ts @@ -0,0 +1,35 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {Injectable} from '@angular/core'; +import {v4 as uuid} from 'uuid'; + +const SESSION_KEY = 'pasid'; + +@Injectable({ + providedIn: 'root', +}) +export class SessionService { + constructor() {} + + getSession(): string | null { + return sessionStorage.getItem(SESSION_KEY); + } + + createSession() { + sessionStorage.setItem(SESSION_KEY, uuid()); + } +} diff --git a/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/app/services/user/user.service.ts b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/app/services/user/user.service.ts new file mode 100644 index 0000000..cf426f6 --- /dev/null +++ b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/app/services/user/user.service.ts @@ -0,0 +1,61 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {Injectable} from '@angular/core'; +import {BehaviorSubject} from 'rxjs'; +import {environment} from 'src/environments/environment'; + +type UserStored = { + uid?: string; + name?: string; + email?: string; + photoURL?: string; + displayName?: string; + domain?: string; +}; + +@Injectable({ + providedIn: 'root', +}) +export class UserService { + readonly loadingSubject = new BehaviorSubject(false); + requiredLogin: string = environment.requiredLogin; + + constructor() {} + + setUserDetails(userStored: UserStored) {} + + showLoading() { + this.loadingSubject.next(true); + } + + hideLoading() { + this.loadingSubject.next(false); + } + + getUserDetails(): UserStored { + if ( + this.requiredLogin === 'True' && + localStorage.getItem('USER_DETAILS') !== null + ) { + const userObj = localStorage.getItem('USER_DETAILS'); + return JSON.parse(userObj || '{}'); + } else { + const userDetails: UserStored = {}; + return userDetails; + } + } +} diff --git a/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/assets/.gitkeep b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/assets/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/assets/gcloud-logo.svg b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/assets/gcloud-logo.svg new file mode 100644 index 0000000..5150056 --- /dev/null +++ b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/assets/gcloud-logo.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/assets/images/SocialHeaderTemplate8.png b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/assets/images/SocialHeaderTemplate8.png new file mode 100644 index 0000000..88f4306 Binary files /dev/null and b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/assets/images/SocialHeaderTemplate8.png differ diff --git a/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/assets/images/answernotfound.png b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/assets/images/answernotfound.png new file mode 100644 index 0000000..874c87d Binary files /dev/null and b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/assets/images/answernotfound.png differ diff --git a/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/assets/images/attached.svg b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/assets/images/attached.svg new file mode 100644 index 0000000..ba09784 --- /dev/null +++ b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/assets/images/attached.svg @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/assets/images/available.svg b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/assets/images/available.svg new file mode 100644 index 0000000..5a460d0 --- /dev/null +++ b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/assets/images/available.svg @@ -0,0 +1,4 @@ + + + + diff --git a/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/assets/images/badge-banner.svg b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/assets/images/badge-banner.svg new file mode 100644 index 0000000..a38efec --- /dev/null +++ b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/assets/images/badge-banner.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/assets/images/badge.svg b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/assets/images/badge.svg new file mode 100644 index 0000000..3fc6b94 --- /dev/null +++ b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/assets/images/badge.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/assets/images/books.png b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/assets/images/books.png new file mode 100644 index 0000000..94feabc Binary files /dev/null and b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/assets/images/books.png differ diff --git a/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/assets/images/bot_logo.svg b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/assets/images/bot_logo.svg new file mode 100644 index 0000000..4be6c2f --- /dev/null +++ b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/assets/images/bot_logo.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/assets/images/cancel-toast.svg b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/assets/images/cancel-toast.svg new file mode 100644 index 0000000..9db4780 --- /dev/null +++ b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/assets/images/cancel-toast.svg @@ -0,0 +1,4 @@ + + + + diff --git a/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/assets/images/chatIcon.svg b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/assets/images/chatIcon.svg new file mode 100644 index 0000000..3303847 --- /dev/null +++ b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/assets/images/chatIcon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/assets/images/chat_background.png b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/assets/images/chat_background.png new file mode 100644 index 0000000..701ec81 Binary files /dev/null and b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/assets/images/chat_background.png differ diff --git a/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/assets/images/colibri_image_fx.jpg b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/assets/images/colibri_image_fx.jpg new file mode 100644 index 0000000..94eff87 Binary files /dev/null and b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/assets/images/colibri_image_fx.jpg differ diff --git a/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/assets/images/crane_image_fx.jpg b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/assets/images/crane_image_fx.jpg new file mode 100644 index 0000000..b6d1c73 Binary files /dev/null and b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/assets/images/crane_image_fx.jpg differ diff --git a/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/assets/images/cross-in-circle-white copy.svg b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/assets/images/cross-in-circle-white copy.svg new file mode 100644 index 0000000..96a0fe3 --- /dev/null +++ b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/assets/images/cross-in-circle-white copy.svg @@ -0,0 +1,3 @@ + + + diff --git a/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/assets/images/cross-in-circle-white.svg b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/assets/images/cross-in-circle-white.svg new file mode 100644 index 0000000..96a0fe3 --- /dev/null +++ b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/assets/images/cross-in-circle-white.svg @@ -0,0 +1,3 @@ + + + diff --git a/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/assets/images/cross-in-circle.svg b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/assets/images/cross-in-circle.svg new file mode 100644 index 0000000..c5c50ea --- /dev/null +++ b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/assets/images/cross-in-circle.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/assets/images/delete_notification_dialog_image.png b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/assets/images/delete_notification_dialog_image.png new file mode 100644 index 0000000..5afbf5c Binary files /dev/null and b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/assets/images/delete_notification_dialog_image.png differ diff --git a/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/assets/images/doc.png b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/assets/images/doc.png new file mode 100644 index 0000000..0b21898 Binary files /dev/null and b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/assets/images/doc.png differ diff --git a/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/assets/images/error_img.svg b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/assets/images/error_img.svg new file mode 100644 index 0000000..ac20193 --- /dev/null +++ b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/assets/images/error_img.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/assets/images/feedback-container.svg b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/assets/images/feedback-container.svg new file mode 100644 index 0000000..7b0f481 --- /dev/null +++ b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/assets/images/feedback-container.svg @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/assets/images/footer.png b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/assets/images/footer.png new file mode 100644 index 0000000..eda0b3b Binary files /dev/null and b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/assets/images/footer.png differ diff --git a/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/assets/images/gcloudlight1.png b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/assets/images/gcloudlight1.png new file mode 100644 index 0000000..5ddb77c Binary files /dev/null and b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/assets/images/gcloudlight1.png differ diff --git a/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/assets/images/gnosis-logo.svg b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/assets/images/gnosis-logo.svg new file mode 100644 index 0000000..17d0b7f --- /dev/null +++ b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/assets/images/gnosis-logo.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/assets/images/gsd-icon.png b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/assets/images/gsd-icon.png new file mode 100644 index 0000000..abeacf9 Binary files /dev/null and b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/assets/images/gsd-icon.png differ diff --git a/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/assets/images/home_bottom.png b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/assets/images/home_bottom.png new file mode 100644 index 0000000..78d1c92 Binary files /dev/null and b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/assets/images/home_bottom.png differ diff --git a/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/assets/images/hot_air_baloon_image_fx.jpg b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/assets/images/hot_air_baloon_image_fx.jpg new file mode 100644 index 0000000..19064b4 Binary files /dev/null and b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/assets/images/hot_air_baloon_image_fx.jpg differ diff --git a/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/assets/images/img.png b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/assets/images/img.png new file mode 100644 index 0000000..53bc884 Binary files /dev/null and b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/assets/images/img.png differ diff --git a/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/assets/images/left_loader_images/Left/1.png b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/assets/images/left_loader_images/Left/1.png new file mode 100644 index 0000000..d930ffb Binary files /dev/null and b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/assets/images/left_loader_images/Left/1.png differ diff --git a/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/assets/images/left_loader_images/Left/2.png b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/assets/images/left_loader_images/Left/2.png new file mode 100644 index 0000000..ac32f6a Binary files /dev/null and b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/assets/images/left_loader_images/Left/2.png differ diff --git a/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/assets/images/left_loader_images/Left/3.png b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/assets/images/left_loader_images/Left/3.png new file mode 100644 index 0000000..cee4db0 Binary files /dev/null and b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/assets/images/left_loader_images/Left/3.png differ diff --git a/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/assets/images/left_loader_images/Left/4.png b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/assets/images/left_loader_images/Left/4.png new file mode 100644 index 0000000..77a6d94 Binary files /dev/null and b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/assets/images/left_loader_images/Left/4.png differ diff --git a/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/assets/images/left_loader_images/Left/5.png b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/assets/images/left_loader_images/Left/5.png new file mode 100644 index 0000000..deba49b Binary files /dev/null and b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/assets/images/left_loader_images/Left/5.png differ diff --git a/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/assets/images/left_loader_images/Left/6.png b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/assets/images/left_loader_images/Left/6.png new file mode 100644 index 0000000..61e1988 Binary files /dev/null and b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/assets/images/left_loader_images/Left/6.png differ diff --git a/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/assets/images/login-globe.png b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/assets/images/login-globe.png new file mode 100644 index 0000000..f64021b Binary files /dev/null and b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/assets/images/login-globe.png differ diff --git a/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/assets/images/login_left.svg b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/assets/images/login_left.svg new file mode 100644 index 0000000..397f17f --- /dev/null +++ b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/assets/images/login_left.svg @@ -0,0 +1,101 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/assets/images/new-pdc-logo.svg b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/assets/images/new-pdc-logo.svg new file mode 100644 index 0000000..c267930 --- /dev/null +++ b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/assets/images/new-pdc-logo.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/assets/images/noPic.svg b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/assets/images/noPic.svg new file mode 100644 index 0000000..0f9c09d --- /dev/null +++ b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/assets/images/noPic.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/assets/images/pdf.png b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/assets/images/pdf.png new file mode 100644 index 0000000..a8c471f Binary files /dev/null and b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/assets/images/pdf.png differ diff --git a/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/assets/images/phoneIcon.png b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/assets/images/phoneIcon.png new file mode 100644 index 0000000..a0af511 Binary files /dev/null and b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/assets/images/phoneIcon.png differ diff --git a/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/assets/images/placeholder_image.png b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/assets/images/placeholder_image.png new file mode 100644 index 0000000..f2ead09 Binary files /dev/null and b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/assets/images/placeholder_image.png differ diff --git a/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/assets/images/right_loader_images/Right/1.png b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/assets/images/right_loader_images/Right/1.png new file mode 100644 index 0000000..c571db1 Binary files /dev/null and b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/assets/images/right_loader_images/Right/1.png differ diff --git a/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/assets/images/right_loader_images/Right/2.png b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/assets/images/right_loader_images/Right/2.png new file mode 100644 index 0000000..d6a1da0 Binary files /dev/null and b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/assets/images/right_loader_images/Right/2.png differ diff --git a/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/assets/images/right_loader_images/Right/3.png b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/assets/images/right_loader_images/Right/3.png new file mode 100644 index 0000000..042ba23 Binary files /dev/null and b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/assets/images/right_loader_images/Right/3.png differ diff --git a/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/assets/images/right_loader_images/Right/4.png b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/assets/images/right_loader_images/Right/4.png new file mode 100644 index 0000000..96c067a Binary files /dev/null and b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/assets/images/right_loader_images/Right/4.png differ diff --git a/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/assets/images/right_loader_images/Right/5.png b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/assets/images/right_loader_images/Right/5.png new file mode 100644 index 0000000..3a15579 Binary files /dev/null and b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/assets/images/right_loader_images/Right/5.png differ diff --git a/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/assets/images/right_loader_images/Right/6.png b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/assets/images/right_loader_images/Right/6.png new file mode 100644 index 0000000..7de6698 Binary files /dev/null and b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/assets/images/right_loader_images/Right/6.png differ diff --git a/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/assets/images/searchIcon.png b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/assets/images/searchIcon.png new file mode 100644 index 0000000..7b95f3c Binary files /dev/null and b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/assets/images/searchIcon.png differ diff --git a/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/assets/images/searchIcon.svg b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/assets/images/searchIcon.svg new file mode 100644 index 0000000..b82fe2f --- /dev/null +++ b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/assets/images/searchIcon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/assets/images/support-page-banner.svg b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/assets/images/support-page-banner.svg new file mode 100644 index 0000000..0fa6347 --- /dev/null +++ b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/assets/images/support-page-banner.svg @@ -0,0 +1,134 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/assets/images/tick-with-circle.svg b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/assets/images/tick-with-circle.svg new file mode 100644 index 0000000..8335e7c --- /dev/null +++ b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/assets/images/tick-with-circle.svg @@ -0,0 +1,4 @@ + + + + diff --git a/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/assets/images/unnamed.jpeg b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/assets/images/unnamed.jpeg new file mode 100644 index 0000000..b33f75d Binary files /dev/null and b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/assets/images/unnamed.jpeg differ diff --git a/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/assets/images/user-logo.svg b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/assets/images/user-logo.svg new file mode 100644 index 0000000..8135e28 --- /dev/null +++ b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/assets/images/user-logo.svg @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/assets/images/voiceIcon.svg b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/assets/images/voiceIcon.svg new file mode 100644 index 0000000..9c3e1d3 --- /dev/null +++ b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/assets/images/voiceIcon.svg @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/assets/images/warning-yellow.png b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/assets/images/warning-yellow.png new file mode 100644 index 0000000..e94f265 Binary files /dev/null and b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/assets/images/warning-yellow.png differ diff --git a/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/assets/videos/Imagen3_Header.mp4 b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/assets/videos/Imagen3_Header.mp4 new file mode 100644 index 0000000..f919cc8 Binary files /dev/null and b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/assets/videos/Imagen3_Header.mp4 differ diff --git a/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/environments/constant.ts b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/environments/constant.ts new file mode 100644 index 0000000..8edf1a8 --- /dev/null +++ b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/environments/constant.ts @@ -0,0 +1,25 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export const search_document_type = ['pdf', 'docx', 'doc']; + +export const search_image_type = ['jpg', 'jpeg', 'png']; + +export const image_name = new Map() + .set('doc', 'assets/images/doc.png') + .set('pdf', 'assets/images/pdf.png'); + +export const PDF = 'pdf'; diff --git a/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/environments/environment.ts b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/environments/environment.ts new file mode 100644 index 0000000..3b04dba --- /dev/null +++ b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/environments/environment.ts @@ -0,0 +1,31 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export const environment = { + firebase: { + apiKey: '', + authDomain: '', + projectId: '', + storageBucket: '', + messagingSenderId: '', + appId: '', + measurementId: '', + }, + requiredLogin: 'False', + backendURL: 'http://localhost:8080/api', + chatbotName: 'My New Agent', + environment: 'development', +}; diff --git a/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/favicon.ico b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/favicon.ico new file mode 100644 index 0000000..997406a Binary files /dev/null and b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/favicon.ico differ diff --git a/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/index.html b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/index.html new file mode 100644 index 0000000..799de59 --- /dev/null +++ b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/index.html @@ -0,0 +1,48 @@ + + + + + + + Quick Bot + + + + + + + + + + + + + + + diff --git a/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/main.ts b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/main.ts new file mode 100644 index 0000000..0da5f95 --- /dev/null +++ b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/main.ts @@ -0,0 +1,23 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {platformBrowserDynamic} from '@angular/platform-browser-dynamic'; + +import {AppModule} from './app/app.module'; + +platformBrowserDynamic() + .bootstrapModule(AppModule) + .catch(err => console.error(err)); diff --git a/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/styles.scss b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/styles.scss new file mode 100644 index 0000000..b4b1118 --- /dev/null +++ b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/src/styles.scss @@ -0,0 +1,86 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* You can add global styles to this file, and also import other style files */ + +html, +body { + height: 100%; + margin: 0; + font-family: 'Google Sans', sans-serif !important; + overflow: hidden; +} + +.material-symbols-rounded { + font-variation-settings: + 'FILL', + 'wght' 600, + 'GRAD' 0, + 'opsz' 48; +} + +.tooltip-list .mdc-tooltip__surface { + white-space: pre; + max-width: unset !important; +} + +.mat-mdc-slide-toggle.mat-accent { + --mdc-switch-selected-focus-state-layer-color: #4285f4; + --mdc-switch-selected-handle-color: #4285f4; + --mdc-switch-selected-hover-state-layer-color: #4285f4; + --mdc-switch-selected-pressed-state-layer-color: #4285f4; + --mdc-switch-selected-focus-handle-color: #3f74e0; + --mdc-switch-selected-hover-handle-color: #3f74e0; + --mdc-switch-selected-pressed-handle-color: #3f74e0; + --mdc-switch-selected-focus-track-color: rgba(66, 133, 244, 0.3); + --mdc-switch-selected-hover-track-color: rgba(66, 133, 244, 0.3); + --mdc-switch-selected-pressed-track-color: rgba(66, 133, 244, 0.3); + --mdc-switch-selected-track-color: rgba(66, 133, 244, 0.3); +} + +.arrow-top { + box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.1) !important; + border-radius: 20px !important; +} + +.mat-mdc-checkbox.mat-accent { + --mdc-checkbox-selected-checkmark-color: #fff; + --mdc-checkbox-selected-focus-icon-color: #4285f4; + --mdc-checkbox-selected-hover-icon-color: #4285f4; + --mdc-checkbox-selected-icon-color: #4285f4; + --mdc-checkbox-selected-pressed-icon-color: #4285f4; + --mdc-checkbox-unselected-focus-icon-color: #212121; + --mdc-checkbox-unselected-hover-icon-color: #212121; + --mdc-checkbox-disabled-selected-icon-color: rgba(0, 0, 0, 0.38); + --mdc-checkbox-disabled-unselected-icon-color: rgba(0, 0, 0, 0.38); + --mdc-checkbox-unselected-icon-color: rgba(0, 0, 0, 0.54); + --mdc-checkbox-unselected-pressed-icon-color: rgba(0, 0, 0, 0.54); +} + +$tour-skip-link-color: red; +$tour-text-color: purple; +$tour-next-text-color: yellow; +$tour-zindex: 100; +$tour-orb-color: cyan; +$tour-next-button-color: green; +$tour-next-button-hover: black; +$tour-back-button-color: white; +$tour-shadow-color: gray; +$tour-step-color: aliceblue; + +@tailwind base; +@tailwind components; +@tailwind utilities; diff --git a/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/tailwind.config.js b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/tailwind.config.js new file mode 100644 index 0000000..8f72439 --- /dev/null +++ b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/tailwind.config.js @@ -0,0 +1,30 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** @type {import('tailwindcss').Config} */ +module.exports = { + content: ['./src/**/*.{html,ts}'], + theme: { + extend: { + colors: { + 'custom-gray': '#DDE6F6', + 'light-gray': '#e0e0e0', + 'lighter-gray': '#f0f0f0', + }, + }, + }, + plugins: [], +}; diff --git a/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/tsconfig.app.json b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/tsconfig.app.json new file mode 100644 index 0000000..374cc9d --- /dev/null +++ b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/tsconfig.app.json @@ -0,0 +1,14 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "./out-tsc/app", + "types": [] + }, + "files": [ + "src/main.ts" + ], + "include": [ + "src/**/*.d.ts" + ] +} diff --git a/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/tsconfig.json b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/tsconfig.json new file mode 100644 index 0000000..fc49b39 --- /dev/null +++ b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/tsconfig.json @@ -0,0 +1,34 @@ +{ + "extends": "./node_modules/gts/tsconfig-google.json", + "compileOnSave": false, + "compilerOptions": { + "ignoreDeprecations": "5.0", + "baseUrl": "./", + "outDir": "./dist/out-tsc", + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "downlevelIteration": true, + "experimentalDecorators": true, + "moduleResolution": "node", + "importHelpers": true, + "target": "ES2022", + "module": "ES2022", + "useDefineForClassFields": false, + "lib": [ + "ES2022", + "dom" + ], + }, + "angularCompilerOptions": { + "enableI18nLegacyMessageIdFormat": false, + "strictInjectionParameters": true, + "strictInputAccessModifiers": true, + "strictTemplates": true, + "suppressImplicitAnyIndexErrors": true, + "strictPropertyInitialization": false, + }, + "include": [ + "src/**/*.ts", + "test/**/*.ts" + ] +} diff --git a/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/tsconfig.spec.json b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/tsconfig.spec.json new file mode 100644 index 0000000..be7e9da --- /dev/null +++ b/gemini/sample-apps/quickbot/image-background-changer-using-imagen3/frontend/tsconfig.spec.json @@ -0,0 +1,14 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "./out-tsc/spec", + "types": [ + "jasmine" + ] + }, + "include": [ + "src/**/*.spec.ts", + "src/**/*.d.ts" + ] +} diff --git a/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/.dockerignore b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/.dockerignore new file mode 100644 index 0000000..d472d1d --- /dev/null +++ b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/.dockerignore @@ -0,0 +1,12 @@ +node_modules +npm-debug.log +tests +coverage +Dockerfile +.dockerignore +.env +*.md +.git +.vscode +.gitignore +Makefile \ No newline at end of file diff --git a/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/README.md b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/README.md new file mode 100644 index 0000000..7d3d8be --- /dev/null +++ b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/README.md @@ -0,0 +1,276 @@ +# Quickbot - LinkedIn Templates using Imagen3 + +Quickbot LinkedIn Templates is an innovative application built with Google Cloud's Imagen3 model on Vertex AI. It provides a user-friendly frontend interface and a robust backend API to generate engaging images for LinkedIn posts using customizable templates and text prompts. + +## Overview + +This project allows users to create professional and eye-catching images for LinkedIn posts by combining text prompts with pre-designed templates, all powered by Google Cloud's Imagen3 model on Vertex AI. It's designed with a decoupled frontend and backend architecture, suitable for scalable deployments. + +## Demo + +See how the LinkedIn Profile Image Generation Template helps create professional photos: + +![LinkedIn Profile Image Demo](./assets/quickbot-linkedin-agent-photo.png) + +## Prerequisites + +Before you begin, ensure you have the following installed: + +* **Docker and Docker Compose v2:** Essential for the containerized deployment. + * Verify your Docker Compose version with `docker compose version`. If you have an older `docker-compose` (with a hyphen), you might need to upgrade to use `docker compose` in the commands. +* **Google Cloud SDK (`gcloud` CLI):** Required for manual backend setup, authentication, and interaction with Google Cloud services (like Vertex AI for Imagen3). +* **Python 3.x:** For backend development (if not using Docker). +* **Node.js and npm (or yarn):** For frontend development (if not using Docker). + +## Getting Started + +You have two main options to get the application running: + +### Option 1: Using Docker Compose (Recommended for Quick Start) + +This is the **simplest way to get the entire application (frontend and backend) up and running!** You just need to run `docker compose up` after initial setup. See the next steps: + +1. **Ensure Docker and Docker Compose v2 are installed and running.** + +2. **Authenticate with Google Cloud for Vertex AI Imagen Access:** + For the application (specifically the backend running in Docker) to interact with Google Cloud's Imagen3 on Vertex AI, you need to provide Google Cloud credentials to the Docker container. The recommended way for local development is using Application Default Credentials (ADC). + Run the following commands in your local terminal: + ```bash + gcloud auth application-default login + gcloud config set project + gcloud auth application-default set-quota-project + + # Verify your configuration + gcloud auth list + gcloud config list project + ``` + This will create or update ADC on your local machine. The `docker-compose.yml` file is typically configured to mount these local credentials into the backend container, allowing it to authenticate. + > **Windows Users:** The path to ADC might differ on Windows. You may need to adjust the commented-out volume mount paths for `gcloud` credentials in the `docker-compose.yml` file to ensure the backend container can access them. + > **Note:** Ensure the Vertex AI API is enabled in your Google Cloud project. + +3. **Build Docker Images:** + Build the Docker images for the frontend and backend services: + ```bash + docker compose build + ``` + The backend will be configured using environment variables (see "Environment Variables" section) to connect to Google Cloud services, including Imagen3 on Vertex AI. + +4. **Run the application:** + After building the images, start the services: + ```bash + docker compose up + ``` + The frontend should typically be available at `http://localhost:4200` (or as configured) and the backend API at `http://localhost:8080`. + +### Option 2: Manual Setup (for Development and Customization) + +Follow these steps if you prefer to run the frontend and backend services manually on your local machine. + +**A. Backend Setup** + +1. **Navigate to the `backend/` directory.** + ```bash + cd backend + ``` + +2. **Create a virtual environment and install dependencies:** + ```bash + # Check if you are already in an environment + pip -V + + # If not, create and activate (for Linux/macOS) + python3 -m venv .venv + source .venv/bin/activate + + # Install requirements + pip3 install -r requirements.txt + ``` + > **VS Code Tip:** If VS Code doesn't recognize your virtual environment, press `Ctrl + Shift + P` (or `Cmd + Shift + P` on Mac), type "Python: Select Interpreter", choose "Enter interpreter path...", and then find and select `.venv/bin/python` inside your `backend` directory. + +3. **Setup Google Cloud (`gcloud`) credentials:** + Ensure you're authenticated and your project is configured correctly. The Vertex AI API should be enabled in your project. + ```bash + gcloud auth login # Login with your user account + gcloud config set project + + # For services using Application Default Credentials (ADC) locally + gcloud auth application-default login + # Optionally, set a quota project for ADC if not inherited + gcloud auth application-default set-quota-project + + # Verify configuration + gcloud auth list + gcloud config list project + ``` + +4. **Configure Environment Variables:** + Backend configuration is managed via environment variables. Create a `.local.env` file in the `backend/` directory (you can copy from `.local.env.example` if one exists). This file should be added to `.gitignore`. + + * **For Mac/Windows (or zsh console on Linux):** + Source the variables directly (from the `backend/` directory): + ```bash + . ./.local.env + ``` + * **For Linux (bash):** + Open `backend/.venv/bin/activate` and append the `export` commands from your `backend/.local.env` file after the `PATH` export section. For example: + ```sh + # ... existing activate script content ... + _OLD_VIRTUAL_PATH="$PATH" + PATH="$VIRTUAL_ENV/bin:$PATH" + export PATH + + # Quickbot env variables (copied from .local.env) + export ENVIRONMENT="development" + export FRONTEND_URL="http://localhost:4200" + export GCP_PROJECT_ID="" + export VERTEX_AI_LOCATION="us-central1" # Example location + export IMAGEN_MODEL_ID="imagegeneration@006" # Example model + # ... other necessary variables ... + ``` + Verify the variables are set by running `env` in your activated terminal. + +5. **Run the setup script (if applicable):** + This script might perform initial configurations if needed by the backend. + ```bash + # from the backend/ directory + python3 setup.py + ``` + +6. **Run the backend application:** + ```bash + # from the backend/ directory + uvicorn main:app --reload --port 8080 + ``` + +**B. Frontend Setup** + +(These instructions assume a typical TypeScript/Angular frontend. Adjust as necessary based on your `frontend/README.md`.) + +1. **Navigate to the `frontend/` directory.** + ```bash + cd frontend + ``` +2. **Install dependencies:** + ```bash + npm install + ``` +3. **Environment Variables (if applicable):** + The frontend might require its own environment configuration (e.g., via a `.env` file or Angular's `environment.ts` files for API endpoints). Check the `frontend/` directory or its `README.md` for specific instructions. +4. **Run the frontend application:** + ```bash + npm start + # Or, for many Angular projects: + # ng serve + ``` + The application will typically be available at `http://localhost:4200`. + +## Project Structure (highlighting important parts) +```text +text-to-imagen-using-imagen3/ +├── backend/ # Python backend (FastAPI/Uvicorn) +│ ├── .venv/ # Python virtual environment (gitignored) +│ ├── .local.env # Local environment variables (gitignored) +│ ├── main.py # Main application file +│ ├── requirements.txt # Backend dependencies +│ ├── setup.py # Backend setup script +│ └── README.md # Backend-specific instructions +├── frontend/ # TypeScript frontend (Angular) +│ ├── node_modules/ # Node.js dependencies (gitignored) +│ ├── src/ # Frontend source code +│ ├── package.json # Frontend dependencies and scripts +│ ├── tsconfig.json # TypeScript configuration +│ └── README.md # Frontend-specific instructions +├── docker-compose.yml # Docker Compose configuration for all services +└── README.md # This file: Root project README +``` + +## Environment Variables + +Configuration for both frontend and backend is primarily managed through environment variables. + +* **Backend:** + * When running manually, backend environment variables are typically defined in `backend/.local.env`. + * When running with Docker, these variables are usually passed into the backend container via the `docker-compose.yml` file (often referencing a `.env` file at the root or `backend/` directory). + * Key variables include: + * `GCLOUD_PROJECT`: Your Google Cloud Project ID. + * `ENVIRONMENT`: Application environment (e.g., `development`, `production`). + * `FRONTEND_URL`: URL of the frontend application (e.g., `http://localhost:4200`). + * Consult `backend/README.md` or `backend/.local.env.example` for a complete list. + +* **Frontend:** + * Frontend environment variables (e.g., API endpoint URLs) are usually managed within the frontend's build system (e.g., Angular's `environment.ts` files or a `.env` file in the `frontend/` directory). + * Consult `frontend/README.md` for specific details. + +## Code Styling & Commit Guidelines + +To maintain code quality and consistency across the project: + +* **TypeScript (Frontend):** We follow the Angular Coding Style Guide by leveraging Google's TypeScript Style Guide using `gts`. This includes a formatter, linter, and automatic code fixer. +* **Python (Backend):** We adhere to the Google Python Style Guide, using tools like `pylint` and `black` for linting and formatting. +* **Commit Messages:** We suggest following Angular's Commit Message Guidelines to create clear and descriptive commit messages. + +### Frontend (TypeScript with `gts`) + +(Assumes setup within the `frontend/` directory) + +1. **Initialize `gts` (if not already done in the project):** + Navigate to `frontend/` and run: + ```bash + npx gts init + ``` + This will set up `gts` and create necessary configuration files (like `tsconfig.json`). Ensure your `tsconfig.json` (or a related `gts` config file like `.gtsrc`) includes an extension for `gts` defaults, typically: + ```json + { + "extends": "./node_modules/gts/tsconfig-google.json" + // ... other configurations + } + ``` +2. **Check for linting issues:** + (This assumes a `lint` script is defined in `frontend/package.json`, e.g., `"lint": "gts lint"`) + ```bash + # from frontend/ directory + npm run lint + ``` +3. **Fix linting issues automatically (where possible):** + (This assumes a `fix` script is defined in `frontend/package.json`, e.g., `"fix": "gts fix"`) + ```bash + # from frontend/ directory + npm run fix + ``` + +### Backend (Python with `pylint` and `black`) + +(Assumes setup within the `backend/` directory and its virtual environment activated) + +1. **Ensure Dependencies are Installed:** + Add `pylint` and `black` to your `backend/requirements.txt` file if not already present: + ``` + pylint + black + ``` + Then install them within your virtual environment: + ```bash + # from backend/ directory, with .venv activated + pip install pylint black + # or pip install -r requirements.txt + ``` +2. **Configure `pylint`:** + It's recommended to have a `.pylintrc` file in your `backend/` directory to configure `pylint` rules. You can generate one if it doesn't exist: + ```bash + # from backend/ directory + pylint --generate-rcfile > .pylintrc + ``` + Customize this file according to your project's needs and the Google Python Style Guide. +3. **Check for linting issues with `pylint`:** + Navigate to the `backend/` directory and run: + ```bash + # from backend/ directory + pylint . + # Or specify modules/packages: pylint your_module_name + ``` +4. **Format code with `black`:** + To automatically format all Python files in the `backend/` directory and its subdirectories: + ```bash + # from backend/ directory + python -m black . --line-length=80 + ``` diff --git a/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/assets/quickbot-linkedin-agent-photo.png b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/assets/quickbot-linkedin-agent-photo.png new file mode 100644 index 0000000..d46b485 Binary files /dev/null and b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/assets/quickbot-linkedin-agent-photo.png differ diff --git a/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/backend/.gcloudignore b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/backend/.gcloudignore new file mode 100644 index 0000000..606a07e --- /dev/null +++ b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/backend/.gcloudignore @@ -0,0 +1,20 @@ +# This file specifies files that are *not* uploaded to Google Cloud +# using gcloud. It follows the same syntax as .gitignore, with the addition of +# "#!include" directives (which insert the entries of the given .gitignore-style +# file at that point). +# +# For more information, run: +# $ gcloud topic gcloudignore +# +.gcloudignore +# If you would like to upload your .git directory, .gitignore file or files +# from your .gitignore file, remove the corresponding line +# below: +.git +.gitignore + +# Python pycache: +__pycache__/ +# Ignored by the build system +/setup.cfg +my_env \ No newline at end of file diff --git a/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/backend/.gitignore b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/backend/.gitignore new file mode 100644 index 0000000..83bb4d6 --- /dev/null +++ b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/backend/.gitignore @@ -0,0 +1,135 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# VSCode +.vscode/* +!.vscode/settings.json +!.vscode/extensions.json + +# Logs +*/output +*.info diff --git a/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/backend/.local.env b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/backend/.local.env new file mode 100644 index 0000000..af26a11 --- /dev/null +++ b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/backend/.local.env @@ -0,0 +1,2 @@ +export ENVIRONMENT=development +export FRONTEND_URL=http://localhost:4200 diff --git a/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/backend/Dockerfile b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/backend/Dockerfile new file mode 100644 index 0000000..4691575 --- /dev/null +++ b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/backend/Dockerfile @@ -0,0 +1,47 @@ +# Copyright 2025 Google LLC +# +# 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 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +FROM python:3.12.8 + +# Upgrade pip! Is a MUST for opencv-python to work properly +RUN pip install --upgrade pip + +# Install cv2 dependencies that are normally present on any local machine +# Important, never put RUN apt-get update alone, that causes a cache issue +RUN apt-get update && apt-get install ffmpeg libsm6 libxext6 -y + +# Install opencv-python separately, it DOES NOT WORK if installed with +# the rest of the dependencies directly from requirements.txt +RUN pip install opencv-python + +# Copy requirements.txt and install other dependencies +COPY requirements.txt /tmp/ +RUN pip install --no-cache-dir -r /tmp/requirements.txt + +# Create working directory +RUN mkdir -p /app +WORKDIR /app + +# Copy the rest of the application code +COPY . /app/ + +# Set the environment variable +ENV ENVIRONMENT="development" +ENV FRONTEND_URL="http://localhost:4200" + +# Expose the port +EXPOSE 8080 + +# Define the command to run your application +ENTRYPOINT ["gunicorn", "main:app", "--workers=2", "--worker-class=uvicorn.workers.UvicornWorker", "--timeout=36000", "--bind=0.0.0.0:8080"] \ No newline at end of file diff --git a/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/backend/README.md b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/backend/README.md new file mode 100644 index 0000000..6483a1f --- /dev/null +++ b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/backend/README.md @@ -0,0 +1,133 @@ +# QuickBot App Backend | linkedin-profile-image-generation-using-imagen3 + +[![linting: pylint](https://img.shields.io/badge/linting-pylint-yellowgreen)](https://github.com/pylint-dev/pylint) +[![Code Style: Google](https://img.shields.io/badge/code%20style-google-blueviolet.svg)](https://github.com/google/gts) + +QuickBot App is a set of templates that can be deployed out of the box into Cloud Run and work independently. Each one can be run independently connected to the user default google cloud auth credentials, and based on the complexity of each template, may require to deploy more or less resources into our Google Cloud Project. +The architecture always follows the following structure: a folder for the frontend which consists in an Angular app, and a backend folder which consists of a FastAPI Python app. + + +## Setting up +### 1. Create virtualenv inside the backend folder and install dependencies +Create a virtual environment on the root of the application, activate it and install the requirements +``` +# check if you are already in the env +pip -V + +# if not then +python3 -m venv .venv +source .venv/bin/activate +pip3 install -r requirements.txt +``` + +> **IMPORTANT!** VS Code may not recognize your env, in that case type "ctrl + shift + P", then select "Python: Select Interpreter" and then select "Enter interpreter path..." and then select your .venv python interpreter, in this case .backend/.venv/bin/python + + +### 2. Setup gcloud credentials +``` +gcloud auth list +gcloud config list + +gcloud auth login +gcloud config set project +gcloud auth application-default set-quota-project + +gcloud auth list +gcloud config list +``` + +### 3. Add environment variables + +#### If you have Mac or Windows (or if you are using zsh console on Linux) +``` +. ./local.env +``` + +#### If you have Linux +Open the file .venv/bin/activate and paste the env variables from `.local.env` after the PATH export, like this: +``` +... + +_OLD_VIRTUAL_PATH="$PATH" +PATH="$VIRTUAL_ENV/bin:$PATH" +export PATH + +# Quickbot env variables +export ENVIRONMENT=development +export FRONTEND_URL=http://localhost:4200 + +... +``` + +Check that the env variables has been taken into account, running: +``` +env +``` +You should see the new env variables set there + + +### 4. Run the application +Finally run using uvicorn +``` +uvicorn main:app --reload --port 8080 +``` + +## Code Styling & Commit Guidelines + +To maintain code quality and consistency: + +* **TypeScript (Frontend):** We follow [Angular Coding Style Guide](https://angular.dev/style-guide) by leveraging the use of [Google's TypeScript Style Guide](https://github.com/google/gts) using `gts`. This includes a formatter, linter, and automatic code fixer. +* **Python (Backend):** We adhere to the [Google Python Style Guide](https://google.github.io/styleguide/pyguide.html), using tools like `pylint` and `black` for linting and formatting. +* **Commit Messages:** We suggest following [Angular's Commit Message Guidelines](https://github.com/angular/angular/blob/main/contributing-docs/commit-message-guidelines.md) to create clear and descriptive commit messages. + +#### Frontend (TypeScript with `gts`) + +1. **Initialize `gts` (if not already done in the project):** + Navigate to the `frontend/` directory and run: + ```bash + npx gts init + ``` + This will set up `gts` and create necessary configuration files (like `tsconfig.json`). Ensure your `tsconfig.json` (or a related gts config file like `.gtsrc`) includes an extension for `gts` defaults, typically: + ```json + { + "extends": "./node_modules/gts/tsconfig-google.json", + // ... other configurations + } + ``` +2. **Check for linting issues:** + ```bash + npm run lint + ``` + (This assumes a `lint` script is defined in `package.json`, e.g., `"lint": "gts lint"`) +3. **Fix linting issues automatically (where possible):** + ```bash + npm run fix + ``` + (This assumes a `fix` script is defined in `package.json`, e.g., `"fix": "gts fix"`) + +#### Backend (Python with `pylint` and `black`) + +1. **Ensure Dependencies are Installed:** + Add `pylint` and `black` to your `backend/requirements.txt` file: + ``` + pylint + black + ``` + Then install them within your virtual environment: + ```bash + pip install pylint black + # or pip install -r requirements.txt + ``` +2. **Configure `pylint`:** + It's recommended to have a `.pylintrc` file in your `backend/` directory to configure `pylint` rules. You might need to copy a standard one or generate one (`pylint --generate-rcfile > .pylintrc`). +3. **Check for linting issues with `pylint`:** + Navigate to the `backend/` directory and run: + ```bash + pylint . + ``` + (Or specify modules/packages: `pylint your_module_name`) +4. **Format code with `black`:** + To automatically format all Python files in the current directory and subdirectories: + ```bash + python -m black . --line-length=80 + ``` \ No newline at end of file diff --git a/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/backend/__init__.py b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/backend/__init__.py new file mode 100644 index 0000000..36a1e8d --- /dev/null +++ b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/backend/__init__.py @@ -0,0 +1,14 @@ +# Copyright 2025 Google LLC +# +# 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 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + diff --git a/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/backend/main.py b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/backend/main.py new file mode 100644 index 0000000..4efb486 --- /dev/null +++ b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/backend/main.py @@ -0,0 +1,96 @@ +# Copyright 2025 Google LLC +# +# 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 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from os import getenv + +from fastapi import FastAPI, File, UploadFile +from fastapi.middleware.cors import CORSMiddleware +from google.cloud import speech + +from src.controller.search import router as search_router + +app = FastAPI() + + +def configure_cors(app): + """Configures CORS middleware based on the environment.""" + environment = getenv("ENVIRONMENT") + allowed_origins = [] + + if environment == "production": + frontend_url = getenv("FRONTEND_URL") + if not frontend_url: + raise ValueError( + "FRONTEND_URL environment variable not set in production" + ) + allowed_origins.append(frontend_url) + elif environment == "development": + allowed_origins.append("*") # Allow all origins in development + else: + raise ValueError( + f"Invalid ENVIRONMENT: {environment}. Must be 'production' or 'development'" + ) + + app.add_middleware( + CORSMiddleware, + allow_origins=allowed_origins, + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], + ) + + +# Create a route to handle GET requests on root +@app.get("/") +async def root(): + return "You are calling Quick Bot Backend" + + +# Create a route to handle GET requests on /version +@app.get("/api/version") +def version(): + return "v0.0.1" + + +@app.post("/api/audio_chat") +async def audio_chat(audio_file: UploadFile = File(...)): + client = speech.SpeechClient() + audio_content = await audio_file.read() + audio = speech.RecognitionAudio(content=audio_content) + config = speech.RecognitionConfig( + language_code="en-US", + sample_rate_hertz=48000, + model="default", + audio_channel_count=1, + enable_word_confidence=True, + enable_word_time_offsets=True, + ) + + operation = client.long_running_recognize(config=config, audio=audio) + + print("Waiting for operation to complete...") + response = operation.result(timeout=90) + print(response) + + text = "" + for result in response.results: + print(f"Transcript: {result.alternatives[0].transcript}") + text = result.alternatives[0].transcript + + return text, 200 + + +configure_cors(app) + +app.include_router(search_router) diff --git a/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/backend/pylintrc b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/backend/pylintrc new file mode 100644 index 0000000..cd99157 --- /dev/null +++ b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/backend/pylintrc @@ -0,0 +1,399 @@ +# This Pylint rcfile contains a best-effort configuration to uphold the +# best-practices and style described in the Google Python style guide: +# https://google.github.io/styleguide/pyguide.html +# +# Its canonical open-source location is: +# https://google.github.io/styleguide/pylintrc + +[MAIN] + +# Files or directories to be skipped. They should be base names, not paths. +ignore=third_party + +# Files or directories matching the regex patterns are skipped. The regex +# matches against base names, not paths. +ignore-patterns= + +# Pickle collected data for later comparisons. +persistent=no + +# List of plugins (as comma separated values of python modules names) to load, +# usually to register additional checkers. +load-plugins= + +# Use multiple processes to speed up Pylint. +jobs=4 + +# Allow loading of arbitrary C extensions. Extensions are imported into the +# active Python interpreter and may run arbitrary code. +unsafe-load-any-extension=no + + +[MESSAGES CONTROL] + +# Only show warnings with the listed confidence levels. Leave empty to show +# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED +confidence= + +# Enable the message, report, category or checker with the given id(s). You can +# either give multiple identifier separated by comma (,) or put this option +# multiple time (only on the command line, not in the configuration file where +# it should appear only once). See also the "--disable" option for examples. +#enable= + +# Disable the message, report, category or checker with the given id(s). You +# can either give multiple identifiers separated by comma (,) or put this +# option multiple times (only on the command line, not in the configuration +# file where it should appear only once).You can also use "--disable=all" to +# disable everything first and then reenable specific checks. For example, if +# you want to run only the similarities checker, you can use "--disable=all +# --enable=similarities". If you want to run only the classes checker, but have +# no Warning level messages displayed, use"--disable=all --enable=classes +# --disable=W" +disable=R, + abstract-method, + apply-builtin, + arguments-differ, + attribute-defined-outside-init, + backtick, + bad-option-value, + basestring-builtin, + buffer-builtin, + c-extension-no-member, + consider-using-enumerate, + cmp-builtin, + cmp-method, + coerce-builtin, + coerce-method, + delslice-method, + div-method, + eq-without-hash, + execfile-builtin, + file-builtin, + filter-builtin-not-iterating, + fixme, + getslice-method, + global-statement, + hex-method, + idiv-method, + implicit-str-concat, + import-error, + import-self, + import-star-module-level, + input-builtin, + intern-builtin, + invalid-str-codec, + locally-disabled, + long-builtin, + long-suffix, + map-builtin-not-iterating, + misplaced-comparison-constant, + missing-function-docstring, + metaclass-assignment, + next-method-called, + next-method-defined, + no-absolute-import, + no-init, # added + no-member, + no-name-in-module, + no-self-use, + nonzero-method, + oct-method, + old-division, + old-ne-operator, + old-octal-literal, + old-raise-syntax, + parameter-unpacking, + print-statement, + raising-string, + range-builtin-not-iterating, + raw_input-builtin, + rdiv-method, + reduce-builtin, + relative-import, + reload-builtin, + round-builtin, + setslice-method, + signature-differs, + standarderror-builtin, + suppressed-message, + sys-max-int, + trailing-newlines, + unichr-builtin, + unicode-builtin, + unnecessary-pass, + unpacking-in-except, + useless-else-on-loop, + useless-suppression, + using-cmp-argument, + wrong-import-order, + xrange-builtin, + zip-builtin-not-iterating, + + +[REPORTS] + +# Set the output format. Available formats are text, parseable, colorized, msvs +# (visual studio) and html. You can also give a reporter class, eg +# mypackage.mymodule.MyReporterClass. +output-format=text + +# Tells whether to display a full report or only the messages +reports=no + +# Python expression which should return a note less than 10 (10 is the highest +# note). You have access to the variables errors warning, statement which +# respectively contain the number of errors / warnings messages and the total +# number of statements analyzed. This is used by the global evaluation report +# (RP0004). +evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) + +# Template used to display messages. This is a python new-style format string +# used to format the message information. See doc for all details +#msg-template= + + +[BASIC] + +# Good variable names which should always be accepted, separated by a comma +good-names=main,_ + +# Bad variable names which should always be refused, separated by a comma +bad-names= + +# Colon-delimited sets of names that determine each other's naming style when +# the name regexes allow several styles. +name-group= + +# Include a hint for the correct naming format with invalid-name +include-naming-hint=no + +# List of decorators that produce properties, such as abc.abstractproperty. Add +# to this list to register other decorators that produce valid properties. +property-classes=abc.abstractproperty,cached_property.cached_property,cached_property.threaded_cached_property,cached_property.cached_property_with_ttl,cached_property.threaded_cached_property_with_ttl + +# Regular expression matching correct function names +function-rgx=^(?:(?PsetUp|tearDown|setUpModule|tearDownModule)|(?P_?[A-Z][a-zA-Z0-9]*)|(?P_?[a-z][a-z0-9_]*))$ + +# Regular expression matching correct variable names +variable-rgx=^[a-z][a-z0-9_]*$ + +# Regular expression matching correct constant names +const-rgx=^(_?[A-Z][A-Z0-9_]*|__[a-z0-9_]+__|_?[a-z][a-z0-9_]*)$ + +# Regular expression matching correct attribute names +attr-rgx=^_{0,2}[a-z][a-z0-9_]*$ + +# Regular expression matching correct argument names +argument-rgx=^[a-z][a-z0-9_]*$ + +# Regular expression matching correct class attribute names +class-attribute-rgx=^(_?[A-Z][A-Z0-9_]*|__[a-z0-9_]+__|_?[a-z][a-z0-9_]*)$ + +# Regular expression matching correct inline iteration names +inlinevar-rgx=^[a-z][a-z0-9_]*$ + +# Regular expression matching correct class names +class-rgx=^_?[A-Z][a-zA-Z0-9]*$ + +# Regular expression matching correct module names +module-rgx=^(_?[a-z][a-z0-9_]*|__init__)$ + +# Regular expression matching correct method names +method-rgx=(?x)^(?:(?P_[a-z0-9_]+__|runTest|setUp|tearDown|setUpTestCase|tearDownTestCase|setupSelf|tearDownClass|setUpClass|(test|assert)_*[A-Z0-9][a-zA-Z0-9_]*|next)|(?P_{0,2}[A-Z][a-zA-Z0-9_]*)|(?P_{0,2}[a-z][a-z0-9_]*))$ + +# Regular expression which should only match function or class names that do +# not require a docstring. +no-docstring-rgx=(__.*__|main|test.*|.*test|.*Test)$ + +# Minimum line length for functions/classes that require docstrings, shorter +# ones are exempt. +docstring-min-length=12 + + +[TYPECHECK] + +# List of decorators that produce context managers, such as +# contextlib.contextmanager. Add to this list to register other decorators that +# produce valid context managers. +contextmanager-decorators=contextlib.contextmanager,contextlib2.contextmanager + +# List of module names for which member attributes should not be checked +# (useful for modules/projects where namespaces are manipulated during runtime +# and thus existing member attributes cannot be deduced by static analysis. It +# supports qualified module names, as well as Unix pattern matching. +ignored-modules= + +# List of class names for which member attributes should not be checked (useful +# for classes with dynamically set attributes). This supports the use of +# qualified names. +ignored-classes=optparse.Values,thread._local,_thread._local + +# List of members which are set dynamically and missed by pylint inference +# system, and so shouldn't trigger E1101 when accessed. Python regular +# expressions are accepted. +generated-members= + + +[FORMAT] + +# Maximum number of characters on a single line. +max-line-length=80 + +# TODO(https://github.com/pylint-dev/pylint/issues/3352): Direct pylint to exempt +# lines made too long by directives to pytype. + +# Regexp for a line that is allowed to be longer than the limit. +ignore-long-lines=(?x)( + ^\s*(\#\ )??$| + ^\s*(from\s+\S+\s+)?import\s+.+$) + +# Allow the body of an if to be on the same line as the test if there is no +# else. +single-line-if-stmt=yes + +# Maximum number of lines in a module +max-module-lines=99999 + +# String used as indentation unit. The internal Google style guide mandates 2 +# spaces. Google's externaly-published style guide says 4, consistent with +# PEP 8. Here, we use 2 spaces, for conformity with many open-sourced Google +# projects (like TensorFlow). +indent-string=' ' + +# Number of spaces of indent required inside a hanging or continued line. +indent-after-paren=4 + +# Expected format of line ending, e.g. empty (any line ending), LF or CRLF. +expected-line-ending-format= + + +[MISCELLANEOUS] + +# List of note tags to take in consideration, separated by a comma. +notes=TODO + + +[STRING] + +# This flag controls whether inconsistent-quotes generates a warning when the +# character used as a quote delimiter is used inconsistently within a module. +check-quote-consistency=yes + + +[VARIABLES] + +# Tells whether we should check for unused import in __init__ files. +init-import=no + +# A regular expression matching the name of dummy variables (i.e. expectedly +# not used). +dummy-variables-rgx=^\*{0,2}(_$|unused_|dummy_) + +# List of additional names supposed to be defined in builtins. Remember that +# you should avoid to define new builtins when possible. +additional-builtins= + +# List of strings which can identify a callback function by name. A callback +# name must start or end with one of those strings. +callbacks=cb_,_cb + +# List of qualified module names which can have objects that can redefine +# builtins. +redefining-builtins-modules=six,six.moves,past.builtins,future.builtins,functools + + +[LOGGING] + +# Logging modules to check that the string format arguments are in logging +# function parameter format +logging-modules=logging,absl.logging,tensorflow.io.logging + + +[SIMILARITIES] + +# Minimum lines number of a similarity. +min-similarity-lines=4 + +# Ignore comments when computing similarities. +ignore-comments=yes + +# Ignore docstrings when computing similarities. +ignore-docstrings=yes + +# Ignore imports when computing similarities. +ignore-imports=no + + +[SPELLING] + +# Spelling dictionary name. Available dictionaries: none. To make it working +# install python-enchant package. +spelling-dict= + +# List of comma separated words that should not be checked. +spelling-ignore-words= + +# A path to a file that contains private dictionary; one word per line. +spelling-private-dict-file= + +# Tells whether to store unknown words to indicated private dictionary in +# --spelling-private-dict-file option instead of raising a message. +spelling-store-unknown-words=no + + +[IMPORTS] + +# Deprecated modules which should not be used, separated by a comma +deprecated-modules=regsub, + TERMIOS, + Bastion, + rexec, + sets + +# Create a graph of every (i.e. internal and external) dependencies in the +# given file (report RP0402 must not be disabled) +import-graph= + +# Create a graph of external dependencies in the given file (report RP0402 must +# not be disabled) +ext-import-graph= + +# Create a graph of internal dependencies in the given file (report RP0402 must +# not be disabled) +int-import-graph= + +# Force import order to recognize a module as part of the standard +# compatibility libraries. +known-standard-library= + +# Force import order to recognize a module as part of a third party library. +known-third-party=enchant, absl + +# Analyse import fallback blocks. This can be used to support both Python 2 and +# 3 compatible code, which means that the block might have code that exists +# only in one or another interpreter, leading to false positives when analysed. +analyse-fallback-blocks=no + + +[CLASSES] + +# List of method names used to declare (i.e. assign) instance attributes. +defining-attr-methods=__init__, + __new__, + setUp + +# List of member names, which should be excluded from the protected access +# warning. +exclude-protected=_asdict, + _fields, + _replace, + _source, + _make + +# List of valid names for the first argument in a class method. +valid-classmethod-first-arg=cls, + class_ + +# List of valid names for the first argument in a metaclass class method. +valid-metaclass-classmethod-first-arg=mcs diff --git a/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/backend/pyproject.toml b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/backend/pyproject.toml new file mode 100644 index 0000000..7ced637 --- /dev/null +++ b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/backend/pyproject.toml @@ -0,0 +1,5 @@ +[tool.coverage.run] +source = ["livecov"] + +[tool.pytest.ini_options] +addopts = "--cov --cov-report=lcov:lcov.info --cov-report=term" diff --git a/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/backend/pytest.ini b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/backend/pytest.ini new file mode 100644 index 0000000..2a3f054 --- /dev/null +++ b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/backend/pytest.ini @@ -0,0 +1,12 @@ +[pytest] +minversion = 7.0 + +# Directories to look for the test files +testpaths = tests + +# Directories to look for the source code +pythonpath = . + +python_files = test_*.py *_tests.py + +log_cli_level = DEBUG diff --git a/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/backend/requirements.txt b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/backend/requirements.txt new file mode 100644 index 0000000..dbb5614 --- /dev/null +++ b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/backend/requirements.txt @@ -0,0 +1,28 @@ +gunicorn==20.1.0; python_version > '3.0' +gunicorn==19.10.0; python_version < '3.0' +uvicorn~=0.17.0 +fastapi~=0.111.1 +fastapi-utilities==0.2.0 +cloudpickle==2.2.1 +grpcio==1.66.2 +urllib3==1.26.16 +typing-inspect==0.9.0 +typing_extensions==4.12.2 +pydantic==2.9.2 +requests==2.31.0 +opencv-python +pillow==11.1.0 +numpy==2.2.4 + +google-cloud-aiplatform==1.69.0 +google-cloud-bigquery==3.26.0 +google-cloud-tasks==2.16.5 +google-cloud-logging==3.11.2 +google-cloud-speech==2.27.0 +google-cloud-discoveryengine==0.13.4 +google-genai==0.8.0 +pylint +black +pytest +pytest-cov +pytest-watch diff --git a/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/backend/src/__init__.py b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/backend/src/__init__.py new file mode 100644 index 0000000..36a1e8d --- /dev/null +++ b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/backend/src/__init__.py @@ -0,0 +1,14 @@ +# Copyright 2025 Google LLC +# +# 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 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + diff --git a/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/backend/src/controller/__init__.py b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/backend/src/controller/__init__.py new file mode 100644 index 0000000..36a1e8d --- /dev/null +++ b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/backend/src/controller/__init__.py @@ -0,0 +1,14 @@ +# Copyright 2025 Google LLC +# +# 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 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + diff --git a/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/backend/src/controller/search.py b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/backend/src/controller/search.py new file mode 100644 index 0000000..8d4d02a --- /dev/null +++ b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/backend/src/controller/search.py @@ -0,0 +1,91 @@ +# Copyright 2025 Google LLC +# +# 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 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from typing import Annotated, Optional +from fastapi import APIRouter, HTTPException, status as Status + +from src.model.search import ( + CreateSearchRequest, + GenerationModelOptionalLiteral, +) +from src.service.search import ImagenSearchService +from fastapi import Form, File, UploadFile + +router = APIRouter( + prefix="/api/search", + tags=["searches"], + responses={404: {"description": "Not found"}}, +) + +ALLOWED_IMAGE_TYPES = ["image/jpg", "image/jpeg", "image/png", "image/webp"] + + +@router.post("") +async def search( + userImage: Annotated[UploadFile, File()], + term: Annotated[Optional[str], Form(min_length=10, max_length=400)], + generationModel: Annotated[ + Optional[GenerationModelOptionalLiteral], + Form(description="Model used for image edition"), + ], + numberOfImages: Annotated[ + Optional[int], + Form(ge=1, le=4, description="Number of images to generate"), + ], + maskDistilation: Annotated[ + Optional[float], + Form( + ge=0, + le=1, + description="Dilation percentage of the mask provided. Float between 0 and 1.", + ), + ], +): + try: + if userImage.content_type not in ALLOWED_IMAGE_TYPES: + raise HTTPException( + status_code=Status.HTTP_400_BAD_REQUEST, + detail=f"Invalid image type. Allowed types are: {', '.join(ALLOWED_IMAGE_TYPES)}", + ) + createSearchRequest = CreateSearchRequest.model_validate( + { + "term": term, + "generation_model": generationModel, + "number_of_images": numberOfImages, + "user_image": userImage.file.read(), + "mask_distilation": maskDistilation, + } + ) + + service = ImagenSearchService() + return service.generate_images(createSearchRequest) + except HTTPException as http_exception: + raise HTTPException( + status_code=Status.HTTP_400_BAD_REQUEST, + detail=str(http_exception), + ) + except ValueError as value_error: + raise HTTPException( + status_code=Status.HTTP_400_BAD_REQUEST, + detail=str(value_error), + ) + except Exception as e: + raise HTTPException( + status_code=( + e.code + if hasattr(e, "code") + else Status.HTTP_500_INTERNAL_SERVER_ERROR + ), + detail=str(e.message) if hasattr(e, "message") else str(e), + ) diff --git a/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/backend/src/model/__init__.py b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/backend/src/model/__init__.py new file mode 100644 index 0000000..36a1e8d --- /dev/null +++ b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/backend/src/model/__init__.py @@ -0,0 +1,14 @@ +# Copyright 2025 Google LLC +# +# 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 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + diff --git a/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/backend/src/model/http_status.py b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/backend/src/model/http_status.py new file mode 100644 index 0000000..ca1c1c3 --- /dev/null +++ b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/backend/src/model/http_status.py @@ -0,0 +1,25 @@ +# Copyright 2025 Google LLC +# +# 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 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from fastapi import HTTPException + + +class ResourceAlreadyExists(HTTPException): + def __init__(self, detail="Resource already exists"): + super().__init__(status_code=400, detail=detail) + + +class BadRequest(HTTPException): + def __init__(self, detail="Resource already exists"): + super().__init__(status_code=400, detail=detail) diff --git a/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/backend/src/model/search.py b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/backend/src/model/search.py new file mode 100644 index 0000000..6c6168f --- /dev/null +++ b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/backend/src/model/search.py @@ -0,0 +1,60 @@ +# Copyright 2025 Google LLC +# +# 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 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from typing import Literal, Optional, Union + +from pydantic import BaseModel, ConfigDict, Field +from pydantic.alias_generators import to_camel + + +# Create a Literal type from the list of valid models +GenerationModelOptionalLiteral = Union[ + Literal["imagen-3.0-capability-001"], Literal["imagegeneration@006"] +] + + +class CreateSearchRequest(BaseModel): + term: str = Field(description="Prompt term to be passed to the model") + generation_model: Optional[GenerationModelOptionalLiteral] = Field( + default="imagen-3.0-capability-001", + description="Model used for image edition", + ) + number_of_images: Optional[int] = Field( + 4, description="Number of images to generate" + ) + user_image: bytes + mask_distilation: Optional[float] = Field( + 0.005, + description="Dilation percentage of the mask provided. Float between 0 and 1.", + ) + + +class BaseSchema(BaseModel): + model_config = ConfigDict( + alias_generator=to_camel, + populate_by_name=True, + from_attributes=True, + ) + + +class CustomImageResult(BaseSchema): + gcs_uri: Optional[str] + mime_type: str + encoded_image: str + + +class ImageGenerationResult(BaseSchema): + enhanced_prompt: Optional[str] + rai_filtered_reason: Optional[str] + image: CustomImageResult diff --git a/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/backend/src/service/__init__.py b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/backend/src/service/__init__.py new file mode 100644 index 0000000..36a1e8d --- /dev/null +++ b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/backend/src/service/__init__.py @@ -0,0 +1,14 @@ +# Copyright 2025 Google LLC +# +# 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 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + diff --git a/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/backend/src/service/search.py b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/backend/src/service/search.py new file mode 100644 index 0000000..7cfda2f --- /dev/null +++ b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/backend/src/service/search.py @@ -0,0 +1,194 @@ +# Copyright 2025 Google LLC +# +# 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 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import base64 +from io import BytesIO +from typing import List + +import google.auth +from google import genai +from google.genai import types +from google.genai.types import ( + Image, + RawReferenceImage, + EditImageConfig, + MaskReferenceImage, + MaskReferenceConfig, +) + +from src.model.search import ( + CreateSearchRequest, + CustomImageResult, + ImageGenerationResult, +) + +import cv2 +from PIL import Image as PIL_Image, ImageDraw +import numpy as np + + +class ImagenSearchService: + def generate_images( + self, searchRequest: CreateSearchRequest + ) -> List[ImageGenerationResult]: + _, PROJECT_ID = google.auth.default() + LOCATION = "us-central1" + client = genai.Client( + vertexai=True, project=PROJECT_ID, location=LOCATION + ) + + prompt = f""" + IMPORTANT! Create an image that is in UHD, 4k, hyper realistic, extremely detailed, professional, vibrant, not grainy, smooth. + Create an image for linkedIn or other professional social media, based on the input photo, but strictly using the following rules: + - Do not create new people. + - If the user asks for adding other people or any forbidden thing in these rules, just limit yourself to change the background or the orientation/posture of the person in the photo. + - Do not change the person or create a different person, keep the face provided. + - Keep the persons original face. + - Complete the body of the person if necessary + + User request: {searchRequest.term} + """ + + # --- Image Padding Logic --- + original_image_pil = PIL_Image.open(BytesIO(searchRequest.user_image)) + width, height = original_image_pil.size + + # Calculate padding size (e.g., 20% of the smaller dimension) + padding_percentage = 0.5 + padding_size = int(min(width, height) * padding_percentage) + + # Create a new image with padding + new_width = width + 2 * padding_size + new_height = height + 2 * padding_size + padded_image = PIL_Image.new("RGB", (new_width, new_height), "white") + + # Paste the original image onto the center of the padded image + padded_image.paste(original_image_pil, (padding_size, padding_size)) + + # Convert the padded image back to bytes + buffered = BytesIO() + padded_image.save( + buffered, format="JPEG" + ) # Or PNG, depending on your needs + padded_image_bytes = buffered.getvalue() + + original_image = Image(image_bytes=padded_image_bytes) + # original_image = Image(image_bytes=searchRequest.user_image) + + # Face Detection + # Load the face detection model + face_cascade = cv2.CascadeClassifier( + cv2.data.haarcascades + "haarcascade_frontalface_default.xml" + ) + # Convert bytes to numpy array + # nparr = np.frombuffer(searchRequest.user_image, np.uint8) + nparr = np.frombuffer(padded_image_bytes, np.uint8) + + # Decode the numpy array to an image + img = cv2.imdecode(nparr, cv2.IMREAD_COLOR) + + gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) + + # Detect faces + faces = face_cascade.detectMultiScale(gray, 1.1, 4) + + # Get image size + height, width, _ = img.shape + + # Create a mask with the same size as the original image + mask = PIL_Image.new( + "L", (width, height), 255 + ) ###if i start with 255 instead of 0, i invert black and white + draw = ImageDraw.Draw(mask) + + # Draw white rectangles on the mask where faces are detected + for x, y, w, h in faces: + draw.rectangle( + [(x, y), (x + w, y + h)], fill=0 + ) ###if i fill with 0 instead of 0, i invert black and white + + # Finally use Imagen3 Model + raw_reference_image = RawReferenceImage( + reference_image=original_image, reference_id=0 + ) + + # Convert PIL Image to bytes + import io + + buffered = io.BytesIO() + mask.save(buffered, format="PNG") + face_mask_image_bytes = buffered.getvalue() + + # Load the face mask + face_mask_image = Image(image_bytes=face_mask_image_bytes) + + # Use MaskReferenceImage with MASK_MODE_USER_PROVIDED + mask_ref_image = MaskReferenceImage( + reference_id=1, + reference_image=face_mask_image, + config=MaskReferenceConfig( + mask_mode="MASK_MODE_USER_PROVIDED", + mask_dilation=searchRequest.mask_distilation, + ), + ) + + images_face_recognition: types.EditImageResponse = ( + client.models.edit_image( + model=searchRequest.generation_model, + prompt=prompt, + reference_images=[raw_reference_image, mask_ref_image], + config=EditImageConfig( + edit_mode="EDIT_MODE_BGSWAP", + number_of_images=searchRequest.number_of_images, + safety_filter_level="BLOCK_MEDIUM_AND_ABOVE", + person_generation="ALLOW_ADULT", + ), + ) + ) + + # Imagen3 edition for just the entire image + images_entire_image: types.EditImageResponse = client.models.edit_image( + model=searchRequest.generation_model, + prompt=prompt, + reference_images=[raw_reference_image], + config=EditImageConfig( + edit_mode="EDIT_MODE_DEFAULT", + number_of_images=searchRequest.number_of_images, + safety_filter_level="BLOCK_MEDIUM_AND_ABOVE", + person_generation="ALLOW_ADULT", + ), + ) + + # Make sure to convert the image from bytes to encoded string before sending to the frontend + all_generated_images = ( + images_entire_image.generated_images + + images_face_recognition.generated_images + ) + + response = [ + ImageGenerationResult( + enhanced_prompt=generated_image.enhanced_prompt, + rai_filtered_reason=generated_image.rai_filtered_reason, + image=CustomImageResult( + gcs_uri=generated_image.image.gcs_uri, + encoded_image=base64.b64encode( + generated_image.image.image_bytes + ).decode("utf-8"), + mime_type=generated_image.image.mime_type, + ), + ) + for generated_image in all_generated_images + ] + + return response diff --git a/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/backend/tests/test_search.py b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/backend/tests/test_search.py new file mode 100644 index 0000000..638323e --- /dev/null +++ b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/backend/tests/test_search.py @@ -0,0 +1,197 @@ +# Copyright 2025 Google LLC +# +# 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 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Tests for the search controller and service.""" + +import base64 +from io import BytesIO +from unittest.mock import MagicMock + +import numpy as np +import pytest +from fastapi.testclient import TestClient +from google.genai import types +from PIL import Image as PIL_Image + +from src.controller.search import router +from src.model.search import ( + CreateSearchRequest, + ImageGenerationResult, + CustomImageResult, +) +from src.service.search import ImagenSearchService + +client = TestClient(router) + + +def create_minimal_image_bytes(image_format="PNG") -> bytes: + """Creates bytes for a 1x1 pixel image.""" + img = PIL_Image.new("RGB", (1, 1), color="red") + buffer = BytesIO() + img.save(buffer, format=image_format) + return buffer.getvalue() + + +@pytest.fixture(scope="function", name="mock_genai_client") +def fixture_mock_genai_client(): + """Provides a mock google.genai Client.""" + mock_client = MagicMock() + mock_response = types.EditImageResponse() + + # Create mock generated images with base64 encoded placeholder image data + mock_image_data = types.Image( + gcs_uri="gs://mock_bucket/mock_image.png", + image_bytes=b"mock_image_bytes", # Must be bytes + mime_type="image/png", + ) + mock_generated_image = types.GeneratedImage( + enhanced_prompt="Mock enhanced prompt", + rai_filtered_reason=None, + image=mock_image_data, + ) + mock_response.generated_images = [ + mock_generated_image, + mock_generated_image, + mock_generated_image, + mock_generated_image, + ] + + mock_client.models.edit_image.return_value = mock_response + return mock_client + + +class TestSearchController: + """Tests for the /api/search endpoint.""" + + def test_search_endpoint(self, monkeypatch, mock_genai_client): + # Mock the ImagenSearchService to avoid actual API calls + # Mock the google.auth.default to avoid authentication issues + with monkeypatch.context() as m: + mock_client_class = MagicMock(return_value=mock_genai_client) + m.setattr("src.service.search.genai.Client", mock_client_class) + m.setattr( + "src.service.search.google.auth.default", + lambda: (None, "test_project_id"), + ) + # Mock OpenCV functions used within the service + mock_cascade = MagicMock() + mock_cascade.detectMultiScale.return_value = [ + (10, 10, 50, 50) + ] # Mock finding one face + m.setattr( + "src.service.search.cv2.CascadeClassifier", + lambda x: mock_cascade, + ) + # Mock imdecode to return a dummy numpy array with a shape + mock_img_array = np.zeros((100, 100, 3), dtype=np.uint8) + m.setattr( + "src.service.search.cv2.imdecode", + lambda buf, flags: mock_img_array, + ) + # Mock cvtColor to return a dummy grayscale array + mock_gray_array = np.zeros((100, 100), dtype=np.uint8) + m.setattr( + "src.service.search.cv2.cvtColor", + lambda img, code: mock_gray_array, + ) + + search_term = "a cute cat wearing a hat" + image_content = create_minimal_image_bytes() + user_image = ("test_image.png", BytesIO(image_content), "image/png") + + response = client.post( + "/api/search", + data={ + "term": search_term, + "numberOfImages": 4, + "maskDistilation": 0.005, + "generationModel": "imagen-3.0-capability-001", + }, + files={"userImage": user_image}, + ) + + assert response.status_code == 200 + data = response.json() + assert len(data) == 8 + + for image_data in data: + assert image_data["enhancedPrompt"] == "Mock enhanced prompt" + assert ( + image_data["image"]["gcsUri"] + == "gs://mock_bucket/mock_image.png" + ) + assert image_data["image"]["mimeType"] == "image/png" + assert image_data["image"]["encodedImage"] == base64.b64encode( + b"mock_image_bytes" + ).decode("utf-8") + + +class TestImagenSearchService: + """Tests for the ImagenSearchService class.""" + + def test_imagen_search_service(self, monkeypatch, mock_genai_client): + + # Mock the google.auth.default to avoid authentication issues + with monkeypatch.context() as m: + mock_client_class = MagicMock(return_value=mock_genai_client) + m.setattr("src.service.search.genai.Client", mock_client_class) + m.setattr( + "src.service.search.google.auth.default", + lambda: (None, "test_project_id"), + ) + # Mock OpenCV functions used within the service + mock_cascade = MagicMock() + mock_cascade.detectMultiScale.return_value = [ + (10, 10, 50, 50) + ] # Mock finding one face + m.setattr( + "src.service.search.cv2.CascadeClassifier", + lambda x: mock_cascade, + ) + # Mock imdecode to return a dummy numpy array with a shape + mock_img_array = np.zeros((100, 100, 3), dtype=np.uint8) + m.setattr( + "src.service.search.cv2.imdecode", + lambda buf, flags: mock_img_array, + ) + # Mock cvtColor to return a dummy grayscale array + mock_gray_array = np.zeros((100, 100), dtype=np.uint8) + m.setattr( + "src.service.search.cv2.cvtColor", + lambda img, code: mock_gray_array, + ) + + valid_image_bytes = create_minimal_image_bytes() + search_request = CreateSearchRequest( + term="a dog playing fetch", + user_image=valid_image_bytes, + number_of_images=2, + mask_distilation=0.1, + generation_model="imagen-3.0-capability-001", + ) + service = ImagenSearchService() + results = service.generate_images(search_request) + + assert isinstance(results, list) + assert len(results) == 8 + assert all( + isinstance(result, ImageGenerationResult) for result in results + ) + mock_client_class.assert_called_once() + + for result in results: + assert isinstance(result.image, CustomImageResult) + assert result.image.encoded_image == base64.b64encode( + b"mock_image_bytes" + ).decode("utf-8") diff --git a/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/docker-compose.yml b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/docker-compose.yml new file mode 100644 index 0000000..2eca7de --- /dev/null +++ b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/docker-compose.yml @@ -0,0 +1,48 @@ +# Copyright 2025 Google LLC +# +# 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 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +services: + backend: + build: + context: ./backend + dockerfile: Dockerfile + container_name: quickbot-linkedin-image-generation-backend + ports: + - "8080:8080" + volumes: + - ./backend:/app + # Mount the gcloud ADC directory. Replace with the correct path for your OS if different. :ro - read-only for better security + - ~/.config/gcloud/:/root/.config/gcloud:ro # Linux/macOS example: + # - %APPDATA%/gcloud:/root/.config/gcloud:ro # Windows example: + environment: + ENVIRONMENT: development + FRONTEND_URL: http://localhost:4200 + GCLOUD_PROJECT: my-project-id + GOOGLE_APPLICATION_CREDENTIALS: "/root/.config/gcloud/application_default_credentials.json" + restart: unless-stopped + + frontend: + build: + context: ./frontend + dockerfile: Dockerfile + container_name: quickbot-linkedin-image-generation-frontend + ports: + - "4200:8080" + volumes: + - ./frontend:/app + # Use an anonymous volume for node_modules to prevent host node_modules + # from interfering and potentially speed up builds/rebuilds + - /app/node_modules + # To change the env variables in the frontend, change the environment.ts file + restart: unless-stopped \ No newline at end of file diff --git a/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/.dockerignore b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/.dockerignore new file mode 100644 index 0000000..7109f56 --- /dev/null +++ b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/.dockerignore @@ -0,0 +1,4 @@ +.angular +.vscode +dist +node_modules \ No newline at end of file diff --git a/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/.editorconfig b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/.editorconfig new file mode 100644 index 0000000..79fe802 --- /dev/null +++ b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/.editorconfig @@ -0,0 +1,8 @@ +root = true + +[*] +indent_style = space +indent_size = 2 +end_of_line = lf +charset = utf-8 +insert_final_newline = true diff --git a/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/.eslintignore b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/.eslintignore new file mode 100644 index 0000000..edd9d60 --- /dev/null +++ b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/.eslintignore @@ -0,0 +1,2 @@ +build/ +dist/ diff --git a/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/.eslintrc.json b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/.eslintrc.json new file mode 100644 index 0000000..f95bb33 --- /dev/null +++ b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/.eslintrc.json @@ -0,0 +1,3 @@ +{ + "extends": "./node_modules/gts/" +} diff --git a/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/.gitignore b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/.gitignore new file mode 100644 index 0000000..0711527 --- /dev/null +++ b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/.gitignore @@ -0,0 +1,42 @@ +# See http://help.github.com/ignore-files/ for more about ignoring files. + +# Compiled output +/dist +/tmp +/out-tsc +/bazel-out + +# Node +/node_modules +npm-debug.log +yarn-error.log + +# IDEs and editors +.idea/ +.project +.classpath +.c9/ +*.launch +.settings/ +*.sublime-workspace + +# Visual Studio Code +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +.history/* + +# Miscellaneous +/.angular/cache +.sass-cache/ +/connect.lock +/coverage +/libpeerconnection.log +testem.log +/typings + +# System files +.DS_Store +Thumbs.db diff --git a/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/.prettierrc.js b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/.prettierrc.js new file mode 100644 index 0000000..535df79 --- /dev/null +++ b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/.prettierrc.js @@ -0,0 +1,33 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +module.exports = { + ...require('gts/.prettierrc.json'), + "overrides": [ + { + "files": "*.html", + "options": { + "parser": "angular" + } + }, + { + "files": "*.scss", + "options": { + "parser": "scss" + } + } + ] +} diff --git a/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/Dockerfile b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/Dockerfile new file mode 100644 index 0000000..e2b6e1c --- /dev/null +++ b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/Dockerfile @@ -0,0 +1,27 @@ +# Copyright 2025 Google LLC +# +# 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 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +FROM node:18.17.1-alpine AS builder + +WORKDIR /app +COPY . /app/ + +RUN npm ci +RUN npm run build:prod + +FROM nginx:alpine +COPY --from=builder /app/dist/quick-bot-app-frontend /usr/share/nginx/html +COPY --from=builder /app/nginx.conf /etc/nginx/conf.d/ + +EXPOSE 8080 \ No newline at end of file diff --git a/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/README.md b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/README.md new file mode 100644 index 0000000..bde160a --- /dev/null +++ b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/README.md @@ -0,0 +1,93 @@ +# QuickBot App Frontend | image-background-changer-using-imagen3 + +[![linting: pylint](https://img.shields.io/badge/linting-pylint-yellowgreen)](https://github.com/pylint-dev/pylint) +[![Code Style: Google](https://img.shields.io/badge/code%20style-google-blueviolet.svg)](https://github.com/google/gts) + +QuickBot App is a set of templates that can be deployed out of the box into Cloud Run and work independently. Each one can be run independently connected to the user default google cloud auth credentials, and based on the complexity of each template, may require to deploy more or less resources into our Google Cloud Project. +The architecture always follows the following structure: a folder for the frontend which consists in an Angular app, and a backend folder which consists of a FastAPI Python app. + +This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 15.1.3. + +## Development server + +Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The application will automatically reload if you change any of the source files. + +## Code scaffolding + +Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`. + +## Build + +Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. + +## Running unit tests + +Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io). + +## Running end-to-end tests + +Run `ng e2e` to execute the end-to-end tests via a platform of your choice. To use this command, you need to first add a package that implements end-to-end testing capabilities. + +## Further help + +To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.io/cli) page. + +## Code Styling & Commit Guidelines + +To maintain code quality and consistency: + +* **TypeScript (Frontend):** We follow [Angular Coding Style Guide](https://angular.dev/style-guide) by leveraging the use of [Google's TypeScript Style Guide](https://github.com/google/gts) using `gts`. This includes a formatter, linter, and automatic code fixer. +* **Python (Backend):** We adhere to the [Google Python Style Guide](https://google.github.io/styleguide/pyguide.html), using tools like `pylint` and `black` for linting and formatting. +* **Commit Messages:** We suggest following [Angular's Commit Message Guidelines](https://github.com/angular/angular/blob/main/contributing-docs/commit-message-guidelines.md) to create clear and descriptive commit messages. + +#### Frontend (TypeScript with `gts`) + +1. **Initialize `gts` (if not already done in the project):** + Navigate to the `frontend/` directory and run: + ```bash + npx gts init + ``` + This will set up `gts` and create necessary configuration files (like `tsconfig.json`). Ensure your `tsconfig.json` (or a related gts config file like `.gtsrc`) includes an extension for `gts` defaults, typically: + ```json + { + "extends": "./node_modules/gts/tsconfig-google.json", + // ... other configurations + } + ``` +2. **Check for linting issues:** + ```bash + npm run lint + ``` + (This assumes a `lint` script is defined in `package.json`, e.g., `"lint": "gts lint"`) +3. **Fix linting issues automatically (where possible):** + ```bash + npm run fix + ``` + (This assumes a `fix` script is defined in `package.json`, e.g., `"fix": "gts fix"`) + +#### Backend (Python with `pylint` and `black`) + +1. **Ensure Dependencies are Installed:** + Add `pylint` and `black` to your `backend/requirements.txt` file: + ``` + pylint + black + ``` + Then install them within your virtual environment: + ```bash + pip install pylint black + # or pip install -r requirements.txt + ``` +2. **Configure `pylint`:** + It's recommended to have a `.pylintrc` file in your `backend/` directory to configure `pylint` rules. You might need to copy a standard one or generate one (`pylint --generate-rcfile > .pylintrc`). +3. **Check for linting issues with `pylint`:** + Navigate to the `backend/` directory and run: + ```bash + pylint . + ``` + (Or specify modules/packages: `pylint your_module_name`) +4. **Format code with `black`:** + To automatically format all Python files in the current directory and subdirectories: + ```bash + python -m black . --line-length=80 + ``` \ No newline at end of file diff --git a/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/angular.json b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/angular.json new file mode 100644 index 0000000..15d3e38 --- /dev/null +++ b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/angular.json @@ -0,0 +1,123 @@ +{ + "$schema": "./node_modules/@angular/cli/lib/config/schema.json", + "version": 1, + "newProjectRoot": "projects", + "projects": { + "quick-bot-app-frontend": { + "projectType": "application", + "schematics": { + "@schematics/angular:component": { + "style": "scss" + } + }, + "root": "", + "sourceRoot": "src", + "prefix": "app", + "architect": { + "build": { + "builder": "@angular-devkit/build-angular:browser", + "options": { + "outputPath": "dist/quick-bot-app-frontend", + "index": "src/index.html", + "main": "src/main.ts", + "polyfills": [ + "zone.js" + ], + "tsConfig": "tsconfig.app.json", + "inlineStyleLanguage": "scss", + "assets": [ + "src/favicon.ico", + "src/assets" + ], + "styles": [ + "@angular/material/prebuilt-themes/indigo-pink.css", + "src/styles.scss", + "node_modules/prismjs/themes/prism-okaidia.css" + ], + "scripts": [ + "node_modules/marked/marked.min.js", + "node_modules/prismjs/prism.js", + "node_modules/prismjs/components/prism-csharp.min.js", + "node_modules/prismjs/components/prism-css.min.js", + "node_modules/clipboard/dist/clipboard.min.js" + ] + }, + "configurations": { + "production": { + "budgets": [ + { + "type": "initial", + "maximumWarning": "4mb", + "maximumError": "10mb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "20kb", + "maximumError": "50kb" + } + ], + "outputHashing": "all" + }, + "development": { + "buildOptimizer": false, + "optimization": false, + "vendorChunk": true, + "extractLicenses": false, + "sourceMap": true, + "namedChunks": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "builder": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "browserTarget": "quick-bot-app-frontend:build:production" + }, + "development": { + "browserTarget": "quick-bot-app-frontend:build:development" + } + }, + "defaultConfiguration": "development" + }, + "extract-i18n": { + "builder": "@angular-devkit/build-angular:extract-i18n", + "options": { + "browserTarget": "quick-bot-app-frontend:build" + } + }, + "test": { + "builder": "@angular-devkit/build-angular:karma", + "options": { + "polyfills": [ + "zone.js", + "zone.js/testing" + ], + "tsConfig": "tsconfig.spec.json", + "inlineStyleLanguage": "scss", + "assets": [ + "src/favicon.ico", + "src/assets" + ], + "styles": [ + "@angular/material/prebuilt-themes/indigo-pink.css", + "src/styles.scss", + "node_modules/prismjs/themes/prism-okaidia.css" + ], + "scripts": [ + "node_modules/marked/marked.min.js", + "node_modules/prismjs/prism.js", + "node_modules/prismjs/components/prism-csharp.min.js", + "node_modules/prismjs/components/prism-css.min.js", + "node_modules/clipboard/dist/clipboard.min.js" + ] + } + } + } + } + }, + "cli": { + "analytics": false + } +} diff --git a/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/nginx.conf b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/nginx.conf new file mode 100644 index 0000000..6c8cce4 --- /dev/null +++ b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/nginx.conf @@ -0,0 +1,11 @@ +server { + listen 8080; + + root /usr/share/nginx/html; + index index.html index.htm; + include /etc/nginx/mime.types; + + location / { + try_files $uri $uri/ /index.html; + } +} \ No newline at end of file diff --git a/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/package-lock.json b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/package-lock.json new file mode 100644 index 0000000..ec556a1 --- /dev/null +++ b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/package-lock.json @@ -0,0 +1,25820 @@ +{ + "name": "quick-bot-app-frontend", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "quick-bot-app-frontend", + "version": "0.0.0", + "hasInstallScript": true, + "license": "ISC", + "dependencies": { + "@angular/animations": "^15.1.0", + "@angular/cdk": "^15.2.9", + "@angular/common": "^15.1.0", + "@angular/compiler": "^15.1.0", + "@angular/core": "^15.1.0", + "@angular/fire": "7.5", + "@angular/flex-layout": "^15.0.0-beta.42", + "@angular/forms": "^15.1.0", + "@angular/material": "^15.2.9", + "@angular/platform-browser": "^15.1.0", + "@angular/platform-browser-dynamic": "^15.1.0", + "@angular/router": "^15.1.0", + "@fortawesome/fontawesome-free": "^6.6.0", + "@ionic/angular": "^7.5.4", + "@ng-idle/core": "^14.0.0", + "autoprefixer": "^10.4.21", + "clipboard": "^2.0.11", + "ionic-emoji-rating": "^1.0.15", + "marked": "^4.3.0", + "moment": "^2.30.1", + "ng2-pdf-viewer": "^7.0.2", + "ngx-markdown": "^15.1.2", + "postcss": "^8.5.3", + "prismjs": "^1.29.0", + "rxfire": "6.0.3", + "rxjs": "~7.8.0", + "tailwindcss": "^3.4.17", + "tslib": "^2.3.0", + "uuid": "^9.0.1", + "zone.js": "~0.12.0" + }, + "devDependencies": { + "@angular-devkit/build-angular": "^15.1.3", + "@angular/cli": "~15.1.3", + "@angular/compiler-cli": "^15.1.0", + "@types/jasmine": "~4.3.0", + "@types/marked": "^4.3.0", + "@types/node": "20.12.7", + "@types/uuid": "^9.0.4", + "firebase-tools": "^11.30.0", + "gts": "^5.3.1", + "jasmine-core": "~4.5.0", + "karma": "~6.4.0", + "karma-chrome-launcher": "~3.1.0", + "karma-coverage": "~2.2.0", + "karma-jasmine": "~5.1.0", + "karma-jasmine-html-reporter": "~2.0.0", + "prettier": "^3.5.1", + "typescript": "~4.9.4" + } + }, + "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/@ampproject/remapping": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.0.tgz", + "integrity": "sha512-qRmjj8nj9qmLTQXXmaR1cck3UXSRMPrbsLJAasZpF+t3riI71BXed5ebIOYwQntykeZuhjsdweEc9BxH5Jc26w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jridgewell/gen-mapping": "^0.1.0", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@angular-devkit/architect": { + "version": "0.1502.11", + "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1502.11.tgz", + "integrity": "sha512-+hkG5UjIaKMRdo6SFLNQs+Cv7yAVeN8ijfDwI2z/mp7/otowuSEy+H3Tii195jfJ8TQ+y1B7svnx2D6O7oOYbQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "15.2.11", + "rxjs": "6.6.7" + }, + "engines": { + "node": "^14.20.0 || ^16.13.0 || >=18.10.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@angular-devkit/architect/node_modules/rxjs": { + "version": "6.6.7", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", + "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^1.9.0" + }, + "engines": { + "npm": ">=2.0.0" + } + }, + "node_modules/@angular-devkit/architect/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true, + "license": "0BSD" + }, + "node_modules/@angular-devkit/build-angular": { + "version": "15.2.11", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-angular/-/build-angular-15.2.11.tgz", + "integrity": "sha512-MnpVCJdk5jHuK7CH/cTcRT0JQkkKkRTEV3WTyOUhTm0O3PlKwvTM6/Sner+zyuhKyw5VFBBMypHh59aTUDEZ1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "2.2.0", + "@angular-devkit/architect": "0.1502.11", + "@angular-devkit/build-webpack": "0.1502.11", + "@angular-devkit/core": "15.2.11", + "@babel/core": "7.20.12", + "@babel/generator": "7.20.14", + "@babel/helper-annotate-as-pure": "7.18.6", + "@babel/helper-split-export-declaration": "7.18.6", + "@babel/plugin-proposal-async-generator-functions": "7.20.7", + "@babel/plugin-transform-async-to-generator": "7.20.7", + "@babel/plugin-transform-runtime": "7.19.6", + "@babel/preset-env": "7.20.2", + "@babel/runtime": "7.20.13", + "@babel/template": "7.20.7", + "@discoveryjs/json-ext": "0.5.7", + "@ngtools/webpack": "15.2.11", + "ansi-colors": "4.1.3", + "autoprefixer": "10.4.13", + "babel-loader": "9.1.2", + "babel-plugin-istanbul": "6.1.1", + "browserslist": "4.21.5", + "cacache": "17.0.4", + "chokidar": "3.5.3", + "copy-webpack-plugin": "11.0.0", + "critters": "0.0.16", + "css-loader": "6.7.3", + "esbuild-wasm": "0.17.8", + "glob": "8.1.0", + "https-proxy-agent": "5.0.1", + "inquirer": "8.2.4", + "jsonc-parser": "3.2.0", + "karma-source-map-support": "1.4.0", + "less": "4.1.3", + "less-loader": "11.1.0", + "license-webpack-plugin": "4.0.2", + "loader-utils": "3.2.1", + "magic-string": "0.29.0", + "mini-css-extract-plugin": "2.7.2", + "open": "8.4.1", + "ora": "5.4.1", + "parse5-html-rewriting-stream": "7.0.0", + "piscina": "3.2.0", + "postcss": "8.4.31", + "postcss-loader": "7.0.2", + "resolve-url-loader": "5.0.0", + "rxjs": "6.6.7", + "sass": "1.58.1", + "sass-loader": "13.2.0", + "semver": "7.5.3", + "source-map-loader": "4.0.1", + "source-map-support": "0.5.21", + "terser": "5.16.3", + "text-table": "0.2.0", + "tree-kill": "1.2.2", + "tslib": "2.5.0", + "webpack": "5.76.1", + "webpack-dev-middleware": "6.1.2", + "webpack-dev-server": "4.11.1", + "webpack-merge": "5.8.0", + "webpack-subresource-integrity": "5.1.0" + }, + "engines": { + "node": "^14.20.0 || ^16.13.0 || >=18.10.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "optionalDependencies": { + "esbuild": "0.17.8" + }, + "peerDependencies": { + "@angular/compiler-cli": "^15.0.0", + "@angular/localize": "^15.0.0", + "@angular/platform-server": "^15.0.0", + "@angular/service-worker": "^15.0.0", + "karma": "^6.3.0", + "ng-packagr": "^15.0.0", + "protractor": "^7.0.0", + "tailwindcss": "^2.0.0 || ^3.0.0", + "typescript": ">=4.8.2 <5.0" + }, + "peerDependenciesMeta": { + "@angular/localize": { + "optional": true + }, + "@angular/platform-server": { + "optional": true + }, + "@angular/service-worker": { + "optional": true + }, + "karma": { + "optional": true + }, + "ng-packagr": { + "optional": true + }, + "protractor": { + "optional": true + }, + "tailwindcss": { + "optional": true + } + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/@ngtools/webpack": { + "version": "15.2.11", + "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-15.2.11.tgz", + "integrity": "sha512-yqp+FziuJ+wIVij4eTqfhuiTPNaG1PU8ukeGOdqkVH4nQMlmzs9UldXy1iYC/6swzn6XO/pkqisU3m/jxemMzA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.20.0 || ^16.13.0 || >=18.10.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "@angular/compiler-cli": "^15.0.0", + "typescript": ">=4.8.2 <5.0", + "webpack": "^5.54.0" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/autoprefixer": { + "version": "10.4.13", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.13.tgz", + "integrity": "sha512-49vKpMqcZYsJjwotvt4+h/BCjJVnhGwcLpDt5xkcaOG3eLrG/HUYLagrihYsQ+qrIBgIzX1Rw7a6L8I/ZA1Atg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + } + ], + "license": "MIT", + "dependencies": { + "browserslist": "^4.21.4", + "caniuse-lite": "^1.0.30001426", + "fraction.js": "^4.2.0", + "normalize-range": "^0.1.2", + "picocolors": "^1.0.0", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/postcss": { + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.6", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/rxjs": { + "version": "6.6.7", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", + "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^1.9.0" + }, + "engines": { + "npm": ">=2.0.0" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/rxjs/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true, + "license": "0BSD" + }, + "node_modules/@angular-devkit/build-angular/node_modules/tslib": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz", + "integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==", + "dev": true, + "license": "0BSD" + }, + "node_modules/@angular-devkit/build-webpack": { + "version": "0.1502.11", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.1502.11.tgz", + "integrity": "sha512-OTONIRp770Jfems4+cULmtoeSzjnpx5UjV2EazojnhRXXBSJMWRMPvwD2QvQl9UO/6eOV3d2mgmP2xOZgc/D6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/architect": "0.1502.11", + "rxjs": "6.6.7" + }, + "engines": { + "node": "^14.20.0 || ^16.13.0 || >=18.10.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "webpack": "^5.30.0", + "webpack-dev-server": "^4.0.0" + } + }, + "node_modules/@angular-devkit/build-webpack/node_modules/rxjs": { + "version": "6.6.7", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", + "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^1.9.0" + }, + "engines": { + "npm": ">=2.0.0" + } + }, + "node_modules/@angular-devkit/build-webpack/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true, + "license": "0BSD" + }, + "node_modules/@angular-devkit/core": { + "version": "15.2.11", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-15.2.11.tgz", + "integrity": "sha512-zd6QelJ8pOPvz6TsehR0JqixjDjzgEOkKywBJBuwNXY+Nw3MJGayJeWS0UgC+Gk+LoTkpI21RoyaYELkAmD/tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "8.12.0", + "ajv-formats": "2.1.1", + "jsonc-parser": "3.2.0", + "rxjs": "6.6.7", + "source-map": "0.7.4" + }, + "engines": { + "node": "^14.20.0 || ^16.13.0 || >=18.10.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "chokidar": "^3.5.2" + }, + "peerDependenciesMeta": { + "chokidar": { + "optional": true + } + } + }, + "node_modules/@angular-devkit/core/node_modules/rxjs": { + "version": "6.6.7", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", + "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^1.9.0" + }, + "engines": { + "npm": ">=2.0.0" + } + }, + "node_modules/@angular-devkit/core/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true, + "license": "0BSD" + }, + "node_modules/@angular-devkit/schematics": { + "version": "15.1.6", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-15.1.6.tgz", + "integrity": "sha512-cwmJFpS43zrdlmfwfHIxG/Nzg5rzFdtKrHx64ZXxNFm6JdyK2JTs/qrHUwv1FYWAcqhdiHn+00jYklMmvsvPOA==", + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "15.1.6", + "jsonc-parser": "3.2.0", + "magic-string": "0.27.0", + "ora": "5.4.1", + "rxjs": "6.6.7" + }, + "engines": { + "node": "^14.20.0 || ^16.13.0 || >=18.10.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@angular-devkit/schematics/node_modules/@angular-devkit/core": { + "version": "15.1.6", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-15.1.6.tgz", + "integrity": "sha512-jGgxyRjecVf6lEyqDxz7ltMEndNPxIg720pk6r40fgsu0dU8w9vjJSJe7k0XdJiXVRcN6wZa/J5nO/xcwWVIsA==", + "license": "MIT", + "dependencies": { + "ajv": "8.12.0", + "ajv-formats": "2.1.1", + "jsonc-parser": "3.2.0", + "rxjs": "6.6.7", + "source-map": "0.7.4" + }, + "engines": { + "node": "^14.20.0 || ^16.13.0 || >=18.10.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "chokidar": "^3.5.2" + }, + "peerDependenciesMeta": { + "chokidar": { + "optional": true + } + } + }, + "node_modules/@angular-devkit/schematics/node_modules/magic-string": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.27.0.tgz", + "integrity": "sha512-8UnnX2PeRAPZuN12svgR9j7M1uWMovg/CEnIwIG0LFkXSJJe4PdfUGiTGl8V9bsBHFUtfVINcSyYxd7q+kx9fA==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.13" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@angular-devkit/schematics/node_modules/rxjs": { + "version": "6.6.7", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", + "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^1.9.0" + }, + "engines": { + "npm": ">=2.0.0" + } + }, + "node_modules/@angular-devkit/schematics/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "license": "0BSD" + }, + "node_modules/@angular/animations": { + "version": "15.2.10", + "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-15.2.10.tgz", + "integrity": "sha512-yxfN8qQpMaukRU5LjFkJBmy85rqrOp86tYVCsf+hmPEFRiXBMUj6xYLeCMcpk3Mt1JtnWGBR34ivGx+7bNeAow==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^14.20.0 || ^16.13.0 || >=18.10.0" + }, + "peerDependencies": { + "@angular/core": "15.2.10" + } + }, + "node_modules/@angular/cdk": { + "version": "15.2.9", + "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-15.2.9.tgz", + "integrity": "sha512-koaM07N1AIQ5oHU27l0/FoQSSoYAwlAYwVZ4Di3bYrJsTBNCN2Xsby7wI8gZxdepMnV4Fe9si382BDBov+oO4Q==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "optionalDependencies": { + "parse5": "^7.1.2" + }, + "peerDependencies": { + "@angular/common": "^15.0.0 || ^16.0.0", + "@angular/core": "^15.0.0 || ^16.0.0", + "rxjs": "^6.5.3 || ^7.4.0" + } + }, + "node_modules/@angular/cli": { + "version": "15.1.6", + "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-15.1.6.tgz", + "integrity": "sha512-GmC9jZK2ipUWj0dlfTI5oEYia4y1fLfO3AtAKU5CylNYrGyB+DRytKY8Bx6Fs4kaNBY8V8YnyLi7E/78gziMdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/architect": "0.1501.6", + "@angular-devkit/core": "15.1.6", + "@angular-devkit/schematics": "15.1.6", + "@schematics/angular": "15.1.6", + "@yarnpkg/lockfile": "1.1.0", + "ansi-colors": "4.1.3", + "ini": "3.0.1", + "inquirer": "8.2.4", + "jsonc-parser": "3.2.0", + "npm-package-arg": "10.1.0", + "npm-pick-manifest": "8.0.1", + "open": "8.4.0", + "ora": "5.4.1", + "pacote": "15.0.8", + "resolve": "1.22.1", + "semver": "7.3.8", + "symbol-observable": "4.0.0", + "yargs": "17.6.2" + }, + "bin": { + "ng": "bin/ng.js" + }, + "engines": { + "node": "^14.20.0 || ^16.13.0 || >=18.10.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@angular/cli/node_modules/@angular-devkit/architect": { + "version": "0.1501.6", + "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1501.6.tgz", + "integrity": "sha512-u07zZFlfrg0Qn4mu5M9Nz0pH2Yd2028XF/73980PsZMxwkSm4diF08v4bHk3UyR7yPT7phwvt4znj6ryZhx1gw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "15.1.6", + "rxjs": "6.6.7" + }, + "engines": { + "node": "^14.20.0 || ^16.13.0 || >=18.10.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@angular/cli/node_modules/@angular-devkit/core": { + "version": "15.1.6", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-15.1.6.tgz", + "integrity": "sha512-jGgxyRjecVf6lEyqDxz7ltMEndNPxIg720pk6r40fgsu0dU8w9vjJSJe7k0XdJiXVRcN6wZa/J5nO/xcwWVIsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "8.12.0", + "ajv-formats": "2.1.1", + "jsonc-parser": "3.2.0", + "rxjs": "6.6.7", + "source-map": "0.7.4" + }, + "engines": { + "node": "^14.20.0 || ^16.13.0 || >=18.10.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "chokidar": "^3.5.2" + }, + "peerDependenciesMeta": { + "chokidar": { + "optional": true + } + } + }, + "node_modules/@angular/cli/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@angular/cli/node_modules/open": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/open/-/open-8.4.0.tgz", + "integrity": "sha512-XgFPPM+B28FtCCgSb9I+s9szOC1vZRSwgWsRUA5ylIxRTgKozqjOCrVOqGsYABPYK5qnfqClxZTFBa8PKt2v6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-lazy-prop": "^2.0.0", + "is-docker": "^2.1.1", + "is-wsl": "^2.2.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@angular/cli/node_modules/rxjs": { + "version": "6.6.7", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", + "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^1.9.0" + }, + "engines": { + "npm": ">=2.0.0" + } + }, + "node_modules/@angular/cli/node_modules/semver": { + "version": "7.3.8", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", + "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", + "dev": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@angular/cli/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true, + "license": "0BSD" + }, + "node_modules/@angular/cli/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/@angular/common": { + "version": "15.2.10", + "resolved": "https://registry.npmjs.org/@angular/common/-/common-15.2.10.tgz", + "integrity": "sha512-jdBn3fctkqoNrJn9VLsUHpcCEhCxWSczdsR+BBbD6T0oLl6vMrAVNjPwfBejnlgfWN1KoRU9kgOYsMxa5apIWQ==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^14.20.0 || ^16.13.0 || >=18.10.0" + }, + "peerDependencies": { + "@angular/core": "15.2.10", + "rxjs": "^6.5.3 || ^7.4.0" + } + }, + "node_modules/@angular/compiler": { + "version": "15.2.10", + "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-15.2.10.tgz", + "integrity": "sha512-M0XkeU0O73UlJZwDvOyp8/apetz9UKj78eTFDseMYJDLcxe6MpkbkxqpsGZnKYDj7LIep8PmCAKEkhtenE82zw==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^14.20.0 || ^16.13.0 || >=18.10.0" + }, + "peerDependencies": { + "@angular/core": "15.2.10" + }, + "peerDependenciesMeta": { + "@angular/core": { + "optional": true + } + } + }, + "node_modules/@angular/compiler-cli": { + "version": "15.2.10", + "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-15.2.10.tgz", + "integrity": "sha512-mCFIxrs60XicKfA2o42hA7LrQvhybi9BQveWuZn/2iIEOXx7R62Iemz8E21pLWftAZHGxEW3NECfBrY1d3gVmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "7.19.3", + "@jridgewell/sourcemap-codec": "^1.4.14", + "chokidar": "^3.0.0", + "convert-source-map": "^1.5.1", + "dependency-graph": "^0.11.0", + "magic-string": "^0.27.0", + "reflect-metadata": "^0.1.2", + "semver": "^7.0.0", + "tslib": "^2.3.0", + "yargs": "^17.2.1" + }, + "bin": { + "ng-xi18n": "bundles/src/bin/ng_xi18n.js", + "ngc": "bundles/src/bin/ngc.js", + "ngcc": "bundles/ngcc/main-ngcc.js" + }, + "engines": { + "node": "^14.20.0 || ^16.13.0 || >=18.10.0" + }, + "peerDependencies": { + "@angular/compiler": "15.2.10", + "typescript": ">=4.8.2 <5.0" + } + }, + "node_modules/@angular/compiler-cli/node_modules/@babel/core": { + "version": "7.19.3", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.19.3.tgz", + "integrity": "sha512-WneDJxdsjEvyKtXKsaBGbDeiyOjR5vYq4HcShxnIbG0qixpoHjI3MqeZM9NDvsojNCEBItQE4juOo/bU6e72gQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.1.0", + "@babel/code-frame": "^7.18.6", + "@babel/generator": "^7.19.3", + "@babel/helper-compilation-targets": "^7.19.3", + "@babel/helper-module-transforms": "^7.19.0", + "@babel/helpers": "^7.19.0", + "@babel/parser": "^7.19.3", + "@babel/template": "^7.18.10", + "@babel/traverse": "^7.19.3", + "@babel/types": "^7.19.3", + "convert-source-map": "^1.7.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.1", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@angular/compiler-cli/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/@angular/compiler-cli/node_modules/magic-string": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.27.0.tgz", + "integrity": "sha512-8UnnX2PeRAPZuN12svgR9j7M1uWMovg/CEnIwIG0LFkXSJJe4PdfUGiTGl8V9bsBHFUtfVINcSyYxd7q+kx9fA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.13" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@angular/core": { + "version": "15.2.10", + "resolved": "https://registry.npmjs.org/@angular/core/-/core-15.2.10.tgz", + "integrity": "sha512-meGGidnitQJGDxYd9/LrqYiVlId+vGaLoiLgJdKBz+o2ZO6OmXQGuNw2VBqf17/Cc0/UjzrOY7+kILNFKkk/WQ==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^14.20.0 || ^16.13.0 || >=18.10.0" + }, + "peerDependencies": { + "rxjs": "^6.5.3 || ^7.4.0", + "zone.js": "~0.11.4 || ~0.12.0 || ~0.13.0" + } + }, + "node_modules/@angular/fire": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/@angular/fire/-/fire-7.5.0.tgz", + "integrity": "sha512-M/JJ5SKnpfNZFLKl0Awjn3r2NhURy2Mx+E8EzSDWSg1sHD7IThrynZYPUSZR+cd3KGsGoEacIP2x8Ra+0a+CVg==", + "license": "MIT", + "dependencies": { + "@angular-devkit/schematics": "^12.0.0 || ^13.0.0 || ^14.0.0 || ^15.0.0", + "@schematics/angular": "^12.0.0 || ^13.0.0 || ^14.0.0 || ^15.0.0", + "file-loader": "^6.2.0", + "firebase": "^9.8.0", + "fs-extra": "^8.0.1", + "fuzzy": "^0.1.3", + "inquirer": "^8.1.1", + "inquirer-autocomplete-prompt": "^1.0.1", + "jsonc-parser": "^3.0.0", + "node-fetch": "^2.6.1", + "open": "^8.0.0", + "ora": "^5.3.0", + "rxfire": "^6.0.0", + "semver": "^7.1.3", + "triple-beam": "^1.3.0", + "tslib": "^2.0.0", + "winston": "^3.0.0" + }, + "peerDependencies": { + "@angular/common": "^12.0.0 || ^13.0.0 || ^14.0.0 || ^15.0.0", + "@angular/core": "^12.0.0 || ^13.0.0 || ^14.0.0 || ^15.0.0", + "@angular/platform-browser": "^12.0.0 || ^13.0.0 || ^14.0.0 || ^15.0.0", + "@angular/platform-browser-dynamic": "^12.0.0 || ^13.0.0 || ^14.0.0 || ^15.0.0", + "firebase-tools": "^9.9.0 || ^10.0.0 || ^11.0.0", + "rxjs": "~6.6.0 || ^7.0.0" + }, + "peerDependenciesMeta": { + "firebase-tools": { + "optional": true + } + } + }, + "node_modules/@angular/flex-layout": { + "version": "15.0.0-beta.42", + "resolved": "https://registry.npmjs.org/@angular/flex-layout/-/flex-layout-15.0.0-beta.42.tgz", + "integrity": "sha512-cTAPVMMxnyIFwpZwdq0PL5mdP9Qh+R8MB7ZBezVaN3Rz2fRrkagzKpLvPX3TFzepXrvHBdpKsU4b8u+NxEC/6g==", + "deprecated": "This package has been deprecated. Please see https://blog.angular.io/modern-css-in-angular-layouts-4a259dca9127", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "peerDependencies": { + "@angular/cdk": ">=15.0.0", + "@angular/common": ">=15.0.2", + "@angular/core": ">=15.0.2", + "@angular/platform-browser": ">=15.0.2", + "rxjs": "^6.5.3 || ^7.4.0" + } + }, + "node_modules/@angular/forms": { + "version": "15.2.10", + "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-15.2.10.tgz", + "integrity": "sha512-NIntGsNcN6o8L1txsbWXOf6f3K/CUBizdKsxsYVYGJIXEW5qU6UnWmfAZffNNXsT/XvbgUCjgDwT0cAwcqZPuQ==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^14.20.0 || ^16.13.0 || >=18.10.0" + }, + "peerDependencies": { + "@angular/common": "15.2.10", + "@angular/core": "15.2.10", + "@angular/platform-browser": "15.2.10", + "rxjs": "^6.5.3 || ^7.4.0" + } + }, + "node_modules/@angular/material": { + "version": "15.2.9", + "resolved": "https://registry.npmjs.org/@angular/material/-/material-15.2.9.tgz", + "integrity": "sha512-emuFF/7+91Jq+6kVCl3FiVoFLtAZoh+woFQWNuK8nhx0HmD4ckLFI8d9a6ERYR3zRuKhq5deSRE2kYsfpjrrsQ==", + "license": "MIT", + "dependencies": { + "@material/animation": "15.0.0-canary.684e33d25.0", + "@material/auto-init": "15.0.0-canary.684e33d25.0", + "@material/banner": "15.0.0-canary.684e33d25.0", + "@material/base": "15.0.0-canary.684e33d25.0", + "@material/button": "15.0.0-canary.684e33d25.0", + "@material/card": "15.0.0-canary.684e33d25.0", + "@material/checkbox": "15.0.0-canary.684e33d25.0", + "@material/chips": "15.0.0-canary.684e33d25.0", + "@material/circular-progress": "15.0.0-canary.684e33d25.0", + "@material/data-table": "15.0.0-canary.684e33d25.0", + "@material/density": "15.0.0-canary.684e33d25.0", + "@material/dialog": "15.0.0-canary.684e33d25.0", + "@material/dom": "15.0.0-canary.684e33d25.0", + "@material/drawer": "15.0.0-canary.684e33d25.0", + "@material/elevation": "15.0.0-canary.684e33d25.0", + "@material/fab": "15.0.0-canary.684e33d25.0", + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "@material/floating-label": "15.0.0-canary.684e33d25.0", + "@material/form-field": "15.0.0-canary.684e33d25.0", + "@material/icon-button": "15.0.0-canary.684e33d25.0", + "@material/image-list": "15.0.0-canary.684e33d25.0", + "@material/layout-grid": "15.0.0-canary.684e33d25.0", + "@material/line-ripple": "15.0.0-canary.684e33d25.0", + "@material/linear-progress": "15.0.0-canary.684e33d25.0", + "@material/list": "15.0.0-canary.684e33d25.0", + "@material/menu": "15.0.0-canary.684e33d25.0", + "@material/menu-surface": "15.0.0-canary.684e33d25.0", + "@material/notched-outline": "15.0.0-canary.684e33d25.0", + "@material/radio": "15.0.0-canary.684e33d25.0", + "@material/ripple": "15.0.0-canary.684e33d25.0", + "@material/rtl": "15.0.0-canary.684e33d25.0", + "@material/segmented-button": "15.0.0-canary.684e33d25.0", + "@material/select": "15.0.0-canary.684e33d25.0", + "@material/shape": "15.0.0-canary.684e33d25.0", + "@material/slider": "15.0.0-canary.684e33d25.0", + "@material/snackbar": "15.0.0-canary.684e33d25.0", + "@material/switch": "15.0.0-canary.684e33d25.0", + "@material/tab": "15.0.0-canary.684e33d25.0", + "@material/tab-bar": "15.0.0-canary.684e33d25.0", + "@material/tab-indicator": "15.0.0-canary.684e33d25.0", + "@material/tab-scroller": "15.0.0-canary.684e33d25.0", + "@material/textfield": "15.0.0-canary.684e33d25.0", + "@material/theme": "15.0.0-canary.684e33d25.0", + "@material/tooltip": "15.0.0-canary.684e33d25.0", + "@material/top-app-bar": "15.0.0-canary.684e33d25.0", + "@material/touch-target": "15.0.0-canary.684e33d25.0", + "@material/typography": "15.0.0-canary.684e33d25.0", + "tslib": "^2.3.0" + }, + "peerDependencies": { + "@angular/animations": "^15.0.0 || ^16.0.0", + "@angular/cdk": "15.2.9", + "@angular/common": "^15.0.0 || ^16.0.0", + "@angular/core": "^15.0.0 || ^16.0.0", + "@angular/forms": "^15.0.0 || ^16.0.0", + "@angular/platform-browser": "^15.0.0 || ^16.0.0", + "rxjs": "^6.5.3 || ^7.4.0" + } + }, + "node_modules/@angular/platform-browser": { + "version": "15.2.10", + "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-15.2.10.tgz", + "integrity": "sha512-9tbgVGSJqwfrOzT8aA/kWBLNhJSQ9gUg0CJxwFBSJm8VkBUJrszoBlDsnSvlxx8/W2ejNULKHFTXeUzq0O/+RQ==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^14.20.0 || ^16.13.0 || >=18.10.0" + }, + "peerDependencies": { + "@angular/animations": "15.2.10", + "@angular/common": "15.2.10", + "@angular/core": "15.2.10" + }, + "peerDependenciesMeta": { + "@angular/animations": { + "optional": true + } + } + }, + "node_modules/@angular/platform-browser-dynamic": { + "version": "15.2.10", + "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-15.2.10.tgz", + "integrity": "sha512-JHP6W+FX715Qv7DhqvfZLuBZXSDJrboiQsR06gUAgDSjAUyhbqmpVg/2YOtgeWpPkzNDtXdPU2PhcRdIv5J3Yg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^14.20.0 || ^16.13.0 || >=18.10.0" + }, + "peerDependencies": { + "@angular/common": "15.2.10", + "@angular/compiler": "15.2.10", + "@angular/core": "15.2.10", + "@angular/platform-browser": "15.2.10" + } + }, + "node_modules/@angular/router": { + "version": "15.2.10", + "resolved": "https://registry.npmjs.org/@angular/router/-/router-15.2.10.tgz", + "integrity": "sha512-LmuqEg0iIXSw7bli6HKJ19cbxP91v37GtRwbGKswyLihqzTgvjBYpvcfMnB5FRQ5LWkTwq5JclkX03dZw290Yg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^14.20.0 || ^16.13.0 || >=18.10.0" + }, + "peerDependencies": { + "@angular/common": "15.2.10", + "@angular/core": "15.2.10", + "@angular/platform-browser": "15.2.10", + "rxjs": "^6.5.3 || ^7.4.0" + } + }, + "node_modules/@assemblyscript/loader": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@assemblyscript/loader/-/loader-0.10.1.tgz", + "integrity": "sha512-H71nDOOL8Y7kWRLqf6Sums+01Q5msqBW2KhDUTemh1tvY04eSkSXrK0uj/4mmY0Xr16/3zyZmsrxN7CKuRbNRg==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/@babel/code-frame": { + "version": "7.26.2", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", + "integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.25.9", + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.26.5", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.26.5.tgz", + "integrity": "sha512-XvcZi1KWf88RVbF9wn8MN6tYFloU5qX8KjuF3E1PVBmJ9eypXfs4GRiJwLuTZL0iSnJUKn1BFPa5BPZZJyFzPg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.20.12", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.20.12.tgz", + "integrity": "sha512-XsMfHovsUYHFMdrIHkZphTN/2Hzzi78R08NuHfDBehym2VsPDL6Zn/JAD/JQdnRvbSsbQc4mVaU1m6JgtTEElg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.1.0", + "@babel/code-frame": "^7.18.6", + "@babel/generator": "^7.20.7", + "@babel/helper-compilation-targets": "^7.20.7", + "@babel/helper-module-transforms": "^7.20.11", + "@babel/helpers": "^7.20.7", + "@babel/parser": "^7.20.7", + "@babel/template": "^7.20.7", + "@babel/traverse": "^7.20.12", + "@babel/types": "^7.20.7", + "convert-source-map": "^1.7.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.2", + "semver": "^6.3.0" + }, + "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.20.14", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.20.14.tgz", + "integrity": "sha512-AEmuXHdcD3A52HHXxaTmYlb8q/xMEhoRP67B3T4Oq7lbmSoqroMZzjnGj3+i1io3pdnF8iBYVu4Ilj+c4hBxYg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.20.7", + "@jridgewell/gen-mapping": "^0.3.2", + "jsesc": "^2.5.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/generator/node_modules/@jridgewell/gen-mapping": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", + "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/helper-annotate-as-pure": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.18.6.tgz", + "integrity": "sha512-duORpUiYrEpzKIop6iNbjnwKLAKnJ47csTyRACyEmWj0QdUrm5aqNJGHSSEQSUAvNW0ojX0dOmK9dZduvkfeXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.26.5", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.26.5.tgz", + "integrity": "sha512-IXuyn5EkouFJscIDuFF5EsiSolseme1s0CZB+QxVugqJLYmKdxI1VfIBOst0SUu4rnk2Z7kqTwmoO1lp3HIfnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.26.5", + "@babel/helper-validator-option": "^7.25.9", + "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/browserslist": { + "version": "4.24.4", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.4.tgz", + "integrity": "sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==", + "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": { + "caniuse-lite": "^1.0.30001688", + "electron-to-chromium": "^1.5.73", + "node-releases": "^2.0.19", + "update-browserslist-db": "^1.1.1" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "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-create-class-features-plugin": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.25.9.tgz", + "integrity": "sha512-UTZQMvt0d/rSz6KI+qdu7GQze5TIajwTS++GUozlw8VBJDEOAqSXwm1WvmYEZwqdqSGQshRocPDqrt4HBZB3fQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.25.9", + "@babel/helper-member-expression-to-functions": "^7.25.9", + "@babel/helper-optimise-call-expression": "^7.25.9", + "@babel/helper-replace-supers": "^7.25.9", + "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9", + "@babel/traverse": "^7.25.9", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-create-class-features-plugin/node_modules/@babel/helper-annotate-as-pure": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.25.9.tgz", + "integrity": "sha512-gv7320KBUFJz1RnylIg5WWYPRXKZ884AGkYpgpWW02TH66Dl+HaC1t1CKd0z3R4b6hdYEcmrNZHUmfCP+1u3/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-create-class-features-plugin/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-create-regexp-features-plugin": { + "version": "7.26.3", + "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.26.3.tgz", + "integrity": "sha512-G7ZRb40uUgdKOQqPLjfD12ZmGA54PzqDFUv2BKImnC9QIfGhIHKvVML0oN8IUiDq4iRqpq74ABpvOaerfWdong==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.25.9", + "regexpu-core": "^6.2.0", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-create-regexp-features-plugin/node_modules/@babel/helper-annotate-as-pure": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.25.9.tgz", + "integrity": "sha512-gv7320KBUFJz1RnylIg5WWYPRXKZ884AGkYpgpWW02TH66Dl+HaC1t1CKd0z3R4b6hdYEcmrNZHUmfCP+1u3/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-create-regexp-features-plugin/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-define-polyfill-provider": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.3.3.tgz", + "integrity": "sha512-z5aQKU4IzbqCC1XH0nAqfsFLMVSo22SBKUc0BxGrLkolTdPTructy0ToNnlO2zA4j9Q/7pjMZf0DSY+DSTYzww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-compilation-targets": "^7.17.7", + "@babel/helper-plugin-utils": "^7.16.7", + "debug": "^4.1.1", + "lodash.debounce": "^4.0.8", + "resolve": "^1.14.2", + "semver": "^6.1.2" + }, + "peerDependencies": { + "@babel/core": "^7.4.0-0" + } + }, + "node_modules/@babel/helper-define-polyfill-provider/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-environment-visitor": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.24.7.tgz", + "integrity": "sha512-DoiN84+4Gnd0ncbBOM9AZENV4a5ZiL39HYMyZJGZ/AZEykHYdJw0wW3kdcsh9/Kn+BRXHLkkklZ51ecPKmI1CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-member-expression-to-functions": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.25.9.tgz", + "integrity": "sha512-wbfdZ9w5vk0C0oyHqAJbc62+vet5prjj01jjJ8sKn3j9h3MQQlflEdXYvuqRWjHnM12coDEqiC1IRCi0U/EKwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.25.9", + "@babel/types": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.25.9.tgz", + "integrity": "sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.25.9", + "@babel/types": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.26.0.tgz", + "integrity": "sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.25.9", + "@babel/helper-validator-identifier": "^7.25.9", + "@babel/traverse": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-optimise-call-expression": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.25.9.tgz", + "integrity": "sha512-FIpuNaz5ow8VyrYcnXQTDRGvV6tTjkNtCK/RYNDXGSLlUD6cBuQTSw43CShGxjvfBTfcUA/r6UhUCbtYqkhcuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.26.5", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.26.5.tgz", + "integrity": "sha512-RS+jZcRdZdRFzMyr+wcsaqOmld1/EqTghfaBGQQd/WnRdzdlvSZ//kF7U8VQTxf1ynZ4cjUcYgjVGx13ewNPMg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-remap-async-to-generator": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.25.9.tgz", + "integrity": "sha512-IZtukuUeBbhgOcaW2s06OXTzVNJR0ybm4W5xC1opWFFJMZbwRj5LCk+ByYH7WdZPZTt8KnFwA8pvjN2yqcPlgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.25.9", + "@babel/helper-wrap-function": "^7.25.9", + "@babel/traverse": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-remap-async-to-generator/node_modules/@babel/helper-annotate-as-pure": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.25.9.tgz", + "integrity": "sha512-gv7320KBUFJz1RnylIg5WWYPRXKZ884AGkYpgpWW02TH66Dl+HaC1t1CKd0z3R4b6hdYEcmrNZHUmfCP+1u3/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-replace-supers": { + "version": "7.26.5", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.26.5.tgz", + "integrity": "sha512-bJ6iIVdYX1YooY2X7w1q6VITt+LnUILtNk7zT78ykuwStx8BauCzxvFqFaHjOpW1bVnSUM1PN1f0p5P21wHxvg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-member-expression-to-functions": "^7.25.9", + "@babel/helper-optimise-call-expression": "^7.25.9", + "@babel/traverse": "^7.26.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-skip-transparent-expression-wrappers": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.25.9.tgz", + "integrity": "sha512-K4Du3BFa3gvyhzgPcntrkDgZzQaq6uozzcpGbOO1OEJaI+EJdqWIMTLgFgQf6lrfiDFo5FU+BxKepI9RmZqahA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.25.9", + "@babel/types": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-split-export-declaration": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.18.6.tgz", + "integrity": "sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", + "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", + "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.25.9.tgz", + "integrity": "sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-wrap-function": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.25.9.tgz", + "integrity": "sha512-ETzz9UTjQSTmw39GboatdymDq4XIQbR8ySgVrylRhPOFpsd+JrKHIuF0de7GCWmem+T4uC5z7EZguod7Wj4A4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.25.9", + "@babel/traverse": "^7.25.9", + "@babel/types": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-wrap-function/node_modules/@babel/template": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.9.tgz", + "integrity": "sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.25.9", + "@babel/parser": "^7.25.9", + "@babel/types": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.26.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.7.tgz", + "integrity": "sha512-8NHiL98vsi0mbPQmYAGWwfcFaOy4j2HY49fXJCfuDcdE7fMIsH9a7GdaeXpIBsbT7307WU8KCMp5pUVDNL4f9A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.25.9", + "@babel/types": "^7.26.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers/node_modules/@babel/template": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.9.tgz", + "integrity": "sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.25.9", + "@babel/parser": "^7.25.9", + "@babel/types": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.26.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.7.tgz", + "integrity": "sha512-kEvgGGgEjRUutvdVvZhbn/BxVt+5VSpwXz1j3WYXQbXDo8KzFOPNG2GQbdAiNq8g6wn1yKk7C/qrke03a84V+w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.26.7" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.25.9.tgz", + "integrity": "sha512-2qUwwfAFpJLZqxd02YW9btUCZHl+RFvdDkNfZwaIJrvB8Tesjsk8pEQkTvGwZXLqXUx/2oyY3ySRhm6HOXuCug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.25.9.tgz", + "integrity": "sha512-6xWgLZTJXwilVjlnV7ospI3xi+sl8lN8rXXbBD6vYn3UYDlGsag8wrZkKcSI8G6KgqKP7vNFaDgeDnfAABq61g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9", + "@babel/plugin-transform-optional-chaining": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.13.0" + } + }, + "node_modules/@babel/plugin-proposal-async-generator-functions": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.20.7.tgz", + "integrity": "sha512-xMbiLsn/8RK7Wq7VeVytytS2L6qE69bXPB10YCmMdDZbKF4okCqY74pI/jJQ/8U0b/F6NrT2+14b8/P9/3AMGA==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-async-generator-functions instead.", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-environment-visitor": "^7.18.9", + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/helper-remap-async-to-generator": "^7.18.9", + "@babel/plugin-syntax-async-generators": "^7.8.4" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-class-properties": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.18.6.tgz", + "integrity": "sha512-cumfXOF0+nzZrrN8Rf0t7M+tF6sZc7vhQwYQck9q1/5w2OExlD+b4v4RpMJFaV1Z7WcDRgO6FqvxqxGlwo+RHQ==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-class-properties instead.", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-class-static-block": { + "version": "7.21.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-static-block/-/plugin-proposal-class-static-block-7.21.0.tgz", + "integrity": "sha512-XP5G9MWNUskFuP30IfFSEFB0Z6HzLIUcjYM4bYOPHXl7eiJ9HFv8tWj6TXTN5QODiEhDZAeI4hLok2iHFFV4hw==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-class-static-block instead.", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.21.0", + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/plugin-syntax-class-static-block": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.12.0" + } + }, + "node_modules/@babel/plugin-proposal-dynamic-import": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.18.6.tgz", + "integrity": "sha512-1auuwmK+Rz13SJj36R+jqFPMJWyKEDd7lLSdOj4oJK0UTgGueSAtkrCvz9ewmgyU/P941Rv2fQwZJN8s6QruXw==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-dynamic-import instead.", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6", + "@babel/plugin-syntax-dynamic-import": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-export-namespace-from": { + "version": "7.18.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-export-namespace-from/-/plugin-proposal-export-namespace-from-7.18.9.tgz", + "integrity": "sha512-k1NtHyOMvlDDFeb9G5PhUXuGj8m/wiwojgQVEhJ/fsVsMCpLyOP4h0uGEjYJKrRI+EVPlb5Jk+Gt9P97lOGwtA==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-export-namespace-from instead.", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.9", + "@babel/plugin-syntax-export-namespace-from": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-json-strings": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.18.6.tgz", + "integrity": "sha512-lr1peyn9kOdbYc0xr0OdHTZ5FMqS6Di+H0Fz2I/JwMzGmzJETNeOFq2pBySw6X/KFL5EWDjlJuMsUGRFb8fQgQ==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-json-strings instead.", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6", + "@babel/plugin-syntax-json-strings": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-logical-assignment-operators": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-logical-assignment-operators/-/plugin-proposal-logical-assignment-operators-7.20.7.tgz", + "integrity": "sha512-y7C7cZgpMIjWlKE5T7eJwp+tnRYM89HmRvWM5EQuB5BoHEONjmQ8lSNmBUwOyy/GFRsohJED51YBF79hE1djug==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-logical-assignment-operators instead.", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-nullish-coalescing-operator": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.18.6.tgz", + "integrity": "sha512-wQxQzxYeJqHcfppzBDnm1yAY0jSRkUXR2z8RePZYrKwMKgMlE8+Z6LUno+bd6LvbGh8Gltvy74+9pIYkr+XkKA==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-nullish-coalescing-operator instead.", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-numeric-separator": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.18.6.tgz", + "integrity": "sha512-ozlZFogPqoLm8WBr5Z8UckIoE4YQ5KESVcNudyXOR8uqIkliTEgJ3RoketfG6pmzLdeZF0H/wjE9/cCEitBl7Q==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-numeric-separator instead.", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6", + "@babel/plugin-syntax-numeric-separator": "^7.10.4" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-object-rest-spread": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.20.7.tgz", + "integrity": "sha512-d2S98yCiLxDVmBmE8UjGcfPvNEUbA1U5q5WxaWFUGRzJSVAZqm5W6MbPct0jxnegUZ0niLeNX+IOzEs7wYg9Dg==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-object-rest-spread instead.", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.20.5", + "@babel/helper-compilation-targets": "^7.20.7", + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-transform-parameters": "^7.20.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-optional-catch-binding": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.18.6.tgz", + "integrity": "sha512-Q40HEhs9DJQyaZfUjjn6vE8Cv4GmMHCYuMGIWUnlxH6400VGxOuwWsPt4FxXxJkC/5eOzgn0z21M9gMT4MOhbw==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-optional-catch-binding instead.", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-optional-chaining": { + "version": "7.21.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.21.0.tgz", + "integrity": "sha512-p4zeefM72gpmEe2fkUr/OnOXpWEf8nAgk7ZYVqqfFiyIG7oFfVZcCrU64hWn5xp4tQ9LkV4bTIa5rD0KANpKNA==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-optional-chaining instead.", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/helper-skip-transparent-expression-wrappers": "^7.20.0", + "@babel/plugin-syntax-optional-chaining": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-private-methods": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-methods/-/plugin-proposal-private-methods-7.18.6.tgz", + "integrity": "sha512-nutsvktDItsNn4rpGItSNV2sz1XwS+nfU0Rg8aCx3W3NOKVzdMjJRu0O5OkgDp3ZGICSTbgRpxZoWsxoKRvbeA==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-private-methods instead.", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-private-property-in-object": { + "version": "7.21.11", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.11.tgz", + "integrity": "sha512-0QZ8qP/3RLDVBwBFoWAwCtgcDZJVwA5LUJRZU8x2YFfKNuFq161wK3cuGrALu5yiPu+vzwTAg/sMWVNeWeNyaw==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-private-property-in-object instead.", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.18.6", + "@babel/helper-create-class-features-plugin": "^7.21.0", + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-unicode-property-regex": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.18.6.tgz", + "integrity": "sha512-2BShG/d5yoZyXZfVePH91urL5wTG6ASZU9M4o03lKK8u8UW1y08OMttBSOADTcJrnPMpvDXRG3G8fyLh4ovs8w==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-unicode-property-regex instead.", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-dynamic-import": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz", + "integrity": "sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-export-namespace-from": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz", + "integrity": "sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.3" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-assertions": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.26.0.tgz", + "integrity": "sha512-QCWT5Hh830hK5EQa7XzuqIkQU9tT/whqbDz7kuaZMHFl1inRRg7JnuAEOQ0Ur0QUl0NufCk1msK2BeY79Aj/eg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-arrow-functions": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.25.9.tgz", + "integrity": "sha512-6jmooXYIwn9ca5/RylZADJ+EnSxVUS5sjeJ9UPk6RWRzXCmOJCy6dqItPJFpw2cuCangPK4OYr5uhGKcmrm5Qg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-async-to-generator": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.20.7.tgz", + "integrity": "sha512-Uo5gwHPT9vgnSXQxqGtpdufUiWp96gk7yiP4Mp5bm1QMkEmLXBO7PAGYbKoJ6DhAwiNkcHFBol/x5zZZkL/t0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.18.6", + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/helper-remap-async-to-generator": "^7.18.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-block-scoped-functions": { + "version": "7.26.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.26.5.tgz", + "integrity": "sha512-chuTSY+hq09+/f5lMj8ZSYgCFpppV2CbYrhNFJ1BFoXpiWPnnAb7R0MqrafCpN8E1+YRrtM1MXZHJdIx8B6rMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.26.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-block-scoping": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.25.9.tgz", + "integrity": "sha512-1F05O7AYjymAtqbsFETboN1NvBdcnzMerO+zlMyJBEz6WkMdejvGWw9p05iTSjC85RLlBseHHQpYaM4gzJkBGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-classes": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.25.9.tgz", + "integrity": "sha512-mD8APIXmseE7oZvZgGABDyM34GUmK45Um2TXiBUt7PnuAxrgoSVf123qUzPxEr/+/BHrRn5NMZCdE2m/1F8DGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.25.9", + "@babel/helper-compilation-targets": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/helper-replace-supers": "^7.25.9", + "@babel/traverse": "^7.25.9", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-classes/node_modules/@babel/helper-annotate-as-pure": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.25.9.tgz", + "integrity": "sha512-gv7320KBUFJz1RnylIg5WWYPRXKZ884AGkYpgpWW02TH66Dl+HaC1t1CKd0z3R4b6hdYEcmrNZHUmfCP+1u3/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/plugin-transform-computed-properties": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.25.9.tgz", + "integrity": "sha512-HnBegGqXZR12xbcTHlJ9HGxw1OniltT26J5YpfruGqtUHlz/xKf/G2ak9e+t0rVqrjXa9WOhvYPz1ERfMj23AA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/template": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-computed-properties/node_modules/@babel/template": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.9.tgz", + "integrity": "sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.25.9", + "@babel/parser": "^7.25.9", + "@babel/types": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/plugin-transform-destructuring": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.25.9.tgz", + "integrity": "sha512-WkCGb/3ZxXepmMiX101nnGiU+1CAdut8oHyEOHxkKuS1qKpU2SMXE2uSvfz8PBuLd49V6LEsbtyPhWC7fnkgvQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-dotall-regex": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.25.9.tgz", + "integrity": "sha512-t7ZQ7g5trIgSRYhI9pIJtRl64KHotutUJsh4Eze5l7olJv+mRSg4/MmbZ0tv1eeqRbdvo/+trvJD/Oc5DmW2cA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-duplicate-keys": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.25.9.tgz", + "integrity": "sha512-LZxhJ6dvBb/f3x8xwWIuyiAHy56nrRG3PeYTpBkkzkYRRQ6tJLu68lEF5VIqMUZiAV7a8+Tb78nEoMCMcqjXBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-exponentiation-operator": { + "version": "7.26.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.26.3.tgz", + "integrity": "sha512-7CAHcQ58z2chuXPWblnn1K6rLDnDWieghSOEmqQsrBenH0P9InCUtOJYD89pvngljmZlJcz3fcmgYsXFNGa1ZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-for-of": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.25.9.tgz", + "integrity": "sha512-LqHxduHoaGELJl2uhImHwRQudhCM50pT46rIBNvtT/Oql3nqiS3wOwP+5ten7NpYSXrrVLgtZU3DZmPtWZo16A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-function-name": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.25.9.tgz", + "integrity": "sha512-8lP+Yxjv14Vc5MuWBpJsoUCd3hD6V9DgBon2FVYL4jJgbnVQ9fTgYmonchzZJOVNgzEgbxp4OwAf6xz6M/14XA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-compilation-targets": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/traverse": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-literals": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.25.9.tgz", + "integrity": "sha512-9N7+2lFziW8W9pBl2TzaNht3+pgMIRP74zizeCSrtnSKVdUl8mAjjOP2OOVQAfZ881P2cNjDj1uAMEdeD50nuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-member-expression-literals": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.25.9.tgz", + "integrity": "sha512-PYazBVfofCQkkMzh2P6IdIUaCEWni3iYEerAsRWuVd8+jlM1S9S9cz1dF9hIzyoZ8IA3+OwVYIp9v9e+GbgZhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-amd": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.25.9.tgz", + "integrity": "sha512-g5T11tnI36jVClQlMlt4qKDLlWnG5pP9CSM4GhdRciTNMRgkfpo5cR6b4rGIOYPgRRuFAvwjPQ/Yk+ql4dyhbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-commonjs": { + "version": "7.26.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.26.3.tgz", + "integrity": "sha512-MgR55l4q9KddUDITEzEFYn5ZsGDXMSsU9E+kh7fjRXTIC3RHqfCo8RPRbyReYJh44HQ/yomFkqbOFohXvDCiIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.26.0", + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-systemjs": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.25.9.tgz", + "integrity": "sha512-hyss7iIlH/zLHaehT+xwiymtPOpsiwIIRlCAOwBB04ta5Tt+lNItADdlXw3jAWZ96VJ2jlhl/c+PNIQPKNfvcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/helper-validator-identifier": "^7.25.9", + "@babel/traverse": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-umd": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.25.9.tgz", + "integrity": "sha512-bS9MVObUgE7ww36HEfwe6g9WakQ0KF07mQF74uuXdkoziUPfKyu/nIm663kz//e5O1nPInPFx36z7WJmJ4yNEw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-named-capturing-groups-regex": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.25.9.tgz", + "integrity": "sha512-oqB6WHdKTGl3q/ItQhpLSnWWOpjUJLsOCLVyeFgeTktkBSCiurvPOsyt93gibI9CmuKvTUEtWmG5VhZD+5T/KA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-new-target": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.25.9.tgz", + "integrity": "sha512-U/3p8X1yCSoKyUj2eOBIx3FOn6pElFOKvAAGf8HTtItuPyB+ZeOqfn+mvTtg9ZlOAjsPdK3ayQEjqHjU/yLeVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-object-super": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.25.9.tgz", + "integrity": "sha512-Kj/Gh+Rw2RNLbCK1VAWj2U48yxxqL2x0k10nPtSdRa0O2xnHXalD0s+o1A6a0W43gJ00ANo38jxkQreckOzv5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/helper-replace-supers": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-optional-chaining": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.25.9.tgz", + "integrity": "sha512-6AvV0FsLULbpnXeBjrY4dmWF8F7gf8QnvTEoO/wX/5xm/xE1Xo8oPuD3MPS+KS9f9XBEAWN7X1aWr4z9HdOr7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-parameters": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.25.9.tgz", + "integrity": "sha512-wzz6MKwpnshBAiRmn4jR8LYz/g8Ksg0o80XmwZDlordjwEk9SxBzTWC7F5ef1jhbrbOW2DJ5J6ayRukrJmnr0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-property-literals": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.25.9.tgz", + "integrity": "sha512-IvIUeV5KrS/VPavfSM/Iu+RE6llrHrYIKY1yfCzyO/lMXHQ+p7uGhonmGVisv6tSBSVgWzMBohTcvkC9vQcQFA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-regenerator": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.25.9.tgz", + "integrity": "sha512-vwDcDNsgMPDGP0nMqzahDWE5/MLcX8sv96+wfX7as7LoF/kr97Bo/7fI00lXY4wUXYfVmwIIyG80fGZ1uvt2qg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9", + "regenerator-transform": "^0.15.2" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-reserved-words": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.25.9.tgz", + "integrity": "sha512-7DL7DKYjn5Su++4RXu8puKZm2XBPHyjWLUidaPEkCUBbE7IPcsrkRHggAOOKydH1dASWdcUBxrkOGNxUv5P3Jg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-runtime": { + "version": "7.19.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.19.6.tgz", + "integrity": "sha512-PRH37lz4JU156lYFW1p8OxE5i7d6Sl/zV58ooyr+q1J1lnQPyg5tIiXlIwNVhJaY4W3TmOtdc8jqdXQcB1v5Yw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.18.6", + "@babel/helper-plugin-utils": "^7.19.0", + "babel-plugin-polyfill-corejs2": "^0.3.3", + "babel-plugin-polyfill-corejs3": "^0.6.0", + "babel-plugin-polyfill-regenerator": "^0.4.1", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-runtime/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/plugin-transform-shorthand-properties": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.25.9.tgz", + "integrity": "sha512-MUv6t0FhO5qHnS/W8XCbHmiRWOphNufpE1IVxhK5kuN3Td9FT1x4rx4K42s3RYdMXCXpfWkGSbCSd0Z64xA7Ng==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-spread": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.25.9.tgz", + "integrity": "sha512-oNknIB0TbURU5pqJFVbOOFspVlrpVwo2H1+HUIsVDvp5VauGGDP1ZEvO8Nn5xyMEs3dakajOxlmkNW7kNgSm6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-sticky-regex": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.25.9.tgz", + "integrity": "sha512-WqBUSgeVwucYDP9U/xNRQam7xV8W5Zf+6Eo7T2SRVUFlhRiMNFdFz58u0KZmCVVqs2i7SHgpRnAhzRNmKfi2uA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-template-literals": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.25.9.tgz", + "integrity": "sha512-o97AE4syN71M/lxrCtQByzphAdlYluKPDBzDVzMmfCobUjjhAryZV0AIpRPrxN0eAkxXO6ZLEScmt+PNhj2OTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-typeof-symbol": { + "version": "7.26.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.26.7.tgz", + "integrity": "sha512-jfoTXXZTgGg36BmhqT3cAYK5qkmqvJpvNrPhaK/52Vgjhw4Rq29s9UqpWWV0D6yuRmgiFH/BUVlkl96zJWqnaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.26.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-escapes": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.25.9.tgz", + "integrity": "sha512-s5EDrE6bW97LtxOcGj1Khcx5AaXwiMmi4toFWRDP9/y0Woo6pXC+iyPu/KuhKtfSrNFd7jJB+/fkOtZy6aIC6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-regex": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.25.9.tgz", + "integrity": "sha512-yoxstj7Rg9dlNn9UQxzk4fcNivwv4nUYz7fYXBaKxvw/lnmPuOm/ikoELygbYq68Bls3D/D+NBPHiLwZdZZ4HA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-env": { + "version": "7.20.2", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.20.2.tgz", + "integrity": "sha512-1G0efQEWR1EHkKvKHqbG+IN/QdgwfByUpM5V5QroDzGV2t3S/WXNQd693cHiHTlCFMpr9B6FkPFXDA2lQcKoDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.20.1", + "@babel/helper-compilation-targets": "^7.20.0", + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/helper-validator-option": "^7.18.6", + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.18.6", + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.18.9", + "@babel/plugin-proposal-async-generator-functions": "^7.20.1", + "@babel/plugin-proposal-class-properties": "^7.18.6", + "@babel/plugin-proposal-class-static-block": "^7.18.6", + "@babel/plugin-proposal-dynamic-import": "^7.18.6", + "@babel/plugin-proposal-export-namespace-from": "^7.18.9", + "@babel/plugin-proposal-json-strings": "^7.18.6", + "@babel/plugin-proposal-logical-assignment-operators": "^7.18.9", + "@babel/plugin-proposal-nullish-coalescing-operator": "^7.18.6", + "@babel/plugin-proposal-numeric-separator": "^7.18.6", + "@babel/plugin-proposal-object-rest-spread": "^7.20.2", + "@babel/plugin-proposal-optional-catch-binding": "^7.18.6", + "@babel/plugin-proposal-optional-chaining": "^7.18.9", + "@babel/plugin-proposal-private-methods": "^7.18.6", + "@babel/plugin-proposal-private-property-in-object": "^7.18.6", + "@babel/plugin-proposal-unicode-property-regex": "^7.18.6", + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-dynamic-import": "^7.8.3", + "@babel/plugin-syntax-export-namespace-from": "^7.8.3", + "@babel/plugin-syntax-import-assertions": "^7.20.0", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5", + "@babel/plugin-transform-arrow-functions": "^7.18.6", + "@babel/plugin-transform-async-to-generator": "^7.18.6", + "@babel/plugin-transform-block-scoped-functions": "^7.18.6", + "@babel/plugin-transform-block-scoping": "^7.20.2", + "@babel/plugin-transform-classes": "^7.20.2", + "@babel/plugin-transform-computed-properties": "^7.18.9", + "@babel/plugin-transform-destructuring": "^7.20.2", + "@babel/plugin-transform-dotall-regex": "^7.18.6", + "@babel/plugin-transform-duplicate-keys": "^7.18.9", + "@babel/plugin-transform-exponentiation-operator": "^7.18.6", + "@babel/plugin-transform-for-of": "^7.18.8", + "@babel/plugin-transform-function-name": "^7.18.9", + "@babel/plugin-transform-literals": "^7.18.9", + "@babel/plugin-transform-member-expression-literals": "^7.18.6", + "@babel/plugin-transform-modules-amd": "^7.19.6", + "@babel/plugin-transform-modules-commonjs": "^7.19.6", + "@babel/plugin-transform-modules-systemjs": "^7.19.6", + "@babel/plugin-transform-modules-umd": "^7.18.6", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.19.1", + "@babel/plugin-transform-new-target": "^7.18.6", + "@babel/plugin-transform-object-super": "^7.18.6", + "@babel/plugin-transform-parameters": "^7.20.1", + "@babel/plugin-transform-property-literals": "^7.18.6", + "@babel/plugin-transform-regenerator": "^7.18.6", + "@babel/plugin-transform-reserved-words": "^7.18.6", + "@babel/plugin-transform-shorthand-properties": "^7.18.6", + "@babel/plugin-transform-spread": "^7.19.0", + "@babel/plugin-transform-sticky-regex": "^7.18.6", + "@babel/plugin-transform-template-literals": "^7.18.9", + "@babel/plugin-transform-typeof-symbol": "^7.18.9", + "@babel/plugin-transform-unicode-escapes": "^7.18.10", + "@babel/plugin-transform-unicode-regex": "^7.18.6", + "@babel/preset-modules": "^0.1.5", + "@babel/types": "^7.20.2", + "babel-plugin-polyfill-corejs2": "^0.3.3", + "babel-plugin-polyfill-corejs3": "^0.6.0", + "babel-plugin-polyfill-regenerator": "^0.4.1", + "core-js-compat": "^3.25.1", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-env/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/preset-modules": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.6.tgz", + "integrity": "sha512-ID2yj6K/4lKfhuU3+EX4UvNbIt7eACFbHmNUjzA+ep+B5971CknnA/9DEWKbRokfbbtblxxxXFJJrH47UEAMVg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/plugin-proposal-unicode-property-regex": "^7.4.4", + "@babel/plugin-transform-dotall-regex": "^7.4.4", + "@babel/types": "^7.4.4", + "esutils": "^2.0.2" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.20.13", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.20.13.tgz", + "integrity": "sha512-gt3PKXs0DBoL9xCvOIIZ2NEqAGZqHjAnmVbfQtB620V0uReIQutpel14KcneZuer7UioY8ALKZ7iocavvzTNFA==", + "dev": true, + "license": "MIT", + "dependencies": { + "regenerator-runtime": "^0.13.11" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.20.7.tgz", + "integrity": "sha512-8SegXApWe6VoNw0r9JHpSteLKTpTiLZ4rMlGIm9JQ18KiCtyQiAMEazujAHrUS5flrcqYZa75ukev3P6QmUwUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.18.6", + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.26.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.26.7.tgz", + "integrity": "sha512-1x1sgeyRLC3r5fQOM0/xtQKsYjyxmFjaOrLJNtZ81inNjyJHGIolTULPiSc/2qe1/qfpFLisLQYFnnZl7QoedA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.26.2", + "@babel/generator": "^7.26.5", + "@babel/parser": "^7.26.7", + "@babel/template": "^7.25.9", + "@babel/types": "^7.26.7", + "debug": "^4.3.1", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse/node_modules/@babel/generator": { + "version": "7.26.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.5.tgz", + "integrity": "sha512-2caSP6fN9I7HOe6nqhtft7V4g7/V/gfDsC3Ag4W7kEzzvRGKqiv0pu0HogPiZ3KaVSoNDhUws6IJjDjpfmYIXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.26.5", + "@babel/types": "^7.26.5", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse/node_modules/@babel/template": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.9.tgz", + "integrity": "sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.25.9", + "@babel/parser": "^7.25.9", + "@babel/types": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse/node_modules/@jridgewell/gen-mapping": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", + "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/traverse/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/@babel/types": { + "version": "7.26.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.7.tgz", + "integrity": "sha512-t8kDRGrKXyp6+tjUh7hw2RLyclsW4TRoRvRHtSyAX9Bb5ldlFh+90YAYY6awRXrlB4G5G2izNeGySpATlFzmOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.25.9", + "@babel/helper-validator-identifier": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@braintree/sanitize-url": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/@braintree/sanitize-url/-/sanitize-url-6.0.4.tgz", + "integrity": "sha512-s3jaWicZd0pkP0jf5ysyHUI/RE7MHos6qlToFcGWXVp+ykHOy77OUMrfbgJ9it2C5bow7OIQwYYaHjk9XlBQ2A==", + "license": "MIT", + "optional": true + }, + "node_modules/@colors/colors": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", + "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/@dabh/diagnostics": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.3.tgz", + "integrity": "sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==", + "license": "MIT", + "dependencies": { + "colorspace": "1.1.x", + "enabled": "2.0.x", + "kuler": "^2.0.0" + } + }, + "node_modules/@discoveryjs/json-ext": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz", + "integrity": "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.17.8", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.17.8.tgz", + "integrity": "sha512-0/rb91GYKhrtbeglJXOhAv9RuYimgI8h623TplY2X+vA4EXnk3Zj1fXZreJ0J3OJJu1bwmb0W7g+2cT/d8/l/w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.17.8", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.17.8.tgz", + "integrity": "sha512-oa/N5j6v1svZQs7EIRPqR8f+Bf8g6HBDjD/xHC02radE/NjKHK7oQmtmLxPs1iVwYyvE+Kolo6lbpfEQ9xnhxQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.17.8", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.17.8.tgz", + "integrity": "sha512-bTliMLqD7pTOoPg4zZkXqCDuzIUguEWLpeqkNfC41ODBHwoUgZ2w5JBeYimv4oP6TDVocoYmEhZrCLQTrH89bg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.17.8", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.17.8.tgz", + "integrity": "sha512-ghAbV3ia2zybEefXRRm7+lx8J/rnupZT0gp9CaGy/3iolEXkJ6LYRq4IpQVI9zR97ID80KJVoUlo3LSeA/sMAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.17.8", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.17.8.tgz", + "integrity": "sha512-n5WOpyvZ9TIdv2V1K3/iIkkJeKmUpKaCTdun9buhGRWfH//osmUjlv4Z5mmWdPWind/VGcVxTHtLfLCOohsOXw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.17.8", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.17.8.tgz", + "integrity": "sha512-a/SATTaOhPIPFWvHZDoZYgxaZRVHn0/LX1fHLGfZ6C13JqFUZ3K6SMD6/HCtwOQ8HnsNaEeokdiDSFLuizqv5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.17.8", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.17.8.tgz", + "integrity": "sha512-xpFJb08dfXr5+rZc4E+ooZmayBW6R3q59daCpKZ/cDU96/kvDM+vkYzNeTJCGd8rtO6fHWMq5Rcv/1cY6p6/0Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.17.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.17.8.tgz", + "integrity": "sha512-6Ij8gfuGszcEwZpi5jQIJCVIACLS8Tz2chnEBfYjlmMzVsfqBP1iGmHQPp7JSnZg5xxK9tjCc+pJ2WtAmPRFVA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.17.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.17.8.tgz", + "integrity": "sha512-v3iwDQuDljLTxpsqQDl3fl/yihjPAyOguxuloON9kFHYwopeJEf1BkDXODzYyXEI19gisEsQlG1bM65YqKSIww==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.17.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.17.8.tgz", + "integrity": "sha512-8svILYKhE5XetuFk/B6raFYIyIqydQi+GngEXJgdPdI7OMKUbSd7uzR02wSY4kb53xBrClLkhH4Xs8P61Q2BaA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.17.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.17.8.tgz", + "integrity": "sha512-B6FyMeRJeV0NpyEOYlm5qtQfxbdlgmiGdD+QsipzKfFky0K5HW5Td6dyK3L3ypu1eY4kOmo7wW0o94SBqlqBSA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.17.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.17.8.tgz", + "integrity": "sha512-CCb67RKahNobjm/eeEqeD/oJfJlrWyw29fgiyB6vcgyq97YAf3gCOuP6qMShYSPXgnlZe/i4a8WFHBw6N8bYAA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.17.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.17.8.tgz", + "integrity": "sha512-bytLJOi55y55+mGSdgwZ5qBm0K9WOCh0rx+vavVPx+gqLLhxtSFU0XbeYy/dsAAD6xECGEv4IQeFILaSS2auXw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.17.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.17.8.tgz", + "integrity": "sha512-2YpRyQJmKVBEHSBLa8kBAtbhucaclb6ex4wchfY0Tj3Kg39kpjeJ9vhRU7x4mUpq8ISLXRXH1L0dBYjAeqzZAw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.17.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.17.8.tgz", + "integrity": "sha512-QgbNY/V3IFXvNf11SS6exkpVcX0LJcob+0RWCgV9OiDAmVElnxciHIisoSix9uzYzScPmS6dJFbZULdSAEkQVw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.17.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.17.8.tgz", + "integrity": "sha512-mM/9S0SbAFDBc4OPoyP6SEOo5324LpUxdpeIUUSrSTOfhHU9hEfqRngmKgqILqwx/0DVJBzeNW7HmLEWp9vcOA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.17.8", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.17.8.tgz", + "integrity": "sha512-eKUYcWaWTaYr9zbj8GertdVtlt1DTS1gNBWov+iQfWuWyuu59YN6gSEJvFzC5ESJ4kMcKR0uqWThKUn5o8We6Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.17.8", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.17.8.tgz", + "integrity": "sha512-Vc9J4dXOboDyMXKD0eCeW0SIeEzr8K9oTHJU+Ci1mZc5njPfhKAqkRt3B/fUNU7dP+mRyralPu8QUkiaQn7iIg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.17.8", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.17.8.tgz", + "integrity": "sha512-0xvOTNuPXI7ft1LYUgiaXtpCEjp90RuBBYovdd2lqAFxje4sEucurg30M1WIm03+3jxByd3mfo+VUmPtRSVuOw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.17.8", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.17.8.tgz", + "integrity": "sha512-G0JQwUI5WdEFEnYNKzklxtBheCPkuDdu1YrtRrjuQv30WsYbkkoixKxLLv8qhJmNI+ATEWquZe/N0d0rpr55Mg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.17.8", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.17.8.tgz", + "integrity": "sha512-Fqy63515xl20OHGFykjJsMnoIWS+38fqfg88ClvPXyDbLtgXal2DTlhb1TfTX34qWi3u4I7Cq563QcHpqgLx8w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.17.8", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.17.8.tgz", + "integrity": "sha512-1iuezdyDNngPnz8rLRDO2C/ZZ/emJLb72OsZeqQ6gL6Avko/XCXZw+NuxBSNhBAP13Hie418V7VMt9et1FMvpg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.1.tgz", + "integrity": "sha512-s3O3waFUrMV8P/XaF/+ZTp1X9XBZW1a4B97ZnjQF2KYWaFD2A8KyFBsrsfSjEmjn3RGWAIuvlneuZm3CUK3jbA==", + "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.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", + "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", + "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/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "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/@eslint/eslintrc/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/@eslint/eslintrc/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/eslintrc/node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/@eslint/eslintrc/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/@eslint/eslintrc/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/eslintrc/node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz", + "integrity": "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@firebase/analytics": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@firebase/analytics/-/analytics-0.10.0.tgz", + "integrity": "sha512-Locv8gAqx0e+GX/0SI3dzmBY5e9kjVDtD+3zCFLJ0tH2hJwuCAiL+5WkHuxKj92rqQj/rvkBUCfA1ewlX2hehg==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.6.4", + "@firebase/installations": "0.6.4", + "@firebase/logger": "0.4.0", + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/analytics-compat": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/@firebase/analytics-compat/-/analytics-compat-0.2.6.tgz", + "integrity": "sha512-4MqpVLFkGK7NJf/5wPEEP7ePBJatwYpyjgJ+wQHQGHfzaCDgntOnl9rL2vbVGGKCnRqWtZDIWhctB86UWXaX2Q==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/analytics": "0.10.0", + "@firebase/analytics-types": "0.8.0", + "@firebase/component": "0.6.4", + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/analytics-types": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@firebase/analytics-types/-/analytics-types-0.8.0.tgz", + "integrity": "sha512-iRP+QKI2+oz3UAh4nPEq14CsEjrjD6a5+fuypjScisAh9kXKFvdJOZJDwk7kikLvWVLGEs9+kIUS4LPQV7VZVw==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/app": { + "version": "0.9.13", + "resolved": "https://registry.npmjs.org/@firebase/app/-/app-0.9.13.tgz", + "integrity": "sha512-GfiI1JxJ7ecluEmDjPzseRXk/PX31hS7+tjgBopL7XjB2hLUdR+0FTMXy2Q3/hXezypDvU6or7gVFizDESrkXw==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.6.4", + "@firebase/logger": "0.4.0", + "@firebase/util": "1.9.3", + "idb": "7.1.1", + "tslib": "^2.1.0" + } + }, + "node_modules/@firebase/app-check": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@firebase/app-check/-/app-check-0.8.0.tgz", + "integrity": "sha512-dRDnhkcaC2FspMiRK/Vbp+PfsOAEP6ZElGm9iGFJ9fDqHoPs0HOPn7dwpJ51lCFi1+2/7n5pRPGhqF/F03I97g==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.6.4", + "@firebase/logger": "0.4.0", + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/app-check-compat": { + "version": "0.3.7", + "resolved": "https://registry.npmjs.org/@firebase/app-check-compat/-/app-check-compat-0.3.7.tgz", + "integrity": "sha512-cW682AxsyP1G+Z0/P7pO/WT2CzYlNxoNe5QejVarW2o5ZxeWSSPAiVEwpEpQR/bUlUmdeWThYTMvBWaopdBsqw==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/app-check": "0.8.0", + "@firebase/app-check-types": "0.5.0", + "@firebase/component": "0.6.4", + "@firebase/logger": "0.4.0", + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/app-check-interop-types": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@firebase/app-check-interop-types/-/app-check-interop-types-0.3.0.tgz", + "integrity": "sha512-xAxHPZPIgFXnI+vb4sbBjZcde7ZluzPPaSK7Lx3/nmuVk4TjZvnL8ONnkd4ERQKL8WePQySU+pRcWkh8rDf5Sg==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/app-check-types": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@firebase/app-check-types/-/app-check-types-0.5.0.tgz", + "integrity": "sha512-uwSUj32Mlubybw7tedRzR24RP8M8JUVR3NPiMk3/Z4bCmgEKTlQBwMXrehDAZ2wF+TsBq0SN1c6ema71U/JPyQ==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/app-compat": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/@firebase/app-compat/-/app-compat-0.2.13.tgz", + "integrity": "sha512-j6ANZaWjeVy5zg6X7uiqh6lM6o3n3LD1+/SJFNs9V781xyryyZWXe+tmnWNWPkP086QfJoNkWN9pMQRqSG4vMg==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/app": "0.9.13", + "@firebase/component": "0.6.4", + "@firebase/logger": "0.4.0", + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" + } + }, + "node_modules/@firebase/app-types": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@firebase/app-types/-/app-types-0.9.0.tgz", + "integrity": "sha512-AeweANOIo0Mb8GiYm3xhTEBVCmPwTYAu9Hcd2qSkLuga/6+j9b1Jskl5bpiSQWy9eJ/j5pavxj6eYogmnuzm+Q==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/auth": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/@firebase/auth/-/auth-0.23.2.tgz", + "integrity": "sha512-dM9iJ0R6tI1JczuGSxXmQbXAgtYie0K4WvKcuyuSTCu9V8eEDiz4tfa1sO3txsfvwg7nOY3AjoCyMYEdqZ8hdg==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.6.4", + "@firebase/logger": "0.4.0", + "@firebase/util": "1.9.3", + "node-fetch": "2.6.7", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/auth-compat": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@firebase/auth-compat/-/auth-compat-0.4.2.tgz", + "integrity": "sha512-Q30e77DWXFmXEt5dg5JbqEDpjw9y3/PcP9LslDPR7fARmAOTIY9MM6HXzm9KC+dlrKH/+p6l8g9ifJiam9mc4A==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/auth": "0.23.2", + "@firebase/auth-types": "0.12.0", + "@firebase/component": "0.6.4", + "@firebase/util": "1.9.3", + "node-fetch": "2.6.7", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/auth-compat/node_modules/node-fetch": { + "version": "2.6.7", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", + "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/@firebase/auth-interop-types": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@firebase/auth-interop-types/-/auth-interop-types-0.2.1.tgz", + "integrity": "sha512-VOaGzKp65MY6P5FI84TfYKBXEPi6LmOCSMMzys6o2BN2LOsqy7pCuZCup7NYnfbk5OkkQKzvIfHOzTm0UDpkyg==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/auth-types": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@firebase/auth-types/-/auth-types-0.12.0.tgz", + "integrity": "sha512-pPwaZt+SPOshK8xNoiQlK5XIrS97kFYc3Rc7xmy373QsOJ9MmqXxLaYssP5Kcds4wd2qK//amx/c+A8O2fVeZA==", + "license": "Apache-2.0", + "peerDependencies": { + "@firebase/app-types": "0.x", + "@firebase/util": "1.x" + } + }, + "node_modules/@firebase/auth/node_modules/node-fetch": { + "version": "2.6.7", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", + "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/@firebase/component": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.6.4.tgz", + "integrity": "sha512-rLMyrXuO9jcAUCaQXCMjCMUsWrba5fzHlNK24xz5j2W6A/SRmK8mZJ/hn7V0fViLbxC0lPMtrK1eYzk6Fg03jA==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" + } + }, + "node_modules/@firebase/database": { + "version": "0.14.4", + "resolved": "https://registry.npmjs.org/@firebase/database/-/database-0.14.4.tgz", + "integrity": "sha512-+Ea/IKGwh42jwdjCyzTmeZeLM3oy1h0mFPsTy6OqCWzcu/KFqRAr5Tt1HRCOBlNOdbh84JPZC47WLU18n2VbxQ==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/auth-interop-types": "0.2.1", + "@firebase/component": "0.6.4", + "@firebase/logger": "0.4.0", + "@firebase/util": "1.9.3", + "faye-websocket": "0.11.4", + "tslib": "^2.1.0" + } + }, + "node_modules/@firebase/database-compat": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@firebase/database-compat/-/database-compat-0.3.4.tgz", + "integrity": "sha512-kuAW+l+sLMUKBThnvxvUZ+Q1ZrF/vFJ58iUY9kAcbX48U03nVzIF6Tmkf0p3WVQwMqiXguSgtOPIB6ZCeF+5Gg==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.6.4", + "@firebase/database": "0.14.4", + "@firebase/database-types": "0.10.4", + "@firebase/logger": "0.4.0", + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" + } + }, + "node_modules/@firebase/database-types": { + "version": "0.10.4", + "resolved": "https://registry.npmjs.org/@firebase/database-types/-/database-types-0.10.4.tgz", + "integrity": "sha512-dPySn0vJ/89ZeBac70T+2tWWPiJXWbmRygYv0smT5TfE3hDrQ09eKMF3Y+vMlTdrMWq7mUdYW5REWPSGH4kAZQ==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/app-types": "0.9.0", + "@firebase/util": "1.9.3" + } + }, + "node_modules/@firebase/firestore": { + "version": "3.13.0", + "resolved": "https://registry.npmjs.org/@firebase/firestore/-/firestore-3.13.0.tgz", + "integrity": "sha512-NwcnU+madJXQ4fbLkGx1bWvL612IJN/qO6bZ6dlPmyf7QRyu5azUosijdAN675r+bOOJxMtP1Bv981bHBXAbUg==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.6.4", + "@firebase/logger": "0.4.0", + "@firebase/util": "1.9.3", + "@firebase/webchannel-wrapper": "0.10.1", + "@grpc/grpc-js": "~1.7.0", + "@grpc/proto-loader": "^0.6.13", + "node-fetch": "2.6.7", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=10.10.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/firestore-compat": { + "version": "0.3.12", + "resolved": "https://registry.npmjs.org/@firebase/firestore-compat/-/firestore-compat-0.3.12.tgz", + "integrity": "sha512-mazuNGAx5Kt9Nph0pm6ULJFp/+j7GSsx+Ncw1GrnKl+ft1CQ4q2LcUssXnjqkX2Ry0fNGqUzC1mfIUrk9bYtjQ==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.6.4", + "@firebase/firestore": "3.13.0", + "@firebase/firestore-types": "2.5.1", + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/firestore-types": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@firebase/firestore-types/-/firestore-types-2.5.1.tgz", + "integrity": "sha512-xG0CA6EMfYo8YeUxC8FeDzf6W3FX1cLlcAGBYV6Cku12sZRI81oWcu61RSKM66K6kUENP+78Qm8mvroBcm1whw==", + "license": "Apache-2.0", + "peerDependencies": { + "@firebase/app-types": "0.x", + "@firebase/util": "1.x" + } + }, + "node_modules/@firebase/firestore/node_modules/node-fetch": { + "version": "2.6.7", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", + "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/@firebase/functions": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@firebase/functions/-/functions-0.10.0.tgz", + "integrity": "sha512-2U+fMNxTYhtwSpkkR6WbBcuNMOVaI7MaH3cZ6UAeNfj7AgEwHwMIFLPpC13YNZhno219F0lfxzTAA0N62ndWzA==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/app-check-interop-types": "0.3.0", + "@firebase/auth-interop-types": "0.2.1", + "@firebase/component": "0.6.4", + "@firebase/messaging-interop-types": "0.2.0", + "@firebase/util": "1.9.3", + "node-fetch": "2.6.7", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/functions-compat": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@firebase/functions-compat/-/functions-compat-0.3.5.tgz", + "integrity": "sha512-uD4jwgwVqdWf6uc3NRKF8cSZ0JwGqSlyhPgackyUPe+GAtnERpS4+Vr66g0b3Gge0ezG4iyHo/EXW/Hjx7QhHw==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.6.4", + "@firebase/functions": "0.10.0", + "@firebase/functions-types": "0.6.0", + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/functions-types": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@firebase/functions-types/-/functions-types-0.6.0.tgz", + "integrity": "sha512-hfEw5VJtgWXIRf92ImLkgENqpL6IWpYaXVYiRkFY1jJ9+6tIhWM7IzzwbevwIIud/jaxKVdRzD7QBWfPmkwCYw==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/functions/node_modules/node-fetch": { + "version": "2.6.7", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", + "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/@firebase/installations": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/@firebase/installations/-/installations-0.6.4.tgz", + "integrity": "sha512-u5y88rtsp7NYkCHC3ElbFBrPtieUybZluXyzl7+4BsIz4sqb4vSAuwHEUgCgCeaQhvsnxDEU6icly8U9zsJigA==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.6.4", + "@firebase/util": "1.9.3", + "idb": "7.0.1", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/installations-compat": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@firebase/installations-compat/-/installations-compat-0.2.4.tgz", + "integrity": "sha512-LI9dYjp0aT9Njkn9U4JRrDqQ6KXeAmFbRC0E7jI7+hxl5YmRWysq5qgQl22hcWpTk+cm3es66d/apoDU/A9n6Q==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.6.4", + "@firebase/installations": "0.6.4", + "@firebase/installations-types": "0.5.0", + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/installations-types": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@firebase/installations-types/-/installations-types-0.5.0.tgz", + "integrity": "sha512-9DP+RGfzoI2jH7gY4SlzqvZ+hr7gYzPODrbzVD82Y12kScZ6ZpRg/i3j6rleto8vTFC8n6Len4560FnV1w2IRg==", + "license": "Apache-2.0", + "peerDependencies": { + "@firebase/app-types": "0.x" + } + }, + "node_modules/@firebase/installations/node_modules/idb": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/idb/-/idb-7.0.1.tgz", + "integrity": "sha512-UUxlE7vGWK5RfB/fDwEGgRf84DY/ieqNha6msMV99UsEMQhJ1RwbCd8AYBj3QMgnE3VZnfQvm4oKVCJTYlqIgg==", + "license": "ISC" + }, + "node_modules/@firebase/logger": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@firebase/logger/-/logger-0.4.0.tgz", + "integrity": "sha512-eRKSeykumZ5+cJPdxxJRgAC3G5NknY2GwEbKfymdnXtnT0Ucm4pspfR6GT4MUQEDuJwRVbVcSx85kgJulMoFFA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@firebase/messaging": { + "version": "0.12.4", + "resolved": "https://registry.npmjs.org/@firebase/messaging/-/messaging-0.12.4.tgz", + "integrity": "sha512-6JLZct6zUaex4g7HI3QbzeUrg9xcnmDAPTWpkoMpd/GoSVWH98zDoWXMGrcvHeCAIsLpFMe4MPoZkJbrPhaASw==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.6.4", + "@firebase/installations": "0.6.4", + "@firebase/messaging-interop-types": "0.2.0", + "@firebase/util": "1.9.3", + "idb": "7.0.1", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/messaging-compat": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@firebase/messaging-compat/-/messaging-compat-0.2.4.tgz", + "integrity": "sha512-lyFjeUhIsPRYDPNIkYX1LcZMpoVbBWXX4rPl7c/rqc7G+EUea7IEtSt4MxTvh6fDfPuzLn7+FZADfscC+tNMfg==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.6.4", + "@firebase/messaging": "0.12.4", + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/messaging-interop-types": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@firebase/messaging-interop-types/-/messaging-interop-types-0.2.0.tgz", + "integrity": "sha512-ujA8dcRuVeBixGR9CtegfpU4YmZf3Lt7QYkcj693FFannwNuZgfAYaTmbJ40dtjB81SAu6tbFPL9YLNT15KmOQ==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/messaging/node_modules/idb": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/idb/-/idb-7.0.1.tgz", + "integrity": "sha512-UUxlE7vGWK5RfB/fDwEGgRf84DY/ieqNha6msMV99UsEMQhJ1RwbCd8AYBj3QMgnE3VZnfQvm4oKVCJTYlqIgg==", + "license": "ISC" + }, + "node_modules/@firebase/performance": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/@firebase/performance/-/performance-0.6.4.tgz", + "integrity": "sha512-HfTn/bd8mfy/61vEqaBelNiNnvAbUtME2S25A67Nb34zVuCSCRIX4SseXY6zBnOFj3oLisaEqhVcJmVPAej67g==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.6.4", + "@firebase/installations": "0.6.4", + "@firebase/logger": "0.4.0", + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/performance-compat": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@firebase/performance-compat/-/performance-compat-0.2.4.tgz", + "integrity": "sha512-nnHUb8uP9G8islzcld/k6Bg5RhX62VpbAb/Anj7IXs/hp32Eb2LqFPZK4sy3pKkBUO5wcrlRWQa6wKOxqlUqsg==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.6.4", + "@firebase/logger": "0.4.0", + "@firebase/performance": "0.6.4", + "@firebase/performance-types": "0.2.0", + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/performance-types": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@firebase/performance-types/-/performance-types-0.2.0.tgz", + "integrity": "sha512-kYrbr8e/CYr1KLrLYZZt2noNnf+pRwDq2KK9Au9jHrBMnb0/C9X9yWSXmZkFt4UIdsQknBq8uBB7fsybZdOBTA==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/remote-config": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/@firebase/remote-config/-/remote-config-0.4.4.tgz", + "integrity": "sha512-x1ioTHGX8ZwDSTOVp8PBLv2/wfwKzb4pxi0gFezS5GCJwbLlloUH4YYZHHS83IPxnua8b6l0IXUaWd0RgbWwzQ==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.6.4", + "@firebase/installations": "0.6.4", + "@firebase/logger": "0.4.0", + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/remote-config-compat": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@firebase/remote-config-compat/-/remote-config-compat-0.2.4.tgz", + "integrity": "sha512-FKiki53jZirrDFkBHglB3C07j5wBpitAaj8kLME6g8Mx+aq7u9P7qfmuSRytiOItADhWUj7O1JIv7n9q87SuwA==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.6.4", + "@firebase/logger": "0.4.0", + "@firebase/remote-config": "0.4.4", + "@firebase/remote-config-types": "0.3.0", + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/remote-config-types": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@firebase/remote-config-types/-/remote-config-types-0.3.0.tgz", + "integrity": "sha512-RtEH4vdcbXZuZWRZbIRmQVBNsE7VDQpet2qFvq6vwKLBIQRQR5Kh58M4ok3A3US8Sr3rubYnaGqZSurCwI8uMA==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/storage": { + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/@firebase/storage/-/storage-0.11.2.tgz", + "integrity": "sha512-CtvoFaBI4hGXlXbaCHf8humajkbXhs39Nbh6MbNxtwJiCqxPy9iH3D3CCfXAvP0QvAAwmJUTK3+z9a++Kc4nkA==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.6.4", + "@firebase/util": "1.9.3", + "node-fetch": "2.6.7", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/storage-compat": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@firebase/storage-compat/-/storage-compat-0.3.2.tgz", + "integrity": "sha512-wvsXlLa9DVOMQJckbDNhXKKxRNNewyUhhbXev3t8kSgoCotd1v3MmqhKKz93ePhDnhHnDs7bYHy+Qa8dRY6BXw==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.6.4", + "@firebase/storage": "0.11.2", + "@firebase/storage-types": "0.8.0", + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/storage-types": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@firebase/storage-types/-/storage-types-0.8.0.tgz", + "integrity": "sha512-isRHcGrTs9kITJC0AVehHfpraWFui39MPaU7Eo8QfWlqW7YPymBmRgjDrlOgFdURh6Cdeg07zmkLP5tzTKRSpg==", + "license": "Apache-2.0", + "peerDependencies": { + "@firebase/app-types": "0.x", + "@firebase/util": "1.x" + } + }, + "node_modules/@firebase/storage/node_modules/node-fetch": { + "version": "2.6.7", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", + "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/@firebase/util": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/@firebase/util/-/util-1.9.3.tgz", + "integrity": "sha512-DY02CRhOZwpzO36fHpuVysz6JZrscPiBXD0fXp6qSrL9oNOx5KWICKdR95C0lSITzxp0TZosVyHqzatE8JbcjA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@firebase/webchannel-wrapper": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@firebase/webchannel-wrapper/-/webchannel-wrapper-0.10.1.tgz", + "integrity": "sha512-Dq5rYfEpdeel0bLVN+nfD1VWmzCkK+pJbSjIawGE+RY4+NIJqhbUDDQjvV0NUK84fMfwxvtFoCtEe70HfZjFcw==", + "license": "Apache-2.0" + }, + "node_modules/@fortawesome/fontawesome-free": { + "version": "6.7.2", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-6.7.2.tgz", + "integrity": "sha512-JUOtgFW6k9u4Y+xeIaEiLr3+cjoUPiAuLXoyKOJSia6Duzb7pq+A76P9ZdPDoAoxHdHzq6gE9/jKBGXlZT8FbA==", + "license": "(CC-BY-4.0 AND OFL-1.1 AND MIT)", + "engines": { + "node": ">=6" + } + }, + "node_modules/@gar/promisify": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz", + "integrity": "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@grpc/grpc-js": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.7.3.tgz", + "integrity": "sha512-H9l79u4kJ2PVSxUNA08HMYAnUBLj9v6KjYQ7SQ71hOZcEXhShE/y5iQCesP8+6/Ik/7i2O0a10bPquIcYfufog==", + "license": "Apache-2.0", + "dependencies": { + "@grpc/proto-loader": "^0.7.0", + "@types/node": ">=12.12.47" + }, + "engines": { + "node": "^8.13.0 || >=10.10.0" + } + }, + "node_modules/@grpc/grpc-js/node_modules/@grpc/proto-loader": { + "version": "0.7.13", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.13.tgz", + "integrity": "sha512-AiXO/bfe9bmxBjxxtYxFAXGZvMaN5s8kO+jBHAJCON8rJoB5YS/D6X7ZNc6XQkuHNmyl4CYaMI1fJ/Gn27RGGw==", + "license": "Apache-2.0", + "dependencies": { + "lodash.camelcase": "^4.3.0", + "long": "^5.0.0", + "protobufjs": "^7.2.5", + "yargs": "^17.7.2" + }, + "bin": { + "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@grpc/grpc-js/node_modules/long": { + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/long/-/long-5.2.4.tgz", + "integrity": "sha512-qtzLbJE8hq7VabR3mISmVGtoXP8KGc2Z/AT8OuqlYD7JTR3oqrgwdjnk07wpj1twXxYmgDXgoKVWUG/fReSzHg==", + "license": "Apache-2.0" + }, + "node_modules/@grpc/grpc-js/node_modules/protobufjs": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.4.0.tgz", + "integrity": "sha512-mRUWCc3KUU4w1jU8sGxICXH/gNS94DvI1gxqDvBzhj1JpcsimQkYiOJfwsPUykUI5ZaspFbSgmBLER8IrQ3tqw==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/node": ">=13.7.0", + "long": "^5.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@grpc/grpc-js/node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@grpc/proto-loader": { + "version": "0.6.13", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.6.13.tgz", + "integrity": "sha512-FjxPYDRTn6Ec3V0arm1FtSpmP6V50wuph2yILpyvTKzjc76oDdoihXqM1DzOW5ubvCC8GivfCnNtfaRE8myJ7g==", + "license": "Apache-2.0", + "dependencies": { + "@types/long": "^4.0.1", + "lodash.camelcase": "^4.3.0", + "long": "^4.0.0", + "protobufjs": "^6.11.3", + "yargs": "^16.2.0" + }, + "bin": { + "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@grpc/proto-loader/node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/@grpc/proto-loader/node_modules/yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "license": "MIT", + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@grpc/proto-loader/node_modules/yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.11.14", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", + "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==", + "deprecated": "Use @eslint/config-array instead", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanwhocodes/object-schema": "^2.0.2", + "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.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "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.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "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/@ionic/angular": { + "version": "7.8.6", + "resolved": "https://registry.npmjs.org/@ionic/angular/-/angular-7.8.6.tgz", + "integrity": "sha512-3Qe53hXpyjtx6fFcxt/NTAlauIawsGmCZJPauV5sAnSKVuX8C82C1zMAZTeJt6m2dnd71wythc98BXUXsx/UxQ==", + "license": "MIT", + "dependencies": { + "@ionic/core": "7.8.6", + "ionicons": "^7.0.0", + "jsonc-parser": "^3.0.0", + "tslib": "^2.3.0" + }, + "peerDependencies": { + "@angular/core": ">=14.0.0", + "@angular/forms": ">=14.0.0", + "@angular/router": ">=14.0.0", + "rxjs": ">=7.5.0", + "zone.js": ">=0.11.0" + } + }, + "node_modules/@ionic/core": { + "version": "7.8.6", + "resolved": "https://registry.npmjs.org/@ionic/core/-/core-7.8.6.tgz", + "integrity": "sha512-HAYZdEmeJgOdo2kDlZkcCGHb+zs/vjU6iv4skbVBL7y+OnSv/oC2u83Yee8S3/aY0YAxkyBgu7hLTYH13Zc2Aw==", + "license": "MIT", + "dependencies": { + "@stencil/core": "^4.12.2", + "ionicons": "^7.2.2", + "tslib": "^2.1.0" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "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/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "license": "MIT" + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "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/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "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/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.1.1.tgz", + "integrity": "sha512-sQXCasFk+U8lWYEe66WxRDOE9PjVz4vSM51fTu3Hw+ClTpUSQb718772vH3pyS5pShp6lvQM7SxgIDXXXmOX7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/set-array": "^1.0.0", + "@jridgewell/sourcemap-codec": "^1.4.10" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.6.tgz", + "integrity": "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, + "node_modules/@jridgewell/source-map/node_modules/@jridgewell/gen-mapping": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", + "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", + "license": "MIT", + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@leichtgewicht/ip-codec": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz", + "integrity": "sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@material/animation": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/animation/-/animation-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-5osi1z4JQIXcklPALbH/zTfOm2pDzHt9Fxm7ZyURy250xIZj6QjULRzPTnzOhC2ropfix9ra2Cfggbf0dcRbEQ==", + "license": "MIT", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@material/auto-init": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/auto-init/-/auto-init-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-OigQTmrVzkcGvxNjOaIe5oItTFPgrO9xLewvharDI6m6yvO1z7OBnkcW+sFN6ggLNYNxd0O1u9v64vMsmeDABQ==", + "license": "MIT", + "dependencies": { + "@material/base": "15.0.0-canary.684e33d25.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/banner": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/banner/-/banner-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-PqtGp3KWzdu58rWv/DIvSfe38m5YKOBbAAbBinSvgadBb/da+IE1t5F7YPNKE1T5lJsQBGVUYx6QBIeXm+aI/A==", + "license": "MIT", + "dependencies": { + "@material/base": "15.0.0-canary.684e33d25.0", + "@material/button": "15.0.0-canary.684e33d25.0", + "@material/dom": "15.0.0-canary.684e33d25.0", + "@material/elevation": "15.0.0-canary.684e33d25.0", + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "@material/ripple": "15.0.0-canary.684e33d25.0", + "@material/rtl": "15.0.0-canary.684e33d25.0", + "@material/shape": "15.0.0-canary.684e33d25.0", + "@material/theme": "15.0.0-canary.684e33d25.0", + "@material/tokens": "15.0.0-canary.684e33d25.0", + "@material/typography": "15.0.0-canary.684e33d25.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/base": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/base/-/base-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-oOaqb/SfjWwTKsdJUZmeh/Qrs41nIJI0N+zELsxnvbGjSIN1ZMAKYZFPMahqvC68OJ6+5CvJM8PoTNs5l+B8IQ==", + "license": "MIT", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@material/button": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/button/-/button-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-Nkekk4edeX+ObVOa7UlwavaHdmckPV5wU4SAJf3iA3R61cmz+KsgAgpzfcwv5WfNhIlc2nLu8QYEecpHdo9d/w==", + "license": "MIT", + "dependencies": { + "@material/density": "15.0.0-canary.684e33d25.0", + "@material/dom": "15.0.0-canary.684e33d25.0", + "@material/elevation": "15.0.0-canary.684e33d25.0", + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "@material/focus-ring": "15.0.0-canary.684e33d25.0", + "@material/ripple": "15.0.0-canary.684e33d25.0", + "@material/rtl": "15.0.0-canary.684e33d25.0", + "@material/shape": "15.0.0-canary.684e33d25.0", + "@material/theme": "15.0.0-canary.684e33d25.0", + "@material/tokens": "15.0.0-canary.684e33d25.0", + "@material/touch-target": "15.0.0-canary.684e33d25.0", + "@material/typography": "15.0.0-canary.684e33d25.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/card": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/card/-/card-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-xhyB7XX5KkEiCEqwSPkl58ZGYL6xFdnY62zimyBXJRG/Eaa0Swj3kW20hVCpt4f7c9Zmp8Se27rg8vnKmhvO3g==", + "license": "MIT", + "dependencies": { + "@material/dom": "15.0.0-canary.684e33d25.0", + "@material/elevation": "15.0.0-canary.684e33d25.0", + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "@material/ripple": "15.0.0-canary.684e33d25.0", + "@material/rtl": "15.0.0-canary.684e33d25.0", + "@material/shape": "15.0.0-canary.684e33d25.0", + "@material/theme": "15.0.0-canary.684e33d25.0", + "@material/tokens": "15.0.0-canary.684e33d25.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/checkbox": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/checkbox/-/checkbox-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-NFpM3TS924PmVsk2KQLNU95OYCf8ZwYgzeqfnAexU0bEfjUJXINBun2Go0AaeOUMjuvWUe+byjrXgv8SFYbMUA==", + "license": "MIT", + "dependencies": { + "@material/animation": "15.0.0-canary.684e33d25.0", + "@material/base": "15.0.0-canary.684e33d25.0", + "@material/density": "15.0.0-canary.684e33d25.0", + "@material/dom": "15.0.0-canary.684e33d25.0", + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "@material/focus-ring": "15.0.0-canary.684e33d25.0", + "@material/ripple": "15.0.0-canary.684e33d25.0", + "@material/theme": "15.0.0-canary.684e33d25.0", + "@material/touch-target": "15.0.0-canary.684e33d25.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/chips": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/chips/-/chips-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-z4ajQ4NnsAQ/Si9tZ4xmxzjj2Qb+vW++4QjCjjjwAGIZbCe0xglAnMh2t66XLJUxt7RoKZuZVEO7ZqcFZpvJFQ==", + "license": "MIT", + "dependencies": { + "@material/animation": "15.0.0-canary.684e33d25.0", + "@material/base": "15.0.0-canary.684e33d25.0", + "@material/checkbox": "15.0.0-canary.684e33d25.0", + "@material/density": "15.0.0-canary.684e33d25.0", + "@material/dom": "15.0.0-canary.684e33d25.0", + "@material/elevation": "15.0.0-canary.684e33d25.0", + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "@material/focus-ring": "15.0.0-canary.684e33d25.0", + "@material/ripple": "15.0.0-canary.684e33d25.0", + "@material/rtl": "15.0.0-canary.684e33d25.0", + "@material/shape": "15.0.0-canary.684e33d25.0", + "@material/theme": "15.0.0-canary.684e33d25.0", + "@material/tokens": "15.0.0-canary.684e33d25.0", + "@material/touch-target": "15.0.0-canary.684e33d25.0", + "@material/typography": "15.0.0-canary.684e33d25.0", + "safevalues": "^0.3.4", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/circular-progress": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/circular-progress/-/circular-progress-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-G6qD0nGNtEUwWnAMJuA9INYFpZoKtx7KFjBaPF4Ol2YLHtmShALNAYyn54TMAK8AZ2IpW08PXjGS7Ye88vrdEQ==", + "license": "MIT", + "dependencies": { + "@material/animation": "15.0.0-canary.684e33d25.0", + "@material/base": "15.0.0-canary.684e33d25.0", + "@material/dom": "15.0.0-canary.684e33d25.0", + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "@material/progress-indicator": "15.0.0-canary.684e33d25.0", + "@material/rtl": "15.0.0-canary.684e33d25.0", + "@material/theme": "15.0.0-canary.684e33d25.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/data-table": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/data-table/-/data-table-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-+wDw1DDDFfAsKAMzs84f/5GCjux39zjNfW8tL4wFbkWNwewmQrG9zaQMJhBpVOtLCrM8Gj6SOgOANqgqoCjvGg==", + "license": "MIT", + "dependencies": { + "@material/animation": "15.0.0-canary.684e33d25.0", + "@material/base": "15.0.0-canary.684e33d25.0", + "@material/checkbox": "15.0.0-canary.684e33d25.0", + "@material/density": "15.0.0-canary.684e33d25.0", + "@material/dom": "15.0.0-canary.684e33d25.0", + "@material/elevation": "15.0.0-canary.684e33d25.0", + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "@material/icon-button": "15.0.0-canary.684e33d25.0", + "@material/linear-progress": "15.0.0-canary.684e33d25.0", + "@material/list": "15.0.0-canary.684e33d25.0", + "@material/menu": "15.0.0-canary.684e33d25.0", + "@material/rtl": "15.0.0-canary.684e33d25.0", + "@material/select": "15.0.0-canary.684e33d25.0", + "@material/shape": "15.0.0-canary.684e33d25.0", + "@material/theme": "15.0.0-canary.684e33d25.0", + "@material/tokens": "15.0.0-canary.684e33d25.0", + "@material/touch-target": "15.0.0-canary.684e33d25.0", + "@material/typography": "15.0.0-canary.684e33d25.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/density": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/density/-/density-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-661yEVRMGrlq6S6WuSbPRO+ZwpdUOg2glCc7y96doM6itSLOa3UEAldjOLfsYZVB74GnKCiuDp//QmfoRyYTfA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@material/dialog": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/dialog/-/dialog-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-szn0dHnfeQTSOC6SSRSGAzX6Tnx+4NnSMUwNkXm+3bwjds8ZVK26+DXwLrP5f3ID5F1K5sFsRf2INo5/TNTHyQ==", + "license": "MIT", + "dependencies": { + "@material/animation": "15.0.0-canary.684e33d25.0", + "@material/base": "15.0.0-canary.684e33d25.0", + "@material/button": "15.0.0-canary.684e33d25.0", + "@material/dom": "15.0.0-canary.684e33d25.0", + "@material/elevation": "15.0.0-canary.684e33d25.0", + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "@material/icon-button": "15.0.0-canary.684e33d25.0", + "@material/ripple": "15.0.0-canary.684e33d25.0", + "@material/rtl": "15.0.0-canary.684e33d25.0", + "@material/shape": "15.0.0-canary.684e33d25.0", + "@material/theme": "15.0.0-canary.684e33d25.0", + "@material/tokens": "15.0.0-canary.684e33d25.0", + "@material/touch-target": "15.0.0-canary.684e33d25.0", + "@material/typography": "15.0.0-canary.684e33d25.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/dom": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/dom/-/dom-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-7pEJLYov+tGgfuD8mZxoVU6rWtPI8ppjTAhz+F27Hz9FG0JETMWTKpDPBXLnKvX7vhIxL83GvZ9geNHCe8Hfog==", + "license": "MIT", + "dependencies": { + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "@material/rtl": "15.0.0-canary.684e33d25.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/drawer": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/drawer/-/drawer-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-/KMckLf1PYU/H3PXnS4e0aFl03qG3JlSv4LGgX6juJufcONqGTl/m63EMO/L/eUy6H1CRrXmVDjik/jzHLyDhg==", + "license": "MIT", + "dependencies": { + "@material/animation": "15.0.0-canary.684e33d25.0", + "@material/base": "15.0.0-canary.684e33d25.0", + "@material/dom": "15.0.0-canary.684e33d25.0", + "@material/elevation": "15.0.0-canary.684e33d25.0", + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "@material/list": "15.0.0-canary.684e33d25.0", + "@material/ripple": "15.0.0-canary.684e33d25.0", + "@material/rtl": "15.0.0-canary.684e33d25.0", + "@material/shape": "15.0.0-canary.684e33d25.0", + "@material/theme": "15.0.0-canary.684e33d25.0", + "@material/typography": "15.0.0-canary.684e33d25.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/elevation": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/elevation/-/elevation-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-WDF8SsRtq3rXUbVVbd9K4DUijIPH0bUFSOreVYxudpuxAfTlDS5+aeS1EK9UIBFYLuba4u5wVT2tDv6e1RTfrQ==", + "license": "MIT", + "dependencies": { + "@material/animation": "15.0.0-canary.684e33d25.0", + "@material/base": "15.0.0-canary.684e33d25.0", + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "@material/rtl": "15.0.0-canary.684e33d25.0", + "@material/theme": "15.0.0-canary.684e33d25.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/fab": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/fab/-/fab-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-KCu87rWOKEAe9vZcAm6K8XazYSWPNjMG+OhrbPjHW6bCO7as1YCgtmkBkhff7csY/rFmcVpIy884xtUfLmSudQ==", + "license": "MIT", + "dependencies": { + "@material/animation": "15.0.0-canary.684e33d25.0", + "@material/dom": "15.0.0-canary.684e33d25.0", + "@material/elevation": "15.0.0-canary.684e33d25.0", + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "@material/focus-ring": "15.0.0-canary.684e33d25.0", + "@material/ripple": "15.0.0-canary.684e33d25.0", + "@material/rtl": "15.0.0-canary.684e33d25.0", + "@material/shape": "15.0.0-canary.684e33d25.0", + "@material/theme": "15.0.0-canary.684e33d25.0", + "@material/tokens": "15.0.0-canary.684e33d25.0", + "@material/touch-target": "15.0.0-canary.684e33d25.0", + "@material/typography": "15.0.0-canary.684e33d25.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/feature-targeting": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/feature-targeting/-/feature-targeting-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-HyH1erNTSjS63sigNSUMaCd0nJhTNdDFeC+myrxwtDaQm+uYJ8troCNtQM3g6mx0XATNtX5aTOoPmrM6yVVi1A==", + "license": "MIT", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@material/floating-label": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/floating-label/-/floating-label-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-f7TPp6bKpGvV3sYYiZHSGlrixXKkXXITW3Esp7KB9jRq42c0H82novmdwvY0eTef4ootmA2JEysr78KQfHBUPg==", + "license": "MIT", + "dependencies": { + "@material/animation": "15.0.0-canary.684e33d25.0", + "@material/base": "15.0.0-canary.684e33d25.0", + "@material/dom": "15.0.0-canary.684e33d25.0", + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "@material/rtl": "15.0.0-canary.684e33d25.0", + "@material/theme": "15.0.0-canary.684e33d25.0", + "@material/typography": "15.0.0-canary.684e33d25.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/focus-ring": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/focus-ring/-/focus-ring-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-ikw2RVUfgzXChpWIzPH1VzRvTjYb5ZKj4H+CZf7jqPUXMstFOZg90Bp7ARLZHqYiyNMuUq3zUTHozS6iHorSqg==", + "license": "MIT", + "dependencies": { + "@material/dom": "15.0.0-canary.684e33d25.0", + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "@material/rtl": "15.0.0-canary.684e33d25.0" + } + }, + "node_modules/@material/form-field": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/form-field/-/form-field-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-vpF9N/uq5no/7+8GAbEH0868FhOuBgxAWRr1Sfb+jthKfBr8OS/wPU/AHzZHdHdAm7PQynbeOXfDsX2dI//PDA==", + "license": "MIT", + "dependencies": { + "@material/base": "15.0.0-canary.684e33d25.0", + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "@material/ripple": "15.0.0-canary.684e33d25.0", + "@material/rtl": "15.0.0-canary.684e33d25.0", + "@material/theme": "15.0.0-canary.684e33d25.0", + "@material/typography": "15.0.0-canary.684e33d25.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/icon-button": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/icon-button/-/icon-button-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-wMI+XGzmIN/o2ePBKg2hLyx7H4pXCRAyyIKMQS1FMp1UKa2tYmiHVX/V8skhKwCqxg3i6Ls/LxMjfPxTR18WvQ==", + "license": "MIT", + "dependencies": { + "@material/base": "15.0.0-canary.684e33d25.0", + "@material/density": "15.0.0-canary.684e33d25.0", + "@material/dom": "15.0.0-canary.684e33d25.0", + "@material/elevation": "15.0.0-canary.684e33d25.0", + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "@material/focus-ring": "15.0.0-canary.684e33d25.0", + "@material/ripple": "15.0.0-canary.684e33d25.0", + "@material/rtl": "15.0.0-canary.684e33d25.0", + "@material/theme": "15.0.0-canary.684e33d25.0", + "@material/touch-target": "15.0.0-canary.684e33d25.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/image-list": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/image-list/-/image-list-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-Ol+uaHYBe5R/cgzlfh5ONnMVX0wO6fV74JMUcQCQlxP6lXau/edARo4tkRc7A7UJUkU3VRv0EpEjLoCRNUPGaA==", + "license": "MIT", + "dependencies": { + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "@material/shape": "15.0.0-canary.684e33d25.0", + "@material/theme": "15.0.0-canary.684e33d25.0", + "@material/typography": "15.0.0-canary.684e33d25.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/layout-grid": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/layout-grid/-/layout-grid-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-ALXE1mqFNb/RB2lVRQ3/r1Aufw2mFZnOjRE+boYDVepmAG/xWyPCyaGoavELJF5l4GAb0tXi8wA/8HeGbLOpuA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@material/line-ripple": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/line-ripple/-/line-ripple-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-7hRx8C/e9i0P6pgQpNOMfTwSS2r1fwEvBL72QDVGLtLuoKKwsjjgP6Z0Jat/GeHJe87u9LQvGBoD4upt+of/HA==", + "license": "MIT", + "dependencies": { + "@material/animation": "15.0.0-canary.684e33d25.0", + "@material/base": "15.0.0-canary.684e33d25.0", + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "@material/theme": "15.0.0-canary.684e33d25.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/linear-progress": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/linear-progress/-/linear-progress-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-iJclt7mKmcMk6pqD7ocXKfCWZhqBoODp7N593jYlxVpTJuEz2wiVAjZUDn/YGj/Uz3CRH+2YFfOiLr9pwWjhDg==", + "license": "MIT", + "dependencies": { + "@material/animation": "15.0.0-canary.684e33d25.0", + "@material/base": "15.0.0-canary.684e33d25.0", + "@material/dom": "15.0.0-canary.684e33d25.0", + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "@material/progress-indicator": "15.0.0-canary.684e33d25.0", + "@material/rtl": "15.0.0-canary.684e33d25.0", + "@material/theme": "15.0.0-canary.684e33d25.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/list": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/list/-/list-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-rQ+FCSdzmwTcT00IYE0uRV3CS4oGSccKFl9hkcF+aHFW61L7ORh/SCGUDPrEfQFrFkMn5f8qroVJjpUAMXBz4g==", + "license": "MIT", + "dependencies": { + "@material/base": "15.0.0-canary.684e33d25.0", + "@material/density": "15.0.0-canary.684e33d25.0", + "@material/dom": "15.0.0-canary.684e33d25.0", + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "@material/ripple": "15.0.0-canary.684e33d25.0", + "@material/rtl": "15.0.0-canary.684e33d25.0", + "@material/shape": "15.0.0-canary.684e33d25.0", + "@material/theme": "15.0.0-canary.684e33d25.0", + "@material/tokens": "15.0.0-canary.684e33d25.0", + "@material/typography": "15.0.0-canary.684e33d25.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/menu": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/menu/-/menu-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-r7wzDLSGSI9629/mfpvsMzkVxpmV75kcD3IrW0Pcu6/Bv/1xi0EvjcUXzNJJoQlwN4Zj35Ymz/PCjZkIDIz68Q==", + "license": "MIT", + "dependencies": { + "@material/base": "15.0.0-canary.684e33d25.0", + "@material/dom": "15.0.0-canary.684e33d25.0", + "@material/elevation": "15.0.0-canary.684e33d25.0", + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "@material/list": "15.0.0-canary.684e33d25.0", + "@material/menu-surface": "15.0.0-canary.684e33d25.0", + "@material/ripple": "15.0.0-canary.684e33d25.0", + "@material/rtl": "15.0.0-canary.684e33d25.0", + "@material/shape": "15.0.0-canary.684e33d25.0", + "@material/theme": "15.0.0-canary.684e33d25.0", + "@material/tokens": "15.0.0-canary.684e33d25.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/menu-surface": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/menu-surface/-/menu-surface-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-RVO5GAYcfWPaKwxsF/NhUAmrYXQCQBKvRQW0TIlbmAJz6lcFeTs6YZqF3u1C7qrL3ZQGz+sur/7ywj6QU0oMow==", + "license": "MIT", + "dependencies": { + "@material/animation": "15.0.0-canary.684e33d25.0", + "@material/base": "15.0.0-canary.684e33d25.0", + "@material/elevation": "15.0.0-canary.684e33d25.0", + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "@material/rtl": "15.0.0-canary.684e33d25.0", + "@material/shape": "15.0.0-canary.684e33d25.0", + "@material/theme": "15.0.0-canary.684e33d25.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/notched-outline": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/notched-outline/-/notched-outline-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-9YHcBkvJLPVYzkHcWoTpBZAFrEd+j1hjhGxLhh0LuNrZe8VroUkZD1TTnUAPHRG3os6EqEWWaKb0RN+aPIF2yQ==", + "license": "MIT", + "dependencies": { + "@material/base": "15.0.0-canary.684e33d25.0", + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "@material/floating-label": "15.0.0-canary.684e33d25.0", + "@material/rtl": "15.0.0-canary.684e33d25.0", + "@material/shape": "15.0.0-canary.684e33d25.0", + "@material/theme": "15.0.0-canary.684e33d25.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/progress-indicator": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/progress-indicator/-/progress-indicator-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-c0icji4faeNWUoqGENGC7Hav0Puxh0RwXIDVizffaUxKIGbajpIp5+4Zop73fK/xFLGMB/npg7TbP+aCGjQ3fw==", + "license": "MIT", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@material/radio": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/radio/-/radio-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-U3Eh8sNUA8trDla1Bq8Bo02foxYvtoewaKeF8A8tAju81XZ4jRiftfOsOWZDZEHCVbbCB2QwvutvFlnay5n+Aw==", + "license": "MIT", + "dependencies": { + "@material/animation": "15.0.0-canary.684e33d25.0", + "@material/base": "15.0.0-canary.684e33d25.0", + "@material/density": "15.0.0-canary.684e33d25.0", + "@material/dom": "15.0.0-canary.684e33d25.0", + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "@material/focus-ring": "15.0.0-canary.684e33d25.0", + "@material/ripple": "15.0.0-canary.684e33d25.0", + "@material/theme": "15.0.0-canary.684e33d25.0", + "@material/touch-target": "15.0.0-canary.684e33d25.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/ripple": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/ripple/-/ripple-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-RyePu7SjIm/OuyyEieZ/gxiPYkNZOZHeid72WRcN9ofdlljj2pifcdPvcfZA+v/DMS33xo5GjG2L/Qj6ClWrKw==", + "license": "MIT", + "dependencies": { + "@material/animation": "15.0.0-canary.684e33d25.0", + "@material/base": "15.0.0-canary.684e33d25.0", + "@material/dom": "15.0.0-canary.684e33d25.0", + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "@material/rtl": "15.0.0-canary.684e33d25.0", + "@material/theme": "15.0.0-canary.684e33d25.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/rtl": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/rtl/-/rtl-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-NqdJl8Ayupp1Th+vCNCpVQHbUFOuF7TCte9LD1norTIBUF/QizIxWby2W5uUEiPbnh5j9PmE1CJtfLwKun3pcw==", + "license": "MIT", + "dependencies": { + "@material/theme": "15.0.0-canary.684e33d25.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/segmented-button": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/segmented-button/-/segmented-button-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-bEGgg8vgXNLyukyV8HRjFMuQ6t6nm5LQ4Pgm22um61Yc8qyi0BOqV41OR4SVdUrUqZxh1aVD+p+4NN03+LfQXw==", + "license": "MIT", + "dependencies": { + "@material/base": "15.0.0-canary.684e33d25.0", + "@material/elevation": "15.0.0-canary.684e33d25.0", + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "@material/ripple": "15.0.0-canary.684e33d25.0", + "@material/theme": "15.0.0-canary.684e33d25.0", + "@material/touch-target": "15.0.0-canary.684e33d25.0", + "@material/typography": "15.0.0-canary.684e33d25.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/select": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/select/-/select-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-kf178/2TeEinTv0mgmSBcmmExQ2h7a7dtR1E3WuqQgisJ/R6+zVLMkC2CnfIyzxYX2vkuUTG0ue3Reh/6XiqSg==", + "license": "MIT", + "dependencies": { + "@material/animation": "15.0.0-canary.684e33d25.0", + "@material/base": "15.0.0-canary.684e33d25.0", + "@material/density": "15.0.0-canary.684e33d25.0", + "@material/dom": "15.0.0-canary.684e33d25.0", + "@material/elevation": "15.0.0-canary.684e33d25.0", + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "@material/floating-label": "15.0.0-canary.684e33d25.0", + "@material/line-ripple": "15.0.0-canary.684e33d25.0", + "@material/list": "15.0.0-canary.684e33d25.0", + "@material/menu": "15.0.0-canary.684e33d25.0", + "@material/menu-surface": "15.0.0-canary.684e33d25.0", + "@material/notched-outline": "15.0.0-canary.684e33d25.0", + "@material/ripple": "15.0.0-canary.684e33d25.0", + "@material/rtl": "15.0.0-canary.684e33d25.0", + "@material/shape": "15.0.0-canary.684e33d25.0", + "@material/theme": "15.0.0-canary.684e33d25.0", + "@material/tokens": "15.0.0-canary.684e33d25.0", + "@material/typography": "15.0.0-canary.684e33d25.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/shape": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/shape/-/shape-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-aEelpaTFmpnCji3TUGP9bVCS/bRVjUmLTHBPZtuu1gOrUVVtJ6kYOg73dZNJF+XOoNL2yOX/LRcKwsop29tptA==", + "license": "MIT", + "dependencies": { + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "@material/rtl": "15.0.0-canary.684e33d25.0", + "@material/theme": "15.0.0-canary.684e33d25.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/slider": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/slider/-/slider-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-WVyK+2pSNSZmj07M2K/a3TADoQ9FBCndfNC/vE7/wGIg4dddJJK5KvQ+yruf9R2cSzTL/S1sZ5WpyyeM8E9HTw==", + "license": "MIT", + "dependencies": { + "@material/animation": "15.0.0-canary.684e33d25.0", + "@material/base": "15.0.0-canary.684e33d25.0", + "@material/dom": "15.0.0-canary.684e33d25.0", + "@material/elevation": "15.0.0-canary.684e33d25.0", + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "@material/ripple": "15.0.0-canary.684e33d25.0", + "@material/rtl": "15.0.0-canary.684e33d25.0", + "@material/theme": "15.0.0-canary.684e33d25.0", + "@material/tokens": "15.0.0-canary.684e33d25.0", + "@material/typography": "15.0.0-canary.684e33d25.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/snackbar": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/snackbar/-/snackbar-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-itO+DCkOannZzR1/cCHcqAm7ifhuFvXmDItNoA8qLEcAyJDJJRkhpwj3XQ01yuo9gBFcSctp7Txt7e+Hncm/Jg==", + "license": "MIT", + "dependencies": { + "@material/animation": "15.0.0-canary.684e33d25.0", + "@material/base": "15.0.0-canary.684e33d25.0", + "@material/button": "15.0.0-canary.684e33d25.0", + "@material/dom": "15.0.0-canary.684e33d25.0", + "@material/elevation": "15.0.0-canary.684e33d25.0", + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "@material/icon-button": "15.0.0-canary.684e33d25.0", + "@material/ripple": "15.0.0-canary.684e33d25.0", + "@material/rtl": "15.0.0-canary.684e33d25.0", + "@material/shape": "15.0.0-canary.684e33d25.0", + "@material/theme": "15.0.0-canary.684e33d25.0", + "@material/tokens": "15.0.0-canary.684e33d25.0", + "@material/typography": "15.0.0-canary.684e33d25.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/switch": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/switch/-/switch-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-Jxi0gl92yvvZZsAPxvVHzXx2ga+T/djMow98jvEczmpUorWnAhgiCr9CsSSRoosahWyRB8NLZOxUQrACxvffjw==", + "license": "MIT", + "dependencies": { + "@material/animation": "15.0.0-canary.684e33d25.0", + "@material/base": "15.0.0-canary.684e33d25.0", + "@material/density": "15.0.0-canary.684e33d25.0", + "@material/dom": "15.0.0-canary.684e33d25.0", + "@material/elevation": "15.0.0-canary.684e33d25.0", + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "@material/focus-ring": "15.0.0-canary.684e33d25.0", + "@material/ripple": "15.0.0-canary.684e33d25.0", + "@material/rtl": "15.0.0-canary.684e33d25.0", + "@material/shape": "15.0.0-canary.684e33d25.0", + "@material/theme": "15.0.0-canary.684e33d25.0", + "@material/tokens": "15.0.0-canary.684e33d25.0", + "safevalues": "^0.3.4", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/tab": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/tab/-/tab-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-WQL3wj9syHNcfe8KbgGGUcA34M8C/xZ+n0Fkkh8Kk6puVwaU+xqUNihsxPY6YzKpmh4PZ4oJaBdiN8zvFT1zqQ==", + "license": "MIT", + "dependencies": { + "@material/base": "15.0.0-canary.684e33d25.0", + "@material/elevation": "15.0.0-canary.684e33d25.0", + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "@material/focus-ring": "15.0.0-canary.684e33d25.0", + "@material/ripple": "15.0.0-canary.684e33d25.0", + "@material/rtl": "15.0.0-canary.684e33d25.0", + "@material/tab-indicator": "15.0.0-canary.684e33d25.0", + "@material/theme": "15.0.0-canary.684e33d25.0", + "@material/tokens": "15.0.0-canary.684e33d25.0", + "@material/typography": "15.0.0-canary.684e33d25.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/tab-bar": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/tab-bar/-/tab-bar-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-SW/cMaDsIGGkM1ag3A7GJRlmr8eXmObWsvitQJzh6Azr5zzZtSI+GQygkMesAEE1gbpqOVN8d40rh3H7VVIAcA==", + "license": "MIT", + "dependencies": { + "@material/animation": "15.0.0-canary.684e33d25.0", + "@material/base": "15.0.0-canary.684e33d25.0", + "@material/density": "15.0.0-canary.684e33d25.0", + "@material/elevation": "15.0.0-canary.684e33d25.0", + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "@material/tab": "15.0.0-canary.684e33d25.0", + "@material/tab-indicator": "15.0.0-canary.684e33d25.0", + "@material/tab-scroller": "15.0.0-canary.684e33d25.0", + "@material/theme": "15.0.0-canary.684e33d25.0", + "@material/tokens": "15.0.0-canary.684e33d25.0", + "@material/typography": "15.0.0-canary.684e33d25.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/tab-indicator": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/tab-indicator/-/tab-indicator-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-kKICqSPqOlaf0lzaFFCmuOqPXJC+cK48Qmsc+m5o6fJhkmuZRCYpIwB2JeP+uZSOq/bTH+SrPtCtnVlgWg6ksA==", + "license": "MIT", + "dependencies": { + "@material/animation": "15.0.0-canary.684e33d25.0", + "@material/base": "15.0.0-canary.684e33d25.0", + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "@material/theme": "15.0.0-canary.684e33d25.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/tab-scroller": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/tab-scroller/-/tab-scroller-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-H6EU/TSiK/M2DyyORX5GEtXD9rKYxTMHC2VxsNWARPMFJGzgeW2ugYkFv+rKI1/c0bs0CJ4e+qFnOlBsQXZvyQ==", + "license": "MIT", + "dependencies": { + "@material/animation": "15.0.0-canary.684e33d25.0", + "@material/base": "15.0.0-canary.684e33d25.0", + "@material/dom": "15.0.0-canary.684e33d25.0", + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "@material/tab": "15.0.0-canary.684e33d25.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/textfield": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/textfield/-/textfield-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-OvgpDXjvpyJTtAWskO69IDybFvDNzr9w2PN/Fk7yFm+uNVupaWz1Ew8lZ4gGslaTNSVmh2XcsvmzxcLINSiiNg==", + "license": "MIT", + "dependencies": { + "@material/animation": "15.0.0-canary.684e33d25.0", + "@material/base": "15.0.0-canary.684e33d25.0", + "@material/density": "15.0.0-canary.684e33d25.0", + "@material/dom": "15.0.0-canary.684e33d25.0", + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "@material/floating-label": "15.0.0-canary.684e33d25.0", + "@material/line-ripple": "15.0.0-canary.684e33d25.0", + "@material/notched-outline": "15.0.0-canary.684e33d25.0", + "@material/ripple": "15.0.0-canary.684e33d25.0", + "@material/rtl": "15.0.0-canary.684e33d25.0", + "@material/shape": "15.0.0-canary.684e33d25.0", + "@material/theme": "15.0.0-canary.684e33d25.0", + "@material/tokens": "15.0.0-canary.684e33d25.0", + "@material/typography": "15.0.0-canary.684e33d25.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/theme": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/theme/-/theme-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-AZxaXXAvRKzAi20RlMxzt2U5UmkCWyv7DMWEBXsxtG5Tk54mi1HsbVUp3fxDPTlmL7Pq8p1/DESg/o7TgRCVlw==", + "license": "MIT", + "dependencies": { + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/tokens": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/tokens/-/tokens-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-wVwbQOTCXDPKYPdHQHLr026y36MMFelID1CmbfRk6mSol4O8yE9U0fXcShfRDW8Qo5E3X31w9c2A6T3neJY7wQ==", + "license": "MIT", + "dependencies": { + "@material/elevation": "15.0.0-canary.684e33d25.0" + } + }, + "node_modules/@material/tooltip": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/tooltip/-/tooltip-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-dtm26QjxyQdinc8btgz6yys07b7bUW4FZgNF2EBPeGrICrPg7jf+JEvDziz5g8VMaTBQLOQRSCGy0MKuRlOjLw==", + "license": "MIT", + "dependencies": { + "@material/animation": "15.0.0-canary.684e33d25.0", + "@material/base": "15.0.0-canary.684e33d25.0", + "@material/button": "15.0.0-canary.684e33d25.0", + "@material/dom": "15.0.0-canary.684e33d25.0", + "@material/elevation": "15.0.0-canary.684e33d25.0", + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "@material/rtl": "15.0.0-canary.684e33d25.0", + "@material/shape": "15.0.0-canary.684e33d25.0", + "@material/theme": "15.0.0-canary.684e33d25.0", + "@material/tokens": "15.0.0-canary.684e33d25.0", + "@material/typography": "15.0.0-canary.684e33d25.0", + "safevalues": "^0.3.4", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/top-app-bar": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/top-app-bar/-/top-app-bar-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-1M+oupUxflfW7u81P1XlxoLZB8bLzwtpKofIfDNRbEsiKhlLTERJR3Yak3BGE9xakNMysAaBHlkb5MrN5bNPFw==", + "license": "MIT", + "dependencies": { + "@material/animation": "15.0.0-canary.684e33d25.0", + "@material/base": "15.0.0-canary.684e33d25.0", + "@material/elevation": "15.0.0-canary.684e33d25.0", + "@material/ripple": "15.0.0-canary.684e33d25.0", + "@material/rtl": "15.0.0-canary.684e33d25.0", + "@material/shape": "15.0.0-canary.684e33d25.0", + "@material/theme": "15.0.0-canary.684e33d25.0", + "@material/typography": "15.0.0-canary.684e33d25.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/touch-target": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/touch-target/-/touch-target-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-zdE69Slg8+T7sTn1OwqZ6H7WBYac9mxJ/JlJqfTqthzIjZRcCxBSYymQJcDHjsrPnUojOtr9U4Tpm5YZ96TEkQ==", + "license": "MIT", + "dependencies": { + "@material/base": "15.0.0-canary.684e33d25.0", + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "@material/rtl": "15.0.0-canary.684e33d25.0", + "@material/theme": "15.0.0-canary.684e33d25.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/typography": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/typography/-/typography-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-aVnvgMwcfNa/K4wujzpKDIxjGl2hbkEL+m+OKDSQqWYjKcP9QrbzCXJruJBqxrBoPRHLbqo47k5f9uT8raSgjw==", + "license": "MIT", + "dependencies": { + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "@material/theme": "15.0.0-canary.684e33d25.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@ng-idle/core": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/@ng-idle/core/-/core-14.0.0.tgz", + "integrity": "sha512-DqseA+F6LXm/Hr4D8zQhDBliuAU06t9BOUpcdSUKQWdDEKo3mlovRxz7DMuXyzg4OBQ0saQpE3MeMqakIEHUHw==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.0.0" + }, + "peerDependencies": { + "@angular/common": ">=9.0.0", + "@angular/core": ">=9.0.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/@npmcli/fs": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-3.1.1.tgz", + "integrity": "sha512-q9CRWjpHCMIh5sVyefoD1cA7PkvILqCZsnSOEUUivORLjxCO/Irmue2DprETiNgEqktDBZaM1Bi+jrarx1XdCg==", + "dev": true, + "license": "ISC", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/git": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@npmcli/git/-/git-4.1.0.tgz", + "integrity": "sha512-9hwoB3gStVfa0N31ymBmrX+GuDGdVA/QWShZVqE0HK2Af+7QGGrCTbZia/SW0ImUTjTne7SP91qxDmtXvDHRPQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/promise-spawn": "^6.0.0", + "lru-cache": "^7.4.4", + "npm-pick-manifest": "^8.0.0", + "proc-log": "^3.0.0", + "promise-inflight": "^1.0.1", + "promise-retry": "^2.0.1", + "semver": "^7.3.5", + "which": "^3.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/git/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/@npmcli/git/node_modules/which": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/which/-/which-3.0.1.tgz", + "integrity": "sha512-XA1b62dzQzLfaEOSQFTCOd5KFf/1VSzZo7/7TUjnya6u0vGGKzU96UQBZTAThCb2j4/xjBAyii1OhRLJEivHvg==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/installed-package-contents": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@npmcli/installed-package-contents/-/installed-package-contents-2.1.0.tgz", + "integrity": "sha512-c8UuGLeZpm69BryRykLuKRyKFZYJsZSCT4aVY5ds4omyZqJ172ApzgfKJ5eV/r3HgLdUYgFVe54KSFVjKoe27w==", + "dev": true, + "license": "ISC", + "dependencies": { + "npm-bundled": "^3.0.0", + "npm-normalize-package-bin": "^3.0.0" + }, + "bin": { + "installed-package-contents": "bin/index.js" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/move-file": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@npmcli/move-file/-/move-file-2.0.1.tgz", + "integrity": "sha512-mJd2Z5TjYWq/ttPLLGqArdtnC74J6bOzg4rMDnN+p1xTacZ2yPRCk2y0oSWQtygLR9YVQXgOcONrwtnk3JupxQ==", + "deprecated": "This functionality has been moved to @npmcli/fs", + "dev": true, + "license": "MIT", + "dependencies": { + "mkdirp": "^1.0.4", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/@npmcli/move-file/node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true, + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@npmcli/node-gyp": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/node-gyp/-/node-gyp-3.0.0.tgz", + "integrity": "sha512-gp8pRXC2oOxu0DUE1/M3bYtb1b3/DbJ5aM113+XJBgfXdussRAsX0YOrOhdd8WvnAR6auDBvJomGAkLKA5ydxA==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/promise-spawn": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@npmcli/promise-spawn/-/promise-spawn-6.0.2.tgz", + "integrity": "sha512-gGq0NJkIGSwdbUt4yhdF8ZrmkGKVz9vAdVzpOfnom+V8PLSmSOVhZwbNvZZS1EYcJN5hzzKBxmmVVAInM6HQLg==", + "dev": true, + "license": "ISC", + "dependencies": { + "which": "^3.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/promise-spawn/node_modules/which": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/which/-/which-3.0.1.tgz", + "integrity": "sha512-XA1b62dzQzLfaEOSQFTCOd5KFf/1VSzZo7/7TUjnya6u0vGGKzU96UQBZTAThCb2j4/xjBAyii1OhRLJEivHvg==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/run-script": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@npmcli/run-script/-/run-script-6.0.2.tgz", + "integrity": "sha512-NCcr1uQo1k5U+SYlnIrbAh3cxy+OQT1VtqiAbxdymSlptbzBb62AjH2xXgjNCoP073hoa1CfCAcwoZ8k96C4nA==", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/node-gyp": "^3.0.0", + "@npmcli/promise-spawn": "^6.0.0", + "node-gyp": "^9.0.0", + "read-package-json-fast": "^3.0.0", + "which": "^3.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/run-script/node_modules/which": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/which/-/which-3.0.1.tgz", + "integrity": "sha512-XA1b62dzQzLfaEOSQFTCOd5KFf/1VSzZo7/7TUjnya6u0vGGKzU96UQBZTAThCb2j4/xjBAyii1OhRLJEivHvg==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@pkgr/core": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.1.1.tgz", + "integrity": "sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/unts" + } + }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", + "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1", + "@protobufjs/inquire": "^1.1.0" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/inquire": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", + "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", + "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==", + "license": "BSD-3-Clause" + }, + "node_modules/@schematics/angular": { + "version": "15.1.6", + "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-15.1.6.tgz", + "integrity": "sha512-y2kIQ1wJL0wR6v/LM5+PFJUivrYtdaIJVRdOXLLWl0AB5aLwObiWgLzAuBsbGm/9//WPPhw9PglS5EFFxTBDzg==", + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "15.1.6", + "@angular-devkit/schematics": "15.1.6", + "jsonc-parser": "3.2.0" + }, + "engines": { + "node": "^14.20.0 || ^16.13.0 || >=18.10.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@schematics/angular/node_modules/@angular-devkit/core": { + "version": "15.1.6", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-15.1.6.tgz", + "integrity": "sha512-jGgxyRjecVf6lEyqDxz7ltMEndNPxIg720pk6r40fgsu0dU8w9vjJSJe7k0XdJiXVRcN6wZa/J5nO/xcwWVIsA==", + "license": "MIT", + "dependencies": { + "ajv": "8.12.0", + "ajv-formats": "2.1.1", + "jsonc-parser": "3.2.0", + "rxjs": "6.6.7", + "source-map": "0.7.4" + }, + "engines": { + "node": "^14.20.0 || ^16.13.0 || >=18.10.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "chokidar": "^3.5.2" + }, + "peerDependenciesMeta": { + "chokidar": { + "optional": true + } + } + }, + "node_modules/@schematics/angular/node_modules/rxjs": { + "version": "6.6.7", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", + "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^1.9.0" + }, + "engines": { + "npm": ">=2.0.0" + } + }, + "node_modules/@schematics/angular/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "license": "0BSD" + }, + "node_modules/@socket.io/component-emitter": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", + "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@stencil/core": { + "version": "4.25.1", + "resolved": "https://registry.npmjs.org/@stencil/core/-/core-4.25.1.tgz", + "integrity": "sha512-SJhAAN6nHj7l2n2F7H6NoOUFhXC5tYXSvOpAaNFi5As6c2jOvaDOvAH+om4OKmw8U2kHI9yihEqpZrtY1J20dQ==", + "license": "MIT", + "bin": { + "stencil": "bin/stencil" + }, + "engines": { + "node": ">=16.0.0", + "npm": ">=7.10.0" + } + }, + "node_modules/@tootallnate/once": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", + "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/@types/body-parser": { + "version": "1.19.5", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", + "integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/bonjour": { + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@types/bonjour/-/bonjour-3.5.13.tgz", + "integrity": "sha512-z9fJ5Im06zvUL548KvYNecEVlA7cVDkGUi6kZusb04mpyEFKCIZJvloCcmpmLaIahDpOQGHaHmG6imtPMmPXGQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/connect-history-api-fallback": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.5.4.tgz", + "integrity": "sha512-n6Cr2xS1h4uAulPRdlw6Jl6s1oG8KrVilPN2yUITEs+K48EzMJJ3W1xy8K5eWuFvjp3R74AOIGSmp2UfBJ8HFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express-serve-static-core": "*", + "@types/node": "*" + } + }, + "node_modules/@types/cors": { + "version": "2.8.17", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz", + "integrity": "sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/eslint": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", + "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", + "license": "MIT", + "dependencies": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "node_modules/@types/eslint-scope": { + "version": "3.7.7", + "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", + "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", + "license": "MIT", + "dependencies": { + "@types/eslint": "*", + "@types/estree": "*" + } + }, + "node_modules/@types/estree": { + "version": "0.0.51", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.51.tgz", + "integrity": "sha512-CuPgU6f3eT/XgKKPqKd/gLZV1Xmvf1a2R5POBOGQa6uv82xpls89HU5zKeVoyR8XzHd1RGNOlQlvUe3CFkjWNQ==", + "license": "MIT" + }, + "node_modules/@types/express": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz", + "integrity": "sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.33", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.6.tgz", + "integrity": "sha512-3xhRnjJPkULekpSzgtoNYYcTWgEZkp4myc+Saevii5JPnHNvHMRlBSHDbs7Bh1iPPoVTERHEZXyhyLbMEsExsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/express/node_modules/@types/express-serve-static-core": { + "version": "4.19.6", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.6.tgz", + "integrity": "sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/http-errors": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", + "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/http-proxy": { + "version": "1.17.15", + "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.15.tgz", + "integrity": "sha512-25g5atgiVNTIv0LBDTg1H74Hvayx0ajtJPLLcYE3whFv75J0pWNtOBzaXJQgDTmrX1bx5U9YC2w/n65BN1HwRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/jasmine": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/@types/jasmine/-/jasmine-4.3.6.tgz", + "integrity": "sha512-3N0FpQTeiWjm+Oo1WUYWguUS7E6JLceiGTriFrG8k5PU7zRLJCzLcWURU3wjMbZGS//a2/LgjsnO3QxIlwxt9g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "license": "MIT" + }, + "node_modules/@types/long": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz", + "integrity": "sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==", + "license": "MIT" + }, + "node_modules/@types/marked": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@types/marked/-/marked-4.3.2.tgz", + "integrity": "sha512-a79Yc3TOk6dGdituy8hmTTJXjOkZ7zsFYV10L337ttq/rec8lRMDBpV7fL3uLx6TgbFCa5DU/h8FmIBQPSbU0w==", + "license": "MIT" + }, + "node_modules/@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/minimist": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-hov8bUuiLiyFPGyFPE1lwWhmzYbirOXQNNo40+y3zow8aFVTeyn3VWL0VFFfdNddA8S4Vf0Tc062rzyNr7Paag==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "20.12.7", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.7.tgz", + "integrity": "sha512-wq0cICSkRLVaf3UGLMGItu/PtdY7oaXaI/RVU+xliKVOtRna3PRY57ZDfztpDL0n11vfymMUnXv8QwYCO7L1wg==", + "license": "MIT", + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/@types/node-forge": { + "version": "1.3.11", + "resolved": "https://registry.npmjs.org/@types/node-forge/-/node-forge-1.3.11.tgz", + "integrity": "sha512-FQx220y22OKNTqaByeBGqHWYz4cl94tpcxeFdvBo3wjG6XPBuZ0BNgNZRV5J5TFmmcsJ4IzsLkmGRiQbnYsBEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/normalize-package-data": { + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz", + "integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/parse-json": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", + "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/qs": { + "version": "6.9.18", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.18.tgz", + "integrity": "sha512-kK7dgTYDyGqS+e2Q4aK9X3D7q234CIZ1Bv0q/7Z5IwRDoADNU81xXJK/YVyLbLTZCoIwUoDoffFeF+p/eIklAA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/semver": { + "version": "7.5.8", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.8.tgz", + "integrity": "sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/send": { + "version": "0.17.4", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", + "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/serve-index": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@types/serve-index/-/serve-index-1.9.4.tgz", + "integrity": "sha512-qLpGZ/c2fhSs5gnYsQxtDEq3Oy8SXPClIXkW5ghvAvsNuVSA8k+gCONcUCS/UjLEYvYps+e8uBtfgXgvhwfNug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.7", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.7.tgz", + "integrity": "sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "*" + } + }, + "node_modules/@types/sockjs": { + "version": "0.3.36", + "resolved": "https://registry.npmjs.org/@types/sockjs/-/sockjs-0.3.36.tgz", + "integrity": "sha512-MK9V6NzAS1+Ud7JV9lJLFqW85VbC9dq3LmwZCuBe4wBDgKC0Kj/jd8Xl+nSviU+Qc3+m7umHHyHg//2KSa0a0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/triple-beam": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.5.tgz", + "integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==", + "license": "MIT" + }, + "node_modules/@types/uuid": { + "version": "9.0.8", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.8.tgz", + "integrity": "sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/ws": { + "version": "8.5.14", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.14.tgz", + "integrity": "sha512-bd/YFLW+URhBzMXurx7lWByOu+xzU9+kb3RboOteXYDfW+tr+JZa99OyNmPINEGB/ahzKrEuc8rcv4gnpJmxTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.62.0.tgz", + "integrity": "sha512-TiZzBSJja/LbhNPvk6yc0JrX9XqhQ0hdh6M2svYfsHGejaKFIAGd9MQ+ERIMzLGlN/kZoYIgdxFV0PuljTKXag==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.4.0", + "@typescript-eslint/scope-manager": "5.62.0", + "@typescript-eslint/type-utils": "5.62.0", + "@typescript-eslint/utils": "5.62.0", + "debug": "^4.3.4", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "natural-compare-lite": "^1.4.0", + "semver": "^7.3.7", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^5.0.0", + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.62.0.tgz", + "integrity": "sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/scope-manager": "5.62.0", + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/typescript-estree": "5.62.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.62.0.tgz", + "integrity": "sha512-VXuvVvZeQCQb5Zgf4HAxc04q5j+WrNAtNh9OwCsCgpKqESMTu3tF/jhZ3xG6T4NZwWl65Bg8KuS2uEvhSfLl0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/visitor-keys": "5.62.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.62.0.tgz", + "integrity": "sha512-xsSQreu+VnfbqQpW5vnCJdq1Z3Q0U31qiWmRhr98ONQmcp/yhiPJFPq8MXiJVLiksmOKSjIldZzkebzHuCGzew==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/typescript-estree": "5.62.0", + "@typescript-eslint/utils": "5.62.0", + "debug": "^4.3.4", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/types": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.62.0.tgz", + "integrity": "sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.62.0.tgz", + "integrity": "sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/visitor-keys": "5.62.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "semver": "^7.3.7", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/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/@typescript-eslint/typescript-estree/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/@typescript-eslint/utils": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.62.0.tgz", + "integrity": "sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@types/json-schema": "^7.0.9", + "@types/semver": "^7.3.12", + "@typescript-eslint/scope-manager": "5.62.0", + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/typescript-estree": "5.62.0", + "eslint-scope": "^5.1.1", + "semver": "^7.3.7" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.62.0.tgz", + "integrity": "sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "5.62.0", + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", + "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", + "dev": true, + "license": "ISC" + }, + "node_modules/@webassemblyjs/ast": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.1.tgz", + "integrity": "sha512-ukBh14qFLjxTQNTXocdyksN5QdM28S1CxHt2rdskFyL+xFV7VremuBLVbmCePj+URalXBENx/9Lm7lnhihtCSw==", + "license": "MIT", + "dependencies": { + "@webassemblyjs/helper-numbers": "1.11.1", + "@webassemblyjs/helper-wasm-bytecode": "1.11.1" + } + }, + "node_modules/@webassemblyjs/floating-point-hex-parser": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.1.tgz", + "integrity": "sha512-iGRfyc5Bq+NnNuX8b5hwBrRjzf0ocrJPI6GWFodBFzmFnyvrQ83SHKhmilCU/8Jv67i4GJZBMhEzltxzcNagtQ==", + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-api-error": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.1.tgz", + "integrity": "sha512-RlhS8CBCXfRUR/cwo2ho9bkheSXG0+NwooXcc3PAILALf2QLdFyj7KGsKRbVc95hZnhnERon4kW/D3SZpp6Tcg==", + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-buffer": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.1.tgz", + "integrity": "sha512-gwikF65aDNeeXa8JxXa2BAk+REjSyhrNC9ZwdT0f8jc4dQQeDQ7G4m0f2QCLPJiMTTO6wfDmRmj/pW0PsUvIcA==", + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-numbers": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.1.tgz", + "integrity": "sha512-vDkbxiB8zfnPdNK9Rajcey5C0w+QJugEglN0of+kmO8l7lDb77AnlKYQF7aarZuCrv+l0UvqL+68gSDr3k9LPQ==", + "license": "MIT", + "dependencies": { + "@webassemblyjs/floating-point-hex-parser": "1.11.1", + "@webassemblyjs/helper-api-error": "1.11.1", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/helper-wasm-bytecode": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.1.tgz", + "integrity": "sha512-PvpoOGiJwXeTrSf/qfudJhwlvDQxFgelbMqtq52WWiXC6Xgg1IREdngmPN3bs4RoO83PnL/nFrxucXj1+BX62Q==", + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-wasm-section": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.1.tgz", + "integrity": "sha512-10P9No29rYX1j7F3EVPX3JvGPQPae+AomuSTPiF9eBQeChHI6iqjMIwR9JmOJXwpnn/oVGDk7I5IlskuMwU/pg==", + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.11.1", + "@webassemblyjs/helper-buffer": "1.11.1", + "@webassemblyjs/helper-wasm-bytecode": "1.11.1", + "@webassemblyjs/wasm-gen": "1.11.1" + } + }, + "node_modules/@webassemblyjs/ieee754": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.11.1.tgz", + "integrity": "sha512-hJ87QIPtAMKbFq6CGTkZYJivEwZDbQUgYd3qKSadTNOhVY7p+gfP6Sr0lLRVTaG1JjFj+r3YchoqRYxNH3M0GQ==", + "license": "MIT", + "dependencies": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "node_modules/@webassemblyjs/leb128": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.11.1.tgz", + "integrity": "sha512-BJ2P0hNZ0u+Th1YZXJpzW6miwqQUGcIHT1G/sf72gLVD9DZ5AdYTqPNbHZh6K1M5VmKvFXwGSWZADz+qBWxeRw==", + "license": "Apache-2.0", + "dependencies": { + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/utf8": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.11.1.tgz", + "integrity": "sha512-9kqcxAEdMhiwQkHpkNiorZzqpGrodQQ2IGrHHxCy+Ozng0ofyMA0lTqiLkVs1uzTRejX+/O0EOT7KxqVPuXosQ==", + "license": "MIT" + }, + "node_modules/@webassemblyjs/wasm-edit": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.1.tgz", + "integrity": "sha512-g+RsupUC1aTHfR8CDgnsVRVZFJqdkFHpsHMfJuWQzWU3tvnLC07UqHICfP+4XyL2tnr1amvl1Sdp06TnYCmVkA==", + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.11.1", + "@webassemblyjs/helper-buffer": "1.11.1", + "@webassemblyjs/helper-wasm-bytecode": "1.11.1", + "@webassemblyjs/helper-wasm-section": "1.11.1", + "@webassemblyjs/wasm-gen": "1.11.1", + "@webassemblyjs/wasm-opt": "1.11.1", + "@webassemblyjs/wasm-parser": "1.11.1", + "@webassemblyjs/wast-printer": "1.11.1" + } + }, + "node_modules/@webassemblyjs/wasm-gen": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.1.tgz", + "integrity": "sha512-F7QqKXwwNlMmsulj6+O7r4mmtAlCWfO/0HdgOxSklZfQcDu0TpLiD1mRt/zF25Bk59FIjEuGAIyn5ei4yMfLhA==", + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.11.1", + "@webassemblyjs/helper-wasm-bytecode": "1.11.1", + "@webassemblyjs/ieee754": "1.11.1", + "@webassemblyjs/leb128": "1.11.1", + "@webassemblyjs/utf8": "1.11.1" + } + }, + "node_modules/@webassemblyjs/wasm-opt": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.1.tgz", + "integrity": "sha512-VqnkNqnZlU5EB64pp1l7hdm3hmQw7Vgqa0KF/KCNO9sIpI6Fk6brDEiX+iCOYrvMuBWDws0NkTOxYEb85XQHHw==", + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.11.1", + "@webassemblyjs/helper-buffer": "1.11.1", + "@webassemblyjs/wasm-gen": "1.11.1", + "@webassemblyjs/wasm-parser": "1.11.1" + } + }, + "node_modules/@webassemblyjs/wasm-parser": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.1.tgz", + "integrity": "sha512-rrBujw+dJu32gYB7/Lup6UhdkPx9S9SnobZzRVL7VcBH9Bt9bCBLEuX/YXOOtBsOZ4NQrRykKhffRWHvigQvOA==", + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.11.1", + "@webassemblyjs/helper-api-error": "1.11.1", + "@webassemblyjs/helper-wasm-bytecode": "1.11.1", + "@webassemblyjs/ieee754": "1.11.1", + "@webassemblyjs/leb128": "1.11.1", + "@webassemblyjs/utf8": "1.11.1" + } + }, + "node_modules/@webassemblyjs/wast-printer": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.11.1.tgz", + "integrity": "sha512-IQboUWM4eKzWW+N/jij2sRatKMh99QEelo3Eb2q0qXkvPRISAj8Qxtmw5itwqK+TTkBuUIE45AxYPToqPtL5gg==", + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.11.1", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@xtuc/ieee754": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", + "license": "BSD-3-Clause" + }, + "node_modules/@xtuc/long": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", + "license": "Apache-2.0" + }, + "node_modules/@yarnpkg/lockfile": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz", + "integrity": "sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/abab": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", + "integrity": "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==", + "deprecated": "Use your platform's native atob() and btoa() methods instead", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "dev": true, + "license": "ISC" + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/accepts/node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.14.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", + "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-import-assertions": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.9.0.tgz", + "integrity": "sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA==", + "deprecated": "package has been renamed to acorn-import-attributes", + "license": "MIT", + "peerDependencies": { + "acorn": "^8" + } + }, + "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/adjust-sourcemap-loader": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/adjust-sourcemap-loader/-/adjust-sourcemap-loader-4.0.0.tgz", + "integrity": "sha512-OXwN5b9pCUXNQHJpwwD2qP40byEmSgzj8B4ydSN0uMNYWiFmJ6x6KwUllMmfk8Rwu/HJDFR7U8ubsWBoN0Xp0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "loader-utils": "^2.0.0", + "regex-parser": "^2.2.11" + }, + "engines": { + "node": ">=8.9" + } + }, + "node_modules/adjust-sourcemap-loader/node_modules/loader-utils": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", + "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" + }, + "engines": { + "node": ">=8.9.0" + } + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/agentkeepalive": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz", + "integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "humanize-ms": "^1.2.1" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/aggregate-error": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", + "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ajv": { + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", + "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/ansi-colors": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "license": "MIT", + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-html-community": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/ansi-html-community/-/ansi-html-community-0.0.8.tgz", + "integrity": "sha512-1APHAyr3+PCamwNw3bXCPp4HFLONZt/yIH0sZp0/469KWNTEy+qN5jQ3GVX6DMZ1UXAi34yVwtTeaG/HpBuuzw==", + "dev": true, + "engines": [ + "node >= 0.8.0" + ], + "license": "Apache-2.0", + "bin": { + "ansi-html": "bin/ansi-html" + } + }, + "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==", + "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==", + "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/aproba": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", + "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/are-we-there-yet": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-3.0.1.tgz", + "integrity": "sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg==", + "deprecated": "This package is no longer supported.", + "dev": true, + "license": "ISC", + "dependencies": { + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "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": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "dev": true, + "license": "MIT" + }, + "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/arrify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", + "integrity": "sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "license": "MIT" + }, + "node_modules/autoprefixer": { + "version": "10.4.21", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.21.tgz", + "integrity": "sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ==", + "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.24.4", + "caniuse-lite": "^1.0.30001702", + "fraction.js": "^4.3.7", + "normalize-range": "^0.1.2", + "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/autoprefixer/node_modules/browserslist": { + "version": "4.24.4", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.4.tgz", + "integrity": "sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==", + "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": { + "caniuse-lite": "^1.0.30001688", + "electron-to-chromium": "^1.5.73", + "node-releases": "^2.0.19", + "update-browserslist-db": "^1.1.1" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/babel-loader": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-9.1.2.tgz", + "integrity": "sha512-mN14niXW43tddohGl8HPu5yfQq70iUThvFL/4QzESA7GcZoC0eVOhvWdQ8+3UlSjaDE9MVtsW9mxDY07W7VpVA==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-cache-dir": "^3.3.2", + "schema-utils": "^4.0.0" + }, + "engines": { + "node": ">= 14.15.0" + }, + "peerDependencies": { + "@babel/core": "^7.12.0", + "webpack": ">=5" + } + }, + "node_modules/babel-plugin-istanbul": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", + "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-instrument": "^5.0.4", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-polyfill-corejs2": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.3.3.tgz", + "integrity": "sha512-8hOdmFYFSZhqg2C/JgLUQ+t52o5nirNwaWM2B9LWteozwIvM14VSwdsCAUET10qT+kmySAlseadmfeeSWFCy+Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.17.7", + "@babel/helper-define-polyfill-provider": "^0.3.3", + "semver": "^6.1.1" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/babel-plugin-polyfill-corejs2/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-plugin-polyfill-corejs3": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.6.0.tgz", + "integrity": "sha512-+eHqR6OPcBhJOGgsIar7xoAB1GcSwVUA3XjAd7HJNzOXT4wv6/H7KIdA/Nc60cvUlDbKApmqNvD1B1bzOt4nyA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.3.3", + "core-js-compat": "^3.25.1" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/babel-plugin-polyfill-regenerator": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.4.1.tgz", + "integrity": "sha512-NtQGmyQDXjQqQ+IzRkBVwEOz9lQ4zxAQZgoAYEtU9dJjnl1Oc98qnN7jcp+bE7O7aYzVpavXE3/VKXNzUbh7aw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.3.3" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "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==", + "license": "MIT" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "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/base64id": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", + "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^4.5.0 || >= 5.9" + } + }, + "node_modules/batch": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz", + "integrity": "sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw==", + "dev": true, + "license": "MIT" + }, + "node_modules/big.js": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", + "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "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/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/body-parser": { + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", + "dev": true, + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.13.0", + "raw-body": "2.5.2", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/body-parser/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/body-parser/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "license": "MIT" + }, + "node_modules/bonjour-service": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/bonjour-service/-/bonjour-service-1.3.0.tgz", + "integrity": "sha512-3YuAUiSkWykd+2Azjgyxei8OWf8thdn8AITIog2M4UICzoqfjlqr64WIjEXZllf/W6vK1goqleSR6brGomxQqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "multicast-dns": "^7.2.5" + } + }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "dev": true, + "license": "ISC" + }, + "node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "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.21.5", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.5.tgz", + "integrity": "sha512-tUkiguQGW7S3IhB7N+c2MV/HZPSCPAAiYBZXLsBhFB/PCy6ZKKsZrmBayHV9fdGV/ARIfJ14NkxKzRDjvp7L6w==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + } + ], + "license": "MIT", + "dependencies": { + "caniuse-lite": "^1.0.30001449", + "electron-to-chromium": "^1.4.284", + "node-releases": "^2.0.8", + "update-browserslist-db": "^1.0.10" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "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": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "license": "MIT" + }, + "node_modules/builtins": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/builtins/-/builtins-5.1.0.tgz", + "integrity": "sha512-SW9lzGTLvWTP1AY8xeAMZimqDrIaSdLQUcVr9DMef51niJ022Ri87SwRRKYm4A6iHfkPaiVUu/Duw2Wc4J7kKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.0.0" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/cacache": { + "version": "17.0.4", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-17.0.4.tgz", + "integrity": "sha512-Z/nL3gU+zTUjz5pCA5vVjYM8pmaw2kxM7JEiE0fv3w77Wj+sFbi70CrBruUWH0uNcEdvLDixFpgA2JM4F4DBjA==", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/fs": "^3.1.0", + "fs-minipass": "^3.0.0", + "glob": "^8.0.1", + "lru-cache": "^7.7.1", + "minipass": "^4.0.0", + "minipass-collect": "^1.0.2", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "p-map": "^4.0.0", + "promise-inflight": "^1.0.1", + "ssri": "^10.0.0", + "tar": "^6.1.11", + "unique-filename": "^3.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/cacache/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.1.tgz", + "integrity": "sha512-BhYE+WDaywFg2TBWYNXAE+8B1ATnThNBqXHP5nQu0jWJdVvY2hvkpyB3qOmtmDePiS5/BDQ8wASEWGMWRG148g==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.3.tgz", + "integrity": "sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "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/camelcase-keys": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-6.2.2.tgz", + "integrity": "sha512-YrwaA0vEKazPBkn0ipTiMpSajYDSe+KjQfrjhcBMxJt/znbvlHd8Pw/Vamaz5EB4Wfhs3SUR3Z9mwRu/P3s3Yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "camelcase": "^5.3.1", + "map-obj": "^4.0.0", + "quick-lru": "^4.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001707", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001707.tgz", + "integrity": "sha512-3qtRjw/HQSMlDWf+X79N206fepf4SOOU6SQLMaq/0KkZLmSjPxAkBOQQ+FxbHKfHmYLZFfdWsO3KA90ceHPSnw==", + "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/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "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/chardet": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", + "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", + "license": "MIT" + }, + "node_modules/chokidar": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "devOptional": true, + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ], + "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" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/chrome-trace-event": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", + "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", + "license": "MIT", + "engines": { + "node": ">=6.0" + } + }, + "node_modules/clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "license": "MIT", + "dependencies": { + "restore-cursor": "^3.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cli-spinners": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", + "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-width": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-3.0.0.tgz", + "integrity": "sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==", + "license": "ISC", + "engines": { + "node": ">= 10" + } + }, + "node_modules/clipboard": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/clipboard/-/clipboard-2.0.11.tgz", + "integrity": "sha512-C+0bbOqkezLIsmWSvlsXS0Q0bmkugu7jcfMIACB+RDEntIzQIkdr148we28AfSloQLRdZlYL/QYyrq05j/3Faw==", + "license": "MIT", + "dependencies": { + "good-listener": "^1.2.2", + "select": "^1.1.2", + "tiny-emitter": "^2.0.0" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/clone": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/clone-deep": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", + "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-plain-object": "^2.0.4", + "kind-of": "^6.0.2", + "shallow-clone": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/color": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/color/-/color-3.2.1.tgz", + "integrity": "sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==", + "license": "MIT", + "dependencies": { + "color-convert": "^1.9.3", + "color-string": "^1.6.0" + } + }, + "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==", + "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==", + "license": "MIT" + }, + "node_modules/color-string": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", + "license": "MIT", + "dependencies": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, + "node_modules/color-support": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", + "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", + "dev": true, + "license": "ISC", + "bin": { + "color-support": "bin.js" + } + }, + "node_modules/color/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "license": "MIT", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/color/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "license": "MIT" + }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/colorspace": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/colorspace/-/colorspace-1.1.4.tgz", + "integrity": "sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w==", + "license": "MIT", + "dependencies": { + "color": "^3.1.3", + "text-hex": "1.0.x" + } + }, + "node_modules/commander": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 12" + } + }, + "node_modules/commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", + "dev": true, + "license": "MIT" + }, + "node_modules/compressible": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", + "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": ">= 1.43.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/compression": { + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.7.5.tgz", + "integrity": "sha512-bQJ0YRck5ak3LgtnpKkiabX5pNF7tMUh1BSy2ZBOTh0Dim0BUu6aPPwByIns6/A5Prh8PufSPerMDUklpzes2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "compressible": "~2.0.18", + "debug": "2.6.9", + "negotiator": "~0.6.4", + "on-headers": "~1.0.2", + "safe-buffer": "5.2.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/compression/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/compression/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "license": "MIT" + }, + "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/connect": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/connect/-/connect-3.7.0.tgz", + "integrity": "sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "finalhandler": "1.1.2", + "parseurl": "~1.3.3", + "utils-merge": "1.0.1" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/connect-history-api-fallback": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-2.0.0.tgz", + "integrity": "sha512-U73+6lQFmfiNPrYbXqr6kZ1i1wiRqXnp2nhMsINseWXO8lDau0LGEffJ8kQi4EjLZympVgRdvqjAgiZ1tgzDDA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/connect/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/connect/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "license": "MIT" + }, + "node_modules/console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/copy-anything": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-2.0.6.tgz", + "integrity": "sha512-1j20GZTsvKNkc4BY3NpMOM8tt///wY3FpIzozTOFO2ffuZcV61nojHXVKIy3WM+7ADCy5FVhdZYHYDdgTU0yJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-what": "^3.14.1" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, + "node_modules/copy-webpack-plugin": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-11.0.0.tgz", + "integrity": "sha512-fX2MWpamkW0hZxMEg0+mYnA40LTosOSa5TqZ9GYIBzyJa9C3QUaMPSE2xAi/buNr8u89SfD9wHSQVBzrRa/SOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-glob": "^3.2.11", + "glob-parent": "^6.0.1", + "globby": "^13.1.1", + "normalize-path": "^3.0.0", + "schema-utils": "^4.0.0", + "serialize-javascript": "^6.0.0" + }, + "engines": { + "node": ">= 14.15.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + } + }, + "node_modules/copy-webpack-plugin/node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/core-js-compat": { + "version": "3.40.0", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.40.0.tgz", + "integrity": "sha512-0XEDpr5y5mijvw8Lbc6E5AkjrHfp7eEoPlu36SWeAbcL8fn1G1ANe8DBlo2XoNN89oVpxWwOjYIPVzR4ZvsKCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "browserslist": "^4.24.3" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "node_modules/core-js-compat/node_modules/browserslist": { + "version": "4.24.4", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.4.tgz", + "integrity": "sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==", + "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": { + "caniuse-lite": "^1.0.30001688", + "electron-to-chromium": "^1.5.73", + "node-releases": "^2.0.19", + "update-browserslist-db": "^1.1.1" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/cose-base": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/cose-base/-/cose-base-1.0.3.tgz", + "integrity": "sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg==", + "license": "MIT", + "optional": true, + "dependencies": { + "layout-base": "^1.0.0" + } + }, + "node_modules/cosmiconfig": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", + "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.2.1", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.10.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/critters": { + "version": "0.0.16", + "resolved": "https://registry.npmjs.org/critters/-/critters-0.0.16.tgz", + "integrity": "sha512-JwjgmO6i3y6RWtLYmXwO5jMd+maZt8Tnfu7VVISmEWyQqfLpB8soBswf8/2bu6SBXxtKA68Al3c+qIG1ApT68A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "chalk": "^4.1.0", + "css-select": "^4.2.0", + "parse5": "^6.0.1", + "parse5-htmlparser2-tree-adapter": "^6.0.1", + "postcss": "^8.3.7", + "pretty-bytes": "^5.3.0" + } + }, + "node_modules/critters/node_modules/parse5": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz", + "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==", + "dev": true, + "license": "MIT" + }, + "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/cross-spawn/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/css-loader": { + "version": "6.7.3", + "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-6.7.3.tgz", + "integrity": "sha512-qhOH1KlBMnZP8FzRO6YCH9UHXQhVMcEGLyNdb7Hv2cpcmJbW0YrddO+tG1ab5nT41KpHIYGsbeHqxB9xPu1pKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "icss-utils": "^5.1.0", + "postcss": "^8.4.19", + "postcss-modules-extract-imports": "^3.0.0", + "postcss-modules-local-by-default": "^4.0.0", + "postcss-modules-scope": "^3.0.0", + "postcss-modules-values": "^4.0.0", + "postcss-value-parser": "^4.2.0", + "semver": "^7.3.8" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" + } + }, + "node_modules/css-select": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-4.3.0.tgz", + "integrity": "sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.0.1", + "domhandler": "^4.3.1", + "domutils": "^2.8.0", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-what": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", + "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "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/custom-event": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/custom-event/-/custom-event-1.0.1.tgz", + "integrity": "sha512-GAj5FOq0Hd+RsCGVJxZuKaIDXDf3h6GQoNEjFgbLLI/trgtavwUbSnZ5pVfg27DVCaWjIohryS0JFwIJyT2cMg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cytoscape": { + "version": "3.31.0", + "resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.31.0.tgz", + "integrity": "sha512-zDGn1K/tfZwEnoGOcHc0H4XazqAAXAuDpcYw9mUnUjATjqljyCNGJv8uEvbvxGaGHaVshxMecyl6oc6uKzRfbw==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/cytoscape-cose-bilkent": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cytoscape-cose-bilkent/-/cytoscape-cose-bilkent-4.1.0.tgz", + "integrity": "sha512-wgQlVIUJF13Quxiv5e1gstZ08rnZj2XaLHGoFMYXz7SkNfCDOOteKBE6SYRfA9WxxI/iBc3ajfDoc6hb/MRAHQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "cose-base": "^1.0.0" + }, + "peerDependencies": { + "cytoscape": "^3.2.0" + } + }, + "node_modules/cytoscape-fcose": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cytoscape-fcose/-/cytoscape-fcose-2.2.0.tgz", + "integrity": "sha512-ki1/VuRIHFCzxWNrsshHYPs6L7TvLu3DL+TyIGEsRcvVERmxokbf5Gdk7mFxZnTdiGtnA4cfSmjZJMviqSuZrQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "cose-base": "^2.2.0" + }, + "peerDependencies": { + "cytoscape": "^3.2.0" + } + }, + "node_modules/cytoscape-fcose/node_modules/cose-base": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cose-base/-/cose-base-2.2.0.tgz", + "integrity": "sha512-AzlgcsCbUMymkADOJtQm3wO9S3ltPfYOFD5033keQn9NJzIbtnZj+UdBJe7DYml/8TdbtHJW3j58SOnKhWY/5g==", + "license": "MIT", + "optional": true, + "dependencies": { + "layout-base": "^2.0.0" + } + }, + "node_modules/cytoscape-fcose/node_modules/layout-base": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/layout-base/-/layout-base-2.0.1.tgz", + "integrity": "sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg==", + "license": "MIT", + "optional": true + }, + "node_modules/d3": { + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/d3/-/d3-7.9.0.tgz", + "integrity": "sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==", + "license": "ISC", + "optional": true, + "dependencies": { + "d3-array": "3", + "d3-axis": "3", + "d3-brush": "3", + "d3-chord": "3", + "d3-color": "3", + "d3-contour": "4", + "d3-delaunay": "6", + "d3-dispatch": "3", + "d3-drag": "3", + "d3-dsv": "3", + "d3-ease": "3", + "d3-fetch": "3", + "d3-force": "3", + "d3-format": "3", + "d3-geo": "3", + "d3-hierarchy": "3", + "d3-interpolate": "3", + "d3-path": "3", + "d3-polygon": "3", + "d3-quadtree": "3", + "d3-random": "3", + "d3-scale": "4", + "d3-scale-chromatic": "3", + "d3-selection": "3", + "d3-shape": "3", + "d3-time": "3", + "d3-time-format": "4", + "d3-timer": "3", + "d3-transition": "3", + "d3-zoom": "3" + }, + "engines": { + "node": ">=12" + } + }, + "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", + "optional": true, + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-axis": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-3.0.0.tgz", + "integrity": "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==", + "license": "ISC", + "optional": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-brush": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-3.0.0.tgz", + "integrity": "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==", + "license": "ISC", + "optional": true, + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "3", + "d3-transition": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-chord": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-chord/-/d3-chord-3.0.1.tgz", + "integrity": "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==", + "license": "ISC", + "optional": true, + "dependencies": { + "d3-path": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "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", + "optional": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-contour": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-contour/-/d3-contour-4.0.2.tgz", + "integrity": "sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==", + "license": "ISC", + "optional": true, + "dependencies": { + "d3-array": "^3.2.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-delaunay": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==", + "license": "ISC", + "optional": true, + "dependencies": { + "delaunator": "5" + }, + "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", + "optional": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-drag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", + "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", + "license": "ISC", + "optional": true, + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-selection": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dsv": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-3.0.1.tgz", + "integrity": "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==", + "license": "ISC", + "optional": true, + "dependencies": { + "commander": "7", + "iconv-lite": "0.6", + "rw": "1" + }, + "bin": { + "csv2json": "bin/dsv2json.js", + "csv2tsv": "bin/dsv2dsv.js", + "dsv2dsv": "bin/dsv2dsv.js", + "dsv2json": "bin/dsv2json.js", + "json2csv": "bin/json2dsv.js", + "json2dsv": "bin/json2dsv.js", + "json2tsv": "bin/json2dsv.js", + "tsv2csv": "bin/dsv2dsv.js", + "tsv2json": "bin/dsv2json.js" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dsv/node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/d3-dsv/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "optional": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", + "optional": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-fetch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-fetch/-/d3-fetch-3.0.1.tgz", + "integrity": "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==", + "license": "ISC", + "optional": true, + "dependencies": { + "d3-dsv": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-force": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-3.0.0.tgz", + "integrity": "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==", + "license": "ISC", + "optional": true, + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-quadtree": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz", + "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==", + "license": "ISC", + "optional": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-geo": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-3.1.1.tgz", + "integrity": "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==", + "license": "ISC", + "optional": true, + "dependencies": { + "d3-array": "2.5.0 - 3" + }, + "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", + "optional": true, + "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", + "optional": true, + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "license": "ISC", + "optional": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-polygon": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-3.0.1.tgz", + "integrity": "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==", + "license": "ISC", + "optional": true, + "engines": { + "node": ">=12" + } + }, + "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", + "optional": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-random": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-random/-/d3-random-3.0.1.tgz", + "integrity": "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==", + "license": "ISC", + "optional": true, + "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", + "optional": true, + "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-scale-chromatic": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", + "integrity": "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==", + "license": "ISC", + "optional": true, + "dependencies": { + "d3-color": "1 - 3", + "d3-interpolate": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-selection": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", + "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", + "license": "ISC", + "optional": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "license": "ISC", + "optional": true, + "dependencies": { + "d3-path": "^3.1.0" + }, + "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", + "optional": true, + "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", + "optional": true, + "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", + "optional": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-transition": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", + "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", + "license": "ISC", + "optional": true, + "dependencies": { + "d3-color": "1 - 3", + "d3-dispatch": "1 - 3", + "d3-ease": "1 - 3", + "d3-interpolate": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "d3-selection": "2 - 3" + } + }, + "node_modules/d3-zoom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz", + "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", + "license": "ISC", + "optional": true, + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "2 - 3", + "d3-transition": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/dagre-d3-es": { + "version": "7.0.9", + "resolved": "https://registry.npmjs.org/dagre-d3-es/-/dagre-d3-es-7.0.9.tgz", + "integrity": "sha512-rYR4QfVmy+sR44IBDvVtcAmOReGBvRCWDpO2QjYwqgh9yijw6eSHBqaPG/LIOEy7aBsniLvtMW6pg19qJhq60w==", + "license": "MIT", + "optional": true, + "dependencies": { + "d3": "^7.8.2", + "lodash-es": "^4.17.21" + } + }, + "node_modules/date-format": { + "version": "4.0.14", + "resolved": "https://registry.npmjs.org/date-format/-/date-format-4.0.14.tgz", + "integrity": "sha512-39BOQLs9ZjKh0/patS9nrT8wc3ioX3/eA/zgbKNopnF2wCqJEoxywwwElATYvRsXdnOxA/OQeQoFZ3rFjVajhg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/dayjs": { + "version": "1.11.13", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz", + "integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==", + "license": "MIT", + "optional": true + }, + "node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/decamelize-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/decamelize-keys/-/decamelize-keys-1.1.1.tgz", + "integrity": "sha512-WiPxgEirIV0/eIOMcnFBA3/IJZAZqKnwAwWyvvdi4lsr1WCN22nhdf/3db3DoZcUjTV2SqfzIwNyp6y2xs3nmg==", + "dev": true, + "license": "MIT", + "dependencies": { + "decamelize": "^1.1.0", + "map-obj": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/decamelize-keys/node_modules/map-obj": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz", + "integrity": "sha512-7N/q3lyZ+LVCp7PzuxrJr4KMbBE2hW7BT7YNia330OFxIf4d3r5zVpicP2650l7CPN6RM9zOJRl3NGpqSiw3Eg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "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/default-gateway": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/default-gateway/-/default-gateway-6.0.3.tgz", + "integrity": "sha512-fwSOJsbbNzZ/CUFpqFBqYfYNLj1NbMPm8MMCIzHjC83iSJRBEGmDUxU+WP661BaBQImeC2yHwXtz+P/O9o+XEg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "execa": "^5.0.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/defaults": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", + "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==", + "license": "MIT", + "dependencies": { + "clone": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/define-lazy-prop": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", + "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/delaunator": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.0.1.tgz", + "integrity": "sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==", + "license": "ISC", + "optional": true, + "dependencies": { + "robust-predicates": "^3.0.2" + } + }, + "node_modules/delegate": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/delegate/-/delegate-3.2.0.tgz", + "integrity": "sha512-IofjkYBZaZivn0V8nnsMJGBr4jVLxHDheKSW88PyxS5QC4Vo9ZbZVvhzlSxY87fVq3STR6r+4cGepyHkcWOQSw==", + "license": "MIT" + }, + "node_modules/delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/dependency-graph": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/dependency-graph/-/dependency-graph-0.11.0.tgz", + "integrity": "sha512-JeMq7fEshyepOWDfcfHK06N3MhyPhz++vtqWhMT5O9A3K42rdsEDpfdVqjaqaAhsw6a+ZqeDvQVtD0hFHQWrzg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/detect-node": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", + "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", + "dev": true, + "license": "MIT" + }, + "node_modules/di": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/di/-/di-0.0.1.tgz", + "integrity": "sha512-uJaamHkagcZtHPqCIHZxnFrXlunQXgBOsZSUOWwFw31QJCAbyTBoHMW75YOTur5ZNx8pIeAKgf6GWIgaqqiLhA==", + "dev": true, + "license": "MIT" + }, + "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/dns-packet": { + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-5.6.1.tgz", + "integrity": "sha512-l4gcSouhcgIKRvyy99RNVOgxXiicE+2jZoNmaNmZ6JXiGajBOJAesk1OBlJuM5k2c+eudGdLxDqXuPCKIj6kpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@leichtgewicht/ip-codec": "^2.0.1" + }, + "engines": { + "node": ">=6" + } + }, + "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/dom-serialize": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/dom-serialize/-/dom-serialize-2.2.1.tgz", + "integrity": "sha512-Yra4DbvoW7/Z6LBN560ZwXMjoNOSAN2wRsKFGc4iBeso+mpIA6qj1vfdf9HpMaKAqG6wXTy+1SYEzmNpKXOSsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "custom-event": "~1.0.0", + "ent": "~2.2.0", + "extend": "^3.0.0", + "void-elements": "^2.0.0" + } + }, + "node_modules/dom-serializer": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz", + "integrity": "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==", + "dev": true, + "license": "MIT", + "dependencies": { + "domelementtype": "^2.0.1", + "domhandler": "^4.2.0", + "entities": "^2.0.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, + "node_modules/domhandler": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz", + "integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.2.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/dompurify": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.4.3.tgz", + "integrity": "sha512-q6QaLcakcRjebxjg8/+NP+h0rPfatOgOzc46Fst9VAA3jF2ApfKBNKMzdP4DYTqtUMXSCd5pRS/8Po/OmoCHZQ==", + "license": "(MPL-2.0 OR Apache-2.0)", + "optional": true + }, + "node_modules/domutils": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", + "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^1.0.1", + "domelementtype": "^2.2.0", + "domhandler": "^4.2.0" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "license": "MIT" + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "dev": true, + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.90", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.90.tgz", + "integrity": "sha512-C3PN4aydfW91Natdyd449Kw+BzhLmof6tzy5W1pFC5SpQxVXT+oyiyOG9AgYYSN9OdA/ik3YkCrpwqI8ug5Tug==", + "license": "ISC" + }, + "node_modules/elkjs": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/elkjs/-/elkjs-0.8.2.tgz", + "integrity": "sha512-L6uRgvZTH+4OF5NE/MBbzQx/WYpru1xCBE9respNj6qznEewGUIfhzmm7horWWxbNO2M0WckQypGctR8lH79xQ==", + "license": "EPL-2.0", + "optional": true + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/emoji-toolkit": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/emoji-toolkit/-/emoji-toolkit-7.0.1.tgz", + "integrity": "sha512-l5aJyAhpC5s4mDuoVuqt4SzVjwIsIvakPh4ZGJJE4KWuWFCEHaXacQFkStVdD9zbRR+/BbRXob7u99o0lQFr8A==", + "license": "MIT", + "optional": true + }, + "node_modules/emojis-list": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", + "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/enabled": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz", + "integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/encoding": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", + "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", + "license": "MIT", + "optional": true, + "dependencies": { + "iconv-lite": "^0.6.2" + } + }, + "node_modules/encoding/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "optional": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/engine.io": { + "version": "6.6.4", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.4.tgz", + "integrity": "sha512-ZCkIjSYNDyGn0R6ewHDtXgns/Zre/NT6Agvq1/WobF7JXgFff4SeDroKiCO3fNJreU9YG429Sc81o4w5ok/W5g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/cors": "^2.8.12", + "@types/node": ">=10.0.0", + "accepts": "~1.3.4", + "base64id": "2.0.0", + "cookie": "~0.7.2", + "cors": "~2.8.5", + "debug": "~4.3.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.17.1" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/engine.io-parser": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", + "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/engine.io/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/enhanced-resolve": { + "version": "5.18.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.0.tgz", + "integrity": "sha512-0/r0MySGYG8YqlayBZ6MuCfECmHFdJ5qyPh8s8wa5Hnm6SaFLSK1VYCbj+NKp090Nm1caZhD+QTnmxO7esYGyQ==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/ent": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/ent/-/ent-2.2.2.tgz", + "integrity": "sha512-kKvD1tO6BM+oK9HzCPpUdRb4vKFQY/FPTFmurMvh6LlN68VMrdj77w8yp51/kDbpkFOS9J8w5W6zIzgM2H8/hw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "punycode": "^1.4.1", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/entities": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", + "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", + "dev": true, + "license": "BSD-2-Clause", + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/err-code": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", + "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", + "dev": true, + "license": "MIT" + }, + "node_modules/errno": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.8.tgz", + "integrity": "sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "prr": "~1.0.1" + }, + "bin": { + "errno": "cli.js" + } + }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-module-lexer": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-0.9.3.tgz", + "integrity": "sha512-1HQ2M2sPtxwnvOvT1ZClHyQDiggdNjURWpY2we6aMKCQiUVxTmVs2UYPLIrD84sS+kMdUwfBSylbJPwNnBrnHQ==", + "license": "MIT" + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.17.8", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.17.8.tgz", + "integrity": "sha512-g24ybC3fWhZddZK6R3uD2iF/RIPnRpwJAqLov6ouX3hMbY4+tKolP0VMF3zuIYCaXun+yHwS5IPQ91N2BT191g==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/android-arm": "0.17.8", + "@esbuild/android-arm64": "0.17.8", + "@esbuild/android-x64": "0.17.8", + "@esbuild/darwin-arm64": "0.17.8", + "@esbuild/darwin-x64": "0.17.8", + "@esbuild/freebsd-arm64": "0.17.8", + "@esbuild/freebsd-x64": "0.17.8", + "@esbuild/linux-arm": "0.17.8", + "@esbuild/linux-arm64": "0.17.8", + "@esbuild/linux-ia32": "0.17.8", + "@esbuild/linux-loong64": "0.17.8", + "@esbuild/linux-mips64el": "0.17.8", + "@esbuild/linux-ppc64": "0.17.8", + "@esbuild/linux-riscv64": "0.17.8", + "@esbuild/linux-s390x": "0.17.8", + "@esbuild/linux-x64": "0.17.8", + "@esbuild/netbsd-x64": "0.17.8", + "@esbuild/openbsd-x64": "0.17.8", + "@esbuild/sunos-x64": "0.17.8", + "@esbuild/win32-arm64": "0.17.8", + "@esbuild/win32-ia32": "0.17.8", + "@esbuild/win32-x64": "0.17.8" + } + }, + "node_modules/esbuild-wasm": { + "version": "0.17.8", + "resolved": "https://registry.npmjs.org/esbuild-wasm/-/esbuild-wasm-0.17.8.tgz", + "integrity": "sha512-zCmpxv95E0FuCmvdw1K836UHnj4EdiQnFfjTby35y3LAjRPtXMj3sbHDRHjbD8Mqg5lTwq3knacr/1qIFU51CQ==", + "dev": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "dev": true, + "license": "MIT" + }, + "node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/eslint": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz", + "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==", + "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.0", + "@humanwhocodes/config-array": "^0.11.14", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-config-prettier": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz", + "integrity": "sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==", + "dev": true, + "license": "MIT", + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-plugin-es": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-es/-/eslint-plugin-es-4.1.0.tgz", + "integrity": "sha512-GILhQTnjYE2WorX5Jyi5i4dz5ALWxBIdQECVQavL6s7cI76IZTDWleTHkxz/QT3kvcs2QlGHvKLYsSlPOlPXnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-utils": "^2.0.0", + "regexpp": "^3.0.0" + }, + "engines": { + "node": ">=8.10.0" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + }, + "peerDependencies": { + "eslint": ">=4.19.1" + } + }, + "node_modules/eslint-plugin-es/node_modules/eslint-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.1.0.tgz", + "integrity": "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^1.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + } + }, + "node_modules/eslint-plugin-es/node_modules/eslint-visitor-keys": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", + "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=4" + } + }, + "node_modules/eslint-plugin-n": { + "version": "15.7.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-n/-/eslint-plugin-n-15.7.0.tgz", + "integrity": "sha512-jDex9s7D/Qial8AGVIHq4W7NswpUD5DPDL2RH8Lzd9EloWUuvUkHfv4FRLMipH5q2UtyurorBkPeNi1wVWNh3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "builtins": "^5.0.1", + "eslint-plugin-es": "^4.1.0", + "eslint-utils": "^3.0.0", + "ignore": "^5.1.1", + "is-core-module": "^2.11.0", + "minimatch": "^3.1.2", + "resolve": "^1.22.1", + "semver": "^7.3.8" + }, + "engines": { + "node": ">=12.22.0" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-plugin-n/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint-plugin-n/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/eslint-plugin-prettier": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.1.3.tgz", + "integrity": "sha512-C9GCVAs4Eq7ZC/XFQHITLiHJxQngdtraXaM+LoUFoFp/lHNl2Zn8f3WQbe9HvTBBQ9YnKFB0/2Ajdqwo5D1EAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "prettier-linter-helpers": "^1.0.0", + "synckit": "^0.8.6" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint-plugin-prettier" + }, + "peerDependencies": { + "@types/eslint": ">=8.0.0", + "eslint": ">=8.0.0", + "eslint-config-prettier": "*", + "prettier": ">=3.0.0" + }, + "peerDependenciesMeta": { + "@types/eslint": { + "optional": true + }, + "eslint-config-prettier": { + "optional": true + } + } + }, + "node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/eslint-utils": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz", + "integrity": "sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^2.0.0" + }, + "engines": { + "node": "^10.0.0 || ^12.0.0 || >= 14.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + }, + "peerDependencies": { + "eslint": ">=5" + } + }, + "node_modules/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", + "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10" + } + }, + "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/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "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/eslint/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/eslint/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/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/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/eslint/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/eslint/node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/eslint/node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/eslint/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/eslint/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/eslint/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/eslint/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/eslint/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/eslint/node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "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/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esquery/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/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esrecurse/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "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/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/eventemitter-asyncresource": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/eventemitter-asyncresource/-/eventemitter-asyncresource-1.0.0.tgz", + "integrity": "sha512-39F7TBIV0G7gTelxwbEqnwhp90eqCPON1k0NwNfwhgKn4Co4ybUbj2pECcXT0B3ztRKZ7Pw1JujUUgmQJHcVAQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "dev": true, + "license": "MIT" + }, + "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/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/exponential-backoff": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.1.tgz", + "integrity": "sha512-dX7e/LHVJ6W3DE1MHWi9S1EYzDESENfLrYohG2G++ovZrYOkm4Knwa0mc1cn84xJOR4KEU0WSchhLbd0UklbHw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/express": { + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", + "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.3", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.7.1", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.3.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.12", + "proxy-addr": "~2.0.7", + "qs": "6.13.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.19.0", + "serve-static": "1.16.2", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express/node_modules/cookie": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/express/node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/express/node_modules/finalhandler": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/express/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "license": "MIT" + }, + "node_modules/express/node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "dev": true, + "license": "MIT" + }, + "node_modules/external-editor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", + "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", + "license": "MIT", + "dependencies": { + "chardet": "^0.7.0", + "iconv-lite": "^0.4.24", + "tmp": "^0.0.33" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-diff": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz", + "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", + "dev": true, + "license": "Apache-2.0" + }, + "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-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==", + "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.19.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.0.tgz", + "integrity": "sha512-7SFSRCNjBQIZH/xZR3iy5iQYR8aGBE0h3VG6/cwlbrpdciNYBMotQav8c1XI3HjHH+NikUpP53nPdlZSdWmFzA==", + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/faye-websocket": { + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz", + "integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==", + "license": "Apache-2.0", + "dependencies": { + "websocket-driver": ">=0.5.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/fecha": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz", + "integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==", + "license": "MIT" + }, + "node_modules/figures": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", + "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^1.0.5" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/file-loader": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/file-loader/-/file-loader-6.2.0.tgz", + "integrity": "sha512-qo3glqyTa61Ytg4u73GultjHGjdRyig3tG6lPtyX/jOEJvHif9uB0/OCI2Kif6ctF3caQTW2G5gym21oAsI4pw==", + "license": "MIT", + "dependencies": { + "loader-utils": "^2.0.0", + "schema-utils": "^3.0.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^4.0.0 || ^5.0.0" + } + }, + "node_modules/file-loader/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "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/file-loader/node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "license": "MIT", + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "node_modules/file-loader/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==", + "license": "MIT" + }, + "node_modules/file-loader/node_modules/loader-utils": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", + "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", + "license": "MIT", + "dependencies": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" + }, + "engines": { + "node": ">=8.9.0" + } + }, + "node_modules/file-loader/node_modules/schema-utils": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "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/finalhandler": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", + "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "statuses": "~1.5.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/finalhandler/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/finalhandler/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "license": "MIT" + }, + "node_modules/finalhandler/node_modules/on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==", + "dev": true, + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/find-cache-dir": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.2.tgz", + "integrity": "sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==", + "dev": true, + "license": "MIT", + "dependencies": { + "commondir": "^1.0.1", + "make-dir": "^3.0.2", + "pkg-dir": "^4.1.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/avajs/find-cache-dir?sponsor=1" + } + }, + "node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase": { + "version": "9.23.0", + "resolved": "https://registry.npmjs.org/firebase/-/firebase-9.23.0.tgz", + "integrity": "sha512-/4lUVY0lUvBDIaeY1q6dUYhS8Sd18Qb9CgWkPZICUo9IXpJNCEagfNZXBBFCkMTTN5L5gx2Hjr27y21a9NzUcA==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/analytics": "0.10.0", + "@firebase/analytics-compat": "0.2.6", + "@firebase/app": "0.9.13", + "@firebase/app-check": "0.8.0", + "@firebase/app-check-compat": "0.3.7", + "@firebase/app-compat": "0.2.13", + "@firebase/app-types": "0.9.0", + "@firebase/auth": "0.23.2", + "@firebase/auth-compat": "0.4.2", + "@firebase/database": "0.14.4", + "@firebase/database-compat": "0.3.4", + "@firebase/firestore": "3.13.0", + "@firebase/firestore-compat": "0.3.12", + "@firebase/functions": "0.10.0", + "@firebase/functions-compat": "0.3.5", + "@firebase/installations": "0.6.4", + "@firebase/installations-compat": "0.2.4", + "@firebase/messaging": "0.12.4", + "@firebase/messaging-compat": "0.2.4", + "@firebase/performance": "0.6.4", + "@firebase/performance-compat": "0.2.4", + "@firebase/remote-config": "0.4.4", + "@firebase/remote-config-compat": "0.2.4", + "@firebase/storage": "0.11.2", + "@firebase/storage-compat": "0.3.2", + "@firebase/util": "1.9.3" + } + }, + "node_modules/firebase-tools": { + "version": "11.30.0", + "resolved": "https://registry.npmjs.org/firebase-tools/-/firebase-tools-11.30.0.tgz", + "integrity": "sha512-rNMhtsFKy35AZqfy0obY2Kgsfmg8N1duEx2UDT14NsMHB7xt2Z7cUd92bkAoQAfD0jAOd7LhRT/7E6J5yB+kGw==", + "devOptional": true, + "hasShrinkwrap": true, + "license": "MIT", + "dependencies": { + "@google-cloud/pubsub": "^3.0.1", + "abort-controller": "^3.0.0", + "ajv": "^6.12.6", + "archiver": "^5.0.0", + "async-lock": "1.3.2", + "body-parser": "^1.19.0", + "chokidar": "^3.0.2", + "cjson": "^0.3.1", + "cli-table": "0.3.11", + "colorette": "^2.0.19", + "commander": "^4.0.1", + "configstore": "^5.0.1", + "cors": "^2.8.5", + "cross-env": "^5.1.3", + "cross-spawn": "^7.0.3", + "csv-parse": "^5.0.4", + "exegesis": "^4.1.0", + "exegesis-express": "^4.0.0", + "express": "^4.16.4", + "filesize": "^6.1.0", + "form-data": "^4.0.0", + "fs-extra": "^10.1.0", + "glob": "^7.1.2", + "google-auth-library": "^7.11.0", + "inquirer": "^8.2.0", + "js-yaml": "^3.13.1", + "jsonwebtoken": "^9.0.0", + "leven": "^3.1.0", + "libsodium-wrappers": "^0.7.10", + "lodash": "^4.17.21", + "marked": "^4.0.14", + "marked-terminal": "^5.1.1", + "mime": "^2.5.2", + "minimatch": "^3.0.4", + "morgan": "^1.10.0", + "node-fetch": "^2.6.7", + "open": "^6.3.0", + "ora": "^5.4.1", + "p-limit": "^3.0.1", + "portfinder": "^1.0.32", + "progress": "^2.0.3", + "proxy-agent": "^5.0.0", + "request": "^2.87.0", + "retry": "^0.13.1", + "rimraf": "^3.0.0", + "semver": "^5.7.1", + "stream-chain": "^2.2.4", + "stream-json": "^1.7.3", + "strip-ansi": "^6.0.1", + "superstatic": "^9.0.3", + "tar": "^6.1.11", + "tcp-port-used": "^1.0.2", + "tmp": "^0.2.1", + "triple-beam": "^1.3.0", + "universal-analytics": "^0.5.3", + "update-notifier-cjs": "^5.1.6", + "uuid": "^8.3.2", + "winston": "^3.0.0", + "winston-transport": "^4.4.0", + "ws": "^7.2.3" + }, + "bin": { + "firebase": "lib/bin/firebase.js" + }, + "engines": { + "node": "^14.18.0 || >=16.4.0" + } + }, + "node_modules/firebase-tools/node_modules/@apidevtools/json-schema-ref-parser": { + "version": "9.0.7", + "resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-9.0.7.tgz", + "integrity": "sha512-QdwOGF1+eeyFh+17v2Tz626WX0nucd1iKOm6JUTUvCZdbolblCOOQCxGrQPY0f7jEhn36PiAWqZnsC2r5vmUWg==", + "devOptional": true, + "dependencies": { + "@jsdevtools/ono": "^7.1.3", + "call-me-maybe": "^1.0.1", + "js-yaml": "^3.13.1" + } + }, + "node_modules/firebase-tools/node_modules/@babel/parser": { + "version": "7.21.4", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.21.4.tgz", + "integrity": "sha512-alVJj7k7zIxqBZ7BTRhz0IqJFxW1VJbm6N8JbcYhQ186df9ZBPbZBmWSqAMXwHGsCJdYks7z/voa3ibiS5bCIw==", + "devOptional": true, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/firebase-tools/node_modules/@colors/colors": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", + "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==", + "optional": true, + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/firebase-tools/node_modules/@gar/promisify": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz", + "integrity": "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==", + "optional": true + }, + "node_modules/firebase-tools/node_modules/@google-cloud/precise-date": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@google-cloud/precise-date/-/precise-date-2.0.3.tgz", + "integrity": "sha512-+SDJ3ZvGkF7hzo6BGa8ZqeK3F6Z4+S+KviC9oOK+XCs3tfMyJCh/4j93XIWINgMMDIh9BgEvlw4306VxlXIlYA==", + "devOptional": true, + "engines": { + "node": ">=10.4.0" + } + }, + "node_modules/firebase-tools/node_modules/@google-cloud/projectify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@google-cloud/projectify/-/projectify-2.0.1.tgz", + "integrity": "sha512-ZDG38U/Yy6Zr21LaR3BTiiLtpJl6RkPS/JwoRT453G+6Q1DhlV0waNf8Lfu+YVYGIIxgKnLayJRfYlFJfiI8iQ==", + "devOptional": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/firebase-tools/node_modules/@google-cloud/promisify": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@google-cloud/promisify/-/promisify-2.0.3.tgz", + "integrity": "sha512-d4VSA86eL/AFTe5xtyZX+ePUjE8dIFu2T8zmdeNBSa5/kNgXPCx/o/wbFNHAGLJdGnk1vddRuMESD9HbOC8irw==", + "devOptional": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/firebase-tools/node_modules/@google-cloud/pubsub": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@google-cloud/pubsub/-/pubsub-3.0.1.tgz", + "integrity": "sha512-dznNbRd/Y8J0C0xvdvCPi3B1msK/dj/Nya+NQZ2doUOLT6eoa261tBwk9umOQs5L5GKcdlqQKbBjrNjDYVbzQA==", + "devOptional": true, + "dependencies": { + "@google-cloud/paginator": "^4.0.0", + "@google-cloud/precise-date": "^2.0.0", + "@google-cloud/projectify": "^2.0.0", + "@google-cloud/promisify": "^2.0.0", + "@opentelemetry/api": "^1.0.0", + "@opentelemetry/semantic-conventions": "^1.0.0", + "@types/duplexify": "^3.6.0", + "@types/long": "^4.0.0", + "arrify": "^2.0.0", + "extend": "^3.0.2", + "google-auth-library": "^8.0.2", + "google-gax": "^3.0.1", + "is-stream-ended": "^0.1.4", + "lodash.snakecase": "^4.1.1", + "p-defer": "^3.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/firebase-tools/node_modules/@google-cloud/pubsub/node_modules/@google-cloud/paginator": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@google-cloud/paginator/-/paginator-4.0.0.tgz", + "integrity": "sha512-wNmCZl+2G2DmgT/VlF+AROf80SoaC/CwS8trwmjNaq26VRNK8yPbU5F/Vy+R9oDAGKWQU2k8+Op5H4kFJVXFaQ==", + "devOptional": true, + "dependencies": { + "arrify": "^2.0.0", + "extend": "^3.0.2" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/firebase-tools/node_modules/@google-cloud/pubsub/node_modules/gaxios": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-5.0.0.tgz", + "integrity": "sha512-VD/yc5ln6XU8Ch1hyYY6kRMBE0Yc2np3fPyeJeYHhrPs1i8rgnsApPMWyrugkl7LLoSqpOJVBWlQIa87OAvt8Q==", + "devOptional": true, + "dependencies": { + "abort-controller": "^3.0.0", + "extend": "^3.0.2", + "https-proxy-agent": "^5.0.0", + "is-stream": "^2.0.0", + "node-fetch": "^2.6.7" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/firebase-tools/node_modules/@google-cloud/pubsub/node_modules/gcp-metadata": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-5.0.0.tgz", + "integrity": "sha512-gfwuX3yA3nNsHSWUL4KG90UulNiq922Ukj3wLTrcnX33BB7PwB1o0ubR8KVvXu9nJH+P5w1j2SQSNNqto+H0DA==", + "devOptional": true, + "dependencies": { + "gaxios": "^5.0.0", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/firebase-tools/node_modules/@google-cloud/pubsub/node_modules/google-auth-library": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-8.0.2.tgz", + "integrity": "sha512-HoG+nWFAThLovKpvcbYzxgn+nBJPTfAwtq0GxPN821nOO+21+8oP7MoEHfd1sbDulUFFGfcjJr2CnJ4YssHcyg==", + "devOptional": true, + "dependencies": { + "arrify": "^2.0.0", + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "fast-text-encoding": "^1.0.0", + "gaxios": "^5.0.0", + "gcp-metadata": "^5.0.0", + "gtoken": "^5.3.2", + "jws": "^4.0.0", + "lru-cache": "^6.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/firebase-tools/node_modules/@google-cloud/pubsub/node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "devOptional": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/firebase-tools/node_modules/@google-cloud/pubsub/node_modules/jwa": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz", + "integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==", + "devOptional": true, + "dependencies": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/firebase-tools/node_modules/@google-cloud/pubsub/node_modules/jws": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", + "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", + "devOptional": true, + "dependencies": { + "jwa": "^2.0.0", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/firebase-tools/node_modules/@grpc/grpc-js": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.7.3.tgz", + "integrity": "sha512-H9l79u4kJ2PVSxUNA08HMYAnUBLj9v6KjYQ7SQ71hOZcEXhShE/y5iQCesP8+6/Ik/7i2O0a10bPquIcYfufog==", + "devOptional": true, + "dependencies": { + "@grpc/proto-loader": "^0.7.0", + "@types/node": ">=12.12.47" + }, + "engines": { + "node": "^8.13.0 || >=10.10.0" + } + }, + "node_modules/firebase-tools/node_modules/@grpc/grpc-js/node_modules/@grpc/proto-loader": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.4.tgz", + "integrity": "sha512-MnWjkGwqQ3W8fx94/c1CwqLsNmHHv2t0CFn+9++6+cDphC1lolpg9M2OU0iebIjK//pBNX9e94ho+gjx6vz39w==", + "devOptional": true, + "dependencies": { + "@types/long": "^4.0.1", + "lodash.camelcase": "^4.3.0", + "long": "^4.0.0", + "protobufjs": "^7.0.0", + "yargs": "^16.2.0" + }, + "bin": { + "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/firebase-tools/node_modules/@grpc/grpc-js/node_modules/protobufjs": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.1.2.tgz", + "integrity": "sha512-4ZPTPkXCdel3+L81yw3dG6+Kq3umdWKh7Dc7GW/CpNk4SX3hK58iPCWeCyhVTDrbkNeKrYNZ7EojM5WDaEWTLQ==", + "devOptional": true, + "hasInstallScript": true, + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/node": ">=13.7.0", + "long": "^5.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/firebase-tools/node_modules/@grpc/grpc-js/node_modules/protobufjs/node_modules/long": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/long/-/long-5.2.1.tgz", + "integrity": "sha512-GKSNGeNAtw8IryjjkhZxuKB3JzlcLTwjtiQCHKvqQet81I93kXslhDQruGI/QsddO83mcDToBVy7GqGS/zYf/A==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/@jsdevtools/ono": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz", + "integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/@jsdoc/salty": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@jsdoc/salty/-/salty-0.2.3.tgz", + "integrity": "sha512-bbtCxCkxcnWhi50I+4Lj6mdz9w3pOXOgEQrID8TCZ/DF51fW7M9GCQW2y45SpBDdHd1Eirm1X/Cf6CkAAe8HPg==", + "devOptional": true, + "dependencies": { + "lodash": "^4.17.21" + }, + "engines": { + "node": ">=v12.0.0" + } + }, + "node_modules/firebase-tools/node_modules/@npmcli/fs": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-2.1.2.tgz", + "integrity": "sha512-yOJKRvohFOaLqipNtwYB9WugyZKhC/DZC4VYPmpaCzDBrA8YpK3qHZ8/HGscMnE4GqbkLNuVcCnxkeQEdGt6LQ==", + "optional": true, + "dependencies": { + "@gar/promisify": "^1.1.3", + "semver": "^7.3.5" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/firebase-tools/node_modules/@npmcli/fs/node_modules/semver": { + "version": "7.3.7", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz", + "integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==", + "optional": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/firebase-tools/node_modules/@npmcli/move-file": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@npmcli/move-file/-/move-file-2.0.1.tgz", + "integrity": "sha512-mJd2Z5TjYWq/ttPLLGqArdtnC74J6bOzg4rMDnN+p1xTacZ2yPRCk2y0oSWQtygLR9YVQXgOcONrwtnk3JupxQ==", + "optional": true, + "dependencies": { + "mkdirp": "^1.0.4", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/firebase-tools/node_modules/@npmcli/move-file/node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "optional": true, + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/firebase-tools/node_modules/@opentelemetry/api": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.0.4.tgz", + "integrity": "sha512-BuJuXRSJNQ3QoKA6GWWDyuLpOUck+9hAXNMCnrloc1aWVoy6Xq6t9PUV08aBZ4Lutqq2LEHM486bpZqoViScog==", + "devOptional": true, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/firebase-tools/node_modules/@opentelemetry/semantic-conventions": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.2.0.tgz", + "integrity": "sha512-BNKB9fiYVghALJzCuWO3eNYfdTExPVK4ykrtmfNfy0A6UWYhOYjGMXifUmkunDJNL8ju9tBobo8jF0WR9zGy1Q==", + "devOptional": true, + "engines": { + "node": ">=8.12.0" + } + }, + "node_modules/firebase-tools/node_modules/@pnpm/network.ca-file": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@pnpm/network.ca-file/-/network.ca-file-1.0.2.tgz", + "integrity": "sha512-YcPQ8a0jwYU9bTdJDpXjMi7Brhkr1mXsXrUJvjqM2mQDgkRiz8jFaQGOdaLxgjtUfQgZhKy/O3cG/YwmgKaxLA==", + "devOptional": true, + "dependencies": { + "graceful-fs": "4.2.10" + }, + "engines": { + "node": ">=12.22.0" + } + }, + "node_modules/firebase-tools/node_modules/@pnpm/npm-conf": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@pnpm/npm-conf/-/npm-conf-1.0.5.tgz", + "integrity": "sha512-hD8ml183638O3R6/Txrh0L8VzGOrFXgRtRDG4qQC4tONdZ5Z1M+tlUUDUvrjYdmK6G+JTBTeaCLMna11cXzi8A==", + "devOptional": true, + "dependencies": { + "@pnpm/network.ca-file": "^1.0.1", + "config-chain": "^1.1.11" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/firebase-tools/node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha1-m4sMxmPWaafY9vXQiToU00jzD78=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/@protobufjs/codegen": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/@protobufjs/eventemitter": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha1-NVy8mLr61ZePntCV85diHx0Ga3A=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/@protobufjs/fetch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", + "integrity": "sha1-upn7WYYUr2VwDBYZ/wbUVLDYTEU=", + "devOptional": true, + "dependencies": { + "@protobufjs/aspromise": "^1.1.1", + "@protobufjs/inquire": "^1.1.0" + } + }, + "node_modules/firebase-tools/node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha1-Xp4avctz/Ap8uLKR33jIy9l7h9E=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/@protobufjs/inquire": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", + "integrity": "sha1-/yAOPnzyQp4tyvwRQIKOjMY48Ik=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha1-bMKyDFya1q0NzP0hynZz2Nf79o0=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha1-Cf0V8tbTq/qbZbw2ZQbWrXhG/1Q=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/@protobufjs/utf8": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", + "integrity": "sha1-p3c2C1s5oaLlEG+OhY8v0tBgxXA=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/@tootallnate/once": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", + "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==", + "devOptional": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/firebase-tools/node_modules/@types/duplexify": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/@types/duplexify/-/duplexify-3.6.0.tgz", + "integrity": "sha512-5zOA53RUlzN74bvrSGwjudssD9F3a797sDZQkiYpUOxW+WHaXTCPz4/d5Dgi6FKnOqZ2CpaTo0DhgIfsXAOE/A==", + "devOptional": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/firebase-tools/node_modules/@types/linkify-it": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-3.0.2.tgz", + "integrity": "sha512-HZQYqbiFVWufzCwexrvh694SOim8z2d+xJl5UNamcvQFejLY/2YUtzXHYi3cHdI7PMlS8ejH2slRAOJQ32aNbA==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/@types/long": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.1.tgz", + "integrity": "sha512-5tXH6Bx/kNGd3MgffdmP4dy2Z+G4eaXw0SE81Tq3BNadtnMR5/ySMzX4SLEzHJzSmPNn4HIdpQsBvXMUykr58w==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/@types/markdown-it": { + "version": "12.2.3", + "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-12.2.3.tgz", + "integrity": "sha512-GKMHFfv3458yYy+v/N8gjufHO6MSZKCOXpZc5GXIWWy8uldwfmPn98vp81gZ5f9SVw8YYBctgfJ22a2d7AOMeQ==", + "devOptional": true, + "dependencies": { + "@types/linkify-it": "*", + "@types/mdurl": "*" + } + }, + "node_modules/firebase-tools/node_modules/@types/mdurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-1.0.2.tgz", + "integrity": "sha512-eC4U9MlIcu2q0KQmXszyn5Akca/0jrQmwDRgpAMJai7qBWq4amIQhZyNau4VYGtCeALvW1/NtjzJJ567aZxfKA==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/@types/node": { + "version": "14.18.36", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.36.tgz", + "integrity": "sha512-FXKWbsJ6a1hIrRxv+FoukuHnGTgEzKYGi7kilfMae96AL9UNkPFNWJEEYWzdRI9ooIkbr4AKldyuSTLql06vLQ==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "optional": true + }, + "node_modules/firebase-tools/node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "devOptional": true, + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, + "node_modules/firebase-tools/node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "devOptional": true, + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/firebase-tools/node_modules/acorn": { + "version": "8.8.2", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz", + "integrity": "sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==", + "devOptional": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/firebase-tools/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==", + "devOptional": true, + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/firebase-tools/node_modules/acorn-walk": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz", + "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==", + "devOptional": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/firebase-tools/node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "devOptional": true, + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/firebase-tools/node_modules/agent-base/node_modules/debug": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "devOptional": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/firebase-tools/node_modules/agent-base/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/agentkeepalive": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.2.1.tgz", + "integrity": "sha512-Zn4cw2NEqd+9fiSVWMscnjyQ1a8Yfoc5oBajLeo5w+YBHgDUcEBY2hS4YpTz6iN5f/2zQiktcuM6tS8x1p9dpA==", + "optional": true, + "dependencies": { + "debug": "^4.1.0", + "depd": "^1.1.2", + "humanize-ms": "^1.2.1" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/firebase-tools/node_modules/agentkeepalive/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "optional": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/firebase-tools/node_modules/agentkeepalive/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "optional": true + }, + "node_modules/firebase-tools/node_modules/aggregate-error": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", + "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "optional": true, + "dependencies": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "devOptional": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/firebase-tools/node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "devOptional": true, + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/firebase-tools/node_modules/ajv-formats/node_modules/ajv": { + "version": "8.8.2", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.8.2.tgz", + "integrity": "sha512-x9VuX+R/jcFj1DHo/fCp99esgGDWiHENrKxaCENuCxpoMCmAt/COCGVDwA7kleEpEzJjDnvh3yGoOuLu0Dtllw==", + "devOptional": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/firebase-tools/node_modules/ajv-formats/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/ansi-align": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.1.tgz", + "integrity": "sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==", + "devOptional": true, + "dependencies": { + "string-width": "^4.1.0" + } + }, + "node_modules/firebase-tools/node_modules/ansi-escapes": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-5.0.0.tgz", + "integrity": "sha512-5GFMVX8HqE/TB+FuBJGuO5XG0WrsA6ptUqoODaT/n9mmUaZFkqnBueB4leqGBCmrUHnCnC4PCZTCd0E7QQ83bA==", + "devOptional": true, + "dependencies": { + "type-fest": "^1.0.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/firebase-tools/node_modules/ansi-escapes/node_modules/type-fest": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-1.4.0.tgz", + "integrity": "sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==", + "devOptional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/firebase-tools/node_modules/ansicolors": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/ansicolors/-/ansicolors-0.3.2.tgz", + "integrity": "sha1-ZlWX3oap/+Oqm/vmyuXG6kJrSXk=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/anymatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", + "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==", + "devOptional": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/firebase-tools/node_modules/aproba": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", + "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==", + "optional": true + }, + "node_modules/firebase-tools/node_modules/archiver": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/archiver/-/archiver-5.3.1.tgz", + "integrity": "sha512-8KyabkmbYrH+9ibcTScQ1xCJC/CGcugdVIwB+53f5sZziXgwUh3iXlAlANMxcZyDEfTHMe6+Z5FofV8nopXP7w==", + "devOptional": true, + "dependencies": { + "archiver-utils": "^2.1.0", + "async": "^3.2.3", + "buffer-crc32": "^0.2.1", + "readable-stream": "^3.6.0", + "readdir-glob": "^1.0.0", + "tar-stream": "^2.2.0", + "zip-stream": "^4.1.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/firebase-tools/node_modules/archiver-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-2.1.0.tgz", + "integrity": "sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw==", + "devOptional": true, + "dependencies": { + "glob": "^7.1.4", + "graceful-fs": "^4.2.0", + "lazystream": "^1.0.0", + "lodash.defaults": "^4.2.0", + "lodash.difference": "^4.5.0", + "lodash.flatten": "^4.4.0", + "lodash.isplainobject": "^4.0.6", + "lodash.union": "^4.6.0", + "normalize-path": "^3.0.0", + "readable-stream": "^2.0.0" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/firebase-tools/node_modules/archiver-utils/node_modules/readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "devOptional": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/firebase-tools/node_modules/archiver/node_modules/async": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.3.tgz", + "integrity": "sha512-spZRyzKL5l5BZQrr/6m/SqFdBN0q3OCI0f9rjfBzCMBIP4p75P620rR3gTmaksNOhmzgdxcaxdNfMy6anrbM0g==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/are-we-there-yet": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-3.0.1.tgz", + "integrity": "sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg==", + "optional": true, + "dependencies": { + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/firebase-tools/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "devOptional": true, + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/firebase-tools/node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/arrify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-2.0.1.tgz", + "integrity": "sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==", + "devOptional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/as-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/as-array/-/as-array-2.0.0.tgz", + "integrity": "sha1-TwSAXYf4/OjlEbwhCPjl46KH1Uc=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/asn1": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", + "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", + "devOptional": true, + "dependencies": { + "safer-buffer": "~2.1.0" + } + }, + "node_modules/firebase-tools/node_modules/assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", + "devOptional": true, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/firebase-tools/node_modules/ast-types": { + "version": "0.13.4", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz", + "integrity": "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==", + "devOptional": true, + "dependencies": { + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/firebase-tools/node_modules/ast-types/node_modules/tslib": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz", + "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/async": { + "version": "2.6.4", + "resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz", + "integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==", + "devOptional": true, + "dependencies": { + "lodash": "^4.17.14" + } + }, + "node_modules/firebase-tools/node_modules/async-lock": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/async-lock/-/async-lock-1.3.2.tgz", + "integrity": "sha512-phnXdS3RP7PPcmP6NWWzWMU0sLTeyvtZCxBPpZdkYE3seGLKSQZs9FrmVO/qwypq98FUtWWUEYxziLkdGk5nnA==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/aws-sign2": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", + "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=", + "devOptional": true, + "engines": { + "node": "*" + } + }, + "node_modules/firebase-tools/node_modules/aws4": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz", + "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "devOptional": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/firebase-tools/node_modules/basic-auth": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", + "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==", + "devOptional": true, + "dependencies": { + "safe-buffer": "5.1.2" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/firebase-tools/node_modules/basic-auth-connect": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/basic-auth-connect/-/basic-auth-connect-1.0.0.tgz", + "integrity": "sha1-/bC0OWLKe0BFanwrtI/hc9otISI=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", + "devOptional": true, + "dependencies": { + "tweetnacl": "^0.14.3" + } + }, + "node_modules/firebase-tools/node_modules/bignumber.js": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.0.2.tgz", + "integrity": "sha512-GAcQvbpsM0pUb0zw1EI0KhQEZ+lRwR5fYaAp3vPOYuP7aDvGy6cVN6XHLauvF8SOga2y0dcLcjt3iQDTSEliyw==", + "devOptional": true, + "engines": { + "node": "*" + } + }, + "node_modules/firebase-tools/node_modules/binary-extensions": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.0.0.tgz", + "integrity": "sha512-Phlt0plgpIIBOGTT/ehfFnbNlfsDEiqmzE2KRXoX1bLIlir4X/MR+zSyBEkL05ffWgnRSf/DXv+WrUAVr93/ow==", + "devOptional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "devOptional": true, + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/firebase-tools/node_modules/body-parser": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", + "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==", + "devOptional": true, + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.4", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.11.0", + "raw-body": "2.5.1", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/firebase-tools/node_modules/body-parser/node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "devOptional": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/firebase-tools/node_modules/body-parser/node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "devOptional": true, + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/firebase-tools/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "devOptional": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/firebase-tools/node_modules/braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "devOptional": true, + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "devOptional": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/firebase-tools/node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI=", + "devOptional": true, + "engines": { + "node": "*" + } + }, + "node_modules/firebase-tools/node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "devOptional": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/firebase-tools/node_modules/cacache": { + "version": "16.1.3", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-16.1.3.tgz", + "integrity": "sha512-/+Emcj9DAXxX4cwlLmRI9c166RuL3w30zp4R7Joiv2cQTtTtA+jeuCAjH3ZlGnYS3tKENSrKhAzVVP9GVyzeYQ==", + "optional": true, + "dependencies": { + "@npmcli/fs": "^2.1.0", + "@npmcli/move-file": "^2.0.0", + "chownr": "^2.0.0", + "fs-minipass": "^2.1.0", + "glob": "^8.0.1", + "infer-owner": "^1.0.4", + "lru-cache": "^7.7.1", + "minipass": "^3.1.6", + "minipass-collect": "^1.0.2", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "mkdirp": "^1.0.4", + "p-map": "^4.0.0", + "promise-inflight": "^1.0.1", + "rimraf": "^3.0.2", + "ssri": "^9.0.0", + "tar": "^6.1.11", + "unique-filename": "^2.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/firebase-tools/node_modules/cacache/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "optional": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/firebase-tools/node_modules/cacache/node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "optional": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/firebase-tools/node_modules/cacache/node_modules/glob": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.0.3.tgz", + "integrity": "sha512-ull455NHSHI/Y1FqGaaYFaLGkNMMJbavMrEGFXG/PGrg6y7sutWHUHrz6gy6WEBH6akM1M414dWKCNs+IhKdiQ==", + "optional": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/firebase-tools/node_modules/cacache/node_modules/lru-cache": { + "version": "7.14.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.14.0.tgz", + "integrity": "sha512-EIRtP1GrSJny0dqb50QXRUNBxHJhcpxHC++M5tD7RYbvLLn5KVWKsbyswSSqDuU15UFi3bgTQIY8nhDMeF6aDQ==", + "optional": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/firebase-tools/node_modules/cacache/node_modules/minimatch": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.0.tgz", + "integrity": "sha512-9TPBGGak4nHfGZsPBohm9AWg6NoT7QTCehS3BIJABslyZbzxfV78QM2Y6+i741OPZIafFAaiiEMh5OyIrJPgtg==", + "optional": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/firebase-tools/node_modules/cacache/node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "optional": true, + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/firebase-tools/node_modules/cacache/node_modules/p-map": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", + "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", + "optional": true, + "dependencies": { + "aggregate-error": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/firebase-tools/node_modules/call-bind": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", + "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "devOptional": true, + "dependencies": { + "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/firebase-tools/node_modules/call-me-maybe": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.1.tgz", + "integrity": "sha1-JtII6onje1y95gJQoV8DHBak1ms=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/cardinal": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/cardinal/-/cardinal-2.1.1.tgz", + "integrity": "sha1-fMEFXYItISlU0HsIXeolHMe8VQU=", + "devOptional": true, + "dependencies": { + "ansicolors": "~0.3.2", + "redeyed": "~2.1.0" + }, + "bin": { + "cdl": "bin/cdl.js" + } + }, + "node_modules/firebase-tools/node_modules/caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/catharsis": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/catharsis/-/catharsis-0.9.0.tgz", + "integrity": "sha512-prMTQVpcns/tzFgFVkVp6ak6RykZyWb3gu8ckUpd6YkTlacOd3DXGJjIpD4Q6zJirizvaiAjSSHlOsA+6sNh2A==", + "devOptional": true, + "dependencies": { + "lodash": "^4.17.15" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/firebase-tools/node_modules/chardet": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", + "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/chokidar": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "devOptional": true, + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ], + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/firebase-tools/node_modules/ci-info": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz", + "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/cjson": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/cjson/-/cjson-0.3.3.tgz", + "integrity": "sha1-qS2ceG5b+bkwgGMp7gXV0yYbSvo=", + "devOptional": true, + "dependencies": { + "json-parse-helpfulerror": "^1.0.3" + }, + "engines": { + "node": ">= 0.3.0" + } + }, + "node_modules/firebase-tools/node_modules/clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", + "optional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/firebase-tools/node_modules/cli-boxes": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-2.2.1.tgz", + "integrity": "sha512-y4coMcylgSCdVinjiDBuR8PCC2bLjyGTwEmPb9NHR/QaNU6EUOXcTY/s6VjGMD6ENSEaeQYHCY0GNGS5jfMwPw==", + "devOptional": true, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/firebase-tools/node_modules/cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "devOptional": true, + "dependencies": { + "restore-cursor": "^3.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/cli-spinners": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.6.1.tgz", + "integrity": "sha512-x/5fWmGMnbKQAaNwN+UZlV79qBLM9JFnJuJ03gIi5whrob0xV0ofNVHy9DhwGdsMJQc2OKv0oGmLzvaqvAVv+g==", + "devOptional": true, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/firebase-tools/node_modules/cli-table": { + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/cli-table/-/cli-table-0.3.11.tgz", + "integrity": "sha512-IqLQi4lO0nIB4tcdTpN4LCB9FI3uqrJZK7RC515EnhZ6qBaglkIgICb1wjeAqpdoOabm1+SuQtkXIPdYC93jhQ==", + "devOptional": true, + "dependencies": { + "colors": "1.0.3" + }, + "engines": { + "node": ">= 0.2.0" + } + }, + "node_modules/firebase-tools/node_modules/cli-table3": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.2.tgz", + "integrity": "sha512-QyavHCaIC80cMivimWu4aWHilIpiDpfm3hGmqAmXVL1UsnbLuBSMd21hTX6VY4ZSDSM73ESLeF8TOYId3rBTbw==", + "devOptional": true, + "dependencies": { + "string-width": "^4.2.0" + }, + "engines": { + "node": "10.* || >= 12.*" + }, + "optionalDependencies": { + "@colors/colors": "1.5.0" + } + }, + "node_modules/firebase-tools/node_modules/cli-width": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-3.0.0.tgz", + "integrity": "sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==", + "devOptional": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/firebase-tools/node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "devOptional": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/firebase-tools/node_modules/clone": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha1-2jCcwmPfFZlMaIypAheco8fNfH4=", + "devOptional": true, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/firebase-tools/node_modules/color": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/color/-/color-3.0.0.tgz", + "integrity": "sha512-jCpd5+s0s0t7p3pHQKpnJ0TpQKKdleP71LWcA0aqiljpiuAkOSUFN/dyH8ZwF0hRmFlrIuRhufds1QyEP9EB+w==", + "devOptional": true, + "dependencies": { + "color-convert": "^1.9.1", + "color-string": "^1.5.2" + } + }, + "node_modules/firebase-tools/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "devOptional": true, + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/firebase-tools/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/color-string": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.5.5.tgz", + "integrity": "sha512-jgIoum0OfQfq9Whcfc2z/VhCNcmQjWbey6qBX0vqt7YICflUmBCh9E9CiQD5GSJ+Uehixm3NUwHVhqUAWRivZg==", + "devOptional": true, + "dependencies": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, + "node_modules/firebase-tools/node_modules/color-support": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", + "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", + "optional": true, + "bin": { + "color-support": "bin.js" + } + }, + "node_modules/firebase-tools/node_modules/colorette": { + "version": "2.0.19", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.19.tgz", + "integrity": "sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/colornames": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/colornames/-/colornames-1.1.1.tgz", + "integrity": "sha1-+IiQMGhcfE/54qVZ9Qd+t2qBb5Y=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/colors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.0.3.tgz", + "integrity": "sha1-BDP0TYCWgP3rYO0mDxsMJi6CpAs=", + "devOptional": true, + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/firebase-tools/node_modules/colorspace": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/colorspace/-/colorspace-1.1.2.tgz", + "integrity": "sha512-vt+OoIP2d76xLhjwbBaucYlNSpPsrJWPlBTtwCpQKIu6/CSMutyzX93O/Do0qzpH3YoHEes8YEFXyZ797rEhzQ==", + "devOptional": true, + "dependencies": { + "color": "3.0.x", + "text-hex": "1.0.x" + } + }, + "node_modules/firebase-tools/node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "devOptional": true, + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/firebase-tools/node_modules/commander": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.0.1.tgz", + "integrity": "sha512-IPF4ouhCP+qdlcmCedhxX4xiGBPyigb8v5NeUp+0LyhwLgxMqyp3S0vl7TAPfS/hiP7FC3caI/PB9lTmP8r1NA==", + "devOptional": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/firebase-tools/node_modules/compress-commons": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-4.1.1.tgz", + "integrity": "sha512-QLdDLCKNV2dtoTorqgxngQCMA+gWXkM/Nwu7FpeBhk/RdkzimqC3jueb/FDmaZeXh+uby1jkBqE3xArsLBE5wQ==", + "devOptional": true, + "dependencies": { + "buffer-crc32": "^0.2.13", + "crc32-stream": "^4.0.2", + "normalize-path": "^3.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/firebase-tools/node_modules/compressible": { + "version": "2.0.17", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.17.tgz", + "integrity": "sha512-BGHeLCK1GV7j1bSmQQAi26X+GgWcTjLr/0tzSvMCl3LH1w1IJ4PFSPoV5316b30cneTziC+B1a+3OjoSUcQYmw==", + "devOptional": true, + "dependencies": { + "mime-db": ">= 1.40.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/firebase-tools/node_modules/compression": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.7.4.tgz", + "integrity": "sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==", + "devOptional": true, + "dependencies": { + "accepts": "~1.3.5", + "bytes": "3.0.0", + "compressible": "~2.0.16", + "debug": "2.6.9", + "on-headers": "~1.0.2", + "safe-buffer": "5.1.2", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/firebase-tools/node_modules/compression/node_modules/bytes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", + "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=", + "devOptional": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/firebase-tools/node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/config-chain": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.13.tgz", + "integrity": "sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==", + "devOptional": true, + "dependencies": { + "ini": "^1.3.4", + "proto-list": "~1.2.1" + } + }, + "node_modules/firebase-tools/node_modules/configstore": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/configstore/-/configstore-5.0.1.tgz", + "integrity": "sha512-aMKprgk5YhBNyH25hj8wGt2+D52Sw1DRRIzqBwLp2Ya9mFmY8KPvvtvmna8SxVR9JMZ4kzMD68N22vlaRpkeFA==", + "devOptional": true, + "dependencies": { + "dot-prop": "^5.2.0", + "graceful-fs": "^4.1.2", + "make-dir": "^3.0.0", + "unique-string": "^2.0.0", + "write-file-atomic": "^3.0.0", + "xdg-basedir": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/configstore/node_modules/crypto-random-string": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz", + "integrity": "sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==", + "devOptional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/configstore/node_modules/dot-prop": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.2.0.tgz", + "integrity": "sha512-uEUyaDKoSQ1M4Oq8l45hSE26SnTxL6snNnqvK/VWx5wJhmff5z0FUVJDKDanor/6w3kzE3i7XZOk+7wC0EXr1A==", + "devOptional": true, + "dependencies": { + "is-obj": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/configstore/node_modules/unique-string": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-2.0.0.tgz", + "integrity": "sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==", + "devOptional": true, + "dependencies": { + "crypto-random-string": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/connect": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/connect/-/connect-3.7.0.tgz", + "integrity": "sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ==", + "devOptional": true, + "dependencies": { + "debug": "2.6.9", + "finalhandler": "1.1.2", + "parseurl": "~1.3.3", + "utils-merge": "1.0.1" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/firebase-tools/node_modules/console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==", + "optional": true + }, + "node_modules/firebase-tools/node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "devOptional": true, + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/firebase-tools/node_modules/content-disposition/node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "devOptional": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/firebase-tools/node_modules/content-type": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", + "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==", + "devOptional": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/firebase-tools/node_modules/cookie": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", + "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", + "devOptional": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/firebase-tools/node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "devOptional": true, + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/firebase-tools/node_modules/crc-32": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", + "devOptional": true, + "bin": { + "crc32": "bin/crc32.njs" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/firebase-tools/node_modules/crc32-stream": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-4.0.2.tgz", + "integrity": "sha512-DxFZ/Hk473b/muq1VJ///PMNLj0ZMnzye9thBpmjpJKCc5eMgB95aK8zCGrGfQ90cWo561Te6HK9D+j4KPdM6w==", + "devOptional": true, + "dependencies": { + "crc-32": "^1.2.0", + "readable-stream": "^3.4.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/firebase-tools/node_modules/cross-env": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-5.2.0.tgz", + "integrity": "sha512-jtdNFfFW1hB7sMhr/H6rW1Z45LFqyI431m3qU6bFXcQ3Eh7LtBuG3h74o7ohHZ3crrRkkqHlo4jYHFPcjroANg==", + "devOptional": true, + "dependencies": { + "cross-spawn": "^6.0.5", + "is-windows": "^1.0.0" + }, + "bin": { + "cross-env": "dist/bin/cross-env.js", + "cross-env-shell": "dist/bin/cross-env-shell.js" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/firebase-tools/node_modules/cross-env/node_modules/cross-spawn": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", + "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", + "devOptional": true, + "dependencies": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + }, + "engines": { + "node": ">=4.8" + } + }, + "node_modules/firebase-tools/node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "devOptional": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/firebase-tools/node_modules/cross-spawn/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==", + "devOptional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/cross-spawn/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==", + "devOptional": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/cross-spawn/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==", + "devOptional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/cross-spawn/node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "devOptional": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/firebase-tools/node_modules/csv-parse": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/csv-parse/-/csv-parse-5.0.4.tgz", + "integrity": "sha512-5AIdl8l6n3iYQYxan5djB5eKDa+vBnhfWZtRpJTcrETWfVLYN0WSj3L9RwvgYt+psoO77juUr8TG8qpfGZifVQ==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", + "devOptional": true, + "dependencies": { + "assert-plus": "^1.0.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/firebase-tools/node_modules/data-uri-to-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-3.0.1.tgz", + "integrity": "sha512-WboRycPNsVw3B3TL559F7kuBUM4d8CgMEvk6xEJlOp7OBPjt6G7z8WMWlD2rOFZLk6OYfFIUGsCOWzcQH9K2og==", + "devOptional": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/firebase-tools/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "devOptional": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/firebase-tools/node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "devOptional": true, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/firebase-tools/node_modules/deep-freeze": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/deep-freeze/-/deep-freeze-0.0.1.tgz", + "integrity": "sha1-OgsABd4YZygZ39OM0x+RF5yJPoQ=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/deep-is": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", + "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/defaults": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.3.tgz", + "integrity": "sha1-xlYFHpgX2f8I7YgUd/P+QBnz730=", + "devOptional": true, + "dependencies": { + "clone": "^1.0.2" + } + }, + "node_modules/firebase-tools/node_modules/degenerator": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-3.0.1.tgz", + "integrity": "sha512-LFsIFEeLPlKvAKXu7j3ssIG6RT0TbI7/GhsqrI0DnHASEQjXQ0LUSYcjJteGgRGmZbl1TnMSxpNQIAiJ7Du5TQ==", + "devOptional": true, + "dependencies": { + "ast-types": "^0.13.2", + "escodegen": "^1.8.1", + "esprima": "^4.0.0", + "vm2": "^3.9.3" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/firebase-tools/node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", + "devOptional": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/firebase-tools/node_modules/delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", + "optional": true + }, + "node_modules/firebase-tools/node_modules/depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=", + "optional": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/firebase-tools/node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "devOptional": true, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/firebase-tools/node_modules/diagnostics": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/diagnostics/-/diagnostics-1.1.1.tgz", + "integrity": "sha512-8wn1PmdunLJ9Tqbx+Fx/ZEuHfJf4NKSN2ZBj7SJC/OWRWha843+WsTjqMe1B5E3p28jqBlp+mJ2fPVxPyNgYKQ==", + "devOptional": true, + "dependencies": { + "colorspace": "1.1.x", + "enabled": "1.0.x", + "kuler": "1.0.x" + } + }, + "node_modules/firebase-tools/node_modules/duplexify": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.1.tgz", + "integrity": "sha512-DY3xVEmVHTv1wSzKNbwoU6nVjzI369Y6sPoqfYr0/xlx3IdX2n94xIszTcjPO8W8ZIv0Wb0PXNcjuZyT4wiICA==", + "devOptional": true, + "dependencies": { + "end-of-stream": "^1.4.1", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1", + "stream-shift": "^1.0.0" + } + }, + "node_modules/firebase-tools/node_modules/ecc-jsbn": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", + "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", + "devOptional": true, + "dependencies": { + "jsbn": "~0.1.0", + "safer-buffer": "^2.1.0" + } + }, + "node_modules/firebase-tools/node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "devOptional": true, + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/firebase-tools/node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/enabled": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/enabled/-/enabled-1.0.2.tgz", + "integrity": "sha1-ll9lE9LC0cX0ZStkouM5ZGf8L5M=", + "devOptional": true, + "dependencies": { + "env-variable": "0.0.x" + } + }, + "node_modules/firebase-tools/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=", + "devOptional": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/firebase-tools/node_modules/encoding": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", + "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", + "optional": true, + "dependencies": { + "iconv-lite": "^0.6.2" + } + }, + "node_modules/firebase-tools/node_modules/encoding/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "optional": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/firebase-tools/node_modules/end-of-stream": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.1.tgz", + "integrity": "sha512-1MkrZNvWTKCaigbn+W15elq2BB/L22nqrSY5DKlo3X6+vclJm8Bb5djXJBmEX6fS3+zCh/F4VBK5Z2KxJt4s2Q==", + "devOptional": true, + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/firebase-tools/node_modules/entities": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.1.0.tgz", + "integrity": "sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w==", + "devOptional": true, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/firebase-tools/node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "optional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/firebase-tools/node_modules/env-variable": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/env-variable/-/env-variable-0.0.6.tgz", + "integrity": "sha512-bHz59NlBbtS0NhftmR8+ExBEekE7br0e01jw+kk0NDro7TtZzBYZ5ScGPs3OmwnpyfHTHOtr1Y6uedCdrIldtg==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/err-code": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", + "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", + "optional": true + }, + "node_modules/firebase-tools/node_modules/escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "devOptional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/firebase-tools/node_modules/escape-goat": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/escape-goat/-/escape-goat-2.1.1.tgz", + "integrity": "sha512-8/uIhbG12Csjy2JEW7D9pHbreaVaS/OpN3ycnyvElTdwM5n6GY6W6e2IPemfvGZeUMqZ9A/3GqIZMgKnBhAw/Q==", + "devOptional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "devOptional": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/firebase-tools/node_modules/escodegen": { + "version": "1.14.3", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.14.3.tgz", + "integrity": "sha512-qFcX0XJkdg+PB3xjZZG/wKSuT1PnQWx57+TVSjIMmILd2yC/6ByYElPwJnslDsuWuSAp4AwJGumarAAmJch5Kw==", + "devOptional": true, + "dependencies": { + "esprima": "^4.0.1", + "estraverse": "^4.2.0", + "esutils": "^2.0.2", + "optionator": "^0.8.1" + }, + "bin": { + "escodegen": "bin/escodegen.js", + "esgenerate": "bin/esgenerate.js" + }, + "engines": { + "node": ">=4.0" + }, + "optionalDependencies": { + "source-map": "~0.6.1" + } + }, + "node_modules/firebase-tools/node_modules/eslint-visitor-keys": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz", + "integrity": "sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA==", + "devOptional": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/firebase-tools/node_modules/espree": { + "version": "9.4.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.4.1.tgz", + "integrity": "sha512-XwctdmTO6SIvCzd9810yyNzIrOrqNYV9Koizx4C/mRhf9uq0o4yHoCEU/670pOxOL/MSraektvSAji79kX90Vg==", + "devOptional": true, + "dependencies": { + "acorn": "^8.8.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/firebase-tools/node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "devOptional": true, + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/firebase-tools/node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "devOptional": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/firebase-tools/node_modules/esutils": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz", + "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=", + "devOptional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/firebase-tools/node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "devOptional": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/firebase-tools/node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "devOptional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/firebase-tools/node_modules/events-listener": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/events-listener/-/events-listener-1.1.0.tgz", + "integrity": "sha512-Kd3EgYfODHueq6GzVfs/VUolh2EgJsS8hkO3KpnDrxVjU3eq63eXM2ujXkhPP+OkeUOhL8CxdfZbQXzryb5C4g==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/exegesis": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/exegesis/-/exegesis-4.1.0.tgz", + "integrity": "sha512-iqc55n+hmv8d1KYNMjq7bCcp4u74oRY6MBcj6Vsux7Wd4mRvlgahKqrBTyLIWwscNjEF3qvPmeJ0RPTj8ORMNg==", + "devOptional": true, + "dependencies": { + "@apidevtools/json-schema-ref-parser": "^9.0.3", + "ajv": "^8.3.0", + "ajv-formats": "^2.1.0", + "body-parser": "^1.18.3", + "content-type": "^1.0.4", + "deep-freeze": "0.0.1", + "events-listener": "^1.1.0", + "glob": "^7.1.3", + "json-ptr": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "lodash": "^4.17.11", + "openapi3-ts": "^2.0.1", + "promise-breaker": "^5.0.0", + "pump": "^3.0.0", + "qs": "^6.6.0", + "raw-body": "^2.3.3", + "semver": "^7.0.0" + }, + "engines": { + "node": ">=6.0.0", + "npm": ">5.0.0" + } + }, + "node_modules/firebase-tools/node_modules/exegesis-express": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/exegesis-express/-/exegesis-express-4.0.0.tgz", + "integrity": "sha512-V2hqwTtYRj0bj43K4MCtm0caD97YWkqOUHFMRCBW5L1x9IjyqOEc7Xa4oQjjiFbeFOSQzzwPV+BzXsQjSz08fw==", + "devOptional": true, + "dependencies": { + "exegesis": "^4.1.0" + }, + "engines": { + "node": ">=6.0.0", + "npm": ">5.0.0" + } + }, + "node_modules/firebase-tools/node_modules/exegesis/node_modules/ajv": { + "version": "8.8.2", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.8.2.tgz", + "integrity": "sha512-x9VuX+R/jcFj1DHo/fCp99esgGDWiHENrKxaCENuCxpoMCmAt/COCGVDwA7kleEpEzJjDnvh3yGoOuLu0Dtllw==", + "devOptional": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/firebase-tools/node_modules/exegesis/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/exegesis/node_modules/semver": { + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", + "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", + "devOptional": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/firebase-tools/node_modules/express": { + "version": "4.18.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz", + "integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==", + "devOptional": true, + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.1", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.5.0", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.2.0", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.7", + "qs": "6.11.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.18.0", + "serve-static": "1.15.0", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/firebase-tools/node_modules/express/node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "devOptional": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/firebase-tools/node_modules/express/node_modules/finalhandler": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", + "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", + "devOptional": true, + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/firebase-tools/node_modules/express/node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "devOptional": true, + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/firebase-tools/node_modules/express/node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "devOptional": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/firebase-tools/node_modules/express/node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "devOptional": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/firebase-tools/node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/external-editor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", + "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", + "devOptional": true, + "dependencies": { + "chardet": "^0.7.0", + "iconv-lite": "^0.4.24", + "tmp": "^0.0.33" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/firebase-tools/node_modules/external-editor/node_modules/tmp": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "devOptional": true, + "dependencies": { + "os-tmpdir": "~1.0.2" + }, + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/firebase-tools/node_modules/extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=", + "devOptional": true, + "engines": [ + "node >=0.6.0" + ] + }, + "node_modules/firebase-tools/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==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/fast-json-stable-stringify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", + "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/fast-safe-stringify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", + "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/fast-text-encoding": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/fast-text-encoding/-/fast-text-encoding-1.0.3.tgz", + "integrity": "sha512-dtm4QZH9nZtcDt8qJiOH9fcQd1NAgi+K1O2DbE6GG1PPCK/BWfOH3idCTRQ4ImXRUOyopDEgDEnVEE7Y/2Wrig==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/fast-url-parser": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/fast-url-parser/-/fast-url-parser-1.1.3.tgz", + "integrity": "sha1-9K8+qfNNiicc9YrSs3WfQx8LMY0=", + "devOptional": true, + "dependencies": { + "punycode": "^1.3.2" + } + }, + "node_modules/firebase-tools/node_modules/fast-url-parser/node_modules/punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/fecha": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fecha/-/fecha-2.3.3.tgz", + "integrity": "sha512-lUGBnIamTAwk4znq5BcqsDaxSmZ9nDVJaij6NvRt/Tg4R69gERA+otPKbS86ROw9nxVMw2/mp1fnaiWqbs6Sdg==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/figures": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", + "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", + "devOptional": true, + "dependencies": { + "escape-string-regexp": "^1.0.5" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/firebase-tools/node_modules/file-uri-to-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-2.0.0.tgz", + "integrity": "sha512-hjPFI8oE/2iQPVe4gbrJ73Pp+Xfub2+WI2LlXDbsaJBwT5wuMh35WNWVYYTpnz895shtwfyutMFLFywpQAFdLg==", + "devOptional": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/firebase-tools/node_modules/filesize": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/filesize/-/filesize-6.1.0.tgz", + "integrity": "sha512-LpCHtPQ3sFx67z+uh2HnSyWSLLu5Jxo21795uRDuar/EOuYWXib5EmPaGIBuSnRqH2IODiKA2k5re/K9OnN/Yg==", + "devOptional": true, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/firebase-tools/node_modules/fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "devOptional": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/finalhandler": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", + "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", + "devOptional": true, + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "statuses": "~1.5.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/firebase-tools/node_modules/forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=", + "devOptional": true, + "engines": { + "node": "*" + } + }, + "node_modules/firebase-tools/node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "devOptional": true, + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/firebase-tools/node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "devOptional": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/firebase-tools/node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=", + "devOptional": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/firebase-tools/node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "devOptional": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/firebase-tools/node_modules/fs-extra/node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "devOptional": true, + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/firebase-tools/node_modules/fs-extra/node_modules/universalify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", + "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", + "devOptional": true, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/firebase-tools/node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "devOptional": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/firebase-tools/node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/firebase-tools/node_modules/ftp": { + "version": "0.3.10", + "resolved": "https://registry.npmjs.org/ftp/-/ftp-0.3.10.tgz", + "integrity": "sha1-kZfYYa2BQvPmPVqDv+TFn3MwiF0=", + "devOptional": true, + "dependencies": { + "readable-stream": "1.1.x", + "xregexp": "2.0.0" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/firebase-tools/node_modules/ftp/node_modules/isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/ftp/node_modules/readable-stream": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", + "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", + "devOptional": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + }, + "node_modules/firebase-tools/node_modules/ftp/node_modules/string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/gauge": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-4.0.4.tgz", + "integrity": "sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg==", + "optional": true, + "dependencies": { + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.3", + "console-control-strings": "^1.1.0", + "has-unicode": "^2.0.1", + "signal-exit": "^3.0.7", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.5" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/firebase-tools/node_modules/gaxios": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-4.1.0.tgz", + "integrity": "sha512-vb0to8xzGnA2qcgywAjtshOKKVDf2eQhJoiL6fHhgW5tVN7wNk7egnYIO9zotfn3lQ3De1VPdf7V5/BWfCtCmg==", + "devOptional": true, + "dependencies": { + "abort-controller": "^3.0.0", + "extend": "^3.0.2", + "https-proxy-agent": "^5.0.0", + "is-stream": "^2.0.0", + "node-fetch": "^2.3.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/firebase-tools/node_modules/gaxios/node_modules/is-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.0.tgz", + "integrity": "sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw==", + "devOptional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/gcp-metadata": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-4.2.0.tgz", + "integrity": "sha512-vQZD57cQkqIA6YPGXM/zc+PIZfNRFdukWGsGZ5+LcJzesi5xp6Gn7a02wRJi4eXPyArNMIYpPET4QMxGqtlk6Q==", + "devOptional": true, + "dependencies": { + "gaxios": "^3.0.0", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/firebase-tools/node_modules/gcp-metadata/node_modules/gaxios": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-3.2.0.tgz", + "integrity": "sha512-+6WPeVzPvOshftpxJwRi2Ozez80tn/hdtOUag7+gajDHRJvAblKxTFSSMPtr2hmnLy7p0mvYz0rMXLBl8pSO7Q==", + "devOptional": true, + "dependencies": { + "abort-controller": "^3.0.0", + "extend": "^3.0.2", + "https-proxy-agent": "^5.0.0", + "is-stream": "^2.0.0", + "node-fetch": "^2.3.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/firebase-tools/node_modules/gcp-metadata/node_modules/is-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.0.tgz", + "integrity": "sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw==", + "devOptional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "devOptional": true, + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/firebase-tools/node_modules/get-intrinsic": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.3.tgz", + "integrity": "sha512-QJVz1Tj7MS099PevUG5jvnt9tSkXN8K14dxQlikJuPt4uD9hHAHjLyLBiLR5zELelBdD9QNRAXZzsJx0WaDL9A==", + "devOptional": true, + "dependencies": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/firebase-tools/node_modules/get-uri": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-3.0.2.tgz", + "integrity": "sha512-+5s0SJbGoyiJTZZ2JTpFPLMPSch72KEqGOTvQsBqg0RBWvwhWUSYZFAtz3TPW0GXJuLBJPts1E241iHg+VRfhg==", + "devOptional": true, + "dependencies": { + "@tootallnate/once": "1", + "data-uri-to-buffer": "3", + "debug": "4", + "file-uri-to-path": "2", + "fs-extra": "^8.1.0", + "ftp": "^0.3.10" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/firebase-tools/node_modules/get-uri/node_modules/debug": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", + "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", + "devOptional": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/firebase-tools/node_modules/get-uri/node_modules/fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "devOptional": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/firebase-tools/node_modules/get-uri/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", + "devOptional": true, + "dependencies": { + "assert-plus": "^1.0.0" + } + }, + "node_modules/firebase-tools/node_modules/glob": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", + "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", + "devOptional": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/firebase-tools/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==", + "devOptional": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/firebase-tools/node_modules/glob-slash": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/glob-slash/-/glob-slash-1.0.0.tgz", + "integrity": "sha1-/lLvpDMjP3Si/mTHq7m8hIICq5U=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/glob-slasher": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/glob-slasher/-/glob-slasher-1.0.1.tgz", + "integrity": "sha1-dHoOW7IiZC7hDT4FRD4QlJPLD44=", + "devOptional": true, + "dependencies": { + "glob-slash": "^1.0.0", + "lodash.isobject": "^2.4.1", + "toxic": "^1.0.0" + } + }, + "node_modules/firebase-tools/node_modules/global-dirs": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-3.0.1.tgz", + "integrity": "sha512-NBcGGFbBA9s1VzD41QXDG+3++t9Mn5t1FpLdhESY6oKY4gYTFpX4wO3sqGUa0Srjtbfj3szX0RnemmrVRUdULA==", + "devOptional": true, + "dependencies": { + "ini": "2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/firebase-tools/node_modules/global-dirs/node_modules/ini": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ini/-/ini-2.0.0.tgz", + "integrity": "sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA==", + "devOptional": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/firebase-tools/node_modules/google-auth-library": { + "version": "7.14.1", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-7.14.1.tgz", + "integrity": "sha512-5Rk7iLNDFhFeBYc3s8l1CqzbEBcdhwR193RlD4vSNFajIcINKI8W8P0JLmBpwymHqqWbX34pJDQu39cSy/6RsA==", + "devOptional": true, + "dependencies": { + "arrify": "^2.0.0", + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "fast-text-encoding": "^1.0.0", + "gaxios": "^4.0.0", + "gcp-metadata": "^4.2.0", + "gtoken": "^5.0.4", + "jws": "^4.0.0", + "lru-cache": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/firebase-tools/node_modules/google-auth-library/node_modules/jwa": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz", + "integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==", + "devOptional": true, + "dependencies": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/firebase-tools/node_modules/google-auth-library/node_modules/jws": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", + "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", + "devOptional": true, + "dependencies": { + "jwa": "^2.0.0", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/firebase-tools/node_modules/google-gax": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/google-gax/-/google-gax-3.5.3.tgz", + "integrity": "sha512-caItkifbcPDVf5xW2j6xLXmuX9bh2dheJN9AzMuQj6VvWacUVV5d0BkjL+Ia8sgX9VvXtbqjf4NKK3nTgV9UUQ==", + "devOptional": true, + "dependencies": { + "@grpc/grpc-js": "~1.7.0", + "@grpc/proto-loader": "^0.7.0", + "@types/long": "^4.0.0", + "abort-controller": "^3.0.0", + "duplexify": "^4.0.0", + "fast-text-encoding": "^1.0.3", + "google-auth-library": "^8.0.2", + "is-stream-ended": "^0.1.4", + "node-fetch": "^2.6.1", + "object-hash": "^3.0.0", + "proto3-json-serializer": "^1.0.0", + "protobufjs": "7.1.2", + "protobufjs-cli": "1.1.0", + "retry-request": "^5.0.0" + }, + "bin": { + "compileProtos": "build/tools/compileProtos.js", + "minifyProtoJson": "build/tools/minify.js" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/firebase-tools/node_modules/google-gax/node_modules/@grpc/proto-loader": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.4.tgz", + "integrity": "sha512-MnWjkGwqQ3W8fx94/c1CwqLsNmHHv2t0CFn+9++6+cDphC1lolpg9M2OU0iebIjK//pBNX9e94ho+gjx6vz39w==", + "devOptional": true, + "dependencies": { + "@types/long": "^4.0.1", + "lodash.camelcase": "^4.3.0", + "long": "^4.0.0", + "protobufjs": "^7.0.0", + "yargs": "^16.2.0" + }, + "bin": { + "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/firebase-tools/node_modules/google-gax/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==", + "devOptional": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/firebase-tools/node_modules/google-gax/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "devOptional": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/firebase-tools/node_modules/google-gax/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "devOptional": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/firebase-tools/node_modules/google-gax/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==", + "devOptional": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/firebase-tools/node_modules/google-gax/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==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/google-gax/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "devOptional": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/firebase-tools/node_modules/google-gax/node_modules/gaxios": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-5.0.2.tgz", + "integrity": "sha512-TjtV2AJOZoMQqRYoy5eM8cCQogYwazWNYLQ72QB0kwa6vHHruYkGmhhyrlzbmgNHK1dNnuP2WSH81urfzyN2Og==", + "devOptional": true, + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^5.0.0", + "is-stream": "^2.0.0", + "node-fetch": "^2.6.7" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/firebase-tools/node_modules/google-gax/node_modules/gcp-metadata": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-5.2.0.tgz", + "integrity": "sha512-aFhhvvNycky2QyhG+dcfEdHBF0FRbYcf39s6WNHUDysKSrbJ5vuFbjydxBcmewtXeV248GP8dWT3ByPNxsyHCw==", + "devOptional": true, + "dependencies": { + "gaxios": "^5.0.0", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/firebase-tools/node_modules/google-gax/node_modules/glob": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "devOptional": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/firebase-tools/node_modules/google-gax/node_modules/google-auth-library": { + "version": "8.7.0", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-8.7.0.tgz", + "integrity": "sha512-1M0NG5VDIvJZEnstHbRdckLZESoJwguinwN8Dhae0j2ZKIQFIV63zxm6Fo6nM4xkgqUr2bbMtV5Dgo+Hy6oo0Q==", + "devOptional": true, + "dependencies": { + "arrify": "^2.0.0", + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "fast-text-encoding": "^1.0.0", + "gaxios": "^5.0.0", + "gcp-metadata": "^5.0.0", + "gtoken": "^6.1.0", + "jws": "^4.0.0", + "lru-cache": "^6.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/firebase-tools/node_modules/google-gax/node_modules/google-p12-pem": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/google-p12-pem/-/google-p12-pem-4.0.1.tgz", + "integrity": "sha512-WPkN4yGtz05WZ5EhtlxNDWPhC4JIic6G8ePitwUWy4l+XPVYec+a0j0Ts47PDtW59y3RwAhUd9/h9ZZ63px6RQ==", + "devOptional": true, + "dependencies": { + "node-forge": "^1.3.1" + }, + "bin": { + "gp12-pem": "build/src/bin/gp12-pem.js" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/firebase-tools/node_modules/google-gax/node_modules/gtoken": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-6.1.2.tgz", + "integrity": "sha512-4ccGpzz7YAr7lxrT2neugmXQ3hP9ho2gcaityLVkiUecAiwiy60Ii8gRbZeOsXV19fYaRjgBSshs8kXw+NKCPQ==", + "devOptional": true, + "dependencies": { + "gaxios": "^5.0.1", + "google-p12-pem": "^4.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/firebase-tools/node_modules/google-gax/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==", + "devOptional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/google-gax/node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "devOptional": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/firebase-tools/node_modules/google-gax/node_modules/jwa": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz", + "integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==", + "devOptional": true, + "dependencies": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/firebase-tools/node_modules/google-gax/node_modules/jws": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", + "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", + "devOptional": true, + "dependencies": { + "jwa": "^2.0.0", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/firebase-tools/node_modules/google-gax/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "devOptional": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/firebase-tools/node_modules/google-gax/node_modules/protobufjs": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.1.2.tgz", + "integrity": "sha512-4ZPTPkXCdel3+L81yw3dG6+Kq3umdWKh7Dc7GW/CpNk4SX3hK58iPCWeCyhVTDrbkNeKrYNZ7EojM5WDaEWTLQ==", + "devOptional": true, + "hasInstallScript": true, + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/node": ">=13.7.0", + "long": "^5.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/firebase-tools/node_modules/google-gax/node_modules/protobufjs-cli": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/protobufjs-cli/-/protobufjs-cli-1.1.0.tgz", + "integrity": "sha512-VXMQn+z3yG2WbN2E+mx5vcyIHF7yJSg2jqyqfxcZLWNOSTqUzSSgAE5vu04/JEpwxTI04JGyrZRDHC36wr04uw==", + "devOptional": true, + "dependencies": { + "chalk": "^4.0.0", + "escodegen": "^1.13.0", + "espree": "^9.0.0", + "estraverse": "^5.1.0", + "glob": "^8.0.0", + "jsdoc": "^4.0.0", + "minimist": "^1.2.0", + "semver": "^7.1.2", + "tmp": "^0.2.1", + "uglify-js": "^3.7.7" + }, + "bin": { + "pbjs": "bin/pbjs", + "pbts": "bin/pbts" + }, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "protobufjs": "^7.0.0" + } + }, + "node_modules/firebase-tools/node_modules/google-gax/node_modules/protobufjs/node_modules/long": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/long/-/long-5.2.1.tgz", + "integrity": "sha512-GKSNGeNAtw8IryjjkhZxuKB3JzlcLTwjtiQCHKvqQet81I93kXslhDQruGI/QsddO83mcDToBVy7GqGS/zYf/A==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/google-gax/node_modules/semver": { + "version": "7.3.8", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", + "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", + "devOptional": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/firebase-tools/node_modules/google-gax/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==", + "devOptional": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/google-p12-pem": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/google-p12-pem/-/google-p12-pem-3.1.3.tgz", + "integrity": "sha512-MC0jISvzymxePDVembypNefkAQp+DRP7dBE+zNUPaIjEspIlYg0++OrsNr248V9tPbz6iqtZ7rX1hxWA5B8qBQ==", + "devOptional": true, + "dependencies": { + "node-forge": "^1.0.0" + }, + "bin": { + "gp12-pem": "build/src/bin/gp12-pem.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/firebase-tools/node_modules/graceful-fs": { + "version": "4.2.10", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", + "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/gtoken": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-5.3.2.tgz", + "integrity": "sha512-gkvEKREW7dXWF8NV8pVrKfW7WqReAmjjkMBh6lNCCGOM4ucS0r0YyXXl0r/9Yj8wcW/32ISkfc8h5mPTDbtifQ==", + "devOptional": true, + "dependencies": { + "gaxios": "^4.0.0", + "google-p12-pem": "^3.1.3", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/firebase-tools/node_modules/gtoken/node_modules/jwa": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz", + "integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==", + "devOptional": true, + "dependencies": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/firebase-tools/node_modules/gtoken/node_modules/jws": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", + "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", + "devOptional": true, + "dependencies": { + "jwa": "^2.0.0", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/firebase-tools/node_modules/har-schema": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", + "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=", + "devOptional": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/firebase-tools/node_modules/har-validator": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.3.tgz", + "integrity": "sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==", + "deprecated": "this library is no longer supported", + "devOptional": true, + "dependencies": { + "ajv": "^6.5.5", + "har-schema": "^2.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/firebase-tools/node_modules/has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "devOptional": true, + "dependencies": { + "function-bind": "^1.1.1" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/firebase-tools/node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "devOptional": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/firebase-tools/node_modules/has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==", + "optional": true + }, + "node_modules/firebase-tools/node_modules/has-yarn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/has-yarn/-/has-yarn-2.1.0.tgz", + "integrity": "sha512-UqBRqi4ju7T+TqGNdqAO0PaSVGsDGJUBQvk9eUWNGRY1CFGDzYhLWoM7JQEemnlvVcv/YEmc2wNW8BC24EnUsw==", + "devOptional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/http-cache-semantics": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", + "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==", + "optional": true + }, + "node_modules/firebase-tools/node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "devOptional": true, + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/firebase-tools/node_modules/http-errors/node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "devOptional": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/firebase-tools/node_modules/http-errors/node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "devOptional": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/firebase-tools/node_modules/http-proxy-agent": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz", + "integrity": "sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==", + "devOptional": true, + "dependencies": { + "@tootallnate/once": "1", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/firebase-tools/node_modules/http-proxy-agent/node_modules/debug": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "devOptional": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/firebase-tools/node_modules/http-proxy-agent/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/http-signature": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", + "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", + "devOptional": true, + "dependencies": { + "assert-plus": "^1.0.0", + "jsprim": "^1.2.2", + "sshpk": "^1.7.0" + }, + "engines": { + "node": ">=0.8", + "npm": ">=1.3.7" + } + }, + "node_modules/firebase-tools/node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "devOptional": true, + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/firebase-tools/node_modules/https-proxy-agent/node_modules/debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "deprecated": "Debug versions >=3.2.0 <3.2.7 || >=4 <4.3.1 have a low-severity ReDos regression when used in a Node.js environment. It is recommended you upgrade to 3.2.7 or 4.3.1. (https://github.com/visionmedia/debug/issues/797)", + "devOptional": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/firebase-tools/node_modules/https-proxy-agent/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/humanize-ms": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", + "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", + "optional": true, + "dependencies": { + "ms": "^2.0.0" + } + }, + "node_modules/firebase-tools/node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "devOptional": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/firebase-tools/node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "devOptional": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/firebase-tools/node_modules/import-lazy": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/import-lazy/-/import-lazy-2.1.0.tgz", + "integrity": "sha1-BWmOPUXIjo1+nZLLBYTnfwlvPkM=", + "devOptional": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/firebase-tools/node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", + "devOptional": true, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/firebase-tools/node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/infer-owner": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/infer-owner/-/infer-owner-1.0.4.tgz", + "integrity": "sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==", + "optional": true + }, + "node_modules/firebase-tools/node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "devOptional": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/firebase-tools/node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/inquirer": { + "version": "8.2.5", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-8.2.5.tgz", + "integrity": "sha512-QAgPDQMEgrDssk1XiwwHoOGYF9BAbUcc1+j+FhEvaOt8/cKRqyLn0U5qA6F74fGhTMGxf92pOvPBeh29jQJDTQ==", + "devOptional": true, + "dependencies": { + "ansi-escapes": "^4.2.1", + "chalk": "^4.1.1", + "cli-cursor": "^3.1.0", + "cli-width": "^3.0.0", + "external-editor": "^3.0.3", + "figures": "^3.0.0", + "lodash": "^4.17.21", + "mute-stream": "0.0.8", + "ora": "^5.4.1", + "run-async": "^2.4.0", + "rxjs": "^7.5.5", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0", + "through": "^2.3.6", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/firebase-tools/node_modules/inquirer/node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "devOptional": true, + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/firebase-tools/node_modules/inquirer/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "devOptional": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/firebase-tools/node_modules/inquirer/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "devOptional": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/firebase-tools/node_modules/inquirer/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "devOptional": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/firebase-tools/node_modules/inquirer/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/inquirer/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "devOptional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/inquirer/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "devOptional": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/inquirer/node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "devOptional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/firebase-tools/node_modules/install-artifact-from-github": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/install-artifact-from-github/-/install-artifact-from-github-1.3.1.tgz", + "integrity": "sha512-3l3Bymg2eKDsN5wQuMfgGEj2x6l5MCAv0zPL6rxHESufFVlEAKW/6oY9F1aGgvY/EgWm5+eWGRjINveL4X7Hgg==", + "optional": true, + "bin": { + "install-from-cache": "bin/install-from-cache.js", + "save-to-github-cache": "bin/save-to-github-cache.js" + } + }, + "node_modules/firebase-tools/node_modules/ip": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.5.tgz", + "integrity": "sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/ip-regex": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-4.3.0.tgz", + "integrity": "sha512-B9ZWJxHHOHUhUjCPrMpLD4xEq35bUTClHM1S6CBU5ixQnkZmwipwgc96vAd7AAGM9TGHvJR+Uss+/Ak6UphK+Q==", + "devOptional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "devOptional": true, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/firebase-tools/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==", + "devOptional": true, + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/is-ci": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-2.0.0.tgz", + "integrity": "sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w==", + "devOptional": true, + "dependencies": { + "ci-info": "^2.0.0" + }, + "bin": { + "is-ci": "bin.js" + } + }, + "node_modules/firebase-tools/node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", + "devOptional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/firebase-tools/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "devOptional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/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==", + "devOptional": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/firebase-tools/node_modules/is-installed-globally": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.4.0.tgz", + "integrity": "sha512-iwGqO3J21aaSkC7jWnHP/difazwS7SFeIqxv6wEtLU8Y5KlzFTjyqcSIT0d8s4+dDhKytsk9PJZ2BkS5eZwQRQ==", + "devOptional": true, + "dependencies": { + "global-dirs": "^3.0.0", + "is-path-inside": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/firebase-tools/node_modules/is-interactive": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", + "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", + "devOptional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/is-lambda": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-lambda/-/is-lambda-1.0.1.tgz", + "integrity": "sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==", + "optional": true + }, + "node_modules/firebase-tools/node_modules/is-npm": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-npm/-/is-npm-5.0.0.tgz", + "integrity": "sha512-WW/rQLOazUq+ST/bCAVBp/2oMERWLsR7OrKyt052dNDk4DHcDE0/7QSXITlmi+VBcV13DfIbysG3tZJm5RfdBA==", + "devOptional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/firebase-tools/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==", + "devOptional": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/firebase-tools/node_modules/is-obj": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", + "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==", + "devOptional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/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==", + "devOptional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=", + "devOptional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/firebase-tools/node_modules/is-stream-ended": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-stream-ended/-/is-stream-ended-0.1.4.tgz", + "integrity": "sha512-xj0XPvmr7bQFTvirqnFr50o0hQIh6ZItDqloxt5aJrR4NQsYeSsyFQERYGCAzfindAcnKjINnwEEgLx4IqVzQw==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "devOptional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/firebase-tools/node_modules/is-url": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/is-url/-/is-url-1.2.4.tgz", + "integrity": "sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/is-windows": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", + "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", + "devOptional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/firebase-tools/node_modules/is-wsl": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-1.1.0.tgz", + "integrity": "sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0=", + "devOptional": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/firebase-tools/node_modules/is-yarn-global": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/is-yarn-global/-/is-yarn-global-0.3.0.tgz", + "integrity": "sha512-VjSeb/lHmkoyd8ryPVIKvOCn4D1koMqY+vqyjjUfc3xyKtP4dYOxM44sZrnqQSzSds3xyOrUTLTC9LVCVgLngw==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/is2": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/is2/-/is2-2.0.7.tgz", + "integrity": "sha512-4vBQoURAXC6hnLFxD4VW7uc04XiwTTl/8ydYJxKvPwkWQrSjInkuM5VZVg6BGr1/natq69zDuvO9lGpLClJqvA==", + "devOptional": true, + "dependencies": { + "deep-is": "^0.1.3", + "ip-regex": "^4.1.0", + "is-url": "^1.2.4" + }, + "engines": { + "node": ">=v0.10.0" + } + }, + "node_modules/firebase-tools/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/isomorphic-fetch": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-3.0.0.tgz", + "integrity": "sha512-qvUtwJ3j6qwsF3jLxkZ72qCgjMysPzDfeV240JHiGZsANBYd+EEuu35v7dfrJ9Up0Ak07D7GGSkGhCHTqg/5wA==", + "devOptional": true, + "dependencies": { + "node-fetch": "^2.6.1", + "whatwg-fetch": "^3.4.1" + } + }, + "node_modules/firebase-tools/node_modules/isomorphic-fetch/node_modules/whatwg-fetch": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.2.tgz", + "integrity": "sha512-bJlen0FcuU/0EMLrdbJ7zOnW6ITZLrZMIarMUVmdKtsGvZna8vxKYaexICWPfZ8qwf9fzNq+UEIZrnSaApt6RA==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/jju": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/jju/-/jju-1.4.0.tgz", + "integrity": "sha1-o6vicYryQaKykE+EpiWXDzia4yo=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/join-path": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/join-path/-/join-path-1.1.1.tgz", + "integrity": "sha1-EFNaEm0ky9Zff/zfFe8uYxB2tQU=", + "devOptional": true, + "dependencies": { + "as-array": "^2.0.0", + "url-join": "0.0.1", + "valid-url": "^1" + } + }, + "node_modules/firebase-tools/node_modules/js-yaml": { + "version": "3.13.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz", + "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==", + "devOptional": true, + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/firebase-tools/node_modules/js2xmlparser": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/js2xmlparser/-/js2xmlparser-4.0.2.tgz", + "integrity": "sha512-6n4D8gLlLf1n5mNLQPRfViYzu9RATblzPEtm1SthMX1Pjao0r9YI9nw7ZIfRxQMERS87mcswrg+r/OYrPRX6jA==", + "devOptional": true, + "dependencies": { + "xmlcreate": "^2.0.4" + } + }, + "node_modules/firebase-tools/node_modules/jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/jsdoc": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsdoc/-/jsdoc-4.0.0.tgz", + "integrity": "sha512-tzTgkklbWKrlaQL2+e3NNgLcZu3NaK2vsHRx7tyHQ+H5jcB9Gx0txSd2eJWlMC/xU1+7LQu4s58Ry0RkuaEQVg==", + "devOptional": true, + "dependencies": { + "@babel/parser": "^7.9.4", + "@jsdoc/salty": "^0.2.1", + "@types/markdown-it": "^12.2.3", + "bluebird": "^3.7.2", + "catharsis": "^0.9.0", + "escape-string-regexp": "^2.0.0", + "js2xmlparser": "^4.0.2", + "klaw": "^3.0.0", + "markdown-it": "^12.3.2", + "markdown-it-anchor": "^8.4.1", + "marked": "^4.0.10", + "mkdirp": "^1.0.4", + "requizzle": "^0.2.3", + "strip-json-comments": "^3.1.0", + "underscore": "~1.13.2" + }, + "bin": { + "jsdoc": "jsdoc.js" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/firebase-tools/node_modules/jsdoc/node_modules/bluebird": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/jsdoc/node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "devOptional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/jsdoc/node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "devOptional": true, + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/firebase-tools/node_modules/jsdoc/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==", + "devOptional": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/firebase-tools/node_modules/json-bigint": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", + "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", + "devOptional": true, + "dependencies": { + "bignumber.js": "^9.0.0" + } + }, + "node_modules/firebase-tools/node_modules/json-parse-helpfulerror": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/json-parse-helpfulerror/-/json-parse-helpfulerror-1.0.3.tgz", + "integrity": "sha1-E/FM4C7tTpgSl7ZOueO5MuLdE9w=", + "devOptional": true, + "dependencies": { + "jju": "^1.1.0" + } + }, + "node_modules/firebase-tools/node_modules/json-ptr": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-ptr/-/json-ptr-3.0.1.tgz", + "integrity": "sha512-hrZ4tElT8huJUH3OwOK+d7F8PRqw09QnGM3Mm3GmqKWDyCCPCG8lGHxXOwQAj0VOxzLirOds07Kz10B5F8M8EA==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/json-schema": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", + "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==", + "devOptional": true + }, + "node_modules/firebase-tools/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==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", + "devOptional": true, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/firebase-tools/node_modules/jsonwebtoken": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.0.tgz", + "integrity": "sha512-tuGfYXxkQGDPnLJ7SibiQgVgeDgfbPq2k2ICcbgqW8WxWLBAxKQM/ZCu/IT8SOSwmaYl4dpTFCW5xZv7YbbWUw==", + "devOptional": true, + "dependencies": { + "jws": "^3.2.2", + "lodash": "^4.17.21", + "ms": "^2.1.1", + "semver": "^7.3.8" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/firebase-tools/node_modules/jsonwebtoken/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==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/jsonwebtoken/node_modules/semver": { + "version": "7.3.8", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", + "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", + "devOptional": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/firebase-tools/node_modules/jsprim": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.2.tgz", + "integrity": "sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw==", + "devOptional": true, + "dependencies": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.4.0", + "verror": "1.10.0" + }, + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/firebase-tools/node_modules/jwa": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", + "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", + "devOptional": true, + "dependencies": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/firebase-tools/node_modules/jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "devOptional": true, + "dependencies": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/firebase-tools/node_modules/klaw": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/klaw/-/klaw-3.0.0.tgz", + "integrity": "sha512-0Fo5oir+O9jnXu5EefYbVK+mHMBeEVEy2cmctR1O1NECcCkPRreJKrS6Qt/j3KC2C148Dfo9i3pCmCMsdqGr0g==", + "devOptional": true, + "dependencies": { + "graceful-fs": "^4.1.9" + } + }, + "node_modules/firebase-tools/node_modules/kuler": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/kuler/-/kuler-1.0.1.tgz", + "integrity": "sha512-J9nVUucG1p/skKul6DU3PUZrhs0LPulNaeUOox0IyXDi8S4CztTHs1gQphhuZmzXG7VOQSf6NJfKuzteQLv9gQ==", + "devOptional": true, + "dependencies": { + "colornames": "^1.1.1" + } + }, + "node_modules/firebase-tools/node_modules/lazystream": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz", + "integrity": "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==", + "devOptional": true, + "dependencies": { + "readable-stream": "^2.0.5" + }, + "engines": { + "node": ">= 0.6.3" + } + }, + "node_modules/firebase-tools/node_modules/lazystream/node_modules/readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "devOptional": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/firebase-tools/node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "devOptional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/firebase-tools/node_modules/levn": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", + "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", + "devOptional": true, + "dependencies": { + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/firebase-tools/node_modules/libsodium": { + "version": "0.7.10", + "resolved": "https://registry.npmjs.org/libsodium/-/libsodium-0.7.10.tgz", + "integrity": "sha512-eY+z7hDrDKxkAK+QKZVNv92A5KYkxfvIshtBJkmg5TSiCnYqZP3i9OO9whE79Pwgm4jGaoHgkM4ao/b9Cyu4zQ==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/libsodium-wrappers": { + "version": "0.7.10", + "resolved": "https://registry.npmjs.org/libsodium-wrappers/-/libsodium-wrappers-0.7.10.tgz", + "integrity": "sha512-pO3F1Q9NPLB/MWIhehim42b/Fwb30JNScCNh8TcQ/kIc+qGLQch8ag8wb0keK3EP5kbGakk1H8Wwo7v+36rNQg==", + "devOptional": true, + "dependencies": { + "libsodium": "^0.7.0" + } + }, + "node_modules/firebase-tools/node_modules/linkify-it": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-3.0.3.tgz", + "integrity": "sha512-ynTsyrFSdE5oZ/O9GEf00kPngmOfVwazR5GKDq6EYfhlpFug3J2zybX56a2PRRpc9P+FuSoGNAwjlbDs9jJBPQ==", + "devOptional": true, + "dependencies": { + "uc.micro": "^1.0.1" + } + }, + "node_modules/firebase-tools/node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/lodash._objecttypes": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/lodash._objecttypes/-/lodash._objecttypes-2.4.1.tgz", + "integrity": "sha1-fAt/admKH3ZSn4kLDNsbTf7BHBE=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha1-soqmKIorn8ZRA1x3EfZathkDMaY=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/lodash.defaults": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", + "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/lodash.difference": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.difference/-/lodash.difference-4.5.0.tgz", + "integrity": "sha512-dS2j+W26TQ7taQBGN8Lbbq04ssV3emRw4NY58WErlTO29pIqS0HmoT5aJ9+TUQ1N3G+JOZSji4eugsWwGp9yPA==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/lodash.flatten": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz", + "integrity": "sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/lodash.isobject": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/lodash.isobject/-/lodash.isobject-2.4.1.tgz", + "integrity": "sha1-Wi5H/mmVPx7mMafrof5k0tBlWPU=", + "devOptional": true, + "dependencies": { + "lodash._objecttypes": "~2.4.1" + } + }, + "node_modules/firebase-tools/node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/lodash.snakecase": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.snakecase/-/lodash.snakecase-4.1.1.tgz", + "integrity": "sha1-OdcUo1NXFHg3rv1ktdy7Fr7Nj40=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/lodash.union": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.union/-/lodash.union-4.6.0.tgz", + "integrity": "sha512-c4pB2CdGrGdjMKYLA+XiRDO7Y0PRQbm/Gzg8qMj+QH+pFVAoTp5sBpO0odL3FjoPCGjK96p6qsP+yQoiLoOBcw==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "devOptional": true, + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/firebase-tools/node_modules/log-symbols/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "devOptional": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/firebase-tools/node_modules/log-symbols/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "devOptional": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/firebase-tools/node_modules/log-symbols/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "devOptional": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/firebase-tools/node_modules/log-symbols/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/log-symbols/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "devOptional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/log-symbols/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "devOptional": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/logform": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/logform/-/logform-2.1.2.tgz", + "integrity": "sha512-+lZh4OpERDBLqjiwDLpAWNQu6KMjnlXH2ByZwCuSqVPJletw0kTWJf5CgSNAUKn1KUkv3m2cUz/LK8zyEy7wzQ==", + "devOptional": true, + "dependencies": { + "colors": "^1.2.1", + "fast-safe-stringify": "^2.0.4", + "fecha": "^2.3.3", + "ms": "^2.1.1", + "triple-beam": "^1.3.0" + } + }, + "node_modules/firebase-tools/node_modules/logform/node_modules/colors": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz", + "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==", + "devOptional": true, + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/firebase-tools/node_modules/logform/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/long": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", + "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "devOptional": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/firebase-tools/node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "devOptional": true, + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/firebase-tools/node_modules/make-dir/node_modules/semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "devOptional": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/firebase-tools/node_modules/make-fetch-happen": { + "version": "10.2.1", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-10.2.1.tgz", + "integrity": "sha512-NgOPbRiaQM10DYXvN3/hhGVI2M5MtITFryzBGxHM5p4wnFxsVCbxkrBrDsk+EZ5OB4jEOT7AjDxtdF+KVEFT7w==", + "optional": true, + "dependencies": { + "agentkeepalive": "^4.2.1", + "cacache": "^16.1.0", + "http-cache-semantics": "^4.1.0", + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.0", + "is-lambda": "^1.0.1", + "lru-cache": "^7.7.1", + "minipass": "^3.1.6", + "minipass-collect": "^1.0.2", + "minipass-fetch": "^2.0.3", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^0.6.3", + "promise-retry": "^2.0.1", + "socks-proxy-agent": "^7.0.0", + "ssri": "^9.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/firebase-tools/node_modules/make-fetch-happen/node_modules/@tootallnate/once": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", + "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", + "optional": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/firebase-tools/node_modules/make-fetch-happen/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "optional": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/firebase-tools/node_modules/make-fetch-happen/node_modules/http-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", + "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", + "optional": true, + "dependencies": { + "@tootallnate/once": "2", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/firebase-tools/node_modules/make-fetch-happen/node_modules/lru-cache": { + "version": "7.14.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.14.0.tgz", + "integrity": "sha512-EIRtP1GrSJny0dqb50QXRUNBxHJhcpxHC++M5tD7RYbvLLn5KVWKsbyswSSqDuU15UFi3bgTQIY8nhDMeF6aDQ==", + "optional": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/firebase-tools/node_modules/make-fetch-happen/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "optional": true + }, + "node_modules/firebase-tools/node_modules/make-fetch-happen/node_modules/socks-proxy-agent": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-7.0.0.tgz", + "integrity": "sha512-Fgl0YPZ902wEsAyiQ+idGd1A7rSFx/ayC1CQVMw5P+EQx2V0SgpGtf6OKFhVjPflPUl9YMmEOnmfjCdMUsygww==", + "optional": true, + "dependencies": { + "agent-base": "^6.0.2", + "debug": "^4.3.3", + "socks": "^2.6.2" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/firebase-tools/node_modules/markdown-it": { + "version": "12.3.2", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-12.3.2.tgz", + "integrity": "sha512-TchMembfxfNVpHkbtriWltGWc+m3xszaRD0CZup7GFFhzIgQqxIfn3eGj1yZpfuflzPvfkt611B2Q/Bsk1YnGg==", + "devOptional": true, + "dependencies": { + "argparse": "^2.0.1", + "entities": "~2.1.0", + "linkify-it": "^3.0.1", + "mdurl": "^1.0.1", + "uc.micro": "^1.0.5" + }, + "bin": { + "markdown-it": "bin/markdown-it.js" + } + }, + "node_modules/firebase-tools/node_modules/markdown-it-anchor": { + "version": "8.6.6", + "resolved": "https://registry.npmjs.org/markdown-it-anchor/-/markdown-it-anchor-8.6.6.tgz", + "integrity": "sha512-jRW30YGywD2ESXDc+l17AiritL0uVaSnWsb26f+68qaW9zgbIIr1f4v2Nsvc0+s0Z2N3uX6t/yAw7BwCQ1wMsA==", + "devOptional": true, + "peerDependencies": { + "@types/markdown-it": "*", + "markdown-it": "*" + } + }, + "node_modules/firebase-tools/node_modules/markdown-it/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==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/marked": { + "version": "4.2.12", + "resolved": "https://registry.npmjs.org/marked/-/marked-4.2.12.tgz", + "integrity": "sha512-yr8hSKa3Fv4D3jdZmtMMPghgVt6TWbk86WQaWhDloQjRSQhMMYCAro7jP7VDJrjjdV8pxVxMssXS8B8Y5DZ5aw==", + "devOptional": true, + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 12" + } + }, + "node_modules/firebase-tools/node_modules/marked-terminal": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/marked-terminal/-/marked-terminal-5.1.1.tgz", + "integrity": "sha512-+cKTOx9P4l7HwINYhzbrBSyzgxO2HaHKGZGuB1orZsMIgXYaJyfidT81VXRdpelW/PcHEWxywscePVgI/oUF6g==", + "devOptional": true, + "dependencies": { + "ansi-escapes": "^5.0.0", + "cardinal": "^2.1.1", + "chalk": "^5.0.0", + "cli-table3": "^0.6.1", + "node-emoji": "^1.11.0", + "supports-hyperlinks": "^2.2.0" + }, + "engines": { + "node": ">=14.13.1 || >=16.0.0" + }, + "peerDependencies": { + "marked": "^1.0.0 || ^2.0.0 || ^3.0.0 || ^4.0.0" + } + }, + "node_modules/firebase-tools/node_modules/marked-terminal/node_modules/chalk": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.0.1.tgz", + "integrity": "sha512-Fo07WOYGqMfCWHOzSXOt2CxDbC6skS/jO9ynEcmpANMoPrD+W1r1K6Vx7iNm+AQmETU1Xr2t+n8nzkV9t6xh3w==", + "devOptional": true, + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/firebase-tools/node_modules/mdurl": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz", + "integrity": "sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=", + "devOptional": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/firebase-tools/node_modules/merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=", + "devOptional": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/firebase-tools/node_modules/mime": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.5.2.tgz", + "integrity": "sha512-tqkh47FzKeCPD2PUiPB6pkbMzsCasjxAfC62/Wap5qrUWcb+sFasXUC5I3gYM5iBM8v/Qpn4UK0x+j0iHyFPDg==", + "devOptional": true, + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/firebase-tools/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "devOptional": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/firebase-tools/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "devOptional": true, + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/firebase-tools/node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "devOptional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/firebase-tools/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "devOptional": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/firebase-tools/node_modules/minimist": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", + "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/minipass": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.1.6.tgz", + "integrity": "sha512-rty5kpw9/z8SX9dmxblFA6edItUmwJgMeYDZRrwlIVN27i8gysGbznJwUggw2V/FVqFSDdWy040ZPS811DYAqQ==", + "devOptional": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/minipass-collect": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-1.0.2.tgz", + "integrity": "sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==", + "optional": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/firebase-tools/node_modules/minipass-fetch": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-2.1.2.tgz", + "integrity": "sha512-LT49Zi2/WMROHYoqGgdlQIZh8mLPZmOrN2NdJjMXxYe4nkN6FUyuPuOAOedNJDrx0IRGg9+4guZewtp8hE6TxA==", + "optional": true, + "dependencies": { + "minipass": "^3.1.6", + "minipass-sized": "^1.0.3", + "minizlib": "^2.1.2" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + }, + "optionalDependencies": { + "encoding": "^0.1.13" + } + }, + "node_modules/firebase-tools/node_modules/minipass-flush": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz", + "integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==", + "optional": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/firebase-tools/node_modules/minipass-pipeline": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", + "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", + "optional": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/minipass-sized": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz", + "integrity": "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==", + "optional": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "devOptional": true, + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/firebase-tools/node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "devOptional": true, + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/firebase-tools/node_modules/morgan": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.0.tgz", + "integrity": "sha512-AbegBVI4sh6El+1gNwvD5YIck7nSA36weD7xvIxG4in80j/UoK8AEGaWnnz8v1GxonMCltmlNs5ZKbGvl9b1XQ==", + "devOptional": true, + "dependencies": { + "basic-auth": "~2.0.1", + "debug": "2.6.9", + "depd": "~2.0.0", + "on-finished": "~2.3.0", + "on-headers": "~1.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/firebase-tools/node_modules/morgan/node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "devOptional": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/firebase-tools/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/mute-stream": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", + "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/nan": { + "version": "2.16.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.16.0.tgz", + "integrity": "sha512-UdAqHyFngu7TfQKsCBgAA6pWDkT8MAO7d0jyOecVhN5354xbLqdn8mV9Tat9gepAupm0bt2DbeaSC8vS52MuFA==", + "optional": true + }, + "node_modules/firebase-tools/node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "devOptional": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/firebase-tools/node_modules/netmask": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.0.2.tgz", + "integrity": "sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==", + "devOptional": true, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/firebase-tools/node_modules/nice-try": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", + "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/node-emoji": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-1.11.0.tgz", + "integrity": "sha512-wo2DpQkQp7Sjm2A0cq+sN7EHKO6Sl0ctXeBdFZrL9T9+UywORbufTcTZxom8YqpLQt/FqNMUkOpkZrJVYSKD3A==", + "devOptional": true, + "dependencies": { + "lodash": "^4.17.21" + } + }, + "node_modules/firebase-tools/node_modules/node-fetch": { + "version": "2.6.7", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", + "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", + "devOptional": true, + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/firebase-tools/node_modules/node-forge": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", + "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==", + "devOptional": true, + "engines": { + "node": ">= 6.13.0" + } + }, + "node_modules/firebase-tools/node_modules/node-gyp": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-9.1.0.tgz", + "integrity": "sha512-HkmN0ZpQJU7FLbJauJTHkHlSVAXlNGDAzH/VYFZGDOnFyn/Na3GlNJfkudmufOdS6/jNFhy88ObzL7ERz9es1g==", + "optional": true, + "dependencies": { + "env-paths": "^2.2.0", + "glob": "^7.1.4", + "graceful-fs": "^4.2.6", + "make-fetch-happen": "^10.0.3", + "nopt": "^5.0.0", + "npmlog": "^6.0.0", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "tar": "^6.1.2", + "which": "^2.0.2" + }, + "bin": { + "node-gyp": "bin/node-gyp.js" + }, + "engines": { + "node": "^12.22 || ^14.13 || >=16" + } + }, + "node_modules/firebase-tools/node_modules/node-gyp/node_modules/semver": { + "version": "7.3.7", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz", + "integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==", + "optional": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/firebase-tools/node_modules/node-gyp/node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "optional": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/firebase-tools/node_modules/nopt": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", + "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", + "optional": true, + "dependencies": { + "abbrev": "1" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/firebase-tools/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==", + "devOptional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/firebase-tools/node_modules/npmlog": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-6.0.2.tgz", + "integrity": "sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg==", + "optional": true, + "dependencies": { + "are-we-there-yet": "^3.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^4.0.3", + "set-blocking": "^2.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/firebase-tools/node_modules/oauth-sign": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", + "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", + "devOptional": true, + "engines": { + "node": "*" + } + }, + "node_modules/firebase-tools/node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", + "devOptional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/firebase-tools/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==", + "devOptional": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/firebase-tools/node_modules/object-inspect": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.2.tgz", + "integrity": "sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==", + "devOptional": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/firebase-tools/node_modules/on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", + "devOptional": true, + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/firebase-tools/node_modules/on-headers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", + "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", + "devOptional": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/firebase-tools/node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "devOptional": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/firebase-tools/node_modules/one-time": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/one-time/-/one-time-0.0.4.tgz", + "integrity": "sha1-+M33eISCb+Tf+T46nMN7HkSAdC4=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "devOptional": true, + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/firebase-tools/node_modules/open": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/open/-/open-6.4.0.tgz", + "integrity": "sha512-IFenVPgF70fSm1keSd2iDBIDIBZkroLeuffXq+wKTzTJlBpesFWojV9lb8mzOfaAzM1sr7HQHuO0vtV0zYekGg==", + "devOptional": true, + "dependencies": { + "is-wsl": "^1.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/openapi3-ts": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/openapi3-ts/-/openapi3-ts-2.0.1.tgz", + "integrity": "sha512-v6X3iwddhi276siej96jHGIqTx3wzVfMTmpGJEQDt7GPI7pI6sywItURLzpEci21SBRpPN/aOWSF5mVfFVNmcg==", + "devOptional": true, + "dependencies": { + "yaml": "^1.10.0" + } + }, + "node_modules/firebase-tools/node_modules/optionator": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", + "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", + "devOptional": true, + "dependencies": { + "deep-is": "~0.1.3", + "fast-levenshtein": "~2.0.6", + "levn": "~0.3.0", + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2", + "word-wrap": "~1.2.3" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/firebase-tools/node_modules/ora": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", + "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", + "devOptional": true, + "dependencies": { + "bl": "^4.1.0", + "chalk": "^4.1.0", + "cli-cursor": "^3.1.0", + "cli-spinners": "^2.5.0", + "is-interactive": "^1.0.0", + "is-unicode-supported": "^0.1.0", + "log-symbols": "^4.1.0", + "strip-ansi": "^6.0.0", + "wcwidth": "^1.0.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/firebase-tools/node_modules/ora/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "devOptional": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/firebase-tools/node_modules/ora/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "devOptional": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/firebase-tools/node_modules/ora/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "devOptional": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/firebase-tools/node_modules/ora/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/ora/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "devOptional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/ora/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "devOptional": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", + "devOptional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/firebase-tools/node_modules/p-defer": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-defer/-/p-defer-3.0.0.tgz", + "integrity": "sha512-ugZxsxmtTln604yeYd29EGrNhazN2lywetzpKhfmQjW/VJmhpDmWbiX+h0zL8V91R0UXkhb3KtPmyq9PZw3aYw==", + "devOptional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/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==", + "devOptional": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/firebase-tools/node_modules/pac-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-5.0.0.tgz", + "integrity": "sha512-CcFG3ZtnxO8McDigozwE3AqAw15zDvGH+OjXO4kzf7IkEKkQ4gxQ+3sdF50WmhQ4P/bVusXcqNE2S3XrNURwzQ==", + "devOptional": true, + "dependencies": { + "@tootallnate/once": "1", + "agent-base": "6", + "debug": "4", + "get-uri": "3", + "http-proxy-agent": "^4.0.1", + "https-proxy-agent": "5", + "pac-resolver": "^5.0.0", + "raw-body": "^2.2.0", + "socks-proxy-agent": "5" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/firebase-tools/node_modules/pac-proxy-agent/node_modules/debug": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", + "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", + "devOptional": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/firebase-tools/node_modules/pac-proxy-agent/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/pac-resolver": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-5.0.0.tgz", + "integrity": "sha512-H+/A6KitiHNNW+bxBKREk2MCGSxljfqRX76NjummWEYIat7ldVXRU3dhRIE3iXZ0nvGBk6smv3nntxKkzRL8NA==", + "devOptional": true, + "dependencies": { + "degenerator": "^3.0.1", + "ip": "^1.1.5", + "netmask": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/firebase-tools/node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "devOptional": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/firebase-tools/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": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "devOptional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/firebase-tools/node_modules/path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=", + "devOptional": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/firebase-tools/node_modules/path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "devOptional": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/firebase-tools/node_modules/portfinder": { + "version": "1.0.32", + "resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.32.tgz", + "integrity": "sha512-on2ZJVVDXRADWE6jnQaX0ioEylzgBpQk8r55NE4wjXW1ZxO+BgDlY6DXwj20i0V8eB4SenDQ00WEaxfiIQPcxg==", + "devOptional": true, + "dependencies": { + "async": "^2.6.4", + "debug": "^3.2.7", + "mkdirp": "^0.5.6" + }, + "engines": { + "node": ">= 0.12.0" + } + }, + "node_modules/firebase-tools/node_modules/portfinder/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "devOptional": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/firebase-tools/node_modules/portfinder/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==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/prelude-ls": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", + "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=", + "devOptional": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/firebase-tools/node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "devOptional": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/firebase-tools/node_modules/promise-breaker": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/promise-breaker/-/promise-breaker-5.0.0.tgz", + "integrity": "sha512-mgsWQuG4kJ1dtO6e/QlNDLFtMkMzzecsC69aI5hlLEjGHFNpHrvGhFi4LiK5jg2SMQj74/diH+wZliL9LpGsyA==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/promise-inflight": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", + "integrity": "sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==", + "optional": true + }, + "node_modules/firebase-tools/node_modules/promise-retry": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", + "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", + "optional": true, + "dependencies": { + "err-code": "^2.0.2", + "retry": "^0.12.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/firebase-tools/node_modules/promise-retry/node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "optional": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/firebase-tools/node_modules/proto-list": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", + "integrity": "sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/proto3-json-serializer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proto3-json-serializer/-/proto3-json-serializer-1.1.0.tgz", + "integrity": "sha512-SjXwUWe/vANGs/mJJTbw5++7U67nwsymg7qsoPtw6GiXqw3kUy8ByojrlEdVE2efxAdKreX8WkDafxvYW95ZQg==", + "devOptional": true, + "dependencies": { + "protobufjs": "^7.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/firebase-tools/node_modules/proto3-json-serializer/node_modules/long": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/long/-/long-5.2.1.tgz", + "integrity": "sha512-GKSNGeNAtw8IryjjkhZxuKB3JzlcLTwjtiQCHKvqQet81I93kXslhDQruGI/QsddO83mcDToBVy7GqGS/zYf/A==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/proto3-json-serializer/node_modules/protobufjs": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.1.2.tgz", + "integrity": "sha512-4ZPTPkXCdel3+L81yw3dG6+Kq3umdWKh7Dc7GW/CpNk4SX3hK58iPCWeCyhVTDrbkNeKrYNZ7EojM5WDaEWTLQ==", + "devOptional": true, + "hasInstallScript": true, + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/node": ">=13.7.0", + "long": "^5.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/firebase-tools/node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "devOptional": true, + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/firebase-tools/node_modules/proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-5.0.0.tgz", + "integrity": "sha512-gkH7BkvLVkSfX9Dk27W6TyNOWWZWRilRfk1XxGNWOYJ2TuedAv1yFpCaU9QSBmBe716XOTNpYNOzhysyw8xn7g==", + "devOptional": true, + "dependencies": { + "agent-base": "^6.0.0", + "debug": "4", + "http-proxy-agent": "^4.0.0", + "https-proxy-agent": "^5.0.0", + "lru-cache": "^5.1.1", + "pac-proxy-agent": "^5.0.0", + "proxy-from-env": "^1.0.0", + "socks-proxy-agent": "^5.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/firebase-tools/node_modules/proxy-agent/node_modules/debug": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", + "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", + "devOptional": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/firebase-tools/node_modules/proxy-agent/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==", + "devOptional": true, + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/firebase-tools/node_modules/proxy-agent/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/proxy-agent/node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/psl": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz", + "integrity": "sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "devOptional": true, + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/firebase-tools/node_modules/punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", + "devOptional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/firebase-tools/node_modules/pupa": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/pupa/-/pupa-2.1.1.tgz", + "integrity": "sha512-l1jNAspIBSFqbT+y+5FosojNpVpF94nlI+wDUpqP9enwOTfHx9f0gh5nB96vl+6yTpsJsypeNrwfzPrKuHB41A==", + "devOptional": true, + "dependencies": { + "escape-goat": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/qs": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", + "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "devOptional": true, + "dependencies": { + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/firebase-tools/node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "devOptional": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/firebase-tools/node_modules/raw-body": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", + "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", + "devOptional": true, + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/firebase-tools/node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "devOptional": true, + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/firebase-tools/node_modules/re2": { + "version": "1.17.7", + "resolved": "https://registry.npmjs.org/re2/-/re2-1.17.7.tgz", + "integrity": "sha512-X8GSuiBoVWwcjuppqSjsIkRxNUKDdjhkO9SBekQbZ2ksqWUReCy7DQPWOVpoTnpdtdz5PIpTTxTFzvJv5UMfjA==", + "hasInstallScript": true, + "optional": true, + "dependencies": { + "install-artifact-from-github": "^1.3.1", + "nan": "^2.16.0", + "node-gyp": "^9.0.0" + } + }, + "node_modules/firebase-tools/node_modules/readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "devOptional": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/firebase-tools/node_modules/readdir-glob": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/readdir-glob/-/readdir-glob-1.1.1.tgz", + "integrity": "sha512-91/k1EzZwDx6HbERR+zucygRFfiPl2zkIYZtv3Jjr6Mn7SkKcVct8aVO+sSRiGMc6fLf72du3d92/uY63YPdEA==", + "devOptional": true, + "dependencies": { + "minimatch": "^3.0.4" + } + }, + "node_modules/firebase-tools/node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "devOptional": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/firebase-tools/node_modules/redeyed": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/redeyed/-/redeyed-2.1.1.tgz", + "integrity": "sha1-iYS1gV2ZyyIEacme7v/jiRPmzAs=", + "devOptional": true, + "dependencies": { + "esprima": "~4.0.0" + } + }, + "node_modules/firebase-tools/node_modules/registry-url": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/registry-url/-/registry-url-5.1.0.tgz", + "integrity": "sha512-8acYXXTI0AkQv6RAOjE3vOaIXZkT9wo4LOFbBKYQEEnnMNBpKqdUrI6S4NT0KPIo/WVvJ5tE/X5LF/TQUf0ekw==", + "devOptional": true, + "dependencies": { + "rc": "^1.2.8" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/request": { + "version": "2.88.2", + "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", + "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==", + "deprecated": "request has been deprecated, see https://github.com/request/request/issues/3142", + "devOptional": true, + "dependencies": { + "aws-sign2": "~0.7.0", + "aws4": "^1.8.0", + "caseless": "~0.12.0", + "combined-stream": "~1.0.6", + "extend": "~3.0.2", + "forever-agent": "~0.6.1", + "form-data": "~2.3.2", + "har-validator": "~5.1.3", + "http-signature": "~1.2.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.19", + "oauth-sign": "~0.9.0", + "performance-now": "^2.1.0", + "qs": "~6.5.2", + "safe-buffer": "^5.1.2", + "tough-cookie": "~2.5.0", + "tunnel-agent": "^0.6.0", + "uuid": "^3.3.2" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/firebase-tools/node_modules/request/node_modules/form-data": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", + "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", + "devOptional": true, + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 0.12" + } + }, + "node_modules/firebase-tools/node_modules/request/node_modules/qs": { + "version": "6.5.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.3.tgz", + "integrity": "sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==", + "devOptional": true, + "engines": { + "node": ">=0.6" + } + }, + "node_modules/firebase-tools/node_modules/request/node_modules/uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", + "deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.", + "devOptional": true, + "bin": { + "uuid": "bin/uuid" + } + }, + "node_modules/firebase-tools/node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", + "devOptional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/firebase-tools/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==", + "devOptional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/firebase-tools/node_modules/requizzle": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/requizzle/-/requizzle-0.2.4.tgz", + "integrity": "sha512-JRrFk1D4OQ4SqovXOgdav+K8EAhSB/LJZqCz8tbX0KObcdeM15Ss59ozWMBWmmINMagCwmqn4ZNryUGpBsl6Jw==", + "devOptional": true, + "dependencies": { + "lodash": "^4.17.21" + } + }, + "node_modules/firebase-tools/node_modules/restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "devOptional": true, + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/retry": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "devOptional": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/firebase-tools/node_modules/retry-request": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/retry-request/-/retry-request-5.0.2.tgz", + "integrity": "sha512-wfI3pk7EE80lCIXprqh7ym48IHYdwmAAzESdbU8Q9l7pnRCk9LEhpbOTNKjz6FARLm/Bl5m+4F0ABxOkYUujSQ==", + "devOptional": true, + "dependencies": { + "debug": "^4.1.1", + "extend": "^3.0.2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/firebase-tools/node_modules/retry-request/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "devOptional": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/firebase-tools/node_modules/retry-request/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "devOptional": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/firebase-tools/node_modules/router": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/router/-/router-1.3.5.tgz", + "integrity": "sha512-kozCJZUhuSJ5VcLhSb3F8fsmGXy+8HaDbKCAerR1G6tq3mnMZFMuSohbFvGv1c5oMFipijDjRZuuN/Sq5nMf3g==", + "devOptional": true, + "dependencies": { + "array-flatten": "3.0.0", + "debug": "2.6.9", + "methods": "~1.1.2", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.7", + "setprototypeof": "1.2.0", + "utils-merge": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/firebase-tools/node_modules/router/node_modules/array-flatten": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-3.0.0.tgz", + "integrity": "sha512-zPMVc3ZYlGLNk4mpK1NzP2wg0ml9t7fUgDsayR5Y5rSzxQilzR9FGu/EH2jQOcKSAeAfWeylyW8juy3OkWRvNA==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/run-async": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", + "integrity": "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==", + "devOptional": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/firebase-tools/node_modules/rxjs": { + "version": "7.5.6", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.5.6.tgz", + "integrity": "sha512-dnyv2/YsXhnm461G+R/Pe5bWP41Nm6LBXEYWI6eiFP4fiwx6WRI/CD0zbdVAudd9xwLEF2IDcKXLHit0FYjUzw==", + "devOptional": true, + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/firebase-tools/node_modules/rxjs/node_modules/tslib": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz", + "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "devOptional": true, + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/firebase-tools/node_modules/semver-diff": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/semver-diff/-/semver-diff-3.1.1.tgz", + "integrity": "sha512-GX0Ix/CJcHyB8c4ykpHGIAvLyOwOobtM/8d+TQkAd81/bEjgPHrfba41Vpesr7jX/t8Uh+R3EX9eAS5be+jQYg==", + "devOptional": true, + "dependencies": { + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/semver-diff/node_modules/semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "devOptional": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/firebase-tools/node_modules/send": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", + "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "devOptional": true, + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/firebase-tools/node_modules/send/node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "devOptional": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/firebase-tools/node_modules/send/node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "devOptional": true, + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/firebase-tools/node_modules/send/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==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/send/node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "devOptional": true, + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/firebase-tools/node_modules/send/node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "devOptional": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/firebase-tools/node_modules/serve-static": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", + "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", + "devOptional": true, + "dependencies": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.18.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/firebase-tools/node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", + "optional": true + }, + "node_modules/firebase-tools/node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", + "devOptional": true, + "dependencies": { + "shebang-regex": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/firebase-tools/node_modules/shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", + "devOptional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/firebase-tools/node_modules/side-channel": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", + "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "devOptional": true, + "dependencies": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/firebase-tools/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/simple-swizzle": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", + "integrity": "sha1-pNprY1/8zMoz9w0Xy5JZLeleVXo=", + "devOptional": true, + "dependencies": { + "is-arrayish": "^0.3.1" + } + }, + "node_modules/firebase-tools/node_modules/simple-swizzle/node_modules/is-arrayish": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", + "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "devOptional": true, + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/firebase-tools/node_modules/socks": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.7.0.tgz", + "integrity": "sha512-scnOe9y4VuiNUULJN72GrM26BNOjVsfPXI+j+98PkyEfsIXroa5ofyjT+FzGvn/xHs73U2JtoBYAVx9Hl4quSA==", + "devOptional": true, + "dependencies": { + "ip": "^2.0.0", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.13.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/firebase-tools/node_modules/socks-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-5.0.1.tgz", + "integrity": "sha512-vZdmnjb9a2Tz6WEQVIurybSwElwPxMZaIc7PzqbJTrezcKNznv6giT7J7tZDZ1BojVaa1jvO/UiUdhDVB0ACoQ==", + "devOptional": true, + "dependencies": { + "agent-base": "^6.0.2", + "debug": "4", + "socks": "^2.3.3" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/firebase-tools/node_modules/socks-proxy-agent/node_modules/debug": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", + "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", + "devOptional": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/firebase-tools/node_modules/socks-proxy-agent/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/socks/node_modules/ip": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ip/-/ip-2.0.0.tgz", + "integrity": "sha512-WKa+XuLG1A1R0UWhl2+1XQSi+fZWMsYKffMZTTYsiZaUD8k2yDAj5atimTUD2TZkyCkNEeYE5NhFZmupOGtjYQ==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/firebase-tools/node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/sshpk": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz", + "integrity": "sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==", + "devOptional": true, + "dependencies": { + "asn1": "~0.2.3", + "assert-plus": "^1.0.0", + "bcrypt-pbkdf": "^1.0.0", + "dashdash": "^1.12.0", + "ecc-jsbn": "~0.1.1", + "getpass": "^0.1.1", + "jsbn": "~0.1.0", + "safer-buffer": "^2.0.2", + "tweetnacl": "~0.14.0" + }, + "bin": { + "sshpk-conv": "bin/sshpk-conv", + "sshpk-sign": "bin/sshpk-sign", + "sshpk-verify": "bin/sshpk-verify" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/firebase-tools/node_modules/ssri": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-9.0.1.tgz", + "integrity": "sha512-o57Wcn66jMQvfHG1FlYbWeZWW/dHZhJXjpIcTfXldXEk5nz5lStPo3mK0OJQfGR3RbZUlbISexbljkJzuEj/8Q==", + "optional": true, + "dependencies": { + "minipass": "^3.1.1" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/firebase-tools/node_modules/stack-trace": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", + "integrity": "sha1-VHxws0fo0ytOEI6hoqFZ5f3eGcA=", + "devOptional": true, + "engines": { + "node": "*" + } + }, + "node_modules/firebase-tools/node_modules/statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=", + "devOptional": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/firebase-tools/node_modules/stream-chain": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/stream-chain/-/stream-chain-2.2.4.tgz", + "integrity": "sha512-9lsl3YM53V5N/I1C2uJtc3Kavyi3kNYN83VkKb/bMWRk7D9imiFyUPYa0PoZbLohSVOX1mYE9YsmwObZUsth6Q==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/stream-json": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/stream-json/-/stream-json-1.7.3.tgz", + "integrity": "sha512-Y6dXn9KKWSwxOqnvHGcdZy1PK+J+7alBwHCeU3W9oRqm4ilLRA0XSPmd1tWwhg7tv9EIxJTMWh7KF15tYelKJg==", + "devOptional": true, + "dependencies": { + "stream-chain": "^2.2.4" + } + }, + "node_modules/firebase-tools/node_modules/stream-shift": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.1.tgz", + "integrity": "sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "devOptional": true, + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/firebase-tools/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "devOptional": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/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==", + "devOptional": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/strip-ansi/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==", + "devOptional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", + "devOptional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/firebase-tools/node_modules/superstatic": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/superstatic/-/superstatic-9.0.3.tgz", + "integrity": "sha512-e/tmW0bsnQ/33ivK6y3CapJT0Ovy4pk/ohNPGhIAGU2oasoNLRQ1cv6enua09NU9w6Y0H/fBu07cjzuiWvLXxw==", + "devOptional": true, + "dependencies": { + "basic-auth-connect": "^1.0.0", + "commander": "^10.0.0", + "compression": "^1.7.0", + "connect": "^3.7.0", + "destroy": "^1.0.4", + "fast-url-parser": "^1.1.3", + "glob-slasher": "^1.0.1", + "is-url": "^1.2.2", + "join-path": "^1.1.1", + "lodash": "^4.17.19", + "mime-types": "^2.1.35", + "minimatch": "^6.1.6", + "morgan": "^1.8.2", + "on-finished": "^2.2.0", + "on-headers": "^1.0.0", + "path-to-regexp": "^1.8.0", + "router": "^1.3.1", + "update-notifier-cjs": "^5.1.6" + }, + "bin": { + "superstatic": "lib/bin/server.js" + }, + "engines": { + "node": "^14.18.0 || >=16.4.0" + }, + "optionalDependencies": { + "re2": "^1.17.7" + } + }, + "node_modules/firebase-tools/node_modules/superstatic/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "devOptional": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/firebase-tools/node_modules/superstatic/node_modules/commander": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.0.tgz", + "integrity": "sha512-zS5PnTI22FIRM6ylNW8G4Ap0IEOyk62fhLSD0+uHRT9McRCLGpkVNvao4bjimpK/GShynyQkFFxHhwMcETmduA==", + "devOptional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/firebase-tools/node_modules/superstatic/node_modules/isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/superstatic/node_modules/minimatch": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-6.2.0.tgz", + "integrity": "sha512-sauLxniAmvnhhRjFwPNnJKaPFYyddAgbYdeUpHULtCT/GhzdCx/MDNy+Y40lBxTQUrMzDE8e0S43Z5uqfO0REg==", + "devOptional": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/firebase-tools/node_modules/superstatic/node_modules/path-to-regexp": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz", + "integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==", + "devOptional": true, + "dependencies": { + "isarray": "0.0.1" + } + }, + "node_modules/firebase-tools/node_modules/supports-hyperlinks": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-2.2.0.tgz", + "integrity": "sha512-6sXEzV5+I5j8Bmq9/vUphGRM/RJNT9SCURJLjwfOg51heRtguGWDzcaBlgAzKhQa0EVNpPEKzQuBwZ8S8WaCeQ==", + "devOptional": true, + "dependencies": { + "has-flag": "^4.0.0", + "supports-color": "^7.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/supports-hyperlinks/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==", + "devOptional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/supports-hyperlinks/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==", + "devOptional": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/tar": { + "version": "6.1.11", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.1.11.tgz", + "integrity": "sha512-an/KZQzQUkZCkuoAA64hM92X0Urb6VpRhAFllDzz44U2mcD5scmT3zBc4VgVpkugF580+DQn8eAFSyoQt0tznA==", + "devOptional": true, + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^3.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/firebase-tools/node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "devOptional": true, + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/firebase-tools/node_modules/tar/node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "devOptional": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/firebase-tools/node_modules/tar/node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "devOptional": true, + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/firebase-tools/node_modules/tcp-port-used": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/tcp-port-used/-/tcp-port-used-1.0.2.tgz", + "integrity": "sha512-l7ar8lLUD3XS1V2lfoJlCBaeoaWo/2xfYt81hM7VlvR4RrMVFqfmzfhLVk40hAb368uitje5gPtBRL1m/DGvLA==", + "devOptional": true, + "dependencies": { + "debug": "4.3.1", + "is2": "^2.0.6" + } + }, + "node_modules/firebase-tools/node_modules/tcp-port-used/node_modules/debug": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "devOptional": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/firebase-tools/node_modules/tcp-port-used/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/text-hex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", + "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/tmp": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz", + "integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==", + "devOptional": true, + "dependencies": { + "rimraf": "^3.0.0" + }, + "engines": { + "node": ">=8.17.0" + } + }, + "node_modules/firebase-tools/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==", + "devOptional": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/firebase-tools/node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "devOptional": true, + "engines": { + "node": ">=0.6" + } + }, + "node_modules/firebase-tools/node_modules/tough-cookie": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", + "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", + "devOptional": true, + "dependencies": { + "psl": "^1.1.28", + "punycode": "^2.1.1" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/firebase-tools/node_modules/toxic": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toxic/-/toxic-1.0.1.tgz", + "integrity": "sha512-WI3rIGdcaKULYg7KVoB0zcjikqvcYYvcuT6D89bFPz2rVR0Rl0PK6x8/X62rtdLtBKIE985NzVf/auTtGegIIg==", + "devOptional": true, + "dependencies": { + "lodash": "^4.17.10" + } + }, + "node_modules/firebase-tools/node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/triple-beam": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.3.0.tgz", + "integrity": "sha512-XrHUvV5HpdLmIj4uVMxHggLbFSZYIn7HEWsqePZcI50pco+MPqJ50wMGY794X7AOOhxOBAjbkqfAbEe/QMp2Lw==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", + "devOptional": true, + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/firebase-tools/node_modules/tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/type-check": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", + "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", + "devOptional": true, + "dependencies": { + "prelude-ls": "~1.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/firebase-tools/node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "devOptional": true, + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/firebase-tools/node_modules/typedarray-to-buffer": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", + "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", + "devOptional": true, + "dependencies": { + "is-typedarray": "^1.0.0" + } + }, + "node_modules/firebase-tools/node_modules/uc.micro": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz", + "integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/uglify-js": { + "version": "3.17.4", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.17.4.tgz", + "integrity": "sha512-T9q82TJI9e/C1TAxYvfb16xO120tMVFZrGA3f9/P4424DNu6ypK103y0GPFVa17yotwSyZW5iYXgjYHkGrJW/g==", + "devOptional": true, + "bin": { + "uglifyjs": "bin/uglifyjs" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/firebase-tools/node_modules/underscore": { + "version": "1.13.6", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.6.tgz", + "integrity": "sha512-+A5Sja4HP1M08MaXya7p5LvjuM7K6q/2EaC0+iovj/wOcMsTzMvDFbasi/oSapiwOlt252IqsKqPjCl7huKS0A==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/unique-filename": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-2.0.1.tgz", + "integrity": "sha512-ODWHtkkdx3IAR+veKxFV+VBkUMcN+FaqzUUd7IZzt+0zhDZFPFxhlqwPF3YQvMHx1TD0tdgYl+kuPnJ8E6ql7A==", + "optional": true, + "dependencies": { + "unique-slug": "^3.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/firebase-tools/node_modules/unique-slug": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-3.0.0.tgz", + "integrity": "sha512-8EyMynh679x/0gqE9fT9oilG+qEt+ibFyqjuVTsZn1+CMxH+XLlpvr2UZx4nVcCwTpx81nICr2JQFkM+HPLq4w==", + "optional": true, + "dependencies": { + "imurmurhash": "^0.1.4" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/firebase-tools/node_modules/universal-analytics": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/universal-analytics/-/universal-analytics-0.5.3.tgz", + "integrity": "sha512-HXSMyIcf2XTvwZ6ZZQLfxfViRm/yTGoRgDeTbojtq6rezeyKB0sTBcKH2fhddnteAHRcHiKgr/ACpbgjGOC6RQ==", + "devOptional": true, + "dependencies": { + "debug": "^4.3.1", + "uuid": "^8.0.0" + }, + "engines": { + "node": ">=12.18.2" + } + }, + "node_modules/firebase-tools/node_modules/universal-analytics/node_modules/debug": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", + "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", + "devOptional": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/firebase-tools/node_modules/universal-analytics/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "devOptional": true, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/firebase-tools/node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=", + "devOptional": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/firebase-tools/node_modules/update-notifier-cjs": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/update-notifier-cjs/-/update-notifier-cjs-5.1.6.tgz", + "integrity": "sha512-wgxdSBWv3x/YpMzsWz5G4p4ec7JWD0HCl8W6bmNB6E5Gwo+1ym5oN4hiXpLf0mPySVEJEIsYlkshnplkg2OP9A==", + "devOptional": true, + "dependencies": { + "boxen": "^5.0.0", + "chalk": "^4.1.0", + "configstore": "^5.0.1", + "has-yarn": "^2.1.0", + "import-lazy": "^2.1.0", + "is-ci": "^2.0.0", + "is-installed-globally": "^0.4.0", + "is-npm": "^5.0.0", + "is-yarn-global": "^0.3.0", + "isomorphic-fetch": "^3.0.0", + "pupa": "^2.1.1", + "registry-auth-token": "^5.0.1", + "registry-url": "^5.1.0", + "semver": "^7.3.7", + "semver-diff": "^3.1.1", + "xdg-basedir": "^4.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/firebase-tools/node_modules/update-notifier-cjs/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==", + "devOptional": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/firebase-tools/node_modules/update-notifier-cjs/node_modules/boxen": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/boxen/-/boxen-5.1.2.tgz", + "integrity": "sha512-9gYgQKXx+1nP8mP7CzFyaUARhg7D3n1dF/FnErWmu9l6JvGpNUN278h0aSb+QjoiKSWG+iZ3uHrcqk0qrY9RQQ==", + "devOptional": true, + "dependencies": { + "ansi-align": "^3.0.0", + "camelcase": "^6.2.0", + "chalk": "^4.1.0", + "cli-boxes": "^2.2.1", + "string-width": "^4.2.2", + "type-fest": "^0.20.2", + "widest-line": "^3.1.0", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/firebase-tools/node_modules/update-notifier-cjs/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "devOptional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/firebase-tools/node_modules/update-notifier-cjs/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "devOptional": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/firebase-tools/node_modules/update-notifier-cjs/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==", + "devOptional": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/firebase-tools/node_modules/update-notifier-cjs/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==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/update-notifier-cjs/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==", + "devOptional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/update-notifier-cjs/node_modules/registry-auth-token": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-5.0.1.tgz", + "integrity": "sha512-UfxVOj8seK1yaIOiieV4FIP01vfBDLsY0H9sQzi9EbbUdJiuuBjJgLa1DpImXMNPnVkBD4eVxTEXcrZA6kfpJA==", + "devOptional": true, + "dependencies": { + "@pnpm/npm-conf": "^1.0.4" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/firebase-tools/node_modules/update-notifier-cjs/node_modules/semver": { + "version": "7.3.8", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", + "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", + "devOptional": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/firebase-tools/node_modules/update-notifier-cjs/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==", + "devOptional": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/update-notifier-cjs/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==", + "devOptional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/firebase-tools/node_modules/uri-js": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", + "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", + "devOptional": true, + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/firebase-tools/node_modules/url-join": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/url-join/-/url-join-0.0.1.tgz", + "integrity": "sha1-HbSK1CLTQCRpqH99l73r/k+x48g=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=", + "devOptional": true, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/firebase-tools/node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "devOptional": true, + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/firebase-tools/node_modules/valid-url": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/valid-url/-/valid-url-1.0.9.tgz", + "integrity": "sha1-HBRHm0DxOXp1eC8RXkCGRHQzogA=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=", + "devOptional": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/firebase-tools/node_modules/verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", + "devOptional": true, + "engines": [ + "node >=0.6.0" + ], + "dependencies": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + } + }, + "node_modules/firebase-tools/node_modules/vm2": { + "version": "3.9.17", + "resolved": "https://registry.npmjs.org/vm2/-/vm2-3.9.17.tgz", + "devOptional": true, + "dependencies": { + "acorn": "^8.7.0", + "acorn-walk": "^8.2.0" + }, + "bin": { + "vm2": "bin/vm2" + }, + "engines": { + "node": ">=6.0" + } + }, + "node_modules/firebase-tools/node_modules/wcwidth": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", + "integrity": "sha1-8LDc+RW8X/FSivrbLA4XtTLaL+g=", + "devOptional": true, + "dependencies": { + "defaults": "^1.0.3" + } + }, + "node_modules/firebase-tools/node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha1-lmRU6HZUYuN2RNNib2dCzotwll0=", + "devOptional": true, + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/firebase-tools/node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "devOptional": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, + "node_modules/firebase-tools/node_modules/wide-align": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", + "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", + "optional": true, + "dependencies": { + "string-width": "^1.0.2 || 2 || 3 || 4" + } + }, + "node_modules/firebase-tools/node_modules/widest-line": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-3.1.0.tgz", + "integrity": "sha512-NsmoXalsWVDMGupxZ5R08ka9flZjjiLvHVAWYOKtiKM8ujtZWr9cRffak+uSE48+Ob8ObalXpwyeUiyDD6QFgg==", + "devOptional": true, + "dependencies": { + "string-width": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/winston": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/winston/-/winston-3.2.1.tgz", + "integrity": "sha512-zU6vgnS9dAWCEKg/QYigd6cgMVVNwyTzKs81XZtTFuRwJOcDdBg7AU0mXVyNbs7O5RH2zdv+BdNZUlx7mXPuOw==", + "devOptional": true, + "dependencies": { + "async": "^2.6.1", + "diagnostics": "^1.1.1", + "is-stream": "^1.1.0", + "logform": "^2.1.1", + "one-time": "0.0.4", + "readable-stream": "^3.1.1", + "stack-trace": "0.0.x", + "triple-beam": "^1.3.0", + "winston-transport": "^4.3.0" + }, + "engines": { + "node": ">= 6.4.0" + } + }, + "node_modules/firebase-tools/node_modules/winston-transport": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.4.0.tgz", + "integrity": "sha512-Lc7/p3GtqtqPBYYtS6KCN3c77/2QCev51DvcJKbkFPQNoj1sinkGwLGFDxkXY9J6p9+EPnYs+D90uwbnaiURTw==", + "devOptional": true, + "dependencies": { + "readable-stream": "^2.3.7", + "triple-beam": "^1.2.0" + }, + "engines": { + "node": ">= 6.4.0" + } + }, + "node_modules/firebase-tools/node_modules/winston-transport/node_modules/readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "devOptional": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/firebase-tools/node_modules/word-wrap": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", + "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", + "devOptional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/firebase-tools/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "devOptional": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/firebase-tools/node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "devOptional": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/firebase-tools/node_modules/wrap-ansi/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "devOptional": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/firebase-tools/node_modules/wrap-ansi/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/write-file-atomic": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz", + "integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==", + "devOptional": true, + "dependencies": { + "imurmurhash": "^0.1.4", + "is-typedarray": "^1.0.0", + "signal-exit": "^3.0.2", + "typedarray-to-buffer": "^3.1.5" + } + }, + "node_modules/firebase-tools/node_modules/ws": { + "version": "7.4.6", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.6.tgz", + "integrity": "sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A==", + "devOptional": true, + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/firebase-tools/node_modules/xdg-basedir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-4.0.0.tgz", + "integrity": "sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q==", + "devOptional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/xmlcreate": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/xmlcreate/-/xmlcreate-2.0.4.tgz", + "integrity": "sha512-nquOebG4sngPmGPICTS5EnxqhKbCmz5Ox5hsszI2T6U5qdrJizBc+0ilYSEjTSzU0yZcmvppztXe/5Al5fUwdg==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/xregexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/xregexp/-/xregexp-2.0.0.tgz", + "integrity": "sha1-UqY+VsoLhKfzpfPWGHLxJq16WUM=", + "devOptional": true, + "engines": { + "node": "*" + } + }, + "node_modules/firebase-tools/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "devOptional": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/firebase-tools/node_modules/yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "devOptional": true, + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/firebase-tools/node_modules/yargs-parser": { + "version": "20.2.4", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz", + "integrity": "sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==", + "devOptional": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/firebase-tools/node_modules/yargs/node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "devOptional": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/firebase-tools/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==", + "devOptional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/firebase-tools/node_modules/zip-stream": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-4.1.0.tgz", + "integrity": "sha512-zshzwQW7gG7hjpBlgeQP9RuyPGNxvJdzR8SUM3QhxCnLjWN2E7j3dOvpeDcQoETfHx0urRS7EtmVToql7YpU4A==", + "devOptional": true, + "dependencies": { + "archiver-utils": "^2.1.0", + "compress-commons": "^4.1.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">= 10" + } + }, + "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.3.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.2.tgz", + "integrity": "sha512-AiwGJM8YcNOaobumgtng+6NHuOqC3A7MixFeDafM3X9cIUM+xUXoS5Vfgf+OihAYe20fxqNM9yPBXJzRtZ/4eA==", + "dev": true, + "license": "ISC" + }, + "node_modules/fn.name": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz", + "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==", + "license": "MIT" + }, + "node_modules/follow-redirects": { + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/foreground-child": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", + "integrity": "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==", + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.0", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/foreground-child/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fraction.js": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", + "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "patreon", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/fs-minipass": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-3.0.3.tgz", + "integrity": "sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/fs-minipass/node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/fs-monkey": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.0.6.tgz", + "integrity": "sha512-b1FMfwetIKymC0eioW7mTywihSQE4oLzQn1dB6rZB5fx/3NpNEdAWeCSMB+60/AeT0TCXsxzAlcYVEFCTAksWg==", + "dev": true, + "license": "Unlicense" + }, + "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/fuzzy": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/fuzzy/-/fuzzy-0.1.3.tgz", + "integrity": "sha512-/gZffu4ykarLrCiP3Ygsa86UAo1E5vEVlvTrpkKywXSbP9Xhln3oSp9QSV57gEq3JFFpGJ4GZ+5zdEp3FcUh4w==", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/gauge": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-4.0.4.tgz", + "integrity": "sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg==", + "deprecated": "This package is no longer supported.", + "dev": true, + "license": "ISC", + "dependencies": { + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.3", + "console-control-strings": "^1.1.0", + "has-unicode": "^2.0.1", + "signal-exit": "^3.0.7", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.5" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "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-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.7.tgz", + "integrity": "sha512-VW6Pxhsrk0KAOqs3WEd0klDiF/+V7gQOpAvY1jVU/LHmaD/kQO4523aiJuikX/QAKYiW6x8Jh+RJej1almdtCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "function-bind": "^1.1.2", + "get-proto": "^1.0.0", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "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/glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "license": "BSD-2-Clause" + }, + "node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/globby": { + "version": "13.2.2", + "resolved": "https://registry.npmjs.org/globby/-/globby-13.2.2.tgz", + "integrity": "sha512-Y1zNGV+pzQdh7H39l9zgB4PJqjRNqydvdYCDG4HFXM4XuvSaQQlEc91IU1yALL8gUTDomgBAfz3XJdmUS+oo0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "dir-glob": "^3.0.1", + "fast-glob": "^3.3.0", + "ignore": "^5.2.4", + "merge2": "^1.4.1", + "slash": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/good-listener": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/good-listener/-/good-listener-1.2.2.tgz", + "integrity": "sha512-goW1b+d9q/HIwbVYZzZ6SsTr4IgE+WA44A0GmPIQstuOrgsFcT7VEJ48nmr9GaRtNu0XTKacFLGnBPAM6Afouw==", + "license": "MIT", + "dependencies": { + "delegate": "^3.1.2" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, + "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/gts": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/gts/-/gts-5.3.1.tgz", + "integrity": "sha512-P9F+krJkGOkisUX+P9pfUas1Xy+U+CxBFZT62uInkJbgvZpnW1ug/pIcMJJmLOthMq1J88lpQUGhXDC9UTvVcw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@typescript-eslint/eslint-plugin": "5.62.0", + "@typescript-eslint/parser": "5.62.0", + "chalk": "^4.1.2", + "eslint": "8.57.0", + "eslint-config-prettier": "9.1.0", + "eslint-plugin-n": "15.7.0", + "eslint-plugin-prettier": "5.1.3", + "execa": "^5.0.0", + "inquirer": "^7.3.3", + "json5": "^2.1.3", + "meow": "^9.0.0", + "ncp": "^2.0.0", + "prettier": "3.2.5", + "rimraf": "3.0.2", + "write-file-atomic": "^4.0.0" + }, + "bin": { + "gts": "build/src/cli.js" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "typescript": ">=3" + } + }, + "node_modules/gts/node_modules/inquirer": { + "version": "7.3.3", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-7.3.3.tgz", + "integrity": "sha512-JG3eIAj5V9CwcGvuOmoo6LB9kbAYT8HXffUl6memuszlwDC/qvFAJw49XJ5NROSFNPxp3iQg1GqkFhaY/CR0IA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-escapes": "^4.2.1", + "chalk": "^4.1.0", + "cli-cursor": "^3.1.0", + "cli-width": "^3.0.0", + "external-editor": "^3.0.3", + "figures": "^3.0.0", + "lodash": "^4.17.19", + "mute-stream": "0.0.8", + "run-async": "^2.4.0", + "rxjs": "^6.6.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0", + "through": "^2.3.6" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/gts/node_modules/prettier": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.5.tgz", + "integrity": "sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/gts/node_modules/rxjs": { + "version": "6.6.7", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", + "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^1.9.0" + }, + "engines": { + "npm": ">=2.0.0" + } + }, + "node_modules/gts/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true, + "license": "0BSD" + }, + "node_modules/handle-thing": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz", + "integrity": "sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg==", + "dev": true, + "license": "MIT" + }, + "node_modules/hard-rejection": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/hard-rejection/-/hard-rejection-2.1.0.tgz", + "integrity": "sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "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==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==", + "dev": true, + "license": "ISC" + }, + "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/hdr-histogram-js": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/hdr-histogram-js/-/hdr-histogram-js-2.0.3.tgz", + "integrity": "sha512-Hkn78wwzWHNCp2uarhzQ2SGFLU3JY8SBDDd3TAABK4fc30wm+MuPOrg5QVFVfkKOQd6Bfz3ukJEI+q9sXEkK1g==", + "dev": true, + "license": "BSD", + "dependencies": { + "@assemblyscript/loader": "^0.10.1", + "base64-js": "^1.2.0", + "pako": "^1.0.3" + } + }, + "node_modules/hdr-histogram-percentiles-obj": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hdr-histogram-percentiles-obj/-/hdr-histogram-percentiles-obj-3.0.0.tgz", + "integrity": "sha512-7kIufnBqdsBGcSZLPJwqHT3yhk1QTsSlFsVD3kx5ixH/AlgBs9yM1q6DPhXZ8f8gtdqgh7N7/5btRLpQsS2gHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/hosted-git-info": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-6.1.3.tgz", + "integrity": "sha512-HVJyzUrLIL1c0QmviVh5E8VGyUS7xCFPS6yydaVd1UegW+ibV/CohqTH9MkOLDp5o+rb82DMo77PTuc9F/8GKw==", + "dev": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^7.5.1" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/hosted-git-info/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/hpack.js": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz", + "integrity": "sha512-zJxVehUdMGIKsRaNt7apO2Gqp0BdqW5yaiGHXXmbpvxgBYVZnAql+BJb4RO5ad2MgpbZKn5G6nMnegrH1FcNYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.1", + "obuf": "^1.0.0", + "readable-stream": "^2.0.1", + "wbuf": "^1.1.0" + } + }, + "node_modules/hpack.js/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/hpack.js/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/hpack.js/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/html-entities": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.5.2.tgz", + "integrity": "sha512-K//PSRMQk4FZ78Kyau+mZurHn3FH0Vwr+H36eE0rPbeYkRRi9YxceYPhuN60UwWorxyKHhqoAJl2OFKa4BVtaA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/mdevils" + }, + { + "type": "patreon", + "url": "https://patreon.com/mdevils" + } + ], + "license": "MIT" + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, + "node_modules/http-cache-semantics": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", + "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/http-deceiver": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz", + "integrity": "sha512-LmpOGxTfbpgtGVxJrj5k7asXHCgNZp5nLfp+hWc8QQRqtb7fUy6kRY3BO1h9ddF6yIPYUARgxGOwB42DnxIaNw==", + "dev": true, + "license": "MIT" + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/http-errors/node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/http-parser-js": { + "version": "0.5.9", + "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.9.tgz", + "integrity": "sha512-n1XsPy3rXVxlqxVioEWdC+0+M+SQw0DpJynwtOPo1X+ZlvdzTLtDBIJJlDQTnwZIFJrZSzSGmIOUdP8tu+SgLw==", + "license": "MIT" + }, + "node_modules/http-proxy": { + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", + "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eventemitter3": "^4.0.0", + "follow-redirects": "^1.0.0", + "requires-port": "^1.0.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/http-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", + "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tootallnate/once": "2", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/http-proxy-middleware": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.7.tgz", + "integrity": "sha512-fgVY8AV7qU7z/MmXJ/rxwbrtQH4jBQ9m7kp3llF0liB7glmFeVZFBepQb32T3y8n8k2+AEYuMPCpinYW+/CuRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-proxy": "^1.17.8", + "http-proxy": "^1.18.1", + "is-glob": "^4.0.1", + "is-plain-obj": "^3.0.0", + "micromatch": "^4.0.2" + }, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "@types/express": "^4.17.13" + }, + "peerDependenciesMeta": { + "@types/express": { + "optional": true + } + } + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/humanize-ms": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", + "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.0.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/icss-utils": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz", + "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/idb": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/idb/-/idb-7.1.1.tgz", + "integrity": "sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ==", + "license": "ISC" + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "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": "BSD-3-Clause" + }, + "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/ignore-walk": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-6.0.5.tgz", + "integrity": "sha512-VuuG0wCnjhnylG1ABXT3dAuIpTNDs/G8jlpmwXY03fXoXy/8ZK8/T+hMzt8L4WnrLCJgdybqgPagnF/f97cg3A==", + "dev": true, + "license": "ISC", + "dependencies": { + "minimatch": "^9.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/ignore-walk/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/image-size": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/image-size/-/image-size-0.5.5.tgz", + "integrity": "sha512-6TDAlDPZxUFCv+fuOkIoXT/V/f3Qbq8e37p+YOiYrUv3v9cc3/6x78VdfPgFVaB9dZYeLUfKgHRebpkm/oP2VQ==", + "dev": true, + "license": "MIT", + "optional": true, + "bin": { + "image-size": "bin/image-size.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/immutable": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.7.tgz", + "integrity": "sha512-1hqclzwYwjRDFLjcFxOM5AYkkG0rpFPpr1RLPMEuGczoS7YA8gLhy8SWXYRAA/XwfEHpfo3cw5JGioS32fnMRw==", + "dev": true, + "license": "MIT" + }, + "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/import-fresh/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/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/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/infer-owner": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/infer-owner/-/infer-owner-1.0.4.tgz", + "integrity": "sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==", + "dev": true, + "license": "ISC" + }, + "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==", + "license": "ISC" + }, + "node_modules/ini": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ini/-/ini-3.0.1.tgz", + "integrity": "sha512-it4HyVAUTKBc6m8e1iXWvXSTdndF7HbdN713+kvLrymxTaU4AUBWrJ4vEooP+V7fexnVD3LKcBshjGGPefSMUQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/inquirer": { + "version": "8.2.4", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-8.2.4.tgz", + "integrity": "sha512-nn4F01dxU8VeKfq192IjLsxu0/OmMZ4Lg3xKAns148rCaXP6ntAoEkVYZThWjwON8AlzdZZi6oqnhNbxUG9hVg==", + "license": "MIT", + "dependencies": { + "ansi-escapes": "^4.2.1", + "chalk": "^4.1.1", + "cli-cursor": "^3.1.0", + "cli-width": "^3.0.0", + "external-editor": "^3.0.3", + "figures": "^3.0.0", + "lodash": "^4.17.21", + "mute-stream": "0.0.8", + "ora": "^5.4.1", + "run-async": "^2.4.0", + "rxjs": "^7.5.5", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0", + "through": "^2.3.6", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/inquirer-autocomplete-prompt": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/inquirer-autocomplete-prompt/-/inquirer-autocomplete-prompt-1.4.0.tgz", + "integrity": "sha512-qHgHyJmbULt4hI+kCmwX92MnSxDs/Yhdt4wPA30qnoa01OF6uTXV8yvH4hKXgdaTNmkZ9D01MHjqKYEuJN+ONw==", + "license": "ISC", + "dependencies": { + "ansi-escapes": "^4.3.1", + "chalk": "^4.0.0", + "figures": "^3.2.0", + "run-async": "^2.4.0", + "rxjs": "^6.6.2" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "inquirer": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/inquirer-autocomplete-prompt/node_modules/rxjs": { + "version": "6.6.7", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", + "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^1.9.0" + }, + "engines": { + "npm": ">=2.0.0" + } + }, + "node_modules/inquirer-autocomplete-prompt/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "license": "0BSD" + }, + "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", + "optional": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/ionic-emoji-rating": { + "version": "1.0.15", + "resolved": "https://registry.npmjs.org/ionic-emoji-rating/-/ionic-emoji-rating-1.0.15.tgz", + "integrity": "sha512-ooNInojKERSgfoMZOAWvLm0MAARmAh1ibGr32x9S98J1jv2RC9DdaXOBWf/Xx9ZfJmvUAgZKjufcriszGcuZFA==", + "license": "MIT" + }, + "node_modules/ionicons": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/ionicons/-/ionicons-7.4.0.tgz", + "integrity": "sha512-ZK94MMqgzMCPPMhmk8Ouu6goyVHFIlw/ACP6oe3FrikcI0N7CX0xcwVaEbUc0G/v3W0shI93vo+9ve/KpvcNhQ==", + "license": "MIT", + "dependencies": { + "@stencil/core": "^4.0.3" + } + }, + "node_modules/ip-address": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz", + "integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "jsbn": "1.1.0", + "sprintf-js": "^1.1.3" + }, + "engines": { + "node": ">= 12" + } + }, + "node_modules/ip-address/node_modules/sprintf-js": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/ipaddr.js": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.2.0.tgz", + "integrity": "sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true, + "license": "MIT" + }, + "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-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "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-interactive": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", + "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-lambda": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-lambda/-/is-lambda-1.0.1.tgz", + "integrity": "sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==", + "dev": true, + "license": "MIT" + }, + "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": "3.0.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-3.0.0.tgz", + "integrity": "sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dev": true, + "license": "MIT", + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-what": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/is-what/-/is-what-3.14.1.tgz", + "integrity": "sha512-sNxgpk9793nzSs7bA6JQJGeIuRBQhAaNGG77kzYQgMkrID+lS6SlK07K5LaptscDlSaIgH+GPFzf+d75FVxozA==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "license": "MIT", + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/isbinaryfile": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-4.0.10.tgz", + "integrity": "sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/gjtorikian/" + } + }, + "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/isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", + "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument/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/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report/node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/istanbul-reports": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", + "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "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/jasmine-core": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-4.5.0.tgz", + "integrity": "sha512-9PMzyvhtocxb3aXJVOPqBDswdgyAeSB81QnLop4npOpbqnheaTEwPc9ZloQeVswugPManznQBjD8kWDTjlnHuw==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-worker": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", + "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "license": "MIT", + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsbn": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz", + "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "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==", + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonc-parser": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.0.tgz", + "integrity": "sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==", + "license": "MIT" + }, + "node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "license": "MIT", + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/jsonparse": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz", + "integrity": "sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==", + "dev": true, + "engines": [ + "node >= 0.2.0" + ], + "license": "MIT" + }, + "node_modules/karma": { + "version": "6.4.4", + "resolved": "https://registry.npmjs.org/karma/-/karma-6.4.4.tgz", + "integrity": "sha512-LrtUxbdvt1gOpo3gxG+VAJlJAEMhbWlM4YrFQgql98FwF7+K8K12LYO4hnDdUkNjeztYrOXEMqgTajSWgmtI/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@colors/colors": "1.5.0", + "body-parser": "^1.19.0", + "braces": "^3.0.2", + "chokidar": "^3.5.1", + "connect": "^3.7.0", + "di": "^0.0.1", + "dom-serialize": "^2.2.1", + "glob": "^7.1.7", + "graceful-fs": "^4.2.6", + "http-proxy": "^1.18.1", + "isbinaryfile": "^4.0.8", + "lodash": "^4.17.21", + "log4js": "^6.4.1", + "mime": "^2.5.2", + "minimatch": "^3.0.4", + "mkdirp": "^0.5.5", + "qjobs": "^1.2.0", + "range-parser": "^1.2.1", + "rimraf": "^3.0.2", + "socket.io": "^4.7.2", + "source-map": "^0.6.1", + "tmp": "^0.2.1", + "ua-parser-js": "^0.7.30", + "yargs": "^16.1.1" + }, + "bin": { + "karma": "bin/karma" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/karma-chrome-launcher": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/karma-chrome-launcher/-/karma-chrome-launcher-3.1.1.tgz", + "integrity": "sha512-hsIglcq1vtboGPAN+DGCISCFOxW+ZVnIqhDQcCMqqCp+4dmJ0Qpq5QAjkbA0X2L9Mi6OBkHi2Srrbmm7pUKkzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "which": "^1.2.1" + } + }, + "node_modules/karma-coverage": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/karma-coverage/-/karma-coverage-2.2.1.tgz", + "integrity": "sha512-yj7hbequkQP2qOSb20GuNSIyE//PgJWHwC2IydLE6XRtsnaflv+/OSGNssPjobYUlhVVagy99TQpqUt3vAUG7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "istanbul-lib-coverage": "^3.2.0", + "istanbul-lib-instrument": "^5.1.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.1", + "istanbul-reports": "^3.0.5", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/karma-coverage/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/karma-coverage/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/karma-jasmine": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/karma-jasmine/-/karma-jasmine-5.1.0.tgz", + "integrity": "sha512-i/zQLFrfEpRyQoJF9fsCdTMOF5c2dK7C7OmsuKg2D0YSsuZSfQDiLuaiktbuio6F2wiCsZSnSnieIQ0ant/uzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "jasmine-core": "^4.1.0" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "karma": "^6.0.0" + } + }, + "node_modules/karma-jasmine-html-reporter": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/karma-jasmine-html-reporter/-/karma-jasmine-html-reporter-2.0.0.tgz", + "integrity": "sha512-SB8HNNiazAHXM1vGEzf8/tSyEhkfxuDdhYdPBX2Mwgzt0OuF2gicApQ+uvXLID/gXyJQgvrM9+1/2SxZFUUDIA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "jasmine-core": "^4.0.0", + "karma": "^6.0.0", + "karma-jasmine": "^5.0.0" + } + }, + "node_modules/karma-source-map-support": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/karma-source-map-support/-/karma-source-map-support-1.4.0.tgz", + "integrity": "sha512-RsBECncGO17KAoJCYXjv+ckIz+Ii9NCi+9enk+rq6XC81ezYkb4/RHE6CTXdA7IOJqoF3wcaLfVG0CPmE5ca6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "source-map-support": "^0.5.5" + } + }, + "node_modules/karma/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/karma/node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/karma/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "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/karma/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/karma/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/karma/node_modules/tmp": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.3.tgz", + "integrity": "sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.14" + } + }, + "node_modules/karma/node_modules/yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/karma/node_modules/yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/katex": { + "version": "0.16.21", + "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.21.tgz", + "integrity": "sha512-XvqR7FgOHtWupfMiigNzmh+MgUVmDGU2kXZm899ZkPfcuoPuFxyHmXsgATDpFZDAXCI8tvinaVcDo8PIIJSo4A==", + "funding": [ + "https://opencollective.com/katex", + "https://github.com/sponsors/katex" + ], + "license": "MIT", + "optional": true, + "dependencies": { + "commander": "^8.3.0" + }, + "bin": { + "katex": "cli.js" + } + }, + "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/khroma": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/khroma/-/khroma-2.1.0.tgz", + "integrity": "sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw==", + "optional": true + }, + "node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/klona": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/klona/-/klona-2.0.6.tgz", + "integrity": "sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/kuler": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz", + "integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==", + "license": "MIT" + }, + "node_modules/layout-base": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/layout-base/-/layout-base-1.0.2.tgz", + "integrity": "sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg==", + "license": "MIT", + "optional": true + }, + "node_modules/less": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/less/-/less-4.1.3.tgz", + "integrity": "sha512-w16Xk/Ta9Hhyei0Gpz9m7VS8F28nieJaL/VyShID7cYvP6IL5oHeL6p4TXSDJqZE/lNv0oJ2pGVjJsRkfwm5FA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "copy-anything": "^2.0.1", + "parse-node-version": "^1.0.1", + "tslib": "^2.3.0" + }, + "bin": { + "lessc": "bin/lessc" + }, + "engines": { + "node": ">=6" + }, + "optionalDependencies": { + "errno": "^0.1.1", + "graceful-fs": "^4.1.2", + "image-size": "~0.5.0", + "make-dir": "^2.1.0", + "mime": "^1.4.1", + "needle": "^3.1.0", + "source-map": "~0.6.0" + } + }, + "node_modules/less-loader": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/less-loader/-/less-loader-11.1.0.tgz", + "integrity": "sha512-C+uDBV7kS7W5fJlUjq5mPBeBVhYpTIm5gB09APT9o3n/ILeaXVsiSFTbZpTJCJwQ/Crczfn3DmfQFwxYusWFug==", + "dev": true, + "license": "MIT", + "dependencies": { + "klona": "^2.0.4" + }, + "engines": { + "node": ">= 14.15.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "less": "^3.5.0 || ^4.0.0", + "webpack": "^5.0.0" + } + }, + "node_modules/less/node_modules/make-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", + "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "pify": "^4.0.1", + "semver": "^5.6.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/less/node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true, + "license": "MIT", + "optional": true, + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/less/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "license": "ISC", + "optional": true, + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/less/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "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/license-webpack-plugin": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/license-webpack-plugin/-/license-webpack-plugin-4.0.2.tgz", + "integrity": "sha512-771TFWFD70G1wLTC4oU2Cw4qvtmNrIw+wRvBtn+okgHl7slJVi7zfNcdmqDL72BojM30VNJ2UHylr1o77U37Jw==", + "dev": true, + "license": "ISC", + "dependencies": { + "webpack-sources": "^3.0.0" + }, + "peerDependenciesMeta": { + "webpack": { + "optional": true + }, + "webpack-sources": { + "optional": true + } + } + }, + "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/loader-runner": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", + "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==", + "license": "MIT", + "engines": { + "node": ">=6.11.5" + } + }, + "node_modules/loader-utils": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-3.2.1.tgz", + "integrity": "sha512-ZvFw1KWS3GVyYBYb7qkmRM/WwL2TQQBxgCK62rlvm4WpVQ23Nb4tYjApUlfjrEGvOs7KHEsmyUn75OHZrJMWPw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12.13.0" + } + }, + "node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "license": "MIT" + }, + "node_modules/lodash-es": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", + "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==", + "license": "MIT", + "optional": true + }, + "node_modules/lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", + "license": "MIT" + }, + "node_modules/lodash.debounce": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", + "dev": true, + "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/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log4js": { + "version": "6.9.1", + "resolved": "https://registry.npmjs.org/log4js/-/log4js-6.9.1.tgz", + "integrity": "sha512-1somDdy9sChrr9/f4UlzhdaGfDR2c/SaD2a4T7qEkG4jTS57/B3qmnjLYePwQ8cqWnUHZI0iAKxMBpCZICiZ2g==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "date-format": "^4.0.14", + "debug": "^4.3.4", + "flatted": "^3.2.7", + "rfdc": "^1.3.0", + "streamroller": "^3.1.5" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/logform": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/logform/-/logform-2.7.0.tgz", + "integrity": "sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ==", + "license": "MIT", + "dependencies": { + "@colors/colors": "1.6.0", + "@types/triple-beam": "^1.3.2", + "fecha": "^4.2.0", + "ms": "^2.1.1", + "safe-stable-stringify": "^2.3.1", + "triple-beam": "^1.3.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/logform/node_modules/@colors/colors": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.6.0.tgz", + "integrity": "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==", + "license": "MIT", + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/long": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", + "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==", + "license": "Apache-2.0" + }, + "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/magic-string": { + "version": "0.29.0", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.29.0.tgz", + "integrity": "sha512-WcfidHrDjMY+eLjlU+8OvwREqHwpgCeKVBUpQ3OhYYuvfaYCUgcbuBzappNzZvg/v8onU3oQj+BYpkOJe9Iw4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.13" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/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/make-fetch-happen": { + "version": "10.2.1", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-10.2.1.tgz", + "integrity": "sha512-NgOPbRiaQM10DYXvN3/hhGVI2M5MtITFryzBGxHM5p4wnFxsVCbxkrBrDsk+EZ5OB4jEOT7AjDxtdF+KVEFT7w==", + "dev": true, + "license": "ISC", + "dependencies": { + "agentkeepalive": "^4.2.1", + "cacache": "^16.1.0", + "http-cache-semantics": "^4.1.0", + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.0", + "is-lambda": "^1.0.1", + "lru-cache": "^7.7.1", + "minipass": "^3.1.6", + "minipass-collect": "^1.0.2", + "minipass-fetch": "^2.0.3", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^0.6.3", + "promise-retry": "^2.0.1", + "socks-proxy-agent": "^7.0.0", + "ssri": "^9.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/make-fetch-happen/node_modules/@npmcli/fs": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-2.1.2.tgz", + "integrity": "sha512-yOJKRvohFOaLqipNtwYB9WugyZKhC/DZC4VYPmpaCzDBrA8YpK3qHZ8/HGscMnE4GqbkLNuVcCnxkeQEdGt6LQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "@gar/promisify": "^1.1.3", + "semver": "^7.3.5" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/make-fetch-happen/node_modules/cacache": { + "version": "16.1.3", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-16.1.3.tgz", + "integrity": "sha512-/+Emcj9DAXxX4cwlLmRI9c166RuL3w30zp4R7Joiv2cQTtTtA+jeuCAjH3ZlGnYS3tKENSrKhAzVVP9GVyzeYQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/fs": "^2.1.0", + "@npmcli/move-file": "^2.0.0", + "chownr": "^2.0.0", + "fs-minipass": "^2.1.0", + "glob": "^8.0.1", + "infer-owner": "^1.0.4", + "lru-cache": "^7.7.1", + "minipass": "^3.1.6", + "minipass-collect": "^1.0.2", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "mkdirp": "^1.0.4", + "p-map": "^4.0.0", + "promise-inflight": "^1.0.1", + "rimraf": "^3.0.2", + "ssri": "^9.0.0", + "tar": "^6.1.11", + "unique-filename": "^2.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/make-fetch-happen/node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/make-fetch-happen/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/make-fetch-happen/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/make-fetch-happen/node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true, + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/make-fetch-happen/node_modules/ssri": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-9.0.1.tgz", + "integrity": "sha512-o57Wcn66jMQvfHG1FlYbWeZWW/dHZhJXjpIcTfXldXEk5nz5lStPo3mK0OJQfGR3RbZUlbISexbljkJzuEj/8Q==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.1.1" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/make-fetch-happen/node_modules/unique-filename": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-2.0.1.tgz", + "integrity": "sha512-ODWHtkkdx3IAR+veKxFV+VBkUMcN+FaqzUUd7IZzt+0zhDZFPFxhlqwPF3YQvMHx1TD0tdgYl+kuPnJ8E6ql7A==", + "dev": true, + "license": "ISC", + "dependencies": { + "unique-slug": "^3.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/make-fetch-happen/node_modules/unique-slug": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-3.0.0.tgz", + "integrity": "sha512-8EyMynh679x/0gqE9fT9oilG+qEt+ibFyqjuVTsZn1+CMxH+XLlpvr2UZx4nVcCwTpx81nICr2JQFkM+HPLq4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/make-fetch-happen/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/map-obj": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-4.3.0.tgz", + "integrity": "sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/marked": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/marked/-/marked-4.3.0.tgz", + "integrity": "sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A==", + "license": "MIT", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 12" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/memfs": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/memfs/-/memfs-3.5.3.tgz", + "integrity": "sha512-UERzLsxzllchadvbPs5aolHh65ISpKpM+ccLbOJ8/vvpBKmAWf+la7dXFy7Mr0ySHbdHrFv5kGFCUHHe6GFEmw==", + "dev": true, + "license": "Unlicense", + "dependencies": { + "fs-monkey": "^1.0.4" + }, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/meow": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/meow/-/meow-9.0.0.tgz", + "integrity": "sha512-+obSblOQmRhcyBt62furQqRAQpNyWXo8BuQ5bN7dG8wmwQ+vwHKp/rCFD4CrTP8CsDQD1sjoZ94K417XEUk8IQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/minimist": "^1.2.0", + "camelcase-keys": "^6.2.2", + "decamelize": "^1.2.0", + "decamelize-keys": "^1.1.0", + "hard-rejection": "^2.1.0", + "minimist-options": "4.1.0", + "normalize-package-data": "^3.0.0", + "read-pkg-up": "^7.0.1", + "redent": "^3.0.0", + "trim-newlines": "^3.0.0", + "type-fest": "^0.18.0", + "yargs-parser": "^20.2.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/meow/node_modules/hosted-git-info": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz", + "integrity": "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==", + "dev": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/meow/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/meow/node_modules/normalize-package-data": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-3.0.3.tgz", + "integrity": "sha512-p2W1sgqij3zMMyRC067Dg16bfzVH+w7hyegmpIvZ4JNjqtGOVAIvLmjBx3yP7YTe9vKJgkoNOPjwQGogDoMXFA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "hosted-git-info": "^4.0.1", + "is-core-module": "^2.5.0", + "semver": "^7.3.4", + "validate-npm-package-license": "^3.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/meow/node_modules/type-fest": { + "version": "0.18.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.18.1.tgz", + "integrity": "sha512-OIAYXk8+ISY+qTOwkHtKqzAuxchoMiD9Udx+FSGQDuiRR+PJKJHc2NJAXlbhkGwTt/4/nKZxELY1w3ReWOL8mw==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/meow/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/meow/node_modules/yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "license": "MIT" + }, + "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/mermaid": { + "version": "9.4.3", + "resolved": "https://registry.npmjs.org/mermaid/-/mermaid-9.4.3.tgz", + "integrity": "sha512-TLkQEtqhRSuEHSE34lh5bCa94KATCyluAXmFnNI2PRZwOpXFeqiJWwZl+d2CcemE1RS6QbbueSSq9QIg8Uxcyw==", + "license": "MIT", + "optional": true, + "dependencies": { + "@braintree/sanitize-url": "^6.0.0", + "cytoscape": "^3.23.0", + "cytoscape-cose-bilkent": "^4.1.0", + "cytoscape-fcose": "^2.1.0", + "d3": "^7.4.0", + "dagre-d3-es": "7.0.9", + "dayjs": "^1.11.7", + "dompurify": "2.4.3", + "elkjs": "^0.8.2", + "khroma": "^2.0.0", + "lodash-es": "^4.17.21", + "non-layered-tidy-tree-layout": "^2.0.2", + "stylis": "^4.1.2", + "ts-dedent": "^2.2.0", + "uuid": "^9.0.0", + "web-worker": "^1.2.0" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "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/mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/mini-css-extract-plugin": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.7.2.tgz", + "integrity": "sha512-EdlUizq13o0Pd+uCp+WO/JpkLvHRVGt97RqfeGhXqAcorYo1ypJSpkV+WDT0vY/kmh/p7wRdJNJtuyK540PXDw==", + "dev": true, + "license": "MIT", + "dependencies": { + "schema-utils": "^4.0.0" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" + } + }, + "node_modules/minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", + "dev": true, + "license": "ISC" + }, + "node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minimist-options": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/minimist-options/-/minimist-options-4.1.0.tgz", + "integrity": "sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "arrify": "^1.0.1", + "is-plain-obj": "^1.1.0", + "kind-of": "^6.0.3" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/minimist-options/node_modules/is-plain-obj": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", + "integrity": "sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/minipass": { + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-4.2.8.tgz", + "integrity": "sha512-fNzuVyifolSLFL4NzpF+wEF4qrgqaaKX0haXPQEdQ7NKAN+WecoKMHV09YcuL/DHxrUsYQOK3MiuDf7Ip2OXfQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-collect": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-1.0.2.tgz", + "integrity": "sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minipass-collect/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-collect/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/minipass-fetch": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-2.1.2.tgz", + "integrity": "sha512-LT49Zi2/WMROHYoqGgdlQIZh8mLPZmOrN2NdJjMXxYe4nkN6FUyuPuOAOedNJDrx0IRGg9+4guZewtp8hE6TxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^3.1.6", + "minipass-sized": "^1.0.3", + "minizlib": "^2.1.2" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + }, + "optionalDependencies": { + "encoding": "^0.1.13" + } + }, + "node_modules/minipass-fetch/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-fetch/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/minipass-flush": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz", + "integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minipass-flush/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-flush/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/minipass-json-stream": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/minipass-json-stream/-/minipass-json-stream-1.0.2.tgz", + "integrity": "sha512-myxeeTm57lYs8pH2nxPzmEEg8DGIgW+9mv6D4JZD2pa81I/OBjeU7PtICXV6c9eRGTA5JMDsuIPUZRCyBMYNhg==", + "dev": true, + "license": "MIT", + "dependencies": { + "jsonparse": "^1.3.1", + "minipass": "^3.0.0" + } + }, + "node_modules/minipass-json-stream/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-json-stream/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/minipass-pipeline": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", + "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-pipeline/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-pipeline/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/minipass-sized": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz", + "integrity": "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-sized/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-sized/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minizlib/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minizlib/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/moment": { + "version": "2.30.1", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", + "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/multicast-dns": { + "version": "7.2.5", + "resolved": "https://registry.npmjs.org/multicast-dns/-/multicast-dns-7.2.5.tgz", + "integrity": "sha512-2eznPJP8z2BFLX50tf0LuODrpINqP1RVIm/CObbTcBRITQgmC/TjcREF1NeTBzIcR5XO/ukWo+YHOjBbFwIupg==", + "dev": true, + "license": "MIT", + "dependencies": { + "dns-packet": "^5.2.2", + "thunky": "^1.0.2" + }, + "bin": { + "multicast-dns": "cli.js" + } + }, + "node_modules/mute-stream": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", + "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", + "license": "ISC" + }, + "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.8", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz", + "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==", + "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/natural-compare-lite": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz", + "integrity": "sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==", + "dev": true, + "license": "MIT" + }, + "node_modules/ncp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ncp/-/ncp-2.0.0.tgz", + "integrity": "sha512-zIdGUrPRFTUELUvr3Gmc7KZ2Sw/h1PiVM0Af/oHB6zgnV1ikqSfRk+TOufi79aHYCW3NiOXmr1BP5nWbzojLaA==", + "dev": true, + "license": "MIT", + "bin": { + "ncp": "bin/ncp" + } + }, + "node_modules/needle": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/needle/-/needle-3.3.1.tgz", + "integrity": "sha512-6k0YULvhpw+RoLNiQCRKOl09Rv1dPLr8hHnVjHqdolKwDrdNyk+Hmrthi4lIGPPz3r39dLx0hsF5s40sZ3Us4Q==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "iconv-lite": "^0.6.3", + "sax": "^1.2.4" + }, + "bin": { + "needle": "bin/needle" + }, + "engines": { + "node": ">= 4.4.x" + } + }, + "node_modules/needle/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/negotiator": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", + "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "license": "MIT" + }, + "node_modules/ng2-pdf-viewer": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/ng2-pdf-viewer/-/ng2-pdf-viewer-7.0.2.tgz", + "integrity": "sha512-yAd8OddmPCnkERv3IWFyZ1GxkMx+n2fc8NQQtWCjQbhr1sKjrP0TQH41AmgudSzTtvWJw9z784V+TKIaWA0eKw==", + "license": "MIT", + "dependencies": { + "pdfjs-dist": "~2.11.338", + "tslib": "^2.0.0" + }, + "peerDependencies": { + "pdfjs-dist": "~2.11.338" + } + }, + "node_modules/ngx-markdown": { + "version": "15.1.2", + "resolved": "https://registry.npmjs.org/ngx-markdown/-/ngx-markdown-15.1.2.tgz", + "integrity": "sha512-mAUORpUnHCV4tnxEHV4oS5YEdIaolUclulCblUrvAEU3AEND8MMTxlwHujqVC2M398/aKH0SBSrjLzDbMUJCoQ==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "optionalDependencies": { + "clipboard": "^2.0.11", + "emoji-toolkit": "^7.0.0", + "katex": "^0.16.0", + "mermaid": "^9.1.2", + "prismjs": "^1.28.0" + }, + "peerDependencies": { + "@angular/common": "^15.0.0", + "@angular/core": "^15.0.0", + "@angular/platform-browser": "^15.0.0", + "@types/marked": "^4.0.3", + "marked": "^4.0.17", + "rxjs": "^6.5.3 || ^7.4.0", + "zone.js": "~0.11.4 || ~0.12.0 || ~0.13.0" + } + }, + "node_modules/nice-napi": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/nice-napi/-/nice-napi-1.0.2.tgz", + "integrity": "sha512-px/KnJAJZf5RuBGcfD+Sp2pAKq0ytz8j+1NehvgIGFkvtvFrDM3T8E4x/JJODXK9WZow8RRGrbA9QQ3hs+pDhA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "!win32" + ], + "dependencies": { + "node-addon-api": "^3.0.0", + "node-gyp-build": "^4.2.2" + } + }, + "node_modules/node-addon-api": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-3.2.1.tgz", + "integrity": "sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-forge": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", + "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==", + "dev": true, + "license": "(BSD-3-Clause OR GPL-2.0)", + "engines": { + "node": ">= 6.13.0" + } + }, + "node_modules/node-gyp": { + "version": "9.4.1", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-9.4.1.tgz", + "integrity": "sha512-OQkWKbjQKbGkMf/xqI1jjy3oCTgMKJac58G2+bjZb3fza6gW2YrCSdMQYaoTb70crvE//Gngr4f0AgVHmqHvBQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "env-paths": "^2.2.0", + "exponential-backoff": "^3.1.1", + "glob": "^7.1.4", + "graceful-fs": "^4.2.6", + "make-fetch-happen": "^10.0.3", + "nopt": "^6.0.0", + "npmlog": "^6.0.0", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "tar": "^6.1.2", + "which": "^2.0.2" + }, + "bin": { + "node-gyp": "bin/node-gyp.js" + }, + "engines": { + "node": "^12.13 || ^14.13 || >=16" + } + }, + "node_modules/node-gyp-build": { + "version": "4.8.4", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", + "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==", + "dev": true, + "license": "MIT", + "optional": true, + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, + "node_modules/node-gyp/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/node-gyp/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "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/node-gyp/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/node-gyp/node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/node-releases": { + "version": "2.0.19", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", + "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", + "license": "MIT" + }, + "node_modules/non-layered-tidy-tree-layout": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/non-layered-tidy-tree-layout/-/non-layered-tidy-tree-layout-2.0.2.tgz", + "integrity": "sha512-gkXMxRzUH+PB0ax9dUN0yYF0S25BqeAYqhgMaLUFmpXLEk7Fcu8f4emJuOAY0V8kjDICxROIKsTAKsV/v355xw==", + "license": "MIT", + "optional": true + }, + "node_modules/nopt": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-6.0.0.tgz", + "integrity": "sha512-ZwLpbTgdhuZUnZzjd7nb1ZV+4DoiC6/sfiVKok72ym/4Tlf+DFdlHYmT2JPmcNNWV6Pi3SDf1kT+A4r9RTuT9g==", + "dev": true, + "license": "ISC", + "dependencies": { + "abbrev": "^1.0.0" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/normalize-package-data": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-5.0.0.tgz", + "integrity": "sha512-h9iPVIfrVZ9wVYQnxFgtw1ugSvGEMOlyPWWtm8BMJhnwyEL/FLbYbTY3V3PpjI/BUK67n9PEWDu6eHzu1fB15Q==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "hosted-git-info": "^6.0.0", + "is-core-module": "^2.8.1", + "semver": "^7.3.5", + "validate-npm-package-license": "^3.0.4" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "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/normalize-range": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-bundled": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-3.0.1.tgz", + "integrity": "sha512-+AvaheE/ww1JEwRHOrn4WHNzOxGtVp+adrg2AeZS/7KuxGUYFuBta98wYpfHBbJp6Tg6j1NKSEVHNcfZzJHQwQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "npm-normalize-package-bin": "^3.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm-install-checks": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/npm-install-checks/-/npm-install-checks-6.3.0.tgz", + "integrity": "sha512-W29RiK/xtpCGqn6f3ixfRYGk+zRyr+Ew9F2E20BfXxT5/euLdA/Nm7fO7OeTGuAmTs30cpgInyJ0cYe708YTZw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "semver": "^7.1.1" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm-normalize-package-bin": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-3.0.1.tgz", + "integrity": "sha512-dMxCf+zZ+3zeQZXKxmyuCKlIDPGuv8EF940xbkC4kQVDTtqoh6rJFO+JTKSA6/Rwi0getWmtuy4Itup0AMcaDQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm-package-arg": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-10.1.0.tgz", + "integrity": "sha512-uFyyCEmgBfZTtrKk/5xDfHp6+MdrqGotX/VoOyEEl3mBwiEE5FlBaePanazJSVMPT7vKepcjYBY2ztg9A3yPIA==", + "dev": true, + "license": "ISC", + "dependencies": { + "hosted-git-info": "^6.0.0", + "proc-log": "^3.0.0", + "semver": "^7.3.5", + "validate-npm-package-name": "^5.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm-packlist": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-7.0.4.tgz", + "integrity": "sha512-d6RGEuRrNS5/N84iglPivjaJPxhDbZmlbTwTDX2IbcRHG5bZCdtysYMhwiPvcF4GisXHGn7xsxv+GQ7T/02M5Q==", + "dev": true, + "license": "ISC", + "dependencies": { + "ignore-walk": "^6.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm-pick-manifest": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/npm-pick-manifest/-/npm-pick-manifest-8.0.1.tgz", + "integrity": "sha512-mRtvlBjTsJvfCCdmPtiu2bdlx8d/KXtF7yNXNWe7G0Z36qWA9Ny5zXsI2PfBZEv7SXgoxTmNaTzGSbbzDZChoA==", + "dev": true, + "license": "ISC", + "dependencies": { + "npm-install-checks": "^6.0.0", + "npm-normalize-package-bin": "^3.0.0", + "npm-package-arg": "^10.0.0", + "semver": "^7.3.5" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm-registry-fetch": { + "version": "14.0.5", + "resolved": "https://registry.npmjs.org/npm-registry-fetch/-/npm-registry-fetch-14.0.5.tgz", + "integrity": "sha512-kIDMIo4aBm6xg7jOttupWZamsZRkAqMqwqqbVXnUqstY5+tapvv6bkH/qMR76jdgV+YljEUCyWx3hRYMrJiAgA==", + "dev": true, + "license": "ISC", + "dependencies": { + "make-fetch-happen": "^11.0.0", + "minipass": "^5.0.0", + "minipass-fetch": "^3.0.0", + "minipass-json-stream": "^1.0.1", + "minizlib": "^2.1.2", + "npm-package-arg": "^10.0.0", + "proc-log": "^3.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm-registry-fetch/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/npm-registry-fetch/node_modules/make-fetch-happen": { + "version": "11.1.1", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-11.1.1.tgz", + "integrity": "sha512-rLWS7GCSTcEujjVBs2YqG7Y4643u8ucvCJeSRqiLYhesrDuzeuFIk37xREzAsfQaqzl8b9rNCE4m6J8tvX4Q8w==", + "dev": true, + "license": "ISC", + "dependencies": { + "agentkeepalive": "^4.2.1", + "cacache": "^17.0.0", + "http-cache-semantics": "^4.1.1", + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.0", + "is-lambda": "^1.0.1", + "lru-cache": "^7.7.1", + "minipass": "^5.0.0", + "minipass-fetch": "^3.0.0", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^0.6.3", + "promise-retry": "^2.0.1", + "socks-proxy-agent": "^7.0.0", + "ssri": "^10.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm-registry-fetch/node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=8" + } + }, + "node_modules/npm-registry-fetch/node_modules/minipass-fetch": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-3.0.5.tgz", + "integrity": "sha512-2N8elDQAtSnFV0Dk7gt15KHsS0Fyz6CbYZ360h0WTYV1Ty46li3rAXVOQj1THMNLdmrD9Vt5pBPtWtVkpwGBqg==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^7.0.3", + "minipass-sized": "^1.0.3", + "minizlib": "^2.1.2" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + }, + "optionalDependencies": { + "encoding": "^0.1.13" + } + }, + "node_modules/npm-registry-fetch/node_modules/minipass-fetch/node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npmlog": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-6.0.2.tgz", + "integrity": "sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg==", + "deprecated": "This package is no longer supported.", + "dev": true, + "license": "ISC", + "dependencies": { + "are-we-there-yet": "^3.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^4.0.3", + "set-blocking": "^2.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "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/object-inspect": { + "version": "1.13.3", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.3.tgz", + "integrity": "sha512-kDCGIbxkDSXE3euJZZXzc6to7fCrKHNI/hSRQnRuQ+BWjFNzZwiFF8fj/6o2t2G9/jTj8PSIYTfCLelLZEeRpA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/obuf": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz", + "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==", + "dev": true, + "license": "MIT" + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/on-headers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", + "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "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/one-time": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/one-time/-/one-time-1.0.0.tgz", + "integrity": "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==", + "license": "MIT", + "dependencies": { + "fn.name": "1.x.x" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/open": { + "version": "8.4.1", + "resolved": "https://registry.npmjs.org/open/-/open-8.4.1.tgz", + "integrity": "sha512-/4b7qZNhv6Uhd7jjnREh1NjnPxlTq+XNWPG88Ydkj5AILcA5m3ajvcg57pB24EQjKv0dK62XnDqk9c/hkIG5Kg==", + "license": "MIT", + "dependencies": { + "define-lazy-prop": "^2.0.0", + "is-docker": "^2.1.1", + "is-wsl": "^2.2.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "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/ora": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", + "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", + "license": "MIT", + "dependencies": { + "bl": "^4.1.0", + "chalk": "^4.1.0", + "cli-cursor": "^3.1.0", + "cli-spinners": "^2.5.0", + "is-interactive": "^1.0.0", + "is-unicode-supported": "^0.1.0", + "log-symbols": "^4.1.0", + "strip-ansi": "^6.0.0", + "wcwidth": "^1.0.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-map": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", + "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "aggregate-error": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-retry": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz", + "integrity": "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/retry": "0.12.0", + "retry": "^0.13.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-retry/node_modules/retry": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "license": "BlueOak-1.0.0" + }, + "node_modules/pacote": { + "version": "15.0.8", + "resolved": "https://registry.npmjs.org/pacote/-/pacote-15.0.8.tgz", + "integrity": "sha512-UlcumB/XS6xyyIMwg/WwMAyUmga+RivB5KgkRwA1hZNtrx+0Bt41KxHCvg1kr0pZ/ZeD8qjhW4fph6VaYRCbLw==", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/git": "^4.0.0", + "@npmcli/installed-package-contents": "^2.0.1", + "@npmcli/promise-spawn": "^6.0.1", + "@npmcli/run-script": "^6.0.0", + "cacache": "^17.0.0", + "fs-minipass": "^3.0.0", + "minipass": "^4.0.0", + "npm-package-arg": "^10.0.0", + "npm-packlist": "^7.0.0", + "npm-pick-manifest": "^8.0.0", + "npm-registry-fetch": "^14.0.0", + "proc-log": "^3.0.0", + "promise-retry": "^2.0.1", + "read-package-json": "^6.0.0", + "read-package-json-fast": "^3.0.0", + "ssri": "^10.0.0", + "tar": "^6.1.11" + }, + "bin": { + "pacote": "lib/bin.js" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "dev": true, + "license": "(MIT AND Zlib)" + }, + "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-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parse-node-version": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parse-node-version/-/parse-node-version-1.0.1.tgz", + "integrity": "sha512-3YHlOa/JgH6Mnpr05jP9eDG254US9ek25LyIxZlDItp2iJtwyaXQb57lBYLdT3MowkUFYEV2XXNAYIPlESvJlA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/parse5": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.2.1.tgz", + "integrity": "sha512-BuBYQYlv1ckiPdQi/ohiivi9Sagc9JG+Ozs0r7b/0iK3sKmrb0b9FdWdBbOdx6hBCM/F9Ir82ofnBhtZOjCRPQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "entities": "^4.5.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-html-rewriting-stream": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/parse5-html-rewriting-stream/-/parse5-html-rewriting-stream-7.0.0.tgz", + "integrity": "sha512-mazCyGWkmCRWDI15Zp+UiCqMp/0dgEmkZRvhlsqqKYr4SsVm/TvnSpD9fCvqCA2zoWJcfRym846ejWBBHRiYEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^4.3.0", + "parse5": "^7.0.0", + "parse5-sax-parser": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-html-rewriting-stream/node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/parse5-htmlparser2-tree-adapter": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-6.0.1.tgz", + "integrity": "sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "parse5": "^6.0.1" + } + }, + "node_modules/parse5-htmlparser2-tree-adapter/node_modules/parse5": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz", + "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==", + "dev": true, + "license": "MIT" + }, + "node_modules/parse5-sax-parser": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/parse5-sax-parser/-/parse5-sax-parser-7.0.0.tgz", + "integrity": "sha512-5A+v2SNsq8T6/mG3ahcz8ZtQ0OUFTatxPbeidoMB7tkJSGDY3tdfl4MHovtLQHkEn5CGxijNWRQHhRQ6IRpXKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5/node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "devOptional": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "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-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "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/path-scurry/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "license": "ISC" + }, + "node_modules/path-scurry/node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "dev": true, + "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/pdfjs-dist": { + "version": "2.11.338", + "resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-2.11.338.tgz", + "integrity": "sha512-Ti5VTB0VvSdtTtc7TG71ghMx0SEuNcEs4ghVuZxW0p6OqLjMc0xekZV1B+MmlxEG2Du2e5jgazucWIG/SXTcdA==", + "license": "Apache-2.0", + "peerDependencies": { + "worker-loader": "^3.0.8" + }, + "peerDependenciesMeta": { + "worker-loader": { + "optional": true + } + } + }, + "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.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=6" + } + }, + "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/piscina": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/piscina/-/piscina-3.2.0.tgz", + "integrity": "sha512-yn/jMdHRw+q2ZJhFhyqsmANcbF6V2QwmD84c6xRau+QpQOmtrBCoRGdvTfeuFDYXB5W2m6MfLkjkvQa9lUSmIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eventemitter-asyncresource": "^1.0.0", + "hdr-histogram-js": "^2.0.1", + "hdr-histogram-percentiles-obj": "^3.0.0" + }, + "optionalDependencies": { + "nice-napi": "^1.0.2" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/postcss": { + "version": "8.5.3", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz", + "integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==", + "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.8", + "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.0.1", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz", + "integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==", + "license": "MIT", + "dependencies": { + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": "^8.4.21" + } + }, + "node_modules/postcss-load-config": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz", + "integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "lilconfig": "^3.0.0", + "yaml": "^2.3.4" + }, + "engines": { + "node": ">= 14" + }, + "peerDependencies": { + "postcss": ">=8.0.9", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "postcss": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/postcss-load-config/node_modules/yaml": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.7.0.tgz", + "integrity": "sha512-+hSoy/QHluxmC9kCIJyL/uyFmLmc+e5CFR5Wa+bpIhIj85LVb9ZH2nVnqrHoSvKogwODv0ClqZkmiSSaIH5LTA==", + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/postcss-loader": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/postcss-loader/-/postcss-loader-7.0.2.tgz", + "integrity": "sha512-fUJzV/QH7NXUAqV8dWJ9Lg4aTkDCezpTS5HgJ2DvqznexTbSTxgi/dTECvTZ15BwKTtk8G/bqI/QTu2HPd3ZCg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cosmiconfig": "^7.0.0", + "klona": "^2.0.5", + "semver": "^7.3.8" + }, + "engines": { + "node": ">= 14.15.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "postcss": "^7.0.0 || ^8.0.1", + "webpack": "^5.0.0" + } + }, + "node_modules/postcss-modules-extract-imports": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.1.0.tgz", + "integrity": "sha512-k3kNe0aNFQDAZGbin48pL2VNidTF0w4/eASDsxlyspobzU3wZQLOGj7L9gfRe0Jo9/4uud09DsjFNH7winGv8Q==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-local-by-default": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.2.0.tgz", + "integrity": "sha512-5kcJm/zk+GJDSfw+V/42fJ5fhjL5YbFDl8nVdXkJPLLW+Vf9mTD5Xe0wqIaDnLuL2U6cDNpTr+UQ+v2HWIBhzw==", + "dev": true, + "license": "MIT", + "dependencies": { + "icss-utils": "^5.0.0", + "postcss-selector-parser": "^7.0.0", + "postcss-value-parser": "^4.1.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-scope": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.2.1.tgz", + "integrity": "sha512-m9jZstCVaqGjTAuny8MdgE88scJnCiQSlSrOWcTQgM2t32UBe+MUmFSO5t7VMSfAf/FJKImAxBav8ooCHJXCJA==", + "dev": true, + "license": "ISC", + "dependencies": { + "postcss-selector-parser": "^7.0.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-values": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz", + "integrity": "sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "icss-utils": "^5.0.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "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": "7.0.0", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.0.0.tgz", + "integrity": "sha512-9RbEr1Y7FFfptd/1eEdntyjMwLeghW1bHX9GWjXo19vx4ytPQhANltvVxDggzJl7mnWM+dX28kb6cyS/4iQjlQ==", + "dev": true, + "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/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/prettier": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.1.tgz", + "integrity": "sha512-hPpFQvHwL3Qv5AdRvBFMhnKo4tYxp0ReXiPn2bxkiohEX6mBeBwEpBSQTkD458RaaDKQMYSp4hX4UtfUTA5wDw==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prettier-linter-helpers": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz", + "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-diff": "^1.1.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/pretty-bytes": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz", + "integrity": "sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/prismjs": { + "version": "1.29.0", + "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.29.0.tgz", + "integrity": "sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/proc-log": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-3.0.0.tgz", + "integrity": "sha512-++Vn7NS4Xf9NacaU9Xq3URUuqZETPsf8L4j5/ckhaRYsfPeRyzGw+iDjFhV/Jr3uNmTvvddEJFWh5R1gRgUH8A==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true, + "license": "MIT" + }, + "node_modules/promise-inflight": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", + "integrity": "sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/promise-retry": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", + "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "err-code": "^2.0.2", + "retry": "^0.12.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/protobufjs": { + "version": "6.11.4", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.11.4.tgz", + "integrity": "sha512-5kQWPaJHi1WoCpjTGszzQ32PG2F4+wRY6BmAT4Vfw56Q2FZ4YZzK20xUYQH4YkfehY1e6QSICrJquM6xXZNcrw==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/long": "^4.0.1", + "@types/node": ">=13.7.0", + "long": "^4.0.0" + }, + "bin": { + "pbjs": "bin/pbjs", + "pbts": "bin/pbts" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/proxy-addr/node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/prr": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", + "integrity": "sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/qjobs": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/qjobs/-/qjobs-1.2.0.tgz", + "integrity": "sha512-8YOJEHtxpySA3fFDyCRxA+UUV+fA+rTWnuWvylOK/NCjhY+b4ocCtmu8TtsWb+mYeU+GCHf/S66KZF/AsteKHg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.9" + } + }, + "node_modules/qs": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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/quick-lru": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-4.0.1.tgz", + "integrity": "sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "dev": true, + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/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/read-cache/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/read-package-json": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/read-package-json/-/read-package-json-6.0.4.tgz", + "integrity": "sha512-AEtWXYfopBj2z5N5PbkAOeNHRPUg5q+Nen7QLxV8M2zJq1ym6/lCz3fYNTCXe19puu2d06jfHhrP7v/S2PtMMw==", + "deprecated": "This package is no longer supported. Please use @npmcli/package-json instead.", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^10.2.2", + "json-parse-even-better-errors": "^3.0.0", + "normalize-package-data": "^5.0.0", + "npm-normalize-package-bin": "^3.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/read-package-json-fast": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/read-package-json-fast/-/read-package-json-fast-3.0.2.tgz", + "integrity": "sha512-0J+Msgym3vrLOUB3hzQCuZHII0xkNGCtz/HJH9xZshwv9DbDwkw1KaE3gx/e2J5rpEY5rtOy6cyhKOPrkP7FZw==", + "dev": true, + "license": "ISC", + "dependencies": { + "json-parse-even-better-errors": "^3.0.0", + "npm-normalize-package-bin": "^3.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/read-package-json-fast/node_modules/json-parse-even-better-errors": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-3.0.2.tgz", + "integrity": "sha512-fi0NG4bPjCHunUJffmLd0gxssIgkNmArMvis4iNah6Owg1MCJjWhEcDLmsK6iGkJq3tHwbDkTlce70/tmXN4cQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/read-package-json/node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": 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/read-package-json/node_modules/json-parse-even-better-errors": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-3.0.2.tgz", + "integrity": "sha512-fi0NG4bPjCHunUJffmLd0gxssIgkNmArMvis4iNah6Owg1MCJjWhEcDLmsK6iGkJq3tHwbDkTlce70/tmXN4cQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/read-package-json/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/read-package-json/node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/read-pkg": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz", + "integrity": "sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/normalize-package-data": "^2.4.0", + "normalize-package-data": "^2.5.0", + "parse-json": "^5.0.0", + "type-fest": "^0.6.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/read-pkg-up": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-7.0.1.tgz", + "integrity": "sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^4.1.0", + "read-pkg": "^5.2.0", + "type-fest": "^0.8.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-pkg-up/node_modules/type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=8" + } + }, + "node_modules/read-pkg/node_modules/hosted-git-info": { + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", + "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", + "dev": true, + "license": "ISC" + }, + "node_modules/read-pkg/node_modules/normalize-package-data": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", + "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "hosted-git-info": "^2.1.4", + "resolve": "^1.10.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + } + }, + "node_modules/read-pkg/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/read-pkg/node_modules/type-fest": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.6.0.tgz", + "integrity": "sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=8" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/reflect-metadata": { + "version": "0.1.14", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.14.tgz", + "integrity": "sha512-ZhYeb6nRaXCfhnndflDK8qI6ZQ/YcWZCISRAWICW9XYqMUwjZM9Z0DveWX/ABN01oxSHwVxKQmxeYZSsm0jh5A==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/regenerate": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", + "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==", + "dev": true, + "license": "MIT" + }, + "node_modules/regenerate-unicode-properties": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.2.0.tgz", + "integrity": "sha512-DqHn3DwbmmPVzeKj9woBadqmXxLvQoQIwu7nopMc72ztvxVmVk2SBhSnx67zuye5TP+lJsb/TBQsjLKhnDf3MA==", + "dev": true, + "license": "MIT", + "dependencies": { + "regenerate": "^1.4.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/regenerator-runtime": { + "version": "0.13.11", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", + "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==", + "dev": true, + "license": "MIT" + }, + "node_modules/regenerator-transform": { + "version": "0.15.2", + "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.15.2.tgz", + "integrity": "sha512-hfMp2BoF0qOk3uc5V20ALGDS2ddjQaLrdl7xrGXvAIow7qeWRM2VA2HuCHkUKk9slq3VwEwLNK3DFBqDfPGYtg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.8.4" + } + }, + "node_modules/regex-parser": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/regex-parser/-/regex-parser-2.3.0.tgz", + "integrity": "sha512-TVILVSz2jY5D47F4mA4MppkBrafEaiUWJO/TcZHEIuI13AqoZMkK1WMA4Om1YkYbTx+9Ki1/tSUXbceyr9saRg==", + "dev": true, + "license": "MIT" + }, + "node_modules/regexpp": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz", + "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + } + }, + "node_modules/regexpu-core": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-6.2.0.tgz", + "integrity": "sha512-H66BPQMrv+V16t8xtmq+UC0CBpiTBA60V8ibS1QVReIp8T1z8hwFxqcGzm9K6lgsN7sB5edVH8a+ze6Fqm4weA==", + "dev": true, + "license": "MIT", + "dependencies": { + "regenerate": "^1.4.2", + "regenerate-unicode-properties": "^10.2.0", + "regjsgen": "^0.8.0", + "regjsparser": "^0.12.0", + "unicode-match-property-ecmascript": "^2.0.0", + "unicode-match-property-value-ecmascript": "^2.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/regjsgen": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.8.0.tgz", + "integrity": "sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/regjsparser": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.12.0.tgz", + "integrity": "sha512-cnE+y8bz4NhMjISKbgeVJtqNbtf5QpjZP+Bslo+UqkIt9QPnX9q095eiRRASJG1/tz6dlNr6Z5NsBiWYokp6EQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "jsesc": "~3.0.2" + }, + "bin": { + "regjsparser": "bin/parser" + } + }, + "node_modules/regjsparser/node_modules/jsesc": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.0.2.tgz", + "integrity": "sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "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/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/resolve": { + "version": "1.22.1", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz", + "integrity": "sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==", + "license": "MIT", + "dependencies": { + "is-core-module": "^2.9.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-url-loader": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-url-loader/-/resolve-url-loader-5.0.0.tgz", + "integrity": "sha512-uZtduh8/8srhBoMx//5bwqjQ+rfYOUq8zC9NrMUGtjBiGTtFJM42s58/36+hTqeqINcnYe08Nj3LkK9lW4N8Xg==", + "dev": true, + "license": "MIT", + "dependencies": { + "adjust-sourcemap-loader": "^4.0.0", + "convert-source-map": "^1.7.0", + "loader-utils": "^2.0.0", + "postcss": "^8.2.14", + "source-map": "0.6.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/resolve-url-loader/node_modules/loader-utils": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", + "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" + }, + "engines": { + "node": ">=8.9.0" + } + }, + "node_modules/resolve-url-loader/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "license": "MIT", + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "dev": true, + "license": "MIT" + }, + "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/rimraf/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/rimraf/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "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/rimraf/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/robust-predicates": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.2.tgz", + "integrity": "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==", + "license": "Unlicense", + "optional": true + }, + "node_modules/run-async": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", + "integrity": "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==", + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "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/rw": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz", + "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==", + "license": "BSD-3-Clause", + "optional": true + }, + "node_modules/rxfire": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/rxfire/-/rxfire-6.0.3.tgz", + "integrity": "sha512-77nkyffHh7jgfi1YA/N9RI+kWxYpgKk6GRML1lyersvaqbJt4hkvWwk1rWib9Rb5Lr5mT+Ha45lu7nM79sJCZA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^1.9.0 || ~2.1.0" + }, + "peerDependencies": { + "firebase": "^9.0.0", + "rxjs": "^6.0.0 || ^7.0.0" + } + }, + "node_modules/rxfire/node_modules/tslib": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.1.0.tgz", + "integrity": "sha512-hcVC3wYEziELGGmEEXue7D75zbwIIVUMWAVbHItGPx0ziyXxrOMQx4rQEVEV45Ut/1IotuEvwqPopzIOkDMf0A==", + "license": "0BSD" + }, + "node_modules/rxjs": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-stable-stringify": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/safevalues": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/safevalues/-/safevalues-0.3.4.tgz", + "integrity": "sha512-LRneZZRXNgjzwG4bDQdOTSbze3fHm1EAKN/8bePxnlEZiBmkYEDggaHbuvHI9/hoqHbGfsEA7tWS9GhYHZBBsw==", + "license": "Apache-2.0" + }, + "node_modules/sass": { + "version": "1.58.1", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.58.1.tgz", + "integrity": "sha512-bnINi6nPXbP1XNRaranMFEBZWUfdW/AF16Ql5+ypRxfTvCRTTKrLsMIakyDcayUt2t/RZotmL4kgJwNH5xO+bg==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": ">=3.0.0 <4.0.0", + "immutable": "^4.0.0", + "source-map-js": ">=0.6.2 <2.0.0" + }, + "bin": { + "sass": "sass.js" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/sass-loader": { + "version": "13.2.0", + "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-13.2.0.tgz", + "integrity": "sha512-JWEp48djQA4nbZxmgC02/Wh0eroSUutulROUusYJO9P9zltRbNN80JCBHqRGzjd4cmZCa/r88xgfkjGD0TXsHg==", + "dev": true, + "license": "MIT", + "dependencies": { + "klona": "^2.0.4", + "neo-async": "^2.6.2" + }, + "engines": { + "node": ">= 14.15.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "fibers": ">= 3.1.0", + "node-sass": "^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0", + "sass": "^1.3.0", + "sass-embedded": "*", + "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "fibers": { + "optional": true + }, + "node-sass": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + } + } + }, + "node_modules/sax": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz", + "integrity": "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==", + "dev": true, + "license": "ISC", + "optional": true + }, + "node_modules/schema-utils": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.0.tgz", + "integrity": "sha512-Gf9qqc58SpCA/xdziiHz35F4GNIWYWZrEshUc/G/r5BnLph6xpKuLeoJoQuj5WfBIx/eQLf+hmVPYHaxJu7V2g==", + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/select": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/select/-/select-1.1.2.tgz", + "integrity": "sha512-OwpTSOfy6xSs1+pwcNrv0RBMOzI39Lp3qQKUTPVVPRjCdNa5JH/oPRiqsesIskK8TVgmRiHwO4KXlV2Li9dANA==", + "license": "MIT" + }, + "node_modules/select-hose": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz", + "integrity": "sha512-mEugaLK+YfkijB4fx0e6kImuJdCIt2LxCRcbEYPqRGCs4F2ogyfZU5IAZRdjCP8JPq2AtdNoC/Dux63d9Kiryg==", + "dev": true, + "license": "MIT" + }, + "node_modules/selfsigned": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-2.4.1.tgz", + "integrity": "sha512-th5B4L2U+eGLq1TVh7zNRGBapioSORUeymIydxgFpwww9d2qyKvtuPU2jJuHvYAwwqi2Y596QBL3eEqcPEYL8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node-forge": "^1.3.0", + "node-forge": "^1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/semver": { + "version": "7.5.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.3.tgz", + "integrity": "sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ==", + "license": "ISC", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/semver/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/semver/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC" + }, + "node_modules/send": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/send/node_modules/debug/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "license": "MIT" + }, + "node_modules/send/node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/send/node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/serialize-javascript": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", + "license": "BSD-3-Clause", + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/serve-index": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/serve-index/-/serve-index-1.9.1.tgz", + "integrity": "sha512-pXHfKNP4qujrtteMrSBb0rc8HJ9Ms/GrXwcUtUtD5s4ewDJI8bT3Cz2zTVRMKtri49pLx2e0Ya8ziP5Ya2pZZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "accepts": "~1.3.4", + "batch": "0.6.1", + "debug": "2.6.9", + "escape-html": "~1.0.3", + "http-errors": "~1.6.2", + "mime-types": "~2.1.17", + "parseurl": "~1.3.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/serve-index/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/serve-index/node_modules/depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/serve-index/node_modules/http-errors": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", + "integrity": "sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.0", + "statuses": ">= 1.4.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/serve-index/node_modules/inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==", + "dev": true, + "license": "ISC" + }, + "node_modules/serve-index/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "license": "MIT" + }, + "node_modules/serve-index/node_modules/setprototypeof": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", + "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/serve-static": { + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.19.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/serve-static/node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "dev": true, + "license": "ISC" + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "dev": true, + "license": "ISC" + }, + "node_modules/shallow-clone": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", + "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", + "dev": true, + "license": "MIT", + "dependencies": { + "kind-of": "^6.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "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/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "license": "ISC" + }, + "node_modules/simple-swizzle": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", + "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.3.1" + } + }, + "node_modules/simple-swizzle/node_modules/is-arrayish": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", + "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==", + "license": "MIT" + }, + "node_modules/slash": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-4.0.0.tgz", + "integrity": "sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socket.io": { + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.1.tgz", + "integrity": "sha512-oZ7iUCxph8WYRHHcjBEc9unw3adt5CmSNlppj/5Q4k2RIrhl8Z5yY2Xr4j9zj0+wzVZ0bxmYoGSzKJnRl6A4yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "accepts": "~1.3.4", + "base64id": "~2.0.0", + "cors": "~2.8.5", + "debug": "~4.3.2", + "engine.io": "~6.6.0", + "socket.io-adapter": "~2.5.2", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/socket.io-adapter": { + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.5.tgz", + "integrity": "sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "~4.3.4", + "ws": "~8.17.1" + } + }, + "node_modules/socket.io-adapter/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io-parser": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", + "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==", + "dev": true, + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-parser/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/sockjs": { + "version": "0.3.24", + "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.24.tgz", + "integrity": "sha512-GJgLTZ7vYb/JtPSSZ10hsOYIvEYsjbNU+zPdIHcUaWVNUEPivzxku31865sSSud0Da0W4lEeOPlmw93zLQchuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "faye-websocket": "^0.11.3", + "uuid": "^8.3.2", + "websocket-driver": "^0.7.4" + } + }, + "node_modules/sockjs/node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "dev": true, + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/socks": { + "version": "2.8.3", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.3.tgz", + "integrity": "sha512-l5x7VUUWbjVFbafGLxPWkYsHIhEvmF85tbIeFZWc8ZPtoMyybuEhL7Jye/ooC4/d48FgOjSJXgsF/AJPYCW8Zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "ip-address": "^9.0.5", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-7.0.0.tgz", + "integrity": "sha512-Fgl0YPZ902wEsAyiQ+idGd1A7rSFx/ayC1CQVMw5P+EQx2V0SgpGtf6OKFhVjPflPUl9YMmEOnmfjCdMUsygww==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^6.0.2", + "debug": "^4.3.3", + "socks": "^2.6.2" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/source-map": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", + "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", + "license": "BSD-3-Clause", + "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/source-map-loader": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/source-map-loader/-/source-map-loader-4.0.1.tgz", + "integrity": "sha512-oqXpzDIByKONVY8g1NUPOTQhe0UTU5bWUl32GSkqK2LjJj0HmwTMVKxcUip0RgAYhY1mqgOxjbQM48a0mmeNfA==", + "dev": true, + "license": "MIT", + "dependencies": { + "abab": "^2.0.6", + "iconv-lite": "^0.6.3", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": ">= 14.15.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.72.1" + } + }, + "node_modules/source-map-loader/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/source-map-support/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/spdx-correct": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", + "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-exceptions": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", + "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==", + "dev": true, + "license": "CC-BY-3.0" + }, + "node_modules/spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-license-ids": { + "version": "3.0.21", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.21.tgz", + "integrity": "sha512-Bvg/8F5XephndSK3JffaRqdT+gyhfqIPwDHpX80tJrF8QQRYMo8sNMeaZ2Dp5+jhwKnUmIOyFFQfHRkjJm5nXg==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/spdy": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/spdy/-/spdy-4.0.2.tgz", + "integrity": "sha512-r46gZQZQV+Kl9oItvl1JZZqJKGr+oEkB08A6BzkiR7593/7IbtuncXHd2YoYeTsG4157ZssMu9KYvUHLcjcDoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.0", + "handle-thing": "^2.0.0", + "http-deceiver": "^1.2.7", + "select-hose": "^2.0.0", + "spdy-transport": "^3.0.0" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/spdy-transport": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/spdy-transport/-/spdy-transport-3.0.0.tgz", + "integrity": "sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.0", + "detect-node": "^2.0.4", + "hpack.js": "^2.1.6", + "obuf": "^1.1.2", + "readable-stream": "^3.0.6", + "wbuf": "^1.7.3" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/ssri": { + "version": "10.0.6", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-10.0.6.tgz", + "integrity": "sha512-MGrFH9Z4NP9Iyhqn16sDtBpRRNJ0Y2hNa6D65h736fVSaPCHr4DM4sWUNvVaSuC+0OBGhwsrydQwmgfg5LncqQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/ssri/node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/stack-trace": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", + "integrity": "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/streamroller": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/streamroller/-/streamroller-3.1.5.tgz", + "integrity": "sha512-KFxaM7XT+irxvdqSP1LGLgNWbYN7ay5owZ3r/8t77p+EtSUAfUgtl7be3xtqtOmGUl9K9YPO2ca8133RlTjvKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "date-format": "^4.0.14", + "debug": "^4.3.4", + "fs-extra": "^8.1.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "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/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "min-indent": "^1.0.0" + }, + "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/stylis": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.5.tgz", + "integrity": "sha512-K7npNOKGRYuhAFFzkzMGfxFDpN6gDwf8hcMiE+uveTVbBgm93HrNP3ZDUpKqzZ4pG7TP6fmb+EMAQPjq9FqqvA==", + "license": "MIT", + "optional": true + }, + "node_modules/sucrase": { + "version": "3.35.0", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz", + "integrity": "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "glob": "^10.3.10", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/sucrase/node_modules/@jridgewell/gen-mapping": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", + "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", + "license": "MIT", + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/sucrase/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/sucrase/node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "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/sucrase/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/sucrase/node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "license": "ISC", + "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==", + "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/symbol-observable": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-4.0.0.tgz", + "integrity": "sha512-b19dMThMV4HVFynSAM1++gBHAbk2Tc/osgLIBZMKsyqh34jb2e8Os7T6ZW/Bt3pJFdBTd2JwAnAAEQV7rSNvcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/synckit": { + "version": "0.8.8", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.8.8.tgz", + "integrity": "sha512-HwOKAP7Wc5aRGYdKH+dw0PRRpbO841v2DENBtjnR5HFWoiNByAl7vrx3p0G/rCyYXQsrxqtX48TImFtPcIHSpQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@pkgr/core": "^0.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/unts" + } + }, + "node_modules/tailwindcss": { + "version": "3.4.17", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz", + "integrity": "sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==", + "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.6", + "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", + "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/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/tailwindcss/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/tailwindcss/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/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/tailwindcss/node_modules/resolve": { + "version": "1.22.10", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", + "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.0", + "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/tapable": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", + "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/tar": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "dev": true, + "license": "ISC", + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/tar/node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/tar/node_modules/fs-minipass/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tar/node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=8" + } + }, + "node_modules/tar/node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true, + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/tar/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/terser": { + "version": "5.16.3", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.16.3.tgz", + "integrity": "sha512-v8wWLaS/xt3nE9dgKEWhNUFP6q4kngO5B8eYFUuebsu7Dw/UNAnpUod6UHo04jSSkv8TzKHjZDSd7EXdDQAl8Q==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@jridgewell/source-map": "^0.3.2", + "acorn": "^8.5.0", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser-webpack-plugin": { + "version": "5.3.11", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.11.tgz", + "integrity": "sha512-RVCsMfuD0+cTt3EwX8hSl2Ks56EbFHWmhluwcqoPKtBnfjiT6olaq7PRIRfhyU8nnC2MrnDrBLfrD/RGE+cVXQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "jest-worker": "^27.4.5", + "schema-utils": "^4.3.0", + "serialize-javascript": "^6.0.2", + "terser": "^5.31.1" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "uglify-js": { + "optional": true + } + } + }, + "node_modules/terser-webpack-plugin/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "license": "MIT" + }, + "node_modules/terser-webpack-plugin/node_modules/terser": { + "version": "5.37.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.37.0.tgz", + "integrity": "sha512-B8wRRkmre4ERucLM/uXx4MOV5cbnOlVAqUst+1+iLKPI0dOgFO28f84ptoQt9HEI537PMzfYa/d+GEPKTRXmYA==", + "license": "BSD-2-Clause", + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.8.2", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/test-exclude/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/test-exclude/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "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/test-exclude/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/text-hex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", + "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==", + "license": "MIT" + }, + "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/through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", + "license": "MIT" + }, + "node_modules/thunky": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz", + "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tiny-emitter": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/tiny-emitter/-/tiny-emitter-2.1.0.tgz", + "integrity": "sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q==", + "license": "MIT" + }, + "node_modules/tmp": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "license": "MIT", + "dependencies": { + "os-tmpdir": "~1.0.2" + }, + "engines": { + "node": ">=0.6.0" + } + }, + "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/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "license": "MIT", + "bin": { + "tree-kill": "cli.js" + } + }, + "node_modules/trim-newlines": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-3.0.1.tgz", + "integrity": "sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/triple-beam": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.4.1.tgz", + "integrity": "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==", + "license": "MIT", + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/ts-dedent": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/ts-dedent/-/ts-dedent-2.2.0.tgz", + "integrity": "sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=6.10" + } + }, + "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/tsutils": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", + "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^1.8.1" + }, + "engines": { + "node": ">= 6" + }, + "peerDependencies": { + "typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta" + } + }, + "node_modules/tsutils/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true, + "license": "0BSD" + }, + "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.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typed-assert": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/typed-assert/-/typed-assert-1.0.9.tgz", + "integrity": "sha512-KNNZtayBCtmnNmbo5mG47p1XsCyrx6iVqomjcZnec/1Y5GGARaxPs6r49RnSPeUP3YjNYiU9sQHAtY4BBvnZwg==", + "dev": true, + "license": "MIT" + }, + "node_modules/typescript": { + "version": "4.9.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", + "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + }, + "node_modules/ua-parser-js": { + "version": "0.7.40", + "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.40.tgz", + "integrity": "sha512-us1E3K+3jJppDBa3Tl0L3MOJiGhe1C6P0+nIvQAFYbxlMAx0h81eOwLmU57xgqToduDDPx3y5QsdjPfDu+FgOQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/ua-parser-js" + }, + { + "type": "paypal", + "url": "https://paypal.me/faisalman" + }, + { + "type": "github", + "url": "https://github.com/sponsors/faisalman" + } + ], + "license": "MIT", + "bin": { + "ua-parser-js": "script/cli.js" + }, + "engines": { + "node": "*" + } + }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "license": "MIT" + }, + "node_modules/unicode-canonical-property-names-ecmascript": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.1.tgz", + "integrity": "sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-ecmascript": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz", + "integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "unicode-canonical-property-names-ecmascript": "^2.0.0", + "unicode-property-aliases-ecmascript": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-value-ecmascript": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.2.0.tgz", + "integrity": "sha512-4IehN3V/+kkr5YeSSDDQG8QLqO26XpL2XP3GQtqwlT/QYSECAwFztxVHjlbh0+gjJ3XmNLS0zDsbgs9jWKExLg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-property-aliases-ecmascript": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz", + "integrity": "sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/unique-filename": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-3.0.0.tgz", + "integrity": "sha512-afXhuC55wkAmZ0P18QsVE6kp8JaxrEokN2HGIoIVv2ijHQd419H0+6EigAFcIzXeMIkcIkNBpB3L/DXB3cTS/g==", + "dev": true, + "license": "ISC", + "dependencies": { + "unique-slug": "^4.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/unique-slug": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-4.0.0.tgz", + "integrity": "sha512-WrcA6AyEfqDX5bWige/4NQfPZMtASNVxdmWR76WESYQVAACSgWcR6e9i0mofqqBxYFtL4oAxPIptY73/0YE1DQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "license": "MIT", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.2.tgz", + "integrity": "sha512-PPypAm5qvlD7XMZC3BujecnaOxwhrtoFR+Dqkk5Aa/6DssiH0ibKoketaj9w8LP7Bont1rYeoV5plxD7RTEPRg==", + "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==", + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/uri-js/node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "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/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "node_modules/validate-npm-package-name": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-5.0.1.tgz", + "integrity": "sha512-OljLrQ9SQdOUqTaQxqL5dEfZWrXExyyWsozYlAWFawPVNuD83igl7uJD2RTkNMbniIYgt8l81eCJGIdQF7avLQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/void-elements": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-2.0.1.tgz", + "integrity": "sha512-qZKX4RnBzH2ugr8Lxa7x+0V6XD9Sb/ouARtiasEQCHB1EVU4NXtmHsDDrx1dO4ne5fc3J6EW05BP1Dl0z0iung==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/watchpack": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.2.tgz", + "integrity": "sha512-TnbFSbcOCcDgjZ4piURLCbJ3nJhznVh9kw6F6iokjiFPl8ONxe9A6nMDVXDiNbrSfLILs6vB07F7wLBrwPYzJw==", + "license": "MIT", + "dependencies": { + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/wbuf": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/wbuf/-/wbuf-1.7.3.tgz", + "integrity": "sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimalistic-assert": "^1.0.0" + } + }, + "node_modules/wcwidth": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", + "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==", + "license": "MIT", + "dependencies": { + "defaults": "^1.0.3" + } + }, + "node_modules/web-worker": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/web-worker/-/web-worker-1.5.0.tgz", + "integrity": "sha512-RiMReJrTAiA+mBjGONMnjVDP2u3p9R1vkcGz6gDIrOMT3oGuYwX2WRMYI9ipkphSuE5XKEhydbhNEJh4NY9mlw==", + "license": "Apache-2.0", + "optional": true + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/webpack": { + "version": "5.76.1", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.76.1.tgz", + "integrity": "sha512-4+YIK4Abzv8172/SGqObnUjaIHjLEuUasz9EwQj/9xmPPkYJy2Mh03Q/lJfSD3YLzbxy5FeTq5Uw0323Oh6SJQ==", + "license": "MIT", + "dependencies": { + "@types/eslint-scope": "^3.7.3", + "@types/estree": "^0.0.51", + "@webassemblyjs/ast": "1.11.1", + "@webassemblyjs/wasm-edit": "1.11.1", + "@webassemblyjs/wasm-parser": "1.11.1", + "acorn": "^8.7.1", + "acorn-import-assertions": "^1.7.6", + "browserslist": "^4.14.5", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.10.0", + "es-module-lexer": "^0.9.0", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.2.9", + "json-parse-even-better-errors": "^2.3.1", + "loader-runner": "^4.2.0", + "mime-types": "^2.1.27", + "neo-async": "^2.6.2", + "schema-utils": "^3.1.0", + "tapable": "^2.1.1", + "terser-webpack-plugin": "^5.1.3", + "watchpack": "^2.4.0", + "webpack-sources": "^3.2.3" + }, + "bin": { + "webpack": "bin/webpack.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-dev-middleware": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-6.1.2.tgz", + "integrity": "sha512-Wu+EHmX326YPYUpQLKmKbTyZZJIB8/n6R09pTmB03kJmnMsVPTo9COzHZFr01txwaCAuZvfBJE4ZCHRcKs5JaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "colorette": "^2.0.10", + "memfs": "^3.4.12", + "mime-types": "^2.1.31", + "range-parser": "^1.2.1", + "schema-utils": "^4.0.0" + }, + "engines": { + "node": ">= 14.15.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "webpack": { + "optional": true + } + } + }, + "node_modules/webpack-dev-server": { + "version": "4.11.1", + "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-4.11.1.tgz", + "integrity": "sha512-lILVz9tAUy1zGFwieuaQtYiadImb5M3d+H+L1zDYalYoDl0cksAB1UNyuE5MMWJrG6zR1tXkCP2fitl7yoUJiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/bonjour": "^3.5.9", + "@types/connect-history-api-fallback": "^1.3.5", + "@types/express": "^4.17.13", + "@types/serve-index": "^1.9.1", + "@types/serve-static": "^1.13.10", + "@types/sockjs": "^0.3.33", + "@types/ws": "^8.5.1", + "ansi-html-community": "^0.0.8", + "bonjour-service": "^1.0.11", + "chokidar": "^3.5.3", + "colorette": "^2.0.10", + "compression": "^1.7.4", + "connect-history-api-fallback": "^2.0.0", + "default-gateway": "^6.0.3", + "express": "^4.17.3", + "graceful-fs": "^4.2.6", + "html-entities": "^2.3.2", + "http-proxy-middleware": "^2.0.3", + "ipaddr.js": "^2.0.1", + "open": "^8.0.9", + "p-retry": "^4.5.0", + "rimraf": "^3.0.2", + "schema-utils": "^4.0.0", + "selfsigned": "^2.1.1", + "serve-index": "^1.9.1", + "sockjs": "^0.3.24", + "spdy": "^4.0.2", + "webpack-dev-middleware": "^5.3.1", + "ws": "^8.4.2" + }, + "bin": { + "webpack-dev-server": "bin/webpack-dev-server.js" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^4.37.0 || ^5.0.0" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-dev-server/node_modules/webpack-dev-middleware": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-5.3.4.tgz", + "integrity": "sha512-BVdTqhhs+0IfoeAf7EoH5WE+exCmqGerHfDM0IL096Px60Tq2Mn9MAbnaGUe6HiMa41KMCYF19gyzZmBcq/o4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "colorette": "^2.0.10", + "memfs": "^3.4.3", + "mime-types": "^2.1.31", + "range-parser": "^1.2.1", + "schema-utils": "^4.0.0" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^4.0.0 || ^5.0.0" + } + }, + "node_modules/webpack-merge": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.8.0.tgz", + "integrity": "sha512-/SaI7xY0831XwP6kzuwhKWVKDP9t1QY1h65lAFLbZqMPIuYcD9QAW4u9STIbU9kaJbPBB/geU/gLr1wDjOhQ+Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "clone-deep": "^4.0.1", + "wildcard": "^2.0.0" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/webpack-sources": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz", + "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==", + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webpack-subresource-integrity": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/webpack-subresource-integrity/-/webpack-subresource-integrity-5.1.0.tgz", + "integrity": "sha512-sacXoX+xd8r4WKsy9MvH/q/vBtEHr86cpImXwyg74pFIpERKt6FmB8cXpeuh0ZLgclOlHI4Wcll7+R5L02xk9Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "typed-assert": "^1.0.8" + }, + "engines": { + "node": ">= 12" + }, + "peerDependencies": { + "html-webpack-plugin": ">= 5.0.0-beta.1 < 6", + "webpack": "^5.12.0" + }, + "peerDependenciesMeta": { + "html-webpack-plugin": { + "optional": true + } + } + }, + "node_modules/webpack/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "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/webpack/node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "license": "MIT", + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "node_modules/webpack/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==", + "license": "MIT" + }, + "node_modules/webpack/node_modules/schema-utils": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/websocket-driver": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", + "integrity": "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==", + "license": "Apache-2.0", + "dependencies": { + "http-parser-js": ">=0.5.1", + "safe-buffer": ">=5.1.0", + "websocket-extensions": ">=0.1.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/websocket-extensions": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz", + "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, + "node_modules/wide-align": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", + "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^1.0.2 || 2 || 3 || 4" + } + }, + "node_modules/wildcard": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.1.tgz", + "integrity": "sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/winston": { + "version": "3.17.0", + "resolved": "https://registry.npmjs.org/winston/-/winston-3.17.0.tgz", + "integrity": "sha512-DLiFIXYC5fMPxaRg832S6F5mJYvePtmO5G9v9IgUFPhXm9/GkXarH/TUrBAVzhTCzAj9anE/+GjrgXp/54nOgw==", + "license": "MIT", + "dependencies": { + "@colors/colors": "^1.6.0", + "@dabh/diagnostics": "^2.0.2", + "async": "^3.2.3", + "is-stream": "^2.0.0", + "logform": "^2.7.0", + "one-time": "^1.0.0", + "readable-stream": "^3.4.0", + "safe-stable-stringify": "^2.3.1", + "stack-trace": "0.0.x", + "triple-beam": "^1.3.0", + "winston-transport": "^4.9.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/winston-transport": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.9.0.tgz", + "integrity": "sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A==", + "license": "MIT", + "dependencies": { + "logform": "^2.7.0", + "readable-stream": "^3.6.2", + "triple-beam": "^1.3.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/winston/node_modules/@colors/colors": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.6.0.tgz", + "integrity": "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==", + "license": "MIT", + "engines": { + "node": ">=0.1.90" + } + }, + "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/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "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/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "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/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/write-file-atomic": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", + "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.7" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/ws": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 6" + } + }, + "node_modules/yargs": { + "version": "17.6.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.6.2.tgz", + "integrity": "sha512-1/9UrdHjDZc0eOU0HxOHoS78C69UD3JRMvzlJ7S79S2nTaWRA/whGCTV8o9e/N/1Va9YIV7Q4sOxD8VV4pCWOw==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zone.js": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.12.0.tgz", + "integrity": "sha512-XtC+I5dXU14HrzidAKBNMqneIVUykLEAA1x+v4KVrd6AUPWlwYORF8KgsVqvgdHiKZ4BkxxjvYi/ksEixTPR0Q==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + } + } + } +} diff --git a/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/package.json b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/package.json new file mode 100644 index 0000000..1698245 --- /dev/null +++ b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/package.json @@ -0,0 +1,81 @@ +{ + "name": "quick-bot-app-frontend", + "version": "0.0.0", + "scripts": { + "ng": "ng", + "start": "ng serve", + "build:prod": "ng build --configuration production --aot --output-hashing=all", + "build:dev": "ng build --configuration development", + "build:staging": "ng build --configuration staging", + "build": "ng build", + "watch": "ng build --watch --configuration development", + "test": "ng test", + "postinstall": "ngcc", + "post-build": "node ./build/post-build.js", + "lint": "gts lint", + "clean": "gts clean", + "compile": "tsc", + "fix": "gts fix", + "prepare": "npm run compile", + "pretest": "npm run compile", + "posttest": "npm run lint", + "format": "prettier --write \"src/**/*.{ts,html,scss,json}\"", + "check-format": "prettier --check \"src/**/*.{ts,html,scss,json}\"" + }, + "private": true, + "dependencies": { + "@angular/animations": "^15.1.0", + "@angular/cdk": "^15.2.9", + "@angular/common": "^15.1.0", + "@angular/compiler": "^15.1.0", + "@angular/core": "^15.1.0", + "@angular/fire": "7.5", + "@angular/flex-layout": "^15.0.0-beta.42", + "@angular/forms": "^15.1.0", + "@angular/material": "^15.2.9", + "@angular/platform-browser": "^15.1.0", + "@angular/platform-browser-dynamic": "^15.1.0", + "@angular/router": "^15.1.0", + "@fortawesome/fontawesome-free": "^6.6.0", + "@ionic/angular": "^7.5.4", + "@ng-idle/core": "^14.0.0", + "clipboard": "^2.0.11", + "ionic-emoji-rating": "^1.0.15", + "marked": "^4.3.0", + "moment": "^2.30.1", + "ng2-pdf-viewer": "^7.0.2", + "ngx-markdown": "^15.1.2", + "prismjs": "^1.29.0", + "rxfire": "6.0.3", + "rxjs": "~7.8.0", + "tslib": "^2.3.0", + "uuid": "^9.0.1", + "zone.js": "~0.12.0", + "autoprefixer": "^10.4.21", + "postcss": "^8.5.3", + "tailwindcss": "^3.4.17" + }, + "devDependencies": { + "@angular-devkit/build-angular": "^15.1.3", + "@angular/cli": "~15.1.3", + "@angular/compiler-cli": "^15.1.0", + "@types/jasmine": "~4.3.0", + "@types/marked": "^4.3.0", + "@types/node": "20.12.7", + "@types/uuid": "^9.0.4", + "firebase-tools": "^11.30.0", + "gts": "^5.3.1", + "jasmine-core": "~4.5.0", + "karma": "~6.4.0", + "karma-chrome-launcher": "~3.1.0", + "karma-coverage": "~2.2.0", + "karma-jasmine": "~5.1.0", + "karma-jasmine-html-reporter": "~2.0.0", + "prettier": "^3.5.1", + "typescript": "~4.9.4" + }, + "description": "This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 15.1.3.", + "main": "index.js", + "author": "", + "license": "ISC" +} diff --git a/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/postcss.config.js b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/postcss.config.js new file mode 100644 index 0000000..30a0865 --- /dev/null +++ b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/postcss.config.js @@ -0,0 +1,22 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +module.exports = { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; diff --git a/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/app/app-routing.module.ts b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/app/app-routing.module.ts new file mode 100644 index 0000000..9a379d6 --- /dev/null +++ b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/app/app-routing.module.ts @@ -0,0 +1,34 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {NgModule} from '@angular/core'; +import {RouterModule, Routes} from '@angular/router'; +import {MainComponent} from './components/main/main.component'; +import {LoginComponent} from './components/login/login.component'; +import {AuthGuard} from './services/login/auth.guard'; +import {SearchResultsComponent} from './components/main/search-results/search-results.component'; + +const routes: Routes = [ + {path: '', component: MainComponent, canActivate: [AuthGuard]}, + {path: 'login', component: LoginComponent}, + {path: 'search', component: SearchResultsComponent, canActivate: [AuthGuard]}, +]; + +@NgModule({ + imports: [RouterModule.forRoot(routes, {onSameUrlNavigation: 'reload'})], + exports: [RouterModule], +}) +export class AppRoutingModule {} diff --git a/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/app/app.component.html b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/app/app.component.html new file mode 100644 index 0000000..da9671f --- /dev/null +++ b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/app/app.component.html @@ -0,0 +1,35 @@ + + + + + +
    + +
    + + diff --git a/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/app/app.component.scss b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/app/app.component.scss new file mode 100644 index 0000000..6bc6422 --- /dev/null +++ b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/app/app.component.scss @@ -0,0 +1,142 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +::ng-deep .tour-step { + background: white; +} + +::ng-deep .tour-top { + margin: -1%; +} + +::ng-deep .tour-left { + margin-left: -1%; +} + +::ng-deep .tour-top-right { + margin-top: -1%; +} + +::ng-deep .tour-bottom-right { + margin-top: 1%; +} + +::ng-deep .tour-bottom-left { + margin-top: 1%; +} + +::ng-deep .tour-bottom { + margin: 1%; +} + +::ng-deep ngx-guided-tour .tour-content { + font-size: 17px !important; + padding-bottom: 20px !important; +} + +::ng-deep ngx-guided-tour .tour-title { + font-size: 22px !important; + padding-bottom: 0 !important; +} + +::ng-deep ngx-guided-tour .tour-step .tour-buttons .skip-button { + font-weight: 500 !important; + font-size: 17px !important; + line-height: 1.7 !important; +} + +::ng-deep ngx-guided-tour .tour-step { + border-radius: 20px !important; +} + +::ng-deep ngx-guided-tour .tour-step .tour-buttons .back-button { + font-weight: 500 !important; + font-size: 17px !important; + line-height: 1.7 !important; +} + +::ng-deep ngx-guided-tour .tour-step .tour-buttons .next-button { + background: #4285f4; + padding-left: 16px !important; + padding-right: 16px !important; + padding-top: 6px !important; + padding-bottom: 6px !important; + font-size: 16px !important; + color: white; + font-weight: 500; + border-radius: 15px !important; +} + +::ng-deep ngx-guided-tour .tour-step.tour-top .tour-block { + margin-bottom: 0 !important; +} + +::ng-deep ngx-guided-tour .tour-step.tour-bottom-left .tour-block { + margin-top: 0 !important; +} + +::ng-deep pre { + background: #485f84 !important; +} + +div { + font-family: 'Google Sans', sans-serif !important; +} + +.video-embed-player { + position: fixed; + top: 20vh; + left: -50vw; + z-index: -10000; + overflow: hidden; + width: 200vw; +} + +// Small devices (landscape phones, 576px and up) +@media (min-width: 576px) { +} + +// Medium devices (tablets, 768px and up) +@media (min-width: 768px) { + .video-embed-player { + top: 20vh; + width: 150vw; + left: -25vw; + } +} + +// Large devices (desktops, 992px and up) +@media (min-width: 992px) { + .video-embed-player { + top: 10vh; + } +} + +// X-Large devices (large desktops, 1200px and up) +@media (min-width: 1200px) { + .video-embed-player { + left: 0; + width: 100vw; + } +} + +// XX-Large devices (larger desktops, 1400px and up) +@media (min-width: 1400px) { + .video-embed-player { + top: -10vw; + width: 110vw; + } +} diff --git a/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/app/app.component.spec.ts b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/app/app.component.spec.ts new file mode 100644 index 0000000..9ff3178 --- /dev/null +++ b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/app/app.component.spec.ts @@ -0,0 +1,49 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {TestBed} from '@angular/core/testing'; +import {RouterTestingModule} from '@angular/router/testing'; +import {AppComponent} from './app.component'; + +describe('AppComponent', () => { + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [RouterTestingModule], + declarations: [AppComponent], + }).compileComponents(); + }); + + it('should create the app', () => { + const fixture = TestBed.createComponent(AppComponent); + const app = fixture.componentInstance; + expect(app).toBeTruthy(); + }); + + it("should have as title 'pac-assist-ui'", () => { + const fixture = TestBed.createComponent(AppComponent); + const app = fixture.componentInstance; + expect(app.title).toEqual('pac-assist-ui'); + }); + + it('should render title', () => { + const fixture = TestBed.createComponent(AppComponent); + fixture.detectChanges(); + const compiled = fixture.nativeElement as HTMLElement; + expect(compiled.querySelector('.content span')?.textContent).toContain( + 'pac-assist-ui app is running!' + ); + }); +}); diff --git a/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/app/app.component.ts b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/app/app.component.ts new file mode 100644 index 0000000..a05a5ba --- /dev/null +++ b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/app/app.component.ts @@ -0,0 +1,67 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {Component} from '@angular/core'; +import {Router, NavigationEnd, Event as NavigationEvent} from '@angular/router'; +import {UserService} from './services/user/user.service'; +import {AuthService} from './services/login/auth.service'; + +@Component({ + selector: 'app-root', + templateUrl: './app.component.html', + styleUrls: ['./app.component.scss'], +}) +export class AppComponent { + title = 'pac-assist-ui'; + showHeader = true; + userInfo: any; + + showLoading = false; + showVideo = true; + + constructor( + private router: Router, + private userService: UserService, + public authService: AuthService + ) { + this.router.events.subscribe((event: NavigationEvent) => { + if (event instanceof NavigationEnd) { + if (event.url === '/') this.showVideo = true; + + if ( + event.url === '/login' || + event.url === '/login/e2e' || + (event.url.includes('login') && event.url.includes('email')) || + (event.url.includes('login') && event.url.includes('tos')) || + event.url.includes('reset-password') || + event.url.includes('support-ticket') + ) { + this.showHeader = false; + } else { + this.userInfo = this.userService.getUserDetails(); + this.showHeader = true; + } + } + }); + } + + ngOnInit(): void { + this.userService.loadingSubject.subscribe(loadingValue => { + this.showLoading = loadingValue; + this.showVideo = loadingValue; + }); + } +} diff --git a/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/app/app.module.ts b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/app/app.module.ts new file mode 100644 index 0000000..196fa43 --- /dev/null +++ b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/app/app.module.ts @@ -0,0 +1,165 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {NgModule} from '@angular/core'; +import {BrowserModule} from '@angular/platform-browser'; +import {AppRoutingModule} from './app-routing.module'; +import {AppComponent} from './app.component'; +import {HeaderComponent} from './components/header/header.component'; +import {FooterComponent} from './components/footer/footer.component'; +import {MainComponent} from './components/main/main.component'; +import {BrowserAnimationsModule} from '@angular/platform-browser/animations'; +import {MatIconModule} from '@angular/material/icon'; +import {MatAutocompleteModule} from '@angular/material/autocomplete'; +import {MatButtonModule} from '@angular/material/button'; +import {MatToolbarModule} from '@angular/material/toolbar'; +import {HttpClientModule} from '@angular/common/http'; +import {MatInputModule} from '@angular/material/input'; +import {NgFor, PathLocationStrategy} from '@angular/common'; +import {MatSelectModule} from '@angular/material/select'; +import {MatFormFieldModule} from '@angular/material/form-field'; +import {FormsModule, ReactiveFormsModule} from '@angular/forms'; +import {MatGridListModule} from '@angular/material/grid-list'; +import {MatCardModule} from '@angular/material/card'; +import {MatProgressSpinnerModule} from '@angular/material/progress-spinner'; +import {MatTooltipModule} from '@angular/material/tooltip'; +import {LoginComponent} from './components/login/login.component'; +import {initializeApp, provideFirebaseApp} from '@angular/fire/app'; +import { + provideAnalytics, + getAnalytics, + ScreenTrackingService, + UserTrackingService, +} from '@angular/fire/analytics'; +import {environment} from '../environments/environment'; +import {provideAuth, getAuth} from '@angular/fire/auth'; +import {MatMenuModule} from '@angular/material/menu'; +import {MatDividerModule} from '@angular/material/divider'; +import {MatChipsModule} from '@angular/material/chips'; +import {LocationStrategy} from '@angular/common'; +import {MarkdownModule} from 'ngx-markdown'; +import {MatDialogModule} from '@angular/material/dialog'; +import {CdkAccordionModule} from '@angular/cdk/accordion'; +import {MatExpansionModule} from '@angular/material/expansion'; +import {IonicRatingModule} from 'ionic-emoji-rating'; +import {MatListModule} from '@angular/material/list'; +import {MatTabsModule} from '@angular/material/tabs'; +import {MatTableModule} from '@angular/material/table'; +import {MatCheckboxModule} from '@angular/material/checkbox'; +import {MatSlideToggleModule} from '@angular/material/slide-toggle'; +import {MatButtonToggleModule} from '@angular/material/button-toggle'; +import {MatSnackBarModule} from '@angular/material/snack-bar'; +import {MatSortModule} from '@angular/material/sort'; +import {MatPaginatorModule} from '@angular/material/paginator'; +import {MatSidenavModule} from '@angular/material/sidenav'; +import {NgIdleModule} from '@ng-idle/core'; +import {ClipboardModule} from '@angular/cdk/clipboard'; +import {MatProgressBarModule} from '@angular/material/progress-bar'; +import {PdfViewerModule} from 'ng2-pdf-viewer'; + +import 'prismjs'; +import 'prismjs/components/prism-typescript.min.js'; +import 'prismjs/plugins/line-numbers/prism-line-numbers.js'; +import 'prismjs/plugins/line-highlight/prism-line-highlight.js'; +import {FlexLayoutModule} from '@angular/flex-layout'; +import {MatSliderModule} from '@angular/material/slider'; +import {MatStepperModule} from '@angular/material/stepper'; +import {ChatInputComponent} from './components/main/chat-input/chat-input.component'; +import {SearchResultsComponent} from './components/main/search-results/search-results.component'; +import {ToastMessageComponent} from './components/toast-message/toast-message.component'; +import {TruncatePipe} from './pipes/truncate.pipe'; +import {BackgroundChangerInputComponent} from './components/background-changer-input/background-changer-input.component'; + +@NgModule({ + declarations: [ + AppComponent, + HeaderComponent, + FooterComponent, + MainComponent, + LoginComponent, + ChatInputComponent, + SearchResultsComponent, + ToastMessageComponent, + TruncatePipe, + BackgroundChangerInputComponent, + ], + imports: [ + BrowserModule, + AppRoutingModule, + BrowserAnimationsModule, + MatSnackBarModule, + MatPaginatorModule, + MatSortModule, + MatSliderModule, + MatToolbarModule, + MatButtonModule, + MatIconModule, + HttpClientModule, + FormsModule, + MatFormFieldModule, + MatSelectModule, + NgFor, + MatInputModule, + MatGridListModule, + MatCardModule, + MatTooltipModule, + MatProgressSpinnerModule, + MatMenuModule, + MatDividerModule, + MatChipsModule, + ReactiveFormsModule, + MatMenuModule, + MatDialogModule, + CdkAccordionModule, + IonicRatingModule, + MarkdownModule.forRoot(), + MatExpansionModule, + MatListModule, + MatTabsModule, + MatCheckboxModule, + MatTableModule, + MatSlideToggleModule, + MatButtonToggleModule, + MatSidenavModule, + MatAutocompleteModule, + environment.requiredLogin === 'True' + ? [ + provideFirebaseApp(() => initializeApp(environment.firebase)), + provideAuth(() => getAuth()), + ] + : [], + environment.requiredLogin === 'True' + ? [provideAnalytics(() => getAnalytics())] + : [], + FlexLayoutModule, + NgIdleModule.forRoot(), + ClipboardModule, + MatStepperModule, + MatProgressBarModule, + PdfViewerModule, + ], + providers: [ + {provide: LocationStrategy, useClass: PathLocationStrategy}, + environment.requiredLogin === 'True' + ? [ + ScreenTrackingService, // Automatically track screen views + UserTrackingService, // Automatically track user interactions + ] + : [], + ], + bootstrap: [AppComponent], +}) +export class AppModule {} diff --git a/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/app/components/background-changer-input/background-changer-input.component.html b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/app/components/background-changer-input/background-changer-input.component.html new file mode 100644 index 0000000..99e4d07 --- /dev/null +++ b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/app/components/background-changer-input/background-changer-input.component.html @@ -0,0 +1,56 @@ + + + + +
    + + + + {{ selectedFile.name }} + + No image selected +
    + +
    + Image Preview +
    +
    + + + +
    diff --git a/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/app/components/background-changer-input/background-changer-input.component.scss b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/app/components/background-changer-input/background-changer-input.component.scss new file mode 100644 index 0000000..16d001f --- /dev/null +++ b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/app/components/background-changer-input/background-changer-input.component.scss @@ -0,0 +1,80 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +.file-input-wrapper { + margin-bottom: 20px; + display: flex; + flex-direction: column; + align-items: center; + gap: 10px; + + .placeholder { + color: rgba(0, 0, 0, 0.54); // Material hint text color + } + + .file-name { + font-style: italic; + color: rgba(0, 0, 0, 0.87); + } +} + +.image-preview { + margin-top: 20px; + border: 1px solid #ccc; + border-radius: 4px; + padding: 5px; + max-width: 100%; + display: flex; + justify-content: center; + + img { + max-width: 100%; + max-height: 300px; // Adjust as needed + display: block; + } +} + +.card-input-wrapper { + // min-width: 40vw; + width: 80%; + min-height: 10rem; + display: flex; + justify-content: center; + flex-wrap: wrap; + flex-direction: column; + align-items: center; + text-align: center; + border: 4px dashed lightgrey; + margin: 2rem; + padding: 1rem; + border-radius: 4px; + cursor: pointer; + transition: border 1s; + text-decoration: none; + + &:hover { + border-color: rgba(66, 133, 244, 0.95); + } +} + +.centered-mat-card { + display: flex; + flex-direction: column; + align-items: center; + align-self: center; + justify-self: center; + width: 40vw; +} diff --git a/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/app/components/background-changer-input/background-changer-input.component.spec.ts b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/app/components/background-changer-input/background-changer-input.component.spec.ts new file mode 100644 index 0000000..6d89124 --- /dev/null +++ b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/app/components/background-changer-input/background-changer-input.component.spec.ts @@ -0,0 +1,38 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {ComponentFixture, TestBed} from '@angular/core/testing'; + +import {BackgroundChangerInputComponent} from './background-changer-input.component'; + +describe('BackgroundChangerComponent', () => { + let component: BackgroundChangerInputComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [BackgroundChangerInputComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(BackgroundChangerInputComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/app/components/background-changer-input/background-changer-input.component.ts b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/app/components/background-changer-input/background-changer-input.component.ts new file mode 100644 index 0000000..24bc9a9 --- /dev/null +++ b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/app/components/background-changer-input/background-changer-input.component.ts @@ -0,0 +1,101 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + Component, + EventEmitter, + Output, + OnInit, + OnDestroy, +} from '@angular/core'; +import {DomSanitizer} from '@angular/platform-browser'; +import {ImageService} from 'src/app/services/image/image.service'; + +@Component({ + selector: 'app-background-changer-input', + templateUrl: './background-changer-input.component.html', + styleUrls: ['./background-changer-input.component.scss'], +}) +export class BackgroundChangerInputComponent implements OnInit, OnDestroy { + @Output() emitSearch = new EventEmitter(); + selectedFile: File | null = null; + imageUrl: any; + userPhotoUrl: string | null = null; + isUserPhotoUrl = false; + + constructor( + private sanitizer: DomSanitizer, + private _ImageService: ImageService + ) {} + + ngOnInit(): void {} + + ngOnDestroy(): void {} + + onFileSelected(event: any) { + if (event) { + this.selectedFile = event.target.files[0]; + if (this.selectedFile) { + const reader = new FileReader(); + reader.onload = (e: any) => { + this.imageUrl = this.sanitizer.bypassSecurityTrustUrl( + e.target.result + ); + this._ImageService.setSelectedImage( + this.selectedFile, + e.target.result, + false + ); + }; + reader.readAsDataURL(this.selectedFile); + this.isUserPhotoUrl = false; + } + } + } + + loadImageFromUserUrl(url: string) { + fetch(url) + .then(response => response.blob()) + .then(blob => { + this.selectedFile = new File([blob], 'user_photo.jpg', { + type: blob.type, + }); + const reader = new FileReader(); + reader.onload = (e: any) => { + this.imageUrl = this.sanitizer.bypassSecurityTrustUrl( + e.target.result + ); + this._ImageService.setSelectedImage( + this.selectedFile, + e.target.result, + true + ); + }; + reader.readAsDataURL(this.selectedFile); + }); + } + + onSubmit(): void { + if (this.selectedFile) { + this.emitSearch.emit({ + file: this.selectedFile, + isUserPhotoUrl: this.isUserPhotoUrl, + }); + } else { + console.log('No image selected to submit.'); + } + } +} diff --git a/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/app/components/footer/footer.component.html b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/app/components/footer/footer.component.html new file mode 100644 index 0000000..28f05d9 --- /dev/null +++ b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/app/components/footer/footer.component.html @@ -0,0 +1,25 @@ + + +

    + +

    + +

    diff --git a/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/app/components/footer/footer.component.scss b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/app/components/footer/footer.component.scss new file mode 100644 index 0000000..7c7700a --- /dev/null +++ b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/app/components/footer/footer.component.scss @@ -0,0 +1,99 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +.container { + padding-left: 4rem; + padding-right: 4rem; +} + +.footer { + margin-top: 1rem; +} + +.genAI { + background: linear-gradient( + 86.95deg, + #34a853 -65.34%, + #fbbc05 -12.75%, + #ea4335 25.15%, + #1a73e8 103.88% + ); + -webkit-background-clip: text; + -moz-background-clip: text; + background-clip: text; + color: transparent; +} + +:host ::ng-deep .mat-toolbar.mat-primary { + background-color: transparent; + color: #504747; + box-shadow: none; + justify-content: center; +} + +.line-spacer { + flex: 1 1 auto; +} + +.links-weight { + margin-left: 10px; + margin-right: 10px; + font-size: 14px; + font-weight: 400; + cursor: pointer; +} + +.links-bold { + margin-right: 10px; + color: #5f6368; + font-family: 'Google Sans', sans-serif !important; + .blue { + color: #4285f4; + } + .red { + color: #ea4335; + } + .yellow { + color: #fbbc04; + } + .green { + color: #34a853; + } +} + +// Small devices (landscape phones, 576px and up) +@media (min-width: 576px) { +} + +// Medium devices (tablets, 768px and up) +@media (min-width: 768px) { +} + +// Large devices (desktops, 992px and up) +@media (min-width: 992px) { +} + +// X-Large devices (large desktops, 1200px and up) +@media (min-width: 1200px) { + .footer { + position: fixed; + bottom: 0; + } +} + +// XX-Large devices (larger desktops, 1400px and up) +@media (min-width: 1400px) { +} diff --git a/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/app/components/footer/footer.component.spec.ts b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/app/components/footer/footer.component.spec.ts new file mode 100644 index 0000000..420323c --- /dev/null +++ b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/app/components/footer/footer.component.spec.ts @@ -0,0 +1,38 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {ComponentFixture, TestBed} from '@angular/core/testing'; + +import {FooterComponent} from './footer.component'; + +describe('FooterComponent', () => { + let component: FooterComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [FooterComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(FooterComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/app/components/footer/footer.component.ts b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/app/components/footer/footer.component.ts new file mode 100644 index 0000000..a38bb6c --- /dev/null +++ b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/app/components/footer/footer.component.ts @@ -0,0 +1,24 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {Component} from '@angular/core'; + +@Component({ + selector: 'app-footer', + templateUrl: './footer.component.html', + styleUrls: ['./footer.component.scss'], +}) +export class FooterComponent {} diff --git a/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/app/components/header/header.component.html b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/app/components/header/header.component.html new file mode 100644 index 0000000..287c2cf --- /dev/null +++ b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/app/components/header/header.component.html @@ -0,0 +1,115 @@ + + + + diff --git a/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/app/components/header/header.component.scss b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/app/components/header/header.component.scss new file mode 100644 index 0000000..a8dac4e --- /dev/null +++ b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/app/components/header/header.component.scss @@ -0,0 +1,640 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +.container { + margin: auto; +} + +.logo-text { + font-family: + Google Sans, + Helvetica Neue, + sans-serif; + font-style: normal; + font-size: 22px; + line-height: 128.7%; + letter-spacing: normal; + color: #2d2c2cbf; +} + +.gradient-text { + font-family: + Google Sans, + Helvetica Neue, + sans-serif; + font-style: normal; + font-size: 1.3rem; + text-align: center; + line-height: 128.7%; + letter-spacing: normal; + background: linear-gradient( + 89.9deg, + rgba(66, 133, 244, 0.95) 21.17%, + #a488f5 44.34%, + rgba(234, 67, 53, 0.88) 81.26% + ); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; + text-fill-color: transparent; +} + +.links-pointer { + cursor: pointer; +} + +.logo-icon { + height: 35px; + width: 125px; + cursor: pointer; +} + +// .profile-container { +// margin-left: 1%; +// font-family: 'Google Sans', sans-serif !important; + +// .circle { +// display: flex; +// justify-content: center; +// width: 40px; +// height: 40px; +// border: 5px solid rgb(255, 255, 255); +// border-radius: 60%; +// overflow: hidden; +// } + +// .circle img { +// width: 100%; +// height: 100%; +// object-fit: cover; +// } +// } + +.card-inside-right { + display: flex; + flex-direction: column; + align-items: flex-start; + color: #3e4245; + font-size: 1rem; + font-weight: 400; + font-family: 'Google Sans', sans-serif !important; + border-radius: 5px; + letter-spacing: 0.4px; + + .container-circle { + width: 100%; + height: 100%; + object-fit: cover; + border-radius: 50px; + padding-top: 4%; + padding-bottom: 4%; + } + + .name { + display: flex; + align-items: center; + font-size: 1rem; + color: #3e4245; + font-weight: 500; + padding: 1.3em 0.4em; + flex-direction: column; + gap: 10px; + background: #dde3ea !important; + width: -webkit-fill-available; + .sub-title { + color: #3e4245; + } + + margin: auto; + } + + .company { + display: flex; + align-items: center; + padding: 0.3em 1em; + width: -webkit-fill-available; + + .logo { + padding-right: 2%; + color: #5f6368; + } + + .text { + min-width: 100px; + + .sub-title { + font-size: 0.85rem; + margin-bottom: 10px; + color: #3e4245; + } + } + + margin-bottom: 10px; + } + + .logout { + background: #dde3ea; + border-radius: 20px; + font-size: 1rem; + font-weight: 500; + font-family: 'Google Sans', sans-serif !important; + margin-bottom: 1em; + cursor: pointer; + width: -webkit-fill-available; + padding: 0.3em 1em; + + .logout-container { + display: flex; + align-items: center; + + .logo { + padding-right: 4%; + color: #5f6368; + } + + .text { + .sub-title { + color: #3e4245; + font-size: 0.85rem; + font-family: 'Google Sans', sans-serif !important; + } + } + } + } +} + +//------------css of card end-------------------// + +.image { + width: 35px; + height: 35px; +} + +.fixed-top { + z-index: 1000; + position: relative; +} + +.top-nav { + background-color: transparent; + padding: 2rem 1rem; + + .parent-container { + .left-heading { + display: flex; + align-items: center; + cursor: pointer; + } + + .logo-icon { + height: 4.2em; + width: 4em; + cursor: pointer; + background-image: url('../../../assets/images/new-pdc-logo.svg'); + background-size: 443px; + background-repeat: round; + border-radius: 28px; + margin-right: 1em; + } + + .user-icon { + height: 50px; + width: 50px; + cursor: pointer; + border-radius: 28px; + } + + .right-menu-items { + display: flex; + align-items: center; + justify-content: center; + + .profile-container { + margin-left: 1%; + font-family: 'Google Sans', sans-serif !important; + + .circle { + display: flex; + justify-content: center; + width: 35px; + height: 35px; + border-radius: 60%; + overflow: hidden; + } + + .circle img { + width: 100%; + height: 100%; + object-fit: cover; + } + } + } + } +} + +.icon-color { + color: #5f6368; + font-size: 24px; +} + +::ng-deep .quick-link.mat-mdc-menu-panel.mat-mdc-menu-panel { + width: 420px !important; + display: flex; + flex-direction: column; + background: #f0f4f9; + + .mat-mdc-menu-content { + padding: 0.5rem; + } + + .ql-container { + width: 100%; + display: flex; + flex-direction: column; + + .ql-child-container { + display: flex; + flex-direction: row; + text-wrap: nowrap; + justify-content: space-between; + align-items: flex-end; + } + + a, + a:active { + text-decoration: none; + } + + .align-link-content { + display: flex; + flex-direction: row; + align-items: baseline; + gap: 10px; + width: 100% !important; + box-shadow: 0px 4px 4px -6px #b9b8b8 !important; + } + + .no-box-shadow { + box-shadow: none !important; + } + + .icon-container { + display: flex; + alignment-baseline: after-edge; + + mat-icon { + color: #161a1d !important; + } + } + } + + .ql-header { + margin: 1rem; + margin-left: 1.7rem; + } + + .avatar { + width: 40px; + height: 40px; //<--use the size you choose + border-radius: 100%; + text-align: center; + } + + div.link-label { + color: #161a1d; + font-size: 16px; + font-style: normal; + font-weight: 400; + line-height: 24px; + letter-spacing: 0.4px; + height: 50px; + margin: 0px 5px; + width: 57%; + } + + .avatar span { + line-height: 40px; + font-size: 16px; + } + + .link-button { + margin: 1rem; + height: 41px; + width: 100% !important; + } +} + +.notification-banner-container { + z-index: 999; + position: relative; + width: inherit; + display: flex; + flex-direction: row; + height: 8%; + background: #f6f9fe; + align-content: center; + flex-wrap: wrap; + padding-left: 1%; + gap: 1%; + margin-bottom: 0.2%; + + .amber { + color: #fbc645; + } + + .red { + color: red; + } + + .blue { + color: #4285f4; + } +} + +.red-border { + border-left: 5px solid red; +} + +.amber-border { + border-left: 5px solid #fbc645; +} + +.blue-border { + border-left: 5px solid #4285f4; +} + +.notification-text { + display: flex; + flex-direction: row; + align-items: center; + gap: 15px; + width: 70%; +} + +.notification-buttons-container { + width: 26%; + justify-content: flex-end; + display: flex; + flex-direction: row; + gap: 15px; + + button { + font-family: 'Google Sans', sans-serif !important; + font-style: normal; + font-weight: 500; + line-height: 23px; + font-size: 14px; + } + + .dismiss-button { + letter-spacing: 0.4px; + color: #4285f4; + } + + .learn-more-button { + color: #fff !important; + background: #4285f4 !important; + border-radius: 30px; + letter-spacing: unset; + box-shadow: none; + } +} + +::ng-deep .profile-menu.mat-mdc-menu-panel.mat-mdc-menu-panel { + max-width: 320px !important; + width: 300px !important; +} + +::ng-deep .profile-menu .mat-mdc-menu-content { + padding: 0% !important; +} + +.action-item-container { + width: -webkit-fill-available; + padding-inline: 5%; + padding-top: 2%; + padding-bottom: 2%; +} + +.logout-button { + background: white !important; + border: 1px solid #4285f4 !important; +} + +.logout-button-text { + color: #4285f4 !important; +} + +.app-version-chip { + font-size: 9px; + border-radius: -2px; + padding-inline: 8px; + line-height: 16px; + border-radius: 11px; + margin-left: 8px; + color: #474c55; + background: + linear-gradient( + 92.1deg, + #bdd1f3 19.16%, + rgba(199, 183, 245, 0.9) 79%, + rgba(235, 165, 158, 0.9) 135.24% + ), + linear-gradient( + 272.04deg, + rgba(158, 188, 237, 0.7) 14.93%, + rgba(158, 188, 237, 0) 104.51% + ) !important; +} + +.bypassLoginLogo { + background: aliceblue; + color: cornflowerblue; + align-items: center; + cursor: pointer; +} + +.bypassMenuContainer { + display: flex; + justify-content: center; + background: aliceblue; + width: 42% !important; + height: 40% !important; + border-radius: 65px !important; +} + +.bypassMenuLogo { + font-size: 6rem; + height: 6rem; + width: 6rem; +} + +.chat-footer { + min-height: 60px; + display: flex; + border: 1px solid #dde6f6; + box-shadow: 0px 4px 18px rgba(195, 209, 226, 0.25); + border-radius: 30px; + width: 66%; + margin-top: 25px; + // box-shadow: 5px 4px 24px rgba(66, 133, 244, 0.15); + + .chat-textarea { + flex-grow: 1; + display: flex; + align-items: center; + + input { + width: 100%; + height: 45px; + border: 0px; + border-radius: 4px; + resize: none; + outline: none; + padding: 14px; + font-family: 'Google Sans', sans-serif !important; + box-sizing: border-box; + -ms-overflow-style: none; + scrollbar-width: none; + font-size: 0.95rem; + line-height: 1.1rem; + color: #534646; + margin-left: 20px; + } + + input::-webkit-scrollbar { + display: none; + } + } + + .micIcon { + display: flex; + align-items: center; + color: #1f1f1f; + margin-top: 2px; + padding-right: 1%; + + img { + max-width: 40%; + cursor: pointer; + } + } +} + +.blue-dot { + /* Ellipse 1226 */ + content: ''; + width: 8px; + height: 8px; + left: 1503px; + top: 445px; + display: flow-root; + border-radius: 15px; + + /* Blue */ + background: #4285f4; + animation: blue-transform 2s infinite; +} + +.green-dot { + content: ''; + width: 8px; + height: 8px; + left: 1522px; + top: 445px; + display: flow-root; + border-radius: 15px; + + /* Green */ + background: #0f9d58; + animation: green-transform 2s infinite; +} + +.yellow-dot { + content: ''; + width: 8px; + height: 8px; + left: 1541px; + top: 445px; + display: flow-root; + border-radius: 15px; + + /* Yellow */ + background: #f4b400; + animation: yellow-transform 2s infinite; +} + +.recording-container { + display: flex; + flex-direction: row; + gap: 10px; + cursor: pointer; + align-items: baseline; +} + +@keyframes blue-transform { + 0% { + transform: translateY(-14px); + } + + 33% { + transform: translateY(0); + } + + 66% { + transform: translateY(0); + } + + 100% { + transform: translateY(-14px); + } +} + +@keyframes green-transform { + 0% { + transform: translateY(0); + } + + 33% { + transform: translateY(-14px); + } + + 66% { + transform: translateY(0); + } + + 100% { + transform: translateY(0); + } +} + +@keyframes yellow-transform { + 0% { + transform: translateY(0); + } + + 33% { + transform: translateY(0); + } + + 66% { + transform: translateY(-14px); + } + + 100% { + transform: translateY(0); + } +} diff --git a/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/app/components/header/header.component.spec.ts b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/app/components/header/header.component.spec.ts new file mode 100644 index 0000000..90d999b --- /dev/null +++ b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/app/components/header/header.component.spec.ts @@ -0,0 +1,38 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {ComponentFixture, TestBed} from '@angular/core/testing'; + +import {HeaderComponent} from './header.component'; + +describe('HeaderComponent', () => { + let component: HeaderComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [HeaderComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(HeaderComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/app/components/header/header.component.ts b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/app/components/header/header.component.ts new file mode 100644 index 0000000..31849c9 --- /dev/null +++ b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/app/components/header/header.component.ts @@ -0,0 +1,83 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {Component, EventEmitter, Output} from '@angular/core'; +import {DomSanitizer} from '@angular/platform-browser'; +import {MatIconRegistry} from '@angular/material/icon'; +import {UserService} from 'src/app/services/user/user.service'; +import {AuthService} from '../../services/login/auth.service'; +import {environment} from 'src/environments/environment'; +import {Router} from '@angular/router'; + +const GOOGLE_CLOUD_ICON = ` + + + + + + + + +`; + +@Component({ + selector: 'app-header', + templateUrl: './header.component.html', + styleUrls: ['./header.component.scss'], +}) +export class HeaderComponent { + headerTitle: string = environment.chatbotName; + requiredLogin: string = environment.requiredLogin; + + showLoading = false; + + @Output() emitSearch: EventEmitter = new EventEmitter(); + + constructor( + iconRegistry: MatIconRegistry, + sanitizer: DomSanitizer, + public _UserService: UserService, + public authService: AuthService, + private router: Router + ) { + iconRegistry.addSvgIconLiteral( + 'google-cloud-icon', + sanitizer.bypassSecurityTrustHtml(GOOGLE_CLOUD_ICON) + ); + } + + ngOnInit(): void { + this._UserService.loadingSubject.subscribe( + loadingValue => (this.showLoading = loadingValue) + ); + } + + isSearchRoute(): boolean { + return this.router.url.startsWith('/search'); + } + + searchTerm(term: string) { + this.emitSearch.emit(term); + } + + goToManageConfig() { + this.router.navigateByUrl('/manage-config'); + } + + logout() { + this.authService.logout(); + } +} diff --git a/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/app/components/login/login.component.html b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/app/components/login/login.component.html new file mode 100644 index 0000000..ab76003 --- /dev/null +++ b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/app/components/login/login.component.html @@ -0,0 +1,63 @@ + + + diff --git a/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/app/components/login/login.component.scss b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/app/components/login/login.component.scss new file mode 100644 index 0000000..bf919c7 --- /dev/null +++ b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/app/components/login/login.component.scss @@ -0,0 +1,569 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +.login-container { + display: flex; + min-height: 100vh; + max-height: 100%; + .spinner { + display: flex; + justify-content: center; + margin: 1em; + } + + .linear-text { + background: linear-gradient( + 89.9deg, + rgba(66, 133, 244, 0.95) 21.17%, + #a488f5 44.34%, + rgba(234, 67, 53, 0.88) 81.26% + ); + -webkit-background-clip: text; + -moz-background-clip: text; + background-clip: text; + color: transparent; + } + + .heading { + font-family: 'Google Sans', sans-serif !important; + font-size: 32px; + font-style: normal; + font-weight: 500; + line-height: 150.7%; + color: white; + } + + .left-container { + background-image: + url('../../../assets/images/colibri_image_fx.jpg'), + url('../../../assets/images/crane_image_fx.jpg'); + background-repeat: no-repeat, repeat; + background-size: contain, contain; + flex: 0 1 55%; + min-height: 100%; + max-width: 60%; + align-items: center; + padding: 0.5em; + overflow: auto; + display: flex; + justify-content: center; + + .login-app-text-sub { + color: #161a1d; + font-size: 1rem; + font-style: normal; + font-weight: 400; + line-height: 150.7%; + } + + .login-app-text { + // margin-bottom: 2rem; + line-height: 1.5em; + } + + .expand-button { + --mdc-fab-container-color: #4285f41a; + --mdc-fab-icon-color: #000000; + border: 1px solid #4285f4; + --mat-mdc-fab-color: #4285f4; + height: 2.2rem; + font-size: 0.8rem; + font-weight: 400; + box-shadow: none; + } + + .expand-less-button { + --mdc-fab-container-color: rgba(92, 95, 97, 0.1); + --mdc-fab-icon-color: #000000; + border: 1px solid rgba(92, 95, 97, 0.5); + --mat-mdc-fab-color: rgba(92, 95, 97, 0.5); + margin-bottom: 1em; + height: 2.2rem; + font-size: 0.8rem; + font-weight: 400; + box-shadow: none; + } + } + + .right-container { + background-image: url('../../../assets/images/hot_air_baloon_image_fx.jpg'); + background-repeat: repeat; + background-size: cover; + flex: 0 1 45%; + min-height: 100%; + max-width: 80%; + align-items: flex-start; + display: flex; + justify-content: center; + overflow-y: auto; + scrollbar-width: thin; + + .login-card { + backdrop-filter: blur(10px); + flex-direction: column; + display: flex; + max-width: 100%; + padding: 3%; + margin: auto; + text-align: center; + + .logo-icon { + height: 145px; + width: 100%; + // margin-bottom: 2rem; + } + + .login-button { + background: #4285f4; + box-sizing: border-box; + border: 1px solid #e0e0e0; + color: #fff; + position: relative; + user-select: none; + cursor: pointer; + outline: none; + -webkit-tap-highlight-color: transparent; + display: inline-block; + font-family: 'Google Sans', sans-serif !important; + font-size: 14px; + font-weight: 500; + white-space: nowrap; + text-decoration: none; + vertical-align: baseline; + text-align: center; + margin: 0; + min-width: 100px; + line-height: 36px; + padding: 0 16px; + border-radius: 4px; + overflow: visible; + transform: translate3d(0, 0, 0); + transition: + background 400ms cubic-bezier(0.25, 0.8, 0.25, 1), + box-shadow 280ms cubic-bezier(0.4, 0, 0.2, 1); + } + + .register-text { + display: flex; + flex-direction: column; + align-items: flex-start; + margin: 1em 0em; + font-weight: 400; + color: #161a1d; + } + + .register-button { + background: white; + box-sizing: border-box; + border: 1px solid #4285f4; + color: #4285f4; + position: relative; + user-select: none; + cursor: pointer; + outline: none; + -webkit-tap-highlight-color: transparent; + display: inline-block; + font-family: 'Google Sans', sans-serif !important; + font-size: 14px; + font-weight: 500; + white-space: nowrap; + text-decoration: none; + vertical-align: baseline; + text-align: center; + margin: 0; + min-width: 64px; + line-height: 36px; + padding: 0 16px; + border-radius: 4px; + overflow: visible; + transform: translate3d(0, 0, 0); + transition: + background 400ms cubic-bezier(0.25, 0.8, 0.25, 1), + box-shadow 280ms cubic-bezier(0.4, 0, 0.2, 1); + } + } + } + + .slide { + width: 100%; + min-height: 15rem; + border-radius: 10px; + background-size: cover; + background-position: center; + font-size: 1rem; + font-style: normal; + font-weight: 400; + line-height: 200.7%; + padding-left: 0px; + + .feature-heading { + font-weight: 700; + } + } + + .slider { + position: relative; + height: 100%; + } + + .dotsContainer { + display: flex; + justify-content: center; + align-items: center; + margin-top: 2em; + } + + .dot { + margin: 0 3px; + cursor: pointer; + font-size: 18px; + color: #d0d3d7; + } + + .active { + color: #1a73e8; + background-color: #d0d3d7; + padding: 2px; + border-radius: 50%; + font-size: 12px; + } + + .material-symbols-outlined { + font-variation-settings: + 'FILL' 1, + 'wght' 400, + 'GRAD' 0, + 'opsz' 18; + } + + h6 { + position: relative; + z-index: 1; + overflow: hidden; + text-align: center; + font-size: 1.2em; + font-style: normal; + font-weight: 500; + line-height: 200.7%; + font-family: 'Google Sans', sans-serif !important; + } + + h6:before, + h6:after { + position: absolute; + top: 51%; + overflow: hidden; + width: 50%; + height: 1px; + content: '\a0'; + background: linear-gradient( + 86.95deg, + #34a853 -65.34%, + #fbbc05 -12.75%, + #ea4335 25.15%, + #1a73e8 103.88% + ); + } + + h6:before { + margin-left: -50%; + text-align: right; + } +} + +.login-buttons-container { + padding-inline: 4%; + gap: 12px; + display: flex; + flex-direction: column; + background: #f7f9ff; + padding-top: 4%; + padding-bottom: 4%; + border-radius: 5px; +} + +.back-button { + color: #5c5f61 !important; + font-weight: 400 !important; +} + +.help-container { + padding-top: 2%; + text-align: center; +} + +.blue { + color: #4285f4; + cursor: pointer; +} + +mat-form-field { + width: 100%; +} + +.user-name-container { + display: flex; + flex-direction: row; + gap: 3%; +} + +.error-message { + font-weight: 500; + font-size: 0.8rem; + letter-spacing: -0.005rem; + color: #ed0c0c; + + .mat-mdc-form-field-subscript-wrapper { + margin-top: -10px; + } +} + +.forgot-password-container { + display: flex; + flex-direction: column; + text-align: center; + padding: 4%; +} + +.forgot-password-icon-container { + width: 100%; + height: 100%; + + mat-icon { + font-size: 105px; + height: 100%; + width: 100%; + padding: 1%; + padding-top: 7%; + color: #3467bb; + } +} + +.forgot-password-heading-container { + font-size: 29px; + font-weight: 600; + font-family: 'Google Sans', sans-serif !important; + line-height: 2.5em; +} + +.forgot-password-subtext-container { + display: flex; + flex-direction: column; + text-align: center; + font-size: 19px; + gap: 10px; + line-height: 1.5em; + + mat-icon { + color: #4385f4; + } +} + +.logo-icon { + height: 170px; + width: 100%; +} + +.login-button { + width: 100%; + background: #4285f4; + box-sizing: border-box; + border: 1px solid #e0e0e0; + color: #fff; + position: relative; + user-select: none; + cursor: pointer; + outline: none; + -webkit-tap-highlight-color: transparent; + display: inline-block; + font-family: 'Google Sans', sans-serif !important; + font-size: 14px; + font-weight: 500; + white-space: nowrap; + text-decoration: none; + vertical-align: baseline; + text-align: center; + margin: 0; + min-width: 64px; + line-height: 44px; + padding: 0 16px; + border-radius: 4px; + overflow: visible; + transform: translate3d(0, 0, 0); + transition: + background 400ms cubic-bezier(0.25, 0.8, 0.25, 1), + box-shadow 280ms cubic-bezier(0.4, 0, 0.2, 1); +} + +.register-text { + display: flex; + flex-direction: column; + align-items: flex-start; + margin-bottom: 0.3em; + font-weight: 400; + color: #5c5f61; +} + +.register-button { + width: 100%; + background: white; + box-sizing: border-box; + border: 1px solid #4285f4; + color: #4285f4; + position: relative; + user-select: none; + cursor: pointer; + outline: none; + -webkit-tap-highlight-color: transparent; + display: inline-block; + font-family: 'Google Sans', sans-serif !important; + font-size: 14px; + font-weight: 500; + white-space: nowrap; + text-decoration: none; + vertical-align: baseline; + text-align: center; + margin: 0; + min-width: 64px; + line-height: 44px; + padding: 0 16px; + border-radius: 4px; + overflow: visible; + transform: translate3d(0, 0, 0); + transition: + background 400ms cubic-bezier(0.25, 0.8, 0.25, 1), + box-shadow 280ms cubic-bezier(0.4, 0, 0.2, 1); +} + +.send-email-button { + background: #40bd40 !important; + font-weight: 600 !important; + font-size: 16px !important; +} + +.close-button { + float: right !important; +} + +.forgot-password-link-container { + width: 100%; + text-align: end; + display: flex; + flex-direction: column; + padding-bottom: 4%; + z-index: 100; + position: relative; +} + +.button-css { + display: flex; + justify-content: center; + + .ok-button { + justify-content: center; + display: flex; + width: 25%; + background: #4285f4; + box-sizing: border-box; + border: 1px solid #e0e0e0; + color: #fff; + position: relative; + user-select: none; + cursor: pointer; + outline: none; + -webkit-tap-highlight-color: transparent; + font-family: 'Google Sans', sans-serif !important; + font-size: 14px; + font-weight: 500; + white-space: nowrap; + text-decoration: none; + vertical-align: baseline; + text-align: center; + margin: 0; + min-width: 64px; + line-height: 44px; + padding: 0 16px; + border-radius: 4px; + overflow: visible; + transform: translate3d(0, 0, 0); + transition: + background 400ms cubic-bezier(0.25, 0.8, 0.25, 1), + box-shadow 280ms cubic-bezier(0.4, 0, 0.2, 1); + } +} + +.icon-css { + font-size: 40px; + color: #f3f30aed; +} + +.text-css { + font-size: 15px; + line-height: 1.45em; + font-family: 'Google Sans', sans-serif !important; + color: #645a5a; +} + +.logo-container { + display: flex; + flex-direction: row; + justify-content: center; + text-align: center; + mat-icon { + height: 20% !important; + width: 100% !important; + font-size: 70px !important; + color: #4285f4 !important; + } +} + +.heading-span { + display: flex; + flex-direction: row; + gap: 8px; + align-items: center; + padding: 2%; + width: 100%; + justify-content: center; + font-size: 18px; + font-weight: 700; + padding-left: 15%; + color: #4285f4 !important; +} + +.heading-container { + display: flex; + flex-direction: row; + justify-content: center; + background: aliceblue; +} + +.sub-parent-container { + display: flex; + flex-direction: column; + text-align: center; + gap: 15px; + font-style: normal; + line-height: normal; + font-family: 'Google Sans', sans-serif !important; +} + +.info { + padding-bottom: 10px !important; + small { + color: #a8a8b5; + } +} diff --git a/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/app/components/login/login.component.spec.ts b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/app/components/login/login.component.spec.ts new file mode 100644 index 0000000..cb6b0f3 --- /dev/null +++ b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/app/components/login/login.component.spec.ts @@ -0,0 +1,38 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {ComponentFixture, TestBed} from '@angular/core/testing'; + +import {LoginComponent} from './login.component'; + +describe('LoginComponent', () => { + let component: LoginComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [LoginComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(LoginComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/app/components/login/login.component.ts b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/app/components/login/login.component.ts new file mode 100644 index 0000000..37b3e71 --- /dev/null +++ b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/app/components/login/login.component.ts @@ -0,0 +1,92 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {Component, NgZone, inject} from '@angular/core'; +import {Auth, signInWithPopup, GoogleAuthProvider} from '@angular/fire/auth'; +import {Router} from '@angular/router'; +import {AuthService} from 'src/app/services/login/auth.service'; +import {MatSnackBar} from '@angular/material/snack-bar'; +import {environment} from 'src/environments/environment'; +import {ToastMessageComponent} from '../toast-message/toast-message.component'; + +const HOME_ROUTE = '/'; + +interface LooseObject { + [key: string]: any; +} + +@Component({ + selector: 'app-login', + templateUrl: './login.component.html', + styleUrls: ['./login.component.scss'], +}) +export class LoginComponent { + private readonly auth: Auth = inject(Auth); + private readonly provider: GoogleAuthProvider = new GoogleAuthProvider(); + loader = false; + chatbotName: string = environment.chatbotName; + + constructor( + private authService: AuthService, + private router: Router, + public ngZone: NgZone, + private _snackBar: MatSnackBar + ) { + this.provider.setCustomParameters({ + prompt: 'select_account', + }); + } + + loginWithGoogle() { + this.loader = true; + signInWithPopup(this.auth, this.provider) + .then((result: any) => { + const user = result.user.toJSON(); + this.ngZone.run(() => { + this.authService.saveUserSession(user.stsTokenManager.accessToken); + this.redirect(user); + }); + }) + .catch(error => { + this.loader = false; + if (error.message !== 'Firebase: Error (auth/popup-closed-by-user).') { + this._snackBar.openFromComponent(ToastMessageComponent, { + panelClass: ['red-toast'], + verticalPosition: 'top', + horizontalPosition: 'right', + duration: 5000, + data: { + text: 'Error with SignIn. Please try again later !!!', + icon: 'cross-in-circle-white', + }, + }); + } + console.error(`Error: ${error}`); + }); + } + + redirect(user: any) { + const userDetails: LooseObject = {}; + userDetails['name'] = user.displayName; + userDetails['email'] = user.email; + userDetails['photoURL'] = user.photoURL; + userDetails['domain'] = user.domain; + (userDetails['uid'] = user.uid), + localStorage.setItem('USER_DETAILS', JSON.stringify(userDetails)); + this.loader = false; + this.router.navigate([HOME_ROUTE]); + } +} diff --git a/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/app/components/main/chat-input/chat-input.component.html b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/app/components/main/chat-input/chat-input.component.html new file mode 100644 index 0000000..cd2a76a --- /dev/null +++ b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/app/components/main/chat-input/chat-input.component.html @@ -0,0 +1,61 @@ + + + diff --git a/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/app/components/main/chat-input/chat-input.component.scss b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/app/components/main/chat-input/chat-input.component.scss new file mode 100644 index 0000000..46ef67e --- /dev/null +++ b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/app/components/main/chat-input/chat-input.component.scss @@ -0,0 +1,201 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +.chat-footer { + min-height: 60px; + display: flex; + border: 1px solid #dde6f6; + box-shadow: 0px 4px 18px rgba(195, 209, 226, 0.25); + border-radius: 30px; + backdrop-filter: blur(10px); + + .chat-textarea { + flex-grow: 1; + display: flex; + align-items: center; + + input { + width: 100%; + height: 45px; + border: 0px; + border-radius: 20px; + resize: none; + outline: none; + padding: 14px; + font-family: 'Google Sans', sans-serif !important; + box-sizing: border-box; + -ms-overflow-style: none; + scrollbar-width: none; + font-size: 0.95rem; + line-height: 1.1rem; + color: #534646; + margin-left: 20px; + } + + input::-webkit-scrollbar { + display: none; + } + } + + .micIcon { + display: flex; + align-items: center; + color: #1f1f1f; + margin-top: 2px; + padding-right: 1%; + + img { + max-width: 40%; + cursor: pointer; + } + } +} + +.recording-container { + display: flex; + flex-direction: row; + gap: 10px; + cursor: pointer; + align-items: baseline; +} + +.blue-dot { + /* Ellipse 1226 */ + content: ''; + width: 8px; + height: 8px; + left: 1503px; + top: 445px; + display: flow-root; + border-radius: 15px; + + /* Blue */ + background: #4285f4; + animation: blue-transform 2s infinite; +} + +.green-dot { + content: ''; + width: 8px; + height: 8px; + left: 1522px; + top: 445px; + display: flow-root; + border-radius: 15px; + + /* Green */ + background: #0f9d58; + animation: green-transform 2s infinite; +} + +.yellow-dot { + content: ''; + width: 8px; + height: 8px; + left: 1541px; + top: 445px; + display: flow-root; + border-radius: 15px; + + /* Yellow */ + background: #f4b400; + animation: yellow-transform 2s infinite; +} + +.file-input-wrapper { + margin-bottom: 20px; + display: flex; + flex-direction: column; + align-items: center; + gap: 10px; + + .placeholder { + color: rgba(0, 0, 0, 0.54); // Material hint text color + } + + .file-name { + font-style: italic; + color: rgba(0, 0, 0, 0.87); + } +} + +.image-preview { + margin-top: 20px; + border: 1px solid #ccc; + border-radius: 4px; + padding: 5px; + max-width: 100%; + display: flex; + justify-content: center; + + img { + max-width: 100%; + max-height: 300px; // Adjust as needed + display: block; + } +} + +.card-input-wrapper { + // min-width: 40vw; + width: 80%; + min-height: 10rem; + display: flex; + justify-content: center; + flex-wrap: wrap; + flex-direction: column; + align-items: center; + text-align: center; + border: 4px dashed lightgrey; + margin: 2rem; + padding: 1rem; + border-radius: 4px; + cursor: pointer; + transition: border 1s; + text-decoration: none; + + &:hover { + border-color: rgba(66, 133, 244, 0.95); + } +} + +.centered-mat-card { + display: flex; + flex-direction: column; + align-items: center; + align-self: center; + justify-self: center; + width: 40vw; +} + +// Small devices (landscape phones, 576px and up) +@media (min-width: 576px) { +} + +// Medium devices (tablets, 768px and up) +@media (min-width: 768px) { +} + +// Large devices (desktops, 992px and up) +@media (min-width: 992px) { +} + +// X-Large devices (large desktops, 1200px and up) +@media (min-width: 1200px) { +} + +// XX-Large devices (larger desktops, 1400px and up) +@media (min-width: 1400px) { +} diff --git a/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/app/components/main/chat-input/chat-input.component.spec.ts b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/app/components/main/chat-input/chat-input.component.spec.ts new file mode 100644 index 0000000..158ea34 --- /dev/null +++ b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/app/components/main/chat-input/chat-input.component.spec.ts @@ -0,0 +1,38 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {ComponentFixture, TestBed} from '@angular/core/testing'; + +import {ChatInputComponent} from './chat-input.component'; + +describe('ChatInputComponent', () => { + let component: ChatInputComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ChatInputComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(ChatInputComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/app/components/main/chat-input/chat-input.component.ts b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/app/components/main/chat-input/chat-input.component.ts new file mode 100644 index 0000000..771a3fd --- /dev/null +++ b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/app/components/main/chat-input/chat-input.component.ts @@ -0,0 +1,89 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {Component, EventEmitter, Output} from '@angular/core'; +import {ActivatedRoute} from '@angular/router'; +import {SpeechToTextService} from 'src/app/services/speech-to-text'; + +@Component({ + selector: 'app-chat-input', + templateUrl: './chat-input.component.html', + styleUrls: ['./chat-input.component.scss'], +}) +export class ChatInputComponent { + isRecording = false; + transcribedText = ''; + mediaRecorder: MediaRecorder | undefined; + audioChunks: Blob[] = []; + + term = ''; + @Output() emitSearch: EventEmitter = new EventEmitter(); + + constructor( + private speechToTextService: SpeechToTextService, + private route: ActivatedRoute + ) { + const query = this.route.snapshot.queryParamMap.get('q'); + if (query) { + this.term = query; + } + } + + ngOnInit() { + navigator.mediaDevices + .getUserMedia({audio: true}) + .then(stream => this.setupMediaRecorder(stream)) + .catch(err => { + console.error(err); + }); + } + + searchTerm() { + this.emitSearch.emit(this.term); + } + + setupMediaRecorder(stream: MediaStream) { + this.mediaRecorder = new MediaRecorder(stream); + this.mediaRecorder.ondataavailable = event => + this.audioChunks.push(event.data); + this.mediaRecorder.onstop = () => this.sendAudioToGCP(); + } + + startRecording() { + this.isRecording = true; + this.audioChunks = []; + if (this.mediaRecorder) this.mediaRecorder.start(); + } + + stopRecording() { + this.isRecording = false; + if (this.mediaRecorder) this.mediaRecorder.stop(); + } + + async sendAudioToGCP() { + const audioBlob = new Blob(this.audioChunks); + (await this.speechToTextService.transcribeAudio(audioBlob)).subscribe( + (response: any) => { + this.term = response[0]; + this.searchTerm(); + }, + (error: any) => { + // Handle errors + console.error('Error transcribing audio:', error); + } + ); + } +} diff --git a/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/app/components/main/main.component.html b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/app/components/main/main.component.html new file mode 100644 index 0000000..5335586 --- /dev/null +++ b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/app/components/main/main.component.html @@ -0,0 +1,31 @@ + + + +
    +
    +
    +
    +
    + Hey, {{ savedUser.name ? savedUser.name!.split(' ')[0] + '!' : '' }} +
    +
    Upload your photo and start now!
    +
    +
    +
    + + +
    diff --git a/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/app/components/main/main.component.scss b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/app/components/main/main.component.scss new file mode 100644 index 0000000..dd537b5 --- /dev/null +++ b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/app/components/main/main.component.scss @@ -0,0 +1,1110 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@import url('https://fonts.googleapis.com/icon?family=Material+Icons|Material+Icons+Outlined'); + +:host + ::ng-deep + mat-form-feild + .mat-form-field-appearance-outline + .mat-form-field-outline-start { + color: red; + border-radius: 50px !important; + font-size: 10px !important; +} + +:host + ::ng-deep + .mdc-text-field--outlined + .mdc-notched-outline + .mdc-notched-outline__leading { + border-radius: 28px 0 0 28px !important; + min-width: 28px !important; + border-color: #babfc4 !important; +} + +:host + ::ng-deep + .mdc-text-field--outlined + .mdc-notched-outline + .mdc-notched-outline__trailing { + border-radius: 0 28px 28px 0 !important; + border-color: #babfc4 !important; +} + +:host ::ng-deep .mat-form-field-outline-end { + border-radius: 0 28px 28px 0 !important; +} + +:host ::ng-deep .mat-mdc-card { + box-shadow: + 0px 2px 1px -1px rgb(217 220 224), + 0px 1px 1px 0px rgba(0, 0, 0, 0.14), + 0px 1px 3px 0px rgb(229 232 234) !important; +} + +:host ::ng-deep .mat-mdc-standard-chip.mat-primary.mat-mdc-chip-selected, +.mat-mdc-standard-chip.mat-primary.mat-mdc-chip-highlighted { + --mdc-chip-elevated-container-color: #edf3fe; + --mdc-chip-elevated-disabled-container-color: #e0e0e0; + --mdc-chip-label-text-color: #4285f4; + --mdc-chip-disabled-label-text-color: #4285f4; + --mdc-chip-with-icon-icon-color: #4285f4; + --mdc-chip-with-icon-disabled-icon-color: #4285f4; + --mdc-chip-with-trailing-icon-disabled-trailing-icon-color: #4285f4; + --mdc-chip-with-trailing-icon-trailing-icon-color: #4285f4; + --mdc-chip-with-icon-selected-icon-color: #4285f4; +} + +.parent-container { + display: flex; + max-height: calc(100vh - 70px); + height: 90vh; + font-family: 'Google Sans', sans-serif !important; + overflow-y: auto; + flex-direction: column; + justify-content: center; + + .img-globe { + // position:absolute; + width: 66%; + } +} + +.wrapping-container { + width: 100%; +} + +.main-div { + font-family: 'Google Sans', sans-serif !important; +} + +.text-control { + width: 32%; +} + +h1 { + line-height: 50px; + width: 34%; +} + +h2 { + font-family: 'Google Sans', sans-serif !important; +} + +.header { + -webkit-background-clip: text; + -webkit-text-fill-color: #48b975; + background-clip: text; + font-size: 3rem; + text-align: center; + font-weight: bold; + font-weight: 700; + -webkit-font-smoothing: antialiased; + height: 100%; + font-family: 'Google Sans', sans-serif; +} + +.send-button { + color: #babfc4; +} + +.logo-icon { + height: 120px; + width: 400px; + margin-bottom: 90px; + margin-left: -20px; +} + +.user-info { + display: flex; + justify-content: center; +} + +.user-info-column { + display: column; +} + +.card-outer-div { + .card-inside-left { + .circle { + width: 100px; + height: 100px; + border: 5px solid rgb(255, 255, 255); + border-radius: 50%; + overflow: hidden; + box-shadow: + 0px 2px 1px -1px rgba(0, 0, 0, 0.2), + 0px 1px 1px 0px rgba(0, 0, 0, 0.14), + 0px 1px 3px 0px rgba(0, 0, 0, 0.12); + } + + .circle img { + width: 100%; + height: 100%; + object-fit: cover; + } + } + + .card-inside-right { + width: 80%; + display: flex; + flex-direction: column; + align-items: center; + color: #747474; + font-size: 0.8rem; + font-weight: 500; + + .name { + .title { + font: 1rem; + } + + .sub-title { + font-size: 0.9rem; + margin-left: 2%; + } + } + + .company { + display: flex; + align-items: center; + + .logo { + padding-right: 1%; + } + + .text { + width: 100%; + + .sub-title { + margin-left: 2%; + } + } + } + } +} + +.mat-mdc-standard-chip { + --mdc-chip-label-text-color: #4285f4; + --mdc-chip-elevated-container-color: #fff; + border: 1px solid #4285f4 !important; + font-family: 'Google Sans', sans-serif !important; + font-size: 15px !important; + font-weight: 500 !important; + padding-left: 12px !important; + padding-right: 12px !important; +} + +.sub-heading { + font-family: 'Google Sans', sans-serif; + color: #161a1d; + font-size: 30px; + line-height: 40px; + margin-top: -20px; + text-align: left; + z-index: 999; +} + +.genAI { + //background-image: linear-gradient(to left, #4285F4, #DB4437, #F4B400, #0F9D58); + // background-image: linear-gradient(to left, #34A853, #FBBC05, #EA4335, #1A73E8); + background: linear-gradient( + 86.95deg, + #34a853 -65.34%, + #fbbc05 -12.75%, + #ea4335 25.15%, + #1a73e8 103.88% + ); + -webkit-background-clip: text; + -moz-background-clip: text; + background-clip: text; + color: transparent; +} + +.margin-div { + margin-top: -20px; +} + +.chat-footer-outer { + display: flex; + align-items: center; + width: 100%; + justify-content: center; + + .chat-footer { + display: flex; + border: 1px solid #dde6f6; + box-shadow: 0px 4px 18px rgba(195, 209, 226, 0.25); + border-radius: 20px; + width: 80%; + backdrop-filter: blur(10px); + + .chat-textarea { + flex-grow: 1; + display: flex; + align-items: center; + + input { + width: 100%; + height: 45px; + border: 0px; + border-radius: 4px; + resize: none; + outline: none; + padding: 14px; + font-family: 'Google Sans', sans-serif !important; + box-sizing: border-box; + -ms-overflow-style: none; + scrollbar-width: none; + font-size: 0.95rem; + line-height: 1.1rem; + color: #534646; + margin-left: 20px; + } + + input::-webkit-scrollbar { + display: none; + } + } + + .micIcon { + display: flex; + align-items: center; + color: #1f1f1f; + margin-top: 2px; + padding-right: 1%; + + img { + max-width: 40%; + cursor: pointer; + } + } + } +} + +.text-gnosis { + color: var(--White, #fff); + font-size: 16px; + font-style: normal; + font-weight: 400; + line-height: 24px; + /* 150% */ + letter-spacing: 0.4px; +} + +.chip-cancel-button { + color: #002dff; +} + +.categories-div { + .mat-mdc-standard-chip { + --mdc-chip-label-text-color: #0f9d58; + --mdc-chip-elevated-container-color: #fff; + border: 1px solid #0f9d58 !important; + font-family: 'Google Sans', sans-serif !important; + // font-weight: 600; + } +} + +.intent-chip { + .mat-mdc-standard-chip { + --mdc-chip-label-text-color: #4285f4; + --mdc-chip-elevated-container-color: #fff; + border: 1px solid #4285f4 !important; + font-family: 'Google Sans', sans-serif !important; + // font-weight: 600; + } + + .chip-cancel-button { + color: #002dff; + } +} + +.category-chip { + .mat-mdc-standard-chip { + --mdc-chip-label-text-color: #0f9d58; + --mdc-chip-elevated-container-color: #fff; + border: 1px solid #0f9d58 !important; + font-family: 'Google Sans', sans-serif !important; + // font-weight: 600; + } + + .chip-cancel-button { + color: #007d06; + } +} + +.questions-div { + flex-direction: column; + margin-top: 2rem; + margin-bottom: 5rem; + + .intent-card { + padding: 0px; + } + + .intent-heading { + color: #787978; + font-size: 16px; + font-style: normal; + font-weight: 400; + line-height: normal; + font-family: 'Google Sans', sans-serif !important; + width: 66%; + } + + .intent-container { + border-radius: 8px; + background-color: rgba(246, 249, 254, 1); + width: 66%; + box-shadow: none !important; + flex-shrink: 0; + + .selected-question { + // margin: 0.5rem 0rem; + &:hover { + background-color: rgba(66, 133, 244, 0.1); + } + } + + .question-item { + display: flex; + justify-content: space-between; + font-family: 'Google Sans', sans-serif !important; + color: #787978; + font-size: 1em; + line-height: 24px; + } + + .question-link { + overflow: hidden; + text-overflow: ellipsis; + } + } +} + +.intent-chip-textarea ::ng-deep .mdc-evolution-chip-set__chips { + align-items: center !important; +} + +.float-button { + position: fixed; + right: 3em; + bottom: 6em; + --mdc-fab-container-color: #4285f4; +} + +.name-container { + font-size: 3.5rem; + font-weight: 400; + line-height: 4rem; + font-family: + Google Sans, + Helvetica Neue, + sans-serif; + letter-spacing: -0.03em; + background: linear-gradient( + 89.9deg, + rgba(66, 133, 244, 0.95) 0.17%, + #a488f5 7.34%, + rgba(234, 67, 53, 0.88) 24.26% + ); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; + text-fill-color: transparent; + animation: effect 2s linear; + display: flex; + flex-direction: row; + gap: 1%; + align-items: center; + justify-content: center; +} + +@keyframes effect { + 0% { + background: linear-gradient( + 89.9deg, + rgba(240, 216, 214, 0.88) 0.17%, + #ebcaec 7.34%, + rgba(194, 214, 247, 0.95) 24.26% + ); + -webkit-background-clip: text; + } + + 10% { + background: linear-gradient( + 89.9deg, + rgba(227, 237, 253, 0.95) 0.17%, + #e5ddfd 7.34%, + rgba(186, 210, 248, 0.95) 24.26% + ); + -webkit-background-clip: text; + } + + 20% { + background: linear-gradient( + 89.9deg, + rgba(218, 231, 253, 0.95) 0.17%, + #dcd1fc 7.34%, + rgba(172, 202, 252, 0.95) 24.26% + ); + -webkit-background-clip: text; + } + + 30% { + background: linear-gradient( + 89.9deg, + rgba(199, 219, 252, 0.95) 0.17%, + #d0c2fa 7.34%, + rgba(161, 196, 252, 0.95) 24.26% + ); + -webkit-background-clip: text; + } + + 40% { + background: linear-gradient( + 89.9deg, + rgba(169, 201, 253, 0.95) 0.17%, + #cdbcff 7.34%, + rgba(149, 188, 250, 0.95) 24.26% + ); + -webkit-background-clip: text; + } + + 50% { + background: linear-gradient( + 89.9deg, + rgba(200, 220, 253, 0.95) 0.17%, + #bba5fc 7.34%, + rgba(203, 221, 250, 0.95) 24.26% + ); + -webkit-background-clip: text; + } + + 60% { + background: linear-gradient( + 89.9deg, + rgba(227, 237, 253, 0.95) 0.17%, + #e5ddfd 7.34%, + rgba(186, 210, 248, 0.95) 24.26% + ); + -webkit-background-clip: text; + } + + 70% { + background: linear-gradient( + 89.9deg, + rgba(183, 210, 253, 0.95) 0.17%, + #e5ddfd 7.34%, + rgba(248, 217, 215, 0.88) 24.26% + ); + -webkit-background-clip: text; + } + + 80% { + background: linear-gradient( + 89.9deg, + rgba(122, 172, 252, 0.95) 0.17%, + #e5ddfd 7.34%, + rgba(247, 182, 176, 0.88) 24.26% + ); + -webkit-background-clip: text; + } + + 90% { + background: linear-gradient( + 89.9deg, + rgba(94, 148, 236, 0.95) 0.17%, + #b49ff1 7.34%, + rgba(240, 121, 111, 0.88) 24.26% + ); + -webkit-background-clip: text; + } + + 100% { + background: linear-gradient( + 89.9deg, + rgba(66, 133, 244, 0.95) 0.17%, + #a488f5 7.34%, + rgba(234, 67, 53, 0.88) 24.26% + ); + -webkit-background-clip: text; + } +} + +.helper-container { + white-space: nowrap; + overflow: hidden; + font-size: 1.5rem; + font-weight: 400; + line-height: 4rem; + font-family: + Google Sans, + Helvetica Neue, + sans-serif; + letter-spacing: -0.03em; + color: #8f9391; + padding-bottom: 3rem; + opacity: 0.5; + animation: + typing 2s steps(22), + blink 0.5s step-end infinite alternate; + justify-content: center; + display: flex; +} + +@keyframes typing { + from { + width: 0; + } +} + +@keyframes blink { + 50% { + border-color: transparent; + } +} + +.quick-bot-introduction-container { + width: 100%; + display: flex; + flex-direction: row; + justify-content: center; + + .introduction-sub-container { + width: 66%; + justify-content: center; + text-align: center; + display: flex; + flex-direction: column; + padding-top: 2%; + padding-bottom: 2%; + background: + radial-gradient(white, #fafcff) padding-box, + linear-gradient( + 89.86deg, + #34a853 -40.93%, + #fbbc05 4.92%, + #ea4335 37.97%, + #1a73e8 106.62% + ) + border-box; + border-radius: 24px; + border: 1px solid transparent; + } + + .logo-image-container { + img { + height: 110px; + width: 110px; + } + } + + .logo-text-heading { + font-style: normal; + font-weight: 500; + font-size: 28px; + line-height: 202%; + /* or 65px */ + text-align: center; + + background: linear-gradient( + 89.86deg, + #34a853 -40.93%, + #fbbc05 4.92%, + #ea4335 37.97%, + #1a73e8 106.62% + ); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; + text-fill-color: transparent; + } + + .logo-text-subheading { + font-style: normal; + font-weight: 500; + font-size: 21px; + line-height: 202%; + /* or 48px */ + text-align: center; + } + + .blue { + color: #4285f4; + } +} + +.helpButton { + position: fixed !important; + bottom: 45px !important; + right: 20px !important; + background-color: #4285f4 !important; + color: white !important; + text-decoration: none !important; + border-radius: 60px !important; + height: 41px !important; + display: inline-flex !important; + align-items: center !important; + overflow: hidden !important; + width: auto !important; + max-width: 41px !important; + -webkit-transition: max-width 0.5s !important; + transition: max-width 0.5s !important; + z-index: 1000 !important; + + &:hover { + max-width: 300px !important; + } + + .text { + white-space: nowrap !important; + padding-right: 15px !important; + font-size: 15px !important; + font-weight: 600 !important; + } +} + +::placeholder { + color: #646566; +} + +.intent-container-box { + box-sizing: border-box; + width: 239px; + height: 239px; + border-radius: 14px; + transition-delay: 2s; + transition: width 8s; + padding: 1%; + cursor: pointer; +} + +.selected-intent-box { + box-sizing: border-box; + height: 239px; + border-radius: 14px; + transition-delay: 2s; + transition: width 8s; + cursor: pointer; + width: max-content !important; + max-width: 100%; + display: flex; + flex-direction: column; + gap: 1%; + padding: 1%; + background: + linear-gradient( + 92.1deg, + #bdd1f3 19.16%, + rgb(199 183 245 / 90%) 79%, + rgb(235 165 158 / 90%) 135.24% + ), + linear-gradient( + 272.04deg, + rgba(158, 188, 237, 0.7) 14.93%, + rgba(158, 188, 237, 0) 104.51% + ) !important; +} + +.cards-outer-container { + width: 65%; + overflow: auto; + scrollbar-width: none; +} + +.cards-outer-container:after { + z-index: -1; + content: ''; + width: 10%; + opacity: 0.5 !important; +} + +.cards-container-all-categories { + width: max-content; + display: flex; + overflow-x: auto; + gap: 1%; + max-width: 160%; + min-width: 100%; + scrollbar-width: none; +} + +.cards-container-few-categories { + width: max-content; + display: flex; + overflow-x: auto; + gap: 1%; + max-width: 160%; + min-width: 100%; + scrollbar-width: none; + justify-content: center; + padding-inline: 5%; +} + +.intent-container-box:hover { + background: #dde3ea !important; +} + +.card-detail-container { + width: 100%; + min-width: 250px; + display: flex; + flex-direction: row; +} + +.selected-intent-suggested-question { + gap: 4%; + display: flex !important; + flex-direction: column; +} + +.suggested-question-chip-row { + margin-top: 0.5rem; + + .suggested-question-chip { + display: flex; + flex-direction: row; + margin: 0.5rem; + padding: 0.5rem; + width: 100%; + + .suggested-question-icon { + padding-right: 0.25rem; + } + + .suggested-question-text { + margin-left: 0.25rem; + + width: 100%; + } + } +} + +.close-button-container { + display: none !important; +} + +.expand-close-button-container { + display: block !important; + + .close-button { + margin-top: -10px !important; + } +} + +.card-heading { + padding-top: 2.5%; + font-size: 17px; + color: #1f1f1f; + width: 90%; +} + +.card-content { + font-size: 13px; + color: #646566; + padding-top: 4%; + white-space: break-spaces; + text-wrap: balance; + padding-right: 3%; + text-align: left; + width: 85%; +} + +::ng-deep .mat-mdc-snack-bar-container { + &.green-snackbar { + --mdc-snackbar-container-color: #0f9d58; + --mat-mdc-snack-bar-button-color: #fff; + --mdc-snackbar-supporting-text-color: #fff; + } + + &.red-snackbar { + --mdc-snackbar-container-color: #e9103f; + --mat-mdc-snack-bar-button-color: #fff; + --mdc-snackbar-supporting-text-color: #fff; + } +} + +::ng-deep .mat-mdc-dialog-surface { + justify-content: center !important; + display: flex !important; + flex-direction: column !important; + text-align: center !important; + background: #ffffff; + border-radius: 20px !important; + box-sizing: border-box; + box-shadow: none !important; + + .badge-prompt-parent-container { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + text-align: center; + + .badge-greetings-container { + width: 100%; + display: flex; + flex-direction: row; + justify-content: center; + + img { + width: 45%; + } + } + + .badge-text-container { + font-style: normal; + font-weight: 400; + font-size: 17px; + line-height: 45px; + display: flex; + align-items: center; + color: rgba(92, 95, 97, 0.7); + } + } +} + +.blue-dot { + /* Ellipse 1226 */ + content: ''; + width: 8px; + height: 8px; + left: 1503px; + top: 445px; + display: flow-root; + border-radius: 15px; + + /* Blue */ + background: #4285f4; + animation: blue-transform 2s infinite; +} + +.green-dot { + content: ''; + width: 8px; + height: 8px; + left: 1522px; + top: 445px; + display: flow-root; + border-radius: 15px; + + /* Green */ + background: #0f9d58; + animation: green-transform 2s infinite; +} + +.yellow-dot { + content: ''; + width: 8px; + height: 8px; + left: 1541px; + top: 445px; + display: flow-root; + border-radius: 15px; + + /* Yellow */ + background: #f4b400; + animation: yellow-transform 2s infinite; +} + +.recording-container { + display: flex; + flex-direction: row; + gap: 10px; + cursor: pointer; + align-items: baseline; +} + +@keyframes blue-transform { + 0% { + transform: translateY(-14px); + } + + 33% { + transform: translateY(0); + } + + 66% { + transform: translateY(0); + } + + 100% { + transform: translateY(-14px); + } +} + +@keyframes green-transform { + 0% { + transform: translateY(0); + } + + 33% { + transform: translateY(-14px); + } + + 66% { + transform: translateY(0); + } + + 100% { + transform: translateY(0); + } +} + +@keyframes yellow-transform { + 0% { + transform: translateY(0); + } + + 33% { + transform: translateY(0); + } + + 66% { + transform: translateY(-14px); + } + + 100% { + transform: translateY(0); + } +} + +.tooltip-container { + display: flex; + width: 100%; + justify-content: center; + align-items: baseline; + padding-top: 2%; + padding-bottom: 2%; + .tooltip-sub-container { + width: 66%; + display: flex; + align-items: baseline; + justify-content: center; + } +} + +.tooltip-icon-container { + display: flex; + flex-direction: row; + // align-self: flex-end; + gap: 10px; + flex-wrap: nowrap; + text-wrap: nowrap; + align-self: center; + color: #5f6368; + + .tooltip-label { + font-family: 'Roboto'; + font-style: normal; + font-weight: 500; + font-size: 16px; + line-height: 20px; + text-align: center; + } + + mat-icon { + font-size: 22px !important; + } +} + +.tooltip-text-container { + flex-wrap: wrap; + word-wrap: break-word; + justify-content: flex-start; + display: flex; + color: #5f6368; + .tooltip-text { + font-family: 'Roboto'; + font-style: normal; + font-weight: 400; + font-size: 16px; + line-height: 20px; + } +} + +.dismiss-container { + font-family: 'Roboto'; + font-style: normal; + font-weight: 500; + font-size: 14px; + line-height: 20px; + /* identical to box height, or 143% */ + text-align: center; + + /* Blue */ + color: #4285f4; + padding-left: 1%; + cursor: pointer; +} + +.dismiss-container:hover { + text-decoration: underline; +} + +.journey-chips { + max-height: 300px; + overflow-y: auto; +} + +::ng-deep .mat-mdc-standard-chip:hover { + background: linear-gradient( + 92.1deg, + #9ebced 19.16%, + rgba(164, 136, 245, 0.9) 79% + ) !important; + --mdc-chip-label-text-color: #fff; +} + +.suggested-question-container { + width: 60%; +} + +// Small devices (landscape phones, 576px and up) +@media (min-width: 576px) { + .helper-container { + font-size: 2rem; + } +} + +// Medium devices (tablets, 768px and up) +@media (min-width: 768px) { + .chat-footer-outer { + .chat-footer { + min-height: 60px; + width: 66%; + } + } + + .helper-container { + font-size: 3.5rem; + } +} + +// Large devices (desktops, 992px and up) +@media (min-width: 992px) { +} + +// X-Large devices (large desktops, 1200px and up) +@media (min-width: 1200px) { +} + +// XX-Large devices (larger desktops, 1400px and up) +@media (min-width: 1400px) { +} diff --git a/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/app/components/main/main.component.spec.ts b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/app/components/main/main.component.spec.ts new file mode 100644 index 0000000..66e7fd9 --- /dev/null +++ b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/app/components/main/main.component.spec.ts @@ -0,0 +1,38 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {ComponentFixture, TestBed} from '@angular/core/testing'; + +import {MainComponent} from './main.component'; + +describe('MainComponent', () => { + let component: MainComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [MainComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(MainComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/app/components/main/main.component.ts b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/app/components/main/main.component.ts new file mode 100644 index 0000000..46ba53a --- /dev/null +++ b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/app/components/main/main.component.ts @@ -0,0 +1,61 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {Component, OnDestroy} from '@angular/core'; +import {UserService} from 'src/app/services/user/user.service'; +import {Router} from '@angular/router'; +import {MatDialog} from '@angular/material/dialog'; +import {ReplaySubject} from 'rxjs'; +import {SearchResponse} from 'src/app/models/search.model'; + +@Component({ + selector: 'app-main', + templateUrl: './main.component.html', + styleUrls: ['./main.component.scss'], +}) +export class MainComponent implements OnDestroy { + private readonly destroyed = new ReplaySubject(1); + term = ''; + showResults = false; + searchResults: SearchResponse = { + summary: undefined, + results: [], + totalSize: 0, + }; + savedUser; + + constructor( + public userService: UserService, + private router: Router, + public dialog: MatDialog + ) { + this.savedUser = userService.getUserDetails(); + } + + goToResults(file: File) { + this.router.navigate(['/search'], { + queryParams: { + q: "Change the background by adding a beautiful, sprawling city with skyscrapers to this professional and elegant LinkedIn profile picture. It's dusk, and you can still see the sun before it sets.", + filename: file.name, + }, + }); + } + + ngOnDestroy() { + this.destroyed.next(); + this.destroyed.complete(); + } +} diff --git a/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/app/components/main/search-results/search-results.component.html b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/app/components/main/search-results/search-results.component.html new file mode 100644 index 0000000..bab0a40 --- /dev/null +++ b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/app/components/main/search-results/search-results.component.html @@ -0,0 +1,138 @@ + + + + +
    +
    +
    +

    Imagen 3 Edition for the entire image

    +
    +
    + + +
    +
    +
    + +
    +

    Imagen 3 Edition for just the background

    +
    +
    + + +
    +
    +
    +
    +
    + + +
    +
    +
    + +
    + + AI Model + + + {{ model.viewValue }} + + + + +
    + Number of Results + + + +
    + +
    + Mask Dilation (only for background editing): + {{ selectedMaskDistilation }} + + + +
    + +
    + +
    + +
    + +
    +
    +
    diff --git a/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/app/components/main/search-results/search-results.component.scss b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/app/components/main/search-results/search-results.component.scss new file mode 100644 index 0000000..951d397 --- /dev/null +++ b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/app/components/main/search-results/search-results.component.scss @@ -0,0 +1,389 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +.overview-container { + display: flex; + justify-content: center; + padding-top: 2%; + padding-right: 1.5%; + + .overview-sub-container { + width: 66%; + background: linear-gradient( + 92.1deg, + #9ebcedb2 19.16%, + rgba(163, 136, 245, 0.651) 79%, + rgba(234, 68, 53, 0.493) 135.24% + ); + border-radius: 12px; + .overview-text { + font-family: 'Roboto'; + font-style: normal; + font-weight: 500; + font-size: 20px; + line-height: 24px; + color: #5c5f61; + padding: 2%; + } + .overview-content { + font-family: 'Roboto'; + font-style: normal; + font-weight: 400; + font-size: 19px; + line-height: 30px; + color: #161a1d; + padding-left: 2%; + padding-right: 2%; + padding-bottom: 2%; + } + + .overview-button { + button { + font-family: 'Roboto'; + font-style: normal; + font-weight: 400; + font-size: 20px; + line-height: 24px; + color: #4285f4; + padding: 40px; + } + } + } +} + +.header-container { + font-family: 'Roboto'; + font-style: normal; + font-weight: 400; + font-size: 16px; + line-height: 19px; + color: #5f6368; + display: flex; + justify-content: flex-start; + width: 66%; +} + +.search-result:hover { + border: 1px solid #dfe0e2; + border-radius: 10px; +} + +.search-result-parent-container { + display: flex; + flex-direction: column; + width: 66%; + gap: 15px; + padding-bottom: 5%; +} + +.search-result-row-container { + display: flex; + flex-direction: row; + gap: 15px; +} + +.search-result { + padding: 1%; + padding-top: 2%; + cursor: pointer; + border: 1px solid transparent; + width: 25%; + + .title-container { + display: flex; + flex-direction: row; + font-family: 'Roboto'; + font-style: normal; + font-weight: 500; + font-size: 16px; + line-height: 31px; + color: #161a1d; + display: flex; + gap: 15px; + + .link-container { + font-size: 14px !important; + font-weight: 400 !important; + color: #676c6f !important; + } + } + + .description-container { + font-family: 'Roboto'; + font-style: normal; + font-weight: 400; + font-size: 16px; + line-height: 24px; + color: #161a1d; + opacity: 0.8; + padding: 1%; + padding-top: 4%; + } + + .overview-button { + padding-top: 2%; + button { + font-family: 'Roboto'; + font-style: normal; + font-weight: 400; + font-size: 16px; + line-height: 24px; + color: #4285f4; + } + } +} + +.padding-top { + padding-top: 0; +} + +.close-container { + display: flex; + justify-content: end; +} + +.pdf-display { + display: flex; + justify-content: center; +} + +.overview { + z-index: 1000; +} + +/* card-list.component.scss */ +.card-list { + display: flex; + gap: 20px; /* Adjust spacing between cards */ +} + +.card { + border: 1px solid #eee; + border-radius: 8px; + flex: 0 0 auto; /* Prevent cards from stretching */ + box-shadow: 2px 2px 5px rgba(0, 0, 0, 0.1); + padding: 16px; + transition: transform 0.2s; + + &:hover { + transform: scale(1.02); + box-shadow: 3px 3px 7px rgba(0, 0, 0, 0.15); + cursor: pointer; + } + + .card-content { + display: flex; + flex-direction: column; + height: 100%; + } + + .card-header { + display: flex; + align-items: center; + justify-content: center; + flex-direction: column; + margin-bottom: 10px; + + .material-symbols-outlined { + font-size: 1.2rem; + margin-right: 5px; + &.bookmark { + margin-left: auto; // Push bookmark to right + cursor: pointer; + } + } + + .card-title { + font-weight: 500; + width: 250px; + } + + img { + width: 100%; + height: 20vh; + } + } + + .card-description { + flex-grow: 1; // Allow description to take up available space + overflow: hidden; + text-overflow: ellipsis; + display: -webkit-box; + -webkit-line-clamp: 10; /* Number of lines to show */ + -webkit-box-orient: vertical; + width: 250px; + } + + .card-footer { + display: flex; + align-items: center; + margin-top: 10px; + + .material-symbols-outlined { + font-size: 1.2rem; + margin-right: 5px; + } + } +} + +.documents-container { + margin-top: 2rem; + display: flex; + justify-content: space-between; + align-items: center; + max-width: 1000px; +} + +.navigation { + display: flex; + justify-content: center; + margin-top: 20px; + + .nav-button { + border: none; + background: none; + cursor: pointer; + transition: background-color 0.2s; + + &:hover { + background-color: #f0f0f0; + } + + &:disabled { + opacity: 0.5; + cursor: default; + } + + .material-symbols-outlined { + font-size: 1.5rem; + } + } +} + +.document-viewer { + background: rgb(255, 255, 255); + border-left: 1px solid var(--md-sys-color-outline); + border-top: 1px solid var(--md-sys-color-outline); + display: block; + height: 75%; + position: fixed; + right: 0px; + bottom: 0px; + width: 35%; + z-index: 9999; + + .document-viewer-header { + -webkit-box-pack: justify; + -webkit-box-align: center; + -webkit-box-flex: 1; + align-items: center; + border-bottom: 1px solid var(--md-sys-color-outline); + display: flex; + flex: 1 1 0%; + justify-content: space-between; + padding: 8px; + } + + .document-viewer-content { + width: 100%; + height: 100%; + } +} + +.mat-grid-tile-content { + width: 60vw; +} + +.image-generation-container { + .image-preview { + padding: 0 2rem; + + .thumbnail { + width: 100px; + height: 100px; + margin-bottom: 10px; + cursor: pointer; + + .thumbnail-placeholder { + width: 100%; + height: 100%; + background-color: #f0f0f0; + + img { + width: 100%; + } + } + } + + .main-image { + flex: 1; + text-align: center; + display: flex; + justify-content: center; + align-items: center; + + img { + max-height: 50vh; + } + } + } + + .configuration { + flex: 1; + display: flex; + flex-direction: column; + + .input-area { + display: flex; + margin-bottom: 20px; + + input { + flex: 1; + padding: 8px; + border: 1px solid #ccc; + } + } + + .aspect-ratio, + .image-style { + margin-bottom: 20px; + + button { + margin: 0 10px 10px 0; + } + } + } +} + +// Small devices (landscape phones, 576px and up) +@media (min-width: 576px) { +} + +// Medium devices (tablets, 768px and up) +@media (min-width: 768px) { +} + +// Large devices (desktops, 992px and up) +@media (min-width: 992px) { +} + +// X-Large devices (large desktops, 1200px and up) +@media (min-width: 1200px) { +} + +// XX-Large devices (larger desktops, 1400px and up) +@media (min-width: 1400px) { +} diff --git a/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/app/components/main/search-results/search-results.component.spec.ts b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/app/components/main/search-results/search-results.component.spec.ts new file mode 100644 index 0000000..1c8bf77 --- /dev/null +++ b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/app/components/main/search-results/search-results.component.spec.ts @@ -0,0 +1,38 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {ComponentFixture, TestBed} from '@angular/core/testing'; + +import {SearchResultsComponent} from './search-results.component'; + +describe('SearchResultsComponent', () => { + let component: SearchResultsComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [SearchResultsComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(SearchResultsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/app/components/main/search-results/search-results.component.ts b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/app/components/main/search-results/search-results.component.ts new file mode 100644 index 0000000..2d46fd0 --- /dev/null +++ b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/app/components/main/search-results/search-results.component.ts @@ -0,0 +1,303 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {Component, OnDestroy, ViewChild, TemplateRef} from '@angular/core'; +import {SearchService} from 'src/app/services/search.service'; +import {ReplaySubject, Subscription} from 'rxjs'; +import {UserService} from 'src/app/services/user/user.service'; +import {ActivatedRoute, Router} from '@angular/router'; +import {PDF, image_name} from 'src/environments/constant'; +import {GeneratedImage} from 'src/app/models/generated-image.model'; +import {SearchRequest} from 'src/app/models/search.model'; +import {MatSnackBar} from '@angular/material/snack-bar'; +import {ToastMessageComponent} from '../../toast-message/toast-message.component'; +import {MatButtonToggleChange} from '@angular/material/button-toggle'; +import {ImageService} from 'src/app/services/image/image.service'; + +interface Imagen3Model { + value: string; + viewValue: string; +} + +interface ImageStylesModel { + value: string; + viewValue: string; +} + +@Component({ + selector: 'app-search-results', + templateUrl: './search-results.component.html', + styleUrls: ['./search-results.component.scss'], +}) +export class SearchResultsComponent implements OnDestroy { + @ViewChild('preview', {static: true}) + previewRef!: TemplateRef<{}>; + summary = ''; + private readonly destroyed = new ReplaySubject(1); + serachResult: any = []; + documents: any = []; + showDefaultDocuments = false; + images: any = []; + pdf = PDF; + imageName = image_name; + currentPage = 0; + pageSize = 8; + selectedResult: GeneratedImage | undefined; + imagen3ModelsList: Imagen3Model[] = [ + { + value: 'imagen-3.0-capability-001', + viewValue: 'Imagen 3: imagen-3.0-capability-001', + }, + ]; + selectedModel = this.imagen3ModelsList[0].value; + imageStylesList: ImageStylesModel[] = [ + {value: 'Modern', viewValue: 'Modern'}, + {value: 'Realistic', viewValue: 'Realistic'}, + {value: 'Vintage', viewValue: 'Vintage'}, + {value: 'Monochrome', viewValue: 'Monochrome'}, + {value: 'Fantasy', viewValue: 'Fantasy'}, + ]; + selectedStyle: string = this.imageStylesList[0].value; + selectedNumberOfResults = 4; + selectedMaskDistilation = 0.005; + searchRequest: SearchRequest = { + term: '', + model: this.selectedModel, + numberOfResults: this.selectedNumberOfResults, + maskDistilation: this.selectedMaskDistilation, + }; + isUserPhotoUrl = false; + private imageSubscription: Subscription; + + constructor( + private router: Router, + private route: ActivatedRoute, + private service: SearchService, + private userService: UserService, + private _snackBar: MatSnackBar, + private _ImageService: ImageService + ) { + this.imageSubscription = this._ImageService.selectedImage$.subscribe( + image => { + if (image) this.isUserPhotoUrl = image.isUserPhotoUrl; + } + ); + + const query = this.route.snapshot.queryParamMap.get('q'); + this.userService.showLoading(); + + if (!query) { + this.documents = [ + { + enhancedPrompt: 'default enhaced prompt', + raiFilteredReason: null, + image: { + gcsUri: null, + mimeType: 'image/png', + encodedImage: 'assets/images/placeholder_image.png', + }, + }, + ]; + this.showDefaultDocuments = true; + this.userService.hideLoading(); + return; + } + + this.searchRequest.term = query || ''; + const newSearchRequest = this.searchRequest; + + this.service.search(newSearchRequest).subscribe({ + next: (searchResponse: GeneratedImage[]) => { + this.summary = searchResponse?.[0]?.enhancedPrompt || ''; + this.documents = searchResponse; + this.serachResult.forEach((element: GeneratedImage) => { + this.images.push(element.image?.encodedImage); + }); + this.selectedResult = searchResponse[0]; + + this.userService.hideLoading(); + }, + error: error => { + this.documents = [ + { + enhancedPrompt: 'default enhaced prompt', + raiFilteredReason: null, + image: { + gcsUri: null, + mimeType: 'image/png', + encodedImage: 'assets/images/placeholder_image.png', + }, + }, + { + enhancedPrompt: 'default enhaced prompt', + raiFilteredReason: null, + image: { + gcsUri: null, + mimeType: 'image/png', + encodedImage: 'assets/images/placeholder_image.png', + }, + }, + ]; + this.showDefaultDocuments = true; + this.userService.hideLoading(); + this.showErrorSnackBar(error); + }, + }); + } + + showErrorSnackBar(error: any): void { + console.error('Search error:', error); + + let errorMessage = ''; + let triedToGeneratePersons = false; + if (error?.error?.detail?.[0]?.msg) + errorMessage = `${error?.error?.detail?.[0]?.msg} - ${error?.error?.detail?.[0]?.loc}`; + else if (error?.error?.detail) + if ( + error.error.detail.includes( + "The image you want to edit contains content that has been blocked because you selected the 'Don't allow' option for Person Generation." + ) + ) { + triedToGeneratePersons = true; + errorMessage = + 'The image you want to edit contains content that has been blocked because there are persons in it. See the safety settings documentation for more details.'; + } else errorMessage = error?.error?.detail; + else if (error?.message) errorMessage = error?.message; + else if (error?.error) errorMessage = error?.error; + else errorMessage = 'Error sending request. Please try again later!'; + + this._snackBar.openFromComponent(ToastMessageComponent, { + panelClass: ['red-toast'], + verticalPosition: 'top', + horizontalPosition: 'right', + duration: 10000, + data: { + text: errorMessage, + icon: 'cross-in-circle-white', + }, + }); + if (triedToGeneratePersons) this.goToHomePage(); + } + + searchTerm({ + term, + model, + numberOfResults, + maskDistilation, + }: { + term?: string | undefined; + model?: string | undefined; + numberOfResults?: number | undefined; + maskDistilation?: number | undefined; + }) { + if (!term) return; + + this.showDefaultDocuments = false; + this.userService.showLoading(); + this.serachResult = []; + this.summary = ''; + this.documents = []; + this.images = []; + + if (term) this.searchRequest.term = term; + if (model) this.searchRequest.model = model; + if (numberOfResults) this.searchRequest.numberOfResults = numberOfResults; + if (maskDistilation) this.searchRequest.maskDistilation = maskDistilation; + + const newSearchRequest = this.searchRequest; + + this.service.search(newSearchRequest).subscribe({ + next: (searchResponse: any) => { + this.summary = searchResponse?.[0]?.enhancedPrompt || ''; + this.documents = searchResponse; + this.serachResult.forEach((element: GeneratedImage) => { + this.images.push(element.image?.encodedImage); + }); + this.selectedResult = searchResponse[0]; + this.userService.hideLoading(); + }, + error: error => { + console.error('Search error:', error); + this.userService.hideLoading(); + this.showErrorSnackBar(error); + }, + }); + } + + openNewWindow(link: string) { + window.open(link, '_blank'); + } + + changeImageSelection(result: GeneratedImage) { + this.selectedResult = result; + } + + changeImagen3Model(model: Imagen3Model) { + this.selectedModel = model.value; + this.searchTerm({model: this.selectedModel}); + } + + ngOnInit(): void {} + + ngOnDestroy() { + this.imageSubscription.unsubscribe(); + this.destroyed.next(); + this.destroyed.complete(); + } + + get pagedDocuments() { + const startIndex = this.currentPage * this.pageSize; + return this.documents.slice(startIndex, startIndex + this.pageSize); + } + + onNumberOfResultsChange(event: Event) { + this.selectedNumberOfResults = + Number((event.target as HTMLInputElement).value) || 0; + } + + selectStyle(event: MatButtonToggleChange) { + this.selectedStyle = event.value; + } + + onSliderChange(event: Event) { + this.selectedMaskDistilation = + Number((event.target as HTMLInputElement).value) || 0; + } + + submitChanges() { + this.searchTerm({ + term: this.searchRequest.term, + model: this.selectedModel, + numberOfResults: this.selectedNumberOfResults, + maskDistilation: this.selectedMaskDistilation, + }); + } + + goToHomePage() { + this._ImageService.clearSelectedImage(); + this.router.navigate(['/']); + } + + get firstHalfPagedDocuments() { + const halfLength = Math.ceil(this.pagedDocuments.length / 2); + return this.pagedDocuments.slice(0, halfLength); + } + + get secondHalfPagedDocuments() { + const halfLength = Math.ceil(this.pagedDocuments.length / 2); + return this.pagedDocuments.slice(halfLength); + } +} diff --git a/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/app/components/toast-message/toast-message.component.html b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/app/components/toast-message/toast-message.component.html new file mode 100644 index 0000000..a83d217 --- /dev/null +++ b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/app/components/toast-message/toast-message.component.html @@ -0,0 +1,26 @@ + + +
    + icon inside toast message + {{ text }} + cross icon inside toast message +
    diff --git a/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/app/components/toast-message/toast-message.component.scss b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/app/components/toast-message/toast-message.component.scss new file mode 100644 index 0000000..0d8177f --- /dev/null +++ b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/app/components/toast-message/toast-message.component.scss @@ -0,0 +1,37 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +.toast-content { + color: #ffffff; + font-family: 'Google Sans', sans-serif !important; +} + +.close-toast { + margin-left: auto; + align-self: baseline; + cursor: pointer; +} + +.red-toast { + background-color: #ed0c0c !important; +} +.green-toast { + background-color: #06865e !important; +} + +.mat-mdc-snack-bar-container .mdc-snackbar__surface { + background-color: transparent !important; +} diff --git a/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/app/components/toast-message/toast-message.component.spec.ts b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/app/components/toast-message/toast-message.component.spec.ts new file mode 100644 index 0000000..d0d405a --- /dev/null +++ b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/app/components/toast-message/toast-message.component.spec.ts @@ -0,0 +1,38 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {ComponentFixture, TestBed} from '@angular/core/testing'; + +import {ToastMessageComponent} from './toast-message.component'; + +describe('ToastMessageComponent', () => { + let component: ToastMessageComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ToastMessageComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(ToastMessageComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/app/components/toast-message/toast-message.component.ts b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/app/components/toast-message/toast-message.component.ts new file mode 100644 index 0000000..ebac36d --- /dev/null +++ b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/app/components/toast-message/toast-message.component.ts @@ -0,0 +1,40 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {Component, Inject, ViewEncapsulation} from '@angular/core'; +import {MatSnackBar, MAT_SNACK_BAR_DATA} from '@angular/material/snack-bar'; + +@Component({ + selector: 'app-toast-message', + templateUrl: './toast-message.component.html', + styleUrls: ['./toast-message.component.scss'], + encapsulation: ViewEncapsulation.None, +}) +export class ToastMessageComponent { + text: string; + icon: string; + constructor( + private _snackBar: MatSnackBar, + @Inject(MAT_SNACK_BAR_DATA) public snackBarData: any + ) { + this.text = snackBarData.text; + this.icon = snackBarData.icon; + } + + closeToast() { + this._snackBar.dismiss(); + } +} diff --git a/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/app/models/config.model.ts b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/app/models/config.model.ts new file mode 100644 index 0000000..fda930d --- /dev/null +++ b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/app/models/config.model.ts @@ -0,0 +1,20 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export type config = { + name: string; + url: string; +}; diff --git a/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/app/models/generated-image.model.ts b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/app/models/generated-image.model.ts new file mode 100644 index 0000000..f136940 --- /dev/null +++ b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/app/models/generated-image.model.ts @@ -0,0 +1,28 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +type Image = { + gcsUri?: string; + imageBytes?: ArrayBuffer; + encodedImage?: string; + mimeType?: string; +}; + +export type GeneratedImage = { + image?: Image; + raiFilteredReason?: string; + enhancedPrompt?: string; +}; diff --git a/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/app/models/search.model.ts b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/app/models/search.model.ts new file mode 100644 index 0000000..d2b0125 --- /dev/null +++ b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/app/models/search.model.ts @@ -0,0 +1,55 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export type SearchRequest = { + term: string; + model: string; + numberOfResults: number; + maskDistilation: number; +}; + +export type SearchResponse = { + summary: any; + results: SearchResult[]; + totalSize: number; +}; + +export type SearchResult = { + document: Document; +}; + +export type Document = { + derivedStructData: DocumentData; +}; + +export type DocumentData = { + title: string; + link: string; + snippets: Snippet[]; + pagemap: PageMap; +}; + +export type Snippet = { + snippet: string; +}; + +export type PageMap = { + cse_image: ImagesData[]; +}; + +export type ImagesData = { + src: string; +}; diff --git a/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/app/pipes/truncate.pipe.spec.ts b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/app/pipes/truncate.pipe.spec.ts new file mode 100644 index 0000000..82e65c6 --- /dev/null +++ b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/app/pipes/truncate.pipe.spec.ts @@ -0,0 +1,24 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {TruncatePipe} from './truncate.pipe'; + +describe('TruncatePipe', () => { + it('create an instance', () => { + const pipe = new TruncatePipe(); + expect(pipe).toBeTruthy(); + }); +}); diff --git a/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/app/pipes/truncate.pipe.ts b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/app/pipes/truncate.pipe.ts new file mode 100644 index 0000000..ae06a3b --- /dev/null +++ b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/app/pipes/truncate.pipe.ts @@ -0,0 +1,26 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {Pipe, PipeTransform} from '@angular/core'; + +@Pipe({ + name: 'truncate', +}) +export class TruncatePipe implements PipeTransform { + transform(value: string, limit = 300, trail = '...'): string { + return value.length > limit ? value.substring(0, limit) + trail : value; + } +} diff --git a/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/app/services/image/image.service.ts b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/app/services/image/image.service.ts new file mode 100644 index 0000000..5579aaf --- /dev/null +++ b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/app/services/image/image.service.ts @@ -0,0 +1,55 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {Injectable} from '@angular/core'; +import {BehaviorSubject, Observable} from 'rxjs'; + +interface StoredImage { + file: File | null; + imageUrl: string | null; + isUserPhotoUrl: boolean; +} + +@Injectable({ + providedIn: 'root', +}) +export class ImageService { + private _selectedImage: BehaviorSubject = + new BehaviorSubject(null); + public selectedImage$: Observable = + this._selectedImage.asObservable(); + + constructor() { + // Initialize with null values + this.setSelectedImage(null, null, false); + } + + setSelectedImage( + file: File | null, + imageUrl: string | null, + isUserPhotoUrl: boolean + ) { + this._selectedImage.next({file, imageUrl, isUserPhotoUrl}); + } + + getSelectedImage(): StoredImage | null { + return this._selectedImage.getValue(); + } + + clearSelectedImage() { + this._selectedImage.next(null); + } +} diff --git a/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/app/services/login/auth.guard.spec.ts b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/app/services/login/auth.guard.spec.ts new file mode 100644 index 0000000..8284e7a --- /dev/null +++ b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/app/services/login/auth.guard.spec.ts @@ -0,0 +1,32 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {TestBed} from '@angular/core/testing'; + +import {AuthGuard} from './auth.guard'; + +describe('AuthGuard', () => { + let guard: AuthGuard; + + beforeEach(() => { + TestBed.configureTestingModule({}); + guard = TestBed.inject(AuthGuard); + }); + + it('should be created', () => { + expect(guard).toBeTruthy(); + }); +}); diff --git a/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/app/services/login/auth.guard.ts b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/app/services/login/auth.guard.ts new file mode 100644 index 0000000..fd95abd --- /dev/null +++ b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/app/services/login/auth.guard.ts @@ -0,0 +1,54 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {Injectable} from '@angular/core'; +import { + ActivatedRouteSnapshot, + CanActivate, + Router, + RouterStateSnapshot, + UrlTree, +} from '@angular/router'; +import {Observable} from 'rxjs'; +import {AuthService} from './auth.service'; +import {environment} from 'src/environments/environment'; + +const LOGIN_ROUTE = '/login'; + +@Injectable({ + providedIn: 'root', +}) +export class AuthGuard implements CanActivate { + constructor( + private auth: AuthService, + private router: Router + ) {} + + canActivate( + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot + ): + | Observable + | Promise + | boolean + | UrlTree { + if (environment.requiredLogin === 'True' && !this.auth.isLoggedIn()) { + this.router.navigate([LOGIN_ROUTE]); + return false; + } + return true; + } +} diff --git a/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/app/services/login/auth.service.spec.ts b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/app/services/login/auth.service.spec.ts new file mode 100644 index 0000000..f08aa1a --- /dev/null +++ b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/app/services/login/auth.service.spec.ts @@ -0,0 +1,32 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {TestBed} from '@angular/core/testing'; + +import {AuthService} from './auth.service'; + +describe('AuthService', () => { + let service: AuthService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(AuthService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/app/services/login/auth.service.ts b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/app/services/login/auth.service.ts new file mode 100644 index 0000000..325d42e --- /dev/null +++ b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/app/services/login/auth.service.ts @@ -0,0 +1,57 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {Injectable} from '@angular/core'; +import {Router} from '@angular/router'; +import {environment} from 'src/environments/environment'; + +const USER_TOKEN_KEY = 'gpau_id'; +const USER_DETAILS = 'USER_DETAILS'; +const LOGIN_ROUTE = '/login'; + +@Injectable({ + providedIn: 'root', +}) +export class AuthService { + private requiredLogin: boolean = environment.requiredLogin === 'True'; + + constructor(private router: Router) {} + + saveUserSession(token: string) { + localStorage.setItem(USER_TOKEN_KEY, token); + } + + logout(route: string = LOGIN_ROUTE) { + localStorage.removeItem(USER_TOKEN_KEY); + localStorage.removeItem(USER_DETAILS); + localStorage.removeItem('showTooltip'); + this.router.navigateByUrl(route); + } + + isLoggedIn() { + const isLoggedIn = localStorage.getItem(USER_TOKEN_KEY) !== null; + if (!isLoggedIn && this.router.url !== LOGIN_ROUTE) { + this.router.navigate([LOGIN_ROUTE]); + } + return isLoggedIn; + } + + isUserLoggedIn() { + if (!this.requiredLogin) return true; + const isLoggedIn = localStorage.getItem(USER_TOKEN_KEY) !== null; + return isLoggedIn; + } +} diff --git a/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/app/services/search.service.spec.ts b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/app/services/search.service.spec.ts new file mode 100644 index 0000000..b024e3b --- /dev/null +++ b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/app/services/search.service.spec.ts @@ -0,0 +1,32 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {TestBed} from '@angular/core/testing'; + +import {SearchService} from './search.service'; + +describe('SearchService', () => { + let service: SearchService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(SearchService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/app/services/search.service.ts b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/app/services/search.service.ts new file mode 100644 index 0000000..d54b0ee --- /dev/null +++ b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/app/services/search.service.ts @@ -0,0 +1,65 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {HttpClient} from '@angular/common/http'; +import {Injectable} from '@angular/core'; +import {environment} from 'src/environments/environment'; +import {map} from 'rxjs/operators'; +import {SearchRequest} from '../models/search.model'; +import {GeneratedImage} from '../models/generated-image.model'; +import {ImageService} from './image/image.service'; +import {Router} from '@angular/router'; +import {Observable, throwError} from 'rxjs'; + +const searchURL = `${environment.backendURL}/search`; + +@Injectable({ + providedIn: 'root', +}) +export class SearchService { + constructor( + private http: HttpClient, + private imageService: ImageService, + private router: Router + ) {} + + search(searchRequest: SearchRequest): Observable { + const userImage = this.imageService.getSelectedImage(); + + if (!userImage?.file) { + // Redirect to homepage and alert that you need to upload an image first + this.router.navigate(['/']); + alert('Please upload an image first.'); + return throwError( + () => new Error('No image and no search term provided.') + ); + } + + const formData = new FormData(); + formData.append('term', searchRequest.term); + formData.append('generationModel', searchRequest.model); + formData.append('numberOfImages', searchRequest.numberOfResults.toString()); + formData.append( + 'maskDistilation', + searchRequest.maskDistilation.toString() + ); + formData.append('userImage', userImage.file, userImage.file.name); + + return this.http + .post(searchURL, formData) + .pipe(map(response => response as GeneratedImage[])); + } +} diff --git a/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/app/services/speech-to-text.ts b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/app/services/speech-to-text.ts new file mode 100644 index 0000000..16b1469 --- /dev/null +++ b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/app/services/speech-to-text.ts @@ -0,0 +1,34 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {Injectable} from '@angular/core'; +import {HttpClient} from '@angular/common/http'; +import {environment} from 'src/environments/environment'; + +const audioChatUrl = `${environment.backendURL}/audio_chat`; + +@Injectable({ + providedIn: 'root', +}) +export class SpeechToTextService { + constructor(private http: HttpClient) {} + + async transcribeAudio(audioBlob: Blob) { + const formData = new FormData(); + formData.append('audio_file', audioBlob, 'audio.wav'); + return this.http.post(audioChatUrl, formData); + } +} diff --git a/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/app/services/user/session.service.spec.ts b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/app/services/user/session.service.spec.ts new file mode 100644 index 0000000..ff45965 --- /dev/null +++ b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/app/services/user/session.service.spec.ts @@ -0,0 +1,32 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {TestBed} from '@angular/core/testing'; + +import {SessionService} from './session.service'; + +describe('SessionService', () => { + let service: SessionService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(SessionService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/app/services/user/session.service.ts b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/app/services/user/session.service.ts new file mode 100644 index 0000000..4a51a17 --- /dev/null +++ b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/app/services/user/session.service.ts @@ -0,0 +1,35 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {Injectable} from '@angular/core'; +import {v4 as uuid} from 'uuid'; + +const SESSION_KEY = 'pasid'; + +@Injectable({ + providedIn: 'root', +}) +export class SessionService { + constructor() {} + + getSession(): string | null { + return sessionStorage.getItem(SESSION_KEY); + } + + createSession() { + sessionStorage.setItem(SESSION_KEY, uuid()); + } +} diff --git a/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/app/services/user/user.service.ts b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/app/services/user/user.service.ts new file mode 100644 index 0000000..a405c99 --- /dev/null +++ b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/app/services/user/user.service.ts @@ -0,0 +1,59 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {Injectable} from '@angular/core'; +import {BehaviorSubject} from 'rxjs'; +import {environment} from 'src/environments/environment'; + +type UserStored = { + uid?: string; + name?: string; + email?: string; + photoURL?: string; + displayName?: string; + domain?: string; +}; + +@Injectable({ + providedIn: 'root', +}) +export class UserService { + readonly loadingSubject = new BehaviorSubject(false); + requiredLogin: string = environment.requiredLogin; + + constructor() {} + + showLoading() { + this.loadingSubject.next(true); + } + + hideLoading() { + this.loadingSubject.next(false); + } + + getUserDetails(): UserStored { + if ( + this.requiredLogin === 'True' && + localStorage.getItem('USER_DETAILS') !== null + ) { + const userObj = localStorage.getItem('USER_DETAILS'); + return JSON.parse(userObj || '{}'); + } else { + const userDetails: UserStored = {}; + return userDetails; + } + } +} diff --git a/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/assets/.gitkeep b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/assets/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/assets/gcloud-logo.svg b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/assets/gcloud-logo.svg new file mode 100644 index 0000000..5150056 --- /dev/null +++ b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/assets/gcloud-logo.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/assets/images/SocialHeaderTemplate8.png b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/assets/images/SocialHeaderTemplate8.png new file mode 100644 index 0000000..88f4306 Binary files /dev/null and b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/assets/images/SocialHeaderTemplate8.png differ diff --git a/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/assets/images/answernotfound.png b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/assets/images/answernotfound.png new file mode 100644 index 0000000..874c87d Binary files /dev/null and b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/assets/images/answernotfound.png differ diff --git a/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/assets/images/attached.svg b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/assets/images/attached.svg new file mode 100644 index 0000000..ba09784 --- /dev/null +++ b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/assets/images/attached.svg @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/assets/images/available.svg b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/assets/images/available.svg new file mode 100644 index 0000000..5a460d0 --- /dev/null +++ b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/assets/images/available.svg @@ -0,0 +1,4 @@ + + + + diff --git a/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/assets/images/badge-banner.svg b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/assets/images/badge-banner.svg new file mode 100644 index 0000000..a38efec --- /dev/null +++ b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/assets/images/badge-banner.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/assets/images/badge.svg b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/assets/images/badge.svg new file mode 100644 index 0000000..3fc6b94 --- /dev/null +++ b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/assets/images/badge.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/assets/images/books.png b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/assets/images/books.png new file mode 100644 index 0000000..94feabc Binary files /dev/null and b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/assets/images/books.png differ diff --git a/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/assets/images/bot_logo.svg b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/assets/images/bot_logo.svg new file mode 100644 index 0000000..4be6c2f --- /dev/null +++ b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/assets/images/bot_logo.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/assets/images/cancel-toast.svg b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/assets/images/cancel-toast.svg new file mode 100644 index 0000000..9db4780 --- /dev/null +++ b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/assets/images/cancel-toast.svg @@ -0,0 +1,4 @@ + + + + diff --git a/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/assets/images/chatIcon.svg b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/assets/images/chatIcon.svg new file mode 100644 index 0000000..3303847 --- /dev/null +++ b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/assets/images/chatIcon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/assets/images/chat_background.png b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/assets/images/chat_background.png new file mode 100644 index 0000000..701ec81 Binary files /dev/null and b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/assets/images/chat_background.png differ diff --git a/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/assets/images/colibri_image_fx.jpg b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/assets/images/colibri_image_fx.jpg new file mode 100644 index 0000000..94eff87 Binary files /dev/null and b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/assets/images/colibri_image_fx.jpg differ diff --git a/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/assets/images/crane_image_fx.jpg b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/assets/images/crane_image_fx.jpg new file mode 100644 index 0000000..b6d1c73 Binary files /dev/null and b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/assets/images/crane_image_fx.jpg differ diff --git a/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/assets/images/cross-in-circle-white copy.svg b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/assets/images/cross-in-circle-white copy.svg new file mode 100644 index 0000000..96a0fe3 --- /dev/null +++ b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/assets/images/cross-in-circle-white copy.svg @@ -0,0 +1,3 @@ + + + diff --git a/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/assets/images/cross-in-circle-white.svg b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/assets/images/cross-in-circle-white.svg new file mode 100644 index 0000000..96a0fe3 --- /dev/null +++ b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/assets/images/cross-in-circle-white.svg @@ -0,0 +1,3 @@ + + + diff --git a/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/assets/images/cross-in-circle.svg b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/assets/images/cross-in-circle.svg new file mode 100644 index 0000000..c5c50ea --- /dev/null +++ b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/assets/images/cross-in-circle.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/assets/images/delete_notification_dialog_image.png b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/assets/images/delete_notification_dialog_image.png new file mode 100644 index 0000000..5afbf5c Binary files /dev/null and b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/assets/images/delete_notification_dialog_image.png differ diff --git a/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/assets/images/doc.png b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/assets/images/doc.png new file mode 100644 index 0000000..0b21898 Binary files /dev/null and b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/assets/images/doc.png differ diff --git a/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/assets/images/error_img.svg b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/assets/images/error_img.svg new file mode 100644 index 0000000..ac20193 --- /dev/null +++ b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/assets/images/error_img.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/assets/images/feedback-container.svg b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/assets/images/feedback-container.svg new file mode 100644 index 0000000..7b0f481 --- /dev/null +++ b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/assets/images/feedback-container.svg @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/assets/images/footer.png b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/assets/images/footer.png new file mode 100644 index 0000000..eda0b3b Binary files /dev/null and b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/assets/images/footer.png differ diff --git a/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/assets/images/gcloudlight1.png b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/assets/images/gcloudlight1.png new file mode 100644 index 0000000..5ddb77c Binary files /dev/null and b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/assets/images/gcloudlight1.png differ diff --git a/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/assets/images/gnosis-logo.svg b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/assets/images/gnosis-logo.svg new file mode 100644 index 0000000..17d0b7f --- /dev/null +++ b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/assets/images/gnosis-logo.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/assets/images/gsd-icon.png b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/assets/images/gsd-icon.png new file mode 100644 index 0000000..abeacf9 Binary files /dev/null and b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/assets/images/gsd-icon.png differ diff --git a/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/assets/images/home_bottom.png b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/assets/images/home_bottom.png new file mode 100644 index 0000000..78d1c92 Binary files /dev/null and b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/assets/images/home_bottom.png differ diff --git a/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/assets/images/hot_air_baloon_image_fx.jpg b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/assets/images/hot_air_baloon_image_fx.jpg new file mode 100644 index 0000000..19064b4 Binary files /dev/null and b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/assets/images/hot_air_baloon_image_fx.jpg differ diff --git a/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/assets/images/img.png b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/assets/images/img.png new file mode 100644 index 0000000..53bc884 Binary files /dev/null and b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/assets/images/img.png differ diff --git a/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/assets/images/left_loader_images/Left/1.png b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/assets/images/left_loader_images/Left/1.png new file mode 100644 index 0000000..d930ffb Binary files /dev/null and b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/assets/images/left_loader_images/Left/1.png differ diff --git a/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/assets/images/left_loader_images/Left/2.png b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/assets/images/left_loader_images/Left/2.png new file mode 100644 index 0000000..ac32f6a Binary files /dev/null and b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/assets/images/left_loader_images/Left/2.png differ diff --git a/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/assets/images/left_loader_images/Left/3.png b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/assets/images/left_loader_images/Left/3.png new file mode 100644 index 0000000..cee4db0 Binary files /dev/null and b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/assets/images/left_loader_images/Left/3.png differ diff --git a/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/assets/images/left_loader_images/Left/4.png b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/assets/images/left_loader_images/Left/4.png new file mode 100644 index 0000000..77a6d94 Binary files /dev/null and b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/assets/images/left_loader_images/Left/4.png differ diff --git a/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/assets/images/left_loader_images/Left/5.png b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/assets/images/left_loader_images/Left/5.png new file mode 100644 index 0000000..deba49b Binary files /dev/null and b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/assets/images/left_loader_images/Left/5.png differ diff --git a/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/assets/images/left_loader_images/Left/6.png b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/assets/images/left_loader_images/Left/6.png new file mode 100644 index 0000000..61e1988 Binary files /dev/null and b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/assets/images/left_loader_images/Left/6.png differ diff --git a/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/assets/images/login-globe.png b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/assets/images/login-globe.png new file mode 100644 index 0000000..f64021b Binary files /dev/null and b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/assets/images/login-globe.png differ diff --git a/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/assets/images/login_left.svg b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/assets/images/login_left.svg new file mode 100644 index 0000000..397f17f --- /dev/null +++ b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/assets/images/login_left.svg @@ -0,0 +1,101 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/assets/images/new-pdc-logo.svg b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/assets/images/new-pdc-logo.svg new file mode 100644 index 0000000..c267930 --- /dev/null +++ b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/assets/images/new-pdc-logo.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/assets/images/noPic.svg b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/assets/images/noPic.svg new file mode 100644 index 0000000..0f9c09d --- /dev/null +++ b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/assets/images/noPic.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/assets/images/pdf.png b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/assets/images/pdf.png new file mode 100644 index 0000000..a8c471f Binary files /dev/null and b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/assets/images/pdf.png differ diff --git a/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/assets/images/phoneIcon.png b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/assets/images/phoneIcon.png new file mode 100644 index 0000000..a0af511 Binary files /dev/null and b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/assets/images/phoneIcon.png differ diff --git a/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/assets/images/placeholder_image.png b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/assets/images/placeholder_image.png new file mode 100644 index 0000000..f2ead09 Binary files /dev/null and b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/assets/images/placeholder_image.png differ diff --git a/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/assets/images/right_loader_images/Right/1.png b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/assets/images/right_loader_images/Right/1.png new file mode 100644 index 0000000..c571db1 Binary files /dev/null and b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/assets/images/right_loader_images/Right/1.png differ diff --git a/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/assets/images/right_loader_images/Right/2.png b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/assets/images/right_loader_images/Right/2.png new file mode 100644 index 0000000..d6a1da0 Binary files /dev/null and b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/assets/images/right_loader_images/Right/2.png differ diff --git a/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/assets/images/right_loader_images/Right/3.png b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/assets/images/right_loader_images/Right/3.png new file mode 100644 index 0000000..042ba23 Binary files /dev/null and b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/assets/images/right_loader_images/Right/3.png differ diff --git a/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/assets/images/right_loader_images/Right/4.png b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/assets/images/right_loader_images/Right/4.png new file mode 100644 index 0000000..96c067a Binary files /dev/null and b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/assets/images/right_loader_images/Right/4.png differ diff --git a/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/assets/images/right_loader_images/Right/5.png b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/assets/images/right_loader_images/Right/5.png new file mode 100644 index 0000000..3a15579 Binary files /dev/null and b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/assets/images/right_loader_images/Right/5.png differ diff --git a/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/assets/images/right_loader_images/Right/6.png b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/assets/images/right_loader_images/Right/6.png new file mode 100644 index 0000000..7de6698 Binary files /dev/null and b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/assets/images/right_loader_images/Right/6.png differ diff --git a/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/assets/images/searchIcon.png b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/assets/images/searchIcon.png new file mode 100644 index 0000000..7b95f3c Binary files /dev/null and b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/assets/images/searchIcon.png differ diff --git a/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/assets/images/searchIcon.svg b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/assets/images/searchIcon.svg new file mode 100644 index 0000000..b82fe2f --- /dev/null +++ b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/assets/images/searchIcon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/assets/images/support-page-banner.svg b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/assets/images/support-page-banner.svg new file mode 100644 index 0000000..0fa6347 --- /dev/null +++ b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/assets/images/support-page-banner.svg @@ -0,0 +1,134 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/assets/images/tick-with-circle.svg b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/assets/images/tick-with-circle.svg new file mode 100644 index 0000000..8335e7c --- /dev/null +++ b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/assets/images/tick-with-circle.svg @@ -0,0 +1,4 @@ + + + + diff --git a/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/assets/images/unnamed.jpeg b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/assets/images/unnamed.jpeg new file mode 100644 index 0000000..b33f75d Binary files /dev/null and b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/assets/images/unnamed.jpeg differ diff --git a/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/assets/images/user-logo.svg b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/assets/images/user-logo.svg new file mode 100644 index 0000000..8135e28 --- /dev/null +++ b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/assets/images/user-logo.svg @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/assets/images/voiceIcon.svg b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/assets/images/voiceIcon.svg new file mode 100644 index 0000000..9c3e1d3 --- /dev/null +++ b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/assets/images/voiceIcon.svg @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/assets/images/warning-yellow.png b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/assets/images/warning-yellow.png new file mode 100644 index 0000000..e94f265 Binary files /dev/null and b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/assets/images/warning-yellow.png differ diff --git a/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/assets/videos/Imagen3_Header.mp4 b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/assets/videos/Imagen3_Header.mp4 new file mode 100644 index 0000000..f919cc8 Binary files /dev/null and b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/assets/videos/Imagen3_Header.mp4 differ diff --git a/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/environments/constant.ts b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/environments/constant.ts new file mode 100644 index 0000000..8edf1a8 --- /dev/null +++ b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/environments/constant.ts @@ -0,0 +1,25 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export const search_document_type = ['pdf', 'docx', 'doc']; + +export const search_image_type = ['jpg', 'jpeg', 'png']; + +export const image_name = new Map() + .set('doc', 'assets/images/doc.png') + .set('pdf', 'assets/images/pdf.png'); + +export const PDF = 'pdf'; diff --git a/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/environments/environment.ts b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/environments/environment.ts new file mode 100644 index 0000000..3573271 --- /dev/null +++ b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/environments/environment.ts @@ -0,0 +1,31 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export const environment = { + firebase: { + apiKey: '', + authDomain: '', + projectId: '', + storageBucket: '', + messagingSenderId: '', + appId: '', + measurementId: '', + }, + requiredLogin: 'True', + backendURL: 'http://localhost:8080/api', + chatbotName: 'LinkedIn Profile Image Generation Agent', + environment: 'development', +}; diff --git a/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/favicon.ico b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/favicon.ico new file mode 100644 index 0000000..997406a Binary files /dev/null and b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/favicon.ico differ diff --git a/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/index.html b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/index.html new file mode 100644 index 0000000..799de59 --- /dev/null +++ b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/index.html @@ -0,0 +1,48 @@ + + + + + + + Quick Bot + + + + + + + + + + + + + + + diff --git a/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/main.ts b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/main.ts new file mode 100644 index 0000000..0da5f95 --- /dev/null +++ b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/main.ts @@ -0,0 +1,23 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {platformBrowserDynamic} from '@angular/platform-browser-dynamic'; + +import {AppModule} from './app/app.module'; + +platformBrowserDynamic() + .bootstrapModule(AppModule) + .catch(err => console.error(err)); diff --git a/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/styles.scss b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/styles.scss new file mode 100644 index 0000000..881030a --- /dev/null +++ b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/src/styles.scss @@ -0,0 +1,85 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* You can add global styles to this file, and also import other style files */ + +html, +body { + height: 100%; + margin: 0; + font-family: 'Google Sans', sans-serif !important; +} + +.material-symbols-rounded { + font-variation-settings: + 'FILL', + 'wght' 600, + 'GRAD' 0, + 'opsz' 48; +} + +.tooltip-list .mdc-tooltip__surface { + white-space: pre; + max-width: unset !important; +} + +.mat-mdc-slide-toggle.mat-accent { + --mdc-switch-selected-focus-state-layer-color: #4285f4; + --mdc-switch-selected-handle-color: #4285f4; + --mdc-switch-selected-hover-state-layer-color: #4285f4; + --mdc-switch-selected-pressed-state-layer-color: #4285f4; + --mdc-switch-selected-focus-handle-color: #3f74e0; + --mdc-switch-selected-hover-handle-color: #3f74e0; + --mdc-switch-selected-pressed-handle-color: #3f74e0; + --mdc-switch-selected-focus-track-color: rgba(66, 133, 244, 0.3); + --mdc-switch-selected-hover-track-color: rgba(66, 133, 244, 0.3); + --mdc-switch-selected-pressed-track-color: rgba(66, 133, 244, 0.3); + --mdc-switch-selected-track-color: rgba(66, 133, 244, 0.3); +} + +.arrow-top { + box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.1) !important; + border-radius: 20px !important; +} + +.mat-mdc-checkbox.mat-accent { + --mdc-checkbox-selected-checkmark-color: #fff; + --mdc-checkbox-selected-focus-icon-color: #4285f4; + --mdc-checkbox-selected-hover-icon-color: #4285f4; + --mdc-checkbox-selected-icon-color: #4285f4; + --mdc-checkbox-selected-pressed-icon-color: #4285f4; + --mdc-checkbox-unselected-focus-icon-color: #212121; + --mdc-checkbox-unselected-hover-icon-color: #212121; + --mdc-checkbox-disabled-selected-icon-color: rgba(0, 0, 0, 0.38); + --mdc-checkbox-disabled-unselected-icon-color: rgba(0, 0, 0, 0.38); + --mdc-checkbox-unselected-icon-color: rgba(0, 0, 0, 0.54); + --mdc-checkbox-unselected-pressed-icon-color: rgba(0, 0, 0, 0.54); +} + +$tour-skip-link-color: red; +$tour-text-color: purple; +$tour-next-text-color: yellow; +$tour-zindex: 100; +$tour-orb-color: cyan; +$tour-next-button-color: green; +$tour-next-button-hover: black; +$tour-back-button-color: white; +$tour-shadow-color: gray; +$tour-step-color: aliceblue; + +@tailwind base; +@tailwind components; +@tailwind utilities; diff --git a/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/tailwind.config.js b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/tailwind.config.js new file mode 100644 index 0000000..8f72439 --- /dev/null +++ b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/tailwind.config.js @@ -0,0 +1,30 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** @type {import('tailwindcss').Config} */ +module.exports = { + content: ['./src/**/*.{html,ts}'], + theme: { + extend: { + colors: { + 'custom-gray': '#DDE6F6', + 'light-gray': '#e0e0e0', + 'lighter-gray': '#f0f0f0', + }, + }, + }, + plugins: [], +}; diff --git a/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/tsconfig.app.json b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/tsconfig.app.json new file mode 100644 index 0000000..374cc9d --- /dev/null +++ b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/tsconfig.app.json @@ -0,0 +1,14 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "./out-tsc/app", + "types": [] + }, + "files": [ + "src/main.ts" + ], + "include": [ + "src/**/*.d.ts" + ] +} diff --git a/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/tsconfig.json b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/tsconfig.json new file mode 100644 index 0000000..fc49b39 --- /dev/null +++ b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/tsconfig.json @@ -0,0 +1,34 @@ +{ + "extends": "./node_modules/gts/tsconfig-google.json", + "compileOnSave": false, + "compilerOptions": { + "ignoreDeprecations": "5.0", + "baseUrl": "./", + "outDir": "./dist/out-tsc", + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "downlevelIteration": true, + "experimentalDecorators": true, + "moduleResolution": "node", + "importHelpers": true, + "target": "ES2022", + "module": "ES2022", + "useDefineForClassFields": false, + "lib": [ + "ES2022", + "dom" + ], + }, + "angularCompilerOptions": { + "enableI18nLegacyMessageIdFormat": false, + "strictInjectionParameters": true, + "strictInputAccessModifiers": true, + "strictTemplates": true, + "suppressImplicitAnyIndexErrors": true, + "strictPropertyInitialization": false, + }, + "include": [ + "src/**/*.ts", + "test/**/*.ts" + ] +} diff --git a/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/tsconfig.spec.json b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/tsconfig.spec.json new file mode 100644 index 0000000..be7e9da --- /dev/null +++ b/gemini/sample-apps/quickbot/linkedin-profile-image-generation-using-imagen3/frontend/tsconfig.spec.json @@ -0,0 +1,14 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "./out-tsc/spec", + "types": [ + "jasmine" + ] + }, + "include": [ + "src/**/*.spec.ts", + "src/**/*.d.ts" + ] +} diff --git a/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/.dockerignore b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/.dockerignore new file mode 100644 index 0000000..d472d1d --- /dev/null +++ b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/.dockerignore @@ -0,0 +1,12 @@ +node_modules +npm-debug.log +tests +coverage +Dockerfile +.dockerignore +.env +*.md +.git +.vscode +.gitignore +Makefile \ No newline at end of file diff --git a/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/README.md b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/README.md new file mode 100644 index 0000000..4a8fc50 --- /dev/null +++ b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/README.md @@ -0,0 +1,283 @@ +# Quickbot - Multi Agent Travel Concierge (ADK + Agent Engine) + +Quickbot Multi Agent Travel Concierge is a sophisticated application designed to deliver highly personalized travel experiences. Leveraging an [Agent Development Kit (ADK)](https://google.github.io/adk-docs/) and powerful [Agent Engine](https://cloud.google.com/vertex-ai/generative-ai/docs/agent-engine/overview) capabilities, this system orchestrates multiple specialized intelligent agents to provide comprehensive support throughout the user’s journey – from initial planning and booking to real-time itinerary alerts and in-trip assistance. It features a user-friendly frontend and a robust backend API to manage agent interactions and deliver a seamless travel planning and support experience. +This Template is taken from the official [Agent Garden samples](https://github.com/google/adk-samples/tree/main), the [Travel Concierge MultiAgent](https://github.com/google/adk-samples/tree/main/agents/travel-concierge) is implemented adding a backend with ADK and a nice Angular Frontend to interact with it in an easy and straightforward way. + +## Overview + +This project provides an advanced framework for a travel concierge service powered by a multi-agent system. By utilizing an Agent Development Kit (ADK), developers can easily create, deploy, and manage specialized agents (e.g., for flights, accommodations, local activities, transportation, real-time alerts). The core Agent Engine orchestrates these agents, enabling them to collaborate and intelligently respond to user needs, offering personalized recommendations and proactive support. The architecture is designed with a decoupled frontend and backend, ensuring scalability and maintainability for a rich user experience. + +## Demo +Here's a look at our Travel Concierge MultiAgent in action! + + + +## Prerequisites + +Before you begin, ensure you have the following installed: + +* **Docker and Docker Compose v2:** Essential for the containerized deployment. + * Verify your Docker Compose version with `docker compose version`. If you have an older `docker-compose` (with a hyphen), you might need to upgrade to use `docker compose` in the commands. +* **Google Cloud SDK (`gcloud` CLI):** May be required if any agents or the Agent Engine interact with Google Cloud services (e.g., for data storage, specific APIs, or managed services). +* **Python 3.x:** For backend development (if not using Docker). +* **Node.js and npm (or yarn):** For frontend development (if not using Docker). + +## Getting Started + +You have two main options to get the application running: + +### Option 1: Using Docker Compose (Recommended for Quick Start) + +This is the **simplest way to get the entire application (frontend and backend) up and running!** You just need to run `docker compose up` after initial setup. See the next steps: + +1. **Ensure Docker and Docker Compose v2 are installed and running.** + +2. **Authenticate with Google Cloud (if applicable):** + If your agents or the Agent Engine need to interact with Google Cloud services, you may need to provide Google Cloud credentials. For local development with ADC: + ```bash + gcloud auth application-default login + gcloud config set project # If using a specific GCP project + gcloud auth application-default set-quota-project # If using a specific GCP project + + # Verify your configuration + gcloud auth list + gcloud config list project + ``` + The `docker-compose.yml` file can be configured to mount these local credentials into the backend container. + > **Windows Users:** The path to ADC might differ. Adjust volume mounts in `docker-compose.yml` if needed. + > **Note:** Ensure any required APIs are enabled in your Google Cloud project if used. + +3. **Build Docker Images:** + Build the Docker images for the frontend and backend services: + ```bash + docker compose build + ``` + The backend will be configured using environment variables (see "Environment Variables" section), including any necessary API keys for travel services, ADK configurations, or Agent Engine settings. + +4. **Run the application:** + After building the images, start the services: + ```bash + docker compose up + ``` + The frontend should typically be available at `http://localhost:4200` (or as configured) and the backend API at `http://localhost:8080`. + +### Option 2: Manual Setup (for Development and Customization) + +Follow these steps if you prefer to run the frontend and backend services manually on your local machine. + +**A. Backend Setup** + +1. **Navigate to the `backend/` directory.** + ```bash + cd backend + ``` + +2. **Create a virtual environment and install dependencies:** + ```bash + # Check if you are already in an environment + pip -V + + # If not, create and activate (for Linux/macOS) + python3 -m venv .venv + source .venv/bin/activate + + # Install requirements + pip3 install -r requirements.txt + ``` + > **VS Code Tip:** If VS Code doesn't recognize your virtual environment, press `Ctrl + Shift + P` (or `Cmd + Shift + P` on Mac), type "Python: Select Interpreter", choose "Enter interpreter path...", and then find and select `.venv/bin/python` inside your `backend` directory. + +3. **Setup Google Cloud (`gcloud`) credentials (if applicable):** + If your backend, agents, or Agent Engine interact with GCP, ensure you're authenticated. + ```bash + gcloud auth login # Login with your user account + gcloud config set project # If using a specific GCP project + + # For services using Application Default Credentials (ADC) locally + gcloud auth application-default login + gcloud auth application-default set-quota-project # If using a specific GCP project + + # Verify configuration + gcloud auth list + gcloud config list project + ``` + +4. **Configure Environment Variables:** + Backend configuration is managed via environment variables. Create a `.local.env` file in the `backend/` directory (copy from `.local.env.example` if one exists). This file should be in `.gitignore`. + + * **For Mac/Windows (or zsh console on Linux):** + Source the variables directly (from the `backend/` directory): + ```bash + . ./.local.env + ``` + * **For Linux (bash):** + Open `backend/.venv/bin/activate` and append the `export` commands from your `backend/.local.env` file after the `PATH` export section. For example: + ```sh + # ... existing activate script content ... + _OLD_VIRTUAL_PATH="$PATH" + PATH="$VIRTUAL_ENV/bin:$PATH" + export PATH + + # Quickbot env variables (copied from .local.env) + export ENVIRONMENT="development" + export FRONTEND_URL="http://localhost:4200" + # ADK, Agent Engine, and Travel API variables + # export ADK_CONFIG_PATH="/path/to/adk_config.json" + # export AGENT_ENGINE_ENDPOINT="http://localhost:xxxx/api/agent-engine" # Or other config + # export FLIGHT_API_KEY="your_flight_api_key" + # export HOTEL_API_KEY="your_hotel_api_key" + # export WEATHER_API_KEY="your_weather_api_key" + # ... other necessary agent or backend variables ... + ``` + Verify the variables are set by running `env` in your activated terminal. + +5. **Run the setup script (if applicable):** + This script might perform initial configurations for the ADK, Agent Engine, or agent registration. + ```bash + # from the backend/ directory + python3 setup.py + ``` + +6. **Run the backend application:** + ```bash + # from the backend/ directory + uvicorn main:app --reload --port 8080 + ``` + +**B. Frontend Setup** + +(These instructions assume a typical TypeScript/Angular frontend. Adjust as necessary based on your `frontend/README.md`.) + +1. **Navigate to the `frontend/` directory.** + ```bash + cd frontend + ``` +2. **Install dependencies:** + ```bash + npm install + ``` +3. **Environment Variables (if applicable):** + The frontend might require its own environment configuration (e.g., via a `.env` file or Angular's `environment.ts` files for API endpoints). Check the `frontend/` directory or its `README.md` for specific instructions. +4. **Run the frontend application:** + ```bash + npm start + # Or, for many Angular projects: + # ng serve + ``` + The application will typically be available at `http://localhost:4200`. + +## Project Structure (highlighting important parts) +```text +multi-agent-travel-concierge-with-adk/ +├── backend/ # Python backend (FastAPI/Uvicorn) for agent orchestration/API +│ ├── .venv/ # Python virtual environment (gitignored) +│ ├── .local.env # Local environment variables (gitignored) +│ ├── main.py # Main application file (e.g., FastAPI app) +│ ├── requirements.txt # Backend dependencies +│ ├── setup.py # Backend setup script (e.g., ADK init, agent registration) +│ └── README.md # Backend-specific instructions +├── frontend/ # TypeScript frontend (Angular) for UI +│ ├── node_modules/ # Node.js dependencies (gitignored) +│ ├── src/ # Frontend source code +│ ├── package.json # Frontend dependencies and scripts +│ ├── tsconfig.json # TypeScript configuration +│ └── README.md # Frontend-specific instructions +├── docker-compose.yml # Docker Compose configuration for all services +└── README.md # This file: Root project README +``` + +## Environment Variables + +Configuration for both frontend and backend is primarily managed through environment variables. + +* **Backend:** + * When running manually, backend environment variables are typically defined in `backend/.local.env`. + * When running with Docker, these variables are usually passed into the backend container via the `docker-compose.yml` file (often referencing a `.env` file at the root or `backend/` directory). + * **Please consult your `docker-compose.yml` for the definitive list of required backend environment variables.**: + * `IS_FIRST_DEPLOYMENT`: Whether to deploy the resources or not when running docker compose. + * `_PROJECT_ID`: Your Google Cloud Project ID (if any GCP services are used by agents or the engine). + * `_REGION`: Your Google Cloud region. + * `ENVIRONMENT`: Application environment (e.g., `development`, `production`). + * `FRONTEND_URL`: URL of the frontend application (e.g., `http://localhost:4200`). + * *(Add/remove/modify based on your actual `docker-compose.yml` and backend needs)* + * Consult `backend/README.md` or `backend/.local.env.example` for a complete and accurate list and details on agent-specific configurations. + +* **Frontend:** + * Frontend environment variables (e.g., API endpoint URLs) are usually managed within the frontend's build system (e.g., Angular's `environment.ts` files or a `.env` file in the `frontend/` directory). + * Consult `frontend/README.md` for specific details. + +## Code Styling & Commit Guidelines + +To maintain code quality and consistency across the project: + +* **TypeScript (Frontend):** We follow the Angular Coding Style Guide by leveraging Google's TypeScript Style Guide using `gts`. This includes a formatter, linter, and automatic code fixer. +* **Python (Backend):** We adhere to the Google Python Style Guide, using tools like `pylint` and `black` for linting and formatting. +* **Commit Messages:** We suggest following Angular's Commit Message Guidelines to create clear and descriptive commit messages. + +### Frontend (TypeScript with `gts`) + +(Assumes setup within the `frontend/` directory) + +1. **Initialize `gts` (if not already done in the project):** + Navigate to `frontend/` and run: + ```bash + npx gts init + ``` + This will set up `gts` and create necessary configuration files (like `tsconfig.json`). Ensure your `tsconfig.json` (or a related `gts` config file like `.gtsrc`) includes an extension for `gts` defaults, typically: + ```json + { + "extends": "./node_modules/gts/tsconfig-google.json" + // ... other configurations + } + ``` +2. **Check for linting issues:** + (This assumes a `lint` script is defined in `frontend/package.json`, e.g., `"lint": "gts lint"`) + ```bash + # from frontend/ directory + npm run lint + ``` +3. **Fix linting issues automatically (where possible):** + (This assumes a `fix` script is defined in `frontend/package.json`, e.g., `"fix": "gts fix"`) + ```bash + # from frontend/ directory + npm run fix + ``` + +### Backend (Python with `pylint` and `black`) + +(Assumes setup within the `backend/` directory and its virtual environment activated) + +1. **Ensure Dependencies are Installed:** + Add `pylint` and `black` to your `backend/requirements.txt` file if not already present: + ``` + pylint + black + ``` + Then install them within your virtual environment: + ```bash + # from backend/ directory, with .venv activated + pip install pylint black + # or pip install -r requirements.txt + ``` +2. **Configure `pylint`:** + It's recommended to have a `.pylintrc` file in your `backend/` directory to configure `pylint` rules. You can generate one if it doesn't exist: + ```bash + # from backend/ directory + pylint --generate-rcfile > .pylintrc + ``` + Customize this file according to your project's needs and the Google Python Style Guide. +3. **Check for linting issues with `pylint`:** + Navigate to the `backend/` directory and run: + ```bash + # from backend/ directory + pylint . + # Or specify modules/packages: pylint agents/ adk_components/ agent_engine/ travel_services/ + ``` +4. **Format code with `black`:** + To automatically format all Python files in the `backend/` directory and its subdirectories: + ```bash + # from backend/ directory + python -m black . --line-length=80 + ``` diff --git a/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/assets/quickbot-adk-travel-multiagent.mp4 b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/assets/quickbot-adk-travel-multiagent.mp4 new file mode 100644 index 0000000..6ab7f71 Binary files /dev/null and b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/assets/quickbot-adk-travel-multiagent.mp4 differ diff --git a/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/backend/.gcloudignore b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/backend/.gcloudignore new file mode 100644 index 0000000..606a07e --- /dev/null +++ b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/backend/.gcloudignore @@ -0,0 +1,20 @@ +# This file specifies files that are *not* uploaded to Google Cloud +# using gcloud. It follows the same syntax as .gitignore, with the addition of +# "#!include" directives (which insert the entries of the given .gitignore-style +# file at that point). +# +# For more information, run: +# $ gcloud topic gcloudignore +# +.gcloudignore +# If you would like to upload your .git directory, .gitignore file or files +# from your .gitignore file, remove the corresponding line +# below: +.git +.gitignore + +# Python pycache: +__pycache__/ +# Ignored by the build system +/setup.cfg +my_env \ No newline at end of file diff --git a/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/backend/.gitignore b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/backend/.gitignore new file mode 100644 index 0000000..00ffb38 --- /dev/null +++ b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/backend/.gitignore @@ -0,0 +1,8 @@ +__pycache__/ +.idea/ +.vscode/ +.venv/ +env +bin +lib +.iml \ No newline at end of file diff --git a/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/backend/Dockerfile b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/backend/Dockerfile new file mode 100644 index 0000000..9f8c6eb --- /dev/null +++ b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/backend/Dockerfile @@ -0,0 +1,29 @@ +# Copyright 2025 Google LLC +# +# 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 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +FROM python:3.11 +COPY requirements.txt /tmp +RUN pip install -r /tmp/requirements.txt +RUN mkdir -p /code +WORKDIR /code +COPY . /code/ + +ENV ENVIRONMENT="development" +ENV BIG_QUERY_DATASET="" +ENV FRONTEND_URL="" +ENV FUNCTION_URL="" +ENV TASK_QUEUE_NAME="" + +EXPOSE 8080 +ENTRYPOINT ["gunicorn", "main:app", "--workers=4", "--worker-class=uvicorn.workers.UvicornWorker", "--timeout=36000", "--bind=0.0.0.0:8080"] \ No newline at end of file diff --git a/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/backend/Dockerfile.local b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/backend/Dockerfile.local new file mode 100644 index 0000000..6abdf16 --- /dev/null +++ b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/backend/Dockerfile.local @@ -0,0 +1,102 @@ +# syntax=docker/dockerfile:1.4 +FROM python:3.11 AS builder + +# Build arguments +ARG IS_FIRST_DEPLOYMENT="False" +# Add ARGs for variables needed by setup.py +ARG _PROJECT_ID="your-default-project-id-for-build" +ARG GCLOUD_PROJECT=$_PROJECT_ID +ARG _REGION="your-default-region-for-build" +ARG ENVIRONMENT="development" +ARG FRONTEND_URL="" +ARG BIG_QUERY_DATASET="" +ARG _CHATBOT_NAME="" +ARG _ADK_GOOGLE_PLACES_API_KEY="" +ARG GOOGLE_APPLICATION_CREDENTIALS="" + +# Environment variables for the build process +ENV IS_FIRST_DEPLOYMENT=$IS_FIRST_DEPLOYMENT +ENV _PROJECT_ID=$_PROJECT_ID +ENV GCLOUD_PROJECT=$GCLOUD_PROJECT +ENV _REGION=$_REGION +ENV ENVIRONMENT=$ENVIRONMENT +ENV FRONTEND_URL=$FRONTEND_URL +ENV BIG_QUERY_DATASET=$BIG_QUERY_DATASET +ENV _CHATBOT_NAME=$_CHATBOT_NAME +ENV _ADK_GOOGLE_PLACES_API_KEY=$_ADK_GOOGLE_PLACES_API_KEY +ENV GOOGLE_APPLICATION_CREDENTIALS=$GOOGLE_APPLICATION_CREDENTIALS + +WORKDIR /code + +COPY . . + +# --- Stage 1: Conditional First Deployment Setup --- +COPY prepare_code.sh ./prepare_code.sh +COPY setup.py ./setup.py + +RUN --mount=type=secret,id=gcp_credentials,target=/tmp/gcp_adc.json,required=true \ + if [ "$IS_FIRST_DEPLOYMENT" = "True" ]; then \ + echo "--- Running first deployment setup (IS_FIRST_DEPLOYMENT=True) ---"; \ + echo "Using _PROJECT_ID=${_PROJECT_ID} and _REGION=${_REGION} for setup"; \ + \ + if [ ! -f /tmp/gcp_adc.json ]; then \ + echo "CRITICAL ERROR: GCP credentials secret was expected but not mounted to /tmp/gcp_adc.json" >&2; \ + exit 1; \ + fi; \ + echo "DEBUG: GCP credentials secret successfully mounted to /tmp/gcp_adc.json"; \ + \ + # Set GOOGLE_APPLICATION_CREDENTIALS specifically for this RUN command's execution context + export GOOGLE_APPLICATION_CREDENTIALS=/tmp/gcp_adc.json; \ + echo "DEBUG: GOOGLE_APPLICATION_CREDENTIALS for setup is set to: $GOOGLE_APPLICATION_CREDENTIALS"; \ + \ + echo "Installing Poetry..."; \ + pip install poetry; \ + export PATH="$HOME/.local/bin:$PATH"; \ + poetry --version; \ + \ + echo "Executing prepare_code.sh..."; \ + chmod +x /code/prepare_code.sh; \ + bash /code/prepare_code.sh || { echo "prepare_code.sh failed!"; exit 1; }; \ + \ + echo "Executing setup.py with Poetry..."; \ + # Now setup.py can access _PROJECT_ID and _REGION as environment variables + poetry run python /code/setup.py || { echo "setup.py with poetry failed!"; exit 1; }; \ + echo "--- First deployment setup finished ---"; \ + else \ + echo "--- IS_FIRST_DEPLOYMENT is False, skipping first deployment setup. ---"; \ + fi + + +FROM python:3.11-slim AS final + +ARG _PROJECT_ID="your-default-project-id-for-build" +ARG GCLOUD_PROJECT=$_PROJECT_ID +ARG _REGION="your-default-region-for-build" +ARG ENVIRONMENT="development" +ARG FRONTEND_URL="" +ARG BIG_QUERY_DATASET="" +ARG _CHATBOT_NAME="" +ARG _ADK_GOOGLE_PLACES_API_KEY="" +ARG GOOGLE_APPLICATION_CREDENTIALS="" + +# Environment variables for the build process +ENV _PROJECT_ID=$_PROJECT_ID +ENV GCLOUD_PROJECT=$GCLOUD_PROJECT +ENV _REGION=$_REGION +ENV ENVIRONMENT=$ENVIRONMENT +ENV FRONTEND_URL=$FRONTEND_URL +ENV BIG_QUERY_DATASET=$BIG_QUERY_DATASET +ENV _CHATBOT_NAME=$_CHATBOT_NAME +ENV _ADK_GOOGLE_PLACES_API_KEY=$_ADK_GOOGLE_PLACES_API_KEY +ENV GOOGLE_APPLICATION_CREDENTIALS="/root/.config/gcloud/application_default_credentials.json" + +WORKDIR /app + +COPY --from=builder /code/requirements.txt /app/requirements.txt +RUN pip install --no-cache-dir -r /app/requirements.txt + +COPY --from=builder /code/main.py /app/main.py +COPY --from=builder /code/src /app/src + +EXPOSE 8080 +ENTRYPOINT ["gunicorn", "main:app", "--workers=4", "--worker-class=uvicorn.workers.UvicornWorker", "--timeout=36000", "--bind=0.0.0.0:8080"] \ No newline at end of file diff --git a/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/backend/README.md b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/backend/README.md new file mode 100644 index 0000000..1b4935d --- /dev/null +++ b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/backend/README.md @@ -0,0 +1,23 @@ +## Setting up + +Create a virtual environment on the root of the application and activate it. + +``` +python3 -m venv .venv +source .venv/bin/activate +pip3 install -r requirements.txt +``` + +### Running the set up script + +``` +python3 setup.py +``` + +### Running the application on local +Create a virtual environment, activate it, install the requirements, set environmental variables from local.env and run the application + +``` +. ./local.env +uvicorn main:app --reload --port 8080 +``` \ No newline at end of file diff --git a/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/backend/__init__.py b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/backend/__init__.py new file mode 100644 index 0000000..36a1e8d --- /dev/null +++ b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/backend/__init__.py @@ -0,0 +1,14 @@ +# Copyright 2025 Google LLC +# +# 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 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + diff --git a/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/backend/deploy_agent_engine.py b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/backend/deploy_agent_engine.py new file mode 100644 index 0000000..e4736ba --- /dev/null +++ b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/backend/deploy_agent_engine.py @@ -0,0 +1,135 @@ +# Copyright 2025 Google LLC +# +# 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 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Deployment script for Travel Concierge.""" + +import os + +import vertexai +from vertexai import agent_engines +from vertexai.preview.reasoning_engines import AdkApp +from google.cloud.storage.bucket import Bucket +# This root_agent comes after cloning the ADK repository by running prepare_code.sh +from travel_concierge.agent import root_agent # type: ignore + +def create(env_vars: dict[str, str]) -> None: + """Creates a new deployment.""" + app = AdkApp( + agent=root_agent, + enable_tracing=True, + env_vars=env_vars, + ) + + remote_agent = agent_engines.create( + app, + requirements=[ + "google-adk (==0.5.0)", + "google-cloud-aiplatform[agent_engines]@git+https://github.com/googleapis/python-aiplatform.git@copybara_738852226", + "google-genai (>=1.5.0,<2.0.0)", + "pydantic (>=2.10.6,<3.0.0)", + "absl-py (>=2.2.1,<3.0.0)", + "requests (>=2.32.3,<3.0.0)", + ], + extra_packages=[ + "./travel_concierge", # The main package + "./eval", + ], + ) + print(f"Created remote agent: {remote_agent.resource_name}") + + return remote_agent.resource_name + + +def delete(resource_id: str) -> None: + remote_agent = agent_engines.get(resource_id) + remote_agent.delete(force=True) + print(f"Deleted remote agent: {resource_id}") + return resource_id + + +def setup_remote_agent(bucket: Bucket) -> str | None: + """ + Sets up the Vertex AI Agent Engine deployment using environment variables. + + Retrieves necessary configuration from environment variables, initializes + Vertex AI, and calls the create function. + + Returns: + The resource name of the created agent engine, or None if setup fails. + """ + env_vars = {} + + # Retrieve configuration directly from environment variables + project_id = os.getenv("_PROJECT_ID") + location = os.getenv("_REGION") + # Sample Scenario Path - Default is an empty itinerary + # This will be loaded upon first user interaction. + # Uncomment one of the two, or create your own. + # _ADK_TRAVEL_CONCIERGE_SCENARIO=profiles/itinerary_seattle_example.json + initial_states_path = os.getenv("_ADK_TRAVEL_CONCIERGE_SCENARIO") if os.getenv("_ADK_TRAVEL_CONCIERGE_SCENARIO") else "eval/itinerary_empty_default.json" + map_key = os.getenv("_ADK_GOOGLE_PLACES_API_KEY") + + # Populate env_vars dictionary for the AdkApp + if initial_states_path: + env_vars["_ADK_TRAVEL_CONCIERGE_SCENARIO"] = initial_states_path + if map_key: + env_vars["_ADK_GOOGLE_PLACES_API_KEY"] = map_key + + # --- Validation --- + missing_vars = [] + if not project_id: + missing_vars.append("_PROJECT_ID") + if not location: + missing_vars.append("_REGION") + if not initial_states_path: + missing_vars.append("_ADK_TRAVEL_CONCIERGE_SCENARIO") + if not map_key: + missing_vars.append("_ADK_GOOGLE_PLACES_API_KEY") + + if missing_vars: + print("Error: Missing required environment variables:") + for var in missing_vars: + print(f"- {var}") + return None + + # --- Print confirmation (mask sensitive keys) --- + print(f"PROJECT: {project_id}") + print(f"LOCATION: {location}") + print(f"BUCKET: {bucket.name}") + print(f"INITIAL_STATE: {initial_states_path}") + print(f"MAP KEY (PARTIAL): {map_key[:5]}...") # Mask most of the key + + # --- Initialize Vertex AI --- + try: + vertexai.init( + project=project_id, + location=location, + staging_bucket=f"gs://{bucket.name}", + ) + print("Vertex AI initialized successfully.") + except Exception as e: + print(f"Error initializing Vertex AI: {e}") + return None + + # --- Create the deployment --- + try: + resource_name = create(env_vars) + return resource_name + except Exception as e: + print(f"Error during agent engine creation: {e}") + return None # Indicate failure + + +if __name__ == "__main__": + setup_remote_agent() diff --git a/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/backend/local.env b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/backend/local.env new file mode 100644 index 0000000..e8173f1 --- /dev/null +++ b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/backend/local.env @@ -0,0 +1,16 @@ +ENVIRONMENT="development" +FRONTEND_URL="http://localhost:4200" + +# --- Google Cloud Configuration --- +# Required for using Vertex AI +GOOGLE_GENAI_USE_VERTEXAI=1 +GOOGLE_CLOUD_PROJECT="your-project-id" # Replace with your Google Cloud Project ID +GOOGLE_CLOUD_LOCATION="us-central1" # Replace with your Google Cloud Location (e.g., us-central1) + +# --- Optional: Agent Engine Configuration --- +GOOGLE_CLOUD_STORAGE_BUCKET="your-gcs-bucket-name" # Used for deploying to Agent Engine on Vertex AI +GOOGLE_PLACES_API_KEY="your-places-api-key" +TRAVEL_CONCIERGE_SCENARIO=eval/itinerary_empty_default.json + +# --- App Configuration --- +BIG_QUERY_DATASET=quick_bot_app diff --git a/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/backend/main.py b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/backend/main.py new file mode 100644 index 0000000..2f2d8ac --- /dev/null +++ b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/backend/main.py @@ -0,0 +1,101 @@ +# Copyright 2025 Google LLC +# +# 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 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import sys +from fastapi import FastAPI, File, UploadFile +from fastapi.middleware.cors import CORSMiddleware +from src.controller.chats import router as chat_router +from src.controller.intents import router as intent_router +from src.controller.models import router as model_router +from google.cloud import speech +from os import getenv +import logging + +logging.basicConfig(level=logging.INFO, stream=sys.stderr) +logging.info("Test message") + +app = FastAPI() + + +def configure_cors(app): + """Configures CORS middleware based on the environment.""" + environment = getenv("ENVIRONMENT") + allowed_origins = [] + + if environment == "production": + frontend_url = getenv("FRONTEND_URL") + if not frontend_url: + raise ValueError("FRONTEND_URL environment variable not set in production") + allowed_origins.append(frontend_url) + elif environment == "development": + allowed_origins.append("*") # Allow all origins in development + else: + raise ValueError( + f"Invalid ENVIRONMENT: {environment}. Must be 'production' or 'development'" + ) + + app.add_middleware( + CORSMiddleware, + allow_origins=allowed_origins, + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], + ) + + +# Create a route to handle GET requests on root +@app.get("/") +async def root(): + return "You are calling Quick Bot Backend" + + +# Create a route to handle GET requests on /version +@app.get("/api/version") +def version(): + return "v0.0.1" + + +@app.post("/api/audio_chat") +async def audio_chat(audio_file: UploadFile = File(...)): + client = speech.SpeechClient() + audio_content = await audio_file.read() + audio = speech.RecognitionAudio(content=audio_content) + config = speech.RecognitionConfig( + language_code="en-US", + sample_rate_hertz=48000, + model="default", + audio_channel_count=1, + enable_word_confidence=True, + enable_word_time_offsets=True, + ) + + operation = client.long_running_recognize(config=config, audio=audio) + + print("Waiting for operation to complete...") + response = operation.result(timeout=90) + print(response) + + text = "" + for result in response.results: + print("Transcript: {}".format(result.alternatives[0].transcript)) + text = result.alternatives[0].transcript + + return text, 200 + + +configure_cors(app) + +app.include_router(chat_router) +app.include_router(intent_router) +app.include_router(model_router) diff --git a/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/backend/prepare_code.sh b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/backend/prepare_code.sh new file mode 100644 index 0000000..06ed761 --- /dev/null +++ b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/backend/prepare_code.sh @@ -0,0 +1,34 @@ +#!/bin/bash +# Copyright 2025 Google LLC +# +# 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 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +echo "Starting prepare_code.sh script..." + +# TODO: Pass this as a template parameter to generalize solution for any ADK agent +# https://github.com/google/adk-samples.git/adk-samples/python/agents/travel-concierge/travel_concierge +git clone https://github.com/google/adk-samples.git +git checkout adf6402 # Go to a particular working commit as this changes quite regularly +cp -r adk-samples/python/agents/travel-concierge/travel_concierge ./travel_concierge +cp -r adk-samples/python/agents/travel-concierge/eval ./eval +cp adk-samples/python/agents/travel-concierge/pyproject.toml . +poetry install --with deployment + +# Install setup.py dependencies +poetry add google-cloud-bigquery +poetry add google-cloud-logging +poetry add google-cloud-storage@2.19.0 +poetry add requests + +echo "Finished prepare_code.sh script." \ No newline at end of file diff --git a/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/backend/requirements.txt b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/backend/requirements.txt new file mode 100644 index 0000000..a3a50d2 --- /dev/null +++ b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/backend/requirements.txt @@ -0,0 +1,25 @@ +poetry +gunicorn +uvicorn[standard] +websocket +fastapi +fastapi-utilities +cloudpickle +h5py +grpcio +urllib3 +typing-inspect +typing_extensions +pydantic +requests +langchain +langchain-community +langchain-core +langchain-google-vertexai +google-adk==1.1.1 +google-cloud-aiplatform[adk, agent_engines]==1.95.1 +google-cloud-bigquery +google-cloud-tasks +google-cloud-logging +google-cloud-speech +python-multipart \ No newline at end of file diff --git a/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/backend/scripts/__init__.py b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/backend/scripts/__init__.py new file mode 100644 index 0000000..36a1e8d --- /dev/null +++ b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/backend/scripts/__init__.py @@ -0,0 +1,14 @@ +# Copyright 2025 Google LLC +# +# 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 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + diff --git a/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/backend/scripts/big_query_setup.py b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/backend/scripts/big_query_setup.py new file mode 100644 index 0000000..bb28bca --- /dev/null +++ b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/backend/scripts/big_query_setup.py @@ -0,0 +1,46 @@ +# Copyright 2025 Google LLC +# +# 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 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from typing import List + +from google.cloud.bigquery import Client +from google.cloud.bigquery import SchemaField +from google.cloud.bigquery import Table + + +def create_dataset(dataset_name: str, bigquery_client: Client): + bigquery_client.delete_dataset( + dataset_name, delete_contents=True, not_found_ok=True + ) + print(f"{dataset_name} Creating dataset...") + bigquery_client.create_dataset(dataset_name) + + +def create_table( + dataset: str, + table_name: str, + schema: List[SchemaField], + project_id: str, + bigquery_client, +): + bigquery_client.create_table(Table(f"{project_id}.{dataset}.{table_name}", schema)) + + +def insert_intent(dataset: str, table_name: str, values: str, bigquery_client: Client): + bigquery_client.query( + f""" + INSERT INTO `{dataset}.{table_name}` + VALUES({values}); + """ + ).result() diff --git a/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/backend/scripts/gcs_setup.py b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/backend/scripts/gcs_setup.py new file mode 100644 index 0000000..643a8a4 --- /dev/null +++ b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/backend/scripts/gcs_setup.py @@ -0,0 +1,28 @@ +# Copyright 2025 Google LLC +# +# 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 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from google.cloud.storage import Client +from google.cloud.storage.bucket import Bucket + +def create_bucket(bucket_name: str, location: str, storage_client: Client) -> Bucket: + storage_bucket = storage_client.bucket(bucket_name) + if not storage_bucket.exists(): + print(f"{bucket_name} Bucket does not exist, creating it...") + storage_bucket = storage_client.create_bucket( + bucket_name, + location=location, + ) + print(f"Successfully created bucket {storage_bucket.name}") + + return storage_bucket diff --git a/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/backend/setup.py b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/backend/setup.py new file mode 100644 index 0000000..d89aac3 --- /dev/null +++ b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/backend/setup.py @@ -0,0 +1,104 @@ +# Copyright 2025 Google LLC +# +# 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 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os + +from google.cloud.bigquery import Client as BigQueryClient +from google.cloud.storage import Client as GCSClient + +from deploy_agent_engine import setup_remote_agent +from scripts.big_query_setup import create_dataset +from scripts.big_query_setup import create_table +from scripts.big_query_setup import insert_intent +from scripts.gcs_setup import create_bucket +from src.model.chats import Chat +from src.model.embedding import Embedding +from src.model.intent import Intent +from src.repository.big_query import CHATS_TABLE +from src.repository.big_query import EMBEDDINGS_TABLE +from src.service.intent import INTENTS_TABLE + +try: + print("Setting up GCS... \n") + project_id = os.getenv("_PROJECT_ID") + location = os.getenv("_REGION") + storage_client = GCSClient() + + bucket = create_bucket(f"quick-bot-{project_id}-travel-concierge-bucket", location, storage_client) + + print("Setting up Remote Agent... \n") + remote_agent_resource_id = setup_remote_agent(bucket) + + DEFAULT_INTENTS = [ + Intent( + name="Travel concierge", + ai_model="gemini-2.0-flash", + ai_temperature=1, + description="", + prompt="", + questions=[], + status="5", + remote_agent_resource_id=remote_agent_resource_id, + ), + ] + + print(f"Successfully setted Agent. Remote agent resource ID: {remote_agent_resource_id} \n") + + print("Setting up BigQuery... \n") + bigquery_client = BigQueryClient() + BIG_QUERY_DATASET = os.getenv("BIG_QUERY_DATASET") if os.getenv("BIG_QUERY_DATASET") else os.getenv("_CHATBOT_NAME", "quickbot").lower().replace(" ", "_").replace("-","_") + print(f"Setting up BigQuery Dataset {BIG_QUERY_DATASET}... \n") + create_dataset(BIG_QUERY_DATASET, bigquery_client) + + print(f"Setting up BigQuery Table {CHATS_TABLE}... \n") + create_table( + BIG_QUERY_DATASET, CHATS_TABLE, Chat.__schema__(), project_id, bigquery_client + ) + print(f"Setting up BigQuery Table {EMBEDDINGS_TABLE}... \n") + create_table( + BIG_QUERY_DATASET, + EMBEDDINGS_TABLE, + Embedding.__schema__(), + project_id, + bigquery_client, + ) + print(f"Setting up BigQuery Table {INTENTS_TABLE}... \n") + create_table( + BIG_QUERY_DATASET, INTENTS_TABLE, Intent.__schema__(), project_id, bigquery_client + ) + + for intent in DEFAULT_INTENTS: + try: + insert_intent( + BIG_QUERY_DATASET, INTENTS_TABLE, intent.to_insert_string(), bigquery_client + ) + print(f"Intent 'Travel concierge' created successfully \n") + except Exception as e: + print(e) + print(f"An error occurred during the setup process: {e}") + print(f"ERROR: A command in the backend setup block failed with code {e.returncode}.") + if e.stdout: print(f"Stdout from failed command:\n{e.stdout}") + else: print("Stdout from failed command: ") + if e.stderr: print(f"Stderr from failed command:\n{e.stderr}") + else: print("Stderr from failed command: ") + + print("\nSuccess!\n") +except Exception as e: + print(f"A critical error occurred during the setup process: {e}") + import traceback + print("Detailed traceback:") + print(traceback.format_exc()) + # If running in Docker build, exiting with non-zero will fail the build + import sys + sys.exit(1) diff --git a/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/backend/src/__init__.py b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/backend/src/__init__.py new file mode 100644 index 0000000..36a1e8d --- /dev/null +++ b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/backend/src/__init__.py @@ -0,0 +1,14 @@ +# Copyright 2025 Google LLC +# +# 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 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + diff --git a/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/backend/src/controller/__init__.py b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/backend/src/controller/__init__.py new file mode 100644 index 0000000..36a1e8d --- /dev/null +++ b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/backend/src/controller/__init__.py @@ -0,0 +1,14 @@ +# Copyright 2025 Google LLC +# +# 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 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + diff --git a/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/backend/src/controller/chats.py b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/backend/src/controller/chats.py new file mode 100644 index 0000000..74b4874 --- /dev/null +++ b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/backend/src/controller/chats.py @@ -0,0 +1,186 @@ +# Copyright 2025 Google LLC +# +# 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 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import asyncio +import json +import logging + +from fastapi import APIRouter +from fastapi import BackgroundTasks +from fastapi import WebSocket +from starlette.websockets import WebSocketDisconnect +from vertexai import agent_engines + +from src.model.chats import Chat +from src.model.chats import CreateChatRequest +from src.service.chats import ChatsService +from src.service.intent import IntentService +from src.service.intent_matching import IntentMatchingService + +DEFAULT_USER_ID = "traveler0115" +INACTIVITY_TIMEOUT_SECONDS = 10 * 60 # 10 minutes + +router = APIRouter( + prefix="/api/chats", + tags=["chats"], + responses={404: {"description": "Not found"}}, +) + + +@router.websocket("") +async def websocket_chat( + *, + websocket: WebSocket, + background_tasks: BackgroundTasks, +): + await websocket.accept() + active_session_id = "N/A (or setup phase)" + + try: + # Create a new session for this WebSocket connection + # This session will be used for the lifetime of this connection. + intent = get_default_intent() # General intent for the connection + remote_agent_resource_id = intent.remote_agent_resource_id + remote_agent = agent_engines.get(remote_agent_resource_id) + print(f"WebSocket connected. Agent resource: {remote_agent_resource_id}") + logging.info(f"WebSocket connected. Agent resource: {remote_agent_resource_id}") + + agent_session = remote_agent.create_session(user_id=DEFAULT_USER_ID) + active_session_id = agent_session["id"] + print(f"Created new agent session for WebSocket connection: {active_session_id}") + logging.info(f"Created new agent session for WebSocket connection: {active_session_id}") + + # Notify client that the session has started, without sending the session ID + await websocket.send_json({"operation": "start"}) + + while True: + try: + item_json = await asyncio.wait_for( + websocket.receive_json(), + timeout=INACTIVITY_TIMEOUT_SECONDS + ) + print(f"Received JSON from client: {item_json}") + logging.info(f"Received JSON from client (session: {active_session_id}): {item_json}") + # The CreateChatRequest might still have chat_id for HTTP, + # but we ignore it for WebSocket session management here. + current_item = CreateChatRequest(**item_json) + except asyncio.TimeoutError: + logging.warning( + f"Client inactive for {INACTIVITY_TIMEOUT_SECONDS} seconds." + f"Closing connection for session: {active_session_id}." + ) + if websocket.client_state == websocket.client_state.CONNECTED: + await websocket.send_json({ + "operation": "timeout", + "message": "Connection closed due to inactivity." + }) + break + except WebSocketDisconnect: + print(f"Client disconnected (session: {active_session_id}).") + logging.info("Client disconnected.") + break # Exit the main loop + except json.JSONDecodeError: + print(f"Failed to decode message as JSON (session: {active_session_id}). Sending error to client.") + logging.error("Failed to decode message as JSON. Sending error to client.") + await websocket.send_json({"error": "Invalid JSON format received."}) + continue # Wait for a new, valid message + except Exception as e: + print(f"Error receiving or parsing client message (session: {active_session_id}): {e}") + logging.error(f"Error receiving or parsing client message: {e}") + await websocket.send_json( + {"error": f"Error processing your request: {str(e)}"} + ) + break # Exit on other receive/parse errors + + current_message_text = current_item.text + + print(f"Processing message: '{current_message_text}' for session: {active_session_id}") + logging.info(f"Processing message: '{current_message_text}' for session: {active_session_id}") + for event in remote_agent.stream_query( + user_id=DEFAULT_USER_ID, + session_id=active_session_id, # Use the session created for this connection + message=current_message_text, + ): + content = event.get("content") + if content: + for part in event["content"]["parts"]: + # Send only the answer part, no session ID needed by client here + answer_part = {"answer": part} + await websocket.send_json(answer_part) + log_response( + background_tasks, + intent, + current_message_text, + json.dumps(part), # Log the streamed part + agent_session, # Pass the created agent_session object + [], # Suggested questions for this part + ) + + # After streaming all parts for the current message's response + # Signal end of turn, no session ID needed by client here + await websocket.send_json({"operation": "end_of_turn"}) + print(f"Sent 'end_of_turn' for session: {active_session_id}. Waiting for next client message...") + logging.info(f"Sent 'end_of_turn' for session: {active_session_id}. Waiting for next client message...") + + except WebSocketDisconnect: + print(f"WebSocket disconnected during operation (session: {active_session_id}).") + logging.error(f"WebSocket disconnected during operation (session: {active_session_id}).") + except Exception as e: + print(f"An unhandled error occurred in WebSocket handler (session: {active_session_id}): {e}") + logging.error(f"An unhandled error occurred in WebSocket handler (session: {active_session_id}): {e}") + try: + if websocket.client_state == websocket.client_state.CONNECTED: + await websocket.send_json( + { + "error": f"An unexpected server error occurred in WebSocket handler (session: {active_session_id}): {e}", + "operation": "fatal_error", + } + ) + except Exception as send_exc: + print(f"Could not send error to client: {send_exc}") + finally: + print(f"Closing WebSocket connection from server-side finally block (session: {active_session_id}).") + logging.info(f"Closing WebSocket connection from server-side finally block (session: {active_session_id}).") + if websocket.client_state == websocket.client_state.CONNECTED: + await websocket.close() + + +# The log_response and get_default_intent functions are still used by the websocket_chat endpoint. +def log_response( + background_tasks, + intent, + message, + model_response_content, + session_details, + suggested_questions, +): + final_response = Chat( + id=session_details["id"], # The session_details will have the id + question=message, + answer=model_response_content, + intent=intent.name, + suggested_questions=suggested_questions, + ) + background_tasks.add_task( + ChatsService().insert_chat, + final_response, + ) + return final_response + + +def get_default_intent(): + intents = IntentService().get_all() + intent_matching_service = IntentMatchingService(intents) + intent = intent_matching_service.get_intent_from_query("") + return intent diff --git a/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/backend/src/controller/intents.py b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/backend/src/controller/intents.py new file mode 100644 index 0000000..df903c4 --- /dev/null +++ b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/backend/src/controller/intents.py @@ -0,0 +1,85 @@ +# Copyright 2025 Google LLC +# +# 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 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from fastapi import APIRouter, HTTPException +from src.model.http_status import BadRequest +from src.model.intent import CreateIntentRequest, Intent +from src.model.event import IntentCreateEvent +from src.repository.task import TaskRepository +from src.service.index_endpoint import IndexEndpointService +from src.service.intent import IntentService + +router = APIRouter( + prefix="/api/intents", + tags=["intents"], + responses={404: {"description": "Not found"}}, +) +@router.get("") +async def get_intents(): + service = IntentService() + intents = service.get_all() + + for intent in intents: + if not intent.is_active(): + index_endpoint_service = IndexEndpointService() + if index_endpoint_service.endpoint_has_deployed_indexes(intent.get_standard_name()): + intent.status = "5" + service.update(intent.name, intent) + + return intents + +@router.post("") +async def create_intent(intent: CreateIntentRequest): + intent_service = IntentService() + index_endpoint_service = IndexEndpointService() + task_repository = TaskRepository() + + saved_intent = None + index_endpoint = None + try: + saved_intent = intent_service.create(intent.to_intent()) + if intent.gcp_bucket: + index_endpoint = index_endpoint_service.create_endpoint(saved_intent.get_standard_name()) + task_repository.create( + IntentCreateEvent( + intent_name=intent.name, + index_endpoint_resource=index_endpoint.resource_name, + ), + ) + except BadRequest as e: + raise HTTPException(status_code=400, detail=e.detail) + except Exception as e: + print(e) + if saved_intent: + intent_service.delete(saved_intent.name) + if index_endpoint: + index_endpoint_service.delete_endpoint(index_endpoint) + + return saved_intent + +@router.delete("/{intent_name}") +async def delete_intent(intent_name: str): + service = IntentService() + intent = service.get(intent_name) + if intent.gcp_bucket: + index_endpoint_service = IndexEndpointService() + endpoint = index_endpoint_service.get_endpoint(intent.get_standard_name()) + index_endpoint_service.delete_endpoint(endpoint) + service.delete(intent_name) + return + +@router.put("/{intent_name}") +async def update_intent(intent_name: str, intent: Intent): + service = IntentService() + return service.update(intent_name, intent) \ No newline at end of file diff --git a/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/backend/src/controller/models.py b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/backend/src/controller/models.py new file mode 100644 index 0000000..5a1a399 --- /dev/null +++ b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/backend/src/controller/models.py @@ -0,0 +1,29 @@ +# Copyright 2025 Google LLC +# +# 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 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from fastapi import APIRouter + +from src.service.models import ModelService + +router = APIRouter( + prefix="/api/models", + tags=["models"], + responses={404: {"description": "Not found"}}, +) + + +@router.get("") +async def get_models(): + service = ModelService() + return service.get_all() \ No newline at end of file diff --git a/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/backend/src/model/__init__.py b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/backend/src/model/__init__.py new file mode 100644 index 0000000..36a1e8d --- /dev/null +++ b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/backend/src/model/__init__.py @@ -0,0 +1,14 @@ +# Copyright 2025 Google LLC +# +# 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 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + diff --git a/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/backend/src/model/chats.py b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/backend/src/model/chats.py new file mode 100644 index 0000000..5bc6f54 --- /dev/null +++ b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/backend/src/model/chats.py @@ -0,0 +1,55 @@ +# Copyright 2025 Google LLC +# +# 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 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from google.cloud.bigquery import SchemaField +from pydantic import BaseModel +from typing import List, Optional + + +class Chat(BaseModel): + id: str + question: str + answer: str + intent: str + suggested_questions: List[str] + timestamp: Optional[str] = None + + @classmethod + def __schema__(cls) -> List[SchemaField]: + return [ + SchemaField("id", "STRING", mode="REQUIRED"), + SchemaField("question", "STRING", mode="REQUIRED"), + SchemaField("answer", "STRING", mode="REQUIRED"), + SchemaField("intent", "STRING", mode="REQUIRED"), + SchemaField("suggested_questions", "STRING", mode="REPEATED"), + SchemaField("timestamp", "TIMESTAMP", mode="REQUIRED"), + ] + + def to_dict(self): + return { + "id": self.id, + "question": self.question, + "answer": self.answer, + "intent": self.intent, + "suggested_questions": self.suggested_questions, + "timestamp": self.timestamp, + } + + def to_insert_string(self): + return f'"{self.id}", """{self.question}""", """{self.answer}""", "{self.intent}", {str(self.suggested_questions)}, CURRENT_TIMESTAMP()' + + +class CreateChatRequest(BaseModel): + text: str + chat_id: Optional[str] = None diff --git a/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/backend/src/model/embedding.py b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/backend/src/model/embedding.py new file mode 100644 index 0000000..ee707ad --- /dev/null +++ b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/backend/src/model/embedding.py @@ -0,0 +1,48 @@ +# Copyright 2025 Google LLC +# +# 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 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +from google.cloud.bigquery import SchemaField +from pydantic import BaseModel +from typing import List, Optional + + +class Embedding(BaseModel): + id: str + text: str + index: str + author: str + timestamp: Optional[str] = None + + @classmethod + def __schema__(cls) -> List[SchemaField]: + return [ + SchemaField("id", "STRING", mode="REQUIRED"), + SchemaField("text", "STRING", mode="REQUIRED"), + SchemaField("index", "STRING", mode="REQUIRED"), + SchemaField("author", "STRING", mode="REQUIRED"), + SchemaField("timestamp", "TIMESTAMP", mode="REQUIRED"), + ] + + def to_dict(self): + return { + "id": self.id, + "text": self.text, + "index": self.index, + "author": self.author, + "timestamp": self.timestamp, + } + + def to_insert_string(self): + return f'"{self.id}", """{self.text}""", "{self.index}", "{self.author}", CURRENT_TIMESTAMP()' \ No newline at end of file diff --git a/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/backend/src/model/event.py b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/backend/src/model/event.py new file mode 100644 index 0000000..aff24fa --- /dev/null +++ b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/backend/src/model/event.py @@ -0,0 +1,27 @@ +# Copyright 2025 Google LLC +# +# 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 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from pydantic import BaseModel + + +class IntentCreateEvent(BaseModel): + + intent_name: str + index_endpoint_resource: str + + def to_dict(self): + return { + "intent_name": self.intent_name, + "index_endpoint_resource": self.index_endpoint_resource, + } \ No newline at end of file diff --git a/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/backend/src/model/http_status.py b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/backend/src/model/http_status.py new file mode 100644 index 0000000..3fcbac0 --- /dev/null +++ b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/backend/src/model/http_status.py @@ -0,0 +1,23 @@ +# Copyright 2025 Google LLC +# +# 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 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from fastapi import HTTPException + +class ResourceAlreadyExists(HTTPException): + def __init__(self, detail="Resource already exists"): + super().__init__(status_code=400, detail=detail) + +class BadRequest(HTTPException): + def __init__(self, detail="Resource already exists"): + super().__init__(status_code=400, detail=detail) \ No newline at end of file diff --git a/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/backend/src/model/intent.py b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/backend/src/model/intent.py new file mode 100644 index 0000000..d772e5c --- /dev/null +++ b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/backend/src/model/intent.py @@ -0,0 +1,116 @@ +# Copyright 2025 Google LLC +# +# 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 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from typing import List + +from google.cloud.bigquery import SchemaField, Row +from pydantic import BaseModel + + +class Intent(BaseModel): + name: str + ai_model: str + ai_temperature: float + description: str + prompt: str + questions: List[str] + status: str + gcp_bucket: str = "" + remote_agent_resource_id: str + + @classmethod + def __schema__(cls) -> List[SchemaField]: + return [ + SchemaField("name", "STRING", mode="REQUIRED"), + SchemaField("ai_model", "STRING", mode="REQUIRED"), + SchemaField("ai_temperature", "NUMERIC", mode="REQUIRED"), + SchemaField("description", "STRING", mode="REQUIRED"), + SchemaField("prompt", "STRING", mode="REQUIRED"), + SchemaField("questions", "STRING", mode="REPEATED"), + SchemaField("status", "STRING", mode="REQUIRED"), + SchemaField("gcp_bucket", "STRING", mode="REQUIRED"), + SchemaField("remote_agent_resource_id", "STRING"), + ] + + @classmethod + def __from_row__(cls, row: Row): + return cls( + name=row["name"], + ai_model=row["ai_model"], + ai_temperature=float(row["ai_temperature"]) if row["ai_temperature"] is not None else 0.0, # Ensure conversion from Decimal/Numeric + description=row["description"], + prompt=row["prompt"], + questions=list(row["questions"]) if row["questions"] is not None else [], # Ensure it's a list + status=row["status"], + gcp_bucket=row["gcp_bucket"], + remote_agent_resource_id=row["remote_agent_resource_id"], + ) + + def to_dict(self): + return { + "name": self.name, + "ai_model": self.ai_model, + "ai_temperature": self.ai_temperature, + "description": self.description, + "prompt": self.prompt, + "questions": self.questions, + "status": self.status, + "gcp_bucket": self.gcp_bucket, + "remote_agent_resource_id": self.remote_agent_resource_id, + } + + def to_insert_string(self): + return f'"{self.name}", "{self.ai_model}", {self.ai_temperature},"{self.description}","""{self.prompt}""", {str(self.questions)}, "{self.status}", "{self.gcp_bucket}", "{self.remote_agent_resource_id}"' + + def is_active(self) -> bool: + return self.status == "5" + + def get_standard_name(self) -> str: + return self.name.lower().replace(" ", "-").replace("_", "-") + + +class CreateIntentRequest(BaseModel): + name: str + gcp_bucket: str = "" + ai_model: str + ai_temperature: float + description: str + prompt: str + questions: List[str] + remote_agent_resource_id: str + + def to_dict(self): + return { + "name": self.name, + "gcp_bucket": self.gcp_bucket, + "ai_model": self.ai_model, + "ai_temperature": self.ai_temperature, + "description": self.description, + "prompt": self.prompt, + "questions": self.questions, + "remote_agent_resource_id": self.remote_agent_resource_id, + } + + def to_intent(self) -> Intent: + return Intent( + name=self.name, + ai_model=self.ai_model, + ai_temperature=self.ai_temperature, + description=self.description, + prompt=self.prompt, + questions=self.questions, + status="1", + gcp_bucket=self.gcp_bucket, + remote_agent_resource_id=self.remote_agent_resource_id, + ) diff --git a/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/backend/src/repository/__init__.py b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/backend/src/repository/__init__.py new file mode 100644 index 0000000..36a1e8d --- /dev/null +++ b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/backend/src/repository/__init__.py @@ -0,0 +1,14 @@ +# Copyright 2025 Google LLC +# +# 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 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + diff --git a/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/backend/src/repository/big_query.py b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/backend/src/repository/big_query.py new file mode 100644 index 0000000..a8086ea --- /dev/null +++ b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/backend/src/repository/big_query.py @@ -0,0 +1,325 @@ +# Copyright 2025 Google LLC +# +# 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 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Provides a repository class for interacting with Google BigQuery. + +This module defines the `BigQueryRepository` class, which encapsulates +common BigQuery operations like running queries, fetching rows by ID, +inserting, updating, and deleting rows. It relies on the 'BIG_QUERY_DATASET' +environment variable to target the correct dataset. +""" + +import datetime +from decimal import Decimal +from os import getenv +from typing import Dict, List, Any, Optional, Tuple +from google.cloud.bigquery import ( + Client, + QueryJobConfig, + ScalarQueryParameter, + ArrayQueryParameter, + TableReference, + DatasetReference +) +from google.cloud.bigquery.table import RowIterator +from google.cloud.exceptions import GoogleCloudError + +_BIG_QUERY_DATASET_ENV = getenv("BIG_QUERY_DATASET") +if not _BIG_QUERY_DATASET_ENV: + raise ValueError( + "The BIG_QUERY_DATASET environment variable is not set. " + "This is required for the BigQueryRepository to function." + ) +BIG_QUERY_DATASET: str = _BIG_QUERY_DATASET_ENV + +CHATS_TABLE = 'chats' +CHATS_ID_COLUMN = "id" + +EMBEDDINGS_TABLE = "embeddings" +EMBEDDINGS_ID_COLUMN = "id" +EMBEDDINGS_TEXT_COLUMN = "text" +EMBEDDINGS_INDEX_COLUMN = "index" + +class BigQueryRepository: + """ + A repository class for simplifying interactions with Google BigQuery, + using parameterized queries to prevent SQL injection. + """ + + def __init__(self): + """Initializes the BigQuery client and project ID.""" + self.client: Client = Client() + self.project_id: str = self.client.project + self.dataset_id: str = BIG_QUERY_DATASET + + def _get_table_ref(self, table_id: str) -> TableReference: + """Helper to get a TableReference object.""" + dataset_ref = DatasetReference(self.project_id, self.dataset_id) + return TableReference(dataset_ref, table_id) + + def _validate_column_name(self, column_name: str): + """Basic validation for column names.""" + if not column_name.replace("_", "").isalnum(): + raise ValueError(f"Invalid column name: {column_name}") + + def _create_scalar_query_parameter(self, name: str, value: Any) -> ScalarQueryParameter: + """ + Creates a ScalarQueryParameter by inferring the BigQuery type from the value. + """ + if isinstance(value, bool): + return ScalarQueryParameter(name, "BOOL", value) + if isinstance(value, int): + return ScalarQueryParameter(name, "INT64", value) + if isinstance(value, float): + return ScalarQueryParameter(name, "FLOAT64", value) + if isinstance(value, str): + return ScalarQueryParameter(name, "STRING", value) + if isinstance(value, datetime.datetime): + return ScalarQueryParameter(name, "TIMESTAMP", value) + if isinstance(value, datetime.date): + return ScalarQueryParameter(name, "DATE", value) + if isinstance(value, Decimal): + return ScalarQueryParameter(name, "NUMERIC", value) + if value is None: + # For None, the type of the column in BQ will ultimately determine how NULL is handled. + # Using STRING type for the parameter is a common safe default. + return ScalarQueryParameter(name, "STRING", None) # Default to STRING for None + + # Fallback: attempt to convert other types to string. + try: + str_value = str(value) + return ScalarQueryParameter(name, "STRING", str_value) + except Exception as e: + raise TypeError( + f"Value for param '{name}' of type {type(value)} " + f"could not be converted to a supported BigQuery scalar type: {e}" + ) + + def run_query(self, query: str, job_config: Optional[QueryJobConfig] = None) -> RowIterator: + """ + Executes a BigQuery SQL query and returns the results. + + Args: + query: The SQL query string to execute. + job_config: Optional QueryJobConfig for parameterized queries. + + Returns: + A RowIterator object to iterate over the query results. + + Raises: + google.cloud.exceptions.GoogleCloudError: If the query fails. + """ + try: + query_job = self.client.query(query, job_config=job_config) + return query_job.result() + except GoogleCloudError as e: + print(f"BigQuery Error: {e}") + print(f"Query: {query}") + if job_config and job_config.query_parameters: + print(f"Parameters: {job_config.query_parameters}") + raise + + def get_row_by_id(self, table_id: str, id_column: str, id_value: Any, id_value_type: str = "STRING") -> RowIterator: + """ + Retrieves a single row from a table based on its ID using parameterized query. + + Args: + table_id: The ID of the table (without dataset prefix). + id_column: The name of the column containing the ID. + id_value: The specific ID value to search for. + id_value_type: The BigQuery type of the ID value (e.g., "STRING", "INT64"). + + Returns: + A RowIterator containing the matching row(s). + """ + self._validate_column_name(id_column) + query = f""" + SELECT * FROM `{self.project_id}.{self.dataset_id}.{table_id}` + WHERE `{id_column}` = @id_value; + """ + job_config = QueryJobConfig( + query_parameters=[ + ScalarQueryParameter("id_value", id_value_type, id_value) + ] + ) + return self.run_query(query, job_config=job_config) + + def insert_rows_json(self, table_id: str, rows: List[Dict[str, Any]]) -> List[Dict]: + """ + Inserts multiple rows into the specified table using JSON. + This is the recommended method for insertions due to its safety and efficiency. + + Args: + table_id: The ID of the table. + rows: A list of dictionaries, where each dictionary represents a row + (column names as keys, values as row data). + + Returns: + A list of error dictionaries if any rows failed to insert, + otherwise an empty list. + """ + table_ref = self._get_table_ref(table_id) + errors_from_client = self.client.insert_rows_json(table_ref, rows) + errors_list: List[Dict[str, Any]] = list(errors_from_client) # type: ignore + if errors_list: + print(f"Errors inserting rows into {table_id}: {errors_list}") + return errors_list + + def insert_row(self, table_id: str, column_names: List[str], values: Tuple[Any, ...]) -> None: + """ + Inserts a new row into the specified table using a parameterized query. + Prefer `insert_rows_json` for multiple rows or complex data. + + Args: + table_id: The ID of the table. + column_names: A list of column names for the insert. + values: A tuple of values corresponding to the column_names. + + Raises: + ValueError: If column names are invalid or counts don't match. + TypeError: If a value in `values` cannot be mapped to a BigQuery scalar type. + """ + if not column_names or not values: + raise ValueError("Column names and values cannot be empty.") + if len(column_names) != len(values): + raise ValueError("Number of column names must match number of values.") + + for col_name in column_names: + self._validate_column_name(col_name) + + cols_str = ", ".join(f"`{col}`" for col in column_names) + placeholders = ", ".join([f"@param{i}" for i in range(len(values))]) + + query_params: List[ScalarQueryParameter] = [] + for i, current_value in enumerate(values): + param_name = f"param{i}" + query_params.append(self._create_scalar_query_parameter(param_name, current_value)) + + query = f""" + INSERT INTO `{self.project_id}.{self.dataset_id}.{table_id}` ({cols_str}) + VALUES ({placeholders}); + """ + job_config = QueryJobConfig(query_parameters=query_params) + self.run_query(query, job_config=job_config) + + def delete_multiple_rows_by_id( + self, table_id: str, id_column: str, ids: List[Any], id_value_type: str = "STRING" + ) -> None: + """ + Deletes multiple rows from a table based on a list of IDs using parameterized query. + + Args: + table_id: The ID of the table. + id_column: The name of the column containing the IDs. + ids: A list of ID values to delete. + id_value_type: The BigQuery type of the ID values. + """ + if not ids: + return # Nothing to delete + self._validate_column_name(id_column) + + query = f""" + DELETE FROM `{self.project_id}.{self.dataset_id}.{table_id}` + WHERE `{id_column}` IN UNNEST(@ids); + """ + job_config = QueryJobConfig( + query_parameters=[ + ArrayQueryParameter("ids", id_value_type, ids) + ] + ) + self.run_query(query, job_config=job_config) + + def update_row_by_id( + self, + table_id: str, + id_column: str, + id_value: Any, + column_values: Dict[str, Any], + id_value_type: str = "STRING", + ) -> None: + """ + Updates specific columns of a row identified by its ID using parameterized query. + + Args: + table_id: The ID of the table. + id_column: The name of the column containing the ID. + id_value: The specific ID value of the row to update. + column_values: A dictionary where keys are column names and + values are the new values. + id_value_type: The BigQuery type of the ID value. + """ + if not column_values: + return + self._validate_column_name(id_column) + + set_clauses = [] + query_params_list: List[ScalarQueryParameter] = [ + ScalarQueryParameter("id_value", id_value_type, id_value) + ] + + param_idx = 0 + for col, current_value in column_values.items(): + self._validate_column_name(col) + update_param_name = f"update_val_{param_idx}" + set_clauses.append(f"`{col}` = @{update_param_name}") + query_params_list.append(self._create_scalar_query_parameter(update_param_name, current_value)) + param_idx += 1 + + if not set_clauses: # Should not happen if column_values is not empty + return + + sets_str = ", ".join(set_clauses) + query = f""" + UPDATE `{self.project_id}.{self.dataset_id}.{table_id}` + SET {sets_str} + WHERE `{id_column}` = @id_value; + """ + job_config = QueryJobConfig(query_parameters=query_params_list) + self.run_query(query, job_config=job_config) + + def get_all_rows(self, table_id: str) -> RowIterator: + """ + Retrieves all rows from the specified table. + + Args: + table_id: The ID of the table. + + Returns: + A RowIterator containing all rows in the table. + """ + query = f"SELECT * FROM `{self.project_id}.{self.dataset_id}.{table_id}`" + return self.run_query(query) + + def delete_row_by_id(self, table_id: str, id_column: str, id_value: Any, id_value_type: str = "STRING") -> None: + """ + Deletes a single row from a table based on its ID using parameterized query. + + Args: + table_id: The ID of the table. + id_column: The name of the column containing the ID. + id_value: The specific ID value of the row to delete. + id_value_type: The BigQuery type of the ID value. + """ + self._validate_column_name(id_column) + query = f""" + DELETE FROM `{self.project_id}.{self.dataset_id}.{table_id}` + WHERE `{id_column}` = @id_value; + """ + job_config = QueryJobConfig( + query_parameters=[ + ScalarQueryParameter("id_value", id_value_type, id_value) + ] + ) + self.run_query(query, job_config=job_config) diff --git a/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/backend/src/repository/cloud_storage.py b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/backend/src/repository/cloud_storage.py new file mode 100644 index 0000000..2b975b2 --- /dev/null +++ b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/backend/src/repository/cloud_storage.py @@ -0,0 +1,34 @@ +# Copyright 2025 Google LLC +# +# 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 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from typing import List +from google.cloud.storage import Client, Blob + +BUCKET="quick-bot" + +INTENT_FOLDER="intents" +EMBEDDINGS_FILE="embeddings.json" +EMBEDDINGS_FOLDER="embeddings" + +CONTENT_TYPE="text/plain" + +class CloudStorageRepository: + + def __init__(self): + self.client = Client() + + def list(self, full_path: str) -> List[Blob]: + bucket = full_path.split("/")[2] + prefix = full_path.replace(f"gs://{bucket}/", "") + return list(self.client.list_blobs(bucket, prefix=prefix)) \ No newline at end of file diff --git a/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/backend/src/repository/task.py b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/backend/src/repository/task.py new file mode 100644 index 0000000..2932e6a --- /dev/null +++ b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/backend/src/repository/task.py @@ -0,0 +1,56 @@ +# Copyright 2025 Google LLC +# +# 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 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from google.cloud.tasks_v2 import CloudTasksClient, HttpMethod +import google.auth +from src.model.event import IntentCreateEvent +from json import dumps +from os import getenv + +LOCATION="us-central1" +INTENT_CREATION_QUEUE=getenv("TASK_QUEUE_NAME") +URL=getenv("FUNCTION_URL", "") + +def get_project_id(): + try: + _, project_id = google.auth.default() + return project_id + except google.auth.exceptions.DefaultCredentialsError as e: + print(f"Error: {e}") + return None + +class TaskRepository(): + + def __init__(self): + self.client = CloudTasksClient() + + def create(self, event: IntentCreateEvent): + # Construct the fully qualified queue name. + project_id = get_project_id() + parent = self.client.queue_path(project_id, LOCATION, INTENT_CREATION_QUEUE) + + task = { + "http_request": { + "http_method": HttpMethod.POST, + "url": URL, + "headers": {"Content-type": "application/json"}, + } + } + + converted_payload = dumps(event.to_dict()).encode() + task["http_request"]["body"] = converted_payload + response = self.client.create_task(request={"parent": parent, "task": task}) + + print("Created task {}".format(response.name)) + return response diff --git a/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/backend/src/service/__init__.py b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/backend/src/service/__init__.py new file mode 100644 index 0000000..36a1e8d --- /dev/null +++ b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/backend/src/service/__init__.py @@ -0,0 +1,14 @@ +# Copyright 2025 Google LLC +# +# 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 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + diff --git a/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/backend/src/service/chats.py b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/backend/src/service/chats.py new file mode 100644 index 0000000..3ff0186 --- /dev/null +++ b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/backend/src/service/chats.py @@ -0,0 +1,43 @@ +# Copyright 2025 Google LLC +# +# 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 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from src.repository.big_query import BigQueryRepository, CHATS_TABLE +from src.model.chats import Chat +import datetime + +class ChatsService: + + def __init__(self): + self.repository = BigQueryRepository() + + def insert_chat(self, chat: Chat): + schema_fields = Chat.__schema__() + column_names = [field.name for field in schema_fields] + + # Prepare values as a tuple, matching the order of schema_fields + # If chat.timestamp is None, we'll insert the current UTC time. + # BigQueryRepository will handle the Python datetime object. + timestamp_value = chat.timestamp if chat.timestamp else datetime.datetime.now(datetime.timezone.utc) + + # Ensure the order here matches the order in Chat.__schema__ + values_tuple = ( + chat.id, + chat.question, + chat.answer, + chat.intent, + chat.suggested_questions, + timestamp_value + ) + + self.repository.insert_row(CHATS_TABLE, column_names, values_tuple) diff --git a/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/backend/src/service/embeddings.py b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/backend/src/service/embeddings.py new file mode 100644 index 0000000..f311c3a --- /dev/null +++ b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/backend/src/service/embeddings.py @@ -0,0 +1,40 @@ +# Copyright 2025 Google LLC +# +# 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 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from src.model.embedding import Embedding +from src.repository.big_query import BigQueryRepository +from typing import List + +EMBEDDINGS_TABLE = "embeddings" +INTENTS_TABLE_ID_COLUMN = "id" + +class EmbeddingService: + + def __init__(self): + self.repository = BigQueryRepository() + + def create(self, embedding: Embedding) -> Embedding: + schema_fields = Embedding.__schema__() + column_names = [field.name for field in schema_fields] + + # Prepare values as a tuple, matching the order of schema_fields + # Ensure the order here matches the order in SearchApplication.__schema__ + values_tuple = (embedding.id, embedding.text, embedding.index, embedding.author) + self.repository.insert_row(EMBEDDINGS_TABLE, column_names, values_tuple) + + return embedding + + def create_all(self, embeddings: List[Embedding]): + for embedding in embeddings: + self.create(embedding) diff --git a/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/backend/src/service/index_endpoint.py b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/backend/src/service/index_endpoint.py new file mode 100644 index 0000000..08c533d --- /dev/null +++ b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/backend/src/service/index_endpoint.py @@ -0,0 +1,60 @@ +# Copyright 2025 Google LLC +# +# 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 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from typing import List +from google.cloud.aiplatform import MatchingEngineIndexEndpoint +from src.repository.big_query import BigQueryRepository +from src.service.vertex_ai import EMBEDDINGS_MODEL + +BIG_QUERY_ID_COLUMN = "id" +BIG_QUERY_INDEX_COLUMN = "index" +BIG_QUERY_TEXT_COLUMN = "text" +BIG_QUERY_AUTHOR_COLUMN = "author" +BIG_QUERY_TIMESTAMP_COLUMN = "timestamp" + +INDEX_DIMENSIONS=768 +INDEX_DISTANCE_MEASURE='DOT_PRODUCT_DISTANCE' +INDEX_NEIGHBORS_COUNT=150 + +class IndexEndpointService: + + def __init__(self): + self.bq_repository = BigQueryRepository() + + def get_endpoint(self, name: str) -> MatchingEngineIndexEndpoint: + index_endpoints = MatchingEngineIndexEndpoint.list( + filter=f'display_name="{name}"', + ) + + if index_endpoints: + return index_endpoints[0] + else: + raise Exception(f"Matching Engine Index Endpoint with name {name} doesn't exist") + + def create_endpoint(self, name: str) -> MatchingEngineIndexEndpoint: + return MatchingEngineIndexEndpoint.create( + display_name=name, + description=name, + public_endpoint_enabled=True, + ) + + def delete_endpoint(self, endpoint: MatchingEngineIndexEndpoint) -> MatchingEngineIndexEndpoint: + MatchingEngineIndexEndpoint.delete(endpoint, force=True) + + def create_embeddings(self, chunk: str) -> List[float]: + return EMBEDDINGS_MODEL.embed_query(chunk) + + def endpoint_has_deployed_indexes(self, name: str) -> bool: + index_endpoint = self.get_endpoint(name) + return len(index_endpoint.deployed_indexes) > 0 \ No newline at end of file diff --git a/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/backend/src/service/intent.py b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/backend/src/service/intent.py new file mode 100644 index 0000000..3c93e52 --- /dev/null +++ b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/backend/src/service/intent.py @@ -0,0 +1,109 @@ +# Copyright 2025 Google LLC +# +# 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 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from src.model.http_status import BadRequest +from src.model.intent import Intent +from src.repository.big_query import BigQueryRepository +from src.repository.cloud_storage import CloudStorageRepository +from typing import List + +INTENTS_TABLE = "intents" +INTENTS_TABLE_ID_COLUMN = "name" + +class IntentService: + + def __init__(self): + self.repository = BigQueryRepository() + self.gcs_repository = CloudStorageRepository() + + def get(self, intent_name: str) -> Intent | None: + """ + Retrieves an intent by its name using a parameterized query. + """ + intent = None + # Use get_row_by_id for safer, parameterized query + results = self.repository.get_row_by_id( + table_id=INTENTS_TABLE, + id_column=INTENTS_TABLE_ID_COLUMN, + id_value=intent_name, + id_value_type="STRING" + ) + # get_row_by_id returns a RowIterator. Expect 0 or 1 row for a unique ID. + row_list = list(results) + if row_list: + intent = Intent.__from_row__(row_list[0]) + return intent + + def get_all(self) -> List[Intent]: + intents = [] + results = self.repository.get_all_rows(INTENTS_TABLE) + for row in list(results): + intent = Intent.__from_row__(row) + intents.append(intent) + return intents + + def create(self, intent: Intent) -> Intent: + """ + Creates a new intent. + Uses the new insert_row method which takes column names and a tuple of values. + """ + if self.get(intent.name): + raise BadRequest(detail=f"Intent with name {intent.name} already exists") + if intent.gcp_bucket and not self.gcs_repository.list(intent.gcp_bucket): + raise BadRequest(detail=f"No data found on {intent.gcp_bucket}, please, add your pdf files in the proper location.") + if not intent.gcp_bucket: + intent.status = "5" # Mark as active if no bucket (no indexing needed) + + intent_data = intent.to_dict() + column_names = list(intent_data.keys()) + values_tuple = tuple(intent_data.values()) + + # Call the updated insert_row method + self.repository.insert_row(INTENTS_TABLE, column_names, values_tuple) + return intent + + def update(self, intent_name: str, intent: Intent) -> None: + """ + Updates an existing intent identified by intent_name. + The 'intent' object contains the new values. + Uses the new update_row_by_id which takes a dictionary of raw column values. + """ + # These are the fields intended to be updatable based on the previous implementation. + # Ensure that the 'intent' Pydantic model instance (`intent`) provides these attributes. + column_values_to_update = { + 'ai_model': intent.ai_model, + 'ai_temperature': intent.ai_temperature, + 'description': intent.description, + 'prompt': intent.prompt, + 'questions': intent.questions, + 'status': intent.status, + 'gcp_bucket': intent.gcp_bucket, + 'remote_agent_resource_id': intent.remote_agent_resource_id, + } + + self.repository.update_row_by_id( + table_id=INTENTS_TABLE, + id_column=INTENTS_TABLE_ID_COLUMN, + id_value=intent_name, + column_values=column_values_to_update + ) + + def delete(self, intent_name: str) -> None: + self.repository.delete_row_by_id( + table_id=INTENTS_TABLE, + id_column=INTENTS_TABLE_ID_COLUMN, + id_value=intent_name, + id_value_type="STRING" + ) + diff --git a/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/backend/src/service/intent_matching.py b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/backend/src/service/intent_matching.py new file mode 100644 index 0000000..96dba96 --- /dev/null +++ b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/backend/src/service/intent_matching.py @@ -0,0 +1,56 @@ +# Copyright 2025 Google LLC +# +# 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 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from numpy import argsort +from langchain.utils.math import cosine_similarity +from src.model.intent import Intent +from src.service.vertex_ai import EMBEDDINGS_MODEL +from typing import List + + +class IntentMatchingService: + + def __init__(self, intents: List[Intent]): + self.intents_map = {} + self.questions_embeddings = {} + + for intent in intents: + self.intents_map[intent.name] = intent + self.questions_embeddings[intent.name] = EMBEDDINGS_MODEL.embed_documents( + intent.questions + ) + + def get_intent_from_query(self, query: str) -> Intent: + # query_embeddings = EMBEDDINGS_MODEL.embed_query(query) + # m = 0 + # intent = None + return self.intents_map["Travel concierge"] + + # for intent_name, questions in self.questions_embeddings.items(): + # similarity = max(cosine_similarity([query_embeddings], questions)[0]) + # if m < similarity: + # m = similarity + # intent = self.intents_map[intent_name] + # return intent + + def get_suggested_questions(self, query: str, intent: Intent) -> List[str]: + questions = intent.questions + query_embeddings = EMBEDDINGS_MODEL.embed_query(query) + similarity = cosine_similarity( + [query_embeddings], self.questions_embeddings[intent.name] + )[0] + suggested_questions = [] + for ix in argsort(similarity)[-3:][::-1]: + suggested_questions.append(questions[ix]) + return suggested_questions[1:] diff --git a/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/backend/src/service/models.py b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/backend/src/service/models.py new file mode 100644 index 0000000..7ce6831 --- /dev/null +++ b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/backend/src/service/models.py @@ -0,0 +1,38 @@ +# Copyright 2025 Google LLC +# +# 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 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from google.cloud.aiplatform import Model + +GOOGLE_AI_MODELS = [ + "gemini-1.5-flash", + "gemini-1.5-flash-8b", + "gemini-1.5-pro", + "gemini-1.0-pro", + +] + +class ModelService: + + def get_all(self): + custom_models = Model.list() + return [ + { + "name": "Gemini", + "models": GOOGLE_AI_MODELS, + }, + { + "name": "Custom", + "models": [cm.display_name for cm in custom_models], + } + ] \ No newline at end of file diff --git a/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/backend/src/service/vertex_ai.py b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/backend/src/service/vertex_ai.py new file mode 100644 index 0000000..0c1846e --- /dev/null +++ b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/backend/src/service/vertex_ai.py @@ -0,0 +1,224 @@ +# Copyright 2025 Google LLC +# +# 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 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from langchain_google_vertexai import VertexAIEmbeddings +from vertexai.preview.generative_models import GenerativeModel +from google.cloud import bigquery +from google.cloud.aiplatform import MatchingEngineIndexEndpoint, MatchingEngineIndex +from typing import List, MutableSequence +from src.model.intent import Intent +from src.repository.big_query import BIG_QUERY_DATASET, EMBEDDINGS_TABLE +from google.cloud.aiplatform_v1 import ( + FindNeighborsRequest, + IndexDatapoint, + MatchServiceClient, + FindNeighborsResponse, +) + +from src.utils.utils import generate_hash, intents_to_json + +MATCHING_ENGINE_INDEX_NEIGHBORS = 5 + +TEXT_EMBEDDING_MODEL = "text-multilingual-embedding-002" + +EMBEDDINGS_MODEL = VertexAIEmbeddings(TEXT_EMBEDDING_MODEL) + +INTENTS_HASH = "" +INDEX_ENDPOINTS = {} +INDEX_ENDPOINTS_CLIENTS = {} + + +class VertexAIService: + + def __init__(self, intents: List[Intent]): + global INTENTS_HASH + self.client = bigquery.Client() + + new_hash = generate_hash(intents_to_json(intents)) + + if INTENTS_HASH != new_hash: + for intent in intents: + if not intent.gcp_bucket: + continue + ixs = MatchingEngineIndexEndpoint.list( + filter=f'display_name="{intent.get_standard_name()}"', + ) + if len(ixs) == 0: + print( + f"Intent {intent.name} is not yet initialized. Please run load_indexes.py to create an index for the intent" + ) + continue + INDEX_ENDPOINTS[intent.get_standard_name()] = ixs[0] + # Configure Vector Search client + INDEX_ENDPOINTS_CLIENTS[intent.get_standard_name()] = ( + MatchServiceClient( + client_options={ + "api_endpoint": ixs[0].public_endpoint_domain_name, + }, + ) + ) + INTENTS_HASH = new_hash + + def vector_search_query( + self, + index_endpoint: MatchingEngineIndexEndpoint, + match_service_client: MatchServiceClient, + question: str, + ) -> MutableSequence[FindNeighborsResponse.Neighbor]: + # Build FindNeighborsRequest object + datapoint = IndexDatapoint( + feature_vector=EMBEDDINGS_MODEL.embed_query(question) + ) + + query = FindNeighborsRequest.Query(datapoint=datapoint, neighbor_count=3) + + return ( + match_service_client.find_neighbors( + FindNeighborsRequest( + index_endpoint=index_endpoint.resource_name, + deployed_index_id=index_endpoint.deployed_indexes[0].id, + # Request can have multiple queries + queries=[query], + return_full_datapoint=False, + ) + ) + .nearest_neighbors[0] + .neighbors + ) + + def get_index_name(self, index_endpoint: MatchingEngineIndexEndpoint) -> str: + return MatchingEngineIndex( + index_endpoint.deployed_indexes[0].index + ).display_name + + def get_text_results_from_bigquery(self, chunk_ids: List[str], index_name: str): + if not chunk_ids: + return [] + query = f""" + SELECT text, id + FROM `{BIG_QUERY_DATASET}.{EMBEDDINGS_TABLE}` + WHERE id IN {str(chunk_ids).replace("[", "(").replace("]", ")")} AND index = "{index_name}"; + """ + + rows = self.client.query(query).result() + texts = [] + + for row in rows: + texts.append(row[0]) + return texts + + def generate_llm_response( + self, + model: GenerativeModel, + prompt: str, + chunked_context, + question: str, + temperature: float, + ) -> str: + + response_list = [] + context = "" + for ix, data in enumerate(chunked_context): + context += f"Context {ix + 1}: {data} \n" + formatted_prompt = f"{prompt} \n {context} Question: {question}" + + responses = model.generate_content( + formatted_prompt, + generation_config={ + "temperature": temperature, + "max_output_tokens": 8192, + "top_p": 0.95, + }, + safety_settings=[], + stream=True, + ) + for response in responses: + if response.candidates[0].content.parts: + response_list.append(response.text) + + return "".join(response_list) + + def generate_out_of_context_response( + self, model: GenerativeModel, question: str + ) -> str: + response_list = [] + prompt = f""" + You are a friendly conversational bot called. Whenever you are asked a question follow this instructions. + 1. Reply that you don't have an answer for the following question given your knowledge and the context provided in the question. + 2. Ask the user to provide feedback via de UI or to try and rephrase the question. + 3. Apologize and state that you will keep on learning in the future. + Question: {question} + """ + + responses = model.generate_content( + prompt, + generation_config={ + "temperature": 1, + "max_output_tokens": 8192, + "top_p": 0.95, + }, + safety_settings=[], + stream=True, + ) + for response in responses: + if response.candidates[0].content.parts: + response_list.append(response.text) + + return "".join(response_list) + + def generate_text_from_model( + self, + query: str, + intent: Intent, + ) -> str: + """ + Given a user query, and an inferred intent + we make a similarity search to VectorSearch, + retrieve the chunked text from bigquery and + finaly generate an LLM response out of it + @param query: The user's query + @param intent: The user's inferred intent + @return LLM response: The LLM generated response + """ + model = GenerativeModel(intent.ai_model) + if intent.gcp_bucket: + index_endpoint = INDEX_ENDPOINTS[intent.get_standard_name()] + match_engine_client = INDEX_ENDPOINTS_CLIENTS[intent.get_standard_name()] + similarity_results = self.vector_search_query( + index_endpoint, + match_engine_client, + query, + ) + if similarity_results: + context = self.get_text_results_from_bigquery( + [res.datapoint.datapoint_id for res in similarity_results], + self.get_index_name(index_endpoint), + ) + return self.generate_llm_response( + model, + intent.prompt, + context, + query, + intent.ai_temperature, + ) + else: + return self.generate_out_of_context_response(model, query) + else: + return self.generate_llm_response( + model, + intent.prompt, + [], + query, + 1, + ) diff --git a/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/backend/src/utils/utils.py b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/backend/src/utils/utils.py new file mode 100644 index 0000000..c23ef9c --- /dev/null +++ b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/backend/src/utils/utils.py @@ -0,0 +1,29 @@ +# Copyright 2025 Google LLC +# +# 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 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from hashlib import sha256 +from json import dumps +from src.model.intent import Intent +from typing import List + + +def intents_to_json(intents: List[Intent]): + """Converts BigQuery results to a JSON string.""" + dictionaries = [intent.to_dict() for intent in intents] + return dumps(dictionaries, sort_keys=True) + +def generate_hash(data): + """Generates a SHA-256 hash of the input data.""" + hash_object = sha256(data.encode()) + return hash_object.hexdigest() \ No newline at end of file diff --git a/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/backend/test_ae.py b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/backend/test_ae.py new file mode 100644 index 0000000..1dc16c6 --- /dev/null +++ b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/backend/test_ae.py @@ -0,0 +1,47 @@ +# Copyright 2025 Google LLC +# +# 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 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from vertexai import agent_engines +from vertexai.preview.reasoning_engines.templates.adk import AdkApp + +from travel_concierge.agent import root_agent + + +message = "Plan for me a week-long trip to greece from poland for a family with 2 kids, aged 9 and 2. start date is May 4th 2025" +remote_agent_resource_id = ( + "projects/882302757206/locations/us-central1/reasoningEngines/6694680010923442176" +) + +# [print(agent) for agent in agent_engines.list()] +remote_agent = agent_engines.get(remote_agent_resource_id) +# local_agent = root_agent +# remote_agent = AdkApp(agent=local_agent) +session = remote_agent.create_session(user_id="traveler0115") +print(f'Trying remote agent with session: {session["id"]}') +print(remote_agent.list_sessions(user_id="traveler0115")) + +answer = [] +for event in remote_agent.stream_query( + user_id="traveler0115", + session_id=session["id"], + message=message, +): + content = event.get("content") + if content: + for part in event["content"]["parts"]: + if part.get("text"): + answer.append(part["text"]) + +model_response = " ".join(answer) +print(f"model response is: {model_response}") diff --git a/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/docker-compose.yml b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/docker-compose.yml new file mode 100644 index 0000000..c8946ba --- /dev/null +++ b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/docker-compose.yml @@ -0,0 +1,64 @@ +# Copyright 2025 Google LLC +# +# 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 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +services: + backend: + build: + secrets: + - gcp_credentials + context: ./backend + dockerfile: Dockerfile.local + args: + IS_FIRST_DEPLOYMENT: "True" + _PROJECT_ID: my-project-id + _REGION: us-central1 + ENVIRONMENT: development + FRONTEND_URL: http://localhost:4200 + BIG_QUERY_DATASET: bigquery_quickbot_travel_concierge + _CHATBOT_NAME: my_quickbot_travel_concierge + _ADK_GOOGLE_PLACES_API_KEY: your-google-maps-places-apikey + container_name: quickbot-multiagent-travel-concierge-backend + ports: + - "8080:8080" + volumes: + - ./backend:/app + # Mount the gcloud ADC directory. Replace with the correct path for your OS if different. :ro - read-only for better security + - ~/.config/gcloud/:/root/.config/gcloud:ro # Linux/macOS example: + # - %APPDATA%/gcloud:/root/.config/gcloud:ro # Windows example: + environment: + GOOGLE_APPLICATION_CREDENTIALS: "/root/.config/gcloud/application_default_credentials.json" + restart: unless-stopped + + frontend: + build: + context: ./frontend + dockerfile: Dockerfile + container_name: quickbot-multiagent-travel-concierge-frontend + ports: + - "4200:8080" + volumes: + - ./frontend:/app + # Use an anonymous volume for node_modules to prevent host node_modules + # from interfering and potentially speed up builds/rebuilds + - /app/node_modules + # To change the env variables in the frontend, change the environment.ts file + restart: unless-stopped + +secrets: + gcp_credentials: # This ID must match the one used in services.backend.build.secrets + # Path to your Google Application Default Credentials JSON file on the HOST machine. + # Replace with the correct path for your OS if different. + file: ~/.config/gcloud/application_default_credentials.json + # Windows example (you might need to adjust the target path inside the container if it's a Linux container): + # - %APPDATA%/gcloud/application_default_credentials.json \ No newline at end of file diff --git a/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/.dockerignore b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/.dockerignore new file mode 100644 index 0000000..7109f56 --- /dev/null +++ b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/.dockerignore @@ -0,0 +1,4 @@ +.angular +.vscode +dist +node_modules \ No newline at end of file diff --git a/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/.editorconfig b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/.editorconfig new file mode 100644 index 0000000..59d9a3a --- /dev/null +++ b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/.editorconfig @@ -0,0 +1,16 @@ +# Editor configuration, see https://editorconfig.org +root = true + +[*] +charset = utf-8 +indent_style = space +indent_size = 2 +insert_final_newline = true +trim_trailing_whitespace = true + +[*.ts] +quote_type = single + +[*.md] +max_line_length = off +trim_trailing_whitespace = false diff --git a/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/.gitignore b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/.gitignore new file mode 100644 index 0000000..0711527 --- /dev/null +++ b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/.gitignore @@ -0,0 +1,42 @@ +# See http://help.github.com/ignore-files/ for more about ignoring files. + +# Compiled output +/dist +/tmp +/out-tsc +/bazel-out + +# Node +/node_modules +npm-debug.log +yarn-error.log + +# IDEs and editors +.idea/ +.project +.classpath +.c9/ +*.launch +.settings/ +*.sublime-workspace + +# Visual Studio Code +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +.history/* + +# Miscellaneous +/.angular/cache +.sass-cache/ +/connect.lock +/coverage +/libpeerconnection.log +testem.log +/typings + +# System files +.DS_Store +Thumbs.db diff --git a/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/Dockerfile b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/Dockerfile new file mode 100644 index 0000000..a58c895 --- /dev/null +++ b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/Dockerfile @@ -0,0 +1,27 @@ +# Copyright 2025 Google LLC +# +# 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 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +FROM node:18.17.1 as build + +RUN mkdir -p /code +WORKDIR /code +COPY . /code/ +RUN npm ci +RUN npm run build:prod + +FROM nginx:alpine +COPY --from=build /code/dist/quick-bot-app-frontend /usr/share/nginx/html +COPY --from=build /code/nginx.conf /etc/nginx/conf.d/ + +EXPOSE 8080 \ No newline at end of file diff --git a/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/README.md b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/README.md new file mode 100644 index 0000000..f9ad9d6 --- /dev/null +++ b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/README.md @@ -0,0 +1,27 @@ +# QuickBotAppFrontend + +This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 15.1.3. + +## Development server + +Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The application will automatically reload if you change any of the source files. + +## Code scaffolding + +Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`. + +## Build + +Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. + +## Running unit tests + +Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io). + +## Running end-to-end tests + +Run `ng e2e` to execute the end-to-end tests via a platform of your choice. To use this command, you need to first add a package that implements end-to-end testing capabilities. + +## Further help + +To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.io/cli) page. diff --git a/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/angular.json b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/angular.json new file mode 100644 index 0000000..15d3e38 --- /dev/null +++ b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/angular.json @@ -0,0 +1,123 @@ +{ + "$schema": "./node_modules/@angular/cli/lib/config/schema.json", + "version": 1, + "newProjectRoot": "projects", + "projects": { + "quick-bot-app-frontend": { + "projectType": "application", + "schematics": { + "@schematics/angular:component": { + "style": "scss" + } + }, + "root": "", + "sourceRoot": "src", + "prefix": "app", + "architect": { + "build": { + "builder": "@angular-devkit/build-angular:browser", + "options": { + "outputPath": "dist/quick-bot-app-frontend", + "index": "src/index.html", + "main": "src/main.ts", + "polyfills": [ + "zone.js" + ], + "tsConfig": "tsconfig.app.json", + "inlineStyleLanguage": "scss", + "assets": [ + "src/favicon.ico", + "src/assets" + ], + "styles": [ + "@angular/material/prebuilt-themes/indigo-pink.css", + "src/styles.scss", + "node_modules/prismjs/themes/prism-okaidia.css" + ], + "scripts": [ + "node_modules/marked/marked.min.js", + "node_modules/prismjs/prism.js", + "node_modules/prismjs/components/prism-csharp.min.js", + "node_modules/prismjs/components/prism-css.min.js", + "node_modules/clipboard/dist/clipboard.min.js" + ] + }, + "configurations": { + "production": { + "budgets": [ + { + "type": "initial", + "maximumWarning": "4mb", + "maximumError": "10mb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "20kb", + "maximumError": "50kb" + } + ], + "outputHashing": "all" + }, + "development": { + "buildOptimizer": false, + "optimization": false, + "vendorChunk": true, + "extractLicenses": false, + "sourceMap": true, + "namedChunks": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "builder": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "browserTarget": "quick-bot-app-frontend:build:production" + }, + "development": { + "browserTarget": "quick-bot-app-frontend:build:development" + } + }, + "defaultConfiguration": "development" + }, + "extract-i18n": { + "builder": "@angular-devkit/build-angular:extract-i18n", + "options": { + "browserTarget": "quick-bot-app-frontend:build" + } + }, + "test": { + "builder": "@angular-devkit/build-angular:karma", + "options": { + "polyfills": [ + "zone.js", + "zone.js/testing" + ], + "tsConfig": "tsconfig.spec.json", + "inlineStyleLanguage": "scss", + "assets": [ + "src/favicon.ico", + "src/assets" + ], + "styles": [ + "@angular/material/prebuilt-themes/indigo-pink.css", + "src/styles.scss", + "node_modules/prismjs/themes/prism-okaidia.css" + ], + "scripts": [ + "node_modules/marked/marked.min.js", + "node_modules/prismjs/prism.js", + "node_modules/prismjs/components/prism-csharp.min.js", + "node_modules/prismjs/components/prism-css.min.js", + "node_modules/clipboard/dist/clipboard.min.js" + ] + } + } + } + } + }, + "cli": { + "analytics": false + } +} diff --git a/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/nginx.conf b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/nginx.conf new file mode 100644 index 0000000..6c8cce4 --- /dev/null +++ b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/nginx.conf @@ -0,0 +1,11 @@ +server { + listen 8080; + + root /usr/share/nginx/html; + index index.html index.htm; + include /etc/nginx/mime.types; + + location / { + try_files $uri $uri/ /index.html; + } +} \ No newline at end of file diff --git a/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/package-lock.json b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/package-lock.json new file mode 100644 index 0000000..0741365 --- /dev/null +++ b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/package-lock.json @@ -0,0 +1,22704 @@ +{ + "name": "quick-bot-app-frontend", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "quick-bot-app-frontend", + "version": "0.0.0", + "hasInstallScript": true, + "dependencies": { + "@angular/animations": "^15.1.0", + "@angular/cdk": "^15.2.9", + "@angular/common": "^15.1.0", + "@angular/compiler": "^15.1.0", + "@angular/core": "^15.1.0", + "@angular/fire": "7.5", + "@angular/flex-layout": "^15.0.0-beta.42", + "@angular/forms": "^15.1.0", + "@angular/material": "^15.2.9", + "@angular/platform-browser": "^15.1.0", + "@angular/platform-browser-dynamic": "^15.1.0", + "@angular/router": "^15.1.0", + "@fortawesome/fontawesome-free": "^6.6.0", + "@ionic/angular": "^7.5.4", + "@ng-idle/core": "^14.0.0", + "clipboard": "^2.0.11", + "ionic-emoji-rating": "^1.0.15", + "marked": "^4.3.0", + "moment": "^2.30.1", + "ngx-markdown": "^15.1.2", + "prismjs": "^1.29.0", + "rxfire": "6.0.3", + "rxjs": "~7.8.0", + "tslib": "^2.3.0", + "uuid": "^9.0.1", + "zone.js": "~0.12.0" + }, + "devDependencies": { + "@angular-devkit/build-angular": "^15.1.3", + "@angular/cli": "~15.1.3", + "@angular/compiler-cli": "^15.1.0", + "@types/jasmine": "~4.3.0", + "@types/marked": "^4.3.0", + "@types/uuid": "^9.0.4", + "firebase-tools": "^11.30.0", + "jasmine-core": "~4.5.0", + "karma": "~6.4.0", + "karma-chrome-launcher": "~3.1.0", + "karma-coverage": "~2.2.0", + "karma-jasmine": "~5.1.0", + "karma-jasmine-html-reporter": "~2.0.0", + "typescript": "~4.9.4" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.0.tgz", + "integrity": "sha512-qRmjj8nj9qmLTQXXmaR1cck3UXSRMPrbsLJAasZpF+t3riI71BXed5ebIOYwQntykeZuhjsdweEc9BxH5Jc26w==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.1.0", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@angular-devkit/architect": { + "version": "0.1502.11", + "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1502.11.tgz", + "integrity": "sha512-+hkG5UjIaKMRdo6SFLNQs+Cv7yAVeN8ijfDwI2z/mp7/otowuSEy+H3Tii195jfJ8TQ+y1B7svnx2D6O7oOYbQ==", + "dev": true, + "dependencies": { + "@angular-devkit/core": "15.2.11", + "rxjs": "6.6.7" + }, + "engines": { + "node": "^14.20.0 || ^16.13.0 || >=18.10.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@angular-devkit/architect/node_modules/rxjs": { + "version": "6.6.7", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", + "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", + "dev": true, + "dependencies": { + "tslib": "^1.9.0" + }, + "engines": { + "npm": ">=2.0.0" + } + }, + "node_modules/@angular-devkit/architect/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + }, + "node_modules/@angular-devkit/build-angular": { + "version": "15.2.11", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-angular/-/build-angular-15.2.11.tgz", + "integrity": "sha512-MnpVCJdk5jHuK7CH/cTcRT0JQkkKkRTEV3WTyOUhTm0O3PlKwvTM6/Sner+zyuhKyw5VFBBMypHh59aTUDEZ1A==", + "dev": true, + "dependencies": { + "@ampproject/remapping": "2.2.0", + "@angular-devkit/architect": "0.1502.11", + "@angular-devkit/build-webpack": "0.1502.11", + "@angular-devkit/core": "15.2.11", + "@babel/core": "7.20.12", + "@babel/generator": "7.20.14", + "@babel/helper-annotate-as-pure": "7.18.6", + "@babel/helper-split-export-declaration": "7.18.6", + "@babel/plugin-proposal-async-generator-functions": "7.20.7", + "@babel/plugin-transform-async-to-generator": "7.20.7", + "@babel/plugin-transform-runtime": "7.19.6", + "@babel/preset-env": "7.20.2", + "@babel/runtime": "7.20.13", + "@babel/template": "7.20.7", + "@discoveryjs/json-ext": "0.5.7", + "@ngtools/webpack": "15.2.11", + "ansi-colors": "4.1.3", + "autoprefixer": "10.4.13", + "babel-loader": "9.1.2", + "babel-plugin-istanbul": "6.1.1", + "browserslist": "4.21.5", + "cacache": "17.0.4", + "chokidar": "3.5.3", + "copy-webpack-plugin": "11.0.0", + "critters": "0.0.16", + "css-loader": "6.7.3", + "esbuild-wasm": "0.17.8", + "glob": "8.1.0", + "https-proxy-agent": "5.0.1", + "inquirer": "8.2.4", + "jsonc-parser": "3.2.0", + "karma-source-map-support": "1.4.0", + "less": "4.1.3", + "less-loader": "11.1.0", + "license-webpack-plugin": "4.0.2", + "loader-utils": "3.2.1", + "magic-string": "0.29.0", + "mini-css-extract-plugin": "2.7.2", + "open": "8.4.1", + "ora": "5.4.1", + "parse5-html-rewriting-stream": "7.0.0", + "piscina": "3.2.0", + "postcss": "8.4.31", + "postcss-loader": "7.0.2", + "resolve-url-loader": "5.0.0", + "rxjs": "6.6.7", + "sass": "1.58.1", + "sass-loader": "13.2.0", + "semver": "7.5.3", + "source-map-loader": "4.0.1", + "source-map-support": "0.5.21", + "terser": "5.16.3", + "text-table": "0.2.0", + "tree-kill": "1.2.2", + "tslib": "2.5.0", + "webpack": "5.76.1", + "webpack-dev-middleware": "6.1.2", + "webpack-dev-server": "4.11.1", + "webpack-merge": "5.8.0", + "webpack-subresource-integrity": "5.1.0" + }, + "engines": { + "node": "^14.20.0 || ^16.13.0 || >=18.10.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "optionalDependencies": { + "esbuild": "0.17.8" + }, + "peerDependencies": { + "@angular/compiler-cli": "^15.0.0", + "@angular/localize": "^15.0.0", + "@angular/platform-server": "^15.0.0", + "@angular/service-worker": "^15.0.0", + "karma": "^6.3.0", + "ng-packagr": "^15.0.0", + "protractor": "^7.0.0", + "tailwindcss": "^2.0.0 || ^3.0.0", + "typescript": ">=4.8.2 <5.0" + }, + "peerDependenciesMeta": { + "@angular/localize": { + "optional": true + }, + "@angular/platform-server": { + "optional": true + }, + "@angular/service-worker": { + "optional": true + }, + "karma": { + "optional": true + }, + "ng-packagr": { + "optional": true + }, + "protractor": { + "optional": true + }, + "tailwindcss": { + "optional": true + } + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/rxjs": { + "version": "6.6.7", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", + "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", + "dev": true, + "dependencies": { + "tslib": "^1.9.0" + }, + "engines": { + "npm": ">=2.0.0" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/rxjs/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + }, + "node_modules/@angular-devkit/build-angular/node_modules/tslib": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz", + "integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==", + "dev": true + }, + "node_modules/@angular-devkit/build-webpack": { + "version": "0.1502.11", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.1502.11.tgz", + "integrity": "sha512-OTONIRp770Jfems4+cULmtoeSzjnpx5UjV2EazojnhRXXBSJMWRMPvwD2QvQl9UO/6eOV3d2mgmP2xOZgc/D6w==", + "dev": true, + "dependencies": { + "@angular-devkit/architect": "0.1502.11", + "rxjs": "6.6.7" + }, + "engines": { + "node": "^14.20.0 || ^16.13.0 || >=18.10.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "webpack": "^5.30.0", + "webpack-dev-server": "^4.0.0" + } + }, + "node_modules/@angular-devkit/build-webpack/node_modules/rxjs": { + "version": "6.6.7", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", + "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", + "dev": true, + "dependencies": { + "tslib": "^1.9.0" + }, + "engines": { + "npm": ">=2.0.0" + } + }, + "node_modules/@angular-devkit/build-webpack/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + }, + "node_modules/@angular-devkit/core": { + "version": "15.2.11", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-15.2.11.tgz", + "integrity": "sha512-zd6QelJ8pOPvz6TsehR0JqixjDjzgEOkKywBJBuwNXY+Nw3MJGayJeWS0UgC+Gk+LoTkpI21RoyaYELkAmD/tw==", + "dev": true, + "dependencies": { + "ajv": "8.12.0", + "ajv-formats": "2.1.1", + "jsonc-parser": "3.2.0", + "rxjs": "6.6.7", + "source-map": "0.7.4" + }, + "engines": { + "node": "^14.20.0 || ^16.13.0 || >=18.10.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "chokidar": "^3.5.2" + }, + "peerDependenciesMeta": { + "chokidar": { + "optional": true + } + } + }, + "node_modules/@angular-devkit/core/node_modules/rxjs": { + "version": "6.6.7", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", + "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", + "dev": true, + "dependencies": { + "tslib": "^1.9.0" + }, + "engines": { + "npm": ">=2.0.0" + } + }, + "node_modules/@angular-devkit/core/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + }, + "node_modules/@angular-devkit/schematics": { + "version": "15.1.6", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-15.1.6.tgz", + "integrity": "sha512-cwmJFpS43zrdlmfwfHIxG/Nzg5rzFdtKrHx64ZXxNFm6JdyK2JTs/qrHUwv1FYWAcqhdiHn+00jYklMmvsvPOA==", + "dependencies": { + "@angular-devkit/core": "15.1.6", + "jsonc-parser": "3.2.0", + "magic-string": "0.27.0", + "ora": "5.4.1", + "rxjs": "6.6.7" + }, + "engines": { + "node": "^14.20.0 || ^16.13.0 || >=18.10.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@angular-devkit/schematics/node_modules/@angular-devkit/core": { + "version": "15.1.6", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-15.1.6.tgz", + "integrity": "sha512-jGgxyRjecVf6lEyqDxz7ltMEndNPxIg720pk6r40fgsu0dU8w9vjJSJe7k0XdJiXVRcN6wZa/J5nO/xcwWVIsA==", + "dependencies": { + "ajv": "8.12.0", + "ajv-formats": "2.1.1", + "jsonc-parser": "3.2.0", + "rxjs": "6.6.7", + "source-map": "0.7.4" + }, + "engines": { + "node": "^14.20.0 || ^16.13.0 || >=18.10.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "chokidar": "^3.5.2" + }, + "peerDependenciesMeta": { + "chokidar": { + "optional": true + } + } + }, + "node_modules/@angular-devkit/schematics/node_modules/magic-string": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.27.0.tgz", + "integrity": "sha512-8UnnX2PeRAPZuN12svgR9j7M1uWMovg/CEnIwIG0LFkXSJJe4PdfUGiTGl8V9bsBHFUtfVINcSyYxd7q+kx9fA==", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.13" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@angular-devkit/schematics/node_modules/rxjs": { + "version": "6.6.7", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", + "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", + "dependencies": { + "tslib": "^1.9.0" + }, + "engines": { + "npm": ">=2.0.0" + } + }, + "node_modules/@angular-devkit/schematics/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + }, + "node_modules/@angular/animations": { + "version": "15.2.10", + "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-15.2.10.tgz", + "integrity": "sha512-yxfN8qQpMaukRU5LjFkJBmy85rqrOp86tYVCsf+hmPEFRiXBMUj6xYLeCMcpk3Mt1JtnWGBR34ivGx+7bNeAow==", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^14.20.0 || ^16.13.0 || >=18.10.0" + }, + "peerDependencies": { + "@angular/core": "15.2.10" + } + }, + "node_modules/@angular/cdk": { + "version": "15.2.9", + "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-15.2.9.tgz", + "integrity": "sha512-koaM07N1AIQ5oHU27l0/FoQSSoYAwlAYwVZ4Di3bYrJsTBNCN2Xsby7wI8gZxdepMnV4Fe9si382BDBov+oO4Q==", + "dependencies": { + "tslib": "^2.3.0" + }, + "optionalDependencies": { + "parse5": "^7.1.2" + }, + "peerDependencies": { + "@angular/common": "^15.0.0 || ^16.0.0", + "@angular/core": "^15.0.0 || ^16.0.0", + "rxjs": "^6.5.3 || ^7.4.0" + } + }, + "node_modules/@angular/cdk/node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "optional": true, + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/@angular/cdk/node_modules/parse5": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz", + "integrity": "sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==", + "optional": true, + "dependencies": { + "entities": "^4.4.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/@angular/cli": { + "version": "15.1.6", + "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-15.1.6.tgz", + "integrity": "sha512-GmC9jZK2ipUWj0dlfTI5oEYia4y1fLfO3AtAKU5CylNYrGyB+DRytKY8Bx6Fs4kaNBY8V8YnyLi7E/78gziMdg==", + "dev": true, + "dependencies": { + "@angular-devkit/architect": "0.1501.6", + "@angular-devkit/core": "15.1.6", + "@angular-devkit/schematics": "15.1.6", + "@schematics/angular": "15.1.6", + "@yarnpkg/lockfile": "1.1.0", + "ansi-colors": "4.1.3", + "ini": "3.0.1", + "inquirer": "8.2.4", + "jsonc-parser": "3.2.0", + "npm-package-arg": "10.1.0", + "npm-pick-manifest": "8.0.1", + "open": "8.4.0", + "ora": "5.4.1", + "pacote": "15.0.8", + "resolve": "1.22.1", + "semver": "7.3.8", + "symbol-observable": "4.0.0", + "yargs": "17.6.2" + }, + "bin": { + "ng": "bin/ng.js" + }, + "engines": { + "node": "^14.20.0 || ^16.13.0 || >=18.10.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@angular/cli/node_modules/@angular-devkit/architect": { + "version": "0.1501.6", + "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1501.6.tgz", + "integrity": "sha512-u07zZFlfrg0Qn4mu5M9Nz0pH2Yd2028XF/73980PsZMxwkSm4diF08v4bHk3UyR7yPT7phwvt4znj6ryZhx1gw==", + "dev": true, + "dependencies": { + "@angular-devkit/core": "15.1.6", + "rxjs": "6.6.7" + }, + "engines": { + "node": "^14.20.0 || ^16.13.0 || >=18.10.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@angular/cli/node_modules/@angular-devkit/core": { + "version": "15.1.6", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-15.1.6.tgz", + "integrity": "sha512-jGgxyRjecVf6lEyqDxz7ltMEndNPxIg720pk6r40fgsu0dU8w9vjJSJe7k0XdJiXVRcN6wZa/J5nO/xcwWVIsA==", + "dev": true, + "dependencies": { + "ajv": "8.12.0", + "ajv-formats": "2.1.1", + "jsonc-parser": "3.2.0", + "rxjs": "6.6.7", + "source-map": "0.7.4" + }, + "engines": { + "node": "^14.20.0 || ^16.13.0 || >=18.10.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "chokidar": "^3.5.2" + }, + "peerDependenciesMeta": { + "chokidar": { + "optional": true + } + } + }, + "node_modules/@angular/cli/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@angular/cli/node_modules/open": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/open/-/open-8.4.0.tgz", + "integrity": "sha512-XgFPPM+B28FtCCgSb9I+s9szOC1vZRSwgWsRUA5ylIxRTgKozqjOCrVOqGsYABPYK5qnfqClxZTFBa8PKt2v6Q==", + "dev": true, + "dependencies": { + "define-lazy-prop": "^2.0.0", + "is-docker": "^2.1.1", + "is-wsl": "^2.2.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@angular/cli/node_modules/rxjs": { + "version": "6.6.7", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", + "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", + "dev": true, + "dependencies": { + "tslib": "^1.9.0" + }, + "engines": { + "npm": ">=2.0.0" + } + }, + "node_modules/@angular/cli/node_modules/semver": { + "version": "7.3.8", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", + "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@angular/cli/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + }, + "node_modules/@angular/cli/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/@angular/common": { + "version": "15.2.10", + "resolved": "https://registry.npmjs.org/@angular/common/-/common-15.2.10.tgz", + "integrity": "sha512-jdBn3fctkqoNrJn9VLsUHpcCEhCxWSczdsR+BBbD6T0oLl6vMrAVNjPwfBejnlgfWN1KoRU9kgOYsMxa5apIWQ==", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^14.20.0 || ^16.13.0 || >=18.10.0" + }, + "peerDependencies": { + "@angular/core": "15.2.10", + "rxjs": "^6.5.3 || ^7.4.0" + } + }, + "node_modules/@angular/compiler": { + "version": "15.2.10", + "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-15.2.10.tgz", + "integrity": "sha512-M0XkeU0O73UlJZwDvOyp8/apetz9UKj78eTFDseMYJDLcxe6MpkbkxqpsGZnKYDj7LIep8PmCAKEkhtenE82zw==", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^14.20.0 || ^16.13.0 || >=18.10.0" + }, + "peerDependencies": { + "@angular/core": "15.2.10" + }, + "peerDependenciesMeta": { + "@angular/core": { + "optional": true + } + } + }, + "node_modules/@angular/compiler-cli": { + "version": "15.2.10", + "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-15.2.10.tgz", + "integrity": "sha512-mCFIxrs60XicKfA2o42hA7LrQvhybi9BQveWuZn/2iIEOXx7R62Iemz8E21pLWftAZHGxEW3NECfBrY1d3gVmA==", + "dev": true, + "dependencies": { + "@babel/core": "7.19.3", + "@jridgewell/sourcemap-codec": "^1.4.14", + "chokidar": "^3.0.0", + "convert-source-map": "^1.5.1", + "dependency-graph": "^0.11.0", + "magic-string": "^0.27.0", + "reflect-metadata": "^0.1.2", + "semver": "^7.0.0", + "tslib": "^2.3.0", + "yargs": "^17.2.1" + }, + "bin": { + "ng-xi18n": "bundles/src/bin/ng_xi18n.js", + "ngc": "bundles/src/bin/ngc.js", + "ngcc": "bundles/ngcc/main-ngcc.js" + }, + "engines": { + "node": "^14.20.0 || ^16.13.0 || >=18.10.0" + }, + "peerDependencies": { + "@angular/compiler": "15.2.10", + "typescript": ">=4.8.2 <5.0" + } + }, + "node_modules/@angular/compiler-cli/node_modules/@babel/core": { + "version": "7.19.3", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.19.3.tgz", + "integrity": "sha512-WneDJxdsjEvyKtXKsaBGbDeiyOjR5vYq4HcShxnIbG0qixpoHjI3MqeZM9NDvsojNCEBItQE4juOo/bU6e72gQ==", + "dev": true, + "dependencies": { + "@ampproject/remapping": "^2.1.0", + "@babel/code-frame": "^7.18.6", + "@babel/generator": "^7.19.3", + "@babel/helper-compilation-targets": "^7.19.3", + "@babel/helper-module-transforms": "^7.19.0", + "@babel/helpers": "^7.19.0", + "@babel/parser": "^7.19.3", + "@babel/template": "^7.18.10", + "@babel/traverse": "^7.19.3", + "@babel/types": "^7.19.3", + "convert-source-map": "^1.7.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.1", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@angular/compiler-cli/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, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@angular/compiler-cli/node_modules/magic-string": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.27.0.tgz", + "integrity": "sha512-8UnnX2PeRAPZuN12svgR9j7M1uWMovg/CEnIwIG0LFkXSJJe4PdfUGiTGl8V9bsBHFUtfVINcSyYxd7q+kx9fA==", + "dev": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.13" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@angular/core": { + "version": "15.2.10", + "resolved": "https://registry.npmjs.org/@angular/core/-/core-15.2.10.tgz", + "integrity": "sha512-meGGidnitQJGDxYd9/LrqYiVlId+vGaLoiLgJdKBz+o2ZO6OmXQGuNw2VBqf17/Cc0/UjzrOY7+kILNFKkk/WQ==", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^14.20.0 || ^16.13.0 || >=18.10.0" + }, + "peerDependencies": { + "rxjs": "^6.5.3 || ^7.4.0", + "zone.js": "~0.11.4 || ~0.12.0 || ~0.13.0" + } + }, + "node_modules/@angular/fire": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/@angular/fire/-/fire-7.5.0.tgz", + "integrity": "sha512-M/JJ5SKnpfNZFLKl0Awjn3r2NhURy2Mx+E8EzSDWSg1sHD7IThrynZYPUSZR+cd3KGsGoEacIP2x8Ra+0a+CVg==", + "dependencies": { + "@angular-devkit/schematics": "^12.0.0 || ^13.0.0 || ^14.0.0 || ^15.0.0", + "@schematics/angular": "^12.0.0 || ^13.0.0 || ^14.0.0 || ^15.0.0", + "file-loader": "^6.2.0", + "firebase": "^9.8.0", + "fs-extra": "^8.0.1", + "fuzzy": "^0.1.3", + "inquirer": "^8.1.1", + "inquirer-autocomplete-prompt": "^1.0.1", + "jsonc-parser": "^3.0.0", + "node-fetch": "^2.6.1", + "open": "^8.0.0", + "ora": "^5.3.0", + "rxfire": "^6.0.0", + "semver": "^7.1.3", + "triple-beam": "^1.3.0", + "tslib": "^2.0.0", + "winston": "^3.0.0" + }, + "peerDependencies": { + "@angular/common": "^12.0.0 || ^13.0.0 || ^14.0.0 || ^15.0.0", + "@angular/core": "^12.0.0 || ^13.0.0 || ^14.0.0 || ^15.0.0", + "@angular/platform-browser": "^12.0.0 || ^13.0.0 || ^14.0.0 || ^15.0.0", + "@angular/platform-browser-dynamic": "^12.0.0 || ^13.0.0 || ^14.0.0 || ^15.0.0", + "firebase-tools": "^9.9.0 || ^10.0.0 || ^11.0.0", + "rxjs": "~6.6.0 || ^7.0.0" + }, + "peerDependenciesMeta": { + "firebase-tools": { + "optional": true + } + } + }, + "node_modules/@angular/flex-layout": { + "version": "15.0.0-beta.42", + "resolved": "https://registry.npmjs.org/@angular/flex-layout/-/flex-layout-15.0.0-beta.42.tgz", + "integrity": "sha512-cTAPVMMxnyIFwpZwdq0PL5mdP9Qh+R8MB7ZBezVaN3Rz2fRrkagzKpLvPX3TFzepXrvHBdpKsU4b8u+NxEC/6g==", + "deprecated": "This package has been deprecated. Please see https://blog.angular.io/modern-css-in-angular-layouts-4a259dca9127", + "dependencies": { + "tslib": "^2.3.0" + }, + "peerDependencies": { + "@angular/cdk": ">=15.0.0", + "@angular/common": ">=15.0.2", + "@angular/core": ">=15.0.2", + "@angular/platform-browser": ">=15.0.2", + "rxjs": "^6.5.3 || ^7.4.0" + } + }, + "node_modules/@angular/forms": { + "version": "15.2.10", + "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-15.2.10.tgz", + "integrity": "sha512-NIntGsNcN6o8L1txsbWXOf6f3K/CUBizdKsxsYVYGJIXEW5qU6UnWmfAZffNNXsT/XvbgUCjgDwT0cAwcqZPuQ==", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^14.20.0 || ^16.13.0 || >=18.10.0" + }, + "peerDependencies": { + "@angular/common": "15.2.10", + "@angular/core": "15.2.10", + "@angular/platform-browser": "15.2.10", + "rxjs": "^6.5.3 || ^7.4.0" + } + }, + "node_modules/@angular/material": { + "version": "15.2.9", + "resolved": "https://registry.npmjs.org/@angular/material/-/material-15.2.9.tgz", + "integrity": "sha512-emuFF/7+91Jq+6kVCl3FiVoFLtAZoh+woFQWNuK8nhx0HmD4ckLFI8d9a6ERYR3zRuKhq5deSRE2kYsfpjrrsQ==", + "dependencies": { + "@material/animation": "15.0.0-canary.684e33d25.0", + "@material/auto-init": "15.0.0-canary.684e33d25.0", + "@material/banner": "15.0.0-canary.684e33d25.0", + "@material/base": "15.0.0-canary.684e33d25.0", + "@material/button": "15.0.0-canary.684e33d25.0", + "@material/card": "15.0.0-canary.684e33d25.0", + "@material/checkbox": "15.0.0-canary.684e33d25.0", + "@material/chips": "15.0.0-canary.684e33d25.0", + "@material/circular-progress": "15.0.0-canary.684e33d25.0", + "@material/data-table": "15.0.0-canary.684e33d25.0", + "@material/density": "15.0.0-canary.684e33d25.0", + "@material/dialog": "15.0.0-canary.684e33d25.0", + "@material/dom": "15.0.0-canary.684e33d25.0", + "@material/drawer": "15.0.0-canary.684e33d25.0", + "@material/elevation": "15.0.0-canary.684e33d25.0", + "@material/fab": "15.0.0-canary.684e33d25.0", + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "@material/floating-label": "15.0.0-canary.684e33d25.0", + "@material/form-field": "15.0.0-canary.684e33d25.0", + "@material/icon-button": "15.0.0-canary.684e33d25.0", + "@material/image-list": "15.0.0-canary.684e33d25.0", + "@material/layout-grid": "15.0.0-canary.684e33d25.0", + "@material/line-ripple": "15.0.0-canary.684e33d25.0", + "@material/linear-progress": "15.0.0-canary.684e33d25.0", + "@material/list": "15.0.0-canary.684e33d25.0", + "@material/menu": "15.0.0-canary.684e33d25.0", + "@material/menu-surface": "15.0.0-canary.684e33d25.0", + "@material/notched-outline": "15.0.0-canary.684e33d25.0", + "@material/radio": "15.0.0-canary.684e33d25.0", + "@material/ripple": "15.0.0-canary.684e33d25.0", + "@material/rtl": "15.0.0-canary.684e33d25.0", + "@material/segmented-button": "15.0.0-canary.684e33d25.0", + "@material/select": "15.0.0-canary.684e33d25.0", + "@material/shape": "15.0.0-canary.684e33d25.0", + "@material/slider": "15.0.0-canary.684e33d25.0", + "@material/snackbar": "15.0.0-canary.684e33d25.0", + "@material/switch": "15.0.0-canary.684e33d25.0", + "@material/tab": "15.0.0-canary.684e33d25.0", + "@material/tab-bar": "15.0.0-canary.684e33d25.0", + "@material/tab-indicator": "15.0.0-canary.684e33d25.0", + "@material/tab-scroller": "15.0.0-canary.684e33d25.0", + "@material/textfield": "15.0.0-canary.684e33d25.0", + "@material/theme": "15.0.0-canary.684e33d25.0", + "@material/tooltip": "15.0.0-canary.684e33d25.0", + "@material/top-app-bar": "15.0.0-canary.684e33d25.0", + "@material/touch-target": "15.0.0-canary.684e33d25.0", + "@material/typography": "15.0.0-canary.684e33d25.0", + "tslib": "^2.3.0" + }, + "peerDependencies": { + "@angular/animations": "^15.0.0 || ^16.0.0", + "@angular/cdk": "15.2.9", + "@angular/common": "^15.0.0 || ^16.0.0", + "@angular/core": "^15.0.0 || ^16.0.0", + "@angular/forms": "^15.0.0 || ^16.0.0", + "@angular/platform-browser": "^15.0.0 || ^16.0.0", + "rxjs": "^6.5.3 || ^7.4.0" + } + }, + "node_modules/@angular/platform-browser": { + "version": "15.2.10", + "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-15.2.10.tgz", + "integrity": "sha512-9tbgVGSJqwfrOzT8aA/kWBLNhJSQ9gUg0CJxwFBSJm8VkBUJrszoBlDsnSvlxx8/W2ejNULKHFTXeUzq0O/+RQ==", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^14.20.0 || ^16.13.0 || >=18.10.0" + }, + "peerDependencies": { + "@angular/animations": "15.2.10", + "@angular/common": "15.2.10", + "@angular/core": "15.2.10" + }, + "peerDependenciesMeta": { + "@angular/animations": { + "optional": true + } + } + }, + "node_modules/@angular/platform-browser-dynamic": { + "version": "15.2.10", + "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-15.2.10.tgz", + "integrity": "sha512-JHP6W+FX715Qv7DhqvfZLuBZXSDJrboiQsR06gUAgDSjAUyhbqmpVg/2YOtgeWpPkzNDtXdPU2PhcRdIv5J3Yg==", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^14.20.0 || ^16.13.0 || >=18.10.0" + }, + "peerDependencies": { + "@angular/common": "15.2.10", + "@angular/compiler": "15.2.10", + "@angular/core": "15.2.10", + "@angular/platform-browser": "15.2.10" + } + }, + "node_modules/@angular/router": { + "version": "15.2.10", + "resolved": "https://registry.npmjs.org/@angular/router/-/router-15.2.10.tgz", + "integrity": "sha512-LmuqEg0iIXSw7bli6HKJ19cbxP91v37GtRwbGKswyLihqzTgvjBYpvcfMnB5FRQ5LWkTwq5JclkX03dZw290Yg==", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^14.20.0 || ^16.13.0 || >=18.10.0" + }, + "peerDependencies": { + "@angular/common": "15.2.10", + "@angular/core": "15.2.10", + "@angular/platform-browser": "15.2.10", + "rxjs": "^6.5.3 || ^7.4.0" + } + }, + "node_modules/@assemblyscript/loader": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@assemblyscript/loader/-/loader-0.10.1.tgz", + "integrity": "sha512-H71nDOOL8Y7kWRLqf6Sums+01Q5msqBW2KhDUTemh1tvY04eSkSXrK0uj/4mmY0Xr16/3zyZmsrxN7CKuRbNRg==", + "dev": true + }, + "node_modules/@babel/code-frame": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.7.tgz", + "integrity": "sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA==", + "dev": true, + "dependencies": { + "@babel/highlight": "^7.24.7", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.24.7.tgz", + "integrity": "sha512-qJzAIcv03PyaWqxRgO4mSU3lihncDT296vnyuE2O8uA4w3UHWI4S3hgeZd1L8W1Bft40w9JxJ2b412iDUFFRhw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.20.12", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.20.12.tgz", + "integrity": "sha512-XsMfHovsUYHFMdrIHkZphTN/2Hzzi78R08NuHfDBehym2VsPDL6Zn/JAD/JQdnRvbSsbQc4mVaU1m6JgtTEElg==", + "dev": true, + "dependencies": { + "@ampproject/remapping": "^2.1.0", + "@babel/code-frame": "^7.18.6", + "@babel/generator": "^7.20.7", + "@babel/helper-compilation-targets": "^7.20.7", + "@babel/helper-module-transforms": "^7.20.11", + "@babel/helpers": "^7.20.7", + "@babel/parser": "^7.20.7", + "@babel/template": "^7.20.7", + "@babel/traverse": "^7.20.12", + "@babel/types": "^7.20.7", + "convert-source-map": "^1.7.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.2", + "semver": "^6.3.0" + }, + "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, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.20.14", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.20.14.tgz", + "integrity": "sha512-AEmuXHdcD3A52HHXxaTmYlb8q/xMEhoRP67B3T4Oq7lbmSoqroMZzjnGj3+i1io3pdnF8iBYVu4Ilj+c4hBxYg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.20.7", + "@jridgewell/gen-mapping": "^0.3.2", + "jsesc": "^2.5.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/generator/node_modules/@jridgewell/gen-mapping": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", + "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", + "dev": true, + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/helper-annotate-as-pure": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.18.6.tgz", + "integrity": "sha512-duORpUiYrEpzKIop6iNbjnwKLAKnJ47csTyRACyEmWj0QdUrm5aqNJGHSSEQSUAvNW0ojX0dOmK9dZduvkfeXA==", + "dev": true, + "dependencies": { + "@babel/types": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-builder-binary-assignment-operator-visitor": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.24.7.tgz", + "integrity": "sha512-xZeCVVdwb4MsDBkkyZ64tReWYrLRHlMN72vP7Bdm3OUOuyFZExhsHUUnuWnm2/XOlAJzR0LfPpB56WXZn0X/lA==", + "dev": true, + "dependencies": { + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.24.7.tgz", + "integrity": "sha512-ctSdRHBi20qWOfy27RUb4Fhp07KSJ3sXcuSvTrXrc4aG8NSYDo1ici3Vhg9bg69y5bj0Mr1lh0aeEgTvc12rMg==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.24.7", + "@babel/helper-validator-option": "^7.24.7", + "browserslist": "^4.22.2", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/browserslist": { + "version": "4.23.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.1.tgz", + "integrity": "sha512-TUfofFo/KsK/bWZ9TWQ5O26tsWW4Uhmt8IYklbnUa70udB6P2wA7w7o4PY4muaEPBQaAX+CEnmmIA41NVHtPVw==", + "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" + } + ], + "dependencies": { + "caniuse-lite": "^1.0.30001629", + "electron-to-chromium": "^1.4.796", + "node-releases": "^2.0.14", + "update-browserslist-db": "^1.0.16" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/@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, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-create-class-features-plugin": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.24.7.tgz", + "integrity": "sha512-kTkaDl7c9vO80zeX1rJxnuRpEsD5tA81yh11X1gQo+PhSti3JS+7qeZo9U4RHobKRiFPKaGK3svUAeb8D0Q7eg==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.24.7", + "@babel/helper-environment-visitor": "^7.24.7", + "@babel/helper-function-name": "^7.24.7", + "@babel/helper-member-expression-to-functions": "^7.24.7", + "@babel/helper-optimise-call-expression": "^7.24.7", + "@babel/helper-replace-supers": "^7.24.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.24.7", + "@babel/helper-split-export-declaration": "^7.24.7", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-create-class-features-plugin/node_modules/@babel/helper-annotate-as-pure": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.24.7.tgz", + "integrity": "sha512-BaDeOonYvhdKw+JoMVkAixAAJzG2jVPIwWoKBPdYuY9b452e2rPuI9QPYh3KpofZ3pW2akOmwZLOiOsHMiqRAg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-create-class-features-plugin/node_modules/@babel/helper-split-export-declaration": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.7.tgz", + "integrity": "sha512-oy5V7pD+UvfkEATUKvIjvIAH/xCzfsFVw7ygW2SI6NClZzquT+mwdTfgfdbUiceh6iQO0CHtCPsyze/MZ2YbAA==", + "dev": true, + "dependencies": { + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-create-class-features-plugin/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-create-regexp-features-plugin": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.24.7.tgz", + "integrity": "sha512-03TCmXy2FtXJEZfbXDTSqq1fRJArk7lX9DOFC/47VthYcxyIOx+eXQmdo6DOQvrbpIix+KfXwvuXdFDZHxt+rA==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.24.7", + "regexpu-core": "^5.3.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-create-regexp-features-plugin/node_modules/@babel/helper-annotate-as-pure": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.24.7.tgz", + "integrity": "sha512-BaDeOonYvhdKw+JoMVkAixAAJzG2jVPIwWoKBPdYuY9b452e2rPuI9QPYh3KpofZ3pW2akOmwZLOiOsHMiqRAg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-create-regexp-features-plugin/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-define-polyfill-provider": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.3.3.tgz", + "integrity": "sha512-z5aQKU4IzbqCC1XH0nAqfsFLMVSo22SBKUc0BxGrLkolTdPTructy0ToNnlO2zA4j9Q/7pjMZf0DSY+DSTYzww==", + "dev": true, + "dependencies": { + "@babel/helper-compilation-targets": "^7.17.7", + "@babel/helper-plugin-utils": "^7.16.7", + "debug": "^4.1.1", + "lodash.debounce": "^4.0.8", + "resolve": "^1.14.2", + "semver": "^6.1.2" + }, + "peerDependencies": { + "@babel/core": "^7.4.0-0" + } + }, + "node_modules/@babel/helper-define-polyfill-provider/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-environment-visitor": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.24.7.tgz", + "integrity": "sha512-DoiN84+4Gnd0ncbBOM9AZENV4a5ZiL39HYMyZJGZ/AZEykHYdJw0wW3kdcsh9/Kn+BRXHLkkklZ51ecPKmI1CQ==", + "dev": true, + "dependencies": { + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-function-name": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.24.7.tgz", + "integrity": "sha512-FyoJTsj/PEUWu1/TYRiXTIHc8lbw+TDYkZuoE43opPS5TrI7MyONBE1oNvfguEXAD9yhQRrVBnXdXzSLQl9XnA==", + "dev": true, + "dependencies": { + "@babel/template": "^7.24.7", + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-function-name/node_modules/@babel/template": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.24.7.tgz", + "integrity": "sha512-jYqfPrU9JTF0PmPy1tLYHW4Mp4KlgxJD9l2nP9fD6yT/ICi554DmrWBAEYpIelzjHf1msDP3PxJIRt/nFNfBig==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.24.7", + "@babel/parser": "^7.24.7", + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-hoist-variables": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.24.7.tgz", + "integrity": "sha512-MJJwhkoGy5c4ehfoRyrJ/owKeMl19U54h27YYftT0o2teQ3FJ3nQUf/I3LlJsX4l3qlw7WRXUmiyajvHXoTubQ==", + "dev": true, + "dependencies": { + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-member-expression-to-functions": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.24.7.tgz", + "integrity": "sha512-LGeMaf5JN4hAT471eJdBs/GK1DoYIJ5GCtZN/EsL6KUiiDZOvO/eKE11AMZJa2zP4zk4qe9V2O/hxAmkRc8p6w==", + "dev": true, + "dependencies": { + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.24.7.tgz", + "integrity": "sha512-8AyH3C+74cgCVVXow/myrynrAGv+nTVg5vKu2nZph9x7RcRwzmh0VFallJuFTZ9mx6u4eSdXZfcOzSqTUm0HCA==", + "dev": true, + "dependencies": { + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.24.7.tgz", + "integrity": "sha512-1fuJEwIrp+97rM4RWdO+qrRsZlAeL1lQJoPqtCYWv0NL115XM93hIH4CSRln2w52SqvmY5hqdtauB6QFCDiZNQ==", + "dev": true, + "dependencies": { + "@babel/helper-environment-visitor": "^7.24.7", + "@babel/helper-module-imports": "^7.24.7", + "@babel/helper-simple-access": "^7.24.7", + "@babel/helper-split-export-declaration": "^7.24.7", + "@babel/helper-validator-identifier": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-module-transforms/node_modules/@babel/helper-split-export-declaration": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.7.tgz", + "integrity": "sha512-oy5V7pD+UvfkEATUKvIjvIAH/xCzfsFVw7ygW2SI6NClZzquT+mwdTfgfdbUiceh6iQO0CHtCPsyze/MZ2YbAA==", + "dev": true, + "dependencies": { + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-optimise-call-expression": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.24.7.tgz", + "integrity": "sha512-jKiTsW2xmWwxT1ixIdfXUZp+P5yURx2suzLZr5Hi64rURpDYdMW0pv+Uf17EYk2Rd428Lx4tLsnjGJzYKDM/6A==", + "dev": true, + "dependencies": { + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.7.tgz", + "integrity": "sha512-Rq76wjt7yz9AAc1KnlRKNAi/dMSVWgDRx43FHoJEbcYU6xOWaE2dVPwcdTukJrjxS65GITyfbvEYHvkirZ6uEg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-remap-async-to-generator": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.24.7.tgz", + "integrity": "sha512-9pKLcTlZ92hNZMQfGCHImUpDOlAgkkpqalWEeftW5FBya75k8Li2ilerxkM/uBEj01iBZXcCIB/bwvDYgWyibA==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.24.7", + "@babel/helper-environment-visitor": "^7.24.7", + "@babel/helper-wrap-function": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-remap-async-to-generator/node_modules/@babel/helper-annotate-as-pure": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.24.7.tgz", + "integrity": "sha512-BaDeOonYvhdKw+JoMVkAixAAJzG2jVPIwWoKBPdYuY9b452e2rPuI9QPYh3KpofZ3pW2akOmwZLOiOsHMiqRAg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-replace-supers": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.24.7.tgz", + "integrity": "sha512-qTAxxBM81VEyoAY0TtLrx1oAEJc09ZK67Q9ljQToqCnA+55eNwCORaxlKyu+rNfX86o8OXRUSNUnrtsAZXM9sg==", + "dev": true, + "dependencies": { + "@babel/helper-environment-visitor": "^7.24.7", + "@babel/helper-member-expression-to-functions": "^7.24.7", + "@babel/helper-optimise-call-expression": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-simple-access": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.24.7.tgz", + "integrity": "sha512-zBAIvbCMh5Ts+b86r/CjU+4XGYIs+R1j951gxI3KmmxBMhCg4oQMsv6ZXQ64XOm/cvzfU1FmoCyt6+owc5QMYg==", + "dev": true, + "dependencies": { + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-skip-transparent-expression-wrappers": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.24.7.tgz", + "integrity": "sha512-IO+DLT3LQUElMbpzlatRASEyQtfhSE0+m465v++3jyyXeBTBUjtVZg28/gHeV5mrTJqvEKhKroBGAvhW+qPHiQ==", + "dev": true, + "dependencies": { + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-split-export-declaration": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.18.6.tgz", + "integrity": "sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA==", + "dev": true, + "dependencies": { + "@babel/types": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.7.tgz", + "integrity": "sha512-7MbVt6xrwFQbunH2DNQsAP5sTGxfqQtErvBIvIMi6EQnbgUOuVYanvREcmFrOPhoXBrTtjhhP+lW+o5UfK+tDg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz", + "integrity": "sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.24.7.tgz", + "integrity": "sha512-yy1/KvjhV/ZCL+SM7hBrvnZJ3ZuT9OuZgIJAGpPEToANvc3iM6iDvBnRjtElWibHU6n8/LPR/EjX9EtIEYO3pw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-wrap-function": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.24.7.tgz", + "integrity": "sha512-N9JIYk3TD+1vq/wn77YnJOqMtfWhNewNE+DJV4puD2X7Ew9J4JvrzrFDfTfyv5EgEXVy9/Wt8QiOErzEmv5Ifw==", + "dev": true, + "dependencies": { + "@babel/helper-function-name": "^7.24.7", + "@babel/template": "^7.24.7", + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-wrap-function/node_modules/@babel/template": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.24.7.tgz", + "integrity": "sha512-jYqfPrU9JTF0PmPy1tLYHW4Mp4KlgxJD9l2nP9fD6yT/ICi554DmrWBAEYpIelzjHf1msDP3PxJIRt/nFNfBig==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.24.7", + "@babel/parser": "^7.24.7", + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.24.7.tgz", + "integrity": "sha512-NlmJJtvcw72yRJRcnCmGvSi+3jDEg8qFu3z0AFoymmzLx5ERVWyzd9kVXr7Th9/8yIJi2Zc6av4Tqz3wFs8QWg==", + "dev": true, + "dependencies": { + "@babel/template": "^7.24.7", + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers/node_modules/@babel/template": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.24.7.tgz", + "integrity": "sha512-jYqfPrU9JTF0PmPy1tLYHW4Mp4KlgxJD9l2nP9fD6yT/ICi554DmrWBAEYpIelzjHf1msDP3PxJIRt/nFNfBig==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.24.7", + "@babel/parser": "^7.24.7", + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.7.tgz", + "integrity": "sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.24.7", + "chalk": "^2.4.2", + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.7.tgz", + "integrity": "sha512-9uUYRm6OqQrCqQdG1iCBwBPZgN8ciDBro2nIOFaiRz1/BCxaI7CNvQbDHvsArAC7Tw9Hda/B3U+6ui9u4HWXPw==", + "dev": true, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.24.7.tgz", + "integrity": "sha512-unaQgZ/iRu/By6tsjMZzpeBZjChYfLYry6HrEXPoz3KmfF0sVBQ1l8zKMQ4xRGLWVsjuvB8nQfjNP/DcfEOCsg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.24.7.tgz", + "integrity": "sha512-+izXIbke1T33mY4MSNnrqhPXDz01WYhEf3yF5NbnUtkiNnm+XBZJl3kNfoK6NKmYlz/D07+l2GWVK/QfDkNCuQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.24.7", + "@babel/plugin-transform-optional-chaining": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.13.0" + } + }, + "node_modules/@babel/plugin-proposal-async-generator-functions": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.20.7.tgz", + "integrity": "sha512-xMbiLsn/8RK7Wq7VeVytytS2L6qE69bXPB10YCmMdDZbKF4okCqY74pI/jJQ/8U0b/F6NrT2+14b8/P9/3AMGA==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-async-generator-functions instead.", + "dev": true, + "dependencies": { + "@babel/helper-environment-visitor": "^7.18.9", + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/helper-remap-async-to-generator": "^7.18.9", + "@babel/plugin-syntax-async-generators": "^7.8.4" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-class-properties": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.18.6.tgz", + "integrity": "sha512-cumfXOF0+nzZrrN8Rf0t7M+tF6sZc7vhQwYQck9q1/5w2OExlD+b4v4RpMJFaV1Z7WcDRgO6FqvxqxGlwo+RHQ==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-class-properties instead.", + "dev": true, + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-class-static-block": { + "version": "7.21.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-static-block/-/plugin-proposal-class-static-block-7.21.0.tgz", + "integrity": "sha512-XP5G9MWNUskFuP30IfFSEFB0Z6HzLIUcjYM4bYOPHXl7eiJ9HFv8tWj6TXTN5QODiEhDZAeI4hLok2iHFFV4hw==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-class-static-block instead.", + "dev": true, + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.21.0", + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/plugin-syntax-class-static-block": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.12.0" + } + }, + "node_modules/@babel/plugin-proposal-dynamic-import": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.18.6.tgz", + "integrity": "sha512-1auuwmK+Rz13SJj36R+jqFPMJWyKEDd7lLSdOj4oJK0UTgGueSAtkrCvz9ewmgyU/P941Rv2fQwZJN8s6QruXw==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-dynamic-import instead.", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6", + "@babel/plugin-syntax-dynamic-import": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-export-namespace-from": { + "version": "7.18.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-export-namespace-from/-/plugin-proposal-export-namespace-from-7.18.9.tgz", + "integrity": "sha512-k1NtHyOMvlDDFeb9G5PhUXuGj8m/wiwojgQVEhJ/fsVsMCpLyOP4h0uGEjYJKrRI+EVPlb5Jk+Gt9P97lOGwtA==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-export-namespace-from instead.", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.9", + "@babel/plugin-syntax-export-namespace-from": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-json-strings": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.18.6.tgz", + "integrity": "sha512-lr1peyn9kOdbYc0xr0OdHTZ5FMqS6Di+H0Fz2I/JwMzGmzJETNeOFq2pBySw6X/KFL5EWDjlJuMsUGRFb8fQgQ==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-json-strings instead.", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6", + "@babel/plugin-syntax-json-strings": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-logical-assignment-operators": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-logical-assignment-operators/-/plugin-proposal-logical-assignment-operators-7.20.7.tgz", + "integrity": "sha512-y7C7cZgpMIjWlKE5T7eJwp+tnRYM89HmRvWM5EQuB5BoHEONjmQ8lSNmBUwOyy/GFRsohJED51YBF79hE1djug==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-logical-assignment-operators instead.", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-nullish-coalescing-operator": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.18.6.tgz", + "integrity": "sha512-wQxQzxYeJqHcfppzBDnm1yAY0jSRkUXR2z8RePZYrKwMKgMlE8+Z6LUno+bd6LvbGh8Gltvy74+9pIYkr+XkKA==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-nullish-coalescing-operator instead.", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-numeric-separator": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.18.6.tgz", + "integrity": "sha512-ozlZFogPqoLm8WBr5Z8UckIoE4YQ5KESVcNudyXOR8uqIkliTEgJ3RoketfG6pmzLdeZF0H/wjE9/cCEitBl7Q==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-numeric-separator instead.", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6", + "@babel/plugin-syntax-numeric-separator": "^7.10.4" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-object-rest-spread": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.20.7.tgz", + "integrity": "sha512-d2S98yCiLxDVmBmE8UjGcfPvNEUbA1U5q5WxaWFUGRzJSVAZqm5W6MbPct0jxnegUZ0niLeNX+IOzEs7wYg9Dg==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-object-rest-spread instead.", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.20.5", + "@babel/helper-compilation-targets": "^7.20.7", + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-transform-parameters": "^7.20.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-optional-catch-binding": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.18.6.tgz", + "integrity": "sha512-Q40HEhs9DJQyaZfUjjn6vE8Cv4GmMHCYuMGIWUnlxH6400VGxOuwWsPt4FxXxJkC/5eOzgn0z21M9gMT4MOhbw==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-optional-catch-binding instead.", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-optional-chaining": { + "version": "7.21.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.21.0.tgz", + "integrity": "sha512-p4zeefM72gpmEe2fkUr/OnOXpWEf8nAgk7ZYVqqfFiyIG7oFfVZcCrU64hWn5xp4tQ9LkV4bTIa5rD0KANpKNA==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-optional-chaining instead.", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/helper-skip-transparent-expression-wrappers": "^7.20.0", + "@babel/plugin-syntax-optional-chaining": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-private-methods": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-methods/-/plugin-proposal-private-methods-7.18.6.tgz", + "integrity": "sha512-nutsvktDItsNn4rpGItSNV2sz1XwS+nfU0Rg8aCx3W3NOKVzdMjJRu0O5OkgDp3ZGICSTbgRpxZoWsxoKRvbeA==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-private-methods instead.", + "dev": true, + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-private-property-in-object": { + "version": "7.21.11", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.11.tgz", + "integrity": "sha512-0QZ8qP/3RLDVBwBFoWAwCtgcDZJVwA5LUJRZU8x2YFfKNuFq161wK3cuGrALu5yiPu+vzwTAg/sMWVNeWeNyaw==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-private-property-in-object instead.", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.18.6", + "@babel/helper-create-class-features-plugin": "^7.21.0", + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-unicode-property-regex": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.18.6.tgz", + "integrity": "sha512-2BShG/d5yoZyXZfVePH91urL5wTG6ASZU9M4o03lKK8u8UW1y08OMttBSOADTcJrnPMpvDXRG3G8fyLh4ovs8w==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-unicode-property-regex instead.", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-dynamic-import": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz", + "integrity": "sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-export-namespace-from": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz", + "integrity": "sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.3" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-assertions": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.24.7.tgz", + "integrity": "sha512-Ec3NRUMoi8gskrkBe3fNmEQfxDvY8bgfQpz6jlk/41kX9eUjvpyqWU7PBP/pLAvMaSQjbMNKJmvX57jP+M6bPg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-arrow-functions": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.24.7.tgz", + "integrity": "sha512-Dt9LQs6iEY++gXUwY03DNFat5C2NbO48jj+j/bSAz6b3HgPs39qcPiYt77fDObIcFwj3/C2ICX9YMwGflUoSHQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-async-to-generator": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.20.7.tgz", + "integrity": "sha512-Uo5gwHPT9vgnSXQxqGtpdufUiWp96gk7yiP4Mp5bm1QMkEmLXBO7PAGYbKoJ6DhAwiNkcHFBol/x5zZZkL/t0Q==", + "dev": true, + "dependencies": { + "@babel/helper-module-imports": "^7.18.6", + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/helper-remap-async-to-generator": "^7.18.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-block-scoped-functions": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.24.7.tgz", + "integrity": "sha512-yO7RAz6EsVQDaBH18IDJcMB1HnrUn2FJ/Jslc/WtPPWcjhpUJXU/rjbwmluzp7v/ZzWcEhTMXELnnsz8djWDwQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-block-scoping": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.24.7.tgz", + "integrity": "sha512-Nd5CvgMbWc+oWzBsuaMcbwjJWAcp5qzrbg69SZdHSP7AMY0AbWFqFO0WTFCA1jxhMCwodRwvRec8k0QUbZk7RQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-classes": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.24.7.tgz", + "integrity": "sha512-CFbbBigp8ln4FU6Bpy6g7sE8B/WmCmzvivzUC6xDAdWVsjYTXijpuuGJmYkAaoWAzcItGKT3IOAbxRItZ5HTjw==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.24.7", + "@babel/helper-compilation-targets": "^7.24.7", + "@babel/helper-environment-visitor": "^7.24.7", + "@babel/helper-function-name": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-replace-supers": "^7.24.7", + "@babel/helper-split-export-declaration": "^7.24.7", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-classes/node_modules/@babel/helper-annotate-as-pure": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.24.7.tgz", + "integrity": "sha512-BaDeOonYvhdKw+JoMVkAixAAJzG2jVPIwWoKBPdYuY9b452e2rPuI9QPYh3KpofZ3pW2akOmwZLOiOsHMiqRAg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/plugin-transform-classes/node_modules/@babel/helper-split-export-declaration": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.7.tgz", + "integrity": "sha512-oy5V7pD+UvfkEATUKvIjvIAH/xCzfsFVw7ygW2SI6NClZzquT+mwdTfgfdbUiceh6iQO0CHtCPsyze/MZ2YbAA==", + "dev": true, + "dependencies": { + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/plugin-transform-computed-properties": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.24.7.tgz", + "integrity": "sha512-25cS7v+707Gu6Ds2oY6tCkUwsJ9YIDbggd9+cu9jzzDgiNq7hR/8dkzxWfKWnTic26vsI3EsCXNd4iEB6e8esQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/template": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-computed-properties/node_modules/@babel/template": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.24.7.tgz", + "integrity": "sha512-jYqfPrU9JTF0PmPy1tLYHW4Mp4KlgxJD9l2nP9fD6yT/ICi554DmrWBAEYpIelzjHf1msDP3PxJIRt/nFNfBig==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.24.7", + "@babel/parser": "^7.24.7", + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/plugin-transform-destructuring": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.24.7.tgz", + "integrity": "sha512-19eJO/8kdCQ9zISOf+SEUJM/bAUIsvY3YDnXZTupUCQ8LgrWnsG/gFB9dvXqdXnRXMAM8fvt7b0CBKQHNGy1mw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-dotall-regex": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.24.7.tgz", + "integrity": "sha512-ZOA3W+1RRTSWvyqcMJDLqbchh7U4NRGqwRfFSVbOLS/ePIP4vHB5e8T8eXcuqyN1QkgKyj5wuW0lcS85v4CrSw==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-duplicate-keys": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.24.7.tgz", + "integrity": "sha512-JdYfXyCRihAe46jUIliuL2/s0x0wObgwwiGxw/UbgJBr20gQBThrokO4nYKgWkD7uBaqM7+9x5TU7NkExZJyzw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-exponentiation-operator": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.24.7.tgz", + "integrity": "sha512-Rqe/vSc9OYgDajNIK35u7ot+KeCoetqQYFXM4Epf7M7ez3lWlOjrDjrwMei6caCVhfdw+mIKD4cgdGNy5JQotQ==", + "dev": true, + "dependencies": { + "@babel/helper-builder-binary-assignment-operator-visitor": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-for-of": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.24.7.tgz", + "integrity": "sha512-wo9ogrDG1ITTTBsy46oGiN1dS9A7MROBTcYsfS8DtsImMkHk9JXJ3EWQM6X2SUw4x80uGPlwj0o00Uoc6nEE3g==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-function-name": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.24.7.tgz", + "integrity": "sha512-U9FcnA821YoILngSmYkW6FjyQe2TyZD5pHt4EVIhmcTkrJw/3KqcrRSxuOo5tFZJi7TE19iDyI1u+weTI7bn2w==", + "dev": true, + "dependencies": { + "@babel/helper-compilation-targets": "^7.24.7", + "@babel/helper-function-name": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-literals": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.24.7.tgz", + "integrity": "sha512-vcwCbb4HDH+hWi8Pqenwnjy+UiklO4Kt1vfspcQYFhJdpthSnW8XvWGyDZWKNVrVbVViI/S7K9PDJZiUmP2fYQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-member-expression-literals": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.24.7.tgz", + "integrity": "sha512-T/hRC1uqrzXMKLQ6UCwMT85S3EvqaBXDGf0FaMf4446Qx9vKwlghvee0+uuZcDUCZU5RuNi4781UQ7R308zzBw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-amd": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.24.7.tgz", + "integrity": "sha512-9+pB1qxV3vs/8Hdmz/CulFB8w2tuu6EB94JZFsjdqxQokwGa9Unap7Bo2gGBGIvPmDIVvQrom7r5m/TCDMURhg==", + "dev": true, + "dependencies": { + "@babel/helper-module-transforms": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-commonjs": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.24.7.tgz", + "integrity": "sha512-iFI8GDxtevHJ/Z22J5xQpVqFLlMNstcLXh994xifFwxxGslr2ZXXLWgtBeLctOD63UFDArdvN6Tg8RFw+aEmjQ==", + "dev": true, + "dependencies": { + "@babel/helper-module-transforms": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-simple-access": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-systemjs": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.24.7.tgz", + "integrity": "sha512-GYQE0tW7YoaN13qFh3O1NCY4MPkUiAH3fiF7UcV/I3ajmDKEdG3l+UOcbAm4zUE3gnvUU+Eni7XrVKo9eO9auw==", + "dev": true, + "dependencies": { + "@babel/helper-hoist-variables": "^7.24.7", + "@babel/helper-module-transforms": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-validator-identifier": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-umd": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.24.7.tgz", + "integrity": "sha512-3aytQvqJ/h9z4g8AsKPLvD4Zqi2qT+L3j7XoFFu1XBlZWEl2/1kWnhmAbxpLgPrHSY0M6UA02jyTiwUVtiKR6A==", + "dev": true, + "dependencies": { + "@babel/helper-module-transforms": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-named-capturing-groups-regex": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.24.7.tgz", + "integrity": "sha512-/jr7h/EWeJtk1U/uz2jlsCioHkZk1JJZVcc8oQsJ1dUlaJD83f4/6Zeh2aHt9BIFokHIsSeDfhUmju0+1GPd6g==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-new-target": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.24.7.tgz", + "integrity": "sha512-RNKwfRIXg4Ls/8mMTza5oPF5RkOW8Wy/WgMAp1/F1yZ8mMbtwXW+HDoJiOsagWrAhI5f57Vncrmr9XeT4CVapA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-object-super": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.24.7.tgz", + "integrity": "sha512-A/vVLwN6lBrMFmMDmPPz0jnE6ZGx7Jq7d6sT/Ev4H65RER6pZ+kczlf1DthF5N0qaPHBsI7UXiE8Zy66nmAovg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-replace-supers": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-optional-chaining": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.24.7.tgz", + "integrity": "sha512-tK+0N9yd4j+x/4hxF3F0e0fu/VdcxU18y5SevtyM/PCFlQvXbR0Zmlo2eBrKtVipGNFzpq56o8WsIIKcJFUCRQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.24.7", + "@babel/plugin-syntax-optional-chaining": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-parameters": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.24.7.tgz", + "integrity": "sha512-yGWW5Rr+sQOhK0Ot8hjDJuxU3XLRQGflvT4lhlSY0DFvdb3TwKaY26CJzHtYllU0vT9j58hc37ndFPsqT1SrzA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-property-literals": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.24.7.tgz", + "integrity": "sha512-EMi4MLQSHfd2nrCqQEWxFdha2gBCqU4ZcCng4WBGZ5CJL4bBRW0ptdqqDdeirGZcpALazVVNJqRmsO8/+oNCBA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-regenerator": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.24.7.tgz", + "integrity": "sha512-lq3fvXPdimDrlg6LWBoqj+r/DEWgONuwjuOuQCSYgRroXDH/IdM1C0IZf59fL5cHLpjEH/O6opIRBbqv7ELnuA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7", + "regenerator-transform": "^0.15.2" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-reserved-words": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.24.7.tgz", + "integrity": "sha512-0DUq0pHcPKbjFZCfTss/pGkYMfy3vFWydkUBd9r0GHpIyfs2eCDENvqadMycRS9wZCXR41wucAfJHJmwA0UmoQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-runtime": { + "version": "7.19.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.19.6.tgz", + "integrity": "sha512-PRH37lz4JU156lYFW1p8OxE5i7d6Sl/zV58ooyr+q1J1lnQPyg5tIiXlIwNVhJaY4W3TmOtdc8jqdXQcB1v5Yw==", + "dev": true, + "dependencies": { + "@babel/helper-module-imports": "^7.18.6", + "@babel/helper-plugin-utils": "^7.19.0", + "babel-plugin-polyfill-corejs2": "^0.3.3", + "babel-plugin-polyfill-corejs3": "^0.6.0", + "babel-plugin-polyfill-regenerator": "^0.4.1", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-runtime/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/plugin-transform-shorthand-properties": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.24.7.tgz", + "integrity": "sha512-KsDsevZMDsigzbA09+vacnLpmPH4aWjcZjXdyFKGzpplxhbeB4wYtury3vglQkg6KM/xEPKt73eCjPPf1PgXBA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-spread": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.24.7.tgz", + "integrity": "sha512-x96oO0I09dgMDxJaANcRyD4ellXFLLiWhuwDxKZX5g2rWP1bTPkBSwCYv96VDXVT1bD9aPj8tppr5ITIh8hBng==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-sticky-regex": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.24.7.tgz", + "integrity": "sha512-kHPSIJc9v24zEml5geKg9Mjx5ULpfncj0wRpYtxbvKyTtHCYDkVE3aHQ03FrpEo4gEe2vrJJS1Y9CJTaThA52g==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-template-literals": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.24.7.tgz", + "integrity": "sha512-AfDTQmClklHCOLxtGoP7HkeMw56k1/bTQjwsfhL6pppo/M4TOBSq+jjBUBLmV/4oeFg4GWMavIl44ZeCtmmZTw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-typeof-symbol": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.24.7.tgz", + "integrity": "sha512-VtR8hDy7YLB7+Pet9IarXjg/zgCMSF+1mNS/EQEiEaUPoFXCVsHG64SIxcaaI2zJgRiv+YmgaQESUfWAdbjzgg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-escapes": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.24.7.tgz", + "integrity": "sha512-U3ap1gm5+4edc2Q/P+9VrBNhGkfnf+8ZqppY71Bo/pzZmXhhLdqgaUl6cuB07O1+AQJtCLfaOmswiNbSQ9ivhw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-regex": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.24.7.tgz", + "integrity": "sha512-hlQ96MBZSAXUq7ltkjtu3FJCCSMx/j629ns3hA3pXnBXjanNP0LHi+JpPeA81zaWgVK1VGH95Xuy7u0RyQ8kMg==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-env": { + "version": "7.20.2", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.20.2.tgz", + "integrity": "sha512-1G0efQEWR1EHkKvKHqbG+IN/QdgwfByUpM5V5QroDzGV2t3S/WXNQd693cHiHTlCFMpr9B6FkPFXDA2lQcKoDg==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.20.1", + "@babel/helper-compilation-targets": "^7.20.0", + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/helper-validator-option": "^7.18.6", + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.18.6", + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.18.9", + "@babel/plugin-proposal-async-generator-functions": "^7.20.1", + "@babel/plugin-proposal-class-properties": "^7.18.6", + "@babel/plugin-proposal-class-static-block": "^7.18.6", + "@babel/plugin-proposal-dynamic-import": "^7.18.6", + "@babel/plugin-proposal-export-namespace-from": "^7.18.9", + "@babel/plugin-proposal-json-strings": "^7.18.6", + "@babel/plugin-proposal-logical-assignment-operators": "^7.18.9", + "@babel/plugin-proposal-nullish-coalescing-operator": "^7.18.6", + "@babel/plugin-proposal-numeric-separator": "^7.18.6", + "@babel/plugin-proposal-object-rest-spread": "^7.20.2", + "@babel/plugin-proposal-optional-catch-binding": "^7.18.6", + "@babel/plugin-proposal-optional-chaining": "^7.18.9", + "@babel/plugin-proposal-private-methods": "^7.18.6", + "@babel/plugin-proposal-private-property-in-object": "^7.18.6", + "@babel/plugin-proposal-unicode-property-regex": "^7.18.6", + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-dynamic-import": "^7.8.3", + "@babel/plugin-syntax-export-namespace-from": "^7.8.3", + "@babel/plugin-syntax-import-assertions": "^7.20.0", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5", + "@babel/plugin-transform-arrow-functions": "^7.18.6", + "@babel/plugin-transform-async-to-generator": "^7.18.6", + "@babel/plugin-transform-block-scoped-functions": "^7.18.6", + "@babel/plugin-transform-block-scoping": "^7.20.2", + "@babel/plugin-transform-classes": "^7.20.2", + "@babel/plugin-transform-computed-properties": "^7.18.9", + "@babel/plugin-transform-destructuring": "^7.20.2", + "@babel/plugin-transform-dotall-regex": "^7.18.6", + "@babel/plugin-transform-duplicate-keys": "^7.18.9", + "@babel/plugin-transform-exponentiation-operator": "^7.18.6", + "@babel/plugin-transform-for-of": "^7.18.8", + "@babel/plugin-transform-function-name": "^7.18.9", + "@babel/plugin-transform-literals": "^7.18.9", + "@babel/plugin-transform-member-expression-literals": "^7.18.6", + "@babel/plugin-transform-modules-amd": "^7.19.6", + "@babel/plugin-transform-modules-commonjs": "^7.19.6", + "@babel/plugin-transform-modules-systemjs": "^7.19.6", + "@babel/plugin-transform-modules-umd": "^7.18.6", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.19.1", + "@babel/plugin-transform-new-target": "^7.18.6", + "@babel/plugin-transform-object-super": "^7.18.6", + "@babel/plugin-transform-parameters": "^7.20.1", + "@babel/plugin-transform-property-literals": "^7.18.6", + "@babel/plugin-transform-regenerator": "^7.18.6", + "@babel/plugin-transform-reserved-words": "^7.18.6", + "@babel/plugin-transform-shorthand-properties": "^7.18.6", + "@babel/plugin-transform-spread": "^7.19.0", + "@babel/plugin-transform-sticky-regex": "^7.18.6", + "@babel/plugin-transform-template-literals": "^7.18.9", + "@babel/plugin-transform-typeof-symbol": "^7.18.9", + "@babel/plugin-transform-unicode-escapes": "^7.18.10", + "@babel/plugin-transform-unicode-regex": "^7.18.6", + "@babel/preset-modules": "^0.1.5", + "@babel/types": "^7.20.2", + "babel-plugin-polyfill-corejs2": "^0.3.3", + "babel-plugin-polyfill-corejs3": "^0.6.0", + "babel-plugin-polyfill-regenerator": "^0.4.1", + "core-js-compat": "^3.25.1", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-env/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/preset-modules": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.6.tgz", + "integrity": "sha512-ID2yj6K/4lKfhuU3+EX4UvNbIt7eACFbHmNUjzA+ep+B5971CknnA/9DEWKbRokfbbtblxxxXFJJrH47UEAMVg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/plugin-proposal-unicode-property-regex": "^7.4.4", + "@babel/plugin-transform-dotall-regex": "^7.4.4", + "@babel/types": "^7.4.4", + "esutils": "^2.0.2" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/@babel/regjsgen": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@babel/regjsgen/-/regjsgen-0.8.0.tgz", + "integrity": "sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA==", + "dev": true + }, + "node_modules/@babel/runtime": { + "version": "7.20.13", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.20.13.tgz", + "integrity": "sha512-gt3PKXs0DBoL9xCvOIIZ2NEqAGZqHjAnmVbfQtB620V0uReIQutpel14KcneZuer7UioY8ALKZ7iocavvzTNFA==", + "dev": true, + "dependencies": { + "regenerator-runtime": "^0.13.11" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.20.7.tgz", + "integrity": "sha512-8SegXApWe6VoNw0r9JHpSteLKTpTiLZ4rMlGIm9JQ18KiCtyQiAMEazujAHrUS5flrcqYZa75ukev3P6QmUwUw==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.18.6", + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.24.7.tgz", + "integrity": "sha512-yb65Ed5S/QAcewNPh0nZczy9JdYXkkAbIsEo+P7BE7yO3txAY30Y/oPa3QkQ5It3xVG2kpKMg9MsdxZaO31uKA==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.24.7", + "@babel/generator": "^7.24.7", + "@babel/helper-environment-visitor": "^7.24.7", + "@babel/helper-function-name": "^7.24.7", + "@babel/helper-hoist-variables": "^7.24.7", + "@babel/helper-split-export-declaration": "^7.24.7", + "@babel/parser": "^7.24.7", + "@babel/types": "^7.24.7", + "debug": "^4.3.1", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse/node_modules/@babel/generator": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.24.7.tgz", + "integrity": "sha512-oipXieGC3i45Y1A41t4tAqpnEZWgB/lC6Ehh6+rOviR5XWpTtMmLN+fGjz9vOiNRt0p6RtO6DtD0pdU3vpqdSA==", + "dev": true, + "dependencies": { + "@babel/types": "^7.24.7", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", + "jsesc": "^2.5.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse/node_modules/@babel/helper-split-export-declaration": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.7.tgz", + "integrity": "sha512-oy5V7pD+UvfkEATUKvIjvIAH/xCzfsFVw7ygW2SI6NClZzquT+mwdTfgfdbUiceh6iQO0CHtCPsyze/MZ2YbAA==", + "dev": true, + "dependencies": { + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse/node_modules/@jridgewell/gen-mapping": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", + "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", + "dev": true, + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.7.tgz", + "integrity": "sha512-XEFXSlxiG5td2EJRe8vOmRbaXVgfcBlszKujvVmWIK/UpywWljQCfzAv3RQCGujWQ1RD4YYWEAqDXfuJiy8f5Q==", + "dev": true, + "dependencies": { + "@babel/helper-string-parser": "^7.24.7", + "@babel/helper-validator-identifier": "^7.24.7", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@braintree/sanitize-url": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/@braintree/sanitize-url/-/sanitize-url-6.0.4.tgz", + "integrity": "sha512-s3jaWicZd0pkP0jf5ysyHUI/RE7MHos6qlToFcGWXVp+ykHOy77OUMrfbgJ9it2C5bow7OIQwYYaHjk9XlBQ2A==", + "optional": true + }, + "node_modules/@colors/colors": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", + "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==", + "dev": true, + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/@dabh/diagnostics": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.3.tgz", + "integrity": "sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==", + "dependencies": { + "colorspace": "1.1.x", + "enabled": "2.0.x", + "kuler": "^2.0.0" + } + }, + "node_modules/@discoveryjs/json-ext": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz", + "integrity": "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==", + "dev": true, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.17.8", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.17.8.tgz", + "integrity": "sha512-0/rb91GYKhrtbeglJXOhAv9RuYimgI8h623TplY2X+vA4EXnk3Zj1fXZreJ0J3OJJu1bwmb0W7g+2cT/d8/l/w==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.17.8", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.17.8.tgz", + "integrity": "sha512-oa/N5j6v1svZQs7EIRPqR8f+Bf8g6HBDjD/xHC02radE/NjKHK7oQmtmLxPs1iVwYyvE+Kolo6lbpfEQ9xnhxQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.17.8", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.17.8.tgz", + "integrity": "sha512-bTliMLqD7pTOoPg4zZkXqCDuzIUguEWLpeqkNfC41ODBHwoUgZ2w5JBeYimv4oP6TDVocoYmEhZrCLQTrH89bg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.17.8", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.17.8.tgz", + "integrity": "sha512-ghAbV3ia2zybEefXRRm7+lx8J/rnupZT0gp9CaGy/3iolEXkJ6LYRq4IpQVI9zR97ID80KJVoUlo3LSeA/sMAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.17.8", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.17.8.tgz", + "integrity": "sha512-n5WOpyvZ9TIdv2V1K3/iIkkJeKmUpKaCTdun9buhGRWfH//osmUjlv4Z5mmWdPWind/VGcVxTHtLfLCOohsOXw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.17.8", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.17.8.tgz", + "integrity": "sha512-a/SATTaOhPIPFWvHZDoZYgxaZRVHn0/LX1fHLGfZ6C13JqFUZ3K6SMD6/HCtwOQ8HnsNaEeokdiDSFLuizqv5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.17.8", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.17.8.tgz", + "integrity": "sha512-xpFJb08dfXr5+rZc4E+ooZmayBW6R3q59daCpKZ/cDU96/kvDM+vkYzNeTJCGd8rtO6fHWMq5Rcv/1cY6p6/0Q==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.17.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.17.8.tgz", + "integrity": "sha512-6Ij8gfuGszcEwZpi5jQIJCVIACLS8Tz2chnEBfYjlmMzVsfqBP1iGmHQPp7JSnZg5xxK9tjCc+pJ2WtAmPRFVA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.17.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.17.8.tgz", + "integrity": "sha512-v3iwDQuDljLTxpsqQDl3fl/yihjPAyOguxuloON9kFHYwopeJEf1BkDXODzYyXEI19gisEsQlG1bM65YqKSIww==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.17.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.17.8.tgz", + "integrity": "sha512-8svILYKhE5XetuFk/B6raFYIyIqydQi+GngEXJgdPdI7OMKUbSd7uzR02wSY4kb53xBrClLkhH4Xs8P61Q2BaA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.17.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.17.8.tgz", + "integrity": "sha512-B6FyMeRJeV0NpyEOYlm5qtQfxbdlgmiGdD+QsipzKfFky0K5HW5Td6dyK3L3ypu1eY4kOmo7wW0o94SBqlqBSA==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.17.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.17.8.tgz", + "integrity": "sha512-CCb67RKahNobjm/eeEqeD/oJfJlrWyw29fgiyB6vcgyq97YAf3gCOuP6qMShYSPXgnlZe/i4a8WFHBw6N8bYAA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.17.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.17.8.tgz", + "integrity": "sha512-bytLJOi55y55+mGSdgwZ5qBm0K9WOCh0rx+vavVPx+gqLLhxtSFU0XbeYy/dsAAD6xECGEv4IQeFILaSS2auXw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.17.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.17.8.tgz", + "integrity": "sha512-2YpRyQJmKVBEHSBLa8kBAtbhucaclb6ex4wchfY0Tj3Kg39kpjeJ9vhRU7x4mUpq8ISLXRXH1L0dBYjAeqzZAw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.17.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.17.8.tgz", + "integrity": "sha512-QgbNY/V3IFXvNf11SS6exkpVcX0LJcob+0RWCgV9OiDAmVElnxciHIisoSix9uzYzScPmS6dJFbZULdSAEkQVw==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.17.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.17.8.tgz", + "integrity": "sha512-mM/9S0SbAFDBc4OPoyP6SEOo5324LpUxdpeIUUSrSTOfhHU9hEfqRngmKgqILqwx/0DVJBzeNW7HmLEWp9vcOA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.17.8", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.17.8.tgz", + "integrity": "sha512-eKUYcWaWTaYr9zbj8GertdVtlt1DTS1gNBWov+iQfWuWyuu59YN6gSEJvFzC5ESJ4kMcKR0uqWThKUn5o8We6Q==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.17.8", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.17.8.tgz", + "integrity": "sha512-Vc9J4dXOboDyMXKD0eCeW0SIeEzr8K9oTHJU+Ci1mZc5njPfhKAqkRt3B/fUNU7dP+mRyralPu8QUkiaQn7iIg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.17.8", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.17.8.tgz", + "integrity": "sha512-0xvOTNuPXI7ft1LYUgiaXtpCEjp90RuBBYovdd2lqAFxje4sEucurg30M1WIm03+3jxByd3mfo+VUmPtRSVuOw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.17.8", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.17.8.tgz", + "integrity": "sha512-G0JQwUI5WdEFEnYNKzklxtBheCPkuDdu1YrtRrjuQv30WsYbkkoixKxLLv8qhJmNI+ATEWquZe/N0d0rpr55Mg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.17.8", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.17.8.tgz", + "integrity": "sha512-Fqy63515xl20OHGFykjJsMnoIWS+38fqfg88ClvPXyDbLtgXal2DTlhb1TfTX34qWi3u4I7Cq563QcHpqgLx8w==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.17.8", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.17.8.tgz", + "integrity": "sha512-1iuezdyDNngPnz8rLRDO2C/ZZ/emJLb72OsZeqQ6gL6Avko/XCXZw+NuxBSNhBAP13Hie418V7VMt9et1FMvpg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@firebase/analytics": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@firebase/analytics/-/analytics-0.10.0.tgz", + "integrity": "sha512-Locv8gAqx0e+GX/0SI3dzmBY5e9kjVDtD+3zCFLJ0tH2hJwuCAiL+5WkHuxKj92rqQj/rvkBUCfA1ewlX2hehg==", + "dependencies": { + "@firebase/component": "0.6.4", + "@firebase/installations": "0.6.4", + "@firebase/logger": "0.4.0", + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/analytics-compat": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/@firebase/analytics-compat/-/analytics-compat-0.2.6.tgz", + "integrity": "sha512-4MqpVLFkGK7NJf/5wPEEP7ePBJatwYpyjgJ+wQHQGHfzaCDgntOnl9rL2vbVGGKCnRqWtZDIWhctB86UWXaX2Q==", + "dependencies": { + "@firebase/analytics": "0.10.0", + "@firebase/analytics-types": "0.8.0", + "@firebase/component": "0.6.4", + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/analytics-types": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@firebase/analytics-types/-/analytics-types-0.8.0.tgz", + "integrity": "sha512-iRP+QKI2+oz3UAh4nPEq14CsEjrjD6a5+fuypjScisAh9kXKFvdJOZJDwk7kikLvWVLGEs9+kIUS4LPQV7VZVw==" + }, + "node_modules/@firebase/app": { + "version": "0.9.13", + "resolved": "https://registry.npmjs.org/@firebase/app/-/app-0.9.13.tgz", + "integrity": "sha512-GfiI1JxJ7ecluEmDjPzseRXk/PX31hS7+tjgBopL7XjB2hLUdR+0FTMXy2Q3/hXezypDvU6or7gVFizDESrkXw==", + "dependencies": { + "@firebase/component": "0.6.4", + "@firebase/logger": "0.4.0", + "@firebase/util": "1.9.3", + "idb": "7.1.1", + "tslib": "^2.1.0" + } + }, + "node_modules/@firebase/app-check": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@firebase/app-check/-/app-check-0.8.0.tgz", + "integrity": "sha512-dRDnhkcaC2FspMiRK/Vbp+PfsOAEP6ZElGm9iGFJ9fDqHoPs0HOPn7dwpJ51lCFi1+2/7n5pRPGhqF/F03I97g==", + "dependencies": { + "@firebase/component": "0.6.4", + "@firebase/logger": "0.4.0", + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/app-check-compat": { + "version": "0.3.7", + "resolved": "https://registry.npmjs.org/@firebase/app-check-compat/-/app-check-compat-0.3.7.tgz", + "integrity": "sha512-cW682AxsyP1G+Z0/P7pO/WT2CzYlNxoNe5QejVarW2o5ZxeWSSPAiVEwpEpQR/bUlUmdeWThYTMvBWaopdBsqw==", + "dependencies": { + "@firebase/app-check": "0.8.0", + "@firebase/app-check-types": "0.5.0", + "@firebase/component": "0.6.4", + "@firebase/logger": "0.4.0", + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/app-check-interop-types": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@firebase/app-check-interop-types/-/app-check-interop-types-0.3.0.tgz", + "integrity": "sha512-xAxHPZPIgFXnI+vb4sbBjZcde7ZluzPPaSK7Lx3/nmuVk4TjZvnL8ONnkd4ERQKL8WePQySU+pRcWkh8rDf5Sg==" + }, + "node_modules/@firebase/app-check-types": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@firebase/app-check-types/-/app-check-types-0.5.0.tgz", + "integrity": "sha512-uwSUj32Mlubybw7tedRzR24RP8M8JUVR3NPiMk3/Z4bCmgEKTlQBwMXrehDAZ2wF+TsBq0SN1c6ema71U/JPyQ==" + }, + "node_modules/@firebase/app-compat": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/@firebase/app-compat/-/app-compat-0.2.13.tgz", + "integrity": "sha512-j6ANZaWjeVy5zg6X7uiqh6lM6o3n3LD1+/SJFNs9V781xyryyZWXe+tmnWNWPkP086QfJoNkWN9pMQRqSG4vMg==", + "dependencies": { + "@firebase/app": "0.9.13", + "@firebase/component": "0.6.4", + "@firebase/logger": "0.4.0", + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" + } + }, + "node_modules/@firebase/app-types": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@firebase/app-types/-/app-types-0.9.0.tgz", + "integrity": "sha512-AeweANOIo0Mb8GiYm3xhTEBVCmPwTYAu9Hcd2qSkLuga/6+j9b1Jskl5bpiSQWy9eJ/j5pavxj6eYogmnuzm+Q==" + }, + "node_modules/@firebase/auth": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/@firebase/auth/-/auth-0.23.2.tgz", + "integrity": "sha512-dM9iJ0R6tI1JczuGSxXmQbXAgtYie0K4WvKcuyuSTCu9V8eEDiz4tfa1sO3txsfvwg7nOY3AjoCyMYEdqZ8hdg==", + "dependencies": { + "@firebase/component": "0.6.4", + "@firebase/logger": "0.4.0", + "@firebase/util": "1.9.3", + "node-fetch": "2.6.7", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/auth-compat": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@firebase/auth-compat/-/auth-compat-0.4.2.tgz", + "integrity": "sha512-Q30e77DWXFmXEt5dg5JbqEDpjw9y3/PcP9LslDPR7fARmAOTIY9MM6HXzm9KC+dlrKH/+p6l8g9ifJiam9mc4A==", + "dependencies": { + "@firebase/auth": "0.23.2", + "@firebase/auth-types": "0.12.0", + "@firebase/component": "0.6.4", + "@firebase/util": "1.9.3", + "node-fetch": "2.6.7", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/auth-compat/node_modules/node-fetch": { + "version": "2.6.7", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", + "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/@firebase/auth-interop-types": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@firebase/auth-interop-types/-/auth-interop-types-0.2.1.tgz", + "integrity": "sha512-VOaGzKp65MY6P5FI84TfYKBXEPi6LmOCSMMzys6o2BN2LOsqy7pCuZCup7NYnfbk5OkkQKzvIfHOzTm0UDpkyg==" + }, + "node_modules/@firebase/auth-types": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@firebase/auth-types/-/auth-types-0.12.0.tgz", + "integrity": "sha512-pPwaZt+SPOshK8xNoiQlK5XIrS97kFYc3Rc7xmy373QsOJ9MmqXxLaYssP5Kcds4wd2qK//amx/c+A8O2fVeZA==", + "peerDependencies": { + "@firebase/app-types": "0.x", + "@firebase/util": "1.x" + } + }, + "node_modules/@firebase/auth/node_modules/node-fetch": { + "version": "2.6.7", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", + "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/@firebase/component": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.6.4.tgz", + "integrity": "sha512-rLMyrXuO9jcAUCaQXCMjCMUsWrba5fzHlNK24xz5j2W6A/SRmK8mZJ/hn7V0fViLbxC0lPMtrK1eYzk6Fg03jA==", + "dependencies": { + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" + } + }, + "node_modules/@firebase/database": { + "version": "0.14.4", + "resolved": "https://registry.npmjs.org/@firebase/database/-/database-0.14.4.tgz", + "integrity": "sha512-+Ea/IKGwh42jwdjCyzTmeZeLM3oy1h0mFPsTy6OqCWzcu/KFqRAr5Tt1HRCOBlNOdbh84JPZC47WLU18n2VbxQ==", + "dependencies": { + "@firebase/auth-interop-types": "0.2.1", + "@firebase/component": "0.6.4", + "@firebase/logger": "0.4.0", + "@firebase/util": "1.9.3", + "faye-websocket": "0.11.4", + "tslib": "^2.1.0" + } + }, + "node_modules/@firebase/database-compat": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@firebase/database-compat/-/database-compat-0.3.4.tgz", + "integrity": "sha512-kuAW+l+sLMUKBThnvxvUZ+Q1ZrF/vFJ58iUY9kAcbX48U03nVzIF6Tmkf0p3WVQwMqiXguSgtOPIB6ZCeF+5Gg==", + "dependencies": { + "@firebase/component": "0.6.4", + "@firebase/database": "0.14.4", + "@firebase/database-types": "0.10.4", + "@firebase/logger": "0.4.0", + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" + } + }, + "node_modules/@firebase/database-types": { + "version": "0.10.4", + "resolved": "https://registry.npmjs.org/@firebase/database-types/-/database-types-0.10.4.tgz", + "integrity": "sha512-dPySn0vJ/89ZeBac70T+2tWWPiJXWbmRygYv0smT5TfE3hDrQ09eKMF3Y+vMlTdrMWq7mUdYW5REWPSGH4kAZQ==", + "dependencies": { + "@firebase/app-types": "0.9.0", + "@firebase/util": "1.9.3" + } + }, + "node_modules/@firebase/firestore": { + "version": "3.13.0", + "resolved": "https://registry.npmjs.org/@firebase/firestore/-/firestore-3.13.0.tgz", + "integrity": "sha512-NwcnU+madJXQ4fbLkGx1bWvL612IJN/qO6bZ6dlPmyf7QRyu5azUosijdAN675r+bOOJxMtP1Bv981bHBXAbUg==", + "dependencies": { + "@firebase/component": "0.6.4", + "@firebase/logger": "0.4.0", + "@firebase/util": "1.9.3", + "@firebase/webchannel-wrapper": "0.10.1", + "@grpc/grpc-js": "~1.7.0", + "@grpc/proto-loader": "^0.6.13", + "node-fetch": "2.6.7", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=10.10.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/firestore-compat": { + "version": "0.3.12", + "resolved": "https://registry.npmjs.org/@firebase/firestore-compat/-/firestore-compat-0.3.12.tgz", + "integrity": "sha512-mazuNGAx5Kt9Nph0pm6ULJFp/+j7GSsx+Ncw1GrnKl+ft1CQ4q2LcUssXnjqkX2Ry0fNGqUzC1mfIUrk9bYtjQ==", + "dependencies": { + "@firebase/component": "0.6.4", + "@firebase/firestore": "3.13.0", + "@firebase/firestore-types": "2.5.1", + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/firestore-types": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@firebase/firestore-types/-/firestore-types-2.5.1.tgz", + "integrity": "sha512-xG0CA6EMfYo8YeUxC8FeDzf6W3FX1cLlcAGBYV6Cku12sZRI81oWcu61RSKM66K6kUENP+78Qm8mvroBcm1whw==", + "peerDependencies": { + "@firebase/app-types": "0.x", + "@firebase/util": "1.x" + } + }, + "node_modules/@firebase/firestore/node_modules/node-fetch": { + "version": "2.6.7", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", + "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/@firebase/functions": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@firebase/functions/-/functions-0.10.0.tgz", + "integrity": "sha512-2U+fMNxTYhtwSpkkR6WbBcuNMOVaI7MaH3cZ6UAeNfj7AgEwHwMIFLPpC13YNZhno219F0lfxzTAA0N62ndWzA==", + "dependencies": { + "@firebase/app-check-interop-types": "0.3.0", + "@firebase/auth-interop-types": "0.2.1", + "@firebase/component": "0.6.4", + "@firebase/messaging-interop-types": "0.2.0", + "@firebase/util": "1.9.3", + "node-fetch": "2.6.7", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/functions-compat": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@firebase/functions-compat/-/functions-compat-0.3.5.tgz", + "integrity": "sha512-uD4jwgwVqdWf6uc3NRKF8cSZ0JwGqSlyhPgackyUPe+GAtnERpS4+Vr66g0b3Gge0ezG4iyHo/EXW/Hjx7QhHw==", + "dependencies": { + "@firebase/component": "0.6.4", + "@firebase/functions": "0.10.0", + "@firebase/functions-types": "0.6.0", + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/functions-types": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@firebase/functions-types/-/functions-types-0.6.0.tgz", + "integrity": "sha512-hfEw5VJtgWXIRf92ImLkgENqpL6IWpYaXVYiRkFY1jJ9+6tIhWM7IzzwbevwIIud/jaxKVdRzD7QBWfPmkwCYw==" + }, + "node_modules/@firebase/functions/node_modules/node-fetch": { + "version": "2.6.7", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", + "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/@firebase/installations": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/@firebase/installations/-/installations-0.6.4.tgz", + "integrity": "sha512-u5y88rtsp7NYkCHC3ElbFBrPtieUybZluXyzl7+4BsIz4sqb4vSAuwHEUgCgCeaQhvsnxDEU6icly8U9zsJigA==", + "dependencies": { + "@firebase/component": "0.6.4", + "@firebase/util": "1.9.3", + "idb": "7.0.1", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/installations-compat": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@firebase/installations-compat/-/installations-compat-0.2.4.tgz", + "integrity": "sha512-LI9dYjp0aT9Njkn9U4JRrDqQ6KXeAmFbRC0E7jI7+hxl5YmRWysq5qgQl22hcWpTk+cm3es66d/apoDU/A9n6Q==", + "dependencies": { + "@firebase/component": "0.6.4", + "@firebase/installations": "0.6.4", + "@firebase/installations-types": "0.5.0", + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/installations-types": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@firebase/installations-types/-/installations-types-0.5.0.tgz", + "integrity": "sha512-9DP+RGfzoI2jH7gY4SlzqvZ+hr7gYzPODrbzVD82Y12kScZ6ZpRg/i3j6rleto8vTFC8n6Len4560FnV1w2IRg==", + "peerDependencies": { + "@firebase/app-types": "0.x" + } + }, + "node_modules/@firebase/installations/node_modules/idb": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/idb/-/idb-7.0.1.tgz", + "integrity": "sha512-UUxlE7vGWK5RfB/fDwEGgRf84DY/ieqNha6msMV99UsEMQhJ1RwbCd8AYBj3QMgnE3VZnfQvm4oKVCJTYlqIgg==" + }, + "node_modules/@firebase/logger": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@firebase/logger/-/logger-0.4.0.tgz", + "integrity": "sha512-eRKSeykumZ5+cJPdxxJRgAC3G5NknY2GwEbKfymdnXtnT0Ucm4pspfR6GT4MUQEDuJwRVbVcSx85kgJulMoFFA==", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@firebase/messaging": { + "version": "0.12.4", + "resolved": "https://registry.npmjs.org/@firebase/messaging/-/messaging-0.12.4.tgz", + "integrity": "sha512-6JLZct6zUaex4g7HI3QbzeUrg9xcnmDAPTWpkoMpd/GoSVWH98zDoWXMGrcvHeCAIsLpFMe4MPoZkJbrPhaASw==", + "dependencies": { + "@firebase/component": "0.6.4", + "@firebase/installations": "0.6.4", + "@firebase/messaging-interop-types": "0.2.0", + "@firebase/util": "1.9.3", + "idb": "7.0.1", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/messaging-compat": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@firebase/messaging-compat/-/messaging-compat-0.2.4.tgz", + "integrity": "sha512-lyFjeUhIsPRYDPNIkYX1LcZMpoVbBWXX4rPl7c/rqc7G+EUea7IEtSt4MxTvh6fDfPuzLn7+FZADfscC+tNMfg==", + "dependencies": { + "@firebase/component": "0.6.4", + "@firebase/messaging": "0.12.4", + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/messaging-interop-types": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@firebase/messaging-interop-types/-/messaging-interop-types-0.2.0.tgz", + "integrity": "sha512-ujA8dcRuVeBixGR9CtegfpU4YmZf3Lt7QYkcj693FFannwNuZgfAYaTmbJ40dtjB81SAu6tbFPL9YLNT15KmOQ==" + }, + "node_modules/@firebase/messaging/node_modules/idb": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/idb/-/idb-7.0.1.tgz", + "integrity": "sha512-UUxlE7vGWK5RfB/fDwEGgRf84DY/ieqNha6msMV99UsEMQhJ1RwbCd8AYBj3QMgnE3VZnfQvm4oKVCJTYlqIgg==" + }, + "node_modules/@firebase/performance": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/@firebase/performance/-/performance-0.6.4.tgz", + "integrity": "sha512-HfTn/bd8mfy/61vEqaBelNiNnvAbUtME2S25A67Nb34zVuCSCRIX4SseXY6zBnOFj3oLisaEqhVcJmVPAej67g==", + "dependencies": { + "@firebase/component": "0.6.4", + "@firebase/installations": "0.6.4", + "@firebase/logger": "0.4.0", + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/performance-compat": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@firebase/performance-compat/-/performance-compat-0.2.4.tgz", + "integrity": "sha512-nnHUb8uP9G8islzcld/k6Bg5RhX62VpbAb/Anj7IXs/hp32Eb2LqFPZK4sy3pKkBUO5wcrlRWQa6wKOxqlUqsg==", + "dependencies": { + "@firebase/component": "0.6.4", + "@firebase/logger": "0.4.0", + "@firebase/performance": "0.6.4", + "@firebase/performance-types": "0.2.0", + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/performance-types": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@firebase/performance-types/-/performance-types-0.2.0.tgz", + "integrity": "sha512-kYrbr8e/CYr1KLrLYZZt2noNnf+pRwDq2KK9Au9jHrBMnb0/C9X9yWSXmZkFt4UIdsQknBq8uBB7fsybZdOBTA==" + }, + "node_modules/@firebase/remote-config": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/@firebase/remote-config/-/remote-config-0.4.4.tgz", + "integrity": "sha512-x1ioTHGX8ZwDSTOVp8PBLv2/wfwKzb4pxi0gFezS5GCJwbLlloUH4YYZHHS83IPxnua8b6l0IXUaWd0RgbWwzQ==", + "dependencies": { + "@firebase/component": "0.6.4", + "@firebase/installations": "0.6.4", + "@firebase/logger": "0.4.0", + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/remote-config-compat": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@firebase/remote-config-compat/-/remote-config-compat-0.2.4.tgz", + "integrity": "sha512-FKiki53jZirrDFkBHglB3C07j5wBpitAaj8kLME6g8Mx+aq7u9P7qfmuSRytiOItADhWUj7O1JIv7n9q87SuwA==", + "dependencies": { + "@firebase/component": "0.6.4", + "@firebase/logger": "0.4.0", + "@firebase/remote-config": "0.4.4", + "@firebase/remote-config-types": "0.3.0", + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/remote-config-types": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@firebase/remote-config-types/-/remote-config-types-0.3.0.tgz", + "integrity": "sha512-RtEH4vdcbXZuZWRZbIRmQVBNsE7VDQpet2qFvq6vwKLBIQRQR5Kh58M4ok3A3US8Sr3rubYnaGqZSurCwI8uMA==" + }, + "node_modules/@firebase/storage": { + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/@firebase/storage/-/storage-0.11.2.tgz", + "integrity": "sha512-CtvoFaBI4hGXlXbaCHf8humajkbXhs39Nbh6MbNxtwJiCqxPy9iH3D3CCfXAvP0QvAAwmJUTK3+z9a++Kc4nkA==", + "dependencies": { + "@firebase/component": "0.6.4", + "@firebase/util": "1.9.3", + "node-fetch": "2.6.7", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/storage-compat": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@firebase/storage-compat/-/storage-compat-0.3.2.tgz", + "integrity": "sha512-wvsXlLa9DVOMQJckbDNhXKKxRNNewyUhhbXev3t8kSgoCotd1v3MmqhKKz93ePhDnhHnDs7bYHy+Qa8dRY6BXw==", + "dependencies": { + "@firebase/component": "0.6.4", + "@firebase/storage": "0.11.2", + "@firebase/storage-types": "0.8.0", + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/storage-types": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@firebase/storage-types/-/storage-types-0.8.0.tgz", + "integrity": "sha512-isRHcGrTs9kITJC0AVehHfpraWFui39MPaU7Eo8QfWlqW7YPymBmRgjDrlOgFdURh6Cdeg07zmkLP5tzTKRSpg==", + "peerDependencies": { + "@firebase/app-types": "0.x", + "@firebase/util": "1.x" + } + }, + "node_modules/@firebase/storage/node_modules/node-fetch": { + "version": "2.6.7", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", + "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/@firebase/util": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/@firebase/util/-/util-1.9.3.tgz", + "integrity": "sha512-DY02CRhOZwpzO36fHpuVysz6JZrscPiBXD0fXp6qSrL9oNOx5KWICKdR95C0lSITzxp0TZosVyHqzatE8JbcjA==", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@firebase/webchannel-wrapper": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@firebase/webchannel-wrapper/-/webchannel-wrapper-0.10.1.tgz", + "integrity": "sha512-Dq5rYfEpdeel0bLVN+nfD1VWmzCkK+pJbSjIawGE+RY4+NIJqhbUDDQjvV0NUK84fMfwxvtFoCtEe70HfZjFcw==" + }, + "node_modules/@fortawesome/fontawesome-free": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-6.6.0.tgz", + "integrity": "sha512-60G28ke/sXdtS9KZCpZSHHkCbdsOGEhIUGlwq6yhY74UpTiToIh8np7A8yphhM4BWsvNFtIvLpi4co+h9Mr9Ow==", + "engines": { + "node": ">=6" + } + }, + "node_modules/@gar/promisify": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz", + "integrity": "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==", + "dev": true + }, + "node_modules/@grpc/grpc-js": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.7.3.tgz", + "integrity": "sha512-H9l79u4kJ2PVSxUNA08HMYAnUBLj9v6KjYQ7SQ71hOZcEXhShE/y5iQCesP8+6/Ik/7i2O0a10bPquIcYfufog==", + "dependencies": { + "@grpc/proto-loader": "^0.7.0", + "@types/node": ">=12.12.47" + }, + "engines": { + "node": "^8.13.0 || >=10.10.0" + } + }, + "node_modules/@grpc/grpc-js/node_modules/@grpc/proto-loader": { + "version": "0.7.13", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.13.tgz", + "integrity": "sha512-AiXO/bfe9bmxBjxxtYxFAXGZvMaN5s8kO+jBHAJCON8rJoB5YS/D6X7ZNc6XQkuHNmyl4CYaMI1fJ/Gn27RGGw==", + "dependencies": { + "lodash.camelcase": "^4.3.0", + "long": "^5.0.0", + "protobufjs": "^7.2.5", + "yargs": "^17.7.2" + }, + "bin": { + "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@grpc/grpc-js/node_modules/long": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/long/-/long-5.2.3.tgz", + "integrity": "sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==" + }, + "node_modules/@grpc/grpc-js/node_modules/protobufjs": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.3.2.tgz", + "integrity": "sha512-RXyHaACeqXeqAKGLDl68rQKbmObRsTIn4TYVUUug1KfS47YWCo5MacGITEryugIgZqORCvJWEk4l449POg5Txg==", + "hasInstallScript": true, + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/node": ">=13.7.0", + "long": "^5.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@grpc/grpc-js/node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@grpc/proto-loader": { + "version": "0.6.13", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.6.13.tgz", + "integrity": "sha512-FjxPYDRTn6Ec3V0arm1FtSpmP6V50wuph2yILpyvTKzjc76oDdoihXqM1DzOW5ubvCC8GivfCnNtfaRE8myJ7g==", + "dependencies": { + "@types/long": "^4.0.1", + "lodash.camelcase": "^4.3.0", + "long": "^4.0.0", + "protobufjs": "^6.11.3", + "yargs": "^16.2.0" + }, + "bin": { + "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@grpc/proto-loader/node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/@grpc/proto-loader/node_modules/yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@grpc/proto-loader/node_modules/yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "engines": { + "node": ">=10" + } + }, + "node_modules/@ionic/angular": { + "version": "7.8.6", + "resolved": "https://registry.npmjs.org/@ionic/angular/-/angular-7.8.6.tgz", + "integrity": "sha512-3Qe53hXpyjtx6fFcxt/NTAlauIawsGmCZJPauV5sAnSKVuX8C82C1zMAZTeJt6m2dnd71wythc98BXUXsx/UxQ==", + "dependencies": { + "@ionic/core": "7.8.6", + "ionicons": "^7.0.0", + "jsonc-parser": "^3.0.0", + "tslib": "^2.3.0" + }, + "peerDependencies": { + "@angular/core": ">=14.0.0", + "@angular/forms": ">=14.0.0", + "@angular/router": ">=14.0.0", + "rxjs": ">=7.5.0", + "zone.js": ">=0.11.0" + } + }, + "node_modules/@ionic/core": { + "version": "7.8.6", + "resolved": "https://registry.npmjs.org/@ionic/core/-/core-7.8.6.tgz", + "integrity": "sha512-HAYZdEmeJgOdo2kDlZkcCGHb+zs/vjU6iv4skbVBL7y+OnSv/oC2u83Yee8S3/aY0YAxkyBgu7hLTYH13Zc2Aw==", + "dependencies": { + "@stencil/core": "^4.12.2", + "ionicons": "^7.2.2", + "tslib": "^2.1.0" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "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/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "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/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "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/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.1.1.tgz", + "integrity": "sha512-sQXCasFk+U8lWYEe66WxRDOE9PjVz4vSM51fTu3Hw+ClTpUSQb718772vH3pyS5pShp6lvQM7SxgIDXXXmOX7w==", + "dev": true, + "dependencies": { + "@jridgewell/set-array": "^1.0.0", + "@jridgewell/sourcemap-codec": "^1.4.10" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.6.tgz", + "integrity": "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, + "node_modules/@jridgewell/source-map/node_modules/@jridgewell/gen-mapping": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", + "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.15", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@leichtgewicht/ip-codec": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz", + "integrity": "sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==", + "dev": true + }, + "node_modules/@material/animation": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/animation/-/animation-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-5osi1z4JQIXcklPALbH/zTfOm2pDzHt9Fxm7ZyURy250xIZj6QjULRzPTnzOhC2ropfix9ra2Cfggbf0dcRbEQ==", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@material/auto-init": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/auto-init/-/auto-init-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-OigQTmrVzkcGvxNjOaIe5oItTFPgrO9xLewvharDI6m6yvO1z7OBnkcW+sFN6ggLNYNxd0O1u9v64vMsmeDABQ==", + "dependencies": { + "@material/base": "15.0.0-canary.684e33d25.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/banner": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/banner/-/banner-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-PqtGp3KWzdu58rWv/DIvSfe38m5YKOBbAAbBinSvgadBb/da+IE1t5F7YPNKE1T5lJsQBGVUYx6QBIeXm+aI/A==", + "dependencies": { + "@material/base": "15.0.0-canary.684e33d25.0", + "@material/button": "15.0.0-canary.684e33d25.0", + "@material/dom": "15.0.0-canary.684e33d25.0", + "@material/elevation": "15.0.0-canary.684e33d25.0", + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "@material/ripple": "15.0.0-canary.684e33d25.0", + "@material/rtl": "15.0.0-canary.684e33d25.0", + "@material/shape": "15.0.0-canary.684e33d25.0", + "@material/theme": "15.0.0-canary.684e33d25.0", + "@material/tokens": "15.0.0-canary.684e33d25.0", + "@material/typography": "15.0.0-canary.684e33d25.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/base": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/base/-/base-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-oOaqb/SfjWwTKsdJUZmeh/Qrs41nIJI0N+zELsxnvbGjSIN1ZMAKYZFPMahqvC68OJ6+5CvJM8PoTNs5l+B8IQ==", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@material/button": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/button/-/button-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-Nkekk4edeX+ObVOa7UlwavaHdmckPV5wU4SAJf3iA3R61cmz+KsgAgpzfcwv5WfNhIlc2nLu8QYEecpHdo9d/w==", + "dependencies": { + "@material/density": "15.0.0-canary.684e33d25.0", + "@material/dom": "15.0.0-canary.684e33d25.0", + "@material/elevation": "15.0.0-canary.684e33d25.0", + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "@material/focus-ring": "15.0.0-canary.684e33d25.0", + "@material/ripple": "15.0.0-canary.684e33d25.0", + "@material/rtl": "15.0.0-canary.684e33d25.0", + "@material/shape": "15.0.0-canary.684e33d25.0", + "@material/theme": "15.0.0-canary.684e33d25.0", + "@material/tokens": "15.0.0-canary.684e33d25.0", + "@material/touch-target": "15.0.0-canary.684e33d25.0", + "@material/typography": "15.0.0-canary.684e33d25.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/card": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/card/-/card-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-xhyB7XX5KkEiCEqwSPkl58ZGYL6xFdnY62zimyBXJRG/Eaa0Swj3kW20hVCpt4f7c9Zmp8Se27rg8vnKmhvO3g==", + "dependencies": { + "@material/dom": "15.0.0-canary.684e33d25.0", + "@material/elevation": "15.0.0-canary.684e33d25.0", + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "@material/ripple": "15.0.0-canary.684e33d25.0", + "@material/rtl": "15.0.0-canary.684e33d25.0", + "@material/shape": "15.0.0-canary.684e33d25.0", + "@material/theme": "15.0.0-canary.684e33d25.0", + "@material/tokens": "15.0.0-canary.684e33d25.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/checkbox": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/checkbox/-/checkbox-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-NFpM3TS924PmVsk2KQLNU95OYCf8ZwYgzeqfnAexU0bEfjUJXINBun2Go0AaeOUMjuvWUe+byjrXgv8SFYbMUA==", + "dependencies": { + "@material/animation": "15.0.0-canary.684e33d25.0", + "@material/base": "15.0.0-canary.684e33d25.0", + "@material/density": "15.0.0-canary.684e33d25.0", + "@material/dom": "15.0.0-canary.684e33d25.0", + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "@material/focus-ring": "15.0.0-canary.684e33d25.0", + "@material/ripple": "15.0.0-canary.684e33d25.0", + "@material/theme": "15.0.0-canary.684e33d25.0", + "@material/touch-target": "15.0.0-canary.684e33d25.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/chips": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/chips/-/chips-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-z4ajQ4NnsAQ/Si9tZ4xmxzjj2Qb+vW++4QjCjjjwAGIZbCe0xglAnMh2t66XLJUxt7RoKZuZVEO7ZqcFZpvJFQ==", + "dependencies": { + "@material/animation": "15.0.0-canary.684e33d25.0", + "@material/base": "15.0.0-canary.684e33d25.0", + "@material/checkbox": "15.0.0-canary.684e33d25.0", + "@material/density": "15.0.0-canary.684e33d25.0", + "@material/dom": "15.0.0-canary.684e33d25.0", + "@material/elevation": "15.0.0-canary.684e33d25.0", + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "@material/focus-ring": "15.0.0-canary.684e33d25.0", + "@material/ripple": "15.0.0-canary.684e33d25.0", + "@material/rtl": "15.0.0-canary.684e33d25.0", + "@material/shape": "15.0.0-canary.684e33d25.0", + "@material/theme": "15.0.0-canary.684e33d25.0", + "@material/tokens": "15.0.0-canary.684e33d25.0", + "@material/touch-target": "15.0.0-canary.684e33d25.0", + "@material/typography": "15.0.0-canary.684e33d25.0", + "safevalues": "^0.3.4", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/circular-progress": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/circular-progress/-/circular-progress-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-G6qD0nGNtEUwWnAMJuA9INYFpZoKtx7KFjBaPF4Ol2YLHtmShALNAYyn54TMAK8AZ2IpW08PXjGS7Ye88vrdEQ==", + "dependencies": { + "@material/animation": "15.0.0-canary.684e33d25.0", + "@material/base": "15.0.0-canary.684e33d25.0", + "@material/dom": "15.0.0-canary.684e33d25.0", + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "@material/progress-indicator": "15.0.0-canary.684e33d25.0", + "@material/rtl": "15.0.0-canary.684e33d25.0", + "@material/theme": "15.0.0-canary.684e33d25.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/data-table": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/data-table/-/data-table-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-+wDw1DDDFfAsKAMzs84f/5GCjux39zjNfW8tL4wFbkWNwewmQrG9zaQMJhBpVOtLCrM8Gj6SOgOANqgqoCjvGg==", + "dependencies": { + "@material/animation": "15.0.0-canary.684e33d25.0", + "@material/base": "15.0.0-canary.684e33d25.0", + "@material/checkbox": "15.0.0-canary.684e33d25.0", + "@material/density": "15.0.0-canary.684e33d25.0", + "@material/dom": "15.0.0-canary.684e33d25.0", + "@material/elevation": "15.0.0-canary.684e33d25.0", + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "@material/icon-button": "15.0.0-canary.684e33d25.0", + "@material/linear-progress": "15.0.0-canary.684e33d25.0", + "@material/list": "15.0.0-canary.684e33d25.0", + "@material/menu": "15.0.0-canary.684e33d25.0", + "@material/rtl": "15.0.0-canary.684e33d25.0", + "@material/select": "15.0.0-canary.684e33d25.0", + "@material/shape": "15.0.0-canary.684e33d25.0", + "@material/theme": "15.0.0-canary.684e33d25.0", + "@material/tokens": "15.0.0-canary.684e33d25.0", + "@material/touch-target": "15.0.0-canary.684e33d25.0", + "@material/typography": "15.0.0-canary.684e33d25.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/density": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/density/-/density-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-661yEVRMGrlq6S6WuSbPRO+ZwpdUOg2glCc7y96doM6itSLOa3UEAldjOLfsYZVB74GnKCiuDp//QmfoRyYTfA==", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@material/dialog": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/dialog/-/dialog-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-szn0dHnfeQTSOC6SSRSGAzX6Tnx+4NnSMUwNkXm+3bwjds8ZVK26+DXwLrP5f3ID5F1K5sFsRf2INo5/TNTHyQ==", + "dependencies": { + "@material/animation": "15.0.0-canary.684e33d25.0", + "@material/base": "15.0.0-canary.684e33d25.0", + "@material/button": "15.0.0-canary.684e33d25.0", + "@material/dom": "15.0.0-canary.684e33d25.0", + "@material/elevation": "15.0.0-canary.684e33d25.0", + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "@material/icon-button": "15.0.0-canary.684e33d25.0", + "@material/ripple": "15.0.0-canary.684e33d25.0", + "@material/rtl": "15.0.0-canary.684e33d25.0", + "@material/shape": "15.0.0-canary.684e33d25.0", + "@material/theme": "15.0.0-canary.684e33d25.0", + "@material/tokens": "15.0.0-canary.684e33d25.0", + "@material/touch-target": "15.0.0-canary.684e33d25.0", + "@material/typography": "15.0.0-canary.684e33d25.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/dom": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/dom/-/dom-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-7pEJLYov+tGgfuD8mZxoVU6rWtPI8ppjTAhz+F27Hz9FG0JETMWTKpDPBXLnKvX7vhIxL83GvZ9geNHCe8Hfog==", + "dependencies": { + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "@material/rtl": "15.0.0-canary.684e33d25.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/drawer": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/drawer/-/drawer-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-/KMckLf1PYU/H3PXnS4e0aFl03qG3JlSv4LGgX6juJufcONqGTl/m63EMO/L/eUy6H1CRrXmVDjik/jzHLyDhg==", + "dependencies": { + "@material/animation": "15.0.0-canary.684e33d25.0", + "@material/base": "15.0.0-canary.684e33d25.0", + "@material/dom": "15.0.0-canary.684e33d25.0", + "@material/elevation": "15.0.0-canary.684e33d25.0", + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "@material/list": "15.0.0-canary.684e33d25.0", + "@material/ripple": "15.0.0-canary.684e33d25.0", + "@material/rtl": "15.0.0-canary.684e33d25.0", + "@material/shape": "15.0.0-canary.684e33d25.0", + "@material/theme": "15.0.0-canary.684e33d25.0", + "@material/typography": "15.0.0-canary.684e33d25.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/elevation": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/elevation/-/elevation-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-WDF8SsRtq3rXUbVVbd9K4DUijIPH0bUFSOreVYxudpuxAfTlDS5+aeS1EK9UIBFYLuba4u5wVT2tDv6e1RTfrQ==", + "dependencies": { + "@material/animation": "15.0.0-canary.684e33d25.0", + "@material/base": "15.0.0-canary.684e33d25.0", + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "@material/rtl": "15.0.0-canary.684e33d25.0", + "@material/theme": "15.0.0-canary.684e33d25.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/fab": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/fab/-/fab-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-KCu87rWOKEAe9vZcAm6K8XazYSWPNjMG+OhrbPjHW6bCO7as1YCgtmkBkhff7csY/rFmcVpIy884xtUfLmSudQ==", + "dependencies": { + "@material/animation": "15.0.0-canary.684e33d25.0", + "@material/dom": "15.0.0-canary.684e33d25.0", + "@material/elevation": "15.0.0-canary.684e33d25.0", + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "@material/focus-ring": "15.0.0-canary.684e33d25.0", + "@material/ripple": "15.0.0-canary.684e33d25.0", + "@material/rtl": "15.0.0-canary.684e33d25.0", + "@material/shape": "15.0.0-canary.684e33d25.0", + "@material/theme": "15.0.0-canary.684e33d25.0", + "@material/tokens": "15.0.0-canary.684e33d25.0", + "@material/touch-target": "15.0.0-canary.684e33d25.0", + "@material/typography": "15.0.0-canary.684e33d25.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/feature-targeting": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/feature-targeting/-/feature-targeting-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-HyH1erNTSjS63sigNSUMaCd0nJhTNdDFeC+myrxwtDaQm+uYJ8troCNtQM3g6mx0XATNtX5aTOoPmrM6yVVi1A==", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@material/floating-label": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/floating-label/-/floating-label-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-f7TPp6bKpGvV3sYYiZHSGlrixXKkXXITW3Esp7KB9jRq42c0H82novmdwvY0eTef4ootmA2JEysr78KQfHBUPg==", + "dependencies": { + "@material/animation": "15.0.0-canary.684e33d25.0", + "@material/base": "15.0.0-canary.684e33d25.0", + "@material/dom": "15.0.0-canary.684e33d25.0", + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "@material/rtl": "15.0.0-canary.684e33d25.0", + "@material/theme": "15.0.0-canary.684e33d25.0", + "@material/typography": "15.0.0-canary.684e33d25.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/focus-ring": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/focus-ring/-/focus-ring-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-ikw2RVUfgzXChpWIzPH1VzRvTjYb5ZKj4H+CZf7jqPUXMstFOZg90Bp7ARLZHqYiyNMuUq3zUTHozS6iHorSqg==", + "dependencies": { + "@material/dom": "15.0.0-canary.684e33d25.0", + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "@material/rtl": "15.0.0-canary.684e33d25.0" + } + }, + "node_modules/@material/form-field": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/form-field/-/form-field-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-vpF9N/uq5no/7+8GAbEH0868FhOuBgxAWRr1Sfb+jthKfBr8OS/wPU/AHzZHdHdAm7PQynbeOXfDsX2dI//PDA==", + "dependencies": { + "@material/base": "15.0.0-canary.684e33d25.0", + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "@material/ripple": "15.0.0-canary.684e33d25.0", + "@material/rtl": "15.0.0-canary.684e33d25.0", + "@material/theme": "15.0.0-canary.684e33d25.0", + "@material/typography": "15.0.0-canary.684e33d25.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/icon-button": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/icon-button/-/icon-button-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-wMI+XGzmIN/o2ePBKg2hLyx7H4pXCRAyyIKMQS1FMp1UKa2tYmiHVX/V8skhKwCqxg3i6Ls/LxMjfPxTR18WvQ==", + "dependencies": { + "@material/base": "15.0.0-canary.684e33d25.0", + "@material/density": "15.0.0-canary.684e33d25.0", + "@material/dom": "15.0.0-canary.684e33d25.0", + "@material/elevation": "15.0.0-canary.684e33d25.0", + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "@material/focus-ring": "15.0.0-canary.684e33d25.0", + "@material/ripple": "15.0.0-canary.684e33d25.0", + "@material/rtl": "15.0.0-canary.684e33d25.0", + "@material/theme": "15.0.0-canary.684e33d25.0", + "@material/touch-target": "15.0.0-canary.684e33d25.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/image-list": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/image-list/-/image-list-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-Ol+uaHYBe5R/cgzlfh5ONnMVX0wO6fV74JMUcQCQlxP6lXau/edARo4tkRc7A7UJUkU3VRv0EpEjLoCRNUPGaA==", + "dependencies": { + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "@material/shape": "15.0.0-canary.684e33d25.0", + "@material/theme": "15.0.0-canary.684e33d25.0", + "@material/typography": "15.0.0-canary.684e33d25.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/layout-grid": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/layout-grid/-/layout-grid-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-ALXE1mqFNb/RB2lVRQ3/r1Aufw2mFZnOjRE+boYDVepmAG/xWyPCyaGoavELJF5l4GAb0tXi8wA/8HeGbLOpuA==", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@material/line-ripple": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/line-ripple/-/line-ripple-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-7hRx8C/e9i0P6pgQpNOMfTwSS2r1fwEvBL72QDVGLtLuoKKwsjjgP6Z0Jat/GeHJe87u9LQvGBoD4upt+of/HA==", + "dependencies": { + "@material/animation": "15.0.0-canary.684e33d25.0", + "@material/base": "15.0.0-canary.684e33d25.0", + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "@material/theme": "15.0.0-canary.684e33d25.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/linear-progress": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/linear-progress/-/linear-progress-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-iJclt7mKmcMk6pqD7ocXKfCWZhqBoODp7N593jYlxVpTJuEz2wiVAjZUDn/YGj/Uz3CRH+2YFfOiLr9pwWjhDg==", + "dependencies": { + "@material/animation": "15.0.0-canary.684e33d25.0", + "@material/base": "15.0.0-canary.684e33d25.0", + "@material/dom": "15.0.0-canary.684e33d25.0", + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "@material/progress-indicator": "15.0.0-canary.684e33d25.0", + "@material/rtl": "15.0.0-canary.684e33d25.0", + "@material/theme": "15.0.0-canary.684e33d25.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/list": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/list/-/list-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-rQ+FCSdzmwTcT00IYE0uRV3CS4oGSccKFl9hkcF+aHFW61L7ORh/SCGUDPrEfQFrFkMn5f8qroVJjpUAMXBz4g==", + "dependencies": { + "@material/base": "15.0.0-canary.684e33d25.0", + "@material/density": "15.0.0-canary.684e33d25.0", + "@material/dom": "15.0.0-canary.684e33d25.0", + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "@material/ripple": "15.0.0-canary.684e33d25.0", + "@material/rtl": "15.0.0-canary.684e33d25.0", + "@material/shape": "15.0.0-canary.684e33d25.0", + "@material/theme": "15.0.0-canary.684e33d25.0", + "@material/tokens": "15.0.0-canary.684e33d25.0", + "@material/typography": "15.0.0-canary.684e33d25.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/menu": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/menu/-/menu-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-r7wzDLSGSI9629/mfpvsMzkVxpmV75kcD3IrW0Pcu6/Bv/1xi0EvjcUXzNJJoQlwN4Zj35Ymz/PCjZkIDIz68Q==", + "dependencies": { + "@material/base": "15.0.0-canary.684e33d25.0", + "@material/dom": "15.0.0-canary.684e33d25.0", + "@material/elevation": "15.0.0-canary.684e33d25.0", + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "@material/list": "15.0.0-canary.684e33d25.0", + "@material/menu-surface": "15.0.0-canary.684e33d25.0", + "@material/ripple": "15.0.0-canary.684e33d25.0", + "@material/rtl": "15.0.0-canary.684e33d25.0", + "@material/shape": "15.0.0-canary.684e33d25.0", + "@material/theme": "15.0.0-canary.684e33d25.0", + "@material/tokens": "15.0.0-canary.684e33d25.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/menu-surface": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/menu-surface/-/menu-surface-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-RVO5GAYcfWPaKwxsF/NhUAmrYXQCQBKvRQW0TIlbmAJz6lcFeTs6YZqF3u1C7qrL3ZQGz+sur/7ywj6QU0oMow==", + "dependencies": { + "@material/animation": "15.0.0-canary.684e33d25.0", + "@material/base": "15.0.0-canary.684e33d25.0", + "@material/elevation": "15.0.0-canary.684e33d25.0", + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "@material/rtl": "15.0.0-canary.684e33d25.0", + "@material/shape": "15.0.0-canary.684e33d25.0", + "@material/theme": "15.0.0-canary.684e33d25.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/notched-outline": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/notched-outline/-/notched-outline-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-9YHcBkvJLPVYzkHcWoTpBZAFrEd+j1hjhGxLhh0LuNrZe8VroUkZD1TTnUAPHRG3os6EqEWWaKb0RN+aPIF2yQ==", + "dependencies": { + "@material/base": "15.0.0-canary.684e33d25.0", + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "@material/floating-label": "15.0.0-canary.684e33d25.0", + "@material/rtl": "15.0.0-canary.684e33d25.0", + "@material/shape": "15.0.0-canary.684e33d25.0", + "@material/theme": "15.0.0-canary.684e33d25.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/progress-indicator": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/progress-indicator/-/progress-indicator-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-c0icji4faeNWUoqGENGC7Hav0Puxh0RwXIDVizffaUxKIGbajpIp5+4Zop73fK/xFLGMB/npg7TbP+aCGjQ3fw==", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@material/radio": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/radio/-/radio-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-U3Eh8sNUA8trDla1Bq8Bo02foxYvtoewaKeF8A8tAju81XZ4jRiftfOsOWZDZEHCVbbCB2QwvutvFlnay5n+Aw==", + "dependencies": { + "@material/animation": "15.0.0-canary.684e33d25.0", + "@material/base": "15.0.0-canary.684e33d25.0", + "@material/density": "15.0.0-canary.684e33d25.0", + "@material/dom": "15.0.0-canary.684e33d25.0", + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "@material/focus-ring": "15.0.0-canary.684e33d25.0", + "@material/ripple": "15.0.0-canary.684e33d25.0", + "@material/theme": "15.0.0-canary.684e33d25.0", + "@material/touch-target": "15.0.0-canary.684e33d25.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/ripple": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/ripple/-/ripple-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-RyePu7SjIm/OuyyEieZ/gxiPYkNZOZHeid72WRcN9ofdlljj2pifcdPvcfZA+v/DMS33xo5GjG2L/Qj6ClWrKw==", + "dependencies": { + "@material/animation": "15.0.0-canary.684e33d25.0", + "@material/base": "15.0.0-canary.684e33d25.0", + "@material/dom": "15.0.0-canary.684e33d25.0", + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "@material/rtl": "15.0.0-canary.684e33d25.0", + "@material/theme": "15.0.0-canary.684e33d25.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/rtl": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/rtl/-/rtl-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-NqdJl8Ayupp1Th+vCNCpVQHbUFOuF7TCte9LD1norTIBUF/QizIxWby2W5uUEiPbnh5j9PmE1CJtfLwKun3pcw==", + "dependencies": { + "@material/theme": "15.0.0-canary.684e33d25.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/segmented-button": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/segmented-button/-/segmented-button-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-bEGgg8vgXNLyukyV8HRjFMuQ6t6nm5LQ4Pgm22um61Yc8qyi0BOqV41OR4SVdUrUqZxh1aVD+p+4NN03+LfQXw==", + "dependencies": { + "@material/base": "15.0.0-canary.684e33d25.0", + "@material/elevation": "15.0.0-canary.684e33d25.0", + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "@material/ripple": "15.0.0-canary.684e33d25.0", + "@material/theme": "15.0.0-canary.684e33d25.0", + "@material/touch-target": "15.0.0-canary.684e33d25.0", + "@material/typography": "15.0.0-canary.684e33d25.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/select": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/select/-/select-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-kf178/2TeEinTv0mgmSBcmmExQ2h7a7dtR1E3WuqQgisJ/R6+zVLMkC2CnfIyzxYX2vkuUTG0ue3Reh/6XiqSg==", + "dependencies": { + "@material/animation": "15.0.0-canary.684e33d25.0", + "@material/base": "15.0.0-canary.684e33d25.0", + "@material/density": "15.0.0-canary.684e33d25.0", + "@material/dom": "15.0.0-canary.684e33d25.0", + "@material/elevation": "15.0.0-canary.684e33d25.0", + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "@material/floating-label": "15.0.0-canary.684e33d25.0", + "@material/line-ripple": "15.0.0-canary.684e33d25.0", + "@material/list": "15.0.0-canary.684e33d25.0", + "@material/menu": "15.0.0-canary.684e33d25.0", + "@material/menu-surface": "15.0.0-canary.684e33d25.0", + "@material/notched-outline": "15.0.0-canary.684e33d25.0", + "@material/ripple": "15.0.0-canary.684e33d25.0", + "@material/rtl": "15.0.0-canary.684e33d25.0", + "@material/shape": "15.0.0-canary.684e33d25.0", + "@material/theme": "15.0.0-canary.684e33d25.0", + "@material/tokens": "15.0.0-canary.684e33d25.0", + "@material/typography": "15.0.0-canary.684e33d25.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/shape": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/shape/-/shape-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-aEelpaTFmpnCji3TUGP9bVCS/bRVjUmLTHBPZtuu1gOrUVVtJ6kYOg73dZNJF+XOoNL2yOX/LRcKwsop29tptA==", + "dependencies": { + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "@material/rtl": "15.0.0-canary.684e33d25.0", + "@material/theme": "15.0.0-canary.684e33d25.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/slider": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/slider/-/slider-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-WVyK+2pSNSZmj07M2K/a3TADoQ9FBCndfNC/vE7/wGIg4dddJJK5KvQ+yruf9R2cSzTL/S1sZ5WpyyeM8E9HTw==", + "dependencies": { + "@material/animation": "15.0.0-canary.684e33d25.0", + "@material/base": "15.0.0-canary.684e33d25.0", + "@material/dom": "15.0.0-canary.684e33d25.0", + "@material/elevation": "15.0.0-canary.684e33d25.0", + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "@material/ripple": "15.0.0-canary.684e33d25.0", + "@material/rtl": "15.0.0-canary.684e33d25.0", + "@material/theme": "15.0.0-canary.684e33d25.0", + "@material/tokens": "15.0.0-canary.684e33d25.0", + "@material/typography": "15.0.0-canary.684e33d25.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/snackbar": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/snackbar/-/snackbar-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-itO+DCkOannZzR1/cCHcqAm7ifhuFvXmDItNoA8qLEcAyJDJJRkhpwj3XQ01yuo9gBFcSctp7Txt7e+Hncm/Jg==", + "dependencies": { + "@material/animation": "15.0.0-canary.684e33d25.0", + "@material/base": "15.0.0-canary.684e33d25.0", + "@material/button": "15.0.0-canary.684e33d25.0", + "@material/dom": "15.0.0-canary.684e33d25.0", + "@material/elevation": "15.0.0-canary.684e33d25.0", + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "@material/icon-button": "15.0.0-canary.684e33d25.0", + "@material/ripple": "15.0.0-canary.684e33d25.0", + "@material/rtl": "15.0.0-canary.684e33d25.0", + "@material/shape": "15.0.0-canary.684e33d25.0", + "@material/theme": "15.0.0-canary.684e33d25.0", + "@material/tokens": "15.0.0-canary.684e33d25.0", + "@material/typography": "15.0.0-canary.684e33d25.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/switch": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/switch/-/switch-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-Jxi0gl92yvvZZsAPxvVHzXx2ga+T/djMow98jvEczmpUorWnAhgiCr9CsSSRoosahWyRB8NLZOxUQrACxvffjw==", + "dependencies": { + "@material/animation": "15.0.0-canary.684e33d25.0", + "@material/base": "15.0.0-canary.684e33d25.0", + "@material/density": "15.0.0-canary.684e33d25.0", + "@material/dom": "15.0.0-canary.684e33d25.0", + "@material/elevation": "15.0.0-canary.684e33d25.0", + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "@material/focus-ring": "15.0.0-canary.684e33d25.0", + "@material/ripple": "15.0.0-canary.684e33d25.0", + "@material/rtl": "15.0.0-canary.684e33d25.0", + "@material/shape": "15.0.0-canary.684e33d25.0", + "@material/theme": "15.0.0-canary.684e33d25.0", + "@material/tokens": "15.0.0-canary.684e33d25.0", + "safevalues": "^0.3.4", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/tab": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/tab/-/tab-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-WQL3wj9syHNcfe8KbgGGUcA34M8C/xZ+n0Fkkh8Kk6puVwaU+xqUNihsxPY6YzKpmh4PZ4oJaBdiN8zvFT1zqQ==", + "dependencies": { + "@material/base": "15.0.0-canary.684e33d25.0", + "@material/elevation": "15.0.0-canary.684e33d25.0", + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "@material/focus-ring": "15.0.0-canary.684e33d25.0", + "@material/ripple": "15.0.0-canary.684e33d25.0", + "@material/rtl": "15.0.0-canary.684e33d25.0", + "@material/tab-indicator": "15.0.0-canary.684e33d25.0", + "@material/theme": "15.0.0-canary.684e33d25.0", + "@material/tokens": "15.0.0-canary.684e33d25.0", + "@material/typography": "15.0.0-canary.684e33d25.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/tab-bar": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/tab-bar/-/tab-bar-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-SW/cMaDsIGGkM1ag3A7GJRlmr8eXmObWsvitQJzh6Azr5zzZtSI+GQygkMesAEE1gbpqOVN8d40rh3H7VVIAcA==", + "dependencies": { + "@material/animation": "15.0.0-canary.684e33d25.0", + "@material/base": "15.0.0-canary.684e33d25.0", + "@material/density": "15.0.0-canary.684e33d25.0", + "@material/elevation": "15.0.0-canary.684e33d25.0", + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "@material/tab": "15.0.0-canary.684e33d25.0", + "@material/tab-indicator": "15.0.0-canary.684e33d25.0", + "@material/tab-scroller": "15.0.0-canary.684e33d25.0", + "@material/theme": "15.0.0-canary.684e33d25.0", + "@material/tokens": "15.0.0-canary.684e33d25.0", + "@material/typography": "15.0.0-canary.684e33d25.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/tab-indicator": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/tab-indicator/-/tab-indicator-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-kKICqSPqOlaf0lzaFFCmuOqPXJC+cK48Qmsc+m5o6fJhkmuZRCYpIwB2JeP+uZSOq/bTH+SrPtCtnVlgWg6ksA==", + "dependencies": { + "@material/animation": "15.0.0-canary.684e33d25.0", + "@material/base": "15.0.0-canary.684e33d25.0", + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "@material/theme": "15.0.0-canary.684e33d25.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/tab-scroller": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/tab-scroller/-/tab-scroller-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-H6EU/TSiK/M2DyyORX5GEtXD9rKYxTMHC2VxsNWARPMFJGzgeW2ugYkFv+rKI1/c0bs0CJ4e+qFnOlBsQXZvyQ==", + "dependencies": { + "@material/animation": "15.0.0-canary.684e33d25.0", + "@material/base": "15.0.0-canary.684e33d25.0", + "@material/dom": "15.0.0-canary.684e33d25.0", + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "@material/tab": "15.0.0-canary.684e33d25.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/textfield": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/textfield/-/textfield-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-OvgpDXjvpyJTtAWskO69IDybFvDNzr9w2PN/Fk7yFm+uNVupaWz1Ew8lZ4gGslaTNSVmh2XcsvmzxcLINSiiNg==", + "dependencies": { + "@material/animation": "15.0.0-canary.684e33d25.0", + "@material/base": "15.0.0-canary.684e33d25.0", + "@material/density": "15.0.0-canary.684e33d25.0", + "@material/dom": "15.0.0-canary.684e33d25.0", + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "@material/floating-label": "15.0.0-canary.684e33d25.0", + "@material/line-ripple": "15.0.0-canary.684e33d25.0", + "@material/notched-outline": "15.0.0-canary.684e33d25.0", + "@material/ripple": "15.0.0-canary.684e33d25.0", + "@material/rtl": "15.0.0-canary.684e33d25.0", + "@material/shape": "15.0.0-canary.684e33d25.0", + "@material/theme": "15.0.0-canary.684e33d25.0", + "@material/tokens": "15.0.0-canary.684e33d25.0", + "@material/typography": "15.0.0-canary.684e33d25.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/theme": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/theme/-/theme-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-AZxaXXAvRKzAi20RlMxzt2U5UmkCWyv7DMWEBXsxtG5Tk54mi1HsbVUp3fxDPTlmL7Pq8p1/DESg/o7TgRCVlw==", + "dependencies": { + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/tokens": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/tokens/-/tokens-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-wVwbQOTCXDPKYPdHQHLr026y36MMFelID1CmbfRk6mSol4O8yE9U0fXcShfRDW8Qo5E3X31w9c2A6T3neJY7wQ==", + "dependencies": { + "@material/elevation": "15.0.0-canary.684e33d25.0" + } + }, + "node_modules/@material/tooltip": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/tooltip/-/tooltip-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-dtm26QjxyQdinc8btgz6yys07b7bUW4FZgNF2EBPeGrICrPg7jf+JEvDziz5g8VMaTBQLOQRSCGy0MKuRlOjLw==", + "dependencies": { + "@material/animation": "15.0.0-canary.684e33d25.0", + "@material/base": "15.0.0-canary.684e33d25.0", + "@material/button": "15.0.0-canary.684e33d25.0", + "@material/dom": "15.0.0-canary.684e33d25.0", + "@material/elevation": "15.0.0-canary.684e33d25.0", + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "@material/rtl": "15.0.0-canary.684e33d25.0", + "@material/shape": "15.0.0-canary.684e33d25.0", + "@material/theme": "15.0.0-canary.684e33d25.0", + "@material/tokens": "15.0.0-canary.684e33d25.0", + "@material/typography": "15.0.0-canary.684e33d25.0", + "safevalues": "^0.3.4", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/top-app-bar": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/top-app-bar/-/top-app-bar-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-1M+oupUxflfW7u81P1XlxoLZB8bLzwtpKofIfDNRbEsiKhlLTERJR3Yak3BGE9xakNMysAaBHlkb5MrN5bNPFw==", + "dependencies": { + "@material/animation": "15.0.0-canary.684e33d25.0", + "@material/base": "15.0.0-canary.684e33d25.0", + "@material/elevation": "15.0.0-canary.684e33d25.0", + "@material/ripple": "15.0.0-canary.684e33d25.0", + "@material/rtl": "15.0.0-canary.684e33d25.0", + "@material/shape": "15.0.0-canary.684e33d25.0", + "@material/theme": "15.0.0-canary.684e33d25.0", + "@material/typography": "15.0.0-canary.684e33d25.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/touch-target": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/touch-target/-/touch-target-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-zdE69Slg8+T7sTn1OwqZ6H7WBYac9mxJ/JlJqfTqthzIjZRcCxBSYymQJcDHjsrPnUojOtr9U4Tpm5YZ96TEkQ==", + "dependencies": { + "@material/base": "15.0.0-canary.684e33d25.0", + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "@material/rtl": "15.0.0-canary.684e33d25.0", + "@material/theme": "15.0.0-canary.684e33d25.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/typography": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/typography/-/typography-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-aVnvgMwcfNa/K4wujzpKDIxjGl2hbkEL+m+OKDSQqWYjKcP9QrbzCXJruJBqxrBoPRHLbqo47k5f9uT8raSgjw==", + "dependencies": { + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "@material/theme": "15.0.0-canary.684e33d25.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@ng-idle/core": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/@ng-idle/core/-/core-14.0.0.tgz", + "integrity": "sha512-DqseA+F6LXm/Hr4D8zQhDBliuAU06t9BOUpcdSUKQWdDEKo3mlovRxz7DMuXyzg4OBQ0saQpE3MeMqakIEHUHw==", + "dependencies": { + "tslib": "^2.0.0" + }, + "peerDependencies": { + "@angular/common": ">=9.0.0", + "@angular/core": ">=9.0.0" + } + }, + "node_modules/@ngtools/webpack": { + "version": "15.2.11", + "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-15.2.11.tgz", + "integrity": "sha512-yqp+FziuJ+wIVij4eTqfhuiTPNaG1PU8ukeGOdqkVH4nQMlmzs9UldXy1iYC/6swzn6XO/pkqisU3m/jxemMzA==", + "dev": true, + "engines": { + "node": "^14.20.0 || ^16.13.0 || >=18.10.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "@angular/compiler-cli": "^15.0.0", + "typescript": ">=4.8.2 <5.0", + "webpack": "^5.54.0" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@npmcli/fs": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-3.1.1.tgz", + "integrity": "sha512-q9CRWjpHCMIh5sVyefoD1cA7PkvILqCZsnSOEUUivORLjxCO/Irmue2DprETiNgEqktDBZaM1Bi+jrarx1XdCg==", + "dev": true, + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/git": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@npmcli/git/-/git-4.1.0.tgz", + "integrity": "sha512-9hwoB3gStVfa0N31ymBmrX+GuDGdVA/QWShZVqE0HK2Af+7QGGrCTbZia/SW0ImUTjTne7SP91qxDmtXvDHRPQ==", + "dev": true, + "dependencies": { + "@npmcli/promise-spawn": "^6.0.0", + "lru-cache": "^7.4.4", + "npm-pick-manifest": "^8.0.0", + "proc-log": "^3.0.0", + "promise-inflight": "^1.0.1", + "promise-retry": "^2.0.1", + "semver": "^7.3.5", + "which": "^3.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/git/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/@npmcli/git/node_modules/which": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/which/-/which-3.0.1.tgz", + "integrity": "sha512-XA1b62dzQzLfaEOSQFTCOd5KFf/1VSzZo7/7TUjnya6u0vGGKzU96UQBZTAThCb2j4/xjBAyii1OhRLJEivHvg==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/installed-package-contents": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@npmcli/installed-package-contents/-/installed-package-contents-2.1.0.tgz", + "integrity": "sha512-c8UuGLeZpm69BryRykLuKRyKFZYJsZSCT4aVY5ds4omyZqJ172ApzgfKJ5eV/r3HgLdUYgFVe54KSFVjKoe27w==", + "dev": true, + "dependencies": { + "npm-bundled": "^3.0.0", + "npm-normalize-package-bin": "^3.0.0" + }, + "bin": { + "installed-package-contents": "bin/index.js" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/move-file": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@npmcli/move-file/-/move-file-2.0.1.tgz", + "integrity": "sha512-mJd2Z5TjYWq/ttPLLGqArdtnC74J6bOzg4rMDnN+p1xTacZ2yPRCk2y0oSWQtygLR9YVQXgOcONrwtnk3JupxQ==", + "deprecated": "This functionality has been moved to @npmcli/fs", + "dev": true, + "dependencies": { + "mkdirp": "^1.0.4", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/@npmcli/move-file/node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true, + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@npmcli/node-gyp": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/node-gyp/-/node-gyp-3.0.0.tgz", + "integrity": "sha512-gp8pRXC2oOxu0DUE1/M3bYtb1b3/DbJ5aM113+XJBgfXdussRAsX0YOrOhdd8WvnAR6auDBvJomGAkLKA5ydxA==", + "dev": true, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/promise-spawn": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@npmcli/promise-spawn/-/promise-spawn-6.0.2.tgz", + "integrity": "sha512-gGq0NJkIGSwdbUt4yhdF8ZrmkGKVz9vAdVzpOfnom+V8PLSmSOVhZwbNvZZS1EYcJN5hzzKBxmmVVAInM6HQLg==", + "dev": true, + "dependencies": { + "which": "^3.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/promise-spawn/node_modules/which": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/which/-/which-3.0.1.tgz", + "integrity": "sha512-XA1b62dzQzLfaEOSQFTCOd5KFf/1VSzZo7/7TUjnya6u0vGGKzU96UQBZTAThCb2j4/xjBAyii1OhRLJEivHvg==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/run-script": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@npmcli/run-script/-/run-script-6.0.2.tgz", + "integrity": "sha512-NCcr1uQo1k5U+SYlnIrbAh3cxy+OQT1VtqiAbxdymSlptbzBb62AjH2xXgjNCoP073hoa1CfCAcwoZ8k96C4nA==", + "dev": true, + "dependencies": { + "@npmcli/node-gyp": "^3.0.0", + "@npmcli/promise-spawn": "^6.0.0", + "node-gyp": "^9.0.0", + "read-package-json-fast": "^3.0.0", + "which": "^3.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/run-script/node_modules/which": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/which/-/which-3.0.1.tgz", + "integrity": "sha512-XA1b62dzQzLfaEOSQFTCOd5KFf/1VSzZo7/7TUjnya6u0vGGKzU96UQBZTAThCb2j4/xjBAyii1OhRLJEivHvg==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==" + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==" + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==" + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==" + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", + "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1", + "@protobufjs/inquire": "^1.1.0" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==" + }, + "node_modules/@protobufjs/inquire": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", + "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==" + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==" + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==" + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", + "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==" + }, + "node_modules/@schematics/angular": { + "version": "15.1.6", + "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-15.1.6.tgz", + "integrity": "sha512-y2kIQ1wJL0wR6v/LM5+PFJUivrYtdaIJVRdOXLLWl0AB5aLwObiWgLzAuBsbGm/9//WPPhw9PglS5EFFxTBDzg==", + "dependencies": { + "@angular-devkit/core": "15.1.6", + "@angular-devkit/schematics": "15.1.6", + "jsonc-parser": "3.2.0" + }, + "engines": { + "node": "^14.20.0 || ^16.13.0 || >=18.10.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@schematics/angular/node_modules/@angular-devkit/core": { + "version": "15.1.6", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-15.1.6.tgz", + "integrity": "sha512-jGgxyRjecVf6lEyqDxz7ltMEndNPxIg720pk6r40fgsu0dU8w9vjJSJe7k0XdJiXVRcN6wZa/J5nO/xcwWVIsA==", + "dependencies": { + "ajv": "8.12.0", + "ajv-formats": "2.1.1", + "jsonc-parser": "3.2.0", + "rxjs": "6.6.7", + "source-map": "0.7.4" + }, + "engines": { + "node": "^14.20.0 || ^16.13.0 || >=18.10.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "chokidar": "^3.5.2" + }, + "peerDependenciesMeta": { + "chokidar": { + "optional": true + } + } + }, + "node_modules/@schematics/angular/node_modules/rxjs": { + "version": "6.6.7", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", + "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", + "dependencies": { + "tslib": "^1.9.0" + }, + "engines": { + "npm": ">=2.0.0" + } + }, + "node_modules/@schematics/angular/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + }, + "node_modules/@socket.io/component-emitter": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", + "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", + "dev": true + }, + "node_modules/@stencil/core": { + "version": "4.19.2", + "resolved": "https://registry.npmjs.org/@stencil/core/-/core-4.19.2.tgz", + "integrity": "sha512-ZdnbHmHEl8E5vN0GWDtONe5w6j3CrSqqxZM4hNLBPkV/aouWKug7D5/Mi6RazfYO5U4fmHQYLwMz60rHcx0G4g==", + "bin": { + "stencil": "bin/stencil" + }, + "engines": { + "node": ">=16.0.0", + "npm": ">=7.10.0" + } + }, + "node_modules/@tootallnate/once": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", + "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", + "dev": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/@types/body-parser": { + "version": "1.19.5", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", + "integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==", + "dev": true, + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/bonjour": { + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@types/bonjour/-/bonjour-3.5.13.tgz", + "integrity": "sha512-z9fJ5Im06zvUL548KvYNecEVlA7cVDkGUi6kZusb04mpyEFKCIZJvloCcmpmLaIahDpOQGHaHmG6imtPMmPXGQ==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/connect-history-api-fallback": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.5.4.tgz", + "integrity": "sha512-n6Cr2xS1h4uAulPRdlw6Jl6s1oG8KrVilPN2yUITEs+K48EzMJJ3W1xy8K5eWuFvjp3R74AOIGSmp2UfBJ8HFw==", + "dev": true, + "dependencies": { + "@types/express-serve-static-core": "*", + "@types/node": "*" + } + }, + "node_modules/@types/cookie": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.1.tgz", + "integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==", + "dev": true + }, + "node_modules/@types/cors": { + "version": "2.8.17", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz", + "integrity": "sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/eslint": { + "version": "8.56.10", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.10.tgz", + "integrity": "sha512-Shavhk87gCtY2fhXDctcfS3e6FdxWkCx1iUZ9eEUbh7rTqlZT0/IzOkCOVt0fCjcFuZ9FPYfuezTBImfHCDBGQ==", + "dependencies": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "node_modules/@types/eslint-scope": { + "version": "3.7.7", + "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", + "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", + "dependencies": { + "@types/eslint": "*", + "@types/estree": "*" + } + }, + "node_modules/@types/estree": { + "version": "0.0.51", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.51.tgz", + "integrity": "sha512-CuPgU6f3eT/XgKKPqKd/gLZV1Xmvf1a2R5POBOGQa6uv82xpls89HU5zKeVoyR8XzHd1RGNOlQlvUe3CFkjWNQ==" + }, + "node_modules/@types/express": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz", + "integrity": "sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==", + "dev": true, + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.33", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "4.19.5", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.5.tgz", + "integrity": "sha512-y6W03tvrACO72aijJ5uF02FRq5cgDR9lUxddQ8vyF+GvmjJQqbzDcJngEjURc+ZsG31VI3hODNZJ2URj86pzmg==", + "dev": true, + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/http-errors": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", + "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==", + "dev": true + }, + "node_modules/@types/http-proxy": { + "version": "1.17.14", + "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.14.tgz", + "integrity": "sha512-SSrD0c1OQzlFX7pGu1eXxSEjemej64aaNPRhhVYUGqXh0BtldAAx37MG8btcumvpgKyZp1F5Gn3JkktdxiFv6w==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/jasmine": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/@types/jasmine/-/jasmine-4.3.6.tgz", + "integrity": "sha512-3N0FpQTeiWjm+Oo1WUYWguUS7E6JLceiGTriFrG8k5PU7zRLJCzLcWURU3wjMbZGS//a2/LgjsnO3QxIlwxt9g==", + "dev": true + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==" + }, + "node_modules/@types/long": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz", + "integrity": "sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==" + }, + "node_modules/@types/marked": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@types/marked/-/marked-4.3.2.tgz", + "integrity": "sha512-a79Yc3TOk6dGdituy8hmTTJXjOkZ7zsFYV10L337ttq/rec8lRMDBpV7fL3uLx6TgbFCa5DU/h8FmIBQPSbU0w==" + }, + "node_modules/@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "dev": true + }, + "node_modules/@types/node": { + "version": "20.14.9", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.9.tgz", + "integrity": "sha512-06OCtnTXtWOZBJlRApleWndH4JsRVs1pDCc8dLSQp+7PpUpX3ePdHyeNSFTeSe7FtKyQkrlPvHwJOW3SLd8Oyg==", + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/@types/node-forge": { + "version": "1.3.11", + "resolved": "https://registry.npmjs.org/@types/node-forge/-/node-forge-1.3.11.tgz", + "integrity": "sha512-FQx220y22OKNTqaByeBGqHWYz4cl94tpcxeFdvBo3wjG6XPBuZ0BNgNZRV5J5TFmmcsJ4IzsLkmGRiQbnYsBEQ==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/parse-json": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", + "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==", + "dev": true + }, + "node_modules/@types/qs": { + "version": "6.9.15", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.15.tgz", + "integrity": "sha512-uXHQKES6DQKKCLh441Xv/dwxOq1TVS3JPUMlEqoEglvlhR6Mxnlew/Xq/LRVHpLyk7iK3zODe1qYHIMltO7XGg==", + "dev": true + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true + }, + "node_modules/@types/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==", + "dev": true + }, + "node_modules/@types/send": { + "version": "0.17.4", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", + "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==", + "dev": true, + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/serve-index": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@types/serve-index/-/serve-index-1.9.4.tgz", + "integrity": "sha512-qLpGZ/c2fhSs5gnYsQxtDEq3Oy8SXPClIXkW5ghvAvsNuVSA8k+gCONcUCS/UjLEYvYps+e8uBtfgXgvhwfNug==", + "dev": true, + "dependencies": { + "@types/express": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.7", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.7.tgz", + "integrity": "sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==", + "dev": true, + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "*" + } + }, + "node_modules/@types/sockjs": { + "version": "0.3.36", + "resolved": "https://registry.npmjs.org/@types/sockjs/-/sockjs-0.3.36.tgz", + "integrity": "sha512-MK9V6NzAS1+Ud7JV9lJLFqW85VbC9dq3LmwZCuBe4wBDgKC0Kj/jd8Xl+nSviU+Qc3+m7umHHyHg//2KSa0a0Q==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/triple-beam": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.5.tgz", + "integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==" + }, + "node_modules/@types/uuid": { + "version": "9.0.8", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.8.tgz", + "integrity": "sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==", + "dev": true + }, + "node_modules/@types/ws": { + "version": "8.5.10", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.10.tgz", + "integrity": "sha512-vmQSUcfalpIq0R9q7uTo2lXs6eGIpt9wtnLdMv9LVpIjCA/+ufZRozlVoVelIYixx1ugCBKDhn89vnsEGOCx9A==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@webassemblyjs/ast": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.1.tgz", + "integrity": "sha512-ukBh14qFLjxTQNTXocdyksN5QdM28S1CxHt2rdskFyL+xFV7VremuBLVbmCePj+URalXBENx/9Lm7lnhihtCSw==", + "dependencies": { + "@webassemblyjs/helper-numbers": "1.11.1", + "@webassemblyjs/helper-wasm-bytecode": "1.11.1" + } + }, + "node_modules/@webassemblyjs/floating-point-hex-parser": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.1.tgz", + "integrity": "sha512-iGRfyc5Bq+NnNuX8b5hwBrRjzf0ocrJPI6GWFodBFzmFnyvrQ83SHKhmilCU/8Jv67i4GJZBMhEzltxzcNagtQ==" + }, + "node_modules/@webassemblyjs/helper-api-error": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.1.tgz", + "integrity": "sha512-RlhS8CBCXfRUR/cwo2ho9bkheSXG0+NwooXcc3PAILALf2QLdFyj7KGsKRbVc95hZnhnERon4kW/D3SZpp6Tcg==" + }, + "node_modules/@webassemblyjs/helper-buffer": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.1.tgz", + "integrity": "sha512-gwikF65aDNeeXa8JxXa2BAk+REjSyhrNC9ZwdT0f8jc4dQQeDQ7G4m0f2QCLPJiMTTO6wfDmRmj/pW0PsUvIcA==" + }, + "node_modules/@webassemblyjs/helper-numbers": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.1.tgz", + "integrity": "sha512-vDkbxiB8zfnPdNK9Rajcey5C0w+QJugEglN0of+kmO8l7lDb77AnlKYQF7aarZuCrv+l0UvqL+68gSDr3k9LPQ==", + "dependencies": { + "@webassemblyjs/floating-point-hex-parser": "1.11.1", + "@webassemblyjs/helper-api-error": "1.11.1", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/helper-wasm-bytecode": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.1.tgz", + "integrity": "sha512-PvpoOGiJwXeTrSf/qfudJhwlvDQxFgelbMqtq52WWiXC6Xgg1IREdngmPN3bs4RoO83PnL/nFrxucXj1+BX62Q==" + }, + "node_modules/@webassemblyjs/helper-wasm-section": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.1.tgz", + "integrity": "sha512-10P9No29rYX1j7F3EVPX3JvGPQPae+AomuSTPiF9eBQeChHI6iqjMIwR9JmOJXwpnn/oVGDk7I5IlskuMwU/pg==", + "dependencies": { + "@webassemblyjs/ast": "1.11.1", + "@webassemblyjs/helper-buffer": "1.11.1", + "@webassemblyjs/helper-wasm-bytecode": "1.11.1", + "@webassemblyjs/wasm-gen": "1.11.1" + } + }, + "node_modules/@webassemblyjs/ieee754": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.11.1.tgz", + "integrity": "sha512-hJ87QIPtAMKbFq6CGTkZYJivEwZDbQUgYd3qKSadTNOhVY7p+gfP6Sr0lLRVTaG1JjFj+r3YchoqRYxNH3M0GQ==", + "dependencies": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "node_modules/@webassemblyjs/leb128": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.11.1.tgz", + "integrity": "sha512-BJ2P0hNZ0u+Th1YZXJpzW6miwqQUGcIHT1G/sf72gLVD9DZ5AdYTqPNbHZh6K1M5VmKvFXwGSWZADz+qBWxeRw==", + "dependencies": { + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/utf8": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.11.1.tgz", + "integrity": "sha512-9kqcxAEdMhiwQkHpkNiorZzqpGrodQQ2IGrHHxCy+Ozng0ofyMA0lTqiLkVs1uzTRejX+/O0EOT7KxqVPuXosQ==" + }, + "node_modules/@webassemblyjs/wasm-edit": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.1.tgz", + "integrity": "sha512-g+RsupUC1aTHfR8CDgnsVRVZFJqdkFHpsHMfJuWQzWU3tvnLC07UqHICfP+4XyL2tnr1amvl1Sdp06TnYCmVkA==", + "dependencies": { + "@webassemblyjs/ast": "1.11.1", + "@webassemblyjs/helper-buffer": "1.11.1", + "@webassemblyjs/helper-wasm-bytecode": "1.11.1", + "@webassemblyjs/helper-wasm-section": "1.11.1", + "@webassemblyjs/wasm-gen": "1.11.1", + "@webassemblyjs/wasm-opt": "1.11.1", + "@webassemblyjs/wasm-parser": "1.11.1", + "@webassemblyjs/wast-printer": "1.11.1" + } + }, + "node_modules/@webassemblyjs/wasm-gen": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.1.tgz", + "integrity": "sha512-F7QqKXwwNlMmsulj6+O7r4mmtAlCWfO/0HdgOxSklZfQcDu0TpLiD1mRt/zF25Bk59FIjEuGAIyn5ei4yMfLhA==", + "dependencies": { + "@webassemblyjs/ast": "1.11.1", + "@webassemblyjs/helper-wasm-bytecode": "1.11.1", + "@webassemblyjs/ieee754": "1.11.1", + "@webassemblyjs/leb128": "1.11.1", + "@webassemblyjs/utf8": "1.11.1" + } + }, + "node_modules/@webassemblyjs/wasm-opt": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.1.tgz", + "integrity": "sha512-VqnkNqnZlU5EB64pp1l7hdm3hmQw7Vgqa0KF/KCNO9sIpI6Fk6brDEiX+iCOYrvMuBWDws0NkTOxYEb85XQHHw==", + "dependencies": { + "@webassemblyjs/ast": "1.11.1", + "@webassemblyjs/helper-buffer": "1.11.1", + "@webassemblyjs/wasm-gen": "1.11.1", + "@webassemblyjs/wasm-parser": "1.11.1" + } + }, + "node_modules/@webassemblyjs/wasm-parser": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.1.tgz", + "integrity": "sha512-rrBujw+dJu32gYB7/Lup6UhdkPx9S9SnobZzRVL7VcBH9Bt9bCBLEuX/YXOOtBsOZ4NQrRykKhffRWHvigQvOA==", + "dependencies": { + "@webassemblyjs/ast": "1.11.1", + "@webassemblyjs/helper-api-error": "1.11.1", + "@webassemblyjs/helper-wasm-bytecode": "1.11.1", + "@webassemblyjs/ieee754": "1.11.1", + "@webassemblyjs/leb128": "1.11.1", + "@webassemblyjs/utf8": "1.11.1" + } + }, + "node_modules/@webassemblyjs/wast-printer": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.11.1.tgz", + "integrity": "sha512-IQboUWM4eKzWW+N/jij2sRatKMh99QEelo3Eb2q0qXkvPRISAj8Qxtmw5itwqK+TTkBuUIE45AxYPToqPtL5gg==", + "dependencies": { + "@webassemblyjs/ast": "1.11.1", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@xtuc/ieee754": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==" + }, + "node_modules/@xtuc/long": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==" + }, + "node_modules/@yarnpkg/lockfile": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz", + "integrity": "sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ==", + "dev": true + }, + "node_modules/abab": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", + "integrity": "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==", + "deprecated": "Use your platform's native atob() and btoa() methods instead", + "dev": true + }, + "node_modules/abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "dev": true + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dev": true, + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.12.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", + "integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-import-assertions": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.9.0.tgz", + "integrity": "sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA==", + "peerDependencies": { + "acorn": "^8" + } + }, + "node_modules/adjust-sourcemap-loader": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/adjust-sourcemap-loader/-/adjust-sourcemap-loader-4.0.0.tgz", + "integrity": "sha512-OXwN5b9pCUXNQHJpwwD2qP40byEmSgzj8B4ydSN0uMNYWiFmJ6x6KwUllMmfk8Rwu/HJDFR7U8ubsWBoN0Xp0A==", + "dev": true, + "dependencies": { + "loader-utils": "^2.0.0", + "regex-parser": "^2.2.11" + }, + "engines": { + "node": ">=8.9" + } + }, + "node_modules/adjust-sourcemap-loader/node_modules/loader-utils": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", + "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", + "dev": true, + "dependencies": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" + }, + "engines": { + "node": ">=8.9.0" + } + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dev": true, + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/agentkeepalive": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.5.0.tgz", + "integrity": "sha512-5GG/5IbQQpC9FpkRGsSvZI5QYeSCzlJHdpBQntCsuTOxhKD8lqKhrleg2Yi7yvMIf82Ycmmqln9U8V9qwEiJew==", + "dev": true, + "dependencies": { + "humanize-ms": "^1.2.1" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/aggregate-error": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", + "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "dev": true, + "dependencies": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ajv": { + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", + "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/ansi-colors": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-html-community": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/ansi-html-community/-/ansi-html-community-0.0.8.tgz", + "integrity": "sha512-1APHAyr3+PCamwNw3bXCPp4HFLONZt/yIH0sZp0/469KWNTEy+qN5jQ3GVX6DMZ1UXAi34yVwtTeaG/HpBuuzw==", + "dev": true, + "engines": [ + "node >= 0.8.0" + ], + "bin": { + "ansi-html": "bin/ansi-html" + } + }, + "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==", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "devOptional": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/aproba": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", + "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==", + "dev": true + }, + "node_modules/are-we-there-yet": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-3.0.1.tgz", + "integrity": "sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg==", + "deprecated": "This package is no longer supported.", + "dev": true, + "dependencies": { + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "dev": true + }, + "node_modules/async": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz", + "integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==" + }, + "node_modules/autoprefixer": { + "version": "10.4.13", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.13.tgz", + "integrity": "sha512-49vKpMqcZYsJjwotvt4+h/BCjJVnhGwcLpDt5xkcaOG3eLrG/HUYLagrihYsQ+qrIBgIzX1Rw7a6L8I/ZA1Atg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + } + ], + "dependencies": { + "browserslist": "^4.21.4", + "caniuse-lite": "^1.0.30001426", + "fraction.js": "^4.2.0", + "normalize-range": "^0.1.2", + "picocolors": "^1.0.0", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/babel-loader": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-9.1.2.tgz", + "integrity": "sha512-mN14niXW43tddohGl8HPu5yfQq70iUThvFL/4QzESA7GcZoC0eVOhvWdQ8+3UlSjaDE9MVtsW9mxDY07W7VpVA==", + "dev": true, + "dependencies": { + "find-cache-dir": "^3.3.2", + "schema-utils": "^4.0.0" + }, + "engines": { + "node": ">= 14.15.0" + }, + "peerDependencies": { + "@babel/core": "^7.12.0", + "webpack": ">=5" + } + }, + "node_modules/babel-plugin-istanbul": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", + "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-instrument": "^5.0.4", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-polyfill-corejs2": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.3.3.tgz", + "integrity": "sha512-8hOdmFYFSZhqg2C/JgLUQ+t52o5nirNwaWM2B9LWteozwIvM14VSwdsCAUET10qT+kmySAlseadmfeeSWFCy+Q==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.17.7", + "@babel/helper-define-polyfill-provider": "^0.3.3", + "semver": "^6.1.1" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/babel-plugin-polyfill-corejs2/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/babel-plugin-polyfill-corejs3": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.6.0.tgz", + "integrity": "sha512-+eHqR6OPcBhJOGgsIar7xoAB1GcSwVUA3XjAd7HJNzOXT4wv6/H7KIdA/Nc60cvUlDbKApmqNvD1B1bzOt4nyA==", + "dev": true, + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.3.3", + "core-js-compat": "^3.25.1" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/babel-plugin-polyfill-regenerator": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.4.1.tgz", + "integrity": "sha512-NtQGmyQDXjQqQ+IzRkBVwEOz9lQ4zxAQZgoAYEtU9dJjnl1Oc98qnN7jcp+bE7O7aYzVpavXE3/VKXNzUbh7aw==", + "dev": true, + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.3.3" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/base64id": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", + "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==", + "dev": true, + "engines": { + "node": "^4.5.0 || >= 5.9" + } + }, + "node_modules/batch": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz", + "integrity": "sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw==", + "dev": true + }, + "node_modules/big.js": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", + "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", + "engines": { + "node": "*" + } + }, + "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==", + "devOptional": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/body-parser": { + "version": "1.20.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", + "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", + "dev": true, + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.11.0", + "raw-body": "2.5.2", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/body-parser/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/body-parser/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "node_modules/bonjour-service": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/bonjour-service/-/bonjour-service-1.2.1.tgz", + "integrity": "sha512-oSzCS2zV14bh2kji6vNe7vrpJYCHGvcZnlffFQ1MEoX/WOeQ/teD8SYWKR942OI3INjq8OMNJlbPK5LLLUxFDw==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.3", + "multicast-dns": "^7.2.5" + } + }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "dev": true + }, + "node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "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==", + "devOptional": true, + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.21.5", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.5.tgz", + "integrity": "sha512-tUkiguQGW7S3IhB7N+c2MV/HZPSCPAAiYBZXLsBhFB/PCy6ZKKsZrmBayHV9fdGV/ARIfJ14NkxKzRDjvp7L6w==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + } + ], + "dependencies": { + "caniuse-lite": "^1.0.30001449", + "electron-to-chromium": "^1.4.284", + "node-releases": "^2.0.8", + "update-browserslist-db": "^1.0.10" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/cacache": { + "version": "17.0.4", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-17.0.4.tgz", + "integrity": "sha512-Z/nL3gU+zTUjz5pCA5vVjYM8pmaw2kxM7JEiE0fv3w77Wj+sFbi70CrBruUWH0uNcEdvLDixFpgA2JM4F4DBjA==", + "dev": true, + "dependencies": { + "@npmcli/fs": "^3.1.0", + "fs-minipass": "^3.0.0", + "glob": "^8.0.1", + "lru-cache": "^7.7.1", + "minipass": "^4.0.0", + "minipass-collect": "^1.0.2", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "p-map": "^4.0.0", + "promise-inflight": "^1.0.1", + "ssri": "^10.0.0", + "tar": "^6.1.11", + "unique-filename": "^3.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/cacache/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/call-bind": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", + "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", + "dev": true, + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001640", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001640.tgz", + "integrity": "sha512-lA4VMpW0PSUrFnkmVuEKBUovSWKhj7puyCg8StBChgu298N1AtuF1sKWEvfDuimSEDbhlb/KqPKC3fs1HbuQUA==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ] + }, + "node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/chardet": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", + "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==" + }, + "node_modules/chokidar": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "devOptional": true, + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ], + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/chrome-trace-event": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", + "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", + "engines": { + "node": ">=6.0" + } + }, + "node_modules/clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "dependencies": { + "restore-cursor": "^3.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cli-spinners": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", + "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-width": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-3.0.0.tgz", + "integrity": "sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==", + "engines": { + "node": ">= 10" + } + }, + "node_modules/clipboard": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/clipboard/-/clipboard-2.0.11.tgz", + "integrity": "sha512-C+0bbOqkezLIsmWSvlsXS0Q0bmkugu7jcfMIACB+RDEntIzQIkdr148we28AfSloQLRdZlYL/QYyrq05j/3Faw==", + "dependencies": { + "good-listener": "^1.2.2", + "select": "^1.1.2", + "tiny-emitter": "^2.0.0" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/clone": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/clone-deep": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", + "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", + "dev": true, + "dependencies": { + "is-plain-object": "^2.0.4", + "kind-of": "^6.0.2", + "shallow-clone": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/color": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/color/-/color-3.2.1.tgz", + "integrity": "sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==", + "dependencies": { + "color-convert": "^1.9.3", + "color-string": "^1.6.0" + } + }, + "node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" + }, + "node_modules/color-string": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", + "dependencies": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, + "node_modules/color-support": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", + "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", + "dev": true, + "bin": { + "color-support": "bin.js" + } + }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true + }, + "node_modules/colorspace": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/colorspace/-/colorspace-1.1.4.tgz", + "integrity": "sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w==", + "dependencies": { + "color": "^3.1.3", + "text-hex": "1.0.x" + } + }, + "node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" + }, + "node_modules/commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", + "dev": true + }, + "node_modules/compressible": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", + "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", + "dev": true, + "dependencies": { + "mime-db": ">= 1.43.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/compression": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.7.4.tgz", + "integrity": "sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==", + "dev": true, + "dependencies": { + "accepts": "~1.3.5", + "bytes": "3.0.0", + "compressible": "~2.0.16", + "debug": "2.6.9", + "on-headers": "~1.0.2", + "safe-buffer": "5.1.2", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/compression/node_modules/bytes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", + "integrity": "sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/compression/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/compression/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "node_modules/compression/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "node_modules/connect": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/connect/-/connect-3.7.0.tgz", + "integrity": "sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ==", + "dev": true, + "dependencies": { + "debug": "2.6.9", + "finalhandler": "1.1.2", + "parseurl": "~1.3.3", + "utils-merge": "1.0.1" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/connect-history-api-fallback": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-2.0.0.tgz", + "integrity": "sha512-U73+6lQFmfiNPrYbXqr6kZ1i1wiRqXnp2nhMsINseWXO8lDau0LGEffJ8kQi4EjLZympVgRdvqjAgiZ1tgzDDA==", + "dev": true, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/connect/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/connect/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "node_modules/console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==", + "dev": true + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "dev": true, + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "dev": true + }, + "node_modules/cookie": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz", + "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "dev": true + }, + "node_modules/copy-anything": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-2.0.6.tgz", + "integrity": "sha512-1j20GZTsvKNkc4BY3NpMOM8tt///wY3FpIzozTOFO2ffuZcV61nojHXVKIy3WM+7ADCy5FVhdZYHYDdgTU0yJw==", + "dev": true, + "dependencies": { + "is-what": "^3.14.1" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, + "node_modules/copy-webpack-plugin": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-11.0.0.tgz", + "integrity": "sha512-fX2MWpamkW0hZxMEg0+mYnA40LTosOSa5TqZ9GYIBzyJa9C3QUaMPSE2xAi/buNr8u89SfD9wHSQVBzrRa/SOQ==", + "dev": true, + "dependencies": { + "fast-glob": "^3.2.11", + "glob-parent": "^6.0.1", + "globby": "^13.1.1", + "normalize-path": "^3.0.0", + "schema-utils": "^4.0.0", + "serialize-javascript": "^6.0.0" + }, + "engines": { + "node": ">= 14.15.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + } + }, + "node_modules/copy-webpack-plugin/node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/core-js-compat": { + "version": "3.37.1", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.37.1.tgz", + "integrity": "sha512-9TNiImhKvQqSUkOvk/mMRZzOANTiEVC7WaBNhHcKM7x+/5E1l5NvsysR19zuDQScE8k+kfQXWRN3AtS/eOSHpg==", + "dev": true, + "dependencies": { + "browserslist": "^4.23.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "node_modules/core-js-compat/node_modules/browserslist": { + "version": "4.23.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.1.tgz", + "integrity": "sha512-TUfofFo/KsK/bWZ9TWQ5O26tsWW4Uhmt8IYklbnUa70udB6P2wA7w7o4PY4muaEPBQaAX+CEnmmIA41NVHtPVw==", + "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" + } + ], + "dependencies": { + "caniuse-lite": "^1.0.30001629", + "electron-to-chromium": "^1.4.796", + "node-releases": "^2.0.14", + "update-browserslist-db": "^1.0.16" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "dev": true + }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "dev": true, + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/cose-base": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/cose-base/-/cose-base-1.0.3.tgz", + "integrity": "sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg==", + "optional": true, + "dependencies": { + "layout-base": "^1.0.0" + } + }, + "node_modules/cosmiconfig": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", + "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==", + "dev": true, + "dependencies": { + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.2.1", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.10.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/critters": { + "version": "0.0.16", + "resolved": "https://registry.npmjs.org/critters/-/critters-0.0.16.tgz", + "integrity": "sha512-JwjgmO6i3y6RWtLYmXwO5jMd+maZt8Tnfu7VVISmEWyQqfLpB8soBswf8/2bu6SBXxtKA68Al3c+qIG1ApT68A==", + "dev": true, + "dependencies": { + "chalk": "^4.1.0", + "css-select": "^4.2.0", + "parse5": "^6.0.1", + "parse5-htmlparser2-tree-adapter": "^6.0.1", + "postcss": "^8.3.7", + "pretty-bytes": "^5.3.0" + } + }, + "node_modules/critters/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/critters/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/critters/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/critters/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/critters/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/critters/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/cross-spawn/node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/css-loader": { + "version": "6.7.3", + "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-6.7.3.tgz", + "integrity": "sha512-qhOH1KlBMnZP8FzRO6YCH9UHXQhVMcEGLyNdb7Hv2cpcmJbW0YrddO+tG1ab5nT41KpHIYGsbeHqxB9xPu1pKQ==", + "dev": true, + "dependencies": { + "icss-utils": "^5.1.0", + "postcss": "^8.4.19", + "postcss-modules-extract-imports": "^3.0.0", + "postcss-modules-local-by-default": "^4.0.0", + "postcss-modules-scope": "^3.0.0", + "postcss-modules-values": "^4.0.0", + "postcss-value-parser": "^4.2.0", + "semver": "^7.3.8" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" + } + }, + "node_modules/css-select": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-4.3.0.tgz", + "integrity": "sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==", + "dev": true, + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.0.1", + "domhandler": "^4.3.1", + "domutils": "^2.8.0", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-what": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", + "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", + "dev": true, + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "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==", + "dev": true, + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/custom-event": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/custom-event/-/custom-event-1.0.1.tgz", + "integrity": "sha512-GAj5FOq0Hd+RsCGVJxZuKaIDXDf3h6GQoNEjFgbLLI/trgtavwUbSnZ5pVfg27DVCaWjIohryS0JFwIJyT2cMg==", + "dev": true + }, + "node_modules/cytoscape": { + "version": "3.30.0", + "resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.30.0.tgz", + "integrity": "sha512-l590mjTHT6/Cbxp13dGPC2Y7VXdgc+rUeF8AnF/JPzhjNevbDJfObnJgaSjlldOgBQZbue+X6IUZ7r5GAgvauQ==", + "optional": true, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/cytoscape-cose-bilkent": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cytoscape-cose-bilkent/-/cytoscape-cose-bilkent-4.1.0.tgz", + "integrity": "sha512-wgQlVIUJF13Quxiv5e1gstZ08rnZj2XaLHGoFMYXz7SkNfCDOOteKBE6SYRfA9WxxI/iBc3ajfDoc6hb/MRAHQ==", + "optional": true, + "dependencies": { + "cose-base": "^1.0.0" + }, + "peerDependencies": { + "cytoscape": "^3.2.0" + } + }, + "node_modules/cytoscape-fcose": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cytoscape-fcose/-/cytoscape-fcose-2.2.0.tgz", + "integrity": "sha512-ki1/VuRIHFCzxWNrsshHYPs6L7TvLu3DL+TyIGEsRcvVERmxokbf5Gdk7mFxZnTdiGtnA4cfSmjZJMviqSuZrQ==", + "optional": true, + "dependencies": { + "cose-base": "^2.2.0" + }, + "peerDependencies": { + "cytoscape": "^3.2.0" + } + }, + "node_modules/cytoscape-fcose/node_modules/cose-base": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cose-base/-/cose-base-2.2.0.tgz", + "integrity": "sha512-AzlgcsCbUMymkADOJtQm3wO9S3ltPfYOFD5033keQn9NJzIbtnZj+UdBJe7DYml/8TdbtHJW3j58SOnKhWY/5g==", + "optional": true, + "dependencies": { + "layout-base": "^2.0.0" + } + }, + "node_modules/cytoscape-fcose/node_modules/layout-base": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/layout-base/-/layout-base-2.0.1.tgz", + "integrity": "sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg==", + "optional": true + }, + "node_modules/d3": { + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/d3/-/d3-7.9.0.tgz", + "integrity": "sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==", + "optional": true, + "dependencies": { + "d3-array": "3", + "d3-axis": "3", + "d3-brush": "3", + "d3-chord": "3", + "d3-color": "3", + "d3-contour": "4", + "d3-delaunay": "6", + "d3-dispatch": "3", + "d3-drag": "3", + "d3-dsv": "3", + "d3-ease": "3", + "d3-fetch": "3", + "d3-force": "3", + "d3-format": "3", + "d3-geo": "3", + "d3-hierarchy": "3", + "d3-interpolate": "3", + "d3-path": "3", + "d3-polygon": "3", + "d3-quadtree": "3", + "d3-random": "3", + "d3-scale": "4", + "d3-scale-chromatic": "3", + "d3-selection": "3", + "d3-shape": "3", + "d3-time": "3", + "d3-time-format": "4", + "d3-timer": "3", + "d3-transition": "3", + "d3-zoom": "3" + }, + "engines": { + "node": ">=12" + } + }, + "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==", + "optional": true, + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-axis": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-3.0.0.tgz", + "integrity": "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==", + "optional": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-brush": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-3.0.0.tgz", + "integrity": "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==", + "optional": true, + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "3", + "d3-transition": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-chord": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-chord/-/d3-chord-3.0.1.tgz", + "integrity": "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==", + "optional": true, + "dependencies": { + "d3-path": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "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==", + "optional": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-contour": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-contour/-/d3-contour-4.0.2.tgz", + "integrity": "sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==", + "optional": true, + "dependencies": { + "d3-array": "^3.2.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-delaunay": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==", + "optional": true, + "dependencies": { + "delaunator": "5" + }, + "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==", + "optional": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-drag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", + "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", + "optional": true, + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-selection": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dsv": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-3.0.1.tgz", + "integrity": "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==", + "optional": true, + "dependencies": { + "commander": "7", + "iconv-lite": "0.6", + "rw": "1" + }, + "bin": { + "csv2json": "bin/dsv2json.js", + "csv2tsv": "bin/dsv2dsv.js", + "dsv2dsv": "bin/dsv2dsv.js", + "dsv2json": "bin/dsv2json.js", + "json2csv": "bin/json2dsv.js", + "json2dsv": "bin/json2dsv.js", + "json2tsv": "bin/json2dsv.js", + "tsv2csv": "bin/dsv2dsv.js", + "tsv2json": "bin/dsv2json.js" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dsv/node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "optional": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/d3-dsv/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "optional": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "optional": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-fetch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-fetch/-/d3-fetch-3.0.1.tgz", + "integrity": "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==", + "optional": true, + "dependencies": { + "d3-dsv": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-force": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-3.0.0.tgz", + "integrity": "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==", + "optional": true, + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-quadtree": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz", + "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==", + "optional": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-geo": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-3.1.1.tgz", + "integrity": "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==", + "optional": true, + "dependencies": { + "d3-array": "2.5.0 - 3" + }, + "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==", + "optional": true, + "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==", + "optional": true, + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "optional": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-polygon": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-3.0.1.tgz", + "integrity": "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==", + "optional": true, + "engines": { + "node": ">=12" + } + }, + "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==", + "optional": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-random": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-random/-/d3-random-3.0.1.tgz", + "integrity": "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==", + "optional": true, + "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==", + "optional": true, + "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-scale-chromatic": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", + "integrity": "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==", + "optional": true, + "dependencies": { + "d3-color": "1 - 3", + "d3-interpolate": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-selection": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", + "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", + "optional": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "optional": true, + "dependencies": { + "d3-path": "^3.1.0" + }, + "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==", + "optional": true, + "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==", + "optional": true, + "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==", + "optional": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-transition": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", + "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", + "optional": true, + "dependencies": { + "d3-color": "1 - 3", + "d3-dispatch": "1 - 3", + "d3-ease": "1 - 3", + "d3-interpolate": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "d3-selection": "2 - 3" + } + }, + "node_modules/d3-zoom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz", + "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", + "optional": true, + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "2 - 3", + "d3-transition": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/dagre-d3-es": { + "version": "7.0.9", + "resolved": "https://registry.npmjs.org/dagre-d3-es/-/dagre-d3-es-7.0.9.tgz", + "integrity": "sha512-rYR4QfVmy+sR44IBDvVtcAmOReGBvRCWDpO2QjYwqgh9yijw6eSHBqaPG/LIOEy7aBsniLvtMW6pg19qJhq60w==", + "optional": true, + "dependencies": { + "d3": "^7.8.2", + "lodash-es": "^4.17.21" + } + }, + "node_modules/date-format": { + "version": "4.0.14", + "resolved": "https://registry.npmjs.org/date-format/-/date-format-4.0.14.tgz", + "integrity": "sha512-39BOQLs9ZjKh0/patS9nrT8wc3ioX3/eA/zgbKNopnF2wCqJEoxywwwElATYvRsXdnOxA/OQeQoFZ3rFjVajhg==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/dayjs": { + "version": "1.11.11", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.11.tgz", + "integrity": "sha512-okzr3f11N6WuqYtZSvm+F776mB41wRZMhKP+hc34YdW+KmtYYK9iqvHSwo2k9FEH3fhGXvOPV6yz2IcSrfRUDg==", + "optional": true + }, + "node_modules/debug": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", + "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/default-gateway": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/default-gateway/-/default-gateway-6.0.3.tgz", + "integrity": "sha512-fwSOJsbbNzZ/CUFpqFBqYfYNLj1NbMPm8MMCIzHjC83iSJRBEGmDUxU+WP661BaBQImeC2yHwXtz+P/O9o+XEg==", + "dev": true, + "dependencies": { + "execa": "^5.0.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/defaults": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", + "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==", + "dependencies": { + "clone": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-lazy-prop": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", + "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", + "engines": { + "node": ">=8" + } + }, + "node_modules/delaunator": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.0.1.tgz", + "integrity": "sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==", + "optional": true, + "dependencies": { + "robust-predicates": "^3.0.2" + } + }, + "node_modules/delegate": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/delegate/-/delegate-3.2.0.tgz", + "integrity": "sha512-IofjkYBZaZivn0V8nnsMJGBr4jVLxHDheKSW88PyxS5QC4Vo9ZbZVvhzlSxY87fVq3STR6r+4cGepyHkcWOQSw==" + }, + "node_modules/delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", + "dev": true + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/dependency-graph": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/dependency-graph/-/dependency-graph-0.11.0.tgz", + "integrity": "sha512-JeMq7fEshyepOWDfcfHK06N3MhyPhz++vtqWhMT5O9A3K42rdsEDpfdVqjaqaAhsw6a+ZqeDvQVtD0hFHQWrzg==", + "dev": true, + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "dev": true, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/detect-node": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", + "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", + "dev": true + }, + "node_modules/di": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/di/-/di-0.0.1.tgz", + "integrity": "sha512-uJaamHkagcZtHPqCIHZxnFrXlunQXgBOsZSUOWwFw31QJCAbyTBoHMW75YOTur5ZNx8pIeAKgf6GWIgaqqiLhA==", + "dev": true + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/dns-packet": { + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-5.6.1.tgz", + "integrity": "sha512-l4gcSouhcgIKRvyy99RNVOgxXiicE+2jZoNmaNmZ6JXiGajBOJAesk1OBlJuM5k2c+eudGdLxDqXuPCKIj6kpw==", + "dev": true, + "dependencies": { + "@leichtgewicht/ip-codec": "^2.0.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/dom-serialize": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/dom-serialize/-/dom-serialize-2.2.1.tgz", + "integrity": "sha512-Yra4DbvoW7/Z6LBN560ZwXMjoNOSAN2wRsKFGc4iBeso+mpIA6qj1vfdf9HpMaKAqG6wXTy+1SYEzmNpKXOSsQ==", + "dev": true, + "dependencies": { + "custom-event": "~1.0.0", + "ent": "~2.2.0", + "extend": "^3.0.0", + "void-elements": "^2.0.0" + } + }, + "node_modules/dom-serializer": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz", + "integrity": "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==", + "dev": true, + "dependencies": { + "domelementtype": "^2.0.1", + "domhandler": "^4.2.0", + "entities": "^2.0.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ] + }, + "node_modules/domhandler": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz", + "integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==", + "dev": true, + "dependencies": { + "domelementtype": "^2.2.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/dompurify": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.4.3.tgz", + "integrity": "sha512-q6QaLcakcRjebxjg8/+NP+h0rPfatOgOzc46Fst9VAA3jF2ApfKBNKMzdP4DYTqtUMXSCd5pRS/8Po/OmoCHZQ==", + "optional": true + }, + "node_modules/domutils": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", + "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==", + "dev": true, + "dependencies": { + "dom-serializer": "^1.0.1", + "domelementtype": "^2.2.0", + "domhandler": "^4.2.0" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "dev": true + }, + "node_modules/electron-to-chromium": { + "version": "1.4.816", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.816.tgz", + "integrity": "sha512-EKH5X5oqC6hLmiS7/vYtZHZFTNdhsYG5NVPRN6Yn0kQHNBlT59+xSM8HBy66P5fxWpKgZbPqb+diC64ng295Jw==" + }, + "node_modules/elkjs": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/elkjs/-/elkjs-0.8.2.tgz", + "integrity": "sha512-L6uRgvZTH+4OF5NE/MBbzQx/WYpru1xCBE9respNj6qznEewGUIfhzmm7horWWxbNO2M0WckQypGctR8lH79xQ==", + "optional": true + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "node_modules/emoji-toolkit": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/emoji-toolkit/-/emoji-toolkit-7.0.1.tgz", + "integrity": "sha512-l5aJyAhpC5s4mDuoVuqt4SzVjwIsIvakPh4ZGJJE4KWuWFCEHaXacQFkStVdD9zbRR+/BbRXob7u99o0lQFr8A==", + "optional": true + }, + "node_modules/emojis-list": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", + "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", + "engines": { + "node": ">= 4" + } + }, + "node_modules/enabled": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz", + "integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==" + }, + "node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/encoding": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", + "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", + "optional": true, + "dependencies": { + "iconv-lite": "^0.6.2" + } + }, + "node_modules/encoding/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "optional": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/engine.io": { + "version": "6.5.5", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.5.5.tgz", + "integrity": "sha512-C5Pn8Wk+1vKBoHghJODM63yk8MvrO9EWZUfkAt5HAqIgPE4/8FF0PEGHXtEd40l223+cE5ABWuPzm38PHFXfMA==", + "dev": true, + "dependencies": { + "@types/cookie": "^0.4.1", + "@types/cors": "^2.8.12", + "@types/node": ">=10.0.0", + "accepts": "~1.3.4", + "base64id": "2.0.0", + "cookie": "~0.4.1", + "cors": "~2.8.5", + "debug": "~4.3.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.17.1" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/engine.io-parser": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.2.tgz", + "integrity": "sha512-RcyUFKA93/CXH20l4SoVvzZfrSDMOTUS3bWVpTt2FuFP+XYrL8i8oonHP7WInRyVHXh0n/ORtoeiE1os+8qkSw==", + "dev": true, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/enhanced-resolve": { + "version": "5.17.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.17.0.tgz", + "integrity": "sha512-dwDPwZL0dmye8Txp2gzFmA6sxALaSvdRDjPH0viLcKrtlOL3tw62nWWweVD1SdILDTJrbrL6tdWVN58Wo6U3eA==", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/ent": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ent/-/ent-2.2.1.tgz", + "integrity": "sha512-QHuXVeZx9d+tIQAz/XztU0ZwZf2Agg9CcXcgE1rurqvdBeDBrpSwjl8/6XUqMg7tw2Y7uAdKb2sRv+bSEFqQ5A==", + "dev": true, + "dependencies": { + "punycode": "^1.4.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/entities": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", + "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", + "dev": true, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/err-code": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", + "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", + "dev": true + }, + "node_modules/errno": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.8.tgz", + "integrity": "sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==", + "dev": true, + "optional": true, + "dependencies": { + "prr": "~1.0.1" + }, + "bin": { + "errno": "cli.js" + } + }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", + "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-module-lexer": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-0.9.3.tgz", + "integrity": "sha512-1HQ2M2sPtxwnvOvT1ZClHyQDiggdNjURWpY2we6aMKCQiUVxTmVs2UYPLIrD84sS+kMdUwfBSylbJPwNnBrnHQ==" + }, + "node_modules/esbuild": { + "version": "0.17.8", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.17.8.tgz", + "integrity": "sha512-g24ybC3fWhZddZK6R3uD2iF/RIPnRpwJAqLov6ouX3hMbY4+tKolP0VMF3zuIYCaXun+yHwS5IPQ91N2BT191g==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/android-arm": "0.17.8", + "@esbuild/android-arm64": "0.17.8", + "@esbuild/android-x64": "0.17.8", + "@esbuild/darwin-arm64": "0.17.8", + "@esbuild/darwin-x64": "0.17.8", + "@esbuild/freebsd-arm64": "0.17.8", + "@esbuild/freebsd-x64": "0.17.8", + "@esbuild/linux-arm": "0.17.8", + "@esbuild/linux-arm64": "0.17.8", + "@esbuild/linux-ia32": "0.17.8", + "@esbuild/linux-loong64": "0.17.8", + "@esbuild/linux-mips64el": "0.17.8", + "@esbuild/linux-ppc64": "0.17.8", + "@esbuild/linux-riscv64": "0.17.8", + "@esbuild/linux-s390x": "0.17.8", + "@esbuild/linux-x64": "0.17.8", + "@esbuild/netbsd-x64": "0.17.8", + "@esbuild/openbsd-x64": "0.17.8", + "@esbuild/sunos-x64": "0.17.8", + "@esbuild/win32-arm64": "0.17.8", + "@esbuild/win32-ia32": "0.17.8", + "@esbuild/win32-x64": "0.17.8" + } + }, + "node_modules/esbuild-wasm": { + "version": "0.17.8", + "resolved": "https://registry.npmjs.org/esbuild-wasm/-/esbuild-wasm-0.17.8.tgz", + "integrity": "sha512-zCmpxv95E0FuCmvdw1K836UHnj4EdiQnFfjTby35y3LAjRPtXMj3sbHDRHjbD8Mqg5lTwq3knacr/1qIFU51CQ==", + "dev": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/escalade": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", + "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "dev": true + }, + "node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "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==", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esrecurse/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/eventemitter-asyncresource": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/eventemitter-asyncresource/-/eventemitter-asyncresource-1.0.0.tgz", + "integrity": "sha512-39F7TBIV0G7gTelxwbEqnwhp90eqCPON1k0NwNfwhgKn4Co4ybUbj2pECcXT0B3ztRKZ7Pw1JujUUgmQJHcVAQ==", + "dev": true + }, + "node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "dev": true + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/exponential-backoff": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.1.tgz", + "integrity": "sha512-dX7e/LHVJ6W3DE1MHWi9S1EYzDESENfLrYohG2G++ovZrYOkm4Knwa0mc1cn84xJOR4KEU0WSchhLbd0UklbHw==", + "dev": true + }, + "node_modules/express": { + "version": "4.19.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz", + "integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==", + "dev": true, + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.2", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.6.0", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.2.0", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.7", + "qs": "6.11.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.18.0", + "serve-static": "1.15.0", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/express/node_modules/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/express/node_modules/finalhandler": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", + "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", + "dev": true, + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/express/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "node_modules/express/node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "dev": true + }, + "node_modules/external-editor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", + "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", + "dependencies": { + "chardet": "^0.7.0", + "iconv-lite": "^0.4.24", + "tmp": "^0.0.33" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" + }, + "node_modules/fast-glob": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", + "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-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==" + }, + "node_modules/fastq": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", + "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", + "dev": true, + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/faye-websocket": { + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz", + "integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==", + "dependencies": { + "websocket-driver": ">=0.5.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/fecha": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz", + "integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==" + }, + "node_modules/figures": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", + "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", + "dependencies": { + "escape-string-regexp": "^1.0.5" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/file-loader": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/file-loader/-/file-loader-6.2.0.tgz", + "integrity": "sha512-qo3glqyTa61Ytg4u73GultjHGjdRyig3tG6lPtyX/jOEJvHif9uB0/OCI2Kif6ctF3caQTW2G5gym21oAsI4pw==", + "dependencies": { + "loader-utils": "^2.0.0", + "schema-utils": "^3.0.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^4.0.0 || ^5.0.0" + } + }, + "node_modules/file-loader/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "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/file-loader/node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "node_modules/file-loader/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==" + }, + "node_modules/file-loader/node_modules/loader-utils": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", + "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", + "dependencies": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" + }, + "engines": { + "node": ">=8.9.0" + } + }, + "node_modules/file-loader/node_modules/schema-utils": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "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==", + "devOptional": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", + "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", + "dev": true, + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "statuses": "~1.5.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/finalhandler/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/finalhandler/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "node_modules/finalhandler/node_modules/on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==", + "dev": true, + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/find-cache-dir": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.2.tgz", + "integrity": "sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==", + "dev": true, + "dependencies": { + "commondir": "^1.0.1", + "make-dir": "^3.0.2", + "pkg-dir": "^4.1.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/avajs/find-cache-dir?sponsor=1" + } + }, + "node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase": { + "version": "9.23.0", + "resolved": "https://registry.npmjs.org/firebase/-/firebase-9.23.0.tgz", + "integrity": "sha512-/4lUVY0lUvBDIaeY1q6dUYhS8Sd18Qb9CgWkPZICUo9IXpJNCEagfNZXBBFCkMTTN5L5gx2Hjr27y21a9NzUcA==", + "dependencies": { + "@firebase/analytics": "0.10.0", + "@firebase/analytics-compat": "0.2.6", + "@firebase/app": "0.9.13", + "@firebase/app-check": "0.8.0", + "@firebase/app-check-compat": "0.3.7", + "@firebase/app-compat": "0.2.13", + "@firebase/app-types": "0.9.0", + "@firebase/auth": "0.23.2", + "@firebase/auth-compat": "0.4.2", + "@firebase/database": "0.14.4", + "@firebase/database-compat": "0.3.4", + "@firebase/firestore": "3.13.0", + "@firebase/firestore-compat": "0.3.12", + "@firebase/functions": "0.10.0", + "@firebase/functions-compat": "0.3.5", + "@firebase/installations": "0.6.4", + "@firebase/installations-compat": "0.2.4", + "@firebase/messaging": "0.12.4", + "@firebase/messaging-compat": "0.2.4", + "@firebase/performance": "0.6.4", + "@firebase/performance-compat": "0.2.4", + "@firebase/remote-config": "0.4.4", + "@firebase/remote-config-compat": "0.2.4", + "@firebase/storage": "0.11.2", + "@firebase/storage-compat": "0.3.2", + "@firebase/util": "1.9.3" + } + }, + "node_modules/firebase-tools": { + "version": "11.30.0", + "resolved": "https://registry.npmjs.org/firebase-tools/-/firebase-tools-11.30.0.tgz", + "integrity": "sha512-rNMhtsFKy35AZqfy0obY2Kgsfmg8N1duEx2UDT14NsMHB7xt2Z7cUd92bkAoQAfD0jAOd7LhRT/7E6J5yB+kGw==", + "devOptional": true, + "hasShrinkwrap": true, + "dependencies": { + "@google-cloud/pubsub": "^3.0.1", + "abort-controller": "^3.0.0", + "ajv": "^6.12.6", + "archiver": "^5.0.0", + "async-lock": "1.3.2", + "body-parser": "^1.19.0", + "chokidar": "^3.0.2", + "cjson": "^0.3.1", + "cli-table": "0.3.11", + "colorette": "^2.0.19", + "commander": "^4.0.1", + "configstore": "^5.0.1", + "cors": "^2.8.5", + "cross-env": "^5.1.3", + "cross-spawn": "^7.0.3", + "csv-parse": "^5.0.4", + "exegesis": "^4.1.0", + "exegesis-express": "^4.0.0", + "express": "^4.16.4", + "filesize": "^6.1.0", + "form-data": "^4.0.0", + "fs-extra": "^10.1.0", + "glob": "^7.1.2", + "google-auth-library": "^7.11.0", + "inquirer": "^8.2.0", + "js-yaml": "^3.13.1", + "jsonwebtoken": "^9.0.0", + "leven": "^3.1.0", + "libsodium-wrappers": "^0.7.10", + "lodash": "^4.17.21", + "marked": "^4.0.14", + "marked-terminal": "^5.1.1", + "mime": "^2.5.2", + "minimatch": "^3.0.4", + "morgan": "^1.10.0", + "node-fetch": "^2.6.7", + "open": "^6.3.0", + "ora": "^5.4.1", + "p-limit": "^3.0.1", + "portfinder": "^1.0.32", + "progress": "^2.0.3", + "proxy-agent": "^5.0.0", + "request": "^2.87.0", + "retry": "^0.13.1", + "rimraf": "^3.0.0", + "semver": "^5.7.1", + "stream-chain": "^2.2.4", + "stream-json": "^1.7.3", + "strip-ansi": "^6.0.1", + "superstatic": "^9.0.3", + "tar": "^6.1.11", + "tcp-port-used": "^1.0.2", + "tmp": "^0.2.1", + "triple-beam": "^1.3.0", + "universal-analytics": "^0.5.3", + "update-notifier-cjs": "^5.1.6", + "uuid": "^8.3.2", + "winston": "^3.0.0", + "winston-transport": "^4.4.0", + "ws": "^7.2.3" + }, + "bin": { + "firebase": "lib/bin/firebase.js" + }, + "engines": { + "node": "^14.18.0 || >=16.4.0" + } + }, + "node_modules/firebase-tools/node_modules/@apidevtools/json-schema-ref-parser": { + "version": "9.0.7", + "resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-9.0.7.tgz", + "integrity": "sha512-QdwOGF1+eeyFh+17v2Tz626WX0nucd1iKOm6JUTUvCZdbolblCOOQCxGrQPY0f7jEhn36PiAWqZnsC2r5vmUWg==", + "devOptional": true, + "dependencies": { + "@jsdevtools/ono": "^7.1.3", + "call-me-maybe": "^1.0.1", + "js-yaml": "^3.13.1" + } + }, + "node_modules/firebase-tools/node_modules/@babel/parser": { + "version": "7.21.4", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.21.4.tgz", + "integrity": "sha512-alVJj7k7zIxqBZ7BTRhz0IqJFxW1VJbm6N8JbcYhQ186df9ZBPbZBmWSqAMXwHGsCJdYks7z/voa3ibiS5bCIw==", + "devOptional": true, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/firebase-tools/node_modules/@colors/colors": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", + "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==", + "optional": true, + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/firebase-tools/node_modules/@gar/promisify": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz", + "integrity": "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==", + "optional": true + }, + "node_modules/firebase-tools/node_modules/@google-cloud/precise-date": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@google-cloud/precise-date/-/precise-date-2.0.3.tgz", + "integrity": "sha512-+SDJ3ZvGkF7hzo6BGa8ZqeK3F6Z4+S+KviC9oOK+XCs3tfMyJCh/4j93XIWINgMMDIh9BgEvlw4306VxlXIlYA==", + "devOptional": true, + "engines": { + "node": ">=10.4.0" + } + }, + "node_modules/firebase-tools/node_modules/@google-cloud/projectify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@google-cloud/projectify/-/projectify-2.0.1.tgz", + "integrity": "sha512-ZDG38U/Yy6Zr21LaR3BTiiLtpJl6RkPS/JwoRT453G+6Q1DhlV0waNf8Lfu+YVYGIIxgKnLayJRfYlFJfiI8iQ==", + "devOptional": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/firebase-tools/node_modules/@google-cloud/promisify": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@google-cloud/promisify/-/promisify-2.0.3.tgz", + "integrity": "sha512-d4VSA86eL/AFTe5xtyZX+ePUjE8dIFu2T8zmdeNBSa5/kNgXPCx/o/wbFNHAGLJdGnk1vddRuMESD9HbOC8irw==", + "devOptional": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/firebase-tools/node_modules/@google-cloud/pubsub": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@google-cloud/pubsub/-/pubsub-3.0.1.tgz", + "integrity": "sha512-dznNbRd/Y8J0C0xvdvCPi3B1msK/dj/Nya+NQZ2doUOLT6eoa261tBwk9umOQs5L5GKcdlqQKbBjrNjDYVbzQA==", + "devOptional": true, + "dependencies": { + "@google-cloud/paginator": "^4.0.0", + "@google-cloud/precise-date": "^2.0.0", + "@google-cloud/projectify": "^2.0.0", + "@google-cloud/promisify": "^2.0.0", + "@opentelemetry/api": "^1.0.0", + "@opentelemetry/semantic-conventions": "^1.0.0", + "@types/duplexify": "^3.6.0", + "@types/long": "^4.0.0", + "arrify": "^2.0.0", + "extend": "^3.0.2", + "google-auth-library": "^8.0.2", + "google-gax": "^3.0.1", + "is-stream-ended": "^0.1.4", + "lodash.snakecase": "^4.1.1", + "p-defer": "^3.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/firebase-tools/node_modules/@google-cloud/pubsub/node_modules/@google-cloud/paginator": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@google-cloud/paginator/-/paginator-4.0.0.tgz", + "integrity": "sha512-wNmCZl+2G2DmgT/VlF+AROf80SoaC/CwS8trwmjNaq26VRNK8yPbU5F/Vy+R9oDAGKWQU2k8+Op5H4kFJVXFaQ==", + "devOptional": true, + "dependencies": { + "arrify": "^2.0.0", + "extend": "^3.0.2" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/firebase-tools/node_modules/@google-cloud/pubsub/node_modules/gaxios": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-5.0.0.tgz", + "integrity": "sha512-VD/yc5ln6XU8Ch1hyYY6kRMBE0Yc2np3fPyeJeYHhrPs1i8rgnsApPMWyrugkl7LLoSqpOJVBWlQIa87OAvt8Q==", + "devOptional": true, + "dependencies": { + "abort-controller": "^3.0.0", + "extend": "^3.0.2", + "https-proxy-agent": "^5.0.0", + "is-stream": "^2.0.0", + "node-fetch": "^2.6.7" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/firebase-tools/node_modules/@google-cloud/pubsub/node_modules/gcp-metadata": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-5.0.0.tgz", + "integrity": "sha512-gfwuX3yA3nNsHSWUL4KG90UulNiq922Ukj3wLTrcnX33BB7PwB1o0ubR8KVvXu9nJH+P5w1j2SQSNNqto+H0DA==", + "devOptional": true, + "dependencies": { + "gaxios": "^5.0.0", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/firebase-tools/node_modules/@google-cloud/pubsub/node_modules/google-auth-library": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-8.0.2.tgz", + "integrity": "sha512-HoG+nWFAThLovKpvcbYzxgn+nBJPTfAwtq0GxPN821nOO+21+8oP7MoEHfd1sbDulUFFGfcjJr2CnJ4YssHcyg==", + "devOptional": true, + "dependencies": { + "arrify": "^2.0.0", + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "fast-text-encoding": "^1.0.0", + "gaxios": "^5.0.0", + "gcp-metadata": "^5.0.0", + "gtoken": "^5.3.2", + "jws": "^4.0.0", + "lru-cache": "^6.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/firebase-tools/node_modules/@google-cloud/pubsub/node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "devOptional": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/firebase-tools/node_modules/@google-cloud/pubsub/node_modules/jwa": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz", + "integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==", + "devOptional": true, + "dependencies": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/firebase-tools/node_modules/@google-cloud/pubsub/node_modules/jws": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", + "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", + "devOptional": true, + "dependencies": { + "jwa": "^2.0.0", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/firebase-tools/node_modules/@grpc/grpc-js": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.7.3.tgz", + "integrity": "sha512-H9l79u4kJ2PVSxUNA08HMYAnUBLj9v6KjYQ7SQ71hOZcEXhShE/y5iQCesP8+6/Ik/7i2O0a10bPquIcYfufog==", + "devOptional": true, + "dependencies": { + "@grpc/proto-loader": "^0.7.0", + "@types/node": ">=12.12.47" + }, + "engines": { + "node": "^8.13.0 || >=10.10.0" + } + }, + "node_modules/firebase-tools/node_modules/@grpc/grpc-js/node_modules/@grpc/proto-loader": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.4.tgz", + "integrity": "sha512-MnWjkGwqQ3W8fx94/c1CwqLsNmHHv2t0CFn+9++6+cDphC1lolpg9M2OU0iebIjK//pBNX9e94ho+gjx6vz39w==", + "devOptional": true, + "dependencies": { + "@types/long": "^4.0.1", + "lodash.camelcase": "^4.3.0", + "long": "^4.0.0", + "protobufjs": "^7.0.0", + "yargs": "^16.2.0" + }, + "bin": { + "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/firebase-tools/node_modules/@grpc/grpc-js/node_modules/protobufjs": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.1.2.tgz", + "integrity": "sha512-4ZPTPkXCdel3+L81yw3dG6+Kq3umdWKh7Dc7GW/CpNk4SX3hK58iPCWeCyhVTDrbkNeKrYNZ7EojM5WDaEWTLQ==", + "devOptional": true, + "hasInstallScript": true, + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/node": ">=13.7.0", + "long": "^5.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/firebase-tools/node_modules/@grpc/grpc-js/node_modules/protobufjs/node_modules/long": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/long/-/long-5.2.1.tgz", + "integrity": "sha512-GKSNGeNAtw8IryjjkhZxuKB3JzlcLTwjtiQCHKvqQet81I93kXslhDQruGI/QsddO83mcDToBVy7GqGS/zYf/A==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/@jsdevtools/ono": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz", + "integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/@jsdoc/salty": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@jsdoc/salty/-/salty-0.2.3.tgz", + "integrity": "sha512-bbtCxCkxcnWhi50I+4Lj6mdz9w3pOXOgEQrID8TCZ/DF51fW7M9GCQW2y45SpBDdHd1Eirm1X/Cf6CkAAe8HPg==", + "devOptional": true, + "dependencies": { + "lodash": "^4.17.21" + }, + "engines": { + "node": ">=v12.0.0" + } + }, + "node_modules/firebase-tools/node_modules/@npmcli/fs": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-2.1.2.tgz", + "integrity": "sha512-yOJKRvohFOaLqipNtwYB9WugyZKhC/DZC4VYPmpaCzDBrA8YpK3qHZ8/HGscMnE4GqbkLNuVcCnxkeQEdGt6LQ==", + "optional": true, + "dependencies": { + "@gar/promisify": "^1.1.3", + "semver": "^7.3.5" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/firebase-tools/node_modules/@npmcli/fs/node_modules/semver": { + "version": "7.3.7", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz", + "integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==", + "optional": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/firebase-tools/node_modules/@npmcli/move-file": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@npmcli/move-file/-/move-file-2.0.1.tgz", + "integrity": "sha512-mJd2Z5TjYWq/ttPLLGqArdtnC74J6bOzg4rMDnN+p1xTacZ2yPRCk2y0oSWQtygLR9YVQXgOcONrwtnk3JupxQ==", + "optional": true, + "dependencies": { + "mkdirp": "^1.0.4", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/firebase-tools/node_modules/@npmcli/move-file/node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "optional": true, + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/firebase-tools/node_modules/@opentelemetry/api": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.0.4.tgz", + "integrity": "sha512-BuJuXRSJNQ3QoKA6GWWDyuLpOUck+9hAXNMCnrloc1aWVoy6Xq6t9PUV08aBZ4Lutqq2LEHM486bpZqoViScog==", + "devOptional": true, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/firebase-tools/node_modules/@opentelemetry/semantic-conventions": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.2.0.tgz", + "integrity": "sha512-BNKB9fiYVghALJzCuWO3eNYfdTExPVK4ykrtmfNfy0A6UWYhOYjGMXifUmkunDJNL8ju9tBobo8jF0WR9zGy1Q==", + "devOptional": true, + "engines": { + "node": ">=8.12.0" + } + }, + "node_modules/firebase-tools/node_modules/@pnpm/network.ca-file": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@pnpm/network.ca-file/-/network.ca-file-1.0.2.tgz", + "integrity": "sha512-YcPQ8a0jwYU9bTdJDpXjMi7Brhkr1mXsXrUJvjqM2mQDgkRiz8jFaQGOdaLxgjtUfQgZhKy/O3cG/YwmgKaxLA==", + "devOptional": true, + "dependencies": { + "graceful-fs": "4.2.10" + }, + "engines": { + "node": ">=12.22.0" + } + }, + "node_modules/firebase-tools/node_modules/@pnpm/npm-conf": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@pnpm/npm-conf/-/npm-conf-1.0.5.tgz", + "integrity": "sha512-hD8ml183638O3R6/Txrh0L8VzGOrFXgRtRDG4qQC4tONdZ5Z1M+tlUUDUvrjYdmK6G+JTBTeaCLMna11cXzi8A==", + "devOptional": true, + "dependencies": { + "@pnpm/network.ca-file": "^1.0.1", + "config-chain": "^1.1.11" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/firebase-tools/node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha1-m4sMxmPWaafY9vXQiToU00jzD78=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/@protobufjs/codegen": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/@protobufjs/eventemitter": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha1-NVy8mLr61ZePntCV85diHx0Ga3A=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/@protobufjs/fetch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", + "integrity": "sha1-upn7WYYUr2VwDBYZ/wbUVLDYTEU=", + "devOptional": true, + "dependencies": { + "@protobufjs/aspromise": "^1.1.1", + "@protobufjs/inquire": "^1.1.0" + } + }, + "node_modules/firebase-tools/node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha1-Xp4avctz/Ap8uLKR33jIy9l7h9E=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/@protobufjs/inquire": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", + "integrity": "sha1-/yAOPnzyQp4tyvwRQIKOjMY48Ik=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha1-bMKyDFya1q0NzP0hynZz2Nf79o0=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha1-Cf0V8tbTq/qbZbw2ZQbWrXhG/1Q=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/@protobufjs/utf8": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", + "integrity": "sha1-p3c2C1s5oaLlEG+OhY8v0tBgxXA=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/@tootallnate/once": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", + "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==", + "devOptional": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/firebase-tools/node_modules/@types/duplexify": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/@types/duplexify/-/duplexify-3.6.0.tgz", + "integrity": "sha512-5zOA53RUlzN74bvrSGwjudssD9F3a797sDZQkiYpUOxW+WHaXTCPz4/d5Dgi6FKnOqZ2CpaTo0DhgIfsXAOE/A==", + "devOptional": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/firebase-tools/node_modules/@types/linkify-it": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-3.0.2.tgz", + "integrity": "sha512-HZQYqbiFVWufzCwexrvh694SOim8z2d+xJl5UNamcvQFejLY/2YUtzXHYi3cHdI7PMlS8ejH2slRAOJQ32aNbA==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/@types/long": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.1.tgz", + "integrity": "sha512-5tXH6Bx/kNGd3MgffdmP4dy2Z+G4eaXw0SE81Tq3BNadtnMR5/ySMzX4SLEzHJzSmPNn4HIdpQsBvXMUykr58w==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/@types/markdown-it": { + "version": "12.2.3", + "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-12.2.3.tgz", + "integrity": "sha512-GKMHFfv3458yYy+v/N8gjufHO6MSZKCOXpZc5GXIWWy8uldwfmPn98vp81gZ5f9SVw8YYBctgfJ22a2d7AOMeQ==", + "devOptional": true, + "dependencies": { + "@types/linkify-it": "*", + "@types/mdurl": "*" + } + }, + "node_modules/firebase-tools/node_modules/@types/mdurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-1.0.2.tgz", + "integrity": "sha512-eC4U9MlIcu2q0KQmXszyn5Akca/0jrQmwDRgpAMJai7qBWq4amIQhZyNau4VYGtCeALvW1/NtjzJJ567aZxfKA==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/@types/node": { + "version": "14.18.36", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.36.tgz", + "integrity": "sha512-FXKWbsJ6a1hIrRxv+FoukuHnGTgEzKYGi7kilfMae96AL9UNkPFNWJEEYWzdRI9ooIkbr4AKldyuSTLql06vLQ==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "optional": true + }, + "node_modules/firebase-tools/node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "devOptional": true, + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, + "node_modules/firebase-tools/node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "devOptional": true, + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/firebase-tools/node_modules/acorn": { + "version": "8.8.2", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz", + "integrity": "sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==", + "devOptional": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/firebase-tools/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==", + "devOptional": true, + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/firebase-tools/node_modules/acorn-walk": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz", + "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==", + "devOptional": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/firebase-tools/node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "devOptional": true, + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/firebase-tools/node_modules/agent-base/node_modules/debug": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "devOptional": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/firebase-tools/node_modules/agent-base/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/agentkeepalive": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.2.1.tgz", + "integrity": "sha512-Zn4cw2NEqd+9fiSVWMscnjyQ1a8Yfoc5oBajLeo5w+YBHgDUcEBY2hS4YpTz6iN5f/2zQiktcuM6tS8x1p9dpA==", + "optional": true, + "dependencies": { + "debug": "^4.1.0", + "depd": "^1.1.2", + "humanize-ms": "^1.2.1" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/firebase-tools/node_modules/agentkeepalive/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "optional": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/firebase-tools/node_modules/agentkeepalive/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "optional": true + }, + "node_modules/firebase-tools/node_modules/aggregate-error": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", + "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "optional": true, + "dependencies": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "devOptional": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/firebase-tools/node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "devOptional": true, + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/firebase-tools/node_modules/ajv-formats/node_modules/ajv": { + "version": "8.8.2", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.8.2.tgz", + "integrity": "sha512-x9VuX+R/jcFj1DHo/fCp99esgGDWiHENrKxaCENuCxpoMCmAt/COCGVDwA7kleEpEzJjDnvh3yGoOuLu0Dtllw==", + "devOptional": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/firebase-tools/node_modules/ajv-formats/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/ansi-align": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.1.tgz", + "integrity": "sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==", + "devOptional": true, + "dependencies": { + "string-width": "^4.1.0" + } + }, + "node_modules/firebase-tools/node_modules/ansi-escapes": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-5.0.0.tgz", + "integrity": "sha512-5GFMVX8HqE/TB+FuBJGuO5XG0WrsA6ptUqoODaT/n9mmUaZFkqnBueB4leqGBCmrUHnCnC4PCZTCd0E7QQ83bA==", + "devOptional": true, + "dependencies": { + "type-fest": "^1.0.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/firebase-tools/node_modules/ansi-escapes/node_modules/type-fest": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-1.4.0.tgz", + "integrity": "sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==", + "devOptional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/firebase-tools/node_modules/ansicolors": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/ansicolors/-/ansicolors-0.3.2.tgz", + "integrity": "sha1-ZlWX3oap/+Oqm/vmyuXG6kJrSXk=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/anymatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", + "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==", + "devOptional": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/firebase-tools/node_modules/aproba": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", + "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==", + "optional": true + }, + "node_modules/firebase-tools/node_modules/archiver": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/archiver/-/archiver-5.3.1.tgz", + "integrity": "sha512-8KyabkmbYrH+9ibcTScQ1xCJC/CGcugdVIwB+53f5sZziXgwUh3iXlAlANMxcZyDEfTHMe6+Z5FofV8nopXP7w==", + "devOptional": true, + "dependencies": { + "archiver-utils": "^2.1.0", + "async": "^3.2.3", + "buffer-crc32": "^0.2.1", + "readable-stream": "^3.6.0", + "readdir-glob": "^1.0.0", + "tar-stream": "^2.2.0", + "zip-stream": "^4.1.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/firebase-tools/node_modules/archiver-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-2.1.0.tgz", + "integrity": "sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw==", + "devOptional": true, + "dependencies": { + "glob": "^7.1.4", + "graceful-fs": "^4.2.0", + "lazystream": "^1.0.0", + "lodash.defaults": "^4.2.0", + "lodash.difference": "^4.5.0", + "lodash.flatten": "^4.4.0", + "lodash.isplainobject": "^4.0.6", + "lodash.union": "^4.6.0", + "normalize-path": "^3.0.0", + "readable-stream": "^2.0.0" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/firebase-tools/node_modules/archiver-utils/node_modules/readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "devOptional": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/firebase-tools/node_modules/archiver/node_modules/async": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.3.tgz", + "integrity": "sha512-spZRyzKL5l5BZQrr/6m/SqFdBN0q3OCI0f9rjfBzCMBIP4p75P620rR3gTmaksNOhmzgdxcaxdNfMy6anrbM0g==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/are-we-there-yet": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-3.0.1.tgz", + "integrity": "sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg==", + "optional": true, + "dependencies": { + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/firebase-tools/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "devOptional": true, + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/firebase-tools/node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/arrify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-2.0.1.tgz", + "integrity": "sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==", + "devOptional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/as-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/as-array/-/as-array-2.0.0.tgz", + "integrity": "sha1-TwSAXYf4/OjlEbwhCPjl46KH1Uc=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/asn1": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", + "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", + "devOptional": true, + "dependencies": { + "safer-buffer": "~2.1.0" + } + }, + "node_modules/firebase-tools/node_modules/assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", + "devOptional": true, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/firebase-tools/node_modules/ast-types": { + "version": "0.13.4", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz", + "integrity": "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==", + "devOptional": true, + "dependencies": { + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/firebase-tools/node_modules/ast-types/node_modules/tslib": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz", + "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/async": { + "version": "2.6.4", + "resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz", + "integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==", + "devOptional": true, + "dependencies": { + "lodash": "^4.17.14" + } + }, + "node_modules/firebase-tools/node_modules/async-lock": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/async-lock/-/async-lock-1.3.2.tgz", + "integrity": "sha512-phnXdS3RP7PPcmP6NWWzWMU0sLTeyvtZCxBPpZdkYE3seGLKSQZs9FrmVO/qwypq98FUtWWUEYxziLkdGk5nnA==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/aws-sign2": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", + "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=", + "devOptional": true, + "engines": { + "node": "*" + } + }, + "node_modules/firebase-tools/node_modules/aws4": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz", + "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "devOptional": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/firebase-tools/node_modules/basic-auth": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", + "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==", + "devOptional": true, + "dependencies": { + "safe-buffer": "5.1.2" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/firebase-tools/node_modules/basic-auth-connect": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/basic-auth-connect/-/basic-auth-connect-1.0.0.tgz", + "integrity": "sha1-/bC0OWLKe0BFanwrtI/hc9otISI=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", + "devOptional": true, + "dependencies": { + "tweetnacl": "^0.14.3" + } + }, + "node_modules/firebase-tools/node_modules/bignumber.js": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.0.2.tgz", + "integrity": "sha512-GAcQvbpsM0pUb0zw1EI0KhQEZ+lRwR5fYaAp3vPOYuP7aDvGy6cVN6XHLauvF8SOga2y0dcLcjt3iQDTSEliyw==", + "devOptional": true, + "engines": { + "node": "*" + } + }, + "node_modules/firebase-tools/node_modules/binary-extensions": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.0.0.tgz", + "integrity": "sha512-Phlt0plgpIIBOGTT/ehfFnbNlfsDEiqmzE2KRXoX1bLIlir4X/MR+zSyBEkL05ffWgnRSf/DXv+WrUAVr93/ow==", + "devOptional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "devOptional": true, + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/firebase-tools/node_modules/body-parser": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", + "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==", + "devOptional": true, + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.4", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.11.0", + "raw-body": "2.5.1", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/firebase-tools/node_modules/body-parser/node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "devOptional": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/firebase-tools/node_modules/body-parser/node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "devOptional": true, + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/firebase-tools/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "devOptional": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/firebase-tools/node_modules/braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "devOptional": true, + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "devOptional": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/firebase-tools/node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI=", + "devOptional": true, + "engines": { + "node": "*" + } + }, + "node_modules/firebase-tools/node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "devOptional": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/firebase-tools/node_modules/cacache": { + "version": "16.1.3", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-16.1.3.tgz", + "integrity": "sha512-/+Emcj9DAXxX4cwlLmRI9c166RuL3w30zp4R7Joiv2cQTtTtA+jeuCAjH3ZlGnYS3tKENSrKhAzVVP9GVyzeYQ==", + "optional": true, + "dependencies": { + "@npmcli/fs": "^2.1.0", + "@npmcli/move-file": "^2.0.0", + "chownr": "^2.0.0", + "fs-minipass": "^2.1.0", + "glob": "^8.0.1", + "infer-owner": "^1.0.4", + "lru-cache": "^7.7.1", + "minipass": "^3.1.6", + "minipass-collect": "^1.0.2", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "mkdirp": "^1.0.4", + "p-map": "^4.0.0", + "promise-inflight": "^1.0.1", + "rimraf": "^3.0.2", + "ssri": "^9.0.0", + "tar": "^6.1.11", + "unique-filename": "^2.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/firebase-tools/node_modules/cacache/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "optional": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/firebase-tools/node_modules/cacache/node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "optional": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/firebase-tools/node_modules/cacache/node_modules/glob": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.0.3.tgz", + "integrity": "sha512-ull455NHSHI/Y1FqGaaYFaLGkNMMJbavMrEGFXG/PGrg6y7sutWHUHrz6gy6WEBH6akM1M414dWKCNs+IhKdiQ==", + "optional": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/firebase-tools/node_modules/cacache/node_modules/lru-cache": { + "version": "7.14.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.14.0.tgz", + "integrity": "sha512-EIRtP1GrSJny0dqb50QXRUNBxHJhcpxHC++M5tD7RYbvLLn5KVWKsbyswSSqDuU15UFi3bgTQIY8nhDMeF6aDQ==", + "optional": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/firebase-tools/node_modules/cacache/node_modules/minimatch": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.0.tgz", + "integrity": "sha512-9TPBGGak4nHfGZsPBohm9AWg6NoT7QTCehS3BIJABslyZbzxfV78QM2Y6+i741OPZIafFAaiiEMh5OyIrJPgtg==", + "optional": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/firebase-tools/node_modules/cacache/node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "optional": true, + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/firebase-tools/node_modules/cacache/node_modules/p-map": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", + "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", + "optional": true, + "dependencies": { + "aggregate-error": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/firebase-tools/node_modules/call-bind": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", + "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "devOptional": true, + "dependencies": { + "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/firebase-tools/node_modules/call-me-maybe": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.1.tgz", + "integrity": "sha1-JtII6onje1y95gJQoV8DHBak1ms=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/cardinal": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/cardinal/-/cardinal-2.1.1.tgz", + "integrity": "sha1-fMEFXYItISlU0HsIXeolHMe8VQU=", + "devOptional": true, + "dependencies": { + "ansicolors": "~0.3.2", + "redeyed": "~2.1.0" + }, + "bin": { + "cdl": "bin/cdl.js" + } + }, + "node_modules/firebase-tools/node_modules/caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/catharsis": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/catharsis/-/catharsis-0.9.0.tgz", + "integrity": "sha512-prMTQVpcns/tzFgFVkVp6ak6RykZyWb3gu8ckUpd6YkTlacOd3DXGJjIpD4Q6zJirizvaiAjSSHlOsA+6sNh2A==", + "devOptional": true, + "dependencies": { + "lodash": "^4.17.15" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/firebase-tools/node_modules/chardet": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", + "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/chokidar": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "devOptional": true, + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ], + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/firebase-tools/node_modules/ci-info": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz", + "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/cjson": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/cjson/-/cjson-0.3.3.tgz", + "integrity": "sha1-qS2ceG5b+bkwgGMp7gXV0yYbSvo=", + "devOptional": true, + "dependencies": { + "json-parse-helpfulerror": "^1.0.3" + }, + "engines": { + "node": ">= 0.3.0" + } + }, + "node_modules/firebase-tools/node_modules/clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", + "optional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/firebase-tools/node_modules/cli-boxes": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-2.2.1.tgz", + "integrity": "sha512-y4coMcylgSCdVinjiDBuR8PCC2bLjyGTwEmPb9NHR/QaNU6EUOXcTY/s6VjGMD6ENSEaeQYHCY0GNGS5jfMwPw==", + "devOptional": true, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/firebase-tools/node_modules/cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "devOptional": true, + "dependencies": { + "restore-cursor": "^3.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/cli-spinners": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.6.1.tgz", + "integrity": "sha512-x/5fWmGMnbKQAaNwN+UZlV79qBLM9JFnJuJ03gIi5whrob0xV0ofNVHy9DhwGdsMJQc2OKv0oGmLzvaqvAVv+g==", + "devOptional": true, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/firebase-tools/node_modules/cli-table": { + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/cli-table/-/cli-table-0.3.11.tgz", + "integrity": "sha512-IqLQi4lO0nIB4tcdTpN4LCB9FI3uqrJZK7RC515EnhZ6qBaglkIgICb1wjeAqpdoOabm1+SuQtkXIPdYC93jhQ==", + "devOptional": true, + "dependencies": { + "colors": "1.0.3" + }, + "engines": { + "node": ">= 0.2.0" + } + }, + "node_modules/firebase-tools/node_modules/cli-table3": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.2.tgz", + "integrity": "sha512-QyavHCaIC80cMivimWu4aWHilIpiDpfm3hGmqAmXVL1UsnbLuBSMd21hTX6VY4ZSDSM73ESLeF8TOYId3rBTbw==", + "devOptional": true, + "dependencies": { + "string-width": "^4.2.0" + }, + "engines": { + "node": "10.* || >= 12.*" + }, + "optionalDependencies": { + "@colors/colors": "1.5.0" + } + }, + "node_modules/firebase-tools/node_modules/cli-width": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-3.0.0.tgz", + "integrity": "sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==", + "devOptional": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/firebase-tools/node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "devOptional": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/firebase-tools/node_modules/clone": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha1-2jCcwmPfFZlMaIypAheco8fNfH4=", + "devOptional": true, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/firebase-tools/node_modules/color": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/color/-/color-3.0.0.tgz", + "integrity": "sha512-jCpd5+s0s0t7p3pHQKpnJ0TpQKKdleP71LWcA0aqiljpiuAkOSUFN/dyH8ZwF0hRmFlrIuRhufds1QyEP9EB+w==", + "devOptional": true, + "dependencies": { + "color-convert": "^1.9.1", + "color-string": "^1.5.2" + } + }, + "node_modules/firebase-tools/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "devOptional": true, + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/firebase-tools/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/color-string": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.5.5.tgz", + "integrity": "sha512-jgIoum0OfQfq9Whcfc2z/VhCNcmQjWbey6qBX0vqt7YICflUmBCh9E9CiQD5GSJ+Uehixm3NUwHVhqUAWRivZg==", + "devOptional": true, + "dependencies": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, + "node_modules/firebase-tools/node_modules/color-support": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", + "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", + "optional": true, + "bin": { + "color-support": "bin.js" + } + }, + "node_modules/firebase-tools/node_modules/colorette": { + "version": "2.0.19", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.19.tgz", + "integrity": "sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/colornames": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/colornames/-/colornames-1.1.1.tgz", + "integrity": "sha1-+IiQMGhcfE/54qVZ9Qd+t2qBb5Y=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/colors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.0.3.tgz", + "integrity": "sha1-BDP0TYCWgP3rYO0mDxsMJi6CpAs=", + "devOptional": true, + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/firebase-tools/node_modules/colorspace": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/colorspace/-/colorspace-1.1.2.tgz", + "integrity": "sha512-vt+OoIP2d76xLhjwbBaucYlNSpPsrJWPlBTtwCpQKIu6/CSMutyzX93O/Do0qzpH3YoHEes8YEFXyZ797rEhzQ==", + "devOptional": true, + "dependencies": { + "color": "3.0.x", + "text-hex": "1.0.x" + } + }, + "node_modules/firebase-tools/node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "devOptional": true, + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/firebase-tools/node_modules/commander": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.0.1.tgz", + "integrity": "sha512-IPF4ouhCP+qdlcmCedhxX4xiGBPyigb8v5NeUp+0LyhwLgxMqyp3S0vl7TAPfS/hiP7FC3caI/PB9lTmP8r1NA==", + "devOptional": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/firebase-tools/node_modules/compress-commons": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-4.1.1.tgz", + "integrity": "sha512-QLdDLCKNV2dtoTorqgxngQCMA+gWXkM/Nwu7FpeBhk/RdkzimqC3jueb/FDmaZeXh+uby1jkBqE3xArsLBE5wQ==", + "devOptional": true, + "dependencies": { + "buffer-crc32": "^0.2.13", + "crc32-stream": "^4.0.2", + "normalize-path": "^3.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/firebase-tools/node_modules/compressible": { + "version": "2.0.17", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.17.tgz", + "integrity": "sha512-BGHeLCK1GV7j1bSmQQAi26X+GgWcTjLr/0tzSvMCl3LH1w1IJ4PFSPoV5316b30cneTziC+B1a+3OjoSUcQYmw==", + "devOptional": true, + "dependencies": { + "mime-db": ">= 1.40.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/firebase-tools/node_modules/compression": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.7.4.tgz", + "integrity": "sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==", + "devOptional": true, + "dependencies": { + "accepts": "~1.3.5", + "bytes": "3.0.0", + "compressible": "~2.0.16", + "debug": "2.6.9", + "on-headers": "~1.0.2", + "safe-buffer": "5.1.2", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/firebase-tools/node_modules/compression/node_modules/bytes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", + "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=", + "devOptional": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/firebase-tools/node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/config-chain": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.13.tgz", + "integrity": "sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==", + "devOptional": true, + "dependencies": { + "ini": "^1.3.4", + "proto-list": "~1.2.1" + } + }, + "node_modules/firebase-tools/node_modules/configstore": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/configstore/-/configstore-5.0.1.tgz", + "integrity": "sha512-aMKprgk5YhBNyH25hj8wGt2+D52Sw1DRRIzqBwLp2Ya9mFmY8KPvvtvmna8SxVR9JMZ4kzMD68N22vlaRpkeFA==", + "devOptional": true, + "dependencies": { + "dot-prop": "^5.2.0", + "graceful-fs": "^4.1.2", + "make-dir": "^3.0.0", + "unique-string": "^2.0.0", + "write-file-atomic": "^3.0.0", + "xdg-basedir": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/configstore/node_modules/crypto-random-string": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz", + "integrity": "sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==", + "devOptional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/configstore/node_modules/dot-prop": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.2.0.tgz", + "integrity": "sha512-uEUyaDKoSQ1M4Oq8l45hSE26SnTxL6snNnqvK/VWx5wJhmff5z0FUVJDKDanor/6w3kzE3i7XZOk+7wC0EXr1A==", + "devOptional": true, + "dependencies": { + "is-obj": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/configstore/node_modules/unique-string": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-2.0.0.tgz", + "integrity": "sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==", + "devOptional": true, + "dependencies": { + "crypto-random-string": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/connect": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/connect/-/connect-3.7.0.tgz", + "integrity": "sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ==", + "devOptional": true, + "dependencies": { + "debug": "2.6.9", + "finalhandler": "1.1.2", + "parseurl": "~1.3.3", + "utils-merge": "1.0.1" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/firebase-tools/node_modules/console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==", + "optional": true + }, + "node_modules/firebase-tools/node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "devOptional": true, + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/firebase-tools/node_modules/content-disposition/node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "devOptional": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/firebase-tools/node_modules/content-type": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", + "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==", + "devOptional": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/firebase-tools/node_modules/cookie": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", + "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", + "devOptional": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/firebase-tools/node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "devOptional": true, + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/firebase-tools/node_modules/crc-32": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", + "devOptional": true, + "bin": { + "crc32": "bin/crc32.njs" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/firebase-tools/node_modules/crc32-stream": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-4.0.2.tgz", + "integrity": "sha512-DxFZ/Hk473b/muq1VJ///PMNLj0ZMnzye9thBpmjpJKCc5eMgB95aK8zCGrGfQ90cWo561Te6HK9D+j4KPdM6w==", + "devOptional": true, + "dependencies": { + "crc-32": "^1.2.0", + "readable-stream": "^3.4.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/firebase-tools/node_modules/cross-env": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-5.2.0.tgz", + "integrity": "sha512-jtdNFfFW1hB7sMhr/H6rW1Z45LFqyI431m3qU6bFXcQ3Eh7LtBuG3h74o7ohHZ3crrRkkqHlo4jYHFPcjroANg==", + "devOptional": true, + "dependencies": { + "cross-spawn": "^6.0.5", + "is-windows": "^1.0.0" + }, + "bin": { + "cross-env": "dist/bin/cross-env.js", + "cross-env-shell": "dist/bin/cross-env-shell.js" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/firebase-tools/node_modules/cross-env/node_modules/cross-spawn": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", + "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", + "devOptional": true, + "dependencies": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + }, + "engines": { + "node": ">=4.8" + } + }, + "node_modules/firebase-tools/node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "devOptional": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/firebase-tools/node_modules/cross-spawn/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==", + "devOptional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/cross-spawn/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==", + "devOptional": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/cross-spawn/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==", + "devOptional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/cross-spawn/node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "devOptional": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/firebase-tools/node_modules/csv-parse": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/csv-parse/-/csv-parse-5.0.4.tgz", + "integrity": "sha512-5AIdl8l6n3iYQYxan5djB5eKDa+vBnhfWZtRpJTcrETWfVLYN0WSj3L9RwvgYt+psoO77juUr8TG8qpfGZifVQ==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", + "devOptional": true, + "dependencies": { + "assert-plus": "^1.0.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/firebase-tools/node_modules/data-uri-to-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-3.0.1.tgz", + "integrity": "sha512-WboRycPNsVw3B3TL559F7kuBUM4d8CgMEvk6xEJlOp7OBPjt6G7z8WMWlD2rOFZLk6OYfFIUGsCOWzcQH9K2og==", + "devOptional": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/firebase-tools/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "devOptional": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/firebase-tools/node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "devOptional": true, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/firebase-tools/node_modules/deep-freeze": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/deep-freeze/-/deep-freeze-0.0.1.tgz", + "integrity": "sha1-OgsABd4YZygZ39OM0x+RF5yJPoQ=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/deep-is": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", + "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/defaults": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.3.tgz", + "integrity": "sha1-xlYFHpgX2f8I7YgUd/P+QBnz730=", + "devOptional": true, + "dependencies": { + "clone": "^1.0.2" + } + }, + "node_modules/firebase-tools/node_modules/degenerator": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-3.0.1.tgz", + "integrity": "sha512-LFsIFEeLPlKvAKXu7j3ssIG6RT0TbI7/GhsqrI0DnHASEQjXQ0LUSYcjJteGgRGmZbl1TnMSxpNQIAiJ7Du5TQ==", + "devOptional": true, + "dependencies": { + "ast-types": "^0.13.2", + "escodegen": "^1.8.1", + "esprima": "^4.0.0", + "vm2": "^3.9.3" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/firebase-tools/node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", + "devOptional": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/firebase-tools/node_modules/delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", + "optional": true + }, + "node_modules/firebase-tools/node_modules/depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=", + "optional": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/firebase-tools/node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "devOptional": true, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/firebase-tools/node_modules/diagnostics": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/diagnostics/-/diagnostics-1.1.1.tgz", + "integrity": "sha512-8wn1PmdunLJ9Tqbx+Fx/ZEuHfJf4NKSN2ZBj7SJC/OWRWha843+WsTjqMe1B5E3p28jqBlp+mJ2fPVxPyNgYKQ==", + "devOptional": true, + "dependencies": { + "colorspace": "1.1.x", + "enabled": "1.0.x", + "kuler": "1.0.x" + } + }, + "node_modules/firebase-tools/node_modules/duplexify": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.1.tgz", + "integrity": "sha512-DY3xVEmVHTv1wSzKNbwoU6nVjzI369Y6sPoqfYr0/xlx3IdX2n94xIszTcjPO8W8ZIv0Wb0PXNcjuZyT4wiICA==", + "devOptional": true, + "dependencies": { + "end-of-stream": "^1.4.1", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1", + "stream-shift": "^1.0.0" + } + }, + "node_modules/firebase-tools/node_modules/ecc-jsbn": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", + "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", + "devOptional": true, + "dependencies": { + "jsbn": "~0.1.0", + "safer-buffer": "^2.1.0" + } + }, + "node_modules/firebase-tools/node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "devOptional": true, + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/firebase-tools/node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/enabled": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/enabled/-/enabled-1.0.2.tgz", + "integrity": "sha1-ll9lE9LC0cX0ZStkouM5ZGf8L5M=", + "devOptional": true, + "dependencies": { + "env-variable": "0.0.x" + } + }, + "node_modules/firebase-tools/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=", + "devOptional": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/firebase-tools/node_modules/encoding": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", + "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", + "optional": true, + "dependencies": { + "iconv-lite": "^0.6.2" + } + }, + "node_modules/firebase-tools/node_modules/encoding/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "optional": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/firebase-tools/node_modules/end-of-stream": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.1.tgz", + "integrity": "sha512-1MkrZNvWTKCaigbn+W15elq2BB/L22nqrSY5DKlo3X6+vclJm8Bb5djXJBmEX6fS3+zCh/F4VBK5Z2KxJt4s2Q==", + "devOptional": true, + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/firebase-tools/node_modules/entities": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.1.0.tgz", + "integrity": "sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w==", + "devOptional": true, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/firebase-tools/node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "optional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/firebase-tools/node_modules/env-variable": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/env-variable/-/env-variable-0.0.6.tgz", + "integrity": "sha512-bHz59NlBbtS0NhftmR8+ExBEekE7br0e01jw+kk0NDro7TtZzBYZ5ScGPs3OmwnpyfHTHOtr1Y6uedCdrIldtg==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/err-code": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", + "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", + "optional": true + }, + "node_modules/firebase-tools/node_modules/escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "devOptional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/firebase-tools/node_modules/escape-goat": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/escape-goat/-/escape-goat-2.1.1.tgz", + "integrity": "sha512-8/uIhbG12Csjy2JEW7D9pHbreaVaS/OpN3ycnyvElTdwM5n6GY6W6e2IPemfvGZeUMqZ9A/3GqIZMgKnBhAw/Q==", + "devOptional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "devOptional": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/firebase-tools/node_modules/escodegen": { + "version": "1.14.3", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.14.3.tgz", + "integrity": "sha512-qFcX0XJkdg+PB3xjZZG/wKSuT1PnQWx57+TVSjIMmILd2yC/6ByYElPwJnslDsuWuSAp4AwJGumarAAmJch5Kw==", + "devOptional": true, + "dependencies": { + "esprima": "^4.0.1", + "estraverse": "^4.2.0", + "esutils": "^2.0.2", + "optionator": "^0.8.1" + }, + "bin": { + "escodegen": "bin/escodegen.js", + "esgenerate": "bin/esgenerate.js" + }, + "engines": { + "node": ">=4.0" + }, + "optionalDependencies": { + "source-map": "~0.6.1" + } + }, + "node_modules/firebase-tools/node_modules/eslint-visitor-keys": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz", + "integrity": "sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA==", + "devOptional": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/firebase-tools/node_modules/espree": { + "version": "9.4.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.4.1.tgz", + "integrity": "sha512-XwctdmTO6SIvCzd9810yyNzIrOrqNYV9Koizx4C/mRhf9uq0o4yHoCEU/670pOxOL/MSraektvSAji79kX90Vg==", + "devOptional": true, + "dependencies": { + "acorn": "^8.8.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/firebase-tools/node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "devOptional": true, + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/firebase-tools/node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "devOptional": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/firebase-tools/node_modules/esutils": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz", + "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=", + "devOptional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/firebase-tools/node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "devOptional": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/firebase-tools/node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "devOptional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/firebase-tools/node_modules/events-listener": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/events-listener/-/events-listener-1.1.0.tgz", + "integrity": "sha512-Kd3EgYfODHueq6GzVfs/VUolh2EgJsS8hkO3KpnDrxVjU3eq63eXM2ujXkhPP+OkeUOhL8CxdfZbQXzryb5C4g==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/exegesis": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/exegesis/-/exegesis-4.1.0.tgz", + "integrity": "sha512-iqc55n+hmv8d1KYNMjq7bCcp4u74oRY6MBcj6Vsux7Wd4mRvlgahKqrBTyLIWwscNjEF3qvPmeJ0RPTj8ORMNg==", + "devOptional": true, + "dependencies": { + "@apidevtools/json-schema-ref-parser": "^9.0.3", + "ajv": "^8.3.0", + "ajv-formats": "^2.1.0", + "body-parser": "^1.18.3", + "content-type": "^1.0.4", + "deep-freeze": "0.0.1", + "events-listener": "^1.1.0", + "glob": "^7.1.3", + "json-ptr": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "lodash": "^4.17.11", + "openapi3-ts": "^2.0.1", + "promise-breaker": "^5.0.0", + "pump": "^3.0.0", + "qs": "^6.6.0", + "raw-body": "^2.3.3", + "semver": "^7.0.0" + }, + "engines": { + "node": ">=6.0.0", + "npm": ">5.0.0" + } + }, + "node_modules/firebase-tools/node_modules/exegesis-express": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/exegesis-express/-/exegesis-express-4.0.0.tgz", + "integrity": "sha512-V2hqwTtYRj0bj43K4MCtm0caD97YWkqOUHFMRCBW5L1x9IjyqOEc7Xa4oQjjiFbeFOSQzzwPV+BzXsQjSz08fw==", + "devOptional": true, + "dependencies": { + "exegesis": "^4.1.0" + }, + "engines": { + "node": ">=6.0.0", + "npm": ">5.0.0" + } + }, + "node_modules/firebase-tools/node_modules/exegesis/node_modules/ajv": { + "version": "8.8.2", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.8.2.tgz", + "integrity": "sha512-x9VuX+R/jcFj1DHo/fCp99esgGDWiHENrKxaCENuCxpoMCmAt/COCGVDwA7kleEpEzJjDnvh3yGoOuLu0Dtllw==", + "devOptional": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/firebase-tools/node_modules/exegesis/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/exegesis/node_modules/semver": { + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", + "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", + "devOptional": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/firebase-tools/node_modules/express": { + "version": "4.18.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz", + "integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==", + "devOptional": true, + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.1", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.5.0", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.2.0", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.7", + "qs": "6.11.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.18.0", + "serve-static": "1.15.0", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/firebase-tools/node_modules/express/node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "devOptional": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/firebase-tools/node_modules/express/node_modules/finalhandler": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", + "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", + "devOptional": true, + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/firebase-tools/node_modules/express/node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "devOptional": true, + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/firebase-tools/node_modules/express/node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "devOptional": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/firebase-tools/node_modules/express/node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "devOptional": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/firebase-tools/node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/external-editor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", + "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", + "devOptional": true, + "dependencies": { + "chardet": "^0.7.0", + "iconv-lite": "^0.4.24", + "tmp": "^0.0.33" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/firebase-tools/node_modules/external-editor/node_modules/tmp": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "devOptional": true, + "dependencies": { + "os-tmpdir": "~1.0.2" + }, + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/firebase-tools/node_modules/extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=", + "devOptional": true, + "engines": [ + "node >=0.6.0" + ] + }, + "node_modules/firebase-tools/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==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/fast-json-stable-stringify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", + "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/fast-safe-stringify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", + "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/fast-text-encoding": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/fast-text-encoding/-/fast-text-encoding-1.0.3.tgz", + "integrity": "sha512-dtm4QZH9nZtcDt8qJiOH9fcQd1NAgi+K1O2DbE6GG1PPCK/BWfOH3idCTRQ4ImXRUOyopDEgDEnVEE7Y/2Wrig==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/fast-url-parser": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/fast-url-parser/-/fast-url-parser-1.1.3.tgz", + "integrity": "sha1-9K8+qfNNiicc9YrSs3WfQx8LMY0=", + "devOptional": true, + "dependencies": { + "punycode": "^1.3.2" + } + }, + "node_modules/firebase-tools/node_modules/fast-url-parser/node_modules/punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/fecha": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fecha/-/fecha-2.3.3.tgz", + "integrity": "sha512-lUGBnIamTAwk4znq5BcqsDaxSmZ9nDVJaij6NvRt/Tg4R69gERA+otPKbS86ROw9nxVMw2/mp1fnaiWqbs6Sdg==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/figures": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", + "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", + "devOptional": true, + "dependencies": { + "escape-string-regexp": "^1.0.5" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/firebase-tools/node_modules/file-uri-to-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-2.0.0.tgz", + "integrity": "sha512-hjPFI8oE/2iQPVe4gbrJ73Pp+Xfub2+WI2LlXDbsaJBwT5wuMh35WNWVYYTpnz895shtwfyutMFLFywpQAFdLg==", + "devOptional": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/firebase-tools/node_modules/filesize": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/filesize/-/filesize-6.1.0.tgz", + "integrity": "sha512-LpCHtPQ3sFx67z+uh2HnSyWSLLu5Jxo21795uRDuar/EOuYWXib5EmPaGIBuSnRqH2IODiKA2k5re/K9OnN/Yg==", + "devOptional": true, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/firebase-tools/node_modules/fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "devOptional": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/finalhandler": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", + "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", + "devOptional": true, + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "statuses": "~1.5.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/firebase-tools/node_modules/forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=", + "devOptional": true, + "engines": { + "node": "*" + } + }, + "node_modules/firebase-tools/node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "devOptional": true, + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/firebase-tools/node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "devOptional": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/firebase-tools/node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=", + "devOptional": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/firebase-tools/node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "devOptional": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/firebase-tools/node_modules/fs-extra/node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "devOptional": true, + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/firebase-tools/node_modules/fs-extra/node_modules/universalify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", + "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", + "devOptional": true, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/firebase-tools/node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "devOptional": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/firebase-tools/node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/firebase-tools/node_modules/ftp": { + "version": "0.3.10", + "resolved": "https://registry.npmjs.org/ftp/-/ftp-0.3.10.tgz", + "integrity": "sha1-kZfYYa2BQvPmPVqDv+TFn3MwiF0=", + "devOptional": true, + "dependencies": { + "readable-stream": "1.1.x", + "xregexp": "2.0.0" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/firebase-tools/node_modules/ftp/node_modules/isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/ftp/node_modules/readable-stream": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", + "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", + "devOptional": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + }, + "node_modules/firebase-tools/node_modules/ftp/node_modules/string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/gauge": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-4.0.4.tgz", + "integrity": "sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg==", + "optional": true, + "dependencies": { + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.3", + "console-control-strings": "^1.1.0", + "has-unicode": "^2.0.1", + "signal-exit": "^3.0.7", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.5" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/firebase-tools/node_modules/gaxios": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-4.1.0.tgz", + "integrity": "sha512-vb0to8xzGnA2qcgywAjtshOKKVDf2eQhJoiL6fHhgW5tVN7wNk7egnYIO9zotfn3lQ3De1VPdf7V5/BWfCtCmg==", + "devOptional": true, + "dependencies": { + "abort-controller": "^3.0.0", + "extend": "^3.0.2", + "https-proxy-agent": "^5.0.0", + "is-stream": "^2.0.0", + "node-fetch": "^2.3.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/firebase-tools/node_modules/gaxios/node_modules/is-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.0.tgz", + "integrity": "sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw==", + "devOptional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/gcp-metadata": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-4.2.0.tgz", + "integrity": "sha512-vQZD57cQkqIA6YPGXM/zc+PIZfNRFdukWGsGZ5+LcJzesi5xp6Gn7a02wRJi4eXPyArNMIYpPET4QMxGqtlk6Q==", + "devOptional": true, + "dependencies": { + "gaxios": "^3.0.0", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/firebase-tools/node_modules/gcp-metadata/node_modules/gaxios": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-3.2.0.tgz", + "integrity": "sha512-+6WPeVzPvOshftpxJwRi2Ozez80tn/hdtOUag7+gajDHRJvAblKxTFSSMPtr2hmnLy7p0mvYz0rMXLBl8pSO7Q==", + "devOptional": true, + "dependencies": { + "abort-controller": "^3.0.0", + "extend": "^3.0.2", + "https-proxy-agent": "^5.0.0", + "is-stream": "^2.0.0", + "node-fetch": "^2.3.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/firebase-tools/node_modules/gcp-metadata/node_modules/is-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.0.tgz", + "integrity": "sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw==", + "devOptional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "devOptional": true, + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/firebase-tools/node_modules/get-intrinsic": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.3.tgz", + "integrity": "sha512-QJVz1Tj7MS099PevUG5jvnt9tSkXN8K14dxQlikJuPt4uD9hHAHjLyLBiLR5zELelBdD9QNRAXZzsJx0WaDL9A==", + "devOptional": true, + "dependencies": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/firebase-tools/node_modules/get-uri": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-3.0.2.tgz", + "integrity": "sha512-+5s0SJbGoyiJTZZ2JTpFPLMPSch72KEqGOTvQsBqg0RBWvwhWUSYZFAtz3TPW0GXJuLBJPts1E241iHg+VRfhg==", + "devOptional": true, + "dependencies": { + "@tootallnate/once": "1", + "data-uri-to-buffer": "3", + "debug": "4", + "file-uri-to-path": "2", + "fs-extra": "^8.1.0", + "ftp": "^0.3.10" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/firebase-tools/node_modules/get-uri/node_modules/debug": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", + "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", + "devOptional": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/firebase-tools/node_modules/get-uri/node_modules/fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "devOptional": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/firebase-tools/node_modules/get-uri/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", + "devOptional": true, + "dependencies": { + "assert-plus": "^1.0.0" + } + }, + "node_modules/firebase-tools/node_modules/glob": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", + "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", + "devOptional": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/firebase-tools/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==", + "devOptional": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/firebase-tools/node_modules/glob-slash": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/glob-slash/-/glob-slash-1.0.0.tgz", + "integrity": "sha1-/lLvpDMjP3Si/mTHq7m8hIICq5U=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/glob-slasher": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/glob-slasher/-/glob-slasher-1.0.1.tgz", + "integrity": "sha1-dHoOW7IiZC7hDT4FRD4QlJPLD44=", + "devOptional": true, + "dependencies": { + "glob-slash": "^1.0.0", + "lodash.isobject": "^2.4.1", + "toxic": "^1.0.0" + } + }, + "node_modules/firebase-tools/node_modules/global-dirs": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-3.0.1.tgz", + "integrity": "sha512-NBcGGFbBA9s1VzD41QXDG+3++t9Mn5t1FpLdhESY6oKY4gYTFpX4wO3sqGUa0Srjtbfj3szX0RnemmrVRUdULA==", + "devOptional": true, + "dependencies": { + "ini": "2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/firebase-tools/node_modules/global-dirs/node_modules/ini": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ini/-/ini-2.0.0.tgz", + "integrity": "sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA==", + "devOptional": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/firebase-tools/node_modules/google-auth-library": { + "version": "7.14.1", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-7.14.1.tgz", + "integrity": "sha512-5Rk7iLNDFhFeBYc3s8l1CqzbEBcdhwR193RlD4vSNFajIcINKI8W8P0JLmBpwymHqqWbX34pJDQu39cSy/6RsA==", + "devOptional": true, + "dependencies": { + "arrify": "^2.0.0", + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "fast-text-encoding": "^1.0.0", + "gaxios": "^4.0.0", + "gcp-metadata": "^4.2.0", + "gtoken": "^5.0.4", + "jws": "^4.0.0", + "lru-cache": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/firebase-tools/node_modules/google-auth-library/node_modules/jwa": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz", + "integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==", + "devOptional": true, + "dependencies": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/firebase-tools/node_modules/google-auth-library/node_modules/jws": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", + "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", + "devOptional": true, + "dependencies": { + "jwa": "^2.0.0", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/firebase-tools/node_modules/google-gax": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/google-gax/-/google-gax-3.5.3.tgz", + "integrity": "sha512-caItkifbcPDVf5xW2j6xLXmuX9bh2dheJN9AzMuQj6VvWacUVV5d0BkjL+Ia8sgX9VvXtbqjf4NKK3nTgV9UUQ==", + "devOptional": true, + "dependencies": { + "@grpc/grpc-js": "~1.7.0", + "@grpc/proto-loader": "^0.7.0", + "@types/long": "^4.0.0", + "abort-controller": "^3.0.0", + "duplexify": "^4.0.0", + "fast-text-encoding": "^1.0.3", + "google-auth-library": "^8.0.2", + "is-stream-ended": "^0.1.4", + "node-fetch": "^2.6.1", + "object-hash": "^3.0.0", + "proto3-json-serializer": "^1.0.0", + "protobufjs": "7.1.2", + "protobufjs-cli": "1.1.0", + "retry-request": "^5.0.0" + }, + "bin": { + "compileProtos": "build/tools/compileProtos.js", + "minifyProtoJson": "build/tools/minify.js" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/firebase-tools/node_modules/google-gax/node_modules/@grpc/proto-loader": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.4.tgz", + "integrity": "sha512-MnWjkGwqQ3W8fx94/c1CwqLsNmHHv2t0CFn+9++6+cDphC1lolpg9M2OU0iebIjK//pBNX9e94ho+gjx6vz39w==", + "devOptional": true, + "dependencies": { + "@types/long": "^4.0.1", + "lodash.camelcase": "^4.3.0", + "long": "^4.0.0", + "protobufjs": "^7.0.0", + "yargs": "^16.2.0" + }, + "bin": { + "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/firebase-tools/node_modules/google-gax/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==", + "devOptional": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/firebase-tools/node_modules/google-gax/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "devOptional": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/firebase-tools/node_modules/google-gax/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "devOptional": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/firebase-tools/node_modules/google-gax/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==", + "devOptional": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/firebase-tools/node_modules/google-gax/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==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/google-gax/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "devOptional": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/firebase-tools/node_modules/google-gax/node_modules/gaxios": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-5.0.2.tgz", + "integrity": "sha512-TjtV2AJOZoMQqRYoy5eM8cCQogYwazWNYLQ72QB0kwa6vHHruYkGmhhyrlzbmgNHK1dNnuP2WSH81urfzyN2Og==", + "devOptional": true, + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^5.0.0", + "is-stream": "^2.0.0", + "node-fetch": "^2.6.7" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/firebase-tools/node_modules/google-gax/node_modules/gcp-metadata": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-5.2.0.tgz", + "integrity": "sha512-aFhhvvNycky2QyhG+dcfEdHBF0FRbYcf39s6WNHUDysKSrbJ5vuFbjydxBcmewtXeV248GP8dWT3ByPNxsyHCw==", + "devOptional": true, + "dependencies": { + "gaxios": "^5.0.0", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/firebase-tools/node_modules/google-gax/node_modules/glob": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "devOptional": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/firebase-tools/node_modules/google-gax/node_modules/google-auth-library": { + "version": "8.7.0", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-8.7.0.tgz", + "integrity": "sha512-1M0NG5VDIvJZEnstHbRdckLZESoJwguinwN8Dhae0j2ZKIQFIV63zxm6Fo6nM4xkgqUr2bbMtV5Dgo+Hy6oo0Q==", + "devOptional": true, + "dependencies": { + "arrify": "^2.0.0", + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "fast-text-encoding": "^1.0.0", + "gaxios": "^5.0.0", + "gcp-metadata": "^5.0.0", + "gtoken": "^6.1.0", + "jws": "^4.0.0", + "lru-cache": "^6.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/firebase-tools/node_modules/google-gax/node_modules/google-p12-pem": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/google-p12-pem/-/google-p12-pem-4.0.1.tgz", + "integrity": "sha512-WPkN4yGtz05WZ5EhtlxNDWPhC4JIic6G8ePitwUWy4l+XPVYec+a0j0Ts47PDtW59y3RwAhUd9/h9ZZ63px6RQ==", + "devOptional": true, + "dependencies": { + "node-forge": "^1.3.1" + }, + "bin": { + "gp12-pem": "build/src/bin/gp12-pem.js" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/firebase-tools/node_modules/google-gax/node_modules/gtoken": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-6.1.2.tgz", + "integrity": "sha512-4ccGpzz7YAr7lxrT2neugmXQ3hP9ho2gcaityLVkiUecAiwiy60Ii8gRbZeOsXV19fYaRjgBSshs8kXw+NKCPQ==", + "devOptional": true, + "dependencies": { + "gaxios": "^5.0.1", + "google-p12-pem": "^4.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/firebase-tools/node_modules/google-gax/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==", + "devOptional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/google-gax/node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "devOptional": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/firebase-tools/node_modules/google-gax/node_modules/jwa": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz", + "integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==", + "devOptional": true, + "dependencies": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/firebase-tools/node_modules/google-gax/node_modules/jws": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", + "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", + "devOptional": true, + "dependencies": { + "jwa": "^2.0.0", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/firebase-tools/node_modules/google-gax/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "devOptional": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/firebase-tools/node_modules/google-gax/node_modules/protobufjs": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.1.2.tgz", + "integrity": "sha512-4ZPTPkXCdel3+L81yw3dG6+Kq3umdWKh7Dc7GW/CpNk4SX3hK58iPCWeCyhVTDrbkNeKrYNZ7EojM5WDaEWTLQ==", + "devOptional": true, + "hasInstallScript": true, + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/node": ">=13.7.0", + "long": "^5.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/firebase-tools/node_modules/google-gax/node_modules/protobufjs-cli": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/protobufjs-cli/-/protobufjs-cli-1.1.0.tgz", + "integrity": "sha512-VXMQn+z3yG2WbN2E+mx5vcyIHF7yJSg2jqyqfxcZLWNOSTqUzSSgAE5vu04/JEpwxTI04JGyrZRDHC36wr04uw==", + "devOptional": true, + "dependencies": { + "chalk": "^4.0.0", + "escodegen": "^1.13.0", + "espree": "^9.0.0", + "estraverse": "^5.1.0", + "glob": "^8.0.0", + "jsdoc": "^4.0.0", + "minimist": "^1.2.0", + "semver": "^7.1.2", + "tmp": "^0.2.1", + "uglify-js": "^3.7.7" + }, + "bin": { + "pbjs": "bin/pbjs", + "pbts": "bin/pbts" + }, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "protobufjs": "^7.0.0" + } + }, + "node_modules/firebase-tools/node_modules/google-gax/node_modules/protobufjs/node_modules/long": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/long/-/long-5.2.1.tgz", + "integrity": "sha512-GKSNGeNAtw8IryjjkhZxuKB3JzlcLTwjtiQCHKvqQet81I93kXslhDQruGI/QsddO83mcDToBVy7GqGS/zYf/A==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/google-gax/node_modules/semver": { + "version": "7.3.8", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", + "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", + "devOptional": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/firebase-tools/node_modules/google-gax/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==", + "devOptional": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/google-p12-pem": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/google-p12-pem/-/google-p12-pem-3.1.3.tgz", + "integrity": "sha512-MC0jISvzymxePDVembypNefkAQp+DRP7dBE+zNUPaIjEspIlYg0++OrsNr248V9tPbz6iqtZ7rX1hxWA5B8qBQ==", + "devOptional": true, + "dependencies": { + "node-forge": "^1.0.0" + }, + "bin": { + "gp12-pem": "build/src/bin/gp12-pem.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/firebase-tools/node_modules/graceful-fs": { + "version": "4.2.10", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", + "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/gtoken": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-5.3.2.tgz", + "integrity": "sha512-gkvEKREW7dXWF8NV8pVrKfW7WqReAmjjkMBh6lNCCGOM4ucS0r0YyXXl0r/9Yj8wcW/32ISkfc8h5mPTDbtifQ==", + "devOptional": true, + "dependencies": { + "gaxios": "^4.0.0", + "google-p12-pem": "^3.1.3", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/firebase-tools/node_modules/gtoken/node_modules/jwa": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz", + "integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==", + "devOptional": true, + "dependencies": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/firebase-tools/node_modules/gtoken/node_modules/jws": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", + "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", + "devOptional": true, + "dependencies": { + "jwa": "^2.0.0", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/firebase-tools/node_modules/har-schema": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", + "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=", + "devOptional": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/firebase-tools/node_modules/har-validator": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.3.tgz", + "integrity": "sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==", + "deprecated": "this library is no longer supported", + "devOptional": true, + "dependencies": { + "ajv": "^6.5.5", + "har-schema": "^2.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/firebase-tools/node_modules/has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "devOptional": true, + "dependencies": { + "function-bind": "^1.1.1" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/firebase-tools/node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "devOptional": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/firebase-tools/node_modules/has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==", + "optional": true + }, + "node_modules/firebase-tools/node_modules/has-yarn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/has-yarn/-/has-yarn-2.1.0.tgz", + "integrity": "sha512-UqBRqi4ju7T+TqGNdqAO0PaSVGsDGJUBQvk9eUWNGRY1CFGDzYhLWoM7JQEemnlvVcv/YEmc2wNW8BC24EnUsw==", + "devOptional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/http-cache-semantics": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", + "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==", + "optional": true + }, + "node_modules/firebase-tools/node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "devOptional": true, + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/firebase-tools/node_modules/http-errors/node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "devOptional": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/firebase-tools/node_modules/http-errors/node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "devOptional": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/firebase-tools/node_modules/http-proxy-agent": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz", + "integrity": "sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==", + "devOptional": true, + "dependencies": { + "@tootallnate/once": "1", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/firebase-tools/node_modules/http-proxy-agent/node_modules/debug": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "devOptional": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/firebase-tools/node_modules/http-proxy-agent/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/http-signature": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", + "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", + "devOptional": true, + "dependencies": { + "assert-plus": "^1.0.0", + "jsprim": "^1.2.2", + "sshpk": "^1.7.0" + }, + "engines": { + "node": ">=0.8", + "npm": ">=1.3.7" + } + }, + "node_modules/firebase-tools/node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "devOptional": true, + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/firebase-tools/node_modules/https-proxy-agent/node_modules/debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "deprecated": "Debug versions >=3.2.0 <3.2.7 || >=4 <4.3.1 have a low-severity ReDos regression when used in a Node.js environment. It is recommended you upgrade to 3.2.7 or 4.3.1. (https://github.com/visionmedia/debug/issues/797)", + "devOptional": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/firebase-tools/node_modules/https-proxy-agent/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/humanize-ms": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", + "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", + "optional": true, + "dependencies": { + "ms": "^2.0.0" + } + }, + "node_modules/firebase-tools/node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "devOptional": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/firebase-tools/node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "devOptional": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/firebase-tools/node_modules/import-lazy": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/import-lazy/-/import-lazy-2.1.0.tgz", + "integrity": "sha1-BWmOPUXIjo1+nZLLBYTnfwlvPkM=", + "devOptional": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/firebase-tools/node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", + "devOptional": true, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/firebase-tools/node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/infer-owner": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/infer-owner/-/infer-owner-1.0.4.tgz", + "integrity": "sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==", + "optional": true + }, + "node_modules/firebase-tools/node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "devOptional": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/firebase-tools/node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/inquirer": { + "version": "8.2.5", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-8.2.5.tgz", + "integrity": "sha512-QAgPDQMEgrDssk1XiwwHoOGYF9BAbUcc1+j+FhEvaOt8/cKRqyLn0U5qA6F74fGhTMGxf92pOvPBeh29jQJDTQ==", + "devOptional": true, + "dependencies": { + "ansi-escapes": "^4.2.1", + "chalk": "^4.1.1", + "cli-cursor": "^3.1.0", + "cli-width": "^3.0.0", + "external-editor": "^3.0.3", + "figures": "^3.0.0", + "lodash": "^4.17.21", + "mute-stream": "0.0.8", + "ora": "^5.4.1", + "run-async": "^2.4.0", + "rxjs": "^7.5.5", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0", + "through": "^2.3.6", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/firebase-tools/node_modules/inquirer/node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "devOptional": true, + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/firebase-tools/node_modules/inquirer/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "devOptional": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/firebase-tools/node_modules/inquirer/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "devOptional": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/firebase-tools/node_modules/inquirer/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "devOptional": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/firebase-tools/node_modules/inquirer/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/inquirer/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "devOptional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/inquirer/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "devOptional": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/inquirer/node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "devOptional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/firebase-tools/node_modules/install-artifact-from-github": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/install-artifact-from-github/-/install-artifact-from-github-1.3.1.tgz", + "integrity": "sha512-3l3Bymg2eKDsN5wQuMfgGEj2x6l5MCAv0zPL6rxHESufFVlEAKW/6oY9F1aGgvY/EgWm5+eWGRjINveL4X7Hgg==", + "optional": true, + "bin": { + "install-from-cache": "bin/install-from-cache.js", + "save-to-github-cache": "bin/save-to-github-cache.js" + } + }, + "node_modules/firebase-tools/node_modules/ip": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.5.tgz", + "integrity": "sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/ip-regex": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-4.3.0.tgz", + "integrity": "sha512-B9ZWJxHHOHUhUjCPrMpLD4xEq35bUTClHM1S6CBU5ixQnkZmwipwgc96vAd7AAGM9TGHvJR+Uss+/Ak6UphK+Q==", + "devOptional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "devOptional": true, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/firebase-tools/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==", + "devOptional": true, + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/is-ci": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-2.0.0.tgz", + "integrity": "sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w==", + "devOptional": true, + "dependencies": { + "ci-info": "^2.0.0" + }, + "bin": { + "is-ci": "bin.js" + } + }, + "node_modules/firebase-tools/node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", + "devOptional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/firebase-tools/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "devOptional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/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==", + "devOptional": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/firebase-tools/node_modules/is-installed-globally": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.4.0.tgz", + "integrity": "sha512-iwGqO3J21aaSkC7jWnHP/difazwS7SFeIqxv6wEtLU8Y5KlzFTjyqcSIT0d8s4+dDhKytsk9PJZ2BkS5eZwQRQ==", + "devOptional": true, + "dependencies": { + "global-dirs": "^3.0.0", + "is-path-inside": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/firebase-tools/node_modules/is-interactive": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", + "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", + "devOptional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/is-lambda": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-lambda/-/is-lambda-1.0.1.tgz", + "integrity": "sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==", + "optional": true + }, + "node_modules/firebase-tools/node_modules/is-npm": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-npm/-/is-npm-5.0.0.tgz", + "integrity": "sha512-WW/rQLOazUq+ST/bCAVBp/2oMERWLsR7OrKyt052dNDk4DHcDE0/7QSXITlmi+VBcV13DfIbysG3tZJm5RfdBA==", + "devOptional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/firebase-tools/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==", + "devOptional": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/firebase-tools/node_modules/is-obj": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", + "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==", + "devOptional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/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==", + "devOptional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=", + "devOptional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/firebase-tools/node_modules/is-stream-ended": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-stream-ended/-/is-stream-ended-0.1.4.tgz", + "integrity": "sha512-xj0XPvmr7bQFTvirqnFr50o0hQIh6ZItDqloxt5aJrR4NQsYeSsyFQERYGCAzfindAcnKjINnwEEgLx4IqVzQw==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "devOptional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/firebase-tools/node_modules/is-url": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/is-url/-/is-url-1.2.4.tgz", + "integrity": "sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/is-windows": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", + "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", + "devOptional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/firebase-tools/node_modules/is-wsl": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-1.1.0.tgz", + "integrity": "sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0=", + "devOptional": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/firebase-tools/node_modules/is-yarn-global": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/is-yarn-global/-/is-yarn-global-0.3.0.tgz", + "integrity": "sha512-VjSeb/lHmkoyd8ryPVIKvOCn4D1koMqY+vqyjjUfc3xyKtP4dYOxM44sZrnqQSzSds3xyOrUTLTC9LVCVgLngw==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/is2": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/is2/-/is2-2.0.7.tgz", + "integrity": "sha512-4vBQoURAXC6hnLFxD4VW7uc04XiwTTl/8ydYJxKvPwkWQrSjInkuM5VZVg6BGr1/natq69zDuvO9lGpLClJqvA==", + "devOptional": true, + "dependencies": { + "deep-is": "^0.1.3", + "ip-regex": "^4.1.0", + "is-url": "^1.2.4" + }, + "engines": { + "node": ">=v0.10.0" + } + }, + "node_modules/firebase-tools/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/isomorphic-fetch": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-3.0.0.tgz", + "integrity": "sha512-qvUtwJ3j6qwsF3jLxkZ72qCgjMysPzDfeV240JHiGZsANBYd+EEuu35v7dfrJ9Up0Ak07D7GGSkGhCHTqg/5wA==", + "devOptional": true, + "dependencies": { + "node-fetch": "^2.6.1", + "whatwg-fetch": "^3.4.1" + } + }, + "node_modules/firebase-tools/node_modules/isomorphic-fetch/node_modules/whatwg-fetch": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.2.tgz", + "integrity": "sha512-bJlen0FcuU/0EMLrdbJ7zOnW6ITZLrZMIarMUVmdKtsGvZna8vxKYaexICWPfZ8qwf9fzNq+UEIZrnSaApt6RA==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/jju": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/jju/-/jju-1.4.0.tgz", + "integrity": "sha1-o6vicYryQaKykE+EpiWXDzia4yo=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/join-path": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/join-path/-/join-path-1.1.1.tgz", + "integrity": "sha1-EFNaEm0ky9Zff/zfFe8uYxB2tQU=", + "devOptional": true, + "dependencies": { + "as-array": "^2.0.0", + "url-join": "0.0.1", + "valid-url": "^1" + } + }, + "node_modules/firebase-tools/node_modules/js-yaml": { + "version": "3.13.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz", + "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==", + "devOptional": true, + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/firebase-tools/node_modules/js2xmlparser": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/js2xmlparser/-/js2xmlparser-4.0.2.tgz", + "integrity": "sha512-6n4D8gLlLf1n5mNLQPRfViYzu9RATblzPEtm1SthMX1Pjao0r9YI9nw7ZIfRxQMERS87mcswrg+r/OYrPRX6jA==", + "devOptional": true, + "dependencies": { + "xmlcreate": "^2.0.4" + } + }, + "node_modules/firebase-tools/node_modules/jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/jsdoc": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsdoc/-/jsdoc-4.0.0.tgz", + "integrity": "sha512-tzTgkklbWKrlaQL2+e3NNgLcZu3NaK2vsHRx7tyHQ+H5jcB9Gx0txSd2eJWlMC/xU1+7LQu4s58Ry0RkuaEQVg==", + "devOptional": true, + "dependencies": { + "@babel/parser": "^7.9.4", + "@jsdoc/salty": "^0.2.1", + "@types/markdown-it": "^12.2.3", + "bluebird": "^3.7.2", + "catharsis": "^0.9.0", + "escape-string-regexp": "^2.0.0", + "js2xmlparser": "^4.0.2", + "klaw": "^3.0.0", + "markdown-it": "^12.3.2", + "markdown-it-anchor": "^8.4.1", + "marked": "^4.0.10", + "mkdirp": "^1.0.4", + "requizzle": "^0.2.3", + "strip-json-comments": "^3.1.0", + "underscore": "~1.13.2" + }, + "bin": { + "jsdoc": "jsdoc.js" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/firebase-tools/node_modules/jsdoc/node_modules/bluebird": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/jsdoc/node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "devOptional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/jsdoc/node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "devOptional": true, + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/firebase-tools/node_modules/jsdoc/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==", + "devOptional": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/firebase-tools/node_modules/json-bigint": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", + "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", + "devOptional": true, + "dependencies": { + "bignumber.js": "^9.0.0" + } + }, + "node_modules/firebase-tools/node_modules/json-parse-helpfulerror": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/json-parse-helpfulerror/-/json-parse-helpfulerror-1.0.3.tgz", + "integrity": "sha1-E/FM4C7tTpgSl7ZOueO5MuLdE9w=", + "devOptional": true, + "dependencies": { + "jju": "^1.1.0" + } + }, + "node_modules/firebase-tools/node_modules/json-ptr": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-ptr/-/json-ptr-3.0.1.tgz", + "integrity": "sha512-hrZ4tElT8huJUH3OwOK+d7F8PRqw09QnGM3Mm3GmqKWDyCCPCG8lGHxXOwQAj0VOxzLirOds07Kz10B5F8M8EA==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/json-schema": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", + "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==", + "devOptional": true + }, + "node_modules/firebase-tools/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==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", + "devOptional": true, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/firebase-tools/node_modules/jsonwebtoken": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.0.tgz", + "integrity": "sha512-tuGfYXxkQGDPnLJ7SibiQgVgeDgfbPq2k2ICcbgqW8WxWLBAxKQM/ZCu/IT8SOSwmaYl4dpTFCW5xZv7YbbWUw==", + "devOptional": true, + "dependencies": { + "jws": "^3.2.2", + "lodash": "^4.17.21", + "ms": "^2.1.1", + "semver": "^7.3.8" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/firebase-tools/node_modules/jsonwebtoken/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==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/jsonwebtoken/node_modules/semver": { + "version": "7.3.8", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", + "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", + "devOptional": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/firebase-tools/node_modules/jsprim": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.2.tgz", + "integrity": "sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw==", + "devOptional": true, + "dependencies": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.4.0", + "verror": "1.10.0" + }, + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/firebase-tools/node_modules/jwa": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", + "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", + "devOptional": true, + "dependencies": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/firebase-tools/node_modules/jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "devOptional": true, + "dependencies": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/firebase-tools/node_modules/klaw": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/klaw/-/klaw-3.0.0.tgz", + "integrity": "sha512-0Fo5oir+O9jnXu5EefYbVK+mHMBeEVEy2cmctR1O1NECcCkPRreJKrS6Qt/j3KC2C148Dfo9i3pCmCMsdqGr0g==", + "devOptional": true, + "dependencies": { + "graceful-fs": "^4.1.9" + } + }, + "node_modules/firebase-tools/node_modules/kuler": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/kuler/-/kuler-1.0.1.tgz", + "integrity": "sha512-J9nVUucG1p/skKul6DU3PUZrhs0LPulNaeUOox0IyXDi8S4CztTHs1gQphhuZmzXG7VOQSf6NJfKuzteQLv9gQ==", + "devOptional": true, + "dependencies": { + "colornames": "^1.1.1" + } + }, + "node_modules/firebase-tools/node_modules/lazystream": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz", + "integrity": "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==", + "devOptional": true, + "dependencies": { + "readable-stream": "^2.0.5" + }, + "engines": { + "node": ">= 0.6.3" + } + }, + "node_modules/firebase-tools/node_modules/lazystream/node_modules/readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "devOptional": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/firebase-tools/node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "devOptional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/firebase-tools/node_modules/levn": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", + "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", + "devOptional": true, + "dependencies": { + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/firebase-tools/node_modules/libsodium": { + "version": "0.7.10", + "resolved": "https://registry.npmjs.org/libsodium/-/libsodium-0.7.10.tgz", + "integrity": "sha512-eY+z7hDrDKxkAK+QKZVNv92A5KYkxfvIshtBJkmg5TSiCnYqZP3i9OO9whE79Pwgm4jGaoHgkM4ao/b9Cyu4zQ==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/libsodium-wrappers": { + "version": "0.7.10", + "resolved": "https://registry.npmjs.org/libsodium-wrappers/-/libsodium-wrappers-0.7.10.tgz", + "integrity": "sha512-pO3F1Q9NPLB/MWIhehim42b/Fwb30JNScCNh8TcQ/kIc+qGLQch8ag8wb0keK3EP5kbGakk1H8Wwo7v+36rNQg==", + "devOptional": true, + "dependencies": { + "libsodium": "^0.7.0" + } + }, + "node_modules/firebase-tools/node_modules/linkify-it": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-3.0.3.tgz", + "integrity": "sha512-ynTsyrFSdE5oZ/O9GEf00kPngmOfVwazR5GKDq6EYfhlpFug3J2zybX56a2PRRpc9P+FuSoGNAwjlbDs9jJBPQ==", + "devOptional": true, + "dependencies": { + "uc.micro": "^1.0.1" + } + }, + "node_modules/firebase-tools/node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/lodash._objecttypes": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/lodash._objecttypes/-/lodash._objecttypes-2.4.1.tgz", + "integrity": "sha1-fAt/admKH3ZSn4kLDNsbTf7BHBE=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha1-soqmKIorn8ZRA1x3EfZathkDMaY=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/lodash.defaults": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", + "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/lodash.difference": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.difference/-/lodash.difference-4.5.0.tgz", + "integrity": "sha512-dS2j+W26TQ7taQBGN8Lbbq04ssV3emRw4NY58WErlTO29pIqS0HmoT5aJ9+TUQ1N3G+JOZSji4eugsWwGp9yPA==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/lodash.flatten": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz", + "integrity": "sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/lodash.isobject": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/lodash.isobject/-/lodash.isobject-2.4.1.tgz", + "integrity": "sha1-Wi5H/mmVPx7mMafrof5k0tBlWPU=", + "devOptional": true, + "dependencies": { + "lodash._objecttypes": "~2.4.1" + } + }, + "node_modules/firebase-tools/node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/lodash.snakecase": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.snakecase/-/lodash.snakecase-4.1.1.tgz", + "integrity": "sha1-OdcUo1NXFHg3rv1ktdy7Fr7Nj40=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/lodash.union": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.union/-/lodash.union-4.6.0.tgz", + "integrity": "sha512-c4pB2CdGrGdjMKYLA+XiRDO7Y0PRQbm/Gzg8qMj+QH+pFVAoTp5sBpO0odL3FjoPCGjK96p6qsP+yQoiLoOBcw==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "devOptional": true, + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/firebase-tools/node_modules/log-symbols/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "devOptional": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/firebase-tools/node_modules/log-symbols/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "devOptional": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/firebase-tools/node_modules/log-symbols/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "devOptional": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/firebase-tools/node_modules/log-symbols/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/log-symbols/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "devOptional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/log-symbols/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "devOptional": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/logform": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/logform/-/logform-2.1.2.tgz", + "integrity": "sha512-+lZh4OpERDBLqjiwDLpAWNQu6KMjnlXH2ByZwCuSqVPJletw0kTWJf5CgSNAUKn1KUkv3m2cUz/LK8zyEy7wzQ==", + "devOptional": true, + "dependencies": { + "colors": "^1.2.1", + "fast-safe-stringify": "^2.0.4", + "fecha": "^2.3.3", + "ms": "^2.1.1", + "triple-beam": "^1.3.0" + } + }, + "node_modules/firebase-tools/node_modules/logform/node_modules/colors": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz", + "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==", + "devOptional": true, + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/firebase-tools/node_modules/logform/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/long": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", + "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "devOptional": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/firebase-tools/node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "devOptional": true, + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/firebase-tools/node_modules/make-dir/node_modules/semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "devOptional": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/firebase-tools/node_modules/make-fetch-happen": { + "version": "10.2.1", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-10.2.1.tgz", + "integrity": "sha512-NgOPbRiaQM10DYXvN3/hhGVI2M5MtITFryzBGxHM5p4wnFxsVCbxkrBrDsk+EZ5OB4jEOT7AjDxtdF+KVEFT7w==", + "optional": true, + "dependencies": { + "agentkeepalive": "^4.2.1", + "cacache": "^16.1.0", + "http-cache-semantics": "^4.1.0", + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.0", + "is-lambda": "^1.0.1", + "lru-cache": "^7.7.1", + "minipass": "^3.1.6", + "minipass-collect": "^1.0.2", + "minipass-fetch": "^2.0.3", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^0.6.3", + "promise-retry": "^2.0.1", + "socks-proxy-agent": "^7.0.0", + "ssri": "^9.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/firebase-tools/node_modules/make-fetch-happen/node_modules/@tootallnate/once": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", + "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", + "optional": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/firebase-tools/node_modules/make-fetch-happen/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "optional": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/firebase-tools/node_modules/make-fetch-happen/node_modules/http-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", + "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", + "optional": true, + "dependencies": { + "@tootallnate/once": "2", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/firebase-tools/node_modules/make-fetch-happen/node_modules/lru-cache": { + "version": "7.14.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.14.0.tgz", + "integrity": "sha512-EIRtP1GrSJny0dqb50QXRUNBxHJhcpxHC++M5tD7RYbvLLn5KVWKsbyswSSqDuU15UFi3bgTQIY8nhDMeF6aDQ==", + "optional": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/firebase-tools/node_modules/make-fetch-happen/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "optional": true + }, + "node_modules/firebase-tools/node_modules/make-fetch-happen/node_modules/socks-proxy-agent": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-7.0.0.tgz", + "integrity": "sha512-Fgl0YPZ902wEsAyiQ+idGd1A7rSFx/ayC1CQVMw5P+EQx2V0SgpGtf6OKFhVjPflPUl9YMmEOnmfjCdMUsygww==", + "optional": true, + "dependencies": { + "agent-base": "^6.0.2", + "debug": "^4.3.3", + "socks": "^2.6.2" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/firebase-tools/node_modules/markdown-it": { + "version": "12.3.2", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-12.3.2.tgz", + "integrity": "sha512-TchMembfxfNVpHkbtriWltGWc+m3xszaRD0CZup7GFFhzIgQqxIfn3eGj1yZpfuflzPvfkt611B2Q/Bsk1YnGg==", + "devOptional": true, + "dependencies": { + "argparse": "^2.0.1", + "entities": "~2.1.0", + "linkify-it": "^3.0.1", + "mdurl": "^1.0.1", + "uc.micro": "^1.0.5" + }, + "bin": { + "markdown-it": "bin/markdown-it.js" + } + }, + "node_modules/firebase-tools/node_modules/markdown-it-anchor": { + "version": "8.6.6", + "resolved": "https://registry.npmjs.org/markdown-it-anchor/-/markdown-it-anchor-8.6.6.tgz", + "integrity": "sha512-jRW30YGywD2ESXDc+l17AiritL0uVaSnWsb26f+68qaW9zgbIIr1f4v2Nsvc0+s0Z2N3uX6t/yAw7BwCQ1wMsA==", + "devOptional": true, + "peerDependencies": { + "@types/markdown-it": "*", + "markdown-it": "*" + } + }, + "node_modules/firebase-tools/node_modules/markdown-it/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==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/marked": { + "version": "4.2.12", + "resolved": "https://registry.npmjs.org/marked/-/marked-4.2.12.tgz", + "integrity": "sha512-yr8hSKa3Fv4D3jdZmtMMPghgVt6TWbk86WQaWhDloQjRSQhMMYCAro7jP7VDJrjjdV8pxVxMssXS8B8Y5DZ5aw==", + "devOptional": true, + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 12" + } + }, + "node_modules/firebase-tools/node_modules/marked-terminal": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/marked-terminal/-/marked-terminal-5.1.1.tgz", + "integrity": "sha512-+cKTOx9P4l7HwINYhzbrBSyzgxO2HaHKGZGuB1orZsMIgXYaJyfidT81VXRdpelW/PcHEWxywscePVgI/oUF6g==", + "devOptional": true, + "dependencies": { + "ansi-escapes": "^5.0.0", + "cardinal": "^2.1.1", + "chalk": "^5.0.0", + "cli-table3": "^0.6.1", + "node-emoji": "^1.11.0", + "supports-hyperlinks": "^2.2.0" + }, + "engines": { + "node": ">=14.13.1 || >=16.0.0" + }, + "peerDependencies": { + "marked": "^1.0.0 || ^2.0.0 || ^3.0.0 || ^4.0.0" + } + }, + "node_modules/firebase-tools/node_modules/marked-terminal/node_modules/chalk": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.0.1.tgz", + "integrity": "sha512-Fo07WOYGqMfCWHOzSXOt2CxDbC6skS/jO9ynEcmpANMoPrD+W1r1K6Vx7iNm+AQmETU1Xr2t+n8nzkV9t6xh3w==", + "devOptional": true, + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/firebase-tools/node_modules/mdurl": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz", + "integrity": "sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=", + "devOptional": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/firebase-tools/node_modules/merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=", + "devOptional": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/firebase-tools/node_modules/mime": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.5.2.tgz", + "integrity": "sha512-tqkh47FzKeCPD2PUiPB6pkbMzsCasjxAfC62/Wap5qrUWcb+sFasXUC5I3gYM5iBM8v/Qpn4UK0x+j0iHyFPDg==", + "devOptional": true, + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/firebase-tools/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "devOptional": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/firebase-tools/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "devOptional": true, + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/firebase-tools/node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "devOptional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/firebase-tools/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "devOptional": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/firebase-tools/node_modules/minimist": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", + "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/minipass": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.1.6.tgz", + "integrity": "sha512-rty5kpw9/z8SX9dmxblFA6edItUmwJgMeYDZRrwlIVN27i8gysGbznJwUggw2V/FVqFSDdWy040ZPS811DYAqQ==", + "devOptional": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/minipass-collect": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-1.0.2.tgz", + "integrity": "sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==", + "optional": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/firebase-tools/node_modules/minipass-fetch": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-2.1.2.tgz", + "integrity": "sha512-LT49Zi2/WMROHYoqGgdlQIZh8mLPZmOrN2NdJjMXxYe4nkN6FUyuPuOAOedNJDrx0IRGg9+4guZewtp8hE6TxA==", + "optional": true, + "dependencies": { + "minipass": "^3.1.6", + "minipass-sized": "^1.0.3", + "minizlib": "^2.1.2" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + }, + "optionalDependencies": { + "encoding": "^0.1.13" + } + }, + "node_modules/firebase-tools/node_modules/minipass-flush": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz", + "integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==", + "optional": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/firebase-tools/node_modules/minipass-pipeline": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", + "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", + "optional": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/minipass-sized": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz", + "integrity": "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==", + "optional": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "devOptional": true, + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/firebase-tools/node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "devOptional": true, + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/firebase-tools/node_modules/morgan": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.0.tgz", + "integrity": "sha512-AbegBVI4sh6El+1gNwvD5YIck7nSA36weD7xvIxG4in80j/UoK8AEGaWnnz8v1GxonMCltmlNs5ZKbGvl9b1XQ==", + "devOptional": true, + "dependencies": { + "basic-auth": "~2.0.1", + "debug": "2.6.9", + "depd": "~2.0.0", + "on-finished": "~2.3.0", + "on-headers": "~1.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/firebase-tools/node_modules/morgan/node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "devOptional": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/firebase-tools/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/mute-stream": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", + "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/nan": { + "version": "2.16.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.16.0.tgz", + "integrity": "sha512-UdAqHyFngu7TfQKsCBgAA6pWDkT8MAO7d0jyOecVhN5354xbLqdn8mV9Tat9gepAupm0bt2DbeaSC8vS52MuFA==", + "optional": true + }, + "node_modules/firebase-tools/node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "devOptional": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/firebase-tools/node_modules/netmask": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.0.2.tgz", + "integrity": "sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==", + "devOptional": true, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/firebase-tools/node_modules/nice-try": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", + "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/node-emoji": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-1.11.0.tgz", + "integrity": "sha512-wo2DpQkQp7Sjm2A0cq+sN7EHKO6Sl0ctXeBdFZrL9T9+UywORbufTcTZxom8YqpLQt/FqNMUkOpkZrJVYSKD3A==", + "devOptional": true, + "dependencies": { + "lodash": "^4.17.21" + } + }, + "node_modules/firebase-tools/node_modules/node-fetch": { + "version": "2.6.7", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", + "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", + "devOptional": true, + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/firebase-tools/node_modules/node-forge": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", + "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==", + "devOptional": true, + "engines": { + "node": ">= 6.13.0" + } + }, + "node_modules/firebase-tools/node_modules/node-gyp": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-9.1.0.tgz", + "integrity": "sha512-HkmN0ZpQJU7FLbJauJTHkHlSVAXlNGDAzH/VYFZGDOnFyn/Na3GlNJfkudmufOdS6/jNFhy88ObzL7ERz9es1g==", + "optional": true, + "dependencies": { + "env-paths": "^2.2.0", + "glob": "^7.1.4", + "graceful-fs": "^4.2.6", + "make-fetch-happen": "^10.0.3", + "nopt": "^5.0.0", + "npmlog": "^6.0.0", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "tar": "^6.1.2", + "which": "^2.0.2" + }, + "bin": { + "node-gyp": "bin/node-gyp.js" + }, + "engines": { + "node": "^12.22 || ^14.13 || >=16" + } + }, + "node_modules/firebase-tools/node_modules/node-gyp/node_modules/semver": { + "version": "7.3.7", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz", + "integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==", + "optional": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/firebase-tools/node_modules/node-gyp/node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "optional": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/firebase-tools/node_modules/nopt": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", + "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", + "optional": true, + "dependencies": { + "abbrev": "1" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/firebase-tools/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==", + "devOptional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/firebase-tools/node_modules/npmlog": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-6.0.2.tgz", + "integrity": "sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg==", + "optional": true, + "dependencies": { + "are-we-there-yet": "^3.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^4.0.3", + "set-blocking": "^2.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/firebase-tools/node_modules/oauth-sign": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", + "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", + "devOptional": true, + "engines": { + "node": "*" + } + }, + "node_modules/firebase-tools/node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", + "devOptional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/firebase-tools/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==", + "devOptional": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/firebase-tools/node_modules/object-inspect": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.2.tgz", + "integrity": "sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==", + "devOptional": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/firebase-tools/node_modules/on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", + "devOptional": true, + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/firebase-tools/node_modules/on-headers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", + "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", + "devOptional": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/firebase-tools/node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "devOptional": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/firebase-tools/node_modules/one-time": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/one-time/-/one-time-0.0.4.tgz", + "integrity": "sha1-+M33eISCb+Tf+T46nMN7HkSAdC4=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "devOptional": true, + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/firebase-tools/node_modules/open": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/open/-/open-6.4.0.tgz", + "integrity": "sha512-IFenVPgF70fSm1keSd2iDBIDIBZkroLeuffXq+wKTzTJlBpesFWojV9lb8mzOfaAzM1sr7HQHuO0vtV0zYekGg==", + "devOptional": true, + "dependencies": { + "is-wsl": "^1.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/openapi3-ts": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/openapi3-ts/-/openapi3-ts-2.0.1.tgz", + "integrity": "sha512-v6X3iwddhi276siej96jHGIqTx3wzVfMTmpGJEQDt7GPI7pI6sywItURLzpEci21SBRpPN/aOWSF5mVfFVNmcg==", + "devOptional": true, + "dependencies": { + "yaml": "^1.10.0" + } + }, + "node_modules/firebase-tools/node_modules/optionator": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", + "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", + "devOptional": true, + "dependencies": { + "deep-is": "~0.1.3", + "fast-levenshtein": "~2.0.6", + "levn": "~0.3.0", + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2", + "word-wrap": "~1.2.3" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/firebase-tools/node_modules/ora": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", + "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", + "devOptional": true, + "dependencies": { + "bl": "^4.1.0", + "chalk": "^4.1.0", + "cli-cursor": "^3.1.0", + "cli-spinners": "^2.5.0", + "is-interactive": "^1.0.0", + "is-unicode-supported": "^0.1.0", + "log-symbols": "^4.1.0", + "strip-ansi": "^6.0.0", + "wcwidth": "^1.0.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/firebase-tools/node_modules/ora/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "devOptional": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/firebase-tools/node_modules/ora/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "devOptional": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/firebase-tools/node_modules/ora/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "devOptional": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/firebase-tools/node_modules/ora/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/ora/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "devOptional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/ora/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "devOptional": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", + "devOptional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/firebase-tools/node_modules/p-defer": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-defer/-/p-defer-3.0.0.tgz", + "integrity": "sha512-ugZxsxmtTln604yeYd29EGrNhazN2lywetzpKhfmQjW/VJmhpDmWbiX+h0zL8V91R0UXkhb3KtPmyq9PZw3aYw==", + "devOptional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/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==", + "devOptional": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/firebase-tools/node_modules/pac-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-5.0.0.tgz", + "integrity": "sha512-CcFG3ZtnxO8McDigozwE3AqAw15zDvGH+OjXO4kzf7IkEKkQ4gxQ+3sdF50WmhQ4P/bVusXcqNE2S3XrNURwzQ==", + "devOptional": true, + "dependencies": { + "@tootallnate/once": "1", + "agent-base": "6", + "debug": "4", + "get-uri": "3", + "http-proxy-agent": "^4.0.1", + "https-proxy-agent": "5", + "pac-resolver": "^5.0.0", + "raw-body": "^2.2.0", + "socks-proxy-agent": "5" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/firebase-tools/node_modules/pac-proxy-agent/node_modules/debug": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", + "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", + "devOptional": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/firebase-tools/node_modules/pac-proxy-agent/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/pac-resolver": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-5.0.0.tgz", + "integrity": "sha512-H+/A6KitiHNNW+bxBKREk2MCGSxljfqRX76NjummWEYIat7ldVXRU3dhRIE3iXZ0nvGBk6smv3nntxKkzRL8NA==", + "devOptional": true, + "dependencies": { + "degenerator": "^3.0.1", + "ip": "^1.1.5", + "netmask": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/firebase-tools/node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "devOptional": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/firebase-tools/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": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "devOptional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/firebase-tools/node_modules/path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=", + "devOptional": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/firebase-tools/node_modules/path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "devOptional": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/firebase-tools/node_modules/portfinder": { + "version": "1.0.32", + "resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.32.tgz", + "integrity": "sha512-on2ZJVVDXRADWE6jnQaX0ioEylzgBpQk8r55NE4wjXW1ZxO+BgDlY6DXwj20i0V8eB4SenDQ00WEaxfiIQPcxg==", + "devOptional": true, + "dependencies": { + "async": "^2.6.4", + "debug": "^3.2.7", + "mkdirp": "^0.5.6" + }, + "engines": { + "node": ">= 0.12.0" + } + }, + "node_modules/firebase-tools/node_modules/portfinder/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "devOptional": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/firebase-tools/node_modules/portfinder/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==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/prelude-ls": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", + "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=", + "devOptional": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/firebase-tools/node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "devOptional": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/firebase-tools/node_modules/promise-breaker": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/promise-breaker/-/promise-breaker-5.0.0.tgz", + "integrity": "sha512-mgsWQuG4kJ1dtO6e/QlNDLFtMkMzzecsC69aI5hlLEjGHFNpHrvGhFi4LiK5jg2SMQj74/diH+wZliL9LpGsyA==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/promise-inflight": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", + "integrity": "sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==", + "optional": true + }, + "node_modules/firebase-tools/node_modules/promise-retry": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", + "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", + "optional": true, + "dependencies": { + "err-code": "^2.0.2", + "retry": "^0.12.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/firebase-tools/node_modules/promise-retry/node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "optional": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/firebase-tools/node_modules/proto-list": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", + "integrity": "sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/proto3-json-serializer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proto3-json-serializer/-/proto3-json-serializer-1.1.0.tgz", + "integrity": "sha512-SjXwUWe/vANGs/mJJTbw5++7U67nwsymg7qsoPtw6GiXqw3kUy8ByojrlEdVE2efxAdKreX8WkDafxvYW95ZQg==", + "devOptional": true, + "dependencies": { + "protobufjs": "^7.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/firebase-tools/node_modules/proto3-json-serializer/node_modules/long": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/long/-/long-5.2.1.tgz", + "integrity": "sha512-GKSNGeNAtw8IryjjkhZxuKB3JzlcLTwjtiQCHKvqQet81I93kXslhDQruGI/QsddO83mcDToBVy7GqGS/zYf/A==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/proto3-json-serializer/node_modules/protobufjs": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.1.2.tgz", + "integrity": "sha512-4ZPTPkXCdel3+L81yw3dG6+Kq3umdWKh7Dc7GW/CpNk4SX3hK58iPCWeCyhVTDrbkNeKrYNZ7EojM5WDaEWTLQ==", + "devOptional": true, + "hasInstallScript": true, + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/node": ">=13.7.0", + "long": "^5.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/firebase-tools/node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "devOptional": true, + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/firebase-tools/node_modules/proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-5.0.0.tgz", + "integrity": "sha512-gkH7BkvLVkSfX9Dk27W6TyNOWWZWRilRfk1XxGNWOYJ2TuedAv1yFpCaU9QSBmBe716XOTNpYNOzhysyw8xn7g==", + "devOptional": true, + "dependencies": { + "agent-base": "^6.0.0", + "debug": "4", + "http-proxy-agent": "^4.0.0", + "https-proxy-agent": "^5.0.0", + "lru-cache": "^5.1.1", + "pac-proxy-agent": "^5.0.0", + "proxy-from-env": "^1.0.0", + "socks-proxy-agent": "^5.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/firebase-tools/node_modules/proxy-agent/node_modules/debug": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", + "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", + "devOptional": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/firebase-tools/node_modules/proxy-agent/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==", + "devOptional": true, + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/firebase-tools/node_modules/proxy-agent/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/proxy-agent/node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/psl": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz", + "integrity": "sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "devOptional": true, + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/firebase-tools/node_modules/punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", + "devOptional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/firebase-tools/node_modules/pupa": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/pupa/-/pupa-2.1.1.tgz", + "integrity": "sha512-l1jNAspIBSFqbT+y+5FosojNpVpF94nlI+wDUpqP9enwOTfHx9f0gh5nB96vl+6yTpsJsypeNrwfzPrKuHB41A==", + "devOptional": true, + "dependencies": { + "escape-goat": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/qs": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", + "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "devOptional": true, + "dependencies": { + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/firebase-tools/node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "devOptional": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/firebase-tools/node_modules/raw-body": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", + "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", + "devOptional": true, + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/firebase-tools/node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "devOptional": true, + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/firebase-tools/node_modules/re2": { + "version": "1.17.7", + "resolved": "https://registry.npmjs.org/re2/-/re2-1.17.7.tgz", + "integrity": "sha512-X8GSuiBoVWwcjuppqSjsIkRxNUKDdjhkO9SBekQbZ2ksqWUReCy7DQPWOVpoTnpdtdz5PIpTTxTFzvJv5UMfjA==", + "hasInstallScript": true, + "optional": true, + "dependencies": { + "install-artifact-from-github": "^1.3.1", + "nan": "^2.16.0", + "node-gyp": "^9.0.0" + } + }, + "node_modules/firebase-tools/node_modules/readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "devOptional": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/firebase-tools/node_modules/readdir-glob": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/readdir-glob/-/readdir-glob-1.1.1.tgz", + "integrity": "sha512-91/k1EzZwDx6HbERR+zucygRFfiPl2zkIYZtv3Jjr6Mn7SkKcVct8aVO+sSRiGMc6fLf72du3d92/uY63YPdEA==", + "devOptional": true, + "dependencies": { + "minimatch": "^3.0.4" + } + }, + "node_modules/firebase-tools/node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "devOptional": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/firebase-tools/node_modules/redeyed": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/redeyed/-/redeyed-2.1.1.tgz", + "integrity": "sha1-iYS1gV2ZyyIEacme7v/jiRPmzAs=", + "devOptional": true, + "dependencies": { + "esprima": "~4.0.0" + } + }, + "node_modules/firebase-tools/node_modules/registry-url": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/registry-url/-/registry-url-5.1.0.tgz", + "integrity": "sha512-8acYXXTI0AkQv6RAOjE3vOaIXZkT9wo4LOFbBKYQEEnnMNBpKqdUrI6S4NT0KPIo/WVvJ5tE/X5LF/TQUf0ekw==", + "devOptional": true, + "dependencies": { + "rc": "^1.2.8" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/request": { + "version": "2.88.2", + "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", + "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==", + "deprecated": "request has been deprecated, see https://github.com/request/request/issues/3142", + "devOptional": true, + "dependencies": { + "aws-sign2": "~0.7.0", + "aws4": "^1.8.0", + "caseless": "~0.12.0", + "combined-stream": "~1.0.6", + "extend": "~3.0.2", + "forever-agent": "~0.6.1", + "form-data": "~2.3.2", + "har-validator": "~5.1.3", + "http-signature": "~1.2.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.19", + "oauth-sign": "~0.9.0", + "performance-now": "^2.1.0", + "qs": "~6.5.2", + "safe-buffer": "^5.1.2", + "tough-cookie": "~2.5.0", + "tunnel-agent": "^0.6.0", + "uuid": "^3.3.2" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/firebase-tools/node_modules/request/node_modules/form-data": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", + "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", + "devOptional": true, + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 0.12" + } + }, + "node_modules/firebase-tools/node_modules/request/node_modules/qs": { + "version": "6.5.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.3.tgz", + "integrity": "sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==", + "devOptional": true, + "engines": { + "node": ">=0.6" + } + }, + "node_modules/firebase-tools/node_modules/request/node_modules/uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", + "deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.", + "devOptional": true, + "bin": { + "uuid": "bin/uuid" + } + }, + "node_modules/firebase-tools/node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", + "devOptional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/firebase-tools/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==", + "devOptional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/firebase-tools/node_modules/requizzle": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/requizzle/-/requizzle-0.2.4.tgz", + "integrity": "sha512-JRrFk1D4OQ4SqovXOgdav+K8EAhSB/LJZqCz8tbX0KObcdeM15Ss59ozWMBWmmINMagCwmqn4ZNryUGpBsl6Jw==", + "devOptional": true, + "dependencies": { + "lodash": "^4.17.21" + } + }, + "node_modules/firebase-tools/node_modules/restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "devOptional": true, + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/retry": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "devOptional": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/firebase-tools/node_modules/retry-request": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/retry-request/-/retry-request-5.0.2.tgz", + "integrity": "sha512-wfI3pk7EE80lCIXprqh7ym48IHYdwmAAzESdbU8Q9l7pnRCk9LEhpbOTNKjz6FARLm/Bl5m+4F0ABxOkYUujSQ==", + "devOptional": true, + "dependencies": { + "debug": "^4.1.1", + "extend": "^3.0.2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/firebase-tools/node_modules/retry-request/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "devOptional": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/firebase-tools/node_modules/retry-request/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "devOptional": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/firebase-tools/node_modules/router": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/router/-/router-1.3.5.tgz", + "integrity": "sha512-kozCJZUhuSJ5VcLhSb3F8fsmGXy+8HaDbKCAerR1G6tq3mnMZFMuSohbFvGv1c5oMFipijDjRZuuN/Sq5nMf3g==", + "devOptional": true, + "dependencies": { + "array-flatten": "3.0.0", + "debug": "2.6.9", + "methods": "~1.1.2", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.7", + "setprototypeof": "1.2.0", + "utils-merge": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/firebase-tools/node_modules/router/node_modules/array-flatten": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-3.0.0.tgz", + "integrity": "sha512-zPMVc3ZYlGLNk4mpK1NzP2wg0ml9t7fUgDsayR5Y5rSzxQilzR9FGu/EH2jQOcKSAeAfWeylyW8juy3OkWRvNA==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/run-async": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", + "integrity": "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==", + "devOptional": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/firebase-tools/node_modules/rxjs": { + "version": "7.5.6", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.5.6.tgz", + "integrity": "sha512-dnyv2/YsXhnm461G+R/Pe5bWP41Nm6LBXEYWI6eiFP4fiwx6WRI/CD0zbdVAudd9xwLEF2IDcKXLHit0FYjUzw==", + "devOptional": true, + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/firebase-tools/node_modules/rxjs/node_modules/tslib": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz", + "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "devOptional": true, + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/firebase-tools/node_modules/semver-diff": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/semver-diff/-/semver-diff-3.1.1.tgz", + "integrity": "sha512-GX0Ix/CJcHyB8c4ykpHGIAvLyOwOobtM/8d+TQkAd81/bEjgPHrfba41Vpesr7jX/t8Uh+R3EX9eAS5be+jQYg==", + "devOptional": true, + "dependencies": { + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/semver-diff/node_modules/semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "devOptional": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/firebase-tools/node_modules/send": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", + "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "devOptional": true, + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/firebase-tools/node_modules/send/node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "devOptional": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/firebase-tools/node_modules/send/node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "devOptional": true, + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/firebase-tools/node_modules/send/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==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/send/node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "devOptional": true, + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/firebase-tools/node_modules/send/node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "devOptional": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/firebase-tools/node_modules/serve-static": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", + "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", + "devOptional": true, + "dependencies": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.18.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/firebase-tools/node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", + "optional": true + }, + "node_modules/firebase-tools/node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", + "devOptional": true, + "dependencies": { + "shebang-regex": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/firebase-tools/node_modules/shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", + "devOptional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/firebase-tools/node_modules/side-channel": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", + "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "devOptional": true, + "dependencies": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/firebase-tools/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/simple-swizzle": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", + "integrity": "sha1-pNprY1/8zMoz9w0Xy5JZLeleVXo=", + "devOptional": true, + "dependencies": { + "is-arrayish": "^0.3.1" + } + }, + "node_modules/firebase-tools/node_modules/simple-swizzle/node_modules/is-arrayish": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", + "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "devOptional": true, + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/firebase-tools/node_modules/socks": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.7.0.tgz", + "integrity": "sha512-scnOe9y4VuiNUULJN72GrM26BNOjVsfPXI+j+98PkyEfsIXroa5ofyjT+FzGvn/xHs73U2JtoBYAVx9Hl4quSA==", + "devOptional": true, + "dependencies": { + "ip": "^2.0.0", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.13.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/firebase-tools/node_modules/socks-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-5.0.1.tgz", + "integrity": "sha512-vZdmnjb9a2Tz6WEQVIurybSwElwPxMZaIc7PzqbJTrezcKNznv6giT7J7tZDZ1BojVaa1jvO/UiUdhDVB0ACoQ==", + "devOptional": true, + "dependencies": { + "agent-base": "^6.0.2", + "debug": "4", + "socks": "^2.3.3" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/firebase-tools/node_modules/socks-proxy-agent/node_modules/debug": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", + "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", + "devOptional": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/firebase-tools/node_modules/socks-proxy-agent/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/socks/node_modules/ip": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ip/-/ip-2.0.0.tgz", + "integrity": "sha512-WKa+XuLG1A1R0UWhl2+1XQSi+fZWMsYKffMZTTYsiZaUD8k2yDAj5atimTUD2TZkyCkNEeYE5NhFZmupOGtjYQ==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/firebase-tools/node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/sshpk": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz", + "integrity": "sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==", + "devOptional": true, + "dependencies": { + "asn1": "~0.2.3", + "assert-plus": "^1.0.0", + "bcrypt-pbkdf": "^1.0.0", + "dashdash": "^1.12.0", + "ecc-jsbn": "~0.1.1", + "getpass": "^0.1.1", + "jsbn": "~0.1.0", + "safer-buffer": "^2.0.2", + "tweetnacl": "~0.14.0" + }, + "bin": { + "sshpk-conv": "bin/sshpk-conv", + "sshpk-sign": "bin/sshpk-sign", + "sshpk-verify": "bin/sshpk-verify" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/firebase-tools/node_modules/ssri": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-9.0.1.tgz", + "integrity": "sha512-o57Wcn66jMQvfHG1FlYbWeZWW/dHZhJXjpIcTfXldXEk5nz5lStPo3mK0OJQfGR3RbZUlbISexbljkJzuEj/8Q==", + "optional": true, + "dependencies": { + "minipass": "^3.1.1" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/firebase-tools/node_modules/stack-trace": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", + "integrity": "sha1-VHxws0fo0ytOEI6hoqFZ5f3eGcA=", + "devOptional": true, + "engines": { + "node": "*" + } + }, + "node_modules/firebase-tools/node_modules/statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=", + "devOptional": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/firebase-tools/node_modules/stream-chain": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/stream-chain/-/stream-chain-2.2.4.tgz", + "integrity": "sha512-9lsl3YM53V5N/I1C2uJtc3Kavyi3kNYN83VkKb/bMWRk7D9imiFyUPYa0PoZbLohSVOX1mYE9YsmwObZUsth6Q==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/stream-json": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/stream-json/-/stream-json-1.7.3.tgz", + "integrity": "sha512-Y6dXn9KKWSwxOqnvHGcdZy1PK+J+7alBwHCeU3W9oRqm4ilLRA0XSPmd1tWwhg7tv9EIxJTMWh7KF15tYelKJg==", + "devOptional": true, + "dependencies": { + "stream-chain": "^2.2.4" + } + }, + "node_modules/firebase-tools/node_modules/stream-shift": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.1.tgz", + "integrity": "sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "devOptional": true, + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/firebase-tools/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "devOptional": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/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==", + "devOptional": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/strip-ansi/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==", + "devOptional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", + "devOptional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/firebase-tools/node_modules/superstatic": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/superstatic/-/superstatic-9.0.3.tgz", + "integrity": "sha512-e/tmW0bsnQ/33ivK6y3CapJT0Ovy4pk/ohNPGhIAGU2oasoNLRQ1cv6enua09NU9w6Y0H/fBu07cjzuiWvLXxw==", + "devOptional": true, + "dependencies": { + "basic-auth-connect": "^1.0.0", + "commander": "^10.0.0", + "compression": "^1.7.0", + "connect": "^3.7.0", + "destroy": "^1.0.4", + "fast-url-parser": "^1.1.3", + "glob-slasher": "^1.0.1", + "is-url": "^1.2.2", + "join-path": "^1.1.1", + "lodash": "^4.17.19", + "mime-types": "^2.1.35", + "minimatch": "^6.1.6", + "morgan": "^1.8.2", + "on-finished": "^2.2.0", + "on-headers": "^1.0.0", + "path-to-regexp": "^1.8.0", + "router": "^1.3.1", + "update-notifier-cjs": "^5.1.6" + }, + "bin": { + "superstatic": "lib/bin/server.js" + }, + "engines": { + "node": "^14.18.0 || >=16.4.0" + }, + "optionalDependencies": { + "re2": "^1.17.7" + } + }, + "node_modules/firebase-tools/node_modules/superstatic/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "devOptional": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/firebase-tools/node_modules/superstatic/node_modules/commander": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.0.tgz", + "integrity": "sha512-zS5PnTI22FIRM6ylNW8G4Ap0IEOyk62fhLSD0+uHRT9McRCLGpkVNvao4bjimpK/GShynyQkFFxHhwMcETmduA==", + "devOptional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/firebase-tools/node_modules/superstatic/node_modules/isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/superstatic/node_modules/minimatch": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-6.2.0.tgz", + "integrity": "sha512-sauLxniAmvnhhRjFwPNnJKaPFYyddAgbYdeUpHULtCT/GhzdCx/MDNy+Y40lBxTQUrMzDE8e0S43Z5uqfO0REg==", + "devOptional": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/firebase-tools/node_modules/superstatic/node_modules/path-to-regexp": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz", + "integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==", + "devOptional": true, + "dependencies": { + "isarray": "0.0.1" + } + }, + "node_modules/firebase-tools/node_modules/supports-hyperlinks": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-2.2.0.tgz", + "integrity": "sha512-6sXEzV5+I5j8Bmq9/vUphGRM/RJNT9SCURJLjwfOg51heRtguGWDzcaBlgAzKhQa0EVNpPEKzQuBwZ8S8WaCeQ==", + "devOptional": true, + "dependencies": { + "has-flag": "^4.0.0", + "supports-color": "^7.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/supports-hyperlinks/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==", + "devOptional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/supports-hyperlinks/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==", + "devOptional": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/tar": { + "version": "6.1.11", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.1.11.tgz", + "integrity": "sha512-an/KZQzQUkZCkuoAA64hM92X0Urb6VpRhAFllDzz44U2mcD5scmT3zBc4VgVpkugF580+DQn8eAFSyoQt0tznA==", + "devOptional": true, + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^3.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/firebase-tools/node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "devOptional": true, + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/firebase-tools/node_modules/tar/node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "devOptional": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/firebase-tools/node_modules/tar/node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "devOptional": true, + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/firebase-tools/node_modules/tcp-port-used": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/tcp-port-used/-/tcp-port-used-1.0.2.tgz", + "integrity": "sha512-l7ar8lLUD3XS1V2lfoJlCBaeoaWo/2xfYt81hM7VlvR4RrMVFqfmzfhLVk40hAb368uitje5gPtBRL1m/DGvLA==", + "devOptional": true, + "dependencies": { + "debug": "4.3.1", + "is2": "^2.0.6" + } + }, + "node_modules/firebase-tools/node_modules/tcp-port-used/node_modules/debug": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "devOptional": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/firebase-tools/node_modules/tcp-port-used/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/text-hex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", + "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/tmp": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz", + "integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==", + "devOptional": true, + "dependencies": { + "rimraf": "^3.0.0" + }, + "engines": { + "node": ">=8.17.0" + } + }, + "node_modules/firebase-tools/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==", + "devOptional": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/firebase-tools/node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "devOptional": true, + "engines": { + "node": ">=0.6" + } + }, + "node_modules/firebase-tools/node_modules/tough-cookie": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", + "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", + "devOptional": true, + "dependencies": { + "psl": "^1.1.28", + "punycode": "^2.1.1" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/firebase-tools/node_modules/toxic": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toxic/-/toxic-1.0.1.tgz", + "integrity": "sha512-WI3rIGdcaKULYg7KVoB0zcjikqvcYYvcuT6D89bFPz2rVR0Rl0PK6x8/X62rtdLtBKIE985NzVf/auTtGegIIg==", + "devOptional": true, + "dependencies": { + "lodash": "^4.17.10" + } + }, + "node_modules/firebase-tools/node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/triple-beam": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.3.0.tgz", + "integrity": "sha512-XrHUvV5HpdLmIj4uVMxHggLbFSZYIn7HEWsqePZcI50pco+MPqJ50wMGY794X7AOOhxOBAjbkqfAbEe/QMp2Lw==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", + "devOptional": true, + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/firebase-tools/node_modules/tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/type-check": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", + "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", + "devOptional": true, + "dependencies": { + "prelude-ls": "~1.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/firebase-tools/node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "devOptional": true, + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/firebase-tools/node_modules/typedarray-to-buffer": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", + "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", + "devOptional": true, + "dependencies": { + "is-typedarray": "^1.0.0" + } + }, + "node_modules/firebase-tools/node_modules/uc.micro": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz", + "integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/uglify-js": { + "version": "3.17.4", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.17.4.tgz", + "integrity": "sha512-T9q82TJI9e/C1TAxYvfb16xO120tMVFZrGA3f9/P4424DNu6ypK103y0GPFVa17yotwSyZW5iYXgjYHkGrJW/g==", + "devOptional": true, + "bin": { + "uglifyjs": "bin/uglifyjs" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/firebase-tools/node_modules/underscore": { + "version": "1.13.6", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.6.tgz", + "integrity": "sha512-+A5Sja4HP1M08MaXya7p5LvjuM7K6q/2EaC0+iovj/wOcMsTzMvDFbasi/oSapiwOlt252IqsKqPjCl7huKS0A==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/unique-filename": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-2.0.1.tgz", + "integrity": "sha512-ODWHtkkdx3IAR+veKxFV+VBkUMcN+FaqzUUd7IZzt+0zhDZFPFxhlqwPF3YQvMHx1TD0tdgYl+kuPnJ8E6ql7A==", + "optional": true, + "dependencies": { + "unique-slug": "^3.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/firebase-tools/node_modules/unique-slug": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-3.0.0.tgz", + "integrity": "sha512-8EyMynh679x/0gqE9fT9oilG+qEt+ibFyqjuVTsZn1+CMxH+XLlpvr2UZx4nVcCwTpx81nICr2JQFkM+HPLq4w==", + "optional": true, + "dependencies": { + "imurmurhash": "^0.1.4" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/firebase-tools/node_modules/universal-analytics": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/universal-analytics/-/universal-analytics-0.5.3.tgz", + "integrity": "sha512-HXSMyIcf2XTvwZ6ZZQLfxfViRm/yTGoRgDeTbojtq6rezeyKB0sTBcKH2fhddnteAHRcHiKgr/ACpbgjGOC6RQ==", + "devOptional": true, + "dependencies": { + "debug": "^4.3.1", + "uuid": "^8.0.0" + }, + "engines": { + "node": ">=12.18.2" + } + }, + "node_modules/firebase-tools/node_modules/universal-analytics/node_modules/debug": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", + "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", + "devOptional": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/firebase-tools/node_modules/universal-analytics/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "devOptional": true, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/firebase-tools/node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=", + "devOptional": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/firebase-tools/node_modules/update-notifier-cjs": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/update-notifier-cjs/-/update-notifier-cjs-5.1.6.tgz", + "integrity": "sha512-wgxdSBWv3x/YpMzsWz5G4p4ec7JWD0HCl8W6bmNB6E5Gwo+1ym5oN4hiXpLf0mPySVEJEIsYlkshnplkg2OP9A==", + "devOptional": true, + "dependencies": { + "boxen": "^5.0.0", + "chalk": "^4.1.0", + "configstore": "^5.0.1", + "has-yarn": "^2.1.0", + "import-lazy": "^2.1.0", + "is-ci": "^2.0.0", + "is-installed-globally": "^0.4.0", + "is-npm": "^5.0.0", + "is-yarn-global": "^0.3.0", + "isomorphic-fetch": "^3.0.0", + "pupa": "^2.1.1", + "registry-auth-token": "^5.0.1", + "registry-url": "^5.1.0", + "semver": "^7.3.7", + "semver-diff": "^3.1.1", + "xdg-basedir": "^4.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/firebase-tools/node_modules/update-notifier-cjs/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==", + "devOptional": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/firebase-tools/node_modules/update-notifier-cjs/node_modules/boxen": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/boxen/-/boxen-5.1.2.tgz", + "integrity": "sha512-9gYgQKXx+1nP8mP7CzFyaUARhg7D3n1dF/FnErWmu9l6JvGpNUN278h0aSb+QjoiKSWG+iZ3uHrcqk0qrY9RQQ==", + "devOptional": true, + "dependencies": { + "ansi-align": "^3.0.0", + "camelcase": "^6.2.0", + "chalk": "^4.1.0", + "cli-boxes": "^2.2.1", + "string-width": "^4.2.2", + "type-fest": "^0.20.2", + "widest-line": "^3.1.0", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/firebase-tools/node_modules/update-notifier-cjs/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "devOptional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/firebase-tools/node_modules/update-notifier-cjs/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "devOptional": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/firebase-tools/node_modules/update-notifier-cjs/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==", + "devOptional": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/firebase-tools/node_modules/update-notifier-cjs/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==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/update-notifier-cjs/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==", + "devOptional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/update-notifier-cjs/node_modules/registry-auth-token": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-5.0.1.tgz", + "integrity": "sha512-UfxVOj8seK1yaIOiieV4FIP01vfBDLsY0H9sQzi9EbbUdJiuuBjJgLa1DpImXMNPnVkBD4eVxTEXcrZA6kfpJA==", + "devOptional": true, + "dependencies": { + "@pnpm/npm-conf": "^1.0.4" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/firebase-tools/node_modules/update-notifier-cjs/node_modules/semver": { + "version": "7.3.8", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", + "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", + "devOptional": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/firebase-tools/node_modules/update-notifier-cjs/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==", + "devOptional": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/update-notifier-cjs/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==", + "devOptional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/firebase-tools/node_modules/uri-js": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", + "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", + "devOptional": true, + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/firebase-tools/node_modules/url-join": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/url-join/-/url-join-0.0.1.tgz", + "integrity": "sha1-HbSK1CLTQCRpqH99l73r/k+x48g=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=", + "devOptional": true, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/firebase-tools/node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "devOptional": true, + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/firebase-tools/node_modules/valid-url": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/valid-url/-/valid-url-1.0.9.tgz", + "integrity": "sha1-HBRHm0DxOXp1eC8RXkCGRHQzogA=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=", + "devOptional": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/firebase-tools/node_modules/verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", + "devOptional": true, + "engines": [ + "node >=0.6.0" + ], + "dependencies": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + } + }, + "node_modules/firebase-tools/node_modules/vm2": { + "version": "3.9.17", + "resolved": "https://registry.npmjs.org/vm2/-/vm2-3.9.17.tgz", + "devOptional": true, + "dependencies": { + "acorn": "^8.7.0", + "acorn-walk": "^8.2.0" + }, + "bin": { + "vm2": "bin/vm2" + }, + "engines": { + "node": ">=6.0" + } + }, + "node_modules/firebase-tools/node_modules/wcwidth": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", + "integrity": "sha1-8LDc+RW8X/FSivrbLA4XtTLaL+g=", + "devOptional": true, + "dependencies": { + "defaults": "^1.0.3" + } + }, + "node_modules/firebase-tools/node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha1-lmRU6HZUYuN2RNNib2dCzotwll0=", + "devOptional": true, + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/firebase-tools/node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "devOptional": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, + "node_modules/firebase-tools/node_modules/wide-align": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", + "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", + "optional": true, + "dependencies": { + "string-width": "^1.0.2 || 2 || 3 || 4" + } + }, + "node_modules/firebase-tools/node_modules/widest-line": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-3.1.0.tgz", + "integrity": "sha512-NsmoXalsWVDMGupxZ5R08ka9flZjjiLvHVAWYOKtiKM8ujtZWr9cRffak+uSE48+Ob8ObalXpwyeUiyDD6QFgg==", + "devOptional": true, + "dependencies": { + "string-width": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/winston": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/winston/-/winston-3.2.1.tgz", + "integrity": "sha512-zU6vgnS9dAWCEKg/QYigd6cgMVVNwyTzKs81XZtTFuRwJOcDdBg7AU0mXVyNbs7O5RH2zdv+BdNZUlx7mXPuOw==", + "devOptional": true, + "dependencies": { + "async": "^2.6.1", + "diagnostics": "^1.1.1", + "is-stream": "^1.1.0", + "logform": "^2.1.1", + "one-time": "0.0.4", + "readable-stream": "^3.1.1", + "stack-trace": "0.0.x", + "triple-beam": "^1.3.0", + "winston-transport": "^4.3.0" + }, + "engines": { + "node": ">= 6.4.0" + } + }, + "node_modules/firebase-tools/node_modules/winston-transport": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.4.0.tgz", + "integrity": "sha512-Lc7/p3GtqtqPBYYtS6KCN3c77/2QCev51DvcJKbkFPQNoj1sinkGwLGFDxkXY9J6p9+EPnYs+D90uwbnaiURTw==", + "devOptional": true, + "dependencies": { + "readable-stream": "^2.3.7", + "triple-beam": "^1.2.0" + }, + "engines": { + "node": ">= 6.4.0" + } + }, + "node_modules/firebase-tools/node_modules/winston-transport/node_modules/readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "devOptional": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/firebase-tools/node_modules/word-wrap": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", + "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", + "devOptional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/firebase-tools/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "devOptional": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/firebase-tools/node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "devOptional": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/firebase-tools/node_modules/wrap-ansi/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "devOptional": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/firebase-tools/node_modules/wrap-ansi/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/write-file-atomic": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz", + "integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==", + "devOptional": true, + "dependencies": { + "imurmurhash": "^0.1.4", + "is-typedarray": "^1.0.0", + "signal-exit": "^3.0.2", + "typedarray-to-buffer": "^3.1.5" + } + }, + "node_modules/firebase-tools/node_modules/ws": { + "version": "7.4.6", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.6.tgz", + "integrity": "sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A==", + "devOptional": true, + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/firebase-tools/node_modules/xdg-basedir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-4.0.0.tgz", + "integrity": "sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q==", + "devOptional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/xmlcreate": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/xmlcreate/-/xmlcreate-2.0.4.tgz", + "integrity": "sha512-nquOebG4sngPmGPICTS5EnxqhKbCmz5Ox5hsszI2T6U5qdrJizBc+0ilYSEjTSzU0yZcmvppztXe/5Al5fUwdg==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/xregexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/xregexp/-/xregexp-2.0.0.tgz", + "integrity": "sha1-UqY+VsoLhKfzpfPWGHLxJq16WUM=", + "devOptional": true, + "engines": { + "node": "*" + } + }, + "node_modules/firebase-tools/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "devOptional": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/firebase-tools/node_modules/yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "devOptional": true, + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/firebase-tools/node_modules/yargs-parser": { + "version": "20.2.4", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz", + "integrity": "sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==", + "devOptional": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/firebase-tools/node_modules/yargs/node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "devOptional": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/firebase-tools/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==", + "devOptional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/firebase-tools/node_modules/zip-stream": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-4.1.0.tgz", + "integrity": "sha512-zshzwQW7gG7hjpBlgeQP9RuyPGNxvJdzR8SUM3QhxCnLjWN2E7j3dOvpeDcQoETfHx0urRS7EtmVToql7YpU4A==", + "devOptional": true, + "dependencies": { + "archiver-utils": "^2.1.0", + "compress-commons": "^4.1.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/flatted": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", + "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", + "dev": true + }, + "node_modules/fn.name": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz", + "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==" + }, + "node_modules/follow-redirects": { + "version": "1.15.6", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", + "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/foreground-child": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.2.1.tgz", + "integrity": "sha512-PXUUyLqrR2XCWICfv6ukppP96sdFwWbNEnfEMt7jNsISjMsvaLNinAHNDYyvkyU+SZG2BTSbT5NjG+vZslfGTA==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.0", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/foreground-child/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fraction.js": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", + "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", + "dev": true, + "engines": { + "node": "*" + }, + "funding": { + "type": "patreon", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/fs-minipass": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-3.0.3.tgz", + "integrity": "sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw==", + "dev": true, + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/fs-minipass/node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/fs-monkey": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.0.6.tgz", + "integrity": "sha512-b1FMfwetIKymC0eioW7mTywihSQE4oLzQn1dB6rZB5fx/3NpNEdAWeCSMB+60/AeT0TCXsxzAlcYVEFCTAksWg==", + "dev": true + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/fuzzy": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/fuzzy/-/fuzzy-0.1.3.tgz", + "integrity": "sha512-/gZffu4ykarLrCiP3Ygsa86UAo1E5vEVlvTrpkKywXSbP9Xhln3oSp9QSV57gEq3JFFpGJ4GZ+5zdEp3FcUh4w==", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/gauge": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-4.0.4.tgz", + "integrity": "sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg==", + "deprecated": "This package is no longer supported.", + "dev": true, + "dependencies": { + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.3", + "console-control-strings": "^1.1.0", + "has-unicode": "^2.0.1", + "signal-exit": "^3.0.7", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.5" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", + "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", + "dev": true, + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "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==", + "devOptional": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==" + }, + "node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/globby": { + "version": "13.2.2", + "resolved": "https://registry.npmjs.org/globby/-/globby-13.2.2.tgz", + "integrity": "sha512-Y1zNGV+pzQdh7H39l9zgB4PJqjRNqydvdYCDG4HFXM4XuvSaQQlEc91IU1yALL8gUTDomgBAfz3XJdmUS+oo0w==", + "dev": true, + "dependencies": { + "dir-glob": "^3.0.1", + "fast-glob": "^3.3.0", + "ignore": "^5.2.4", + "merge2": "^1.4.1", + "slash": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/good-listener": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/good-listener/-/good-listener-1.2.2.tgz", + "integrity": "sha512-goW1b+d9q/HIwbVYZzZ6SsTr4IgE+WA44A0GmPIQstuOrgsFcT7VEJ48nmr9GaRtNu0XTKacFLGnBPAM6Afouw==", + "dependencies": { + "delegate": "^3.1.2" + } + }, + "node_modules/gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.1.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" + }, + "node_modules/handle-thing": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz", + "integrity": "sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg==", + "dev": true + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", + "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==", + "dev": true + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hdr-histogram-js": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/hdr-histogram-js/-/hdr-histogram-js-2.0.3.tgz", + "integrity": "sha512-Hkn78wwzWHNCp2uarhzQ2SGFLU3JY8SBDDd3TAABK4fc30wm+MuPOrg5QVFVfkKOQd6Bfz3ukJEI+q9sXEkK1g==", + "dev": true, + "dependencies": { + "@assemblyscript/loader": "^0.10.1", + "base64-js": "^1.2.0", + "pako": "^1.0.3" + } + }, + "node_modules/hdr-histogram-percentiles-obj": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hdr-histogram-percentiles-obj/-/hdr-histogram-percentiles-obj-3.0.0.tgz", + "integrity": "sha512-7kIufnBqdsBGcSZLPJwqHT3yhk1QTsSlFsVD3kx5ixH/AlgBs9yM1q6DPhXZ8f8gtdqgh7N7/5btRLpQsS2gHw==", + "dev": true + }, + "node_modules/hosted-git-info": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-6.1.1.tgz", + "integrity": "sha512-r0EI+HBMcXadMrugk0GCQ+6BQV39PiWAZVfq7oIckeGiN7sjRGyQxPdft3nQekFTCQbYxLBH+/axZMeH8UX6+w==", + "dev": true, + "dependencies": { + "lru-cache": "^7.5.1" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/hosted-git-info/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/hpack.js": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz", + "integrity": "sha512-zJxVehUdMGIKsRaNt7apO2Gqp0BdqW5yaiGHXXmbpvxgBYVZnAql+BJb4RO5ad2MgpbZKn5G6nMnegrH1FcNYQ==", + "dev": true, + "dependencies": { + "inherits": "^2.0.1", + "obuf": "^1.0.0", + "readable-stream": "^2.0.1", + "wbuf": "^1.1.0" + } + }, + "node_modules/hpack.js/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/hpack.js/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, + "node_modules/hpack.js/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/html-entities": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.5.2.tgz", + "integrity": "sha512-K//PSRMQk4FZ78Kyau+mZurHn3FH0Vwr+H36eE0rPbeYkRRi9YxceYPhuN60UwWorxyKHhqoAJl2OFKa4BVtaA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/mdevils" + }, + { + "type": "patreon", + "url": "https://patreon.com/mdevils" + } + ] + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true + }, + "node_modules/http-cache-semantics": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", + "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==", + "dev": true + }, + "node_modules/http-deceiver": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz", + "integrity": "sha512-LmpOGxTfbpgtGVxJrj5k7asXHCgNZp5nLfp+hWc8QQRqtb7fUy6kRY3BO1h9ddF6yIPYUARgxGOwB42DnxIaNw==", + "dev": true + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "dev": true, + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/http-errors/node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/http-parser-js": { + "version": "0.5.8", + "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.8.tgz", + "integrity": "sha512-SGeBX54F94Wgu5RH3X5jsDtf4eHyRogWX1XGT3b4HuW3tQPM4AaBzoUji/4AAJNXCEOWZ5O0DgZmJw1947gD5Q==" + }, + "node_modules/http-proxy": { + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", + "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==", + "dev": true, + "dependencies": { + "eventemitter3": "^4.0.0", + "follow-redirects": "^1.0.0", + "requires-port": "^1.0.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/http-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", + "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", + "dev": true, + "dependencies": { + "@tootallnate/once": "2", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/http-proxy-middleware": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.6.tgz", + "integrity": "sha512-ya/UeJ6HVBYxrgYotAZo1KvPWlgB48kUJLDePFeneHsVujFaW5WNj2NgWCAE//B1Dl02BIfYlpNgBy8Kf8Rjmw==", + "dev": true, + "dependencies": { + "@types/http-proxy": "^1.17.8", + "http-proxy": "^1.18.1", + "is-glob": "^4.0.1", + "is-plain-obj": "^3.0.0", + "micromatch": "^4.0.2" + }, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "@types/express": "^4.17.13" + }, + "peerDependenciesMeta": { + "@types/express": { + "optional": true + } + } + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "dev": true, + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/humanize-ms": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", + "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", + "dev": true, + "dependencies": { + "ms": "^2.0.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/icss-utils": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz", + "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==", + "dev": true, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/idb": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/idb/-/idb-7.1.1.tgz", + "integrity": "sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ==" + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/ignore": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", + "integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/ignore-walk": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-6.0.5.tgz", + "integrity": "sha512-VuuG0wCnjhnylG1ABXT3dAuIpTNDs/G8jlpmwXY03fXoXy/8ZK8/T+hMzt8L4WnrLCJgdybqgPagnF/f97cg3A==", + "dev": true, + "dependencies": { + "minimatch": "^9.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/ignore-walk/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/image-size": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/image-size/-/image-size-0.5.5.tgz", + "integrity": "sha512-6TDAlDPZxUFCv+fuOkIoXT/V/f3Qbq8e37p+YOiYrUv3v9cc3/6x78VdfPgFVaB9dZYeLUfKgHRebpkm/oP2VQ==", + "dev": true, + "optional": true, + "bin": { + "image-size": "bin/image-size.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/immutable": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.6.tgz", + "integrity": "sha512-Ju0+lEMyzMVZarkTn/gqRpdqd5dOPaz1mCZ0SH3JV6iFw81PldE/PEB1hWVEA288HPt4WXW8O7AWxB10M+03QQ==", + "dev": true + }, + "node_modules/import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/import-fresh/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, + "engines": { + "node": ">=4" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/infer-owner": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/infer-owner/-/infer-owner-1.0.4.tgz", + "integrity": "sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==", + "dev": true + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/ini": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ini/-/ini-3.0.1.tgz", + "integrity": "sha512-it4HyVAUTKBc6m8e1iXWvXSTdndF7HbdN713+kvLrymxTaU4AUBWrJ4vEooP+V7fexnVD3LKcBshjGGPefSMUQ==", + "dev": true, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/inquirer": { + "version": "8.2.4", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-8.2.4.tgz", + "integrity": "sha512-nn4F01dxU8VeKfq192IjLsxu0/OmMZ4Lg3xKAns148rCaXP6ntAoEkVYZThWjwON8AlzdZZi6oqnhNbxUG9hVg==", + "dependencies": { + "ansi-escapes": "^4.2.1", + "chalk": "^4.1.1", + "cli-cursor": "^3.1.0", + "cli-width": "^3.0.0", + "external-editor": "^3.0.3", + "figures": "^3.0.0", + "lodash": "^4.17.21", + "mute-stream": "0.0.8", + "ora": "^5.4.1", + "run-async": "^2.4.0", + "rxjs": "^7.5.5", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0", + "through": "^2.3.6", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/inquirer-autocomplete-prompt": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/inquirer-autocomplete-prompt/-/inquirer-autocomplete-prompt-1.4.0.tgz", + "integrity": "sha512-qHgHyJmbULt4hI+kCmwX92MnSxDs/Yhdt4wPA30qnoa01OF6uTXV8yvH4hKXgdaTNmkZ9D01MHjqKYEuJN+ONw==", + "dependencies": { + "ansi-escapes": "^4.3.1", + "chalk": "^4.0.0", + "figures": "^3.2.0", + "run-async": "^2.4.0", + "rxjs": "^6.6.2" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "inquirer": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/inquirer-autocomplete-prompt/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/inquirer-autocomplete-prompt/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/inquirer-autocomplete-prompt/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/inquirer-autocomplete-prompt/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/inquirer-autocomplete-prompt/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/inquirer-autocomplete-prompt/node_modules/rxjs": { + "version": "6.6.7", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", + "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", + "dependencies": { + "tslib": "^1.9.0" + }, + "engines": { + "npm": ">=2.0.0" + } + }, + "node_modules/inquirer-autocomplete-prompt/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/inquirer-autocomplete-prompt/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + }, + "node_modules/inquirer/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/inquirer/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/inquirer/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/inquirer/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/inquirer/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/inquirer/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "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==", + "optional": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/ionic-emoji-rating": { + "version": "1.0.15", + "resolved": "https://registry.npmjs.org/ionic-emoji-rating/-/ionic-emoji-rating-1.0.15.tgz", + "integrity": "sha512-ooNInojKERSgfoMZOAWvLm0MAARmAh1ibGr32x9S98J1jv2RC9DdaXOBWf/Xx9ZfJmvUAgZKjufcriszGcuZFA==" + }, + "node_modules/ionicons": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/ionicons/-/ionicons-7.4.0.tgz", + "integrity": "sha512-ZK94MMqgzMCPPMhmk8Ouu6goyVHFIlw/ACP6oe3FrikcI0N7CX0xcwVaEbUc0G/v3W0shI93vo+9ve/KpvcNhQ==", + "dependencies": { + "@stencil/core": "^4.0.3" + } + }, + "node_modules/ip-address": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz", + "integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==", + "dev": true, + "dependencies": { + "jsbn": "1.1.0", + "sprintf-js": "^1.1.3" + }, + "engines": { + "node": ">= 12" + } + }, + "node_modules/ip-address/node_modules/sprintf-js": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", + "dev": true + }, + "node_modules/ipaddr.js": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.2.0.tgz", + "integrity": "sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA==", + "dev": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true + }, + "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==", + "devOptional": true, + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.14.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.14.0.tgz", + "integrity": "sha512-a5dFJih5ZLYlRtDc0dZWP7RiKr6xIKzmn/oAYCDvdLThadVgyJwlaoQPmRtMSpz+rk0OGAgIu+TcM9HUF0fk1A==", + "dev": true, + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "devOptional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "engines": { + "node": ">=8" + } + }, + "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==", + "devOptional": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-interactive": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", + "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-lambda": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-lambda/-/is-lambda-1.0.1.tgz", + "integrity": "sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==", + "dev": true + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "devOptional": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-plain-obj": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-3.0.0.tgz", + "integrity": "sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dev": true, + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-what": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/is-what/-/is-what-3.14.1.tgz", + "integrity": "sha512-sNxgpk9793nzSs7bA6JQJGeIuRBQhAaNGG77kzYQgMkrID+lS6SlK07K5LaptscDlSaIgH+GPFzf+d75FVxozA==", + "dev": true + }, + "node_modules/is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true + }, + "node_modules/isbinaryfile": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-4.0.10.tgz", + "integrity": "sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw==", + "dev": true, + "engines": { + "node": ">= 8.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/gjtorikian/" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "node_modules/isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", + "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", + "dev": true, + "dependencies": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report/node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/istanbul-lib-report/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "dev": true, + "dependencies": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/istanbul-reports": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", + "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", + "dev": true, + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jackspeak": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.0.tgz", + "integrity": "sha512-JVYhQnN59LVPFCEcVa2C3CrEKYacvjRfqIQl+h8oi91aLYQVWRYbxjPcv1bUiUy/kLmQaANrYfNMCO3kuEDHfw==", + "dev": true, + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jasmine-core": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-4.5.0.tgz", + "integrity": "sha512-9PMzyvhtocxb3aXJVOPqBDswdgyAeSB81QnLop4npOpbqnheaTEwPc9ZloQeVswugPManznQBjD8kWDTjlnHuw==", + "dev": true + }, + "node_modules/jest-worker": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", + "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/jest-worker/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "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==", + "dev": true + }, + "node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsbn": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz", + "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==", + "dev": true + }, + "node_modules/jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "dev": true, + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==" + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonc-parser": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.0.tgz", + "integrity": "sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==" + }, + "node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/jsonparse": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz", + "integrity": "sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==", + "dev": true, + "engines": [ + "node >= 0.2.0" + ] + }, + "node_modules/karma": { + "version": "6.4.3", + "resolved": "https://registry.npmjs.org/karma/-/karma-6.4.3.tgz", + "integrity": "sha512-LuucC/RE92tJ8mlCwqEoRWXP38UMAqpnq98vktmS9SznSoUPPUJQbc91dHcxcunROvfQjdORVA/YFviH+Xci9Q==", + "dev": true, + "dependencies": { + "@colors/colors": "1.5.0", + "body-parser": "^1.19.0", + "braces": "^3.0.2", + "chokidar": "^3.5.1", + "connect": "^3.7.0", + "di": "^0.0.1", + "dom-serialize": "^2.2.1", + "glob": "^7.1.7", + "graceful-fs": "^4.2.6", + "http-proxy": "^1.18.1", + "isbinaryfile": "^4.0.8", + "lodash": "^4.17.21", + "log4js": "^6.4.1", + "mime": "^2.5.2", + "minimatch": "^3.0.4", + "mkdirp": "^0.5.5", + "qjobs": "^1.2.0", + "range-parser": "^1.2.1", + "rimraf": "^3.0.2", + "socket.io": "^4.7.2", + "source-map": "^0.6.1", + "tmp": "^0.2.1", + "ua-parser-js": "^0.7.30", + "yargs": "^16.1.1" + }, + "bin": { + "karma": "bin/karma" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/karma-chrome-launcher": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/karma-chrome-launcher/-/karma-chrome-launcher-3.1.1.tgz", + "integrity": "sha512-hsIglcq1vtboGPAN+DGCISCFOxW+ZVnIqhDQcCMqqCp+4dmJ0Qpq5QAjkbA0X2L9Mi6OBkHi2Srrbmm7pUKkzQ==", + "dev": true, + "dependencies": { + "which": "^1.2.1" + } + }, + "node_modules/karma-coverage": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/karma-coverage/-/karma-coverage-2.2.1.tgz", + "integrity": "sha512-yj7hbequkQP2qOSb20GuNSIyE//PgJWHwC2IydLE6XRtsnaflv+/OSGNssPjobYUlhVVagy99TQpqUt3vAUG7A==", + "dev": true, + "dependencies": { + "istanbul-lib-coverage": "^3.2.0", + "istanbul-lib-instrument": "^5.1.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.1", + "istanbul-reports": "^3.0.5", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/karma-coverage/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/karma-coverage/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/karma-jasmine": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/karma-jasmine/-/karma-jasmine-5.1.0.tgz", + "integrity": "sha512-i/zQLFrfEpRyQoJF9fsCdTMOF5c2dK7C7OmsuKg2D0YSsuZSfQDiLuaiktbuio6F2wiCsZSnSnieIQ0ant/uzQ==", + "dev": true, + "dependencies": { + "jasmine-core": "^4.1.0" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "karma": "^6.0.0" + } + }, + "node_modules/karma-jasmine-html-reporter": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/karma-jasmine-html-reporter/-/karma-jasmine-html-reporter-2.0.0.tgz", + "integrity": "sha512-SB8HNNiazAHXM1vGEzf8/tSyEhkfxuDdhYdPBX2Mwgzt0OuF2gicApQ+uvXLID/gXyJQgvrM9+1/2SxZFUUDIA==", + "dev": true, + "peerDependencies": { + "jasmine-core": "^4.0.0", + "karma": "^6.0.0", + "karma-jasmine": "^5.0.0" + } + }, + "node_modules/karma-source-map-support": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/karma-source-map-support/-/karma-source-map-support-1.4.0.tgz", + "integrity": "sha512-RsBECncGO17KAoJCYXjv+ckIz+Ii9NCi+9enk+rq6XC81ezYkb4/RHE6CTXdA7IOJqoF3wcaLfVG0CPmE5ca6A==", + "dev": true, + "dependencies": { + "source-map-support": "^0.5.5" + } + }, + "node_modules/karma/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/karma/node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/karma/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/karma/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/karma/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/karma/node_modules/tmp": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.3.tgz", + "integrity": "sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w==", + "dev": true, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/karma/node_modules/yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "dev": true, + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/karma/node_modules/yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/katex": { + "version": "0.16.11", + "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.11.tgz", + "integrity": "sha512-RQrI8rlHY92OLf3rho/Ts8i/XvjgguEjOkO1BEXcU3N8BqPpSzBNwV/G0Ukr+P/l3ivvJUE/Fa/CwbS6HesGNQ==", + "funding": [ + "https://opencollective.com/katex", + "https://github.com/sponsors/katex" + ], + "optional": true, + "dependencies": { + "commander": "^8.3.0" + }, + "bin": { + "katex": "cli.js" + } + }, + "node_modules/katex/node_modules/commander": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", + "optional": true, + "engines": { + "node": ">= 12" + } + }, + "node_modules/khroma": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/khroma/-/khroma-2.1.0.tgz", + "integrity": "sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw==", + "optional": true + }, + "node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/klona": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/klona/-/klona-2.0.6.tgz", + "integrity": "sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/kuler": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz", + "integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==" + }, + "node_modules/layout-base": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/layout-base/-/layout-base-1.0.2.tgz", + "integrity": "sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg==", + "optional": true + }, + "node_modules/less": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/less/-/less-4.1.3.tgz", + "integrity": "sha512-w16Xk/Ta9Hhyei0Gpz9m7VS8F28nieJaL/VyShID7cYvP6IL5oHeL6p4TXSDJqZE/lNv0oJ2pGVjJsRkfwm5FA==", + "dev": true, + "dependencies": { + "copy-anything": "^2.0.1", + "parse-node-version": "^1.0.1", + "tslib": "^2.3.0" + }, + "bin": { + "lessc": "bin/lessc" + }, + "engines": { + "node": ">=6" + }, + "optionalDependencies": { + "errno": "^0.1.1", + "graceful-fs": "^4.1.2", + "image-size": "~0.5.0", + "make-dir": "^2.1.0", + "mime": "^1.4.1", + "needle": "^3.1.0", + "source-map": "~0.6.0" + } + }, + "node_modules/less-loader": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/less-loader/-/less-loader-11.1.0.tgz", + "integrity": "sha512-C+uDBV7kS7W5fJlUjq5mPBeBVhYpTIm5gB09APT9o3n/ILeaXVsiSFTbZpTJCJwQ/Crczfn3DmfQFwxYusWFug==", + "dev": true, + "dependencies": { + "klona": "^2.0.4" + }, + "engines": { + "node": ">= 14.15.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "less": "^3.5.0 || ^4.0.0", + "webpack": "^5.0.0" + } + }, + "node_modules/less/node_modules/make-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", + "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", + "dev": true, + "optional": true, + "dependencies": { + "pify": "^4.0.1", + "semver": "^5.6.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/less/node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true, + "optional": true, + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/less/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "optional": true, + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/less/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/license-webpack-plugin": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/license-webpack-plugin/-/license-webpack-plugin-4.0.2.tgz", + "integrity": "sha512-771TFWFD70G1wLTC4oU2Cw4qvtmNrIw+wRvBtn+okgHl7slJVi7zfNcdmqDL72BojM30VNJ2UHylr1o77U37Jw==", + "dev": true, + "dependencies": { + "webpack-sources": "^3.0.0" + }, + "peerDependenciesMeta": { + "webpack": { + "optional": true + }, + "webpack-sources": { + "optional": true + } + } + }, + "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==", + "dev": true + }, + "node_modules/loader-runner": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", + "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==", + "engines": { + "node": ">=6.11.5" + } + }, + "node_modules/loader-utils": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-3.2.1.tgz", + "integrity": "sha512-ZvFw1KWS3GVyYBYb7qkmRM/WwL2TQQBxgCK62rlvm4WpVQ23Nb4tYjApUlfjrEGvOs7KHEsmyUn75OHZrJMWPw==", + "dev": true, + "engines": { + "node": ">= 12.13.0" + } + }, + "node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, + "node_modules/lodash-es": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", + "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==", + "optional": true + }, + "node_modules/lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==" + }, + "node_modules/lodash.debounce": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", + "dev": true + }, + "node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-symbols/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/log-symbols/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/log-symbols/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/log-symbols/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/log-symbols/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/log-symbols/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/log4js": { + "version": "6.9.1", + "resolved": "https://registry.npmjs.org/log4js/-/log4js-6.9.1.tgz", + "integrity": "sha512-1somDdy9sChrr9/f4UlzhdaGfDR2c/SaD2a4T7qEkG4jTS57/B3qmnjLYePwQ8cqWnUHZI0iAKxMBpCZICiZ2g==", + "dev": true, + "dependencies": { + "date-format": "^4.0.14", + "debug": "^4.3.4", + "flatted": "^3.2.7", + "rfdc": "^1.3.0", + "streamroller": "^3.1.5" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/logform": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/logform/-/logform-2.6.0.tgz", + "integrity": "sha512-1ulHeNPp6k/LD8H91o7VYFBng5i1BDE7HoKxVbZiGFidS1Rj65qcywLxX+pVfAPoQJEjRdvKcusKwOupHCVOVQ==", + "dependencies": { + "@colors/colors": "1.6.0", + "@types/triple-beam": "^1.3.2", + "fecha": "^4.2.0", + "ms": "^2.1.1", + "safe-stable-stringify": "^2.3.1", + "triple-beam": "^1.3.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/logform/node_modules/@colors/colors": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.6.0.tgz", + "integrity": "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==", + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/long": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", + "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==" + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/magic-string": { + "version": "0.29.0", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.29.0.tgz", + "integrity": "sha512-WcfidHrDjMY+eLjlU+8OvwREqHwpgCeKVBUpQ3OhYYuvfaYCUgcbuBzappNzZvg/v8onU3oQj+BYpkOJe9Iw4Q==", + "dev": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.13" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dev": true, + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/make-fetch-happen": { + "version": "10.2.1", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-10.2.1.tgz", + "integrity": "sha512-NgOPbRiaQM10DYXvN3/hhGVI2M5MtITFryzBGxHM5p4wnFxsVCbxkrBrDsk+EZ5OB4jEOT7AjDxtdF+KVEFT7w==", + "dev": true, + "dependencies": { + "agentkeepalive": "^4.2.1", + "cacache": "^16.1.0", + "http-cache-semantics": "^4.1.0", + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.0", + "is-lambda": "^1.0.1", + "lru-cache": "^7.7.1", + "minipass": "^3.1.6", + "minipass-collect": "^1.0.2", + "minipass-fetch": "^2.0.3", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^0.6.3", + "promise-retry": "^2.0.1", + "socks-proxy-agent": "^7.0.0", + "ssri": "^9.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/make-fetch-happen/node_modules/@npmcli/fs": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-2.1.2.tgz", + "integrity": "sha512-yOJKRvohFOaLqipNtwYB9WugyZKhC/DZC4VYPmpaCzDBrA8YpK3qHZ8/HGscMnE4GqbkLNuVcCnxkeQEdGt6LQ==", + "dev": true, + "dependencies": { + "@gar/promisify": "^1.1.3", + "semver": "^7.3.5" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/make-fetch-happen/node_modules/cacache": { + "version": "16.1.3", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-16.1.3.tgz", + "integrity": "sha512-/+Emcj9DAXxX4cwlLmRI9c166RuL3w30zp4R7Joiv2cQTtTtA+jeuCAjH3ZlGnYS3tKENSrKhAzVVP9GVyzeYQ==", + "dev": true, + "dependencies": { + "@npmcli/fs": "^2.1.0", + "@npmcli/move-file": "^2.0.0", + "chownr": "^2.0.0", + "fs-minipass": "^2.1.0", + "glob": "^8.0.1", + "infer-owner": "^1.0.4", + "lru-cache": "^7.7.1", + "minipass": "^3.1.6", + "minipass-collect": "^1.0.2", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "mkdirp": "^1.0.4", + "p-map": "^4.0.0", + "promise-inflight": "^1.0.1", + "rimraf": "^3.0.2", + "ssri": "^9.0.0", + "tar": "^6.1.11", + "unique-filename": "^2.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/make-fetch-happen/node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "dev": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/make-fetch-happen/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/make-fetch-happen/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/make-fetch-happen/node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true, + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/make-fetch-happen/node_modules/ssri": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-9.0.1.tgz", + "integrity": "sha512-o57Wcn66jMQvfHG1FlYbWeZWW/dHZhJXjpIcTfXldXEk5nz5lStPo3mK0OJQfGR3RbZUlbISexbljkJzuEj/8Q==", + "dev": true, + "dependencies": { + "minipass": "^3.1.1" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/make-fetch-happen/node_modules/unique-filename": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-2.0.1.tgz", + "integrity": "sha512-ODWHtkkdx3IAR+veKxFV+VBkUMcN+FaqzUUd7IZzt+0zhDZFPFxhlqwPF3YQvMHx1TD0tdgYl+kuPnJ8E6ql7A==", + "dev": true, + "dependencies": { + "unique-slug": "^3.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/make-fetch-happen/node_modules/unique-slug": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-3.0.0.tgz", + "integrity": "sha512-8EyMynh679x/0gqE9fT9oilG+qEt+ibFyqjuVTsZn1+CMxH+XLlpvr2UZx4nVcCwTpx81nICr2JQFkM+HPLq4w==", + "dev": true, + "dependencies": { + "imurmurhash": "^0.1.4" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/make-fetch-happen/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/marked": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/marked/-/marked-4.3.0.tgz", + "integrity": "sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A==", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 12" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/memfs": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/memfs/-/memfs-3.5.3.tgz", + "integrity": "sha512-UERzLsxzllchadvbPs5aolHh65ISpKpM+ccLbOJ8/vvpBKmAWf+la7dXFy7Mr0ySHbdHrFv5kGFCUHHe6GFEmw==", + "dev": true, + "dependencies": { + "fs-monkey": "^1.0.4" + }, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==", + "dev": true + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==" + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/mermaid": { + "version": "9.4.3", + "resolved": "https://registry.npmjs.org/mermaid/-/mermaid-9.4.3.tgz", + "integrity": "sha512-TLkQEtqhRSuEHSE34lh5bCa94KATCyluAXmFnNI2PRZwOpXFeqiJWwZl+d2CcemE1RS6QbbueSSq9QIg8Uxcyw==", + "optional": true, + "dependencies": { + "@braintree/sanitize-url": "^6.0.0", + "cytoscape": "^3.23.0", + "cytoscape-cose-bilkent": "^4.1.0", + "cytoscape-fcose": "^2.1.0", + "d3": "^7.4.0", + "dagre-d3-es": "7.0.9", + "dayjs": "^1.11.7", + "dompurify": "2.4.3", + "elkjs": "^0.8.2", + "khroma": "^2.0.0", + "lodash-es": "^4.17.21", + "non-layered-tidy-tree-layout": "^2.0.2", + "stylis": "^4.1.2", + "ts-dedent": "^2.2.0", + "uuid": "^9.0.0", + "web-worker": "^1.2.0" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/micromatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.7.tgz", + "integrity": "sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==", + "dev": true, + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "dev": true, + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "engines": { + "node": ">=6" + } + }, + "node_modules/mini-css-extract-plugin": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.7.2.tgz", + "integrity": "sha512-EdlUizq13o0Pd+uCp+WO/JpkLvHRVGt97RqfeGhXqAcorYo1ypJSpkV+WDT0vY/kmh/p7wRdJNJtuyK540PXDw==", + "dev": true, + "dependencies": { + "schema-utils": "^4.0.0" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" + } + }, + "node_modules/minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", + "dev": true + }, + "node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-4.2.8.tgz", + "integrity": "sha512-fNzuVyifolSLFL4NzpF+wEF4qrgqaaKX0haXPQEdQ7NKAN+WecoKMHV09YcuL/DHxrUsYQOK3MiuDf7Ip2OXfQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-collect": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-1.0.2.tgz", + "integrity": "sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==", + "dev": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minipass-collect/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-collect/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/minipass-fetch": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-2.1.2.tgz", + "integrity": "sha512-LT49Zi2/WMROHYoqGgdlQIZh8mLPZmOrN2NdJjMXxYe4nkN6FUyuPuOAOedNJDrx0IRGg9+4guZewtp8hE6TxA==", + "dev": true, + "dependencies": { + "minipass": "^3.1.6", + "minipass-sized": "^1.0.3", + "minizlib": "^2.1.2" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + }, + "optionalDependencies": { + "encoding": "^0.1.13" + } + }, + "node_modules/minipass-fetch/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-fetch/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/minipass-flush": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz", + "integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==", + "dev": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minipass-flush/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-flush/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/minipass-json-stream": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minipass-json-stream/-/minipass-json-stream-1.0.1.tgz", + "integrity": "sha512-ODqY18UZt/I8k+b7rl2AENgbWE8IDYam+undIJONvigAz8KR5GWblsFTEfQs0WODsjbSXWlm+JHEv8Gr6Tfdbg==", + "dev": true, + "dependencies": { + "jsonparse": "^1.3.1", + "minipass": "^3.0.0" + } + }, + "node_modules/minipass-json-stream/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-json-stream/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/minipass-pipeline": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", + "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", + "dev": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-pipeline/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-pipeline/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/minipass-sized": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz", + "integrity": "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==", + "dev": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-sized/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-sized/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "dev": true, + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minizlib/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minizlib/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dev": true, + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/moment": { + "version": "2.30.1", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", + "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/multicast-dns": { + "version": "7.2.5", + "resolved": "https://registry.npmjs.org/multicast-dns/-/multicast-dns-7.2.5.tgz", + "integrity": "sha512-2eznPJP8z2BFLX50tf0LuODrpINqP1RVIm/CObbTcBRITQgmC/TjcREF1NeTBzIcR5XO/ukWo+YHOjBbFwIupg==", + "dev": true, + "dependencies": { + "dns-packet": "^5.2.2", + "thunky": "^1.0.2" + }, + "bin": { + "multicast-dns": "cli.js" + } + }, + "node_modules/mute-stream": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", + "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==" + }, + "node_modules/nanoid": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/needle": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/needle/-/needle-3.3.1.tgz", + "integrity": "sha512-6k0YULvhpw+RoLNiQCRKOl09Rv1dPLr8hHnVjHqdolKwDrdNyk+Hmrthi4lIGPPz3r39dLx0hsF5s40sZ3Us4Q==", + "dev": true, + "optional": true, + "dependencies": { + "iconv-lite": "^0.6.3", + "sax": "^1.2.4" + }, + "bin": { + "needle": "bin/needle" + }, + "engines": { + "node": ">= 4.4.x" + } + }, + "node_modules/needle/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "optional": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==" + }, + "node_modules/ngx-markdown": { + "version": "15.1.2", + "resolved": "https://registry.npmjs.org/ngx-markdown/-/ngx-markdown-15.1.2.tgz", + "integrity": "sha512-mAUORpUnHCV4tnxEHV4oS5YEdIaolUclulCblUrvAEU3AEND8MMTxlwHujqVC2M398/aKH0SBSrjLzDbMUJCoQ==", + "dependencies": { + "tslib": "^2.3.0" + }, + "optionalDependencies": { + "clipboard": "^2.0.11", + "emoji-toolkit": "^7.0.0", + "katex": "^0.16.0", + "mermaid": "^9.1.2", + "prismjs": "^1.28.0" + }, + "peerDependencies": { + "@angular/common": "^15.0.0", + "@angular/core": "^15.0.0", + "@angular/platform-browser": "^15.0.0", + "@types/marked": "^4.0.3", + "marked": "^4.0.17", + "rxjs": "^6.5.3 || ^7.4.0", + "zone.js": "~0.11.4 || ~0.12.0 || ~0.13.0" + } + }, + "node_modules/nice-napi": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/nice-napi/-/nice-napi-1.0.2.tgz", + "integrity": "sha512-px/KnJAJZf5RuBGcfD+Sp2pAKq0ytz8j+1NehvgIGFkvtvFrDM3T8E4x/JJODXK9WZow8RRGrbA9QQ3hs+pDhA==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "!win32" + ], + "dependencies": { + "node-addon-api": "^3.0.0", + "node-gyp-build": "^4.2.2" + } + }, + "node_modules/node-addon-api": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-3.2.1.tgz", + "integrity": "sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A==", + "dev": true, + "optional": true + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-forge": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", + "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==", + "dev": true, + "engines": { + "node": ">= 6.13.0" + } + }, + "node_modules/node-gyp": { + "version": "9.4.1", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-9.4.1.tgz", + "integrity": "sha512-OQkWKbjQKbGkMf/xqI1jjy3oCTgMKJac58G2+bjZb3fza6gW2YrCSdMQYaoTb70crvE//Gngr4f0AgVHmqHvBQ==", + "dev": true, + "dependencies": { + "env-paths": "^2.2.0", + "exponential-backoff": "^3.1.1", + "glob": "^7.1.4", + "graceful-fs": "^4.2.6", + "make-fetch-happen": "^10.0.3", + "nopt": "^6.0.0", + "npmlog": "^6.0.0", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "tar": "^6.1.2", + "which": "^2.0.2" + }, + "bin": { + "node-gyp": "bin/node-gyp.js" + }, + "engines": { + "node": "^12.13 || ^14.13 || >=16" + } + }, + "node_modules/node-gyp-build": { + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.1.tgz", + "integrity": "sha512-OSs33Z9yWr148JZcbZd5WiAXhh/n9z8TxQcdMhIOlpN9AhWpLfvVFO73+m77bBABQMaY9XSvIa+qk0jlI7Gcaw==", + "dev": true, + "optional": true, + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, + "node_modules/node-gyp/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/node-gyp/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/node-gyp/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/node-gyp/node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/node-releases": { + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", + "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==" + }, + "node_modules/non-layered-tidy-tree-layout": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/non-layered-tidy-tree-layout/-/non-layered-tidy-tree-layout-2.0.2.tgz", + "integrity": "sha512-gkXMxRzUH+PB0ax9dUN0yYF0S25BqeAYqhgMaLUFmpXLEk7Fcu8f4emJuOAY0V8kjDICxROIKsTAKsV/v355xw==", + "optional": true + }, + "node_modules/nopt": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-6.0.0.tgz", + "integrity": "sha512-ZwLpbTgdhuZUnZzjd7nb1ZV+4DoiC6/sfiVKok72ym/4Tlf+DFdlHYmT2JPmcNNWV6Pi3SDf1kT+A4r9RTuT9g==", + "dev": true, + "dependencies": { + "abbrev": "^1.0.0" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/normalize-package-data": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-5.0.0.tgz", + "integrity": "sha512-h9iPVIfrVZ9wVYQnxFgtw1ugSvGEMOlyPWWtm8BMJhnwyEL/FLbYbTY3V3PpjI/BUK67n9PEWDu6eHzu1fB15Q==", + "dev": true, + "dependencies": { + "hosted-git-info": "^6.0.0", + "is-core-module": "^2.8.1", + "semver": "^7.3.5", + "validate-npm-package-license": "^3.0.4" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "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==", + "devOptional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-range": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-bundled": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-3.0.1.tgz", + "integrity": "sha512-+AvaheE/ww1JEwRHOrn4WHNzOxGtVp+adrg2AeZS/7KuxGUYFuBta98wYpfHBbJp6Tg6j1NKSEVHNcfZzJHQwQ==", + "dev": true, + "dependencies": { + "npm-normalize-package-bin": "^3.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm-install-checks": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/npm-install-checks/-/npm-install-checks-6.3.0.tgz", + "integrity": "sha512-W29RiK/xtpCGqn6f3ixfRYGk+zRyr+Ew9F2E20BfXxT5/euLdA/Nm7fO7OeTGuAmTs30cpgInyJ0cYe708YTZw==", + "dev": true, + "dependencies": { + "semver": "^7.1.1" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm-normalize-package-bin": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-3.0.1.tgz", + "integrity": "sha512-dMxCf+zZ+3zeQZXKxmyuCKlIDPGuv8EF940xbkC4kQVDTtqoh6rJFO+JTKSA6/Rwi0getWmtuy4Itup0AMcaDQ==", + "dev": true, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm-package-arg": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-10.1.0.tgz", + "integrity": "sha512-uFyyCEmgBfZTtrKk/5xDfHp6+MdrqGotX/VoOyEEl3mBwiEE5FlBaePanazJSVMPT7vKepcjYBY2ztg9A3yPIA==", + "dev": true, + "dependencies": { + "hosted-git-info": "^6.0.0", + "proc-log": "^3.0.0", + "semver": "^7.3.5", + "validate-npm-package-name": "^5.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm-packlist": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-7.0.4.tgz", + "integrity": "sha512-d6RGEuRrNS5/N84iglPivjaJPxhDbZmlbTwTDX2IbcRHG5bZCdtysYMhwiPvcF4GisXHGn7xsxv+GQ7T/02M5Q==", + "dev": true, + "dependencies": { + "ignore-walk": "^6.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm-pick-manifest": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/npm-pick-manifest/-/npm-pick-manifest-8.0.1.tgz", + "integrity": "sha512-mRtvlBjTsJvfCCdmPtiu2bdlx8d/KXtF7yNXNWe7G0Z36qWA9Ny5zXsI2PfBZEv7SXgoxTmNaTzGSbbzDZChoA==", + "dev": true, + "dependencies": { + "npm-install-checks": "^6.0.0", + "npm-normalize-package-bin": "^3.0.0", + "npm-package-arg": "^10.0.0", + "semver": "^7.3.5" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm-registry-fetch": { + "version": "14.0.5", + "resolved": "https://registry.npmjs.org/npm-registry-fetch/-/npm-registry-fetch-14.0.5.tgz", + "integrity": "sha512-kIDMIo4aBm6xg7jOttupWZamsZRkAqMqwqqbVXnUqstY5+tapvv6bkH/qMR76jdgV+YljEUCyWx3hRYMrJiAgA==", + "dev": true, + "dependencies": { + "make-fetch-happen": "^11.0.0", + "minipass": "^5.0.0", + "minipass-fetch": "^3.0.0", + "minipass-json-stream": "^1.0.1", + "minizlib": "^2.1.2", + "npm-package-arg": "^10.0.0", + "proc-log": "^3.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm-registry-fetch/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/npm-registry-fetch/node_modules/make-fetch-happen": { + "version": "11.1.1", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-11.1.1.tgz", + "integrity": "sha512-rLWS7GCSTcEujjVBs2YqG7Y4643u8ucvCJeSRqiLYhesrDuzeuFIk37xREzAsfQaqzl8b9rNCE4m6J8tvX4Q8w==", + "dev": true, + "dependencies": { + "agentkeepalive": "^4.2.1", + "cacache": "^17.0.0", + "http-cache-semantics": "^4.1.1", + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.0", + "is-lambda": "^1.0.1", + "lru-cache": "^7.7.1", + "minipass": "^5.0.0", + "minipass-fetch": "^3.0.0", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^0.6.3", + "promise-retry": "^2.0.1", + "socks-proxy-agent": "^7.0.0", + "ssri": "^10.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm-registry-fetch/node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm-registry-fetch/node_modules/minipass-fetch": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-3.0.5.tgz", + "integrity": "sha512-2N8elDQAtSnFV0Dk7gt15KHsS0Fyz6CbYZ360h0WTYV1Ty46li3rAXVOQj1THMNLdmrD9Vt5pBPtWtVkpwGBqg==", + "dev": true, + "dependencies": { + "minipass": "^7.0.3", + "minipass-sized": "^1.0.3", + "minizlib": "^2.1.2" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + }, + "optionalDependencies": { + "encoding": "^0.1.13" + } + }, + "node_modules/npm-registry-fetch/node_modules/minipass-fetch/node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npmlog": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-6.0.2.tgz", + "integrity": "sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg==", + "deprecated": "This package is no longer supported.", + "dev": true, + "dependencies": { + "are-we-there-yet": "^3.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^4.0.3", + "set-blocking": "^2.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dev": true, + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "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==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz", + "integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/obuf": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz", + "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==", + "dev": true + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dev": true, + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/on-headers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", + "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/one-time": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/one-time/-/one-time-1.0.0.tgz", + "integrity": "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==", + "dependencies": { + "fn.name": "1.x.x" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/open": { + "version": "8.4.1", + "resolved": "https://registry.npmjs.org/open/-/open-8.4.1.tgz", + "integrity": "sha512-/4b7qZNhv6Uhd7jjnREh1NjnPxlTq+XNWPG88Ydkj5AILcA5m3ajvcg57pB24EQjKv0dK62XnDqk9c/hkIG5Kg==", + "dependencies": { + "define-lazy-prop": "^2.0.0", + "is-docker": "^2.1.1", + "is-wsl": "^2.2.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", + "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", + "dependencies": { + "bl": "^4.1.0", + "chalk": "^4.1.0", + "cli-cursor": "^3.1.0", + "cli-spinners": "^2.5.0", + "is-interactive": "^1.0.0", + "is-unicode-supported": "^0.1.0", + "log-symbols": "^4.1.0", + "strip-ansi": "^6.0.0", + "wcwidth": "^1.0.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/ora/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/ora/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/ora/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/ora/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/ora/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-map": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", + "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", + "dev": true, + "dependencies": { + "aggregate-error": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-retry": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz", + "integrity": "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==", + "dev": true, + "dependencies": { + "@types/retry": "0.12.0", + "retry": "^0.13.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-retry/node_modules/retry": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.0.tgz", + "integrity": "sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw==", + "dev": true + }, + "node_modules/pacote": { + "version": "15.0.8", + "resolved": "https://registry.npmjs.org/pacote/-/pacote-15.0.8.tgz", + "integrity": "sha512-UlcumB/XS6xyyIMwg/WwMAyUmga+RivB5KgkRwA1hZNtrx+0Bt41KxHCvg1kr0pZ/ZeD8qjhW4fph6VaYRCbLw==", + "dev": true, + "dependencies": { + "@npmcli/git": "^4.0.0", + "@npmcli/installed-package-contents": "^2.0.1", + "@npmcli/promise-spawn": "^6.0.1", + "@npmcli/run-script": "^6.0.0", + "cacache": "^17.0.0", + "fs-minipass": "^3.0.0", + "minipass": "^4.0.0", + "npm-package-arg": "^10.0.0", + "npm-packlist": "^7.0.0", + "npm-pick-manifest": "^8.0.0", + "npm-registry-fetch": "^14.0.0", + "proc-log": "^3.0.0", + "promise-retry": "^2.0.1", + "read-package-json": "^6.0.0", + "read-package-json-fast": "^3.0.0", + "ssri": "^10.0.0", + "tar": "^6.1.11" + }, + "bin": { + "pacote": "lib/bin.js" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "dev": true + }, + "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, + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parse-node-version": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parse-node-version/-/parse-node-version-1.0.1.tgz", + "integrity": "sha512-3YHlOa/JgH6Mnpr05jP9eDG254US9ek25LyIxZlDItp2iJtwyaXQb57lBYLdT3MowkUFYEV2XXNAYIPlESvJlA==", + "dev": true, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/parse5": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz", + "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==", + "dev": true + }, + "node_modules/parse5-html-rewriting-stream": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/parse5-html-rewriting-stream/-/parse5-html-rewriting-stream-7.0.0.tgz", + "integrity": "sha512-mazCyGWkmCRWDI15Zp+UiCqMp/0dgEmkZRvhlsqqKYr4SsVm/TvnSpD9fCvqCA2zoWJcfRym846ejWBBHRiYEg==", + "dev": true, + "dependencies": { + "entities": "^4.3.0", + "parse5": "^7.0.0", + "parse5-sax-parser": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-html-rewriting-stream/node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/parse5-html-rewriting-stream/node_modules/parse5": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz", + "integrity": "sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==", + "dev": true, + "dependencies": { + "entities": "^4.4.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-htmlparser2-tree-adapter": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-6.0.1.tgz", + "integrity": "sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA==", + "dev": true, + "dependencies": { + "parse5": "^6.0.1" + } + }, + "node_modules/parse5-sax-parser": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/parse5-sax-parser/-/parse5-sax-parser-7.0.0.tgz", + "integrity": "sha512-5A+v2SNsq8T6/mG3ahcz8ZtQ0OUFTatxPbeidoMB7tkJSGDY3tdfl4MHovtLQHkEn5CGxijNWRQHhRQ6IRpXKg==", + "dev": true, + "dependencies": { + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-sax-parser/node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/parse5-sax-parser/node_modules/parse5": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz", + "integrity": "sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==", + "dev": true, + "dependencies": { + "entities": "^4.4.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "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/path-scurry/node_modules/lru-cache": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.3.0.tgz", + "integrity": "sha512-CQl19J/g+Hbjbv4Y3mFNNXFEL/5t/KCg8POCuUqd4rMKjGG+j1ybER83hxV58zL+dFI1PTkt3GNFSHRt+d8qEQ==", + "dev": true, + "engines": { + "node": "14 || >=16.14" + } + }, + "node_modules/path-scurry/node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==", + "dev": true + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/picocolors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", + "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "devOptional": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "dev": true, + "optional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/piscina": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/piscina/-/piscina-3.2.0.tgz", + "integrity": "sha512-yn/jMdHRw+q2ZJhFhyqsmANcbF6V2QwmD84c6xRau+QpQOmtrBCoRGdvTfeuFDYXB5W2m6MfLkjkvQa9lUSmIA==", + "dev": true, + "dependencies": { + "eventemitter-asyncresource": "^1.0.0", + "hdr-histogram-js": "^2.0.1", + "hdr-histogram-percentiles-obj": "^3.0.0" + }, + "optionalDependencies": { + "nice-napi": "^1.0.2" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/postcss": { + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.6", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-loader": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/postcss-loader/-/postcss-loader-7.0.2.tgz", + "integrity": "sha512-fUJzV/QH7NXUAqV8dWJ9Lg4aTkDCezpTS5HgJ2DvqznexTbSTxgi/dTECvTZ15BwKTtk8G/bqI/QTu2HPd3ZCg==", + "dev": true, + "dependencies": { + "cosmiconfig": "^7.0.0", + "klona": "^2.0.5", + "semver": "^7.3.8" + }, + "engines": { + "node": ">= 14.15.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "postcss": "^7.0.0 || ^8.0.1", + "webpack": "^5.0.0" + } + }, + "node_modules/postcss-modules-extract-imports": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.1.0.tgz", + "integrity": "sha512-k3kNe0aNFQDAZGbin48pL2VNidTF0w4/eASDsxlyspobzU3wZQLOGj7L9gfRe0Jo9/4uud09DsjFNH7winGv8Q==", + "dev": true, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-local-by-default": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.5.tgz", + "integrity": "sha512-6MieY7sIfTK0hYfafw1OMEG+2bg8Q1ocHCpoWLqOKj3JXlKu4G7btkmM/B7lFubYkYWmRSPLZi5chid63ZaZYw==", + "dev": true, + "dependencies": { + "icss-utils": "^5.0.0", + "postcss-selector-parser": "^6.0.2", + "postcss-value-parser": "^4.1.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-scope": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.2.0.tgz", + "integrity": "sha512-oq+g1ssrsZOsx9M96c5w8laRmvEu9C3adDSjI8oTcbfkrTE8hx/zfyobUoWIxaKPO8bt6S62kxpw5GqypEw1QQ==", + "dev": true, + "dependencies": { + "postcss-selector-parser": "^6.0.4" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-values": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz", + "integrity": "sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==", + "dev": true, + "dependencies": { + "icss-utils": "^5.0.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.0.tgz", + "integrity": "sha512-UMz42UD0UY0EApS0ZL9o1XnLhSTtvvvLe5Dc2H2O56fvRZi+KulDyf5ctDhhtYJBGKStV2FL1fy6253cmLgqVQ==", + "dev": true, + "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==", + "dev": true + }, + "node_modules/pretty-bytes": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz", + "integrity": "sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==", + "dev": true, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/prismjs": { + "version": "1.29.0", + "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.29.0.tgz", + "integrity": "sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q==", + "engines": { + "node": ">=6" + } + }, + "node_modules/proc-log": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-3.0.0.tgz", + "integrity": "sha512-++Vn7NS4Xf9NacaU9Xq3URUuqZETPsf8L4j5/ckhaRYsfPeRyzGw+iDjFhV/Jr3uNmTvvddEJFWh5R1gRgUH8A==", + "dev": true, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true + }, + "node_modules/promise-inflight": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", + "integrity": "sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==", + "dev": true + }, + "node_modules/promise-retry": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", + "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", + "dev": true, + "dependencies": { + "err-code": "^2.0.2", + "retry": "^0.12.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/protobufjs": { + "version": "6.11.4", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.11.4.tgz", + "integrity": "sha512-5kQWPaJHi1WoCpjTGszzQ32PG2F4+wRY6BmAT4Vfw56Q2FZ4YZzK20xUYQH4YkfehY1e6QSICrJquM6xXZNcrw==", + "hasInstallScript": true, + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/long": "^4.0.1", + "@types/node": ">=13.7.0", + "long": "^4.0.0" + }, + "bin": { + "pbjs": "bin/pbjs", + "pbts": "bin/pbts" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dev": true, + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/proxy-addr/node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "dev": true, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/prr": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", + "integrity": "sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw==", + "dev": true, + "optional": true + }, + "node_modules/punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==", + "dev": true + }, + "node_modules/qjobs": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/qjobs/-/qjobs-1.2.0.tgz", + "integrity": "sha512-8YOJEHtxpySA3fFDyCRxA+UUV+fA+rTWnuWvylOK/NCjhY+b4ocCtmu8TtsWb+mYeU+GCHf/S66KZF/AsteKHg==", + "dev": true, + "engines": { + "node": ">=0.9" + } + }, + "node_modules/qs": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", + "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "dev": true, + "dependencies": { + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "dev": true, + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/read-package-json": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/read-package-json/-/read-package-json-6.0.4.tgz", + "integrity": "sha512-AEtWXYfopBj2z5N5PbkAOeNHRPUg5q+Nen7QLxV8M2zJq1ym6/lCz3fYNTCXe19puu2d06jfHhrP7v/S2PtMMw==", + "deprecated": "This package is no longer supported. Please use @npmcli/package-json instead.", + "dev": true, + "dependencies": { + "glob": "^10.2.2", + "json-parse-even-better-errors": "^3.0.0", + "normalize-package-data": "^5.0.0", + "npm-normalize-package-bin": "^3.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/read-package-json-fast": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/read-package-json-fast/-/read-package-json-fast-3.0.2.tgz", + "integrity": "sha512-0J+Msgym3vrLOUB3hzQCuZHII0xkNGCtz/HJH9xZshwv9DbDwkw1KaE3gx/e2J5rpEY5rtOy6cyhKOPrkP7FZw==", + "dev": true, + "dependencies": { + "json-parse-even-better-errors": "^3.0.0", + "npm-normalize-package-bin": "^3.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/read-package-json-fast/node_modules/json-parse-even-better-errors": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-3.0.2.tgz", + "integrity": "sha512-fi0NG4bPjCHunUJffmLd0gxssIgkNmArMvis4iNah6Owg1MCJjWhEcDLmsK6iGkJq3tHwbDkTlce70/tmXN4cQ==", + "dev": true, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/read-package-json/node_modules/glob": { + "version": "10.4.2", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.2.tgz", + "integrity": "sha512-GwMlUF6PkPo3Gk21UxkCohOv0PLcIXVtKyLlpEI28R/cO/4eNOdmLk3CMW1wROV/WR/EsZOWAfBbBOqYvs88/w==", + "dev": true, + "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" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/read-package-json/node_modules/json-parse-even-better-errors": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-3.0.2.tgz", + "integrity": "sha512-fi0NG4bPjCHunUJffmLd0gxssIgkNmArMvis4iNah6Owg1MCJjWhEcDLmsK6iGkJq3tHwbDkTlce70/tmXN4cQ==", + "dev": true, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/read-package-json/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/read-package-json/node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "devOptional": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/reflect-metadata": { + "version": "0.1.14", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.14.tgz", + "integrity": "sha512-ZhYeb6nRaXCfhnndflDK8qI6ZQ/YcWZCISRAWICW9XYqMUwjZM9Z0DveWX/ABN01oxSHwVxKQmxeYZSsm0jh5A==", + "dev": true + }, + "node_modules/regenerate": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", + "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==", + "dev": true + }, + "node_modules/regenerate-unicode-properties": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.1.1.tgz", + "integrity": "sha512-X007RyZLsCJVVrjgEFVpLUTZwyOZk3oiL75ZcuYjlIWd6rNJtOjkBwQc5AsRrpbKVkxN6sklw/k/9m2jJYOf8Q==", + "dev": true, + "dependencies": { + "regenerate": "^1.4.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/regenerator-runtime": { + "version": "0.13.11", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", + "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==", + "dev": true + }, + "node_modules/regenerator-transform": { + "version": "0.15.2", + "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.15.2.tgz", + "integrity": "sha512-hfMp2BoF0qOk3uc5V20ALGDS2ddjQaLrdl7xrGXvAIow7qeWRM2VA2HuCHkUKk9slq3VwEwLNK3DFBqDfPGYtg==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.8.4" + } + }, + "node_modules/regex-parser": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/regex-parser/-/regex-parser-2.3.0.tgz", + "integrity": "sha512-TVILVSz2jY5D47F4mA4MppkBrafEaiUWJO/TcZHEIuI13AqoZMkK1WMA4Om1YkYbTx+9Ki1/tSUXbceyr9saRg==", + "dev": true + }, + "node_modules/regexpu-core": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-5.3.2.tgz", + "integrity": "sha512-RAM5FlZz+Lhmo7db9L298p2vHP5ZywrVXmVXpmAD9GuL5MPH6t9ROw1iA/wfHkQ76Qe7AaPF0nGuim96/IrQMQ==", + "dev": true, + "dependencies": { + "@babel/regjsgen": "^0.8.0", + "regenerate": "^1.4.2", + "regenerate-unicode-properties": "^10.1.0", + "regjsparser": "^0.9.1", + "unicode-match-property-ecmascript": "^2.0.0", + "unicode-match-property-value-ecmascript": "^2.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/regjsparser": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.9.1.tgz", + "integrity": "sha512-dQUtn90WanSNl+7mQKcXAgZxvUe7Z0SqXlgzv0za4LwiUhyzBC58yQO3liFoUgu8GiJVInAhJjkj1N0EtQ5nkQ==", + "dev": true, + "dependencies": { + "jsesc": "~0.5.0" + }, + "bin": { + "regjsparser": "bin/parser" + } + }, + "node_modules/regjsparser/node_modules/jsesc": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", + "integrity": "sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==", + "dev": true, + "bin": { + "jsesc": "bin/jsesc" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "engines": { + "node": ">=0.10.0" + } + }, + "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==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "dev": true + }, + "node_modules/resolve": { + "version": "1.22.1", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz", + "integrity": "sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==", + "dev": true, + "dependencies": { + "is-core-module": "^2.9.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-url-loader": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-url-loader/-/resolve-url-loader-5.0.0.tgz", + "integrity": "sha512-uZtduh8/8srhBoMx//5bwqjQ+rfYOUq8zC9NrMUGtjBiGTtFJM42s58/36+hTqeqINcnYe08Nj3LkK9lW4N8Xg==", + "dev": true, + "dependencies": { + "adjust-sourcemap-loader": "^4.0.0", + "convert-source-map": "^1.7.0", + "loader-utils": "^2.0.0", + "postcss": "^8.2.14", + "source-map": "0.6.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/resolve-url-loader/node_modules/loader-utils": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", + "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", + "dev": true, + "dependencies": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" + }, + "engines": { + "node": ">=8.9.0" + } + }, + "node_modules/resolve-url-loader/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "dev": true + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/rimraf/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/robust-predicates": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.2.tgz", + "integrity": "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==", + "optional": true + }, + "node_modules/run-async": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", + "integrity": "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/rw": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz", + "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==", + "optional": true + }, + "node_modules/rxfire": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/rxfire/-/rxfire-6.0.3.tgz", + "integrity": "sha512-77nkyffHh7jgfi1YA/N9RI+kWxYpgKk6GRML1lyersvaqbJt4hkvWwk1rWib9Rb5Lr5mT+Ha45lu7nM79sJCZA==", + "dependencies": { + "tslib": "^1.9.0 || ~2.1.0" + }, + "peerDependencies": { + "firebase": "^9.0.0", + "rxjs": "^6.0.0 || ^7.0.0" + } + }, + "node_modules/rxfire/node_modules/tslib": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.1.0.tgz", + "integrity": "sha512-hcVC3wYEziELGGmEEXue7D75zbwIIVUMWAVbHItGPx0ziyXxrOMQx4rQEVEV45Ut/1IotuEvwqPopzIOkDMf0A==" + }, + "node_modules/rxjs": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/safe-stable-stringify": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.4.3.tgz", + "integrity": "sha512-e2bDA2WJT0wxseVd4lsDP4+3ONX6HpMXQa1ZhFQ7SU+GjvORCmShbCMltrtIDfkYhVHrOcPtj+KhmDBdPdZD1g==", + "engines": { + "node": ">=10" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "node_modules/safevalues": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/safevalues/-/safevalues-0.3.4.tgz", + "integrity": "sha512-LRneZZRXNgjzwG4bDQdOTSbze3fHm1EAKN/8bePxnlEZiBmkYEDggaHbuvHI9/hoqHbGfsEA7tWS9GhYHZBBsw==" + }, + "node_modules/sass": { + "version": "1.58.1", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.58.1.tgz", + "integrity": "sha512-bnINi6nPXbP1XNRaranMFEBZWUfdW/AF16Ql5+ypRxfTvCRTTKrLsMIakyDcayUt2t/RZotmL4kgJwNH5xO+bg==", + "dev": true, + "dependencies": { + "chokidar": ">=3.0.0 <4.0.0", + "immutable": "^4.0.0", + "source-map-js": ">=0.6.2 <2.0.0" + }, + "bin": { + "sass": "sass.js" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/sass-loader": { + "version": "13.2.0", + "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-13.2.0.tgz", + "integrity": "sha512-JWEp48djQA4nbZxmgC02/Wh0eroSUutulROUusYJO9P9zltRbNN80JCBHqRGzjd4cmZCa/r88xgfkjGD0TXsHg==", + "dev": true, + "dependencies": { + "klona": "^2.0.4", + "neo-async": "^2.6.2" + }, + "engines": { + "node": ">= 14.15.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "fibers": ">= 3.1.0", + "node-sass": "^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0", + "sass": "^1.3.0", + "sass-embedded": "*", + "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "fibers": { + "optional": true + }, + "node-sass": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + } + } + }, + "node_modules/sax": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz", + "integrity": "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==", + "dev": true, + "optional": true + }, + "node_modules/schema-utils": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.2.0.tgz", + "integrity": "sha512-L0jRsrPpjdckP3oPug3/VxNKt2trR8TcabrM6FOAAlvC/9Phcmm+cuAgTlxBqdBR1WJx7Naj9WHw+aOmheSVbw==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/select": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/select/-/select-1.1.2.tgz", + "integrity": "sha512-OwpTSOfy6xSs1+pwcNrv0RBMOzI39Lp3qQKUTPVVPRjCdNa5JH/oPRiqsesIskK8TVgmRiHwO4KXlV2Li9dANA==" + }, + "node_modules/select-hose": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz", + "integrity": "sha512-mEugaLK+YfkijB4fx0e6kImuJdCIt2LxCRcbEYPqRGCs4F2ogyfZU5IAZRdjCP8JPq2AtdNoC/Dux63d9Kiryg==", + "dev": true + }, + "node_modules/selfsigned": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-2.4.1.tgz", + "integrity": "sha512-th5B4L2U+eGLq1TVh7zNRGBapioSORUeymIydxgFpwww9d2qyKvtuPU2jJuHvYAwwqi2Y596QBL3eEqcPEYL8Q==", + "dev": true, + "dependencies": { + "@types/node-forge": "^1.3.0", + "node-forge": "^1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/semver": { + "version": "7.5.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.3.tgz", + "integrity": "sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ==", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/semver/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/semver/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + }, + "node_modules/send": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", + "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "dev": true, + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/send/node_modules/debug/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "node_modules/send/node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true, + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/send/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==", + "dev": true + }, + "node_modules/send/node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/serialize-javascript": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/serve-index": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/serve-index/-/serve-index-1.9.1.tgz", + "integrity": "sha512-pXHfKNP4qujrtteMrSBb0rc8HJ9Ms/GrXwcUtUtD5s4ewDJI8bT3Cz2zTVRMKtri49pLx2e0Ya8ziP5Ya2pZZw==", + "dev": true, + "dependencies": { + "accepts": "~1.3.4", + "batch": "0.6.1", + "debug": "2.6.9", + "escape-html": "~1.0.3", + "http-errors": "~1.6.2", + "mime-types": "~2.1.17", + "parseurl": "~1.3.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/serve-index/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/serve-index/node_modules/depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/serve-index/node_modules/http-errors": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", + "integrity": "sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A==", + "dev": true, + "dependencies": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.0", + "statuses": ">= 1.4.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/serve-index/node_modules/inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==", + "dev": true + }, + "node_modules/serve-index/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "node_modules/serve-index/node_modules/setprototypeof": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", + "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==", + "dev": true + }, + "node_modules/serve-static": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", + "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", + "dev": true, + "dependencies": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.18.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "dev": true + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "dev": true + }, + "node_modules/shallow-clone": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", + "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", + "dev": true, + "dependencies": { + "kind-of": "^6.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", + "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", + "object-inspect": "^1.13.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==" + }, + "node_modules/simple-swizzle": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", + "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", + "dependencies": { + "is-arrayish": "^0.3.1" + } + }, + "node_modules/simple-swizzle/node_modules/is-arrayish": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", + "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==" + }, + "node_modules/slash": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-4.0.0.tgz", + "integrity": "sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "dev": true, + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socket.io": { + "version": "4.7.5", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.7.5.tgz", + "integrity": "sha512-DmeAkF6cwM9jSfmp6Dr/5/mfMwb5Z5qRrSXLpo3Fq5SqyU8CMF15jIN4ZhfSwu35ksM1qmHZDQ/DK5XTccSTvA==", + "dev": true, + "dependencies": { + "accepts": "~1.3.4", + "base64id": "~2.0.0", + "cors": "~2.8.5", + "debug": "~4.3.2", + "engine.io": "~6.5.2", + "socket.io-adapter": "~2.5.2", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/socket.io-adapter": { + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.5.tgz", + "integrity": "sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg==", + "dev": true, + "dependencies": { + "debug": "~4.3.4", + "ws": "~8.17.1" + } + }, + "node_modules/socket.io-parser": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", + "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==", + "dev": true, + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/sockjs": { + "version": "0.3.24", + "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.24.tgz", + "integrity": "sha512-GJgLTZ7vYb/JtPSSZ10hsOYIvEYsjbNU+zPdIHcUaWVNUEPivzxku31865sSSud0Da0W4lEeOPlmw93zLQchuQ==", + "dev": true, + "dependencies": { + "faye-websocket": "^0.11.3", + "uuid": "^8.3.2", + "websocket-driver": "^0.7.4" + } + }, + "node_modules/sockjs/node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "dev": true, + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/socks": { + "version": "2.8.3", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.3.tgz", + "integrity": "sha512-l5x7VUUWbjVFbafGLxPWkYsHIhEvmF85tbIeFZWc8ZPtoMyybuEhL7Jye/ooC4/d48FgOjSJXgsF/AJPYCW8Zw==", + "dev": true, + "dependencies": { + "ip-address": "^9.0.5", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-7.0.0.tgz", + "integrity": "sha512-Fgl0YPZ902wEsAyiQ+idGd1A7rSFx/ayC1CQVMw5P+EQx2V0SgpGtf6OKFhVjPflPUl9YMmEOnmfjCdMUsygww==", + "dev": true, + "dependencies": { + "agent-base": "^6.0.2", + "debug": "^4.3.3", + "socks": "^2.6.2" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/source-map": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", + "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", + "engines": { + "node": ">= 8" + } + }, + "node_modules/source-map-js": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", + "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-loader": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/source-map-loader/-/source-map-loader-4.0.1.tgz", + "integrity": "sha512-oqXpzDIByKONVY8g1NUPOTQhe0UTU5bWUl32GSkqK2LjJj0HmwTMVKxcUip0RgAYhY1mqgOxjbQM48a0mmeNfA==", + "dev": true, + "dependencies": { + "abab": "^2.0.6", + "iconv-lite": "^0.6.3", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": ">= 14.15.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.72.1" + } + }, + "node_modules/source-map-loader/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/source-map-support/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/spdx-correct": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", + "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", + "dev": true, + "dependencies": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-exceptions": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", + "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==", + "dev": true + }, + "node_modules/spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "dev": true, + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-license-ids": { + "version": "3.0.18", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.18.tgz", + "integrity": "sha512-xxRs31BqRYHwiMzudOrpSiHtZ8i/GeionCBDSilhYRj+9gIcI8wCZTlXZKu9vZIVqViP3dcp9qE5G6AlIaD+TQ==", + "dev": true + }, + "node_modules/spdy": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/spdy/-/spdy-4.0.2.tgz", + "integrity": "sha512-r46gZQZQV+Kl9oItvl1JZZqJKGr+oEkB08A6BzkiR7593/7IbtuncXHd2YoYeTsG4157ZssMu9KYvUHLcjcDoA==", + "dev": true, + "dependencies": { + "debug": "^4.1.0", + "handle-thing": "^2.0.0", + "http-deceiver": "^1.2.7", + "select-hose": "^2.0.0", + "spdy-transport": "^3.0.0" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/spdy-transport": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/spdy-transport/-/spdy-transport-3.0.0.tgz", + "integrity": "sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw==", + "dev": true, + "dependencies": { + "debug": "^4.1.0", + "detect-node": "^2.0.4", + "hpack.js": "^2.1.6", + "obuf": "^1.1.2", + "readable-stream": "^3.0.6", + "wbuf": "^1.7.3" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true + }, + "node_modules/ssri": { + "version": "10.0.6", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-10.0.6.tgz", + "integrity": "sha512-MGrFH9Z4NP9Iyhqn16sDtBpRRNJ0Y2hNa6D65h736fVSaPCHr4DM4sWUNvVaSuC+0OBGhwsrydQwmgfg5LncqQ==", + "dev": true, + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/ssri/node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/stack-trace": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", + "integrity": "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==", + "engines": { + "node": "*" + } + }, + "node_modules/statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/streamroller": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/streamroller/-/streamroller-3.1.5.tgz", + "integrity": "sha512-KFxaM7XT+irxvdqSP1LGLgNWbYN7ay5owZ3r/8t77p+EtSUAfUgtl7be3xtqtOmGUl9K9YPO2ca8133RlTjvKw==", + "dev": true, + "dependencies": { + "date-format": "^4.0.14", + "debug": "^4.3.4", + "fs-extra": "^8.1.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/stylis": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.2.tgz", + "integrity": "sha512-bhtUjWd/z6ltJiQwg0dUfxEJ+W+jdqQd8TbWLWyeIJHlnsqmGLRFFd8e5mA0AZi/zx90smXRlN66YMTcaSFifg==", + "optional": true + }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/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==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/symbol-observable": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-4.0.0.tgz", + "integrity": "sha512-b19dMThMV4HVFynSAM1++gBHAbk2Tc/osgLIBZMKsyqh34jb2e8Os7T6ZW/Bt3pJFdBTd2JwAnAAEQV7rSNvcQ==", + "dev": true, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/tapable": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", + "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", + "engines": { + "node": ">=6" + } + }, + "node_modules/tar": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "dev": true, + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/tar/node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "dev": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/tar/node_modules/fs-minipass/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tar/node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/tar/node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true, + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/tar/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/terser": { + "version": "5.16.3", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.16.3.tgz", + "integrity": "sha512-v8wWLaS/xt3nE9dgKEWhNUFP6q4kngO5B8eYFUuebsu7Dw/UNAnpUod6UHo04jSSkv8TzKHjZDSd7EXdDQAl8Q==", + "dev": true, + "dependencies": { + "@jridgewell/source-map": "^0.3.2", + "acorn": "^8.5.0", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser-webpack-plugin": { + "version": "5.3.10", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.10.tgz", + "integrity": "sha512-BKFPWlPDndPs+NGGCr1U59t0XScL5317Y0UReNrHaw9/FwhPENlq6bfgs+4yPfyP51vqC1bQ4rp1EfXW5ZSH9w==", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.20", + "jest-worker": "^27.4.5", + "schema-utils": "^3.1.1", + "serialize-javascript": "^6.0.1", + "terser": "^5.26.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "uglify-js": { + "optional": true + } + } + }, + "node_modules/terser-webpack-plugin/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "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/terser-webpack-plugin/node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "node_modules/terser-webpack-plugin/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==" + }, + "node_modules/terser-webpack-plugin/node_modules/schema-utils": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/terser-webpack-plugin/node_modules/terser": { + "version": "5.31.1", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.31.1.tgz", + "integrity": "sha512-37upzU1+viGvuFtBo9NPufCb9dwM0+l9hMxYyWfBA+fbwrPqNJAhbZ6W47bBFnZHKHTUBnMvi87434qq+qnxOg==", + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.8.2", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/test-exclude/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/test-exclude/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/test-exclude/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/text-hex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", + "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==" + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true + }, + "node_modules/through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==" + }, + "node_modules/thunky": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz", + "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==", + "dev": true + }, + "node_modules/tiny-emitter": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/tiny-emitter/-/tiny-emitter-2.1.0.tgz", + "integrity": "sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q==" + }, + "node_modules/tmp": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "dependencies": { + "os-tmpdir": "~1.0.2" + }, + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "devOptional": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "dev": true, + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "bin": { + "tree-kill": "cli.js" + } + }, + "node_modules/triple-beam": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.4.1.tgz", + "integrity": "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==", + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/ts-dedent": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/ts-dedent/-/ts-dedent-2.2.0.tgz", + "integrity": "sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==", + "optional": true, + "engines": { + "node": ">=6.10" + } + }, + "node_modules/tslib": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", + "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==" + }, + "node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dev": true, + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typed-assert": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/typed-assert/-/typed-assert-1.0.9.tgz", + "integrity": "sha512-KNNZtayBCtmnNmbo5mG47p1XsCyrx6iVqomjcZnec/1Y5GGARaxPs6r49RnSPeUP3YjNYiU9sQHAtY4BBvnZwg==", + "dev": true + }, + "node_modules/typescript": { + "version": "4.9.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", + "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + }, + "node_modules/ua-parser-js": { + "version": "0.7.38", + "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.38.tgz", + "integrity": "sha512-fYmIy7fKTSFAhG3fuPlubeGaMoAd6r0rSnfEsO5nEY55i26KSLt9EH7PLQiiqPUhNqYIJvSkTy1oArIcXAbPbA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/ua-parser-js" + }, + { + "type": "paypal", + "url": "https://paypal.me/faisalman" + }, + { + "type": "github", + "url": "https://github.com/sponsors/faisalman" + } + ], + "engines": { + "node": "*" + } + }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" + }, + "node_modules/unicode-canonical-property-names-ecmascript": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz", + "integrity": "sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-ecmascript": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz", + "integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==", + "dev": true, + "dependencies": { + "unicode-canonical-property-names-ecmascript": "^2.0.0", + "unicode-property-aliases-ecmascript": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-value-ecmascript": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.1.0.tgz", + "integrity": "sha512-qxkjQt6qjg/mYscYMC0XKRn3Rh0wFPlfxB0xkt9CfyTvpX1Ra0+rAmdX2QyAobptSEvuy4RtpPRui6XkV+8wjA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-property-aliases-ecmascript": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz", + "integrity": "sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/unique-filename": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-3.0.0.tgz", + "integrity": "sha512-afXhuC55wkAmZ0P18QsVE6kp8JaxrEokN2HGIoIVv2ijHQd419H0+6EigAFcIzXeMIkcIkNBpB3L/DXB3cTS/g==", + "dev": true, + "dependencies": { + "unique-slug": "^4.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/unique-slug": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-4.0.0.tgz", + "integrity": "sha512-WrcA6AyEfqDX5bWige/4NQfPZMtASNVxdmWR76WESYQVAACSgWcR6e9i0mofqqBxYFtL4oAxPIptY73/0YE1DQ==", + "dev": true, + "dependencies": { + "imurmurhash": "^0.1.4" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.0.tgz", + "integrity": "sha512-EdRAaAyk2cUE1wOf2DkEhzxqOQvFOoRJFNS6NeyJ01Gp2beMRpBAINjM2iDXE3KCuKhwnvHIQCJm6ThL2Z+HzQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "escalade": "^3.1.2", + "picocolors": "^1.0.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/uri-js/node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "engines": { + "node": ">=6" + } + }, + "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==" + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "dev": true, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "dev": true, + "dependencies": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "node_modules/validate-npm-package-name": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-5.0.1.tgz", + "integrity": "sha512-OljLrQ9SQdOUqTaQxqL5dEfZWrXExyyWsozYlAWFawPVNuD83igl7uJD2RTkNMbniIYgt8l81eCJGIdQF7avLQ==", + "dev": true, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/void-elements": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-2.0.1.tgz", + "integrity": "sha512-qZKX4RnBzH2ugr8Lxa7x+0V6XD9Sb/ouARtiasEQCHB1EVU4NXtmHsDDrx1dO4ne5fc3J6EW05BP1Dl0z0iung==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/watchpack": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.1.tgz", + "integrity": "sha512-8wrBCMtVhqcXP2Sup1ctSkga6uc2Bx0IIvKyT7yTFier5AXHooSI+QyQQAtTb7+E0IUCCKyTFmXqdqgum2XWGg==", + "dependencies": { + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/wbuf": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/wbuf/-/wbuf-1.7.3.tgz", + "integrity": "sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA==", + "dev": true, + "dependencies": { + "minimalistic-assert": "^1.0.0" + } + }, + "node_modules/wcwidth": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", + "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==", + "dependencies": { + "defaults": "^1.0.3" + } + }, + "node_modules/web-worker": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/web-worker/-/web-worker-1.3.0.tgz", + "integrity": "sha512-BSR9wyRsy/KOValMgd5kMyr3JzpdeoR9KVId8u5GVlTTAtNChlsE4yTxeY7zMdNSyOmoKBv8NH2qeRY9Tg+IaA==", + "optional": true + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + }, + "node_modules/webpack": { + "version": "5.76.1", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.76.1.tgz", + "integrity": "sha512-4+YIK4Abzv8172/SGqObnUjaIHjLEuUasz9EwQj/9xmPPkYJy2Mh03Q/lJfSD3YLzbxy5FeTq5Uw0323Oh6SJQ==", + "dependencies": { + "@types/eslint-scope": "^3.7.3", + "@types/estree": "^0.0.51", + "@webassemblyjs/ast": "1.11.1", + "@webassemblyjs/wasm-edit": "1.11.1", + "@webassemblyjs/wasm-parser": "1.11.1", + "acorn": "^8.7.1", + "acorn-import-assertions": "^1.7.6", + "browserslist": "^4.14.5", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.10.0", + "es-module-lexer": "^0.9.0", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.2.9", + "json-parse-even-better-errors": "^2.3.1", + "loader-runner": "^4.2.0", + "mime-types": "^2.1.27", + "neo-async": "^2.6.2", + "schema-utils": "^3.1.0", + "tapable": "^2.1.1", + "terser-webpack-plugin": "^5.1.3", + "watchpack": "^2.4.0", + "webpack-sources": "^3.2.3" + }, + "bin": { + "webpack": "bin/webpack.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-dev-middleware": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-6.1.2.tgz", + "integrity": "sha512-Wu+EHmX326YPYUpQLKmKbTyZZJIB8/n6R09pTmB03kJmnMsVPTo9COzHZFr01txwaCAuZvfBJE4ZCHRcKs5JaQ==", + "dev": true, + "dependencies": { + "colorette": "^2.0.10", + "memfs": "^3.4.12", + "mime-types": "^2.1.31", + "range-parser": "^1.2.1", + "schema-utils": "^4.0.0" + }, + "engines": { + "node": ">= 14.15.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "webpack": { + "optional": true + } + } + }, + "node_modules/webpack-dev-server": { + "version": "4.11.1", + "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-4.11.1.tgz", + "integrity": "sha512-lILVz9tAUy1zGFwieuaQtYiadImb5M3d+H+L1zDYalYoDl0cksAB1UNyuE5MMWJrG6zR1tXkCP2fitl7yoUJiw==", + "dev": true, + "dependencies": { + "@types/bonjour": "^3.5.9", + "@types/connect-history-api-fallback": "^1.3.5", + "@types/express": "^4.17.13", + "@types/serve-index": "^1.9.1", + "@types/serve-static": "^1.13.10", + "@types/sockjs": "^0.3.33", + "@types/ws": "^8.5.1", + "ansi-html-community": "^0.0.8", + "bonjour-service": "^1.0.11", + "chokidar": "^3.5.3", + "colorette": "^2.0.10", + "compression": "^1.7.4", + "connect-history-api-fallback": "^2.0.0", + "default-gateway": "^6.0.3", + "express": "^4.17.3", + "graceful-fs": "^4.2.6", + "html-entities": "^2.3.2", + "http-proxy-middleware": "^2.0.3", + "ipaddr.js": "^2.0.1", + "open": "^8.0.9", + "p-retry": "^4.5.0", + "rimraf": "^3.0.2", + "schema-utils": "^4.0.0", + "selfsigned": "^2.1.1", + "serve-index": "^1.9.1", + "sockjs": "^0.3.24", + "spdy": "^4.0.2", + "webpack-dev-middleware": "^5.3.1", + "ws": "^8.4.2" + }, + "bin": { + "webpack-dev-server": "bin/webpack-dev-server.js" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^4.37.0 || ^5.0.0" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-dev-server/node_modules/webpack-dev-middleware": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-5.3.4.tgz", + "integrity": "sha512-BVdTqhhs+0IfoeAf7EoH5WE+exCmqGerHfDM0IL096Px60Tq2Mn9MAbnaGUe6HiMa41KMCYF19gyzZmBcq/o4Q==", + "dev": true, + "dependencies": { + "colorette": "^2.0.10", + "memfs": "^3.4.3", + "mime-types": "^2.1.31", + "range-parser": "^1.2.1", + "schema-utils": "^4.0.0" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^4.0.0 || ^5.0.0" + } + }, + "node_modules/webpack-merge": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.8.0.tgz", + "integrity": "sha512-/SaI7xY0831XwP6kzuwhKWVKDP9t1QY1h65lAFLbZqMPIuYcD9QAW4u9STIbU9kaJbPBB/geU/gLr1wDjOhQ+Q==", + "dev": true, + "dependencies": { + "clone-deep": "^4.0.1", + "wildcard": "^2.0.0" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/webpack-sources": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz", + "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webpack-subresource-integrity": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/webpack-subresource-integrity/-/webpack-subresource-integrity-5.1.0.tgz", + "integrity": "sha512-sacXoX+xd8r4WKsy9MvH/q/vBtEHr86cpImXwyg74pFIpERKt6FmB8cXpeuh0ZLgclOlHI4Wcll7+R5L02xk9Q==", + "dev": true, + "dependencies": { + "typed-assert": "^1.0.8" + }, + "engines": { + "node": ">= 12" + }, + "peerDependencies": { + "html-webpack-plugin": ">= 5.0.0-beta.1 < 6", + "webpack": "^5.12.0" + }, + "peerDependenciesMeta": { + "html-webpack-plugin": { + "optional": true + } + } + }, + "node_modules/webpack/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "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/webpack/node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "node_modules/webpack/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==" + }, + "node_modules/webpack/node_modules/schema-utils": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/websocket-driver": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", + "integrity": "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==", + "dependencies": { + "http-parser-js": ">=0.5.1", + "safe-buffer": ">=5.1.0", + "websocket-extensions": ">=0.1.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/websocket-extensions": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz", + "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, + "node_modules/wide-align": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", + "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", + "dev": true, + "dependencies": { + "string-width": "^1.0.2 || 2 || 3 || 4" + } + }, + "node_modules/wildcard": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.1.tgz", + "integrity": "sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==", + "dev": true + }, + "node_modules/winston": { + "version": "3.13.0", + "resolved": "https://registry.npmjs.org/winston/-/winston-3.13.0.tgz", + "integrity": "sha512-rwidmA1w3SE4j0E5MuIufFhyJPBDG7Nu71RkZor1p2+qHvJSZ9GYDA81AyleQcZbh/+V6HjeBdfnTZJm9rSeQQ==", + "dependencies": { + "@colors/colors": "^1.6.0", + "@dabh/diagnostics": "^2.0.2", + "async": "^3.2.3", + "is-stream": "^2.0.0", + "logform": "^2.4.0", + "one-time": "^1.0.0", + "readable-stream": "^3.4.0", + "safe-stable-stringify": "^2.3.1", + "stack-trace": "0.0.x", + "triple-beam": "^1.3.0", + "winston-transport": "^4.7.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/winston-transport": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.7.0.tgz", + "integrity": "sha512-ajBj65K5I7denzer2IYW6+2bNIVqLGDHqDw3Ow8Ohh+vdW+rv4MZ6eiDvHoKhfJFZ2auyN8byXieDDJ96ViONg==", + "dependencies": { + "logform": "^2.3.2", + "readable-stream": "^3.6.0", + "triple-beam": "^1.3.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/winston/node_modules/@colors/colors": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.6.0.tgz", + "integrity": "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==", + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "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/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/wrap-ansi/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true + }, + "node_modules/ws": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "dev": true, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true + }, + "node_modules/yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/yargs": { + "version": "17.6.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.6.2.tgz", + "integrity": "sha512-1/9UrdHjDZc0eOU0HxOHoS78C69UD3JRMvzlJ7S79S2nTaWRA/whGCTV8o9e/N/1Va9YIV7Q4sOxD8VV4pCWOw==", + "dev": true, + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "engines": { + "node": ">=12" + } + }, + "node_modules/zone.js": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.12.0.tgz", + "integrity": "sha512-XtC+I5dXU14HrzidAKBNMqneIVUykLEAA1x+v4KVrd6AUPWlwYORF8KgsVqvgdHiKZ4BkxxjvYi/ksEixTPR0Q==", + "dependencies": { + "tslib": "^2.3.0" + } + } + } +} diff --git a/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/package.json b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/package.json new file mode 100644 index 0000000..ad81bde --- /dev/null +++ b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/package.json @@ -0,0 +1,61 @@ +{ + "name": "quick-bot-app-frontend", + "version": "0.0.0", + "scripts": { + "ng": "ng", + "start": "ng serve", + "build:prod": "ng build --configuration production --aot --output-hashing=all", + "build:dev": "ng build --configuration development", + "build:staging": "ng build --configuration staging", + "build": "ng build", + "watch": "ng build --watch --configuration development", + "test": "ng test", + "postinstall": "ngcc", + "post-build": "node ./build/post-build.js" + }, + "private": true, + "dependencies": { + "@angular/animations": "^15.1.0", + "@angular/cdk": "^15.2.9", + "@angular/common": "^15.1.0", + "@angular/compiler": "^15.1.0", + "@angular/core": "^15.1.0", + "@angular/fire": "7.5", + "@angular/flex-layout": "^15.0.0-beta.42", + "@angular/forms": "^15.1.0", + "@angular/material": "^15.2.9", + "@angular/platform-browser": "^15.1.0", + "@angular/platform-browser-dynamic": "^15.1.0", + "@angular/router": "^15.1.0", + "@fortawesome/fontawesome-free": "^6.6.0", + "@ionic/angular": "^7.5.4", + "@ng-idle/core": "^14.0.0", + "clipboard": "^2.0.11", + "ionic-emoji-rating": "^1.0.15", + "marked": "^4.3.0", + "moment": "^2.30.1", + "ngx-markdown": "^15.1.2", + "prismjs": "^1.29.0", + "rxfire": "6.0.3", + "rxjs": "~7.8.0", + "tslib": "^2.3.0", + "uuid": "^9.0.1", + "zone.js": "~0.12.0" + }, + "devDependencies": { + "@angular-devkit/build-angular": "^15.1.3", + "@angular/cli": "~15.1.3", + "@angular/compiler-cli": "^15.1.0", + "@types/jasmine": "~4.3.0", + "@types/marked": "^4.3.0", + "@types/uuid": "^9.0.4", + "firebase-tools": "^11.30.0", + "jasmine-core": "~4.5.0", + "karma": "~6.4.0", + "karma-chrome-launcher": "~3.1.0", + "karma-coverage": "~2.2.0", + "karma-jasmine": "~5.1.0", + "karma-jasmine-html-reporter": "~2.0.0", + "typescript": "~4.9.4" + } +} diff --git a/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/app/app-routing.module.ts b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/app/app-routing.module.ts new file mode 100644 index 0000000..3cf60ed --- /dev/null +++ b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/app/app-routing.module.ts @@ -0,0 +1,35 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { NgModule } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; +import { ChatComponent } from './components/main/chat/chat.component'; +import { MainComponent } from './components/main/main.component'; +import { LoginComponent } from './components/login/login.component'; +import { AuthGuard } from './services/login/auth.guard'; + +const routes: Routes = [ + {path: '', component: MainComponent, canActivate: [AuthGuard]}, + {path: 'login', component: LoginComponent}, + {path: 'chat', component: ChatComponent, canActivate: [AuthGuard]}, +]; + + +@NgModule({ + imports: [RouterModule.forRoot(routes)], + exports: [RouterModule] +}) +export class AppRoutingModule { } diff --git a/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/app/app.component.html b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/app/app.component.html new file mode 100644 index 0000000..3f4eee0 --- /dev/null +++ b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/app/app.component.html @@ -0,0 +1,21 @@ + + + + + + + \ No newline at end of file diff --git a/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/app/app.component.scss b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/app/app.component.scss new file mode 100644 index 0000000..9bf31fd --- /dev/null +++ b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/app/app.component.scss @@ -0,0 +1,98 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +::ng-deep .tour-step { + background: white; +} + +::ng-deep .tour-top{ + margin: -1%; +} + +::ng-deep .tour-left{ + margin-left: -1%; +} + +::ng-deep .tour-top-right{ + margin-top: -1%; + +} + +::ng-deep .tour-bottom-right{ + margin-top: 1%; +} + +::ng-deep .tour-bottom-left{ + margin-top: 1%; +} + +::ng-deep .tour-bottom{ + margin: 1%; +} + +::ng-deep ngx-guided-tour .tour-content{ + font-size: 17px !important; + padding-bottom: 20px !important; +} + +::ng-deep ngx-guided-tour .tour-title{ + font-size: 22px !important; + padding-bottom: 0 !important; +} + +::ng-deep ngx-guided-tour .tour-step .tour-buttons .skip-button{ + font-weight: 500 !important; + font-size: 17px !important; + line-height: 1.7 !important; +} + +::ng-deep ngx-guided-tour .tour-step{ + border-radius: 20px !important; +} + +::ng-deep ngx-guided-tour .tour-step .tour-buttons .back-button{ + font-weight: 500 !important; + font-size: 17px !important; + line-height: 1.7 !important; +} + +::ng-deep ngx-guided-tour .tour-step .tour-buttons .next-button{ + background: #4285F4; + padding-left: 16px !important; + padding-right: 16px !important; + padding-top: 6px !important; + padding-bottom: 6px !important; + font-size: 16px !important; + color: white; + font-weight: 500; + border-radius: 15px !important; +} + +::ng-deep ngx-guided-tour .tour-step.tour-top .tour-block{ + margin-bottom: 0 !important; +} + +::ng-deep ngx-guided-tour .tour-step.tour-bottom-left .tour-block{ + margin-top: 0 !important; +} + +::ng-deep pre{ + background: #485f84 !important; +} + +div { + font-family: "Google Sans", sans-serif !important; +} diff --git a/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/app/app.component.spec.ts b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/app/app.component.spec.ts new file mode 100644 index 0000000..18aa0f8 --- /dev/null +++ b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/app/app.component.spec.ts @@ -0,0 +1,51 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { TestBed } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; +import { AppComponent } from './app.component'; + +describe('AppComponent', () => { + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ + RouterTestingModule + ], + declarations: [ + AppComponent + ], + }).compileComponents(); + }); + + it('should create the app', () => { + const fixture = TestBed.createComponent(AppComponent); + const app = fixture.componentInstance; + expect(app).toBeTruthy(); + }); + + it(`should have as title 'pac-assist-ui'`, () => { + const fixture = TestBed.createComponent(AppComponent); + const app = fixture.componentInstance; + expect(app.title).toEqual('pac-assist-ui'); + }); + + it('should render title', () => { + const fixture = TestBed.createComponent(AppComponent); + fixture.detectChanges(); + const compiled = fixture.nativeElement as HTMLElement; + expect(compiled.querySelector('.content span')?.textContent).toContain('pac-assist-ui app is running!'); + }); +}); diff --git a/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/app/app.component.ts b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/app/app.component.ts new file mode 100644 index 0000000..85bb63d --- /dev/null +++ b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/app/app.component.ts @@ -0,0 +1,85 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Component, ChangeDetectorRef } from '@angular/core'; +import { Router, NavigationEnd, Event as NavigationEvent } from '@angular/router'; +import { UserService } from './services/user/user.service'; +import { Idle, DEFAULT_INTERRUPTSOURCES } from '@ng-idle/core'; +import { MatDialog, MatDialogRef } from '@angular/material/dialog'; +import { DialogueBoxComponent } from './dialogue-box/dialogue-box.component'; +import { AuthService } from './services/login/auth.service'; + +@Component({ + selector: 'app-root', + templateUrl: './app.component.html', + styleUrls: ['./app.component.scss'] +}) +export class AppComponent { + title = 'pac-assist-ui'; + showHeader: boolean = true + userInfo: any; + ref?: MatDialogRef; + + constructor(private router: Router, + private userService: UserService, + private idle: Idle, cd: ChangeDetectorRef, + public authService: AuthService, + private dialog: MatDialog) { + this.router.events.subscribe( + (event: NavigationEvent) => { + if (event instanceof NavigationEnd) { + if (event.url == '/login' || event.url == '/login/e2e' || (event.url.includes('login') && event.url.includes('email')) || (event.url.includes('login') && event.url.includes('tos')) || event.url.includes('reset-password') || event.url.includes('support-ticket')) { + this.showHeader = false + } + else { + this.userInfo = this.userService.getUserDetails(); + this.showHeader = true; + // idle for 2 days then show dialog + idle.setIdle(172800); + idle.setInterrupts(DEFAULT_INTERRUPTSOURCES); + idle.onIdleStart.subscribe(() => { + this.showIdleDialog(); + }); + this.idle.watch(); + } + } + }); + } + + showIdleDialog() { + if(!this.ref && this.showHeader){ + const config = { + title: "Session Idle", + icon: "av_timer", + description: "Session has been inactive for 48 hours, user will be logged out automatically.", + showConfirmBtn: false, + confirmBtnText: "logout", + cancelCallBackFunction: () => { + this.idle.watch(); + }, + needsExtraProcessing: true, + extraProcessingFunction: () => { + setTimeout(() =>{this.ref?.close();this.authService.logout();},20000); + } + }; + + this.ref = this.dialog.open(DialogueBoxComponent, { data: config, disableClose: true }); + } + this.ref?.afterClosed().subscribe(()=>{ + this.ref = undefined; + }); + } +} diff --git a/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/app/app.module.ts b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/app/app.module.ts new file mode 100644 index 0000000..59e3e01 --- /dev/null +++ b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/app/app.module.ts @@ -0,0 +1,163 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { NgModule } from '@angular/core'; +import { BrowserModule } from '@angular/platform-browser'; +import { AppRoutingModule } from './app-routing.module'; +import { AppComponent } from './app.component'; +import { HeaderComponent } from './components/header/header.component'; +import { FooterComponent } from './components/footer/footer.component'; +import { MainComponent } from './components/main/main.component'; +import { ChatComponent } from './components/main/chat/chat.component'; +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; +import { MatIconModule } from '@angular/material/icon'; +import { MatAutocompleteModule } from '@angular/material/autocomplete'; +import { MatButtonModule } from '@angular/material/button'; +import { MatToolbarModule } from '@angular/material/toolbar'; +import { HttpClientModule } from '@angular/common/http'; +import { MatInputModule } from '@angular/material/input'; +import { NgFor, PathLocationStrategy } from '@angular/common'; +import { MatSelectModule } from '@angular/material/select'; +import { MatFormFieldModule } from '@angular/material/form-field'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { MatGridListModule } from '@angular/material/grid-list'; +import { MatCardModule } from '@angular/material/card'; +import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; +import { ChatbarComponent } from './components/main/chat/chatbar/chatbar.component'; +import { MatTooltipModule } from '@angular/material/tooltip'; +import { LoginComponent } from './components/login/login.component'; +import { initializeApp, provideFirebaseApp } from '@angular/fire/app'; +import { + provideAnalytics, + getAnalytics, + ScreenTrackingService, + UserTrackingService, +} from '@angular/fire/analytics'; +import { environment } from '../environments/environment'; +import { provideAuth, getAuth } from '@angular/fire/auth'; +import { MatMenuModule } from '@angular/material/menu'; +import { MatDividerModule } from '@angular/material/divider'; +import { MatChipsModule } from '@angular/material/chips'; +import { LocationStrategy } from '@angular/common'; +import { MarkdownModule } from 'ngx-markdown'; +import { MatDialogModule } from '@angular/material/dialog'; +import { CdkAccordionModule } from '@angular/cdk/accordion'; +import { MatExpansionModule } from '@angular/material/expansion'; +import { IonicRatingModule } from 'ionic-emoji-rating'; +import { MatListModule } from '@angular/material/list'; +import { MatTabsModule } from '@angular/material/tabs'; +import { MatTableModule } from '@angular/material/table'; +import { MatCheckboxModule } from '@angular/material/checkbox'; +import { MatSlideToggleModule } from '@angular/material/slide-toggle'; +import { MatButtonToggleModule } from '@angular/material/button-toggle'; +import { MatSnackBarModule } from '@angular/material/snack-bar'; +import { MatSortModule } from '@angular/material/sort'; +import { MatPaginatorModule } from '@angular/material/paginator'; +import { SuggestionCardComponent } from './components/elements/suggestion-card/suggestion-card.component'; +import { MatSidenavModule } from '@angular/material/sidenav'; +import { NgIdleModule } from '@ng-idle/core'; +import { ClipboardModule } from '@angular/cdk/clipboard'; + +import 'prismjs'; +import 'prismjs/components/prism-typescript.min.js'; +import 'prismjs/plugins/line-numbers/prism-line-numbers.js'; +import 'prismjs/plugins/line-highlight/prism-line-highlight.js'; +import { ToastMessageComponent } from './components/shared/toast-message/toast-message.component'; +import { FlexLayoutModule } from '@angular/flex-layout'; +import { DialogueBoxComponent } from './dialogue-box/dialogue-box.component'; +import { MatSliderModule } from '@angular/material/slider'; +import { MatStepperModule } from '@angular/material/stepper'; +import { FormatAgentNamePipe } from './format-agent-name.pipe'; + +@NgModule({ + declarations: [ + AppComponent, + HeaderComponent, + FooterComponent, + MainComponent, + ChatComponent, + ChatbarComponent, + LoginComponent, + SuggestionCardComponent, + ToastMessageComponent, + DialogueBoxComponent, + FormatAgentNamePipe, + ], + imports: [ + BrowserModule, + AppRoutingModule, + BrowserAnimationsModule, + MatSnackBarModule, + MatPaginatorModule, + MatSortModule, + MatSliderModule, + MatToolbarModule, + MatButtonModule, + MatIconModule, + HttpClientModule, + FormsModule, + MatFormFieldModule, + MatSelectModule, + NgFor, + MatInputModule, + MatGridListModule, + MatCardModule, + MatTooltipModule, + MatProgressSpinnerModule, + MatMenuModule, + MatDividerModule, + MatChipsModule, + ReactiveFormsModule, + MatMenuModule, + MatDialogModule, + CdkAccordionModule, + IonicRatingModule, + MarkdownModule.forRoot(), + MatExpansionModule, + MatListModule, + MatTabsModule, + MatCheckboxModule, + MatTableModule, + MatSlideToggleModule, + MatButtonToggleModule, + MatSidenavModule, + MatAutocompleteModule, + environment.requiredLogin == 'True' + ? [ + provideFirebaseApp(() => initializeApp(environment.firebase)), + provideAuth(() => getAuth()), + ] + : [], + environment.requiredLogin == 'True' + ? [provideAnalytics(() => getAnalytics())] + : [], + FlexLayoutModule, + NgIdleModule.forRoot(), + ClipboardModule, + MatStepperModule, + ], + providers: [ + { provide: LocationStrategy, useClass: PathLocationStrategy }, + environment.requiredLogin == 'True' + ? [ + ScreenTrackingService, // Automatically track screen views + UserTrackingService, // Automatically track user interactions + ] + : [], + ], + bootstrap: [AppComponent], +}) +export class AppModule {} diff --git a/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/app/components/elements/suggestion-card/suggestion-card.component.html b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/app/components/elements/suggestion-card/suggestion-card.component.html new file mode 100644 index 0000000..ea446af --- /dev/null +++ b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/app/components/elements/suggestion-card/suggestion-card.component.html @@ -0,0 +1,34 @@ + + +
    +
    + + + + keyboard_return + {{question}} + + + +
    +
    + +
    +
    \ No newline at end of file diff --git a/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/app/components/elements/suggestion-card/suggestion-card.component.scss b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/app/components/elements/suggestion-card/suggestion-card.component.scss new file mode 100644 index 0000000..05492a6 --- /dev/null +++ b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/app/components/elements/suggestion-card/suggestion-card.component.scss @@ -0,0 +1,95 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +.mat-mdc-standard-chip { + --mdc-chip-label-text-color: #4285F4; + --mdc-chip-elevated-container-color: #fff; + border: 1px solid #4285F4 !important; + font-family: 'Google Sans', sans-serif !important; + font-size: 1em !important; + mat-icon{ + color: #4285F4 !important; + } +} + + +:host ::ng-deep .mdc-evolution-chip__cell { +justify-content: center; +} + +.heading-parent-container{ + display: flex; + flex-direction: row; +} + +.close-button{ + justify-content: flex-end; + display: flex; + width: 6%; + +} + +.intent-card { + padding: 0px; + font-size: 0.95rem; +} + +.intent-heading { + color: rgba(95, 99, 104, 1); + font-size: 0.95rem; + font-style: normal; + font-weight: 400; + line-height: 42px; + font-family: 'Google Sans', sans-serif !important; + width: 20%; + +} + +.chips-container{ + width: 94%; +} + +.intent-container { + border-radius: 8px; + background-color: rgba(246, 249, 254, 1); + width: 100%; + box-shadow: none !important; + flex-shrink: 0; + + .selected-question { + + // margin: 0.5rem 0rem; + &:hover { + background-color: rgba(66, 133, 244, 0.10); + } + } + + .question-item { + display: flex; + justify-content: space-between; + font-family: 'Google Sans', sans-serif !important; + color: #161A1D; + font-size: 0.95rem; + line-height: 24px; + flex-wrap: wrap; + } + +} + +.text-span{ + display: flex !important; + gap: 10px !important; +} \ No newline at end of file diff --git a/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/app/components/elements/suggestion-card/suggestion-card.component.spec.ts b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/app/components/elements/suggestion-card/suggestion-card.component.spec.ts new file mode 100644 index 0000000..01459b6 --- /dev/null +++ b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/app/components/elements/suggestion-card/suggestion-card.component.spec.ts @@ -0,0 +1,39 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { SuggestionCardComponent } from './suggestion-card.component'; + +describe('SuggestionCardComponent', () => { + let component: SuggestionCardComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ SuggestionCardComponent ] + }) + .compileComponents(); + + fixture = TestBed.createComponent(SuggestionCardComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/app/components/elements/suggestion-card/suggestion-card.component.ts b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/app/components/elements/suggestion-card/suggestion-card.component.ts new file mode 100644 index 0000000..6debbb2 --- /dev/null +++ b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/app/components/elements/suggestion-card/suggestion-card.component.ts @@ -0,0 +1,37 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Component, Input, Output, EventEmitter } from '@angular/core'; +import { SuggestionData } from 'src/app/models/messegeType.model'; +@Component({ + selector: 'app-suggestion-card', + templateUrl: './suggestion-card.component.html', + styleUrls: ['./suggestion-card.component.scss'] +}) +export class SuggestionCardComponent { + @Input() data: SuggestionData; + @Output() raiseQuery = new EventEmitter(); + @Output() dismissSuggestionCard = new EventEmitter(); + + + assignQToChatQuery(question: string){ + this.raiseQuery.emit(question); + } + + closeSuggestionCard(){ + this.dismissSuggestionCard.emit(); + } +} diff --git a/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/app/components/footer/footer.component.html b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/app/components/footer/footer.component.html new file mode 100644 index 0000000..0200895 --- /dev/null +++ b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/app/components/footer/footer.component.html @@ -0,0 +1,23 @@ + + +

    + +

    + +

    \ No newline at end of file diff --git a/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/app/components/footer/footer.component.scss b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/app/components/footer/footer.component.scss new file mode 100644 index 0000000..4c78c90 --- /dev/null +++ b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/app/components/footer/footer.component.scss @@ -0,0 +1,71 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +.container { + padding-left: 4rem; + padding-right: 4rem; +} + +.footer { + position: fixed; + bottom: 0; +} + + +.genAI { + background: linear-gradient(86.95deg, #34A853 -65.34%, #FBBC05 -12.75%, #EA4335 25.15%, #1A73E8 103.88%); + -webkit-background-clip: text; + -moz-background-clip: text; + background-clip: text; + color: transparent; +} + +:host ::ng-deep .mat-toolbar.mat-primary { + background-color: transparent; + color: #504747; + box-shadow: none; + justify-content: center; +} + +.line-spacer { + flex: 1 1 auto; +} + +.links-weight { + margin-left: 10px; + margin-right: 10px; + font-size: 14px; + font-weight: 400; + cursor: pointer; +} + +.links-bold { + margin-right: 10px; + color: #5f6368; + font-family: 'Google Sans', sans-serif !important; + .blue{ + color: #4285f4; + } + .red{ + color: #ea4335; + } + .yellow{ + color: #fbbc04; + } + .green{ + color: #34a853; + } +} \ No newline at end of file diff --git a/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/app/components/footer/footer.component.spec.ts b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/app/components/footer/footer.component.spec.ts new file mode 100644 index 0000000..eb41e5f --- /dev/null +++ b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/app/components/footer/footer.component.spec.ts @@ -0,0 +1,39 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { FooterComponent } from './footer.component'; + +describe('FooterComponent', () => { + let component: FooterComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ FooterComponent ] + }) + .compileComponents(); + + fixture = TestBed.createComponent(FooterComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/app/components/footer/footer.component.ts b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/app/components/footer/footer.component.ts new file mode 100644 index 0000000..484263f --- /dev/null +++ b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/app/components/footer/footer.component.ts @@ -0,0 +1,25 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-footer', + templateUrl: './footer.component.html', + styleUrls: ['./footer.component.scss'] +}) +export class FooterComponent { +} diff --git a/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/app/components/header/header.component.html b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/app/components/header/header.component.html new file mode 100644 index 0000000..fd93e6d --- /dev/null +++ b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/app/components/header/header.component.html @@ -0,0 +1,62 @@ + + + +
    +
    + info_outline + New intent is being created! Prepare to be able to chat with me about {{i.name}} +
    +
    diff --git a/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/app/components/header/header.component.scss b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/app/components/header/header.component.scss new file mode 100644 index 0000000..1524596 --- /dev/null +++ b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/app/components/header/header.component.scss @@ -0,0 +1,480 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +.container { + margin: auto; +} + +.logo-text { + font-family: Google Sans,Helvetica Neue,sans-serif; + font-style:normal; + font-size: 22px; + line-height: 128.7%; + letter-spacing: normal; + color: #2d2c2cbf; +} + +.gradient-text { + font-family: Google Sans,Helvetica Neue,sans-serif; + font-style:normal; + font-size: 30px; + line-height: 128.7%; + letter-spacing: normal; + background: linear-gradient(89.9deg, rgba(66, 133, 244, 0.95) 21.17%, #A488F5 44.34%, rgba(234, 67, 53, 0.88) 81.26%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; + text-fill-color: transparent; +} + + +.links-pointer { + cursor: pointer; +} + +.logo-icon { + height: 35px; + width: 125px; + cursor: pointer; +} + +// .profile-container { +// margin-left: 1%; +// font-family: 'Google Sans', sans-serif !important; + +// .circle { +// display: flex; +// justify-content: center; +// width: 40px; +// height: 40px; +// border: 5px solid rgb(255, 255, 255); +// border-radius: 60%; +// overflow: hidden; +// } + +// .circle img { +// width: 100%; +// height: 100%; +// object-fit: cover; +// } +// } + +.card-inside-right { + display: flex; + flex-direction: column; + align-items: flex-start; + color: #3E4245; + font-size: 1rem; + font-weight: 400; + font-family: 'Google Sans', sans-serif !important; + border-radius: 5px; + letter-spacing: 0.4px; + + .container-circle { + width: 100%; + height: 100%; + object-fit: cover; + border-radius: 50px; + padding-top: 4%; + padding-bottom: 4%; + } + + .name { + display: flex; + align-items: center; + font-size: 1rem; + color: #3E4245; + font-weight: 500; + padding: 1.3em 0.4em; + flex-direction: column; + gap: 10px; + background: #DDE3EA !important; + width: -webkit-fill-available;; + .sub-title { + + color: #3E4245; + } + + margin: auto; + } + + .company { + display: flex; + align-items: center; + padding: 0.3em 1em; + width: -webkit-fill-available;; + + .logo { + padding-right: 2%; + color: #5F6368; + } + + .text { + min-width: 100px; + + .sub-title { + font-size: 0.85rem; + margin-bottom: 10px; + color: #3E4245; + } + } + + margin-bottom: 10px; + } + + .logout { + background: #DDE3EA; + border-radius: 20px; + font-size: 1rem; + font-weight: 500; + font-family: 'Google Sans', sans-serif !important; + margin-bottom: 1em; + cursor: pointer; + width: -webkit-fill-available;; + padding: 0.3em 1em; + + .logout-container { + display: flex; + align-items: center; + + + .logo { + padding-right: 4%; + color: #5F6368; + } + + .text { + .sub-title { + color: #3E4245; + font-size: 0.85rem; + font-family: 'Google Sans', sans-serif !important; + + } + } + } + } +} + +//------------css of card end-------------------// + +.image { + width: 35px; + height: 35px; +} + + +.fixed-top { + z-index: 1000; + position: relative; +} + +.top-nav { + height: 60px; + background-color: transparent; + // box-shadow: (0px 4px 16px rgba(195, 209, 226, 0.25)); + + + .parent-container { + // width: 1600px; + display: flex; + margin: auto; + justify-content: space-between; + /* border: 1px solid black; */ + height: 100%; + align-items: center; + padding: 0% 1.5%; + background-color: white; + + .left-heading { + display: flex; + align-items: center; + cursor: pointer; + } + + .logo-icon { + height: 4.2em; + width: 4em; + cursor: pointer; + background-image: url('../../../assets/images/new-pdc-logo.svg'); + background-size: 443px; + background-repeat: round; + border-radius: 28px; + margin-right:1em; + } + + .user-icon { + height: 50px; + width: 50px; + cursor: pointer; + border-radius: 28px; + + } + + .right-menu-items { + display: flex; + align-items: center; + + .profile-container { + margin-left: 1%; + font-family: 'Google Sans', sans-serif !important; + + .circle { + display: flex; + justify-content: center; + width: 35px; + height: 35px; + border-radius: 60%; + overflow: hidden; + } + + .circle img { + width: 100%; + height: 100%; + object-fit: cover; + } + } + + } + } +} + +.icon-color { + color: #5F6368; + font-size: 24px; +} + +::ng-deep .quick-link.mat-mdc-menu-panel.mat-mdc-menu-panel { + width: 420px !important; + display: flex; + flex-direction: column; + background: #F0F4F9; + + .mat-mdc-menu-content { + padding: 0.5rem; + } + + .ql-container { + width: 100%; + display: flex; + flex-direction: column; + + .ql-child-container{ + display: flex; + flex-direction: row; + text-wrap: nowrap ; + justify-content: space-between; + align-items: flex-end; + } + + a, a:active{ + text-decoration: none; + } + + + .align-link-content{ + display: flex; + flex-direction: row; + align-items: baseline; + gap: 10px; + width: 100% !important; + box-shadow: 0px 4px 4px -6px #b9b8b8 !important; + } + + .no-box-shadow{ + box-shadow: none !important; + } + + .icon-container { + display: flex; + alignment-baseline: after-edge; + + mat-icon { + color: #161A1D !important; + } + } + + } + + + .ql-header{ + margin: 1rem; + margin-left: 1.7rem; + } + + .avatar { + width: 40px; + height: 40px; //<--use the size you choose + border-radius: 100%; + text-align: center; + } + + div.link-label { + color: #161A1D; + font-size: 16px; + font-style: normal; + font-weight: 400; + line-height: 24px; + letter-spacing: 0.4px; + height: 50px; + margin: 0px 5px; + width: 57%; + } + + .avatar span { + line-height: 40px; + font-size: 16px; + } + + .link-button { + margin: 1rem; + height: 41px; + width: 100% !important; + } +} + +.notification-banner-container { + z-index: 999; + position: relative; + width: inherit; + display: flex; + flex-direction: row; + height: 8%; + background: #F6F9FE; + align-content: center; + flex-wrap: wrap; + padding-left: 1%; + gap: 1%; + margin-bottom: 0.2%; + + .amber { + color: #fbc645; + } + + .red { + color: red; + } + + .blue { + color: #4285F4; + } +} + +.red-border { + border-left: 5px solid red; +} + +.amber-border { + border-left: 5px solid #fbc645; +} + +.blue-border { + border-left: 5px solid #4285F4; +} + +.notification-text { + display: flex; + flex-direction: row; + align-items: center; + gap: 15px; + width: 70%; +} + +.notification-buttons-container { + width: 26%; + justify-content: flex-end; + display: flex; + flex-direction: row; + gap: 15px; + + button { + font-family: 'Google Sans', sans-serif !important; + font-style: normal; + font-weight: 500; + line-height: 23px; + font-size: 14px; + } + + .dismiss-button { + letter-spacing: 0.4px; + color: #4285F4; + } + + .learn-more-button { + color: #fff !important; + background: #4285F4 !important; + border-radius: 30px; + letter-spacing: unset; + box-shadow: none; + } +} + +::ng-deep .profile-menu.mat-mdc-menu-panel.mat-mdc-menu-panel { + max-width: 320px !important; + width: 300px !important; +} + +::ng-deep .profile-menu .mat-mdc-menu-content { + padding: 0% !important; +} + +.action-item-container { + width: -webkit-fill-available; + padding-inline: 5%; + padding-top: 2%; + padding-bottom: 2%; +} + +.logout-button { + background: white !important; + border: 1px solid #4285F4 !important; +} + +.logout-button-text { + color: #4285F4 !important; +} + +.app-version-chip { + font-size: 9px; + border-radius: -2px; + padding-inline: 8px; + line-height: 16px; + border-radius: 11px; + margin-left: 8px; + color: #474c55; + background: linear-gradient(92.1deg, #bdd1f3 19.16%, rgba(199, 183, 245, 0.9) 79%, rgba(235, 165, 158, 0.9) 135.24%), linear-gradient(272.04deg, rgba(158, 188, 237, 0.7) 14.93%, rgba(158, 188, 237, 0) 104.51%) !important; +} + +.bypassLoginLogo { + background: aliceblue; + color: cornflowerblue; + align-items: center; + cursor: pointer; +} + +.bypassMenuContainer { + display: flex; + justify-content: center; + background: aliceblue; + width: 42% !important; + height: 40% !important; + border-radius: 65px !important; +} + +.bypassMenuLogo { + font-size: 6rem; + height: 6rem; + width: 6rem; +} diff --git a/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/app/components/header/header.component.spec.ts b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/app/components/header/header.component.spec.ts new file mode 100644 index 0000000..21f0b64 --- /dev/null +++ b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/app/components/header/header.component.spec.ts @@ -0,0 +1,39 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { HeaderComponent } from './header.component'; + +describe('HeaderComponent', () => { + let component: HeaderComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ HeaderComponent ] + }) + .compileComponents(); + + fixture = TestBed.createComponent(HeaderComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/app/components/header/header.component.ts b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/app/components/header/header.component.ts new file mode 100644 index 0000000..a1a0d85 --- /dev/null +++ b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/app/components/header/header.component.ts @@ -0,0 +1,78 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Component } from '@angular/core'; +import { DomSanitizer } from '@angular/platform-browser'; +import { MatIconRegistry } from '@angular/material/icon'; +import { Router } from '@angular/router'; +import { UserService } from 'src/app/services/user/user.service'; +import { AuthService } from '../../services/login/auth.service'; +import { MatDialog } from '@angular/material/dialog'; +import { environment } from 'src/environments/environment'; +import { IntentDetails, IntentService } from 'src/app/services/intent.service'; + +const GOOGLE_CLOUD_ICON = + ` + + + + + + + + +`; + +@Component({ + selector: 'app-header', + templateUrl: './header.component.html', + styleUrls: ['./header.component.scss'], +}) +export class HeaderComponent { + headerTitle: string = environment.chatbotName; + intentsInProgress: IntentDetails[] = []; + requiredLogin: string = environment.requiredLogin + + constructor(iconRegistry: MatIconRegistry, + sanitizer: DomSanitizer, + private router: Router, + public _UserService: UserService, + public authService: AuthService, + public dialog: MatDialog, + private intentsService: IntentService, + ) { + iconRegistry.addSvgIconLiteral('google-cloud-icon', sanitizer.bypassSecurityTrustHtml(GOOGLE_CLOUD_ICON)); + this.intentsService.getAllIntent().subscribe(allIntents => { + this.intentsInProgress = allIntents.filter(i => i.status === "1" || i.status === "3") + }) + } + + navigate() { + this.router.navigateByUrl('/'); + }; + + goToManageIntentPage(){ + this.router.navigateByUrl('/intent-management'); + } + + logout() { + this.authService.logout(); + } + + openURL(link: string) { + (window as any).open(link, "_blank"); + } +} diff --git a/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/app/components/login/login.component.html b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/app/components/login/login.component.html new file mode 100644 index 0000000..e26608e --- /dev/null +++ b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/app/components/login/login.component.html @@ -0,0 +1,48 @@ + + + diff --git a/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/app/components/login/login.component.scss b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/app/components/login/login.component.scss new file mode 100644 index 0000000..54a6df8 --- /dev/null +++ b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/app/components/login/login.component.scss @@ -0,0 +1,545 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +.login-container { + display: flex; + min-height: 100vh; + max-height: 100%; + .spinner{ + display: flex; + justify-content: center; + margin: 1em; + } + + .linear-text { + background: linear-gradient(89.9deg, rgba(66, 133, 244, 0.95) 21.17%, #A488F5 44.34%, rgba(234, 67, 53, 0.88) 81.26%); + -webkit-background-clip: text; + -moz-background-clip: text; + background-clip: text; + color: transparent; + } + + .heading { + font-family: 'Google Sans', sans-serif !important; + font-size: 32px; + font-style: normal; + font-weight: 500; + line-height: 150.7%; + color: rgba(45, 44, 44, 0.7490196078); + } + + .left-container { + flex: 0 1 55%; + min-height: 100%; + max-width: 60%; + align-items: center; + background: #fafcfe; + padding: .5em; + overflow: auto; + display: flex; + justify-content: center; + + .login-app-text-sub { + color: #161A1D; + font-size: 1rem; + font-style: normal; + font-weight: 400; + line-height: 150.7%; + } + + .login-app-text { + // margin-bottom: 2rem; + line-height: 1.5em; + } + + + + .expand-button { + --mdc-fab-container-color: #4285F41A; + --mdc-fab-icon-color: #000000; + border: 1px solid #4285F4; + --mat-mdc-fab-color: #4285F4; + height: 2.2rem; + font-size: 0.8rem; + font-weight: 400; + box-shadow: none; + } + + .expand-less-button { + --mdc-fab-container-color: rgba(92, 95, 97, 0.10); + --mdc-fab-icon-color: #000000; + border: 1px solid rgba(92, 95, 97, 0.50); + --mat-mdc-fab-color: rgba(92, 95, 97, 0.50); + margin-bottom: 1em; + height: 2.2rem; + font-size: 0.8rem; + font-weight: 400; + box-shadow: none + } + } + + .right-container { + flex: 0 1 45%; + min-height: 100%; + max-width: 80%; + align-items: flex-start; + display: flex; + justify-content: center; + overflow-y: auto; + scrollbar-width: thin; + + .login-card { + flex-direction: column; + display: flex; + max-width: 100%; + padding: 3%; + margin: auto; + + .logo-icon { + height: 145px; + width: 100%; + // margin-bottom: 2rem; + } + + .login-button { + background: #4285F4; + box-sizing: border-box; + border: 1px solid #e0e0e0; + color: #fff; + position: relative; + user-select: none; + cursor: pointer; + outline: none; + -webkit-tap-highlight-color: transparent; + display: inline-block; + font-family: 'Google Sans', sans-serif !important; + font-size: 14px; + font-weight: 500; + white-space: nowrap; + text-decoration: none; + vertical-align: baseline; + text-align: center; + margin: 0; + min-width: 100px; + line-height: 36px; + padding: 0 16px; + border-radius: 4px; + overflow: visible; + transform: translate3d(0, 0, 0); + transition: background 400ms cubic-bezier(0.25, 0.8, 0.25, 1), box-shadow 280ms cubic-bezier(0.4, 0, 0.2, 1); + } + + .register-text { + display: flex; + flex-direction: column; + align-items: flex-start; + margin: 1em 0em; + font-weight: 400; + color: #161A1D + } + + .register-button { + background: white; + box-sizing: border-box; + border: 1px solid #4285F4; + color: #4285F4; + position: relative; + user-select: none; + cursor: pointer; + outline: none; + -webkit-tap-highlight-color: transparent; + display: inline-block; + font-family: 'Google Sans', sans-serif !important; + font-size: 14px; + font-weight: 500; + white-space: nowrap; + text-decoration: none; + vertical-align: baseline; + text-align: center; + margin: 0; + min-width: 64px; + line-height: 36px; + padding: 0 16px; + border-radius: 4px; + overflow: visible; + transform: translate3d(0, 0, 0); + transition: background 400ms cubic-bezier(0.25, 0.8, 0.25, 1), box-shadow 280ms cubic-bezier(0.4, 0, 0.2, 1); + } + } + } + + + .slide { + width: 100%; + min-height: 15rem; + border-radius: 10px; + background-size: cover; + background-position: center; + font-size: 1rem; + font-style: normal; + font-weight: 400; + line-height: 200.7%; + padding-left: 0px; + + .feature-heading { + font-weight: 700; + } + } + + .slider { + position: relative; + height: 100%; + } + + .dotsContainer { + display: flex; + justify-content: center; + align-items: center; + margin-top: 2em; + } + + .dot { + margin: 0 3px; + cursor: pointer; + font-size: 18px; + color: #D0D3D7; + } + + .active { + color: #1A73E8; + background-color: #D0D3D7; + padding: 2px; + border-radius: 50%; + font-size: 12px; + } + + .material-symbols-outlined { + font-variation-settings: + 'FILL' 1, + 'wght' 400, + 'GRAD' 0, + 'opsz' 18 + } + + h6 { + position: relative; + z-index: 1; + overflow: hidden; + text-align: center; + font-size: 1.2em; + font-style: normal; + font-weight: 500; + line-height: 200.7%; + font-family: 'Google Sans', sans-serif !important; + } + + h6:before, + h6:after { + position: absolute; + top: 51%; + overflow: hidden; + width: 50%; + height: 1px; + content: '\a0'; + background: linear-gradient(86.95deg, #34A853 -65.34%, #FBBC05 -12.75%, #EA4335 25.15%, #1A73E8 103.88%); + } + + h6:before { + margin-left: -50%; + text-align: right; + } + +} + +.login-buttons-container { + padding-inline: 4%; + gap: 12px; + display: flex; + flex-direction: column; + background: #F7F9FF; + padding-top: 4%; + padding-bottom: 4%; + border-radius: 5px; +} + +.back-button { + color: #5C5F61 !important; + font-weight: 400 !important; +} + +.help-container { + padding-top: 2%; + text-align: center; +} + +.blue { + color: #4285F4; + cursor: pointer; +} + +mat-form-field { + width: 100%; +} + +.user-name-container{ + display: flex; + flex-direction: row; + gap: 3%; +} + +.error-message { + font-weight: 500; + font-size: 0.8rem; + letter-spacing: -0.005rem; + color: #ed0c0c; + + .mat-mdc-form-field-subscript-wrapper { + margin-top: -10px; + } +} + +.forgot-password-container { + display: flex; + flex-direction: column; + text-align: center; + padding: 4%; +} + +.forgot-password-icon-container { + width: 100%; + height: 100%; + + mat-icon{ + font-size: 105px; + height: 100%; + width: 100%; + padding: 1%; + padding-top: 7%; + color: #3467bb; + } +} + +.forgot-password-heading-container { + font-size: 29px; + font-weight: 600; + font-family: 'Google Sans', sans-serif !important; + line-height: 2.5em; +} + +.forgot-password-subtext-container{ + display: flex; + flex-direction: column; + text-align: center; + font-size: 19px; + gap: 10px; + line-height: 1.5em; + + mat-icon{ + color: #4385f4; + } +} + +.logo-icon { + height: 170px; + width: 100%; +} + +.login-button { + width: 100%; + background: #4285F4; + box-sizing: border-box; + border: 1px solid #e0e0e0; + color: #fff; + position: relative; + user-select: none; + cursor: pointer; + outline: none; + -webkit-tap-highlight-color: transparent; + display: inline-block; + font-family: 'Google Sans', sans-serif !important; + font-size: 14px; + font-weight: 500; + white-space: nowrap; + text-decoration: none; + vertical-align: baseline; + text-align: center; + margin: 0; + min-width: 64px; + line-height: 44px; + padding: 0 16px; + border-radius: 4px; + overflow: visible; + transform: translate3d(0, 0, 0); + transition: background 400ms cubic-bezier(0.25, 0.8, 0.25, 1), box-shadow 280ms cubic-bezier(0.4, 0, 0.2, 1); +} + +.register-text { + display: flex; + flex-direction: column; + align-items: flex-start; + margin-bottom: 0.3em; + font-weight: 400; + color: #5C5F61 +} + +.register-button { + width: 100%; + background: white; + box-sizing: border-box; + border: 1px solid #4285F4; + color: #4285F4; + position: relative; + user-select: none; + cursor: pointer; + outline: none; + -webkit-tap-highlight-color: transparent; + display: inline-block; + font-family: 'Google Sans', sans-serif !important; + font-size: 14px; + font-weight: 500; + white-space: nowrap; + text-decoration: none; + vertical-align: baseline; + text-align: center; + margin: 0; + min-width: 64px; + line-height: 44px; + padding: 0 16px; + border-radius: 4px; + overflow: visible; + transform: translate3d(0, 0, 0); + transition: background 400ms cubic-bezier(0.25, 0.8, 0.25, 1), box-shadow 280ms cubic-bezier(0.4, 0, 0.2, 1); +} + +.send-email-button { + background: #40bd40 !important; + font-weight: 600 !important; + font-size: 16px !important; +} + +.close-button { + float: right !important; +} + +.forgot-password-link-container { + width: 100%; + text-align: end; + display: flex; + flex-direction: column; + padding-bottom: 4%; + z-index: 100; + position: relative; +} + +.button-css { + display: flex; + justify-content: center; + + .ok-button{ + justify-content: center; + display: flex; + width: 25%; + background: #4285F4; + box-sizing: border-box; + border: 1px solid #e0e0e0; + color: #fff; + position: relative; + user-select: none; + cursor: pointer; + outline: none; + -webkit-tap-highlight-color: transparent; + font-family: 'Google Sans', sans-serif !important; + font-size: 14px; + font-weight: 500; + white-space: nowrap; + text-decoration: none; + vertical-align: baseline; + text-align: center; + margin: 0; + min-width: 64px; + line-height: 44px; + padding: 0 16px; + border-radius: 4px; + overflow: visible; + transform: translate3d(0, 0, 0); + transition: background 400ms cubic-bezier(0.25, 0.8, 0.25, 1), box-shadow 280ms cubic-bezier(0.4, 0, 0.2, 1); + } +} + +.icon-css { + font-size: 40px; + color: #f3f30aed +} + +.text-css { + font-size: 15px; + line-height: 1.45em; + font-family: "Google Sans", sans-serif !important; + color: #645a5a; +} + +.logo-container { + display: flex; + flex-direction: row; + justify-content: center; + text-align: center; + mat-icon{ + height: 20% !important; + width: 100% !important; + font-size: 70px !important; + color: #4285F4 !important; + } +} + +.heading-span { + display: flex; + flex-direction: row; + gap: 8px; + align-items: center; + padding: 2%; + width: 100%; + justify-content: center; + font-size: 18px; + font-weight: 700; + padding-left: 15%; + color: #4285F4 !important; +} + +.heading-container { + display: flex; + flex-direction: row; + justify-content: center; + background: aliceblue; +} + +.sub-parent-container { + display: flex; + flex-direction: column; + text-align: center; + gap: 15px; + font-style: normal; + line-height: normal; + font-family: 'Google Sans', sans-serif !important; +} + +.info { + padding-bottom: 10px !important; + small { + color: #a8a8b5; + + } +} + diff --git a/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/app/components/login/login.component.spec.ts b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/app/components/login/login.component.spec.ts new file mode 100644 index 0000000..980b9c1 --- /dev/null +++ b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/app/components/login/login.component.spec.ts @@ -0,0 +1,39 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { LoginComponent } from './login.component'; + +describe('LoginComponent', () => { + let component: LoginComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ LoginComponent ] + }) + .compileComponents(); + + fixture = TestBed.createComponent(LoginComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/app/components/login/login.component.ts b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/app/components/login/login.component.ts new file mode 100644 index 0000000..1bb5fdd --- /dev/null +++ b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/app/components/login/login.component.ts @@ -0,0 +1,88 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Component, NgZone, inject } from '@angular/core'; +import { Auth, signInWithPopup, GoogleAuthProvider } from '@angular/fire/auth'; +import { Router } from '@angular/router'; +import { AuthService } from 'src/app/services/login/auth.service'; +import { MatSnackBar } from '@angular/material/snack-bar'; +import { ToastMessageComponent } from '../shared/toast-message/toast-message.component'; +import { environment } from 'src/environments/environment'; + +const HOME_ROUTE = '/' + +interface LooseObject { + [key: string]: any +} + +@Component({ + selector: 'app-login', + templateUrl: './login.component.html', + styleUrls: ['./login.component.scss'] +}) +export class LoginComponent { + private readonly auth: Auth = inject(Auth); + private readonly provider: GoogleAuthProvider = new GoogleAuthProvider(); + loader: boolean = false; + chatbotName: string = environment.chatbotName; + + constructor(private authService: AuthService, + private router: Router, + public ngZone: NgZone, + private _snackBar: MatSnackBar, + ) { + this.provider.setCustomParameters({ + prompt: "select_account" + }); + } + + + loginWithGoogle() { + this.loader = true; + signInWithPopup(this.auth, this.provider) + .then((result: any) => { + const user = result.user.toJSON(); + this.ngZone.run(() => { + this.authService.saveUserSession(user.stsTokenManager.accessToken); + this.redirect(user); + }); + }).catch((error) => { + this.loader = false; + if (error.message !== "Firebase: Error (auth/popup-closed-by-user).") { + this._snackBar.openFromComponent(ToastMessageComponent, { + panelClass: ["red-toast"], + verticalPosition: "top", + horizontalPosition: "right", + duration: 5000, + data: { text: "Error with SignIn. Please try again later !!!", icon: "cross-in-circle-white" }, + }); + } + console.error(`Error: ${error}`); + }); + } + + redirect(user: any) { + let userDetails: LooseObject = {} + userDetails['name'] = user.displayName + userDetails['email'] = user.email + userDetails['photoURL'] = user.photoURL + userDetails['domain'] = user.domain + userDetails['uid'] = user.uid, + localStorage.setItem('USER_DETAILS', JSON.stringify(userDetails)); + this.loader = false; + this.router.navigate([HOME_ROUTE]); + } +} diff --git a/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/app/components/main/chat/chat.component.html b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/app/components/main/chat/chat.component.html new file mode 100644 index 0000000..2de5158 --- /dev/null +++ b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/app/components/main/chat/chat.component.html @@ -0,0 +1,22 @@ + + +
    +
    + +
    + +
    \ No newline at end of file diff --git a/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/app/components/main/chat/chat.component.scss b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/app/components/main/chat/chat.component.scss new file mode 100644 index 0000000..3e140d9 --- /dev/null +++ b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/app/components/main/chat/chat.component.scss @@ -0,0 +1,74 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// #chatContainer { +// position: fixed; +// top: 0%; +// bottom: 0%; +// margin: auto; +// width: 90%; +// height: 75%; +// background-color: #f7f9ff; +// right: 0px; +// left: 0px; +// border-radius: 3px; +// display: flex; +// flex-direction: row; +// overflow-y: auto; + +// app-sidebar { +// flex-basis: 20%; +// display: block; +// height: 100%; +// border-right: 1px solid #e8e8e8; +// min-width: 20%; +// } + +// #chat { +// background-color: #f7f9fa; +// flex-grow: 1; +// } +// } + +// .custom-outer-container{ +// position: fixed; +// top: 0px; +// bottom: 0px; +// right: 0px; +// left: 0px; +// /* width: 100%; */ +// /* height: 100%; */ +// /* margin: 0 0; */ +// /* padding: 0; */ +// margin-top: 4rem; +// background-color: red; +// } + +.custom-outer-container { + width: 100%; + height: 100%; + position: absolute; + bottom: 0px; + display: flex; + justify-content: center; + align-items: center; + overflow: hidden; + + .chat-body-outer { + width: 70%; + height: 95%; + } +} \ No newline at end of file diff --git a/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/app/components/main/chat/chat.component.spec.ts b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/app/components/main/chat/chat.component.spec.ts new file mode 100644 index 0000000..b687f75 --- /dev/null +++ b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/app/components/main/chat/chat.component.spec.ts @@ -0,0 +1,39 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ChatComponent } from './chat.component'; + +describe('ChatComponent', () => { + let component: ChatComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ ChatComponent ] + }) + .compileComponents(); + + fixture = TestBed.createComponent(ChatComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/app/components/main/chat/chat.component.ts b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/app/components/main/chat/chat.component.ts new file mode 100644 index 0000000..a59cec3 --- /dev/null +++ b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/app/components/main/chat/chat.component.ts @@ -0,0 +1,33 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Component } from '@angular/core'; +import { UserService } from 'src/app/services/user/user.service'; + +@Component({ + selector: 'app-chat', + templateUrl: './chat.component.html', + styleUrls: ['./chat.component.scss'] +}) +export class ChatComponent { + userInfo:any; + + constructor( + private userService: UserService + ) { + this.userInfo = this.userService.getUserDetails(); + } +} diff --git a/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/app/components/main/chat/chatbar/chatbar.component.html b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/app/components/main/chat/chatbar/chatbar.component.html new file mode 100644 index 0000000..eacd1a6 --- /dev/null +++ b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/app/components/main/chat/chatbar/chatbar.component.html @@ -0,0 +1,192 @@ + + +
    +
    + + +
    +
    + +
    +
    + + +
    +
    +
    +
    +
    + +
    + +
    +
    +
    + + + + +
    +
    +
    + + + {{message.contentParts[0].text}} + +
    +
    + +
    +
    + +
    +
    +
    + +
    +
    +
    +
    +
    + +
    + + + +
    + + + {{ (part.thinkingSteps?.[0]?.name | formatAgentName) }} + + + + + Travel Concierge + + will transfer you to {{ part.thinkingSteps?.[0]?.data?.agent_name | formatAgentName }}... + + + + Travel Concierge + + is memorizing the input... + +
    + +
    + Function Call: {{ step.name }}
    + Arguments:
    {{ step.data | json }}
    +
    +
    + Function Response: {{ step.name }}
    + Response:
    {{ step.data | json }}
    +
    +
    +
    +
    + + + +
    Places Found:
    +
    +
    + + + +
    +
    {{ place.place_name }}
    +

    Address: {{ place.address }}

    +

    Highlights: {{ place.highlights }}

    +

    Rating: {{ place.review_ratings }} / 5

    + + map View on Maps + +
    +
    +
    +
    + + + +
    + +
    +
    +
    +
    +
    +
    + +
    + Generated in {{message.responseTime}}s +
    + + + {{suggestion}} + + +
    +
    +
    + +
    + +
    diff --git a/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/app/components/main/chat/chatbar/chatbar.component.scss b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/app/components/main/chat/chatbar/chatbar.component.scss new file mode 100644 index 0000000..dcc46dc --- /dev/null +++ b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/app/components/main/chat/chatbar/chatbar.component.scss @@ -0,0 +1,1240 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@import url("https://fonts.googleapis.com/icon?family=Material+Icons|Material+Icons+Outlined"); + +pre { + text-wrap: auto; + font-size: 0.8rem; + color: white; + padding: 1rem; +} + +.chat-container { + display: flex; + background-color: #fff; + flex-direction: column; + font-family: "Google Sans", sans-serif !important; + padding: 2%; + height: calc(100% - 11%); + + + .chat-header { + min-height: 64.5px; + background-color: #eeeeee; + box-shadow: 2px 0px 4px #ccc; + display: flex; + justify-content: space-between; + align-items: center; + + .avatarSection { + display: flex; + + .picture { + min-width: 40px; + max-width: 40px; + margin: 0 10px; + + img { + max-width: 100%; + } + } + + .nameSection { + flex-grow: 1; + color: #48b975; + display: flex; + align-items: center; + font-size: 1.2rem; + font-weight: 500; + } + } + } + + .chat-body { + flex-grow: 1; + display: flex; + flex-direction: column; + flex-direction: column-reverse; + padding: 20px 5px; + padding-bottom: 2%; + overflow-y: auto; + padding-right: 2%; + width: 100%; + scrollbar-width: none; + + .user-message { + display: flex; + align-items: center; + color: #767676; + flex-direction: row; + // justify-content: flex-end; + word-break: break-word; + margin-bottom: 10px; + margin-top: 10px; + width: 100%; + + .user-message-wrapping-container { + background: #FFFFFF; + border: 1px solid #E4EDFD; + box-shadow: 0px 4px 15px 0px rgba(195, 209, 226, 0.25); + border-radius: 20px 20px 0px 20px; + width: 100%; + display: flex; + justify-content: space-between; + padding: 0.5% 1%; + align-items: center; + color: #161A1D; + + .messageContent { + font-size: 0.95rem; + font-family: "Google Sans", sans-serif !important; + padding-left: 1%; + max-width: 96%; + + } + } + } + + .user-message:first-child { + margin-top: 25px; + } + + .bot-message { + margin-top: 10px; + align-items: center; + color: #767676; + flex-direction: row; + margin-bottom: 10px; + width: 100%; + + .bot-message-wrapping-container { + border: 0.5px solid #dfe0e2; + display: flex; + border: 1px solid #E4EDFD; + // box-shadow: 0px 4px 16px rgba(140, 140, 156, 0.25); + border-radius: 0 20px 20px 0; + padding: 0.0% 1%; + background-color: #F6F9FE; + color: #161A1D; + + .bot-message-content { + margin-left: 20px; + line-height: 2rem; + font-size: 0.95rem; + text-wrap: wrap; + width: 95%; + word-break: break-word; + max-width: 100%; + margin-top: 12px; + } + + .bot-message-action { + display: flex; + flex-direction: column; + height: 100%; + align-self: flex-start; + padding-top: 1em; + padding-bottom: 1em; + align-items: center; + + + div { + height: 3.5em; + width: 3.5em; + background-size: 443px; + background-repeat: round; + border-radius: 28px; + } + } + } + + .action-content { + display: flex; + justify-content: space-between; + align-items: center; + width: 100%; + + .action-message { + font-size: 0.95rem; + } + } + } + + } + + .chat-footer-outer { + display: flex; + align-items: center; + width: 100%; + justify-content: space-between; + + .chat-footer { + min-height: 60px; + display: flex; + border: 1px solid #D8D8D8; + background: #F0F4F9; + + border-radius: 30px; + width: 100%; + box-shadow: 0px 4px 18px 0px rgba(195, 209, 226, 0.25); + padding-inline: 1%; + // #spacer { + // flex-basis: 50px; + // } + + .chat-textarea { + flex-grow: 1; + display: flex; + align-items: center; + padding-left: 1%; + + textarea { + width: 100%; + height: 45px; + border: 0px; + border-radius: 4px; + resize: none; + outline: none; + padding: 14px; + font-family: 'Google Sans', sans-serif !important; + box-sizing: border-box; + -ms-overflow-style: none; + /* Internet Explorer 10+ */ + scrollbar-width: none; + /* Firefox */ + font-size: 0.95rem; + line-height: 1.1rem; + color: #534646; + background: #F0F4F9; + + &::placeholder { + color: #646566; + } + } + + textarea::-webkit-scrollbar { + display: none; + /* Safari and Chrome */ + } + } + + .micIcon { + flex-basis: 75px; + display: flex; + align-items: center; + /* justify-content: center; */ + color: #545454; + margin-top: 2px; + padding-right: 1%; + + .send-icon { + color: #4f5255; + } + + img { + max-width: 40%; + cursor: pointer; + } + } + } + + } + + +} + +.circle { + width: 40px; + height: 40px; + /* border: 5px solid rgb(255, 255, 255); */ + border-radius: 50%; + overflow: hidden; + background-color: #fff; + /* align-items: center; */ + text-align: center; + display: flex; + align-items: inherit; + justify-content: center; +} + +.circle img { + width: 75%; + height: 73%; + object-fit: cover; +} + +.circle .material-symbols-rounded { + width: 100%; + height: 100%; + object-fit: cover; + color: #ffffff; + font-size: 1.9rem; + margin-top: 12%; +} + +.user-circle { + width: 40px; + height: 40px; + border-radius: 50%; + overflow: hidden; + background-color: #fff; + text-align: center; + display: flex; + align-items: inherit; + justify-content: center; + align-self: flex-end; + + img { + width: 100%; + height: 100%; + object-fit: cover; + } + +} + +/* width */ +::-webkit-scrollbar { + width: 8px; +} + +/* Track */ +::-webkit-scrollbar-track { + background: #f1f1f1; +} + +/* Handle */ +::-webkit-scrollbar-thumb { + background: #bec4c4; +} + +/* Handle on hover */ +::-webkit-scrollbar-thumb:hover { + background: #555; + border-radius: 10px; +} + +/** Loader Styling Block */ +.loader { + height: 263px; + background-size: contain; + background-repeat: repeat; + text-align: center; + font-size: 1.2rem; + border: double 0.01em transparent; + border-radius: 20px 20px 20px 0px; + /*added a colourstop here, without the third colourstop you get a hard edge*/ + background: linear-gradient(#fefeff, #fbfbfc), + linear-gradient(var(--angle),rgba(164, 136, 245, 0.9) 81.43%, rgba(234, 67, 53, 0.9) 135.5%); + background-origin: border-box; + background-clip: content-box, border-box; + animation: 1s rotate linear infinite; + margin-top: 2%; + display: flex; + color: #3E4245; + justify-content: center; +} + +@keyframes rotate { + to { + --angle: 360deg; + } +} + +@property --angle { + syntax: ""; + initial-value: 0deg; + inherits: false; +} + +.loader-child-container { + height: 263px; + background-size: cover; + background-repeat: repeat; + display: flex; + text-align: center; + justify-content: center; + align-items: center; + width: 70%; + line-height: 150%; + border-top-right-radius: 20px; + border-bottom-right-radius: 20px; +} + +.left-side-container-1 { + background-size: 443px; + background-repeat: no-repeat; + height: 263px; + width: 50%; + border-radius: 28px +} + +.left-side-container-2 { + background-size: 443px; + background-repeat: no-repeat; + height: 263px; + width: 50%; + border-radius: 28px +} + +.left-side-container-3 { + background-size: 443px; + background-repeat: no-repeat; + height: 263px; + width: 50%; + border-radius: 28px +} + +.left-side-container-error { + background-image: url('../../../../../assets/images/error_img.svg'); + background-size: 443px; + background-repeat: no-repeat; + height: 263px; + width: 50%; + border-radius: 28px +} + +.right-side-container-1{ + display: flex; + flex-direction: column; + padding-right: 3%; + background: linear-gradient( #f7fffb, #f7fffb), + linear-gradient(var(--angle), rgb(22, 165, 29), rgb(248, 206, 17), rgb(236, 17, 17), rgb(194, 17, 248), rgb(46, 75, 240)); +} + +.right-side-container-2{ + display: flex; + flex-direction: column; + padding-right: 3%; + background: linear-gradient(#ffffff, #ffffff), + linear-gradient(var(--angle), rgb(22, 165, 29), rgb(248, 206, 17), rgb(236, 17, 17), rgb(194, 17, 248), rgb(46, 75, 240)); +} + +.right-side-container-3{ + display: flex; + flex-direction: column; + padding-right: 3%; + background: linear-gradient(#fdfdfe, #fdfdfe), + linear-gradient(var(--angle), rgb(22, 165, 29), rgb(248, 206, 17), rgb(236, 17, 17), rgb(194, 17, 248), rgb(46, 75, 240)); +} + +.right-side-container-error{ + display: flex; + flex-direction: column; + padding-right: 3%; + background: linear-gradient(#fefeff, #fbfbfc), + linear-gradient(var(--angle), rgb(22, 165, 29), rgb(248, 206, 17), rgb(236, 17, 17), rgb(194, 17, 248), rgb(46, 75, 240)); +} + +.loader-text-container { + text-align: center; + margin-top: auto; + padding-top: 8%; +} + +.answer-not-found { + background-image: url('../../../../../assets/images/delete_notification_dialog_image.png'), url('../../../../../assets/images/books.png'); + background-size: 66%, 45%; + background-repeat: no-repeat, no-repeat; + height: 298px; + width: 25%; + border-radius: 28px; + display: flex; + flex-direction: row; + gap: 18px; + background-position: left, right; + background-color: none; +} + +@-webkit-keyframes fadeOut { + 0% { + opacity: 0; + } + + 50% { + opacity: 0.5; + } + + 100% { + opacity: 1; + } +} + +@keyframes fadeOut { + 0% { + opacity: 0; + } + + 50% { + opacity: 0.5; + } + + 100% { + opacity: 1; + } +} + +.mat-mdc-standard-chip { + --mdc-chip-label-text-color: #4285F4; + --mdc-chip-elevated-container-color: #fff; + border: 2px solid #4285F4 !important; + font-family: 'Google Sans', sans-serif !important; + font-size: 18px !important; + // font-weight: 600; +} + +:host ::ng-deep .mdc-evolution-chip__cell { + justify-content: center; +} + +.container { + display: flex; + justify-content: center; +} + +.generating-text { + font-family: "Google Sans", sans-serif !important; + font-style: normal; + font-weight: 500; + font-size: 12px; + line-height: normal; + letter-spacing: 0.4px; + color: #3E4143; + background: linear-gradient(92.1deg, #bdd1f3 19.16%, rgba(199, 183, 245, 0.9) 79%, rgba(235, 165, 158, 0.9) 135.24%), linear-gradient(272.04deg, rgba(158, 188, 237, 0.7) 14.93%, rgba(158, 188, 237, 0) 104.51%); + padding: 4px; + padding-inline: 2%; + border-bottom-left-radius: 13px; + border-bottom-right-radius: 13px; + // background: linear-gradient(95.31deg, #9EBCED -1.7%, rgba(164, 136, 245, 0.9) 81.43%, rgba(234, 67, 53, 0.9) 135.5%); +} + +.chip-container { + margin-bottom: 0.75rem; +} + +.chip-min-size { + min-width: 12rem; +} + +.bot-card-content { + display: flex; + flex-direction: row; + margin-bottom: 25px; + gap: 15px; + background: #F6F9FE; + padding: 2%; + border-radius: 8px; + width: fit-content; + margin-left: 2%; + + mat-card { + border-radius: 8px; + + img { + // width: 44%; + width: 85px; + height: 96.6px; + margin-left: auto; + margin-right: auto; + } + + mat-card-content { + padding-top: 9%; + word-break: break-word; + } + } + + a { + width: 215px; + pointer-events: visibleFill; + text-decoration: none; + line-height: 1.4rem; + } + + // a:hover { + // text-decoration: underline; + // } + + .info-card { + height: 100%; + font-family: 'Google Sans', sans-serif !important; + font-style: normal; + font-weight: 300; + font-size: 14px; + line-height: 24px; + /* or 133% */ + text-align: center; + letter-spacing: 0.4px; + + /* Black */ + color: #161A1D; + + // opacity: 0.75; + .card-view-button-div { + padding: 6%; + } + + .card-view-button { + background: #4285f436; + border-radius: 17px; + width: 87px; + height: 33.86px; + justify-content: center; + color: 4285F4; + color: #4285F4; + } + + .footer-info { + color: var(--Green, #0F9D58); + text-align: center; + font-size: 13px; + font-style: normal; + padding: 0.5em; + line-height: 24px; + /* 150% */ + letter-spacing: 0.4px; + background: rgba(52, 168, 83, 0.05); + + a:hover { + text-decoration: none !important; + } + } + } + + .score-card { + height: 100%; + font-family: 'Google Sans', sans-serif !important; + font-style: normal; + font-weight: 400; + font-size: 40px; + line-height: 24px; + text-align: center; + letter-spacing: 0.4px; + + .footer-info { + margin-top: 20px; + color: #161A1D; + text-align: center; + font-size: 14px; + font-style: normal; + padding: 0.5em; + line-height: 24px; + letter-spacing: 0.4px; + background: #F8F9FC; + border-bottom-left-radius: 8px; + border-bottom-right-radius: 8px; + a:hover { + text-decoration: none !important; + } + } + + .header-info { + color: #f1f1f1; + text-align: center; + font-size: 16px; + font-style: normal; + padding: 0.5em; + line-height: 24px; + letter-spacing: 0.4px; + background: #4285f4f2; + border-top-left-radius: 8px; + border-top-right-radius: 8px; + display: flex; + justify-content: center; + a:hover { + text-decoration: none !important; + } + } + } +} + +.ticket-label { + display: block; + color: #85898B; + font-size: 13px; +} + +.ticket-data { + display: block; + font-size: 13px; + line-height: 1rem; +} + +.ticket-data-subject { + color: #3E4245; + font-size: 16px; + font-style: normal; + font-weight: 400; + line-height: 24px; + text-align: center; + margin: 0.5em; +} + +.ticket-data-id { + text-align: center; + + mat-icon { + color: #4285F4; + font-size: 20px; + } + + span { + line-height: 1.6rem; + } + + .contact-name { + color: rgba(22, 26, 29, 1); + text-align: center; + font-size: 24px; + font-weight: 500; + line-height: 24px; + letter-spacing: 0.4px; + opacity: 0.75; + } + + .material-symbols-rounded { + color: #5F6368; + font-size: 5.6em; + width: 90px; + height: 90px; + font-variation-settings: + 'FILL' 1, + 'wght' 400, + 'GRAD' 0, + 'opsz' 24 + } +} + +.ticket-card { + max-width: 220px; + width: 220px; + word-break: break-word; +} + +.ticket-panel { + margin: 1em; + border-radius: 4px; + background: #ffffff; + box-shadow: none; + + .panel-header, + .mat-expansion-panel-content { + background: #ffffff !important; + + :hover, + :active { + background: #ffffff; + } + } + + .panel-logo { + margin-top: 3px; + } + + mat-icon { + color: #4285F4; + font-size: 20px; + } + + .panel-content { + padding: 1em; + display: block + } + + .ticket-id { + color: #161A1D; + font-size: 16px; + font-weight: 400; + line-height: 24px; + } + + .tic-label { + color: #A8A8B5; + font-size: 14px; + font-weight: 500; + line-height: 24px; + } + + .ticket-subject { + color: #3E4245; + font-size: 14px; + font-weight: 400; + line-height: 24px; + } +} + +.h2-mat { + margin-bottom: 10px; + margin-top: 5px; + font-weight: 700; +} + +.challenge-card { + color: #4285F4; +} + +.share-button { + color: #5F6368; + font-size: 15px; + line-height: 25px; + margin-right: 10px; +} + +::ng-deep .share-menu { + min-height: 30px !important; + padding: 0.5em 1em; +} + +::ng-deep .share-card { + border-radius: 6px !important; + box-shadow: 0px 4px 16px 0px rgba(140, 140, 156, 0.25) !important; + + >.mat-mdc-menu-content { + padding: 0px !important; + } +} + +.share-text { + color: #3E4245; + font-family: 'Google Sans', sans-serif !important; + font-size: 12px; + font-style: normal; + font-weight: 400; + line-height: 24px; + letter-spacing: 0.4px; +} + +.dep-accordion { + display: block; + width: 100%; +} + +.dep-accordion-item { + display: block; + border: solid 1px #ccc; + background-color: #fff; +} + +.dep-accordion-item+.dep-accordion-item { + border-top: none; + background-color: #fff; +} + +.dep-accordion-item-header { + display: flex; + align-content: center; + justify-content: space-between; +} + +.dep-accordion-item-description { + font-size: 0.85em; + color: #999; + background-color: #fff; +} + +.dep-accordion-item-header { + padding-top: 10px; + padding-bottom: 10px; + padding-left: 16px; + padding-right: 16px; +} + +.dep-accordion-item-body { + padding-bottom: 5px; + padding-left: 16px; + padding-right: 16px; +} + +.dep-accordion-item-header:hover { + cursor: pointer; +} + +.dep-accordion-item { + border-radius: 4px; + margin-bottom: 2%; + border-left: 5px solid; +} + +.dri-heading { + font-size: 16px; + display: block; + white-space: pre-line; +} + +ul { + margin-top: -5px; +} + + +a.chat-help-link { + display: inline-flex; + padding: 3px 25px; + justify-content: center; + align-items: center; + gap: 10px; + border-radius: 17px; + background: #E4EDFD; + color: #4285F4; + font-size: 16px; + font-style: normal; + font-weight: 400; + line-height: 28px; + /* 175% */ + letter-spacing: 0.4px; + + &:hover, + &:active { + background: #E4EDFD; + color: #4285F4; + } + + --mat-mdc-button-persistent-ripple-color: #E4EDFD; + + .mat-icon { + color: #4285F4; + font-size: 20px; + line-height: 18px; + } +} + +.static-text { + margin-bottom: 10px; +} + +.dep-offerin-detail-card { + display: flex; + flex-direction: column; + gap: 0; + margin-left: 0% !important; +} + +.dep-offering-highlight-accordion { + margin-left: 2%; +} + + + +.graph-container { + border-radius: 8px; + background: rgba(255, 255, 255, 0.35); + padding: 0em 1em 1em 1em; + + & div:first-child { + display: block; + } + + & div:nth-child(1) { + margin-right: 1.5em; + } + + & div:nth-child(n+1) { + display: inline-flex; + padding: 3px 2px; + margin-top: 0.5em; + justify-content: center; + align-items: center; + gap: 10px; + } + + .graph-title { + color: #161A1D; + font-size: 12px; + font-style: normal; + font-weight: 400; + line-height: 24px; + letter-spacing: 0.4px; + } +} + +.badge-container { + border-radius: 8px; + background: rgba(255, 255, 255, 0.35); + padding: 1em 1em .25em 1em; + + & div:first-child { + display: block; + } + + .ticket-heading { + color: #161A1D; + font-family: 'Google Sans', sans-serif !important; + font-size: 16px; + font-style: normal; + font-weight: 400; + line-height: 24px; + letter-spacing: 0.4px; + display: flex; + align-items: center; + } + + .ticket-icon { + width: 24px; + height: 24px; + flex-shrink: 0; + color: rgba(66, 133, 244, 1); + } +} + +.initial { + color: #5F6368; +} + +.material-symbols-rounded.like { + font-variation-settings: + 'FILL' 1, + 'wght' 400, + 'GRAD' 0, + 'opsz' 24 +} + +.icon-color { + color: rgb(171 160 243); +} + +.bot-suggestion-container { + justify-content: center; + display: flex; + flex-direction: row; + margin-bottom: -0.5%; + width: 100%; + + .bot-suggestion-container-child { + width: 100%; + background: linear-gradient(92.1deg, #bdd1f3 19.16%, rgba(199, 183, 245, 0.9) 79%, rgba(235, 165, 158, 0.9) 135.24%), linear-gradient(272.04deg, rgba(158, 188, 237, 0.7) 14.93%, rgba(158, 188, 237, 0) 104.51%) !important; + padding-inline: 2%; + padding-inline-end: 1%; + padding-bottom: 0.7%; + border-radius: 22px; + padding-top: 1%; + } +} + +.enabled-button { + color: #4285F4 !important; + border: none !important; +} + +.delete-button { + color: #ed143d !important; +} + +::ng-deep .mat-mdc-snack-bar-container { + &.green-snackbar { + --mdc-snackbar-container-color: #0F9D58; + --mat-mdc-snack-bar-button-color: #fff; + --mdc-snackbar-supporting-text-color: #fff; + } + &.red-snackbar { + --mdc-snackbar-container-color: #e9103f; + --mat-mdc-snack-bar-button-color: #fff; + --mdc-snackbar-supporting-text-color: #fff; + } +} + +.blue-dot { + /* Ellipse 1226 */ + content: ''; + width: 8px; + height: 8px; + left: 1503px; + top: 445px; + display: flow-root; + border-radius: 15px; + + /* Blue */ + background: #4285F4; + animation: blue-transform 2s infinite ; + +} + +.green-dot { + content: ''; + width: 8px; + height: 8px; + left: 1522px; + top: 445px; + display: flow-root; + border-radius: 15px; + + /* Green */ + background: #0F9D58; + animation: green-transform 2s infinite ; +} + +.yellow-dot { + content: ''; + width: 8px; + height: 8px; + left: 1541px; + top: 445px; + display: flow-root; + border-radius: 15px; + + /* Yellow */ + background: #F4B400; + animation: yellow-transform 2s infinite ; + +} + +.recording-container { + display: flex; + flex-direction: row; + gap: 10px; + cursor: pointer; + align-items: baseline; +} + + +@keyframes blue-transform { + 0% {transform: translateY(-14px)} + 33% {transform: translateY(0)} + 66% {transform: translateY(0)} + 100% {transform: translateY(-14px) } + +} + +@keyframes green-transform { + 0% {transform: translateY(0)} + 33% {transform: translateY(-14px)} + 66% {transform: translateY(0)} + 100% {transform: translateY(0) } + +} + +@keyframes yellow-transform { + 0% {transform: translateY(0)} + 33% {transform: translateY(0)} + 66% {transform: translateY(-14px)} + 100% {transform: translateY(0) } + +} + +.bypassMenuContainer { + display: flex; + justify-content: center; + background: aliceblue; + width: 42% !important; + height: 40% !important; + border-radius: 65px !important; +} + +.container-circle { + width: 100%; + height: 100%; + object-fit: cover; + border-radius: 50px; + padding-top: 4%; + padding-bottom: 4%; +} + +.user-icon { + width: 40px; + height: 40px; + border-radius: 50%; + overflow: hidden; + background-color: aliceblue; + color: cornflowerblue; + text-align: center; + display: flex; + align-items: inherit; + justify-content: center; + align-self: flex-end; + + img { + width: 100%; + height: 100%; + object-fit: cover; + } +} + + + +.function-details-container { + padding-left: 1.5em; // Indent details a bit + border-left: 2px solid #eee; // Visual separator + margin-left: 10px; // Space from the summary arrow + padding-top: 0.5em; +} + +.map-tool-response { + margin-bottom: 1em; + display: flex; + justify-content: center; + + .place-card { + border: 1px solid #e0e0e0; + border-radius: 8px; + padding: 12px; + margin: 0 .5rem; + margin-bottom: 12px; + background-color: #f9f9f9; + display: flex; + align-items: center; + flex-direction: column; + gap: 12px; // Space between image and text + + .place-image { + // width: 200px; // Adjust as needed + // height: 200px; // Adjust as needed + // object-fit: cover; + border-radius: 4px; + flex-shrink: 0; // Prevent image from shrinking + } + + .place-details { + flex-grow: 1; // Allow text details to take remaining space + h5 { + margin-top: 0; + margin-bottom: 0.3em; + font-size: 1.1em; + color: #333; + } + p { + font-size: 0.9em; + margin-bottom: 0; + color: #555; + strong { + color: #444; + } + } + .map-button { + margin-top: 0.5em; + .mat-icon { + margin-right: 4px; // Space between icon and text in button + } + } + } + } + // If no image, let details take full width + .place-card:not(:has(.place-image)) .place-details { + width: 100%; + } +} + +.function-step { + // Your existing styles for function-step, or add new ones + margin-bottom: 0.5em; + padding: 0.5em; + background-color: #f0f0f0; + border-radius: 4px; + pre { + white-space: pre-wrap; + word-break: break-all; + background-color: #e9e9e9; + padding: 0.3em; + border-radius: 3px; + } +} diff --git a/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/app/components/main/chat/chatbar/chatbar.component.ts b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/app/components/main/chat/chatbar/chatbar.component.ts new file mode 100644 index 0000000..9d9868f --- /dev/null +++ b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/app/components/main/chat/chatbar/chatbar.component.ts @@ -0,0 +1,612 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Component, EventEmitter, Output, OnDestroy, OnInit } from '@angular/core'; +import { ChatService, ServerMessage, ConnectionStatus } from 'src/app/services/chat.service'; +import { UserService } from 'src/app/services/user/user.service'; +import { BroadcastService } from 'src/app/services/broadcast.service'; +import { Observable, ReplaySubject, Subscription, firstValueFrom } from 'rxjs'; +import { takeUntil, distinctUntilChanged } from 'rxjs/operators'; +import { SessionService } from 'src/app/services/user/session.service'; +import { Router } from '@angular/router'; +import { MatDialog } from '@angular/material/dialog'; +import { animate, sequence, state, style, transition, trigger } from '@angular/animations'; +import { MatSnackBar } from '@angular/material/snack-bar'; +import { SpeechToTextService } from 'src/app/services/speech-to-text'; +import { Message, SuggestionData, ThinkingStep, MessageContentPart } from 'src/app/models/messegeType.model'; +import { environment } from 'src/environments/environment'; + +@Component({ + selector: 'app-chatbar', + templateUrl: './chatbar.component.html', + styleUrls: ['./chatbar.component.scss'], + animations: [ + trigger('bounce', [ + state('true', style({ transform: 'translateY(0)', color: '#4285F4' })), + transition('* => true', [ + sequence([ + style({ transform: 'translateY(0)' }), + animate("400ms cubic-bezier(0,0,0,1)", style({ transform: 'translateY(-14px)', color: '#4285F4' })), + animate("300ms cubic-bezier(1,0,1,1)", style({ transform: 'translateY(0)', color: '#4285F4' })), + animate("200ms cubic-bezier(0,0,0,1)", style({ transform: 'translateY(-10px)', color: '#4285F4' })), + animate("150ms cubic-bezier(1,0,1,1)", style({ transform: 'translateY(0)', color: '#4285F4' })), + animate("100ms cubic-bezier(0,0,0,1)", style({ transform: 'translateY(-5px)', color: '#4285F4' })), + animate("80ms cubic-bezier(1,0,1,1)", style({ transform: 'translateY(0)', color: '#4285F4' })), + ]), + ]) + ]), + trigger('delete', [ + state('true', style({ transform: 'translateY(0)', color: '#ed143d' })), + transition('* => true', [ + sequence([ + style({ transform: 'translateY(0)' }), + animate("400ms cubic-bezier(0,0,0,1)", style({ transform: 'translateX(14px)', color: '#ed143d' })), + animate("300ms cubic-bezier(1,0,1,1)", style({ transform: 'translateX(0)', color: '#ed143d' })), + animate("200ms cubic-bezier(0,0,0,1)", style({ transform: 'translateX(-14px)', color: '#ed143d' })), + animate("150ms cubic-bezier(1,0,1,1)", style({ transform: 'translateX(0)', color: '#ed143d' })), + animate("100ms cubic-bezier(0,0,0,1)", style({ transform: 'translateX(7px)', color: '#ed143d' })), + animate("80ms cubic-bezier(1,0,1,1)", style({ transform: 'translateX(0)', color: '#ed143d' })), + ]), + ]) + ]), + ] +}) +export class ChatbarComponent implements OnInit, OnDestroy { + + @Output() onSubmit: EventEmitter = new EventEmitter(); + + isSuggestedQuestion: string = ''; + chatQuery: string = ''; + // chatQuery$: Observable; + showLoader: boolean = false; + startTimer: boolean = false; + conversation: Message[] = []; + leftContainerClass = ""; + rightContainerClass = ""; + index = 2; + loaderTextArray: string[] = [ + "I am a conversation bot built on Google Cloud's Vertex AI tools.", + "Tool Tip: You can reset the conversation anytime using the reset button next the the text input box.", + "Joke : Why did the robot go on a diet? Because it had too many bytes!", + "Joke : Why can't bicycles stand up by themselves? Because they are two-tired!", + "Joke : How does a computer get drunk? It takes screenshots." + ]; + loaderTextTimeout: undefined | ReturnType; + loaderText = ""; + loaderTextIndex = 0; + loaderIndex = 1; + isChatDisabled: boolean = false; + botStartTime: number = 0; + initialQuestion: string = ''; + ticketId: string = ''; + categoryIntent: string = ""; + changeImageInterval: undefined | ReturnType; + panelOpenState = false; + like = false; + dislike = false; + loaderSelectedChip = ''; + showLoaderLikeDislikeButtons = false; + outOfContextAnswerResponseObject = { + like: false, + dislike: false + } + showSuggestion = false; + suggestedQuestionMessage!: Message; + loader_chat_id: any; + questionArray: { question: string, id?: any }[] = []; + + borderColors = ['#4285F4', '#0F9D58', '#F4B400', '#DB4437']; + + private readonly destroyed = new ReplaySubject(1); + isRecording = false; + transcribedText = ''; + mediaRecorder!: MediaRecorder; + audioChunks: Blob[] = []; + + requiredLogin: string = environment.requiredLogin; + + private currentConnectionStatus: ConnectionStatus = 'disconnected'; + private currentBotMessage: Message | null = null; + private pendingUserMessageFromBroadcast: Message | null = null; + + constructor(private router: Router, + public dialog: MatDialog, + private chatService: ChatService, + private sessionService: SessionService, + public userService: UserService, + private broadcastService: BroadcastService, + private _snackBar: MatSnackBar, + private speechToTextService: SpeechToTextService, + ) { + this.broadcastService.chatQuery$.pipe(takeUntil(this.destroyed)).subscribe((latestMessage: Message) => { + const isInitialBotGreetingFromService = latestMessage.type === 'bot' && latestMessage.contentParts[0]?.text === 'Ask me anything and start your journey! 🚀🚀🚀'; + + if (latestMessage.type === 'user' && latestMessage.contentParts[0]?.text) { + console.log("ChatbarComponent: Received user message via broadcast/initial value.", latestMessage); + // If it's a user message, it takes precedence. + // Add to UI if not already the head of the conversation + if (this.conversation.length === 0 || this.conversation[0] !== latestMessage) { + this.conversation.unshift(latestMessage); + } + this.pendingUserMessageFromBroadcast = latestMessage; + this.processPendingUserMessage(); // Attempt to process + } else if (isInitialBotGreetingFromService) { + // This is the default greeting from BroadcastService. + // Add it only if the conversation is empty (i.e., no user message took precedence). + if (this.conversation.length === 0) { + this.conversation.push(latestMessage); + } + } + }); + this.loaderText = this.loaderTextArray[Math.floor(Math.random() * this.loaderTextArray.length)]; + } + + ngOnInit(): void { + this.chatService.connect(); + this.subscribeToConnectionStatus(); + this.subscribeToWebSocketMessages(); + } + + ngOnDestroy() { + this.destroyed.next(); + this.destroyed.complete(); + this.clearTimeoutForLoaderText(); + if (this.changeImageInterval) clearTimeout(this.changeImageInterval); + this.chatService.close("ChatbarComponent destroyed"); + } + + private processPendingUserMessage(): void { + if (!this.pendingUserMessageFromBroadcast || !this.pendingUserMessageFromBroadcast.contentParts[0]?.text) { + return; // No pending message or message is empty + } + + const queryToSend = this.pendingUserMessageFromBroadcast.contentParts[0].text; + + if (this.currentConnectionStatus === 'connected' || this.currentConnectionStatus === 'processing_complete') { + console.log("ChatbarComponent: Processing pending user message via WebSocket.", queryToSend); + + this.outOfContextAnswerResponseObject = { like: false, dislike: false }; + this.removeSuggestionElement(); + + this.showLoader = true; + this.loaderText = "Sending your message..."; + this.setTimeoutForLoaderText(); + this.setCyclicBackgroundImages(); + this.botStartTime = new Date().getTime(); + this.pushQuestion(queryToSend); + this.currentBotMessage = null; + + this.chatService.sendMessage(queryToSend); + this.pendingUserMessageFromBroadcast = null; // Clear after sending + + this.scrollToBottom(); + } else { + console.warn(`ChatbarComponent: Pending user message "${queryToSend}" cannot be sent yet. WS status: ${this.currentConnectionStatus}`); + // If disconnected, error, or aborted, and not already trying to connect, attempt to connect. + // chatService.connect() has internal guards against multiple simultaneous connection attempts. + if (this.currentConnectionStatus === 'disconnected' || this.currentConnectionStatus === 'error' || this.currentConnectionStatus === 'aborted') { + console.log("ChatbarComponent: Attempting to (re)connect to send pending message."); + this.chatService.connect(); + } + // If 'connecting' or 'reconnecting', we just wait for the status update to trigger this method again. + } + } + + public getPlaceDisplayUrl(place: any): string { + const keywords = place.place_name.split(' ').join(',').toLowerCase(); + return `https://maps.googleapis.com/maps/api/staticmap?center=${encodeURIComponent(keywords)}&zoom=14&size=200x200&key=${environment.googleMapsApiKey}`; + } + + private subscribeToConnectionStatus(): void { + this.chatService.getConnectionStatus() + .pipe( + takeUntil(this.destroyed), + distinctUntilChanged() + ) + .subscribe((status: ConnectionStatus) => { + console.log("ChatbarComponent - Connection Status:", status); + this.currentConnectionStatus = status; + this.isChatDisabled = (status === 'connecting' || status === 'reconnecting'); + + switch (status) { + case 'connecting': + case 'reconnecting': + this.showLoader = true; + this.loaderText = status === 'connecting' ? "Connecting to assistant..." : "Reconnecting..."; + this.setCyclicBackgroundImages(); + break; + case 'connected': + this.loaderText = "Connection established. Assistant is ready."; + this.processPendingUserMessage(); // Send any pending message + if (!this.showLoader && !this.pendingUserMessageFromBroadcast) { + this.isChatDisabled = false; + } + break; + case 'processing_complete': + this.showLoader = false; + this.clearTimeoutForLoaderText(); + this.loaderText = "Assistant is ready for your next message."; + this.isChatDisabled = false; + this.currentBotMessage = null; // Ready for new interaction + this.processPendingUserMessage(); + // currentBotMessage is finalized when 'end_of_turn' is received + break; + case 'closed_by_server': + case 'closed_by_client': + case 'disconnected': + case 'aborted': + this.showLoader = false; + this.clearTimeoutForLoaderText(); + if (status === 'disconnected' && this.conversation.length > 0 && this.conversation[0]?.type === 'user' && !this.showLoader && !this.pendingUserMessageFromBroadcast) { + this.setErrorMessage("Connection lost. Please try sending your message again."); + } else if (status === 'closed_by_server') { + this.loaderText = "Connection closed by server."; + } else if (status === 'closed_by_client' && this.conversation.length > 0) { + this.loaderText = "Chat session ended."; + } + this.isChatDisabled = false; + this.currentBotMessage = null; + break; + case 'error': + this.setErrorMessage("An error occurred with the connection."); + this.isChatDisabled = false; + this.currentBotMessage = null; + // processPendingUserMessage might try to reconnect if a message is pending + this.processPendingUserMessage(); + break; + } + }); + } + + private subscribeToWebSocketMessages(): void { + this.chatService.getMessages().pipe(takeUntil(this.destroyed)).subscribe({ + next: (serverMessage: ServerMessage) => { + console.log("ChatbarComponent - Received Server Message:", serverMessage); + + // 'start' means the backend session is ready. + if (serverMessage.operation === 'start') { // No active user query + if (this.showLoader && !this.botStartTime && !this.pendingUserMessageFromBroadcast) { + this.loaderText = "Ask a question and start your journey! 🚀🚀🚀"; + this.setCyclicBackgroundImages(); + } else if (this.showLoader){ // A user query is active or was just sent + this.loaderText = "Waiting for assistant's response..."; + } + this.clearTimeoutForLoaderText(); + console.log("Chat session turn started by server."); + } else if (serverMessage.operation === 'end_of_turn') { + console.log("Server indicated end_of_turn."); + if (this.currentBotMessage) { + const endTime = new Date().getTime(); + this.currentBotMessage.responseTime = this.botStartTime ? ((endTime - this.botStartTime) / 1000).toString() : "N/A"; + // Ensure there's some content if nothing else was added + if (this.currentBotMessage.contentParts.length === 0) { + this.currentBotMessage.contentParts.push({type: 'text', text: "[Empty response]"}); + } + } + // The 'processing_complete' status will reset currentBotMessage + } else if (serverMessage.error) { + console.error("Error message from server:", serverMessage.error); + this.setErrorMessage(serverMessage.error); + this.currentBotMessage = null; + } else if (serverMessage.answer) { + this.showLoader = false; + this.clearTimeoutForLoaderText(); + this.handleWebSocketDataMessage(serverMessage); + } else { + console.warn("Received unhandled server message structure:", serverMessage); + } + }, + error: (err) => { + console.error("ChatbarComponent - Error from WebSocket messages observable:", err); + this.setErrorMessage("Failed to process message from server."); + this.currentBotMessage = null; + } + }); + } + + private handleWebSocketDataMessage(data: ServerMessage): void { + console.log("ChatbarComponent - Handling WebSocket Data Message:", data); + + if (!this.currentBotMessage || this.conversation[0] !== this.currentBotMessage) { + this.currentBotMessage = { + contentParts: [], // Initialize with contentParts + type: 'bot', + shareable: true, + categoryIntent: data.intent || this.categoryIntent, + botStartTime: this.botStartTime ? this.botStartTime.toString() : "N/A", + extras: { like: false, dislike: false, delete: false }, + }; + this.conversation.unshift(this.currentBotMessage); + } + + const activeBotMessage = this.currentBotMessage; + let lastPart = activeBotMessage.contentParts.length > 0 ? activeBotMessage.contentParts[activeBotMessage.contentParts.length - 1] : null; + + if (data.answer && data.answer.function_call) { + const thinkingStep: ThinkingStep = { + type: 'functionCall', + name: data.answer.function_call.name, + data: data.answer.function_call.args, + }; + if (lastPart && lastPart.type === 'thinking') { + lastPart.thinkingSteps = lastPart.thinkingSteps || []; + lastPart.thinkingSteps.push(thinkingStep); + } else { + activeBotMessage.contentParts.push({ type: 'thinking', thinkingSteps: [thinkingStep] }); + } + } else if (data.answer && data.answer.function_response) { + const thinkingStep: ThinkingStep = { + type: 'functionResponse', + name: data.answer.function_response.name, + data: data.answer.function_response.response, + }; + if (lastPart && lastPart.type === 'thinking') { + lastPart.thinkingSteps = lastPart.thinkingSteps || []; + lastPart.thinkingSteps.push(thinkingStep); + } else { + // This case might be less common if a function_call always precedes a response + // but good to handle if a response can start a thinking block. + activeBotMessage.contentParts.push({ type: 'thinking', thinkingSteps: [thinkingStep] }); + } + } else if (data.answer && typeof data.answer.text === 'string') { + if (lastPart && lastPart.type === 'text') { + lastPart.text += data.answer.text; // Append to existing text part + } else { + activeBotMessage.contentParts.push({ type: 'text', text: data.answer.text }); + } + } else if (typeof data.answer === 'string') { // Fallback for plain string answer + if (lastPart && lastPart.type === 'text') { + lastPart.text += data.answer; + } else { + activeBotMessage.contentParts.push({ type: 'text', text: data.answer }); + } + } else if (data.answer && Object.keys(data.answer).length > 0) { + console.warn("Received answer object with unknown structure, adding as new thinking step:", data.answer); + const thinkingStep: ThinkingStep = { + type: 'functionResponse', // Treat as a generic tool output + name: 'Unknown Tool Output', + data: data.answer, + }; + if (lastPart && lastPart.type === 'thinking') { + lastPart.thinkingSteps = lastPart.thinkingSteps || []; + lastPart.thinkingSteps.push(thinkingStep); + } else { + activeBotMessage.contentParts.push({ type: 'thinking', thinkingSteps: [thinkingStep] }); + } + } else { + console.log("Received WS data part without actionable content for current message:", data); + } + + if (data.intent) { + activeBotMessage.categoryIntent = data.intent; + } + if (data.suggested_questions && data.suggested_questions.length > 0) { + activeBotMessage.suggestedQuestion = data.suggested_questions; + } + this.scrollToBottom(); + } + + submitMessage(event: any) { + this.outOfContextAnswerResponseObject = { like: false, dislike: false }; + this.removeSuggestionElement(); + + if (event && typeof event.preventDefault === 'function') { + event.preventDefault(); + } + + const queryToSend = this.chatQuery.trim(); + if (!queryToSend) { + return; + } + this.chatQuery = ''; + + const userMessage: Message = { + contentParts: [{ type: 'text', text: queryToSend }], // User message is simple text + type: 'user', + shareable: false, + }; + this.conversation.unshift(userMessage); + this.currentBotMessage = null; + + if (this.currentConnectionStatus === 'connected' || this.currentConnectionStatus === 'processing_complete') { + this.showLoader = true; + this.loaderText = "Sending your message..."; + this.setTimeoutForLoaderText(); + this.setCyclicBackgroundImages(); + this.botStartTime = new Date().getTime(); + this.pushQuestion(queryToSend); + + console.log("submitMessage: Sending message via WebSocket."); + this.chatService.sendMessage(queryToSend); + } else { + console.error("submitMessage: WebSocket not in a ready state to send message. Status:", this.currentConnectionStatus); + this.setErrorMessage("Cannot send message. Connection not ready. Please try again."); + if (this.currentConnectionStatus === 'disconnected' || this.currentConnectionStatus === 'error' || this.currentConnectionStatus === 'aborted') { + this.chatService.connect(); + } + } + this.scrollToBottom(); + } + + setErrorMessage(customMessage?: string) { + this.clearTimeoutForLoaderText(); + this.leftContainerClass = 'left-side-container-error'; + this.loaderText = customMessage || 'Oops, something went wrong. Please try sending your message again.'; + this.rightContainerClass = 'right-side-container-error'; + this.showLoaderLikeDislikeButtons = false; + this.showLoader = true; + } + + resetBrowser() { + console.log("Chatbar: Resetting chat session."); + this.chatService.close("User reset chat session"); + this.conversation = []; + this.initialQuestion = ''; + this.questionArray = []; + this.showLoader = false; + this.isChatDisabled = false; + this.clearTimeoutForLoaderText(); + this.removeSuggestionElement(); + this.currentBotMessage = null; + this.pendingUserMessageFromBroadcast = null; + this.botStartTime = 0; + this.openSnackBar("Chat has been reset.", "success-snackbar"); + + setTimeout(() => this.chatService.connect(), 100); + } + + setupMediaRecorder(stream: MediaStream) { + this.mediaRecorder = new MediaRecorder(stream); + this.mediaRecorder.ondataavailable = event => this.audioChunks.push(event.data); + this.mediaRecorder.onstop = () => this.sendAudioToGCP(); + } + + async sendAudioToGCP() { + const audioBlob = new Blob(this.audioChunks); + try { // CHG: Added try-catch for async operation + const response: any = await (await this.speechToTextService.transcribeAudio(audioBlob)).toPromise(); // CHG: Convert Observable to Promise for await + this.chatQuery = response[0]; // Assuming response structure + } catch (error) { + console.error("Error transcribing audio:", error); + this.openSnackBar("Error transcribing audio. Please try again.", "error-snackbar"); + } + } + + getStringData(obj: any): string { + let str = (obj as string); + if (str === "" || str.length === 0) { + return "Sorry! I don't have sufficient information to answer this question at the moment."; + } + return (obj as string); + } + + getSuggestionData(obj: any): SuggestionData { + return (obj as SuggestionData); + } + + assignId(id: any) { + for (let i = this.questionArray.length - 1; i >= 0; i--) { + if (!this.questionArray[i].id) { + this.questionArray[i].id = id; + break; + } + } + } + getQuestion(id: any) { return '';} + setAnswerNotFoundText(loaderText: string) { } + stopTicketCreationFlow() { } + setCyclicBackgroundImages() { } + setTimeoutForLoaderText() { + if (this.loaderTextTimeout) clearInterval(this.loaderTextTimeout); + this.loaderText = this.loaderTextArray[Math.floor(Math.random() * this.loaderTextArray.length)]; + this.showLoaderLikeDislikeButtons = false; + this.loaderTextTimeout = setInterval(() => { this.setCyclicLoaderText(); }, 3000); + } + + clearTimeoutForLoaderText() { + if (this.loaderTextTimeout) { + clearInterval(this.loaderTextTimeout); + this.loaderTextTimeout = undefined; + } + } + setCyclicLoaderText() { this.loaderText = this.loaderTextArray[Math.floor(Math.random() * this.loaderTextArray.length)]; } + + setSuggestedQuestionInChat(response: ServerMessage, endTime: number) { + if (this.currentBotMessage && response.suggested_questions && response.suggested_questions.length > 0) { + this.currentBotMessage.suggestedQuestion = response.suggested_questions; + this.showSuggestion = true; + this.suggestedQuestionMessage = this.currentBotMessage; + setTimeout(() => { this.scrollToBottom(); }, 100); + } else if (response.suggested_questions && response.suggested_questions.length > 0) { + this.showSuggestion = true; + this.suggestedQuestionMessage = { + contentParts: [], + type: 'bot', + responseTime: this.botStartTime ? ((endTime - this.botStartTime) / 1000).toString() : "N/A", + shareable: false, + categoryIntent: response.intent || this.categoryIntent, + extras: { like: false, dislike: false }, + suggestedQuestion: response.suggested_questions, + }; + setTimeout(() => { this.scrollToBottom(); }, 100); + } + } + chipControlOnSelect(event: any) { + const selectedText = event.target.innerText; + if (selectedText) { + this.chatQuery = selectedText; + this.submitMessage(event); + } + } + getResponseforSuggestionQuery(suggestedQuery: string) { + this.isSuggestedQuestion = suggestedQuery; + this.chatQuery = suggestedQuery; + this.removeSuggestionElement(); + this.submitMessage(suggestedQuery); + } + removeSuggestionElement() { this.showSuggestion = false; } + openSnackBar(message: string, panelClass: string) { + this._snackBar.open(message, 'Close', { + panelClass: [panelClass], + horizontalPosition: 'end', + verticalPosition: 'top', + duration: 3000, + }); + } + startRecording() { + if (this.mediaRecorder && this.mediaRecorder.state === "inactive") { + this.isRecording = true; + this.audioChunks = []; + this.mediaRecorder.start(); + } else if (!this.mediaRecorder) { + console.warn("MediaRecorder not initialized. Attempting to initialize..."); + navigator.mediaDevices.getUserMedia({ audio: true }) + .then(stream => { + this.setupMediaRecorder(stream); + if (this.mediaRecorder && this.mediaRecorder.state === "inactive") { + this.isRecording = true; + this.audioChunks = []; + this.mediaRecorder.start(); + } + }) + .catch(err => { + console.error("Error accessing microphone:", err); + this.openSnackBar("Microphone access denied or unavailable.", "error-snackbar"); + }); + } else { + console.warn("MediaRecorder is already recording or in an unexpected state:", this.mediaRecorder.state); + } + } + stopRecording() { + if (this.mediaRecorder && this.mediaRecorder.state === "recording") { + this.isRecording = false; + this.mediaRecorder.stop(); + } + } + scrollToBottom(): void { + try { + const parentElement = document.getElementsByClassName('chat-body'); + if (parentElement && parentElement[0]) { + setTimeout(() => parentElement[0].scrollTo(0, parentElement[0].scrollHeight), 0); + } + } catch (err) { + console.error("Error scrolling to bottom:", err); + } + } + pushQuestion(question: string, id?: any) { + this.questionArray.push({ question, id }); + } +} diff --git a/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/app/components/main/main.component.html b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/app/components/main/main.component.html new file mode 100644 index 0000000..bea202d --- /dev/null +++ b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/app/components/main/main.component.html @@ -0,0 +1,94 @@ + + +
    +
    +
    +
    +
    + Hey, {{savedUser.name ? savedUser.name!.split(" ")[0]+'!' : ''}} +
    +
    How can I help you today?
    +
    +
    + + + + + +
    diff --git a/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/app/components/main/main.component.scss b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/app/components/main/main.component.scss new file mode 100644 index 0000000..8debfa0 --- /dev/null +++ b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/app/components/main/main.component.scss @@ -0,0 +1,988 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@import url("https://fonts.googleapis.com/icon?family=Material+Icons|Material+Icons+Outlined"); + +:host ::ng-deep mat-form-feild .mat-form-field-appearance-outline .mat-form-field-outline-start { + color: red; + border-radius: 50px !important; + font-size: 10px !important; +} + +:host ::ng-deep .mdc-text-field--outlined .mdc-notched-outline .mdc-notched-outline__leading { + border-radius: 28px 0 0 28px !important; + min-width: 28px !important; + border-color: #babfc4 !important; +} + +:host ::ng-deep .mdc-text-field--outlined .mdc-notched-outline .mdc-notched-outline__trailing { + border-radius: 0 28px 28px 0 !important; + border-color: #babfc4 !important; +} + +:host ::ng-deep .mat-form-field-outline-end { + border-radius: 0 28px 28px 0 !important; +} + +:host ::ng-deep .mat-mdc-card { + box-shadow: 0px 2px 1px -1px rgb(217 220 224), 0px 1px 1px 0px rgba(0, 0, 0, 0.14), 0px 1px 3px 0px rgb(229 232 234) !important; +} + +:host ::ng-deep .mat-mdc-standard-chip.mat-primary.mat-mdc-chip-selected, +.mat-mdc-standard-chip.mat-primary.mat-mdc-chip-highlighted { + --mdc-chip-elevated-container-color: #EDF3FE; + --mdc-chip-elevated-disabled-container-color: #e0e0e0; + --mdc-chip-label-text-color: #4285F4; + ; + --mdc-chip-disabled-label-text-color: #4285F4; + ; + --mdc-chip-with-icon-icon-color: #4285F4; + ; + --mdc-chip-with-icon-disabled-icon-color: #4285F4; + ; + --mdc-chip-with-trailing-icon-disabled-trailing-icon-color: #4285F4; + ; + --mdc-chip-with-trailing-icon-trailing-icon-color: #4285F4; + ; + --mdc-chip-with-icon-selected-icon-color: #4285F4; + ; +} + +.parent-container { + // background-position: bottom; + // background-image: url("../../../assets/images/footer.png"); + // background-repeat: no-repeat; + // background-size: 30%; + // height: calc(100% - 60px); + // overflow: auto; + display: flex; + max-height: calc(100vh - 70px); + height: 100%; + font-family: 'Google Sans', sans-serif !important; + overflow-y: auto; + flex-direction: column; + + .img-globe { + // position:absolute; + width: 66%; + } +} + +.wrapping-container { + margin-top: 6%; + width: 100%; + padding-bottom: 1.5%; +} + +.main-div { + display: flex; + justify-content: center; + align-items: center; + gap: 15px; + font-family: 'Google Sans', sans-serif !important; +} + +.text-control { + width: 32%; +} + +h1 { + line-height: 50px; + width: 34%; +} + +h2 { + font-family: 'Google Sans', sans-serif !important; +} + +.header { + -webkit-background-clip: text; + -webkit-text-fill-color: #48b975; + background-clip: text; + font-size: 3rem; + text-align: center; + font-weight: bold; + font-weight: 700; + -webkit-font-smoothing: antialiased; + height: 100%; + font-family: 'Google Sans', sans-serif; +} + +.send-button { + color: #babfc4; +} + +.logo-icon { + height: 120px; + width: 400px; + margin-bottom: 90px; + margin-left: -20px; +} + +.user-info { + display: flex; + justify-content: center; +} + +.user-info-column { + display: column; +} + +.card-outer-div { + .card-inside-left { + .circle { + width: 100px; + height: 100px; + border: 5px solid rgb(255, 255, 255); + border-radius: 50%; + overflow: hidden; + box-shadow: 0px 2px 1px -1px rgba(0, 0, 0, 0.2), 0px 1px 1px 0px rgba(0, 0, 0, 0.14), 0px 1px 3px 0px rgba(0, 0, 0, 0.12); + } + + .circle img { + width: 100%; + height: 100%; + object-fit: cover; + } + } + + .card-inside-right { + width: 80%; + display: flex; + flex-direction: column; + align-items: center; + color: #747474; + font-size: 0.8rem; + font-weight: 500; + + .name { + .title { + font: 1rem; + } + + .sub-title { + font-size: 0.9rem; + margin-left: 2%; + } + } + + .company { + display: flex; + align-items: center; + + .logo { + padding-right: 1%; + } + + .text { + width: 100%; + + .sub-title { + margin-left: 2%; + } + } + } + } +} + +.mat-mdc-standard-chip { + --mdc-chip-label-text-color: #4285F4; + --mdc-chip-elevated-container-color: #fff; + border: 1px solid #4285F4 !important; + font-family: "Google Sans", sans-serif !important; + font-size: 15px !important; + font-weight: 500 !important; + padding-left: 12px !important; + padding-right: 12px !important; +} + +.sub-heading { + font-family: 'Google Sans', sans-serif; + width: 66%; + color: #161A1D; + text-align: center; + font-size: 30px; + line-height: 40px; + margin-top: -20px; + text-align: left; + z-index: 999; +} + +.genAI { + //background-image: linear-gradient(to left, #4285F4, #DB4437, #F4B400, #0F9D58); + // background-image: linear-gradient(to left, #34A853, #FBBC05, #EA4335, #1A73E8); + background: linear-gradient(86.95deg, #34A853 -65.34%, #FBBC05 -12.75%, #EA4335 25.15%, #1A73E8 103.88%); + -webkit-background-clip: text; + -moz-background-clip: text; + background-clip: text; + color: transparent; +} + +.margin-div { + margin-top: -20px; +} + +.chat-footer-outer { + display: flex; + align-items: center; + width: 100%; + justify-content: center; + padding-bottom: 4.7%; + padding-top: 1.7%; + + .chat-footer { + min-height: 60px; + display: flex; + border: 1px solid #DDE6F6; + box-shadow: 0px 4px 18px rgba(195, 209, 226, 0.25); + border-radius: 30px; + width: 66%; + // box-shadow: 5px 4px 24px rgba(66, 133, 244, 0.15); + background: #F0F4F9; + + .chat-textarea { + flex-grow: 1; + display: flex; + align-items: center; + padding-left: 2%; + + input { + width: 100%; + height: 45px; + border: 0px; + border-radius: 4px; + resize: none; + outline: none; + padding: 14px; + font-family: 'Google Sans', sans-serif !important; + box-sizing: border-box; + -ms-overflow-style: none; + scrollbar-width: none; + font-size: 0.95rem; + line-height: 1.1rem; + color: #534646; + background: #F0F4F9; + } + + input::-webkit-scrollbar { + display: none; + } + } + + .micIcon { + display: flex; + align-items: center; + color: #1F1F1F; + margin-top: 2px; + padding-right: 1%; + + img { + max-width: 40%; + cursor: pointer; + } + } + } + +} + +.text-gnosis { + color: var(--White, #FFF); + font-size: 16px; + font-style: normal; + font-weight: 400; + line-height: 24px; + /* 150% */ + letter-spacing: 0.4px; +} + +.chip-cancel-button { + color: #002dff; +} + +.categories-div { + + .mat-mdc-standard-chip { + --mdc-chip-label-text-color: #0F9D58; + --mdc-chip-elevated-container-color: #fff; + border: 1px solid #0F9D58 !important; + font-family: 'Google Sans', sans-serif !important; + // font-weight: 600; + } +} + +.intent-chip { + + .mat-mdc-standard-chip { + --mdc-chip-label-text-color: #4285F4; + --mdc-chip-elevated-container-color: #fff; + border: 1px solid #4285F4 !important; + font-family: 'Google Sans', sans-serif !important; + // font-weight: 600; + } + + .chip-cancel-button { + color: #002dff; + } +} + +.category-chip { + .mat-mdc-standard-chip { + --mdc-chip-label-text-color: #0F9D58; + --mdc-chip-elevated-container-color: #fff; + border: 1px solid #0F9D58 !important; + font-family: 'Google Sans', sans-serif !important; + // font-weight: 600; + } + + .chip-cancel-button { + color: #007d06; + } +} + +.questions-div { + flex-direction: column; + margin-top: 2rem; + margin-bottom: 5rem; + + .intent-card { + padding: 0px; + } + + .intent-heading { + color: #787978; + font-size: 16px; + font-style: normal; + font-weight: 400; + line-height: normal; + font-family: 'Google Sans', sans-serif !important; + width: 66%; + + } + + .intent-container { + border-radius: 8px; + background-color: rgba(246, 249, 254, 1); + width: 66%; + box-shadow: none !important; + flex-shrink: 0; + + .selected-question { + + // margin: 0.5rem 0rem; + &:hover { + background-color: rgba(66, 133, 244, 0.10); + } + } + + .question-item { + display: flex; + justify-content: space-between; + font-family: 'Google Sans', sans-serif !important; + color: #787978; + font-size: 1em; + line-height: 24px; + } + + .question-link { + overflow: hidden; + text-overflow: ellipsis; + } + } +} + +.intent-chip-textarea ::ng-deep .mdc-evolution-chip-set__chips { + align-items: center !important; +} + +.float-button { + position: fixed; + right: 3em; + bottom: 6em; + --mdc-fab-container-color: #4285F4; +} + +.name-container { + font-size: 3.5rem; + font-weight: 400; + line-height: 4rem; + font-family: Google Sans, Helvetica Neue, sans-serif; + letter-spacing: -.03em; + background: linear-gradient(89.9deg, rgba(66, 133, 244, 0.95) 0.17%, #A488F5 7.34%, rgba(234, 67, 53, 0.88) 24.26%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; + text-fill-color: transparent; + animation: effect 2s linear; + display: flex; + flex-direction: row; + gap: 1%; + align-items: center; +} + +@keyframes effect { + + 0% { + background: linear-gradient(89.9deg, rgba(240, 216, 214, 0.88) 0.17%, #ebcaec 7.34%, rgba(194, 214, 247, 0.95) 24.26%); + -webkit-background-clip: text; + } + + 10% { + background: linear-gradient(89.9deg, rgba(227, 237, 253, 0.95) 0.17%, #e5ddfd 7.34%, rgba(186, 210, 248, 0.95) 24.26%); + -webkit-background-clip: text; + } + + 20% { + background: linear-gradient(89.9deg, rgba(218, 231, 253, 0.95) 0.17%, #dcd1fc 7.34%, rgba(172, 202, 252, 0.95) 24.26%); + -webkit-background-clip: text; + } + + 30% { + background: linear-gradient(89.9deg, rgba(199, 219, 252, 0.95) 0.17%, #d0c2fa 7.34%, rgba(161, 196, 252, 0.95) 24.26%); + -webkit-background-clip: text; + } + + 40% { + background: linear-gradient(89.9deg, rgba(169, 201, 253, 0.95) 0.17%, #cdbcff 7.34%, rgba(149, 188, 250, 0.95) 24.26%); + -webkit-background-clip: text; + } + + 50% { + background: linear-gradient(89.9deg, rgba(200, 220, 253, 0.95) 0.17%, #bba5fc 7.34%, rgba(203, 221, 250, 0.95) 24.26%); + -webkit-background-clip: text; + } + + 60% { + background: linear-gradient(89.9deg, rgba(227, 237, 253, 0.95) 0.17%, #e5ddfd 7.34%, rgba(186, 210, 248, 0.95) 24.26%); + -webkit-background-clip: text; + } + + 70% { + background: linear-gradient(89.9deg, rgba(183, 210, 253, 0.95) 0.17%, #e5ddfd 7.34%, rgba(248, 217, 215, 0.88) 24.26%); + -webkit-background-clip: text; + } + + 80% { + background: linear-gradient(89.9deg, rgba(122, 172, 252, 0.95) 0.17%, #e5ddfd 7.34%, rgba(247, 182, 176, 0.88) 24.26%); + -webkit-background-clip: text; + } + + 90% { + background: linear-gradient(89.9deg, rgba(94, 148, 236, 0.95) 0.17%, #b49ff1 7.34%, rgba(240, 121, 111, 0.88) 24.26%); + -webkit-background-clip: text; + } + + 100% { + background: linear-gradient(89.9deg, rgba(66, 133, 244, 0.95) 0.17%, #A488F5 7.34%, rgba(234, 67, 53, 0.88) 24.26%); + -webkit-background-clip: text; + } +} + +.helper-container { + white-space: nowrap; + overflow: hidden; + font-size: 3.5rem; + font-weight: 400; + line-height: 4rem; + font-family: Google Sans, Helvetica Neue, sans-serif; + letter-spacing: -.03em; + color: #8f9391; + padding-bottom: 1%; + opacity: 0.5; + animation: typing 2s steps(22), blink .5s step-end infinite alternate; + width: 22ch; +} + +@keyframes typing { + from { + width: 0 + } +} + +@keyframes blink { + 50% { + border-color: transparent + } +} + +.quick-bot-introduction-container { + width: 100%; + display: flex; + flex-direction: row; + justify-content: center; + + .introduction-sub-container { + width: 66%; + justify-content: center; + text-align: center; + display: flex; + flex-direction: column; + padding-top: 2%; + padding-bottom: 2%; + background: radial-gradient(white, #FAFCFF) padding-box, linear-gradient(89.86deg, #34A853 -40.93%, #FBBC05 4.92%, #EA4335 37.97%, #1A73E8 106.62%) border-box; + border-radius: 24px; + border: 1px solid transparent; + + } + + .logo-image-container { + img { + height: 110px; + width: 110px; + } + } + + .logo-text-heading { + font-style: normal; + font-weight: 500; + font-size: 28px; + line-height: 202%; + /* or 65px */ + text-align: center; + + background: linear-gradient(89.86deg, #34A853 -40.93%, #FBBC05 4.92%, #EA4335 37.97%, #1A73E8 106.62%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; + text-fill-color: transparent; + } + + .logo-text-subheading { + font-style: normal; + font-weight: 500; + font-size: 21px; + line-height: 202%; + /* or 48px */ + text-align: center; + } + + .blue { + color: #4285F4; + } +} + +.helpButton { + position: fixed !important; + bottom: 45px !important; + right: 20px !important; + background-color: #4285f4 !important; + color: white !important; + text-decoration: none !important; + border-radius: 60px !important; + height: 41px !important; + display: inline-flex !important; + align-items: center !important; + overflow: hidden !important; + width: auto !important; + max-width: 41px !important; + -webkit-transition: max-width 0.5s !important; + transition: max-width 0.5s !important; + z-index: 1000 !important; + + &:hover { + max-width: 300px !important; + } + + .text { + white-space: nowrap !important; + padding-right: 15px !important; + font-size: 15px !important; + font-weight: 600 !important; + } +} + +::placeholder { + color: #646566; +} + +.intent-container-box { + box-sizing: border-box; + width: 239px; + height: 239px; + background: #F0F4F9 !important; + border-radius: 14px; + transition-delay: 2s; + transition: width 8s; + padding: 1%; + cursor: pointer; +} + +.selected-intent-box { + box-sizing: border-box; + height: 239px; + border-radius: 14px; + transition-delay: 2s; + transition: width 8s; + cursor: pointer; + width: max-content !important; + max-width: 100%; + display: flex; + flex-direction: column; + gap: 1%; + padding: 1%; + background: linear-gradient(92.1deg, #bdd1f3 19.16%, rgb(199 183 245 / 90%) 79%, rgb(235 165 158 / 90%) 135.24%), linear-gradient(272.04deg, rgba(158, 188, 237, 0.7) 14.93%, rgba(158, 188, 237, 0) 104.51%) !important; +} + +.cards-outer-container { + width: 65%; + overflow: auto; + scrollbar-width: none; +} + +.cards-outer-container:after { + z-index: -1; + content: ''; + width: 10%; + opacity: 0.5 !important; +} + +.cards-container-all-categories { + width: max-content; + display: flex; + overflow-x: auto; + gap: 1%; + max-width: 160%; + min-width: 100%; + scrollbar-width: none; +} + +.cards-container-few-categories { + width: max-content; + display: flex; + overflow-x: auto; + gap: 1%; + max-width: 160%; + min-width: 100%; + scrollbar-width: none; + justify-content: center; + padding-inline: 5%; +} + + +.intent-container-box:hover { + background: #DDE3EA !important; +} + +.card-detail-container { + width: 100%; + min-width: 250px; + display: flex; + flex-direction: row; +} + +.suggested-question-container { + display: none; +} + +.selected-intent-suggested-question { + gap: 4%; + display: flex !important; + flex-direction: column; +} + +.suggested-question-chip-row { + + margin-top: 0.5rem; + + .suggested-question-chip { + display: flex; + flex-direction: row; + margin: 0.5rem; + padding: 0.5rem; + width: 100%; + + .suggested-question-icon { + padding-right: 0.25rem + } + + .suggested-question-text { + margin-left: 0.25rem; + overflow: hidden; + text-overflow: ellipsis; + word-wrap: break-word; + width: 100%; + max-width: 180px; + } + } +} + +.close-button-container { + display: none !important; +} + +.expand-close-button-container { + display: block !important; + + .close-button { + margin-top: -10px !important; + } +} + +.card-heading { + padding-top: 2.5%; + font-size: 17px; + color: #1F1F1F; + width: 90%; +} + +.card-content { + font-size: 13px; + color: #646566; + padding-top: 4%; + white-space: break-spaces; + text-wrap: balance; + padding-right: 3%; + text-align: left; + width: 85%; +} + +::ng-deep .mat-mdc-snack-bar-container { + &.green-snackbar { + --mdc-snackbar-container-color: #0F9D58; + --mat-mdc-snack-bar-button-color: #fff; + --mdc-snackbar-supporting-text-color: #fff; + } + + &.red-snackbar { + --mdc-snackbar-container-color: #e9103f; + --mat-mdc-snack-bar-button-color: #fff; + --mdc-snackbar-supporting-text-color: #fff; + } +} + +::ng-deep .mat-mdc-dialog-surface { + justify-content: center !important; + display: flex !important; + flex-direction: column !important; + text-align: center !important; + background: #FFFFFF; + border-radius: 20px !important; + box-sizing: border-box; + box-shadow: none !important; + + .badge-prompt-parent-container { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + text-align: center; + + .badge-greetings-container { + width: 100%; + display: flex; + flex-direction: row; + justify-content: center; + + img { + width: 45%; + } + } + + .badge-text-container { + font-style: normal; + font-weight: 400; + font-size: 17px; + line-height: 45px; + display: flex; + align-items: center; + color: rgba(92, 95, 97, 0.7); + } + } +} + +.blue-dot { + /* Ellipse 1226 */ + content: ''; + width: 8px; + height: 8px; + left: 1503px; + top: 445px; + display: flow-root; + border-radius: 15px; + + /* Blue */ + background: #4285F4; + animation: blue-transform 2s infinite; + +} + +.green-dot { + content: ''; + width:8px; + height: 8px; + left: 1522px; + top: 445px; + display: flow-root; + border-radius: 15px; + + /* Green */ + background: #0F9D58; + animation: green-transform 2s infinite; +} + +.yellow-dot { + content: ''; + width: 8px; + height: 8px; + left: 1541px; + top: 445px; + display: flow-root; + border-radius: 15px; + + /* Yellow */ + background: #F4B400; + animation: yellow-transform 2s infinite; + +} + +.recording-container { + display: flex; + flex-direction: row; + gap: 10px; + cursor: pointer; + align-items: baseline; +} + + +@keyframes blue-transform { + 0% { + transform: translateY(-14px) + } + + 33% { + transform: translateY(0) + } + + 66% { + transform: translateY(0) + } + + 100% { + transform: translateY(-14px) + } + +} + +@keyframes green-transform { + 0% { + transform: translateY(0) + } + + 33% { + transform: translateY(-14px) + } + + 66% { + transform: translateY(0) + } + + 100% { + transform: translateY(0) + } + +} + +@keyframes yellow-transform { + 0% { + transform: translateY(0) + } + + 33% { + transform: translateY(0) + } + + 66% { + transform: translateY(-14px) + } + + 100% { + transform: translateY(0) + } + +} + +.tooltip-container { + display: flex; + width: 100%; + justify-content: center; + align-items: baseline; + padding-top: 2%; + padding-bottom: 2%; + .tooltip-sub-container { + width: 66%; + display: flex; + align-items: baseline; + justify-content: center; + } +} + +.tooltip-icon-container { + display: flex; + flex-direction: row; + // align-self: flex-end; + gap: 10px; + flex-wrap: nowrap; + text-wrap: nowrap; + align-self: center; + color: #5F6368; + + .tooltip-label { + font-family: 'Roboto'; + font-style: normal; + font-weight: 500; + font-size: 16px; + line-height: 20px; + text-align: center; + } + + mat-icon { + font-size: 22px !important; + } +} + +.tooltip-text-container { + flex-wrap: wrap; + word-wrap: break-word; + justify-content: flex-start; + display: flex; + color: #5F6368; + .tooltip-text { + font-family: 'Roboto'; + font-style: normal; + font-weight: 400; + font-size: 16px; + line-height: 20px; + } +} + + +.dismiss-container { + font-family: 'Roboto'; + font-style: normal; + font-weight: 500; + font-size: 14px; + line-height: 20px; + /* identical to box height, or 143% */ + text-align: center; + + /* Blue */ + color: #4285F4; + padding-left: 1%; + cursor: pointer; +} + +.dismiss-container:hover { + text-decoration: underline; +} diff --git a/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/app/components/main/main.component.spec.ts b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/app/components/main/main.component.spec.ts new file mode 100644 index 0000000..9c5ca32 --- /dev/null +++ b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/app/components/main/main.component.spec.ts @@ -0,0 +1,39 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { MainComponent } from './main.component'; + +describe('MainComponent', () => { + let component: MainComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ MainComponent ] + }) + .compileComponents(); + + fixture = TestBed.createComponent(MainComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/app/components/main/main.component.ts b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/app/components/main/main.component.ts new file mode 100644 index 0000000..50c1a4e --- /dev/null +++ b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/app/components/main/main.component.ts @@ -0,0 +1,245 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { IntentDetails, IntentService } from './../../services/intent.service'; +import { Component, TemplateRef, ViewChild } from '@angular/core'; +import { Router } from '@angular/router'; +import { BroadcastService } from 'src/app/services/broadcast.service'; +import { UntypedFormGroup, UntypedFormBuilder } from '@angular/forms'; +import { UserService } from 'src/app/services/user/user.service'; +import { Message } from 'src/app/models/messegeType.model'; +import { SessionService } from 'src/app/services/user/session.service'; +import { MatDialog } from '@angular/material/dialog'; +import { ReplaySubject } from 'rxjs'; +import { MatSnackBar } from '@angular/material/snack-bar'; +import { animate, sequence, state, style, transition, trigger } from '@angular/animations'; +import { SpeechToTextService } from '../../services/speech-to-text'; + +@Component({ + selector: 'app-main', + templateUrl: './main.component.html', + styleUrls: ['./main.component.scss'], + animations: [ + trigger('scale', [ + state('true', style({ transform: 'translateY(0)', color: '#4285F4' })), + transition('* => true', [ + sequence([ + style({ transform: 'translateY(0)' }), + animate("450ms cubic-bezier(0,0,0,1)", style({ transform: 'scale(0.8)', color: '#4285F4' })), + animate("400ms cubic-bezier(1,0,1,1)", style({ transform: 'scale(1.2)', color: '#4285F4' })), + animate("350ms cubic-bezier(1,0,1,1)", style({ transform: 'scale(0.8)', color: '#4285F4' })), + animate("250ms cubic-bezier(0,0,0,1)", style({ transform: 'scale(1)', color: '#4285F4' })), + animate("200ms cubic-bezier(0,0,0,1)", style({ transform: 'scale(-1,1)', color: '#4285F4' })), + animate("150ms cubic-bezier(1,0,1,1)", style({ transform: 'scale(-1,1)', color: '#4285F4' })), + ]), + ]) + ]) + ] +}) +export class MainComponent { + isRecording = false; + transcribedText = ''; + mediaRecorder: MediaRecorder; + audioChunks: Blob[] = []; + + searchForm: UntypedFormGroup + selectedType: string = 'chat'; + chatQuery: string = ''; + chipSelected: string = ''; + allQuestions: Map = new Map() + onHover: boolean = false; + savedUser; + lastExpandedElement: string = ''; + showTos = false; + showBadge = false; + tooltipTextList: string[] = []; + + @ViewChild('userBadgeTemplate', { static: true }) + userBadgeTemplate!: TemplateRef<{}>; + + intentSelected: boolean; + intents: IntentDetails[] = []; + dialogRef: any; + + private readonly destroyed = new ReplaySubject(1); + toolTipText: string | undefined; + tooltipTextTimeout: undefined | ReturnType;; + + constructor(private router: Router, + private broadcastService: BroadcastService, + private fb: UntypedFormBuilder, + private sessionService: SessionService, + public userService: UserService, + private intentsService: IntentService, + public dialog: MatDialog, + private _snackBar: MatSnackBar, + private speechToTextService: SpeechToTextService, + ) { + this.intentsService.getAllIntent().subscribe(response => { + this.intents = response.filter((i) => i.status === "5"); + }); + this.searchForm = this.fb.group({ + searchTerm: this.fb.control('') + }); + this.savedUser = userService.getUserDetails(); + this.sessionService.createSession(); + this.setTimeoutForToolTipText(); + } + + navigate() { + let userMessage: Message = { + contentParts: [{'type': 'text', 'text': this.chatQuery}], + type: 'user', + shareable: false, + } + this.chatQuery && this.broadcastService.nextChatQuery(userMessage); + this.router.navigateByUrl('/' + this.selectedType); + }; + + changeSelectedAssistance(assistantType: string) { + this.selectedType = assistantType; + } + + chipControlOnSelect(intent: IntentDetails) { + let queryIntent = intent.name; + this.chipSelected = queryIntent; + } + + removeIntentSelection() { + this.chipSelected = ''; + } + + assignQToChatQuery(question: string) { + this.chatQuery = question; + let userMessage: Message = { + contentParts: [{'type': 'text', 'text': this.chatQuery}], + type: 'user', + shareable: false, + } + this.chatQuery && this.broadcastService.nextChatQuery(userMessage); + this.router.navigateByUrl('/' + this.selectedType); + } + + showFullButton() { + this.onHover = true; + } + hideFullButton() { + this.onHover = false; + } + + expandIntentContainer(intent: IntentDetails) { + let classNameToFilterElement = intent.name; + this.chipControlOnSelect(intent); + if (this.lastExpandedElement != '') { + document.getElementsByClassName(this.lastExpandedElement)[0]?.classList.add('intent-container-box'); + document.getElementsByClassName(this.lastExpandedElement)[0]?.classList.remove('selected-intent-box'); + document.getElementsByClassName(this.lastExpandedElement + "_close_button_container")[0]?.classList.remove('expand-close-button-container') + document.getElementsByClassName(this.lastExpandedElement + "_suggested_questions_container")[0]?.classList.remove('selected-intent-suggested-question'); + } + if (this.lastExpandedElement == classNameToFilterElement) { + document.getElementsByClassName(this.lastExpandedElement)[0]?.classList.remove('selected-intent-box'); + document.getElementsByClassName(classNameToFilterElement + "_close_button_container")[0]?.classList.remove('expand-close-button-container') + document.getElementsByClassName(this.lastExpandedElement + "_suggested_questions_container")[0]?.classList.remove('selected-intent-suggested-question'); + this.lastExpandedElement = ''; + return; + } + this.lastExpandedElement = classNameToFilterElement; + const elementToExpand = document.getElementsByClassName(classNameToFilterElement); + elementToExpand[0]?.classList.remove('intent-container-box'); + elementToExpand[0]?.classList.add('selected-intent-box'); + const suggestedQuestionElement = document.getElementsByClassName(classNameToFilterElement + "_suggested_questions_container"); + suggestedQuestionElement[0]?.classList.add('selected-intent-suggested-question'); + const closeButtonElement = document.getElementsByClassName(classNameToFilterElement + "_close_button_container"); + closeButtonElement[0]?.classList.add('expand-close-button-container'); + + setTimeout(() => { this.scrollToSelectedElement(classNameToFilterElement) }, 100); + + return; + } + + getHumanReadablestring(s: string) { + return s.replace("_", " ").replace(/(^\w{1})|(\s+\w{1})/g, letter => letter.toUpperCase()); + } + + scrollToSelectedElement(classNameToFilterElement: string) { + const childElement = document.getElementById(classNameToFilterElement); + childElement?.scrollIntoView(); + } + + ngOnDestroy() { + this.destroyed.next(); + this.destroyed.complete(); + } + + openSnackBar(message: string, color: string) { + this._snackBar.open(message, 'Close', { + panelClass: [color], + horizontalPosition: 'end', + verticalPosition: 'top', + duration: 3000, + }); + } + + ngOnInit() { + navigator.mediaDevices.getUserMedia({ audio: true }) + .then(stream => this.setupMediaRecorder(stream)); + } + + setupMediaRecorder(stream: MediaStream) { + this.mediaRecorder = new MediaRecorder(stream); + this.mediaRecorder.ondataavailable = event => this.audioChunks.push(event.data); + this.mediaRecorder.onstop = () => this.sendAudioToGCP(); + } + + startRecording() { + this.isRecording = true; + this.audioChunks = []; + this.mediaRecorder.start(); + } + + stopRecording() { + this.isRecording = false; + this.mediaRecorder.stop(); + } + + async sendAudioToGCP() { + const audioBlob = new Blob(this.audioChunks); + // console.log(audioBlob); + (await this.speechToTextService.transcribeAudio(audioBlob)).subscribe( + (response: any) => { + // console.log(response) + this.chatQuery = response[0] + }, + (error: any) => { + // Handle errors + } + ); + } + + setTimeoutForToolTipText() { + if (!window.localStorage['showTooltip']) { + this.toolTipText = this.tooltipTextList[Math.floor(Math.random() * this.tooltipTextList.length)]; + this.tooltipTextTimeout = setInterval(() => { this.toolTipText = this.tooltipTextList[Math.floor(Math.random() * this.tooltipTextList.length)]; }, 7000); + } + } + + dismissToolTip() { + window.localStorage['showTooltip'] = true; + this.toolTipText = undefined; + clearTimeout(this.tooltipTextTimeout); + } + +} diff --git a/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/app/components/shared/toast-message/toast-message.component.html b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/app/components/shared/toast-message/toast-message.component.html new file mode 100644 index 0000000..c34f11f --- /dev/null +++ b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/app/components/shared/toast-message/toast-message.component.html @@ -0,0 +1,22 @@ + + +
    + icon inside toast message + {{text}} + cross icon inside toast message +
    \ No newline at end of file diff --git a/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/app/components/shared/toast-message/toast-message.component.scss b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/app/components/shared/toast-message/toast-message.component.scss new file mode 100644 index 0000000..2bcdf85 --- /dev/null +++ b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/app/components/shared/toast-message/toast-message.component.scss @@ -0,0 +1,37 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +.toast-content{ + color: #FFFFFF; + font-family: 'Google Sans', sans-serif !important; +} + +.close-toast{ + margin-left: auto; + align-self: baseline; + cursor: pointer; +} + +.red-toast { + background-color: #ED0C0C !important; +} +.green-toast { + background-color: #06865e !important; +} + +.mat-mdc-snack-bar-container .mdc-snackbar__surface{ + background-color: transparent !important; +} \ No newline at end of file diff --git a/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/app/components/shared/toast-message/toast-message.component.spec.ts b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/app/components/shared/toast-message/toast-message.component.spec.ts new file mode 100644 index 0000000..f9492a8 --- /dev/null +++ b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/app/components/shared/toast-message/toast-message.component.spec.ts @@ -0,0 +1,39 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ToastMessageComponent } from './toast-message.component'; + +describe('ToastMessageComponent', () => { + let component: ToastMessageComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ ToastMessageComponent ] + }) + .compileComponents(); + + fixture = TestBed.createComponent(ToastMessageComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/app/components/shared/toast-message/toast-message.component.ts b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/app/components/shared/toast-message/toast-message.component.ts new file mode 100644 index 0000000..a9bdcb8 --- /dev/null +++ b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/app/components/shared/toast-message/toast-message.component.ts @@ -0,0 +1,42 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Component, Inject,ViewEncapsulation } from '@angular/core'; +import {MatSnackBar, MAT_SNACK_BAR_DATA } from '@angular/material/snack-bar'; + +@Component({ + selector: 'app-toast-message', + templateUrl: './toast-message.component.html', + styleUrls: ['./toast-message.component.scss'], + encapsulation: ViewEncapsulation.None, +}) +export class ToastMessageComponent { + + text: string + icon: string + constructor( + private _snackBar: MatSnackBar, + @Inject(MAT_SNACK_BAR_DATA) public snackBarData: any + ) { + this.text = snackBarData.text + this.icon = snackBarData.icon + } + + closeToast() { + this._snackBar.dismiss() + } + +} \ No newline at end of file diff --git a/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/app/dialogue-box/dialogue-box.component.html b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/app/dialogue-box/dialogue-box.component.html new file mode 100644 index 0000000..40ce5f5 --- /dev/null +++ b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/app/dialogue-box/dialogue-box.component.html @@ -0,0 +1,44 @@ + + + +
    +
    +
    + + {{data?.title}} + +
    +
    + {{data?.icon}} +
    +
    +
    + {{data?.description}} +
    +
    +
    + + +
    +
    +
    diff --git a/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/app/dialogue-box/dialogue-box.component.scss b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/app/dialogue-box/dialogue-box.component.scss new file mode 100644 index 0000000..78cd436 --- /dev/null +++ b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/app/dialogue-box/dialogue-box.component.scss @@ -0,0 +1,204 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +.forgot-password-container { + display: flex; + flex-direction: column; + text-align: center; + padding: 4%; +} + +.forgot-password-icon-container { + width: 100%; + height: 100%; + + mat-icon{ + font-size: 105px; + height: 100%; + width: 100%; + padding: 1%; + padding-top: 7%; + color: #3467bb; + } +} + +.forgot-password-heading-container { + font-size: 29px; + font-weight: 600; + font-family: 'Google Sans', sans-serif !important; + line-height: 2.5em; +} + +.forgot-password-subtext-container{ + display: flex; + flex-direction: column; + text-align: center; + font-size: 19px; + gap: 10px; + line-height: 1.5em; + + mat-icon{ + color: #4385f4; + } +} + +.logo-icon { + height: 170px; + width: 100%; +} + +.login-button { + width: 25%; + background: #4285F4; + box-sizing: border-box; + border: 1px solid #e0e0e0; + color: #fff; + position: relative; + user-select: none; + cursor: pointer; + outline: none; + -webkit-tap-highlight-color: transparent; + display: inline-block; + font-family: 'Google Sans', sans-serif !important; + font-size: 14px; + font-weight: 500; + white-space: nowrap; + text-decoration: none; + vertical-align: baseline; + text-align: center; + margin: 0; + min-width: 64px; + line-height: 44px; + padding: 0 16px; + border-radius: 4px; + overflow: visible; + transform: translate3d(0, 0, 0); + transition: background 400ms cubic-bezier(0.25, 0.8, 0.25, 1), box-shadow 280ms cubic-bezier(0.4, 0, 0.2, 1); +} + +.register-text { + display: flex; + flex-direction: column; + align-items: flex-start; + margin-bottom: 0.3em; + font-weight: 400; + color: #5C5F61 +} + +.ok-button { + width: 25%; + background: white; + box-sizing: border-box; + border: 1px solid #4285F4; + color: #4285F4; + position: relative; + user-select: none; + cursor: pointer; + outline: none; + -webkit-tap-highlight-color: transparent; + display: inline-block; + font-family: 'Google Sans', sans-serif !important; + font-size: 14px; + font-weight: 500; + white-space: nowrap; + text-decoration: none; + vertical-align: baseline; + text-align: center; + margin: 0; + min-width: 64px; + line-height: 44px; + padding: 0 16px; + border-radius: 4px; + overflow: visible; + transform: translate3d(0, 0, 0); + transition: background 400ms cubic-bezier(0.25, 0.8, 0.25, 1), box-shadow 280ms cubic-bezier(0.4, 0, 0.2, 1); +} + +.close-button { + float: right !important; +} + +.forgot-password-link-container { + width: 100%; + text-align: end; + display: flex; + flex-direction: column; + padding-bottom: 4%; + z-index: 100; + position: relative; +} + +.button-css { + display: flex; + justify-content: center; + gap: 2%; +} + +.icon-css { + font-size: 40px; + color: #f3f30aed +} + +.text-css { + font-size: 15px; + line-height: 1.45em; + font-family: "Google Sans", sans-serif !important; + color: #645a5a; +} + +.logo-container { + display: flex; + flex-direction: row; + justify-content: center; + text-align: center; + mat-icon{ + height: 20% !important; + width: 100% !important; + font-size: 70px !important; + color: #4285F4 !important; + } +} + +.heading-span { + display: flex; + flex-direction: row; + gap: 8px; + align-items: center; + padding: 2%; + width: 100%; + justify-content: center; + font-size: 18px; + font-weight: 700; + padding-left: 15%; + color: #4285F4 !important; +} + +.heading-container { + display: flex; + flex-direction: row; + justify-content: center; + background: aliceblue; +} + +.sub-parent-container { + display: flex; + flex-direction: column; + text-align: center; + gap: 15px; + font-style: normal; + line-height: normal; + font-family: 'Google Sans', sans-serif !important; +} diff --git a/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/app/dialogue-box/dialogue-box.component.spec.ts b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/app/dialogue-box/dialogue-box.component.spec.ts new file mode 100644 index 0000000..3cbe873 --- /dev/null +++ b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/app/dialogue-box/dialogue-box.component.spec.ts @@ -0,0 +1,39 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { DialogueBoxComponent } from './dialogue-box.component'; + +describe('DialogueBoxComponent', () => { + let component: DialogueBoxComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ DialogueBoxComponent ] + }) + .compileComponents(); + + fixture = TestBed.createComponent(DialogueBoxComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/app/dialogue-box/dialogue-box.component.ts b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/app/dialogue-box/dialogue-box.component.ts new file mode 100644 index 0000000..2cdb410 --- /dev/null +++ b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/app/dialogue-box/dialogue-box.component.ts @@ -0,0 +1,60 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Component, Inject } from '@angular/core'; +import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog'; + + +@Component({ + selector: 'app-dialogue-box', + templateUrl: './dialogue-box.component.html', + styleUrls: ['./dialogue-box.component.scss'] +}) +export class DialogueBoxComponent { + data: any; + + constructor( + private dialogRef: MatDialogRef, + @Inject(MAT_DIALOG_DATA) data: { title: string, + icon: string, + description: string, + cancelBtnText?: string, + showConfirmBtn?:boolean, + cancelCallBackFunction?: ()=>{}, + confirmBtnText?: string, + confirmCallBackFunction?: ()=>{}, + needsExtraProcessing: boolean, + extraProcessingFunction?: ()=>{} + } + ) { + this.data = data; + if(this.data.needsExtraProcessing){ + this.data?.extraProcessingFunction(); + } + + } + + getCallBackFunction( callback : ()=>{} | undefined){ + + let returnFunction = ()=>{this.dialogRef.close()}; + if(typeof callback === "function"){ + returnFunction = ()=>{callback();this.dialogRef.close()}; + } + return returnFunction(); + + } + +} diff --git a/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/app/format-agent-name.pipe.spec.ts b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/app/format-agent-name.pipe.spec.ts new file mode 100644 index 0000000..cccdf0b --- /dev/null +++ b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/app/format-agent-name.pipe.spec.ts @@ -0,0 +1,24 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { FormatAgentNamePipe } from './format-agent-name.pipe'; + +describe('FormatAgentNamePipe', () => { + it('create an instance', () => { + const pipe = new FormatAgentNamePipe(); + expect(pipe).toBeTruthy(); + }); +}); diff --git a/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/app/format-agent-name.pipe.ts b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/app/format-agent-name.pipe.ts new file mode 100644 index 0000000..fa43879 --- /dev/null +++ b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/app/format-agent-name.pipe.ts @@ -0,0 +1,34 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Pipe, PipeTransform } from '@angular/core'; + +@Pipe({ + name: 'formatAgentName' +}) +export class FormatAgentNamePipe implements PipeTransform { + + transform(value: string | undefined | null, ...args: unknown[]): string { + if (!value || typeof value !== 'string') { + // Return a default or empty string if value is not as expected + return 'Unknown Agent'; + } + return value + .split('_') + .map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()) + .join(' '); + } +} diff --git a/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/app/models/chat.model.ts b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/app/models/chat.model.ts new file mode 100644 index 0000000..351d7e5 --- /dev/null +++ b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/app/models/chat.model.ts @@ -0,0 +1,37 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export type CreateChatRequest = { + text: string, + chat_id?: string +} + +export type Chat = { + id: string, + question: string, + answer: string, + intent: string, + suggested_questions: string[], +} + +export type DialogQuestion = { + questionId: string, + questionText: string, + hasChip: boolean, + options: string[], + questionSequence: string, + answer: string +} diff --git a/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/app/models/messegeType.model.ts b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/app/models/messegeType.model.ts new file mode 100644 index 0000000..c3a0b10 --- /dev/null +++ b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/app/models/messegeType.model.ts @@ -0,0 +1,50 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export interface ThinkingStep { + type: 'functionCall' | 'functionResponse'; + name: string; + data: any; // For args in functionCall, or response in functionResponse +} + +// New interface to represent a part of the message content +export interface MessageContentPart { + type: 'text' | 'thinking'; + text?: string; // For 'text' type + thinkingSteps?: ThinkingStep[]; // For 'thinking' type +} + +export interface Message { + // body: string; // We might deprecate this or use it as a summary. For now, contentParts is primary. + contentParts: MessageContentPart[]; // Array to hold the sequence of text and thinking blocks + type: string; + responseTime?: string; + shareable: boolean; + categoryIntent?: string; + extras?: Extras; + suggestedQuestion?: string[]; + botStartTime?: string; +} + +export type Extras = { + like: boolean; + dislike: boolean; + delete?: boolean; +}; + +export interface SuggestionData { + suggestedQuestion: string[]; +} diff --git a/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/app/services/broadcast.service.spec.ts b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/app/services/broadcast.service.spec.ts new file mode 100644 index 0000000..39f3aab --- /dev/null +++ b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/app/services/broadcast.service.spec.ts @@ -0,0 +1,32 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { TestBed } from '@angular/core/testing'; + +import { BroadcastService } from './broadcast.service'; + +describe('BroadcastService', () => { + let service: BroadcastService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(BroadcastService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/app/services/broadcast.service.ts b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/app/services/broadcast.service.ts new file mode 100644 index 0000000..d6d6191 --- /dev/null +++ b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/app/services/broadcast.service.ts @@ -0,0 +1,35 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Injectable } from '@angular/core'; +import { BehaviorSubject } from 'rxjs'; +import { Message } from '../models/messegeType.model'; + +@Injectable({ + providedIn: 'root' +}) +export class BroadcastService { + initialChatQuery = ''; + chatQuery$: BehaviorSubject; + + constructor() { + this.chatQuery$ = new BehaviorSubject({'type': 'bot', 'contentParts': [{'text':'Ask me anything and start your journey! 🚀🚀🚀','type':'text'}], shareable: false,}); + } + + nextChatQuery(chatQuery: Message) { + this.chatQuery$.next(chatQuery); + } +} diff --git a/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/app/services/chat.service.spec.ts b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/app/services/chat.service.spec.ts new file mode 100644 index 0000000..ef16d44 --- /dev/null +++ b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/app/services/chat.service.spec.ts @@ -0,0 +1,32 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { TestBed } from '@angular/core/testing'; + +import { ChatService } from './chat.service'; + +describe('ChatService', () => { + let service: ChatService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(ChatService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/app/services/chat.service.ts b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/app/services/chat.service.ts new file mode 100644 index 0000000..bef850a --- /dev/null +++ b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/app/services/chat.service.ts @@ -0,0 +1,236 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Injectable, OnDestroy } from '@angular/core'; +import { Subject, Observable, BehaviorSubject, Subscription } from 'rxjs'; +import { environment } from 'src/environments/environment'; + +// Define a type for messages from the server for better type safety +export interface ServerMessage { + operation?: 'start' | 'end_of_turn' | 'fatal_error' | 'error_and_close'; + answer?: any; + intent?: string; + suggested_questions?: any[]; + error?: string; + [key: string]: any; +} + +export type ConnectionStatus = 'disconnected' | 'connecting' | 'connected' | 'error' | 'reconnecting' | 'closed_by_server' | 'closed_by_client' | 'aborted' | 'processing_complete'; + +/** + * Helper function to convert HTTP/HTTPS URL to WS/WSS URL. + */ +function getWebSocketUrl(httpUrl: string | undefined): string { + const defaultWsUrl = "ws://localhost:8000/api/chats"; + if (!httpUrl) { + console.warn("environment.backendURL is not defined, falling back to default WebSocket URL:", defaultWsUrl); + return defaultWsUrl; + } + + let wsUrl = httpUrl; + if (httpUrl.startsWith('https://')) { + wsUrl = 'wss://' + httpUrl.substring('https://'.length); + } else if (httpUrl.startsWith('http://')) { + wsUrl = 'ws://' + httpUrl.substring('http://'.length); + } else { + console.warn(`backendURL "${httpUrl}" does not have a standard http/https protocol prefix. Assuming ws:// and appending /chats`); + wsUrl = 'ws://' + httpUrl; + } + + const chatsApiPath = "/chats"; + if (wsUrl.endsWith('/')) { + wsUrl = wsUrl.slice(0, -1); + } + if (!wsUrl.endsWith(chatsApiPath)) { + if (wsUrl.includes('/api/') && !wsUrl.endsWith('/chats')) { + wsUrl = wsUrl + (wsUrl.endsWith('/') ? 'chats' : 'chats'); + } else if (!wsUrl.includes('/api/')) { + wsUrl = wsUrl + chatsApiPath; + } + } + console.log("Constructed WebSocket URL:", wsUrl); + return wsUrl; +} + + +@Injectable({ + providedIn: 'root' +}) +export class ChatService implements OnDestroy { + private BASE_SOCKET_URL = getWebSocketUrl(environment.backendURL); + private websocket: WebSocket | null = null; + private serviceSubscriptions: Subscription = new Subscription(); + + private messageSubject = new Subject(); + private connectionStatusSubject = new BehaviorSubject('disconnected'); + + constructor() { + console.log("ChatService (WebSocket) instantiated with URL:", this.BASE_SOCKET_URL); + // Optionally, you could call connect() here if the service should always + // try to connect upon instantiation. Or, let the consuming component call it. + // this.connect(); + } + + public getMessages(): Observable { + return this.messageSubject.asObservable(); + } + + public getConnectionStatus(): Observable { + return this.connectionStatusSubject.asObservable(); + } + + /** + * Establishes a WebSocket connection if not already connected or connecting. + */ + public connect(): void { + if (this.websocket && (this.websocket.readyState === WebSocket.OPEN || this.websocket.readyState === WebSocket.CONNECTING)) { + console.warn("connect() called while WebSocket is already open or connecting."); + return; + } + + this.connectionStatusSubject.next('connecting'); + const socketUrl = this.BASE_SOCKET_URL; + console.log("Attempting to connect to WebSocket at:", socketUrl); + + try { + this.websocket = new WebSocket(socketUrl); + } catch (error) { + console.error("Failed to create WebSocket:", error); + this.connectionStatusSubject.next('error'); + this.messageSubject.error(error); + return; + } + + this.websocket.onopen = (event) => { + console.log("WebSocket connection established:", event); + this.connectionStatusSubject.next('connected'); + // No initial message sent automatically on open anymore. + // The backend will send {"operation": "start"} after creating a session. + }; + + this.websocket.onmessage = (event) => { + try { + const serverMessage: ServerMessage = JSON.parse(event.data as string); + console.log("Message from server: ", serverMessage); + this.messageSubject.next(serverMessage); + + if (serverMessage.operation === "end_of_turn") { + console.log("Server indicated end of turn."); + this.connectionStatusSubject.next('processing_complete'); + } else if (serverMessage.operation === "start") { + console.log("Server indicated session start."); + // 'connected' status is already set by onopen. + // If 'start' implies the bot is immediately ready for input after connection, + // you could also set to 'processing_complete' here, or a new 'ready' status. + // For now, 'connected' means the channel is open. + } else if (serverMessage.operation === "fatal_error" || serverMessage.operation === "error_and_close") { + console.error("Server indicated a fatal error or an error requiring close:", serverMessage.error); + this.connectionStatusSubject.next('error'); + } + } catch (e) { + console.error("Error parsing JSON message from server or received non-JSON message:", event.data, e); + this.messageSubject.error(new Error(`Failed to parse server message: ${event.data}`)); + this.connectionStatusSubject.next('error'); + } + }; + + this.websocket.onerror = (event) => { + console.error("WebSocket error observed:", event); + this.connectionStatusSubject.next('error'); + this.messageSubject.error(event); + }; + + this.websocket.onclose = (event) => { + console.log("WebSocket connection closed:", event); + const currentStatus = this.connectionStatusSubject.value; + if (event.wasClean) { + console.log(`Connection closed cleanly, code=${event.code} reason=${event.reason}`); + if (currentStatus !== 'closed_by_server' && currentStatus !== 'closed_by_client' && currentStatus !== 'aborted') { + this.connectionStatusSubject.next('disconnected'); + } + } else { + console.error('Connection died unexpectedly. Code:', event.code, 'Reason:', event.reason); + if (currentStatus !== 'error' && currentStatus !== 'aborted') { + this.connectionStatusSubject.next('error'); + } + } + this.websocket = null; + }; + } + + /** + * Sends a message (query) over the existing WebSocket connection. + * @param text - The text for the message. + */ + public sendMessage(text: string): void { + if (this.websocket && this.websocket.readyState === WebSocket.OPEN) { + const message: { text: string } = { text: text }; // No chat_id needed for WS messages + try { + this.websocket.send(JSON.stringify(message)); + console.log("Sent WebSocket message:", message); + // If status was 'processing_complete', it's now back to 'connected' (actively communicating/waiting for response) + if(this.connectionStatusSubject.value !== 'connected') { + this.connectionStatusSubject.next('connected'); + } + } catch (e) { + console.error("Error sending WebSocket message:", e); + this.messageSubject.error(e); + this.connectionStatusSubject.next('error'); + } + } else { + console.error("WebSocket not connected or not open. Cannot send message. Current state:", this.websocket?.readyState); + this.connectionStatusSubject.next('error'); // Or 'disconnected' + this.messageSubject.error(new Error("Attempted to send message on non-open WebSocket.")); + // Optionally, try to reconnect if the socket is not open + // if (!this.websocket || this.websocket.readyState === WebSocket.CLOSED) { + // console.log("Attempting to reconnect and send message..."); + // this.connect(); // This will establish connection + // // You might need a mechanism to queue the message and send it once connected. + // // For simplicity now, it just errors out. + // } + } + } + + public close(reason: string = "Client initiated disconnect"): void { + if (this.websocket) { + const currentState = this.websocket.readyState; + if (currentState === WebSocket.OPEN || currentState === WebSocket.CONNECTING) { + console.log(`Closing WebSocket connection (state: ${currentState === WebSocket.OPEN ? 'OPEN' : 'CONNECTING'}). Reason: ${reason}`); + if (currentState === WebSocket.CONNECTING) { + this.connectionStatusSubject.next('aborted'); + } else { + this.connectionStatusSubject.next('closed_by_client'); + } + this.websocket.close(1000, reason); + } else { + console.log(`WebSocket is not OPEN or CONNECTING (state: ${currentState}). No action taken by close().`); + } + } else { + console.log("WebSocket instance is null. No connection to close."); + if (this.connectionStatusSubject.value !== 'disconnected' && this.connectionStatusSubject.value !== 'error') { + this.connectionStatusSubject.next('disconnected'); + } + } + } + + ngOnDestroy(): void { + console.log("ChatService ngOnDestroy called."); + this.close("ChatService destroyed by Angular"); + this.messageSubject.complete(); + this.connectionStatusSubject.complete(); + this.serviceSubscriptions.unsubscribe(); + } +} diff --git a/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/app/services/intent.service.spec.ts b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/app/services/intent.service.spec.ts new file mode 100644 index 0000000..860dde0 --- /dev/null +++ b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/app/services/intent.service.spec.ts @@ -0,0 +1,32 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { TestBed } from '@angular/core/testing'; + +import { IntentService } from './intent.service'; + +describe('IntentService', () => { + let service: IntentService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(IntentService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/app/services/intent.service.ts b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/app/services/intent.service.ts new file mode 100644 index 0000000..15c3946 --- /dev/null +++ b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/app/services/intent.service.ts @@ -0,0 +1,104 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Injectable } from '@angular/core'; +import { environment } from 'src/environments/environment'; +import { HttpClient } from '@angular/common/http'; + +const intentURL = `${environment.backendURL}/questions/intents`; +const manageIntentURL = `${environment.backendURL}/intents`; + +export type Intent = { + id?: string, + question: string, + intent: string, + author: string, + timestamp?: string, +} + +export interface IntentDetails { + name: string, + gcp_bucket?: string, + ai_model: string, + ai_temperature: string, + description: string, + prompt: string, + questions: string[], + status: string, +} + +export interface Model { + name: string, + models: string, +} + +export type GetChunksRequest = { + question: string, + intent?: string, +} + +export interface IntentChunk { + intent?: string; + suggested_question?:string[]; + information_chunks?: Chunk[]; +} + +export interface Chunk { + id?:string; + distance?:string; + content?:string; +} + + +@Injectable({ + providedIn: 'root' +}) +export class IntentService { + + constructor(private http: HttpClient) { } + + getAllQuestion(){ + return this.http.get(intentURL); + } + + addQuestion(intent: Intent){ + return this.http.post(intentURL, intent); + } + + deleteQuestion(question_id: string){ + return this.http.delete(`${intentURL}/${question_id}`); + } + + getIntentChunks(reqObj: GetChunksRequest){ + return this.http.post(`${intentURL}/get_chunks`, reqObj); + } + + saveIntent(reqObj: IntentDetails){ + return this.http.post(manageIntentURL, reqObj); + } + + updateIntent(reqObj: IntentDetails){ + return this.http.put(`${manageIntentURL}/${reqObj.name}`, reqObj); + } + + getAllIntent(){ + return this.http.get>(manageIntentURL); + } + + deleteIntent(name: string){ + return this.http.delete(`${manageIntentURL}/${name}`); + } +} diff --git a/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/app/services/login/auth.guard.spec.ts b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/app/services/login/auth.guard.spec.ts new file mode 100644 index 0000000..d45198d --- /dev/null +++ b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/app/services/login/auth.guard.spec.ts @@ -0,0 +1,32 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { TestBed } from '@angular/core/testing'; + +import { AuthGuard } from './auth.guard'; + +describe('AuthGuard', () => { + let guard: AuthGuard; + + beforeEach(() => { + TestBed.configureTestingModule({}); + guard = TestBed.inject(AuthGuard); + }); + + it('should be created', () => { + expect(guard).toBeTruthy(); + }); +}); diff --git a/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/app/services/login/auth.guard.ts b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/app/services/login/auth.guard.ts new file mode 100644 index 0000000..17e7ad6 --- /dev/null +++ b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/app/services/login/auth.guard.ts @@ -0,0 +1,40 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Injectable } from '@angular/core'; +import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot, UrlTree } from '@angular/router'; +import { Observable } from 'rxjs'; +import { AuthService } from './auth.service'; +import { environment } from 'src/environments/environment'; + +const LOGIN_ROUTE = '/login' + +@Injectable({ + providedIn: 'root' +}) +export class AuthGuard implements CanActivate { + constructor(private auth: AuthService, private router: Router) {} + + canActivate( + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot): Observable | Promise | boolean | UrlTree { + if( environment.requiredLogin == "True" && !this.auth.isLoggedIn()) { + this.router.navigate([LOGIN_ROUTE]); + return false; + } + return true; + } +} diff --git a/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/app/services/login/auth.service.spec.ts b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/app/services/login/auth.service.spec.ts new file mode 100644 index 0000000..de42604 --- /dev/null +++ b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/app/services/login/auth.service.spec.ts @@ -0,0 +1,32 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { TestBed } from '@angular/core/testing'; + +import { AuthService } from './auth.service'; + +describe('AuthService', () => { + let service: AuthService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(AuthService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/app/services/login/auth.service.ts b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/app/services/login/auth.service.ts new file mode 100644 index 0000000..644dbb8 --- /dev/null +++ b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/app/services/login/auth.service.ts @@ -0,0 +1,65 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Injectable } from '@angular/core'; +import { Router } from '@angular/router'; +import { HttpErrorResponse } from "@angular/common/http"; +import { environment } from 'src/environments/environment'; +import { throwError } from "rxjs"; + +const USER_TOKEN_KEY = 'gpau_id' +const USER_DETAILS = 'USER_DETAILS'; +const LOGIN_ROUTE = '/login' + + + +@Injectable({ + providedIn: 'root' +}) +export class AuthService { + + private requiredLogin: boolean = environment.requiredLogin == "True" + + constructor( + private router: Router, + ) { + } + + saveUserSession(token: string) { + localStorage.setItem(USER_TOKEN_KEY, token) + } + + logout(route: string = LOGIN_ROUTE) { + localStorage.removeItem(USER_TOKEN_KEY); + localStorage.removeItem(USER_DETAILS); + localStorage.removeItem('showTooltip') + this.router.navigateByUrl(route); + } + + isLoggedIn() { + var isLoggedIn = localStorage.getItem(USER_TOKEN_KEY) !== null + if (!isLoggedIn && this.router.url !== LOGIN_ROUTE) { + this.router.navigate([LOGIN_ROUTE]) + } + return isLoggedIn; + } + + isUserLoggedIn(){ + if(!this.requiredLogin) return true; + var isLoggedIn = localStorage.getItem(USER_TOKEN_KEY) !== null; + return isLoggedIn; + } +} diff --git a/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/app/services/models.service.spec.ts b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/app/services/models.service.spec.ts new file mode 100644 index 0000000..d54f444 --- /dev/null +++ b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/app/services/models.service.spec.ts @@ -0,0 +1,32 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { TestBed } from '@angular/core/testing'; + +import { ModelsService } from './models.service'; + +describe('ModelsService', () => { + let service: ModelsService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(ModelsService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/app/services/models.service.ts b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/app/services/models.service.ts new file mode 100644 index 0000000..70e376f --- /dev/null +++ b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/app/services/models.service.ts @@ -0,0 +1,48 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { HttpClient } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { environment } from 'src/environments/environment'; +import { Observable, catchError, map, throwError } from 'rxjs'; +import { Model } from './intent.service'; + +const allModels = `${environment.backendURL}/models`; + +@Injectable({ + providedIn: 'root' +}) +export class ModelsService { + + constructor(private http: HttpClient) {} + + getAll(): Observable { + return this.http.get(allModels).pipe( + map(response => { + return response as Model[]; + }), + catchError(this.handleError)); + } + + protected handleError(error: Response) { + if (error.status === 404) { + return throwError(() => new Error()) + } else if (error.status === 400) { + return throwError(() => error) + } + return throwError(() => error); + } +} diff --git a/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/app/services/speech-to-text.ts b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/app/services/speech-to-text.ts new file mode 100644 index 0000000..ade5ea5 --- /dev/null +++ b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/app/services/speech-to-text.ts @@ -0,0 +1,36 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Injectable } from '@angular/core'; +import { HttpClient, HttpHeaders } from '@angular/common/http'; +import { environment } from 'src/environments/environment'; + +// const audioChatUrl = `http://localhost:8080/api/audio_chat`; +const audioChatUrl = `${environment.backendURL}/audio_chat`; + +@Injectable({ + providedIn: 'root' +}) +export class SpeechToTextService { + + constructor(private http: HttpClient) {} + + async transcribeAudio(audioBlob: Blob) { + const formData = new FormData(); + formData.append('audio_file', audioBlob, 'audio.wav'); + return this.http.post(audioChatUrl, formData); + } +} \ No newline at end of file diff --git a/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/app/services/user/session.service.spec.ts b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/app/services/user/session.service.spec.ts new file mode 100644 index 0000000..49619c8 --- /dev/null +++ b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/app/services/user/session.service.spec.ts @@ -0,0 +1,32 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { TestBed } from '@angular/core/testing'; + +import { SessionService } from './session.service'; + +describe('SessionService', () => { + let service: SessionService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(SessionService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/app/services/user/session.service.ts b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/app/services/user/session.service.ts new file mode 100644 index 0000000..0e49a3a --- /dev/null +++ b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/app/services/user/session.service.ts @@ -0,0 +1,36 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Injectable } from '@angular/core'; +import { v4 as uuid } from 'uuid'; + +const SESSION_KEY = "pasid" + +@Injectable({ + providedIn: 'root' +}) +export class SessionService { + + constructor() { } + + getSession(): string | null { + return sessionStorage.getItem(SESSION_KEY); + } + + createSession() { + sessionStorage.setItem(SESSION_KEY, uuid()) + } +} \ No newline at end of file diff --git a/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/app/services/user/user.service.ts b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/app/services/user/user.service.ts new file mode 100644 index 0000000..c135490 --- /dev/null +++ b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/app/services/user/user.service.ts @@ -0,0 +1,54 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Injectable } from '@angular/core'; + +interface LooseObject { + [key:string]: any +} + +type UserStored = { + uid?: string, + name?: string, + email?: string, + photoURL?: string, + displayName?: string, + domain?: string +} + +@Injectable({ + providedIn: 'root' +}) +export class UserService { + + constructor() { } + + setUserDetails(userStored: UserStored) { + + } + + getUserDetails(): UserStored{ + if(localStorage.getItem('USER_DETAILS') != null){ + let userObj = localStorage.getItem("USER_DETAILS") + return JSON.parse(userObj || '{}') + } + else{ + let userDetails: UserStored = {} + return userDetails + } + } + +} diff --git a/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/assets/.gitkeep b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/assets/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/assets/gcloud-logo.svg b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/assets/gcloud-logo.svg new file mode 100644 index 0000000..5150056 --- /dev/null +++ b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/assets/gcloud-logo.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/assets/images/SocialHeaderTemplate8.png b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/assets/images/SocialHeaderTemplate8.png new file mode 100644 index 0000000..88f4306 Binary files /dev/null and b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/assets/images/SocialHeaderTemplate8.png differ diff --git a/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/assets/images/answernotfound.png b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/assets/images/answernotfound.png new file mode 100644 index 0000000..874c87d Binary files /dev/null and b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/assets/images/answernotfound.png differ diff --git a/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/assets/images/attached.svg b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/assets/images/attached.svg new file mode 100644 index 0000000..ba09784 --- /dev/null +++ b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/assets/images/attached.svg @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/assets/images/available.svg b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/assets/images/available.svg new file mode 100644 index 0000000..5a460d0 --- /dev/null +++ b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/assets/images/available.svg @@ -0,0 +1,4 @@ + + + + diff --git a/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/assets/images/badge-banner.svg b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/assets/images/badge-banner.svg new file mode 100644 index 0000000..a38efec --- /dev/null +++ b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/assets/images/badge-banner.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/assets/images/badge.svg b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/assets/images/badge.svg new file mode 100644 index 0000000..3fc6b94 --- /dev/null +++ b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/assets/images/badge.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/assets/images/books.png b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/assets/images/books.png new file mode 100644 index 0000000..94feabc Binary files /dev/null and b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/assets/images/books.png differ diff --git a/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/assets/images/bot_logo.svg b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/assets/images/bot_logo.svg new file mode 100644 index 0000000..4be6c2f --- /dev/null +++ b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/assets/images/bot_logo.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/assets/images/cancel-toast.svg b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/assets/images/cancel-toast.svg new file mode 100644 index 0000000..9db4780 --- /dev/null +++ b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/assets/images/cancel-toast.svg @@ -0,0 +1,4 @@ + + + + diff --git a/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/assets/images/chatIcon.svg b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/assets/images/chatIcon.svg new file mode 100644 index 0000000..3303847 --- /dev/null +++ b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/assets/images/chatIcon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/assets/images/chat_background.png b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/assets/images/chat_background.png new file mode 100644 index 0000000..701ec81 Binary files /dev/null and b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/assets/images/chat_background.png differ diff --git a/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/assets/images/cross-in-circle-white copy.svg b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/assets/images/cross-in-circle-white copy.svg new file mode 100644 index 0000000..96a0fe3 --- /dev/null +++ b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/assets/images/cross-in-circle-white copy.svg @@ -0,0 +1,3 @@ + + + diff --git a/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/assets/images/cross-in-circle-white.svg b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/assets/images/cross-in-circle-white.svg new file mode 100644 index 0000000..96a0fe3 --- /dev/null +++ b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/assets/images/cross-in-circle-white.svg @@ -0,0 +1,3 @@ + + + diff --git a/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/assets/images/cross-in-circle.svg b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/assets/images/cross-in-circle.svg new file mode 100644 index 0000000..c5c50ea --- /dev/null +++ b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/assets/images/cross-in-circle.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/assets/images/delete_notification_dialog_image.png b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/assets/images/delete_notification_dialog_image.png new file mode 100644 index 0000000..5afbf5c Binary files /dev/null and b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/assets/images/delete_notification_dialog_image.png differ diff --git a/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/assets/images/error_img.svg b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/assets/images/error_img.svg new file mode 100644 index 0000000..ac20193 --- /dev/null +++ b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/assets/images/error_img.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/assets/images/feedback-container.svg b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/assets/images/feedback-container.svg new file mode 100644 index 0000000..7b0f481 --- /dev/null +++ b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/assets/images/feedback-container.svg @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/assets/images/footer.png b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/assets/images/footer.png new file mode 100644 index 0000000..eda0b3b Binary files /dev/null and b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/assets/images/footer.png differ diff --git a/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/assets/images/gcloudlight1.png b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/assets/images/gcloudlight1.png new file mode 100644 index 0000000..5ddb77c Binary files /dev/null and b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/assets/images/gcloudlight1.png differ diff --git a/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/assets/images/gnosis-logo.svg b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/assets/images/gnosis-logo.svg new file mode 100644 index 0000000..17d0b7f --- /dev/null +++ b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/assets/images/gnosis-logo.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/assets/images/gsd-icon.png b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/assets/images/gsd-icon.png new file mode 100644 index 0000000..abeacf9 Binary files /dev/null and b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/assets/images/gsd-icon.png differ diff --git a/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/assets/images/home_bottom.png b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/assets/images/home_bottom.png new file mode 100644 index 0000000..78d1c92 Binary files /dev/null and b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/assets/images/home_bottom.png differ diff --git a/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/assets/images/left_loader_images/Left/1.png b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/assets/images/left_loader_images/Left/1.png new file mode 100644 index 0000000..d930ffb Binary files /dev/null and b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/assets/images/left_loader_images/Left/1.png differ diff --git a/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/assets/images/left_loader_images/Left/2.png b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/assets/images/left_loader_images/Left/2.png new file mode 100644 index 0000000..ac32f6a Binary files /dev/null and b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/assets/images/left_loader_images/Left/2.png differ diff --git a/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/assets/images/left_loader_images/Left/3.png b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/assets/images/left_loader_images/Left/3.png new file mode 100644 index 0000000..cee4db0 Binary files /dev/null and b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/assets/images/left_loader_images/Left/3.png differ diff --git a/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/assets/images/left_loader_images/Left/4.png b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/assets/images/left_loader_images/Left/4.png new file mode 100644 index 0000000..77a6d94 Binary files /dev/null and b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/assets/images/left_loader_images/Left/4.png differ diff --git a/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/assets/images/left_loader_images/Left/5.png b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/assets/images/left_loader_images/Left/5.png new file mode 100644 index 0000000..deba49b Binary files /dev/null and b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/assets/images/left_loader_images/Left/5.png differ diff --git a/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/assets/images/left_loader_images/Left/6.png b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/assets/images/left_loader_images/Left/6.png new file mode 100644 index 0000000..61e1988 Binary files /dev/null and b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/assets/images/left_loader_images/Left/6.png differ diff --git a/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/assets/images/login-globe.png b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/assets/images/login-globe.png new file mode 100644 index 0000000..f64021b Binary files /dev/null and b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/assets/images/login-globe.png differ diff --git a/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/assets/images/login_left.svg b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/assets/images/login_left.svg new file mode 100644 index 0000000..397f17f --- /dev/null +++ b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/assets/images/login_left.svg @@ -0,0 +1,101 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/assets/images/new-pdc-logo.svg b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/assets/images/new-pdc-logo.svg new file mode 100644 index 0000000..c267930 --- /dev/null +++ b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/assets/images/new-pdc-logo.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/assets/images/noPic.svg b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/assets/images/noPic.svg new file mode 100644 index 0000000..0f9c09d --- /dev/null +++ b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/assets/images/noPic.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/assets/images/phoneIcon.png b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/assets/images/phoneIcon.png new file mode 100644 index 0000000..a0af511 Binary files /dev/null and b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/assets/images/phoneIcon.png differ diff --git a/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/assets/images/right_loader_images/Right/1.png b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/assets/images/right_loader_images/Right/1.png new file mode 100644 index 0000000..c571db1 Binary files /dev/null and b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/assets/images/right_loader_images/Right/1.png differ diff --git a/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/assets/images/right_loader_images/Right/2.png b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/assets/images/right_loader_images/Right/2.png new file mode 100644 index 0000000..d6a1da0 Binary files /dev/null and b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/assets/images/right_loader_images/Right/2.png differ diff --git a/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/assets/images/right_loader_images/Right/3.png b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/assets/images/right_loader_images/Right/3.png new file mode 100644 index 0000000..042ba23 Binary files /dev/null and b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/assets/images/right_loader_images/Right/3.png differ diff --git a/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/assets/images/right_loader_images/Right/4.png b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/assets/images/right_loader_images/Right/4.png new file mode 100644 index 0000000..96c067a Binary files /dev/null and b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/assets/images/right_loader_images/Right/4.png differ diff --git a/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/assets/images/right_loader_images/Right/5.png b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/assets/images/right_loader_images/Right/5.png new file mode 100644 index 0000000..3a15579 Binary files /dev/null and b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/assets/images/right_loader_images/Right/5.png differ diff --git a/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/assets/images/right_loader_images/Right/6.png b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/assets/images/right_loader_images/Right/6.png new file mode 100644 index 0000000..7de6698 Binary files /dev/null and b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/assets/images/right_loader_images/Right/6.png differ diff --git a/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/assets/images/searchIcon.png b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/assets/images/searchIcon.png new file mode 100644 index 0000000..7b95f3c Binary files /dev/null and b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/assets/images/searchIcon.png differ diff --git a/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/assets/images/searchIcon.svg b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/assets/images/searchIcon.svg new file mode 100644 index 0000000..b82fe2f --- /dev/null +++ b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/assets/images/searchIcon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/assets/images/support-page-banner.svg b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/assets/images/support-page-banner.svg new file mode 100644 index 0000000..0fa6347 --- /dev/null +++ b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/assets/images/support-page-banner.svg @@ -0,0 +1,134 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/assets/images/tick-with-circle.svg b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/assets/images/tick-with-circle.svg new file mode 100644 index 0000000..8335e7c --- /dev/null +++ b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/assets/images/tick-with-circle.svg @@ -0,0 +1,4 @@ + + + + diff --git a/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/assets/images/unnamed.jpeg b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/assets/images/unnamed.jpeg new file mode 100644 index 0000000..b33f75d Binary files /dev/null and b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/assets/images/unnamed.jpeg differ diff --git a/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/assets/images/user-logo.svg b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/assets/images/user-logo.svg new file mode 100644 index 0000000..8135e28 --- /dev/null +++ b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/assets/images/user-logo.svg @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/assets/images/voiceIcon.svg b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/assets/images/voiceIcon.svg new file mode 100644 index 0000000..9c3e1d3 --- /dev/null +++ b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/assets/images/voiceIcon.svg @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/assets/images/warning-yellow.png b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/assets/images/warning-yellow.png new file mode 100644 index 0000000..e94f265 Binary files /dev/null and b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/assets/images/warning-yellow.png differ diff --git a/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/environments/environment.ts b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/environments/environment.ts new file mode 100644 index 0000000..954259a --- /dev/null +++ b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/environments/environment.ts @@ -0,0 +1,32 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export const environment = { + firebase: { + apiKey: '', + authDomain: '', + projectId: '', + storageBucket: "", + messagingSenderId: "", + appId: "", + measurementId: "" + }, + requiredLogin: 'False', + backendURL: 'http://localhost:8080/api', + chatbotName: 'Travel Concierge', + environment: 'development', + googleMapsApiKey: '' +}; diff --git a/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/favicon.ico b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/favicon.ico new file mode 100644 index 0000000..997406a Binary files /dev/null and b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/favicon.ico differ diff --git a/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/index.html b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/index.html new file mode 100644 index 0000000..d1b75a6 --- /dev/null +++ b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/index.html @@ -0,0 +1,40 @@ + + + + + + + + Quick Bot + + + + + + + + + + + + + + + + diff --git a/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/main.ts b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/main.ts new file mode 100644 index 0000000..cc83865 --- /dev/null +++ b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/main.ts @@ -0,0 +1,23 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + +import { AppModule } from './app/app.module'; + + +platformBrowserDynamic().bootstrapModule(AppModule) + .catch(err => console.error(err)); diff --git a/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/styles.scss b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/styles.scss new file mode 100644 index 0000000..50cb6b6 --- /dev/null +++ b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/src/styles.scss @@ -0,0 +1,76 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* You can add global styles to this file, and also import other style files */ + +html, body { height: 100%;margin: 0; font-family: 'Google Sans', sans-serif !important; } + +.material-symbols-rounded { +font-variation-settings: +'FILL', +'wght' 600, +'GRAD' 0, +'opsz' 48 +} + +.tooltip-list .mdc-tooltip__surface { + white-space: pre; + max-width: unset !important; +} + +.mat-mdc-slide-toggle.mat-accent { + --mdc-switch-selected-focus-state-layer-color: #4285F4; + --mdc-switch-selected-handle-color: #4285F4; + --mdc-switch-selected-hover-state-layer-color: #4285F4; + --mdc-switch-selected-pressed-state-layer-color: #4285F4; + --mdc-switch-selected-focus-handle-color: #3f74e0; + --mdc-switch-selected-hover-handle-color: #3f74e0; + --mdc-switch-selected-pressed-handle-color: #3f74e0; + --mdc-switch-selected-focus-track-color: rgba(66, 133, 244, 0.3); + --mdc-switch-selected-hover-track-color: rgba(66, 133, 244, 0.3); + --mdc-switch-selected-pressed-track-color: rgba(66, 133, 244, 0.3); + --mdc-switch-selected-track-color: rgba(66, 133, 244, 0.3); +} + +.arrow-top { + box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.1) !important; + border-radius: 20px !important; +} + +.mat-mdc-checkbox.mat-accent { + --mdc-checkbox-selected-checkmark-color: #fff; + --mdc-checkbox-selected-focus-icon-color: #4285F4; + --mdc-checkbox-selected-hover-icon-color: #4285F4; + --mdc-checkbox-selected-icon-color: #4285F4; + --mdc-checkbox-selected-pressed-icon-color: #4285F4; + --mdc-checkbox-unselected-focus-icon-color: #212121; + --mdc-checkbox-unselected-hover-icon-color: #212121; + --mdc-checkbox-disabled-selected-icon-color: rgba(0, 0, 0, 0.38); + --mdc-checkbox-disabled-unselected-icon-color: rgba(0, 0, 0, 0.38); + --mdc-checkbox-unselected-icon-color: rgba(0, 0, 0, 0.54); + --mdc-checkbox-unselected-pressed-icon-color: rgba(0, 0, 0, 0.54); +} + +$tour-skip-link-color: red; +$tour-text-color: purple; +$tour-next-text-color: yellow; +$tour-zindex: 100; +$tour-orb-color: cyan; +$tour-next-button-color: green; +$tour-next-button-hover: black; +$tour-back-button-color: white; +$tour-shadow-color: gray; +$tour-step-color: aliceblue; diff --git a/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/tsconfig.app.json b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/tsconfig.app.json new file mode 100644 index 0000000..374cc9d --- /dev/null +++ b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/tsconfig.app.json @@ -0,0 +1,14 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "./out-tsc/app", + "types": [] + }, + "files": [ + "src/main.ts" + ], + "include": [ + "src/**/*.d.ts" + ] +} diff --git a/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/tsconfig.json b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/tsconfig.json new file mode 100644 index 0000000..8db5a74 --- /dev/null +++ b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/tsconfig.json @@ -0,0 +1,38 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "compileOnSave": false, + "compilerOptions": { + "ignoreDeprecations": "5.0", + "baseUrl": "./", + "outDir": "./dist/out-tsc", + "forceConsistentCasingInFileNames": true, + "strict": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "sourceMap": true, + "declaration": false, + "downlevelIteration": true, + "experimentalDecorators": true, + "moduleResolution": "node", + "importHelpers": true, + "target": "ES2022", + "module": "ES2022", + "useDefineForClassFields": false, + "lib": [ + "ES2022", + "dom" + ], + "strictPropertyInitialization": false, + "resolveJsonModule": true, + }, + "angularCompilerOptions": { + "enableI18nLegacyMessageIdFormat": false, + "strictInjectionParameters": true, + "strictInputAccessModifiers": true, + "strictTemplates": true, + "suppressImplicitAnyIndexErrors": true, + "strictPropertyInitialization": false, + } +} diff --git a/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/tsconfig.spec.json b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/tsconfig.spec.json new file mode 100644 index 0000000..be7e9da --- /dev/null +++ b/gemini/sample-apps/quickbot/multi-agent-travel-concierge-with-adk/frontend/tsconfig.spec.json @@ -0,0 +1,14 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "./out-tsc/spec", + "types": [ + "jasmine" + ] + }, + "include": [ + "src/**/*.spec.ts", + "src/**/*.d.ts" + ] +} diff --git a/gemini/sample-apps/quickbot/text-to-image-using-imagen3/.dockerignore b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/.dockerignore new file mode 100644 index 0000000..d472d1d --- /dev/null +++ b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/.dockerignore @@ -0,0 +1,12 @@ +node_modules +npm-debug.log +tests +coverage +Dockerfile +.dockerignore +.env +*.md +.git +.vscode +.gitignore +Makefile \ No newline at end of file diff --git a/gemini/sample-apps/quickbot/text-to-image-using-imagen3/README.md b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/README.md new file mode 100644 index 0000000..b1a8cc7 --- /dev/null +++ b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/README.md @@ -0,0 +1,279 @@ +# Quickbot - Text-to-Image using Imagen3 + +Quickbot Text-to-Image Template is an innovative application built with Google Cloud's Imagen3 model on Vertex AI. It provides a user-friendly frontend interface and a robust backend API to generate high-quality images from text prompts. + +## Overview + +This project allows users to generate images from text prompts using Google Cloud's Imagen3 model on Vertex AI. It's designed with a decoupled frontend and backend architecture, suitable for scalable deployments. + +## Demo + +Here's a quick look at the Image Generation Template in action: + + + +## Prerequisites + +Before you begin, ensure you have the following installed: + +* **Docker and Docker Compose v2:** Essential for the containerized deployment. + * Verify your Docker Compose version with `docker compose version`. If you have an older `docker-compose` (with a hyphen), you might need to upgrade to use `docker compose` in the commands. +* **Google Cloud SDK (`gcloud` CLI):** Required for manual backend setup, authentication, and interaction with Google Cloud services (like Vertex AI for Imagen3). +* **Python 3.x:** For backend development (if not using Docker). +* **Node.js and npm (or yarn):** For frontend development (if not using Docker). + +## Getting Started + +You have two main options to get the application running: + +### Option 1: Using Docker Compose (Recommended for Quick Start) + +This is the **simplest way to get the entire application (frontend and backend) up and running!** You just need to run `docker compose up` after initial setup. See the next steps: + +1. **Ensure Docker and Docker Compose v2 are installed and running.** + +2. **Authenticate with Google Cloud for Vertex AI Imagen Access:** + For the application (specifically the backend running in Docker) to interact with Google Cloud's Imagen3 on Vertex AI, you need to provide Google Cloud credentials to the Docker container. The recommended way for local development is using Application Default Credentials (ADC). + Run the following commands in your local terminal: + ```bash + gcloud auth application-default login + gcloud config set project + gcloud auth application-default set-quota-project + + # Verify your configuration + gcloud auth list + gcloud config list project + ``` + This will create or update ADC on your local machine. The `docker-compose.yml` file is typically configured to mount these local credentials into the backend container, allowing it to authenticate. + > **Windows Users:** The path to ADC might differ on Windows. You may need to adjust the commented-out volume mount paths for `gcloud` credentials in the `docker-compose.yml` file to ensure the backend container can access them. + > **Note:** Ensure the Vertex AI API is enabled in your Google Cloud project. + +3. **Build Docker Images:** + Build the Docker images for the frontend and backend services: + ```bash + docker compose build + ``` + The backend will be configured using environment variables (see "Environment Variables" section) to connect to Google Cloud services, including Imagen3 on Vertex AI. + +4. **Run the application:** + After building the images, start the services: + ```bash + docker compose up + ``` + The frontend should typically be available at `http://localhost:4200` (or as configured) and the backend API at `http://localhost:8080`. + +### Option 2: Manual Setup (for Development and Customization) + +Follow these steps if you prefer to run the frontend and backend services manually on your local machine. + +**A. Backend Setup** + +1. **Navigate to the `backend/` directory.** + ```bash + cd backend + ``` + +2. **Create a virtual environment and install dependencies:** + ```bash + # Check if you are already in an environment + pip -V + + # If not, create and activate (for Linux/macOS) + python3 -m venv .venv + source .venv/bin/activate + + # Install requirements + pip3 install -r requirements.txt + ``` + > **VS Code Tip:** If VS Code doesn't recognize your virtual environment, press `Ctrl + Shift + P` (or `Cmd + Shift + P` on Mac), type "Python: Select Interpreter", choose "Enter interpreter path...", and then find and select `.venv/bin/python` inside your `backend` directory. + +3. **Setup Google Cloud (`gcloud`) credentials:** + Ensure you're authenticated and your project is configured correctly. The Vertex AI API should be enabled in your project. + ```bash + gcloud auth login # Login with your user account + gcloud config set project + + # For services using Application Default Credentials (ADC) locally + gcloud auth application-default login + # Optionally, set a quota project for ADC if not inherited + gcloud auth application-default set-quota-project + + # Verify configuration + gcloud auth list + gcloud config list project + ``` + +4. **Configure Environment Variables:** + Backend configuration is managed via environment variables. Create a `.local.env` file in the `backend/` directory (you can copy from `.local.env.example` if one exists). This file should be added to `.gitignore`. + + * **For Mac/Windows (or zsh console on Linux):** + Source the variables directly (from the `backend/` directory): + ```bash + . ./.local.env + ``` + * **For Linux (bash):** + Open `backend/.venv/bin/activate` and append the `export` commands from your `backend/.local.env` file after the `PATH` export section. For example: + ```sh + # ... existing activate script content ... + _OLD_VIRTUAL_PATH="$PATH" + PATH="$VIRTUAL_ENV/bin:$PATH" + export PATH + + # Quickbot env variables (copied from .local.env) + export ENVIRONMENT="development" + export FRONTEND_URL="http://localhost:4200" + export GCP_PROJECT_ID="" + export VERTEX_AI_LOCATION="us-central1" # Example location + export IMAGEN_MODEL_ID="imagegeneration@006" # Example model + # ... other necessary variables ... + ``` + Verify the variables are set by running `env` in your activated terminal. + +5. **Run the setup script (if applicable):** + This script might perform initial configurations if needed by the backend. + ```bash + # from the backend/ directory + python3 setup.py + ``` + +6. **Run the backend application:** + ```bash + # from the backend/ directory + uvicorn main:app --reload --port 8080 + ``` + +**B. Frontend Setup** + +(These instructions assume a typical TypeScript/Angular frontend. Adjust as necessary based on your `frontend/README.md`.) + +1. **Navigate to the `frontend/` directory.** + ```bash + cd frontend + ``` +2. **Install dependencies:** + ```bash + npm install + ``` +3. **Environment Variables (if applicable):** + The frontend might require its own environment configuration (e.g., via a `.env` file or Angular's `environment.ts` files for API endpoints). Check the `frontend/` directory or its `README.md` for specific instructions. +4. **Run the frontend application:** + ```bash + npm start + # Or, for many Angular projects: + # ng serve + ``` + The application will typically be available at `http://localhost:4200`. + +## Project Structure (highlighting important parts) +```text +text-to-imagen-using-imagen3/ +├── backend/ # Python backend (FastAPI/Uvicorn) +│ ├── .venv/ # Python virtual environment (gitignored) +│ ├── .local.env # Local environment variables (gitignored) +│ ├── main.py # Main application file +│ ├── requirements.txt # Backend dependencies +│ ├── setup.py # Backend setup script +│ └── README.md # Backend-specific instructions +├── frontend/ # TypeScript frontend (Angular) +│ ├── node_modules/ # Node.js dependencies (gitignored) +│ ├── src/ # Frontend source code +│ ├── package.json # Frontend dependencies and scripts +│ ├── tsconfig.json # TypeScript configuration +│ └── README.md # Frontend-specific instructions +├── docker-compose.yml # Docker Compose configuration for all services +└── README.md # This file: Root project README +``` + +## Environment Variables + +Configuration for both frontend and backend is primarily managed through environment variables. + +* **Backend:** + * When running manually, backend environment variables are typically defined in `backend/.local.env`. + * When running with Docker, these variables are usually passed into the backend container via the `docker-compose.yml` file (often referencing a `.env` file at the root or `backend/` directory). + * Key variables include: + * `GCLOUD_PROJECT`: Your Google Cloud Project ID. + * `ENVIRONMENT`: Application environment (e.g., `development`, `production`). + * `FRONTEND_URL`: URL of the frontend application (e.g., `http://localhost:4200`). + * Consult `backend/README.md` or `backend/.local.env.example` for a complete list. + +* **Frontend:** + * Frontend environment variables (e.g., API endpoint URLs) are usually managed within the frontend's build system (e.g., Angular's `environment.ts` files or a `.env` file in the `frontend/` directory). + * Consult `frontend/README.md` for specific details. + +## Code Styling & Commit Guidelines + +To maintain code quality and consistency across the project: + +* **TypeScript (Frontend):** We follow the Angular Coding Style Guide by leveraging Google's TypeScript Style Guide using `gts`. This includes a formatter, linter, and automatic code fixer. +* **Python (Backend):** We adhere to the Google Python Style Guide, using tools like `pylint` and `black` for linting and formatting. +* **Commit Messages:** We suggest following Angular's Commit Message Guidelines to create clear and descriptive commit messages. + +### Frontend (TypeScript with `gts`) + +(Assumes setup within the `frontend/` directory) + +1. **Initialize `gts` (if not already done in the project):** + Navigate to `frontend/` and run: + ```bash + npx gts init + ``` + This will set up `gts` and create necessary configuration files (like `tsconfig.json`). Ensure your `tsconfig.json` (or a related `gts` config file like `.gtsrc`) includes an extension for `gts` defaults, typically: + ```json + { + "extends": "./node_modules/gts/tsconfig-google.json" + // ... other configurations + } + ``` +2. **Check for linting issues:** + (This assumes a `lint` script is defined in `frontend/package.json`, e.g., `"lint": "gts lint"`) + ```bash + # from frontend/ directory + npm run lint + ``` +3. **Fix linting issues automatically (where possible):** + (This assumes a `fix` script is defined in `frontend/package.json`, e.g., `"fix": "gts fix"`) + ```bash + # from frontend/ directory + npm run fix + ``` + +### Backend (Python with `pylint` and `black`) + +(Assumes setup within the `backend/` directory and its virtual environment activated) + +1. **Ensure Dependencies are Installed:** + Add `pylint` and `black` to your `backend/requirements.txt` file if not already present: + ``` + pylint + black + ``` + Then install them within your virtual environment: + ```bash + # from backend/ directory, with .venv activated + pip install pylint black + # or pip install -r requirements.txt + ``` +2. **Configure `pylint`:** + It's recommended to have a `.pylintrc` file in your `backend/` directory to configure `pylint` rules. You can generate one if it doesn't exist: + ```bash + # from backend/ directory + pylint --generate-rcfile > .pylintrc + ``` + Customize this file according to your project's needs and the Google Python Style Guide. +3. **Check for linting issues with `pylint`:** + Navigate to the `backend/` directory and run: + ```bash + # from backend/ directory + pylint . + # Or specify modules/packages: pylint your_module_name + ``` +4. **Format code with `black`:** + To automatically format all Python files in the `backend/` directory and its subdirectories: + ```bash + # from backend/ directory + python -m black . --line-length=80 + ``` diff --git a/gemini/sample-apps/quickbot/text-to-image-using-imagen3/assets/quickbot-text-to-imagen.mp4 b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/assets/quickbot-text-to-imagen.mp4 new file mode 100644 index 0000000..465f049 Binary files /dev/null and b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/assets/quickbot-text-to-imagen.mp4 differ diff --git a/gemini/sample-apps/quickbot/text-to-image-using-imagen3/backend/.gcloudignore b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/backend/.gcloudignore new file mode 100644 index 0000000..606a07e --- /dev/null +++ b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/backend/.gcloudignore @@ -0,0 +1,20 @@ +# This file specifies files that are *not* uploaded to Google Cloud +# using gcloud. It follows the same syntax as .gitignore, with the addition of +# "#!include" directives (which insert the entries of the given .gitignore-style +# file at that point). +# +# For more information, run: +# $ gcloud topic gcloudignore +# +.gcloudignore +# If you would like to upload your .git directory, .gitignore file or files +# from your .gitignore file, remove the corresponding line +# below: +.git +.gitignore + +# Python pycache: +__pycache__/ +# Ignored by the build system +/setup.cfg +my_env \ No newline at end of file diff --git a/gemini/sample-apps/quickbot/text-to-image-using-imagen3/backend/.gitignore b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/backend/.gitignore new file mode 100644 index 0000000..83bb4d6 --- /dev/null +++ b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/backend/.gitignore @@ -0,0 +1,135 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# VSCode +.vscode/* +!.vscode/settings.json +!.vscode/extensions.json + +# Logs +*/output +*.info diff --git a/gemini/sample-apps/quickbot/text-to-image-using-imagen3/backend/Dockerfile b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/backend/Dockerfile new file mode 100644 index 0000000..e796690 --- /dev/null +++ b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/backend/Dockerfile @@ -0,0 +1,27 @@ +# Copyright 2025 Google LLC +# +# 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 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +FROM python:3.11-alpine + +COPY requirements.txt /tmp +RUN pip install -r /tmp/requirements.txt + +WORKDIR /app +COPY . /app/ + +ENV ENVIRONMENT="development" +ENV FRONTEND_URL="http://localhost:4200" + +EXPOSE 8080 +ENTRYPOINT ["gunicorn", "main:app", "--workers=4", "--worker-class=uvicorn.workers.UvicornWorker", "--timeout=36000", "--bind=0.0.0.0:8080"] \ No newline at end of file diff --git a/gemini/sample-apps/quickbot/text-to-image-using-imagen3/backend/README.md b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/backend/README.md new file mode 100644 index 0000000..4482d77 --- /dev/null +++ b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/backend/README.md @@ -0,0 +1,133 @@ +# QuickBot App Backend | text-to-image-using-imagen3 + +[![linting: pylint](https://img.shields.io/badge/linting-pylint-yellowgreen)](https://github.com/pylint-dev/pylint) +[![Code Style: Google](https://img.shields.io/badge/code%20style-google-blueviolet.svg)](https://github.com/google/gts) + +QuickBot App is a set of templates that can be deployed out of the box into Cloud Run and work independently. Each one can be run independently connected to the user default google cloud auth credentials, and based on the complexity of each template, may require to deploy more or less resources into our Google Cloud Project. +The architecture always follows the following structure: a folder for the frontend which consists in an Angular app, and a backend folder which consists of a FastAPI Python app. + + +## Setting up +### 1. Create virtualenv inside the backend folder and install dependencies +Create a virtual environment on the root of the application, activate it and install the requirements +``` +# check if you are already in the env +pip -V + +# if not then +python3 -m venv .venv +source .venv/bin/activate +pip3 install -r requirements.txt +``` + +> **IMPORTANT!** VS Code may not recognize your env, in that case type "ctrl + shift + P", then select "Python: Select Interpreter" and then select "Enter interpreter path..." and then select your .venv python interpreter, in this case .backend/.venv/bin/python + + +### 2. Setup gcloud credentials +``` +gcloud auth list +gcloud config list + +gcloud auth login +gcloud config set project +gcloud auth application-default set-quota-project + +gcloud auth list +gcloud config list +``` + +### 3. Add environment variables + +#### If you have Mac or Windows (or if you are using zsh console on Linux) +``` +. ./local.env +``` + +#### If you have Linux +Open the file .venv/bin/activate and paste the env variables from `.local.env` after the PATH export, like this: +``` +... + +_OLD_VIRTUAL_PATH="$PATH" +PATH="$VIRTUAL_ENV/bin:$PATH" +export PATH + +# Quickbot env variables +export ENVIRONMENT=development +export FRONTEND_URL=http://localhost:4200 + +... +``` + +Check that the env variables has been taken into account, running: +``` +env +``` +You should see the new env variables set there + + +### 4. Run the application +Finally run using uvicorn +``` +uvicorn main:app --reload --port 8080 +``` + +## Code Styling & Commit Guidelines + +To maintain code quality and consistency: + +* **TypeScript (Frontend):** We follow [Angular Coding Style Guide](https://angular.dev/style-guide) by leveraging the use of [Google's TypeScript Style Guide](https://github.com/google/gts) using `gts`. This includes a formatter, linter, and automatic code fixer. +* **Python (Backend):** We adhere to the [Google Python Style Guide](https://google.github.io/styleguide/pyguide.html), using tools like `pylint` and `black` for linting and formatting. +* **Commit Messages:** We suggest following [Angular's Commit Message Guidelines](https://github.com/angular/angular/blob/main/contributing-docs/commit-message-guidelines.md) to create clear and descriptive commit messages. + +#### Frontend (TypeScript with `gts`) + +1. **Initialize `gts` (if not already done in the project):** + Navigate to the `frontend/` directory and run: + ```bash + npx gts init + ``` + This will set up `gts` and create necessary configuration files (like `tsconfig.json`). Ensure your `tsconfig.json` (or a related gts config file like `.gtsrc`) includes an extension for `gts` defaults, typically: + ```json + { + "extends": "./node_modules/gts/tsconfig-google.json", + // ... other configurations + } + ``` +2. **Check for linting issues:** + ```bash + npm run lint + ``` + (This assumes a `lint` script is defined in `package.json`, e.g., `"lint": "gts lint"`) +3. **Fix linting issues automatically (where possible):** + ```bash + npm run fix + ``` + (This assumes a `fix` script is defined in `package.json`, e.g., `"fix": "gts fix"`) + +#### Backend (Python with `pylint` and `black`) + +1. **Ensure Dependencies are Installed:** + Add `pylint` and `black` to your `backend/requirements.txt` file: + ``` + pylint + black + ``` + Then install them within your virtual environment: + ```bash + pip install pylint black + # or pip install -r requirements.txt + ``` +2. **Configure `pylint`:** + It's recommended to have a `.pylintrc` file in your `backend/` directory to configure `pylint` rules. You might need to copy a standard one or generate one (`pylint --generate-rcfile > .pylintrc`). +3. **Check for linting issues with `pylint`:** + Navigate to the `backend/` directory and run: + ```bash + pylint . + ``` + (Or specify modules/packages: `pylint your_module_name`) +4. **Format code with `black`:** + To automatically format all Python files in the current directory and subdirectories: + ```bash + python -m black . --line-length=80 + ``` diff --git a/gemini/sample-apps/quickbot/text-to-image-using-imagen3/backend/__init__.py b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/backend/__init__.py new file mode 100644 index 0000000..36a1e8d --- /dev/null +++ b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/backend/__init__.py @@ -0,0 +1,14 @@ +# Copyright 2025 Google LLC +# +# 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 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + diff --git a/gemini/sample-apps/quickbot/text-to-image-using-imagen3/backend/local.env b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/backend/local.env new file mode 100644 index 0000000..af26a11 --- /dev/null +++ b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/backend/local.env @@ -0,0 +1,2 @@ +export ENVIRONMENT=development +export FRONTEND_URL=http://localhost:4200 diff --git a/gemini/sample-apps/quickbot/text-to-image-using-imagen3/backend/main.py b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/backend/main.py new file mode 100644 index 0000000..a9dc475 --- /dev/null +++ b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/backend/main.py @@ -0,0 +1,95 @@ +# Copyright 2025 Google LLC +# +# 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 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from os import getenv + +from fastapi import FastAPI, File, UploadFile +from fastapi.middleware.cors import CORSMiddleware +from google.cloud import speech + +from src.controller.search import router as search_router + +app = FastAPI() + + +def configure_cors(app): + """Configures CORS middleware based on the environment.""" + environment = getenv("ENVIRONMENT") + allowed_origins = [] + + if environment == "production": + frontend_url = getenv("FRONTEND_URL") + if not frontend_url: + raise ValueError( + "FRONTEND_URL environment variable not set in production" + ) + allowed_origins.append(frontend_url) + elif environment == "development": + allowed_origins.append("*") # Allow all origins in development + else: + raise ValueError( + f"Invalid ENVIRONMENT: {environment}. Must be 'production' or 'development'" + ) + + app.add_middleware( + CORSMiddleware, + allow_origins=allowed_origins, + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], + ) + + +# Create a route to handle GET requests on root +@app.get("/") +async def root(): + return "You are calling Quick Bot Backend" + + +# Create a route to handle GET requests on /version +@app.get("/api/version") +def version(): + return "v0.0.1" + + +@app.post("/api/audio_chat") +async def audio_chat(audio_file: UploadFile = File(...)): + client = speech.SpeechClient() + audio_content = await audio_file.read() + audio = speech.RecognitionAudio(content=audio_content) + config = speech.RecognitionConfig( + language_code="en-US", + sample_rate_hertz=48000, + model="default", + audio_channel_count=1, + enable_word_confidence=True, + enable_word_time_offsets=True, + ) + + operation = client.long_running_recognize(config=config, audio=audio) + + print("Waiting for operation to complete...") + response = operation.result(timeout=90) + + text = "" + for result in response.results: + print(f"Transcript: {result.alternatives[0].transcript}") + text = result.alternatives[0].transcript + + return text, 200 + + +configure_cors(app) + +app.include_router(search_router) diff --git a/gemini/sample-apps/quickbot/text-to-image-using-imagen3/backend/pylintrc b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/backend/pylintrc new file mode 100644 index 0000000..cd99157 --- /dev/null +++ b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/backend/pylintrc @@ -0,0 +1,399 @@ +# This Pylint rcfile contains a best-effort configuration to uphold the +# best-practices and style described in the Google Python style guide: +# https://google.github.io/styleguide/pyguide.html +# +# Its canonical open-source location is: +# https://google.github.io/styleguide/pylintrc + +[MAIN] + +# Files or directories to be skipped. They should be base names, not paths. +ignore=third_party + +# Files or directories matching the regex patterns are skipped. The regex +# matches against base names, not paths. +ignore-patterns= + +# Pickle collected data for later comparisons. +persistent=no + +# List of plugins (as comma separated values of python modules names) to load, +# usually to register additional checkers. +load-plugins= + +# Use multiple processes to speed up Pylint. +jobs=4 + +# Allow loading of arbitrary C extensions. Extensions are imported into the +# active Python interpreter and may run arbitrary code. +unsafe-load-any-extension=no + + +[MESSAGES CONTROL] + +# Only show warnings with the listed confidence levels. Leave empty to show +# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED +confidence= + +# Enable the message, report, category or checker with the given id(s). You can +# either give multiple identifier separated by comma (,) or put this option +# multiple time (only on the command line, not in the configuration file where +# it should appear only once). See also the "--disable" option for examples. +#enable= + +# Disable the message, report, category or checker with the given id(s). You +# can either give multiple identifiers separated by comma (,) or put this +# option multiple times (only on the command line, not in the configuration +# file where it should appear only once).You can also use "--disable=all" to +# disable everything first and then reenable specific checks. For example, if +# you want to run only the similarities checker, you can use "--disable=all +# --enable=similarities". If you want to run only the classes checker, but have +# no Warning level messages displayed, use"--disable=all --enable=classes +# --disable=W" +disable=R, + abstract-method, + apply-builtin, + arguments-differ, + attribute-defined-outside-init, + backtick, + bad-option-value, + basestring-builtin, + buffer-builtin, + c-extension-no-member, + consider-using-enumerate, + cmp-builtin, + cmp-method, + coerce-builtin, + coerce-method, + delslice-method, + div-method, + eq-without-hash, + execfile-builtin, + file-builtin, + filter-builtin-not-iterating, + fixme, + getslice-method, + global-statement, + hex-method, + idiv-method, + implicit-str-concat, + import-error, + import-self, + import-star-module-level, + input-builtin, + intern-builtin, + invalid-str-codec, + locally-disabled, + long-builtin, + long-suffix, + map-builtin-not-iterating, + misplaced-comparison-constant, + missing-function-docstring, + metaclass-assignment, + next-method-called, + next-method-defined, + no-absolute-import, + no-init, # added + no-member, + no-name-in-module, + no-self-use, + nonzero-method, + oct-method, + old-division, + old-ne-operator, + old-octal-literal, + old-raise-syntax, + parameter-unpacking, + print-statement, + raising-string, + range-builtin-not-iterating, + raw_input-builtin, + rdiv-method, + reduce-builtin, + relative-import, + reload-builtin, + round-builtin, + setslice-method, + signature-differs, + standarderror-builtin, + suppressed-message, + sys-max-int, + trailing-newlines, + unichr-builtin, + unicode-builtin, + unnecessary-pass, + unpacking-in-except, + useless-else-on-loop, + useless-suppression, + using-cmp-argument, + wrong-import-order, + xrange-builtin, + zip-builtin-not-iterating, + + +[REPORTS] + +# Set the output format. Available formats are text, parseable, colorized, msvs +# (visual studio) and html. You can also give a reporter class, eg +# mypackage.mymodule.MyReporterClass. +output-format=text + +# Tells whether to display a full report or only the messages +reports=no + +# Python expression which should return a note less than 10 (10 is the highest +# note). You have access to the variables errors warning, statement which +# respectively contain the number of errors / warnings messages and the total +# number of statements analyzed. This is used by the global evaluation report +# (RP0004). +evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) + +# Template used to display messages. This is a python new-style format string +# used to format the message information. See doc for all details +#msg-template= + + +[BASIC] + +# Good variable names which should always be accepted, separated by a comma +good-names=main,_ + +# Bad variable names which should always be refused, separated by a comma +bad-names= + +# Colon-delimited sets of names that determine each other's naming style when +# the name regexes allow several styles. +name-group= + +# Include a hint for the correct naming format with invalid-name +include-naming-hint=no + +# List of decorators that produce properties, such as abc.abstractproperty. Add +# to this list to register other decorators that produce valid properties. +property-classes=abc.abstractproperty,cached_property.cached_property,cached_property.threaded_cached_property,cached_property.cached_property_with_ttl,cached_property.threaded_cached_property_with_ttl + +# Regular expression matching correct function names +function-rgx=^(?:(?PsetUp|tearDown|setUpModule|tearDownModule)|(?P_?[A-Z][a-zA-Z0-9]*)|(?P_?[a-z][a-z0-9_]*))$ + +# Regular expression matching correct variable names +variable-rgx=^[a-z][a-z0-9_]*$ + +# Regular expression matching correct constant names +const-rgx=^(_?[A-Z][A-Z0-9_]*|__[a-z0-9_]+__|_?[a-z][a-z0-9_]*)$ + +# Regular expression matching correct attribute names +attr-rgx=^_{0,2}[a-z][a-z0-9_]*$ + +# Regular expression matching correct argument names +argument-rgx=^[a-z][a-z0-9_]*$ + +# Regular expression matching correct class attribute names +class-attribute-rgx=^(_?[A-Z][A-Z0-9_]*|__[a-z0-9_]+__|_?[a-z][a-z0-9_]*)$ + +# Regular expression matching correct inline iteration names +inlinevar-rgx=^[a-z][a-z0-9_]*$ + +# Regular expression matching correct class names +class-rgx=^_?[A-Z][a-zA-Z0-9]*$ + +# Regular expression matching correct module names +module-rgx=^(_?[a-z][a-z0-9_]*|__init__)$ + +# Regular expression matching correct method names +method-rgx=(?x)^(?:(?P_[a-z0-9_]+__|runTest|setUp|tearDown|setUpTestCase|tearDownTestCase|setupSelf|tearDownClass|setUpClass|(test|assert)_*[A-Z0-9][a-zA-Z0-9_]*|next)|(?P_{0,2}[A-Z][a-zA-Z0-9_]*)|(?P_{0,2}[a-z][a-z0-9_]*))$ + +# Regular expression which should only match function or class names that do +# not require a docstring. +no-docstring-rgx=(__.*__|main|test.*|.*test|.*Test)$ + +# Minimum line length for functions/classes that require docstrings, shorter +# ones are exempt. +docstring-min-length=12 + + +[TYPECHECK] + +# List of decorators that produce context managers, such as +# contextlib.contextmanager. Add to this list to register other decorators that +# produce valid context managers. +contextmanager-decorators=contextlib.contextmanager,contextlib2.contextmanager + +# List of module names for which member attributes should not be checked +# (useful for modules/projects where namespaces are manipulated during runtime +# and thus existing member attributes cannot be deduced by static analysis. It +# supports qualified module names, as well as Unix pattern matching. +ignored-modules= + +# List of class names for which member attributes should not be checked (useful +# for classes with dynamically set attributes). This supports the use of +# qualified names. +ignored-classes=optparse.Values,thread._local,_thread._local + +# List of members which are set dynamically and missed by pylint inference +# system, and so shouldn't trigger E1101 when accessed. Python regular +# expressions are accepted. +generated-members= + + +[FORMAT] + +# Maximum number of characters on a single line. +max-line-length=80 + +# TODO(https://github.com/pylint-dev/pylint/issues/3352): Direct pylint to exempt +# lines made too long by directives to pytype. + +# Regexp for a line that is allowed to be longer than the limit. +ignore-long-lines=(?x)( + ^\s*(\#\ )??$| + ^\s*(from\s+\S+\s+)?import\s+.+$) + +# Allow the body of an if to be on the same line as the test if there is no +# else. +single-line-if-stmt=yes + +# Maximum number of lines in a module +max-module-lines=99999 + +# String used as indentation unit. The internal Google style guide mandates 2 +# spaces. Google's externaly-published style guide says 4, consistent with +# PEP 8. Here, we use 2 spaces, for conformity with many open-sourced Google +# projects (like TensorFlow). +indent-string=' ' + +# Number of spaces of indent required inside a hanging or continued line. +indent-after-paren=4 + +# Expected format of line ending, e.g. empty (any line ending), LF or CRLF. +expected-line-ending-format= + + +[MISCELLANEOUS] + +# List of note tags to take in consideration, separated by a comma. +notes=TODO + + +[STRING] + +# This flag controls whether inconsistent-quotes generates a warning when the +# character used as a quote delimiter is used inconsistently within a module. +check-quote-consistency=yes + + +[VARIABLES] + +# Tells whether we should check for unused import in __init__ files. +init-import=no + +# A regular expression matching the name of dummy variables (i.e. expectedly +# not used). +dummy-variables-rgx=^\*{0,2}(_$|unused_|dummy_) + +# List of additional names supposed to be defined in builtins. Remember that +# you should avoid to define new builtins when possible. +additional-builtins= + +# List of strings which can identify a callback function by name. A callback +# name must start or end with one of those strings. +callbacks=cb_,_cb + +# List of qualified module names which can have objects that can redefine +# builtins. +redefining-builtins-modules=six,six.moves,past.builtins,future.builtins,functools + + +[LOGGING] + +# Logging modules to check that the string format arguments are in logging +# function parameter format +logging-modules=logging,absl.logging,tensorflow.io.logging + + +[SIMILARITIES] + +# Minimum lines number of a similarity. +min-similarity-lines=4 + +# Ignore comments when computing similarities. +ignore-comments=yes + +# Ignore docstrings when computing similarities. +ignore-docstrings=yes + +# Ignore imports when computing similarities. +ignore-imports=no + + +[SPELLING] + +# Spelling dictionary name. Available dictionaries: none. To make it working +# install python-enchant package. +spelling-dict= + +# List of comma separated words that should not be checked. +spelling-ignore-words= + +# A path to a file that contains private dictionary; one word per line. +spelling-private-dict-file= + +# Tells whether to store unknown words to indicated private dictionary in +# --spelling-private-dict-file option instead of raising a message. +spelling-store-unknown-words=no + + +[IMPORTS] + +# Deprecated modules which should not be used, separated by a comma +deprecated-modules=regsub, + TERMIOS, + Bastion, + rexec, + sets + +# Create a graph of every (i.e. internal and external) dependencies in the +# given file (report RP0402 must not be disabled) +import-graph= + +# Create a graph of external dependencies in the given file (report RP0402 must +# not be disabled) +ext-import-graph= + +# Create a graph of internal dependencies in the given file (report RP0402 must +# not be disabled) +int-import-graph= + +# Force import order to recognize a module as part of the standard +# compatibility libraries. +known-standard-library= + +# Force import order to recognize a module as part of a third party library. +known-third-party=enchant, absl + +# Analyse import fallback blocks. This can be used to support both Python 2 and +# 3 compatible code, which means that the block might have code that exists +# only in one or another interpreter, leading to false positives when analysed. +analyse-fallback-blocks=no + + +[CLASSES] + +# List of method names used to declare (i.e. assign) instance attributes. +defining-attr-methods=__init__, + __new__, + setUp + +# List of member names, which should be excluded from the protected access +# warning. +exclude-protected=_asdict, + _fields, + _replace, + _source, + _make + +# List of valid names for the first argument in a class method. +valid-classmethod-first-arg=cls, + class_ + +# List of valid names for the first argument in a metaclass class method. +valid-metaclass-classmethod-first-arg=mcs diff --git a/gemini/sample-apps/quickbot/text-to-image-using-imagen3/backend/pyproject.toml b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/backend/pyproject.toml new file mode 100644 index 0000000..7ced637 --- /dev/null +++ b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/backend/pyproject.toml @@ -0,0 +1,5 @@ +[tool.coverage.run] +source = ["livecov"] + +[tool.pytest.ini_options] +addopts = "--cov --cov-report=lcov:lcov.info --cov-report=term" diff --git a/gemini/sample-apps/quickbot/text-to-image-using-imagen3/backend/pytest.ini b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/backend/pytest.ini new file mode 100644 index 0000000..2a3f054 --- /dev/null +++ b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/backend/pytest.ini @@ -0,0 +1,12 @@ +[pytest] +minversion = 7.0 + +# Directories to look for the test files +testpaths = tests + +# Directories to look for the source code +pythonpath = . + +python_files = test_*.py *_tests.py + +log_cli_level = DEBUG diff --git a/gemini/sample-apps/quickbot/text-to-image-using-imagen3/backend/requirements.txt b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/backend/requirements.txt new file mode 100644 index 0000000..a9f6017 --- /dev/null +++ b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/backend/requirements.txt @@ -0,0 +1,25 @@ +gunicorn==20.1.0; python_version > '3.0' +gunicorn==19.10.0; python_version < '3.0' +uvicorn~=0.17.0 +fastapi~=0.111.1 +fastapi-utilities==0.2.0 +cloudpickle==2.2.1 +grpcio==1.66.2 +urllib3==1.26.16 +typing-inspect==0.9.0 +typing_extensions==4.12.2 +pydantic==2.9.2 +requests==2.31.0 + +google-cloud-aiplatform==1.69.0 +google-cloud-bigquery==3.26.0 +google-cloud-tasks==2.16.5 +google-cloud-logging==3.11.2 +google-cloud-speech==2.27.0 +google-cloud-discoveryengine==0.13.4 +google-genai==0.8.0 +pylint +black +pytest +pytest-cov +pytest-watch diff --git a/gemini/sample-apps/quickbot/text-to-image-using-imagen3/backend/src/__init__.py b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/backend/src/__init__.py new file mode 100644 index 0000000..36a1e8d --- /dev/null +++ b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/backend/src/__init__.py @@ -0,0 +1,14 @@ +# Copyright 2025 Google LLC +# +# 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 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + diff --git a/gemini/sample-apps/quickbot/text-to-image-using-imagen3/backend/src/controller/__init__.py b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/backend/src/controller/__init__.py new file mode 100644 index 0000000..36a1e8d --- /dev/null +++ b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/backend/src/controller/__init__.py @@ -0,0 +1,14 @@ +# Copyright 2025 Google LLC +# +# 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 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + diff --git a/gemini/sample-apps/quickbot/text-to-image-using-imagen3/backend/src/controller/search.py b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/backend/src/controller/search.py new file mode 100644 index 0000000..9161b6e --- /dev/null +++ b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/backend/src/controller/search.py @@ -0,0 +1,59 @@ +# Copyright 2025 Google LLC +# +# 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 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from typing import List +from fastapi import APIRouter, HTTPException, status as Status +from pydantic import BaseModel + +from src.model.search import CreateSearchRequest, ImageGenerationResult, SearchResponse +from src.service.search import ImagenSearchService + +router = APIRouter( + prefix="/api/search", + tags=["searches"], + responses={404: {"description": "Not found"}}, +) + +@router.post("") +async def search( + item: CreateSearchRequest, +) -> SearchResponse: + try: + # Access parameters from CreateSearchRequest + term = item.term + generation_model = item.generation_model + aspect_ratio = item.aspect_ratio + number_of_images = item.number_of_images + image_style = item.image_style + + service = ImagenSearchService() + return await service.generate_images( + term=term, + generation_model=generation_model, + aspect_ratio=aspect_ratio, + number_of_images=number_of_images, + image_style=image_style, + ) + except HTTPException as http_exception: + raise http_exception + except ValueError as value_error: + raise HTTPException( + status_code=Status.HTTP_400_BAD_REQUEST, + detail=str(value_error), + ) + except Exception as e: + raise HTTPException( + status_code=Status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=str(e), + ) diff --git a/gemini/sample-apps/quickbot/text-to-image-using-imagen3/backend/src/model/__init__.py b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/backend/src/model/__init__.py new file mode 100644 index 0000000..36a1e8d --- /dev/null +++ b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/backend/src/model/__init__.py @@ -0,0 +1,14 @@ +# Copyright 2025 Google LLC +# +# 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 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + diff --git a/gemini/sample-apps/quickbot/text-to-image-using-imagen3/backend/src/model/http_status.py b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/backend/src/model/http_status.py new file mode 100644 index 0000000..ca1c1c3 --- /dev/null +++ b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/backend/src/model/http_status.py @@ -0,0 +1,25 @@ +# Copyright 2025 Google LLC +# +# 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 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from fastapi import HTTPException + + +class ResourceAlreadyExists(HTTPException): + def __init__(self, detail="Resource already exists"): + super().__init__(status_code=400, detail=detail) + + +class BadRequest(HTTPException): + def __init__(self, detail="Resource already exists"): + super().__init__(status_code=400, detail=detail) diff --git a/gemini/sample-apps/quickbot/text-to-image-using-imagen3/backend/src/model/search.py b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/backend/src/model/search.py new file mode 100644 index 0000000..68493bb --- /dev/null +++ b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/backend/src/model/search.py @@ -0,0 +1,102 @@ +# Copyright 2025 Google LLC +# +# 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 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from typing import Annotated, List, Literal, Optional, Union + +from fastapi import Query +from pydantic import BaseModel, ConfigDict, Field, field_validator +from pydantic.alias_generators import to_camel + + +# Create a Literal type from the list of valid models +GenerationModelOptionalLiteral = Union[ + Literal["imagen-4.0-ultra-generate-exp-05-20"], + Literal["imagen-3.0-generate-001"], + Literal["imagen-3.0-fast-generate-001"], + Literal["imagen-3.0-generate-002"], + Literal["imagegeneration@006"], + Literal["imagegeneration@005"], + Literal["imagegeneration@002"], +] + +AspectRatioLiteral = Union[ + Literal["1:1"], + Literal["9:16"], + Literal["16:9"], + Literal["3:4"], + Literal["4:3"], +] + +ImageStyleLiteral = Union[ + Literal["Modern"], + Literal["Realistic"], + Literal["Vintage"], + Literal["Monochrome"], + Literal["Fantasy"], + Literal["Sketch"], +] + + +class BaseSchema(BaseModel): + model_config = ConfigDict( + alias_generator=to_camel, + populate_by_name=True, + from_attributes=True, + ) + + +class CreateSearchRequest(BaseSchema): + term: Annotated[str, Query(max_length=150)] = Field( + description="Prompt term to be passed to the model" + ) + generation_model: Annotated[ + Optional[GenerationModelOptionalLiteral], + Query(description="Model used for image generation"), + ] = Field( + description="Model used for image generation", + ) + aspect_ratio: Annotated[ + Optional[AspectRatioLiteral], + Query(description="Aspect ratio of the image"), + ] = Field(description="Aspect ratio of the image") + number_of_images: Annotated[ + Optional[int], + Field(ge=1, le=4, description="Number of images to generate"), + ] = Field(description="Number of images to generate") + image_style: Annotated[ + Optional[ImageStyleLiteral], Query(description="Style of the image") + ] = Field(description="Style of the image") + + @field_validator("term") + def term_must_not_be_empty(cls, value: str) -> str: + if not value.strip(): + raise ValueError("Term cannot be empty or whitespace only") + return value + + +class CustomImageResult(BaseSchema): + gcs_uri: Optional[str] + mime_type: str + encoded_image: str + + +class ImageGenerationResult(BaseSchema): + enhanced_prompt: str + rai_filtered_reason: Optional[str] + image: CustomImageResult + + +class SearchResponse(BaseSchema): + gemini_results: List[ImageGenerationResult] + imagen_results: List[ImageGenerationResult] diff --git a/gemini/sample-apps/quickbot/text-to-image-using-imagen3/backend/src/service/__init__.py b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/backend/src/service/__init__.py new file mode 100644 index 0000000..36a1e8d --- /dev/null +++ b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/backend/src/service/__init__.py @@ -0,0 +1,14 @@ +# Copyright 2025 Google LLC +# +# 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 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + diff --git a/gemini/sample-apps/quickbot/text-to-image-using-imagen3/backend/src/service/search.py b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/backend/src/service/search.py new file mode 100644 index 0000000..2215256 --- /dev/null +++ b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/backend/src/service/search.py @@ -0,0 +1,254 @@ +# Copyright 2025 Google LLC +# +# 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 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import asyncio +import base64 +from typing import List + +import google.auth +from google import genai +from google.genai import types + +from src.model.search import ( + CustomImageResult, + ImageGenerationResult, + SearchResponse, +) + + +class ImagenSearchService: + async def _generate_with_imagen( + self, + client: genai.Client, + term: str, + generation_model: str, + aspect_ratio: str, + number_of_images: int, + image_style: str, + ) -> List[ImageGenerationResult]: + try: + prompt_imagen = f"Make the image with a style '{image_style}'. The user prompt is: {term}" + print( + f"Calling Imagen model: {generation_model} for '{term}' with style '{image_style}'" + ) + + # Run the synchronous SDK call in a separate thread + images_imagen_response: types.GenerateImagesResponse = ( + await asyncio.to_thread( + client.models.generate_images, + model=generation_model, + prompt=prompt_imagen, + config=types.GenerateImagesConfig( + number_of_images=number_of_images, + aspect_ratio=aspect_ratio, + enhance_prompt=True, + safety_filter_level="BLOCK_MEDIUM_AND_ABOVE", + person_generation="DONT_ALLOW", + ), + ) + ) + + response_imagen = [ + ImageGenerationResult( + enhanced_prompt=generated_image.enhanced_prompt, + rai_filtered_reason=generated_image.rai_filtered_reason, + image=CustomImageResult( + gcs_uri=generated_image.image.gcs_uri, + encoded_image=base64.b64encode( + generated_image.image.image_bytes + ).decode("utf-8"), + mime_type=generated_image.image.mime_type, + ), + ) + for generated_image in images_imagen_response.generated_images + ] + print(f"Number of images created by Imagen: {len(response_imagen)}") + return response_imagen + except Exception as e: + print(f"Error during Imagen3 generation: {e}") + return [] + + async def _generate_with_gemini( + self, + client: genai.Client, + term: str, + number_of_images: int, + image_style: str, + ) -> List[ImageGenerationResult]: + response_gemini: List[ImageGenerationResult] = [] + try: + gemini_prompt_text = f"Create an image with a style '{image_style}' based on this user prompt: {term}" + print( + f"Calling Gemini model for '{term}' with style '{image_style}'" + ) + + for i in range( + number_of_images + ): # Loop as many times as images wanted + # Run the synchronous SDK call in a separate thread + gemini_api_response = await asyncio.to_thread( + client.models.generate_content, + model="gemini-2.0-flash-preview-image-generation", + contents=gemini_prompt_text, + config=types.GenerateContentConfig( + response_modalities=["TEXT", "IMAGE"] + ), + ) + + for candidate in gemini_api_response.candidates: + for part in candidate.content.parts: + if ( + part.inline_data is not None + and part.inline_data.mime_type.startswith("image/") + ): + encoded_image_bytes = base64.b64encode( + part.inline_data.data + ).decode("utf-8") + generated_text_for_prompt = "" + for p_text in candidate.content.parts: + if p_text.text is not None: + generated_text_for_prompt += ( + p_text.text + " " + ) + + finish_reason_str = ( + candidate.finish_reason.name + if candidate.finish_reason + else None + ) + if ( + gemini_api_response.prompt_feedback + and gemini_api_response.prompt_feedback.blocked + ): + block_reason = ( + gemini_api_response.prompt_feedback.block_reason + ) + block_reason_message = ( + gemini_api_response.prompt_feedback.block_reason_message + ) + finish_reason_str = block_reason_message or ( + block_reason.name + if block_reason + else "Blocked" + ) + + response_gemini.append( + ImageGenerationResult( + enhanced_prompt=generated_text_for_prompt.strip() + or gemini_prompt_text, + rai_filtered_reason=finish_reason_str, + image=CustomImageResult( + gcs_uri=None, + encoded_image=encoded_image_bytes, + mime_type=part.inline_data.mime_type, + ), + ) + ) + elif part.text is not None: + print( + f"Gemini Text Output (not an image part): {part.text}" + ) + + print(f"Number of images created by Gemini: {len(response_gemini)}") + return response_gemini + except Exception as e: + print(f"Error during Gemini generation: {e}") + return [] + + async def generate_images( + self, + term: str, + generation_model: str = "imagen-3.0-generate-002", + aspect_ratio: str = "1:1", + number_of_images: int = 2, + image_style: str = "modern", + ) -> SearchResponse: + _, PROJECT_ID = google.auth.default() + LOCATION = "us-central1" + + client = genai.Client( + vertexai=True, project=PROJECT_ID, location=LOCATION + ) + print("Async generate_images method called! Init parallel generation.") + + # Create a list of coroutines for Imagen generation + if generation_model == "imagen-4.0-ultra-generate-exp-05-20": + imagen_coroutines = [ + self._generate_with_imagen( + client=client, + term=term, + generation_model=generation_model, + aspect_ratio=aspect_ratio, + number_of_images=1, # Imagen 4 generates 1 image per call + image_style=image_style, + ) + for _ in range(number_of_images) + ] + else: + imagen_coroutines = [ + self._generate_with_imagen( + client=client, + term=term, + generation_model=generation_model, + aspect_ratio=aspect_ratio, + number_of_images=number_of_images, + image_style=image_style, + ) + ] + + gemini_coroutine = self._generate_with_gemini( + client=client, + term=term, + number_of_images=number_of_images, + image_style=image_style, + ) + + # Run tasks concurrently and gather results + # return_exceptions=True allows us to get results even if one task fails + all_tasks_to_gather = [*imagen_coroutines, gemini_coroutine] + results = await asyncio.gather( + *all_tasks_to_gather, return_exceptions=True + ) + + response_imagen: List[ImageGenerationResult] = [] + response_gemini: List[ImageGenerationResult] = [] + + num_imagen_tasks = len(imagen_coroutines) + + for i in range(num_imagen_tasks): + imagen_task_result = results[i] + if isinstance(imagen_task_result, Exception): + print( + f"Exception in Imagen generation task {i + 1}: {imagen_task_result}" + ) + elif imagen_task_result is not None: + response_imagen.extend(imagen_task_result) + + gemini_result_index = num_imagen_tasks + if gemini_result_index < len(results): + gemini_task_result = results[gemini_result_index] + if isinstance(gemini_task_result, Exception): + print( + f"Exception in Gemini generation task: {gemini_task_result}" + ) + elif gemini_task_result is not None: + response_gemini = gemini_task_result + else: + print( + "Gemini task result not found in the expected position in results list." + ) + + return SearchResponse( + gemini_results=response_gemini, imagen_results=response_imagen + ) diff --git a/gemini/sample-apps/quickbot/text-to-image-using-imagen3/backend/tests/test_search.py b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/backend/tests/test_search.py new file mode 100644 index 0000000..1a499bd --- /dev/null +++ b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/backend/tests/test_search.py @@ -0,0 +1,143 @@ +# Copyright 2025 Google LLC +# +# 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 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Tests for the search controller and service.""" + +import base64 +from unittest.mock import MagicMock + +import pytest +from fastapi.testclient import TestClient +from google.genai import types + +from src.controller.search import router +from src.model.search import ( + ImageGenerationResult, + CustomImageResult, +) +from src.service.search import ImagenSearchService + +# Create a test client for the FastAPI app +client = TestClient(router) + + +@pytest.fixture(scope="function", name="mock_genai_client") +def fixture_mock_genai_client(): + """Provides a mock google.genai Client.""" + mock_client = MagicMock() + mock_response = types.GenerateImagesResponse() + + # Create mock generated images with base64 encoded placeholder image data + mock_image = types.GeneratedImage() + mock_image.enhanced_prompt = "Mock enhanced prompt" + mock_image.image = types.Image() + mock_image.image.gcs_uri = "gs://mock_bucket/mock_image.png" + mock_image.image.image_bytes = b"mock_image_bytes" # Must be bytes + mock_image.image.mime_type = "image/png" + mock_response.generated_images = [ + mock_image, + mock_image, + mock_image, + mock_image, + ] + + mock_client.models.generate_images.return_value = mock_response + return mock_client + + +@pytest.fixture(scope="function", name="mock_imagen_search_service") +def fixture_mock_imagen_search_service(mock_genai_client): + """Provides a mock ImagenSearchService with a mock genai client.""" + service = ImagenSearchService() + service.client = mock_genai_client # Inject the mock client + return service + + +class TestSearchController: + """Tests for the /api/search endpoint.""" + + def test_search_endpoint(self, monkeypatch, mock_imagen_search_service): + # Mock the ImagenSearchService to avoid actual API calls + # Mock the google.auth.default to avoid authentication issues + with monkeypatch.context() as m: # use a context for clarity + mock_client_class = MagicMock( + return_value=mock_imagen_search_service.client + ) + m.setattr( + "src.controller.search.ImagenSearchService", + lambda: mock_imagen_search_service, + ) + m.setattr( + "src.service.search.google.auth.default", + lambda: (None, "test_project_id"), + ) + m.setattr( + "src.service.search.google.genai.Client", mock_client_class + ) + + search_term = "test search term" + response = client.post("/api/search", json={"term": search_term}) + + assert response.status_code == 200 + data = response.json() + assert len(data) == 4 + + for image_data in data: + assert image_data["enhancedPrompt"] == "Mock enhanced prompt" + assert ( + image_data["image"]["gcsUri"] + == "gs://mock_bucket/mock_image.png" + ) + assert image_data["image"]["mimeType"] == "image/png" + assert image_data["image"]["encodedImage"] == base64.b64encode( + b"mock_image_bytes" + ).decode("utf-8") + # other assertions based on the model + + +class TestImagenSearchService: + """Tests for the ImagenSearchService class.""" + + def test_imagen_search_service( + self, monkeypatch, mock_imagen_search_service + ): + + # Mock the google.auth.default to avoid authentication issues + with monkeypatch.context() as m: # use a context for clarity + mock_client_class = MagicMock( + return_value=mock_imagen_search_service.client + ) + m.setattr( + "src.service.search.google.auth.default", + lambda: (None, "test_project_id"), + ) + m.setattr( + "src.service.search.google.genai.Client", mock_client_class + ) + + search_term = "test search term" + results = mock_imagen_search_service.generate_images(search_term) + + assert isinstance(results, list) + assert len(results) == 4 # Number of mock images + assert all( + isinstance(result, ImageGenerationResult) for result in results + ) + mock_client_class.assert_called_once() + + for result in results: + assert isinstance(result.image, CustomImageResult) + assert result.image.encoded_image == base64.b64encode( + b"mock_image_bytes" + ).decode("utf-8") diff --git a/gemini/sample-apps/quickbot/text-to-image-using-imagen3/docker-compose.yml b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/docker-compose.yml new file mode 100644 index 0000000..06639d8 --- /dev/null +++ b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/docker-compose.yml @@ -0,0 +1,48 @@ +# Copyright 2025 Google LLC +# +# 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 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +services: + backend: + build: + context: ./backend + dockerfile: Dockerfile + container_name: quickbot-text-to-imagen-backend + ports: + - "8080:8080" + volumes: + - ./backend:/app + # Mount the gcloud ADC directory. Replace with the correct path for your OS if different. :ro - read-only for better security + - ~/.config/gcloud/:/root/.config/gcloud:ro # Linux/macOS example: + # - %APPDATA%/gcloud:/root/.config/gcloud:ro # Windows example: + environment: + ENVIRONMENT: development + FRONTEND_URL: http://localhost:4200 + GCLOUD_PROJECT: my-project-id + GOOGLE_APPLICATION_CREDENTIALS: "/root/.config/gcloud/application_default_credentials.json" + restart: unless-stopped + + frontend: + build: + context: ./frontend + dockerfile: Dockerfile + container_name: quickbot-text-to-imagen-frontend + ports: + - "4200:8080" + volumes: + - ./frontend:/app + # Use an anonymous volume for node_modules to prevent host node_modules + # from interfering and potentially speed up builds/rebuilds + - /app/node_modules + # To change the env variables in the frontend, change the environment.ts file + restart: unless-stopped \ No newline at end of file diff --git a/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/.dockerignore b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/.dockerignore new file mode 100644 index 0000000..7109f56 --- /dev/null +++ b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/.dockerignore @@ -0,0 +1,4 @@ +.angular +.vscode +dist +node_modules \ No newline at end of file diff --git a/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/.editorconfig b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/.editorconfig new file mode 100644 index 0000000..79fe802 --- /dev/null +++ b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/.editorconfig @@ -0,0 +1,8 @@ +root = true + +[*] +indent_style = space +indent_size = 2 +end_of_line = lf +charset = utf-8 +insert_final_newline = true diff --git a/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/.eslintignore b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/.eslintignore new file mode 100644 index 0000000..edd9d60 --- /dev/null +++ b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/.eslintignore @@ -0,0 +1,2 @@ +build/ +dist/ diff --git a/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/.eslintrc.json b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/.eslintrc.json new file mode 100644 index 0000000..f95bb33 --- /dev/null +++ b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/.eslintrc.json @@ -0,0 +1,3 @@ +{ + "extends": "./node_modules/gts/" +} diff --git a/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/.gitignore b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/.gitignore new file mode 100644 index 0000000..0711527 --- /dev/null +++ b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/.gitignore @@ -0,0 +1,42 @@ +# See http://help.github.com/ignore-files/ for more about ignoring files. + +# Compiled output +/dist +/tmp +/out-tsc +/bazel-out + +# Node +/node_modules +npm-debug.log +yarn-error.log + +# IDEs and editors +.idea/ +.project +.classpath +.c9/ +*.launch +.settings/ +*.sublime-workspace + +# Visual Studio Code +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +.history/* + +# Miscellaneous +/.angular/cache +.sass-cache/ +/connect.lock +/coverage +/libpeerconnection.log +testem.log +/typings + +# System files +.DS_Store +Thumbs.db diff --git a/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/.prettierrc.js b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/.prettierrc.js new file mode 100644 index 0000000..535df79 --- /dev/null +++ b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/.prettierrc.js @@ -0,0 +1,33 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +module.exports = { + ...require('gts/.prettierrc.json'), + "overrides": [ + { + "files": "*.html", + "options": { + "parser": "angular" + } + }, + { + "files": "*.scss", + "options": { + "parser": "scss" + } + } + ] +} diff --git a/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/Dockerfile b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/Dockerfile new file mode 100644 index 0000000..e2b6e1c --- /dev/null +++ b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/Dockerfile @@ -0,0 +1,27 @@ +# Copyright 2025 Google LLC +# +# 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 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +FROM node:18.17.1-alpine AS builder + +WORKDIR /app +COPY . /app/ + +RUN npm ci +RUN npm run build:prod + +FROM nginx:alpine +COPY --from=builder /app/dist/quick-bot-app-frontend /usr/share/nginx/html +COPY --from=builder /app/nginx.conf /etc/nginx/conf.d/ + +EXPOSE 8080 \ No newline at end of file diff --git a/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/README.md b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/README.md new file mode 100644 index 0000000..088bde5 --- /dev/null +++ b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/README.md @@ -0,0 +1,94 @@ +# QuickBot App Frontend | text-to-image-using-imagen3 + +[![linting: pylint](https://img.shields.io/badge/linting-pylint-yellowgreen)](https://github.com/pylint-dev/pylint) +[![Code Style: Google](https://img.shields.io/badge/code%20style-google-blueviolet.svg)](https://github.com/google/gts) + +QuickBot App is a set of templates that can be deployed out of the box into Cloud Run and work independently. Each one can be run independently connected to the user default google cloud auth credentials, and based on the complexity of each template, may require to deploy more or less resources into our Google Cloud Project. +The architecture always follows the following structure: a folder for the frontend which consists in an Angular app, and a backend folder which consists of a FastAPI Python app. + +This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 15.1.3. + +## Development server + +Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The application will automatically reload if you change any of the source files. + +## Code scaffolding + +Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`. + +## Build + +Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. + +## Running unit tests + +Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io). + +## Running end-to-end tests + +Run `ng e2e` to execute the end-to-end tests via a platform of your choice. To use this command, you need to first add a package that implements end-to-end testing capabilities. + +## Further help + +To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.io/cli) page. + + +## Code Styling & Commit Guidelines + +To maintain code quality and consistency: + +* **TypeScript (Frontend):** We follow [Angular Coding Style Guide](https://angular.dev/style-guide) by leveraging the use of [Google's TypeScript Style Guide](https://github.com/google/gts) using `gts`. This includes a formatter, linter, and automatic code fixer. +* **Python (Backend):** We adhere to the [Google Python Style Guide](https://google.github.io/styleguide/pyguide.html), using tools like `pylint` and `black` for linting and formatting. +* **Commit Messages:** We suggest following [Angular's Commit Message Guidelines](https://github.com/angular/angular/blob/main/contributing-docs/commit-message-guidelines.md) to create clear and descriptive commit messages. + +#### Frontend (TypeScript with `gts`) + +1. **Initialize `gts` (if not already done in the project):** + Navigate to the `frontend/` directory and run: + ```bash + npx gts init + ``` + This will set up `gts` and create necessary configuration files (like `tsconfig.json`). Ensure your `tsconfig.json` (or a related gts config file like `.gtsrc`) includes an extension for `gts` defaults, typically: + ```json + { + "extends": "./node_modules/gts/tsconfig-google.json", + // ... other configurations + } + ``` +2. **Check for linting issues:** + ```bash + npm run lint + ``` + (This assumes a `lint` script is defined in `package.json`, e.g., `"lint": "gts lint"`) +3. **Fix linting issues automatically (where possible):** + ```bash + npm run fix + ``` + (This assumes a `fix` script is defined in `package.json`, e.g., `"fix": "gts fix"`) + +#### Backend (Python with `pylint` and `black`) + +1. **Ensure Dependencies are Installed:** + Add `pylint` and `black` to your `backend/requirements.txt` file: + ``` + pylint + black + ``` + Then install them within your virtual environment: + ```bash + pip install pylint black + # or pip install -r requirements.txt + ``` +2. **Configure `pylint`:** + It's recommended to have a `.pylintrc` file in your `backend/` directory to configure `pylint` rules. You might need to copy a standard one or generate one (`pylint --generate-rcfile > .pylintrc`). +3. **Check for linting issues with `pylint`:** + Navigate to the `backend/` directory and run: + ```bash + pylint . + ``` + (Or specify modules/packages: `pylint your_module_name`) +4. **Format code with `black`:** + To automatically format all Python files in the current directory and subdirectories: + ```bash + python -m black . --line-length=80 + ``` diff --git a/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/angular.json b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/angular.json new file mode 100644 index 0000000..15d3e38 --- /dev/null +++ b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/angular.json @@ -0,0 +1,123 @@ +{ + "$schema": "./node_modules/@angular/cli/lib/config/schema.json", + "version": 1, + "newProjectRoot": "projects", + "projects": { + "quick-bot-app-frontend": { + "projectType": "application", + "schematics": { + "@schematics/angular:component": { + "style": "scss" + } + }, + "root": "", + "sourceRoot": "src", + "prefix": "app", + "architect": { + "build": { + "builder": "@angular-devkit/build-angular:browser", + "options": { + "outputPath": "dist/quick-bot-app-frontend", + "index": "src/index.html", + "main": "src/main.ts", + "polyfills": [ + "zone.js" + ], + "tsConfig": "tsconfig.app.json", + "inlineStyleLanguage": "scss", + "assets": [ + "src/favicon.ico", + "src/assets" + ], + "styles": [ + "@angular/material/prebuilt-themes/indigo-pink.css", + "src/styles.scss", + "node_modules/prismjs/themes/prism-okaidia.css" + ], + "scripts": [ + "node_modules/marked/marked.min.js", + "node_modules/prismjs/prism.js", + "node_modules/prismjs/components/prism-csharp.min.js", + "node_modules/prismjs/components/prism-css.min.js", + "node_modules/clipboard/dist/clipboard.min.js" + ] + }, + "configurations": { + "production": { + "budgets": [ + { + "type": "initial", + "maximumWarning": "4mb", + "maximumError": "10mb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "20kb", + "maximumError": "50kb" + } + ], + "outputHashing": "all" + }, + "development": { + "buildOptimizer": false, + "optimization": false, + "vendorChunk": true, + "extractLicenses": false, + "sourceMap": true, + "namedChunks": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "builder": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "browserTarget": "quick-bot-app-frontend:build:production" + }, + "development": { + "browserTarget": "quick-bot-app-frontend:build:development" + } + }, + "defaultConfiguration": "development" + }, + "extract-i18n": { + "builder": "@angular-devkit/build-angular:extract-i18n", + "options": { + "browserTarget": "quick-bot-app-frontend:build" + } + }, + "test": { + "builder": "@angular-devkit/build-angular:karma", + "options": { + "polyfills": [ + "zone.js", + "zone.js/testing" + ], + "tsConfig": "tsconfig.spec.json", + "inlineStyleLanguage": "scss", + "assets": [ + "src/favicon.ico", + "src/assets" + ], + "styles": [ + "@angular/material/prebuilt-themes/indigo-pink.css", + "src/styles.scss", + "node_modules/prismjs/themes/prism-okaidia.css" + ], + "scripts": [ + "node_modules/marked/marked.min.js", + "node_modules/prismjs/prism.js", + "node_modules/prismjs/components/prism-csharp.min.js", + "node_modules/prismjs/components/prism-css.min.js", + "node_modules/clipboard/dist/clipboard.min.js" + ] + } + } + } + } + }, + "cli": { + "analytics": false + } +} diff --git a/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/nginx.conf b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/nginx.conf new file mode 100644 index 0000000..6c8cce4 --- /dev/null +++ b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/nginx.conf @@ -0,0 +1,11 @@ +server { + listen 8080; + + root /usr/share/nginx/html; + index index.html index.htm; + include /etc/nginx/mime.types; + + location / { + try_files $uri $uri/ /index.html; + } +} \ No newline at end of file diff --git a/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/package-lock.json b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/package-lock.json new file mode 100644 index 0000000..49327ed --- /dev/null +++ b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/package-lock.json @@ -0,0 +1,25882 @@ +{ + "name": "quick-bot-app-frontend", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "quick-bot-app-frontend", + "version": "0.0.0", + "hasInstallScript": true, + "license": "ISC", + "dependencies": { + "@angular/animations": "^15.1.0", + "@angular/cdk": "^15.2.9", + "@angular/common": "^15.1.0", + "@angular/compiler": "^15.1.0", + "@angular/core": "^15.1.0", + "@angular/fire": "7.5", + "@angular/flex-layout": "^15.0.0-beta.42", + "@angular/forms": "^15.1.0", + "@angular/material": "^15.2.9", + "@angular/platform-browser": "^15.1.0", + "@angular/platform-browser-dynamic": "^15.1.0", + "@angular/router": "^15.1.0", + "@fortawesome/fontawesome-free": "^6.6.0", + "@ionic/angular": "^7.5.4", + "@ng-idle/core": "^14.0.0", + "clipboard": "^2.0.11", + "ionic-emoji-rating": "^1.0.15", + "marked": "^4.3.0", + "moment": "^2.30.1", + "ng2-pdf-viewer": "^7.0.2", + "ngx-markdown": "^15.1.2", + "prismjs": "^1.29.0", + "rxfire": "6.0.3", + "rxjs": "~7.8.0", + "tslib": "^2.3.0", + "uuid": "^9.0.1", + "zone.js": "~0.12.0" + }, + "devDependencies": { + "@angular-devkit/build-angular": "^15.1.3", + "@angular/cli": "~15.1.3", + "@angular/compiler-cli": "^15.1.0", + "@types/jasmine": "~4.3.0", + "@types/marked": "^4.3.0", + "@types/node": "20.12.7", + "@types/uuid": "^9.0.4", + "autoprefixer": "^10.4.21", + "firebase-tools": "^11.30.0", + "gts": "^5.3.1", + "jasmine-core": "~4.5.0", + "karma": "~6.4.0", + "karma-chrome-launcher": "~3.1.0", + "karma-coverage": "~2.2.0", + "karma-jasmine": "~5.1.0", + "karma-jasmine-html-reporter": "~2.0.0", + "postcss": "^8.5.3", + "prettier": "^3.5.1", + "tailwindcss": "^3.4.17", + "typescript": "~4.9.4" + } + }, + "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==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.0.tgz", + "integrity": "sha512-qRmjj8nj9qmLTQXXmaR1cck3UXSRMPrbsLJAasZpF+t3riI71BXed5ebIOYwQntykeZuhjsdweEc9BxH5Jc26w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jridgewell/gen-mapping": "^0.1.0", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@angular-devkit/architect": { + "version": "0.1502.11", + "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1502.11.tgz", + "integrity": "sha512-+hkG5UjIaKMRdo6SFLNQs+Cv7yAVeN8ijfDwI2z/mp7/otowuSEy+H3Tii195jfJ8TQ+y1B7svnx2D6O7oOYbQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "15.2.11", + "rxjs": "6.6.7" + }, + "engines": { + "node": "^14.20.0 || ^16.13.0 || >=18.10.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@angular-devkit/architect/node_modules/rxjs": { + "version": "6.6.7", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", + "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^1.9.0" + }, + "engines": { + "npm": ">=2.0.0" + } + }, + "node_modules/@angular-devkit/architect/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true, + "license": "0BSD" + }, + "node_modules/@angular-devkit/build-angular": { + "version": "15.2.11", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-angular/-/build-angular-15.2.11.tgz", + "integrity": "sha512-MnpVCJdk5jHuK7CH/cTcRT0JQkkKkRTEV3WTyOUhTm0O3PlKwvTM6/Sner+zyuhKyw5VFBBMypHh59aTUDEZ1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "2.2.0", + "@angular-devkit/architect": "0.1502.11", + "@angular-devkit/build-webpack": "0.1502.11", + "@angular-devkit/core": "15.2.11", + "@babel/core": "7.20.12", + "@babel/generator": "7.20.14", + "@babel/helper-annotate-as-pure": "7.18.6", + "@babel/helper-split-export-declaration": "7.18.6", + "@babel/plugin-proposal-async-generator-functions": "7.20.7", + "@babel/plugin-transform-async-to-generator": "7.20.7", + "@babel/plugin-transform-runtime": "7.19.6", + "@babel/preset-env": "7.20.2", + "@babel/runtime": "7.20.13", + "@babel/template": "7.20.7", + "@discoveryjs/json-ext": "0.5.7", + "@ngtools/webpack": "15.2.11", + "ansi-colors": "4.1.3", + "autoprefixer": "10.4.13", + "babel-loader": "9.1.2", + "babel-plugin-istanbul": "6.1.1", + "browserslist": "4.21.5", + "cacache": "17.0.4", + "chokidar": "3.5.3", + "copy-webpack-plugin": "11.0.0", + "critters": "0.0.16", + "css-loader": "6.7.3", + "esbuild-wasm": "0.17.8", + "glob": "8.1.0", + "https-proxy-agent": "5.0.1", + "inquirer": "8.2.4", + "jsonc-parser": "3.2.0", + "karma-source-map-support": "1.4.0", + "less": "4.1.3", + "less-loader": "11.1.0", + "license-webpack-plugin": "4.0.2", + "loader-utils": "3.2.1", + "magic-string": "0.29.0", + "mini-css-extract-plugin": "2.7.2", + "open": "8.4.1", + "ora": "5.4.1", + "parse5-html-rewriting-stream": "7.0.0", + "piscina": "3.2.0", + "postcss": "8.4.31", + "postcss-loader": "7.0.2", + "resolve-url-loader": "5.0.0", + "rxjs": "6.6.7", + "sass": "1.58.1", + "sass-loader": "13.2.0", + "semver": "7.5.3", + "source-map-loader": "4.0.1", + "source-map-support": "0.5.21", + "terser": "5.16.3", + "text-table": "0.2.0", + "tree-kill": "1.2.2", + "tslib": "2.5.0", + "webpack": "5.76.1", + "webpack-dev-middleware": "6.1.2", + "webpack-dev-server": "4.11.1", + "webpack-merge": "5.8.0", + "webpack-subresource-integrity": "5.1.0" + }, + "engines": { + "node": "^14.20.0 || ^16.13.0 || >=18.10.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "optionalDependencies": { + "esbuild": "0.17.8" + }, + "peerDependencies": { + "@angular/compiler-cli": "^15.0.0", + "@angular/localize": "^15.0.0", + "@angular/platform-server": "^15.0.0", + "@angular/service-worker": "^15.0.0", + "karma": "^6.3.0", + "ng-packagr": "^15.0.0", + "protractor": "^7.0.0", + "tailwindcss": "^2.0.0 || ^3.0.0", + "typescript": ">=4.8.2 <5.0" + }, + "peerDependenciesMeta": { + "@angular/localize": { + "optional": true + }, + "@angular/platform-server": { + "optional": true + }, + "@angular/service-worker": { + "optional": true + }, + "karma": { + "optional": true + }, + "ng-packagr": { + "optional": true + }, + "protractor": { + "optional": true + }, + "tailwindcss": { + "optional": true + } + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/@ngtools/webpack": { + "version": "15.2.11", + "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-15.2.11.tgz", + "integrity": "sha512-yqp+FziuJ+wIVij4eTqfhuiTPNaG1PU8ukeGOdqkVH4nQMlmzs9UldXy1iYC/6swzn6XO/pkqisU3m/jxemMzA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.20.0 || ^16.13.0 || >=18.10.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "@angular/compiler-cli": "^15.0.0", + "typescript": ">=4.8.2 <5.0", + "webpack": "^5.54.0" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/autoprefixer": { + "version": "10.4.13", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.13.tgz", + "integrity": "sha512-49vKpMqcZYsJjwotvt4+h/BCjJVnhGwcLpDt5xkcaOG3eLrG/HUYLagrihYsQ+qrIBgIzX1Rw7a6L8I/ZA1Atg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + } + ], + "dependencies": { + "browserslist": "^4.21.4", + "caniuse-lite": "^1.0.30001426", + "fraction.js": "^4.2.0", + "normalize-range": "^0.1.2", + "picocolors": "^1.0.0", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/postcss": { + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.6", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/rxjs": { + "version": "6.6.7", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", + "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^1.9.0" + }, + "engines": { + "npm": ">=2.0.0" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/rxjs/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true, + "license": "0BSD" + }, + "node_modules/@angular-devkit/build-angular/node_modules/tslib": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz", + "integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==", + "dev": true, + "license": "0BSD" + }, + "node_modules/@angular-devkit/build-webpack": { + "version": "0.1502.11", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.1502.11.tgz", + "integrity": "sha512-OTONIRp770Jfems4+cULmtoeSzjnpx5UjV2EazojnhRXXBSJMWRMPvwD2QvQl9UO/6eOV3d2mgmP2xOZgc/D6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/architect": "0.1502.11", + "rxjs": "6.6.7" + }, + "engines": { + "node": "^14.20.0 || ^16.13.0 || >=18.10.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "webpack": "^5.30.0", + "webpack-dev-server": "^4.0.0" + } + }, + "node_modules/@angular-devkit/build-webpack/node_modules/rxjs": { + "version": "6.6.7", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", + "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^1.9.0" + }, + "engines": { + "npm": ">=2.0.0" + } + }, + "node_modules/@angular-devkit/build-webpack/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true, + "license": "0BSD" + }, + "node_modules/@angular-devkit/core": { + "version": "15.2.11", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-15.2.11.tgz", + "integrity": "sha512-zd6QelJ8pOPvz6TsehR0JqixjDjzgEOkKywBJBuwNXY+Nw3MJGayJeWS0UgC+Gk+LoTkpI21RoyaYELkAmD/tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "8.12.0", + "ajv-formats": "2.1.1", + "jsonc-parser": "3.2.0", + "rxjs": "6.6.7", + "source-map": "0.7.4" + }, + "engines": { + "node": "^14.20.0 || ^16.13.0 || >=18.10.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "chokidar": "^3.5.2" + }, + "peerDependenciesMeta": { + "chokidar": { + "optional": true + } + } + }, + "node_modules/@angular-devkit/core/node_modules/rxjs": { + "version": "6.6.7", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", + "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^1.9.0" + }, + "engines": { + "npm": ">=2.0.0" + } + }, + "node_modules/@angular-devkit/core/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true, + "license": "0BSD" + }, + "node_modules/@angular-devkit/schematics": { + "version": "15.1.6", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-15.1.6.tgz", + "integrity": "sha512-cwmJFpS43zrdlmfwfHIxG/Nzg5rzFdtKrHx64ZXxNFm6JdyK2JTs/qrHUwv1FYWAcqhdiHn+00jYklMmvsvPOA==", + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "15.1.6", + "jsonc-parser": "3.2.0", + "magic-string": "0.27.0", + "ora": "5.4.1", + "rxjs": "6.6.7" + }, + "engines": { + "node": "^14.20.0 || ^16.13.0 || >=18.10.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@angular-devkit/schematics/node_modules/@angular-devkit/core": { + "version": "15.1.6", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-15.1.6.tgz", + "integrity": "sha512-jGgxyRjecVf6lEyqDxz7ltMEndNPxIg720pk6r40fgsu0dU8w9vjJSJe7k0XdJiXVRcN6wZa/J5nO/xcwWVIsA==", + "license": "MIT", + "dependencies": { + "ajv": "8.12.0", + "ajv-formats": "2.1.1", + "jsonc-parser": "3.2.0", + "rxjs": "6.6.7", + "source-map": "0.7.4" + }, + "engines": { + "node": "^14.20.0 || ^16.13.0 || >=18.10.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "chokidar": "^3.5.2" + }, + "peerDependenciesMeta": { + "chokidar": { + "optional": true + } + } + }, + "node_modules/@angular-devkit/schematics/node_modules/magic-string": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.27.0.tgz", + "integrity": "sha512-8UnnX2PeRAPZuN12svgR9j7M1uWMovg/CEnIwIG0LFkXSJJe4PdfUGiTGl8V9bsBHFUtfVINcSyYxd7q+kx9fA==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.13" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@angular-devkit/schematics/node_modules/rxjs": { + "version": "6.6.7", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", + "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^1.9.0" + }, + "engines": { + "npm": ">=2.0.0" + } + }, + "node_modules/@angular-devkit/schematics/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "license": "0BSD" + }, + "node_modules/@angular/animations": { + "version": "15.2.10", + "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-15.2.10.tgz", + "integrity": "sha512-yxfN8qQpMaukRU5LjFkJBmy85rqrOp86tYVCsf+hmPEFRiXBMUj6xYLeCMcpk3Mt1JtnWGBR34ivGx+7bNeAow==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^14.20.0 || ^16.13.0 || >=18.10.0" + }, + "peerDependencies": { + "@angular/core": "15.2.10" + } + }, + "node_modules/@angular/cdk": { + "version": "15.2.9", + "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-15.2.9.tgz", + "integrity": "sha512-koaM07N1AIQ5oHU27l0/FoQSSoYAwlAYwVZ4Di3bYrJsTBNCN2Xsby7wI8gZxdepMnV4Fe9si382BDBov+oO4Q==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "optionalDependencies": { + "parse5": "^7.1.2" + }, + "peerDependencies": { + "@angular/common": "^15.0.0 || ^16.0.0", + "@angular/core": "^15.0.0 || ^16.0.0", + "rxjs": "^6.5.3 || ^7.4.0" + } + }, + "node_modules/@angular/cli": { + "version": "15.1.6", + "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-15.1.6.tgz", + "integrity": "sha512-GmC9jZK2ipUWj0dlfTI5oEYia4y1fLfO3AtAKU5CylNYrGyB+DRytKY8Bx6Fs4kaNBY8V8YnyLi7E/78gziMdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/architect": "0.1501.6", + "@angular-devkit/core": "15.1.6", + "@angular-devkit/schematics": "15.1.6", + "@schematics/angular": "15.1.6", + "@yarnpkg/lockfile": "1.1.0", + "ansi-colors": "4.1.3", + "ini": "3.0.1", + "inquirer": "8.2.4", + "jsonc-parser": "3.2.0", + "npm-package-arg": "10.1.0", + "npm-pick-manifest": "8.0.1", + "open": "8.4.0", + "ora": "5.4.1", + "pacote": "15.0.8", + "resolve": "1.22.1", + "semver": "7.3.8", + "symbol-observable": "4.0.0", + "yargs": "17.6.2" + }, + "bin": { + "ng": "bin/ng.js" + }, + "engines": { + "node": "^14.20.0 || ^16.13.0 || >=18.10.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@angular/cli/node_modules/@angular-devkit/architect": { + "version": "0.1501.6", + "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1501.6.tgz", + "integrity": "sha512-u07zZFlfrg0Qn4mu5M9Nz0pH2Yd2028XF/73980PsZMxwkSm4diF08v4bHk3UyR7yPT7phwvt4znj6ryZhx1gw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "15.1.6", + "rxjs": "6.6.7" + }, + "engines": { + "node": "^14.20.0 || ^16.13.0 || >=18.10.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@angular/cli/node_modules/@angular-devkit/core": { + "version": "15.1.6", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-15.1.6.tgz", + "integrity": "sha512-jGgxyRjecVf6lEyqDxz7ltMEndNPxIg720pk6r40fgsu0dU8w9vjJSJe7k0XdJiXVRcN6wZa/J5nO/xcwWVIsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "8.12.0", + "ajv-formats": "2.1.1", + "jsonc-parser": "3.2.0", + "rxjs": "6.6.7", + "source-map": "0.7.4" + }, + "engines": { + "node": "^14.20.0 || ^16.13.0 || >=18.10.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "chokidar": "^3.5.2" + }, + "peerDependenciesMeta": { + "chokidar": { + "optional": true + } + } + }, + "node_modules/@angular/cli/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@angular/cli/node_modules/open": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/open/-/open-8.4.0.tgz", + "integrity": "sha512-XgFPPM+B28FtCCgSb9I+s9szOC1vZRSwgWsRUA5ylIxRTgKozqjOCrVOqGsYABPYK5qnfqClxZTFBa8PKt2v6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-lazy-prop": "^2.0.0", + "is-docker": "^2.1.1", + "is-wsl": "^2.2.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@angular/cli/node_modules/rxjs": { + "version": "6.6.7", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", + "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^1.9.0" + }, + "engines": { + "npm": ">=2.0.0" + } + }, + "node_modules/@angular/cli/node_modules/semver": { + "version": "7.3.8", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", + "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", + "dev": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@angular/cli/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true, + "license": "0BSD" + }, + "node_modules/@angular/cli/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/@angular/common": { + "version": "15.2.10", + "resolved": "https://registry.npmjs.org/@angular/common/-/common-15.2.10.tgz", + "integrity": "sha512-jdBn3fctkqoNrJn9VLsUHpcCEhCxWSczdsR+BBbD6T0oLl6vMrAVNjPwfBejnlgfWN1KoRU9kgOYsMxa5apIWQ==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^14.20.0 || ^16.13.0 || >=18.10.0" + }, + "peerDependencies": { + "@angular/core": "15.2.10", + "rxjs": "^6.5.3 || ^7.4.0" + } + }, + "node_modules/@angular/compiler": { + "version": "15.2.10", + "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-15.2.10.tgz", + "integrity": "sha512-M0XkeU0O73UlJZwDvOyp8/apetz9UKj78eTFDseMYJDLcxe6MpkbkxqpsGZnKYDj7LIep8PmCAKEkhtenE82zw==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^14.20.0 || ^16.13.0 || >=18.10.0" + }, + "peerDependencies": { + "@angular/core": "15.2.10" + }, + "peerDependenciesMeta": { + "@angular/core": { + "optional": true + } + } + }, + "node_modules/@angular/compiler-cli": { + "version": "15.2.10", + "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-15.2.10.tgz", + "integrity": "sha512-mCFIxrs60XicKfA2o42hA7LrQvhybi9BQveWuZn/2iIEOXx7R62Iemz8E21pLWftAZHGxEW3NECfBrY1d3gVmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "7.19.3", + "@jridgewell/sourcemap-codec": "^1.4.14", + "chokidar": "^3.0.0", + "convert-source-map": "^1.5.1", + "dependency-graph": "^0.11.0", + "magic-string": "^0.27.0", + "reflect-metadata": "^0.1.2", + "semver": "^7.0.0", + "tslib": "^2.3.0", + "yargs": "^17.2.1" + }, + "bin": { + "ng-xi18n": "bundles/src/bin/ng_xi18n.js", + "ngc": "bundles/src/bin/ngc.js", + "ngcc": "bundles/ngcc/main-ngcc.js" + }, + "engines": { + "node": "^14.20.0 || ^16.13.0 || >=18.10.0" + }, + "peerDependencies": { + "@angular/compiler": "15.2.10", + "typescript": ">=4.8.2 <5.0" + } + }, + "node_modules/@angular/compiler-cli/node_modules/@babel/core": { + "version": "7.19.3", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.19.3.tgz", + "integrity": "sha512-WneDJxdsjEvyKtXKsaBGbDeiyOjR5vYq4HcShxnIbG0qixpoHjI3MqeZM9NDvsojNCEBItQE4juOo/bU6e72gQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.1.0", + "@babel/code-frame": "^7.18.6", + "@babel/generator": "^7.19.3", + "@babel/helper-compilation-targets": "^7.19.3", + "@babel/helper-module-transforms": "^7.19.0", + "@babel/helpers": "^7.19.0", + "@babel/parser": "^7.19.3", + "@babel/template": "^7.18.10", + "@babel/traverse": "^7.19.3", + "@babel/types": "^7.19.3", + "convert-source-map": "^1.7.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.1", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@angular/compiler-cli/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/@angular/compiler-cli/node_modules/magic-string": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.27.0.tgz", + "integrity": "sha512-8UnnX2PeRAPZuN12svgR9j7M1uWMovg/CEnIwIG0LFkXSJJe4PdfUGiTGl8V9bsBHFUtfVINcSyYxd7q+kx9fA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.13" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@angular/core": { + "version": "15.2.10", + "resolved": "https://registry.npmjs.org/@angular/core/-/core-15.2.10.tgz", + "integrity": "sha512-meGGidnitQJGDxYd9/LrqYiVlId+vGaLoiLgJdKBz+o2ZO6OmXQGuNw2VBqf17/Cc0/UjzrOY7+kILNFKkk/WQ==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^14.20.0 || ^16.13.0 || >=18.10.0" + }, + "peerDependencies": { + "rxjs": "^6.5.3 || ^7.4.0", + "zone.js": "~0.11.4 || ~0.12.0 || ~0.13.0" + } + }, + "node_modules/@angular/fire": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/@angular/fire/-/fire-7.5.0.tgz", + "integrity": "sha512-M/JJ5SKnpfNZFLKl0Awjn3r2NhURy2Mx+E8EzSDWSg1sHD7IThrynZYPUSZR+cd3KGsGoEacIP2x8Ra+0a+CVg==", + "license": "MIT", + "dependencies": { + "@angular-devkit/schematics": "^12.0.0 || ^13.0.0 || ^14.0.0 || ^15.0.0", + "@schematics/angular": "^12.0.0 || ^13.0.0 || ^14.0.0 || ^15.0.0", + "file-loader": "^6.2.0", + "firebase": "^9.8.0", + "fs-extra": "^8.0.1", + "fuzzy": "^0.1.3", + "inquirer": "^8.1.1", + "inquirer-autocomplete-prompt": "^1.0.1", + "jsonc-parser": "^3.0.0", + "node-fetch": "^2.6.1", + "open": "^8.0.0", + "ora": "^5.3.0", + "rxfire": "^6.0.0", + "semver": "^7.1.3", + "triple-beam": "^1.3.0", + "tslib": "^2.0.0", + "winston": "^3.0.0" + }, + "peerDependencies": { + "@angular/common": "^12.0.0 || ^13.0.0 || ^14.0.0 || ^15.0.0", + "@angular/core": "^12.0.0 || ^13.0.0 || ^14.0.0 || ^15.0.0", + "@angular/platform-browser": "^12.0.0 || ^13.0.0 || ^14.0.0 || ^15.0.0", + "@angular/platform-browser-dynamic": "^12.0.0 || ^13.0.0 || ^14.0.0 || ^15.0.0", + "firebase-tools": "^9.9.0 || ^10.0.0 || ^11.0.0", + "rxjs": "~6.6.0 || ^7.0.0" + }, + "peerDependenciesMeta": { + "firebase-tools": { + "optional": true + } + } + }, + "node_modules/@angular/flex-layout": { + "version": "15.0.0-beta.42", + "resolved": "https://registry.npmjs.org/@angular/flex-layout/-/flex-layout-15.0.0-beta.42.tgz", + "integrity": "sha512-cTAPVMMxnyIFwpZwdq0PL5mdP9Qh+R8MB7ZBezVaN3Rz2fRrkagzKpLvPX3TFzepXrvHBdpKsU4b8u+NxEC/6g==", + "deprecated": "This package has been deprecated. Please see https://blog.angular.io/modern-css-in-angular-layouts-4a259dca9127", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "peerDependencies": { + "@angular/cdk": ">=15.0.0", + "@angular/common": ">=15.0.2", + "@angular/core": ">=15.0.2", + "@angular/platform-browser": ">=15.0.2", + "rxjs": "^6.5.3 || ^7.4.0" + } + }, + "node_modules/@angular/forms": { + "version": "15.2.10", + "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-15.2.10.tgz", + "integrity": "sha512-NIntGsNcN6o8L1txsbWXOf6f3K/CUBizdKsxsYVYGJIXEW5qU6UnWmfAZffNNXsT/XvbgUCjgDwT0cAwcqZPuQ==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^14.20.0 || ^16.13.0 || >=18.10.0" + }, + "peerDependencies": { + "@angular/common": "15.2.10", + "@angular/core": "15.2.10", + "@angular/platform-browser": "15.2.10", + "rxjs": "^6.5.3 || ^7.4.0" + } + }, + "node_modules/@angular/material": { + "version": "15.2.9", + "resolved": "https://registry.npmjs.org/@angular/material/-/material-15.2.9.tgz", + "integrity": "sha512-emuFF/7+91Jq+6kVCl3FiVoFLtAZoh+woFQWNuK8nhx0HmD4ckLFI8d9a6ERYR3zRuKhq5deSRE2kYsfpjrrsQ==", + "license": "MIT", + "dependencies": { + "@material/animation": "15.0.0-canary.684e33d25.0", + "@material/auto-init": "15.0.0-canary.684e33d25.0", + "@material/banner": "15.0.0-canary.684e33d25.0", + "@material/base": "15.0.0-canary.684e33d25.0", + "@material/button": "15.0.0-canary.684e33d25.0", + "@material/card": "15.0.0-canary.684e33d25.0", + "@material/checkbox": "15.0.0-canary.684e33d25.0", + "@material/chips": "15.0.0-canary.684e33d25.0", + "@material/circular-progress": "15.0.0-canary.684e33d25.0", + "@material/data-table": "15.0.0-canary.684e33d25.0", + "@material/density": "15.0.0-canary.684e33d25.0", + "@material/dialog": "15.0.0-canary.684e33d25.0", + "@material/dom": "15.0.0-canary.684e33d25.0", + "@material/drawer": "15.0.0-canary.684e33d25.0", + "@material/elevation": "15.0.0-canary.684e33d25.0", + "@material/fab": "15.0.0-canary.684e33d25.0", + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "@material/floating-label": "15.0.0-canary.684e33d25.0", + "@material/form-field": "15.0.0-canary.684e33d25.0", + "@material/icon-button": "15.0.0-canary.684e33d25.0", + "@material/image-list": "15.0.0-canary.684e33d25.0", + "@material/layout-grid": "15.0.0-canary.684e33d25.0", + "@material/line-ripple": "15.0.0-canary.684e33d25.0", + "@material/linear-progress": "15.0.0-canary.684e33d25.0", + "@material/list": "15.0.0-canary.684e33d25.0", + "@material/menu": "15.0.0-canary.684e33d25.0", + "@material/menu-surface": "15.0.0-canary.684e33d25.0", + "@material/notched-outline": "15.0.0-canary.684e33d25.0", + "@material/radio": "15.0.0-canary.684e33d25.0", + "@material/ripple": "15.0.0-canary.684e33d25.0", + "@material/rtl": "15.0.0-canary.684e33d25.0", + "@material/segmented-button": "15.0.0-canary.684e33d25.0", + "@material/select": "15.0.0-canary.684e33d25.0", + "@material/shape": "15.0.0-canary.684e33d25.0", + "@material/slider": "15.0.0-canary.684e33d25.0", + "@material/snackbar": "15.0.0-canary.684e33d25.0", + "@material/switch": "15.0.0-canary.684e33d25.0", + "@material/tab": "15.0.0-canary.684e33d25.0", + "@material/tab-bar": "15.0.0-canary.684e33d25.0", + "@material/tab-indicator": "15.0.0-canary.684e33d25.0", + "@material/tab-scroller": "15.0.0-canary.684e33d25.0", + "@material/textfield": "15.0.0-canary.684e33d25.0", + "@material/theme": "15.0.0-canary.684e33d25.0", + "@material/tooltip": "15.0.0-canary.684e33d25.0", + "@material/top-app-bar": "15.0.0-canary.684e33d25.0", + "@material/touch-target": "15.0.0-canary.684e33d25.0", + "@material/typography": "15.0.0-canary.684e33d25.0", + "tslib": "^2.3.0" + }, + "peerDependencies": { + "@angular/animations": "^15.0.0 || ^16.0.0", + "@angular/cdk": "15.2.9", + "@angular/common": "^15.0.0 || ^16.0.0", + "@angular/core": "^15.0.0 || ^16.0.0", + "@angular/forms": "^15.0.0 || ^16.0.0", + "@angular/platform-browser": "^15.0.0 || ^16.0.0", + "rxjs": "^6.5.3 || ^7.4.0" + } + }, + "node_modules/@angular/platform-browser": { + "version": "15.2.10", + "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-15.2.10.tgz", + "integrity": "sha512-9tbgVGSJqwfrOzT8aA/kWBLNhJSQ9gUg0CJxwFBSJm8VkBUJrszoBlDsnSvlxx8/W2ejNULKHFTXeUzq0O/+RQ==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^14.20.0 || ^16.13.0 || >=18.10.0" + }, + "peerDependencies": { + "@angular/animations": "15.2.10", + "@angular/common": "15.2.10", + "@angular/core": "15.2.10" + }, + "peerDependenciesMeta": { + "@angular/animations": { + "optional": true + } + } + }, + "node_modules/@angular/platform-browser-dynamic": { + "version": "15.2.10", + "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-15.2.10.tgz", + "integrity": "sha512-JHP6W+FX715Qv7DhqvfZLuBZXSDJrboiQsR06gUAgDSjAUyhbqmpVg/2YOtgeWpPkzNDtXdPU2PhcRdIv5J3Yg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^14.20.0 || ^16.13.0 || >=18.10.0" + }, + "peerDependencies": { + "@angular/common": "15.2.10", + "@angular/compiler": "15.2.10", + "@angular/core": "15.2.10", + "@angular/platform-browser": "15.2.10" + } + }, + "node_modules/@angular/router": { + "version": "15.2.10", + "resolved": "https://registry.npmjs.org/@angular/router/-/router-15.2.10.tgz", + "integrity": "sha512-LmuqEg0iIXSw7bli6HKJ19cbxP91v37GtRwbGKswyLihqzTgvjBYpvcfMnB5FRQ5LWkTwq5JclkX03dZw290Yg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^14.20.0 || ^16.13.0 || >=18.10.0" + }, + "peerDependencies": { + "@angular/common": "15.2.10", + "@angular/core": "15.2.10", + "@angular/platform-browser": "15.2.10", + "rxjs": "^6.5.3 || ^7.4.0" + } + }, + "node_modules/@assemblyscript/loader": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@assemblyscript/loader/-/loader-0.10.1.tgz", + "integrity": "sha512-H71nDOOL8Y7kWRLqf6Sums+01Q5msqBW2KhDUTemh1tvY04eSkSXrK0uj/4mmY0Xr16/3zyZmsrxN7CKuRbNRg==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/@babel/code-frame": { + "version": "7.26.2", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", + "integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.25.9", + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.26.5", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.26.5.tgz", + "integrity": "sha512-XvcZi1KWf88RVbF9wn8MN6tYFloU5qX8KjuF3E1PVBmJ9eypXfs4GRiJwLuTZL0iSnJUKn1BFPa5BPZZJyFzPg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.20.12", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.20.12.tgz", + "integrity": "sha512-XsMfHovsUYHFMdrIHkZphTN/2Hzzi78R08NuHfDBehym2VsPDL6Zn/JAD/JQdnRvbSsbQc4mVaU1m6JgtTEElg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.1.0", + "@babel/code-frame": "^7.18.6", + "@babel/generator": "^7.20.7", + "@babel/helper-compilation-targets": "^7.20.7", + "@babel/helper-module-transforms": "^7.20.11", + "@babel/helpers": "^7.20.7", + "@babel/parser": "^7.20.7", + "@babel/template": "^7.20.7", + "@babel/traverse": "^7.20.12", + "@babel/types": "^7.20.7", + "convert-source-map": "^1.7.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.2", + "semver": "^6.3.0" + }, + "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.20.14", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.20.14.tgz", + "integrity": "sha512-AEmuXHdcD3A52HHXxaTmYlb8q/xMEhoRP67B3T4Oq7lbmSoqroMZzjnGj3+i1io3pdnF8iBYVu4Ilj+c4hBxYg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.20.7", + "@jridgewell/gen-mapping": "^0.3.2", + "jsesc": "^2.5.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/generator/node_modules/@jridgewell/gen-mapping": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", + "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/helper-annotate-as-pure": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.18.6.tgz", + "integrity": "sha512-duORpUiYrEpzKIop6iNbjnwKLAKnJ47csTyRACyEmWj0QdUrm5aqNJGHSSEQSUAvNW0ojX0dOmK9dZduvkfeXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.26.5", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.26.5.tgz", + "integrity": "sha512-IXuyn5EkouFJscIDuFF5EsiSolseme1s0CZB+QxVugqJLYmKdxI1VfIBOst0SUu4rnk2Z7kqTwmoO1lp3HIfnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.26.5", + "@babel/helper-validator-option": "^7.25.9", + "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/browserslist": { + "version": "4.24.4", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.4.tgz", + "integrity": "sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==", + "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": { + "caniuse-lite": "^1.0.30001688", + "electron-to-chromium": "^1.5.73", + "node-releases": "^2.0.19", + "update-browserslist-db": "^1.1.1" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "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-create-class-features-plugin": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.25.9.tgz", + "integrity": "sha512-UTZQMvt0d/rSz6KI+qdu7GQze5TIajwTS++GUozlw8VBJDEOAqSXwm1WvmYEZwqdqSGQshRocPDqrt4HBZB3fQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.25.9", + "@babel/helper-member-expression-to-functions": "^7.25.9", + "@babel/helper-optimise-call-expression": "^7.25.9", + "@babel/helper-replace-supers": "^7.25.9", + "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9", + "@babel/traverse": "^7.25.9", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-create-class-features-plugin/node_modules/@babel/helper-annotate-as-pure": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.25.9.tgz", + "integrity": "sha512-gv7320KBUFJz1RnylIg5WWYPRXKZ884AGkYpgpWW02TH66Dl+HaC1t1CKd0z3R4b6hdYEcmrNZHUmfCP+1u3/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-create-class-features-plugin/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-create-regexp-features-plugin": { + "version": "7.26.3", + "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.26.3.tgz", + "integrity": "sha512-G7ZRb40uUgdKOQqPLjfD12ZmGA54PzqDFUv2BKImnC9QIfGhIHKvVML0oN8IUiDq4iRqpq74ABpvOaerfWdong==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.25.9", + "regexpu-core": "^6.2.0", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-create-regexp-features-plugin/node_modules/@babel/helper-annotate-as-pure": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.25.9.tgz", + "integrity": "sha512-gv7320KBUFJz1RnylIg5WWYPRXKZ884AGkYpgpWW02TH66Dl+HaC1t1CKd0z3R4b6hdYEcmrNZHUmfCP+1u3/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-create-regexp-features-plugin/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-define-polyfill-provider": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.3.3.tgz", + "integrity": "sha512-z5aQKU4IzbqCC1XH0nAqfsFLMVSo22SBKUc0BxGrLkolTdPTructy0ToNnlO2zA4j9Q/7pjMZf0DSY+DSTYzww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-compilation-targets": "^7.17.7", + "@babel/helper-plugin-utils": "^7.16.7", + "debug": "^4.1.1", + "lodash.debounce": "^4.0.8", + "resolve": "^1.14.2", + "semver": "^6.1.2" + }, + "peerDependencies": { + "@babel/core": "^7.4.0-0" + } + }, + "node_modules/@babel/helper-define-polyfill-provider/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-environment-visitor": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.24.7.tgz", + "integrity": "sha512-DoiN84+4Gnd0ncbBOM9AZENV4a5ZiL39HYMyZJGZ/AZEykHYdJw0wW3kdcsh9/Kn+BRXHLkkklZ51ecPKmI1CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-member-expression-to-functions": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.25.9.tgz", + "integrity": "sha512-wbfdZ9w5vk0C0oyHqAJbc62+vet5prjj01jjJ8sKn3j9h3MQQlflEdXYvuqRWjHnM12coDEqiC1IRCi0U/EKwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.25.9", + "@babel/types": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.25.9.tgz", + "integrity": "sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.25.9", + "@babel/types": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.26.0.tgz", + "integrity": "sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.25.9", + "@babel/helper-validator-identifier": "^7.25.9", + "@babel/traverse": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-optimise-call-expression": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.25.9.tgz", + "integrity": "sha512-FIpuNaz5ow8VyrYcnXQTDRGvV6tTjkNtCK/RYNDXGSLlUD6cBuQTSw43CShGxjvfBTfcUA/r6UhUCbtYqkhcuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.26.5", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.26.5.tgz", + "integrity": "sha512-RS+jZcRdZdRFzMyr+wcsaqOmld1/EqTghfaBGQQd/WnRdzdlvSZ//kF7U8VQTxf1ynZ4cjUcYgjVGx13ewNPMg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-remap-async-to-generator": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.25.9.tgz", + "integrity": "sha512-IZtukuUeBbhgOcaW2s06OXTzVNJR0ybm4W5xC1opWFFJMZbwRj5LCk+ByYH7WdZPZTt8KnFwA8pvjN2yqcPlgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.25.9", + "@babel/helper-wrap-function": "^7.25.9", + "@babel/traverse": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-remap-async-to-generator/node_modules/@babel/helper-annotate-as-pure": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.25.9.tgz", + "integrity": "sha512-gv7320KBUFJz1RnylIg5WWYPRXKZ884AGkYpgpWW02TH66Dl+HaC1t1CKd0z3R4b6hdYEcmrNZHUmfCP+1u3/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-replace-supers": { + "version": "7.26.5", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.26.5.tgz", + "integrity": "sha512-bJ6iIVdYX1YooY2X7w1q6VITt+LnUILtNk7zT78ykuwStx8BauCzxvFqFaHjOpW1bVnSUM1PN1f0p5P21wHxvg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-member-expression-to-functions": "^7.25.9", + "@babel/helper-optimise-call-expression": "^7.25.9", + "@babel/traverse": "^7.26.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-skip-transparent-expression-wrappers": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.25.9.tgz", + "integrity": "sha512-K4Du3BFa3gvyhzgPcntrkDgZzQaq6uozzcpGbOO1OEJaI+EJdqWIMTLgFgQf6lrfiDFo5FU+BxKepI9RmZqahA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.25.9", + "@babel/types": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-split-export-declaration": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.18.6.tgz", + "integrity": "sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", + "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", + "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.25.9.tgz", + "integrity": "sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-wrap-function": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.25.9.tgz", + "integrity": "sha512-ETzz9UTjQSTmw39GboatdymDq4XIQbR8ySgVrylRhPOFpsd+JrKHIuF0de7GCWmem+T4uC5z7EZguod7Wj4A4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.25.9", + "@babel/traverse": "^7.25.9", + "@babel/types": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-wrap-function/node_modules/@babel/template": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.9.tgz", + "integrity": "sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.25.9", + "@babel/parser": "^7.25.9", + "@babel/types": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.26.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.7.tgz", + "integrity": "sha512-8NHiL98vsi0mbPQmYAGWwfcFaOy4j2HY49fXJCfuDcdE7fMIsH9a7GdaeXpIBsbT7307WU8KCMp5pUVDNL4f9A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.25.9", + "@babel/types": "^7.26.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers/node_modules/@babel/template": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.9.tgz", + "integrity": "sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.25.9", + "@babel/parser": "^7.25.9", + "@babel/types": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.26.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.7.tgz", + "integrity": "sha512-kEvgGGgEjRUutvdVvZhbn/BxVt+5VSpwXz1j3WYXQbXDo8KzFOPNG2GQbdAiNq8g6wn1yKk7C/qrke03a84V+w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.26.7" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.25.9.tgz", + "integrity": "sha512-2qUwwfAFpJLZqxd02YW9btUCZHl+RFvdDkNfZwaIJrvB8Tesjsk8pEQkTvGwZXLqXUx/2oyY3ySRhm6HOXuCug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.25.9.tgz", + "integrity": "sha512-6xWgLZTJXwilVjlnV7ospI3xi+sl8lN8rXXbBD6vYn3UYDlGsag8wrZkKcSI8G6KgqKP7vNFaDgeDnfAABq61g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9", + "@babel/plugin-transform-optional-chaining": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.13.0" + } + }, + "node_modules/@babel/plugin-proposal-async-generator-functions": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.20.7.tgz", + "integrity": "sha512-xMbiLsn/8RK7Wq7VeVytytS2L6qE69bXPB10YCmMdDZbKF4okCqY74pI/jJQ/8U0b/F6NrT2+14b8/P9/3AMGA==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-async-generator-functions instead.", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-environment-visitor": "^7.18.9", + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/helper-remap-async-to-generator": "^7.18.9", + "@babel/plugin-syntax-async-generators": "^7.8.4" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-class-properties": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.18.6.tgz", + "integrity": "sha512-cumfXOF0+nzZrrN8Rf0t7M+tF6sZc7vhQwYQck9q1/5w2OExlD+b4v4RpMJFaV1Z7WcDRgO6FqvxqxGlwo+RHQ==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-class-properties instead.", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-class-static-block": { + "version": "7.21.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-static-block/-/plugin-proposal-class-static-block-7.21.0.tgz", + "integrity": "sha512-XP5G9MWNUskFuP30IfFSEFB0Z6HzLIUcjYM4bYOPHXl7eiJ9HFv8tWj6TXTN5QODiEhDZAeI4hLok2iHFFV4hw==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-class-static-block instead.", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.21.0", + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/plugin-syntax-class-static-block": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.12.0" + } + }, + "node_modules/@babel/plugin-proposal-dynamic-import": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.18.6.tgz", + "integrity": "sha512-1auuwmK+Rz13SJj36R+jqFPMJWyKEDd7lLSdOj4oJK0UTgGueSAtkrCvz9ewmgyU/P941Rv2fQwZJN8s6QruXw==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-dynamic-import instead.", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6", + "@babel/plugin-syntax-dynamic-import": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-export-namespace-from": { + "version": "7.18.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-export-namespace-from/-/plugin-proposal-export-namespace-from-7.18.9.tgz", + "integrity": "sha512-k1NtHyOMvlDDFeb9G5PhUXuGj8m/wiwojgQVEhJ/fsVsMCpLyOP4h0uGEjYJKrRI+EVPlb5Jk+Gt9P97lOGwtA==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-export-namespace-from instead.", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.9", + "@babel/plugin-syntax-export-namespace-from": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-json-strings": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.18.6.tgz", + "integrity": "sha512-lr1peyn9kOdbYc0xr0OdHTZ5FMqS6Di+H0Fz2I/JwMzGmzJETNeOFq2pBySw6X/KFL5EWDjlJuMsUGRFb8fQgQ==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-json-strings instead.", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6", + "@babel/plugin-syntax-json-strings": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-logical-assignment-operators": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-logical-assignment-operators/-/plugin-proposal-logical-assignment-operators-7.20.7.tgz", + "integrity": "sha512-y7C7cZgpMIjWlKE5T7eJwp+tnRYM89HmRvWM5EQuB5BoHEONjmQ8lSNmBUwOyy/GFRsohJED51YBF79hE1djug==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-logical-assignment-operators instead.", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-nullish-coalescing-operator": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.18.6.tgz", + "integrity": "sha512-wQxQzxYeJqHcfppzBDnm1yAY0jSRkUXR2z8RePZYrKwMKgMlE8+Z6LUno+bd6LvbGh8Gltvy74+9pIYkr+XkKA==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-nullish-coalescing-operator instead.", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-numeric-separator": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.18.6.tgz", + "integrity": "sha512-ozlZFogPqoLm8WBr5Z8UckIoE4YQ5KESVcNudyXOR8uqIkliTEgJ3RoketfG6pmzLdeZF0H/wjE9/cCEitBl7Q==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-numeric-separator instead.", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6", + "@babel/plugin-syntax-numeric-separator": "^7.10.4" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-object-rest-spread": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.20.7.tgz", + "integrity": "sha512-d2S98yCiLxDVmBmE8UjGcfPvNEUbA1U5q5WxaWFUGRzJSVAZqm5W6MbPct0jxnegUZ0niLeNX+IOzEs7wYg9Dg==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-object-rest-spread instead.", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.20.5", + "@babel/helper-compilation-targets": "^7.20.7", + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-transform-parameters": "^7.20.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-optional-catch-binding": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.18.6.tgz", + "integrity": "sha512-Q40HEhs9DJQyaZfUjjn6vE8Cv4GmMHCYuMGIWUnlxH6400VGxOuwWsPt4FxXxJkC/5eOzgn0z21M9gMT4MOhbw==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-optional-catch-binding instead.", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-optional-chaining": { + "version": "7.21.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.21.0.tgz", + "integrity": "sha512-p4zeefM72gpmEe2fkUr/OnOXpWEf8nAgk7ZYVqqfFiyIG7oFfVZcCrU64hWn5xp4tQ9LkV4bTIa5rD0KANpKNA==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-optional-chaining instead.", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/helper-skip-transparent-expression-wrappers": "^7.20.0", + "@babel/plugin-syntax-optional-chaining": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-private-methods": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-methods/-/plugin-proposal-private-methods-7.18.6.tgz", + "integrity": "sha512-nutsvktDItsNn4rpGItSNV2sz1XwS+nfU0Rg8aCx3W3NOKVzdMjJRu0O5OkgDp3ZGICSTbgRpxZoWsxoKRvbeA==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-private-methods instead.", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-private-property-in-object": { + "version": "7.21.11", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.11.tgz", + "integrity": "sha512-0QZ8qP/3RLDVBwBFoWAwCtgcDZJVwA5LUJRZU8x2YFfKNuFq161wK3cuGrALu5yiPu+vzwTAg/sMWVNeWeNyaw==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-private-property-in-object instead.", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.18.6", + "@babel/helper-create-class-features-plugin": "^7.21.0", + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-unicode-property-regex": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.18.6.tgz", + "integrity": "sha512-2BShG/d5yoZyXZfVePH91urL5wTG6ASZU9M4o03lKK8u8UW1y08OMttBSOADTcJrnPMpvDXRG3G8fyLh4ovs8w==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-unicode-property-regex instead.", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-dynamic-import": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz", + "integrity": "sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-export-namespace-from": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz", + "integrity": "sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.3" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-assertions": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.26.0.tgz", + "integrity": "sha512-QCWT5Hh830hK5EQa7XzuqIkQU9tT/whqbDz7kuaZMHFl1inRRg7JnuAEOQ0Ur0QUl0NufCk1msK2BeY79Aj/eg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-arrow-functions": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.25.9.tgz", + "integrity": "sha512-6jmooXYIwn9ca5/RylZADJ+EnSxVUS5sjeJ9UPk6RWRzXCmOJCy6dqItPJFpw2cuCangPK4OYr5uhGKcmrm5Qg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-async-to-generator": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.20.7.tgz", + "integrity": "sha512-Uo5gwHPT9vgnSXQxqGtpdufUiWp96gk7yiP4Mp5bm1QMkEmLXBO7PAGYbKoJ6DhAwiNkcHFBol/x5zZZkL/t0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.18.6", + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/helper-remap-async-to-generator": "^7.18.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-block-scoped-functions": { + "version": "7.26.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.26.5.tgz", + "integrity": "sha512-chuTSY+hq09+/f5lMj8ZSYgCFpppV2CbYrhNFJ1BFoXpiWPnnAb7R0MqrafCpN8E1+YRrtM1MXZHJdIx8B6rMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.26.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-block-scoping": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.25.9.tgz", + "integrity": "sha512-1F05O7AYjymAtqbsFETboN1NvBdcnzMerO+zlMyJBEz6WkMdejvGWw9p05iTSjC85RLlBseHHQpYaM4gzJkBGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-classes": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.25.9.tgz", + "integrity": "sha512-mD8APIXmseE7oZvZgGABDyM34GUmK45Um2TXiBUt7PnuAxrgoSVf123qUzPxEr/+/BHrRn5NMZCdE2m/1F8DGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.25.9", + "@babel/helper-compilation-targets": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/helper-replace-supers": "^7.25.9", + "@babel/traverse": "^7.25.9", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-classes/node_modules/@babel/helper-annotate-as-pure": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.25.9.tgz", + "integrity": "sha512-gv7320KBUFJz1RnylIg5WWYPRXKZ884AGkYpgpWW02TH66Dl+HaC1t1CKd0z3R4b6hdYEcmrNZHUmfCP+1u3/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/plugin-transform-computed-properties": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.25.9.tgz", + "integrity": "sha512-HnBegGqXZR12xbcTHlJ9HGxw1OniltT26J5YpfruGqtUHlz/xKf/G2ak9e+t0rVqrjXa9WOhvYPz1ERfMj23AA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/template": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-computed-properties/node_modules/@babel/template": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.9.tgz", + "integrity": "sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.25.9", + "@babel/parser": "^7.25.9", + "@babel/types": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/plugin-transform-destructuring": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.25.9.tgz", + "integrity": "sha512-WkCGb/3ZxXepmMiX101nnGiU+1CAdut8oHyEOHxkKuS1qKpU2SMXE2uSvfz8PBuLd49V6LEsbtyPhWC7fnkgvQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-dotall-regex": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.25.9.tgz", + "integrity": "sha512-t7ZQ7g5trIgSRYhI9pIJtRl64KHotutUJsh4Eze5l7olJv+mRSg4/MmbZ0tv1eeqRbdvo/+trvJD/Oc5DmW2cA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-duplicate-keys": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.25.9.tgz", + "integrity": "sha512-LZxhJ6dvBb/f3x8xwWIuyiAHy56nrRG3PeYTpBkkzkYRRQ6tJLu68lEF5VIqMUZiAV7a8+Tb78nEoMCMcqjXBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-exponentiation-operator": { + "version": "7.26.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.26.3.tgz", + "integrity": "sha512-7CAHcQ58z2chuXPWblnn1K6rLDnDWieghSOEmqQsrBenH0P9InCUtOJYD89pvngljmZlJcz3fcmgYsXFNGa1ZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-for-of": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.25.9.tgz", + "integrity": "sha512-LqHxduHoaGELJl2uhImHwRQudhCM50pT46rIBNvtT/Oql3nqiS3wOwP+5ten7NpYSXrrVLgtZU3DZmPtWZo16A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-function-name": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.25.9.tgz", + "integrity": "sha512-8lP+Yxjv14Vc5MuWBpJsoUCd3hD6V9DgBon2FVYL4jJgbnVQ9fTgYmonchzZJOVNgzEgbxp4OwAf6xz6M/14XA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-compilation-targets": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/traverse": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-literals": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.25.9.tgz", + "integrity": "sha512-9N7+2lFziW8W9pBl2TzaNht3+pgMIRP74zizeCSrtnSKVdUl8mAjjOP2OOVQAfZ881P2cNjDj1uAMEdeD50nuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-member-expression-literals": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.25.9.tgz", + "integrity": "sha512-PYazBVfofCQkkMzh2P6IdIUaCEWni3iYEerAsRWuVd8+jlM1S9S9cz1dF9hIzyoZ8IA3+OwVYIp9v9e+GbgZhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-amd": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.25.9.tgz", + "integrity": "sha512-g5T11tnI36jVClQlMlt4qKDLlWnG5pP9CSM4GhdRciTNMRgkfpo5cR6b4rGIOYPgRRuFAvwjPQ/Yk+ql4dyhbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-commonjs": { + "version": "7.26.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.26.3.tgz", + "integrity": "sha512-MgR55l4q9KddUDITEzEFYn5ZsGDXMSsU9E+kh7fjRXTIC3RHqfCo8RPRbyReYJh44HQ/yomFkqbOFohXvDCiIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.26.0", + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-systemjs": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.25.9.tgz", + "integrity": "sha512-hyss7iIlH/zLHaehT+xwiymtPOpsiwIIRlCAOwBB04ta5Tt+lNItADdlXw3jAWZ96VJ2jlhl/c+PNIQPKNfvcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/helper-validator-identifier": "^7.25.9", + "@babel/traverse": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-umd": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.25.9.tgz", + "integrity": "sha512-bS9MVObUgE7ww36HEfwe6g9WakQ0KF07mQF74uuXdkoziUPfKyu/nIm663kz//e5O1nPInPFx36z7WJmJ4yNEw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-named-capturing-groups-regex": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.25.9.tgz", + "integrity": "sha512-oqB6WHdKTGl3q/ItQhpLSnWWOpjUJLsOCLVyeFgeTktkBSCiurvPOsyt93gibI9CmuKvTUEtWmG5VhZD+5T/KA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-new-target": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.25.9.tgz", + "integrity": "sha512-U/3p8X1yCSoKyUj2eOBIx3FOn6pElFOKvAAGf8HTtItuPyB+ZeOqfn+mvTtg9ZlOAjsPdK3ayQEjqHjU/yLeVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-object-super": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.25.9.tgz", + "integrity": "sha512-Kj/Gh+Rw2RNLbCK1VAWj2U48yxxqL2x0k10nPtSdRa0O2xnHXalD0s+o1A6a0W43gJ00ANo38jxkQreckOzv5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/helper-replace-supers": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-optional-chaining": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.25.9.tgz", + "integrity": "sha512-6AvV0FsLULbpnXeBjrY4dmWF8F7gf8QnvTEoO/wX/5xm/xE1Xo8oPuD3MPS+KS9f9XBEAWN7X1aWr4z9HdOr7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-parameters": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.25.9.tgz", + "integrity": "sha512-wzz6MKwpnshBAiRmn4jR8LYz/g8Ksg0o80XmwZDlordjwEk9SxBzTWC7F5ef1jhbrbOW2DJ5J6ayRukrJmnr0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-property-literals": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.25.9.tgz", + "integrity": "sha512-IvIUeV5KrS/VPavfSM/Iu+RE6llrHrYIKY1yfCzyO/lMXHQ+p7uGhonmGVisv6tSBSVgWzMBohTcvkC9vQcQFA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-regenerator": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.25.9.tgz", + "integrity": "sha512-vwDcDNsgMPDGP0nMqzahDWE5/MLcX8sv96+wfX7as7LoF/kr97Bo/7fI00lXY4wUXYfVmwIIyG80fGZ1uvt2qg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9", + "regenerator-transform": "^0.15.2" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-reserved-words": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.25.9.tgz", + "integrity": "sha512-7DL7DKYjn5Su++4RXu8puKZm2XBPHyjWLUidaPEkCUBbE7IPcsrkRHggAOOKydH1dASWdcUBxrkOGNxUv5P3Jg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-runtime": { + "version": "7.19.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.19.6.tgz", + "integrity": "sha512-PRH37lz4JU156lYFW1p8OxE5i7d6Sl/zV58ooyr+q1J1lnQPyg5tIiXlIwNVhJaY4W3TmOtdc8jqdXQcB1v5Yw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.18.6", + "@babel/helper-plugin-utils": "^7.19.0", + "babel-plugin-polyfill-corejs2": "^0.3.3", + "babel-plugin-polyfill-corejs3": "^0.6.0", + "babel-plugin-polyfill-regenerator": "^0.4.1", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-runtime/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/plugin-transform-shorthand-properties": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.25.9.tgz", + "integrity": "sha512-MUv6t0FhO5qHnS/W8XCbHmiRWOphNufpE1IVxhK5kuN3Td9FT1x4rx4K42s3RYdMXCXpfWkGSbCSd0Z64xA7Ng==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-spread": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.25.9.tgz", + "integrity": "sha512-oNknIB0TbURU5pqJFVbOOFspVlrpVwo2H1+HUIsVDvp5VauGGDP1ZEvO8Nn5xyMEs3dakajOxlmkNW7kNgSm6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-sticky-regex": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.25.9.tgz", + "integrity": "sha512-WqBUSgeVwucYDP9U/xNRQam7xV8W5Zf+6Eo7T2SRVUFlhRiMNFdFz58u0KZmCVVqs2i7SHgpRnAhzRNmKfi2uA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-template-literals": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.25.9.tgz", + "integrity": "sha512-o97AE4syN71M/lxrCtQByzphAdlYluKPDBzDVzMmfCobUjjhAryZV0AIpRPrxN0eAkxXO6ZLEScmt+PNhj2OTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-typeof-symbol": { + "version": "7.26.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.26.7.tgz", + "integrity": "sha512-jfoTXXZTgGg36BmhqT3cAYK5qkmqvJpvNrPhaK/52Vgjhw4Rq29s9UqpWWV0D6yuRmgiFH/BUVlkl96zJWqnaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.26.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-escapes": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.25.9.tgz", + "integrity": "sha512-s5EDrE6bW97LtxOcGj1Khcx5AaXwiMmi4toFWRDP9/y0Woo6pXC+iyPu/KuhKtfSrNFd7jJB+/fkOtZy6aIC6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-regex": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.25.9.tgz", + "integrity": "sha512-yoxstj7Rg9dlNn9UQxzk4fcNivwv4nUYz7fYXBaKxvw/lnmPuOm/ikoELygbYq68Bls3D/D+NBPHiLwZdZZ4HA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-env": { + "version": "7.20.2", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.20.2.tgz", + "integrity": "sha512-1G0efQEWR1EHkKvKHqbG+IN/QdgwfByUpM5V5QroDzGV2t3S/WXNQd693cHiHTlCFMpr9B6FkPFXDA2lQcKoDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.20.1", + "@babel/helper-compilation-targets": "^7.20.0", + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/helper-validator-option": "^7.18.6", + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.18.6", + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.18.9", + "@babel/plugin-proposal-async-generator-functions": "^7.20.1", + "@babel/plugin-proposal-class-properties": "^7.18.6", + "@babel/plugin-proposal-class-static-block": "^7.18.6", + "@babel/plugin-proposal-dynamic-import": "^7.18.6", + "@babel/plugin-proposal-export-namespace-from": "^7.18.9", + "@babel/plugin-proposal-json-strings": "^7.18.6", + "@babel/plugin-proposal-logical-assignment-operators": "^7.18.9", + "@babel/plugin-proposal-nullish-coalescing-operator": "^7.18.6", + "@babel/plugin-proposal-numeric-separator": "^7.18.6", + "@babel/plugin-proposal-object-rest-spread": "^7.20.2", + "@babel/plugin-proposal-optional-catch-binding": "^7.18.6", + "@babel/plugin-proposal-optional-chaining": "^7.18.9", + "@babel/plugin-proposal-private-methods": "^7.18.6", + "@babel/plugin-proposal-private-property-in-object": "^7.18.6", + "@babel/plugin-proposal-unicode-property-regex": "^7.18.6", + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-dynamic-import": "^7.8.3", + "@babel/plugin-syntax-export-namespace-from": "^7.8.3", + "@babel/plugin-syntax-import-assertions": "^7.20.0", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5", + "@babel/plugin-transform-arrow-functions": "^7.18.6", + "@babel/plugin-transform-async-to-generator": "^7.18.6", + "@babel/plugin-transform-block-scoped-functions": "^7.18.6", + "@babel/plugin-transform-block-scoping": "^7.20.2", + "@babel/plugin-transform-classes": "^7.20.2", + "@babel/plugin-transform-computed-properties": "^7.18.9", + "@babel/plugin-transform-destructuring": "^7.20.2", + "@babel/plugin-transform-dotall-regex": "^7.18.6", + "@babel/plugin-transform-duplicate-keys": "^7.18.9", + "@babel/plugin-transform-exponentiation-operator": "^7.18.6", + "@babel/plugin-transform-for-of": "^7.18.8", + "@babel/plugin-transform-function-name": "^7.18.9", + "@babel/plugin-transform-literals": "^7.18.9", + "@babel/plugin-transform-member-expression-literals": "^7.18.6", + "@babel/plugin-transform-modules-amd": "^7.19.6", + "@babel/plugin-transform-modules-commonjs": "^7.19.6", + "@babel/plugin-transform-modules-systemjs": "^7.19.6", + "@babel/plugin-transform-modules-umd": "^7.18.6", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.19.1", + "@babel/plugin-transform-new-target": "^7.18.6", + "@babel/plugin-transform-object-super": "^7.18.6", + "@babel/plugin-transform-parameters": "^7.20.1", + "@babel/plugin-transform-property-literals": "^7.18.6", + "@babel/plugin-transform-regenerator": "^7.18.6", + "@babel/plugin-transform-reserved-words": "^7.18.6", + "@babel/plugin-transform-shorthand-properties": "^7.18.6", + "@babel/plugin-transform-spread": "^7.19.0", + "@babel/plugin-transform-sticky-regex": "^7.18.6", + "@babel/plugin-transform-template-literals": "^7.18.9", + "@babel/plugin-transform-typeof-symbol": "^7.18.9", + "@babel/plugin-transform-unicode-escapes": "^7.18.10", + "@babel/plugin-transform-unicode-regex": "^7.18.6", + "@babel/preset-modules": "^0.1.5", + "@babel/types": "^7.20.2", + "babel-plugin-polyfill-corejs2": "^0.3.3", + "babel-plugin-polyfill-corejs3": "^0.6.0", + "babel-plugin-polyfill-regenerator": "^0.4.1", + "core-js-compat": "^3.25.1", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-env/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/preset-modules": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.6.tgz", + "integrity": "sha512-ID2yj6K/4lKfhuU3+EX4UvNbIt7eACFbHmNUjzA+ep+B5971CknnA/9DEWKbRokfbbtblxxxXFJJrH47UEAMVg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/plugin-proposal-unicode-property-regex": "^7.4.4", + "@babel/plugin-transform-dotall-regex": "^7.4.4", + "@babel/types": "^7.4.4", + "esutils": "^2.0.2" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.20.13", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.20.13.tgz", + "integrity": "sha512-gt3PKXs0DBoL9xCvOIIZ2NEqAGZqHjAnmVbfQtB620V0uReIQutpel14KcneZuer7UioY8ALKZ7iocavvzTNFA==", + "dev": true, + "license": "MIT", + "dependencies": { + "regenerator-runtime": "^0.13.11" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.20.7.tgz", + "integrity": "sha512-8SegXApWe6VoNw0r9JHpSteLKTpTiLZ4rMlGIm9JQ18KiCtyQiAMEazujAHrUS5flrcqYZa75ukev3P6QmUwUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.18.6", + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.26.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.26.7.tgz", + "integrity": "sha512-1x1sgeyRLC3r5fQOM0/xtQKsYjyxmFjaOrLJNtZ81inNjyJHGIolTULPiSc/2qe1/qfpFLisLQYFnnZl7QoedA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.26.2", + "@babel/generator": "^7.26.5", + "@babel/parser": "^7.26.7", + "@babel/template": "^7.25.9", + "@babel/types": "^7.26.7", + "debug": "^4.3.1", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse/node_modules/@babel/generator": { + "version": "7.26.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.5.tgz", + "integrity": "sha512-2caSP6fN9I7HOe6nqhtft7V4g7/V/gfDsC3Ag4W7kEzzvRGKqiv0pu0HogPiZ3KaVSoNDhUws6IJjDjpfmYIXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.26.5", + "@babel/types": "^7.26.5", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse/node_modules/@babel/template": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.9.tgz", + "integrity": "sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.25.9", + "@babel/parser": "^7.25.9", + "@babel/types": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse/node_modules/@jridgewell/gen-mapping": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", + "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/traverse/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/@babel/types": { + "version": "7.26.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.7.tgz", + "integrity": "sha512-t8kDRGrKXyp6+tjUh7hw2RLyclsW4TRoRvRHtSyAX9Bb5ldlFh+90YAYY6awRXrlB4G5G2izNeGySpATlFzmOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.25.9", + "@babel/helper-validator-identifier": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@braintree/sanitize-url": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/@braintree/sanitize-url/-/sanitize-url-6.0.4.tgz", + "integrity": "sha512-s3jaWicZd0pkP0jf5ysyHUI/RE7MHos6qlToFcGWXVp+ykHOy77OUMrfbgJ9it2C5bow7OIQwYYaHjk9XlBQ2A==", + "license": "MIT", + "optional": true + }, + "node_modules/@colors/colors": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", + "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/@dabh/diagnostics": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.3.tgz", + "integrity": "sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==", + "license": "MIT", + "dependencies": { + "colorspace": "1.1.x", + "enabled": "2.0.x", + "kuler": "^2.0.0" + } + }, + "node_modules/@discoveryjs/json-ext": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz", + "integrity": "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.17.8", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.17.8.tgz", + "integrity": "sha512-0/rb91GYKhrtbeglJXOhAv9RuYimgI8h623TplY2X+vA4EXnk3Zj1fXZreJ0J3OJJu1bwmb0W7g+2cT/d8/l/w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.17.8", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.17.8.tgz", + "integrity": "sha512-oa/N5j6v1svZQs7EIRPqR8f+Bf8g6HBDjD/xHC02radE/NjKHK7oQmtmLxPs1iVwYyvE+Kolo6lbpfEQ9xnhxQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.17.8", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.17.8.tgz", + "integrity": "sha512-bTliMLqD7pTOoPg4zZkXqCDuzIUguEWLpeqkNfC41ODBHwoUgZ2w5JBeYimv4oP6TDVocoYmEhZrCLQTrH89bg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.17.8", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.17.8.tgz", + "integrity": "sha512-ghAbV3ia2zybEefXRRm7+lx8J/rnupZT0gp9CaGy/3iolEXkJ6LYRq4IpQVI9zR97ID80KJVoUlo3LSeA/sMAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.17.8", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.17.8.tgz", + "integrity": "sha512-n5WOpyvZ9TIdv2V1K3/iIkkJeKmUpKaCTdun9buhGRWfH//osmUjlv4Z5mmWdPWind/VGcVxTHtLfLCOohsOXw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.17.8", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.17.8.tgz", + "integrity": "sha512-a/SATTaOhPIPFWvHZDoZYgxaZRVHn0/LX1fHLGfZ6C13JqFUZ3K6SMD6/HCtwOQ8HnsNaEeokdiDSFLuizqv5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.17.8", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.17.8.tgz", + "integrity": "sha512-xpFJb08dfXr5+rZc4E+ooZmayBW6R3q59daCpKZ/cDU96/kvDM+vkYzNeTJCGd8rtO6fHWMq5Rcv/1cY6p6/0Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.17.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.17.8.tgz", + "integrity": "sha512-6Ij8gfuGszcEwZpi5jQIJCVIACLS8Tz2chnEBfYjlmMzVsfqBP1iGmHQPp7JSnZg5xxK9tjCc+pJ2WtAmPRFVA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.17.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.17.8.tgz", + "integrity": "sha512-v3iwDQuDljLTxpsqQDl3fl/yihjPAyOguxuloON9kFHYwopeJEf1BkDXODzYyXEI19gisEsQlG1bM65YqKSIww==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.17.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.17.8.tgz", + "integrity": "sha512-8svILYKhE5XetuFk/B6raFYIyIqydQi+GngEXJgdPdI7OMKUbSd7uzR02wSY4kb53xBrClLkhH4Xs8P61Q2BaA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.17.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.17.8.tgz", + "integrity": "sha512-B6FyMeRJeV0NpyEOYlm5qtQfxbdlgmiGdD+QsipzKfFky0K5HW5Td6dyK3L3ypu1eY4kOmo7wW0o94SBqlqBSA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.17.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.17.8.tgz", + "integrity": "sha512-CCb67RKahNobjm/eeEqeD/oJfJlrWyw29fgiyB6vcgyq97YAf3gCOuP6qMShYSPXgnlZe/i4a8WFHBw6N8bYAA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.17.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.17.8.tgz", + "integrity": "sha512-bytLJOi55y55+mGSdgwZ5qBm0K9WOCh0rx+vavVPx+gqLLhxtSFU0XbeYy/dsAAD6xECGEv4IQeFILaSS2auXw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.17.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.17.8.tgz", + "integrity": "sha512-2YpRyQJmKVBEHSBLa8kBAtbhucaclb6ex4wchfY0Tj3Kg39kpjeJ9vhRU7x4mUpq8ISLXRXH1L0dBYjAeqzZAw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.17.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.17.8.tgz", + "integrity": "sha512-QgbNY/V3IFXvNf11SS6exkpVcX0LJcob+0RWCgV9OiDAmVElnxciHIisoSix9uzYzScPmS6dJFbZULdSAEkQVw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.17.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.17.8.tgz", + "integrity": "sha512-mM/9S0SbAFDBc4OPoyP6SEOo5324LpUxdpeIUUSrSTOfhHU9hEfqRngmKgqILqwx/0DVJBzeNW7HmLEWp9vcOA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.17.8", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.17.8.tgz", + "integrity": "sha512-eKUYcWaWTaYr9zbj8GertdVtlt1DTS1gNBWov+iQfWuWyuu59YN6gSEJvFzC5ESJ4kMcKR0uqWThKUn5o8We6Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.17.8", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.17.8.tgz", + "integrity": "sha512-Vc9J4dXOboDyMXKD0eCeW0SIeEzr8K9oTHJU+Ci1mZc5njPfhKAqkRt3B/fUNU7dP+mRyralPu8QUkiaQn7iIg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.17.8", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.17.8.tgz", + "integrity": "sha512-0xvOTNuPXI7ft1LYUgiaXtpCEjp90RuBBYovdd2lqAFxje4sEucurg30M1WIm03+3jxByd3mfo+VUmPtRSVuOw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.17.8", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.17.8.tgz", + "integrity": "sha512-G0JQwUI5WdEFEnYNKzklxtBheCPkuDdu1YrtRrjuQv30WsYbkkoixKxLLv8qhJmNI+ATEWquZe/N0d0rpr55Mg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.17.8", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.17.8.tgz", + "integrity": "sha512-Fqy63515xl20OHGFykjJsMnoIWS+38fqfg88ClvPXyDbLtgXal2DTlhb1TfTX34qWi3u4I7Cq563QcHpqgLx8w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.17.8", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.17.8.tgz", + "integrity": "sha512-1iuezdyDNngPnz8rLRDO2C/ZZ/emJLb72OsZeqQ6gL6Avko/XCXZw+NuxBSNhBAP13Hie418V7VMt9et1FMvpg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.1.tgz", + "integrity": "sha512-s3O3waFUrMV8P/XaF/+ZTp1X9XBZW1a4B97ZnjQF2KYWaFD2A8KyFBsrsfSjEmjn3RGWAIuvlneuZm3CUK3jbA==", + "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.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", + "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", + "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/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "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/@eslint/eslintrc/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/@eslint/eslintrc/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/eslintrc/node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/@eslint/eslintrc/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/@eslint/eslintrc/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/eslintrc/node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz", + "integrity": "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@firebase/analytics": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@firebase/analytics/-/analytics-0.10.0.tgz", + "integrity": "sha512-Locv8gAqx0e+GX/0SI3dzmBY5e9kjVDtD+3zCFLJ0tH2hJwuCAiL+5WkHuxKj92rqQj/rvkBUCfA1ewlX2hehg==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.6.4", + "@firebase/installations": "0.6.4", + "@firebase/logger": "0.4.0", + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/analytics-compat": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/@firebase/analytics-compat/-/analytics-compat-0.2.6.tgz", + "integrity": "sha512-4MqpVLFkGK7NJf/5wPEEP7ePBJatwYpyjgJ+wQHQGHfzaCDgntOnl9rL2vbVGGKCnRqWtZDIWhctB86UWXaX2Q==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/analytics": "0.10.0", + "@firebase/analytics-types": "0.8.0", + "@firebase/component": "0.6.4", + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/analytics-types": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@firebase/analytics-types/-/analytics-types-0.8.0.tgz", + "integrity": "sha512-iRP+QKI2+oz3UAh4nPEq14CsEjrjD6a5+fuypjScisAh9kXKFvdJOZJDwk7kikLvWVLGEs9+kIUS4LPQV7VZVw==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/app": { + "version": "0.9.13", + "resolved": "https://registry.npmjs.org/@firebase/app/-/app-0.9.13.tgz", + "integrity": "sha512-GfiI1JxJ7ecluEmDjPzseRXk/PX31hS7+tjgBopL7XjB2hLUdR+0FTMXy2Q3/hXezypDvU6or7gVFizDESrkXw==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.6.4", + "@firebase/logger": "0.4.0", + "@firebase/util": "1.9.3", + "idb": "7.1.1", + "tslib": "^2.1.0" + } + }, + "node_modules/@firebase/app-check": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@firebase/app-check/-/app-check-0.8.0.tgz", + "integrity": "sha512-dRDnhkcaC2FspMiRK/Vbp+PfsOAEP6ZElGm9iGFJ9fDqHoPs0HOPn7dwpJ51lCFi1+2/7n5pRPGhqF/F03I97g==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.6.4", + "@firebase/logger": "0.4.0", + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/app-check-compat": { + "version": "0.3.7", + "resolved": "https://registry.npmjs.org/@firebase/app-check-compat/-/app-check-compat-0.3.7.tgz", + "integrity": "sha512-cW682AxsyP1G+Z0/P7pO/WT2CzYlNxoNe5QejVarW2o5ZxeWSSPAiVEwpEpQR/bUlUmdeWThYTMvBWaopdBsqw==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/app-check": "0.8.0", + "@firebase/app-check-types": "0.5.0", + "@firebase/component": "0.6.4", + "@firebase/logger": "0.4.0", + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/app-check-interop-types": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@firebase/app-check-interop-types/-/app-check-interop-types-0.3.0.tgz", + "integrity": "sha512-xAxHPZPIgFXnI+vb4sbBjZcde7ZluzPPaSK7Lx3/nmuVk4TjZvnL8ONnkd4ERQKL8WePQySU+pRcWkh8rDf5Sg==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/app-check-types": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@firebase/app-check-types/-/app-check-types-0.5.0.tgz", + "integrity": "sha512-uwSUj32Mlubybw7tedRzR24RP8M8JUVR3NPiMk3/Z4bCmgEKTlQBwMXrehDAZ2wF+TsBq0SN1c6ema71U/JPyQ==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/app-compat": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/@firebase/app-compat/-/app-compat-0.2.13.tgz", + "integrity": "sha512-j6ANZaWjeVy5zg6X7uiqh6lM6o3n3LD1+/SJFNs9V781xyryyZWXe+tmnWNWPkP086QfJoNkWN9pMQRqSG4vMg==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/app": "0.9.13", + "@firebase/component": "0.6.4", + "@firebase/logger": "0.4.0", + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" + } + }, + "node_modules/@firebase/app-types": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@firebase/app-types/-/app-types-0.9.0.tgz", + "integrity": "sha512-AeweANOIo0Mb8GiYm3xhTEBVCmPwTYAu9Hcd2qSkLuga/6+j9b1Jskl5bpiSQWy9eJ/j5pavxj6eYogmnuzm+Q==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/auth": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/@firebase/auth/-/auth-0.23.2.tgz", + "integrity": "sha512-dM9iJ0R6tI1JczuGSxXmQbXAgtYie0K4WvKcuyuSTCu9V8eEDiz4tfa1sO3txsfvwg7nOY3AjoCyMYEdqZ8hdg==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.6.4", + "@firebase/logger": "0.4.0", + "@firebase/util": "1.9.3", + "node-fetch": "2.6.7", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/auth-compat": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@firebase/auth-compat/-/auth-compat-0.4.2.tgz", + "integrity": "sha512-Q30e77DWXFmXEt5dg5JbqEDpjw9y3/PcP9LslDPR7fARmAOTIY9MM6HXzm9KC+dlrKH/+p6l8g9ifJiam9mc4A==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/auth": "0.23.2", + "@firebase/auth-types": "0.12.0", + "@firebase/component": "0.6.4", + "@firebase/util": "1.9.3", + "node-fetch": "2.6.7", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/auth-compat/node_modules/node-fetch": { + "version": "2.6.7", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", + "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/@firebase/auth-interop-types": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@firebase/auth-interop-types/-/auth-interop-types-0.2.1.tgz", + "integrity": "sha512-VOaGzKp65MY6P5FI84TfYKBXEPi6LmOCSMMzys6o2BN2LOsqy7pCuZCup7NYnfbk5OkkQKzvIfHOzTm0UDpkyg==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/auth-types": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@firebase/auth-types/-/auth-types-0.12.0.tgz", + "integrity": "sha512-pPwaZt+SPOshK8xNoiQlK5XIrS97kFYc3Rc7xmy373QsOJ9MmqXxLaYssP5Kcds4wd2qK//amx/c+A8O2fVeZA==", + "license": "Apache-2.0", + "peerDependencies": { + "@firebase/app-types": "0.x", + "@firebase/util": "1.x" + } + }, + "node_modules/@firebase/auth/node_modules/node-fetch": { + "version": "2.6.7", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", + "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/@firebase/component": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.6.4.tgz", + "integrity": "sha512-rLMyrXuO9jcAUCaQXCMjCMUsWrba5fzHlNK24xz5j2W6A/SRmK8mZJ/hn7V0fViLbxC0lPMtrK1eYzk6Fg03jA==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" + } + }, + "node_modules/@firebase/database": { + "version": "0.14.4", + "resolved": "https://registry.npmjs.org/@firebase/database/-/database-0.14.4.tgz", + "integrity": "sha512-+Ea/IKGwh42jwdjCyzTmeZeLM3oy1h0mFPsTy6OqCWzcu/KFqRAr5Tt1HRCOBlNOdbh84JPZC47WLU18n2VbxQ==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/auth-interop-types": "0.2.1", + "@firebase/component": "0.6.4", + "@firebase/logger": "0.4.0", + "@firebase/util": "1.9.3", + "faye-websocket": "0.11.4", + "tslib": "^2.1.0" + } + }, + "node_modules/@firebase/database-compat": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@firebase/database-compat/-/database-compat-0.3.4.tgz", + "integrity": "sha512-kuAW+l+sLMUKBThnvxvUZ+Q1ZrF/vFJ58iUY9kAcbX48U03nVzIF6Tmkf0p3WVQwMqiXguSgtOPIB6ZCeF+5Gg==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.6.4", + "@firebase/database": "0.14.4", + "@firebase/database-types": "0.10.4", + "@firebase/logger": "0.4.0", + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" + } + }, + "node_modules/@firebase/database-types": { + "version": "0.10.4", + "resolved": "https://registry.npmjs.org/@firebase/database-types/-/database-types-0.10.4.tgz", + "integrity": "sha512-dPySn0vJ/89ZeBac70T+2tWWPiJXWbmRygYv0smT5TfE3hDrQ09eKMF3Y+vMlTdrMWq7mUdYW5REWPSGH4kAZQ==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/app-types": "0.9.0", + "@firebase/util": "1.9.3" + } + }, + "node_modules/@firebase/firestore": { + "version": "3.13.0", + "resolved": "https://registry.npmjs.org/@firebase/firestore/-/firestore-3.13.0.tgz", + "integrity": "sha512-NwcnU+madJXQ4fbLkGx1bWvL612IJN/qO6bZ6dlPmyf7QRyu5azUosijdAN675r+bOOJxMtP1Bv981bHBXAbUg==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.6.4", + "@firebase/logger": "0.4.0", + "@firebase/util": "1.9.3", + "@firebase/webchannel-wrapper": "0.10.1", + "@grpc/grpc-js": "~1.7.0", + "@grpc/proto-loader": "^0.6.13", + "node-fetch": "2.6.7", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=10.10.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/firestore-compat": { + "version": "0.3.12", + "resolved": "https://registry.npmjs.org/@firebase/firestore-compat/-/firestore-compat-0.3.12.tgz", + "integrity": "sha512-mazuNGAx5Kt9Nph0pm6ULJFp/+j7GSsx+Ncw1GrnKl+ft1CQ4q2LcUssXnjqkX2Ry0fNGqUzC1mfIUrk9bYtjQ==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.6.4", + "@firebase/firestore": "3.13.0", + "@firebase/firestore-types": "2.5.1", + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/firestore-types": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@firebase/firestore-types/-/firestore-types-2.5.1.tgz", + "integrity": "sha512-xG0CA6EMfYo8YeUxC8FeDzf6W3FX1cLlcAGBYV6Cku12sZRI81oWcu61RSKM66K6kUENP+78Qm8mvroBcm1whw==", + "license": "Apache-2.0", + "peerDependencies": { + "@firebase/app-types": "0.x", + "@firebase/util": "1.x" + } + }, + "node_modules/@firebase/firestore/node_modules/node-fetch": { + "version": "2.6.7", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", + "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/@firebase/functions": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@firebase/functions/-/functions-0.10.0.tgz", + "integrity": "sha512-2U+fMNxTYhtwSpkkR6WbBcuNMOVaI7MaH3cZ6UAeNfj7AgEwHwMIFLPpC13YNZhno219F0lfxzTAA0N62ndWzA==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/app-check-interop-types": "0.3.0", + "@firebase/auth-interop-types": "0.2.1", + "@firebase/component": "0.6.4", + "@firebase/messaging-interop-types": "0.2.0", + "@firebase/util": "1.9.3", + "node-fetch": "2.6.7", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/functions-compat": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@firebase/functions-compat/-/functions-compat-0.3.5.tgz", + "integrity": "sha512-uD4jwgwVqdWf6uc3NRKF8cSZ0JwGqSlyhPgackyUPe+GAtnERpS4+Vr66g0b3Gge0ezG4iyHo/EXW/Hjx7QhHw==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.6.4", + "@firebase/functions": "0.10.0", + "@firebase/functions-types": "0.6.0", + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/functions-types": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@firebase/functions-types/-/functions-types-0.6.0.tgz", + "integrity": "sha512-hfEw5VJtgWXIRf92ImLkgENqpL6IWpYaXVYiRkFY1jJ9+6tIhWM7IzzwbevwIIud/jaxKVdRzD7QBWfPmkwCYw==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/functions/node_modules/node-fetch": { + "version": "2.6.7", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", + "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/@firebase/installations": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/@firebase/installations/-/installations-0.6.4.tgz", + "integrity": "sha512-u5y88rtsp7NYkCHC3ElbFBrPtieUybZluXyzl7+4BsIz4sqb4vSAuwHEUgCgCeaQhvsnxDEU6icly8U9zsJigA==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.6.4", + "@firebase/util": "1.9.3", + "idb": "7.0.1", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/installations-compat": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@firebase/installations-compat/-/installations-compat-0.2.4.tgz", + "integrity": "sha512-LI9dYjp0aT9Njkn9U4JRrDqQ6KXeAmFbRC0E7jI7+hxl5YmRWysq5qgQl22hcWpTk+cm3es66d/apoDU/A9n6Q==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.6.4", + "@firebase/installations": "0.6.4", + "@firebase/installations-types": "0.5.0", + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/installations-types": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@firebase/installations-types/-/installations-types-0.5.0.tgz", + "integrity": "sha512-9DP+RGfzoI2jH7gY4SlzqvZ+hr7gYzPODrbzVD82Y12kScZ6ZpRg/i3j6rleto8vTFC8n6Len4560FnV1w2IRg==", + "license": "Apache-2.0", + "peerDependencies": { + "@firebase/app-types": "0.x" + } + }, + "node_modules/@firebase/installations/node_modules/idb": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/idb/-/idb-7.0.1.tgz", + "integrity": "sha512-UUxlE7vGWK5RfB/fDwEGgRf84DY/ieqNha6msMV99UsEMQhJ1RwbCd8AYBj3QMgnE3VZnfQvm4oKVCJTYlqIgg==", + "license": "ISC" + }, + "node_modules/@firebase/logger": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@firebase/logger/-/logger-0.4.0.tgz", + "integrity": "sha512-eRKSeykumZ5+cJPdxxJRgAC3G5NknY2GwEbKfymdnXtnT0Ucm4pspfR6GT4MUQEDuJwRVbVcSx85kgJulMoFFA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@firebase/messaging": { + "version": "0.12.4", + "resolved": "https://registry.npmjs.org/@firebase/messaging/-/messaging-0.12.4.tgz", + "integrity": "sha512-6JLZct6zUaex4g7HI3QbzeUrg9xcnmDAPTWpkoMpd/GoSVWH98zDoWXMGrcvHeCAIsLpFMe4MPoZkJbrPhaASw==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.6.4", + "@firebase/installations": "0.6.4", + "@firebase/messaging-interop-types": "0.2.0", + "@firebase/util": "1.9.3", + "idb": "7.0.1", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/messaging-compat": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@firebase/messaging-compat/-/messaging-compat-0.2.4.tgz", + "integrity": "sha512-lyFjeUhIsPRYDPNIkYX1LcZMpoVbBWXX4rPl7c/rqc7G+EUea7IEtSt4MxTvh6fDfPuzLn7+FZADfscC+tNMfg==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.6.4", + "@firebase/messaging": "0.12.4", + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/messaging-interop-types": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@firebase/messaging-interop-types/-/messaging-interop-types-0.2.0.tgz", + "integrity": "sha512-ujA8dcRuVeBixGR9CtegfpU4YmZf3Lt7QYkcj693FFannwNuZgfAYaTmbJ40dtjB81SAu6tbFPL9YLNT15KmOQ==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/messaging/node_modules/idb": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/idb/-/idb-7.0.1.tgz", + "integrity": "sha512-UUxlE7vGWK5RfB/fDwEGgRf84DY/ieqNha6msMV99UsEMQhJ1RwbCd8AYBj3QMgnE3VZnfQvm4oKVCJTYlqIgg==", + "license": "ISC" + }, + "node_modules/@firebase/performance": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/@firebase/performance/-/performance-0.6.4.tgz", + "integrity": "sha512-HfTn/bd8mfy/61vEqaBelNiNnvAbUtME2S25A67Nb34zVuCSCRIX4SseXY6zBnOFj3oLisaEqhVcJmVPAej67g==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.6.4", + "@firebase/installations": "0.6.4", + "@firebase/logger": "0.4.0", + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/performance-compat": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@firebase/performance-compat/-/performance-compat-0.2.4.tgz", + "integrity": "sha512-nnHUb8uP9G8islzcld/k6Bg5RhX62VpbAb/Anj7IXs/hp32Eb2LqFPZK4sy3pKkBUO5wcrlRWQa6wKOxqlUqsg==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.6.4", + "@firebase/logger": "0.4.0", + "@firebase/performance": "0.6.4", + "@firebase/performance-types": "0.2.0", + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/performance-types": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@firebase/performance-types/-/performance-types-0.2.0.tgz", + "integrity": "sha512-kYrbr8e/CYr1KLrLYZZt2noNnf+pRwDq2KK9Au9jHrBMnb0/C9X9yWSXmZkFt4UIdsQknBq8uBB7fsybZdOBTA==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/remote-config": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/@firebase/remote-config/-/remote-config-0.4.4.tgz", + "integrity": "sha512-x1ioTHGX8ZwDSTOVp8PBLv2/wfwKzb4pxi0gFezS5GCJwbLlloUH4YYZHHS83IPxnua8b6l0IXUaWd0RgbWwzQ==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.6.4", + "@firebase/installations": "0.6.4", + "@firebase/logger": "0.4.0", + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/remote-config-compat": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@firebase/remote-config-compat/-/remote-config-compat-0.2.4.tgz", + "integrity": "sha512-FKiki53jZirrDFkBHglB3C07j5wBpitAaj8kLME6g8Mx+aq7u9P7qfmuSRytiOItADhWUj7O1JIv7n9q87SuwA==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.6.4", + "@firebase/logger": "0.4.0", + "@firebase/remote-config": "0.4.4", + "@firebase/remote-config-types": "0.3.0", + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/remote-config-types": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@firebase/remote-config-types/-/remote-config-types-0.3.0.tgz", + "integrity": "sha512-RtEH4vdcbXZuZWRZbIRmQVBNsE7VDQpet2qFvq6vwKLBIQRQR5Kh58M4ok3A3US8Sr3rubYnaGqZSurCwI8uMA==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/storage": { + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/@firebase/storage/-/storage-0.11.2.tgz", + "integrity": "sha512-CtvoFaBI4hGXlXbaCHf8humajkbXhs39Nbh6MbNxtwJiCqxPy9iH3D3CCfXAvP0QvAAwmJUTK3+z9a++Kc4nkA==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.6.4", + "@firebase/util": "1.9.3", + "node-fetch": "2.6.7", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/storage-compat": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@firebase/storage-compat/-/storage-compat-0.3.2.tgz", + "integrity": "sha512-wvsXlLa9DVOMQJckbDNhXKKxRNNewyUhhbXev3t8kSgoCotd1v3MmqhKKz93ePhDnhHnDs7bYHy+Qa8dRY6BXw==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.6.4", + "@firebase/storage": "0.11.2", + "@firebase/storage-types": "0.8.0", + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/storage-types": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@firebase/storage-types/-/storage-types-0.8.0.tgz", + "integrity": "sha512-isRHcGrTs9kITJC0AVehHfpraWFui39MPaU7Eo8QfWlqW7YPymBmRgjDrlOgFdURh6Cdeg07zmkLP5tzTKRSpg==", + "license": "Apache-2.0", + "peerDependencies": { + "@firebase/app-types": "0.x", + "@firebase/util": "1.x" + } + }, + "node_modules/@firebase/storage/node_modules/node-fetch": { + "version": "2.6.7", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", + "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/@firebase/util": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/@firebase/util/-/util-1.9.3.tgz", + "integrity": "sha512-DY02CRhOZwpzO36fHpuVysz6JZrscPiBXD0fXp6qSrL9oNOx5KWICKdR95C0lSITzxp0TZosVyHqzatE8JbcjA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@firebase/webchannel-wrapper": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@firebase/webchannel-wrapper/-/webchannel-wrapper-0.10.1.tgz", + "integrity": "sha512-Dq5rYfEpdeel0bLVN+nfD1VWmzCkK+pJbSjIawGE+RY4+NIJqhbUDDQjvV0NUK84fMfwxvtFoCtEe70HfZjFcw==", + "license": "Apache-2.0" + }, + "node_modules/@fortawesome/fontawesome-free": { + "version": "6.7.2", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-6.7.2.tgz", + "integrity": "sha512-JUOtgFW6k9u4Y+xeIaEiLr3+cjoUPiAuLXoyKOJSia6Duzb7pq+A76P9ZdPDoAoxHdHzq6gE9/jKBGXlZT8FbA==", + "license": "(CC-BY-4.0 AND OFL-1.1 AND MIT)", + "engines": { + "node": ">=6" + } + }, + "node_modules/@gar/promisify": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz", + "integrity": "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@grpc/grpc-js": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.7.3.tgz", + "integrity": "sha512-H9l79u4kJ2PVSxUNA08HMYAnUBLj9v6KjYQ7SQ71hOZcEXhShE/y5iQCesP8+6/Ik/7i2O0a10bPquIcYfufog==", + "license": "Apache-2.0", + "dependencies": { + "@grpc/proto-loader": "^0.7.0", + "@types/node": ">=12.12.47" + }, + "engines": { + "node": "^8.13.0 || >=10.10.0" + } + }, + "node_modules/@grpc/grpc-js/node_modules/@grpc/proto-loader": { + "version": "0.7.13", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.13.tgz", + "integrity": "sha512-AiXO/bfe9bmxBjxxtYxFAXGZvMaN5s8kO+jBHAJCON8rJoB5YS/D6X7ZNc6XQkuHNmyl4CYaMI1fJ/Gn27RGGw==", + "license": "Apache-2.0", + "dependencies": { + "lodash.camelcase": "^4.3.0", + "long": "^5.0.0", + "protobufjs": "^7.2.5", + "yargs": "^17.7.2" + }, + "bin": { + "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@grpc/grpc-js/node_modules/long": { + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/long/-/long-5.2.4.tgz", + "integrity": "sha512-qtzLbJE8hq7VabR3mISmVGtoXP8KGc2Z/AT8OuqlYD7JTR3oqrgwdjnk07wpj1twXxYmgDXgoKVWUG/fReSzHg==", + "license": "Apache-2.0" + }, + "node_modules/@grpc/grpc-js/node_modules/protobufjs": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.4.0.tgz", + "integrity": "sha512-mRUWCc3KUU4w1jU8sGxICXH/gNS94DvI1gxqDvBzhj1JpcsimQkYiOJfwsPUykUI5ZaspFbSgmBLER8IrQ3tqw==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/node": ">=13.7.0", + "long": "^5.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@grpc/grpc-js/node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@grpc/proto-loader": { + "version": "0.6.13", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.6.13.tgz", + "integrity": "sha512-FjxPYDRTn6Ec3V0arm1FtSpmP6V50wuph2yILpyvTKzjc76oDdoihXqM1DzOW5ubvCC8GivfCnNtfaRE8myJ7g==", + "license": "Apache-2.0", + "dependencies": { + "@types/long": "^4.0.1", + "lodash.camelcase": "^4.3.0", + "long": "^4.0.0", + "protobufjs": "^6.11.3", + "yargs": "^16.2.0" + }, + "bin": { + "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@grpc/proto-loader/node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/@grpc/proto-loader/node_modules/yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "license": "MIT", + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@grpc/proto-loader/node_modules/yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.11.14", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", + "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==", + "deprecated": "Use @eslint/config-array instead", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanwhocodes/object-schema": "^2.0.2", + "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.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "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.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "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/@ionic/angular": { + "version": "7.8.6", + "resolved": "https://registry.npmjs.org/@ionic/angular/-/angular-7.8.6.tgz", + "integrity": "sha512-3Qe53hXpyjtx6fFcxt/NTAlauIawsGmCZJPauV5sAnSKVuX8C82C1zMAZTeJt6m2dnd71wythc98BXUXsx/UxQ==", + "license": "MIT", + "dependencies": { + "@ionic/core": "7.8.6", + "ionicons": "^7.0.0", + "jsonc-parser": "^3.0.0", + "tslib": "^2.3.0" + }, + "peerDependencies": { + "@angular/core": ">=14.0.0", + "@angular/forms": ">=14.0.0", + "@angular/router": ">=14.0.0", + "rxjs": ">=7.5.0", + "zone.js": ">=0.11.0" + } + }, + "node_modules/@ionic/core": { + "version": "7.8.6", + "resolved": "https://registry.npmjs.org/@ionic/core/-/core-7.8.6.tgz", + "integrity": "sha512-HAYZdEmeJgOdo2kDlZkcCGHb+zs/vjU6iv4skbVBL7y+OnSv/oC2u83Yee8S3/aY0YAxkyBgu7hLTYH13Zc2Aw==", + "license": "MIT", + "dependencies": { + "@stencil/core": "^4.12.2", + "ionicons": "^7.2.2", + "tslib": "^2.1.0" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": 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/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": 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/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": 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/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.1.1.tgz", + "integrity": "sha512-sQXCasFk+U8lWYEe66WxRDOE9PjVz4vSM51fTu3Hw+ClTpUSQb718772vH3pyS5pShp6lvQM7SxgIDXXXmOX7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/set-array": "^1.0.0", + "@jridgewell/sourcemap-codec": "^1.4.10" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.6.tgz", + "integrity": "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, + "node_modules/@jridgewell/source-map/node_modules/@jridgewell/gen-mapping": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", + "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", + "license": "MIT", + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@leichtgewicht/ip-codec": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz", + "integrity": "sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@material/animation": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/animation/-/animation-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-5osi1z4JQIXcklPALbH/zTfOm2pDzHt9Fxm7ZyURy250xIZj6QjULRzPTnzOhC2ropfix9ra2Cfggbf0dcRbEQ==", + "license": "MIT", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@material/auto-init": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/auto-init/-/auto-init-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-OigQTmrVzkcGvxNjOaIe5oItTFPgrO9xLewvharDI6m6yvO1z7OBnkcW+sFN6ggLNYNxd0O1u9v64vMsmeDABQ==", + "license": "MIT", + "dependencies": { + "@material/base": "15.0.0-canary.684e33d25.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/banner": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/banner/-/banner-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-PqtGp3KWzdu58rWv/DIvSfe38m5YKOBbAAbBinSvgadBb/da+IE1t5F7YPNKE1T5lJsQBGVUYx6QBIeXm+aI/A==", + "license": "MIT", + "dependencies": { + "@material/base": "15.0.0-canary.684e33d25.0", + "@material/button": "15.0.0-canary.684e33d25.0", + "@material/dom": "15.0.0-canary.684e33d25.0", + "@material/elevation": "15.0.0-canary.684e33d25.0", + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "@material/ripple": "15.0.0-canary.684e33d25.0", + "@material/rtl": "15.0.0-canary.684e33d25.0", + "@material/shape": "15.0.0-canary.684e33d25.0", + "@material/theme": "15.0.0-canary.684e33d25.0", + "@material/tokens": "15.0.0-canary.684e33d25.0", + "@material/typography": "15.0.0-canary.684e33d25.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/base": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/base/-/base-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-oOaqb/SfjWwTKsdJUZmeh/Qrs41nIJI0N+zELsxnvbGjSIN1ZMAKYZFPMahqvC68OJ6+5CvJM8PoTNs5l+B8IQ==", + "license": "MIT", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@material/button": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/button/-/button-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-Nkekk4edeX+ObVOa7UlwavaHdmckPV5wU4SAJf3iA3R61cmz+KsgAgpzfcwv5WfNhIlc2nLu8QYEecpHdo9d/w==", + "license": "MIT", + "dependencies": { + "@material/density": "15.0.0-canary.684e33d25.0", + "@material/dom": "15.0.0-canary.684e33d25.0", + "@material/elevation": "15.0.0-canary.684e33d25.0", + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "@material/focus-ring": "15.0.0-canary.684e33d25.0", + "@material/ripple": "15.0.0-canary.684e33d25.0", + "@material/rtl": "15.0.0-canary.684e33d25.0", + "@material/shape": "15.0.0-canary.684e33d25.0", + "@material/theme": "15.0.0-canary.684e33d25.0", + "@material/tokens": "15.0.0-canary.684e33d25.0", + "@material/touch-target": "15.0.0-canary.684e33d25.0", + "@material/typography": "15.0.0-canary.684e33d25.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/card": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/card/-/card-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-xhyB7XX5KkEiCEqwSPkl58ZGYL6xFdnY62zimyBXJRG/Eaa0Swj3kW20hVCpt4f7c9Zmp8Se27rg8vnKmhvO3g==", + "license": "MIT", + "dependencies": { + "@material/dom": "15.0.0-canary.684e33d25.0", + "@material/elevation": "15.0.0-canary.684e33d25.0", + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "@material/ripple": "15.0.0-canary.684e33d25.0", + "@material/rtl": "15.0.0-canary.684e33d25.0", + "@material/shape": "15.0.0-canary.684e33d25.0", + "@material/theme": "15.0.0-canary.684e33d25.0", + "@material/tokens": "15.0.0-canary.684e33d25.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/checkbox": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/checkbox/-/checkbox-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-NFpM3TS924PmVsk2KQLNU95OYCf8ZwYgzeqfnAexU0bEfjUJXINBun2Go0AaeOUMjuvWUe+byjrXgv8SFYbMUA==", + "license": "MIT", + "dependencies": { + "@material/animation": "15.0.0-canary.684e33d25.0", + "@material/base": "15.0.0-canary.684e33d25.0", + "@material/density": "15.0.0-canary.684e33d25.0", + "@material/dom": "15.0.0-canary.684e33d25.0", + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "@material/focus-ring": "15.0.0-canary.684e33d25.0", + "@material/ripple": "15.0.0-canary.684e33d25.0", + "@material/theme": "15.0.0-canary.684e33d25.0", + "@material/touch-target": "15.0.0-canary.684e33d25.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/chips": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/chips/-/chips-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-z4ajQ4NnsAQ/Si9tZ4xmxzjj2Qb+vW++4QjCjjjwAGIZbCe0xglAnMh2t66XLJUxt7RoKZuZVEO7ZqcFZpvJFQ==", + "license": "MIT", + "dependencies": { + "@material/animation": "15.0.0-canary.684e33d25.0", + "@material/base": "15.0.0-canary.684e33d25.0", + "@material/checkbox": "15.0.0-canary.684e33d25.0", + "@material/density": "15.0.0-canary.684e33d25.0", + "@material/dom": "15.0.0-canary.684e33d25.0", + "@material/elevation": "15.0.0-canary.684e33d25.0", + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "@material/focus-ring": "15.0.0-canary.684e33d25.0", + "@material/ripple": "15.0.0-canary.684e33d25.0", + "@material/rtl": "15.0.0-canary.684e33d25.0", + "@material/shape": "15.0.0-canary.684e33d25.0", + "@material/theme": "15.0.0-canary.684e33d25.0", + "@material/tokens": "15.0.0-canary.684e33d25.0", + "@material/touch-target": "15.0.0-canary.684e33d25.0", + "@material/typography": "15.0.0-canary.684e33d25.0", + "safevalues": "^0.3.4", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/circular-progress": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/circular-progress/-/circular-progress-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-G6qD0nGNtEUwWnAMJuA9INYFpZoKtx7KFjBaPF4Ol2YLHtmShALNAYyn54TMAK8AZ2IpW08PXjGS7Ye88vrdEQ==", + "license": "MIT", + "dependencies": { + "@material/animation": "15.0.0-canary.684e33d25.0", + "@material/base": "15.0.0-canary.684e33d25.0", + "@material/dom": "15.0.0-canary.684e33d25.0", + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "@material/progress-indicator": "15.0.0-canary.684e33d25.0", + "@material/rtl": "15.0.0-canary.684e33d25.0", + "@material/theme": "15.0.0-canary.684e33d25.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/data-table": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/data-table/-/data-table-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-+wDw1DDDFfAsKAMzs84f/5GCjux39zjNfW8tL4wFbkWNwewmQrG9zaQMJhBpVOtLCrM8Gj6SOgOANqgqoCjvGg==", + "license": "MIT", + "dependencies": { + "@material/animation": "15.0.0-canary.684e33d25.0", + "@material/base": "15.0.0-canary.684e33d25.0", + "@material/checkbox": "15.0.0-canary.684e33d25.0", + "@material/density": "15.0.0-canary.684e33d25.0", + "@material/dom": "15.0.0-canary.684e33d25.0", + "@material/elevation": "15.0.0-canary.684e33d25.0", + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "@material/icon-button": "15.0.0-canary.684e33d25.0", + "@material/linear-progress": "15.0.0-canary.684e33d25.0", + "@material/list": "15.0.0-canary.684e33d25.0", + "@material/menu": "15.0.0-canary.684e33d25.0", + "@material/rtl": "15.0.0-canary.684e33d25.0", + "@material/select": "15.0.0-canary.684e33d25.0", + "@material/shape": "15.0.0-canary.684e33d25.0", + "@material/theme": "15.0.0-canary.684e33d25.0", + "@material/tokens": "15.0.0-canary.684e33d25.0", + "@material/touch-target": "15.0.0-canary.684e33d25.0", + "@material/typography": "15.0.0-canary.684e33d25.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/density": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/density/-/density-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-661yEVRMGrlq6S6WuSbPRO+ZwpdUOg2glCc7y96doM6itSLOa3UEAldjOLfsYZVB74GnKCiuDp//QmfoRyYTfA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@material/dialog": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/dialog/-/dialog-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-szn0dHnfeQTSOC6SSRSGAzX6Tnx+4NnSMUwNkXm+3bwjds8ZVK26+DXwLrP5f3ID5F1K5sFsRf2INo5/TNTHyQ==", + "license": "MIT", + "dependencies": { + "@material/animation": "15.0.0-canary.684e33d25.0", + "@material/base": "15.0.0-canary.684e33d25.0", + "@material/button": "15.0.0-canary.684e33d25.0", + "@material/dom": "15.0.0-canary.684e33d25.0", + "@material/elevation": "15.0.0-canary.684e33d25.0", + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "@material/icon-button": "15.0.0-canary.684e33d25.0", + "@material/ripple": "15.0.0-canary.684e33d25.0", + "@material/rtl": "15.0.0-canary.684e33d25.0", + "@material/shape": "15.0.0-canary.684e33d25.0", + "@material/theme": "15.0.0-canary.684e33d25.0", + "@material/tokens": "15.0.0-canary.684e33d25.0", + "@material/touch-target": "15.0.0-canary.684e33d25.0", + "@material/typography": "15.0.0-canary.684e33d25.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/dom": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/dom/-/dom-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-7pEJLYov+tGgfuD8mZxoVU6rWtPI8ppjTAhz+F27Hz9FG0JETMWTKpDPBXLnKvX7vhIxL83GvZ9geNHCe8Hfog==", + "license": "MIT", + "dependencies": { + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "@material/rtl": "15.0.0-canary.684e33d25.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/drawer": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/drawer/-/drawer-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-/KMckLf1PYU/H3PXnS4e0aFl03qG3JlSv4LGgX6juJufcONqGTl/m63EMO/L/eUy6H1CRrXmVDjik/jzHLyDhg==", + "license": "MIT", + "dependencies": { + "@material/animation": "15.0.0-canary.684e33d25.0", + "@material/base": "15.0.0-canary.684e33d25.0", + "@material/dom": "15.0.0-canary.684e33d25.0", + "@material/elevation": "15.0.0-canary.684e33d25.0", + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "@material/list": "15.0.0-canary.684e33d25.0", + "@material/ripple": "15.0.0-canary.684e33d25.0", + "@material/rtl": "15.0.0-canary.684e33d25.0", + "@material/shape": "15.0.0-canary.684e33d25.0", + "@material/theme": "15.0.0-canary.684e33d25.0", + "@material/typography": "15.0.0-canary.684e33d25.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/elevation": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/elevation/-/elevation-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-WDF8SsRtq3rXUbVVbd9K4DUijIPH0bUFSOreVYxudpuxAfTlDS5+aeS1EK9UIBFYLuba4u5wVT2tDv6e1RTfrQ==", + "license": "MIT", + "dependencies": { + "@material/animation": "15.0.0-canary.684e33d25.0", + "@material/base": "15.0.0-canary.684e33d25.0", + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "@material/rtl": "15.0.0-canary.684e33d25.0", + "@material/theme": "15.0.0-canary.684e33d25.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/fab": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/fab/-/fab-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-KCu87rWOKEAe9vZcAm6K8XazYSWPNjMG+OhrbPjHW6bCO7as1YCgtmkBkhff7csY/rFmcVpIy884xtUfLmSudQ==", + "license": "MIT", + "dependencies": { + "@material/animation": "15.0.0-canary.684e33d25.0", + "@material/dom": "15.0.0-canary.684e33d25.0", + "@material/elevation": "15.0.0-canary.684e33d25.0", + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "@material/focus-ring": "15.0.0-canary.684e33d25.0", + "@material/ripple": "15.0.0-canary.684e33d25.0", + "@material/rtl": "15.0.0-canary.684e33d25.0", + "@material/shape": "15.0.0-canary.684e33d25.0", + "@material/theme": "15.0.0-canary.684e33d25.0", + "@material/tokens": "15.0.0-canary.684e33d25.0", + "@material/touch-target": "15.0.0-canary.684e33d25.0", + "@material/typography": "15.0.0-canary.684e33d25.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/feature-targeting": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/feature-targeting/-/feature-targeting-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-HyH1erNTSjS63sigNSUMaCd0nJhTNdDFeC+myrxwtDaQm+uYJ8troCNtQM3g6mx0XATNtX5aTOoPmrM6yVVi1A==", + "license": "MIT", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@material/floating-label": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/floating-label/-/floating-label-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-f7TPp6bKpGvV3sYYiZHSGlrixXKkXXITW3Esp7KB9jRq42c0H82novmdwvY0eTef4ootmA2JEysr78KQfHBUPg==", + "license": "MIT", + "dependencies": { + "@material/animation": "15.0.0-canary.684e33d25.0", + "@material/base": "15.0.0-canary.684e33d25.0", + "@material/dom": "15.0.0-canary.684e33d25.0", + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "@material/rtl": "15.0.0-canary.684e33d25.0", + "@material/theme": "15.0.0-canary.684e33d25.0", + "@material/typography": "15.0.0-canary.684e33d25.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/focus-ring": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/focus-ring/-/focus-ring-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-ikw2RVUfgzXChpWIzPH1VzRvTjYb5ZKj4H+CZf7jqPUXMstFOZg90Bp7ARLZHqYiyNMuUq3zUTHozS6iHorSqg==", + "license": "MIT", + "dependencies": { + "@material/dom": "15.0.0-canary.684e33d25.0", + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "@material/rtl": "15.0.0-canary.684e33d25.0" + } + }, + "node_modules/@material/form-field": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/form-field/-/form-field-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-vpF9N/uq5no/7+8GAbEH0868FhOuBgxAWRr1Sfb+jthKfBr8OS/wPU/AHzZHdHdAm7PQynbeOXfDsX2dI//PDA==", + "license": "MIT", + "dependencies": { + "@material/base": "15.0.0-canary.684e33d25.0", + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "@material/ripple": "15.0.0-canary.684e33d25.0", + "@material/rtl": "15.0.0-canary.684e33d25.0", + "@material/theme": "15.0.0-canary.684e33d25.0", + "@material/typography": "15.0.0-canary.684e33d25.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/icon-button": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/icon-button/-/icon-button-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-wMI+XGzmIN/o2ePBKg2hLyx7H4pXCRAyyIKMQS1FMp1UKa2tYmiHVX/V8skhKwCqxg3i6Ls/LxMjfPxTR18WvQ==", + "license": "MIT", + "dependencies": { + "@material/base": "15.0.0-canary.684e33d25.0", + "@material/density": "15.0.0-canary.684e33d25.0", + "@material/dom": "15.0.0-canary.684e33d25.0", + "@material/elevation": "15.0.0-canary.684e33d25.0", + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "@material/focus-ring": "15.0.0-canary.684e33d25.0", + "@material/ripple": "15.0.0-canary.684e33d25.0", + "@material/rtl": "15.0.0-canary.684e33d25.0", + "@material/theme": "15.0.0-canary.684e33d25.0", + "@material/touch-target": "15.0.0-canary.684e33d25.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/image-list": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/image-list/-/image-list-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-Ol+uaHYBe5R/cgzlfh5ONnMVX0wO6fV74JMUcQCQlxP6lXau/edARo4tkRc7A7UJUkU3VRv0EpEjLoCRNUPGaA==", + "license": "MIT", + "dependencies": { + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "@material/shape": "15.0.0-canary.684e33d25.0", + "@material/theme": "15.0.0-canary.684e33d25.0", + "@material/typography": "15.0.0-canary.684e33d25.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/layout-grid": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/layout-grid/-/layout-grid-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-ALXE1mqFNb/RB2lVRQ3/r1Aufw2mFZnOjRE+boYDVepmAG/xWyPCyaGoavELJF5l4GAb0tXi8wA/8HeGbLOpuA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@material/line-ripple": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/line-ripple/-/line-ripple-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-7hRx8C/e9i0P6pgQpNOMfTwSS2r1fwEvBL72QDVGLtLuoKKwsjjgP6Z0Jat/GeHJe87u9LQvGBoD4upt+of/HA==", + "license": "MIT", + "dependencies": { + "@material/animation": "15.0.0-canary.684e33d25.0", + "@material/base": "15.0.0-canary.684e33d25.0", + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "@material/theme": "15.0.0-canary.684e33d25.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/linear-progress": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/linear-progress/-/linear-progress-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-iJclt7mKmcMk6pqD7ocXKfCWZhqBoODp7N593jYlxVpTJuEz2wiVAjZUDn/YGj/Uz3CRH+2YFfOiLr9pwWjhDg==", + "license": "MIT", + "dependencies": { + "@material/animation": "15.0.0-canary.684e33d25.0", + "@material/base": "15.0.0-canary.684e33d25.0", + "@material/dom": "15.0.0-canary.684e33d25.0", + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "@material/progress-indicator": "15.0.0-canary.684e33d25.0", + "@material/rtl": "15.0.0-canary.684e33d25.0", + "@material/theme": "15.0.0-canary.684e33d25.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/list": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/list/-/list-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-rQ+FCSdzmwTcT00IYE0uRV3CS4oGSccKFl9hkcF+aHFW61L7ORh/SCGUDPrEfQFrFkMn5f8qroVJjpUAMXBz4g==", + "license": "MIT", + "dependencies": { + "@material/base": "15.0.0-canary.684e33d25.0", + "@material/density": "15.0.0-canary.684e33d25.0", + "@material/dom": "15.0.0-canary.684e33d25.0", + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "@material/ripple": "15.0.0-canary.684e33d25.0", + "@material/rtl": "15.0.0-canary.684e33d25.0", + "@material/shape": "15.0.0-canary.684e33d25.0", + "@material/theme": "15.0.0-canary.684e33d25.0", + "@material/tokens": "15.0.0-canary.684e33d25.0", + "@material/typography": "15.0.0-canary.684e33d25.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/menu": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/menu/-/menu-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-r7wzDLSGSI9629/mfpvsMzkVxpmV75kcD3IrW0Pcu6/Bv/1xi0EvjcUXzNJJoQlwN4Zj35Ymz/PCjZkIDIz68Q==", + "license": "MIT", + "dependencies": { + "@material/base": "15.0.0-canary.684e33d25.0", + "@material/dom": "15.0.0-canary.684e33d25.0", + "@material/elevation": "15.0.0-canary.684e33d25.0", + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "@material/list": "15.0.0-canary.684e33d25.0", + "@material/menu-surface": "15.0.0-canary.684e33d25.0", + "@material/ripple": "15.0.0-canary.684e33d25.0", + "@material/rtl": "15.0.0-canary.684e33d25.0", + "@material/shape": "15.0.0-canary.684e33d25.0", + "@material/theme": "15.0.0-canary.684e33d25.0", + "@material/tokens": "15.0.0-canary.684e33d25.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/menu-surface": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/menu-surface/-/menu-surface-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-RVO5GAYcfWPaKwxsF/NhUAmrYXQCQBKvRQW0TIlbmAJz6lcFeTs6YZqF3u1C7qrL3ZQGz+sur/7ywj6QU0oMow==", + "license": "MIT", + "dependencies": { + "@material/animation": "15.0.0-canary.684e33d25.0", + "@material/base": "15.0.0-canary.684e33d25.0", + "@material/elevation": "15.0.0-canary.684e33d25.0", + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "@material/rtl": "15.0.0-canary.684e33d25.0", + "@material/shape": "15.0.0-canary.684e33d25.0", + "@material/theme": "15.0.0-canary.684e33d25.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/notched-outline": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/notched-outline/-/notched-outline-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-9YHcBkvJLPVYzkHcWoTpBZAFrEd+j1hjhGxLhh0LuNrZe8VroUkZD1TTnUAPHRG3os6EqEWWaKb0RN+aPIF2yQ==", + "license": "MIT", + "dependencies": { + "@material/base": "15.0.0-canary.684e33d25.0", + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "@material/floating-label": "15.0.0-canary.684e33d25.0", + "@material/rtl": "15.0.0-canary.684e33d25.0", + "@material/shape": "15.0.0-canary.684e33d25.0", + "@material/theme": "15.0.0-canary.684e33d25.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/progress-indicator": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/progress-indicator/-/progress-indicator-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-c0icji4faeNWUoqGENGC7Hav0Puxh0RwXIDVizffaUxKIGbajpIp5+4Zop73fK/xFLGMB/npg7TbP+aCGjQ3fw==", + "license": "MIT", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@material/radio": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/radio/-/radio-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-U3Eh8sNUA8trDla1Bq8Bo02foxYvtoewaKeF8A8tAju81XZ4jRiftfOsOWZDZEHCVbbCB2QwvutvFlnay5n+Aw==", + "license": "MIT", + "dependencies": { + "@material/animation": "15.0.0-canary.684e33d25.0", + "@material/base": "15.0.0-canary.684e33d25.0", + "@material/density": "15.0.0-canary.684e33d25.0", + "@material/dom": "15.0.0-canary.684e33d25.0", + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "@material/focus-ring": "15.0.0-canary.684e33d25.0", + "@material/ripple": "15.0.0-canary.684e33d25.0", + "@material/theme": "15.0.0-canary.684e33d25.0", + "@material/touch-target": "15.0.0-canary.684e33d25.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/ripple": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/ripple/-/ripple-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-RyePu7SjIm/OuyyEieZ/gxiPYkNZOZHeid72WRcN9ofdlljj2pifcdPvcfZA+v/DMS33xo5GjG2L/Qj6ClWrKw==", + "license": "MIT", + "dependencies": { + "@material/animation": "15.0.0-canary.684e33d25.0", + "@material/base": "15.0.0-canary.684e33d25.0", + "@material/dom": "15.0.0-canary.684e33d25.0", + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "@material/rtl": "15.0.0-canary.684e33d25.0", + "@material/theme": "15.0.0-canary.684e33d25.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/rtl": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/rtl/-/rtl-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-NqdJl8Ayupp1Th+vCNCpVQHbUFOuF7TCte9LD1norTIBUF/QizIxWby2W5uUEiPbnh5j9PmE1CJtfLwKun3pcw==", + "license": "MIT", + "dependencies": { + "@material/theme": "15.0.0-canary.684e33d25.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/segmented-button": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/segmented-button/-/segmented-button-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-bEGgg8vgXNLyukyV8HRjFMuQ6t6nm5LQ4Pgm22um61Yc8qyi0BOqV41OR4SVdUrUqZxh1aVD+p+4NN03+LfQXw==", + "license": "MIT", + "dependencies": { + "@material/base": "15.0.0-canary.684e33d25.0", + "@material/elevation": "15.0.0-canary.684e33d25.0", + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "@material/ripple": "15.0.0-canary.684e33d25.0", + "@material/theme": "15.0.0-canary.684e33d25.0", + "@material/touch-target": "15.0.0-canary.684e33d25.0", + "@material/typography": "15.0.0-canary.684e33d25.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/select": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/select/-/select-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-kf178/2TeEinTv0mgmSBcmmExQ2h7a7dtR1E3WuqQgisJ/R6+zVLMkC2CnfIyzxYX2vkuUTG0ue3Reh/6XiqSg==", + "license": "MIT", + "dependencies": { + "@material/animation": "15.0.0-canary.684e33d25.0", + "@material/base": "15.0.0-canary.684e33d25.0", + "@material/density": "15.0.0-canary.684e33d25.0", + "@material/dom": "15.0.0-canary.684e33d25.0", + "@material/elevation": "15.0.0-canary.684e33d25.0", + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "@material/floating-label": "15.0.0-canary.684e33d25.0", + "@material/line-ripple": "15.0.0-canary.684e33d25.0", + "@material/list": "15.0.0-canary.684e33d25.0", + "@material/menu": "15.0.0-canary.684e33d25.0", + "@material/menu-surface": "15.0.0-canary.684e33d25.0", + "@material/notched-outline": "15.0.0-canary.684e33d25.0", + "@material/ripple": "15.0.0-canary.684e33d25.0", + "@material/rtl": "15.0.0-canary.684e33d25.0", + "@material/shape": "15.0.0-canary.684e33d25.0", + "@material/theme": "15.0.0-canary.684e33d25.0", + "@material/tokens": "15.0.0-canary.684e33d25.0", + "@material/typography": "15.0.0-canary.684e33d25.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/shape": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/shape/-/shape-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-aEelpaTFmpnCji3TUGP9bVCS/bRVjUmLTHBPZtuu1gOrUVVtJ6kYOg73dZNJF+XOoNL2yOX/LRcKwsop29tptA==", + "license": "MIT", + "dependencies": { + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "@material/rtl": "15.0.0-canary.684e33d25.0", + "@material/theme": "15.0.0-canary.684e33d25.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/slider": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/slider/-/slider-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-WVyK+2pSNSZmj07M2K/a3TADoQ9FBCndfNC/vE7/wGIg4dddJJK5KvQ+yruf9R2cSzTL/S1sZ5WpyyeM8E9HTw==", + "license": "MIT", + "dependencies": { + "@material/animation": "15.0.0-canary.684e33d25.0", + "@material/base": "15.0.0-canary.684e33d25.0", + "@material/dom": "15.0.0-canary.684e33d25.0", + "@material/elevation": "15.0.0-canary.684e33d25.0", + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "@material/ripple": "15.0.0-canary.684e33d25.0", + "@material/rtl": "15.0.0-canary.684e33d25.0", + "@material/theme": "15.0.0-canary.684e33d25.0", + "@material/tokens": "15.0.0-canary.684e33d25.0", + "@material/typography": "15.0.0-canary.684e33d25.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/snackbar": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/snackbar/-/snackbar-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-itO+DCkOannZzR1/cCHcqAm7ifhuFvXmDItNoA8qLEcAyJDJJRkhpwj3XQ01yuo9gBFcSctp7Txt7e+Hncm/Jg==", + "license": "MIT", + "dependencies": { + "@material/animation": "15.0.0-canary.684e33d25.0", + "@material/base": "15.0.0-canary.684e33d25.0", + "@material/button": "15.0.0-canary.684e33d25.0", + "@material/dom": "15.0.0-canary.684e33d25.0", + "@material/elevation": "15.0.0-canary.684e33d25.0", + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "@material/icon-button": "15.0.0-canary.684e33d25.0", + "@material/ripple": "15.0.0-canary.684e33d25.0", + "@material/rtl": "15.0.0-canary.684e33d25.0", + "@material/shape": "15.0.0-canary.684e33d25.0", + "@material/theme": "15.0.0-canary.684e33d25.0", + "@material/tokens": "15.0.0-canary.684e33d25.0", + "@material/typography": "15.0.0-canary.684e33d25.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/switch": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/switch/-/switch-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-Jxi0gl92yvvZZsAPxvVHzXx2ga+T/djMow98jvEczmpUorWnAhgiCr9CsSSRoosahWyRB8NLZOxUQrACxvffjw==", + "license": "MIT", + "dependencies": { + "@material/animation": "15.0.0-canary.684e33d25.0", + "@material/base": "15.0.0-canary.684e33d25.0", + "@material/density": "15.0.0-canary.684e33d25.0", + "@material/dom": "15.0.0-canary.684e33d25.0", + "@material/elevation": "15.0.0-canary.684e33d25.0", + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "@material/focus-ring": "15.0.0-canary.684e33d25.0", + "@material/ripple": "15.0.0-canary.684e33d25.0", + "@material/rtl": "15.0.0-canary.684e33d25.0", + "@material/shape": "15.0.0-canary.684e33d25.0", + "@material/theme": "15.0.0-canary.684e33d25.0", + "@material/tokens": "15.0.0-canary.684e33d25.0", + "safevalues": "^0.3.4", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/tab": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/tab/-/tab-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-WQL3wj9syHNcfe8KbgGGUcA34M8C/xZ+n0Fkkh8Kk6puVwaU+xqUNihsxPY6YzKpmh4PZ4oJaBdiN8zvFT1zqQ==", + "license": "MIT", + "dependencies": { + "@material/base": "15.0.0-canary.684e33d25.0", + "@material/elevation": "15.0.0-canary.684e33d25.0", + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "@material/focus-ring": "15.0.0-canary.684e33d25.0", + "@material/ripple": "15.0.0-canary.684e33d25.0", + "@material/rtl": "15.0.0-canary.684e33d25.0", + "@material/tab-indicator": "15.0.0-canary.684e33d25.0", + "@material/theme": "15.0.0-canary.684e33d25.0", + "@material/tokens": "15.0.0-canary.684e33d25.0", + "@material/typography": "15.0.0-canary.684e33d25.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/tab-bar": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/tab-bar/-/tab-bar-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-SW/cMaDsIGGkM1ag3A7GJRlmr8eXmObWsvitQJzh6Azr5zzZtSI+GQygkMesAEE1gbpqOVN8d40rh3H7VVIAcA==", + "license": "MIT", + "dependencies": { + "@material/animation": "15.0.0-canary.684e33d25.0", + "@material/base": "15.0.0-canary.684e33d25.0", + "@material/density": "15.0.0-canary.684e33d25.0", + "@material/elevation": "15.0.0-canary.684e33d25.0", + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "@material/tab": "15.0.0-canary.684e33d25.0", + "@material/tab-indicator": "15.0.0-canary.684e33d25.0", + "@material/tab-scroller": "15.0.0-canary.684e33d25.0", + "@material/theme": "15.0.0-canary.684e33d25.0", + "@material/tokens": "15.0.0-canary.684e33d25.0", + "@material/typography": "15.0.0-canary.684e33d25.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/tab-indicator": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/tab-indicator/-/tab-indicator-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-kKICqSPqOlaf0lzaFFCmuOqPXJC+cK48Qmsc+m5o6fJhkmuZRCYpIwB2JeP+uZSOq/bTH+SrPtCtnVlgWg6ksA==", + "license": "MIT", + "dependencies": { + "@material/animation": "15.0.0-canary.684e33d25.0", + "@material/base": "15.0.0-canary.684e33d25.0", + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "@material/theme": "15.0.0-canary.684e33d25.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/tab-scroller": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/tab-scroller/-/tab-scroller-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-H6EU/TSiK/M2DyyORX5GEtXD9rKYxTMHC2VxsNWARPMFJGzgeW2ugYkFv+rKI1/c0bs0CJ4e+qFnOlBsQXZvyQ==", + "license": "MIT", + "dependencies": { + "@material/animation": "15.0.0-canary.684e33d25.0", + "@material/base": "15.0.0-canary.684e33d25.0", + "@material/dom": "15.0.0-canary.684e33d25.0", + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "@material/tab": "15.0.0-canary.684e33d25.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/textfield": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/textfield/-/textfield-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-OvgpDXjvpyJTtAWskO69IDybFvDNzr9w2PN/Fk7yFm+uNVupaWz1Ew8lZ4gGslaTNSVmh2XcsvmzxcLINSiiNg==", + "license": "MIT", + "dependencies": { + "@material/animation": "15.0.0-canary.684e33d25.0", + "@material/base": "15.0.0-canary.684e33d25.0", + "@material/density": "15.0.0-canary.684e33d25.0", + "@material/dom": "15.0.0-canary.684e33d25.0", + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "@material/floating-label": "15.0.0-canary.684e33d25.0", + "@material/line-ripple": "15.0.0-canary.684e33d25.0", + "@material/notched-outline": "15.0.0-canary.684e33d25.0", + "@material/ripple": "15.0.0-canary.684e33d25.0", + "@material/rtl": "15.0.0-canary.684e33d25.0", + "@material/shape": "15.0.0-canary.684e33d25.0", + "@material/theme": "15.0.0-canary.684e33d25.0", + "@material/tokens": "15.0.0-canary.684e33d25.0", + "@material/typography": "15.0.0-canary.684e33d25.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/theme": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/theme/-/theme-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-AZxaXXAvRKzAi20RlMxzt2U5UmkCWyv7DMWEBXsxtG5Tk54mi1HsbVUp3fxDPTlmL7Pq8p1/DESg/o7TgRCVlw==", + "license": "MIT", + "dependencies": { + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/tokens": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/tokens/-/tokens-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-wVwbQOTCXDPKYPdHQHLr026y36MMFelID1CmbfRk6mSol4O8yE9U0fXcShfRDW8Qo5E3X31w9c2A6T3neJY7wQ==", + "license": "MIT", + "dependencies": { + "@material/elevation": "15.0.0-canary.684e33d25.0" + } + }, + "node_modules/@material/tooltip": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/tooltip/-/tooltip-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-dtm26QjxyQdinc8btgz6yys07b7bUW4FZgNF2EBPeGrICrPg7jf+JEvDziz5g8VMaTBQLOQRSCGy0MKuRlOjLw==", + "license": "MIT", + "dependencies": { + "@material/animation": "15.0.0-canary.684e33d25.0", + "@material/base": "15.0.0-canary.684e33d25.0", + "@material/button": "15.0.0-canary.684e33d25.0", + "@material/dom": "15.0.0-canary.684e33d25.0", + "@material/elevation": "15.0.0-canary.684e33d25.0", + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "@material/rtl": "15.0.0-canary.684e33d25.0", + "@material/shape": "15.0.0-canary.684e33d25.0", + "@material/theme": "15.0.0-canary.684e33d25.0", + "@material/tokens": "15.0.0-canary.684e33d25.0", + "@material/typography": "15.0.0-canary.684e33d25.0", + "safevalues": "^0.3.4", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/top-app-bar": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/top-app-bar/-/top-app-bar-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-1M+oupUxflfW7u81P1XlxoLZB8bLzwtpKofIfDNRbEsiKhlLTERJR3Yak3BGE9xakNMysAaBHlkb5MrN5bNPFw==", + "license": "MIT", + "dependencies": { + "@material/animation": "15.0.0-canary.684e33d25.0", + "@material/base": "15.0.0-canary.684e33d25.0", + "@material/elevation": "15.0.0-canary.684e33d25.0", + "@material/ripple": "15.0.0-canary.684e33d25.0", + "@material/rtl": "15.0.0-canary.684e33d25.0", + "@material/shape": "15.0.0-canary.684e33d25.0", + "@material/theme": "15.0.0-canary.684e33d25.0", + "@material/typography": "15.0.0-canary.684e33d25.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/touch-target": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/touch-target/-/touch-target-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-zdE69Slg8+T7sTn1OwqZ6H7WBYac9mxJ/JlJqfTqthzIjZRcCxBSYymQJcDHjsrPnUojOtr9U4Tpm5YZ96TEkQ==", + "license": "MIT", + "dependencies": { + "@material/base": "15.0.0-canary.684e33d25.0", + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "@material/rtl": "15.0.0-canary.684e33d25.0", + "@material/theme": "15.0.0-canary.684e33d25.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/typography": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/typography/-/typography-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-aVnvgMwcfNa/K4wujzpKDIxjGl2hbkEL+m+OKDSQqWYjKcP9QrbzCXJruJBqxrBoPRHLbqo47k5f9uT8raSgjw==", + "license": "MIT", + "dependencies": { + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "@material/theme": "15.0.0-canary.684e33d25.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@ng-idle/core": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/@ng-idle/core/-/core-14.0.0.tgz", + "integrity": "sha512-DqseA+F6LXm/Hr4D8zQhDBliuAU06t9BOUpcdSUKQWdDEKo3mlovRxz7DMuXyzg4OBQ0saQpE3MeMqakIEHUHw==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.0.0" + }, + "peerDependencies": { + "@angular/common": ">=9.0.0", + "@angular/core": ">=9.0.0" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@npmcli/fs": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-3.1.1.tgz", + "integrity": "sha512-q9CRWjpHCMIh5sVyefoD1cA7PkvILqCZsnSOEUUivORLjxCO/Irmue2DprETiNgEqktDBZaM1Bi+jrarx1XdCg==", + "dev": true, + "license": "ISC", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/git": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@npmcli/git/-/git-4.1.0.tgz", + "integrity": "sha512-9hwoB3gStVfa0N31ymBmrX+GuDGdVA/QWShZVqE0HK2Af+7QGGrCTbZia/SW0ImUTjTne7SP91qxDmtXvDHRPQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/promise-spawn": "^6.0.0", + "lru-cache": "^7.4.4", + "npm-pick-manifest": "^8.0.0", + "proc-log": "^3.0.0", + "promise-inflight": "^1.0.1", + "promise-retry": "^2.0.1", + "semver": "^7.3.5", + "which": "^3.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/git/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/@npmcli/git/node_modules/which": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/which/-/which-3.0.1.tgz", + "integrity": "sha512-XA1b62dzQzLfaEOSQFTCOd5KFf/1VSzZo7/7TUjnya6u0vGGKzU96UQBZTAThCb2j4/xjBAyii1OhRLJEivHvg==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/installed-package-contents": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@npmcli/installed-package-contents/-/installed-package-contents-2.1.0.tgz", + "integrity": "sha512-c8UuGLeZpm69BryRykLuKRyKFZYJsZSCT4aVY5ds4omyZqJ172ApzgfKJ5eV/r3HgLdUYgFVe54KSFVjKoe27w==", + "dev": true, + "license": "ISC", + "dependencies": { + "npm-bundled": "^3.0.0", + "npm-normalize-package-bin": "^3.0.0" + }, + "bin": { + "installed-package-contents": "bin/index.js" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/move-file": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@npmcli/move-file/-/move-file-2.0.1.tgz", + "integrity": "sha512-mJd2Z5TjYWq/ttPLLGqArdtnC74J6bOzg4rMDnN+p1xTacZ2yPRCk2y0oSWQtygLR9YVQXgOcONrwtnk3JupxQ==", + "deprecated": "This functionality has been moved to @npmcli/fs", + "dev": true, + "license": "MIT", + "dependencies": { + "mkdirp": "^1.0.4", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/@npmcli/move-file/node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true, + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@npmcli/node-gyp": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/node-gyp/-/node-gyp-3.0.0.tgz", + "integrity": "sha512-gp8pRXC2oOxu0DUE1/M3bYtb1b3/DbJ5aM113+XJBgfXdussRAsX0YOrOhdd8WvnAR6auDBvJomGAkLKA5ydxA==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/promise-spawn": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@npmcli/promise-spawn/-/promise-spawn-6.0.2.tgz", + "integrity": "sha512-gGq0NJkIGSwdbUt4yhdF8ZrmkGKVz9vAdVzpOfnom+V8PLSmSOVhZwbNvZZS1EYcJN5hzzKBxmmVVAInM6HQLg==", + "dev": true, + "license": "ISC", + "dependencies": { + "which": "^3.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/promise-spawn/node_modules/which": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/which/-/which-3.0.1.tgz", + "integrity": "sha512-XA1b62dzQzLfaEOSQFTCOd5KFf/1VSzZo7/7TUjnya6u0vGGKzU96UQBZTAThCb2j4/xjBAyii1OhRLJEivHvg==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/run-script": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@npmcli/run-script/-/run-script-6.0.2.tgz", + "integrity": "sha512-NCcr1uQo1k5U+SYlnIrbAh3cxy+OQT1VtqiAbxdymSlptbzBb62AjH2xXgjNCoP073hoa1CfCAcwoZ8k96C4nA==", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/node-gyp": "^3.0.0", + "@npmcli/promise-spawn": "^6.0.0", + "node-gyp": "^9.0.0", + "read-package-json-fast": "^3.0.0", + "which": "^3.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/run-script/node_modules/which": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/which/-/which-3.0.1.tgz", + "integrity": "sha512-XA1b62dzQzLfaEOSQFTCOd5KFf/1VSzZo7/7TUjnya6u0vGGKzU96UQBZTAThCb2j4/xjBAyii1OhRLJEivHvg==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@pkgr/core": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.1.1.tgz", + "integrity": "sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/unts" + } + }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", + "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1", + "@protobufjs/inquire": "^1.1.0" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/inquire": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", + "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", + "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==", + "license": "BSD-3-Clause" + }, + "node_modules/@schematics/angular": { + "version": "15.1.6", + "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-15.1.6.tgz", + "integrity": "sha512-y2kIQ1wJL0wR6v/LM5+PFJUivrYtdaIJVRdOXLLWl0AB5aLwObiWgLzAuBsbGm/9//WPPhw9PglS5EFFxTBDzg==", + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "15.1.6", + "@angular-devkit/schematics": "15.1.6", + "jsonc-parser": "3.2.0" + }, + "engines": { + "node": "^14.20.0 || ^16.13.0 || >=18.10.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@schematics/angular/node_modules/@angular-devkit/core": { + "version": "15.1.6", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-15.1.6.tgz", + "integrity": "sha512-jGgxyRjecVf6lEyqDxz7ltMEndNPxIg720pk6r40fgsu0dU8w9vjJSJe7k0XdJiXVRcN6wZa/J5nO/xcwWVIsA==", + "license": "MIT", + "dependencies": { + "ajv": "8.12.0", + "ajv-formats": "2.1.1", + "jsonc-parser": "3.2.0", + "rxjs": "6.6.7", + "source-map": "0.7.4" + }, + "engines": { + "node": "^14.20.0 || ^16.13.0 || >=18.10.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "chokidar": "^3.5.2" + }, + "peerDependenciesMeta": { + "chokidar": { + "optional": true + } + } + }, + "node_modules/@schematics/angular/node_modules/rxjs": { + "version": "6.6.7", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", + "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^1.9.0" + }, + "engines": { + "npm": ">=2.0.0" + } + }, + "node_modules/@schematics/angular/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "license": "0BSD" + }, + "node_modules/@socket.io/component-emitter": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", + "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@stencil/core": { + "version": "4.25.1", + "resolved": "https://registry.npmjs.org/@stencil/core/-/core-4.25.1.tgz", + "integrity": "sha512-SJhAAN6nHj7l2n2F7H6NoOUFhXC5tYXSvOpAaNFi5As6c2jOvaDOvAH+om4OKmw8U2kHI9yihEqpZrtY1J20dQ==", + "license": "MIT", + "bin": { + "stencil": "bin/stencil" + }, + "engines": { + "node": ">=16.0.0", + "npm": ">=7.10.0" + } + }, + "node_modules/@tootallnate/once": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", + "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/@types/body-parser": { + "version": "1.19.5", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", + "integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/bonjour": { + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@types/bonjour/-/bonjour-3.5.13.tgz", + "integrity": "sha512-z9fJ5Im06zvUL548KvYNecEVlA7cVDkGUi6kZusb04mpyEFKCIZJvloCcmpmLaIahDpOQGHaHmG6imtPMmPXGQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/connect-history-api-fallback": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.5.4.tgz", + "integrity": "sha512-n6Cr2xS1h4uAulPRdlw6Jl6s1oG8KrVilPN2yUITEs+K48EzMJJ3W1xy8K5eWuFvjp3R74AOIGSmp2UfBJ8HFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express-serve-static-core": "*", + "@types/node": "*" + } + }, + "node_modules/@types/cors": { + "version": "2.8.17", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz", + "integrity": "sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/eslint": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", + "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", + "license": "MIT", + "dependencies": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "node_modules/@types/eslint-scope": { + "version": "3.7.7", + "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", + "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", + "license": "MIT", + "dependencies": { + "@types/eslint": "*", + "@types/estree": "*" + } + }, + "node_modules/@types/estree": { + "version": "0.0.51", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.51.tgz", + "integrity": "sha512-CuPgU6f3eT/XgKKPqKd/gLZV1Xmvf1a2R5POBOGQa6uv82xpls89HU5zKeVoyR8XzHd1RGNOlQlvUe3CFkjWNQ==", + "license": "MIT" + }, + "node_modules/@types/express": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz", + "integrity": "sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.33", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.6.tgz", + "integrity": "sha512-3xhRnjJPkULekpSzgtoNYYcTWgEZkp4myc+Saevii5JPnHNvHMRlBSHDbs7Bh1iPPoVTERHEZXyhyLbMEsExsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/express/node_modules/@types/express-serve-static-core": { + "version": "4.19.6", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.6.tgz", + "integrity": "sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/http-errors": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", + "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/http-proxy": { + "version": "1.17.15", + "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.15.tgz", + "integrity": "sha512-25g5atgiVNTIv0LBDTg1H74Hvayx0ajtJPLLcYE3whFv75J0pWNtOBzaXJQgDTmrX1bx5U9YC2w/n65BN1HwRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/jasmine": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/@types/jasmine/-/jasmine-4.3.6.tgz", + "integrity": "sha512-3N0FpQTeiWjm+Oo1WUYWguUS7E6JLceiGTriFrG8k5PU7zRLJCzLcWURU3wjMbZGS//a2/LgjsnO3QxIlwxt9g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "license": "MIT" + }, + "node_modules/@types/long": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz", + "integrity": "sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==", + "license": "MIT" + }, + "node_modules/@types/marked": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@types/marked/-/marked-4.3.2.tgz", + "integrity": "sha512-a79Yc3TOk6dGdituy8hmTTJXjOkZ7zsFYV10L337ttq/rec8lRMDBpV7fL3uLx6TgbFCa5DU/h8FmIBQPSbU0w==", + "license": "MIT" + }, + "node_modules/@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/minimist": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-hov8bUuiLiyFPGyFPE1lwWhmzYbirOXQNNo40+y3zow8aFVTeyn3VWL0VFFfdNddA8S4Vf0Tc062rzyNr7Paag==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "20.12.7", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.7.tgz", + "integrity": "sha512-wq0cICSkRLVaf3UGLMGItu/PtdY7oaXaI/RVU+xliKVOtRna3PRY57ZDfztpDL0n11vfymMUnXv8QwYCO7L1wg==", + "license": "MIT", + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/@types/node-forge": { + "version": "1.3.11", + "resolved": "https://registry.npmjs.org/@types/node-forge/-/node-forge-1.3.11.tgz", + "integrity": "sha512-FQx220y22OKNTqaByeBGqHWYz4cl94tpcxeFdvBo3wjG6XPBuZ0BNgNZRV5J5TFmmcsJ4IzsLkmGRiQbnYsBEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/normalize-package-data": { + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz", + "integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/parse-json": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", + "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/qs": { + "version": "6.9.18", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.18.tgz", + "integrity": "sha512-kK7dgTYDyGqS+e2Q4aK9X3D7q234CIZ1Bv0q/7Z5IwRDoADNU81xXJK/YVyLbLTZCoIwUoDoffFeF+p/eIklAA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/semver": { + "version": "7.5.8", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.8.tgz", + "integrity": "sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/send": { + "version": "0.17.4", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", + "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/serve-index": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@types/serve-index/-/serve-index-1.9.4.tgz", + "integrity": "sha512-qLpGZ/c2fhSs5gnYsQxtDEq3Oy8SXPClIXkW5ghvAvsNuVSA8k+gCONcUCS/UjLEYvYps+e8uBtfgXgvhwfNug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.7", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.7.tgz", + "integrity": "sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "*" + } + }, + "node_modules/@types/sockjs": { + "version": "0.3.36", + "resolved": "https://registry.npmjs.org/@types/sockjs/-/sockjs-0.3.36.tgz", + "integrity": "sha512-MK9V6NzAS1+Ud7JV9lJLFqW85VbC9dq3LmwZCuBe4wBDgKC0Kj/jd8Xl+nSviU+Qc3+m7umHHyHg//2KSa0a0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/triple-beam": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.5.tgz", + "integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==", + "license": "MIT" + }, + "node_modules/@types/uuid": { + "version": "9.0.8", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.8.tgz", + "integrity": "sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/ws": { + "version": "8.5.14", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.14.tgz", + "integrity": "sha512-bd/YFLW+URhBzMXurx7lWByOu+xzU9+kb3RboOteXYDfW+tr+JZa99OyNmPINEGB/ahzKrEuc8rcv4gnpJmxTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.62.0.tgz", + "integrity": "sha512-TiZzBSJja/LbhNPvk6yc0JrX9XqhQ0hdh6M2svYfsHGejaKFIAGd9MQ+ERIMzLGlN/kZoYIgdxFV0PuljTKXag==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.4.0", + "@typescript-eslint/scope-manager": "5.62.0", + "@typescript-eslint/type-utils": "5.62.0", + "@typescript-eslint/utils": "5.62.0", + "debug": "^4.3.4", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "natural-compare-lite": "^1.4.0", + "semver": "^7.3.7", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^5.0.0", + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.62.0.tgz", + "integrity": "sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/scope-manager": "5.62.0", + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/typescript-estree": "5.62.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.62.0.tgz", + "integrity": "sha512-VXuvVvZeQCQb5Zgf4HAxc04q5j+WrNAtNh9OwCsCgpKqESMTu3tF/jhZ3xG6T4NZwWl65Bg8KuS2uEvhSfLl0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/visitor-keys": "5.62.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.62.0.tgz", + "integrity": "sha512-xsSQreu+VnfbqQpW5vnCJdq1Z3Q0U31qiWmRhr98ONQmcp/yhiPJFPq8MXiJVLiksmOKSjIldZzkebzHuCGzew==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/typescript-estree": "5.62.0", + "@typescript-eslint/utils": "5.62.0", + "debug": "^4.3.4", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/types": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.62.0.tgz", + "integrity": "sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.62.0.tgz", + "integrity": "sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/visitor-keys": "5.62.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "semver": "^7.3.7", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/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/@typescript-eslint/typescript-estree/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/@typescript-eslint/utils": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.62.0.tgz", + "integrity": "sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@types/json-schema": "^7.0.9", + "@types/semver": "^7.3.12", + "@typescript-eslint/scope-manager": "5.62.0", + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/typescript-estree": "5.62.0", + "eslint-scope": "^5.1.1", + "semver": "^7.3.7" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.62.0.tgz", + "integrity": "sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "5.62.0", + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", + "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", + "dev": true, + "license": "ISC" + }, + "node_modules/@webassemblyjs/ast": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.1.tgz", + "integrity": "sha512-ukBh14qFLjxTQNTXocdyksN5QdM28S1CxHt2rdskFyL+xFV7VremuBLVbmCePj+URalXBENx/9Lm7lnhihtCSw==", + "license": "MIT", + "dependencies": { + "@webassemblyjs/helper-numbers": "1.11.1", + "@webassemblyjs/helper-wasm-bytecode": "1.11.1" + } + }, + "node_modules/@webassemblyjs/floating-point-hex-parser": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.1.tgz", + "integrity": "sha512-iGRfyc5Bq+NnNuX8b5hwBrRjzf0ocrJPI6GWFodBFzmFnyvrQ83SHKhmilCU/8Jv67i4GJZBMhEzltxzcNagtQ==", + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-api-error": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.1.tgz", + "integrity": "sha512-RlhS8CBCXfRUR/cwo2ho9bkheSXG0+NwooXcc3PAILALf2QLdFyj7KGsKRbVc95hZnhnERon4kW/D3SZpp6Tcg==", + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-buffer": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.1.tgz", + "integrity": "sha512-gwikF65aDNeeXa8JxXa2BAk+REjSyhrNC9ZwdT0f8jc4dQQeDQ7G4m0f2QCLPJiMTTO6wfDmRmj/pW0PsUvIcA==", + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-numbers": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.1.tgz", + "integrity": "sha512-vDkbxiB8zfnPdNK9Rajcey5C0w+QJugEglN0of+kmO8l7lDb77AnlKYQF7aarZuCrv+l0UvqL+68gSDr3k9LPQ==", + "license": "MIT", + "dependencies": { + "@webassemblyjs/floating-point-hex-parser": "1.11.1", + "@webassemblyjs/helper-api-error": "1.11.1", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/helper-wasm-bytecode": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.1.tgz", + "integrity": "sha512-PvpoOGiJwXeTrSf/qfudJhwlvDQxFgelbMqtq52WWiXC6Xgg1IREdngmPN3bs4RoO83PnL/nFrxucXj1+BX62Q==", + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-wasm-section": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.1.tgz", + "integrity": "sha512-10P9No29rYX1j7F3EVPX3JvGPQPae+AomuSTPiF9eBQeChHI6iqjMIwR9JmOJXwpnn/oVGDk7I5IlskuMwU/pg==", + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.11.1", + "@webassemblyjs/helper-buffer": "1.11.1", + "@webassemblyjs/helper-wasm-bytecode": "1.11.1", + "@webassemblyjs/wasm-gen": "1.11.1" + } + }, + "node_modules/@webassemblyjs/ieee754": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.11.1.tgz", + "integrity": "sha512-hJ87QIPtAMKbFq6CGTkZYJivEwZDbQUgYd3qKSadTNOhVY7p+gfP6Sr0lLRVTaG1JjFj+r3YchoqRYxNH3M0GQ==", + "license": "MIT", + "dependencies": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "node_modules/@webassemblyjs/leb128": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.11.1.tgz", + "integrity": "sha512-BJ2P0hNZ0u+Th1YZXJpzW6miwqQUGcIHT1G/sf72gLVD9DZ5AdYTqPNbHZh6K1M5VmKvFXwGSWZADz+qBWxeRw==", + "license": "Apache-2.0", + "dependencies": { + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/utf8": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.11.1.tgz", + "integrity": "sha512-9kqcxAEdMhiwQkHpkNiorZzqpGrodQQ2IGrHHxCy+Ozng0ofyMA0lTqiLkVs1uzTRejX+/O0EOT7KxqVPuXosQ==", + "license": "MIT" + }, + "node_modules/@webassemblyjs/wasm-edit": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.1.tgz", + "integrity": "sha512-g+RsupUC1aTHfR8CDgnsVRVZFJqdkFHpsHMfJuWQzWU3tvnLC07UqHICfP+4XyL2tnr1amvl1Sdp06TnYCmVkA==", + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.11.1", + "@webassemblyjs/helper-buffer": "1.11.1", + "@webassemblyjs/helper-wasm-bytecode": "1.11.1", + "@webassemblyjs/helper-wasm-section": "1.11.1", + "@webassemblyjs/wasm-gen": "1.11.1", + "@webassemblyjs/wasm-opt": "1.11.1", + "@webassemblyjs/wasm-parser": "1.11.1", + "@webassemblyjs/wast-printer": "1.11.1" + } + }, + "node_modules/@webassemblyjs/wasm-gen": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.1.tgz", + "integrity": "sha512-F7QqKXwwNlMmsulj6+O7r4mmtAlCWfO/0HdgOxSklZfQcDu0TpLiD1mRt/zF25Bk59FIjEuGAIyn5ei4yMfLhA==", + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.11.1", + "@webassemblyjs/helper-wasm-bytecode": "1.11.1", + "@webassemblyjs/ieee754": "1.11.1", + "@webassemblyjs/leb128": "1.11.1", + "@webassemblyjs/utf8": "1.11.1" + } + }, + "node_modules/@webassemblyjs/wasm-opt": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.1.tgz", + "integrity": "sha512-VqnkNqnZlU5EB64pp1l7hdm3hmQw7Vgqa0KF/KCNO9sIpI6Fk6brDEiX+iCOYrvMuBWDws0NkTOxYEb85XQHHw==", + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.11.1", + "@webassemblyjs/helper-buffer": "1.11.1", + "@webassemblyjs/wasm-gen": "1.11.1", + "@webassemblyjs/wasm-parser": "1.11.1" + } + }, + "node_modules/@webassemblyjs/wasm-parser": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.1.tgz", + "integrity": "sha512-rrBujw+dJu32gYB7/Lup6UhdkPx9S9SnobZzRVL7VcBH9Bt9bCBLEuX/YXOOtBsOZ4NQrRykKhffRWHvigQvOA==", + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.11.1", + "@webassemblyjs/helper-api-error": "1.11.1", + "@webassemblyjs/helper-wasm-bytecode": "1.11.1", + "@webassemblyjs/ieee754": "1.11.1", + "@webassemblyjs/leb128": "1.11.1", + "@webassemblyjs/utf8": "1.11.1" + } + }, + "node_modules/@webassemblyjs/wast-printer": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.11.1.tgz", + "integrity": "sha512-IQboUWM4eKzWW+N/jij2sRatKMh99QEelo3Eb2q0qXkvPRISAj8Qxtmw5itwqK+TTkBuUIE45AxYPToqPtL5gg==", + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.11.1", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@xtuc/ieee754": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", + "license": "BSD-3-Clause" + }, + "node_modules/@xtuc/long": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", + "license": "Apache-2.0" + }, + "node_modules/@yarnpkg/lockfile": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz", + "integrity": "sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/abab": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", + "integrity": "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==", + "deprecated": "Use your platform's native atob() and btoa() methods instead", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "dev": true, + "license": "ISC" + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/accepts/node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.14.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", + "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-import-assertions": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.9.0.tgz", + "integrity": "sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA==", + "deprecated": "package has been renamed to acorn-import-attributes", + "license": "MIT", + "peerDependencies": { + "acorn": "^8" + } + }, + "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/adjust-sourcemap-loader": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/adjust-sourcemap-loader/-/adjust-sourcemap-loader-4.0.0.tgz", + "integrity": "sha512-OXwN5b9pCUXNQHJpwwD2qP40byEmSgzj8B4ydSN0uMNYWiFmJ6x6KwUllMmfk8Rwu/HJDFR7U8ubsWBoN0Xp0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "loader-utils": "^2.0.0", + "regex-parser": "^2.2.11" + }, + "engines": { + "node": ">=8.9" + } + }, + "node_modules/adjust-sourcemap-loader/node_modules/loader-utils": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", + "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" + }, + "engines": { + "node": ">=8.9.0" + } + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/agentkeepalive": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz", + "integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "humanize-ms": "^1.2.1" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/aggregate-error": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", + "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ajv": { + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", + "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/ansi-colors": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "license": "MIT", + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-html-community": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/ansi-html-community/-/ansi-html-community-0.0.8.tgz", + "integrity": "sha512-1APHAyr3+PCamwNw3bXCPp4HFLONZt/yIH0sZp0/469KWNTEy+qN5jQ3GVX6DMZ1UXAi34yVwtTeaG/HpBuuzw==", + "dev": true, + "engines": [ + "node >= 0.8.0" + ], + "license": "Apache-2.0", + "bin": { + "ansi-html": "bin/ansi-html" + } + }, + "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==", + "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==", + "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==", + "dev": true + }, + "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==", + "devOptional": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/aproba": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", + "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/are-we-there-yet": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-3.0.1.tgz", + "integrity": "sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg==", + "deprecated": "This package is no longer supported.", + "dev": true, + "license": "ISC", + "dependencies": { + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "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==", + "dev": true + }, + "node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "dev": true, + "license": "MIT" + }, + "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/arrify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", + "integrity": "sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "license": "MIT" + }, + "node_modules/autoprefixer": { + "version": "10.4.21", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.21.tgz", + "integrity": "sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ==", + "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" + } + ], + "dependencies": { + "browserslist": "^4.24.4", + "caniuse-lite": "^1.0.30001702", + "fraction.js": "^4.3.7", + "normalize-range": "^0.1.2", + "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/autoprefixer/node_modules/browserslist": { + "version": "4.24.4", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.4.tgz", + "integrity": "sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==", + "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" + } + ], + "dependencies": { + "caniuse-lite": "^1.0.30001688", + "electron-to-chromium": "^1.5.73", + "node-releases": "^2.0.19", + "update-browserslist-db": "^1.1.1" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/babel-loader": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-9.1.2.tgz", + "integrity": "sha512-mN14niXW43tddohGl8HPu5yfQq70iUThvFL/4QzESA7GcZoC0eVOhvWdQ8+3UlSjaDE9MVtsW9mxDY07W7VpVA==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-cache-dir": "^3.3.2", + "schema-utils": "^4.0.0" + }, + "engines": { + "node": ">= 14.15.0" + }, + "peerDependencies": { + "@babel/core": "^7.12.0", + "webpack": ">=5" + } + }, + "node_modules/babel-plugin-istanbul": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", + "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-instrument": "^5.0.4", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-polyfill-corejs2": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.3.3.tgz", + "integrity": "sha512-8hOdmFYFSZhqg2C/JgLUQ+t52o5nirNwaWM2B9LWteozwIvM14VSwdsCAUET10qT+kmySAlseadmfeeSWFCy+Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.17.7", + "@babel/helper-define-polyfill-provider": "^0.3.3", + "semver": "^6.1.1" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/babel-plugin-polyfill-corejs2/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-plugin-polyfill-corejs3": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.6.0.tgz", + "integrity": "sha512-+eHqR6OPcBhJOGgsIar7xoAB1GcSwVUA3XjAd7HJNzOXT4wv6/H7KIdA/Nc60cvUlDbKApmqNvD1B1bzOt4nyA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.3.3", + "core-js-compat": "^3.25.1" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/babel-plugin-polyfill-regenerator": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.4.1.tgz", + "integrity": "sha512-NtQGmyQDXjQqQ+IzRkBVwEOz9lQ4zxAQZgoAYEtU9dJjnl1Oc98qnN7jcp+bE7O7aYzVpavXE3/VKXNzUbh7aw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.3.3" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "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/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "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/base64id": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", + "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^4.5.0 || >= 5.9" + } + }, + "node_modules/batch": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz", + "integrity": "sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw==", + "dev": true, + "license": "MIT" + }, + "node_modules/big.js": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", + "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "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==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/body-parser": { + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", + "dev": true, + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.13.0", + "raw-body": "2.5.2", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/body-parser/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/body-parser/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "license": "MIT" + }, + "node_modules/bonjour-service": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/bonjour-service/-/bonjour-service-1.3.0.tgz", + "integrity": "sha512-3YuAUiSkWykd+2Azjgyxei8OWf8thdn8AITIog2M4UICzoqfjlqr64WIjEXZllf/W6vK1goqleSR6brGomxQqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "multicast-dns": "^7.2.5" + } + }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "dev": true, + "license": "ISC" + }, + "node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.21.5", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.5.tgz", + "integrity": "sha512-tUkiguQGW7S3IhB7N+c2MV/HZPSCPAAiYBZXLsBhFB/PCy6ZKKsZrmBayHV9fdGV/ARIfJ14NkxKzRDjvp7L6w==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + } + ], + "license": "MIT", + "dependencies": { + "caniuse-lite": "^1.0.30001449", + "electron-to-chromium": "^1.4.284", + "node-releases": "^2.0.8", + "update-browserslist-db": "^1.0.10" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "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": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "license": "MIT" + }, + "node_modules/builtins": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/builtins/-/builtins-5.1.0.tgz", + "integrity": "sha512-SW9lzGTLvWTP1AY8xeAMZimqDrIaSdLQUcVr9DMef51niJ022Ri87SwRRKYm4A6iHfkPaiVUu/Duw2Wc4J7kKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.0.0" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/cacache": { + "version": "17.0.4", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-17.0.4.tgz", + "integrity": "sha512-Z/nL3gU+zTUjz5pCA5vVjYM8pmaw2kxM7JEiE0fv3w77Wj+sFbi70CrBruUWH0uNcEdvLDixFpgA2JM4F4DBjA==", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/fs": "^3.1.0", + "fs-minipass": "^3.0.0", + "glob": "^8.0.1", + "lru-cache": "^7.7.1", + "minipass": "^4.0.0", + "minipass-collect": "^1.0.2", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "p-map": "^4.0.0", + "promise-inflight": "^1.0.1", + "ssri": "^10.0.0", + "tar": "^6.1.11", + "unique-filename": "^3.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/cacache/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.1.tgz", + "integrity": "sha512-BhYE+WDaywFg2TBWYNXAE+8B1ATnThNBqXHP5nQu0jWJdVvY2hvkpyB3qOmtmDePiS5/BDQ8wASEWGMWRG148g==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.3.tgz", + "integrity": "sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "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==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/camelcase-keys": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-6.2.2.tgz", + "integrity": "sha512-YrwaA0vEKazPBkn0ipTiMpSajYDSe+KjQfrjhcBMxJt/znbvlHd8Pw/Vamaz5EB4Wfhs3SUR3Z9mwRu/P3s3Yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "camelcase": "^5.3.1", + "map-obj": "^4.0.0", + "quick-lru": "^4.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001707", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001707.tgz", + "integrity": "sha512-3qtRjw/HQSMlDWf+X79N206fepf4SOOU6SQLMaq/0KkZLmSjPxAkBOQQ+FxbHKfHmYLZFfdWsO3KA90ceHPSnw==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ] + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "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/chardet": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", + "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", + "license": "MIT" + }, + "node_modules/chokidar": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "devOptional": true, + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ], + "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" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/chrome-trace-event": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", + "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", + "license": "MIT", + "engines": { + "node": ">=6.0" + } + }, + "node_modules/clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "license": "MIT", + "dependencies": { + "restore-cursor": "^3.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cli-spinners": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", + "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-width": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-3.0.0.tgz", + "integrity": "sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==", + "license": "ISC", + "engines": { + "node": ">= 10" + } + }, + "node_modules/clipboard": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/clipboard/-/clipboard-2.0.11.tgz", + "integrity": "sha512-C+0bbOqkezLIsmWSvlsXS0Q0bmkugu7jcfMIACB+RDEntIzQIkdr148we28AfSloQLRdZlYL/QYyrq05j/3Faw==", + "license": "MIT", + "dependencies": { + "good-listener": "^1.2.2", + "select": "^1.1.2", + "tiny-emitter": "^2.0.0" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/clone": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/clone-deep": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", + "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-plain-object": "^2.0.4", + "kind-of": "^6.0.2", + "shallow-clone": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/color": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/color/-/color-3.2.1.tgz", + "integrity": "sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==", + "license": "MIT", + "dependencies": { + "color-convert": "^1.9.3", + "color-string": "^1.6.0" + } + }, + "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==", + "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==", + "license": "MIT" + }, + "node_modules/color-string": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", + "license": "MIT", + "dependencies": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, + "node_modules/color-support": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", + "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", + "dev": true, + "license": "ISC", + "bin": { + "color-support": "bin.js" + } + }, + "node_modules/color/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "license": "MIT", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/color/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "license": "MIT" + }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/colorspace": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/colorspace/-/colorspace-1.1.4.tgz", + "integrity": "sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w==", + "license": "MIT", + "dependencies": { + "color": "^3.1.3", + "text-hex": "1.0.x" + } + }, + "node_modules/commander": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 12" + } + }, + "node_modules/commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", + "dev": true, + "license": "MIT" + }, + "node_modules/compressible": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", + "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": ">= 1.43.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/compression": { + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.7.5.tgz", + "integrity": "sha512-bQJ0YRck5ak3LgtnpKkiabX5pNF7tMUh1BSy2ZBOTh0Dim0BUu6aPPwByIns6/A5Prh8PufSPerMDUklpzes2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "compressible": "~2.0.18", + "debug": "2.6.9", + "negotiator": "~0.6.4", + "on-headers": "~1.0.2", + "safe-buffer": "5.2.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/compression/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/compression/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "license": "MIT" + }, + "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/connect": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/connect/-/connect-3.7.0.tgz", + "integrity": "sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "finalhandler": "1.1.2", + "parseurl": "~1.3.3", + "utils-merge": "1.0.1" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/connect-history-api-fallback": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-2.0.0.tgz", + "integrity": "sha512-U73+6lQFmfiNPrYbXqr6kZ1i1wiRqXnp2nhMsINseWXO8lDau0LGEffJ8kQi4EjLZympVgRdvqjAgiZ1tgzDDA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/connect/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/connect/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "license": "MIT" + }, + "node_modules/console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/copy-anything": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-2.0.6.tgz", + "integrity": "sha512-1j20GZTsvKNkc4BY3NpMOM8tt///wY3FpIzozTOFO2ffuZcV61nojHXVKIy3WM+7ADCy5FVhdZYHYDdgTU0yJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-what": "^3.14.1" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, + "node_modules/copy-webpack-plugin": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-11.0.0.tgz", + "integrity": "sha512-fX2MWpamkW0hZxMEg0+mYnA40LTosOSa5TqZ9GYIBzyJa9C3QUaMPSE2xAi/buNr8u89SfD9wHSQVBzrRa/SOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-glob": "^3.2.11", + "glob-parent": "^6.0.1", + "globby": "^13.1.1", + "normalize-path": "^3.0.0", + "schema-utils": "^4.0.0", + "serialize-javascript": "^6.0.0" + }, + "engines": { + "node": ">= 14.15.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + } + }, + "node_modules/copy-webpack-plugin/node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/core-js-compat": { + "version": "3.40.0", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.40.0.tgz", + "integrity": "sha512-0XEDpr5y5mijvw8Lbc6E5AkjrHfp7eEoPlu36SWeAbcL8fn1G1ANe8DBlo2XoNN89oVpxWwOjYIPVzR4ZvsKCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "browserslist": "^4.24.3" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "node_modules/core-js-compat/node_modules/browserslist": { + "version": "4.24.4", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.4.tgz", + "integrity": "sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==", + "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": { + "caniuse-lite": "^1.0.30001688", + "electron-to-chromium": "^1.5.73", + "node-releases": "^2.0.19", + "update-browserslist-db": "^1.1.1" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/cose-base": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/cose-base/-/cose-base-1.0.3.tgz", + "integrity": "sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg==", + "license": "MIT", + "optional": true, + "dependencies": { + "layout-base": "^1.0.0" + } + }, + "node_modules/cosmiconfig": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", + "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.2.1", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.10.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/critters": { + "version": "0.0.16", + "resolved": "https://registry.npmjs.org/critters/-/critters-0.0.16.tgz", + "integrity": "sha512-JwjgmO6i3y6RWtLYmXwO5jMd+maZt8Tnfu7VVISmEWyQqfLpB8soBswf8/2bu6SBXxtKA68Al3c+qIG1ApT68A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "chalk": "^4.1.0", + "css-select": "^4.2.0", + "parse5": "^6.0.1", + "parse5-htmlparser2-tree-adapter": "^6.0.1", + "postcss": "^8.3.7", + "pretty-bytes": "^5.3.0" + } + }, + "node_modules/critters/node_modules/parse5": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz", + "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/cross-spawn/node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/css-loader": { + "version": "6.7.3", + "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-6.7.3.tgz", + "integrity": "sha512-qhOH1KlBMnZP8FzRO6YCH9UHXQhVMcEGLyNdb7Hv2cpcmJbW0YrddO+tG1ab5nT41KpHIYGsbeHqxB9xPu1pKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "icss-utils": "^5.1.0", + "postcss": "^8.4.19", + "postcss-modules-extract-imports": "^3.0.0", + "postcss-modules-local-by-default": "^4.0.0", + "postcss-modules-scope": "^3.0.0", + "postcss-modules-values": "^4.0.0", + "postcss-value-parser": "^4.2.0", + "semver": "^7.3.8" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" + } + }, + "node_modules/css-select": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-4.3.0.tgz", + "integrity": "sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.0.1", + "domhandler": "^4.3.1", + "domutils": "^2.8.0", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-what": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", + "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "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==", + "dev": true, + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/custom-event": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/custom-event/-/custom-event-1.0.1.tgz", + "integrity": "sha512-GAj5FOq0Hd+RsCGVJxZuKaIDXDf3h6GQoNEjFgbLLI/trgtavwUbSnZ5pVfg27DVCaWjIohryS0JFwIJyT2cMg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cytoscape": { + "version": "3.31.0", + "resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.31.0.tgz", + "integrity": "sha512-zDGn1K/tfZwEnoGOcHc0H4XazqAAXAuDpcYw9mUnUjATjqljyCNGJv8uEvbvxGaGHaVshxMecyl6oc6uKzRfbw==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/cytoscape-cose-bilkent": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cytoscape-cose-bilkent/-/cytoscape-cose-bilkent-4.1.0.tgz", + "integrity": "sha512-wgQlVIUJF13Quxiv5e1gstZ08rnZj2XaLHGoFMYXz7SkNfCDOOteKBE6SYRfA9WxxI/iBc3ajfDoc6hb/MRAHQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "cose-base": "^1.0.0" + }, + "peerDependencies": { + "cytoscape": "^3.2.0" + } + }, + "node_modules/cytoscape-fcose": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cytoscape-fcose/-/cytoscape-fcose-2.2.0.tgz", + "integrity": "sha512-ki1/VuRIHFCzxWNrsshHYPs6L7TvLu3DL+TyIGEsRcvVERmxokbf5Gdk7mFxZnTdiGtnA4cfSmjZJMviqSuZrQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "cose-base": "^2.2.0" + }, + "peerDependencies": { + "cytoscape": "^3.2.0" + } + }, + "node_modules/cytoscape-fcose/node_modules/cose-base": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cose-base/-/cose-base-2.2.0.tgz", + "integrity": "sha512-AzlgcsCbUMymkADOJtQm3wO9S3ltPfYOFD5033keQn9NJzIbtnZj+UdBJe7DYml/8TdbtHJW3j58SOnKhWY/5g==", + "license": "MIT", + "optional": true, + "dependencies": { + "layout-base": "^2.0.0" + } + }, + "node_modules/cytoscape-fcose/node_modules/layout-base": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/layout-base/-/layout-base-2.0.1.tgz", + "integrity": "sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg==", + "license": "MIT", + "optional": true + }, + "node_modules/d3": { + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/d3/-/d3-7.9.0.tgz", + "integrity": "sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==", + "license": "ISC", + "optional": true, + "dependencies": { + "d3-array": "3", + "d3-axis": "3", + "d3-brush": "3", + "d3-chord": "3", + "d3-color": "3", + "d3-contour": "4", + "d3-delaunay": "6", + "d3-dispatch": "3", + "d3-drag": "3", + "d3-dsv": "3", + "d3-ease": "3", + "d3-fetch": "3", + "d3-force": "3", + "d3-format": "3", + "d3-geo": "3", + "d3-hierarchy": "3", + "d3-interpolate": "3", + "d3-path": "3", + "d3-polygon": "3", + "d3-quadtree": "3", + "d3-random": "3", + "d3-scale": "4", + "d3-scale-chromatic": "3", + "d3-selection": "3", + "d3-shape": "3", + "d3-time": "3", + "d3-time-format": "4", + "d3-timer": "3", + "d3-transition": "3", + "d3-zoom": "3" + }, + "engines": { + "node": ">=12" + } + }, + "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", + "optional": true, + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-axis": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-3.0.0.tgz", + "integrity": "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==", + "license": "ISC", + "optional": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-brush": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-3.0.0.tgz", + "integrity": "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==", + "license": "ISC", + "optional": true, + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "3", + "d3-transition": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-chord": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-chord/-/d3-chord-3.0.1.tgz", + "integrity": "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==", + "license": "ISC", + "optional": true, + "dependencies": { + "d3-path": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "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", + "optional": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-contour": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-contour/-/d3-contour-4.0.2.tgz", + "integrity": "sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==", + "license": "ISC", + "optional": true, + "dependencies": { + "d3-array": "^3.2.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-delaunay": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==", + "license": "ISC", + "optional": true, + "dependencies": { + "delaunator": "5" + }, + "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", + "optional": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-drag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", + "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", + "license": "ISC", + "optional": true, + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-selection": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dsv": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-3.0.1.tgz", + "integrity": "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==", + "license": "ISC", + "optional": true, + "dependencies": { + "commander": "7", + "iconv-lite": "0.6", + "rw": "1" + }, + "bin": { + "csv2json": "bin/dsv2json.js", + "csv2tsv": "bin/dsv2dsv.js", + "dsv2dsv": "bin/dsv2dsv.js", + "dsv2json": "bin/dsv2json.js", + "json2csv": "bin/json2dsv.js", + "json2dsv": "bin/json2dsv.js", + "json2tsv": "bin/json2dsv.js", + "tsv2csv": "bin/dsv2dsv.js", + "tsv2json": "bin/dsv2json.js" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dsv/node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/d3-dsv/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "optional": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", + "optional": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-fetch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-fetch/-/d3-fetch-3.0.1.tgz", + "integrity": "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==", + "license": "ISC", + "optional": true, + "dependencies": { + "d3-dsv": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-force": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-3.0.0.tgz", + "integrity": "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==", + "license": "ISC", + "optional": true, + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-quadtree": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz", + "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==", + "license": "ISC", + "optional": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-geo": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-3.1.1.tgz", + "integrity": "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==", + "license": "ISC", + "optional": true, + "dependencies": { + "d3-array": "2.5.0 - 3" + }, + "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", + "optional": true, + "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", + "optional": true, + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "license": "ISC", + "optional": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-polygon": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-3.0.1.tgz", + "integrity": "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==", + "license": "ISC", + "optional": true, + "engines": { + "node": ">=12" + } + }, + "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", + "optional": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-random": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-random/-/d3-random-3.0.1.tgz", + "integrity": "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==", + "license": "ISC", + "optional": true, + "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", + "optional": true, + "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-scale-chromatic": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", + "integrity": "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==", + "license": "ISC", + "optional": true, + "dependencies": { + "d3-color": "1 - 3", + "d3-interpolate": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-selection": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", + "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", + "license": "ISC", + "optional": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "license": "ISC", + "optional": true, + "dependencies": { + "d3-path": "^3.1.0" + }, + "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", + "optional": true, + "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", + "optional": true, + "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", + "optional": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-transition": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", + "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", + "license": "ISC", + "optional": true, + "dependencies": { + "d3-color": "1 - 3", + "d3-dispatch": "1 - 3", + "d3-ease": "1 - 3", + "d3-interpolate": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "d3-selection": "2 - 3" + } + }, + "node_modules/d3-zoom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz", + "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", + "license": "ISC", + "optional": true, + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "2 - 3", + "d3-transition": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/dagre-d3-es": { + "version": "7.0.9", + "resolved": "https://registry.npmjs.org/dagre-d3-es/-/dagre-d3-es-7.0.9.tgz", + "integrity": "sha512-rYR4QfVmy+sR44IBDvVtcAmOReGBvRCWDpO2QjYwqgh9yijw6eSHBqaPG/LIOEy7aBsniLvtMW6pg19qJhq60w==", + "license": "MIT", + "optional": true, + "dependencies": { + "d3": "^7.8.2", + "lodash-es": "^4.17.21" + } + }, + "node_modules/date-format": { + "version": "4.0.14", + "resolved": "https://registry.npmjs.org/date-format/-/date-format-4.0.14.tgz", + "integrity": "sha512-39BOQLs9ZjKh0/patS9nrT8wc3ioX3/eA/zgbKNopnF2wCqJEoxywwwElATYvRsXdnOxA/OQeQoFZ3rFjVajhg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/dayjs": { + "version": "1.11.13", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz", + "integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==", + "license": "MIT", + "optional": true + }, + "node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/decamelize-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/decamelize-keys/-/decamelize-keys-1.1.1.tgz", + "integrity": "sha512-WiPxgEirIV0/eIOMcnFBA3/IJZAZqKnwAwWyvvdi4lsr1WCN22nhdf/3db3DoZcUjTV2SqfzIwNyp6y2xs3nmg==", + "dev": true, + "license": "MIT", + "dependencies": { + "decamelize": "^1.1.0", + "map-obj": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/decamelize-keys/node_modules/map-obj": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz", + "integrity": "sha512-7N/q3lyZ+LVCp7PzuxrJr4KMbBE2hW7BT7YNia330OFxIf4d3r5zVpicP2650l7CPN6RM9zOJRl3NGpqSiw3Eg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "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/default-gateway": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/default-gateway/-/default-gateway-6.0.3.tgz", + "integrity": "sha512-fwSOJsbbNzZ/CUFpqFBqYfYNLj1NbMPm8MMCIzHjC83iSJRBEGmDUxU+WP661BaBQImeC2yHwXtz+P/O9o+XEg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "execa": "^5.0.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/defaults": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", + "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==", + "license": "MIT", + "dependencies": { + "clone": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/define-lazy-prop": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", + "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/delaunator": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.0.1.tgz", + "integrity": "sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==", + "license": "ISC", + "optional": true, + "dependencies": { + "robust-predicates": "^3.0.2" + } + }, + "node_modules/delegate": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/delegate/-/delegate-3.2.0.tgz", + "integrity": "sha512-IofjkYBZaZivn0V8nnsMJGBr4jVLxHDheKSW88PyxS5QC4Vo9ZbZVvhzlSxY87fVq3STR6r+4cGepyHkcWOQSw==", + "license": "MIT" + }, + "node_modules/delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/dependency-graph": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/dependency-graph/-/dependency-graph-0.11.0.tgz", + "integrity": "sha512-JeMq7fEshyepOWDfcfHK06N3MhyPhz++vtqWhMT5O9A3K42rdsEDpfdVqjaqaAhsw6a+ZqeDvQVtD0hFHQWrzg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/detect-node": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", + "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", + "dev": true, + "license": "MIT" + }, + "node_modules/di": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/di/-/di-0.0.1.tgz", + "integrity": "sha512-uJaamHkagcZtHPqCIHZxnFrXlunQXgBOsZSUOWwFw31QJCAbyTBoHMW75YOTur5ZNx8pIeAKgf6GWIgaqqiLhA==", + "dev": true, + "license": "MIT" + }, + "node_modules/didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "dev": true + }, + "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==", + "dev": true + }, + "node_modules/dns-packet": { + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-5.6.1.tgz", + "integrity": "sha512-l4gcSouhcgIKRvyy99RNVOgxXiicE+2jZoNmaNmZ6JXiGajBOJAesk1OBlJuM5k2c+eudGdLxDqXuPCKIj6kpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@leichtgewicht/ip-codec": "^2.0.1" + }, + "engines": { + "node": ">=6" + } + }, + "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/dom-serialize": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/dom-serialize/-/dom-serialize-2.2.1.tgz", + "integrity": "sha512-Yra4DbvoW7/Z6LBN560ZwXMjoNOSAN2wRsKFGc4iBeso+mpIA6qj1vfdf9HpMaKAqG6wXTy+1SYEzmNpKXOSsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "custom-event": "~1.0.0", + "ent": "~2.2.0", + "extend": "^3.0.0", + "void-elements": "^2.0.0" + } + }, + "node_modules/dom-serializer": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz", + "integrity": "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==", + "dev": true, + "license": "MIT", + "dependencies": { + "domelementtype": "^2.0.1", + "domhandler": "^4.2.0", + "entities": "^2.0.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, + "node_modules/domhandler": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz", + "integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.2.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/dompurify": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.4.3.tgz", + "integrity": "sha512-q6QaLcakcRjebxjg8/+NP+h0rPfatOgOzc46Fst9VAA3jF2ApfKBNKMzdP4DYTqtUMXSCd5pRS/8Po/OmoCHZQ==", + "license": "(MPL-2.0 OR Apache-2.0)", + "optional": true + }, + "node_modules/domutils": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", + "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^1.0.1", + "domelementtype": "^2.2.0", + "domhandler": "^4.2.0" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "dev": true, + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.90", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.90.tgz", + "integrity": "sha512-C3PN4aydfW91Natdyd449Kw+BzhLmof6tzy5W1pFC5SpQxVXT+oyiyOG9AgYYSN9OdA/ik3YkCrpwqI8ug5Tug==", + "license": "ISC" + }, + "node_modules/elkjs": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/elkjs/-/elkjs-0.8.2.tgz", + "integrity": "sha512-L6uRgvZTH+4OF5NE/MBbzQx/WYpru1xCBE9respNj6qznEewGUIfhzmm7horWWxbNO2M0WckQypGctR8lH79xQ==", + "license": "EPL-2.0", + "optional": true + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/emoji-toolkit": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/emoji-toolkit/-/emoji-toolkit-7.0.1.tgz", + "integrity": "sha512-l5aJyAhpC5s4mDuoVuqt4SzVjwIsIvakPh4ZGJJE4KWuWFCEHaXacQFkStVdD9zbRR+/BbRXob7u99o0lQFr8A==", + "license": "MIT", + "optional": true + }, + "node_modules/emojis-list": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", + "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/enabled": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz", + "integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/encoding": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", + "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", + "license": "MIT", + "optional": true, + "dependencies": { + "iconv-lite": "^0.6.2" + } + }, + "node_modules/encoding/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "optional": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/engine.io": { + "version": "6.6.4", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.4.tgz", + "integrity": "sha512-ZCkIjSYNDyGn0R6ewHDtXgns/Zre/NT6Agvq1/WobF7JXgFff4SeDroKiCO3fNJreU9YG429Sc81o4w5ok/W5g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/cors": "^2.8.12", + "@types/node": ">=10.0.0", + "accepts": "~1.3.4", + "base64id": "2.0.0", + "cookie": "~0.7.2", + "cors": "~2.8.5", + "debug": "~4.3.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.17.1" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/engine.io-parser": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", + "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/engine.io/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/enhanced-resolve": { + "version": "5.18.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.0.tgz", + "integrity": "sha512-0/r0MySGYG8YqlayBZ6MuCfECmHFdJ5qyPh8s8wa5Hnm6SaFLSK1VYCbj+NKp090Nm1caZhD+QTnmxO7esYGyQ==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/ent": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/ent/-/ent-2.2.2.tgz", + "integrity": "sha512-kKvD1tO6BM+oK9HzCPpUdRb4vKFQY/FPTFmurMvh6LlN68VMrdj77w8yp51/kDbpkFOS9J8w5W6zIzgM2H8/hw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "punycode": "^1.4.1", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/entities": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", + "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", + "dev": true, + "license": "BSD-2-Clause", + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/err-code": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", + "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", + "dev": true, + "license": "MIT" + }, + "node_modules/errno": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.8.tgz", + "integrity": "sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "prr": "~1.0.1" + }, + "bin": { + "errno": "cli.js" + } + }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-module-lexer": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-0.9.3.tgz", + "integrity": "sha512-1HQ2M2sPtxwnvOvT1ZClHyQDiggdNjURWpY2we6aMKCQiUVxTmVs2UYPLIrD84sS+kMdUwfBSylbJPwNnBrnHQ==", + "license": "MIT" + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.17.8", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.17.8.tgz", + "integrity": "sha512-g24ybC3fWhZddZK6R3uD2iF/RIPnRpwJAqLov6ouX3hMbY4+tKolP0VMF3zuIYCaXun+yHwS5IPQ91N2BT191g==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/android-arm": "0.17.8", + "@esbuild/android-arm64": "0.17.8", + "@esbuild/android-x64": "0.17.8", + "@esbuild/darwin-arm64": "0.17.8", + "@esbuild/darwin-x64": "0.17.8", + "@esbuild/freebsd-arm64": "0.17.8", + "@esbuild/freebsd-x64": "0.17.8", + "@esbuild/linux-arm": "0.17.8", + "@esbuild/linux-arm64": "0.17.8", + "@esbuild/linux-ia32": "0.17.8", + "@esbuild/linux-loong64": "0.17.8", + "@esbuild/linux-mips64el": "0.17.8", + "@esbuild/linux-ppc64": "0.17.8", + "@esbuild/linux-riscv64": "0.17.8", + "@esbuild/linux-s390x": "0.17.8", + "@esbuild/linux-x64": "0.17.8", + "@esbuild/netbsd-x64": "0.17.8", + "@esbuild/openbsd-x64": "0.17.8", + "@esbuild/sunos-x64": "0.17.8", + "@esbuild/win32-arm64": "0.17.8", + "@esbuild/win32-ia32": "0.17.8", + "@esbuild/win32-x64": "0.17.8" + } + }, + "node_modules/esbuild-wasm": { + "version": "0.17.8", + "resolved": "https://registry.npmjs.org/esbuild-wasm/-/esbuild-wasm-0.17.8.tgz", + "integrity": "sha512-zCmpxv95E0FuCmvdw1K836UHnj4EdiQnFfjTby35y3LAjRPtXMj3sbHDRHjbD8Mqg5lTwq3knacr/1qIFU51CQ==", + "dev": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "dev": true, + "license": "MIT" + }, + "node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/eslint": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz", + "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==", + "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.0", + "@humanwhocodes/config-array": "^0.11.14", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-config-prettier": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz", + "integrity": "sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==", + "dev": true, + "license": "MIT", + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-plugin-es": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-es/-/eslint-plugin-es-4.1.0.tgz", + "integrity": "sha512-GILhQTnjYE2WorX5Jyi5i4dz5ALWxBIdQECVQavL6s7cI76IZTDWleTHkxz/QT3kvcs2QlGHvKLYsSlPOlPXnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-utils": "^2.0.0", + "regexpp": "^3.0.0" + }, + "engines": { + "node": ">=8.10.0" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + }, + "peerDependencies": { + "eslint": ">=4.19.1" + } + }, + "node_modules/eslint-plugin-es/node_modules/eslint-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.1.0.tgz", + "integrity": "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^1.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + } + }, + "node_modules/eslint-plugin-es/node_modules/eslint-visitor-keys": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", + "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=4" + } + }, + "node_modules/eslint-plugin-n": { + "version": "15.7.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-n/-/eslint-plugin-n-15.7.0.tgz", + "integrity": "sha512-jDex9s7D/Qial8AGVIHq4W7NswpUD5DPDL2RH8Lzd9EloWUuvUkHfv4FRLMipH5q2UtyurorBkPeNi1wVWNh3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "builtins": "^5.0.1", + "eslint-plugin-es": "^4.1.0", + "eslint-utils": "^3.0.0", + "ignore": "^5.1.1", + "is-core-module": "^2.11.0", + "minimatch": "^3.1.2", + "resolve": "^1.22.1", + "semver": "^7.3.8" + }, + "engines": { + "node": ">=12.22.0" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-plugin-n/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint-plugin-n/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/eslint-plugin-prettier": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.1.3.tgz", + "integrity": "sha512-C9GCVAs4Eq7ZC/XFQHITLiHJxQngdtraXaM+LoUFoFp/lHNl2Zn8f3WQbe9HvTBBQ9YnKFB0/2Ajdqwo5D1EAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "prettier-linter-helpers": "^1.0.0", + "synckit": "^0.8.6" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint-plugin-prettier" + }, + "peerDependencies": { + "@types/eslint": ">=8.0.0", + "eslint": ">=8.0.0", + "eslint-config-prettier": "*", + "prettier": ">=3.0.0" + }, + "peerDependenciesMeta": { + "@types/eslint": { + "optional": true + }, + "eslint-config-prettier": { + "optional": true + } + } + }, + "node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/eslint-utils": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz", + "integrity": "sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^2.0.0" + }, + "engines": { + "node": "^10.0.0 || ^12.0.0 || >= 14.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + }, + "peerDependencies": { + "eslint": ">=5" + } + }, + "node_modules/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", + "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10" + } + }, + "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/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "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/eslint/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/eslint/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/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/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/eslint/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/eslint/node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/eslint/node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/eslint/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/eslint/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/eslint/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/eslint/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/eslint/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/eslint/node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "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/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esquery/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/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esrecurse/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "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/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/eventemitter-asyncresource": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/eventemitter-asyncresource/-/eventemitter-asyncresource-1.0.0.tgz", + "integrity": "sha512-39F7TBIV0G7gTelxwbEqnwhp90eqCPON1k0NwNfwhgKn4Co4ybUbj2pECcXT0B3ztRKZ7Pw1JujUUgmQJHcVAQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "dev": true, + "license": "MIT" + }, + "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/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/exponential-backoff": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.1.tgz", + "integrity": "sha512-dX7e/LHVJ6W3DE1MHWi9S1EYzDESENfLrYohG2G++ovZrYOkm4Knwa0mc1cn84xJOR4KEU0WSchhLbd0UklbHw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/express": { + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", + "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.3", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.7.1", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.3.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.12", + "proxy-addr": "~2.0.7", + "qs": "6.13.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.19.0", + "serve-static": "1.16.2", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express/node_modules/cookie": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/express/node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/express/node_modules/finalhandler": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/express/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "license": "MIT" + }, + "node_modules/express/node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "dev": true, + "license": "MIT" + }, + "node_modules/external-editor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", + "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", + "license": "MIT", + "dependencies": { + "chardet": "^0.7.0", + "iconv-lite": "^0.4.24", + "tmp": "^0.0.33" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-diff": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz", + "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", + "dev": true, + "license": "Apache-2.0" + }, + "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==", + "dev": true, + "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-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==", + "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.19.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.0.tgz", + "integrity": "sha512-7SFSRCNjBQIZH/xZR3iy5iQYR8aGBE0h3VG6/cwlbrpdciNYBMotQav8c1XI3HjHH+NikUpP53nPdlZSdWmFzA==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/faye-websocket": { + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz", + "integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==", + "license": "Apache-2.0", + "dependencies": { + "websocket-driver": ">=0.5.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/fecha": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz", + "integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==", + "license": "MIT" + }, + "node_modules/figures": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", + "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^1.0.5" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/file-loader": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/file-loader/-/file-loader-6.2.0.tgz", + "integrity": "sha512-qo3glqyTa61Ytg4u73GultjHGjdRyig3tG6lPtyX/jOEJvHif9uB0/OCI2Kif6ctF3caQTW2G5gym21oAsI4pw==", + "license": "MIT", + "dependencies": { + "loader-utils": "^2.0.0", + "schema-utils": "^3.0.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^4.0.0 || ^5.0.0" + } + }, + "node_modules/file-loader/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "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/file-loader/node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "license": "MIT", + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "node_modules/file-loader/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==", + "license": "MIT" + }, + "node_modules/file-loader/node_modules/loader-utils": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", + "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", + "license": "MIT", + "dependencies": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" + }, + "engines": { + "node": ">=8.9.0" + } + }, + "node_modules/file-loader/node_modules/schema-utils": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "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==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", + "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "statuses": "~1.5.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/finalhandler/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/finalhandler/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "license": "MIT" + }, + "node_modules/finalhandler/node_modules/on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==", + "dev": true, + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/find-cache-dir": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.2.tgz", + "integrity": "sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==", + "dev": true, + "license": "MIT", + "dependencies": { + "commondir": "^1.0.1", + "make-dir": "^3.0.2", + "pkg-dir": "^4.1.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/avajs/find-cache-dir?sponsor=1" + } + }, + "node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase": { + "version": "9.23.0", + "resolved": "https://registry.npmjs.org/firebase/-/firebase-9.23.0.tgz", + "integrity": "sha512-/4lUVY0lUvBDIaeY1q6dUYhS8Sd18Qb9CgWkPZICUo9IXpJNCEagfNZXBBFCkMTTN5L5gx2Hjr27y21a9NzUcA==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/analytics": "0.10.0", + "@firebase/analytics-compat": "0.2.6", + "@firebase/app": "0.9.13", + "@firebase/app-check": "0.8.0", + "@firebase/app-check-compat": "0.3.7", + "@firebase/app-compat": "0.2.13", + "@firebase/app-types": "0.9.0", + "@firebase/auth": "0.23.2", + "@firebase/auth-compat": "0.4.2", + "@firebase/database": "0.14.4", + "@firebase/database-compat": "0.3.4", + "@firebase/firestore": "3.13.0", + "@firebase/firestore-compat": "0.3.12", + "@firebase/functions": "0.10.0", + "@firebase/functions-compat": "0.3.5", + "@firebase/installations": "0.6.4", + "@firebase/installations-compat": "0.2.4", + "@firebase/messaging": "0.12.4", + "@firebase/messaging-compat": "0.2.4", + "@firebase/performance": "0.6.4", + "@firebase/performance-compat": "0.2.4", + "@firebase/remote-config": "0.4.4", + "@firebase/remote-config-compat": "0.2.4", + "@firebase/storage": "0.11.2", + "@firebase/storage-compat": "0.3.2", + "@firebase/util": "1.9.3" + } + }, + "node_modules/firebase-tools": { + "version": "11.30.0", + "resolved": "https://registry.npmjs.org/firebase-tools/-/firebase-tools-11.30.0.tgz", + "integrity": "sha512-rNMhtsFKy35AZqfy0obY2Kgsfmg8N1duEx2UDT14NsMHB7xt2Z7cUd92bkAoQAfD0jAOd7LhRT/7E6J5yB+kGw==", + "devOptional": true, + "hasShrinkwrap": true, + "license": "MIT", + "dependencies": { + "@google-cloud/pubsub": "^3.0.1", + "abort-controller": "^3.0.0", + "ajv": "^6.12.6", + "archiver": "^5.0.0", + "async-lock": "1.3.2", + "body-parser": "^1.19.0", + "chokidar": "^3.0.2", + "cjson": "^0.3.1", + "cli-table": "0.3.11", + "colorette": "^2.0.19", + "commander": "^4.0.1", + "configstore": "^5.0.1", + "cors": "^2.8.5", + "cross-env": "^5.1.3", + "cross-spawn": "^7.0.3", + "csv-parse": "^5.0.4", + "exegesis": "^4.1.0", + "exegesis-express": "^4.0.0", + "express": "^4.16.4", + "filesize": "^6.1.0", + "form-data": "^4.0.0", + "fs-extra": "^10.1.0", + "glob": "^7.1.2", + "google-auth-library": "^7.11.0", + "inquirer": "^8.2.0", + "js-yaml": "^3.13.1", + "jsonwebtoken": "^9.0.0", + "leven": "^3.1.0", + "libsodium-wrappers": "^0.7.10", + "lodash": "^4.17.21", + "marked": "^4.0.14", + "marked-terminal": "^5.1.1", + "mime": "^2.5.2", + "minimatch": "^3.0.4", + "morgan": "^1.10.0", + "node-fetch": "^2.6.7", + "open": "^6.3.0", + "ora": "^5.4.1", + "p-limit": "^3.0.1", + "portfinder": "^1.0.32", + "progress": "^2.0.3", + "proxy-agent": "^5.0.0", + "request": "^2.87.0", + "retry": "^0.13.1", + "rimraf": "^3.0.0", + "semver": "^5.7.1", + "stream-chain": "^2.2.4", + "stream-json": "^1.7.3", + "strip-ansi": "^6.0.1", + "superstatic": "^9.0.3", + "tar": "^6.1.11", + "tcp-port-used": "^1.0.2", + "tmp": "^0.2.1", + "triple-beam": "^1.3.0", + "universal-analytics": "^0.5.3", + "update-notifier-cjs": "^5.1.6", + "uuid": "^8.3.2", + "winston": "^3.0.0", + "winston-transport": "^4.4.0", + "ws": "^7.2.3" + }, + "bin": { + "firebase": "lib/bin/firebase.js" + }, + "engines": { + "node": "^14.18.0 || >=16.4.0" + } + }, + "node_modules/firebase-tools/node_modules/@apidevtools/json-schema-ref-parser": { + "version": "9.0.7", + "resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-9.0.7.tgz", + "integrity": "sha512-QdwOGF1+eeyFh+17v2Tz626WX0nucd1iKOm6JUTUvCZdbolblCOOQCxGrQPY0f7jEhn36PiAWqZnsC2r5vmUWg==", + "devOptional": true, + "dependencies": { + "@jsdevtools/ono": "^7.1.3", + "call-me-maybe": "^1.0.1", + "js-yaml": "^3.13.1" + } + }, + "node_modules/firebase-tools/node_modules/@babel/parser": { + "version": "7.21.4", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.21.4.tgz", + "integrity": "sha512-alVJj7k7zIxqBZ7BTRhz0IqJFxW1VJbm6N8JbcYhQ186df9ZBPbZBmWSqAMXwHGsCJdYks7z/voa3ibiS5bCIw==", + "devOptional": true, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/firebase-tools/node_modules/@colors/colors": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", + "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==", + "optional": true, + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/firebase-tools/node_modules/@gar/promisify": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz", + "integrity": "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==", + "optional": true + }, + "node_modules/firebase-tools/node_modules/@google-cloud/precise-date": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@google-cloud/precise-date/-/precise-date-2.0.3.tgz", + "integrity": "sha512-+SDJ3ZvGkF7hzo6BGa8ZqeK3F6Z4+S+KviC9oOK+XCs3tfMyJCh/4j93XIWINgMMDIh9BgEvlw4306VxlXIlYA==", + "devOptional": true, + "engines": { + "node": ">=10.4.0" + } + }, + "node_modules/firebase-tools/node_modules/@google-cloud/projectify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@google-cloud/projectify/-/projectify-2.0.1.tgz", + "integrity": "sha512-ZDG38U/Yy6Zr21LaR3BTiiLtpJl6RkPS/JwoRT453G+6Q1DhlV0waNf8Lfu+YVYGIIxgKnLayJRfYlFJfiI8iQ==", + "devOptional": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/firebase-tools/node_modules/@google-cloud/promisify": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@google-cloud/promisify/-/promisify-2.0.3.tgz", + "integrity": "sha512-d4VSA86eL/AFTe5xtyZX+ePUjE8dIFu2T8zmdeNBSa5/kNgXPCx/o/wbFNHAGLJdGnk1vddRuMESD9HbOC8irw==", + "devOptional": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/firebase-tools/node_modules/@google-cloud/pubsub": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@google-cloud/pubsub/-/pubsub-3.0.1.tgz", + "integrity": "sha512-dznNbRd/Y8J0C0xvdvCPi3B1msK/dj/Nya+NQZ2doUOLT6eoa261tBwk9umOQs5L5GKcdlqQKbBjrNjDYVbzQA==", + "devOptional": true, + "dependencies": { + "@google-cloud/paginator": "^4.0.0", + "@google-cloud/precise-date": "^2.0.0", + "@google-cloud/projectify": "^2.0.0", + "@google-cloud/promisify": "^2.0.0", + "@opentelemetry/api": "^1.0.0", + "@opentelemetry/semantic-conventions": "^1.0.0", + "@types/duplexify": "^3.6.0", + "@types/long": "^4.0.0", + "arrify": "^2.0.0", + "extend": "^3.0.2", + "google-auth-library": "^8.0.2", + "google-gax": "^3.0.1", + "is-stream-ended": "^0.1.4", + "lodash.snakecase": "^4.1.1", + "p-defer": "^3.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/firebase-tools/node_modules/@google-cloud/pubsub/node_modules/@google-cloud/paginator": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@google-cloud/paginator/-/paginator-4.0.0.tgz", + "integrity": "sha512-wNmCZl+2G2DmgT/VlF+AROf80SoaC/CwS8trwmjNaq26VRNK8yPbU5F/Vy+R9oDAGKWQU2k8+Op5H4kFJVXFaQ==", + "devOptional": true, + "dependencies": { + "arrify": "^2.0.0", + "extend": "^3.0.2" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/firebase-tools/node_modules/@google-cloud/pubsub/node_modules/gaxios": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-5.0.0.tgz", + "integrity": "sha512-VD/yc5ln6XU8Ch1hyYY6kRMBE0Yc2np3fPyeJeYHhrPs1i8rgnsApPMWyrugkl7LLoSqpOJVBWlQIa87OAvt8Q==", + "devOptional": true, + "dependencies": { + "abort-controller": "^3.0.0", + "extend": "^3.0.2", + "https-proxy-agent": "^5.0.0", + "is-stream": "^2.0.0", + "node-fetch": "^2.6.7" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/firebase-tools/node_modules/@google-cloud/pubsub/node_modules/gcp-metadata": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-5.0.0.tgz", + "integrity": "sha512-gfwuX3yA3nNsHSWUL4KG90UulNiq922Ukj3wLTrcnX33BB7PwB1o0ubR8KVvXu9nJH+P5w1j2SQSNNqto+H0DA==", + "devOptional": true, + "dependencies": { + "gaxios": "^5.0.0", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/firebase-tools/node_modules/@google-cloud/pubsub/node_modules/google-auth-library": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-8.0.2.tgz", + "integrity": "sha512-HoG+nWFAThLovKpvcbYzxgn+nBJPTfAwtq0GxPN821nOO+21+8oP7MoEHfd1sbDulUFFGfcjJr2CnJ4YssHcyg==", + "devOptional": true, + "dependencies": { + "arrify": "^2.0.0", + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "fast-text-encoding": "^1.0.0", + "gaxios": "^5.0.0", + "gcp-metadata": "^5.0.0", + "gtoken": "^5.3.2", + "jws": "^4.0.0", + "lru-cache": "^6.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/firebase-tools/node_modules/@google-cloud/pubsub/node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "devOptional": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/firebase-tools/node_modules/@google-cloud/pubsub/node_modules/jwa": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz", + "integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==", + "devOptional": true, + "dependencies": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/firebase-tools/node_modules/@google-cloud/pubsub/node_modules/jws": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", + "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", + "devOptional": true, + "dependencies": { + "jwa": "^2.0.0", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/firebase-tools/node_modules/@grpc/grpc-js": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.7.3.tgz", + "integrity": "sha512-H9l79u4kJ2PVSxUNA08HMYAnUBLj9v6KjYQ7SQ71hOZcEXhShE/y5iQCesP8+6/Ik/7i2O0a10bPquIcYfufog==", + "devOptional": true, + "dependencies": { + "@grpc/proto-loader": "^0.7.0", + "@types/node": ">=12.12.47" + }, + "engines": { + "node": "^8.13.0 || >=10.10.0" + } + }, + "node_modules/firebase-tools/node_modules/@grpc/grpc-js/node_modules/@grpc/proto-loader": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.4.tgz", + "integrity": "sha512-MnWjkGwqQ3W8fx94/c1CwqLsNmHHv2t0CFn+9++6+cDphC1lolpg9M2OU0iebIjK//pBNX9e94ho+gjx6vz39w==", + "devOptional": true, + "dependencies": { + "@types/long": "^4.0.1", + "lodash.camelcase": "^4.3.0", + "long": "^4.0.0", + "protobufjs": "^7.0.0", + "yargs": "^16.2.0" + }, + "bin": { + "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/firebase-tools/node_modules/@grpc/grpc-js/node_modules/protobufjs": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.1.2.tgz", + "integrity": "sha512-4ZPTPkXCdel3+L81yw3dG6+Kq3umdWKh7Dc7GW/CpNk4SX3hK58iPCWeCyhVTDrbkNeKrYNZ7EojM5WDaEWTLQ==", + "devOptional": true, + "hasInstallScript": true, + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/node": ">=13.7.0", + "long": "^5.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/firebase-tools/node_modules/@grpc/grpc-js/node_modules/protobufjs/node_modules/long": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/long/-/long-5.2.1.tgz", + "integrity": "sha512-GKSNGeNAtw8IryjjkhZxuKB3JzlcLTwjtiQCHKvqQet81I93kXslhDQruGI/QsddO83mcDToBVy7GqGS/zYf/A==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/@jsdevtools/ono": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz", + "integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/@jsdoc/salty": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@jsdoc/salty/-/salty-0.2.3.tgz", + "integrity": "sha512-bbtCxCkxcnWhi50I+4Lj6mdz9w3pOXOgEQrID8TCZ/DF51fW7M9GCQW2y45SpBDdHd1Eirm1X/Cf6CkAAe8HPg==", + "devOptional": true, + "dependencies": { + "lodash": "^4.17.21" + }, + "engines": { + "node": ">=v12.0.0" + } + }, + "node_modules/firebase-tools/node_modules/@npmcli/fs": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-2.1.2.tgz", + "integrity": "sha512-yOJKRvohFOaLqipNtwYB9WugyZKhC/DZC4VYPmpaCzDBrA8YpK3qHZ8/HGscMnE4GqbkLNuVcCnxkeQEdGt6LQ==", + "optional": true, + "dependencies": { + "@gar/promisify": "^1.1.3", + "semver": "^7.3.5" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/firebase-tools/node_modules/@npmcli/fs/node_modules/semver": { + "version": "7.3.7", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz", + "integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==", + "optional": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/firebase-tools/node_modules/@npmcli/move-file": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@npmcli/move-file/-/move-file-2.0.1.tgz", + "integrity": "sha512-mJd2Z5TjYWq/ttPLLGqArdtnC74J6bOzg4rMDnN+p1xTacZ2yPRCk2y0oSWQtygLR9YVQXgOcONrwtnk3JupxQ==", + "optional": true, + "dependencies": { + "mkdirp": "^1.0.4", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/firebase-tools/node_modules/@npmcli/move-file/node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "optional": true, + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/firebase-tools/node_modules/@opentelemetry/api": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.0.4.tgz", + "integrity": "sha512-BuJuXRSJNQ3QoKA6GWWDyuLpOUck+9hAXNMCnrloc1aWVoy6Xq6t9PUV08aBZ4Lutqq2LEHM486bpZqoViScog==", + "devOptional": true, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/firebase-tools/node_modules/@opentelemetry/semantic-conventions": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.2.0.tgz", + "integrity": "sha512-BNKB9fiYVghALJzCuWO3eNYfdTExPVK4ykrtmfNfy0A6UWYhOYjGMXifUmkunDJNL8ju9tBobo8jF0WR9zGy1Q==", + "devOptional": true, + "engines": { + "node": ">=8.12.0" + } + }, + "node_modules/firebase-tools/node_modules/@pnpm/network.ca-file": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@pnpm/network.ca-file/-/network.ca-file-1.0.2.tgz", + "integrity": "sha512-YcPQ8a0jwYU9bTdJDpXjMi7Brhkr1mXsXrUJvjqM2mQDgkRiz8jFaQGOdaLxgjtUfQgZhKy/O3cG/YwmgKaxLA==", + "devOptional": true, + "dependencies": { + "graceful-fs": "4.2.10" + }, + "engines": { + "node": ">=12.22.0" + } + }, + "node_modules/firebase-tools/node_modules/@pnpm/npm-conf": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@pnpm/npm-conf/-/npm-conf-1.0.5.tgz", + "integrity": "sha512-hD8ml183638O3R6/Txrh0L8VzGOrFXgRtRDG4qQC4tONdZ5Z1M+tlUUDUvrjYdmK6G+JTBTeaCLMna11cXzi8A==", + "devOptional": true, + "dependencies": { + "@pnpm/network.ca-file": "^1.0.1", + "config-chain": "^1.1.11" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/firebase-tools/node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha1-m4sMxmPWaafY9vXQiToU00jzD78=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/@protobufjs/codegen": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/@protobufjs/eventemitter": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha1-NVy8mLr61ZePntCV85diHx0Ga3A=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/@protobufjs/fetch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", + "integrity": "sha1-upn7WYYUr2VwDBYZ/wbUVLDYTEU=", + "devOptional": true, + "dependencies": { + "@protobufjs/aspromise": "^1.1.1", + "@protobufjs/inquire": "^1.1.0" + } + }, + "node_modules/firebase-tools/node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha1-Xp4avctz/Ap8uLKR33jIy9l7h9E=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/@protobufjs/inquire": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", + "integrity": "sha1-/yAOPnzyQp4tyvwRQIKOjMY48Ik=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha1-bMKyDFya1q0NzP0hynZz2Nf79o0=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha1-Cf0V8tbTq/qbZbw2ZQbWrXhG/1Q=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/@protobufjs/utf8": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", + "integrity": "sha1-p3c2C1s5oaLlEG+OhY8v0tBgxXA=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/@tootallnate/once": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", + "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==", + "devOptional": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/firebase-tools/node_modules/@types/duplexify": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/@types/duplexify/-/duplexify-3.6.0.tgz", + "integrity": "sha512-5zOA53RUlzN74bvrSGwjudssD9F3a797sDZQkiYpUOxW+WHaXTCPz4/d5Dgi6FKnOqZ2CpaTo0DhgIfsXAOE/A==", + "devOptional": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/firebase-tools/node_modules/@types/linkify-it": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-3.0.2.tgz", + "integrity": "sha512-HZQYqbiFVWufzCwexrvh694SOim8z2d+xJl5UNamcvQFejLY/2YUtzXHYi3cHdI7PMlS8ejH2slRAOJQ32aNbA==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/@types/long": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.1.tgz", + "integrity": "sha512-5tXH6Bx/kNGd3MgffdmP4dy2Z+G4eaXw0SE81Tq3BNadtnMR5/ySMzX4SLEzHJzSmPNn4HIdpQsBvXMUykr58w==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/@types/markdown-it": { + "version": "12.2.3", + "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-12.2.3.tgz", + "integrity": "sha512-GKMHFfv3458yYy+v/N8gjufHO6MSZKCOXpZc5GXIWWy8uldwfmPn98vp81gZ5f9SVw8YYBctgfJ22a2d7AOMeQ==", + "devOptional": true, + "dependencies": { + "@types/linkify-it": "*", + "@types/mdurl": "*" + } + }, + "node_modules/firebase-tools/node_modules/@types/mdurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-1.0.2.tgz", + "integrity": "sha512-eC4U9MlIcu2q0KQmXszyn5Akca/0jrQmwDRgpAMJai7qBWq4amIQhZyNau4VYGtCeALvW1/NtjzJJ567aZxfKA==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/@types/node": { + "version": "14.18.36", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.36.tgz", + "integrity": "sha512-FXKWbsJ6a1hIrRxv+FoukuHnGTgEzKYGi7kilfMae96AL9UNkPFNWJEEYWzdRI9ooIkbr4AKldyuSTLql06vLQ==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "optional": true + }, + "node_modules/firebase-tools/node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "devOptional": true, + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, + "node_modules/firebase-tools/node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "devOptional": true, + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/firebase-tools/node_modules/acorn": { + "version": "8.8.2", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz", + "integrity": "sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==", + "devOptional": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/firebase-tools/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==", + "devOptional": true, + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/firebase-tools/node_modules/acorn-walk": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz", + "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==", + "devOptional": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/firebase-tools/node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "devOptional": true, + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/firebase-tools/node_modules/agent-base/node_modules/debug": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "devOptional": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/firebase-tools/node_modules/agent-base/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/agentkeepalive": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.2.1.tgz", + "integrity": "sha512-Zn4cw2NEqd+9fiSVWMscnjyQ1a8Yfoc5oBajLeo5w+YBHgDUcEBY2hS4YpTz6iN5f/2zQiktcuM6tS8x1p9dpA==", + "optional": true, + "dependencies": { + "debug": "^4.1.0", + "depd": "^1.1.2", + "humanize-ms": "^1.2.1" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/firebase-tools/node_modules/agentkeepalive/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "optional": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/firebase-tools/node_modules/agentkeepalive/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "optional": true + }, + "node_modules/firebase-tools/node_modules/aggregate-error": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", + "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "optional": true, + "dependencies": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "devOptional": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/firebase-tools/node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "devOptional": true, + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/firebase-tools/node_modules/ajv-formats/node_modules/ajv": { + "version": "8.8.2", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.8.2.tgz", + "integrity": "sha512-x9VuX+R/jcFj1DHo/fCp99esgGDWiHENrKxaCENuCxpoMCmAt/COCGVDwA7kleEpEzJjDnvh3yGoOuLu0Dtllw==", + "devOptional": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/firebase-tools/node_modules/ajv-formats/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/ansi-align": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.1.tgz", + "integrity": "sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==", + "devOptional": true, + "dependencies": { + "string-width": "^4.1.0" + } + }, + "node_modules/firebase-tools/node_modules/ansi-escapes": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-5.0.0.tgz", + "integrity": "sha512-5GFMVX8HqE/TB+FuBJGuO5XG0WrsA6ptUqoODaT/n9mmUaZFkqnBueB4leqGBCmrUHnCnC4PCZTCd0E7QQ83bA==", + "devOptional": true, + "dependencies": { + "type-fest": "^1.0.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/firebase-tools/node_modules/ansi-escapes/node_modules/type-fest": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-1.4.0.tgz", + "integrity": "sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==", + "devOptional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/firebase-tools/node_modules/ansicolors": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/ansicolors/-/ansicolors-0.3.2.tgz", + "integrity": "sha1-ZlWX3oap/+Oqm/vmyuXG6kJrSXk=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/anymatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", + "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==", + "devOptional": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/firebase-tools/node_modules/aproba": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", + "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==", + "optional": true + }, + "node_modules/firebase-tools/node_modules/archiver": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/archiver/-/archiver-5.3.1.tgz", + "integrity": "sha512-8KyabkmbYrH+9ibcTScQ1xCJC/CGcugdVIwB+53f5sZziXgwUh3iXlAlANMxcZyDEfTHMe6+Z5FofV8nopXP7w==", + "devOptional": true, + "dependencies": { + "archiver-utils": "^2.1.0", + "async": "^3.2.3", + "buffer-crc32": "^0.2.1", + "readable-stream": "^3.6.0", + "readdir-glob": "^1.0.0", + "tar-stream": "^2.2.0", + "zip-stream": "^4.1.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/firebase-tools/node_modules/archiver-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-2.1.0.tgz", + "integrity": "sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw==", + "devOptional": true, + "dependencies": { + "glob": "^7.1.4", + "graceful-fs": "^4.2.0", + "lazystream": "^1.0.0", + "lodash.defaults": "^4.2.0", + "lodash.difference": "^4.5.0", + "lodash.flatten": "^4.4.0", + "lodash.isplainobject": "^4.0.6", + "lodash.union": "^4.6.0", + "normalize-path": "^3.0.0", + "readable-stream": "^2.0.0" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/firebase-tools/node_modules/archiver-utils/node_modules/readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "devOptional": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/firebase-tools/node_modules/archiver/node_modules/async": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.3.tgz", + "integrity": "sha512-spZRyzKL5l5BZQrr/6m/SqFdBN0q3OCI0f9rjfBzCMBIP4p75P620rR3gTmaksNOhmzgdxcaxdNfMy6anrbM0g==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/are-we-there-yet": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-3.0.1.tgz", + "integrity": "sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg==", + "optional": true, + "dependencies": { + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/firebase-tools/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "devOptional": true, + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/firebase-tools/node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/arrify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-2.0.1.tgz", + "integrity": "sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==", + "devOptional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/as-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/as-array/-/as-array-2.0.0.tgz", + "integrity": "sha1-TwSAXYf4/OjlEbwhCPjl46KH1Uc=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/asn1": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", + "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", + "devOptional": true, + "dependencies": { + "safer-buffer": "~2.1.0" + } + }, + "node_modules/firebase-tools/node_modules/assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", + "devOptional": true, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/firebase-tools/node_modules/ast-types": { + "version": "0.13.4", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz", + "integrity": "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==", + "devOptional": true, + "dependencies": { + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/firebase-tools/node_modules/ast-types/node_modules/tslib": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz", + "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/async": { + "version": "2.6.4", + "resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz", + "integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==", + "devOptional": true, + "dependencies": { + "lodash": "^4.17.14" + } + }, + "node_modules/firebase-tools/node_modules/async-lock": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/async-lock/-/async-lock-1.3.2.tgz", + "integrity": "sha512-phnXdS3RP7PPcmP6NWWzWMU0sLTeyvtZCxBPpZdkYE3seGLKSQZs9FrmVO/qwypq98FUtWWUEYxziLkdGk5nnA==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/aws-sign2": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", + "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=", + "devOptional": true, + "engines": { + "node": "*" + } + }, + "node_modules/firebase-tools/node_modules/aws4": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz", + "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "devOptional": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/firebase-tools/node_modules/basic-auth": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", + "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==", + "devOptional": true, + "dependencies": { + "safe-buffer": "5.1.2" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/firebase-tools/node_modules/basic-auth-connect": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/basic-auth-connect/-/basic-auth-connect-1.0.0.tgz", + "integrity": "sha1-/bC0OWLKe0BFanwrtI/hc9otISI=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", + "devOptional": true, + "dependencies": { + "tweetnacl": "^0.14.3" + } + }, + "node_modules/firebase-tools/node_modules/bignumber.js": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.0.2.tgz", + "integrity": "sha512-GAcQvbpsM0pUb0zw1EI0KhQEZ+lRwR5fYaAp3vPOYuP7aDvGy6cVN6XHLauvF8SOga2y0dcLcjt3iQDTSEliyw==", + "devOptional": true, + "engines": { + "node": "*" + } + }, + "node_modules/firebase-tools/node_modules/binary-extensions": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.0.0.tgz", + "integrity": "sha512-Phlt0plgpIIBOGTT/ehfFnbNlfsDEiqmzE2KRXoX1bLIlir4X/MR+zSyBEkL05ffWgnRSf/DXv+WrUAVr93/ow==", + "devOptional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "devOptional": true, + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/firebase-tools/node_modules/body-parser": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", + "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==", + "devOptional": true, + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.4", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.11.0", + "raw-body": "2.5.1", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/firebase-tools/node_modules/body-parser/node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "devOptional": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/firebase-tools/node_modules/body-parser/node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "devOptional": true, + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/firebase-tools/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "devOptional": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/firebase-tools/node_modules/braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "devOptional": true, + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "devOptional": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/firebase-tools/node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI=", + "devOptional": true, + "engines": { + "node": "*" + } + }, + "node_modules/firebase-tools/node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "devOptional": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/firebase-tools/node_modules/cacache": { + "version": "16.1.3", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-16.1.3.tgz", + "integrity": "sha512-/+Emcj9DAXxX4cwlLmRI9c166RuL3w30zp4R7Joiv2cQTtTtA+jeuCAjH3ZlGnYS3tKENSrKhAzVVP9GVyzeYQ==", + "optional": true, + "dependencies": { + "@npmcli/fs": "^2.1.0", + "@npmcli/move-file": "^2.0.0", + "chownr": "^2.0.0", + "fs-minipass": "^2.1.0", + "glob": "^8.0.1", + "infer-owner": "^1.0.4", + "lru-cache": "^7.7.1", + "minipass": "^3.1.6", + "minipass-collect": "^1.0.2", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "mkdirp": "^1.0.4", + "p-map": "^4.0.0", + "promise-inflight": "^1.0.1", + "rimraf": "^3.0.2", + "ssri": "^9.0.0", + "tar": "^6.1.11", + "unique-filename": "^2.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/firebase-tools/node_modules/cacache/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "optional": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/firebase-tools/node_modules/cacache/node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "optional": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/firebase-tools/node_modules/cacache/node_modules/glob": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.0.3.tgz", + "integrity": "sha512-ull455NHSHI/Y1FqGaaYFaLGkNMMJbavMrEGFXG/PGrg6y7sutWHUHrz6gy6WEBH6akM1M414dWKCNs+IhKdiQ==", + "optional": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/firebase-tools/node_modules/cacache/node_modules/lru-cache": { + "version": "7.14.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.14.0.tgz", + "integrity": "sha512-EIRtP1GrSJny0dqb50QXRUNBxHJhcpxHC++M5tD7RYbvLLn5KVWKsbyswSSqDuU15UFi3bgTQIY8nhDMeF6aDQ==", + "optional": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/firebase-tools/node_modules/cacache/node_modules/minimatch": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.0.tgz", + "integrity": "sha512-9TPBGGak4nHfGZsPBohm9AWg6NoT7QTCehS3BIJABslyZbzxfV78QM2Y6+i741OPZIafFAaiiEMh5OyIrJPgtg==", + "optional": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/firebase-tools/node_modules/cacache/node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "optional": true, + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/firebase-tools/node_modules/cacache/node_modules/p-map": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", + "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", + "optional": true, + "dependencies": { + "aggregate-error": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/firebase-tools/node_modules/call-bind": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", + "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "devOptional": true, + "dependencies": { + "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/firebase-tools/node_modules/call-me-maybe": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.1.tgz", + "integrity": "sha1-JtII6onje1y95gJQoV8DHBak1ms=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/cardinal": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/cardinal/-/cardinal-2.1.1.tgz", + "integrity": "sha1-fMEFXYItISlU0HsIXeolHMe8VQU=", + "devOptional": true, + "dependencies": { + "ansicolors": "~0.3.2", + "redeyed": "~2.1.0" + }, + "bin": { + "cdl": "bin/cdl.js" + } + }, + "node_modules/firebase-tools/node_modules/caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/catharsis": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/catharsis/-/catharsis-0.9.0.tgz", + "integrity": "sha512-prMTQVpcns/tzFgFVkVp6ak6RykZyWb3gu8ckUpd6YkTlacOd3DXGJjIpD4Q6zJirizvaiAjSSHlOsA+6sNh2A==", + "devOptional": true, + "dependencies": { + "lodash": "^4.17.15" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/firebase-tools/node_modules/chardet": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", + "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/chokidar": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "devOptional": true, + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ], + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/firebase-tools/node_modules/ci-info": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz", + "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/cjson": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/cjson/-/cjson-0.3.3.tgz", + "integrity": "sha1-qS2ceG5b+bkwgGMp7gXV0yYbSvo=", + "devOptional": true, + "dependencies": { + "json-parse-helpfulerror": "^1.0.3" + }, + "engines": { + "node": ">= 0.3.0" + } + }, + "node_modules/firebase-tools/node_modules/clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", + "optional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/firebase-tools/node_modules/cli-boxes": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-2.2.1.tgz", + "integrity": "sha512-y4coMcylgSCdVinjiDBuR8PCC2bLjyGTwEmPb9NHR/QaNU6EUOXcTY/s6VjGMD6ENSEaeQYHCY0GNGS5jfMwPw==", + "devOptional": true, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/firebase-tools/node_modules/cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "devOptional": true, + "dependencies": { + "restore-cursor": "^3.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/cli-spinners": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.6.1.tgz", + "integrity": "sha512-x/5fWmGMnbKQAaNwN+UZlV79qBLM9JFnJuJ03gIi5whrob0xV0ofNVHy9DhwGdsMJQc2OKv0oGmLzvaqvAVv+g==", + "devOptional": true, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/firebase-tools/node_modules/cli-table": { + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/cli-table/-/cli-table-0.3.11.tgz", + "integrity": "sha512-IqLQi4lO0nIB4tcdTpN4LCB9FI3uqrJZK7RC515EnhZ6qBaglkIgICb1wjeAqpdoOabm1+SuQtkXIPdYC93jhQ==", + "devOptional": true, + "dependencies": { + "colors": "1.0.3" + }, + "engines": { + "node": ">= 0.2.0" + } + }, + "node_modules/firebase-tools/node_modules/cli-table3": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.2.tgz", + "integrity": "sha512-QyavHCaIC80cMivimWu4aWHilIpiDpfm3hGmqAmXVL1UsnbLuBSMd21hTX6VY4ZSDSM73ESLeF8TOYId3rBTbw==", + "devOptional": true, + "dependencies": { + "string-width": "^4.2.0" + }, + "engines": { + "node": "10.* || >= 12.*" + }, + "optionalDependencies": { + "@colors/colors": "1.5.0" + } + }, + "node_modules/firebase-tools/node_modules/cli-width": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-3.0.0.tgz", + "integrity": "sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==", + "devOptional": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/firebase-tools/node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "devOptional": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/firebase-tools/node_modules/clone": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha1-2jCcwmPfFZlMaIypAheco8fNfH4=", + "devOptional": true, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/firebase-tools/node_modules/color": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/color/-/color-3.0.0.tgz", + "integrity": "sha512-jCpd5+s0s0t7p3pHQKpnJ0TpQKKdleP71LWcA0aqiljpiuAkOSUFN/dyH8ZwF0hRmFlrIuRhufds1QyEP9EB+w==", + "devOptional": true, + "dependencies": { + "color-convert": "^1.9.1", + "color-string": "^1.5.2" + } + }, + "node_modules/firebase-tools/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "devOptional": true, + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/firebase-tools/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/color-string": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.5.5.tgz", + "integrity": "sha512-jgIoum0OfQfq9Whcfc2z/VhCNcmQjWbey6qBX0vqt7YICflUmBCh9E9CiQD5GSJ+Uehixm3NUwHVhqUAWRivZg==", + "devOptional": true, + "dependencies": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, + "node_modules/firebase-tools/node_modules/color-support": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", + "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", + "optional": true, + "bin": { + "color-support": "bin.js" + } + }, + "node_modules/firebase-tools/node_modules/colorette": { + "version": "2.0.19", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.19.tgz", + "integrity": "sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/colornames": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/colornames/-/colornames-1.1.1.tgz", + "integrity": "sha1-+IiQMGhcfE/54qVZ9Qd+t2qBb5Y=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/colors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.0.3.tgz", + "integrity": "sha1-BDP0TYCWgP3rYO0mDxsMJi6CpAs=", + "devOptional": true, + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/firebase-tools/node_modules/colorspace": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/colorspace/-/colorspace-1.1.2.tgz", + "integrity": "sha512-vt+OoIP2d76xLhjwbBaucYlNSpPsrJWPlBTtwCpQKIu6/CSMutyzX93O/Do0qzpH3YoHEes8YEFXyZ797rEhzQ==", + "devOptional": true, + "dependencies": { + "color": "3.0.x", + "text-hex": "1.0.x" + } + }, + "node_modules/firebase-tools/node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "devOptional": true, + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/firebase-tools/node_modules/commander": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.0.1.tgz", + "integrity": "sha512-IPF4ouhCP+qdlcmCedhxX4xiGBPyigb8v5NeUp+0LyhwLgxMqyp3S0vl7TAPfS/hiP7FC3caI/PB9lTmP8r1NA==", + "devOptional": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/firebase-tools/node_modules/compress-commons": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-4.1.1.tgz", + "integrity": "sha512-QLdDLCKNV2dtoTorqgxngQCMA+gWXkM/Nwu7FpeBhk/RdkzimqC3jueb/FDmaZeXh+uby1jkBqE3xArsLBE5wQ==", + "devOptional": true, + "dependencies": { + "buffer-crc32": "^0.2.13", + "crc32-stream": "^4.0.2", + "normalize-path": "^3.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/firebase-tools/node_modules/compressible": { + "version": "2.0.17", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.17.tgz", + "integrity": "sha512-BGHeLCK1GV7j1bSmQQAi26X+GgWcTjLr/0tzSvMCl3LH1w1IJ4PFSPoV5316b30cneTziC+B1a+3OjoSUcQYmw==", + "devOptional": true, + "dependencies": { + "mime-db": ">= 1.40.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/firebase-tools/node_modules/compression": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.7.4.tgz", + "integrity": "sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==", + "devOptional": true, + "dependencies": { + "accepts": "~1.3.5", + "bytes": "3.0.0", + "compressible": "~2.0.16", + "debug": "2.6.9", + "on-headers": "~1.0.2", + "safe-buffer": "5.1.2", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/firebase-tools/node_modules/compression/node_modules/bytes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", + "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=", + "devOptional": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/firebase-tools/node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/config-chain": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.13.tgz", + "integrity": "sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==", + "devOptional": true, + "dependencies": { + "ini": "^1.3.4", + "proto-list": "~1.2.1" + } + }, + "node_modules/firebase-tools/node_modules/configstore": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/configstore/-/configstore-5.0.1.tgz", + "integrity": "sha512-aMKprgk5YhBNyH25hj8wGt2+D52Sw1DRRIzqBwLp2Ya9mFmY8KPvvtvmna8SxVR9JMZ4kzMD68N22vlaRpkeFA==", + "devOptional": true, + "dependencies": { + "dot-prop": "^5.2.0", + "graceful-fs": "^4.1.2", + "make-dir": "^3.0.0", + "unique-string": "^2.0.0", + "write-file-atomic": "^3.0.0", + "xdg-basedir": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/configstore/node_modules/crypto-random-string": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz", + "integrity": "sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==", + "devOptional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/configstore/node_modules/dot-prop": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.2.0.tgz", + "integrity": "sha512-uEUyaDKoSQ1M4Oq8l45hSE26SnTxL6snNnqvK/VWx5wJhmff5z0FUVJDKDanor/6w3kzE3i7XZOk+7wC0EXr1A==", + "devOptional": true, + "dependencies": { + "is-obj": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/configstore/node_modules/unique-string": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-2.0.0.tgz", + "integrity": "sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==", + "devOptional": true, + "dependencies": { + "crypto-random-string": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/connect": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/connect/-/connect-3.7.0.tgz", + "integrity": "sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ==", + "devOptional": true, + "dependencies": { + "debug": "2.6.9", + "finalhandler": "1.1.2", + "parseurl": "~1.3.3", + "utils-merge": "1.0.1" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/firebase-tools/node_modules/console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==", + "optional": true + }, + "node_modules/firebase-tools/node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "devOptional": true, + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/firebase-tools/node_modules/content-disposition/node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "devOptional": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/firebase-tools/node_modules/content-type": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", + "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==", + "devOptional": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/firebase-tools/node_modules/cookie": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", + "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", + "devOptional": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/firebase-tools/node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "devOptional": true, + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/firebase-tools/node_modules/crc-32": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", + "devOptional": true, + "bin": { + "crc32": "bin/crc32.njs" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/firebase-tools/node_modules/crc32-stream": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-4.0.2.tgz", + "integrity": "sha512-DxFZ/Hk473b/muq1VJ///PMNLj0ZMnzye9thBpmjpJKCc5eMgB95aK8zCGrGfQ90cWo561Te6HK9D+j4KPdM6w==", + "devOptional": true, + "dependencies": { + "crc-32": "^1.2.0", + "readable-stream": "^3.4.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/firebase-tools/node_modules/cross-env": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-5.2.0.tgz", + "integrity": "sha512-jtdNFfFW1hB7sMhr/H6rW1Z45LFqyI431m3qU6bFXcQ3Eh7LtBuG3h74o7ohHZ3crrRkkqHlo4jYHFPcjroANg==", + "devOptional": true, + "dependencies": { + "cross-spawn": "^6.0.5", + "is-windows": "^1.0.0" + }, + "bin": { + "cross-env": "dist/bin/cross-env.js", + "cross-env-shell": "dist/bin/cross-env-shell.js" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/firebase-tools/node_modules/cross-env/node_modules/cross-spawn": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", + "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", + "devOptional": true, + "dependencies": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + }, + "engines": { + "node": ">=4.8" + } + }, + "node_modules/firebase-tools/node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "devOptional": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/firebase-tools/node_modules/cross-spawn/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==", + "devOptional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/cross-spawn/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==", + "devOptional": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/cross-spawn/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==", + "devOptional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/cross-spawn/node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "devOptional": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/firebase-tools/node_modules/csv-parse": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/csv-parse/-/csv-parse-5.0.4.tgz", + "integrity": "sha512-5AIdl8l6n3iYQYxan5djB5eKDa+vBnhfWZtRpJTcrETWfVLYN0WSj3L9RwvgYt+psoO77juUr8TG8qpfGZifVQ==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", + "devOptional": true, + "dependencies": { + "assert-plus": "^1.0.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/firebase-tools/node_modules/data-uri-to-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-3.0.1.tgz", + "integrity": "sha512-WboRycPNsVw3B3TL559F7kuBUM4d8CgMEvk6xEJlOp7OBPjt6G7z8WMWlD2rOFZLk6OYfFIUGsCOWzcQH9K2og==", + "devOptional": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/firebase-tools/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "devOptional": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/firebase-tools/node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "devOptional": true, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/firebase-tools/node_modules/deep-freeze": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/deep-freeze/-/deep-freeze-0.0.1.tgz", + "integrity": "sha1-OgsABd4YZygZ39OM0x+RF5yJPoQ=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/deep-is": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", + "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/defaults": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.3.tgz", + "integrity": "sha1-xlYFHpgX2f8I7YgUd/P+QBnz730=", + "devOptional": true, + "dependencies": { + "clone": "^1.0.2" + } + }, + "node_modules/firebase-tools/node_modules/degenerator": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-3.0.1.tgz", + "integrity": "sha512-LFsIFEeLPlKvAKXu7j3ssIG6RT0TbI7/GhsqrI0DnHASEQjXQ0LUSYcjJteGgRGmZbl1TnMSxpNQIAiJ7Du5TQ==", + "devOptional": true, + "dependencies": { + "ast-types": "^0.13.2", + "escodegen": "^1.8.1", + "esprima": "^4.0.0", + "vm2": "^3.9.3" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/firebase-tools/node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", + "devOptional": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/firebase-tools/node_modules/delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", + "optional": true + }, + "node_modules/firebase-tools/node_modules/depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=", + "optional": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/firebase-tools/node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "devOptional": true, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/firebase-tools/node_modules/diagnostics": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/diagnostics/-/diagnostics-1.1.1.tgz", + "integrity": "sha512-8wn1PmdunLJ9Tqbx+Fx/ZEuHfJf4NKSN2ZBj7SJC/OWRWha843+WsTjqMe1B5E3p28jqBlp+mJ2fPVxPyNgYKQ==", + "devOptional": true, + "dependencies": { + "colorspace": "1.1.x", + "enabled": "1.0.x", + "kuler": "1.0.x" + } + }, + "node_modules/firebase-tools/node_modules/duplexify": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.1.tgz", + "integrity": "sha512-DY3xVEmVHTv1wSzKNbwoU6nVjzI369Y6sPoqfYr0/xlx3IdX2n94xIszTcjPO8W8ZIv0Wb0PXNcjuZyT4wiICA==", + "devOptional": true, + "dependencies": { + "end-of-stream": "^1.4.1", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1", + "stream-shift": "^1.0.0" + } + }, + "node_modules/firebase-tools/node_modules/ecc-jsbn": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", + "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", + "devOptional": true, + "dependencies": { + "jsbn": "~0.1.0", + "safer-buffer": "^2.1.0" + } + }, + "node_modules/firebase-tools/node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "devOptional": true, + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/firebase-tools/node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/enabled": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/enabled/-/enabled-1.0.2.tgz", + "integrity": "sha1-ll9lE9LC0cX0ZStkouM5ZGf8L5M=", + "devOptional": true, + "dependencies": { + "env-variable": "0.0.x" + } + }, + "node_modules/firebase-tools/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=", + "devOptional": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/firebase-tools/node_modules/encoding": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", + "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", + "optional": true, + "dependencies": { + "iconv-lite": "^0.6.2" + } + }, + "node_modules/firebase-tools/node_modules/encoding/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "optional": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/firebase-tools/node_modules/end-of-stream": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.1.tgz", + "integrity": "sha512-1MkrZNvWTKCaigbn+W15elq2BB/L22nqrSY5DKlo3X6+vclJm8Bb5djXJBmEX6fS3+zCh/F4VBK5Z2KxJt4s2Q==", + "devOptional": true, + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/firebase-tools/node_modules/entities": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.1.0.tgz", + "integrity": "sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w==", + "devOptional": true, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/firebase-tools/node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "optional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/firebase-tools/node_modules/env-variable": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/env-variable/-/env-variable-0.0.6.tgz", + "integrity": "sha512-bHz59NlBbtS0NhftmR8+ExBEekE7br0e01jw+kk0NDro7TtZzBYZ5ScGPs3OmwnpyfHTHOtr1Y6uedCdrIldtg==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/err-code": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", + "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", + "optional": true + }, + "node_modules/firebase-tools/node_modules/escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "devOptional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/firebase-tools/node_modules/escape-goat": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/escape-goat/-/escape-goat-2.1.1.tgz", + "integrity": "sha512-8/uIhbG12Csjy2JEW7D9pHbreaVaS/OpN3ycnyvElTdwM5n6GY6W6e2IPemfvGZeUMqZ9A/3GqIZMgKnBhAw/Q==", + "devOptional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "devOptional": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/firebase-tools/node_modules/escodegen": { + "version": "1.14.3", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.14.3.tgz", + "integrity": "sha512-qFcX0XJkdg+PB3xjZZG/wKSuT1PnQWx57+TVSjIMmILd2yC/6ByYElPwJnslDsuWuSAp4AwJGumarAAmJch5Kw==", + "devOptional": true, + "dependencies": { + "esprima": "^4.0.1", + "estraverse": "^4.2.0", + "esutils": "^2.0.2", + "optionator": "^0.8.1" + }, + "bin": { + "escodegen": "bin/escodegen.js", + "esgenerate": "bin/esgenerate.js" + }, + "engines": { + "node": ">=4.0" + }, + "optionalDependencies": { + "source-map": "~0.6.1" + } + }, + "node_modules/firebase-tools/node_modules/eslint-visitor-keys": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz", + "integrity": "sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA==", + "devOptional": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/firebase-tools/node_modules/espree": { + "version": "9.4.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.4.1.tgz", + "integrity": "sha512-XwctdmTO6SIvCzd9810yyNzIrOrqNYV9Koizx4C/mRhf9uq0o4yHoCEU/670pOxOL/MSraektvSAji79kX90Vg==", + "devOptional": true, + "dependencies": { + "acorn": "^8.8.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/firebase-tools/node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "devOptional": true, + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/firebase-tools/node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "devOptional": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/firebase-tools/node_modules/esutils": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz", + "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=", + "devOptional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/firebase-tools/node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "devOptional": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/firebase-tools/node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "devOptional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/firebase-tools/node_modules/events-listener": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/events-listener/-/events-listener-1.1.0.tgz", + "integrity": "sha512-Kd3EgYfODHueq6GzVfs/VUolh2EgJsS8hkO3KpnDrxVjU3eq63eXM2ujXkhPP+OkeUOhL8CxdfZbQXzryb5C4g==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/exegesis": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/exegesis/-/exegesis-4.1.0.tgz", + "integrity": "sha512-iqc55n+hmv8d1KYNMjq7bCcp4u74oRY6MBcj6Vsux7Wd4mRvlgahKqrBTyLIWwscNjEF3qvPmeJ0RPTj8ORMNg==", + "devOptional": true, + "dependencies": { + "@apidevtools/json-schema-ref-parser": "^9.0.3", + "ajv": "^8.3.0", + "ajv-formats": "^2.1.0", + "body-parser": "^1.18.3", + "content-type": "^1.0.4", + "deep-freeze": "0.0.1", + "events-listener": "^1.1.0", + "glob": "^7.1.3", + "json-ptr": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "lodash": "^4.17.11", + "openapi3-ts": "^2.0.1", + "promise-breaker": "^5.0.0", + "pump": "^3.0.0", + "qs": "^6.6.0", + "raw-body": "^2.3.3", + "semver": "^7.0.0" + }, + "engines": { + "node": ">=6.0.0", + "npm": ">5.0.0" + } + }, + "node_modules/firebase-tools/node_modules/exegesis-express": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/exegesis-express/-/exegesis-express-4.0.0.tgz", + "integrity": "sha512-V2hqwTtYRj0bj43K4MCtm0caD97YWkqOUHFMRCBW5L1x9IjyqOEc7Xa4oQjjiFbeFOSQzzwPV+BzXsQjSz08fw==", + "devOptional": true, + "dependencies": { + "exegesis": "^4.1.0" + }, + "engines": { + "node": ">=6.0.0", + "npm": ">5.0.0" + } + }, + "node_modules/firebase-tools/node_modules/exegesis/node_modules/ajv": { + "version": "8.8.2", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.8.2.tgz", + "integrity": "sha512-x9VuX+R/jcFj1DHo/fCp99esgGDWiHENrKxaCENuCxpoMCmAt/COCGVDwA7kleEpEzJjDnvh3yGoOuLu0Dtllw==", + "devOptional": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/firebase-tools/node_modules/exegesis/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/exegesis/node_modules/semver": { + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", + "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", + "devOptional": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/firebase-tools/node_modules/express": { + "version": "4.18.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz", + "integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==", + "devOptional": true, + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.1", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.5.0", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.2.0", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.7", + "qs": "6.11.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.18.0", + "serve-static": "1.15.0", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/firebase-tools/node_modules/express/node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "devOptional": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/firebase-tools/node_modules/express/node_modules/finalhandler": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", + "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", + "devOptional": true, + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/firebase-tools/node_modules/express/node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "devOptional": true, + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/firebase-tools/node_modules/express/node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "devOptional": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/firebase-tools/node_modules/express/node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "devOptional": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/firebase-tools/node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/external-editor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", + "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", + "devOptional": true, + "dependencies": { + "chardet": "^0.7.0", + "iconv-lite": "^0.4.24", + "tmp": "^0.0.33" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/firebase-tools/node_modules/external-editor/node_modules/tmp": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "devOptional": true, + "dependencies": { + "os-tmpdir": "~1.0.2" + }, + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/firebase-tools/node_modules/extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=", + "devOptional": true, + "engines": [ + "node >=0.6.0" + ] + }, + "node_modules/firebase-tools/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==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/fast-json-stable-stringify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", + "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/fast-safe-stringify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", + "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/fast-text-encoding": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/fast-text-encoding/-/fast-text-encoding-1.0.3.tgz", + "integrity": "sha512-dtm4QZH9nZtcDt8qJiOH9fcQd1NAgi+K1O2DbE6GG1PPCK/BWfOH3idCTRQ4ImXRUOyopDEgDEnVEE7Y/2Wrig==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/fast-url-parser": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/fast-url-parser/-/fast-url-parser-1.1.3.tgz", + "integrity": "sha1-9K8+qfNNiicc9YrSs3WfQx8LMY0=", + "devOptional": true, + "dependencies": { + "punycode": "^1.3.2" + } + }, + "node_modules/firebase-tools/node_modules/fast-url-parser/node_modules/punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/fecha": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fecha/-/fecha-2.3.3.tgz", + "integrity": "sha512-lUGBnIamTAwk4znq5BcqsDaxSmZ9nDVJaij6NvRt/Tg4R69gERA+otPKbS86ROw9nxVMw2/mp1fnaiWqbs6Sdg==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/figures": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", + "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", + "devOptional": true, + "dependencies": { + "escape-string-regexp": "^1.0.5" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/firebase-tools/node_modules/file-uri-to-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-2.0.0.tgz", + "integrity": "sha512-hjPFI8oE/2iQPVe4gbrJ73Pp+Xfub2+WI2LlXDbsaJBwT5wuMh35WNWVYYTpnz895shtwfyutMFLFywpQAFdLg==", + "devOptional": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/firebase-tools/node_modules/filesize": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/filesize/-/filesize-6.1.0.tgz", + "integrity": "sha512-LpCHtPQ3sFx67z+uh2HnSyWSLLu5Jxo21795uRDuar/EOuYWXib5EmPaGIBuSnRqH2IODiKA2k5re/K9OnN/Yg==", + "devOptional": true, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/firebase-tools/node_modules/fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "devOptional": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/finalhandler": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", + "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", + "devOptional": true, + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "statuses": "~1.5.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/firebase-tools/node_modules/forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=", + "devOptional": true, + "engines": { + "node": "*" + } + }, + "node_modules/firebase-tools/node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "devOptional": true, + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/firebase-tools/node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "devOptional": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/firebase-tools/node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=", + "devOptional": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/firebase-tools/node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "devOptional": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/firebase-tools/node_modules/fs-extra/node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "devOptional": true, + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/firebase-tools/node_modules/fs-extra/node_modules/universalify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", + "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", + "devOptional": true, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/firebase-tools/node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "devOptional": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/firebase-tools/node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/firebase-tools/node_modules/ftp": { + "version": "0.3.10", + "resolved": "https://registry.npmjs.org/ftp/-/ftp-0.3.10.tgz", + "integrity": "sha1-kZfYYa2BQvPmPVqDv+TFn3MwiF0=", + "devOptional": true, + "dependencies": { + "readable-stream": "1.1.x", + "xregexp": "2.0.0" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/firebase-tools/node_modules/ftp/node_modules/isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/ftp/node_modules/readable-stream": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", + "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", + "devOptional": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + }, + "node_modules/firebase-tools/node_modules/ftp/node_modules/string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/gauge": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-4.0.4.tgz", + "integrity": "sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg==", + "optional": true, + "dependencies": { + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.3", + "console-control-strings": "^1.1.0", + "has-unicode": "^2.0.1", + "signal-exit": "^3.0.7", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.5" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/firebase-tools/node_modules/gaxios": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-4.1.0.tgz", + "integrity": "sha512-vb0to8xzGnA2qcgywAjtshOKKVDf2eQhJoiL6fHhgW5tVN7wNk7egnYIO9zotfn3lQ3De1VPdf7V5/BWfCtCmg==", + "devOptional": true, + "dependencies": { + "abort-controller": "^3.0.0", + "extend": "^3.0.2", + "https-proxy-agent": "^5.0.0", + "is-stream": "^2.0.0", + "node-fetch": "^2.3.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/firebase-tools/node_modules/gaxios/node_modules/is-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.0.tgz", + "integrity": "sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw==", + "devOptional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/gcp-metadata": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-4.2.0.tgz", + "integrity": "sha512-vQZD57cQkqIA6YPGXM/zc+PIZfNRFdukWGsGZ5+LcJzesi5xp6Gn7a02wRJi4eXPyArNMIYpPET4QMxGqtlk6Q==", + "devOptional": true, + "dependencies": { + "gaxios": "^3.0.0", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/firebase-tools/node_modules/gcp-metadata/node_modules/gaxios": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-3.2.0.tgz", + "integrity": "sha512-+6WPeVzPvOshftpxJwRi2Ozez80tn/hdtOUag7+gajDHRJvAblKxTFSSMPtr2hmnLy7p0mvYz0rMXLBl8pSO7Q==", + "devOptional": true, + "dependencies": { + "abort-controller": "^3.0.0", + "extend": "^3.0.2", + "https-proxy-agent": "^5.0.0", + "is-stream": "^2.0.0", + "node-fetch": "^2.3.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/firebase-tools/node_modules/gcp-metadata/node_modules/is-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.0.tgz", + "integrity": "sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw==", + "devOptional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "devOptional": true, + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/firebase-tools/node_modules/get-intrinsic": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.3.tgz", + "integrity": "sha512-QJVz1Tj7MS099PevUG5jvnt9tSkXN8K14dxQlikJuPt4uD9hHAHjLyLBiLR5zELelBdD9QNRAXZzsJx0WaDL9A==", + "devOptional": true, + "dependencies": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/firebase-tools/node_modules/get-uri": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-3.0.2.tgz", + "integrity": "sha512-+5s0SJbGoyiJTZZ2JTpFPLMPSch72KEqGOTvQsBqg0RBWvwhWUSYZFAtz3TPW0GXJuLBJPts1E241iHg+VRfhg==", + "devOptional": true, + "dependencies": { + "@tootallnate/once": "1", + "data-uri-to-buffer": "3", + "debug": "4", + "file-uri-to-path": "2", + "fs-extra": "^8.1.0", + "ftp": "^0.3.10" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/firebase-tools/node_modules/get-uri/node_modules/debug": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", + "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", + "devOptional": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/firebase-tools/node_modules/get-uri/node_modules/fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "devOptional": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/firebase-tools/node_modules/get-uri/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", + "devOptional": true, + "dependencies": { + "assert-plus": "^1.0.0" + } + }, + "node_modules/firebase-tools/node_modules/glob": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", + "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", + "devOptional": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/firebase-tools/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==", + "devOptional": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/firebase-tools/node_modules/glob-slash": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/glob-slash/-/glob-slash-1.0.0.tgz", + "integrity": "sha1-/lLvpDMjP3Si/mTHq7m8hIICq5U=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/glob-slasher": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/glob-slasher/-/glob-slasher-1.0.1.tgz", + "integrity": "sha1-dHoOW7IiZC7hDT4FRD4QlJPLD44=", + "devOptional": true, + "dependencies": { + "glob-slash": "^1.0.0", + "lodash.isobject": "^2.4.1", + "toxic": "^1.0.0" + } + }, + "node_modules/firebase-tools/node_modules/global-dirs": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-3.0.1.tgz", + "integrity": "sha512-NBcGGFbBA9s1VzD41QXDG+3++t9Mn5t1FpLdhESY6oKY4gYTFpX4wO3sqGUa0Srjtbfj3szX0RnemmrVRUdULA==", + "devOptional": true, + "dependencies": { + "ini": "2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/firebase-tools/node_modules/global-dirs/node_modules/ini": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ini/-/ini-2.0.0.tgz", + "integrity": "sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA==", + "devOptional": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/firebase-tools/node_modules/google-auth-library": { + "version": "7.14.1", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-7.14.1.tgz", + "integrity": "sha512-5Rk7iLNDFhFeBYc3s8l1CqzbEBcdhwR193RlD4vSNFajIcINKI8W8P0JLmBpwymHqqWbX34pJDQu39cSy/6RsA==", + "devOptional": true, + "dependencies": { + "arrify": "^2.0.0", + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "fast-text-encoding": "^1.0.0", + "gaxios": "^4.0.0", + "gcp-metadata": "^4.2.0", + "gtoken": "^5.0.4", + "jws": "^4.0.0", + "lru-cache": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/firebase-tools/node_modules/google-auth-library/node_modules/jwa": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz", + "integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==", + "devOptional": true, + "dependencies": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/firebase-tools/node_modules/google-auth-library/node_modules/jws": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", + "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", + "devOptional": true, + "dependencies": { + "jwa": "^2.0.0", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/firebase-tools/node_modules/google-gax": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/google-gax/-/google-gax-3.5.3.tgz", + "integrity": "sha512-caItkifbcPDVf5xW2j6xLXmuX9bh2dheJN9AzMuQj6VvWacUVV5d0BkjL+Ia8sgX9VvXtbqjf4NKK3nTgV9UUQ==", + "devOptional": true, + "dependencies": { + "@grpc/grpc-js": "~1.7.0", + "@grpc/proto-loader": "^0.7.0", + "@types/long": "^4.0.0", + "abort-controller": "^3.0.0", + "duplexify": "^4.0.0", + "fast-text-encoding": "^1.0.3", + "google-auth-library": "^8.0.2", + "is-stream-ended": "^0.1.4", + "node-fetch": "^2.6.1", + "object-hash": "^3.0.0", + "proto3-json-serializer": "^1.0.0", + "protobufjs": "7.1.2", + "protobufjs-cli": "1.1.0", + "retry-request": "^5.0.0" + }, + "bin": { + "compileProtos": "build/tools/compileProtos.js", + "minifyProtoJson": "build/tools/minify.js" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/firebase-tools/node_modules/google-gax/node_modules/@grpc/proto-loader": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.4.tgz", + "integrity": "sha512-MnWjkGwqQ3W8fx94/c1CwqLsNmHHv2t0CFn+9++6+cDphC1lolpg9M2OU0iebIjK//pBNX9e94ho+gjx6vz39w==", + "devOptional": true, + "dependencies": { + "@types/long": "^4.0.1", + "lodash.camelcase": "^4.3.0", + "long": "^4.0.0", + "protobufjs": "^7.0.0", + "yargs": "^16.2.0" + }, + "bin": { + "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/firebase-tools/node_modules/google-gax/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==", + "devOptional": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/firebase-tools/node_modules/google-gax/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "devOptional": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/firebase-tools/node_modules/google-gax/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "devOptional": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/firebase-tools/node_modules/google-gax/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==", + "devOptional": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/firebase-tools/node_modules/google-gax/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==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/google-gax/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "devOptional": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/firebase-tools/node_modules/google-gax/node_modules/gaxios": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-5.0.2.tgz", + "integrity": "sha512-TjtV2AJOZoMQqRYoy5eM8cCQogYwazWNYLQ72QB0kwa6vHHruYkGmhhyrlzbmgNHK1dNnuP2WSH81urfzyN2Og==", + "devOptional": true, + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^5.0.0", + "is-stream": "^2.0.0", + "node-fetch": "^2.6.7" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/firebase-tools/node_modules/google-gax/node_modules/gcp-metadata": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-5.2.0.tgz", + "integrity": "sha512-aFhhvvNycky2QyhG+dcfEdHBF0FRbYcf39s6WNHUDysKSrbJ5vuFbjydxBcmewtXeV248GP8dWT3ByPNxsyHCw==", + "devOptional": true, + "dependencies": { + "gaxios": "^5.0.0", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/firebase-tools/node_modules/google-gax/node_modules/glob": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "devOptional": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/firebase-tools/node_modules/google-gax/node_modules/google-auth-library": { + "version": "8.7.0", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-8.7.0.tgz", + "integrity": "sha512-1M0NG5VDIvJZEnstHbRdckLZESoJwguinwN8Dhae0j2ZKIQFIV63zxm6Fo6nM4xkgqUr2bbMtV5Dgo+Hy6oo0Q==", + "devOptional": true, + "dependencies": { + "arrify": "^2.0.0", + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "fast-text-encoding": "^1.0.0", + "gaxios": "^5.0.0", + "gcp-metadata": "^5.0.0", + "gtoken": "^6.1.0", + "jws": "^4.0.0", + "lru-cache": "^6.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/firebase-tools/node_modules/google-gax/node_modules/google-p12-pem": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/google-p12-pem/-/google-p12-pem-4.0.1.tgz", + "integrity": "sha512-WPkN4yGtz05WZ5EhtlxNDWPhC4JIic6G8ePitwUWy4l+XPVYec+a0j0Ts47PDtW59y3RwAhUd9/h9ZZ63px6RQ==", + "devOptional": true, + "dependencies": { + "node-forge": "^1.3.1" + }, + "bin": { + "gp12-pem": "build/src/bin/gp12-pem.js" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/firebase-tools/node_modules/google-gax/node_modules/gtoken": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-6.1.2.tgz", + "integrity": "sha512-4ccGpzz7YAr7lxrT2neugmXQ3hP9ho2gcaityLVkiUecAiwiy60Ii8gRbZeOsXV19fYaRjgBSshs8kXw+NKCPQ==", + "devOptional": true, + "dependencies": { + "gaxios": "^5.0.1", + "google-p12-pem": "^4.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/firebase-tools/node_modules/google-gax/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==", + "devOptional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/google-gax/node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "devOptional": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/firebase-tools/node_modules/google-gax/node_modules/jwa": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz", + "integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==", + "devOptional": true, + "dependencies": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/firebase-tools/node_modules/google-gax/node_modules/jws": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", + "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", + "devOptional": true, + "dependencies": { + "jwa": "^2.0.0", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/firebase-tools/node_modules/google-gax/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "devOptional": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/firebase-tools/node_modules/google-gax/node_modules/protobufjs": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.1.2.tgz", + "integrity": "sha512-4ZPTPkXCdel3+L81yw3dG6+Kq3umdWKh7Dc7GW/CpNk4SX3hK58iPCWeCyhVTDrbkNeKrYNZ7EojM5WDaEWTLQ==", + "devOptional": true, + "hasInstallScript": true, + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/node": ">=13.7.0", + "long": "^5.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/firebase-tools/node_modules/google-gax/node_modules/protobufjs-cli": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/protobufjs-cli/-/protobufjs-cli-1.1.0.tgz", + "integrity": "sha512-VXMQn+z3yG2WbN2E+mx5vcyIHF7yJSg2jqyqfxcZLWNOSTqUzSSgAE5vu04/JEpwxTI04JGyrZRDHC36wr04uw==", + "devOptional": true, + "dependencies": { + "chalk": "^4.0.0", + "escodegen": "^1.13.0", + "espree": "^9.0.0", + "estraverse": "^5.1.0", + "glob": "^8.0.0", + "jsdoc": "^4.0.0", + "minimist": "^1.2.0", + "semver": "^7.1.2", + "tmp": "^0.2.1", + "uglify-js": "^3.7.7" + }, + "bin": { + "pbjs": "bin/pbjs", + "pbts": "bin/pbts" + }, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "protobufjs": "^7.0.0" + } + }, + "node_modules/firebase-tools/node_modules/google-gax/node_modules/protobufjs/node_modules/long": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/long/-/long-5.2.1.tgz", + "integrity": "sha512-GKSNGeNAtw8IryjjkhZxuKB3JzlcLTwjtiQCHKvqQet81I93kXslhDQruGI/QsddO83mcDToBVy7GqGS/zYf/A==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/google-gax/node_modules/semver": { + "version": "7.3.8", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", + "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", + "devOptional": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/firebase-tools/node_modules/google-gax/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==", + "devOptional": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/google-p12-pem": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/google-p12-pem/-/google-p12-pem-3.1.3.tgz", + "integrity": "sha512-MC0jISvzymxePDVembypNefkAQp+DRP7dBE+zNUPaIjEspIlYg0++OrsNr248V9tPbz6iqtZ7rX1hxWA5B8qBQ==", + "devOptional": true, + "dependencies": { + "node-forge": "^1.0.0" + }, + "bin": { + "gp12-pem": "build/src/bin/gp12-pem.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/firebase-tools/node_modules/graceful-fs": { + "version": "4.2.10", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", + "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/gtoken": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-5.3.2.tgz", + "integrity": "sha512-gkvEKREW7dXWF8NV8pVrKfW7WqReAmjjkMBh6lNCCGOM4ucS0r0YyXXl0r/9Yj8wcW/32ISkfc8h5mPTDbtifQ==", + "devOptional": true, + "dependencies": { + "gaxios": "^4.0.0", + "google-p12-pem": "^3.1.3", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/firebase-tools/node_modules/gtoken/node_modules/jwa": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz", + "integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==", + "devOptional": true, + "dependencies": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/firebase-tools/node_modules/gtoken/node_modules/jws": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", + "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", + "devOptional": true, + "dependencies": { + "jwa": "^2.0.0", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/firebase-tools/node_modules/har-schema": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", + "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=", + "devOptional": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/firebase-tools/node_modules/har-validator": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.3.tgz", + "integrity": "sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==", + "deprecated": "this library is no longer supported", + "devOptional": true, + "dependencies": { + "ajv": "^6.5.5", + "har-schema": "^2.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/firebase-tools/node_modules/has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "devOptional": true, + "dependencies": { + "function-bind": "^1.1.1" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/firebase-tools/node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "devOptional": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/firebase-tools/node_modules/has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==", + "optional": true + }, + "node_modules/firebase-tools/node_modules/has-yarn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/has-yarn/-/has-yarn-2.1.0.tgz", + "integrity": "sha512-UqBRqi4ju7T+TqGNdqAO0PaSVGsDGJUBQvk9eUWNGRY1CFGDzYhLWoM7JQEemnlvVcv/YEmc2wNW8BC24EnUsw==", + "devOptional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/http-cache-semantics": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", + "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==", + "optional": true + }, + "node_modules/firebase-tools/node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "devOptional": true, + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/firebase-tools/node_modules/http-errors/node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "devOptional": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/firebase-tools/node_modules/http-errors/node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "devOptional": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/firebase-tools/node_modules/http-proxy-agent": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz", + "integrity": "sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==", + "devOptional": true, + "dependencies": { + "@tootallnate/once": "1", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/firebase-tools/node_modules/http-proxy-agent/node_modules/debug": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "devOptional": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/firebase-tools/node_modules/http-proxy-agent/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/http-signature": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", + "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", + "devOptional": true, + "dependencies": { + "assert-plus": "^1.0.0", + "jsprim": "^1.2.2", + "sshpk": "^1.7.0" + }, + "engines": { + "node": ">=0.8", + "npm": ">=1.3.7" + } + }, + "node_modules/firebase-tools/node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "devOptional": true, + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/firebase-tools/node_modules/https-proxy-agent/node_modules/debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "deprecated": "Debug versions >=3.2.0 <3.2.7 || >=4 <4.3.1 have a low-severity ReDos regression when used in a Node.js environment. It is recommended you upgrade to 3.2.7 or 4.3.1. (https://github.com/visionmedia/debug/issues/797)", + "devOptional": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/firebase-tools/node_modules/https-proxy-agent/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/humanize-ms": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", + "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", + "optional": true, + "dependencies": { + "ms": "^2.0.0" + } + }, + "node_modules/firebase-tools/node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "devOptional": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/firebase-tools/node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "devOptional": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/firebase-tools/node_modules/import-lazy": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/import-lazy/-/import-lazy-2.1.0.tgz", + "integrity": "sha1-BWmOPUXIjo1+nZLLBYTnfwlvPkM=", + "devOptional": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/firebase-tools/node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", + "devOptional": true, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/firebase-tools/node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/infer-owner": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/infer-owner/-/infer-owner-1.0.4.tgz", + "integrity": "sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==", + "optional": true + }, + "node_modules/firebase-tools/node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "devOptional": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/firebase-tools/node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/inquirer": { + "version": "8.2.5", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-8.2.5.tgz", + "integrity": "sha512-QAgPDQMEgrDssk1XiwwHoOGYF9BAbUcc1+j+FhEvaOt8/cKRqyLn0U5qA6F74fGhTMGxf92pOvPBeh29jQJDTQ==", + "devOptional": true, + "dependencies": { + "ansi-escapes": "^4.2.1", + "chalk": "^4.1.1", + "cli-cursor": "^3.1.0", + "cli-width": "^3.0.0", + "external-editor": "^3.0.3", + "figures": "^3.0.0", + "lodash": "^4.17.21", + "mute-stream": "0.0.8", + "ora": "^5.4.1", + "run-async": "^2.4.0", + "rxjs": "^7.5.5", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0", + "through": "^2.3.6", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/firebase-tools/node_modules/inquirer/node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "devOptional": true, + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/firebase-tools/node_modules/inquirer/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "devOptional": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/firebase-tools/node_modules/inquirer/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "devOptional": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/firebase-tools/node_modules/inquirer/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "devOptional": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/firebase-tools/node_modules/inquirer/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/inquirer/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "devOptional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/inquirer/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "devOptional": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/inquirer/node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "devOptional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/firebase-tools/node_modules/install-artifact-from-github": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/install-artifact-from-github/-/install-artifact-from-github-1.3.1.tgz", + "integrity": "sha512-3l3Bymg2eKDsN5wQuMfgGEj2x6l5MCAv0zPL6rxHESufFVlEAKW/6oY9F1aGgvY/EgWm5+eWGRjINveL4X7Hgg==", + "optional": true, + "bin": { + "install-from-cache": "bin/install-from-cache.js", + "save-to-github-cache": "bin/save-to-github-cache.js" + } + }, + "node_modules/firebase-tools/node_modules/ip": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.5.tgz", + "integrity": "sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/ip-regex": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-4.3.0.tgz", + "integrity": "sha512-B9ZWJxHHOHUhUjCPrMpLD4xEq35bUTClHM1S6CBU5ixQnkZmwipwgc96vAd7AAGM9TGHvJR+Uss+/Ak6UphK+Q==", + "devOptional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "devOptional": true, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/firebase-tools/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==", + "devOptional": true, + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/is-ci": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-2.0.0.tgz", + "integrity": "sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w==", + "devOptional": true, + "dependencies": { + "ci-info": "^2.0.0" + }, + "bin": { + "is-ci": "bin.js" + } + }, + "node_modules/firebase-tools/node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", + "devOptional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/firebase-tools/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "devOptional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/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==", + "devOptional": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/firebase-tools/node_modules/is-installed-globally": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.4.0.tgz", + "integrity": "sha512-iwGqO3J21aaSkC7jWnHP/difazwS7SFeIqxv6wEtLU8Y5KlzFTjyqcSIT0d8s4+dDhKytsk9PJZ2BkS5eZwQRQ==", + "devOptional": true, + "dependencies": { + "global-dirs": "^3.0.0", + "is-path-inside": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/firebase-tools/node_modules/is-interactive": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", + "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", + "devOptional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/is-lambda": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-lambda/-/is-lambda-1.0.1.tgz", + "integrity": "sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==", + "optional": true + }, + "node_modules/firebase-tools/node_modules/is-npm": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-npm/-/is-npm-5.0.0.tgz", + "integrity": "sha512-WW/rQLOazUq+ST/bCAVBp/2oMERWLsR7OrKyt052dNDk4DHcDE0/7QSXITlmi+VBcV13DfIbysG3tZJm5RfdBA==", + "devOptional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/firebase-tools/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==", + "devOptional": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/firebase-tools/node_modules/is-obj": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", + "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==", + "devOptional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/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==", + "devOptional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=", + "devOptional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/firebase-tools/node_modules/is-stream-ended": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-stream-ended/-/is-stream-ended-0.1.4.tgz", + "integrity": "sha512-xj0XPvmr7bQFTvirqnFr50o0hQIh6ZItDqloxt5aJrR4NQsYeSsyFQERYGCAzfindAcnKjINnwEEgLx4IqVzQw==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "devOptional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/firebase-tools/node_modules/is-url": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/is-url/-/is-url-1.2.4.tgz", + "integrity": "sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/is-windows": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", + "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", + "devOptional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/firebase-tools/node_modules/is-wsl": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-1.1.0.tgz", + "integrity": "sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0=", + "devOptional": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/firebase-tools/node_modules/is-yarn-global": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/is-yarn-global/-/is-yarn-global-0.3.0.tgz", + "integrity": "sha512-VjSeb/lHmkoyd8ryPVIKvOCn4D1koMqY+vqyjjUfc3xyKtP4dYOxM44sZrnqQSzSds3xyOrUTLTC9LVCVgLngw==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/is2": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/is2/-/is2-2.0.7.tgz", + "integrity": "sha512-4vBQoURAXC6hnLFxD4VW7uc04XiwTTl/8ydYJxKvPwkWQrSjInkuM5VZVg6BGr1/natq69zDuvO9lGpLClJqvA==", + "devOptional": true, + "dependencies": { + "deep-is": "^0.1.3", + "ip-regex": "^4.1.0", + "is-url": "^1.2.4" + }, + "engines": { + "node": ">=v0.10.0" + } + }, + "node_modules/firebase-tools/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/isomorphic-fetch": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-3.0.0.tgz", + "integrity": "sha512-qvUtwJ3j6qwsF3jLxkZ72qCgjMysPzDfeV240JHiGZsANBYd+EEuu35v7dfrJ9Up0Ak07D7GGSkGhCHTqg/5wA==", + "devOptional": true, + "dependencies": { + "node-fetch": "^2.6.1", + "whatwg-fetch": "^3.4.1" + } + }, + "node_modules/firebase-tools/node_modules/isomorphic-fetch/node_modules/whatwg-fetch": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.2.tgz", + "integrity": "sha512-bJlen0FcuU/0EMLrdbJ7zOnW6ITZLrZMIarMUVmdKtsGvZna8vxKYaexICWPfZ8qwf9fzNq+UEIZrnSaApt6RA==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/jju": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/jju/-/jju-1.4.0.tgz", + "integrity": "sha1-o6vicYryQaKykE+EpiWXDzia4yo=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/join-path": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/join-path/-/join-path-1.1.1.tgz", + "integrity": "sha1-EFNaEm0ky9Zff/zfFe8uYxB2tQU=", + "devOptional": true, + "dependencies": { + "as-array": "^2.0.0", + "url-join": "0.0.1", + "valid-url": "^1" + } + }, + "node_modules/firebase-tools/node_modules/js-yaml": { + "version": "3.13.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz", + "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==", + "devOptional": true, + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/firebase-tools/node_modules/js2xmlparser": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/js2xmlparser/-/js2xmlparser-4.0.2.tgz", + "integrity": "sha512-6n4D8gLlLf1n5mNLQPRfViYzu9RATblzPEtm1SthMX1Pjao0r9YI9nw7ZIfRxQMERS87mcswrg+r/OYrPRX6jA==", + "devOptional": true, + "dependencies": { + "xmlcreate": "^2.0.4" + } + }, + "node_modules/firebase-tools/node_modules/jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/jsdoc": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsdoc/-/jsdoc-4.0.0.tgz", + "integrity": "sha512-tzTgkklbWKrlaQL2+e3NNgLcZu3NaK2vsHRx7tyHQ+H5jcB9Gx0txSd2eJWlMC/xU1+7LQu4s58Ry0RkuaEQVg==", + "devOptional": true, + "dependencies": { + "@babel/parser": "^7.9.4", + "@jsdoc/salty": "^0.2.1", + "@types/markdown-it": "^12.2.3", + "bluebird": "^3.7.2", + "catharsis": "^0.9.0", + "escape-string-regexp": "^2.0.0", + "js2xmlparser": "^4.0.2", + "klaw": "^3.0.0", + "markdown-it": "^12.3.2", + "markdown-it-anchor": "^8.4.1", + "marked": "^4.0.10", + "mkdirp": "^1.0.4", + "requizzle": "^0.2.3", + "strip-json-comments": "^3.1.0", + "underscore": "~1.13.2" + }, + "bin": { + "jsdoc": "jsdoc.js" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/firebase-tools/node_modules/jsdoc/node_modules/bluebird": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/jsdoc/node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "devOptional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/jsdoc/node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "devOptional": true, + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/firebase-tools/node_modules/jsdoc/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==", + "devOptional": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/firebase-tools/node_modules/json-bigint": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", + "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", + "devOptional": true, + "dependencies": { + "bignumber.js": "^9.0.0" + } + }, + "node_modules/firebase-tools/node_modules/json-parse-helpfulerror": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/json-parse-helpfulerror/-/json-parse-helpfulerror-1.0.3.tgz", + "integrity": "sha1-E/FM4C7tTpgSl7ZOueO5MuLdE9w=", + "devOptional": true, + "dependencies": { + "jju": "^1.1.0" + } + }, + "node_modules/firebase-tools/node_modules/json-ptr": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-ptr/-/json-ptr-3.0.1.tgz", + "integrity": "sha512-hrZ4tElT8huJUH3OwOK+d7F8PRqw09QnGM3Mm3GmqKWDyCCPCG8lGHxXOwQAj0VOxzLirOds07Kz10B5F8M8EA==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/json-schema": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", + "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==", + "devOptional": true + }, + "node_modules/firebase-tools/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==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", + "devOptional": true, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/firebase-tools/node_modules/jsonwebtoken": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.0.tgz", + "integrity": "sha512-tuGfYXxkQGDPnLJ7SibiQgVgeDgfbPq2k2ICcbgqW8WxWLBAxKQM/ZCu/IT8SOSwmaYl4dpTFCW5xZv7YbbWUw==", + "devOptional": true, + "dependencies": { + "jws": "^3.2.2", + "lodash": "^4.17.21", + "ms": "^2.1.1", + "semver": "^7.3.8" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/firebase-tools/node_modules/jsonwebtoken/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==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/jsonwebtoken/node_modules/semver": { + "version": "7.3.8", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", + "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", + "devOptional": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/firebase-tools/node_modules/jsprim": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.2.tgz", + "integrity": "sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw==", + "devOptional": true, + "dependencies": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.4.0", + "verror": "1.10.0" + }, + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/firebase-tools/node_modules/jwa": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", + "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", + "devOptional": true, + "dependencies": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/firebase-tools/node_modules/jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "devOptional": true, + "dependencies": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/firebase-tools/node_modules/klaw": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/klaw/-/klaw-3.0.0.tgz", + "integrity": "sha512-0Fo5oir+O9jnXu5EefYbVK+mHMBeEVEy2cmctR1O1NECcCkPRreJKrS6Qt/j3KC2C148Dfo9i3pCmCMsdqGr0g==", + "devOptional": true, + "dependencies": { + "graceful-fs": "^4.1.9" + } + }, + "node_modules/firebase-tools/node_modules/kuler": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/kuler/-/kuler-1.0.1.tgz", + "integrity": "sha512-J9nVUucG1p/skKul6DU3PUZrhs0LPulNaeUOox0IyXDi8S4CztTHs1gQphhuZmzXG7VOQSf6NJfKuzteQLv9gQ==", + "devOptional": true, + "dependencies": { + "colornames": "^1.1.1" + } + }, + "node_modules/firebase-tools/node_modules/lazystream": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz", + "integrity": "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==", + "devOptional": true, + "dependencies": { + "readable-stream": "^2.0.5" + }, + "engines": { + "node": ">= 0.6.3" + } + }, + "node_modules/firebase-tools/node_modules/lazystream/node_modules/readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "devOptional": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/firebase-tools/node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "devOptional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/firebase-tools/node_modules/levn": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", + "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", + "devOptional": true, + "dependencies": { + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/firebase-tools/node_modules/libsodium": { + "version": "0.7.10", + "resolved": "https://registry.npmjs.org/libsodium/-/libsodium-0.7.10.tgz", + "integrity": "sha512-eY+z7hDrDKxkAK+QKZVNv92A5KYkxfvIshtBJkmg5TSiCnYqZP3i9OO9whE79Pwgm4jGaoHgkM4ao/b9Cyu4zQ==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/libsodium-wrappers": { + "version": "0.7.10", + "resolved": "https://registry.npmjs.org/libsodium-wrappers/-/libsodium-wrappers-0.7.10.tgz", + "integrity": "sha512-pO3F1Q9NPLB/MWIhehim42b/Fwb30JNScCNh8TcQ/kIc+qGLQch8ag8wb0keK3EP5kbGakk1H8Wwo7v+36rNQg==", + "devOptional": true, + "dependencies": { + "libsodium": "^0.7.0" + } + }, + "node_modules/firebase-tools/node_modules/linkify-it": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-3.0.3.tgz", + "integrity": "sha512-ynTsyrFSdE5oZ/O9GEf00kPngmOfVwazR5GKDq6EYfhlpFug3J2zybX56a2PRRpc9P+FuSoGNAwjlbDs9jJBPQ==", + "devOptional": true, + "dependencies": { + "uc.micro": "^1.0.1" + } + }, + "node_modules/firebase-tools/node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/lodash._objecttypes": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/lodash._objecttypes/-/lodash._objecttypes-2.4.1.tgz", + "integrity": "sha1-fAt/admKH3ZSn4kLDNsbTf7BHBE=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha1-soqmKIorn8ZRA1x3EfZathkDMaY=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/lodash.defaults": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", + "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/lodash.difference": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.difference/-/lodash.difference-4.5.0.tgz", + "integrity": "sha512-dS2j+W26TQ7taQBGN8Lbbq04ssV3emRw4NY58WErlTO29pIqS0HmoT5aJ9+TUQ1N3G+JOZSji4eugsWwGp9yPA==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/lodash.flatten": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz", + "integrity": "sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/lodash.isobject": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/lodash.isobject/-/lodash.isobject-2.4.1.tgz", + "integrity": "sha1-Wi5H/mmVPx7mMafrof5k0tBlWPU=", + "devOptional": true, + "dependencies": { + "lodash._objecttypes": "~2.4.1" + } + }, + "node_modules/firebase-tools/node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/lodash.snakecase": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.snakecase/-/lodash.snakecase-4.1.1.tgz", + "integrity": "sha1-OdcUo1NXFHg3rv1ktdy7Fr7Nj40=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/lodash.union": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.union/-/lodash.union-4.6.0.tgz", + "integrity": "sha512-c4pB2CdGrGdjMKYLA+XiRDO7Y0PRQbm/Gzg8qMj+QH+pFVAoTp5sBpO0odL3FjoPCGjK96p6qsP+yQoiLoOBcw==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "devOptional": true, + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/firebase-tools/node_modules/log-symbols/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "devOptional": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/firebase-tools/node_modules/log-symbols/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "devOptional": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/firebase-tools/node_modules/log-symbols/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "devOptional": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/firebase-tools/node_modules/log-symbols/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/log-symbols/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "devOptional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/log-symbols/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "devOptional": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/logform": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/logform/-/logform-2.1.2.tgz", + "integrity": "sha512-+lZh4OpERDBLqjiwDLpAWNQu6KMjnlXH2ByZwCuSqVPJletw0kTWJf5CgSNAUKn1KUkv3m2cUz/LK8zyEy7wzQ==", + "devOptional": true, + "dependencies": { + "colors": "^1.2.1", + "fast-safe-stringify": "^2.0.4", + "fecha": "^2.3.3", + "ms": "^2.1.1", + "triple-beam": "^1.3.0" + } + }, + "node_modules/firebase-tools/node_modules/logform/node_modules/colors": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz", + "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==", + "devOptional": true, + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/firebase-tools/node_modules/logform/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/long": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", + "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "devOptional": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/firebase-tools/node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "devOptional": true, + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/firebase-tools/node_modules/make-dir/node_modules/semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "devOptional": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/firebase-tools/node_modules/make-fetch-happen": { + "version": "10.2.1", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-10.2.1.tgz", + "integrity": "sha512-NgOPbRiaQM10DYXvN3/hhGVI2M5MtITFryzBGxHM5p4wnFxsVCbxkrBrDsk+EZ5OB4jEOT7AjDxtdF+KVEFT7w==", + "optional": true, + "dependencies": { + "agentkeepalive": "^4.2.1", + "cacache": "^16.1.0", + "http-cache-semantics": "^4.1.0", + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.0", + "is-lambda": "^1.0.1", + "lru-cache": "^7.7.1", + "minipass": "^3.1.6", + "minipass-collect": "^1.0.2", + "minipass-fetch": "^2.0.3", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^0.6.3", + "promise-retry": "^2.0.1", + "socks-proxy-agent": "^7.0.0", + "ssri": "^9.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/firebase-tools/node_modules/make-fetch-happen/node_modules/@tootallnate/once": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", + "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", + "optional": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/firebase-tools/node_modules/make-fetch-happen/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "optional": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/firebase-tools/node_modules/make-fetch-happen/node_modules/http-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", + "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", + "optional": true, + "dependencies": { + "@tootallnate/once": "2", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/firebase-tools/node_modules/make-fetch-happen/node_modules/lru-cache": { + "version": "7.14.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.14.0.tgz", + "integrity": "sha512-EIRtP1GrSJny0dqb50QXRUNBxHJhcpxHC++M5tD7RYbvLLn5KVWKsbyswSSqDuU15UFi3bgTQIY8nhDMeF6aDQ==", + "optional": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/firebase-tools/node_modules/make-fetch-happen/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "optional": true + }, + "node_modules/firebase-tools/node_modules/make-fetch-happen/node_modules/socks-proxy-agent": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-7.0.0.tgz", + "integrity": "sha512-Fgl0YPZ902wEsAyiQ+idGd1A7rSFx/ayC1CQVMw5P+EQx2V0SgpGtf6OKFhVjPflPUl9YMmEOnmfjCdMUsygww==", + "optional": true, + "dependencies": { + "agent-base": "^6.0.2", + "debug": "^4.3.3", + "socks": "^2.6.2" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/firebase-tools/node_modules/markdown-it": { + "version": "12.3.2", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-12.3.2.tgz", + "integrity": "sha512-TchMembfxfNVpHkbtriWltGWc+m3xszaRD0CZup7GFFhzIgQqxIfn3eGj1yZpfuflzPvfkt611B2Q/Bsk1YnGg==", + "devOptional": true, + "dependencies": { + "argparse": "^2.0.1", + "entities": "~2.1.0", + "linkify-it": "^3.0.1", + "mdurl": "^1.0.1", + "uc.micro": "^1.0.5" + }, + "bin": { + "markdown-it": "bin/markdown-it.js" + } + }, + "node_modules/firebase-tools/node_modules/markdown-it-anchor": { + "version": "8.6.6", + "resolved": "https://registry.npmjs.org/markdown-it-anchor/-/markdown-it-anchor-8.6.6.tgz", + "integrity": "sha512-jRW30YGywD2ESXDc+l17AiritL0uVaSnWsb26f+68qaW9zgbIIr1f4v2Nsvc0+s0Z2N3uX6t/yAw7BwCQ1wMsA==", + "devOptional": true, + "peerDependencies": { + "@types/markdown-it": "*", + "markdown-it": "*" + } + }, + "node_modules/firebase-tools/node_modules/markdown-it/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==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/marked": { + "version": "4.2.12", + "resolved": "https://registry.npmjs.org/marked/-/marked-4.2.12.tgz", + "integrity": "sha512-yr8hSKa3Fv4D3jdZmtMMPghgVt6TWbk86WQaWhDloQjRSQhMMYCAro7jP7VDJrjjdV8pxVxMssXS8B8Y5DZ5aw==", + "devOptional": true, + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 12" + } + }, + "node_modules/firebase-tools/node_modules/marked-terminal": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/marked-terminal/-/marked-terminal-5.1.1.tgz", + "integrity": "sha512-+cKTOx9P4l7HwINYhzbrBSyzgxO2HaHKGZGuB1orZsMIgXYaJyfidT81VXRdpelW/PcHEWxywscePVgI/oUF6g==", + "devOptional": true, + "dependencies": { + "ansi-escapes": "^5.0.0", + "cardinal": "^2.1.1", + "chalk": "^5.0.0", + "cli-table3": "^0.6.1", + "node-emoji": "^1.11.0", + "supports-hyperlinks": "^2.2.0" + }, + "engines": { + "node": ">=14.13.1 || >=16.0.0" + }, + "peerDependencies": { + "marked": "^1.0.0 || ^2.0.0 || ^3.0.0 || ^4.0.0" + } + }, + "node_modules/firebase-tools/node_modules/marked-terminal/node_modules/chalk": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.0.1.tgz", + "integrity": "sha512-Fo07WOYGqMfCWHOzSXOt2CxDbC6skS/jO9ynEcmpANMoPrD+W1r1K6Vx7iNm+AQmETU1Xr2t+n8nzkV9t6xh3w==", + "devOptional": true, + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/firebase-tools/node_modules/mdurl": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz", + "integrity": "sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=", + "devOptional": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/firebase-tools/node_modules/merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=", + "devOptional": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/firebase-tools/node_modules/mime": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.5.2.tgz", + "integrity": "sha512-tqkh47FzKeCPD2PUiPB6pkbMzsCasjxAfC62/Wap5qrUWcb+sFasXUC5I3gYM5iBM8v/Qpn4UK0x+j0iHyFPDg==", + "devOptional": true, + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/firebase-tools/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "devOptional": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/firebase-tools/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "devOptional": true, + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/firebase-tools/node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "devOptional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/firebase-tools/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "devOptional": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/firebase-tools/node_modules/minimist": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", + "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/minipass": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.1.6.tgz", + "integrity": "sha512-rty5kpw9/z8SX9dmxblFA6edItUmwJgMeYDZRrwlIVN27i8gysGbznJwUggw2V/FVqFSDdWy040ZPS811DYAqQ==", + "devOptional": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/minipass-collect": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-1.0.2.tgz", + "integrity": "sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==", + "optional": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/firebase-tools/node_modules/minipass-fetch": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-2.1.2.tgz", + "integrity": "sha512-LT49Zi2/WMROHYoqGgdlQIZh8mLPZmOrN2NdJjMXxYe4nkN6FUyuPuOAOedNJDrx0IRGg9+4guZewtp8hE6TxA==", + "optional": true, + "dependencies": { + "minipass": "^3.1.6", + "minipass-sized": "^1.0.3", + "minizlib": "^2.1.2" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + }, + "optionalDependencies": { + "encoding": "^0.1.13" + } + }, + "node_modules/firebase-tools/node_modules/minipass-flush": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz", + "integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==", + "optional": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/firebase-tools/node_modules/minipass-pipeline": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", + "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", + "optional": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/minipass-sized": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz", + "integrity": "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==", + "optional": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "devOptional": true, + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/firebase-tools/node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "devOptional": true, + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/firebase-tools/node_modules/morgan": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.0.tgz", + "integrity": "sha512-AbegBVI4sh6El+1gNwvD5YIck7nSA36weD7xvIxG4in80j/UoK8AEGaWnnz8v1GxonMCltmlNs5ZKbGvl9b1XQ==", + "devOptional": true, + "dependencies": { + "basic-auth": "~2.0.1", + "debug": "2.6.9", + "depd": "~2.0.0", + "on-finished": "~2.3.0", + "on-headers": "~1.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/firebase-tools/node_modules/morgan/node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "devOptional": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/firebase-tools/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/mute-stream": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", + "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/nan": { + "version": "2.16.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.16.0.tgz", + "integrity": "sha512-UdAqHyFngu7TfQKsCBgAA6pWDkT8MAO7d0jyOecVhN5354xbLqdn8mV9Tat9gepAupm0bt2DbeaSC8vS52MuFA==", + "optional": true + }, + "node_modules/firebase-tools/node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "devOptional": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/firebase-tools/node_modules/netmask": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.0.2.tgz", + "integrity": "sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==", + "devOptional": true, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/firebase-tools/node_modules/nice-try": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", + "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/node-emoji": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-1.11.0.tgz", + "integrity": "sha512-wo2DpQkQp7Sjm2A0cq+sN7EHKO6Sl0ctXeBdFZrL9T9+UywORbufTcTZxom8YqpLQt/FqNMUkOpkZrJVYSKD3A==", + "devOptional": true, + "dependencies": { + "lodash": "^4.17.21" + } + }, + "node_modules/firebase-tools/node_modules/node-fetch": { + "version": "2.6.7", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", + "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", + "devOptional": true, + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/firebase-tools/node_modules/node-forge": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", + "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==", + "devOptional": true, + "engines": { + "node": ">= 6.13.0" + } + }, + "node_modules/firebase-tools/node_modules/node-gyp": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-9.1.0.tgz", + "integrity": "sha512-HkmN0ZpQJU7FLbJauJTHkHlSVAXlNGDAzH/VYFZGDOnFyn/Na3GlNJfkudmufOdS6/jNFhy88ObzL7ERz9es1g==", + "optional": true, + "dependencies": { + "env-paths": "^2.2.0", + "glob": "^7.1.4", + "graceful-fs": "^4.2.6", + "make-fetch-happen": "^10.0.3", + "nopt": "^5.0.0", + "npmlog": "^6.0.0", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "tar": "^6.1.2", + "which": "^2.0.2" + }, + "bin": { + "node-gyp": "bin/node-gyp.js" + }, + "engines": { + "node": "^12.22 || ^14.13 || >=16" + } + }, + "node_modules/firebase-tools/node_modules/node-gyp/node_modules/semver": { + "version": "7.3.7", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz", + "integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==", + "optional": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/firebase-tools/node_modules/node-gyp/node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "optional": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/firebase-tools/node_modules/nopt": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", + "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", + "optional": true, + "dependencies": { + "abbrev": "1" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/firebase-tools/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==", + "devOptional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/firebase-tools/node_modules/npmlog": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-6.0.2.tgz", + "integrity": "sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg==", + "optional": true, + "dependencies": { + "are-we-there-yet": "^3.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^4.0.3", + "set-blocking": "^2.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/firebase-tools/node_modules/oauth-sign": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", + "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", + "devOptional": true, + "engines": { + "node": "*" + } + }, + "node_modules/firebase-tools/node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", + "devOptional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/firebase-tools/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==", + "devOptional": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/firebase-tools/node_modules/object-inspect": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.2.tgz", + "integrity": "sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==", + "devOptional": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/firebase-tools/node_modules/on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", + "devOptional": true, + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/firebase-tools/node_modules/on-headers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", + "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", + "devOptional": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/firebase-tools/node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "devOptional": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/firebase-tools/node_modules/one-time": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/one-time/-/one-time-0.0.4.tgz", + "integrity": "sha1-+M33eISCb+Tf+T46nMN7HkSAdC4=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "devOptional": true, + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/firebase-tools/node_modules/open": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/open/-/open-6.4.0.tgz", + "integrity": "sha512-IFenVPgF70fSm1keSd2iDBIDIBZkroLeuffXq+wKTzTJlBpesFWojV9lb8mzOfaAzM1sr7HQHuO0vtV0zYekGg==", + "devOptional": true, + "dependencies": { + "is-wsl": "^1.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/openapi3-ts": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/openapi3-ts/-/openapi3-ts-2.0.1.tgz", + "integrity": "sha512-v6X3iwddhi276siej96jHGIqTx3wzVfMTmpGJEQDt7GPI7pI6sywItURLzpEci21SBRpPN/aOWSF5mVfFVNmcg==", + "devOptional": true, + "dependencies": { + "yaml": "^1.10.0" + } + }, + "node_modules/firebase-tools/node_modules/optionator": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", + "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", + "devOptional": true, + "dependencies": { + "deep-is": "~0.1.3", + "fast-levenshtein": "~2.0.6", + "levn": "~0.3.0", + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2", + "word-wrap": "~1.2.3" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/firebase-tools/node_modules/ora": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", + "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", + "devOptional": true, + "dependencies": { + "bl": "^4.1.0", + "chalk": "^4.1.0", + "cli-cursor": "^3.1.0", + "cli-spinners": "^2.5.0", + "is-interactive": "^1.0.0", + "is-unicode-supported": "^0.1.0", + "log-symbols": "^4.1.0", + "strip-ansi": "^6.0.0", + "wcwidth": "^1.0.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/firebase-tools/node_modules/ora/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "devOptional": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/firebase-tools/node_modules/ora/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "devOptional": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/firebase-tools/node_modules/ora/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "devOptional": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/firebase-tools/node_modules/ora/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/ora/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "devOptional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/ora/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "devOptional": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", + "devOptional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/firebase-tools/node_modules/p-defer": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-defer/-/p-defer-3.0.0.tgz", + "integrity": "sha512-ugZxsxmtTln604yeYd29EGrNhazN2lywetzpKhfmQjW/VJmhpDmWbiX+h0zL8V91R0UXkhb3KtPmyq9PZw3aYw==", + "devOptional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/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==", + "devOptional": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/firebase-tools/node_modules/pac-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-5.0.0.tgz", + "integrity": "sha512-CcFG3ZtnxO8McDigozwE3AqAw15zDvGH+OjXO4kzf7IkEKkQ4gxQ+3sdF50WmhQ4P/bVusXcqNE2S3XrNURwzQ==", + "devOptional": true, + "dependencies": { + "@tootallnate/once": "1", + "agent-base": "6", + "debug": "4", + "get-uri": "3", + "http-proxy-agent": "^4.0.1", + "https-proxy-agent": "5", + "pac-resolver": "^5.0.0", + "raw-body": "^2.2.0", + "socks-proxy-agent": "5" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/firebase-tools/node_modules/pac-proxy-agent/node_modules/debug": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", + "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", + "devOptional": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/firebase-tools/node_modules/pac-proxy-agent/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/pac-resolver": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-5.0.0.tgz", + "integrity": "sha512-H+/A6KitiHNNW+bxBKREk2MCGSxljfqRX76NjummWEYIat7ldVXRU3dhRIE3iXZ0nvGBk6smv3nntxKkzRL8NA==", + "devOptional": true, + "dependencies": { + "degenerator": "^3.0.1", + "ip": "^1.1.5", + "netmask": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/firebase-tools/node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "devOptional": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/firebase-tools/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": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "devOptional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/firebase-tools/node_modules/path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=", + "devOptional": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/firebase-tools/node_modules/path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "devOptional": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/firebase-tools/node_modules/portfinder": { + "version": "1.0.32", + "resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.32.tgz", + "integrity": "sha512-on2ZJVVDXRADWE6jnQaX0ioEylzgBpQk8r55NE4wjXW1ZxO+BgDlY6DXwj20i0V8eB4SenDQ00WEaxfiIQPcxg==", + "devOptional": true, + "dependencies": { + "async": "^2.6.4", + "debug": "^3.2.7", + "mkdirp": "^0.5.6" + }, + "engines": { + "node": ">= 0.12.0" + } + }, + "node_modules/firebase-tools/node_modules/portfinder/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "devOptional": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/firebase-tools/node_modules/portfinder/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==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/prelude-ls": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", + "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=", + "devOptional": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/firebase-tools/node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "devOptional": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/firebase-tools/node_modules/promise-breaker": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/promise-breaker/-/promise-breaker-5.0.0.tgz", + "integrity": "sha512-mgsWQuG4kJ1dtO6e/QlNDLFtMkMzzecsC69aI5hlLEjGHFNpHrvGhFi4LiK5jg2SMQj74/diH+wZliL9LpGsyA==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/promise-inflight": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", + "integrity": "sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==", + "optional": true + }, + "node_modules/firebase-tools/node_modules/promise-retry": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", + "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", + "optional": true, + "dependencies": { + "err-code": "^2.0.2", + "retry": "^0.12.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/firebase-tools/node_modules/promise-retry/node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "optional": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/firebase-tools/node_modules/proto-list": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", + "integrity": "sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/proto3-json-serializer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proto3-json-serializer/-/proto3-json-serializer-1.1.0.tgz", + "integrity": "sha512-SjXwUWe/vANGs/mJJTbw5++7U67nwsymg7qsoPtw6GiXqw3kUy8ByojrlEdVE2efxAdKreX8WkDafxvYW95ZQg==", + "devOptional": true, + "dependencies": { + "protobufjs": "^7.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/firebase-tools/node_modules/proto3-json-serializer/node_modules/long": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/long/-/long-5.2.1.tgz", + "integrity": "sha512-GKSNGeNAtw8IryjjkhZxuKB3JzlcLTwjtiQCHKvqQet81I93kXslhDQruGI/QsddO83mcDToBVy7GqGS/zYf/A==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/proto3-json-serializer/node_modules/protobufjs": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.1.2.tgz", + "integrity": "sha512-4ZPTPkXCdel3+L81yw3dG6+Kq3umdWKh7Dc7GW/CpNk4SX3hK58iPCWeCyhVTDrbkNeKrYNZ7EojM5WDaEWTLQ==", + "devOptional": true, + "hasInstallScript": true, + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/node": ">=13.7.0", + "long": "^5.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/firebase-tools/node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "devOptional": true, + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/firebase-tools/node_modules/proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-5.0.0.tgz", + "integrity": "sha512-gkH7BkvLVkSfX9Dk27W6TyNOWWZWRilRfk1XxGNWOYJ2TuedAv1yFpCaU9QSBmBe716XOTNpYNOzhysyw8xn7g==", + "devOptional": true, + "dependencies": { + "agent-base": "^6.0.0", + "debug": "4", + "http-proxy-agent": "^4.0.0", + "https-proxy-agent": "^5.0.0", + "lru-cache": "^5.1.1", + "pac-proxy-agent": "^5.0.0", + "proxy-from-env": "^1.0.0", + "socks-proxy-agent": "^5.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/firebase-tools/node_modules/proxy-agent/node_modules/debug": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", + "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", + "devOptional": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/firebase-tools/node_modules/proxy-agent/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==", + "devOptional": true, + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/firebase-tools/node_modules/proxy-agent/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/proxy-agent/node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/psl": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz", + "integrity": "sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "devOptional": true, + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/firebase-tools/node_modules/punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", + "devOptional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/firebase-tools/node_modules/pupa": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/pupa/-/pupa-2.1.1.tgz", + "integrity": "sha512-l1jNAspIBSFqbT+y+5FosojNpVpF94nlI+wDUpqP9enwOTfHx9f0gh5nB96vl+6yTpsJsypeNrwfzPrKuHB41A==", + "devOptional": true, + "dependencies": { + "escape-goat": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/qs": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", + "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "devOptional": true, + "dependencies": { + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/firebase-tools/node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "devOptional": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/firebase-tools/node_modules/raw-body": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", + "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", + "devOptional": true, + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/firebase-tools/node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "devOptional": true, + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/firebase-tools/node_modules/re2": { + "version": "1.17.7", + "resolved": "https://registry.npmjs.org/re2/-/re2-1.17.7.tgz", + "integrity": "sha512-X8GSuiBoVWwcjuppqSjsIkRxNUKDdjhkO9SBekQbZ2ksqWUReCy7DQPWOVpoTnpdtdz5PIpTTxTFzvJv5UMfjA==", + "hasInstallScript": true, + "optional": true, + "dependencies": { + "install-artifact-from-github": "^1.3.1", + "nan": "^2.16.0", + "node-gyp": "^9.0.0" + } + }, + "node_modules/firebase-tools/node_modules/readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "devOptional": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/firebase-tools/node_modules/readdir-glob": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/readdir-glob/-/readdir-glob-1.1.1.tgz", + "integrity": "sha512-91/k1EzZwDx6HbERR+zucygRFfiPl2zkIYZtv3Jjr6Mn7SkKcVct8aVO+sSRiGMc6fLf72du3d92/uY63YPdEA==", + "devOptional": true, + "dependencies": { + "minimatch": "^3.0.4" + } + }, + "node_modules/firebase-tools/node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "devOptional": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/firebase-tools/node_modules/redeyed": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/redeyed/-/redeyed-2.1.1.tgz", + "integrity": "sha1-iYS1gV2ZyyIEacme7v/jiRPmzAs=", + "devOptional": true, + "dependencies": { + "esprima": "~4.0.0" + } + }, + "node_modules/firebase-tools/node_modules/registry-url": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/registry-url/-/registry-url-5.1.0.tgz", + "integrity": "sha512-8acYXXTI0AkQv6RAOjE3vOaIXZkT9wo4LOFbBKYQEEnnMNBpKqdUrI6S4NT0KPIo/WVvJ5tE/X5LF/TQUf0ekw==", + "devOptional": true, + "dependencies": { + "rc": "^1.2.8" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/request": { + "version": "2.88.2", + "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", + "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==", + "deprecated": "request has been deprecated, see https://github.com/request/request/issues/3142", + "devOptional": true, + "dependencies": { + "aws-sign2": "~0.7.0", + "aws4": "^1.8.0", + "caseless": "~0.12.0", + "combined-stream": "~1.0.6", + "extend": "~3.0.2", + "forever-agent": "~0.6.1", + "form-data": "~2.3.2", + "har-validator": "~5.1.3", + "http-signature": "~1.2.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.19", + "oauth-sign": "~0.9.0", + "performance-now": "^2.1.0", + "qs": "~6.5.2", + "safe-buffer": "^5.1.2", + "tough-cookie": "~2.5.0", + "tunnel-agent": "^0.6.0", + "uuid": "^3.3.2" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/firebase-tools/node_modules/request/node_modules/form-data": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", + "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", + "devOptional": true, + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 0.12" + } + }, + "node_modules/firebase-tools/node_modules/request/node_modules/qs": { + "version": "6.5.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.3.tgz", + "integrity": "sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==", + "devOptional": true, + "engines": { + "node": ">=0.6" + } + }, + "node_modules/firebase-tools/node_modules/request/node_modules/uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", + "deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.", + "devOptional": true, + "bin": { + "uuid": "bin/uuid" + } + }, + "node_modules/firebase-tools/node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", + "devOptional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/firebase-tools/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==", + "devOptional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/firebase-tools/node_modules/requizzle": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/requizzle/-/requizzle-0.2.4.tgz", + "integrity": "sha512-JRrFk1D4OQ4SqovXOgdav+K8EAhSB/LJZqCz8tbX0KObcdeM15Ss59ozWMBWmmINMagCwmqn4ZNryUGpBsl6Jw==", + "devOptional": true, + "dependencies": { + "lodash": "^4.17.21" + } + }, + "node_modules/firebase-tools/node_modules/restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "devOptional": true, + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/retry": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "devOptional": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/firebase-tools/node_modules/retry-request": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/retry-request/-/retry-request-5.0.2.tgz", + "integrity": "sha512-wfI3pk7EE80lCIXprqh7ym48IHYdwmAAzESdbU8Q9l7pnRCk9LEhpbOTNKjz6FARLm/Bl5m+4F0ABxOkYUujSQ==", + "devOptional": true, + "dependencies": { + "debug": "^4.1.1", + "extend": "^3.0.2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/firebase-tools/node_modules/retry-request/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "devOptional": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/firebase-tools/node_modules/retry-request/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "devOptional": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/firebase-tools/node_modules/router": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/router/-/router-1.3.5.tgz", + "integrity": "sha512-kozCJZUhuSJ5VcLhSb3F8fsmGXy+8HaDbKCAerR1G6tq3mnMZFMuSohbFvGv1c5oMFipijDjRZuuN/Sq5nMf3g==", + "devOptional": true, + "dependencies": { + "array-flatten": "3.0.0", + "debug": "2.6.9", + "methods": "~1.1.2", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.7", + "setprototypeof": "1.2.0", + "utils-merge": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/firebase-tools/node_modules/router/node_modules/array-flatten": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-3.0.0.tgz", + "integrity": "sha512-zPMVc3ZYlGLNk4mpK1NzP2wg0ml9t7fUgDsayR5Y5rSzxQilzR9FGu/EH2jQOcKSAeAfWeylyW8juy3OkWRvNA==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/run-async": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", + "integrity": "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==", + "devOptional": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/firebase-tools/node_modules/rxjs": { + "version": "7.5.6", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.5.6.tgz", + "integrity": "sha512-dnyv2/YsXhnm461G+R/Pe5bWP41Nm6LBXEYWI6eiFP4fiwx6WRI/CD0zbdVAudd9xwLEF2IDcKXLHit0FYjUzw==", + "devOptional": true, + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/firebase-tools/node_modules/rxjs/node_modules/tslib": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz", + "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "devOptional": true, + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/firebase-tools/node_modules/semver-diff": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/semver-diff/-/semver-diff-3.1.1.tgz", + "integrity": "sha512-GX0Ix/CJcHyB8c4ykpHGIAvLyOwOobtM/8d+TQkAd81/bEjgPHrfba41Vpesr7jX/t8Uh+R3EX9eAS5be+jQYg==", + "devOptional": true, + "dependencies": { + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/semver-diff/node_modules/semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "devOptional": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/firebase-tools/node_modules/send": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", + "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "devOptional": true, + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/firebase-tools/node_modules/send/node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "devOptional": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/firebase-tools/node_modules/send/node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "devOptional": true, + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/firebase-tools/node_modules/send/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==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/send/node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "devOptional": true, + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/firebase-tools/node_modules/send/node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "devOptional": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/firebase-tools/node_modules/serve-static": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", + "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", + "devOptional": true, + "dependencies": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.18.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/firebase-tools/node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", + "optional": true + }, + "node_modules/firebase-tools/node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", + "devOptional": true, + "dependencies": { + "shebang-regex": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/firebase-tools/node_modules/shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", + "devOptional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/firebase-tools/node_modules/side-channel": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", + "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "devOptional": true, + "dependencies": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/firebase-tools/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/simple-swizzle": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", + "integrity": "sha1-pNprY1/8zMoz9w0Xy5JZLeleVXo=", + "devOptional": true, + "dependencies": { + "is-arrayish": "^0.3.1" + } + }, + "node_modules/firebase-tools/node_modules/simple-swizzle/node_modules/is-arrayish": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", + "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "devOptional": true, + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/firebase-tools/node_modules/socks": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.7.0.tgz", + "integrity": "sha512-scnOe9y4VuiNUULJN72GrM26BNOjVsfPXI+j+98PkyEfsIXroa5ofyjT+FzGvn/xHs73U2JtoBYAVx9Hl4quSA==", + "devOptional": true, + "dependencies": { + "ip": "^2.0.0", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.13.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/firebase-tools/node_modules/socks-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-5.0.1.tgz", + "integrity": "sha512-vZdmnjb9a2Tz6WEQVIurybSwElwPxMZaIc7PzqbJTrezcKNznv6giT7J7tZDZ1BojVaa1jvO/UiUdhDVB0ACoQ==", + "devOptional": true, + "dependencies": { + "agent-base": "^6.0.2", + "debug": "4", + "socks": "^2.3.3" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/firebase-tools/node_modules/socks-proxy-agent/node_modules/debug": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", + "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", + "devOptional": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/firebase-tools/node_modules/socks-proxy-agent/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/socks/node_modules/ip": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ip/-/ip-2.0.0.tgz", + "integrity": "sha512-WKa+XuLG1A1R0UWhl2+1XQSi+fZWMsYKffMZTTYsiZaUD8k2yDAj5atimTUD2TZkyCkNEeYE5NhFZmupOGtjYQ==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/firebase-tools/node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/sshpk": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz", + "integrity": "sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==", + "devOptional": true, + "dependencies": { + "asn1": "~0.2.3", + "assert-plus": "^1.0.0", + "bcrypt-pbkdf": "^1.0.0", + "dashdash": "^1.12.0", + "ecc-jsbn": "~0.1.1", + "getpass": "^0.1.1", + "jsbn": "~0.1.0", + "safer-buffer": "^2.0.2", + "tweetnacl": "~0.14.0" + }, + "bin": { + "sshpk-conv": "bin/sshpk-conv", + "sshpk-sign": "bin/sshpk-sign", + "sshpk-verify": "bin/sshpk-verify" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/firebase-tools/node_modules/ssri": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-9.0.1.tgz", + "integrity": "sha512-o57Wcn66jMQvfHG1FlYbWeZWW/dHZhJXjpIcTfXldXEk5nz5lStPo3mK0OJQfGR3RbZUlbISexbljkJzuEj/8Q==", + "optional": true, + "dependencies": { + "minipass": "^3.1.1" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/firebase-tools/node_modules/stack-trace": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", + "integrity": "sha1-VHxws0fo0ytOEI6hoqFZ5f3eGcA=", + "devOptional": true, + "engines": { + "node": "*" + } + }, + "node_modules/firebase-tools/node_modules/statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=", + "devOptional": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/firebase-tools/node_modules/stream-chain": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/stream-chain/-/stream-chain-2.2.4.tgz", + "integrity": "sha512-9lsl3YM53V5N/I1C2uJtc3Kavyi3kNYN83VkKb/bMWRk7D9imiFyUPYa0PoZbLohSVOX1mYE9YsmwObZUsth6Q==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/stream-json": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/stream-json/-/stream-json-1.7.3.tgz", + "integrity": "sha512-Y6dXn9KKWSwxOqnvHGcdZy1PK+J+7alBwHCeU3W9oRqm4ilLRA0XSPmd1tWwhg7tv9EIxJTMWh7KF15tYelKJg==", + "devOptional": true, + "dependencies": { + "stream-chain": "^2.2.4" + } + }, + "node_modules/firebase-tools/node_modules/stream-shift": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.1.tgz", + "integrity": "sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "devOptional": true, + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/firebase-tools/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "devOptional": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/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==", + "devOptional": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/strip-ansi/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==", + "devOptional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", + "devOptional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/firebase-tools/node_modules/superstatic": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/superstatic/-/superstatic-9.0.3.tgz", + "integrity": "sha512-e/tmW0bsnQ/33ivK6y3CapJT0Ovy4pk/ohNPGhIAGU2oasoNLRQ1cv6enua09NU9w6Y0H/fBu07cjzuiWvLXxw==", + "devOptional": true, + "dependencies": { + "basic-auth-connect": "^1.0.0", + "commander": "^10.0.0", + "compression": "^1.7.0", + "connect": "^3.7.0", + "destroy": "^1.0.4", + "fast-url-parser": "^1.1.3", + "glob-slasher": "^1.0.1", + "is-url": "^1.2.2", + "join-path": "^1.1.1", + "lodash": "^4.17.19", + "mime-types": "^2.1.35", + "minimatch": "^6.1.6", + "morgan": "^1.8.2", + "on-finished": "^2.2.0", + "on-headers": "^1.0.0", + "path-to-regexp": "^1.8.0", + "router": "^1.3.1", + "update-notifier-cjs": "^5.1.6" + }, + "bin": { + "superstatic": "lib/bin/server.js" + }, + "engines": { + "node": "^14.18.0 || >=16.4.0" + }, + "optionalDependencies": { + "re2": "^1.17.7" + } + }, + "node_modules/firebase-tools/node_modules/superstatic/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "devOptional": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/firebase-tools/node_modules/superstatic/node_modules/commander": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.0.tgz", + "integrity": "sha512-zS5PnTI22FIRM6ylNW8G4Ap0IEOyk62fhLSD0+uHRT9McRCLGpkVNvao4bjimpK/GShynyQkFFxHhwMcETmduA==", + "devOptional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/firebase-tools/node_modules/superstatic/node_modules/isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/superstatic/node_modules/minimatch": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-6.2.0.tgz", + "integrity": "sha512-sauLxniAmvnhhRjFwPNnJKaPFYyddAgbYdeUpHULtCT/GhzdCx/MDNy+Y40lBxTQUrMzDE8e0S43Z5uqfO0REg==", + "devOptional": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/firebase-tools/node_modules/superstatic/node_modules/path-to-regexp": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz", + "integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==", + "devOptional": true, + "dependencies": { + "isarray": "0.0.1" + } + }, + "node_modules/firebase-tools/node_modules/supports-hyperlinks": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-2.2.0.tgz", + "integrity": "sha512-6sXEzV5+I5j8Bmq9/vUphGRM/RJNT9SCURJLjwfOg51heRtguGWDzcaBlgAzKhQa0EVNpPEKzQuBwZ8S8WaCeQ==", + "devOptional": true, + "dependencies": { + "has-flag": "^4.0.0", + "supports-color": "^7.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/supports-hyperlinks/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==", + "devOptional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/supports-hyperlinks/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==", + "devOptional": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/tar": { + "version": "6.1.11", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.1.11.tgz", + "integrity": "sha512-an/KZQzQUkZCkuoAA64hM92X0Urb6VpRhAFllDzz44U2mcD5scmT3zBc4VgVpkugF580+DQn8eAFSyoQt0tznA==", + "devOptional": true, + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^3.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/firebase-tools/node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "devOptional": true, + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/firebase-tools/node_modules/tar/node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "devOptional": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/firebase-tools/node_modules/tar/node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "devOptional": true, + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/firebase-tools/node_modules/tcp-port-used": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/tcp-port-used/-/tcp-port-used-1.0.2.tgz", + "integrity": "sha512-l7ar8lLUD3XS1V2lfoJlCBaeoaWo/2xfYt81hM7VlvR4RrMVFqfmzfhLVk40hAb368uitje5gPtBRL1m/DGvLA==", + "devOptional": true, + "dependencies": { + "debug": "4.3.1", + "is2": "^2.0.6" + } + }, + "node_modules/firebase-tools/node_modules/tcp-port-used/node_modules/debug": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "devOptional": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/firebase-tools/node_modules/tcp-port-used/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/text-hex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", + "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/tmp": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz", + "integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==", + "devOptional": true, + "dependencies": { + "rimraf": "^3.0.0" + }, + "engines": { + "node": ">=8.17.0" + } + }, + "node_modules/firebase-tools/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==", + "devOptional": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/firebase-tools/node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "devOptional": true, + "engines": { + "node": ">=0.6" + } + }, + "node_modules/firebase-tools/node_modules/tough-cookie": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", + "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", + "devOptional": true, + "dependencies": { + "psl": "^1.1.28", + "punycode": "^2.1.1" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/firebase-tools/node_modules/toxic": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toxic/-/toxic-1.0.1.tgz", + "integrity": "sha512-WI3rIGdcaKULYg7KVoB0zcjikqvcYYvcuT6D89bFPz2rVR0Rl0PK6x8/X62rtdLtBKIE985NzVf/auTtGegIIg==", + "devOptional": true, + "dependencies": { + "lodash": "^4.17.10" + } + }, + "node_modules/firebase-tools/node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/triple-beam": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.3.0.tgz", + "integrity": "sha512-XrHUvV5HpdLmIj4uVMxHggLbFSZYIn7HEWsqePZcI50pco+MPqJ50wMGY794X7AOOhxOBAjbkqfAbEe/QMp2Lw==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", + "devOptional": true, + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/firebase-tools/node_modules/tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/type-check": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", + "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", + "devOptional": true, + "dependencies": { + "prelude-ls": "~1.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/firebase-tools/node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "devOptional": true, + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/firebase-tools/node_modules/typedarray-to-buffer": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", + "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", + "devOptional": true, + "dependencies": { + "is-typedarray": "^1.0.0" + } + }, + "node_modules/firebase-tools/node_modules/uc.micro": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz", + "integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/uglify-js": { + "version": "3.17.4", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.17.4.tgz", + "integrity": "sha512-T9q82TJI9e/C1TAxYvfb16xO120tMVFZrGA3f9/P4424DNu6ypK103y0GPFVa17yotwSyZW5iYXgjYHkGrJW/g==", + "devOptional": true, + "bin": { + "uglifyjs": "bin/uglifyjs" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/firebase-tools/node_modules/underscore": { + "version": "1.13.6", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.6.tgz", + "integrity": "sha512-+A5Sja4HP1M08MaXya7p5LvjuM7K6q/2EaC0+iovj/wOcMsTzMvDFbasi/oSapiwOlt252IqsKqPjCl7huKS0A==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/unique-filename": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-2.0.1.tgz", + "integrity": "sha512-ODWHtkkdx3IAR+veKxFV+VBkUMcN+FaqzUUd7IZzt+0zhDZFPFxhlqwPF3YQvMHx1TD0tdgYl+kuPnJ8E6ql7A==", + "optional": true, + "dependencies": { + "unique-slug": "^3.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/firebase-tools/node_modules/unique-slug": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-3.0.0.tgz", + "integrity": "sha512-8EyMynh679x/0gqE9fT9oilG+qEt+ibFyqjuVTsZn1+CMxH+XLlpvr2UZx4nVcCwTpx81nICr2JQFkM+HPLq4w==", + "optional": true, + "dependencies": { + "imurmurhash": "^0.1.4" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/firebase-tools/node_modules/universal-analytics": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/universal-analytics/-/universal-analytics-0.5.3.tgz", + "integrity": "sha512-HXSMyIcf2XTvwZ6ZZQLfxfViRm/yTGoRgDeTbojtq6rezeyKB0sTBcKH2fhddnteAHRcHiKgr/ACpbgjGOC6RQ==", + "devOptional": true, + "dependencies": { + "debug": "^4.3.1", + "uuid": "^8.0.0" + }, + "engines": { + "node": ">=12.18.2" + } + }, + "node_modules/firebase-tools/node_modules/universal-analytics/node_modules/debug": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", + "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", + "devOptional": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/firebase-tools/node_modules/universal-analytics/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "devOptional": true, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/firebase-tools/node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=", + "devOptional": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/firebase-tools/node_modules/update-notifier-cjs": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/update-notifier-cjs/-/update-notifier-cjs-5.1.6.tgz", + "integrity": "sha512-wgxdSBWv3x/YpMzsWz5G4p4ec7JWD0HCl8W6bmNB6E5Gwo+1ym5oN4hiXpLf0mPySVEJEIsYlkshnplkg2OP9A==", + "devOptional": true, + "dependencies": { + "boxen": "^5.0.0", + "chalk": "^4.1.0", + "configstore": "^5.0.1", + "has-yarn": "^2.1.0", + "import-lazy": "^2.1.0", + "is-ci": "^2.0.0", + "is-installed-globally": "^0.4.0", + "is-npm": "^5.0.0", + "is-yarn-global": "^0.3.0", + "isomorphic-fetch": "^3.0.0", + "pupa": "^2.1.1", + "registry-auth-token": "^5.0.1", + "registry-url": "^5.1.0", + "semver": "^7.3.7", + "semver-diff": "^3.1.1", + "xdg-basedir": "^4.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/firebase-tools/node_modules/update-notifier-cjs/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==", + "devOptional": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/firebase-tools/node_modules/update-notifier-cjs/node_modules/boxen": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/boxen/-/boxen-5.1.2.tgz", + "integrity": "sha512-9gYgQKXx+1nP8mP7CzFyaUARhg7D3n1dF/FnErWmu9l6JvGpNUN278h0aSb+QjoiKSWG+iZ3uHrcqk0qrY9RQQ==", + "devOptional": true, + "dependencies": { + "ansi-align": "^3.0.0", + "camelcase": "^6.2.0", + "chalk": "^4.1.0", + "cli-boxes": "^2.2.1", + "string-width": "^4.2.2", + "type-fest": "^0.20.2", + "widest-line": "^3.1.0", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/firebase-tools/node_modules/update-notifier-cjs/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "devOptional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/firebase-tools/node_modules/update-notifier-cjs/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "devOptional": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/firebase-tools/node_modules/update-notifier-cjs/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==", + "devOptional": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/firebase-tools/node_modules/update-notifier-cjs/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==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/update-notifier-cjs/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==", + "devOptional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/update-notifier-cjs/node_modules/registry-auth-token": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-5.0.1.tgz", + "integrity": "sha512-UfxVOj8seK1yaIOiieV4FIP01vfBDLsY0H9sQzi9EbbUdJiuuBjJgLa1DpImXMNPnVkBD4eVxTEXcrZA6kfpJA==", + "devOptional": true, + "dependencies": { + "@pnpm/npm-conf": "^1.0.4" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/firebase-tools/node_modules/update-notifier-cjs/node_modules/semver": { + "version": "7.3.8", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", + "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", + "devOptional": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/firebase-tools/node_modules/update-notifier-cjs/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==", + "devOptional": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/update-notifier-cjs/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==", + "devOptional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/firebase-tools/node_modules/uri-js": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", + "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", + "devOptional": true, + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/firebase-tools/node_modules/url-join": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/url-join/-/url-join-0.0.1.tgz", + "integrity": "sha1-HbSK1CLTQCRpqH99l73r/k+x48g=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=", + "devOptional": true, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/firebase-tools/node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "devOptional": true, + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/firebase-tools/node_modules/valid-url": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/valid-url/-/valid-url-1.0.9.tgz", + "integrity": "sha1-HBRHm0DxOXp1eC8RXkCGRHQzogA=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=", + "devOptional": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/firebase-tools/node_modules/verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", + "devOptional": true, + "engines": [ + "node >=0.6.0" + ], + "dependencies": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + } + }, + "node_modules/firebase-tools/node_modules/vm2": { + "version": "3.9.17", + "resolved": "https://registry.npmjs.org/vm2/-/vm2-3.9.17.tgz", + "devOptional": true, + "dependencies": { + "acorn": "^8.7.0", + "acorn-walk": "^8.2.0" + }, + "bin": { + "vm2": "bin/vm2" + }, + "engines": { + "node": ">=6.0" + } + }, + "node_modules/firebase-tools/node_modules/wcwidth": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", + "integrity": "sha1-8LDc+RW8X/FSivrbLA4XtTLaL+g=", + "devOptional": true, + "dependencies": { + "defaults": "^1.0.3" + } + }, + "node_modules/firebase-tools/node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha1-lmRU6HZUYuN2RNNib2dCzotwll0=", + "devOptional": true, + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/firebase-tools/node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "devOptional": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, + "node_modules/firebase-tools/node_modules/wide-align": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", + "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", + "optional": true, + "dependencies": { + "string-width": "^1.0.2 || 2 || 3 || 4" + } + }, + "node_modules/firebase-tools/node_modules/widest-line": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-3.1.0.tgz", + "integrity": "sha512-NsmoXalsWVDMGupxZ5R08ka9flZjjiLvHVAWYOKtiKM8ujtZWr9cRffak+uSE48+Ob8ObalXpwyeUiyDD6QFgg==", + "devOptional": true, + "dependencies": { + "string-width": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/winston": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/winston/-/winston-3.2.1.tgz", + "integrity": "sha512-zU6vgnS9dAWCEKg/QYigd6cgMVVNwyTzKs81XZtTFuRwJOcDdBg7AU0mXVyNbs7O5RH2zdv+BdNZUlx7mXPuOw==", + "devOptional": true, + "dependencies": { + "async": "^2.6.1", + "diagnostics": "^1.1.1", + "is-stream": "^1.1.0", + "logform": "^2.1.1", + "one-time": "0.0.4", + "readable-stream": "^3.1.1", + "stack-trace": "0.0.x", + "triple-beam": "^1.3.0", + "winston-transport": "^4.3.0" + }, + "engines": { + "node": ">= 6.4.0" + } + }, + "node_modules/firebase-tools/node_modules/winston-transport": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.4.0.tgz", + "integrity": "sha512-Lc7/p3GtqtqPBYYtS6KCN3c77/2QCev51DvcJKbkFPQNoj1sinkGwLGFDxkXY9J6p9+EPnYs+D90uwbnaiURTw==", + "devOptional": true, + "dependencies": { + "readable-stream": "^2.3.7", + "triple-beam": "^1.2.0" + }, + "engines": { + "node": ">= 6.4.0" + } + }, + "node_modules/firebase-tools/node_modules/winston-transport/node_modules/readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "devOptional": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/firebase-tools/node_modules/word-wrap": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", + "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", + "devOptional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/firebase-tools/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "devOptional": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/firebase-tools/node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "devOptional": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/firebase-tools/node_modules/wrap-ansi/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "devOptional": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/firebase-tools/node_modules/wrap-ansi/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/write-file-atomic": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz", + "integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==", + "devOptional": true, + "dependencies": { + "imurmurhash": "^0.1.4", + "is-typedarray": "^1.0.0", + "signal-exit": "^3.0.2", + "typedarray-to-buffer": "^3.1.5" + } + }, + "node_modules/firebase-tools/node_modules/ws": { + "version": "7.4.6", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.6.tgz", + "integrity": "sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A==", + "devOptional": true, + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/firebase-tools/node_modules/xdg-basedir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-4.0.0.tgz", + "integrity": "sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q==", + "devOptional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/xmlcreate": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/xmlcreate/-/xmlcreate-2.0.4.tgz", + "integrity": "sha512-nquOebG4sngPmGPICTS5EnxqhKbCmz5Ox5hsszI2T6U5qdrJizBc+0ilYSEjTSzU0yZcmvppztXe/5Al5fUwdg==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/xregexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/xregexp/-/xregexp-2.0.0.tgz", + "integrity": "sha1-UqY+VsoLhKfzpfPWGHLxJq16WUM=", + "devOptional": true, + "engines": { + "node": "*" + } + }, + "node_modules/firebase-tools/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "devOptional": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/firebase-tools/node_modules/yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "devOptional": true, + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/firebase-tools/node_modules/yargs-parser": { + "version": "20.2.4", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz", + "integrity": "sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==", + "devOptional": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/firebase-tools/node_modules/yargs/node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "devOptional": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/firebase-tools/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==", + "devOptional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/firebase-tools/node_modules/zip-stream": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-4.1.0.tgz", + "integrity": "sha512-zshzwQW7gG7hjpBlgeQP9RuyPGNxvJdzR8SUM3QhxCnLjWN2E7j3dOvpeDcQoETfHx0urRS7EtmVToql7YpU4A==", + "devOptional": true, + "dependencies": { + "archiver-utils": "^2.1.0", + "compress-commons": "^4.1.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">= 10" + } + }, + "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.3.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.2.tgz", + "integrity": "sha512-AiwGJM8YcNOaobumgtng+6NHuOqC3A7MixFeDafM3X9cIUM+xUXoS5Vfgf+OihAYe20fxqNM9yPBXJzRtZ/4eA==", + "dev": true, + "license": "ISC" + }, + "node_modules/fn.name": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz", + "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==", + "license": "MIT" + }, + "node_modules/follow-redirects": { + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/foreground-child": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", + "integrity": "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.0", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/foreground-child/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fraction.js": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", + "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "patreon", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/fs-minipass": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-3.0.3.tgz", + "integrity": "sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/fs-minipass/node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/fs-monkey": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.0.6.tgz", + "integrity": "sha512-b1FMfwetIKymC0eioW7mTywihSQE4oLzQn1dB6rZB5fx/3NpNEdAWeCSMB+60/AeT0TCXsxzAlcYVEFCTAksWg==", + "dev": true, + "license": "Unlicense" + }, + "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==", + "dev": true, + "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==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/fuzzy": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/fuzzy/-/fuzzy-0.1.3.tgz", + "integrity": "sha512-/gZffu4ykarLrCiP3Ygsa86UAo1E5vEVlvTrpkKywXSbP9Xhln3oSp9QSV57gEq3JFFpGJ4GZ+5zdEp3FcUh4w==", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/gauge": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-4.0.4.tgz", + "integrity": "sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg==", + "deprecated": "This package is no longer supported.", + "dev": true, + "license": "ISC", + "dependencies": { + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.3", + "console-control-strings": "^1.1.0", + "has-unicode": "^2.0.1", + "signal-exit": "^3.0.7", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.5" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "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-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.7.tgz", + "integrity": "sha512-VW6Pxhsrk0KAOqs3WEd0klDiF/+V7gQOpAvY1jVU/LHmaD/kQO4523aiJuikX/QAKYiW6x8Jh+RJej1almdtCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "function-bind": "^1.1.2", + "get-proto": "^1.0.0", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "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==", + "devOptional": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "license": "BSD-2-Clause" + }, + "node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/globby": { + "version": "13.2.2", + "resolved": "https://registry.npmjs.org/globby/-/globby-13.2.2.tgz", + "integrity": "sha512-Y1zNGV+pzQdh7H39l9zgB4PJqjRNqydvdYCDG4HFXM4XuvSaQQlEc91IU1yALL8gUTDomgBAfz3XJdmUS+oo0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "dir-glob": "^3.0.1", + "fast-glob": "^3.3.0", + "ignore": "^5.2.4", + "merge2": "^1.4.1", + "slash": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/good-listener": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/good-listener/-/good-listener-1.2.2.tgz", + "integrity": "sha512-goW1b+d9q/HIwbVYZzZ6SsTr4IgE+WA44A0GmPIQstuOrgsFcT7VEJ48nmr9GaRtNu0XTKacFLGnBPAM6Afouw==", + "license": "MIT", + "dependencies": { + "delegate": "^3.1.2" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, + "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/gts": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/gts/-/gts-5.3.1.tgz", + "integrity": "sha512-P9F+krJkGOkisUX+P9pfUas1Xy+U+CxBFZT62uInkJbgvZpnW1ug/pIcMJJmLOthMq1J88lpQUGhXDC9UTvVcw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@typescript-eslint/eslint-plugin": "5.62.0", + "@typescript-eslint/parser": "5.62.0", + "chalk": "^4.1.2", + "eslint": "8.57.0", + "eslint-config-prettier": "9.1.0", + "eslint-plugin-n": "15.7.0", + "eslint-plugin-prettier": "5.1.3", + "execa": "^5.0.0", + "inquirer": "^7.3.3", + "json5": "^2.1.3", + "meow": "^9.0.0", + "ncp": "^2.0.0", + "prettier": "3.2.5", + "rimraf": "3.0.2", + "write-file-atomic": "^4.0.0" + }, + "bin": { + "gts": "build/src/cli.js" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "typescript": ">=3" + } + }, + "node_modules/gts/node_modules/inquirer": { + "version": "7.3.3", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-7.3.3.tgz", + "integrity": "sha512-JG3eIAj5V9CwcGvuOmoo6LB9kbAYT8HXffUl6memuszlwDC/qvFAJw49XJ5NROSFNPxp3iQg1GqkFhaY/CR0IA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-escapes": "^4.2.1", + "chalk": "^4.1.0", + "cli-cursor": "^3.1.0", + "cli-width": "^3.0.0", + "external-editor": "^3.0.3", + "figures": "^3.0.0", + "lodash": "^4.17.19", + "mute-stream": "0.0.8", + "run-async": "^2.4.0", + "rxjs": "^6.6.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0", + "through": "^2.3.6" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/gts/node_modules/prettier": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.5.tgz", + "integrity": "sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/gts/node_modules/rxjs": { + "version": "6.6.7", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", + "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^1.9.0" + }, + "engines": { + "npm": ">=2.0.0" + } + }, + "node_modules/gts/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true, + "license": "0BSD" + }, + "node_modules/handle-thing": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz", + "integrity": "sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg==", + "dev": true, + "license": "MIT" + }, + "node_modules/hard-rejection": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/hard-rejection/-/hard-rejection-2.1.0.tgz", + "integrity": "sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "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==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hdr-histogram-js": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/hdr-histogram-js/-/hdr-histogram-js-2.0.3.tgz", + "integrity": "sha512-Hkn78wwzWHNCp2uarhzQ2SGFLU3JY8SBDDd3TAABK4fc30wm+MuPOrg5QVFVfkKOQd6Bfz3ukJEI+q9sXEkK1g==", + "dev": true, + "license": "BSD", + "dependencies": { + "@assemblyscript/loader": "^0.10.1", + "base64-js": "^1.2.0", + "pako": "^1.0.3" + } + }, + "node_modules/hdr-histogram-percentiles-obj": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hdr-histogram-percentiles-obj/-/hdr-histogram-percentiles-obj-3.0.0.tgz", + "integrity": "sha512-7kIufnBqdsBGcSZLPJwqHT3yhk1QTsSlFsVD3kx5ixH/AlgBs9yM1q6DPhXZ8f8gtdqgh7N7/5btRLpQsS2gHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/hosted-git-info": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-6.1.3.tgz", + "integrity": "sha512-HVJyzUrLIL1c0QmviVh5E8VGyUS7xCFPS6yydaVd1UegW+ibV/CohqTH9MkOLDp5o+rb82DMo77PTuc9F/8GKw==", + "dev": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^7.5.1" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/hosted-git-info/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/hpack.js": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz", + "integrity": "sha512-zJxVehUdMGIKsRaNt7apO2Gqp0BdqW5yaiGHXXmbpvxgBYVZnAql+BJb4RO5ad2MgpbZKn5G6nMnegrH1FcNYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.1", + "obuf": "^1.0.0", + "readable-stream": "^2.0.1", + "wbuf": "^1.1.0" + } + }, + "node_modules/hpack.js/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/hpack.js/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/hpack.js/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/html-entities": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.5.2.tgz", + "integrity": "sha512-K//PSRMQk4FZ78Kyau+mZurHn3FH0Vwr+H36eE0rPbeYkRRi9YxceYPhuN60UwWorxyKHhqoAJl2OFKa4BVtaA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/mdevils" + }, + { + "type": "patreon", + "url": "https://patreon.com/mdevils" + } + ], + "license": "MIT" + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, + "node_modules/http-cache-semantics": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", + "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/http-deceiver": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz", + "integrity": "sha512-LmpOGxTfbpgtGVxJrj5k7asXHCgNZp5nLfp+hWc8QQRqtb7fUy6kRY3BO1h9ddF6yIPYUARgxGOwB42DnxIaNw==", + "dev": true, + "license": "MIT" + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/http-errors/node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/http-parser-js": { + "version": "0.5.9", + "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.9.tgz", + "integrity": "sha512-n1XsPy3rXVxlqxVioEWdC+0+M+SQw0DpJynwtOPo1X+ZlvdzTLtDBIJJlDQTnwZIFJrZSzSGmIOUdP8tu+SgLw==", + "license": "MIT" + }, + "node_modules/http-proxy": { + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", + "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eventemitter3": "^4.0.0", + "follow-redirects": "^1.0.0", + "requires-port": "^1.0.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/http-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", + "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tootallnate/once": "2", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/http-proxy-middleware": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.7.tgz", + "integrity": "sha512-fgVY8AV7qU7z/MmXJ/rxwbrtQH4jBQ9m7kp3llF0liB7glmFeVZFBepQb32T3y8n8k2+AEYuMPCpinYW+/CuRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-proxy": "^1.17.8", + "http-proxy": "^1.18.1", + "is-glob": "^4.0.1", + "is-plain-obj": "^3.0.0", + "micromatch": "^4.0.2" + }, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "@types/express": "^4.17.13" + }, + "peerDependenciesMeta": { + "@types/express": { + "optional": true + } + } + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/humanize-ms": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", + "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.0.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/icss-utils": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz", + "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/idb": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/idb/-/idb-7.1.1.tgz", + "integrity": "sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ==", + "license": "ISC" + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "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": "BSD-3-Clause" + }, + "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/ignore-walk": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-6.0.5.tgz", + "integrity": "sha512-VuuG0wCnjhnylG1ABXT3dAuIpTNDs/G8jlpmwXY03fXoXy/8ZK8/T+hMzt8L4WnrLCJgdybqgPagnF/f97cg3A==", + "dev": true, + "license": "ISC", + "dependencies": { + "minimatch": "^9.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/ignore-walk/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/image-size": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/image-size/-/image-size-0.5.5.tgz", + "integrity": "sha512-6TDAlDPZxUFCv+fuOkIoXT/V/f3Qbq8e37p+YOiYrUv3v9cc3/6x78VdfPgFVaB9dZYeLUfKgHRebpkm/oP2VQ==", + "dev": true, + "license": "MIT", + "optional": true, + "bin": { + "image-size": "bin/image-size.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/immutable": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.7.tgz", + "integrity": "sha512-1hqclzwYwjRDFLjcFxOM5AYkkG0rpFPpr1RLPMEuGczoS7YA8gLhy8SWXYRAA/XwfEHpfo3cw5JGioS32fnMRw==", + "dev": true, + "license": "MIT" + }, + "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/import-fresh/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/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/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/infer-owner": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/infer-owner/-/infer-owner-1.0.4.tgz", + "integrity": "sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==", + "dev": true, + "license": "ISC" + }, + "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==", + "license": "ISC" + }, + "node_modules/ini": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ini/-/ini-3.0.1.tgz", + "integrity": "sha512-it4HyVAUTKBc6m8e1iXWvXSTdndF7HbdN713+kvLrymxTaU4AUBWrJ4vEooP+V7fexnVD3LKcBshjGGPefSMUQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/inquirer": { + "version": "8.2.4", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-8.2.4.tgz", + "integrity": "sha512-nn4F01dxU8VeKfq192IjLsxu0/OmMZ4Lg3xKAns148rCaXP6ntAoEkVYZThWjwON8AlzdZZi6oqnhNbxUG9hVg==", + "license": "MIT", + "dependencies": { + "ansi-escapes": "^4.2.1", + "chalk": "^4.1.1", + "cli-cursor": "^3.1.0", + "cli-width": "^3.0.0", + "external-editor": "^3.0.3", + "figures": "^3.0.0", + "lodash": "^4.17.21", + "mute-stream": "0.0.8", + "ora": "^5.4.1", + "run-async": "^2.4.0", + "rxjs": "^7.5.5", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0", + "through": "^2.3.6", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/inquirer-autocomplete-prompt": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/inquirer-autocomplete-prompt/-/inquirer-autocomplete-prompt-1.4.0.tgz", + "integrity": "sha512-qHgHyJmbULt4hI+kCmwX92MnSxDs/Yhdt4wPA30qnoa01OF6uTXV8yvH4hKXgdaTNmkZ9D01MHjqKYEuJN+ONw==", + "license": "ISC", + "dependencies": { + "ansi-escapes": "^4.3.1", + "chalk": "^4.0.0", + "figures": "^3.2.0", + "run-async": "^2.4.0", + "rxjs": "^6.6.2" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "inquirer": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/inquirer-autocomplete-prompt/node_modules/rxjs": { + "version": "6.6.7", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", + "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^1.9.0" + }, + "engines": { + "npm": ">=2.0.0" + } + }, + "node_modules/inquirer-autocomplete-prompt/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "license": "0BSD" + }, + "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", + "optional": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/ionic-emoji-rating": { + "version": "1.0.15", + "resolved": "https://registry.npmjs.org/ionic-emoji-rating/-/ionic-emoji-rating-1.0.15.tgz", + "integrity": "sha512-ooNInojKERSgfoMZOAWvLm0MAARmAh1ibGr32x9S98J1jv2RC9DdaXOBWf/Xx9ZfJmvUAgZKjufcriszGcuZFA==", + "license": "MIT" + }, + "node_modules/ionicons": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/ionicons/-/ionicons-7.4.0.tgz", + "integrity": "sha512-ZK94MMqgzMCPPMhmk8Ouu6goyVHFIlw/ACP6oe3FrikcI0N7CX0xcwVaEbUc0G/v3W0shI93vo+9ve/KpvcNhQ==", + "license": "MIT", + "dependencies": { + "@stencil/core": "^4.0.3" + } + }, + "node_modules/ip-address": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz", + "integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "jsbn": "1.1.0", + "sprintf-js": "^1.1.3" + }, + "engines": { + "node": ">= 12" + } + }, + "node_modules/ip-address/node_modules/sprintf-js": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/ipaddr.js": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.2.0.tgz", + "integrity": "sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true, + "license": "MIT" + }, + "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==", + "devOptional": true, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "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==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-interactive": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", + "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-lambda": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-lambda/-/is-lambda-1.0.1.tgz", + "integrity": "sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==", + "dev": true, + "license": "MIT" + }, + "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==", + "devOptional": true, + "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": "3.0.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-3.0.0.tgz", + "integrity": "sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dev": true, + "license": "MIT", + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-what": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/is-what/-/is-what-3.14.1.tgz", + "integrity": "sha512-sNxgpk9793nzSs7bA6JQJGeIuRBQhAaNGG77kzYQgMkrID+lS6SlK07K5LaptscDlSaIgH+GPFzf+d75FVxozA==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "license": "MIT", + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/isbinaryfile": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-4.0.10.tgz", + "integrity": "sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/gjtorikian/" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", + "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument/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/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report/node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/istanbul-reports": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", + "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": 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/jasmine-core": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-4.5.0.tgz", + "integrity": "sha512-9PMzyvhtocxb3aXJVOPqBDswdgyAeSB81QnLop4npOpbqnheaTEwPc9ZloQeVswugPManznQBjD8kWDTjlnHuw==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-worker": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", + "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "license": "MIT", + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "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==", + "dev": true, + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsbn": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz", + "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "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==", + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonc-parser": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.0.tgz", + "integrity": "sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==", + "license": "MIT" + }, + "node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "license": "MIT", + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/jsonparse": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz", + "integrity": "sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==", + "dev": true, + "engines": [ + "node >= 0.2.0" + ], + "license": "MIT" + }, + "node_modules/karma": { + "version": "6.4.4", + "resolved": "https://registry.npmjs.org/karma/-/karma-6.4.4.tgz", + "integrity": "sha512-LrtUxbdvt1gOpo3gxG+VAJlJAEMhbWlM4YrFQgql98FwF7+K8K12LYO4hnDdUkNjeztYrOXEMqgTajSWgmtI/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@colors/colors": "1.5.0", + "body-parser": "^1.19.0", + "braces": "^3.0.2", + "chokidar": "^3.5.1", + "connect": "^3.7.0", + "di": "^0.0.1", + "dom-serialize": "^2.2.1", + "glob": "^7.1.7", + "graceful-fs": "^4.2.6", + "http-proxy": "^1.18.1", + "isbinaryfile": "^4.0.8", + "lodash": "^4.17.21", + "log4js": "^6.4.1", + "mime": "^2.5.2", + "minimatch": "^3.0.4", + "mkdirp": "^0.5.5", + "qjobs": "^1.2.0", + "range-parser": "^1.2.1", + "rimraf": "^3.0.2", + "socket.io": "^4.7.2", + "source-map": "^0.6.1", + "tmp": "^0.2.1", + "ua-parser-js": "^0.7.30", + "yargs": "^16.1.1" + }, + "bin": { + "karma": "bin/karma" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/karma-chrome-launcher": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/karma-chrome-launcher/-/karma-chrome-launcher-3.1.1.tgz", + "integrity": "sha512-hsIglcq1vtboGPAN+DGCISCFOxW+ZVnIqhDQcCMqqCp+4dmJ0Qpq5QAjkbA0X2L9Mi6OBkHi2Srrbmm7pUKkzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "which": "^1.2.1" + } + }, + "node_modules/karma-coverage": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/karma-coverage/-/karma-coverage-2.2.1.tgz", + "integrity": "sha512-yj7hbequkQP2qOSb20GuNSIyE//PgJWHwC2IydLE6XRtsnaflv+/OSGNssPjobYUlhVVagy99TQpqUt3vAUG7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "istanbul-lib-coverage": "^3.2.0", + "istanbul-lib-instrument": "^5.1.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.1", + "istanbul-reports": "^3.0.5", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/karma-coverage/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/karma-coverage/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/karma-jasmine": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/karma-jasmine/-/karma-jasmine-5.1.0.tgz", + "integrity": "sha512-i/zQLFrfEpRyQoJF9fsCdTMOF5c2dK7C7OmsuKg2D0YSsuZSfQDiLuaiktbuio6F2wiCsZSnSnieIQ0ant/uzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "jasmine-core": "^4.1.0" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "karma": "^6.0.0" + } + }, + "node_modules/karma-jasmine-html-reporter": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/karma-jasmine-html-reporter/-/karma-jasmine-html-reporter-2.0.0.tgz", + "integrity": "sha512-SB8HNNiazAHXM1vGEzf8/tSyEhkfxuDdhYdPBX2Mwgzt0OuF2gicApQ+uvXLID/gXyJQgvrM9+1/2SxZFUUDIA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "jasmine-core": "^4.0.0", + "karma": "^6.0.0", + "karma-jasmine": "^5.0.0" + } + }, + "node_modules/karma-source-map-support": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/karma-source-map-support/-/karma-source-map-support-1.4.0.tgz", + "integrity": "sha512-RsBECncGO17KAoJCYXjv+ckIz+Ii9NCi+9enk+rq6XC81ezYkb4/RHE6CTXdA7IOJqoF3wcaLfVG0CPmE5ca6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "source-map-support": "^0.5.5" + } + }, + "node_modules/karma/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/karma/node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/karma/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "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/karma/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/karma/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/karma/node_modules/tmp": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.3.tgz", + "integrity": "sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.14" + } + }, + "node_modules/karma/node_modules/yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/karma/node_modules/yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/katex": { + "version": "0.16.21", + "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.21.tgz", + "integrity": "sha512-XvqR7FgOHtWupfMiigNzmh+MgUVmDGU2kXZm899ZkPfcuoPuFxyHmXsgATDpFZDAXCI8tvinaVcDo8PIIJSo4A==", + "funding": [ + "https://opencollective.com/katex", + "https://github.com/sponsors/katex" + ], + "license": "MIT", + "optional": true, + "dependencies": { + "commander": "^8.3.0" + }, + "bin": { + "katex": "cli.js" + } + }, + "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/khroma": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/khroma/-/khroma-2.1.0.tgz", + "integrity": "sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw==", + "optional": true + }, + "node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/klona": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/klona/-/klona-2.0.6.tgz", + "integrity": "sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/kuler": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz", + "integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==", + "license": "MIT" + }, + "node_modules/layout-base": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/layout-base/-/layout-base-1.0.2.tgz", + "integrity": "sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg==", + "license": "MIT", + "optional": true + }, + "node_modules/less": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/less/-/less-4.1.3.tgz", + "integrity": "sha512-w16Xk/Ta9Hhyei0Gpz9m7VS8F28nieJaL/VyShID7cYvP6IL5oHeL6p4TXSDJqZE/lNv0oJ2pGVjJsRkfwm5FA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "copy-anything": "^2.0.1", + "parse-node-version": "^1.0.1", + "tslib": "^2.3.0" + }, + "bin": { + "lessc": "bin/lessc" + }, + "engines": { + "node": ">=6" + }, + "optionalDependencies": { + "errno": "^0.1.1", + "graceful-fs": "^4.1.2", + "image-size": "~0.5.0", + "make-dir": "^2.1.0", + "mime": "^1.4.1", + "needle": "^3.1.0", + "source-map": "~0.6.0" + } + }, + "node_modules/less-loader": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/less-loader/-/less-loader-11.1.0.tgz", + "integrity": "sha512-C+uDBV7kS7W5fJlUjq5mPBeBVhYpTIm5gB09APT9o3n/ILeaXVsiSFTbZpTJCJwQ/Crczfn3DmfQFwxYusWFug==", + "dev": true, + "license": "MIT", + "dependencies": { + "klona": "^2.0.4" + }, + "engines": { + "node": ">= 14.15.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "less": "^3.5.0 || ^4.0.0", + "webpack": "^5.0.0" + } + }, + "node_modules/less/node_modules/make-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", + "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "pify": "^4.0.1", + "semver": "^5.6.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/less/node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true, + "license": "MIT", + "optional": true, + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/less/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "license": "ISC", + "optional": true, + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/less/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "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/license-webpack-plugin": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/license-webpack-plugin/-/license-webpack-plugin-4.0.2.tgz", + "integrity": "sha512-771TFWFD70G1wLTC4oU2Cw4qvtmNrIw+wRvBtn+okgHl7slJVi7zfNcdmqDL72BojM30VNJ2UHylr1o77U37Jw==", + "dev": true, + "license": "ISC", + "dependencies": { + "webpack-sources": "^3.0.0" + }, + "peerDependenciesMeta": { + "webpack": { + "optional": true + }, + "webpack-sources": { + "optional": true + } + } + }, + "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==", + "dev": true, + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/loader-runner": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", + "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==", + "license": "MIT", + "engines": { + "node": ">=6.11.5" + } + }, + "node_modules/loader-utils": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-3.2.1.tgz", + "integrity": "sha512-ZvFw1KWS3GVyYBYb7qkmRM/WwL2TQQBxgCK62rlvm4WpVQ23Nb4tYjApUlfjrEGvOs7KHEsmyUn75OHZrJMWPw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12.13.0" + } + }, + "node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "license": "MIT" + }, + "node_modules/lodash-es": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", + "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==", + "license": "MIT", + "optional": true + }, + "node_modules/lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", + "license": "MIT" + }, + "node_modules/lodash.debounce": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", + "dev": true, + "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/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log4js": { + "version": "6.9.1", + "resolved": "https://registry.npmjs.org/log4js/-/log4js-6.9.1.tgz", + "integrity": "sha512-1somDdy9sChrr9/f4UlzhdaGfDR2c/SaD2a4T7qEkG4jTS57/B3qmnjLYePwQ8cqWnUHZI0iAKxMBpCZICiZ2g==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "date-format": "^4.0.14", + "debug": "^4.3.4", + "flatted": "^3.2.7", + "rfdc": "^1.3.0", + "streamroller": "^3.1.5" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/logform": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/logform/-/logform-2.7.0.tgz", + "integrity": "sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ==", + "license": "MIT", + "dependencies": { + "@colors/colors": "1.6.0", + "@types/triple-beam": "^1.3.2", + "fecha": "^4.2.0", + "ms": "^2.1.1", + "safe-stable-stringify": "^2.3.1", + "triple-beam": "^1.3.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/logform/node_modules/@colors/colors": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.6.0.tgz", + "integrity": "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==", + "license": "MIT", + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/long": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", + "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==", + "license": "Apache-2.0" + }, + "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/magic-string": { + "version": "0.29.0", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.29.0.tgz", + "integrity": "sha512-WcfidHrDjMY+eLjlU+8OvwREqHwpgCeKVBUpQ3OhYYuvfaYCUgcbuBzappNzZvg/v8onU3oQj+BYpkOJe9Iw4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.13" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/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/make-fetch-happen": { + "version": "10.2.1", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-10.2.1.tgz", + "integrity": "sha512-NgOPbRiaQM10DYXvN3/hhGVI2M5MtITFryzBGxHM5p4wnFxsVCbxkrBrDsk+EZ5OB4jEOT7AjDxtdF+KVEFT7w==", + "dev": true, + "license": "ISC", + "dependencies": { + "agentkeepalive": "^4.2.1", + "cacache": "^16.1.0", + "http-cache-semantics": "^4.1.0", + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.0", + "is-lambda": "^1.0.1", + "lru-cache": "^7.7.1", + "minipass": "^3.1.6", + "minipass-collect": "^1.0.2", + "minipass-fetch": "^2.0.3", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^0.6.3", + "promise-retry": "^2.0.1", + "socks-proxy-agent": "^7.0.0", + "ssri": "^9.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/make-fetch-happen/node_modules/@npmcli/fs": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-2.1.2.tgz", + "integrity": "sha512-yOJKRvohFOaLqipNtwYB9WugyZKhC/DZC4VYPmpaCzDBrA8YpK3qHZ8/HGscMnE4GqbkLNuVcCnxkeQEdGt6LQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "@gar/promisify": "^1.1.3", + "semver": "^7.3.5" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/make-fetch-happen/node_modules/cacache": { + "version": "16.1.3", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-16.1.3.tgz", + "integrity": "sha512-/+Emcj9DAXxX4cwlLmRI9c166RuL3w30zp4R7Joiv2cQTtTtA+jeuCAjH3ZlGnYS3tKENSrKhAzVVP9GVyzeYQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/fs": "^2.1.0", + "@npmcli/move-file": "^2.0.0", + "chownr": "^2.0.0", + "fs-minipass": "^2.1.0", + "glob": "^8.0.1", + "infer-owner": "^1.0.4", + "lru-cache": "^7.7.1", + "minipass": "^3.1.6", + "minipass-collect": "^1.0.2", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "mkdirp": "^1.0.4", + "p-map": "^4.0.0", + "promise-inflight": "^1.0.1", + "rimraf": "^3.0.2", + "ssri": "^9.0.0", + "tar": "^6.1.11", + "unique-filename": "^2.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/make-fetch-happen/node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/make-fetch-happen/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/make-fetch-happen/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/make-fetch-happen/node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true, + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/make-fetch-happen/node_modules/ssri": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-9.0.1.tgz", + "integrity": "sha512-o57Wcn66jMQvfHG1FlYbWeZWW/dHZhJXjpIcTfXldXEk5nz5lStPo3mK0OJQfGR3RbZUlbISexbljkJzuEj/8Q==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.1.1" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/make-fetch-happen/node_modules/unique-filename": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-2.0.1.tgz", + "integrity": "sha512-ODWHtkkdx3IAR+veKxFV+VBkUMcN+FaqzUUd7IZzt+0zhDZFPFxhlqwPF3YQvMHx1TD0tdgYl+kuPnJ8E6ql7A==", + "dev": true, + "license": "ISC", + "dependencies": { + "unique-slug": "^3.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/make-fetch-happen/node_modules/unique-slug": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-3.0.0.tgz", + "integrity": "sha512-8EyMynh679x/0gqE9fT9oilG+qEt+ibFyqjuVTsZn1+CMxH+XLlpvr2UZx4nVcCwTpx81nICr2JQFkM+HPLq4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/make-fetch-happen/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/map-obj": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-4.3.0.tgz", + "integrity": "sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/marked": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/marked/-/marked-4.3.0.tgz", + "integrity": "sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A==", + "license": "MIT", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 12" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/memfs": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/memfs/-/memfs-3.5.3.tgz", + "integrity": "sha512-UERzLsxzllchadvbPs5aolHh65ISpKpM+ccLbOJ8/vvpBKmAWf+la7dXFy7Mr0ySHbdHrFv5kGFCUHHe6GFEmw==", + "dev": true, + "license": "Unlicense", + "dependencies": { + "fs-monkey": "^1.0.4" + }, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/meow": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/meow/-/meow-9.0.0.tgz", + "integrity": "sha512-+obSblOQmRhcyBt62furQqRAQpNyWXo8BuQ5bN7dG8wmwQ+vwHKp/rCFD4CrTP8CsDQD1sjoZ94K417XEUk8IQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/minimist": "^1.2.0", + "camelcase-keys": "^6.2.2", + "decamelize": "^1.2.0", + "decamelize-keys": "^1.1.0", + "hard-rejection": "^2.1.0", + "minimist-options": "4.1.0", + "normalize-package-data": "^3.0.0", + "read-pkg-up": "^7.0.1", + "redent": "^3.0.0", + "trim-newlines": "^3.0.0", + "type-fest": "^0.18.0", + "yargs-parser": "^20.2.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/meow/node_modules/hosted-git-info": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz", + "integrity": "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==", + "dev": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/meow/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/meow/node_modules/normalize-package-data": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-3.0.3.tgz", + "integrity": "sha512-p2W1sgqij3zMMyRC067Dg16bfzVH+w7hyegmpIvZ4JNjqtGOVAIvLmjBx3yP7YTe9vKJgkoNOPjwQGogDoMXFA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "hosted-git-info": "^4.0.1", + "is-core-module": "^2.5.0", + "semver": "^7.3.4", + "validate-npm-package-license": "^3.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/meow/node_modules/type-fest": { + "version": "0.18.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.18.1.tgz", + "integrity": "sha512-OIAYXk8+ISY+qTOwkHtKqzAuxchoMiD9Udx+FSGQDuiRR+PJKJHc2NJAXlbhkGwTt/4/nKZxELY1w3ReWOL8mw==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/meow/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/meow/node_modules/yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "license": "MIT" + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/mermaid": { + "version": "9.4.3", + "resolved": "https://registry.npmjs.org/mermaid/-/mermaid-9.4.3.tgz", + "integrity": "sha512-TLkQEtqhRSuEHSE34lh5bCa94KATCyluAXmFnNI2PRZwOpXFeqiJWwZl+d2CcemE1RS6QbbueSSq9QIg8Uxcyw==", + "license": "MIT", + "optional": true, + "dependencies": { + "@braintree/sanitize-url": "^6.0.0", + "cytoscape": "^3.23.0", + "cytoscape-cose-bilkent": "^4.1.0", + "cytoscape-fcose": "^2.1.0", + "d3": "^7.4.0", + "dagre-d3-es": "7.0.9", + "dayjs": "^1.11.7", + "dompurify": "2.4.3", + "elkjs": "^0.8.2", + "khroma": "^2.0.0", + "lodash-es": "^4.17.21", + "non-layered-tidy-tree-layout": "^2.0.2", + "stylis": "^4.1.2", + "ts-dedent": "^2.2.0", + "uuid": "^9.0.0", + "web-worker": "^1.2.0" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/mini-css-extract-plugin": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.7.2.tgz", + "integrity": "sha512-EdlUizq13o0Pd+uCp+WO/JpkLvHRVGt97RqfeGhXqAcorYo1ypJSpkV+WDT0vY/kmh/p7wRdJNJtuyK540PXDw==", + "dev": true, + "license": "MIT", + "dependencies": { + "schema-utils": "^4.0.0" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" + } + }, + "node_modules/minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", + "dev": true, + "license": "ISC" + }, + "node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minimist-options": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/minimist-options/-/minimist-options-4.1.0.tgz", + "integrity": "sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "arrify": "^1.0.1", + "is-plain-obj": "^1.1.0", + "kind-of": "^6.0.3" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/minimist-options/node_modules/is-plain-obj": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", + "integrity": "sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/minipass": { + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-4.2.8.tgz", + "integrity": "sha512-fNzuVyifolSLFL4NzpF+wEF4qrgqaaKX0haXPQEdQ7NKAN+WecoKMHV09YcuL/DHxrUsYQOK3MiuDf7Ip2OXfQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-collect": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-1.0.2.tgz", + "integrity": "sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minipass-collect/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-collect/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/minipass-fetch": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-2.1.2.tgz", + "integrity": "sha512-LT49Zi2/WMROHYoqGgdlQIZh8mLPZmOrN2NdJjMXxYe4nkN6FUyuPuOAOedNJDrx0IRGg9+4guZewtp8hE6TxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^3.1.6", + "minipass-sized": "^1.0.3", + "minizlib": "^2.1.2" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + }, + "optionalDependencies": { + "encoding": "^0.1.13" + } + }, + "node_modules/minipass-fetch/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-fetch/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/minipass-flush": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz", + "integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minipass-flush/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-flush/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/minipass-json-stream": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/minipass-json-stream/-/minipass-json-stream-1.0.2.tgz", + "integrity": "sha512-myxeeTm57lYs8pH2nxPzmEEg8DGIgW+9mv6D4JZD2pa81I/OBjeU7PtICXV6c9eRGTA5JMDsuIPUZRCyBMYNhg==", + "dev": true, + "license": "MIT", + "dependencies": { + "jsonparse": "^1.3.1", + "minipass": "^3.0.0" + } + }, + "node_modules/minipass-json-stream/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-json-stream/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/minipass-pipeline": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", + "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-pipeline/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-pipeline/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/minipass-sized": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz", + "integrity": "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-sized/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-sized/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minizlib/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minizlib/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/moment": { + "version": "2.30.1", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", + "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/multicast-dns": { + "version": "7.2.5", + "resolved": "https://registry.npmjs.org/multicast-dns/-/multicast-dns-7.2.5.tgz", + "integrity": "sha512-2eznPJP8z2BFLX50tf0LuODrpINqP1RVIm/CObbTcBRITQgmC/TjcREF1NeTBzIcR5XO/ukWo+YHOjBbFwIupg==", + "dev": true, + "license": "MIT", + "dependencies": { + "dns-packet": "^5.2.2", + "thunky": "^1.0.2" + }, + "bin": { + "multicast-dns": "cli.js" + } + }, + "node_modules/mute-stream": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", + "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", + "license": "ISC" + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.8", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz", + "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==", + "dev": true, + "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/natural-compare-lite": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz", + "integrity": "sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==", + "dev": true, + "license": "MIT" + }, + "node_modules/ncp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ncp/-/ncp-2.0.0.tgz", + "integrity": "sha512-zIdGUrPRFTUELUvr3Gmc7KZ2Sw/h1PiVM0Af/oHB6zgnV1ikqSfRk+TOufi79aHYCW3NiOXmr1BP5nWbzojLaA==", + "dev": true, + "license": "MIT", + "bin": { + "ncp": "bin/ncp" + } + }, + "node_modules/needle": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/needle/-/needle-3.3.1.tgz", + "integrity": "sha512-6k0YULvhpw+RoLNiQCRKOl09Rv1dPLr8hHnVjHqdolKwDrdNyk+Hmrthi4lIGPPz3r39dLx0hsF5s40sZ3Us4Q==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "iconv-lite": "^0.6.3", + "sax": "^1.2.4" + }, + "bin": { + "needle": "bin/needle" + }, + "engines": { + "node": ">= 4.4.x" + } + }, + "node_modules/needle/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/negotiator": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", + "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "license": "MIT" + }, + "node_modules/ng2-pdf-viewer": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/ng2-pdf-viewer/-/ng2-pdf-viewer-7.0.2.tgz", + "integrity": "sha512-yAd8OddmPCnkERv3IWFyZ1GxkMx+n2fc8NQQtWCjQbhr1sKjrP0TQH41AmgudSzTtvWJw9z784V+TKIaWA0eKw==", + "license": "MIT", + "dependencies": { + "pdfjs-dist": "~2.11.338", + "tslib": "^2.0.0" + }, + "peerDependencies": { + "pdfjs-dist": "~2.11.338" + } + }, + "node_modules/ngx-markdown": { + "version": "15.1.2", + "resolved": "https://registry.npmjs.org/ngx-markdown/-/ngx-markdown-15.1.2.tgz", + "integrity": "sha512-mAUORpUnHCV4tnxEHV4oS5YEdIaolUclulCblUrvAEU3AEND8MMTxlwHujqVC2M398/aKH0SBSrjLzDbMUJCoQ==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "optionalDependencies": { + "clipboard": "^2.0.11", + "emoji-toolkit": "^7.0.0", + "katex": "^0.16.0", + "mermaid": "^9.1.2", + "prismjs": "^1.28.0" + }, + "peerDependencies": { + "@angular/common": "^15.0.0", + "@angular/core": "^15.0.0", + "@angular/platform-browser": "^15.0.0", + "@types/marked": "^4.0.3", + "marked": "^4.0.17", + "rxjs": "^6.5.3 || ^7.4.0", + "zone.js": "~0.11.4 || ~0.12.0 || ~0.13.0" + } + }, + "node_modules/nice-napi": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/nice-napi/-/nice-napi-1.0.2.tgz", + "integrity": "sha512-px/KnJAJZf5RuBGcfD+Sp2pAKq0ytz8j+1NehvgIGFkvtvFrDM3T8E4x/JJODXK9WZow8RRGrbA9QQ3hs+pDhA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "!win32" + ], + "dependencies": { + "node-addon-api": "^3.0.0", + "node-gyp-build": "^4.2.2" + } + }, + "node_modules/node-addon-api": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-3.2.1.tgz", + "integrity": "sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-forge": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", + "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==", + "dev": true, + "license": "(BSD-3-Clause OR GPL-2.0)", + "engines": { + "node": ">= 6.13.0" + } + }, + "node_modules/node-gyp": { + "version": "9.4.1", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-9.4.1.tgz", + "integrity": "sha512-OQkWKbjQKbGkMf/xqI1jjy3oCTgMKJac58G2+bjZb3fza6gW2YrCSdMQYaoTb70crvE//Gngr4f0AgVHmqHvBQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "env-paths": "^2.2.0", + "exponential-backoff": "^3.1.1", + "glob": "^7.1.4", + "graceful-fs": "^4.2.6", + "make-fetch-happen": "^10.0.3", + "nopt": "^6.0.0", + "npmlog": "^6.0.0", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "tar": "^6.1.2", + "which": "^2.0.2" + }, + "bin": { + "node-gyp": "bin/node-gyp.js" + }, + "engines": { + "node": "^12.13 || ^14.13 || >=16" + } + }, + "node_modules/node-gyp-build": { + "version": "4.8.4", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", + "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==", + "dev": true, + "license": "MIT", + "optional": true, + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, + "node_modules/node-gyp/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/node-gyp/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "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/node-gyp/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/node-gyp/node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/node-releases": { + "version": "2.0.19", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", + "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", + "license": "MIT" + }, + "node_modules/non-layered-tidy-tree-layout": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/non-layered-tidy-tree-layout/-/non-layered-tidy-tree-layout-2.0.2.tgz", + "integrity": "sha512-gkXMxRzUH+PB0ax9dUN0yYF0S25BqeAYqhgMaLUFmpXLEk7Fcu8f4emJuOAY0V8kjDICxROIKsTAKsV/v355xw==", + "license": "MIT", + "optional": true + }, + "node_modules/nopt": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-6.0.0.tgz", + "integrity": "sha512-ZwLpbTgdhuZUnZzjd7nb1ZV+4DoiC6/sfiVKok72ym/4Tlf+DFdlHYmT2JPmcNNWV6Pi3SDf1kT+A4r9RTuT9g==", + "dev": true, + "license": "ISC", + "dependencies": { + "abbrev": "^1.0.0" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/normalize-package-data": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-5.0.0.tgz", + "integrity": "sha512-h9iPVIfrVZ9wVYQnxFgtw1ugSvGEMOlyPWWtm8BMJhnwyEL/FLbYbTY3V3PpjI/BUK67n9PEWDu6eHzu1fB15Q==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "hosted-git-info": "^6.0.0", + "is-core-module": "^2.8.1", + "semver": "^7.3.5", + "validate-npm-package-license": "^3.0.4" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "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==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-range": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-bundled": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-3.0.1.tgz", + "integrity": "sha512-+AvaheE/ww1JEwRHOrn4WHNzOxGtVp+adrg2AeZS/7KuxGUYFuBta98wYpfHBbJp6Tg6j1NKSEVHNcfZzJHQwQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "npm-normalize-package-bin": "^3.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm-install-checks": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/npm-install-checks/-/npm-install-checks-6.3.0.tgz", + "integrity": "sha512-W29RiK/xtpCGqn6f3ixfRYGk+zRyr+Ew9F2E20BfXxT5/euLdA/Nm7fO7OeTGuAmTs30cpgInyJ0cYe708YTZw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "semver": "^7.1.1" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm-normalize-package-bin": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-3.0.1.tgz", + "integrity": "sha512-dMxCf+zZ+3zeQZXKxmyuCKlIDPGuv8EF940xbkC4kQVDTtqoh6rJFO+JTKSA6/Rwi0getWmtuy4Itup0AMcaDQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm-package-arg": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-10.1.0.tgz", + "integrity": "sha512-uFyyCEmgBfZTtrKk/5xDfHp6+MdrqGotX/VoOyEEl3mBwiEE5FlBaePanazJSVMPT7vKepcjYBY2ztg9A3yPIA==", + "dev": true, + "license": "ISC", + "dependencies": { + "hosted-git-info": "^6.0.0", + "proc-log": "^3.0.0", + "semver": "^7.3.5", + "validate-npm-package-name": "^5.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm-packlist": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-7.0.4.tgz", + "integrity": "sha512-d6RGEuRrNS5/N84iglPivjaJPxhDbZmlbTwTDX2IbcRHG5bZCdtysYMhwiPvcF4GisXHGn7xsxv+GQ7T/02M5Q==", + "dev": true, + "license": "ISC", + "dependencies": { + "ignore-walk": "^6.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm-pick-manifest": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/npm-pick-manifest/-/npm-pick-manifest-8.0.1.tgz", + "integrity": "sha512-mRtvlBjTsJvfCCdmPtiu2bdlx8d/KXtF7yNXNWe7G0Z36qWA9Ny5zXsI2PfBZEv7SXgoxTmNaTzGSbbzDZChoA==", + "dev": true, + "license": "ISC", + "dependencies": { + "npm-install-checks": "^6.0.0", + "npm-normalize-package-bin": "^3.0.0", + "npm-package-arg": "^10.0.0", + "semver": "^7.3.5" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm-registry-fetch": { + "version": "14.0.5", + "resolved": "https://registry.npmjs.org/npm-registry-fetch/-/npm-registry-fetch-14.0.5.tgz", + "integrity": "sha512-kIDMIo4aBm6xg7jOttupWZamsZRkAqMqwqqbVXnUqstY5+tapvv6bkH/qMR76jdgV+YljEUCyWx3hRYMrJiAgA==", + "dev": true, + "license": "ISC", + "dependencies": { + "make-fetch-happen": "^11.0.0", + "minipass": "^5.0.0", + "minipass-fetch": "^3.0.0", + "minipass-json-stream": "^1.0.1", + "minizlib": "^2.1.2", + "npm-package-arg": "^10.0.0", + "proc-log": "^3.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm-registry-fetch/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/npm-registry-fetch/node_modules/make-fetch-happen": { + "version": "11.1.1", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-11.1.1.tgz", + "integrity": "sha512-rLWS7GCSTcEujjVBs2YqG7Y4643u8ucvCJeSRqiLYhesrDuzeuFIk37xREzAsfQaqzl8b9rNCE4m6J8tvX4Q8w==", + "dev": true, + "license": "ISC", + "dependencies": { + "agentkeepalive": "^4.2.1", + "cacache": "^17.0.0", + "http-cache-semantics": "^4.1.1", + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.0", + "is-lambda": "^1.0.1", + "lru-cache": "^7.7.1", + "minipass": "^5.0.0", + "minipass-fetch": "^3.0.0", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^0.6.3", + "promise-retry": "^2.0.1", + "socks-proxy-agent": "^7.0.0", + "ssri": "^10.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm-registry-fetch/node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=8" + } + }, + "node_modules/npm-registry-fetch/node_modules/minipass-fetch": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-3.0.5.tgz", + "integrity": "sha512-2N8elDQAtSnFV0Dk7gt15KHsS0Fyz6CbYZ360h0WTYV1Ty46li3rAXVOQj1THMNLdmrD9Vt5pBPtWtVkpwGBqg==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^7.0.3", + "minipass-sized": "^1.0.3", + "minizlib": "^2.1.2" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + }, + "optionalDependencies": { + "encoding": "^0.1.13" + } + }, + "node_modules/npm-registry-fetch/node_modules/minipass-fetch/node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npmlog": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-6.0.2.tgz", + "integrity": "sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg==", + "deprecated": "This package is no longer supported.", + "dev": true, + "license": "ISC", + "dependencies": { + "are-we-there-yet": "^3.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^4.0.3", + "set-blocking": "^2.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "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==", + "dev": true, + "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==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/object-inspect": { + "version": "1.13.3", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.3.tgz", + "integrity": "sha512-kDCGIbxkDSXE3euJZZXzc6to7fCrKHNI/hSRQnRuQ+BWjFNzZwiFF8fj/6o2t2G9/jTj8PSIYTfCLelLZEeRpA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/obuf": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz", + "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==", + "dev": true, + "license": "MIT" + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/on-headers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", + "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "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/one-time": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/one-time/-/one-time-1.0.0.tgz", + "integrity": "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==", + "license": "MIT", + "dependencies": { + "fn.name": "1.x.x" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/open": { + "version": "8.4.1", + "resolved": "https://registry.npmjs.org/open/-/open-8.4.1.tgz", + "integrity": "sha512-/4b7qZNhv6Uhd7jjnREh1NjnPxlTq+XNWPG88Ydkj5AILcA5m3ajvcg57pB24EQjKv0dK62XnDqk9c/hkIG5Kg==", + "license": "MIT", + "dependencies": { + "define-lazy-prop": "^2.0.0", + "is-docker": "^2.1.1", + "is-wsl": "^2.2.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "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/ora": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", + "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", + "license": "MIT", + "dependencies": { + "bl": "^4.1.0", + "chalk": "^4.1.0", + "cli-cursor": "^3.1.0", + "cli-spinners": "^2.5.0", + "is-interactive": "^1.0.0", + "is-unicode-supported": "^0.1.0", + "log-symbols": "^4.1.0", + "strip-ansi": "^6.0.0", + "wcwidth": "^1.0.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-map": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", + "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "aggregate-error": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-retry": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz", + "integrity": "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/retry": "0.12.0", + "retry": "^0.13.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-retry/node_modules/retry": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, + "node_modules/pacote": { + "version": "15.0.8", + "resolved": "https://registry.npmjs.org/pacote/-/pacote-15.0.8.tgz", + "integrity": "sha512-UlcumB/XS6xyyIMwg/WwMAyUmga+RivB5KgkRwA1hZNtrx+0Bt41KxHCvg1kr0pZ/ZeD8qjhW4fph6VaYRCbLw==", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/git": "^4.0.0", + "@npmcli/installed-package-contents": "^2.0.1", + "@npmcli/promise-spawn": "^6.0.1", + "@npmcli/run-script": "^6.0.0", + "cacache": "^17.0.0", + "fs-minipass": "^3.0.0", + "minipass": "^4.0.0", + "npm-package-arg": "^10.0.0", + "npm-packlist": "^7.0.0", + "npm-pick-manifest": "^8.0.0", + "npm-registry-fetch": "^14.0.0", + "proc-log": "^3.0.0", + "promise-retry": "^2.0.1", + "read-package-json": "^6.0.0", + "read-package-json-fast": "^3.0.0", + "ssri": "^10.0.0", + "tar": "^6.1.11" + }, + "bin": { + "pacote": "lib/bin.js" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "dev": true, + "license": "(MIT AND Zlib)" + }, + "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-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parse-node-version": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parse-node-version/-/parse-node-version-1.0.1.tgz", + "integrity": "sha512-3YHlOa/JgH6Mnpr05jP9eDG254US9ek25LyIxZlDItp2iJtwyaXQb57lBYLdT3MowkUFYEV2XXNAYIPlESvJlA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/parse5": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.2.1.tgz", + "integrity": "sha512-BuBYQYlv1ckiPdQi/ohiivi9Sagc9JG+Ozs0r7b/0iK3sKmrb0b9FdWdBbOdx6hBCM/F9Ir82ofnBhtZOjCRPQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "entities": "^4.5.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-html-rewriting-stream": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/parse5-html-rewriting-stream/-/parse5-html-rewriting-stream-7.0.0.tgz", + "integrity": "sha512-mazCyGWkmCRWDI15Zp+UiCqMp/0dgEmkZRvhlsqqKYr4SsVm/TvnSpD9fCvqCA2zoWJcfRym846ejWBBHRiYEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^4.3.0", + "parse5": "^7.0.0", + "parse5-sax-parser": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-html-rewriting-stream/node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/parse5-htmlparser2-tree-adapter": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-6.0.1.tgz", + "integrity": "sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "parse5": "^6.0.1" + } + }, + "node_modules/parse5-htmlparser2-tree-adapter/node_modules/parse5": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz", + "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==", + "dev": true, + "license": "MIT" + }, + "node_modules/parse5-sax-parser": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/parse5-sax-parser/-/parse5-sax-parser-7.0.0.tgz", + "integrity": "sha512-5A+v2SNsq8T6/mG3ahcz8ZtQ0OUFTatxPbeidoMB7tkJSGDY3tdfl4MHovtLQHkEn5CGxijNWRQHhRQ6IRpXKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5/node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "devOptional": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "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==", + "dev": true, + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": 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/path-scurry/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/path-scurry/node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "dev": true, + "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/pdfjs-dist": { + "version": "2.11.338", + "resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-2.11.338.tgz", + "integrity": "sha512-Ti5VTB0VvSdtTtc7TG71ghMx0SEuNcEs4ghVuZxW0p6OqLjMc0xekZV1B+MmlxEG2Du2e5jgazucWIG/SXTcdA==", + "license": "Apache-2.0", + "peerDependencies": { + "worker-loader": "^3.0.8" + }, + "peerDependenciesMeta": { + "worker-loader": { + "optional": true + } + } + }, + "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.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/pirates": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", + "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/piscina": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/piscina/-/piscina-3.2.0.tgz", + "integrity": "sha512-yn/jMdHRw+q2ZJhFhyqsmANcbF6V2QwmD84c6xRau+QpQOmtrBCoRGdvTfeuFDYXB5W2m6MfLkjkvQa9lUSmIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eventemitter-asyncresource": "^1.0.0", + "hdr-histogram-js": "^2.0.1", + "hdr-histogram-percentiles-obj": "^3.0.0" + }, + "optionalDependencies": { + "nice-napi": "^1.0.2" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/postcss": { + "version": "8.5.3", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz", + "integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.8", + "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==", + "dev": true, + "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.0.1", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz", + "integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==", + "dev": true, + "dependencies": { + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": "^8.4.21" + } + }, + "node_modules/postcss-load-config": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz", + "integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "lilconfig": "^3.0.0", + "yaml": "^2.3.4" + }, + "engines": { + "node": ">= 14" + }, + "peerDependencies": { + "postcss": ">=8.0.9", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "postcss": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/postcss-load-config/node_modules/yaml": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.7.0.tgz", + "integrity": "sha512-+hSoy/QHluxmC9kCIJyL/uyFmLmc+e5CFR5Wa+bpIhIj85LVb9ZH2nVnqrHoSvKogwODv0ClqZkmiSSaIH5LTA==", + "dev": true, + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/postcss-loader": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/postcss-loader/-/postcss-loader-7.0.2.tgz", + "integrity": "sha512-fUJzV/QH7NXUAqV8dWJ9Lg4aTkDCezpTS5HgJ2DvqznexTbSTxgi/dTECvTZ15BwKTtk8G/bqI/QTu2HPd3ZCg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cosmiconfig": "^7.0.0", + "klona": "^2.0.5", + "semver": "^7.3.8" + }, + "engines": { + "node": ">= 14.15.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "postcss": "^7.0.0 || ^8.0.1", + "webpack": "^5.0.0" + } + }, + "node_modules/postcss-modules-extract-imports": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.1.0.tgz", + "integrity": "sha512-k3kNe0aNFQDAZGbin48pL2VNidTF0w4/eASDsxlyspobzU3wZQLOGj7L9gfRe0Jo9/4uud09DsjFNH7winGv8Q==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-local-by-default": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.2.0.tgz", + "integrity": "sha512-5kcJm/zk+GJDSfw+V/42fJ5fhjL5YbFDl8nVdXkJPLLW+Vf9mTD5Xe0wqIaDnLuL2U6cDNpTr+UQ+v2HWIBhzw==", + "dev": true, + "license": "MIT", + "dependencies": { + "icss-utils": "^5.0.0", + "postcss-selector-parser": "^7.0.0", + "postcss-value-parser": "^4.1.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-scope": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.2.1.tgz", + "integrity": "sha512-m9jZstCVaqGjTAuny8MdgE88scJnCiQSlSrOWcTQgM2t32UBe+MUmFSO5t7VMSfAf/FJKImAxBav8ooCHJXCJA==", + "dev": true, + "license": "ISC", + "dependencies": { + "postcss-selector-parser": "^7.0.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-values": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz", + "integrity": "sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "icss-utils": "^5.0.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "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==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "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==", + "dev": true, + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-selector-parser": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.0.0.tgz", + "integrity": "sha512-9RbEr1Y7FFfptd/1eEdntyjMwLeghW1bHX9GWjXo19vx4ytPQhANltvVxDggzJl7mnWM+dX28kb6cyS/4iQjlQ==", + "dev": true, + "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==", + "dev": true, + "license": "MIT" + }, + "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/prettier": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.1.tgz", + "integrity": "sha512-hPpFQvHwL3Qv5AdRvBFMhnKo4tYxp0ReXiPn2bxkiohEX6mBeBwEpBSQTkD458RaaDKQMYSp4hX4UtfUTA5wDw==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prettier-linter-helpers": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz", + "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-diff": "^1.1.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/pretty-bytes": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz", + "integrity": "sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/prismjs": { + "version": "1.29.0", + "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.29.0.tgz", + "integrity": "sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/proc-log": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-3.0.0.tgz", + "integrity": "sha512-++Vn7NS4Xf9NacaU9Xq3URUuqZETPsf8L4j5/ckhaRYsfPeRyzGw+iDjFhV/Jr3uNmTvvddEJFWh5R1gRgUH8A==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true, + "license": "MIT" + }, + "node_modules/promise-inflight": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", + "integrity": "sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/promise-retry": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", + "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "err-code": "^2.0.2", + "retry": "^0.12.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/protobufjs": { + "version": "6.11.4", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.11.4.tgz", + "integrity": "sha512-5kQWPaJHi1WoCpjTGszzQ32PG2F4+wRY6BmAT4Vfw56Q2FZ4YZzK20xUYQH4YkfehY1e6QSICrJquM6xXZNcrw==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/long": "^4.0.1", + "@types/node": ">=13.7.0", + "long": "^4.0.0" + }, + "bin": { + "pbjs": "bin/pbjs", + "pbts": "bin/pbts" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/proxy-addr/node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/prr": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", + "integrity": "sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/qjobs": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/qjobs/-/qjobs-1.2.0.tgz", + "integrity": "sha512-8YOJEHtxpySA3fFDyCRxA+UUV+fA+rTWnuWvylOK/NCjhY+b4ocCtmu8TtsWb+mYeU+GCHf/S66KZF/AsteKHg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.9" + } + }, + "node_modules/qs": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/quick-lru": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-4.0.1.tgz", + "integrity": "sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "dev": true, + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/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==", + "dev": true, + "dependencies": { + "pify": "^2.3.0" + } + }, + "node_modules/read-cache/node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/read-package-json": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/read-package-json/-/read-package-json-6.0.4.tgz", + "integrity": "sha512-AEtWXYfopBj2z5N5PbkAOeNHRPUg5q+Nen7QLxV8M2zJq1ym6/lCz3fYNTCXe19puu2d06jfHhrP7v/S2PtMMw==", + "deprecated": "This package is no longer supported. Please use @npmcli/package-json instead.", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^10.2.2", + "json-parse-even-better-errors": "^3.0.0", + "normalize-package-data": "^5.0.0", + "npm-normalize-package-bin": "^3.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/read-package-json-fast": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/read-package-json-fast/-/read-package-json-fast-3.0.2.tgz", + "integrity": "sha512-0J+Msgym3vrLOUB3hzQCuZHII0xkNGCtz/HJH9xZshwv9DbDwkw1KaE3gx/e2J5rpEY5rtOy6cyhKOPrkP7FZw==", + "dev": true, + "license": "ISC", + "dependencies": { + "json-parse-even-better-errors": "^3.0.0", + "npm-normalize-package-bin": "^3.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/read-package-json-fast/node_modules/json-parse-even-better-errors": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-3.0.2.tgz", + "integrity": "sha512-fi0NG4bPjCHunUJffmLd0gxssIgkNmArMvis4iNah6Owg1MCJjWhEcDLmsK6iGkJq3tHwbDkTlce70/tmXN4cQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/read-package-json/node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": 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/read-package-json/node_modules/json-parse-even-better-errors": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-3.0.2.tgz", + "integrity": "sha512-fi0NG4bPjCHunUJffmLd0gxssIgkNmArMvis4iNah6Owg1MCJjWhEcDLmsK6iGkJq3tHwbDkTlce70/tmXN4cQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/read-package-json/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/read-package-json/node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/read-pkg": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz", + "integrity": "sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/normalize-package-data": "^2.4.0", + "normalize-package-data": "^2.5.0", + "parse-json": "^5.0.0", + "type-fest": "^0.6.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/read-pkg-up": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-7.0.1.tgz", + "integrity": "sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^4.1.0", + "read-pkg": "^5.2.0", + "type-fest": "^0.8.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-pkg-up/node_modules/type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=8" + } + }, + "node_modules/read-pkg/node_modules/hosted-git-info": { + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", + "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", + "dev": true, + "license": "ISC" + }, + "node_modules/read-pkg/node_modules/normalize-package-data": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", + "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "hosted-git-info": "^2.1.4", + "resolve": "^1.10.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + } + }, + "node_modules/read-pkg/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/read-pkg/node_modules/type-fest": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.6.0.tgz", + "integrity": "sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=8" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/reflect-metadata": { + "version": "0.1.14", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.14.tgz", + "integrity": "sha512-ZhYeb6nRaXCfhnndflDK8qI6ZQ/YcWZCISRAWICW9XYqMUwjZM9Z0DveWX/ABN01oxSHwVxKQmxeYZSsm0jh5A==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/regenerate": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", + "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==", + "dev": true, + "license": "MIT" + }, + "node_modules/regenerate-unicode-properties": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.2.0.tgz", + "integrity": "sha512-DqHn3DwbmmPVzeKj9woBadqmXxLvQoQIwu7nopMc72ztvxVmVk2SBhSnx67zuye5TP+lJsb/TBQsjLKhnDf3MA==", + "dev": true, + "license": "MIT", + "dependencies": { + "regenerate": "^1.4.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/regenerator-runtime": { + "version": "0.13.11", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", + "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==", + "dev": true, + "license": "MIT" + }, + "node_modules/regenerator-transform": { + "version": "0.15.2", + "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.15.2.tgz", + "integrity": "sha512-hfMp2BoF0qOk3uc5V20ALGDS2ddjQaLrdl7xrGXvAIow7qeWRM2VA2HuCHkUKk9slq3VwEwLNK3DFBqDfPGYtg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.8.4" + } + }, + "node_modules/regex-parser": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/regex-parser/-/regex-parser-2.3.0.tgz", + "integrity": "sha512-TVILVSz2jY5D47F4mA4MppkBrafEaiUWJO/TcZHEIuI13AqoZMkK1WMA4Om1YkYbTx+9Ki1/tSUXbceyr9saRg==", + "dev": true, + "license": "MIT" + }, + "node_modules/regexpp": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz", + "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + } + }, + "node_modules/regexpu-core": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-6.2.0.tgz", + "integrity": "sha512-H66BPQMrv+V16t8xtmq+UC0CBpiTBA60V8ibS1QVReIp8T1z8hwFxqcGzm9K6lgsN7sB5edVH8a+ze6Fqm4weA==", + "dev": true, + "license": "MIT", + "dependencies": { + "regenerate": "^1.4.2", + "regenerate-unicode-properties": "^10.2.0", + "regjsgen": "^0.8.0", + "regjsparser": "^0.12.0", + "unicode-match-property-ecmascript": "^2.0.0", + "unicode-match-property-value-ecmascript": "^2.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/regjsgen": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.8.0.tgz", + "integrity": "sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/regjsparser": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.12.0.tgz", + "integrity": "sha512-cnE+y8bz4NhMjISKbgeVJtqNbtf5QpjZP+Bslo+UqkIt9QPnX9q095eiRRASJG1/tz6dlNr6Z5NsBiWYokp6EQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "jsesc": "~3.0.2" + }, + "bin": { + "regjsparser": "bin/parser" + } + }, + "node_modules/regjsparser/node_modules/jsesc": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.0.2.tgz", + "integrity": "sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "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/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/resolve": { + "version": "1.22.1", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz", + "integrity": "sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.9.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-url-loader": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-url-loader/-/resolve-url-loader-5.0.0.tgz", + "integrity": "sha512-uZtduh8/8srhBoMx//5bwqjQ+rfYOUq8zC9NrMUGtjBiGTtFJM42s58/36+hTqeqINcnYe08Nj3LkK9lW4N8Xg==", + "dev": true, + "license": "MIT", + "dependencies": { + "adjust-sourcemap-loader": "^4.0.0", + "convert-source-map": "^1.7.0", + "loader-utils": "^2.0.0", + "postcss": "^8.2.14", + "source-map": "0.6.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/resolve-url-loader/node_modules/loader-utils": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", + "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" + }, + "engines": { + "node": ">=8.9.0" + } + }, + "node_modules/resolve-url-loader/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "license": "MIT", + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "dev": true, + "license": "MIT" + }, + "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/rimraf/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/rimraf/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "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/rimraf/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/robust-predicates": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.2.tgz", + "integrity": "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==", + "license": "Unlicense", + "optional": true + }, + "node_modules/run-async": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", + "integrity": "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==", + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/rw": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz", + "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==", + "license": "BSD-3-Clause", + "optional": true + }, + "node_modules/rxfire": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/rxfire/-/rxfire-6.0.3.tgz", + "integrity": "sha512-77nkyffHh7jgfi1YA/N9RI+kWxYpgKk6GRML1lyersvaqbJt4hkvWwk1rWib9Rb5Lr5mT+Ha45lu7nM79sJCZA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^1.9.0 || ~2.1.0" + }, + "peerDependencies": { + "firebase": "^9.0.0", + "rxjs": "^6.0.0 || ^7.0.0" + } + }, + "node_modules/rxfire/node_modules/tslib": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.1.0.tgz", + "integrity": "sha512-hcVC3wYEziELGGmEEXue7D75zbwIIVUMWAVbHItGPx0ziyXxrOMQx4rQEVEV45Ut/1IotuEvwqPopzIOkDMf0A==", + "license": "0BSD" + }, + "node_modules/rxjs": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-stable-stringify": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/safevalues": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/safevalues/-/safevalues-0.3.4.tgz", + "integrity": "sha512-LRneZZRXNgjzwG4bDQdOTSbze3fHm1EAKN/8bePxnlEZiBmkYEDggaHbuvHI9/hoqHbGfsEA7tWS9GhYHZBBsw==", + "license": "Apache-2.0" + }, + "node_modules/sass": { + "version": "1.58.1", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.58.1.tgz", + "integrity": "sha512-bnINi6nPXbP1XNRaranMFEBZWUfdW/AF16Ql5+ypRxfTvCRTTKrLsMIakyDcayUt2t/RZotmL4kgJwNH5xO+bg==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": ">=3.0.0 <4.0.0", + "immutable": "^4.0.0", + "source-map-js": ">=0.6.2 <2.0.0" + }, + "bin": { + "sass": "sass.js" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/sass-loader": { + "version": "13.2.0", + "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-13.2.0.tgz", + "integrity": "sha512-JWEp48djQA4nbZxmgC02/Wh0eroSUutulROUusYJO9P9zltRbNN80JCBHqRGzjd4cmZCa/r88xgfkjGD0TXsHg==", + "dev": true, + "license": "MIT", + "dependencies": { + "klona": "^2.0.4", + "neo-async": "^2.6.2" + }, + "engines": { + "node": ">= 14.15.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "fibers": ">= 3.1.0", + "node-sass": "^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0", + "sass": "^1.3.0", + "sass-embedded": "*", + "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "fibers": { + "optional": true + }, + "node-sass": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + } + } + }, + "node_modules/sax": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz", + "integrity": "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==", + "dev": true, + "license": "ISC", + "optional": true + }, + "node_modules/schema-utils": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.0.tgz", + "integrity": "sha512-Gf9qqc58SpCA/xdziiHz35F4GNIWYWZrEshUc/G/r5BnLph6xpKuLeoJoQuj5WfBIx/eQLf+hmVPYHaxJu7V2g==", + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/select": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/select/-/select-1.1.2.tgz", + "integrity": "sha512-OwpTSOfy6xSs1+pwcNrv0RBMOzI39Lp3qQKUTPVVPRjCdNa5JH/oPRiqsesIskK8TVgmRiHwO4KXlV2Li9dANA==", + "license": "MIT" + }, + "node_modules/select-hose": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz", + "integrity": "sha512-mEugaLK+YfkijB4fx0e6kImuJdCIt2LxCRcbEYPqRGCs4F2ogyfZU5IAZRdjCP8JPq2AtdNoC/Dux63d9Kiryg==", + "dev": true, + "license": "MIT" + }, + "node_modules/selfsigned": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-2.4.1.tgz", + "integrity": "sha512-th5B4L2U+eGLq1TVh7zNRGBapioSORUeymIydxgFpwww9d2qyKvtuPU2jJuHvYAwwqi2Y596QBL3eEqcPEYL8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node-forge": "^1.3.0", + "node-forge": "^1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/semver": { + "version": "7.5.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.3.tgz", + "integrity": "sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ==", + "license": "ISC", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/semver/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/semver/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC" + }, + "node_modules/send": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/send/node_modules/debug/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "license": "MIT" + }, + "node_modules/send/node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/send/node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/serialize-javascript": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", + "license": "BSD-3-Clause", + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/serve-index": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/serve-index/-/serve-index-1.9.1.tgz", + "integrity": "sha512-pXHfKNP4qujrtteMrSBb0rc8HJ9Ms/GrXwcUtUtD5s4ewDJI8bT3Cz2zTVRMKtri49pLx2e0Ya8ziP5Ya2pZZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "accepts": "~1.3.4", + "batch": "0.6.1", + "debug": "2.6.9", + "escape-html": "~1.0.3", + "http-errors": "~1.6.2", + "mime-types": "~2.1.17", + "parseurl": "~1.3.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/serve-index/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/serve-index/node_modules/depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/serve-index/node_modules/http-errors": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", + "integrity": "sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.0", + "statuses": ">= 1.4.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/serve-index/node_modules/inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==", + "dev": true, + "license": "ISC" + }, + "node_modules/serve-index/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "license": "MIT" + }, + "node_modules/serve-index/node_modules/setprototypeof": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", + "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/serve-static": { + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.19.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/serve-static/node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "dev": true, + "license": "ISC" + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "dev": true, + "license": "ISC" + }, + "node_modules/shallow-clone": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", + "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", + "dev": true, + "license": "MIT", + "dependencies": { + "kind-of": "^6.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "license": "ISC" + }, + "node_modules/simple-swizzle": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", + "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.3.1" + } + }, + "node_modules/simple-swizzle/node_modules/is-arrayish": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", + "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==", + "license": "MIT" + }, + "node_modules/slash": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-4.0.0.tgz", + "integrity": "sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socket.io": { + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.1.tgz", + "integrity": "sha512-oZ7iUCxph8WYRHHcjBEc9unw3adt5CmSNlppj/5Q4k2RIrhl8Z5yY2Xr4j9zj0+wzVZ0bxmYoGSzKJnRl6A4yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "accepts": "~1.3.4", + "base64id": "~2.0.0", + "cors": "~2.8.5", + "debug": "~4.3.2", + "engine.io": "~6.6.0", + "socket.io-adapter": "~2.5.2", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/socket.io-adapter": { + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.5.tgz", + "integrity": "sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "~4.3.4", + "ws": "~8.17.1" + } + }, + "node_modules/socket.io-adapter/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io-parser": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", + "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==", + "dev": true, + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-parser/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/sockjs": { + "version": "0.3.24", + "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.24.tgz", + "integrity": "sha512-GJgLTZ7vYb/JtPSSZ10hsOYIvEYsjbNU+zPdIHcUaWVNUEPivzxku31865sSSud0Da0W4lEeOPlmw93zLQchuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "faye-websocket": "^0.11.3", + "uuid": "^8.3.2", + "websocket-driver": "^0.7.4" + } + }, + "node_modules/sockjs/node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "dev": true, + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/socks": { + "version": "2.8.3", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.3.tgz", + "integrity": "sha512-l5x7VUUWbjVFbafGLxPWkYsHIhEvmF85tbIeFZWc8ZPtoMyybuEhL7Jye/ooC4/d48FgOjSJXgsF/AJPYCW8Zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "ip-address": "^9.0.5", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-7.0.0.tgz", + "integrity": "sha512-Fgl0YPZ902wEsAyiQ+idGd1A7rSFx/ayC1CQVMw5P+EQx2V0SgpGtf6OKFhVjPflPUl9YMmEOnmfjCdMUsygww==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^6.0.2", + "debug": "^4.3.3", + "socks": "^2.6.2" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/source-map": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", + "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", + "license": "BSD-3-Clause", + "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==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-loader": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/source-map-loader/-/source-map-loader-4.0.1.tgz", + "integrity": "sha512-oqXpzDIByKONVY8g1NUPOTQhe0UTU5bWUl32GSkqK2LjJj0HmwTMVKxcUip0RgAYhY1mqgOxjbQM48a0mmeNfA==", + "dev": true, + "license": "MIT", + "dependencies": { + "abab": "^2.0.6", + "iconv-lite": "^0.6.3", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": ">= 14.15.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.72.1" + } + }, + "node_modules/source-map-loader/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/source-map-support/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/spdx-correct": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", + "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-exceptions": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", + "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==", + "dev": true, + "license": "CC-BY-3.0" + }, + "node_modules/spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-license-ids": { + "version": "3.0.21", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.21.tgz", + "integrity": "sha512-Bvg/8F5XephndSK3JffaRqdT+gyhfqIPwDHpX80tJrF8QQRYMo8sNMeaZ2Dp5+jhwKnUmIOyFFQfHRkjJm5nXg==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/spdy": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/spdy/-/spdy-4.0.2.tgz", + "integrity": "sha512-r46gZQZQV+Kl9oItvl1JZZqJKGr+oEkB08A6BzkiR7593/7IbtuncXHd2YoYeTsG4157ZssMu9KYvUHLcjcDoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.0", + "handle-thing": "^2.0.0", + "http-deceiver": "^1.2.7", + "select-hose": "^2.0.0", + "spdy-transport": "^3.0.0" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/spdy-transport": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/spdy-transport/-/spdy-transport-3.0.0.tgz", + "integrity": "sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.0", + "detect-node": "^2.0.4", + "hpack.js": "^2.1.6", + "obuf": "^1.1.2", + "readable-stream": "^3.0.6", + "wbuf": "^1.7.3" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/ssri": { + "version": "10.0.6", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-10.0.6.tgz", + "integrity": "sha512-MGrFH9Z4NP9Iyhqn16sDtBpRRNJ0Y2hNa6D65h736fVSaPCHr4DM4sWUNvVaSuC+0OBGhwsrydQwmgfg5LncqQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/ssri/node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/stack-trace": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", + "integrity": "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/streamroller": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/streamroller/-/streamroller-3.1.5.tgz", + "integrity": "sha512-KFxaM7XT+irxvdqSP1LGLgNWbYN7ay5owZ3r/8t77p+EtSUAfUgtl7be3xtqtOmGUl9K9YPO2ca8133RlTjvKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "date-format": "^4.0.14", + "debug": "^4.3.4", + "fs-extra": "^8.1.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "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/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "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-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "min-indent": "^1.0.0" + }, + "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/stylis": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.5.tgz", + "integrity": "sha512-K7npNOKGRYuhAFFzkzMGfxFDpN6gDwf8hcMiE+uveTVbBgm93HrNP3ZDUpKqzZ4pG7TP6fmb+EMAQPjq9FqqvA==", + "license": "MIT", + "optional": true + }, + "node_modules/sucrase": { + "version": "3.35.0", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz", + "integrity": "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "glob": "^10.3.10", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/sucrase/node_modules/@jridgewell/gen-mapping": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", + "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", + "dev": true, + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/sucrase/node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/sucrase/node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "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/sucrase/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/sucrase/node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "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==", + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/symbol-observable": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-4.0.0.tgz", + "integrity": "sha512-b19dMThMV4HVFynSAM1++gBHAbk2Tc/osgLIBZMKsyqh34jb2e8Os7T6ZW/Bt3pJFdBTd2JwAnAAEQV7rSNvcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/synckit": { + "version": "0.8.8", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.8.8.tgz", + "integrity": "sha512-HwOKAP7Wc5aRGYdKH+dw0PRRpbO841v2DENBtjnR5HFWoiNByAl7vrx3p0G/rCyYXQsrxqtX48TImFtPcIHSpQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@pkgr/core": "^0.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/unts" + } + }, + "node_modules/tailwindcss": { + "version": "3.4.17", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz", + "integrity": "sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==", + "dev": true, + "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.6", + "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", + "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/node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/tailwindcss/node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/tailwindcss/node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/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==", + "dev": true, + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/tailwindcss/node_modules/resolve": { + "version": "1.22.10", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", + "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", + "dev": true, + "dependencies": { + "is-core-module": "^2.16.0", + "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/tapable": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", + "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/tar": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "dev": true, + "license": "ISC", + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/tar/node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/tar/node_modules/fs-minipass/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tar/node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=8" + } + }, + "node_modules/tar/node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true, + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/tar/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/terser": { + "version": "5.16.3", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.16.3.tgz", + "integrity": "sha512-v8wWLaS/xt3nE9dgKEWhNUFP6q4kngO5B8eYFUuebsu7Dw/UNAnpUod6UHo04jSSkv8TzKHjZDSd7EXdDQAl8Q==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@jridgewell/source-map": "^0.3.2", + "acorn": "^8.5.0", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser-webpack-plugin": { + "version": "5.3.11", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.11.tgz", + "integrity": "sha512-RVCsMfuD0+cTt3EwX8hSl2Ks56EbFHWmhluwcqoPKtBnfjiT6olaq7PRIRfhyU8nnC2MrnDrBLfrD/RGE+cVXQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "jest-worker": "^27.4.5", + "schema-utils": "^4.3.0", + "serialize-javascript": "^6.0.2", + "terser": "^5.31.1" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "uglify-js": { + "optional": true + } + } + }, + "node_modules/terser-webpack-plugin/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "license": "MIT" + }, + "node_modules/terser-webpack-plugin/node_modules/terser": { + "version": "5.37.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.37.0.tgz", + "integrity": "sha512-B8wRRkmre4ERucLM/uXx4MOV5cbnOlVAqUst+1+iLKPI0dOgFO28f84ptoQt9HEI537PMzfYa/d+GEPKTRXmYA==", + "license": "BSD-2-Clause", + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.8.2", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/test-exclude/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/test-exclude/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "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/test-exclude/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/text-hex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", + "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==", + "license": "MIT" + }, + "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==", + "dev": true, + "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==", + "dev": true, + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", + "license": "MIT" + }, + "node_modules/thunky": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz", + "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tiny-emitter": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/tiny-emitter/-/tiny-emitter-2.1.0.tgz", + "integrity": "sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q==", + "license": "MIT" + }, + "node_modules/tmp": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "license": "MIT", + "dependencies": { + "os-tmpdir": "~1.0.2" + }, + "engines": { + "node": ">=0.6.0" + } + }, + "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==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "license": "MIT", + "bin": { + "tree-kill": "cli.js" + } + }, + "node_modules/trim-newlines": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-3.0.1.tgz", + "integrity": "sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/triple-beam": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.4.1.tgz", + "integrity": "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==", + "license": "MIT", + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/ts-dedent": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/ts-dedent/-/ts-dedent-2.2.0.tgz", + "integrity": "sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=6.10" + } + }, + "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==", + "dev": true + }, + "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/tsutils": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", + "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^1.8.1" + }, + "engines": { + "node": ">= 6" + }, + "peerDependencies": { + "typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta" + } + }, + "node_modules/tsutils/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true, + "license": "0BSD" + }, + "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.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typed-assert": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/typed-assert/-/typed-assert-1.0.9.tgz", + "integrity": "sha512-KNNZtayBCtmnNmbo5mG47p1XsCyrx6iVqomjcZnec/1Y5GGARaxPs6r49RnSPeUP3YjNYiU9sQHAtY4BBvnZwg==", + "dev": true, + "license": "MIT" + }, + "node_modules/typescript": { + "version": "4.9.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", + "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + }, + "node_modules/ua-parser-js": { + "version": "0.7.40", + "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.40.tgz", + "integrity": "sha512-us1E3K+3jJppDBa3Tl0L3MOJiGhe1C6P0+nIvQAFYbxlMAx0h81eOwLmU57xgqToduDDPx3y5QsdjPfDu+FgOQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/ua-parser-js" + }, + { + "type": "paypal", + "url": "https://paypal.me/faisalman" + }, + { + "type": "github", + "url": "https://github.com/sponsors/faisalman" + } + ], + "license": "MIT", + "bin": { + "ua-parser-js": "script/cli.js" + }, + "engines": { + "node": "*" + } + }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "license": "MIT" + }, + "node_modules/unicode-canonical-property-names-ecmascript": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.1.tgz", + "integrity": "sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-ecmascript": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz", + "integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "unicode-canonical-property-names-ecmascript": "^2.0.0", + "unicode-property-aliases-ecmascript": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-value-ecmascript": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.2.0.tgz", + "integrity": "sha512-4IehN3V/+kkr5YeSSDDQG8QLqO26XpL2XP3GQtqwlT/QYSECAwFztxVHjlbh0+gjJ3XmNLS0zDsbgs9jWKExLg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-property-aliases-ecmascript": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz", + "integrity": "sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/unique-filename": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-3.0.0.tgz", + "integrity": "sha512-afXhuC55wkAmZ0P18QsVE6kp8JaxrEokN2HGIoIVv2ijHQd419H0+6EigAFcIzXeMIkcIkNBpB3L/DXB3cTS/g==", + "dev": true, + "license": "ISC", + "dependencies": { + "unique-slug": "^4.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/unique-slug": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-4.0.0.tgz", + "integrity": "sha512-WrcA6AyEfqDX5bWige/4NQfPZMtASNVxdmWR76WESYQVAACSgWcR6e9i0mofqqBxYFtL4oAxPIptY73/0YE1DQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "license": "MIT", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.2.tgz", + "integrity": "sha512-PPypAm5qvlD7XMZC3BujecnaOxwhrtoFR+Dqkk5Aa/6DssiH0ibKoketaj9w8LP7Bont1rYeoV5plxD7RTEPRg==", + "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==", + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/uri-js/node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "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/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "node_modules/validate-npm-package-name": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-5.0.1.tgz", + "integrity": "sha512-OljLrQ9SQdOUqTaQxqL5dEfZWrXExyyWsozYlAWFawPVNuD83igl7uJD2RTkNMbniIYgt8l81eCJGIdQF7avLQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/void-elements": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-2.0.1.tgz", + "integrity": "sha512-qZKX4RnBzH2ugr8Lxa7x+0V6XD9Sb/ouARtiasEQCHB1EVU4NXtmHsDDrx1dO4ne5fc3J6EW05BP1Dl0z0iung==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/watchpack": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.2.tgz", + "integrity": "sha512-TnbFSbcOCcDgjZ4piURLCbJ3nJhznVh9kw6F6iokjiFPl8ONxe9A6nMDVXDiNbrSfLILs6vB07F7wLBrwPYzJw==", + "license": "MIT", + "dependencies": { + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/wbuf": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/wbuf/-/wbuf-1.7.3.tgz", + "integrity": "sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimalistic-assert": "^1.0.0" + } + }, + "node_modules/wcwidth": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", + "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==", + "license": "MIT", + "dependencies": { + "defaults": "^1.0.3" + } + }, + "node_modules/web-worker": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/web-worker/-/web-worker-1.5.0.tgz", + "integrity": "sha512-RiMReJrTAiA+mBjGONMnjVDP2u3p9R1vkcGz6gDIrOMT3oGuYwX2WRMYI9ipkphSuE5XKEhydbhNEJh4NY9mlw==", + "license": "Apache-2.0", + "optional": true + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/webpack": { + "version": "5.76.1", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.76.1.tgz", + "integrity": "sha512-4+YIK4Abzv8172/SGqObnUjaIHjLEuUasz9EwQj/9xmPPkYJy2Mh03Q/lJfSD3YLzbxy5FeTq5Uw0323Oh6SJQ==", + "license": "MIT", + "dependencies": { + "@types/eslint-scope": "^3.7.3", + "@types/estree": "^0.0.51", + "@webassemblyjs/ast": "1.11.1", + "@webassemblyjs/wasm-edit": "1.11.1", + "@webassemblyjs/wasm-parser": "1.11.1", + "acorn": "^8.7.1", + "acorn-import-assertions": "^1.7.6", + "browserslist": "^4.14.5", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.10.0", + "es-module-lexer": "^0.9.0", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.2.9", + "json-parse-even-better-errors": "^2.3.1", + "loader-runner": "^4.2.0", + "mime-types": "^2.1.27", + "neo-async": "^2.6.2", + "schema-utils": "^3.1.0", + "tapable": "^2.1.1", + "terser-webpack-plugin": "^5.1.3", + "watchpack": "^2.4.0", + "webpack-sources": "^3.2.3" + }, + "bin": { + "webpack": "bin/webpack.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-dev-middleware": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-6.1.2.tgz", + "integrity": "sha512-Wu+EHmX326YPYUpQLKmKbTyZZJIB8/n6R09pTmB03kJmnMsVPTo9COzHZFr01txwaCAuZvfBJE4ZCHRcKs5JaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "colorette": "^2.0.10", + "memfs": "^3.4.12", + "mime-types": "^2.1.31", + "range-parser": "^1.2.1", + "schema-utils": "^4.0.0" + }, + "engines": { + "node": ">= 14.15.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "webpack": { + "optional": true + } + } + }, + "node_modules/webpack-dev-server": { + "version": "4.11.1", + "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-4.11.1.tgz", + "integrity": "sha512-lILVz9tAUy1zGFwieuaQtYiadImb5M3d+H+L1zDYalYoDl0cksAB1UNyuE5MMWJrG6zR1tXkCP2fitl7yoUJiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/bonjour": "^3.5.9", + "@types/connect-history-api-fallback": "^1.3.5", + "@types/express": "^4.17.13", + "@types/serve-index": "^1.9.1", + "@types/serve-static": "^1.13.10", + "@types/sockjs": "^0.3.33", + "@types/ws": "^8.5.1", + "ansi-html-community": "^0.0.8", + "bonjour-service": "^1.0.11", + "chokidar": "^3.5.3", + "colorette": "^2.0.10", + "compression": "^1.7.4", + "connect-history-api-fallback": "^2.0.0", + "default-gateway": "^6.0.3", + "express": "^4.17.3", + "graceful-fs": "^4.2.6", + "html-entities": "^2.3.2", + "http-proxy-middleware": "^2.0.3", + "ipaddr.js": "^2.0.1", + "open": "^8.0.9", + "p-retry": "^4.5.0", + "rimraf": "^3.0.2", + "schema-utils": "^4.0.0", + "selfsigned": "^2.1.1", + "serve-index": "^1.9.1", + "sockjs": "^0.3.24", + "spdy": "^4.0.2", + "webpack-dev-middleware": "^5.3.1", + "ws": "^8.4.2" + }, + "bin": { + "webpack-dev-server": "bin/webpack-dev-server.js" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^4.37.0 || ^5.0.0" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-dev-server/node_modules/webpack-dev-middleware": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-5.3.4.tgz", + "integrity": "sha512-BVdTqhhs+0IfoeAf7EoH5WE+exCmqGerHfDM0IL096Px60Tq2Mn9MAbnaGUe6HiMa41KMCYF19gyzZmBcq/o4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "colorette": "^2.0.10", + "memfs": "^3.4.3", + "mime-types": "^2.1.31", + "range-parser": "^1.2.1", + "schema-utils": "^4.0.0" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^4.0.0 || ^5.0.0" + } + }, + "node_modules/webpack-merge": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.8.0.tgz", + "integrity": "sha512-/SaI7xY0831XwP6kzuwhKWVKDP9t1QY1h65lAFLbZqMPIuYcD9QAW4u9STIbU9kaJbPBB/geU/gLr1wDjOhQ+Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "clone-deep": "^4.0.1", + "wildcard": "^2.0.0" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/webpack-sources": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz", + "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==", + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webpack-subresource-integrity": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/webpack-subresource-integrity/-/webpack-subresource-integrity-5.1.0.tgz", + "integrity": "sha512-sacXoX+xd8r4WKsy9MvH/q/vBtEHr86cpImXwyg74pFIpERKt6FmB8cXpeuh0ZLgclOlHI4Wcll7+R5L02xk9Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "typed-assert": "^1.0.8" + }, + "engines": { + "node": ">= 12" + }, + "peerDependencies": { + "html-webpack-plugin": ">= 5.0.0-beta.1 < 6", + "webpack": "^5.12.0" + }, + "peerDependenciesMeta": { + "html-webpack-plugin": { + "optional": true + } + } + }, + "node_modules/webpack/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "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/webpack/node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "license": "MIT", + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "node_modules/webpack/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==", + "license": "MIT" + }, + "node_modules/webpack/node_modules/schema-utils": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/websocket-driver": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", + "integrity": "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==", + "license": "Apache-2.0", + "dependencies": { + "http-parser-js": ">=0.5.1", + "safe-buffer": ">=5.1.0", + "websocket-extensions": ">=0.1.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/websocket-extensions": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz", + "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, + "node_modules/wide-align": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", + "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^1.0.2 || 2 || 3 || 4" + } + }, + "node_modules/wildcard": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.1.tgz", + "integrity": "sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/winston": { + "version": "3.17.0", + "resolved": "https://registry.npmjs.org/winston/-/winston-3.17.0.tgz", + "integrity": "sha512-DLiFIXYC5fMPxaRg832S6F5mJYvePtmO5G9v9IgUFPhXm9/GkXarH/TUrBAVzhTCzAj9anE/+GjrgXp/54nOgw==", + "license": "MIT", + "dependencies": { + "@colors/colors": "^1.6.0", + "@dabh/diagnostics": "^2.0.2", + "async": "^3.2.3", + "is-stream": "^2.0.0", + "logform": "^2.7.0", + "one-time": "^1.0.0", + "readable-stream": "^3.4.0", + "safe-stable-stringify": "^2.3.1", + "stack-trace": "0.0.x", + "triple-beam": "^1.3.0", + "winston-transport": "^4.9.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/winston-transport": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.9.0.tgz", + "integrity": "sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A==", + "license": "MIT", + "dependencies": { + "logform": "^2.7.0", + "readable-stream": "^3.6.2", + "triple-beam": "^1.3.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/winston/node_modules/@colors/colors": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.6.0.tgz", + "integrity": "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==", + "license": "MIT", + "engines": { + "node": ">=0.1.90" + } + }, + "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/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "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/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/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/write-file-atomic": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", + "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.7" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/ws": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 6" + } + }, + "node_modules/yargs": { + "version": "17.6.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.6.2.tgz", + "integrity": "sha512-1/9UrdHjDZc0eOU0HxOHoS78C69UD3JRMvzlJ7S79S2nTaWRA/whGCTV8o9e/N/1Va9YIV7Q4sOxD8VV4pCWOw==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zone.js": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.12.0.tgz", + "integrity": "sha512-XtC+I5dXU14HrzidAKBNMqneIVUykLEAA1x+v4KVrd6AUPWlwYORF8KgsVqvgdHiKZ4BkxxjvYi/ksEixTPR0Q==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + } + } + } +} diff --git a/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/package.json b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/package.json new file mode 100644 index 0000000..57019c1 --- /dev/null +++ b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/package.json @@ -0,0 +1,81 @@ +{ + "name": "quick-bot-app-frontend", + "version": "0.0.0", + "scripts": { + "ng": "ng", + "start": "ng serve", + "build:prod": "ng build --configuration production --aot --output-hashing=all", + "build:dev": "ng build --configuration development", + "build:staging": "ng build --configuration staging", + "build": "ng build", + "watch": "ng build --watch --configuration development", + "test": "ng test", + "postinstall": "ngcc", + "post-build": "node ./build/post-build.js", + "lint": "gts lint", + "clean": "gts clean", + "compile": "tsc", + "fix": "gts fix", + "prepare": "npm run compile", + "pretest": "npm run compile", + "posttest": "npm run lint", + "format": "prettier --write \"src/**/*.{ts,html,scss,json}\"", + "check-format": "prettier --check \"src/**/*.{ts,html,scss,json}\"" + }, + "private": true, + "dependencies": { + "@angular/animations": "^15.1.0", + "@angular/cdk": "^15.2.9", + "@angular/common": "^15.1.0", + "@angular/compiler": "^15.1.0", + "@angular/core": "^15.1.0", + "@angular/fire": "7.5", + "@angular/flex-layout": "^15.0.0-beta.42", + "@angular/forms": "^15.1.0", + "@angular/material": "^15.2.9", + "@angular/platform-browser": "^15.1.0", + "@angular/platform-browser-dynamic": "^15.1.0", + "@angular/router": "^15.1.0", + "@fortawesome/fontawesome-free": "^6.6.0", + "@ionic/angular": "^7.5.4", + "@ng-idle/core": "^14.0.0", + "clipboard": "^2.0.11", + "ionic-emoji-rating": "^1.0.15", + "marked": "^4.3.0", + "moment": "^2.30.1", + "ng2-pdf-viewer": "^7.0.2", + "ngx-markdown": "^15.1.2", + "prismjs": "^1.29.0", + "rxfire": "6.0.3", + "rxjs": "~7.8.0", + "tslib": "^2.3.0", + "uuid": "^9.0.1", + "zone.js": "~0.12.0" + }, + "devDependencies": { + "@angular-devkit/build-angular": "^15.1.3", + "@angular/cli": "~15.1.3", + "@angular/compiler-cli": "^15.1.0", + "@types/jasmine": "~4.3.0", + "@types/marked": "^4.3.0", + "@types/node": "20.12.7", + "@types/uuid": "^9.0.4", + "autoprefixer": "^10.4.21", + "firebase-tools": "^11.30.0", + "gts": "^5.3.1", + "jasmine-core": "~4.5.0", + "karma": "~6.4.0", + "karma-chrome-launcher": "~3.1.0", + "karma-coverage": "~2.2.0", + "karma-jasmine": "~5.1.0", + "karma-jasmine-html-reporter": "~2.0.0", + "postcss": "^8.5.3", + "prettier": "^3.5.1", + "tailwindcss": "^3.4.17", + "typescript": "~4.9.4" + }, + "description": "This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 15.1.3.", + "main": "index.js", + "author": "", + "license": "ISC" +} diff --git a/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/postcss.config.js b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/postcss.config.js new file mode 100644 index 0000000..30a0865 --- /dev/null +++ b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/postcss.config.js @@ -0,0 +1,22 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +module.exports = { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; diff --git a/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/app/app-routing.module.ts b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/app/app-routing.module.ts new file mode 100644 index 0000000..9a379d6 --- /dev/null +++ b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/app/app-routing.module.ts @@ -0,0 +1,34 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {NgModule} from '@angular/core'; +import {RouterModule, Routes} from '@angular/router'; +import {MainComponent} from './components/main/main.component'; +import {LoginComponent} from './components/login/login.component'; +import {AuthGuard} from './services/login/auth.guard'; +import {SearchResultsComponent} from './components/main/search-results/search-results.component'; + +const routes: Routes = [ + {path: '', component: MainComponent, canActivate: [AuthGuard]}, + {path: 'login', component: LoginComponent}, + {path: 'search', component: SearchResultsComponent, canActivate: [AuthGuard]}, +]; + +@NgModule({ + imports: [RouterModule.forRoot(routes, {onSameUrlNavigation: 'reload'})], + exports: [RouterModule], +}) +export class AppRoutingModule {} diff --git a/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/app/app.component.html b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/app/app.component.html new file mode 100644 index 0000000..be3bd01 --- /dev/null +++ b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/app/app.component.html @@ -0,0 +1,34 @@ + + + + +
    + +
    + + diff --git a/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/app/app.component.scss b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/app/app.component.scss new file mode 100644 index 0000000..ea0b4c3 --- /dev/null +++ b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/app/app.component.scss @@ -0,0 +1,141 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +::ng-deep .tour-step { + background: white; +} + +::ng-deep .tour-top { + margin: -1%; +} + +::ng-deep .tour-left { + margin-left: -1%; +} + +::ng-deep .tour-top-right { + margin-top: -1%; +} + +::ng-deep .tour-bottom-right { + margin-top: 1%; +} + +::ng-deep .tour-bottom-left { + margin-top: 1%; +} + +::ng-deep .tour-bottom { + margin: 1%; +} + +::ng-deep ngx-guided-tour .tour-content { + font-size: 17px !important; + padding-bottom: 20px !important; +} + +::ng-deep ngx-guided-tour .tour-title { + font-size: 22px !important; + padding-bottom: 0 !important; +} + +::ng-deep ngx-guided-tour .tour-step .tour-buttons .skip-button { + font-weight: 500 !important; + font-size: 17px !important; + line-height: 1.7 !important; +} + +::ng-deep ngx-guided-tour .tour-step { + border-radius: 20px !important; +} + +::ng-deep ngx-guided-tour .tour-step .tour-buttons .back-button { + font-weight: 500 !important; + font-size: 17px !important; + line-height: 1.7 !important; +} + +::ng-deep ngx-guided-tour .tour-step .tour-buttons .next-button { + background: #4285f4; + padding-left: 16px !important; + padding-right: 16px !important; + padding-top: 6px !important; + padding-bottom: 6px !important; + font-size: 16px !important; + color: white; + font-weight: 500; + border-radius: 15px !important; +} + +::ng-deep ngx-guided-tour .tour-step.tour-top .tour-block { + margin-bottom: 0 !important; +} + +::ng-deep ngx-guided-tour .tour-step.tour-bottom-left .tour-block { + margin-top: 0 !important; +} + +::ng-deep pre { + background: #485f84 !important; +} + +div { + font-family: 'Google Sans', sans-serif !important; +} + +.video-embed-player { + position: fixed; + top: 20vh; + left: -50vw; + z-index: -10000; + overflow: hidden; + width: 200vw; +} + +// Small devices (landscape phones, 576px and up) +@media (min-width: 576px) { +} + +// Medium devices (tablets, 768px and up) +@media (min-width: 768px) { + .video-embed-player { + top: 20vh; + width: 150vw; + left: -25vw; + } +} + +// Large devices (desktops, 992px and up) +@media (min-width: 992px) { + .video-embed-player { + top: 10vh; + } +} + +// X-Large devices (large desktops, 1200px and up) +@media (min-width: 1200px) { + .video-embed-player { + left: 0; + width: 100vw; + } +} + +// XX-Large devices (larger desktops, 1400px and up) +@media (min-width: 1400px) { + .video-embed-player { + top: 0; + } +} diff --git a/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/app/app.component.spec.ts b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/app/app.component.spec.ts new file mode 100644 index 0000000..9ff3178 --- /dev/null +++ b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/app/app.component.spec.ts @@ -0,0 +1,49 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {TestBed} from '@angular/core/testing'; +import {RouterTestingModule} from '@angular/router/testing'; +import {AppComponent} from './app.component'; + +describe('AppComponent', () => { + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [RouterTestingModule], + declarations: [AppComponent], + }).compileComponents(); + }); + + it('should create the app', () => { + const fixture = TestBed.createComponent(AppComponent); + const app = fixture.componentInstance; + expect(app).toBeTruthy(); + }); + + it("should have as title 'pac-assist-ui'", () => { + const fixture = TestBed.createComponent(AppComponent); + const app = fixture.componentInstance; + expect(app.title).toEqual('pac-assist-ui'); + }); + + it('should render title', () => { + const fixture = TestBed.createComponent(AppComponent); + fixture.detectChanges(); + const compiled = fixture.nativeElement as HTMLElement; + expect(compiled.querySelector('.content span')?.textContent).toContain( + 'pac-assist-ui app is running!' + ); + }); +}); diff --git a/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/app/app.component.ts b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/app/app.component.ts new file mode 100644 index 0000000..a05a5ba --- /dev/null +++ b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/app/app.component.ts @@ -0,0 +1,67 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {Component} from '@angular/core'; +import {Router, NavigationEnd, Event as NavigationEvent} from '@angular/router'; +import {UserService} from './services/user/user.service'; +import {AuthService} from './services/login/auth.service'; + +@Component({ + selector: 'app-root', + templateUrl: './app.component.html', + styleUrls: ['./app.component.scss'], +}) +export class AppComponent { + title = 'pac-assist-ui'; + showHeader = true; + userInfo: any; + + showLoading = false; + showVideo = true; + + constructor( + private router: Router, + private userService: UserService, + public authService: AuthService + ) { + this.router.events.subscribe((event: NavigationEvent) => { + if (event instanceof NavigationEnd) { + if (event.url === '/') this.showVideo = true; + + if ( + event.url === '/login' || + event.url === '/login/e2e' || + (event.url.includes('login') && event.url.includes('email')) || + (event.url.includes('login') && event.url.includes('tos')) || + event.url.includes('reset-password') || + event.url.includes('support-ticket') + ) { + this.showHeader = false; + } else { + this.userInfo = this.userService.getUserDetails(); + this.showHeader = true; + } + } + }); + } + + ngOnInit(): void { + this.userService.loadingSubject.subscribe(loadingValue => { + this.showLoading = loadingValue; + this.showVideo = loadingValue; + }); + } +} diff --git a/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/app/app.module.ts b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/app/app.module.ts new file mode 100644 index 0000000..ce17c1d --- /dev/null +++ b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/app/app.module.ts @@ -0,0 +1,163 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {NgModule} from '@angular/core'; +import {BrowserModule} from '@angular/platform-browser'; +import {AppRoutingModule} from './app-routing.module'; +import {AppComponent} from './app.component'; +import {HeaderComponent} from './components/header/header.component'; +import {FooterComponent} from './components/footer/footer.component'; +import {MainComponent} from './components/main/main.component'; +import {BrowserAnimationsModule} from '@angular/platform-browser/animations'; +import {MatIconModule} from '@angular/material/icon'; +import {MatAutocompleteModule} from '@angular/material/autocomplete'; +import {MatButtonModule} from '@angular/material/button'; +import {MatToolbarModule} from '@angular/material/toolbar'; +import {HttpClientModule} from '@angular/common/http'; +import {MatInputModule} from '@angular/material/input'; +import {NgFor, PathLocationStrategy} from '@angular/common'; +import {MatSelectModule} from '@angular/material/select'; +import {MatFormFieldModule} from '@angular/material/form-field'; +import {FormsModule, ReactiveFormsModule} from '@angular/forms'; +import {MatGridListModule} from '@angular/material/grid-list'; +import {MatCardModule} from '@angular/material/card'; +import {MatProgressSpinnerModule} from '@angular/material/progress-spinner'; +import {MatTooltipModule} from '@angular/material/tooltip'; +import {LoginComponent} from './components/login/login.component'; +import {initializeApp, provideFirebaseApp} from '@angular/fire/app'; +import { + provideAnalytics, + getAnalytics, + ScreenTrackingService, + UserTrackingService, +} from '@angular/fire/analytics'; +import {environment} from '../environments/environment'; +import {provideAuth, getAuth} from '@angular/fire/auth'; +import {MatMenuModule} from '@angular/material/menu'; +import {MatDividerModule} from '@angular/material/divider'; +import {MatChipsModule} from '@angular/material/chips'; +import {LocationStrategy} from '@angular/common'; +import {MarkdownModule} from 'ngx-markdown'; +import {MatDialogModule} from '@angular/material/dialog'; +import {CdkAccordionModule} from '@angular/cdk/accordion'; +import {MatExpansionModule} from '@angular/material/expansion'; +import {IonicRatingModule} from 'ionic-emoji-rating'; +import {MatListModule} from '@angular/material/list'; +import {MatTabsModule} from '@angular/material/tabs'; +import {MatTableModule} from '@angular/material/table'; +import {MatCheckboxModule} from '@angular/material/checkbox'; +import {MatSlideToggleModule} from '@angular/material/slide-toggle'; +import {MatButtonToggleModule} from '@angular/material/button-toggle'; +import {MatSnackBarModule} from '@angular/material/snack-bar'; +import {MatSortModule} from '@angular/material/sort'; +import {MatPaginatorModule} from '@angular/material/paginator'; +import {MatSidenavModule} from '@angular/material/sidenav'; +import {NgIdleModule} from '@ng-idle/core'; +import {ClipboardModule} from '@angular/cdk/clipboard'; +import {MatProgressBarModule} from '@angular/material/progress-bar'; +import {PdfViewerModule} from 'ng2-pdf-viewer'; + +import 'prismjs'; +import 'prismjs/components/prism-typescript.min.js'; +import 'prismjs/plugins/line-numbers/prism-line-numbers.js'; +import 'prismjs/plugins/line-highlight/prism-line-highlight.js'; +import {FlexLayoutModule} from '@angular/flex-layout'; +import {MatSliderModule} from '@angular/material/slider'; +import {MatStepperModule} from '@angular/material/stepper'; +import {ChatInputComponent} from './components/main/chat-input/chat-input.component'; +import {SearchResultsComponent} from './components/main/search-results/search-results.component'; +import {ToastMessageComponent} from './components/toast-message/toast-message.component'; +import {TruncatePipe} from './pipes/truncate.pipe'; + +@NgModule({ + declarations: [ + AppComponent, + HeaderComponent, + FooterComponent, + MainComponent, + LoginComponent, + ChatInputComponent, + SearchResultsComponent, + ToastMessageComponent, + TruncatePipe, + ], + imports: [ + BrowserModule, + AppRoutingModule, + BrowserAnimationsModule, + MatSnackBarModule, + MatPaginatorModule, + MatSortModule, + MatSliderModule, + MatToolbarModule, + MatButtonModule, + MatIconModule, + HttpClientModule, + FormsModule, + MatFormFieldModule, + MatSelectModule, + NgFor, + MatInputModule, + MatGridListModule, + MatCardModule, + MatTooltipModule, + MatProgressSpinnerModule, + MatMenuModule, + MatDividerModule, + MatChipsModule, + ReactiveFormsModule, + MatMenuModule, + MatDialogModule, + CdkAccordionModule, + IonicRatingModule, + MarkdownModule.forRoot(), + MatExpansionModule, + MatListModule, + MatTabsModule, + MatCheckboxModule, + MatTableModule, + MatSlideToggleModule, + MatButtonToggleModule, + MatSidenavModule, + MatAutocompleteModule, + environment.requiredLogin === 'True' + ? [ + provideFirebaseApp(() => initializeApp(environment.firebase)), + provideAuth(() => getAuth()), + ] + : [], + environment.requiredLogin === 'True' + ? [provideAnalytics(() => getAnalytics())] + : [], + FlexLayoutModule, + NgIdleModule.forRoot(), + ClipboardModule, + MatStepperModule, + MatProgressBarModule, + PdfViewerModule, + ], + providers: [ + {provide: LocationStrategy, useClass: PathLocationStrategy}, + environment.requiredLogin === 'True' + ? [ + ScreenTrackingService, // Automatically track screen views + UserTrackingService, // Automatically track user interactions + ] + : [], + ], + bootstrap: [AppComponent], +}) +export class AppModule {} diff --git a/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/app/components/footer/footer.component.html b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/app/components/footer/footer.component.html new file mode 100644 index 0000000..e649379 --- /dev/null +++ b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/app/components/footer/footer.component.html @@ -0,0 +1,25 @@ + + +

    + +

    + +

    diff --git a/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/app/components/footer/footer.component.scss b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/app/components/footer/footer.component.scss new file mode 100644 index 0000000..b405613 --- /dev/null +++ b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/app/components/footer/footer.component.scss @@ -0,0 +1,76 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +.container { + padding-left: 4rem; + padding-right: 4rem; +} + +.footer { + position: fixed; + bottom: 0; +} + +.genAI { + background: linear-gradient( + 86.95deg, + #34a853 -65.34%, + #fbbc05 -12.75%, + #ea4335 25.15%, + #1a73e8 103.88% + ); + -webkit-background-clip: text; + -moz-background-clip: text; + background-clip: text; + color: transparent; +} + +:host ::ng-deep .mat-toolbar.mat-primary { + background-color: transparent; + color: #504747; + box-shadow: none; + justify-content: center; +} + +.line-spacer { + flex: 1 1 auto; +} + +.links-weight { + margin-left: 10px; + margin-right: 10px; + font-size: 14px; + font-weight: 400; + cursor: pointer; +} + +.links-bold { + margin-right: 10px; + color: #5f6368; + font-family: 'Google Sans', sans-serif !important; + .blue { + color: #4285f4; + } + .red { + color: #ea4335; + } + .yellow { + color: #fbbc04; + } + .green { + color: #34a853; + } +} diff --git a/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/app/components/footer/footer.component.spec.ts b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/app/components/footer/footer.component.spec.ts new file mode 100644 index 0000000..420323c --- /dev/null +++ b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/app/components/footer/footer.component.spec.ts @@ -0,0 +1,38 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {ComponentFixture, TestBed} from '@angular/core/testing'; + +import {FooterComponent} from './footer.component'; + +describe('FooterComponent', () => { + let component: FooterComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [FooterComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(FooterComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/app/components/footer/footer.component.ts b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/app/components/footer/footer.component.ts new file mode 100644 index 0000000..a38bb6c --- /dev/null +++ b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/app/components/footer/footer.component.ts @@ -0,0 +1,24 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {Component} from '@angular/core'; + +@Component({ + selector: 'app-footer', + templateUrl: './footer.component.html', + styleUrls: ['./footer.component.scss'], +}) +export class FooterComponent {} diff --git a/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/app/components/header/header.component.html b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/app/components/header/header.component.html new file mode 100644 index 0000000..306be0b --- /dev/null +++ b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/app/components/header/header.component.html @@ -0,0 +1,101 @@ + + + + diff --git a/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/app/components/header/header.component.scss b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/app/components/header/header.component.scss new file mode 100644 index 0000000..f2913bd --- /dev/null +++ b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/app/components/header/header.component.scss @@ -0,0 +1,643 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +.container { + margin: auto; +} + +.logo-text { + font-family: + Google Sans, + Helvetica Neue, + sans-serif; + font-style: normal; + font-size: 22px; + line-height: 128.7%; + letter-spacing: normal; + color: #2d2c2cbf; +} + +.gradient-text { + font-family: + Google Sans, + Helvetica Neue, + sans-serif; + font-style: normal; + font-size: 30px; + line-height: 128.7%; + letter-spacing: normal; + background: linear-gradient( + 89.9deg, + rgba(66, 133, 244, 0.95) 21.17%, + #a488f5 44.34%, + rgba(234, 67, 53, 0.88) 81.26% + ); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; + text-fill-color: transparent; +} + +.links-pointer { + cursor: pointer; +} + +.logo-icon { + height: 35px; + width: 125px; + cursor: pointer; +} + +// .profile-container { +// margin-left: 1%; +// font-family: 'Google Sans', sans-serif !important; + +// .circle { +// display: flex; +// justify-content: center; +// width: 40px; +// height: 40px; +// border: 5px solid rgb(255, 255, 255); +// border-radius: 60%; +// overflow: hidden; +// } + +// .circle img { +// width: 100%; +// height: 100%; +// object-fit: cover; +// } +// } + +.card-inside-right { + display: flex; + flex-direction: column; + align-items: flex-start; + color: #3e4245; + font-size: 1rem; + font-weight: 400; + font-family: 'Google Sans', sans-serif !important; + border-radius: 5px; + letter-spacing: 0.4px; + + .container-circle { + width: 100%; + height: 100%; + object-fit: cover; + border-radius: 50px; + padding-top: 4%; + padding-bottom: 4%; + } + + .name { + display: flex; + align-items: center; + font-size: 1rem; + color: #3e4245; + font-weight: 500; + padding: 1.3em 0.4em; + flex-direction: column; + gap: 10px; + background: #dde3ea !important; + width: -webkit-fill-available; + .sub-title { + color: #3e4245; + } + + margin: auto; + } + + .company { + display: flex; + align-items: center; + padding: 0.3em 1em; + width: -webkit-fill-available; + + .logo { + padding-right: 2%; + color: #5f6368; + } + + .text { + min-width: 100px; + + .sub-title { + font-size: 0.85rem; + margin-bottom: 10px; + color: #3e4245; + } + } + + margin-bottom: 10px; + } + + .logout { + background: #dde3ea; + border-radius: 20px; + font-size: 1rem; + font-weight: 500; + font-family: 'Google Sans', sans-serif !important; + margin-bottom: 1em; + cursor: pointer; + width: -webkit-fill-available; + padding: 0.3em 1em; + + .logout-container { + display: flex; + align-items: center; + + .logo { + padding-right: 4%; + color: #5f6368; + } + + .text { + .sub-title { + color: #3e4245; + font-size: 0.85rem; + font-family: 'Google Sans', sans-serif !important; + } + } + } + } +} + +//------------css of card end-------------------// + +.image { + width: 35px; + height: 35px; +} + +.fixed-top { + z-index: 1000; + position: relative; +} + +.top-nav { + height: 60px; + background-color: transparent; + + .parent-container { + display: flex; + justify-content: space-between; + align-items: center; + padding: 1rem 3rem; + + .left-heading { + display: flex; + align-items: center; + cursor: pointer; + } + + .logo-icon { + height: 4.2em; + width: 4em; + cursor: pointer; + background-image: url('../../../assets/images/new-pdc-logo.svg'); + background-size: 443px; + background-repeat: round; + border-radius: 28px; + margin-right: 1em; + } + + .user-icon { + height: 50px; + width: 50px; + cursor: pointer; + border-radius: 28px; + } + + .right-menu-items { + display: flex; + align-items: center; + + .profile-container { + margin-left: 1%; + font-family: 'Google Sans', sans-serif !important; + + .circle { + display: flex; + justify-content: center; + width: 35px; + height: 35px; + border-radius: 60%; + overflow: hidden; + } + + .circle img { + width: 100%; + height: 100%; + object-fit: cover; + } + } + } + } +} + +.icon-color { + color: #5f6368; + font-size: 24px; +} + +::ng-deep .quick-link.mat-mdc-menu-panel.mat-mdc-menu-panel { + width: 420px !important; + display: flex; + flex-direction: column; + background: #f0f4f9; + + .mat-mdc-menu-content { + padding: 0.5rem; + } + + .ql-container { + width: 100%; + display: flex; + flex-direction: column; + + .ql-child-container { + display: flex; + flex-direction: row; + text-wrap: nowrap; + justify-content: space-between; + align-items: flex-end; + } + + a, + a:active { + text-decoration: none; + } + + .align-link-content { + display: flex; + flex-direction: row; + align-items: baseline; + gap: 10px; + width: 100% !important; + box-shadow: 0px 4px 4px -6px #b9b8b8 !important; + } + + .no-box-shadow { + box-shadow: none !important; + } + + .icon-container { + display: flex; + alignment-baseline: after-edge; + + mat-icon { + color: #161a1d !important; + } + } + } + + .ql-header { + margin: 1rem; + margin-left: 1.7rem; + } + + .avatar { + width: 40px; + height: 40px; //<--use the size you choose + border-radius: 100%; + text-align: center; + } + + div.link-label { + color: #161a1d; + font-size: 16px; + font-style: normal; + font-weight: 400; + line-height: 24px; + letter-spacing: 0.4px; + height: 50px; + margin: 0px 5px; + width: 57%; + } + + .avatar span { + line-height: 40px; + font-size: 16px; + } + + .link-button { + margin: 1rem; + height: 41px; + width: 100% !important; + } +} + +.notification-banner-container { + z-index: 999; + position: relative; + width: inherit; + display: flex; + flex-direction: row; + height: 8%; + background: #f6f9fe; + align-content: center; + flex-wrap: wrap; + padding-left: 1%; + gap: 1%; + margin-bottom: 0.2%; + + .amber { + color: #fbc645; + } + + .red { + color: red; + } + + .blue { + color: #4285f4; + } +} + +.red-border { + border-left: 5px solid red; +} + +.amber-border { + border-left: 5px solid #fbc645; +} + +.blue-border { + border-left: 5px solid #4285f4; +} + +.notification-text { + display: flex; + flex-direction: row; + align-items: center; + gap: 15px; + width: 70%; +} + +.notification-buttons-container { + width: 26%; + justify-content: flex-end; + display: flex; + flex-direction: row; + gap: 15px; + + button { + font-family: 'Google Sans', sans-serif !important; + font-style: normal; + font-weight: 500; + line-height: 23px; + font-size: 14px; + } + + .dismiss-button { + letter-spacing: 0.4px; + color: #4285f4; + } + + .learn-more-button { + color: #fff !important; + background: #4285f4 !important; + border-radius: 30px; + letter-spacing: unset; + box-shadow: none; + } +} + +::ng-deep .profile-menu.mat-mdc-menu-panel.mat-mdc-menu-panel { + max-width: 320px !important; + width: 300px !important; +} + +::ng-deep .profile-menu .mat-mdc-menu-content { + padding: 0% !important; +} + +.action-item-container { + width: -webkit-fill-available; + padding-inline: 5%; + padding-top: 2%; + padding-bottom: 2%; +} + +.logout-button { + background: white !important; + border: 1px solid #4285f4 !important; +} + +.logout-button-text { + color: #4285f4 !important; +} + +.app-version-chip { + font-size: 9px; + border-radius: -2px; + padding-inline: 8px; + line-height: 16px; + border-radius: 11px; + margin-left: 8px; + color: #474c55; + background: + linear-gradient( + 92.1deg, + #bdd1f3 19.16%, + rgba(199, 183, 245, 0.9) 79%, + rgba(235, 165, 158, 0.9) 135.24% + ), + linear-gradient( + 272.04deg, + rgba(158, 188, 237, 0.7) 14.93%, + rgba(158, 188, 237, 0) 104.51% + ) !important; +} + +.bypassLoginLogo { + background: aliceblue; + color: cornflowerblue; + align-items: center; + cursor: pointer; +} + +.bypassMenuContainer { + display: flex; + justify-content: center; + background: aliceblue; + width: 42% !important; + height: 40% !important; + border-radius: 65px !important; +} + +.bypassMenuLogo { + font-size: 6rem; + height: 6rem; + width: 6rem; +} + +.chat-footer { + min-height: 60px; + display: flex; + border: 1px solid #dde6f6; + box-shadow: 0px 4px 18px rgba(195, 209, 226, 0.25); + border-radius: 30px; + width: 66%; + margin-top: 25px; + // box-shadow: 5px 4px 24px rgba(66, 133, 244, 0.15); + + .chat-textarea { + flex-grow: 1; + display: flex; + align-items: center; + + input { + width: 100%; + height: 45px; + border: 0px; + border-radius: 4px; + resize: none; + outline: none; + padding: 14px; + font-family: 'Google Sans', sans-serif !important; + box-sizing: border-box; + -ms-overflow-style: none; + scrollbar-width: none; + font-size: 0.95rem; + line-height: 1.1rem; + color: #534646; + margin-left: 20px; + } + + input::-webkit-scrollbar { + display: none; + } + } + + .micIcon { + display: flex; + align-items: center; + color: #1f1f1f; + margin-top: 2px; + padding-right: 1%; + + img { + max-width: 40%; + cursor: pointer; + } + } +} + +.blue-dot { + /* Ellipse 1226 */ + content: ''; + width: 8px; + height: 8px; + left: 1503px; + top: 445px; + display: flow-root; + border-radius: 15px; + + /* Blue */ + background: #4285f4; + animation: blue-transform 2s infinite; +} + +.green-dot { + content: ''; + width: 8px; + height: 8px; + left: 1522px; + top: 445px; + display: flow-root; + border-radius: 15px; + + /* Green */ + background: #0f9d58; + animation: green-transform 2s infinite; +} + +.yellow-dot { + content: ''; + width: 8px; + height: 8px; + left: 1541px; + top: 445px; + display: flow-root; + border-radius: 15px; + + /* Yellow */ + background: #f4b400; + animation: yellow-transform 2s infinite; +} + +.recording-container { + display: flex; + flex-direction: row; + gap: 10px; + cursor: pointer; + align-items: baseline; +} + +@keyframes blue-transform { + 0% { + transform: translateY(-14px); + } + + 33% { + transform: translateY(0); + } + + 66% { + transform: translateY(0); + } + + 100% { + transform: translateY(-14px); + } +} + +@keyframes green-transform { + 0% { + transform: translateY(0); + } + + 33% { + transform: translateY(-14px); + } + + 66% { + transform: translateY(0); + } + + 100% { + transform: translateY(0); + } +} + +@keyframes yellow-transform { + 0% { + transform: translateY(0); + } + + 33% { + transform: translateY(0); + } + + 66% { + transform: translateY(-14px); + } + + 100% { + transform: translateY(0); + } +} diff --git a/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/app/components/header/header.component.spec.ts b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/app/components/header/header.component.spec.ts new file mode 100644 index 0000000..90d999b --- /dev/null +++ b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/app/components/header/header.component.spec.ts @@ -0,0 +1,38 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {ComponentFixture, TestBed} from '@angular/core/testing'; + +import {HeaderComponent} from './header.component'; + +describe('HeaderComponent', () => { + let component: HeaderComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [HeaderComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(HeaderComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/app/components/header/header.component.ts b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/app/components/header/header.component.ts new file mode 100644 index 0000000..31849c9 --- /dev/null +++ b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/app/components/header/header.component.ts @@ -0,0 +1,83 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {Component, EventEmitter, Output} from '@angular/core'; +import {DomSanitizer} from '@angular/platform-browser'; +import {MatIconRegistry} from '@angular/material/icon'; +import {UserService} from 'src/app/services/user/user.service'; +import {AuthService} from '../../services/login/auth.service'; +import {environment} from 'src/environments/environment'; +import {Router} from '@angular/router'; + +const GOOGLE_CLOUD_ICON = ` + + + + + + + + +`; + +@Component({ + selector: 'app-header', + templateUrl: './header.component.html', + styleUrls: ['./header.component.scss'], +}) +export class HeaderComponent { + headerTitle: string = environment.chatbotName; + requiredLogin: string = environment.requiredLogin; + + showLoading = false; + + @Output() emitSearch: EventEmitter = new EventEmitter(); + + constructor( + iconRegistry: MatIconRegistry, + sanitizer: DomSanitizer, + public _UserService: UserService, + public authService: AuthService, + private router: Router + ) { + iconRegistry.addSvgIconLiteral( + 'google-cloud-icon', + sanitizer.bypassSecurityTrustHtml(GOOGLE_CLOUD_ICON) + ); + } + + ngOnInit(): void { + this._UserService.loadingSubject.subscribe( + loadingValue => (this.showLoading = loadingValue) + ); + } + + isSearchRoute(): boolean { + return this.router.url.startsWith('/search'); + } + + searchTerm(term: string) { + this.emitSearch.emit(term); + } + + goToManageConfig() { + this.router.navigateByUrl('/manage-config'); + } + + logout() { + this.authService.logout(); + } +} diff --git a/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/app/components/login/login.component.html b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/app/components/login/login.component.html new file mode 100644 index 0000000..ab76003 --- /dev/null +++ b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/app/components/login/login.component.html @@ -0,0 +1,63 @@ + + + diff --git a/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/app/components/login/login.component.scss b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/app/components/login/login.component.scss new file mode 100644 index 0000000..bf919c7 --- /dev/null +++ b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/app/components/login/login.component.scss @@ -0,0 +1,569 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +.login-container { + display: flex; + min-height: 100vh; + max-height: 100%; + .spinner { + display: flex; + justify-content: center; + margin: 1em; + } + + .linear-text { + background: linear-gradient( + 89.9deg, + rgba(66, 133, 244, 0.95) 21.17%, + #a488f5 44.34%, + rgba(234, 67, 53, 0.88) 81.26% + ); + -webkit-background-clip: text; + -moz-background-clip: text; + background-clip: text; + color: transparent; + } + + .heading { + font-family: 'Google Sans', sans-serif !important; + font-size: 32px; + font-style: normal; + font-weight: 500; + line-height: 150.7%; + color: white; + } + + .left-container { + background-image: + url('../../../assets/images/colibri_image_fx.jpg'), + url('../../../assets/images/crane_image_fx.jpg'); + background-repeat: no-repeat, repeat; + background-size: contain, contain; + flex: 0 1 55%; + min-height: 100%; + max-width: 60%; + align-items: center; + padding: 0.5em; + overflow: auto; + display: flex; + justify-content: center; + + .login-app-text-sub { + color: #161a1d; + font-size: 1rem; + font-style: normal; + font-weight: 400; + line-height: 150.7%; + } + + .login-app-text { + // margin-bottom: 2rem; + line-height: 1.5em; + } + + .expand-button { + --mdc-fab-container-color: #4285f41a; + --mdc-fab-icon-color: #000000; + border: 1px solid #4285f4; + --mat-mdc-fab-color: #4285f4; + height: 2.2rem; + font-size: 0.8rem; + font-weight: 400; + box-shadow: none; + } + + .expand-less-button { + --mdc-fab-container-color: rgba(92, 95, 97, 0.1); + --mdc-fab-icon-color: #000000; + border: 1px solid rgba(92, 95, 97, 0.5); + --mat-mdc-fab-color: rgba(92, 95, 97, 0.5); + margin-bottom: 1em; + height: 2.2rem; + font-size: 0.8rem; + font-weight: 400; + box-shadow: none; + } + } + + .right-container { + background-image: url('../../../assets/images/hot_air_baloon_image_fx.jpg'); + background-repeat: repeat; + background-size: cover; + flex: 0 1 45%; + min-height: 100%; + max-width: 80%; + align-items: flex-start; + display: flex; + justify-content: center; + overflow-y: auto; + scrollbar-width: thin; + + .login-card { + backdrop-filter: blur(10px); + flex-direction: column; + display: flex; + max-width: 100%; + padding: 3%; + margin: auto; + text-align: center; + + .logo-icon { + height: 145px; + width: 100%; + // margin-bottom: 2rem; + } + + .login-button { + background: #4285f4; + box-sizing: border-box; + border: 1px solid #e0e0e0; + color: #fff; + position: relative; + user-select: none; + cursor: pointer; + outline: none; + -webkit-tap-highlight-color: transparent; + display: inline-block; + font-family: 'Google Sans', sans-serif !important; + font-size: 14px; + font-weight: 500; + white-space: nowrap; + text-decoration: none; + vertical-align: baseline; + text-align: center; + margin: 0; + min-width: 100px; + line-height: 36px; + padding: 0 16px; + border-radius: 4px; + overflow: visible; + transform: translate3d(0, 0, 0); + transition: + background 400ms cubic-bezier(0.25, 0.8, 0.25, 1), + box-shadow 280ms cubic-bezier(0.4, 0, 0.2, 1); + } + + .register-text { + display: flex; + flex-direction: column; + align-items: flex-start; + margin: 1em 0em; + font-weight: 400; + color: #161a1d; + } + + .register-button { + background: white; + box-sizing: border-box; + border: 1px solid #4285f4; + color: #4285f4; + position: relative; + user-select: none; + cursor: pointer; + outline: none; + -webkit-tap-highlight-color: transparent; + display: inline-block; + font-family: 'Google Sans', sans-serif !important; + font-size: 14px; + font-weight: 500; + white-space: nowrap; + text-decoration: none; + vertical-align: baseline; + text-align: center; + margin: 0; + min-width: 64px; + line-height: 36px; + padding: 0 16px; + border-radius: 4px; + overflow: visible; + transform: translate3d(0, 0, 0); + transition: + background 400ms cubic-bezier(0.25, 0.8, 0.25, 1), + box-shadow 280ms cubic-bezier(0.4, 0, 0.2, 1); + } + } + } + + .slide { + width: 100%; + min-height: 15rem; + border-radius: 10px; + background-size: cover; + background-position: center; + font-size: 1rem; + font-style: normal; + font-weight: 400; + line-height: 200.7%; + padding-left: 0px; + + .feature-heading { + font-weight: 700; + } + } + + .slider { + position: relative; + height: 100%; + } + + .dotsContainer { + display: flex; + justify-content: center; + align-items: center; + margin-top: 2em; + } + + .dot { + margin: 0 3px; + cursor: pointer; + font-size: 18px; + color: #d0d3d7; + } + + .active { + color: #1a73e8; + background-color: #d0d3d7; + padding: 2px; + border-radius: 50%; + font-size: 12px; + } + + .material-symbols-outlined { + font-variation-settings: + 'FILL' 1, + 'wght' 400, + 'GRAD' 0, + 'opsz' 18; + } + + h6 { + position: relative; + z-index: 1; + overflow: hidden; + text-align: center; + font-size: 1.2em; + font-style: normal; + font-weight: 500; + line-height: 200.7%; + font-family: 'Google Sans', sans-serif !important; + } + + h6:before, + h6:after { + position: absolute; + top: 51%; + overflow: hidden; + width: 50%; + height: 1px; + content: '\a0'; + background: linear-gradient( + 86.95deg, + #34a853 -65.34%, + #fbbc05 -12.75%, + #ea4335 25.15%, + #1a73e8 103.88% + ); + } + + h6:before { + margin-left: -50%; + text-align: right; + } +} + +.login-buttons-container { + padding-inline: 4%; + gap: 12px; + display: flex; + flex-direction: column; + background: #f7f9ff; + padding-top: 4%; + padding-bottom: 4%; + border-radius: 5px; +} + +.back-button { + color: #5c5f61 !important; + font-weight: 400 !important; +} + +.help-container { + padding-top: 2%; + text-align: center; +} + +.blue { + color: #4285f4; + cursor: pointer; +} + +mat-form-field { + width: 100%; +} + +.user-name-container { + display: flex; + flex-direction: row; + gap: 3%; +} + +.error-message { + font-weight: 500; + font-size: 0.8rem; + letter-spacing: -0.005rem; + color: #ed0c0c; + + .mat-mdc-form-field-subscript-wrapper { + margin-top: -10px; + } +} + +.forgot-password-container { + display: flex; + flex-direction: column; + text-align: center; + padding: 4%; +} + +.forgot-password-icon-container { + width: 100%; + height: 100%; + + mat-icon { + font-size: 105px; + height: 100%; + width: 100%; + padding: 1%; + padding-top: 7%; + color: #3467bb; + } +} + +.forgot-password-heading-container { + font-size: 29px; + font-weight: 600; + font-family: 'Google Sans', sans-serif !important; + line-height: 2.5em; +} + +.forgot-password-subtext-container { + display: flex; + flex-direction: column; + text-align: center; + font-size: 19px; + gap: 10px; + line-height: 1.5em; + + mat-icon { + color: #4385f4; + } +} + +.logo-icon { + height: 170px; + width: 100%; +} + +.login-button { + width: 100%; + background: #4285f4; + box-sizing: border-box; + border: 1px solid #e0e0e0; + color: #fff; + position: relative; + user-select: none; + cursor: pointer; + outline: none; + -webkit-tap-highlight-color: transparent; + display: inline-block; + font-family: 'Google Sans', sans-serif !important; + font-size: 14px; + font-weight: 500; + white-space: nowrap; + text-decoration: none; + vertical-align: baseline; + text-align: center; + margin: 0; + min-width: 64px; + line-height: 44px; + padding: 0 16px; + border-radius: 4px; + overflow: visible; + transform: translate3d(0, 0, 0); + transition: + background 400ms cubic-bezier(0.25, 0.8, 0.25, 1), + box-shadow 280ms cubic-bezier(0.4, 0, 0.2, 1); +} + +.register-text { + display: flex; + flex-direction: column; + align-items: flex-start; + margin-bottom: 0.3em; + font-weight: 400; + color: #5c5f61; +} + +.register-button { + width: 100%; + background: white; + box-sizing: border-box; + border: 1px solid #4285f4; + color: #4285f4; + position: relative; + user-select: none; + cursor: pointer; + outline: none; + -webkit-tap-highlight-color: transparent; + display: inline-block; + font-family: 'Google Sans', sans-serif !important; + font-size: 14px; + font-weight: 500; + white-space: nowrap; + text-decoration: none; + vertical-align: baseline; + text-align: center; + margin: 0; + min-width: 64px; + line-height: 44px; + padding: 0 16px; + border-radius: 4px; + overflow: visible; + transform: translate3d(0, 0, 0); + transition: + background 400ms cubic-bezier(0.25, 0.8, 0.25, 1), + box-shadow 280ms cubic-bezier(0.4, 0, 0.2, 1); +} + +.send-email-button { + background: #40bd40 !important; + font-weight: 600 !important; + font-size: 16px !important; +} + +.close-button { + float: right !important; +} + +.forgot-password-link-container { + width: 100%; + text-align: end; + display: flex; + flex-direction: column; + padding-bottom: 4%; + z-index: 100; + position: relative; +} + +.button-css { + display: flex; + justify-content: center; + + .ok-button { + justify-content: center; + display: flex; + width: 25%; + background: #4285f4; + box-sizing: border-box; + border: 1px solid #e0e0e0; + color: #fff; + position: relative; + user-select: none; + cursor: pointer; + outline: none; + -webkit-tap-highlight-color: transparent; + font-family: 'Google Sans', sans-serif !important; + font-size: 14px; + font-weight: 500; + white-space: nowrap; + text-decoration: none; + vertical-align: baseline; + text-align: center; + margin: 0; + min-width: 64px; + line-height: 44px; + padding: 0 16px; + border-radius: 4px; + overflow: visible; + transform: translate3d(0, 0, 0); + transition: + background 400ms cubic-bezier(0.25, 0.8, 0.25, 1), + box-shadow 280ms cubic-bezier(0.4, 0, 0.2, 1); + } +} + +.icon-css { + font-size: 40px; + color: #f3f30aed; +} + +.text-css { + font-size: 15px; + line-height: 1.45em; + font-family: 'Google Sans', sans-serif !important; + color: #645a5a; +} + +.logo-container { + display: flex; + flex-direction: row; + justify-content: center; + text-align: center; + mat-icon { + height: 20% !important; + width: 100% !important; + font-size: 70px !important; + color: #4285f4 !important; + } +} + +.heading-span { + display: flex; + flex-direction: row; + gap: 8px; + align-items: center; + padding: 2%; + width: 100%; + justify-content: center; + font-size: 18px; + font-weight: 700; + padding-left: 15%; + color: #4285f4 !important; +} + +.heading-container { + display: flex; + flex-direction: row; + justify-content: center; + background: aliceblue; +} + +.sub-parent-container { + display: flex; + flex-direction: column; + text-align: center; + gap: 15px; + font-style: normal; + line-height: normal; + font-family: 'Google Sans', sans-serif !important; +} + +.info { + padding-bottom: 10px !important; + small { + color: #a8a8b5; + } +} diff --git a/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/app/components/login/login.component.spec.ts b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/app/components/login/login.component.spec.ts new file mode 100644 index 0000000..cb6b0f3 --- /dev/null +++ b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/app/components/login/login.component.spec.ts @@ -0,0 +1,38 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {ComponentFixture, TestBed} from '@angular/core/testing'; + +import {LoginComponent} from './login.component'; + +describe('LoginComponent', () => { + let component: LoginComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [LoginComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(LoginComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/app/components/login/login.component.ts b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/app/components/login/login.component.ts new file mode 100644 index 0000000..37b3e71 --- /dev/null +++ b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/app/components/login/login.component.ts @@ -0,0 +1,92 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {Component, NgZone, inject} from '@angular/core'; +import {Auth, signInWithPopup, GoogleAuthProvider} from '@angular/fire/auth'; +import {Router} from '@angular/router'; +import {AuthService} from 'src/app/services/login/auth.service'; +import {MatSnackBar} from '@angular/material/snack-bar'; +import {environment} from 'src/environments/environment'; +import {ToastMessageComponent} from '../toast-message/toast-message.component'; + +const HOME_ROUTE = '/'; + +interface LooseObject { + [key: string]: any; +} + +@Component({ + selector: 'app-login', + templateUrl: './login.component.html', + styleUrls: ['./login.component.scss'], +}) +export class LoginComponent { + private readonly auth: Auth = inject(Auth); + private readonly provider: GoogleAuthProvider = new GoogleAuthProvider(); + loader = false; + chatbotName: string = environment.chatbotName; + + constructor( + private authService: AuthService, + private router: Router, + public ngZone: NgZone, + private _snackBar: MatSnackBar + ) { + this.provider.setCustomParameters({ + prompt: 'select_account', + }); + } + + loginWithGoogle() { + this.loader = true; + signInWithPopup(this.auth, this.provider) + .then((result: any) => { + const user = result.user.toJSON(); + this.ngZone.run(() => { + this.authService.saveUserSession(user.stsTokenManager.accessToken); + this.redirect(user); + }); + }) + .catch(error => { + this.loader = false; + if (error.message !== 'Firebase: Error (auth/popup-closed-by-user).') { + this._snackBar.openFromComponent(ToastMessageComponent, { + panelClass: ['red-toast'], + verticalPosition: 'top', + horizontalPosition: 'right', + duration: 5000, + data: { + text: 'Error with SignIn. Please try again later !!!', + icon: 'cross-in-circle-white', + }, + }); + } + console.error(`Error: ${error}`); + }); + } + + redirect(user: any) { + const userDetails: LooseObject = {}; + userDetails['name'] = user.displayName; + userDetails['email'] = user.email; + userDetails['photoURL'] = user.photoURL; + userDetails['domain'] = user.domain; + (userDetails['uid'] = user.uid), + localStorage.setItem('USER_DETAILS', JSON.stringify(userDetails)); + this.loader = false; + this.router.navigate([HOME_ROUTE]); + } +} diff --git a/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/app/components/main/chat-input/chat-input.component.html b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/app/components/main/chat-input/chat-input.component.html new file mode 100644 index 0000000..cd2a76a --- /dev/null +++ b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/app/components/main/chat-input/chat-input.component.html @@ -0,0 +1,61 @@ + + + diff --git a/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/app/components/main/chat-input/chat-input.component.scss b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/app/components/main/chat-input/chat-input.component.scss new file mode 100644 index 0000000..814f6c1 --- /dev/null +++ b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/app/components/main/chat-input/chat-input.component.scss @@ -0,0 +1,146 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +.chat-footer { + min-height: 60px; + display: flex; + border: 1px solid #dde6f6; + box-shadow: 0px 4px 18px rgba(195, 209, 226, 0.25); + border-radius: 30px; + width: 90vw; + backdrop-filter: blur(10px); + + .chat-textarea { + flex-grow: 1; + display: flex; + align-items: center; + + input { + width: 100%; + height: 45px; + border: 0px; + border-radius: 20px; + resize: none; + outline: none; + padding: 14px; + font-family: 'Google Sans', sans-serif !important; + box-sizing: border-box; + -ms-overflow-style: none; + scrollbar-width: none; + font-size: 0.95rem; + line-height: 1.1rem; + color: #534646; + margin-left: 20px; + } + + input::-webkit-scrollbar { + display: none; + } + } + + .micIcon { + display: flex; + align-items: center; + color: #1f1f1f; + margin-top: 2px; + padding-right: 1%; + + img { + max-width: 40%; + cursor: pointer; + } + } +} + +.recording-container { + display: flex; + flex-direction: row; + gap: 10px; + cursor: pointer; + align-items: baseline; +} + +.blue-dot { + /* Ellipse 1226 */ + content: ''; + width: 8px; + height: 8px; + left: 1503px; + top: 445px; + display: flow-root; + border-radius: 15px; + + /* Blue */ + background: #4285f4; + animation: blue-transform 2s infinite; +} + +.green-dot { + content: ''; + width: 8px; + height: 8px; + left: 1522px; + top: 445px; + display: flow-root; + border-radius: 15px; + + /* Green */ + background: #0f9d58; + animation: green-transform 2s infinite; +} + +.yellow-dot { + content: ''; + width: 8px; + height: 8px; + left: 1541px; + top: 445px; + display: flow-root; + border-radius: 15px; + + /* Yellow */ + background: #f4b400; + animation: yellow-transform 2s infinite; +} + +// Small devices (landscape phones, 576px and up) +@media (min-width: 576px) { +} + +// Medium devices (tablets, 768px and up) +@media (min-width: 768px) { + .chat-footer { + min-width: 720px; + } +} + +// Large devices (desktops, 992px and up) +@media (min-width: 992px) { + .chat-footer { + width: 60vw; + } +} + +// X-Large devices (large desktops, 1200px and up) +@media (min-width: 1200px) { +} + +// XX-Large devices (larger desktops, 1400px and up) +@media (min-width: 1400px) { + .chat-footer { + width: 70vw; + } +} diff --git a/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/app/components/main/chat-input/chat-input.component.spec.ts b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/app/components/main/chat-input/chat-input.component.spec.ts new file mode 100644 index 0000000..158ea34 --- /dev/null +++ b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/app/components/main/chat-input/chat-input.component.spec.ts @@ -0,0 +1,38 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {ComponentFixture, TestBed} from '@angular/core/testing'; + +import {ChatInputComponent} from './chat-input.component'; + +describe('ChatInputComponent', () => { + let component: ChatInputComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ChatInputComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(ChatInputComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/app/components/main/chat-input/chat-input.component.ts b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/app/components/main/chat-input/chat-input.component.ts new file mode 100644 index 0000000..6c02495 --- /dev/null +++ b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/app/components/main/chat-input/chat-input.component.ts @@ -0,0 +1,91 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {Component, EventEmitter, Output, Input} from '@angular/core'; +import {ActivatedRoute} from '@angular/router'; +import {SpeechToTextService} from 'src/app/services/speech-to-text'; + +@Component({ + selector: 'app-chat-input', + templateUrl: './chat-input.component.html', + styleUrls: ['./chat-input.component.scss'], +}) +export class ChatInputComponent { + isRecording = false; + transcribedText = ''; + mediaRecorder: MediaRecorder | undefined; + audioChunks: Blob[] = []; + + @Input() term = ''; + @Output() emitSearch: EventEmitter = new EventEmitter(); + + constructor( + private speechToTextService: SpeechToTextService, + private route: ActivatedRoute + ) { + const query = this.route.snapshot.queryParamMap.get('q'); + if (query) { + this.term = query; + } + } + + ngOnInit() { + navigator.mediaDevices + .getUserMedia({audio: true}) + .then(stream => this.setupMediaRecorder(stream)) + .catch(err => { + console.error(err); + }); + } + + searchTerm() { + if (this.term && this.term.trim()) { + this.emitSearch.emit(this.term.trim()); + } + } + + setupMediaRecorder(stream: MediaStream) { + this.mediaRecorder = new MediaRecorder(stream); + this.mediaRecorder.ondataavailable = event => + this.audioChunks.push(event.data); + this.mediaRecorder.onstop = () => this.sendAudioToGCP(); + } + + startRecording() { + this.isRecording = true; + this.audioChunks = []; + if (this.mediaRecorder) this.mediaRecorder.start(); + } + + stopRecording() { + this.isRecording = false; + if (this.mediaRecorder) this.mediaRecorder.stop(); + } + + async sendAudioToGCP() { + const audioBlob = new Blob(this.audioChunks); + (await this.speechToTextService.transcribeAudio(audioBlob)).subscribe( + (response: any) => { + this.term = response[0]; + this.searchTerm(); + }, + (error: any) => { + // Handle errors + console.error('Error transcribing audio:', error); + } + ); + } +} diff --git a/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/app/components/main/main.component.html b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/app/components/main/main.component.html new file mode 100644 index 0000000..8aa88a1 --- /dev/null +++ b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/app/components/main/main.component.html @@ -0,0 +1,33 @@ + + + +
    +
    +
    +
    +
    + Hey, {{ savedUser.name ? savedUser.name!.split(' ')[0] + '!' : '' }} +
    +
    How can I help you today?
    +
    +
    +
    + + +
    diff --git a/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/app/components/main/main.component.scss b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/app/components/main/main.component.scss new file mode 100644 index 0000000..fe536c3 --- /dev/null +++ b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/app/components/main/main.component.scss @@ -0,0 +1,1110 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@import url('https://fonts.googleapis.com/icon?family=Material+Icons|Material+Icons+Outlined'); + +:host + ::ng-deep + mat-form-feild + .mat-form-field-appearance-outline + .mat-form-field-outline-start { + color: red; + border-radius: 50px !important; + font-size: 10px !important; +} + +:host + ::ng-deep + .mdc-text-field--outlined + .mdc-notched-outline + .mdc-notched-outline__leading { + border-radius: 28px 0 0 28px !important; + min-width: 28px !important; + border-color: #babfc4 !important; +} + +:host + ::ng-deep + .mdc-text-field--outlined + .mdc-notched-outline + .mdc-notched-outline__trailing { + border-radius: 0 28px 28px 0 !important; + border-color: #babfc4 !important; +} + +:host ::ng-deep .mat-form-field-outline-end { + border-radius: 0 28px 28px 0 !important; +} + +:host ::ng-deep .mat-mdc-card { + box-shadow: + 0px 2px 1px -1px rgb(217 220 224), + 0px 1px 1px 0px rgba(0, 0, 0, 0.14), + 0px 1px 3px 0px rgb(229 232 234) !important; +} + +:host ::ng-deep .mat-mdc-standard-chip.mat-primary.mat-mdc-chip-selected, +.mat-mdc-standard-chip.mat-primary.mat-mdc-chip-highlighted { + --mdc-chip-elevated-container-color: #edf3fe; + --mdc-chip-elevated-disabled-container-color: #e0e0e0; + --mdc-chip-label-text-color: #4285f4; + --mdc-chip-disabled-label-text-color: #4285f4; + --mdc-chip-with-icon-icon-color: #4285f4; + --mdc-chip-with-icon-disabled-icon-color: #4285f4; + --mdc-chip-with-trailing-icon-disabled-trailing-icon-color: #4285f4; + --mdc-chip-with-trailing-icon-trailing-icon-color: #4285f4; + --mdc-chip-with-icon-selected-icon-color: #4285f4; +} + +.parent-container { + display: flex; + max-height: calc(100vh - 70px); + height: 90vh; + font-family: 'Google Sans', sans-serif !important; + overflow-y: auto; + flex-direction: column; + justify-content: center; + + .img-globe { + // position:absolute; + width: 66%; + } +} + +.wrapping-container { + width: 100%; +} + +.main-div { + font-family: 'Google Sans', sans-serif !important; +} + +.text-control { + width: 32%; +} + +h1 { + line-height: 50px; + width: 34%; +} + +h2 { + font-family: 'Google Sans', sans-serif !important; +} + +.header { + -webkit-background-clip: text; + -webkit-text-fill-color: #48b975; + background-clip: text; + font-size: 3rem; + text-align: center; + font-weight: bold; + font-weight: 700; + -webkit-font-smoothing: antialiased; + height: 100%; + font-family: 'Google Sans', sans-serif; +} + +.send-button { + color: #babfc4; +} + +.logo-icon { + height: 120px; + width: 400px; + margin-bottom: 90px; + margin-left: -20px; +} + +.user-info { + display: flex; + justify-content: center; +} + +.user-info-column { + display: column; +} + +.card-outer-div { + .card-inside-left { + .circle { + width: 100px; + height: 100px; + border: 5px solid rgb(255, 255, 255); + border-radius: 50%; + overflow: hidden; + box-shadow: + 0px 2px 1px -1px rgba(0, 0, 0, 0.2), + 0px 1px 1px 0px rgba(0, 0, 0, 0.14), + 0px 1px 3px 0px rgba(0, 0, 0, 0.12); + } + + .circle img { + width: 100%; + height: 100%; + object-fit: cover; + } + } + + .card-inside-right { + width: 80%; + display: flex; + flex-direction: column; + align-items: center; + color: #747474; + font-size: 0.8rem; + font-weight: 500; + + .name { + .title { + font: 1rem; + } + + .sub-title { + font-size: 0.9rem; + margin-left: 2%; + } + } + + .company { + display: flex; + align-items: center; + + .logo { + padding-right: 1%; + } + + .text { + width: 100%; + + .sub-title { + margin-left: 2%; + } + } + } + } +} + +.mat-mdc-standard-chip { + --mdc-chip-label-text-color: #4285f4; + --mdc-chip-elevated-container-color: #fff; + border: 1px solid #4285f4 !important; + font-family: 'Google Sans', sans-serif !important; + font-size: 15px !important; + font-weight: 500 !important; + padding-left: 12px !important; + padding-right: 12px !important; +} + +.sub-heading { + font-family: 'Google Sans', sans-serif; + color: #161a1d; + font-size: 30px; + line-height: 40px; + margin-top: -20px; + text-align: left; + z-index: 999; +} + +.genAI { + //background-image: linear-gradient(to left, #4285F4, #DB4437, #F4B400, #0F9D58); + // background-image: linear-gradient(to left, #34A853, #FBBC05, #EA4335, #1A73E8); + background: linear-gradient( + 86.95deg, + #34a853 -65.34%, + #fbbc05 -12.75%, + #ea4335 25.15%, + #1a73e8 103.88% + ); + -webkit-background-clip: text; + -moz-background-clip: text; + background-clip: text; + color: transparent; +} + +.margin-div { + margin-top: -20px; +} + +.chat-footer-outer { + display: flex; + align-items: center; + width: 100%; + justify-content: center; + + .chat-footer { + display: flex; + border: 1px solid #dde6f6; + box-shadow: 0px 4px 18px rgba(195, 209, 226, 0.25); + border-radius: 20px; + width: 80%; + backdrop-filter: blur(10px); + + .chat-textarea { + flex-grow: 1; + display: flex; + align-items: center; + + input { + width: 100%; + height: 45px; + border: 0px; + border-radius: 4px; + resize: none; + outline: none; + padding: 14px; + font-family: 'Google Sans', sans-serif !important; + box-sizing: border-box; + -ms-overflow-style: none; + scrollbar-width: none; + font-size: 0.95rem; + line-height: 1.1rem; + color: #534646; + margin-left: 20px; + } + + input::-webkit-scrollbar { + display: none; + } + } + + .micIcon { + display: flex; + align-items: center; + color: #1f1f1f; + margin-top: 2px; + padding-right: 1%; + + img { + max-width: 40%; + cursor: pointer; + } + } + } +} + +.text-gnosis { + color: var(--White, #fff); + font-size: 16px; + font-style: normal; + font-weight: 400; + line-height: 24px; + /* 150% */ + letter-spacing: 0.4px; +} + +.chip-cancel-button { + color: #002dff; +} + +.categories-div { + .mat-mdc-standard-chip { + --mdc-chip-label-text-color: #0f9d58; + --mdc-chip-elevated-container-color: #fff; + border: 1px solid #0f9d58 !important; + font-family: 'Google Sans', sans-serif !important; + // font-weight: 600; + } +} + +.intent-chip { + .mat-mdc-standard-chip { + --mdc-chip-label-text-color: #4285f4; + --mdc-chip-elevated-container-color: #fff; + border: 1px solid #4285f4 !important; + font-family: 'Google Sans', sans-serif !important; + // font-weight: 600; + } + + .chip-cancel-button { + color: #002dff; + } +} + +.category-chip { + .mat-mdc-standard-chip { + --mdc-chip-label-text-color: #0f9d58; + --mdc-chip-elevated-container-color: #fff; + border: 1px solid #0f9d58 !important; + font-family: 'Google Sans', sans-serif !important; + // font-weight: 600; + } + + .chip-cancel-button { + color: #007d06; + } +} + +.questions-div { + flex-direction: column; + margin-top: 2rem; + margin-bottom: 5rem; + + .intent-card { + padding: 0px; + } + + .intent-heading { + color: #787978; + font-size: 16px; + font-style: normal; + font-weight: 400; + line-height: normal; + font-family: 'Google Sans', sans-serif !important; + width: 66%; + } + + .intent-container { + border-radius: 8px; + background-color: rgba(246, 249, 254, 1); + width: 66%; + box-shadow: none !important; + flex-shrink: 0; + + .selected-question { + // margin: 0.5rem 0rem; + &:hover { + background-color: rgba(66, 133, 244, 0.1); + } + } + + .question-item { + display: flex; + justify-content: space-between; + font-family: 'Google Sans', sans-serif !important; + color: #787978; + font-size: 1em; + line-height: 24px; + } + + .question-link { + overflow: hidden; + text-overflow: ellipsis; + } + } +} + +.intent-chip-textarea ::ng-deep .mdc-evolution-chip-set__chips { + align-items: center !important; +} + +.float-button { + position: fixed; + right: 3em; + bottom: 6em; + --mdc-fab-container-color: #4285f4; +} + +.name-container { + font-size: 3.5rem; + font-weight: 400; + line-height: 4rem; + font-family: + Google Sans, + Helvetica Neue, + sans-serif; + letter-spacing: -0.03em; + background: linear-gradient( + 89.9deg, + rgba(66, 133, 244, 0.95) 0.17%, + #a488f5 7.34%, + rgba(234, 67, 53, 0.88) 24.26% + ); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; + text-fill-color: transparent; + animation: effect 2s linear; + display: flex; + flex-direction: row; + gap: 1%; + align-items: center; + justify-content: center; +} + +@keyframes effect { + 0% { + background: linear-gradient( + 89.9deg, + rgba(240, 216, 214, 0.88) 0.17%, + #ebcaec 7.34%, + rgba(194, 214, 247, 0.95) 24.26% + ); + -webkit-background-clip: text; + } + + 10% { + background: linear-gradient( + 89.9deg, + rgba(227, 237, 253, 0.95) 0.17%, + #e5ddfd 7.34%, + rgba(186, 210, 248, 0.95) 24.26% + ); + -webkit-background-clip: text; + } + + 20% { + background: linear-gradient( + 89.9deg, + rgba(218, 231, 253, 0.95) 0.17%, + #dcd1fc 7.34%, + rgba(172, 202, 252, 0.95) 24.26% + ); + -webkit-background-clip: text; + } + + 30% { + background: linear-gradient( + 89.9deg, + rgba(199, 219, 252, 0.95) 0.17%, + #d0c2fa 7.34%, + rgba(161, 196, 252, 0.95) 24.26% + ); + -webkit-background-clip: text; + } + + 40% { + background: linear-gradient( + 89.9deg, + rgba(169, 201, 253, 0.95) 0.17%, + #cdbcff 7.34%, + rgba(149, 188, 250, 0.95) 24.26% + ); + -webkit-background-clip: text; + } + + 50% { + background: linear-gradient( + 89.9deg, + rgba(200, 220, 253, 0.95) 0.17%, + #bba5fc 7.34%, + rgba(203, 221, 250, 0.95) 24.26% + ); + -webkit-background-clip: text; + } + + 60% { + background: linear-gradient( + 89.9deg, + rgba(227, 237, 253, 0.95) 0.17%, + #e5ddfd 7.34%, + rgba(186, 210, 248, 0.95) 24.26% + ); + -webkit-background-clip: text; + } + + 70% { + background: linear-gradient( + 89.9deg, + rgba(183, 210, 253, 0.95) 0.17%, + #e5ddfd 7.34%, + rgba(248, 217, 215, 0.88) 24.26% + ); + -webkit-background-clip: text; + } + + 80% { + background: linear-gradient( + 89.9deg, + rgba(122, 172, 252, 0.95) 0.17%, + #e5ddfd 7.34%, + rgba(247, 182, 176, 0.88) 24.26% + ); + -webkit-background-clip: text; + } + + 90% { + background: linear-gradient( + 89.9deg, + rgba(94, 148, 236, 0.95) 0.17%, + #b49ff1 7.34%, + rgba(240, 121, 111, 0.88) 24.26% + ); + -webkit-background-clip: text; + } + + 100% { + background: linear-gradient( + 89.9deg, + rgba(66, 133, 244, 0.95) 0.17%, + #a488f5 7.34%, + rgba(234, 67, 53, 0.88) 24.26% + ); + -webkit-background-clip: text; + } +} + +.helper-container { + white-space: nowrap; + overflow: hidden; + font-size: 1.5rem; + font-weight: 400; + line-height: 4rem; + font-family: + Google Sans, + Helvetica Neue, + sans-serif; + letter-spacing: -0.03em; + color: #8f9391; + padding-bottom: 1%; + opacity: 0.5; + animation: + typing 2s steps(22), + blink 0.5s step-end infinite alternate; + justify-content: center; + display: flex; +} + +@keyframes typing { + from { + width: 0; + } +} + +@keyframes blink { + 50% { + border-color: transparent; + } +} + +.quick-bot-introduction-container { + width: 100%; + display: flex; + flex-direction: row; + justify-content: center; + + .introduction-sub-container { + width: 66%; + justify-content: center; + text-align: center; + display: flex; + flex-direction: column; + padding-top: 2%; + padding-bottom: 2%; + background: + radial-gradient(white, #fafcff) padding-box, + linear-gradient( + 89.86deg, + #34a853 -40.93%, + #fbbc05 4.92%, + #ea4335 37.97%, + #1a73e8 106.62% + ) + border-box; + border-radius: 24px; + border: 1px solid transparent; + } + + .logo-image-container { + img { + height: 110px; + width: 110px; + } + } + + .logo-text-heading { + font-style: normal; + font-weight: 500; + font-size: 28px; + line-height: 202%; + /* or 65px */ + text-align: center; + + background: linear-gradient( + 89.86deg, + #34a853 -40.93%, + #fbbc05 4.92%, + #ea4335 37.97%, + #1a73e8 106.62% + ); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; + text-fill-color: transparent; + } + + .logo-text-subheading { + font-style: normal; + font-weight: 500; + font-size: 21px; + line-height: 202%; + /* or 48px */ + text-align: center; + } + + .blue { + color: #4285f4; + } +} + +.helpButton { + position: fixed !important; + bottom: 45px !important; + right: 20px !important; + background-color: #4285f4 !important; + color: white !important; + text-decoration: none !important; + border-radius: 60px !important; + height: 41px !important; + display: inline-flex !important; + align-items: center !important; + overflow: hidden !important; + width: auto !important; + max-width: 41px !important; + -webkit-transition: max-width 0.5s !important; + transition: max-width 0.5s !important; + z-index: 1000 !important; + + &:hover { + max-width: 300px !important; + } + + .text { + white-space: nowrap !important; + padding-right: 15px !important; + font-size: 15px !important; + font-weight: 600 !important; + } +} + +::placeholder { + color: #646566; +} + +.intent-container-box { + box-sizing: border-box; + width: 239px; + height: 239px; + border-radius: 14px; + transition-delay: 2s; + transition: width 8s; + padding: 1%; + cursor: pointer; +} + +.selected-intent-box { + box-sizing: border-box; + height: 239px; + border-radius: 14px; + transition-delay: 2s; + transition: width 8s; + cursor: pointer; + width: max-content !important; + max-width: 100%; + display: flex; + flex-direction: column; + gap: 1%; + padding: 1%; + background: + linear-gradient( + 92.1deg, + #bdd1f3 19.16%, + rgb(199 183 245 / 90%) 79%, + rgb(235 165 158 / 90%) 135.24% + ), + linear-gradient( + 272.04deg, + rgba(158, 188, 237, 0.7) 14.93%, + rgba(158, 188, 237, 0) 104.51% + ) !important; +} + +.cards-outer-container { + width: 65%; + overflow: auto; + scrollbar-width: none; +} + +.cards-outer-container:after { + z-index: -1; + content: ''; + width: 10%; + opacity: 0.5 !important; +} + +.cards-container-all-categories { + width: max-content; + display: flex; + overflow-x: auto; + gap: 1%; + max-width: 160%; + min-width: 100%; + scrollbar-width: none; +} + +.cards-container-few-categories { + width: max-content; + display: flex; + overflow-x: auto; + gap: 1%; + max-width: 160%; + min-width: 100%; + scrollbar-width: none; + justify-content: center; + padding-inline: 5%; +} + +.intent-container-box:hover { + background: #dde3ea !important; +} + +.card-detail-container { + width: 100%; + min-width: 250px; + display: flex; + flex-direction: row; +} + +.selected-intent-suggested-question { + gap: 4%; + display: flex !important; + flex-direction: column; +} + +.suggested-question-chip-row { + margin-top: 0.5rem; + + .suggested-question-chip { + display: flex; + flex-direction: row; + margin: 0.5rem; + padding: 0.5rem; + width: 100%; + + .suggested-question-icon { + padding-right: 0.25rem; + } + + .suggested-question-text { + margin-left: 0.25rem; + + width: 100%; + } + } +} + +.close-button-container { + display: none !important; +} + +.expand-close-button-container { + display: block !important; + + .close-button { + margin-top: -10px !important; + } +} + +.card-heading { + padding-top: 2.5%; + font-size: 17px; + color: #1f1f1f; + width: 90%; +} + +.card-content { + font-size: 13px; + color: #646566; + padding-top: 4%; + white-space: break-spaces; + text-wrap: balance; + padding-right: 3%; + text-align: left; + width: 85%; +} + +::ng-deep .mat-mdc-snack-bar-container { + &.green-snackbar { + --mdc-snackbar-container-color: #0f9d58; + --mat-mdc-snack-bar-button-color: #fff; + --mdc-snackbar-supporting-text-color: #fff; + } + + &.red-snackbar { + --mdc-snackbar-container-color: #e9103f; + --mat-mdc-snack-bar-button-color: #fff; + --mdc-snackbar-supporting-text-color: #fff; + } +} + +::ng-deep .mat-mdc-dialog-surface { + justify-content: center !important; + display: flex !important; + flex-direction: column !important; + text-align: center !important; + background: #ffffff; + border-radius: 20px !important; + box-sizing: border-box; + box-shadow: none !important; + + .badge-prompt-parent-container { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + text-align: center; + + .badge-greetings-container { + width: 100%; + display: flex; + flex-direction: row; + justify-content: center; + + img { + width: 45%; + } + } + + .badge-text-container { + font-style: normal; + font-weight: 400; + font-size: 17px; + line-height: 45px; + display: flex; + align-items: center; + color: rgba(92, 95, 97, 0.7); + } + } +} + +.blue-dot { + /* Ellipse 1226 */ + content: ''; + width: 8px; + height: 8px; + left: 1503px; + top: 445px; + display: flow-root; + border-radius: 15px; + + /* Blue */ + background: #4285f4; + animation: blue-transform 2s infinite; +} + +.green-dot { + content: ''; + width: 8px; + height: 8px; + left: 1522px; + top: 445px; + display: flow-root; + border-radius: 15px; + + /* Green */ + background: #0f9d58; + animation: green-transform 2s infinite; +} + +.yellow-dot { + content: ''; + width: 8px; + height: 8px; + left: 1541px; + top: 445px; + display: flow-root; + border-radius: 15px; + + /* Yellow */ + background: #f4b400; + animation: yellow-transform 2s infinite; +} + +.recording-container { + display: flex; + flex-direction: row; + gap: 10px; + cursor: pointer; + align-items: baseline; +} + +@keyframes blue-transform { + 0% { + transform: translateY(-14px); + } + + 33% { + transform: translateY(0); + } + + 66% { + transform: translateY(0); + } + + 100% { + transform: translateY(-14px); + } +} + +@keyframes green-transform { + 0% { + transform: translateY(0); + } + + 33% { + transform: translateY(-14px); + } + + 66% { + transform: translateY(0); + } + + 100% { + transform: translateY(0); + } +} + +@keyframes yellow-transform { + 0% { + transform: translateY(0); + } + + 33% { + transform: translateY(0); + } + + 66% { + transform: translateY(-14px); + } + + 100% { + transform: translateY(0); + } +} + +.tooltip-container { + display: flex; + width: 100%; + justify-content: center; + align-items: baseline; + padding-top: 2%; + padding-bottom: 2%; + .tooltip-sub-container { + width: 66%; + display: flex; + align-items: baseline; + justify-content: center; + } +} + +.tooltip-icon-container { + display: flex; + flex-direction: row; + // align-self: flex-end; + gap: 10px; + flex-wrap: nowrap; + text-wrap: nowrap; + align-self: center; + color: #5f6368; + + .tooltip-label { + font-family: 'Roboto'; + font-style: normal; + font-weight: 500; + font-size: 16px; + line-height: 20px; + text-align: center; + } + + mat-icon { + font-size: 22px !important; + } +} + +.tooltip-text-container { + flex-wrap: wrap; + word-wrap: break-word; + justify-content: flex-start; + display: flex; + color: #5f6368; + .tooltip-text { + font-family: 'Roboto'; + font-style: normal; + font-weight: 400; + font-size: 16px; + line-height: 20px; + } +} + +.dismiss-container { + font-family: 'Roboto'; + font-style: normal; + font-weight: 500; + font-size: 14px; + line-height: 20px; + /* identical to box height, or 143% */ + text-align: center; + + /* Blue */ + color: #4285f4; + padding-left: 1%; + cursor: pointer; +} + +.dismiss-container:hover { + text-decoration: underline; +} + +.journey-chips { + max-height: 300px; + overflow-y: auto; +} + +::ng-deep .mat-mdc-standard-chip:hover { + background: linear-gradient( + 92.1deg, + #9ebced 19.16%, + rgba(164, 136, 245, 0.9) 79% + ) !important; + --mdc-chip-label-text-color: #fff; +} + +.suggested-question-container { + width: 60%; +} + +// Small devices (landscape phones, 576px and up) +@media (min-width: 576px) { + .helper-container { + font-size: 2rem; + } +} + +// Medium devices (tablets, 768px and up) +@media (min-width: 768px) { + .chat-footer-outer { + .chat-footer { + min-height: 60px; + width: 66%; + } + } + + .helper-container { + font-size: 3.5rem; + } +} + +// Large devices (desktops, 992px and up) +@media (min-width: 992px) { +} + +// X-Large devices (large desktops, 1200px and up) +@media (min-width: 1200px) { +} + +// XX-Large devices (larger desktops, 1400px and up) +@media (min-width: 1400px) { +} diff --git a/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/app/components/main/main.component.spec.ts b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/app/components/main/main.component.spec.ts new file mode 100644 index 0000000..66e7fd9 --- /dev/null +++ b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/app/components/main/main.component.spec.ts @@ -0,0 +1,38 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {ComponentFixture, TestBed} from '@angular/core/testing'; + +import {MainComponent} from './main.component'; + +describe('MainComponent', () => { + let component: MainComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [MainComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(MainComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/app/components/main/main.component.ts b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/app/components/main/main.component.ts new file mode 100644 index 0000000..535e406 --- /dev/null +++ b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/app/components/main/main.component.ts @@ -0,0 +1,55 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {Component, OnDestroy} from '@angular/core'; +import {UserService} from 'src/app/services/user/user.service'; +import {Router} from '@angular/router'; +import {MatDialog} from '@angular/material/dialog'; +import {ReplaySubject} from 'rxjs'; +import {SearchResponse} from 'src/app/models/search.model'; +@Component({ + selector: 'app-main', + templateUrl: './main.component.html', + styleUrls: ['./main.component.scss'], +}) +export class MainComponent implements OnDestroy { + private readonly destroyed = new ReplaySubject(1); + term = ''; + showResults = false; + searchResults: SearchResponse = { + summary: undefined, + results: [], + totalSize: 0, + }; + savedUser; + + constructor( + public userService: UserService, + private router: Router, + public dialog: MatDialog + ) { + this.savedUser = userService.getUserDetails(); + } + + goToResults(term: string) { + this.router.navigate(['/search'], {queryParams: {q: term}}); + } + + ngOnDestroy() { + this.destroyed.next(); + this.destroyed.complete(); + } +} diff --git a/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/app/components/main/search-results/search-results.component.html b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/app/components/main/search-results/search-results.component.html new file mode 100644 index 0000000..ed6908e --- /dev/null +++ b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/app/components/main/search-results/search-results.component.html @@ -0,0 +1,150 @@ + + + + +
    +
    + Image Generation +

    + Prompt: {{ selectedResult?.enhancedPrompt }} +

    +

    + {{ summary }} +

    +
    + + +
    + + +
    + Selected Image + + Image Placeholder + +
    + No images were generated for this query. +
    + + +
    + Source: {{ selectedResult?.source }} +
    +
    + + +
    + + +
    +

    Imagen Model Results

    +
    +
    +
    + Imagen Thumbnail +
    +
    + No Imagen results. +
    +
    +
    +
    + + +
    +

    Gemini 2.0 Flash Results

    +
    +
    +
    + Gemini Thumbnail +
    +
    + No Gemini results. +
    +
    +
    +
    +
    + + + +
    + + + +
    +
    +

    Configuration

    + + AI Model + + + {{ model.viewValue }} + + + + +

    Aspect Ratio

    +
    + +
    + +

    Image Style

    +
    + +
    + +
    + Number of Results + + + +
    +
    +
    +
    diff --git a/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/app/components/main/search-results/search-results.component.scss b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/app/components/main/search-results/search-results.component.scss new file mode 100644 index 0000000..d39c7ee --- /dev/null +++ b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/app/components/main/search-results/search-results.component.scss @@ -0,0 +1,155 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +.custom-heading { + margin: 0px; +} + +.mat-grid-list { + border: 1px solid #DDE6F6; + margin-top: 25px; + margin-left: 25px; + margin-right: 25px; +} + +.image-grid { + border: 1px solid #DDE6F6; +} + +.image-generation-container { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + // height: 100%; + + .image-preview { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + width: 100%; + + .main-image { + flex: 1; + display: flex; + // align-items: center; + justify-content: center; + width: 100%; + + .main-image-placeholder { + width: 100%; + background-color: #e0e0e0; + + img { + width: 100%; + height: 50vh; + } + } + } + + .thumbnail-container { + display: flex; + justify-content: center; + margin-top: 20px; + margin-bottom: 20px; + + .thumbnail { + width: 100px; + height: 100px; + margin: 0 10px; + cursor: pointer; + + .thumbnail-placeholder { + width: 100%; + height: 100%; + background-color: #f0f0f0; + + img { + width: 100%; + } + } + } + } + } +} + +.configuration { + display: flex; + flex-direction: column; + padding: 20px; + + .aspect-ratio, + .image-style { + margin-bottom: 20px; + + button { + margin: 0 10px 10px 0; + } + } + + .number-results { + display: flex; + align-items: center; + } +} + +.aspect-button { + border: 1px solid #ccc; +} + +.aspect-button.active { + background-color: #e0e0e0; + /* Example active background */ + border-color: #a0a0a0; + /* Example active border */ + font-weight: bold; + box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.2); + /* Example inset shadow for pressed look */ +} + +.style-button.active { + background-color: #3f51b5; + /* Example primary color */ + color: white; +} + +// Small devices (landscape phones, 576px and up) +@media (min-width: 576px) {} + +// Medium devices (tablets, 768px and up) +@media (min-width: 768px) {} + +// Large devices (desktops, 992px and up) +@media (min-width: 992px) { + .max-height-image { + width: auto; + } +} + +// X-Large devices (large desktops, 1200px and up) +@media (min-width: 1200px) { + .max-height-image { + max-width: 40vw; + } +} + +// XX-Large devices (larger desktops, 1400px and up) +@media (min-width: 1400px) { + .max-height-image { + max-width: 30vw; + } +} \ No newline at end of file diff --git a/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/app/components/main/search-results/search-results.component.spec.ts b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/app/components/main/search-results/search-results.component.spec.ts new file mode 100644 index 0000000..1c8bf77 --- /dev/null +++ b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/app/components/main/search-results/search-results.component.spec.ts @@ -0,0 +1,38 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {ComponentFixture, TestBed} from '@angular/core/testing'; + +import {SearchResultsComponent} from './search-results.component'; + +describe('SearchResultsComponent', () => { + let component: SearchResultsComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [SearchResultsComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(SearchResultsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/app/components/main/search-results/search-results.component.ts b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/app/components/main/search-results/search-results.component.ts new file mode 100644 index 0000000..4e2ea55 --- /dev/null +++ b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/app/components/main/search-results/search-results.component.ts @@ -0,0 +1,297 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {Component, OnDestroy, ViewChild, TemplateRef} from '@angular/core'; +import {SearchService} from 'src/app/services/search.service'; +import {ReplaySubject} from 'rxjs'; +import {UserService} from 'src/app/services/user/user.service'; +import {ActivatedRoute, Router} from '@angular/router'; +import {PDF, image_name} from 'src/environments/constant'; +import { + SafeUrl, +} from '@angular/platform-browser'; +import {CombinedImageResults, GeneratedImage} from 'src/app/models/generated-image.model'; +import {SearchRequest} from 'src/app/models/search.model'; +import {MatSnackBar} from '@angular/material/snack-bar'; +import {ToastMessageComponent} from '../../toast-message/toast-message.component'; + +interface Imagen3Model { + value: string; + viewValue: string; +} + +interface AspectRatio { + value: string; + viewValue: string; +} + +interface Style { + value: string; +} + +@Component({ + selector: 'app-search-results', + templateUrl: './search-results.component.html', + styleUrls: ['./search-results.component.scss'], +}) +export class SearchResultsComponent implements OnDestroy { + @ViewChild('preview', {static: true}) + previewRef!: TemplateRef<{}>; + summary = ''; + private readonly destroyed = new ReplaySubject(1); + searchResult: any = []; + isLoading = false; + imagenDocuments: GeneratedImage[] = []; + geminiDocuments: GeneratedImage[] = []; + showDefaultDocuments = false; + defaultPlaceholderImageUrl = 'assets/images/placeholder_image.png'; + images: any = []; + pdf = PDF; + imageName = image_name; + safeUrl: SafeUrl | undefined; + selectedResult: GeneratedImage | undefined; + selectedImageStyle = 'Modern'; + currentSearchTerm = ''; + numberOfResults = 2; + imagen3ModelsList: Imagen3Model[] = [ + {value: 'imagen-4.0-ultra-generate-exp-05-20', viewValue: 'Imagen 4.0-ultra-generate-exp-05-20'}, + {value: 'imagen-3.0-generate-001', viewValue: 'imagen-3.0-generate-001'}, + { + value: 'imagen-3.0-fast-generate-001', + viewValue: 'imagen-3.0-fast-generate-001', + }, + {value: 'imagen-3.0-generate-002', viewValue: 'imagen-3.0-generate-002'}, + {value: 'imagegeneration@006', viewValue: 'imagegeneration@006'}, + {value: 'imagegeneration@005', viewValue: 'imagegeneration@005'}, + {value: 'imagegeneration@002', viewValue: 'imagegeneration@002'}, + ]; + selectedModel = this.imagen3ModelsList[0].value; + aspectRatioList: AspectRatio[] = [ + {value: '1:1', viewValue: '1:1'}, + {value: '9:16', viewValue: '9:16'}, + {value: '16:9', viewValue: '16:9'}, + {value: '3:4', viewValue: '3:4'}, + {value: '4:3', viewValue: '4:3'}, + ]; + selectedAspectRatio = this.aspectRatioList[0]; + searchRequest: SearchRequest = { + term: '', + generationModel: this.selectedModel, + aspectRatio: this.selectedAspectRatio.value, + imageStyle: this.selectedImageStyle, + numberOfImages: this.numberOfResults, + }; + imageStyleList: Style[] = [ + {value: 'Modern'}, + {value: 'Realistic'}, + {value: 'Vintage'}, + {value: 'Monochrome'}, + {value: 'Fantasy'}, + {value: 'Sketch'}, + ]; + activatedRoute: ActivatedRoute | null | undefined; + + constructor( + private router: Router, + private route: ActivatedRoute, + private service: SearchService, + private userService: UserService, + private _snackBar: MatSnackBar + ) { + const query = this.route.snapshot.queryParamMap.get('q'); + this.userService.showLoading(); + + if (!query) { + this.selectedResult = undefined; + this.showDefaultDocuments = true; + this.userService.hideLoading(); + return; + } + + this.searchRequest.term = query || ''; + this.currentSearchTerm = query; + const newSearchRequest = this.searchRequest; + + this.service.search(newSearchRequest).subscribe({ + next: (searchResponse: CombinedImageResults) => { + this.processSearchResults(searchResponse); + this.userService.hideLoading(); + }, + error: error => { + this.handleSearchError(error) + }, + }); + } + + private handleSearchError(error: any) { + console.error('Search error:', error); + this.imagenDocuments = []; + this.geminiDocuments = []; + this.selectedResult = undefined; + this.showDefaultDocuments = true; + this.summary = 'An error occurred while generating images.'; + this.userService.hideLoading(); + this.showErrorSnackBar(error); + } + + private processSearchResults(searchResponse: CombinedImageResults) { + this.imagenDocuments = (searchResponse.imagenResults || []).map(img => ({ + ...img, + source: 'Imagen Model', + })); + this.geminiDocuments = (searchResponse.geminiResults || []).map(img => ({ + ...img, + source: 'Gemini 2.0 Model', + })); + + const hasImagenResults = this.imagenDocuments.length > 0; + const hasGeminiResults = this.geminiDocuments.length > 0; + + if (hasImagenResults || hasGeminiResults) { + this.showDefaultDocuments = false; + this.selectedResult = hasImagenResults ? this.imagenDocuments[0] : this.geminiDocuments[0]; + this.summary = this.selectedResult?.enhancedPrompt || 'Image generation results displayed.'; + } else { + this.showDefaultDocuments = true; + this.selectedResult = undefined; + this.summary = 'No images were generated for your prompt.'; + } + } + + showErrorSnackBar(error: any): void { + console.error('Search error:', error); + console.error('Search typeof error:', typeof error); + console.error('Search error?.message:', error?.message); + console.error( + 'Search typeof JSON.stringify:', + JSON.stringify(error, null, 2) + ); + + let errorMessage = ''; + if (error?.error?.detail?.[0]?.msg) + errorMessage = `${error?.error?.detail?.[0]?.msg} - ${error?.error?.detail?.[0]?.loc}`; + else if (error?.error?.detail) + if ( + error.error.detail.includes( + "The image you want to edit contains content that has been blocked because you selected the 'Don't allow' option for Person Generation." + ) + ) { + errorMessage = + 'The image you want to edit contains content that has been blocked because there are persons in it. See the safety settings documentation for more details.'; + } else errorMessage = error?.error?.detail; + else 'Error sending request. Please try again later!'; + + this._snackBar.openFromComponent(ToastMessageComponent, { + panelClass: ['red-toast'], + verticalPosition: 'top', + horizontalPosition: 'right', + duration: 10000, + data: { + text: errorMessage, + icon: 'cross-in-circle-white', + }, + }); + } + + goToResults(term: string) { + this.router.navigate(['/search'], {queryParams: {q: term}}); + } + + searchTerm({ + term, + aspectRatio, + model, + imageStyle, + numberOfImages, + }: { + term?: string | undefined; + aspectRatio?: string | undefined; + model?: string | undefined; + imageStyle?: string | undefined; + numberOfImages?: number | undefined; + }) { + if (!term) return; + + this.showDefaultDocuments = false; + this.userService.showLoading(); + this.searchResult = []; + this.summary = ''; + this.imagenDocuments = []; + this.geminiDocuments = []; + this.images = []; + + this.searchRequest.term = term || this.searchRequest.term; + this.searchRequest.aspectRatio = + aspectRatio || this.selectedAspectRatio.value; + this.searchRequest.generationModel = model || this.selectedModel; + this.searchRequest.imageStyle = imageStyle || this.selectedImageStyle; + this.searchRequest.numberOfImages = numberOfImages || this.numberOfResults; + + const newSearchRequest = this.searchRequest; + console.log('Search request:', newSearchRequest); + this.currentSearchTerm = newSearchRequest.term; + + this.service.search(newSearchRequest).subscribe({ + next: (searchResponse: CombinedImageResults) => { + this.processSearchResults(searchResponse); + this.userService.hideLoading(); + console.log('Search response:', searchResponse); + }, + error: error => { + console.error('Search error:', error); + this.userService.hideLoading(); + this.showErrorSnackBar(error); + }, + }); + + this.router.navigate([], { + relativeTo: this.activatedRoute, + queryParams: {q: this.currentSearchTerm}, + queryParamsHandling: '', + }); + } + + changeImageSelection(result: GeneratedImage) { + this.selectedResult = result; + this.summary = result.enhancedPrompt || ''; + } + + changeImagen3Model(model: Imagen3Model) { + this.selectedModel = model.value; + this.searchTerm({model: this.selectedModel}); + } + + changeAspectRatio(aspectRatio: AspectRatio) { + this.selectedAspectRatio = aspectRatio; + this.searchTerm({aspectRatio: aspectRatio.value}); + console.log('Selected Aspect Ratio:', this.selectedAspectRatio); + } + + changeImageStyle(style: Style) { + this.selectedImageStyle = style.value; + console.log('Selected Image Style:', this.selectedImageStyle); + } + + onNumberOfResultsChange(event: any) { + this.numberOfResults = event.target.value; + console.log('Selected Number of Results:', this.numberOfResults); + } + + ngOnDestroy() { + this.destroyed.next(); + this.destroyed.complete(); + } +} diff --git a/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/app/components/toast-message/toast-message.component.html b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/app/components/toast-message/toast-message.component.html new file mode 100644 index 0000000..a83d217 --- /dev/null +++ b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/app/components/toast-message/toast-message.component.html @@ -0,0 +1,26 @@ + + +
    + icon inside toast message + {{ text }} + cross icon inside toast message +
    diff --git a/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/app/components/toast-message/toast-message.component.scss b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/app/components/toast-message/toast-message.component.scss new file mode 100644 index 0000000..0d8177f --- /dev/null +++ b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/app/components/toast-message/toast-message.component.scss @@ -0,0 +1,37 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +.toast-content { + color: #ffffff; + font-family: 'Google Sans', sans-serif !important; +} + +.close-toast { + margin-left: auto; + align-self: baseline; + cursor: pointer; +} + +.red-toast { + background-color: #ed0c0c !important; +} +.green-toast { + background-color: #06865e !important; +} + +.mat-mdc-snack-bar-container .mdc-snackbar__surface { + background-color: transparent !important; +} diff --git a/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/app/components/toast-message/toast-message.component.spec.ts b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/app/components/toast-message/toast-message.component.spec.ts new file mode 100644 index 0000000..d0d405a --- /dev/null +++ b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/app/components/toast-message/toast-message.component.spec.ts @@ -0,0 +1,38 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {ComponentFixture, TestBed} from '@angular/core/testing'; + +import {ToastMessageComponent} from './toast-message.component'; + +describe('ToastMessageComponent', () => { + let component: ToastMessageComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ToastMessageComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(ToastMessageComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/app/components/toast-message/toast-message.component.ts b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/app/components/toast-message/toast-message.component.ts new file mode 100644 index 0000000..ebac36d --- /dev/null +++ b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/app/components/toast-message/toast-message.component.ts @@ -0,0 +1,40 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {Component, Inject, ViewEncapsulation} from '@angular/core'; +import {MatSnackBar, MAT_SNACK_BAR_DATA} from '@angular/material/snack-bar'; + +@Component({ + selector: 'app-toast-message', + templateUrl: './toast-message.component.html', + styleUrls: ['./toast-message.component.scss'], + encapsulation: ViewEncapsulation.None, +}) +export class ToastMessageComponent { + text: string; + icon: string; + constructor( + private _snackBar: MatSnackBar, + @Inject(MAT_SNACK_BAR_DATA) public snackBarData: any + ) { + this.text = snackBarData.text; + this.icon = snackBarData.icon; + } + + closeToast() { + this._snackBar.dismiss(); + } +} diff --git a/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/app/models/config.model.ts b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/app/models/config.model.ts new file mode 100644 index 0000000..fda930d --- /dev/null +++ b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/app/models/config.model.ts @@ -0,0 +1,20 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export type config = { + name: string; + url: string; +}; diff --git a/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/app/models/generated-image.model.ts b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/app/models/generated-image.model.ts new file mode 100644 index 0000000..3e62e86 --- /dev/null +++ b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/app/models/generated-image.model.ts @@ -0,0 +1,34 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +type Image = { + gcsUri?: string; + imageBytes?: ArrayBuffer; + encodedImage?: string; + mimeType?: string; +}; + +export type GeneratedImage = { + image?: Image; + raiFilteredReason?: string; + enhancedPrompt?: string; + source?: string +}; + +export interface CombinedImageResults { + geminiResults: GeneratedImage[]; + imagenResults: GeneratedImage[]; +} \ No newline at end of file diff --git a/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/app/models/search.model.ts b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/app/models/search.model.ts new file mode 100644 index 0000000..bb64fd7 --- /dev/null +++ b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/app/models/search.model.ts @@ -0,0 +1,56 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export type SearchRequest = { + term: string; + generationModel: string; + aspectRatio: string; + imageStyle: string; + numberOfImages: number; +}; + +export type SearchResponse = { + summary: any; + results: SearchResult[]; + totalSize: number; +}; + +export type SearchResult = { + document: Document; +}; + +export type Document = { + derivedStructData: DocumentData; +}; + +export type DocumentData = { + title: string; + link: string; + snippets: Snippet[]; + pagemap: PageMap; +}; + +export type Snippet = { + snippet: string; +}; + +export type PageMap = { + cse_image: ImagesData[]; +}; + +export type ImagesData = { + src: string; +}; diff --git a/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/app/pipes/truncate.pipe.spec.ts b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/app/pipes/truncate.pipe.spec.ts new file mode 100644 index 0000000..82e65c6 --- /dev/null +++ b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/app/pipes/truncate.pipe.spec.ts @@ -0,0 +1,24 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {TruncatePipe} from './truncate.pipe'; + +describe('TruncatePipe', () => { + it('create an instance', () => { + const pipe = new TruncatePipe(); + expect(pipe).toBeTruthy(); + }); +}); diff --git a/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/app/pipes/truncate.pipe.ts b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/app/pipes/truncate.pipe.ts new file mode 100644 index 0000000..ae06a3b --- /dev/null +++ b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/app/pipes/truncate.pipe.ts @@ -0,0 +1,26 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {Pipe, PipeTransform} from '@angular/core'; + +@Pipe({ + name: 'truncate', +}) +export class TruncatePipe implements PipeTransform { + transform(value: string, limit = 300, trail = '...'): string { + return value.length > limit ? value.substring(0, limit) + trail : value; + } +} diff --git a/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/app/services/login/auth.guard.spec.ts b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/app/services/login/auth.guard.spec.ts new file mode 100644 index 0000000..8284e7a --- /dev/null +++ b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/app/services/login/auth.guard.spec.ts @@ -0,0 +1,32 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {TestBed} from '@angular/core/testing'; + +import {AuthGuard} from './auth.guard'; + +describe('AuthGuard', () => { + let guard: AuthGuard; + + beforeEach(() => { + TestBed.configureTestingModule({}); + guard = TestBed.inject(AuthGuard); + }); + + it('should be created', () => { + expect(guard).toBeTruthy(); + }); +}); diff --git a/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/app/services/login/auth.guard.ts b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/app/services/login/auth.guard.ts new file mode 100644 index 0000000..fd95abd --- /dev/null +++ b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/app/services/login/auth.guard.ts @@ -0,0 +1,54 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {Injectable} from '@angular/core'; +import { + ActivatedRouteSnapshot, + CanActivate, + Router, + RouterStateSnapshot, + UrlTree, +} from '@angular/router'; +import {Observable} from 'rxjs'; +import {AuthService} from './auth.service'; +import {environment} from 'src/environments/environment'; + +const LOGIN_ROUTE = '/login'; + +@Injectable({ + providedIn: 'root', +}) +export class AuthGuard implements CanActivate { + constructor( + private auth: AuthService, + private router: Router + ) {} + + canActivate( + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot + ): + | Observable + | Promise + | boolean + | UrlTree { + if (environment.requiredLogin === 'True' && !this.auth.isLoggedIn()) { + this.router.navigate([LOGIN_ROUTE]); + return false; + } + return true; + } +} diff --git a/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/app/services/login/auth.service.spec.ts b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/app/services/login/auth.service.spec.ts new file mode 100644 index 0000000..f08aa1a --- /dev/null +++ b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/app/services/login/auth.service.spec.ts @@ -0,0 +1,32 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {TestBed} from '@angular/core/testing'; + +import {AuthService} from './auth.service'; + +describe('AuthService', () => { + let service: AuthService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(AuthService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/app/services/login/auth.service.ts b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/app/services/login/auth.service.ts new file mode 100644 index 0000000..325d42e --- /dev/null +++ b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/app/services/login/auth.service.ts @@ -0,0 +1,57 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {Injectable} from '@angular/core'; +import {Router} from '@angular/router'; +import {environment} from 'src/environments/environment'; + +const USER_TOKEN_KEY = 'gpau_id'; +const USER_DETAILS = 'USER_DETAILS'; +const LOGIN_ROUTE = '/login'; + +@Injectable({ + providedIn: 'root', +}) +export class AuthService { + private requiredLogin: boolean = environment.requiredLogin === 'True'; + + constructor(private router: Router) {} + + saveUserSession(token: string) { + localStorage.setItem(USER_TOKEN_KEY, token); + } + + logout(route: string = LOGIN_ROUTE) { + localStorage.removeItem(USER_TOKEN_KEY); + localStorage.removeItem(USER_DETAILS); + localStorage.removeItem('showTooltip'); + this.router.navigateByUrl(route); + } + + isLoggedIn() { + const isLoggedIn = localStorage.getItem(USER_TOKEN_KEY) !== null; + if (!isLoggedIn && this.router.url !== LOGIN_ROUTE) { + this.router.navigate([LOGIN_ROUTE]); + } + return isLoggedIn; + } + + isUserLoggedIn() { + if (!this.requiredLogin) return true; + const isLoggedIn = localStorage.getItem(USER_TOKEN_KEY) !== null; + return isLoggedIn; + } +} diff --git a/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/app/services/search.service.spec.ts b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/app/services/search.service.spec.ts new file mode 100644 index 0000000..b024e3b --- /dev/null +++ b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/app/services/search.service.spec.ts @@ -0,0 +1,32 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {TestBed} from '@angular/core/testing'; + +import {SearchService} from './search.service'; + +describe('SearchService', () => { + let service: SearchService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(SearchService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/app/services/search.service.ts b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/app/services/search.service.ts new file mode 100644 index 0000000..ea7fc74 --- /dev/null +++ b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/app/services/search.service.ts @@ -0,0 +1,37 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {HttpClient} from '@angular/common/http'; +import {Injectable} from '@angular/core'; +import {environment} from 'src/environments/environment'; +import {map} from 'rxjs/operators'; +import {SearchRequest} from '../models/search.model'; +import {CombinedImageResults} from '../models/generated-image.model'; + +const searchURL = `${environment.backendURL}/search`; + +@Injectable({ + providedIn: 'root', +}) +export class SearchService { + constructor(private http: HttpClient) {} + + search(searchRequest: SearchRequest) { + return this.http + .post(searchURL, searchRequest) + .pipe(map(response => response as CombinedImageResults)); + } +} diff --git a/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/app/services/speech-to-text.ts b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/app/services/speech-to-text.ts new file mode 100644 index 0000000..16b1469 --- /dev/null +++ b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/app/services/speech-to-text.ts @@ -0,0 +1,34 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {Injectable} from '@angular/core'; +import {HttpClient} from '@angular/common/http'; +import {environment} from 'src/environments/environment'; + +const audioChatUrl = `${environment.backendURL}/audio_chat`; + +@Injectable({ + providedIn: 'root', +}) +export class SpeechToTextService { + constructor(private http: HttpClient) {} + + async transcribeAudio(audioBlob: Blob) { + const formData = new FormData(); + formData.append('audio_file', audioBlob, 'audio.wav'); + return this.http.post(audioChatUrl, formData); + } +} diff --git a/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/app/services/user/session.service.spec.ts b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/app/services/user/session.service.spec.ts new file mode 100644 index 0000000..ff45965 --- /dev/null +++ b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/app/services/user/session.service.spec.ts @@ -0,0 +1,32 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {TestBed} from '@angular/core/testing'; + +import {SessionService} from './session.service'; + +describe('SessionService', () => { + let service: SessionService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(SessionService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/app/services/user/session.service.ts b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/app/services/user/session.service.ts new file mode 100644 index 0000000..4a51a17 --- /dev/null +++ b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/app/services/user/session.service.ts @@ -0,0 +1,35 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {Injectable} from '@angular/core'; +import {v4 as uuid} from 'uuid'; + +const SESSION_KEY = 'pasid'; + +@Injectable({ + providedIn: 'root', +}) +export class SessionService { + constructor() {} + + getSession(): string | null { + return sessionStorage.getItem(SESSION_KEY); + } + + createSession() { + sessionStorage.setItem(SESSION_KEY, uuid()); + } +} diff --git a/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/app/services/user/user.service.ts b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/app/services/user/user.service.ts new file mode 100644 index 0000000..cf426f6 --- /dev/null +++ b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/app/services/user/user.service.ts @@ -0,0 +1,61 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {Injectable} from '@angular/core'; +import {BehaviorSubject} from 'rxjs'; +import {environment} from 'src/environments/environment'; + +type UserStored = { + uid?: string; + name?: string; + email?: string; + photoURL?: string; + displayName?: string; + domain?: string; +}; + +@Injectable({ + providedIn: 'root', +}) +export class UserService { + readonly loadingSubject = new BehaviorSubject(false); + requiredLogin: string = environment.requiredLogin; + + constructor() {} + + setUserDetails(userStored: UserStored) {} + + showLoading() { + this.loadingSubject.next(true); + } + + hideLoading() { + this.loadingSubject.next(false); + } + + getUserDetails(): UserStored { + if ( + this.requiredLogin === 'True' && + localStorage.getItem('USER_DETAILS') !== null + ) { + const userObj = localStorage.getItem('USER_DETAILS'); + return JSON.parse(userObj || '{}'); + } else { + const userDetails: UserStored = {}; + return userDetails; + } + } +} diff --git a/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/assets/.gitkeep b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/assets/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/assets/gcloud-logo.svg b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/assets/gcloud-logo.svg new file mode 100644 index 0000000..5150056 --- /dev/null +++ b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/assets/gcloud-logo.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/assets/images/SocialHeaderTemplate8.png b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/assets/images/SocialHeaderTemplate8.png new file mode 100644 index 0000000..88f4306 Binary files /dev/null and b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/assets/images/SocialHeaderTemplate8.png differ diff --git a/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/assets/images/answernotfound.png b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/assets/images/answernotfound.png new file mode 100644 index 0000000..874c87d Binary files /dev/null and b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/assets/images/answernotfound.png differ diff --git a/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/assets/images/attached.svg b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/assets/images/attached.svg new file mode 100644 index 0000000..ba09784 --- /dev/null +++ b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/assets/images/attached.svg @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/assets/images/available.svg b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/assets/images/available.svg new file mode 100644 index 0000000..5a460d0 --- /dev/null +++ b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/assets/images/available.svg @@ -0,0 +1,4 @@ + + + + diff --git a/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/assets/images/badge-banner.svg b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/assets/images/badge-banner.svg new file mode 100644 index 0000000..a38efec --- /dev/null +++ b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/assets/images/badge-banner.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/assets/images/badge.svg b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/assets/images/badge.svg new file mode 100644 index 0000000..3fc6b94 --- /dev/null +++ b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/assets/images/badge.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/assets/images/books.png b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/assets/images/books.png new file mode 100644 index 0000000..94feabc Binary files /dev/null and b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/assets/images/books.png differ diff --git a/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/assets/images/bot_logo.svg b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/assets/images/bot_logo.svg new file mode 100644 index 0000000..4be6c2f --- /dev/null +++ b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/assets/images/bot_logo.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/assets/images/cancel-toast.svg b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/assets/images/cancel-toast.svg new file mode 100644 index 0000000..9db4780 --- /dev/null +++ b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/assets/images/cancel-toast.svg @@ -0,0 +1,4 @@ + + + + diff --git a/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/assets/images/chatIcon.svg b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/assets/images/chatIcon.svg new file mode 100644 index 0000000..3303847 --- /dev/null +++ b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/assets/images/chatIcon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/assets/images/chat_background.png b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/assets/images/chat_background.png new file mode 100644 index 0000000..701ec81 Binary files /dev/null and b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/assets/images/chat_background.png differ diff --git a/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/assets/images/colibri_image_fx.jpg b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/assets/images/colibri_image_fx.jpg new file mode 100644 index 0000000..94eff87 Binary files /dev/null and b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/assets/images/colibri_image_fx.jpg differ diff --git a/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/assets/images/crane_image_fx.jpg b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/assets/images/crane_image_fx.jpg new file mode 100644 index 0000000..b6d1c73 Binary files /dev/null and b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/assets/images/crane_image_fx.jpg differ diff --git a/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/assets/images/cross-in-circle-white copy.svg b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/assets/images/cross-in-circle-white copy.svg new file mode 100644 index 0000000..96a0fe3 --- /dev/null +++ b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/assets/images/cross-in-circle-white copy.svg @@ -0,0 +1,3 @@ + + + diff --git a/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/assets/images/cross-in-circle-white.svg b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/assets/images/cross-in-circle-white.svg new file mode 100644 index 0000000..96a0fe3 --- /dev/null +++ b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/assets/images/cross-in-circle-white.svg @@ -0,0 +1,3 @@ + + + diff --git a/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/assets/images/cross-in-circle.svg b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/assets/images/cross-in-circle.svg new file mode 100644 index 0000000..c5c50ea --- /dev/null +++ b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/assets/images/cross-in-circle.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/assets/images/delete_notification_dialog_image.png b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/assets/images/delete_notification_dialog_image.png new file mode 100644 index 0000000..5afbf5c Binary files /dev/null and b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/assets/images/delete_notification_dialog_image.png differ diff --git a/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/assets/images/doc.png b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/assets/images/doc.png new file mode 100644 index 0000000..0b21898 Binary files /dev/null and b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/assets/images/doc.png differ diff --git a/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/assets/images/error_img.svg b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/assets/images/error_img.svg new file mode 100644 index 0000000..ac20193 --- /dev/null +++ b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/assets/images/error_img.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/assets/images/feedback-container.svg b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/assets/images/feedback-container.svg new file mode 100644 index 0000000..7b0f481 --- /dev/null +++ b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/assets/images/feedback-container.svg @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/assets/images/footer.png b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/assets/images/footer.png new file mode 100644 index 0000000..eda0b3b Binary files /dev/null and b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/assets/images/footer.png differ diff --git a/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/assets/images/gcloudlight1.png b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/assets/images/gcloudlight1.png new file mode 100644 index 0000000..5ddb77c Binary files /dev/null and b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/assets/images/gcloudlight1.png differ diff --git a/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/assets/images/gnosis-logo.svg b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/assets/images/gnosis-logo.svg new file mode 100644 index 0000000..17d0b7f --- /dev/null +++ b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/assets/images/gnosis-logo.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/assets/images/gsd-icon.png b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/assets/images/gsd-icon.png new file mode 100644 index 0000000..abeacf9 Binary files /dev/null and b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/assets/images/gsd-icon.png differ diff --git a/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/assets/images/home_bottom.png b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/assets/images/home_bottom.png new file mode 100644 index 0000000..78d1c92 Binary files /dev/null and b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/assets/images/home_bottom.png differ diff --git a/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/assets/images/hot_air_baloon_image_fx.jpg b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/assets/images/hot_air_baloon_image_fx.jpg new file mode 100644 index 0000000..19064b4 Binary files /dev/null and b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/assets/images/hot_air_baloon_image_fx.jpg differ diff --git a/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/assets/images/img.png b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/assets/images/img.png new file mode 100644 index 0000000..53bc884 Binary files /dev/null and b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/assets/images/img.png differ diff --git a/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/assets/images/left_loader_images/Left/1.png b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/assets/images/left_loader_images/Left/1.png new file mode 100644 index 0000000..d930ffb Binary files /dev/null and b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/assets/images/left_loader_images/Left/1.png differ diff --git a/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/assets/images/left_loader_images/Left/2.png b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/assets/images/left_loader_images/Left/2.png new file mode 100644 index 0000000..ac32f6a Binary files /dev/null and b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/assets/images/left_loader_images/Left/2.png differ diff --git a/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/assets/images/left_loader_images/Left/3.png b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/assets/images/left_loader_images/Left/3.png new file mode 100644 index 0000000..cee4db0 Binary files /dev/null and b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/assets/images/left_loader_images/Left/3.png differ diff --git a/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/assets/images/left_loader_images/Left/4.png b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/assets/images/left_loader_images/Left/4.png new file mode 100644 index 0000000..77a6d94 Binary files /dev/null and b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/assets/images/left_loader_images/Left/4.png differ diff --git a/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/assets/images/left_loader_images/Left/5.png b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/assets/images/left_loader_images/Left/5.png new file mode 100644 index 0000000..deba49b Binary files /dev/null and b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/assets/images/left_loader_images/Left/5.png differ diff --git a/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/assets/images/left_loader_images/Left/6.png b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/assets/images/left_loader_images/Left/6.png new file mode 100644 index 0000000..61e1988 Binary files /dev/null and b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/assets/images/left_loader_images/Left/6.png differ diff --git a/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/assets/images/login-globe.png b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/assets/images/login-globe.png new file mode 100644 index 0000000..f64021b Binary files /dev/null and b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/assets/images/login-globe.png differ diff --git a/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/assets/images/login_left.svg b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/assets/images/login_left.svg new file mode 100644 index 0000000..397f17f --- /dev/null +++ b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/assets/images/login_left.svg @@ -0,0 +1,101 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/assets/images/new-pdc-logo.svg b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/assets/images/new-pdc-logo.svg new file mode 100644 index 0000000..c267930 --- /dev/null +++ b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/assets/images/new-pdc-logo.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/assets/images/noPic.svg b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/assets/images/noPic.svg new file mode 100644 index 0000000..0f9c09d --- /dev/null +++ b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/assets/images/noPic.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/assets/images/pdf.png b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/assets/images/pdf.png new file mode 100644 index 0000000..a8c471f Binary files /dev/null and b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/assets/images/pdf.png differ diff --git a/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/assets/images/phoneIcon.png b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/assets/images/phoneIcon.png new file mode 100644 index 0000000..a0af511 Binary files /dev/null and b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/assets/images/phoneIcon.png differ diff --git a/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/assets/images/placeholder_image.png b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/assets/images/placeholder_image.png new file mode 100644 index 0000000..f2ead09 Binary files /dev/null and b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/assets/images/placeholder_image.png differ diff --git a/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/assets/images/right_loader_images/Right/1.png b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/assets/images/right_loader_images/Right/1.png new file mode 100644 index 0000000..c571db1 Binary files /dev/null and b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/assets/images/right_loader_images/Right/1.png differ diff --git a/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/assets/images/right_loader_images/Right/2.png b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/assets/images/right_loader_images/Right/2.png new file mode 100644 index 0000000..d6a1da0 Binary files /dev/null and b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/assets/images/right_loader_images/Right/2.png differ diff --git a/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/assets/images/right_loader_images/Right/3.png b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/assets/images/right_loader_images/Right/3.png new file mode 100644 index 0000000..042ba23 Binary files /dev/null and b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/assets/images/right_loader_images/Right/3.png differ diff --git a/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/assets/images/right_loader_images/Right/4.png b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/assets/images/right_loader_images/Right/4.png new file mode 100644 index 0000000..96c067a Binary files /dev/null and b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/assets/images/right_loader_images/Right/4.png differ diff --git a/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/assets/images/right_loader_images/Right/5.png b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/assets/images/right_loader_images/Right/5.png new file mode 100644 index 0000000..3a15579 Binary files /dev/null and b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/assets/images/right_loader_images/Right/5.png differ diff --git a/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/assets/images/right_loader_images/Right/6.png b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/assets/images/right_loader_images/Right/6.png new file mode 100644 index 0000000..7de6698 Binary files /dev/null and b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/assets/images/right_loader_images/Right/6.png differ diff --git a/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/assets/images/searchIcon.png b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/assets/images/searchIcon.png new file mode 100644 index 0000000..7b95f3c Binary files /dev/null and b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/assets/images/searchIcon.png differ diff --git a/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/assets/images/searchIcon.svg b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/assets/images/searchIcon.svg new file mode 100644 index 0000000..b82fe2f --- /dev/null +++ b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/assets/images/searchIcon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/assets/images/support-page-banner.svg b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/assets/images/support-page-banner.svg new file mode 100644 index 0000000..0fa6347 --- /dev/null +++ b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/assets/images/support-page-banner.svg @@ -0,0 +1,134 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/assets/images/tick-with-circle.svg b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/assets/images/tick-with-circle.svg new file mode 100644 index 0000000..8335e7c --- /dev/null +++ b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/assets/images/tick-with-circle.svg @@ -0,0 +1,4 @@ + + + + diff --git a/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/assets/images/unnamed.jpeg b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/assets/images/unnamed.jpeg new file mode 100644 index 0000000..b33f75d Binary files /dev/null and b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/assets/images/unnamed.jpeg differ diff --git a/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/assets/images/user-logo.svg b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/assets/images/user-logo.svg new file mode 100644 index 0000000..8135e28 --- /dev/null +++ b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/assets/images/user-logo.svg @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/assets/images/voiceIcon.svg b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/assets/images/voiceIcon.svg new file mode 100644 index 0000000..9c3e1d3 --- /dev/null +++ b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/assets/images/voiceIcon.svg @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/assets/images/warning-yellow.png b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/assets/images/warning-yellow.png new file mode 100644 index 0000000..e94f265 Binary files /dev/null and b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/assets/images/warning-yellow.png differ diff --git a/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/assets/videos/Imagen3_Header.mp4 b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/assets/videos/Imagen3_Header.mp4 new file mode 100644 index 0000000..f919cc8 Binary files /dev/null and b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/assets/videos/Imagen3_Header.mp4 differ diff --git a/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/environments/constant.ts b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/environments/constant.ts new file mode 100644 index 0000000..8edf1a8 --- /dev/null +++ b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/environments/constant.ts @@ -0,0 +1,25 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export const search_document_type = ['pdf', 'docx', 'doc']; + +export const search_image_type = ['jpg', 'jpeg', 'png']; + +export const image_name = new Map() + .set('doc', 'assets/images/doc.png') + .set('pdf', 'assets/images/pdf.png'); + +export const PDF = 'pdf'; diff --git a/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/environments/environment.ts b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/environments/environment.ts new file mode 100644 index 0000000..3c4b189 --- /dev/null +++ b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/environments/environment.ts @@ -0,0 +1,31 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export const environment = { + firebase: { + apiKey: '', + authDomain: '', + projectId: '', + storageBucket: '', + messagingSenderId: '', + appId: '', + measurementId: '', + }, + requiredLogin: 'False', + backendURL: 'http://localhost:8080/api', + chatbotName: 'Test 1', + environment: 'development', +}; diff --git a/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/favicon.ico b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/favicon.ico new file mode 100644 index 0000000..997406a Binary files /dev/null and b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/favicon.ico differ diff --git a/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/index.html b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/index.html new file mode 100644 index 0000000..799de59 --- /dev/null +++ b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/index.html @@ -0,0 +1,48 @@ + + + + + + + Quick Bot + + + + + + + + + + + + + + + diff --git a/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/main.ts b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/main.ts new file mode 100644 index 0000000..0da5f95 --- /dev/null +++ b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/main.ts @@ -0,0 +1,23 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {platformBrowserDynamic} from '@angular/platform-browser-dynamic'; + +import {AppModule} from './app/app.module'; + +platformBrowserDynamic() + .bootstrapModule(AppModule) + .catch(err => console.error(err)); diff --git a/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/styles.scss b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/styles.scss new file mode 100644 index 0000000..b4b1118 --- /dev/null +++ b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/src/styles.scss @@ -0,0 +1,86 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* You can add global styles to this file, and also import other style files */ + +html, +body { + height: 100%; + margin: 0; + font-family: 'Google Sans', sans-serif !important; + overflow: hidden; +} + +.material-symbols-rounded { + font-variation-settings: + 'FILL', + 'wght' 600, + 'GRAD' 0, + 'opsz' 48; +} + +.tooltip-list .mdc-tooltip__surface { + white-space: pre; + max-width: unset !important; +} + +.mat-mdc-slide-toggle.mat-accent { + --mdc-switch-selected-focus-state-layer-color: #4285f4; + --mdc-switch-selected-handle-color: #4285f4; + --mdc-switch-selected-hover-state-layer-color: #4285f4; + --mdc-switch-selected-pressed-state-layer-color: #4285f4; + --mdc-switch-selected-focus-handle-color: #3f74e0; + --mdc-switch-selected-hover-handle-color: #3f74e0; + --mdc-switch-selected-pressed-handle-color: #3f74e0; + --mdc-switch-selected-focus-track-color: rgba(66, 133, 244, 0.3); + --mdc-switch-selected-hover-track-color: rgba(66, 133, 244, 0.3); + --mdc-switch-selected-pressed-track-color: rgba(66, 133, 244, 0.3); + --mdc-switch-selected-track-color: rgba(66, 133, 244, 0.3); +} + +.arrow-top { + box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.1) !important; + border-radius: 20px !important; +} + +.mat-mdc-checkbox.mat-accent { + --mdc-checkbox-selected-checkmark-color: #fff; + --mdc-checkbox-selected-focus-icon-color: #4285f4; + --mdc-checkbox-selected-hover-icon-color: #4285f4; + --mdc-checkbox-selected-icon-color: #4285f4; + --mdc-checkbox-selected-pressed-icon-color: #4285f4; + --mdc-checkbox-unselected-focus-icon-color: #212121; + --mdc-checkbox-unselected-hover-icon-color: #212121; + --mdc-checkbox-disabled-selected-icon-color: rgba(0, 0, 0, 0.38); + --mdc-checkbox-disabled-unselected-icon-color: rgba(0, 0, 0, 0.38); + --mdc-checkbox-unselected-icon-color: rgba(0, 0, 0, 0.54); + --mdc-checkbox-unselected-pressed-icon-color: rgba(0, 0, 0, 0.54); +} + +$tour-skip-link-color: red; +$tour-text-color: purple; +$tour-next-text-color: yellow; +$tour-zindex: 100; +$tour-orb-color: cyan; +$tour-next-button-color: green; +$tour-next-button-hover: black; +$tour-back-button-color: white; +$tour-shadow-color: gray; +$tour-step-color: aliceblue; + +@tailwind base; +@tailwind components; +@tailwind utilities; diff --git a/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/tailwind.config.js b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/tailwind.config.js new file mode 100644 index 0000000..8f72439 --- /dev/null +++ b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/tailwind.config.js @@ -0,0 +1,30 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** @type {import('tailwindcss').Config} */ +module.exports = { + content: ['./src/**/*.{html,ts}'], + theme: { + extend: { + colors: { + 'custom-gray': '#DDE6F6', + 'light-gray': '#e0e0e0', + 'lighter-gray': '#f0f0f0', + }, + }, + }, + plugins: [], +}; diff --git a/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/tsconfig.app.json b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/tsconfig.app.json new file mode 100644 index 0000000..374cc9d --- /dev/null +++ b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/tsconfig.app.json @@ -0,0 +1,14 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "./out-tsc/app", + "types": [] + }, + "files": [ + "src/main.ts" + ], + "include": [ + "src/**/*.d.ts" + ] +} diff --git a/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/tsconfig.json b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/tsconfig.json new file mode 100644 index 0000000..fc49b39 --- /dev/null +++ b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/tsconfig.json @@ -0,0 +1,34 @@ +{ + "extends": "./node_modules/gts/tsconfig-google.json", + "compileOnSave": false, + "compilerOptions": { + "ignoreDeprecations": "5.0", + "baseUrl": "./", + "outDir": "./dist/out-tsc", + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "downlevelIteration": true, + "experimentalDecorators": true, + "moduleResolution": "node", + "importHelpers": true, + "target": "ES2022", + "module": "ES2022", + "useDefineForClassFields": false, + "lib": [ + "ES2022", + "dom" + ], + }, + "angularCompilerOptions": { + "enableI18nLegacyMessageIdFormat": false, + "strictInjectionParameters": true, + "strictInputAccessModifiers": true, + "strictTemplates": true, + "suppressImplicitAnyIndexErrors": true, + "strictPropertyInitialization": false, + }, + "include": [ + "src/**/*.ts", + "test/**/*.ts" + ] +} diff --git a/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/tsconfig.spec.json b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/tsconfig.spec.json new file mode 100644 index 0000000..be7e9da --- /dev/null +++ b/gemini/sample-apps/quickbot/text-to-image-using-imagen3/frontend/tsconfig.spec.json @@ -0,0 +1,14 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "./out-tsc/spec", + "types": [ + "jasmine" + ] + }, + "include": [ + "src/**/*.spec.ts", + "src/**/*.d.ts" + ] +} diff --git a/gemini/sample-apps/quickbot/website-search-using-agent-builder/.dockerignore b/gemini/sample-apps/quickbot/website-search-using-agent-builder/.dockerignore new file mode 100644 index 0000000..d472d1d --- /dev/null +++ b/gemini/sample-apps/quickbot/website-search-using-agent-builder/.dockerignore @@ -0,0 +1,12 @@ +node_modules +npm-debug.log +tests +coverage +Dockerfile +.dockerignore +.env +*.md +.git +.vscode +.gitignore +Makefile \ No newline at end of file diff --git a/gemini/sample-apps/quickbot/website-search-using-agent-builder/README.md b/gemini/sample-apps/quickbot/website-search-using-agent-builder/README.md new file mode 100644 index 0000000..a074aca --- /dev/null +++ b/gemini/sample-apps/quickbot/website-search-using-agent-builder/README.md @@ -0,0 +1,353 @@ +# QuickBot - Website Search using Custom RAG + +QuickBot Website Search Template is a powerful website search application. It features a user-friendly frontend interface and a robust backend API that leverages a custom Google Cloud Function for its Retrieval Augmented Generation (RAG) capabilities. + +## Overview + +This project allows users to perform intelligent searches over website content. The backend uses a custom Google Cloud Function (e.g., `create-intent-index`) for processing website data, generating embeddings, and managing the search index. This README guides you through setting up the necessary cloud infrastructure (primarily the Cloud Function) and then running both the frontend and backend components. + +## Demo + +See the Website Search Template in action, learning from a live website: + +![Website Search Demo](./assets/quickbot-website-search.png) + +## Prerequisites + +Before you begin, ensure you have the following installed: + +* **Google Cloud SDK (`gcloud` CLI):** Essential for authentication, deploying the Cloud Function, and interacting with Google Cloud services. +* **Docker and Docker Compose v2:** Recommended for running the frontend and backend services in a containerized environment. + * Verify your Docker Compose version with `docker compose version`. +* **Python 3.10+:** Required if you choose the manual setup path for the backend. +* **Node.js and npm (or yarn):** Required if you choose the manual setup path for the frontend. + +## Getting Started + +The setup process involves two main phases: +1. **Initial Cloud Setup:** Deploying the essential Google Cloud Function. +2. **Running the Application:** Choosing between Docker Compose or manual setup for the frontend and backend. + +### Phase 1: Initial Cloud Setup (Required for All Run Options) + +This phase ensures the core cloud infrastructure, specifically the RAG Cloud Function, is deployed. + +#### 1. Configure Google Cloud (`gcloud`) + +Authenticate with `gcloud` and set your default project. This account will be used to deploy the Cloud Function and for the application to interact with other GCP services. + +``` +# Log in with your Google account +gcloud auth login + +# Set your active Google Cloud project +gcloud config set project + +# Set up Application Default Credentials (ADC) for local development +# This allows your local application to authenticate as you. +gcloud auth application-default login +gcloud auth application-default set-quota-project + +# Verify your configuration +gcloud auth list +gcloud config list project +``` +Replace `` with your actual Google Cloud Project ID. + +#### 2. Deploy the RAG Cloud Function (e.g., `create-intent-index`) + +The backend relies on this Cloud Function for its RAG capabilities. + +**Function Details (Example):** +* **Name:** `create-intent-index` (you can choose another name, but be consistent) +* **Source Location (relative to project root):** `functions/create-intent` +* **Entry Point:** `create_intent_index` +* **Runtime:** `python311` (or as specified in the function's `requirements.txt`) +* **Region:** Choose a GCP region (e.g., `us-central1`) + +**Deployment Steps:** + +1. **Ensure you are in the project root directory.** +2. **Deploy the Cloud Function using `gcloud`:** + Replace ``, ``, and `` with your actual values. + ``` + gcloud functions deploy \ + --project= \ + --gen2 \ + --runtime=python311 \ + --region= \ + --source=./functions/create-intent \ + --entry-point=create_intent_index \ + --trigger-http \ + --allow-unauthenticated \ + --memory=1Gi \ + --timeout=3600s + ``` + * **Note:** `--allow-unauthenticated` is used here for development simplicity. For production, you should configure IAM to secure your Cloud Function and grant specific invoker permissions. + * The deployment process might take a few minutes. + +3. **Retrieve the Deployed Function's URL and Name:** + After successful deployment, you need the HTTP trigger URL and the exact name you used. + ``` + # Get the URL + gcloud run services describe \ + --project= \ + --region= \ + --format="value(status.url)" + ``` + **Important:** Carefully note down the `` you used and the `FUNCTION_URL` output by the command above. You will need these for the environment variable configuration in the next phase. + +#### 3. Prepare Environment Variable Values + +After deploying the Cloud Function, you will have the following critical pieces of information: +* Your `` +* The `` for the Cloud Function +* The `` +* The `FUNCTION_URL` (retrieved in the previous step) + +You will need to use these values to configure your application. + +* **For Docker Compose:** You will directly edit the `docker-compose.yml` file. +* **For Manual Setup:** You will create a `backend/.local.env` file. + +#### 4. Run Backend Setup Script (e.g., for BigQuery) + +The backend might have a setup script (e.g., `backend/setup.py`) to initialize resources like BigQuery tables. Run this after configuring your `gcloud` and ensuring the necessary environment variables (like `GCP_PROJECT_ID`, `BIG_QUERY_DATASET`) are available in your shell. + +* Temporarily set environment variables if not using a sourced `.env` file for this step: + ``` + export GCP_PROJECT_ID="" + export BIG_QUERY_DATASET="quickbot_website_search_bq" # Or your chosen name + # ... any other vars needed by setup.py + ``` +* Run the script: + ``` + python3 backend/setup.py + ``` + +### Phase 2: Running the Application + +After completing Phase 1, choose one of the following options: + +#### Option 1: Using Docker Compose (Recommended for Quick Start) + +This option runs the backend and frontend services within Docker containers. It assumes the Cloud Function is already deployed (Phase 1). + +1. **Ensure Docker and Docker Compose v2 are installed and running.** +2. **Navigate to the project root directory.** +3. **Configure `docker-compose.yml`:** + Open your `docker-compose.yml` file. Locate the `environment` section for your `backend` service (and `frontend` service, if applicable). You will need to set the environment variables directly here using the values obtained in "Phase 1". + + **Example `docker-compose.yml` backend service snippet:** + ```yaml + services: + backend: + # ... build context, Dockerfile ... + environment: + - GCP_PROJECT_ID= + - GCP_REGION= + - FUNCTION_URL= + - BIG_QUERY_DATASET=quickbot_website_search_bq # Or your chosen name + - ENVIRONMENT=development + - FRONTEND_URL=http://localhost:4200 # Or the frontend container's address + # - GOOGLE_APPLICATION_CREDENTIALS=/root/.config/gcloud/application_default_credentials.json # If mounting ADC + # volumes: + # - ~/.config/gcloud/application_default_credentials.json:/root/.config/gcloud/application_default_credentials.json:ro # For ADC + ports: + - "8080:8080" # Or your backend port + # frontend: + # ... + # environment: + # - REACT_APP_API_URL=http://localhost:8080/api # Example for frontend + ``` + Replace the placeholder values (e.g., ``) with your actual values. + +4. **Build Docker Images:** + ``` + docker compose build + ``` +5. **Run the application:** + ``` + docker compose up + ``` + The frontend should typically be available at `http://localhost:4200` (or as configured) and the backend API at `http://localhost:8080`. + +#### Option 2: Manual Setup (for Development and Customization) + +Follow these steps if you prefer to run the frontend and backend services manually on your local machine. This requires the Cloud Function to be deployed (Phase 1). + +**A. Backend Setup** + +1. **Navigate to the `backend/` directory.** + ``` + cd backend + ``` +2. **Create a virtual environment and install dependencies (if not already done):** + ``` + python3 -m venv .venv + source .venv/bin/activate + pip3 install -r requirements.txt + ``` +3. **Configure Environment Variables for Manual Run:** + Create a `.local.env` file in the `backend/` directory using the values from "Phase 1". + **Example `backend/.local.env`:** + ```env + GCP_PROJECT_ID="" + GCP_REGION="" + FUNCTION_URL="" + ENVIRONMENT="development" + FRONTEND_URL="http://localhost:4200" + BIG_QUERY_DATASET="quickbot_website_search_bq" + ``` +4. **Source the local environment variables:** + * **For Mac/Windows (or zsh console on Linux):** + ``` + . ./.local.env + ``` + * **For Linux (bash):** + Modify `backend/.venv/bin/activate` to include exports from `.local.env` (see previous README examples for how to do this). Then re-activate. + Verify with `env`. + +5. **Run the backend application:** + ``` + # from the backend/ directory, with .venv active and .local.env sourced + uvicorn main:app --reload --port 8080 + ``` + +**B. Frontend Setup** + +(These instructions assume a typical TypeScript/Angular frontend. Adjust as necessary based on your `frontend/README.md`.) + +1. **Navigate to the `frontend/` directory.** + ``` + cd frontend + ``` +2. **Install dependencies:** + ``` + npm install + ``` +3. **Environment Variables (if applicable):** + The frontend might require its own environment configuration (e.g., via a `.env` file or Angular's `environment.ts` files to point to the backend API at `http://localhost:8080`). Check the `frontend/` directory or its `README.md` for specific instructions. +4. **Run the frontend application:** + ``` + npm start + # Or, for many Angular projects: + # ng serve + ``` + The application will typically be available at `http://localhost:4200`. + +## Project Structure (highlighting important parts) +```text +website-search-using-custom-rag/ +├── backend/ # Python backend (FastAPI/Uvicorn) +│ ├── .venv/ # Python virtual environment (gitignored) +│ ├── .local.env # Local environment variables for manual run (gitignored) +│ ├── main.py # Main application file +│ ├── requirements.txt # Backend dependencies +│ ├── setup.py # Backend setup script (e.g., for BigQuery) +│ └── README.md # Backend-specific instructions +├── frontend/ # TypeScript frontend (e.g., Angular) +│ ├── node_modules/ # Node.js dependencies (gitignored) +│ ├── src/ # Frontend source code +│ ├── package.json # Frontend dependencies and scripts +│ └── README.md # Frontend-specific instructions +├── functions/ # Source code for Google Cloud Functions +│ └── create-intent/ # Example: Source for the RAG Cloud Function +│ └── ... +├── docker-compose.yml # Docker Compose configuration for all services +└── README.md # This file: Root project README +``` + +## Environment Variables + +Configuration for both frontend and backend is primarily managed through environment variables. + +* **Backend:** + * When running manually, backend environment variables are defined in `backend/.local.env`. + * When running with Docker, these variables are set directly in the `environment` section of the `backend` service in `docker-compose.yml`. + * Key variables include: + * `GCP_PROJECT_ID`: Your Google Cloud Project ID. + * `GCP_REGION`: The GCP region where your Cloud Function is deployed. + * `FUNCTION_URL`: The HTTP trigger URL of your deployed RAG Cloud Function. + * `BIG_QUERY_DATASET`: (If used) Name of the BigQuery dataset. + * `ENVIRONMENT`: Application environment (e.g., `development`, `production`). + * `FRONTEND_URL`: URL of the frontend application (e.g., `http://localhost:4200`) for CORS. + * Consult `backend/.local.env.example` (if one exists) or the backend's configuration logic for a complete list. + +* **Frontend:** + * Frontend environment variables (e.g., API endpoint URLs) are usually managed within the frontend's build system (e.g., Angular's `environment.ts` files or a `.env` file in the `frontend/` directory). + * Consult `frontend/README.md` for specific details. + +## Code Styling & Commit Guidelines + +To maintain code quality and consistency across the project: + +* **TypeScript (Frontend):** We follow the Angular Coding Style Guide by leveraging Google's TypeScript Style Guide using `gts`. This includes a formatter, linter, and automatic code fixer. +* **Python (Backend):** We adhere to the Google Python Style Guide, using tools like `pylint` and `black` for linting and formatting. +* **Commit Messages:** We suggest following Angular's Commit Message Guidelines to create clear and descriptive commit messages. + +### Frontend (TypeScript with `gts`) + +(Assumes setup within the `frontend/` directory) + +1. **Initialize `gts` (if not already done in the project):** + Navigate to `frontend/` and run: + ```bash + npx gts init + ``` + This will set up `gts` and create necessary configuration files (like `tsconfig.json`). Ensure your `tsconfig.json` (or a related `gts` config file like `.gtsrc`) includes an extension for `gts` defaults, typically: + ```json + { + "extends": "./node_modules/gts/tsconfig-google.json" + // ... other configurations + } + ``` +2. **Check for linting issues:** + (This assumes a `lint` script is defined in `frontend/package.json`, e.g., `"lint": "gts lint"`) + ```bash + # from frontend/ directory + npm run lint + ``` +3. **Fix linting issues automatically (where possible):** + (This assumes a `fix` script is defined in `frontend/package.json`, e.g., `"fix": "gts fix"`) + ```bash + # from frontend/ directory + npm run fix + ``` + +### Backend (Python with `pylint` and `black`) + +(Assumes setup within the `backend/` directory and its virtual environment activated) + +1. **Ensure Dependencies are Installed:** + Add `pylint` and `black` to your `backend/requirements.txt` file if not already present: + ``` + pylint + black + ``` + Then install them within your virtual environment: + ```bash + # from backend/ directory, with .venv activated + pip3 install pylint black + # or pip3 install -r requirements.txt + ``` +2. **Configure `pylint`:** + It's recommended to have a `.pylintrc` file in your `backend/` directory to configure `pylint` rules. You can generate one if it doesn't exist: + ```bash + # from backend/ directory + pylint --generate-rcfile > .pylintrc + ``` + Customize this file according to your project's needs and the Google Python Style Guide. +3. **Check for linting issues with `pylint`:** + Navigate to the `backend/` directory and run: + ```bash + # from backend/ directory + pylint . + # Or specify modules/packages: pylint src/ your_module_name + ``` +4. **Format code with `black`:** + To automatically format all Python files in the `backend/` directory and its subdirectories: + ```bash + # from backend/ directory + python3 -m black . --line-length=80 + ``` diff --git a/gemini/sample-apps/quickbot/website-search-using-agent-builder/assets/quickbot-website-search.png b/gemini/sample-apps/quickbot/website-search-using-agent-builder/assets/quickbot-website-search.png new file mode 100644 index 0000000..f039701 Binary files /dev/null and b/gemini/sample-apps/quickbot/website-search-using-agent-builder/assets/quickbot-website-search.png differ diff --git a/gemini/sample-apps/quickbot/website-search-using-agent-builder/backend/.gcloudignore b/gemini/sample-apps/quickbot/website-search-using-agent-builder/backend/.gcloudignore new file mode 100644 index 0000000..606a07e --- /dev/null +++ b/gemini/sample-apps/quickbot/website-search-using-agent-builder/backend/.gcloudignore @@ -0,0 +1,20 @@ +# This file specifies files that are *not* uploaded to Google Cloud +# using gcloud. It follows the same syntax as .gitignore, with the addition of +# "#!include" directives (which insert the entries of the given .gitignore-style +# file at that point). +# +# For more information, run: +# $ gcloud topic gcloudignore +# +.gcloudignore +# If you would like to upload your .git directory, .gitignore file or files +# from your .gitignore file, remove the corresponding line +# below: +.git +.gitignore + +# Python pycache: +__pycache__/ +# Ignored by the build system +/setup.cfg +my_env \ No newline at end of file diff --git a/gemini/sample-apps/quickbot/website-search-using-agent-builder/backend/.gitignore b/gemini/sample-apps/quickbot/website-search-using-agent-builder/backend/.gitignore new file mode 100644 index 0000000..a9ce35f --- /dev/null +++ b/gemini/sample-apps/quickbot/website-search-using-agent-builder/backend/.gitignore @@ -0,0 +1,7 @@ +__pycache__/ +.idea/ +.vscode/ +.venv/ +env +bin +lib \ No newline at end of file diff --git a/gemini/sample-apps/quickbot/website-search-using-agent-builder/backend/Dockerfile b/gemini/sample-apps/quickbot/website-search-using-agent-builder/backend/Dockerfile new file mode 100644 index 0000000..7e32e09 --- /dev/null +++ b/gemini/sample-apps/quickbot/website-search-using-agent-builder/backend/Dockerfile @@ -0,0 +1,27 @@ +# Copyright 2025 Google LLC +# +# 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 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +FROM python:3.11 +COPY requirements.txt /tmp +RUN pip install -r /tmp/requirements.txt +RUN mkdir -p /code +WORKDIR /code +COPY . /code/ + +ENV ENVIRONMENT="development" +ENV BIG_QUERY_DATASET="" +ENV FRONTEND_URL="" + +EXPOSE 8080 +ENTRYPOINT ["gunicorn", "main:app", "--workers=4", "--worker-class=uvicorn.workers.UvicornWorker", "--timeout=36000", "--bind=0.0.0.0:8080"] \ No newline at end of file diff --git a/gemini/sample-apps/quickbot/website-search-using-agent-builder/backend/Dockerfile.local b/gemini/sample-apps/quickbot/website-search-using-agent-builder/backend/Dockerfile.local new file mode 100644 index 0000000..a633be7 --- /dev/null +++ b/gemini/sample-apps/quickbot/website-search-using-agent-builder/backend/Dockerfile.local @@ -0,0 +1,90 @@ +# Copyright 2025 Google LLC +# +# 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 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# syntax=docker/dockerfile:1.4 +FROM python:3.11 AS builder + +# Build arguments +ARG IS_FIRST_DEPLOYMENT="False" +ARG GCLOUD_PROJECT="" +ARG ENVIRONMENT="development" +ARG FRONTEND_URL="" +ARG FUNCTION_URL="" +ARG BIG_QUERY_DATASET="" + +# Environment variables for the build process +ENV IS_FIRST_DEPLOYMENT=$IS_FIRST_DEPLOYMENT +ENV GCLOUD_PROJECT=$GCLOUD_PROJECT +ENV ENVIRONMENT=$ENVIRONMENT +ENV FRONTEND_URL=$FRONTEND_URL +ENV FUNCTION_URL=$FUNCTION_URL +ENV BIG_QUERY_DATASET=$BIG_QUERY_DATASET + +WORKDIR /code + +COPY . . +COPY setup.py ./setup.py +RUN pip install --no-cache-dir -r /code/requirements.txt + +RUN --mount=type=secret,id=gcp_credentials,target=/tmp/gcp_adc.json,required=true \ + if [ "$IS_FIRST_DEPLOYMENT" = "True" ]; then \ + echo "--- Running first deployment setup (IS_FIRST_DEPLOYMENT=True) ---"; \ + echo "Using GCLOUD_PROJECT=${GCLOUD_PROJECT} for setup"; \ + \ + if [ ! -f /tmp/gcp_adc.json ]; then \ + echo "CRITICAL ERROR: GCP credentials secret was expected but not mounted to /tmp/gcp_adc.json" >&2; \ + exit 1; \ + fi; \ + echo "DEBUG: GCP credentials secret successfully mounted to /tmp/gcp_adc.json"; \ + \ + # Set GOOGLE_APPLICATION_CREDENTIALS specifically for this RUN command's execution context + export GOOGLE_APPLICATION_CREDENTIALS=/tmp/gcp_adc.json; \ + echo "DEBUG: GOOGLE_APPLICATION_CREDENTIALS for setup is set to: $GOOGLE_APPLICATION_CREDENTIALS"; \ + \ + echo "Executing setup.py ..."; \ + # Now setup.py can make requests to GCP with provided credentials + python /code/setup.py || { echo "setup.py with failed!"; exit 1; }; \ + echo "--- First deployment setup finished ---"; \ + else \ + echo "--- IS_FIRST_DEPLOYMENT is False, skipping first deployment setup. ---"; \ + fi + + +FROM python:3.11-slim AS final + +ARG GCLOUD_PROJECT="" +ARG GOOGLE_APPLICATION_CREDENTIALS="" +ARG ENVIRONMENT="development" +ARG FRONTEND_URL="" +ARG FUNCTION_URL="" +ARG BIG_QUERY_DATASET="" + +# Environment variables for the build process +ENV GCLOUD_PROJECT=$GCLOUD_PROJECT +ENV GOOGLE_APPLICATION_CREDENTIALS="/root/.config/gcloud/application_default_credentials.json" +ENV ENVIRONMENT=$ENVIRONMENT +ENV FRONTEND_URL=$FRONTEND_URL +ENV FUNCTION_URL=$FUNCTION_URL +ENV BIG_QUERY_DATASET=$BIG_QUERY_DATASET + +WORKDIR /app + +COPY --from=builder /code/requirements.txt /app/requirements.txt +RUN pip install --no-cache-dir -r /app/requirements.txt + +COPY --from=builder /code/main.py /app/main.py +COPY --from=builder /code/src /app/src + +EXPOSE 8080 +ENTRYPOINT ["gunicorn", "main:app", "--workers=4", "--worker-class=uvicorn.workers.UvicornWorker", "--timeout=36000", "--bind=0.0.0.0:8080"] \ No newline at end of file diff --git a/gemini/sample-apps/quickbot/website-search-using-agent-builder/backend/README.md b/gemini/sample-apps/quickbot/website-search-using-agent-builder/backend/README.md new file mode 100644 index 0000000..0207c87 --- /dev/null +++ b/gemini/sample-apps/quickbot/website-search-using-agent-builder/backend/README.md @@ -0,0 +1,132 @@ +# QuickBotApp website-search-using-agent-builder + +## Setting up +### 1. Create virtualenv and install dependencies +Create a virtual environment on the root of the application, activate it and install the requirements +``` +python3 -m venv .venv +source .venv/bin/activate +pip3 install -r requirements.txt +``` + +### 2. Setup gcloud credentials +``` +gcloud auth list +gcloud config list + +gcloud auth login +gcloud config set project +gcloud auth application-default set-quota-project + +gcloud auth list +gcloud config list +``` + +### 3. Running the set up script + +Ensure the necessary APIs are enabled for your project. +At a minimum, you will need: +**BigQuery API** +**Discovery Engine API** (IAM roles to interact with Discovery Engine e.g. +"Discovery Engine Viewer" or "Discovery Engine Admin") +Then run + +``` +python3 setup.py +``` + +### 4. Add environment variables + +#### If you have Mac or Windows +``` +. ./local.env +``` + +#### If you have Linux +Open the file .venv/bin/activate and paste the env variables from `.local.env` after the PATH export, like this: +``` +... + +_OLD_VIRTUAL_PATH="$PATH" +PATH="$VIRTUAL_ENV/bin:$PATH" +export PATH + +# QUICKBOT ENV VARIABLES +export ENVIRONMENT=development +export FRONTEND_URL=http://localhost:4200 +export BIG_QUERY_DATASET=bigquery_quickbot_website_search +... +``` + +Check that the env variables has been taken into account, running: +``` +env +``` +You should see the new env variables set there + + +### 5. Run the application +Finally run using uvicorn +``` +uvicorn main:app --reload --port 8080 +``` + +## Code Styling & Commit Guidelines + +To maintain code quality and consistency: + +* **TypeScript (Frontend):** We follow [Angular Coding Style Guide](https://angular.dev/style-guide) by leveraging the use of [Google's TypeScript Style Guide](https://github.com/google/gts) using `gts`. This includes a formatter, linter, and automatic code fixer. +* **Python (Backend):** We adhere to the [Google Python Style Guide](https://google.github.io/styleguide/pyguide.html), using tools like `pylint` and `black` for linting and formatting. +* **Commit Messages:** We suggest following [Angular's Commit Message Guidelines](https://github.com/angular/angular/blob/main/contributing-docs/commit-message-guidelines.md) to create clear and descriptive commit messages. + +#### Frontend (TypeScript with `gts`) + +1. **Initialize `gts` (if not already done in the project):** + Navigate to the `frontend/` directory and run: + ```bash + npx gts init + ``` + This will set up `gts` and create necessary configuration files (like `tsconfig.json`). Ensure your `tsconfig.json` (or a related gts config file like `.gtsrc`) includes an extension for `gts` defaults, typically: + ```json + { + "extends": "./node_modules/gts/tsconfig-google.json", + // ... other configurations + } + ``` +2. **Check for linting issues:** + ```bash + npm run lint + ``` + (This assumes a `lint` script is defined in `package.json`, e.g., `"lint": "gts lint"`) +3. **Fix linting issues automatically (where possible):** + ```bash + npm run fix + ``` + (This assumes a `fix` script is defined in `package.json`, e.g., `"fix": "gts fix"`) + +#### Backend (Python with `pylint` and `black`) + +1. **Ensure Dependencies are Installed:** + Add `pylint` and `black` to your `backend/requirements.txt` file: + ``` + pylint + black + ``` + Then install them within your virtual environment: + ```bash + pip install pylint black + # or pip install -r requirements.txt + ``` +2. **Configure `pylint`:** + It's recommended to have a `.pylintrc` file in your `backend/` directory to configure `pylint` rules. You might need to copy a standard one or generate one (`pylint --generate-rcfile > .pylintrc`). +3. **Check for linting issues with `pylint`:** + Navigate to the `backend/` directory and run: + ```bash + pylint . + ``` + (Or specify modules/packages: `pylint your_module_name`) +4. **Format code with `black`:** + To automatically format all Python files in the current directory and subdirectories: + ```bash + python -m black . --line-length=80 + ``` diff --git a/gemini/sample-apps/quickbot/website-search-using-agent-builder/backend/__init__.py b/gemini/sample-apps/quickbot/website-search-using-agent-builder/backend/__init__.py new file mode 100644 index 0000000..36a1e8d --- /dev/null +++ b/gemini/sample-apps/quickbot/website-search-using-agent-builder/backend/__init__.py @@ -0,0 +1,14 @@ +# Copyright 2025 Google LLC +# +# 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 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + diff --git a/gemini/sample-apps/quickbot/website-search-using-agent-builder/backend/local.env b/gemini/sample-apps/quickbot/website-search-using-agent-builder/backend/local.env new file mode 100644 index 0000000..87f3818 --- /dev/null +++ b/gemini/sample-apps/quickbot/website-search-using-agent-builder/backend/local.env @@ -0,0 +1,3 @@ +export ENVIRONMENT=development +export FRONTEND_URL=http://localhost:4200 +export BIG_QUERY_DATASET=bigquery_quickbot_website_search \ No newline at end of file diff --git a/gemini/sample-apps/quickbot/website-search-using-agent-builder/backend/main.py b/gemini/sample-apps/quickbot/website-search-using-agent-builder/backend/main.py new file mode 100644 index 0000000..9f51427 --- /dev/null +++ b/gemini/sample-apps/quickbot/website-search-using-agent-builder/backend/main.py @@ -0,0 +1,104 @@ +# Copyright 2025 Google LLC +# +# 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 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Main FastAPI application entry point for the Quick Bot Backend. + +This module initializes the FastAPI application, configures CORS based on +the environment, defines root and version endpoints, includes API routers +(e.g., for search functionality), and provides an endpoint for audio +transcription using Google Cloud Speech-to-Text. +""" + +from fastapi import FastAPI, File, UploadFile +from fastapi.middleware.cors import CORSMiddleware +from src.controller.search import router as search_router +from google.cloud import speech +from os import getenv + +app = FastAPI() + + +def configure_cors(app): + """Configures CORS middleware based on the environment.""" + environment = getenv("ENVIRONMENT") + allowed_origins = [] + + if environment == "production": + frontend_url = getenv("FRONTEND_URL") + if not frontend_url: + raise ValueError( + "FRONTEND_URL environment variable not set in production" + ) + allowed_origins.append(frontend_url) + elif environment == "development": + allowed_origins.append("*") # Allow all origins in development + else: + raise ValueError( + f"""Invalid ENVIRONMENT: {environment}. + Must be 'production' or 'development'""" + ) + + app.add_middleware( + CORSMiddleware, + allow_origins=allowed_origins, + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], + ) + + +# Create a route to handle GET requests on root +@app.get("/") +async def root(): + return "You are calling Quick Bot Backend" + + +# Create a route to handle GET requests on /version +@app.get("/api/version") +def version(): + return "v0.0.1" + + +@app.post("/api/audio_chat") +async def audio_chat(audio_file: UploadFile = File(...)): + client = speech.SpeechClient() + audio_content = await audio_file.read() + audio = speech.RecognitionAudio(content=audio_content) + config = speech.RecognitionConfig( + language_code="en-US", + sample_rate_hertz=48000, + model="default", + audio_channel_count=1, + enable_word_confidence=True, + enable_word_time_offsets=True, + ) + + operation = client.long_running_recognize(config=config, audio=audio) + + print("Waiting for operation to complete...") + response = operation.result(timeout=90) + print(response) + + text = "" + for result in response.results: + print(f"Transcript: {result.alternatives[0].transcript}") + text = result.alternatives[0].transcript + + return text, 200 + + +configure_cors(app) + +app.include_router(search_router) diff --git a/gemini/sample-apps/quickbot/website-search-using-agent-builder/backend/requirements.txt b/gemini/sample-apps/quickbot/website-search-using-agent-builder/backend/requirements.txt new file mode 100644 index 0000000..435f600 --- /dev/null +++ b/gemini/sample-apps/quickbot/website-search-using-agent-builder/backend/requirements.txt @@ -0,0 +1,30 @@ +gunicorn==20.1.0; python_version > '3.0' +gunicorn==19.10.0; python_version < '3.0' +uvicorn~=0.17.0 +fastapi~=0.111.1 +fastapi-utilities==0.2.0 +cloudpickle==2.2.1 +grpcio==1.66.2 +urllib3==1.26.16 +typing-inspect==0.9.0 +typing_extensions==4.12.2 +pydantic==2.9.2 +requests==2.31.0 + +langchain==0.3.2 +langchain-community==0.3.1 +langchain-core==0.3.9 +langchain-google-vertexai==2.0.4 + +google-cloud-aiplatform==1.69.0 +google-cloud-bigquery==3.26.0 +google-cloud-tasks==2.16.5 +google-cloud-logging==3.11.2 +google-cloud-speech==2.27.0 +google-cloud-discoveryengine==0.13.4 + +pylint +black +pytest +pytest-cov +pytest-watch \ No newline at end of file diff --git a/gemini/sample-apps/quickbot/website-search-using-agent-builder/backend/scripts/__init__.py b/gemini/sample-apps/quickbot/website-search-using-agent-builder/backend/scripts/__init__.py new file mode 100644 index 0000000..36a1e8d --- /dev/null +++ b/gemini/sample-apps/quickbot/website-search-using-agent-builder/backend/scripts/__init__.py @@ -0,0 +1,14 @@ +# Copyright 2025 Google LLC +# +# 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 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + diff --git a/gemini/sample-apps/quickbot/website-search-using-agent-builder/backend/scripts/big_query_setup.py b/gemini/sample-apps/quickbot/website-search-using-agent-builder/backend/scripts/big_query_setup.py new file mode 100644 index 0000000..6d51770 --- /dev/null +++ b/gemini/sample-apps/quickbot/website-search-using-agent-builder/backend/scripts/big_query_setup.py @@ -0,0 +1,69 @@ +# Copyright 2025 Google LLC +# +# 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 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Utility functions for setting up Google BigQuery datasets and tables.""" + +from typing import List +from google.cloud.bigquery import Client as BigQueryClient, Table, SchemaField, TableReference + +bigquery_client = BigQueryClient() +PROJECT_ID = bigquery_client.project + + +def create_dataset(dataset_name: str): + """Deletes an existing BigQuery dataset and recreates it. + + If the dataset already exists, it and all its contents + will be deleted first. + Then, a new empty dataset with the specified name is created. + + Args: + dataset_name: The name for the BigQuery dataset. + """ + dataset_id = f"{PROJECT_ID}.{dataset_name}" + try: + bigquery_client.delete_dataset( + dataset_id, delete_contents=True, not_found_ok=True + ) + print(f"Dataset {dataset_id} deleted (if it existed).") + except Exception as e: + print(f"Error deleting dataset {dataset_id}: {e}") + + print(f"Creating dataset {dataset_id}...") + bigquery_client.create_dataset(dataset_name, exists_ok=True) # exists_ok=True in case delete failed but it exists + print(f"Dataset {dataset_id} ensured.") + + +def create_table(dataset: str, table_name: str, schema: List[SchemaField]): + """Creates a BigQuery table within a specified dataset. + If the table already exists, it will be deleted and recreated. + + Args: + dataset: The name of the dataset where the table will be created. + table_name: The name for the new BigQuery table. + schema: A list of SchemaField objects defining the table's structure. + """ + table_id_full = f"{PROJECT_ID}.{dataset}.{table_name}" + table_ref = TableReference.from_string(table_id_full) + + try: + bigquery_client.delete_table(table_ref, not_found_ok=True) + print(f"Table {table_id_full} deleted (if it existed).") + except Exception as e: + print(f"Notice: Could not delete table {table_id_full} (may not exist or other issue): {e}") + + print(f"Creating table {table_id_full}...") + bq_table = Table(table_ref, schema=schema) + bigquery_client.create_table(bq_table) + print(f"Table {table_id_full} created successfully.") diff --git a/gemini/sample-apps/quickbot/website-search-using-agent-builder/backend/setup.py b/gemini/sample-apps/quickbot/website-search-using-agent-builder/backend/setup.py new file mode 100644 index 0000000..264fef4 --- /dev/null +++ b/gemini/sample-apps/quickbot/website-search-using-agent-builder/backend/setup.py @@ -0,0 +1,46 @@ +# Copyright 2025 Google LLC +# +# 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 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Initializes the BigQuery environment for the application. + +This script performs the following actions: +1. Retrieves the target BigQuery dataset name from the 'BIG_QUERY_DATASET' + environment variable. +2. Creates the specified BigQuery dataset if it doesn't already exist. +3. Creates the 'search_applications' table within that dataset, using the + schema defined by the `SearchApplication` model, if the table doesn't + already exist. + +Usage: + Run this script directly (e.g., `python setup.py`) to set up the + necessary BigQuery resources. Ensure the 'BIG_QUERY_DATASET' environment + variable is set beforehand. +""" + +from scripts.big_query_setup import create_dataset, create_table +from src.service.search_application import SEARCH_APPLICATION_TABLE +from src.model.search import SearchApplication + +BIG_QUERY_DATASET = "" + +if not BIG_QUERY_DATASET: # Extra check in case the default was also empty or env var was set to empty + raise ValueError("BIGQUERY_DATASET_ID resolved to an empty string. Please ensure it's set correctly.") + +create_dataset(BIG_QUERY_DATASET) +create_table( + BIG_QUERY_DATASET, SEARCH_APPLICATION_TABLE, SearchApplication.__schema__() +) + +print("\nSuccess!\n") diff --git a/gemini/sample-apps/quickbot/website-search-using-agent-builder/backend/src/__init__.py b/gemini/sample-apps/quickbot/website-search-using-agent-builder/backend/src/__init__.py new file mode 100644 index 0000000..36a1e8d --- /dev/null +++ b/gemini/sample-apps/quickbot/website-search-using-agent-builder/backend/src/__init__.py @@ -0,0 +1,14 @@ +# Copyright 2025 Google LLC +# +# 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 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + diff --git a/gemini/sample-apps/quickbot/website-search-using-agent-builder/backend/src/controller/__init__.py b/gemini/sample-apps/quickbot/website-search-using-agent-builder/backend/src/controller/__init__.py new file mode 100644 index 0000000..36a1e8d --- /dev/null +++ b/gemini/sample-apps/quickbot/website-search-using-agent-builder/backend/src/controller/__init__.py @@ -0,0 +1,14 @@ +# Copyright 2025 Google LLC +# +# 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 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + diff --git a/gemini/sample-apps/quickbot/website-search-using-agent-builder/backend/src/controller/search.py b/gemini/sample-apps/quickbot/website-search-using-agent-builder/backend/src/controller/search.py new file mode 100644 index 0000000..b07ba9c --- /dev/null +++ b/gemini/sample-apps/quickbot/website-search-using-agent-builder/backend/src/controller/search.py @@ -0,0 +1,102 @@ +# Copyright 2025 Google LLC +# +# 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 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""API endpoints for managing and performing website searches.""" + +from fastapi import APIRouter +from src.model.http_status import BadRequest +from src.model.search import CreateSearchRequest, SearchApplication + +from src.service.engine import EngineService +from src.service.search import SearchService +from src.service.search_application import SearchApplicationService + +router = APIRouter( + prefix="/api/search", + tags=["searches"], + responses={404: {"description": "Not found"}}, +) + + +@router.post("") +async def search(item: CreateSearchRequest): + """ + Performs a search using the configured Search Application. + + Args: + item: The search request containing the search term. + + Raises: + BadRequest: If no Search Application is configured for the project. + + Returns: + The search results from the SearchService. + """ + service = SearchApplicationService() + search_application = service.get() + if not search_application: + raise BadRequest(detail=f"No Search Application found on project") + + service = SearchService( + search_application, + ) + return service.search(item.term) + + +@router.get("/engines") +async def get_all_engines(): + """Retrieves all available Search Engines.""" + service = EngineService() + return service.get_all() + + +@router.get("/application") +async def get_search_application(): + """Retrieves the currently configured Search Application.""" + service = SearchApplicationService() + return service.get() + + +@router.post("/application") +async def create_search_application(search_application: SearchApplication): + """ + Creates a new Search Application configuration. + + Args: + search_application: The details of the Search Application to create. + + Returns: + The created Search Application configuration. + """ + service = SearchApplicationService() + return service.create(search_application) + + +@router.put("/application/{engine_id}") +async def update_search_application( + engine_id: str, search_application: SearchApplication +): + """ + Updates an existing Search Application configuration. + + Args: + engine_id: The ID of the engine associated with + the application to update. + search_application: The updated details for the Search Application. + + Returns: + The updated Search Application configuration. + """ + service = SearchApplicationService() + return service.update(engine_id, search_application) diff --git a/gemini/sample-apps/quickbot/website-search-using-agent-builder/backend/src/model/__init__.py b/gemini/sample-apps/quickbot/website-search-using-agent-builder/backend/src/model/__init__.py new file mode 100644 index 0000000..36a1e8d --- /dev/null +++ b/gemini/sample-apps/quickbot/website-search-using-agent-builder/backend/src/model/__init__.py @@ -0,0 +1,14 @@ +# Copyright 2025 Google LLC +# +# 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 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + diff --git a/gemini/sample-apps/quickbot/website-search-using-agent-builder/backend/src/model/http_status.py b/gemini/sample-apps/quickbot/website-search-using-agent-builder/backend/src/model/http_status.py new file mode 100644 index 0000000..1f085f4 --- /dev/null +++ b/gemini/sample-apps/quickbot/website-search-using-agent-builder/backend/src/model/http_status.py @@ -0,0 +1,53 @@ +# Copyright 2025 Google LLC +# +# 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 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Defines custom HTTP exception classes for the FastAPI application. + +This module provides reusable exception classes that inherit from FastAPI's +HTTPException, allowing for standardized error responses with specific +status codes and detail messages. +""" + +from fastapi import HTTPException + + +class ResourceAlreadyExists(HTTPException): + """ + Custom exception for HTTP 400 Bad Request errors. + + Used specifically when an attempt is made to create a resource that + already exists (e.g., creating a duplicate configuration). + Defaults to status code 400. + """ + + def __init__(self, detail="Resource already exists"): + """Initializes the exception with a default detail message.""" + super().__init__(status_code=400, detail=detail) + + +class BadRequest(HTTPException): + """ + Custom exception for general HTTP 400 Bad Request errors. + + Can be used for various client-side errors like invalid input, + missing required data, or violating business logic rules before + processing. Defaults to status code 400. + + Note: The default detail message "Resource already exists" seems + inconsistent with a general BadRequest. Consider changing it to + a more generic message like "Bad Request" or requiring a specific + detail message upon instantiation. + """ + def __init__(self, detail="Resource already exists"): + super().__init__(status_code=400, detail=detail) diff --git a/gemini/sample-apps/quickbot/website-search-using-agent-builder/backend/src/model/search.py b/gemini/sample-apps/quickbot/website-search-using-agent-builder/backend/src/model/search.py new file mode 100644 index 0000000..bca95ce --- /dev/null +++ b/gemini/sample-apps/quickbot/website-search-using-agent-builder/backend/src/model/search.py @@ -0,0 +1,119 @@ +# Copyright 2025 Google LLC +# +# 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 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Defines data models for search operations and configurations. + +This module includes Pydantic models for API requests/responses related to +search applications and engines, as well as dataclasses for representing +search results internally. It also handles fetching the default Google Cloud +Project ID. +""" + +from dataclasses import dataclass +from typing import List, Optional +from pydantic import BaseModel +from google.cloud.bigquery import SchemaField, Row +from google.api_core.client_options import ClientOptions +import google.auth + +_, PROJECT_ID = google.auth.default() + + +class CreateSearchRequest(BaseModel): + """Request model for initiating a search.""" + term: str + + +class SearchApplication(BaseModel): + """Represents the configuration for a Discovery Engine + Search Application.""" + engine_id: str + region: str + + @classmethod + def __schema__(cls) -> List[SchemaField]: + """Defines the BigQuery schema for storing SearchApplication data.""" + return [ + SchemaField("engine_id", "STRING", mode="REQUIRED"), + SchemaField("region", "STRING", mode="REQUIRED"), + ] + + @classmethod + def from_row(cls, row: Row) -> 'SearchApplication': + """Creates a SearchApplication instance from a BigQuery Row object. + + Args: + row: The BigQuery Row object. Assumes row contains fields matching the schema. + + Returns: + A SearchApplication instance. + """ + # Access by field name for robustness, assuming schema matches + return cls(engine_id=row["engine_id"], region=row["region"]) + + def to_dict(self): + """Converts the SearchApplication instance to a dictionary.""" + return { + "engine_id": self.engine_id, + "region": self.region, + } + + def get_client_options(self) -> Optional[ClientOptions]: + """Generates API client options based on the application's region. + + Returns: + ClientOptions configured with the regional endpoint, or None if + the region is 'global'. + """ + return ( + ClientOptions( + api_endpoint=f"{self.region}-discoveryengine.googleapis.com" + ) + if self.region != "global" + else None + ) + + def get_serving_config(self) -> str: + """Constructs the full serving config path for the Discovery Engine API. + + Returns: + The formatted serving config string. + """ + serving_config = f"projects/{PROJECT_ID}/locations/{self.region}" + serving_config += f"/collections/default_collection/engines/{self.engine_id}" + serving_config += "/servingConfigs/default_config" + return serving_config + +class Engine(BaseModel): + """Represents a discovered Discovery Engine instance.""" + name: str + engine_id: str + region: str + + +@dataclass +class SearchResult: + """Represents a single document result from a search query.""" + document_id: str + title: str + snippet: str + link: Optional[str] = None + content: Optional[str] = None + + +@dataclass +class SearchResultsWithSummary: + """Represents the complete results of a search, including a summary.""" + results: List[SearchResult] + summary: Optional[str] = None diff --git a/gemini/sample-apps/quickbot/website-search-using-agent-builder/backend/src/repository/__init__.py b/gemini/sample-apps/quickbot/website-search-using-agent-builder/backend/src/repository/__init__.py new file mode 100644 index 0000000..36a1e8d --- /dev/null +++ b/gemini/sample-apps/quickbot/website-search-using-agent-builder/backend/src/repository/__init__.py @@ -0,0 +1,14 @@ +# Copyright 2025 Google LLC +# +# 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 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + diff --git a/gemini/sample-apps/quickbot/website-search-using-agent-builder/backend/src/repository/big_query.py b/gemini/sample-apps/quickbot/website-search-using-agent-builder/backend/src/repository/big_query.py new file mode 100644 index 0000000..a8086ea --- /dev/null +++ b/gemini/sample-apps/quickbot/website-search-using-agent-builder/backend/src/repository/big_query.py @@ -0,0 +1,325 @@ +# Copyright 2025 Google LLC +# +# 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 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Provides a repository class for interacting with Google BigQuery. + +This module defines the `BigQueryRepository` class, which encapsulates +common BigQuery operations like running queries, fetching rows by ID, +inserting, updating, and deleting rows. It relies on the 'BIG_QUERY_DATASET' +environment variable to target the correct dataset. +""" + +import datetime +from decimal import Decimal +from os import getenv +from typing import Dict, List, Any, Optional, Tuple +from google.cloud.bigquery import ( + Client, + QueryJobConfig, + ScalarQueryParameter, + ArrayQueryParameter, + TableReference, + DatasetReference +) +from google.cloud.bigquery.table import RowIterator +from google.cloud.exceptions import GoogleCloudError + +_BIG_QUERY_DATASET_ENV = getenv("BIG_QUERY_DATASET") +if not _BIG_QUERY_DATASET_ENV: + raise ValueError( + "The BIG_QUERY_DATASET environment variable is not set. " + "This is required for the BigQueryRepository to function." + ) +BIG_QUERY_DATASET: str = _BIG_QUERY_DATASET_ENV + +CHATS_TABLE = 'chats' +CHATS_ID_COLUMN = "id" + +EMBEDDINGS_TABLE = "embeddings" +EMBEDDINGS_ID_COLUMN = "id" +EMBEDDINGS_TEXT_COLUMN = "text" +EMBEDDINGS_INDEX_COLUMN = "index" + +class BigQueryRepository: + """ + A repository class for simplifying interactions with Google BigQuery, + using parameterized queries to prevent SQL injection. + """ + + def __init__(self): + """Initializes the BigQuery client and project ID.""" + self.client: Client = Client() + self.project_id: str = self.client.project + self.dataset_id: str = BIG_QUERY_DATASET + + def _get_table_ref(self, table_id: str) -> TableReference: + """Helper to get a TableReference object.""" + dataset_ref = DatasetReference(self.project_id, self.dataset_id) + return TableReference(dataset_ref, table_id) + + def _validate_column_name(self, column_name: str): + """Basic validation for column names.""" + if not column_name.replace("_", "").isalnum(): + raise ValueError(f"Invalid column name: {column_name}") + + def _create_scalar_query_parameter(self, name: str, value: Any) -> ScalarQueryParameter: + """ + Creates a ScalarQueryParameter by inferring the BigQuery type from the value. + """ + if isinstance(value, bool): + return ScalarQueryParameter(name, "BOOL", value) + if isinstance(value, int): + return ScalarQueryParameter(name, "INT64", value) + if isinstance(value, float): + return ScalarQueryParameter(name, "FLOAT64", value) + if isinstance(value, str): + return ScalarQueryParameter(name, "STRING", value) + if isinstance(value, datetime.datetime): + return ScalarQueryParameter(name, "TIMESTAMP", value) + if isinstance(value, datetime.date): + return ScalarQueryParameter(name, "DATE", value) + if isinstance(value, Decimal): + return ScalarQueryParameter(name, "NUMERIC", value) + if value is None: + # For None, the type of the column in BQ will ultimately determine how NULL is handled. + # Using STRING type for the parameter is a common safe default. + return ScalarQueryParameter(name, "STRING", None) # Default to STRING for None + + # Fallback: attempt to convert other types to string. + try: + str_value = str(value) + return ScalarQueryParameter(name, "STRING", str_value) + except Exception as e: + raise TypeError( + f"Value for param '{name}' of type {type(value)} " + f"could not be converted to a supported BigQuery scalar type: {e}" + ) + + def run_query(self, query: str, job_config: Optional[QueryJobConfig] = None) -> RowIterator: + """ + Executes a BigQuery SQL query and returns the results. + + Args: + query: The SQL query string to execute. + job_config: Optional QueryJobConfig for parameterized queries. + + Returns: + A RowIterator object to iterate over the query results. + + Raises: + google.cloud.exceptions.GoogleCloudError: If the query fails. + """ + try: + query_job = self.client.query(query, job_config=job_config) + return query_job.result() + except GoogleCloudError as e: + print(f"BigQuery Error: {e}") + print(f"Query: {query}") + if job_config and job_config.query_parameters: + print(f"Parameters: {job_config.query_parameters}") + raise + + def get_row_by_id(self, table_id: str, id_column: str, id_value: Any, id_value_type: str = "STRING") -> RowIterator: + """ + Retrieves a single row from a table based on its ID using parameterized query. + + Args: + table_id: The ID of the table (without dataset prefix). + id_column: The name of the column containing the ID. + id_value: The specific ID value to search for. + id_value_type: The BigQuery type of the ID value (e.g., "STRING", "INT64"). + + Returns: + A RowIterator containing the matching row(s). + """ + self._validate_column_name(id_column) + query = f""" + SELECT * FROM `{self.project_id}.{self.dataset_id}.{table_id}` + WHERE `{id_column}` = @id_value; + """ + job_config = QueryJobConfig( + query_parameters=[ + ScalarQueryParameter("id_value", id_value_type, id_value) + ] + ) + return self.run_query(query, job_config=job_config) + + def insert_rows_json(self, table_id: str, rows: List[Dict[str, Any]]) -> List[Dict]: + """ + Inserts multiple rows into the specified table using JSON. + This is the recommended method for insertions due to its safety and efficiency. + + Args: + table_id: The ID of the table. + rows: A list of dictionaries, where each dictionary represents a row + (column names as keys, values as row data). + + Returns: + A list of error dictionaries if any rows failed to insert, + otherwise an empty list. + """ + table_ref = self._get_table_ref(table_id) + errors_from_client = self.client.insert_rows_json(table_ref, rows) + errors_list: List[Dict[str, Any]] = list(errors_from_client) # type: ignore + if errors_list: + print(f"Errors inserting rows into {table_id}: {errors_list}") + return errors_list + + def insert_row(self, table_id: str, column_names: List[str], values: Tuple[Any, ...]) -> None: + """ + Inserts a new row into the specified table using a parameterized query. + Prefer `insert_rows_json` for multiple rows or complex data. + + Args: + table_id: The ID of the table. + column_names: A list of column names for the insert. + values: A tuple of values corresponding to the column_names. + + Raises: + ValueError: If column names are invalid or counts don't match. + TypeError: If a value in `values` cannot be mapped to a BigQuery scalar type. + """ + if not column_names or not values: + raise ValueError("Column names and values cannot be empty.") + if len(column_names) != len(values): + raise ValueError("Number of column names must match number of values.") + + for col_name in column_names: + self._validate_column_name(col_name) + + cols_str = ", ".join(f"`{col}`" for col in column_names) + placeholders = ", ".join([f"@param{i}" for i in range(len(values))]) + + query_params: List[ScalarQueryParameter] = [] + for i, current_value in enumerate(values): + param_name = f"param{i}" + query_params.append(self._create_scalar_query_parameter(param_name, current_value)) + + query = f""" + INSERT INTO `{self.project_id}.{self.dataset_id}.{table_id}` ({cols_str}) + VALUES ({placeholders}); + """ + job_config = QueryJobConfig(query_parameters=query_params) + self.run_query(query, job_config=job_config) + + def delete_multiple_rows_by_id( + self, table_id: str, id_column: str, ids: List[Any], id_value_type: str = "STRING" + ) -> None: + """ + Deletes multiple rows from a table based on a list of IDs using parameterized query. + + Args: + table_id: The ID of the table. + id_column: The name of the column containing the IDs. + ids: A list of ID values to delete. + id_value_type: The BigQuery type of the ID values. + """ + if not ids: + return # Nothing to delete + self._validate_column_name(id_column) + + query = f""" + DELETE FROM `{self.project_id}.{self.dataset_id}.{table_id}` + WHERE `{id_column}` IN UNNEST(@ids); + """ + job_config = QueryJobConfig( + query_parameters=[ + ArrayQueryParameter("ids", id_value_type, ids) + ] + ) + self.run_query(query, job_config=job_config) + + def update_row_by_id( + self, + table_id: str, + id_column: str, + id_value: Any, + column_values: Dict[str, Any], + id_value_type: str = "STRING", + ) -> None: + """ + Updates specific columns of a row identified by its ID using parameterized query. + + Args: + table_id: The ID of the table. + id_column: The name of the column containing the ID. + id_value: The specific ID value of the row to update. + column_values: A dictionary where keys are column names and + values are the new values. + id_value_type: The BigQuery type of the ID value. + """ + if not column_values: + return + self._validate_column_name(id_column) + + set_clauses = [] + query_params_list: List[ScalarQueryParameter] = [ + ScalarQueryParameter("id_value", id_value_type, id_value) + ] + + param_idx = 0 + for col, current_value in column_values.items(): + self._validate_column_name(col) + update_param_name = f"update_val_{param_idx}" + set_clauses.append(f"`{col}` = @{update_param_name}") + query_params_list.append(self._create_scalar_query_parameter(update_param_name, current_value)) + param_idx += 1 + + if not set_clauses: # Should not happen if column_values is not empty + return + + sets_str = ", ".join(set_clauses) + query = f""" + UPDATE `{self.project_id}.{self.dataset_id}.{table_id}` + SET {sets_str} + WHERE `{id_column}` = @id_value; + """ + job_config = QueryJobConfig(query_parameters=query_params_list) + self.run_query(query, job_config=job_config) + + def get_all_rows(self, table_id: str) -> RowIterator: + """ + Retrieves all rows from the specified table. + + Args: + table_id: The ID of the table. + + Returns: + A RowIterator containing all rows in the table. + """ + query = f"SELECT * FROM `{self.project_id}.{self.dataset_id}.{table_id}`" + return self.run_query(query) + + def delete_row_by_id(self, table_id: str, id_column: str, id_value: Any, id_value_type: str = "STRING") -> None: + """ + Deletes a single row from a table based on its ID using parameterized query. + + Args: + table_id: The ID of the table. + id_column: The name of the column containing the ID. + id_value: The specific ID value of the row to delete. + id_value_type: The BigQuery type of the ID value. + """ + self._validate_column_name(id_column) + query = f""" + DELETE FROM `{self.project_id}.{self.dataset_id}.{table_id}` + WHERE `{id_column}` = @id_value; + """ + job_config = QueryJobConfig( + query_parameters=[ + ScalarQueryParameter("id_value", id_value_type, id_value) + ] + ) + self.run_query(query, job_config=job_config) diff --git a/gemini/sample-apps/quickbot/website-search-using-agent-builder/backend/src/service/__init__.py b/gemini/sample-apps/quickbot/website-search-using-agent-builder/backend/src/service/__init__.py new file mode 100644 index 0000000..36a1e8d --- /dev/null +++ b/gemini/sample-apps/quickbot/website-search-using-agent-builder/backend/src/service/__init__.py @@ -0,0 +1,14 @@ +# Copyright 2025 Google LLC +# +# 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 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + diff --git a/gemini/sample-apps/quickbot/website-search-using-agent-builder/backend/src/service/engine.py b/gemini/sample-apps/quickbot/website-search-using-agent-builder/backend/src/service/engine.py new file mode 100644 index 0000000..685d40b --- /dev/null +++ b/gemini/sample-apps/quickbot/website-search-using-agent-builder/backend/src/service/engine.py @@ -0,0 +1,77 @@ +# Copyright 2025 Google LLC +# +# 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 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Service for interacting with Google Cloud Discovery Engine Engines.""" + +from typing import List +from google.cloud.discoveryengine_v1 import ( + EngineServiceClient, + ListEnginesRequest, +) +from google.api_core.client_options import ClientOptions + +from src.model.search import PROJECT_ID, Engine + +LOCATIONS = [ + "global", + "us", +] + + +class EngineService: + """Provides methods to list Discovery Engine Engines.""" + + def get_all(self) -> List[Engine]: + """ + Retrieves all available Discovery Engines for the configured project + across specified locations. + + Fetches engines from predefined locations ('global', 'us') within the + default collection. + + Returns: + A list of Engine objects, each containing details about a + discovered engine. Returns an empty list if no engines are found + or if an error occurs during API calls. + + Raises: + Prints error logs if API calls fail for a specific location. + """ + engines = [] + for location in LOCATIONS: + client_options = ( + ClientOptions( + api_endpoint=f"{location}-discoveryengine.googleapis.com" + ) + if location != "global" + else None + ) + + # Create a client + client = EngineServiceClient(client_options=client_options) + list_engines = client.list_engines( + ListEnginesRequest( + parent=f"projects/{PROJECT_ID}/locations/{location}" + "/collections/default_collection" + ) + ) + for engine in list_engines.engines: + engines.append( + Engine( + name=engine.display_name, + engine_id=engine.name.split("/")[-1], + region=location, + ) + ) + return engines diff --git a/gemini/sample-apps/quickbot/website-search-using-agent-builder/backend/src/service/search.py b/gemini/sample-apps/quickbot/website-search-using-agent-builder/backend/src/service/search.py new file mode 100644 index 0000000..5812c30 --- /dev/null +++ b/gemini/sample-apps/quickbot/website-search-using-agent-builder/backend/src/service/search.py @@ -0,0 +1,135 @@ +# Copyright 2025 Google LLC +# +# 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 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Service layer for interacting with the Google Cloud Discovery Engine Search API. + +This module provides the SearchService class, which encapsulates the logic for +constructing search requests, executing them against a configured +Discovery Engine, and processing the results into a standardized format. +""" + +from typing import List +from google.cloud.discoveryengine_v1 import SearchRequest, SearchServiceClient +from src.model.search import SearchApplication, SearchResult + +CONTENT_SEARCH_SPEC = SearchRequest.ContentSearchSpec( + snippet_spec=SearchRequest.ContentSearchSpec.SnippetSpec( + return_snippet=True + ), + extractive_content_spec=SearchRequest.ContentSearchSpec.ExtractiveContentSpec( + max_extractive_answer_count=1 + ), +) +QUERY_EXPANSION_SPEC = SearchRequest.QueryExpansionSpec( + condition=SearchRequest.QueryExpansionSpec.Condition.AUTO, +) +SPELL_CORRECTION_SPEC = SearchRequest.SpellCorrectionSpec( + mode=SearchRequest.SpellCorrectionSpec.Mode.AUTO +) + + +class SearchService: + """ + Handles search operations using Google Cloud Discovery Engine. + + This service takes a SearchApplication configuration, initializes the + necessary Discovery Engine client, and provides a method to perform + searches based on a given term. + """ + + def __init__(self, search_application: SearchApplication): + """ + Initializes the SearchService. + + Sets up the Discovery Engine SearchServiceClient with appropriate + client options based on the region specified in the search_application. + It also stores the serving configuration path derived from the + search_application details. + + Args: + search_application: The SearchApplication configuration containing + engine ID, region, and other necessary details + to connect to the correct Discovery Engine. + """ + self.search_client = SearchServiceClient( + client_options=search_application.get_client_options() + ) + self.serving_config = search_application.get_serving_config() + + def search(self, term: str) -> List[SearchResult]: + """ + Performs a search query against the configured Discovery Engine. + + Constructs a SearchRequest with the provided search term and predefined + configs for content search, query expansion and spell correction. + It then calls the Discovery Engine API, processes the results, filters + them (currently only keeping 'en' locale results), extracts relevant + information like title, snippet, link, and image, and maps them to + SearchResult objects. + + Args: + term: The search query string entered by the user. + + Returns: + A list of SearchResult objects representing the processed and + filtered search results. Returns an empty list if no relevant + results are found or if an error occurs during the API call + (though errors are not explicitly handled here beyond the client). + """ + request = SearchRequest( + serving_config=self.serving_config, + query=term, + page_size=10, + content_search_spec=CONTENT_SEARCH_SPEC, + query_expansion_spec=QUERY_EXPANSION_SPEC, + spell_correction_spec=SPELL_CORRECTION_SPEC, + ) + + data = self.search_client.search(request) + results = [] + + # Process results + for r in data.results: + document = r.document + derived_data = document.derived_struct_data + if ( + derived_data.get("pagemap").get("metatags")[0].get("og:locale") + != "en" + ): + continue + + # Extract snippet safely + snippets = derived_data.get("snippets") + snippet_text = ( + snippets[0].get("snippet", "No snippet available") + if snippets + else "No snippet available" + ) + + # Map to SearchResult + mapped_result = SearchResult( + document_id=document.id, + title=derived_data.get("title", "Untitled"), + snippet=snippet_text, + link=derived_data.get("link"), + formatted_url=derived_data.get("formattedUrl"), + img=derived_data.get("pagemap") + .get("cse_thumbnail")[0] + .get("src"), + displayLink=derived_data.get("displayLink"), + ) + results.append(mapped_result) + + return results diff --git a/gemini/sample-apps/quickbot/website-search-using-agent-builder/backend/src/service/search_application.py b/gemini/sample-apps/quickbot/website-search-using-agent-builder/backend/src/service/search_application.py new file mode 100644 index 0000000..d8f795f --- /dev/null +++ b/gemini/sample-apps/quickbot/website-search-using-agent-builder/backend/src/service/search_application.py @@ -0,0 +1,119 @@ +# Copyright 2025 Google LLC +# +# 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 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Service for managing the Search Application configuration +stored in BigQuery.""" + +from src.model.http_status import BadRequest +from src.model.search import SearchApplication +from src.repository.big_query import BigQueryRepository + +SEARCH_APPLICATION_TABLE = "search_applications" +SEARCH_APPLICATION_TABLE_ID_COLUMN = "engine_id" + + +class SearchApplicationService: + """Handles business logic for Search Application configuration.""" + + def __init__(self): + """Initializes the service with a BigQuery repository.""" + self.repository = BigQueryRepository() + + def get(self): + """ + Retrieves the currently configured Search Application. + + Assumes that there is at most one Search Application configuration + stored in the BigQuery table. If multiple rows exist, it returns + the last one processed. + + Returns: + A SearchApplication object if found, otherwise None. + """ + search_application = None + results = self.repository.get_all_rows(SEARCH_APPLICATION_TABLE) + # Convert RowIterator to list to check if it's empty or get the last item + rows = list(results) + if rows: + # Assuming we take the last row if multiple exist, or the only one + search_application = SearchApplication.from_row(rows[-1]) + + return search_application + + def create( + self, search_application: SearchApplication + ) -> SearchApplication: + """ + Creates a new Search Application configuration in BigQuery. + + Ensures that no configuration already exists before creating a new one. + + Args: + search_application: The SearchApplication object to create. + + Returns: + The created SearchApplication object. + + Raises: + BadRequest: If a Search Application configuration already exists + for the project. + """ + if self.get(): + raise BadRequest( + detail="Search Application for this project already exists" + ) + schema_fields = SearchApplication.__schema__() + column_names = [field.name for field in schema_fields] + + # Prepare values as a tuple, matching the order of schema_fields + # Ensure the order here matches the order in SearchApplication.__schema__ + values_tuple = (search_application.engine_id, search_application.region) + + self.repository.insert_row( + SEARCH_APPLICATION_TABLE, + column_names, + values_tuple, + ) + return search_application + + def update(self, engine_id: str, search_application: SearchApplication): + """ + Updates an existing Search Application configuration in BigQuery. + + Identifies the row to update using the provided engine_id. + + Args: + engine_id: The engine_id of the Search Application configuration + to update. + search_application: A SearchApplication object containing the + updated details (engine_id and region). + + Returns: + The updated SearchApplication object. + + Raises: + # Note: The underlying repository might raise exceptions on failure, + # which are not explicitly handled here. + # Consider adding error handling. + """ + update_dict = { + "engine_id": f'"{search_application.engine_id}"', + "region": f'"{search_application.region}"', + } + self.repository.update_row_by_id( + SEARCH_APPLICATION_TABLE, + SEARCH_APPLICATION_TABLE_ID_COLUMN, + engine_id, + update_dict, + ) diff --git a/gemini/sample-apps/quickbot/website-search-using-agent-builder/docker-compose.yml b/gemini/sample-apps/quickbot/website-search-using-agent-builder/docker-compose.yml new file mode 100644 index 0000000..31d69c9 --- /dev/null +++ b/gemini/sample-apps/quickbot/website-search-using-agent-builder/docker-compose.yml @@ -0,0 +1,61 @@ +# Copyright 2025 Google LLC +# +# 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 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +services: + backend: + build: + secrets: + - gcp_credentials + context: ./backend + dockerfile: Dockerfile.local + args: + IS_FIRST_DEPLOYMENT: "True" + GCLOUD_PROJECT: my-project-id + ENVIRONMENT: development + FRONTEND_URL: http://localhost:4200 + BIG_QUERY_DATASET: bigquery_quickbot_website_search + container_name: quickbot-website-search-backend + ports: + - "8080:8080" + volumes: + - ./backend:/app + # Mount the gcloud ADC directory. Replace with the correct path for your OS if different. :ro - read-only for better security + - ~/.config/gcloud/:/root/.config/gcloud:ro # Linux/macOS example: + # - %APPDATA%/gcloud:/root/.config/gcloud:ro # Windows example: + environment: + GOOGLE_APPLICATION_CREDENTIALS: "/root/.config/gcloud/application_default_credentials.json" + restart: unless-stopped + + frontend: + build: + context: ./frontend + dockerfile: Dockerfile + container_name: quickbot-website-search-frontend + ports: + - "4200:8080" + volumes: + - ./frontend:/app + # Use an anonymous volume for node_modules to prevent host node_modules + # from interfering and potentially speed up builds/rebuilds + - /app/node_modules + # To change the env variables in the frontend, change the environment.ts file + restart: unless-stopped + +secrets: + gcp_credentials: # This ID must match the one used in services.backend.build.secrets + # Path to your Google Application Default Credentials JSON file on the HOST machine. + # Replace with the correct path for your OS if different. + file: ~/.config/gcloud/application_default_credentials.json + # Windows example (you might need to adjust the target path inside the container if it's a Linux container): + # - %APPDATA%/gcloud/application_default_credentials.json \ No newline at end of file diff --git a/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/.dockerignore b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/.dockerignore new file mode 100644 index 0000000..7109f56 --- /dev/null +++ b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/.dockerignore @@ -0,0 +1,4 @@ +.angular +.vscode +dist +node_modules \ No newline at end of file diff --git a/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/.editorconfig b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/.editorconfig new file mode 100644 index 0000000..59d9a3a --- /dev/null +++ b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/.editorconfig @@ -0,0 +1,16 @@ +# Editor configuration, see https://editorconfig.org +root = true + +[*] +charset = utf-8 +indent_style = space +indent_size = 2 +insert_final_newline = true +trim_trailing_whitespace = true + +[*.ts] +quote_type = single + +[*.md] +max_line_length = off +trim_trailing_whitespace = false diff --git a/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/.eslintignore b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/.eslintignore new file mode 100644 index 0000000..5df9179 --- /dev/null +++ b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/.eslintignore @@ -0,0 +1,2 @@ +build/ +dist/ \ No newline at end of file diff --git a/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/.eslintrc.json b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/.eslintrc.json new file mode 100644 index 0000000..f95bb33 --- /dev/null +++ b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/.eslintrc.json @@ -0,0 +1,3 @@ +{ + "extends": "./node_modules/gts/" +} diff --git a/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/.gitignore b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/.gitignore new file mode 100644 index 0000000..0711527 --- /dev/null +++ b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/.gitignore @@ -0,0 +1,42 @@ +# See http://help.github.com/ignore-files/ for more about ignoring files. + +# Compiled output +/dist +/tmp +/out-tsc +/bazel-out + +# Node +/node_modules +npm-debug.log +yarn-error.log + +# IDEs and editors +.idea/ +.project +.classpath +.c9/ +*.launch +.settings/ +*.sublime-workspace + +# Visual Studio Code +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +.history/* + +# Miscellaneous +/.angular/cache +.sass-cache/ +/connect.lock +/coverage +/libpeerconnection.log +testem.log +/typings + +# System files +.DS_Store +Thumbs.db diff --git a/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/.prettierrc.js b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/.prettierrc.js new file mode 100644 index 0000000..535df79 --- /dev/null +++ b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/.prettierrc.js @@ -0,0 +1,33 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +module.exports = { + ...require('gts/.prettierrc.json'), + "overrides": [ + { + "files": "*.html", + "options": { + "parser": "angular" + } + }, + { + "files": "*.scss", + "options": { + "parser": "scss" + } + } + ] +} diff --git a/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/Dockerfile b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/Dockerfile new file mode 100644 index 0000000..a58c895 --- /dev/null +++ b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/Dockerfile @@ -0,0 +1,27 @@ +# Copyright 2025 Google LLC +# +# 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 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +FROM node:18.17.1 as build + +RUN mkdir -p /code +WORKDIR /code +COPY . /code/ +RUN npm ci +RUN npm run build:prod + +FROM nginx:alpine +COPY --from=build /code/dist/quick-bot-app-frontend /usr/share/nginx/html +COPY --from=build /code/nginx.conf /etc/nginx/conf.d/ + +EXPOSE 8080 \ No newline at end of file diff --git a/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/README.md b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/README.md new file mode 100644 index 0000000..b551428 --- /dev/null +++ b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/README.md @@ -0,0 +1,87 @@ +# QuickBotAppFrontend + +This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 15.1.3. + +## Development server + +Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The application will automatically reload if you change any of the source files. + +## Code scaffolding + +Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`. + +## Build + +Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. + +## Running unit tests + +Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io). + +## Running end-to-end tests + +Run `ng e2e` to execute the end-to-end tests via a platform of your choice. To use this command, you need to first add a package that implements end-to-end testing capabilities. + +## Further help + +To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.io/cli) page. + +## Code Styling & Commit Guidelines + +To maintain code quality and consistency: + +* **TypeScript (Frontend):** We follow [Angular Coding Style Guide](https://angular.dev/style-guide) by leveraging the use of [Google's TypeScript Style Guide](https://github.com/google/gts) using `gts`. This includes a formatter, linter, and automatic code fixer. +* **Python (Backend):** We adhere to the [Google Python Style Guide](https://google.github.io/styleguide/pyguide.html), using tools like `pylint` and `black` for linting and formatting. +* **Commit Messages:** We suggest following [Angular's Commit Message Guidelines](https://github.com/angular/angular/blob/main/contributing-docs/commit-message-guidelines.md) to create clear and descriptive commit messages. + +#### Frontend (TypeScript with `gts`) + +1. **Initialize `gts` (if not already done in the project):** + Navigate to the `frontend/` directory and run: + ```bash + npx gts init + ``` + This will set up `gts` and create necessary configuration files (like `tsconfig.json`). Ensure your `tsconfig.json` (or a related gts config file like `.gtsrc`) includes an extension for `gts` defaults, typically: + ```json + { + "extends": "./node_modules/gts/tsconfig-google.json", + // ... other configurations + } + ``` +2. **Check for linting issues:** + ```bash + npm run lint + ``` + (This assumes a `lint` script is defined in `package.json`, e.g., `"lint": "gts lint"`) +3. **Fix linting issues automatically (where possible):** + ```bash + npm run fix + ``` + (This assumes a `fix` script is defined in `package.json`, e.g., `"fix": "gts fix"`) + +#### Backend (Python with `pylint` and `black`) + +1. **Ensure Dependencies are Installed:** + Add `pylint` and `black` to your `backend/requirements.txt` file: + ``` + pylint + black + ``` + Then install them within your virtual environment: + ```bash + pip install pylint black + # or pip install -r requirements.txt + ``` +2. **Configure `pylint`:** + It's recommended to have a `.pylintrc` file in your `backend/` directory to configure `pylint` rules. You might need to copy a standard one or generate one (`pylint --generate-rcfile > .pylintrc`). +3. **Check for linting issues with `pylint`:** + Navigate to the `backend/` directory and run: + ```bash + pylint . + ``` + (Or specify modules/packages: `pylint your_module_name`) +4. **Format code with `black`:** + To automatically format all Python files in the current directory and subdirectories: + ```bash + python -m black . --line-length=80 + ``` diff --git a/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/angular.json b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/angular.json new file mode 100644 index 0000000..15d3e38 --- /dev/null +++ b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/angular.json @@ -0,0 +1,123 @@ +{ + "$schema": "./node_modules/@angular/cli/lib/config/schema.json", + "version": 1, + "newProjectRoot": "projects", + "projects": { + "quick-bot-app-frontend": { + "projectType": "application", + "schematics": { + "@schematics/angular:component": { + "style": "scss" + } + }, + "root": "", + "sourceRoot": "src", + "prefix": "app", + "architect": { + "build": { + "builder": "@angular-devkit/build-angular:browser", + "options": { + "outputPath": "dist/quick-bot-app-frontend", + "index": "src/index.html", + "main": "src/main.ts", + "polyfills": [ + "zone.js" + ], + "tsConfig": "tsconfig.app.json", + "inlineStyleLanguage": "scss", + "assets": [ + "src/favicon.ico", + "src/assets" + ], + "styles": [ + "@angular/material/prebuilt-themes/indigo-pink.css", + "src/styles.scss", + "node_modules/prismjs/themes/prism-okaidia.css" + ], + "scripts": [ + "node_modules/marked/marked.min.js", + "node_modules/prismjs/prism.js", + "node_modules/prismjs/components/prism-csharp.min.js", + "node_modules/prismjs/components/prism-css.min.js", + "node_modules/clipboard/dist/clipboard.min.js" + ] + }, + "configurations": { + "production": { + "budgets": [ + { + "type": "initial", + "maximumWarning": "4mb", + "maximumError": "10mb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "20kb", + "maximumError": "50kb" + } + ], + "outputHashing": "all" + }, + "development": { + "buildOptimizer": false, + "optimization": false, + "vendorChunk": true, + "extractLicenses": false, + "sourceMap": true, + "namedChunks": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "builder": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "browserTarget": "quick-bot-app-frontend:build:production" + }, + "development": { + "browserTarget": "quick-bot-app-frontend:build:development" + } + }, + "defaultConfiguration": "development" + }, + "extract-i18n": { + "builder": "@angular-devkit/build-angular:extract-i18n", + "options": { + "browserTarget": "quick-bot-app-frontend:build" + } + }, + "test": { + "builder": "@angular-devkit/build-angular:karma", + "options": { + "polyfills": [ + "zone.js", + "zone.js/testing" + ], + "tsConfig": "tsconfig.spec.json", + "inlineStyleLanguage": "scss", + "assets": [ + "src/favicon.ico", + "src/assets" + ], + "styles": [ + "@angular/material/prebuilt-themes/indigo-pink.css", + "src/styles.scss", + "node_modules/prismjs/themes/prism-okaidia.css" + ], + "scripts": [ + "node_modules/marked/marked.min.js", + "node_modules/prismjs/prism.js", + "node_modules/prismjs/components/prism-csharp.min.js", + "node_modules/prismjs/components/prism-css.min.js", + "node_modules/clipboard/dist/clipboard.min.js" + ] + } + } + } + } + }, + "cli": { + "analytics": false + } +} diff --git a/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/nginx.conf b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/nginx.conf new file mode 100644 index 0000000..6c8cce4 --- /dev/null +++ b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/nginx.conf @@ -0,0 +1,11 @@ +server { + listen 8080; + + root /usr/share/nginx/html; + index index.html index.htm; + include /etc/nginx/mime.types; + + location / { + try_files $uri $uri/ /index.html; + } +} \ No newline at end of file diff --git a/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/package-lock.json b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/package-lock.json new file mode 100644 index 0000000..ecd1769 --- /dev/null +++ b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/package-lock.json @@ -0,0 +1,24627 @@ +{ + "name": "quick-bot-app-frontend", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "quick-bot-app-frontend", + "version": "0.0.0", + "hasInstallScript": true, + "dependencies": { + "@angular/animations": "^15.1.0", + "@angular/cdk": "^15.2.9", + "@angular/common": "^15.1.0", + "@angular/compiler": "^15.1.0", + "@angular/core": "^15.1.0", + "@angular/fire": "7.5", + "@angular/flex-layout": "^15.0.0-beta.42", + "@angular/forms": "^15.1.0", + "@angular/material": "^15.2.9", + "@angular/platform-browser": "^15.1.0", + "@angular/platform-browser-dynamic": "^15.1.0", + "@angular/router": "^15.1.0", + "@fortawesome/fontawesome-free": "^6.6.0", + "@ionic/angular": "^7.5.4", + "@ng-idle/core": "^14.0.0", + "clipboard": "^2.0.11", + "ionic-emoji-rating": "^1.0.15", + "marked": "^4.3.0", + "moment": "^2.30.1", + "ngx-markdown": "^15.1.2", + "prismjs": "^1.29.0", + "rxfire": "6.0.3", + "rxjs": "~7.8.0", + "tslib": "^2.3.0", + "uuid": "^9.0.1", + "zone.js": "~0.12.0" + }, + "devDependencies": { + "@angular-devkit/build-angular": "^15.1.3", + "@angular/cli": "~15.1.3", + "@angular/compiler-cli": "^15.1.0", + "@types/jasmine": "~4.3.0", + "@types/marked": "^4.3.0", + "@types/node": "^22.7.5", + "@types/uuid": "^9.0.4", + "firebase-tools": "^11.30.0", + "gts": "^5.3.1", + "jasmine-core": "~4.5.0", + "karma": "~6.4.0", + "karma-chrome-launcher": "~3.1.0", + "karma-coverage": "~2.2.0", + "karma-jasmine": "~5.1.0", + "karma-jasmine-html-reporter": "~2.0.0", + "typescript": "~4.9.4" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.0.tgz", + "integrity": "sha512-qRmjj8nj9qmLTQXXmaR1cck3UXSRMPrbsLJAasZpF+t3riI71BXed5ebIOYwQntykeZuhjsdweEc9BxH5Jc26w==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.1.0", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@angular-devkit/architect": { + "version": "0.1502.11", + "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1502.11.tgz", + "integrity": "sha512-+hkG5UjIaKMRdo6SFLNQs+Cv7yAVeN8ijfDwI2z/mp7/otowuSEy+H3Tii195jfJ8TQ+y1B7svnx2D6O7oOYbQ==", + "dev": true, + "dependencies": { + "@angular-devkit/core": "15.2.11", + "rxjs": "6.6.7" + }, + "engines": { + "node": "^14.20.0 || ^16.13.0 || >=18.10.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@angular-devkit/architect/node_modules/rxjs": { + "version": "6.6.7", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", + "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", + "dev": true, + "dependencies": { + "tslib": "^1.9.0" + }, + "engines": { + "npm": ">=2.0.0" + } + }, + "node_modules/@angular-devkit/architect/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + }, + "node_modules/@angular-devkit/build-angular": { + "version": "15.2.11", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-angular/-/build-angular-15.2.11.tgz", + "integrity": "sha512-MnpVCJdk5jHuK7CH/cTcRT0JQkkKkRTEV3WTyOUhTm0O3PlKwvTM6/Sner+zyuhKyw5VFBBMypHh59aTUDEZ1A==", + "dev": true, + "dependencies": { + "@ampproject/remapping": "2.2.0", + "@angular-devkit/architect": "0.1502.11", + "@angular-devkit/build-webpack": "0.1502.11", + "@angular-devkit/core": "15.2.11", + "@babel/core": "7.20.12", + "@babel/generator": "7.20.14", + "@babel/helper-annotate-as-pure": "7.18.6", + "@babel/helper-split-export-declaration": "7.18.6", + "@babel/plugin-proposal-async-generator-functions": "7.20.7", + "@babel/plugin-transform-async-to-generator": "7.20.7", + "@babel/plugin-transform-runtime": "7.19.6", + "@babel/preset-env": "7.20.2", + "@babel/runtime": "7.20.13", + "@babel/template": "7.20.7", + "@discoveryjs/json-ext": "0.5.7", + "@ngtools/webpack": "15.2.11", + "ansi-colors": "4.1.3", + "autoprefixer": "10.4.13", + "babel-loader": "9.1.2", + "babel-plugin-istanbul": "6.1.1", + "browserslist": "4.21.5", + "cacache": "17.0.4", + "chokidar": "3.5.3", + "copy-webpack-plugin": "11.0.0", + "critters": "0.0.16", + "css-loader": "6.7.3", + "esbuild-wasm": "0.17.8", + "glob": "8.1.0", + "https-proxy-agent": "5.0.1", + "inquirer": "8.2.4", + "jsonc-parser": "3.2.0", + "karma-source-map-support": "1.4.0", + "less": "4.1.3", + "less-loader": "11.1.0", + "license-webpack-plugin": "4.0.2", + "loader-utils": "3.2.1", + "magic-string": "0.29.0", + "mini-css-extract-plugin": "2.7.2", + "open": "8.4.1", + "ora": "5.4.1", + "parse5-html-rewriting-stream": "7.0.0", + "piscina": "3.2.0", + "postcss": "8.4.31", + "postcss-loader": "7.0.2", + "resolve-url-loader": "5.0.0", + "rxjs": "6.6.7", + "sass": "1.58.1", + "sass-loader": "13.2.0", + "semver": "7.5.3", + "source-map-loader": "4.0.1", + "source-map-support": "0.5.21", + "terser": "5.16.3", + "text-table": "0.2.0", + "tree-kill": "1.2.2", + "tslib": "2.5.0", + "webpack": "5.76.1", + "webpack-dev-middleware": "6.1.2", + "webpack-dev-server": "4.11.1", + "webpack-merge": "5.8.0", + "webpack-subresource-integrity": "5.1.0" + }, + "engines": { + "node": "^14.20.0 || ^16.13.0 || >=18.10.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "optionalDependencies": { + "esbuild": "0.17.8" + }, + "peerDependencies": { + "@angular/compiler-cli": "^15.0.0", + "@angular/localize": "^15.0.0", + "@angular/platform-server": "^15.0.0", + "@angular/service-worker": "^15.0.0", + "karma": "^6.3.0", + "ng-packagr": "^15.0.0", + "protractor": "^7.0.0", + "tailwindcss": "^2.0.0 || ^3.0.0", + "typescript": ">=4.8.2 <5.0" + }, + "peerDependenciesMeta": { + "@angular/localize": { + "optional": true + }, + "@angular/platform-server": { + "optional": true + }, + "@angular/service-worker": { + "optional": true + }, + "karma": { + "optional": true + }, + "ng-packagr": { + "optional": true + }, + "protractor": { + "optional": true + }, + "tailwindcss": { + "optional": true + } + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/rxjs": { + "version": "6.6.7", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", + "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", + "dev": true, + "dependencies": { + "tslib": "^1.9.0" + }, + "engines": { + "npm": ">=2.0.0" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/rxjs/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + }, + "node_modules/@angular-devkit/build-angular/node_modules/tslib": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz", + "integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==", + "dev": true + }, + "node_modules/@angular-devkit/build-webpack": { + "version": "0.1502.11", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.1502.11.tgz", + "integrity": "sha512-OTONIRp770Jfems4+cULmtoeSzjnpx5UjV2EazojnhRXXBSJMWRMPvwD2QvQl9UO/6eOV3d2mgmP2xOZgc/D6w==", + "dev": true, + "dependencies": { + "@angular-devkit/architect": "0.1502.11", + "rxjs": "6.6.7" + }, + "engines": { + "node": "^14.20.0 || ^16.13.0 || >=18.10.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "webpack": "^5.30.0", + "webpack-dev-server": "^4.0.0" + } + }, + "node_modules/@angular-devkit/build-webpack/node_modules/rxjs": { + "version": "6.6.7", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", + "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", + "dev": true, + "dependencies": { + "tslib": "^1.9.0" + }, + "engines": { + "npm": ">=2.0.0" + } + }, + "node_modules/@angular-devkit/build-webpack/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + }, + "node_modules/@angular-devkit/core": { + "version": "15.2.11", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-15.2.11.tgz", + "integrity": "sha512-zd6QelJ8pOPvz6TsehR0JqixjDjzgEOkKywBJBuwNXY+Nw3MJGayJeWS0UgC+Gk+LoTkpI21RoyaYELkAmD/tw==", + "dev": true, + "dependencies": { + "ajv": "8.12.0", + "ajv-formats": "2.1.1", + "jsonc-parser": "3.2.0", + "rxjs": "6.6.7", + "source-map": "0.7.4" + }, + "engines": { + "node": "^14.20.0 || ^16.13.0 || >=18.10.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "chokidar": "^3.5.2" + }, + "peerDependenciesMeta": { + "chokidar": { + "optional": true + } + } + }, + "node_modules/@angular-devkit/core/node_modules/rxjs": { + "version": "6.6.7", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", + "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", + "dev": true, + "dependencies": { + "tslib": "^1.9.0" + }, + "engines": { + "npm": ">=2.0.0" + } + }, + "node_modules/@angular-devkit/core/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + }, + "node_modules/@angular-devkit/schematics": { + "version": "15.1.6", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-15.1.6.tgz", + "integrity": "sha512-cwmJFpS43zrdlmfwfHIxG/Nzg5rzFdtKrHx64ZXxNFm6JdyK2JTs/qrHUwv1FYWAcqhdiHn+00jYklMmvsvPOA==", + "dependencies": { + "@angular-devkit/core": "15.1.6", + "jsonc-parser": "3.2.0", + "magic-string": "0.27.0", + "ora": "5.4.1", + "rxjs": "6.6.7" + }, + "engines": { + "node": "^14.20.0 || ^16.13.0 || >=18.10.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@angular-devkit/schematics/node_modules/@angular-devkit/core": { + "version": "15.1.6", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-15.1.6.tgz", + "integrity": "sha512-jGgxyRjecVf6lEyqDxz7ltMEndNPxIg720pk6r40fgsu0dU8w9vjJSJe7k0XdJiXVRcN6wZa/J5nO/xcwWVIsA==", + "dependencies": { + "ajv": "8.12.0", + "ajv-formats": "2.1.1", + "jsonc-parser": "3.2.0", + "rxjs": "6.6.7", + "source-map": "0.7.4" + }, + "engines": { + "node": "^14.20.0 || ^16.13.0 || >=18.10.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "chokidar": "^3.5.2" + }, + "peerDependenciesMeta": { + "chokidar": { + "optional": true + } + } + }, + "node_modules/@angular-devkit/schematics/node_modules/magic-string": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.27.0.tgz", + "integrity": "sha512-8UnnX2PeRAPZuN12svgR9j7M1uWMovg/CEnIwIG0LFkXSJJe4PdfUGiTGl8V9bsBHFUtfVINcSyYxd7q+kx9fA==", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.13" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@angular-devkit/schematics/node_modules/rxjs": { + "version": "6.6.7", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", + "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", + "dependencies": { + "tslib": "^1.9.0" + }, + "engines": { + "npm": ">=2.0.0" + } + }, + "node_modules/@angular-devkit/schematics/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + }, + "node_modules/@angular/animations": { + "version": "15.2.10", + "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-15.2.10.tgz", + "integrity": "sha512-yxfN8qQpMaukRU5LjFkJBmy85rqrOp86tYVCsf+hmPEFRiXBMUj6xYLeCMcpk3Mt1JtnWGBR34ivGx+7bNeAow==", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^14.20.0 || ^16.13.0 || >=18.10.0" + }, + "peerDependencies": { + "@angular/core": "15.2.10" + } + }, + "node_modules/@angular/cdk": { + "version": "15.2.9", + "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-15.2.9.tgz", + "integrity": "sha512-koaM07N1AIQ5oHU27l0/FoQSSoYAwlAYwVZ4Di3bYrJsTBNCN2Xsby7wI8gZxdepMnV4Fe9si382BDBov+oO4Q==", + "dependencies": { + "tslib": "^2.3.0" + }, + "optionalDependencies": { + "parse5": "^7.1.2" + }, + "peerDependencies": { + "@angular/common": "^15.0.0 || ^16.0.0", + "@angular/core": "^15.0.0 || ^16.0.0", + "rxjs": "^6.5.3 || ^7.4.0" + } + }, + "node_modules/@angular/cdk/node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "optional": true, + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/@angular/cdk/node_modules/parse5": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz", + "integrity": "sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==", + "optional": true, + "dependencies": { + "entities": "^4.4.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/@angular/cli": { + "version": "15.1.6", + "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-15.1.6.tgz", + "integrity": "sha512-GmC9jZK2ipUWj0dlfTI5oEYia4y1fLfO3AtAKU5CylNYrGyB+DRytKY8Bx6Fs4kaNBY8V8YnyLi7E/78gziMdg==", + "dev": true, + "dependencies": { + "@angular-devkit/architect": "0.1501.6", + "@angular-devkit/core": "15.1.6", + "@angular-devkit/schematics": "15.1.6", + "@schematics/angular": "15.1.6", + "@yarnpkg/lockfile": "1.1.0", + "ansi-colors": "4.1.3", + "ini": "3.0.1", + "inquirer": "8.2.4", + "jsonc-parser": "3.2.0", + "npm-package-arg": "10.1.0", + "npm-pick-manifest": "8.0.1", + "open": "8.4.0", + "ora": "5.4.1", + "pacote": "15.0.8", + "resolve": "1.22.1", + "semver": "7.3.8", + "symbol-observable": "4.0.0", + "yargs": "17.6.2" + }, + "bin": { + "ng": "bin/ng.js" + }, + "engines": { + "node": "^14.20.0 || ^16.13.0 || >=18.10.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@angular/cli/node_modules/@angular-devkit/architect": { + "version": "0.1501.6", + "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1501.6.tgz", + "integrity": "sha512-u07zZFlfrg0Qn4mu5M9Nz0pH2Yd2028XF/73980PsZMxwkSm4diF08v4bHk3UyR7yPT7phwvt4znj6ryZhx1gw==", + "dev": true, + "dependencies": { + "@angular-devkit/core": "15.1.6", + "rxjs": "6.6.7" + }, + "engines": { + "node": "^14.20.0 || ^16.13.0 || >=18.10.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@angular/cli/node_modules/@angular-devkit/core": { + "version": "15.1.6", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-15.1.6.tgz", + "integrity": "sha512-jGgxyRjecVf6lEyqDxz7ltMEndNPxIg720pk6r40fgsu0dU8w9vjJSJe7k0XdJiXVRcN6wZa/J5nO/xcwWVIsA==", + "dev": true, + "dependencies": { + "ajv": "8.12.0", + "ajv-formats": "2.1.1", + "jsonc-parser": "3.2.0", + "rxjs": "6.6.7", + "source-map": "0.7.4" + }, + "engines": { + "node": "^14.20.0 || ^16.13.0 || >=18.10.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "chokidar": "^3.5.2" + }, + "peerDependenciesMeta": { + "chokidar": { + "optional": true + } + } + }, + "node_modules/@angular/cli/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@angular/cli/node_modules/open": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/open/-/open-8.4.0.tgz", + "integrity": "sha512-XgFPPM+B28FtCCgSb9I+s9szOC1vZRSwgWsRUA5ylIxRTgKozqjOCrVOqGsYABPYK5qnfqClxZTFBa8PKt2v6Q==", + "dev": true, + "dependencies": { + "define-lazy-prop": "^2.0.0", + "is-docker": "^2.1.1", + "is-wsl": "^2.2.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@angular/cli/node_modules/rxjs": { + "version": "6.6.7", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", + "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", + "dev": true, + "dependencies": { + "tslib": "^1.9.0" + }, + "engines": { + "npm": ">=2.0.0" + } + }, + "node_modules/@angular/cli/node_modules/semver": { + "version": "7.3.8", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", + "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@angular/cli/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + }, + "node_modules/@angular/cli/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/@angular/common": { + "version": "15.2.10", + "resolved": "https://registry.npmjs.org/@angular/common/-/common-15.2.10.tgz", + "integrity": "sha512-jdBn3fctkqoNrJn9VLsUHpcCEhCxWSczdsR+BBbD6T0oLl6vMrAVNjPwfBejnlgfWN1KoRU9kgOYsMxa5apIWQ==", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^14.20.0 || ^16.13.0 || >=18.10.0" + }, + "peerDependencies": { + "@angular/core": "15.2.10", + "rxjs": "^6.5.3 || ^7.4.0" + } + }, + "node_modules/@angular/compiler": { + "version": "15.2.10", + "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-15.2.10.tgz", + "integrity": "sha512-M0XkeU0O73UlJZwDvOyp8/apetz9UKj78eTFDseMYJDLcxe6MpkbkxqpsGZnKYDj7LIep8PmCAKEkhtenE82zw==", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^14.20.0 || ^16.13.0 || >=18.10.0" + }, + "peerDependencies": { + "@angular/core": "15.2.10" + }, + "peerDependenciesMeta": { + "@angular/core": { + "optional": true + } + } + }, + "node_modules/@angular/compiler-cli": { + "version": "15.2.10", + "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-15.2.10.tgz", + "integrity": "sha512-mCFIxrs60XicKfA2o42hA7LrQvhybi9BQveWuZn/2iIEOXx7R62Iemz8E21pLWftAZHGxEW3NECfBrY1d3gVmA==", + "dev": true, + "dependencies": { + "@babel/core": "7.19.3", + "@jridgewell/sourcemap-codec": "^1.4.14", + "chokidar": "^3.0.0", + "convert-source-map": "^1.5.1", + "dependency-graph": "^0.11.0", + "magic-string": "^0.27.0", + "reflect-metadata": "^0.1.2", + "semver": "^7.0.0", + "tslib": "^2.3.0", + "yargs": "^17.2.1" + }, + "bin": { + "ng-xi18n": "bundles/src/bin/ng_xi18n.js", + "ngc": "bundles/src/bin/ngc.js", + "ngcc": "bundles/ngcc/main-ngcc.js" + }, + "engines": { + "node": "^14.20.0 || ^16.13.0 || >=18.10.0" + }, + "peerDependencies": { + "@angular/compiler": "15.2.10", + "typescript": ">=4.8.2 <5.0" + } + }, + "node_modules/@angular/compiler-cli/node_modules/@babel/core": { + "version": "7.19.3", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.19.3.tgz", + "integrity": "sha512-WneDJxdsjEvyKtXKsaBGbDeiyOjR5vYq4HcShxnIbG0qixpoHjI3MqeZM9NDvsojNCEBItQE4juOo/bU6e72gQ==", + "dev": true, + "dependencies": { + "@ampproject/remapping": "^2.1.0", + "@babel/code-frame": "^7.18.6", + "@babel/generator": "^7.19.3", + "@babel/helper-compilation-targets": "^7.19.3", + "@babel/helper-module-transforms": "^7.19.0", + "@babel/helpers": "^7.19.0", + "@babel/parser": "^7.19.3", + "@babel/template": "^7.18.10", + "@babel/traverse": "^7.19.3", + "@babel/types": "^7.19.3", + "convert-source-map": "^1.7.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.1", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@angular/compiler-cli/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, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@angular/compiler-cli/node_modules/magic-string": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.27.0.tgz", + "integrity": "sha512-8UnnX2PeRAPZuN12svgR9j7M1uWMovg/CEnIwIG0LFkXSJJe4PdfUGiTGl8V9bsBHFUtfVINcSyYxd7q+kx9fA==", + "dev": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.13" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@angular/core": { + "version": "15.2.10", + "resolved": "https://registry.npmjs.org/@angular/core/-/core-15.2.10.tgz", + "integrity": "sha512-meGGidnitQJGDxYd9/LrqYiVlId+vGaLoiLgJdKBz+o2ZO6OmXQGuNw2VBqf17/Cc0/UjzrOY7+kILNFKkk/WQ==", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^14.20.0 || ^16.13.0 || >=18.10.0" + }, + "peerDependencies": { + "rxjs": "^6.5.3 || ^7.4.0", + "zone.js": "~0.11.4 || ~0.12.0 || ~0.13.0" + } + }, + "node_modules/@angular/fire": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/@angular/fire/-/fire-7.5.0.tgz", + "integrity": "sha512-M/JJ5SKnpfNZFLKl0Awjn3r2NhURy2Mx+E8EzSDWSg1sHD7IThrynZYPUSZR+cd3KGsGoEacIP2x8Ra+0a+CVg==", + "dependencies": { + "@angular-devkit/schematics": "^12.0.0 || ^13.0.0 || ^14.0.0 || ^15.0.0", + "@schematics/angular": "^12.0.0 || ^13.0.0 || ^14.0.0 || ^15.0.0", + "file-loader": "^6.2.0", + "firebase": "^9.8.0", + "fs-extra": "^8.0.1", + "fuzzy": "^0.1.3", + "inquirer": "^8.1.1", + "inquirer-autocomplete-prompt": "^1.0.1", + "jsonc-parser": "^3.0.0", + "node-fetch": "^2.6.1", + "open": "^8.0.0", + "ora": "^5.3.0", + "rxfire": "^6.0.0", + "semver": "^7.1.3", + "triple-beam": "^1.3.0", + "tslib": "^2.0.0", + "winston": "^3.0.0" + }, + "peerDependencies": { + "@angular/common": "^12.0.0 || ^13.0.0 || ^14.0.0 || ^15.0.0", + "@angular/core": "^12.0.0 || ^13.0.0 || ^14.0.0 || ^15.0.0", + "@angular/platform-browser": "^12.0.0 || ^13.0.0 || ^14.0.0 || ^15.0.0", + "@angular/platform-browser-dynamic": "^12.0.0 || ^13.0.0 || ^14.0.0 || ^15.0.0", + "firebase-tools": "^9.9.0 || ^10.0.0 || ^11.0.0", + "rxjs": "~6.6.0 || ^7.0.0" + }, + "peerDependenciesMeta": { + "firebase-tools": { + "optional": true + } + } + }, + "node_modules/@angular/flex-layout": { + "version": "15.0.0-beta.42", + "resolved": "https://registry.npmjs.org/@angular/flex-layout/-/flex-layout-15.0.0-beta.42.tgz", + "integrity": "sha512-cTAPVMMxnyIFwpZwdq0PL5mdP9Qh+R8MB7ZBezVaN3Rz2fRrkagzKpLvPX3TFzepXrvHBdpKsU4b8u+NxEC/6g==", + "deprecated": "This package has been deprecated. Please see https://blog.angular.io/modern-css-in-angular-layouts-4a259dca9127", + "dependencies": { + "tslib": "^2.3.0" + }, + "peerDependencies": { + "@angular/cdk": ">=15.0.0", + "@angular/common": ">=15.0.2", + "@angular/core": ">=15.0.2", + "@angular/platform-browser": ">=15.0.2", + "rxjs": "^6.5.3 || ^7.4.0" + } + }, + "node_modules/@angular/forms": { + "version": "15.2.10", + "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-15.2.10.tgz", + "integrity": "sha512-NIntGsNcN6o8L1txsbWXOf6f3K/CUBizdKsxsYVYGJIXEW5qU6UnWmfAZffNNXsT/XvbgUCjgDwT0cAwcqZPuQ==", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^14.20.0 || ^16.13.0 || >=18.10.0" + }, + "peerDependencies": { + "@angular/common": "15.2.10", + "@angular/core": "15.2.10", + "@angular/platform-browser": "15.2.10", + "rxjs": "^6.5.3 || ^7.4.0" + } + }, + "node_modules/@angular/material": { + "version": "15.2.9", + "resolved": "https://registry.npmjs.org/@angular/material/-/material-15.2.9.tgz", + "integrity": "sha512-emuFF/7+91Jq+6kVCl3FiVoFLtAZoh+woFQWNuK8nhx0HmD4ckLFI8d9a6ERYR3zRuKhq5deSRE2kYsfpjrrsQ==", + "dependencies": { + "@material/animation": "15.0.0-canary.684e33d25.0", + "@material/auto-init": "15.0.0-canary.684e33d25.0", + "@material/banner": "15.0.0-canary.684e33d25.0", + "@material/base": "15.0.0-canary.684e33d25.0", + "@material/button": "15.0.0-canary.684e33d25.0", + "@material/card": "15.0.0-canary.684e33d25.0", + "@material/checkbox": "15.0.0-canary.684e33d25.0", + "@material/chips": "15.0.0-canary.684e33d25.0", + "@material/circular-progress": "15.0.0-canary.684e33d25.0", + "@material/data-table": "15.0.0-canary.684e33d25.0", + "@material/density": "15.0.0-canary.684e33d25.0", + "@material/dialog": "15.0.0-canary.684e33d25.0", + "@material/dom": "15.0.0-canary.684e33d25.0", + "@material/drawer": "15.0.0-canary.684e33d25.0", + "@material/elevation": "15.0.0-canary.684e33d25.0", + "@material/fab": "15.0.0-canary.684e33d25.0", + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "@material/floating-label": "15.0.0-canary.684e33d25.0", + "@material/form-field": "15.0.0-canary.684e33d25.0", + "@material/icon-button": "15.0.0-canary.684e33d25.0", + "@material/image-list": "15.0.0-canary.684e33d25.0", + "@material/layout-grid": "15.0.0-canary.684e33d25.0", + "@material/line-ripple": "15.0.0-canary.684e33d25.0", + "@material/linear-progress": "15.0.0-canary.684e33d25.0", + "@material/list": "15.0.0-canary.684e33d25.0", + "@material/menu": "15.0.0-canary.684e33d25.0", + "@material/menu-surface": "15.0.0-canary.684e33d25.0", + "@material/notched-outline": "15.0.0-canary.684e33d25.0", + "@material/radio": "15.0.0-canary.684e33d25.0", + "@material/ripple": "15.0.0-canary.684e33d25.0", + "@material/rtl": "15.0.0-canary.684e33d25.0", + "@material/segmented-button": "15.0.0-canary.684e33d25.0", + "@material/select": "15.0.0-canary.684e33d25.0", + "@material/shape": "15.0.0-canary.684e33d25.0", + "@material/slider": "15.0.0-canary.684e33d25.0", + "@material/snackbar": "15.0.0-canary.684e33d25.0", + "@material/switch": "15.0.0-canary.684e33d25.0", + "@material/tab": "15.0.0-canary.684e33d25.0", + "@material/tab-bar": "15.0.0-canary.684e33d25.0", + "@material/tab-indicator": "15.0.0-canary.684e33d25.0", + "@material/tab-scroller": "15.0.0-canary.684e33d25.0", + "@material/textfield": "15.0.0-canary.684e33d25.0", + "@material/theme": "15.0.0-canary.684e33d25.0", + "@material/tooltip": "15.0.0-canary.684e33d25.0", + "@material/top-app-bar": "15.0.0-canary.684e33d25.0", + "@material/touch-target": "15.0.0-canary.684e33d25.0", + "@material/typography": "15.0.0-canary.684e33d25.0", + "tslib": "^2.3.0" + }, + "peerDependencies": { + "@angular/animations": "^15.0.0 || ^16.0.0", + "@angular/cdk": "15.2.9", + "@angular/common": "^15.0.0 || ^16.0.0", + "@angular/core": "^15.0.0 || ^16.0.0", + "@angular/forms": "^15.0.0 || ^16.0.0", + "@angular/platform-browser": "^15.0.0 || ^16.0.0", + "rxjs": "^6.5.3 || ^7.4.0" + } + }, + "node_modules/@angular/platform-browser": { + "version": "15.2.10", + "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-15.2.10.tgz", + "integrity": "sha512-9tbgVGSJqwfrOzT8aA/kWBLNhJSQ9gUg0CJxwFBSJm8VkBUJrszoBlDsnSvlxx8/W2ejNULKHFTXeUzq0O/+RQ==", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^14.20.0 || ^16.13.0 || >=18.10.0" + }, + "peerDependencies": { + "@angular/animations": "15.2.10", + "@angular/common": "15.2.10", + "@angular/core": "15.2.10" + }, + "peerDependenciesMeta": { + "@angular/animations": { + "optional": true + } + } + }, + "node_modules/@angular/platform-browser-dynamic": { + "version": "15.2.10", + "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-15.2.10.tgz", + "integrity": "sha512-JHP6W+FX715Qv7DhqvfZLuBZXSDJrboiQsR06gUAgDSjAUyhbqmpVg/2YOtgeWpPkzNDtXdPU2PhcRdIv5J3Yg==", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^14.20.0 || ^16.13.0 || >=18.10.0" + }, + "peerDependencies": { + "@angular/common": "15.2.10", + "@angular/compiler": "15.2.10", + "@angular/core": "15.2.10", + "@angular/platform-browser": "15.2.10" + } + }, + "node_modules/@angular/router": { + "version": "15.2.10", + "resolved": "https://registry.npmjs.org/@angular/router/-/router-15.2.10.tgz", + "integrity": "sha512-LmuqEg0iIXSw7bli6HKJ19cbxP91v37GtRwbGKswyLihqzTgvjBYpvcfMnB5FRQ5LWkTwq5JclkX03dZw290Yg==", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^14.20.0 || ^16.13.0 || >=18.10.0" + }, + "peerDependencies": { + "@angular/common": "15.2.10", + "@angular/core": "15.2.10", + "@angular/platform-browser": "15.2.10", + "rxjs": "^6.5.3 || ^7.4.0" + } + }, + "node_modules/@assemblyscript/loader": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@assemblyscript/loader/-/loader-0.10.1.tgz", + "integrity": "sha512-H71nDOOL8Y7kWRLqf6Sums+01Q5msqBW2KhDUTemh1tvY04eSkSXrK0uj/4mmY0Xr16/3zyZmsrxN7CKuRbNRg==", + "dev": true + }, + "node_modules/@babel/code-frame": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.7.tgz", + "integrity": "sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA==", + "dev": true, + "dependencies": { + "@babel/highlight": "^7.24.7", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.24.7.tgz", + "integrity": "sha512-qJzAIcv03PyaWqxRgO4mSU3lihncDT296vnyuE2O8uA4w3UHWI4S3hgeZd1L8W1Bft40w9JxJ2b412iDUFFRhw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.20.12", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.20.12.tgz", + "integrity": "sha512-XsMfHovsUYHFMdrIHkZphTN/2Hzzi78R08NuHfDBehym2VsPDL6Zn/JAD/JQdnRvbSsbQc4mVaU1m6JgtTEElg==", + "dev": true, + "dependencies": { + "@ampproject/remapping": "^2.1.0", + "@babel/code-frame": "^7.18.6", + "@babel/generator": "^7.20.7", + "@babel/helper-compilation-targets": "^7.20.7", + "@babel/helper-module-transforms": "^7.20.11", + "@babel/helpers": "^7.20.7", + "@babel/parser": "^7.20.7", + "@babel/template": "^7.20.7", + "@babel/traverse": "^7.20.12", + "@babel/types": "^7.20.7", + "convert-source-map": "^1.7.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.2", + "semver": "^6.3.0" + }, + "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, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.20.14", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.20.14.tgz", + "integrity": "sha512-AEmuXHdcD3A52HHXxaTmYlb8q/xMEhoRP67B3T4Oq7lbmSoqroMZzjnGj3+i1io3pdnF8iBYVu4Ilj+c4hBxYg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.20.7", + "@jridgewell/gen-mapping": "^0.3.2", + "jsesc": "^2.5.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/generator/node_modules/@jridgewell/gen-mapping": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", + "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", + "dev": true, + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/helper-annotate-as-pure": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.18.6.tgz", + "integrity": "sha512-duORpUiYrEpzKIop6iNbjnwKLAKnJ47csTyRACyEmWj0QdUrm5aqNJGHSSEQSUAvNW0ojX0dOmK9dZduvkfeXA==", + "dev": true, + "dependencies": { + "@babel/types": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-builder-binary-assignment-operator-visitor": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.24.7.tgz", + "integrity": "sha512-xZeCVVdwb4MsDBkkyZ64tReWYrLRHlMN72vP7Bdm3OUOuyFZExhsHUUnuWnm2/XOlAJzR0LfPpB56WXZn0X/lA==", + "dev": true, + "dependencies": { + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.24.7.tgz", + "integrity": "sha512-ctSdRHBi20qWOfy27RUb4Fhp07KSJ3sXcuSvTrXrc4aG8NSYDo1ici3Vhg9bg69y5bj0Mr1lh0aeEgTvc12rMg==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.24.7", + "@babel/helper-validator-option": "^7.24.7", + "browserslist": "^4.22.2", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/browserslist": { + "version": "4.23.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.1.tgz", + "integrity": "sha512-TUfofFo/KsK/bWZ9TWQ5O26tsWW4Uhmt8IYklbnUa70udB6P2wA7w7o4PY4muaEPBQaAX+CEnmmIA41NVHtPVw==", + "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" + } + ], + "dependencies": { + "caniuse-lite": "^1.0.30001629", + "electron-to-chromium": "^1.4.796", + "node-releases": "^2.0.14", + "update-browserslist-db": "^1.0.16" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/@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, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-create-class-features-plugin": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.24.7.tgz", + "integrity": "sha512-kTkaDl7c9vO80zeX1rJxnuRpEsD5tA81yh11X1gQo+PhSti3JS+7qeZo9U4RHobKRiFPKaGK3svUAeb8D0Q7eg==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.24.7", + "@babel/helper-environment-visitor": "^7.24.7", + "@babel/helper-function-name": "^7.24.7", + "@babel/helper-member-expression-to-functions": "^7.24.7", + "@babel/helper-optimise-call-expression": "^7.24.7", + "@babel/helper-replace-supers": "^7.24.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.24.7", + "@babel/helper-split-export-declaration": "^7.24.7", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-create-class-features-plugin/node_modules/@babel/helper-annotate-as-pure": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.24.7.tgz", + "integrity": "sha512-BaDeOonYvhdKw+JoMVkAixAAJzG2jVPIwWoKBPdYuY9b452e2rPuI9QPYh3KpofZ3pW2akOmwZLOiOsHMiqRAg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-create-class-features-plugin/node_modules/@babel/helper-split-export-declaration": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.7.tgz", + "integrity": "sha512-oy5V7pD+UvfkEATUKvIjvIAH/xCzfsFVw7ygW2SI6NClZzquT+mwdTfgfdbUiceh6iQO0CHtCPsyze/MZ2YbAA==", + "dev": true, + "dependencies": { + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-create-class-features-plugin/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-create-regexp-features-plugin": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.24.7.tgz", + "integrity": "sha512-03TCmXy2FtXJEZfbXDTSqq1fRJArk7lX9DOFC/47VthYcxyIOx+eXQmdo6DOQvrbpIix+KfXwvuXdFDZHxt+rA==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.24.7", + "regexpu-core": "^5.3.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-create-regexp-features-plugin/node_modules/@babel/helper-annotate-as-pure": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.24.7.tgz", + "integrity": "sha512-BaDeOonYvhdKw+JoMVkAixAAJzG2jVPIwWoKBPdYuY9b452e2rPuI9QPYh3KpofZ3pW2akOmwZLOiOsHMiqRAg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-create-regexp-features-plugin/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-define-polyfill-provider": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.3.3.tgz", + "integrity": "sha512-z5aQKU4IzbqCC1XH0nAqfsFLMVSo22SBKUc0BxGrLkolTdPTructy0ToNnlO2zA4j9Q/7pjMZf0DSY+DSTYzww==", + "dev": true, + "dependencies": { + "@babel/helper-compilation-targets": "^7.17.7", + "@babel/helper-plugin-utils": "^7.16.7", + "debug": "^4.1.1", + "lodash.debounce": "^4.0.8", + "resolve": "^1.14.2", + "semver": "^6.1.2" + }, + "peerDependencies": { + "@babel/core": "^7.4.0-0" + } + }, + "node_modules/@babel/helper-define-polyfill-provider/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-environment-visitor": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.24.7.tgz", + "integrity": "sha512-DoiN84+4Gnd0ncbBOM9AZENV4a5ZiL39HYMyZJGZ/AZEykHYdJw0wW3kdcsh9/Kn+BRXHLkkklZ51ecPKmI1CQ==", + "dev": true, + "dependencies": { + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-function-name": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.24.7.tgz", + "integrity": "sha512-FyoJTsj/PEUWu1/TYRiXTIHc8lbw+TDYkZuoE43opPS5TrI7MyONBE1oNvfguEXAD9yhQRrVBnXdXzSLQl9XnA==", + "dev": true, + "dependencies": { + "@babel/template": "^7.24.7", + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-function-name/node_modules/@babel/template": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.24.7.tgz", + "integrity": "sha512-jYqfPrU9JTF0PmPy1tLYHW4Mp4KlgxJD9l2nP9fD6yT/ICi554DmrWBAEYpIelzjHf1msDP3PxJIRt/nFNfBig==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.24.7", + "@babel/parser": "^7.24.7", + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-hoist-variables": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.24.7.tgz", + "integrity": "sha512-MJJwhkoGy5c4ehfoRyrJ/owKeMl19U54h27YYftT0o2teQ3FJ3nQUf/I3LlJsX4l3qlw7WRXUmiyajvHXoTubQ==", + "dev": true, + "dependencies": { + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-member-expression-to-functions": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.24.7.tgz", + "integrity": "sha512-LGeMaf5JN4hAT471eJdBs/GK1DoYIJ5GCtZN/EsL6KUiiDZOvO/eKE11AMZJa2zP4zk4qe9V2O/hxAmkRc8p6w==", + "dev": true, + "dependencies": { + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.24.7.tgz", + "integrity": "sha512-8AyH3C+74cgCVVXow/myrynrAGv+nTVg5vKu2nZph9x7RcRwzmh0VFallJuFTZ9mx6u4eSdXZfcOzSqTUm0HCA==", + "dev": true, + "dependencies": { + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.24.7.tgz", + "integrity": "sha512-1fuJEwIrp+97rM4RWdO+qrRsZlAeL1lQJoPqtCYWv0NL115XM93hIH4CSRln2w52SqvmY5hqdtauB6QFCDiZNQ==", + "dev": true, + "dependencies": { + "@babel/helper-environment-visitor": "^7.24.7", + "@babel/helper-module-imports": "^7.24.7", + "@babel/helper-simple-access": "^7.24.7", + "@babel/helper-split-export-declaration": "^7.24.7", + "@babel/helper-validator-identifier": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-module-transforms/node_modules/@babel/helper-split-export-declaration": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.7.tgz", + "integrity": "sha512-oy5V7pD+UvfkEATUKvIjvIAH/xCzfsFVw7ygW2SI6NClZzquT+mwdTfgfdbUiceh6iQO0CHtCPsyze/MZ2YbAA==", + "dev": true, + "dependencies": { + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-optimise-call-expression": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.24.7.tgz", + "integrity": "sha512-jKiTsW2xmWwxT1ixIdfXUZp+P5yURx2suzLZr5Hi64rURpDYdMW0pv+Uf17EYk2Rd428Lx4tLsnjGJzYKDM/6A==", + "dev": true, + "dependencies": { + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.7.tgz", + "integrity": "sha512-Rq76wjt7yz9AAc1KnlRKNAi/dMSVWgDRx43FHoJEbcYU6xOWaE2dVPwcdTukJrjxS65GITyfbvEYHvkirZ6uEg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-remap-async-to-generator": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.24.7.tgz", + "integrity": "sha512-9pKLcTlZ92hNZMQfGCHImUpDOlAgkkpqalWEeftW5FBya75k8Li2ilerxkM/uBEj01iBZXcCIB/bwvDYgWyibA==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.24.7", + "@babel/helper-environment-visitor": "^7.24.7", + "@babel/helper-wrap-function": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-remap-async-to-generator/node_modules/@babel/helper-annotate-as-pure": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.24.7.tgz", + "integrity": "sha512-BaDeOonYvhdKw+JoMVkAixAAJzG2jVPIwWoKBPdYuY9b452e2rPuI9QPYh3KpofZ3pW2akOmwZLOiOsHMiqRAg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-replace-supers": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.24.7.tgz", + "integrity": "sha512-qTAxxBM81VEyoAY0TtLrx1oAEJc09ZK67Q9ljQToqCnA+55eNwCORaxlKyu+rNfX86o8OXRUSNUnrtsAZXM9sg==", + "dev": true, + "dependencies": { + "@babel/helper-environment-visitor": "^7.24.7", + "@babel/helper-member-expression-to-functions": "^7.24.7", + "@babel/helper-optimise-call-expression": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-simple-access": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.24.7.tgz", + "integrity": "sha512-zBAIvbCMh5Ts+b86r/CjU+4XGYIs+R1j951gxI3KmmxBMhCg4oQMsv6ZXQ64XOm/cvzfU1FmoCyt6+owc5QMYg==", + "dev": true, + "dependencies": { + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-skip-transparent-expression-wrappers": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.24.7.tgz", + "integrity": "sha512-IO+DLT3LQUElMbpzlatRASEyQtfhSE0+m465v++3jyyXeBTBUjtVZg28/gHeV5mrTJqvEKhKroBGAvhW+qPHiQ==", + "dev": true, + "dependencies": { + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-split-export-declaration": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.18.6.tgz", + "integrity": "sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA==", + "dev": true, + "dependencies": { + "@babel/types": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.7.tgz", + "integrity": "sha512-7MbVt6xrwFQbunH2DNQsAP5sTGxfqQtErvBIvIMi6EQnbgUOuVYanvREcmFrOPhoXBrTtjhhP+lW+o5UfK+tDg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz", + "integrity": "sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.24.7.tgz", + "integrity": "sha512-yy1/KvjhV/ZCL+SM7hBrvnZJ3ZuT9OuZgIJAGpPEToANvc3iM6iDvBnRjtElWibHU6n8/LPR/EjX9EtIEYO3pw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-wrap-function": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.24.7.tgz", + "integrity": "sha512-N9JIYk3TD+1vq/wn77YnJOqMtfWhNewNE+DJV4puD2X7Ew9J4JvrzrFDfTfyv5EgEXVy9/Wt8QiOErzEmv5Ifw==", + "dev": true, + "dependencies": { + "@babel/helper-function-name": "^7.24.7", + "@babel/template": "^7.24.7", + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-wrap-function/node_modules/@babel/template": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.24.7.tgz", + "integrity": "sha512-jYqfPrU9JTF0PmPy1tLYHW4Mp4KlgxJD9l2nP9fD6yT/ICi554DmrWBAEYpIelzjHf1msDP3PxJIRt/nFNfBig==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.24.7", + "@babel/parser": "^7.24.7", + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.24.7.tgz", + "integrity": "sha512-NlmJJtvcw72yRJRcnCmGvSi+3jDEg8qFu3z0AFoymmzLx5ERVWyzd9kVXr7Th9/8yIJi2Zc6av4Tqz3wFs8QWg==", + "dev": true, + "dependencies": { + "@babel/template": "^7.24.7", + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers/node_modules/@babel/template": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.24.7.tgz", + "integrity": "sha512-jYqfPrU9JTF0PmPy1tLYHW4Mp4KlgxJD9l2nP9fD6yT/ICi554DmrWBAEYpIelzjHf1msDP3PxJIRt/nFNfBig==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.24.7", + "@babel/parser": "^7.24.7", + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.7.tgz", + "integrity": "sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.24.7", + "chalk": "^2.4.2", + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.7.tgz", + "integrity": "sha512-9uUYRm6OqQrCqQdG1iCBwBPZgN8ciDBro2nIOFaiRz1/BCxaI7CNvQbDHvsArAC7Tw9Hda/B3U+6ui9u4HWXPw==", + "dev": true, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.24.7.tgz", + "integrity": "sha512-unaQgZ/iRu/By6tsjMZzpeBZjChYfLYry6HrEXPoz3KmfF0sVBQ1l8zKMQ4xRGLWVsjuvB8nQfjNP/DcfEOCsg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.24.7.tgz", + "integrity": "sha512-+izXIbke1T33mY4MSNnrqhPXDz01WYhEf3yF5NbnUtkiNnm+XBZJl3kNfoK6NKmYlz/D07+l2GWVK/QfDkNCuQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.24.7", + "@babel/plugin-transform-optional-chaining": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.13.0" + } + }, + "node_modules/@babel/plugin-proposal-async-generator-functions": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.20.7.tgz", + "integrity": "sha512-xMbiLsn/8RK7Wq7VeVytytS2L6qE69bXPB10YCmMdDZbKF4okCqY74pI/jJQ/8U0b/F6NrT2+14b8/P9/3AMGA==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-async-generator-functions instead.", + "dev": true, + "dependencies": { + "@babel/helper-environment-visitor": "^7.18.9", + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/helper-remap-async-to-generator": "^7.18.9", + "@babel/plugin-syntax-async-generators": "^7.8.4" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-class-properties": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.18.6.tgz", + "integrity": "sha512-cumfXOF0+nzZrrN8Rf0t7M+tF6sZc7vhQwYQck9q1/5w2OExlD+b4v4RpMJFaV1Z7WcDRgO6FqvxqxGlwo+RHQ==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-class-properties instead.", + "dev": true, + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-class-static-block": { + "version": "7.21.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-static-block/-/plugin-proposal-class-static-block-7.21.0.tgz", + "integrity": "sha512-XP5G9MWNUskFuP30IfFSEFB0Z6HzLIUcjYM4bYOPHXl7eiJ9HFv8tWj6TXTN5QODiEhDZAeI4hLok2iHFFV4hw==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-class-static-block instead.", + "dev": true, + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.21.0", + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/plugin-syntax-class-static-block": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.12.0" + } + }, + "node_modules/@babel/plugin-proposal-dynamic-import": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.18.6.tgz", + "integrity": "sha512-1auuwmK+Rz13SJj36R+jqFPMJWyKEDd7lLSdOj4oJK0UTgGueSAtkrCvz9ewmgyU/P941Rv2fQwZJN8s6QruXw==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-dynamic-import instead.", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6", + "@babel/plugin-syntax-dynamic-import": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-export-namespace-from": { + "version": "7.18.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-export-namespace-from/-/plugin-proposal-export-namespace-from-7.18.9.tgz", + "integrity": "sha512-k1NtHyOMvlDDFeb9G5PhUXuGj8m/wiwojgQVEhJ/fsVsMCpLyOP4h0uGEjYJKrRI+EVPlb5Jk+Gt9P97lOGwtA==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-export-namespace-from instead.", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.9", + "@babel/plugin-syntax-export-namespace-from": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-json-strings": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.18.6.tgz", + "integrity": "sha512-lr1peyn9kOdbYc0xr0OdHTZ5FMqS6Di+H0Fz2I/JwMzGmzJETNeOFq2pBySw6X/KFL5EWDjlJuMsUGRFb8fQgQ==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-json-strings instead.", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6", + "@babel/plugin-syntax-json-strings": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-logical-assignment-operators": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-logical-assignment-operators/-/plugin-proposal-logical-assignment-operators-7.20.7.tgz", + "integrity": "sha512-y7C7cZgpMIjWlKE5T7eJwp+tnRYM89HmRvWM5EQuB5BoHEONjmQ8lSNmBUwOyy/GFRsohJED51YBF79hE1djug==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-logical-assignment-operators instead.", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-nullish-coalescing-operator": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.18.6.tgz", + "integrity": "sha512-wQxQzxYeJqHcfppzBDnm1yAY0jSRkUXR2z8RePZYrKwMKgMlE8+Z6LUno+bd6LvbGh8Gltvy74+9pIYkr+XkKA==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-nullish-coalescing-operator instead.", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-numeric-separator": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.18.6.tgz", + "integrity": "sha512-ozlZFogPqoLm8WBr5Z8UckIoE4YQ5KESVcNudyXOR8uqIkliTEgJ3RoketfG6pmzLdeZF0H/wjE9/cCEitBl7Q==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-numeric-separator instead.", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6", + "@babel/plugin-syntax-numeric-separator": "^7.10.4" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-object-rest-spread": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.20.7.tgz", + "integrity": "sha512-d2S98yCiLxDVmBmE8UjGcfPvNEUbA1U5q5WxaWFUGRzJSVAZqm5W6MbPct0jxnegUZ0niLeNX+IOzEs7wYg9Dg==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-object-rest-spread instead.", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.20.5", + "@babel/helper-compilation-targets": "^7.20.7", + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-transform-parameters": "^7.20.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-optional-catch-binding": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.18.6.tgz", + "integrity": "sha512-Q40HEhs9DJQyaZfUjjn6vE8Cv4GmMHCYuMGIWUnlxH6400VGxOuwWsPt4FxXxJkC/5eOzgn0z21M9gMT4MOhbw==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-optional-catch-binding instead.", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-optional-chaining": { + "version": "7.21.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.21.0.tgz", + "integrity": "sha512-p4zeefM72gpmEe2fkUr/OnOXpWEf8nAgk7ZYVqqfFiyIG7oFfVZcCrU64hWn5xp4tQ9LkV4bTIa5rD0KANpKNA==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-optional-chaining instead.", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/helper-skip-transparent-expression-wrappers": "^7.20.0", + "@babel/plugin-syntax-optional-chaining": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-private-methods": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-methods/-/plugin-proposal-private-methods-7.18.6.tgz", + "integrity": "sha512-nutsvktDItsNn4rpGItSNV2sz1XwS+nfU0Rg8aCx3W3NOKVzdMjJRu0O5OkgDp3ZGICSTbgRpxZoWsxoKRvbeA==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-private-methods instead.", + "dev": true, + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-private-property-in-object": { + "version": "7.21.11", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.11.tgz", + "integrity": "sha512-0QZ8qP/3RLDVBwBFoWAwCtgcDZJVwA5LUJRZU8x2YFfKNuFq161wK3cuGrALu5yiPu+vzwTAg/sMWVNeWeNyaw==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-private-property-in-object instead.", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.18.6", + "@babel/helper-create-class-features-plugin": "^7.21.0", + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-unicode-property-regex": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.18.6.tgz", + "integrity": "sha512-2BShG/d5yoZyXZfVePH91urL5wTG6ASZU9M4o03lKK8u8UW1y08OMttBSOADTcJrnPMpvDXRG3G8fyLh4ovs8w==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-unicode-property-regex instead.", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-dynamic-import": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz", + "integrity": "sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-export-namespace-from": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz", + "integrity": "sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.3" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-assertions": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.24.7.tgz", + "integrity": "sha512-Ec3NRUMoi8gskrkBe3fNmEQfxDvY8bgfQpz6jlk/41kX9eUjvpyqWU7PBP/pLAvMaSQjbMNKJmvX57jP+M6bPg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-arrow-functions": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.24.7.tgz", + "integrity": "sha512-Dt9LQs6iEY++gXUwY03DNFat5C2NbO48jj+j/bSAz6b3HgPs39qcPiYt77fDObIcFwj3/C2ICX9YMwGflUoSHQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-async-to-generator": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.20.7.tgz", + "integrity": "sha512-Uo5gwHPT9vgnSXQxqGtpdufUiWp96gk7yiP4Mp5bm1QMkEmLXBO7PAGYbKoJ6DhAwiNkcHFBol/x5zZZkL/t0Q==", + "dev": true, + "dependencies": { + "@babel/helper-module-imports": "^7.18.6", + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/helper-remap-async-to-generator": "^7.18.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-block-scoped-functions": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.24.7.tgz", + "integrity": "sha512-yO7RAz6EsVQDaBH18IDJcMB1HnrUn2FJ/Jslc/WtPPWcjhpUJXU/rjbwmluzp7v/ZzWcEhTMXELnnsz8djWDwQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-block-scoping": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.24.7.tgz", + "integrity": "sha512-Nd5CvgMbWc+oWzBsuaMcbwjJWAcp5qzrbg69SZdHSP7AMY0AbWFqFO0WTFCA1jxhMCwodRwvRec8k0QUbZk7RQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-classes": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.24.7.tgz", + "integrity": "sha512-CFbbBigp8ln4FU6Bpy6g7sE8B/WmCmzvivzUC6xDAdWVsjYTXijpuuGJmYkAaoWAzcItGKT3IOAbxRItZ5HTjw==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.24.7", + "@babel/helper-compilation-targets": "^7.24.7", + "@babel/helper-environment-visitor": "^7.24.7", + "@babel/helper-function-name": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-replace-supers": "^7.24.7", + "@babel/helper-split-export-declaration": "^7.24.7", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-classes/node_modules/@babel/helper-annotate-as-pure": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.24.7.tgz", + "integrity": "sha512-BaDeOonYvhdKw+JoMVkAixAAJzG2jVPIwWoKBPdYuY9b452e2rPuI9QPYh3KpofZ3pW2akOmwZLOiOsHMiqRAg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/plugin-transform-classes/node_modules/@babel/helper-split-export-declaration": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.7.tgz", + "integrity": "sha512-oy5V7pD+UvfkEATUKvIjvIAH/xCzfsFVw7ygW2SI6NClZzquT+mwdTfgfdbUiceh6iQO0CHtCPsyze/MZ2YbAA==", + "dev": true, + "dependencies": { + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/plugin-transform-computed-properties": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.24.7.tgz", + "integrity": "sha512-25cS7v+707Gu6Ds2oY6tCkUwsJ9YIDbggd9+cu9jzzDgiNq7hR/8dkzxWfKWnTic26vsI3EsCXNd4iEB6e8esQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/template": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-computed-properties/node_modules/@babel/template": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.24.7.tgz", + "integrity": "sha512-jYqfPrU9JTF0PmPy1tLYHW4Mp4KlgxJD9l2nP9fD6yT/ICi554DmrWBAEYpIelzjHf1msDP3PxJIRt/nFNfBig==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.24.7", + "@babel/parser": "^7.24.7", + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/plugin-transform-destructuring": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.24.7.tgz", + "integrity": "sha512-19eJO/8kdCQ9zISOf+SEUJM/bAUIsvY3YDnXZTupUCQ8LgrWnsG/gFB9dvXqdXnRXMAM8fvt7b0CBKQHNGy1mw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-dotall-regex": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.24.7.tgz", + "integrity": "sha512-ZOA3W+1RRTSWvyqcMJDLqbchh7U4NRGqwRfFSVbOLS/ePIP4vHB5e8T8eXcuqyN1QkgKyj5wuW0lcS85v4CrSw==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-duplicate-keys": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.24.7.tgz", + "integrity": "sha512-JdYfXyCRihAe46jUIliuL2/s0x0wObgwwiGxw/UbgJBr20gQBThrokO4nYKgWkD7uBaqM7+9x5TU7NkExZJyzw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-exponentiation-operator": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.24.7.tgz", + "integrity": "sha512-Rqe/vSc9OYgDajNIK35u7ot+KeCoetqQYFXM4Epf7M7ez3lWlOjrDjrwMei6caCVhfdw+mIKD4cgdGNy5JQotQ==", + "dev": true, + "dependencies": { + "@babel/helper-builder-binary-assignment-operator-visitor": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-for-of": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.24.7.tgz", + "integrity": "sha512-wo9ogrDG1ITTTBsy46oGiN1dS9A7MROBTcYsfS8DtsImMkHk9JXJ3EWQM6X2SUw4x80uGPlwj0o00Uoc6nEE3g==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-function-name": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.24.7.tgz", + "integrity": "sha512-U9FcnA821YoILngSmYkW6FjyQe2TyZD5pHt4EVIhmcTkrJw/3KqcrRSxuOo5tFZJi7TE19iDyI1u+weTI7bn2w==", + "dev": true, + "dependencies": { + "@babel/helper-compilation-targets": "^7.24.7", + "@babel/helper-function-name": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-literals": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.24.7.tgz", + "integrity": "sha512-vcwCbb4HDH+hWi8Pqenwnjy+UiklO4Kt1vfspcQYFhJdpthSnW8XvWGyDZWKNVrVbVViI/S7K9PDJZiUmP2fYQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-member-expression-literals": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.24.7.tgz", + "integrity": "sha512-T/hRC1uqrzXMKLQ6UCwMT85S3EvqaBXDGf0FaMf4446Qx9vKwlghvee0+uuZcDUCZU5RuNi4781UQ7R308zzBw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-amd": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.24.7.tgz", + "integrity": "sha512-9+pB1qxV3vs/8Hdmz/CulFB8w2tuu6EB94JZFsjdqxQokwGa9Unap7Bo2gGBGIvPmDIVvQrom7r5m/TCDMURhg==", + "dev": true, + "dependencies": { + "@babel/helper-module-transforms": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-commonjs": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.24.7.tgz", + "integrity": "sha512-iFI8GDxtevHJ/Z22J5xQpVqFLlMNstcLXh994xifFwxxGslr2ZXXLWgtBeLctOD63UFDArdvN6Tg8RFw+aEmjQ==", + "dev": true, + "dependencies": { + "@babel/helper-module-transforms": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-simple-access": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-systemjs": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.24.7.tgz", + "integrity": "sha512-GYQE0tW7YoaN13qFh3O1NCY4MPkUiAH3fiF7UcV/I3ajmDKEdG3l+UOcbAm4zUE3gnvUU+Eni7XrVKo9eO9auw==", + "dev": true, + "dependencies": { + "@babel/helper-hoist-variables": "^7.24.7", + "@babel/helper-module-transforms": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-validator-identifier": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-umd": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.24.7.tgz", + "integrity": "sha512-3aytQvqJ/h9z4g8AsKPLvD4Zqi2qT+L3j7XoFFu1XBlZWEl2/1kWnhmAbxpLgPrHSY0M6UA02jyTiwUVtiKR6A==", + "dev": true, + "dependencies": { + "@babel/helper-module-transforms": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-named-capturing-groups-regex": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.24.7.tgz", + "integrity": "sha512-/jr7h/EWeJtk1U/uz2jlsCioHkZk1JJZVcc8oQsJ1dUlaJD83f4/6Zeh2aHt9BIFokHIsSeDfhUmju0+1GPd6g==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-new-target": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.24.7.tgz", + "integrity": "sha512-RNKwfRIXg4Ls/8mMTza5oPF5RkOW8Wy/WgMAp1/F1yZ8mMbtwXW+HDoJiOsagWrAhI5f57Vncrmr9XeT4CVapA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-object-super": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.24.7.tgz", + "integrity": "sha512-A/vVLwN6lBrMFmMDmPPz0jnE6ZGx7Jq7d6sT/Ev4H65RER6pZ+kczlf1DthF5N0qaPHBsI7UXiE8Zy66nmAovg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-replace-supers": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-optional-chaining": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.24.7.tgz", + "integrity": "sha512-tK+0N9yd4j+x/4hxF3F0e0fu/VdcxU18y5SevtyM/PCFlQvXbR0Zmlo2eBrKtVipGNFzpq56o8WsIIKcJFUCRQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.24.7", + "@babel/plugin-syntax-optional-chaining": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-parameters": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.24.7.tgz", + "integrity": "sha512-yGWW5Rr+sQOhK0Ot8hjDJuxU3XLRQGflvT4lhlSY0DFvdb3TwKaY26CJzHtYllU0vT9j58hc37ndFPsqT1SrzA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-property-literals": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.24.7.tgz", + "integrity": "sha512-EMi4MLQSHfd2nrCqQEWxFdha2gBCqU4ZcCng4WBGZ5CJL4bBRW0ptdqqDdeirGZcpALazVVNJqRmsO8/+oNCBA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-regenerator": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.24.7.tgz", + "integrity": "sha512-lq3fvXPdimDrlg6LWBoqj+r/DEWgONuwjuOuQCSYgRroXDH/IdM1C0IZf59fL5cHLpjEH/O6opIRBbqv7ELnuA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7", + "regenerator-transform": "^0.15.2" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-reserved-words": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.24.7.tgz", + "integrity": "sha512-0DUq0pHcPKbjFZCfTss/pGkYMfy3vFWydkUBd9r0GHpIyfs2eCDENvqadMycRS9wZCXR41wucAfJHJmwA0UmoQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-runtime": { + "version": "7.19.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.19.6.tgz", + "integrity": "sha512-PRH37lz4JU156lYFW1p8OxE5i7d6Sl/zV58ooyr+q1J1lnQPyg5tIiXlIwNVhJaY4W3TmOtdc8jqdXQcB1v5Yw==", + "dev": true, + "dependencies": { + "@babel/helper-module-imports": "^7.18.6", + "@babel/helper-plugin-utils": "^7.19.0", + "babel-plugin-polyfill-corejs2": "^0.3.3", + "babel-plugin-polyfill-corejs3": "^0.6.0", + "babel-plugin-polyfill-regenerator": "^0.4.1", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-runtime/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/plugin-transform-shorthand-properties": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.24.7.tgz", + "integrity": "sha512-KsDsevZMDsigzbA09+vacnLpmPH4aWjcZjXdyFKGzpplxhbeB4wYtury3vglQkg6KM/xEPKt73eCjPPf1PgXBA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-spread": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.24.7.tgz", + "integrity": "sha512-x96oO0I09dgMDxJaANcRyD4ellXFLLiWhuwDxKZX5g2rWP1bTPkBSwCYv96VDXVT1bD9aPj8tppr5ITIh8hBng==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-sticky-regex": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.24.7.tgz", + "integrity": "sha512-kHPSIJc9v24zEml5geKg9Mjx5ULpfncj0wRpYtxbvKyTtHCYDkVE3aHQ03FrpEo4gEe2vrJJS1Y9CJTaThA52g==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-template-literals": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.24.7.tgz", + "integrity": "sha512-AfDTQmClklHCOLxtGoP7HkeMw56k1/bTQjwsfhL6pppo/M4TOBSq+jjBUBLmV/4oeFg4GWMavIl44ZeCtmmZTw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-typeof-symbol": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.24.7.tgz", + "integrity": "sha512-VtR8hDy7YLB7+Pet9IarXjg/zgCMSF+1mNS/EQEiEaUPoFXCVsHG64SIxcaaI2zJgRiv+YmgaQESUfWAdbjzgg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-escapes": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.24.7.tgz", + "integrity": "sha512-U3ap1gm5+4edc2Q/P+9VrBNhGkfnf+8ZqppY71Bo/pzZmXhhLdqgaUl6cuB07O1+AQJtCLfaOmswiNbSQ9ivhw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-regex": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.24.7.tgz", + "integrity": "sha512-hlQ96MBZSAXUq7ltkjtu3FJCCSMx/j629ns3hA3pXnBXjanNP0LHi+JpPeA81zaWgVK1VGH95Xuy7u0RyQ8kMg==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-env": { + "version": "7.20.2", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.20.2.tgz", + "integrity": "sha512-1G0efQEWR1EHkKvKHqbG+IN/QdgwfByUpM5V5QroDzGV2t3S/WXNQd693cHiHTlCFMpr9B6FkPFXDA2lQcKoDg==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.20.1", + "@babel/helper-compilation-targets": "^7.20.0", + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/helper-validator-option": "^7.18.6", + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.18.6", + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.18.9", + "@babel/plugin-proposal-async-generator-functions": "^7.20.1", + "@babel/plugin-proposal-class-properties": "^7.18.6", + "@babel/plugin-proposal-class-static-block": "^7.18.6", + "@babel/plugin-proposal-dynamic-import": "^7.18.6", + "@babel/plugin-proposal-export-namespace-from": "^7.18.9", + "@babel/plugin-proposal-json-strings": "^7.18.6", + "@babel/plugin-proposal-logical-assignment-operators": "^7.18.9", + "@babel/plugin-proposal-nullish-coalescing-operator": "^7.18.6", + "@babel/plugin-proposal-numeric-separator": "^7.18.6", + "@babel/plugin-proposal-object-rest-spread": "^7.20.2", + "@babel/plugin-proposal-optional-catch-binding": "^7.18.6", + "@babel/plugin-proposal-optional-chaining": "^7.18.9", + "@babel/plugin-proposal-private-methods": "^7.18.6", + "@babel/plugin-proposal-private-property-in-object": "^7.18.6", + "@babel/plugin-proposal-unicode-property-regex": "^7.18.6", + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-dynamic-import": "^7.8.3", + "@babel/plugin-syntax-export-namespace-from": "^7.8.3", + "@babel/plugin-syntax-import-assertions": "^7.20.0", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5", + "@babel/plugin-transform-arrow-functions": "^7.18.6", + "@babel/plugin-transform-async-to-generator": "^7.18.6", + "@babel/plugin-transform-block-scoped-functions": "^7.18.6", + "@babel/plugin-transform-block-scoping": "^7.20.2", + "@babel/plugin-transform-classes": "^7.20.2", + "@babel/plugin-transform-computed-properties": "^7.18.9", + "@babel/plugin-transform-destructuring": "^7.20.2", + "@babel/plugin-transform-dotall-regex": "^7.18.6", + "@babel/plugin-transform-duplicate-keys": "^7.18.9", + "@babel/plugin-transform-exponentiation-operator": "^7.18.6", + "@babel/plugin-transform-for-of": "^7.18.8", + "@babel/plugin-transform-function-name": "^7.18.9", + "@babel/plugin-transform-literals": "^7.18.9", + "@babel/plugin-transform-member-expression-literals": "^7.18.6", + "@babel/plugin-transform-modules-amd": "^7.19.6", + "@babel/plugin-transform-modules-commonjs": "^7.19.6", + "@babel/plugin-transform-modules-systemjs": "^7.19.6", + "@babel/plugin-transform-modules-umd": "^7.18.6", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.19.1", + "@babel/plugin-transform-new-target": "^7.18.6", + "@babel/plugin-transform-object-super": "^7.18.6", + "@babel/plugin-transform-parameters": "^7.20.1", + "@babel/plugin-transform-property-literals": "^7.18.6", + "@babel/plugin-transform-regenerator": "^7.18.6", + "@babel/plugin-transform-reserved-words": "^7.18.6", + "@babel/plugin-transform-shorthand-properties": "^7.18.6", + "@babel/plugin-transform-spread": "^7.19.0", + "@babel/plugin-transform-sticky-regex": "^7.18.6", + "@babel/plugin-transform-template-literals": "^7.18.9", + "@babel/plugin-transform-typeof-symbol": "^7.18.9", + "@babel/plugin-transform-unicode-escapes": "^7.18.10", + "@babel/plugin-transform-unicode-regex": "^7.18.6", + "@babel/preset-modules": "^0.1.5", + "@babel/types": "^7.20.2", + "babel-plugin-polyfill-corejs2": "^0.3.3", + "babel-plugin-polyfill-corejs3": "^0.6.0", + "babel-plugin-polyfill-regenerator": "^0.4.1", + "core-js-compat": "^3.25.1", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-env/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/preset-modules": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.6.tgz", + "integrity": "sha512-ID2yj6K/4lKfhuU3+EX4UvNbIt7eACFbHmNUjzA+ep+B5971CknnA/9DEWKbRokfbbtblxxxXFJJrH47UEAMVg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/plugin-proposal-unicode-property-regex": "^7.4.4", + "@babel/plugin-transform-dotall-regex": "^7.4.4", + "@babel/types": "^7.4.4", + "esutils": "^2.0.2" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/@babel/regjsgen": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@babel/regjsgen/-/regjsgen-0.8.0.tgz", + "integrity": "sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA==", + "dev": true + }, + "node_modules/@babel/runtime": { + "version": "7.20.13", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.20.13.tgz", + "integrity": "sha512-gt3PKXs0DBoL9xCvOIIZ2NEqAGZqHjAnmVbfQtB620V0uReIQutpel14KcneZuer7UioY8ALKZ7iocavvzTNFA==", + "dev": true, + "dependencies": { + "regenerator-runtime": "^0.13.11" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.20.7.tgz", + "integrity": "sha512-8SegXApWe6VoNw0r9JHpSteLKTpTiLZ4rMlGIm9JQ18KiCtyQiAMEazujAHrUS5flrcqYZa75ukev3P6QmUwUw==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.18.6", + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.24.7.tgz", + "integrity": "sha512-yb65Ed5S/QAcewNPh0nZczy9JdYXkkAbIsEo+P7BE7yO3txAY30Y/oPa3QkQ5It3xVG2kpKMg9MsdxZaO31uKA==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.24.7", + "@babel/generator": "^7.24.7", + "@babel/helper-environment-visitor": "^7.24.7", + "@babel/helper-function-name": "^7.24.7", + "@babel/helper-hoist-variables": "^7.24.7", + "@babel/helper-split-export-declaration": "^7.24.7", + "@babel/parser": "^7.24.7", + "@babel/types": "^7.24.7", + "debug": "^4.3.1", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse/node_modules/@babel/generator": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.24.7.tgz", + "integrity": "sha512-oipXieGC3i45Y1A41t4tAqpnEZWgB/lC6Ehh6+rOviR5XWpTtMmLN+fGjz9vOiNRt0p6RtO6DtD0pdU3vpqdSA==", + "dev": true, + "dependencies": { + "@babel/types": "^7.24.7", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", + "jsesc": "^2.5.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse/node_modules/@babel/helper-split-export-declaration": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.7.tgz", + "integrity": "sha512-oy5V7pD+UvfkEATUKvIjvIAH/xCzfsFVw7ygW2SI6NClZzquT+mwdTfgfdbUiceh6iQO0CHtCPsyze/MZ2YbAA==", + "dev": true, + "dependencies": { + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse/node_modules/@jridgewell/gen-mapping": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", + "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", + "dev": true, + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.7.tgz", + "integrity": "sha512-XEFXSlxiG5td2EJRe8vOmRbaXVgfcBlszKujvVmWIK/UpywWljQCfzAv3RQCGujWQ1RD4YYWEAqDXfuJiy8f5Q==", + "dev": true, + "dependencies": { + "@babel/helper-string-parser": "^7.24.7", + "@babel/helper-validator-identifier": "^7.24.7", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@braintree/sanitize-url": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/@braintree/sanitize-url/-/sanitize-url-6.0.4.tgz", + "integrity": "sha512-s3jaWicZd0pkP0jf5ysyHUI/RE7MHos6qlToFcGWXVp+ykHOy77OUMrfbgJ9it2C5bow7OIQwYYaHjk9XlBQ2A==", + "optional": true + }, + "node_modules/@colors/colors": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", + "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==", + "dev": true, + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/@dabh/diagnostics": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.3.tgz", + "integrity": "sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==", + "dependencies": { + "colorspace": "1.1.x", + "enabled": "2.0.x", + "kuler": "^2.0.0" + } + }, + "node_modules/@discoveryjs/json-ext": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz", + "integrity": "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==", + "dev": true, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.17.8", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.17.8.tgz", + "integrity": "sha512-0/rb91GYKhrtbeglJXOhAv9RuYimgI8h623TplY2X+vA4EXnk3Zj1fXZreJ0J3OJJu1bwmb0W7g+2cT/d8/l/w==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.17.8", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.17.8.tgz", + "integrity": "sha512-oa/N5j6v1svZQs7EIRPqR8f+Bf8g6HBDjD/xHC02radE/NjKHK7oQmtmLxPs1iVwYyvE+Kolo6lbpfEQ9xnhxQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.17.8", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.17.8.tgz", + "integrity": "sha512-bTliMLqD7pTOoPg4zZkXqCDuzIUguEWLpeqkNfC41ODBHwoUgZ2w5JBeYimv4oP6TDVocoYmEhZrCLQTrH89bg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.17.8", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.17.8.tgz", + "integrity": "sha512-ghAbV3ia2zybEefXRRm7+lx8J/rnupZT0gp9CaGy/3iolEXkJ6LYRq4IpQVI9zR97ID80KJVoUlo3LSeA/sMAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.17.8", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.17.8.tgz", + "integrity": "sha512-n5WOpyvZ9TIdv2V1K3/iIkkJeKmUpKaCTdun9buhGRWfH//osmUjlv4Z5mmWdPWind/VGcVxTHtLfLCOohsOXw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.17.8", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.17.8.tgz", + "integrity": "sha512-a/SATTaOhPIPFWvHZDoZYgxaZRVHn0/LX1fHLGfZ6C13JqFUZ3K6SMD6/HCtwOQ8HnsNaEeokdiDSFLuizqv5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.17.8", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.17.8.tgz", + "integrity": "sha512-xpFJb08dfXr5+rZc4E+ooZmayBW6R3q59daCpKZ/cDU96/kvDM+vkYzNeTJCGd8rtO6fHWMq5Rcv/1cY6p6/0Q==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.17.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.17.8.tgz", + "integrity": "sha512-6Ij8gfuGszcEwZpi5jQIJCVIACLS8Tz2chnEBfYjlmMzVsfqBP1iGmHQPp7JSnZg5xxK9tjCc+pJ2WtAmPRFVA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.17.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.17.8.tgz", + "integrity": "sha512-v3iwDQuDljLTxpsqQDl3fl/yihjPAyOguxuloON9kFHYwopeJEf1BkDXODzYyXEI19gisEsQlG1bM65YqKSIww==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.17.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.17.8.tgz", + "integrity": "sha512-8svILYKhE5XetuFk/B6raFYIyIqydQi+GngEXJgdPdI7OMKUbSd7uzR02wSY4kb53xBrClLkhH4Xs8P61Q2BaA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.17.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.17.8.tgz", + "integrity": "sha512-B6FyMeRJeV0NpyEOYlm5qtQfxbdlgmiGdD+QsipzKfFky0K5HW5Td6dyK3L3ypu1eY4kOmo7wW0o94SBqlqBSA==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.17.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.17.8.tgz", + "integrity": "sha512-CCb67RKahNobjm/eeEqeD/oJfJlrWyw29fgiyB6vcgyq97YAf3gCOuP6qMShYSPXgnlZe/i4a8WFHBw6N8bYAA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.17.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.17.8.tgz", + "integrity": "sha512-bytLJOi55y55+mGSdgwZ5qBm0K9WOCh0rx+vavVPx+gqLLhxtSFU0XbeYy/dsAAD6xECGEv4IQeFILaSS2auXw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.17.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.17.8.tgz", + "integrity": "sha512-2YpRyQJmKVBEHSBLa8kBAtbhucaclb6ex4wchfY0Tj3Kg39kpjeJ9vhRU7x4mUpq8ISLXRXH1L0dBYjAeqzZAw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.17.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.17.8.tgz", + "integrity": "sha512-QgbNY/V3IFXvNf11SS6exkpVcX0LJcob+0RWCgV9OiDAmVElnxciHIisoSix9uzYzScPmS6dJFbZULdSAEkQVw==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.17.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.17.8.tgz", + "integrity": "sha512-mM/9S0SbAFDBc4OPoyP6SEOo5324LpUxdpeIUUSrSTOfhHU9hEfqRngmKgqILqwx/0DVJBzeNW7HmLEWp9vcOA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.17.8", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.17.8.tgz", + "integrity": "sha512-eKUYcWaWTaYr9zbj8GertdVtlt1DTS1gNBWov+iQfWuWyuu59YN6gSEJvFzC5ESJ4kMcKR0uqWThKUn5o8We6Q==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.17.8", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.17.8.tgz", + "integrity": "sha512-Vc9J4dXOboDyMXKD0eCeW0SIeEzr8K9oTHJU+Ci1mZc5njPfhKAqkRt3B/fUNU7dP+mRyralPu8QUkiaQn7iIg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.17.8", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.17.8.tgz", + "integrity": "sha512-0xvOTNuPXI7ft1LYUgiaXtpCEjp90RuBBYovdd2lqAFxje4sEucurg30M1WIm03+3jxByd3mfo+VUmPtRSVuOw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.17.8", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.17.8.tgz", + "integrity": "sha512-G0JQwUI5WdEFEnYNKzklxtBheCPkuDdu1YrtRrjuQv30WsYbkkoixKxLLv8qhJmNI+ATEWquZe/N0d0rpr55Mg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.17.8", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.17.8.tgz", + "integrity": "sha512-Fqy63515xl20OHGFykjJsMnoIWS+38fqfg88ClvPXyDbLtgXal2DTlhb1TfTX34qWi3u4I7Cq563QcHpqgLx8w==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.17.8", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.17.8.tgz", + "integrity": "sha512-1iuezdyDNngPnz8rLRDO2C/ZZ/emJLb72OsZeqQ6gL6Avko/XCXZw+NuxBSNhBAP13Hie418V7VMt9et1FMvpg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz", + "integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", + "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", + "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/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "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/@eslint/eslintrc/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/@eslint/eslintrc/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/eslintrc/node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/@eslint/eslintrc/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/@eslint/eslintrc/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/eslintrc/node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz", + "integrity": "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@firebase/analytics": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@firebase/analytics/-/analytics-0.10.0.tgz", + "integrity": "sha512-Locv8gAqx0e+GX/0SI3dzmBY5e9kjVDtD+3zCFLJ0tH2hJwuCAiL+5WkHuxKj92rqQj/rvkBUCfA1ewlX2hehg==", + "dependencies": { + "@firebase/component": "0.6.4", + "@firebase/installations": "0.6.4", + "@firebase/logger": "0.4.0", + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/analytics-compat": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/@firebase/analytics-compat/-/analytics-compat-0.2.6.tgz", + "integrity": "sha512-4MqpVLFkGK7NJf/5wPEEP7ePBJatwYpyjgJ+wQHQGHfzaCDgntOnl9rL2vbVGGKCnRqWtZDIWhctB86UWXaX2Q==", + "dependencies": { + "@firebase/analytics": "0.10.0", + "@firebase/analytics-types": "0.8.0", + "@firebase/component": "0.6.4", + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/analytics-types": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@firebase/analytics-types/-/analytics-types-0.8.0.tgz", + "integrity": "sha512-iRP+QKI2+oz3UAh4nPEq14CsEjrjD6a5+fuypjScisAh9kXKFvdJOZJDwk7kikLvWVLGEs9+kIUS4LPQV7VZVw==" + }, + "node_modules/@firebase/app": { + "version": "0.9.13", + "resolved": "https://registry.npmjs.org/@firebase/app/-/app-0.9.13.tgz", + "integrity": "sha512-GfiI1JxJ7ecluEmDjPzseRXk/PX31hS7+tjgBopL7XjB2hLUdR+0FTMXy2Q3/hXezypDvU6or7gVFizDESrkXw==", + "dependencies": { + "@firebase/component": "0.6.4", + "@firebase/logger": "0.4.0", + "@firebase/util": "1.9.3", + "idb": "7.1.1", + "tslib": "^2.1.0" + } + }, + "node_modules/@firebase/app-check": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@firebase/app-check/-/app-check-0.8.0.tgz", + "integrity": "sha512-dRDnhkcaC2FspMiRK/Vbp+PfsOAEP6ZElGm9iGFJ9fDqHoPs0HOPn7dwpJ51lCFi1+2/7n5pRPGhqF/F03I97g==", + "dependencies": { + "@firebase/component": "0.6.4", + "@firebase/logger": "0.4.0", + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/app-check-compat": { + "version": "0.3.7", + "resolved": "https://registry.npmjs.org/@firebase/app-check-compat/-/app-check-compat-0.3.7.tgz", + "integrity": "sha512-cW682AxsyP1G+Z0/P7pO/WT2CzYlNxoNe5QejVarW2o5ZxeWSSPAiVEwpEpQR/bUlUmdeWThYTMvBWaopdBsqw==", + "dependencies": { + "@firebase/app-check": "0.8.0", + "@firebase/app-check-types": "0.5.0", + "@firebase/component": "0.6.4", + "@firebase/logger": "0.4.0", + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/app-check-interop-types": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@firebase/app-check-interop-types/-/app-check-interop-types-0.3.0.tgz", + "integrity": "sha512-xAxHPZPIgFXnI+vb4sbBjZcde7ZluzPPaSK7Lx3/nmuVk4TjZvnL8ONnkd4ERQKL8WePQySU+pRcWkh8rDf5Sg==" + }, + "node_modules/@firebase/app-check-types": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@firebase/app-check-types/-/app-check-types-0.5.0.tgz", + "integrity": "sha512-uwSUj32Mlubybw7tedRzR24RP8M8JUVR3NPiMk3/Z4bCmgEKTlQBwMXrehDAZ2wF+TsBq0SN1c6ema71U/JPyQ==" + }, + "node_modules/@firebase/app-compat": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/@firebase/app-compat/-/app-compat-0.2.13.tgz", + "integrity": "sha512-j6ANZaWjeVy5zg6X7uiqh6lM6o3n3LD1+/SJFNs9V781xyryyZWXe+tmnWNWPkP086QfJoNkWN9pMQRqSG4vMg==", + "dependencies": { + "@firebase/app": "0.9.13", + "@firebase/component": "0.6.4", + "@firebase/logger": "0.4.0", + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" + } + }, + "node_modules/@firebase/app-types": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@firebase/app-types/-/app-types-0.9.0.tgz", + "integrity": "sha512-AeweANOIo0Mb8GiYm3xhTEBVCmPwTYAu9Hcd2qSkLuga/6+j9b1Jskl5bpiSQWy9eJ/j5pavxj6eYogmnuzm+Q==" + }, + "node_modules/@firebase/auth": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/@firebase/auth/-/auth-0.23.2.tgz", + "integrity": "sha512-dM9iJ0R6tI1JczuGSxXmQbXAgtYie0K4WvKcuyuSTCu9V8eEDiz4tfa1sO3txsfvwg7nOY3AjoCyMYEdqZ8hdg==", + "dependencies": { + "@firebase/component": "0.6.4", + "@firebase/logger": "0.4.0", + "@firebase/util": "1.9.3", + "node-fetch": "2.6.7", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/auth-compat": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@firebase/auth-compat/-/auth-compat-0.4.2.tgz", + "integrity": "sha512-Q30e77DWXFmXEt5dg5JbqEDpjw9y3/PcP9LslDPR7fARmAOTIY9MM6HXzm9KC+dlrKH/+p6l8g9ifJiam9mc4A==", + "dependencies": { + "@firebase/auth": "0.23.2", + "@firebase/auth-types": "0.12.0", + "@firebase/component": "0.6.4", + "@firebase/util": "1.9.3", + "node-fetch": "2.6.7", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/auth-compat/node_modules/node-fetch": { + "version": "2.6.7", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", + "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/@firebase/auth-interop-types": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@firebase/auth-interop-types/-/auth-interop-types-0.2.1.tgz", + "integrity": "sha512-VOaGzKp65MY6P5FI84TfYKBXEPi6LmOCSMMzys6o2BN2LOsqy7pCuZCup7NYnfbk5OkkQKzvIfHOzTm0UDpkyg==" + }, + "node_modules/@firebase/auth-types": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@firebase/auth-types/-/auth-types-0.12.0.tgz", + "integrity": "sha512-pPwaZt+SPOshK8xNoiQlK5XIrS97kFYc3Rc7xmy373QsOJ9MmqXxLaYssP5Kcds4wd2qK//amx/c+A8O2fVeZA==", + "peerDependencies": { + "@firebase/app-types": "0.x", + "@firebase/util": "1.x" + } + }, + "node_modules/@firebase/auth/node_modules/node-fetch": { + "version": "2.6.7", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", + "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/@firebase/component": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.6.4.tgz", + "integrity": "sha512-rLMyrXuO9jcAUCaQXCMjCMUsWrba5fzHlNK24xz5j2W6A/SRmK8mZJ/hn7V0fViLbxC0lPMtrK1eYzk6Fg03jA==", + "dependencies": { + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" + } + }, + "node_modules/@firebase/database": { + "version": "0.14.4", + "resolved": "https://registry.npmjs.org/@firebase/database/-/database-0.14.4.tgz", + "integrity": "sha512-+Ea/IKGwh42jwdjCyzTmeZeLM3oy1h0mFPsTy6OqCWzcu/KFqRAr5Tt1HRCOBlNOdbh84JPZC47WLU18n2VbxQ==", + "dependencies": { + "@firebase/auth-interop-types": "0.2.1", + "@firebase/component": "0.6.4", + "@firebase/logger": "0.4.0", + "@firebase/util": "1.9.3", + "faye-websocket": "0.11.4", + "tslib": "^2.1.0" + } + }, + "node_modules/@firebase/database-compat": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@firebase/database-compat/-/database-compat-0.3.4.tgz", + "integrity": "sha512-kuAW+l+sLMUKBThnvxvUZ+Q1ZrF/vFJ58iUY9kAcbX48U03nVzIF6Tmkf0p3WVQwMqiXguSgtOPIB6ZCeF+5Gg==", + "dependencies": { + "@firebase/component": "0.6.4", + "@firebase/database": "0.14.4", + "@firebase/database-types": "0.10.4", + "@firebase/logger": "0.4.0", + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" + } + }, + "node_modules/@firebase/database-types": { + "version": "0.10.4", + "resolved": "https://registry.npmjs.org/@firebase/database-types/-/database-types-0.10.4.tgz", + "integrity": "sha512-dPySn0vJ/89ZeBac70T+2tWWPiJXWbmRygYv0smT5TfE3hDrQ09eKMF3Y+vMlTdrMWq7mUdYW5REWPSGH4kAZQ==", + "dependencies": { + "@firebase/app-types": "0.9.0", + "@firebase/util": "1.9.3" + } + }, + "node_modules/@firebase/firestore": { + "version": "3.13.0", + "resolved": "https://registry.npmjs.org/@firebase/firestore/-/firestore-3.13.0.tgz", + "integrity": "sha512-NwcnU+madJXQ4fbLkGx1bWvL612IJN/qO6bZ6dlPmyf7QRyu5azUosijdAN675r+bOOJxMtP1Bv981bHBXAbUg==", + "dependencies": { + "@firebase/component": "0.6.4", + "@firebase/logger": "0.4.0", + "@firebase/util": "1.9.3", + "@firebase/webchannel-wrapper": "0.10.1", + "@grpc/grpc-js": "~1.7.0", + "@grpc/proto-loader": "^0.6.13", + "node-fetch": "2.6.7", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=10.10.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/firestore-compat": { + "version": "0.3.12", + "resolved": "https://registry.npmjs.org/@firebase/firestore-compat/-/firestore-compat-0.3.12.tgz", + "integrity": "sha512-mazuNGAx5Kt9Nph0pm6ULJFp/+j7GSsx+Ncw1GrnKl+ft1CQ4q2LcUssXnjqkX2Ry0fNGqUzC1mfIUrk9bYtjQ==", + "dependencies": { + "@firebase/component": "0.6.4", + "@firebase/firestore": "3.13.0", + "@firebase/firestore-types": "2.5.1", + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/firestore-types": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@firebase/firestore-types/-/firestore-types-2.5.1.tgz", + "integrity": "sha512-xG0CA6EMfYo8YeUxC8FeDzf6W3FX1cLlcAGBYV6Cku12sZRI81oWcu61RSKM66K6kUENP+78Qm8mvroBcm1whw==", + "peerDependencies": { + "@firebase/app-types": "0.x", + "@firebase/util": "1.x" + } + }, + "node_modules/@firebase/firestore/node_modules/node-fetch": { + "version": "2.6.7", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", + "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/@firebase/functions": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@firebase/functions/-/functions-0.10.0.tgz", + "integrity": "sha512-2U+fMNxTYhtwSpkkR6WbBcuNMOVaI7MaH3cZ6UAeNfj7AgEwHwMIFLPpC13YNZhno219F0lfxzTAA0N62ndWzA==", + "dependencies": { + "@firebase/app-check-interop-types": "0.3.0", + "@firebase/auth-interop-types": "0.2.1", + "@firebase/component": "0.6.4", + "@firebase/messaging-interop-types": "0.2.0", + "@firebase/util": "1.9.3", + "node-fetch": "2.6.7", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/functions-compat": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@firebase/functions-compat/-/functions-compat-0.3.5.tgz", + "integrity": "sha512-uD4jwgwVqdWf6uc3NRKF8cSZ0JwGqSlyhPgackyUPe+GAtnERpS4+Vr66g0b3Gge0ezG4iyHo/EXW/Hjx7QhHw==", + "dependencies": { + "@firebase/component": "0.6.4", + "@firebase/functions": "0.10.0", + "@firebase/functions-types": "0.6.0", + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/functions-types": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@firebase/functions-types/-/functions-types-0.6.0.tgz", + "integrity": "sha512-hfEw5VJtgWXIRf92ImLkgENqpL6IWpYaXVYiRkFY1jJ9+6tIhWM7IzzwbevwIIud/jaxKVdRzD7QBWfPmkwCYw==" + }, + "node_modules/@firebase/functions/node_modules/node-fetch": { + "version": "2.6.7", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", + "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/@firebase/installations": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/@firebase/installations/-/installations-0.6.4.tgz", + "integrity": "sha512-u5y88rtsp7NYkCHC3ElbFBrPtieUybZluXyzl7+4BsIz4sqb4vSAuwHEUgCgCeaQhvsnxDEU6icly8U9zsJigA==", + "dependencies": { + "@firebase/component": "0.6.4", + "@firebase/util": "1.9.3", + "idb": "7.0.1", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/installations-compat": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@firebase/installations-compat/-/installations-compat-0.2.4.tgz", + "integrity": "sha512-LI9dYjp0aT9Njkn9U4JRrDqQ6KXeAmFbRC0E7jI7+hxl5YmRWysq5qgQl22hcWpTk+cm3es66d/apoDU/A9n6Q==", + "dependencies": { + "@firebase/component": "0.6.4", + "@firebase/installations": "0.6.4", + "@firebase/installations-types": "0.5.0", + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/installations-types": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@firebase/installations-types/-/installations-types-0.5.0.tgz", + "integrity": "sha512-9DP+RGfzoI2jH7gY4SlzqvZ+hr7gYzPODrbzVD82Y12kScZ6ZpRg/i3j6rleto8vTFC8n6Len4560FnV1w2IRg==", + "peerDependencies": { + "@firebase/app-types": "0.x" + } + }, + "node_modules/@firebase/installations/node_modules/idb": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/idb/-/idb-7.0.1.tgz", + "integrity": "sha512-UUxlE7vGWK5RfB/fDwEGgRf84DY/ieqNha6msMV99UsEMQhJ1RwbCd8AYBj3QMgnE3VZnfQvm4oKVCJTYlqIgg==" + }, + "node_modules/@firebase/logger": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@firebase/logger/-/logger-0.4.0.tgz", + "integrity": "sha512-eRKSeykumZ5+cJPdxxJRgAC3G5NknY2GwEbKfymdnXtnT0Ucm4pspfR6GT4MUQEDuJwRVbVcSx85kgJulMoFFA==", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@firebase/messaging": { + "version": "0.12.4", + "resolved": "https://registry.npmjs.org/@firebase/messaging/-/messaging-0.12.4.tgz", + "integrity": "sha512-6JLZct6zUaex4g7HI3QbzeUrg9xcnmDAPTWpkoMpd/GoSVWH98zDoWXMGrcvHeCAIsLpFMe4MPoZkJbrPhaASw==", + "dependencies": { + "@firebase/component": "0.6.4", + "@firebase/installations": "0.6.4", + "@firebase/messaging-interop-types": "0.2.0", + "@firebase/util": "1.9.3", + "idb": "7.0.1", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/messaging-compat": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@firebase/messaging-compat/-/messaging-compat-0.2.4.tgz", + "integrity": "sha512-lyFjeUhIsPRYDPNIkYX1LcZMpoVbBWXX4rPl7c/rqc7G+EUea7IEtSt4MxTvh6fDfPuzLn7+FZADfscC+tNMfg==", + "dependencies": { + "@firebase/component": "0.6.4", + "@firebase/messaging": "0.12.4", + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/messaging-interop-types": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@firebase/messaging-interop-types/-/messaging-interop-types-0.2.0.tgz", + "integrity": "sha512-ujA8dcRuVeBixGR9CtegfpU4YmZf3Lt7QYkcj693FFannwNuZgfAYaTmbJ40dtjB81SAu6tbFPL9YLNT15KmOQ==" + }, + "node_modules/@firebase/messaging/node_modules/idb": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/idb/-/idb-7.0.1.tgz", + "integrity": "sha512-UUxlE7vGWK5RfB/fDwEGgRf84DY/ieqNha6msMV99UsEMQhJ1RwbCd8AYBj3QMgnE3VZnfQvm4oKVCJTYlqIgg==" + }, + "node_modules/@firebase/performance": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/@firebase/performance/-/performance-0.6.4.tgz", + "integrity": "sha512-HfTn/bd8mfy/61vEqaBelNiNnvAbUtME2S25A67Nb34zVuCSCRIX4SseXY6zBnOFj3oLisaEqhVcJmVPAej67g==", + "dependencies": { + "@firebase/component": "0.6.4", + "@firebase/installations": "0.6.4", + "@firebase/logger": "0.4.0", + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/performance-compat": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@firebase/performance-compat/-/performance-compat-0.2.4.tgz", + "integrity": "sha512-nnHUb8uP9G8islzcld/k6Bg5RhX62VpbAb/Anj7IXs/hp32Eb2LqFPZK4sy3pKkBUO5wcrlRWQa6wKOxqlUqsg==", + "dependencies": { + "@firebase/component": "0.6.4", + "@firebase/logger": "0.4.0", + "@firebase/performance": "0.6.4", + "@firebase/performance-types": "0.2.0", + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/performance-types": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@firebase/performance-types/-/performance-types-0.2.0.tgz", + "integrity": "sha512-kYrbr8e/CYr1KLrLYZZt2noNnf+pRwDq2KK9Au9jHrBMnb0/C9X9yWSXmZkFt4UIdsQknBq8uBB7fsybZdOBTA==" + }, + "node_modules/@firebase/remote-config": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/@firebase/remote-config/-/remote-config-0.4.4.tgz", + "integrity": "sha512-x1ioTHGX8ZwDSTOVp8PBLv2/wfwKzb4pxi0gFezS5GCJwbLlloUH4YYZHHS83IPxnua8b6l0IXUaWd0RgbWwzQ==", + "dependencies": { + "@firebase/component": "0.6.4", + "@firebase/installations": "0.6.4", + "@firebase/logger": "0.4.0", + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/remote-config-compat": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@firebase/remote-config-compat/-/remote-config-compat-0.2.4.tgz", + "integrity": "sha512-FKiki53jZirrDFkBHglB3C07j5wBpitAaj8kLME6g8Mx+aq7u9P7qfmuSRytiOItADhWUj7O1JIv7n9q87SuwA==", + "dependencies": { + "@firebase/component": "0.6.4", + "@firebase/logger": "0.4.0", + "@firebase/remote-config": "0.4.4", + "@firebase/remote-config-types": "0.3.0", + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/remote-config-types": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@firebase/remote-config-types/-/remote-config-types-0.3.0.tgz", + "integrity": "sha512-RtEH4vdcbXZuZWRZbIRmQVBNsE7VDQpet2qFvq6vwKLBIQRQR5Kh58M4ok3A3US8Sr3rubYnaGqZSurCwI8uMA==" + }, + "node_modules/@firebase/storage": { + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/@firebase/storage/-/storage-0.11.2.tgz", + "integrity": "sha512-CtvoFaBI4hGXlXbaCHf8humajkbXhs39Nbh6MbNxtwJiCqxPy9iH3D3CCfXAvP0QvAAwmJUTK3+z9a++Kc4nkA==", + "dependencies": { + "@firebase/component": "0.6.4", + "@firebase/util": "1.9.3", + "node-fetch": "2.6.7", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/storage-compat": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@firebase/storage-compat/-/storage-compat-0.3.2.tgz", + "integrity": "sha512-wvsXlLa9DVOMQJckbDNhXKKxRNNewyUhhbXev3t8kSgoCotd1v3MmqhKKz93ePhDnhHnDs7bYHy+Qa8dRY6BXw==", + "dependencies": { + "@firebase/component": "0.6.4", + "@firebase/storage": "0.11.2", + "@firebase/storage-types": "0.8.0", + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/storage-types": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@firebase/storage-types/-/storage-types-0.8.0.tgz", + "integrity": "sha512-isRHcGrTs9kITJC0AVehHfpraWFui39MPaU7Eo8QfWlqW7YPymBmRgjDrlOgFdURh6Cdeg07zmkLP5tzTKRSpg==", + "peerDependencies": { + "@firebase/app-types": "0.x", + "@firebase/util": "1.x" + } + }, + "node_modules/@firebase/storage/node_modules/node-fetch": { + "version": "2.6.7", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", + "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/@firebase/util": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/@firebase/util/-/util-1.9.3.tgz", + "integrity": "sha512-DY02CRhOZwpzO36fHpuVysz6JZrscPiBXD0fXp6qSrL9oNOx5KWICKdR95C0lSITzxp0TZosVyHqzatE8JbcjA==", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@firebase/webchannel-wrapper": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@firebase/webchannel-wrapper/-/webchannel-wrapper-0.10.1.tgz", + "integrity": "sha512-Dq5rYfEpdeel0bLVN+nfD1VWmzCkK+pJbSjIawGE+RY4+NIJqhbUDDQjvV0NUK84fMfwxvtFoCtEe70HfZjFcw==" + }, + "node_modules/@fortawesome/fontawesome-free": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-6.6.0.tgz", + "integrity": "sha512-60G28ke/sXdtS9KZCpZSHHkCbdsOGEhIUGlwq6yhY74UpTiToIh8np7A8yphhM4BWsvNFtIvLpi4co+h9Mr9Ow==", + "engines": { + "node": ">=6" + } + }, + "node_modules/@gar/promisify": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz", + "integrity": "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==", + "dev": true + }, + "node_modules/@grpc/grpc-js": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.7.3.tgz", + "integrity": "sha512-H9l79u4kJ2PVSxUNA08HMYAnUBLj9v6KjYQ7SQ71hOZcEXhShE/y5iQCesP8+6/Ik/7i2O0a10bPquIcYfufog==", + "dependencies": { + "@grpc/proto-loader": "^0.7.0", + "@types/node": ">=12.12.47" + }, + "engines": { + "node": "^8.13.0 || >=10.10.0" + } + }, + "node_modules/@grpc/grpc-js/node_modules/@grpc/proto-loader": { + "version": "0.7.13", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.13.tgz", + "integrity": "sha512-AiXO/bfe9bmxBjxxtYxFAXGZvMaN5s8kO+jBHAJCON8rJoB5YS/D6X7ZNc6XQkuHNmyl4CYaMI1fJ/Gn27RGGw==", + "dependencies": { + "lodash.camelcase": "^4.3.0", + "long": "^5.0.0", + "protobufjs": "^7.2.5", + "yargs": "^17.7.2" + }, + "bin": { + "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@grpc/grpc-js/node_modules/long": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/long/-/long-5.2.3.tgz", + "integrity": "sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==" + }, + "node_modules/@grpc/grpc-js/node_modules/protobufjs": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.3.2.tgz", + "integrity": "sha512-RXyHaACeqXeqAKGLDl68rQKbmObRsTIn4TYVUUug1KfS47YWCo5MacGITEryugIgZqORCvJWEk4l449POg5Txg==", + "hasInstallScript": true, + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/node": ">=13.7.0", + "long": "^5.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@grpc/grpc-js/node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@grpc/proto-loader": { + "version": "0.6.13", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.6.13.tgz", + "integrity": "sha512-FjxPYDRTn6Ec3V0arm1FtSpmP6V50wuph2yILpyvTKzjc76oDdoihXqM1DzOW5ubvCC8GivfCnNtfaRE8myJ7g==", + "dependencies": { + "@types/long": "^4.0.1", + "lodash.camelcase": "^4.3.0", + "long": "^4.0.0", + "protobufjs": "^6.11.3", + "yargs": "^16.2.0" + }, + "bin": { + "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@grpc/proto-loader/node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/@grpc/proto-loader/node_modules/yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@grpc/proto-loader/node_modules/yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "engines": { + "node": ">=10" + } + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.11.14", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", + "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==", + "deprecated": "Use @eslint/config-array instead", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanwhocodes/object-schema": "^2.0.2", + "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.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "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.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "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/@ionic/angular": { + "version": "7.8.6", + "resolved": "https://registry.npmjs.org/@ionic/angular/-/angular-7.8.6.tgz", + "integrity": "sha512-3Qe53hXpyjtx6fFcxt/NTAlauIawsGmCZJPauV5sAnSKVuX8C82C1zMAZTeJt6m2dnd71wythc98BXUXsx/UxQ==", + "dependencies": { + "@ionic/core": "7.8.6", + "ionicons": "^7.0.0", + "jsonc-parser": "^3.0.0", + "tslib": "^2.3.0" + }, + "peerDependencies": { + "@angular/core": ">=14.0.0", + "@angular/forms": ">=14.0.0", + "@angular/router": ">=14.0.0", + "rxjs": ">=7.5.0", + "zone.js": ">=0.11.0" + } + }, + "node_modules/@ionic/core": { + "version": "7.8.6", + "resolved": "https://registry.npmjs.org/@ionic/core/-/core-7.8.6.tgz", + "integrity": "sha512-HAYZdEmeJgOdo2kDlZkcCGHb+zs/vjU6iv4skbVBL7y+OnSv/oC2u83Yee8S3/aY0YAxkyBgu7hLTYH13Zc2Aw==", + "dependencies": { + "@stencil/core": "^4.12.2", + "ionicons": "^7.2.2", + "tslib": "^2.1.0" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "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/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "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/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "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/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.1.1.tgz", + "integrity": "sha512-sQXCasFk+U8lWYEe66WxRDOE9PjVz4vSM51fTu3Hw+ClTpUSQb718772vH3pyS5pShp6lvQM7SxgIDXXXmOX7w==", + "dev": true, + "dependencies": { + "@jridgewell/set-array": "^1.0.0", + "@jridgewell/sourcemap-codec": "^1.4.10" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.6.tgz", + "integrity": "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, + "node_modules/@jridgewell/source-map/node_modules/@jridgewell/gen-mapping": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", + "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.15", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@leichtgewicht/ip-codec": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz", + "integrity": "sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==", + "dev": true + }, + "node_modules/@material/animation": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/animation/-/animation-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-5osi1z4JQIXcklPALbH/zTfOm2pDzHt9Fxm7ZyURy250xIZj6QjULRzPTnzOhC2ropfix9ra2Cfggbf0dcRbEQ==", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@material/auto-init": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/auto-init/-/auto-init-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-OigQTmrVzkcGvxNjOaIe5oItTFPgrO9xLewvharDI6m6yvO1z7OBnkcW+sFN6ggLNYNxd0O1u9v64vMsmeDABQ==", + "dependencies": { + "@material/base": "15.0.0-canary.684e33d25.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/banner": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/banner/-/banner-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-PqtGp3KWzdu58rWv/DIvSfe38m5YKOBbAAbBinSvgadBb/da+IE1t5F7YPNKE1T5lJsQBGVUYx6QBIeXm+aI/A==", + "dependencies": { + "@material/base": "15.0.0-canary.684e33d25.0", + "@material/button": "15.0.0-canary.684e33d25.0", + "@material/dom": "15.0.0-canary.684e33d25.0", + "@material/elevation": "15.0.0-canary.684e33d25.0", + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "@material/ripple": "15.0.0-canary.684e33d25.0", + "@material/rtl": "15.0.0-canary.684e33d25.0", + "@material/shape": "15.0.0-canary.684e33d25.0", + "@material/theme": "15.0.0-canary.684e33d25.0", + "@material/tokens": "15.0.0-canary.684e33d25.0", + "@material/typography": "15.0.0-canary.684e33d25.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/base": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/base/-/base-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-oOaqb/SfjWwTKsdJUZmeh/Qrs41nIJI0N+zELsxnvbGjSIN1ZMAKYZFPMahqvC68OJ6+5CvJM8PoTNs5l+B8IQ==", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@material/button": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/button/-/button-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-Nkekk4edeX+ObVOa7UlwavaHdmckPV5wU4SAJf3iA3R61cmz+KsgAgpzfcwv5WfNhIlc2nLu8QYEecpHdo9d/w==", + "dependencies": { + "@material/density": "15.0.0-canary.684e33d25.0", + "@material/dom": "15.0.0-canary.684e33d25.0", + "@material/elevation": "15.0.0-canary.684e33d25.0", + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "@material/focus-ring": "15.0.0-canary.684e33d25.0", + "@material/ripple": "15.0.0-canary.684e33d25.0", + "@material/rtl": "15.0.0-canary.684e33d25.0", + "@material/shape": "15.0.0-canary.684e33d25.0", + "@material/theme": "15.0.0-canary.684e33d25.0", + "@material/tokens": "15.0.0-canary.684e33d25.0", + "@material/touch-target": "15.0.0-canary.684e33d25.0", + "@material/typography": "15.0.0-canary.684e33d25.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/card": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/card/-/card-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-xhyB7XX5KkEiCEqwSPkl58ZGYL6xFdnY62zimyBXJRG/Eaa0Swj3kW20hVCpt4f7c9Zmp8Se27rg8vnKmhvO3g==", + "dependencies": { + "@material/dom": "15.0.0-canary.684e33d25.0", + "@material/elevation": "15.0.0-canary.684e33d25.0", + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "@material/ripple": "15.0.0-canary.684e33d25.0", + "@material/rtl": "15.0.0-canary.684e33d25.0", + "@material/shape": "15.0.0-canary.684e33d25.0", + "@material/theme": "15.0.0-canary.684e33d25.0", + "@material/tokens": "15.0.0-canary.684e33d25.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/checkbox": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/checkbox/-/checkbox-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-NFpM3TS924PmVsk2KQLNU95OYCf8ZwYgzeqfnAexU0bEfjUJXINBun2Go0AaeOUMjuvWUe+byjrXgv8SFYbMUA==", + "dependencies": { + "@material/animation": "15.0.0-canary.684e33d25.0", + "@material/base": "15.0.0-canary.684e33d25.0", + "@material/density": "15.0.0-canary.684e33d25.0", + "@material/dom": "15.0.0-canary.684e33d25.0", + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "@material/focus-ring": "15.0.0-canary.684e33d25.0", + "@material/ripple": "15.0.0-canary.684e33d25.0", + "@material/theme": "15.0.0-canary.684e33d25.0", + "@material/touch-target": "15.0.0-canary.684e33d25.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/chips": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/chips/-/chips-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-z4ajQ4NnsAQ/Si9tZ4xmxzjj2Qb+vW++4QjCjjjwAGIZbCe0xglAnMh2t66XLJUxt7RoKZuZVEO7ZqcFZpvJFQ==", + "dependencies": { + "@material/animation": "15.0.0-canary.684e33d25.0", + "@material/base": "15.0.0-canary.684e33d25.0", + "@material/checkbox": "15.0.0-canary.684e33d25.0", + "@material/density": "15.0.0-canary.684e33d25.0", + "@material/dom": "15.0.0-canary.684e33d25.0", + "@material/elevation": "15.0.0-canary.684e33d25.0", + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "@material/focus-ring": "15.0.0-canary.684e33d25.0", + "@material/ripple": "15.0.0-canary.684e33d25.0", + "@material/rtl": "15.0.0-canary.684e33d25.0", + "@material/shape": "15.0.0-canary.684e33d25.0", + "@material/theme": "15.0.0-canary.684e33d25.0", + "@material/tokens": "15.0.0-canary.684e33d25.0", + "@material/touch-target": "15.0.0-canary.684e33d25.0", + "@material/typography": "15.0.0-canary.684e33d25.0", + "safevalues": "^0.3.4", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/circular-progress": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/circular-progress/-/circular-progress-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-G6qD0nGNtEUwWnAMJuA9INYFpZoKtx7KFjBaPF4Ol2YLHtmShALNAYyn54TMAK8AZ2IpW08PXjGS7Ye88vrdEQ==", + "dependencies": { + "@material/animation": "15.0.0-canary.684e33d25.0", + "@material/base": "15.0.0-canary.684e33d25.0", + "@material/dom": "15.0.0-canary.684e33d25.0", + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "@material/progress-indicator": "15.0.0-canary.684e33d25.0", + "@material/rtl": "15.0.0-canary.684e33d25.0", + "@material/theme": "15.0.0-canary.684e33d25.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/data-table": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/data-table/-/data-table-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-+wDw1DDDFfAsKAMzs84f/5GCjux39zjNfW8tL4wFbkWNwewmQrG9zaQMJhBpVOtLCrM8Gj6SOgOANqgqoCjvGg==", + "dependencies": { + "@material/animation": "15.0.0-canary.684e33d25.0", + "@material/base": "15.0.0-canary.684e33d25.0", + "@material/checkbox": "15.0.0-canary.684e33d25.0", + "@material/density": "15.0.0-canary.684e33d25.0", + "@material/dom": "15.0.0-canary.684e33d25.0", + "@material/elevation": "15.0.0-canary.684e33d25.0", + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "@material/icon-button": "15.0.0-canary.684e33d25.0", + "@material/linear-progress": "15.0.0-canary.684e33d25.0", + "@material/list": "15.0.0-canary.684e33d25.0", + "@material/menu": "15.0.0-canary.684e33d25.0", + "@material/rtl": "15.0.0-canary.684e33d25.0", + "@material/select": "15.0.0-canary.684e33d25.0", + "@material/shape": "15.0.0-canary.684e33d25.0", + "@material/theme": "15.0.0-canary.684e33d25.0", + "@material/tokens": "15.0.0-canary.684e33d25.0", + "@material/touch-target": "15.0.0-canary.684e33d25.0", + "@material/typography": "15.0.0-canary.684e33d25.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/density": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/density/-/density-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-661yEVRMGrlq6S6WuSbPRO+ZwpdUOg2glCc7y96doM6itSLOa3UEAldjOLfsYZVB74GnKCiuDp//QmfoRyYTfA==", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@material/dialog": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/dialog/-/dialog-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-szn0dHnfeQTSOC6SSRSGAzX6Tnx+4NnSMUwNkXm+3bwjds8ZVK26+DXwLrP5f3ID5F1K5sFsRf2INo5/TNTHyQ==", + "dependencies": { + "@material/animation": "15.0.0-canary.684e33d25.0", + "@material/base": "15.0.0-canary.684e33d25.0", + "@material/button": "15.0.0-canary.684e33d25.0", + "@material/dom": "15.0.0-canary.684e33d25.0", + "@material/elevation": "15.0.0-canary.684e33d25.0", + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "@material/icon-button": "15.0.0-canary.684e33d25.0", + "@material/ripple": "15.0.0-canary.684e33d25.0", + "@material/rtl": "15.0.0-canary.684e33d25.0", + "@material/shape": "15.0.0-canary.684e33d25.0", + "@material/theme": "15.0.0-canary.684e33d25.0", + "@material/tokens": "15.0.0-canary.684e33d25.0", + "@material/touch-target": "15.0.0-canary.684e33d25.0", + "@material/typography": "15.0.0-canary.684e33d25.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/dom": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/dom/-/dom-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-7pEJLYov+tGgfuD8mZxoVU6rWtPI8ppjTAhz+F27Hz9FG0JETMWTKpDPBXLnKvX7vhIxL83GvZ9geNHCe8Hfog==", + "dependencies": { + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "@material/rtl": "15.0.0-canary.684e33d25.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/drawer": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/drawer/-/drawer-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-/KMckLf1PYU/H3PXnS4e0aFl03qG3JlSv4LGgX6juJufcONqGTl/m63EMO/L/eUy6H1CRrXmVDjik/jzHLyDhg==", + "dependencies": { + "@material/animation": "15.0.0-canary.684e33d25.0", + "@material/base": "15.0.0-canary.684e33d25.0", + "@material/dom": "15.0.0-canary.684e33d25.0", + "@material/elevation": "15.0.0-canary.684e33d25.0", + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "@material/list": "15.0.0-canary.684e33d25.0", + "@material/ripple": "15.0.0-canary.684e33d25.0", + "@material/rtl": "15.0.0-canary.684e33d25.0", + "@material/shape": "15.0.0-canary.684e33d25.0", + "@material/theme": "15.0.0-canary.684e33d25.0", + "@material/typography": "15.0.0-canary.684e33d25.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/elevation": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/elevation/-/elevation-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-WDF8SsRtq3rXUbVVbd9K4DUijIPH0bUFSOreVYxudpuxAfTlDS5+aeS1EK9UIBFYLuba4u5wVT2tDv6e1RTfrQ==", + "dependencies": { + "@material/animation": "15.0.0-canary.684e33d25.0", + "@material/base": "15.0.0-canary.684e33d25.0", + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "@material/rtl": "15.0.0-canary.684e33d25.0", + "@material/theme": "15.0.0-canary.684e33d25.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/fab": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/fab/-/fab-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-KCu87rWOKEAe9vZcAm6K8XazYSWPNjMG+OhrbPjHW6bCO7as1YCgtmkBkhff7csY/rFmcVpIy884xtUfLmSudQ==", + "dependencies": { + "@material/animation": "15.0.0-canary.684e33d25.0", + "@material/dom": "15.0.0-canary.684e33d25.0", + "@material/elevation": "15.0.0-canary.684e33d25.0", + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "@material/focus-ring": "15.0.0-canary.684e33d25.0", + "@material/ripple": "15.0.0-canary.684e33d25.0", + "@material/rtl": "15.0.0-canary.684e33d25.0", + "@material/shape": "15.0.0-canary.684e33d25.0", + "@material/theme": "15.0.0-canary.684e33d25.0", + "@material/tokens": "15.0.0-canary.684e33d25.0", + "@material/touch-target": "15.0.0-canary.684e33d25.0", + "@material/typography": "15.0.0-canary.684e33d25.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/feature-targeting": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/feature-targeting/-/feature-targeting-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-HyH1erNTSjS63sigNSUMaCd0nJhTNdDFeC+myrxwtDaQm+uYJ8troCNtQM3g6mx0XATNtX5aTOoPmrM6yVVi1A==", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@material/floating-label": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/floating-label/-/floating-label-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-f7TPp6bKpGvV3sYYiZHSGlrixXKkXXITW3Esp7KB9jRq42c0H82novmdwvY0eTef4ootmA2JEysr78KQfHBUPg==", + "dependencies": { + "@material/animation": "15.0.0-canary.684e33d25.0", + "@material/base": "15.0.0-canary.684e33d25.0", + "@material/dom": "15.0.0-canary.684e33d25.0", + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "@material/rtl": "15.0.0-canary.684e33d25.0", + "@material/theme": "15.0.0-canary.684e33d25.0", + "@material/typography": "15.0.0-canary.684e33d25.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/focus-ring": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/focus-ring/-/focus-ring-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-ikw2RVUfgzXChpWIzPH1VzRvTjYb5ZKj4H+CZf7jqPUXMstFOZg90Bp7ARLZHqYiyNMuUq3zUTHozS6iHorSqg==", + "dependencies": { + "@material/dom": "15.0.0-canary.684e33d25.0", + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "@material/rtl": "15.0.0-canary.684e33d25.0" + } + }, + "node_modules/@material/form-field": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/form-field/-/form-field-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-vpF9N/uq5no/7+8GAbEH0868FhOuBgxAWRr1Sfb+jthKfBr8OS/wPU/AHzZHdHdAm7PQynbeOXfDsX2dI//PDA==", + "dependencies": { + "@material/base": "15.0.0-canary.684e33d25.0", + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "@material/ripple": "15.0.0-canary.684e33d25.0", + "@material/rtl": "15.0.0-canary.684e33d25.0", + "@material/theme": "15.0.0-canary.684e33d25.0", + "@material/typography": "15.0.0-canary.684e33d25.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/icon-button": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/icon-button/-/icon-button-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-wMI+XGzmIN/o2ePBKg2hLyx7H4pXCRAyyIKMQS1FMp1UKa2tYmiHVX/V8skhKwCqxg3i6Ls/LxMjfPxTR18WvQ==", + "dependencies": { + "@material/base": "15.0.0-canary.684e33d25.0", + "@material/density": "15.0.0-canary.684e33d25.0", + "@material/dom": "15.0.0-canary.684e33d25.0", + "@material/elevation": "15.0.0-canary.684e33d25.0", + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "@material/focus-ring": "15.0.0-canary.684e33d25.0", + "@material/ripple": "15.0.0-canary.684e33d25.0", + "@material/rtl": "15.0.0-canary.684e33d25.0", + "@material/theme": "15.0.0-canary.684e33d25.0", + "@material/touch-target": "15.0.0-canary.684e33d25.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/image-list": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/image-list/-/image-list-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-Ol+uaHYBe5R/cgzlfh5ONnMVX0wO6fV74JMUcQCQlxP6lXau/edARo4tkRc7A7UJUkU3VRv0EpEjLoCRNUPGaA==", + "dependencies": { + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "@material/shape": "15.0.0-canary.684e33d25.0", + "@material/theme": "15.0.0-canary.684e33d25.0", + "@material/typography": "15.0.0-canary.684e33d25.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/layout-grid": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/layout-grid/-/layout-grid-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-ALXE1mqFNb/RB2lVRQ3/r1Aufw2mFZnOjRE+boYDVepmAG/xWyPCyaGoavELJF5l4GAb0tXi8wA/8HeGbLOpuA==", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@material/line-ripple": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/line-ripple/-/line-ripple-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-7hRx8C/e9i0P6pgQpNOMfTwSS2r1fwEvBL72QDVGLtLuoKKwsjjgP6Z0Jat/GeHJe87u9LQvGBoD4upt+of/HA==", + "dependencies": { + "@material/animation": "15.0.0-canary.684e33d25.0", + "@material/base": "15.0.0-canary.684e33d25.0", + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "@material/theme": "15.0.0-canary.684e33d25.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/linear-progress": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/linear-progress/-/linear-progress-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-iJclt7mKmcMk6pqD7ocXKfCWZhqBoODp7N593jYlxVpTJuEz2wiVAjZUDn/YGj/Uz3CRH+2YFfOiLr9pwWjhDg==", + "dependencies": { + "@material/animation": "15.0.0-canary.684e33d25.0", + "@material/base": "15.0.0-canary.684e33d25.0", + "@material/dom": "15.0.0-canary.684e33d25.0", + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "@material/progress-indicator": "15.0.0-canary.684e33d25.0", + "@material/rtl": "15.0.0-canary.684e33d25.0", + "@material/theme": "15.0.0-canary.684e33d25.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/list": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/list/-/list-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-rQ+FCSdzmwTcT00IYE0uRV3CS4oGSccKFl9hkcF+aHFW61L7ORh/SCGUDPrEfQFrFkMn5f8qroVJjpUAMXBz4g==", + "dependencies": { + "@material/base": "15.0.0-canary.684e33d25.0", + "@material/density": "15.0.0-canary.684e33d25.0", + "@material/dom": "15.0.0-canary.684e33d25.0", + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "@material/ripple": "15.0.0-canary.684e33d25.0", + "@material/rtl": "15.0.0-canary.684e33d25.0", + "@material/shape": "15.0.0-canary.684e33d25.0", + "@material/theme": "15.0.0-canary.684e33d25.0", + "@material/tokens": "15.0.0-canary.684e33d25.0", + "@material/typography": "15.0.0-canary.684e33d25.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/menu": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/menu/-/menu-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-r7wzDLSGSI9629/mfpvsMzkVxpmV75kcD3IrW0Pcu6/Bv/1xi0EvjcUXzNJJoQlwN4Zj35Ymz/PCjZkIDIz68Q==", + "dependencies": { + "@material/base": "15.0.0-canary.684e33d25.0", + "@material/dom": "15.0.0-canary.684e33d25.0", + "@material/elevation": "15.0.0-canary.684e33d25.0", + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "@material/list": "15.0.0-canary.684e33d25.0", + "@material/menu-surface": "15.0.0-canary.684e33d25.0", + "@material/ripple": "15.0.0-canary.684e33d25.0", + "@material/rtl": "15.0.0-canary.684e33d25.0", + "@material/shape": "15.0.0-canary.684e33d25.0", + "@material/theme": "15.0.0-canary.684e33d25.0", + "@material/tokens": "15.0.0-canary.684e33d25.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/menu-surface": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/menu-surface/-/menu-surface-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-RVO5GAYcfWPaKwxsF/NhUAmrYXQCQBKvRQW0TIlbmAJz6lcFeTs6YZqF3u1C7qrL3ZQGz+sur/7ywj6QU0oMow==", + "dependencies": { + "@material/animation": "15.0.0-canary.684e33d25.0", + "@material/base": "15.0.0-canary.684e33d25.0", + "@material/elevation": "15.0.0-canary.684e33d25.0", + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "@material/rtl": "15.0.0-canary.684e33d25.0", + "@material/shape": "15.0.0-canary.684e33d25.0", + "@material/theme": "15.0.0-canary.684e33d25.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/notched-outline": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/notched-outline/-/notched-outline-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-9YHcBkvJLPVYzkHcWoTpBZAFrEd+j1hjhGxLhh0LuNrZe8VroUkZD1TTnUAPHRG3os6EqEWWaKb0RN+aPIF2yQ==", + "dependencies": { + "@material/base": "15.0.0-canary.684e33d25.0", + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "@material/floating-label": "15.0.0-canary.684e33d25.0", + "@material/rtl": "15.0.0-canary.684e33d25.0", + "@material/shape": "15.0.0-canary.684e33d25.0", + "@material/theme": "15.0.0-canary.684e33d25.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/progress-indicator": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/progress-indicator/-/progress-indicator-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-c0icji4faeNWUoqGENGC7Hav0Puxh0RwXIDVizffaUxKIGbajpIp5+4Zop73fK/xFLGMB/npg7TbP+aCGjQ3fw==", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@material/radio": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/radio/-/radio-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-U3Eh8sNUA8trDla1Bq8Bo02foxYvtoewaKeF8A8tAju81XZ4jRiftfOsOWZDZEHCVbbCB2QwvutvFlnay5n+Aw==", + "dependencies": { + "@material/animation": "15.0.0-canary.684e33d25.0", + "@material/base": "15.0.0-canary.684e33d25.0", + "@material/density": "15.0.0-canary.684e33d25.0", + "@material/dom": "15.0.0-canary.684e33d25.0", + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "@material/focus-ring": "15.0.0-canary.684e33d25.0", + "@material/ripple": "15.0.0-canary.684e33d25.0", + "@material/theme": "15.0.0-canary.684e33d25.0", + "@material/touch-target": "15.0.0-canary.684e33d25.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/ripple": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/ripple/-/ripple-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-RyePu7SjIm/OuyyEieZ/gxiPYkNZOZHeid72WRcN9ofdlljj2pifcdPvcfZA+v/DMS33xo5GjG2L/Qj6ClWrKw==", + "dependencies": { + "@material/animation": "15.0.0-canary.684e33d25.0", + "@material/base": "15.0.0-canary.684e33d25.0", + "@material/dom": "15.0.0-canary.684e33d25.0", + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "@material/rtl": "15.0.0-canary.684e33d25.0", + "@material/theme": "15.0.0-canary.684e33d25.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/rtl": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/rtl/-/rtl-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-NqdJl8Ayupp1Th+vCNCpVQHbUFOuF7TCte9LD1norTIBUF/QizIxWby2W5uUEiPbnh5j9PmE1CJtfLwKun3pcw==", + "dependencies": { + "@material/theme": "15.0.0-canary.684e33d25.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/segmented-button": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/segmented-button/-/segmented-button-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-bEGgg8vgXNLyukyV8HRjFMuQ6t6nm5LQ4Pgm22um61Yc8qyi0BOqV41OR4SVdUrUqZxh1aVD+p+4NN03+LfQXw==", + "dependencies": { + "@material/base": "15.0.0-canary.684e33d25.0", + "@material/elevation": "15.0.0-canary.684e33d25.0", + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "@material/ripple": "15.0.0-canary.684e33d25.0", + "@material/theme": "15.0.0-canary.684e33d25.0", + "@material/touch-target": "15.0.0-canary.684e33d25.0", + "@material/typography": "15.0.0-canary.684e33d25.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/select": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/select/-/select-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-kf178/2TeEinTv0mgmSBcmmExQ2h7a7dtR1E3WuqQgisJ/R6+zVLMkC2CnfIyzxYX2vkuUTG0ue3Reh/6XiqSg==", + "dependencies": { + "@material/animation": "15.0.0-canary.684e33d25.0", + "@material/base": "15.0.0-canary.684e33d25.0", + "@material/density": "15.0.0-canary.684e33d25.0", + "@material/dom": "15.0.0-canary.684e33d25.0", + "@material/elevation": "15.0.0-canary.684e33d25.0", + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "@material/floating-label": "15.0.0-canary.684e33d25.0", + "@material/line-ripple": "15.0.0-canary.684e33d25.0", + "@material/list": "15.0.0-canary.684e33d25.0", + "@material/menu": "15.0.0-canary.684e33d25.0", + "@material/menu-surface": "15.0.0-canary.684e33d25.0", + "@material/notched-outline": "15.0.0-canary.684e33d25.0", + "@material/ripple": "15.0.0-canary.684e33d25.0", + "@material/rtl": "15.0.0-canary.684e33d25.0", + "@material/shape": "15.0.0-canary.684e33d25.0", + "@material/theme": "15.0.0-canary.684e33d25.0", + "@material/tokens": "15.0.0-canary.684e33d25.0", + "@material/typography": "15.0.0-canary.684e33d25.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/shape": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/shape/-/shape-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-aEelpaTFmpnCji3TUGP9bVCS/bRVjUmLTHBPZtuu1gOrUVVtJ6kYOg73dZNJF+XOoNL2yOX/LRcKwsop29tptA==", + "dependencies": { + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "@material/rtl": "15.0.0-canary.684e33d25.0", + "@material/theme": "15.0.0-canary.684e33d25.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/slider": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/slider/-/slider-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-WVyK+2pSNSZmj07M2K/a3TADoQ9FBCndfNC/vE7/wGIg4dddJJK5KvQ+yruf9R2cSzTL/S1sZ5WpyyeM8E9HTw==", + "dependencies": { + "@material/animation": "15.0.0-canary.684e33d25.0", + "@material/base": "15.0.0-canary.684e33d25.0", + "@material/dom": "15.0.0-canary.684e33d25.0", + "@material/elevation": "15.0.0-canary.684e33d25.0", + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "@material/ripple": "15.0.0-canary.684e33d25.0", + "@material/rtl": "15.0.0-canary.684e33d25.0", + "@material/theme": "15.0.0-canary.684e33d25.0", + "@material/tokens": "15.0.0-canary.684e33d25.0", + "@material/typography": "15.0.0-canary.684e33d25.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/snackbar": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/snackbar/-/snackbar-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-itO+DCkOannZzR1/cCHcqAm7ifhuFvXmDItNoA8qLEcAyJDJJRkhpwj3XQ01yuo9gBFcSctp7Txt7e+Hncm/Jg==", + "dependencies": { + "@material/animation": "15.0.0-canary.684e33d25.0", + "@material/base": "15.0.0-canary.684e33d25.0", + "@material/button": "15.0.0-canary.684e33d25.0", + "@material/dom": "15.0.0-canary.684e33d25.0", + "@material/elevation": "15.0.0-canary.684e33d25.0", + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "@material/icon-button": "15.0.0-canary.684e33d25.0", + "@material/ripple": "15.0.0-canary.684e33d25.0", + "@material/rtl": "15.0.0-canary.684e33d25.0", + "@material/shape": "15.0.0-canary.684e33d25.0", + "@material/theme": "15.0.0-canary.684e33d25.0", + "@material/tokens": "15.0.0-canary.684e33d25.0", + "@material/typography": "15.0.0-canary.684e33d25.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/switch": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/switch/-/switch-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-Jxi0gl92yvvZZsAPxvVHzXx2ga+T/djMow98jvEczmpUorWnAhgiCr9CsSSRoosahWyRB8NLZOxUQrACxvffjw==", + "dependencies": { + "@material/animation": "15.0.0-canary.684e33d25.0", + "@material/base": "15.0.0-canary.684e33d25.0", + "@material/density": "15.0.0-canary.684e33d25.0", + "@material/dom": "15.0.0-canary.684e33d25.0", + "@material/elevation": "15.0.0-canary.684e33d25.0", + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "@material/focus-ring": "15.0.0-canary.684e33d25.0", + "@material/ripple": "15.0.0-canary.684e33d25.0", + "@material/rtl": "15.0.0-canary.684e33d25.0", + "@material/shape": "15.0.0-canary.684e33d25.0", + "@material/theme": "15.0.0-canary.684e33d25.0", + "@material/tokens": "15.0.0-canary.684e33d25.0", + "safevalues": "^0.3.4", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/tab": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/tab/-/tab-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-WQL3wj9syHNcfe8KbgGGUcA34M8C/xZ+n0Fkkh8Kk6puVwaU+xqUNihsxPY6YzKpmh4PZ4oJaBdiN8zvFT1zqQ==", + "dependencies": { + "@material/base": "15.0.0-canary.684e33d25.0", + "@material/elevation": "15.0.0-canary.684e33d25.0", + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "@material/focus-ring": "15.0.0-canary.684e33d25.0", + "@material/ripple": "15.0.0-canary.684e33d25.0", + "@material/rtl": "15.0.0-canary.684e33d25.0", + "@material/tab-indicator": "15.0.0-canary.684e33d25.0", + "@material/theme": "15.0.0-canary.684e33d25.0", + "@material/tokens": "15.0.0-canary.684e33d25.0", + "@material/typography": "15.0.0-canary.684e33d25.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/tab-bar": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/tab-bar/-/tab-bar-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-SW/cMaDsIGGkM1ag3A7GJRlmr8eXmObWsvitQJzh6Azr5zzZtSI+GQygkMesAEE1gbpqOVN8d40rh3H7VVIAcA==", + "dependencies": { + "@material/animation": "15.0.0-canary.684e33d25.0", + "@material/base": "15.0.0-canary.684e33d25.0", + "@material/density": "15.0.0-canary.684e33d25.0", + "@material/elevation": "15.0.0-canary.684e33d25.0", + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "@material/tab": "15.0.0-canary.684e33d25.0", + "@material/tab-indicator": "15.0.0-canary.684e33d25.0", + "@material/tab-scroller": "15.0.0-canary.684e33d25.0", + "@material/theme": "15.0.0-canary.684e33d25.0", + "@material/tokens": "15.0.0-canary.684e33d25.0", + "@material/typography": "15.0.0-canary.684e33d25.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/tab-indicator": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/tab-indicator/-/tab-indicator-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-kKICqSPqOlaf0lzaFFCmuOqPXJC+cK48Qmsc+m5o6fJhkmuZRCYpIwB2JeP+uZSOq/bTH+SrPtCtnVlgWg6ksA==", + "dependencies": { + "@material/animation": "15.0.0-canary.684e33d25.0", + "@material/base": "15.0.0-canary.684e33d25.0", + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "@material/theme": "15.0.0-canary.684e33d25.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/tab-scroller": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/tab-scroller/-/tab-scroller-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-H6EU/TSiK/M2DyyORX5GEtXD9rKYxTMHC2VxsNWARPMFJGzgeW2ugYkFv+rKI1/c0bs0CJ4e+qFnOlBsQXZvyQ==", + "dependencies": { + "@material/animation": "15.0.0-canary.684e33d25.0", + "@material/base": "15.0.0-canary.684e33d25.0", + "@material/dom": "15.0.0-canary.684e33d25.0", + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "@material/tab": "15.0.0-canary.684e33d25.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/textfield": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/textfield/-/textfield-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-OvgpDXjvpyJTtAWskO69IDybFvDNzr9w2PN/Fk7yFm+uNVupaWz1Ew8lZ4gGslaTNSVmh2XcsvmzxcLINSiiNg==", + "dependencies": { + "@material/animation": "15.0.0-canary.684e33d25.0", + "@material/base": "15.0.0-canary.684e33d25.0", + "@material/density": "15.0.0-canary.684e33d25.0", + "@material/dom": "15.0.0-canary.684e33d25.0", + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "@material/floating-label": "15.0.0-canary.684e33d25.0", + "@material/line-ripple": "15.0.0-canary.684e33d25.0", + "@material/notched-outline": "15.0.0-canary.684e33d25.0", + "@material/ripple": "15.0.0-canary.684e33d25.0", + "@material/rtl": "15.0.0-canary.684e33d25.0", + "@material/shape": "15.0.0-canary.684e33d25.0", + "@material/theme": "15.0.0-canary.684e33d25.0", + "@material/tokens": "15.0.0-canary.684e33d25.0", + "@material/typography": "15.0.0-canary.684e33d25.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/theme": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/theme/-/theme-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-AZxaXXAvRKzAi20RlMxzt2U5UmkCWyv7DMWEBXsxtG5Tk54mi1HsbVUp3fxDPTlmL7Pq8p1/DESg/o7TgRCVlw==", + "dependencies": { + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/tokens": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/tokens/-/tokens-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-wVwbQOTCXDPKYPdHQHLr026y36MMFelID1CmbfRk6mSol4O8yE9U0fXcShfRDW8Qo5E3X31w9c2A6T3neJY7wQ==", + "dependencies": { + "@material/elevation": "15.0.0-canary.684e33d25.0" + } + }, + "node_modules/@material/tooltip": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/tooltip/-/tooltip-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-dtm26QjxyQdinc8btgz6yys07b7bUW4FZgNF2EBPeGrICrPg7jf+JEvDziz5g8VMaTBQLOQRSCGy0MKuRlOjLw==", + "dependencies": { + "@material/animation": "15.0.0-canary.684e33d25.0", + "@material/base": "15.0.0-canary.684e33d25.0", + "@material/button": "15.0.0-canary.684e33d25.0", + "@material/dom": "15.0.0-canary.684e33d25.0", + "@material/elevation": "15.0.0-canary.684e33d25.0", + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "@material/rtl": "15.0.0-canary.684e33d25.0", + "@material/shape": "15.0.0-canary.684e33d25.0", + "@material/theme": "15.0.0-canary.684e33d25.0", + "@material/tokens": "15.0.0-canary.684e33d25.0", + "@material/typography": "15.0.0-canary.684e33d25.0", + "safevalues": "^0.3.4", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/top-app-bar": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/top-app-bar/-/top-app-bar-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-1M+oupUxflfW7u81P1XlxoLZB8bLzwtpKofIfDNRbEsiKhlLTERJR3Yak3BGE9xakNMysAaBHlkb5MrN5bNPFw==", + "dependencies": { + "@material/animation": "15.0.0-canary.684e33d25.0", + "@material/base": "15.0.0-canary.684e33d25.0", + "@material/elevation": "15.0.0-canary.684e33d25.0", + "@material/ripple": "15.0.0-canary.684e33d25.0", + "@material/rtl": "15.0.0-canary.684e33d25.0", + "@material/shape": "15.0.0-canary.684e33d25.0", + "@material/theme": "15.0.0-canary.684e33d25.0", + "@material/typography": "15.0.0-canary.684e33d25.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/touch-target": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/touch-target/-/touch-target-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-zdE69Slg8+T7sTn1OwqZ6H7WBYac9mxJ/JlJqfTqthzIjZRcCxBSYymQJcDHjsrPnUojOtr9U4Tpm5YZ96TEkQ==", + "dependencies": { + "@material/base": "15.0.0-canary.684e33d25.0", + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "@material/rtl": "15.0.0-canary.684e33d25.0", + "@material/theme": "15.0.0-canary.684e33d25.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/typography": { + "version": "15.0.0-canary.684e33d25.0", + "resolved": "https://registry.npmjs.org/@material/typography/-/typography-15.0.0-canary.684e33d25.0.tgz", + "integrity": "sha512-aVnvgMwcfNa/K4wujzpKDIxjGl2hbkEL+m+OKDSQqWYjKcP9QrbzCXJruJBqxrBoPRHLbqo47k5f9uT8raSgjw==", + "dependencies": { + "@material/feature-targeting": "15.0.0-canary.684e33d25.0", + "@material/theme": "15.0.0-canary.684e33d25.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@ng-idle/core": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/@ng-idle/core/-/core-14.0.0.tgz", + "integrity": "sha512-DqseA+F6LXm/Hr4D8zQhDBliuAU06t9BOUpcdSUKQWdDEKo3mlovRxz7DMuXyzg4OBQ0saQpE3MeMqakIEHUHw==", + "dependencies": { + "tslib": "^2.0.0" + }, + "peerDependencies": { + "@angular/common": ">=9.0.0", + "@angular/core": ">=9.0.0" + } + }, + "node_modules/@ngtools/webpack": { + "version": "15.2.11", + "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-15.2.11.tgz", + "integrity": "sha512-yqp+FziuJ+wIVij4eTqfhuiTPNaG1PU8ukeGOdqkVH4nQMlmzs9UldXy1iYC/6swzn6XO/pkqisU3m/jxemMzA==", + "dev": true, + "engines": { + "node": "^14.20.0 || ^16.13.0 || >=18.10.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "@angular/compiler-cli": "^15.0.0", + "typescript": ">=4.8.2 <5.0", + "webpack": "^5.54.0" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@npmcli/fs": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-3.1.1.tgz", + "integrity": "sha512-q9CRWjpHCMIh5sVyefoD1cA7PkvILqCZsnSOEUUivORLjxCO/Irmue2DprETiNgEqktDBZaM1Bi+jrarx1XdCg==", + "dev": true, + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/git": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@npmcli/git/-/git-4.1.0.tgz", + "integrity": "sha512-9hwoB3gStVfa0N31ymBmrX+GuDGdVA/QWShZVqE0HK2Af+7QGGrCTbZia/SW0ImUTjTne7SP91qxDmtXvDHRPQ==", + "dev": true, + "dependencies": { + "@npmcli/promise-spawn": "^6.0.0", + "lru-cache": "^7.4.4", + "npm-pick-manifest": "^8.0.0", + "proc-log": "^3.0.0", + "promise-inflight": "^1.0.1", + "promise-retry": "^2.0.1", + "semver": "^7.3.5", + "which": "^3.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/git/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/@npmcli/git/node_modules/which": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/which/-/which-3.0.1.tgz", + "integrity": "sha512-XA1b62dzQzLfaEOSQFTCOd5KFf/1VSzZo7/7TUjnya6u0vGGKzU96UQBZTAThCb2j4/xjBAyii1OhRLJEivHvg==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/installed-package-contents": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@npmcli/installed-package-contents/-/installed-package-contents-2.1.0.tgz", + "integrity": "sha512-c8UuGLeZpm69BryRykLuKRyKFZYJsZSCT4aVY5ds4omyZqJ172ApzgfKJ5eV/r3HgLdUYgFVe54KSFVjKoe27w==", + "dev": true, + "dependencies": { + "npm-bundled": "^3.0.0", + "npm-normalize-package-bin": "^3.0.0" + }, + "bin": { + "installed-package-contents": "bin/index.js" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/move-file": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@npmcli/move-file/-/move-file-2.0.1.tgz", + "integrity": "sha512-mJd2Z5TjYWq/ttPLLGqArdtnC74J6bOzg4rMDnN+p1xTacZ2yPRCk2y0oSWQtygLR9YVQXgOcONrwtnk3JupxQ==", + "deprecated": "This functionality has been moved to @npmcli/fs", + "dev": true, + "dependencies": { + "mkdirp": "^1.0.4", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/@npmcli/move-file/node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true, + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@npmcli/node-gyp": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/node-gyp/-/node-gyp-3.0.0.tgz", + "integrity": "sha512-gp8pRXC2oOxu0DUE1/M3bYtb1b3/DbJ5aM113+XJBgfXdussRAsX0YOrOhdd8WvnAR6auDBvJomGAkLKA5ydxA==", + "dev": true, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/promise-spawn": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@npmcli/promise-spawn/-/promise-spawn-6.0.2.tgz", + "integrity": "sha512-gGq0NJkIGSwdbUt4yhdF8ZrmkGKVz9vAdVzpOfnom+V8PLSmSOVhZwbNvZZS1EYcJN5hzzKBxmmVVAInM6HQLg==", + "dev": true, + "dependencies": { + "which": "^3.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/promise-spawn/node_modules/which": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/which/-/which-3.0.1.tgz", + "integrity": "sha512-XA1b62dzQzLfaEOSQFTCOd5KFf/1VSzZo7/7TUjnya6u0vGGKzU96UQBZTAThCb2j4/xjBAyii1OhRLJEivHvg==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/run-script": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@npmcli/run-script/-/run-script-6.0.2.tgz", + "integrity": "sha512-NCcr1uQo1k5U+SYlnIrbAh3cxy+OQT1VtqiAbxdymSlptbzBb62AjH2xXgjNCoP073hoa1CfCAcwoZ8k96C4nA==", + "dev": true, + "dependencies": { + "@npmcli/node-gyp": "^3.0.0", + "@npmcli/promise-spawn": "^6.0.0", + "node-gyp": "^9.0.0", + "read-package-json-fast": "^3.0.0", + "which": "^3.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/run-script/node_modules/which": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/which/-/which-3.0.1.tgz", + "integrity": "sha512-XA1b62dzQzLfaEOSQFTCOd5KFf/1VSzZo7/7TUjnya6u0vGGKzU96UQBZTAThCb2j4/xjBAyii1OhRLJEivHvg==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@pkgr/core": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.1.2.tgz", + "integrity": "sha512-fdDH1LSGfZdTH2sxdpVMw31BanV28K/Gry0cVFxaNP77neJSkd82mM8ErPNYs9e+0O7SdHBLTDzDgwUuy18RnQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/unts" + } + }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==" + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==" + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==" + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==" + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", + "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1", + "@protobufjs/inquire": "^1.1.0" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==" + }, + "node_modules/@protobufjs/inquire": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", + "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==" + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==" + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==" + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", + "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==" + }, + "node_modules/@schematics/angular": { + "version": "15.1.6", + "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-15.1.6.tgz", + "integrity": "sha512-y2kIQ1wJL0wR6v/LM5+PFJUivrYtdaIJVRdOXLLWl0AB5aLwObiWgLzAuBsbGm/9//WPPhw9PglS5EFFxTBDzg==", + "dependencies": { + "@angular-devkit/core": "15.1.6", + "@angular-devkit/schematics": "15.1.6", + "jsonc-parser": "3.2.0" + }, + "engines": { + "node": "^14.20.0 || ^16.13.0 || >=18.10.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@schematics/angular/node_modules/@angular-devkit/core": { + "version": "15.1.6", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-15.1.6.tgz", + "integrity": "sha512-jGgxyRjecVf6lEyqDxz7ltMEndNPxIg720pk6r40fgsu0dU8w9vjJSJe7k0XdJiXVRcN6wZa/J5nO/xcwWVIsA==", + "dependencies": { + "ajv": "8.12.0", + "ajv-formats": "2.1.1", + "jsonc-parser": "3.2.0", + "rxjs": "6.6.7", + "source-map": "0.7.4" + }, + "engines": { + "node": "^14.20.0 || ^16.13.0 || >=18.10.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "chokidar": "^3.5.2" + }, + "peerDependenciesMeta": { + "chokidar": { + "optional": true + } + } + }, + "node_modules/@schematics/angular/node_modules/rxjs": { + "version": "6.6.7", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", + "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", + "dependencies": { + "tslib": "^1.9.0" + }, + "engines": { + "npm": ">=2.0.0" + } + }, + "node_modules/@schematics/angular/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + }, + "node_modules/@socket.io/component-emitter": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", + "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", + "dev": true + }, + "node_modules/@stencil/core": { + "version": "4.19.2", + "resolved": "https://registry.npmjs.org/@stencil/core/-/core-4.19.2.tgz", + "integrity": "sha512-ZdnbHmHEl8E5vN0GWDtONe5w6j3CrSqqxZM4hNLBPkV/aouWKug7D5/Mi6RazfYO5U4fmHQYLwMz60rHcx0G4g==", + "bin": { + "stencil": "bin/stencil" + }, + "engines": { + "node": ">=16.0.0", + "npm": ">=7.10.0" + } + }, + "node_modules/@tootallnate/once": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", + "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", + "dev": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/@types/body-parser": { + "version": "1.19.5", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", + "integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==", + "dev": true, + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/bonjour": { + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@types/bonjour/-/bonjour-3.5.13.tgz", + "integrity": "sha512-z9fJ5Im06zvUL548KvYNecEVlA7cVDkGUi6kZusb04mpyEFKCIZJvloCcmpmLaIahDpOQGHaHmG6imtPMmPXGQ==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/connect-history-api-fallback": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.5.4.tgz", + "integrity": "sha512-n6Cr2xS1h4uAulPRdlw6Jl6s1oG8KrVilPN2yUITEs+K48EzMJJ3W1xy8K5eWuFvjp3R74AOIGSmp2UfBJ8HFw==", + "dev": true, + "dependencies": { + "@types/express-serve-static-core": "*", + "@types/node": "*" + } + }, + "node_modules/@types/cookie": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.1.tgz", + "integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==", + "dev": true + }, + "node_modules/@types/cors": { + "version": "2.8.17", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz", + "integrity": "sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/eslint": { + "version": "8.56.10", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.10.tgz", + "integrity": "sha512-Shavhk87gCtY2fhXDctcfS3e6FdxWkCx1iUZ9eEUbh7rTqlZT0/IzOkCOVt0fCjcFuZ9FPYfuezTBImfHCDBGQ==", + "dependencies": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "node_modules/@types/eslint-scope": { + "version": "3.7.7", + "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", + "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", + "dependencies": { + "@types/eslint": "*", + "@types/estree": "*" + } + }, + "node_modules/@types/estree": { + "version": "0.0.51", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.51.tgz", + "integrity": "sha512-CuPgU6f3eT/XgKKPqKd/gLZV1Xmvf1a2R5POBOGQa6uv82xpls89HU5zKeVoyR8XzHd1RGNOlQlvUe3CFkjWNQ==" + }, + "node_modules/@types/express": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz", + "integrity": "sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==", + "dev": true, + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.33", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "4.19.5", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.5.tgz", + "integrity": "sha512-y6W03tvrACO72aijJ5uF02FRq5cgDR9lUxddQ8vyF+GvmjJQqbzDcJngEjURc+ZsG31VI3hODNZJ2URj86pzmg==", + "dev": true, + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/http-errors": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", + "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==", + "dev": true + }, + "node_modules/@types/http-proxy": { + "version": "1.17.14", + "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.14.tgz", + "integrity": "sha512-SSrD0c1OQzlFX7pGu1eXxSEjemej64aaNPRhhVYUGqXh0BtldAAx37MG8btcumvpgKyZp1F5Gn3JkktdxiFv6w==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/jasmine": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/@types/jasmine/-/jasmine-4.3.6.tgz", + "integrity": "sha512-3N0FpQTeiWjm+Oo1WUYWguUS7E6JLceiGTriFrG8k5PU7zRLJCzLcWURU3wjMbZGS//a2/LgjsnO3QxIlwxt9g==", + "dev": true + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==" + }, + "node_modules/@types/long": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz", + "integrity": "sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==" + }, + "node_modules/@types/marked": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@types/marked/-/marked-4.3.2.tgz", + "integrity": "sha512-a79Yc3TOk6dGdituy8hmTTJXjOkZ7zsFYV10L337ttq/rec8lRMDBpV7fL3uLx6TgbFCa5DU/h8FmIBQPSbU0w==" + }, + "node_modules/@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "dev": true + }, + "node_modules/@types/minimist": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-hov8bUuiLiyFPGyFPE1lwWhmzYbirOXQNNo40+y3zow8aFVTeyn3VWL0VFFfdNddA8S4Vf0Tc062rzyNr7Paag==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.15.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.3.tgz", + "integrity": "sha512-lX7HFZeHf4QG/J7tBZqrCAXwz9J5RD56Y6MpP0eJkka8p+K0RY/yBTW7CYFJ4VGCclxqOLKmiGP5juQc6MKgcw==", + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/node-forge": { + "version": "1.3.11", + "resolved": "https://registry.npmjs.org/@types/node-forge/-/node-forge-1.3.11.tgz", + "integrity": "sha512-FQx220y22OKNTqaByeBGqHWYz4cl94tpcxeFdvBo3wjG6XPBuZ0BNgNZRV5J5TFmmcsJ4IzsLkmGRiQbnYsBEQ==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/normalize-package-data": { + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz", + "integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/parse-json": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", + "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==", + "dev": true + }, + "node_modules/@types/qs": { + "version": "6.9.15", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.15.tgz", + "integrity": "sha512-uXHQKES6DQKKCLh441Xv/dwxOq1TVS3JPUMlEqoEglvlhR6Mxnlew/Xq/LRVHpLyk7iK3zODe1qYHIMltO7XGg==", + "dev": true + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true + }, + "node_modules/@types/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==", + "dev": true + }, + "node_modules/@types/semver": { + "version": "7.7.0", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.7.0.tgz", + "integrity": "sha512-k107IF4+Xr7UHjwDc7Cfd6PRQfbdkiRabXGRjo07b4WyPahFBZCZ1sE+BNxYIJPPg73UkfOsVOLwqVc/6ETrIA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/send": { + "version": "0.17.4", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", + "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==", + "dev": true, + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/serve-index": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@types/serve-index/-/serve-index-1.9.4.tgz", + "integrity": "sha512-qLpGZ/c2fhSs5gnYsQxtDEq3Oy8SXPClIXkW5ghvAvsNuVSA8k+gCONcUCS/UjLEYvYps+e8uBtfgXgvhwfNug==", + "dev": true, + "dependencies": { + "@types/express": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.7", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.7.tgz", + "integrity": "sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==", + "dev": true, + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "*" + } + }, + "node_modules/@types/sockjs": { + "version": "0.3.36", + "resolved": "https://registry.npmjs.org/@types/sockjs/-/sockjs-0.3.36.tgz", + "integrity": "sha512-MK9V6NzAS1+Ud7JV9lJLFqW85VbC9dq3LmwZCuBe4wBDgKC0Kj/jd8Xl+nSviU+Qc3+m7umHHyHg//2KSa0a0Q==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/triple-beam": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.5.tgz", + "integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==" + }, + "node_modules/@types/uuid": { + "version": "9.0.8", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.8.tgz", + "integrity": "sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==", + "dev": true + }, + "node_modules/@types/ws": { + "version": "8.5.10", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.10.tgz", + "integrity": "sha512-vmQSUcfalpIq0R9q7uTo2lXs6eGIpt9wtnLdMv9LVpIjCA/+ufZRozlVoVelIYixx1ugCBKDhn89vnsEGOCx9A==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.62.0.tgz", + "integrity": "sha512-TiZzBSJja/LbhNPvk6yc0JrX9XqhQ0hdh6M2svYfsHGejaKFIAGd9MQ+ERIMzLGlN/kZoYIgdxFV0PuljTKXag==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.4.0", + "@typescript-eslint/scope-manager": "5.62.0", + "@typescript-eslint/type-utils": "5.62.0", + "@typescript-eslint/utils": "5.62.0", + "debug": "^4.3.4", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "natural-compare-lite": "^1.4.0", + "semver": "^7.3.7", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^5.0.0", + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.62.0.tgz", + "integrity": "sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/scope-manager": "5.62.0", + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/typescript-estree": "5.62.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.62.0.tgz", + "integrity": "sha512-VXuvVvZeQCQb5Zgf4HAxc04q5j+WrNAtNh9OwCsCgpKqESMTu3tF/jhZ3xG6T4NZwWl65Bg8KuS2uEvhSfLl0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/visitor-keys": "5.62.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.62.0.tgz", + "integrity": "sha512-xsSQreu+VnfbqQpW5vnCJdq1Z3Q0U31qiWmRhr98ONQmcp/yhiPJFPq8MXiJVLiksmOKSjIldZzkebzHuCGzew==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/typescript-estree": "5.62.0", + "@typescript-eslint/utils": "5.62.0", + "debug": "^4.3.4", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/types": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.62.0.tgz", + "integrity": "sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.62.0.tgz", + "integrity": "sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/visitor-keys": "5.62.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "semver": "^7.3.7", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/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/@typescript-eslint/typescript-estree/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/@typescript-eslint/utils": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.62.0.tgz", + "integrity": "sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@types/json-schema": "^7.0.9", + "@types/semver": "^7.3.12", + "@typescript-eslint/scope-manager": "5.62.0", + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/typescript-estree": "5.62.0", + "eslint-scope": "^5.1.1", + "semver": "^7.3.7" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.62.0.tgz", + "integrity": "sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "5.62.0", + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", + "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", + "dev": true, + "license": "ISC" + }, + "node_modules/@webassemblyjs/ast": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.1.tgz", + "integrity": "sha512-ukBh14qFLjxTQNTXocdyksN5QdM28S1CxHt2rdskFyL+xFV7VremuBLVbmCePj+URalXBENx/9Lm7lnhihtCSw==", + "dependencies": { + "@webassemblyjs/helper-numbers": "1.11.1", + "@webassemblyjs/helper-wasm-bytecode": "1.11.1" + } + }, + "node_modules/@webassemblyjs/floating-point-hex-parser": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.1.tgz", + "integrity": "sha512-iGRfyc5Bq+NnNuX8b5hwBrRjzf0ocrJPI6GWFodBFzmFnyvrQ83SHKhmilCU/8Jv67i4GJZBMhEzltxzcNagtQ==" + }, + "node_modules/@webassemblyjs/helper-api-error": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.1.tgz", + "integrity": "sha512-RlhS8CBCXfRUR/cwo2ho9bkheSXG0+NwooXcc3PAILALf2QLdFyj7KGsKRbVc95hZnhnERon4kW/D3SZpp6Tcg==" + }, + "node_modules/@webassemblyjs/helper-buffer": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.1.tgz", + "integrity": "sha512-gwikF65aDNeeXa8JxXa2BAk+REjSyhrNC9ZwdT0f8jc4dQQeDQ7G4m0f2QCLPJiMTTO6wfDmRmj/pW0PsUvIcA==" + }, + "node_modules/@webassemblyjs/helper-numbers": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.1.tgz", + "integrity": "sha512-vDkbxiB8zfnPdNK9Rajcey5C0w+QJugEglN0of+kmO8l7lDb77AnlKYQF7aarZuCrv+l0UvqL+68gSDr3k9LPQ==", + "dependencies": { + "@webassemblyjs/floating-point-hex-parser": "1.11.1", + "@webassemblyjs/helper-api-error": "1.11.1", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/helper-wasm-bytecode": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.1.tgz", + "integrity": "sha512-PvpoOGiJwXeTrSf/qfudJhwlvDQxFgelbMqtq52WWiXC6Xgg1IREdngmPN3bs4RoO83PnL/nFrxucXj1+BX62Q==" + }, + "node_modules/@webassemblyjs/helper-wasm-section": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.1.tgz", + "integrity": "sha512-10P9No29rYX1j7F3EVPX3JvGPQPae+AomuSTPiF9eBQeChHI6iqjMIwR9JmOJXwpnn/oVGDk7I5IlskuMwU/pg==", + "dependencies": { + "@webassemblyjs/ast": "1.11.1", + "@webassemblyjs/helper-buffer": "1.11.1", + "@webassemblyjs/helper-wasm-bytecode": "1.11.1", + "@webassemblyjs/wasm-gen": "1.11.1" + } + }, + "node_modules/@webassemblyjs/ieee754": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.11.1.tgz", + "integrity": "sha512-hJ87QIPtAMKbFq6CGTkZYJivEwZDbQUgYd3qKSadTNOhVY7p+gfP6Sr0lLRVTaG1JjFj+r3YchoqRYxNH3M0GQ==", + "dependencies": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "node_modules/@webassemblyjs/leb128": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.11.1.tgz", + "integrity": "sha512-BJ2P0hNZ0u+Th1YZXJpzW6miwqQUGcIHT1G/sf72gLVD9DZ5AdYTqPNbHZh6K1M5VmKvFXwGSWZADz+qBWxeRw==", + "dependencies": { + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/utf8": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.11.1.tgz", + "integrity": "sha512-9kqcxAEdMhiwQkHpkNiorZzqpGrodQQ2IGrHHxCy+Ozng0ofyMA0lTqiLkVs1uzTRejX+/O0EOT7KxqVPuXosQ==" + }, + "node_modules/@webassemblyjs/wasm-edit": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.1.tgz", + "integrity": "sha512-g+RsupUC1aTHfR8CDgnsVRVZFJqdkFHpsHMfJuWQzWU3tvnLC07UqHICfP+4XyL2tnr1amvl1Sdp06TnYCmVkA==", + "dependencies": { + "@webassemblyjs/ast": "1.11.1", + "@webassemblyjs/helper-buffer": "1.11.1", + "@webassemblyjs/helper-wasm-bytecode": "1.11.1", + "@webassemblyjs/helper-wasm-section": "1.11.1", + "@webassemblyjs/wasm-gen": "1.11.1", + "@webassemblyjs/wasm-opt": "1.11.1", + "@webassemblyjs/wasm-parser": "1.11.1", + "@webassemblyjs/wast-printer": "1.11.1" + } + }, + "node_modules/@webassemblyjs/wasm-gen": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.1.tgz", + "integrity": "sha512-F7QqKXwwNlMmsulj6+O7r4mmtAlCWfO/0HdgOxSklZfQcDu0TpLiD1mRt/zF25Bk59FIjEuGAIyn5ei4yMfLhA==", + "dependencies": { + "@webassemblyjs/ast": "1.11.1", + "@webassemblyjs/helper-wasm-bytecode": "1.11.1", + "@webassemblyjs/ieee754": "1.11.1", + "@webassemblyjs/leb128": "1.11.1", + "@webassemblyjs/utf8": "1.11.1" + } + }, + "node_modules/@webassemblyjs/wasm-opt": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.1.tgz", + "integrity": "sha512-VqnkNqnZlU5EB64pp1l7hdm3hmQw7Vgqa0KF/KCNO9sIpI6Fk6brDEiX+iCOYrvMuBWDws0NkTOxYEb85XQHHw==", + "dependencies": { + "@webassemblyjs/ast": "1.11.1", + "@webassemblyjs/helper-buffer": "1.11.1", + "@webassemblyjs/wasm-gen": "1.11.1", + "@webassemblyjs/wasm-parser": "1.11.1" + } + }, + "node_modules/@webassemblyjs/wasm-parser": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.1.tgz", + "integrity": "sha512-rrBujw+dJu32gYB7/Lup6UhdkPx9S9SnobZzRVL7VcBH9Bt9bCBLEuX/YXOOtBsOZ4NQrRykKhffRWHvigQvOA==", + "dependencies": { + "@webassemblyjs/ast": "1.11.1", + "@webassemblyjs/helper-api-error": "1.11.1", + "@webassemblyjs/helper-wasm-bytecode": "1.11.1", + "@webassemblyjs/ieee754": "1.11.1", + "@webassemblyjs/leb128": "1.11.1", + "@webassemblyjs/utf8": "1.11.1" + } + }, + "node_modules/@webassemblyjs/wast-printer": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.11.1.tgz", + "integrity": "sha512-IQboUWM4eKzWW+N/jij2sRatKMh99QEelo3Eb2q0qXkvPRISAj8Qxtmw5itwqK+TTkBuUIE45AxYPToqPtL5gg==", + "dependencies": { + "@webassemblyjs/ast": "1.11.1", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@xtuc/ieee754": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==" + }, + "node_modules/@xtuc/long": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==" + }, + "node_modules/@yarnpkg/lockfile": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz", + "integrity": "sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ==", + "dev": true + }, + "node_modules/abab": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", + "integrity": "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==", + "deprecated": "Use your platform's native atob() and btoa() methods instead", + "dev": true + }, + "node_modules/abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "dev": true + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dev": true, + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.12.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", + "integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-import-assertions": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.9.0.tgz", + "integrity": "sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA==", + "peerDependencies": { + "acorn": "^8" + } + }, + "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/adjust-sourcemap-loader": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/adjust-sourcemap-loader/-/adjust-sourcemap-loader-4.0.0.tgz", + "integrity": "sha512-OXwN5b9pCUXNQHJpwwD2qP40byEmSgzj8B4ydSN0uMNYWiFmJ6x6KwUllMmfk8Rwu/HJDFR7U8ubsWBoN0Xp0A==", + "dev": true, + "dependencies": { + "loader-utils": "^2.0.0", + "regex-parser": "^2.2.11" + }, + "engines": { + "node": ">=8.9" + } + }, + "node_modules/adjust-sourcemap-loader/node_modules/loader-utils": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", + "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", + "dev": true, + "dependencies": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" + }, + "engines": { + "node": ">=8.9.0" + } + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dev": true, + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/agentkeepalive": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.5.0.tgz", + "integrity": "sha512-5GG/5IbQQpC9FpkRGsSvZI5QYeSCzlJHdpBQntCsuTOxhKD8lqKhrleg2Yi7yvMIf82Ycmmqln9U8V9qwEiJew==", + "dev": true, + "dependencies": { + "humanize-ms": "^1.2.1" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/aggregate-error": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", + "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "dev": true, + "dependencies": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ajv": { + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", + "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/ansi-colors": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-html-community": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/ansi-html-community/-/ansi-html-community-0.0.8.tgz", + "integrity": "sha512-1APHAyr3+PCamwNw3bXCPp4HFLONZt/yIH0sZp0/469KWNTEy+qN5jQ3GVX6DMZ1UXAi34yVwtTeaG/HpBuuzw==", + "dev": true, + "engines": [ + "node >= 0.8.0" + ], + "bin": { + "ansi-html": "bin/ansi-html" + } + }, + "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==", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "devOptional": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/aproba": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", + "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==", + "dev": true + }, + "node_modules/are-we-there-yet": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-3.0.1.tgz", + "integrity": "sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg==", + "deprecated": "This package is no longer supported.", + "dev": true, + "dependencies": { + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "dev": true + }, + "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/arrify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", + "integrity": "sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/async": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz", + "integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==" + }, + "node_modules/autoprefixer": { + "version": "10.4.13", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.13.tgz", + "integrity": "sha512-49vKpMqcZYsJjwotvt4+h/BCjJVnhGwcLpDt5xkcaOG3eLrG/HUYLagrihYsQ+qrIBgIzX1Rw7a6L8I/ZA1Atg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + } + ], + "dependencies": { + "browserslist": "^4.21.4", + "caniuse-lite": "^1.0.30001426", + "fraction.js": "^4.2.0", + "normalize-range": "^0.1.2", + "picocolors": "^1.0.0", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/babel-loader": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-9.1.2.tgz", + "integrity": "sha512-mN14niXW43tddohGl8HPu5yfQq70iUThvFL/4QzESA7GcZoC0eVOhvWdQ8+3UlSjaDE9MVtsW9mxDY07W7VpVA==", + "dev": true, + "dependencies": { + "find-cache-dir": "^3.3.2", + "schema-utils": "^4.0.0" + }, + "engines": { + "node": ">= 14.15.0" + }, + "peerDependencies": { + "@babel/core": "^7.12.0", + "webpack": ">=5" + } + }, + "node_modules/babel-plugin-istanbul": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", + "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-instrument": "^5.0.4", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-polyfill-corejs2": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.3.3.tgz", + "integrity": "sha512-8hOdmFYFSZhqg2C/JgLUQ+t52o5nirNwaWM2B9LWteozwIvM14VSwdsCAUET10qT+kmySAlseadmfeeSWFCy+Q==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.17.7", + "@babel/helper-define-polyfill-provider": "^0.3.3", + "semver": "^6.1.1" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/babel-plugin-polyfill-corejs2/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/babel-plugin-polyfill-corejs3": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.6.0.tgz", + "integrity": "sha512-+eHqR6OPcBhJOGgsIar7xoAB1GcSwVUA3XjAd7HJNzOXT4wv6/H7KIdA/Nc60cvUlDbKApmqNvD1B1bzOt4nyA==", + "dev": true, + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.3.3", + "core-js-compat": "^3.25.1" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/babel-plugin-polyfill-regenerator": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.4.1.tgz", + "integrity": "sha512-NtQGmyQDXjQqQ+IzRkBVwEOz9lQ4zxAQZgoAYEtU9dJjnl1Oc98qnN7jcp+bE7O7aYzVpavXE3/VKXNzUbh7aw==", + "dev": true, + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.3.3" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/base64id": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", + "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==", + "dev": true, + "engines": { + "node": "^4.5.0 || >= 5.9" + } + }, + "node_modules/batch": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz", + "integrity": "sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw==", + "dev": true + }, + "node_modules/big.js": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", + "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", + "engines": { + "node": "*" + } + }, + "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==", + "devOptional": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/body-parser": { + "version": "1.20.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", + "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", + "dev": true, + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.11.0", + "raw-body": "2.5.2", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/body-parser/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/body-parser/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "node_modules/bonjour-service": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/bonjour-service/-/bonjour-service-1.2.1.tgz", + "integrity": "sha512-oSzCS2zV14bh2kji6vNe7vrpJYCHGvcZnlffFQ1MEoX/WOeQ/teD8SYWKR942OI3INjq8OMNJlbPK5LLLUxFDw==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.3", + "multicast-dns": "^7.2.5" + } + }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "dev": true + }, + "node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "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==", + "devOptional": true, + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.21.5", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.5.tgz", + "integrity": "sha512-tUkiguQGW7S3IhB7N+c2MV/HZPSCPAAiYBZXLsBhFB/PCy6ZKKsZrmBayHV9fdGV/ARIfJ14NkxKzRDjvp7L6w==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + } + ], + "dependencies": { + "caniuse-lite": "^1.0.30001449", + "electron-to-chromium": "^1.4.284", + "node-releases": "^2.0.8", + "update-browserslist-db": "^1.0.10" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" + }, + "node_modules/builtins": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/builtins/-/builtins-5.1.0.tgz", + "integrity": "sha512-SW9lzGTLvWTP1AY8xeAMZimqDrIaSdLQUcVr9DMef51niJ022Ri87SwRRKYm4A6iHfkPaiVUu/Duw2Wc4J7kKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.0.0" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/cacache": { + "version": "17.0.4", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-17.0.4.tgz", + "integrity": "sha512-Z/nL3gU+zTUjz5pCA5vVjYM8pmaw2kxM7JEiE0fv3w77Wj+sFbi70CrBruUWH0uNcEdvLDixFpgA2JM4F4DBjA==", + "dev": true, + "dependencies": { + "@npmcli/fs": "^3.1.0", + "fs-minipass": "^3.0.0", + "glob": "^8.0.1", + "lru-cache": "^7.7.1", + "minipass": "^4.0.0", + "minipass-collect": "^1.0.2", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "p-map": "^4.0.0", + "promise-inflight": "^1.0.1", + "ssri": "^10.0.0", + "tar": "^6.1.11", + "unique-filename": "^3.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/cacache/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/call-bind": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", + "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", + "dev": true, + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase-keys": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-6.2.2.tgz", + "integrity": "sha512-YrwaA0vEKazPBkn0ipTiMpSajYDSe+KjQfrjhcBMxJt/znbvlHd8Pw/Vamaz5EB4Wfhs3SUR3Z9mwRu/P3s3Yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "camelcase": "^5.3.1", + "map-obj": "^4.0.0", + "quick-lru": "^4.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001640", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001640.tgz", + "integrity": "sha512-lA4VMpW0PSUrFnkmVuEKBUovSWKhj7puyCg8StBChgu298N1AtuF1sKWEvfDuimSEDbhlb/KqPKC3fs1HbuQUA==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ] + }, + "node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/chardet": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", + "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==" + }, + "node_modules/chokidar": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "devOptional": true, + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ], + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/chrome-trace-event": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", + "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", + "engines": { + "node": ">=6.0" + } + }, + "node_modules/clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "dependencies": { + "restore-cursor": "^3.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cli-spinners": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", + "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-width": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-3.0.0.tgz", + "integrity": "sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==", + "engines": { + "node": ">= 10" + } + }, + "node_modules/clipboard": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/clipboard/-/clipboard-2.0.11.tgz", + "integrity": "sha512-C+0bbOqkezLIsmWSvlsXS0Q0bmkugu7jcfMIACB+RDEntIzQIkdr148we28AfSloQLRdZlYL/QYyrq05j/3Faw==", + "dependencies": { + "good-listener": "^1.2.2", + "select": "^1.1.2", + "tiny-emitter": "^2.0.0" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/clone": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/clone-deep": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", + "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", + "dev": true, + "dependencies": { + "is-plain-object": "^2.0.4", + "kind-of": "^6.0.2", + "shallow-clone": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/color": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/color/-/color-3.2.1.tgz", + "integrity": "sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==", + "dependencies": { + "color-convert": "^1.9.3", + "color-string": "^1.6.0" + } + }, + "node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" + }, + "node_modules/color-string": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", + "dependencies": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, + "node_modules/color-support": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", + "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", + "dev": true, + "bin": { + "color-support": "bin.js" + } + }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true + }, + "node_modules/colorspace": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/colorspace/-/colorspace-1.1.4.tgz", + "integrity": "sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w==", + "dependencies": { + "color": "^3.1.3", + "text-hex": "1.0.x" + } + }, + "node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" + }, + "node_modules/commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", + "dev": true + }, + "node_modules/compressible": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", + "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", + "dev": true, + "dependencies": { + "mime-db": ">= 1.43.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/compression": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.7.4.tgz", + "integrity": "sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==", + "dev": true, + "dependencies": { + "accepts": "~1.3.5", + "bytes": "3.0.0", + "compressible": "~2.0.16", + "debug": "2.6.9", + "on-headers": "~1.0.2", + "safe-buffer": "5.1.2", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/compression/node_modules/bytes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", + "integrity": "sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/compression/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/compression/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "node_modules/compression/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "node_modules/connect": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/connect/-/connect-3.7.0.tgz", + "integrity": "sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ==", + "dev": true, + "dependencies": { + "debug": "2.6.9", + "finalhandler": "1.1.2", + "parseurl": "~1.3.3", + "utils-merge": "1.0.1" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/connect-history-api-fallback": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-2.0.0.tgz", + "integrity": "sha512-U73+6lQFmfiNPrYbXqr6kZ1i1wiRqXnp2nhMsINseWXO8lDau0LGEffJ8kQi4EjLZympVgRdvqjAgiZ1tgzDDA==", + "dev": true, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/connect/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/connect/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "node_modules/console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==", + "dev": true + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "dev": true, + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "dev": true + }, + "node_modules/cookie": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz", + "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "dev": true + }, + "node_modules/copy-anything": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-2.0.6.tgz", + "integrity": "sha512-1j20GZTsvKNkc4BY3NpMOM8tt///wY3FpIzozTOFO2ffuZcV61nojHXVKIy3WM+7ADCy5FVhdZYHYDdgTU0yJw==", + "dev": true, + "dependencies": { + "is-what": "^3.14.1" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, + "node_modules/copy-webpack-plugin": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-11.0.0.tgz", + "integrity": "sha512-fX2MWpamkW0hZxMEg0+mYnA40LTosOSa5TqZ9GYIBzyJa9C3QUaMPSE2xAi/buNr8u89SfD9wHSQVBzrRa/SOQ==", + "dev": true, + "dependencies": { + "fast-glob": "^3.2.11", + "glob-parent": "^6.0.1", + "globby": "^13.1.1", + "normalize-path": "^3.0.0", + "schema-utils": "^4.0.0", + "serialize-javascript": "^6.0.0" + }, + "engines": { + "node": ">= 14.15.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + } + }, + "node_modules/copy-webpack-plugin/node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/core-js-compat": { + "version": "3.37.1", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.37.1.tgz", + "integrity": "sha512-9TNiImhKvQqSUkOvk/mMRZzOANTiEVC7WaBNhHcKM7x+/5E1l5NvsysR19zuDQScE8k+kfQXWRN3AtS/eOSHpg==", + "dev": true, + "dependencies": { + "browserslist": "^4.23.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "node_modules/core-js-compat/node_modules/browserslist": { + "version": "4.23.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.1.tgz", + "integrity": "sha512-TUfofFo/KsK/bWZ9TWQ5O26tsWW4Uhmt8IYklbnUa70udB6P2wA7w7o4PY4muaEPBQaAX+CEnmmIA41NVHtPVw==", + "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" + } + ], + "dependencies": { + "caniuse-lite": "^1.0.30001629", + "electron-to-chromium": "^1.4.796", + "node-releases": "^2.0.14", + "update-browserslist-db": "^1.0.16" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "dev": true + }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "dev": true, + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/cose-base": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/cose-base/-/cose-base-1.0.3.tgz", + "integrity": "sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg==", + "optional": true, + "dependencies": { + "layout-base": "^1.0.0" + } + }, + "node_modules/cosmiconfig": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", + "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==", + "dev": true, + "dependencies": { + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.2.1", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.10.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/critters": { + "version": "0.0.16", + "resolved": "https://registry.npmjs.org/critters/-/critters-0.0.16.tgz", + "integrity": "sha512-JwjgmO6i3y6RWtLYmXwO5jMd+maZt8Tnfu7VVISmEWyQqfLpB8soBswf8/2bu6SBXxtKA68Al3c+qIG1ApT68A==", + "dev": true, + "dependencies": { + "chalk": "^4.1.0", + "css-select": "^4.2.0", + "parse5": "^6.0.1", + "parse5-htmlparser2-tree-adapter": "^6.0.1", + "postcss": "^8.3.7", + "pretty-bytes": "^5.3.0" + } + }, + "node_modules/critters/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/critters/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/critters/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/critters/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/critters/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/critters/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/cross-spawn/node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/css-loader": { + "version": "6.7.3", + "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-6.7.3.tgz", + "integrity": "sha512-qhOH1KlBMnZP8FzRO6YCH9UHXQhVMcEGLyNdb7Hv2cpcmJbW0YrddO+tG1ab5nT41KpHIYGsbeHqxB9xPu1pKQ==", + "dev": true, + "dependencies": { + "icss-utils": "^5.1.0", + "postcss": "^8.4.19", + "postcss-modules-extract-imports": "^3.0.0", + "postcss-modules-local-by-default": "^4.0.0", + "postcss-modules-scope": "^3.0.0", + "postcss-modules-values": "^4.0.0", + "postcss-value-parser": "^4.2.0", + "semver": "^7.3.8" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" + } + }, + "node_modules/css-select": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-4.3.0.tgz", + "integrity": "sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==", + "dev": true, + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.0.1", + "domhandler": "^4.3.1", + "domutils": "^2.8.0", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-what": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", + "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", + "dev": true, + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "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==", + "dev": true, + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/custom-event": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/custom-event/-/custom-event-1.0.1.tgz", + "integrity": "sha512-GAj5FOq0Hd+RsCGVJxZuKaIDXDf3h6GQoNEjFgbLLI/trgtavwUbSnZ5pVfg27DVCaWjIohryS0JFwIJyT2cMg==", + "dev": true + }, + "node_modules/cytoscape": { + "version": "3.30.0", + "resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.30.0.tgz", + "integrity": "sha512-l590mjTHT6/Cbxp13dGPC2Y7VXdgc+rUeF8AnF/JPzhjNevbDJfObnJgaSjlldOgBQZbue+X6IUZ7r5GAgvauQ==", + "optional": true, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/cytoscape-cose-bilkent": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cytoscape-cose-bilkent/-/cytoscape-cose-bilkent-4.1.0.tgz", + "integrity": "sha512-wgQlVIUJF13Quxiv5e1gstZ08rnZj2XaLHGoFMYXz7SkNfCDOOteKBE6SYRfA9WxxI/iBc3ajfDoc6hb/MRAHQ==", + "optional": true, + "dependencies": { + "cose-base": "^1.0.0" + }, + "peerDependencies": { + "cytoscape": "^3.2.0" + } + }, + "node_modules/cytoscape-fcose": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cytoscape-fcose/-/cytoscape-fcose-2.2.0.tgz", + "integrity": "sha512-ki1/VuRIHFCzxWNrsshHYPs6L7TvLu3DL+TyIGEsRcvVERmxokbf5Gdk7mFxZnTdiGtnA4cfSmjZJMviqSuZrQ==", + "optional": true, + "dependencies": { + "cose-base": "^2.2.0" + }, + "peerDependencies": { + "cytoscape": "^3.2.0" + } + }, + "node_modules/cytoscape-fcose/node_modules/cose-base": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cose-base/-/cose-base-2.2.0.tgz", + "integrity": "sha512-AzlgcsCbUMymkADOJtQm3wO9S3ltPfYOFD5033keQn9NJzIbtnZj+UdBJe7DYml/8TdbtHJW3j58SOnKhWY/5g==", + "optional": true, + "dependencies": { + "layout-base": "^2.0.0" + } + }, + "node_modules/cytoscape-fcose/node_modules/layout-base": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/layout-base/-/layout-base-2.0.1.tgz", + "integrity": "sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg==", + "optional": true + }, + "node_modules/d3": { + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/d3/-/d3-7.9.0.tgz", + "integrity": "sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==", + "optional": true, + "dependencies": { + "d3-array": "3", + "d3-axis": "3", + "d3-brush": "3", + "d3-chord": "3", + "d3-color": "3", + "d3-contour": "4", + "d3-delaunay": "6", + "d3-dispatch": "3", + "d3-drag": "3", + "d3-dsv": "3", + "d3-ease": "3", + "d3-fetch": "3", + "d3-force": "3", + "d3-format": "3", + "d3-geo": "3", + "d3-hierarchy": "3", + "d3-interpolate": "3", + "d3-path": "3", + "d3-polygon": "3", + "d3-quadtree": "3", + "d3-random": "3", + "d3-scale": "4", + "d3-scale-chromatic": "3", + "d3-selection": "3", + "d3-shape": "3", + "d3-time": "3", + "d3-time-format": "4", + "d3-timer": "3", + "d3-transition": "3", + "d3-zoom": "3" + }, + "engines": { + "node": ">=12" + } + }, + "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==", + "optional": true, + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-axis": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-3.0.0.tgz", + "integrity": "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==", + "optional": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-brush": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-3.0.0.tgz", + "integrity": "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==", + "optional": true, + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "3", + "d3-transition": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-chord": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-chord/-/d3-chord-3.0.1.tgz", + "integrity": "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==", + "optional": true, + "dependencies": { + "d3-path": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "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==", + "optional": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-contour": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-contour/-/d3-contour-4.0.2.tgz", + "integrity": "sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==", + "optional": true, + "dependencies": { + "d3-array": "^3.2.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-delaunay": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==", + "optional": true, + "dependencies": { + "delaunator": "5" + }, + "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==", + "optional": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-drag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", + "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", + "optional": true, + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-selection": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dsv": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-3.0.1.tgz", + "integrity": "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==", + "optional": true, + "dependencies": { + "commander": "7", + "iconv-lite": "0.6", + "rw": "1" + }, + "bin": { + "csv2json": "bin/dsv2json.js", + "csv2tsv": "bin/dsv2dsv.js", + "dsv2dsv": "bin/dsv2dsv.js", + "dsv2json": "bin/dsv2json.js", + "json2csv": "bin/json2dsv.js", + "json2dsv": "bin/json2dsv.js", + "json2tsv": "bin/json2dsv.js", + "tsv2csv": "bin/dsv2dsv.js", + "tsv2json": "bin/dsv2json.js" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dsv/node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "optional": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/d3-dsv/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "optional": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "optional": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-fetch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-fetch/-/d3-fetch-3.0.1.tgz", + "integrity": "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==", + "optional": true, + "dependencies": { + "d3-dsv": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-force": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-3.0.0.tgz", + "integrity": "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==", + "optional": true, + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-quadtree": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz", + "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==", + "optional": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-geo": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-3.1.1.tgz", + "integrity": "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==", + "optional": true, + "dependencies": { + "d3-array": "2.5.0 - 3" + }, + "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==", + "optional": true, + "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==", + "optional": true, + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "optional": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-polygon": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-3.0.1.tgz", + "integrity": "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==", + "optional": true, + "engines": { + "node": ">=12" + } + }, + "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==", + "optional": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-random": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-random/-/d3-random-3.0.1.tgz", + "integrity": "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==", + "optional": true, + "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==", + "optional": true, + "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-scale-chromatic": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", + "integrity": "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==", + "optional": true, + "dependencies": { + "d3-color": "1 - 3", + "d3-interpolate": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-selection": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", + "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", + "optional": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "optional": true, + "dependencies": { + "d3-path": "^3.1.0" + }, + "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==", + "optional": true, + "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==", + "optional": true, + "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==", + "optional": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-transition": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", + "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", + "optional": true, + "dependencies": { + "d3-color": "1 - 3", + "d3-dispatch": "1 - 3", + "d3-ease": "1 - 3", + "d3-interpolate": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "d3-selection": "2 - 3" + } + }, + "node_modules/d3-zoom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz", + "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", + "optional": true, + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "2 - 3", + "d3-transition": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/dagre-d3-es": { + "version": "7.0.9", + "resolved": "https://registry.npmjs.org/dagre-d3-es/-/dagre-d3-es-7.0.9.tgz", + "integrity": "sha512-rYR4QfVmy+sR44IBDvVtcAmOReGBvRCWDpO2QjYwqgh9yijw6eSHBqaPG/LIOEy7aBsniLvtMW6pg19qJhq60w==", + "optional": true, + "dependencies": { + "d3": "^7.8.2", + "lodash-es": "^4.17.21" + } + }, + "node_modules/date-format": { + "version": "4.0.14", + "resolved": "https://registry.npmjs.org/date-format/-/date-format-4.0.14.tgz", + "integrity": "sha512-39BOQLs9ZjKh0/patS9nrT8wc3ioX3/eA/zgbKNopnF2wCqJEoxywwwElATYvRsXdnOxA/OQeQoFZ3rFjVajhg==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/dayjs": { + "version": "1.11.11", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.11.tgz", + "integrity": "sha512-okzr3f11N6WuqYtZSvm+F776mB41wRZMhKP+hc34YdW+KmtYYK9iqvHSwo2k9FEH3fhGXvOPV6yz2IcSrfRUDg==", + "optional": true + }, + "node_modules/debug": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", + "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/decamelize-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/decamelize-keys/-/decamelize-keys-1.1.1.tgz", + "integrity": "sha512-WiPxgEirIV0/eIOMcnFBA3/IJZAZqKnwAwWyvvdi4lsr1WCN22nhdf/3db3DoZcUjTV2SqfzIwNyp6y2xs3nmg==", + "dev": true, + "license": "MIT", + "dependencies": { + "decamelize": "^1.1.0", + "map-obj": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/decamelize-keys/node_modules/map-obj": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz", + "integrity": "sha512-7N/q3lyZ+LVCp7PzuxrJr4KMbBE2hW7BT7YNia330OFxIf4d3r5zVpicP2650l7CPN6RM9zOJRl3NGpqSiw3Eg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "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/default-gateway": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/default-gateway/-/default-gateway-6.0.3.tgz", + "integrity": "sha512-fwSOJsbbNzZ/CUFpqFBqYfYNLj1NbMPm8MMCIzHjC83iSJRBEGmDUxU+WP661BaBQImeC2yHwXtz+P/O9o+XEg==", + "dev": true, + "dependencies": { + "execa": "^5.0.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/defaults": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", + "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==", + "dependencies": { + "clone": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-lazy-prop": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", + "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", + "engines": { + "node": ">=8" + } + }, + "node_modules/delaunator": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.0.1.tgz", + "integrity": "sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==", + "optional": true, + "dependencies": { + "robust-predicates": "^3.0.2" + } + }, + "node_modules/delegate": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/delegate/-/delegate-3.2.0.tgz", + "integrity": "sha512-IofjkYBZaZivn0V8nnsMJGBr4jVLxHDheKSW88PyxS5QC4Vo9ZbZVvhzlSxY87fVq3STR6r+4cGepyHkcWOQSw==" + }, + "node_modules/delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", + "dev": true + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/dependency-graph": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/dependency-graph/-/dependency-graph-0.11.0.tgz", + "integrity": "sha512-JeMq7fEshyepOWDfcfHK06N3MhyPhz++vtqWhMT5O9A3K42rdsEDpfdVqjaqaAhsw6a+ZqeDvQVtD0hFHQWrzg==", + "dev": true, + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "dev": true, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/detect-node": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", + "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", + "dev": true + }, + "node_modules/di": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/di/-/di-0.0.1.tgz", + "integrity": "sha512-uJaamHkagcZtHPqCIHZxnFrXlunQXgBOsZSUOWwFw31QJCAbyTBoHMW75YOTur5ZNx8pIeAKgf6GWIgaqqiLhA==", + "dev": true + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/dns-packet": { + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-5.6.1.tgz", + "integrity": "sha512-l4gcSouhcgIKRvyy99RNVOgxXiicE+2jZoNmaNmZ6JXiGajBOJAesk1OBlJuM5k2c+eudGdLxDqXuPCKIj6kpw==", + "dev": true, + "dependencies": { + "@leichtgewicht/ip-codec": "^2.0.1" + }, + "engines": { + "node": ">=6" + } + }, + "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/dom-serialize": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/dom-serialize/-/dom-serialize-2.2.1.tgz", + "integrity": "sha512-Yra4DbvoW7/Z6LBN560ZwXMjoNOSAN2wRsKFGc4iBeso+mpIA6qj1vfdf9HpMaKAqG6wXTy+1SYEzmNpKXOSsQ==", + "dev": true, + "dependencies": { + "custom-event": "~1.0.0", + "ent": "~2.2.0", + "extend": "^3.0.0", + "void-elements": "^2.0.0" + } + }, + "node_modules/dom-serializer": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz", + "integrity": "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==", + "dev": true, + "dependencies": { + "domelementtype": "^2.0.1", + "domhandler": "^4.2.0", + "entities": "^2.0.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ] + }, + "node_modules/domhandler": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz", + "integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==", + "dev": true, + "dependencies": { + "domelementtype": "^2.2.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/dompurify": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.4.3.tgz", + "integrity": "sha512-q6QaLcakcRjebxjg8/+NP+h0rPfatOgOzc46Fst9VAA3jF2ApfKBNKMzdP4DYTqtUMXSCd5pRS/8Po/OmoCHZQ==", + "optional": true + }, + "node_modules/domutils": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", + "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==", + "dev": true, + "dependencies": { + "dom-serializer": "^1.0.1", + "domelementtype": "^2.2.0", + "domhandler": "^4.2.0" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "dev": true + }, + "node_modules/electron-to-chromium": { + "version": "1.4.816", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.816.tgz", + "integrity": "sha512-EKH5X5oqC6hLmiS7/vYtZHZFTNdhsYG5NVPRN6Yn0kQHNBlT59+xSM8HBy66P5fxWpKgZbPqb+diC64ng295Jw==" + }, + "node_modules/elkjs": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/elkjs/-/elkjs-0.8.2.tgz", + "integrity": "sha512-L6uRgvZTH+4OF5NE/MBbzQx/WYpru1xCBE9respNj6qznEewGUIfhzmm7horWWxbNO2M0WckQypGctR8lH79xQ==", + "optional": true + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "node_modules/emoji-toolkit": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/emoji-toolkit/-/emoji-toolkit-7.0.1.tgz", + "integrity": "sha512-l5aJyAhpC5s4mDuoVuqt4SzVjwIsIvakPh4ZGJJE4KWuWFCEHaXacQFkStVdD9zbRR+/BbRXob7u99o0lQFr8A==", + "optional": true + }, + "node_modules/emojis-list": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", + "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", + "engines": { + "node": ">= 4" + } + }, + "node_modules/enabled": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz", + "integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==" + }, + "node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/encoding": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", + "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", + "optional": true, + "dependencies": { + "iconv-lite": "^0.6.2" + } + }, + "node_modules/encoding/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "optional": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/engine.io": { + "version": "6.5.5", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.5.5.tgz", + "integrity": "sha512-C5Pn8Wk+1vKBoHghJODM63yk8MvrO9EWZUfkAt5HAqIgPE4/8FF0PEGHXtEd40l223+cE5ABWuPzm38PHFXfMA==", + "dev": true, + "dependencies": { + "@types/cookie": "^0.4.1", + "@types/cors": "^2.8.12", + "@types/node": ">=10.0.0", + "accepts": "~1.3.4", + "base64id": "2.0.0", + "cookie": "~0.4.1", + "cors": "~2.8.5", + "debug": "~4.3.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.17.1" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/engine.io-parser": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.2.tgz", + "integrity": "sha512-RcyUFKA93/CXH20l4SoVvzZfrSDMOTUS3bWVpTt2FuFP+XYrL8i8oonHP7WInRyVHXh0n/ORtoeiE1os+8qkSw==", + "dev": true, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/enhanced-resolve": { + "version": "5.17.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.17.0.tgz", + "integrity": "sha512-dwDPwZL0dmye8Txp2gzFmA6sxALaSvdRDjPH0viLcKrtlOL3tw62nWWweVD1SdILDTJrbrL6tdWVN58Wo6U3eA==", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/ent": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ent/-/ent-2.2.1.tgz", + "integrity": "sha512-QHuXVeZx9d+tIQAz/XztU0ZwZf2Agg9CcXcgE1rurqvdBeDBrpSwjl8/6XUqMg7tw2Y7uAdKb2sRv+bSEFqQ5A==", + "dev": true, + "dependencies": { + "punycode": "^1.4.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/entities": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", + "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", + "dev": true, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/err-code": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", + "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", + "dev": true + }, + "node_modules/errno": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.8.tgz", + "integrity": "sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==", + "dev": true, + "optional": true, + "dependencies": { + "prr": "~1.0.1" + }, + "bin": { + "errno": "cli.js" + } + }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", + "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-module-lexer": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-0.9.3.tgz", + "integrity": "sha512-1HQ2M2sPtxwnvOvT1ZClHyQDiggdNjURWpY2we6aMKCQiUVxTmVs2UYPLIrD84sS+kMdUwfBSylbJPwNnBrnHQ==" + }, + "node_modules/esbuild": { + "version": "0.17.8", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.17.8.tgz", + "integrity": "sha512-g24ybC3fWhZddZK6R3uD2iF/RIPnRpwJAqLov6ouX3hMbY4+tKolP0VMF3zuIYCaXun+yHwS5IPQ91N2BT191g==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/android-arm": "0.17.8", + "@esbuild/android-arm64": "0.17.8", + "@esbuild/android-x64": "0.17.8", + "@esbuild/darwin-arm64": "0.17.8", + "@esbuild/darwin-x64": "0.17.8", + "@esbuild/freebsd-arm64": "0.17.8", + "@esbuild/freebsd-x64": "0.17.8", + "@esbuild/linux-arm": "0.17.8", + "@esbuild/linux-arm64": "0.17.8", + "@esbuild/linux-ia32": "0.17.8", + "@esbuild/linux-loong64": "0.17.8", + "@esbuild/linux-mips64el": "0.17.8", + "@esbuild/linux-ppc64": "0.17.8", + "@esbuild/linux-riscv64": "0.17.8", + "@esbuild/linux-s390x": "0.17.8", + "@esbuild/linux-x64": "0.17.8", + "@esbuild/netbsd-x64": "0.17.8", + "@esbuild/openbsd-x64": "0.17.8", + "@esbuild/sunos-x64": "0.17.8", + "@esbuild/win32-arm64": "0.17.8", + "@esbuild/win32-ia32": "0.17.8", + "@esbuild/win32-x64": "0.17.8" + } + }, + "node_modules/esbuild-wasm": { + "version": "0.17.8", + "resolved": "https://registry.npmjs.org/esbuild-wasm/-/esbuild-wasm-0.17.8.tgz", + "integrity": "sha512-zCmpxv95E0FuCmvdw1K836UHnj4EdiQnFfjTby35y3LAjRPtXMj3sbHDRHjbD8Mqg5lTwq3knacr/1qIFU51CQ==", + "dev": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/escalade": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", + "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "dev": true + }, + "node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/eslint": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz", + "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==", + "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.0", + "@humanwhocodes/config-array": "^0.11.14", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-config-prettier": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz", + "integrity": "sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==", + "dev": true, + "license": "MIT", + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-plugin-es": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-es/-/eslint-plugin-es-4.1.0.tgz", + "integrity": "sha512-GILhQTnjYE2WorX5Jyi5i4dz5ALWxBIdQECVQavL6s7cI76IZTDWleTHkxz/QT3kvcs2QlGHvKLYsSlPOlPXnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-utils": "^2.0.0", + "regexpp": "^3.0.0" + }, + "engines": { + "node": ">=8.10.0" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + }, + "peerDependencies": { + "eslint": ">=4.19.1" + } + }, + "node_modules/eslint-plugin-es/node_modules/eslint-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.1.0.tgz", + "integrity": "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^1.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + } + }, + "node_modules/eslint-plugin-es/node_modules/eslint-visitor-keys": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", + "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=4" + } + }, + "node_modules/eslint-plugin-n": { + "version": "15.7.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-n/-/eslint-plugin-n-15.7.0.tgz", + "integrity": "sha512-jDex9s7D/Qial8AGVIHq4W7NswpUD5DPDL2RH8Lzd9EloWUuvUkHfv4FRLMipH5q2UtyurorBkPeNi1wVWNh3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "builtins": "^5.0.1", + "eslint-plugin-es": "^4.1.0", + "eslint-utils": "^3.0.0", + "ignore": "^5.1.1", + "is-core-module": "^2.11.0", + "minimatch": "^3.1.2", + "resolve": "^1.22.1", + "semver": "^7.3.8" + }, + "engines": { + "node": ">=12.22.0" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-plugin-n/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint-plugin-n/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/eslint-plugin-prettier": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.1.3.tgz", + "integrity": "sha512-C9GCVAs4Eq7ZC/XFQHITLiHJxQngdtraXaM+LoUFoFp/lHNl2Zn8f3WQbe9HvTBBQ9YnKFB0/2Ajdqwo5D1EAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "prettier-linter-helpers": "^1.0.0", + "synckit": "^0.8.6" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint-plugin-prettier" + }, + "peerDependencies": { + "@types/eslint": ">=8.0.0", + "eslint": ">=8.0.0", + "eslint-config-prettier": "*", + "prettier": ">=3.0.0" + }, + "peerDependenciesMeta": { + "@types/eslint": { + "optional": true + }, + "eslint-config-prettier": { + "optional": true + } + } + }, + "node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/eslint-utils": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz", + "integrity": "sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^2.0.0" + }, + "engines": { + "node": "^10.0.0 || ^12.0.0 || >= 14.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + }, + "peerDependencies": { + "eslint": ">=5" + } + }, + "node_modules/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", + "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10" + } + }, + "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/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "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/eslint/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/eslint/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/eslint/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "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/eslint/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/eslint/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/eslint/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/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/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/eslint/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/eslint/node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/eslint/node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/eslint/node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/eslint/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/eslint/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/eslint/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/eslint/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/eslint/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/eslint/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/eslint/node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "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/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esquery/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/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esrecurse/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/eventemitter-asyncresource": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/eventemitter-asyncresource/-/eventemitter-asyncresource-1.0.0.tgz", + "integrity": "sha512-39F7TBIV0G7gTelxwbEqnwhp90eqCPON1k0NwNfwhgKn4Co4ybUbj2pECcXT0B3ztRKZ7Pw1JujUUgmQJHcVAQ==", + "dev": true + }, + "node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "dev": true + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/exponential-backoff": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.1.tgz", + "integrity": "sha512-dX7e/LHVJ6W3DE1MHWi9S1EYzDESENfLrYohG2G++ovZrYOkm4Knwa0mc1cn84xJOR4KEU0WSchhLbd0UklbHw==", + "dev": true + }, + "node_modules/express": { + "version": "4.19.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz", + "integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==", + "dev": true, + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.2", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.6.0", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.2.0", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.7", + "qs": "6.11.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.18.0", + "serve-static": "1.15.0", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/express/node_modules/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/express/node_modules/finalhandler": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", + "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", + "dev": true, + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/express/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "node_modules/express/node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "dev": true + }, + "node_modules/external-editor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", + "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", + "dependencies": { + "chardet": "^0.7.0", + "iconv-lite": "^0.4.24", + "tmp": "^0.0.33" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" + }, + "node_modules/fast-diff": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz", + "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/fast-glob": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", + "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-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==" + }, + "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.17.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", + "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", + "dev": true, + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/faye-websocket": { + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz", + "integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==", + "dependencies": { + "websocket-driver": ">=0.5.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/fecha": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz", + "integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==" + }, + "node_modules/figures": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", + "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", + "dependencies": { + "escape-string-regexp": "^1.0.5" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/file-loader": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/file-loader/-/file-loader-6.2.0.tgz", + "integrity": "sha512-qo3glqyTa61Ytg4u73GultjHGjdRyig3tG6lPtyX/jOEJvHif9uB0/OCI2Kif6ctF3caQTW2G5gym21oAsI4pw==", + "dependencies": { + "loader-utils": "^2.0.0", + "schema-utils": "^3.0.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^4.0.0 || ^5.0.0" + } + }, + "node_modules/file-loader/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "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/file-loader/node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "node_modules/file-loader/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==" + }, + "node_modules/file-loader/node_modules/loader-utils": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", + "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", + "dependencies": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" + }, + "engines": { + "node": ">=8.9.0" + } + }, + "node_modules/file-loader/node_modules/schema-utils": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "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==", + "devOptional": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", + "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", + "dev": true, + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "statuses": "~1.5.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/finalhandler/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/finalhandler/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "node_modules/finalhandler/node_modules/on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==", + "dev": true, + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/find-cache-dir": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.2.tgz", + "integrity": "sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==", + "dev": true, + "dependencies": { + "commondir": "^1.0.1", + "make-dir": "^3.0.2", + "pkg-dir": "^4.1.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/avajs/find-cache-dir?sponsor=1" + } + }, + "node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase": { + "version": "9.23.0", + "resolved": "https://registry.npmjs.org/firebase/-/firebase-9.23.0.tgz", + "integrity": "sha512-/4lUVY0lUvBDIaeY1q6dUYhS8Sd18Qb9CgWkPZICUo9IXpJNCEagfNZXBBFCkMTTN5L5gx2Hjr27y21a9NzUcA==", + "dependencies": { + "@firebase/analytics": "0.10.0", + "@firebase/analytics-compat": "0.2.6", + "@firebase/app": "0.9.13", + "@firebase/app-check": "0.8.0", + "@firebase/app-check-compat": "0.3.7", + "@firebase/app-compat": "0.2.13", + "@firebase/app-types": "0.9.0", + "@firebase/auth": "0.23.2", + "@firebase/auth-compat": "0.4.2", + "@firebase/database": "0.14.4", + "@firebase/database-compat": "0.3.4", + "@firebase/firestore": "3.13.0", + "@firebase/firestore-compat": "0.3.12", + "@firebase/functions": "0.10.0", + "@firebase/functions-compat": "0.3.5", + "@firebase/installations": "0.6.4", + "@firebase/installations-compat": "0.2.4", + "@firebase/messaging": "0.12.4", + "@firebase/messaging-compat": "0.2.4", + "@firebase/performance": "0.6.4", + "@firebase/performance-compat": "0.2.4", + "@firebase/remote-config": "0.4.4", + "@firebase/remote-config-compat": "0.2.4", + "@firebase/storage": "0.11.2", + "@firebase/storage-compat": "0.3.2", + "@firebase/util": "1.9.3" + } + }, + "node_modules/firebase-tools": { + "version": "11.30.0", + "resolved": "https://registry.npmjs.org/firebase-tools/-/firebase-tools-11.30.0.tgz", + "integrity": "sha512-rNMhtsFKy35AZqfy0obY2Kgsfmg8N1duEx2UDT14NsMHB7xt2Z7cUd92bkAoQAfD0jAOd7LhRT/7E6J5yB+kGw==", + "devOptional": true, + "hasShrinkwrap": true, + "dependencies": { + "@google-cloud/pubsub": "^3.0.1", + "abort-controller": "^3.0.0", + "ajv": "^6.12.6", + "archiver": "^5.0.0", + "async-lock": "1.3.2", + "body-parser": "^1.19.0", + "chokidar": "^3.0.2", + "cjson": "^0.3.1", + "cli-table": "0.3.11", + "colorette": "^2.0.19", + "commander": "^4.0.1", + "configstore": "^5.0.1", + "cors": "^2.8.5", + "cross-env": "^5.1.3", + "cross-spawn": "^7.0.3", + "csv-parse": "^5.0.4", + "exegesis": "^4.1.0", + "exegesis-express": "^4.0.0", + "express": "^4.16.4", + "filesize": "^6.1.0", + "form-data": "^4.0.0", + "fs-extra": "^10.1.0", + "glob": "^7.1.2", + "google-auth-library": "^7.11.0", + "inquirer": "^8.2.0", + "js-yaml": "^3.13.1", + "jsonwebtoken": "^9.0.0", + "leven": "^3.1.0", + "libsodium-wrappers": "^0.7.10", + "lodash": "^4.17.21", + "marked": "^4.0.14", + "marked-terminal": "^5.1.1", + "mime": "^2.5.2", + "minimatch": "^3.0.4", + "morgan": "^1.10.0", + "node-fetch": "^2.6.7", + "open": "^6.3.0", + "ora": "^5.4.1", + "p-limit": "^3.0.1", + "portfinder": "^1.0.32", + "progress": "^2.0.3", + "proxy-agent": "^5.0.0", + "request": "^2.87.0", + "retry": "^0.13.1", + "rimraf": "^3.0.0", + "semver": "^5.7.1", + "stream-chain": "^2.2.4", + "stream-json": "^1.7.3", + "strip-ansi": "^6.0.1", + "superstatic": "^9.0.3", + "tar": "^6.1.11", + "tcp-port-used": "^1.0.2", + "tmp": "^0.2.1", + "triple-beam": "^1.3.0", + "universal-analytics": "^0.5.3", + "update-notifier-cjs": "^5.1.6", + "uuid": "^8.3.2", + "winston": "^3.0.0", + "winston-transport": "^4.4.0", + "ws": "^7.2.3" + }, + "bin": { + "firebase": "lib/bin/firebase.js" + }, + "engines": { + "node": "^14.18.0 || >=16.4.0" + } + }, + "node_modules/firebase-tools/node_modules/@apidevtools/json-schema-ref-parser": { + "version": "9.0.7", + "resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-9.0.7.tgz", + "integrity": "sha512-QdwOGF1+eeyFh+17v2Tz626WX0nucd1iKOm6JUTUvCZdbolblCOOQCxGrQPY0f7jEhn36PiAWqZnsC2r5vmUWg==", + "devOptional": true, + "dependencies": { + "@jsdevtools/ono": "^7.1.3", + "call-me-maybe": "^1.0.1", + "js-yaml": "^3.13.1" + } + }, + "node_modules/firebase-tools/node_modules/@babel/parser": { + "version": "7.21.4", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.21.4.tgz", + "integrity": "sha512-alVJj7k7zIxqBZ7BTRhz0IqJFxW1VJbm6N8JbcYhQ186df9ZBPbZBmWSqAMXwHGsCJdYks7z/voa3ibiS5bCIw==", + "devOptional": true, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/firebase-tools/node_modules/@colors/colors": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", + "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==", + "optional": true, + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/firebase-tools/node_modules/@gar/promisify": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz", + "integrity": "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==", + "optional": true + }, + "node_modules/firebase-tools/node_modules/@google-cloud/precise-date": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@google-cloud/precise-date/-/precise-date-2.0.3.tgz", + "integrity": "sha512-+SDJ3ZvGkF7hzo6BGa8ZqeK3F6Z4+S+KviC9oOK+XCs3tfMyJCh/4j93XIWINgMMDIh9BgEvlw4306VxlXIlYA==", + "devOptional": true, + "engines": { + "node": ">=10.4.0" + } + }, + "node_modules/firebase-tools/node_modules/@google-cloud/projectify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@google-cloud/projectify/-/projectify-2.0.1.tgz", + "integrity": "sha512-ZDG38U/Yy6Zr21LaR3BTiiLtpJl6RkPS/JwoRT453G+6Q1DhlV0waNf8Lfu+YVYGIIxgKnLayJRfYlFJfiI8iQ==", + "devOptional": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/firebase-tools/node_modules/@google-cloud/promisify": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@google-cloud/promisify/-/promisify-2.0.3.tgz", + "integrity": "sha512-d4VSA86eL/AFTe5xtyZX+ePUjE8dIFu2T8zmdeNBSa5/kNgXPCx/o/wbFNHAGLJdGnk1vddRuMESD9HbOC8irw==", + "devOptional": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/firebase-tools/node_modules/@google-cloud/pubsub": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@google-cloud/pubsub/-/pubsub-3.0.1.tgz", + "integrity": "sha512-dznNbRd/Y8J0C0xvdvCPi3B1msK/dj/Nya+NQZ2doUOLT6eoa261tBwk9umOQs5L5GKcdlqQKbBjrNjDYVbzQA==", + "devOptional": true, + "dependencies": { + "@google-cloud/paginator": "^4.0.0", + "@google-cloud/precise-date": "^2.0.0", + "@google-cloud/projectify": "^2.0.0", + "@google-cloud/promisify": "^2.0.0", + "@opentelemetry/api": "^1.0.0", + "@opentelemetry/semantic-conventions": "^1.0.0", + "@types/duplexify": "^3.6.0", + "@types/long": "^4.0.0", + "arrify": "^2.0.0", + "extend": "^3.0.2", + "google-auth-library": "^8.0.2", + "google-gax": "^3.0.1", + "is-stream-ended": "^0.1.4", + "lodash.snakecase": "^4.1.1", + "p-defer": "^3.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/firebase-tools/node_modules/@google-cloud/pubsub/node_modules/@google-cloud/paginator": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@google-cloud/paginator/-/paginator-4.0.0.tgz", + "integrity": "sha512-wNmCZl+2G2DmgT/VlF+AROf80SoaC/CwS8trwmjNaq26VRNK8yPbU5F/Vy+R9oDAGKWQU2k8+Op5H4kFJVXFaQ==", + "devOptional": true, + "dependencies": { + "arrify": "^2.0.0", + "extend": "^3.0.2" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/firebase-tools/node_modules/@google-cloud/pubsub/node_modules/gaxios": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-5.0.0.tgz", + "integrity": "sha512-VD/yc5ln6XU8Ch1hyYY6kRMBE0Yc2np3fPyeJeYHhrPs1i8rgnsApPMWyrugkl7LLoSqpOJVBWlQIa87OAvt8Q==", + "devOptional": true, + "dependencies": { + "abort-controller": "^3.0.0", + "extend": "^3.0.2", + "https-proxy-agent": "^5.0.0", + "is-stream": "^2.0.0", + "node-fetch": "^2.6.7" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/firebase-tools/node_modules/@google-cloud/pubsub/node_modules/gcp-metadata": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-5.0.0.tgz", + "integrity": "sha512-gfwuX3yA3nNsHSWUL4KG90UulNiq922Ukj3wLTrcnX33BB7PwB1o0ubR8KVvXu9nJH+P5w1j2SQSNNqto+H0DA==", + "devOptional": true, + "dependencies": { + "gaxios": "^5.0.0", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/firebase-tools/node_modules/@google-cloud/pubsub/node_modules/google-auth-library": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-8.0.2.tgz", + "integrity": "sha512-HoG+nWFAThLovKpvcbYzxgn+nBJPTfAwtq0GxPN821nOO+21+8oP7MoEHfd1sbDulUFFGfcjJr2CnJ4YssHcyg==", + "devOptional": true, + "dependencies": { + "arrify": "^2.0.0", + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "fast-text-encoding": "^1.0.0", + "gaxios": "^5.0.0", + "gcp-metadata": "^5.0.0", + "gtoken": "^5.3.2", + "jws": "^4.0.0", + "lru-cache": "^6.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/firebase-tools/node_modules/@google-cloud/pubsub/node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "devOptional": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/firebase-tools/node_modules/@google-cloud/pubsub/node_modules/jwa": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz", + "integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==", + "devOptional": true, + "dependencies": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/firebase-tools/node_modules/@google-cloud/pubsub/node_modules/jws": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", + "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", + "devOptional": true, + "dependencies": { + "jwa": "^2.0.0", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/firebase-tools/node_modules/@grpc/grpc-js": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.7.3.tgz", + "integrity": "sha512-H9l79u4kJ2PVSxUNA08HMYAnUBLj9v6KjYQ7SQ71hOZcEXhShE/y5iQCesP8+6/Ik/7i2O0a10bPquIcYfufog==", + "devOptional": true, + "dependencies": { + "@grpc/proto-loader": "^0.7.0", + "@types/node": ">=12.12.47" + }, + "engines": { + "node": "^8.13.0 || >=10.10.0" + } + }, + "node_modules/firebase-tools/node_modules/@grpc/grpc-js/node_modules/@grpc/proto-loader": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.4.tgz", + "integrity": "sha512-MnWjkGwqQ3W8fx94/c1CwqLsNmHHv2t0CFn+9++6+cDphC1lolpg9M2OU0iebIjK//pBNX9e94ho+gjx6vz39w==", + "devOptional": true, + "dependencies": { + "@types/long": "^4.0.1", + "lodash.camelcase": "^4.3.0", + "long": "^4.0.0", + "protobufjs": "^7.0.0", + "yargs": "^16.2.0" + }, + "bin": { + "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/firebase-tools/node_modules/@grpc/grpc-js/node_modules/protobufjs": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.1.2.tgz", + "integrity": "sha512-4ZPTPkXCdel3+L81yw3dG6+Kq3umdWKh7Dc7GW/CpNk4SX3hK58iPCWeCyhVTDrbkNeKrYNZ7EojM5WDaEWTLQ==", + "devOptional": true, + "hasInstallScript": true, + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/node": ">=13.7.0", + "long": "^5.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/firebase-tools/node_modules/@grpc/grpc-js/node_modules/protobufjs/node_modules/long": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/long/-/long-5.2.1.tgz", + "integrity": "sha512-GKSNGeNAtw8IryjjkhZxuKB3JzlcLTwjtiQCHKvqQet81I93kXslhDQruGI/QsddO83mcDToBVy7GqGS/zYf/A==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/@jsdevtools/ono": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz", + "integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/@jsdoc/salty": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@jsdoc/salty/-/salty-0.2.3.tgz", + "integrity": "sha512-bbtCxCkxcnWhi50I+4Lj6mdz9w3pOXOgEQrID8TCZ/DF51fW7M9GCQW2y45SpBDdHd1Eirm1X/Cf6CkAAe8HPg==", + "devOptional": true, + "dependencies": { + "lodash": "^4.17.21" + }, + "engines": { + "node": ">=v12.0.0" + } + }, + "node_modules/firebase-tools/node_modules/@npmcli/fs": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-2.1.2.tgz", + "integrity": "sha512-yOJKRvohFOaLqipNtwYB9WugyZKhC/DZC4VYPmpaCzDBrA8YpK3qHZ8/HGscMnE4GqbkLNuVcCnxkeQEdGt6LQ==", + "optional": true, + "dependencies": { + "@gar/promisify": "^1.1.3", + "semver": "^7.3.5" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/firebase-tools/node_modules/@npmcli/fs/node_modules/semver": { + "version": "7.3.7", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz", + "integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==", + "optional": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/firebase-tools/node_modules/@npmcli/move-file": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@npmcli/move-file/-/move-file-2.0.1.tgz", + "integrity": "sha512-mJd2Z5TjYWq/ttPLLGqArdtnC74J6bOzg4rMDnN+p1xTacZ2yPRCk2y0oSWQtygLR9YVQXgOcONrwtnk3JupxQ==", + "optional": true, + "dependencies": { + "mkdirp": "^1.0.4", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/firebase-tools/node_modules/@npmcli/move-file/node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "optional": true, + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/firebase-tools/node_modules/@opentelemetry/api": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.0.4.tgz", + "integrity": "sha512-BuJuXRSJNQ3QoKA6GWWDyuLpOUck+9hAXNMCnrloc1aWVoy6Xq6t9PUV08aBZ4Lutqq2LEHM486bpZqoViScog==", + "devOptional": true, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/firebase-tools/node_modules/@opentelemetry/semantic-conventions": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.2.0.tgz", + "integrity": "sha512-BNKB9fiYVghALJzCuWO3eNYfdTExPVK4ykrtmfNfy0A6UWYhOYjGMXifUmkunDJNL8ju9tBobo8jF0WR9zGy1Q==", + "devOptional": true, + "engines": { + "node": ">=8.12.0" + } + }, + "node_modules/firebase-tools/node_modules/@pnpm/network.ca-file": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@pnpm/network.ca-file/-/network.ca-file-1.0.2.tgz", + "integrity": "sha512-YcPQ8a0jwYU9bTdJDpXjMi7Brhkr1mXsXrUJvjqM2mQDgkRiz8jFaQGOdaLxgjtUfQgZhKy/O3cG/YwmgKaxLA==", + "devOptional": true, + "dependencies": { + "graceful-fs": "4.2.10" + }, + "engines": { + "node": ">=12.22.0" + } + }, + "node_modules/firebase-tools/node_modules/@pnpm/npm-conf": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@pnpm/npm-conf/-/npm-conf-1.0.5.tgz", + "integrity": "sha512-hD8ml183638O3R6/Txrh0L8VzGOrFXgRtRDG4qQC4tONdZ5Z1M+tlUUDUvrjYdmK6G+JTBTeaCLMna11cXzi8A==", + "devOptional": true, + "dependencies": { + "@pnpm/network.ca-file": "^1.0.1", + "config-chain": "^1.1.11" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/firebase-tools/node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha1-m4sMxmPWaafY9vXQiToU00jzD78=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/@protobufjs/codegen": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/@protobufjs/eventemitter": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha1-NVy8mLr61ZePntCV85diHx0Ga3A=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/@protobufjs/fetch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", + "integrity": "sha1-upn7WYYUr2VwDBYZ/wbUVLDYTEU=", + "devOptional": true, + "dependencies": { + "@protobufjs/aspromise": "^1.1.1", + "@protobufjs/inquire": "^1.1.0" + } + }, + "node_modules/firebase-tools/node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha1-Xp4avctz/Ap8uLKR33jIy9l7h9E=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/@protobufjs/inquire": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", + "integrity": "sha1-/yAOPnzyQp4tyvwRQIKOjMY48Ik=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha1-bMKyDFya1q0NzP0hynZz2Nf79o0=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha1-Cf0V8tbTq/qbZbw2ZQbWrXhG/1Q=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/@protobufjs/utf8": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", + "integrity": "sha1-p3c2C1s5oaLlEG+OhY8v0tBgxXA=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/@tootallnate/once": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", + "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==", + "devOptional": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/firebase-tools/node_modules/@types/duplexify": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/@types/duplexify/-/duplexify-3.6.0.tgz", + "integrity": "sha512-5zOA53RUlzN74bvrSGwjudssD9F3a797sDZQkiYpUOxW+WHaXTCPz4/d5Dgi6FKnOqZ2CpaTo0DhgIfsXAOE/A==", + "devOptional": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/firebase-tools/node_modules/@types/linkify-it": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-3.0.2.tgz", + "integrity": "sha512-HZQYqbiFVWufzCwexrvh694SOim8z2d+xJl5UNamcvQFejLY/2YUtzXHYi3cHdI7PMlS8ejH2slRAOJQ32aNbA==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/@types/long": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.1.tgz", + "integrity": "sha512-5tXH6Bx/kNGd3MgffdmP4dy2Z+G4eaXw0SE81Tq3BNadtnMR5/ySMzX4SLEzHJzSmPNn4HIdpQsBvXMUykr58w==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/@types/markdown-it": { + "version": "12.2.3", + "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-12.2.3.tgz", + "integrity": "sha512-GKMHFfv3458yYy+v/N8gjufHO6MSZKCOXpZc5GXIWWy8uldwfmPn98vp81gZ5f9SVw8YYBctgfJ22a2d7AOMeQ==", + "devOptional": true, + "dependencies": { + "@types/linkify-it": "*", + "@types/mdurl": "*" + } + }, + "node_modules/firebase-tools/node_modules/@types/mdurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-1.0.2.tgz", + "integrity": "sha512-eC4U9MlIcu2q0KQmXszyn5Akca/0jrQmwDRgpAMJai7qBWq4amIQhZyNau4VYGtCeALvW1/NtjzJJ567aZxfKA==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/@types/node": { + "version": "14.18.36", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.36.tgz", + "integrity": "sha512-FXKWbsJ6a1hIrRxv+FoukuHnGTgEzKYGi7kilfMae96AL9UNkPFNWJEEYWzdRI9ooIkbr4AKldyuSTLql06vLQ==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "optional": true + }, + "node_modules/firebase-tools/node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "devOptional": true, + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, + "node_modules/firebase-tools/node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "devOptional": true, + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/firebase-tools/node_modules/acorn": { + "version": "8.8.2", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz", + "integrity": "sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==", + "devOptional": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/firebase-tools/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==", + "devOptional": true, + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/firebase-tools/node_modules/acorn-walk": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz", + "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==", + "devOptional": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/firebase-tools/node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "devOptional": true, + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/firebase-tools/node_modules/agent-base/node_modules/debug": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "devOptional": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/firebase-tools/node_modules/agent-base/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/agentkeepalive": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.2.1.tgz", + "integrity": "sha512-Zn4cw2NEqd+9fiSVWMscnjyQ1a8Yfoc5oBajLeo5w+YBHgDUcEBY2hS4YpTz6iN5f/2zQiktcuM6tS8x1p9dpA==", + "optional": true, + "dependencies": { + "debug": "^4.1.0", + "depd": "^1.1.2", + "humanize-ms": "^1.2.1" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/firebase-tools/node_modules/agentkeepalive/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "optional": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/firebase-tools/node_modules/agentkeepalive/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "optional": true + }, + "node_modules/firebase-tools/node_modules/aggregate-error": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", + "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "optional": true, + "dependencies": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "devOptional": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/firebase-tools/node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "devOptional": true, + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/firebase-tools/node_modules/ajv-formats/node_modules/ajv": { + "version": "8.8.2", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.8.2.tgz", + "integrity": "sha512-x9VuX+R/jcFj1DHo/fCp99esgGDWiHENrKxaCENuCxpoMCmAt/COCGVDwA7kleEpEzJjDnvh3yGoOuLu0Dtllw==", + "devOptional": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/firebase-tools/node_modules/ajv-formats/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/ansi-align": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.1.tgz", + "integrity": "sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==", + "devOptional": true, + "dependencies": { + "string-width": "^4.1.0" + } + }, + "node_modules/firebase-tools/node_modules/ansi-escapes": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-5.0.0.tgz", + "integrity": "sha512-5GFMVX8HqE/TB+FuBJGuO5XG0WrsA6ptUqoODaT/n9mmUaZFkqnBueB4leqGBCmrUHnCnC4PCZTCd0E7QQ83bA==", + "devOptional": true, + "dependencies": { + "type-fest": "^1.0.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/firebase-tools/node_modules/ansi-escapes/node_modules/type-fest": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-1.4.0.tgz", + "integrity": "sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==", + "devOptional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/firebase-tools/node_modules/ansicolors": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/ansicolors/-/ansicolors-0.3.2.tgz", + "integrity": "sha1-ZlWX3oap/+Oqm/vmyuXG6kJrSXk=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/anymatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", + "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==", + "devOptional": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/firebase-tools/node_modules/aproba": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", + "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==", + "optional": true + }, + "node_modules/firebase-tools/node_modules/archiver": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/archiver/-/archiver-5.3.1.tgz", + "integrity": "sha512-8KyabkmbYrH+9ibcTScQ1xCJC/CGcugdVIwB+53f5sZziXgwUh3iXlAlANMxcZyDEfTHMe6+Z5FofV8nopXP7w==", + "devOptional": true, + "dependencies": { + "archiver-utils": "^2.1.0", + "async": "^3.2.3", + "buffer-crc32": "^0.2.1", + "readable-stream": "^3.6.0", + "readdir-glob": "^1.0.0", + "tar-stream": "^2.2.0", + "zip-stream": "^4.1.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/firebase-tools/node_modules/archiver-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-2.1.0.tgz", + "integrity": "sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw==", + "devOptional": true, + "dependencies": { + "glob": "^7.1.4", + "graceful-fs": "^4.2.0", + "lazystream": "^1.0.0", + "lodash.defaults": "^4.2.0", + "lodash.difference": "^4.5.0", + "lodash.flatten": "^4.4.0", + "lodash.isplainobject": "^4.0.6", + "lodash.union": "^4.6.0", + "normalize-path": "^3.0.0", + "readable-stream": "^2.0.0" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/firebase-tools/node_modules/archiver-utils/node_modules/readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "devOptional": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/firebase-tools/node_modules/archiver/node_modules/async": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.3.tgz", + "integrity": "sha512-spZRyzKL5l5BZQrr/6m/SqFdBN0q3OCI0f9rjfBzCMBIP4p75P620rR3gTmaksNOhmzgdxcaxdNfMy6anrbM0g==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/are-we-there-yet": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-3.0.1.tgz", + "integrity": "sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg==", + "optional": true, + "dependencies": { + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/firebase-tools/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "devOptional": true, + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/firebase-tools/node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/arrify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-2.0.1.tgz", + "integrity": "sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==", + "devOptional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/as-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/as-array/-/as-array-2.0.0.tgz", + "integrity": "sha1-TwSAXYf4/OjlEbwhCPjl46KH1Uc=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/asn1": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", + "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", + "devOptional": true, + "dependencies": { + "safer-buffer": "~2.1.0" + } + }, + "node_modules/firebase-tools/node_modules/assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", + "devOptional": true, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/firebase-tools/node_modules/ast-types": { + "version": "0.13.4", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz", + "integrity": "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==", + "devOptional": true, + "dependencies": { + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/firebase-tools/node_modules/ast-types/node_modules/tslib": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz", + "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/async": { + "version": "2.6.4", + "resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz", + "integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==", + "devOptional": true, + "dependencies": { + "lodash": "^4.17.14" + } + }, + "node_modules/firebase-tools/node_modules/async-lock": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/async-lock/-/async-lock-1.3.2.tgz", + "integrity": "sha512-phnXdS3RP7PPcmP6NWWzWMU0sLTeyvtZCxBPpZdkYE3seGLKSQZs9FrmVO/qwypq98FUtWWUEYxziLkdGk5nnA==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/aws-sign2": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", + "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=", + "devOptional": true, + "engines": { + "node": "*" + } + }, + "node_modules/firebase-tools/node_modules/aws4": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz", + "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "devOptional": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/firebase-tools/node_modules/basic-auth": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", + "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==", + "devOptional": true, + "dependencies": { + "safe-buffer": "5.1.2" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/firebase-tools/node_modules/basic-auth-connect": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/basic-auth-connect/-/basic-auth-connect-1.0.0.tgz", + "integrity": "sha1-/bC0OWLKe0BFanwrtI/hc9otISI=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", + "devOptional": true, + "dependencies": { + "tweetnacl": "^0.14.3" + } + }, + "node_modules/firebase-tools/node_modules/bignumber.js": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.0.2.tgz", + "integrity": "sha512-GAcQvbpsM0pUb0zw1EI0KhQEZ+lRwR5fYaAp3vPOYuP7aDvGy6cVN6XHLauvF8SOga2y0dcLcjt3iQDTSEliyw==", + "devOptional": true, + "engines": { + "node": "*" + } + }, + "node_modules/firebase-tools/node_modules/binary-extensions": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.0.0.tgz", + "integrity": "sha512-Phlt0plgpIIBOGTT/ehfFnbNlfsDEiqmzE2KRXoX1bLIlir4X/MR+zSyBEkL05ffWgnRSf/DXv+WrUAVr93/ow==", + "devOptional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "devOptional": true, + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/firebase-tools/node_modules/body-parser": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", + "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==", + "devOptional": true, + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.4", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.11.0", + "raw-body": "2.5.1", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/firebase-tools/node_modules/body-parser/node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "devOptional": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/firebase-tools/node_modules/body-parser/node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "devOptional": true, + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/firebase-tools/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "devOptional": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/firebase-tools/node_modules/braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "devOptional": true, + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "devOptional": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/firebase-tools/node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI=", + "devOptional": true, + "engines": { + "node": "*" + } + }, + "node_modules/firebase-tools/node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "devOptional": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/firebase-tools/node_modules/cacache": { + "version": "16.1.3", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-16.1.3.tgz", + "integrity": "sha512-/+Emcj9DAXxX4cwlLmRI9c166RuL3w30zp4R7Joiv2cQTtTtA+jeuCAjH3ZlGnYS3tKENSrKhAzVVP9GVyzeYQ==", + "optional": true, + "dependencies": { + "@npmcli/fs": "^2.1.0", + "@npmcli/move-file": "^2.0.0", + "chownr": "^2.0.0", + "fs-minipass": "^2.1.0", + "glob": "^8.0.1", + "infer-owner": "^1.0.4", + "lru-cache": "^7.7.1", + "minipass": "^3.1.6", + "minipass-collect": "^1.0.2", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "mkdirp": "^1.0.4", + "p-map": "^4.0.0", + "promise-inflight": "^1.0.1", + "rimraf": "^3.0.2", + "ssri": "^9.0.0", + "tar": "^6.1.11", + "unique-filename": "^2.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/firebase-tools/node_modules/cacache/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "optional": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/firebase-tools/node_modules/cacache/node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "optional": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/firebase-tools/node_modules/cacache/node_modules/glob": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.0.3.tgz", + "integrity": "sha512-ull455NHSHI/Y1FqGaaYFaLGkNMMJbavMrEGFXG/PGrg6y7sutWHUHrz6gy6WEBH6akM1M414dWKCNs+IhKdiQ==", + "optional": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/firebase-tools/node_modules/cacache/node_modules/lru-cache": { + "version": "7.14.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.14.0.tgz", + "integrity": "sha512-EIRtP1GrSJny0dqb50QXRUNBxHJhcpxHC++M5tD7RYbvLLn5KVWKsbyswSSqDuU15UFi3bgTQIY8nhDMeF6aDQ==", + "optional": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/firebase-tools/node_modules/cacache/node_modules/minimatch": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.0.tgz", + "integrity": "sha512-9TPBGGak4nHfGZsPBohm9AWg6NoT7QTCehS3BIJABslyZbzxfV78QM2Y6+i741OPZIafFAaiiEMh5OyIrJPgtg==", + "optional": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/firebase-tools/node_modules/cacache/node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "optional": true, + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/firebase-tools/node_modules/cacache/node_modules/p-map": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", + "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", + "optional": true, + "dependencies": { + "aggregate-error": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/firebase-tools/node_modules/call-bind": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", + "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "devOptional": true, + "dependencies": { + "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/firebase-tools/node_modules/call-me-maybe": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.1.tgz", + "integrity": "sha1-JtII6onje1y95gJQoV8DHBak1ms=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/cardinal": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/cardinal/-/cardinal-2.1.1.tgz", + "integrity": "sha1-fMEFXYItISlU0HsIXeolHMe8VQU=", + "devOptional": true, + "dependencies": { + "ansicolors": "~0.3.2", + "redeyed": "~2.1.0" + }, + "bin": { + "cdl": "bin/cdl.js" + } + }, + "node_modules/firebase-tools/node_modules/caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/catharsis": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/catharsis/-/catharsis-0.9.0.tgz", + "integrity": "sha512-prMTQVpcns/tzFgFVkVp6ak6RykZyWb3gu8ckUpd6YkTlacOd3DXGJjIpD4Q6zJirizvaiAjSSHlOsA+6sNh2A==", + "devOptional": true, + "dependencies": { + "lodash": "^4.17.15" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/firebase-tools/node_modules/chardet": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", + "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/chokidar": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "devOptional": true, + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ], + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/firebase-tools/node_modules/ci-info": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz", + "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/cjson": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/cjson/-/cjson-0.3.3.tgz", + "integrity": "sha1-qS2ceG5b+bkwgGMp7gXV0yYbSvo=", + "devOptional": true, + "dependencies": { + "json-parse-helpfulerror": "^1.0.3" + }, + "engines": { + "node": ">= 0.3.0" + } + }, + "node_modules/firebase-tools/node_modules/clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", + "optional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/firebase-tools/node_modules/cli-boxes": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-2.2.1.tgz", + "integrity": "sha512-y4coMcylgSCdVinjiDBuR8PCC2bLjyGTwEmPb9NHR/QaNU6EUOXcTY/s6VjGMD6ENSEaeQYHCY0GNGS5jfMwPw==", + "devOptional": true, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/firebase-tools/node_modules/cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "devOptional": true, + "dependencies": { + "restore-cursor": "^3.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/cli-spinners": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.6.1.tgz", + "integrity": "sha512-x/5fWmGMnbKQAaNwN+UZlV79qBLM9JFnJuJ03gIi5whrob0xV0ofNVHy9DhwGdsMJQc2OKv0oGmLzvaqvAVv+g==", + "devOptional": true, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/firebase-tools/node_modules/cli-table": { + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/cli-table/-/cli-table-0.3.11.tgz", + "integrity": "sha512-IqLQi4lO0nIB4tcdTpN4LCB9FI3uqrJZK7RC515EnhZ6qBaglkIgICb1wjeAqpdoOabm1+SuQtkXIPdYC93jhQ==", + "devOptional": true, + "dependencies": { + "colors": "1.0.3" + }, + "engines": { + "node": ">= 0.2.0" + } + }, + "node_modules/firebase-tools/node_modules/cli-table3": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.2.tgz", + "integrity": "sha512-QyavHCaIC80cMivimWu4aWHilIpiDpfm3hGmqAmXVL1UsnbLuBSMd21hTX6VY4ZSDSM73ESLeF8TOYId3rBTbw==", + "devOptional": true, + "dependencies": { + "string-width": "^4.2.0" + }, + "engines": { + "node": "10.* || >= 12.*" + }, + "optionalDependencies": { + "@colors/colors": "1.5.0" + } + }, + "node_modules/firebase-tools/node_modules/cli-width": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-3.0.0.tgz", + "integrity": "sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==", + "devOptional": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/firebase-tools/node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "devOptional": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/firebase-tools/node_modules/clone": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha1-2jCcwmPfFZlMaIypAheco8fNfH4=", + "devOptional": true, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/firebase-tools/node_modules/color": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/color/-/color-3.0.0.tgz", + "integrity": "sha512-jCpd5+s0s0t7p3pHQKpnJ0TpQKKdleP71LWcA0aqiljpiuAkOSUFN/dyH8ZwF0hRmFlrIuRhufds1QyEP9EB+w==", + "devOptional": true, + "dependencies": { + "color-convert": "^1.9.1", + "color-string": "^1.5.2" + } + }, + "node_modules/firebase-tools/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "devOptional": true, + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/firebase-tools/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/color-string": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.5.5.tgz", + "integrity": "sha512-jgIoum0OfQfq9Whcfc2z/VhCNcmQjWbey6qBX0vqt7YICflUmBCh9E9CiQD5GSJ+Uehixm3NUwHVhqUAWRivZg==", + "devOptional": true, + "dependencies": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, + "node_modules/firebase-tools/node_modules/color-support": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", + "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", + "optional": true, + "bin": { + "color-support": "bin.js" + } + }, + "node_modules/firebase-tools/node_modules/colorette": { + "version": "2.0.19", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.19.tgz", + "integrity": "sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/colornames": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/colornames/-/colornames-1.1.1.tgz", + "integrity": "sha1-+IiQMGhcfE/54qVZ9Qd+t2qBb5Y=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/colors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.0.3.tgz", + "integrity": "sha1-BDP0TYCWgP3rYO0mDxsMJi6CpAs=", + "devOptional": true, + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/firebase-tools/node_modules/colorspace": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/colorspace/-/colorspace-1.1.2.tgz", + "integrity": "sha512-vt+OoIP2d76xLhjwbBaucYlNSpPsrJWPlBTtwCpQKIu6/CSMutyzX93O/Do0qzpH3YoHEes8YEFXyZ797rEhzQ==", + "devOptional": true, + "dependencies": { + "color": "3.0.x", + "text-hex": "1.0.x" + } + }, + "node_modules/firebase-tools/node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "devOptional": true, + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/firebase-tools/node_modules/commander": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.0.1.tgz", + "integrity": "sha512-IPF4ouhCP+qdlcmCedhxX4xiGBPyigb8v5NeUp+0LyhwLgxMqyp3S0vl7TAPfS/hiP7FC3caI/PB9lTmP8r1NA==", + "devOptional": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/firebase-tools/node_modules/compress-commons": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-4.1.1.tgz", + "integrity": "sha512-QLdDLCKNV2dtoTorqgxngQCMA+gWXkM/Nwu7FpeBhk/RdkzimqC3jueb/FDmaZeXh+uby1jkBqE3xArsLBE5wQ==", + "devOptional": true, + "dependencies": { + "buffer-crc32": "^0.2.13", + "crc32-stream": "^4.0.2", + "normalize-path": "^3.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/firebase-tools/node_modules/compressible": { + "version": "2.0.17", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.17.tgz", + "integrity": "sha512-BGHeLCK1GV7j1bSmQQAi26X+GgWcTjLr/0tzSvMCl3LH1w1IJ4PFSPoV5316b30cneTziC+B1a+3OjoSUcQYmw==", + "devOptional": true, + "dependencies": { + "mime-db": ">= 1.40.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/firebase-tools/node_modules/compression": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.7.4.tgz", + "integrity": "sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==", + "devOptional": true, + "dependencies": { + "accepts": "~1.3.5", + "bytes": "3.0.0", + "compressible": "~2.0.16", + "debug": "2.6.9", + "on-headers": "~1.0.2", + "safe-buffer": "5.1.2", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/firebase-tools/node_modules/compression/node_modules/bytes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", + "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=", + "devOptional": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/firebase-tools/node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/config-chain": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.13.tgz", + "integrity": "sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==", + "devOptional": true, + "dependencies": { + "ini": "^1.3.4", + "proto-list": "~1.2.1" + } + }, + "node_modules/firebase-tools/node_modules/configstore": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/configstore/-/configstore-5.0.1.tgz", + "integrity": "sha512-aMKprgk5YhBNyH25hj8wGt2+D52Sw1DRRIzqBwLp2Ya9mFmY8KPvvtvmna8SxVR9JMZ4kzMD68N22vlaRpkeFA==", + "devOptional": true, + "dependencies": { + "dot-prop": "^5.2.0", + "graceful-fs": "^4.1.2", + "make-dir": "^3.0.0", + "unique-string": "^2.0.0", + "write-file-atomic": "^3.0.0", + "xdg-basedir": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/configstore/node_modules/crypto-random-string": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz", + "integrity": "sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==", + "devOptional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/configstore/node_modules/dot-prop": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.2.0.tgz", + "integrity": "sha512-uEUyaDKoSQ1M4Oq8l45hSE26SnTxL6snNnqvK/VWx5wJhmff5z0FUVJDKDanor/6w3kzE3i7XZOk+7wC0EXr1A==", + "devOptional": true, + "dependencies": { + "is-obj": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/configstore/node_modules/unique-string": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-2.0.0.tgz", + "integrity": "sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==", + "devOptional": true, + "dependencies": { + "crypto-random-string": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/connect": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/connect/-/connect-3.7.0.tgz", + "integrity": "sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ==", + "devOptional": true, + "dependencies": { + "debug": "2.6.9", + "finalhandler": "1.1.2", + "parseurl": "~1.3.3", + "utils-merge": "1.0.1" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/firebase-tools/node_modules/console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==", + "optional": true + }, + "node_modules/firebase-tools/node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "devOptional": true, + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/firebase-tools/node_modules/content-disposition/node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "devOptional": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/firebase-tools/node_modules/content-type": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", + "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==", + "devOptional": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/firebase-tools/node_modules/cookie": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", + "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", + "devOptional": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/firebase-tools/node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "devOptional": true, + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/firebase-tools/node_modules/crc-32": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", + "devOptional": true, + "bin": { + "crc32": "bin/crc32.njs" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/firebase-tools/node_modules/crc32-stream": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-4.0.2.tgz", + "integrity": "sha512-DxFZ/Hk473b/muq1VJ///PMNLj0ZMnzye9thBpmjpJKCc5eMgB95aK8zCGrGfQ90cWo561Te6HK9D+j4KPdM6w==", + "devOptional": true, + "dependencies": { + "crc-32": "^1.2.0", + "readable-stream": "^3.4.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/firebase-tools/node_modules/cross-env": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-5.2.0.tgz", + "integrity": "sha512-jtdNFfFW1hB7sMhr/H6rW1Z45LFqyI431m3qU6bFXcQ3Eh7LtBuG3h74o7ohHZ3crrRkkqHlo4jYHFPcjroANg==", + "devOptional": true, + "dependencies": { + "cross-spawn": "^6.0.5", + "is-windows": "^1.0.0" + }, + "bin": { + "cross-env": "dist/bin/cross-env.js", + "cross-env-shell": "dist/bin/cross-env-shell.js" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/firebase-tools/node_modules/cross-env/node_modules/cross-spawn": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", + "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", + "devOptional": true, + "dependencies": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + }, + "engines": { + "node": ">=4.8" + } + }, + "node_modules/firebase-tools/node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "devOptional": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/firebase-tools/node_modules/cross-spawn/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==", + "devOptional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/cross-spawn/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==", + "devOptional": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/cross-spawn/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==", + "devOptional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/cross-spawn/node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "devOptional": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/firebase-tools/node_modules/csv-parse": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/csv-parse/-/csv-parse-5.0.4.tgz", + "integrity": "sha512-5AIdl8l6n3iYQYxan5djB5eKDa+vBnhfWZtRpJTcrETWfVLYN0WSj3L9RwvgYt+psoO77juUr8TG8qpfGZifVQ==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", + "devOptional": true, + "dependencies": { + "assert-plus": "^1.0.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/firebase-tools/node_modules/data-uri-to-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-3.0.1.tgz", + "integrity": "sha512-WboRycPNsVw3B3TL559F7kuBUM4d8CgMEvk6xEJlOp7OBPjt6G7z8WMWlD2rOFZLk6OYfFIUGsCOWzcQH9K2og==", + "devOptional": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/firebase-tools/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "devOptional": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/firebase-tools/node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "devOptional": true, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/firebase-tools/node_modules/deep-freeze": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/deep-freeze/-/deep-freeze-0.0.1.tgz", + "integrity": "sha1-OgsABd4YZygZ39OM0x+RF5yJPoQ=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/deep-is": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", + "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/defaults": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.3.tgz", + "integrity": "sha1-xlYFHpgX2f8I7YgUd/P+QBnz730=", + "devOptional": true, + "dependencies": { + "clone": "^1.0.2" + } + }, + "node_modules/firebase-tools/node_modules/degenerator": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-3.0.1.tgz", + "integrity": "sha512-LFsIFEeLPlKvAKXu7j3ssIG6RT0TbI7/GhsqrI0DnHASEQjXQ0LUSYcjJteGgRGmZbl1TnMSxpNQIAiJ7Du5TQ==", + "devOptional": true, + "dependencies": { + "ast-types": "^0.13.2", + "escodegen": "^1.8.1", + "esprima": "^4.0.0", + "vm2": "^3.9.3" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/firebase-tools/node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", + "devOptional": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/firebase-tools/node_modules/delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", + "optional": true + }, + "node_modules/firebase-tools/node_modules/depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=", + "optional": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/firebase-tools/node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "devOptional": true, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/firebase-tools/node_modules/diagnostics": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/diagnostics/-/diagnostics-1.1.1.tgz", + "integrity": "sha512-8wn1PmdunLJ9Tqbx+Fx/ZEuHfJf4NKSN2ZBj7SJC/OWRWha843+WsTjqMe1B5E3p28jqBlp+mJ2fPVxPyNgYKQ==", + "devOptional": true, + "dependencies": { + "colorspace": "1.1.x", + "enabled": "1.0.x", + "kuler": "1.0.x" + } + }, + "node_modules/firebase-tools/node_modules/duplexify": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.1.tgz", + "integrity": "sha512-DY3xVEmVHTv1wSzKNbwoU6nVjzI369Y6sPoqfYr0/xlx3IdX2n94xIszTcjPO8W8ZIv0Wb0PXNcjuZyT4wiICA==", + "devOptional": true, + "dependencies": { + "end-of-stream": "^1.4.1", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1", + "stream-shift": "^1.0.0" + } + }, + "node_modules/firebase-tools/node_modules/ecc-jsbn": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", + "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", + "devOptional": true, + "dependencies": { + "jsbn": "~0.1.0", + "safer-buffer": "^2.1.0" + } + }, + "node_modules/firebase-tools/node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "devOptional": true, + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/firebase-tools/node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/enabled": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/enabled/-/enabled-1.0.2.tgz", + "integrity": "sha1-ll9lE9LC0cX0ZStkouM5ZGf8L5M=", + "devOptional": true, + "dependencies": { + "env-variable": "0.0.x" + } + }, + "node_modules/firebase-tools/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=", + "devOptional": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/firebase-tools/node_modules/encoding": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", + "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", + "optional": true, + "dependencies": { + "iconv-lite": "^0.6.2" + } + }, + "node_modules/firebase-tools/node_modules/encoding/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "optional": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/firebase-tools/node_modules/end-of-stream": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.1.tgz", + "integrity": "sha512-1MkrZNvWTKCaigbn+W15elq2BB/L22nqrSY5DKlo3X6+vclJm8Bb5djXJBmEX6fS3+zCh/F4VBK5Z2KxJt4s2Q==", + "devOptional": true, + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/firebase-tools/node_modules/entities": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.1.0.tgz", + "integrity": "sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w==", + "devOptional": true, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/firebase-tools/node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "optional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/firebase-tools/node_modules/env-variable": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/env-variable/-/env-variable-0.0.6.tgz", + "integrity": "sha512-bHz59NlBbtS0NhftmR8+ExBEekE7br0e01jw+kk0NDro7TtZzBYZ5ScGPs3OmwnpyfHTHOtr1Y6uedCdrIldtg==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/err-code": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", + "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", + "optional": true + }, + "node_modules/firebase-tools/node_modules/escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "devOptional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/firebase-tools/node_modules/escape-goat": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/escape-goat/-/escape-goat-2.1.1.tgz", + "integrity": "sha512-8/uIhbG12Csjy2JEW7D9pHbreaVaS/OpN3ycnyvElTdwM5n6GY6W6e2IPemfvGZeUMqZ9A/3GqIZMgKnBhAw/Q==", + "devOptional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "devOptional": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/firebase-tools/node_modules/escodegen": { + "version": "1.14.3", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.14.3.tgz", + "integrity": "sha512-qFcX0XJkdg+PB3xjZZG/wKSuT1PnQWx57+TVSjIMmILd2yC/6ByYElPwJnslDsuWuSAp4AwJGumarAAmJch5Kw==", + "devOptional": true, + "dependencies": { + "esprima": "^4.0.1", + "estraverse": "^4.2.0", + "esutils": "^2.0.2", + "optionator": "^0.8.1" + }, + "bin": { + "escodegen": "bin/escodegen.js", + "esgenerate": "bin/esgenerate.js" + }, + "engines": { + "node": ">=4.0" + }, + "optionalDependencies": { + "source-map": "~0.6.1" + } + }, + "node_modules/firebase-tools/node_modules/eslint-visitor-keys": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz", + "integrity": "sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA==", + "devOptional": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/firebase-tools/node_modules/espree": { + "version": "9.4.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.4.1.tgz", + "integrity": "sha512-XwctdmTO6SIvCzd9810yyNzIrOrqNYV9Koizx4C/mRhf9uq0o4yHoCEU/670pOxOL/MSraektvSAji79kX90Vg==", + "devOptional": true, + "dependencies": { + "acorn": "^8.8.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/firebase-tools/node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "devOptional": true, + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/firebase-tools/node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "devOptional": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/firebase-tools/node_modules/esutils": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz", + "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=", + "devOptional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/firebase-tools/node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "devOptional": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/firebase-tools/node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "devOptional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/firebase-tools/node_modules/events-listener": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/events-listener/-/events-listener-1.1.0.tgz", + "integrity": "sha512-Kd3EgYfODHueq6GzVfs/VUolh2EgJsS8hkO3KpnDrxVjU3eq63eXM2ujXkhPP+OkeUOhL8CxdfZbQXzryb5C4g==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/exegesis": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/exegesis/-/exegesis-4.1.0.tgz", + "integrity": "sha512-iqc55n+hmv8d1KYNMjq7bCcp4u74oRY6MBcj6Vsux7Wd4mRvlgahKqrBTyLIWwscNjEF3qvPmeJ0RPTj8ORMNg==", + "devOptional": true, + "dependencies": { + "@apidevtools/json-schema-ref-parser": "^9.0.3", + "ajv": "^8.3.0", + "ajv-formats": "^2.1.0", + "body-parser": "^1.18.3", + "content-type": "^1.0.4", + "deep-freeze": "0.0.1", + "events-listener": "^1.1.0", + "glob": "^7.1.3", + "json-ptr": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "lodash": "^4.17.11", + "openapi3-ts": "^2.0.1", + "promise-breaker": "^5.0.0", + "pump": "^3.0.0", + "qs": "^6.6.0", + "raw-body": "^2.3.3", + "semver": "^7.0.0" + }, + "engines": { + "node": ">=6.0.0", + "npm": ">5.0.0" + } + }, + "node_modules/firebase-tools/node_modules/exegesis-express": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/exegesis-express/-/exegesis-express-4.0.0.tgz", + "integrity": "sha512-V2hqwTtYRj0bj43K4MCtm0caD97YWkqOUHFMRCBW5L1x9IjyqOEc7Xa4oQjjiFbeFOSQzzwPV+BzXsQjSz08fw==", + "devOptional": true, + "dependencies": { + "exegesis": "^4.1.0" + }, + "engines": { + "node": ">=6.0.0", + "npm": ">5.0.0" + } + }, + "node_modules/firebase-tools/node_modules/exegesis/node_modules/ajv": { + "version": "8.8.2", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.8.2.tgz", + "integrity": "sha512-x9VuX+R/jcFj1DHo/fCp99esgGDWiHENrKxaCENuCxpoMCmAt/COCGVDwA7kleEpEzJjDnvh3yGoOuLu0Dtllw==", + "devOptional": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/firebase-tools/node_modules/exegesis/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/exegesis/node_modules/semver": { + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", + "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", + "devOptional": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/firebase-tools/node_modules/express": { + "version": "4.18.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz", + "integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==", + "devOptional": true, + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.1", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.5.0", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.2.0", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.7", + "qs": "6.11.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.18.0", + "serve-static": "1.15.0", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/firebase-tools/node_modules/express/node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "devOptional": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/firebase-tools/node_modules/express/node_modules/finalhandler": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", + "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", + "devOptional": true, + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/firebase-tools/node_modules/express/node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "devOptional": true, + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/firebase-tools/node_modules/express/node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "devOptional": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/firebase-tools/node_modules/express/node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "devOptional": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/firebase-tools/node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/external-editor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", + "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", + "devOptional": true, + "dependencies": { + "chardet": "^0.7.0", + "iconv-lite": "^0.4.24", + "tmp": "^0.0.33" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/firebase-tools/node_modules/external-editor/node_modules/tmp": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "devOptional": true, + "dependencies": { + "os-tmpdir": "~1.0.2" + }, + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/firebase-tools/node_modules/extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=", + "devOptional": true, + "engines": [ + "node >=0.6.0" + ] + }, + "node_modules/firebase-tools/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==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/fast-json-stable-stringify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", + "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/fast-safe-stringify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", + "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/fast-text-encoding": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/fast-text-encoding/-/fast-text-encoding-1.0.3.tgz", + "integrity": "sha512-dtm4QZH9nZtcDt8qJiOH9fcQd1NAgi+K1O2DbE6GG1PPCK/BWfOH3idCTRQ4ImXRUOyopDEgDEnVEE7Y/2Wrig==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/fast-url-parser": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/fast-url-parser/-/fast-url-parser-1.1.3.tgz", + "integrity": "sha1-9K8+qfNNiicc9YrSs3WfQx8LMY0=", + "devOptional": true, + "dependencies": { + "punycode": "^1.3.2" + } + }, + "node_modules/firebase-tools/node_modules/fast-url-parser/node_modules/punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/fecha": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fecha/-/fecha-2.3.3.tgz", + "integrity": "sha512-lUGBnIamTAwk4znq5BcqsDaxSmZ9nDVJaij6NvRt/Tg4R69gERA+otPKbS86ROw9nxVMw2/mp1fnaiWqbs6Sdg==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/figures": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", + "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", + "devOptional": true, + "dependencies": { + "escape-string-regexp": "^1.0.5" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/firebase-tools/node_modules/file-uri-to-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-2.0.0.tgz", + "integrity": "sha512-hjPFI8oE/2iQPVe4gbrJ73Pp+Xfub2+WI2LlXDbsaJBwT5wuMh35WNWVYYTpnz895shtwfyutMFLFywpQAFdLg==", + "devOptional": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/firebase-tools/node_modules/filesize": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/filesize/-/filesize-6.1.0.tgz", + "integrity": "sha512-LpCHtPQ3sFx67z+uh2HnSyWSLLu5Jxo21795uRDuar/EOuYWXib5EmPaGIBuSnRqH2IODiKA2k5re/K9OnN/Yg==", + "devOptional": true, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/firebase-tools/node_modules/fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "devOptional": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/finalhandler": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", + "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", + "devOptional": true, + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "statuses": "~1.5.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/firebase-tools/node_modules/forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=", + "devOptional": true, + "engines": { + "node": "*" + } + }, + "node_modules/firebase-tools/node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "devOptional": true, + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/firebase-tools/node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "devOptional": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/firebase-tools/node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=", + "devOptional": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/firebase-tools/node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "devOptional": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/firebase-tools/node_modules/fs-extra/node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "devOptional": true, + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/firebase-tools/node_modules/fs-extra/node_modules/universalify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", + "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", + "devOptional": true, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/firebase-tools/node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "devOptional": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/firebase-tools/node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/firebase-tools/node_modules/ftp": { + "version": "0.3.10", + "resolved": "https://registry.npmjs.org/ftp/-/ftp-0.3.10.tgz", + "integrity": "sha1-kZfYYa2BQvPmPVqDv+TFn3MwiF0=", + "devOptional": true, + "dependencies": { + "readable-stream": "1.1.x", + "xregexp": "2.0.0" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/firebase-tools/node_modules/ftp/node_modules/isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/ftp/node_modules/readable-stream": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", + "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", + "devOptional": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + }, + "node_modules/firebase-tools/node_modules/ftp/node_modules/string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/gauge": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-4.0.4.tgz", + "integrity": "sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg==", + "optional": true, + "dependencies": { + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.3", + "console-control-strings": "^1.1.0", + "has-unicode": "^2.0.1", + "signal-exit": "^3.0.7", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.5" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/firebase-tools/node_modules/gaxios": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-4.1.0.tgz", + "integrity": "sha512-vb0to8xzGnA2qcgywAjtshOKKVDf2eQhJoiL6fHhgW5tVN7wNk7egnYIO9zotfn3lQ3De1VPdf7V5/BWfCtCmg==", + "devOptional": true, + "dependencies": { + "abort-controller": "^3.0.0", + "extend": "^3.0.2", + "https-proxy-agent": "^5.0.0", + "is-stream": "^2.0.0", + "node-fetch": "^2.3.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/firebase-tools/node_modules/gaxios/node_modules/is-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.0.tgz", + "integrity": "sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw==", + "devOptional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/gcp-metadata": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-4.2.0.tgz", + "integrity": "sha512-vQZD57cQkqIA6YPGXM/zc+PIZfNRFdukWGsGZ5+LcJzesi5xp6Gn7a02wRJi4eXPyArNMIYpPET4QMxGqtlk6Q==", + "devOptional": true, + "dependencies": { + "gaxios": "^3.0.0", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/firebase-tools/node_modules/gcp-metadata/node_modules/gaxios": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-3.2.0.tgz", + "integrity": "sha512-+6WPeVzPvOshftpxJwRi2Ozez80tn/hdtOUag7+gajDHRJvAblKxTFSSMPtr2hmnLy7p0mvYz0rMXLBl8pSO7Q==", + "devOptional": true, + "dependencies": { + "abort-controller": "^3.0.0", + "extend": "^3.0.2", + "https-proxy-agent": "^5.0.0", + "is-stream": "^2.0.0", + "node-fetch": "^2.3.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/firebase-tools/node_modules/gcp-metadata/node_modules/is-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.0.tgz", + "integrity": "sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw==", + "devOptional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "devOptional": true, + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/firebase-tools/node_modules/get-intrinsic": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.3.tgz", + "integrity": "sha512-QJVz1Tj7MS099PevUG5jvnt9tSkXN8K14dxQlikJuPt4uD9hHAHjLyLBiLR5zELelBdD9QNRAXZzsJx0WaDL9A==", + "devOptional": true, + "dependencies": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/firebase-tools/node_modules/get-uri": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-3.0.2.tgz", + "integrity": "sha512-+5s0SJbGoyiJTZZ2JTpFPLMPSch72KEqGOTvQsBqg0RBWvwhWUSYZFAtz3TPW0GXJuLBJPts1E241iHg+VRfhg==", + "devOptional": true, + "dependencies": { + "@tootallnate/once": "1", + "data-uri-to-buffer": "3", + "debug": "4", + "file-uri-to-path": "2", + "fs-extra": "^8.1.0", + "ftp": "^0.3.10" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/firebase-tools/node_modules/get-uri/node_modules/debug": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", + "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", + "devOptional": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/firebase-tools/node_modules/get-uri/node_modules/fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "devOptional": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/firebase-tools/node_modules/get-uri/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", + "devOptional": true, + "dependencies": { + "assert-plus": "^1.0.0" + } + }, + "node_modules/firebase-tools/node_modules/glob": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", + "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", + "devOptional": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/firebase-tools/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==", + "devOptional": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/firebase-tools/node_modules/glob-slash": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/glob-slash/-/glob-slash-1.0.0.tgz", + "integrity": "sha1-/lLvpDMjP3Si/mTHq7m8hIICq5U=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/glob-slasher": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/glob-slasher/-/glob-slasher-1.0.1.tgz", + "integrity": "sha1-dHoOW7IiZC7hDT4FRD4QlJPLD44=", + "devOptional": true, + "dependencies": { + "glob-slash": "^1.0.0", + "lodash.isobject": "^2.4.1", + "toxic": "^1.0.0" + } + }, + "node_modules/firebase-tools/node_modules/global-dirs": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-3.0.1.tgz", + "integrity": "sha512-NBcGGFbBA9s1VzD41QXDG+3++t9Mn5t1FpLdhESY6oKY4gYTFpX4wO3sqGUa0Srjtbfj3szX0RnemmrVRUdULA==", + "devOptional": true, + "dependencies": { + "ini": "2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/firebase-tools/node_modules/global-dirs/node_modules/ini": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ini/-/ini-2.0.0.tgz", + "integrity": "sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA==", + "devOptional": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/firebase-tools/node_modules/google-auth-library": { + "version": "7.14.1", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-7.14.1.tgz", + "integrity": "sha512-5Rk7iLNDFhFeBYc3s8l1CqzbEBcdhwR193RlD4vSNFajIcINKI8W8P0JLmBpwymHqqWbX34pJDQu39cSy/6RsA==", + "devOptional": true, + "dependencies": { + "arrify": "^2.0.0", + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "fast-text-encoding": "^1.0.0", + "gaxios": "^4.0.0", + "gcp-metadata": "^4.2.0", + "gtoken": "^5.0.4", + "jws": "^4.0.0", + "lru-cache": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/firebase-tools/node_modules/google-auth-library/node_modules/jwa": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz", + "integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==", + "devOptional": true, + "dependencies": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/firebase-tools/node_modules/google-auth-library/node_modules/jws": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", + "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", + "devOptional": true, + "dependencies": { + "jwa": "^2.0.0", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/firebase-tools/node_modules/google-gax": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/google-gax/-/google-gax-3.5.3.tgz", + "integrity": "sha512-caItkifbcPDVf5xW2j6xLXmuX9bh2dheJN9AzMuQj6VvWacUVV5d0BkjL+Ia8sgX9VvXtbqjf4NKK3nTgV9UUQ==", + "devOptional": true, + "dependencies": { + "@grpc/grpc-js": "~1.7.0", + "@grpc/proto-loader": "^0.7.0", + "@types/long": "^4.0.0", + "abort-controller": "^3.0.0", + "duplexify": "^4.0.0", + "fast-text-encoding": "^1.0.3", + "google-auth-library": "^8.0.2", + "is-stream-ended": "^0.1.4", + "node-fetch": "^2.6.1", + "object-hash": "^3.0.0", + "proto3-json-serializer": "^1.0.0", + "protobufjs": "7.1.2", + "protobufjs-cli": "1.1.0", + "retry-request": "^5.0.0" + }, + "bin": { + "compileProtos": "build/tools/compileProtos.js", + "minifyProtoJson": "build/tools/minify.js" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/firebase-tools/node_modules/google-gax/node_modules/@grpc/proto-loader": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.4.tgz", + "integrity": "sha512-MnWjkGwqQ3W8fx94/c1CwqLsNmHHv2t0CFn+9++6+cDphC1lolpg9M2OU0iebIjK//pBNX9e94ho+gjx6vz39w==", + "devOptional": true, + "dependencies": { + "@types/long": "^4.0.1", + "lodash.camelcase": "^4.3.0", + "long": "^4.0.0", + "protobufjs": "^7.0.0", + "yargs": "^16.2.0" + }, + "bin": { + "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/firebase-tools/node_modules/google-gax/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==", + "devOptional": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/firebase-tools/node_modules/google-gax/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "devOptional": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/firebase-tools/node_modules/google-gax/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "devOptional": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/firebase-tools/node_modules/google-gax/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==", + "devOptional": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/firebase-tools/node_modules/google-gax/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==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/google-gax/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "devOptional": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/firebase-tools/node_modules/google-gax/node_modules/gaxios": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-5.0.2.tgz", + "integrity": "sha512-TjtV2AJOZoMQqRYoy5eM8cCQogYwazWNYLQ72QB0kwa6vHHruYkGmhhyrlzbmgNHK1dNnuP2WSH81urfzyN2Og==", + "devOptional": true, + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^5.0.0", + "is-stream": "^2.0.0", + "node-fetch": "^2.6.7" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/firebase-tools/node_modules/google-gax/node_modules/gcp-metadata": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-5.2.0.tgz", + "integrity": "sha512-aFhhvvNycky2QyhG+dcfEdHBF0FRbYcf39s6WNHUDysKSrbJ5vuFbjydxBcmewtXeV248GP8dWT3ByPNxsyHCw==", + "devOptional": true, + "dependencies": { + "gaxios": "^5.0.0", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/firebase-tools/node_modules/google-gax/node_modules/glob": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "devOptional": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/firebase-tools/node_modules/google-gax/node_modules/google-auth-library": { + "version": "8.7.0", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-8.7.0.tgz", + "integrity": "sha512-1M0NG5VDIvJZEnstHbRdckLZESoJwguinwN8Dhae0j2ZKIQFIV63zxm6Fo6nM4xkgqUr2bbMtV5Dgo+Hy6oo0Q==", + "devOptional": true, + "dependencies": { + "arrify": "^2.0.0", + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "fast-text-encoding": "^1.0.0", + "gaxios": "^5.0.0", + "gcp-metadata": "^5.0.0", + "gtoken": "^6.1.0", + "jws": "^4.0.0", + "lru-cache": "^6.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/firebase-tools/node_modules/google-gax/node_modules/google-p12-pem": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/google-p12-pem/-/google-p12-pem-4.0.1.tgz", + "integrity": "sha512-WPkN4yGtz05WZ5EhtlxNDWPhC4JIic6G8ePitwUWy4l+XPVYec+a0j0Ts47PDtW59y3RwAhUd9/h9ZZ63px6RQ==", + "devOptional": true, + "dependencies": { + "node-forge": "^1.3.1" + }, + "bin": { + "gp12-pem": "build/src/bin/gp12-pem.js" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/firebase-tools/node_modules/google-gax/node_modules/gtoken": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-6.1.2.tgz", + "integrity": "sha512-4ccGpzz7YAr7lxrT2neugmXQ3hP9ho2gcaityLVkiUecAiwiy60Ii8gRbZeOsXV19fYaRjgBSshs8kXw+NKCPQ==", + "devOptional": true, + "dependencies": { + "gaxios": "^5.0.1", + "google-p12-pem": "^4.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/firebase-tools/node_modules/google-gax/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==", + "devOptional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/google-gax/node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "devOptional": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/firebase-tools/node_modules/google-gax/node_modules/jwa": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz", + "integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==", + "devOptional": true, + "dependencies": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/firebase-tools/node_modules/google-gax/node_modules/jws": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", + "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", + "devOptional": true, + "dependencies": { + "jwa": "^2.0.0", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/firebase-tools/node_modules/google-gax/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "devOptional": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/firebase-tools/node_modules/google-gax/node_modules/protobufjs": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.1.2.tgz", + "integrity": "sha512-4ZPTPkXCdel3+L81yw3dG6+Kq3umdWKh7Dc7GW/CpNk4SX3hK58iPCWeCyhVTDrbkNeKrYNZ7EojM5WDaEWTLQ==", + "devOptional": true, + "hasInstallScript": true, + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/node": ">=13.7.0", + "long": "^5.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/firebase-tools/node_modules/google-gax/node_modules/protobufjs-cli": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/protobufjs-cli/-/protobufjs-cli-1.1.0.tgz", + "integrity": "sha512-VXMQn+z3yG2WbN2E+mx5vcyIHF7yJSg2jqyqfxcZLWNOSTqUzSSgAE5vu04/JEpwxTI04JGyrZRDHC36wr04uw==", + "devOptional": true, + "dependencies": { + "chalk": "^4.0.0", + "escodegen": "^1.13.0", + "espree": "^9.0.0", + "estraverse": "^5.1.0", + "glob": "^8.0.0", + "jsdoc": "^4.0.0", + "minimist": "^1.2.0", + "semver": "^7.1.2", + "tmp": "^0.2.1", + "uglify-js": "^3.7.7" + }, + "bin": { + "pbjs": "bin/pbjs", + "pbts": "bin/pbts" + }, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "protobufjs": "^7.0.0" + } + }, + "node_modules/firebase-tools/node_modules/google-gax/node_modules/protobufjs/node_modules/long": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/long/-/long-5.2.1.tgz", + "integrity": "sha512-GKSNGeNAtw8IryjjkhZxuKB3JzlcLTwjtiQCHKvqQet81I93kXslhDQruGI/QsddO83mcDToBVy7GqGS/zYf/A==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/google-gax/node_modules/semver": { + "version": "7.3.8", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", + "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", + "devOptional": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/firebase-tools/node_modules/google-gax/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==", + "devOptional": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/google-p12-pem": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/google-p12-pem/-/google-p12-pem-3.1.3.tgz", + "integrity": "sha512-MC0jISvzymxePDVembypNefkAQp+DRP7dBE+zNUPaIjEspIlYg0++OrsNr248V9tPbz6iqtZ7rX1hxWA5B8qBQ==", + "devOptional": true, + "dependencies": { + "node-forge": "^1.0.0" + }, + "bin": { + "gp12-pem": "build/src/bin/gp12-pem.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/firebase-tools/node_modules/graceful-fs": { + "version": "4.2.10", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", + "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/gtoken": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-5.3.2.tgz", + "integrity": "sha512-gkvEKREW7dXWF8NV8pVrKfW7WqReAmjjkMBh6lNCCGOM4ucS0r0YyXXl0r/9Yj8wcW/32ISkfc8h5mPTDbtifQ==", + "devOptional": true, + "dependencies": { + "gaxios": "^4.0.0", + "google-p12-pem": "^3.1.3", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/firebase-tools/node_modules/gtoken/node_modules/jwa": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz", + "integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==", + "devOptional": true, + "dependencies": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/firebase-tools/node_modules/gtoken/node_modules/jws": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", + "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", + "devOptional": true, + "dependencies": { + "jwa": "^2.0.0", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/firebase-tools/node_modules/har-schema": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", + "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=", + "devOptional": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/firebase-tools/node_modules/har-validator": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.3.tgz", + "integrity": "sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==", + "deprecated": "this library is no longer supported", + "devOptional": true, + "dependencies": { + "ajv": "^6.5.5", + "har-schema": "^2.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/firebase-tools/node_modules/has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "devOptional": true, + "dependencies": { + "function-bind": "^1.1.1" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/firebase-tools/node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "devOptional": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/firebase-tools/node_modules/has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==", + "optional": true + }, + "node_modules/firebase-tools/node_modules/has-yarn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/has-yarn/-/has-yarn-2.1.0.tgz", + "integrity": "sha512-UqBRqi4ju7T+TqGNdqAO0PaSVGsDGJUBQvk9eUWNGRY1CFGDzYhLWoM7JQEemnlvVcv/YEmc2wNW8BC24EnUsw==", + "devOptional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/http-cache-semantics": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", + "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==", + "optional": true + }, + "node_modules/firebase-tools/node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "devOptional": true, + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/firebase-tools/node_modules/http-errors/node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "devOptional": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/firebase-tools/node_modules/http-errors/node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "devOptional": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/firebase-tools/node_modules/http-proxy-agent": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz", + "integrity": "sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==", + "devOptional": true, + "dependencies": { + "@tootallnate/once": "1", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/firebase-tools/node_modules/http-proxy-agent/node_modules/debug": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "devOptional": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/firebase-tools/node_modules/http-proxy-agent/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/http-signature": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", + "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", + "devOptional": true, + "dependencies": { + "assert-plus": "^1.0.0", + "jsprim": "^1.2.2", + "sshpk": "^1.7.0" + }, + "engines": { + "node": ">=0.8", + "npm": ">=1.3.7" + } + }, + "node_modules/firebase-tools/node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "devOptional": true, + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/firebase-tools/node_modules/https-proxy-agent/node_modules/debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "deprecated": "Debug versions >=3.2.0 <3.2.7 || >=4 <4.3.1 have a low-severity ReDos regression when used in a Node.js environment. It is recommended you upgrade to 3.2.7 or 4.3.1. (https://github.com/visionmedia/debug/issues/797)", + "devOptional": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/firebase-tools/node_modules/https-proxy-agent/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/humanize-ms": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", + "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", + "optional": true, + "dependencies": { + "ms": "^2.0.0" + } + }, + "node_modules/firebase-tools/node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "devOptional": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/firebase-tools/node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "devOptional": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/firebase-tools/node_modules/import-lazy": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/import-lazy/-/import-lazy-2.1.0.tgz", + "integrity": "sha1-BWmOPUXIjo1+nZLLBYTnfwlvPkM=", + "devOptional": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/firebase-tools/node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", + "devOptional": true, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/firebase-tools/node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/infer-owner": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/infer-owner/-/infer-owner-1.0.4.tgz", + "integrity": "sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==", + "optional": true + }, + "node_modules/firebase-tools/node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "devOptional": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/firebase-tools/node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/inquirer": { + "version": "8.2.5", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-8.2.5.tgz", + "integrity": "sha512-QAgPDQMEgrDssk1XiwwHoOGYF9BAbUcc1+j+FhEvaOt8/cKRqyLn0U5qA6F74fGhTMGxf92pOvPBeh29jQJDTQ==", + "devOptional": true, + "dependencies": { + "ansi-escapes": "^4.2.1", + "chalk": "^4.1.1", + "cli-cursor": "^3.1.0", + "cli-width": "^3.0.0", + "external-editor": "^3.0.3", + "figures": "^3.0.0", + "lodash": "^4.17.21", + "mute-stream": "0.0.8", + "ora": "^5.4.1", + "run-async": "^2.4.0", + "rxjs": "^7.5.5", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0", + "through": "^2.3.6", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/firebase-tools/node_modules/inquirer/node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "devOptional": true, + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/firebase-tools/node_modules/inquirer/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "devOptional": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/firebase-tools/node_modules/inquirer/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "devOptional": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/firebase-tools/node_modules/inquirer/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "devOptional": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/firebase-tools/node_modules/inquirer/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/inquirer/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "devOptional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/inquirer/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "devOptional": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/inquirer/node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "devOptional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/firebase-tools/node_modules/install-artifact-from-github": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/install-artifact-from-github/-/install-artifact-from-github-1.3.1.tgz", + "integrity": "sha512-3l3Bymg2eKDsN5wQuMfgGEj2x6l5MCAv0zPL6rxHESufFVlEAKW/6oY9F1aGgvY/EgWm5+eWGRjINveL4X7Hgg==", + "optional": true, + "bin": { + "install-from-cache": "bin/install-from-cache.js", + "save-to-github-cache": "bin/save-to-github-cache.js" + } + }, + "node_modules/firebase-tools/node_modules/ip": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.5.tgz", + "integrity": "sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/ip-regex": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-4.3.0.tgz", + "integrity": "sha512-B9ZWJxHHOHUhUjCPrMpLD4xEq35bUTClHM1S6CBU5ixQnkZmwipwgc96vAd7AAGM9TGHvJR+Uss+/Ak6UphK+Q==", + "devOptional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "devOptional": true, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/firebase-tools/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==", + "devOptional": true, + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/is-ci": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-2.0.0.tgz", + "integrity": "sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w==", + "devOptional": true, + "dependencies": { + "ci-info": "^2.0.0" + }, + "bin": { + "is-ci": "bin.js" + } + }, + "node_modules/firebase-tools/node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", + "devOptional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/firebase-tools/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "devOptional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/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==", + "devOptional": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/firebase-tools/node_modules/is-installed-globally": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.4.0.tgz", + "integrity": "sha512-iwGqO3J21aaSkC7jWnHP/difazwS7SFeIqxv6wEtLU8Y5KlzFTjyqcSIT0d8s4+dDhKytsk9PJZ2BkS5eZwQRQ==", + "devOptional": true, + "dependencies": { + "global-dirs": "^3.0.0", + "is-path-inside": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/firebase-tools/node_modules/is-interactive": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", + "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", + "devOptional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/is-lambda": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-lambda/-/is-lambda-1.0.1.tgz", + "integrity": "sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==", + "optional": true + }, + "node_modules/firebase-tools/node_modules/is-npm": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-npm/-/is-npm-5.0.0.tgz", + "integrity": "sha512-WW/rQLOazUq+ST/bCAVBp/2oMERWLsR7OrKyt052dNDk4DHcDE0/7QSXITlmi+VBcV13DfIbysG3tZJm5RfdBA==", + "devOptional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/firebase-tools/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==", + "devOptional": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/firebase-tools/node_modules/is-obj": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", + "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==", + "devOptional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/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==", + "devOptional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=", + "devOptional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/firebase-tools/node_modules/is-stream-ended": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-stream-ended/-/is-stream-ended-0.1.4.tgz", + "integrity": "sha512-xj0XPvmr7bQFTvirqnFr50o0hQIh6ZItDqloxt5aJrR4NQsYeSsyFQERYGCAzfindAcnKjINnwEEgLx4IqVzQw==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "devOptional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/firebase-tools/node_modules/is-url": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/is-url/-/is-url-1.2.4.tgz", + "integrity": "sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/is-windows": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", + "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", + "devOptional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/firebase-tools/node_modules/is-wsl": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-1.1.0.tgz", + "integrity": "sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0=", + "devOptional": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/firebase-tools/node_modules/is-yarn-global": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/is-yarn-global/-/is-yarn-global-0.3.0.tgz", + "integrity": "sha512-VjSeb/lHmkoyd8ryPVIKvOCn4D1koMqY+vqyjjUfc3xyKtP4dYOxM44sZrnqQSzSds3xyOrUTLTC9LVCVgLngw==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/is2": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/is2/-/is2-2.0.7.tgz", + "integrity": "sha512-4vBQoURAXC6hnLFxD4VW7uc04XiwTTl/8ydYJxKvPwkWQrSjInkuM5VZVg6BGr1/natq69zDuvO9lGpLClJqvA==", + "devOptional": true, + "dependencies": { + "deep-is": "^0.1.3", + "ip-regex": "^4.1.0", + "is-url": "^1.2.4" + }, + "engines": { + "node": ">=v0.10.0" + } + }, + "node_modules/firebase-tools/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/isomorphic-fetch": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-3.0.0.tgz", + "integrity": "sha512-qvUtwJ3j6qwsF3jLxkZ72qCgjMysPzDfeV240JHiGZsANBYd+EEuu35v7dfrJ9Up0Ak07D7GGSkGhCHTqg/5wA==", + "devOptional": true, + "dependencies": { + "node-fetch": "^2.6.1", + "whatwg-fetch": "^3.4.1" + } + }, + "node_modules/firebase-tools/node_modules/isomorphic-fetch/node_modules/whatwg-fetch": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.2.tgz", + "integrity": "sha512-bJlen0FcuU/0EMLrdbJ7zOnW6ITZLrZMIarMUVmdKtsGvZna8vxKYaexICWPfZ8qwf9fzNq+UEIZrnSaApt6RA==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/jju": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/jju/-/jju-1.4.0.tgz", + "integrity": "sha1-o6vicYryQaKykE+EpiWXDzia4yo=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/join-path": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/join-path/-/join-path-1.1.1.tgz", + "integrity": "sha1-EFNaEm0ky9Zff/zfFe8uYxB2tQU=", + "devOptional": true, + "dependencies": { + "as-array": "^2.0.0", + "url-join": "0.0.1", + "valid-url": "^1" + } + }, + "node_modules/firebase-tools/node_modules/js-yaml": { + "version": "3.13.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz", + "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==", + "devOptional": true, + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/firebase-tools/node_modules/js2xmlparser": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/js2xmlparser/-/js2xmlparser-4.0.2.tgz", + "integrity": "sha512-6n4D8gLlLf1n5mNLQPRfViYzu9RATblzPEtm1SthMX1Pjao0r9YI9nw7ZIfRxQMERS87mcswrg+r/OYrPRX6jA==", + "devOptional": true, + "dependencies": { + "xmlcreate": "^2.0.4" + } + }, + "node_modules/firebase-tools/node_modules/jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/jsdoc": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsdoc/-/jsdoc-4.0.0.tgz", + "integrity": "sha512-tzTgkklbWKrlaQL2+e3NNgLcZu3NaK2vsHRx7tyHQ+H5jcB9Gx0txSd2eJWlMC/xU1+7LQu4s58Ry0RkuaEQVg==", + "devOptional": true, + "dependencies": { + "@babel/parser": "^7.9.4", + "@jsdoc/salty": "^0.2.1", + "@types/markdown-it": "^12.2.3", + "bluebird": "^3.7.2", + "catharsis": "^0.9.0", + "escape-string-regexp": "^2.0.0", + "js2xmlparser": "^4.0.2", + "klaw": "^3.0.0", + "markdown-it": "^12.3.2", + "markdown-it-anchor": "^8.4.1", + "marked": "^4.0.10", + "mkdirp": "^1.0.4", + "requizzle": "^0.2.3", + "strip-json-comments": "^3.1.0", + "underscore": "~1.13.2" + }, + "bin": { + "jsdoc": "jsdoc.js" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/firebase-tools/node_modules/jsdoc/node_modules/bluebird": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/jsdoc/node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "devOptional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/jsdoc/node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "devOptional": true, + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/firebase-tools/node_modules/jsdoc/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==", + "devOptional": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/firebase-tools/node_modules/json-bigint": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", + "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", + "devOptional": true, + "dependencies": { + "bignumber.js": "^9.0.0" + } + }, + "node_modules/firebase-tools/node_modules/json-parse-helpfulerror": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/json-parse-helpfulerror/-/json-parse-helpfulerror-1.0.3.tgz", + "integrity": "sha1-E/FM4C7tTpgSl7ZOueO5MuLdE9w=", + "devOptional": true, + "dependencies": { + "jju": "^1.1.0" + } + }, + "node_modules/firebase-tools/node_modules/json-ptr": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-ptr/-/json-ptr-3.0.1.tgz", + "integrity": "sha512-hrZ4tElT8huJUH3OwOK+d7F8PRqw09QnGM3Mm3GmqKWDyCCPCG8lGHxXOwQAj0VOxzLirOds07Kz10B5F8M8EA==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/json-schema": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", + "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==", + "devOptional": true + }, + "node_modules/firebase-tools/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==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", + "devOptional": true, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/firebase-tools/node_modules/jsonwebtoken": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.0.tgz", + "integrity": "sha512-tuGfYXxkQGDPnLJ7SibiQgVgeDgfbPq2k2ICcbgqW8WxWLBAxKQM/ZCu/IT8SOSwmaYl4dpTFCW5xZv7YbbWUw==", + "devOptional": true, + "dependencies": { + "jws": "^3.2.2", + "lodash": "^4.17.21", + "ms": "^2.1.1", + "semver": "^7.3.8" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/firebase-tools/node_modules/jsonwebtoken/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==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/jsonwebtoken/node_modules/semver": { + "version": "7.3.8", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", + "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", + "devOptional": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/firebase-tools/node_modules/jsprim": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.2.tgz", + "integrity": "sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw==", + "devOptional": true, + "dependencies": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.4.0", + "verror": "1.10.0" + }, + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/firebase-tools/node_modules/jwa": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", + "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", + "devOptional": true, + "dependencies": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/firebase-tools/node_modules/jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "devOptional": true, + "dependencies": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/firebase-tools/node_modules/klaw": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/klaw/-/klaw-3.0.0.tgz", + "integrity": "sha512-0Fo5oir+O9jnXu5EefYbVK+mHMBeEVEy2cmctR1O1NECcCkPRreJKrS6Qt/j3KC2C148Dfo9i3pCmCMsdqGr0g==", + "devOptional": true, + "dependencies": { + "graceful-fs": "^4.1.9" + } + }, + "node_modules/firebase-tools/node_modules/kuler": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/kuler/-/kuler-1.0.1.tgz", + "integrity": "sha512-J9nVUucG1p/skKul6DU3PUZrhs0LPulNaeUOox0IyXDi8S4CztTHs1gQphhuZmzXG7VOQSf6NJfKuzteQLv9gQ==", + "devOptional": true, + "dependencies": { + "colornames": "^1.1.1" + } + }, + "node_modules/firebase-tools/node_modules/lazystream": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz", + "integrity": "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==", + "devOptional": true, + "dependencies": { + "readable-stream": "^2.0.5" + }, + "engines": { + "node": ">= 0.6.3" + } + }, + "node_modules/firebase-tools/node_modules/lazystream/node_modules/readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "devOptional": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/firebase-tools/node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "devOptional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/firebase-tools/node_modules/levn": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", + "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", + "devOptional": true, + "dependencies": { + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/firebase-tools/node_modules/libsodium": { + "version": "0.7.10", + "resolved": "https://registry.npmjs.org/libsodium/-/libsodium-0.7.10.tgz", + "integrity": "sha512-eY+z7hDrDKxkAK+QKZVNv92A5KYkxfvIshtBJkmg5TSiCnYqZP3i9OO9whE79Pwgm4jGaoHgkM4ao/b9Cyu4zQ==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/libsodium-wrappers": { + "version": "0.7.10", + "resolved": "https://registry.npmjs.org/libsodium-wrappers/-/libsodium-wrappers-0.7.10.tgz", + "integrity": "sha512-pO3F1Q9NPLB/MWIhehim42b/Fwb30JNScCNh8TcQ/kIc+qGLQch8ag8wb0keK3EP5kbGakk1H8Wwo7v+36rNQg==", + "devOptional": true, + "dependencies": { + "libsodium": "^0.7.0" + } + }, + "node_modules/firebase-tools/node_modules/linkify-it": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-3.0.3.tgz", + "integrity": "sha512-ynTsyrFSdE5oZ/O9GEf00kPngmOfVwazR5GKDq6EYfhlpFug3J2zybX56a2PRRpc9P+FuSoGNAwjlbDs9jJBPQ==", + "devOptional": true, + "dependencies": { + "uc.micro": "^1.0.1" + } + }, + "node_modules/firebase-tools/node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/lodash._objecttypes": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/lodash._objecttypes/-/lodash._objecttypes-2.4.1.tgz", + "integrity": "sha1-fAt/admKH3ZSn4kLDNsbTf7BHBE=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha1-soqmKIorn8ZRA1x3EfZathkDMaY=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/lodash.defaults": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", + "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/lodash.difference": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.difference/-/lodash.difference-4.5.0.tgz", + "integrity": "sha512-dS2j+W26TQ7taQBGN8Lbbq04ssV3emRw4NY58WErlTO29pIqS0HmoT5aJ9+TUQ1N3G+JOZSji4eugsWwGp9yPA==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/lodash.flatten": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz", + "integrity": "sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/lodash.isobject": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/lodash.isobject/-/lodash.isobject-2.4.1.tgz", + "integrity": "sha1-Wi5H/mmVPx7mMafrof5k0tBlWPU=", + "devOptional": true, + "dependencies": { + "lodash._objecttypes": "~2.4.1" + } + }, + "node_modules/firebase-tools/node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/lodash.snakecase": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.snakecase/-/lodash.snakecase-4.1.1.tgz", + "integrity": "sha1-OdcUo1NXFHg3rv1ktdy7Fr7Nj40=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/lodash.union": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.union/-/lodash.union-4.6.0.tgz", + "integrity": "sha512-c4pB2CdGrGdjMKYLA+XiRDO7Y0PRQbm/Gzg8qMj+QH+pFVAoTp5sBpO0odL3FjoPCGjK96p6qsP+yQoiLoOBcw==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "devOptional": true, + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/firebase-tools/node_modules/log-symbols/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "devOptional": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/firebase-tools/node_modules/log-symbols/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "devOptional": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/firebase-tools/node_modules/log-symbols/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "devOptional": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/firebase-tools/node_modules/log-symbols/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/log-symbols/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "devOptional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/log-symbols/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "devOptional": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/logform": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/logform/-/logform-2.1.2.tgz", + "integrity": "sha512-+lZh4OpERDBLqjiwDLpAWNQu6KMjnlXH2ByZwCuSqVPJletw0kTWJf5CgSNAUKn1KUkv3m2cUz/LK8zyEy7wzQ==", + "devOptional": true, + "dependencies": { + "colors": "^1.2.1", + "fast-safe-stringify": "^2.0.4", + "fecha": "^2.3.3", + "ms": "^2.1.1", + "triple-beam": "^1.3.0" + } + }, + "node_modules/firebase-tools/node_modules/logform/node_modules/colors": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz", + "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==", + "devOptional": true, + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/firebase-tools/node_modules/logform/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/long": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", + "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "devOptional": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/firebase-tools/node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "devOptional": true, + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/firebase-tools/node_modules/make-dir/node_modules/semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "devOptional": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/firebase-tools/node_modules/make-fetch-happen": { + "version": "10.2.1", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-10.2.1.tgz", + "integrity": "sha512-NgOPbRiaQM10DYXvN3/hhGVI2M5MtITFryzBGxHM5p4wnFxsVCbxkrBrDsk+EZ5OB4jEOT7AjDxtdF+KVEFT7w==", + "optional": true, + "dependencies": { + "agentkeepalive": "^4.2.1", + "cacache": "^16.1.0", + "http-cache-semantics": "^4.1.0", + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.0", + "is-lambda": "^1.0.1", + "lru-cache": "^7.7.1", + "minipass": "^3.1.6", + "minipass-collect": "^1.0.2", + "minipass-fetch": "^2.0.3", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^0.6.3", + "promise-retry": "^2.0.1", + "socks-proxy-agent": "^7.0.0", + "ssri": "^9.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/firebase-tools/node_modules/make-fetch-happen/node_modules/@tootallnate/once": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", + "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", + "optional": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/firebase-tools/node_modules/make-fetch-happen/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "optional": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/firebase-tools/node_modules/make-fetch-happen/node_modules/http-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", + "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", + "optional": true, + "dependencies": { + "@tootallnate/once": "2", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/firebase-tools/node_modules/make-fetch-happen/node_modules/lru-cache": { + "version": "7.14.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.14.0.tgz", + "integrity": "sha512-EIRtP1GrSJny0dqb50QXRUNBxHJhcpxHC++M5tD7RYbvLLn5KVWKsbyswSSqDuU15UFi3bgTQIY8nhDMeF6aDQ==", + "optional": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/firebase-tools/node_modules/make-fetch-happen/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "optional": true + }, + "node_modules/firebase-tools/node_modules/make-fetch-happen/node_modules/socks-proxy-agent": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-7.0.0.tgz", + "integrity": "sha512-Fgl0YPZ902wEsAyiQ+idGd1A7rSFx/ayC1CQVMw5P+EQx2V0SgpGtf6OKFhVjPflPUl9YMmEOnmfjCdMUsygww==", + "optional": true, + "dependencies": { + "agent-base": "^6.0.2", + "debug": "^4.3.3", + "socks": "^2.6.2" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/firebase-tools/node_modules/markdown-it": { + "version": "12.3.2", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-12.3.2.tgz", + "integrity": "sha512-TchMembfxfNVpHkbtriWltGWc+m3xszaRD0CZup7GFFhzIgQqxIfn3eGj1yZpfuflzPvfkt611B2Q/Bsk1YnGg==", + "devOptional": true, + "dependencies": { + "argparse": "^2.0.1", + "entities": "~2.1.0", + "linkify-it": "^3.0.1", + "mdurl": "^1.0.1", + "uc.micro": "^1.0.5" + }, + "bin": { + "markdown-it": "bin/markdown-it.js" + } + }, + "node_modules/firebase-tools/node_modules/markdown-it-anchor": { + "version": "8.6.6", + "resolved": "https://registry.npmjs.org/markdown-it-anchor/-/markdown-it-anchor-8.6.6.tgz", + "integrity": "sha512-jRW30YGywD2ESXDc+l17AiritL0uVaSnWsb26f+68qaW9zgbIIr1f4v2Nsvc0+s0Z2N3uX6t/yAw7BwCQ1wMsA==", + "devOptional": true, + "peerDependencies": { + "@types/markdown-it": "*", + "markdown-it": "*" + } + }, + "node_modules/firebase-tools/node_modules/markdown-it/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==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/marked": { + "version": "4.2.12", + "resolved": "https://registry.npmjs.org/marked/-/marked-4.2.12.tgz", + "integrity": "sha512-yr8hSKa3Fv4D3jdZmtMMPghgVt6TWbk86WQaWhDloQjRSQhMMYCAro7jP7VDJrjjdV8pxVxMssXS8B8Y5DZ5aw==", + "devOptional": true, + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 12" + } + }, + "node_modules/firebase-tools/node_modules/marked-terminal": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/marked-terminal/-/marked-terminal-5.1.1.tgz", + "integrity": "sha512-+cKTOx9P4l7HwINYhzbrBSyzgxO2HaHKGZGuB1orZsMIgXYaJyfidT81VXRdpelW/PcHEWxywscePVgI/oUF6g==", + "devOptional": true, + "dependencies": { + "ansi-escapes": "^5.0.0", + "cardinal": "^2.1.1", + "chalk": "^5.0.0", + "cli-table3": "^0.6.1", + "node-emoji": "^1.11.0", + "supports-hyperlinks": "^2.2.0" + }, + "engines": { + "node": ">=14.13.1 || >=16.0.0" + }, + "peerDependencies": { + "marked": "^1.0.0 || ^2.0.0 || ^3.0.0 || ^4.0.0" + } + }, + "node_modules/firebase-tools/node_modules/marked-terminal/node_modules/chalk": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.0.1.tgz", + "integrity": "sha512-Fo07WOYGqMfCWHOzSXOt2CxDbC6skS/jO9ynEcmpANMoPrD+W1r1K6Vx7iNm+AQmETU1Xr2t+n8nzkV9t6xh3w==", + "devOptional": true, + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/firebase-tools/node_modules/mdurl": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz", + "integrity": "sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=", + "devOptional": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/firebase-tools/node_modules/merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=", + "devOptional": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/firebase-tools/node_modules/mime": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.5.2.tgz", + "integrity": "sha512-tqkh47FzKeCPD2PUiPB6pkbMzsCasjxAfC62/Wap5qrUWcb+sFasXUC5I3gYM5iBM8v/Qpn4UK0x+j0iHyFPDg==", + "devOptional": true, + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/firebase-tools/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "devOptional": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/firebase-tools/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "devOptional": true, + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/firebase-tools/node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "devOptional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/firebase-tools/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "devOptional": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/firebase-tools/node_modules/minimist": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", + "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/minipass": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.1.6.tgz", + "integrity": "sha512-rty5kpw9/z8SX9dmxblFA6edItUmwJgMeYDZRrwlIVN27i8gysGbznJwUggw2V/FVqFSDdWy040ZPS811DYAqQ==", + "devOptional": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/minipass-collect": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-1.0.2.tgz", + "integrity": "sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==", + "optional": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/firebase-tools/node_modules/minipass-fetch": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-2.1.2.tgz", + "integrity": "sha512-LT49Zi2/WMROHYoqGgdlQIZh8mLPZmOrN2NdJjMXxYe4nkN6FUyuPuOAOedNJDrx0IRGg9+4guZewtp8hE6TxA==", + "optional": true, + "dependencies": { + "minipass": "^3.1.6", + "minipass-sized": "^1.0.3", + "minizlib": "^2.1.2" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + }, + "optionalDependencies": { + "encoding": "^0.1.13" + } + }, + "node_modules/firebase-tools/node_modules/minipass-flush": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz", + "integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==", + "optional": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/firebase-tools/node_modules/minipass-pipeline": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", + "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", + "optional": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/minipass-sized": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz", + "integrity": "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==", + "optional": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "devOptional": true, + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/firebase-tools/node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "devOptional": true, + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/firebase-tools/node_modules/morgan": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.0.tgz", + "integrity": "sha512-AbegBVI4sh6El+1gNwvD5YIck7nSA36weD7xvIxG4in80j/UoK8AEGaWnnz8v1GxonMCltmlNs5ZKbGvl9b1XQ==", + "devOptional": true, + "dependencies": { + "basic-auth": "~2.0.1", + "debug": "2.6.9", + "depd": "~2.0.0", + "on-finished": "~2.3.0", + "on-headers": "~1.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/firebase-tools/node_modules/morgan/node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "devOptional": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/firebase-tools/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/mute-stream": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", + "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/nan": { + "version": "2.16.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.16.0.tgz", + "integrity": "sha512-UdAqHyFngu7TfQKsCBgAA6pWDkT8MAO7d0jyOecVhN5354xbLqdn8mV9Tat9gepAupm0bt2DbeaSC8vS52MuFA==", + "optional": true + }, + "node_modules/firebase-tools/node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "devOptional": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/firebase-tools/node_modules/netmask": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.0.2.tgz", + "integrity": "sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==", + "devOptional": true, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/firebase-tools/node_modules/nice-try": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", + "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/node-emoji": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-1.11.0.tgz", + "integrity": "sha512-wo2DpQkQp7Sjm2A0cq+sN7EHKO6Sl0ctXeBdFZrL9T9+UywORbufTcTZxom8YqpLQt/FqNMUkOpkZrJVYSKD3A==", + "devOptional": true, + "dependencies": { + "lodash": "^4.17.21" + } + }, + "node_modules/firebase-tools/node_modules/node-fetch": { + "version": "2.6.7", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", + "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", + "devOptional": true, + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/firebase-tools/node_modules/node-forge": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", + "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==", + "devOptional": true, + "engines": { + "node": ">= 6.13.0" + } + }, + "node_modules/firebase-tools/node_modules/node-gyp": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-9.1.0.tgz", + "integrity": "sha512-HkmN0ZpQJU7FLbJauJTHkHlSVAXlNGDAzH/VYFZGDOnFyn/Na3GlNJfkudmufOdS6/jNFhy88ObzL7ERz9es1g==", + "optional": true, + "dependencies": { + "env-paths": "^2.2.0", + "glob": "^7.1.4", + "graceful-fs": "^4.2.6", + "make-fetch-happen": "^10.0.3", + "nopt": "^5.0.0", + "npmlog": "^6.0.0", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "tar": "^6.1.2", + "which": "^2.0.2" + }, + "bin": { + "node-gyp": "bin/node-gyp.js" + }, + "engines": { + "node": "^12.22 || ^14.13 || >=16" + } + }, + "node_modules/firebase-tools/node_modules/node-gyp/node_modules/semver": { + "version": "7.3.7", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz", + "integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==", + "optional": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/firebase-tools/node_modules/node-gyp/node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "optional": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/firebase-tools/node_modules/nopt": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", + "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", + "optional": true, + "dependencies": { + "abbrev": "1" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/firebase-tools/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==", + "devOptional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/firebase-tools/node_modules/npmlog": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-6.0.2.tgz", + "integrity": "sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg==", + "optional": true, + "dependencies": { + "are-we-there-yet": "^3.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^4.0.3", + "set-blocking": "^2.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/firebase-tools/node_modules/oauth-sign": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", + "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", + "devOptional": true, + "engines": { + "node": "*" + } + }, + "node_modules/firebase-tools/node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", + "devOptional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/firebase-tools/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==", + "devOptional": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/firebase-tools/node_modules/object-inspect": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.2.tgz", + "integrity": "sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==", + "devOptional": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/firebase-tools/node_modules/on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", + "devOptional": true, + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/firebase-tools/node_modules/on-headers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", + "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", + "devOptional": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/firebase-tools/node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "devOptional": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/firebase-tools/node_modules/one-time": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/one-time/-/one-time-0.0.4.tgz", + "integrity": "sha1-+M33eISCb+Tf+T46nMN7HkSAdC4=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "devOptional": true, + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/firebase-tools/node_modules/open": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/open/-/open-6.4.0.tgz", + "integrity": "sha512-IFenVPgF70fSm1keSd2iDBIDIBZkroLeuffXq+wKTzTJlBpesFWojV9lb8mzOfaAzM1sr7HQHuO0vtV0zYekGg==", + "devOptional": true, + "dependencies": { + "is-wsl": "^1.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/openapi3-ts": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/openapi3-ts/-/openapi3-ts-2.0.1.tgz", + "integrity": "sha512-v6X3iwddhi276siej96jHGIqTx3wzVfMTmpGJEQDt7GPI7pI6sywItURLzpEci21SBRpPN/aOWSF5mVfFVNmcg==", + "devOptional": true, + "dependencies": { + "yaml": "^1.10.0" + } + }, + "node_modules/firebase-tools/node_modules/optionator": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", + "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", + "devOptional": true, + "dependencies": { + "deep-is": "~0.1.3", + "fast-levenshtein": "~2.0.6", + "levn": "~0.3.0", + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2", + "word-wrap": "~1.2.3" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/firebase-tools/node_modules/ora": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", + "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", + "devOptional": true, + "dependencies": { + "bl": "^4.1.0", + "chalk": "^4.1.0", + "cli-cursor": "^3.1.0", + "cli-spinners": "^2.5.0", + "is-interactive": "^1.0.0", + "is-unicode-supported": "^0.1.0", + "log-symbols": "^4.1.0", + "strip-ansi": "^6.0.0", + "wcwidth": "^1.0.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/firebase-tools/node_modules/ora/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "devOptional": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/firebase-tools/node_modules/ora/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "devOptional": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/firebase-tools/node_modules/ora/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "devOptional": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/firebase-tools/node_modules/ora/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/ora/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "devOptional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/ora/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "devOptional": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", + "devOptional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/firebase-tools/node_modules/p-defer": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-defer/-/p-defer-3.0.0.tgz", + "integrity": "sha512-ugZxsxmtTln604yeYd29EGrNhazN2lywetzpKhfmQjW/VJmhpDmWbiX+h0zL8V91R0UXkhb3KtPmyq9PZw3aYw==", + "devOptional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/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==", + "devOptional": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/firebase-tools/node_modules/pac-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-5.0.0.tgz", + "integrity": "sha512-CcFG3ZtnxO8McDigozwE3AqAw15zDvGH+OjXO4kzf7IkEKkQ4gxQ+3sdF50WmhQ4P/bVusXcqNE2S3XrNURwzQ==", + "devOptional": true, + "dependencies": { + "@tootallnate/once": "1", + "agent-base": "6", + "debug": "4", + "get-uri": "3", + "http-proxy-agent": "^4.0.1", + "https-proxy-agent": "5", + "pac-resolver": "^5.0.0", + "raw-body": "^2.2.0", + "socks-proxy-agent": "5" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/firebase-tools/node_modules/pac-proxy-agent/node_modules/debug": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", + "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", + "devOptional": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/firebase-tools/node_modules/pac-proxy-agent/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/pac-resolver": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-5.0.0.tgz", + "integrity": "sha512-H+/A6KitiHNNW+bxBKREk2MCGSxljfqRX76NjummWEYIat7ldVXRU3dhRIE3iXZ0nvGBk6smv3nntxKkzRL8NA==", + "devOptional": true, + "dependencies": { + "degenerator": "^3.0.1", + "ip": "^1.1.5", + "netmask": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/firebase-tools/node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "devOptional": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/firebase-tools/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": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "devOptional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/firebase-tools/node_modules/path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=", + "devOptional": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/firebase-tools/node_modules/path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "devOptional": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/firebase-tools/node_modules/portfinder": { + "version": "1.0.32", + "resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.32.tgz", + "integrity": "sha512-on2ZJVVDXRADWE6jnQaX0ioEylzgBpQk8r55NE4wjXW1ZxO+BgDlY6DXwj20i0V8eB4SenDQ00WEaxfiIQPcxg==", + "devOptional": true, + "dependencies": { + "async": "^2.6.4", + "debug": "^3.2.7", + "mkdirp": "^0.5.6" + }, + "engines": { + "node": ">= 0.12.0" + } + }, + "node_modules/firebase-tools/node_modules/portfinder/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "devOptional": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/firebase-tools/node_modules/portfinder/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==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/prelude-ls": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", + "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=", + "devOptional": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/firebase-tools/node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "devOptional": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/firebase-tools/node_modules/promise-breaker": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/promise-breaker/-/promise-breaker-5.0.0.tgz", + "integrity": "sha512-mgsWQuG4kJ1dtO6e/QlNDLFtMkMzzecsC69aI5hlLEjGHFNpHrvGhFi4LiK5jg2SMQj74/diH+wZliL9LpGsyA==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/promise-inflight": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", + "integrity": "sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==", + "optional": true + }, + "node_modules/firebase-tools/node_modules/promise-retry": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", + "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", + "optional": true, + "dependencies": { + "err-code": "^2.0.2", + "retry": "^0.12.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/firebase-tools/node_modules/promise-retry/node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "optional": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/firebase-tools/node_modules/proto-list": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", + "integrity": "sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/proto3-json-serializer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proto3-json-serializer/-/proto3-json-serializer-1.1.0.tgz", + "integrity": "sha512-SjXwUWe/vANGs/mJJTbw5++7U67nwsymg7qsoPtw6GiXqw3kUy8ByojrlEdVE2efxAdKreX8WkDafxvYW95ZQg==", + "devOptional": true, + "dependencies": { + "protobufjs": "^7.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/firebase-tools/node_modules/proto3-json-serializer/node_modules/long": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/long/-/long-5.2.1.tgz", + "integrity": "sha512-GKSNGeNAtw8IryjjkhZxuKB3JzlcLTwjtiQCHKvqQet81I93kXslhDQruGI/QsddO83mcDToBVy7GqGS/zYf/A==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/proto3-json-serializer/node_modules/protobufjs": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.1.2.tgz", + "integrity": "sha512-4ZPTPkXCdel3+L81yw3dG6+Kq3umdWKh7Dc7GW/CpNk4SX3hK58iPCWeCyhVTDrbkNeKrYNZ7EojM5WDaEWTLQ==", + "devOptional": true, + "hasInstallScript": true, + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/node": ">=13.7.0", + "long": "^5.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/firebase-tools/node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "devOptional": true, + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/firebase-tools/node_modules/proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-5.0.0.tgz", + "integrity": "sha512-gkH7BkvLVkSfX9Dk27W6TyNOWWZWRilRfk1XxGNWOYJ2TuedAv1yFpCaU9QSBmBe716XOTNpYNOzhysyw8xn7g==", + "devOptional": true, + "dependencies": { + "agent-base": "^6.0.0", + "debug": "4", + "http-proxy-agent": "^4.0.0", + "https-proxy-agent": "^5.0.0", + "lru-cache": "^5.1.1", + "pac-proxy-agent": "^5.0.0", + "proxy-from-env": "^1.0.0", + "socks-proxy-agent": "^5.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/firebase-tools/node_modules/proxy-agent/node_modules/debug": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", + "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", + "devOptional": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/firebase-tools/node_modules/proxy-agent/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==", + "devOptional": true, + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/firebase-tools/node_modules/proxy-agent/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/proxy-agent/node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/psl": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz", + "integrity": "sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "devOptional": true, + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/firebase-tools/node_modules/punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", + "devOptional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/firebase-tools/node_modules/pupa": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/pupa/-/pupa-2.1.1.tgz", + "integrity": "sha512-l1jNAspIBSFqbT+y+5FosojNpVpF94nlI+wDUpqP9enwOTfHx9f0gh5nB96vl+6yTpsJsypeNrwfzPrKuHB41A==", + "devOptional": true, + "dependencies": { + "escape-goat": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/qs": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", + "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "devOptional": true, + "dependencies": { + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/firebase-tools/node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "devOptional": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/firebase-tools/node_modules/raw-body": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", + "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", + "devOptional": true, + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/firebase-tools/node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "devOptional": true, + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/firebase-tools/node_modules/re2": { + "version": "1.17.7", + "resolved": "https://registry.npmjs.org/re2/-/re2-1.17.7.tgz", + "integrity": "sha512-X8GSuiBoVWwcjuppqSjsIkRxNUKDdjhkO9SBekQbZ2ksqWUReCy7DQPWOVpoTnpdtdz5PIpTTxTFzvJv5UMfjA==", + "hasInstallScript": true, + "optional": true, + "dependencies": { + "install-artifact-from-github": "^1.3.1", + "nan": "^2.16.0", + "node-gyp": "^9.0.0" + } + }, + "node_modules/firebase-tools/node_modules/readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "devOptional": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/firebase-tools/node_modules/readdir-glob": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/readdir-glob/-/readdir-glob-1.1.1.tgz", + "integrity": "sha512-91/k1EzZwDx6HbERR+zucygRFfiPl2zkIYZtv3Jjr6Mn7SkKcVct8aVO+sSRiGMc6fLf72du3d92/uY63YPdEA==", + "devOptional": true, + "dependencies": { + "minimatch": "^3.0.4" + } + }, + "node_modules/firebase-tools/node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "devOptional": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/firebase-tools/node_modules/redeyed": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/redeyed/-/redeyed-2.1.1.tgz", + "integrity": "sha1-iYS1gV2ZyyIEacme7v/jiRPmzAs=", + "devOptional": true, + "dependencies": { + "esprima": "~4.0.0" + } + }, + "node_modules/firebase-tools/node_modules/registry-url": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/registry-url/-/registry-url-5.1.0.tgz", + "integrity": "sha512-8acYXXTI0AkQv6RAOjE3vOaIXZkT9wo4LOFbBKYQEEnnMNBpKqdUrI6S4NT0KPIo/WVvJ5tE/X5LF/TQUf0ekw==", + "devOptional": true, + "dependencies": { + "rc": "^1.2.8" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/request": { + "version": "2.88.2", + "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", + "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==", + "deprecated": "request has been deprecated, see https://github.com/request/request/issues/3142", + "devOptional": true, + "dependencies": { + "aws-sign2": "~0.7.0", + "aws4": "^1.8.0", + "caseless": "~0.12.0", + "combined-stream": "~1.0.6", + "extend": "~3.0.2", + "forever-agent": "~0.6.1", + "form-data": "~2.3.2", + "har-validator": "~5.1.3", + "http-signature": "~1.2.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.19", + "oauth-sign": "~0.9.0", + "performance-now": "^2.1.0", + "qs": "~6.5.2", + "safe-buffer": "^5.1.2", + "tough-cookie": "~2.5.0", + "tunnel-agent": "^0.6.0", + "uuid": "^3.3.2" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/firebase-tools/node_modules/request/node_modules/form-data": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", + "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", + "devOptional": true, + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 0.12" + } + }, + "node_modules/firebase-tools/node_modules/request/node_modules/qs": { + "version": "6.5.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.3.tgz", + "integrity": "sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==", + "devOptional": true, + "engines": { + "node": ">=0.6" + } + }, + "node_modules/firebase-tools/node_modules/request/node_modules/uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", + "deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.", + "devOptional": true, + "bin": { + "uuid": "bin/uuid" + } + }, + "node_modules/firebase-tools/node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", + "devOptional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/firebase-tools/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==", + "devOptional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/firebase-tools/node_modules/requizzle": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/requizzle/-/requizzle-0.2.4.tgz", + "integrity": "sha512-JRrFk1D4OQ4SqovXOgdav+K8EAhSB/LJZqCz8tbX0KObcdeM15Ss59ozWMBWmmINMagCwmqn4ZNryUGpBsl6Jw==", + "devOptional": true, + "dependencies": { + "lodash": "^4.17.21" + } + }, + "node_modules/firebase-tools/node_modules/restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "devOptional": true, + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/retry": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "devOptional": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/firebase-tools/node_modules/retry-request": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/retry-request/-/retry-request-5.0.2.tgz", + "integrity": "sha512-wfI3pk7EE80lCIXprqh7ym48IHYdwmAAzESdbU8Q9l7pnRCk9LEhpbOTNKjz6FARLm/Bl5m+4F0ABxOkYUujSQ==", + "devOptional": true, + "dependencies": { + "debug": "^4.1.1", + "extend": "^3.0.2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/firebase-tools/node_modules/retry-request/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "devOptional": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/firebase-tools/node_modules/retry-request/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "devOptional": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/firebase-tools/node_modules/router": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/router/-/router-1.3.5.tgz", + "integrity": "sha512-kozCJZUhuSJ5VcLhSb3F8fsmGXy+8HaDbKCAerR1G6tq3mnMZFMuSohbFvGv1c5oMFipijDjRZuuN/Sq5nMf3g==", + "devOptional": true, + "dependencies": { + "array-flatten": "3.0.0", + "debug": "2.6.9", + "methods": "~1.1.2", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.7", + "setprototypeof": "1.2.0", + "utils-merge": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/firebase-tools/node_modules/router/node_modules/array-flatten": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-3.0.0.tgz", + "integrity": "sha512-zPMVc3ZYlGLNk4mpK1NzP2wg0ml9t7fUgDsayR5Y5rSzxQilzR9FGu/EH2jQOcKSAeAfWeylyW8juy3OkWRvNA==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/run-async": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", + "integrity": "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==", + "devOptional": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/firebase-tools/node_modules/rxjs": { + "version": "7.5.6", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.5.6.tgz", + "integrity": "sha512-dnyv2/YsXhnm461G+R/Pe5bWP41Nm6LBXEYWI6eiFP4fiwx6WRI/CD0zbdVAudd9xwLEF2IDcKXLHit0FYjUzw==", + "devOptional": true, + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/firebase-tools/node_modules/rxjs/node_modules/tslib": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz", + "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "devOptional": true, + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/firebase-tools/node_modules/semver-diff": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/semver-diff/-/semver-diff-3.1.1.tgz", + "integrity": "sha512-GX0Ix/CJcHyB8c4ykpHGIAvLyOwOobtM/8d+TQkAd81/bEjgPHrfba41Vpesr7jX/t8Uh+R3EX9eAS5be+jQYg==", + "devOptional": true, + "dependencies": { + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/semver-diff/node_modules/semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "devOptional": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/firebase-tools/node_modules/send": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", + "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "devOptional": true, + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/firebase-tools/node_modules/send/node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "devOptional": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/firebase-tools/node_modules/send/node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "devOptional": true, + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/firebase-tools/node_modules/send/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==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/send/node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "devOptional": true, + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/firebase-tools/node_modules/send/node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "devOptional": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/firebase-tools/node_modules/serve-static": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", + "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", + "devOptional": true, + "dependencies": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.18.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/firebase-tools/node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", + "optional": true + }, + "node_modules/firebase-tools/node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", + "devOptional": true, + "dependencies": { + "shebang-regex": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/firebase-tools/node_modules/shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", + "devOptional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/firebase-tools/node_modules/side-channel": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", + "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "devOptional": true, + "dependencies": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/firebase-tools/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/simple-swizzle": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", + "integrity": "sha1-pNprY1/8zMoz9w0Xy5JZLeleVXo=", + "devOptional": true, + "dependencies": { + "is-arrayish": "^0.3.1" + } + }, + "node_modules/firebase-tools/node_modules/simple-swizzle/node_modules/is-arrayish": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", + "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "devOptional": true, + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/firebase-tools/node_modules/socks": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.7.0.tgz", + "integrity": "sha512-scnOe9y4VuiNUULJN72GrM26BNOjVsfPXI+j+98PkyEfsIXroa5ofyjT+FzGvn/xHs73U2JtoBYAVx9Hl4quSA==", + "devOptional": true, + "dependencies": { + "ip": "^2.0.0", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.13.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/firebase-tools/node_modules/socks-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-5.0.1.tgz", + "integrity": "sha512-vZdmnjb9a2Tz6WEQVIurybSwElwPxMZaIc7PzqbJTrezcKNznv6giT7J7tZDZ1BojVaa1jvO/UiUdhDVB0ACoQ==", + "devOptional": true, + "dependencies": { + "agent-base": "^6.0.2", + "debug": "4", + "socks": "^2.3.3" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/firebase-tools/node_modules/socks-proxy-agent/node_modules/debug": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", + "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", + "devOptional": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/firebase-tools/node_modules/socks-proxy-agent/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/socks/node_modules/ip": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ip/-/ip-2.0.0.tgz", + "integrity": "sha512-WKa+XuLG1A1R0UWhl2+1XQSi+fZWMsYKffMZTTYsiZaUD8k2yDAj5atimTUD2TZkyCkNEeYE5NhFZmupOGtjYQ==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/firebase-tools/node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/sshpk": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz", + "integrity": "sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==", + "devOptional": true, + "dependencies": { + "asn1": "~0.2.3", + "assert-plus": "^1.0.0", + "bcrypt-pbkdf": "^1.0.0", + "dashdash": "^1.12.0", + "ecc-jsbn": "~0.1.1", + "getpass": "^0.1.1", + "jsbn": "~0.1.0", + "safer-buffer": "^2.0.2", + "tweetnacl": "~0.14.0" + }, + "bin": { + "sshpk-conv": "bin/sshpk-conv", + "sshpk-sign": "bin/sshpk-sign", + "sshpk-verify": "bin/sshpk-verify" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/firebase-tools/node_modules/ssri": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-9.0.1.tgz", + "integrity": "sha512-o57Wcn66jMQvfHG1FlYbWeZWW/dHZhJXjpIcTfXldXEk5nz5lStPo3mK0OJQfGR3RbZUlbISexbljkJzuEj/8Q==", + "optional": true, + "dependencies": { + "minipass": "^3.1.1" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/firebase-tools/node_modules/stack-trace": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", + "integrity": "sha1-VHxws0fo0ytOEI6hoqFZ5f3eGcA=", + "devOptional": true, + "engines": { + "node": "*" + } + }, + "node_modules/firebase-tools/node_modules/statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=", + "devOptional": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/firebase-tools/node_modules/stream-chain": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/stream-chain/-/stream-chain-2.2.4.tgz", + "integrity": "sha512-9lsl3YM53V5N/I1C2uJtc3Kavyi3kNYN83VkKb/bMWRk7D9imiFyUPYa0PoZbLohSVOX1mYE9YsmwObZUsth6Q==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/stream-json": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/stream-json/-/stream-json-1.7.3.tgz", + "integrity": "sha512-Y6dXn9KKWSwxOqnvHGcdZy1PK+J+7alBwHCeU3W9oRqm4ilLRA0XSPmd1tWwhg7tv9EIxJTMWh7KF15tYelKJg==", + "devOptional": true, + "dependencies": { + "stream-chain": "^2.2.4" + } + }, + "node_modules/firebase-tools/node_modules/stream-shift": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.1.tgz", + "integrity": "sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "devOptional": true, + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/firebase-tools/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "devOptional": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/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==", + "devOptional": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/strip-ansi/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==", + "devOptional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", + "devOptional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/firebase-tools/node_modules/superstatic": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/superstatic/-/superstatic-9.0.3.tgz", + "integrity": "sha512-e/tmW0bsnQ/33ivK6y3CapJT0Ovy4pk/ohNPGhIAGU2oasoNLRQ1cv6enua09NU9w6Y0H/fBu07cjzuiWvLXxw==", + "devOptional": true, + "dependencies": { + "basic-auth-connect": "^1.0.0", + "commander": "^10.0.0", + "compression": "^1.7.0", + "connect": "^3.7.0", + "destroy": "^1.0.4", + "fast-url-parser": "^1.1.3", + "glob-slasher": "^1.0.1", + "is-url": "^1.2.2", + "join-path": "^1.1.1", + "lodash": "^4.17.19", + "mime-types": "^2.1.35", + "minimatch": "^6.1.6", + "morgan": "^1.8.2", + "on-finished": "^2.2.0", + "on-headers": "^1.0.0", + "path-to-regexp": "^1.8.0", + "router": "^1.3.1", + "update-notifier-cjs": "^5.1.6" + }, + "bin": { + "superstatic": "lib/bin/server.js" + }, + "engines": { + "node": "^14.18.0 || >=16.4.0" + }, + "optionalDependencies": { + "re2": "^1.17.7" + } + }, + "node_modules/firebase-tools/node_modules/superstatic/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "devOptional": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/firebase-tools/node_modules/superstatic/node_modules/commander": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.0.tgz", + "integrity": "sha512-zS5PnTI22FIRM6ylNW8G4Ap0IEOyk62fhLSD0+uHRT9McRCLGpkVNvao4bjimpK/GShynyQkFFxHhwMcETmduA==", + "devOptional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/firebase-tools/node_modules/superstatic/node_modules/isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/superstatic/node_modules/minimatch": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-6.2.0.tgz", + "integrity": "sha512-sauLxniAmvnhhRjFwPNnJKaPFYyddAgbYdeUpHULtCT/GhzdCx/MDNy+Y40lBxTQUrMzDE8e0S43Z5uqfO0REg==", + "devOptional": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/firebase-tools/node_modules/superstatic/node_modules/path-to-regexp": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz", + "integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==", + "devOptional": true, + "dependencies": { + "isarray": "0.0.1" + } + }, + "node_modules/firebase-tools/node_modules/supports-hyperlinks": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-2.2.0.tgz", + "integrity": "sha512-6sXEzV5+I5j8Bmq9/vUphGRM/RJNT9SCURJLjwfOg51heRtguGWDzcaBlgAzKhQa0EVNpPEKzQuBwZ8S8WaCeQ==", + "devOptional": true, + "dependencies": { + "has-flag": "^4.0.0", + "supports-color": "^7.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/supports-hyperlinks/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==", + "devOptional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/supports-hyperlinks/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==", + "devOptional": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/tar": { + "version": "6.1.11", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.1.11.tgz", + "integrity": "sha512-an/KZQzQUkZCkuoAA64hM92X0Urb6VpRhAFllDzz44U2mcD5scmT3zBc4VgVpkugF580+DQn8eAFSyoQt0tznA==", + "devOptional": true, + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^3.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/firebase-tools/node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "devOptional": true, + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/firebase-tools/node_modules/tar/node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "devOptional": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/firebase-tools/node_modules/tar/node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "devOptional": true, + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/firebase-tools/node_modules/tcp-port-used": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/tcp-port-used/-/tcp-port-used-1.0.2.tgz", + "integrity": "sha512-l7ar8lLUD3XS1V2lfoJlCBaeoaWo/2xfYt81hM7VlvR4RrMVFqfmzfhLVk40hAb368uitje5gPtBRL1m/DGvLA==", + "devOptional": true, + "dependencies": { + "debug": "4.3.1", + "is2": "^2.0.6" + } + }, + "node_modules/firebase-tools/node_modules/tcp-port-used/node_modules/debug": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "devOptional": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/firebase-tools/node_modules/tcp-port-used/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/text-hex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", + "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/tmp": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz", + "integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==", + "devOptional": true, + "dependencies": { + "rimraf": "^3.0.0" + }, + "engines": { + "node": ">=8.17.0" + } + }, + "node_modules/firebase-tools/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==", + "devOptional": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/firebase-tools/node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "devOptional": true, + "engines": { + "node": ">=0.6" + } + }, + "node_modules/firebase-tools/node_modules/tough-cookie": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", + "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", + "devOptional": true, + "dependencies": { + "psl": "^1.1.28", + "punycode": "^2.1.1" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/firebase-tools/node_modules/toxic": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toxic/-/toxic-1.0.1.tgz", + "integrity": "sha512-WI3rIGdcaKULYg7KVoB0zcjikqvcYYvcuT6D89bFPz2rVR0Rl0PK6x8/X62rtdLtBKIE985NzVf/auTtGegIIg==", + "devOptional": true, + "dependencies": { + "lodash": "^4.17.10" + } + }, + "node_modules/firebase-tools/node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/triple-beam": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.3.0.tgz", + "integrity": "sha512-XrHUvV5HpdLmIj4uVMxHggLbFSZYIn7HEWsqePZcI50pco+MPqJ50wMGY794X7AOOhxOBAjbkqfAbEe/QMp2Lw==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", + "devOptional": true, + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/firebase-tools/node_modules/tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/type-check": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", + "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", + "devOptional": true, + "dependencies": { + "prelude-ls": "~1.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/firebase-tools/node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "devOptional": true, + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/firebase-tools/node_modules/typedarray-to-buffer": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", + "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", + "devOptional": true, + "dependencies": { + "is-typedarray": "^1.0.0" + } + }, + "node_modules/firebase-tools/node_modules/uc.micro": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz", + "integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/uglify-js": { + "version": "3.17.4", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.17.4.tgz", + "integrity": "sha512-T9q82TJI9e/C1TAxYvfb16xO120tMVFZrGA3f9/P4424DNu6ypK103y0GPFVa17yotwSyZW5iYXgjYHkGrJW/g==", + "devOptional": true, + "bin": { + "uglifyjs": "bin/uglifyjs" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/firebase-tools/node_modules/underscore": { + "version": "1.13.6", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.6.tgz", + "integrity": "sha512-+A5Sja4HP1M08MaXya7p5LvjuM7K6q/2EaC0+iovj/wOcMsTzMvDFbasi/oSapiwOlt252IqsKqPjCl7huKS0A==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/unique-filename": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-2.0.1.tgz", + "integrity": "sha512-ODWHtkkdx3IAR+veKxFV+VBkUMcN+FaqzUUd7IZzt+0zhDZFPFxhlqwPF3YQvMHx1TD0tdgYl+kuPnJ8E6ql7A==", + "optional": true, + "dependencies": { + "unique-slug": "^3.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/firebase-tools/node_modules/unique-slug": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-3.0.0.tgz", + "integrity": "sha512-8EyMynh679x/0gqE9fT9oilG+qEt+ibFyqjuVTsZn1+CMxH+XLlpvr2UZx4nVcCwTpx81nICr2JQFkM+HPLq4w==", + "optional": true, + "dependencies": { + "imurmurhash": "^0.1.4" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/firebase-tools/node_modules/universal-analytics": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/universal-analytics/-/universal-analytics-0.5.3.tgz", + "integrity": "sha512-HXSMyIcf2XTvwZ6ZZQLfxfViRm/yTGoRgDeTbojtq6rezeyKB0sTBcKH2fhddnteAHRcHiKgr/ACpbgjGOC6RQ==", + "devOptional": true, + "dependencies": { + "debug": "^4.3.1", + "uuid": "^8.0.0" + }, + "engines": { + "node": ">=12.18.2" + } + }, + "node_modules/firebase-tools/node_modules/universal-analytics/node_modules/debug": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", + "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", + "devOptional": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/firebase-tools/node_modules/universal-analytics/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "devOptional": true, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/firebase-tools/node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=", + "devOptional": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/firebase-tools/node_modules/update-notifier-cjs": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/update-notifier-cjs/-/update-notifier-cjs-5.1.6.tgz", + "integrity": "sha512-wgxdSBWv3x/YpMzsWz5G4p4ec7JWD0HCl8W6bmNB6E5Gwo+1ym5oN4hiXpLf0mPySVEJEIsYlkshnplkg2OP9A==", + "devOptional": true, + "dependencies": { + "boxen": "^5.0.0", + "chalk": "^4.1.0", + "configstore": "^5.0.1", + "has-yarn": "^2.1.0", + "import-lazy": "^2.1.0", + "is-ci": "^2.0.0", + "is-installed-globally": "^0.4.0", + "is-npm": "^5.0.0", + "is-yarn-global": "^0.3.0", + "isomorphic-fetch": "^3.0.0", + "pupa": "^2.1.1", + "registry-auth-token": "^5.0.1", + "registry-url": "^5.1.0", + "semver": "^7.3.7", + "semver-diff": "^3.1.1", + "xdg-basedir": "^4.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/firebase-tools/node_modules/update-notifier-cjs/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==", + "devOptional": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/firebase-tools/node_modules/update-notifier-cjs/node_modules/boxen": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/boxen/-/boxen-5.1.2.tgz", + "integrity": "sha512-9gYgQKXx+1nP8mP7CzFyaUARhg7D3n1dF/FnErWmu9l6JvGpNUN278h0aSb+QjoiKSWG+iZ3uHrcqk0qrY9RQQ==", + "devOptional": true, + "dependencies": { + "ansi-align": "^3.0.0", + "camelcase": "^6.2.0", + "chalk": "^4.1.0", + "cli-boxes": "^2.2.1", + "string-width": "^4.2.2", + "type-fest": "^0.20.2", + "widest-line": "^3.1.0", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/firebase-tools/node_modules/update-notifier-cjs/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "devOptional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/firebase-tools/node_modules/update-notifier-cjs/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "devOptional": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/firebase-tools/node_modules/update-notifier-cjs/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==", + "devOptional": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/firebase-tools/node_modules/update-notifier-cjs/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==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/update-notifier-cjs/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==", + "devOptional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/update-notifier-cjs/node_modules/registry-auth-token": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-5.0.1.tgz", + "integrity": "sha512-UfxVOj8seK1yaIOiieV4FIP01vfBDLsY0H9sQzi9EbbUdJiuuBjJgLa1DpImXMNPnVkBD4eVxTEXcrZA6kfpJA==", + "devOptional": true, + "dependencies": { + "@pnpm/npm-conf": "^1.0.4" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/firebase-tools/node_modules/update-notifier-cjs/node_modules/semver": { + "version": "7.3.8", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", + "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", + "devOptional": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/firebase-tools/node_modules/update-notifier-cjs/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==", + "devOptional": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/update-notifier-cjs/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==", + "devOptional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/firebase-tools/node_modules/uri-js": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", + "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", + "devOptional": true, + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/firebase-tools/node_modules/url-join": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/url-join/-/url-join-0.0.1.tgz", + "integrity": "sha1-HbSK1CLTQCRpqH99l73r/k+x48g=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=", + "devOptional": true, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/firebase-tools/node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "devOptional": true, + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/firebase-tools/node_modules/valid-url": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/valid-url/-/valid-url-1.0.9.tgz", + "integrity": "sha1-HBRHm0DxOXp1eC8RXkCGRHQzogA=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=", + "devOptional": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/firebase-tools/node_modules/verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", + "devOptional": true, + "engines": [ + "node >=0.6.0" + ], + "dependencies": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + } + }, + "node_modules/firebase-tools/node_modules/vm2": { + "version": "3.9.17", + "resolved": "https://registry.npmjs.org/vm2/-/vm2-3.9.17.tgz", + "devOptional": true, + "dependencies": { + "acorn": "^8.7.0", + "acorn-walk": "^8.2.0" + }, + "bin": { + "vm2": "bin/vm2" + }, + "engines": { + "node": ">=6.0" + } + }, + "node_modules/firebase-tools/node_modules/wcwidth": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", + "integrity": "sha1-8LDc+RW8X/FSivrbLA4XtTLaL+g=", + "devOptional": true, + "dependencies": { + "defaults": "^1.0.3" + } + }, + "node_modules/firebase-tools/node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha1-lmRU6HZUYuN2RNNib2dCzotwll0=", + "devOptional": true, + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/firebase-tools/node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "devOptional": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, + "node_modules/firebase-tools/node_modules/wide-align": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", + "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", + "optional": true, + "dependencies": { + "string-width": "^1.0.2 || 2 || 3 || 4" + } + }, + "node_modules/firebase-tools/node_modules/widest-line": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-3.1.0.tgz", + "integrity": "sha512-NsmoXalsWVDMGupxZ5R08ka9flZjjiLvHVAWYOKtiKM8ujtZWr9cRffak+uSE48+Ob8ObalXpwyeUiyDD6QFgg==", + "devOptional": true, + "dependencies": { + "string-width": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/winston": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/winston/-/winston-3.2.1.tgz", + "integrity": "sha512-zU6vgnS9dAWCEKg/QYigd6cgMVVNwyTzKs81XZtTFuRwJOcDdBg7AU0mXVyNbs7O5RH2zdv+BdNZUlx7mXPuOw==", + "devOptional": true, + "dependencies": { + "async": "^2.6.1", + "diagnostics": "^1.1.1", + "is-stream": "^1.1.0", + "logform": "^2.1.1", + "one-time": "0.0.4", + "readable-stream": "^3.1.1", + "stack-trace": "0.0.x", + "triple-beam": "^1.3.0", + "winston-transport": "^4.3.0" + }, + "engines": { + "node": ">= 6.4.0" + } + }, + "node_modules/firebase-tools/node_modules/winston-transport": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.4.0.tgz", + "integrity": "sha512-Lc7/p3GtqtqPBYYtS6KCN3c77/2QCev51DvcJKbkFPQNoj1sinkGwLGFDxkXY9J6p9+EPnYs+D90uwbnaiURTw==", + "devOptional": true, + "dependencies": { + "readable-stream": "^2.3.7", + "triple-beam": "^1.2.0" + }, + "engines": { + "node": ">= 6.4.0" + } + }, + "node_modules/firebase-tools/node_modules/winston-transport/node_modules/readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "devOptional": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/firebase-tools/node_modules/word-wrap": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", + "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", + "devOptional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/firebase-tools/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "devOptional": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/firebase-tools/node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "devOptional": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/firebase-tools/node_modules/wrap-ansi/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "devOptional": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/firebase-tools/node_modules/wrap-ansi/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/write-file-atomic": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz", + "integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==", + "devOptional": true, + "dependencies": { + "imurmurhash": "^0.1.4", + "is-typedarray": "^1.0.0", + "signal-exit": "^3.0.2", + "typedarray-to-buffer": "^3.1.5" + } + }, + "node_modules/firebase-tools/node_modules/ws": { + "version": "7.4.6", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.6.tgz", + "integrity": "sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A==", + "devOptional": true, + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/firebase-tools/node_modules/xdg-basedir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-4.0.0.tgz", + "integrity": "sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q==", + "devOptional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-tools/node_modules/xmlcreate": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/xmlcreate/-/xmlcreate-2.0.4.tgz", + "integrity": "sha512-nquOebG4sngPmGPICTS5EnxqhKbCmz5Ox5hsszI2T6U5qdrJizBc+0ilYSEjTSzU0yZcmvppztXe/5Al5fUwdg==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/xregexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/xregexp/-/xregexp-2.0.0.tgz", + "integrity": "sha1-UqY+VsoLhKfzpfPWGHLxJq16WUM=", + "devOptional": true, + "engines": { + "node": "*" + } + }, + "node_modules/firebase-tools/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "devOptional": true + }, + "node_modules/firebase-tools/node_modules/yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "devOptional": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/firebase-tools/node_modules/yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "devOptional": true, + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/firebase-tools/node_modules/yargs-parser": { + "version": "20.2.4", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz", + "integrity": "sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==", + "devOptional": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/firebase-tools/node_modules/yargs/node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "devOptional": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/firebase-tools/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==", + "devOptional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/firebase-tools/node_modules/zip-stream": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-4.1.0.tgz", + "integrity": "sha512-zshzwQW7gG7hjpBlgeQP9RuyPGNxvJdzR8SUM3QhxCnLjWN2E7j3dOvpeDcQoETfHx0urRS7EtmVToql7YpU4A==", + "devOptional": true, + "dependencies": { + "archiver-utils": "^2.1.0", + "compress-commons": "^4.1.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">= 10" + } + }, + "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.3.1", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", + "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", + "dev": true + }, + "node_modules/fn.name": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz", + "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==" + }, + "node_modules/follow-redirects": { + "version": "1.15.6", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", + "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/foreground-child": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.2.1.tgz", + "integrity": "sha512-PXUUyLqrR2XCWICfv6ukppP96sdFwWbNEnfEMt7jNsISjMsvaLNinAHNDYyvkyU+SZG2BTSbT5NjG+vZslfGTA==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.0", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/foreground-child/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fraction.js": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", + "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", + "dev": true, + "engines": { + "node": "*" + }, + "funding": { + "type": "patreon", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/fs-minipass": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-3.0.3.tgz", + "integrity": "sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw==", + "dev": true, + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/fs-minipass/node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/fs-monkey": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.0.6.tgz", + "integrity": "sha512-b1FMfwetIKymC0eioW7mTywihSQE4oLzQn1dB6rZB5fx/3NpNEdAWeCSMB+60/AeT0TCXsxzAlcYVEFCTAksWg==", + "dev": true + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/fuzzy": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/fuzzy/-/fuzzy-0.1.3.tgz", + "integrity": "sha512-/gZffu4ykarLrCiP3Ygsa86UAo1E5vEVlvTrpkKywXSbP9Xhln3oSp9QSV57gEq3JFFpGJ4GZ+5zdEp3FcUh4w==", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/gauge": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-4.0.4.tgz", + "integrity": "sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg==", + "deprecated": "This package is no longer supported.", + "dev": true, + "dependencies": { + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.3", + "console-control-strings": "^1.1.0", + "has-unicode": "^2.0.1", + "signal-exit": "^3.0.7", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.5" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", + "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", + "dev": true, + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "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==", + "devOptional": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==" + }, + "node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/globby": { + "version": "13.2.2", + "resolved": "https://registry.npmjs.org/globby/-/globby-13.2.2.tgz", + "integrity": "sha512-Y1zNGV+pzQdh7H39l9zgB4PJqjRNqydvdYCDG4HFXM4XuvSaQQlEc91IU1yALL8gUTDomgBAfz3XJdmUS+oo0w==", + "dev": true, + "dependencies": { + "dir-glob": "^3.0.1", + "fast-glob": "^3.3.0", + "ignore": "^5.2.4", + "merge2": "^1.4.1", + "slash": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/good-listener": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/good-listener/-/good-listener-1.2.2.tgz", + "integrity": "sha512-goW1b+d9q/HIwbVYZzZ6SsTr4IgE+WA44A0GmPIQstuOrgsFcT7VEJ48nmr9GaRtNu0XTKacFLGnBPAM6Afouw==", + "dependencies": { + "delegate": "^3.1.2" + } + }, + "node_modules/gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.1.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" + }, + "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/gts": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/gts/-/gts-5.3.1.tgz", + "integrity": "sha512-P9F+krJkGOkisUX+P9pfUas1Xy+U+CxBFZT62uInkJbgvZpnW1ug/pIcMJJmLOthMq1J88lpQUGhXDC9UTvVcw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@typescript-eslint/eslint-plugin": "5.62.0", + "@typescript-eslint/parser": "5.62.0", + "chalk": "^4.1.2", + "eslint": "8.57.0", + "eslint-config-prettier": "9.1.0", + "eslint-plugin-n": "15.7.0", + "eslint-plugin-prettier": "5.1.3", + "execa": "^5.0.0", + "inquirer": "^7.3.3", + "json5": "^2.1.3", + "meow": "^9.0.0", + "ncp": "^2.0.0", + "prettier": "3.2.5", + "rimraf": "3.0.2", + "write-file-atomic": "^4.0.0" + }, + "bin": { + "gts": "build/src/cli.js" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "typescript": ">=3" + } + }, + "node_modules/gts/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/gts/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/gts/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/gts/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/gts/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/gts/node_modules/inquirer": { + "version": "7.3.3", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-7.3.3.tgz", + "integrity": "sha512-JG3eIAj5V9CwcGvuOmoo6LB9kbAYT8HXffUl6memuszlwDC/qvFAJw49XJ5NROSFNPxp3iQg1GqkFhaY/CR0IA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-escapes": "^4.2.1", + "chalk": "^4.1.0", + "cli-cursor": "^3.1.0", + "cli-width": "^3.0.0", + "external-editor": "^3.0.3", + "figures": "^3.0.0", + "lodash": "^4.17.19", + "mute-stream": "0.0.8", + "run-async": "^2.4.0", + "rxjs": "^6.6.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0", + "through": "^2.3.6" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/gts/node_modules/rxjs": { + "version": "6.6.7", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", + "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^1.9.0" + }, + "engines": { + "npm": ">=2.0.0" + } + }, + "node_modules/gts/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/gts/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true, + "license": "0BSD" + }, + "node_modules/handle-thing": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz", + "integrity": "sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg==", + "dev": true + }, + "node_modules/hard-rejection": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/hard-rejection/-/hard-rejection-2.1.0.tgz", + "integrity": "sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", + "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==", + "dev": true + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hdr-histogram-js": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/hdr-histogram-js/-/hdr-histogram-js-2.0.3.tgz", + "integrity": "sha512-Hkn78wwzWHNCp2uarhzQ2SGFLU3JY8SBDDd3TAABK4fc30wm+MuPOrg5QVFVfkKOQd6Bfz3ukJEI+q9sXEkK1g==", + "dev": true, + "dependencies": { + "@assemblyscript/loader": "^0.10.1", + "base64-js": "^1.2.0", + "pako": "^1.0.3" + } + }, + "node_modules/hdr-histogram-percentiles-obj": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hdr-histogram-percentiles-obj/-/hdr-histogram-percentiles-obj-3.0.0.tgz", + "integrity": "sha512-7kIufnBqdsBGcSZLPJwqHT3yhk1QTsSlFsVD3kx5ixH/AlgBs9yM1q6DPhXZ8f8gtdqgh7N7/5btRLpQsS2gHw==", + "dev": true + }, + "node_modules/hosted-git-info": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-6.1.1.tgz", + "integrity": "sha512-r0EI+HBMcXadMrugk0GCQ+6BQV39PiWAZVfq7oIckeGiN7sjRGyQxPdft3nQekFTCQbYxLBH+/axZMeH8UX6+w==", + "dev": true, + "dependencies": { + "lru-cache": "^7.5.1" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/hosted-git-info/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/hpack.js": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz", + "integrity": "sha512-zJxVehUdMGIKsRaNt7apO2Gqp0BdqW5yaiGHXXmbpvxgBYVZnAql+BJb4RO5ad2MgpbZKn5G6nMnegrH1FcNYQ==", + "dev": true, + "dependencies": { + "inherits": "^2.0.1", + "obuf": "^1.0.0", + "readable-stream": "^2.0.1", + "wbuf": "^1.1.0" + } + }, + "node_modules/hpack.js/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/hpack.js/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, + "node_modules/hpack.js/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/html-entities": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.5.2.tgz", + "integrity": "sha512-K//PSRMQk4FZ78Kyau+mZurHn3FH0Vwr+H36eE0rPbeYkRRi9YxceYPhuN60UwWorxyKHhqoAJl2OFKa4BVtaA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/mdevils" + }, + { + "type": "patreon", + "url": "https://patreon.com/mdevils" + } + ] + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true + }, + "node_modules/http-cache-semantics": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", + "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==", + "dev": true + }, + "node_modules/http-deceiver": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz", + "integrity": "sha512-LmpOGxTfbpgtGVxJrj5k7asXHCgNZp5nLfp+hWc8QQRqtb7fUy6kRY3BO1h9ddF6yIPYUARgxGOwB42DnxIaNw==", + "dev": true + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "dev": true, + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/http-errors/node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/http-parser-js": { + "version": "0.5.8", + "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.8.tgz", + "integrity": "sha512-SGeBX54F94Wgu5RH3X5jsDtf4eHyRogWX1XGT3b4HuW3tQPM4AaBzoUji/4AAJNXCEOWZ5O0DgZmJw1947gD5Q==" + }, + "node_modules/http-proxy": { + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", + "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==", + "dev": true, + "dependencies": { + "eventemitter3": "^4.0.0", + "follow-redirects": "^1.0.0", + "requires-port": "^1.0.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/http-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", + "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", + "dev": true, + "dependencies": { + "@tootallnate/once": "2", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/http-proxy-middleware": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.6.tgz", + "integrity": "sha512-ya/UeJ6HVBYxrgYotAZo1KvPWlgB48kUJLDePFeneHsVujFaW5WNj2NgWCAE//B1Dl02BIfYlpNgBy8Kf8Rjmw==", + "dev": true, + "dependencies": { + "@types/http-proxy": "^1.17.8", + "http-proxy": "^1.18.1", + "is-glob": "^4.0.1", + "is-plain-obj": "^3.0.0", + "micromatch": "^4.0.2" + }, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "@types/express": "^4.17.13" + }, + "peerDependenciesMeta": { + "@types/express": { + "optional": true + } + } + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "dev": true, + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/humanize-ms": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", + "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", + "dev": true, + "dependencies": { + "ms": "^2.0.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/icss-utils": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz", + "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==", + "dev": true, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/idb": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/idb/-/idb-7.1.1.tgz", + "integrity": "sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ==" + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/ignore": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", + "integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/ignore-walk": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-6.0.5.tgz", + "integrity": "sha512-VuuG0wCnjhnylG1ABXT3dAuIpTNDs/G8jlpmwXY03fXoXy/8ZK8/T+hMzt8L4WnrLCJgdybqgPagnF/f97cg3A==", + "dev": true, + "dependencies": { + "minimatch": "^9.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/ignore-walk/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/image-size": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/image-size/-/image-size-0.5.5.tgz", + "integrity": "sha512-6TDAlDPZxUFCv+fuOkIoXT/V/f3Qbq8e37p+YOiYrUv3v9cc3/6x78VdfPgFVaB9dZYeLUfKgHRebpkm/oP2VQ==", + "dev": true, + "optional": true, + "bin": { + "image-size": "bin/image-size.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/immutable": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.6.tgz", + "integrity": "sha512-Ju0+lEMyzMVZarkTn/gqRpdqd5dOPaz1mCZ0SH3JV6iFw81PldE/PEB1hWVEA288HPt4WXW8O7AWxB10M+03QQ==", + "dev": true + }, + "node_modules/import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/import-fresh/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, + "engines": { + "node": ">=4" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/infer-owner": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/infer-owner/-/infer-owner-1.0.4.tgz", + "integrity": "sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==", + "dev": true + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/ini": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ini/-/ini-3.0.1.tgz", + "integrity": "sha512-it4HyVAUTKBc6m8e1iXWvXSTdndF7HbdN713+kvLrymxTaU4AUBWrJ4vEooP+V7fexnVD3LKcBshjGGPefSMUQ==", + "dev": true, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/inquirer": { + "version": "8.2.4", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-8.2.4.tgz", + "integrity": "sha512-nn4F01dxU8VeKfq192IjLsxu0/OmMZ4Lg3xKAns148rCaXP6ntAoEkVYZThWjwON8AlzdZZi6oqnhNbxUG9hVg==", + "dependencies": { + "ansi-escapes": "^4.2.1", + "chalk": "^4.1.1", + "cli-cursor": "^3.1.0", + "cli-width": "^3.0.0", + "external-editor": "^3.0.3", + "figures": "^3.0.0", + "lodash": "^4.17.21", + "mute-stream": "0.0.8", + "ora": "^5.4.1", + "run-async": "^2.4.0", + "rxjs": "^7.5.5", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0", + "through": "^2.3.6", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/inquirer-autocomplete-prompt": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/inquirer-autocomplete-prompt/-/inquirer-autocomplete-prompt-1.4.0.tgz", + "integrity": "sha512-qHgHyJmbULt4hI+kCmwX92MnSxDs/Yhdt4wPA30qnoa01OF6uTXV8yvH4hKXgdaTNmkZ9D01MHjqKYEuJN+ONw==", + "dependencies": { + "ansi-escapes": "^4.3.1", + "chalk": "^4.0.0", + "figures": "^3.2.0", + "run-async": "^2.4.0", + "rxjs": "^6.6.2" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "inquirer": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/inquirer-autocomplete-prompt/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/inquirer-autocomplete-prompt/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/inquirer-autocomplete-prompt/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/inquirer-autocomplete-prompt/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/inquirer-autocomplete-prompt/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/inquirer-autocomplete-prompt/node_modules/rxjs": { + "version": "6.6.7", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", + "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", + "dependencies": { + "tslib": "^1.9.0" + }, + "engines": { + "npm": ">=2.0.0" + } + }, + "node_modules/inquirer-autocomplete-prompt/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/inquirer-autocomplete-prompt/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + }, + "node_modules/inquirer/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/inquirer/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/inquirer/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/inquirer/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/inquirer/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/inquirer/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "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==", + "optional": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/ionic-emoji-rating": { + "version": "1.0.15", + "resolved": "https://registry.npmjs.org/ionic-emoji-rating/-/ionic-emoji-rating-1.0.15.tgz", + "integrity": "sha512-ooNInojKERSgfoMZOAWvLm0MAARmAh1ibGr32x9S98J1jv2RC9DdaXOBWf/Xx9ZfJmvUAgZKjufcriszGcuZFA==" + }, + "node_modules/ionicons": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/ionicons/-/ionicons-7.4.0.tgz", + "integrity": "sha512-ZK94MMqgzMCPPMhmk8Ouu6goyVHFIlw/ACP6oe3FrikcI0N7CX0xcwVaEbUc0G/v3W0shI93vo+9ve/KpvcNhQ==", + "dependencies": { + "@stencil/core": "^4.0.3" + } + }, + "node_modules/ip-address": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz", + "integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==", + "dev": true, + "dependencies": { + "jsbn": "1.1.0", + "sprintf-js": "^1.1.3" + }, + "engines": { + "node": ">= 12" + } + }, + "node_modules/ip-address/node_modules/sprintf-js": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", + "dev": true + }, + "node_modules/ipaddr.js": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.2.0.tgz", + "integrity": "sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA==", + "dev": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true + }, + "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==", + "devOptional": true, + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.14.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.14.0.tgz", + "integrity": "sha512-a5dFJih5ZLYlRtDc0dZWP7RiKr6xIKzmn/oAYCDvdLThadVgyJwlaoQPmRtMSpz+rk0OGAgIu+TcM9HUF0fk1A==", + "dev": true, + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "devOptional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "engines": { + "node": ">=8" + } + }, + "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==", + "devOptional": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-interactive": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", + "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-lambda": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-lambda/-/is-lambda-1.0.1.tgz", + "integrity": "sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==", + "dev": true + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "devOptional": true, + "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": "3.0.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-3.0.0.tgz", + "integrity": "sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dev": true, + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-what": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/is-what/-/is-what-3.14.1.tgz", + "integrity": "sha512-sNxgpk9793nzSs7bA6JQJGeIuRBQhAaNGG77kzYQgMkrID+lS6SlK07K5LaptscDlSaIgH+GPFzf+d75FVxozA==", + "dev": true + }, + "node_modules/is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true + }, + "node_modules/isbinaryfile": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-4.0.10.tgz", + "integrity": "sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw==", + "dev": true, + "engines": { + "node": ">= 8.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/gjtorikian/" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "node_modules/isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", + "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", + "dev": true, + "dependencies": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report/node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/istanbul-lib-report/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "dev": true, + "dependencies": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/istanbul-reports": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", + "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", + "dev": true, + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jackspeak": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.0.tgz", + "integrity": "sha512-JVYhQnN59LVPFCEcVa2C3CrEKYacvjRfqIQl+h8oi91aLYQVWRYbxjPcv1bUiUy/kLmQaANrYfNMCO3kuEDHfw==", + "dev": true, + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jasmine-core": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-4.5.0.tgz", + "integrity": "sha512-9PMzyvhtocxb3aXJVOPqBDswdgyAeSB81QnLop4npOpbqnheaTEwPc9ZloQeVswugPManznQBjD8kWDTjlnHuw==", + "dev": true + }, + "node_modules/jest-worker": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", + "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/jest-worker/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "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==", + "dev": true + }, + "node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsbn": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz", + "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==", + "dev": true + }, + "node_modules/jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "dev": true, + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==" + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" + }, + "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==", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonc-parser": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.0.tgz", + "integrity": "sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==" + }, + "node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/jsonparse": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz", + "integrity": "sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==", + "dev": true, + "engines": [ + "node >= 0.2.0" + ] + }, + "node_modules/karma": { + "version": "6.4.3", + "resolved": "https://registry.npmjs.org/karma/-/karma-6.4.3.tgz", + "integrity": "sha512-LuucC/RE92tJ8mlCwqEoRWXP38UMAqpnq98vktmS9SznSoUPPUJQbc91dHcxcunROvfQjdORVA/YFviH+Xci9Q==", + "dev": true, + "dependencies": { + "@colors/colors": "1.5.0", + "body-parser": "^1.19.0", + "braces": "^3.0.2", + "chokidar": "^3.5.1", + "connect": "^3.7.0", + "di": "^0.0.1", + "dom-serialize": "^2.2.1", + "glob": "^7.1.7", + "graceful-fs": "^4.2.6", + "http-proxy": "^1.18.1", + "isbinaryfile": "^4.0.8", + "lodash": "^4.17.21", + "log4js": "^6.4.1", + "mime": "^2.5.2", + "minimatch": "^3.0.4", + "mkdirp": "^0.5.5", + "qjobs": "^1.2.0", + "range-parser": "^1.2.1", + "rimraf": "^3.0.2", + "socket.io": "^4.7.2", + "source-map": "^0.6.1", + "tmp": "^0.2.1", + "ua-parser-js": "^0.7.30", + "yargs": "^16.1.1" + }, + "bin": { + "karma": "bin/karma" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/karma-chrome-launcher": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/karma-chrome-launcher/-/karma-chrome-launcher-3.1.1.tgz", + "integrity": "sha512-hsIglcq1vtboGPAN+DGCISCFOxW+ZVnIqhDQcCMqqCp+4dmJ0Qpq5QAjkbA0X2L9Mi6OBkHi2Srrbmm7pUKkzQ==", + "dev": true, + "dependencies": { + "which": "^1.2.1" + } + }, + "node_modules/karma-coverage": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/karma-coverage/-/karma-coverage-2.2.1.tgz", + "integrity": "sha512-yj7hbequkQP2qOSb20GuNSIyE//PgJWHwC2IydLE6XRtsnaflv+/OSGNssPjobYUlhVVagy99TQpqUt3vAUG7A==", + "dev": true, + "dependencies": { + "istanbul-lib-coverage": "^3.2.0", + "istanbul-lib-instrument": "^5.1.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.1", + "istanbul-reports": "^3.0.5", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/karma-coverage/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/karma-coverage/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/karma-jasmine": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/karma-jasmine/-/karma-jasmine-5.1.0.tgz", + "integrity": "sha512-i/zQLFrfEpRyQoJF9fsCdTMOF5c2dK7C7OmsuKg2D0YSsuZSfQDiLuaiktbuio6F2wiCsZSnSnieIQ0ant/uzQ==", + "dev": true, + "dependencies": { + "jasmine-core": "^4.1.0" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "karma": "^6.0.0" + } + }, + "node_modules/karma-jasmine-html-reporter": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/karma-jasmine-html-reporter/-/karma-jasmine-html-reporter-2.0.0.tgz", + "integrity": "sha512-SB8HNNiazAHXM1vGEzf8/tSyEhkfxuDdhYdPBX2Mwgzt0OuF2gicApQ+uvXLID/gXyJQgvrM9+1/2SxZFUUDIA==", + "dev": true, + "peerDependencies": { + "jasmine-core": "^4.0.0", + "karma": "^6.0.0", + "karma-jasmine": "^5.0.0" + } + }, + "node_modules/karma-source-map-support": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/karma-source-map-support/-/karma-source-map-support-1.4.0.tgz", + "integrity": "sha512-RsBECncGO17KAoJCYXjv+ckIz+Ii9NCi+9enk+rq6XC81ezYkb4/RHE6CTXdA7IOJqoF3wcaLfVG0CPmE5ca6A==", + "dev": true, + "dependencies": { + "source-map-support": "^0.5.5" + } + }, + "node_modules/karma/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/karma/node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/karma/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/karma/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/karma/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/karma/node_modules/tmp": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.3.tgz", + "integrity": "sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w==", + "dev": true, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/karma/node_modules/yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "dev": true, + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/karma/node_modules/yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/katex": { + "version": "0.16.11", + "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.11.tgz", + "integrity": "sha512-RQrI8rlHY92OLf3rho/Ts8i/XvjgguEjOkO1BEXcU3N8BqPpSzBNwV/G0Ukr+P/l3ivvJUE/Fa/CwbS6HesGNQ==", + "funding": [ + "https://opencollective.com/katex", + "https://github.com/sponsors/katex" + ], + "optional": true, + "dependencies": { + "commander": "^8.3.0" + }, + "bin": { + "katex": "cli.js" + } + }, + "node_modules/katex/node_modules/commander": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", + "optional": true, + "engines": { + "node": ">= 12" + } + }, + "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/khroma": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/khroma/-/khroma-2.1.0.tgz", + "integrity": "sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw==", + "optional": true + }, + "node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/klona": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/klona/-/klona-2.0.6.tgz", + "integrity": "sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/kuler": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz", + "integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==" + }, + "node_modules/layout-base": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/layout-base/-/layout-base-1.0.2.tgz", + "integrity": "sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg==", + "optional": true + }, + "node_modules/less": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/less/-/less-4.1.3.tgz", + "integrity": "sha512-w16Xk/Ta9Hhyei0Gpz9m7VS8F28nieJaL/VyShID7cYvP6IL5oHeL6p4TXSDJqZE/lNv0oJ2pGVjJsRkfwm5FA==", + "dev": true, + "dependencies": { + "copy-anything": "^2.0.1", + "parse-node-version": "^1.0.1", + "tslib": "^2.3.0" + }, + "bin": { + "lessc": "bin/lessc" + }, + "engines": { + "node": ">=6" + }, + "optionalDependencies": { + "errno": "^0.1.1", + "graceful-fs": "^4.1.2", + "image-size": "~0.5.0", + "make-dir": "^2.1.0", + "mime": "^1.4.1", + "needle": "^3.1.0", + "source-map": "~0.6.0" + } + }, + "node_modules/less-loader": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/less-loader/-/less-loader-11.1.0.tgz", + "integrity": "sha512-C+uDBV7kS7W5fJlUjq5mPBeBVhYpTIm5gB09APT9o3n/ILeaXVsiSFTbZpTJCJwQ/Crczfn3DmfQFwxYusWFug==", + "dev": true, + "dependencies": { + "klona": "^2.0.4" + }, + "engines": { + "node": ">= 14.15.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "less": "^3.5.0 || ^4.0.0", + "webpack": "^5.0.0" + } + }, + "node_modules/less/node_modules/make-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", + "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", + "dev": true, + "optional": true, + "dependencies": { + "pify": "^4.0.1", + "semver": "^5.6.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/less/node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true, + "optional": true, + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/less/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "optional": true, + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/less/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "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/license-webpack-plugin": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/license-webpack-plugin/-/license-webpack-plugin-4.0.2.tgz", + "integrity": "sha512-771TFWFD70G1wLTC4oU2Cw4qvtmNrIw+wRvBtn+okgHl7slJVi7zfNcdmqDL72BojM30VNJ2UHylr1o77U37Jw==", + "dev": true, + "dependencies": { + "webpack-sources": "^3.0.0" + }, + "peerDependenciesMeta": { + "webpack": { + "optional": true + }, + "webpack-sources": { + "optional": true + } + } + }, + "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==", + "dev": true + }, + "node_modules/loader-runner": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", + "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==", + "engines": { + "node": ">=6.11.5" + } + }, + "node_modules/loader-utils": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-3.2.1.tgz", + "integrity": "sha512-ZvFw1KWS3GVyYBYb7qkmRM/WwL2TQQBxgCK62rlvm4WpVQ23Nb4tYjApUlfjrEGvOs7KHEsmyUn75OHZrJMWPw==", + "dev": true, + "engines": { + "node": ">= 12.13.0" + } + }, + "node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, + "node_modules/lodash-es": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", + "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==", + "optional": true + }, + "node_modules/lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==" + }, + "node_modules/lodash.debounce": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", + "dev": true + }, + "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/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-symbols/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/log-symbols/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/log-symbols/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/log-symbols/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/log-symbols/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/log-symbols/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/log4js": { + "version": "6.9.1", + "resolved": "https://registry.npmjs.org/log4js/-/log4js-6.9.1.tgz", + "integrity": "sha512-1somDdy9sChrr9/f4UlzhdaGfDR2c/SaD2a4T7qEkG4jTS57/B3qmnjLYePwQ8cqWnUHZI0iAKxMBpCZICiZ2g==", + "dev": true, + "dependencies": { + "date-format": "^4.0.14", + "debug": "^4.3.4", + "flatted": "^3.2.7", + "rfdc": "^1.3.0", + "streamroller": "^3.1.5" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/logform": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/logform/-/logform-2.6.0.tgz", + "integrity": "sha512-1ulHeNPp6k/LD8H91o7VYFBng5i1BDE7HoKxVbZiGFidS1Rj65qcywLxX+pVfAPoQJEjRdvKcusKwOupHCVOVQ==", + "dependencies": { + "@colors/colors": "1.6.0", + "@types/triple-beam": "^1.3.2", + "fecha": "^4.2.0", + "ms": "^2.1.1", + "safe-stable-stringify": "^2.3.1", + "triple-beam": "^1.3.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/logform/node_modules/@colors/colors": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.6.0.tgz", + "integrity": "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==", + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/long": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", + "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==" + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/magic-string": { + "version": "0.29.0", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.29.0.tgz", + "integrity": "sha512-WcfidHrDjMY+eLjlU+8OvwREqHwpgCeKVBUpQ3OhYYuvfaYCUgcbuBzappNzZvg/v8onU3oQj+BYpkOJe9Iw4Q==", + "dev": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.13" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dev": true, + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/make-fetch-happen": { + "version": "10.2.1", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-10.2.1.tgz", + "integrity": "sha512-NgOPbRiaQM10DYXvN3/hhGVI2M5MtITFryzBGxHM5p4wnFxsVCbxkrBrDsk+EZ5OB4jEOT7AjDxtdF+KVEFT7w==", + "dev": true, + "dependencies": { + "agentkeepalive": "^4.2.1", + "cacache": "^16.1.0", + "http-cache-semantics": "^4.1.0", + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.0", + "is-lambda": "^1.0.1", + "lru-cache": "^7.7.1", + "minipass": "^3.1.6", + "minipass-collect": "^1.0.2", + "minipass-fetch": "^2.0.3", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^0.6.3", + "promise-retry": "^2.0.1", + "socks-proxy-agent": "^7.0.0", + "ssri": "^9.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/make-fetch-happen/node_modules/@npmcli/fs": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-2.1.2.tgz", + "integrity": "sha512-yOJKRvohFOaLqipNtwYB9WugyZKhC/DZC4VYPmpaCzDBrA8YpK3qHZ8/HGscMnE4GqbkLNuVcCnxkeQEdGt6LQ==", + "dev": true, + "dependencies": { + "@gar/promisify": "^1.1.3", + "semver": "^7.3.5" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/make-fetch-happen/node_modules/cacache": { + "version": "16.1.3", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-16.1.3.tgz", + "integrity": "sha512-/+Emcj9DAXxX4cwlLmRI9c166RuL3w30zp4R7Joiv2cQTtTtA+jeuCAjH3ZlGnYS3tKENSrKhAzVVP9GVyzeYQ==", + "dev": true, + "dependencies": { + "@npmcli/fs": "^2.1.0", + "@npmcli/move-file": "^2.0.0", + "chownr": "^2.0.0", + "fs-minipass": "^2.1.0", + "glob": "^8.0.1", + "infer-owner": "^1.0.4", + "lru-cache": "^7.7.1", + "minipass": "^3.1.6", + "minipass-collect": "^1.0.2", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "mkdirp": "^1.0.4", + "p-map": "^4.0.0", + "promise-inflight": "^1.0.1", + "rimraf": "^3.0.2", + "ssri": "^9.0.0", + "tar": "^6.1.11", + "unique-filename": "^2.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/make-fetch-happen/node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "dev": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/make-fetch-happen/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/make-fetch-happen/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/make-fetch-happen/node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true, + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/make-fetch-happen/node_modules/ssri": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-9.0.1.tgz", + "integrity": "sha512-o57Wcn66jMQvfHG1FlYbWeZWW/dHZhJXjpIcTfXldXEk5nz5lStPo3mK0OJQfGR3RbZUlbISexbljkJzuEj/8Q==", + "dev": true, + "dependencies": { + "minipass": "^3.1.1" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/make-fetch-happen/node_modules/unique-filename": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-2.0.1.tgz", + "integrity": "sha512-ODWHtkkdx3IAR+veKxFV+VBkUMcN+FaqzUUd7IZzt+0zhDZFPFxhlqwPF3YQvMHx1TD0tdgYl+kuPnJ8E6ql7A==", + "dev": true, + "dependencies": { + "unique-slug": "^3.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/make-fetch-happen/node_modules/unique-slug": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-3.0.0.tgz", + "integrity": "sha512-8EyMynh679x/0gqE9fT9oilG+qEt+ibFyqjuVTsZn1+CMxH+XLlpvr2UZx4nVcCwTpx81nICr2JQFkM+HPLq4w==", + "dev": true, + "dependencies": { + "imurmurhash": "^0.1.4" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/make-fetch-happen/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/map-obj": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-4.3.0.tgz", + "integrity": "sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/marked": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/marked/-/marked-4.3.0.tgz", + "integrity": "sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A==", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 12" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/memfs": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/memfs/-/memfs-3.5.3.tgz", + "integrity": "sha512-UERzLsxzllchadvbPs5aolHh65ISpKpM+ccLbOJ8/vvpBKmAWf+la7dXFy7Mr0ySHbdHrFv5kGFCUHHe6GFEmw==", + "dev": true, + "dependencies": { + "fs-monkey": "^1.0.4" + }, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/meow": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/meow/-/meow-9.0.0.tgz", + "integrity": "sha512-+obSblOQmRhcyBt62furQqRAQpNyWXo8BuQ5bN7dG8wmwQ+vwHKp/rCFD4CrTP8CsDQD1sjoZ94K417XEUk8IQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/minimist": "^1.2.0", + "camelcase-keys": "^6.2.2", + "decamelize": "^1.2.0", + "decamelize-keys": "^1.1.0", + "hard-rejection": "^2.1.0", + "minimist-options": "4.1.0", + "normalize-package-data": "^3.0.0", + "read-pkg-up": "^7.0.1", + "redent": "^3.0.0", + "trim-newlines": "^3.0.0", + "type-fest": "^0.18.0", + "yargs-parser": "^20.2.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/meow/node_modules/hosted-git-info": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz", + "integrity": "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==", + "dev": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/meow/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/meow/node_modules/normalize-package-data": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-3.0.3.tgz", + "integrity": "sha512-p2W1sgqij3zMMyRC067Dg16bfzVH+w7hyegmpIvZ4JNjqtGOVAIvLmjBx3yP7YTe9vKJgkoNOPjwQGogDoMXFA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "hosted-git-info": "^4.0.1", + "is-core-module": "^2.5.0", + "semver": "^7.3.4", + "validate-npm-package-license": "^3.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/meow/node_modules/type-fest": { + "version": "0.18.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.18.1.tgz", + "integrity": "sha512-OIAYXk8+ISY+qTOwkHtKqzAuxchoMiD9Udx+FSGQDuiRR+PJKJHc2NJAXlbhkGwTt/4/nKZxELY1w3ReWOL8mw==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/meow/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/meow/node_modules/yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==", + "dev": true + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==" + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/mermaid": { + "version": "9.4.3", + "resolved": "https://registry.npmjs.org/mermaid/-/mermaid-9.4.3.tgz", + "integrity": "sha512-TLkQEtqhRSuEHSE34lh5bCa94KATCyluAXmFnNI2PRZwOpXFeqiJWwZl+d2CcemE1RS6QbbueSSq9QIg8Uxcyw==", + "optional": true, + "dependencies": { + "@braintree/sanitize-url": "^6.0.0", + "cytoscape": "^3.23.0", + "cytoscape-cose-bilkent": "^4.1.0", + "cytoscape-fcose": "^2.1.0", + "d3": "^7.4.0", + "dagre-d3-es": "7.0.9", + "dayjs": "^1.11.7", + "dompurify": "2.4.3", + "elkjs": "^0.8.2", + "khroma": "^2.0.0", + "lodash-es": "^4.17.21", + "non-layered-tidy-tree-layout": "^2.0.2", + "stylis": "^4.1.2", + "ts-dedent": "^2.2.0", + "uuid": "^9.0.0", + "web-worker": "^1.2.0" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/micromatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.7.tgz", + "integrity": "sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==", + "dev": true, + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "dev": true, + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "engines": { + "node": ">=6" + } + }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/mini-css-extract-plugin": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.7.2.tgz", + "integrity": "sha512-EdlUizq13o0Pd+uCp+WO/JpkLvHRVGt97RqfeGhXqAcorYo1ypJSpkV+WDT0vY/kmh/p7wRdJNJtuyK540PXDw==", + "dev": true, + "dependencies": { + "schema-utils": "^4.0.0" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" + } + }, + "node_modules/minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", + "dev": true + }, + "node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minimist-options": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/minimist-options/-/minimist-options-4.1.0.tgz", + "integrity": "sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "arrify": "^1.0.1", + "is-plain-obj": "^1.1.0", + "kind-of": "^6.0.3" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/minimist-options/node_modules/is-plain-obj": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", + "integrity": "sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/minipass": { + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-4.2.8.tgz", + "integrity": "sha512-fNzuVyifolSLFL4NzpF+wEF4qrgqaaKX0haXPQEdQ7NKAN+WecoKMHV09YcuL/DHxrUsYQOK3MiuDf7Ip2OXfQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-collect": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-1.0.2.tgz", + "integrity": "sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==", + "dev": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minipass-collect/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-collect/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/minipass-fetch": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-2.1.2.tgz", + "integrity": "sha512-LT49Zi2/WMROHYoqGgdlQIZh8mLPZmOrN2NdJjMXxYe4nkN6FUyuPuOAOedNJDrx0IRGg9+4guZewtp8hE6TxA==", + "dev": true, + "dependencies": { + "minipass": "^3.1.6", + "minipass-sized": "^1.0.3", + "minizlib": "^2.1.2" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + }, + "optionalDependencies": { + "encoding": "^0.1.13" + } + }, + "node_modules/minipass-fetch/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-fetch/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/minipass-flush": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz", + "integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==", + "dev": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minipass-flush/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-flush/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/minipass-json-stream": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minipass-json-stream/-/minipass-json-stream-1.0.1.tgz", + "integrity": "sha512-ODqY18UZt/I8k+b7rl2AENgbWE8IDYam+undIJONvigAz8KR5GWblsFTEfQs0WODsjbSXWlm+JHEv8Gr6Tfdbg==", + "dev": true, + "dependencies": { + "jsonparse": "^1.3.1", + "minipass": "^3.0.0" + } + }, + "node_modules/minipass-json-stream/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-json-stream/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/minipass-pipeline": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", + "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", + "dev": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-pipeline/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-pipeline/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/minipass-sized": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz", + "integrity": "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==", + "dev": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-sized/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-sized/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "dev": true, + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minizlib/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minizlib/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dev": true, + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/moment": { + "version": "2.30.1", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", + "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/multicast-dns": { + "version": "7.2.5", + "resolved": "https://registry.npmjs.org/multicast-dns/-/multicast-dns-7.2.5.tgz", + "integrity": "sha512-2eznPJP8z2BFLX50tf0LuODrpINqP1RVIm/CObbTcBRITQgmC/TjcREF1NeTBzIcR5XO/ukWo+YHOjBbFwIupg==", + "dev": true, + "dependencies": { + "dns-packet": "^5.2.2", + "thunky": "^1.0.2" + }, + "bin": { + "multicast-dns": "cli.js" + } + }, + "node_modules/mute-stream": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", + "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==" + }, + "node_modules/nanoid": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/natural-compare-lite": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz", + "integrity": "sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==", + "dev": true, + "license": "MIT" + }, + "node_modules/ncp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ncp/-/ncp-2.0.0.tgz", + "integrity": "sha512-zIdGUrPRFTUELUvr3Gmc7KZ2Sw/h1PiVM0Af/oHB6zgnV1ikqSfRk+TOufi79aHYCW3NiOXmr1BP5nWbzojLaA==", + "dev": true, + "license": "MIT", + "bin": { + "ncp": "bin/ncp" + } + }, + "node_modules/needle": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/needle/-/needle-3.3.1.tgz", + "integrity": "sha512-6k0YULvhpw+RoLNiQCRKOl09Rv1dPLr8hHnVjHqdolKwDrdNyk+Hmrthi4lIGPPz3r39dLx0hsF5s40sZ3Us4Q==", + "dev": true, + "optional": true, + "dependencies": { + "iconv-lite": "^0.6.3", + "sax": "^1.2.4" + }, + "bin": { + "needle": "bin/needle" + }, + "engines": { + "node": ">= 4.4.x" + } + }, + "node_modules/needle/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "optional": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==" + }, + "node_modules/ngx-markdown": { + "version": "15.1.2", + "resolved": "https://registry.npmjs.org/ngx-markdown/-/ngx-markdown-15.1.2.tgz", + "integrity": "sha512-mAUORpUnHCV4tnxEHV4oS5YEdIaolUclulCblUrvAEU3AEND8MMTxlwHujqVC2M398/aKH0SBSrjLzDbMUJCoQ==", + "dependencies": { + "tslib": "^2.3.0" + }, + "optionalDependencies": { + "clipboard": "^2.0.11", + "emoji-toolkit": "^7.0.0", + "katex": "^0.16.0", + "mermaid": "^9.1.2", + "prismjs": "^1.28.0" + }, + "peerDependencies": { + "@angular/common": "^15.0.0", + "@angular/core": "^15.0.0", + "@angular/platform-browser": "^15.0.0", + "@types/marked": "^4.0.3", + "marked": "^4.0.17", + "rxjs": "^6.5.3 || ^7.4.0", + "zone.js": "~0.11.4 || ~0.12.0 || ~0.13.0" + } + }, + "node_modules/nice-napi": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/nice-napi/-/nice-napi-1.0.2.tgz", + "integrity": "sha512-px/KnJAJZf5RuBGcfD+Sp2pAKq0ytz8j+1NehvgIGFkvtvFrDM3T8E4x/JJODXK9WZow8RRGrbA9QQ3hs+pDhA==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "!win32" + ], + "dependencies": { + "node-addon-api": "^3.0.0", + "node-gyp-build": "^4.2.2" + } + }, + "node_modules/node-addon-api": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-3.2.1.tgz", + "integrity": "sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A==", + "dev": true, + "optional": true + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-forge": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", + "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==", + "dev": true, + "engines": { + "node": ">= 6.13.0" + } + }, + "node_modules/node-gyp": { + "version": "9.4.1", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-9.4.1.tgz", + "integrity": "sha512-OQkWKbjQKbGkMf/xqI1jjy3oCTgMKJac58G2+bjZb3fza6gW2YrCSdMQYaoTb70crvE//Gngr4f0AgVHmqHvBQ==", + "dev": true, + "dependencies": { + "env-paths": "^2.2.0", + "exponential-backoff": "^3.1.1", + "glob": "^7.1.4", + "graceful-fs": "^4.2.6", + "make-fetch-happen": "^10.0.3", + "nopt": "^6.0.0", + "npmlog": "^6.0.0", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "tar": "^6.1.2", + "which": "^2.0.2" + }, + "bin": { + "node-gyp": "bin/node-gyp.js" + }, + "engines": { + "node": "^12.13 || ^14.13 || >=16" + } + }, + "node_modules/node-gyp-build": { + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.1.tgz", + "integrity": "sha512-OSs33Z9yWr148JZcbZd5WiAXhh/n9z8TxQcdMhIOlpN9AhWpLfvVFO73+m77bBABQMaY9XSvIa+qk0jlI7Gcaw==", + "dev": true, + "optional": true, + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, + "node_modules/node-gyp/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/node-gyp/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/node-gyp/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/node-gyp/node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/node-releases": { + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", + "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==" + }, + "node_modules/non-layered-tidy-tree-layout": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/non-layered-tidy-tree-layout/-/non-layered-tidy-tree-layout-2.0.2.tgz", + "integrity": "sha512-gkXMxRzUH+PB0ax9dUN0yYF0S25BqeAYqhgMaLUFmpXLEk7Fcu8f4emJuOAY0V8kjDICxROIKsTAKsV/v355xw==", + "optional": true + }, + "node_modules/nopt": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-6.0.0.tgz", + "integrity": "sha512-ZwLpbTgdhuZUnZzjd7nb1ZV+4DoiC6/sfiVKok72ym/4Tlf+DFdlHYmT2JPmcNNWV6Pi3SDf1kT+A4r9RTuT9g==", + "dev": true, + "dependencies": { + "abbrev": "^1.0.0" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/normalize-package-data": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-5.0.0.tgz", + "integrity": "sha512-h9iPVIfrVZ9wVYQnxFgtw1ugSvGEMOlyPWWtm8BMJhnwyEL/FLbYbTY3V3PpjI/BUK67n9PEWDu6eHzu1fB15Q==", + "dev": true, + "dependencies": { + "hosted-git-info": "^6.0.0", + "is-core-module": "^2.8.1", + "semver": "^7.3.5", + "validate-npm-package-license": "^3.0.4" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "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==", + "devOptional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-range": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-bundled": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-3.0.1.tgz", + "integrity": "sha512-+AvaheE/ww1JEwRHOrn4WHNzOxGtVp+adrg2AeZS/7KuxGUYFuBta98wYpfHBbJp6Tg6j1NKSEVHNcfZzJHQwQ==", + "dev": true, + "dependencies": { + "npm-normalize-package-bin": "^3.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm-install-checks": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/npm-install-checks/-/npm-install-checks-6.3.0.tgz", + "integrity": "sha512-W29RiK/xtpCGqn6f3ixfRYGk+zRyr+Ew9F2E20BfXxT5/euLdA/Nm7fO7OeTGuAmTs30cpgInyJ0cYe708YTZw==", + "dev": true, + "dependencies": { + "semver": "^7.1.1" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm-normalize-package-bin": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-3.0.1.tgz", + "integrity": "sha512-dMxCf+zZ+3zeQZXKxmyuCKlIDPGuv8EF940xbkC4kQVDTtqoh6rJFO+JTKSA6/Rwi0getWmtuy4Itup0AMcaDQ==", + "dev": true, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm-package-arg": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-10.1.0.tgz", + "integrity": "sha512-uFyyCEmgBfZTtrKk/5xDfHp6+MdrqGotX/VoOyEEl3mBwiEE5FlBaePanazJSVMPT7vKepcjYBY2ztg9A3yPIA==", + "dev": true, + "dependencies": { + "hosted-git-info": "^6.0.0", + "proc-log": "^3.0.0", + "semver": "^7.3.5", + "validate-npm-package-name": "^5.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm-packlist": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-7.0.4.tgz", + "integrity": "sha512-d6RGEuRrNS5/N84iglPivjaJPxhDbZmlbTwTDX2IbcRHG5bZCdtysYMhwiPvcF4GisXHGn7xsxv+GQ7T/02M5Q==", + "dev": true, + "dependencies": { + "ignore-walk": "^6.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm-pick-manifest": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/npm-pick-manifest/-/npm-pick-manifest-8.0.1.tgz", + "integrity": "sha512-mRtvlBjTsJvfCCdmPtiu2bdlx8d/KXtF7yNXNWe7G0Z36qWA9Ny5zXsI2PfBZEv7SXgoxTmNaTzGSbbzDZChoA==", + "dev": true, + "dependencies": { + "npm-install-checks": "^6.0.0", + "npm-normalize-package-bin": "^3.0.0", + "npm-package-arg": "^10.0.0", + "semver": "^7.3.5" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm-registry-fetch": { + "version": "14.0.5", + "resolved": "https://registry.npmjs.org/npm-registry-fetch/-/npm-registry-fetch-14.0.5.tgz", + "integrity": "sha512-kIDMIo4aBm6xg7jOttupWZamsZRkAqMqwqqbVXnUqstY5+tapvv6bkH/qMR76jdgV+YljEUCyWx3hRYMrJiAgA==", + "dev": true, + "dependencies": { + "make-fetch-happen": "^11.0.0", + "minipass": "^5.0.0", + "minipass-fetch": "^3.0.0", + "minipass-json-stream": "^1.0.1", + "minizlib": "^2.1.2", + "npm-package-arg": "^10.0.0", + "proc-log": "^3.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm-registry-fetch/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/npm-registry-fetch/node_modules/make-fetch-happen": { + "version": "11.1.1", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-11.1.1.tgz", + "integrity": "sha512-rLWS7GCSTcEujjVBs2YqG7Y4643u8ucvCJeSRqiLYhesrDuzeuFIk37xREzAsfQaqzl8b9rNCE4m6J8tvX4Q8w==", + "dev": true, + "dependencies": { + "agentkeepalive": "^4.2.1", + "cacache": "^17.0.0", + "http-cache-semantics": "^4.1.1", + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.0", + "is-lambda": "^1.0.1", + "lru-cache": "^7.7.1", + "minipass": "^5.0.0", + "minipass-fetch": "^3.0.0", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^0.6.3", + "promise-retry": "^2.0.1", + "socks-proxy-agent": "^7.0.0", + "ssri": "^10.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm-registry-fetch/node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm-registry-fetch/node_modules/minipass-fetch": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-3.0.5.tgz", + "integrity": "sha512-2N8elDQAtSnFV0Dk7gt15KHsS0Fyz6CbYZ360h0WTYV1Ty46li3rAXVOQj1THMNLdmrD9Vt5pBPtWtVkpwGBqg==", + "dev": true, + "dependencies": { + "minipass": "^7.0.3", + "minipass-sized": "^1.0.3", + "minizlib": "^2.1.2" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + }, + "optionalDependencies": { + "encoding": "^0.1.13" + } + }, + "node_modules/npm-registry-fetch/node_modules/minipass-fetch/node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npmlog": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-6.0.2.tgz", + "integrity": "sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg==", + "deprecated": "This package is no longer supported.", + "dev": true, + "dependencies": { + "are-we-there-yet": "^3.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^4.0.3", + "set-blocking": "^2.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dev": true, + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "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==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz", + "integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/obuf": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz", + "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==", + "dev": true + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dev": true, + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/on-headers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", + "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/one-time": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/one-time/-/one-time-1.0.0.tgz", + "integrity": "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==", + "dependencies": { + "fn.name": "1.x.x" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/open": { + "version": "8.4.1", + "resolved": "https://registry.npmjs.org/open/-/open-8.4.1.tgz", + "integrity": "sha512-/4b7qZNhv6Uhd7jjnREh1NjnPxlTq+XNWPG88Ydkj5AILcA5m3ajvcg57pB24EQjKv0dK62XnDqk9c/hkIG5Kg==", + "dependencies": { + "define-lazy-prop": "^2.0.0", + "is-docker": "^2.1.1", + "is-wsl": "^2.2.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "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/ora": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", + "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", + "dependencies": { + "bl": "^4.1.0", + "chalk": "^4.1.0", + "cli-cursor": "^3.1.0", + "cli-spinners": "^2.5.0", + "is-interactive": "^1.0.0", + "is-unicode-supported": "^0.1.0", + "log-symbols": "^4.1.0", + "strip-ansi": "^6.0.0", + "wcwidth": "^1.0.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/ora/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/ora/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/ora/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/ora/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/ora/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-map": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", + "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", + "dev": true, + "dependencies": { + "aggregate-error": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-retry": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz", + "integrity": "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==", + "dev": true, + "dependencies": { + "@types/retry": "0.12.0", + "retry": "^0.13.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-retry/node_modules/retry": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.0.tgz", + "integrity": "sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw==", + "dev": true + }, + "node_modules/pacote": { + "version": "15.0.8", + "resolved": "https://registry.npmjs.org/pacote/-/pacote-15.0.8.tgz", + "integrity": "sha512-UlcumB/XS6xyyIMwg/WwMAyUmga+RivB5KgkRwA1hZNtrx+0Bt41KxHCvg1kr0pZ/ZeD8qjhW4fph6VaYRCbLw==", + "dev": true, + "dependencies": { + "@npmcli/git": "^4.0.0", + "@npmcli/installed-package-contents": "^2.0.1", + "@npmcli/promise-spawn": "^6.0.1", + "@npmcli/run-script": "^6.0.0", + "cacache": "^17.0.0", + "fs-minipass": "^3.0.0", + "minipass": "^4.0.0", + "npm-package-arg": "^10.0.0", + "npm-packlist": "^7.0.0", + "npm-pick-manifest": "^8.0.0", + "npm-registry-fetch": "^14.0.0", + "proc-log": "^3.0.0", + "promise-retry": "^2.0.1", + "read-package-json": "^6.0.0", + "read-package-json-fast": "^3.0.0", + "ssri": "^10.0.0", + "tar": "^6.1.11" + }, + "bin": { + "pacote": "lib/bin.js" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "dev": true + }, + "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, + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parse-node-version": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parse-node-version/-/parse-node-version-1.0.1.tgz", + "integrity": "sha512-3YHlOa/JgH6Mnpr05jP9eDG254US9ek25LyIxZlDItp2iJtwyaXQb57lBYLdT3MowkUFYEV2XXNAYIPlESvJlA==", + "dev": true, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/parse5": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz", + "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==", + "dev": true + }, + "node_modules/parse5-html-rewriting-stream": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/parse5-html-rewriting-stream/-/parse5-html-rewriting-stream-7.0.0.tgz", + "integrity": "sha512-mazCyGWkmCRWDI15Zp+UiCqMp/0dgEmkZRvhlsqqKYr4SsVm/TvnSpD9fCvqCA2zoWJcfRym846ejWBBHRiYEg==", + "dev": true, + "dependencies": { + "entities": "^4.3.0", + "parse5": "^7.0.0", + "parse5-sax-parser": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-html-rewriting-stream/node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/parse5-html-rewriting-stream/node_modules/parse5": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz", + "integrity": "sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==", + "dev": true, + "dependencies": { + "entities": "^4.4.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-htmlparser2-tree-adapter": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-6.0.1.tgz", + "integrity": "sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA==", + "dev": true, + "dependencies": { + "parse5": "^6.0.1" + } + }, + "node_modules/parse5-sax-parser": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/parse5-sax-parser/-/parse5-sax-parser-7.0.0.tgz", + "integrity": "sha512-5A+v2SNsq8T6/mG3ahcz8ZtQ0OUFTatxPbeidoMB7tkJSGDY3tdfl4MHovtLQHkEn5CGxijNWRQHhRQ6IRpXKg==", + "dev": true, + "dependencies": { + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-sax-parser/node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/parse5-sax-parser/node_modules/parse5": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz", + "integrity": "sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==", + "dev": true, + "dependencies": { + "entities": "^4.4.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "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/path-scurry/node_modules/lru-cache": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.3.0.tgz", + "integrity": "sha512-CQl19J/g+Hbjbv4Y3mFNNXFEL/5t/KCg8POCuUqd4rMKjGG+j1ybER83hxV58zL+dFI1PTkt3GNFSHRt+d8qEQ==", + "dev": true, + "engines": { + "node": "14 || >=16.14" + } + }, + "node_modules/path-scurry/node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==", + "dev": true + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/picocolors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", + "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "devOptional": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "dev": true, + "optional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/piscina": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/piscina/-/piscina-3.2.0.tgz", + "integrity": "sha512-yn/jMdHRw+q2ZJhFhyqsmANcbF6V2QwmD84c6xRau+QpQOmtrBCoRGdvTfeuFDYXB5W2m6MfLkjkvQa9lUSmIA==", + "dev": true, + "dependencies": { + "eventemitter-asyncresource": "^1.0.0", + "hdr-histogram-js": "^2.0.1", + "hdr-histogram-percentiles-obj": "^3.0.0" + }, + "optionalDependencies": { + "nice-napi": "^1.0.2" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/postcss": { + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.6", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-loader": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/postcss-loader/-/postcss-loader-7.0.2.tgz", + "integrity": "sha512-fUJzV/QH7NXUAqV8dWJ9Lg4aTkDCezpTS5HgJ2DvqznexTbSTxgi/dTECvTZ15BwKTtk8G/bqI/QTu2HPd3ZCg==", + "dev": true, + "dependencies": { + "cosmiconfig": "^7.0.0", + "klona": "^2.0.5", + "semver": "^7.3.8" + }, + "engines": { + "node": ">= 14.15.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "postcss": "^7.0.0 || ^8.0.1", + "webpack": "^5.0.0" + } + }, + "node_modules/postcss-modules-extract-imports": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.1.0.tgz", + "integrity": "sha512-k3kNe0aNFQDAZGbin48pL2VNidTF0w4/eASDsxlyspobzU3wZQLOGj7L9gfRe0Jo9/4uud09DsjFNH7winGv8Q==", + "dev": true, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-local-by-default": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.5.tgz", + "integrity": "sha512-6MieY7sIfTK0hYfafw1OMEG+2bg8Q1ocHCpoWLqOKj3JXlKu4G7btkmM/B7lFubYkYWmRSPLZi5chid63ZaZYw==", + "dev": true, + "dependencies": { + "icss-utils": "^5.0.0", + "postcss-selector-parser": "^6.0.2", + "postcss-value-parser": "^4.1.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-scope": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.2.0.tgz", + "integrity": "sha512-oq+g1ssrsZOsx9M96c5w8laRmvEu9C3adDSjI8oTcbfkrTE8hx/zfyobUoWIxaKPO8bt6S62kxpw5GqypEw1QQ==", + "dev": true, + "dependencies": { + "postcss-selector-parser": "^6.0.4" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-values": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz", + "integrity": "sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==", + "dev": true, + "dependencies": { + "icss-utils": "^5.0.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.0.tgz", + "integrity": "sha512-UMz42UD0UY0EApS0ZL9o1XnLhSTtvvvLe5Dc2H2O56fvRZi+KulDyf5ctDhhtYJBGKStV2FL1fy6253cmLgqVQ==", + "dev": true, + "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==", + "dev": true + }, + "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/prettier": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.5.tgz", + "integrity": "sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prettier-linter-helpers": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz", + "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-diff": "^1.1.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/pretty-bytes": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz", + "integrity": "sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==", + "dev": true, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/prismjs": { + "version": "1.29.0", + "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.29.0.tgz", + "integrity": "sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q==", + "engines": { + "node": ">=6" + } + }, + "node_modules/proc-log": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-3.0.0.tgz", + "integrity": "sha512-++Vn7NS4Xf9NacaU9Xq3URUuqZETPsf8L4j5/ckhaRYsfPeRyzGw+iDjFhV/Jr3uNmTvvddEJFWh5R1gRgUH8A==", + "dev": true, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true + }, + "node_modules/promise-inflight": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", + "integrity": "sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==", + "dev": true + }, + "node_modules/promise-retry": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", + "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", + "dev": true, + "dependencies": { + "err-code": "^2.0.2", + "retry": "^0.12.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/protobufjs": { + "version": "6.11.4", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.11.4.tgz", + "integrity": "sha512-5kQWPaJHi1WoCpjTGszzQ32PG2F4+wRY6BmAT4Vfw56Q2FZ4YZzK20xUYQH4YkfehY1e6QSICrJquM6xXZNcrw==", + "hasInstallScript": true, + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/long": "^4.0.1", + "@types/node": ">=13.7.0", + "long": "^4.0.0" + }, + "bin": { + "pbjs": "bin/pbjs", + "pbts": "bin/pbts" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dev": true, + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/proxy-addr/node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "dev": true, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/prr": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", + "integrity": "sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw==", + "dev": true, + "optional": true + }, + "node_modules/punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==", + "dev": true + }, + "node_modules/qjobs": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/qjobs/-/qjobs-1.2.0.tgz", + "integrity": "sha512-8YOJEHtxpySA3fFDyCRxA+UUV+fA+rTWnuWvylOK/NCjhY+b4ocCtmu8TtsWb+mYeU+GCHf/S66KZF/AsteKHg==", + "dev": true, + "engines": { + "node": ">=0.9" + } + }, + "node_modules/qs": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", + "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "dev": true, + "dependencies": { + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/quick-lru": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-4.0.1.tgz", + "integrity": "sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "dev": true, + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/read-package-json": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/read-package-json/-/read-package-json-6.0.4.tgz", + "integrity": "sha512-AEtWXYfopBj2z5N5PbkAOeNHRPUg5q+Nen7QLxV8M2zJq1ym6/lCz3fYNTCXe19puu2d06jfHhrP7v/S2PtMMw==", + "deprecated": "This package is no longer supported. Please use @npmcli/package-json instead.", + "dev": true, + "dependencies": { + "glob": "^10.2.2", + "json-parse-even-better-errors": "^3.0.0", + "normalize-package-data": "^5.0.0", + "npm-normalize-package-bin": "^3.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/read-package-json-fast": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/read-package-json-fast/-/read-package-json-fast-3.0.2.tgz", + "integrity": "sha512-0J+Msgym3vrLOUB3hzQCuZHII0xkNGCtz/HJH9xZshwv9DbDwkw1KaE3gx/e2J5rpEY5rtOy6cyhKOPrkP7FZw==", + "dev": true, + "dependencies": { + "json-parse-even-better-errors": "^3.0.0", + "npm-normalize-package-bin": "^3.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/read-package-json-fast/node_modules/json-parse-even-better-errors": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-3.0.2.tgz", + "integrity": "sha512-fi0NG4bPjCHunUJffmLd0gxssIgkNmArMvis4iNah6Owg1MCJjWhEcDLmsK6iGkJq3tHwbDkTlce70/tmXN4cQ==", + "dev": true, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/read-package-json/node_modules/glob": { + "version": "10.4.2", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.2.tgz", + "integrity": "sha512-GwMlUF6PkPo3Gk21UxkCohOv0PLcIXVtKyLlpEI28R/cO/4eNOdmLk3CMW1wROV/WR/EsZOWAfBbBOqYvs88/w==", + "dev": true, + "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" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/read-package-json/node_modules/json-parse-even-better-errors": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-3.0.2.tgz", + "integrity": "sha512-fi0NG4bPjCHunUJffmLd0gxssIgkNmArMvis4iNah6Owg1MCJjWhEcDLmsK6iGkJq3tHwbDkTlce70/tmXN4cQ==", + "dev": true, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/read-package-json/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/read-package-json/node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/read-pkg": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz", + "integrity": "sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/normalize-package-data": "^2.4.0", + "normalize-package-data": "^2.5.0", + "parse-json": "^5.0.0", + "type-fest": "^0.6.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/read-pkg-up": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-7.0.1.tgz", + "integrity": "sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^4.1.0", + "read-pkg": "^5.2.0", + "type-fest": "^0.8.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-pkg-up/node_modules/type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=8" + } + }, + "node_modules/read-pkg/node_modules/hosted-git-info": { + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", + "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", + "dev": true, + "license": "ISC" + }, + "node_modules/read-pkg/node_modules/normalize-package-data": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", + "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "hosted-git-info": "^2.1.4", + "resolve": "^1.10.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + } + }, + "node_modules/read-pkg/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/read-pkg/node_modules/type-fest": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.6.0.tgz", + "integrity": "sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=8" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "devOptional": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/reflect-metadata": { + "version": "0.1.14", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.14.tgz", + "integrity": "sha512-ZhYeb6nRaXCfhnndflDK8qI6ZQ/YcWZCISRAWICW9XYqMUwjZM9Z0DveWX/ABN01oxSHwVxKQmxeYZSsm0jh5A==", + "dev": true + }, + "node_modules/regenerate": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", + "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==", + "dev": true + }, + "node_modules/regenerate-unicode-properties": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.1.1.tgz", + "integrity": "sha512-X007RyZLsCJVVrjgEFVpLUTZwyOZk3oiL75ZcuYjlIWd6rNJtOjkBwQc5AsRrpbKVkxN6sklw/k/9m2jJYOf8Q==", + "dev": true, + "dependencies": { + "regenerate": "^1.4.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/regenerator-runtime": { + "version": "0.13.11", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", + "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==", + "dev": true + }, + "node_modules/regenerator-transform": { + "version": "0.15.2", + "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.15.2.tgz", + "integrity": "sha512-hfMp2BoF0qOk3uc5V20ALGDS2ddjQaLrdl7xrGXvAIow7qeWRM2VA2HuCHkUKk9slq3VwEwLNK3DFBqDfPGYtg==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.8.4" + } + }, + "node_modules/regex-parser": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/regex-parser/-/regex-parser-2.3.0.tgz", + "integrity": "sha512-TVILVSz2jY5D47F4mA4MppkBrafEaiUWJO/TcZHEIuI13AqoZMkK1WMA4Om1YkYbTx+9Ki1/tSUXbceyr9saRg==", + "dev": true + }, + "node_modules/regexpp": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz", + "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + } + }, + "node_modules/regexpu-core": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-5.3.2.tgz", + "integrity": "sha512-RAM5FlZz+Lhmo7db9L298p2vHP5ZywrVXmVXpmAD9GuL5MPH6t9ROw1iA/wfHkQ76Qe7AaPF0nGuim96/IrQMQ==", + "dev": true, + "dependencies": { + "@babel/regjsgen": "^0.8.0", + "regenerate": "^1.4.2", + "regenerate-unicode-properties": "^10.1.0", + "regjsparser": "^0.9.1", + "unicode-match-property-ecmascript": "^2.0.0", + "unicode-match-property-value-ecmascript": "^2.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/regjsparser": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.9.1.tgz", + "integrity": "sha512-dQUtn90WanSNl+7mQKcXAgZxvUe7Z0SqXlgzv0za4LwiUhyzBC58yQO3liFoUgu8GiJVInAhJjkj1N0EtQ5nkQ==", + "dev": true, + "dependencies": { + "jsesc": "~0.5.0" + }, + "bin": { + "regjsparser": "bin/parser" + } + }, + "node_modules/regjsparser/node_modules/jsesc": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", + "integrity": "sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==", + "dev": true, + "bin": { + "jsesc": "bin/jsesc" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "engines": { + "node": ">=0.10.0" + } + }, + "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==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "dev": true + }, + "node_modules/resolve": { + "version": "1.22.1", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz", + "integrity": "sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==", + "dev": true, + "dependencies": { + "is-core-module": "^2.9.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-url-loader": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-url-loader/-/resolve-url-loader-5.0.0.tgz", + "integrity": "sha512-uZtduh8/8srhBoMx//5bwqjQ+rfYOUq8zC9NrMUGtjBiGTtFJM42s58/36+hTqeqINcnYe08Nj3LkK9lW4N8Xg==", + "dev": true, + "dependencies": { + "adjust-sourcemap-loader": "^4.0.0", + "convert-source-map": "^1.7.0", + "loader-utils": "^2.0.0", + "postcss": "^8.2.14", + "source-map": "0.6.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/resolve-url-loader/node_modules/loader-utils": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", + "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", + "dev": true, + "dependencies": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" + }, + "engines": { + "node": ">=8.9.0" + } + }, + "node_modules/resolve-url-loader/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "dev": true + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/rimraf/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/robust-predicates": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.2.tgz", + "integrity": "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==", + "optional": true + }, + "node_modules/run-async": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", + "integrity": "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/rw": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz", + "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==", + "optional": true + }, + "node_modules/rxfire": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/rxfire/-/rxfire-6.0.3.tgz", + "integrity": "sha512-77nkyffHh7jgfi1YA/N9RI+kWxYpgKk6GRML1lyersvaqbJt4hkvWwk1rWib9Rb5Lr5mT+Ha45lu7nM79sJCZA==", + "dependencies": { + "tslib": "^1.9.0 || ~2.1.0" + }, + "peerDependencies": { + "firebase": "^9.0.0", + "rxjs": "^6.0.0 || ^7.0.0" + } + }, + "node_modules/rxfire/node_modules/tslib": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.1.0.tgz", + "integrity": "sha512-hcVC3wYEziELGGmEEXue7D75zbwIIVUMWAVbHItGPx0ziyXxrOMQx4rQEVEV45Ut/1IotuEvwqPopzIOkDMf0A==" + }, + "node_modules/rxjs": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/safe-stable-stringify": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.4.3.tgz", + "integrity": "sha512-e2bDA2WJT0wxseVd4lsDP4+3ONX6HpMXQa1ZhFQ7SU+GjvORCmShbCMltrtIDfkYhVHrOcPtj+KhmDBdPdZD1g==", + "engines": { + "node": ">=10" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "node_modules/safevalues": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/safevalues/-/safevalues-0.3.4.tgz", + "integrity": "sha512-LRneZZRXNgjzwG4bDQdOTSbze3fHm1EAKN/8bePxnlEZiBmkYEDggaHbuvHI9/hoqHbGfsEA7tWS9GhYHZBBsw==" + }, + "node_modules/sass": { + "version": "1.58.1", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.58.1.tgz", + "integrity": "sha512-bnINi6nPXbP1XNRaranMFEBZWUfdW/AF16Ql5+ypRxfTvCRTTKrLsMIakyDcayUt2t/RZotmL4kgJwNH5xO+bg==", + "dev": true, + "dependencies": { + "chokidar": ">=3.0.0 <4.0.0", + "immutable": "^4.0.0", + "source-map-js": ">=0.6.2 <2.0.0" + }, + "bin": { + "sass": "sass.js" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/sass-loader": { + "version": "13.2.0", + "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-13.2.0.tgz", + "integrity": "sha512-JWEp48djQA4nbZxmgC02/Wh0eroSUutulROUusYJO9P9zltRbNN80JCBHqRGzjd4cmZCa/r88xgfkjGD0TXsHg==", + "dev": true, + "dependencies": { + "klona": "^2.0.4", + "neo-async": "^2.6.2" + }, + "engines": { + "node": ">= 14.15.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "fibers": ">= 3.1.0", + "node-sass": "^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0", + "sass": "^1.3.0", + "sass-embedded": "*", + "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "fibers": { + "optional": true + }, + "node-sass": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + } + } + }, + "node_modules/sax": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz", + "integrity": "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==", + "dev": true, + "optional": true + }, + "node_modules/schema-utils": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.2.0.tgz", + "integrity": "sha512-L0jRsrPpjdckP3oPug3/VxNKt2trR8TcabrM6FOAAlvC/9Phcmm+cuAgTlxBqdBR1WJx7Naj9WHw+aOmheSVbw==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/select": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/select/-/select-1.1.2.tgz", + "integrity": "sha512-OwpTSOfy6xSs1+pwcNrv0RBMOzI39Lp3qQKUTPVVPRjCdNa5JH/oPRiqsesIskK8TVgmRiHwO4KXlV2Li9dANA==" + }, + "node_modules/select-hose": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz", + "integrity": "sha512-mEugaLK+YfkijB4fx0e6kImuJdCIt2LxCRcbEYPqRGCs4F2ogyfZU5IAZRdjCP8JPq2AtdNoC/Dux63d9Kiryg==", + "dev": true + }, + "node_modules/selfsigned": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-2.4.1.tgz", + "integrity": "sha512-th5B4L2U+eGLq1TVh7zNRGBapioSORUeymIydxgFpwww9d2qyKvtuPU2jJuHvYAwwqi2Y596QBL3eEqcPEYL8Q==", + "dev": true, + "dependencies": { + "@types/node-forge": "^1.3.0", + "node-forge": "^1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/semver": { + "version": "7.5.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.3.tgz", + "integrity": "sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ==", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/semver/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/semver/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + }, + "node_modules/send": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", + "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "dev": true, + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/send/node_modules/debug/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "node_modules/send/node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true, + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/send/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==", + "dev": true + }, + "node_modules/send/node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/serialize-javascript": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/serve-index": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/serve-index/-/serve-index-1.9.1.tgz", + "integrity": "sha512-pXHfKNP4qujrtteMrSBb0rc8HJ9Ms/GrXwcUtUtD5s4ewDJI8bT3Cz2zTVRMKtri49pLx2e0Ya8ziP5Ya2pZZw==", + "dev": true, + "dependencies": { + "accepts": "~1.3.4", + "batch": "0.6.1", + "debug": "2.6.9", + "escape-html": "~1.0.3", + "http-errors": "~1.6.2", + "mime-types": "~2.1.17", + "parseurl": "~1.3.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/serve-index/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/serve-index/node_modules/depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/serve-index/node_modules/http-errors": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", + "integrity": "sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A==", + "dev": true, + "dependencies": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.0", + "statuses": ">= 1.4.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/serve-index/node_modules/inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==", + "dev": true + }, + "node_modules/serve-index/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "node_modules/serve-index/node_modules/setprototypeof": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", + "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==", + "dev": true + }, + "node_modules/serve-static": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", + "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", + "dev": true, + "dependencies": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.18.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "dev": true + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "dev": true + }, + "node_modules/shallow-clone": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", + "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", + "dev": true, + "dependencies": { + "kind-of": "^6.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", + "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", + "object-inspect": "^1.13.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==" + }, + "node_modules/simple-swizzle": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", + "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", + "dependencies": { + "is-arrayish": "^0.3.1" + } + }, + "node_modules/simple-swizzle/node_modules/is-arrayish": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", + "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==" + }, + "node_modules/slash": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-4.0.0.tgz", + "integrity": "sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "dev": true, + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socket.io": { + "version": "4.7.5", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.7.5.tgz", + "integrity": "sha512-DmeAkF6cwM9jSfmp6Dr/5/mfMwb5Z5qRrSXLpo3Fq5SqyU8CMF15jIN4ZhfSwu35ksM1qmHZDQ/DK5XTccSTvA==", + "dev": true, + "dependencies": { + "accepts": "~1.3.4", + "base64id": "~2.0.0", + "cors": "~2.8.5", + "debug": "~4.3.2", + "engine.io": "~6.5.2", + "socket.io-adapter": "~2.5.2", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/socket.io-adapter": { + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.5.tgz", + "integrity": "sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg==", + "dev": true, + "dependencies": { + "debug": "~4.3.4", + "ws": "~8.17.1" + } + }, + "node_modules/socket.io-parser": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", + "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==", + "dev": true, + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/sockjs": { + "version": "0.3.24", + "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.24.tgz", + "integrity": "sha512-GJgLTZ7vYb/JtPSSZ10hsOYIvEYsjbNU+zPdIHcUaWVNUEPivzxku31865sSSud0Da0W4lEeOPlmw93zLQchuQ==", + "dev": true, + "dependencies": { + "faye-websocket": "^0.11.3", + "uuid": "^8.3.2", + "websocket-driver": "^0.7.4" + } + }, + "node_modules/sockjs/node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "dev": true, + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/socks": { + "version": "2.8.3", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.3.tgz", + "integrity": "sha512-l5x7VUUWbjVFbafGLxPWkYsHIhEvmF85tbIeFZWc8ZPtoMyybuEhL7Jye/ooC4/d48FgOjSJXgsF/AJPYCW8Zw==", + "dev": true, + "dependencies": { + "ip-address": "^9.0.5", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-7.0.0.tgz", + "integrity": "sha512-Fgl0YPZ902wEsAyiQ+idGd1A7rSFx/ayC1CQVMw5P+EQx2V0SgpGtf6OKFhVjPflPUl9YMmEOnmfjCdMUsygww==", + "dev": true, + "dependencies": { + "agent-base": "^6.0.2", + "debug": "^4.3.3", + "socks": "^2.6.2" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/source-map": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", + "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", + "engines": { + "node": ">= 8" + } + }, + "node_modules/source-map-js": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", + "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-loader": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/source-map-loader/-/source-map-loader-4.0.1.tgz", + "integrity": "sha512-oqXpzDIByKONVY8g1NUPOTQhe0UTU5bWUl32GSkqK2LjJj0HmwTMVKxcUip0RgAYhY1mqgOxjbQM48a0mmeNfA==", + "dev": true, + "dependencies": { + "abab": "^2.0.6", + "iconv-lite": "^0.6.3", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": ">= 14.15.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.72.1" + } + }, + "node_modules/source-map-loader/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/source-map-support/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/spdx-correct": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", + "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", + "dev": true, + "dependencies": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-exceptions": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", + "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==", + "dev": true + }, + "node_modules/spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "dev": true, + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-license-ids": { + "version": "3.0.18", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.18.tgz", + "integrity": "sha512-xxRs31BqRYHwiMzudOrpSiHtZ8i/GeionCBDSilhYRj+9gIcI8wCZTlXZKu9vZIVqViP3dcp9qE5G6AlIaD+TQ==", + "dev": true + }, + "node_modules/spdy": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/spdy/-/spdy-4.0.2.tgz", + "integrity": "sha512-r46gZQZQV+Kl9oItvl1JZZqJKGr+oEkB08A6BzkiR7593/7IbtuncXHd2YoYeTsG4157ZssMu9KYvUHLcjcDoA==", + "dev": true, + "dependencies": { + "debug": "^4.1.0", + "handle-thing": "^2.0.0", + "http-deceiver": "^1.2.7", + "select-hose": "^2.0.0", + "spdy-transport": "^3.0.0" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/spdy-transport": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/spdy-transport/-/spdy-transport-3.0.0.tgz", + "integrity": "sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw==", + "dev": true, + "dependencies": { + "debug": "^4.1.0", + "detect-node": "^2.0.4", + "hpack.js": "^2.1.6", + "obuf": "^1.1.2", + "readable-stream": "^3.0.6", + "wbuf": "^1.7.3" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true + }, + "node_modules/ssri": { + "version": "10.0.6", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-10.0.6.tgz", + "integrity": "sha512-MGrFH9Z4NP9Iyhqn16sDtBpRRNJ0Y2hNa6D65h736fVSaPCHr4DM4sWUNvVaSuC+0OBGhwsrydQwmgfg5LncqQ==", + "dev": true, + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/ssri/node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/stack-trace": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", + "integrity": "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==", + "engines": { + "node": "*" + } + }, + "node_modules/statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/streamroller": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/streamroller/-/streamroller-3.1.5.tgz", + "integrity": "sha512-KFxaM7XT+irxvdqSP1LGLgNWbYN7ay5owZ3r/8t77p+EtSUAfUgtl7be3xtqtOmGUl9K9YPO2ca8133RlTjvKw==", + "dev": true, + "dependencies": { + "date-format": "^4.0.14", + "debug": "^4.3.4", + "fs-extra": "^8.1.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "min-indent": "^1.0.0" + }, + "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/stylis": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.2.tgz", + "integrity": "sha512-bhtUjWd/z6ltJiQwg0dUfxEJ+W+jdqQd8TbWLWyeIJHlnsqmGLRFFd8e5mA0AZi/zx90smXRlN66YMTcaSFifg==", + "optional": true + }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/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==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/symbol-observable": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-4.0.0.tgz", + "integrity": "sha512-b19dMThMV4HVFynSAM1++gBHAbk2Tc/osgLIBZMKsyqh34jb2e8Os7T6ZW/Bt3pJFdBTd2JwAnAAEQV7rSNvcQ==", + "dev": true, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/synckit": { + "version": "0.8.8", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.8.8.tgz", + "integrity": "sha512-HwOKAP7Wc5aRGYdKH+dw0PRRpbO841v2DENBtjnR5HFWoiNByAl7vrx3p0G/rCyYXQsrxqtX48TImFtPcIHSpQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@pkgr/core": "^0.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/unts" + } + }, + "node_modules/tapable": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", + "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", + "engines": { + "node": ">=6" + } + }, + "node_modules/tar": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "dev": true, + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/tar/node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "dev": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/tar/node_modules/fs-minipass/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tar/node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/tar/node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true, + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/tar/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/terser": { + "version": "5.16.3", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.16.3.tgz", + "integrity": "sha512-v8wWLaS/xt3nE9dgKEWhNUFP6q4kngO5B8eYFUuebsu7Dw/UNAnpUod6UHo04jSSkv8TzKHjZDSd7EXdDQAl8Q==", + "dev": true, + "dependencies": { + "@jridgewell/source-map": "^0.3.2", + "acorn": "^8.5.0", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser-webpack-plugin": { + "version": "5.3.10", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.10.tgz", + "integrity": "sha512-BKFPWlPDndPs+NGGCr1U59t0XScL5317Y0UReNrHaw9/FwhPENlq6bfgs+4yPfyP51vqC1bQ4rp1EfXW5ZSH9w==", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.20", + "jest-worker": "^27.4.5", + "schema-utils": "^3.1.1", + "serialize-javascript": "^6.0.1", + "terser": "^5.26.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "uglify-js": { + "optional": true + } + } + }, + "node_modules/terser-webpack-plugin/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "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/terser-webpack-plugin/node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "node_modules/terser-webpack-plugin/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==" + }, + "node_modules/terser-webpack-plugin/node_modules/schema-utils": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/terser-webpack-plugin/node_modules/terser": { + "version": "5.31.1", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.31.1.tgz", + "integrity": "sha512-37upzU1+viGvuFtBo9NPufCb9dwM0+l9hMxYyWfBA+fbwrPqNJAhbZ6W47bBFnZHKHTUBnMvi87434qq+qnxOg==", + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.8.2", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/test-exclude/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/test-exclude/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/test-exclude/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/text-hex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", + "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==" + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true + }, + "node_modules/through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==" + }, + "node_modules/thunky": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz", + "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==", + "dev": true + }, + "node_modules/tiny-emitter": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/tiny-emitter/-/tiny-emitter-2.1.0.tgz", + "integrity": "sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q==" + }, + "node_modules/tmp": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "dependencies": { + "os-tmpdir": "~1.0.2" + }, + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "devOptional": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "dev": true, + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "bin": { + "tree-kill": "cli.js" + } + }, + "node_modules/trim-newlines": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-3.0.1.tgz", + "integrity": "sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/triple-beam": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.4.1.tgz", + "integrity": "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==", + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/ts-dedent": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/ts-dedent/-/ts-dedent-2.2.0.tgz", + "integrity": "sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==", + "optional": true, + "engines": { + "node": ">=6.10" + } + }, + "node_modules/tslib": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", + "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==" + }, + "node_modules/tsutils": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", + "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^1.8.1" + }, + "engines": { + "node": ">= 6" + }, + "peerDependencies": { + "typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta" + } + }, + "node_modules/tsutils/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true, + "license": "0BSD" + }, + "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.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dev": true, + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typed-assert": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/typed-assert/-/typed-assert-1.0.9.tgz", + "integrity": "sha512-KNNZtayBCtmnNmbo5mG47p1XsCyrx6iVqomjcZnec/1Y5GGARaxPs6r49RnSPeUP3YjNYiU9sQHAtY4BBvnZwg==", + "dev": true + }, + "node_modules/typescript": { + "version": "4.9.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", + "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + }, + "node_modules/ua-parser-js": { + "version": "0.7.38", + "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.38.tgz", + "integrity": "sha512-fYmIy7fKTSFAhG3fuPlubeGaMoAd6r0rSnfEsO5nEY55i26KSLt9EH7PLQiiqPUhNqYIJvSkTy1oArIcXAbPbA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/ua-parser-js" + }, + { + "type": "paypal", + "url": "https://paypal.me/faisalman" + }, + { + "type": "github", + "url": "https://github.com/sponsors/faisalman" + } + ], + "engines": { + "node": "*" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "license": "MIT" + }, + "node_modules/unicode-canonical-property-names-ecmascript": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz", + "integrity": "sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-ecmascript": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz", + "integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==", + "dev": true, + "dependencies": { + "unicode-canonical-property-names-ecmascript": "^2.0.0", + "unicode-property-aliases-ecmascript": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-value-ecmascript": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.1.0.tgz", + "integrity": "sha512-qxkjQt6qjg/mYscYMC0XKRn3Rh0wFPlfxB0xkt9CfyTvpX1Ra0+rAmdX2QyAobptSEvuy4RtpPRui6XkV+8wjA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-property-aliases-ecmascript": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz", + "integrity": "sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/unique-filename": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-3.0.0.tgz", + "integrity": "sha512-afXhuC55wkAmZ0P18QsVE6kp8JaxrEokN2HGIoIVv2ijHQd419H0+6EigAFcIzXeMIkcIkNBpB3L/DXB3cTS/g==", + "dev": true, + "dependencies": { + "unique-slug": "^4.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/unique-slug": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-4.0.0.tgz", + "integrity": "sha512-WrcA6AyEfqDX5bWige/4NQfPZMtASNVxdmWR76WESYQVAACSgWcR6e9i0mofqqBxYFtL4oAxPIptY73/0YE1DQ==", + "dev": true, + "dependencies": { + "imurmurhash": "^0.1.4" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.0.tgz", + "integrity": "sha512-EdRAaAyk2cUE1wOf2DkEhzxqOQvFOoRJFNS6NeyJ01Gp2beMRpBAINjM2iDXE3KCuKhwnvHIQCJm6ThL2Z+HzQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "escalade": "^3.1.2", + "picocolors": "^1.0.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/uri-js/node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "engines": { + "node": ">=6" + } + }, + "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==" + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "dev": true, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "dev": true, + "dependencies": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "node_modules/validate-npm-package-name": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-5.0.1.tgz", + "integrity": "sha512-OljLrQ9SQdOUqTaQxqL5dEfZWrXExyyWsozYlAWFawPVNuD83igl7uJD2RTkNMbniIYgt8l81eCJGIdQF7avLQ==", + "dev": true, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/void-elements": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-2.0.1.tgz", + "integrity": "sha512-qZKX4RnBzH2ugr8Lxa7x+0V6XD9Sb/ouARtiasEQCHB1EVU4NXtmHsDDrx1dO4ne5fc3J6EW05BP1Dl0z0iung==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/watchpack": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.1.tgz", + "integrity": "sha512-8wrBCMtVhqcXP2Sup1ctSkga6uc2Bx0IIvKyT7yTFier5AXHooSI+QyQQAtTb7+E0IUCCKyTFmXqdqgum2XWGg==", + "dependencies": { + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/wbuf": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/wbuf/-/wbuf-1.7.3.tgz", + "integrity": "sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA==", + "dev": true, + "dependencies": { + "minimalistic-assert": "^1.0.0" + } + }, + "node_modules/wcwidth": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", + "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==", + "dependencies": { + "defaults": "^1.0.3" + } + }, + "node_modules/web-worker": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/web-worker/-/web-worker-1.3.0.tgz", + "integrity": "sha512-BSR9wyRsy/KOValMgd5kMyr3JzpdeoR9KVId8u5GVlTTAtNChlsE4yTxeY7zMdNSyOmoKBv8NH2qeRY9Tg+IaA==", + "optional": true + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + }, + "node_modules/webpack": { + "version": "5.76.1", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.76.1.tgz", + "integrity": "sha512-4+YIK4Abzv8172/SGqObnUjaIHjLEuUasz9EwQj/9xmPPkYJy2Mh03Q/lJfSD3YLzbxy5FeTq5Uw0323Oh6SJQ==", + "dependencies": { + "@types/eslint-scope": "^3.7.3", + "@types/estree": "^0.0.51", + "@webassemblyjs/ast": "1.11.1", + "@webassemblyjs/wasm-edit": "1.11.1", + "@webassemblyjs/wasm-parser": "1.11.1", + "acorn": "^8.7.1", + "acorn-import-assertions": "^1.7.6", + "browserslist": "^4.14.5", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.10.0", + "es-module-lexer": "^0.9.0", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.2.9", + "json-parse-even-better-errors": "^2.3.1", + "loader-runner": "^4.2.0", + "mime-types": "^2.1.27", + "neo-async": "^2.6.2", + "schema-utils": "^3.1.0", + "tapable": "^2.1.1", + "terser-webpack-plugin": "^5.1.3", + "watchpack": "^2.4.0", + "webpack-sources": "^3.2.3" + }, + "bin": { + "webpack": "bin/webpack.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-dev-middleware": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-6.1.2.tgz", + "integrity": "sha512-Wu+EHmX326YPYUpQLKmKbTyZZJIB8/n6R09pTmB03kJmnMsVPTo9COzHZFr01txwaCAuZvfBJE4ZCHRcKs5JaQ==", + "dev": true, + "dependencies": { + "colorette": "^2.0.10", + "memfs": "^3.4.12", + "mime-types": "^2.1.31", + "range-parser": "^1.2.1", + "schema-utils": "^4.0.0" + }, + "engines": { + "node": ">= 14.15.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "webpack": { + "optional": true + } + } + }, + "node_modules/webpack-dev-server": { + "version": "4.11.1", + "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-4.11.1.tgz", + "integrity": "sha512-lILVz9tAUy1zGFwieuaQtYiadImb5M3d+H+L1zDYalYoDl0cksAB1UNyuE5MMWJrG6zR1tXkCP2fitl7yoUJiw==", + "dev": true, + "dependencies": { + "@types/bonjour": "^3.5.9", + "@types/connect-history-api-fallback": "^1.3.5", + "@types/express": "^4.17.13", + "@types/serve-index": "^1.9.1", + "@types/serve-static": "^1.13.10", + "@types/sockjs": "^0.3.33", + "@types/ws": "^8.5.1", + "ansi-html-community": "^0.0.8", + "bonjour-service": "^1.0.11", + "chokidar": "^3.5.3", + "colorette": "^2.0.10", + "compression": "^1.7.4", + "connect-history-api-fallback": "^2.0.0", + "default-gateway": "^6.0.3", + "express": "^4.17.3", + "graceful-fs": "^4.2.6", + "html-entities": "^2.3.2", + "http-proxy-middleware": "^2.0.3", + "ipaddr.js": "^2.0.1", + "open": "^8.0.9", + "p-retry": "^4.5.0", + "rimraf": "^3.0.2", + "schema-utils": "^4.0.0", + "selfsigned": "^2.1.1", + "serve-index": "^1.9.1", + "sockjs": "^0.3.24", + "spdy": "^4.0.2", + "webpack-dev-middleware": "^5.3.1", + "ws": "^8.4.2" + }, + "bin": { + "webpack-dev-server": "bin/webpack-dev-server.js" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^4.37.0 || ^5.0.0" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-dev-server/node_modules/webpack-dev-middleware": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-5.3.4.tgz", + "integrity": "sha512-BVdTqhhs+0IfoeAf7EoH5WE+exCmqGerHfDM0IL096Px60Tq2Mn9MAbnaGUe6HiMa41KMCYF19gyzZmBcq/o4Q==", + "dev": true, + "dependencies": { + "colorette": "^2.0.10", + "memfs": "^3.4.3", + "mime-types": "^2.1.31", + "range-parser": "^1.2.1", + "schema-utils": "^4.0.0" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^4.0.0 || ^5.0.0" + } + }, + "node_modules/webpack-merge": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.8.0.tgz", + "integrity": "sha512-/SaI7xY0831XwP6kzuwhKWVKDP9t1QY1h65lAFLbZqMPIuYcD9QAW4u9STIbU9kaJbPBB/geU/gLr1wDjOhQ+Q==", + "dev": true, + "dependencies": { + "clone-deep": "^4.0.1", + "wildcard": "^2.0.0" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/webpack-sources": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz", + "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webpack-subresource-integrity": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/webpack-subresource-integrity/-/webpack-subresource-integrity-5.1.0.tgz", + "integrity": "sha512-sacXoX+xd8r4WKsy9MvH/q/vBtEHr86cpImXwyg74pFIpERKt6FmB8cXpeuh0ZLgclOlHI4Wcll7+R5L02xk9Q==", + "dev": true, + "dependencies": { + "typed-assert": "^1.0.8" + }, + "engines": { + "node": ">= 12" + }, + "peerDependencies": { + "html-webpack-plugin": ">= 5.0.0-beta.1 < 6", + "webpack": "^5.12.0" + }, + "peerDependenciesMeta": { + "html-webpack-plugin": { + "optional": true + } + } + }, + "node_modules/webpack/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "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/webpack/node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "node_modules/webpack/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==" + }, + "node_modules/webpack/node_modules/schema-utils": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/websocket-driver": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", + "integrity": "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==", + "dependencies": { + "http-parser-js": ">=0.5.1", + "safe-buffer": ">=5.1.0", + "websocket-extensions": ">=0.1.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/websocket-extensions": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz", + "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, + "node_modules/wide-align": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", + "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", + "dev": true, + "dependencies": { + "string-width": "^1.0.2 || 2 || 3 || 4" + } + }, + "node_modules/wildcard": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.1.tgz", + "integrity": "sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==", + "dev": true + }, + "node_modules/winston": { + "version": "3.13.0", + "resolved": "https://registry.npmjs.org/winston/-/winston-3.13.0.tgz", + "integrity": "sha512-rwidmA1w3SE4j0E5MuIufFhyJPBDG7Nu71RkZor1p2+qHvJSZ9GYDA81AyleQcZbh/+V6HjeBdfnTZJm9rSeQQ==", + "dependencies": { + "@colors/colors": "^1.6.0", + "@dabh/diagnostics": "^2.0.2", + "async": "^3.2.3", + "is-stream": "^2.0.0", + "logform": "^2.4.0", + "one-time": "^1.0.0", + "readable-stream": "^3.4.0", + "safe-stable-stringify": "^2.3.1", + "stack-trace": "0.0.x", + "triple-beam": "^1.3.0", + "winston-transport": "^4.7.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/winston-transport": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.7.0.tgz", + "integrity": "sha512-ajBj65K5I7denzer2IYW6+2bNIVqLGDHqDw3Ow8Ohh+vdW+rv4MZ6eiDvHoKhfJFZ2auyN8byXieDDJ96ViONg==", + "dependencies": { + "logform": "^2.3.2", + "readable-stream": "^3.6.0", + "triple-beam": "^1.3.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/winston/node_modules/@colors/colors": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.6.0.tgz", + "integrity": "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==", + "engines": { + "node": ">=0.1.90" + } + }, + "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/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "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/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/wrap-ansi/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true + }, + "node_modules/write-file-atomic": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", + "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.7" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/ws": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "dev": true, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true + }, + "node_modules/yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/yargs": { + "version": "17.6.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.6.2.tgz", + "integrity": "sha512-1/9UrdHjDZc0eOU0HxOHoS78C69UD3JRMvzlJ7S79S2nTaWRA/whGCTV8o9e/N/1Va9YIV7Q4sOxD8VV4pCWOw==", + "dev": true, + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "engines": { + "node": ">=12" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zone.js": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.12.0.tgz", + "integrity": "sha512-XtC+I5dXU14HrzidAKBNMqneIVUykLEAA1x+v4KVrd6AUPWlwYORF8KgsVqvgdHiKZ4BkxxjvYi/ksEixTPR0Q==", + "dependencies": { + "tslib": "^2.3.0" + } + } + } +} diff --git a/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/package.json b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/package.json new file mode 100644 index 0000000..e0c0a34 --- /dev/null +++ b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/package.json @@ -0,0 +1,70 @@ +{ + "name": "quick-bot-app-frontend", + "version": "0.0.0", + "scripts": { + "ng": "ng", + "start": "ng serve", + "build:prod": "ng build --configuration production --aot --output-hashing=all", + "build:dev": "ng build --configuration development", + "build:staging": "ng build --configuration staging", + "build": "ng build", + "watch": "ng build --watch --configuration development", + "test": "ng test", + "postinstall": "ngcc", + "post-build": "node ./build/post-build.js", + "lint": "gts lint", + "clean": "gts clean", + "compile": "tsc", + "fix": "gts fix", + "prepare": "npm run compile", + "pretest": "npm run compile", + "posttest": "npm run lint" + }, + "private": true, + "dependencies": { + "@angular/animations": "^15.1.0", + "@angular/cdk": "^15.2.9", + "@angular/common": "^15.1.0", + "@angular/compiler": "^15.1.0", + "@angular/core": "^15.1.0", + "@angular/fire": "7.5", + "@angular/flex-layout": "^15.0.0-beta.42", + "@angular/forms": "^15.1.0", + "@angular/material": "^15.2.9", + "@angular/platform-browser": "^15.1.0", + "@angular/platform-browser-dynamic": "^15.1.0", + "@angular/router": "^15.1.0", + "@fortawesome/fontawesome-free": "^6.6.0", + "@ionic/angular": "^7.5.4", + "@ng-idle/core": "^14.0.0", + "clipboard": "^2.0.11", + "ionic-emoji-rating": "^1.0.15", + "marked": "^4.3.0", + "moment": "^2.30.1", + "ngx-markdown": "^15.1.2", + "prismjs": "^1.29.0", + "rxfire": "6.0.3", + "rxjs": "~7.8.0", + "tslib": "^2.3.0", + "uuid": "^9.0.1", + "zone.js": "~0.12.0" + }, + "devDependencies": { + "@angular-devkit/build-angular": "^15.1.3", + "@angular/cli": "~15.1.3", + "@angular/compiler-cli": "^15.1.0", + "@types/jasmine": "~4.3.0", + "@types/marked": "^4.3.0", + "@types/uuid": "^9.0.4", + "firebase-tools": "^11.30.0", + "jasmine-core": "~4.5.0", + "karma": "~6.4.0", + "karma-chrome-launcher": "~3.1.0", + "karma-coverage": "~2.2.0", + "karma-jasmine": "~5.1.0", + "karma-jasmine-html-reporter": "~2.0.0", + "typescript": "~4.9.4", + "gts": "^5.3.1", + "@types/node": "^22.7.5" + } +} diff --git a/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/app/app-routing.module.ts b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/app/app-routing.module.ts new file mode 100644 index 0000000..23660f4 --- /dev/null +++ b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/app/app-routing.module.ts @@ -0,0 +1,41 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {NgModule} from '@angular/core'; +import {RouterModule, Routes} from '@angular/router'; +import {MainComponent} from './components/main/main.component'; +import {LoginComponent} from './components/login/login.component'; +import {AuthGuard} from './services/login/auth.guard'; +import {SearchResultsComponent} from './components/main/search-results/search-results.component'; +import {ManageSearchApplicationComponent} from './components/search-application/manage-search-application/manage-search-application.component'; + +const routes: Routes = [ + {path: '', component: MainComponent, canActivate: [AuthGuard]}, + {path: 'login', component: LoginComponent}, + {path: 'search', component: SearchResultsComponent, canActivate: [AuthGuard]}, + // TODO: Add new AddNewAgentComponent + { + path: 'manage-config', + component: ManageSearchApplicationComponent, + canActivate: [AuthGuard], + }, +]; + +@NgModule({ + imports: [RouterModule.forRoot(routes, {onSameUrlNavigation: 'reload'})], + exports: [RouterModule], +}) +export class AppRoutingModule {} diff --git a/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/app/app.component.html b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/app/app.component.html new file mode 100644 index 0000000..871a1d1 --- /dev/null +++ b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/app/app.component.html @@ -0,0 +1,18 @@ + + + + diff --git a/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/app/app.component.scss b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/app/app.component.scss new file mode 100644 index 0000000..9bf31fd --- /dev/null +++ b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/app/app.component.scss @@ -0,0 +1,98 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +::ng-deep .tour-step { + background: white; +} + +::ng-deep .tour-top{ + margin: -1%; +} + +::ng-deep .tour-left{ + margin-left: -1%; +} + +::ng-deep .tour-top-right{ + margin-top: -1%; + +} + +::ng-deep .tour-bottom-right{ + margin-top: 1%; +} + +::ng-deep .tour-bottom-left{ + margin-top: 1%; +} + +::ng-deep .tour-bottom{ + margin: 1%; +} + +::ng-deep ngx-guided-tour .tour-content{ + font-size: 17px !important; + padding-bottom: 20px !important; +} + +::ng-deep ngx-guided-tour .tour-title{ + font-size: 22px !important; + padding-bottom: 0 !important; +} + +::ng-deep ngx-guided-tour .tour-step .tour-buttons .skip-button{ + font-weight: 500 !important; + font-size: 17px !important; + line-height: 1.7 !important; +} + +::ng-deep ngx-guided-tour .tour-step{ + border-radius: 20px !important; +} + +::ng-deep ngx-guided-tour .tour-step .tour-buttons .back-button{ + font-weight: 500 !important; + font-size: 17px !important; + line-height: 1.7 !important; +} + +::ng-deep ngx-guided-tour .tour-step .tour-buttons .next-button{ + background: #4285F4; + padding-left: 16px !important; + padding-right: 16px !important; + padding-top: 6px !important; + padding-bottom: 6px !important; + font-size: 16px !important; + color: white; + font-weight: 500; + border-radius: 15px !important; +} + +::ng-deep ngx-guided-tour .tour-step.tour-top .tour-block{ + margin-bottom: 0 !important; +} + +::ng-deep ngx-guided-tour .tour-step.tour-bottom-left .tour-block{ + margin-top: 0 !important; +} + +::ng-deep pre{ + background: #485f84 !important; +} + +div { + font-family: "Google Sans", sans-serif !important; +} diff --git a/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/app/app.component.spec.ts b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/app/app.component.spec.ts new file mode 100644 index 0000000..9ff3178 --- /dev/null +++ b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/app/app.component.spec.ts @@ -0,0 +1,49 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {TestBed} from '@angular/core/testing'; +import {RouterTestingModule} from '@angular/router/testing'; +import {AppComponent} from './app.component'; + +describe('AppComponent', () => { + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [RouterTestingModule], + declarations: [AppComponent], + }).compileComponents(); + }); + + it('should create the app', () => { + const fixture = TestBed.createComponent(AppComponent); + const app = fixture.componentInstance; + expect(app).toBeTruthy(); + }); + + it("should have as title 'pac-assist-ui'", () => { + const fixture = TestBed.createComponent(AppComponent); + const app = fixture.componentInstance; + expect(app.title).toEqual('pac-assist-ui'); + }); + + it('should render title', () => { + const fixture = TestBed.createComponent(AppComponent); + fixture.detectChanges(); + const compiled = fixture.nativeElement as HTMLElement; + expect(compiled.querySelector('.content span')?.textContent).toContain( + 'pac-assist-ui app is running!' + ); + }); +}); diff --git a/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/app/app.component.ts b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/app/app.component.ts new file mode 100644 index 0000000..1986eaa --- /dev/null +++ b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/app/app.component.ts @@ -0,0 +1,55 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {Component} from '@angular/core'; +import {Router, NavigationEnd, Event as NavigationEvent} from '@angular/router'; +import {UserService} from './services/user/user.service'; +import {AuthService} from './services/login/auth.service'; + +@Component({ + selector: 'app-root', + templateUrl: './app.component.html', + styleUrls: ['./app.component.scss'], +}) +export class AppComponent { + title = 'pac-assist-ui'; + showHeader = true; + userInfo: any; + + constructor( + private router: Router, + private userService: UserService, + public authService: AuthService + ) { + this.router.events.subscribe((event: NavigationEvent) => { + if (event instanceof NavigationEnd) { + if ( + event.url === '/login' || + event.url === '/login/e2e' || + (event.url.includes('login') && event.url.includes('email')) || + (event.url.includes('login') && event.url.includes('tos')) || + event.url.includes('reset-password') || + event.url.includes('support-ticket') + ) { + this.showHeader = false; + } else { + this.userInfo = this.userService.getUserDetails(); + this.showHeader = true; + } + } + }); + } +} diff --git a/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/app/app.module.ts b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/app/app.module.ts new file mode 100644 index 0000000..02b46e3 --- /dev/null +++ b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/app/app.module.ts @@ -0,0 +1,163 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {NgModule} from '@angular/core'; +import {BrowserModule} from '@angular/platform-browser'; +import {AppRoutingModule} from './app-routing.module'; +import {AppComponent} from './app.component'; +import {HeaderComponent} from './components/header/header.component'; +import {FooterComponent} from './components/footer/footer.component'; +import {MainComponent} from './components/main/main.component'; +import {BrowserAnimationsModule} from '@angular/platform-browser/animations'; +import {MatIconModule} from '@angular/material/icon'; +import {MatAutocompleteModule} from '@angular/material/autocomplete'; +import {MatButtonModule} from '@angular/material/button'; +import {MatToolbarModule} from '@angular/material/toolbar'; +import {HttpClientModule} from '@angular/common/http'; +import {MatInputModule} from '@angular/material/input'; +import {NgFor, PathLocationStrategy} from '@angular/common'; +import {MatSelectModule} from '@angular/material/select'; +import {MatFormFieldModule} from '@angular/material/form-field'; +import {FormsModule, ReactiveFormsModule} from '@angular/forms'; +import {MatGridListModule} from '@angular/material/grid-list'; +import {MatCardModule} from '@angular/material/card'; +import {MatProgressSpinnerModule} from '@angular/material/progress-spinner'; +import {MatTooltipModule} from '@angular/material/tooltip'; +import {LoginComponent} from './components/login/login.component'; +import {initializeApp, provideFirebaseApp} from '@angular/fire/app'; +import { + provideAnalytics, + getAnalytics, + ScreenTrackingService, + UserTrackingService, +} from '@angular/fire/analytics'; +import {environment} from '../environments/environment'; +import {provideAuth, getAuth} from '@angular/fire/auth'; +import {MatMenuModule} from '@angular/material/menu'; +import {MatDividerModule} from '@angular/material/divider'; +import {MatChipsModule} from '@angular/material/chips'; +import {LocationStrategy} from '@angular/common'; +import {MarkdownModule} from 'ngx-markdown'; +import {MatDialogModule} from '@angular/material/dialog'; +import {CdkAccordionModule} from '@angular/cdk/accordion'; +import {MatExpansionModule} from '@angular/material/expansion'; +import {IonicRatingModule} from 'ionic-emoji-rating'; +import {MatListModule} from '@angular/material/list'; +import {MatTabsModule} from '@angular/material/tabs'; +import {MatTableModule} from '@angular/material/table'; +import {MatCheckboxModule} from '@angular/material/checkbox'; +import {MatSlideToggleModule} from '@angular/material/slide-toggle'; +import {MatButtonToggleModule} from '@angular/material/button-toggle'; +import {MatSnackBarModule} from '@angular/material/snack-bar'; +import {MatSortModule} from '@angular/material/sort'; +import {MatPaginatorModule} from '@angular/material/paginator'; +import {MatSidenavModule} from '@angular/material/sidenav'; +import {NgIdleModule} from '@ng-idle/core'; +import {ClipboardModule} from '@angular/cdk/clipboard'; +import {MatProgressBarModule} from '@angular/material/progress-bar'; + +import 'prismjs'; +import 'prismjs/components/prism-typescript.min.js'; +import 'prismjs/plugins/line-numbers/prism-line-numbers.js'; +import 'prismjs/plugins/line-highlight/prism-line-highlight.js'; +import {FlexLayoutModule} from '@angular/flex-layout'; +import {MatSliderModule} from '@angular/material/slider'; +import {MatStepperModule} from '@angular/material/stepper'; +import {ChatInputComponent} from './components/main/chat-input/chat-input.component'; +import {SearchResultsComponent} from './components/main/search-results/search-results.component'; +import {ToastMessageComponent} from './components/toast-message/toast-message.component'; +import {ManageSearchApplicationComponent} from './components/search-application/manage-search-application/manage-search-application.component'; +import {SearchApplicationFormComponent} from './components/search-application/search-application-form/search-application-form.component'; + +@NgModule({ + declarations: [ + AppComponent, + HeaderComponent, + FooterComponent, + MainComponent, + LoginComponent, + ChatInputComponent, + SearchResultsComponent, + ToastMessageComponent, + SearchApplicationFormComponent, + ManageSearchApplicationComponent, + ], + imports: [ + BrowserModule, + AppRoutingModule, + BrowserAnimationsModule, + MatSnackBarModule, + MatPaginatorModule, + MatSortModule, + MatSliderModule, + MatToolbarModule, + MatButtonModule, + MatIconModule, + HttpClientModule, + FormsModule, + MatFormFieldModule, + MatSelectModule, + NgFor, + MatInputModule, + MatGridListModule, + MatCardModule, + MatTooltipModule, + MatProgressSpinnerModule, + MatMenuModule, + MatDividerModule, + MatChipsModule, + ReactiveFormsModule, + MatMenuModule, + MatDialogModule, + CdkAccordionModule, + IonicRatingModule, + MarkdownModule.forRoot(), + MatExpansionModule, + MatListModule, + MatTabsModule, + MatCheckboxModule, + MatTableModule, + MatSlideToggleModule, + MatButtonToggleModule, + MatSidenavModule, + MatAutocompleteModule, + environment.requiredLogin === 'True' + ? [ + provideFirebaseApp(() => initializeApp(environment.firebase)), + provideAuth(() => getAuth()), + ] + : [], + environment.requiredLogin === 'True' + ? [provideAnalytics(() => getAnalytics())] + : [], + FlexLayoutModule, + NgIdleModule.forRoot(), + ClipboardModule, + MatStepperModule, + MatProgressBarModule, + ], + providers: [ + {provide: LocationStrategy, useClass: PathLocationStrategy}, + environment.requiredLogin === 'True' + ? [ + ScreenTrackingService, // Automatically track screen views + UserTrackingService, // Automatically track user interactions + ] + : [], + ], + bootstrap: [AppComponent], +}) +export class AppModule {} diff --git a/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/app/components/footer/footer.component.html b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/app/components/footer/footer.component.html new file mode 100644 index 0000000..0200895 --- /dev/null +++ b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/app/components/footer/footer.component.html @@ -0,0 +1,23 @@ + + +

    + +

    + +

    \ No newline at end of file diff --git a/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/app/components/footer/footer.component.scss b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/app/components/footer/footer.component.scss new file mode 100644 index 0000000..4c78c90 --- /dev/null +++ b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/app/components/footer/footer.component.scss @@ -0,0 +1,71 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +.container { + padding-left: 4rem; + padding-right: 4rem; +} + +.footer { + position: fixed; + bottom: 0; +} + + +.genAI { + background: linear-gradient(86.95deg, #34A853 -65.34%, #FBBC05 -12.75%, #EA4335 25.15%, #1A73E8 103.88%); + -webkit-background-clip: text; + -moz-background-clip: text; + background-clip: text; + color: transparent; +} + +:host ::ng-deep .mat-toolbar.mat-primary { + background-color: transparent; + color: #504747; + box-shadow: none; + justify-content: center; +} + +.line-spacer { + flex: 1 1 auto; +} + +.links-weight { + margin-left: 10px; + margin-right: 10px; + font-size: 14px; + font-weight: 400; + cursor: pointer; +} + +.links-bold { + margin-right: 10px; + color: #5f6368; + font-family: 'Google Sans', sans-serif !important; + .blue{ + color: #4285f4; + } + .red{ + color: #ea4335; + } + .yellow{ + color: #fbbc04; + } + .green{ + color: #34a853; + } +} \ No newline at end of file diff --git a/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/app/components/footer/footer.component.spec.ts b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/app/components/footer/footer.component.spec.ts new file mode 100644 index 0000000..420323c --- /dev/null +++ b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/app/components/footer/footer.component.spec.ts @@ -0,0 +1,38 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {ComponentFixture, TestBed} from '@angular/core/testing'; + +import {FooterComponent} from './footer.component'; + +describe('FooterComponent', () => { + let component: FooterComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [FooterComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(FooterComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/app/components/footer/footer.component.ts b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/app/components/footer/footer.component.ts new file mode 100644 index 0000000..a38bb6c --- /dev/null +++ b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/app/components/footer/footer.component.ts @@ -0,0 +1,24 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {Component} from '@angular/core'; + +@Component({ + selector: 'app-footer', + templateUrl: './footer.component.html', + styleUrls: ['./footer.component.scss'], +}) +export class FooterComponent {} diff --git a/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/app/components/header/header.component.html b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/app/components/header/header.component.html new file mode 100644 index 0000000..1e2f72c --- /dev/null +++ b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/app/components/header/header.component.html @@ -0,0 +1,66 @@ + + + + diff --git a/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/app/components/header/header.component.scss b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/app/components/header/header.component.scss new file mode 100644 index 0000000..f65361e --- /dev/null +++ b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/app/components/header/header.component.scss @@ -0,0 +1,644 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +.container { + margin: auto; +} + +.logo-text { + font-family: Google Sans,Helvetica Neue,sans-serif; + font-style:normal; + font-size: 22px; + line-height: 128.7%; + letter-spacing: normal; + color: #2d2c2cbf; +} + +.gradient-text { + font-family: Google Sans,Helvetica Neue,sans-serif; + font-style:normal; + font-size: 30px; + line-height: 128.7%; + letter-spacing: normal; + background: linear-gradient(89.9deg, rgba(66, 133, 244, 0.95) 21.17%, #A488F5 44.34%, rgba(234, 67, 53, 0.88) 81.26%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; + text-fill-color: transparent; +} + + +.links-pointer { + cursor: pointer; +} + +.logo-icon { + height: 35px; + width: 125px; + cursor: pointer; +} + +// .profile-container { +// margin-left: 1%; +// font-family: 'Google Sans', sans-serif !important; + +// .circle { +// display: flex; +// justify-content: center; +// width: 40px; +// height: 40px; +// border: 5px solid rgb(255, 255, 255); +// border-radius: 60%; +// overflow: hidden; +// } + +// .circle img { +// width: 100%; +// height: 100%; +// object-fit: cover; +// } +// } + +.card-inside-right { + display: flex; + flex-direction: column; + align-items: flex-start; + color: #3E4245; + font-size: 1rem; + font-weight: 400; + font-family: 'Google Sans', sans-serif !important; + border-radius: 5px; + letter-spacing: 0.4px; + + .container-circle { + width: 100%; + height: 100%; + object-fit: cover; + border-radius: 50px; + padding-top: 4%; + padding-bottom: 4%; + } + + .name { + display: flex; + align-items: center; + font-size: 1rem; + color: #3E4245; + font-weight: 500; + padding: 1.3em 0.4em; + flex-direction: column; + gap: 10px; + background: #DDE3EA !important; + width: -webkit-fill-available;; + .sub-title { + + color: #3E4245; + } + + margin: auto; + } + + .company { + display: flex; + align-items: center; + padding: 0.3em 1em; + width: -webkit-fill-available;; + + .logo { + padding-right: 2%; + color: #5F6368; + } + + .text { + min-width: 100px; + + .sub-title { + font-size: 0.85rem; + margin-bottom: 10px; + color: #3E4245; + } + } + + margin-bottom: 10px; + } + + .logout { + background: #DDE3EA; + border-radius: 20px; + font-size: 1rem; + font-weight: 500; + font-family: 'Google Sans', sans-serif !important; + margin-bottom: 1em; + cursor: pointer; + width: -webkit-fill-available;; + padding: 0.3em 1em; + + .logout-container { + display: flex; + align-items: center; + + + .logo { + padding-right: 4%; + color: #5F6368; + } + + .text { + .sub-title { + color: #3E4245; + font-size: 0.85rem; + font-family: 'Google Sans', sans-serif !important; + + } + } + } + } +} + +//------------css of card end-------------------// + +.image { + width: 35px; + height: 35px; +} + + +.fixed-top { + z-index: 1000; + position: relative; +} + +.top-nav { + height: 60px; + background-color: transparent; + // box-shadow: (0px 4px 16px rgba(195, 209, 226, 0.25)); + + + .parent-container { + // width: 1600px; + display: flex; + margin: auto; + justify-content: space-between; + /* border: 1px solid black; */ + height: 100%; + align-items: center; + padding: 0% 1.5%; + + .left-heading { + display: flex; + align-items: center; + cursor: pointer; + } + + .logo-icon { + height: 4.2em; + width: 4em; + cursor: pointer; + background-image: url('../../../assets/images/new-pdc-logo.svg'); + background-size: 443px; + background-repeat: round; + border-radius: 28px; + margin-right:1em; + } + + .user-icon { + height: 50px; + width: 50px; + cursor: pointer; + border-radius: 28px; + + } + + .right-menu-items { + display: flex; + align-items: center; + + .profile-container { + margin-left: 1%; + font-family: 'Google Sans', sans-serif !important; + + .circle { + display: flex; + justify-content: center; + width: 35px; + height: 35px; + border-radius: 60%; + overflow: hidden; + } + + .circle img { + width: 100%; + height: 100%; + object-fit: cover; + } + } + + } + } +} + +.icon-color { + color: #5F6368; + font-size: 24px; +} + +::ng-deep .quick-link.mat-mdc-menu-panel.mat-mdc-menu-panel { + width: 420px !important; + display: flex; + flex-direction: column; + background: #F0F4F9; + + .mat-mdc-menu-content { + padding: 0.5rem; + } + + .ql-container { + width: 100%; + display: flex; + flex-direction: column; + + .ql-child-container{ + display: flex; + flex-direction: row; + text-wrap: nowrap ; + justify-content: space-between; + align-items: flex-end; + } + + a, a:active{ + text-decoration: none; + } + + + .align-link-content{ + display: flex; + flex-direction: row; + align-items: baseline; + gap: 10px; + width: 100% !important; + box-shadow: 0px 4px 4px -6px #b9b8b8 !important; + } + + .no-box-shadow{ + box-shadow: none !important; + } + + .icon-container { + display: flex; + alignment-baseline: after-edge; + + mat-icon { + color: #161A1D !important; + } + } + + } + + + .ql-header{ + margin: 1rem; + margin-left: 1.7rem; + } + + .avatar { + width: 40px; + height: 40px; //<--use the size you choose + border-radius: 100%; + text-align: center; + } + + div.link-label { + color: #161A1D; + font-size: 16px; + font-style: normal; + font-weight: 400; + line-height: 24px; + letter-spacing: 0.4px; + height: 50px; + margin: 0px 5px; + width: 57%; + } + + .avatar span { + line-height: 40px; + font-size: 16px; + } + + .link-button { + margin: 1rem; + height: 41px; + width: 100% !important; + } +} + +.notification-banner-container { + z-index: 999; + position: relative; + width: inherit; + display: flex; + flex-direction: row; + height: 8%; + background: #F6F9FE; + align-content: center; + flex-wrap: wrap; + padding-left: 1%; + gap: 1%; + margin-bottom: 0.2%; + + .amber { + color: #fbc645; + } + + .red { + color: red; + } + + .blue { + color: #4285F4; + } +} + +.red-border { + border-left: 5px solid red; +} + +.amber-border { + border-left: 5px solid #fbc645; +} + +.blue-border { + border-left: 5px solid #4285F4; +} + +.notification-text { + display: flex; + flex-direction: row; + align-items: center; + gap: 15px; + width: 70%; +} + +.notification-buttons-container { + width: 26%; + justify-content: flex-end; + display: flex; + flex-direction: row; + gap: 15px; + + button { + font-family: 'Google Sans', sans-serif !important; + font-style: normal; + font-weight: 500; + line-height: 23px; + font-size: 14px; + } + + .dismiss-button { + letter-spacing: 0.4px; + color: #4285F4; + } + + .learn-more-button { + color: #fff !important; + background: #4285F4 !important; + border-radius: 30px; + letter-spacing: unset; + box-shadow: none; + } +} + +::ng-deep .profile-menu.mat-mdc-menu-panel.mat-mdc-menu-panel { + max-width: 320px !important; + width: 300px !important; +} + +::ng-deep .profile-menu .mat-mdc-menu-content { + padding: 0% !important; +} + +.action-item-container { + width: -webkit-fill-available; + padding-inline: 5%; + padding-top: 2%; + padding-bottom: 2%; +} + +.logout-button { + background: white !important; + border: 1px solid #4285F4 !important; +} + +.logout-button-text { + color: #4285F4 !important; +} + +.app-version-chip { + font-size: 9px; + border-radius: -2px; + padding-inline: 8px; + line-height: 16px; + border-radius: 11px; + margin-left: 8px; + color: #474c55; + background: linear-gradient(92.1deg, #bdd1f3 19.16%, rgba(199, 183, 245, 0.9) 79%, rgba(235, 165, 158, 0.9) 135.24%), linear-gradient(272.04deg, rgba(158, 188, 237, 0.7) 14.93%, rgba(158, 188, 237, 0) 104.51%) !important; +} + +.bypassLoginLogo { + background: aliceblue; + color: cornflowerblue; + align-items: center; + cursor: pointer; +} + +.bypassMenuContainer { + display: flex; + justify-content: center; + background: aliceblue; + width: 42% !important; + height: 40% !important; + border-radius: 65px !important; +} + +.bypassMenuLogo { + font-size: 6rem; + height: 6rem; + width: 6rem; +} + + +.chat-footer { + min-height: 60px; + display: flex; + border: 1px solid #DDE6F6; + box-shadow: 0px 4px 18px rgba(195, 209, 226, 0.25); + border-radius: 30px; + width: 66%; + margin-top: 25px; + // box-shadow: 5px 4px 24px rgba(66, 133, 244, 0.15); + + .chat-textarea { + flex-grow: 1; + display: flex; + align-items: center; + padding-left: 2%; + + input { + width: 100%; + height: 45px; + border: 0px; + border-radius: 4px; + resize: none; + outline: none; + padding: 14px; + font-family: 'Google Sans', sans-serif !important; + box-sizing: border-box; + -ms-overflow-style: none; + scrollbar-width: none; + font-size: 0.95rem; + line-height: 1.1rem; + color: #534646; + } + + input::-webkit-scrollbar { + display: none; + } + } + + .micIcon { + display: flex; + align-items: center; + color: #1F1F1F; + margin-top: 2px; + padding-right: 1%; + + img { + max-width: 40%; + cursor: pointer; + } + } +} + +.blue-dot { + /* Ellipse 1226 */ + content: ''; + width: 8px; + height: 8px; + left: 1503px; + top: 445px; + display: flow-root; + border-radius: 15px; + + /* Blue */ + background: #4285F4; + animation: blue-transform 2s infinite; + +} + +.green-dot { + content: ''; + width:8px; + height: 8px; + left: 1522px; + top: 445px; + display: flow-root; + border-radius: 15px; + + /* Green */ + background: #0F9D58; + animation: green-transform 2s infinite; +} + +.yellow-dot { + content: ''; + width: 8px; + height: 8px; + left: 1541px; + top: 445px; + display: flow-root; + border-radius: 15px; + + /* Yellow */ + background: #F4B400; + animation: yellow-transform 2s infinite; + +} + +.recording-container { + display: flex; + flex-direction: row; + gap: 10px; + cursor: pointer; + align-items: baseline; +} + + +@keyframes blue-transform { + 0% { + transform: translateY(-14px) + } + + 33% { + transform: translateY(0) + } + + 66% { + transform: translateY(0) + } + + 100% { + transform: translateY(-14px) + } + +} + +@keyframes green-transform { + 0% { + transform: translateY(0) + } + + 33% { + transform: translateY(-14px) + } + + 66% { + transform: translateY(0) + } + + 100% { + transform: translateY(0) + } + +} + +@keyframes yellow-transform { + 0% { + transform: translateY(0) + } + + 33% { + transform: translateY(0) + } + + 66% { + transform: translateY(-14px) + } + + 100% { + transform: translateY(0) + } + +} + diff --git a/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/app/components/header/header.component.spec.ts b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/app/components/header/header.component.spec.ts new file mode 100644 index 0000000..90d999b --- /dev/null +++ b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/app/components/header/header.component.spec.ts @@ -0,0 +1,38 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {ComponentFixture, TestBed} from '@angular/core/testing'; + +import {HeaderComponent} from './header.component'; + +describe('HeaderComponent', () => { + let component: HeaderComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [HeaderComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(HeaderComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/app/components/header/header.component.ts b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/app/components/header/header.component.ts new file mode 100644 index 0000000..55718a4 --- /dev/null +++ b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/app/components/header/header.component.ts @@ -0,0 +1,76 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {Component, EventEmitter, Output} from '@angular/core'; +import {DomSanitizer} from '@angular/platform-browser'; +import {MatIconRegistry} from '@angular/material/icon'; +import {UserService} from 'src/app/services/user/user.service'; +import {AuthService} from '../../services/login/auth.service'; +import {environment} from 'src/environments/environment'; +import {Router} from '@angular/router'; + +const GOOGLE_CLOUD_ICON = ` + + + + + + + + +`; + +@Component({ + selector: 'app-header', + templateUrl: './header.component.html', + styleUrls: ['./header.component.scss'], +}) +export class HeaderComponent { + headerTitle: string = environment.chatbotName; + requiredLogin: string = environment.requiredLogin; + + showLoading = false; + @Output() emitSearch: EventEmitter = new EventEmitter(); + + constructor( + iconRegistry: MatIconRegistry, + sanitizer: DomSanitizer, + public _UserService: UserService, + public authService: AuthService, + private router: Router + ) { + iconRegistry.addSvgIconLiteral( + 'google-cloud-icon', + sanitizer.bypassSecurityTrustHtml(GOOGLE_CLOUD_ICON) + ); + } + + isSearchRoute(): boolean { + return this.router.url.startsWith('/search'); + } + + searchTerm(term: string) { + this.emitSearch.emit(term); + } + + goToManageConfig() { + this.router.navigateByUrl('/manage-config'); + } + + logout() { + this.authService.logout(); + } +} diff --git a/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/app/components/login/login.component.html b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/app/components/login/login.component.html new file mode 100644 index 0000000..e26608e --- /dev/null +++ b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/app/components/login/login.component.html @@ -0,0 +1,48 @@ + + + diff --git a/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/app/components/login/login.component.scss b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/app/components/login/login.component.scss new file mode 100644 index 0000000..54a6df8 --- /dev/null +++ b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/app/components/login/login.component.scss @@ -0,0 +1,545 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +.login-container { + display: flex; + min-height: 100vh; + max-height: 100%; + .spinner{ + display: flex; + justify-content: center; + margin: 1em; + } + + .linear-text { + background: linear-gradient(89.9deg, rgba(66, 133, 244, 0.95) 21.17%, #A488F5 44.34%, rgba(234, 67, 53, 0.88) 81.26%); + -webkit-background-clip: text; + -moz-background-clip: text; + background-clip: text; + color: transparent; + } + + .heading { + font-family: 'Google Sans', sans-serif !important; + font-size: 32px; + font-style: normal; + font-weight: 500; + line-height: 150.7%; + color: rgba(45, 44, 44, 0.7490196078); + } + + .left-container { + flex: 0 1 55%; + min-height: 100%; + max-width: 60%; + align-items: center; + background: #fafcfe; + padding: .5em; + overflow: auto; + display: flex; + justify-content: center; + + .login-app-text-sub { + color: #161A1D; + font-size: 1rem; + font-style: normal; + font-weight: 400; + line-height: 150.7%; + } + + .login-app-text { + // margin-bottom: 2rem; + line-height: 1.5em; + } + + + + .expand-button { + --mdc-fab-container-color: #4285F41A; + --mdc-fab-icon-color: #000000; + border: 1px solid #4285F4; + --mat-mdc-fab-color: #4285F4; + height: 2.2rem; + font-size: 0.8rem; + font-weight: 400; + box-shadow: none; + } + + .expand-less-button { + --mdc-fab-container-color: rgba(92, 95, 97, 0.10); + --mdc-fab-icon-color: #000000; + border: 1px solid rgba(92, 95, 97, 0.50); + --mat-mdc-fab-color: rgba(92, 95, 97, 0.50); + margin-bottom: 1em; + height: 2.2rem; + font-size: 0.8rem; + font-weight: 400; + box-shadow: none + } + } + + .right-container { + flex: 0 1 45%; + min-height: 100%; + max-width: 80%; + align-items: flex-start; + display: flex; + justify-content: center; + overflow-y: auto; + scrollbar-width: thin; + + .login-card { + flex-direction: column; + display: flex; + max-width: 100%; + padding: 3%; + margin: auto; + + .logo-icon { + height: 145px; + width: 100%; + // margin-bottom: 2rem; + } + + .login-button { + background: #4285F4; + box-sizing: border-box; + border: 1px solid #e0e0e0; + color: #fff; + position: relative; + user-select: none; + cursor: pointer; + outline: none; + -webkit-tap-highlight-color: transparent; + display: inline-block; + font-family: 'Google Sans', sans-serif !important; + font-size: 14px; + font-weight: 500; + white-space: nowrap; + text-decoration: none; + vertical-align: baseline; + text-align: center; + margin: 0; + min-width: 100px; + line-height: 36px; + padding: 0 16px; + border-radius: 4px; + overflow: visible; + transform: translate3d(0, 0, 0); + transition: background 400ms cubic-bezier(0.25, 0.8, 0.25, 1), box-shadow 280ms cubic-bezier(0.4, 0, 0.2, 1); + } + + .register-text { + display: flex; + flex-direction: column; + align-items: flex-start; + margin: 1em 0em; + font-weight: 400; + color: #161A1D + } + + .register-button { + background: white; + box-sizing: border-box; + border: 1px solid #4285F4; + color: #4285F4; + position: relative; + user-select: none; + cursor: pointer; + outline: none; + -webkit-tap-highlight-color: transparent; + display: inline-block; + font-family: 'Google Sans', sans-serif !important; + font-size: 14px; + font-weight: 500; + white-space: nowrap; + text-decoration: none; + vertical-align: baseline; + text-align: center; + margin: 0; + min-width: 64px; + line-height: 36px; + padding: 0 16px; + border-radius: 4px; + overflow: visible; + transform: translate3d(0, 0, 0); + transition: background 400ms cubic-bezier(0.25, 0.8, 0.25, 1), box-shadow 280ms cubic-bezier(0.4, 0, 0.2, 1); + } + } + } + + + .slide { + width: 100%; + min-height: 15rem; + border-radius: 10px; + background-size: cover; + background-position: center; + font-size: 1rem; + font-style: normal; + font-weight: 400; + line-height: 200.7%; + padding-left: 0px; + + .feature-heading { + font-weight: 700; + } + } + + .slider { + position: relative; + height: 100%; + } + + .dotsContainer { + display: flex; + justify-content: center; + align-items: center; + margin-top: 2em; + } + + .dot { + margin: 0 3px; + cursor: pointer; + font-size: 18px; + color: #D0D3D7; + } + + .active { + color: #1A73E8; + background-color: #D0D3D7; + padding: 2px; + border-radius: 50%; + font-size: 12px; + } + + .material-symbols-outlined { + font-variation-settings: + 'FILL' 1, + 'wght' 400, + 'GRAD' 0, + 'opsz' 18 + } + + h6 { + position: relative; + z-index: 1; + overflow: hidden; + text-align: center; + font-size: 1.2em; + font-style: normal; + font-weight: 500; + line-height: 200.7%; + font-family: 'Google Sans', sans-serif !important; + } + + h6:before, + h6:after { + position: absolute; + top: 51%; + overflow: hidden; + width: 50%; + height: 1px; + content: '\a0'; + background: linear-gradient(86.95deg, #34A853 -65.34%, #FBBC05 -12.75%, #EA4335 25.15%, #1A73E8 103.88%); + } + + h6:before { + margin-left: -50%; + text-align: right; + } + +} + +.login-buttons-container { + padding-inline: 4%; + gap: 12px; + display: flex; + flex-direction: column; + background: #F7F9FF; + padding-top: 4%; + padding-bottom: 4%; + border-radius: 5px; +} + +.back-button { + color: #5C5F61 !important; + font-weight: 400 !important; +} + +.help-container { + padding-top: 2%; + text-align: center; +} + +.blue { + color: #4285F4; + cursor: pointer; +} + +mat-form-field { + width: 100%; +} + +.user-name-container{ + display: flex; + flex-direction: row; + gap: 3%; +} + +.error-message { + font-weight: 500; + font-size: 0.8rem; + letter-spacing: -0.005rem; + color: #ed0c0c; + + .mat-mdc-form-field-subscript-wrapper { + margin-top: -10px; + } +} + +.forgot-password-container { + display: flex; + flex-direction: column; + text-align: center; + padding: 4%; +} + +.forgot-password-icon-container { + width: 100%; + height: 100%; + + mat-icon{ + font-size: 105px; + height: 100%; + width: 100%; + padding: 1%; + padding-top: 7%; + color: #3467bb; + } +} + +.forgot-password-heading-container { + font-size: 29px; + font-weight: 600; + font-family: 'Google Sans', sans-serif !important; + line-height: 2.5em; +} + +.forgot-password-subtext-container{ + display: flex; + flex-direction: column; + text-align: center; + font-size: 19px; + gap: 10px; + line-height: 1.5em; + + mat-icon{ + color: #4385f4; + } +} + +.logo-icon { + height: 170px; + width: 100%; +} + +.login-button { + width: 100%; + background: #4285F4; + box-sizing: border-box; + border: 1px solid #e0e0e0; + color: #fff; + position: relative; + user-select: none; + cursor: pointer; + outline: none; + -webkit-tap-highlight-color: transparent; + display: inline-block; + font-family: 'Google Sans', sans-serif !important; + font-size: 14px; + font-weight: 500; + white-space: nowrap; + text-decoration: none; + vertical-align: baseline; + text-align: center; + margin: 0; + min-width: 64px; + line-height: 44px; + padding: 0 16px; + border-radius: 4px; + overflow: visible; + transform: translate3d(0, 0, 0); + transition: background 400ms cubic-bezier(0.25, 0.8, 0.25, 1), box-shadow 280ms cubic-bezier(0.4, 0, 0.2, 1); +} + +.register-text { + display: flex; + flex-direction: column; + align-items: flex-start; + margin-bottom: 0.3em; + font-weight: 400; + color: #5C5F61 +} + +.register-button { + width: 100%; + background: white; + box-sizing: border-box; + border: 1px solid #4285F4; + color: #4285F4; + position: relative; + user-select: none; + cursor: pointer; + outline: none; + -webkit-tap-highlight-color: transparent; + display: inline-block; + font-family: 'Google Sans', sans-serif !important; + font-size: 14px; + font-weight: 500; + white-space: nowrap; + text-decoration: none; + vertical-align: baseline; + text-align: center; + margin: 0; + min-width: 64px; + line-height: 44px; + padding: 0 16px; + border-radius: 4px; + overflow: visible; + transform: translate3d(0, 0, 0); + transition: background 400ms cubic-bezier(0.25, 0.8, 0.25, 1), box-shadow 280ms cubic-bezier(0.4, 0, 0.2, 1); +} + +.send-email-button { + background: #40bd40 !important; + font-weight: 600 !important; + font-size: 16px !important; +} + +.close-button { + float: right !important; +} + +.forgot-password-link-container { + width: 100%; + text-align: end; + display: flex; + flex-direction: column; + padding-bottom: 4%; + z-index: 100; + position: relative; +} + +.button-css { + display: flex; + justify-content: center; + + .ok-button{ + justify-content: center; + display: flex; + width: 25%; + background: #4285F4; + box-sizing: border-box; + border: 1px solid #e0e0e0; + color: #fff; + position: relative; + user-select: none; + cursor: pointer; + outline: none; + -webkit-tap-highlight-color: transparent; + font-family: 'Google Sans', sans-serif !important; + font-size: 14px; + font-weight: 500; + white-space: nowrap; + text-decoration: none; + vertical-align: baseline; + text-align: center; + margin: 0; + min-width: 64px; + line-height: 44px; + padding: 0 16px; + border-radius: 4px; + overflow: visible; + transform: translate3d(0, 0, 0); + transition: background 400ms cubic-bezier(0.25, 0.8, 0.25, 1), box-shadow 280ms cubic-bezier(0.4, 0, 0.2, 1); + } +} + +.icon-css { + font-size: 40px; + color: #f3f30aed +} + +.text-css { + font-size: 15px; + line-height: 1.45em; + font-family: "Google Sans", sans-serif !important; + color: #645a5a; +} + +.logo-container { + display: flex; + flex-direction: row; + justify-content: center; + text-align: center; + mat-icon{ + height: 20% !important; + width: 100% !important; + font-size: 70px !important; + color: #4285F4 !important; + } +} + +.heading-span { + display: flex; + flex-direction: row; + gap: 8px; + align-items: center; + padding: 2%; + width: 100%; + justify-content: center; + font-size: 18px; + font-weight: 700; + padding-left: 15%; + color: #4285F4 !important; +} + +.heading-container { + display: flex; + flex-direction: row; + justify-content: center; + background: aliceblue; +} + +.sub-parent-container { + display: flex; + flex-direction: column; + text-align: center; + gap: 15px; + font-style: normal; + line-height: normal; + font-family: 'Google Sans', sans-serif !important; +} + +.info { + padding-bottom: 10px !important; + small { + color: #a8a8b5; + + } +} + diff --git a/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/app/components/login/login.component.spec.ts b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/app/components/login/login.component.spec.ts new file mode 100644 index 0000000..cb6b0f3 --- /dev/null +++ b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/app/components/login/login.component.spec.ts @@ -0,0 +1,38 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {ComponentFixture, TestBed} from '@angular/core/testing'; + +import {LoginComponent} from './login.component'; + +describe('LoginComponent', () => { + let component: LoginComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [LoginComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(LoginComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/app/components/login/login.component.ts b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/app/components/login/login.component.ts new file mode 100644 index 0000000..37b3e71 --- /dev/null +++ b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/app/components/login/login.component.ts @@ -0,0 +1,92 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {Component, NgZone, inject} from '@angular/core'; +import {Auth, signInWithPopup, GoogleAuthProvider} from '@angular/fire/auth'; +import {Router} from '@angular/router'; +import {AuthService} from 'src/app/services/login/auth.service'; +import {MatSnackBar} from '@angular/material/snack-bar'; +import {environment} from 'src/environments/environment'; +import {ToastMessageComponent} from '../toast-message/toast-message.component'; + +const HOME_ROUTE = '/'; + +interface LooseObject { + [key: string]: any; +} + +@Component({ + selector: 'app-login', + templateUrl: './login.component.html', + styleUrls: ['./login.component.scss'], +}) +export class LoginComponent { + private readonly auth: Auth = inject(Auth); + private readonly provider: GoogleAuthProvider = new GoogleAuthProvider(); + loader = false; + chatbotName: string = environment.chatbotName; + + constructor( + private authService: AuthService, + private router: Router, + public ngZone: NgZone, + private _snackBar: MatSnackBar + ) { + this.provider.setCustomParameters({ + prompt: 'select_account', + }); + } + + loginWithGoogle() { + this.loader = true; + signInWithPopup(this.auth, this.provider) + .then((result: any) => { + const user = result.user.toJSON(); + this.ngZone.run(() => { + this.authService.saveUserSession(user.stsTokenManager.accessToken); + this.redirect(user); + }); + }) + .catch(error => { + this.loader = false; + if (error.message !== 'Firebase: Error (auth/popup-closed-by-user).') { + this._snackBar.openFromComponent(ToastMessageComponent, { + panelClass: ['red-toast'], + verticalPosition: 'top', + horizontalPosition: 'right', + duration: 5000, + data: { + text: 'Error with SignIn. Please try again later !!!', + icon: 'cross-in-circle-white', + }, + }); + } + console.error(`Error: ${error}`); + }); + } + + redirect(user: any) { + const userDetails: LooseObject = {}; + userDetails['name'] = user.displayName; + userDetails['email'] = user.email; + userDetails['photoURL'] = user.photoURL; + userDetails['domain'] = user.domain; + (userDetails['uid'] = user.uid), + localStorage.setItem('USER_DETAILS', JSON.stringify(userDetails)); + this.loader = false; + this.router.navigate([HOME_ROUTE]); + } +} diff --git a/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/app/components/main/chat-input/chat-input.component.html b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/app/components/main/chat-input/chat-input.component.html new file mode 100644 index 0000000..7c573a8 --- /dev/null +++ b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/app/components/main/chat-input/chat-input.component.html @@ -0,0 +1,43 @@ + + + diff --git a/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/app/components/main/chat-input/chat-input.component.scss b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/app/components/main/chat-input/chat-input.component.scss new file mode 100644 index 0000000..55b94b2 --- /dev/null +++ b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/app/components/main/chat-input/chat-input.component.scss @@ -0,0 +1,120 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +.chat-footer { + margin-top: 1rem; + min-height: 60px; + display: flex; + border: 1px solid #dde6f6; + box-shadow: 0px 4px 18px rgba(195, 209, 226, 0.25); + border-radius: 30px; + width: 100%; + min-width: 720px; + // box-shadow: 5px 4px 24px rgba(66, 133, 244, 0.15); + + .chat-textarea { + flex-grow: 1; + display: flex; + align-items: center; + padding-left: 2%; + + input { + width: 100%; + height: 45px; + border: 0px; + border-radius: 4px; + resize: none; + outline: none; + padding: 14px; + font-family: "Google Sans", sans-serif !important; + box-sizing: border-box; + -ms-overflow-style: none; + scrollbar-width: none; + font-size: 0.95rem; + line-height: 1.1rem; + color: #534646; + } + + input::-webkit-scrollbar { + display: none; + } + } + + .micIcon { + display: flex; + align-items: center; + color: #1f1f1f; + margin-top: 2px; + padding-right: 1%; + + img { + max-width: 40%; + cursor: pointer; + } + } +} + +.recording-container { + display: flex; + flex-direction: row; + gap: 10px; + cursor: pointer; + align-items: baseline; +} + +.blue-dot { + /* Ellipse 1226 */ + content: ''; + width: 8px; + height: 8px; + left: 1503px; + top: 445px; + display: flow-root; + border-radius: 15px; + + /* Blue */ + background: #4285F4; + animation: blue-transform 2s infinite; + +} + +.green-dot { + content: ''; + width:8px; + height: 8px; + left: 1522px; + top: 445px; + display: flow-root; + border-radius: 15px; + + /* Green */ + background: #0F9D58; + animation: green-transform 2s infinite; +} + +.yellow-dot { + content: ''; + width: 8px; + height: 8px; + left: 1541px; + top: 445px; + display: flow-root; + border-radius: 15px; + + /* Yellow */ + background: #F4B400; + animation: yellow-transform 2s infinite; +} diff --git a/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/app/components/main/chat-input/chat-input.component.spec.ts b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/app/components/main/chat-input/chat-input.component.spec.ts new file mode 100644 index 0000000..158ea34 --- /dev/null +++ b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/app/components/main/chat-input/chat-input.component.spec.ts @@ -0,0 +1,38 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {ComponentFixture, TestBed} from '@angular/core/testing'; + +import {ChatInputComponent} from './chat-input.component'; + +describe('ChatInputComponent', () => { + let component: ChatInputComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ChatInputComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(ChatInputComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/app/components/main/chat-input/chat-input.component.ts b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/app/components/main/chat-input/chat-input.component.ts new file mode 100644 index 0000000..0d7cf4b --- /dev/null +++ b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/app/components/main/chat-input/chat-input.component.ts @@ -0,0 +1,88 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {Component, EventEmitter, Output} from '@angular/core'; +import {ActivatedRoute} from '@angular/router'; +import {SpeechToTextService} from 'src/app/services/speech-to-text'; + +@Component({ + selector: 'app-chat-input', + templateUrl: './chat-input.component.html', + styleUrls: ['./chat-input.component.scss'], +}) +export class ChatInputComponent { + isRecording = false; + transcribedText = ''; + mediaRecorder: MediaRecorder | undefined; + audioChunks: Blob[] = []; + + term = ''; + @Output() emitSearch: EventEmitter = new EventEmitter(); + + constructor( + private speechToTextService: SpeechToTextService, + private route: ActivatedRoute + ) { + const query = this.route.snapshot.queryParamMap.get('q'); + if (query) { + this.term = query; + } + } + + ngOnInit() { + navigator.mediaDevices + .getUserMedia({audio: true}) + .then(stream => this.setupMediaRecorder(stream)) + .catch(err => console.error(err)); + } + + searchTerm() { + this.emitSearch.emit(this.term); + } + + setupMediaRecorder(stream: MediaStream) { + this.mediaRecorder = new MediaRecorder(stream); + this.mediaRecorder.ondataavailable = event => + this.audioChunks.push(event.data); + this.mediaRecorder.onstop = () => this.sendAudioToGCP(); + } + + startRecording() { + this.isRecording = true; + this.audioChunks = []; + if (this.mediaRecorder) this.mediaRecorder.start(); + } + + stopRecording() { + this.isRecording = false; + if (this.mediaRecorder) this.mediaRecorder.stop(); + } + + async sendAudioToGCP() { + const audioBlob = new Blob(this.audioChunks); + // console.log(audioBlob); + (await this.speechToTextService.transcribeAudio(audioBlob)).subscribe( + (response: any) => { + // console.log(response) + this.term = response[0]; + this.searchTerm(); + }, + (error: any) => { + // Handle errors + } + ); + } +} diff --git a/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/app/components/main/main.component.html b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/app/components/main/main.component.html new file mode 100644 index 0000000..57c1ee9 --- /dev/null +++ b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/app/components/main/main.component.html @@ -0,0 +1,32 @@ + + + +
    +
    +
    +
    +
    + Hey, {{savedUser.name ? savedUser.name!.split(" ")[0]+'!' : ''}} +
    +
    How can I help you today?
    +
    +
    +
    + + diff --git a/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/app/components/main/main.component.scss b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/app/components/main/main.component.scss new file mode 100644 index 0000000..24ffad2 --- /dev/null +++ b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/app/components/main/main.component.scss @@ -0,0 +1,995 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@import url("https://fonts.googleapis.com/icon?family=Material+Icons|Material+Icons+Outlined"); + +:host ::ng-deep mat-form-feild .mat-form-field-appearance-outline .mat-form-field-outline-start { + color: red; + border-radius: 50px !important; + font-size: 10px !important; +} + +:host ::ng-deep .mdc-text-field--outlined .mdc-notched-outline .mdc-notched-outline__leading { + border-radius: 28px 0 0 28px !important; + min-width: 28px !important; + border-color: #babfc4 !important; +} + +:host ::ng-deep .mdc-text-field--outlined .mdc-notched-outline .mdc-notched-outline__trailing { + border-radius: 0 28px 28px 0 !important; + border-color: #babfc4 !important; +} + +:host ::ng-deep .mat-form-field-outline-end { + border-radius: 0 28px 28px 0 !important; +} + +:host ::ng-deep .mat-mdc-card { + box-shadow: 0px 2px 1px -1px rgb(217 220 224), 0px 1px 1px 0px rgba(0, 0, 0, 0.14), 0px 1px 3px 0px rgb(229 232 234) !important; +} + +:host ::ng-deep .mat-mdc-standard-chip.mat-primary.mat-mdc-chip-selected, +.mat-mdc-standard-chip.mat-primary.mat-mdc-chip-highlighted { + --mdc-chip-elevated-container-color: #EDF3FE; + --mdc-chip-elevated-disabled-container-color: #e0e0e0; + --mdc-chip-label-text-color: #4285F4; + ; + --mdc-chip-disabled-label-text-color: #4285F4; + ; + --mdc-chip-with-icon-icon-color: #4285F4; + ; + --mdc-chip-with-icon-disabled-icon-color: #4285F4; + ; + --mdc-chip-with-trailing-icon-disabled-trailing-icon-color: #4285F4; + ; + --mdc-chip-with-trailing-icon-trailing-icon-color: #4285F4; + ; + --mdc-chip-with-icon-selected-icon-color: #4285F4; + ; +} + +.parent-container { + // background-position: bottom; + // background-image: url("../../../assets/images/footer.png"); + // background-repeat: no-repeat; + // background-size: 30%; + // height: calc(100% - 60px); + // overflow: auto; + display: flex; + max-height: calc(100vh - 70px); + height: 100%; + font-family: 'Google Sans', sans-serif !important; + overflow-y: auto; + flex-direction: column; + + .img-globe { + // position:absolute; + width: 66%; + } +} + +.wrapping-container { + margin-top: 6%; + width: 100%; + padding-bottom: 1.5%; +} + +.main-div { + display: flex; + justify-content: center; + align-items: center; + gap: 15px; + font-family: 'Google Sans', sans-serif !important; +} + +.text-control { + width: 32%; +} + +h1 { + line-height: 50px; + width: 34%; +} + +h2 { + font-family: 'Google Sans', sans-serif !important; +} + +.header { + -webkit-background-clip: text; + -webkit-text-fill-color: #48b975; + background-clip: text; + font-size: 3rem; + text-align: center; + font-weight: bold; + font-weight: 700; + -webkit-font-smoothing: antialiased; + height: 100%; + font-family: 'Google Sans', sans-serif; +} + +.send-button { + color: #babfc4; +} + +.logo-icon { + height: 120px; + width: 400px; + margin-bottom: 90px; + margin-left: -20px; +} + +.user-info { + display: flex; + justify-content: center; +} + +.user-info-column { + display: column; +} + +.card-outer-div { + .card-inside-left { + .circle { + width: 100px; + height: 100px; + border: 5px solid rgb(255, 255, 255); + border-radius: 50%; + overflow: hidden; + box-shadow: 0px 2px 1px -1px rgba(0, 0, 0, 0.2), 0px 1px 1px 0px rgba(0, 0, 0, 0.14), 0px 1px 3px 0px rgba(0, 0, 0, 0.12); + } + + .circle img { + width: 100%; + height: 100%; + object-fit: cover; + } + } + + .card-inside-right { + width: 80%; + display: flex; + flex-direction: column; + align-items: center; + color: #747474; + font-size: 0.8rem; + font-weight: 500; + + .name { + .title { + font: 1rem; + } + + .sub-title { + font-size: 0.9rem; + margin-left: 2%; + } + } + + .company { + display: flex; + align-items: center; + + .logo { + padding-right: 1%; + } + + .text { + width: 100%; + + .sub-title { + margin-left: 2%; + } + } + } + } +} + +.mat-mdc-standard-chip { + --mdc-chip-label-text-color: #4285F4; + --mdc-chip-elevated-container-color: #fff; + border: 1px solid #4285F4 !important; + font-family: "Google Sans", sans-serif !important; + font-size: 15px !important; + font-weight: 500 !important; + padding-left: 12px !important; + padding-right: 12px !important; +} + +.sub-heading { + font-family: 'Google Sans', sans-serif; + width: 66%; + color: #161A1D; + text-align: center; + font-size: 30px; + line-height: 40px; + margin-top: -20px; + text-align: left; + z-index: 999; +} + +.genAI { + //background-image: linear-gradient(to left, #4285F4, #DB4437, #F4B400, #0F9D58); + // background-image: linear-gradient(to left, #34A853, #FBBC05, #EA4335, #1A73E8); + background: linear-gradient(86.95deg, #34A853 -65.34%, #FBBC05 -12.75%, #EA4335 25.15%, #1A73E8 103.88%); + -webkit-background-clip: text; + -moz-background-clip: text; + background-clip: text; + color: transparent; +} + +.margin-div { + margin-top: -20px; +} + +.chat-footer-outer { + display: flex; + align-items: center; + width: 100%; + justify-content: center; + padding-bottom: 4.7%; + padding-top: 1.7%; + + .chat-footer { + min-height: 60px; + display: flex; + border: 1px solid #DDE6F6; + box-shadow: 0px 4px 18px rgba(195, 209, 226, 0.25); + border-radius: 30px; + width: 66%; + // box-shadow: 5px 4px 24px rgba(66, 133, 244, 0.15); + + .chat-textarea { + flex-grow: 1; + display: flex; + align-items: center; + padding-left: 2%; + + input { + width: 100%; + height: 45px; + border: 0px; + border-radius: 4px; + resize: none; + outline: none; + padding: 14px; + font-family: 'Google Sans', sans-serif !important; + box-sizing: border-box; + -ms-overflow-style: none; + scrollbar-width: none; + font-size: 0.95rem; + line-height: 1.1rem; + color: #534646; + } + + input::-webkit-scrollbar { + display: none; + } + } + + .micIcon { + display: flex; + align-items: center; + color: #1F1F1F; + margin-top: 2px; + padding-right: 1%; + + img { + max-width: 40%; + cursor: pointer; + } + } + } + +} + +.text-gnosis { + color: var(--White, #FFF); + font-size: 16px; + font-style: normal; + font-weight: 400; + line-height: 24px; + /* 150% */ + letter-spacing: 0.4px; +} + +.chip-cancel-button { + color: #002dff; +} + +.categories-div { + + .mat-mdc-standard-chip { + --mdc-chip-label-text-color: #0F9D58; + --mdc-chip-elevated-container-color: #fff; + border: 1px solid #0F9D58 !important; + font-family: 'Google Sans', sans-serif !important; + // font-weight: 600; + } +} + +.intent-chip { + + .mat-mdc-standard-chip { + --mdc-chip-label-text-color: #4285F4; + --mdc-chip-elevated-container-color: #fff; + border: 1px solid #4285F4 !important; + font-family: 'Google Sans', sans-serif !important; + // font-weight: 600; + } + + .chip-cancel-button { + color: #002dff; + } +} + +.category-chip { + .mat-mdc-standard-chip { + --mdc-chip-label-text-color: #0F9D58; + --mdc-chip-elevated-container-color: #fff; + border: 1px solid #0F9D58 !important; + font-family: 'Google Sans', sans-serif !important; + // font-weight: 600; + } + + .chip-cancel-button { + color: #007d06; + } +} + +.questions-div { + flex-direction: column; + margin-top: 2rem; + margin-bottom: 5rem; + + .intent-card { + padding: 0px; + } + + .intent-heading { + color: #787978; + font-size: 16px; + font-style: normal; + font-weight: 400; + line-height: normal; + font-family: 'Google Sans', sans-serif !important; + width: 66%; + + } + + .intent-container { + border-radius: 8px; + background-color: rgba(246, 249, 254, 1); + width: 66%; + box-shadow: none !important; + flex-shrink: 0; + + .selected-question { + + // margin: 0.5rem 0rem; + &:hover { + background-color: rgba(66, 133, 244, 0.10); + } + } + + .question-item { + display: flex; + justify-content: space-between; + font-family: 'Google Sans', sans-serif !important; + color: #787978; + font-size: 1em; + line-height: 24px; + } + + .question-link { + overflow: hidden; + text-overflow: ellipsis; + } + } +} + +.intent-chip-textarea ::ng-deep .mdc-evolution-chip-set__chips { + align-items: center !important; +} + +.float-button { + position: fixed; + right: 3em; + bottom: 6em; + --mdc-fab-container-color: #4285F4; +} + +.name-container { + font-size: 3.5rem; + font-weight: 400; + line-height: 4rem; + font-family: Google Sans, Helvetica Neue, sans-serif; + letter-spacing: -.03em; + background: linear-gradient(89.9deg, rgba(66, 133, 244, 0.95) 0.17%, #A488F5 7.34%, rgba(234, 67, 53, 0.88) 24.26%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; + text-fill-color: transparent; + animation: effect 2s linear; + display: flex; + flex-direction: row; + gap: 1%; + align-items: center; + justify-content: center; +} + +@keyframes effect { + + 0% { + background: linear-gradient(89.9deg, rgba(240, 216, 214, 0.88) 0.17%, #ebcaec 7.34%, rgba(194, 214, 247, 0.95) 24.26%); + -webkit-background-clip: text; + } + + 10% { + background: linear-gradient(89.9deg, rgba(227, 237, 253, 0.95) 0.17%, #e5ddfd 7.34%, rgba(186, 210, 248, 0.95) 24.26%); + -webkit-background-clip: text; + } + + 20% { + background: linear-gradient(89.9deg, rgba(218, 231, 253, 0.95) 0.17%, #dcd1fc 7.34%, rgba(172, 202, 252, 0.95) 24.26%); + -webkit-background-clip: text; + } + + 30% { + background: linear-gradient(89.9deg, rgba(199, 219, 252, 0.95) 0.17%, #d0c2fa 7.34%, rgba(161, 196, 252, 0.95) 24.26%); + -webkit-background-clip: text; + } + + 40% { + background: linear-gradient(89.9deg, rgba(169, 201, 253, 0.95) 0.17%, #cdbcff 7.34%, rgba(149, 188, 250, 0.95) 24.26%); + -webkit-background-clip: text; + } + + 50% { + background: linear-gradient(89.9deg, rgba(200, 220, 253, 0.95) 0.17%, #bba5fc 7.34%, rgba(203, 221, 250, 0.95) 24.26%); + -webkit-background-clip: text; + } + + 60% { + background: linear-gradient(89.9deg, rgba(227, 237, 253, 0.95) 0.17%, #e5ddfd 7.34%, rgba(186, 210, 248, 0.95) 24.26%); + -webkit-background-clip: text; + } + + 70% { + background: linear-gradient(89.9deg, rgba(183, 210, 253, 0.95) 0.17%, #e5ddfd 7.34%, rgba(248, 217, 215, 0.88) 24.26%); + -webkit-background-clip: text; + } + + 80% { + background: linear-gradient(89.9deg, rgba(122, 172, 252, 0.95) 0.17%, #e5ddfd 7.34%, rgba(247, 182, 176, 0.88) 24.26%); + -webkit-background-clip: text; + } + + 90% { + background: linear-gradient(89.9deg, rgba(94, 148, 236, 0.95) 0.17%, #b49ff1 7.34%, rgba(240, 121, 111, 0.88) 24.26%); + -webkit-background-clip: text; + } + + 100% { + background: linear-gradient(89.9deg, rgba(66, 133, 244, 0.95) 0.17%, #A488F5 7.34%, rgba(234, 67, 53, 0.88) 24.26%); + -webkit-background-clip: text; + } +} + +.helper-container { + white-space: nowrap; + overflow: hidden; + font-size: 3.5rem; + font-weight: 400; + line-height: 4rem; + font-family: Google Sans, Helvetica Neue, sans-serif; + letter-spacing: -.03em; + color: #8f9391; + padding-bottom: 1%; + opacity: 0.5; + animation: typing 2s steps(22), blink .5s step-end infinite alternate; + justify-content: center; + display: flex; +} + +@keyframes typing { + from { + width: 0 + } +} + +@keyframes blink { + 50% { + border-color: transparent + } +} + +.quick-bot-introduction-container { + width: 100%; + display: flex; + flex-direction: row; + justify-content: center; + + .introduction-sub-container { + width: 66%; + justify-content: center; + text-align: center; + display: flex; + flex-direction: column; + padding-top: 2%; + padding-bottom: 2%; + background: radial-gradient(white, #FAFCFF) padding-box, linear-gradient(89.86deg, #34A853 -40.93%, #FBBC05 4.92%, #EA4335 37.97%, #1A73E8 106.62%) border-box; + border-radius: 24px; + border: 1px solid transparent; + + } + + .logo-image-container { + img { + height: 110px; + width: 110px; + } + } + + .logo-text-heading { + font-style: normal; + font-weight: 500; + font-size: 28px; + line-height: 202%; + /* or 65px */ + text-align: center; + + background: linear-gradient(89.86deg, #34A853 -40.93%, #FBBC05 4.92%, #EA4335 37.97%, #1A73E8 106.62%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; + text-fill-color: transparent; + } + + .logo-text-subheading { + font-style: normal; + font-weight: 500; + font-size: 21px; + line-height: 202%; + /* or 48px */ + text-align: center; + } + + .blue { + color: #4285F4; + } +} + +.helpButton { + position: fixed !important; + bottom: 45px !important; + right: 20px !important; + background-color: #4285f4 !important; + color: white !important; + text-decoration: none !important; + border-radius: 60px !important; + height: 41px !important; + display: inline-flex !important; + align-items: center !important; + overflow: hidden !important; + width: auto !important; + max-width: 41px !important; + -webkit-transition: max-width 0.5s !important; + transition: max-width 0.5s !important; + z-index: 1000 !important; + + &:hover { + max-width: 300px !important; + } + + .text { + white-space: nowrap !important; + padding-right: 15px !important; + font-size: 15px !important; + font-weight: 600 !important; + } +} + +::placeholder { + color: #646566; +} + +.intent-container-box { + box-sizing: border-box; + width: 239px; + height: 239px; + border-radius: 14px; + transition-delay: 2s; + transition: width 8s; + padding: 1%; + cursor: pointer; +} + +.selected-intent-box { + box-sizing: border-box; + height: 239px; + border-radius: 14px; + transition-delay: 2s; + transition: width 8s; + cursor: pointer; + width: max-content !important; + max-width: 100%; + display: flex; + flex-direction: column; + gap: 1%; + padding: 1%; + background: linear-gradient(92.1deg, #bdd1f3 19.16%, rgb(199 183 245 / 90%) 79%, rgb(235 165 158 / 90%) 135.24%), linear-gradient(272.04deg, rgba(158, 188, 237, 0.7) 14.93%, rgba(158, 188, 237, 0) 104.51%) !important; +} + +.cards-outer-container { + width: 65%; + overflow: auto; + scrollbar-width: none; +} + +.cards-outer-container:after { + z-index: -1; + content: ''; + width: 10%; + opacity: 0.5 !important; +} + +.cards-container-all-categories { + width: max-content; + display: flex; + overflow-x: auto; + gap: 1%; + max-width: 160%; + min-width: 100%; + scrollbar-width: none; +} + +.cards-container-few-categories { + width: max-content; + display: flex; + overflow-x: auto; + gap: 1%; + max-width: 160%; + min-width: 100%; + scrollbar-width: none; + justify-content: center; + padding-inline: 5%; +} + + +.intent-container-box:hover { + background: #DDE3EA !important; +} + +.card-detail-container { + width: 100%; + min-width: 250px; + display: flex; + flex-direction: row; +} + + +.selected-intent-suggested-question { + gap: 4%; + display: flex !important; + flex-direction: column; +} + +.suggested-question-chip-row { + + margin-top: 0.5rem; + + .suggested-question-chip { + display: flex; + flex-direction: row; + margin: 0.5rem; + padding: 0.5rem; + width: 100%; + + .suggested-question-icon { + padding-right: 0.25rem + } + + .suggested-question-text { + margin-left: 0.25rem; + + width: 100%; + } + } +} + +.close-button-container { + display: none !important; +} + +.expand-close-button-container { + display: block !important; + + .close-button { + margin-top: -10px !important; + } +} + +.card-heading { + padding-top: 2.5%; + font-size: 17px; + color: #1F1F1F; + width: 90%; +} + +.card-content { + font-size: 13px; + color: #646566; + padding-top: 4%; + white-space: break-spaces; + text-wrap: balance; + padding-right: 3%; + text-align: left; + width: 85%; +} + +::ng-deep .mat-mdc-snack-bar-container { + &.green-snackbar { + --mdc-snackbar-container-color: #0F9D58; + --mat-mdc-snack-bar-button-color: #fff; + --mdc-snackbar-supporting-text-color: #fff; + } + + &.red-snackbar { + --mdc-snackbar-container-color: #e9103f; + --mat-mdc-snack-bar-button-color: #fff; + --mdc-snackbar-supporting-text-color: #fff; + } +} + +::ng-deep .mat-mdc-dialog-surface { + justify-content: center !important; + display: flex !important; + flex-direction: column !important; + text-align: center !important; + background: #FFFFFF; + border-radius: 20px !important; + box-sizing: border-box; + box-shadow: none !important; + + .badge-prompt-parent-container { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + text-align: center; + + .badge-greetings-container { + width: 100%; + display: flex; + flex-direction: row; + justify-content: center; + + img { + width: 45%; + } + } + + .badge-text-container { + font-style: normal; + font-weight: 400; + font-size: 17px; + line-height: 45px; + display: flex; + align-items: center; + color: rgba(92, 95, 97, 0.7); + } + } +} + +.blue-dot { + /* Ellipse 1226 */ + content: ''; + width: 8px; + height: 8px; + left: 1503px; + top: 445px; + display: flow-root; + border-radius: 15px; + + /* Blue */ + background: #4285F4; + animation: blue-transform 2s infinite; + +} + +.green-dot { + content: ''; + width:8px; + height: 8px; + left: 1522px; + top: 445px; + display: flow-root; + border-radius: 15px; + + /* Green */ + background: #0F9D58; + animation: green-transform 2s infinite; +} + +.yellow-dot { + content: ''; + width: 8px; + height: 8px; + left: 1541px; + top: 445px; + display: flow-root; + border-radius: 15px; + + /* Yellow */ + background: #F4B400; + animation: yellow-transform 2s infinite; + +} + +.recording-container { + display: flex; + flex-direction: row; + gap: 10px; + cursor: pointer; + align-items: baseline; +} + + +@keyframes blue-transform { + 0% { + transform: translateY(-14px) + } + + 33% { + transform: translateY(0) + } + + 66% { + transform: translateY(0) + } + + 100% { + transform: translateY(-14px) + } + +} + +@keyframes green-transform { + 0% { + transform: translateY(0) + } + + 33% { + transform: translateY(-14px) + } + + 66% { + transform: translateY(0) + } + + 100% { + transform: translateY(0) + } + +} + +@keyframes yellow-transform { + 0% { + transform: translateY(0) + } + + 33% { + transform: translateY(0) + } + + 66% { + transform: translateY(-14px) + } + + 100% { + transform: translateY(0) + } + +} + +.tooltip-container { + display: flex; + width: 100%; + justify-content: center; + align-items: baseline; + padding-top: 2%; + padding-bottom: 2%; + .tooltip-sub-container { + width: 66%; + display: flex; + align-items: baseline; + justify-content: center; + } +} + +.tooltip-icon-container { + display: flex; + flex-direction: row; + // align-self: flex-end; + gap: 10px; + flex-wrap: nowrap; + text-wrap: nowrap; + align-self: center; + color: #5F6368; + + .tooltip-label { + font-family: 'Roboto'; + font-style: normal; + font-weight: 500; + font-size: 16px; + line-height: 20px; + text-align: center; + } + + mat-icon { + font-size: 22px !important; + } +} + +.tooltip-text-container { + flex-wrap: wrap; + word-wrap: break-word; + justify-content: flex-start; + display: flex; + color: #5F6368; + .tooltip-text { + font-family: 'Roboto'; + font-style: normal; + font-weight: 400; + font-size: 16px; + line-height: 20px; + } +} + + +.dismiss-container { + font-family: 'Roboto'; + font-style: normal; + font-weight: 500; + font-size: 14px; + line-height: 20px; + /* identical to box height, or 143% */ + text-align: center; + + /* Blue */ + color: #4285F4; + padding-left: 1%; + cursor: pointer; +} + +.dismiss-container:hover { + text-decoration: underline; +} + +.journey-chips { + max-height: 300px; + overflow-y: auto; +} + +::ng-deep .mat-mdc-standard-chip:hover { + background: linear-gradient(92.1deg, #9EBCED 19.16%, rgba(164, 136, 245, 0.9) 79%) !important; + --mdc-chip-label-text-color: #fff; +} + +.suggested-question-container { + width: 60%; +} \ No newline at end of file diff --git a/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/app/components/main/main.component.spec.ts b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/app/components/main/main.component.spec.ts new file mode 100644 index 0000000..66e7fd9 --- /dev/null +++ b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/app/components/main/main.component.spec.ts @@ -0,0 +1,38 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {ComponentFixture, TestBed} from '@angular/core/testing'; + +import {MainComponent} from './main.component'; + +describe('MainComponent', () => { + let component: MainComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [MainComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(MainComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/app/components/main/main.component.ts b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/app/components/main/main.component.ts new file mode 100644 index 0000000..508fc94 --- /dev/null +++ b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/app/components/main/main.component.ts @@ -0,0 +1,76 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {Component, OnDestroy} from '@angular/core'; +import {UserService} from 'src/app/services/user/user.service'; +import {Router} from '@angular/router'; +import {MatDialog} from '@angular/material/dialog'; +import {SearchApplicationFormComponent} from '../search-application/search-application-form/search-application-form.component'; +import {ReplaySubject} from 'rxjs'; +import {takeUntil} from 'rxjs/operators'; +import {SearchResponse} from 'src/app/models/search.model'; +import {SearchApplicationService} from 'src/app/services/search_application.service'; + +@Component({ + selector: 'app-main', + templateUrl: './main.component.html', + styleUrls: ['./main.component.scss'], +}) +export class MainComponent implements OnDestroy { + private readonly destroyed = new ReplaySubject(1); + term = ''; + showResults = false; + searchResults: SearchResponse; + savedUser; + + constructor( + public userService: UserService, + private router: Router, + public dialog: MatDialog, + private readonly searchApplicationService: SearchApplicationService + ) { + this.savedUser = userService.getUserDetails(); + this.checkConfiguration(); + } + + checkConfiguration() { + this.searchApplicationService + .get() + .pipe(takeUntil(this.destroyed)) + .subscribe(searchApplication => { + if (!searchApplication) { + this.openAddAgentBuilderConfigForm(); + } + }); + } + + goToResults(term: string) { + this.router.navigate(['/search'], {queryParams: {q: term}}); + } + + openAddAgentBuilderConfigForm() { + this.dialog.open(SearchApplicationFormComponent, { + disableClose: true, + height: '600px', + width: '1120px', + }); + } + + ngOnDestroy() { + this.destroyed.next(); + this.destroyed.complete(); + } +} diff --git a/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/app/components/main/search-results/search-results.component.html b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/app/components/main/search-results/search-results.component.html new file mode 100644 index 0000000..b6890f8 --- /dev/null +++ b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/app/components/main/search-results/search-results.component.html @@ -0,0 +1,55 @@ + + + +
    +
    +
    auto_awesome AI Overview
    +
    {{summary}}
    +
    + +
    +
    +
    +
    +
    + Links +
    +
    +
    +
    +
    +
    + +
    +
    + {{result.title}} +
    + +
    +
    + +
    + {{result.snippet}} +
    +
    + +
    +
    +
    +
    diff --git a/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/app/components/main/search-results/search-results.component.scss b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/app/components/main/search-results/search-results.component.scss new file mode 100644 index 0000000..cf62df8 --- /dev/null +++ b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/app/components/main/search-results/search-results.component.scss @@ -0,0 +1,144 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +.overview-container { + display: flex; + justify-content: center; + padding-bottom: 1%; + padding-top: 2%; + padding-right: 1.5%; + + .overview-sub-container { + width: 66%; + background: linear-gradient(92.1deg, #9ebcedb2 19.16%, rgba(163, 136, 245, 0.651) 79%, rgba(234, 68, 53, 0.493) 135.24%); + border-radius: 12px; + .overview-text { + font-family: 'Roboto'; + font-style: normal; + font-weight: 500; + font-size: 20px; + line-height: 24px; + color: #5C5F61; + padding: 2%; + } + .overview-content { + + font-family: 'Roboto'; + font-style: normal; + font-weight: 400; + font-size: 19px; + line-height: 30px; + color: #161A1D; + padding-left: 2%; + padding-right: 2%; + padding-bottom: 2%; + + } + + .overview-button { + padding-left: 2%; + padding-right: 2%; + padding-bottom: 2%; + + button { + font-family: 'Roboto'; + font-style: normal; + font-weight: 400; + font-size: 20px; + line-height: 24px; + color: #4285F4; + } + } + } + +} + +.header-container { + font-family: 'Roboto'; + font-style: normal; + font-weight: 400; + font-size: 16px; + line-height: 19px; + color: #5F6368; + display: flex; + justify-content: flex-start; + width: 66%; +} + +.search-result:hover { + border: 1px solid #DFE0E2; + border-radius: 10px; +} + +.search-result-parent-container{ + display: flex; + flex-direction: column; + width: 66%; + gap: 15px; +} + +.search-result { + padding: 1%; + padding-top: 2%; + cursor: pointer; + + .title-container { + display: flex; + flex-direction: row; + font-family: 'Roboto'; + font-style: normal; + font-weight: 500; + font-size: 16px; + line-height: 31px; + color: #161A1D; + display: flex; + + .link-container { + font-size: 14px !important; + font-weight: 400 !important; + color: #676c6f !important; + } + } + + .description-container { + font-family: 'Roboto'; + font-style: normal; + font-weight: 400; + font-size: 16px; + line-height: 24px; + color: #161A1D; + opacity: 0.8; + padding: 1%; + padding-left: 22px; + } + + .overview-button { + padding-left: 22px; + button { + font-family: 'Roboto'; + font-style: normal; + font-weight: 400; + font-size: 16px; + line-height: 24px; + color: #4285F4; + } + } + +} + +.padding-top { + padding-top: 0; +} \ No newline at end of file diff --git a/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/app/components/main/search-results/search-results.component.spec.ts b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/app/components/main/search-results/search-results.component.spec.ts new file mode 100644 index 0000000..1c8bf77 --- /dev/null +++ b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/app/components/main/search-results/search-results.component.spec.ts @@ -0,0 +1,38 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {ComponentFixture, TestBed} from '@angular/core/testing'; + +import {SearchResultsComponent} from './search-results.component'; + +describe('SearchResultsComponent', () => { + let component: SearchResultsComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [SearchResultsComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(SearchResultsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/app/components/main/search-results/search-results.component.ts b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/app/components/main/search-results/search-results.component.ts new file mode 100644 index 0000000..90755bb --- /dev/null +++ b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/app/components/main/search-results/search-results.component.ts @@ -0,0 +1,74 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {Component, OnDestroy} from '@angular/core'; +import {SearchService} from 'src/app/services/search.service'; +import {ReplaySubject} from 'rxjs'; +import {UserService} from 'src/app/services/user/user.service'; +import {ActivatedRoute, Router} from '@angular/router'; + +@Component({ + selector: 'app-search-results', + templateUrl: './search-results.component.html', + styleUrls: ['./search-results.component.scss'], +}) +export class SearchResultsComponent implements OnDestroy { + summary = ''; + private readonly destroyed = new ReplaySubject(1); + serachResult: any = []; + + constructor( + private router: Router, + private route: ActivatedRoute, + private service: SearchService, + private userService: UserService + ) { + const query = this.route.snapshot.queryParamMap.get('q'); + + this.service.search(query!).subscribe({ + next: (searchRespone: any) => { + this.serachResult = searchRespone; + this.userService.hideLoading(); + }, + error: () => { + this.userService.hideLoading(); + }, + }); + } + + searchTerm(term: string) { + this.router.navigate(['/search'], {queryParams: {q: term}}); + + this.service.search(term).subscribe({ + next: (searchRespone: any) => { + this.serachResult = searchRespone; + this.userService.hideLoading(); + }, + error: () => { + this.userService.hideLoading(); + }, + }); + } + + openNewWindow(link: string) { + window.open(link, '_blank'); + } + + ngOnDestroy() { + this.destroyed.next(); + this.destroyed.complete(); + } +} diff --git a/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/app/components/search-application/manage-search-application/manage-search-application.component.html b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/app/components/search-application/manage-search-application/manage-search-application.component.html new file mode 100644 index 0000000..51a5baa --- /dev/null +++ b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/app/components/search-application/manage-search-application/manage-search-application.component.html @@ -0,0 +1,74 @@ + + +
    + +
    +
    +
    +
    +
    + Manage Agent Builder Config +
    +
    +
    +
    + + Choose an Engine + + {{ + engine.name + }} + + +
    + + + +
    +
    +
    +
    +
    diff --git a/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/app/components/search-application/manage-search-application/manage-search-application.component.scss b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/app/components/search-application/manage-search-application/manage-search-application.component.scss new file mode 100644 index 0000000..3b7067f --- /dev/null +++ b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/app/components/search-application/manage-search-application/manage-search-application.component.scss @@ -0,0 +1,571 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +.root-container { + display: flex; + width: 100%; + justify-content: center; + + .max-width-container { + width: 100%; + max-width: 1280px; + } +} + +.action-button-container { + padding: 0; +} + +.table-container { + justify-content: center; + display: block; + flex-direction: row; + padding: 5.5% 3%; + padding-bottom: 5%; + + .row { + display: flex; + flex-direction: row; + + .mat-mdc-form-field { + width: 100% !important; + } + + } + +} + +.amber { + color: #fbc645; +} + +.red { + color: red; +} + +.blue { + color: #4285F4; +} + +::ng-deep .mat-checkbox-checked .mat-checkbox-background, +.mat-checkbox-indeterminate .mat-checkbox-background { + background-color: #4285F4 !important; +} + +// overwrite the ripple overlay on hover and click +::ng-deep .mat-checkbox:not(.mat-checkbox-disabled) .mat-checkbox-ripple .mat-ripple-element { + background-color: #4285F4 !important; +} + +::ng-deep .mdc-checkbox__ripple { + background-color: #4285F4 !important; +} + +::ng-deep .mat-mdc-checkbox .mdc-checkbox .mdc-checkbox__native-control:enabled:checked~.mdc-checkbox__background { + background-color: #4285F4 !important; + border-color: #4285F4 !important; +} + +::ng-deep .mat-mdc-checkbox .mdc-checkbox .mdc-checkbox__native-control:enabled:indeterminate~.mdc-checkbox__background { + background-color: #4285F4 !important; + border-color: #4285F4 !important; + +} + +::ng-deep .mat-mdc-slide-toggle.mat-mdc-slide-toggle-checked:not(.mat-disabled) .mdc-switch__shadow { + background-color: #4285F4; + /*replace with your color*/ +} + +::ng-deep .mat-mdc-slide-toggle.mat-mdc-slide-toggle-checked:not(.mat-disabled) .mdc-switch__track::after { + background-color: #4285F4 !important; + /*replace with your color*/ +} + +.buttons-container { + display: flex; + flex-direction: row; + justify-content: flex-start; + width: 100%; + padding-top: 1%; + padding-bottom: 1%; + padding-left: 3%; +} + +.saved-intents-container { + padding-top: 2%; + padding-bottom: 2%; +} + +.heading-container { + width: 73%; + align-content: center; + flex-wrap: wrap; + display: flex; + flex-direction: row; + + span { + font-family: 'Google Sans', sans-serif !important; + font-style: normal; + font-weight: 400; + font-size: 20px; + line-height: 14px; + color: #161A1D; + } +} + +.button-child-container { + display: flex; + flex-direction: row; + gap: 20px; + width: 24%; + justify-content: center; + + button { + border-radius: 30px; + font-family: 'Google Sans', sans-serif !important; + font-style: normal; + font-weight: 500; + font-size: 14px; + line-height: 23px; + letter-spacing: unset; + box-shadow: none; + } + + .bulk-delete-button { + color: #f70808; + border: 1px solid #f70808; + } + + .create-notification-button { + color: #fff; + background: #4285F4; + } +} + +.form-field { + width: 100%; +} + +.dialog-toggle-button-container { + font-family: 'Google Sans', sans-serif !important; + font-style: normal; + width: 100%; + display: flex; + flex-direction: row; + gap: 10px; + padding-bottom: 4%; + + button { + height: 45px; + border: none; + } + + .error { + background: #e9867e33; + width: 28%; + } + + .warning { + background: rgb(241 226 183 / 44%); + width: 28%; + } + + .info { + background: #F6F9FE; + width: 35%; + } + + .errorOutline { + border: 1px solid red; + } + + .warningOutline { + border: 1px solid #fbc645; + } + + .infoOutline { + border: 1px solid #4285F4; + } +} + +.delete-confirmation-text { + font-family: 'Google Sans', sans-serif !important; + font-style: normal; + width: 100%; + display: flex; + justify-content: center; + font-size: 20px; + color: #161A1D; +} + +.dialog-delete-button { + border-radius: 30px; + font-family: 'Google Sans', sans-serif !important; + font-style: normal; + font-weight: 500; + font-size: 14px; + line-height: 23px; + letter-spacing: unset; + box-shadow: none; + color: #f70808; + border: 1px solid #f70808; +} + +.disableDiv { + pointer-events: none; +} + +.spinner-container { + width: 100%; + justify-content: center; + display: flex; + flex-direction: row; + text-align: center; + position: absolute; + top: 50%; +} + +.spinner-dialog-container { + width: 100%; + justify-content: flex-end; + display: flex; + flex-direction: row; + padding-right: 5%; + padding-bottom: 4%; +} + +.text-column { + width: 30% !important; + word-break: break-word; +} + +.comment-detail-container { + display: flex; + flex-direction: column; + gap: 20px; +} + +.details-container { + font-weight: 600; + border-radius: 5px; + border: 1px solid #9e9e9e; + text-align: start; + padding: 3%; + gap: 14px; + display: flex; + flex-direction: column; + + .question-details-container { + background: white; + font-weight: 400; + border-radius: 12px; + } +} + +.chip-color { + background: aliceblue !important; + border: 1px solid #4285f4; +} + +.action-column { + width: 8%; +} + +::-webkit-scrollbar { + width: 9px; +} + +::-webkit-scrollbar-track { + background: transparent; +} + +::-webkit-scrollbar-thumb { + background-color: rgba(155, 155, 155, 0.5); + border-radius: 20px; + border: transparent; +} + +.search-question-field { + width: 100%; + + mat-icon { + color: #363e45; + cursor: pointer; + } +} + +::ng-deep .search-question-field .mat-mdc-text-field-wrapper.mdc-text-field--outlined .mat-mdc-form-field-infix { + padding-top: 10px !important; + padding-bottom: 10px !important; +} + +::ng-deep .search-question-field .mdc-text-field__input { + height: 34px !important; +} + +.input-field-container { + display: flex; + flex-direction: row; + gap: 2%; + align-items: center; +} + +.submit-button { + // width: 8% !important; + background: #4285F4 !important; + border: 1px solid #4285F4; + box-sizing: border-box; + // border: 1px solid #e0e0e0 !important; + color: #fff !important; + position: relative; + user-select: none; + cursor: pointer; + outline: none; + -webkit-tap-highlight-color: transparent; + font-family: 'Google Sans', sans-serif !important; + font-size: 14px; + font-weight: 500; + white-space: nowrap; + text-decoration: none; + vertical-align: baseline; + text-align: center; + margin: 0; + min-width: 64px; + line-height: 36px; + padding: 20px 46px; + border-radius: 48px !important; + overflow: visible; + transform: translate3d(0, 0, 0); + transition: background 400ms cubic-bezier(0.25, 0.8, 0.25, 1), box-shadow 280ms cubic-bezier(0.4, 0, 0.2, 1); +} + +.reset-button { + background: white !important; + box-sizing: border-box !important; + border: 1px solid #4285F4 !important; + color: #4285F4 !important; + cursor: pointer; + outline: none; + font-family: 'Google Sans', sans-serif !important; + font-size: 14px; + font-weight: 500; + margin: 0; + min-width: 130px; + line-height: 36px; + padding: 0px 25px; + height: 100%; + border-radius: 48px !important; + display: flex; + justify-content: center; + align-content: center; + align-items: center; + flex-direction: row; +} + +.inactive-button { + + background: #ffffff !important; + box-sizing: border-box !important; + border: 1px solid rgba(43, 41, 40, 0.502) !important; + color: rgba(43, 41, 40, 0.93) !important; + cursor: pointer; + outline: none; + font-family: "Google Sans", sans-serif !important; + font-size: 14px; + font-weight: 500; + margin: 0; + min-width: 64px; + line-height: 36px; + padding: 0px 45px; + height: 60%; + margin-left: 2rem; + border-radius: 48px !important; + display: flex; + justify-content: center; + flex-direction: row; + flex-wrap: wrap; + align-content: center; + align-items: center; +} + +.active-button { + + background: rgb(226 249 225) !important; + box-sizing: border-box !important; + border: 1px solid rgb(59 153 29 / 84%) !important; + color: rgb(17 193 56 / 93%) !important; + cursor: pointer; + outline: none; + font-family: "Google Sans", sans-serif !important; + font-size: 14px; + font-weight: 500; + margin: 0; + min-width: 64px; + line-height: 36px; + padding: 0px 45px; + height: 60%; + margin-left: 2rem; + border-radius: 48px !important; + display: flex; + justify-content: center; + flex-direction: column; + flex-wrap: wrap; + align-content: center; + align-items: center; +} + +.intent-field { + width: 50%; + + .my-icon { + margin-right: 0.5rem; + color: #005cbb; + border-radius: 2rem; + padding: 1rem; + } +} + +.intent-heading-container { + font-style: normal; + font-weight: 400; + font-size: 16px; + line-height: 23px; + color: #363e45; + padding-top: 2%; + padding-bottom: 1%; +} + +.internal-spinner-container { + height: 31px +} + +pre { + width: 100%; + color: white; + padding: 2%; + font-family: monospace; + text-wrap: pretty; + display: flex; + flex-direction: row; +} + + +::-webkit-scrollbar-track { + -webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3); + border-radius: 10px; + background-color: #F5F5F5; +} + +::-webkit-scrollbar { + width: 12px; + background-color: #F5F5F5; +} + +::-webkit-scrollbar-thumb { + border-radius: 10px; + -webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, .3); + background-color: #D62929; +} + +.intent-chunk { + display: flex; + flex-direction: row; + width: 90%; +} + +.copy-button-container { + width: 10%; + display: flex; + justify-content: flex-end; +} + +.configure-title { + display: flex; + align-content: center; + flex-wrap: wrap; + width: 50%; +} + +.intent-buttons-container { + display: flex; + gap: 2%; + justify-content: flex-end; + padding-top: 1%; + padding-bottom: 1%; +} + +.image-container { + justify-content: center; + display: flex; + width: 90%; + + img { + height: 47%; + width: 47%; + } + } + + .delete-confirmation-text { + font-family: 'Google Sans', sans-serif !important; + font-style: normal; + width: 100%; + display: flex; + justify-content: center; + font-size: 20px; + color: #161A1D; + } + + .dialog-actions-container { + padding-bottom: 2%; + justify-content: center; + display: flex; + flex-direction: row; + text-align: end; + width: 100%; + padding-right: 2%; + + .discard-button { + color: rgba(92, 95, 97, 0.5); + border: 1px solid rgb(92 95 97 / 28%); + border-radius: 40px; + box-shadow: none; + letter-spacing: inherit; + } + + .save-button { + background: #4285F4; + color: #fff; + border-radius: 40px; + box-shadow: none; + letter-spacing: inherit; + } + + .dialog-delete-button { + border-radius: 30px; + font-family: 'Google Sans', sans-serif !important; + font-style: normal; + font-weight: 500; + font-size: 14px; + line-height: 23px; + letter-spacing: unset; + box-shadow: none; + color: #f70808; + border: 1px solid #f70808; + } + } + + diff --git a/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/app/components/search-application/manage-search-application/manage-search-application.component.spec.ts b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/app/components/search-application/manage-search-application/manage-search-application.component.spec.ts new file mode 100644 index 0000000..bf04b68 --- /dev/null +++ b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/app/components/search-application/manage-search-application/manage-search-application.component.spec.ts @@ -0,0 +1,38 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {ComponentFixture, TestBed} from '@angular/core/testing'; + +import {ManageSearchApplicationComponent} from './manage-search-application.component'; + +describe('ManageAgentBuilderComponent', () => { + let component: ManageSearchApplicationComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ManageSearchApplicationComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(ManageSearchApplicationComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/app/components/search-application/manage-search-application/manage-search-application.component.ts b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/app/components/search-application/manage-search-application/manage-search-application.component.ts new file mode 100644 index 0000000..67a6f9a --- /dev/null +++ b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/app/components/search-application/manage-search-application/manage-search-application.component.ts @@ -0,0 +1,111 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {Component, OnDestroy} from '@angular/core'; +import {SearchApplicationService} from 'src/app/services/search_application.service'; +import {ReplaySubject} from 'rxjs'; +import {takeUntil} from 'rxjs/operators'; +import {UserService} from 'src/app/services/user/user.service'; +import {Router} from '@angular/router'; +import {Engine} from 'src/app/models/engine.model'; +import {EnginesService} from 'src/app/services/engines.service'; + +@Component({ + selector: 'app-manage-search-application', + templateUrl: './manage-search-application.component.html', + styleUrls: ['./manage-search-application.component.scss'], +}) +export class ManageSearchApplicationComponent implements OnDestroy { + selectedEngine: Engine = { + name: '', + engine_id: '', + region: '', + }; + engines: Engine[] = []; + editMode = false; + savedEngineID = ''; + private readonly destroyed = new ReplaySubject(1); + + constructor( + private readonly searchApplicationService: SearchApplicationService, + private readonly userService: UserService, + private readonly router: Router, + private enginesService: EnginesService + ) { + this.enginesService + .getAll() + .subscribe(response => (this.engines = response)); + this.disableForm(); + this.getConfigData(); + } + + disableForm() { + this.editMode = false; + } + + enableForm() { + this.editMode = true; + } + + getConfigData() { + this.userService.showLoading(); + this.searchApplicationService + .get() + .pipe(takeUntil(this.destroyed)) + .subscribe({ + next: response => { + this.savedEngineID = response.engine_id; + this.selectedEngine = this.engines.filter( + e => e.engine_id === response.engine_id + )[0]; + this.userService.hideLoading(); + }, + error: () => { + this.userService.hideLoading(); + }, + }); + } + + saveForm() { + this.userService.showLoading(); + const searchApplication = { + engine_id: this.selectedEngine.engine_id, + region: this.selectedEngine.region, + }; + + this.searchApplicationService + .update(this.savedEngineID, searchApplication) + .subscribe({ + next: () => { + this.savedEngineID = this.selectedEngine.engine_id; + this.userService.hideLoading(); + this.disableForm(); + }, + error: () => { + this.userService.hideLoading(); + }, + }); + } + + navigateToMain() { + this.router.navigateByUrl('/'); + } + + ngOnDestroy(): void { + this.destroyed.next(); + this.destroyed.complete(); + } +} diff --git a/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/app/components/search-application/search-application-form/search-application-form.component.html b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/app/components/search-application/search-application-form/search-application-form.component.html new file mode 100644 index 0000000..f47c004 --- /dev/null +++ b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/app/components/search-application/search-application-form/search-application-form.component.html @@ -0,0 +1,52 @@ + + +
    +
    + Configure Search Application +
    + +
    + + Choose an Engine + + {{engine.name}} + + +
    + +
    + +
    + +
    +
    +
    diff --git a/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/app/components/search-application/search-application-form/search-application-form.component.scss b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/app/components/search-application/search-application-form/search-application-form.component.scss new file mode 100644 index 0000000..df1b17b --- /dev/null +++ b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/app/components/search-application/search-application-form/search-application-form.component.scss @@ -0,0 +1,106 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +.configure-title { + display: flex; + align-content: center; + flex-wrap: wrap; + width: 100%; + justify-content: center; + margin-top: -26px; + padding-bottom: 5%; + font-family: 'Roboto'; + font-style: normal; + font-weight: 400; + font-size: 24px; + line-height: 24px; + color: #161A1D; +} + +.form-field { + width: 70%; + + .my-icon { + margin-right: 0.5rem; + color: #005cbb; + border-radius: 2rem; + padding: 1rem; + } + } + + .button-container { + justify-content: center; + display: flex; + gap: 10px; + margin-bottom: -30px; + padding-top: 3%; + + .submit-button { + width: 7%; + background: #4285F4; + box-sizing: border-box; + border: 1px solid #e0e0e0; + color: #fff; + position: relative; + user-select: none; + cursor: pointer; + outline: none; + -webkit-tap-highlight-color: transparent; + display: inline-block; + font-family: 'Google Sans', sans-serif !important; + font-size: 14px; + font-weight: 500; + white-space: nowrap; + text-decoration: none; + vertical-align: baseline; + text-align: center; + margin: 0; + min-width: 64px; + padding: 0 16px; + border-radius: 35px; + overflow: visible; + transform: translate3d(0, 0, 0); + transition: background 400ms cubic-bezier(0.25, 0.8, 0.25, 1), box-shadow 280ms cubic-bezier(0.4, 0, 0.2, 1); + } + + .cancel-button { + width: 7%; + background: white; + box-sizing: border-box; + border: 1px solid #4285F4; + color: #4285F4; + position: relative; + user-select: none; + cursor: pointer; + outline: none; + -webkit-tap-highlight-color: transparent; + display: inline-block; + font-family: 'Google Sans', sans-serif !important; + font-size: 14px; + font-weight: 500; + white-space: nowrap; + text-decoration: none; + vertical-align: baseline; + text-align: center; + margin: 0; + min-width: 64px; + padding: 0 16px; + border-radius: 35px; + overflow: visible; + transform: translate3d(0, 0, 0); + transition: background 400ms cubic-bezier(0.25, 0.8, 0.25, 1), box-shadow 280ms cubic-bezier(0.4, 0, 0.2, 1); + } + } diff --git a/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/app/components/search-application/search-application-form/search-application-form.component.spec.ts b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/app/components/search-application/search-application-form/search-application-form.component.spec.ts new file mode 100644 index 0000000..96b03fa --- /dev/null +++ b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/app/components/search-application/search-application-form/search-application-form.component.spec.ts @@ -0,0 +1,38 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {ComponentFixture, TestBed} from '@angular/core/testing'; + +import {SearchApplicationFormComponent} from './search-application-form.component'; + +describe('AddAgentBuilderComponent', () => { + let component: SearchApplicationFormComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [SearchApplicationFormComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(SearchApplicationFormComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/app/components/search-application/search-application-form/search-application-form.component.ts b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/app/components/search-application/search-application-form/search-application-form.component.ts new file mode 100644 index 0000000..2bd03b9 --- /dev/null +++ b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/app/components/search-application/search-application-form/search-application-form.component.ts @@ -0,0 +1,66 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {Component} from '@angular/core'; +import {MatDialogRef} from '@angular/material/dialog'; +import {Router} from '@angular/router'; +import {Engine} from 'src/app/models/engine.model'; +import {EnginesService} from 'src/app/services/engines.service'; +import {SearchApplicationService} from 'src/app/services/search_application.service'; +import {UserService} from 'src/app/services/user/user.service'; + +@Component({ + selector: 'app-search-application-form', + templateUrl: './search-application-form.component.html', + styleUrls: ['./search-application-form.component.scss'], +}) +export class SearchApplicationFormComponent { + showSpinner = false; + selectedEngine: Engine; + engines: Engine[] = []; + + constructor( + private dialogRef: MatDialogRef, + private readonly router: Router, + private readonly searchApplicationService: SearchApplicationService, + private userService: UserService, + private enginesService: EnginesService + ) { + this.enginesService + .getAll() + .subscribe(response => (this.engines = response)); + } + + saveForm() { + if (this.selectedEngine) { + const searchApplication = { + engine_id: this.selectedEngine.engine_id, + region: this.selectedEngine.region, + }; + this.userService.showLoading(); + this.searchApplicationService.create(searchApplication).subscribe({ + next: () => { + this.userService.hideLoading(); + this.dialogRef.close()!; + this.router.navigateByUrl('/'); + }, + error: () => { + this.userService.hideLoading(); + }, + }); + } + } +} diff --git a/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/app/components/toast-message/toast-message.component.html b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/app/components/toast-message/toast-message.component.html new file mode 100644 index 0000000..a83d217 --- /dev/null +++ b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/app/components/toast-message/toast-message.component.html @@ -0,0 +1,26 @@ + + +
    + icon inside toast message + {{ text }} + cross icon inside toast message +
    diff --git a/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/app/components/toast-message/toast-message.component.scss b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/app/components/toast-message/toast-message.component.scss new file mode 100644 index 0000000..f29e152 --- /dev/null +++ b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/app/components/toast-message/toast-message.component.scss @@ -0,0 +1,37 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +.toast-content { + color: #ffffff; + font-family: "Google Sans", sans-serif !important; +} + +.close-toast { + margin-left: auto; + align-self: baseline; + cursor: pointer; +} + +.red-toast { + background-color: #ed0c0c !important; +} +.green-toast { + background-color: #06865e !important; +} + +.mat-mdc-snack-bar-container .mdc-snackbar__surface { + background-color: transparent !important; +} diff --git a/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/app/components/toast-message/toast-message.component.spec.ts b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/app/components/toast-message/toast-message.component.spec.ts new file mode 100644 index 0000000..d0d405a --- /dev/null +++ b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/app/components/toast-message/toast-message.component.spec.ts @@ -0,0 +1,38 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {ComponentFixture, TestBed} from '@angular/core/testing'; + +import {ToastMessageComponent} from './toast-message.component'; + +describe('ToastMessageComponent', () => { + let component: ToastMessageComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ToastMessageComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(ToastMessageComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/app/components/toast-message/toast-message.component.ts b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/app/components/toast-message/toast-message.component.ts new file mode 100644 index 0000000..ebac36d --- /dev/null +++ b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/app/components/toast-message/toast-message.component.ts @@ -0,0 +1,40 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {Component, Inject, ViewEncapsulation} from '@angular/core'; +import {MatSnackBar, MAT_SNACK_BAR_DATA} from '@angular/material/snack-bar'; + +@Component({ + selector: 'app-toast-message', + templateUrl: './toast-message.component.html', + styleUrls: ['./toast-message.component.scss'], + encapsulation: ViewEncapsulation.None, +}) +export class ToastMessageComponent { + text: string; + icon: string; + constructor( + private _snackBar: MatSnackBar, + @Inject(MAT_SNACK_BAR_DATA) public snackBarData: any + ) { + this.text = snackBarData.text; + this.icon = snackBarData.icon; + } + + closeToast() { + this._snackBar.dismiss(); + } +} diff --git a/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/app/models/engine.model.ts b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/app/models/engine.model.ts new file mode 100644 index 0000000..b7f7de8 --- /dev/null +++ b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/app/models/engine.model.ts @@ -0,0 +1,21 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export type Engine = { + name: string; + engine_id: string; + region: string; +}; diff --git a/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/app/models/search.model.ts b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/app/models/search.model.ts new file mode 100644 index 0000000..e281135 --- /dev/null +++ b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/app/models/search.model.ts @@ -0,0 +1,52 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export type SearchRequest = { + term: string; +}; + +export type SearchResponse = { + summary: any; + results: SearchResult[]; + totalSize: number; +}; + +export type SearchResult = { + document: Document; +}; + +export type Document = { + derivedStructData: DocumentData; +}; + +export type DocumentData = { + title: string; + link: string; + snippets: Snippet[]; + pagemap: PageMap; +}; + +export type Snippet = { + snippet: string; +}; + +export type PageMap = { + cse_image: ImagesData[]; +}; + +export type ImagesData = { + src: string; +}; diff --git a/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/app/models/search_application.model.ts b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/app/models/search_application.model.ts new file mode 100644 index 0000000..6e6b273 --- /dev/null +++ b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/app/models/search_application.model.ts @@ -0,0 +1,20 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export type SearchApplication = { + engine_id: string; + region: string; +}; diff --git a/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/app/services/engines.service.spec.ts b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/app/services/engines.service.spec.ts new file mode 100644 index 0000000..dd94b99 --- /dev/null +++ b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/app/services/engines.service.spec.ts @@ -0,0 +1,32 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {TestBed} from '@angular/core/testing'; + +import {EnginesService} from './engines.service'; + +describe('EnginesService', () => { + let service: EnginesService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(EnginesService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/app/services/engines.service.ts b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/app/services/engines.service.ts new file mode 100644 index 0000000..7a17897 --- /dev/null +++ b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/app/services/engines.service.ts @@ -0,0 +1,36 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {map, Observable} from 'rxjs'; +import {HttpClient} from '@angular/common/http'; +import {Injectable} from '@angular/core'; +import {Engine} from '../models/engine.model'; +import {environment} from 'src/environments/environment'; + +const ENGINES_URL = `${environment.backendURL}/search/engines`; + +@Injectable({ + providedIn: 'root', +}) +export class EnginesService { + constructor(private http: HttpClient) {} + + getAll(): Observable { + return this.http + .get(ENGINES_URL) + .pipe(map(response => response as Engine[])); + } +} diff --git a/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/app/services/login/auth.guard.spec.ts b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/app/services/login/auth.guard.spec.ts new file mode 100644 index 0000000..8284e7a --- /dev/null +++ b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/app/services/login/auth.guard.spec.ts @@ -0,0 +1,32 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {TestBed} from '@angular/core/testing'; + +import {AuthGuard} from './auth.guard'; + +describe('AuthGuard', () => { + let guard: AuthGuard; + + beforeEach(() => { + TestBed.configureTestingModule({}); + guard = TestBed.inject(AuthGuard); + }); + + it('should be created', () => { + expect(guard).toBeTruthy(); + }); +}); diff --git a/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/app/services/login/auth.guard.ts b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/app/services/login/auth.guard.ts new file mode 100644 index 0000000..fd95abd --- /dev/null +++ b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/app/services/login/auth.guard.ts @@ -0,0 +1,54 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {Injectable} from '@angular/core'; +import { + ActivatedRouteSnapshot, + CanActivate, + Router, + RouterStateSnapshot, + UrlTree, +} from '@angular/router'; +import {Observable} from 'rxjs'; +import {AuthService} from './auth.service'; +import {environment} from 'src/environments/environment'; + +const LOGIN_ROUTE = '/login'; + +@Injectable({ + providedIn: 'root', +}) +export class AuthGuard implements CanActivate { + constructor( + private auth: AuthService, + private router: Router + ) {} + + canActivate( + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot + ): + | Observable + | Promise + | boolean + | UrlTree { + if (environment.requiredLogin === 'True' && !this.auth.isLoggedIn()) { + this.router.navigate([LOGIN_ROUTE]); + return false; + } + return true; + } +} diff --git a/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/app/services/login/auth.service.spec.ts b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/app/services/login/auth.service.spec.ts new file mode 100644 index 0000000..f08aa1a --- /dev/null +++ b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/app/services/login/auth.service.spec.ts @@ -0,0 +1,32 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {TestBed} from '@angular/core/testing'; + +import {AuthService} from './auth.service'; + +describe('AuthService', () => { + let service: AuthService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(AuthService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/app/services/login/auth.service.ts b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/app/services/login/auth.service.ts new file mode 100644 index 0000000..325d42e --- /dev/null +++ b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/app/services/login/auth.service.ts @@ -0,0 +1,57 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {Injectable} from '@angular/core'; +import {Router} from '@angular/router'; +import {environment} from 'src/environments/environment'; + +const USER_TOKEN_KEY = 'gpau_id'; +const USER_DETAILS = 'USER_DETAILS'; +const LOGIN_ROUTE = '/login'; + +@Injectable({ + providedIn: 'root', +}) +export class AuthService { + private requiredLogin: boolean = environment.requiredLogin === 'True'; + + constructor(private router: Router) {} + + saveUserSession(token: string) { + localStorage.setItem(USER_TOKEN_KEY, token); + } + + logout(route: string = LOGIN_ROUTE) { + localStorage.removeItem(USER_TOKEN_KEY); + localStorage.removeItem(USER_DETAILS); + localStorage.removeItem('showTooltip'); + this.router.navigateByUrl(route); + } + + isLoggedIn() { + const isLoggedIn = localStorage.getItem(USER_TOKEN_KEY) !== null; + if (!isLoggedIn && this.router.url !== LOGIN_ROUTE) { + this.router.navigate([LOGIN_ROUTE]); + } + return isLoggedIn; + } + + isUserLoggedIn() { + if (!this.requiredLogin) return true; + const isLoggedIn = localStorage.getItem(USER_TOKEN_KEY) !== null; + return isLoggedIn; + } +} diff --git a/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/app/services/search.service.spec.ts b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/app/services/search.service.spec.ts new file mode 100644 index 0000000..b024e3b --- /dev/null +++ b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/app/services/search.service.spec.ts @@ -0,0 +1,32 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {TestBed} from '@angular/core/testing'; + +import {SearchService} from './search.service'; + +describe('SearchService', () => { + let service: SearchService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(SearchService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/app/services/search.service.ts b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/app/services/search.service.ts new file mode 100644 index 0000000..1495b60 --- /dev/null +++ b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/app/services/search.service.ts @@ -0,0 +1,37 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {HttpClient} from '@angular/common/http'; +import {Injectable} from '@angular/core'; +import {environment} from 'src/environments/environment'; +import {map} from 'rxjs/operators'; +import {SearchRequest, SearchResponse} from '../models/search.model'; + +const searchURL = `${environment.backendURL}/search`; + +@Injectable({ + providedIn: 'root', +}) +export class SearchService { + constructor(private http: HttpClient) {} + + search(term: string) { + const request: SearchRequest = {term: term}; + return this.http + .post(searchURL, request) + .pipe(map(response => response as SearchResponse)); + } +} diff --git a/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/app/services/search_application.service.spec.ts b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/app/services/search_application.service.spec.ts new file mode 100644 index 0000000..c2da7d1 --- /dev/null +++ b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/app/services/search_application.service.spec.ts @@ -0,0 +1,31 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {TestBed} from '@angular/core/testing'; +import {SearchApplicationService} from './search_application.service'; + +describe('ConfigurationService', () => { + let service: SearchApplicationService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(SearchApplicationService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/app/services/search_application.service.ts b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/app/services/search_application.service.ts new file mode 100644 index 0000000..1e419fb --- /dev/null +++ b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/app/services/search_application.service.ts @@ -0,0 +1,45 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {Injectable} from '@angular/core'; +import {HttpClient} from '@angular/common/http'; +import {environment} from 'src/environments/environment'; +import {map} from 'rxjs/operators'; +import {Observable} from 'rxjs'; +import {SearchApplication} from '../models/search_application.model'; + +const configURL = `${environment.backendURL}/search/application`; + +@Injectable({ + providedIn: 'root', +}) +export class SearchApplicationService { + constructor(private readonly http: HttpClient) {} + + get(): Observable { + return this.http + .get(configURL) + .pipe(map(response => response as SearchApplication)); + } + + create(searchApplication: SearchApplication) { + return this.http.post(configURL, searchApplication); + } + + update(engine_id: string, searchApplication: SearchApplication) { + return this.http.put(`${configURL}/${engine_id}`, searchApplication); + } +} diff --git a/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/app/services/speech-to-text.ts b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/app/services/speech-to-text.ts new file mode 100644 index 0000000..16b1469 --- /dev/null +++ b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/app/services/speech-to-text.ts @@ -0,0 +1,34 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {Injectable} from '@angular/core'; +import {HttpClient} from '@angular/common/http'; +import {environment} from 'src/environments/environment'; + +const audioChatUrl = `${environment.backendURL}/audio_chat`; + +@Injectable({ + providedIn: 'root', +}) +export class SpeechToTextService { + constructor(private http: HttpClient) {} + + async transcribeAudio(audioBlob: Blob) { + const formData = new FormData(); + formData.append('audio_file', audioBlob, 'audio.wav'); + return this.http.post(audioChatUrl, formData); + } +} diff --git a/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/app/services/user/session.service.spec.ts b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/app/services/user/session.service.spec.ts new file mode 100644 index 0000000..ff45965 --- /dev/null +++ b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/app/services/user/session.service.spec.ts @@ -0,0 +1,32 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {TestBed} from '@angular/core/testing'; + +import {SessionService} from './session.service'; + +describe('SessionService', () => { + let service: SessionService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(SessionService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/app/services/user/session.service.ts b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/app/services/user/session.service.ts new file mode 100644 index 0000000..4a51a17 --- /dev/null +++ b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/app/services/user/session.service.ts @@ -0,0 +1,35 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {Injectable} from '@angular/core'; +import {v4 as uuid} from 'uuid'; + +const SESSION_KEY = 'pasid'; + +@Injectable({ + providedIn: 'root', +}) +export class SessionService { + constructor() {} + + getSession(): string | null { + return sessionStorage.getItem(SESSION_KEY); + } + + createSession() { + sessionStorage.setItem(SESSION_KEY, uuid()); + } +} diff --git a/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/app/services/user/user.service.ts b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/app/services/user/user.service.ts new file mode 100644 index 0000000..cda7580 --- /dev/null +++ b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/app/services/user/user.service.ts @@ -0,0 +1,54 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {Injectable} from '@angular/core'; +import {BehaviorSubject} from 'rxjs'; + +type UserStored = { + uid?: string; + name?: string; + email?: string; + photoURL?: string; + displayName?: string; + domain?: string; +}; + +@Injectable({ + providedIn: 'root', +}) +export class UserService { + readonly loadingSubject = new BehaviorSubject(false); + + constructor() {} + + showLoading() { + this.loadingSubject.next(true); + } + + hideLoading() { + this.loadingSubject.next(false); + } + + getUserDetails(): UserStored { + if (localStorage.getItem('USER_DETAILS') !== null) { + const userObj = localStorage.getItem('USER_DETAILS'); + return JSON.parse(userObj || '{}'); + } else { + const userDetails: UserStored = {}; + return userDetails; + } + } +} diff --git a/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/assets/.gitkeep b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/assets/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/assets/gcloud-logo.svg b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/assets/gcloud-logo.svg new file mode 100644 index 0000000..5150056 --- /dev/null +++ b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/assets/gcloud-logo.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/assets/images/SocialHeaderTemplate8.png b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/assets/images/SocialHeaderTemplate8.png new file mode 100644 index 0000000..88f4306 Binary files /dev/null and b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/assets/images/SocialHeaderTemplate8.png differ diff --git a/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/assets/images/answernotfound.png b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/assets/images/answernotfound.png new file mode 100644 index 0000000..874c87d Binary files /dev/null and b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/assets/images/answernotfound.png differ diff --git a/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/assets/images/attached.svg b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/assets/images/attached.svg new file mode 100644 index 0000000..ba09784 --- /dev/null +++ b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/assets/images/attached.svg @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/assets/images/available.svg b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/assets/images/available.svg new file mode 100644 index 0000000..5a460d0 --- /dev/null +++ b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/assets/images/available.svg @@ -0,0 +1,4 @@ + + + + diff --git a/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/assets/images/badge-banner.svg b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/assets/images/badge-banner.svg new file mode 100644 index 0000000..a38efec --- /dev/null +++ b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/assets/images/badge-banner.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/assets/images/badge.svg b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/assets/images/badge.svg new file mode 100644 index 0000000..3fc6b94 --- /dev/null +++ b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/assets/images/badge.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/assets/images/books.png b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/assets/images/books.png new file mode 100644 index 0000000..94feabc Binary files /dev/null and b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/assets/images/books.png differ diff --git a/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/assets/images/bot_logo.svg b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/assets/images/bot_logo.svg new file mode 100644 index 0000000..4be6c2f --- /dev/null +++ b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/assets/images/bot_logo.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/assets/images/cancel-toast.svg b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/assets/images/cancel-toast.svg new file mode 100644 index 0000000..9db4780 --- /dev/null +++ b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/assets/images/cancel-toast.svg @@ -0,0 +1,4 @@ + + + + diff --git a/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/assets/images/chatIcon.svg b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/assets/images/chatIcon.svg new file mode 100644 index 0000000..3303847 --- /dev/null +++ b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/assets/images/chatIcon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/assets/images/chat_background.png b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/assets/images/chat_background.png new file mode 100644 index 0000000..701ec81 Binary files /dev/null and b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/assets/images/chat_background.png differ diff --git a/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/assets/images/cross-in-circle-white copy.svg b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/assets/images/cross-in-circle-white copy.svg new file mode 100644 index 0000000..96a0fe3 --- /dev/null +++ b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/assets/images/cross-in-circle-white copy.svg @@ -0,0 +1,3 @@ + + + diff --git a/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/assets/images/cross-in-circle-white.svg b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/assets/images/cross-in-circle-white.svg new file mode 100644 index 0000000..96a0fe3 --- /dev/null +++ b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/assets/images/cross-in-circle-white.svg @@ -0,0 +1,3 @@ + + + diff --git a/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/assets/images/cross-in-circle.svg b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/assets/images/cross-in-circle.svg new file mode 100644 index 0000000..c5c50ea --- /dev/null +++ b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/assets/images/cross-in-circle.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/assets/images/delete_notification_dialog_image.png b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/assets/images/delete_notification_dialog_image.png new file mode 100644 index 0000000..5afbf5c Binary files /dev/null and b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/assets/images/delete_notification_dialog_image.png differ diff --git a/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/assets/images/error_img.svg b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/assets/images/error_img.svg new file mode 100644 index 0000000..ac20193 --- /dev/null +++ b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/assets/images/error_img.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/assets/images/feedback-container.svg b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/assets/images/feedback-container.svg new file mode 100644 index 0000000..7b0f481 --- /dev/null +++ b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/assets/images/feedback-container.svg @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/assets/images/footer.png b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/assets/images/footer.png new file mode 100644 index 0000000..eda0b3b Binary files /dev/null and b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/assets/images/footer.png differ diff --git a/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/assets/images/gcloudlight1.png b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/assets/images/gcloudlight1.png new file mode 100644 index 0000000..5ddb77c Binary files /dev/null and b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/assets/images/gcloudlight1.png differ diff --git a/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/assets/images/gnosis-logo.svg b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/assets/images/gnosis-logo.svg new file mode 100644 index 0000000..17d0b7f --- /dev/null +++ b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/assets/images/gnosis-logo.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/assets/images/gsd-icon.png b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/assets/images/gsd-icon.png new file mode 100644 index 0000000..abeacf9 Binary files /dev/null and b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/assets/images/gsd-icon.png differ diff --git a/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/assets/images/home_bottom.png b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/assets/images/home_bottom.png new file mode 100644 index 0000000..78d1c92 Binary files /dev/null and b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/assets/images/home_bottom.png differ diff --git a/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/assets/images/left_loader_images/Left/1.png b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/assets/images/left_loader_images/Left/1.png new file mode 100644 index 0000000..d930ffb Binary files /dev/null and b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/assets/images/left_loader_images/Left/1.png differ diff --git a/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/assets/images/left_loader_images/Left/2.png b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/assets/images/left_loader_images/Left/2.png new file mode 100644 index 0000000..ac32f6a Binary files /dev/null and b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/assets/images/left_loader_images/Left/2.png differ diff --git a/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/assets/images/left_loader_images/Left/3.png b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/assets/images/left_loader_images/Left/3.png new file mode 100644 index 0000000..cee4db0 Binary files /dev/null and b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/assets/images/left_loader_images/Left/3.png differ diff --git a/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/assets/images/left_loader_images/Left/4.png b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/assets/images/left_loader_images/Left/4.png new file mode 100644 index 0000000..77a6d94 Binary files /dev/null and b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/assets/images/left_loader_images/Left/4.png differ diff --git a/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/assets/images/left_loader_images/Left/5.png b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/assets/images/left_loader_images/Left/5.png new file mode 100644 index 0000000..deba49b Binary files /dev/null and b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/assets/images/left_loader_images/Left/5.png differ diff --git a/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/assets/images/left_loader_images/Left/6.png b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/assets/images/left_loader_images/Left/6.png new file mode 100644 index 0000000..61e1988 Binary files /dev/null and b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/assets/images/left_loader_images/Left/6.png differ diff --git a/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/assets/images/login-globe.png b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/assets/images/login-globe.png new file mode 100644 index 0000000..f64021b Binary files /dev/null and b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/assets/images/login-globe.png differ diff --git a/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/assets/images/login_left.svg b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/assets/images/login_left.svg new file mode 100644 index 0000000..397f17f --- /dev/null +++ b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/assets/images/login_left.svg @@ -0,0 +1,101 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/assets/images/new-pdc-logo.svg b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/assets/images/new-pdc-logo.svg new file mode 100644 index 0000000..c267930 --- /dev/null +++ b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/assets/images/new-pdc-logo.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/assets/images/noPic.svg b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/assets/images/noPic.svg new file mode 100644 index 0000000..0f9c09d --- /dev/null +++ b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/assets/images/noPic.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/assets/images/phoneIcon.png b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/assets/images/phoneIcon.png new file mode 100644 index 0000000..a0af511 Binary files /dev/null and b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/assets/images/phoneIcon.png differ diff --git a/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/assets/images/right_loader_images/Right/1.png b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/assets/images/right_loader_images/Right/1.png new file mode 100644 index 0000000..c571db1 Binary files /dev/null and b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/assets/images/right_loader_images/Right/1.png differ diff --git a/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/assets/images/right_loader_images/Right/2.png b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/assets/images/right_loader_images/Right/2.png new file mode 100644 index 0000000..d6a1da0 Binary files /dev/null and b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/assets/images/right_loader_images/Right/2.png differ diff --git a/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/assets/images/right_loader_images/Right/3.png b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/assets/images/right_loader_images/Right/3.png new file mode 100644 index 0000000..042ba23 Binary files /dev/null and b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/assets/images/right_loader_images/Right/3.png differ diff --git a/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/assets/images/right_loader_images/Right/4.png b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/assets/images/right_loader_images/Right/4.png new file mode 100644 index 0000000..96c067a Binary files /dev/null and b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/assets/images/right_loader_images/Right/4.png differ diff --git a/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/assets/images/right_loader_images/Right/5.png b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/assets/images/right_loader_images/Right/5.png new file mode 100644 index 0000000..3a15579 Binary files /dev/null and b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/assets/images/right_loader_images/Right/5.png differ diff --git a/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/assets/images/right_loader_images/Right/6.png b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/assets/images/right_loader_images/Right/6.png new file mode 100644 index 0000000..7de6698 Binary files /dev/null and b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/assets/images/right_loader_images/Right/6.png differ diff --git a/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/assets/images/searchIcon.png b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/assets/images/searchIcon.png new file mode 100644 index 0000000..7b95f3c Binary files /dev/null and b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/assets/images/searchIcon.png differ diff --git a/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/assets/images/searchIcon.svg b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/assets/images/searchIcon.svg new file mode 100644 index 0000000..b82fe2f --- /dev/null +++ b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/assets/images/searchIcon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/assets/images/support-page-banner.svg b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/assets/images/support-page-banner.svg new file mode 100644 index 0000000..0fa6347 --- /dev/null +++ b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/assets/images/support-page-banner.svg @@ -0,0 +1,134 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/assets/images/tick-with-circle.svg b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/assets/images/tick-with-circle.svg new file mode 100644 index 0000000..8335e7c --- /dev/null +++ b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/assets/images/tick-with-circle.svg @@ -0,0 +1,4 @@ + + + + diff --git a/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/assets/images/unnamed.jpeg b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/assets/images/unnamed.jpeg new file mode 100644 index 0000000..b33f75d Binary files /dev/null and b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/assets/images/unnamed.jpeg differ diff --git a/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/assets/images/user-logo.svg b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/assets/images/user-logo.svg new file mode 100644 index 0000000..8135e28 --- /dev/null +++ b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/assets/images/user-logo.svg @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/assets/images/voiceIcon.svg b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/assets/images/voiceIcon.svg new file mode 100644 index 0000000..9c3e1d3 --- /dev/null +++ b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/assets/images/voiceIcon.svg @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/assets/images/warning-yellow.png b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/assets/images/warning-yellow.png new file mode 100644 index 0000000..e94f265 Binary files /dev/null and b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/assets/images/warning-yellow.png differ diff --git a/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/environments/environment.ts b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/environments/environment.ts new file mode 100644 index 0000000..3b04dba --- /dev/null +++ b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/environments/environment.ts @@ -0,0 +1,31 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export const environment = { + firebase: { + apiKey: '', + authDomain: '', + projectId: '', + storageBucket: '', + messagingSenderId: '', + appId: '', + measurementId: '', + }, + requiredLogin: 'False', + backendURL: 'http://localhost:8080/api', + chatbotName: 'My New Agent', + environment: 'development', +}; diff --git a/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/favicon.ico b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/favicon.ico new file mode 100644 index 0000000..997406a Binary files /dev/null and b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/favicon.ico differ diff --git a/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/index.html b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/index.html new file mode 100644 index 0000000..d1b75a6 --- /dev/null +++ b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/index.html @@ -0,0 +1,40 @@ + + + + + + + + Quick Bot + + + + + + + + + + + + + + + + diff --git a/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/main.ts b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/main.ts new file mode 100644 index 0000000..0da5f95 --- /dev/null +++ b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/main.ts @@ -0,0 +1,23 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {platformBrowserDynamic} from '@angular/platform-browser-dynamic'; + +import {AppModule} from './app/app.module'; + +platformBrowserDynamic() + .bootstrapModule(AppModule) + .catch(err => console.error(err)); diff --git a/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/styles.scss b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/styles.scss new file mode 100644 index 0000000..50cb6b6 --- /dev/null +++ b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/src/styles.scss @@ -0,0 +1,76 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* You can add global styles to this file, and also import other style files */ + +html, body { height: 100%;margin: 0; font-family: 'Google Sans', sans-serif !important; } + +.material-symbols-rounded { +font-variation-settings: +'FILL', +'wght' 600, +'GRAD' 0, +'opsz' 48 +} + +.tooltip-list .mdc-tooltip__surface { + white-space: pre; + max-width: unset !important; +} + +.mat-mdc-slide-toggle.mat-accent { + --mdc-switch-selected-focus-state-layer-color: #4285F4; + --mdc-switch-selected-handle-color: #4285F4; + --mdc-switch-selected-hover-state-layer-color: #4285F4; + --mdc-switch-selected-pressed-state-layer-color: #4285F4; + --mdc-switch-selected-focus-handle-color: #3f74e0; + --mdc-switch-selected-hover-handle-color: #3f74e0; + --mdc-switch-selected-pressed-handle-color: #3f74e0; + --mdc-switch-selected-focus-track-color: rgba(66, 133, 244, 0.3); + --mdc-switch-selected-hover-track-color: rgba(66, 133, 244, 0.3); + --mdc-switch-selected-pressed-track-color: rgba(66, 133, 244, 0.3); + --mdc-switch-selected-track-color: rgba(66, 133, 244, 0.3); +} + +.arrow-top { + box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.1) !important; + border-radius: 20px !important; +} + +.mat-mdc-checkbox.mat-accent { + --mdc-checkbox-selected-checkmark-color: #fff; + --mdc-checkbox-selected-focus-icon-color: #4285F4; + --mdc-checkbox-selected-hover-icon-color: #4285F4; + --mdc-checkbox-selected-icon-color: #4285F4; + --mdc-checkbox-selected-pressed-icon-color: #4285F4; + --mdc-checkbox-unselected-focus-icon-color: #212121; + --mdc-checkbox-unselected-hover-icon-color: #212121; + --mdc-checkbox-disabled-selected-icon-color: rgba(0, 0, 0, 0.38); + --mdc-checkbox-disabled-unselected-icon-color: rgba(0, 0, 0, 0.38); + --mdc-checkbox-unselected-icon-color: rgba(0, 0, 0, 0.54); + --mdc-checkbox-unselected-pressed-icon-color: rgba(0, 0, 0, 0.54); +} + +$tour-skip-link-color: red; +$tour-text-color: purple; +$tour-next-text-color: yellow; +$tour-zindex: 100; +$tour-orb-color: cyan; +$tour-next-button-color: green; +$tour-next-button-hover: black; +$tour-back-button-color: white; +$tour-shadow-color: gray; +$tour-step-color: aliceblue; diff --git a/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/tsconfig.app.json b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/tsconfig.app.json new file mode 100644 index 0000000..374cc9d --- /dev/null +++ b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/tsconfig.app.json @@ -0,0 +1,14 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "./out-tsc/app", + "types": [] + }, + "files": [ + "src/main.ts" + ], + "include": [ + "src/**/*.d.ts" + ] +} diff --git a/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/tsconfig.json b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/tsconfig.json new file mode 100644 index 0000000..74167f2 --- /dev/null +++ b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/tsconfig.json @@ -0,0 +1,35 @@ +{ + "extends": "./node_modules/gts/tsconfig-google.json", + "compileOnSave": false, + "compilerOptions": { + "ignoreDeprecations": "5.0", + "baseUrl": "./", + "outDir": "./dist/out-tsc", + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "downlevelIteration": true, + "experimentalDecorators": true, + "moduleResolution": "node", + "importHelpers": true, + "target": "ES2022", + "module": "ES2022", + "useDefineForClassFields": false, + "lib": [ + "ES2022", + "dom" + ], + }, + "angularCompilerOptions": { + "enableI18nLegacyMessageIdFormat": false, + "strictInjectionParameters": true, + "strictInputAccessModifiers": true, + "strictTemplates": true, + "suppressImplicitAnyIndexErrors": true, + "strictPropertyInitialization": false, + }, + "include": [ + "src/**/*.ts", + "test/**/*.ts" + ] +} + diff --git a/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/tsconfig.spec.json b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/tsconfig.spec.json new file mode 100644 index 0000000..be7e9da --- /dev/null +++ b/gemini/sample-apps/quickbot/website-search-using-agent-builder/frontend/tsconfig.spec.json @@ -0,0 +1,14 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "./out-tsc/spec", + "types": [ + "jasmine" + ] + }, + "include": [ + "src/**/*.spec.ts", + "src/**/*.d.ts" + ] +} diff --git a/gemini/sample-apps/quickbot/website-search-using-agent-builder/pylintrc b/gemini/sample-apps/quickbot/website-search-using-agent-builder/pylintrc new file mode 100644 index 0000000..cd99157 --- /dev/null +++ b/gemini/sample-apps/quickbot/website-search-using-agent-builder/pylintrc @@ -0,0 +1,399 @@ +# This Pylint rcfile contains a best-effort configuration to uphold the +# best-practices and style described in the Google Python style guide: +# https://google.github.io/styleguide/pyguide.html +# +# Its canonical open-source location is: +# https://google.github.io/styleguide/pylintrc + +[MAIN] + +# Files or directories to be skipped. They should be base names, not paths. +ignore=third_party + +# Files or directories matching the regex patterns are skipped. The regex +# matches against base names, not paths. +ignore-patterns= + +# Pickle collected data for later comparisons. +persistent=no + +# List of plugins (as comma separated values of python modules names) to load, +# usually to register additional checkers. +load-plugins= + +# Use multiple processes to speed up Pylint. +jobs=4 + +# Allow loading of arbitrary C extensions. Extensions are imported into the +# active Python interpreter and may run arbitrary code. +unsafe-load-any-extension=no + + +[MESSAGES CONTROL] + +# Only show warnings with the listed confidence levels. Leave empty to show +# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED +confidence= + +# Enable the message, report, category or checker with the given id(s). You can +# either give multiple identifier separated by comma (,) or put this option +# multiple time (only on the command line, not in the configuration file where +# it should appear only once). See also the "--disable" option for examples. +#enable= + +# Disable the message, report, category or checker with the given id(s). You +# can either give multiple identifiers separated by comma (,) or put this +# option multiple times (only on the command line, not in the configuration +# file where it should appear only once).You can also use "--disable=all" to +# disable everything first and then reenable specific checks. For example, if +# you want to run only the similarities checker, you can use "--disable=all +# --enable=similarities". If you want to run only the classes checker, but have +# no Warning level messages displayed, use"--disable=all --enable=classes +# --disable=W" +disable=R, + abstract-method, + apply-builtin, + arguments-differ, + attribute-defined-outside-init, + backtick, + bad-option-value, + basestring-builtin, + buffer-builtin, + c-extension-no-member, + consider-using-enumerate, + cmp-builtin, + cmp-method, + coerce-builtin, + coerce-method, + delslice-method, + div-method, + eq-without-hash, + execfile-builtin, + file-builtin, + filter-builtin-not-iterating, + fixme, + getslice-method, + global-statement, + hex-method, + idiv-method, + implicit-str-concat, + import-error, + import-self, + import-star-module-level, + input-builtin, + intern-builtin, + invalid-str-codec, + locally-disabled, + long-builtin, + long-suffix, + map-builtin-not-iterating, + misplaced-comparison-constant, + missing-function-docstring, + metaclass-assignment, + next-method-called, + next-method-defined, + no-absolute-import, + no-init, # added + no-member, + no-name-in-module, + no-self-use, + nonzero-method, + oct-method, + old-division, + old-ne-operator, + old-octal-literal, + old-raise-syntax, + parameter-unpacking, + print-statement, + raising-string, + range-builtin-not-iterating, + raw_input-builtin, + rdiv-method, + reduce-builtin, + relative-import, + reload-builtin, + round-builtin, + setslice-method, + signature-differs, + standarderror-builtin, + suppressed-message, + sys-max-int, + trailing-newlines, + unichr-builtin, + unicode-builtin, + unnecessary-pass, + unpacking-in-except, + useless-else-on-loop, + useless-suppression, + using-cmp-argument, + wrong-import-order, + xrange-builtin, + zip-builtin-not-iterating, + + +[REPORTS] + +# Set the output format. Available formats are text, parseable, colorized, msvs +# (visual studio) and html. You can also give a reporter class, eg +# mypackage.mymodule.MyReporterClass. +output-format=text + +# Tells whether to display a full report or only the messages +reports=no + +# Python expression which should return a note less than 10 (10 is the highest +# note). You have access to the variables errors warning, statement which +# respectively contain the number of errors / warnings messages and the total +# number of statements analyzed. This is used by the global evaluation report +# (RP0004). +evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) + +# Template used to display messages. This is a python new-style format string +# used to format the message information. See doc for all details +#msg-template= + + +[BASIC] + +# Good variable names which should always be accepted, separated by a comma +good-names=main,_ + +# Bad variable names which should always be refused, separated by a comma +bad-names= + +# Colon-delimited sets of names that determine each other's naming style when +# the name regexes allow several styles. +name-group= + +# Include a hint for the correct naming format with invalid-name +include-naming-hint=no + +# List of decorators that produce properties, such as abc.abstractproperty. Add +# to this list to register other decorators that produce valid properties. +property-classes=abc.abstractproperty,cached_property.cached_property,cached_property.threaded_cached_property,cached_property.cached_property_with_ttl,cached_property.threaded_cached_property_with_ttl + +# Regular expression matching correct function names +function-rgx=^(?:(?PsetUp|tearDown|setUpModule|tearDownModule)|(?P_?[A-Z][a-zA-Z0-9]*)|(?P_?[a-z][a-z0-9_]*))$ + +# Regular expression matching correct variable names +variable-rgx=^[a-z][a-z0-9_]*$ + +# Regular expression matching correct constant names +const-rgx=^(_?[A-Z][A-Z0-9_]*|__[a-z0-9_]+__|_?[a-z][a-z0-9_]*)$ + +# Regular expression matching correct attribute names +attr-rgx=^_{0,2}[a-z][a-z0-9_]*$ + +# Regular expression matching correct argument names +argument-rgx=^[a-z][a-z0-9_]*$ + +# Regular expression matching correct class attribute names +class-attribute-rgx=^(_?[A-Z][A-Z0-9_]*|__[a-z0-9_]+__|_?[a-z][a-z0-9_]*)$ + +# Regular expression matching correct inline iteration names +inlinevar-rgx=^[a-z][a-z0-9_]*$ + +# Regular expression matching correct class names +class-rgx=^_?[A-Z][a-zA-Z0-9]*$ + +# Regular expression matching correct module names +module-rgx=^(_?[a-z][a-z0-9_]*|__init__)$ + +# Regular expression matching correct method names +method-rgx=(?x)^(?:(?P_[a-z0-9_]+__|runTest|setUp|tearDown|setUpTestCase|tearDownTestCase|setupSelf|tearDownClass|setUpClass|(test|assert)_*[A-Z0-9][a-zA-Z0-9_]*|next)|(?P_{0,2}[A-Z][a-zA-Z0-9_]*)|(?P_{0,2}[a-z][a-z0-9_]*))$ + +# Regular expression which should only match function or class names that do +# not require a docstring. +no-docstring-rgx=(__.*__|main|test.*|.*test|.*Test)$ + +# Minimum line length for functions/classes that require docstrings, shorter +# ones are exempt. +docstring-min-length=12 + + +[TYPECHECK] + +# List of decorators that produce context managers, such as +# contextlib.contextmanager. Add to this list to register other decorators that +# produce valid context managers. +contextmanager-decorators=contextlib.contextmanager,contextlib2.contextmanager + +# List of module names for which member attributes should not be checked +# (useful for modules/projects where namespaces are manipulated during runtime +# and thus existing member attributes cannot be deduced by static analysis. It +# supports qualified module names, as well as Unix pattern matching. +ignored-modules= + +# List of class names for which member attributes should not be checked (useful +# for classes with dynamically set attributes). This supports the use of +# qualified names. +ignored-classes=optparse.Values,thread._local,_thread._local + +# List of members which are set dynamically and missed by pylint inference +# system, and so shouldn't trigger E1101 when accessed. Python regular +# expressions are accepted. +generated-members= + + +[FORMAT] + +# Maximum number of characters on a single line. +max-line-length=80 + +# TODO(https://github.com/pylint-dev/pylint/issues/3352): Direct pylint to exempt +# lines made too long by directives to pytype. + +# Regexp for a line that is allowed to be longer than the limit. +ignore-long-lines=(?x)( + ^\s*(\#\ )??$| + ^\s*(from\s+\S+\s+)?import\s+.+$) + +# Allow the body of an if to be on the same line as the test if there is no +# else. +single-line-if-stmt=yes + +# Maximum number of lines in a module +max-module-lines=99999 + +# String used as indentation unit. The internal Google style guide mandates 2 +# spaces. Google's externaly-published style guide says 4, consistent with +# PEP 8. Here, we use 2 spaces, for conformity with many open-sourced Google +# projects (like TensorFlow). +indent-string=' ' + +# Number of spaces of indent required inside a hanging or continued line. +indent-after-paren=4 + +# Expected format of line ending, e.g. empty (any line ending), LF or CRLF. +expected-line-ending-format= + + +[MISCELLANEOUS] + +# List of note tags to take in consideration, separated by a comma. +notes=TODO + + +[STRING] + +# This flag controls whether inconsistent-quotes generates a warning when the +# character used as a quote delimiter is used inconsistently within a module. +check-quote-consistency=yes + + +[VARIABLES] + +# Tells whether we should check for unused import in __init__ files. +init-import=no + +# A regular expression matching the name of dummy variables (i.e. expectedly +# not used). +dummy-variables-rgx=^\*{0,2}(_$|unused_|dummy_) + +# List of additional names supposed to be defined in builtins. Remember that +# you should avoid to define new builtins when possible. +additional-builtins= + +# List of strings which can identify a callback function by name. A callback +# name must start or end with one of those strings. +callbacks=cb_,_cb + +# List of qualified module names which can have objects that can redefine +# builtins. +redefining-builtins-modules=six,six.moves,past.builtins,future.builtins,functools + + +[LOGGING] + +# Logging modules to check that the string format arguments are in logging +# function parameter format +logging-modules=logging,absl.logging,tensorflow.io.logging + + +[SIMILARITIES] + +# Minimum lines number of a similarity. +min-similarity-lines=4 + +# Ignore comments when computing similarities. +ignore-comments=yes + +# Ignore docstrings when computing similarities. +ignore-docstrings=yes + +# Ignore imports when computing similarities. +ignore-imports=no + + +[SPELLING] + +# Spelling dictionary name. Available dictionaries: none. To make it working +# install python-enchant package. +spelling-dict= + +# List of comma separated words that should not be checked. +spelling-ignore-words= + +# A path to a file that contains private dictionary; one word per line. +spelling-private-dict-file= + +# Tells whether to store unknown words to indicated private dictionary in +# --spelling-private-dict-file option instead of raising a message. +spelling-store-unknown-words=no + + +[IMPORTS] + +# Deprecated modules which should not be used, separated by a comma +deprecated-modules=regsub, + TERMIOS, + Bastion, + rexec, + sets + +# Create a graph of every (i.e. internal and external) dependencies in the +# given file (report RP0402 must not be disabled) +import-graph= + +# Create a graph of external dependencies in the given file (report RP0402 must +# not be disabled) +ext-import-graph= + +# Create a graph of internal dependencies in the given file (report RP0402 must +# not be disabled) +int-import-graph= + +# Force import order to recognize a module as part of the standard +# compatibility libraries. +known-standard-library= + +# Force import order to recognize a module as part of a third party library. +known-third-party=enchant, absl + +# Analyse import fallback blocks. This can be used to support both Python 2 and +# 3 compatible code, which means that the block might have code that exists +# only in one or another interpreter, leading to false positives when analysed. +analyse-fallback-blocks=no + + +[CLASSES] + +# List of method names used to declare (i.e. assign) instance attributes. +defining-attr-methods=__init__, + __new__, + setUp + +# List of member names, which should be excluded from the protected access +# warning. +exclude-protected=_asdict, + _fields, + _replace, + _source, + _make + +# List of valid names for the first argument in a class method. +valid-classmethod-first-arg=cls, + class_ + +# List of valid names for the first argument in a metaclass class method. +valid-metaclass-classmethod-first-arg=mcs diff --git a/gemini/sample-apps/seq_flask.png b/gemini/sample-apps/seq_flask.png new file mode 100644 index 0000000..a2a3109 Binary files /dev/null and b/gemini/sample-apps/seq_flask.png differ diff --git a/gemini/sample-apps/swot-agent/.dockerignore b/gemini/sample-apps/swot-agent/.dockerignore new file mode 100644 index 0000000..550ea34 --- /dev/null +++ b/gemini/sample-apps/swot-agent/.dockerignore @@ -0,0 +1,7 @@ + __pycache__ + *.pyc + *.pyo + .git + .gitignore + tests/ + .pytest_cache/ diff --git a/gemini/sample-apps/swot-agent/Dockerfile b/gemini/sample-apps/swot-agent/Dockerfile new file mode 100644 index 0000000..1452b6b --- /dev/null +++ b/gemini/sample-apps/swot-agent/Dockerfile @@ -0,0 +1,10 @@ +FROM python:3.13-slim + +WORKDIR /app + +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +COPY . . + +CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8080"] diff --git a/gemini/sample-apps/swot-agent/LICENSE b/gemini/sample-apps/swot-agent/LICENSE new file mode 100644 index 0000000..7a4a3ea --- /dev/null +++ b/gemini/sample-apps/swot-agent/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. \ No newline at end of file diff --git a/gemini/sample-apps/swot-agent/README.md b/gemini/sample-apps/swot-agent/README.md new file mode 100644 index 0000000..ddeed6b --- /dev/null +++ b/gemini/sample-apps/swot-agent/README.md @@ -0,0 +1,130 @@ +# swot-agent + +[![Python 3.10+](https://img.shields.io/badge/python-3.10+-blue.svg?logo=python&logoColor=white)](https://www.python.org/downloads/release/python-3100/) +[![FastAPI](https://img.shields.io/badge/FastAPI-0.115.6+-green.svg)](https://fastapi.tiangolo.com) +[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) + +A web application that performs automated [SWOT analysis](https://en.wikipedia.org/wiki/SWOT_analysis) (Strengths, Weaknesses, Opportunities, Threats) analysis using the [Gemini 2.0 Flash model](https://ai.google.dev/gemini-api/docs/models/gemini-v2) and the [Pydantic AI](https://ai.pydantic.dev/) agent framework. The application is built with [FastAPI](https://fastapi.tiangolo.com/), [HTMX](https://htmx.org/), and [Tailwind CSS](https://tailwindcss.com/). + +![SWOT Analysis Demo](https://storage.googleapis.com/github-repo/generative-ai/sample-apps/swot-agent/swot-agent.gif) + +The agent includes three tools: + +- **Content Extraction**: Extracts content from a web page given a URL +- **Community Insights**: Calls Reddit API to get community insights from relevant subreddits +- **Competitive Analysis**: Calls Gemini API to get competitive analysis + +## Getting Started + +### Prerequisites + +- Python 3.10+ +- Google Cloud API credentials +- Optional: [Reddit API credentials](https://www.reddit.com/prefs/apps) (for Reddit content extraction) + +### Installation + +1. Clone the repository and change to the `swot-agent` directory. + +1. Install the required dependencies: + + ```bash + pip install -r requirements.txt + ``` + +1. Set up your environment variables: + + ```bash + # Google Cloud settings + export GOOGLE_CLOUD_PROJECT=your_project_id + export GOOGLE_APPLICATION_CREDENTIALS=path_to_service_account.json + + # (Optional) Reddit API credentials + export REDDIT_CLIENT_ID=your_reddit_client_id + export REDDIT_CLIENT_SECRET=your_reddit_client_secret + + # (Optional) Application settings + export APP_SECRET_KEY=your_secret_key + ``` + +1. Run the application: + + ```bash + python main.py + ``` + + You can also use the [FastAPI CLI](https://fastapi.tiangolo.com/fastapi-cli/): + + ```bash + fastapi dev --port 8080 + ``` + +1. Open your web browser and navigate to `http://localhost:8080`. + +## Usage + +1. Enter a valid URL in the input field +1. Click **"Analyze"** to initiate the AI SWOT analysis +1. The AI agent will: + - Extract content from the provided URL + - Process the content using Gemini 2.0 + - Generate structured SWOT insights + - Present results in an organized format +1. View the SWOT analysis results + +## Deployment + +To deploy the application to [Google Cloud Run](https://cloud.google.com/run), run the following command: + +```bash +gcloud run deploy swot-agent --source . --region us-central1 --allow-unauthenticated +``` + +You may need to add the `aiplatform.user` [IAM role](https://cloud.google.com/vertex-ai/docs/general/access-control#aiplatform.user) to your service account. + +[Configure secrets](https://cloud.google.com/run/docs/configuring/services/secrets) for the `APP_SECRET_KEY`, `REDDIT_CLIENT_ID`, and `REDDIT_CLIENT_SECRET`. You can run the application without setting these, but the Reddit tool will not be available. + +## Troubleshooting + +If you receive an error about the Gemini quota being exceeded, you can [request a quota increase](https://cloud.google.com/vertex-ai/docs/generative-ai/quotas-genai) or try another model. + +## Testing + +The project includes test suites for both the AI agent and the FastAPI application in the `tests` directory. + +### Running Tests + +1. Install test dependencies: + + ```bash + pip install pytest pytest-asyncio httpx + ``` + +1. Run all tests: + + ```bash + pytest -v + ``` + +## Project Structure + +```text +swot-agent/ +├── main.py # FastAPI application and server setup +├── agent.py # SWOT analysis agent implementation +├── tests/ # Test suites +│ ├── __init__.py # To make tests a Python package +│ ├── test_agent.py # AI agent test suite +│ └── test_main.py # FastAPI endpoint test suite +├── templates/ # HTML templates +│ ├── index.html # Main application page +│ ├── status.html # Analysis status updates +│ └── result.html # SWOT analysis results +├── requirements.txt # Python dependencies +├── LICENSE # License information +└── README.md # Project documentation +``` + +## Contributing + +Contributions are welcome! Please feel free to submit a Pull Request. diff --git a/gemini/sample-apps/swot-agent/agent.py b/gemini/sample-apps/swot-agent/agent.py new file mode 100644 index 0000000..92dabdb --- /dev/null +++ b/gemini/sample-apps/swot-agent/agent.py @@ -0,0 +1,305 @@ +# Copyright 2024 Google, LLC. +# This software is provided as-is, without warranty +# or representation for any use or purpose. +# Your use of it is subject to your agreement with Google. + +# 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 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import asyncio +from dataclasses import dataclass, field +import logging +import os +import sys +from typing import Any, Callable, List, Optional + +from bs4 import BeautifulSoup +from google import genai +import httpx +import praw +from pydantic import BaseModel, Field +from pydantic_ai import Agent, ModelRetry, RunContext +from pydantic_ai.models.vertexai import VertexAIModel +from pydantic_ai.tools import ToolDefinition + +# --- General Configuration --- +USER_AGENT = "SwotAgent/1.0" +ANALYZE_URL = "https://wikipedia.org" +RETRIES = 3 +logging.basicConfig(level=logging.INFO) +sys.excepthook = lambda *args: None + +# --- Google Cloud Configuration --- +PROJECT_ID = os.environ.get("GOOGLE_CLOUD_PROJECT") +SERVICE_ACCOUNT_FILE = os.environ.get("GOOGLE_APPLICATION_CREDENTIALS") +LOCATION = "us-central1" +MODEL = "gemini-2.0-flash-live-preview-04-09" + +# --- Reddit API Configuration --- +REDDIT_MAX_INSIGHTS = 5 +REDDIT_MAX_INSIGHT_LENGTH = 400 +REDDIT_CLIENT_ID = os.environ.get("REDDIT_CLIENT_ID") +REDDIT_CLIENT_SECRET = os.environ.get("REDDIT_CLIENT_SECRET") + + +@dataclass +class SwotAgentDeps: + """Dependencies for the SwotAgent.""" + + request: Optional[Any] = None + update_status_func: Optional[Callable] = None + tool_history: List[str] = field(default_factory=list) + try: + reddit: praw.Reddit = praw.Reddit( + client_id=REDDIT_CLIENT_ID, + client_secret=REDDIT_CLIENT_SECRET, + user_agent=USER_AGENT, + check_for_async=False, + ) + except praw.exceptions.PRAWException as e: + reddit = None + logging.info( + f"Reddit client not initialized. Please set the REDDIT_CLIENT_ID and REDDIT_CLIENT_SECRET environment variables: {e}" + ) + try: + client = genai.Client(vertexai=True, project=PROJECT_ID, location=LOCATION) + except ValueError as e: + client = None + logging.info( + f"Gemini client not initialized. Please set the GOOGLE_CLOUD_PROJECT and GOOGLE_APPLICATION_CREDENTIALS environment variables: {e}" + ) + + +class SwotAnalysis(BaseModel): + """Represents a SWOT analysis with strengths, weaknesses, opportunities, threats, and an overall analysis.""" + + strengths: List[str] = Field( + description="Internal strengths of the product/service" + ) + weaknesses: List[str] = Field( + description="Internal weaknesses of the product/service" + ) + opportunities: List[str] = Field(description="External opportunities in the market") + threats: List[str] = Field(description="External threats in the market") + analysis: str = Field( + description="A comprehensive analysis explaining the SWOT findings and their implications" + ) + + +async def report_tool_usage( + ctx: RunContext[SwotAgentDeps], tool_def: ToolDefinition +) -> ToolDefinition: + """Reports tool usage and results to update_status_func.""" + + if tool_def.name in ctx.deps.tool_history: + # Tool has already been used and reported + return tool_def + + if ctx.deps.update_status_func: + await ctx.deps.update_status_func( + ctx.deps.request, + f"Using tool: {tool_def.name}...", + ) + ctx.deps.tool_history.append(tool_def.name) + + return tool_def + + +swot_agent = Agent( + model=VertexAIModel( + model_name=MODEL, + service_account_file=SERVICE_ACCOUNT_FILE, + project_id=PROJECT_ID, + region=LOCATION, + ), + deps_type=SwotAgentDeps, + result_type=SwotAnalysis, + system_prompt=""" + You are a strategic business analyst tasked with performing SWOT analysis. + Analyze the given URL, identify internal strengths and weaknesses, + and evaluate external opportunities and threats based on market conditions + and competitive landscape. Use community insights to validate findings. + + For each category: + - Strengths: Focus on internal advantages and unique selling points + - Weaknesses: Identify internal limitations and areas for improvement + - Opportunities: Analyze external factors that could be advantageous + - Threats: Evaluate external challenges and competitive pressures + + Provide a detailed analysis that synthesizes these findings into actionable insights. + """, + retries=RETRIES, +) + + +@swot_agent.result_validator +def validate_analysis( + _ctx: RunContext[SwotAgentDeps], value: SwotAnalysis +) -> SwotAnalysis: + """Validates the SWOT analysis for completeness and quality.""" + issues = [] + + # Check minimum number of points in each category + min_points = 2 + categories = { + "Strengths": value.strengths, + "Weaknesses": value.weaknesses, + "Opportunities": value.opportunities, + "Threats": value.threats, + } + + for category_name, points in categories.items(): + if len(points) < min_points: + issues.append( + f"{category_name}: Should have at least {min_points} points. Currently has {len(points)}." + ) + + # Check analysis length + min_analysis_length = 100 + if len(value.analysis) < min_analysis_length: + issues.append( + f"Analysis should be at least {min_analysis_length} characters. Currently {len(value.analysis)} characters." + ) + + if issues: + logging.info(f"Validation issues: {issues}") + raise ModelRetry("\n".join(issues)) + + return value + + +# --- Tools --- +@swot_agent.tool(prepare=report_tool_usage) +async def fetch_website_content(_ctx: RunContext[SwotAgentDeps], url: str) -> str: + """Fetches the HTML content of the given URL.""" + logging.info(f"Fetching website content for: {url}") + async with httpx.AsyncClient(follow_redirects=True) as http_client: + try: + response = await http_client.get(url) + response.raise_for_status() + html_content = response.text + soup = BeautifulSoup(html_content, "html.parser") + text_content = soup.get_text(separator=" ", strip=True) + return text_content + except httpx.HTTPError as e: + logging.info(f"Request failed: {e}") + raise + + +# pylint: disable=W0718 +@swot_agent.tool(prepare=report_tool_usage) +async def analyze_competition( + ctx: RunContext[SwotAgentDeps], + product_name: str, + product_description: str, +) -> str: + """Analyzes the competition for the given product using the Gemini model.""" + logging.info(f"Analyzing competition for: {product_name}") + + prompt = f""" + You are a competitive analysis expert. Analyze the competition for the following product: + Product Name: {product_name} + Product Description: {product_description} + + Provide a detailed analysis of: + 1. Key competitors and their market position + 2. Competitive advantages and disadvantages + 3. Market trends and potential disruptions + 4. Entry barriers and competitive pressures + """ + + if not ctx.deps.client: + logging.info("Error: Gemini client not initialized.") + return "" + try: + response = await ctx.deps.client.aio.models.generate_content( + model=MODEL, + contents=prompt, + ) + return response.text + except Exception as e: + logging.info(f"Error analyzing competition: {e}") + return f"Error analyzing competition: {e}" + + +@swot_agent.tool(prepare=report_tool_usage) +async def get_reddit_insights( + ctx: RunContext[SwotAgentDeps], + query: str, + subreddit_name: str = "googlecloud", +) -> str: + """Gathers insights from a specific subreddit related to a query using PRAW.""" + logging.info(f"Getting Reddit insights from r/{subreddit_name} for query: {query}") + if not ctx.deps.reddit: + logging.info("Error: Reddit client not initialized.") + return "" + try: + subreddit = ctx.deps.reddit.subreddit(subreddit_name) + search_results = subreddit.search( + query, limit=REDDIT_MAX_INSIGHTS, sort="relevance", time_filter="all" + ) + + insights = [] + for post in search_results: + insights.append( + f"Title: {post.title}\n" + f"URL: {post.url}\n" + f"Content: {post.selftext[:REDDIT_MAX_INSIGHT_LENGTH]}...\n" + ) + return "\n".join(insights) + except praw.exceptions.PRAWException as e: + logging.info(f"Error fetching Reddit data: {e}") + return f"Error fetching Reddit data: {e}" + + +# pylint: disable=W0718 +async def run_agent( + url: str = ANALYZE_URL, + deps: SwotAgentDeps = SwotAgentDeps(), +) -> SwotAnalysis | Exception: + """ + Runs the SWOT analysis agent. + + Args: + url: The URL to analyze. + deps: The dependencies for the agent. + + Returns: + The SWOT analysis result or an exception if an error occurred. + """ + + try: + deps.tool_history = [] + result = await swot_agent.run( + f"Perform a comprehensive SWOT analysis for this product: {url}", + deps=deps, + ) + logging.info(f"Agent result: {result}") + + # Send the final result to the UI via update_status_func + if deps.update_status_func: + await deps.update_status_func(deps.request, "Analysis Complete") + + return result.data + except Exception as e: + logging.exception(f"Error during agent run: {e}") + + # Send the error to the UI via update_status_func + if deps.update_status_func: + await deps.update_status_func(deps.request, f"Error: {e}") + + return e + + +if __name__ == "__main__": + data = asyncio.run(run_agent()) + logging.info(data) diff --git a/gemini/sample-apps/swot-agent/main.py b/gemini/sample-apps/swot-agent/main.py new file mode 100644 index 0000000..024d2fe --- /dev/null +++ b/gemini/sample-apps/swot-agent/main.py @@ -0,0 +1,203 @@ +# Copyright 2024 Google, LLC. +# This software is provided as-is, without warranty +# or representation for any use or purpose. +# Your use of it is subject to your agreement with Google. + +# 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 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import asyncio +from collections import defaultdict +import logging +import os +import random +import time +from typing import Any, Dict, Final + +from agent import SwotAgentDeps, SwotAnalysis, run_agent +from fastapi import FastAPI, Form, Request +from fastapi.responses import HTMLResponse +from fastapi.templating import Jinja2Templates +from starlette.middleware.sessions import SessionMiddleware +import uvicorn + +# Configure logging +logging.basicConfig(level=logging.INFO) + +# Constants +ANALYZING_MESSAGE: Final = "Analyzing..." +ANALYSIS_COMPLETE_MESSAGE = "Analysis complete!" + +# Define default development key as a constant +DEFAULT_DEV_KEY: Final = "your-placeholder-development-key-123" + +# Get secret key with warning if using default +SECRET_KEY = os.environ.get("APP_SECRET_KEY", DEFAULT_DEV_KEY) + +if SECRET_KEY == DEFAULT_DEV_KEY: + logging.warning( + "Using default development secret key. " + "Set APP_SECRET_KEY environment variable in production!" + ) + +# Store running tasks and status messages +running_tasks = set() +status_store: Dict[str, list] = defaultdict(list) +result_store: Dict[str, Any] = {} + +app = FastAPI() +app.add_middleware( + SessionMiddleware, + secret_key=SECRET_KEY, + session_cookie="swot_session", + max_age=3600, + same_site="lax", + https_only=False, +) + +templates = Jinja2Templates(directory="templates") + + +@app.get("/", response_class=HTMLResponse) +async def read_root(request: Request) -> HTMLResponse: + """Serves the index page.""" + return templates.TemplateResponse("index.html", {"request": request}) + + +@app.post("/analyze", response_class=HTMLResponse) +async def analyze_url(request: Request, url: str = Form(...)) -> HTMLResponse: + """Analyzes the given URL using the SWOT analysis agent.""" + # Generate a unique ID for this analysis session + session_id = str(id(request)) + request.session["analysis_id"] = session_id + request.session["start_time"] = asyncio.get_event_loop().time() + + # Clear previous status messages for this session + status_store[session_id] = [] + result_store[session_id] = None + + status_store[session_id].append(ANALYZING_MESSAGE) + + logging.info(f"Starting new analysis with session ID: {session_id}") + + # Start the analysis task in the background + task = asyncio.create_task(run_agent_with_progress(session_id, url)) + running_tasks.add(task) + task.add_done_callback(running_tasks.discard) + + # Updated response to render ONLY the status div and not the full page + return templates.TemplateResponse( + "status.html", + {"request": request, "messages": [ANALYZING_MESSAGE], "result": False}, + ) + + +@app.get("/status", response_class=HTMLResponse) +async def get_status(request: Request) -> HTMLResponse: + """Returns the current status messages.""" + session_id = request.session.get("analysis_id") + if not session_id: + return templates.TemplateResponse( + "status.html", + {"request": request, "messages": [], "result": False}, + ) + + messages = status_store.get(session_id, []) + # Check if analysis is complete by looking for the final message + result = ANALYSIS_COMPLETE_MESSAGE in messages + + logging.info(f"Status check - Session ID: {session_id}, Messages: {messages}") + + response = templates.TemplateResponse( + "status.html", + { + "request": request, + "messages": messages, + "result": result, + }, + ) + + return response + + +@app.get("/result", response_class=HTMLResponse) +async def get_result(request: Request) -> HTMLResponse: + """Returns the SWOT analysis result.""" + session_id = request.session.get("analysis_id") + + if session_id and session_id in result_store: + result = result_store[session_id] + else: + result = None + + return templates.TemplateResponse( + "result.html", + {"request": request, "result": result}, + ) + + +async def run_agent_with_progress(session_id: str, url: str) -> None: + """Runs the agent and provides progress updates.""" + try: + # Create a custom deps object that uses the session_id + deps = SwotAgentDeps( + request=None, + update_status_func=lambda request, msg: update_status(session_id, msg), + ) + result = await run_agent(url=url, deps=deps) + + if not isinstance(result, Exception): + logging.info(f"Successfully analyzed URL: {url}") + result_store[session_id] = result + + except Exception as e: # noqa: W0718 + logging.error(f"An unexpected error occurred: {e}") + await update_status(session_id, f"Unexpected error: {e}") + raise + + +def emulate_tool_completion(session_id: str, message: str) -> None: + """Pydantic AI doesn't provide a post-processing hook, so we need to emulate one.""" + + # Sleep a random amount of time between 0 and 5 seconds + time.sleep(random.randint(0, 5)) + status_store[session_id].append(message) + + +async def update_status(session_id: str, message: Any) -> None: + """Updates status messages and handles SWOT analysis results.""" + logging.info(f"Updating status for session {session_id}: {message}") + + # Handle SWOT analysis result + if isinstance(message, SwotAnalysis): + result_store[session_id] = message.model_dump() + status_store[session_id].append(ANALYSIS_COMPLETE_MESSAGE) + return + + # Handle string messages + if isinstance(message, str): + # Instantly store first status message, emulate tool completion for others + if message == ANALYSIS_COMPLETE_MESSAGE: + status_store[session_id].append(message) + else: + loop = asyncio.get_running_loop() + await loop.run_in_executor( + None, emulate_tool_completion, session_id, message + ) + + logging.info( + f"Status messages for session {session_id}: {status_store[session_id]}" + ) + + +if __name__ == "__main__": + uvicorn.run(app, host="0.0.0.0", port=8080) diff --git a/gemini/sample-apps/swot-agent/requirements.txt b/gemini/sample-apps/swot-agent/requirements.txt new file mode 100644 index 0000000..4aa67e0 --- /dev/null +++ b/gemini/sample-apps/swot-agent/requirements.txt @@ -0,0 +1,9 @@ +beautifulsoup4==4.13.3 +fastapi[standard]==0.115.12 +google-genai==0.8.0 +httpx==0.28.1 +itsdangerous==2.2.0 +praw==7.8.1 +pydantic-ai[vertexai]==0.0.49 +pytest-asyncio==0.26.0 +pytest==8.3.5 diff --git a/gemini/sample-apps/swot-agent/templates/index.html b/gemini/sample-apps/swot-agent/templates/index.html new file mode 100644 index 0000000..ebf4a34 --- /dev/null +++ b/gemini/sample-apps/swot-agent/templates/index.html @@ -0,0 +1,182 @@ + + + + + + SWOT Analysis + + + + + + +
    +
    +
    +
    +
    +
    +
    + +
    +
    +

    + SWOT Agent +

    +
    +
    + +

    + Analyze the Strengths, Weaknesses, Opportunities, and Threats
    + of any business or product with your own personal AI agent. +

    + +
    + + + + + Instant Analysis + + + + + + AI-Powered + + + + + + Comprehensive + +
    +
    +
    + +
    +
    +
    + +
    + +
    +
    + +
    + +
    +
    +
    + + diff --git a/gemini/sample-apps/swot-agent/templates/result.html b/gemini/sample-apps/swot-agent/templates/result.html new file mode 100644 index 0000000..532920f --- /dev/null +++ b/gemini/sample-apps/swot-agent/templates/result.html @@ -0,0 +1,163 @@ +{% if result %} +
    +
    +
    +
    + + + +
    +

    Analysis Complete

    +
    + +
    +
    +
    + + + +

    Strengths

    +
    +
      + {% for strength in result.strengths %} +
    • + + {{ strength }} +
    • + {% endfor %} +
    +
    + +
    +
    + + + +

    Weaknesses

    +
    +
      + {% for weakness in result.weaknesses %} +
    • + + {{ weakness }} +
    • + {% endfor %} +
    +
    + +
    +
    + + + +

    Opportunities

    +
    +
      + {% for opportunity in result.opportunities %} +
    • + + {{ opportunity }} +
    • + {% endfor %} +
    +
    + +
    +
    + + + +

    Threats

    +
    +
      + {% for threat in result.threats %} +
    • + + {{ threat }} +
    • + {% endfor %} +
    +
    +
    + + {% if result.analysis %} +
    +
    + + + +

    Analysis Summary

    +
    +
    +
    + + {% endif %} +
    +
    +{% endif %} diff --git a/gemini/sample-apps/swot-agent/templates/status.html b/gemini/sample-apps/swot-agent/templates/status.html new file mode 100644 index 0000000..a8346cf --- /dev/null +++ b/gemini/sample-apps/swot-agent/templates/status.html @@ -0,0 +1,82 @@ +{% if messages %} +
    + {% for message in messages %} {% set is_error = message.startswith('Error:') + %} {% set is_loading = loop.last and not result %} {% set is_tool_message = + message.startswith('Using tool') %} + +
    + {% if is_error %} + + + + {% elif is_loading %} +
    + + + + +
    + {% else %} +
    + + + +
    + {% endif %} + +
    + {% if is_error %} {% set error_parts = message.split('body:', 1) %} + {{ error_parts[0] }} + {% if error_parts|length > 1 %} +
    +{{ error_parts[1]|trim }}
    + {% endif %} {% elif is_tool_message %} {% set parts = message.split(' ', + 2) %} {% set tool_parts = parts[2].split('...', 1) %} + {{ parts[0] }} {{ parts[1] }} + {{ tool_parts[0] }}{% if tool_parts|length > 1 %}...{{ tool_parts[1] }}{% endif %} + {% else %} + {{ message }} + {% endif %} +
    +
    + {% endfor %} +
    +{% endif %} diff --git a/gemini/sample-apps/swot-agent/tests/__init__.py b/gemini/sample-apps/swot-agent/tests/__init__.py new file mode 100644 index 0000000..9e0c6bb --- /dev/null +++ b/gemini/sample-apps/swot-agent/tests/__init__.py @@ -0,0 +1 @@ +"""Test suite for swot-agent""" diff --git a/gemini/sample-apps/swot-agent/tests/test_agent.py b/gemini/sample-apps/swot-agent/tests/test_agent.py new file mode 100644 index 0000000..75b3922 --- /dev/null +++ b/gemini/sample-apps/swot-agent/tests/test_agent.py @@ -0,0 +1,86 @@ +# Copyright 2024 Google, LLC. This software is provided as-is, without +# warranty or representation for any use or purpose. Your use of it is +# subject to your agreement with Google. + +# 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 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from agent import SwotAgentDeps, SwotAnalysis +from pydantic_ai import models +import pytest + +# Prevent accidental model requests during testing +models.ALLOW_MODEL_REQUESTS = False + + +class TestSWOTAnalysis: + """Test the SWOT Analysis data model""" + + def test_swot_analysis_model(self) -> None: + """Test that the SWOT Analysis model can be instantiated with valid data""" + analysis = SwotAnalysis( + strengths=["Strong brand"], + weaknesses=["High costs"], + opportunities=["New markets"], + threats=["Competition"], + analysis="Test analysis", + ) + assert isinstance(analysis, SwotAnalysis) + assert len(analysis.strengths) == 1 + assert len(analysis.weaknesses) == 1 + assert len(analysis.opportunities) == 1 + assert len(analysis.threats) == 1 + assert analysis.analysis == "Test analysis" + + +class TestSwotAgentDeps: + """Test the SWOT Agent Dependencies""" + + @pytest.fixture + def agent_deps(self) -> SwotAgentDeps: + """Create a basic SwotAgentDeps instance for testing""" + return SwotAgentDeps() + + def test_agent_initialization(self, agent_deps: SwotAgentDeps) -> None: + """Test that the agent dependencies are properly initialized""" + assert agent_deps.tool_history == [] + assert agent_deps.request is None + assert agent_deps.update_status_func is None + + def test_gemini_client_initialization(self, agent_deps: SwotAgentDeps) -> None: + """Test Gemini client initialization""" + # Just verify that the Gemini client exists (could be None or initialized) + assert hasattr(agent_deps, "client") + + def test_reddit_client_initialization(self, agent_deps: SwotAgentDeps) -> None: + """Test Reddit client initialization""" + # Just verify that the reddit client exists (could be None or initialized) + assert hasattr(agent_deps, "reddit") + + def test_mock_analysis(self, agent_deps: SwotAgentDeps) -> None: + """Test that the required attributes exist for analysis""" + # Test SwotAnalysis model structure using model_fields + required_fields = { + "strengths", + "weaknesses", + "opportunities", + "threats", + "analysis", + } + model_fields = set(SwotAnalysis.model_fields.keys()) + assert required_fields.issubset( + model_fields + ), f"Missing fields: {required_fields - model_fields}" + + # Test agent dependencies + assert hasattr(agent_deps, "client") + assert hasattr(agent_deps, "tool_history") diff --git a/gemini/sample-apps/swot-agent/tests/test_main.py b/gemini/sample-apps/swot-agent/tests/test_main.py new file mode 100644 index 0000000..a4aead2 --- /dev/null +++ b/gemini/sample-apps/swot-agent/tests/test_main.py @@ -0,0 +1,94 @@ +# Copyright 2024 Google, LLC. This software is provided as-is, without +# warranty or representation for any use or purpose. Your use of it is +# subject to your agreement with Google. + +# 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 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from fastapi.testclient import TestClient +from main import app +import pytest + +client = TestClient(app) + + +def test_read_root() -> None: + """Test the root endpoint returns the index page.""" + response = client.get("/") + assert response.status_code == 200 + assert "text/html" in response.headers["content-type"] + assert "SWOT Analysis" in response.text + + +def test_analyze_endpoint() -> None: + """Test the analyze endpoint with a valid URL.""" + test_url = "https://example.com" + response = client.post("/analyze", data={"url": test_url}) + assert response.status_code == 200 + assert response.headers["content-type"] == "text/html; charset=utf-8" + + +def test_analyze_invalid_url() -> None: + """Test the analyze endpoint with an invalid URL.""" + test_url = "not-a-valid-url" + response = client.post("/analyze", data={"url": test_url}) + assert response.status_code == 200 + assert response.headers["content-type"] == "text/html; charset=utf-8" + + +def test_analyze_empty_url() -> None: + """Test the analyze endpoint with an empty URL.""" + response = client.post("/analyze", data={"url": ""}) + assert response.status_code == 200 + assert response.headers["content-type"] == "text/html; charset=utf-8" + + +def test_status_endpoint() -> None: + """Test the status endpoint.""" + response = client.get("/status") + assert response.status_code == 200 + assert response.headers["content-type"] == "text/html; charset=utf-8" + + +def test_result_endpoint_no_result() -> None: + """Test the result endpoint when no analysis is complete.""" + response = client.get("/result") + assert response.status_code == 200 + assert response.text.strip() == "" + + +@pytest.mark.asyncio +async def test_full_analysis_flow() -> None: + """Test the complete analysis flow from submission to result.""" + # 1. Submit URL for analysis + test_url = "https://google.com" + analyze_response = client.post("/analyze", data={"url": test_url}) + assert analyze_response.status_code == 200 + + # 2. Check status + status_response = client.get("/status") + assert status_response.status_code == 200 + + # 3. Check result (might be empty initially) + result_response = client.get("/result") + assert result_response.status_code == 200 + + +def test_concurrent_requests() -> None: + """Test handling of concurrent analysis requests.""" + test_url = "https://google.com" + # Submit multiple requests in quick succession + responses = [client.post("/analyze", data={"url": test_url}) for _ in range(3)] + + # All requests should be accepted + for response in responses: + assert response.status_code == 200 diff --git a/gemini/thinking/intro_thought_signatures.ipynb b/gemini/thinking/intro_thought_signatures.ipynb new file mode 100644 index 0000000..4bf2777 --- /dev/null +++ b/gemini/thinking/intro_thought_signatures.ipynb @@ -0,0 +1,603 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "sqi5B7V_Rjim" + }, + "outputs": [], + "source": [ + "# Copyright 2025 Google LLC\n", + "#\n", + "# Licensed under the Apache License, Version 2.0 (the \"License\");\n", + "# you may not use this file except in compliance with the License.\n", + "# You may obtain a copy of the License at\n", + "#\n", + "# https://www.apache.org/licenses/LICENSE-2.0\n", + "#\n", + "# Unless required by applicable law or agreed to in writing, software\n", + "# distributed under the License is distributed on an \"AS IS\" BASIS,\n", + "# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n", + "# See the License for the specific language governing permissions and\n", + "# limitations under the License." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "VyPmicX9RlZX" + }, + "source": [ + "# Intro to thought signatures\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + "
    \n", + " \n", + " \"Google
    Open in Colab\n", + "
    \n", + "
    \n", + " \n", + " \"Google
    Open in Colab Enterprise\n", + "
    \n", + "
    \n", + " \n", + " \"Vertex
    Open in Vertex AI Workbench\n", + "
    \n", + "
    \n", + " \n", + " \"GitHub
    View on GitHub\n", + "
    \n", + "
    \n", + "\n", + "
    \n", + "\n", + "Share to:\n", + "\n", + "\n", + " \"LinkedIn\n", + "\n", + "\n", + "\n", + " \"Bluesky\n", + "\n", + "\n", + "\n", + " \"X\n", + "\n", + "\n", + "\n", + " \"Reddit\n", + "\n", + "\n", + "\n", + " \"Facebook\n", + "" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "8MqT58L6Rm_q" + }, + "source": [ + "| Authors |\n", + "| --- |\n", + "| [Eric Dong](https://github.com/gericdong) |" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "nVxnv1D5RoZw" + }, + "source": [ + "## Overview\n", + "\n", + "A thought signature is an encrypted representation of the model's internal reasoning process for a given turn in a conversation. By passing this signature back to the model in subsequent requests, you provide it with the context of its previous thoughts, allowing it to build upon its reasoning and maintain a coherent line of inquiry.\n", + "\n", + "Thought signatures provide a powerful mechanism to maintain context in multi-turn conversations, enabling the model to tackle more complex, multi-step tasks that require reasoning and the use of external tools through function calling.\n", + "\n", + "\n", + "### Objectives\n", + "\n", + "This tutorial explores the thought signatures feature of the Gemini API.\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "gPiTOAHURvTM" + }, + "source": [ + "## Getting Started" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "CHRZUpfWSEpp" + }, + "source": [ + "### Install the Google Gen AI SDK for Python\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "sG3_LKsWSD3A" + }, + "outputs": [], + "source": [ + "%pip install --upgrade --quiet google-genai" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "HlMVjiAWSMNX" + }, + "source": [ + "### Authenticate your notebook environment\n", + "\n", + "If you are running this notebook on Google Colab, run the cell below to authenticate your environment." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "12fnq4V0SNV3" + }, + "outputs": [], + "source": [ + "import sys\n", + "\n", + "if \"google.colab\" in sys.modules:\n", + " from google.colab import auth\n", + "\n", + " auth.authenticate_user()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "be18ac9c5ec8" + }, + "source": [ + "### Authenticate to Vertex AI on Google Cloud\n", + "\n", + "You'll need to set up authentication by choosing **one** of the following methods:\n", + "\n", + "1. **Use a Google Cloud Project:** (Recommended for most users)\n", + " - See instructions [Set up a project and development environment](https://cloud.google.com/vertex-ai/docs/start/cloud-environment)\n", + "2. **Use a Vertex AI API Key (Express Mode):** For quick experimentation.\n", + " - [Get an API Key](https://cloud.google.com/vertex-ai/generative-ai/docs/start/express-mode/overview)\n", + " - See tutorial [Getting started with Gemini using Vertex AI in Express Mode](https://github.com/GoogleCloudPlatform/generative-ai/blob/main/gemini/getting-started/intro_gemini_express.ipynb).\n", + "\n", + "This tutorial uses a Google Cloud Project for authentication." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "6wXh1aH7BlPl" + }, + "outputs": [], + "source": [ + "import os\n", + "\n", + "# fmt: off\n", + "PROJECT_ID = \"[your-project-id]\" # @param {type: \"string\", placeholder: \"[your-project-id]\", isTemplate: true}\n", + "# fmt: on\n", + "if not PROJECT_ID or PROJECT_ID == \"[your-project-id]\":\n", + " PROJECT_ID = str(os.environ.get(\"GOOGLE_CLOUD_PROJECT\"))\n", + "\n", + "LOCATION = \"global\"" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "EdvJRUWRNGHE" + }, + "source": [ + "### Import libraries\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "qgdSpVmDbdQ9" + }, + "outputs": [], + "source": [ + "from IPython.display import Markdown, display\n", + "from google import genai\n", + "from google.genai.types import (\n", + " Content,\n", + " GenerateContentConfig,\n", + " Part,\n", + " ThinkingConfig,\n", + " Tool,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "uhAZMeVrCcKS" + }, + "source": [ + "### Connect to the generative AI service on Vertex AI" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "pv_22mKMCjQY" + }, + "outputs": [], + "source": [ + "client = genai.Client(vertexai=True, project=PROJECT_ID, location=LOCATION)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "n4yRkFg6BBu4" + }, + "source": [ + "### Supported Models\n", + "\n", + "Learn more about [supported Gemini models](https://cloud.google.com/vertex-ai/generative-ai/docs/models). This tutorial uses the Gemini 2.5 Flash model." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "-coEslfWPrxo" + }, + "outputs": [], + "source": [ + "MODEL_ID = \"gemini-2.5-flash\" # @param {type: \"string\"}" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "37CH91ddY9kG" + }, + "source": [ + "## Use Thought Signatures\n", + "\n", + "When thinking is enabled, the API response includes a `thought_signature` field containing an encrypted representation of the model's reasoning. When a function's execution result is sent back to the server, including the `thought_signature` allows the model to restore its previous thinking context, which will likely improve function calling performance.\n", + "\n", + "Example:\n", + "\n", + "```\n", + "thought_signature=b\"\\n\\xa9\\x06\\x01\\xcb...'\n", + "```\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "MMYRh0I5JZu3" + }, + "source": [ + "### Use Cases\n", + "The primary and most powerful use case for thought signatures is to enable sophisticated, multi-turn interactions with external tools. Imagine a scenario where a user's request requires multiple steps to resolve, such as:\n", + "\n", + "- Finding the current weather.\n", + "- Based on the weather, deciding on an appropriate activity.\n", + "- Booking a reservation for that activity.\n", + "\n", + "Each of these steps might involve calling a different function. Without thought signatures, the model would treat each step as an isolated request, losing the crucial context of the user's initial intent and the results of previous function calls. With thought signatures, the model can chain these calls together, understanding the dependencies and making informed decisions at each step.\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "hlVCCsncIlYJ" + }, + "source": [ + "## Example: Conditional Thermostat Control\n", + "\n", + "In this scenario, a user wants to set a thermostat based on the current weather. The request is: \"If it's too hot or too cold in London, set the thermostat to a comfortable temperature.\"\n", + "\n", + "This requires the model to:\n", + "\n", + "- Call a tool to get the weather in London.\n", + "- Use the returned weather information to decide if another tool needs to be called.\n", + "-Call the tool to set the thermostat if the condition is met." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "KmEQftHMJY8c" + }, + "source": [ + "### **Step 1**: Define Functions and Tools\n", + "\n", + "First, we define the two functions the model can use: `get_current_temperature` and `set_thermostat_temperature`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "ifMXpv-rABZk" + }, + "outputs": [], + "source": [ + "# Mock Function Implementations\n", + "\n", + "\n", + "def get_current_temperature(location: str) -> dict:\n", + " \"\"\"Gets the current weather temperature for a given location.\"\"\"\n", + " print(f\"Tool Call: get_current_temperature(location={location})\")\n", + " # Mocking a real API call.\n", + " return {\"temperature\": 30, \"unit\": \"celsius\"}\n", + "\n", + "\n", + "def set_thermostat_temperature(temperature: int) -> dict:\n", + " \"\"\"Sets the thermostat to a desired temperature.\"\"\"\n", + " print(f\"Tool Call: set_thermostat_temperature(temperature={temperature})\")\n", + " # In a real app, this would interact with a thermostat API.\n", + " return {\"status\": \"success\"}\n", + "\n", + "\n", + "# Function Declarations for the model\n", + "\n", + "\n", + "get_weather_declaration = {\n", + " \"name\": \"get_current_temperature\",\n", + " \"description\": \"Gets the current weather temperature for a given location.\",\n", + " \"parameters\": {\n", + " \"type\": \"object\",\n", + " \"properties\": {\"location\": {\"type\": \"string\"}},\n", + " \"required\": [\"location\"],\n", + " },\n", + "}\n", + "\n", + "set_thermostat_declaration = {\n", + " \"name\": \"set_thermostat_temperature\",\n", + " \"description\": \"Sets the thermostat to a desired temperature.\",\n", + " \"parameters\": {\n", + " \"type\": \"object\",\n", + " \"properties\": {\"temperature\": {\"type\": \"integer\"}},\n", + " \"required\": [\"temperature\"],\n", + " },\n", + "}\n", + "\n", + "# Create a single Tool with both functions\n", + "# The model will choose which, if any, to call.\n", + "thermostat_tools = Tool(\n", + " function_declarations=[\n", + " get_weather_declaration,\n", + " set_thermostat_declaration,\n", + " ]\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "vTWbrR1RJP_K" + }, + "source": [ + "### **Step 2**: First Turn - Get the Weather\n", + "\n", + "We send the initial prompt to the model. We expect it to call the `get_current_temperature` function and return a thought signature to maintain the context of the user's conditional request." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "Md-H1xPZAL9m" + }, + "outputs": [], + "source": [ + "# The user's initial multistep request\n", + "prompt = \"\"\"\n", + "If it's too hot or too cold in London, set the temperature to a comfortable level.\n", + "Make your own reasonable assumption for what 'comfortable' means and do not ask for clarification.\n", + "\"\"\"\n", + "\n", + "contents = [Content(role=\"user\", parts=[Part(text=prompt)])]\n", + "\n", + "# Send the first request\n", + "config = GenerateContentConfig(\n", + " tools=[thermostat_tools],\n", + " thinking_config=ThinkingConfig(\n", + " include_thoughts=True, # Optional, include_thoughts can only be enabled when thinking is enabled\n", + " ),\n", + ")\n", + "\n", + "response_turn_1 = client.models.generate_content(\n", + " model=MODEL_ID, config=config, contents=contents\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "fSXaHZLsNSBK" + }, + "outputs": [], + "source": [ + "def process_tool_call(response):\n", + " \"\"\"Processes a model's response to extract and execute a tool call.\n", + "\n", + " Args:\n", + " response: The response object from the model.\n", + "\n", + " Returns:\n", + " A tuple containing the tool_call object and the result of its execution.\n", + " Returns (None, None) if no function call is found.\n", + " \"\"\"\n", + " tool_call = None\n", + " result = None\n", + "\n", + " # Extract the function call, thought signatures, thought summary\n", + " # from the response parts\n", + " for part in response.candidates[0].content.parts:\n", + " if part.function_call:\n", + " tool_call = part.function_call\n", + " if hasattr(part, \"thought_signature\") and part.thought_signature:\n", + " display(Markdown(f\"**Thought Signature**: {part.thought_signature}\"))\n", + " if part.thought:\n", + " display(Markdown(f\"**Thought Summary**: {part.text}\"))\n", + " else:\n", + " display(Markdown(f\"**Answer**: {part.text}\"))\n", + "\n", + " if tool_call:\n", + " if tool_call.name == \"get_current_temperature\":\n", + " result = get_current_temperature(**tool_call.args)\n", + " display(Markdown(f\"**Tool Response**: {result}\"))\n", + " elif tool_call.name == \"set_thermostat_temperature\":\n", + " result = set_thermostat_temperature(**tool_call.args)\n", + " display(Markdown(f\"**Tool Response**: {result}\"))\n", + " else:\n", + " print(\"No valid tool call found in the response.\")\n", + "\n", + " return tool_call, result" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "uHvIv1LsUZ0R" + }, + "outputs": [], + "source": [ + "# Process the response\n", + "print(\"--- Turn 1 ---\")\n", + "tool_call_1, result_1 = process_tool_call(response_turn_1)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "DOWwBqHuLUQ-" + }, + "source": [ + "### **Step 3**: Second Turn - Set the Thermostat\n", + "\n", + "Now, we send the model's previous response (containing the thought signature) and the result from our first tool call back to the model. Using this context, the model will decide if it needs to call the `set_thermostat_temperature` function.\n", + "\n", + "There is no additional work for you to do. The `\"thought_signature\"` part will be sent back in the model turn's `Content`, it is also handled transparently by\n", + "automatic function calling." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "w1OIEbyRAQzh" + }, + "outputs": [], + "source": [ + "# Append the model's function call message from turn 1 (this includes the signature)\n", + "contents.append(response_turn_1.candidates[0].content)\n", + "\n", + "# Append the result of the function execution from turn 1\n", + "contents.append(\n", + " Content(\n", + " role=\"tool\",\n", + " parts=[\n", + " Part.from_function_response(\n", + " name=tool_call_1.name,\n", + " response=result_1,\n", + " )\n", + " ],\n", + " )\n", + ")\n", + "\n", + "# Send the second request\n", + "response_turn_2 = client.models.generate_content(\n", + " model=MODEL_ID, config=config, contents=contents\n", + ")\n", + "\n", + "# Process the response\n", + "tool_call_2, result_2 = process_tool_call(response_turn_2)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "MH0bJ2QBSCAg" + }, + "source": [ + "### **Step 4**: Generate a user-friendly response\n", + "\n", + "Generate a user-friendly response, informed by the action it just took." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "ktEwDwf5R22J" + }, + "outputs": [], + "source": [ + "# Append this turn's messages to history for a final response\n", + "contents.append(response_turn_2.candidates[0].content)\n", + "contents.append(\n", + " Content(\n", + " role=\"tool\",\n", + " parts=[\n", + " Part.from_function_response(\n", + " name=tool_call_2.name,\n", + " response=result_2,\n", + " )\n", + " ],\n", + " )\n", + ")\n", + "\n", + "# Get the final text response\n", + "final_response = client.models.generate_content(\n", + " model=MODEL_ID, config=config, contents=contents\n", + ")\n", + "\n", + "display(Markdown(f\"**Final Response**: {final_response.text}\"))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "wnbAejb8Mtbd" + }, + "source": [ + "This multi-turn process, held together by passing the model's response history (including the encrypted thought signature) back each time, allows the model to perform complex, sequential tasks." + ] + } + ], + "metadata": { + "colab": { + "name": "intro_thought_signatures.ipynb", + "toc_visible": true + }, + "kernelspec": { + "display_name": "Python 3", + "name": "python3" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} diff --git a/gemini/thinking/intro_thought_signatures_rest.ipynb b/gemini/thinking/intro_thought_signatures_rest.ipynb new file mode 100644 index 0000000..339c0db --- /dev/null +++ b/gemini/thinking/intro_thought_signatures_rest.ipynb @@ -0,0 +1,608 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "id": "sqi5B7V_Rjim" + }, + "outputs": [], + "source": [ + "# @title Copyright 2025 Google LLC\n", + "#\n", + "# Licensed under the Apache License, Version 2.0 (the \"License\");\n", + "# you may not use this file except in compliance with the License.\n", + "# You may obtain a copy of the License at\n", + "#\n", + "# https://www.apache.org/licenses/LICENSE-2.0\n", + "#\n", + "# Unless required by applicable law or agreed to in writing, software\n", + "# distributed under the License is distributed on an \"AS IS\" BASIS,\n", + "# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n", + "# See the License for the specific language governing permissions and\n", + "# limitations under the License." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "VyPmicX9RlZX" + }, + "source": [ + "# Intro to thought signatures with REST API\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + "
    \n", + " \n", + " \"Google
    Open in Colab\n", + "
    \n", + "
    \n", + " \n", + " \"Google
    Open in Colab Enterprise\n", + "
    \n", + "
    \n", + " \n", + " \"Vertex
    Open in Vertex AI Workbench\n", + "
    \n", + "
    \n", + " \n", + " \"GitHub
    View on GitHub\n", + "
    \n", + "
    \n", + "\n", + "

    \n", + "Share to:\n", + "\n", + "\n", + " \"LinkedIn\n", + "\n", + "\n", + "\n", + " \"Bluesky\n", + "\n", + "\n", + "\n", + " \"X\n", + "\n", + "\n", + "\n", + " \"Reddit\n", + "\n", + "\n", + "\n", + " \"Facebook\n", + "\n", + "

    " + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "8MqT58L6Rm_q" + }, + "source": [ + "| Authors |\n", + "| --- |\n", + "| [Eric Dong](https://github.com/gericdong) |" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "nVxnv1D5RoZw" + }, + "source": [ + "## Overview\n", + "\n", + "A thought signature is an encrypted representation of the model's internal reasoning process for a given turn in a conversation. By passing this signature back to the model in subsequent requests, you provide it with the context of its previous thoughts, allowing it to build upon its reasoning and maintain a coherent line of inquiry.\n", + "\n", + "This tutorial explores how to use the thought signatures feature of the Gemini API with cURL and the REST API." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "gPiTOAHURvTM" + }, + "source": [ + "## Getting Started" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "5f7c203ffaa1" + }, + "source": [ + "### Install required libraries" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "4e66b2f6d36f" + }, + "outputs": [], + "source": [ + "%%capture\n", + "\n", + "!sudo apt install -q jq" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "dmWOrTJ3gx13" + }, + "source": [ + "### Authenticate your notebook environment (Colab only)\n", + "\n", + "If you are running this notebook on Google Colab, run the following cell to authenticate your environment." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "NyKGtVQjgx13" + }, + "outputs": [], + "source": [ + "import sys\n", + "\n", + "if \"google.colab\" in sys.modules:\n", + " from google.colab import auth\n", + "\n", + " auth.authenticate_user()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "O6ZGaZlxP9L0" + }, + "source": [ + "### Set Google Cloud project\n", + "\n", + "To get started using Vertex AI, you must have an existing Google Cloud project and [enable the Vertex AI API](https://console.cloud.google.com/flows/enableapi?apiid=aiplatform.googleapis.com).\n", + "\n", + "Learn more about [setting up a project and a development environment](https://cloud.google.com/vertex-ai/docs/start/cloud-environment)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "u8IivOG5SqY6" + }, + "outputs": [], + "source": [ + "import os\n", + "\n", + "# fmt: off\n", + "PROJECT_ID = \"[your-project-id]\" # @param {type: \"string\", placeholder: \"[your-project-id]\", isTemplate: true}\n", + "# fmt: on\n", + "if not PROJECT_ID or PROJECT_ID == \"[your-project-id]\":\n", + " PROJECT_ID = str(os.environ.get(\"GOOGLE_CLOUD_PROJECT\"))\n", + "\n", + "LOCATION = os.environ.get(\"GOOGLE_CLOUD_REGION\", \"global\")\n", + "\n", + "os.environ[\"GOOGLE_CLOUD_PROJECT\"] = PROJECT_ID\n", + "os.environ[\"GOOGLE_CLOUD_REGION\"] = LOCATION" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "854fbf388e2b" + }, + "source": [ + "## Use the Gemini 2.5 Flash model" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "7eeb063ac6d4" + }, + "outputs": [], + "source": [ + "MODEL_ID = \"gemini-2.5-flash\"\n", + "\n", + "api_host = \"aiplatform.googleapis.com\"\n", + "if LOCATION != \"global\":\n", + " api_host = f\"{LOCATION}-aiplatform.googleapis.com\"\n", + "\n", + "os.environ[\"API_ENDPOINT\"] = (\n", + " f\"{api_host}/v1/projects/{PROJECT_ID}/locations/{LOCATION}/publishers/google/models/{MODEL_ID}\"\n", + ")\n", + "API_ENDPOINT = os.environ[\"API_ENDPOINT\"]" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "37CH91ddY9kG" + }, + "source": [ + "## Use Thought Signatures\n", + "\n", + "When thinking is enabled, the API response includes a `thought_signature` field containing an encrypted representation of the model's reasoning. When a function's execution result is sent back to the server, including the `thought_signature` allows the model to restore its previous thinking context, which will likely improve function calling performance.\n", + "\n", + "Optionally, to enable thought summaries, include the `\"thinking_config\": { \"include_thoughts\": true }` object in your request body. " + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "hlVCCsncIlYJ" + }, + "source": [ + "## Example: Conditional Thermostat Control\n", + "\n", + "In this scenario, a user wants to set a thermostat based on the current weather. The request is: \"If it's too hot or too cold in London, set the thermostat to a comfortable temperature.\"\n", + "\n", + "This requires the model to:\n", + "\n", + "- Call a tool to get the weather in London.\n", + "- Use the returned weather information to decide if another tool needs to be called.\n", + "- Call the tool to set the thermostat if the condition is met." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "vTWbrR1RJP_K" + }, + "source": [ + "### **Step 1**: First Turn - Get the Weather\n", + "\n", + "We send the initial prompt to the model, along with the definitions of the tools it can use and the configuration to enable thinking. We expect it to call the `get_current_temperature` function and return a thought signature to maintain the context of the user's conditional request." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "p8-eTvw5oDE8" + }, + "outputs": [], + "source": [ + "%%bash\n", + "\n", + "curl -X POST \\\n", + "-H \"Authorization: Bearer $(gcloud auth print-access-token)\" \\\n", + "-H \"Content-Type: application/json\" \\\n", + "https://${API_ENDPOINT}:generateContent \\\n", + "-d @- \\\n", + "> response1.json 2>/dev/null < response2.json 2>/dev/null < response3.json 2>/dev/null <\n", + " \n", + " \n", + " \"Google
    Open in Colab\n", + "
    \n", + " \n", + " \n", + " \n", + " \"Google
    Open in Colab Enterprise\n", + "
    \n", + " \n", + " \n", + " \n", + " \"Workbench
    Open in Workbench\n", + "
    \n", + " \n", + " \n", + " \n", + " \"GitHub
    View on GitHub\n", + "
    \n", + " \n", + "\n", + "\n", + "
    \n", + "\n", + "

    \n", + "Share to:\n", + "\n", + "\n", + " \"LinkedIn\n", + "\n", + "\n", + "\n", + " \"Bluesky\n", + "\n", + "\n", + "\n", + " \"X\n", + "\n", + "\n", + "\n", + " \"Reddit\n", + "\n", + "\n", + "\n", + " \"Facebook\n", + "\n", + "

    \n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "c659c424dc78" + }, + "source": [ + "| Author(s) |\n", + "| ------------------------------------------------ |\n", + "| [Laurent Picard](https://github.com/PicardParis) |\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "b290cca63bbb" + }, + "source": [ + "---\n", + "\n", + "## ✹ Overview\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "2bdfbb16707c" + }, + "source": [ + "![intro image](https://storage.googleapis.com/github-repo/generative-ai/gemini/token-counting/local_token_counting.png)\n", + "\n", + "This notebook demonstrates how to count and analyze tokens locally using the Google Gen AI Python SDK. You'll learn how to use the `LocalTokenizer` to estimate text token counts offline, understand how different modalities (images, audio, video, and PDFs) are tokenized, and see how to retrieve precise token usage metadata from Gemini API responses.\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "61ae1a234536" + }, + "source": [ + "---\n", + "\n", + "## ⚙ Setup\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "59ab08032d8c" + }, + "source": [ + "---\n", + "\n", + "### 🐍 Google Gen AI Python SDK\n", + "\n", + "To call the Gemini API, we'll use the [Google Gen AI Python SDK](https://pypi.org/project/google-genai). The Gemini API provides a `count_tokens` method, and the SDK offers an experimental implementation of a `LocalTokenizer` class.\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "f3df3fbbb5e7" + }, + "source": [ + "Make sure you have a recent version of the `google-genai` package with its `local-tokenizer` extra:\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "3effa6dc4bb0" + }, + "outputs": [], + "source": [ + "%pip install --quiet \"google-genai[local-tokenizer]>=2.9.0\"" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "f237f55cb971" + }, + "source": [ + "---\n", + "\n", + "### 🔑 Google Colab Authentication\n", + "\n", + "If you are running this notebook in **Google Colab**, run the cell below to authenticate your account.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "0080a36ae450" + }, + "outputs": [], + "source": [ + "import sys\n", + "\n", + "if \"google.colab\" in sys.modules:\n", + " from google.colab import auth # type: ignore\n", + "\n", + " auth.authenticate_user()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "5eb3096228eb" + }, + "source": [ + "---\n", + "\n", + "### đŸ› ïž Google Cloud Project\n", + "\n", + "To get started using the Gemini API on Agent Platform, you must have an existing Google Cloud project and [enable the Agent Platform API](https://console.cloud.google.com/flows/enableapi?apiid=aiplatform.googleapis.com).\n", + "\n", + "Learn more about [setting up a project](https://docs.cloud.google.com/resource-manager/docs/creating-managing-projects) and a [development environment](https://docs.cloud.google.com/docs/authentication/set-up-adc-local-dev-environment).\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "266fbcb27e26" + }, + "outputs": [], + "source": [ + "import os\n", + "\n", + "# fmt: off\n", + "PROJECT_ID = \"\" # @param {type: \"string\", placeholder: \"[your-project-id]\", isTemplate: true}\n", + "LOCATION = \"global\" # @param {type: \"string\", placeholder: \"[your-region]\", isTemplate: true}\n", + "# fmt: on\n", + "\n", + "if not PROJECT_ID:\n", + " PROJECT_ID = os.environ.get(\"GOOGLE_CLOUD_PROJECT\")\n", + " assert PROJECT_ID, \"❌ Please set the PROJECT_ID variable\"\n", + "if not LOCATION:\n", + " LOCATION = os.environ.get(\"GOOGLE_CLOUD_REGION\", \"global\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "53bd8452b5cb" + }, + "source": [ + "---\n", + "\n", + "### đŸ€– Gen AI SDK Client\n", + "\n", + "To interact with the Gemini API, we initialize a `genai.Client`. Since we're using the enterprise-ready Agent Platform backend (formerly Vertex AI), we pass `enterprise=True` along with our Google Cloud `project` and `location`:\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "dd22683317ec" + }, + "outputs": [], + "source": [ + "from google import genai\n", + "\n", + "def print_configuration(client: genai.Client) -> None:\n", + " service = \"Agent Platform\" if client.vertexai else \"Google AI\"\n", + " print(f\"â„č Using the {service} API\", end=\"\")\n", + " if client._api_client.project:\n", + " print(f' with project \"{client._api_client.project[:7]}
\"', end=\"\")\n", + " print(f' in location \"{client._api_client.location}\"')\n", + " elif client._api_client.api_key:\n", + " api_key = client._api_client.api_key\n", + " print(f' with API key \"{api_key[:5]}
{api_key[-5:]}\"', end=\"\")\n", + " print(f\" (in case of error, make sure it was created for {service})\")\n", + "\n", + "\n", + "client = genai.Client(enterprise=True, project=PROJECT_ID, location=LOCATION)\n", + "\n", + "print_configuration(client)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "447c2358dfff" + }, + "source": [ + "---\n", + "\n", + "### 🧠 Gemini Model\n", + "\n", + "We'll use `gemini-3.1-flash-lite` as our default model for token counting and content generation. This lightweight, fast model is ideal for high-throughput tasks.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "84c89dfa75b9" + }, + "outputs": [], + "source": [ + "MODEL_ID = \"gemini-3.1-flash-lite\"" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "70bf1cd86554" + }, + "source": [ + "---\n", + "\n", + "## đŸ§© The Basics: Tokens and Tokenizers\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "c83b26aa4325" + }, + "source": [ + "**Tokens**\n", + "\n", + "Large language models (LLMs) don't process our inputs directly, nor do they generate the final text or media we see. Instead, they operate on fundamental units called tokens, ingesting them as inputs and generating them as outputs.\n", + "\n", + "Here's what happens when we send an LLM request:\n", + "\n", + "1. Our inputs are transformed into tokens. In other words, they are tokenized.\n", + "2. The model generates output tokens, which represent the most likely next tokens based on the overall context.\n", + "3. These output tokens are transformed back into the final content we can use.\n", + "\n", + "You can think of a token as a piece of information, and this tokenization process acts as an information compression codec:\n", + "\n", + "1. Encoding: Input → Input tokens\n", + "2. Decoding: Output tokens → Output\n", + "\n", + "Tokenization is necessary to compress information to the right level of semantic granularity, allowing the model's attention mechanism to focus and develop an understanding of the provided data.\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "7b22f3a2cdc0" + }, + "source": [ + "**Tokenizers**\n", + "\n", + "Gemini is natively multimodal and accepts text, images, audio, video, and PDFs. These media types can be processed by a set of three tokenizers:\n", + "\n", + "| Input | Text Tokenizer | Image Tokenizer | Audio Tokenizer | Comment |\n", + "| ----- | :------------: | :-------------: | :-------------: | :----------------------------------------------------------------------------------------------------------------- |\n", + "| Text | ✅ | | | The original tokenizer type, when LLMs were only chatbots. |\n", + "| Image | | ✅ | | An image ~~is~~ can be worth a thousand ~~words~~ tokens! |\n", + "| Audio | ✅ | | ✅ | Text tokens are used for timestamps (`MM:SS` or `H:MM:SS`). |\n", + "| Video | ✅ | ✅ | [✅] | By default, one frame is sampled per second, along with its corresponding timestamp. Audio is optional for videos. |\n", + "| PDF | ✅ | ✅ | | PDFs are processed by vision tokenizers. Text tokens are used for OCR and pagination data. |\n", + "\n", + "As you can see, up to three tokenizers can be involved, depending on the modality.\n", + "\n", + "> 💡 Keep in mind that not all underlying tokens are necessarily billed. See the `usage_metadata` section below for examples of tokens actually billed per modality.\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "d84582ebec9e" + }, + "source": [ + "**Vocabulary**\n", + "\n", + "The complete set of unique tokens that an LLM can ingest or generate makes up its vocabulary. Once an LLM is trained, its vocabulary is fixed and is used for inference.\n", + "\n", + "A vocabulary is essentially a lookup table mapping text sequences to token IDs (which correspond to vector representations in a semantic space). This means tokenizers are simply algorithms that use this vocabulary to encode and decode tokens (i.e., to convert data to and from token IDs).\n", + "\n", + "For example, the Gemini text tokenizers process common words like this:\n", + "\n", + "| Text | Tokens | Tokenization | Token IDs |\n", + "| ---------------- | :----: | --------------------------------------------- | --------------- |\n", + "| `hello` | 1 | A single token for most common sequences | `23391` |\n", + "| `passion` | 1 | `passion` | `208039` |\n", + "| `passionate` | 2 | `pass ‱ ionate` | `4373 ‱ 84242` |\n", + "| `passionnĂ©` | 2 | `passion ‱ nĂ©` (passionate in French) | `208039 ‱ 8504` |\n", + "| `passionately` | 2 | `passion ‱ ately` | `208039 ‱ 2295` |\n", + "| `passionalmente` | 2 | `pass ‱ ionalmente` (passionately in Italian) | `4373 ‱ 134916` |\n", + "\n", + "> 💡 As you can see, words with the same root aren't necessarily split the same way. Text tokenizers have no concept of syllables, prefixes, or suffixes. They don't think like linguists or grammarians; they think like statisticians and look for statistically optimal combinations.\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "c61a95379a77" + }, + "source": [ + "---\n", + "\n", + "## 🌐 Baseline: API Token Counting\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "9690ad1a9521" + }, + "source": [ + "The Gemini API lets you count tokens for any multimodal input by sending a `count_tokens` request. While you need to be authenticated to use it, this method is free of charge, so you can audit your prompts before committing to a paid request. Likewise, the `compute_tokens` method lets you retrieve the list of corresponding tokens and token IDs.\n", + "\n", + "Let's reproduce the previous table:\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "b0b71071e8a6" + }, + "outputs": [], + "source": [ + "from collections.abc import Iterator\n", + "\n", + "import IPython.display\n", + "from google.genai.types import (\n", + " ComputeTokensResponse,\n", + " ComputeTokensResult,\n", + " CountTokensResponse,\n", + " CountTokensResult,\n", + ")\n", + "\n", + "RowData = tuple[str, str, str, str]\n", + "\n", + "\n", + "def display_token_info_from_api(model: str, texts: list[str]) -> None:\n", + " def yield_data() -> Iterator[RowData]:\n", + " for text in texts:\n", + " count_result = client.models.count_tokens(model=model, contents=text)\n", + " compute_result = client.models.compute_tokens(model=model, contents=text)\n", + " yield get_text_token_info(text, count_result, compute_result)\n", + "\n", + " display_token_info(yield_data())\n", + "\n", + "\n", + "def display_token_info(yield_data: Iterator[RowData]) -> None:\n", + " def yield_row() -> Iterator[RowData]:\n", + " yield \"Text\", \"Tokens\", \"Tokenization\", \"Token IDs\"\n", + " yield \"-\", \":-:\", \"-\", \"-\"\n", + " yield from yield_data\n", + "\n", + " markdown = \"\\n\".join(\"| \" + \" | \".join(row) + \" |\" for row in yield_row())\n", + " IPython.display.display(IPython.display.Markdown(markdown))\n", + "\n", + "\n", + "def get_text_token_info(\n", + " text: str,\n", + " count_tokens_res: CountTokensResponse | CountTokensResult,\n", + " compute_tokens_res: ComputeTokensResponse | ComputeTokensResult,\n", + ") -> RowData:\n", + " def inline_code(s: str) -> str:\n", + " return f\"`{s}`\"\n", + "\n", + " total_tokens = count_tokens_res.total_tokens\n", + " tokens_info = compute_tokens_res.tokens_info\n", + " assert tokens_info is not None and len(tokens_info) == 1\n", + " info = tokens_info[0]\n", + " assert info.tokens is not None and info.token_ids is not None\n", + " tokenization = \" ‱ \".join(t.decode(\"utf-8\", errors=\"replace\") for t in info.tokens)\n", + " token_ids = \" ‱ \".join(str(token_id) for token_id in info.token_ids)\n", + "\n", + " return (\n", + " inline_code(text),\n", + " str(total_tokens),\n", + " inline_code(tokenization),\n", + " inline_code(token_ids),\n", + " )\n", + "\n", + "\n", + "TEXTS = [\n", + " \"hello\",\n", + " \"passion\",\n", + " \"passionate\",\n", + " \"passionnĂ©\",\n", + " \"passionately\",\n", + " \"passionalmente\",\n", + "]\n", + "display_token_info_from_api(MODEL_ID, TEXTS)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "16e28d6f2a63" + }, + "source": [ + "---\n", + "\n", + "## 🚀 Why Count Tokens Locally?\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "55ec32576d98" + }, + "source": [ + "Here are a few use cases where counting (or just estimating) tokens locally is useful:\n", + "\n", + "- **Offline & Speed:** You can count tokens completely offline. Plus, even when you're online, doing it locally means you don't have to wait for a network round-trip to the Gemini API just to check your prompt size.\n", + "- **Quotas:** While the `count_tokens` method is free, counting locally saves bandwidth and prevents you from hitting API rate limits, especially during high-volume token counting.\n", + "- **Latency:** You can estimate how much time is needed to process your text input before you start receiving a response (for a given model, the time-to-first-token latency is roughly proportional to the number of input tokens).\n", + "- **Cost Control:** You can estimate and budget your API costs before committing to a paid request.\n", + "- **Routing:** Knowing which token-count bucket your input falls into lets you route requests to different models based on speed, cost, or context size.\n", + "- **Privacy:** You can audit the token count of sensitive data without sending it over your network.\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "bf072680cb47" + }, + "source": [ + "---\n", + "\n", + "## đŸ”€ Using the Local Text Tokenizer\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "84fb998d1e55" + }, + "source": [ + "Create a local tokenizer for the specific Gemini model you're using:\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "873c6f062470" + }, + "outputs": [], + "source": [ + "from google.genai.local_tokenizer import LocalTokenizer\n", + "\n", + "tokenizer = LocalTokenizer(model_name=MODEL_ID)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "19a8a5787f7d" + }, + "source": [ + "> 💡 Remarks\n", + ">\n", + "> - Creating a tokenizer takes a few seconds, during which the configuration and vocabulary are loaded into memory.\n", + "> - On the first call, the tokenizer data is downloaded and stored in a local cache. This step requires an internet connection and about 30MB of storage.\n", + "> - If you want to build a fully offline solution, you can check out the SDK source code and persist the tokenizer assets (e.g., by configuring a persistent cache directory or building a container image).\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "e184ec0f8633" + }, + "source": [ + "Checking the internal tokenizer name confirms that the Gemma open-weight models share the same text tokenizer as the Gemini 3 family:\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "e75584dab3fa" + }, + "outputs": [], + "source": [ + "print(f'Text tokenizer name for \"{MODEL_ID}\": \"{tokenizer._tokenizer_name}\"')" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "055e946e6c84" + }, + "source": [ + "Call the `count_tokens()` method on a small text input:\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "35060d07ec1e" + }, + "outputs": [], + "source": [ + "contents = \"Hello World!\"\n", + "result = tokenizer.count_tokens(contents)\n", + "\n", + "print(f\"{result.total_tokens=}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "133cf17ecdae" + }, + "source": [ + "Now, let's reproduce the previous API tokenization tests with our local tokenizer:\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "a1f1de88d956" + }, + "outputs": [], + "source": [ + "def display_token_info_from_local_tokenizer(\n", + " tokenizer: LocalTokenizer, texts: list[str]\n", + ") -> None:\n", + " def yield_data() -> Iterator[RowData]:\n", + " for text in texts:\n", + " count_result = tokenizer.count_tokens(contents=text)\n", + " compute_result = tokenizer.compute_tokens(contents=text)\n", + " yield get_text_token_info(text, count_result, compute_result)\n", + "\n", + " display_token_info(yield_data())\n", + "\n", + "\n", + "display_token_info_from_local_tokenizer(tokenizer, TEXTS)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "d6f2fceb5e37" + }, + "source": [ + "> 💡 As expected, we get exactly the same results, but with 100% local execution this time.\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "87a0a7bc585f" + }, + "source": [ + "Finally, let's download a longer text, like _Hamlet_:\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "9360b3f481ba" + }, + "outputs": [], + "source": [ + "import requests\n", + "\n", + "def get_text_from_url(content_url: str, force_encoding: str = \"\") -> str:\n", + " response = requests.get(content_url, timeout=10)\n", + " response.raise_for_status()\n", + " if force_encoding: # Use for HTTP headers with unknown/incorrect charset\n", + " response.encoding = force_encoding\n", + " return response.text\n", + "\n", + "\n", + "TEXT_URL = \"https://storage.googleapis.com/dataflow-samples/shakespeare/hamlet.txt\"\n", + "contents = get_text_from_url(TEXT_URL)\n", + "\n", + "print(contents[:256] + \"[
]\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "e0eefc6cb099" + }, + "source": [ + "How many tokens do we need to encode _Hamlet_?\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "9e75cf418178" + }, + "outputs": [], + "source": [ + "result = tokenizer.count_tokens(contents)\n", + "\n", + "print(f\"{result.total_tokens=:,}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "de16e96e8c72" + }, + "source": [ + "> 💡 _Hamlet_ gets broken down locally into 50k+ tokens in a fraction of a second. If you tokenize _War and Peace_, you'll get 850k+ tokens.\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "08da8c9a8c97" + }, + "source": [ + "---\n", + "\n", + "## đŸ•”ïžâ€â™‚ïž Accounting for \"Hidden\" Tokens\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "f1a4b7604cd6" + }, + "source": [ + "When you send a request to Gemini, the total input token count isn't always just the sum of your input data.\n", + "\n", + "To keep things simple, we tested text token counts with default parameters. The `count_tokens` and `compute_tokens` methods both have a `config` parameter. Depending on your request configuration, your inputs and outputs may include additional tokens.\n", + "\n", + "Keep an eye out for these hidden additions:\n", + "\n", + "- **System Instructions:** Any system prompt you set will add to the total token count.\n", + "- **Thinking:** If thinking is enabled, an internal chain of thought can generate additional thinking tokens.\n", + "- **Tools and Functions:** If you provide a list of tools (like Python execution or custom functions), their declarations, calls, and responses are part of your prompt payload.\n", + "- **Response Schema:** Enforcing structured outputs (like JSON) requires the model to process the schema definition you provide, which consumes input tokens.\n", + "- **Chat History:** In multi-turn conversations, the entire chat history is sent back to the model with every new message, meaning your input token count grows with each turn.\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "7680e4605950" + }, + "source": [ + "---\n", + "\n", + "## 🧼 Multimodal Token Math\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "ddc6e4dc5ed9" + }, + "source": [ + "Multimodal inputs (images, audio, video, and documents) aren't tokenized like text. They usually have specific calculation rules based on the model (and its underlying tokenizers), the media type, and the request configuration.\n", + "\n", + "For multimodal inputs, refer to the documentation for details on how token counts are calculated for different media types:\n", + "\n", + "- [Image understanding](https://docs.cloud.google.com/gemini-enterprise-agent-platform/models/capabilities/image-understanding)\n", + "- [Audio understanding](https://docs.cloud.google.com/gemini-enterprise-agent-platform/models/capabilities/audio-understanding)\n", + "- [Video understanding](https://docs.cloud.google.com/gemini-enterprise-agent-platform/models/capabilities/video-understanding)\n", + "- [Document understanding](https://docs.cloud.google.com/gemini-enterprise-agent-platform/models/capabilities/document-understanding)\n", + "\n", + "There are generally multiple tokenization options, even for a single modality. You can use the `count_tokens` method and the calculation rules to estimate the token count of your own payloads. To get a clearer picture, let's look at actual requests and see how token counts are broken down by modality
\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "b399dec264b7" + }, + "source": [ + "---\n", + "\n", + "## 🎯 Tracking Actual Token Usage\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "14c0ac69fa3e" + }, + "source": [ + "While estimating token counts is super useful, you should always rely on the `usage_metadata` returned in the API response when you need to track your actual usage down to the exact token. It's the single source of truth for billing.\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "d258108cabcb" + }, + "source": [ + "Here's the gist of how `usage_metadata` lets you get the token counts by modality:\n", + "\n", + "```python\n", + "class GenerateContentResponse:\n", + " # 
\n", + " usage_metadata: Optional[GenerateContentResponseUsageMetadata]\n", + " # 
\n", + "\n", + "\n", + "class GenerateContentResponseUsageMetadata:\n", + " # 
\n", + " prompt_token_count: Optional[int]\n", + " prompt_tokens_details: Optional[list[ModalityTokenCount]]\n", + " # 
\n", + "\n", + "\n", + "class ModalityTokenCount:\n", + " modality: Optional[MediaModality]\n", + " token_count: Optional[int]\n", + "\n", + "\n", + "class MediaModality(StrEnum):\n", + " MODALITY_UNSPECIFIED = \"MODALITY_UNSPECIFIED\"\n", + " TEXT = \"TEXT\"\n", + " IMAGE = \"IMAGE\"\n", + " VIDEO = \"VIDEO\"\n", + " AUDIO = \"AUDIO\"\n", + " DOCUMENT = \"DOCUMENT\"\n", + "```\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "55236501b41b" + }, + "source": [ + "🐍 Let's define a few helpers:\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "c998f8ef84c6" + }, + "outputs": [], + "source": [ + "from google.genai.types import (\n", + " FileData,\n", + " GenerateContentResponse,\n", + " MediaModality,\n", + " Part,\n", + " PartMediaResolution,\n", + " PartMediaResolutionLevel,\n", + " VideoMetadata,\n", + ")\n", + "\n", + "TokensPerModality = dict[MediaModality, int]\n", + "\n", + "\n", + "def display_tokens_per_modality(response: GenerateContentResponse) -> None:\n", + " usage_metadata = response.usage_metadata\n", + " if not usage_metadata:\n", + " print(\"⚠ No usage metadata found in the response.\")\n", + " return\n", + " prompt_tokens_details = usage_metadata.prompt_tokens_details or []\n", + " tokens_per_modality = get_empty_tokens_per_modality()\n", + "\n", + " for tokens_details in prompt_tokens_details:\n", + " modality = tokens_details.modality\n", + " if modality and modality in tokens_per_modality:\n", + " tokens_per_modality[modality] += tokens_details.token_count or 0\n", + "\n", + " prompt_token_count = usage_metadata.prompt_token_count or 0\n", + " display_token_table(tokens_per_modality, prompt_token_count)\n", + "\n", + "\n", + "def get_empty_tokens_per_modality() -> TokensPerModality:\n", + " return {\n", + " modality: 0\n", + " for modality in MediaModality\n", + " if modality != MediaModality.MODALITY_UNSPECIFIED\n", + " }\n", + "\n", + "\n", + "def display_token_table(\n", + " tokens_per_modality: TokensPerModality,\n", + " total_tokens: int,\n", + ") -> None:\n", + " def yield_row() -> Iterator[list[str]]:\n", + " yield [mod.value for mod in tokens_per_modality.keys()] + [\"Total\"]\n", + " yield [\":-:\" for _ in range(len(tokens_per_modality) + 1)]\n", + " yield [f\"{t:,d}\" for t in tokens_per_modality.values()] + [f\"{total_tokens:,d}\"]\n", + "\n", + " markdown = \"\\n\".join(\"| \" + \" | \".join(row) + \" |\" for row in yield_row())\n", + " IPython.display.display(IPython.display.Markdown(markdown))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "00203dc2790a" + }, + "source": [ + "Let's check a few examples
\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "e27421b8d0b2" + }, + "source": [ + "---\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "51f00e6f607f" + }, + "source": [ + "### đŸ–Œïž Image Tokenization\n", + "\n", + "Image token counts depend on the image itself and the configured media resolution:\n", + "\n", + "```python\n", + "class PartMediaResolutionLevel(StrEnum):\n", + " MEDIA_RESOLUTION_UNSPECIFIED = \"MEDIA_RESOLUTION_UNSPECIFIED\"\n", + " MEDIA_RESOLUTION_LOW = \"MEDIA_RESOLUTION_LOW\"\n", + " MEDIA_RESOLUTION_MEDIUM = \"MEDIA_RESOLUTION_MEDIUM\"\n", + " MEDIA_RESOLUTION_HIGH = \"MEDIA_RESOLUTION_HIGH\"\n", + " MEDIA_RESOLUTION_ULTRA_HIGH = \"MEDIA_RESOLUTION_ULTRA_HIGH\"\n", + "```\n", + "\n", + "For a given media resolution level, the Gemini 3 tokenizers will use these maximum token budgets per image:\n", + "\n", + "| `media_resolution` | Tokens |\n", + "| --------------------------------- | -----: |\n", + "| `MEDIA_RESOLUTION_LOW` | 280 |\n", + "| `MEDIA_RESOLUTION_MEDIUM` | 560 |\n", + "| `MEDIA_RESOLUTION_HIGH` (default) | 1,120 |\n", + "| `MEDIA_RESOLUTION_ULTRA_HIGH` | 2,240 |\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "7e794542f383" + }, + "source": [ + "🐍 Check how this cat image is tokenized by default:\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "372de2a3390f" + }, + "outputs": [], + "source": [ + "def display_tokens_for_image(\n", + " image_uri: str,\n", + " media_resolution_level: PartMediaResolutionLevel | None = None,\n", + ") -> None:\n", + " print(f\"đŸ§Ș {media_resolution_level=}\")\n", + " contents = Part.from_uri(\n", + " file_uri=image_uri,\n", + " mime_type=\"image/*\",\n", + " media_resolution=(\n", + " PartMediaResolution(level=media_resolution_level)\n", + " if media_resolution_level\n", + " else None\n", + " ),\n", + " )\n", + " response = client.models.generate_content(model=MODEL_ID, contents=contents)\n", + " display_tokens_per_modality(response)\n", + "\n", + "\n", + "IMAGE_URI = \"https://storage.googleapis.com/cloud-samples-data/generative-ai/image/chair-cat.png\"\n", + "display_tokens_for_image(IMAGE_URI)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "c01333d460dc" + }, + "source": [ + "> 💡 This image is tokenized into only 1,080 tokens (instead of the maximum 1,120), saving us 40 tokens! It's a nice touch that helps keep costs down rather than defaulting to the upper limit.\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "cf5b5cd4a64a" + }, + "source": [ + "🐍 For less detailed images, you can reduce token counts by a factor of 2 or 4 using the medium or low levels:\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "66d55269cb87" + }, + "outputs": [], + "source": [ + "display_tokens_for_image(IMAGE_URI, PartMediaResolutionLevel.MEDIA_RESOLUTION_LOW)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "f1504242b84b" + }, + "source": [ + "> 💡 At the other end of the media resolution range, the ultra-high level is great for detailed images (like a photo of a circuit board with many components), ensuring maximum visual understanding. An image at this level uses between 2,000 and 2,240 tokens.\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "4e51500157d4" + }, + "source": [ + "---\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "1d1e0801c499" + }, + "source": [ + "### 🔊 Audio Tokenization\n", + "\n", + "Audio tokenization currently uses 25 tokens per second to represent the audio stream semantically.\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "bb0cf82b97d3" + }, + "source": [ + "🐍 Here is the tokenization for a 3.049-second audio file:\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "7d7b298c022d" + }, + "outputs": [], + "source": [ + "def display_tokens_for_audio(audio_uri: str) -> None:\n", + " contents = Part.from_uri(file_uri=audio_uri, mime_type=\"audio/*\")\n", + " response = client.models.generate_content(model=MODEL_ID, contents=contents)\n", + " display_tokens_per_modality(response)\n", + "\n", + "\n", + "AUDIO_URI = \"https://storage.googleapis.com/cloud-samples-data/generative-ai/audio/hello_gemini_are_you_there.wav\"\n", + "display_tokens_for_audio(AUDIO_URI)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "4218b7f807da" + }, + "source": [ + "> 💡 `ceil(3.049 s × 25 tok/s) = ceil(76.225 tok) = 77 tok`\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "7780c8be15f0" + }, + "source": [ + "🐍 A longer, 30.772-second audio file requires 10 times as many tokens, as expected:\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "06d6d54e98d8" + }, + "outputs": [], + "source": [ + "AUDIO_URI = \"https://storage.googleapis.com/cloud-samples-data/generative-ai/audio/sailor_audio.mp3\"\n", + "display_tokens_for_audio(AUDIO_URI)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "4ad5d9a0834c" + }, + "source": [ + "> 💡 `ceil(30.772 s × 25 tok/s) = ceil(769.3 tok) = 770 tok`\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "e93b1d6a7340" + }, + "source": [ + "---\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "79aab236e3c3" + }, + "source": [ + "### 🎬 Video Tokenization\n", + "\n", + "For videos:\n", + "\n", + "- The audio tokenizer is the same as for standalone audio (25 tokens per second).\n", + "- Video frames are sampled (1 FPS by default) and tokenized based on the media resolution.\n", + "\n", + "For a given media resolution level, the Gemini 3 tokenizers will use these maximum token budgets per sampled frame:\n", + "\n", + "| `media_resolution` | Max. tokens |\n", + "| ---------------------------------------------------------- | ----------: |\n", + "| `MEDIA_RESOLUTION_LOW`/`MEDIA_RESOLUTION_MEDIUM` (default) | 70 |\n", + "| `MEDIA_RESOLUTION_HIGH` | 280 |\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "ea4cd90767f1" + }, + "source": [ + "🐍 Here's the tokenization for a 59-second video:\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "c6818766c3c5" + }, + "outputs": [], + "source": [ + "def display_tokens_for_video(\n", + " video_uri: str,\n", + " fps: float | None = None,\n", + " media_resolution_level: PartMediaResolutionLevel | None = None,\n", + ") -> None:\n", + " print(f\"đŸ§Ș {fps=}, {media_resolution_level=}\")\n", + " contents = Part(\n", + " file_data=FileData(file_uri=video_uri, mime_type=\"video/*\"),\n", + " video_metadata=VideoMetadata(fps=fps) if fps is not None else None,\n", + " media_resolution=(\n", + " PartMediaResolution(level=media_resolution_level)\n", + " if media_resolution_level\n", + " else None\n", + " ),\n", + " )\n", + " response = client.models.generate_content(model=MODEL_ID, contents=contents)\n", + " display_tokens_per_modality(response)\n", + "\n", + "\n", + "VIDEO_URI = \"https://www.youtube.com/watch?v=0pJn3g8dfwk\"\n", + "display_tokens_for_video(VIDEO_URI)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "911750d99ada" + }, + "source": [ + "> 💡 Details\n", + ">\n", + "> - Video: `ceil(59 s × 1 frame/s × 66 tok/frame) = ceil(3894 tok) = 3894 tok`\n", + "> - Audio: `ceil(59 s × 25 tok/s) = ceil(1475 tok) = 1475 tok`\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "61c68473fd31" + }, + "source": [ + "🐍 Doubling the sampling rate requires twice as many video tokens:\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "72c5a4c54cf1" + }, + "outputs": [], + "source": [ + "display_tokens_for_video(VIDEO_URI, fps=2)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "99603286f424" + }, + "source": [ + "> 💡 Details\n", + ">\n", + "> - Video: `ceil(59 s × 2 frame/s × 66 tok/frame) = ceil(7788 tok) = 7788 tok`\n", + "> - Audio: `ceil(59 s × 25 tok/s) = ceil(1475 tok) = 1475 tok`\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "ee0634f9462e" + }, + "source": [ + "🐍 If you switch from low/medium to high media resolution, sampled frames are tokenized in greater detail, requiring four times as many video tokens:\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "1636c1c58644" + }, + "outputs": [], + "source": [ + "VIDEO_URI = \"https://www.youtube.com/watch?v=0pJn3g8dfwk\"\n", + "display_tokens_for_video(\n", + " VIDEO_URI,\n", + " media_resolution_level=PartMediaResolutionLevel.MEDIA_RESOLUTION_HIGH,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "f9bb43148a16" + }, + "source": [ + "> 💡 Details\n", + ">\n", + "> - Video: `ceil(59 s × 1 frame/s × 264 tok/frame) = ceil(15576 tok) = 15576 tok`\n", + "> - Audio: `ceil(59 s × 25 tok/s) = ceil(1475 tok) = 1475 tok`\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "c50a4b7bc4a7" + }, + "source": [ + "---\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "8dcb4d99137f" + }, + "source": [ + "### 📄 Document Tokenization\n", + "\n", + "For a given media resolution level, the Gemini 3 tokenizers will use these maximum token budgets per PDF page:\n", + "\n", + "| `media_resolution` | Tokens |\n", + "| ----------------------------------- | -----: |\n", + "| `MEDIA_RESOLUTION_LOW` | 280 |\n", + "| `MEDIA_RESOLUTION_MEDIUM` (default) | 560 |\n", + "| `MEDIA_RESOLUTION_HIGH` | 1,120 |\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "780cb4762e3e" + }, + "source": [ + "🐍 Here's the tokenization for a one-page PDF at different media resolutions:\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "a16be8afe5cf" + }, + "outputs": [], + "source": [ + "def display_tokens_for_document(\n", + " document_uri: str,\n", + " media_resolution_level: PartMediaResolutionLevel | None = None,\n", + ") -> None:\n", + " print(f\"đŸ§Ș {media_resolution_level=}\")\n", + " contents = Part.from_uri(\n", + " file_uri=document_uri,\n", + " mime_type=\"application/pdf\",\n", + " media_resolution=(\n", + " PartMediaResolution(level=media_resolution_level)\n", + " if media_resolution_level\n", + " else None\n", + " ),\n", + " )\n", + " response = client.models.generate_content(model=MODEL_ID, contents=contents)\n", + " display_tokens_per_modality(response)\n", + "\n", + "\n", + "DOCUMENT_URI = (\n", + " \"https://storage.googleapis.com/cloud-samples-data/generative-ai/pdf/invoice.pdf\"\n", + ")\n", + "media_resolution_levels = [\n", + " PartMediaResolutionLevel.MEDIA_RESOLUTION_LOW,\n", + " PartMediaResolutionLevel.MEDIA_RESOLUTION_MEDIUM,\n", + " PartMediaResolutionLevel.MEDIA_RESOLUTION_HIGH,\n", + "]\n", + "for media_resolution_level in media_resolution_levels:\n", + " display_tokens_for_document(DOCUMENT_URI, media_resolution_level)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "4da56d000312" + }, + "source": [ + "> 💡 Remarks\n", + ">\n", + "> - Low: `266 tok/pg`\n", + "> - Medium: `532 tok/pg`\n", + "> - High: `1092 tok/pg`\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "4f9f6fc2ca48" + }, + "source": [ + "🐍 Here's another test for a 15-page PDF:\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "fb64c59765ef" + }, + "outputs": [], + "source": [ + "DOCUMENT_URI = \"https://storage.googleapis.com/cloud-samples-data/generative-ai/pdf/1706.03762v7.pdf\"\n", + "for media_resolution_level in media_resolution_levels:\n", + " display_tokens_for_document(DOCUMENT_URI, media_resolution_level)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "99ffb34b15ca" + }, + "source": [ + "> 💡 Remarks\n", + ">\n", + "> - Low: `3990 tok / 15 pg = 266 tok/pg`\n", + "> - Medium: `7800 tok / 15 pg = 520 tok/pg`\n", + "> - High: `16530 tok / 15 pg = 1102 tok/pg`\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "332a048ed528" + }, + "source": [ + "---\n", + "\n", + "## 🎉 Conclusion\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "23bbc4417933" + }, + "source": [ + "You've now mastered token counting both locally and via the Gemini API!\n", + "\n", + "With the `LocalTokenizer`, you can estimate text token counts completely offline, saving bandwidth and avoiding rate limits. You've also seen how Gemini's multimodal tokenizers handle images, audio, video, and PDFs, and how to extract precise token usage from `usage_metadata` for accurate tracking and billing.\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "57e70568cea6" + }, + "source": [ + "---\n", + "\n", + "## 📚 Additional Resources\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "11c477b74fda" + }, + "source": [ + "- Check out all available models in [Model Garden](https://docs.cloud.google.com/gemini-enterprise-agent-platform/models/model-garden/explore-models).\n", + "- Explore typical use cases in the [Agent Platform Prompt Gallery](https://console.cloud.google.com/agent-platform/studio/prompt-gallery).\n", + "- Stay updated with the [Agent Platform Release Notes](https://docs.cloud.google.com/gemini-enterprise-agent-platform/release-notes).\n", + "- Explore other notebooks in this repository to dive deeper into Gemini's capabilities!\n" + ] + } + ], + "metadata": { + "colab": { + "name": "local_token_counting.ipynb", + "toc_visible": true + }, + "kernelspec": { + "display_name": "Python 3", + "name": "python3" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} diff --git a/gemini/tuning/dpo_gemini_data_prep_tuning.ipynb b/gemini/tuning/dpo_gemini_data_prep_tuning.ipynb new file mode 100644 index 0000000..1e88879 --- /dev/null +++ b/gemini/tuning/dpo_gemini_data_prep_tuning.ipynb @@ -0,0 +1,737 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "1oLIdNNH4xTY" + }, + "outputs": [], + "source": [ + "# Copyright 2025 Google LLC\n", + "#\n", + "# Licensed under the Apache License, Version 2.0 (the \"License\");\n", + "# you may not use this file except in compliance with the License.\n", + "# You may obtain a copy of the License at\n", + "#\n", + "# https://www.apache.org/licenses/LICENSE-2.0\n", + "#\n", + "# Unless required by applicable law or agreed to in writing, software\n", + "# distributed under the License is distributed on an \"AS IS\" BASIS,\n", + "# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n", + "# See the License for the specific language governing permissions and\n", + "# limitations under the License." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "dYFAt0Bs4xTZ" + }, + "source": [ + "# Prepare High-Quality Preference Data for Gemini 2.5\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + "
    \n", + " \n", + " \"Google
    Open in Colab\n", + "
    \n", + "
    \n", + " \n", + " \"Google
    Open in Colab Enterprise\n", + "
    \n", + "
    \n", + " \n", + " \"Vertex
    Open in Vertex AI Workbench\n", + "
    \n", + "
    \n", + " \n", + " \"GitHub
    View on GitHub\n", + "
    \n", + "
    \n", + "\n", + "
    \n", + "\n", + "Share to:\n", + "\n", + "\n", + " \"LinkedIn\n", + "\n", + "\n", + "\n", + " \"Bluesky\n", + "\n", + "\n", + "\n", + " \"X\n", + "\n", + "\n", + "\n", + " \"Reddit\n", + "\n", + "\n", + "\n", + " \"Facebook\n", + " " + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "9pQonORh4xTa" + }, + "source": [ + "| Authors |\n", + "| --- |\n", + "| Haichao Yu |\n", + "| [Ivan Nardini](https://github.com/inardini) |" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "cG8Zkh1u4xTa" + }, + "source": [ + "## Overview\n", + "\n", + "This tutorial teaches you how to prepare high-quality preference data for tuning Gemini 2.5. Before running Direct Preference Optimization (DPO), it's crucial to ensure your training data is high quality. We will use **Vertex AI Gen AI Evaluation SDK** to filter out low-quality preference pairs.\n", + "\n", + "### Why does data quality matter?\n", + "\n", + "Imagine you're teaching by showing examples. If half your \"good examples\" are actually mediocre, and half your \"bad examples\" are actually decent, you'll confuse the student. The same happens when training AI models.\n", + "\n", + "**Think of preference data like graded essays:**\n", + "- Some essays marked \"A\" might actually be B-quality\n", + "- Some essays marked \"F\" might actually deserve a C\n", + "- If you don't filter these out, your model learns the wrong patterns\n", + "\n", + "### What you'll learn\n", + "\n", + "By the end of this tutorial, you will:\n", + "1. Load a preference dataset (UltraFeedback).\n", + "2. Use **Vertex AI Gen AI Evaluation SDK** to score responses.\n", + "3. Visualize quality scores to understand your data.\n", + "4. Apply filtering strategies (Absolute Threshold and Win Margin).\n", + "5. Create a Supervised Fine-Tuning (SFT) dataset from high-quality responses." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "XCj5nYbJ4xTa" + }, + "source": [ + "## Get started" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "m0QHzqS04xTb" + }, + "source": [ + "### Install required packages\n", + "\n", + "We'll install the necessary packages for Google Cloud, data handling, and evaluation.\n", + "\n", + "**⚠ Expected Behavior**: After running this cell, Colab will ask you to **restart the runtime**. This is normal and necessary." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "XT-CQJGw4xTb" + }, + "outputs": [], + "source": [ + "%pip install --upgrade --quiet \"google-cloud-aiplatform[evaluation]\" datasets matplotlib" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "KNH1nB2Q4xTc" + }, + "source": [ + "### Authenticate (Colab only)\n", + "\n", + "If you're running this in Google Colab, you need to authenticate to access your Google Cloud project." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "n2-GakFW4xTc" + }, + "outputs": [], + "source": [ + "import sys\n", + "\n", + "if \"google.colab\" in sys.modules:\n", + " from google.colab import auth\n", + "\n", + " auth.authenticate_user()\n", + " print(\"✅ Authentication successful!\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "i2RkKJ6m4xTc" + }, + "source": [ + "### Set up your Google Cloud project\n", + "\n", + "Replace `[your-project-id]` below with your actual Google Cloud project ID." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "tRILcSNa4xTc" + }, + "outputs": [], + "source": [ + "import os\n", + "\n", + "from vertexai import Client, types\n", + "\n", + "# TODO: Replace with your actual project ID\n", + "# fmt: off\n", + "PROJECT_ID = \"[your-project-id]\" # @param {type: \"string\", placeholder: \"[your-project-id]\", isTemplate: true}\n", + "# fmt: on\n", + "\n", + "if not PROJECT_ID or PROJECT_ID == \"[your-project-id]\":\n", + " PROJECT_ID = str(os.environ.get(\"GOOGLE_CLOUD_PROJECT\", \"\"))\n", + "\n", + "if not PROJECT_ID:\n", + " raise ValueError(\"Please set your PROJECT_ID above\")\n", + "\n", + "LOCATION = \"us-central1\"\n", + "BUCKET_NAME = f\"{PROJECT_ID}-data-prep\"\n", + "BUCKET_URI = f\"gs://{BUCKET_NAME}\"\n", + "\n", + "print(f\"📩 Creating GCS bucket: {BUCKET_NAME}...\")\n", + "! gcloud storage buckets create --location {LOCATION} --project {PROJECT_ID} {BUCKET_URI} 2>/dev/null || echo \"(Bucket already exists, continuing...)\"\n", + "\n", + "# Initialize the Client\n", + "client = Client(project=PROJECT_ID, location=LOCATION)\n", + "\n", + "print(f\"✅ Using project: {PROJECT_ID}\")\n", + "print(f\"✅ Bucket: {BUCKET_URI}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "Yf9fb7rv4xTd" + }, + "source": [ + "### Import libraries" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "lT-m_c4R4xTd" + }, + "outputs": [], + "source": [ + "import json\n", + "\n", + "import matplotlib.pyplot as plt\n", + "import numpy as np\n", + "import pandas as pd\n", + "from IPython.display import display\n", + "from datasets import load_dataset\n", + "\n", + "print(\"✅ Libraries imported\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "YL-i-tw64xTd" + }, + "source": [ + "## Step 1: Load the training dataset\n", + "\n", + "We'll use the **UltraFeedback** dataset, same as in the DPO tutorial. This dataset contains prompts with human-rated responses." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "CmsEnuRt4xTd" + }, + "outputs": [], + "source": [ + "print(\"đŸ“„ Loading UltraFeedback dataset...\")\n", + "dataset = load_dataset(\"zhengr/ultrafeedback_binarized\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "FuGVPOF_0MrI" + }, + "outputs": [], + "source": [ + "# Define the Filter Function\n", + "def is_valid_example(example):\n", + " \"\"\"Returns True if the example has a valid prompt and valid responses.\"\"\"\n", + " # Check Prompt\n", + " if not example.get(\"prompt\"):\n", + " return False\n", + "\n", + " # Check Chosen (Preferred) content\n", + " # Ensure list exists, is not empty, and last message has content\n", + " if not example.get(\"chosen\") or not example[\"chosen\"][-1].get(\"content\"):\n", + " return False\n", + "\n", + " # Check Rejected (Dispreferred) content\n", + " if not example.get(\"rejected\") or not example[\"rejected\"][-1].get(\"content\"):\n", + " return False\n", + "\n", + " return True\n", + "\n", + "\n", + "# Apply Filter\n", + "print(\"đŸ§č Filtering empty items from dataset...\")\n", + "# This removes invalid rows from the entire dataset split efficiently\n", + "clean_dataset = dataset[\"train_prefs\"].filter(is_valid_example)\n", + "\n", + "print(f\"Original size: {len(dataset['train_prefs'])}\")\n", + "print(f\"Clean size: {len(clean_dataset)}\")\n", + "\n", + "# Select Subset\n", + "# Now we select 100 items knowing they are all valid\n", + "# We use min() just in case the dataset is smaller than 100 after filtering\n", + "n_examples = 100\n", + "train_data = clean_dataset.select(range(min(n_examples, len(clean_dataset))))\n", + "\n", + "print(f\"✅ Loaded {len(train_data)} CLEAN examples for training.\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "cApaIy_F4xTd" + }, + "source": [ + "## Step 2: Data Quality Evaluation\n", + "\n", + "We will use Vertex AI's Gen AI Evaluation SDK to score both preferred and dispreferred responses. This provides a streamlined way to run evaluations without manual API calls or parallelization." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "tlq-Ou7O5rHN" + }, + "source": [ + "### 2.1. Prepare Data for Evaluation\n", + "\n", + "We need to extract the prompt and the response for each example. We'll create two dataframes: one for preferred responses and one for dispreferred. The SDK expects columns named `prompt` and `response`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "o9cRvIwR4xTd" + }, + "outputs": [], + "source": [ + "def prepare_eval_data(data, subset=\"preferred\"):\n", + " prompts = []\n", + " responses = []\n", + "\n", + " print(f\"Processing subset: {subset}\")\n", + "\n", + " for i, example in enumerate(data):\n", + " # Extract content first\n", + " current_prompt = example[\"prompt\"]\n", + "\n", + " if subset == \"preferred\":\n", + " resp = example[\"chosen\"][-1][\"content\"]\n", + " else:\n", + " resp = example[\"rejected\"][-1][\"content\"]\n", + "\n", + " # Append BOTH at the same time\n", + " # This guarantees prompts and responses have the exact same length\n", + " prompts.append(current_prompt)\n", + " responses.append(resp)\n", + "\n", + " return pd.DataFrame({\"prompt\": prompts, \"response\": responses})\n", + "\n", + "\n", + "df_preferred = prepare_eval_data(train_data, \"preferred\")\n", + "df_dispreferred = prepare_eval_data(train_data, \"dispreferred\")\n", + "\n", + "print(\"✅ Data prepared for evaluation.\")\n", + "display(df_preferred.head(2))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "HeVLgsYu4xTd" + }, + "source": [ + "### 2.2. Run Auto-Evaluation using Gen AI Evaluation SDK\n", + "\n", + "We'll use the `client.evals.evaluate` method. We'll use the `general_quality_v1` metric." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "jF4BtXHN6rz9" + }, + "outputs": [], + "source": [ + "# Define the evaluation metric\n", + "metric_to_use = types.RubricMetric.GENERAL_QUALITY\n", + "metric_name_for_results = \"general_quality_v1\"\n", + "\n", + "# Helper Function to Extract Scores\n", + "\n", + "\n", + "def extract_scores_from_result(result, metric_name):\n", + " \"\"\"Iterates through the EvaluationResult object to extract individual scores.\"\"\"\n", + " scores = []\n", + " # Ensure we process them in order of their index to match the DataFrame\n", + " sorted_cases = sorted(result.eval_case_results, key=lambda x: x.eval_case_index)\n", + "\n", + " for case in sorted_cases:\n", + " try:\n", + " # Get the result for the first candidate (we only evaluate 1 response per row)\n", + " metric_result = case.response_candidate_results[0].metric_results[\n", + " metric_name\n", + " ]\n", + " scores.append(metric_result.score)\n", + " except (KeyError, IndexError, AttributeError):\n", + " scores.append(None) # Handle cases where evaluation might have failed\n", + "\n", + " return scores\n", + "\n", + "\n", + "print(\"Running evaluation on preferred responses...\")\n", + "result_preferred = client.evals.evaluate(\n", + " dataset=df_preferred,\n", + " metrics=[metric_to_use],\n", + ")\n", + "\n", + "df_preferred[\"score\"] = extract_scores_from_result(\n", + " result_preferred, metric_name_for_results\n", + ")\n", + "\n", + "print(\"Running evaluation on dispreferred responses...\")\n", + "result_dispreferred = client.evals.evaluate(\n", + " dataset=df_dispreferred,\n", + " metrics=[metric_to_use],\n", + ")\n", + "df_dispreferred[\"score\"] = extract_scores_from_result(\n", + " result_dispreferred, metric_name_for_results\n", + ")\n", + "\n", + "print(\"✅ Evaluation complete.\")\n", + "print()\n", + "display(df_preferred[[\"prompt\", \"score\"]].head())" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "FRUAaVAG4xTd" + }, + "source": [ + "## Step 3: Visualize Quality Scores\n", + "\n", + "Now for the interesting part! Let's visualize the quality scores to see how well your \"preferred\" and \"dispreferred\" labels match actual quality.\n", + "\n", + "**What we're looking for:**\n", + "- **Good data**: Preferred responses score higher than dispreferred ones (clear separation in the histogram)\n", + "- **Problematic data**: Lots of overlap between the two distributions\n", + "\n", + "This visualization will help you decide where to set filtering thresholds in the next step." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "wr5kjp5u4xTd" + }, + "outputs": [], + "source": [ + "# Create histogram\n", + "# Using 20 bins to get better granularity on the 0-1 scale\n", + "bins = np.linspace(0, 1, 20)\n", + "\n", + "plt.figure(figsize=(10, 6))\n", + "plt.hist(\n", + " df_preferred[\"score\"].dropna(),\n", + " bins,\n", + " alpha=0.5,\n", + " label=f\"Preferred (avg: {df_preferred['score'].mean():.2f})\",\n", + " color=\"green\",\n", + ")\n", + "plt.hist(\n", + " df_dispreferred[\"score\"].dropna(),\n", + " bins,\n", + " alpha=0.5,\n", + " label=f\"Dispreferred (avg: {df_dispreferred['score'].mean():.2f})\",\n", + " color=\"red\",\n", + ")\n", + "\n", + "# Axis labels reflect the 0-1 scale\n", + "plt.xlabel(\"Quality Score (0.0 = poor, 1.0 = perfect)\")\n", + "plt.ylabel(\"Number of Responses\")\n", + "plt.title(\"Distribution of Quality Scores: Preferred vs Dispreferred Responses\")\n", + "plt.legend()\n", + "plt.grid(True, alpha=0.3)\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "dH2JYRWT4xTe" + }, + "source": [ + "## Step 4: Data Cleaning (Filtering)\n", + "\n", + "Now we'll filter the dataset based on the scores we just computed.\n", + "\n", + "### Filtering Strategies\n", + "\n", + "| Strategy | When to Use | How It Works |\n", + "|----------|-------------|--------------|\n", + "| **Absolute Threshold** | You want to set a minimum quality bar | Keep if preferred > threshold AND dispreferred < threshold |\n", + "| **Win Margin** | You want clear winners | Keep if preferred score is at least X points higher than dispreferred |" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "DDeK5qPA5yEW" + }, + "source": [ + "### Strategy 1: Absolute Threshold\n", + "\n", + "We keep pairs where:\n", + "- Preferred score > 0.7 (out of 5)\n", + "- Dispreferred score < 0.5" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "XZjNaajT4xTe" + }, + "outputs": [], + "source": [ + "PREFERRED_THRESHOLD = 0.65\n", + "DISPREFERRED_THRESHOLD = 0.95\n", + "\n", + "# Calculate how much data each rule removes.\n", + "remove_mask_abs_preferred = df_preferred[\"score\"] <= PREFERRED_THRESHOLD\n", + "remove_mask_abs_dispreferred = df_dispreferred[\"score\"] >= DISPREFERRED_THRESHOLD\n", + "keep_mask_abs = (df_preferred[\"score\"] > PREFERRED_THRESHOLD) & (\n", + " df_dispreferred[\"score\"] < DISPREFERRED_THRESHOLD\n", + ")\n", + "\n", + "print(\n", + " f\"Pairs removed with preferred response filtering only: {remove_mask_abs_preferred.sum()} out of {len(train_data)}\"\n", + ")\n", + "print(\n", + " f\"Pairs removed with dispreferred response filtering only: {remove_mask_abs_dispreferred.sum()} out of {len(train_data)}\"\n", + ")\n", + "print(\n", + " f\"Pairs kept with Absolute Threshold: {keep_mask_abs.sum()} out of {len(train_data)}\"\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "hPNa27Sj4xTe" + }, + "source": [ + "### Strategy 2: Win Margin\n", + "\n", + "We keep pairs where the preferred score is at least 1 point higher than the dispreferred score." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "TIqEc5lb4xTe" + }, + "outputs": [], + "source": [ + "MARGIN = 0.05\n", + "keep_mask_margin = (df_preferred[\"score\"] - df_dispreferred[\"score\"]) >= MARGIN\n", + "\n", + "print(f\"Pairs kept with Win Margin: {keep_mask_margin.sum()} out of {len(train_data)}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "McmR4HyJ4xTe" + }, + "source": [ + "### Save Cleaned Data\n", + "\n", + "Let's save the cleaned data using the Win Margin strategy, transformed into Gemini format." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "spFOD9DI4xTe" + }, + "outputs": [], + "source": [ + "cleaned_data = []\n", + "for i, keep in enumerate(keep_mask_margin):\n", + " if keep:\n", + " example = train_data[i]\n", + " gemini_format = {\n", + " \"contents\": [{\"role\": \"user\", \"parts\": [{\"text\": example[\"prompt\"]}]}],\n", + " \"completions\": [\n", + " {\n", + " \"score\": 1.0,\n", + " \"completion\": {\n", + " \"role\": \"model\",\n", + " \"parts\": [{\"text\": example[\"chosen\"][-1][\"content\"]}],\n", + " },\n", + " },\n", + " {\n", + " \"score\": 0.0,\n", + " \"completion\": {\n", + " \"role\": \"model\",\n", + " \"parts\": [{\"text\": example[\"rejected\"][-1][\"content\"]}],\n", + " },\n", + " },\n", + " ],\n", + " }\n", + " cleaned_data.append(gemini_format)\n", + "\n", + "output_file = \"cleaned_preference_data.jsonl\"\n", + "with open(output_file, \"w\") as f:\n", + " for entry in cleaned_data:\n", + " f.write(json.dumps(entry) + \"\\n\")\n", + "\n", + "!gcloud storage cp {output_file} {BUCKET_URI}/{output_file}\n", + "print(f\"✅ Cleaned data saved to {BUCKET_URI}/{output_file}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "QT89EumD4xTe" + }, + "source": [ + "## Step 5: Create SFT Dataset (Optional)\n", + "\n", + "**Bonus step!** You can extract just the preferred responses to create a supervised fine-tuning (SFT) dataset.\n", + "\n", + "### Why would you want this?\n", + "\n", + "The [preference optimization tutorial](https://colab.research.google.com/github/GoogleCloudPlatform/generative-ai/blob/main/gemini/tuning/dpo_gemini.ipynb) recommends a **two-phase approach**:\n", + "\n", + "1. **Phase 1: SFT (Supervised Fine-Tuning)** - Train the model on just the good responses\n", + "2. **Phase 2: DPO (Direct Preference Optimization)** - Show the model pairs to refine its judgment\n", + "\n", + "**Think of it like teaching:**\n", + "- **SFT** = \"Here are examples of good essays\" (establishes baseline competence)\n", + "- **DPO** = \"Now here's why this essay is better than that one\" (refines judgment)\n", + "\n", + "This two-phase approach often produces better results than jumping straight to preference optimization." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "xHdzGzyK4xTe" + }, + "outputs": [], + "source": [ + "sft_data = []\n", + "for entry in cleaned_data:\n", + " # Extract preferred response (score 1.0)\n", + " preferred_resp = next(\n", + " c[\"completion\"] for c in entry[\"completions\"] if c[\"score\"] == 1.0\n", + " )\n", + " sft_entry = {\"contents\": [entry[\"contents\"][0], preferred_resp]}\n", + " sft_data.append(sft_entry)\n", + "\n", + "sft_output_file = \"sft_data.jsonl\"\n", + "with open(sft_output_file, \"w\") as f:\n", + " for entry in sft_data:\n", + " f.write(json.dumps(entry) + \"\\n\")\n", + "\n", + "!gcloud storage cp {sft_output_file} {BUCKET_URI}/{sft_output_file}\n", + "print(f\"✅ SFT data saved to {BUCKET_URI}/{sft_output_file}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "kSG0pCDR4xTe" + }, + "source": [ + "## Conclusion\n", + "\n", + "You have now prepared a high-quality preference dataset and an SFT dataset using the Vertex AI Gen AI Evaluation SDK. These are ready to be used in the [Gemini DPO Tutorial](https://colab.research.google.com/github/GoogleCloudPlatform/generative-ai/blob/main/gemini/tuning/dpo_gemini.ipynb).\n", + "\n", + "### Clean up\n", + "\n", + "To avoid charges, you can delete the GCS bucket created in this tutorial." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "X6JD1n3Z4xTe" + }, + "outputs": [], + "source": [ + "# !gcloud storage rm --recursive {BUCKET_URI}" + ] + } + ], + "metadata": { + "colab": { + "name": "dpo_gemini_data_prep_tuning.ipynb", + "toc_visible": true + }, + "kernelspec": { + "display_name": "Python 3", + "name": "python3" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} diff --git a/gemini/tuning/dpo_gemini_get_started.ipynb b/gemini/tuning/dpo_gemini_get_started.ipynb new file mode 100644 index 0000000..78bbf21 --- /dev/null +++ b/gemini/tuning/dpo_gemini_get_started.ipynb @@ -0,0 +1,1085 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "acrGm0JPOKS5" + }, + "outputs": [], + "source": [ + "# Copyright 2025 Google LLC\n", + "#\n", + "# Licensed under the Apache License, Version 2.0 (the \"License\");\n", + "# you may not use this file except in compliance with the License.\n", + "# You may obtain a copy of the License at\n", + "#\n", + "# https://www.apache.org/licenses/LICENSE-2.0\n", + "#\n", + "# Unless required by applicable law or agreed to in writing, software\n", + "# distributed under the License is distributed on an \"AS IS\" BASIS,\n", + "# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n", + "# See the License for the specific language governing permissions and\n", + "# limitations under the License." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "cXVx3nfOOKS7" + }, + "source": [ + "# Get Started with Gemini Preference Optimization\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + "
    \n", + " \n", + " \"Google
    Open in Colab\n", + "
    \n", + "
    \n", + " \n", + " \"Google
    Open in Colab Enterprise\n", + "
    \n", + "
    \n", + " \n", + " \"Vertex
    Open in Vertex AI Workbench\n", + "
    \n", + "
    \n", + " \n", + " \"GitHub
    View on GitHub\n", + "
    \n", + "
    \n", + "\n", + "
    \n", + "\n", + "Share to:\n", + "\n", + "\n", + " \"LinkedIn\n", + "\n", + "\n", + "\n", + " \"Bluesky\n", + "\n", + "\n", + "\n", + " \"X\n", + "\n", + "\n", + "\n", + " \"Reddit\n", + "\n", + "\n", + "\n", + " \"Facebook\n", + " " + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "uWOZOJjqOKS7" + }, + "source": [ + "| Authors |\n", + "| --- |\n", + "| James Su |\n", + "| [Ivan Nardini](https://github.com/inardini) |" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "niHZi5ftOKS7" + }, + "source": [ + "## Overview\n", + "\n", + "This tutorial shows you how to teach Gemini to generate better responses by showing it examples of what humans prefer.\n", + "\n", + "### What is Preference Optimization?\n", + "\n", + "Instead of labeling responses as \"correct\" or \"incorrect,\" preference optimization works with human preferences. You show the model pairs of responses to the same question—one that humans preferred and one they didn't—and the model learns to generate responses more like the preferred ones.\n", + "\n", + "Think of it like this: rather than teaching a student the \"right answer,\" you're showing them two essays and saying \"this style is better than that style.\"\n", + "\n", + "### What you'll learn\n", + "\n", + "By the end of this tutorial, you will:\n", + "1. Load a dataset with human preference ratings\n", + "2. Train Gemini to align with those preferences\n", + "3. See a clear before-and-after comparison of model outputs" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "DG0krygnOKS8" + }, + "source": [ + "## Get started" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "wxx1rwJkZjO-" + }, + "source": [ + "### Prerequisites\n", + "\n", + "- A Google Cloud project with billing enabled\n", + "- The Vertex AI API enabled ([enable it here](https://console.cloud.google.com/flows/enableapi?apiid=aiplatform.googleapis.com))\n", + "- No machine learning experience required!\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "fb0a5lzIOKS8" + }, + "source": [ + "### Install required packages\n", + "\n", + "We'll install three packages:\n", + "- `google-genai`: The Gemini SDK for Python\n", + "- `google-cloud-aiplatform`: Vertex AI client library\n", + "- `datasets`: HuggingFace library to easily load training data\n", + "\n", + "**⚠ Expected Behavior**: After running this cell, Colab will ask you to **restart the runtime**. This is normal and necessary—click the \"Restart Runtime\" button when you see it." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "hfxF-BvuOKS8" + }, + "outputs": [], + "source": [ + "%pip install --upgrade --quiet google-genai google-cloud-aiplatform datasets" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "MvNbfnGBOKS8" + }, + "source": [ + "### Authenticate (Colab only)\n", + "\n", + "If you're running this in Google Colab, you need to authenticate so the notebook can access your Google Cloud project. This cell will prompt you to sign in." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "a30oTrxLOKS8" + }, + "outputs": [], + "source": [ + "import sys\n", + "\n", + "if \"google.colab\" in sys.modules:\n", + " from google.colab import auth\n", + "\n", + " auth.authenticate_user()\n", + " print(\"✅ Authentication successful!\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "-G0StuemOKS8" + }, + "source": [ + "### Set up your Google Cloud project\n", + "\n", + "Replace `[your-project-id]` below with your actual Google Cloud project ID. You can find this in the [Google Cloud Console](https://console.cloud.google.com/)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "hYD8PtwFOKS9" + }, + "outputs": [], + "source": [ + "import os\n", + "\n", + "from google import genai\n", + "from google.genai import types\n", + "\n", + "# TODO: Replace with your actual project ID\n", + "# fmt: off\n", + "PROJECT_ID = \"[your-project-id]\" # @param {type: \"string\", placeholder: \"[your-project-id]\", isTemplate: true}\n", + "# fmt: on\n", + "\n", + "# Auto-detect from environment if not set\n", + "if not PROJECT_ID or PROJECT_ID == \"[your-project-id]\":\n", + " PROJECT_ID = str(os.environ.get(\"GOOGLE_CLOUD_PROJECT\", \"\"))\n", + "\n", + "if not PROJECT_ID:\n", + " raise ValueError(\"Please set your PROJECT_ID above\")\n", + "\n", + "# Region where the model will be tuned (us-central1 has the best availability)\n", + "LOCATION = \"us-central1\"\n", + "\n", + "# Create a unique bucket name using your project ID\n", + "# Bucket names must be globally unique across all of Google Cloud\n", + "BUCKET_NAME = f\"{PROJECT_ID}-preference-tuning\"\n", + "BUCKET_URI = f\"gs://{BUCKET_NAME}\"\n", + "\n", + "print(f\"📩 Creating GCS bucket: {BUCKET_NAME}...\")\n", + "\n", + "# Create the bucket (the 2>/dev/null hides the error if it already exists)\n", + "! gsutil mb -l {LOCATION} -p {PROJECT_ID} {BUCKET_URI} 2>/dev/null || echo \"(Bucket already exists, continuing...)\"\n", + "\n", + "# Initialize the Gemini client with your project\n", + "client = genai.Client(vertexai=True, project=PROJECT_ID, location=LOCATION)\n", + "\n", + "print(f\"✅ Using project: {PROJECT_ID}\")\n", + "print(f\"✅ Using region: {LOCATION}\")\n", + "print(f\"✅ Bucket: {BUCKET_URI}\")\n", + "print(\"✅ Client initialized\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "uh5l-wRGOKS9" + }, + "source": [ + "### Import libraries\n", + "\n", + "Now we'll import the Python libraries we need and set up the Gemini client." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "1IGsi7QaOKS9" + }, + "outputs": [], + "source": [ + "import json\n", + "\n", + "from IPython.display import Markdown, display\n", + "from datasets import load_dataset\n", + "\n", + "print(\"✅ Libraries imported\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "c_cFpT7xOKS9" + }, + "source": [ + "## Step 1: Load the training dataset\n", + "\n", + "We'll use **[UltraFeedback](https://huggingface.co/datasets/zhengr/ultrafeedback_binarized)**, a dataset containing thousands of prompts with multiple AI-generated responses rated by humans. For each prompt, humans indicated which response they preferred.\n", + "\n", + "This is perfect for teaching Gemini what kind of responses humans find more helpful.\n", + "\n", + "**What's in this dataset?**\n", + "- `prompt`: The user's question\n", + "- `chosen`: The response humans preferred (scored higher)\n", + "- `rejected`: The response humans didn't prefer (scored lower)\n", + "- `score_chosen` and `score_rejected`: The numerical ratings" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "W9OPLbJuOKS9" + }, + "outputs": [], + "source": [ + "print(\"đŸ“„ Loading UltraFeedback dataset from HuggingFace...\")\n", + "print(\"(This may take 1-2 minutes on first load)\\n\")\n", + "\n", + "# Load the dataset\n", + "# The dataset has already been processed to identify preferred vs rejected responses\n", + "dataset = load_dataset(\"zhengr/ultrafeedback_binarized\")\n", + "\n", + "print(\"✅ Dataset loaded!\\n\")\n", + "print(f\"Training examples: {len(dataset['train_prefs']):,}\")\n", + "print(f\"Test examples: {len(dataset['test_prefs']):,}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "8kHYzi0EOKS9" + }, + "source": [ + "Let's look at one example to understand the structure:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "u9JB-gtbOKS9" + }, + "outputs": [], + "source": [ + "# Grab the first example from the training set\n", + "sample = dataset[\"train_prefs\"][0]\n", + "\n", + "print(\"📋 Example from the dataset:\\n\")\n", + "print(f\"Prompt: {sample['prompt']}\\n\")\n", + "print(f\"Chosen score: {sample.get('score_chosen', 'N/A')}\")\n", + "print(f\"Rejected score: {sample.get('score_rejected', 'N/A')}\\n\")\n", + "print(\"Full structure:\")\n", + "print(json.dumps(sample, indent=2)[:1200] + \"\\n...\\n\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "L04UNQXUOKS9" + }, + "source": [ + "## Step 2: Transform the data for Gemini\n", + "\n", + "The dataset is in a generic format, but Gemini's tuning API expects a specific structure. We need to transform each example.\n", + "\n", + "**From UltraFeedback format:**\n", + "\n", + "```json\n", + "{\n", + " \"chosen\": [{\"role\": \"user\", \"content\": \"...\"},\n", + " {\"role\": \"assistant\", \"content\": \"...\"}],\n", + " \"rejected\": [{\"role\": \"user\", \"content\": \"...\"},\n", + " {\"role\": \"assistant\", \"content\": \"...\"}]\n", + "}\n", + "```\n", + "\n", + "**To Gemini format:**\n", + "\n", + "```json\n", + "{\n", + " \"contents\": [{\"role\": \"user\", \"parts\": [{\"text\": \"...\"}]}],\n", + " \"completions\": [\n", + " {\"score\": 1.0, \"completion\": {\"role\": \"model\", \"parts\": [{\"text\": \"...\"}]}},\n", + " {\"score\": 0.0, \"completion\": {\"role\": \"model\", \"parts\": [{\"text\": \"...\"}]}}\n", + " ]\n", + "}\n", + "```\n", + "\n", + "We'll write a helper function to do this transformation:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "61UFc70KOKS9" + }, + "outputs": [], + "source": [ + "def transform_to_gemini_format(example: dict) -> dict:\n", + " \"\"\"Convert one UltraFeedback example to Gemini's preference tuning format.\n", + "\n", + " Args:\n", + " example: A dict with 'chosen' and 'rejected' fields\n", + "\n", + " Returns:\n", + " A dict in Gemini's format with 'contents' and 'completions'\n", + " None if the example is invalid\n", + " \"\"\"\n", + " try:\n", + " # Access the fields directly\n", + " chosen = example[\"chosen\"]\n", + " rejected = example[\"rejected\"]\n", + "\n", + " # Validate: need at least 2 messages in each\n", + " if not isinstance(chosen, list) or len(chosen) < 2:\n", + " return None\n", + " if not isinstance(rejected, list) or len(rejected) < 2:\n", + " return None\n", + "\n", + " # Extract the user prompt (first message)\n", + " user_prompt = chosen[0].get(\"content\", \"\").strip()\n", + "\n", + " # Extract the model responses (second message)\n", + " chosen_response = chosen[1].get(\"content\", \"\").strip()\n", + " rejected_response = rejected[1].get(\"content\", \"\").strip()\n", + "\n", + " # Validate: all must be non-empty\n", + " if not user_prompt or not chosen_response or not rejected_response:\n", + " return None\n", + "\n", + " # Build the Gemini format\n", + " return {\n", + " \"contents\": [{\"role\": \"user\", \"parts\": [{\"text\": user_prompt}]}],\n", + " \"completions\": [\n", + " {\n", + " \"score\": 1.0,\n", + " \"completion\": {\n", + " \"role\": \"model\",\n", + " \"parts\": [{\"text\": chosen_response}],\n", + " },\n", + " },\n", + " {\n", + " \"score\": 0.0,\n", + " \"completion\": {\n", + " \"role\": \"model\",\n", + " \"parts\": [{\"text\": rejected_response}],\n", + " },\n", + " },\n", + " ],\n", + " }\n", + " except Exception:\n", + " return None\n", + "\n", + "\n", + "# Test it\n", + "transformed = transform_to_gemini_format(dataset[\"train_prefs\"][0])\n", + "print(\"✅ Transformation function ready\\n\")\n", + "print(\"Example transformed data:\")\n", + "print(json.dumps(transformed, indent=2)[:800] + \"\\n...\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "dn4TqxXyOKS9" + }, + "source": [ + "Now transform the full dataset and save to files:\n", + "\n", + "**Why we're doing this:** Vertex AI reads training data from files, not from Python variables. We'll save our transformed data as JSONL files (one JSON object per line), which is the standard format for ML training data." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "UbYGUDbsOKS9" + }, + "outputs": [], + "source": [ + "# For this tutorial, we'll use a subset to make training faster\n", + "# For production, you'd use more data (10k-100k examples)\n", + "NUM_TRAIN = 1000\n", + "NUM_VAL = 100\n", + "\n", + "# Transform training examples - keep going until we have NUM_TRAIN valid examples\n", + "print(f\"📝 Transforming training examples (target: {NUM_TRAIN})...\")\n", + "train_transformed = []\n", + "train_data = list(dataset[\"train_prefs\"])\n", + "index = 0\n", + "skipped = 0\n", + "\n", + "while len(train_transformed) < NUM_TRAIN and index < len(train_data):\n", + " result = transform_to_gemini_format(train_data[index])\n", + " if result is not None:\n", + " train_transformed.append(result)\n", + " else:\n", + " skipped += 1\n", + " index += 1\n", + "\n", + "print(\n", + " f\"✅ Successfully transformed {len(train_transformed)} training examples (skipped {skipped} invalid)\"\n", + ")\n", + "\n", + "# Transform validation examples - keep going until we have NUM_VAL valid examples\n", + "print(f\"\\n📝 Transforming validation examples (target: {NUM_VAL})...\")\n", + "val_transformed = []\n", + "val_index = index # Start where training left off\n", + "val_skipped = 0\n", + "\n", + "while len(val_transformed) < NUM_VAL and val_index < len(train_data):\n", + " result = transform_to_gemini_format(train_data[val_index])\n", + " if result is not None:\n", + " val_transformed.append(result)\n", + " else:\n", + " val_skipped += 1\n", + " val_index += 1\n", + "\n", + "print(\n", + " f\"✅ Successfully transformed {len(val_transformed)} validation examples (skipped {val_skipped} invalid)\"\n", + ")\n", + "\n", + "# Write to JSONL files (one JSON object per line)\n", + "with open(\"train_data.jsonl\", \"w\") as f:\n", + " for item in train_transformed:\n", + " f.write(json.dumps(item) + \"\\n\")\n", + "\n", + "with open(\"val_data.jsonl\", \"w\") as f:\n", + " for item in val_transformed:\n", + " f.write(json.dumps(item) + \"\\n\")\n", + "\n", + "print(\"\\n✅ Created training data files:\")\n", + "print(f\" - train_data.jsonl ({len(train_transformed)} examples)\")\n", + "print(f\" - val_data.jsonl ({len(val_transformed)} examples)\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "CLKG6Bx3OKS9" + }, + "source": [ + "## Step 3: Upload data to Google Cloud Storage\n", + "\n", + "Vertex AI runs in the cloud, so it needs our training files to be in cloud storage (Google Cloud Storage, or GCS).\n", + "\n", + "**What this cell does:**\n", + "1. Creates a GCS bucket (think of it like a cloud folder)\n", + "2. Uploads our JSONL files to that bucket\n", + "\n", + "**About `gsutil`:** This is Google's command-line tool for working with cloud storage. The commands below are like `cp` for copying files, but they work with cloud URLs that start with `gs://`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "cyJxGrnhOKS9" + }, + "outputs": [], + "source": [ + "print(\"\\nđŸ“€ Uploading training files to cloud storage...\")\n", + "\n", + "# Upload our JSONL files to the bucket\n", + "! gsutil cp train_data.jsonl {BUCKET_URI}/data/train_data.jsonl\n", + "! gsutil cp val_data.jsonl {BUCKET_URI}/data/val_data.jsonl\n", + "\n", + "# Store the cloud paths for use in the next step\n", + "TRAIN_URI = f\"{BUCKET_URI}/data/train_data.jsonl\"\n", + "VAL_URI = f\"{BUCKET_URI}/data/val_data.jsonl\"\n", + "\n", + "print(\"\\n✅ Data uploaded successfully!\")\n", + "print(f\" Training data: {TRAIN_URI}\")\n", + "print(f\" Validation data: {VAL_URI}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "cGZCW93eOKS-" + }, + "source": [ + "## Step 4: Configure tuning hyperparameters\n", + "\n", + "Before we start training, we need to set some configuration options (called \"hyperparameters\"). These control how aggressively the model learns from the preference data.\n", + "\n", + "**Here's what each parameter does:**\n", + "\n", + "| Parameter | What it controls | Good range | Our default |\n", + "|-----------|------------------|------------|-------------|\n", + "| **`beta`** | How much to change the model's behavior. Lower = more aggressive changes. | `0.01` - `0.5` | `0.1` |\n", + "| **`learning_rate_multiplier`** | How fast the model learns. Higher = faster but riskier. | `0.5` - `2.0` | `1.0` |\n", + "| **`adapter_size`** | How many parameters to tune. Bigger = more expressive but slower. | `ONE`, `TWO`, `FOUR`, `EIGHT`, `SIXTEEN` | `ONE` |\n", + "| **`epochs`** | How many times to go through the full dataset. | 1 - 3 | 1 |\n", + "\n", + "**For this tutorial, we're using these recommended defaults.** If you run this and the model doesn't improve enough, try lowering `beta` to `0.05`. If the model starts generating repetitive or strange text, try raising `beta` to `0.2`.\n", + "\n", + "⚠ **Important:** Setting `beta=0` will completely prevent learning. Don't do that!" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "qccSVi6lOKS-" + }, + "outputs": [], + "source": [ + "# Hyperparameter settings for this tuning run\n", + "# These are defined here because we're about to use them in the next cell\n", + "\n", + "EPOCHS = 1\n", + "ADAPTER_SIZE = \"ADAPTER_SIZE_ONE\"\n", + "LEARNING_RATE = 1.0\n", + "BETA = 0.1 # Lower beta = more aggressive alignment with preferences\n", + "\n", + "print(\"⚙ Hyperparameters configured:\")\n", + "print(f\" Epochs: {EPOCHS}\")\n", + "print(f\" Adapter size: {ADAPTER_SIZE}\")\n", + "print(f\" Learning rate multiplier: {LEARNING_RATE}\")\n", + "print(f\" Beta: {BETA}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "9jHMgCZhOKS-" + }, + "source": [ + "## Step 5: Submit the tuning job\n", + "\n", + "Now we're ready to start training! This cell will submit a tuning job to Vertex AI using the new SDK.\n", + "\n", + "**What happens when you run this:**\n", + "1. We call `client.tunings.tune()` with our configuration\n", + "2. Vertex AI queues the job (it might wait a few minutes for resources)\n", + "3. Training starts and runs for 30-60 minutes\n", + "4. When done, Vertex AI deploys your tuned model to an endpoint\n", + "\n", + "**This is asynchronous:** The job runs in the cloud. You'll get a job object immediately with a name/ID, but the training happens in the background. We'll check the status in the next step.\n", + "\n", + "**Expected output:** You should see `✅ Tuning job submitted successfully!` followed by a job name. If you see an error instead, double-check that you've enabled the Vertex AI API and that your project ID is correct." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "wA9TInM3OKS-" + }, + "outputs": [], + "source": [ + "print(\"🚀 Submitting tuning job to Vertex AI...\\n\")\n", + "\n", + "try:\n", + " # Create dataset objects for training and validation\n", + " # These reference the files we uploaded to GCS in Step 3\n", + " training_dataset = types.TuningDataset(gcs_uri=TRAIN_URI) # Defined in Step 3\n", + " validation_dataset = types.TuningDataset(gcs_uri=VAL_URI) # Defined in Step 3\n", + "\n", + " # Submit the tuning job using the new SDK\n", + " tuning_job = client.tunings.tune(\n", + " base_model=\"gemini-2.5-flash\",\n", + " training_dataset=training_dataset,\n", + " config=types.CreateTuningJobConfig(\n", + " tuned_model_display_name=\"gemini-2.5-flash-preference-tuned\",\n", + " method=\"PREFERENCE_TUNING\", # This specifies we're doing preference optimization\n", + " epoch_count=EPOCHS, # Defined in Step 4\n", + " adapter_size=ADAPTER_SIZE, # Defined in Step 4\n", + " learning_rate_multiplier=LEARNING_RATE, # Defined in Step 4\n", + " beta=BETA, # Defined in Step 4\n", + " validation_dataset=validation_dataset,\n", + " ),\n", + " )\n", + "\n", + " # Extract the job name for status checking\n", + " job_name = tuning_job.name\n", + " job_id = job_name.split(\"/\")[-1]\n", + "\n", + " print(\"✅ Tuning job submitted successfully!\\n\")\n", + " print(f\"Job Name: {job_name}\\n\")\n", + " print(f\"Job ID: {job_id}\\n\")\n", + " print(\"Monitor progress in the console:\")\n", + " print(\n", + " f\"https://console.cloud.google.com/vertex-ai/tuning/locations/{LOCATION}/tuningJob/{job_id}/monitor?project={PROJECT_ID}\"\n", + " )\n", + " print(\"\\n⏱ Training will take approximately 30-60 minutes.\")\n", + " print(\" You can run the next cell to check the status.\")\n", + "\n", + "except Exception as e:\n", + " print(f\"❌ Error submitting job: {e}\")\n", + " job_name = None" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "D_O6sEL9OKS-" + }, + "source": [ + "## Step 6: Check the job status\n", + "\n", + "The tuning job is running in the background. This cell lets you check on its progress.\n", + "\n", + "**Job states:**\n", + "- `JOB_STATE_PENDING`: Waiting for resources (can take 2-5 minutes)\n", + "- `JOB_STATE_RUNNING`: Training is in progress (30-60 minutes)\n", + "- `JOB_STATE_SUCCEEDED`: Done! Your model is ready to use\n", + "- `JOB_STATE_FAILED`: Something went wrong (check the error message)\n", + "\n", + "**What you need to do:**\n", + "1. Copy the Job Name from the cell above (it starts with `projects/...`)\n", + "2. Paste it into the `JOB_NAME` field below\n", + "3. Run this cell\n", + "\n", + "You can run this cell multiple times to keep checking the status. When it says `SUCCEEDED`, you're ready for Step 7!" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "1sKw724BOKS-" + }, + "outputs": [], + "source": [ + "# TODO: Paste your job name from Step 5 here\n", + "# fmt: off\n", + "JOB_NAME = \"projects/801452371447/locations/us-central1/tuningJobs/7028195920948756480\" # @param {type:\"string\"}\n", + "# fmt: on\n", + "\n", + "if not JOB_NAME or JOB_NAME == \"[your_job_name]\":\n", + " print(\"⚠ Please set JOB_NAME above with the value from Step 5\")\n", + " print(\n", + " \" (It should look like: projects/12345/locations/us-central1/tuningJobs/67890)\"\n", + " )\n", + "else:\n", + " try:\n", + " # Use the SDK to get job status\n", + " tuning_job = client.tunings.get(name=JOB_NAME)\n", + "\n", + " state = tuning_job.state\n", + "\n", + " print(f\"📊 Current Status: {state}\\n\")\n", + "\n", + " if state == \"JOB_STATE_PENDING\":\n", + " print(\"⏳ Job is queued, waiting for compute resources...\")\n", + " print(\" This can take 2-5 minutes. Run this cell again in a few minutes.\")\n", + "\n", + " elif state == \"JOB_STATE_RUNNING\":\n", + " print(\"🔄 Training is in progress!\")\n", + " print(\" This typically takes 30-60 minutes for 1000 examples.\")\n", + " print(\" Feel free to take a break and come back to check.\")\n", + "\n", + " elif state == \"JOB_STATE_SUCCEEDED\":\n", + " print(\"✅ Training completed successfully!\\n\")\n", + " print(\"🎯 Your tuned model is ready to use!\")\n", + " print(\" Continue to Step 7 to test it.\\n\")\n", + "\n", + " # Show the tuned model info\n", + " if hasattr(tuning_job, \"tuned_model\") and tuning_job.tuned_model:\n", + " if hasattr(tuning_job.tuned_model, \"endpoint\"):\n", + " print(f\"📝 Endpoint: {tuning_job.tuned_model.endpoint}\")\n", + "\n", + " elif state == \"JOB_STATE_FAILED\":\n", + " print(\"❌ Training failed.\\n\")\n", + " if hasattr(tuning_job, \"error\"):\n", + " print(\"Error details:\")\n", + " print(tuning_job.error)\n", + "\n", + " else:\n", + " print(f\"❓ Unknown state: {state}\")\n", + "\n", + " except Exception as e:\n", + " print(f\"❌ Error checking job status: {e}\")\n", + " print(\" Make sure you copied the full job name from Step 5.\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "J6FF6o5ROKS-" + }, + "source": [ + "## Step 7: Test the base model (before tuning)\n", + "\n", + "While we wait for tuning to complete (or after it's done), let's test the **original** Gemini model to see how it responds.\n", + "\n", + "We'll use a carefully selected test prompt from our validation set—one where human preferences were clear. This will let us see the \"before\" so we can compare it to the \"after.\"\n", + "\n", + "**Why this matters:** The whole point of tuning is to improve the model. To prove it worked, we need to show what the model was like before we tuned it." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "Vk_o7e0dOKS-" + }, + "outputs": [], + "source": [ + "# Select a prompt from our validation set (data the model hasn't seen during training)\n", + "# We'll pick example #50 because it tends to show clear improvement after tuning\n", + "test_example = dataset[\"train_prefs\"][NUM_TRAIN + 50]\n", + "test_prompt = test_example[\"prompt\"]\n", + "\n", + "print(\"📝 Test Prompt:\")\n", + "print(f\" {test_prompt}\")\n", + "print(\"\\n\" + \"=\" * 80)\n", + "\n", + "print(\"\\nđŸ€– Generating response from BASE model (untuned Gemini 2.5 Flash)...\\n\")\n", + "\n", + "# Call the base Gemini model\n", + "try:\n", + " base_response = client.models.generate_content(\n", + " model=\"gemini-2.5-flash\",\n", + " contents=test_prompt,\n", + " config={\n", + " \"max_output_tokens\": 1024, # Ensure we get complete responses\n", + " \"temperature\": 1.0,\n", + " },\n", + " )\n", + "\n", + " base_text = base_response.text\n", + "\n", + " print(\"=\" * 80)\n", + " print(\"BASE MODEL RESPONSE\")\n", + " print(\"=\" * 80)\n", + " print(base_text)\n", + " print(\"=\" * 80)\n", + "\n", + " print(\"\\n✅ Base model response generated\")\n", + " print(\" Keep this in mind for comparison with the tuned model!\")\n", + "\n", + "except Exception as e:\n", + " print(f\"❌ Error generating base model response: {e}\")\n", + " base_text = \"[Error: Could not generate base response]\"" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "0SKAgGmNOKS-" + }, + "source": [ + "## Step 8: Test the tuned model (after training)\n", + "\n", + "Now for the moment of truth! Let's test your newly tuned model with the **exact same prompt** from Step 7.\n", + "\n", + "**What to expect:** The tuned model should generate a response that better matches the style and quality of the \"chosen\" responses in your training data. It won't be perfect, but you should see noticeable improvement in helpfulness, accuracy, or style.\n", + "\n", + "**Important note:** We set `thinking_budget=0` because preference-tuned models learn to generate good responses directly, without needing to \"think\" first. Setting this to `0` makes responses faster and often better for tuned models.\n", + "\n", + "**What you need to do:**\n", + "1. Make sure Step 6 showed `SUCCEEDED`\n", + "2. You should still have the `tuning_job` object from Step 6 in memory\n", + "3. Run this cell to generate a response from your tuned model" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "zfQKCkm4OKS-" + }, + "outputs": [], + "source": [ + "# Check if we have a tuning_job object from Step 6\n", + "if \"tuning_job\" not in locals():\n", + " print(\"⚠ Please run Step 6 first to get the tuning job status\")\n", + " print(\" Make sure the job status is SUCCEEDED before running this cell\")\n", + "elif not hasattr(tuning_job, \"tuned_model\") or not tuning_job.tuned_model:\n", + " print(\"⚠ The tuning job doesn't have a tuned model yet\")\n", + " print(\" Make sure the job status is SUCCEEDED (check Step 6)\")\n", + "elif (\n", + " not hasattr(tuning_job.tuned_model, \"endpoint\")\n", + " or not tuning_job.tuned_model.endpoint\n", + "):\n", + " print(\"⚠ The tuned model doesn't have an endpoint\")\n", + " print(\" Make sure the job status is SUCCEEDED (check Step 6)\")\n", + "else:\n", + " print(\"đŸ€– Generating response from TUNED model...\\n\")\n", + "\n", + " try:\n", + " # Use the tuned model's endpoint to generate content\n", + " # The SDK makes this simple - just use the endpoint path as the model name\n", + " tuned_response = client.models.generate_content(\n", + " model=tuning_job.tuned_model.endpoint,\n", + " contents=test_prompt, # Defined in Step 7\n", + " config={\n", + " \"thinking_config\": {\n", + " \"thinking_budget\": 0\n", + " }, # No thinking needed for tuned models\n", + " \"max_output_tokens\": 1024, # Match the base model setting\n", + " \"temperature\": 1.0,\n", + " },\n", + " )\n", + "\n", + " tuned_text = tuned_response.text\n", + "\n", + " print(\"=\" * 80)\n", + " print(\"TUNED MODEL RESPONSE\")\n", + " print(\"=\" * 80)\n", + " print(tuned_text)\n", + " print(\"=\" * 80)\n", + "\n", + " print(\"\\n✅ Tuned model response generated successfully!\")\n", + " print(\" Continue to Step 9 to see a side-by-side comparison.\")\n", + "\n", + " except Exception as e:\n", + " print(f\"❌ Error generating response: {e}\")\n", + " print(\" Make sure the tuning job completed successfully.\")\n", + " tuned_text = \"[Error: Could not generate tuned response]\"" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "SXBNTCSKOKS-" + }, + "source": [ + "## Step 9: Side-by-side comparison 🎯\n", + "\n", + "This is the **\"wow moment\"** of the tutorial. Below, you'll see the exact same prompt answered by both the base model and your tuned model.\n", + "\n", + "**What to look for:**\n", + "- Is the tuned response more helpful or detailed?\n", + "- Does it avoid patterns from \"rejected\" responses in the training data?\n", + "- Does it better match the style humans preferred?\n", + "\n", + "The improvement might be subtle or dramatic depending on the prompt. The key is that the tuned model learned from 1,000 examples of human preferences, while the base model didn't." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "7WLskeV6OKS-" + }, + "outputs": [], + "source": [ + "# Build a formatted comparison\n", + "comparison_md = f\"\"\"\n", + "# 🎯 Before and After Comparison\n", + "\n", + "## Prompt\n", + "\n", + "> {test_prompt if \"test_prompt\" in locals() else \"Run Step 7 first\"}\n", + "\n", + "---\n", + "\n", + "## Base Model Response (Untuned)\n", + "\n", + "{base_text if \"base_text\" in locals() else \"*Run Step 7 first*\"}\n", + "\n", + "---\n", + "\n", + "## Tuned Model Response (After Preference Optimization)\n", + "\n", + "{tuned_text if \"tuned_text\" in locals() else \"*Run Step 8 first*\"}\n", + "\n", + "---\n", + "\n", + "## 📊 Analysis\n", + "\n", + "Compare the two responses above. Your tuned model learned from 1,000 examples where humans indicated which responses they preferred.\n", + "\n", + "**Key improvements to notice:**\n", + "- **Helpfulness**: Is the tuned response more complete or actionable?\n", + "- **Style**: Does it match the tone humans preferred in the training data?\n", + "- **Accuracy**: Does it avoid common mistakes seen in rejected responses?\n", + "\n", + "**Try it yourself:** You can replace the test prompt in Step 7 with any question and see how your tuned model compares to the base model!\n", + "\"\"\"\n", + "\n", + "display(Markdown(comparison_md))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "cGtxH4a0OKS-" + }, + "source": [ + "## 🎉 Congratulations!\n", + "\n", + "You've successfully:\n", + "1. Loaded a dataset with human preference ratings\n", + "2. Transformed it into Gemini's format\n", + "3. Tuned Gemini to align with those preferences\n", + "4. Compared the base and tuned models\n", + "\n", + "### Next steps\n", + "\n", + "- **Experiment with hyperparameters**: Try lowering `beta` to `0.05` or increasing training data to 5,000 examples\n", + "- **Use your own data**: Replace the UltraFeedback dataset with your own preference data\n", + "- **Combine with SFT**: For best results, first do supervised fine-tuning on preferred responses, *then* apply preference optimization\n", + "\n", + "### Learn more\n", + "\n", + "- [Vertex AI Tuning Documentation](https://cloud.google.com/vertex-ai/docs/generative-ai/models/tune-models)\n", + "- [DPO Paper (Direct Preference Optimization)](https://arxiv.org/abs/2305.18290)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "6Yfulsj0OKS-" + }, + "source": [ + "## Cleaning up\n", + "\n", + "To avoid ongoing charges, you can delete the resources created in this tutorial.\n", + "\n", + "**To delete resources**, uncomment and run the cells below.\n", + "\n", + "**Note:** Deleting the endpoint stops billing for the deployed model. The training data in GCS has minimal cost, but you can delete the bucket if you won't need it again." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "njjs_G7WOKS-" + }, + "outputs": [], + "source": [ + "# # Delete the tuned model endpoint\n", + "# # This stops billing for the deployed model\n", + "#\n", + "# if 'tuning_job' in locals() and hasattr(tuning_job, 'tuned_model'):\n", + "# if tuning_job.tuned_model and hasattr(tuning_job.tuned_model, 'endpoint'):\n", + "# endpoint_path = tuning_job.tuned_model.endpoint\n", + "#\n", + "# try:\n", + "# from google.cloud import aiplatform\n", + "#\n", + "# # Initialize aiplatform client\n", + "# aiplatform.init(project=PROJECT_ID, location=LOCATION)\n", + "#\n", + "# # Extract endpoint ID from the full path\n", + "# endpoint_id = endpoint_path.split(\"/\")[-1]\n", + "#\n", + "# # Get and delete the endpoint\n", + "# endpoint = aiplatform.Endpoint(endpoint_id)\n", + "# endpoint.delete(force=True)\n", + "#\n", + "# print(\"✅ Endpoint deletion initiated successfully\")\n", + "# print(\" (Deletion may take a few minutes to complete)\")\n", + "#\n", + "# except Exception as e:\n", + "# print(f\"⚠ Could not delete endpoint: {e}\")\n", + "# print(\" You can manually delete it in the Vertex AI console\")\n", + "# else:\n", + "# print(\"⚠ No endpoint found in tuning_job object\")\n", + "# else:\n", + "# print(\"⚠ No tuning_job object found. Run Step 6 first.\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "7vMsReVROKS-" + }, + "outputs": [], + "source": [ + "# # Delete the GCS bucket and all training data\n", + "# # Warning: This permanently deletes all files in the bucket\n", + "#\n", + "# if 'BUCKET_URI' in locals():\n", + "# try:\n", + "# ! gsutil -m rm -r {BUCKET_URI}\n", + "# print(f\"✅ Deleted bucket: {BUCKET_URI}\")\n", + "# except Exception as e:\n", + "# print(f\"⚠ Could not delete bucket: {e}\")\n", + "# print(\" You can manually delete it in the Cloud Storage console\")" + ] + } + ], + "metadata": { + "colab": { + "name": "dpo_gemini_get_started.ipynb", + "toc_visible": true + }, + "kernelspec": { + "display_name": "Python 3", + "name": "python3" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} diff --git a/gemini/tuning/genai-mlops-tune-and-eval/.gitignore b/gemini/tuning/genai-mlops-tune-and-eval/.gitignore new file mode 100644 index 0000000..678e23b --- /dev/null +++ b/gemini/tuning/genai-mlops-tune-and-eval/.gitignore @@ -0,0 +1,6 @@ +__pycache__ +pipeline.json +venv +local/local_outputs +local/pipeline.json +local/venv \ No newline at end of file diff --git a/gemini/tuning/genai-mlops-tune-and-eval/README.md b/gemini/tuning/genai-mlops-tune-and-eval/README.md new file mode 100644 index 0000000..2f1a1f5 --- /dev/null +++ b/gemini/tuning/genai-mlops-tune-and-eval/README.md @@ -0,0 +1,83 @@ +# Gen AI MLOps Tune and Evaluation + +Author: [Chris Willis](https://github.com/willisc7) + +This tutorial will take you through using Vertex AI Pipelines to automate tuning an LLM and evaluating it against a previously tuned LLM. The example used is an LLM that summarizes a week of glucose values for a diabetes patient. + +![Diagram](https://storage.googleapis.com/github-repo/generative-ai/gemini/tuning/mlops-tune-and-eval/diagram.png) + +## Optional: Prepare the data + +This step is optional because I've already prepared the data in `gs://github-repo/generative-ai/gemini/tuning/mlops-tune-and-eval/patient_1_glucose_examples.jsonl`. + +- Create a week of glucose sample data for one patient using the following prompt with Gemini: + + ```none + Create a CSV with a week's worth of example glucose values for a diabetic patient. The columns should be date, time, patient ID, and glucose value. Each day there should be timestamps for 7am, 8am, 11am, 12pm, 5pm, and 6pm. Most of the glucose values should be between 70 and 100. Some of the glucose values should be 100-150. + ``` + +- Flatten the CSV by doing the following: + 1. Open the CSV + 2. Press Ctrl + a to select all text + 3. Press Alt + Shift + i to go to the end of each line + 4. Add a newline character (i.e. \n) + 5. Press Delete to squash it all to a single line +- Copy `glucose_examples_template.jsonl` (or create it if it doesn't exist) to `patient_X_glucose_examples.jsonl` +- Copy the flattened CSV and paste it into the patient_X_glucose_examples.jsonl +- Flatten the contents of the patient_X_glucose_examples.jsonl file using a JSON to JSONL converter online + +## Setup IAM, Tuning Examples, and Vertex AI Pipelines + +- Grant Default Compute Service Account IAM permissions + + ```sh + PROJECT_NUMBER=$(gcloud projects describe $(gcloud config get-value project) --format="value(projectNumber)") + SERVICE_ACCOUNT="${PROJECT_NUMBER}-compute@developer.gserviceaccount.com" + + gcloud projects add-iam-policy-binding $PROJECT_NUMBER \ + --member="serviceAccount:${SERVICE_ACCOUNT}" \ + --role="roles/aiplatform.user" + gcloud projects add-iam-policy-binding $PROJECT_NUMBER \ + --member="serviceAccount:${SERVICE_ACCOUNT}" \ + --role="roles/storage.objectUser" + ``` + +- Enable the Cloud Resource Manager API + + ```sh + gcloud services enable cloudresourcemanager.googleapis.com + ``` + +- Create the pipeline root bucket + + ```sh + gcloud storage buckets create gs://vertex-ai-pipeline-root-$(date +%Y%m%d) + ``` + +## Run Vertex AI Pipelines + +- Install required packages and compile the pipeline + + ```sh + python3 -m venv venv + source venv/bin/activate + pip install -r requirements.txt + kfp dsl compile --py pipeline.py --output pipeline.json + ``` + +- Edit `pipeline.py` and change the following: + + - `project` - change to your project ID + +- Edit `submit_pipeline_job.py` and change the following: + + - `pipeline_root` - change to the `gs://vertex-ai-pipeline-root-` bucket you created earlier + - `project` - change to your project ID + +- Create the pipeline run + + ```sh + python submit_pipeline_job.py + ``` + +- For subsequent runs, change `baseline_model_endpoint` in `submit_pipeline_job.py` to a tuned model endpoint you want to compare against (typically the previously trained endpoint) diff --git a/gemini/tuning/genai-mlops-tune-and-eval/pipeline.py b/gemini/tuning/genai-mlops-tune-and-eval/pipeline.py new file mode 100644 index 0000000..28bd870 --- /dev/null +++ b/gemini/tuning/genai-mlops-tune-and-eval/pipeline.py @@ -0,0 +1,267 @@ +# pylint: disable=import-outside-toplevel,too-many-locals,too-many-arguments,too-many-positional-arguments,unused-argument +from typing import NamedTuple + +from kfp.v2 import dsl +from kfp.v2.dsl import component + + +@component( + packages_to_install=["google-cloud-aiplatform", "vertexai"], + base_image="python:3.9", +) +def gemini_tuning_component( + project: str, + location: str, + source_model: str, + train_dataset_uri: str, +) -> str: + """Output the tuned model name as a string""" + import time + + import vertexai + from vertexai.tuning import sft + + vertexai.init(project=project, location=location) + + tuned_model_display_name = f"tuned-{source_model}-{int(time.time())}" + + sft_tuning_job = sft.train( + source_model=source_model, + train_dataset=train_dataset_uri, + tuned_model_display_name=tuned_model_display_name, + ) + + while not sft_tuning_job.has_ended: + time.sleep(60) + sft_tuning_job.refresh() + + print(f"Tuned Model Endpoint Name: {sft_tuning_job.tuned_model_endpoint_name}") + return sft_tuning_job.tuned_model_endpoint_name + + +@component( + packages_to_install=[ + "google-cloud-aiplatform", + "vertexai", + "plotly", + "pandas", + "IPython", + "google-cloud-aiplatform[evaluation]", + ], + base_image="python:3.9", +) +def model_comparison_component( + project: str, + location: str, + baseline_model_endpoint: str, # Baseline model name + candidate_model_endpoint: str, # Candidate model name +) -> NamedTuple("outputs", best_response=str, metrics=dict): # type: ignore[valid-type] + """Compares base model to newly tuned model""" + import functools + import typing + import uuid + from functools import partial + from typing import Union + + import pandas as pd + from vertexai.evaluation import EvalResult, EvalTask, MetricPromptTemplateExamples + from vertexai.generative_models import GenerationConfig, GenerativeModel + + experiment_name = "qa-quality" + + def pairwise_greater( + instructions: str, + context: str, + project: str, + location: str, + experiment_name: str, + baseline: str, + candidate: str, + ) -> tuple: + """Takes Instructions, Context and two different responses. + Returns the response which best matches the instructions/Context for the given + quality metric ( in this case question answering). + More details on the web API and different quality metrics which this function + can be extended to can be found on + https://cloud.google.com/vertex-ai/generative-ai/docs/model-reference/evaluation + """ + eval_dataset = pd.DataFrame( + { + "instruction": [instructions], + "context": [context], + "response": [candidate], + "baseline_model_response": [baseline], + } + ) + + eval_task = EvalTask( + dataset=eval_dataset, + metrics=[ + MetricPromptTemplateExamples.Pairwise.QUESTION_ANSWERING_QUALITY, + ], + experiment=experiment_name, + ) + results = eval_task.evaluate( + prompt_template="{instruction} \n {context}", + experiment_run_name="gemini-qa-pairwise-" + str(uuid.uuid4()), + ) + result = results.metrics_table[ + [ + "pairwise_question_answering_quality/pairwise_choice", + "pairwise_question_answering_quality/explanation", + ] + ].to_dict("records")[0] + choice = ( + baseline + if result["pairwise_question_answering_quality/pairwise_choice"] + == "BASELINE" + else candidate + ) + return (choice, result["pairwise_question_answering_quality/explanation"]) + + def greater(cmp: typing.Callable, a: str, b: str) -> int: + """A comparison function which takes the comparison function, and two variables as input + and returns the one which is greater according to the logic defined inside the cmp function. + """ + choice, _ = cmp(a, b) + + if choice == a: + return 1 + return -1 + + def pointwise_eval( + instruction: str, + context: str, + responses: list[str], + eval_metrics: Union[list[MetricPromptTemplateExamples.Pointwise], None] = None, + experiment_name: str = experiment_name, + ) -> EvalResult: + """Takes the instruction, context and a variable number of corresponding + generated responses, and returns the pointwise evaluation metrics + for each of the provided metrics. For this example the metrics are + Q & A related, however the full list can be found on the website: + https://cloud.google.com/vertex-ai/generative-ai/docs/models/online-pipeline-services + """ + instructions = [instruction] * len(responses) + + contexts = [context] * len(responses) + + eval_dataset = pd.DataFrame( + { + "instruction": instructions, + "context": contexts, + "response": responses, + } + ) + + eval_metrics = eval_metrics or [ + MetricPromptTemplateExamples.Pointwise.QUESTION_ANSWERING_QUALITY, + MetricPromptTemplateExamples.Pointwise.GROUNDEDNESS, + ] + + eval_task = EvalTask( + dataset=eval_dataset, metrics=eval_metrics, experiment=experiment_name + ) + results = eval_task.evaluate( + prompt_template="{instruction} \n {context}", + experiment_run_name="gemini-qa-pointwise-" + str(uuid.uuid4()), + ) + return results + + def rank_responses(instruction: str, context: str, responses: list[str]) -> tuple: + """Takes the instruction, context and a variable number of responses as + input, and returns the best performing response as well as its associated + human-readable pointwise quality metrics for the configured criteria in the above functions. + The process consists of two steps: + 1. Selecting the best response by using Pairwise comparisons between the responses for + the user specified metric ( e.g. Q & A) + 2. Doing pointwise evaluation of the best response and returning human-readable quality + metrics and explanation along with the best response. + """ + cmp_f = partial( + pairwise_greater, instruction, context, project, location, experiment_name + ) + cmp_greater = partial(greater, cmp_f) + + pairwise_best_response = max(responses, key=functools.cmp_to_key(cmp_greater)) + pointwise_metric = pointwise_eval( + instruction, context, [pairwise_best_response] + ) + qa_metrics = pointwise_metric.metrics_table[ + [ + col + for col in pointwise_metric.metrics_table.columns + if ("question_answering" in col) or ("groundedness" in col) + ] + ].to_dict("records")[0] + + return pairwise_best_response, qa_metrics + + # Compare response from baseline model to candidate model to see which is better + generation_config = GenerationConfig( + temperature=0.4, + max_output_tokens=512, + ) + baseline_model = GenerativeModel( + baseline_model_endpoint, + generation_config=generation_config, + ) + candidate_model = GenerativeModel( + candidate_model_endpoint, + generation_config=generation_config, + ) + + instruction_qa = "Analyze the glucose trends in the glucose values provided in the CSV contained in the context. Ensure the analysis you provide can easily be understood by a diabetes patient with no medical expertise." + context_qa = ( + "Context:\n" + "```csv\ndate,time,patient ID,glucose\n2024-11-12,7:00 AM,1,80\n2024-11-12,8:00 AM,1,96\n2024-11-12,11:00 AM,1,90\n2024-11-12,12:00 PM,1,115\n2024-11-12,5:00 PM,1,77\n2024-11-12,6:00 PM,1,80\n2024-11-13,7:00 AM,1,94\n2024-11-13,8:00 AM,1,100\n2024-11-13,11:00 AM,1,87\n2024-11-13,12:00 PM,1,126\n2024-11-13,5:00 PM,1,71\n2024-11-13,6:00 PM,1,82\n2024-11-14,7:00 AM,1,84\n2024-11-14,8:00 AM,1,72\n2024-11-14,11:00 AM,1,96\n2024-11-14,12:00 PM,1,110\n2024-11-14,5:00 PM,1,99\n2024-11-14,6:00 PM,1,74\n2024-11-15,7:00 AM,1,96\n2024-11-15,8:00 AM,1,97\n2024-11-15,11:00 AM,1,99\n2024-11-15,12:00 PM,1,130\n2024-11-15,5:00 PM,1,99\n2024-11-15,6:00 PM,1,87\n2024-11-16,7:00 AM,1,89\n2024-11-16,8:00 AM,1,92\n2024-11-16,11:00 AM,1,77\n2024-11-16,12:00 PM,1,105\n2024-11-16,5:00 PM,1,79\n2024-11-16,6:00 PM,1,90\n2024-11-17,7:00 AM,1,74\n2024-11-17,8:00 AM,1,82\n2024-11-17,11:00 AM,1,74\n2024-11-17,12:00 PM,1,78\n2024-11-17,5:00 PM,1,95\n2024-11-17,6:00 PM,1,74\n2024-11-18,7:00 AM,1,95\n2024-11-18,8:00 AM,1,87\n2024-11-18,11:00 AM,1,79\n2024-11-18,12:00 PM,1,90\n2024-11-18,5:00 PM,1,79\n2024-11-18,6:00 PM,1,77\n" + ) + prompt_qa = instruction_qa + "\n" + context_qa + "\n\nAnswer:\n" + + baseline_model_response = baseline_model.generate_content( + contents=prompt_qa, + ) + candidate_model_response = candidate_model.generate_content( + contents=prompt_qa, + ) + responses = [ + baseline_model_response.candidates[0].text, + candidate_model_response.candidates[0].text, + ] + + best_response, metrics = rank_responses(instruction_qa, context_qa, responses) + + for ix, response in enumerate(responses, start=1): + print(f"Response no. {ix}: \n {response}") + + print(f"Best response: {best_response}") + print(f"Metrics: {metrics}") + outputs = NamedTuple("outputs", best_response=str, metrics=dict) # type: ignore[misc] + return outputs(best_response, metrics) # type: ignore[call-arg] + + +@dsl.pipeline(name="gemini-tuning-pipeline") +def gemini_tuning_pipeline( + project: str = "[your-project-id]", + location: str = "us-central1", + source_model_name: str = "gemini-2.0-flash", + train_data_uri: str = "gs://github-repo/generative-ai/gemini/tuning/mlops-tune-and-eval/patient_1_glucose_examples.jsonl", + # For first run, set `baseline_model_endpoint`` to any tunable Gemini model + # because a tuned model endpoint doesn't exist yet + baseline_model_endpoint: str = "gemini-2.0-flash", + # For subsequent runs, set baseline_model_endpoint to a tuned model endpoint + # baseline_model_endpoint: str = "projects/824264063118/locations/us-central1/endpoints/797393320253849600", +) -> None: + """Defines the pipeline to tune a model and compare it to the previously tuned model""" + tuning_task = gemini_tuning_component( + project=project, + location=location, + source_model=source_model_name, + train_dataset_uri=train_data_uri, + ) + model_comparison_component( + project=project, + location=location, + baseline_model_endpoint=baseline_model_endpoint, + candidate_model_endpoint=tuning_task.output, + ) diff --git a/gemini/tuning/genai-mlops-tune-and-eval/requirements.txt b/gemini/tuning/genai-mlops-tune-and-eval/requirements.txt new file mode 100644 index 0000000..2bc81d0 --- /dev/null +++ b/gemini/tuning/genai-mlops-tune-and-eval/requirements.txt @@ -0,0 +1,45 @@ +annotated-types==0.7.0 +cachetools==5.5.2 +certifi==2024.12.14 +charset-normalizer==3.4.1 +click==8.1.8 +docstring_parser==0.16 +google-api-core==2.24.2 +google-auth==2.38.0 +google-cloud-aiplatform==1.87.0 +google-cloud-bigquery==3.31.0 +google-cloud-core==2.4.3 +google-cloud-resource-manager==1.14.2 +google-cloud-storage==2.19.0 +google-crc32c==1.7.1 +google-resumable-media==2.7.2 +googleapis-common-protos==1.69.2 +grpc-google-iam-v1==0.14.2 +grpcio==1.71.0 +grpcio-status==1.71.0 +idna==3.10 +kfp==2.12.1 +kfp-pipeline-spec==0.6.0 +kfp-server-api==2.4.0 +kubernetes==30.1.0 +numpy==2.2.4 +oauthlib==3.2.2 +packaging==24.2 +proto-plus==1.26.1 +protobuf==4.25.8 +pyasn1==0.6.1 +pyasn1_modules==0.4.2 +pydantic==2.11.1 +pydantic_core==2.33.1 +python-dateutil==2.9.0.post0 +PyYAML==6.0.2 +requests==2.32.3 +requests-oauthlib==2.0.0 +requests-toolbelt==0.10.1 +rsa==4.9 +shapely==2.0.7 +six==1.17.0 +tabulate==0.9.0 +typing_extensions==4.13.0 +urllib3==1.26.20 +websocket-client==1.8.0 diff --git a/gemini/tuning/genai-mlops-tune-and-eval/submit_pipeline_job.py b/gemini/tuning/genai-mlops-tune-and-eval/submit_pipeline_job.py new file mode 100644 index 0000000..00763cf --- /dev/null +++ b/gemini/tuning/genai-mlops-tune-and-eval/submit_pipeline_job.py @@ -0,0 +1,19 @@ +from google.cloud import aiplatform + +aiplatform.init( + project="[your-project-id]", location="us-central1" +) # Initialize the Vertex AI SDK + +job = aiplatform.PipelineJob( + display_name="tuned-gemini-2.0-flash", # Give your run a name + template_path="pipeline.json", # Path to the compiled pipeline JSON + pipeline_root="gs://vertex-ai-pipeline-root-20250116", # Cloud Storage location for pipeline artifacts + parameter_values={ + "project": "[your-project-id]", + "location": "us-central1", + "source_model_name": "gemini-2.0-flash", + "train_data_uri": "gs://github-repo/generative-ai/gemini/tuning/mlops-tune-and-eval/patient_1_glucose_examples.jsonl", + }, # Pass pipeline parameter values here +) + +job.run() # Submit the pipeline for execution diff --git a/gemini/tuning/sft_gemini_automatic_evaluation.ipynb b/gemini/tuning/sft_gemini_automatic_evaluation.ipynb new file mode 100644 index 0000000..c861ba8 --- /dev/null +++ b/gemini/tuning/sft_gemini_automatic_evaluation.ipynb @@ -0,0 +1,1251 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "i3oNB_qC4X2Y" + }, + "outputs": [], + "source": [ + "# Copyright 2025 Google LLC\n", + "#\n", + "# Licensed under the Apache License, Version 2.0 (the \"License\");\n", + "# you may not use this file except in compliance with the License.\n", + "# You may obtain a copy of the License at\n", + "#\n", + "# https://www.apache.org/licenses/LICENSE-2.0\n", + "#\n", + "# Unless required by applicable law or agreed to in writing, software\n", + "# distributed under the License is distributed on an \"AS IS\" BASIS,\n", + "# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n", + "# See the License for the specific language governing permissions and\n", + "# limitations under the License." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "c4-kxwz23nzr" + }, + "source": [ + "# Supervised Fine-Tuning with integrated Gen AI Evaluation\n", + "\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + "
    \n", + " \n", + " \"Google
    Open in Colab\n", + "
    \n", + "
    \n", + " \n", + " \"Google
    Open in Colab Enterprise\n", + "
    \n", + "
    \n", + " \n", + " \"Vertex
    Open in Workbench\n", + "
    \n", + "
    \n", + " \n", + " \"GitHub
    View on GitHub\n", + "
    \n", + "
    \n", + "\n", + "
    \n", + "\n", + "Share to:\n", + "\n", + "\n", + " \"LinkedIn\n", + "\n", + "\n", + "\n", + " \"Bluesky\n", + "\n", + "\n", + "\n", + " \"X\n", + "\n", + "\n", + "\n", + " \"Reddit\n", + "\n", + "\n", + "\n", + " \"Facebook\n", + " " + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "pO98gUu-4eTJ" + }, + "source": [ + "| Authors |\n", + "| --- |\n", + "| Kelsi Lakey |\n", + "| [Ivan Nardini](https://github.com/inardini) |" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "RaUnT2fXLRe5" + }, + "source": [ + "This notebook demonstrates a powerful new feature in Vertex AI: [integrated evaluation for supervised fine-tuning (SFT)]((https://cloud.devsite.corp.google.com/vertex-ai/generative-ai/docs/models/gemini-use-supervised-tuning#create_a_text_model_supervised_tuning_job)).\n", + "\n", + "Traditionally, tuning a model and evaluating it are two separate, time-consuming steps. This creates a slow feedback loop, making it difficult to iterate quickly.\n", + "\n", + "This notebook shows you the new, integrated workflow where you can get performance metrics on model checkpoints *as they are being trained*. By adding a simple EvaluationConfig to your tuning job, you can now get performance metrics using Gen AI Evaluation Service on model checkpoints as they are being trained.\n", + "\n", + "This gives allows you:\n", + "\n", + "* Faster Iteration: See if a tuning run is working in minutes, not hours.\n", + "* Cost Savings: Identify and stop bad runs early.\n", + "* Better Models: Get a clear view of how your model improves over time, helping you pinpoint the best checkpoint and avoid overfitting.\n", + "\n", + "\n", + "You will learn how to:\n", + "\n", + "* Configure an EvaluationConfig to define custom, model-based metrics. \n", + "* Launch a supervised fine-tuning job for gemini-2.5-flash with automatic evaluation enabled. \n", + "* Monitor the job and retrieve performance results for each checkpoint programmatically. \n", + "* Find and analyze your tuning and evaluation results in the Vertex AI Experiments UI." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "llEFILYz2aye" + }, + "source": [ + "## Get started" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "oo2rh4cC2e1r" + }, + "source": [ + "### Install Google Gen AI SDK and other required packages\n", + "\n", + "The new Google Gen AI SDK provides a unified interface to Gemini through both the Gemini Developer API and the Gemini API on Vertex AI. With a few exceptions, code that runs on one platform will run on both. This means that you can prototype an application using the Developer API and then migrate the application to Vertex AI without rewriting your code." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "l_ok3vdw2cyf" + }, + "outputs": [], + "source": [ + "%pip install --upgrade --user --quiet google-genai google-cloud-aiplatform gradio plotly" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "21gF8JP8RPso" + }, + "source": [ + "### Authenticate your notebook environment (Colab only)\n", + "\n", + "You'll need a Google Cloud project with the Vertex AI API enabled. Authenticate your notebook environment to continue." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "86VNaqlgD9rK" + }, + "outputs": [], + "source": [ + "import sys\n", + "\n", + "if \"google.colab\" in sys.modules:\n", + " from google.colab import auth\n", + "\n", + " auth.authenticate_user()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "zEWOpk9Qd-g3" + }, + "source": [ + "### Set Google Cloud project information\n", + "\n", + "To get started using Vertex AI, you must have an existing Google Cloud project and [enable the Vertex AI API](https://console.cloud.google.com/flows/enableapi?apiid=aiplatform.googleapis.com).\n", + "\n", + "Learn more about [setting up a project and a development environment](https://cloud.google.com/vertex-ai/docs/start/cloud-environment)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "8OmiMYjpeHv8" + }, + "outputs": [], + "source": [ + "# Use the environment variable if the user doesn't provide Project ID.\n", + "import os\n", + "\n", + "from google import genai\n", + "from google.cloud import aiplatform\n", + "\n", + "# Please fill in these values for your project.\n", + "PROJECT_ID = \"[your-project-id]\" # @param {type: \"string\", placeholder: \"[your-project-id]\", isTemplate: true}\n", + "if not PROJECT_ID or PROJECT_ID == \"[your-project-id]\":\n", + " PROJECT_ID = str(os.environ.get(\"GOOGLE_CLOUD_PROJECT\"))\n", + "\n", + "LOCATION = os.environ.get(\"GOOGLE_CLOUD_REGION\", \"us-central1\")\n", + "\n", + "# A GCS bucket is required for tuning and evaluation artifacts.\n", + "BUCKET_NAME = \"[your-gcs-bucket-name]\" # @param {type:\"string\", placeholder: \"[your-gcs-bucket-name]\", isTemplate: true}\n", + "BUCKET_URI = f\"gs://{BUCKET_NAME}\"\n", + "\n", + "# Create the GCS bucket if it doesn't exist\n", + "!gsutil mb -l {LOCATION} -p {PROJECT_ID} {BUCKET_URI}\n", + "\n", + "# Initialize the Vertex AI and Gen AI SDKs\n", + "aiplatform.init(project=PROJECT_ID, location=LOCATION, staging_bucket=BUCKET_URI)\n", + "client = genai.Client(vertexai=True, project=PROJECT_ID, location=LOCATION)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "k8CI-TcqD06L" + }, + "source": [ + "### Import libraries" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "rerpHL_eEG8D" + }, + "outputs": [], + "source": [ + "import time\n", + "import uuid\n", + "\n", + "import pandas as pd\n", + "from google.genai import types" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "DhjmRffOOPAS" + }, + "source": [ + "## Initialize model\n", + "\n", + "Define the model to be tuned. `gemini-2.5-flash` is the Gemini text model support supervised tuning." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "jL-zRl5_OVZW" + }, + "outputs": [], + "source": [ + "base_model = \"gemini-2.5-flash\"" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "4xIqi0Vqeqwn" + }, + "source": [ + "## Prepare your datasets\n", + "\n", + "For integrated evaluation, you need both a training dataset and a validation dataset. The model trains on the former and is tested against the latter at each checkpoint. The data should be in JSONL format, where each line is a JSON object containing prompt and response keys.\n", + "\n", + "Example format:\n", + "\n", + "```json\n", + "{\n", + " \"contents\": [\n", + " {\n", + " \"role\": \"user\",\n", + " \"parts\": [\n", + " {\n", + " \"text\": \"Honesty is usually the best policy. It is disrespectful to lie to someone. If you don't want to date someone, you should say so. Sometimes it is easy to be honest. ... It is not necessary for everyone around you to know that you are turning down a date.\\n\\nProvide a summary of the article in two or three sentences:\\n\\n\"\n", + " }\n", + " ]\n", + " },\n", + " {\n", + " \"role\": \"model\",\n", + " \"parts\": [\n", + " {\n", + " \"text\": \"Tell the truth. Use a \\\"compliment sandwich\\\". Be direct. Treat the person with respect. Communicate effectively.\"\n", + " }\n", + " ]\n", + " }\n", + " ]\n", + "}\n", + "```\n", + "\n", + "For this tutorial, we will use public sample datasets." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "VpzmI1K61Tn2" + }, + "outputs": [], + "source": [ + "training_dataset_uri = \"gs://cloud-samples-data/ai-platform/generative_ai/gemini-2_0/text/sft_train_data.jsonl\"\n", + "validation_dataset_uri = \"gs://cloud-samples-data/ai-platform/generative_ai/gemini-2_0/text/sft_validation_data.jsonl\"" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "7YGurtXHJy_y" + }, + "source": [ + "## Define the Evaluation Config\n", + "\n", + "This is the core of the new feature.\n", + "\n", + "The `EvaluationConfig` object tells the tuning job how to measure performance using another model (an \"autorater\") to judge the quality of your tuned model's responses. Here, we'll define a `metric` to evaluate the \"fluency\" of the output.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "_SdsjiWGVze8" + }, + "outputs": [], + "source": [ + "evaluation_config = types.EvaluationConfig(\n", + " # Required. Define a list of metrics. A minimum of 1 is required.\n", + " metrics=[\n", + " types.Metric(\n", + " name=\"fluency\",\n", + " prompt_template=\"Evaluate the sentence fluency of the response. Provide a score from 1-5.\\n RESPONSE: {response}\",\n", + " # Optional. Guide the autorater's persona for better results.\n", + " judge_model_system_instruction=\"You are a professional editor specializing in linguistics.\",\n", + " ),\n", + " ],\n", + " # Required. Define where to store the detailed, row-by-row evaluation results.\n", + " output_config=types.OutputConfig(\n", + " gcs_destination=types.GcsDestination(\n", + " output_uri_prefix=f\"{BUCKET_URI}/evaluation\"\n", + " )\n", + " ),\n", + " # Optional. Configure the autorater itself.\n", + " autorater_config=types.AutoraterConfig(\n", + " # The number of validation samples to evaluate for each checkpoint.\n", + " # This is a trade-off: more samples = better metrics but higher cost/time.\n", + " sampling_count=6,\n", + " ),\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "IgMb3E0YEqL2" + }, + "source": [ + "## Fine-tune the Model with Integrated Evaluation\n", + "\n", + "Now we launch the tuning job. We provide the base model, datasets, and our evaluation\\_config. The service will handle the rest, automatically running evaluations on checkpoints.\n", + "\n", + "*Note: The default hyperparameter settings are optimized for most use cases. You can customize parameters like adapter\\_size to address specific performance needs.*" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "vQM2vDBZ27b_" + }, + "outputs": [], + "source": [ + "tuned_model_display_name = (\n", + " \"gemini-flash-integrated-eval-demo\" + f\"_{str(uuid.uuid4())[:8]}\"\n", + ")\n", + "\n", + "training_dataset = {\n", + " \"gcs_uri\": training_dataset_uri,\n", + "}\n", + "\n", + "validation_dataset = types.TuningValidationDataset(\n", + " gcs_uri=validation_dataset_uri,\n", + ")\n", + "\n", + "# Start the tuning job. This is an asynchronous call.\n", + "# The SDK's tuning implementation is experimental and may change in future versions.\n", + "sft_tuning_job = client.tunings.tune(\n", + " base_model=base_model,\n", + " training_dataset=training_dataset,\n", + " config=types.CreateTuningJobConfig(\n", + " tuned_model_display_name=tuned_model_display_name,\n", + " validation_dataset=validation_dataset,\n", + " evaluation_config=evaluation_config,\n", + " ),\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "yLlAgVjCNqXg" + }, + "outputs": [], + "source": [ + "# Get the tuning job for status checks.\n", + "tuning_job = client.tunings.get(name=sft_tuning_job.name)\n", + "print(tuning_job)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "gpM4q8PkhUHE" + }, + "source": [ + "## Monitor the tuning job\n", + "\n", + "Tuning time depends on several factors, such as training data size, number of epochs, learning rate multiplier, etc.\n", + "\n", + "\n", + "**⚠ It will take \\~45 mins for this model tuning job and evaluation to complete.**" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "p8o3CTerhi_r" + }, + "outputs": [], + "source": [ + "print(\n", + " \"Monitoring job... This will take time. You can safely close this notebook and come back later.\"\n", + ")\n", + "\n", + "while sft_tuning_job.state not in [\n", + " types.JobState.JOB_STATE_CANCELLED,\n", + " types.JobState.JOB_STATE_FAILED,\n", + " types.JobState.JOB_STATE_SUCCEEDED,\n", + "]:\n", + " time.sleep(600) # Check status every 10 minutes\n", + " sft_tuning_job.refresh()\n", + " print(f\"Current job state: {sft_tuning_job.state.name!s}\")\n", + "\n", + "print(f\"Job finished with state: {sft_tuning_job.state.name}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "4jHBD7nah7fZ" + }, + "source": [ + "## Get the final tuned model and experiment details\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "e1O1xCBS6spi" + }, + "outputs": [], + "source": [ + "tuned_model = tuning_job.tuned_model.endpoint\n", + "experiment_name = tuning_job.experiment\n", + "\n", + "print(\"Tuned model experiment:\", experiment_name)\n", + "print(\"Tuned model endpoint resource name:\", tuned_model)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "8DzlWWKpbGcu" + }, + "source": [ + "## View Gen AI Evaluation results\n", + "\n", + "Evaluation results for each checkpoint are logged to the associated Vertex Experiment. You can view them in the UI or access them programmatically.\n", + "\n", + "Each checkpoint will be described with a `tuning-evaluation-checkpoint-#` \\\n", + "Experiment Run. This will include the following Gen AI Evaluation metrics:\n", + " - Error message (if failed)\n", + " - `gcsDestination` (location of row based evaluation results)\n", + " - `gcsSource` (location of evaluation dataset)\n", + " - Aggregated evaluation metric results\n", + " - For SDK: `AVERAGE` and `STANDARD_DEVIATION` values for each metric\n", + " - For API: User defined aggregation metrics\n", + " - Aggregated results are indexed in the order they were defined in the Evaluation Config" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "psRbCfzwWz_g" + }, + "source": [ + "### View Gen AI Evaluation Metrics using Vertex AI Experiments SDK" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "5J1LP3nCbNlg" + }, + "outputs": [], + "source": [ + "# Locate the Vertex AI Experiment and list the evaluation runs for each checkpoint\n", + "experiment_runs = aiplatform.ExperimentRun.list(experiment=experiment_name)\n", + "for run in experiment_runs:\n", + " if \"-evaluation-\" in run.name:\n", + " print(f\"--- Results for Run: {run.name} ---\")\n", + " print(pd.DataFrame.from_dict(run.get_metrics(), orient=\"index\"))\n", + " print(\"\\n\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "QNyncQ4nh_Ur" + }, + "source": [ + "### View Results in the Google Cloud Console\n", + "\n", + "You can also get a rich, visual view of the results:\n", + "\n", + "1. Navigate to the **Vertex AI \\> Generative AI \\> Tuning** page in the Google Cloud Console. \n", + "2. Click on your tuning job (gemini-flash-integrated-eval-demo). \n", + "3. On the Details page, click the link under the **Experiment** field. This will take you directly to the Vertex AI Experiments page where you can compare metrics across all checkpoints.\n", + "\n", + "Here you can see an example:\n", + "\n", + "![7pdquo8m2vaizrs-1.png](https://storage.googleapis.com/github-repo/generative-ai/sft_gemini_automatic_evaluation/7pdquo8m2vaizrs-1.png)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "HMpW3AmBwB6c" + }, + "source": [ + "## (Optional) Evaluation Results Viewer\n", + "\n", + "Launch a Gradio app to visualize evaluation results from a GCS bucket." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "1MFd7AA-wwQu" + }, + "outputs": [], + "source": [ + "import json\n", + "\n", + "import gradio as gr\n", + "import numpy as np\n", + "import pandas as pd\n", + "import plotly.graph_objects as go\n", + "from google.cloud import storage\n", + "\n", + "\n", + "class EvaluationViewer:\n", + " def __init__(self):\n", + " self.client = None\n", + " self.bucket_name = None\n", + " self.evaluation_data = {}\n", + "\n", + " def connect_to_gcs(self, bucket_name: str) -> str:\n", + " \"\"\"Connect to GCS bucket and load evaluation data.\"\"\"\n", + " try:\n", + " self.client = storage.Client()\n", + " self.bucket_name = bucket_name\n", + " self.bucket = self.client.bucket(bucket_name)\n", + "\n", + " # Load evaluation data\n", + " self.load_evaluation_data()\n", + "\n", + " return f\"✓ Successfully connected to bucket: {bucket_name}\\nFound {len(self.evaluation_data)} evaluation runs\"\n", + " except Exception as e:\n", + " return f\"✗ Error connecting to bucket: {e!s}\"\n", + "\n", + " def load_evaluation_data(self):\n", + " \"\"\"Load all evaluation data from the bucket.\"\"\"\n", + " self.evaluation_data = {}\n", + "\n", + " # List all evaluation folders\n", + " blobs = self.bucket.list_blobs(prefix=\"evaluation/evaluation_\")\n", + "\n", + " eval_folders = set()\n", + " for blob in blobs:\n", + " parts = blob.name.split(\"/\")\n", + " # Only add folders that start with 'evaluation_' and don't contain 'dataset_checkpoint'\n", + " if (\n", + " len(parts) >= 2\n", + " and parts[1].startswith(\"evaluation_\")\n", + " and \"dataset_checkpoint\" not in parts[1]\n", + " ):\n", + " eval_folders.add(parts[1])\n", + "\n", + " # Sort folders by timestamp to assign checkpoint numbers\n", + " sorted_folders = sorted(eval_folders)\n", + "\n", + " # Load data from each evaluation folder with checkpoint naming\n", + " for idx, folder in enumerate(sorted_folders, 1):\n", + " timestamp = folder.replace(\"evaluation_\", \"\")\n", + " checkpoint_name = f\"checkpoint_{idx}\"\n", + " self.evaluation_data[timestamp] = {\n", + " \"aggregation\": [],\n", + " \"results\": [],\n", + " \"checkpoint_name\": checkpoint_name,\n", + " }\n", + "\n", + " # Load aggregation results\n", + " agg_blob = self.bucket.blob(\n", + " f\"evaluation/{folder}/aggregation_results.jsonl\"\n", + " )\n", + " if agg_blob.exists():\n", + " content = agg_blob.download_as_text()\n", + " for line in content.strip().split(\"\\n\"):\n", + " if line:\n", + " self.evaluation_data[timestamp][\"aggregation\"].append(\n", + " json.loads(line)\n", + " )\n", + "\n", + " # Load evaluation results (sample first 100 for performance)\n", + " res_blob = self.bucket.blob(f\"evaluation/{folder}/evaluation_results.jsonl\")\n", + " if res_blob.exists():\n", + " content = res_blob.download_as_text()\n", + " lines = content.strip().split(\"\\n\")\n", + " for line in lines[:100]: # Limit to first 100 for performance\n", + " if line:\n", + " self.evaluation_data[timestamp][\"results\"].append(\n", + " json.loads(line)\n", + " )\n", + "\n", + " def get_overview_stats(self) -> dict:\n", + " \"\"\"Calculate overview statistics for all evaluation runs.\"\"\"\n", + " if not self.evaluation_data:\n", + " return {}\n", + "\n", + " stats = {\n", + " \"total_runs\": len(self.evaluation_data),\n", + " \"total_samples\": 0,\n", + " \"overall_avg\": [],\n", + " \"overall_std\": [],\n", + " \"latest_run\": None,\n", + " \"latest_checkpoint\": None,\n", + " \"latest_avg\": None,\n", + " \"latest_std\": None,\n", + " \"best_run\": None,\n", + " \"best_checkpoint\": None,\n", + " \"best_avg\": float(\"-inf\"),\n", + " \"best_std\": None,\n", + " \"worst_run\": None,\n", + " \"worst_checkpoint\": None,\n", + " \"worst_avg\": float(\"inf\"),\n", + " \"worst_std\": None,\n", + " }\n", + "\n", + " # Process each evaluation run\n", + " for timestamp, data in sorted(self.evaluation_data.items(), reverse=True):\n", + " avg_score = None\n", + " std_score = None\n", + "\n", + " for agg in data[\"aggregation\"]:\n", + " if agg[\"aggregationMetric\"] == \"AVERAGE\":\n", + " avg_score = agg[\"pointwiseMetricResult\"][\"score\"]\n", + " elif agg[\"aggregationMetric\"] == \"STANDARD_DEVIATION\":\n", + " std_score = agg[\"pointwiseMetricResult\"][\"score\"]\n", + "\n", + " if avg_score is not None:\n", + " stats[\"overall_avg\"].append(avg_score)\n", + " if std_score is not None:\n", + " stats[\"overall_std\"].append(std_score)\n", + "\n", + " # Track latest run\n", + " if stats[\"latest_run\"] is None:\n", + " stats[\"latest_run\"] = timestamp\n", + " stats[\"latest_checkpoint\"] = data.get(\"checkpoint_name\", \"N/A\")\n", + " stats[\"latest_avg\"] = avg_score\n", + " stats[\"latest_std\"] = std_score\n", + "\n", + " # Track best and worst\n", + " if avg_score > stats[\"best_avg\"]:\n", + " stats[\"best_avg\"] = avg_score\n", + " stats[\"best_std\"] = std_score\n", + " stats[\"best_run\"] = timestamp\n", + " stats[\"best_checkpoint\"] = data.get(\"checkpoint_name\", \"N/A\")\n", + "\n", + " if avg_score < stats[\"worst_avg\"]:\n", + " stats[\"worst_avg\"] = avg_score\n", + " stats[\"worst_std\"] = std_score\n", + " stats[\"worst_run\"] = timestamp\n", + " stats[\"worst_checkpoint\"] = data.get(\"checkpoint_name\", \"N/A\")\n", + "\n", + " stats[\"total_samples\"] += len(data[\"results\"])\n", + "\n", + " # Calculate overall statistics\n", + " if stats[\"overall_avg\"]:\n", + " stats[\"global_avg\"] = np.mean(stats[\"overall_avg\"])\n", + " stats[\"global_std\"] = (\n", + " np.mean(stats[\"overall_std\"]) if stats[\"overall_std\"] else 0\n", + " )\n", + " stats[\"avg_range\"] = (min(stats[\"overall_avg\"]), max(stats[\"overall_avg\"]))\n", + "\n", + " return stats\n", + "\n", + " def get_aggregated_metrics_plot(self) -> go.Figure:\n", + " \"\"\"Create aggregated metrics visualization.\"\"\"\n", + " if not self.evaluation_data:\n", + " return go.Figure().add_annotation(\n", + " text=\"No evaluation data available\", showarrow=False\n", + " )\n", + "\n", + " # Prepare data for plotting\n", + " timestamps = []\n", + " checkpoint_labels = []\n", + " averages = []\n", + " std_devs = []\n", + "\n", + " for timestamp, data in sorted(self.evaluation_data.items()):\n", + " timestamps.append(timestamp[:19]) # Truncate microseconds for readability\n", + " checkpoint_labels.append(data.get(\"checkpoint_name\", \"N/A\"))\n", + "\n", + " avg_score = None\n", + " std_score = None\n", + "\n", + " for agg in data[\"aggregation\"]:\n", + " if agg[\"aggregationMetric\"] == \"AVERAGE\":\n", + " avg_score = agg[\"pointwiseMetricResult\"][\"score\"]\n", + " elif agg[\"aggregationMetric\"] == \"STANDARD_DEVIATION\":\n", + " std_score = agg[\"pointwiseMetricResult\"][\"score\"]\n", + "\n", + " averages.append(avg_score if avg_score is not None else 0)\n", + " std_devs.append(std_score if std_score is not None else 0)\n", + "\n", + " # Create subplots\n", + " fig = go.Figure()\n", + "\n", + " # Add average scores with error bars\n", + " fig.add_trace(\n", + " go.Scatter(\n", + " x=timestamps,\n", + " y=averages,\n", + " error_y=dict(type=\"data\", array=std_devs, visible=True),\n", + " mode=\"lines+markers\",\n", + " name=\"Average Score\",\n", + " line=dict(color=\"blue\", width=2),\n", + " marker=dict(size=8),\n", + " text=checkpoint_labels,\n", + " hovertemplate=\"%{text}
    Time: %{x}
    Score: %{y:.3f} ± %{error_y.array:.3f}\",\n", + " )\n", + " )\n", + "\n", + " fig.update_layout(\n", + " title=\"Evaluation Metrics Over Time\",\n", + " xaxis_title=\"Evaluation Run\",\n", + " yaxis_title=\"Score\",\n", + " hovermode=\"x unified\",\n", + " showlegend=True,\n", + " height=400,\n", + " )\n", + "\n", + " return fig\n", + "\n", + " def get_evaluation_results_table(\n", + " self, eval_run: str, start_idx: int = 0, page_size: int = 10\n", + " ) -> pd.DataFrame:\n", + " \"\"\"Get evaluation results as a paginated table.\"\"\"\n", + " if not eval_run or eval_run not in self.evaluation_data:\n", + " return pd.DataFrame(\n", + " {\"Message\": [\"Select an evaluation run to view results\"]}\n", + " )\n", + "\n", + " results = self.evaluation_data[eval_run][\"results\"]\n", + "\n", + " if not results:\n", + " return pd.DataFrame({\"Message\": [\"No results available for this run\"]})\n", + "\n", + " # Prepare data for display\n", + " table_data = []\n", + " end_idx = min(start_idx + page_size, len(results))\n", + "\n", + " for i in range(start_idx, end_idx):\n", + " result = results[i]\n", + " instance = json.loads(result[\"jsonInstance\"])\n", + "\n", + " # Extract evaluation score and explanation\n", + " score = None\n", + " explanation = None\n", + " if result.get(\"evaluationResults\"):\n", + " eval_result = result[\"evaluationResults\"][0]\n", + " if \"pointwiseMetricResult\" in eval_result:\n", + " score = eval_result[\"pointwiseMetricResult\"].get(\"score\", \"N/A\")\n", + " explanation = eval_result[\"pointwiseMetricResult\"].get(\n", + " \"explanation\", \"N/A\"\n", + " )\n", + "\n", + " table_data.append(\n", + " {\n", + " \"Index\": i + 1,\n", + " \"Request\": instance.get(\"request\", \"N/A\")[:100] + \"...\"\n", + " if len(instance.get(\"request\", \"\")) > 100\n", + " else instance.get(\"request\", \"N/A\"),\n", + " \"Response\": instance.get(\"response\", \"N/A\")[:100] + \"...\"\n", + " if len(instance.get(\"response\", \"\")) > 100\n", + " else instance.get(\"response\", \"N/A\"),\n", + " \"Reference\": instance.get(\"reference\", \"N/A\")[:100] + \"...\"\n", + " if len(instance.get(\"reference\", \"\")) > 100\n", + " else instance.get(\"reference\", \"N/A\"),\n", + " \"Score\": score,\n", + " \"Explanation\": explanation[:100] + \"...\"\n", + " if explanation and len(explanation) > 100\n", + " else explanation,\n", + " }\n", + " )\n", + "\n", + " return pd.DataFrame(table_data)\n", + "\n", + " def get_single_result_detail(self, eval_run: str, index: int) -> dict:\n", + " \"\"\"Get detailed view of a single evaluation result.\"\"\"\n", + " if not eval_run or eval_run not in self.evaluation_data:\n", + " return {\"error\": \"Invalid evaluation run\"}\n", + "\n", + " results = self.evaluation_data[eval_run][\"results\"]\n", + "\n", + " if index < 0 or index >= len(results):\n", + " return {\"error\": \"Invalid index\"}\n", + "\n", + " result = results[index]\n", + " instance = json.loads(result[\"jsonInstance\"])\n", + "\n", + " # Extract evaluation details\n", + " eval_details = {}\n", + " if result.get(\"evaluationResults\"):\n", + " eval_result = result[\"evaluationResults\"][0]\n", + " if \"pointwiseMetricResult\" in eval_result:\n", + " eval_details = eval_result[\"pointwiseMetricResult\"]\n", + "\n", + " return {\n", + " \"request\": instance.get(\"request\", \"N/A\"),\n", + " \"response\": instance.get(\"response\", \"N/A\"),\n", + " \"reference\": instance.get(\"reference\", \"N/A\"),\n", + " \"baseline_model_response\": instance.get(\"baseline_model_response\", \"N/A\"),\n", + " \"score\": eval_details.get(\"score\", \"N/A\"),\n", + " \"explanation\": eval_details.get(\"explanation\", \"N/A\"),\n", + " }\n", + "\n", + "\n", + "# Initialize the viewer\n", + "viewer = EvaluationViewer()\n", + "\n", + "# Create Gradio interface\n", + "with gr.Blocks(title=\"Evaluation Results Viewer\") as app:\n", + " gr.Markdown(\"# 📊 Evaluation Results Viewer\")\n", + " gr.Markdown(\n", + " \"Load data from your GCS bucket to visualize evaluation metrics and results.\"\n", + " )\n", + "\n", + " with gr.Row():\n", + " bucket_input = gr.Textbox(\n", + " label=\"GCS Bucket Name\",\n", + " placeholder=\"Enter your GCS bucket name (e.g., my-evaluation-bucket)\",\n", + " scale=3,\n", + " )\n", + " connect_btn = gr.Button(\"Load from Bucket\", variant=\"primary\", scale=1)\n", + "\n", + " status_output = gr.Textbox(label=\"Connection Status\", interactive=False)\n", + "\n", + " with gr.Tabs():\n", + " with gr.Tab(\"📊 Overview\"):\n", + " gr.Markdown(\"### Evaluation Summary\")\n", + " with gr.Row():\n", + " with gr.Column(scale=1):\n", + " overview_total_runs = gr.Number(\n", + " label=\"Total Evaluation Runs\", interactive=False\n", + " )\n", + " overview_total_samples = gr.Number(\n", + " label=\"Total Samples Evaluated\", interactive=False\n", + " )\n", + " with gr.Column(scale=1):\n", + " overview_global_avg = gr.Number(\n", + " label=\"Global Average Score\", interactive=False, precision=3\n", + " )\n", + " overview_global_std = gr.Number(\n", + " label=\"Global Std Deviation\", interactive=False, precision=3\n", + " )\n", + "\n", + " with gr.Row():\n", + " with gr.Column(scale=1):\n", + " gr.Markdown(\"#### Latest Run\")\n", + " overview_latest_checkpoint = gr.Textbox(\n", + " label=\"Checkpoint\", interactive=False\n", + " )\n", + " overview_latest_run = gr.Textbox(\n", + " label=\"Timestamp\", interactive=False\n", + " )\n", + " overview_latest_avg = gr.Number(\n", + " label=\"Average Score\", interactive=False, precision=3\n", + " )\n", + " overview_latest_std = gr.Number(\n", + " label=\"Std Deviation\", interactive=False, precision=3\n", + " )\n", + " with gr.Column(scale=1):\n", + " gr.Markdown(\"#### Best Run\")\n", + " overview_best_checkpoint = gr.Textbox(\n", + " label=\"Checkpoint\", interactive=False\n", + " )\n", + " overview_best_run = gr.Textbox(label=\"Timestamp\", interactive=False)\n", + " overview_best_avg = gr.Number(\n", + " label=\"Average Score\", interactive=False, precision=3\n", + " )\n", + " overview_best_std = gr.Number(\n", + " label=\"Std Deviation\", interactive=False, precision=3\n", + " )\n", + " with gr.Column(scale=1):\n", + " gr.Markdown(\"#### Worst Run\")\n", + " overview_worst_checkpoint = gr.Textbox(\n", + " label=\"Checkpoint\", interactive=False\n", + " )\n", + " overview_worst_run = gr.Textbox(\n", + " label=\"Timestamp\", interactive=False\n", + " )\n", + " overview_worst_avg = gr.Number(\n", + " label=\"Average Score\", interactive=False, precision=3\n", + " )\n", + " overview_worst_std = gr.Number(\n", + " label=\"Std Deviation\", interactive=False, precision=3\n", + " )\n", + "\n", + " gr.Markdown(\"#### Score Range\")\n", + " overview_range = gr.Textbox(\n", + " label=\"Min - Max Average Scores\", interactive=False\n", + " )\n", + "\n", + " with gr.Tab(\"📈 Aggregated Metrics\"):\n", + " gr.Markdown(\"### Overall Evaluation Metrics\")\n", + " metrics_plot = gr.Plot(label=\"Metrics Over Time\")\n", + "\n", + " with gr.Tab(\"📋 Evaluation Results\"):\n", + " gr.Markdown(\"### Browse Individual Evaluation Results\")\n", + "\n", + " with gr.Row():\n", + " eval_run_dropdown = gr.Dropdown(\n", + " label=\"Select Evaluation Run\", choices=[], interactive=True\n", + " )\n", + " refresh_runs_btn = gr.Button(\"🔄 Refresh\", scale=1)\n", + "\n", + " with gr.Row():\n", + " page_size = gr.Slider(\n", + " minimum=5, maximum=50, value=10, step=5, label=\"Results per page\"\n", + " )\n", + " page_number = gr.Number(value=1, label=\"Page\", minimum=1, precision=0)\n", + "\n", + " results_table = gr.DataFrame(\n", + " label=\"Evaluation Results\", interactive=False, wrap=True\n", + " )\n", + "\n", + " with gr.Tab(\"🔍 Result Details\"):\n", + " gr.Markdown(\"### Detailed View of Single Result\")\n", + "\n", + " with gr.Row():\n", + " detail_eval_run = gr.Dropdown(\n", + " label=\"Select Evaluation Run\", choices=[], interactive=True\n", + " )\n", + " result_index = gr.Number(\n", + " value=0, label=\"Result Index (0-based)\", minimum=0, precision=0\n", + " )\n", + " load_detail_btn = gr.Button(\"Load Details\", variant=\"primary\")\n", + "\n", + " with gr.Column():\n", + " gr.Markdown(\"#### Request\")\n", + " detail_request = gr.Textbox(label=\"\", lines=5, interactive=False)\n", + "\n", + " gr.Markdown(\"#### Response\")\n", + " detail_response = gr.Textbox(label=\"\", lines=3, interactive=False)\n", + "\n", + " gr.Markdown(\"#### Reference\")\n", + " detail_reference = gr.Textbox(label=\"\", lines=3, interactive=False)\n", + "\n", + " gr.Markdown(\"#### Baseline Model Response\")\n", + " detail_baseline = gr.Textbox(label=\"\", lines=3, interactive=False)\n", + "\n", + " with gr.Row():\n", + " detail_score = gr.Number(label=\"Score\", interactive=False)\n", + " detail_explanation = gr.Textbox(\n", + " label=\"Explanation\", lines=2, interactive=False\n", + " )\n", + "\n", + " # Define interactions\n", + " def connect_and_load(bucket_name):\n", + " status = viewer.connect_to_gcs(bucket_name)\n", + "\n", + " if \"Successfully\" in status:\n", + " # Get overview stats\n", + " stats = viewer.get_overview_stats()\n", + "\n", + " # Update plots\n", + " metrics_fig = viewer.get_aggregated_metrics_plot()\n", + "\n", + " # Update dropdown choices\n", + " eval_runs = list(viewer.evaluation_data.keys())\n", + "\n", + " # Format overview outputs\n", + " overview_outputs = [\n", + " stats.get(\"total_runs\", 0),\n", + " stats.get(\"total_samples\", 0),\n", + " stats.get(\"global_avg\", 0),\n", + " stats.get(\"global_std\", 0),\n", + " stats.get(\"latest_checkpoint\", \"N/A\"),\n", + " stats.get(\"latest_run\", \"N/A\")[:19]\n", + " if stats.get(\"latest_run\")\n", + " else \"N/A\",\n", + " stats.get(\"latest_avg\", 0),\n", + " stats.get(\"latest_std\", 0),\n", + " stats.get(\"best_checkpoint\", \"N/A\"),\n", + " stats.get(\"best_run\", \"N/A\")[:19] if stats.get(\"best_run\") else \"N/A\",\n", + " stats.get(\"best_avg\", 0),\n", + " stats.get(\"best_std\", 0),\n", + " stats.get(\"worst_checkpoint\", \"N/A\"),\n", + " stats.get(\"worst_run\", \"N/A\")[:19] if stats.get(\"worst_run\") else \"N/A\",\n", + " stats.get(\"worst_avg\", 0),\n", + " stats.get(\"worst_std\", 0),\n", + " f\"{stats.get('avg_range', (0, 0))[0]:.3f} - {stats.get('avg_range', (0, 0))[1]:.3f}\"\n", + " if stats.get(\"avg_range\")\n", + " else \"N/A\",\n", + " ]\n", + "\n", + " return (\n", + " status,\n", + " *overview_outputs,\n", + " metrics_fig,\n", + " gr.update(choices=eval_runs, value=eval_runs[0] if eval_runs else None),\n", + " gr.update(choices=eval_runs, value=eval_runs[0] if eval_runs else None),\n", + " )\n", + " return (\n", + " status,\n", + " 0,\n", + " 0,\n", + " 0,\n", + " 0,\n", + " \"N/A\",\n", + " \"N/A\",\n", + " 0,\n", + " 0,\n", + " \"N/A\",\n", + " \"N/A\",\n", + " 0,\n", + " 0,\n", + " \"N/A\",\n", + " \"N/A\",\n", + " 0,\n", + " 0,\n", + " \"N/A\",\n", + " go.Figure(),\n", + " gr.update(choices=[]),\n", + " gr.update(choices=[]),\n", + " )\n", + "\n", + " def update_results_table(eval_run, page_num, page_size):\n", + " if not eval_run:\n", + " return pd.DataFrame()\n", + " start_idx = (page_num - 1) * page_size\n", + " return viewer.get_evaluation_results_table(eval_run, start_idx, page_size)\n", + "\n", + " def load_result_details(eval_run, index):\n", + " details = viewer.get_single_result_detail(eval_run, int(index))\n", + "\n", + " if \"error\" in details:\n", + " return \"\", \"\", \"\", \"\", None, details[\"error\"]\n", + "\n", + " return (\n", + " details[\"request\"],\n", + " details[\"response\"],\n", + " details[\"reference\"],\n", + " details[\"baseline_model_response\"],\n", + " details[\"score\"],\n", + " details[\"explanation\"],\n", + " )\n", + "\n", + " def refresh_runs():\n", + " eval_runs = list(viewer.evaluation_data.keys())\n", + " return (\n", + " gr.update(choices=eval_runs, value=eval_runs[0] if eval_runs else None),\n", + " gr.update(choices=eval_runs, value=eval_runs[0] if eval_runs else None),\n", + " )\n", + "\n", + " # Connect button events\n", + " connect_btn.click(\n", + " fn=connect_and_load,\n", + " inputs=[bucket_input],\n", + " outputs=[\n", + " status_output,\n", + " overview_total_runs,\n", + " overview_total_samples,\n", + " overview_global_avg,\n", + " overview_global_std,\n", + " overview_latest_checkpoint,\n", + " overview_latest_run,\n", + " overview_latest_avg,\n", + " overview_latest_std,\n", + " overview_best_checkpoint,\n", + " overview_best_run,\n", + " overview_best_avg,\n", + " overview_best_std,\n", + " overview_worst_checkpoint,\n", + " overview_worst_run,\n", + " overview_worst_avg,\n", + " overview_worst_std,\n", + " overview_range,\n", + " metrics_plot,\n", + " eval_run_dropdown,\n", + " detail_eval_run,\n", + " ],\n", + " )\n", + "\n", + " # Update table when dropdown or pagination changes\n", + " eval_run_dropdown.change(\n", + " fn=update_results_table,\n", + " inputs=[eval_run_dropdown, page_number, page_size],\n", + " outputs=[results_table],\n", + " )\n", + "\n", + " page_number.change(\n", + " fn=update_results_table,\n", + " inputs=[eval_run_dropdown, page_number, page_size],\n", + " outputs=[results_table],\n", + " )\n", + "\n", + " page_size.change(\n", + " fn=update_results_table,\n", + " inputs=[eval_run_dropdown, page_number, page_size],\n", + " outputs=[results_table],\n", + " )\n", + "\n", + " # Refresh runs button\n", + " refresh_runs_btn.click(\n", + " fn=refresh_runs, outputs=[eval_run_dropdown, detail_eval_run]\n", + " )\n", + "\n", + " # Load detail button\n", + " load_detail_btn.click(\n", + " fn=load_result_details,\n", + " inputs=[detail_eval_run, result_index],\n", + " outputs=[\n", + " detail_request,\n", + " detail_response,\n", + " detail_reference,\n", + " detail_baseline,\n", + " detail_score,\n", + " detail_explanation,\n", + " ],\n", + " )" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "0Y0z-H5xxMUg" + }, + "outputs": [], + "source": [ + "app.launch(share=True, height=800)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "M2ACJCgNxgXV" + }, + "outputs": [], + "source": [ + "app.close()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "mXnkGPjGjYWy" + }, + "source": [ + "## Cleaning up\n", + "\n", + "To avoid incurring unexpected charges, it's important to clean up the resources created in this notebook." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "_BotIB6NjQlv" + }, + "outputs": [], + "source": [ + "delete_experiments = True\n", + "delete_endpoint = True\n", + "delete_bucket = True\n", + "\n", + "# Deleting experiment\n", + "if delete_experiments:\n", + " experiment = aiplatform.Experiment.list()[0]\n", + " experiment.delete()\n", + "\n", + "# Deleting the endpoint itself removes the resource configuration.\n", + "if delete_endpoint:\n", + " endpoint = aiplatform.Endpoint.list()[0]\n", + " endpoint.delete(force=True)\n", + "\n", + "# To fully clean up, you should also delete the model artifacts and dataset from your GCS bucket.\n", + "# You can do this via the command line or the Google Cloud Console.\n", + "if delete_bucket:\n", + " !gsutil -m rm -r {BUCKET_URI}" + ] + } + ], + "metadata": { + "colab": { + "name": "sft_gemini_automatic_evaluation.ipynb", + "toc_visible": true + }, + "kernelspec": { + "display_name": "Python 3", + "name": "python3" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} diff --git a/gemini/tuning/sft_gemini_custom_metric_evaluation.ipynb b/gemini/tuning/sft_gemini_custom_metric_evaluation.ipynb new file mode 100644 index 0000000..e51b3d4 --- /dev/null +++ b/gemini/tuning/sft_gemini_custom_metric_evaluation.ipynb @@ -0,0 +1,702 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "ur8xi4C7S06n" + }, + "outputs": [], + "source": [ + "# Copyright 2025 Google LLC\n", + "#\n", + "# Licensed under the Apache License, Version 2.0 (the \"License\");\n", + "# you may not use this file except in compliance with the License.\n", + "# You may obtain a copy of the License at\n", + "#\n", + "# https://www.apache.org/licenses/LICENSE-2.0\n", + "#\n", + "# Unless required by applicable law or agreed to in writing, software\n", + "# distributed under the License is distributed on an \"AS IS\" BASIS,\n", + "# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n", + "# See the License for the specific language governing permissions and\n", + "# limitations under the License." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "JAPoU8Sm5E6e" + }, + "source": [ + "# Integrate Custom Metrics into Gemini Supervised Fine-Tuning\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + "
    \n", + " \n", + " \"Google
    Open in Colab\n", + "
    \n", + "
    \n", + " \n", + " \"Google
    Open in Colab Enterprise\n", + "
    \n", + "
    \n", + " \n", + " \"Vertex
    Open in Vertex AI Workbench\n", + "
    \n", + "
    \n", + " \n", + " \"GitHub
    View on GitHub\n", + "
    \n", + "
    \n", + "\n", + "
    \n", + "\n", + "

    \n", + "Share to:\n", + "\n", + "\n", + " \"LinkedIn\n", + "\n", + "\n", + "\n", + " \"Bluesky\n", + "\n", + "\n", + "\n", + " \"X\n", + "\n", + "\n", + "\n", + " \"Reddit\n", + "\n", + "\n", + "\n", + " \"Facebook\n", + "\n", + "

    " + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "84f0f73a0f76" + }, + "source": [ + "| Author(s) |\n", + "| --- |\n", + "| Jessica Wang |\n", + "| [Ivan Nardini](https://github.com/inardini) |" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "tvgnzT1CKxrO" + }, + "source": [ + "## Overview\n", + "\n", + "This tutorial shows you how to integrate custom Python evaluation metrics into Gemini supervised fine-tuning (SFT) workflows using Vertex AI Gen AI Evaluation service.\n", + "\n", + "### Why Custom Metrics for Tuning?\n", + "\n", + "When fine-tuning Gemini, training loss doesn't tell you if your model is improving on **your specific quality criteria**. Custom metrics let you track what matters:\n", + "\n", + "- **Summary quality**: Is the model generating concise, accurate summaries?\n", + "- **Content coverage**: Does the summary capture the key points from the source text?\n", + "- **Writing style**: Is the summary following your preferred format (bullet points, sentences, etc.)?\n", + "\n", + "By integrating custom metrics into tuning, you can **measure model improvement on the criteria you care about** as it trains.\n", + "\n", + "### What You'll Learn\n", + "\n", + "In this tutorial, you will:\n", + "1. **Write a custom evaluation function** that scores summary quality\n", + "2. **Submit a tuning job** with your custom metric integrated via REST API\n", + "3. **Monitor the custom metric** as your model trains\n", + "\n", + "### Prerequisites\n", + "\n", + "- A Google Cloud project with billing enabled\n", + "- The Vertex AI API enabled ([enable it here](https://console.cloud.google.com/flows/enableapi?apiid=aiplatform.googleapis.com))\n", + "- A Google Cloud Storage bucket\n", + "- Training and validation datasets in supervised tuning format\n", + "- **No SDK required**—we use the Vertex AI REST API directly!" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "61RBz8LLbxCR" + }, + "source": [ + "## Get started" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "dmWOrTJ3gx13" + }, + "source": [ + "### Authenticate your notebook environment\n", + "\n", + "If you are running this notebook in **Google Colab**, run the cell below to authenticate your account." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "NyKGtVQjgx13" + }, + "outputs": [], + "source": [ + "import sys\n", + "\n", + "if \"google.colab\" in sys.modules:\n", + " from google.colab import auth\n", + "\n", + " auth.authenticate_user()\n", + " print(\"✅ Authentication successful!\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "DF4l8DTdWgPY" + }, + "source": [ + "### Set Google Cloud project information\n", + "\n", + "To get started using Vertex AI, you must have an existing Google Cloud project and [enable the Vertex AI API](https://console.cloud.google.com/flows/enableapi?apiid=aiplatform.googleapis.com).\n", + "\n", + "Learn more about [setting up a project and a development environment](https://cloud.google.com/vertex-ai/docs/start/cloud-environment)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "Nqwi-5ufWp_B" + }, + "outputs": [], + "source": [ + "import os\n", + "import json\n", + "\n", + "# TODO: Replace with your actual project ID\n", + "# fmt: off\n", + "PROJECT_ID = \"[your-project-id]\" # @param {type: \"string\", placeholder: \"[your-project-id]\", isTemplate: true}\n", + "LOCATION = \"us-central1\" # @param {type: \"string\"}\n", + "# fmt: on\n", + "\n", + "# Auto-detect from environment if not set\n", + "if not PROJECT_ID or PROJECT_ID == \"[your-project-id]\":\n", + " PROJECT_ID = str(os.environ.get(\"GOOGLE_CLOUD_PROJECT\", \"\"))\n", + "\n", + "if not PROJECT_ID:\n", + " raise ValueError(\"❌ Please set your PROJECT_ID above\")\n", + "\n", + "# Define GCS paths\n", + "BUCKET_NAME = f\"{PROJECT_ID}-gemini-sft-eval\"\n", + "BUCKET_URI = f\"gs://{BUCKET_NAME}\"\n", + "\n", + "print(f\"📩 Creating GCS bucket: {BUCKET_NAME}...\")\n", + "\n", + "# Create the bucket\n", + "!gcloud storage buckets create {BUCKET_URI} --location {LOCATION} --project {PROJECT_ID}\n", + "\n", + "print(f\"✅ Using project: {PROJECT_ID}\")\n", + "print(f\"✅ Using region: {LOCATION}\")\n", + "print(f\"✅ Bucket: {BUCKET_NAME}\")\n", + "print(f\"✅ Bucket URI: {BUCKET_URI}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "AgKr6gF8xZ7P" + }, + "source": [ + "## Step 1: Write Your Custom Evaluation Function\n", + "\n", + "Let's start creating a custom evaluation function that measures how well the model generates summaries.\n", + "\n", + "### What Makes a Good Custom Metric?\n", + "\n", + "For this summarization task, we want to measure:\n", + "- **Content overlap**: Does the summary include the key information?\n", + "- **Word-level accuracy**: How many words from the reference appear in the prediction?\n", + "- **Completeness**: Does the prediction cover all the main points?\n", + "\n", + "We'll use an **F1 score** approach based on word overlap:\n", + "- **Precision**: What fraction of the predicted words appear in the reference?\n", + "- **Recall**: What fraction of the reference words appear in the prediction?\n", + "- **F1**: The harmonic mean of precision and recall (0.0 to 1.0)\n", + "\n", + "This is a simple but effective metric for evaluating summary quality.\n", + "\n", + "### Define the evaluation function\n", + "\n", + "Your evaluation function **must**:\n", + "1. Be named `evaluate`\n", + "2. Accept one parameter: `instance` (a dictionary with `prediction` and `reference` fields)\n", + "3. Return a number (the score)\n", + "\n", + "**Important:** The function is defined as a string because it will be sent to the Vertex AI API and executed in a secure sandbox environment." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "Aj1FscMYxgzZ" + }, + "outputs": [], + "source": [ + "# Define the custom evaluation function as a string\n", + "# This function compares summary text using word overlap F1 score\n", + "\n", + "evaluation_function = '''def evaluate(instance):\n", + " \"\"\"\n", + " Evaluate summary quality by comparing prediction to reference.\n", + "\n", + " Args:\n", + " instance: Dict with 'prediction' (model output) and 'reference' (ground truth)\n", + "\n", + " Returns:\n", + " F1 score between 0.0 and 1.0 based on word overlap\n", + " \"\"\"\n", + " # Get prediction and reference texts\n", + " prediction = instance.get(\"prediction\", \"\").strip().lower()\n", + " reference = instance.get(\"reference\", \"\").strip().lower()\n", + "\n", + " # If either is empty, return 0\n", + " if not prediction or not reference:\n", + " return 0.0\n", + "\n", + " # Exact match gets perfect score\n", + " if prediction == reference:\n", + " return 1.0\n", + "\n", + " # Calculate word-level overlap (F1-like metric)\n", + " pred_words = set(prediction.split())\n", + " ref_words = set(reference.split())\n", + "\n", + " if not pred_words or not ref_words:\n", + " return 0.0\n", + "\n", + " # Calculate overlap\n", + " overlap = pred_words.intersection(ref_words)\n", + "\n", + " # Precision: what fraction of predicted words are in reference\n", + " precision = len(overlap) / len(pred_words) if pred_words else 0.0\n", + "\n", + " # Recall: what fraction of reference words are in prediction\n", + " recall = len(overlap) / len(ref_words) if ref_words else 0.0\n", + "\n", + " # F1 score\n", + " if precision + recall == 0:\n", + " return 0.0\n", + "\n", + " f1 = 2 * (precision * recall) / (precision + recall)\n", + " return f1\n", + "'''\n", + "\n", + "print(\"✅ Custom evaluation function defined\")\n", + "print(\"\\nFunction summary:\")\n", + "print(\" - Compares prediction text to reference text\")\n", + "print(\" - Calculates word-level F1 score\")\n", + "print(\" - Returns 1.0 for exact match\")\n", + "print(\" - Returns 0.0 for no overlap\")\n", + "print(\" - Returns F1 score (0.0 to 1.0) based on word overlap\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "wNHSPPI0xqgE" + }, + "source": [ + "## Step 2: Integrate Custom Metric into Tuning Job\n", + "\n", + "Now comes the exciting part: integrating your custom metric into a Gemini tuning job!\n", + "\n", + "### How This Works\n", + "\n", + "When you submit a tuning job with `evaluationConfig`, Vertex AI will:\n", + "1. Train the model on your training data\n", + "2. Periodically generate predictions on your validation data\n", + "3. For each prediction, run your custom evaluation function\n", + "4. Aggregate the scores (e.g., compute average)\n", + "5. Report the metrics so you can track improvement\n", + "\n", + "### Prerequisites for This Step\n", + "\n", + "You'll need:\n", + "- **Training dataset**: JSONL file with examples in supervised tuning format\n", + "- **Validation dataset**: JSONL file with validation examples (also in SFT format)\n", + "- Both datasets uploaded to Google Cloud Storage\n", + "\n", + "**Note:** For this tutorial, we'll use placeholder GCS paths. In production, replace these with paths to your actual training/validation datasets." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "4QgPjY9IxwCZ" + }, + "source": [ + "### Step 2.1: Configure dataset paths\n", + "\n", + "Define the GCS paths for your training, and validation datasets.\n", + "\n", + "**Important:**\n", + "- Your **training and validation datasets** should be in standard supervised tuning format\n", + "- All files must be uploaded to GCS before submitting the tuning job" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "Pm1FRd9kx1NU" + }, + "outputs": [], + "source": [ + "# Configure dataset paths\n", + "# TODO: Replace these with your actual dataset GCS paths\n", + "\n", + "# Training and validation datasets (standard SFT format)\n", + "TRAINING_DATASET_URI = \"gs://cloud-samples-data/ai-platform/generative_ai/gemini-2_0/text/sft_train_data.jsonl\"\n", + "VALIDATION_DATASET_URI = \"gs://cloud-samples-data/ai-platform/generative_ai/gemini-2_0/text/sft_validation_data.jsonl\"\n", + "\n", + "# Where to save evaluation results\n", + "EVAL_OUTPUT_URI = f\"{BUCKET_URI}/evaluation_results\"\n", + "\n", + "print(\"✅ Dataset paths configured\")\n", + "print(f\"\\nTraining data: {TRAINING_DATASET_URI}\")\n", + "print(f\"Validation data: {VALIDATION_DATASET_URI}\")\n", + "print(f\"\\nEvaluation results will be saved to: {EVAL_OUTPUT_URI}\")\n", + "print(\"\\n💡 For production: Replace training/validation paths with your own datasets\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "1hSjwIuSyD50" + }, + "source": [ + "### Step 2.2: Build the tuning job request\n", + "\n", + "Now let's construct the REST API request for the tuning job with integrated custom evaluation.\n", + "\n", + "**Key sections in the request are**:\n", + "\n", + "| Section | Purpose |\n", + "|---------|---------|\n", + "| `base_model` | The foundation model to fine-tune (e.g., gemini-2.5-flash) |\n", + "| `supervisedTuningSpec` | Configuration for supervised fine-tuning |\n", + "| `trainingDatasetUri` | Your training examples |\n", + "| `validationDatasetUri` | Your validation examples |\n", + "| **`evaluationConfig`** | **This is where we integrate the custom metric!** |\n", + "| `metrics.custom_code_execution_spec` | Your custom evaluation function |\n", + "| `metrics.aggregation_metrics` | How to aggregate scores (AVERAGE, MAXIMUM, etc.) |\n", + "| `outputConfig` | Where to save detailed evaluation results |\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "ujetjIgkyJes" + }, + "outputs": [], + "source": [ + "# Build the tuning job request with custom evaluation\n", + "tuning_request = {\n", + " \"description\": \"Gemini tuning with custom summary evaluation metric\",\n", + " \"base_model\": \"gemini-2.5-flash\",\n", + " \"supervisedTuningSpec\": {\n", + " # Standard tuning configuration\n", + " \"trainingDatasetUri\": TRAINING_DATASET_URI,\n", + " \"validationDatasetUri\": VALIDATION_DATASET_URI,\n", + "\n", + " # ============================================================\n", + " # THIS IS THE KEY PART: Custom evaluation configuration\n", + " # ============================================================\n", + " \"evaluationConfig\": {\n", + " \"metrics\": {\n", + " # Request AVERAGE score across all evaluation examples\n", + " \"aggregation_metrics\": [\"AVERAGE\"],\n", + "\n", + " # Provide our custom evaluation function\n", + " \"custom_code_execution_spec\": {\n", + " \"evaluation_function\": evaluation_function\n", + " }\n", + " },\n", + " # Save detailed evaluation results to GCS\n", + " \"outputConfig\": {\n", + " \"gcs_destination\": {\n", + " \"output_uri_prefix\": EVAL_OUTPUT_URI\n", + " }\n", + " }\n", + " }\n", + " }\n", + "}\n", + "\n", + "# Save the request to a JSON file\n", + "with open(\"tuning_request.json\", \"w\") as f:\n", + " json.dump(tuning_request, f, indent=2)\n", + "\n", + "print(\"✅ Tuning job request created\")\n", + "print(\"\\nRequest configuration:\")\n", + "print(\" ✓ Base model: gemini-2.5-flash\")\n", + "print(f\" ✓ Training dataset: {TRAINING_DATASET_URI}\")\n", + "print(f\" ✓ Validation dataset: {VALIDATION_DATASET_URI}\")\n", + "print(\" ✓ Custom metric: Summary Word Overlap F1 Score\")\n", + "print(\" ✓ Aggregation: AVERAGE\")\n", + "print(f\" ✓ Results output: {EVAL_OUTPUT_URI}\")\n", + "print(\"\\nSaved to: tuning_request.json\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "_2kYi8o7yN4G" + }, + "source": [ + "### Step 2.3: Submit the tuning job\n", + "\n", + "Now we'll submit the tuning job using the Vertex AI REST API with `curl`.\n", + "\n", + "**What happens when you run this cell:**\n", + "1. The API creates a new tuning job\n", + "2. Returns immediately with a job ID\n", + "3. Training starts in the background (takes 30-60 minutes)\n", + "4. Your custom metric will be evaluated periodically during training\n", + "\n", + "**Expected output:** You'll receive a JSON response containing:\n", + "- `name`: The full tuning job resource name\n", + "- `state`: Should be `JOB_STATE_PENDING` initially\n", + "- `tunedModelDisplayName`: The name of your tuned model\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "NGmRlEEuyRa3" + }, + "outputs": [], + "source": [ + "# Build the Vertex AI tuning jobs API endpoint\n", + "API_ENDPOINT = f\"https://{LOCATION}-aiplatform.googleapis.com/v1beta1/projects/{PROJECT_ID}/locations/{LOCATION}/tuningJobs\"\n", + "\n", + "print(\"🚀 Submitting tuning job with custom evaluation metric...\")\n", + "print(f\"\\nAPI Endpoint: {API_ENDPOINT}\\n\")\n", + "\n", + "# Submit the tuning job using curl\n", + "!curl -X POST \\\n", + " -H \"Content-Type: application/json\" \\\n", + " -H \"Authorization: Bearer $(gcloud auth print-access-token)\" \\\n", + " {API_ENDPOINT} \\\n", + " -d @tuning_request.json\n", + "\n", + "print(\"\\n\" + \"=\"*80)\n", + "print(\"✅ Tuning job submitted successfully!\")\n", + "print(\"=\"*80)\n", + "print(\"\\n📋 Next steps:\")\n", + "print(\" 1. Copy the 'name' field from the response above\")\n", + "print(\" 2. Run the next cell to monitor the job status\")\n", + "print(\" 3. Your custom metric will be evaluated during training\")\n", + "print(\"\\n⏱ Expected training time: 30-60 minutes\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "n26eKAvZyW0N" + }, + "source": [ + "## Step 3: Monitor Your Custom Metric\n", + "\n", + "Now that your tuning job is running, let's check its status and see your custom metric results. Below you have a quick overview to understanding Tuning Job States:\n", + "\n", + "| State | Meaning |\n", + "|-------|---------|\n", + "| `JOB_STATE_PENDING` | Waiting for resources |\n", + "| `JOB_STATE_RUNNING` | Training in progress |\n", + "| `JOB_STATE_SUCCEEDED` | Training complete! |\n", + "| `JOB_STATE_FAILED` | Something went wrong - check error message |" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "o3SCooxKygFZ" + }, + "source": [ + "### Step 3.1: Check tuning job status\n", + "\n", + "Paste the tuning job name from the previous cell's output to check its status.\n", + "\n", + "**How to use this cell:**\n", + "1. Find the `\"name\"` field in the response above (looks like `projects/.../tuningJobs/...`)\n", + "2. Copy the full path\n", + "3. Paste it in the `TUNING_JOB_NAME` field below\n", + "4. Run the cell\n", + "\n", + "**What you'll see:**\n", + "- Current job state\n", + "- Tuned model details (when complete)\n", + "- Any error messages (if failed)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "eGL2r-AiytMS" + }, + "outputs": [], + "source": [ + "# TODO: Paste your tuning job name from the previous cell\n", + "# fmt: off\n", + "TUNING_JOB_NAME = \"projects/YOUR_PROJECT/locations/us-central1/tuningJobs/YOUR_JOB_ID\" # @param {type:\"string\"}\n", + "TUNING_JOB_NAME = \"projects/541923329259/locations/us-central1/tuningJobs/2125697426391040000\" # @param {type:\"string\"}\n", + "# fmt: on\n", + "\n", + "if \"YOUR_PROJECT\" in TUNING_JOB_NAME or \"YOUR_JOB\" in TUNING_JOB_NAME:\n", + " print(\"⚠ Please paste your tuning job name from the cell above\")\n", + " print(\" It should look like: projects/12345/locations/us-central1/tuningJobs/67890\")\n", + "else:\n", + " # Build the status check URL\n", + " STATUS_URL = f\"https://{LOCATION}-aiplatform.googleapis.com/v1beta1/{TUNING_JOB_NAME}\"\n", + "\n", + " print(f\"📊 Checking tuning job status...\\n\")\n", + "\n", + " # Get the job status\n", + " !curl -s \\\n", + " -H \"Authorization: Bearer $(gcloud auth print-access-token)\" \\\n", + " -H \"Content-Type: application/json\" \\\n", + " {STATUS_URL}\n", + "\n", + " print(\"\\n\" + \"=\"*80)\n", + " print(\"💡 Job Status Tips:\")\n", + " print(\"=\"*80)\n", + " print(\" - PENDING: Job is queued, waiting for resources\")\n", + " print(\" - RUNNING: Training is in progress\")\n", + " print(\" - SUCCEEDED: Training complete! Check evaluationConfig results in GCS\")\n", + " print(\" - FAILED: Check the error message\")\n", + " print(\"\\n Run this cell again to refresh the status\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "bXb3pAa8zNPS" + }, + "source": [ + "### Step 3.2: View custom metric results\n", + "\n", + "Once training completes, your custom metric evaluation results will be saved to Google Cloud Storage.\n", + "\n", + "**What gets saved:**\n", + "- Detailed per-example evaluation scores\n", + "- Aggregate statistics (AVERAGE in our case)\n", + "- Timestamp information\n", + "\n", + "**To view your results:**" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "GmNI6gGSzQ5v" + }, + "outputs": [], + "source": [ + "# List evaluation result files in GCS\n", + "print(f\"📂 Looking for evaluation results in: {EVAL_OUTPUT_URI}\\n\")\n", + "\n", + "!gcloud storage ls --recursive {EVAL_OUTPUT_URI}\n", + "\n", + "print(\"\\n\" + \"=\"*80)\n", + "print(\"📊 Viewing Custom Metric Results\")\n", + "print(\"=\"*80)\n", + "print(f\"\\nEvaluation results are saved in: {EVAL_OUTPUT_URI}\")\n", + "print(\"\\nTo download and view the results:\")\n", + "print(f\"\\n gcloud storage cp --recursive {EVAL_OUTPUT_URI}/* ./eval_results/\")\n", + "print(\"\\nThe results will include:\")\n", + "print(\" - Individual summary evaluation scores\")\n", + "print(\" - Aggregate metrics (AVERAGE F1 score)\")\n", + "print(\" - Model-generated summaries vs. reference summaries\")\n", + "print(\"\\n💡 Use these metrics to track if your summary quality improves during training!\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "bU_NrFeJN1mz" + }, + "source": [ + "## Congratulations!\n", + "\n", + "You've successfully integrated a custom evaluation metric into Gemini supervised fine-tuning!\n", + "\n", + "### What You Accomplished\n", + "\n", + "1. **Wrote a custom metric**: Implemented a word overlap F1 score evaluator for summaries\n", + "2. **Integrated into tuning**: Added the custom metric to a tuning job configuration\n", + "3. **Submitted via REST API**: Used curl to submit the tuning job (no SDK required!)\n", + "4. **Monitored results**: Learned how to check job status and view metric outputs\n", + "\n", + "### Key Takeaways\n", + "\n", + "- **Custom metrics provide visibility**: You can now track summary quality metrics that matter for your specific use case during training\n", + "- **REST API is powerful**: No SDK required—curl gives you full control\n", + "- **Results are stored in GCS**: Detailed per-example scores help you understand model behavior\n", + "\n", + "### Next Steps\n", + "\n", + "**Customize for your use case:**\n", + "- **Multiple aggregations**: Add `MAXIMUM`, `MINIMUM`, `PERCENTILE_P99` to track different statistics\n", + "- **Real datasets**: Replace the sample data with your actual production examples\n", + "- **Compare models**: Run multiple tuning jobs with different configurations and compare custom metrics\n", + "\n", + "**Learn more:**\n", + "- [Vertex AI Tuning Documentation](https://cloud.google.com/vertex-ai/docs/generative-ai/models/tune-models)\n", + "- [Vertex AI Gen AI Evaluation Documentation](https://docs.cloud.google.com/vertex-ai/generative-ai/docs/models/evaluation-overview)" + ] + } + ], + "metadata": { + "colab": { + "name": "sft_gemini_custom_metric_evaluation.ipynb", + "toc_visible": true + }, + "environment": { + "kernel": "python3", + "name": "workbench-notebooks.m132", + "type": "gcloud", + "uri": "us-docker.pkg.dev/deeplearning-platform-release/gcr.io/workbench-notebooks:m132" + }, + "kernelspec": { + "display_name": "Python 3", + "name": "python3" + }, + "language_info": { + "name": "" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/gemini/tuning/sft_gemini_migration_recipe.ipynb b/gemini/tuning/sft_gemini_migration_recipe.ipynb new file mode 100644 index 0000000..7db14e9 --- /dev/null +++ b/gemini/tuning/sft_gemini_migration_recipe.ipynb @@ -0,0 +1,402 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "ur8xi4C7S06n" + }, + "outputs": [], + "source": [ + "# Copyright 2025 Google LLC\n", + "#\n", + "# Licensed under the Apache License, Version 2.0 (the \"License\");\n", + "# you may not use this file except in compliance with the License.\n", + "# You may obtain a copy of the License at\n", + "#\n", + "# https://www.apache.org/licenses/LICENSE-2.0\n", + "#\n", + "# Unless required by applicable law or agreed to in writing, software\n", + "# distributed under the License is distributed on an \"AS IS\" BASIS,\n", + "# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n", + "# See the License for the specific language governing permissions and\n", + "# limitations under the License." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "JAPoU8Sm5E6e" + }, + "source": [ + "# Vertex AI SFT Gemini Migration Recipe\n", + "\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
    \n", + " \n", + " \"Google
    Open in Colab\n", + "
    \n", + "
    \n", + " \n", + " \"Google
    Open in Colab Enterprise\n", + "
    \n", + "
    \n", + " \n", + " \"Vertex
    Open in Vertex AI Workbench\n", + "
    \n", + "
    \n", + " \n", + " \"BigQuery
    Open in BigQuery Studio\n", + "
    \n", + "
    \n", + " \n", + " \"GitHub
    View on GitHub\n", + "
    \n", + "
    \n", + "\n", + "
    \n", + "\n", + "Share to:\n", + "\n", + "\n", + " \"LinkedIn\n", + "\n", + "\n", + "\n", + " \"Bluesky\n", + "\n", + "\n", + "\n", + " \"X\n", + "\n", + "\n", + "\n", + " \"Reddit\n", + "\n", + "\n", + "\n", + " \"Facebook\n", + "" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "84f0f73a0f76" + }, + "source": [ + "| Author(s) |\n", + "| --- |\n", + "| [Jeremy Hao](https://github.com/zh272)\n", + "| [Erwin Huizenga](https://github.com/erwinh85)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "tvgnzT1CKxrO" + }, + "source": [ + "## Overview\n", + "\n", + "This colab provides migration recipe for Gemini managed tuning on the following migration pairs:\n", + "\n", + "- `gemini-1.5-flash-002` -> `gemini-2.5-flash`\n", + "\n", + "Gemini tuning supports the following hyperparameters:\n", + "\n", + "- **Epochs**: The number of complete passes the model makes over the entire training dataset during training.\n", + "- **Adapter size**: The Adapter size to use for the tuning job. The adapter size influences the number of trainable parameters for the tuning job. A larger adapter size implies that the model can learn more complex tasks, but it requires a larger training dataset and longer training times.\n", + "- **Learning Rate Multiplier**: A multiplier to apply to the recommended learning rate. You can increase the value to converge faster, or decrease the value to avoid overfitting.\n", + "\n", + "The values for these hyperparameters impacts the quality of tuned models. Because of model architecture change and tuning infra changes, we do NOT recommend to apply the same hyperparameters from legacy Gemini models to the latest Gemini models.\n", + "\n", + "\n", + "\n", + "\n", + "Note that the provided migration recipe below is for those tuning jobs whose hyper-parameters were set EXPLICITLY on legacy models. In cases where the tuning job to be migrated used default hyper-parameters (i.e. you did not specify hyperparameters explicitly), you can rely on the API or SDK to populate new default values for you on new models." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "61RBz8LLbxCR" + }, + "source": [ + "## Get started" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "No17Cw5hgx12" + }, + "source": [ + "### Install Google Gen AI SDK and other required packages\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "tFy3H3aPgx12" + }, + "outputs": [], + "source": [ + "%pip install --upgrade --quiet google-genai" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "dmWOrTJ3gx13" + }, + "source": [ + "### Authenticate your notebook environment (Colab only)\n", + "\n", + "If you're running this notebook on Google Colab, run the cell below to authenticate your environment." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "NyKGtVQjgx13" + }, + "outputs": [], + "source": [ + "import sys\n", + "\n", + "if \"google.colab\" in sys.modules:\n", + " from google.colab import auth\n", + "\n", + " auth.authenticate_user()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "DF4l8DTdWgPY" + }, + "source": [ + "### Set Google Cloud project information\n", + "\n", + "To get started using Vertex AI, you must have an existing Google Cloud project and [enable the Vertex AI API](https://console.cloud.google.com/flows/enableapi?apiid=aiplatform.googleapis.com).\n", + "\n", + "Learn more about [setting up a project and a development environment](https://cloud.google.com/vertex-ai/docs/start/cloud-environment)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "Nqwi-5ufWp_B" + }, + "outputs": [], + "source": [ + "# Use the environment variable if the user doesn't provide Project ID.\n", + "import os\n", + "\n", + "PROJECT_ID = \"[your-project-id]\" # @param {type: \"string\", placeholder: \"[your-project-id]\", isTemplate: true}\n", + "if not PROJECT_ID or PROJECT_ID == \"[your-project-id]\":\n", + " PROJECT_ID = str(os.environ.get(\"GOOGLE_CLOUD_PROJECT\"))\n", + "\n", + "LOCATION = os.environ.get(\"GOOGLE_CLOUD_REGION\", \"global\")\n", + "\n", + "from google import genai\n", + "\n", + "client = genai.Client(vertexai=True, project=PROJECT_ID, location=LOCATION)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Import libraries" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from IPython.display import Markdown, display" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "id": "6fc324893334" + }, + "outputs": [], + "source": [ + "# @title library\n", + "import math\n", + "import json\n", + "from google.genai import types\n", + "\n", + "def get_tuning_job_hparams(tuning_job):\n", + " job_specs = {}\n", + " job_specs['state'] = tuning_job.state\n", + " job_specs['base_model'] = tuning_job.base_model\n", + " job_specs['tuned_model_display_name'] = tuning_job.tuned_model_display_name\n", + "\n", + " supervised_tuning_spec = tuning_job.supervised_tuning_spec\n", + " job_specs['training_dataset_uri'] = supervised_tuning_spec.training_dataset_uri\n", + " job_specs['validation_dataset_uri'] = supervised_tuning_spec.validation_dataset_uri\n", + "\n", + " hparams = supervised_tuning_spec.hyper_parameters\n", + " job_specs['adapter_size'] = hparams.adapter_size\n", + " job_specs['epoch_count'] = hparams.epoch_count\n", + " job_specs['learning_rate_multiplier'] = hparams.learning_rate_multiplier\n", + "\n", + " sft_data_stats = tuning_job.tuning_data_stats.supervised_tuning_data_stats\n", + " job_specs['sft_total_tokens'] = sft_data_stats.total_billable_token_count\n", + " job_specs['sft_num_examples'] = sft_data_stats.tuning_dataset_example_count\n", + " job_specs['sft_avg_seq_len'] = job_specs['sft_total_tokens'] / job_specs['sft_num_examples']\n", + " # WARNING: API does not give max_seq_len, so using max_input_len+max_output_len as a surrogate, but this may be inaccurate.\n", + " job_specs['sft_max_seq_len'] = sft_data_stats.user_input_token_distribution.max + sft_data_stats.user_output_token_distribution.max\n", + "\n", + " return job_specs\n", + "\n", + "\n", + "def epoch_1_5_flash_to_2_5_flash_short_context(old_epochs: int):\n", + " new_epochs = old_epochs\n", + " if 10< old_epochs < 80:\n", + " new_epochs = math.ceil(10 + 6 * (old_epochs-10) / 70)\n", + " elif old_epochs >= 80:\n", + " new_epochs = 0.2 * old_epochs\n", + " return math.ceil(new_epochs)\n", + "\n", + "def lrm_1_5_flash_to_2_5_flash(old_lrm: float):\n", + " max_lrm = 10\n", + " new_lrm = math.log(1+old_lrm)/math.log(2)\n", + " new_lrm = min(max_lrm, new_lrm)\n", + " return round(new_lrm, 1)\n", + "\n", + "def gemini_1_5_flash_to_gemini_2_5_flash(old_specs: dict):\n", + " if old_specs.get('base_model') != 'gemini-1.5-flash-002':\n", + " return None\n", + " new_specs = {\n", + " 'base_model': 'gemini-2.5-flash',\n", + " 'tuned_model_display_name': old_specs['tuned_model_display_name'] + '_migrated_gemini_2_5_flash',\n", + " 'training_dataset_uri': old_specs['training_dataset_uri'],\n", + " 'validation_dataset_uri': old_specs['validation_dataset_uri'],\n", + " }\n", + " if 'adapter_size' in old_specs:\n", + " new_specs['adapter_size'] = old_specs['adapter_size']\n", + "\n", + " if 'epoch_count' in old_specs:\n", + " old_epochs = old_specs['epoch_count']\n", + " if old_specs['sft_avg_seq_len'] <= 700 and old_specs['sft_max_seq_len'] <= 8192 and old_specs['sft_num_examples'] < 10000:\n", + " new_epochs = epoch_1_5_flash_to_2_5_flash_short_context(old_epochs)\n", + " elif old_specs['sft_avg_seq_len'] <= 3000 and old_specs['sft_max_seq_len'] > 8192:\n", + " new_epochs = epoch_1_5_flash_to_2_5_flash_short_context(old_epochs)\n", + " else:\n", + " new_epochs = old_epochs\n", + " new_specs['epoch_count'] = new_epochs\n", + "\n", + " if 'learning_rate_multiplier' in old_specs:\n", + " new_specs['learning_rate_multiplier'] = lrm_1_5_flash_to_2_5_flash(\n", + " old_specs['learning_rate_multiplier']\n", + " )\n", + " return new_specs\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "EdvJRUWRNGHE" + }, + "source": [ + "## Example" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "id": "cf93d5f0ce00" + }, + "outputs": [], + "source": [ + "# @title Load legacy tuning job\n", + "\n", + "legacy_tuning_job_number = \"[your-tuning-job-number]\" # @param {type:\"string\"}\n", + "if not legacy_tuning_job_number or legacy_tuning_job_number == \"[your-tuning-job-number]\":\n", + " raise Exception(\"Please provide a tuning job number\")\n", + "\n", + "legacy_tuning_job_name = f'projects/{PROJECT_NUMBER}/locations/{LOCATION}/tuningJobs/{legacy_tuning_job_number}'\n", + "print('Double check tuning job name:', legacy_tuning_job_name)\n", + "legacy_tuning_job = client.tunings.get(name=legacy_tuning_job_name)\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "ERUq7IrBOSKn" + }, + "outputs": [], + "source": [ + "# @title gemini-1.5-flash-002 --> gemini-2.5-flash\n", + "\n", + "legacy_job_specs = get_tuning_job_hparams(legacy_tuning_job)\n", + "new_job_specs = gemini_1_5_flash_to_gemini_2_5_flash(legacy_job_specs)\n", + "print('[1.5-flash tuning hparams]:')\n", + "print(json.dumps(legacy_job_specs, indent=2, default=str))\n", + "print('[2.5-flash tuning hparams]:')\n", + "print(json.dumps(new_job_specs, indent=2, default=str))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "LGtyr2d3Omr-" + }, + "outputs": [], + "source": [ + "# @title Create tuning job with new base_model\n", + "\n", + "tuning_job = client.tunings.tune(\n", + " base_model=new_job_specs['base_model'],\n", + " training_dataset=types.TuningDataset(\n", + " gcs_uri=new_job_specs['training_dataset_uri'],\n", + " ),\n", + " config=types.CreateTuningJobConfig(\n", + " epoch_count=new_job_specs['epoch_count'],\n", + " adapter_size=new_job_specs['adapter_size'],\n", + " learning_rate_multiplier=new_job_specs['learning_rate_multiplier'],\n", + " tuned_model_display_name=new_job_specs['tuned_model_display_name'],\n", + " validation_dataset=types.TuningValidationDataset(\n", + " gcs_uri=new_job_specs['validation_dataset_uri'],\n", + " ),\n", + " ),\n", + ")\n", + "print(tuning_job)" + ] + } + ], + "metadata": { + "colab": { + "provenance": [] + }, + "kernelspec": { + "display_name": "Python 3", + "name": "python3" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} diff --git a/gemini/tuning/sft_gemini_on_image_captioning.ipynb b/gemini/tuning/sft_gemini_on_image_captioning.ipynb new file mode 100644 index 0000000..259c212 --- /dev/null +++ b/gemini/tuning/sft_gemini_on_image_captioning.ipynb @@ -0,0 +1,1918 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "id": "9f0d0f32-23b4-41a6-b364-579da297c326" + }, + "outputs": [], + "source": [ + "# @title Copyright & License (click to expand)\n", + "# Copyright 2024 Google LLC\n", + "#\n", + "# Licensed under the Apache License, Version 2.0 (the \"License\");\n", + "# you may not use this file except in compliance with the License.\n", + "# You may obtain a copy of the License at\n", + "#\n", + "# https://www.apache.org/licenses/LICENSE-2.0\n", + "#\n", + "# Unless required by applicable law or agreed to in writing, software\n", + "# distributed under the License is distributed on an \"AS IS\" BASIS,\n", + "# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n", + "# See the License for the specific language governing permissions and\n", + "# limitations under the License." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "dd53d60c-97eb-4c72-91ea-f274a753ab34" + }, + "source": [ + "# Supervised Fine Tuning with Gemini 2.5 Flash for Image Captioning\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + "
    \n", + " \n", + " \"Google
    Open in Colab\n", + "
    \n", + "
    \n", + " \n", + " \"Google
    Open in Colab Enterprise\n", + "
    \n", + "
    \n", + " \n", + " \"Vertex
    Open in Workbench\n", + "
    \n", + "
    \n", + " \n", + " \"GitHub
    View on GitHub\n", + "
    \n", + "
    \n", + "\n", + "
    \n", + "\n", + "Share to:\n", + "\n", + "\n", + " \"LinkedIn\n", + "\n", + "\n", + "\n", + " \"Bluesky\n", + "\n", + "\n", + "\n", + " \"X\n", + "\n", + "\n", + "\n", + " \"Reddit\n", + "\n", + "\n", + "\n", + " \"Facebook\n", + " " + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "MgVK7IeKpW27" + }, + "source": [ + "| Author(s) |\n", + "| --- |\n", + "| [Deepak Moonat](https://github.com/dmoonat) \n", + "| [Erwin Huizenga](https://github.com/erwinh85)\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "9ef820fb-1203-4cab-965f-17093a4ba25e" + }, + "source": [ + "## Overview\n", + "\n", + "**Gemini** is a family of generative AI models developed by Google DeepMind that is designed for multimodal use cases. The Gemini API gives you access to the various Gemini models, such as Gemini 2.0 Pro/Flash, Gemini 2.0/Flash, Gemini/Flash and more.\n", + "\n", + "This notebook demonstrates how to fine-tune the Gemini 2.5 Flash generative model using the Vertex AI Supervised Tuning feature. Supervised Tuning allows you to use your own training data to further refine the base model's capabilities towards your specific tasks.\n", + "\n", + "\n", + "Supervised Tuning uses labeled examples to tune a model. Each example demonstrates the output you want from your text model during inference.\n", + "\n", + "First, ensure your training data is of high quality, well-labeled, and directly relevant to the target task. This is crucial as low-quality data can adversely affect the performance and introduce bias in the fine-tuned model.\n", + "- Training: Experiment with different configurations to optimize the model's performance on the target task.\n", + "- Evaluation:\n", + " - Metric: Choose appropriate evaluation metrics that accurately reflect the success of the fine-tuned model for your specific task\n", + " - Evaluation Set: Use a separate set of data to evaluate the model's performance" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "74b00940-376c-4056-90fb-d22c1ce6eedf" + }, + "source": [ + "### Objective\n", + "\n", + "In this tutorial, you will learn how to use `Vertex AI` to tune a `Gemini 2.5 Flash` model.\n", + "\n", + "\n", + "This tutorial uses the following Google Cloud ML services:\n", + "\n", + "- `Vertex AI`\n", + "\n", + "\n", + "The steps performed include:\n", + "\n", + "- Prepare and load the dataset\n", + "- Load the `gemini-2.5-flash` model\n", + "- Evaluate the model before tuning\n", + "- Tune the model.\n", + " - This will automatically create a Vertex AI endpoint and deploy the model to it\n", + "- Evaluate the model after tuning\n", + "- Make a prediction using tuned model." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "X0xdTMs10K7y" + }, + "source": [ + "### Dataset" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "jCMczwd00N9T" + }, + "source": [ + "Dataset used in this notebook is about image captioning. [Reference](https://ai.google.dev/gemma/docs/paligemma/fine-tuning-paligemma#download_the_model_checkpoint)\n", + "\n", + "```\n", + "Licensed under the Creative Commons Attribution 4.0 License\n", + "```" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "6d7b5435-e947-49bb-9ce3-aa8a42c30118" + }, + "source": [ + "### Costs\n", + "\n", + "This tutorial uses billable components of Google Cloud:\n", + "\n", + "* Vertex AI\n", + "* Cloud Storage\n", + "\n", + "Learn about [Vertex AI\n", + "pricing](https://cloud.google.com/vertex-ai/pricing), [Cloud Storage\n", + "pricing](https://cloud.google.com/storage/pricing), and use the [Pricing\n", + "Calculator](https://cloud.google.com/products/calculator/)\n", + "to generate a cost estimate based on your projected usage." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "0cbf01f0-5f6e-4bcd-903f-84ccaad5332c" + }, + "source": [ + "### Install Gen AI SDK and other required packages" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "b8e4d4521362" + }, + "source": [ + "The new Google Gen AI SDK provides a unified interface to Gemini through both the Gemini Developer API and the Gemini API on Vertex AI. With a few exceptions, code that runs on one platform will run on both. This means that you can prototype an application using the Developer API and then migrate the application to Vertex AI without rewriting your code." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "MpDAgOsK6kZn" + }, + "outputs": [], + "source": [ + "%pip install --upgrade --user --quiet google-genai google-cloud-aiplatform jsonlines rouge_score" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "Moror1y0Qq2z" + }, + "source": [ + "### Restart runtime (Colab only)\n", + "\n", + "To use the newly installed packages, you must restart the runtime on Google Colab." + ] + }, + + { + "cell_type": "markdown", + "metadata": { + "id": "dpSnJTbIrFsh" + }, + "source": [ + "
    \n", + "⚠ The kernel is going to restart. Please wait until it is finished before continuing to the next step. ⚠\n", + "
    " + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "b37d4259-7e39-417b-8879-24f7575732c8" + }, + "source": [ + "## Before you begin\n", + "\n", + "### Set your project ID\n", + "\n", + "**If you don't know your project ID**, try the following:\n", + "* Run `gcloud config list`.\n", + "* Run `gcloud projects list`.\n", + "* See the support page: [Locate the project ID](https://support.google.com/googleapi/answer/7014113)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "caaf0d7e-c6cb-4e56-af5c-553db5180e00" + }, + "outputs": [], + "source": [ + "PROJECT_ID = \"[YOUR_PROJECT_ID]\" # @param {type:\"string\"}\n", + "# Set the project id\n", + "! gcloud config set project {PROJECT_ID}" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "054d794d-cd2e-4280-95ac-859b264ea2d6" + }, + "source": [ + "#### Region\n", + "\n", + "You can also change the `REGION` variable used by Vertex AI. Learn more about [Vertex AI regions](https://cloud.google.com/vertex-ai/docs/general/locations)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "0121bf60-1acd-4272-afaf-aa54b4ded263" + }, + "outputs": [], + "source": [ + "REGION = \"us-central1\" # @param {type:\"string\"}" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "czjH2JfKaGfH" + }, + "source": [ + "#### Bucket\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "c_iZzYtraF3y" + }, + "outputs": [], + "source": [ + "BUCKET_NAME = \"[YOUR_BUCKET_NAME]\" # @param {type:\"string\"}\n", + "BUCKET_URI = f\"gs://{BUCKET_NAME}\"" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "eac9e842-d225-4876-836f-afdb1937d800" + }, + "source": [ + "### Authenticate your Google Cloud account\n", + "\n", + "Depending on your Jupyter environment, you may have to manually authenticate. Follow the relevant instructions below.\n", + "\n", + "**1. Vertex AI Workbench**\n", + "* Do nothing as you are already authenticated.\n", + "\n", + "**2. Local JupyterLab instance, uncomment and run:**" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "23082eec-b1bd-4594-b5b5-56fe2b74db6f" + }, + "outputs": [], + "source": [ + "# ! gcloud auth login" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "3c20f923-3c46-4d6d-80d2-d7cb22b1a8da" + }, + "source": [ + "**3. Authenticate your notebook environment**\n", + "\n", + "If you are running this notebook on Google Colab, run the cell below to authenticate your environment." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "60302a3f-fad9-452c-8998-a9c9822d2732" + }, + "outputs": [], + "source": [ + "from google.colab import auth\n", + "\n", + "auth.authenticate_user()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "ac33116d-b079-46cb-9614-86326c211e00" + }, + "source": [ + "**4. Service account or other**\n", + "* See how to grant Cloud Storage permissions to your service account at https://cloud.google.com/storage/docs/gsutil/commands/iam#ch-examples." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "e6a924d0-a034-4e53-b240-03d356c7b7a6" + }, + "source": [ + "### Import libraries" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "463729ba-ec3c-4302-95bf-80207b0f9e2d" + }, + "outputs": [], + "source": [ + "import io\n", + "import time\n", + "\n", + "# For visualization.\n", + "from PIL import Image\n", + "from google import genai\n", + "\n", + "# For Google Cloud Storage service.\n", + "from google.cloud import storage\n", + "\n", + "# For fine tuning Gemini model.\n", + "import google.cloud.aiplatform as aiplatform\n", + "from google.genai import types\n", + "\n", + "# For data handling.\n", + "import jsonlines\n", + "import pandas as pd\n", + "\n", + "# For evaluation.\n", + "from rouge_score import rouge_scorer\n", + "from tqdm import tqdm" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "a522acfe-d0b6-4b4e-b201-0a4ccf59b133" + }, + "source": [ + "## Initialize Vertex AI and Gen AI SDK for python" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "c845aca6-4f72-4d3b-b9ed-de4a18fcbbf8" + }, + "outputs": [], + "source": [ + "aiplatform.init(project=PROJECT_ID, location=REGION)\n", + "\n", + "client = genai.Client(vertexai=True, project=PROJECT_ID, location=REGION)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "okht6CExcw4d" + }, + "source": [ + "## Prepare Multimodal Dataset" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "8N1QCz0MzyD6" + }, + "source": [ + "The dataset used to tune a foundation model needs to include examples that align with the task that you want the model to perform." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "9yp9SQ1M7FSP" + }, + "source": [ + "Note:\n", + "- Only support images and text as input, and text only as output.\n", + "- Maximum 30 Images per tuning example.\n", + "- Maximum image file size: 20MB\n", + "- Image needs to be in `jpeg` or `png` format. Supported mimetypes: `image/jpeg` and `image/png`\n", + "\n", + "Input is a jsonl file with each json string being on one line.\n", + "Each json instance have the format (Expanded for clarity):\n", + "```\n", + "{\n", + " \"contents\":[\n", + " {\n", + " \"role\":\"user\", # This indicate input content\n", + " \"parts\":[ # Interleaved image and text, could be in any order.\n", + " {\n", + " \"fileData\":{ # FileData needs to be reference to image file in gcs. No inline data.\n", + " \"mimeType\":\"image/jpeg\", # Provide the mimeType about this image\n", + " \"fileUri\":\"gs://path/to/image_uri\"\n", + " }\n", + " }\n", + " {\n", + " \"text\":\"What is in this image?\"\n", + " }\n", + " ]\n", + " },\n", + " {\n", + " \"role\":\"model\", # This indicate target content\n", + " \"parts\":[ # text only\n", + " {\n", + " \"text\":\"Something about this image.\"\n", + " }\n", + " ]\n", + " } # Single turn input and response.\n", + " ]\n", + "}\n", + "```\n", + "\n", + "Example:\n", + "```\n", + "{\n", + " \"contents\":[\n", + " {\n", + " \"role\":\"user\",\n", + " \"parts\":[\n", + " {\n", + " \"fileData\":{\n", + " \"mimeType\":\"image/jpeg\",\n", + " \"fileUri\":\"gs://bucketname/data/vision_data/task/image_description/image/1.jpeg\"\n", + " }\n", + " },\n", + " {\n", + " \"text\":\"Describe this image that captures the essence of it.\"\n", + " }\n", + " ]\n", + " },\n", + " {\n", + " \"role\":\"model\",\n", + " \"parts\":[\n", + " {\n", + " \"text\":\"A person wearing a pink shirt and a long-sleeved shirt with a large cuff, ....\"\n", + " }\n", + " ]\n", + " }\n", + " ]\n", + "}\n", + "```\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "DESw8v4QrLHR" + }, + "source": [ + "### Data files\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "uiTVJqMXTvM5" + }, + "source": [ + "Data used in this notebook is present in the public Cloud Storage(GCS) bucket, `gs://longcap100`.\n", + "\n", + "Sample:\n", + "\n", + "> {\"prefix\": \"\", \"suffix\": \"A person wearing a pink shirt and a long-sleeved shirt with a large cuff, has their hand on a concrete ledge. The hand is on the edge of the ledge, and the thumb is on the edge of the hand. The shirt has a large cuff, and the sleeve is rolled up. The shadow of the hand is on the wall.\", \"image\": \"91.jpeg\"}\n", + "\n", + "\n", + "\n", + "- `data_train90.jsonl`: Contains training samples in json lines as shown above\n", + "- `data_val10.jsonl`: Contains validation samples in json lines as shown above\n", + "- `images`: Contains 100 images, training and validation data" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "MLcuIXlzz36C" + }, + "source": [ + "To run a tuning job, you need to upload one or more datasets to a Cloud Storage bucket. You can either create a new Cloud Storage bucket or use an existing one to store dataset files. The region of the bucket doesn't matter, but we recommend that you use a bucket that's in the same Google Cloud project where you plan to tune your model." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "sfIUgj-mU8K9" + }, + "source": [ + "### Create a Cloud Storage bucket" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "T_uC6nuFU-XU" + }, + "source": [ + "- Create a storage bucket to store intermediate artifacts such as datasets.\n", + "\n", + "- Only if your bucket doesn't already exist: Run the following cell to create your Cloud Storage bucket.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "M-L1BH8TU9Gn" + }, + "outputs": [], + "source": [ + "!gsutil mb -l {REGION} -p {PROJECT_ID} {BUCKET_URI}" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "ZUGi7ZThbChr" + }, + "source": [ + "### Copy images to specified Bucket" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "DHdC-9nj071o" + }, + "outputs": [], + "source": [ + "!gsutil -m -q cp -n -r gs://longcap100/*.jpeg {BUCKET_URI}/images/" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "fpyJR6tlVRXh" + }, + "source": [ + "- Download the training and validation dataset jsonlines files from the bucket." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "peUixIt_2DLP" + }, + "outputs": [], + "source": [ + "!gsutil -m -q cp -n -r gs://longcap100/data_train90.jsonl ." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "rtXMRqAi1WiF" + }, + "outputs": [], + "source": [ + "!gsutil -m -q cp -n -r gs://longcap100/data_val10.jsonl ." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "a9N-rN7pECKa" + }, + "source": [ + "### Prepare dataset for Training and Evaluation" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "KEfGLRVfsrii" + }, + "source": [ + "- Utility function to save json instances into jsonlines format" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "zdVGCwFWsrCB" + }, + "outputs": [], + "source": [ + "def save_jsonlines(file, instances):\n", + " \"\"\"\n", + " Saves a list of json instances to a jsonlines file.\n", + " \"\"\"\n", + " with jsonlines.open(file, mode=\"w\") as writer:\n", + " writer.write_all(instances)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "-hMIYgYBsbUt" + }, + "source": [ + "- Below function converts the dataset into Gemini-1.5 tuning format" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "0TFcj_tjaALV" + }, + "outputs": [], + "source": [ + "task_prompt = \"Describe this image in detail that captures the essence of it.\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "LZ1cauVkz8Vv" + }, + "outputs": [], + "source": [ + "def create_tuning_samples(file_path):\n", + " \"\"\"\n", + " Creates tuning samples from a file.\n", + " \"\"\"\n", + " with jsonlines.open(file_path) as reader:\n", + " instances = []\n", + " for obj in reader:\n", + " instance = {\n", + " \"contents\": [\n", + " {\n", + " \"role\": \"user\", # This indicate input content\n", + " \"parts\": [ # Interleaved image and text, could be in any order.\n", + " {\n", + " \"fileData\": { # FileData needs to be reference to image file in gcs. No inline data.\n", + " \"mimeType\": \"image/jpeg\", # Provide the mimeType about this image\n", + " \"fileUri\": f\"{BUCKET_URI}/images/{obj['image']}\",\n", + " }\n", + " },\n", + " {\"text\": task_prompt},\n", + " ],\n", + " },\n", + " {\n", + " \"role\": \"model\", # This indicate target content\n", + " \"parts\": [{\"text\": obj[\"suffix\"]}], # text only\n", + " }, # Single turn input and response.\n", + " ]\n", + " }\n", + " instances.append(instance)\n", + " return instances" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "tqh6WYHg6X4z" + }, + "source": [ + "- Training data" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "b685Iy27z1E1" + }, + "outputs": [], + "source": [ + "train_file_path = \"data_train90.jsonl\"\n", + "train_instances = create_tuning_samples(train_file_path)\n", + "# save the training instances to jsonl file\n", + "save_jsonlines(\"train.jsonl\", train_instances)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "UC4ULRC46mA-" + }, + "outputs": [], + "source": [ + "train_instances[0]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "nyn5Xgw41bhc" + }, + "outputs": [], + "source": [ + "# save the training data to GCS bucket\n", + "!gsutil cp train.jsonl {BUCKET_URI}/train/" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "HLsC3IBL6ZWk" + }, + "source": [ + "- Validation data" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "LIp0hdag6bS0" + }, + "outputs": [], + "source": [ + "val_file_path = \"data_val10.jsonl\"\n", + "val_instances = create_tuning_samples(val_file_path)\n", + "# save the training instances to jsonl file\n", + "save_jsonlines(\"val.jsonl\", val_instances)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "TBTBTx4n6koL" + }, + "outputs": [], + "source": [ + "val_instances[0]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "xy-6ihNR6gx3" + }, + "outputs": [], + "source": [ + "# save the validation data to GCS bucket\n", + "!gsutil cp val.jsonl {BUCKET_URI}/val/" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "QhejcJumTAj3" + }, + "source": [ + "- Below code transforms the jsonl format to following structure\n", + "\n", + "`\n", + "[{'file_uri': '',\n", + " 'ground_truth': 'Experiment with different parameter values to get the best values for the task\n", + "\n", + "Refer to the following [link](https://cloud.google.com/vertex-ai/generative-ai/docs/learn/prompts/adjust-parameter-values) for understanding different parameters" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "zUx23W_r0F8z" + }, + "source": [ + "**Prompt** is a natural language request submitted to a language model to receive a response back\n", + "\n", + "Some best practices include\n", + " - Clearly communicate what content or information is most important\n", + " - Structure the prompt:\n", + " - Defining the role if using one. For example, You are an experienced UX designer at a top tech company\n", + " - Include context and input data\n", + " - Provide the instructions to the model\n", + " - Add example(s) if you are using them\n", + "\n", + "Refer to the following [link](https://cloud.google.com/vertex-ai/generative-ai/docs/learn/prompts/prompt-design-strategies) for prompt design strategies." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "uuKHRy2OVX0w" + }, + "source": [ + "### Task" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "U-YD1J3VTSoI" + }, + "source": [ + "***Task prompt:***\n", + "\n", + "`\n", + "\", Describe this image that captures the essence of it. \"\n", + "`" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "zTZS4IJMTVR1" + }, + "source": [ + "***Query Image (image)***\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "-Ry2IjT2TWwd" + }, + "outputs": [], + "source": [ + "query_image_uri = val_instances[0][\"contents\"][0][\"parts\"][0][\"fileData\"][\"fileUri\"]\n", + "blob_name = query_image_uri.replace(f\"{BUCKET_URI}/\", \"\")\n", + "img = read_image_bytes_from_gcs(BUCKET_NAME, blob_name)\n", + "\n", + "# Display image bytes using pil python library\n", + "image = Image.open(io.BytesIO(img))\n", + "resized_img = image.resize((300, 300))\n", + "display(resized_img)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "04lAlLK53IYS" + }, + "source": [ + "- Test on single instance" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "-MeiP8z-o6qt" + }, + "outputs": [], + "source": [ + "response = client.models.generate_content(\n", + " model=base_model,\n", + " contents=[\n", + " types.Part.from_uri(file_uri=str(query_image_uri), mime_type=\"image/jpeg\"),\n", + " \"Describe this image that captures the essence of it.\",\n", + " ],\n", + " # Optional config\n", + " config={\n", + " \"temperature\": 0.0,\n", + " },\n", + ")\n", + "\n", + "print(response.text.strip())" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "5LISwh5_4R1U" + }, + "source": [ + "- Ground truth" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "aGXbUVK-3lO5" + }, + "outputs": [], + "source": [ + "val_instances[0][\"contents\"][1][\"parts\"][0][\"text\"]" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "MRVAwGLB6KUX" + }, + "source": [ + "- Change prompt to get detailed description for the provided image" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "JO-C5BAVsdfd" + }, + "outputs": [], + "source": [ + "response = client.models.generate_content(\n", + " model=base_model,\n", + " contents=[\n", + " types.Part.from_uri(file_uri=str(query_image_uri), mime_type=\"image/jpeg\"),\n", + " \"Describe this image in detail that captures the essence of it.\",\n", + " ],\n", + " # Optional config\n", + " config={\n", + " \"temperature\": 0.0,\n", + " },\n", + ")\n", + "\n", + "print(response.text.strip())" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "snYSjdzCVjGA" + }, + "source": [ + "## Evaluation before model tuning" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "vVvGqqTSVzUZ" + }, + "source": [ + "- Evaluate the Gemini model on the validation dataset before tuning it on the training dataset." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "otIRm3XBwQnW" + }, + "outputs": [], + "source": [ + "def get_prediction(query_image_uri, base_model):\n", + " \"\"\"Gets the prediction for a given instance.\n", + "\n", + " Args:\n", + " query_image: The path to the query image.\n", + " candidates: A list of paths to the candidate images.\n", + " generation_model: The generation model to use for prediction.\n", + "\n", + " Returns:\n", + " A string containing the prediction.\n", + " \"\"\"\n", + " response = client.models.generate_content(\n", + " model=base_model,\n", + " contents=[\n", + " types.Part.from_uri(file_uri=str(query_image_uri), mime_type=\"image/jpeg\"),\n", + " task_prompt,\n", + " ],\n", + " # Optional config\n", + " config={\n", + " \"temperature\": 0.0,\n", + " },\n", + " )\n", + "\n", + " return response.text.strip()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "rRW5UVau3xfO" + }, + "outputs": [], + "source": [ + "def run_eval(val_df, model=base_model):\n", + " \"\"\"Runs evaluation on the validation dataset.\n", + "\n", + " Args:\n", + " val_df: The validation dataframe.\n", + " generation_model: The generation model to use for evaluation.\n", + "\n", + " Returns:\n", + " A list of predictions on val_df.\n", + " \"\"\"\n", + " predictions = []\n", + " for i, row in tqdm(val_df.iterrows(), total=val_df.shape[0]):\n", + " try:\n", + " prediction = get_prediction(row[\"file_uri\"], model)\n", + " except:\n", + " time.sleep(30)\n", + " prediction = get_prediction(row[\"file_uri\"], model)\n", + " predictions.append(prediction)\n", + " time.sleep(1)\n", + " return predictions" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "29O4EccbqbIa" + }, + "source": [ + "- Evaluate the Gemini model on the test dataset before tuning it on the training dataset.\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "0LunPnr5Tvce" + }, + "source": [ + "
    \n", + "⚠ It will take ~1 min for the model to generate predictions on the provided validation dataset. ⚠\n", + "
    " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "Y2Uy75youUor" + }, + "outputs": [], + "source": [ + "%%time\n", + "predictions = run_eval(val_df, model=base_model)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "7BOg0EZpgg3D" + }, + "outputs": [], + "source": [ + "len(predictions)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "N22X-_V5mlev" + }, + "outputs": [], + "source": [ + "val_df.loc[:, \"basePredictions\"] = predictions" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "bzA_YLSQ67Jc" + }, + "outputs": [], + "source": [ + "val_df" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "nbPYwzNVWgz-" + }, + "source": [ + "### Evaluation metric" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "mvqIYHNCWigP" + }, + "source": [ + "The type of metrics used for evaluation depends on the task that you are evaluating. The following table shows the supported tasks and the metrics used to evaluate each task:\n", + "\n", + "| Task | Metric(s) |\n", + "|-----------------|---------------------------------|\n", + "| Classification | Micro-F1, Macro-F1, Per class F1 |\n", + "| Summarization | ROUGE-L |\n", + "| Question Answering | Exact Match |\n", + "| Text Generation | BLEU, ROUGE-L |\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "BTkLeYDJWre1" + }, + "source": [ + "For this task, we'll using ROUGE metric.\n", + "\n", + "- **Recall-Oriented Understudy for Gisting Evaluation (ROUGE)**: A metric used to evaluate the quality of automatic summaries of text. It works by comparing a generated summary to a set of reference summaries created by humans." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "TIlOr8KFWzqt" + }, + "source": [ + "Now you can take the candidate and reference to evaluate the performance. In this case, ROUGE will give you:\n", + "\n", + "- `rouge-1`, which measures unigram overlap\n", + "- `rouge-2`, which measures bigram overlap\n", + "- `rouge-l`, which measures the longest common subsequence" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "sIVb60EaW2oW" + }, + "source": [ + "- *Recall vs. Precision*\n", + "\n", + " **Recall**, meaning it prioritizes how much of the information in the reference summaries is captured in the generated summary.\n", + "\n", + " **Precision**, which measures how much of the generated summary is relevant to the original text." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "rDwfndw9OAW9" + }, + "source": [ + "- Initialize `rouge_score` object" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "1SEVHIrk69kj" + }, + "outputs": [], + "source": [ + "scorer = rouge_scorer.RougeScorer([\"rouge1\", \"rouge2\", \"rougeL\"], use_stemmer=True)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "_X9vv_gMORkr" + }, + "source": [ + "- Define function to calculate rouge score" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "P6C6EkvFOQzW" + }, + "outputs": [], + "source": [ + "def get_rouge_score(groundTruth, prediction):\n", + " \"\"\"Function to compute rouge score.\n", + "\n", + " Args:\n", + " groundTruth: The ground truth text.\n", + " prediction: The predicted text.\n", + " Returns:\n", + " The rouge score.\n", + " \"\"\"\n", + " scores = scorer.score(target=groundTruth, prediction=prediction)\n", + " return scores" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "J6qBe-Mbtem_" + }, + "source": [ + "- Single instance evaluation" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "BtP0f3GO7zG7" + }, + "outputs": [], + "source": [ + "get_rouge_score(val_df.loc[0, \"ground_truth\"], val_df.loc[0, \"basePredictions\"])" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "3zl1PpGA9oWE" + }, + "outputs": [], + "source": [ + "def calculate_metrics(val_df, prediction_col=\"basePredictions\"):\n", + " \"\"\"Function to compute rouge scores for all instances in the validation dataset.\n", + " Args:\n", + " val_df: The validation dataframe.\n", + " prediction_col: The column name of the predictions.\n", + " Returns:\n", + " A dataframe containing the rouge scores.\n", + " \"\"\"\n", + " records = []\n", + " for row, instance in val_df.iterrows():\n", + " scores = get_rouge_score(instance[\"ground_truth\"], instance[prediction_col])\n", + " records.append(\n", + " {\n", + " \"rouge1_precision\": scores.get(\"rouge1\").precision,\n", + " \"rouge1_recall\": scores.get(\"rouge1\").recall,\n", + " \"rouge1_fmeasure\": scores.get(\"rouge1\").fmeasure,\n", + " \"rouge2_precision\": scores.get(\"rouge2\").precision,\n", + " \"rouge2_recall\": scores.get(\"rouge2\").recall,\n", + " \"rouge2_fmeasure\": scores.get(\"rouge2\").fmeasure,\n", + " \"rougeL_precision\": scores.get(\"rougeL\").precision,\n", + " \"rougeL_recall\": scores.get(\"rougeL\").recall,\n", + " \"rougeL_fmeasure\": scores.get(\"rougeL\").fmeasure,\n", + " }\n", + " )\n", + " metrics = pd.DataFrame(records)\n", + " return metrics" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "SS2UrB9g8NBt" + }, + "outputs": [], + "source": [ + "evaluation_df_stats = calculate_metrics(val_df, prediction_col=\"basePredictions\")\n", + "evaluation_df_stats" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "ZEyRYhEBZwy9" + }, + "outputs": [], + "source": [ + "print(\"Mean rougeL_precision is\", evaluation_df_stats.rougeL_precision.mean())\n", + "print(\"Mean rougeL_recall is\", evaluation_df_stats.rougeL_recall.mean())\n", + "print(\"Mean rougeL_fmeasure is\", evaluation_df_stats.rougeL_fmeasure.mean())" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "uYAjjpdG_cpP" + }, + "source": [ + "## Fine-tune the model" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "lQehNcLG_4Nc" + }, + "source": [ + "You can create a supervised fine-tuning job by using the Google Gen AI SDK for Python.\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "d1263b90fbc4" + }, + "source": [ + "When you run a supervised fine-tuning job, the model learns additional parameters that help it encode the necessary information to perform the desired task or learn the desired behavior. These parameters are used during inference. The output of the tuning job is a new model that combines the newly learned parameters with the original model." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "EyqBRoY5rscI" + }, + "source": [ + "**Tuning Job parameters**\n", + "\n", + "- `source_model`: Specifies the base Gemini model version you want to fine-tune.\n", + "- `train_dataset`: Path to your training data in JSONL format.\n", + "\n", + "\n", + " *Optional parameters*\n", + " - `validation_dataset`: If provided, this data is used to evaluate the model during tuning.\n", + " - `tuned_model_display_name`: Display name for the tuned model.\n", + "\n", + " *Hyperparameters*\n", + " - `epochs`: The number of training epochs to run.\n", + " - `learning_rate_multiplier`: A value to scale the learning rate during training.\n", + " - `adapter_size` : Gemini 2.5 Flash supports Adapter length [1, 2, 4, 8], default value is 4.\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "UJ0gxBeyqO9k" + }, + "source": [ + "**Note: The default hyperparameter settings are optimized for optimal performance based on rigorous testing and are recommended for initial use. Users may customize these parameters to address specific performance requirements.**" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "anlX0A5aAIPx" + }, + "source": [ + "- Check out the [documentation](https://cloud.google.com/vertex-ai/generative-ai/docs/models/gemini-use-supervised-tuning#tuning_hyperparameters) to learn more.\n", + "- [Gen AI SDK for tuning job](https://googleapis.github.io/python-genai/genai.html#genai.types.CreateTuningJobConfig)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "_vbe8o4_8qV6" + }, + "outputs": [], + "source": [ + "tuned_model_display_name = \"[DISPLAY NAME FOR TUNED MODEL]\" # @param {type:\"string\"}\n", + "\n", + "training_dataset = {\n", + " \"gcs_uri\": f\"{BUCKET_URI}/train/train.jsonl\",\n", + "}\n", + "\n", + "validation_dataset = types.TuningValidationDataset(\n", + " gcs_uri=f\"{BUCKET_URI}/val/val.jsonl\"\n", + ")\n", + "\n", + "\n", + "sft_tuning_job = client.tunings.tune(\n", + " base_model=base_model,\n", + " training_dataset=training_dataset,\n", + " config=types.CreateTuningJobConfig(\n", + " adapter_size=\"ADAPTER_SIZE_EIGHT\",\n", + " epoch_count=1, # set to one to keep time and cost low\n", + " tuned_model_display_name=tuned_model_display_name,\n", + " validation_dataset=validation_dataset,\n", + " ),\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "hSDpQGUeERcH" + }, + "outputs": [], + "source": [ + "# Get the tuning job info.\n", + "tuning_job = client.tunings.get(name=sft_tuning_job.name)\n", + "tuning_job" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "RE1a3AgRsqJh" + }, + "source": [ + "**Note: Tuning time depends on several factors, such as training data size, number of epochs, learning rate multiplier, etc.**" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "qHlfSLjKsruX" + }, + "source": [ + "
    \n", + "⚠ It will take 30-40 mins for the model tuning job to complete on the provided dataset and set configurations/hyperparameters. ⚠\n", + "
    " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "97EUpJwisv_Q" + }, + "outputs": [], + "source": [ + "%%time\n", + "# Wait for job completion\n", + "\n", + "running_states = [\n", + " \"JOB_STATE_PENDING\",\n", + " \"JOB_STATE_RUNNING\",\n", + "]\n", + "\n", + "while tuning_job.state.name in running_states:\n", + " tuning_job = client.tunings.get(name=sft_tuning_job.name)\n", + " time.sleep(10)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "5680557f-67bd-4e8c-a383-02ab655246c5" + }, + "source": [ + "## Evaluation Post-tuning" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "c3d1f75bddea" + }, + "source": [ + "- Evaluate the Gemini model on the validation dataset with tuned model." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "bK2Cyrhavw-Y" + }, + "outputs": [], + "source": [ + "tuned_model = tuning_job.tuned_model.endpoint\n", + "tuning_experiment_name = tuning_job.experiment\n", + "\n", + "print(\"Tuned model experiment\", tuning_experiment_name)\n", + "print(\"Tuned model endpoint resource name:\", tuned_model)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "oVEEGZ-cuYx2" + }, + "source": [ + "- Get a prediction from tuned model" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "Bp4yHwjNJbLQ" + }, + "outputs": [], + "source": [ + "response = client.models.generate_content(\n", + " model=tuned_model,\n", + " contents=[\n", + " types.Part.from_uri(file_uri=str(query_image_uri), mime_type=\"image/jpeg\"),\n", + " task_prompt,\n", + " ],\n", + " # Optional config\n", + " config={\n", + " \"temperature\": 0,\n", + " },\n", + ")\n", + "\n", + "print(response.text.strip())" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "s_1-lbJZugY0" + }, + "source": [ + "- Evaluate the tuned model on entire validation set" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "B7sRtCFCUiag" + }, + "source": [ + "
    \n", + "⚠ It will take ~1 min for the model to generate predictions on the provided validation dataset. ⚠\n", + "
    " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "pWxg3i3a391K" + }, + "outputs": [], + "source": [ + "%%time\n", + "predictions_tuned = run_eval(val_df, model=tuned_model)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "V0wJNPEf5-6I" + }, + "outputs": [], + "source": [ + "val_df.loc[:, \"tunedPredictions\"] = predictions_tuned" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "og4hVmwCuuPW" + }, + "outputs": [], + "source": [ + "evaluation_df_post_tuning_stats = calculate_metrics(\n", + " val_df, prediction_col=\"tunedPredictions\"\n", + ")\n", + "evaluation_df_post_tuning_stats" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "heKx9Lu5vBYb" + }, + "source": [ + "- Improvement" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "X2AVUCh3S656" + }, + "outputs": [], + "source": [ + "evaluation_df_post_tuning_stats.rougeL_precision.mean()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "kTnfegPcvC-P" + }, + "outputs": [], + "source": [ + "improvement = round(\n", + " (\n", + " (\n", + " evaluation_df_post_tuning_stats.rougeL_precision.mean()\n", + " - evaluation_df_stats.rougeL_precision.mean()\n", + " )\n", + " / evaluation_df_stats.rougeL_precision.mean()\n", + " )\n", + " * 100,\n", + " 2,\n", + ")\n", + "print(\n", + " f\"Model tuning has improved the rougeL_precision by {improvement}% (result might differ based on each tuning iteration)\"\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "qrs0o6-p6Ebr" + }, + "outputs": [], + "source": [ + "# Save predicitons\n", + "predictions_all = val_df.to_csv(\"validation_pred.csv\", index=False)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "yUuvCQ2O-1OW" + }, + "source": [ + "## Conclusion" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "me908QT9-26J" + }, + "source": [ + "Performance could be further improved:\n", + "- By adding more training samples. In general, improve your training data quality and/or quantity towards getting a more diverse and comprehensive dataset for your task\n", + "- By tuning the hyperparameters, such as epochs, learning rate multiplier or adapter size\n", + " - To find the optimal number of epochs for your dataset, we recommend experimenting with different values. While increasing epochs can lead to better performance, it's important to be mindful of overfitting, especially with smaller datasets. If you see signs of overfitting, reducing the number of epochs can help mitigate the issue\n", + "- You may try different prompt structures/formats and opt for the one with better performance" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "F7pq-hvxvy8_" + }, + "source": [ + "## Cleaning up" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "LokkxNS0vzM-" + }, + "source": [ + "To clean up all Google Cloud resources used in this project, you can [delete the Google Cloud\n", + "project](https://cloud.google.com/resource-manager/docs/creating-managing-projects#shutting_down_projects) you used for the tutorial.\n", + "\n", + "\n", + "Otherwise, you can delete the individual resources you created in this tutorial.\n", + "\n", + "Refer to this [instructions](https://cloud.google.com/vertex-ai/docs/tutorials/image-classification-custom/cleanup#delete_resources) to delete the resources from console." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "H38EHjj3vwib" + }, + "outputs": [], + "source": [ + "# Delete Experiment.\n", + "delete_experiments = True\n", + "if delete_experiments:\n", + " experiments_list = aiplatform.Experiment.list()\n", + " for experiment in experiments_list:\n", + " if experiment.resource_name == tuning_experiment_name:\n", + " print(experiment.resource_name)\n", + " experiment.delete()\n", + " break\n", + "\n", + "print(\"***\" * 10)\n", + "\n", + "# Delete Endpoint.\n", + "delete_endpoint = True\n", + "# If force is set to True, all deployed models on this\n", + "# Endpoint will be first undeployed.\n", + "if delete_endpoint:\n", + " for endpoint in aiplatform.Endpoint.list():\n", + " if endpoint.resource_name == tuned_model:\n", + " print(endpoint.resource_name)\n", + " endpoint.delete(force=True)\n", + " break\n", + "\n", + "print(\"***\" * 10)\n", + "\n", + "# Delete Cloud Storage Bucket.\n", + "delete_bucket = True\n", + "if delete_bucket:\n", + " ! gsutil -m rm -r $BUCKET_URI" + ] + } + ], + "metadata": { + "colab": { + "name": "sft_gemini_on_image_captioning.ipynb", + "toc_visible": true + }, + "kernelspec": { + "display_name": "Python 3", + "name": "python3" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} diff --git a/gemini/tuning/sft_gemini_on_multiple_images.ipynb b/gemini/tuning/sft_gemini_on_multiple_images.ipynb new file mode 100644 index 0000000..7c55e8a --- /dev/null +++ b/gemini/tuning/sft_gemini_on_multiple_images.ipynb @@ -0,0 +1,929 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "ur8xi4C7S06n" + }, + "outputs": [], + "source": [ + "# Copyright 2024 Google LLC\n", + "#\n", + "# Licensed under the Apache License, Version 2.0 (the \"License\");\n", + "# you may not use this file except in compliance with the License.\n", + "# You may obtain a copy of the License at\n", + "#\n", + "# https://www.apache.org/licenses/LICENSE-2.0\n", + "#\n", + "# Unless required by applicable law or agreed to in writing, software\n", + "# distributed under the License is distributed on an \"AS IS\" BASIS,\n", + "# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n", + "# See the License for the specific language governing permissions and\n", + "# limitations under the License." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "JAPoU8Sm5E6e" + }, + "source": [ + "# Supervised Fine Tuning with Gemini 2.0 Flash for change detection using the Google Gen AI SDK\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + "
    \n", + " \n", + " \"Google
    Open in Colab\n", + "
    \n", + "
    \n", + " \n", + " \"Google
    Open in Colab Enterprise\n", + "
    \n", + "
    \n", + " \n", + " \"Vertex
    Open in Vertex AI Workbench\n", + "
    \n", + "
    \n", + " \n", + " \"GitHub
    View on GitHub\n", + "
    \n", + "
    \n", + "\n", + "
    \n", + "\n", + "Share to:\n", + "\n", + "\n", + " \"LinkedIn\n", + "\n", + "\n", + "\n", + " \"Bluesky\n", + "\n", + "\n", + "\n", + " \"X\n", + "\n", + "\n", + "\n", + " \"Reddit\n", + "\n", + "\n", + "\n", + " \"Facebook\n", + "" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "84f0f73a0f76" + }, + "source": [ + "| Author(s) |\n", + "| --- |\n", + "| [Ivan Nardini](https://github.com/inardini) |\n", + "| [Erwin Huizenga](https://github.com/erwinh85) |" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "tvgnzT1CKxrO" + }, + "source": [ + "## Overview\n", + "\n", + "Fine-tuning Gemini on image data using supervised learning lets you specializing Gemini models for several vision tasks including visual inspection where you want to train a model to identify specific objects or defects either within or between images.\n", + "\n", + "This notebook demonstrates how to fine-tune the Gemini 2.0 model for change detection task (spot differences) with the Vertex AI Supervised Tuning feature using multiple images and text as inputs.\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "BHX-mT4NWVsZ" + }, + "source": [ + "### Dataset\n", + "\n", + "Dataset used is a modified subsample of the [Spot-the-diff dataset](https://github.com/harsh19/spot-the-diff/tree/master), introduced by Jhamtani et al. in Learning to Describe Differences Between Pairs of Similar Images.\n", + "\n", + "```bibtex\n", + "@inproceedings{jhamtani2018learning,\n", + " title={Learning to Describe Differences Between Pairs of Similar Images},\n", + " author={Jhamtani, Harsh and Berg-Kirkpatrick, Taylor},\n", + " booktitle={Proceedings of the 2018 Conference on Empirical Methods in Natural Language Processing (EMNLP)},\n", + " year={2018}\n", + "}\n", + "```\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "61RBz8LLbxCR" + }, + "source": [ + "## Get started" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "No17Cw5hgx12" + }, + "source": [ + "### Install Google Gen AI SDK and other required packages\n", + "The new Google Gen AI SDK provides a unified interface to Gemini through both the Gemini Developer API and the Gemini API on Vertex AI. With a few exceptions, code that runs on one platform will run on both. This means that you can prototype an application using the Developer API and then migrate the application to Vertex AI without rewriting your code.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "tFy3H3aPgx12" + }, + "outputs": [], + "source": [ + "%pip install --upgrade --quiet google-cloud-aiplatform etils google-genai" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "R5Xep4W9lq-Z" + }, + "source": [ + "### Restart runtime\n", + "\n", + "To use the newly installed packages in this Jupyter runtime, you must restart the runtime. You can do this by running the cell below, which restarts the current kernel.\n", + "\n", + "The restart might take a minute or longer. After it's restarted, continue to the next step." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "XRvKdaPDTznN" + }, + "outputs": [], + "source": [ + "import IPython\n", + "\n", + "app = IPython.Application.instance()\n", + "app.kernel.do_shutdown(True)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "SbmM4z7FOBpM" + }, + "source": [ + "
    \n", + "⚠ The kernel is going to restart. In Colab or Colab Enterprise, you might see an error message that says \"Your session crashed for an unknown reason.\" This is expected. Wait until it's finished before continuing to the next step. ⚠\n", + "
    \n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "dmWOrTJ3gx13" + }, + "source": [ + "### Authenticate your notebook environment (Colab only)\n", + "\n", + "If you're running this notebook on Google Colab, run the cell below to authenticate your environment." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "NyKGtVQjgx13" + }, + "outputs": [], + "source": [ + "import sys\n", + "\n", + "if \"google.colab\" in sys.modules:\n", + " from google.colab import auth\n", + "\n", + " auth.authenticate_user()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "DF4l8DTdWgPY" + }, + "source": [ + "### Set Google Cloud project information and initialize the Google Gen AI SDK\n", + "\n", + "To get started using Vertex AI, you must have an existing Google Cloud project and [enable the Vertex AI API](https://console.cloud.google.com/flows/enableapi?apiid=aiplatform.googleapis.com).\n", + "\n", + "Learn more about [setting up a project and a development environment](https://cloud.google.com/vertex-ai/docs/start/cloud-environment)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "code", + "id": "Nqwi-5ufWp_B" + }, + "outputs": [], + "source": [ + "# Use the environment variable if the user doesn't provide Project ID.\n", + "import os\n", + "\n", + "from google import genai\n", + "from google.genai import types\n", + "import vertexai\n", + "\n", + "PROJECT_ID = \"[your-project-id]\" # @param {type: \"string\", placeholder: \"[your-project-id]\", isTemplate: true}\n", + "if not PROJECT_ID or PROJECT_ID == \"[your-project-id]\":\n", + " PROJECT_ID = str(os.environ.get(\"GOOGLE_CLOUD_PROJECT\"))\n", + "\n", + "REGION = os.environ.get(\"GOOGLE_CLOUD_REGION\", \"us-central1\")\n", + "\n", + "BUCKET_NAME = \"[your-bucket-name]\" # @param {type:\"string\"}\n", + "BUCKET_URI = f\"gs://{BUCKET_NAME}\"\n", + "\n", + "vertexai.init(project=PROJECT_ID, location=REGION, staging_bucket=BUCKET_URI)\n", + "\n", + "client = genai.Client(vertexai=True, project=PROJECT_ID, location=REGION)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "sfIUgj-mU8K9" + }, + "source": [ + "### Create a Cloud Storage bucket\n", + "\n", + "Only run the code below if you want to create a new Google Cloud Storage bucket." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "M-L1BH8TU9Gn" + }, + "outputs": [], + "source": [ + "# ! gsutil mb -l {REGION} -p {PROJECT_ID} {BUCKET_URI}" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "5303c05f7aa6" + }, + "source": [ + "### Import libraries" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "6fc324893334" + }, + "outputs": [], + "source": [ + "from io import BytesIO\n", + "\n", + "# General\n", + "import json\n", + "import random\n", + "import time\n", + "from typing import Any\n", + "\n", + "from PIL import Image\n", + "from etils.epath import Path\n", + "\n", + "# For model evaluation.\n", + "import matplotlib.pyplot as plt\n", + "import numpy as np\n", + "\n", + "# For model fine tuning." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "fH2oJFXjOAFz" + }, + "source": [ + "### Set constants" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "IySWS37JOCJU" + }, + "outputs": [], + "source": [ + "INPUT_SOURCE_DATA_URI = \"gs://github-repo/tuning/data\"\n", + "INPUT_DATA_URI = f\"{BUCKET_URI}/data\"\n", + "MODEL_ID = \"gemini-2.0-flash-001\" # @param {type:\"string\", isTemplate: true}\n", + "\n", + "SYSTEM_INSTRUCTION = \"\"\"You are an expert in \"spot the difference\" games. Your task is to analyze two images and identify their differences.\n", + " Instructions:\n", + " 1. Carefully examine the two provided images.\n", + " 2. List the differences you find between the two images in a clear and concise manner, using bullet points. For example:\n", + " * \"The color of the car in image 1 is red, while in image 2 it is blue.\"\n", + " * \"The house in image 1 has a chimney, but the house in image 2 does not.\"\n", + " 3. If the images are identical, output: \"no difference between two images\"\n", + " \"\"\".strip()\n", + "\n", + "TASK_PROMPT = \"Compare the two images and find differences.\"" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "TbbbPuv2MLSY" + }, + "source": [ + "### Set helpers" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "iMbvf4daTif-" + }, + "outputs": [], + "source": [ + "def save_jsonlines(instances: list[dict[str, Any]], file_path: str) -> None:\n", + " \"\"\"\n", + " Saves a list of JSON-serializable instances to a jsonlines file.\n", + " \"\"\"\n", + " try:\n", + " bucket_path = Path(file_path)\n", + " with bucket_path.open(\"w\") as f:\n", + " for i, instance in enumerate(instances):\n", + " try:\n", + " json.dump(instance, f, ensure_ascii=False)\n", + " f.write(\"\\n\")\n", + " except (TypeError, ValueError) as e:\n", + " raise TypeError(f\"Failed to serialize instance at index {i}: {e}\")\n", + " except Exception as e:\n", + " raise OSError(f\"Failed to write to {file_path}: {e}\")\n", + "\n", + "\n", + "def create_tuning_samples(file_path: str, split: str = \"train\") -> list[dict[str, Any]]:\n", + " \"\"\"\n", + " Creates tuning samples from a jsonlines file for image comparison tasks.\n", + " \"\"\"\n", + "\n", + " bucket_path = Path(file_path)\n", + " instances = []\n", + "\n", + " # Read and parse the jsonlines file\n", + " with bucket_path.open() as f:\n", + " content = f.read()\n", + " data = [\n", + " json.loads(line) for line in content.strip().split(\"\\n\") if line.strip()\n", + " ]\n", + "\n", + " # Process each instance\n", + " for obj in data:\n", + " image_path = f\"{bucket_path.parent}/{split}/{obj['img_id']}\"\n", + " instance = {\n", + " \"systemInstruction\": {\n", + " \"role\": \"string\",\n", + " \"parts\": [{\"text\": SYSTEM_INSTRUCTION}],\n", + " },\n", + " \"contents\": [\n", + " {\n", + " \"role\": \"user\",\n", + " \"parts\": [\n", + " {\n", + " \"fileData\": {\n", + " \"mimeType\": \"image/png\",\n", + " \"fileUri\": f\"{image_path}.png\",\n", + " }\n", + " },\n", + " {\"text\": \"Image 1.\"},\n", + " {\n", + " \"fileData\": {\n", + " \"mimeType\": \"image/png\",\n", + " \"fileUri\": f\"{image_path}_2.png\",\n", + " }\n", + " },\n", + " {\"text\": \"Image 2.\"},\n", + " {\"text\": TASK_PROMPT},\n", + " ],\n", + " },\n", + " {\"role\": \"model\", \"parts\": [{\"text\": obj[\"sentences\"]}]},\n", + " ],\n", + " }\n", + " instances.append(instance)\n", + " return instances\n", + "\n", + "\n", + "def sample_test(input_test_file_path: str) -> dict[str, Any]:\n", + " \"\"\"\n", + " Random sample one image_id and its data from a jsonlines file.\n", + " \"\"\"\n", + " bucket_path = Path(input_test_file_path)\n", + "\n", + " with bucket_path.open() as f:\n", + " content = f.read()\n", + " data = [\n", + " json.loads(line) for line in content.strip().split(\"\\n\") if line.strip()\n", + " ]\n", + "\n", + " # Randomly select one instance\n", + " sampled_instance = random.choice(data)\n", + " return sampled_instance\n", + "\n", + "\n", + "def plot_images_from_uri(image_one_uri: str | Path, image_two_uri: str | Path) -> None:\n", + " \"\"\"\n", + " Plot two images side by side from their URIs using etils for cloud storage support.\n", + " \"\"\"\n", + "\n", + " def load_image(uri):\n", + " path = Path(uri)\n", + " with path.open(\"rb\") as f:\n", + " img_data = f.read()\n", + " img = Image.open(BytesIO(img_data))\n", + " # Convert to RGB if image is in RGBA format\n", + " if img.mode == \"RGBA\":\n", + " img = img.convert(\"RGB\")\n", + " return np.array(img)\n", + "\n", + " # Create figure with two subplots side by side\n", + " fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 6))\n", + "\n", + " # Load and display first image\n", + " img1 = load_image(image_one_uri)\n", + " ax1.imshow(img1)\n", + " ax1.axis(\"off\")\n", + " ax1.set_title(\"Image 1\")\n", + "\n", + " # Load and display second image\n", + " img2 = load_image(image_two_uri)\n", + " ax2.imshow(img2)\n", + " ax2.axis(\"off\")\n", + " ax2.set_title(\"Image 2\")\n", + "\n", + " # Adjust layout and display\n", + " plt.tight_layout()\n", + " plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "WZsHiu5iXwHq" + }, + "source": [ + "## Prepare your multimodal data with multiple inputs\n", + "\n", + "According to the [Prepare supervised fine-tuning data for Gemini models](https://cloud.google.com/vertex-ai/generative-ai/docs/models/gemini-supervised-tuning-prepare) documentation, the model tuning dataset must be in the JSON Lines (JSONL) format, where each line contains a single tuning example.\n", + "\n", + "With multiple multimodal inputs, you have the following JSONL structure:\n", + "\n", + "```\n", + "{\n", + " \"systemInstruction\":{\n", + " \"role\":\"string\",\n", + " \"parts\":[\n", + " {\n", + " \"text\":\"Say something to the model.\"\n", + " }\n", + " ]\n", + " },\n", + " \"contents\":[\n", + " {\n", + " \"role\":\"user\",\n", + " \"parts\":[\n", + " {\n", + " \"fileData\":{\n", + " \"mimeType\":\"image/png\",\n", + " \"fileUri\":\"gs://path/to/image1\"\n", + " }\n", + " },\n", + " {\n", + " \"text\":\"This is the image 1\"\n", + " },\n", + " {\n", + " \"fileData\":{\n", + " \"mimeType\":\"image/png\",\n", + " \"fileUri\":\"gs://path/to/image2\"\n", + " }\n", + " },\n", + " {\n", + " \"text\":\"This is the image 2\"\n", + " },\n", + " {\n", + " \"text\":\"Do something with image 1 and image 2.\"\n", + " }\n", + " ]\n", + " },\n", + " {\n", + " \"role\":\"model\",\n", + " \"parts\":[\n", + " {\n", + " \"text\":\"\"\n", + " }\n", + " ]\n", + " }\n", + " ]\n", + "}\n", + "```\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "ZUGi7ZThbChr" + }, + "source": [ + "### Replicate the multimodal dataset in your bucket\n", + "\n", + "Create a copy of the tutorial dataset in your bucket." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "DHdC-9nj071o" + }, + "outputs": [], + "source": [ + "! gsutil -m -q cp -n -r {INPUT_SOURCE_DATA_URI} {INPUT_DATA_URI}" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "TGOrkqV3M4UJ" + }, + "source": [ + "### Prepare the training dataset\n", + "\n", + "For each sample in the original JSONL train file, prepare the tuning instance.\n", + "Then save training instances in the new JSONL train file." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "-R01yFuFXz4w" + }, + "outputs": [], + "source": [ + "input_train_file_path = Path(INPUT_DATA_URI) / \"train.jsonl\"\n", + "output_train_file_path = Path(INPUT_DATA_URI) / \"prepared_train.jsonl\"\n", + "\n", + "train_instances = create_tuning_samples(input_train_file_path)\n", + "save_jsonlines(train_instances, output_train_file_path)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "Uo2CsadnCPqF" + }, + "source": [ + "### Prepare the val dataset\n", + "\n", + "For each sample in the original JSONL val file, prepare the val instance.\n", + "Then save val instances in the new JSONL val file." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "cPIQoC_qiQjy" + }, + "outputs": [], + "source": [ + "input_val_file_path = Path(INPUT_DATA_URI) / \"val.jsonl\"\n", + "output_val_file_path = Path(INPUT_DATA_URI) / \"prepared_val.jsonl\"\n", + "\n", + "val_instances = create_tuning_samples(input_val_file_path, split=\"val\")\n", + "save_jsonlines(train_instances, output_val_file_path)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "uYAjjpdG_cpP" + }, + "source": [ + "## Tune Gemini model by using supervised fine-tuning\n", + "\n", + "You can create a supervised fine-tuning job by using the Google Gen AI SDK for Python.\n", + "\n", + "While Vertex AI Tuning offers customizable hyperparameters, we recommend using the default settings for optimal performance—these are rigorously tested and ideal for initial runs. Advanced users can adjust these parameters to meet specific needs.\n", + "\n", + "[Check out the documentation](https://cloud.google.com/vertex-ai/generative-ai/docs/models/gemini-use-supervised-tuning#tuning_hyperparameters) to learn more.\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "LIMn6fq6LM3r" + }, + "source": [ + "#### Create a tuning job\n", + "\n", + "Start the tuning job with its default configuration." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "Sr8r90AAiyoB" + }, + "outputs": [], + "source": [ + "train_dataset = str(Path(INPUT_DATA_URI) / \"prepared_train.jsonl\")\n", + "validation_dataset = str(Path(INPUT_DATA_URI) / \"prepared_val.jsonl\")\n", + "\n", + "training_dataset = {\n", + " \"gcs_uri\": train_dataset,\n", + "}\n", + "\n", + "validation_dataset = types.TuningValidationDataset(gcs_uri=validation_dataset)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "GToiYxswipZA" + }, + "outputs": [], + "source": [ + "tuned_model_display_name = \"spot-the-difference-tuning-job\" # @param {type:\"string\"}\n", + "\n", + "sft_tuning_job = client.tunings.tune(\n", + " base_model=MODEL_ID,\n", + " training_dataset=training_dataset,\n", + " config=types.CreateTuningJobConfig(\n", + " adapter_size=\"ADAPTER_SIZE_EIGHT\",\n", + " epoch_count=1, # set to one to keep time and cost low\n", + " tuned_model_display_name=tuned_model_display_name,\n", + " ),\n", + ")\n", + "sft_tuning_job" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "HF3HVA8GTh8N" + }, + "source": [ + "⚠ It will take ~30 mins for the model tuning job to complete on the provided dataset and set configurations/hyperparameters. ⚠" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "0bGX6OjmitaR" + }, + "outputs": [], + "source": [ + "tuning_job = client.tunings.get(name=sft_tuning_job.name)\n", + "tuning_job" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "Whf-i9f-LTvO" + }, + "source": [ + "#### Monitor the tuning job\n", + "\n", + "Check the tuning job's progress." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "sqaAHUmufq8-" + }, + "outputs": [], + "source": [ + "import time\n", + "\n", + "running_states = {\n", + " \"JOB_STATE_PENDING\",\n", + " \"JOB_STATE_RUNNING\",\n", + "}\n", + "\n", + "while sft_tuning_job.state in running_states:\n", + " print(sft_tuning_job.state)\n", + " tuning_job = client.tunings.get(name=sft_tuning_job.name)\n", + " time.sleep(10)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "69IAIZYHLhSu" + }, + "source": [ + "### Get some tuning job details\n", + "\n", + "Following a successful tuning run, retrieve the registered model's resource name and the deployed tuned model's endpoint." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "Up7_r1lWLo6i" + }, + "outputs": [], + "source": [ + "tuned_model_endpoint_name = tuning_job.tuned_model.endpoint\n", + "tuning_experiment_name = sft_tuning_job.experiment\n", + "\n", + "print(\"Tuned model experiment\", tuning_experiment_name)\n", + "print(\"Tuned model endpoint resource name:\", tuned_model_endpoint_name)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "Ag6Zac0HB_Q1" + }, + "source": [ + "## Qualitatively evaluate the tuned model\n", + "\n", + "Assess the tuned model's performance in spotting differences between the two new images within this multimodal, multi-input context. In this simple tutorial, a qualitative analysis is sufficient.\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "qx5m-O3MVRGd" + }, + "source": [ + "#### Extract a random sample\n", + "\n", + "Draw a random image." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "bK2Cyrhavw-Y" + }, + "outputs": [], + "source": [ + "input_test_file_path = Path(INPUT_DATA_URI) / \"test.jsonl\"\n", + "test_sample = sample_test(input_test_file_path)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "mmuoplaJVTxE" + }, + "source": [ + "#### Prepare the image paths\n", + "\n", + "Specify the image paths for comparative analysis." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "tnDiOqu0QpQf" + }, + "outputs": [], + "source": [ + "input_image_one_uri = Path(INPUT_DATA_URI) / \"test\" / f\"{test_sample['img_id']}.png\"\n", + "input_image_two_uri = Path(INPUT_DATA_URI) / \"test\" / f\"{test_sample['img_id']}_2.png\"" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "rqosop77ZIdJ" + }, + "source": [ + "### Plot image samples\n", + "\n", + "Look at the sampled images." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "1UiaDg_3ZKuN" + }, + "outputs": [], + "source": [ + "plot_images_from_uri(input_image_one_uri, input_image_two_uri)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "ZAnOFlhRVYZ-" + }, + "source": [ + "#### Generate the prediction\n", + "\n", + "Make a prediction with the tuned model." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "Bp4yHwjNJbLQ" + }, + "outputs": [], + "source": [ + "tuned_model = tuning_job.tuned_model.endpoint\n", + "\n", + "contents = [\n", + " \"Image 1:\",\n", + " types.Part.from_uri(file_uri=str(input_image_one_uri), mime_type=\"image/jpeg\"),\n", + " \"Image 2:\",\n", + " types.Part.from_uri(file_uri=str(input_image_two_uri), mime_type=\"image/jpeg\"),\n", + "]\n", + "\n", + "response = client.models.generate_content(\n", + " model=tuned_model,\n", + " contents=contents,\n", + " config={\n", + " \"temperature\": 0,\n", + " },\n", + ")\n", + "\n", + "response" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "qGvaDslGVdIC" + }, + "source": [ + "#### Compare the ground truth with the new answer from tuned model\n", + "\n", + "Assess the accuracy of the tuned model's response by comparing it to the ground truth." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "yPXoHT_dTaHt" + }, + "outputs": [], + "source": [ + "print(\"Ground truth answer:\", test_sample[\"sentences\"])\n", + "print(\"Generated answer with fine-tuned model:\", response.text.strip())" + ] + } + ], + "metadata": { + "colab": { + "name": "sft_gemini_on_multiple_images.ipynb", + "toc_visible": true + }, + "kernelspec": { + "display_name": "Python 3", + "name": "python3" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} diff --git a/gemini/tuning/sft_gemini_predictive_maintenance.ipynb b/gemini/tuning/sft_gemini_predictive_maintenance.ipynb new file mode 100644 index 0000000..4e7a7e5 --- /dev/null +++ b/gemini/tuning/sft_gemini_predictive_maintenance.ipynb @@ -0,0 +1,1023 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "id": "GzMT0d7XRdQ3" + }, + "source": [ + "# Supervised Fine-tuning Gemini 2.5 Flash for Predictive Maintenance\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + "
    \n", + " \n", + " \"Google
    Open in Colab\n", + "
    \n", + "
    \n", + " \n", + " \"Google
    Open in Colab Enterprise\n", + "
    \n", + "
    \n", + " \n", + " \"Vertex
    Open in Vertex AI Workbench\n", + "
    \n", + "
    \n", + " \n", + " \"GitHub
    View on GitHub\n", + "
    \n", + "
    \n", + "\n", + "
    " + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "_uco5wDNcIRq" + }, + "source": [ + "Share to:\n", + "\n", + "\n", + " \"LinkedIn\n", + "\n", + "\n", + "\n", + " \"Bluesky\n", + "\n", + "\n", + "\n", + " \"X\n", + "\n", + "\n", + "\n", + " \"Reddit\n", + "\n", + "\n", + "\n", + " \"Facebook\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "M04y-KnqcSCq" + }, + "source": [ + "| Author |\n", + "| --- |\n", + "| [Aniket Agrawal](https://github.com/aniketagrawal2012) |" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "WURYK3ZRRdQ5" + }, + "source": [ + "## Overview\n", + "\n", + "This notebook demonstrates how to perform **supervised fine-tuning** on a Gemini model for a predictive maintenance task within an industrial infrastructure context. We will use the `google-genai` SDK integrated with Vertex AI to train the model to classify equipment status based on simulated sensor readings.\n", + "\n", + "### Use Case: Classifying Equipment Status from Sensor Data\n", + "\n", + "Instead of predicting exact time-to-failure, we'll fine-tune Gemini to classify the operational state of equipment (e.g., \"Normal\", \"Warning\", \"Critical\") based on recent sensor trends. This simplifies the task into a text-generation problem suitable for LLM fine-tuning.\n", + "\n", + "**Workflow:**\n", + "1. **Load/Generate Data**: Create simulated sensor readings and maintenance/failure logs.\n", + "2. **Prepare Tuning Data (JSONL)**: Convert time-series data snippets and corresponding status labels into the JSON Lines format required for Gemini supervised tuning.\n", + "3. **Upload to GCS**: Store the formatted tuning data in a Google Cloud Storage bucket.\n", + "4. **Launch Fine-tuning Job**: Use the `google-genai` SDK client (configured for Vertex AI) to start the supervised tuning job.\n", + "5. **Monitor Job**: Track the progress of the fine-tuning job.\n", + "6. **Evaluate Tuned Model**: Make predictions on new sensor data prompts using the fine-tuned model endpoint and compare qualitatively.\n", + "7. **Integrate Gemini for Reporting**: Use a base Gemini model to summarize the tuning job results." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "NTmKZlmIRdQ5" + }, + "source": [ + "## Setup\n", + "\n", + "### Install required packages" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "PdrVBfYMRdQ5" + }, + "outputs": [], + "source": [ + "import sys # noqa: F401\n", + "\n", + "# Install necessary libraries\n", + "# gcsfs is added to allow pandas to write directly to GCS\n", + "!{sys.executable} -m pip install --upgrade --user --quiet pandas numpy google-cloud-aiplatform google-genai google-cloud-storage gcsfs" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "Yqy4-3cBRdQ6" + }, + "source": [ + "**⚠ Important:** Restart the kernel after installation." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "KS9rUufMRdQ6" + }, + "source": [ + "### Authenticate and Initialize Vertex AI\n", + "\n", + "Set your project, region, and GCS bucket information. We configure the notebook for Vertex AI fine-tuning and reporting." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "PyQnTYGlRdQ6" + }, + "outputs": [], + "source": [ + "import os\n", + "\n", + "import vertexai\n", + "from google.genai import (\n", + " Client as VertexClient, # This is for Vertex AI tuning/models client\n", + ")\n", + "\n", + "# --- Vertex AI Configuration (Required for Fine-tuning Job) ---\n", + "PROJECT_ID = \"\" # @param {type: \"string\", placeholder: \"your-gcp-project-id\"}\n", + "REGION = \"\" # @param {type:\"string\"}\n", + "BUCKET_NAME = \"\" # @param {type:\"string\", placeholder: \"your-gcs-bucket-name\"}\n", + "BUCKET_URI = f\"gs://{BUCKET_NAME}\"\n", + "\n", + "# --- Authentication (Colab/Workbench for Vertex AI) ---\n", + "if not PROJECT_ID or PROJECT_ID == \"\":\n", + " try:\n", + " from google.colab import auth\n", + "\n", + " auth.authenticate_user()\n", + " import subprocess\n", + "\n", + " PROJECT_ID = (\n", + " subprocess.check_output([\"gcloud\", \"config\", \"get-value\", \"project\"])\n", + " .decode(\"utf-8\")\n", + " .strip()\n", + " )\n", + " print(f\"Retrieved Project ID: {PROJECT_ID}\")\n", + " except Exception as e:\n", + " print(\n", + " f\"Could not automatically retrieve Project ID. Please set it manually. Error: {e}\"\n", + " )\n", + "\n", + "# Ensure BUCKET_NAME is set, and attempt to create the bucket\n", + "if not BUCKET_NAME or BUCKET_NAME == \"\":\n", + " if PROJECT_ID:\n", + " BUCKET_NAME = f\"{PROJECT_ID}-gemini-tuning-bucket\"\n", + " BUCKET_URI = f\"gs://{BUCKET_NAME}\"\n", + " print(f\"Bucket name not provided. Using default: {BUCKET_NAME}\")\n", + " else:\n", + " raise ValueError(\n", + " \"Please provide a valid GCS Bucket name or ensure PROJECT_ID is set for default bucket creation.\"\n", + " )\n", + "\n", + "print(f\"Checking/Creating bucket: {BUCKET_URI}\")\n", + "# Use '!' for shell commands in notebooks. `gsutil mb` creates if it doesn't exist.\n", + "try:\n", + " # The '||' syntax works in shell to execute the second command only if the first fails\n", + " # `gsutil ls` returns 0 if bucket exists, non-zero if not.\n", + " # `gsutil mb` creates the bucket.\n", + " creation_command = f\"gsutil ls {BUCKET_URI} > /dev/null 2>&1 || gsutil mb -l {REGION} -p {PROJECT_ID} {BUCKET_URI}\"\n", + " print(f\"Running: {creation_command}\")\n", + " # Using os.system as '!' might behave differently depending on the environment.\n", + " # os.system returns the exit status of the command.\n", + " exit_code = os.system(creation_command)\n", + " if exit_code != 0:\n", + " print(\n", + " f\"Warning: Bucket command finished with exit code {exit_code}. Check GCS permissions or bucket status.\"\n", + " )\n", + " else:\n", + " print(f\"Bucket {BUCKET_URI} ensured to exist.\")\n", + "except Exception as bucket_e:\n", + " print(f\"Error checking/creating bucket: {bucket_e}\")\n", + " raise ValueError(\"Bucket check/creation failed.\") from bucket_e\n", + "\n", + "\n", + "if PROJECT_ID:\n", + " print(\n", + " f\"Initializing Vertex AI for project: {PROJECT_ID} in {REGION} using bucket {BUCKET_URI}\"\n", + " )\n", + " # Initialize Vertex AI SDK (needed for launching the tuning job)\n", + " vertexai.init(project=PROJECT_ID, location=REGION, staging_bucket=BUCKET_URI)\n", + " # Initialize the genai client specifically for Vertex AI operations (like tuning)\n", + " vertex_client = VertexClient(vertexai=True, project=PROJECT_ID, location=REGION)\n", + " print(\"Vertex AI SDK Initialized.\")\n", + "else:\n", + " raise ValueError(\"PROJECT_ID must be set for Vertex AI operations.\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "fQ3ECMSARdQ6" + }, + "source": [ + "### Imports and Global Configuration" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "bV74HX1oRdQ6" + }, + "outputs": [], + "source": [ + "import json\n", + "import random\n", + "import time\n", + "import warnings\n", + "from typing import Any\n", + "\n", + "import numpy as np\n", + "import pandas as pd\n", + "from google.genai import types as genai_types\n", + "\n", + "# --- Global Settings ---\n", + "warnings.filterwarnings(\"ignore\", category=UserWarning)\n", + "warnings.filterwarnings(\"ignore\", category=FutureWarning)\n", + "np.random.seed(42)\n", + "random.seed(42)\n", + "\n", + "# --- Constants ---\n", + "BASE_MODEL_ID = \"gemini-2.5-flash\" # Tunable model ID on Vertex AI\n", + "TUNED_MODEL_DISPLAY_NAME = f\"pred-maint-gemini-tuned-{int(time.time())}\"\n", + "DATA_DIR_GCS = f\"{BUCKET_URI}/pred_maint_tuning_data\"\n", + "TRAIN_JSONL_GCS_URI = f\"{DATA_DIR_GCS}/train_data.jsonl\"\n", + "VALIDATION_JSONL_GCS_URI = f\"{DATA_DIR_GCS}/validation_data.jsonl\"\n", + "TEST_JSONL_GCS_URI = f\"{DATA_DIR_GCS}/test_data.jsonl\" # For qualitative eval later\n", + "\n", + "SEQUENCE_LENGTH = 12 # Use 12 hours of data for context\n", + "FAILURE_PREDICTION_HORIZON_HOURS = 24\n", + "WARNING_HORIZON_HOURS = 72 # Issue warning if failure is within 72 hours\n", + "\n", + "print(f\"Base model for tuning: {BASE_MODEL_ID}\")\n", + "print(f\"Tuning data GCS path: {DATA_DIR_GCS}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "TEUFLjeYRdQ7" + }, + "source": [ + "## Step 1: Generate Simulated Data" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "8RqK0Td6RdQ7" + }, + "outputs": [], + "source": [ + "# Reusing the data generation function from the previous notebook\n", + "def generate_maintenance_data(\n", + " filename=\"equipment_sensor_data.csv\",\n", + " log_filename=\"maintenance_failure_logs.csv\",\n", + " num_rows=2000,\n", + " equipment_id=\"EQ-001\",\n", + ") -> tuple[pd.DataFrame, pd.DataFrame]:\n", + " \"\"\"Generates or loads simulated sensor data and maintenance/failure logs.\"\"\"\n", + " if os.path.exists(filename) and os.path.exists(log_filename):\n", + " print(\n", + " f\"Data files '{filename}' and '{log_filename}' already exist. Loading data.\"\n", + " )\n", + " sensor_df = pd.read_csv(filename, parse_dates=[\"timestamp\"])\n", + " log_df = pd.read_csv(log_filename, parse_dates=[\"timestamp\"])\n", + " return sensor_df, log_df\n", + "\n", + " print(\"Generating new sensor and maintenance log data...\")\n", + " # Generate timestamps with timezone awareness, matching typical sensor data\n", + " timestamps = pd.date_range(\n", + " end=pd.Timestamp.now(tz=\"UTC\"), periods=num_rows, freq=\"h\"\n", + " )\n", + "\n", + " data = {\"timestamp\": timestamps, \"equipment_id\": equipment_id}\n", + " data[\"temperature_c\"] = np.random.normal(\n", + " loc=60, scale=5, size=num_rows\n", + " ) + np.linspace(0, 15, num_rows)\n", + " data[\"vibration_hz\"] = np.random.normal(\n", + " loc=50, scale=2, size=num_rows\n", + " ) + np.random.normal(0, np.linspace(0, 5, num_rows))\n", + " data[\"pressure_psi\"] = np.random.normal(\n", + " loc=100, scale=10, size=num_rows\n", + " ) - np.linspace(0, 5, num_rows)\n", + " sensor_df = pd.DataFrame(data)\n", + "\n", + " log_data = []\n", + " maintenance_indices = np.random.choice(num_rows, size=num_rows // 50, replace=False)\n", + " for idx in maintenance_indices:\n", + " # Check index bounds\n", + " if idx < len(timestamps):\n", + " log_data.append(\n", + " {\n", + " \"timestamp\": timestamps[idx],\n", + " \"equipment_id\": equipment_id,\n", + " \"event_type\": \"Maintenance\",\n", + " \"details\": \"Routine Check\",\n", + " }\n", + " )\n", + "\n", + " failure_indices = np.linspace(num_rows * 0.9, num_rows - 1, num=5).astype(int)\n", + " for idx in failure_indices:\n", + " # Ensure index and timestamp exist before adding log\n", + " if idx < len(timestamps):\n", + " log_data.append(\n", + " {\n", + " \"timestamp\": timestamps[idx],\n", + " \"equipment_id\": equipment_id,\n", + " \"event_type\": \"Failure\",\n", + " \"details\": \"Component Failure\",\n", + " }\n", + " )\n", + " # Introduce anomalies around failures - ensure indices are valid\n", + " start_anomaly = max(0, idx - 10)\n", + " end_anomaly = min(num_rows, idx + 2) # Correct upper bound exclusive issue\n", + " anomaly_size = (end_anomaly - start_anomaly, 2)\n", + " # Ensure anomaly size is valid before applying\n", + " if start_anomaly < end_anomaly and anomaly_size[0] > 0:\n", + " sensor_df.loc[\n", + " start_anomaly : end_anomaly - 1, [\"temperature_c\", \"vibration_hz\"]\n", + " ] *= np.random.uniform(1.05, 1.25, size=anomaly_size)\n", + "\n", + " log_df = pd.DataFrame(log_data)\n", + " # Ensure timestamp column exists and sort\n", + " if \"timestamp\" in log_df.columns and not log_df.empty:\n", + " # Convert to UTC if not already, to ensure consistency before sorting\n", + " if log_df[\"timestamp\"].dt.tz is None:\n", + " log_df[\"timestamp\"] = log_df[\"timestamp\"].dt.tz_localize(\"UTC\")\n", + " else:\n", + " log_df[\"timestamp\"] = log_df[\"timestamp\"].dt.tz_convert(\"UTC\")\n", + " log_df = log_df.sort_values(\"timestamp\").reset_index(drop=True)\n", + " else:\n", + " print(\"Warning: Log data is empty or missing 'timestamp' column.\")\n", + " # Create an empty df with expected columns if needed\n", + " log_df = pd.DataFrame(\n", + " columns=[\"timestamp\", \"equipment_id\", \"event_type\", \"details\"]\n", + " )\n", + " log_df[\"timestamp\"] = pd.to_datetime(log_df[\"timestamp\"]).dt.tz_localize(\n", + " \"UTC\"\n", + " ) # Ensure dtype even if empty\n", + "\n", + " # Ensure sensor data timestamp is also UTC for consistent comparison later\n", + " if sensor_df[\"timestamp\"].dt.tz is None:\n", + " sensor_df[\"timestamp\"] = sensor_df[\"timestamp\"].dt.tz_localize(\"UTC\")\n", + " else:\n", + " sensor_df[\"timestamp\"] = sensor_df[\"timestamp\"].dt.tz_convert(\"UTC\")\n", + "\n", + " sensor_df.to_csv(filename, index=False)\n", + " log_df.to_csv(log_filename, index=False)\n", + " print(f\"Generated {len(sensor_df)} sensor records to '{filename}'.\")\n", + " print(f\"Generated {len(log_df)} log entries to '{log_filename}'.\")\n", + "\n", + " return sensor_df, log_df\n", + "\n", + "\n", + "# Load or generate data\n", + "sensor_data_df, log_data_df = generate_maintenance_data()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "9jbGD8gFRdQ7" + }, + "source": [ + "## Step 2: Prepare Tuning Data (JSONL)\n", + "\n", + "We convert the raw data into sequences and format them as JSON Lines, where each line represents a prompt (sensor data summary) and the expected completion (equipment status)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "w9pXnVJARdQ7" + }, + "outputs": [], + "source": [ + "def create_tuning_jsonl(\n", + " sensor_df: pd.DataFrame,\n", + " log_df: pd.DataFrame,\n", + " sequence_length: int,\n", + " failure_horizon_h: int,\n", + " warning_horizon_h: int,\n", + ") -> list[dict[str, Any]]:\n", + " \"\"\"Creates JSONL data for Gemini supervised tuning.\"\"\"\n", + " print(\"\\n--- Preparing JSONL Tuning Data ---\")\n", + " df = sensor_df.copy()\n", + " # Ensure log_df has timestamps before proceeding\n", + " if log_df.empty or \"timestamp\" not in log_df.columns:\n", + " print(\n", + " \"Warning: Log DataFrame is empty or missing 'timestamp'. Cannot determine failure times.\"\n", + " )\n", + " failure_times = pd.Series(dtype=\"datetime64[ns, UTC]\") # Empty series\n", + " else:\n", + " # Ensure log_df timestamps are UTC\n", + " if log_df[\"timestamp\"].dt.tz is None:\n", + " log_df[\"timestamp\"] = log_df[\"timestamp\"].dt.tz_localize(\"UTC\")\n", + " else:\n", + " log_df[\"timestamp\"] = log_df[\"timestamp\"].dt.tz_convert(\"UTC\")\n", + " failure_times = log_df[log_df[\"event_type\"] == \"Failure\"][\"timestamp\"]\n", + "\n", + " # Define Status based on proximity to failure\n", + " df[\"status\"] = \"Status: Normal\"\n", + " fail_horizon = pd.Timedelta(hours=failure_horizon_h)\n", + " warn_horizon = pd.Timedelta(hours=warning_horizon_h)\n", + "\n", + " # Ensure df timestamps are UTC\n", + " if df[\"timestamp\"].dt.tz is None:\n", + " df[\"timestamp\"] = df[\"timestamp\"].dt.tz_localize(\"UTC\")\n", + " else:\n", + " df[\"timestamp\"] = df[\"timestamp\"].dt.tz_convert(\"UTC\")\n", + "\n", + " for f_time in failure_times:\n", + " # Ensure f_time is timezone-aware (should be UTC from previous step)\n", + " if f_time.tzinfo is None:\n", + " f_time = f_time.tz_localize(\"UTC\")\n", + "\n", + " # Critical within failure horizon\n", + " crit_mask = (df[\"timestamp\"] >= f_time - fail_horizon) & (\n", + " df[\"timestamp\"] < f_time\n", + " )\n", + " df.loc[crit_mask, \"status\"] = \"Status: Critical - Failure imminent\"\n", + " # Warning within warning horizon (but not critical)\n", + " warn_mask = (df[\"timestamp\"] >= f_time - warn_horizon) & (\n", + " df[\"timestamp\"] < f_time - fail_horizon\n", + " )\n", + " df.loc[warn_mask, \"status\"] = \"Status: Warning - Elevated risk detected\"\n", + "\n", + " print(f\"Status distribution:\\n{df['status'].value_counts()}\")\n", + "\n", + " feature_columns = [\"temperature_c\", \"vibration_hz\", \"pressure_psi\"]\n", + "\n", + " jsonl_data = []\n", + " # Iterate through possible end points for sequences\n", + " for i in range(sequence_length, len(df)):\n", + " sequence_df = df.iloc[i - sequence_length : i]\n", + " # Check if the sequence is valid (e.g., no NaNs introduced by iloc edge cases)\n", + " if sequence_df.isnull().values.any() or sequence_df.empty:\n", + " continue\n", + "\n", + " target_status = df.iloc[i][\"status\"]\n", + " current_equipment_id = df.iloc[i][\"equipment_id\"] # Get ID for the prompt\n", + "\n", + " # Create a text prompt summarizing the sequence\n", + " prompt = f\"Equipment {current_equipment_id} sensor data for the last {sequence_length} hours:\\n\"\n", + " for col in feature_columns:\n", + " mean_val = sequence_df[col].mean()\n", + " std_val = sequence_df[col].std()\n", + " # Calculate trend more robustly\n", + " diff_mean = sequence_df[col].diff().mean()\n", + " trend = (\n", + " \"stable\"\n", + " if pd.isna(diff_mean) or abs(diff_mean) < 0.1\n", + " else (\"rising\" if diff_mean > 0 else \"falling\")\n", + " )\n", + " prompt += f\"- {col}: Average {mean_val:.1f}, StdDev {std_val:.1f}, Trend {trend}\\n\"\n", + " prompt += \"\\nClassify the equipment status based on this data (Normal, Warning, or Critical).\"\n", + "\n", + " # Format according to Gemini tuning requirements\n", + " instance = {\n", + " \"contents\": [\n", + " {\"role\": \"user\", \"parts\": [{\"text\": prompt}]},\n", + " {\"role\": \"model\", \"parts\": [{\"text\": target_status}]},\n", + " ]\n", + " }\n", + " jsonl_data.append(instance)\n", + "\n", + " print(f\"Generated {len(jsonl_data)} JSONL instances.\")\n", + " return jsonl_data\n", + "\n", + "\n", + "# Create JSONL data\n", + "tuning_data_jsonl = create_tuning_jsonl(\n", + " sensor_data_df,\n", + " log_data_df,\n", + " sequence_length=SEQUENCE_LENGTH,\n", + " failure_horizon_h=FAILURE_PREDICTION_HORIZON_HOURS,\n", + " warning_horizon_h=WARNING_HORIZON_HOURS,\n", + ")\n", + "\n", + "# Shuffle and Split data\n", + "if tuning_data_jsonl:\n", + " random.shuffle(tuning_data_jsonl)\n", + " split_idx_val = int(len(tuning_data_jsonl) * 0.8) # 80% train\n", + " split_idx_test = int(len(tuning_data_jsonl) * 0.9) # 10% validation, 10% test\n", + "\n", + " train_split = tuning_data_jsonl[:split_idx_val]\n", + " validation_split = tuning_data_jsonl[split_idx_val:split_idx_test]\n", + " test_split = tuning_data_jsonl[split_idx_test:]\n", + "\n", + " print(\n", + " f\"Split sizes: Train={len(train_split)}, Validation={len(validation_split)}, Test={len(test_split)}\"\n", + " )\n", + "\n", + " # Display a sample\n", + " print(\"\\n--- Sample JSONL Instance ---\")\n", + " print(json.dumps(train_split[0], indent=2))\n", + "else:\n", + " print(\n", + " \"\\nWarning: No tuning data generated, possibly due to short data sequence or lack of failure events.\"\n", + " )\n", + " # Initialize splits as empty lists to prevent errors later\n", + " train_split, validation_split, test_split = [], [], []" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "-OARmnqbRdQ7" + }, + "source": [ + "## Step 3: Upload Tuning Data to GCS\n", + "\n", + "The fine-tuning service reads data directly from Google Cloud Storage." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "sb9OuMrjRdQ7" + }, + "outputs": [], + "source": [ + "import google.auth\n", + "\n", + "def save_jsonl_to_gcs(instances: list[dict[str, Any]], gcs_uri: str):\n", + " \"\"\"Saves a list of dictionaries as a JSONL file to GCS using Pandas.\"\"\"\n", + " if not instances:\n", + " print(f\"No instances to upload to {gcs_uri}. Skipping upload.\")\n", + " return\n", + "\n", + " print(f\"Uploading {len(instances)} instances to {gcs_uri}...\")\n", + "\n", + " try:\n", + " # Get the application default credentials\n", + " credentials, _ = google.auth.default()\n", + "\n", + " # Convert list of dicts to DataFrame\n", + " df = pd.DataFrame(instances)\n", + "\n", + " # Save DataFrame to GCS as JSONL\n", + " # We MUST pass the 'token' (credentials) to authenticate the request\n", + " storage_options = {\"project\": PROJECT_ID, \"token\": credentials}\n", + "\n", + " df.to_json(\n", + " gcs_uri, orient=\"records\", lines=True, storage_options=storage_options\n", + " )\n", + "\n", + " print(\"Upload complete.\")\n", + " except Exception as e:\n", + " print(f\"ERROR during GCS upload to {gcs_uri}: {e}\")\n", + " print(\n", + " \"Please ensure your GCS bucket is accessible and pandas has GCS permissions (installed via gcsfs).\"\n", + " )\n", + "\n", + "\n", + "# Save splits to GCS\n", + "save_jsonl_to_gcs(train_split, TRAIN_JSONL_GCS_URI)\n", + "save_jsonl_to_gcs(validation_split, VALIDATION_JSONL_GCS_URI)\n", + "save_jsonl_to_gcs(\n", + " test_split, TEST_JSONL_GCS_URI\n", + ") # Save test split for later evaluation" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "bPa8ylyURdQ8" + }, + "source": [ + "## Step 4: Launch Fine-tuning Job\n", + "\n", + "We use the `google-genai` client **configured for Vertex AI** (`vertex_client`) to start the supervised tuning job, as fine-tuning management is a Vertex AI feature." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "HF9z_1k3RdQ8" + }, + "outputs": [], + "source": [ + "TUNING_JOB_NAME = None # Initialize\n", + "if not train_split or not validation_split:\n", + " print(\"Skipping fine-tuning job launch as training or validation data is empty.\")\n", + "else:\n", + " print(f\"Starting supervised fine-tuning job for model: {BASE_MODEL_ID}\")\n", + " print(f\"Tuned model display name: {TUNED_MODEL_DISPLAY_NAME}\")\n", + "\n", + " training_dataset = {\n", + " \"gcs_uri\": TRAIN_JSONL_GCS_URI,\n", + " }\n", + "\n", + " validation_dataset = genai_types.TuningValidationDataset(\n", + " gcs_uri=VALIDATION_JSONL_GCS_URI\n", + " )\n", + "\n", + " try:\n", + " # Use the vertex_client configured specifically for Vertex AI operations\n", + " sft_tuning_job = vertex_client.tunings.tune(\n", + " base_model=BASE_MODEL_ID,\n", + " training_dataset=training_dataset,\n", + " config=genai_types.CreateTuningJobConfig(\n", + " adapter_size=\"ADAPTER_SIZE_FOUR\", # Smaller adapter for faster tuning\n", + " epoch_count=3, # Keep low for demonstration\n", + " tuned_model_display_name=TUNED_MODEL_DISPLAY_NAME,\n", + " validation_dataset=validation_dataset,\n", + " ),\n", + " )\n", + " print(\"\\nTuning job created:\")\n", + " print(sft_tuning_job)\n", + " TUNING_JOB_NAME = sft_tuning_job.name # Save for monitoring\n", + "\n", + " except Exception as e:\n", + " print(f\"ERROR starting tuning job: {e}\")\n", + " # Attempt to list existing jobs with the same display name in case of interruption\n", + " try:\n", + " print(\n", + " f\"Checking for existing tuning jobs named '{TUNED_MODEL_DISPLAY_NAME}'...\"\n", + " )\n", + " existing_jobs = vertex_client.tunings.list(\n", + " page_size=100\n", + " ) # List might need pagination for many jobs\n", + " for job in existing_jobs:\n", + " # Check if config exists and has the attribute\n", + " job_config = getattr(job, \"config\", None)\n", + " if (\n", + " job_config\n", + " and getattr(job_config, \"tuned_model_display_name\", None)\n", + " == TUNED_MODEL_DISPLAY_NAME\n", + " ):\n", + " print(f\"Found existing job: {job.name} with state {job.state}\")\n", + " TUNING_JOB_NAME = job.name # Use the existing job name\n", + " break\n", + " except Exception as list_e:\n", + " print(f\"Could not list existing tuning jobs: {list_e}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "VV8iimM8RdQ8" + }, + "source": [ + "**Note:** Fine-tuning can take a significant amount of time (potentially 30 minutes to several hours depending on the dataset size, base model, and adapter size)." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "Vs9s8uLRRdQ8" + }, + "source": [ + "## Step 5: Monitor Job" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "9mVFYfL2RdQ8" + }, + "outputs": [], + "source": [ + "TUNED_MODEL_ENDPOINT = None # Initialize\n", + "if TUNING_JOB_NAME:\n", + " print(f\"Monitoring tuning job: {TUNING_JOB_NAME}\")\n", + " running_states = {\n", + " genai_types.JobState.JOB_STATE_PENDING,\n", + " genai_types.JobState.JOB_STATE_RUNNING,\n", + " }\n", + "\n", + " tuning_job = vertex_client.tunings.get(name=TUNING_JOB_NAME)\n", + "\n", + " while tuning_job.state in running_states:\n", + " # Extract the simple state name for printing\n", + " current_state_name = str(tuning_job.state).split(\".\")[-1]\n", + " print(f\" Current state: {current_state_name}...\")\n", + " time.sleep(60) # Check every minute\n", + " # Poll the job status using the vertex_client\n", + " try:\n", + " tuning_job = vertex_client.tunings.get(name=TUNING_JOB_NAME)\n", + " except Exception as e:\n", + " print(\n", + " f\"Error polling tuning job status: {e}. Assuming job might still be running.\"\n", + " )\n", + " # Optional: Add retry logic or break after several failures\n", + " time.sleep(120) # Wait longer if polling fails\n", + "\n", + " final_state_name = str(tuning_job.state).split(\".\")[-1]\n", + " print(f\"\\nTuning job finished with state: {final_state_name}\")\n", + "\n", + " if tuning_job.state == genai_types.JobState.JOB_STATE_SUCCEEDED:\n", + " # Check if tuned_model attribute exists and has endpoint\n", + " if (\n", + " hasattr(tuning_job, \"tuned_model\")\n", + " and tuning_job.tuned_model\n", + " and hasattr(tuning_job.tuned_model, \"endpoint\")\n", + " ):\n", + " TUNED_MODEL_ENDPOINT = tuning_job.tuned_model.endpoint\n", + " print(f\"Tuned model endpoint ready: {TUNED_MODEL_ENDPOINT}\")\n", + " else:\n", + " print(\n", + " \"Tuning job succeeded, but tuned model endpoint information is missing.\"\n", + " )\n", + " print(\"Please check the job details in the Google Cloud Console.\")\n", + " else:\n", + " print(\"Tuning job did not succeed.\")\n", + " # Check for error attribute before printing\n", + " job_error = getattr(tuning_job, \"error\", None)\n", + " if job_error:\n", + " print(f\"Error details: {job_error}\")\n", + " else:\n", + " print(\"No specific error details available.\")\n", + "else:\n", + " print(\n", + " \"Skipping monitoring as tuning job name is not set (creation might have failed or data was empty).\"\n", + " )" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "JeiZwXsMRdQ8" + }, + "source": [ + "## Step 6: Evaluate Tuned Model (Qualitative)\n", + "\n", + "We take a sample from our test set (which the model hasn't seen during tuning) and compare the tuned model's prediction to the expected output." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "0fG27PjrRdQ8" + }, + "outputs": [], + "source": [ + "def evaluate_qualitatively(\n", + " tuned_endpoint: str, test_data: list[dict[str, Any]], num_samples: int = 3\n", + "):\n", + " \"\"\"Makes predictions with the tuned model and prints comparisons.\"\"\"\n", + " if not tuned_endpoint:\n", + " print(\"Tuned model endpoint not available. Skipping evaluation.\")\n", + " return\n", + "\n", + " if not test_data:\n", + " print(\"No test data available for evaluation.\")\n", + " return\n", + "\n", + " print(f\"\\n--- Qualitative Evaluation of Tuned Model ({tuned_endpoint}) ---\")\n", + "\n", + " # Select random samples from the test set\n", + " samples = random.sample(test_data, min(num_samples, len(test_data)))\n", + "\n", + " for i, sample in enumerate(samples):\n", + " print(f\"\\n--- Sample {i + 1} ---\")\n", + " # Ensure the sample structure is correct\n", + " try:\n", + " user_prompt = sample[\"contents\"][0][\"parts\"][0][\"text\"]\n", + " expected_output = sample[\"contents\"][1][\"parts\"][0][\"text\"]\n", + " except (KeyError, IndexError, TypeError) as e:\n", + " print(f\"Skipping sample due to unexpected format: {e}\")\n", + " continue\n", + "\n", + " print(f\"Input Prompt:\\n{user_prompt}\")\n", + " print(f\"\\nExpected Output: {expected_output}\")\n", + "\n", + " try:\n", + " # Prepare contents for prediction (only user part)\n", + " prediction_contents = [{\"role\": \"user\", \"parts\": [{\"text\": user_prompt}]}]\n", + "\n", + " # Use the vertex_client for predictions against the tuned endpoint\n", + " # Note: The 'model' argument takes the endpoint resource name string directly\n", + " response = vertex_client.models.generate_content(\n", + " model=tuned_endpoint,\n", + " contents=prediction_contents,\n", + " config={\n", + " \"temperature\": 0.1, # Low temperature for more deterministic output\n", + " \"max_output_tokens\": 50,\n", + " },\n", + " )\n", + " # Safely access predicted text\n", + " predicted_output = \"(No text generated)\"\n", + " if response and hasattr(response, \"text\"):\n", + " predicted_output = response.text.strip()\n", + " elif response and hasattr(response, \"candidates\") and response.candidates:\n", + " # Handle potential multi-candidate responses if safety filters trigger, etc.\n", + " first_candidate = response.candidates[0]\n", + " # Check finish reason before accessing content\n", + " finish_reason = getattr(first_candidate, \"finish_reason\", None)\n", + " if (\n", + " finish_reason == genai_types.FinishReason.STOP\n", + " and hasattr(first_candidate, \"content\")\n", + " and first_candidate.content.parts\n", + " ):\n", + " predicted_output = first_candidate.content.parts[0].text.strip()\n", + " else:\n", + " predicted_output = f\"(Generation stopped: {finish_reason})\"\n", + "\n", + " print(f\"Predicted Output: {predicted_output}\")\n", + "\n", + " # Simple comparison\n", + " if predicted_output == expected_output:\n", + " print(\"Result: MATCH\")\n", + " else:\n", + " print(\"Result: MISMATCH\")\n", + "\n", + " except Exception as e:\n", + " print(f\"ERROR during prediction for sample {i + 1}: {e}\")\n", + "\n", + "\n", + "# Run qualitative evaluation (only if tuning succeeded and test data exists)\n", + "evaluate_qualitatively(TUNED_MODEL_ENDPOINT, test_split)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "jW2wMC1yRdQ8" + }, + "source": [ + "## Step 7: Integrating Gemini for Reporting (Using Base Model)\n", + "\n", + "We can use a base Gemini model (accessed via Vertex AI) to summarize the fine-tuning job itself." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "WZ5N1MAQRdQ8" + }, + "outputs": [], + "source": [ + "def generate_tuning_summary_with_gemini(tuning_job_details: Any):\n", + " \"\"\"Generates a summary of the tuning job using the Gemini API.\"\"\"\n", + " print(\"\\n--- Generating Tuning Job Summary with Gemini ---\")\n", + "\n", + " if not tuning_job_details:\n", + " print(\"No tuning job details provided. Skipping summary.\")\n", + " return\n", + "\n", + " # We will use the Vertex AI client, which is already initialized.\n", + " model_name_for_vertex_ai = \"gemini-2.5-flash\" # Use a standard model for reporting\n", + " reporting_client = None\n", + "\n", + " try:\n", + " # This uses the high-level vertexai SDK for base model generation\n", + " # Correctly import GenerativeModel from vertexai.preview.generative_models\n", + " from vertexai.preview.generative_models import GenerativeModel\n", + "\n", + " reporting_client = GenerativeModel(model_name_for_vertex_ai)\n", + " print(f\"Using Vertex AI model ({model_name_for_vertex_ai}) for reporting.\")\n", + " except Exception as e:\n", + " print(\n", + " f\"Failed to initialize Vertex AI client for reporting with {model_name_for_vertex_ai}: {e}\"\n", + " )\n", + " print(\"Skipping summary generation.\")\n", + " return\n", + "\n", + " try:\n", + " # Extract relevant details safely\n", + " job_name = getattr(tuning_job_details, \"name\", \"N/A\")\n", + " job_state_enum = getattr(\n", + " tuning_job_details, \"state\", genai_types.JobState.JOB_STATE_UNSPECIFIED\n", + " ) # Default to unspecified\n", + " job_state = str(job_state_enum).split(\".\")[\n", + " -1\n", + " ] # Get 'SUCCEEDED', 'FAILED', etc.\n", + " base_model = getattr(tuning_job_details, \"base_model\", \"N/A\")\n", + " tuned_model_obj = getattr(tuning_job_details, \"tuned_model\", None)\n", + " tuned_endpoint = (\n", + " getattr(tuned_model_obj, \"endpoint\", \"N/A\") if tuned_model_obj else \"N/A\"\n", + " )\n", + " error_obj = getattr(tuning_job_details, \"error\", None)\n", + " error_message = str(error_obj) if error_obj else \"None\"\n", + " config_obj = getattr(tuning_job_details, \"config\", None)\n", + " display_name = (\n", + " getattr(config_obj, \"tuned_model_display_name\", \"N/A\")\n", + " if config_obj\n", + " else \"N/A\"\n", + " )\n", + "\n", + " prompt = f\"\"\"Generate a brief status report for a Gemini model fine-tuning job.\n", + " Job Name: {job_name}\n", + " Base Model: {base_model}\n", + " Tuned Model Display Name: {display_name}\n", + " Final Status: {job_state}\n", + " Tuned Model Endpoint: {tuned_endpoint}\n", + " Error (if any): {error_message}\n", + "\n", + " Summarize the outcome of this tuning job in 1-2 sentences.\"\"\"\n", + "\n", + " print(\"\\nSending request to Gemini...\")\n", + " # Use the selected reporting_client (Vertex AI based)\n", + " response = reporting_client.generate_content(prompt)\n", + "\n", + " print(\"\\n--- Gemini Tuning Job Summary ---\")\n", + " # Handle potential response variations\n", + " response_text = \"(No text content found in response)\"\n", + " try:\n", + " # Standard access\n", + " if hasattr(response, \"text\"):\n", + " response_text = response.text\n", + " # Access through candidates (common for safety filtering etc.)\n", + " elif hasattr(response, \"candidates\") and response.candidates:\n", + " first_candidate = response.candidates[0]\n", + " # Check finish reason before accessing content\n", + " finish_reason = getattr(first_candidate, \"finish_reason\", None)\n", + " # Check if STOPPED or MAX_TOKENS (can still have partial content)\n", + " if (\n", + " finish_reason\n", + " in [\n", + " genai_types.FinishReason.STOP,\n", + " genai_types.FinishReason.MAX_TOKENS,\n", + " ]\n", + " and hasattr(first_candidate, \"content\")\n", + " and first_candidate.content.parts\n", + " ):\n", + " response_text = first_candidate.content.parts[0].text\n", + " else:\n", + " # Include finish reason if generation didn't stop normally\n", + " response_text = f\"(Generation stopped: {finish_reason})\"\n", + " except Exception as resp_e:\n", + " print(f\"Error extracting text from response: {resp_e}\")\n", + " print(f\"Raw Response: {response}\")\n", + "\n", + " print(response_text)\n", + " print(\"---------------------------------\")\n", + "\n", + " except Exception as e:\n", + " print(f\"\\nERROR: Failed to generate Gemini summary: {e}\")\n", + " if (\n", + " \"permission denied\" in str(e).lower()\n", + " or \"consumer project\" in str(e).lower()\n", + " ):\n", + " print(\n", + " \"Please ensure the Vertex AI API is enabled in your project and the runtime environment has the correct permissions.\"\n", + " )\n", + " else:\n", + " print(\n", + " \"Please check your Vertex AI setup, model name, and network connection.\"\n", + " )\n", + "\n", + "\n", + "# Get the final job details again using the vertex_client (which manages tuning)\n", + "final_tuning_job = None\n", + "if TUNING_JOB_NAME:\n", + " try:\n", + " # Use vertex_client to get the job status\n", + " final_tuning_job = vertex_client.tunings.get(name=TUNING_JOB_NAME)\n", + " except Exception as e:\n", + " print(f\"Error retrieving final tuning job details: {e}\")\n", + "\n", + "# Generate the summary using the Vertex Gemini client\n", + "generate_tuning_summary_with_gemini(final_tuning_job)" + ] + } + ], + "metadata": { + "colab": { + "name": "sft_gemini_predictive_maintenance.ipynb", + "toc_visible": true + }, + "kernelspec": { + "display_name": "Python 3", + "name": "python3" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} diff --git a/gemini/tuning/sft_gemini_qa.ipynb b/gemini/tuning/sft_gemini_qa.ipynb new file mode 100644 index 0000000..b2056af --- /dev/null +++ b/gemini/tuning/sft_gemini_qa.ipynb @@ -0,0 +1,1310 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "ur8xi4C7S06n" + }, + "outputs": [], + "source": [ + "# Copyright 2024 Google LLC\n", + "#\n", + "# Licensed under the Apache License, Version 2.0 (the \"License\");\n", + "# you may not use this file except in compliance with the License.\n", + "# You may obtain a copy of the License at\n", + "#\n", + "# https://www.apache.org/licenses/LICENSE-2.0\n", + "#\n", + "# Unless required by applicable law or agreed to in writing, software\n", + "# distributed under the License is distributed on an \"AS IS\" BASIS,\n", + "# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n", + "# See the License for the specific language governing permissions and\n", + "# limitations under the License." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "ojoyvz6mH1Hv" + }, + "source": [ + "# Supervised fine-tuning with Gemini 2.0 Flash for Q&A using the Google Gen AI SDK\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
    \n", + " \n", + " \"Google
    Open in Colab\n", + "
    \n", + "
    \n", + " \n", + " \"Google
    Open in Colab Enterprise\n", + "
    \n", + "
    \n", + " \n", + " \"Vertex
    Open in Workbench\n", + "
    \n", + "
    \n", + " \n", + " \"GitHub
    View on GitHub\n", + "
    \n", + "
    \n", + " \n", + " \"Google
    Open in Cloud Skills Boost\n", + "
    \n", + "
    \n", + "\n", + "
    \n", + "\n", + "Share to:\n", + "\n", + "\n", + " \"LinkedIn\n", + "\n", + "\n", + "\n", + " \"Bluesky\n", + "\n", + "\n", + "\n", + " \"X\n", + "\n", + "\n", + "\n", + " \"Reddit\n", + "\n", + "\n", + "\n", + " \"Facebook\n", + " " + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "84f0f73a0f76" + }, + "source": [ + "| Author(s) |\n", + "| --- |\n", + "| [Erwin Huizenga](https://github.com/erwinh85) |" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "JAPoU8Sm5E6e" + }, + "source": [ + "## Overview\n", + "\n", + "**Gemini** is a family of generative AI models developed by Google DeepMind designed for multimodal use cases. The Gemini API gives you access to the various Gemini models, such as Gemini 2.0 and Gemini 2.0.\n", + "This notebook demonstrates fine-tuning the Gemini 2.0 using the Vertex AI Supervised Tuning feature. Supervised Tuning allows you to use your own labeled training data to further refine the base model's capabilities toward your specific tasks.\n", + "Supervised Tuning uses labeled examples to tune a model. Each example demonstrates the output you want from your text model during inference.\n", + "First, ensure your training data is of high quality, well-labeled, and directly relevant to the target task. This is crucial as low-quality data can adversely affect the performance and introduce bias in the fine-tuned model.\n", + "Training: Experiment with different configurations to optimize the model's performance on the target task.\n", + "Evaluation:\n", + "Metric: Choose appropriate evaluation metrics that accurately reflect the success of the fine-tuned model for your specific task\n", + "Evaluation Set: Use a separate set of data to evaluate the model's performance\n", + "\n", + "\n", + "Refer to public [documentation](https://cloud.google.com/vertex-ai/generative-ai/docs/models/gemini-supervised-tuning) for more details.\n", + "\n", + "
    \n", + "\n", + "Before running this notebook, ensure you have:\n", + "\n", + "- A Google Cloud project: Provide your project ID in the `PROJECT_ID` variable.\n", + "\n", + "- Authenticated your Colab environment: Run the authentication code block at the beginning.\n", + "\n", + "- Prepared training data (Test with your own data or use the one in the notebook): Data should be formatted in JSONL with prompts and corresponding completions." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "f7SS5pzuIA-1" + }, + "source": [ + "### Costs\n", + "\n", + "This tutorial uses billable components of Google Cloud:\n", + "\n", + "* Vertex AI\n", + "* Cloud Storage\n", + "\n", + "Learn about [Vertex AI\n", + "pricing](https://cloud.google.com/vertex-ai/pricing), [Cloud Storage\n", + "pricing](https://cloud.google.com/storage/pricing), and use the [Pricing\n", + "Calculator](https://cloud.google.com/products/calculator/)\n", + "to generate a cost estimate based on your projected usage.\n", + "\n", + "To estimate the cost of token please have a look at this [notebook](https://github.com/GoogleCloudPlatform/generative-ai/blob/main/gemini/tuning/vertexai_supervised_tuning_token_count_and_cost_estimation.ipynb)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "61RBz8LLbxCR" + }, + "source": [ + "## Get started" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "No17Cw5hgx12" + }, + "source": [ + "### Install the Google Gen AI SDK and other required packages\n", + "\n", + "The new Google Gen AI SDK provides a unified interface to Gemini through both the Gemini Developer API and the Gemini API on Vertex AI. With a few exceptions, code that runs on one platform will run on both. This means that you can prototype an application using the Developer API and then migrate the application to Vertex AI without rewriting your code.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "tFy3H3aPgx12" + }, + "outputs": [], + "source": [ + "%pip install --upgrade --quiet google-cloud-aiplatform google-genai" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "R5Xep4W9lq-Z" + }, + "source": [ + "### Restart runtime\n", + "\n", + "To use the newly installed packages in this Jupyter runtime, you must restart the runtime. You can do this by running the cell below, which restarts the current kernel.\n", + "\n", + "The restart might take a minute or longer. After it's restarted, continue to the next step." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "XRvKdaPDTznN" + }, + "outputs": [], + "source": [ + "import IPython\n", + "\n", + "app = IPython.Application.instance()\n", + "app.kernel.do_shutdown(True)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "SbmM4z7FOBpM" + }, + "source": [ + "
    \n", + "⚠ The kernel is going to restart. Wait until it's finished before continuing to the next step. ⚠\n", + "
    \n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "dmWOrTJ3gx13" + }, + "source": [ + "### Authenticate your notebook environment (Colab only)\n", + "\n", + "If you're running this notebook on Google Colab, run the cell below to authenticate your environment." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "NyKGtVQjgx13" + }, + "outputs": [], + "source": [ + "import sys\n", + "\n", + "if \"google.colab\" in sys.modules:\n", + " from google.colab import auth\n", + "\n", + " auth.authenticate_user()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "DF4l8DTdWgPY" + }, + "source": [ + "### Set the Google Cloud project information and initialize the Google Gen AI SDK\n", + "\n", + "To get started using Vertex AI, you must have an existing Google Cloud project and [enable the Vertex AI API](https://console.cloud.google.com/flows/enableapi?apiid=aiplatform.googleapis.com).\n", + "\n", + "Learn more about [setting up a project and a development environment](https://cloud.google.com/vertex-ai/docs/start/cloud-environment)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "code", + "id": "Nqwi-5ufWp_B" + }, + "outputs": [], + "source": [ + "# Use the environment variable if the user doesn't provide Project ID.\n", + "import os\n", + "\n", + "from google import genai\n", + "from google.genai import types\n", + "\n", + "PROJECT_ID = \"[your-project-id]\" # @param {type:\"string\", isTemplate: true}\n", + "if PROJECT_ID == \"[your-project-id]\":\n", + " PROJECT_ID = str(os.environ.get(\"GOOGLE_CLOUD_PROJECT\"))\n", + "\n", + "LOCATION = os.environ.get(\"GOOGLE_CLOUD_REGION\", \"us-central1\")\n", + "\n", + "client = genai.Client(vertexai=True, project=PROJECT_ID, location=LOCATION)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "5303c05f7aa6" + }, + "source": [ + "### Import libraries" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "6fc324893334" + }, + "outputs": [], + "source": [ + "from collections import Counter\n", + "import json\n", + "import random\n", + "\n", + "# Vertex AI SDK\n", + "from google.cloud import aiplatform\n", + "from google.cloud.aiplatform.metadata import context\n", + "from google.cloud.aiplatform.metadata import utils as metadata_utils\n", + "import numpy as np\n", + "import pandas as pd\n", + "import plotly.graph_objects as go\n", + "from plotly.subplots import make_subplots\n", + "import vertexai\n", + "\n", + "vertexai.init(project=PROJECT_ID, location=LOCATION)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "6bBZa2I-c-x8" + }, + "source": [ + "### Data\n", + "\n", + "#### SQuAD dataset\n", + "Stanford Question Answering Dataset (SQuAD) is a reading comprehension dataset, consisting of questions posed by crowdworkers on a set of Wikipedia articles, where the answer to every question is a segment of text, or span, from the corresponding reading passage, or the question might be unanswerable.\n", + "\n", + "You can find more information on the SQuAD [github page](https://rajpurkar.github.io/SQuAD-explorer/)\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "MllVfoJfOkDQ" + }, + "source": [ + "```bibtex\n", + "@inproceedings{rajpurkar-etal-2016-squad,\n", + " title = \"{SQ}u{AD}: 100,000+ Questions for Machine Comprehension of Text\",\n", + " author = \"Rajpurkar, Pranav and\n", + " Zhang, Jian and\n", + " Lopyrev, Konstantin and\n", + " Liang, Percy\",\n", + " editor = \"Su, Jian and\n", + " Duh, Kevin and\n", + " Carreras, Xavier\",\n", + " booktitle = \"Proceedings of the 2016 Conference on Empirical Methods in Natural Language Processing\",\n", + " month = nov,\n", + " year = \"2016\",\n", + " address = \"Austin, Texas\",\n", + " publisher = \"Association for Computational Linguistics\",\n", + " url = \"https://aclanthology.org/D16-1264\",\n", + " doi = \"10.18653/v1/D16-1264\",\n", + " pages = \"2383--2392\",\n", + " eprint={1606.05250},\n", + " archivePrefix={arXiv},\n", + " primaryClass={cs.CL},\n", + "}\n", + "```" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "KhebDJjRKePL" + }, + "source": [ + "First update the `BUCKET_NAME` parameter below. You can either use an existing bucket or create a new one." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "lit30Cktbfvo" + }, + "outputs": [], + "source": [ + "# Provide a bucket name\n", + "BUCKET_NAME = \"[your-bucket-name]\" # @param {type:\"string\"}\n", + "BUCKET_URI = f\"gs://{BUCKET_NAME}\"\n", + "print(BUCKET_URI)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "ed-G-9cyKmPY" + }, + "source": [ + "Only run the code below if you want to create a new Google Cloud Storage bucket." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "0UJ8S9YFA1pZ" + }, + "outputs": [], + "source": [ + "# ! gsutil mb -l {LOCATION} -p {PROJECT_ID} {BUCKET_URI}" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "izjwF63tLLEq" + }, + "source": [ + "Next you will copy the data into your bucket." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "wjvcxx_sA3xP" + }, + "outputs": [], + "source": [ + "!gsutil cp gs://github-repo/generative-ai/gemini/tuning/qa/squad_test.csv .\n", + "!gsutil cp gs://github-repo/generative-ai/gemini/tuning/qa/squad_train.csv .\n", + "!gsutil cp gs://github-repo/generative-ai/gemini/tuning/qa/squad_validation.csv ." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "3F10LuZeL3kt" + }, + "source": [ + "### Baseline\n", + "\n", + "Next you will prepare some data that you will use to establish a baseline. This means evaluating the out of the box default model on a representative sample of your dataset before any fine-tuning. A baseline allows you to quantify the improvements achieved through fine-tuning." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "LkOmXpegA8CW" + }, + "outputs": [], + "source": [ + "test_df = pd.read_csv(\"squad_test.csv\")\n", + "test_df.head(2)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "Px6HUVKn8ErZ" + }, + "source": [ + "First you need to prepare some data to evaluate the out of the box model and set a baseline. In this case, we will lower the text and remove extra whitespace, but preserve newlines." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "6HXBgGFZB3hx" + }, + "outputs": [], + "source": [ + "row_dataset = random.randint(0, 100) # lets take a random example from the dataset\n", + "\n", + "\n", + "def normalize_answer(s):\n", + " \"\"\"Lower text and remove extra whitespace, but preserve newlines.\"\"\"\n", + "\n", + " def white_space_fix(text):\n", + " return \" \".join(text.split()) # Splits by any whitespace, including \\n\n", + "\n", + " def lower(text):\n", + " return text.lower()\n", + "\n", + " return white_space_fix(lower(s))\n", + "\n", + "\n", + "test_df[\"answers\"] = test_df[\"answers\"].apply(normalize_answer)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "mrLxcVVcMsNO" + }, + "source": [ + "You want to make sure that you test data looks the same as your training data to prevent training / serving skew. We will add a system instruction to the dataset:\n", + "\n", + "- `SystemInstruct`: System instructions are a set of instructions that the model processes before it processes prompts. We recommend that you use system instructions to tell the model how you want it to behave and respond to prompts.\n", + "- You will also combine the `context` and `question`. Both will be send to the model to generate a response." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "jdKUBaA4ZOFg" + }, + "outputs": [], + "source": [ + "few_shot_examples = test_df.sample(3)\n", + "# Get the indices of the sampled rows\n", + "dropped_indices = few_shot_examples.index\n", + "# Remove the sampled rows from the original DataFrame\n", + "test_df = test_df.drop(dropped_indices)\n", + "\n", + "few_shot_prompt = \"\"\n", + "for _, row in few_shot_examples.iterrows():\n", + " few_shot_prompt += (\n", + " f\"Context: {row.context}\\nQuestion: {row.question}\\nAnswer: {row.answers}\\n\\n\"\n", + " )\n", + "\n", + "print(few_shot_prompt)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "-uJSrdU3aDi9" + }, + "outputs": [], + "source": [ + "# Incorporate few-shot examples into the system instruction\n", + "systemInstruct = f\"\"\"Answer the question with a concise extract from the given context. Do not add any additional information, capital letters (only for names) or a punctuation mark in the end.\\n\\n\n", + "Here are some examples: \\n\\n\n", + "{few_shot_prompt}\"\"\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "N_u3VzUMsyqj" + }, + "outputs": [], + "source": [ + "# combine the systeminstruct + context + question into one column. This will be your input prompt.\n", + "test_df[\"systemInstruct\"] = systemInstruct\n", + "\n", + "test_df[\"input_question\"] = (\n", + " \"\\n\\n **Below the question with context that you need to answer**\"\n", + " + \"\\n Context: \"\n", + " + test_df[\"context\"]\n", + " + \"\\n Question: \"\n", + " + test_df[\"question\"]\n", + ")\n", + "\n", + "test_systemInstruct = test_df[\"systemInstruct\"].iloc[row_dataset]\n", + "print(test_systemInstruct)\n", + "test_question = test_df[\"input_question\"].iloc[row_dataset]\n", + "print(test_question)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "FSxYYqMGWrmj" + }, + "source": [ + "Next, set the model that you will use. In this example you will use `\"gemini-2.0-flash-001\"`, a multimodal model that is designed for high-volume, cost-effective applications, and which delivers speed and efficiency to build fast, lower-cost applications that don't compromise on quality.\n", + "\n", + "For the latest Gemini models and versions, please have a look at our [documentation](https://cloud.google.com/vertex-ai/generative-ai/docs/learn/models).\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "t-5X4goiqqBQ" + }, + "outputs": [], + "source": [ + "base_model = \"gemini-2.0-flash-001\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "cfZM3RQ3OJ_t" + }, + "outputs": [], + "source": [ + "y_true = test_df[\"answers\"].values\n", + "y_pred_question = test_df[\"input_question\"].values\n", + "\n", + "# Check two pairs of question and answer.\n", + "for i in range(2): # Loop through the first two indices\n", + " print(f\"Pair {i+1}:\")\n", + " print(f\" True Answer: {y_true[i]}\")\n", + " print(f\" Predicted Question: {y_pred_question[i]}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "wyscyIenW4WZ" + }, + "source": [ + "Next lets take a question and get a prediction from Gemini that we can compare to the actual answer." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "udTxzY8mpGYf" + }, + "outputs": [], + "source": [ + "def get_predictions(question: str, model_version: str) -> str:\n", + "\n", + " prompt = question\n", + " base_model = model_version\n", + "\n", + " response = client.models.generate_content(\n", + " model=base_model,\n", + " contents=prompt,\n", + " config={\n", + " \"system_instruction\": systemInstruct,\n", + " \"temperature\": 0.3,\n", + " },\n", + " )\n", + "\n", + " return response.text" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "PFvwmGll3MIv" + }, + "outputs": [], + "source": [ + "test_answer = test_df[\"answers\"].iloc[row_dataset]\n", + "response = get_predictions(test_question, base_model)\n", + "\n", + "print(f\"Gemini response: {response}\")\n", + "print(f\"Actual answer: {test_answer}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "OGRJTHKrdujw" + }, + "source": [ + "Sometimes you might get an answer from Gemini is more lengthy. However, answers in the SQuAD dataset are typically concise and clear.\n", + "\n", + "Fine-tuning is a great way to control the type of output your use case requires. In this instance, you would want the model to provide short, clear answers." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "oQEEaRZZFgCD" + }, + "outputs": [], + "source": [ + "# Apply the get_prediction() function to the 'question_column'\n", + "test_df[\"predicted_answer\"] = test_df[\"input_question\"].apply(get_predictions)\n", + "test_df.head(2)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "T9ZnoFRm-uUZ" + }, + "source": [ + "You also need to make sure that the predicted answer is in the same format." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "B-kcXWYWHM3n" + }, + "outputs": [], + "source": [ + "test_df[\"predicted_answer\"] = test_df[\"predicted_answer\"].apply(normalize_answer)\n", + "test_df.head(4)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "hvi7m8pKE8WB" + }, + "source": [ + "Next, let's establish a baseline using evaluation metrics.\n", + "\n", + "Evaluating the performance of a Question Answering (QA) system requires specific metrics. Two commonly used metrics are Exact Match (EM) and F1 score.\n", + "\n", + "EM is a strict measure that only considers an answer correct if it perfectly matches the ground truth, even down to the punctuation. It's a binary metric - either 1 for a perfect match or 0 otherwise. This makes it sensitive to minor variations in phrasing.\n", + "\n", + "F1 score is more flexible. It considers the overlap between the predicted answer and the true answer in terms of individual words or tokens. It calculates the harmonic mean of precision (proportion of correctly predicted words out of all predicted words) and recall (proportion of correctly predicted words out of all true answer words). This allows for partial credit and is less sensitive to minor wording differences.\n", + "\n", + "In practice, EM is useful when exact wording is crucial, while F1 is more suitable when evaluating the overall understanding and semantic accuracy of the QA system. Often, both metrics are used together to provide a comprehensive evaluation." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "6AI57wAgIB6A" + }, + "outputs": [], + "source": [ + "def f1_score_squad(prediction, ground_truth):\n", + " prediction_tokens = normalize_answer(prediction).split()\n", + " ground_truth_tokens = normalize_answer(ground_truth).split()\n", + " common = Counter(prediction_tokens) & Counter(ground_truth_tokens)\n", + " num_same = sum(common.values())\n", + " if num_same == 0:\n", + " return 0\n", + " precision = 1.0 * num_same / len(prediction_tokens)\n", + " recall = 1.0 * num_same / len(ground_truth_tokens)\n", + " f1 = (2 * precision * recall) / (precision + recall)\n", + " return f1\n", + "\n", + "\n", + "def exact_match_score(prediction, ground_truth):\n", + " return normalize_answer(prediction) == normalize_answer(ground_truth)\n", + "\n", + "\n", + "def calculate_em_and_f1(y_true, y_pred):\n", + " \"\"\"Calculates EM and F1 scores for DataFrame columns.\"\"\"\n", + "\n", + " # Ensure inputs are Series\n", + " if not isinstance(y_true, pd.Series):\n", + " y_true = pd.Series(y_true)\n", + " if not isinstance(y_pred, pd.Series):\n", + " y_pred = pd.Series(y_pred)\n", + "\n", + " em = np.mean(y_true.combine(y_pred, exact_match_score))\n", + " f1 = np.mean(y_true.combine(y_pred, f1_score_squad))\n", + "\n", + " # # Print non-matching pairs (using index for clarity)\n", + " # for i, (t, p) in enumerate(zip(y_true, y_pred)):\n", + " # if not exact_match_score(p, t):\n", + " # print(f\"No EM Match at index {i}:\\nTrue: {t}\\nPred: {p}\\n\")\n", + "\n", + " return em, f1" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "iuHUevIX_fMn" + }, + "outputs": [], + "source": [ + "em, f1 = calculate_em_and_f1(test_df[\"answers\"], test_df[\"predicted_answer\"])\n", + "print(f\"EM score: {em}\")\n", + "print(f\"F1 score: {f1}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "22DfexbNfUHm" + }, + "source": [ + "### Prepare the data for fine-tuning\n", + "\n", + "To optimize the supervised fine-tuning process for a foundation model, ensure your dataset includes examples that reflect the desired task. Each record in the dataset pairs an input text (or prompt) with its corresponding expected output. This supervised tuning approach uses the dataset to effectively teach the model the specific behavior or task you need it to perform, by providing numerous illustrative examples.\n", + "\n", + "The size of your dataset will vary depending on the complexity of the task, but as a general rule, the more examples you include, the better the model's performance. For fine-tuning Gemini on Vertex AI, the minimum number of examples are 100.\n", + "\n", + "#### Dataset Format\n", + "Your training data should be structured in a JSONL file and stored at a Google Cloud Storage (GCS) URI. Each line in the JSONL file must adhere to the following schema:\n", + "\n", + "A `contents` array containing objects that define:\n", + "- A `role` (\"user\" for user input or \"model\" for model output)\n", + "- `parts` containing the input data.\n", + "\n", + "```json\n", + "{\n", + " \"contents\":[\n", + " {\n", + " \"role\":\"user\", # This indicate input content\n", + " \"parts\":[\n", + " {\n", + " \"text\":\"How are you?\"\n", + " }\n", + " ]\n", + " },\n", + " {\n", + " \"role\":\"model\", # This indicate target content\n", + " \"parts\":[ # text only\n", + " {\n", + " \"text\":\"I am good, thank you!\"\n", + " }\n", + " ]\n", + " }\n", + " # ... repeat \"user\", \"model\" for multi turns.\n", + " ]\n", + "}\n", + "```\n", + "\n", + "Refer to the public [documentation](https://cloud.google.com/vertex-ai/generative-ai/docs/models/gemini-supervised-tuning-prepare#about-datasets) for more details." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "I0YcID-jCKLk" + }, + "outputs": [], + "source": [ + "# combine the systeminstruct + context + question into one column.\n", + "train_df = pd.read_csv(\"squad_train.csv\")\n", + "validation_df = pd.read_csv(\"squad_validation.csv\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "4DqrQp4cLqRy" + }, + "outputs": [], + "source": [ + "# combine the systeminstruct + context + question into one column.\n", + "train_df[\"input_question\"] = (\n", + " \"\\n\\n **Below the question with context that you need to answer**\"\n", + " + \"\\n Context: \"\n", + " + train_df[\"context\"]\n", + " + \"\\n Question: \"\n", + " + train_df[\"question\"]\n", + ")\n", + "validation_df[\"input_question\"] = (\n", + " \"\\n\\n **Below the question with context that you need to answer**\"\n", + " + \"\\n Context: \"\n", + " + validation_df[\"context\"]\n", + " + \"\\n Question: \"\n", + " + validation_df[\"question\"]\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "Pmzyz1migvHN" + }, + "outputs": [], + "source": [ + "def df_to_jsonl(df, output_file):\n", + " \"\"\"Converts a Pandas DataFrame to JSONL format and saves it to a file.\n", + "\n", + " Args:\n", + " df: The DataFrame to convert.\n", + " output_file: The name of the output file.\n", + " \"\"\"\n", + "\n", + " with open(output_file, \"w\") as f:\n", + " for row in df.itertuples(index=False):\n", + " jsonl_obj = {\n", + " \"systemInstruction\": {\"parts\": [{\"text\": f\"{systemInstruct}\"}]},\n", + " \"contents\": [\n", + " {\n", + " \"role\": \"user\",\n", + " \"parts\": [{\"text\": f\"{row.input_question}\"}],\n", + " },\n", + " {\"role\": \"model\", \"parts\": [{\"text\": row.answers}]},\n", + " ],\n", + " }\n", + " f.write(json.dumps(jsonl_obj) + \"\\n\")\n", + "\n", + "\n", + "# Process the DataFrames\n", + "df_to_jsonl(train_df, \"squad_train.jsonl\")\n", + "df_to_jsonl(validation_df, \"squad_validation.jsonl\")\n", + "\n", + "print(f\"JSONL data written to squad_train.jsonl\")\n", + "print(f\"JSONL data written to squad_validation.jsonl\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "5OQv-ZMpJDhi" + }, + "source": [ + "Next you will copy the files into your Google Cloud bucket" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "O5k1jYJ10IeW" + }, + "outputs": [], + "source": [ + "!gsutil cp ./squad_train.jsonl {BUCKET_URI}\n", + "!gsutil cp ./squad_validation.jsonl {BUCKET_URI}" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "UAHMYgFJJHjm" + }, + "source": [ + "### Start fine-tuning job\n", + "Next you can start the fine-tuning job.\n", + "\n", + "- `source_model`: Specifies the base Gemini model version you want to fine-tune.\n", + " - `train_dataset`: Path to your training data in JSONL format.\n", + "\n", + " *Optional parameters*\n", + " - `validation_dataset`: If provided, this data is used to evaluate the model during tuning.\n", + " - `tuned_model_display_name`: Display name for the tuned model.\n", + " - `epochs`: The number of training epochs to run.\n", + " - `learning_rate_multiplier`: A value to scale the learning rate during training.\n", + " - `adapter_size` : Gemini 2.0 supports Adapter length [1, 4], default value is 4.\n", + "\n", + " **Important**: The default hyperparameter settings are optimized for optimal performance based on rigorous testing and are recommended for initial use. Users may customize these parameters to address specific performance requirements.**" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "gdcy4umfpGZE" + }, + "outputs": [], + "source": [ + "train_dataset = f\"\"\"{BUCKET_URI}/squad_train.jsonl\"\"\"\n", + "validation_dataset = f\"\"\"{BUCKET_URI}/squad_train.jsonl\"\"\"\n", + "\n", + "training_dataset = {\n", + " \"gcs_uri\": train_dataset,\n", + "}\n", + "\n", + "validation_dataset = types.TuningValidationDataset(gcs_uri=validation_dataset)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "NkboVUkoqWSp" + }, + "outputs": [], + "source": [ + "sft_tuning_job = client.tunings.tune(\n", + " base_model=base_model,\n", + " training_dataset=training_dataset,\n", + " config=types.CreateTuningJobConfig(\n", + " adapter_size=\"ADAPTER_SIZE_EIGHT\",\n", + " epoch_count=1, # set to one to keep time and cost low\n", + " tuned_model_display_name=\"gemini-flash-1.5-qa\",\n", + " ),\n", + ")\n", + "sft_tuning_job" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "UKo8cwF2KVM5" + }, + "source": [ + "**Important:** Tuning time depends on several factors, such as training data size, number of epochs, learning rate multiplier, etc." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "8NiZnPkIKcwm" + }, + "source": [ + "
    \n", + "⚠ It will take ~30 mins for the model tuning job to complete on the provided dataset and set configurations/hyperparameters. ⚠\n", + "
    " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "WECSLyPRth6M" + }, + "outputs": [], + "source": [ + "sft_tuning_job.state" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "_iwz4lhUDC_f" + }, + "outputs": [], + "source": [ + "tuning_job = client.tunings.get(name=sft_tuning_job.name)\n", + "tuning_job" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "gV1ukBznKmlN" + }, + "source": [ + "#### Model tuning metrics\n", + "\n", + "- `/train_total_loss`: Loss for the tuning dataset at a training step.\n", + "- `/train_fraction_of_correct_next_step_preds`: The token accuracy at a training step. A single prediction consists of a sequence of tokens. This metric measures the accuracy of the predicted tokens when compared to the ground truth in the tuning dataset.\n", + "- `/train_num_predictions`: Number of predicted tokens at a training step\n", + "\n", + "#### Model evaluation metrics:\n", + "\n", + "- `/eval_total_loss`: Loss for the evaluation dataset at an evaluation step.\n", + "- `/eval_fraction_of_correct_next_step_preds`: The token accuracy at an evaluation step. A single prediction consists of a sequence of tokens. This metric measures the accuracy of the predicted tokens when compared to the ground truth in the evaluation dataset.\n", + "- `/eval_num_predictions`: Number of predicted tokens at an evaluation step.\n", + "\n", + "The metrics visualizations are available after the model tuning job completes. If you don't specify a validation dataset when you create the tuning job, only the visualizations for the tuning metrics are available." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "_IoiiRH5Lhpf" + }, + "outputs": [], + "source": [ + "experiment_name = tuning_job.experiment\n", + "experiment_name" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "DH0guHM---Jo" + }, + "outputs": [], + "source": [ + "# Locate Vertex AI Experiment and Vertex AI Experiment Run\n", + "experiment = aiplatform.Experiment(experiment_name=experiment_name)\n", + "filter_str = metadata_utils._make_filter_string(\n", + " schema_title=\"system.ExperimentRun\",\n", + " parent_contexts=[experiment.resource_name],\n", + ")\n", + "experiment_run = context.Context.list(filter_str)[0]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "hggHQFIl_FXC" + }, + "outputs": [], + "source": [ + "# Read data from Tensorboard\n", + "tensorboard_run_name = f\"{experiment.get_backing_tensorboard_resource().resource_name}/experiments/{experiment.name}/runs/{experiment_run.name.replace(experiment.name, '')[1:]}\"\n", + "tensorboard_run = aiplatform.TensorboardRun(tensorboard_run_name)\n", + "metrics = tensorboard_run.read_time_series_data()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "BdHKZdqG_bHf" + }, + "outputs": [], + "source": [ + "def get_metrics(metric: str = \"/train_total_loss\"):\n", + " \"\"\"\n", + " Get metrics from Tensorboard.\n", + "\n", + " Args:\n", + " metric: metric name, eg. /train_total_loss or /eval_total_loss.\n", + " Returns:\n", + " steps: list of steps.\n", + " steps_loss: list of loss values.\n", + " \"\"\"\n", + " loss_values = metrics[metric].values\n", + " steps_loss = []\n", + " steps = []\n", + " for loss in loss_values:\n", + " steps_loss.append(loss.scalar.value)\n", + " steps.append(loss.step)\n", + " return steps, steps_loss" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "_pDrlpA7_e9o" + }, + "outputs": [], + "source": [ + "# Get Train and Eval Loss\n", + "train_loss = get_metrics(metric=\"/train_total_loss\")\n", + "eval_loss = get_metrics(metric=\"/eval_total_loss\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "DL07j7u__iZx" + }, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + "\n", + "\n", + "
    \n", + "
    \n", + "\n", + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# Plot the train and eval loss metrics using Plotly python library\n", + "fig = make_subplots(\n", + " rows=1, cols=2, shared_xaxes=True, subplot_titles=(\"Train Loss\", \"Eval Loss\")\n", + ")\n", + "\n", + "# Add traces\n", + "fig.add_trace(\n", + " go.Scatter(x=train_loss[0], y=train_loss[1], name=\"Train Loss\", mode=\"lines\"),\n", + " row=1,\n", + " col=1,\n", + ")\n", + "fig.add_trace(\n", + " go.Scatter(x=eval_loss[0], y=eval_loss[1], name=\"Eval Loss\", mode=\"lines\"),\n", + " row=1,\n", + " col=2,\n", + ")\n", + "\n", + "# Add figure title\n", + "fig.update_layout(title=\"Train and Eval Loss\", xaxis_title=\"Steps\", yaxis_title=\"Loss\")\n", + "\n", + "# Set x-axis title\n", + "fig.update_xaxes(title_text=\"Steps\")\n", + "\n", + "# Set y-axes titles\n", + "fig.update_yaxes(title_text=\"Loss\")\n", + "\n", + "# Show plot\n", + "fig.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "pivmh4Lwbgy1" + }, + "source": [ + "### Use the fine-tuned model and evaluation" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "qO6ln4teagw1" + }, + "outputs": [], + "source": [ + "prompt = \"\"\"\n", + "Answer the question based on the context\n", + "\n", + "Context: In the 1840s and 50s, there were attempts to overcome this problem by means of various patent valve gears with a separate, variable cutoff expansion valve riding on the back of the main slide valve; the latter usually had fixed or limited cutoff.\n", + "The combined setup gave a fair approximation of the ideal events, at the expense of increased friction and wear, and the mechanism tended to be complicated.\n", + "The usual compromise solution has been to provide lap by lengthening rubbing surfaces of the valve in such a way as to overlap the port on the admission side, with the effect that the exhaust side remains open for a longer period after cut-off on the admission side has occurred.\n", + "This expedient has since been generally considered satisfactory for most purposes and makes possible the use of the simpler Stephenson, Joy and Walschaerts motions.\n", + "Corliss, and later, poppet valve gears had separate admission and exhaust valves driven by trip mechanisms or cams profiled so as to give ideal events; most of these gears never succeeded outside of the stationary marketplace due to various other issues including leakage and more delicate mechanisms.\n", + "\n", + "Question: How is lap provided by overlapping the admission side port?\n", + "\"\"\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "QJgK_ZfL5dsj" + }, + "outputs": [], + "source": [ + "tuned_model = tuning_job.tuned_model.endpoint\n", + "tuned_model" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "ifhRboiCOBje" + }, + "outputs": [], + "source": [ + "get_predictions(prompt, tuned_model)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "SyrEDGAoA4f5" + }, + "outputs": [], + "source": [ + "# Apply the get_prediction() function to the 'question_column'\n", + "test_df[\"predicted_answer\"] = test_df[\"input_question\"].apply(get_predictions)\n", + "test_df.head(2)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "IjQO_tqVFKf3" + }, + "outputs": [], + "source": [ + "test_df[\"predicted_answer\"] = test_df[\"predicted_answer\"].apply(normalize_answer)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "kBawjkvKQ_Q-" + }, + "source": [ + "After running the evaluation you can see that the model generally performs better on our use case after fine-tuning. Of course, depending on things like use case or data quality performance will differ." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "zjoRgjg6fr0P" + }, + "outputs": [], + "source": [ + "em, f1 = calculate_em_and_f1(test_df[\"answers\"], test_df[\"predicted_answer\"])\n", + "print(f\"EM score: {em}\")\n", + "print(f\"F1 score: {f1}\")" + ] + } + ], + "metadata": { + "colab": { + "name": "sft_gemini_qa.ipynb", + "toc_visible": true + }, + "kernelspec": { + "display_name": "Python 3", + "name": "python3" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} diff --git a/gemini/tuning/sft_gemini_summarization.ipynb b/gemini/tuning/sft_gemini_summarization.ipynb new file mode 100644 index 0000000..fc36c34 --- /dev/null +++ b/gemini/tuning/sft_gemini_summarization.ipynb @@ -0,0 +1,1718 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "i3oNB_qC4X2Y" + }, + "outputs": [], + "source": [ + "# Copyright 2024 Google LLC\n", + "#\n", + "# Licensed under the Apache License, Version 2.0 (the \"License\");\n", + "# you may not use this file except in compliance with the License.\n", + "# You may obtain a copy of the License at\n", + "#\n", + "# https://www.apache.org/licenses/LICENSE-2.0\n", + "#\n", + "# Unless required by applicable law or agreed to in writing, software\n", + "# distributed under the License is distributed on an \"AS IS\" BASIS,\n", + "# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n", + "# See the License for the specific language governing permissions and\n", + "# limitations under the License." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "c4-kxwz23nzr" + }, + "source": [ + "# Supervised Fine Tuning with Gemini 2.5 Flash for Article Summarization\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + "
    \n", + " \n", + " \"Google
    Open in Colab\n", + "
    \n", + "
    \n", + " \n", + " \"Google
    Open in Colab Enterprise\n", + "
    \n", + "
    \n", + " \n", + " \"Vertex
    Open in Workbench\n", + "
    \n", + "
    \n", + " \n", + " \"GitHub
    View on GitHub\n", + "
    \n", + "
    \n", + "\n", + "
    \n", + "\n", + "Share to:\n", + "\n", + "\n", + " \"LinkedIn\n", + "\n", + "\n", + "\n", + " \"Bluesky\n", + "\n", + "\n", + "\n", + " \"X\n", + "\n", + "\n", + "\n", + " \"Reddit\n", + "\n", + "\n", + "\n", + " \"Facebook\n", + " " + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "pO98gUu-4eTJ" + }, + "source": [ + "| Authors |\n", + "| --- |\n", + "| [Erwin Huizenga](https://www.linkedin.com/in/erwinhuizenga/) |\n", + "| [Deepak Moonat](https://github.com/dmoonat) |\n", + "| [Safiuddin Khaja](https://github.com/Safikh) |" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "PN72vQp6DWck" + }, + "source": [ + "## Overview\n", + "\n", + "**Gemini** is a family of generative AI models developed by Google DeepMind that is designed for multimodal use cases. The Gemini API gives you access to the various Gemini models, such as Gemini 2.5 Pro/Flash, Gemini 2.5/Flash, Gemini/Flash and more.\n", + "\n", + "This notebook demonstrates how to fine-tune the Gemini 2.5 Flash generative model using the Vertex AI Supervised Tuning feature. Supervised Tuning allows you to use your own training data to further refine the base model's capabilities towards your specific tasks.\n", + "\n", + "Supervised Tuning uses labeled examples to tune a model. Each example demonstrates the output you want from your text model during inference.\n", + "\n", + "First, ensure your training data is of high quality, well-labeled, and directly relevant to the target task. This is crucial as low-quality data can adversely affect the performance and introduce bias in the fine-tuned model.\n", + "- Training: Experiment with different configurations to optimize the model's performance on the target task.\n", + "- Evaluation:\n", + " - Metric: Choose appropriate evaluation metrics that accurately reflect the success of the fine-tuned model for your specific task\n", + " - Evaluation Set: Use a separate set of data to evaluate the model's performance\n", + "\n", + "\n", + "Refer to public [documentation](https://cloud.google.com/vertex-ai/generative-ai/docs/models/gemini-supervised-tuning) for more details.\n", + "\n", + "\n", + "
    \n", + "\n", + "Before running this notebook, ensure you have:\n", + "\n", + "- A Google Cloud project: Provide your project ID in the `PROJECT_ID` variable.\n", + "\n", + "- Authenticated your Colab environment: Run the authentication code block at the beginning.\n", + "\n", + "- Prepared training data (Test with your own data or use the one in the notebook): Data should be formatted in JSONL with prompts and corresponding completions." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "77ppk4eke7G4" + }, + "source": [ + "### Objective\n", + "\n", + "In this tutorial, you will learn how to use `Vertex AI` to tune a `Gemini 2.5 Flash` model.\n", + "\n", + "\n", + "This tutorial uses the following Google Cloud ML services:\n", + "\n", + "- `Vertex AI`\n", + "\n", + "\n", + "The steps performed include:\n", + "\n", + "- Prepare and load the dataset\n", + "- Load the `gemini-2.5-flash` model\n", + "- Evaluate the model before tuning\n", + "- Tune the model.\n", + " - This will automatically create a Vertex AI endpoint and deploy the model to it\n", + "- Make a prediction using tuned model\n", + "- Evaluate the model after tuning" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "PVRaH5wqfIy3" + }, + "source": [ + "### Costs\n", + "\n", + "This tutorial uses billable components of Google Cloud:\n", + "\n", + "* Vertex AI\n", + "* Cloud Storage\n", + "\n", + "Learn about [Vertex AI\n", + "pricing](https://cloud.google.com/vertex-ai/pricing), [Cloud Storage\n", + "pricing](https://cloud.google.com/storage/pricing), and use the [Pricing\n", + "Calculator](https://cloud.google.com/products/calculator/)\n", + "to generate a cost estimate based on your projected usage." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "sfQYl84Cu_xL" + }, + "source": [ + "## Wikilingua Dataset" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "SxAXgV2FvBPz" + }, + "source": [ + "The dataset includes article and summary pairs from WikiHow. It consists of article-summary pairs in multiple languages. Refer to the following [github repository](https://github.com/esdurmus/Wikilingua) for more details.\n", + "\n", + "For this notebook, we have picked `english` language dataset." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "KTHBNpb-BBdc" + }, + "source": [ + "### Dataset Citation\n", + "\n", + "```bibtex\n", + "@inproceedings{ladhak-wiki-2020,\n", + " title={WikiLingua: A New Benchmark Dataset for Multilingual Abstractive Summarization},\n", + " author={Faisal Ladhak, Esin Durmus, Claire Cardie and Kathleen McKeown},\n", + " booktitle={Findings of EMNLP, 2020},\n", + " year={2020}\n", + "}\n", + "```" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "llEFILYz2aye" + }, + "source": [ + "## Getting Started" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "oo2rh4cC2e1r" + }, + "source": [ + "### Install Gen AI SDK and other required packages" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "0d8bad6906dc" + }, + "source": [ + "The new Google Gen AI SDK provides a unified interface to Gemini through both the Gemini Developer API and the Gemini API on Vertex AI. With a few exceptions, code that runs on one platform will run on both. This means that you can prototype an application using the Developer API and then migrate the application to Vertex AI without rewriting your code.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "l_ok3vdw2cyf" + }, + "outputs": [], + "source": [ + "%pip install --upgrade --user --quiet google-genai google-cloud-aiplatform rouge_score plotly jsonlines" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "6in46hzz3At9" + }, + "source": [ + "### Restart runtime (Colab only)\n", + "\n", + "To use the newly installed packages, you must restart the runtime on Google Colab.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "haJlKcSY3EsE" + }, + "outputs": [], + "source": [ + "# # Uncomment the below code to restart the colab kernel\n", + "# import sys\n", + "\n", + "# if \"google.colab\" in sys.modules:\n", + "# import IPython\n", + "\n", + "# app = IPython.Application.instance()\n", + "# app.kernel.do_shutdown(True)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "iTOjupCM3TDb" + }, + "source": [ + "
    \n", + "⚠ The kernel is going to restart. Please wait until it is finished before continuing to the next step. ⚠\n", + "
    " + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "F0HMlz-MD9Yt" + }, + "source": [ + "## Step0: Authenticate your notebook environment (Colab only)\n", + "\n", + "If you are running this notebook on Google Colab, run the cell below to authenticate your environment." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "86VNaqlgD9rK" + }, + "outputs": [], + "source": [ + "import sys\n", + "\n", + "if \"google.colab\" in sys.modules:\n", + " from google.colab import auth\n", + "\n", + " auth.authenticate_user()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "yKRPFNzWJLVY" + }, + "source": [ + "- If you are running this notebook in a local development environment:\n", + " - Install the [Google Cloud SDK](https://cloud.google.com/sdk).\n", + " - Obtain authentication credentials. Create local credentials by running the following command and following the oauth2 flow (read more about the command [here](https://cloud.google.com/sdk/gcloud/reference/beta/auth/application-default/login)):\n", + "\n", + " ```bash\n", + " gcloud auth application-default login\n", + " ```" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "k8CI-TcqD06L" + }, + "source": [ + "## Step1: Import Libraries" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "rerpHL_eEG8D" + }, + "outputs": [], + "source": [ + "import time\n", + "\n", + "# For data handling.\n", + "import jsonlines\n", + "import pandas as pd\n", + "\n", + "# For visualization.\n", + "import plotly.graph_objects as go\n", + "\n", + "# For fine tuning Gemini model.\n", + "import vertexai\n", + "from google import genai\n", + "\n", + "# For extracting vertex experiment details.\n", + "from google.cloud import aiplatform\n", + "from google.cloud.aiplatform.metadata import context\n", + "from google.cloud.aiplatform.metadata import utils as metadata_utils\n", + "from google.genai import types\n", + "from plotly.subplots import make_subplots\n", + "\n", + "# For evaluation metric computation.\n", + "from rouge_score import rouge_scorer\n", + "from tqdm import tqdm" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "FBY9nK3qEJLk" + }, + "source": [ + "## Step2: Set Google Cloud project information and initialize Vertex AI and Gen AI SDK\n", + "\n", + "To get started using Vertex AI, you must have an existing Google Cloud project and [enable the Vertex AI API](https://console.cloud.google.com/flows/enableapi?apiid=aiplatform.googleapis.com).\n", + "\n", + "Learn more about [setting up a project and a development environment](https://cloud.google.com/vertex-ai/docs/start/cloud-environment).\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "VpzmI1K61Tn2" + }, + "outputs": [], + "source": [ + "PROJECT_ID = \"[your-project-id]\" # @param {type:\"string\"}\n", + "REGION = \"us-central1\" # @param {type:\"string\"}" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "7MZmIZvlQUhy" + }, + "outputs": [], + "source": [ + "vertexai.init(project=PROJECT_ID, location=REGION)\n", + "\n", + "client = genai.Client(vertexai=True, project=PROJECT_ID, location=REGION)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "JUEloBlsCPFr" + }, + "source": [ + "## Step3: Create Dataset in correct format\n", + "\n", + "The dataset used to tune a foundation model needs to include examples that align with the task that you want the model to perform. Structure your training dataset in a text-to-text format. Each record, or row, in the dataset contains the input text (also referred to as the prompt) which is paired with its expected output from the model. Supervised tuning uses the dataset to teach the model to mimic a behavior, or task, you need by giving it hundreds of examples that illustrate that behavior.\n", + "\n", + "Your dataset size depends on the task, and follows the recommendation mentioned in the `Overview` section. The more examples you provide in your dataset, the better the results.\n", + "\n", + "### Dataset format\n", + "\n", + "Training data should be structured within a JSONL file located at a Google Cloud Storage (GCS) URI. Each line (or row) of the JSONL file must adhere to a specific schema: It should contain a `contents` array, with objects inside defining a `role` (either \"user\" for user input or \"model\" for model output) and `parts`, containing the input data. For example, a valid data row would look like this:\n", + "\n", + "\n", + "```json\n", + "{\n", + " \"contents\": [\n", + " {\n", + " \"role\": \"user\", # This indicates input content\n", + " \"parts\": [\n", + " {\n", + " \"text\": \"How are you?\"\n", + " }\n", + " ]\n", + " },\n", + " {\n", + " \"role\": \"model\", # This indicates target content\n", + " \"parts\": [ # text only\n", + " {\n", + " \"text\": \"I am good, thank you!\"\n", + " }\n", + " ]\n", + " }\n", + " ] # ... repeat \"user\", \"model\" for multi turns.\n", + "}\n", + "```\n", + "\n", + "Refer to the public [documentation](https://cloud.google.com/vertex-ai/generative-ai/docs/models/gemini-supervised-tuning-prepare#about-datasets) for more details." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "TglfI3tQr2oZ" + }, + "source": [ + "To run a tuning job, you need to upload one or more datasets to a Cloud Storage bucket. You can either create a new Cloud Storage bucket or use an existing one to store dataset files. The region of the bucket doesn't matter, but we recommend that you use a bucket that's in the same Google Cloud project where you plan to tune your model." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "OsNdYgnaITuz" + }, + "source": [ + "### Step3 [a]: Create a Cloud Storage bucket\n", + "\n", + "Create a storage bucket to store intermediate artifacts such as datasets.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "WveeKANmITK5" + }, + "outputs": [], + "source": [ + "# Provide a bucket name\n", + "BUCKET_NAME = \"[your-bucket-name]\" # @param {type:\"string\"}\n", + "BUCKET_URI = f\"gs://{BUCKET_NAME}\"" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "lBSGTEiyJfSR" + }, + "source": [ + "Only if your bucket doesn't already exist: Run the following cell to create your Cloud Storage bucket.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "GQSJ3LJkJhLm" + }, + "outputs": [], + "source": [ + "! gsutil mb -l {REGION} -p {PROJECT_ID} {BUCKET_URI}" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "7YGurtXHJy_y" + }, + "source": [ + "### Step3 [b]: Upload tuning data to Cloud Storage" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "ip8rErN2r3ah" + }, + "source": [ + "- Data used in this notebook is present in the public Google Cloud Storage(GCS) bucket.\n", + "- It's in Gemini finetuning dataset format" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "aV_GZg_LaNmV" + }, + "outputs": [], + "source": [ + "!gsutil ls gs://github-repo/generative-ai/gemini/tuning/summarization/wikilingua" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "b-EQ4FcExIfp" + }, + "outputs": [], + "source": [ + "!gsutil cp gs://github-repo/generative-ai/gemini/tuning/summarization/wikilingua/* ." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "6wmBkAUoyzdJ" + }, + "source": [ + "#### Convert Gemini tuning dataset to Gemini 2.5 tuning dataset format" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "7bhtxKbha_wS" + }, + "outputs": [], + "source": [ + "def save_jsonlines(file, instances) -> None:\n", + " \"\"\"Saves a list of json instances to a jsonlines file.\"\"\"\n", + " with jsonlines.open(file, mode=\"w\") as writer:\n", + " writer.write_all(instances)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "-3xbpRnSyxH9" + }, + "outputs": [], + "source": [ + "def create_tuning_samples(file_path):\n", + " \"\"\"Creates tuning samples from a file.\"\"\"\n", + " with jsonlines.open(file_path) as reader:\n", + " instances = []\n", + " for obj in reader:\n", + " instance = []\n", + " for content in obj[\"messages\"]:\n", + " instance.append(\n", + " {\"role\": content[\"role\"], \"parts\": [{\"text\": content[\"content\"]}]}\n", + " )\n", + " instances.append({\"contents\": instance})\n", + " return instances" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "orHUpTagyw-z" + }, + "outputs": [], + "source": [ + "train_file = \"sft_train_samples.jsonl\"\n", + "train_instances = create_tuning_samples(train_file)\n", + "len(train_instances)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "U3m05gAM1mlp" + }, + "outputs": [], + "source": [ + "# save the training instances to jsonl file\n", + "save_jsonlines(train_file, train_instances)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "xZICW6uU1T3Z" + }, + "outputs": [], + "source": [ + "val_file = \"sft_val_samples.jsonl\"\n", + "val_instances = create_tuning_samples(val_file)\n", + "len(val_instances)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "n1s7adZH1CCe" + }, + "outputs": [], + "source": [ + "# save the validation instances to jsonl file\n", + "save_jsonlines(val_file, val_instances)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "AVL2gfP-J5SL" + }, + "outputs": [], + "source": [ + "# Copy the tuning and evaluation data to your bucket.\n", + "!gsutil cp {train_file} {BUCKET_URI}/train/\n", + "!gsutil cp {val_file} {BUCKET_URI}/val/" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "kV5X6_DsIXPm" + }, + "source": [ + "### Step3 [c]: Test dataset\n", + "\n", + "- It contains document text(`input_text`) and corresponding reference summary(`output_text`), which will be compared with the model generated summary" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "wtxPI3GPIckU" + }, + "outputs": [], + "source": [ + "# Load the test dataset using pandas as it's in the csv format.\n", + "testing_data_path = \"sft_test_samples.csv\"\n", + "test_data = pd.read_csv(testing_data_path)\n", + "test_data.head()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "bRBtYfN_PPaP" + }, + "outputs": [], + "source": [ + "test_data.loc[0, \"input_text\"]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "tTt7qjSeSHRW" + }, + "outputs": [], + "source": [ + "# Article summary stats\n", + "stats = test_data[\"output_text\"].apply(len).describe()\n", + "stats" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "WKptd49cdjSi" + }, + "outputs": [], + "source": [ + "print(f\"Total `{stats['count']}` test records\")\n", + "print(f\"Average length is `{stats['mean']}` and max is `{stats['max']}` characters\")\n", + "print(\"\\nConsidering 1 token = 4 chars\")\n", + "\n", + "# Get ceil value of the tokens required.\n", + "tokens = (stats[\"max\"] / 4).__ceil__()\n", + "print(\n", + " f\"\\nSet max_token_length = stats['max']/4 = {stats['max'] / 4} ~ {tokens} characters\"\n", + ")\n", + "print(f\"\\nLet's keep output tokens upto `{tokens}`\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "idM1p_UNvA7w" + }, + "outputs": [], + "source": [ + "# Maximum number of tokens that can be generated in the response by the LLM.\n", + "# Experiment with this number to get optimal output.\n", + "max_output_tokens = tokens" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "DhjmRffOOPAS" + }, + "source": [ + "## Step4: Initailize model" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "UhhD1VWDsLat" + }, + "source": [ + "The following Gemini text model support supervised tuning:\n", + "\n", + "* `gemini-2.5-flash`" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "jL-zRl5_OVZW" + }, + "outputs": [], + "source": [ + "base_model = \"gemini-2.5-flash\"" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "ieJe8yGlOtFD" + }, + "source": [ + "## Step5: Test the Gemini model" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "F8DFUzRnHMi8" + }, + "source": [ + "### Generation config\n", + "\n", + "- Each call that you send to a model includes parameter values that control how the model generates a response. The model can generate different results for different parameter values\n", + "- Experiment with different parameter values to get the best values for the task\n", + "\n", + "Refer to the following [link](https://cloud.google.com/vertex-ai/generative-ai/docs/learn/prompts/adjust-parameter-values) for understanding different parameters" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "6hbaeT8AcniS" + }, + "source": [ + "**Prompt** is a natural language request submitted to a language model to receive a response back\n", + "\n", + "Some best practices include\n", + " - Clearly communicate what content or information is most important\n", + " - Structure the prompt:\n", + " - Defining the role if using one. For example, You are an experienced UX designer at a top tech company\n", + " - Include context and input data\n", + " - Provide the instructions to the model\n", + " - Add example(s) if you are using them\n", + "\n", + "Refer to the following [link](https://cloud.google.com/vertex-ai/generative-ai/docs/learn/prompts/prompt-design-strategies) for prompt design strategies." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "hZUcvQr0rAWA" + }, + "source": [ + "Wikilingua data contains the following task prompt at the end of the article, `Provide a summary of the article in two or three sentences:`" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "iu6OuIhFOv4C" + }, + "outputs": [], + "source": [ + "test_doc = test_data.loc[0, \"input_text\"]\n", + "\n", + "prompt = f\"\"\"\n", + "{test_doc}\n", + "\"\"\"\n", + "\n", + "config = {\n", + " \"temperature\": 0.1,\n", + " \"max_output_tokens\": max_output_tokens,\n", + "}\n", + "\n", + "response = client.models.generate_content(\n", + " model=base_model,\n", + " contents=prompt,\n", + " config=config,\n", + ").text\n", + "print(response)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "8YvlMfmIQqK8" + }, + "outputs": [], + "source": [ + "# Ground truth\n", + "test_data.loc[0, \"output_text\"]" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "jGPUKZlcP69-" + }, + "source": [ + "## Step6: Evaluation before model tuning" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "1yayTQdd9oE5" + }, + "source": [ + "- Evaluate the Gemini model on the test dataset before tuning it on the training dataset." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "610J64SpQ5TE" + }, + "outputs": [], + "source": [ + "# Convert the pandas dataframe to records (list of dictionaries).\n", + "corpus = test_data.to_dict(orient=\"records\")\n", + "# Check number of records.\n", + "len(corpus)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "KkKldH90MY4v" + }, + "source": [ + "### Evaluation metric\n", + "\n", + "The type of metrics used for evaluation depends on the task that you are evaluating. The following table shows the supported tasks and the metrics used to evaluate each task:" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "t6oLtUEWMHVu" + }, + "source": [ + "| Task | Metric(s) |\n", + "|-----------------|---------------------------------|\n", + "| Classification | Micro-F1, Macro-F1, Per class F1 |\n", + "| Summarization | ROUGE-L |\n", + "| Question Answering | Exact Match |\n", + "| Text Generation | BLEU, ROUGE-L |\n", + "\n", + "\n", + "
    \n", + "\n", + "Refer to this [documentation](https://cloud.google.com/vertex-ai/generative-ai/docs/models/evaluate-models) for metric based evaluation." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "iKNk3zG4CNSS" + }, + "source": [ + "- **Recall-Oriented Understudy for Gisting Evaluation (ROUGE)**: A metric used to evaluate the quality of automatic summaries of text. It works by comparing a generated summary to a set of reference summaries created by humans." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "5XP9VOaTd3z8" + }, + "source": [ + "Now you can take the candidate and reference to evaluate the performance. In this case, ROUGE will give you:\n", + "\n", + "- `rouge-1`, which measures unigram overlap\n", + "- `rouge-2`, which measures bigram overlap\n", + "- `rouge-l`, which measures the longest common subsequence" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "I2CrcvvzFfBL" + }, + "source": [ + "#### *Recall vs. Precision*\n", + "\n", + "**Recall**, meaning it prioritizes how much of the information in the reference summaries is captured in the generated summary.\n", + "\n", + "**Precision**, which measures how much of the generated summary is relevant to the original text." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "uU6K4YdaGGyp" + }, + "source": [ + "Alternate Evaluation method: Check out the [AutoSxS](https://cloud.google.com/vertex-ai/generative-ai/docs/models/side-by-side-eval) evaluation for automatic evaluation of the task.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "p3YZAOZcQWtW" + }, + "outputs": [], + "source": [ + "# Create rouge_scorer object for evaluation\n", + "scorer = rouge_scorer.RougeScorer([\"rouge1\", \"rouge2\", \"rougeL\"], use_stemmer=True)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "N8Y06N_b_EP5" + }, + "outputs": [], + "source": [ + "def run_evaluation(model, corpus: list[dict]) -> pd.DataFrame:\n", + " \"\"\"Runs evaluation for the given model and data.\n", + "\n", + " Args:\n", + " model: The generation model.\n", + " corpus: The test data.\n", + "\n", + " Returns:\n", + " A pandas DataFrame containing the evaluation results.\n", + " \"\"\"\n", + " records = []\n", + " for item in tqdm(corpus):\n", + " document = item.get(\"input_text\")\n", + " summary = item.get(\"output_text\")\n", + "\n", + " # Catch any exception that occur during model evaluation.\n", + " try:\n", + " response = client.models.generate_content(\n", + " model=model,\n", + " contents=document,\n", + " config=config,\n", + " )\n", + "\n", + " # Check if response is generated by the model, if response is empty then continue to next item.\n", + " if not (\n", + " response\n", + " and response.candidates\n", + " and response.candidates[0].content.parts\n", + " ):\n", + " print(\n", + " f\"\\nModel has blocked the response for the document.\\n Response: {response}\\n Document: {document}\"\n", + " )\n", + " continue\n", + "\n", + " # Calculates the ROUGE score for a given reference and generated summary.\n", + " scores = scorer.score(target=summary, prediction=response.text)\n", + "\n", + " # Append the results to the records list\n", + " records.append(\n", + " {\n", + " \"document\": document,\n", + " \"summary\": summary,\n", + " \"generated_summary\": response.text,\n", + " \"scores\": scores,\n", + " \"rouge1_precision\": scores.get(\"rouge1\").precision,\n", + " \"rouge1_recall\": scores.get(\"rouge1\").recall,\n", + " \"rouge1_fmeasure\": scores.get(\"rouge1\").fmeasure,\n", + " \"rouge2_precision\": scores.get(\"rouge2\").precision,\n", + " \"rouge2_recall\": scores.get(\"rouge2\").recall,\n", + " \"rouge2_fmeasure\": scores.get(\"rouge2\").fmeasure,\n", + " \"rougeL_precision\": scores.get(\"rougeL\").precision,\n", + " \"rougeL_recall\": scores.get(\"rougeL\").recall,\n", + " \"rougeL_fmeasure\": scores.get(\"rougeL\").fmeasure,\n", + " }\n", + " )\n", + " except AttributeError as attr_err:\n", + " print(\"Attribute Error:\", attr_err)\n", + " continue\n", + " except Exception as err:\n", + " print(\"Error:\", err)\n", + " continue\n", + " return pd.DataFrame(records)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "4afTSo5cpM73" + }, + "outputs": [], + "source": [ + "# Batch of test data.\n", + "corpus_batch = corpus[:100]" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "oM10zigp7kTZ" + }, + "source": [ + "
    \n", + "⚠ It will take ~2 mins for the evaluation run on the provided batch. ⚠\n", + "
    " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "aO8JhIg1pYkE" + }, + "outputs": [], + "source": [ + "# Run evaluation using loaded model and test data corpus\n", + "evaluation_df = run_evaluation(base_model, corpus_batch)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "H4VUFeb9tRBP" + }, + "outputs": [], + "source": [ + "evaluation_df.head()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "18m75w6m0R10" + }, + "outputs": [], + "source": [ + "evaluation_df_stats = evaluation_df.dropna().describe()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "dYokPWqdUIMv" + }, + "outputs": [], + "source": [ + "# Statistics of the evaluation dataframe.\n", + "evaluation_df_stats" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "4fdVjY_JWcmq" + }, + "outputs": [], + "source": [ + "print(\"Mean rougeL_precision is\", evaluation_df_stats.rougeL_precision[\"mean\"])" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "IgMb3E0YEqL2" + }, + "source": [ + "## Step7: Fine-tune the Model\n", + "\n", + " - `source_model`: Specifies the base Gemini model version you want to fine-tune.\n", + " - `train_dataset`: Path to your training data in JSONL format.\n", + "\n", + " *Optional parameters*\n", + " - `validation_dataset`: If provided, this data is used to evaluate the model during tuning.\n", + " - `tuned_model_display_name`: Display name for the tuned model.\n", + " - `epochs`: The number of training epochs to run.\n", + " - `learning_rate_multiplier`: A value to scale the learning rate during training.\n", + " - `adapter_size` : Gemini 2.5 Flash supports Adapter length [1, 2, 4, 8], default value is 4." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "4e81137766c6" + }, + "source": [ + "**Note: The default hyperparameter settings are optimized for optimal performance based on rigorous testing and are recommended for initial use. Users may customize these parameters to address specific performance requirements.**" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "vQM2vDBZ27b_" + }, + "outputs": [], + "source": [ + "tuned_model_display_name = \"[DISPLAY NAME FOR TUNED MODEL]\" # @param {type:\"string\"}\n", + "\n", + "training_dataset = {\n", + " \"gcs_uri\": f\"{BUCKET_URI}/train/sft_train_samples.jsonl\",\n", + "}\n", + "\n", + "validation_dataset = types.TuningValidationDataset(\n", + " gcs_uri=f\"{BUCKET_URI}/val/sft_val_samples.jsonl\"\n", + ")\n", + "\n", + "# Tune a model using `tune` method.\n", + "sft_tuning_job = client.tunings.tune(\n", + " base_model=base_model,\n", + " training_dataset=training_dataset,\n", + " config=types.CreateTuningJobConfig(\n", + " tuned_model_display_name=tuned_model_display_name,\n", + " validation_dataset=validation_dataset,\n", + " ),\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "yLlAgVjCNqXg" + }, + "outputs": [], + "source": [ + "# Get the tuning job info.\n", + "tuning_job = client.tunings.get(name=sft_tuning_job.name)\n", + "tuning_job" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "22QZ035C8GJ3" + }, + "source": [ + "**Note: Tuning time depends on several factors, such as training data size, number of epochs, learning rate multiplier, etc.**" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "NN1KX-_WyKeu" + }, + "source": [ + "
    \n", + "⚠ It will take ~15 mins for the model tuning job to complete on the provided dataset and set configurations/hyperparameters. ⚠\n", + "
    " + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "233ec153ccc0" + }, + "source": [ + "### [Optional] Cancel Tuning Job" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "c3d93e91ff0e" + }, + "source": [ + "- Uncomment the below code to cancel the tuning job" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "2bae78ec8856" + }, + "outputs": [], + "source": [ + "## Cancel the tuning job\n", + "# tuning_job = client.tunings.cancel(name=sft_tuning_job.name)\n", + "# tuning_job" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "5f3380996dc4" + }, + "source": [ + "### Status Check" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "2ma3P6tZ6suI" + }, + "outputs": [], + "source": [ + "%%time\n", + "# Wait for job completion\n", + "\n", + "running_states = [\n", + " \"JOB_STATE_PENDING\",\n", + " \"JOB_STATE_RUNNING\",\n", + "]\n", + "\n", + "while tuning_job.state.name in running_states:\n", + " print(\".\", end=\"\")\n", + " tuning_job = client.tunings.get(name=tuning_job.name)\n", + " time.sleep(10)\n", + "print()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "e1O1xCBS6spi" + }, + "outputs": [], + "source": [ + "tuned_model = tuning_job.tuned_model.endpoint\n", + "experiment_name = tuning_job.experiment\n", + "\n", + "print(\"Tuned model experiment\", experiment_name)\n", + "print(\"Tuned model endpoint resource name:\", tuned_model)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "8DzlWWKpbGcu" + }, + "source": [ + "### Step7 [a]: Tuning and evaluation metrics" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "psRbCfzwWz_g" + }, + "source": [ + "#### Model tuning metrics\n", + "\n", + "- `/train_total_loss`: Loss for the tuning dataset at a training step.\n", + "- `/train_fraction_of_correct_next_step_preds`: The token accuracy at a training step. A single prediction consists of a sequence of tokens. This metric measures the accuracy of the predicted tokens when compared to the ground truth in the tuning dataset.\n", + "- `/train_num_predictions`: Number of predicted tokens at a training step\n", + "\n", + "#### Model evaluation metrics:\n", + "\n", + "- `/eval_total_loss`: Loss for the evaluation dataset at an evaluation step.\n", + "- `/eval_fraction_of_correct_next_step_preds`: The token accuracy at an evaluation step. A single prediction consists of a sequence of tokens. This metric measures the accuracy of the predicted tokens when compared to the ground truth in the evaluation dataset.\n", + "- `/eval_num_predictions`: Number of predicted tokens at an evaluation step.\n", + "\n", + "The metrics visualizations are available after the model tuning job completes. If you don't specify a validation dataset when you create the tuning job, only the visualizations for the tuning metrics are available.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "5J1LP3nCbNlg" + }, + "outputs": [], + "source": [ + "# Locate Vertex AI Experiment and Vertex AI Experiment Run\n", + "experiment = aiplatform.Experiment(experiment_name=experiment_name)\n", + "filter_str = metadata_utils._make_filter_string(\n", + " schema_title=\"system.ExperimentRun\",\n", + " parent_contexts=[experiment.resource_name],\n", + ")\n", + "experiment_run = context.Context.list(filter_str)[0]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "htBrcQY1bPyh" + }, + "outputs": [], + "source": [ + "# Read data from Tensorboard\n", + "tensorboard_run_name = f\"{experiment.get_backing_tensorboard_resource().resource_name}/experiments/{experiment.name}/runs/{experiment_run.name.replace(experiment.name, '')[1:]}\"\n", + "tensorboard_run = aiplatform.TensorboardRun(tensorboard_run_name)\n", + "metrics = tensorboard_run.read_time_series_data()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "uRZ-UZXcbYj5" + }, + "outputs": [], + "source": [ + "def get_metrics(metric: str = \"/train_total_loss\"):\n", + " \"\"\"Get metrics from Tensorboard.\n", + "\n", + " Args:\n", + " metric: metric name, eg. /train_total_loss or /eval_total_loss.\n", + "\n", + " Returns:\n", + " steps: list of steps.\n", + " steps_loss: list of loss values.\n", + " \"\"\"\n", + " loss_values = metrics[metric].values\n", + " steps_loss = []\n", + " steps = []\n", + " for loss in loss_values:\n", + " steps_loss.append(loss.scalar.value)\n", + " steps.append(loss.step)\n", + " return steps, steps_loss" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "ImR4doLZblaH" + }, + "outputs": [], + "source": [ + "# Get Train and Eval Loss\n", + "train_loss = get_metrics(metric=\"/train_total_loss\")\n", + "eval_loss = get_metrics(metric=\"/eval_total_loss\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "NuN-m1Ikbn15" + }, + "source": [ + "### Step7 [b]: Plot the metrics" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "1KWWkVR5jQkA" + }, + "outputs": [], + "source": [ + "# Plot the train and eval loss metrics using Plotly python library\n", + "\n", + "fig = make_subplots(\n", + " rows=1, cols=2, shared_xaxes=True, subplot_titles=(\"Train Loss\", \"Eval Loss\")\n", + ")\n", + "\n", + "# Add traces\n", + "fig.add_trace(\n", + " go.Scatter(x=train_loss[0], y=train_loss[1], name=\"Train Loss\", mode=\"lines\"),\n", + " row=1,\n", + " col=1,\n", + ")\n", + "fig.add_trace(\n", + " go.Scatter(x=eval_loss[0], y=eval_loss[1], name=\"Eval Loss\", mode=\"lines\"),\n", + " row=1,\n", + " col=2,\n", + ")\n", + "\n", + "# Add figure title\n", + "fig.update_layout(title=\"Train and Eval Loss\", xaxis_title=\"Steps\", yaxis_title=\"Loss\")\n", + "\n", + "# Set x-axis title\n", + "fig.update_xaxes(title_text=\"Steps\")\n", + "\n", + "# Set y-axes titles\n", + "fig.update_yaxes(title_text=\"Loss\")\n", + "\n", + "# Show plot\n", + "fig.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "KY-eiVk0FI-M" + }, + "source": [ + "## Step8: Load the Tuned Model\n", + "\n", + " - Load the fine-tuned model using `GenerativeModel` class with the tuning job model endpoint name.\n", + "\n", + " - Test the tuned model with the following prompt" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "GiJ831VMDQNy" + }, + "outputs": [], + "source": [ + "prompt" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "65SYYpaNT4QR" + }, + "outputs": [], + "source": [ + "if True:\n", + " # Test with the loaded model.\n", + " print(\"***Testing***\")\n", + " print(\n", + " client.models.generate_content(\n", + " model=tuned_model, contents=prompt, config=config\n", + " ).text\n", + " )\n", + "else:\n", + " print(\"State:\", tuning_job.state.name.state)\n", + " print(\"Error:\", tuning_job.state.name.error)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "d54ce2b88af3" + }, + "source": [ + "- We can clearly see the difference between summary generated pre and post tuning, as tuned summary is more inline with the ground truth format (**Note**: Pre and Post outputs, might vary based on the set parameters.)\n", + "\n", + " - *Pre*: `This article describes a method for applying lotion to your back using your forearms as applicators. By squeezing lotion onto your forearms and then reaching behind your back, you can use a windshield wiper motion to spread the lotion across your back. The method acknowledges potential limitations for those with shoulder pain or limited flexibility.`\n", + " - *Post*: `Squeeze a line of lotion on your forearm. Reach behind you and rub your back.`\n", + " - *Ground Truth*:` Squeeze a line of lotion onto the tops of both forearms and the backs of your hands. Place your arms behind your back. Move your arms in a windshield wiper motion.`" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "oYsIpFakU4CC" + }, + "source": [ + "## Step9: Evaluation post model tuning" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "mwlCcKPZ62Of" + }, + "source": [ + "
    \n", + "⚠ It will take ~5 mins for the evaluation on the provided batch. ⚠\n", + "
    " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "KrBk1amTU3r2" + }, + "outputs": [], + "source": [ + "# run evaluation\n", + "evaluation_df_post_tuning = run_evaluation(tuned_model, corpus_batch)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "ONnlEkSex-iO" + }, + "outputs": [], + "source": [ + "evaluation_df_post_tuning.head()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "xDJrlD8O0B4d" + }, + "outputs": [], + "source": [ + "evaluation_df_post_tuning_stats = evaluation_df_post_tuning.dropna().describe()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "c24-mE12y4Nm" + }, + "outputs": [], + "source": [ + "# Statistics of the evaluation dataframe post model tuning.\n", + "evaluation_df_post_tuning_stats" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "9VU-8Ql2bqlo" + }, + "outputs": [], + "source": [ + "print(\n", + " \"Mean rougeL_precision is\", evaluation_df_post_tuning_stats.rougeL_precision[\"mean\"]\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "4Q8hN7SE08-X" + }, + "source": [ + "#### Improvement" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "j0ctGzdnznYO" + }, + "outputs": [], + "source": [ + "improvement = round(\n", + " (\n", + " (\n", + " evaluation_df_post_tuning_stats.rougeL_precision[\"mean\"]\n", + " - evaluation_df_stats.rougeL_precision[\"mean\"]\n", + " )\n", + " / evaluation_df_stats.rougeL_precision[\"mean\"]\n", + " )\n", + " * 100,\n", + " 2,\n", + ")\n", + "print(\n", + " f\"Model tuning has improved the rougeL_precision by {improvement}% (result might differ based on each tuning iteration)\"\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "LQkpAMnpw-jH" + }, + "source": [ + "## Conclusion" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "Esra6YPgxBiV" + }, + "source": [ + "Performance could be further improved:\n", + "- By adding more training samples. In general, improve your training data quality and/or quantity towards getting a more diverse and comprehensive dataset for your task\n", + "- By tuning the hyperparameters, such as epochs and learning rate multiplier\n", + " - To find the optimal number of epochs for your dataset, we recommend experimenting with different values. While increasing epochs can lead to better performance, it's important to be mindful of overfitting, especially with smaller datasets. If you see signs of overfitting, reducing the number of epochs can help mitigate the issue\n", + "- You may try different prompt structures/formats and opt for the one with better performance" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "3e6fd3649040" + }, + "source": [ + "## Cleaning up" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "5528064b2cdf" + }, + "source": [ + "To clean up all Google Cloud resources used in this project, you can [delete the Google Cloud\n", + "project](https://cloud.google.com/resource-manager/docs/creating-managing-projects#shutting_down_projects) you used for the tutorial.\n", + "\n", + "\n", + "Otherwise, you can delete the individual resources you created in this tutorial.\n", + "\n", + "Refer to this [instructions](https://cloud.google.com/vertex-ai/docs/tutorials/image-classification-custom/cleanup#delete_resources) to delete the resources from console." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "c4dd0f5d2a21" + }, + "outputs": [], + "source": [ + "# Delete Experiment.\n", + "delete_experiments = True\n", + "if delete_experiments:\n", + " experiments_list = aiplatform.Experiment.list()\n", + " for experiment in experiments_list:\n", + " if experiment.resource_name == experiment_name:\n", + " print(experiment.resource_name)\n", + " experiment.delete()\n", + " break\n", + "\n", + "print(\"***\" * 10)\n", + "\n", + "# Delete Endpoint.\n", + "delete_endpoint = True\n", + "# If force is set to True, all deployed models on this\n", + "# Endpoint will be first undeployed.\n", + "if delete_endpoint:\n", + " for endpoint in aiplatform.Endpoint.list():\n", + " if endpoint.resource_name == tuned_model:\n", + " print(endpoint.resource_name)\n", + " endpoint.delete(force=True)\n", + " break\n", + "\n", + "print(\"***\" * 10)\n", + "\n", + "# Delete Cloud Storage Bucket.\n", + "delete_bucket = True\n", + "if delete_bucket:\n", + " ! gsutil -m rm -r $BUCKET_URI" + ] + } + ], + "metadata": { + "colab": { + "name": "sft_gemini_summarization.ipynb", + "toc_visible": true + }, + "kernelspec": { + "display_name": "Python 3", + "name": "python3" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} diff --git a/gemini/tuning/sft_gemini_visual_defect_detection.ipynb b/gemini/tuning/sft_gemini_visual_defect_detection.ipynb new file mode 100644 index 0000000..160ef37 --- /dev/null +++ b/gemini/tuning/sft_gemini_visual_defect_detection.ipynb @@ -0,0 +1,979 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "id": "GzMT0d7XRdQ3" + }, + "source": [ + "# Supervised Fine-tuning Gemini 2.5 Flash for Visual Defect Detection\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + "
    \n", + " \n", + " \"Google
    Open in Colab\n", + "
    \n", + "
    \n", + " \n", + " \"Google
    Open in Colab Enterprise\n", + "
    \n", + "
    \n", + " \n", + " \"Vertex
    Open in Vertex AI Workbench\n", + "
    \n", + "
    \n", + " \n", + " \"GitHub
    View on GitHub\n", + "
    \n", + "
    \n", + "\n", + "
    " + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "_uco5wDNcIRq" + }, + "source": [ + "Share to:\n", + "\n", + "\n", + " \"LinkedIn\n", + "\n", + "\n", + "\n", + " \"Bluesky\n", + "\n", + "\n", + "\n", + " \"X\n", + "\n", + "\n", + "\n", + " \"Reddit\n", + "\n", + "\n", + "\n", + " \"Facebook\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "M04y-KnqcSCq" + }, + "source": [ + "| Author |\n", + "| --- |\n", + "| [Aniket Agrawal](https://github.com/aniketagrawal2012) |" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "WURYK3ZRRdQ5" + }, + "source": [ + "## Overview\n", + "\n", + "This notebook demonstrates how to perform **supervised fine-tuning** on a Gemini model for a **visual defect detection** task within a manufacturing context. We will use the `google-genai` SDK integrated with Vertex AI to train the model to classify product images and identify flaws.\n", + "\n", + "### Use Case: Classifying Product Quality from Images\n", + "\n", + "We'll fine-tune Gemini to analyze an image of a product from a manufacturing line and classify its quality (e.g., \"Pass\", \"Defect\") and provide a short description of the issue if one is found. This is a multimodal task combining image analysis (vision) with text generation (classification and description).\n", + "\n", + "**Workflow:**\n", + "1. **Generate Data**: Create simulated product images (Pass/Defect), upload them to GCS, and create a manifest DataFrame.\n", + "2. **Prepare Tuning Data (JSONL)**: Convert image GCS URIs and corresponding labels (e.g., \"Status: Defect - Scratch detected\") into the JSON Lines format required for Gemini supervised tuning.\n", + "3. **Upload to GCS**: Store the formatted tuning data (JSONL files) in a Google Cloud Storage bucket.\n", + "4. **Launch Fine-tuning Job**: Use the `google-genai` SDK client (configured for Vertex AI) to start the supervised tuning job.\n", + "5. **Monitor Job**: Track the progress of the fine-tuning job.\n", + "6. **Evaluate Tuned Model**: Make predictions on new product images using the fine-tuned model endpoint and compare qualitatively.\n", + "7. **Integrate Gemini for Reporting**: Use a base Gemini model to summarize the tuning job results." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "NTmKZlmIRdQ5" + }, + "source": [ + "## Setup\n", + "\n", + "### Install required packages" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "PdrVBfYMRdQ5" + }, + "outputs": [], + "source": [ + "import sys # noqa: F401\n", + "\n", + "# Install necessary libraries\n", + "# gcsfs is added to allow pandas to write directly to GCS\n", + "# Pillow (PIL) is needed for image generation\n", + "!{sys.executable} -m pip install --upgrade --user --quiet pandas numpy google-cloud-aiplatform google-genai google-cloud-storage gcsfs Pillow" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "Yqy4-3cBRdQ6" + }, + "source": [ + "**⚠ Important:** Restart the kernel after installation." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "KS9rUufMRdQ6" + }, + "source": [ + "### Authenticate and Initialize Vertex AI\n", + "\n", + "Set your project, region, and GCS bucket information. We configure the notebook for Vertex AI fine-tuning and reporting." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "PyQnTYGlRdQ6" + }, + "outputs": [], + "source": [ + "import subprocess\n", + "\n", + "import vertexai\n", + "from google.genai import (\n", + " Client as VertexClient, # This is for Vertex AI tuning/models client\n", + ")\n", + "\n", + "# --- Vertex AI Configuration (Required for Fine-tuning Job) ---\n", + "PROJECT_ID = \"\" # @param {type: \"string\", placeholder: \"your-gcp-project-id\"}\n", + "REGION = \"\" # @param {type:\"string\"}\n", + "BUCKET_NAME = \"\" # @param {type:\"string\", placeholder: \"your-gcs-bucket-name\"}\n", + "BUCKET_URI = f\"gs://{BUCKET_NAME}\"\n", + "\n", + "\n", + "def ensure_gcs_bucket_exists(project_id: str, region: str, bucket_uri: str) -> None:\n", + " \"\"\"Ensures the specified GCS bucket exists, creating it if necessary.\"\"\"\n", + " print(f\"Checking/Creating bucket: {bucket_uri}\")\n", + "\n", + " # 1. Check if bucket exists. If yes, return immediately (Guard Clause).\n", + " try:\n", + " subprocess.run(\n", + " [\"gsutil\", \"ls\", bucket_uri],\n", + " check=True,\n", + " stdout=subprocess.PIPE,\n", + " stderr=subprocess.PIPE,\n", + " )\n", + " print(f\"Bucket {bucket_uri} already exists.\")\n", + " return\n", + " except subprocess.CalledProcessError:\n", + " # Fall through only if check failed\n", + " print(f\"Bucket {bucket_uri} not found. Attempting to create it.\")\n", + "\n", + " # 2. Create bucket (No longer indented inside an 'else' or 'except' block)\n", + " try:\n", + " subprocess.run(\n", + " [\"gsutil\", \"mb\", \"-l\", region, \"-p\", project_id, bucket_uri],\n", + " check=True,\n", + " stdout=subprocess.PIPE,\n", + " stderr=subprocess.PIPE,\n", + " )\n", + " print(f\"Bucket {bucket_uri} created successfully.\")\n", + " except subprocess.CalledProcessError as e:\n", + " error_msg = (\n", + " f\"Failed to create bucket {bucket_uri}. Error: {e.stderr.decode().strip()}\"\n", + " )\n", + " raise ValueError(error_msg) from e\n", + "\n", + "\n", + "# --- Authentication (Colab/Workbench for Vertex AI) ---\n", + "if not PROJECT_ID or PROJECT_ID == \"\":\n", + " try:\n", + " from google.colab import auth\n", + "\n", + " auth.authenticate_user()\n", + "\n", + " PROJECT_ID = (\n", + " subprocess.check_output([\"gcloud\", \"config\", \"get-value\", \"project\"])\n", + " .decode(\"utf-8\")\n", + " .strip()\n", + " )\n", + " print(f\"Retrieved Project ID: {PROJECT_ID}\")\n", + " except Exception as e:\n", + " print(\n", + " f\"Could not automatically retrieve Project ID. Please set it manually. Error: {e}\"\n", + " )\n", + "\n", + "# Ensure BUCKET_NAME is set, and attempt to create the bucket\n", + "if not BUCKET_NAME or BUCKET_NAME == \"\":\n", + " if PROJECT_ID:\n", + " BUCKET_NAME = f\"{PROJECT_ID}-gemini-tuning-bucket\"\n", + " BUCKET_URI = f\"gs://{BUCKET_NAME}\"\n", + " print(f\"Bucket name not provided. Using default: {BUCKET_NAME}\")\n", + " else:\n", + " raise ValueError(\n", + " \"Please provide a valid GCS Bucket name or ensure PROJECT_ID is set for default bucket creation.\"\n", + " )\n", + "\n", + "ensure_gcs_bucket_exists(PROJECT_ID, REGION, BUCKET_URI)\n", + "\n", + "if PROJECT_ID:\n", + " print(\n", + " f\"Initializing Vertex AI for project: {PROJECT_ID} in {REGION} using bucket {BUCKET_URI}\"\n", + " )\n", + " # Initialize Vertex AI SDK (needed for launching the tuning job)\n", + " vertexai.init(project=PROJECT_ID, location=REGION, staging_bucket=BUCKET_URI)\n", + " # Initialize the genai client specifically for Vertex AI operations (like tuning)\n", + " vertex_client = VertexClient(vertexai=True, project=PROJECT_ID, location=REGION)\n", + " print(\"Vertex AI SDK Initialized.\")\n", + "else:\n", + " raise ValueError(\"PROJECT_ID must be set for Vertex AI operations.\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "fQ3ECMSARdQ6" + }, + "source": [ + "### Imports and Global Configuration" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "bV74HX1oRdQ6" + }, + "outputs": [], + "source": [ + "import io\n", + "import json\n", + "import random\n", + "import time\n", + "import warnings\n", + "from typing import Any\n", + "\n", + "import numpy as np\n", + "import pandas as pd\n", + "from PIL import Image, ImageDraw\n", + "from google.cloud import storage\n", + "from google.genai import types as genai_types\n", + "\n", + "# --- Global Settings ---\n", + "warnings.filterwarnings(\"ignore\", category=UserWarning)\n", + "warnings.filterwarnings(\"ignore\", category=FutureWarning)\n", + "np.random.seed(42)\n", + "random.seed(42)\n", + "\n", + "# --- Constants ---\n", + "BASE_MODEL_ID = \"gemini-2.5-flash\" # Tunable model ID on Vertex AI\n", + "TUNED_MODEL_DISPLAY_NAME = f\"visual-defect-gemini-tuned-{int(time.time())}\"\n", + "DATA_DIR_GCS = f\"{BUCKET_URI}/visual_defect_tuning_data\"\n", + "IMAGE_DIR_GCS_PATH = \"visual_defect_tuning_data/images\" # Relative path for client\n", + "IMAGE_DIR_GCS_URI = f\"{DATA_DIR_GCS}/images\"\n", + "TRAIN_JSONL_GCS_URI = f\"{DATA_DIR_GCS}/train_data.jsonl\"\n", + "VALIDATION_JSONL_GCS_URI = f\"{DATA_DIR_GCS}/validation_data.jsonl\"\n", + "TEST_JSONL_GCS_URI = f\"{DATA_DIR_GCS}/test_data.jsonl\" # For qualitative eval later\n", + "\n", + "print(f\"Base model for tuning: {BASE_MODEL_ID}\")\n", + "print(f\"Tuning data GCS path: {DATA_DIR_GCS}\")\n", + "print(f\"Image GCS URI: {IMAGE_DIR_GCS_URI}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "TEUFLjeYRdQ7" + }, + "source": [ + "## Step 1: Generate Simulated Image Data\n", + "\n", + "Instead of loading data, we'll generate simulated product images (simple shapes) and upload them to GCS. We'll create 'Pass' images (clean) and 'Defect' images (with a visual flaw). We will return a Pandas DataFrame acting as a manifest file." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "8RqK0Td6RdQ7" + }, + "outputs": [], + "source": [ + "def _upload_image_blob(bucket, blob_name: str, image: Image.Image) -> bool:\n", + " \"\"\"Helper to safely upload a PIL image to GCS.\"\"\"\n", + " try:\n", + " blob = bucket.blob(blob_name)\n", + " with io.BytesIO() as output:\n", + " image.save(output, format=\"PNG\")\n", + " blob.upload_from_string(output.getvalue(), content_type=\"image/png\")\n", + " return True\n", + " except Exception as e:\n", + " print(f\"Failed to upload {blob_name}: {e}\")\n", + " return False\n", + "\n", + "\n", + "def generate_and_upload_images(\n", + " bucket_name: str,\n", + " gcs_image_path: str,\n", + " num_images: int = 100,\n", + " defect_ratio: float = 0.4,\n", + ") -> pd.DataFrame:\n", + " \"\"\"Generates simple images, uploads to GCS, and returns a manifest DataFrame.\"\"\"\n", + " print(f\"Generating {num_images} simulated images...\")\n", + " storage_client = storage.Client(project=PROJECT_ID)\n", + " bucket = storage_client.bucket(bucket_name)\n", + " manifest = []\n", + "\n", + " for i in range(num_images):\n", + " img = Image.new(\"RGB\", (100, 100), color=\"#DDDDDD\")\n", + " draw = ImageDraw.Draw(img)\n", + " draw.rectangle((20, 20, 80, 80), fill=\"#5555AA\") # Main product shape\n", + "\n", + " is_defect = random.random() < defect_ratio\n", + " image_name = f\"product_image_{i}.png\"\n", + " gcs_blob_name = f\"{gcs_image_path}/{image_name}\"\n", + " gcs_uri = f\"gs://{bucket_name}/{gcs_blob_name}\"\n", + " defect_type = \"None\"\n", + " label = \"Status: Pass\"\n", + "\n", + " if is_defect:\n", + " # Add a random defect\n", + " defect_type = random.choice([\"Scratch\", \"Crack\", \"Discoloration\"])\n", + " if defect_type == \"Scratch\":\n", + " draw.line((30, 30, 70, 70), fill=\"#FF3333\", width=2)\n", + " label = \"Status: Defect - Scratch detected on product surface.\"\n", + " elif defect_type == \"Crack\":\n", + " draw.line((30, 50, 70, 45), fill=\"#FFFFFF\", width=3)\n", + " label = \"Status: Defect - Crack identified in main body.\"\n", + " elif defect_type == \"Discoloration\":\n", + " draw.ellipse((55, 55, 75, 75), fill=\"#44AA44\")\n", + " label = \"Status: Defect - Discoloration spot found.\"\n", + "\n", + " # Upload to GCS\n", + " if not _upload_image_blob(bucket, gcs_blob_name, img):\n", + " continue\n", + "\n", + " manifest.append(\n", + " {\n", + " \"image_name\": image_name,\n", + " \"gcs_uri\": gcs_uri,\n", + " \"status\": \"Defect\" if is_defect else \"Pass\",\n", + " \"defect_type\": defect_type,\n", + " \"label\": label,\n", + " }\n", + " )\n", + "\n", + " if (i + 1) % 20 == 0:\n", + " print(f\" ...generated and uploaded {i + 1}/{num_images} images.\")\n", + "\n", + " print(f\"Image generation complete. Uploaded {len(manifest)} images.\")\n", + " return pd.DataFrame(manifest)\n", + "\n", + "\n", + "# Generate data (e.g., 200 samples for this demo)\n", + "# For a real project, you'd need many more (100s or 1000s)\n", + "image_manifest_df = generate_and_upload_images(\n", + " bucket_name=BUCKET_NAME,\n", + " gcs_image_path=IMAGE_DIR_GCS_PATH,\n", + " num_images=200,\n", + " defect_ratio=0.5,\n", + ")\n", + "\n", + "print(\"\\n--- Image Manifest Sample ---\")\n", + "print(image_manifest_df.head())" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "9jbGD8gFRdQ7" + }, + "source": [ + "## Step 2: Prepare Tuning Data (JSONL)\n", + "\n", + "We convert the manifest DataFrame into the required JSON Lines format. Each line will contain a **multimodal prompt** (text + image) and the expected completion (the classification label)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "w9pXnVJARdQ7" + }, + "outputs": [], + "source": [ + "def _build_tuning_example(prompt: str, image_uri: str, label: str) -> dict:\n", + " \"\"\"Constructs the dictionary for a single tuning example.\"\"\"\n", + " return {\n", + " \"contents\": [\n", + " {\n", + " \"role\": \"user\",\n", + " \"parts\": [\n", + " {\"text\": prompt},\n", + " {\"fileData\": {\"mimeType\": \"image/png\", \"fileUri\": image_uri}},\n", + " ],\n", + " },\n", + " {\"role\": \"model\", \"parts\": [{\"text\": label}]},\n", + " ]\n", + " }\n", + "\n", + "\n", + "def create_tuning_jsonl_from_manifest(\n", + " manifest_df: pd.DataFrame,\n", + ") -> list[dict[str, Any]]:\n", + " \"\"\"Creates JSONL data for Gemini multimodal supervised tuning.\"\"\"\n", + " print(\"\\n--- Preparing JSONL Tuning Data ---\")\n", + "\n", + " jsonl_data = []\n", + " base_prompt = \"Analyze the following product image for manufacturing defects. Classify its status as 'Pass' or 'Defect' and provide a brief description if a defect is present.\"\n", + "\n", + " for _, row in manifest_df.iterrows():\n", + " image_uri = row[\"gcs_uri\"]\n", + " target_label = row[\"label\"]\n", + "\n", + " if not image_uri or pd.isna(image_uri):\n", + " print(\"Skipping row with missing image URI.\")\n", + " continue\n", + "\n", + " # Format according to Gemini multimodal tuning requirements\n", + " # The 'user' role contains both the text prompt and the image file data.\n", + " instance = _build_tuning_example(base_prompt, image_uri, target_label)\n", + " jsonl_data.append(instance)\n", + "\n", + " print(f\"Generated {len(jsonl_data)} JSONL instances.\")\n", + " return jsonl_data\n", + "\n", + "\n", + "# Create JSONL data\n", + "tuning_data_jsonl = create_tuning_jsonl_from_manifest(image_manifest_df)\n", + "\n", + "# Shuffle and Split data\n", + "if tuning_data_jsonl:\n", + " random.shuffle(tuning_data_jsonl)\n", + " # Using 80% train, 10% validation, 10% test split\n", + " split_idx_val = int(len(tuning_data_jsonl) * 0.8)\n", + " split_idx_test = int(len(tuning_data_jsonl) * 0.9)\n", + "\n", + " train_split = tuning_data_jsonl[:split_idx_val]\n", + " validation_split = tuning_data_jsonl[split_idx_val:split_idx_test]\n", + " test_split = tuning_data_jsonl[split_idx_test:]\n", + "\n", + " print(\n", + " f\"Split sizes: Train={len(train_split)}, Validation={len(validation_split)}, Test={len(test_split)}\"\n", + " )\n", + "\n", + " # Display a sample\n", + " print(\"\\n--- Sample JSONL Instance ---\")\n", + " print(json.dumps(train_split[0], indent=2))\n", + "else:\n", + " print(\"\\nWarning: No tuning data generated.\")\n", + " # Initialize splits as empty lists to prevent errors later\n", + " train_split, validation_split, test_split = [], [], []" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "-OARmnqbRdQ7" + }, + "source": [ + "## Step 3: Upload Tuning Data to GCS\n", + "\n", + "The fine-tuning service reads data directly from Google Cloud Storage." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "sb9OuMrjRdQ7" + }, + "outputs": [], + "source": [ + "import google.auth\n", + "\n", + "def save_jsonl_to_gcs(instances: list[dict[str, Any]], gcs_uri: str) -> None:\n", + " \"\"\"Saves a list of dictionaries as a JSONL file to GCS using Pandas.\"\"\"\n", + " if not instances:\n", + " print(f\"No instances to upload to {gcs_uri}. Skipping upload.\")\n", + " return\n", + "\n", + " print(f\"Uploading {len(instances)} instances to {gcs_uri}...\")\n", + "\n", + " try:\n", + " # Get the application default credentials\n", + " credentials, _ = google.auth.default()\n", + "\n", + " # Convert list of dicts to DataFrame\n", + " df = pd.DataFrame(instances)\n", + "\n", + " # Save DataFrame to GCS as JSONL\n", + " # We MUST pass the 'token' (credentials) to authenticate the request\n", + " storage_options = {\"project\": PROJECT_ID, \"token\": credentials}\n", + "\n", + " df.to_json(\n", + " gcs_uri, orient=\"records\", lines=True, storage_options=storage_options\n", + " )\n", + "\n", + " print(\"Upload complete.\")\n", + " except Exception as e:\n", + " print(f\"ERROR during GCS upload to {gcs_uri}: {e}\")\n", + " print(\n", + " \"Please ensure your GCS bucket is accessible and pandas has GCS permissions (installed via gcsfs).\"\n", + " )\n", + "\n", + "\n", + "# Save splits to GCS\n", + "save_jsonl_to_gcs(train_split, TRAIN_JSONL_GCS_URI)\n", + "save_jsonl_to_gcs(validation_split, VALIDATION_JSONL_GCS_URI)\n", + "save_jsonl_to_gcs(\n", + " test_split, TEST_JSONL_GCS_URI\n", + ") # Save test split for later evaluation" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "bPa8ylyURdQ8" + }, + "source": [ + "## Step 4: Launch Fine-tuning Job\n", + "\n", + "We use the `google-genai` client **configured for Vertex AI** (`vertex_client`) to start the supervised tuning job, as fine-tuning management is a Vertex AI feature." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "HF9z_1k3RdQ8" + }, + "outputs": [], + "source": [ + "TUNING_JOB_NAME = None # Initialize\n", + "if not train_split or not validation_split:\n", + " print(\"Skipping fine-tuning job launch as training or validation data is empty.\")\n", + "else:\n", + " print(f\"Starting supervised fine-tuning job for model: {BASE_MODEL_ID}\")\n", + " print(f\"Tuned model display name: {TUNED_MODEL_DISPLAY_NAME}\")\n", + "\n", + " training_dataset = {\n", + " \"gcs_uri\": TRAIN_JSONL_GCS_URI,\n", + " }\n", + "\n", + " validation_dataset = genai_types.TuningValidationDataset(\n", + " gcs_uri=VALIDATION_JSONL_GCS_URI\n", + " )\n", + "\n", + " try:\n", + " # Use the vertex_client configured specifically for Vertex AI operations\n", + " sft_tuning_job = vertex_client.tunings.tune(\n", + " base_model=BASE_MODEL_ID,\n", + " training_dataset=training_dataset,\n", + " config=genai_types.CreateTuningJobConfig(\n", + " adapter_size=\"ADAPTER_SIZE_FOUR\", # Smaller adapter for faster tuning\n", + " epoch_count=5, # Increased epochs for better specialization\n", + " tuned_model_display_name=TUNED_MODEL_DISPLAY_NAME,\n", + " validation_dataset=validation_dataset,\n", + " ),\n", + " )\n", + " print(\"\\nTuning job created:\")\n", + " print(sft_tuning_job)\n", + " TUNING_JOB_NAME = sft_tuning_job.name # Save for monitoring\n", + "\n", + " except Exception as e:\n", + " print(f\"ERROR starting tuning job: {e}\")\n", + " # Attempt to list existing jobs with the same display name in case of interruption\n", + " try:\n", + " print(\n", + " f\"Checking for existing tuning jobs named '{TUNED_MODEL_DISPLAY_NAME}'...\"\n", + " )\n", + " existing_jobs = vertex_client.tunings.list(\n", + " page_size=100\n", + " ) # List might need pagination for many jobs\n", + " for job in existing_jobs:\n", + " # Check if config exists and has the attribute\n", + " job_config = getattr(job, \"config\", None)\n", + " if (\n", + " job_config\n", + " and getattr(job_config, \"tuned_model_display_name\", None)\n", + " == TUNED_MODEL_DISPLAY_NAME\n", + " ):\n", + " print(f\"Found existing job: {job.name} with state {job.state}\")\n", + " TUNING_JOB_NAME = job.name # Use the existing job name\n", + " break\n", + " except Exception as list_e:\n", + " print(f\"Could not list existing tuning jobs: {list_e}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "VV8iimM8RdQ8" + }, + "source": [ + "**Note:** Fine-tuning can take a significant amount of time (potentially 30 minutes to several hours depending on the dataset size, base model, and adapter size)." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "Vs9s8uLRRdQ8" + }, + "source": [ + "## Step 5: Monitor Job" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "9mVFYfL2RdQ8" + }, + "outputs": [], + "source": [ + "def monitor_tuning_job(job_name: str) -> Any:\n", + " \"\"\"Polls the tuning job until it reaches a terminal state.\"\"\"\n", + " print(f\"Monitoring tuning job: {job_name}\")\n", + " running_states = {\n", + " genai_types.JobState.JOB_STATE_PENDING,\n", + " genai_types.JobState.JOB_STATE_RUNNING,\n", + " }\n", + "\n", + " while True:\n", + " try:\n", + " tuning_job = vertex_client.tunings.get(name=job_name)\n", + " except Exception as e:\n", + " print(f\"Error polling status: {e}. Retrying in 60s...\")\n", + " time.sleep(60)\n", + " continue\n", + "\n", + " if tuning_job.state not in running_states:\n", + " return tuning_job\n", + "\n", + " state_name = str(tuning_job.state).split(\".\")[-1]\n", + " print(f\" Current state: {state_name}...\")\n", + " time.sleep(60)\n", + "\n", + "\n", + "TUNED_MODEL_ENDPOINT = None\n", + "\n", + "if TUNING_JOB_NAME:\n", + " final_job = monitor_tuning_job(TUNING_JOB_NAME)\n", + "\n", + " final_state_name = str(final_job.state).split(\".\")[-1]\n", + " print(f\"\\nTuning job finished with state: {final_state_name}\")\n", + "\n", + " if final_job.state == genai_types.JobState.JOB_STATE_SUCCEEDED:\n", + " if hasattr(final_job, \"tuned_model\") and final_job.tuned_model.endpoint:\n", + " TUNED_MODEL_ENDPOINT = final_job.tuned_model.endpoint\n", + " print(f\"Tuned model endpoint ready: {TUNED_MODEL_ENDPOINT}\")\n", + " else:\n", + " print(\"Warning: Job succeeded but endpoint not found.\")\n", + " else:\n", + " error_msg = getattr(final_job, \"error\", \"Unknown error\")\n", + " print(f\"Tuning job failed. Error: {error_msg}\")\n", + "else:\n", + " print(\"Skipping monitoring...\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "JeiZwXsMRdQ8" + }, + "source": [ + "## Step 6: Evaluate Tuned Model (Qualitative)\n", + "\n", + "We take a sample from our test set (which the model hasn't seen during tuning) and send the multimodal prompt (text + image) to the tuned endpoint to compare the prediction to the expected output." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "0fG27PjrRdQ8" + }, + "outputs": [], + "source": [ + "import re # <-- Import regex module\n", + "\n", + "from google.genai import types as genai_types # Ensure genai_types is available\n", + "\n", + "def _extract_predicted_text(response: Any) -> str:\n", + " \"\"\"Safely extracts the predicted text using guard clauses.\"\"\"\n", + " if not response:\n", + " return \"(Response is None)\"\n", + "\n", + " # 1. Check simplified text attribute\n", + " if hasattr(response, \"text\") and response.text:\n", + " return response.text.strip()\n", + "\n", + " # 2. Validate candidates exist\n", + " if not hasattr(response, \"candidates\") or not response.candidates:\n", + " return \"(No candidates found)\"\n", + "\n", + " first_candidate = response.candidates[0]\n", + "\n", + " # 3. Validate finish reason\n", + " finish_reason = getattr(first_candidate, \"finish_reason\", None)\n", + " if finish_reason != genai_types.FinishReason.STOP:\n", + " return f\"(Generation stopped: {finish_reason})\"\n", + "\n", + " # 4. Validate content parts\n", + " if not (hasattr(first_candidate, \"content\") and first_candidate.content.parts):\n", + " return \"(No content parts)\"\n", + "\n", + " return first_candidate.content.parts[0].text.strip()\n", + "\n", + "\n", + "def evaluate_qualitatively(\n", + " tuned_endpoint: str, test_data: list[dict[str, Any]], num_samples: int = 3\n", + ") -> None:\n", + " \"\"\"Makes predictions with the tuned model and prints comparisons.\"\"\"\n", + " if not tuned_endpoint:\n", + " print(\"Tuned model endpoint not available. Skipping evaluation.\")\n", + " return\n", + "\n", + " if not test_data:\n", + " print(\"No test data available for evaluation.\")\n", + " return\n", + "\n", + " print(f\"\\n--- Qualitative Evaluation of Tuned Model ({tuned_endpoint}) ---\")\n", + "\n", + " # Select random samples from the test set\n", + " samples = random.sample(test_data, min(num_samples, len(test_data)))\n", + "\n", + " for i, sample in enumerate(samples):\n", + " print(f\"\\n--- Sample {i + 1} ---\")\n", + " # Ensure the sample structure is correct\n", + " try:\n", + " # Extract multimodal prompt parts\n", + " user_parts = sample[\"contents\"][0][\"parts\"]\n", + " expected_output = sample[\"contents\"][1][\"parts\"][0][\"text\"]\n", + "\n", + " # Reconstruct the text and image parts for the prompt\n", + " prompt_text_part = user_parts[0][\"text\"]\n", + " image_file_part = user_parts[1][\"fileData\"]\n", + " image_uri = image_file_part[\"fileUri\"]\n", + " image_mime = image_file_part[\"mimeType\"]\n", + "\n", + " except (KeyError, IndexError, TypeError) as e:\n", + " print(f\"Skipping sample due to unexpected format: {e}\")\n", + " print(f\"Problematic sample: {sample}\")\n", + " continue\n", + "\n", + " print(f\"Input Prompt Text: {prompt_text_part}\")\n", + " print(f\"Input Image URI: {image_uri}\")\n", + " print(f\"\\nExpected Output: {expected_output}\")\n", + "\n", + " try:\n", + " # Prepare contents for prediction (both text and image parts)\n", + " prediction_contents = [\n", + " {\n", + " \"role\": \"user\",\n", + " \"parts\": [\n", + " {\"text\": prompt_text_part},\n", + " {\"fileData\": {\"mimeType\": image_mime, \"fileUri\": image_uri}},\n", + " ],\n", + " }\n", + " ]\n", + "\n", + " # Use the vertex_client for predictions against the tuned endpoint\n", + " response = vertex_client.models.generate_content(\n", + " model=tuned_endpoint,\n", + " contents=prediction_contents,\n", + " config={\n", + " \"temperature\": 0.1, # Low temperature for deterministic classification\n", + " \"max_output_tokens\": 2000,\n", + " },\n", + " )\n", + "\n", + " # Safely access predicted text\n", + " predicted_output = _extract_predicted_text(response)\n", + "\n", + " print(f\"Predicted Output: {predicted_output}\")\n", + "\n", + " # --- NEW REGEX EVALUATION LOGIC ---\n", + " result_str = \"MISMATCH\" # Default\n", + "\n", + " if expected_output == \"Status: Pass\":\n", + " if predicted_output == \"Status: Pass\":\n", + " result_str = \"MATCH\"\n", + " elif \"Defect\" in expected_output:\n", + " # 1. Check if prediction also says \"Defect\"\n", + " if \"Defect\" not in predicted_output:\n", + " result_str = \"MISMATCH (Predicted 'Pass' for 'Defect')\"\n", + " else:\n", + " # 2. Check if the key defect word is present\n", + " key_defect_match = re.search(\n", + " r\"(Scratch|Crack|Discoloration)\", expected_output\n", + " )\n", + " if key_defect_match:\n", + " defect_type = key_defect_match.group(1)\n", + " # Check if the predicted string contains the defect type (case-insensitive)\n", + " if re.search(defect_type, predicted_output, re.IGNORECASE):\n", + " result_str = \"MATCH (Regex)\"\n", + " else:\n", + " result_str = f\"MISMATCH (Missing key defect: {defect_type})\"\n", + " else:\n", + " # Fallback if it's a defect but not one of the known types\n", + " result_str = \"MATCH (Regex - 'Defect' present)\"\n", + "\n", + " print(f\"Result: {result_str}\")\n", + " # --- END OF NEW LOGIC ---\n", + "\n", + " except Exception as e:\n", + " print(f\"ERROR during prediction for sample {i + 1}: {e}\")\n", + "\n", + "\n", + "# Run qualitative evaluation (only if tuning succeeded and test data exists)\n", + "evaluate_qualitatively(TUNED_MODEL_ENDPOINT, test_split)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "jW2wMC1yRdQ8" + }, + "source": [ + "## Step 7: Integrating Gemini for Reporting (Using Base Model)\n", + "\n", + "We can use a base Gemini model (accessed via Vertex AI) to summarize the fine-tuning job itself." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "WZ5N1MAQRdQ8" + }, + "outputs": [], + "source": [ + "def generate_tuning_summary_with_gemini(tuning_job_details: Any) -> None:\n", + " \"\"\"Generates a summary of the tuning job using the Gemini API.\"\"\"\n", + " print(\"\\n--- Generating Tuning Job Summary with Gemini ---\")\n", + "\n", + " if not tuning_job_details:\n", + " print(\"No tuning job details provided. Skipping summary.\")\n", + " return\n", + "\n", + " # We will use the Vertex AI client, which is already initialized.\n", + " model_name_for_vertex_ai = \"gemini-2.5-flash\" # Use a standard model for reporting\n", + " reporting_client = None\n", + "\n", + " try:\n", + " # This uses the high-level vertexai SDK for base model generation\n", + " # Correctly import GenerativeModel from vertexai.preview.generative_models\n", + " from vertexai.preview.generative_models import GenerativeModel\n", + "\n", + " reporting_client = GenerativeModel(model_name_for_vertex_ai)\n", + " print(f\"Using Vertex AI model ({model_name_for_vertex_ai}) for reporting.\")\n", + " except Exception as e:\n", + " print(\n", + " f\"Failed to initialize Vertex AI client for reporting with {model_name_for_vertex_ai}: {e}\"\n", + " )\n", + " print(\"Skipping summary generation.\")\n", + " return\n", + "\n", + " try:\n", + " # Extract relevant details safely\n", + " job_name = getattr(tuning_job_details, \"name\", \"N/A\")\n", + " job_state_enum = getattr(\n", + " tuning_job_details, \"state\", genai_types.JobState.JOB_STATE_UNSPECIFIED\n", + " ) # Default to unspecified\n", + " job_state = str(job_state_enum).split(\".\")[\n", + " -1\n", + " ] # Get 'SUCCEEDED', 'FAILED', etc.\n", + " base_model = getattr(tuning_job_details, \"base_model\", \"N/A\")\n", + " tuned_model_obj = getattr(tuning_job_details, \"tuned_model\", None)\n", + " tuned_endpoint = (\n", + " getattr(tuned_model_obj, \"endpoint\", \"N/A\") if tuned_model_obj else \"N/A\"\n", + " )\n", + " error_obj = getattr(tuning_job_details, \"error\", None)\n", + " error_message = str(error_obj) if error_obj else \"None\"\n", + " config_obj = getattr(tuning_job_details, \"config\", None)\n", + " display_name = (\n", + " getattr(config_obj, \"tuned_model_display_name\", \"N/A\")\n", + " if config_obj\n", + " else \"N/A\"\n", + " )\n", + "\n", + " prompt = f\"\"\"Generate a brief status report for a Gemini model fine-tuning job for a 'Visual Defect Detection' use case.\n", + " Job Name: {job_name}\n", + " Base Model: {base_model}\n", + " Tuned Model Display Name: {display_name}\n", + " Final Status: {job_state}\n", + " Tuned Model Endpoint: {tuned_endpoint}\n", + " Error (if any): {error_message}\n", + "\n", + " Summarize the outcome of this tuning job in 1-2 sentences, specifically mentioning its readiness for the manufacturing defect analysis task.\"\"\"\n", + "\n", + " print(\"\\nSending request to Gemini...\")\n", + " # Use the selected reporting_client (Vertex AI based)\n", + " response = reporting_client.generate_content(prompt)\n", + "\n", + " print(\"\\n--- Gemini Tuning Job Summary ---\")\n", + " # Handle potential response variations\n", + " response_text = _extract_predicted_text(response)\n", + " print(response_text)\n", + " print(\"---------------------------------\")\n", + "\n", + " except Exception as e:\n", + " print(f\"\\nERROR: Failed to generate Gemini summary: {e}\")\n", + " if (\n", + " \"permission denied\" in str(e).lower()\n", + " or \"consumer project\" in str(e).lower()\n", + " ):\n", + " print(\n", + " \"Please ensure the Vertex AI API is enabled in your project and the runtime environment has the correct permissions.\"\n", + " )\n", + " else:\n", + " print(\n", + " \"Please check your Vertex AI setup, model name, and network connection.\"\n", + " )\n", + "\n", + "\n", + "# Get the final job details again using the vertex_client (which manages tuning)\n", + "final_tuning_job = None\n", + "if TUNING_JOB_NAME:\n", + " try:\n", + " # Use vertex_client to get the job status\n", + " final_tuning_job = vertex_client.tunings.get(name=TUNING_JOB_NAME)\n", + " except Exception as e:\n", + " print(f\"Error retrieving final tuning job details: {e}\")\n", + "\n", + "# Generate the summary using the Vertex Gemini client\n", + "generate_tuning_summary_with_gemini(final_tuning_job)" + ] + } + ], + "metadata": { + "colab": { + "name": "sft_gemini_visual_defect_detection.ipynb", + "toc_visible": true + }, + "kernelspec": { + "display_name": "Python 3", + "name": "python3" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} diff --git a/gemini/url-context/intro_url_context.ipynb b/gemini/url-context/intro_url_context.ipynb new file mode 100644 index 0000000..a4ec0a8 --- /dev/null +++ b/gemini/url-context/intro_url_context.ipynb @@ -0,0 +1,563 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "sqi5B7V_Rjim" + }, + "outputs": [], + "source": [ + "# Copyright 2025 Google LLC\n", + "#\n", + "# Licensed under the Apache License, Version 2.0 (the \"License\");\n", + "# you may not use this file except in compliance with the License.\n", + "# You may obtain a copy of the License at\n", + "#\n", + "# https://www.apache.org/licenses/LICENSE-2.0\n", + "#\n", + "# Unless required by applicable law or agreed to in writing, software\n", + "# distributed under the License is distributed on an \"AS IS\" BASIS,\n", + "# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n", + "# See the License for the specific language governing permissions and\n", + "# limitations under the License." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "VyPmicX9RlZX" + }, + "source": [ + "# Intro to Url Context\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + "
    \n", + " \n", + " \"Google
    Open in Colab\n", + "
    \n", + "
    \n", + " \n", + " \"Google
    Open in Colab Enterprise\n", + "
    \n", + "
    \n", + " \n", + " \"Vertex
    Open in Vertex AI Workbench\n", + "
    \n", + "
    \n", + " \n", + " \"GitHub
    View on GitHub\n", + "
    \n", + "
    \n", + "\n", + "
    \n", + "\n", + "Share to:\n", + "\n", + "\n", + " \"LinkedIn\n", + "\n", + "\n", + "\n", + " \"Bluesky\n", + "\n", + "\n", + "\n", + " \"X\n", + "\n", + "\n", + "\n", + " \"Reddit\n", + "\n", + "\n", + "\n", + " \"Facebook\n", + "" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "8MqT58L6Rm_q" + }, + "source": [ + "| Author |\n", + "| --- |\n", + "| [Eric Dong](https://github.com/gericdong) |" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "nVxnv1D5RoZw" + }, + "source": [ + "## Overview\n", + "\n", + "The URL context tool in the Gemini API allows you to include URLs as context for your prompts. The model can then use the content from these URLs to generate more informed and relevant responses.\n", + "\n", + "### Objectives\n", + "\n", + "This tutorial provides a guide to using the URL context in the Gemini API.\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "gPiTOAHURvTM" + }, + "source": [ + "## Getting Started" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "CHRZUpfWSEpp" + }, + "source": [ + "### Install the Google Gen AI SDK for Python\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "sG3_LKsWSD3A" + }, + "outputs": [], + "source": [ + "%pip install --upgrade --quiet google-genai" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "HlMVjiAWSMNX" + }, + "source": [ + "### Authenticate your notebook environment\n", + "\n", + "If you are running this notebook on Google Colab, run the cell below to authenticate your environment." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "12fnq4V0SNV3" + }, + "outputs": [], + "source": [ + "import sys\n", + "\n", + "if \"google.colab\" in sys.modules:\n", + " from google.colab import auth\n", + "\n", + " auth.authenticate_user()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "be18ac9c5ec8" + }, + "source": [ + "### Authenticate to Vertex AI on Google Cloud\n", + "\n", + "You'll need to set up authentication by choosing **one** of the following methods:\n", + "\n", + "1. **Use a Google Cloud Project:** (Recommended for most users)\n", + " - See instructions [Set up a project and development environment](https://cloud.google.com/vertex-ai/docs/start/cloud-environment)\n", + "2. **Use a Vertex AI API Key (Express Mode):** For quick experimentation.\n", + " - [Get an API Key](https://cloud.google.com/vertex-ai/generative-ai/docs/start/express-mode/overview)\n", + " - See tutorial [Getting started with Gemini using Vertex AI in Express Mode](https://github.com/GoogleCloudPlatform/generative-ai/blob/main/gemini/getting-started/intro_gemini_express.ipynb).\n", + "\n", + "This tutorial uses a Google Cloud Project for authentication." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "6wXh1aH7BlPl" + }, + "outputs": [], + "source": [ + "import os\n", + "\n", + "# fmt: off\n", + "PROJECT_ID = \"[your-project-id]\" # @param {type: \"string\", placeholder: \"[your-project-id]\", isTemplate: true}\n", + "# fmt: on\n", + "if not PROJECT_ID or PROJECT_ID == \"[your-project-id]\":\n", + " PROJECT_ID = str(os.environ.get(\"GOOGLE_CLOUD_PROJECT\"))\n", + "\n", + "LOCATION = \"global\"" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "EdvJRUWRNGHE" + }, + "source": [ + "### Import libraries\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "qgdSpVmDbdQ9" + }, + "outputs": [], + "source": [ + "from IPython.display import Markdown, display\n", + "from google import genai\n", + "from google.genai.types import (\n", + " GenerateContentConfig,\n", + " GoogleSearch,\n", + " HttpOptions,\n", + " Tool,\n", + " UrlContext,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "uhAZMeVrCcKS" + }, + "source": [ + "### Connect to the generative AI service on Vertex AI" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "pv_22mKMCjQY" + }, + "outputs": [], + "source": [ + "client = genai.Client(enterprise=True, project=PROJECT_ID, location=LOCATION)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "n4yRkFg6BBu4" + }, + "source": [ + "### Supported Models\n", + "\n", + "Learn more about [supported Gemini models](https://cloud.google.com/vertex-ai/generative-ai/docs/models). This tutorial uses the Gemini 2.5 Flash model." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "-coEslfWPrxo" + }, + "outputs": [], + "source": [ + "# fmt: off\n", + "MODEL_ID = \"gemini-2.5-flash\" # @param {type: \"string\"}\n", + "# fmt: on" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "37CH91ddY9kG" + }, + "source": [ + "## Use URL Context\n", + "\n", + "The URL context tool is particularly useful for a variety of tasks, such as:\n", + "\n", + "- Extracting data: You can pull key data points or talking points directly from articles.\n", + "- Comparing information: It allows for the comparison of information across multiple different links.\n", + "- Synthesizing data: The tool can be used to synthesize data from several different online sources.\n", + "- Answering questions: It's useful for answering questions based on the content found on one or more specific web pages.\n", + "- Content analysis: You can analyze content for a specific purpose, such as writing a job description or creating test questions from a webpage's text\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "mdlFHjmDEoDo" + }, + "source": [ + "#### **Example 1**: One URL only\n", + "\n", + "The most direct way to use the feature is to provide a specific URL in your prompt that you want the model to analyze." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "xRJuHj0KZ8xz" + }, + "outputs": [], + "source": [ + "# Define the Url context tool\n", + "url_context_tool = Tool(url_context=UrlContext)\n", + "\n", + "response = client.models.generate_content(\n", + " model=MODEL_ID,\n", + " contents=\"Summarize this document: https://blog.google/technology/developers/introducing-gemini-cli-open-source-ai-agent/\",\n", + " config=GenerateContentConfig(\n", + " tools=[url_context_tool],\n", + " ),\n", + ")\n", + "\n", + "display(Markdown(response.text))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "VUnF-lUjIa_w" + }, + "source": [ + "**Inspect url context metadata**\n", + "\n", + "When the model uses URLs to inform its answer, the response object will contain\n", + "`url_context_metadata`. This metadata lists the URLs the model retrieved and the status of that retrieval.\n", + "\n", + "You can inspect this metadata to confirm which sources were used, as shown in the code example below. Inspecting this metadata is useful for debugging which sources were successfully retrieved or for verifying the model's information sources.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "cMZWGgrBIWGI" + }, + "outputs": [], + "source": [ + "print(response.candidates[0].url_context_metadata)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "EfMCR7pkKMo5" + }, + "source": [ + "### **Example 2**: Multiple URLs\n", + "\n", + "The feature currently supports a maximum of 20 URLs per request." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "VRy0vsPtMt_a" + }, + "outputs": [], + "source": [ + "url_context_tool = Tool(url_context=UrlContext)\n", + "\n", + "URL1 = \"https://www.allrecipes.com/recipe/10813/best-chocolate-chip-cookies/\"\n", + "URL2 = \"https://www.allrecipes.com/recipe/17165/big-soft-ginger-cookies/\"\n", + "\n", + "prompt = f\"Compare the ingredients and instructions from the recipe at {URL1} with the one at {URL2}. What are the key differences?\"\n", + "\n", + "response = client.models.generate_content(\n", + " model=MODEL_ID,\n", + " contents=prompt,\n", + " config=GenerateContentConfig(\n", + " tools=[url_context_tool],\n", + " ),\n", + ")\n", + "\n", + "display(Markdown(response.text))\n", + "\n", + "print(response.candidates[0].url_context_metadata)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "9_FXg9BZSOdw" + }, + "source": [ + "### **Example 3**: Grounding with Google Search\n", + "\n", + "You can combine the URL context tool with Grounding with Google Search. This allows the model to first search for relevant information online and then use the URL context tool to get a deeper understanding of the content from the search results.\n", + "\n", + "> ⚠ This feature is experimental and only available in API version `v1beta1`. By default, the SDK uses the beta API endpoints. You also can set the API version to `v1beta1` explicitly, as below:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "d33a5e5d1427" + }, + "outputs": [], + "source": [ + "client_v1beta1 = genai.Client(\n", + " enterprise=True,\n", + " project=PROJECT_ID,\n", + " location=LOCATION,\n", + " http_options=HttpOptions(api_version=\"v1beta1\"),\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "urM7aDRKhuav" + }, + "outputs": [], + "source": [ + "# Define Url Context and Google Search tools\n", + "tools = [Tool(url_context=UrlContext), Tool(google_search=GoogleSearch())]" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "Zya1jFS0Vh_4" + }, + "source": [ + "#### **Use Case**: Answering a question using search and a specific page\n", + "\n", + "In this example, we ask the model to create a schedule based on a specific URL but also ask a follow-up question. The model can use the provided URL and perform a Google search to answer the entire prompt comprehensively." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "A339OQ05Sq8q" + }, + "outputs": [], + "source": [ + "URL = \"https://www.metmuseum.org/visit/plan-your-visit\"\n", + "\n", + "prompt = f\"\"\"Using the 'Exhibitions' and 'Must-See Artwork' sections of the URL {URL},\n", + "design a 2-hour art tour of the Met for someone primarily interested in European paintings.\n", + "Also, what are the museum's hours for next Saturday?\n", + "\"\"\"\n", + "\n", + "response = client_v1beta1.models.generate_content(\n", + " model=MODEL_ID,\n", + " contents=prompt,\n", + " config=GenerateContentConfig(\n", + " tools=tools,\n", + " ),\n", + ")\n", + "\n", + "display(Markdown(response.text))\n", + "\n", + "print(response.candidates[0].url_context_metadata)\n", + "print(response.candidates[0].grounding_metadata)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "O6lHgdmcZurD" + }, + "source": [ + "#### **Use Case**: Analyze an API docs guide\n", + "\n", + "Developers constantly need to learn new APIs. This example demonstrates using the URL context to analyze API documentation and extract key information, making integration faster. This is a great way to synthesize data from several sources." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "VggoVVB5XOur" + }, + "outputs": [], + "source": [ + "URL = \"https://jsonplaceholder.typicode.com/guide/\"\n", + "\n", + "prompt = f\"\"\"\"Analyze the API documentation guide at the provided URL {URL}.\n", + "Provide a summary of the available resource endpoints and list all the HTTP methods\n", + "that are supported for the '/posts' endpoint.\n", + "\"\"\"\n", + "\n", + "response = client_v1beta1.models.generate_content(\n", + " model=MODEL_ID,\n", + " contents=prompt,\n", + " config=GenerateContentConfig(\n", + " tools=tools,\n", + " ),\n", + ")\n", + "\n", + "display(Markdown(response.text))\n", + "\n", + "print(response.candidates[0].url_context_metadata)\n", + "print(response.candidates[0].grounding_metadata)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "6u5VrSCebF4O" + }, + "source": [ + "#### **Use Case**: Generate code based on a tutorial\n", + "\n", + "This example shows how to use the URL context to read a programming tutorial and write functional code based on its concepts, a task that can dramatically speed up learning and development." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "gx9Us4sQagC9" + }, + "outputs": [], + "source": [ + "URL = \"https://realpython.com/python-requests/\"\n", + "\n", + "prompt = f\"\"\"\"\"Read the tutorial from the provided URL {URL}. Write a Python script\n", + "that performs a GET request to 'https://api.github.com/users/google' and prints the\n", + "user's name and number of public repositories. Include comments explaining each step\n", + "and basic error handling for the request.\n", + "\"\"\"\n", + "\n", + "response = client_v1beta1.models.generate_content(\n", + " model=MODEL_ID,\n", + " contents=prompt,\n", + " config=GenerateContentConfig(\n", + " tools=tools,\n", + " ),\n", + ")\n", + "\n", + "display(Markdown(response.text))\n", + "\n", + "print(response.candidates[0].url_context_metadata)\n", + "print(response.candidates[0].grounding_metadata)" + ] + } + ], + "metadata": { + "colab": { + "name": "intro_url_context.ipynb", + "toc_visible": true + }, + "kernelspec": { + "display_name": "Python 3", + "name": "python3" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} diff --git a/gemini/use-cases/README.md b/gemini/use-cases/README.md new file mode 100644 index 0000000..b686a38 --- /dev/null +++ b/gemini/use-cases/README.md @@ -0,0 +1,33 @@ +# Gemini Use Cases + +This directory contains notebooks and resources that demonstrate how to use Gemini for a variety of use cases. + +## Notebooks + +- [Gemini: An Overview of Multimodal Use Cases](intro_multimodal_use_cases.ipynb): Explore a variety of different use cases enabled by multimodality with Gemini. + +## Use Cases + +- [Applying LLMs to Data](applying-llms-to-data/): Learn how to apply LLMs to your data in BigQuery using Gemini. +- [Code](code/): Learn how to use Gemini for various code-related tasks like analysis, retrieval-augmented generation, and security scanning. +- [Customer Support](customer-support/): Learn how to build a customer support solution with Gemini. +- [Data Augmentation](data-augmentation/): Learn how to use Gemini for text data augmentation. +- [Data Generation](data-generation/): Learn how to generate synthetic data using Gemini. +- [Diarized Transcription Summarization](diarized-transcription-summarization/): Learn how to perform diarized transcription and summarization with Gemini. +- [Document Processing](document-processing/): Learn how to process and understand documents using Gemini. +- [Education](education/): Explore use cases for Gemini in the education sector. +- [Entity Extraction](entity-extraction/): A flexible solution for extracting structured information from documents using Gemini models via the Vertex AI API. +- [GraphRAG](graphrag/): Learn how to use GraphRAG with Gemini. +- [Healthcare](healthcare/): Learn how to use Gemini with healthcare APIs. +- [Know Your Customer (KYC)](kyc/): Learn how to perform Know Your Customer (KYC) with grounding. +- [Marketing](marketing/): Learn how to create marketing assets with Gemini. +- [Multimodal Data Curation](multimodal-data-curation/): A comprehensive solution for curating pre-training data for text-to-video models on Google Cloud. +- [Multimodal Sentiment Analysis](multimodal-sentiment-analysis/): Learn how to perform multimodal sentiment analysis with Gemini. +- [Productivity](productivity/): Learn how to use Gemini to boost your productivity. +- [Retail](retail/): Explore retail use cases for Gemini. +- [Retrieval-Augmented Generation](retrieval-augmented-generation/): Learn about Retrieval-Augmented Generation (RAG) with Gemini. +- [Sheets Integration](sheets-integration/): Learn how to integrate Gemini with Google Sheets. +- [Spatial Understanding](spatial-understanding/): Learn about spatial understanding with Gemini. +- [Video Analysis](video-analysis/): Learn how to perform video analysis with Gemini. +- [Video Thumbnail Generation](video-thumbnail-generation/): Learn how to generate video thumbnails with Gemini. +- [Vision Assistant](vision-assistant/): Learn how to build an enhanced vision assistant with Gemini. diff --git a/gemini/use-cases/applying-llms-to-data/ai-assisted-data-science/ai-assisted-data-science.ipynb b/gemini/use-cases/applying-llms-to-data/ai-assisted-data-science/ai-assisted-data-science.ipynb new file mode 100644 index 0000000..998463c --- /dev/null +++ b/gemini/use-cases/applying-llms-to-data/ai-assisted-data-science/ai-assisted-data-science.ipynb @@ -0,0 +1,1365 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "id": "ht3MT6z2u4B6", + "metadata": { + "id": "ht3MT6z2u4B6" + }, + "outputs": [], + "source": [ + "# Copyright 2025 Google LLC\n", + "#\n", + "# Licensed under the Apache License, Version 2.0 (the \"License\");\n", + "# you may not use this file except in compliance with the License.\n", + "# You may obtain a copy of the License at\n", + "#\n", + "# https://www.apache.org/licenses/LICENSE-2.0\n", + "#\n", + "# Unless required by applicable law or agreed to in writing, software\n", + "# distributed under the License is distributed on an \"AS IS\" BASIS,\n", + "# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n", + "# See the License for the specific language governing permissions and\n", + "# limitations under the License." + ] + }, + { + "cell_type": "markdown", + "id": "9Ku6cXXnwATV", + "metadata": { + "id": "9Ku6cXXnwATV" + }, + "source": [ + "# AI-Assisted Data Science Workflows in BigQuery\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + "
    \n", + " \n", + " \"BigQuery
    Open in BigQuery Studio\n", + "
    \n", + "
    \n", + " \n", + " \"Google
    Open in Colab\n", + "
    \n", + "
    \n", + " \n", + " \"Google
    Open in Colab Enterprise\n", + "
    \n", + "
    \n", + " \n", + " \"GitHub
    View on GitHub\n", + "
    \n", + "
    \n", + "\n", + "
    " + ] + }, + { + "cell_type": "markdown", + "id": "lANfIejdvZYQ", + "metadata": { + "id": "lANfIejdvZYQ" + }, + "source": [ + "| Author |\n", + "| --- |\n", + "| [Jeff Nelson](https://github.com/jeffonelson) |" + ] + }, + { + "cell_type": "markdown", + "id": "XJy67x0Cn4s4", + "metadata": { + "id": "XJy67x0Cn4s4" + }, + "source": [ + "## Overview\n", + "\n", + "In this notebook, you'll explore a multimodal data science workflow in BigQuery. You will start with a raw dataset of house listings, enrich it with AI, build a clustering model to find customer segments, and finally, build a powerful text-to-image and image-to-image search engine.\n", + "\n", + "Next, you'll compare this SQL-native workflow with a modern, generative AI approach by using the [Data Science Agent](https://cloud.google.com/bigquery/docs/colab-data-science-agent) to automatically generate a Python-based clustering model from a simple text prompt.\n", + "\n", + "## Objectives\n", + "\n", + "* **Prepare data** and perform feature engineering on a raw dataset.\n", + "* **Enrich data** by using BigQuery ML's AI functions to analyze images.\n", + "* **Build and evaluate** a K-means clustering model using BQML.\n", + "* **Automate model creation** by using the Data Science Agent to generate a Python clustering model.\n", + "* **Generate embeddings** for house images using a multimodal model.\n", + "* **Perform vector search** to find similar houses using text or image queries." + ] + }, + { + "cell_type": "markdown", + "id": "B8vBqblSM_9w", + "metadata": { + "id": "B8vBqblSM_9w" + }, + "source": [ + "## Setup\n", + "\n", + "Before you can begin the analysis, you need to set up your environment. This involves enabling the necessary APIs, creating a resource connection for BigQuery, and configuring permissions for the service account that connection will use." + ] + }, + { + "cell_type": "markdown", + "id": "LjsOfBUTNMR5", + "metadata": { + "id": "LjsOfBUTNMR5" + }, + "source": [ + "### Create BigQuery Cloud resource connection\n", + "\n", + "Create a [Cloud resource connection](https://cloud.google.com/bigquery/docs/create-cloud-resource-connection). This connection acts as a secure bridge between BigQuery and other Google Cloud services. It allows BigQuery to call the Gemini models hosted on Vertex AI for inference and data enrichment." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "mt4Xdf2uM73o", + "metadata": { + "id": "mt4Xdf2uM73o" + }, + "outputs": [], + "source": [ + "!bq mk --connection --location=us --connection_type=CLOUD_RESOURCE ai_connection" + ] + }, + { + "cell_type": "markdown", + "id": "3Z1O6H-8NaSx", + "metadata": { + "id": "3Z1O6H-8NaSx" + }, + "source": [ + "### Configure environment and grant permissions\n", + "\n", + "The next block of code handles the necessary setup for your environment. These cells perform several key actions:\n", + "\n", + "* **Capture variables:** Your `PROJECT_ID` and the connection's service account email are captured as Python variables.\n", + "* **Initialize a GCS client:** A connection to Google Cloud Storage is created so you can access the house listing images.\n", + "* **Grant permissions:** The necessary IAM roles ([`Storage Object Viewer`](https://cloud.google.com/storage/docs/access-control/iam-roles#storage.objectViewer) and [`Vertex AI User`](https://cloud.google.com/vertex-ai/docs/general/access-control#aiplatform.user)) are granted to the connection's service account, allowing it to read images and call Vertex AI models.\n", + "* **Define a helper function:** The `display_images` function is defined, which you'll use to visualize images throughout the notebook.\n", + "* **Enable interactive tables:** A Colab extension is enabled to render pandas DataFrames as interactive tables." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "jdSIMRCcNit8", + "metadata": { + "id": "jdSIMRCcNit8" + }, + "outputs": [], + "source": [ + "# Create Service Account\n", + "SERVICE_ACCT = !bq show --format=prettyjson --connection us.ai_connection | grep \"serviceAccountId\" | cut -d '\"' -f 4\n", + "SERVICE_ACCT_EMAIL = SERVICE_ACCT[-1]\n", + "print(SERVICE_ACCT_EMAIL)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "D9MC8XqQmD97", + "metadata": { + "id": "D9MC8XqQmD97" + }, + "outputs": [], + "source": [ + "# Set project id variables for Python and Shell\n", + "PROJECT_ID = !gcloud config get-value project\n", + "PROJECT_ID = PROJECT_ID[-1]\n", + "%env PROJECT_ID=$PROJECT_ID" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "iRMV48N6XoYY", + "metadata": { + "id": "iRMV48N6XoYY" + }, + "outputs": [], + "source": [ + "from google.cloud import storage\n", + "\n", + "# Define bucket containing housing images\n", + "bucket_name = \"drw001-data-science-with-notebooks\"\n", + "# Create Cloud Storage client\n", + "client = storage.Client(project=PROJECT_ID)\n", + "bucket = client.bucket(bucket_name)\n", + "\n", + "%env BUCKET_NAME=$bucket_name" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "HvATOolaNmoQ", + "metadata": { + "id": "HvATOolaNmoQ" + }, + "outputs": [], + "source": [ + "import time\n", + "\n", + "# Grant permissions\n", + "!gcloud projects add-iam-policy-binding --format=none $PROJECT_ID --member=serviceAccount:$SERVICE_ACCT_EMAIL --role='roles/storage.objectViewer'\n", + "!gcloud projects add-iam-policy-binding --format=none $PROJECT_ID --member=serviceAccount:$SERVICE_ACCT_EMAIL --role='roles/aiplatform.user'\n", + "\n", + "# Wait ~60 seconds, to give IAM updates time to propagate. Otherwise, subsequent cells may fail.\n", + "time.sleep(60)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "_zLmwszkkJC4", + "metadata": { + "id": "_zLmwszkkJC4" + }, + "outputs": [], + "source": [ + "import io\n", + "import math\n", + "\n", + "import PIL.Image\n", + "import matplotlib.pyplot as plt\n", + "import pandas as pd\n", + "from google.cloud import storage\n", + "\n", + "# Helper function to display images in a DataFrame\n", + "\n", + "\n", + "def display_images(\n", + " df: pd.DataFrame, uri_column: str = \"uri\", title_column: str | None = None\n", + ") -> None:\n", + " \"\"\"Displays images from a DataFrame in a 3-column grid.\n", + "\n", + " Args:\n", + " df (pd.DataFrame): DataFrame with a column containing GCS URIs.\n", + " uri_column (str): The name of the column containing the GCS URIs.\n", + " title_column (str, optional): The name of the column to use for the image title.\n", + " If None, the image filename is used.\n", + " \"\"\"\n", + " # Calculate grid size\n", + " num_images = len(df)\n", + " if num_images == 0:\n", + " print(\"No images to display.\")\n", + " return\n", + "\n", + " cols = 3\n", + " rows = math.ceil(num_images / cols)\n", + "\n", + " # Handle single image case for correct subplot array shape\n", + " if num_images == 1:\n", + " fig, axs = plt.subplots(1, 1, figsize=(3.5, 3.5))\n", + " axs = [axs]\n", + " else:\n", + " fig, axs = plt.subplots(rows, cols, figsize=(10, rows * 3.5))\n", + " axs = axs.flatten()\n", + "\n", + " for i, row in df.iterrows():\n", + " if i >= len(axs):\n", + " break\n", + "\n", + " # 1. Extract the image path from the full GCS URI\n", + " image_uri = row[uri_column]\n", + " image_path = image_uri.split(\"/\", 3)[-1]\n", + "\n", + " # 2. Get the image from GCS\n", + " try:\n", + " blob = bucket.blob(image_path)\n", + " image_bytes = blob.download_as_bytes()\n", + " image = PIL.Image.open(io.BytesIO(image_bytes))\n", + " except Exception as e:\n", + " print(f\"Could not load image {image_uri}: {e}\")\n", + " continue\n", + "\n", + " # 3. Plot the image\n", + " axs[i].imshow(image)\n", + "\n", + " # Determine the title\n", + " if title_column and title_column in df.columns:\n", + " title = f\"Cluster: {row[title_column]}\"\n", + " else:\n", + " title = image_path.split(\"/\")[-1]\n", + "\n", + " axs[i].set_title(title, fontsize=10)\n", + " axs[i].axis(\"off\")\n", + "\n", + " # Hide any unused subplots\n", + " for j in range(num_images, len(axs)):\n", + " axs[j].axis(\"off\")\n", + "\n", + " plt.tight_layout()\n", + " plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "9YUTHxYpXGQH", + "metadata": { + "id": "9YUTHxYpXGQH" + }, + "source": [ + "### Enable interactive tables for DataFrames\n", + "\n", + "For a better data exploration experience, you can enable a [Colab extension](https://colab.sandbox.google.com/notebooks/data_table.ipynb) that renders pandas DataFrames as interactive, sortable tables. This makes it much easier to inspect large amounts of data." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "h8DPEKFrXA2q", + "metadata": { + "id": "h8DPEKFrXA2q" + }, + "outputs": [], + "source": [ + "%load_ext google.colab.data_table" + ] + }, + { + "cell_type": "markdown", + "id": "FONKhnbyWx-x", + "metadata": { + "id": "FONKhnbyWx-x" + }, + "source": [ + "---\n", + "## Data Preparation and Feature Engineering\n", + "\n", + "### Create a BigQuery dataset\n", + "\n", + "A [BigQuery dataset](https://cloud.google.com/bigquery/docs/datasets-intro) is a container that holds all of your other BigQuery objects, including tables, views, and models." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "L3Ih_zHiW5g8", + "metadata": { + "id": "L3Ih_zHiW5g8" + }, + "outputs": [], + "source": [ + "!bq mk --dataset housing_dataset" + ] + }, + { + "cell_type": "markdown", + "id": "EYtdDkK-ZbD5", + "metadata": { + "id": "EYtdDkK-ZbD5" + }, + "source": [ + "### Load the raw data into a BigQuery table\n", + "\n", + "You can now load your source data using the [`bq load`](https://cloud.google.com/bigquery/docs/loading-data-cloud-storage-csv#loading_csv_data_into_a_table) command. It directly loads a CSV file from a public Google Cloud Storage bucket into a new BigQuery table named `listings`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1JqWUw49XxWN", + "metadata": { + "id": "1JqWUw49XxWN" + }, + "outputs": [], + "source": [ + "!bq load \\\n", + " --autodetect \\\n", + " --source_format=CSV \\\n", + " --skip_leading_rows=1 \\\n", + " housing_dataset.listings \\\n", + " gs://$BUCKET_NAME/tabular/housing-data.csv" + ] + }, + { + "cell_type": "markdown", + "id": "e6952daf", + "metadata": { + "id": "e6952daf" + }, + "source": [ + "## Explore the data with `ML.DESCRIBE_DATA`\n", + "\n", + "Next, you'll perform exploratory data analysis to understand the data's characteristics. Instead of writing many individual `COUNT`, `AVG`, and `STDDEV` queries, you can use the [`ML.DESCRIBE_DATA`](https://cloud.google.com/bigquery/docs/reference/standard-sql/bigqueryml-syntax-describe-data) function.\n", + "\n", + "The function returns descriptive statistics for each column in a table, giving you a quick overview of distributions, missing values, and potential outliers.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f1331e1c", + "metadata": { + "id": "f1331e1c" + }, + "outputs": [], + "source": [ + "%%bigquery --project {PROJECT_ID}\n", + "\n", + "SELECT *\n", + "FROM\n", + " ML.DESCRIBE_DATA((SELECT * FROM `housing_dataset.listings`));" + ] + }, + { + "cell_type": "markdown", + "id": "gAOfp_g9IUbx", + "metadata": { + "id": "gAOfp_g9IUbx" + }, + "source": [ + "### Clean, filter, and enrich the data\n", + "\n", + "Create a new, cleaned table from the raw data. The following query:\n", + "\n", + "1. **Filters the data** to include only listings that are currently 'For Sale'.\n", + "2. **Creates a new feature**, `property_age`, by subtracting the `year_built` from the current year.\n", + "3. **Creates an [`ObjectRef`](https://cloud.google.com/blog/products/data-analytics/new-objectref-data-type-brings-unstructured-data-into-bigquery?e=48754805)**, a BigQuery object that allows you to reference unstructured image data in Google Cloud Storage." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "dbfde5ec", + "metadata": { + "id": "dbfde5ec" + }, + "outputs": [], + "source": [ + "%%bigquery --project {PROJECT_ID}\n", + "\n", + "CREATE OR REPLACE TABLE `housing_dataset.listings` AS\n", + "SELECT\n", + " *,\n", + " EXTRACT(YEAR FROM CURRENT_DATE()) - year_built AS property_age,\n", + " OBJ.FETCH_METADATA(OBJ.MAKE_REF(house_uri, 'us.ai_connection')) AS image_ref\n", + "FROM\n", + " `housing_dataset.listings`\n", + "WHERE sale_status = 'For Sale';" + ] + }, + { + "cell_type": "markdown", + "id": "E19E_rcpcoXi", + "metadata": { + "id": "E19E_rcpcoXi" + }, + "source": [ + "---\n", + "## Multimodal Enrichment with AI Functions\n", + "\n", + "### Create a remote model for Gemini\n", + "\n", + "To use a like Gemini from within BigQuery, you need to create a [Remote Model](https://cloud.google.com/bigquery/docs/reference/standard-sql/bigqueryml-syntax-create-remote-model). This object acts as a secure pointer to the underlying model hosted on Vertex AI.\n", + "\n", + "Then, you can reference this remote model in your SQL queries, allowing you to use Gemini's powerful capabilities to analyze and enrich your data with functions like [`AI.GENERATE_TABLE`](https://cloud.google.com/bigquery/docs/reference/standard-sql/bigqueryml-syntax-generate-table)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "muSly43kbGdt", + "metadata": { + "id": "muSly43kbGdt" + }, + "outputs": [], + "source": [ + "%%bigquery --project {PROJECT_ID}\n", + "\n", + "CREATE OR REPLACE MODEL `housing_dataset.gemini`\n", + "REMOTE WITH CONNECTION `us.ai_connection`\n", + " OPTIONS(ENDPOINT = 'gemini-2.5-flash');" + ] + }, + { + "cell_type": "markdown", + "id": "DijByYGYIZIU", + "metadata": { + "id": "DijByYGYIZIU" + }, + "source": [ + "### Enrich images with `AI.GENERATE_TABLE`\n", + "\n", + "You'll use the remote model with the `AI.GENERATE_TABLE` function. The following query contains:\n", + "1. A **prompt** to the Gemini model, asking it to analyze the house image (`image_ref`) and extract specific features.\n", + "2. An **output schema**. You can define exact column names and data types (e.g. `near_water BOOL`) you want it to return.\n", + "\n", + "The result is a new table, `listings_multimodal`, that contains all the columns plus the new, AI-generated features." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7L0lP5SObaJr", + "metadata": { + "id": "7L0lP5SObaJr" + }, + "outputs": [], + "source": [ + "%%bigquery --project {PROJECT_ID}\n", + "\n", + "CREATE OR REPLACE TABLE `housing_dataset.listings_multimodal` AS (\n", + "SELECT\n", + "id,\n", + "price,\n", + "sq_ft,\n", + "year_built,\n", + "city,\n", + "zipcode,\n", + "number_of_rooms,\n", + "number_of_baths,\n", + "acre_lot,\n", + "property_age,\n", + "near_water,\n", + "number_windows,\n", + "prop_description,\n", + "image_ref\n", + "FROM AI.GENERATE_TABLE(\n", + " MODEL `housing_dataset.gemini`,\n", + " (\n", + " SELECT (\n", + " 'Analyze the following image to find whether it is near a body of water,'\n", + " 'the number of windows, and give a brief description of the property.'\n", + " , image_ref\n", + " ) AS prompt,\n", + " *\n", + " FROM `housing_dataset.listings`\n", + " ),\n", + " STRUCT(\n", + " \"near_water BOOL, number_windows INT64, prop_description STRING\" AS output_schema\n", + " )\n", + "));" + ] + }, + { + "cell_type": "markdown", + "id": "6O5pFprsjIG_", + "metadata": { + "id": "6O5pFprsjIG_" + }, + "source": [ + "### Review the enriched data\n", + "\n", + "Let's examine the new table. The last three columns (`near_water`, `number_windows`, `prop_description`) were generated by the Gemini model analyzing each house image." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "30HQkt6sjK7Z", + "metadata": { + "id": "30HQkt6sjK7Z" + }, + "outputs": [], + "source": [ + "%%bigquery --project {PROJECT_ID}\n", + "\n", + "SELECT *\n", + "FROM `housing_dataset.listings_multimodal`\n", + "LIMIT 2" + ] + }, + { + "cell_type": "markdown", + "id": "e3522675", + "metadata": { + "id": "e3522675" + }, + "source": [ + "---\n", + "## Model Training with K-Means Clustering\n", + "\n", + "With your enriched and feature-engineered dataset, you can now build a machine learning model. Your goal is to segment the house listings into distinct groups based on their characteristics. K-means clustering is an unsupervised learning algorithm that groups data points based on their similarity.\n", + "\n", + "You'll use `CREATE MODEL` to train a K-means model with 3 clusters. By including the `model_registry='VERTEX_AI'` [option](https://cloud.google.com/bigquery/docs/managing-models-vertex), the model is also automatically registered in the [Vertex AI Model Registry](https://cloud.google.com/vertex-ai/docs/model-registry/introduction) upon completion. This makes it visible and deployable alongside your other custom models in Vertex AI." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "057ad575", + "metadata": { + "id": "057ad575" + }, + "outputs": [], + "source": [ + "%%bigquery --project {PROJECT_ID}\n", + "\n", + "CREATE OR REPLACE MODEL `housing_dataset.kmeans_clustering_model`\n", + "OPTIONS(model_type='KMEANS', num_clusters=3,\n", + " model_registry = 'VERTEX_AI', VERTEX_AI_MODEL_ID = 'housing_clustering') AS\n", + "SELECT\n", + " price,\n", + " sq_ft,\n", + " year_built,\n", + " number_of_rooms,\n", + " number_of_baths,\n", + " acre_lot,\n", + " property_age,\n", + " near_water,\n", + " number_windows\n", + "FROM\n", + " `housing_dataset.listings_multimodal`;" + ] + }, + { + "cell_type": "markdown", + "id": "52e64434", + "metadata": { + "id": "52e64434" + }, + "source": [ + "---\n", + "## Model Evaluation and Prediction\n", + "\n", + "### Evaluate the clustering model\n", + "\n", + "Evaluate the model with [`ML.EVALUATE`](https://cloud.google.com/bigquery/docs/reference/standard-sql/bigqueryml-syntax-evaluate). This function returns key metrics like the Davies-Bouldin index to help you measure cluster quality and separation." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "26e0edd7", + "metadata": { + "id": "26e0edd7" + }, + "outputs": [], + "source": [ + "%%bigquery --project {PROJECT_ID}\n", + "\n", + "SELECT * FROM ML.EVALUATE(MODEL `housing_dataset.kmeans_clustering_model`);" + ] + }, + { + "cell_type": "markdown", + "id": "rD6LwGszJ3bR", + "metadata": { + "id": "rD6LwGszJ3bR" + }, + "source": [ + "### Analyze cluster centroids\n", + "\n", + "Inspect the cluster centroids with [`ML.CENTROIDS`](https://cloud.google.com/bigquery/docs/reference/standard-sql/bigqueryml-syntax-centroids). Each centroid represents the \"average\" house in a cluster, making it easy to compare the defining characteristics of each segment." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0R8ZGIjYJkg-", + "metadata": { + "id": "0R8ZGIjYJkg-" + }, + "outputs": [], + "source": [ + "%%bigquery --project {PROJECT_ID}\n", + "\n", + "SELECT * FROM ML.CENTROIDS(MODEL `housing_dataset.kmeans_clustering_model`)\n", + "WHERE feature IN ( 'price', 'property_age' )\n", + "ORDER BY feature, centroid_id;" + ] + }, + { + "cell_type": "markdown", + "id": "wPrkl7Gcky_t", + "metadata": { + "id": "wPrkl7Gcky_t" + }, + "source": [ + "### Assign listings to clusters with `ML.PREDICT`\n", + "\n", + "Use [`ML.PREDICT`](https://cloud.google.com/bigquery/docs/reference/standard-sql/bigqueryml-syntax-predict) to assign each house to a cluster. The function returns a `CENTROID_ID` for each listing, indicating which cluster it belongs to. We'll rename `CENTROID_ID` to `cluster` for ease of use." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "bc9392ad", + "metadata": { + "id": "bc9392ad" + }, + "outputs": [], + "source": [ + "%%bigquery df --project {PROJECT_ID}\n", + "\n", + "SELECT\n", + " CENTROID_ID AS cluster,\n", + " * EXCEPT(CENTROID_ID)\n", + "FROM\n", + " ML.PREDICT(\n", + " MODEL `housing_dataset.kmeans_clustering_model`,\n", + " TABLE `housing_dataset.listings_multimodal`\n", + ");" + ] + }, + { + "cell_type": "markdown", + "id": "IiPMp9jkLGxj", + "metadata": { + "id": "IiPMp9jkLGxj" + }, + "source": [ + "---\n", + "\n", + "\n", + "## Visualize and Interpret Clusters\n", + "\n", + "With each property now assigned to a cluster, you can visualize the results to better understand the distinct housing segments." + ] + }, + { + "cell_type": "markdown", + "id": "1FLU3-7fp6Ja", + "metadata": { + "id": "1FLU3-7fp6Ja" + }, + "source": [ + "### Compare cluster characteristics\n", + "\n", + "Visualizations make it easy to compare the clusters. The following plots use:\n", + "* **Box plots** to compare the distribution of numeric features like `price`.\n", + "* **Bar charts** to compare the counts of boolean features like `near_water`." + ] + }, + { + "cell_type": "markdown", + "id": "xU8jWif2HlPh", + "metadata": { + "id": "xU8jWif2HlPh" + }, + "source": [ + "#### Box Plots for Numeric Features" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "hKjcSmkqpg7x", + "metadata": { + "id": "hKjcSmkqpg7x" + }, + "outputs": [], + "source": [ + "import seaborn as sns\n", + "\n", + "sns.set_style(\"whitegrid\")\n", + "\n", + "plt.figure(figsize=(14, 6))\n", + "plt.suptitle(\"Sample Numeric Variable Distributions by Cluster\", fontsize=16)\n", + "\n", + "# Boxplot for Price\n", + "plt.subplot(1, 2, 1)\n", + "# Assign 'cluster' to hue and turn off the redundant legend\n", + "sns.boxplot(\n", + " x=\"cluster\", y=\"price\", data=df, hue=\"cluster\", palette=\"tab20\", legend=False\n", + ")\n", + "plt.title(\"Price Distribution per Cluster\")\n", + "plt.ylabel(\"Price\")\n", + "plt.xlabel(\"Cluster ID\")\n", + "\n", + "# Boxplot for Property Age\n", + "plt.subplot(1, 2, 2)\n", + "# Assign 'cluster' to hue and turn off the redundant legend\n", + "sns.boxplot(\n", + " x=\"cluster\", y=\"property_age\", data=df, hue=\"cluster\", palette=\"tab20\", legend=False\n", + ")\n", + "plt.title(\"Property Age Distribution per Cluster\")\n", + "plt.ylabel(\"Property Age (Years)\")\n", + "plt.xlabel(\"Cluster ID\")\n", + "\n", + "plt.tight_layout(rect=[0, 0, 1, 0.96])\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "TvSRM4Xnp1Av", + "metadata": { + "id": "TvSRM4Xnp1Av" + }, + "source": [ + "#### Bar Charts for Categorical/Boolean Features" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "omgSiqGpHHZ_", + "metadata": { + "id": "omgSiqGpHHZ_" + }, + "outputs": [], + "source": [ + "water_crosstab = pd.crosstab(df[\"cluster\"], df[\"near_water\"])\n", + "\n", + "water_proportions = water_crosstab.div(water_crosstab.sum(axis=1), axis=0)\n", + "\n", + "water_proportions.plot(kind=\"bar\", stacked=True, figsize=(10, 6), colormap=\"tab20\")\n", + "\n", + "plt.title(\"Proportion of Properties Near Water within Each Cluster\", fontsize=16)\n", + "plt.xlabel(\"Cluster ID\")\n", + "plt.ylabel(\"Proportion\")\n", + "plt.xticks(rotation=0)\n", + "plt.legend(title=\"Is Near Water?\", labels=[\"No\", \"Yes\"])\n", + "plt.tight_layout()\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "Nu7u6X7PlHUv", + "metadata": { + "id": "Nu7u6X7PlHUv" + }, + "source": [ + "---\n", + "## Generate Cluster Descriptions with Gemini\n", + "\n", + "While the centroids and visualizations are useful, you can use generative AI to create human-readable descriptions for each cluster.\n", + "\n", + "First, you'll aggregate the average statistics for each cluster into a pandas DataFrame." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ECCeTbB7K9w4", + "metadata": { + "id": "ECCeTbB7K9w4" + }, + "outputs": [], + "source": [ + "%%bigquery df_cluster_stats --project {PROJECT_ID}\n", + "\n", + "SELECT\n", + " centroid_id AS cluster,\n", + " AVG(price) AS avg_price,\n", + " AVG(sq_ft) AS avg_sq_ft,\n", + " AVG(year_built) AS avg_year_built,\n", + " AVG(number_of_rooms) AS avg_number_of_rooms,\n", + " AVG(number_of_baths) AS avg_number_of_baths,\n", + " AVG(acre_lot) AS avg_acre_lot,\n", + " AVG(property_age) AS avg_property_age,\n", + " AVG(number_windows) AS avg_number_windows,\n", + " COUNT(*) AS cluster_size\n", + "FROM\n", + " ML.PREDICT(MODEL `housing_dataset.kmeans_clustering_model`,\n", + " TABLE `housing_dataset.listings_multimodal`)\n", + "GROUP BY\n", + " cluster\n", + "ORDER BY\n", + " cluster;" + ] + }, + { + "cell_type": "markdown", + "id": "XH2W6Ju7Lv24", + "metadata": { + "id": "XH2W6Ju7Lv24" + }, + "source": [ + "### Prepare the prompt for Gemini\n", + "\n", + "Next, you'll convert the statistics DataFrame into a simple string and create a prompt template. This prompt instructs the model to act as a real estate professional and provides a clear structure for the desired output." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "w9Vzn69PgDgb", + "metadata": { + "id": "w9Vzn69PgDgb" + }, + "outputs": [], + "source": [ + "# Convert the df_cluster_stats to a string for input to a Gemini model\n", + "cluster_info = df_cluster_stats.to_string()\n", + "\n", + "# Define the prompt template\n", + "prompt = f\"\"\"You're a real estate professional. Come up with a description of each cluster.\n", + "\n", + "Clusters:\n", + "{cluster_info}\n", + "\n", + "For each cluster, please return:\n", + "1. A category for the cluster that describes the type of houses\n", + "2. A couple of summary statistics - average price and square footage\n", + "3. Key characteristics of the cluster\n", + "4. A target buyer\n", + "\"\"\"" + ] + }, + { + "cell_type": "markdown", + "id": "EXWfa2Hsn2kY", + "metadata": { + "id": "EXWfa2Hsn2kY" + }, + "source": [ + "### Generate cluster descriptions with a Gemini model\n", + "\n", + "With the prompt and data prepared, you can now send the request to a Gemini model through the Vertex AI SDK to generate the descriptions." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "gU9iZu-BgQdQ", + "metadata": { + "id": "gU9iZu-BgQdQ" + }, + "outputs": [], + "source": [ + "from IPython.display import Markdown\n", + "from google import genai\n", + "\n", + "LOCATION = \"global\"\n", + "\n", + "client = genai.Client(vertexai=True, project=PROJECT_ID, location=LOCATION)\n", + "response = client.models.generate_content(\n", + " model=\"gemini-2.5-flash\",\n", + " contents=prompt,\n", + ")\n", + "\n", + "# Display the response\n", + "display(Markdown(response.text))" + ] + }, + { + "cell_type": "markdown", + "id": "t_E8RmodtbbP", + "metadata": { + "id": "t_E8RmodtbbP" + }, + "source": [ + "### Visualize a sample from each cluster\n", + "\n", + "To complement the generated text descriptions, you can also visualize a sample image from each cluster. This gives a quick visual check on what a sample from each cluster looks like." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "tI4JfgONkqrV", + "metadata": { + "id": "tI4JfgONkqrV" + }, + "outputs": [], + "source": [ + "%%bigquery df_example_images --project {PROJECT_ID}\n", + "\n", + "# Select one row per centroid_id and generate signed URLs\n", + "\n", + "SELECT\n", + " t1.centroid_id AS cluster,\n", + " ANY_VALUE(t2.image_ref).uri AS uri,\n", + " ANY_VALUE(t2.id) AS id\n", + "FROM\n", + " ML.PREDICT(MODEL `housing_dataset.kmeans_clustering_model`, TABLE `housing_dataset.listings_multimodal`) AS t1\n", + "JOIN\n", + " `housing_dataset.listings_multimodal` AS t2\n", + "ON\n", + " t1.id = t2.id\n", + "GROUP BY\n", + " cluster\n", + "ORDER BY\n", + " cluster" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "tNZEDhJhkr3H", + "metadata": { + "id": "tNZEDhJhkr3H" + }, + "outputs": [], + "source": [ + "display_images(df_example_images, uri_column=\"uri\", title_column=\"cluster\")" + ] + }, + { + "cell_type": "markdown", + "id": "iUco9FZnl0iR", + "metadata": { + "id": "iUco9FZnl0iR" + }, + "source": [ + "---\n", + "## Automate Modeling with the Data Science Agent\n", + "\n", + "Now, you'll explore a different approach to the same problem. Instead of writing SQL and Python code manually, you will use the integrated [**Data Science Agent**](https://cloud.google.com/bigquery/docs/colab-data-science-agent) to automatically generate code for a K-means clustering model.\n", + "\n", + "This section is a placeholder to demonstrate where you would switch to the agent. In a live environment, you would open a new notebook and use the following prompt to have the agent build the model for you.\n", + "\n", + "To use the Data Science Agent, follow these steps:\n", + "1. Follow [documentation steps](https://cloud.google.com/bigquery/docs/colab-data-science-agent#before_you_begin) to enable the proper APIs.\n", + "2. Open a new Colab Enterprise Notebook in BigQuery.\n", + "3. Open the Data Science Agent (it opens as a modal at the bottom of the screen or as a side pane).\n", + "4. Write `@housing_dataset.listings_multimodal` to @ select the proper table for context.\n", + "5. Give the Agent the following prompt and click-to-run the code that is generated: ```Use this table to generate a k-means clustering model to generate 3 clusters for housing listings. Then help me understand the characteristics of each of these clusters, so I can market to them as a real estate professional. Use Python.```\n", + "6. Feel free to ask follow-up questions, ask for SQL or Spark instead of Python, or simply play around with the integrated agent.\n", + "7. Once satisfied with the results, return back to this notebook for the next section." + ] + }, + { + "cell_type": "markdown", + "id": "sfHY9-E2OxgG", + "metadata": { + "id": "sfHY9-E2OxgG" + }, + "source": [ + "---\n", + "## Multimodal Search with Embeddings and Vector Search\n", + "\n", + "In this final section, you will use a multimodal embedding model to convert images into [vector embeddings](https://cloud.google.com/bigquery/docs/reference/standard-sql/bigqueryml-syntax-generate-embedding#embeddings) and then use BigQuery's built-in [vector search](https://cloud.google.com/bigquery/docs/vector-search-intro) capabilities to find similar houses based on a text description or another image." + ] + }, + { + "cell_type": "markdown", + "id": "64ORffYcmp_E", + "metadata": { + "id": "64ORffYcmp_E" + }, + "source": [ + "### Create a remote model for embeddings\n", + "\n", + "Create a remote model that points to the [`multimodalembedding@001`](https://cloud.google.com/vertex-ai/generative-ai/docs/embeddings/get-multimodal-embeddings) model in Vertex AI. This model converts images and text into high-dimensional numeric vectors, known as embeddings." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "zXKyKu9yPLbn", + "metadata": { + "id": "zXKyKu9yPLbn" + }, + "outputs": [], + "source": [ + "%%bigquery --project {PROJECT_ID}\n", + "\n", + "CREATE OR REPLACE MODEL housing_dataset.multimodal_embedding_model\n", + "REMOTE WITH CONNECTION DEFAULT\n", + "OPTIONS (ENDPOINT = 'multimodalembedding@001');" + ] + }, + { + "cell_type": "markdown", + "id": "7u3R0xGQm0Q7", + "metadata": { + "id": "7u3R0xGQm0Q7" + }, + "source": [ + "### Generate image embeddings\n", + "\n", + "Use [`ML.GENERATE_EMBEDDING`](https://cloud.google.com/bigquery/docs/reference/standard-sql/bigqueryml-syntax-generate-embedding) to pass each house image to the model. This creates a new table, `home_embeddings`, containing the original image reference and its corresponding 1408-dimension vector embedding." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1Z3WYs3sPf_D", + "metadata": { + "id": "1Z3WYs3sPf_D" + }, + "outputs": [], + "source": [ + "%%bigquery --project {PROJECT_ID}\n", + "\n", + "CREATE OR REPLACE TABLE housing_dataset.home_embeddings AS\n", + " SELECT\n", + " id,\n", + " price,\n", + " sq_ft,\n", + " prop_description,\n", + " image_ref,\n", + " ml_generate_embedding_result AS mm_embedding\n", + "FROM ML.GENERATE_EMBEDDING(\n", + " MODEL housing_dataset.multimodal_embedding_model,\n", + " (\n", + " SELECT *, image_ref AS content from housing_dataset.listings_multimodal),\n", + " STRUCT(TRUE AS flatten_json_output\n", + " )\n", + ");" + ] + }, + { + "cell_type": "markdown", + "id": "mFW3MzIMSvvu", + "metadata": { + "id": "mFW3MzIMSvvu" + }, + "source": [ + "### Review the generated embeddings\n", + "\n", + "Run a quick `SELECT` query to inspect the `home_embeddings` table. This is a good practice to verify that the `mm_embedding` column was created correctly before you build an index on it." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "xJzQpqNMRJOn", + "metadata": { + "id": "xJzQpqNMRJOn" + }, + "outputs": [], + "source": [ + "%%bigquery --project {PROJECT_ID}\n", + "\n", + "SELECT *\n", + "FROM housing_dataset.home_embeddings\n", + "LIMIT 2" + ] + }, + { + "cell_type": "markdown", + "id": "VP9QOrB5T86e", + "metadata": { + "id": "VP9QOrB5T86e" + }, + "source": [ + "### Create a vector index for efficient search\n", + "\n", + "To enable fast and scalable similarity searches, create a [`VECTOR INDEX`](https://cloud.google.com/bigquery/docs/vector-index) on the embedding column. The index allows BigQuery to find the nearest neighbors much more efficiently than a full table scan.\n", + "\n", + "Vector indexes are ideal for large datasets. Because we only have ~80 records here, we'll get an error that our data isn't large enough to benefit from an index." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2lQL9YG1SJX-", + "metadata": { + "id": "2lQL9YG1SJX-" + }, + "outputs": [], + "source": [ + "%%bigquery --project $PROJECT_ID\n", + "\n", + "CREATE OR REPLACE\n", + " VECTOR INDEX `house_images_index`\n", + "ON\n", + " housing_dataset.home_embeddings(mm_embedding)\n", + " OPTIONS (\n", + " index_type = 'IVF',\n", + " distance_type = 'COSINE');" + ] + }, + { + "cell_type": "markdown", + "id": "dVzjjsrqnt9T", + "metadata": { + "id": "dVzjjsrqnt9T" + }, + "source": [ + "### Perform text-to-image search\n", + "\n", + "Now, perform a similarity search using [`VECTOR_SEARCH`](https://cloud.google.com/bigquery/docs/reference/standard-sql/search_functions#vector_search). A subquery first generates an embedding for the text \"house near the ocean\" and the main query then finds the 3 images whose embeddings are most similar to that text query.\n", + "\n", + "Note that we're searching text-to-image without using metadata tags on the images." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "PGFdnrHyUG-9", + "metadata": { + "id": "PGFdnrHyUG-9" + }, + "outputs": [], + "source": [ + "%%bigquery text_to_image_df --project {PROJECT_ID}\n", + "\n", + "SELECT base.image_ref.uri\n", + " FROM\n", + " VECTOR_SEARCH(\n", + " TABLE `housing_dataset.home_embeddings`,\n", + " 'mm_embedding',\n", + " (\n", + " -- GENERATE AN EMBEDDING AS A SUBQUERY\n", + " SELECT\n", + " ml_generate_embedding_result,\n", + " content AS query\n", + " FROM\n", + " ML.GENERATE_EMBEDDING(\n", + " MODEL housing_dataset.multimodal_embedding_model,\n", + " ( SELECT \"house near the ocean\" AS content)\n", + " )\n", + " ),\n", + " top_k => 3)\n", + "ORDER BY distance ASC;" + ] + }, + { + "cell_type": "markdown", + "id": "NG3VLVXkdCOx", + "metadata": { + "id": "NG3VLVXkdCOx" + }, + "source": [ + "Next, let's visualize the top results returned from the `VECTOR_SEARCH`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7xrFhVGYWR0H", + "metadata": { + "id": "7xrFhVGYWR0H" + }, + "outputs": [], + "source": [ + "display_images(text_to_image_df)" + ] + }, + { + "cell_type": "markdown", + "id": "S2XgHbMlnaNS", + "metadata": { + "id": "S2XgHbMlnaNS" + }, + "source": [ + "### Perform image-to-image search\n", + "\n", + "The process for image-to-image search is nearly identical. Instead of using a text string, the subquery generates an embedding for a sample image URI, and `VECTOR_SEARCH` finds the most visually similar images." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "WqMDudECj5b7", + "metadata": { + "id": "WqMDudECj5b7" + }, + "outputs": [], + "source": [ + "# Get a sample house image from GCS\n", + "sample_house = f\"gs://{bucket_name}/images/test_image/house_test_image.jpg\"\n", + "image_df = pd.DataFrame({\"uri\": [sample_house]})\n", + "display_images(image_df)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4jMgLYBmWVsr", + "metadata": { + "id": "4jMgLYBmWVsr" + }, + "outputs": [], + "source": [ + "%%bigquery image_to_image_df --project {PROJECT_ID}\n", + "\n", + "SELECT base.image_ref.uri\n", + " FROM\n", + " VECTOR_SEARCH(\n", + " TABLE `housing_dataset.home_embeddings`,\n", + " 'mm_embedding',\n", + " (\n", + " -- GENERATE AN EMBEDDING AS A SUBQUERY\n", + " SELECT\n", + " ml_generate_embedding_result,\n", + " content AS query\n", + " FROM\n", + " ML.GENERATE_EMBEDDING(\n", + " MODEL housing_dataset.multimodal_embedding_model,\n", + " ( SELECT OBJ.FETCH_METADATA(OBJ.MAKE_REF('gs://drw001-data-science-with-notebooks/images/test_image/house_test_image.jpg', 'us.ai_connection')) AS content)\n", + " )\n", + " ),\n", + " top_k => 3)\n", + "ORDER BY distance ASC;" + ] + }, + { + "cell_type": "markdown", + "id": "4heV5z3SdIE-", + "metadata": { + "id": "4heV5z3SdIE-" + }, + "source": [ + "Visualize the results from the image-to-image vector search." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "YRG4GBtiX-5n", + "metadata": { + "id": "YRG4GBtiX-5n" + }, + "outputs": [], + "source": [ + "display_images(image_to_image_df)" + ] + }, + { + "cell_type": "markdown", + "id": "rm4jpiP7HEhP", + "metadata": { + "id": "rm4jpiP7HEhP" + }, + "source": [ + "---\n", + "\n", + "\n", + "## Cleaning Up\n", + "\n", + "To clean up all Google Cloud resources used in this project, you can [delete the Google Cloud project](https://cloud.google.com/resource-manager/docs/creating-managing-projects#shutting_down_projects) you used for the tutorial.\n", + "\n", + "Otherwise, you can delete the individual resources you created:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "JSf6m7Z7j213", + "metadata": { + "id": "JSf6m7Z7j213" + }, + "outputs": [], + "source": [ + "# Delete the BigQuery tables\n", + "!bq rm --table -f housing_dataset.listings\n", + "!bq rm --table -f housing_dataset.listings_multimodal\n", + "!bq rm --table -f housing_dataset.home_embeddings\n", + "\n", + "# Delete the remote model\n", + "!bq rm --model -f housing_dataset.gemini\n", + "!bq rm --model -f housing_dataset.kmeans_clustering_model\n", + "!bq rm --model -f housing_dataset.multimodal_embedding_model\n", + "\n", + "# Delete the remote connection\n", + "!bq rm --connection --project_id=$PROJECT_ID --location=us ai_connection\n", + "\n", + "# Delete the BigQuery dataset\n", + "!bq rm -r -f $PROJECT_ID:housing_dataset" + ] + } + ], + "metadata": { + "colab": { + "collapsed_sections": [ + "B8vBqblSM_9w" + ], + "name": "ai-assisted-data-science.ipynb", + "toc_visible": true + }, + "kernelspec": { + "display_name": "Python 3", + "name": "python3" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} diff --git a/gemini/use-cases/applying-llms-to-data/analyze-poster-images-in-bigquery/poster_image_analysis.ipynb b/gemini/use-cases/applying-llms-to-data/analyze-poster-images-in-bigquery/poster_image_analysis.ipynb new file mode 100644 index 0000000..60f57c5 --- /dev/null +++ b/gemini/use-cases/applying-llms-to-data/analyze-poster-images-in-bigquery/poster_image_analysis.ipynb @@ -0,0 +1,3670 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "gNz_7idNEdlE" + }, + "outputs": [], + "source": [ + "# Copyright 2026 Google LLC\n", + "#\n", + "# Licensed under the Apache License, Version 2.0 (the \"License\");\n", + "# you may not use this file except in compliance with the License.\n", + "# You may obtain a copy of the License at\n", + "#\n", + "# https://www.apache.org/licenses/LICENSE-2.0\n", + "#\n", + "# Unless required by applicable law or agreed to in writing, software\n", + "# distributed under the License is distributed on an \"AS IS\" BASIS,\n", + "# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n", + "# See the License for the specific language governing permissions and\n", + "# limitations under the License." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "IXbw-R0ZGiWf" + }, + "source": [ + "# Analyzing movie posters in BigQuery with Gemini\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
    \n", + " \n", + " \"Google
    Open in Colab\n", + "
    \n", + "
    \n", + " \n", + " \"Google
    Open in Colab Enterprise\n", + "
    \n", + "
    \n", + " \n", + " \"Vertex
    Open in Vertex AI Workbench\n", + "
    \n", + "
    \n", + " \n", + " \"BigQuery
    Open in BigQuery Studio\n", + "
    \n", + "
    \n", + " \n", + " \"GitHub
    View on GitHub\n", + "
    \n", + "
    \n", + "\n", + "
    \n", + "\n", + "Share to:\n", + "\n", + "\n", + " \"LinkedIn\n", + "\n", + "\n", + "\n", + " \"Bluesky\n", + "\n", + "\n", + "\n", + " \"X\n", + "\n", + "\n", + "\n", + " \"Reddit\n", + "\n", + "\n", + "\n", + " \"Facebook\n", + " " + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "4qTZ1THsriwh" + }, + "source": [ + "| Author |\n", + "| --- |\n", + "| [Alicia Williams](https://github.com/aliciawilliams) |" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "_cGtn8TvG7SB" + }, + "source": [ + "## Overview\n", + "\n", + "The objective is to demonstrate how to use the Gemini models for analysis in BigQuery, including:\n", + "\n", + "\n", + "* Completing setup steps for accessing Vertex AI (including Gemini models) from BigQuery\n", + "* Creating an Object Table for the unstructured dataset being analyzed\n", + "* Performing analysis over the object table using the Gemini model\n", + "* Joining analysis results back to structured table for deeper insights\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "sf3ioTDBzuHR" + }, + "source": [ + "## About the dataset\n", + "\n", + "The dataset of movie poster images used in this demo are stored in a public Google Cloud Storage bucket: [`gs://cloud-samples-data/vertex-ai/dataset-management/datasets/classic-movie-posters`](https://console.cloud.google.com/storage/browser/cloud-samples-data/vertex-ai/dataset-management/datasets/classic-movie-posters)\n", + "\n", + "![Movie Posters](https://storage.googleapis.com/github-repo/generative-ai/gemini/use-cases/applying-llms-to-data/movie-posters.jpeg)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "E8sI7ir1fVgI" + }, + "source": [ + "## Services and Costs\n", + "\n", + "This tutorial uses the following Google Cloud data analytics and ML services, they are billable components of Google Cloud:\n", + "\n", + "* BigQuery & BigQuery ML [(pricing)](https://cloud.google.com/bigquery/pricing)\n", + "* Vertex AI API [(pricing)](https://cloud.google.com/vertex-ai/pricing)\n", + "\n", + "Use the [Pricing Calculator](https://cloud.google.com/products/calculator/) to generate a cost estimate based on your projected usage.\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "P10XEmXlzeOF" + }, + "source": [ + "# Setup steps for accessing Cloud Storage and Vertex AI models from BigQuery" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "6xxnNycUFs8Z" + }, + "source": [ + "## Enable the Vertex AI and BigQuery Connection APIs" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "82RF-HLRFhVn" + }, + "outputs": [], + "source": [ + "!gcloud services enable aiplatform.googleapis.com bigqueryconnection.googleapis.com" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "-_fb9Y_t94a_" + }, + "source": [ + "## Create a Cloud resource connection\n", + "\n", + "*Note: This demo analyzes a dataset that sits in a public Cloud Storage bucket. When analyzing a dataset in a non-public bucket, the service account associated with the Cloud resource connection will also need \"Storage Object Viewer\" role on that storage bucket using [these instructions](https://docs.cloud.google.com/bigquery/docs/create-cloud-resource-connection#access-storage).*" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "zHgqlk3B95BE" + }, + "outputs": [], + "source": [ + "!bq mk --connection --location=us \\\n", + " --connection_type=CLOUD_RESOURCE gemini_conn" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "B-fcQvmo8bcU" + }, + "source": [ + "## Configure end user permissions\n", + "\n", + "In order to run a query job that calls a Vertex AI model, your user account needs the following IAM roles:\n", + "\n", + "* BigQuery Job User (`roles/bigquery.jobUser`)\n", + "\n", + "* Vertex AI User (`roles/aiplatform.user`)\n", + "\n", + "You can follow [these instructions](https://docs.cloud.google.com/bigquery/docs/permissions-for-ai-functions#grant_the_required_roles_to_the_user_or_group) for how to grant these roles.\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "0TU6VxJA9bRI" + }, + "source": [ + "# Create an object table for the movie poster images" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "fm840uHo4kHP" + }, + "source": [ + "## Create a new dataset named `'gemini_demo'`" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "fdSiqoR04jeQ" + }, + "outputs": [], + "source": [ + "%%bigquery\n", + "CREATE SCHEMA\n", + " `gemini_demo` OPTIONS (location = 'US');" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "PaLtG8c3GZFi" + }, + "source": [ + "## Create an object table referencing Google Cloud Storage bucket" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "T97do1tjGZsm" + }, + "outputs": [], + "source": [ + "%%bigquery\n", + "CREATE OR REPLACE EXTERNAL TABLE\n", + " `gemini_demo.movie_posters`\n", + "WITH CONNECTION `us.gemini_conn`\n", + "OPTIONS (\n", + " object_metadata = 'SIMPLE',\n", + " uris = ['gs://cloud-samples-data/vertex-ai/dataset-management/datasets/classic-movie-posters/*']\n", + " );" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": { + "id": "Xqh_kLTcYR2n" + }, + "outputs": [ + { + "data": { + "text/plain": [ + "Query is running: 0%| |" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + "Downloading: 0%| |" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/vnd.google.colaboratory.intrinsic+json": { + "summary": "{\n \"name\": \"get_ipython()\",\n \"rows\": 5,\n \"fields\": [\n {\n \"column\": \"uri\",\n \"properties\": {\n \"dtype\": \"string\",\n \"num_unique_values\": 5,\n \"samples\": [\n \"gs://cloud-samples-data/vertex-ai/dataset-management/datasets/classic-movie-posters/barque_sortant_du_port.jpeg\",\n \"gs://cloud-samples-data/vertex-ai/dataset-management/datasets/classic-movie-posters/der_student_von_prag.jpg\",\n \"gs://cloud-samples-data/vertex-ai/dataset-management/datasets/classic-movie-posters/battling_butler.jpg\"\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"generation\",\n \"properties\": {\n \"dtype\": \"Int64\",\n \"num_unique_values\": 5,\n \"samples\": [\n 1683653080024577,\n 1683653080624441,\n 1683653080233711\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"content_type\",\n \"properties\": {\n \"dtype\": \"category\",\n \"num_unique_values\": 1,\n \"samples\": [\n \"image/jpeg\"\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"size\",\n \"properties\": {\n \"dtype\": \"Int64\",\n \"num_unique_values\": 5,\n \"samples\": [\n 30086\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"md5_hash\",\n \"properties\": {\n \"dtype\": \"string\",\n \"num_unique_values\": 5,\n \"samples\": [\n \"1a831e349e8f954abc9fabe120fb9220\"\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"updated\",\n \"properties\": {\n \"dtype\": \"date\",\n \"min\": \"2023-05-09 17:24:39.856000+00:00\",\n \"max\": \"2023-05-09 17:24:40.669000+00:00\",\n \"num_unique_values\": 5,\n \"samples\": [\n \"2023-05-09 17:24:40.069000+00:00\"\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"metadata\",\n \"properties\": {\n \"dtype\": \"object\",\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"ref\",\n \"properties\": {\n \"dtype\": \"object\",\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n }\n ]\n}", + "type": "dataframe" + }, + "text/html": [ + "\n", + "
    \n", + "
    \n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
    urigenerationcontent_typesizemd5_hashupdatedmetadataref
    0gs://cloud-samples-data/vertex-ai/dataset-mana...1683653079811418image/jpeg819528b48292aba74c3c1812ca7bdd0be7fa72023-05-09 17:24:39.856000+00:00[]{'uri': 'gs://cloud-samples-data/vertex-ai/dat...
    1gs://cloud-samples-data/vertex-ai/dataset-mana...1683653080024577image/jpeg300861a831e349e8f954abc9fabe120fb92202023-05-09 17:24:40.069000+00:00[]{'uri': 'gs://cloud-samples-data/vertex-ai/dat...
    2gs://cloud-samples-data/vertex-ai/dataset-mana...1683653080233711image/jpeg147585c18dd6e6a87d75a95801c47f975457062023-05-09 17:24:40.280000+00:00[]{'uri': 'gs://cloud-samples-data/vertex-ai/dat...
    3gs://cloud-samples-data/vertex-ai/dataset-mana...1683653080449371image/jpeg10782193d93b6aacb9b1d8ace3c037d683a9612023-05-09 17:24:40.495000+00:00[]{'uri': 'gs://cloud-samples-data/vertex-ai/dat...
    4gs://cloud-samples-data/vertex-ai/dataset-mana...1683653080624441image/jpeg96294c78573f7f29b9da284b1a2dda85654e92023-05-09 17:24:40.669000+00:00[]{'uri': 'gs://cloud-samples-data/vertex-ai/dat...
    \n", + "
    \n", + "
    \n", + "\n", + "
    \n", + " \n", + "\n", + " \n", + "\n", + " \n", + "
    \n", + "\n", + "\n", + "
    \n", + " \n", + "\n", + "\n", + "\n", + " \n", + "
    \n", + "\n", + "
    \n", + "
    \n" + ], + "text/plain": [ + " uri generation \\\n", + "0 gs://cloud-samples-data/vertex-ai/dataset-mana... 1683653079811418 \n", + "1 gs://cloud-samples-data/vertex-ai/dataset-mana... 1683653080024577 \n", + "2 gs://cloud-samples-data/vertex-ai/dataset-mana... 1683653080233711 \n", + "3 gs://cloud-samples-data/vertex-ai/dataset-mana... 1683653080449371 \n", + "4 gs://cloud-samples-data/vertex-ai/dataset-mana... 1683653080624441 \n", + "\n", + " content_type size md5_hash \\\n", + "0 image/jpeg 81952 8b48292aba74c3c1812ca7bdd0be7fa7 \n", + "1 image/jpeg 30086 1a831e349e8f954abc9fabe120fb9220 \n", + "2 image/jpeg 147585 c18dd6e6a87d75a95801c47f97545706 \n", + "3 image/jpeg 107821 93d93b6aacb9b1d8ace3c037d683a961 \n", + "4 image/jpeg 96294 c78573f7f29b9da284b1a2dda85654e9 \n", + "\n", + " updated metadata \\\n", + "0 2023-05-09 17:24:39.856000+00:00 [] \n", + "1 2023-05-09 17:24:40.069000+00:00 [] \n", + "2 2023-05-09 17:24:40.280000+00:00 [] \n", + "3 2023-05-09 17:24:40.495000+00:00 [] \n", + "4 2023-05-09 17:24:40.669000+00:00 [] \n", + "\n", + " ref \n", + "0 {'uri': 'gs://cloud-samples-data/vertex-ai/dat... \n", + "1 {'uri': 'gs://cloud-samples-data/vertex-ai/dat... \n", + "2 {'uri': 'gs://cloud-samples-data/vertex-ai/dat... \n", + "3 {'uri': 'gs://cloud-samples-data/vertex-ai/dat... \n", + "4 {'uri': 'gs://cloud-samples-data/vertex-ai/dat... " + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "%%bigquery\n", + "SELECT * FROM `gemini_demo.movie_posters` LIMIT 5" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "JcYVG4qNKAuY" + }, + "source": [ + "# Perform analysis on the movie poster images" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "KCZ-nme8GjTN" + }, + "source": [ + "## Prompt Gemini to analyze the movie poster image object table" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "iLN9ecDLXJPe" + }, + "outputs": [], + "source": [ + "%%bigquery\n", + "CREATE OR REPLACE TABLE\n", + " `gemini_demo.movie_posters_results` AS (\n", + " SELECT\n", + " uri,\n", + " STRING(OBJ.GET_ACCESS_URL(ref,'r').access_urls.read_url) AS signed_url,\n", + " AI.GENERATE(\n", + " prompt => ('What is the movie title and year of release for this poster?', OBJ.GET_ACCESS_URL(ref, 'r')),\n", + " output_schema => 'title STRING, year INT64'\n", + " ).* EXCEPT(full_response,status)\n", + " FROM\n", + " `gemini_demo.movie_posters`\n", + ");" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": { + "id": "8bcyGsNWHPeZ" + }, + "outputs": [ + { + "data": { + "text/plain": [ + "Query is running: 0%| |" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + "Downloading: 0%| |" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/vnd.google.colaboratory.intrinsic+json": { + "summary": "{\n \"name\": \"get_ipython()\",\n \"rows\": 19,\n \"fields\": [\n {\n \"column\": \"uri\",\n \"properties\": {\n \"dtype\": \"string\",\n \"num_unique_values\": 19,\n \"samples\": [\n \"gs://cloud-samples-data/vertex-ai/dataset-management/datasets/classic-movie-posters/barque_sortant_du_port.jpeg\",\n \"gs://cloud-samples-data/vertex-ai/dataset-management/datasets/classic-movie-posters/shoulder_arms.jpeg\",\n \"gs://cloud-samples-data/vertex-ai/dataset-management/datasets/classic-movie-posters/little_annie_rooney.jpg\"\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"signed_url\",\n \"properties\": {\n \"dtype\": \"string\",\n \"num_unique_values\": 19,\n \"samples\": [\n \"https://storage.googleapis.com/cloud-samples-data/vertex-ai%2Fdataset-management%2Fdatasets%2Fclassic-movie-posters%2Fbarque_sortant_du_port.jpeg?X-Goog-Algorithm=GOOG4-RSA-SHA256&X-Goog-Credential=bqcx-735508242970-dy4t%40gcp-sa-bigquery-condel.iam.gserviceaccount.com%2F20260227%2Fauto%2Fstorage%2Fgoog4_request&X-Goog-Date=20260227T212652Z&X-Goog-Expires=21600&X-Goog-SignedHeaders=host&generation=1683653080024577&X-Goog-Signature=6461a0202b488317133e0413080a2afb07ba2936abee619958ff2ed4bad61de1ce80db6bd44598e783fea15809c1fea6ef3daccb78940f20cf42fdaadd614d5739aff3353cddcac4b0d2f41b117f34ebc53b97f18c4cc995da7b389c25ae0e6d923b018bc0f5d2e8200ce47e01bf22fba8c30cc59b2f6f33abbce44142fbda8d0f358debb2d97607155a4505c559f70f918741de76da570973852d7d3d51433051fb39944cd86006182a72ab7fe7dddb59669e40ab4f9d4b59e8b8c0182cc76fbad230a7db98d741f106cfb5b17f9f8c03858d2f1479cd4794b619d633117df2c8cb77c7c8c50f2cf8b8026e9776e2c0d8d81a80909532e5f1545c53de7ded1f\",\n \"https://storage.googleapis.com/cloud-samples-data/vertex-ai%2Fdataset-management%2Fdatasets%2Fclassic-movie-posters%2Fshoulder_arms.jpeg?X-Goog-Algorithm=GOOG4-RSA-SHA256&X-Goog-Credential=bqcx-735508242970-dy4t%40gcp-sa-bigquery-condel.iam.gserviceaccount.com%2F20260227%2Fauto%2Fstorage%2Fgoog4_request&X-Goog-Date=20260227T212652Z&X-Goog-Expires=21600&X-Goog-SignedHeaders=host&generation=1683653082560296&X-Goog-Signature=930dbf0a667ebd04d760f51cefad2807ecdd10476d453c1e3c57ccbbd3ba3555c8b60f0bd5075536c96f690ed740ab4b2e098a9bb7c6d2705f16811cdf23e239121fa08cd748e2547809d21ceb0724fe606dd0c212df7588320ab8ccc0b1c5e69c1fd69645706d25de1db6c6d45dbc230dbb59d32906015c228e6b0cdf17facc9907d19cef8c97f0ef01f0722af9389f124aadb4433d87a4715f9a052bcff1a80a5497a2b9c9a09c57f23ab67d46abbd4c7f602e7c2f406e4616c53a337446f4e0ac55e2e322c3ff95515a47d8b94b524d6c551c2d0ac72364fb17670bf1fa58c3cc7051f78ceadd04d8e5d29b8eb4121b9179ada3882a3e41f9571b70a5601f\",\n \"https://storage.googleapis.com/cloud-samples-data/vertex-ai%2Fdataset-management%2Fdatasets%2Fclassic-movie-posters%2Flittle_annie_rooney.jpg?X-Goog-Algorithm=GOOG4-RSA-SHA256&X-Goog-Credential=bqcx-735508242970-dy4t%40gcp-sa-bigquery-condel.iam.gserviceaccount.com%2F20260227%2Fauto%2Fstorage%2Fgoog4_request&X-Goog-Date=20260227T212652Z&X-Goog-Expires=21600&X-Goog-SignedHeaders=host&generation=1683653081385488&X-Goog-Signature=98fb370979cf232710e31a73fd8fcbe7dcaf39e07b71db3beebeedd8717b68bca854883fb4e249bc520e761680c08cec0b180aa7060f0f69f16a4bc1a5d0e27cbe9447eaa3c93191d4a91e3ff197a632e979f7ae1117aa01f427115c9e14494371f593605663c5e71b8e4f7662a05f210141367ed4b3c3b8e65ea78b4c6fc00e801e145f5e168b3e0bfc695cdbec1b1435f17c723cf12049ccafaad3d185a51ad2446e6f11fe5c483362db53d3fb6efec2fbd87f0400f3f84bad6d32f788ed07747e1509e92a09b38ece0c500169148aaf96160932bc9ca1f57529126979dad97c96d1cb87409255d1a9d82350e1958352ce767b8db5588a327c26d1ef1cd928\"\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"title\",\n \"properties\": {\n \"dtype\": \"string\",\n \"num_unique_values\": 19,\n \"samples\": [\n \"Barque sortant du port\",\n \"Shoulder Arms\",\n \"Little Annie Rooney\"\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"year\",\n \"properties\": {\n \"dtype\": \"Int64\",\n \"num_unique_values\": 13,\n \"samples\": [\n 1927,\n 1925,\n 1895\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n }\n ]\n}", + "type": "dataframe" + }, + "text/html": [ + "\n", + "
    \n", + "
    \n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
    urisigned_urltitleyear
    0gs://cloud-samples-data/vertex-ai/dataset-mana...https://storage.googleapis.com/cloud-samples-d...Barque sortant du port1895
    1gs://cloud-samples-data/vertex-ai/dataset-mana...https://storage.googleapis.com/cloud-samples-d...The Great Train Robbery1903
    2gs://cloud-samples-data/vertex-ai/dataset-mana...https://storage.googleapis.com/cloud-samples-d...Mabel's Busy Day1914
    3gs://cloud-samples-data/vertex-ai/dataset-mana...https://storage.googleapis.com/cloud-samples-d...Les Vampires1915
    4gs://cloud-samples-data/vertex-ai/dataset-mana...https://storage.googleapis.com/cloud-samples-d...He Did and He Didn't1916
    5gs://cloud-samples-data/vertex-ai/dataset-mana...https://storage.googleapis.com/cloud-samples-d...Shoulder Arms1918
    6gs://cloud-samples-data/vertex-ai/dataset-mana...https://storage.googleapis.com/cloud-samples-d...Little Lord Fauntleroy1921
    7gs://cloud-samples-data/vertex-ai/dataset-mana...https://storage.googleapis.com/cloud-samples-d...Felix in Hollywood1923
    8gs://cloud-samples-data/vertex-ai/dataset-mana...https://storage.googleapis.com/cloud-samples-d...West of Hot Dog1924
    9gs://cloud-samples-data/vertex-ai/dataset-mana...https://storage.googleapis.com/cloud-samples-d...AU SECOURS!1924
    10gs://cloud-samples-data/vertex-ai/dataset-mana...https://storage.googleapis.com/cloud-samples-d...Sally of the Sawdust1925
    11gs://cloud-samples-data/vertex-ai/dataset-mana...https://storage.googleapis.com/cloud-samples-d...Little Annie Rooney1925
    12gs://cloud-samples-data/vertex-ai/dataset-mana...https://storage.googleapis.com/cloud-samples-d...The Lost World1925
    13gs://cloud-samples-data/vertex-ai/dataset-mana...https://storage.googleapis.com/cloud-samples-d...Der Student von Prag1926
    14gs://cloud-samples-data/vertex-ai/dataset-mana...https://storage.googleapis.com/cloud-samples-d...Battling Butler1926
    15gs://cloud-samples-data/vertex-ai/dataset-mana...https://storage.googleapis.com/cloud-samples-d...Brown of Harvard1926
    16gs://cloud-samples-data/vertex-ai/dataset-mana...https://storage.googleapis.com/cloud-samples-d...Putting Pants on Philip1927
    17gs://cloud-samples-data/vertex-ai/dataset-mana...https://storage.googleapis.com/cloud-samples-d...The Beloved Rogue1927
    18gs://cloud-samples-data/vertex-ai/dataset-mana...https://storage.googleapis.com/cloud-samples-d...The Merry Widow1934
    \n", + "
    \n", + "
    \n", + "\n", + "
    \n", + " \n", + "\n", + " \n", + "\n", + " \n", + "
    \n", + "\n", + "\n", + "
    \n", + " \n", + "\n", + "\n", + "\n", + " \n", + "
    \n", + "\n", + "
    \n", + "
    \n" + ], + "text/plain": [ + " uri \\\n", + "0 gs://cloud-samples-data/vertex-ai/dataset-mana... \n", + "1 gs://cloud-samples-data/vertex-ai/dataset-mana... \n", + "2 gs://cloud-samples-data/vertex-ai/dataset-mana... \n", + "3 gs://cloud-samples-data/vertex-ai/dataset-mana... \n", + "4 gs://cloud-samples-data/vertex-ai/dataset-mana... \n", + "5 gs://cloud-samples-data/vertex-ai/dataset-mana... \n", + "6 gs://cloud-samples-data/vertex-ai/dataset-mana... \n", + "7 gs://cloud-samples-data/vertex-ai/dataset-mana... \n", + "8 gs://cloud-samples-data/vertex-ai/dataset-mana... \n", + "9 gs://cloud-samples-data/vertex-ai/dataset-mana... \n", + "10 gs://cloud-samples-data/vertex-ai/dataset-mana... \n", + "11 gs://cloud-samples-data/vertex-ai/dataset-mana... \n", + "12 gs://cloud-samples-data/vertex-ai/dataset-mana... \n", + "13 gs://cloud-samples-data/vertex-ai/dataset-mana... \n", + "14 gs://cloud-samples-data/vertex-ai/dataset-mana... \n", + "15 gs://cloud-samples-data/vertex-ai/dataset-mana... \n", + "16 gs://cloud-samples-data/vertex-ai/dataset-mana... \n", + "17 gs://cloud-samples-data/vertex-ai/dataset-mana... \n", + "18 gs://cloud-samples-data/vertex-ai/dataset-mana... \n", + "\n", + " signed_url \\\n", + "0 https://storage.googleapis.com/cloud-samples-d... \n", + "1 https://storage.googleapis.com/cloud-samples-d... \n", + "2 https://storage.googleapis.com/cloud-samples-d... \n", + "3 https://storage.googleapis.com/cloud-samples-d... \n", + "4 https://storage.googleapis.com/cloud-samples-d... \n", + "5 https://storage.googleapis.com/cloud-samples-d... \n", + "6 https://storage.googleapis.com/cloud-samples-d... \n", + "7 https://storage.googleapis.com/cloud-samples-d... \n", + "8 https://storage.googleapis.com/cloud-samples-d... \n", + "9 https://storage.googleapis.com/cloud-samples-d... \n", + "10 https://storage.googleapis.com/cloud-samples-d... \n", + "11 https://storage.googleapis.com/cloud-samples-d... \n", + "12 https://storage.googleapis.com/cloud-samples-d... \n", + "13 https://storage.googleapis.com/cloud-samples-d... \n", + "14 https://storage.googleapis.com/cloud-samples-d... \n", + "15 https://storage.googleapis.com/cloud-samples-d... \n", + "16 https://storage.googleapis.com/cloud-samples-d... \n", + "17 https://storage.googleapis.com/cloud-samples-d... \n", + "18 https://storage.googleapis.com/cloud-samples-d... \n", + "\n", + " title year \n", + "0 Barque sortant du port 1895 \n", + "1 The Great Train Robbery 1903 \n", + "2 Mabel's Busy Day 1914 \n", + "3 Les Vampires 1915 \n", + "4 He Did and He Didn't 1916 \n", + "5 Shoulder Arms 1918 \n", + "6 Little Lord Fauntleroy 1921 \n", + "7 Felix in Hollywood 1923 \n", + "8 West of Hot Dog 1924 \n", + "9 AU SECOURS! 1924 \n", + "10 Sally of the Sawdust 1925 \n", + "11 Little Annie Rooney 1925 \n", + "12 The Lost World 1925 \n", + "13 Der Student von Prag 1926 \n", + "14 Battling Butler 1926 \n", + "15 Brown of Harvard 1926 \n", + "16 Putting Pants on Philip 1927 \n", + "17 The Beloved Rogue 1927 \n", + "18 The Merry Widow 1934 " + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "%%bigquery\n", + "SELECT * FROM `gemini_demo.movie_posters_results`" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "M4swkWSH08yE" + }, + "source": [ + "## Prompt Gemini to provide movie summaries for each movie poster image" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "metadata": { + "id": "TxRXjZbAZyI8" + }, + "outputs": [ + { + "data": { + "text/plain": [ + "Query is running: 0%| |" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + "Downloading: 0%| |" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/vnd.google.colaboratory.intrinsic+json": { + "summary": "{\n \"name\": \"get_ipython()\",\n \"rows\": 19,\n \"fields\": [\n {\n \"column\": \"uri\",\n \"properties\": {\n \"dtype\": \"string\",\n \"num_unique_values\": 19,\n \"samples\": [\n \"gs://cloud-samples-data/vertex-ai/dataset-management/datasets/classic-movie-posters/mabels_busy_day.jpeg\",\n \"gs://cloud-samples-data/vertex-ai/dataset-management/datasets/classic-movie-posters/little_annie_rooney.jpg\",\n \"gs://cloud-samples-data/vertex-ai/dataset-management/datasets/classic-movie-posters/barque_sortant_du_port.jpeg\"\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"title\",\n \"properties\": {\n \"dtype\": \"string\",\n \"num_unique_values\": 19,\n \"samples\": [\n \"Mabel's Busy Day\",\n \"Little Annie Rooney\",\n \"Barque sortant du port\"\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"year\",\n \"properties\": {\n \"dtype\": \"Int64\",\n \"num_unique_values\": 13,\n \"samples\": [\n 1923,\n 1895,\n 1914\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"movie_summary\",\n \"properties\": {\n \"dtype\": \"string\",\n \"num_unique_values\": 19,\n \"samples\": [\n \"In \\\"Mabel's Busy Day\\\" (1914), Mabel Normand plays a struggling hot dog vendor at a busy racetrack. Her attempts to sell her frankfurters are constantly thwarted and complicated by the arrival of Charlie Chaplin's mischievous Tramp character.\\n\\nThe hungry Tramp repeatedly tries to steal Mabel's hot dogs, leading to a series of comedic misunderstandings, chases, and typical Keystone slapstick antics as Mabel tries to protect her livelihood from the persistent thief and the general chaos of the day.\",\n \"\\\"Little Annie Rooney\\\" (1925) stars Mary Pickford as the titular character, a feisty and independent tomboy growing up in a tough, working-class Irish-American neighborhood in New York. Her beloved father is a respected police officer.\\n\\nAnnie often finds herself in the middle of street brawls between rival youth gangs. Tragedy strikes when her father is accidentally killed during one such gang altercation. Devastated and seeking justice, Annie initially vows revenge. However, she finds an unexpected ally and eventual love interest in Tony Harrison, a young man from one of the rival factions.\\n\\nThe film follows Annie's journey from grief and anger towards understanding and forgiveness, ultimately leading to a resolution that sees her embrace a more mature role and find happiness amidst the challenges of her community. It's a classic Mary Pickford vehicle, showcasing her energetic persona in a story of street-level drama and enduring love.\",\n \"\\\"Barque sortant du port\\\" (Boat Leaving the Port) is a short, silent, black-and-white documentary film from 1895, produced by the **Lumi\\u00e8re Brothers**.\\n\\nIt features a single, static shot of a small sailboat (a \\\"barque\\\") being pushed out of a harbor or quay by several men using poles. The film captures a simple, everyday scene of manual labor and maritime activity, showcasing the nascent art of cinema's ability to record real-life events. It is one of the very earliest films ever made.\"\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n }\n ]\n}", + "type": "dataframe" + }, + "text/html": [ + "\n", + "
    \n", + "
    \n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
    urititleyearmovie_summary
    0gs://cloud-samples-data/vertex-ai/dataset-mana...Mabel's Busy Day1914In \"Mabel's Busy Day\" (1914), Mabel Normand pl...
    1gs://cloud-samples-data/vertex-ai/dataset-mana...He Did and He Didn't1916\"He Did and He Didn't\" (1916) is a silent slap...
    2gs://cloud-samples-data/vertex-ai/dataset-mana...Der Student von Prag1926\"Der Student von Prag\" (1926) is a classic Ger...
    3gs://cloud-samples-data/vertex-ai/dataset-mana...The Lost World1925The 1925 silent film **The Lost World** is bas...
    4gs://cloud-samples-data/vertex-ai/dataset-mana...Shoulder Arms1918Charlie Chaplin stars as a bumbling, raw Ameri...
    5gs://cloud-samples-data/vertex-ai/dataset-mana...Little Annie Rooney1925\"Little Annie Rooney\" (1925) stars Mary Pickfo...
    6gs://cloud-samples-data/vertex-ai/dataset-mana...The Beloved Rogue1927\"The Beloved Rogue\" (1927) stars John Barrymor...
    7gs://cloud-samples-data/vertex-ai/dataset-mana...The Great Train Robbery1903\"The Great Train Robbery\" (1903) is a landmark...
    8gs://cloud-samples-data/vertex-ai/dataset-mana...Little Lord Fauntleroy1921The 1921 silent film *Little Lord Fauntleroy* ...
    9gs://cloud-samples-data/vertex-ai/dataset-mana...West of Hot Dog1924\"West of Hot Dog\" is a 1924 silent Western com...
    10gs://cloud-samples-data/vertex-ai/dataset-mana...Battling Butler1926The 1926 silent comedy **Battling Butler** sta...
    11gs://cloud-samples-data/vertex-ai/dataset-mana...Barque sortant du port1895\"Barque sortant du port\" (Boat Leaving the Por...
    12gs://cloud-samples-data/vertex-ai/dataset-mana...Les Vampires1915**Les Vampires** is a 1915 French silent crime...
    13gs://cloud-samples-data/vertex-ai/dataset-mana...Felix in Hollywood1923In the 1923 animated short **\"Felix in Hollywo...
    14gs://cloud-samples-data/vertex-ai/dataset-mana...Sally of the Sawdust1925The 1925 silent film **Sally of the Sawdust** ...
    15gs://cloud-samples-data/vertex-ai/dataset-mana...Brown of Harvard1926The 1926 silent film **\"Brown of Harvard\"** st...
    16gs://cloud-samples-data/vertex-ai/dataset-mana...Putting Pants on Philip1927\"Putting Pants on Philip,\" a 1927 silent film,...
    17gs://cloud-samples-data/vertex-ai/dataset-mana...The Merry Widow1934Ernst Lubitsch's 1934 musical comedy **The Mer...
    18gs://cloud-samples-data/vertex-ai/dataset-mana...AU SECOURS!1924The 1924 French silent film **AU SECOURS!** (H...
    \n", + "
    \n", + "
    \n", + "\n", + "
    \n", + " \n", + "\n", + " \n", + "\n", + " \n", + "
    \n", + "\n", + "\n", + "
    \n", + " \n", + "\n", + "\n", + "\n", + " \n", + "
    \n", + "\n", + "
    \n", + "
    \n" + ], + "text/plain": [ + " uri \\\n", + "0 gs://cloud-samples-data/vertex-ai/dataset-mana... \n", + "1 gs://cloud-samples-data/vertex-ai/dataset-mana... \n", + "2 gs://cloud-samples-data/vertex-ai/dataset-mana... \n", + "3 gs://cloud-samples-data/vertex-ai/dataset-mana... \n", + "4 gs://cloud-samples-data/vertex-ai/dataset-mana... \n", + "5 gs://cloud-samples-data/vertex-ai/dataset-mana... \n", + "6 gs://cloud-samples-data/vertex-ai/dataset-mana... \n", + "7 gs://cloud-samples-data/vertex-ai/dataset-mana... \n", + "8 gs://cloud-samples-data/vertex-ai/dataset-mana... \n", + "9 gs://cloud-samples-data/vertex-ai/dataset-mana... \n", + "10 gs://cloud-samples-data/vertex-ai/dataset-mana... \n", + "11 gs://cloud-samples-data/vertex-ai/dataset-mana... \n", + "12 gs://cloud-samples-data/vertex-ai/dataset-mana... \n", + "13 gs://cloud-samples-data/vertex-ai/dataset-mana... \n", + "14 gs://cloud-samples-data/vertex-ai/dataset-mana... \n", + "15 gs://cloud-samples-data/vertex-ai/dataset-mana... \n", + "16 gs://cloud-samples-data/vertex-ai/dataset-mana... \n", + "17 gs://cloud-samples-data/vertex-ai/dataset-mana... \n", + "18 gs://cloud-samples-data/vertex-ai/dataset-mana... \n", + "\n", + " title year \\\n", + "0 Mabel's Busy Day 1914 \n", + "1 He Did and He Didn't 1916 \n", + "2 Der Student von Prag 1926 \n", + "3 The Lost World 1925 \n", + "4 Shoulder Arms 1918 \n", + "5 Little Annie Rooney 1925 \n", + "6 The Beloved Rogue 1927 \n", + "7 The Great Train Robbery 1903 \n", + "8 Little Lord Fauntleroy 1921 \n", + "9 West of Hot Dog 1924 \n", + "10 Battling Butler 1926 \n", + "11 Barque sortant du port 1895 \n", + "12 Les Vampires 1915 \n", + "13 Felix in Hollywood 1923 \n", + "14 Sally of the Sawdust 1925 \n", + "15 Brown of Harvard 1926 \n", + "16 Putting Pants on Philip 1927 \n", + "17 The Merry Widow 1934 \n", + "18 AU SECOURS! 1924 \n", + "\n", + " movie_summary \n", + "0 In \"Mabel's Busy Day\" (1914), Mabel Normand pl... \n", + "1 \"He Did and He Didn't\" (1916) is a silent slap... \n", + "2 \"Der Student von Prag\" (1926) is a classic Ger... \n", + "3 The 1925 silent film **The Lost World** is bas... \n", + "4 Charlie Chaplin stars as a bumbling, raw Ameri... \n", + "5 \"Little Annie Rooney\" (1925) stars Mary Pickfo... \n", + "6 \"The Beloved Rogue\" (1927) stars John Barrymor... \n", + "7 \"The Great Train Robbery\" (1903) is a landmark... \n", + "8 The 1921 silent film *Little Lord Fauntleroy* ... \n", + "9 \"West of Hot Dog\" is a 1924 silent Western com... \n", + "10 The 1926 silent comedy **Battling Butler** sta... \n", + "11 \"Barque sortant du port\" (Boat Leaving the Por... \n", + "12 **Les Vampires** is a 1915 French silent crime... \n", + "13 In the 1923 animated short **\"Felix in Hollywo... \n", + "14 The 1925 silent film **Sally of the Sawdust** ... \n", + "15 The 1926 silent film **\"Brown of Harvard\"** st... \n", + "16 \"Putting Pants on Philip,\" a 1927 silent film,... \n", + "17 Ernst Lubitsch's 1934 musical comedy **The Mer... \n", + "18 The 1924 French silent film **AU SECOURS!** (H... " + ] + }, + "execution_count": 18, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "%%bigquery\n", + "SELECT\n", + " uri,\n", + " title,\n", + " year,\n", + " AI.GENERATE(\n", + " prompt => ('Provide a short summary of movie titled ' || title || ' from the year ' || year)\n", + " ).result AS movie_summary\n", + "FROM\n", + " `gemini_demo.movie_posters_results`" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "Vj7gGTPy2qpQ" + }, + "source": [ + "# Join analysis results with structured data for deeper insights\n", + "Using text embeddings to perform similarity joins of movie poster images to a movie reviews dataset in BigQuery" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "woZDvRuV11IQ" + }, + "source": [ + "## Generate text embeddings for title and year associated with movie posters" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "VbC6MUnEbN8q" + }, + "outputs": [], + "source": [ + "%%bigquery\n", + "CREATE OR REPLACE TABLE\n", + " `gemini_demo.movie_posters_results_embeddings` AS (\n", + " SELECT\n", + " title,\n", + " year,\n", + " uri,\n", + " AI.EMBED(\n", + " content => 'The movie titled ' || title || ' from the year ' || year,\n", + " endpoint => 'gemini-embedding-001'\n", + " ).result AS embedding\n", + " FROM\n", + " `gemini_demo.movie_posters_results`\n", + ");" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": { + "id": "0PDZT8T52B95" + }, + "outputs": [ + { + "data": { + "text/plain": [ + "Query is running: 0%| |" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + "Downloading: 0%| |" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/vnd.google.colaboratory.intrinsic+json": { + "summary": "{\n \"name\": \"get_ipython()\",\n \"rows\": 5,\n \"fields\": [\n {\n \"column\": \"title\",\n \"properties\": {\n \"dtype\": \"string\",\n \"num_unique_values\": 5,\n \"samples\": [\n \"The Great Train Robbery\",\n \"He Did and He Didn't\",\n \"Mabel's Busy Day\"\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"year\",\n \"properties\": {\n \"dtype\": \"Int64\",\n \"num_unique_values\": 5,\n \"samples\": [\n 1903,\n 1916,\n 1914\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"uri\",\n \"properties\": {\n \"dtype\": \"string\",\n \"num_unique_values\": 5,\n \"samples\": [\n \"gs://cloud-samples-data/vertex-ai/dataset-management/datasets/classic-movie-posters/the_great_train_robbery.jpg\",\n \"gs://cloud-samples-data/vertex-ai/dataset-management/datasets/classic-movie-posters/he_did_and_he_didnt.jpeg\",\n \"gs://cloud-samples-data/vertex-ai/dataset-management/datasets/classic-movie-posters/mabels_busy_day.jpeg\"\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"embedding\",\n \"properties\": {\n \"dtype\": \"object\",\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n }\n ]\n}", + "type": "dataframe" + }, + "text/html": [ + "\n", + "
    \n", + "
    \n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
    titleyearuriembedding
    0Barque sortant du port1895gs://cloud-samples-data/vertex-ai/dataset-mana...[-0.01909666322171688, 0.005955183878540993, -...
    1The Great Train Robbery1903gs://cloud-samples-data/vertex-ai/dataset-mana...[-0.021316001191735268, -0.012172936461865902,...
    2Mabel's Busy Day1914gs://cloud-samples-data/vertex-ai/dataset-mana...[-0.024318691343069077, -0.000487475743284449,...
    3Les Vampires1915gs://cloud-samples-data/vertex-ai/dataset-mana...[-0.025294441729784012, 0.017817920073866844, ...
    4He Did and He Didn't1916gs://cloud-samples-data/vertex-ai/dataset-mana...[-0.022868528962135315, 0.009465230628848076, ...
    \n", + "
    \n", + "
    \n", + "\n", + "
    \n", + " \n", + "\n", + " \n", + "\n", + " \n", + "
    \n", + "\n", + "\n", + "
    \n", + " \n", + "\n", + "\n", + "\n", + " \n", + "
    \n", + "\n", + "
    \n", + "
    \n" + ], + "text/plain": [ + " title year \\\n", + "0 Barque sortant du port 1895 \n", + "1 The Great Train Robbery 1903 \n", + "2 Mabel's Busy Day 1914 \n", + "3 Les Vampires 1915 \n", + "4 He Did and He Didn't 1916 \n", + "\n", + " uri \\\n", + "0 gs://cloud-samples-data/vertex-ai/dataset-mana... \n", + "1 gs://cloud-samples-data/vertex-ai/dataset-mana... \n", + "2 gs://cloud-samples-data/vertex-ai/dataset-mana... \n", + "3 gs://cloud-samples-data/vertex-ai/dataset-mana... \n", + "4 gs://cloud-samples-data/vertex-ai/dataset-mana... \n", + "\n", + " embedding \n", + "0 [-0.01909666322171688, 0.005955183878540993, -... \n", + "1 [-0.021316001191735268, -0.012172936461865902,... \n", + "2 [-0.024318691343069077, -0.000487475743284449,... \n", + "3 [-0.025294441729784012, 0.017817920073866844, ... \n", + "4 [-0.022868528962135315, 0.009465230628848076, ... " + ] + }, + "execution_count": 12, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "%%bigquery\n", + "SELECT * FROM `gemini_demo.movie_posters_results_embeddings` LIMIT 5" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "7UUZPBpuGHPO" + }, + "source": [ + "## Generate text embeddings for subset of IMDB dataset" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "4Jh7AJ2pIZuB" + }, + "outputs": [], + "source": [ + "%%bigquery\n", + "# Create table containing movie_id, title, and year for movies in IMDB reviews dataset that were release prior to 1935\n", + "CREATE OR REPLACE VIEW\n", + " `gemini_demo.imdb_movies` AS (\n", + " WITH\n", + " reviews AS (\n", + " SELECT\n", + " reviews.movie_id AS movie_id,\n", + " title.primary_title AS title,\n", + " title.start_year AS year,\n", + " reviews.review AS review\n", + " FROM\n", + " `bigquery-public-data.imdb.reviews` reviews\n", + " LEFT JOIN\n", + " `bigquery-public-data.imdb.title_basics` title\n", + " ON\n", + " reviews.movie_id = title.tconst)\n", + " SELECT\n", + " DISTINCT(movie_id),\n", + " title,\n", + " year\n", + " FROM\n", + " reviews\n", + " WHERE\n", + " year < 1935)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "9yzdx5W5czgd" + }, + "outputs": [], + "source": [ + "%%bigquery\n", + "CREATE OR REPLACE TABLE\n", + " `gemini_demo.imdb_movies_embeddings` AS (\n", + " SELECT\n", + " title,\n", + " year,\n", + " movie_id,\n", + " AI.EMBED(\n", + " content => 'The movie titled ' || title || ' from the year ' || year,\n", + " endpoint => 'gemini-embedding-001'\n", + " ).result AS embedding\n", + " FROM\n", + " `gemini_demo.imdb_movies`\n", + ");" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": { + "id": "a9t-8ZfCdUIk" + }, + "outputs": [ + { + "data": { + "text/plain": [ + "Query is running: 0%| |" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + "Downloading: 0%| |" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/vnd.google.colaboratory.intrinsic+json": { + "summary": "{\n \"name\": \"get_ipython()\",\n \"rows\": 5,\n \"fields\": [\n {\n \"column\": \"title\",\n \"properties\": {\n \"dtype\": \"string\",\n \"num_unique_values\": 5,\n \"samples\": [\n \"Leaving the Factory\",\n \"Rough Sea at Dover\",\n \"Boat Leaving the Port\"\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"year\",\n \"properties\": {\n \"dtype\": \"Int64\",\n \"num_unique_values\": 2,\n \"samples\": [\n 1895,\n 1894\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"movie_id\",\n \"properties\": {\n \"dtype\": \"string\",\n \"num_unique_values\": 5,\n \"samples\": [\n \"tt0000010\",\n \"tt0000030\"\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"embedding\",\n \"properties\": {\n \"dtype\": \"object\",\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n }\n ]\n}", + "type": "dataframe" + }, + "text/html": [ + "\n", + "
    \n", + "
    \n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
    titleyearmovie_idembedding
    0Dickson Experimental Sound Film1894tt0177707[-0.011816445738077164, -0.041168369352817535,...
    1Leaving the Factory1895tt0000010[-0.011916860938072205, 0.015054121613502502, ...
    2Boat Leaving the Port1895tt0000016[-0.029500054195523262, 0.005711852107197046, ...
    3The Execution of Mary, Queen of Scots1895tt0132134[-0.024642348289489746, 0.024970047175884247, ...
    4Rough Sea at Dover1895tt0000030[-0.007878931239247322, 0.027580272406339645, ...
    \n", + "
    \n", + "
    \n", + "\n", + "
    \n", + " \n", + "\n", + " \n", + "\n", + " \n", + "
    \n", + "\n", + "\n", + "
    \n", + " \n", + "\n", + "\n", + "\n", + " \n", + "
    \n", + "\n", + "
    \n", + "
    \n" + ], + "text/plain": [ + " title year movie_id \\\n", + "0 Dickson Experimental Sound Film 1894 tt0177707 \n", + "1 Leaving the Factory 1895 tt0000010 \n", + "2 Boat Leaving the Port 1895 tt0000016 \n", + "3 The Execution of Mary, Queen of Scots 1895 tt0132134 \n", + "4 Rough Sea at Dover 1895 tt0000030 \n", + "\n", + " embedding \n", + "0 [-0.011816445738077164, -0.041168369352817535,... \n", + "1 [-0.011916860938072205, 0.015054121613502502, ... \n", + "2 [-0.029500054195523262, 0.005711852107197046, ... \n", + "3 [-0.024642348289489746, 0.024970047175884247, ... \n", + "4 [-0.007878931239247322, 0.027580272406339645, ... " + ] + }, + "execution_count": 15, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "%%bigquery\n", + "SELECT * FROM `gemini_demo.imdb_movies_embeddings` LIMIT 5" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "1SZ1OmVc2IN5" + }, + "source": [ + "## Match movie poster images to IMDB `movie_id` using BigQuery `VECTOR_SEARCH`" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": { + "id": "N4pBoQPP2Y3k" + }, + "outputs": [ + { + "data": { + "text/plain": [ + "Query is running: 0%| |" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + "Downloading: 0%| |" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/vnd.google.colaboratory.intrinsic+json": { + "summary": "{\n \"name\": \"get_ipython()\",\n \"rows\": 19,\n \"fields\": [\n {\n \"column\": \"poster_uri\",\n \"properties\": {\n \"dtype\": \"string\",\n \"num_unique_values\": 19,\n \"samples\": [\n \"gs://cloud-samples-data/vertex-ai/dataset-management/datasets/classic-movie-posters/putting_paints_on_philip.jpeg\",\n \"gs://cloud-samples-data/vertex-ai/dataset-management/datasets/classic-movie-posters/sally_of_the_sawdust.jpg\",\n \"gs://cloud-samples-data/vertex-ai/dataset-management/datasets/classic-movie-posters/battling_butler.jpg\"\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"poster_title\",\n \"properties\": {\n \"dtype\": \"string\",\n \"num_unique_values\": 19,\n \"samples\": [\n \"Putting Pants on Philip\",\n \"Sally of the Sawdust\",\n \"Battling Butler\"\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"poster_year\",\n \"properties\": {\n \"dtype\": \"Int64\",\n \"num_unique_values\": 13,\n \"samples\": [\n 1915,\n 1914,\n 1927\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"imdb_title\",\n \"properties\": {\n \"dtype\": \"string\",\n \"num_unique_values\": 19,\n \"samples\": [\n \"Putting Pants on Philip\",\n \"Sally of the Sawdust\",\n \"Battling Butler\"\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"imdb_year\",\n \"properties\": {\n \"dtype\": \"Int64\",\n \"num_unique_values\": 13,\n \"samples\": [\n 1915,\n 1913,\n 1927\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"imdb_movie_id\",\n \"properties\": {\n \"dtype\": \"string\",\n \"num_unique_values\": 19,\n \"samples\": [\n \"tt0018294\",\n \"tt0016308\",\n \"tt0016630\"\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"distance\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": 0.06310667859438789,\n \"min\": 0.0,\n \"max\": 0.18508910419754754,\n \"num_unique_values\": 5,\n \"samples\": [\n 0.15144097037365933,\n 0.09469846438580964,\n 0.18508910419754754\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n }\n ]\n}", + "type": "dataframe" + }, + "text/html": [ + "\n", + "
    \n", + "
    \n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
    poster_uriposter_titleposter_yearimdb_titleimdb_yearimdb_movie_iddistance
    0gs://cloud-samples-data/vertex-ai/dataset-mana...Putting Pants on Philip1927Putting Pants on Philip1927tt00182940.000000
    1gs://cloud-samples-data/vertex-ai/dataset-mana...Little Lord Fauntleroy1921Little Lord Fauntleroy1921tt00123970.000000
    2gs://cloud-samples-data/vertex-ai/dataset-mana...Shoulder Arms1918Shoulder Arms1918tt00096110.000000
    3gs://cloud-samples-data/vertex-ai/dataset-mana...Brown of Harvard1926Brown of Harvard1926tt00166900.000000
    4gs://cloud-samples-data/vertex-ai/dataset-mana...The Merry Widow1934The Merry Widow1925tt00161040.151441
    5gs://cloud-samples-data/vertex-ai/dataset-mana...Sally of the Sawdust1925Sally of the Sawdust1925tt00163080.000000
    6gs://cloud-samples-data/vertex-ai/dataset-mana...He Did and He Didn't1916He Did and He Didn't1916tt00067640.000000
    7gs://cloud-samples-data/vertex-ai/dataset-mana...AU SECOURS!1924Help!1924tt00138450.185089
    8gs://cloud-samples-data/vertex-ai/dataset-mana...The Great Train Robbery1903The Great Train Robbery1903tt00004390.000000
    9gs://cloud-samples-data/vertex-ai/dataset-mana...Mabel's Busy Day1914Mabel's Busy Day1914tt00042800.000000
    10gs://cloud-samples-data/vertex-ai/dataset-mana...The Beloved Rogue1927The Beloved Rogue1927tt00176670.000000
    11gs://cloud-samples-data/vertex-ai/dataset-mana...Battling Butler1926Battling Butler1926tt00166300.000000
    12gs://cloud-samples-data/vertex-ai/dataset-mana...West of Hot Dog1924West of Hot Dog1924tt00154770.000000
    13gs://cloud-samples-data/vertex-ai/dataset-mana...Little Annie Rooney1925Little Annie Rooney1925tt00160280.000000
    14gs://cloud-samples-data/vertex-ai/dataset-mana...Der Student von Prag1926The Student of Prague1913tt00034190.153324
    15gs://cloud-samples-data/vertex-ai/dataset-mana...Barque sortant du port1895Boat Leaving the Port1895tt00000160.094698
    16gs://cloud-samples-data/vertex-ai/dataset-mana...The Lost World1925The Lost World1925tt00160390.000000
    17gs://cloud-samples-data/vertex-ai/dataset-mana...Les Vampires1915Les Vampires1915tt00062060.000000
    18gs://cloud-samples-data/vertex-ai/dataset-mana...Felix in Hollywood1923Felix in Hollywood1923tt00178710.000000
    \n", + "
    \n", + "
    \n", + "\n", + "
    \n", + " \n", + "\n", + " \n", + "\n", + " \n", + "
    \n", + "\n", + "\n", + "
    \n", + " \n", + "\n", + "\n", + "\n", + " \n", + "
    \n", + "\n", + "
    \n", + "
    \n" + ], + "text/plain": [ + " poster_uri \\\n", + "0 gs://cloud-samples-data/vertex-ai/dataset-mana... \n", + "1 gs://cloud-samples-data/vertex-ai/dataset-mana... \n", + "2 gs://cloud-samples-data/vertex-ai/dataset-mana... \n", + "3 gs://cloud-samples-data/vertex-ai/dataset-mana... \n", + "4 gs://cloud-samples-data/vertex-ai/dataset-mana... \n", + "5 gs://cloud-samples-data/vertex-ai/dataset-mana... \n", + "6 gs://cloud-samples-data/vertex-ai/dataset-mana... \n", + "7 gs://cloud-samples-data/vertex-ai/dataset-mana... \n", + "8 gs://cloud-samples-data/vertex-ai/dataset-mana... \n", + "9 gs://cloud-samples-data/vertex-ai/dataset-mana... \n", + "10 gs://cloud-samples-data/vertex-ai/dataset-mana... \n", + "11 gs://cloud-samples-data/vertex-ai/dataset-mana... \n", + "12 gs://cloud-samples-data/vertex-ai/dataset-mana... \n", + "13 gs://cloud-samples-data/vertex-ai/dataset-mana... \n", + "14 gs://cloud-samples-data/vertex-ai/dataset-mana... \n", + "15 gs://cloud-samples-data/vertex-ai/dataset-mana... \n", + "16 gs://cloud-samples-data/vertex-ai/dataset-mana... \n", + "17 gs://cloud-samples-data/vertex-ai/dataset-mana... \n", + "18 gs://cloud-samples-data/vertex-ai/dataset-mana... \n", + "\n", + " poster_title poster_year imdb_title imdb_year \\\n", + "0 Putting Pants on Philip 1927 Putting Pants on Philip 1927 \n", + "1 Little Lord Fauntleroy 1921 Little Lord Fauntleroy 1921 \n", + "2 Shoulder Arms 1918 Shoulder Arms 1918 \n", + "3 Brown of Harvard 1926 Brown of Harvard 1926 \n", + "4 The Merry Widow 1934 The Merry Widow 1925 \n", + "5 Sally of the Sawdust 1925 Sally of the Sawdust 1925 \n", + "6 He Did and He Didn't 1916 He Did and He Didn't 1916 \n", + "7 AU SECOURS! 1924 Help! 1924 \n", + "8 The Great Train Robbery 1903 The Great Train Robbery 1903 \n", + "9 Mabel's Busy Day 1914 Mabel's Busy Day 1914 \n", + "10 The Beloved Rogue 1927 The Beloved Rogue 1927 \n", + "11 Battling Butler 1926 Battling Butler 1926 \n", + "12 West of Hot Dog 1924 West of Hot Dog 1924 \n", + "13 Little Annie Rooney 1925 Little Annie Rooney 1925 \n", + "14 Der Student von Prag 1926 The Student of Prague 1913 \n", + "15 Barque sortant du port 1895 Boat Leaving the Port 1895 \n", + "16 The Lost World 1925 The Lost World 1925 \n", + "17 Les Vampires 1915 Les Vampires 1915 \n", + "18 Felix in Hollywood 1923 Felix in Hollywood 1923 \n", + "\n", + " imdb_movie_id distance \n", + "0 tt0018294 0.000000 \n", + "1 tt0012397 0.000000 \n", + "2 tt0009611 0.000000 \n", + "3 tt0016690 0.000000 \n", + "4 tt0016104 0.151441 \n", + "5 tt0016308 0.000000 \n", + "6 tt0006764 0.000000 \n", + "7 tt0013845 0.185089 \n", + "8 tt0000439 0.000000 \n", + "9 tt0004280 0.000000 \n", + "10 tt0017667 0.000000 \n", + "11 tt0016630 0.000000 \n", + "12 tt0015477 0.000000 \n", + "13 tt0016028 0.000000 \n", + "14 tt0003419 0.153324 \n", + "15 tt0000016 0.094698 \n", + "16 tt0016039 0.000000 \n", + "17 tt0006206 0.000000 \n", + "18 tt0017871 0.000000 " + ] + }, + "execution_count": 16, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "%%bigquery\n", + "SELECT\n", + " query.uri AS poster_uri,\n", + " query.title AS poster_title,\n", + " query.year AS poster_year,\n", + " base.title AS imdb_title,\n", + " base.year AS imdb_year,\n", + " base.movie_id AS imdb_movie_id,\n", + " distance\n", + "FROM\n", + " VECTOR_SEARCH( TABLE `gemini_demo.imdb_movies_embeddings`,\n", + " 'embedding',\n", + " TABLE `gemini_demo.movie_posters_results_embeddings`,\n", + " 'embedding',\n", + " top_k => 1,\n", + " distance_type => 'COSINE');" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "RCoxBtpSLxNi" + }, + "source": [ + "Now let's join some additional information on ratings..." + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": { + "id": "pbz_nMtdY7S6" + }, + "outputs": [ + { + "data": { + "text/plain": [ + "Query is running: 0%| |" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + "Downloading: 0%| |" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/vnd.google.colaboratory.intrinsic+json": { + "summary": "{\n \"name\": \"get_ipython()\",\n \"rows\": 19,\n \"fields\": [\n {\n \"column\": \"poster_uri\",\n \"properties\": {\n \"dtype\": \"string\",\n \"num_unique_values\": 19,\n \"samples\": [\n \"gs://cloud-samples-data/vertex-ai/dataset-management/datasets/classic-movie-posters/shoulder_arms.jpeg\",\n \"gs://cloud-samples-data/vertex-ai/dataset-management/datasets/classic-movie-posters/the_beloved_rogue.jpeg\",\n \"gs://cloud-samples-data/vertex-ai/dataset-management/datasets/classic-movie-posters/felix_in_hollywood.jpeg\"\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"poster_title\",\n \"properties\": {\n \"dtype\": \"string\",\n \"num_unique_values\": 19,\n \"samples\": [\n \"Shoulder Arms\",\n \"The Beloved Rogue\",\n \"Felix in Hollywood\"\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"poster_year\",\n \"properties\": {\n \"dtype\": \"Int64\",\n \"num_unique_values\": 13,\n \"samples\": [\n 1895,\n 1923,\n 1918\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"imdb_title\",\n \"properties\": {\n \"dtype\": \"string\",\n \"num_unique_values\": 19,\n \"samples\": [\n \"Shoulder Arms\",\n \"The Beloved Rogue\",\n \"Felix in Hollywood\"\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"imdb_year\",\n \"properties\": {\n \"dtype\": \"Int64\",\n \"num_unique_values\": 13,\n \"samples\": [\n 1895,\n 1913,\n 1918\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"imdb_movie_id\",\n \"properties\": {\n \"dtype\": \"string\",\n \"num_unique_values\": 19,\n \"samples\": [\n \"tt0009611\",\n \"tt0017667\",\n \"tt0017871\"\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"distance\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": 0.06310667859438797,\n \"min\": 0.0,\n \"max\": 0.18508910419754754,\n \"num_unique_values\": 5,\n \"samples\": [\n 0.15144097037365933,\n 0.09469846438580964,\n 0.18508910419754754\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"average_rating\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": 0.5610141193457283,\n \"min\": 5.4,\n \"max\": 7.3,\n \"num_unique_values\": 12,\n \"samples\": [\n 5.5,\n 5.9,\n 7.3\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"num_votes\",\n \"properties\": {\n \"dtype\": \"Int64\",\n \"num_unique_values\": 19,\n \"samples\": [\n 7965,\n 980,\n 1093\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n }\n ]\n}", + "type": "dataframe" + }, + "text/html": [ + "\n", + "
    \n", + "
    \n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
    poster_uriposter_titleposter_yearimdb_titleimdb_yearimdb_movie_iddistanceaverage_ratingnum_votes
    0gs://cloud-samples-data/vertex-ai/dataset-mana...Shoulder Arms1918Shoulder Arms1918tt00096110.0000007.37965
    1gs://cloud-samples-data/vertex-ai/dataset-mana...Les Vampires1915Les Vampires1915tt00062060.0000007.35857
    2gs://cloud-samples-data/vertex-ai/dataset-mana...The Great Train Robbery1903The Great Train Robbery1903tt00004390.0000007.222637
    3gs://cloud-samples-data/vertex-ai/dataset-mana...The Merry Widow1934The Merry Widow1925tt00161040.1514417.22729
    4gs://cloud-samples-data/vertex-ai/dataset-mana...The Lost World1925The Lost World1925tt00160390.0000007.06527
    5gs://cloud-samples-data/vertex-ai/dataset-mana...The Beloved Rogue1927The Beloved Rogue1927tt00176670.0000007.0980
    6gs://cloud-samples-data/vertex-ai/dataset-mana...Battling Butler1926Battling Butler1926tt00166300.0000007.04373
    7gs://cloud-samples-data/vertex-ai/dataset-mana...Little Annie Rooney1925Little Annie Rooney1925tt00160280.0000006.81121
    8gs://cloud-samples-data/vertex-ai/dataset-mana...AU SECOURS!1924Help!1924tt00138450.1850896.8552
    9gs://cloud-samples-data/vertex-ai/dataset-mana...Little Lord Fauntleroy1921Little Lord Fauntleroy1921tt00123970.0000006.7396
    10gs://cloud-samples-data/vertex-ai/dataset-mana...Sally of the Sawdust1925Sally of the Sawdust1925tt00163080.0000006.6633
    11gs://cloud-samples-data/vertex-ai/dataset-mana...Felix in Hollywood1923Felix in Hollywood1923tt00178710.0000006.51093
    12gs://cloud-samples-data/vertex-ai/dataset-mana...Putting Pants on Philip1927Putting Pants on Philip1927tt00182940.0000006.51176
    13gs://cloud-samples-data/vertex-ai/dataset-mana...Der Student von Prag1926The Student of Prague1913tt00034190.1533246.42679
    14gs://cloud-samples-data/vertex-ai/dataset-mana...He Did and He Didn't1916He Did and He Didn't1916tt00067640.0000006.4360
    15gs://cloud-samples-data/vertex-ai/dataset-mana...Brown of Harvard1926Brown of Harvard1926tt00166900.0000006.21573
    16gs://cloud-samples-data/vertex-ai/dataset-mana...Barque sortant du port1895Boat Leaving the Port1895tt00000160.0946985.91707
    17gs://cloud-samples-data/vertex-ai/dataset-mana...West of Hot Dog1924West of Hot Dog1924tt00154770.0000005.5335
    18gs://cloud-samples-data/vertex-ai/dataset-mana...Mabel's Busy Day1914Mabel's Busy Day1914tt00042800.0000005.41141
    \n", + "
    \n", + "
    \n", + "\n", + "
    \n", + " \n", + "\n", + " \n", + "\n", + " \n", + "
    \n", + "\n", + "\n", + "
    \n", + " \n", + "\n", + "\n", + "\n", + " \n", + "
    \n", + "\n", + "
    \n", + "
    \n" + ], + "text/plain": [ + " poster_uri \\\n", + "0 gs://cloud-samples-data/vertex-ai/dataset-mana... \n", + "1 gs://cloud-samples-data/vertex-ai/dataset-mana... \n", + "2 gs://cloud-samples-data/vertex-ai/dataset-mana... \n", + "3 gs://cloud-samples-data/vertex-ai/dataset-mana... \n", + "4 gs://cloud-samples-data/vertex-ai/dataset-mana... \n", + "5 gs://cloud-samples-data/vertex-ai/dataset-mana... \n", + "6 gs://cloud-samples-data/vertex-ai/dataset-mana... \n", + "7 gs://cloud-samples-data/vertex-ai/dataset-mana... \n", + "8 gs://cloud-samples-data/vertex-ai/dataset-mana... \n", + "9 gs://cloud-samples-data/vertex-ai/dataset-mana... \n", + "10 gs://cloud-samples-data/vertex-ai/dataset-mana... \n", + "11 gs://cloud-samples-data/vertex-ai/dataset-mana... \n", + "12 gs://cloud-samples-data/vertex-ai/dataset-mana... \n", + "13 gs://cloud-samples-data/vertex-ai/dataset-mana... \n", + "14 gs://cloud-samples-data/vertex-ai/dataset-mana... \n", + "15 gs://cloud-samples-data/vertex-ai/dataset-mana... \n", + "16 gs://cloud-samples-data/vertex-ai/dataset-mana... \n", + "17 gs://cloud-samples-data/vertex-ai/dataset-mana... \n", + "18 gs://cloud-samples-data/vertex-ai/dataset-mana... \n", + "\n", + " poster_title poster_year imdb_title imdb_year \\\n", + "0 Shoulder Arms 1918 Shoulder Arms 1918 \n", + "1 Les Vampires 1915 Les Vampires 1915 \n", + "2 The Great Train Robbery 1903 The Great Train Robbery 1903 \n", + "3 The Merry Widow 1934 The Merry Widow 1925 \n", + "4 The Lost World 1925 The Lost World 1925 \n", + "5 The Beloved Rogue 1927 The Beloved Rogue 1927 \n", + "6 Battling Butler 1926 Battling Butler 1926 \n", + "7 Little Annie Rooney 1925 Little Annie Rooney 1925 \n", + "8 AU SECOURS! 1924 Help! 1924 \n", + "9 Little Lord Fauntleroy 1921 Little Lord Fauntleroy 1921 \n", + "10 Sally of the Sawdust 1925 Sally of the Sawdust 1925 \n", + "11 Felix in Hollywood 1923 Felix in Hollywood 1923 \n", + "12 Putting Pants on Philip 1927 Putting Pants on Philip 1927 \n", + "13 Der Student von Prag 1926 The Student of Prague 1913 \n", + "14 He Did and He Didn't 1916 He Did and He Didn't 1916 \n", + "15 Brown of Harvard 1926 Brown of Harvard 1926 \n", + "16 Barque sortant du port 1895 Boat Leaving the Port 1895 \n", + "17 West of Hot Dog 1924 West of Hot Dog 1924 \n", + "18 Mabel's Busy Day 1914 Mabel's Busy Day 1914 \n", + "\n", + " imdb_movie_id distance average_rating num_votes \n", + "0 tt0009611 0.000000 7.3 7965 \n", + "1 tt0006206 0.000000 7.3 5857 \n", + "2 tt0000439 0.000000 7.2 22637 \n", + "3 tt0016104 0.151441 7.2 2729 \n", + "4 tt0016039 0.000000 7.0 6527 \n", + "5 tt0017667 0.000000 7.0 980 \n", + "6 tt0016630 0.000000 7.0 4373 \n", + "7 tt0016028 0.000000 6.8 1121 \n", + "8 tt0013845 0.185089 6.8 552 \n", + "9 tt0012397 0.000000 6.7 396 \n", + "10 tt0016308 0.000000 6.6 633 \n", + "11 tt0017871 0.000000 6.5 1093 \n", + "12 tt0018294 0.000000 6.5 1176 \n", + "13 tt0003419 0.153324 6.4 2679 \n", + "14 tt0006764 0.000000 6.4 360 \n", + "15 tt0016690 0.000000 6.2 1573 \n", + "16 tt0000016 0.094698 5.9 1707 \n", + "17 tt0015477 0.000000 5.5 335 \n", + "18 tt0004280 0.000000 5.4 1141 " + ] + }, + "execution_count": 17, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "%%bigquery\n", + "SELECT\n", + " query.uri AS poster_uri,\n", + " query.title AS poster_title,\n", + " query.year AS poster_year,\n", + " base.title AS imdb_title,\n", + " base.year AS imdb_year,\n", + " base.movie_id AS imdb_movie_id,\n", + " distance,\n", + " imdb.average_rating,\n", + " imdb.num_votes\n", + "FROM\n", + " VECTOR_SEARCH( TABLE `gemini_demo.imdb_movies_embeddings`,\n", + " 'embedding',\n", + " TABLE `gemini_demo.movie_posters_results_embeddings`,\n", + " 'embedding',\n", + " top_k => 1,\n", + " distance_type => 'COSINE') DATA\n", + "LEFT JOIN\n", + " `bigquery-public-data.imdb.title_ratings` imdb\n", + "ON\n", + " base.movie_id = imdb.tconst\n", + "ORDER BY\n", + " imdb.average_rating DESC" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "UB8VKTHJJlKx" + }, + "source": [ + "# Cleaning up\n", + "\n", + "To clean up all Google Cloud resources used in this project, you can [delete the Google Cloud project](https://cloud.google.com/resource-manager/docs/creating-managing-projects#shutting_down_projects) you used for the tutorial.\n", + "\n", + "Otherwise, you can delete the individual resources you created in this tutorial by uncommenting the below:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "nSF-ZhhPMjfs" + }, + "outputs": [], + "source": [ + "#\n", + "# !bq rm -r -f $PROJECT_ID:gemini_demo\n", + "#" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "gFWBsvpbhGtE" + }, + "source": [ + "# Wrap up\n", + "\n", + "In this you have seen an example of how to integrate BigQuery with Vertex AI LLMs, and given examples of how the `AI.GENERATE` function can be applied directly to multimodal data stored in BigQuery, as well as how to generate embeddings with `AI.EMBED`.\n", + "\n", + "Check out our BigQuery documentation on [generating text](https://docs.cloud.google.com/bigquery/docs/reference/standard-sql/bigqueryml-syntax-ai-generate) and [generating embeddings](https://docs.cloud.google.com/bigquery/docs/reference/standard-sql/bigqueryml-syntax-ai-embed) to learn more about generative AI in BigQuery." + ] + } + ], + "metadata": { + "colab": { + "name": "poster_image_analysis.ipynb", + "toc_visible": true + }, + "kernelspec": { + "display_name": "Python 3", + "name": "python3" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} diff --git a/gemini/use-cases/applying-llms-to-data/bigquery_ai_operators.ipynb b/gemini/use-cases/applying-llms-to-data/bigquery_ai_operators.ipynb new file mode 100644 index 0000000..13ce8af --- /dev/null +++ b/gemini/use-cases/applying-llms-to-data/bigquery_ai_operators.ipynb @@ -0,0 +1,1032 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "cMQCs0oQf5Jo" + }, + "outputs": [], + "source": [ + "# Copyright 2026 Google LLC\n", + "#\n", + "# Licensed under the Apache License, Version 2.0 (the \"License\");\n", + "# you may not use this file except in compliance with the License.\n", + "# You may obtain a copy of the License at\n", + "#\n", + "# https://www.apache.org/licenses/LICENSE-2.0\n", + "#\n", + "# Unless required by applicable law or agreed to in writing, software\n", + "# distributed under the License is distributed on an \"AS IS\" BASIS,\n", + "# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n", + "# See the License for the specific language governing permissions and\n", + "# limitations under the License." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "iR0jdheRGG89" + }, + "source": [ + "# Semantic Analysis in BigQuery with AI Functions" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "Lt2rldOmcJg9" + }, + "source": [ + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
    \n", + " \n", + " \"Google
    Open in Colab\n", + "
    \n", + "
    \n", + " \n", + " \"Google
    Open in Colab Enterprise\n", + "
    \n", + "
    \n", + " \n", + " \"Workbench
    Open in Workbench\n", + "
    \n", + "
    \n", + " \n", + " \"BigQuery
    Open in BigQuery Studio\n", + "
    \n", + "
    \n", + " \n", + " \"GitHub
    View on GitHub\n", + "
    \n", + "
    \n", + "\n", + "
    \n", + "\n", + "

    \n", + "Share to:\n", + "\n", + "\n", + " \"LinkedIn\n", + "\n", + "\n", + "\n", + " \"Bluesky\n", + "\n", + "\n", + "\n", + " \"X\n", + "\n", + "\n", + "\n", + " \"Reddit\n", + "\n", + "\n", + "\n", + " \"Facebook\n", + "\n", + "

    " + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "5h3O5b6P8WEx" + }, + "source": [ + "| Author |\n", + "| --- |\n", + "| [Alicia Williams](https://github.com/aliciawilliams) |" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "intro_md_new" + }, + "source": [ + "## Overview\n", + "\n", + "This tutorial will guide you through the powerful AI functions available in BigQuery. You'll get hands-on experience using two collections of functions that integrate directly with powerful Gemini models, allowing you to perform sophisticated AI-driven analysis on your data right within your familiar SQL environment.\n", + "\n", + "1. **managed AI functions ([`AI.SCORE`](https://cloud.google.com/bigquery/docs/reference/standard-sql/bigqueryml-syntax-ai-score), [`AI.CLASSIFY`](https://cloud.google.com/bigquery/docs/reference/standard-sql/bigqueryml-syntax-ai-classify), [`AI.IF`](https://cloud.google.com/bigquery/docs/reference/standard-sql/bigqueryml-syntax-ai-if), [`AI.AGG`](https://cloud.google.com/bigquery/docs/reference/standard-sql/bigqueryml-syntax-ai-agg))**: These are high-level, easy-to-use functions for common tasks like semantic ranking, classification, filtering, joining, and aggregation. BigQuery uses prompt engineering and can select the appropriate model and parameters to use for the specific task to optimize the quality and consistency of your results, making them ideal for data analysts who are not necessarily prompt engineers or ML practitioners. Furthermore, BigQuery supports **optimized mode** for `AI.IF` and `AI.CLASSIFY`, utilizing proxy models and model distillation to process large datasets at significantly reduced cost and latency.\n", + "\n", + "2. **general-purpose AI functions ([`AI.GENERATE_BOOL`](https://cloud.google.com/bigquery/docs/reference/standard-sql/bigqueryml-syntax-ai-generate-bool), [`AI.GENERATE_DOUBLE`](https://cloud.google.com/bigquery/docs/reference/standard-sql/bigqueryml-syntax-ai-generate-double), [`AI.GENERATE_INT`](https://cloud.google.com/bigquery/docs/reference/standard-sql/bigqueryml-syntax-ai-generate-int))**: These are inference functions for power-users who want full control over the prompt. They are perfect for row-level AI tasks, especially enriching data in a `SELECT` clause, and returning a specific data type (`BOOL`, `DOUBLE`, or `INT`)." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "SwQLISjqGTHd" + }, + "source": [ + "### Objectives\n", + "\n", + "We'll cover how to:\n", + "\n", + "* **Perform semantic ranking** with the managed `AI.SCORE` function.\n", + "* **Perform classification** with the managed `AI.CLASSIFY` function.\n", + "* **Perform semantic filtering** with the managed `AI.IF` function.\n", + "* **Perform semantic joins** with the managed `AI.IF` function.\n", + "* **Perform semantic aggregation** over unstructured text and images with the managed `AI.AGG` function.\n", + "* **Optimize AI query costs and performance** for large-scale datasets using optimized mode in `AI.IF` and `AI.CLASSIFY`.\n", + "* **Perform powerful, row-level analysis** using general-purpose inference functions like `AI.GENERATE_BOOL`, `AI.GENERATE_DOUBLE`, and `AI.GENERATE_INT`." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "costs_md" + }, + "source": [ + "### Services and Costs\n", + "\n", + "This tutorial uses the following billable components of Google Cloud:\n", + "\n", + "* **BigQuery**: [Pricing](https://cloud.google.com/bigquery/pricing)\n", + "\n", + "* **BigQuery ML**: [Pricing](https://cloud.google.com/bigquery/pricing#bqml)\n", + "\n", + "* **Gemini Enterprise Agent Platform**: [Pricing](https://cloud.google.com/gemini-enterprise-agent-platform/generative-ai/pricing)\n", + "\n", + "You can use the [Pricing Calculator](https://cloud.google.com/products/calculator) to generate a cost estimate based on your projected usage." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "nkoCFoFVSPii" + }, + "source": [ + "---\n", + "\n", + "## Before you begin" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "setup_md_1" + }, + "source": [ + "### Setting up your Google Cloud project\n", + "**The following steps are required, regardless of your notebook environment.**\n", + "\n", + "1. [Select or create a Google Cloud project](https://console.cloud.google.com/cloud-resource-manager). When you first create an account, you get a $300 free credit towards your compute/storage costs.\n", + "\n", + "2. [Make sure that billing is enabled for your project](https://cloud.google.com/billing/docs/how-to/modify-project).\n", + "\n", + "3. [Enable the BigQuery, BigQuery Connection, and Agent Platform APIs](https://console.cloud.google.com/flows/enableapi?apiid=bigquery.googleapis.com,bigqueryconnection.googleapis.com,aiplatform.googleapis.com).\n", + "\n", + "4. If you are running this notebook locally, you need to install the [Cloud SDK](https://cloud.google.com/sdk)." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "YQ3g-h7uTaSf" + }, + "source": [ + "### Setting your project ID" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "set_project_id" + }, + "outputs": [], + "source": [ + "PROJECT_ID = \"\" # @param {type:\"string\"}\n", + "\n", + "# Set the project id\n", + "! gcloud config set project {PROJECT_ID}" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "auth_md" + }, + "source": [ + "### Authenticating to your Google Cloud account\n", + "\n", + "Depending on your Jupyter environment, you may have to manually authenticate. Follow the relevant instructions below." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "V6NjZRCXU5Ro" + }, + "source": [ + "**1. Colab Enterprise or BigQuery Studio Notebooks**\n", + "* Do nothing as you are already authenticated." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "l0dV1hvAU1ed" + }, + "source": [ + "**2. Colab, uncomment and run:**" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "auth_code" + }, + "outputs": [], + "source": [ + "# from google.colab import auth\n", + "#\n", + "# auth.authenticate_user()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "3JiX7KA5uVl3" + }, + "source": [ + "###Creating a helper function to view images\n", + "This is a helpful utility function that you'll use later in the tutorial. It takes the results of your search query (stored in a pandas DataFrame) and displays the corresponding product images in a nice grid format, making it easy to see the results of a query that contain images." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "DVncmYt5ufC9" + }, + "outputs": [], + "source": [ + "## Code created with Gemini\n", + "\n", + "import base64\n", + "import html\n", + "import mimetypes\n", + "from io import BytesIO\n", + "\n", + "import pandas as pd\n", + "from IPython.display import HTML, display\n", + "from PIL import Image\n", + "from google.cloud import storage\n", + "\n", + "def display_image_grid(\n", + " df: pd.DataFrame, url_column: str = \"uri\", image_width: int = 220\n", + "):\n", + " \"\"\"Renders a grid of cards, each with an image and its corresponding\n", + " DataFrame row details. Automatically resizes GCS images to prevent\n", + " exceeding notebook output size limits.\n", + "\n", + " Args:\n", + " df (pd.DataFrame): DataFrame containing image URLs and other metadata.\n", + " url_column (str): The name of the column that contains the image URLs.\n", + " image_width (int): The width of each card in pixels.\n", + " \"\"\"\n", + " # --- Validate Input ---\n", + " if not isinstance(df, pd.DataFrame) or df.empty:\n", + " print(\"Input is not a valid or non-empty DataFrame. Nothing to display.\")\n", + " return\n", + "\n", + " # Auto-detect URL column if the default/specified column is not found\n", + " if url_column not in df.columns:\n", + " for fallback_col in [\"uri\", \"signed_url\", \"url\", \"image_uri\", \"gcs_uri\"]:\n", + " if fallback_col in df.columns:\n", + " url_column = fallback_col\n", + " break\n", + "\n", + " if url_column not in df.columns:\n", + " print(f\"Error: Column '{url_column}' not found in the DataFrame.\")\n", + " return\n", + "\n", + " # Get a list of all columns that are NOT the url_column\n", + " detail_columns = [col for col in df.columns if col != url_column]\n", + "\n", + " # Initialize GCS client lazily if needed\n", + " gcs_client = None\n", + "\n", + " # --- Build HTML for each card ---\n", + " card_html_list = []\n", + " for index, row in df.iterrows():\n", + " # Strip any leading/trailing single or double quotes from the URL\n", + " url = str(row[url_column]).strip(\"'\\\"\")\n", + "\n", + " # Handle raw gs:// URIs via GCS client, PIL Thumbnailing & Base64\n", + " if url.startswith(\"gs://\"):\n", + " try:\n", + " if gcs_client is None:\n", + " gcs_client = storage.Client()\n", + " # Parse bucket and blob name from gs://bucket_name/blob_path\n", + " parts = url[5:].split(\"/\", 1)\n", + " bucket = gcs_client.bucket(parts[0])\n", + " blob = bucket.blob(parts[1])\n", + " image_bytes = blob.download_as_bytes()\n", + "\n", + " # --- NEW: Thumbnail and compress image using PIL ---\n", + " try:\n", + " img = Image.open(BytesIO(image_bytes))\n", + " if img.mode != \"RGB\":\n", + " img = img.convert(\"RGB\")\n", + " # Resize to 2x card width for sharp Retina display while keeping file size tiny\n", + " max_dim = image_width * 2\n", + " img.thumbnail((max_dim, max_dim), Image.Resampling.LANCZOS)\n", + "\n", + " buffer = BytesIO()\n", + " img.save(buffer, format=\"JPEG\", quality=75)\n", + " image_bytes = buffer.getvalue()\n", + " mime_type = \"image/jpeg\"\n", + " except Exception:\n", + " # Fall back to original bytes if PIL processing fails\n", + " mime_type, _ = mimetypes.guess_type(url)\n", + " if not mime_type:\n", + " mime_type = \"image/png\"\n", + "\n", + " b64_data = base64.b64encode(image_bytes).decode(\"utf-8\")\n", + " url = f\"data:{mime_type};base64,{b64_data}\"\n", + " except Exception as e:\n", + " print(f\"Failed to load GCS image {url}: {e}\")\n", + "\n", + " # Create an HTML block for the other details\n", + " details_html = \"\"\n", + " for col in detail_columns:\n", + " # Escape data to prevent HTML rendering issues\n", + " value = html.escape(str(row[col]))\n", + " col_name = html.escape(col.replace(\"_\", \" \").title())\n", + " details_html += f'

    {col_name}: {value}

    '\n", + "\n", + " # Assemble the full card\n", + " card_html_list.append(f\"\"\"\n", + "
    \n", + " \"Product\n", + "
    \n", + " {details_html}\n", + "
    \n", + "
    \n", + " \"\"\")\n", + "\n", + " # --- Display the final grid ---\n", + " all_cards_string = \"\".join(card_html_list)\n", + " final_html = f\"\"\"\n", + "

    Displaying {len(card_html_list)} result(s):

    \n", + "
    \n", + " {all_cards_string}\n", + "
    \n", + " \"\"\"\n", + " display(HTML(final_html))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "SMU-CoJcr8ls" + }, + "source": [ + "### Exploring the sample data\n", + "\n", + "This tutorial uses data from a fictional e-commerce pet supply company called **Cymbal Pets**, which is available as a public dataset in BigQuery: `bigquery-public-data.cymbal_pets`." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "BNVKfg6BN7yg" + }, + "source": [ + "Let's take a peek at a few rows of each table to get familiar with the data." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "9IXviWAgPe-j" + }, + "outputs": [], + "source": [ + "%%bigquery --project {PROJECT_ID}\n", + "\n", + "SELECT *\n", + "FROM bigquery-public-data.cymbal_pets.products\n", + "LIMIT 2" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "5usXj1u2Phuy" + }, + "outputs": [], + "source": [ + "%%bigquery --project {PROJECT_ID}\n", + "\n", + "SELECT *\n", + "FROM bigquery-public-data.cymbal_pets.product_images\n", + "LIMIT 2" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "aiqe_md_intro" + }, + "source": [ + "---\n", + "\n", + "## BigQuery managed AI functions\n", + "\n", + "The [**BigQuery managed AI functions**](https://docs.cloud.google.com/bigquery/docs/generative-ai-overview#managed_ai_functions) ([`AI.SCORE`](https://cloud.google.com/bigquery/docs/reference/standard-sql/bigqueryml-syntax-ai-score), [`AI.CLASSIFY`](https://cloud.google.com/bigquery/docs/reference/standard-sql/bigqueryml-syntax-ai-classify), [`AI.IF`](https://cloud.google.com/bigquery/docs/reference/standard-sql/bigqueryml-syntax-ai-if), and [`AI.AGG`](https://cloud.google.com/bigquery/docs/reference/standard-sql/bigqueryml-syntax-ai-agg)) extend traditional SQL with natural language conditions and aggregation.\n", + "\n", + "These functions are designed to be accessible to all users. They enhance quality by applying **prompt rewrite strategies** automatically, allowing you to write simple prompts while achieving accurate results.\n", + "\n", + "We'll run through a few examples of using these functions for analysis with the **`bigquery-public-data.cymbal_pets`** dataset." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "aiqe_score_md" + }, + "source": [ + "###Using `AI.SCORE`: Ranking by \"giftability\"\n", + "\n", + "[`AI.SCORE`](https://cloud.google.com/bigquery/docs/reference/standard-sql/bigqueryml-syntax-ai-score) is a function that accepts text input and uses a Gemini model to rate those inputs based on a scoring system that you describe as part of the prompt. If you do not provide a scoring system, the function automatically rewrites your prompt to generate a scoring rubric.\n", + "\n", + "It is perfect for ranking items based on semantic criteria that are not explicitly in the data. Let's apply it to a potential marketing use case: determining how suitable a product is as a gift for a pet owner." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "aiqe_score_code" + }, + "outputs": [], + "source": [ + "%%bigquery --project {PROJECT_ID}\n", + "\n", + "SELECT\n", + " product_name,\n", + " description,\n", + " AI.SCORE(\n", + " ('How \"giftable\" is this product for a pet owner? ', description,\n", + " 'Use a scale from 1-10.')\n", + " ) AS giftability_score\n", + "FROM\n", + " `bigquery-public-data.cymbal_pets.products`\n", + "ORDER BY\n", + " giftability_score DESC" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "aiqe_classify_md" + }, + "source": [ + "### Using `AI.CLASSIFY`: Classifying by intended animal\n", + "\n", + "[`AI.CLASSIFY`](https://cloud.google.com/bigquery/docs/reference/standard-sql/bigqueryml-syntax-ai-classify) uses a Gemini model to classify inputs into categories that you provide. `AI.CLASSIFY` accepts multimodal input (text, image, video, etc), and can be used for tasks such as classifying reviews by sentiment, classifying support tickets or emails by topic, or\n", + "classifying an image by its style or contents.\n", + "\n", + "Let's use it to assign each toy product to an animal type using the product name and description. We'll define a set of target categories and ask the model to assign each product to the most appropriate one. Notice we include a fallback \"All Pets\" category." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "aiqe_classify_code" + }, + "outputs": [], + "source": [ + "%%bigquery --project {PROJECT_ID}\n", + "\n", + "SELECT\n", + " product_name,\n", + " AI.CLASSIFY(\n", + " ('What animal is this product for?',product_name,' ',description),\n", + " categories => [\"Dog\", \"Cat\", \"Bird\", \"Fish\", \"Small Animal\", \"All Pets\"]\n", + " ) AS animal_type\n", + "FROM\n", + " `bigquery-public-data.cymbal_pets.products`\n", + "WHERE category = \"Toys\"\n", + "LIMIT 20" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "aiqe_if_md" + }, + "source": [ + "### Using `AI.IF`: Filtering product images\n", + "\n", + "[`AI.IF`](https://cloud.google.com/bigquery/docs/reference/standard-sql/bigqueryml-syntax-ai-if) uses a Gemini model to evaluate a condition described in natural language and returns a `BOOL`. Similar to `AI.CLASSIFY`, it can also accept multimodal input. If you haven't yet worked with multimodal data in BigQuery, you can learn more in the [Analyzing Multimodal Data in BigQuery](https://github.com/GoogleCloudPlatform/generative-ai/blob/main/gemini/use-cases/applying-llms-to-data/multimodal-analysis-bigquery/analyze_multimodal_data_bigquery.ipynb) notebook.\n", + "\n", + "Let's use `AI.IF` to perform a visual filtering task on our `product_images` table. We'll use it to find all product images that contain a ball by using `AI.IF` within the `WHERE` clause.\n", + "\n", + "> **Note on Image URLs:** Since we are querying a public dataset without a bound connection service account, we rely on End User Credentials (EUC) and pass the `uri` to the helper function directly to view the image. Normally, when working with your own private datasets and a Cloud resource connection, you would generate a temporary read-only URL by selecting `OBJ.GET_READ_URL(ref).url AS signed_url` (shown commented out in the queries below)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "F3EQDlngy3Fb" + }, + "outputs": [], + "source": [ + "%%bigquery images_df --project {PROJECT_ID}\n", + "\n", + "SELECT\n", + " uri,\n", + " -- OBJ.GET_READ_URL(ref).url AS signed_url,\n", + " metadata\n", + "FROM\n", + " `bigquery-public-data.cymbal_pets.product_images`\n", + "WHERE\n", + " AI.IF(\n", + " ('Does this product image contain a ball? ',ref)\n", + " );" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "sesnAbr-vQMP" + }, + "source": [ + "The results of the previous query are now stored in a pandas DataFrame called `images_df` (a parameter added to the [`%%bigquery magic` utility](https://googleapis.dev/python/bigquery-magics/latest/) in the prior cell).\n", + "\n", + "Let's take a look at the results." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "DJyw9P67vTDg" + }, + "outputs": [], + "source": [ + "images_df" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "Jmt9B-Y3vUMa" + }, + "source": [ + "Now, let's view these images using the helper function created in the **Before you begin** section of this notebook." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "JGe1xdhcvaWx" + }, + "outputs": [], + "source": [ + "display_image_grid(images_df)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "FQyhHKaMdTHD" + }, + "source": [ + "### Using `AI.IF`: Performing a semantic join of product images with product table\n", + "\n", + "We can also use [`AI.IF`](https://cloud.google.com/bigquery/docs/reference/standard-sql/bigqueryml-syntax-ai-if) to perform semantic joins. In this next query, we will use it within the `JOIN` clause to join the `products` table (text) with the `product_images` table (image). The join will only succeed if the image *semantically matches* the product description." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "yZMPtc-HG8D4" + }, + "outputs": [], + "source": [ + "%%bigquery join_df --project {PROJECT_ID}\n", + "\n", + "SELECT\n", + " product_name,\n", + " description,\n", + " brand,\n", + " images.uri AS uri\n", + " -- OBJ.GET_READ_URL(images.ref).url AS signed_url\n", + "FROM\n", + " `bigquery-public-data.cymbal_pets.products` as products\n", + "INNER JOIN\n", + " `bigquery-public-data.cymbal_pets.product_images` as images\n", + "ON\n", + " AI.IF(\n", + " ('You will be provided an image of a pet product. ',\n", + " 'Determine if the image is of the following pet toy: ',\n", + " products.product_name,\n", + " products.description,\n", + " images.ref\n", + " )\n", + " )\n", + "WHERE\n", + " products.category = \"Toys\" AND\n", + " products.brand = \"Fluffy Buns\"" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "kLA2ol2N4OBp" + }, + "source": [ + "Let's take a look at the resulting DataFrame." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "ZEBuaLcowCzH" + }, + "outputs": [], + "source": [ + "join_df" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "AJAHROEJ4m2B" + }, + "source": [ + "Now, let's view the results including images using the helper function." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "slbCltSr4vF0" + }, + "outputs": [], + "source": [ + "display_image_grid(join_df)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "903e146a" + }, + "source": [ + "### Using `AI.AGG`: Summarizing and extracting insights across rows\n", + "\n", + "While functions like `AI.CLASSIFY` and `AI.IF` analyze individual rows, analyzing unstructured data at scale often requires synthesizing information across multiple rows. The [`AI.AGG`](https://cloud.google.com/bigquery/docs/reference/standard-sql/bigqueryml-syntax-ai-agg) function allows you to use natural-language instructions within a single line of SQL to summarize or synthesize information over millions of rows of unstructured or multimodal data.\n", + "\n", + "For example, let's say we want to discover the major product categories across the `bigquery-public-data.cymbal_pets` catalog. With `AI.AGG`, we can ask the model to analyze the raw product names and descriptions across all rows to identify the overarching categories:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "cf90b2ba" + }, + "outputs": [], + "source": [ + "%%bigquery --project {PROJECT_ID}\n", + "\n", + "SELECT\n", + " AI.AGG(\n", + " ('Product: ', product_name, ' - Description: ', description),\n", + " 'What are the major categories of these products?'\n", + " ) AS category_description\n", + "FROM\n", + " `bigquery-public-data.cymbal_pets.products`" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "5818f537" + }, + "source": [ + "### Optimizing `AI.IF` and `AI.CLASSIFY` with optimized mode\n", + "\n", + "When processing large datasets containing thousands or even billions of rows, calling a remote LLM for every row can result in high token consumption and query latency. To solve this, BigQuery provides an **[optimized mode](https://docs.cloud.google.com/bigquery/docs/optimize-ai-functions)** for `AI.IF` and `AI.CLASSIFY`, which leverages **[proxy models and on-the-fly model distillation](https://cloud.google.com/blog/products/data-analytics/more-than-100x-faster-and-cheaper-llm-powered-sql-queries-with-proxy-models)** to deliver over 100x faster and cheaper queries.\n", + "\n", + "#### How optimized mode works:\n", + "1. **Sampling and labeling**: BigQuery automatically selects a small representative sample of your data and calls Gemini to provide labels.\n", + "2. **Distilled model training**: A local, ultra-lightweight distilled model (such as a logistic regression proxy model) is trained just-in-time on CPU using the LLM labels and data embeddings as features.\n", + "3. **Quality check**: BigQuery evaluates the distilled model's accuracy against the LLM's results. If it meets the quality threshold, it is used to process the majority of the dataset.\n", + "4. **Inference**: The proxy model processes the remaining rows locally at ultra-low latency and cost.\n", + "\n", + "> **Minimum row count**: Official documentation recommends an input dataset containing approximately **3,000 rows or more** to ensure robust model distillation.\n", + "\n", + "The following example demonstrates how to enable optimized mode using `optimization_mode => 'MINIMIZE_COST'` with `AI.IF` in a `WHERE` clause to filter news articles. We use the public dataset `bigquery-public-data.bbc_news.fulltext`, which contains over 2,200 news articles. While slightly below the 3,000-row guideline, this example is able to exceed the absolute minimum threshold required to trigger model distillation:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "ac276e00" + }, + "outputs": [], + "source": [ + "%%bigquery --project {PROJECT_ID}\n", + "\n", + "SELECT\n", + " title,\n", + " body\n", + "FROM\n", + " `bigquery-public-data.bbc_news.fulltext`\n", + "WHERE\n", + " AI.IF(\n", + " ('The following news story is about a natural disaster: ', body),\n", + " embeddings => AI.EMBED(body, endpoint => 'text-embedding-005', task_type => 'CLASSIFICATION').result,\n", + " optimization_mode => 'MINIMIZE_COST'\n", + " );" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "hBpgd1tg3eb6" + }, + "source": [ + "After running the query above, you can verify exactly how many rows were optimized by inspecting your job history in the Google Cloud Console:\n", + "\n", + "1. Go to the **[BigQuery Console](https://console.cloud.google.com/bigquery)** and open **Job history** from the **Explorer** pane.\n", + "2. Find the query job you just executed, click the three-dot menu (**⋮** More actions) at the end of the row, and select **Show job details**.\n", + "3. In the job details panel, scroll to the **Gen AI Function Optimizations**) section. You will see a summary similar to:\n", + " > `AI.IF('The following news s'): 875 out of 2225 rows optimized. Cost Optimization successfully applied.`\n", + "This confirms that BigQuery successfully trained a lightweight proxy model on a sample of your data and used it to process the remaining rows locally at zero LLM token cost!" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "scalar_md_new" + }, + "source": [ + "---\n", + "\n", + "## General-purpose AI functions\n", + "\n", + "BigQuery's general-purpose AI functions ([`AI.GENERATE_BOOL`](https://cloud.google.com/bigquery/docs/reference/standard-sql/bigqueryml-syntax-ai-generate-bool), [`AI.GENERATE_DOUBLE`](https://cloud.google.com/bigquery/docs/reference/standard-sql/bigqueryml-syntax-ai-generate-double), and [`AI.GENERATE_INT`](https://cloud.google.com/bigquery/docs/reference/standard-sql/bigqueryml-syntax-ai-generate-int)) are another set of capabilities that bring the power of LLMs for AI-driven data extraction and inference tasks directly within your SQL queries.\n", + "\n", + "These functions are considered \"general-purpose\" because they provide full control over the prompt and are designed for power-users. Similar to the managed functions, they can be used alongside your standard SQL in `SELECT` and `WHERE` clauses, giving you the power to do complex analysis with natural language." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "generate_bool_md" + }, + "source": [ + "### Using `AI.GENERATE_BOOL`: Enriching product details\n", + "\n", + "The [`AI.GENERATE_BOOL`](https://cloud.google.com/bigquery/docs/reference/standard-sql/bigqueryml-syntax-ai-generate-bool) function allows you to analyze any combination of text and unstructured data and returns a `BOOL` value for each row in the table.\n", + "\n", + "Let's use `AI.GENERATE_BOOL` for a data enrichment task. We'll find the products in our catalog that require a power source (like electricity or batteries) to operate and add a clear attribute that isn't already available in our `products` table." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "generate_bool_code" + }, + "outputs": [], + "source": [ + "%%bigquery --project {PROJECT_ID}\n", + "\n", + "SELECT\n", + " product_name,\n", + " category,\n", + " description,\n", + " AI.GENERATE_BOOL(\n", + " ('Does this product require electricity, batteries, ',\n", + " 'or a power source to operate?',product_name,' ',description),\n", + " endpoint => 'gemini-2.5-flash').* EXCEPT(full_response)\n", + "FROM\n", + " `bigquery-public-data.cymbal_pets.products`" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "I7QyOnztBpCs" + }, + "source": [ + "A few items to notice from the query text:\n", + "* `AI.GENERATE_BOOL` accepts an [`endpoint` argument](https://cloud.google.com/bigquery/docs/reference/standard-sql/bigqueryml-syntax-ai-generate-bool#arguments). This allows you to specify any [generally available](https://cloud.google.com/vertex-ai/generative-ai/docs/models#generally_available_models) or [preview](https://cloud.google.com/vertex-ai/generative-ai/docs/models#preview_models) Gemini model. If you don't specify an `endpoint` value, BigQuery selects a recent stable version of Gemini to use.\n", + "* `AI.GENERATE_BOOL` allows you to [specify output options](https://cloud.google.com/bigquery/docs/reference/standard-sql/bigqueryml-syntax-ai-generate-bool#output):\n", + " * `result` - the `BOOL` value containing the model's response to the prompt\n", + " * `full_response` - a JSON value containing all fields from the [response](https://cloud.google.com/vertex-ai/docs/reference/rest/v1/GenerateContentResponse)\n", + " * `status` - a `STRING` value that contains the API response status for the corresponding row (this value is empty if the operation was successful)\n", + "\n", + "In this query, we chose Gemini 2.5 Flash as the `endpoint` and specified the `result` and `status` fields be returned (by using the `*` wildcard after the function and adding an `EXCEPT` to skip returning the `full_response`)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "PfG0QtYmQnyi" + }, + "source": [ + "---\n", + "### Comparison: `AI.GENERATE_BOOL` vs. `AI.IF`\n", + "\n", + "While both [`AI.GENERATE_BOOL`](https://cloud.google.com/bigquery/docs/reference/standard-sql/bigqueryml-syntax-ai-generate-bool) and [`AI.IF`](https://cloud.google.com/bigquery/docs/reference/standard-sql/bigqueryml-syntax-ai-if) use generative AI models to evaluate a condition and return a boolean value, they are optimized for different workflows and capabilities:\n", + "\n", + "\n", + "| Feature | `AI.GENERATE_BOOL` | `AI.IF` |\n", + "| :--- | :--- | :--- |\n", + "| **Optimized Mode** | **Not supported**
    Calls the remote LLM for every row. | **Supported**
    Supports on-the-fly model distillation
    (`MINIMIZE_COST`) to scale efficiently. |\n", + "| **Prompt Optimization** | **Direct Passthrough**
    The model evaluates your exact prompt
    as written without modification. | **Automatic Optimization**
    Automatically structures and enhances
    your prompt to improve output quality
    and consistency. |\n", + "| **Model Parameters** | **Customizable**
    Allows specifying custom `model_params`
    (such as temperature, top_p, and top_k). | **Automated**
    Model parameters are managed
    automatically for optimal evaluation.
    *(Note: Both functions allow specifying
    a custom model via `endpoint`).* |\n", + "| **Output & Metadata** | **STRUCT Output**
    Returns a `STRUCT` containing the `BOOL`
    result along with detailed metadata
    (safety ratings, citations, API status). | **Scalar BOOL Output**
    Returns a simple scalar `BOOL`, making
    it much easier and cleaner to use in
    SQL predicates and join conditions. |\n", + "| **Error Handling** | **Recorded in Output**
    Records detailed error information
    inside the output `STRUCT`. | **Returns NULL**
    If an error occurs for any input row,
    the function cleanly returns `NULL`. |\n", + "\n", + "**Key Takeaway:**\n", + "* Use `AI.GENERATE_BOOL` for use cases where you want full control, or when using it to augment data in the `SELECT` clause.\n", + "* Use `AI.IF` for smart, semantic filtering in the `WHERE` clause, for joining with the `JOIN ON` clause, or when you want to take advantage of **optimized mode**.\n", + "---" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "generate_double_md_corrected" + }, + "source": [ + "### Using `AI.GENERATE_INT`: Counting mentioned ingredients\n", + "\n", + "[`AI.GENERATE_INT`](https://cloud.google.com/bigquery/docs/reference/standard-sql/bigqueryml-syntax-ai-generate-int) is similar to `AI.GENERATE_BOOL`, but differs in that it will return an integer value.\n", + "\n", + "Let's use `AI.GENERATE_INT` to perform an extraction task, specifically to count how many distinct ingredients or food items it can identify in the food product descriptions. While the descriptions are high-level marketing text and not detailed ingredient lists, this will test the model's ability to infer ingredients from a product's name and general description." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "generate_int_code" + }, + "outputs": [], + "source": [ + "%%bigquery --project {PROJECT_ID}\n", + "\n", + "SELECT\n", + " product_name,\n", + " description,\n", + " AI.GENERATE_INT(\n", + " ('Based on the text, how many distinct food ingredients ',\n", + " 'can you identify? If none are listed, return 0.',\n", + " product_name,' ',description),\n", + " endpoint => 'gemini-2.5-flash').result AS ingredient_count\n", + "FROM\n", + " `bigquery-public-data.cymbal_pets.products`\n", + "WHERE\n", + " category = 'Food'\n", + "ORDER BY\n", + " ingredient_count DESC" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "generate_int_md_corrected" + }, + "source": [ + "### Using `AI.GENERATE_DOUBLE`: Estimating shipping weight\n", + "\n", + "[`AI.GENERATE_DOUBLE`](https://cloud.google.com/bigquery/docs/reference/standard-sql/bigqueryml-syntax-ai-generate-double) can be used for similar tasks as `AI.GENERATE_INT`, but differs in that it will return a decimal value. Both functions can be used for *extraction* tasks, as we saw in the last example. In this example, we will see an *inference* task.\n", + "\n", + "Since our product data doesn't include shipping weights, let's ask the model to estimate the weight in pounds with decimal precision. It will have to infer this based on the product's name and description (e.g., a \"50 Gallon Aquarium\" is much heavier than a \"Dog Bone\"). This is a great way to generate missing data." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "xT9vakyw1PZ2" + }, + "outputs": [], + "source": [ + "%%bigquery --project {PROJECT_ID}\n", + "\n", + "SELECT\n", + " product_name,\n", + " description,\n", + " AI.GENERATE_DOUBLE(\n", + " ('Based on this product description, what is a rough ',\n", + " 'estimated weight of the product for shipping in pounds (lbs)?',\n", + " product_name,' ',description),\n", + " endpoint => 'gemini-2.5-flash').result AS estimated_shipping_weight\n", + "FROM\n", + " `bigquery-public-data.cymbal_pets.products`" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "hyl5LncS0UBh" + }, + "source": [ + "## Recap\n", + "\n", + "In this notebook, you explored the powerful suite of AI functions in BigQuery to perform advanced data analysis using natural language.\n", + "\n", + "You learned how to:\n", + "* **Perform semantic analysis with managed AI functions**, which are ideal for all users:\n", + " * Used [`AI.SCORE`](https://cloud.google.com/bigquery/docs/reference/standard-sql/bigqueryml-syntax-ai-score) to **rank** products based on a subjective quality like \"giftability.\"\n", + " * Used [`AI.CLASSIFY`](https://cloud.google.com/bigquery/docs/reference/standard-sql/bigqueryml-syntax-ai-classify) to **categorize** products into predefined animal types.\n", + " * Used [`AI.IF`](https://cloud.google.com/bigquery/docs/reference/standard-sql/bigqueryml-syntax-ai-if) for powerful **multimodal filtering** (finding balls in product images) and **semantic joins** (matching products to their images).\n", + " * Used [`AI.AGG`](https://cloud.google.com/bigquery/docs/reference/standard-sql/bigqueryml-syntax-ai-agg) to **summarize and synthesize** insights across millions of rows of unstructured or multimodal data.\n", + " * Leveraged **optimized mode** (`optimization_mode => 'MINIMIZE_COST'`) to process large datasets faster and cheaper using on-the-fly model distillation and proxy models.\n", + "* **Execute row-level tasks with general-purpose AI functions**, which give power-users full control:\n", + " * Used [`AI.GENERATE_BOOL`](https://cloud.google.com/bigquery/docs/reference/standard-sql/bigqueryml-syntax-ai-generate-bool) to **enrich data** to determine which products require power based on a yes/no condition.\n", + " * Used [`AI.GENERATE_INT`](https://cloud.google.com/bigquery/docs/reference/standard-sql/bigqueryml-syntax-ai-generate-int) to **extract** structured data (a count of ingredients) from unstructured text.\n", + " * Used [`AI.GENERATE_DOUBLE`](https://cloud.google.com/bigquery/docs/reference/standard-sql/bigqueryml-syntax-ai-generate-double) to **infer** and create new data points (estimating shipping weights) that were not present in the original dataset." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "U4Zv6BqE4ko6" + }, + "source": [ + "## Next Steps\n", + "Continue your learning with the following notebooks:\n", + "* [Introduction to Generative AI functions in BigQuery](https://github.com/GoogleCloudPlatform/generative-ai/blob/main/gemini/use-cases/applying-llms-to-data/bigquery_generative_ai_intro.ipynb)\n", + "* [Analyze Multimodal Data in BigQuery](https://github.com/GoogleCloudPlatform/generative-ai/blob/main/gemini/use-cases/applying-llms-to-data/multimodal-analysis-bigquery/analyze_multimodal_data_bigquery.ipynb)\n", + "* [Text + multimodal embedding generation and vector search in BigQuery](https://github.com/GoogleCloudPlatform/generative-ai/blob/main/gemini/use-cases/applying-llms-to-data/bigquery_embeddings_vector_search.ipynb)\n", + "\n", + "Take a look at the product documentation:\n", + "* the [Generative AI Overview](https://cloud.google.com/bigquery/docs/generative-ai-overview) landing page\n", + "* the [Optimize AI functions with model distillation](https://cloud.google.com/bigquery/docs/optimize-ai-functions) guide\n", + "* the [`AI.SCORE`](https://cloud.google.com/bigquery/docs/reference/standard-sql/bigqueryml-syntax-ai-score) function documentation\n", + "* the [`AI.CLASSIFY`](https://cloud.google.com/bigquery/docs/reference/standard-sql/bigqueryml-syntax-ai-classify) function documentation\n", + "* the [`AI.IF`](https://cloud.google.com/bigquery/docs/reference/standard-sql/bigqueryml-syntax-ai-if) function documentation\n", + "* the [`AI.AGG`](https://cloud.google.com/bigquery/docs/reference/standard-sql/bigqueryml-syntax-ai-agg) function documentation\n", + "* the [`AI.GENERATE_BOOL`](https://cloud.google.com/bigquery/docs/reference/standard-sql/bigqueryml-syntax-ai-generate-bool) function documentation\n", + "* the [`AI.GENERATE_INT`](https://cloud.google.com/bigquery/docs/reference/standard-sql/bigqueryml-syntax-ai-generate-int) function documentation\n", + "* the [`AI.GENERATE_DOUBLE`](https://cloud.google.com/bigquery/docs/reference/standard-sql/bigqueryml-syntax-ai-generate-double) function documentation\n", + "\n", + "Read more about these features on the Google Cloud Blog:\n", + "* [Deep dive into BigQuery AI.AGG function](https://cloud.google.com/blog/products/data-analytics/deep-dive-into-bigquery-ai-agg-function)\n", + "* [More than 100x faster and cheaper LLM-powered SQL queries with proxy models](https://cloud.google.com/blog/products/data-analytics/more-than-100x-faster-and-cheaper-llm-powered-sql-queries-with-proxy-models)\n", + "* [Deep Dive: Categorize Unstructured Data with BigQuery’s AI.CLASSIFY](https://medium.com/google-cloud/deep-dive-categorize-unstructured-data-with-bigquerys-ai-classify-6e760ac7ebed)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "cleanup_md" + }, + "source": [ + "# Cleaning Up\n", + "\n", + "Running the examples in this notebook did not create any new resources in your Google Cloud project. However, if you created a project solely to run through this notebook, you can [delete the project](https://cloud.google.com/resource-manager/docs/creating-managing-projects#shutting_down_projects)." + ] + } + ], + "metadata": { + "colab": { + "name": "bigquery_ai_operators.ipynb", + "toc_visible": true + }, + "kernelspec": { + "display_name": "Python 3", + "name": "python3" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} diff --git a/gemini/use-cases/applying-llms-to-data/bigquery_dataframes_ml_drug_name_generation.ipynb b/gemini/use-cases/applying-llms-to-data/bigquery_dataframes_ml_drug_name_generation.ipynb new file mode 100644 index 0000000..2fdffa6 --- /dev/null +++ b/gemini/use-cases/applying-llms-to-data/bigquery_dataframes_ml_drug_name_generation.ipynb @@ -0,0 +1,1006 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "ur8xi4C7S06n" + }, + "outputs": [], + "source": [ + "# Copyright 2025 Google LLC\n", + "#\n", + "# Licensed under the Apache License, Version 2.0 (the \"License\");\n", + "# you may not use this file except in compliance with the License.\n", + "# You may obtain a copy of the License at\n", + "#\n", + "# https://www.apache.org/licenses/LICENSE-2.0\n", + "#\n", + "# Unless required by applicable law or agreed to in writing, software\n", + "# distributed under the License is distributed on an \"AS IS\" BASIS,\n", + "# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n", + "# See the License for the specific language governing permissions and\n", + "# limitations under the License." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "JAPoU8Sm5E6e" + }, + "source": [ + "# BigQuery DataFrames ML: Prescription Drug Name Generation\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
    \n", + " \n", + " \"Google
    Open in Colab\n", + "
    \n", + "
    \n", + " \n", + " \"Google
    Open in Colab Enterprise\n", + "
    \n", + "
    \n", + " \n", + " \"Vertex
    Open in Vertex AI Workbench\n", + "
    \n", + "
    \n", + " \n", + " \"BigQuery
    Open in BigQuery Studio\n", + "
    \n", + "
    \n", + " \n", + " \"GitHub
    View on GitHub\n", + "
    \n", + "
    \n", + "\n", + "
    \n", + "\n", + "Share to:\n", + "\n", + "\n", + " \"LinkedIn\n", + "\n", + "\n", + "\n", + " \"Bluesky\n", + "\n", + "\n", + "\n", + " \"X\n", + "\n", + "\n", + "\n", + " \"Reddit\n", + "\n", + "\n", + "\n", + " \"Facebook\n", + " " + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "a3a54161ab79" + }, + "source": [ + "| Authors |\n", + "| --- |\n", + "| [Ashley Xu](https://github.com/ashleyxuu) |\n", + "| [Holt Skinner](https://github.com/holtskinner) |" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "24743cf4a1e1" + }, + "source": [ + "**_NOTE_**: This notebook has been tested in the following environment:\n", + "\n", + "* Python version = 3.9" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "tvgnzT1CKxrO" + }, + "source": [ + "## Overview\n", + "\n", + "The goal of this notebook is to demonstrate an enterprise generative AI use case. A marketing user can provide information about a new pharmaceutical drug and its generic name, and receive ideas on marketing-oriented brand names for that drug.\n", + "\n", + "Learn more about [BigQuery DataFrames](https://cloud.google.com/bigquery/docs/dataframes-quickstart)." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "d975e698c9a4" + }, + "source": [ + "### Objective\n", + "\n", + "In this tutorial, you learn about Generative AI concepts such as prompting and few-shot learning, as well as how to use BigFrames ML for performing these tasks simply using an intuitive dataframe API.\n", + "\n", + "The steps performed include:\n", + "\n", + "1. Ask the user for the generic name and usage for the drug.\n", + "1. Use `bigframes` to query the FDA dataset of over 100,000 drugs, filtered on the brand name, generic name, and indications & usage columns.\n", + "1. Filter this dataset to find prototypical brand names that can be used as examples in prompt tuning.\n", + "1. Create a prompt with the user input, general instructions, examples and counter-examples for the desired brand name.\n", + "1. Use [`bigframes.ml.llm.GeminiTextGenerator`](https://dataframes.bigquery.dev/reference/api/bigframes.ml.llm.GeminiTextGenerator.html) to generate choices of brand names." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "08d289fa873f" + }, + "source": [ + "### Dataset\n", + "\n", + "This notebook uses the [FDA dataset](https://cloud.google.com/blog/topics/healthcare-life-sciences/fda-mystudies-comes-to-google-cloud) available at [`bigquery-public-data.fda_drug`](https://console.cloud.google.com/bigquery?ws=!1m4!1m3!3m2!1sbigquery-public-data!2sfda_drug)." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "aed92deeb4a0" + }, + "source": [ + "### Costs\n", + "\n", + "This tutorial uses billable components of Google Cloud:\n", + "\n", + "* BigQuery (compute)\n", + "* BigQuery ML\n", + "\n", + "Learn about [BigQuery compute pricing](https://cloud.google.com/bigquery/pricing#analysis_pricing_models),\n", + "and [BigQuery ML pricing](https://cloud.google.com/bigquery/pricing#bqml),\n", + "and use the [Pricing Calculator](https://cloud.google.com/products/calculator/)\n", + "to generate a cost estimate based on your projected usage." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "i7EUnXsZhAGF" + }, + "source": [ + "## Installation\n", + "\n", + "Install the following packages required to execute this notebook." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "2b4ef9b72d43" + }, + "outputs": [], + "source": [ + "%pip install --upgrade --quiet bigframes google-cloud-bigquery-connection" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "960505627ddf" + }, + "source": [ + "### Import libraries" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "sBCra4QMA2wR" + }, + "source": [ + "### Authenticate your Google Cloud account\n", + "\n", + "Depending on your Jupyter environment, you may have to manually authenticate. Follow the relevant instructions below." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "74ccc9e52986" + }, + "source": [ + "**1. Vertex AI Workbench**\n", + "* Do nothing as you are already authenticated." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "de775a3773ba" + }, + "source": [ + "**2. Local JupyterLab instance, run:**" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "254614fa0c46" + }, + "outputs": [], + "source": [ + "! gcloud auth login" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "ef21552ccea8" + }, + "source": [ + "**3. Colab, run:**" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "603adbbf0532" + }, + "outputs": [], + "source": [ + "import sys\n", + "\n", + "import bigframes.pandas as bpd\n", + "from IPython.display import Markdown\n", + "from bigframes.ml.llm import GeminiTextGenerator\n", + "from google.cloud import bigquery_connection_v1 as bq_connection\n", + "\n", + "if \"google.colab\" in sys.modules:\n", + " from google.colab import auth\n", + "\n", + " auth.authenticate_user()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "BF1j6f9HApxa" + }, + "source": [ + "## Before you begin\n", + "\n", + "### Set up your Google Cloud project\n", + "\n", + "**The following steps are required, regardless of your notebook environment.**\n", + "\n", + "1. [Select or create a Google Cloud project](https://console.cloud.google.com/cloud-resource-manager). When you first create an account, you get a $300 free credit towards your compute/storage costs.\n", + "\n", + "2. [Make sure that billing is enabled for your project](https://cloud.google.com/billing/docs/how-to/modify-project).\n", + "\n", + "3. [Enable the BigQuery API](https://console.cloud.google.com/flows/enableapi?apiid=bigquery.googleapis.com).\n", + "\n", + "4. If you are running this notebook locally, you need to install the [Cloud SDK](https://cloud.google.com/sdk)." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "WReHDGG5g0XY" + }, + "source": [ + "#### Set your project ID\n", + "\n", + "**If you don't know your project ID**, try the following:\n", + "* Run `gcloud config list`.\n", + "* Run `gcloud projects list`.\n", + "* See the support page: [Locate the project ID](https://support.google.com/googleapi/answer/7014113)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "oM1iC_MfAts1" + }, + "outputs": [], + "source": [ + "# Use the environment variable if the user doesn't provide Project ID.\n", + "import os\n", + "\n", + "# fmt: off\n", + "PROJECT_ID = \"document-ai-test-337818\" # @param {type: \"string\", placeholder: \"[your-project-id]\", isTemplate: true}\n", + "# fmt: on\n", + "if not PROJECT_ID or PROJECT_ID == \"[your-project-id]\":\n", + " PROJECT_ID = str(os.environ.get(\"GOOGLE_CLOUD_PROJECT\"))\n", + "\n", + "# Set the project id\n", + "! gcloud config set project {PROJECT_ID}" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "t5w0GGdYsDjS" + }, + "source": [ + "### Load the Gemini model\n", + "\n", + "Learn more about all [Gemini models on Vertex AI](https://cloud.google.com/vertex-ai/generative-ai/docs/learn/models#gemini-models)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "NrUfvucbauu0" + }, + "outputs": [], + "source": [ + "MODEL_ID = \"gemini-2.5-flash\"" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "evsJaAj5te0X" + }, + "source": [ + "#### BigFrames configuration\n", + "\n", + "Next, we will specify a [BigQuery connection](https://cloud.google.com/bigquery/docs/working-with-connections). If you already have a connection, you can simplify provide the name and skip the following creation steps." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "G1vVsPiMsL2X" + }, + "outputs": [], + "source": [ + "# Please fill in these values.\n", + "LOCATION = \"us\" # @param {type:\"string\"}\n", + "CONNECTION = \"bigframes-ml\" # @param {type:\"string\"}\n", + "\n", + "connection_name = f\"{PROJECT_ID}.{LOCATION}.{CONNECTION}\"" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "WGS_TzhWlPBN" + }, + "source": [ + "We will now try to use the provided connection, and if it doesn't exist, create a new one. We will also print the service account used." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "56Hw42m6kFrj" + }, + "outputs": [], + "source": [ + "# Initialize client and set request parameters\n", + "client = bq_connection.ConnectionServiceClient()\n", + "CONNECTION_NAME = client.connection_path(PROJECT_ID, LOCATION, CONNECTION)\n", + "\n", + "# Try to connect using provided connection\n", + "try:\n", + " response = client.get_connection(name=CONNECTION_NAME)\n", + "# Create a new connection on error\n", + "except Exception:\n", + " connection = bq_connection.Connection(\n", + " friendly_name=CONNECTION, cloud_resource=bq_connection.CloudResourceProperties()\n", + " )\n", + " response = client.create_connection(\n", + " request=bq_connection.CreateConnectionRequest(\n", + " parent=client.common_location_path(PROJECT_ID, LOCATION),\n", + " connection_id=CONNECTION,\n", + " connection=connection,\n", + " )\n", + " )\n", + "finally:\n", + " CONN_SERVICE_ACCOUNT = (\n", + " f\"serviceAccount:{response.cloud_resource.service_account_id}\"\n", + " )\n", + "\n", + "print(CONN_SERVICE_ACCOUNT)\n", + "\n", + "# Set service account permissions\n", + "!gcloud projects add-iam-policy-binding {PROJECT_ID} --condition=None --no-user-output-enabled --member={CONN_SERVICE_ACCOUNT} --role='roles/bigquery.connectionUser'\n", + "!gcloud projects add-iam-policy-binding {PROJECT_ID} --condition=None --no-user-output-enabled --member={CONN_SERVICE_ACCOUNT} --role='roles/aiplatform.user'\n", + "!gcloud projects add-iam-policy-binding {PROJECT_ID} --condition=None --no-user-output-enabled --member={CONN_SERVICE_ACCOUNT} --role='roles/run.invoker'" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "init_aip:mbsdk,all" + }, + "source": [ + "### Initialize BigFrames client\n", + "\n", + "Here, we set the project configuration based on the provided parameters." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "OCccLirpkSRz" + }, + "outputs": [], + "source": [ + "bpd.options.bigquery.project = PROJECT_ID\n", + "bpd.options.bigquery.location = LOCATION" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "m8UCEtX9uLn6" + }, + "source": [ + "## Generate a name\n", + "\n", + "Let's start with entering a generic name and description of the drug." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "oxphj2gnuKou" + }, + "outputs": [], + "source": [ + "GENERIC_NAME = \"Entropofloxacin\" # @param {type:\"string\"}\n", + "# fmt: off\n", + "USAGE = \"Entropofloxacin is a fluoroquinolone antibiotic that is used to treat a variety of bacterial infections, including: pneumonia, streptococcus infections, salmonella infections, escherichia coli infections, and pseudomonas aeruginosa infections It is taken by mouth or by injection. The dosage and frequency of administration will vary depending on the type of infection being treated. It should be taken for the full course of treatment, even if symptoms improve after a few days. Stopping the medication early may increase the risk of the infection coming back.\" # @param {type: \"string\"}\n", + "# fmt: on\n", + "NUM_NAMES = 10 # @param {type: \"integer\"}" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "1q-vlbalzu1Q" + }, + "source": [ + "We can now create a prompt string, and populate it with the name and description." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "0knz5ZWMzed-" + }, + "outputs": [], + "source": [ + "zero_shot_prompt = f\"\"\"Provide {NUM_NAMES} unique and modern brand names in Markdown bullet point format. Do not provide any additional explanation.\n", + "\n", + "Be creative with the brand names. Don't use English words directly; use variants or invented words.\n", + "\n", + "The generic name is: {GENERIC_NAME}\n", + "\n", + "The indications and usage are: {USAGE}.\"\"\"\n", + "\n", + "print(zero_shot_prompt)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "LCRE2L720f5y" + }, + "source": [ + "Next, let's create a helper function to predict with our model. It will take a string input, and add it to a temporary BigFrames `DataFrame`. It will also return the string extracted from the response `DataFrame`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "LB3xgDroIxlx" + }, + "outputs": [], + "source": [ + "def predict(model: GeminiTextGenerator, prompt: str) -> str:\n", + " # Create DataFrame\n", + " input = bpd.DataFrame(\n", + " {\n", + " \"prompt\": [prompt],\n", + " }\n", + " )\n", + "\n", + " # Return response\n", + " return model.predict(input).ml_generate_text_llm_result.iloc[0]" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "b1ZapNZsJW2p" + }, + "source": [ + "We can now initialize the model, and get a response to our prompt!" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "UW2fQ2k5Hsic" + }, + "outputs": [], + "source": [ + "# Get BigFrames session\n", + "session = bpd.get_global_session()\n", + "\n", + "# Define the model\n", + "model = GeminiTextGenerator(\n", + " model_name=MODEL_ID, session=session, connection_name=connection_name\n", + ")\n", + "\n", + "# Invoke LLM with prompt\n", + "response = predict(model, zero_shot_prompt)\n", + "\n", + "# Print results as Markdown\n", + "Markdown(response)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "o3yIhHV2jsUT" + }, + "source": [ + "We're off to a great start! Let's see if we can refine our response." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "mBroUzWS8xOL" + }, + "source": [ + "## Few-shot prompting\n", + "\n", + "Let's try using [few-shot prompting](https://www.promptingguide.ai/techniques/fewshot). We will provide a few examples of what we're looking for along with our prompt.\n", + "\n", + "Our prompt will consist of 3 parts:\n", + "* General instructions (e.g. generate $n$ brand names)\n", + "* Multiple examples\n", + "* Information about the drug we'd like to generate a name for\n", + "\n", + "Let's walk through how to construct this prompt.\n", + "\n", + "Our first step will be to define how many examples we want to provide in the prompt." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "MXdI78SOElyt" + }, + "outputs": [], + "source": [ + "# Specify number of examples to include\n", + "\n", + "NUM_EXAMPLES = 3 # @param {type: \"integer\"}" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "U8w4puVM_892" + }, + "source": [ + "Next, let's define a prefix that will set the overall context." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "aQ2iscnhF2cx" + }, + "outputs": [], + "source": [ + "prefix_prompt = f\"\"\"Provide {NUM_NAMES} unique and modern brand names in Markdown bullet point format, related to the drug at the bottom of this prompt.\n", + "\n", + "Be creative with the brand names. Don't use English words directly; use variants or invented words.\n", + "\n", + "First, we will provide {NUM_EXAMPLES} examples to help with your thought process.\n", + "\n", + "Then, we will provide the generic name and usage for the drug we'd like you to generate brand names for.\n", + "\"\"\"\n", + "\n", + "print(prefix_prompt)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "VI0Spv-axN7d" + }, + "source": [ + "Our next step will be to include examples into the prompt.\n", + "\n", + "We will start out by retrieving the raw data for the examples, by querying the BigQuery public dataset." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "IoO_Bp8wA07N" + }, + "outputs": [], + "source": [ + "# Query 3 columns of interest from drug label dataset\n", + "df = bpd.read_gbq(\n", + " \"bigquery-public-data.fda_drug.drug_label\",\n", + " col_order=[\"openfda_generic_name\", \"openfda_brand_name\", \"indications_and_usage\"],\n", + ")\n", + "\n", + "# Exclude any rows with missing data\n", + "df = df.dropna()\n", + "\n", + "# Drop duplicate rows\n", + "df = df.drop_duplicates()\n", + "\n", + "# Print values\n", + "df.head()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "W5kOtbNGBTI2" + }, + "source": [ + "Let's now filter the results to remove atypical names." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "95WDe2eCCeLx" + }, + "outputs": [], + "source": [ + "# Remove names with spaces\n", + "df = df[df[\"openfda_brand_name\"].str.find(\" \") == -1]\n", + "\n", + "# Remove names with 5 or fewer characters\n", + "df = df[df[\"openfda_brand_name\"].str.len() > 5]\n", + "\n", + "# Remove names where the generic and brand name match (case-insensitive)\n", + "df = df[df[\"openfda_generic_name\"].str.lower() != df[\"openfda_brand_name\"].str.lower()]" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "FZD89ep4EyYc" + }, + "source": [ + "Let's take `NUM_EXAMPLES` samples to include in the prompt." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "2ohZYg7QEyJV" + }, + "outputs": [], + "source": [ + "# Take a sample and convert to a Pandas dataframe for local usage.\n", + "df_examples = df.sample(NUM_EXAMPLES, random_state=5).to_pandas()\n", + "\n", + "df_examples" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "oU4mb1Dwgq64" + }, + "source": [ + "Let's now load the examples data into a prompt. For consistency, we'll capitalize each example brand name.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "kzAVsF6wJ93S" + }, + "outputs": [], + "source": [ + "example_prompt = \"\\n\\n---\\n\\n\".join(\n", + " f\"Generic name: {generic_name}\\nUsage: {usage}\\nBrand name: {brand_name.capitalize()}\"\n", + " for brand_name, generic_name, usage in zip(\n", + " df_examples[\"openfda_brand_name\"],\n", + " df_examples[\"openfda_generic_name\"],\n", + " df_examples[\"indications_and_usage\"],\n", + " strict=False,\n", + " )\n", + ")\n", + "example_prompt" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "kbV2X1CXAyLV" + }, + "source": [ + "Finally, we can create a suffix to our prompt. This will contain the generic name of the drug, its usage, ending with a request for brand names." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "OYp6W_XfHTlo" + }, + "outputs": [], + "source": [ + "suffix_prompt = f\"\"\"\\n\n", + "---\n", + "\n", + "Generic name: {GENERIC_NAME}\n", + "Usage: {USAGE}\n", + "Brand names:\"\"\"\n", + "\n", + "print(suffix_prompt)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "RiaisW1nihJP" + }, + "source": [ + "Let's pull it altogether into a few-shot prompt." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "99xdU7l8C1h8" + }, + "outputs": [], + "source": [ + "# Define the prompt\n", + "few_shot_prompt = prefix_prompt + example_prompt + suffix_prompt\n", + "\n", + "# Print the prompt\n", + "print(few_shot_prompt)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "nbUWdHtfitWn" + }, + "source": [ + "Now, let's pass our prompt to the LLM, and get a response!" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "d4ODRJdvLhlQ" + }, + "outputs": [], + "source": [ + "response = predict(few_shot_prompt)\n", + "\n", + "Markdown(response)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "pFakjrTElOBs" + }, + "source": [ + "# Batch generation\n", + "\n", + "Let's take these experiments to the next level by generating many names in a batch. We'll see how to leverage BigFrames at scale!\n", + "\n", + "We can start by finding drugs that are missing brand names. There are approximately 4,000 drugs that meet this criteria. We'll put a limit of 100 in this notebook." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "8eAutS41mx6U" + }, + "outputs": [], + "source": [ + "# Query 3 columns of interest from drug label dataset\n", + "df_missing = bpd.read_gbq(\n", + " \"bigquery-public-data.fda_drug.drug_label\",\n", + " col_order=[\"openfda_generic_name\", \"openfda_brand_name\", \"indications_and_usage\"],\n", + ")\n", + "\n", + "# Exclude any rows with missing data\n", + "df_missing = df_missing.dropna()\n", + "\n", + "# Include rows in which openfda_brand_name equals openfda_generic_name\n", + "df_missing = df_missing[\n", + " df_missing[\"openfda_generic_name\"].str.lower()\n", + " == df_missing[\"openfda_brand_name\"].str.lower()\n", + "]\n", + "\n", + "# Limit the number of rows for demonstration purposes\n", + "LIMIT = 10\n", + "df_missing = df_missing.head(LIMIT)\n", + "\n", + "# Print values\n", + "df_missing.head()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "Fm6L8S7eVnCI" + }, + "source": [ + "We will create a column `prompt` with a customized prompt for each row." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "19TvGN1PVmVX" + }, + "outputs": [], + "source": [ + "df_missing[\"prompt\"] = (\n", + " \"Provide a unique and modern brand name related to this pharmaceutical drug.\"\n", + " \"Don't use English words directly; use variants or invented words. The generic name is: \"\n", + " + df_missing[\"openfda_generic_name\"]\n", + " + \". The indications and usage are: \"\n", + " + df_missing[\"indications_and_usage\"]\n", + " + \".\"\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "njxwBvCKgMPE" + }, + "source": [ + "We'll create a new helper method, `batch_predict()` and query the LLM. The job may take a couple minutes to execute." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "tiSHa5B4aFhw" + }, + "outputs": [], + "source": [ + "def batch_predict(model: GeminiTextGenerator, input: bpd.DataFrame) -> bpd.DataFrame:\n", + " return model.predict(input).ml_generate_text_llm_result\n", + "\n", + "\n", + "response = batch_predict(model, df_missing[\"prompt\"])" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "K5a2nHdLgZEj" + }, + "source": [ + "Let's check the results for one of our responses!" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "TnizdeqBdbZj" + }, + "outputs": [], + "source": [ + "# Pick a sample\n", + "k = 0\n", + "\n", + "# Gather the prompt and response details\n", + "df_missing = df_missing.head(LIMIT).reset_index(drop=True)\n", + "response = response.head(LIMIT).reset_index(drop=True)\n", + "\n", + "prompt_generic = df_missing[\"openfda_generic_name\"][k]\n", + "prompt_usage = df_missing[\"indications_and_usage\"][k]\n", + "response_str = response[k]\n", + "\n", + "# Print details\n", + "print(f\"Generic name: {prompt_generic}\")\n", + "print(f\"Brand name: {prompt_usage}\")\n", + "print(f\"Response: {response_str}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "W4MviwyMI-Qh" + }, + "source": [ + "Congratulations! You have learned how to use generative AI to jump-start the creative process.\n", + "\n", + "You've also seen how BigFrames can manage each step of the process, including gathering data, data manipulation, and querying the LLM." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "Bys6--dVmq7R" + }, + "source": [ + "## Cleaning up\n", + "\n", + "To clean up all Google Cloud resources used in this project, you can [delete the Google Cloud\n", + "project](https://cloud.google.com/resource-manager/docs/creating-managing-projects#shutting_down_projects) you used for the tutorial.\n", + "\n", + "Otherwise, you can uncomment the remaining cells and run them to delete the individual resources you created in this tutorial:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "cIODjOLump_-" + }, + "outputs": [], + "source": [ + "# Delete the BigQuery Connection\n", + "from google.cloud import bigquery_connection_v1 as bq_connection\n", + "\n", + "client = bq_connection.ConnectionServiceClient()\n", + "client.delete_connection(name=CONNECTION_NAME)\n", + "print(f\"Deleted connection {CONNECTION_NAME}.\")" + ] + } + ], + "metadata": { + "colab": { + "name": "bigquery_dataframes_ml_drug_name_generation.ipynb", + "toc_visible": true + }, + "kernelspec": { + "display_name": "Python 3", + "name": "python3" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} diff --git a/gemini/use-cases/applying-llms-to-data/bigquery_embeddings_vector_search.ipynb b/gemini/use-cases/applying-llms-to-data/bigquery_embeddings_vector_search.ipynb new file mode 100644 index 0000000..654e24b --- /dev/null +++ b/gemini/use-cases/applying-llms-to-data/bigquery_embeddings_vector_search.ipynb @@ -0,0 +1,2335 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "cMQCs0oQf5Jo" + }, + "outputs": [], + "source": [ + "# Copyright 2025 Google LLC\n", + "#\n", + "# Licensed under the Apache License, Version 2.0 (the \"License\");\n", + "# you may not use this file except in compliance with the License.\n", + "# You may obtain a copy of the License at\n", + "#\n", + "# https://www.apache.org/licenses/LICENSE-2.0\n", + "#\n", + "# Unless required by applicable law or agreed to in writing, software\n", + "# distributed under the License is distributed on an \"AS IS\" BASIS,\n", + "# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n", + "# See the License for the specific language governing permissions and\n", + "# limitations under the License." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "iR0jdheRGG89" + }, + "source": [ + "# Text + multimodal embedding generation and vector search in BigQuery\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "td9kx9LVgSve" + }, + "source": [ + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
    \n", + " \n", + " \"Google
    Open in Colab\n", + "
    \n", + "
    \n", + " \n", + " \"Google
    Open in Colab Enterprise\n", + "
    \n", + "
    \n", + " \n", + " \"Vertex
    Open in Vertex AI Workbench\n", + "
    \n", + "
    \n", + " \n", + " \"BigQuery
    Open in BigQuery Studio\n", + "
    \n", + "
    \n", + " \n", + " \"GitHub
    View on GitHub\n", + "
    \n", + "
    \n", + "\n", + "
    \n", + "\n", + "Share to:\n", + "\n", + "\n", + " \"LinkedIn\n", + "\n", + "\n", + "\n", + " \"Bluesky\n", + "\n", + "\n", + "\n", + " \"X\n", + "\n", + "\n", + "\n", + " \"Reddit\n", + "\n", + "\n", + "\n", + " \"Facebook\n", + "" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "QoWWwsHwSrCc" + }, + "source": [ + "| | |\n", + "|-|-|\n", + "|Authors | [Alicia Williams](https://github.com/aliciawilliams), [Jeff Nelson](https://github.com/jeffonelson) |" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "intro_md" + }, + "source": [ + "## Overview\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "SwQLISjqGTHd" + }, + "source": [ + "### Objectives\n", + "\n", + "In this tutorial, you'll build a [**semantic search**](https://cloud.google.com/discover/what-is-semantic-search) utility for a fictional e-commerce pet store. You will use [BigQuery](https://cloud.google.com/bigquery/docs) and [Vertex AI](https://cloud.google.com/vertex-ai/docs), to go beyond simple keyword matching by generating [**vector embeddings**](https://cloud.google.com/vertex-ai/generative-ai/docs/embeddings) for product text descriptions _and_ for images. This allows customers to find products based on conceptual meaning, not just keywords, creating a more intuitive shopping experience.\n", + "\n", + "By the end, you will know how to:\n", + "\n", + "* **Generate embeddings (numerical representations)** for text and image data.\n", + "\n", + "* **Perform text-to-text vector search** to find products with similar text descriptions\n", + "\n", + "* **Perform text-to-image vector search** to find product images that visually match a text query\n", + "\n", + "\n", + "All within BigQuery!\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "costs_md" + }, + "source": [ + "### Services and Costs\n", + "\n", + "This tutorial uses the following billable components of Google Cloud:\n", + "\n", + "* **BigQuery**: [Pricing](https://cloud.google.com/bigquery/pricing)\n", + "\n", + "* **BigQuery ML**: [Pricing](https://cloud.google.com/bigquery/pricing#bqml)\n", + "\n", + "* **Vertex AI**: [Pricing](https://cloud.google.com/vertex-ai/generative-ai/pricing)\n", + "\n", + "You can use the [Pricing Calculator](https://cloud.google.com/products/calculator) to generate a cost estimate based on your projected usage." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "nkoCFoFVSPii" + }, + "source": [ + "---\n", + "\n", + "## Before you begin" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "setup_md_1" + }, + "source": [ + "### Set up your Google Cloud project\n", + "**The following steps are required, regardless of your notebook environment.**\n", + "\n", + "1. [Select or create a Google Cloud project](https://console.cloud.google.com/cloud-resource-manager). When you first create an account, you get a $300 free credit towards your compute/storage costs.\n", + "\n", + "2. [Make sure that billing is enabled for your project](https://cloud.google.com/billing/docs/how-to/modify-project).\n", + "\n", + "3. [Enable the BigQuery, BigQuery Connection, and Vertex AI APIs](https://console.cloud.google.com/flows/enableapi?apiid=bigquery.googleapis.com,bigqueryconnection.googleapis.com,aiplatform.googleapis.com).\n", + "\n", + "4. If you are running this notebook locally, you need to install the [Cloud SDK](https://cloud.google.com/sdk)." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "YQ3g-h7uTaSf" + }, + "source": [ + "### Set your project ID" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "set_project_id" + }, + "outputs": [], + "source": [ + "PROJECT_ID = \"\" # @param {type:\"string\"}\n", + "\n", + "# Set the project id\n", + "! gcloud config set project {PROJECT_ID}" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "auth_md" + }, + "source": [ + "### Authenticate to your Google Cloud account\n", + "\n", + "Depending on your Jupyter environment, you may have to manually authenticate. Follow the relevant instructions below." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "V6NjZRCXU5Ro" + }, + "source": [ + "**1. Colab Enterprise or BigQuery Studio Notebooks**\n", + "* Do nothing as you are already authenticated." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "l0dV1hvAU1ed" + }, + "source": [ + "**2. Colab, uncomment and run:**" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "auth_code" + }, + "outputs": [], + "source": [ + "from google.colab import auth\n", + "\n", + "auth.authenticate_user()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "4Pp4LAJ3UyRP" + }, + "source": [ + "**3. Local JupyterLab instance, uncomment and run:**\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "AzU7S3fMVDkW" + }, + "outputs": [], + "source": [ + "# ! gcloud auth login" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "conn_md" + }, + "source": [ + "### Create BigQuery Cloud resource connection\n", + "\n", + "To allow BigQuery to securely interact with other Google Cloud services like Vertex AI (where embedding models are hosted), you need to create a [Cloud resource connection](https://cloud.google.com/bigquery/docs/create-cloud-resource-connection). This code uses the `bq` command-line tool to create a `CLOUD_RESOURCE` connection named `cymbal_conn` in the `US` location." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "conn_code" + }, + "outputs": [], + "source": [ + "!bq mk --connection --location=us \\\n", + " --connection_type=CLOUD_RESOURCE cymbal_conn" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "perms_md" + }, + "source": [ + "### Set permissions for Service Account\n", + "\n", + "The resource connection service account requires certain project-level permissions to interact with Vertex AI and Google Cloud Storage." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "-3iizFFY8503" + }, + "source": [ + "First, you need to find the email address associated with your new connection's service account." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "6OqpZNY953xR" + }, + "outputs": [], + "source": [ + "SERVICE_ACCT = !bq show --format=prettyjson --connection us.cymbal_conn | grep \"serviceAccountId\" | cut -d '\"' -f 4\n", + "SERVICE_ACCT_EMAIL = SERVICE_ACCT[-1]\n", + "print(SERVICE_ACCT_EMAIL)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "mt9Tb-li9BSu" + }, + "source": [ + "Next you'll assign roles that give their service account permission to be a \"BigQuery Connection User,\" an \"AI Platform User\" (so it can access Gemini models in Vertex AI), and a \"Storage Object Viewer\" (so it can access images in Cloud Storage).\n", + "\n", + "*A 60 second pause is added to allow the new IAM permissions to propagate throughout Google Cloud.*" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "perms_code" + }, + "outputs": [], + "source": [ + "import time\n", + "\n", + "!gcloud projects add-iam-policy-binding --format=none $PROJECT_ID --member=serviceAccount:$SERVICE_ACCT_EMAIL --role='roles/bigquery.connectionUser'\n", + "!gcloud projects add-iam-policy-binding --format=none $PROJECT_ID --member=serviceAccount:$SERVICE_ACCT_EMAIL --role='roles/aiplatform.user'\n", + "!gcloud projects add-iam-policy-binding --format=none $PROJECT_ID --member=serviceAccount:$SERVICE_ACCT_EMAIL --role='roles/storage.objectViewer'\n", + "\n", + "time.sleep(60)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "BfaINa3YxYQ3" + }, + "source": [ + "### Create helper function to render images from Google Cloud Storage in this notebook\n", + "\n", + "This is a helpful utility function that you'll use later in the tutorial. It takes the results of your search query (stored in a pandas DataFrame) and displays the corresponding product images in a nice grid format, making it easy to see how well your search worked." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "4g4dsEJKusGN" + }, + "source": [ + "#### Import necessary libraries for the helper function" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "jiepvKmLuyob" + }, + "outputs": [], + "source": [ + "# Imports for GCS, plotting, and image handling\n", + "import io\n", + "import math\n", + "\n", + "import matplotlib.pyplot as plt\n", + "from PIL import Image\n", + "from google.cloud import storage" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "rfm005SWxhxg" + }, + "source": [ + "#### Create a Google Cloud Storage client\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "xUcaWbhWxkVv" + }, + "outputs": [], + "source": [ + "# Set variables\n", + "REGION = \"US\"\n", + "bucket = \"sample-data-and-media\"\n", + "# Create Cloud Storage client\n", + "client = storage.Client(project=PROJECT_ID)\n", + "bucket = client.bucket(bucket)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "s6YFp-2Axer1" + }, + "outputs": [], + "source": [ + "def display_product_images(df):\n", + " \"\"\"Displays product images from a DataFrame in a 3-column grid.\n", + " Simplified for clarity in a tutorial context.\n", + " \"\"\"\n", + " # Calculate grid size\n", + " num_images = len(df)\n", + " cols = 3\n", + " rows = math.ceil(num_images / cols)\n", + "\n", + " fig, axs = plt.subplots(rows, cols, figsize=(10, rows * 3.5))\n", + "\n", + " # Flatten the axes array for easy iteration, regardless of shape\n", + " axs = axs.flatten()\n", + "\n", + " for i, row in df.iterrows():\n", + " # Extract the image path from the full GCS URI\n", + " image_path = row[\"uri\"].split(\"/\", 3)[-1]\n", + "\n", + " # Get the image from GCS\n", + " blob = bucket.blob(image_path)\n", + " image = Image.open(io.BytesIO(blob.download_as_bytes()))\n", + "\n", + " # Plot the image\n", + " axs[i].imshow(image)\n", + " axs[i].set_title(image_path.split(\"/\")[-1], fontsize=10)\n", + " axs[i].axis(\"off\")\n", + "\n", + " # Hide any unused subplots\n", + " for j in range(num_images, len(axs)):\n", + " axs[j].axis(\"off\")\n", + "\n", + " plt.tight_layout()\n", + " plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "HozZFfxrg9d_" + }, + "source": [ + "---\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "yla8q_2EFQl9" + }, + "source": [ + "## Load and prepare the sample data" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "prepare_md" + }, + "source": [ + "### Create a BigQuery dataset\n", + "\n", + "\n", + "Before you load data into new tables, you need a place to store them. Running the following query will create a BigQuery dataset called **`cymbal_pets`**." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "create_dataset_code" + }, + "outputs": [], + "source": [ + "%%bigquery --project {PROJECT_ID}\n", + "\n", + "CREATE SCHEMA `cymbal_pets` OPTIONS (location = 'us');" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "CFncMrqEGMAO" + }, + "source": [ + "### Create the `products` table\n", + "\n", + "This query uses the `LOAD DATA` command to create a new table called `products` inside your `cymbal_pets` dataset. It simultaneously loads the table with product data from a Parquet file in Google Cloud Storage (GCS)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "3N94VjIMeX-F" + }, + "outputs": [], + "source": [ + "%%bigquery --project {PROJECT_ID}\n", + "\n", + "LOAD DATA OVERWRITE cymbal_pets.products\n", + "FROM FILES (\n", + " uris = ['gs://sample-data-and-media/cymbal-pets/tables/products'],\n", + " format = 'PARQUET'\n", + ");" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "cgSJkD3Q93l7" + }, + "source": [ + "Run a quick `SELECT` query to view a couple of rows and confirm that your `products` table was created and loaded correctly." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "tIKDAa-a0K13" + }, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "3cd8d1266a12498881b5d5559e130101", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Query is running: 0%| |" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "f5d93345481541379624cafd4cee6891", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Downloading: 0%| |" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/vnd.google.colaboratory.intrinsic+json": { + "repr_error": "Out of range float values are not JSON compliant: nan", + "type": "dataframe" + }, + "text/html": [ + "\n", + "
    \n", + "
    \n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
    product_idproduct_namebrandcategorysubcategoryanimal_typesearch_keywordspricedescriptioninventory_levelsupplier_idaverage_ratinguri
    03AquaClear 50 Gallon AquariumAquaClearAccessoriesNoneNone{'list': []}199.99This stunning aquarium provides a spacious and...151034.0gs://sample-data-and-media/cymbal-pets/images/...
    19AquaClear 20 Gallon AquariumAquaClearAccessoriesNoneNone{'list': []}129.99This spacious glass enclosure provides a comfo...251032.5gs://sample-data-and-media/cymbal-pets/images/...
    \n", + "
    \n", + "
    \n", + "\n", + "
    \n", + " \n", + "\n", + " \n", + "\n", + " \n", + "
    \n", + "\n", + "\n", + "
    \n", + " \n", + "\n", + "\n", + "\n", + " \n", + "
    \n", + "\n", + "
    \n", + "
    \n" + ], + "text/plain": [ + " product_id product_name brand category \\\n", + "0 3 AquaClear 50 Gallon Aquarium AquaClear Accessories \n", + "1 9 AquaClear 20 Gallon Aquarium AquaClear Accessories \n", + "\n", + " subcategory animal_type search_keywords price \\\n", + "0 None None {'list': []} 199.99 \n", + "1 None None {'list': []} 129.99 \n", + "\n", + " description inventory_level \\\n", + "0 This stunning aquarium provides a spacious and... 15 \n", + "1 This spacious glass enclosure provides a comfo... 25 \n", + "\n", + " supplier_id average_rating \\\n", + "0 103 4.0 \n", + "1 103 2.5 \n", + "\n", + " uri \n", + "0 gs://sample-data-and-media/cymbal-pets/images/... \n", + "1 gs://sample-data-and-media/cymbal-pets/images/... " + ] + }, + "execution_count": 11, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "%%bigquery --project {PROJECT_ID}\n", + "\n", + "SELECT *\n", + "FROM cymbal_pets.products\n", + "LIMIT 2" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "039VFiVBwzsF" + }, + "source": [ + "---\n", + "\n", + "\n", + "## Perform text-to-text semantic search\n", + "\n", + "With the product data prepared, it's time to perform the first [semantic search](https://cloud.google.com/discover/what-is-semantic-search). You want to input a text query, like \"kitten toy\" and find the most similar products. To do this, you first need to convert the product names and descriptions into [vector embeddings](https://cloud.google.com/vertex-ai/generative-ai/docs/embeddings) -- numerical representations that capture their semantic meaning. Once the embeddings are generated, you can run a vector search.\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "Y8yup-saGjtc" + }, + "source": [ + "### Create remote model for text embedding\n", + "\n", + "Begin by creating a remote model in BigQuery named `text_embedding_model`. This command doesn't create a new model; instead, it creates a pointer to an existing, pre-trained text embedding model hosted on Vertex AI ([`gemini-embedding-001`](https://ai.google.dev/gemini-api/docs/embeddings#model-versions)). This allows you to leverage Vertex AI models directly from your SQL queries." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "qHc6MHazGqR9" + }, + "outputs": [], + "source": [ + "%%bigquery --project {PROJECT_ID}\n", + "\n", + "CREATE OR REPLACE MODEL `cymbal_pets.text_embedding_model`\n", + " REMOTE WITH CONNECTION `us.cymbal_conn`\n", + " OPTIONS (ENDPOINT = 'gemini-embedding-001');" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "AgzCgXcUwcm4" + }, + "source": [ + "### Generate text embeddings for product descriptions" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "dtgmxoax-pUT" + }, + "source": [ + " To store embeddings, alter the `products` table to add a new column. Since embeddings are numerical representations of text (an array of floating point numbers), the column type will be `ARRAY`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "614L4qqk4ePX" + }, + "outputs": [], + "source": [ + "%%bigquery --project {PROJECT_ID}\n", + "\n", + "ALTER TABLE cymbal_pets.products\n", + "ADD COLUMN text_embedding ARRAY;" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "kOo1l6rdOYk8" + }, + "source": [ + "Now, generate embeddings with the [`ML.GENERATE_EMBEDDING`](https://cloud.google.com/bigquery/docs/reference/standard-sql/bigqueryml-syntax-generate-embedding) function. The following query passes the `product_name` and `description` to the `text_embedding_model` for each item. An `UPDATE` statement saves the resulting embeddings in a new column." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "pLKxq9UrwreS" + }, + "outputs": [], + "source": [ + "%%bigquery --project {PROJECT_ID}\n", + "\n", + "UPDATE `cymbal_pets.products` AS t\n", + "SET t.text_embedding = s.ml_generate_embedding_result\n", + "FROM (\n", + " SELECT\n", + " product_id,\n", + " ml_generate_embedding_result\n", + " FROM\n", + " ML.GENERATE_EMBEDDING(\n", + " MODEL `cymbal_pets.text_embedding_model`,\n", + " (\n", + " SELECT\n", + " product_id,\n", + " CONCAT(product_name, ' ', description) AS content\n", + " FROM `cymbal_pets.products`\n", + " ),\n", + " STRUCT(TRUE AS flatten_json_output)\n", + " )\n", + ") AS s\n", + "WHERE t.product_id = s.product_id;" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "FylUH24h_kXN" + }, + "source": [ + "You should now see the `text_embedding` column populated with a long array of numbers. Those are vector embeddings that numerically represent each products' name and description!" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "wdwWBdL-5yiP" + }, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "748969aad71e47e7aecf35ef0b6fb475", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Query is running: 0%| |" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "407d8a55b98a49218fa7ffc88c4e746e", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Downloading: 0%| |" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/vnd.google.colaboratory.intrinsic+json": { + "summary": "{\n \"name\": \"get_ipython()\",\n \"rows\": 2,\n \"fields\": [\n {\n \"column\": \"product_id\",\n \"properties\": {\n \"dtype\": \"Int64\",\n \"num_unique_values\": 2,\n \"samples\": [\n 171,\n 57\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"product_name\",\n \"properties\": {\n \"dtype\": \"string\",\n \"num_unique_values\": 2,\n \"samples\": [\n \"AquaClear Aquarium Fish Net\",\n \"AquaClear Aquarium Filter Cartridge\"\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"description\",\n \"properties\": {\n \"dtype\": \"string\",\n \"num_unique_values\": 2,\n \"samples\": [\n \"This convenient net makes it easy to safely transfer fish or other aquatic pets. The soft mesh is gentle on delicate scales and fins, while the sturdy handle provides a comfortable grip. Ideal for aquarium maintenance and cleaning.\",\n \"This multi-stage filter provides superior water quality for your aquarium, ensuring a healthy and thriving environment for your fish. The layered design effectively removes debris, impurities, and unwanted particles, promoting clear and clean water. Enjoy a pristine aquarium with this high-performance filtration system.\"\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"text_embedding\",\n \"properties\": {\n \"dtype\": \"object\",\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n }\n ]\n}", + "type": "dataframe" + }, + "text/html": [ + "\n", + "
    \n", + "
    \n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
    product_idproduct_namedescriptiontext_embedding
    057AquaClear Aquarium Filter CartridgeThis multi-stage filter provides superior wate...[0.011718875728547573, 0.008779315277934074, 0...
    1171AquaClear Aquarium Fish NetThis convenient net makes it easy to safely tr...[-0.013994182460010052, -0.009928294457495213,...
    \n", + "
    \n", + "
    \n", + "\n", + "
    \n", + " \n", + "\n", + " \n", + "\n", + " \n", + "
    \n", + "\n", + "\n", + "
    \n", + " \n", + "\n", + "\n", + "\n", + " \n", + "
    \n", + "\n", + "
    \n", + "
    \n" + ], + "text/plain": [ + " product_id product_name \\\n", + "0 57 AquaClear Aquarium Filter Cartridge \n", + "1 171 AquaClear Aquarium Fish Net \n", + "\n", + " description \\\n", + "0 This multi-stage filter provides superior wate... \n", + "1 This convenient net makes it easy to safely tr... \n", + "\n", + " text_embedding \n", + "0 [0.011718875728547573, 0.008779315277934074, 0... \n", + "1 [-0.013994182460010052, -0.009928294457495213,... " + ] + }, + "execution_count": 15, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "%%bigquery --project {PROJECT_ID}\n", + "\n", + "SELECT product_id, product_name, description, text_embedding\n", + "FROM cymbal_pets.products\n", + "LIMIT 2" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "EBBg2qw0_7Q-" + }, + "source": [ + "### Use `VECTOR SEARCH` to perform a text-to-text search\n", + "\n", + "Now, use [`VECTOR_SEARCH`](https://cloud.google.com/bigquery/docs/reference/standard-sql/search_functions#vector_search) to perform a text-to-text search. This function works in two main steps:\n", + "* A subquery generates an embedding for a search query (e.g. \"kitten toy\")\n", + "* `VECTOR_SEARCH` then compares this new embedding against all existing product embeddings and returns the `top_k` (3 in this case) most similar results" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "7bPZj0FM5heu" + }, + "outputs": [], + "source": [ + "%%bigquery text_matches --project {PROJECT_ID}\n", + "\n", + "SELECT base.product_id, base.product_name, base.brand, base.category, base.description, base.average_rating, base.uri, distance\n", + " FROM\n", + " VECTOR_SEARCH(\n", + " TABLE `cymbal_pets.products`,\n", + " 'text_embedding',\n", + " (\n", + " -- GENERATE AN EMBEDDING AS A SUBQUERY\n", + " SELECT\n", + " ml_generate_embedding_result,\n", + " content AS query\n", + " FROM\n", + " ML.GENERATE_EMBEDDING(\n", + " MODEL `cymbal_pets.text_embedding_model`,\n", + " ( SELECT \"kitten toy\" AS content)\n", + " )\n", + " ),\n", + " top_k => 3)\n", + "ORDER BY distance ASC;" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "xzaZ3Gs2ASLu" + }, + "source": [ + "The results of the vector search are now stored in a pandas DataFrame called `text_matches` (a parameter added to the [`%%bigquery` magic](https://cloud.google.com/python/docs/reference/bigquery/latest/magics) utility in the prior cell).\n", + "\n", + "Let's display the top rows." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "a1SlsPNz79nk" + }, + "outputs": [ + { + "data": { + "application/vnd.google.colaboratory.intrinsic+json": { + "summary": "{\n \"name\": \"text_matches\",\n \"rows\": 3,\n \"fields\": [\n {\n \"column\": \"product_id\",\n \"properties\": {\n \"dtype\": \"Int64\",\n \"num_unique_values\": 3,\n \"samples\": [\n 27,\n 33,\n 157\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"product_name\",\n \"properties\": {\n \"dtype\": \"string\",\n \"num_unique_values\": 3,\n \"samples\": [\n \"Purrfect Perch Cat Toy\",\n \"Cozy Naps Catnip Toy\",\n \"Cozy Naps Catnip Toy Ball\"\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"brand\",\n \"properties\": {\n \"dtype\": \"string\",\n \"num_unique_values\": 2,\n \"samples\": [\n \"Cozy Naps\",\n \"Purrfect Perch\"\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"category\",\n \"properties\": {\n \"dtype\": \"category\",\n \"num_unique_values\": 1,\n \"samples\": [\n \"Toys\"\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"description\",\n \"properties\": {\n \"dtype\": \"string\",\n \"num_unique_values\": 3,\n \"samples\": [\n \"This cat tree provides multiple levels for climbing, scratching, and resting, allowing your cat to enjoy playtime and relaxation. The soft, plush surfaces offer comfortable spots to nap, while the sisal-covered post satisfies natural scratching instincts.\"\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"average_rating\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": 0.43588989435406733,\n \"min\": 4.0,\n \"max\": 4.8,\n \"num_unique_values\": 3,\n \"samples\": [\n 4.0\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"uri\",\n \"properties\": {\n \"dtype\": \"string\",\n \"num_unique_values\": 3,\n \"samples\": [\n \"gs://sample-data-and-media/cymbal-pets/images/purrfect-perch-cat-toy.png\"\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"distance\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": 0.006529728333769095,\n \"min\": 0.7847798709976486,\n \"max\": 0.797839228798168,\n \"num_unique_values\": 3,\n \"samples\": [\n 0.7847798709976486\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n }\n ]\n}", + "type": "dataframe", + "variable_name": "text_matches" + }, + "text/html": [ + "\n", + "
    \n", + "
    \n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
    product_idproduct_namebrandcategorydescriptionaverage_ratinguridistance
    027Purrfect Perch Cat ToyPurrfect PerchToysThis cat tree provides multiple levels for cli...4.0gs://sample-data-and-media/cymbal-pets/images/...0.784780
    133Cozy Naps Catnip ToyCozy NapsToysThis soft, plush cushion provides a cozy and c...4.7gs://sample-data-and-media/cymbal-pets/images/...0.791354
    2157Cozy Naps Catnip Toy BallCozy NapsToysThis yarn is perfect for creating cozy and com...4.8gs://sample-data-and-media/cymbal-pets/images/...0.797839
    \n", + "
    \n", + "
    \n", + "\n", + "
    \n", + " \n", + "\n", + " \n", + "\n", + " \n", + "
    \n", + "\n", + "\n", + "
    \n", + " \n", + "\n", + "\n", + "\n", + " \n", + "
    \n", + "\n", + "
    \n", + "
    \n" + ], + "text/plain": [ + " product_id product_name brand category \\\n", + "0 27 Purrfect Perch Cat Toy Purrfect Perch Toys \n", + "1 33 Cozy Naps Catnip Toy Cozy Naps Toys \n", + "2 157 Cozy Naps Catnip Toy Ball Cozy Naps Toys \n", + "\n", + " description average_rating \\\n", + "0 This cat tree provides multiple levels for cli... 4.0 \n", + "1 This soft, plush cushion provides a cozy and c... 4.7 \n", + "2 This yarn is perfect for creating cozy and com... 4.8 \n", + "\n", + " uri distance \n", + "0 gs://sample-data-and-media/cymbal-pets/images/... 0.784780 \n", + "1 gs://sample-data-and-media/cymbal-pets/images/... 0.791354 \n", + "2 gs://sample-data-and-media/cymbal-pets/images/... 0.797839 " + ] + }, + "execution_count": 17, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "text_matches.head()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "J-Ev_nYsAjVI" + }, + "source": [ + "### View search results\n", + "Now you can use the helper function to display the images of the top 3 products the vector search returned. As you can see, the results are semantically related to \"kitten toy\" even if the exact words don't appear in the product names or descriptions." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "VlCaWeq78Aaz" + }, + "outputs": [ + { + "data": { + "image/png": "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+dxzz+WzzjqLL7zwQr7uuuv4LW95Cw+HQ77mmmuYmfnyyy/n4XDIP/MzP8NXXHEFf/WrX+Xf//3f59tvv50PHTrET3ziE/mVr3wlHzhwgA8cOMBlWSbbev7553O/3+fHPvax/LnPfY6/+MUv8uMf/3g+66yzfJoLL7yQt2/fzhdccAFff/31/M///M+8b98+fuMb3+jTAOA9e/bwBz7wAb7++uv5pptu4r/7u7/jPM/513/91/nrX/86X3HFFfxbv/VbPs/evXt59+7d/K53vYuvvfZa/u3f/m3Osoyvvvrqzr751re+xbt37+bnP//5fNlll/H+/fv5Ax/4gM/z6U9/mj/84Q/zVVddxV//+tf5Fa94BR9//PF85MgRZma+7bbbGACff/75fODAAb7tttuS9aysrPCLXvQi/qEf+iEvw8lkwqurq3ziiSfy85//fL7yyiv505/+NJ9yyil89tln+7wPetCD+Hd/93f99XQ65WOPPZY/8IEPRPUA4GOOOYbf//738/79+/lXf/VXOc9z/vrXv6506cEPfjD/3d/9He/fv59f8IIX8N69e7koCmZm/n//7/9xlmX8lre8hffv38/vete7ePfu3bxjx45OOc5pTvcm/dIv/RLv2rWLL7jgAr7uuuv4oosu4ve///1bshdnXwcOHODrrruOTzvtNH7Zy17GzMy/+Zu/yQ95yENUXT//8z/P3/d939fJi8O0pz/96XzZZZfxl770JT7zzDP5J3/yJ32azXCCeXPbfMtb3sL3v//9+cILL+RvfOMbfNFFF/FHPvKRTr5WVlb41FNP5Sc/+cl80UUX8bXXXst//ud/zp/73OeYmfmKK67gP/zDP+Qrr7ySr7nmGv7VX/1VHo1GfNNNNzEz85133sknn3wyv/nNb/by2mp7zz77bH7Oc57jrzfzcSnqws5PfOIT3O/3+V3vehfv37+f3/a2t3Ge5/wv//IvzMx88cUXc57n/O1vf9uX9YlPfIKXlpZ4ZWUl2Xez/NF5553Hy8vLXqcuvPBCPuGEE/gNb3iDT3Puuefy3r17+VOf+hRfeeWV/LznPY+3bdvGr33tazvbN6c5ddEc3+b4dm/gmyMX15188sn8F3/xF/z1r3+dzz33XN62bRvfcccdzNzEjb/+67/Ol112Gd9www2+DX/+538+s92zMO68887jpaUlftrTnsaXX345f/azn+XTTjstkuX27dv5Va96FV911VX8t3/7t5Hsnv70p/MjH/lIvuSSS/hLX/oSP+UpT+GFhQV+xzve0Vn3/59oPvC+G/SUpzyFzzzzTK7r2t/75V/+ZT7zzDOZeesD79/7vd9Tac4++2w+/vjjeTKZqPuvfe1r+SlPeYq/vummmzjPc77llltUuh/4gR/g17/+9czM/JKXvISf9KQnzWzDVoKM888/nwHwJZdc4u9dddVVDIC/8IUv+HrlgJmZ+cMf/jCfeOKJ/hoA/8Iv/IJK88QnPpFf+tKXdta9d+9e/u///b/767quec+ePfye97ynM8/rX/96PuWUU3g6nW7aNmbmqqp427Zt/Ld/+7eKV9t/KbKgxsz8vve9j3ft2sWrq6v+3t///d9zlmV86623MjPz7/zO73hdYWb++Mc/zsvLyyqP5OVVr3qVuve93/u9/DM/8zPMHHTpj/7oj/zzr33tawyAr7rqKmZmfvGLX8zPfvazVRkvfelL5wPvOf270JEjR3g4HPL73//+6NlW7MVRXdf8vOc9jx/zmMfw+vo6MzPfcsstnOe5xyI3iXXBBRd08uMw7brrrvP33vWud/Hxxx/fmacLJ2bZ5mte8xp+2tOepvzELHrve9/L27Zt85OrW6GHPvSh/Pu///v+eu/evVFws5X2pgK0WT6ui1LYedZZZ/ErX/lKde+FL3wh//AP/7C/fshDHsK/8zu/469/9Ed/lM8555yo/K34o/POO48XFxfVIOIXf/EX+Xu/93uZudHHfr/PH/vYx/zzQ4cO8eLi4nzgPaejpjm+zfHt3sI3Ry6u+9//+3/7e0VR8Mknn6zKsfRzP/dz/OM//uP++u4MvPM8529961v+3ic/+UnOssxPdJx99tm8d+9etVj3whe+kF/84hczc8Djyy67zD+/9tprGcB84N3SfKv53aQnPOEJfqsLADzxiU/Etddei6qqtlzGYx/72Ojewx72MAwGg5n5rrzySlRVhQc96EFYXl72/332s5/1W6qvuOIK/MAP/MCWeQGAhz70ob6sZz3rWf5+r9fD4x73OH/94Ac/GDt37sRVV10FAPjyl7+MN7/5zYqXV77ylThw4ADW19c727sVHh/+8If730SEE044AbfddhsA4FnPepav76EPfagv88lPfnLn++Pf+c538MpXvhKnn346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YFx66aWqno997GP4wAc+gGuuuQbnnXceLr30Ug++W6HXvOY1+Id/+Ae8/e1vx7XXXov3vve9+OQnP6m2Wc1pTvcVjUYj/PIv/zJ+6Zd+CR/60Idw/fXX45JLLsEf//Efb2ovt956K573vOfhJ37iJ/DMZz4Tt956K2699Vbcfvvtvvw8z3HOOefg9a9/PU4//fRoe+Tdoa3gBDDbNt/+9rfjox/9KK6++mpcc801+NjHPoYTTjih82sCL3nJS3DCCSfguc99Li6++GLccMMN+PjHP+6/UHD66afjE5/4BK644gp8+ctfxk/+5E9GuL1v3z5ceOGFuOWWW5KB9NHQLB/XRSns/MVf/EVccMEFeM973oNrr70Wb3/72/GJT3wiWuU599xz8cEPfhBXXXWVD2L/4A/+INrCu5k/2oy2bduGs88+G7/4i7+Iz3zmM/ja176GV7ziFciybI6JczpqmuPbHN/uLr5tRu9617vwl3/5l7j66qvxcz/3czh48KDfon766afji1/8Iv7pn/4J11xzDX7t137tqBdUXv/616uJFQBeX7/85S/joosuws///M/jRS96EU444YQtlfngBz8YT3/60/HTP/3TuPTSS3H55Zfjp3/6p7GwsDDHV0ff1TfM/5PSU57yFP7Zn/1ZftWrXsXbt2/nXbt28Rve8AZ/UMMtt9zCP/iDP8hLS0t8+umn8z/8wz8kD1e7/PLLVbmpw7qY48PVmMOJhvv27eN+v88nnngiP+95z+OvfOUrPs2//uu/8llnncXD4ZB37tzJz3zmM/ngwYPMzLx//35+whOewAsLCwyAb7zxxmRbzz//fN6xYwd//OMf51NPPZWHwyE//elP9ydNOvrHf/xHPuuss3hhYYG3b9/Oj3/849Uph+g4sOzjH/84P/KRj+TBYMDHHnssP//5z/fPUgdpPOIRj+DzzjsvyaujL3/5y/yDP/iDvLi4yNu2beMnP/nJfP311zMz87/927/xYx/7WB6NRnz66afzxz72saiev/mbv+HTTjuNe70e7927t7Oe2267jZ/xjGfw8vIyA+DPfOYzzMz8la98hb//+7+fR6MR7969m1/5ylcmT6/8tV/7NXXapdMLV46T27ve9S5+xjOewcPhkPft26dOrUzp0sGDB6Ny3ve+9/H97nc/XlhY4Oc+97n8G7/xG3zCCSfMlOOc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\n", + "text/plain": [ + "
    " + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "display_product_images(text_matches)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "9ytmT_Q6-J2I" + }, + "source": [ + "---\n", + "\n", + "\n", + "## Perform text-to-image (multimodal) semantic search\n", + "\n", + "The previous example performed a text-to-text search. Now we can look at multimodal search. This allows you to use a text query to find products based on their visual characteristics (not using any written descriptions). Note that multimodal search also extends to audio, video, and more." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "9Y31x-x_HC92" + }, + "source": [ + "### Create remote model for multimodal embedding\n", + "\n", + "To work with images, you'll need a different embedding model. You'll now create a remote model pointing to [`multimodalembedding@001`](https://cloud.google.com/vertex-ai/generative-ai/docs/embeddings/get-multimodal-embeddings#supported-models). Unlike the previous text-only model, this one is **multimodal**, meaning it can generate a single vector embedding from content that's text, an image, a video, audio, or more." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "HpJwFmAOGrXt" + }, + "outputs": [], + "source": [ + "%%bigquery --project {PROJECT_ID}\n", + "\n", + "CREATE OR REPLACE MODEL `cymbal_pets.mm_embedding_model`\n", + " REMOTE WITH CONNECTION `us.cymbal_conn`\n", + " OPTIONS (ENDPOINT = 'multimodalembedding@001');" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "86UojNj1HLGB" + }, + "source": [ + "### Generate multimodal embeddings for product images" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "C603D8_wBLpv" + }, + "source": [ + "You need a new column in your table to store the embeddings generated from the product images." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "sLliKpuc-Xra" + }, + "outputs": [], + "source": [ + "%%bigquery --project {PROJECT_ID}\n", + "\n", + "ALTER TABLE cymbal_pets.products\n", + "ADD COLUMN mm_embedding ARRAY;" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "uqON6RSYBW-y" + }, + "source": [ + "This step generates embeddings directly from product images. The process again uses [`ML.GENERATE_EMBEDDING`](https://cloud.google.com/bigquery/docs/reference/standard-sql/bigqueryml-syntax-generate-embedding), but with the `mm_embedding_model`.\n", + "\n", + "The key difference is how you provide the image to the model. Instead of a text string, the model requires a temporary, secure URL for each image. Because we haven't stored [`ObjectRef`](https://cloud.google.com/bigquery/docs/reference/standard-sql/objectref_functions)s in our table, we can generate them on the fly by chaining three functions together:\n", + "\n", + "* [`OBJ.MAKE_REF`](https://cloud.google.com/bigquery/docs/reference/standard-sql/objectref_functions#objmake_ref): First, it converts the GCS path from the `uri` column into a structured `ObjectRef`\n", + "* [`OBJ.FETCH_METADATA`](https://cloud.google.com/bigquery/docs/reference/standard-sql/objectref_functions#objfetch_metadata): Next, it populates the `ObjectRef` with file metadata from Cloud Storage\n", + "* [`OBJ.GET_ACCESS_URL`](https://cloud.google.com/bigquery/docs/reference/standard-sql/objectref_functions#objget_access_url): Finally, it generates a signed URL from the metadata-rich `ObjectRef` that the model can access.\n", + "\n", + "The resulting vector embedding for each image is then saved to the `mm_embedding` column with an `UPDATE` statement." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "CLVJ0o9s-fEJ" + }, + "outputs": [], + "source": [ + "%%bigquery --project {PROJECT_ID}\n", + "\n", + "UPDATE `cymbal_pets.products` AS t\n", + "SET\n", + " t.mm_embedding = s.ml_generate_embedding_result\n", + "FROM (\n", + " SELECT\n", + " product_id,\n", + " ml_generate_embedding_result\n", + " FROM\n", + " ML.GENERATE_EMBEDDING(\n", + " MODEL `cymbal_pets.mm_embedding_model`,\n", + " (\n", + " SELECT\n", + " product_id,\n", + " OBJ.GET_ACCESS_URL(OBJ.FETCH_METADATA(OBJ.MAKE_REF(uri, 'us.cymbal_conn')), 'r') AS content\n", + " FROM\n", + " `cymbal_pets.products`\n", + " ),\n", + " STRUCT(TRUE AS flatten_json_output)\n", + " )\n", + ") AS s\n", + "WHERE t.product_id = s.product_id;" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "YhJ9OzDmCANS" + }, + "source": [ + " Inspect the table one more time to see your new `mm_embedding` column filled with multimodal embeddings, derived from product images." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "7j2I3Hk3ANF6" + }, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "479e8ba9507944f1b0e2c5ab13560d85", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Query is running: 0%| |" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "fdbf50cbe8274a29bde7c825a8fb107c", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Downloading: 0%| |" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/vnd.google.colaboratory.intrinsic+json": { + "summary": "{\n \"name\": \"get_ipython()\",\n \"rows\": 2,\n \"fields\": [\n {\n \"column\": \"product_id\",\n \"properties\": {\n \"dtype\": \"Int64\",\n \"num_unique_values\": 2,\n \"samples\": [\n 9,\n 3\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"product_name\",\n \"properties\": {\n \"dtype\": \"string\",\n \"num_unique_values\": 2,\n \"samples\": [\n \"AquaClear 20 Gallon Aquarium\",\n \"AquaClear 50 Gallon Aquarium\"\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"description\",\n \"properties\": {\n \"dtype\": \"string\",\n \"num_unique_values\": 2,\n \"samples\": [\n \"This spacious glass enclosure provides a comfortable and transparent habitat for your aquatic pets, allowing for easy observation of their behavior and interaction with their environment. The sleek design complements any home decor while offering ample room for swimming, exploration, and creating a thriving underwater ecosystem.\",\n \"This stunning aquarium provides a spacious and elegant home for your aquatic pets, allowing them to thrive in a crystal-clear environment. The sleek design complements any home decor, while the large viewing area offers an unobstructed view of your fish and other aquatic life. Its durable construction ensures a safe and long-lasting habitat for your underwater companions.\"\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"mm_embedding\",\n \"properties\": {\n \"dtype\": \"object\",\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n }\n ]\n}", + "type": "dataframe" + }, + "text/html": [ + "\n", + "
    \n", + "
    \n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
    product_idproduct_namedescriptionmm_embedding
    03AquaClear 50 Gallon AquariumThis stunning aquarium provides a spacious and...[0.0190890729, 0.0193080865, -0.00221736147, -...
    19AquaClear 20 Gallon AquariumThis spacious glass enclosure provides a comfo...[0.0341621898, 0.0419731215, -0.0227391645, 0....
    \n", + "
    \n", + "
    \n", + "\n", + "
    \n", + " \n", + "\n", + " \n", + "\n", + " \n", + "
    \n", + "\n", + "\n", + "
    \n", + " \n", + "\n", + "\n", + "\n", + " \n", + "
    \n", + "\n", + "
    \n", + "
    \n" + ], + "text/plain": [ + " product_id product_name \\\n", + "0 3 AquaClear 50 Gallon Aquarium \n", + "1 9 AquaClear 20 Gallon Aquarium \n", + "\n", + " description \\\n", + "0 This stunning aquarium provides a spacious and... \n", + "1 This spacious glass enclosure provides a comfo... \n", + "\n", + " mm_embedding \n", + "0 [0.0190890729, 0.0193080865, -0.00221736147, -... \n", + "1 [0.0341621898, 0.0419731215, -0.0227391645, 0.... " + ] + }, + "execution_count": 22, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "%%bigquery --project {PROJECT_ID}\n", + "\n", + "SELECT product_id, product_name, description, mm_embedding\n", + "FROM cymbal_pets.products\n", + "LIMIT 2" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "Yoy6iKuXCKI4" + }, + "source": [ + "###Use `VECTOR SEARCH` to perform a text-to-image search\n", + "\n", + "The power of a multimodal model is that it places both text and images into the same \"embedding space.\" This allows you to use an embedding generated from a text string (like \"kitten toy\") and directly compare it against embeddings generated from images.\n", + "\n", + "The process for this text-to-image search mirrors the previous text-to-text search, with two key differences:\n", + "* A text query (\"kitten toy\") is embedded using the multimodal `mm_embedding_model`\n", + "* The search is performed against the image embedding column (`embedding_mm_image`) to find the most visually similar items" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "PiC0CLa2AAlT" + }, + "outputs": [], + "source": [ + "%%bigquery multimodal_matches --project {PROJECT_ID}\n", + "\n", + "SELECT base.product_id, base.product_name, base.brand, base.category, base.description, base.average_rating, base.uri, distance\n", + " FROM\n", + " VECTOR_SEARCH(\n", + " TABLE`cymbal_pets.products`,\n", + " 'mm_embedding',\n", + " (\n", + " SELECT\n", + " ml_generate_embedding_result,\n", + " content AS query\n", + " FROM\n", + " ML.GENERATE_EMBEDDING(\n", + " MODEL `cymbal_pets.mm_embedding_model`,\n", + " (SELECT \"kitten toy\" AS content)\n", + " )\n", + " ),\n", + " 'ml_generate_embedding_result',\n", + " top_k => 3)\n", + "ORDER BY distance ASC;" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "ma7A9jF0HbUg" + }, + "source": [ + "### View search results" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "D1SRowjwCsda" + }, + "source": [ + "Finally, let's visualize the results of the text-to-image search. You should see images of products that are conceptually related to \"kitten toy,\" demonstrating the power of multimodal embeddings to find relevant items based on visual similarity to a text description." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "BNOV5oYY3I74" + }, + "outputs": [ + { + "data": { + "image/png": "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+tNp3333pTe96U109dVX0wUXXEBvfOMb6YYbbqCDDz6YfvVXf5W++MUv0kc+8hG6/e1vT6effnp69pvf/Gb67+qrr6YjjjiCHvvYxxIR0Z//+Z/THe94x6KtJz/5yXS/+91vIS/nnHMOVVVFJ510El188cX0uc99jo455hh6zGMek8p85CMfobe+9a10+eWX05e//GV6whOeQAceeCD96Ec/SmUA0P77709vfOMb6YorrqA/+ZM/IWMMffnLXyYiohe/+MV06KGH0sc//nH66le/ShdccAG9/e1vX8jX9ddfT4cffjjd9773pQsuuICuuuoqeuc730mf/OQniYjo0ksvpde97nX0xS9+ka688kr6kz/5E5pOp/S1r32NiIi++93v0k//9E/T8573vDReY3TTTTfRH/7hH9Kd7nSnVO6mm24i7z3d+c53pl/4hV+gz372s3TRRRfRXe96VzrhhBPSsyeffDL9zu/8TlHfcccdR89+9rNH23rf+95HKysrdP3116dr733ve2llZSWN5T/+4z/Su9/9brrqqqvo85//PD30oQ+lY489lrz3RER07bXXEgA67LDD6N3vfjf953/+J33jG9+gc845h/bee+9U78c//nHavn07velNb6JrrrmGzjvvPDrssMPoOc95DhERffvb3yYAdM4559A3v/lN+va3v71TfSIiOv3002nbtm106qmn0pe+9CV63/veRwcccAA961nPSmXW69d683322WfT8ccfT0REl112GR100EH0x3/8xwt5Ilp/TcaxPProo+l973sfXXHFFfSIRzyCduzYQW3bEhHRJz7xCdJa04tf/GK64oor6NWvfjXtt99+xVgv6ceDlvvqj/++SkR00kkn0UMf+lC6+OKL6corr6Q//MM/pP3335+++93vboiHiy++mIwx9Pa3v52++tWv0iWXXEKvfOUrU9vPf/7z6eijj6YPfvCDdM0119A555xDk8mEzj//fCIi+tjHPkYA6LjjjqPzzjuPrr766tR2Tne5y13oxS9+ceJpv/32o7qu0/59xhln0GmnnZbK/9mf/RldeOGFdO2119K//uu/0oEHHkh/8Rd/ke6fffbZtG3btrQOP/7xj9NBBx1U7JO//du/Tbe73e3owx/+MH3hC1+gX/7lX6a99tqLnvKUpyyc07PPPpu2bt1KD3jAA+jzn/88/fu//zsdccQRxVo7/fTTafv27XTmmWfS5ZdfTu9973tpy5Yt9IY3vCGVOemkk+jOd74zXXTRRfS5z32OTjjhBFpZWaGXv/zlC9te0o8vLffDW24/3Eh/Tz/9dHrYwx6Wfp9wwgm0bds2espTnkJf+cpX6O/+7u8G72ifFslx73nPe6iqKnr1q19NV1xxBb30pS8lYwx99KMfJSKiCy+8kIwx9I1vfCPV9Z73vIe2bt1ayKc55fLSBz/4Qfryl79M97znPemud70rnXjiifSJT3yCLrnkEjriiCPozDPPTM+97GUvo+3bt9M73vEO+spXvkJPf/rTqaoquvLKK4mo24+///3vp2c+//nPEwC69tpriYjoIQ95CJ188sn0hS98ga655hp673vfS//+7/9ORETf//736YADDqBnPvOZdPnll9Mll1xCJ598Mt3//vdfOG5xfu52t7vRJz/5SfrsZz9L97jHPeje9753KrOR/fqMM86gHTt20Ic//GH64he/SKeccsq6+/WtnW6VwPtHP/oRTSYTeuMb3zi494Y3vIH23XdfuuGGG9K197///aS1pm9961tF2RACnXLKKXTXu941CTT//d//TcYY+vSnP01ERE3T0G1ucxt605vetJCfc845hwDQ1Vdfna69+tWvpgMPPHDhM9572muvvei9731vugageNmIiH7+53+efvu3f5uIiJ70pCfRAx7wAAohLKw3p9e//vW01157jQpEi+hOd7oT/dVf/VX6vWPHjg0JDTmoi3TeeeeRMYauu+66dO0//uM/CAB95jOfISKid77znbTvvvvSbDYjIqLPfe5zpJRKm0Wf2ral29zmNvSWt7wlXXv0ox9Np5566kLe/vd//5cA0Be/+EUi6ja/V7ziFUW5PvD+xV/8RXrBC15QlHnrW99KBx98cPoNgP7pn/6pKLPZPhHxB2S//fajG2+8MV177WtfS9u2bUvAer1+rTffcY4uvPBC2nfffeklL3nJQn7y/q21JuNY/vVf/3W6H+f48ssvJyKiU089lR7ykIcUdZx22mlL4P1jRst99daxr15wwQW0ffv2tL9EusMd7kCvf/3rN8TDu9/9btq+fXshkEeazWa0ZcuWJDhHesITnkCPfvSjiagT9P75n/95Tf7/4A/+IL37r3jFK+jUU0+l448/ns4991wiIjriiCPWFIpf/OIX013vetf0++yzz6YtW7YUfD/taU+jn//5nyciBgF1XdM//MM/pPvf/e53aWVlZV3gbYyh//qv/0rXzj33XNJaJyBw+umn044dO8g5l8o88pGPTN+eyy+/nADQxRdfnO5fddVVBGAJvG+FtNwPb9n9cCP9HQPexxxzTMH7M57xDDrmmGPW5GdMjrv3ve9NT3ziE4trj3zkI+nBD35w+n3HO96xOBh86EMfSo9//OMXtjMmL73jHe8gAPSRj3wkXXvhC19IRx11VPp9yCGH0J//+Z8Xdd397ndPCp6NAO9jjz02KY369Gd/9mf0f/7P/ymuff3rXycAdMUVV4w+E+fnoosuStfiHhjX9Xr79Y9+9COqqore9a53pfs/+MEPaMuWLT/RwPtWaWp++eWXYz6f4xd/8RdH7x1//PHYunVrunaf+9wHIQRcccUVRdlnPetZ+NSnPoV/+Zd/Sb69hxxyCB7ykIfgb//2bwEA733vezGfz/HIRz4SAHCnO90J27ZtS+YgkbZs2YI73OEO6ffBBx+Mb3/72+n3//zP/+CJT3wijjzySOy9997Yvn07brjhBlx33XUFT/e6170Gvy+//HIAbFZz6aWX4qijjsKTn/xknHfeeancmWeemfjatm0bADY1+bmf+znst99+o+N4ww034KlPfSqOOeYY7LPPPti2bRsuv/zyAU99yts588wzF5a7/PLLceihhxamhHe84x2xzz77pD49/OEPhzEG//RP/wSAzVfuf//747DDDsN1111XtPWCF7wA1lo86lGPwtve9jYAbFb4L//yLzjttNNSG1dddRUe/ehH4/DDD8f27duT6VC/X3e7293W7Odll12G5z3veQUPT3ziE/HNb34TN91008LnNtunSMcffzy2bNmSft/rXvfCDTfckEx31uvXevMdy5588sl49rOfjT/8wz9cs/85H/3fcf4iHXfccenvgw8+GADS+r/iiitwj3vcoyjf/72kW56W++qtY1+97LLLcMMNN2D//fcvnrn22muT68l6PJx88snYsWMHDj/8cDz2sY/F2972trSnXX311bjppptw8sknF/W/5S1vGbi25HvoGP8nnHACPvGJT8B7j3//93/HiSeeiBNPPBHnn38+vvGNb+Dqq6/GiSeemOp45zvfifvc5z446KCDsG3bNvzJn/zJYNwOO+ww7LXXXul3viauueYaNE2Dn//5n0/399tvPxx11FFrjj0A3O52t8NP/dRPpd/3ute9Buv7Tne6E4wxo21fccUVsNbiLne5S7p/xBFHYN9991237SX9+NFyP7xl98ON9HeM7nnPexbuO/e6171w1VVXwXuPF7zgBQX/a/Fw+eWX4z73uU9x7T73uU8h+5xxxhk455xzAPDYn3vuufjN3/xNAONjFSmXlw488EAAwLHHHltci/380Y9+hG984xvr8rIePfnJT8bzn/983Oc+98HZZ5+NL3zhC+neZZddho997GMFv0cffTQADPb8nKy1uPvd755+H3300YV8D6y9X//nf/4n2rYt5MG99957Q/v1rZlulZFQdkcArL/7u7/Dy1/+cpx//vnFxxbgl+mxj30sXv7yl+Occ87BqaeemgDRBz7wgeTTlfNRVVVRh1Kq8Cs5/fTT8d3vfhevfOUrsWPHDkwmE9zrXvcqfHjXo7vc5S649tprce655+LDH/4wHvWoR+Gkk07CP/7jP+J5z3senvrUpxbl1xunpz71qfjQhz6El7zkJTjiiCOwsrKCRzziEevydOmll6a/t2/fvmH+x6iuazzucY/DOeecg1/91V/F29/+drzyla8EwB+nvK24sZ922mk44YQT8O1vfxsf+tCHsLKygl/6pV9K5R760Idix44deOMb34hDDjkEIQT87M/+7KBf+UdzjG644QY897nPxa/+6q8O7k2n093ap43Qev3ayHtxwAEH4JBDDsE73vEO/OZv/uYuz1+kfP3Hj94if/Yl/XjScl+9deyrN9xwAw4++GCcf/75g3sxbsJ6POy111645JJLcP755+O8887Ds5/9bDznOc/BxRdfnPxU3//+9w/mcDKZFL/zPXSM//vd7364/vrrcckll+DjH/84XvCCF+Cggw7C//2//xfHH388DjnkEBx55JEAgE996lM47bTT8NznPhcPfOADsffee+Pv//7vBz7aY2vi5tprbsm2l3Tz0nI/vGX3w430d7N05pln4lGPelT6fcghh+x0XQDwuMc9Dn/0R3+ET33qU/jkJz+J29/+9rjvfe8LAKNjFWlMXupf28y+ojXrUPOxyWNPALzeHvjAB+L9738/zjvvPLzwhS/ES1/6UjzpSU/CDTfcgIc+9KH4i7/4i0HdUZGys7TcM4d0qwTeRx55JFZWVvCRj3wEZ5xxRnHvmGOOwZve9CbceOONSSi48MILobVOpyif+tSncMYZZ+D1r399Eegl0oMf/GBs3boVr33ta/HBD34QH//4x9O9HTt27BTPF154IV7zmtfgwQ9+MADg61//+mjggosuugiPe9zjit8/93M/l35v374dp556Kk499VQ84hGPwC/90i/he9/7Hm5729vitre9bVHXcccdh7/+67/G9773vVGAd+GFF+Lxj388TjnlFAAs0MVADJHquob3vrh2xBFHDOoaK3fMMcfg61//Or7+9a8nrfeXv/xl/OAHP8Ad73jHVO6MM87Az/7sz+I1r3kNnHMJ6FprR9u6973vjUMPPRTvfOc7ce655+KRj3xkerm/+93v4oorrsAb3/jGtAH2g/5slO5yl7vgiiuuGOUhUlVVg37vTJ8APnVcXV1NH7KLLroI27Ztw6GHHrqhfq033wB/JN/3vvfhwQ9+MB74wAfivPPOK04jx2i9NbkeHXXUUbj44ouLa/3fS7rlabmv3jr21bvc5S741re+BWttEQhoszxYa3HSSSfhpJNOwtlnn4199tkHH/3oR3HyySdjMpnguuuuwwknnDBa/xiN8b/PPvvguOOOw6te9SpUVYWjjz4at73tbXHqqafife97X1H/Jz/5SezYsQN//Md/nK597Wtf23D7AHCHO9wBVVXh05/+NG53u9sBAL7//e/jyiuvXLcv1113Hb7xjW8kYfyiiy4q1vd6dNRRR8E5h89//vO4613vCoCtB77//e9vqg9L+vGg5X54y+6HO0v9YLIXXXQRjjzySBhjsN9++43yOCbHHXPMMbjwwgtx+umnF33JZdf9998fD3/4w3HOOefgU5/6FH7jN34j3Rsbq52h7du345BDDsGFF15Y7GEXXnhh0hQfcMABADi4Y7SwyQ9CIx166KE488wzceaZZ+KZz3wm3vjGN+JJT3oS7nKXu+Dd7343DjvssE1lpnDO4bOf/Wzi44orrsAPfvADHHPMMRt6/vDDD0dVVbj44ovTfv3DH/4QV155Je53v/ttmI9bG90qTc2n0yme8Yxn4OlPf3oyf7vooovwN3/zNzjttNMwnU5x+umn40tf+hI+9rGP4UlPehIe+9jH4sADD8S3vvUtnHLKKfj1X/91PPCBD8S3vvUtfOtb3yoiCBpj8PjHPx7PfOYzceSRRw7McnaGjjzySLz1rW/F5Zdfjk9/+tM47bTTRk8K3/Wud+Fv//ZvceWVV+Lss8/GZz7zGZx11lkAgJe97GV4xzvega985Su48sor8a53vQsHHXTQwujQj370o3HQQQfh4Q9/OC688EL853/+J9797neniNNHHnkk3vOe9+DSSy/FZZddhsc85jGDk6jDDjsMH//4x/Hf//3fa0Y4POyww3Dttdfi0ksvxXe+8x3M53OcdNJJOPbYY3HaaafhkksuwWc+8xk87nGPwwknnFCYKB5zzDG45z3viWc84xl49KMfvaGT5sc85jF43etehw996EOFmfm+++6L/fffH294wxtw9dVX46Mf/Sj+4A/+YN36xujZz3423vKWt+C5z30u/uM//gOXX345/v7v/x5/8id/UvT7Ix/5CL71rW8VAtbO9KlpGjzhCU/Al7/8ZXzgAx/A2WefjbPOOgta6w31a735jrR161a8//3vh7UWD3rQg5KG67//+79x9NFH4zOf+UxRfq01uRF60pOehA984AN42ctehquuugqvf/3rce655y6M5rykW4aW++qtZ1+9173uhYc//OE477zz8NWvfhWf/OQn8cd//Mcpgvh6PLzvfe/DX/7lX+LSSy/F1772NbzlLW9BCAFHHXUU9tprLzz1qU/F7//+7+PNb34zrrnmGlxyySX4q7/6K7z5zW/e1PwAHGn4bW97WxIa99tvPxxzzDEpq0SkI488Etdddx3+/u//Htdccw3+8i//MrnrbJS2bduGJzzhCXja056Gj370o/jSl76Exz/+8UkjFOmZz3xmATwApPV92WWX4YILLsCTn/xkPOpRj8JBBx20obaPPvponHTSSfit3/otfOYzn8HnP/95/NZv/RZWVlaWe92tkJb74Y/ffrgRuu666/AHf/AHuOKKK/COd7wDf/VXf4WnPOUpaz4zJsc97WlPw5ve9Ca89rWvxVVXXYWXvexleM973jPQYp9xxhl485vfjMsvv7wA6buTnva0p+Ev/uIv8M53vhNXXHEF/uiP/giXXnpp6tcRRxyBQw89FM95znNw1VVX4f3vf//AUuj3fu/38G//9m+49tprcckll+BjH/tYAsi/+7u/i+9973t49KMfjYsvvhjXXHMN/u3f/g2/8Ru/kQ4kXvWqVw3cLqqqwpOe9CR8+tOfxuc+9zk8/vGPxz3vec8NuxLutddeOP300/G0pz0NH/vYx/Af//EfeMITngCt9U/2nnmLepjvAnnv6fnPfz7t2LGDqqqi293udikQ1he+8AW6//3vT9PplPbbbz964hOfmKIMxiAE/f927NhR1H/NNdcQAHrRi160Li/9oFxERP/0T/9E+fBecskldLe73Y2m0ykdeeSR9K53vWsQUAIAvfrVr6aTTz6ZJpMJHXbYYfTOd74z3X/DG95Ad77znWnr1q20fft2+sVf/EW65JJL1uTtq1/9Kv3ar/0abd++nbZs2UJ3u9vdUuCDa6+9lu5///vTysoKHXroofSqV72KTjjhhCKowac+9Sk67rjjaDKZ0FrLZTab0a/92q/RPvvskyJEEhF97Wtfo1/5lV+hrVu30l577UWPfOQjB8FHiIj+5m/+pgi6th59+ctfTvPWDwLyoQ99iI455hiaTCZ03HHH0fnnn18Ez4gBLj7/+c8Xz43N4wc/+EG6973vTSsrK7R9+3a6xz3uUQQD+td//Vc64ogjyFo7WEOb6VMMEvLsZz+b9t9/f9q2bRs98YlPLAIordcvorXnux+o6frrr6d73/vedL/73Y9uuOGGNC4f+9jHUpn11uTYWH7/+98f1POGN7yBfuqnfopWVlbo4Q9/OD3/+c+ngw46aN1xWdLNS8t99daxr/7oRz+iJz3pSXTIIYdQVVV06KGH0mmnnZYCWa7HwwUXXEAnnHAC7bvvvrSyskLHHXdcMSYhBHrFK15BRx11FFVVRQcccAA98IEPTFFwx4L5LKI4Z6997WvTtac85SkEgL7yla8UZZ/2tKel/e/UU0+ll7/85cUaGAs29/KXv7xYZ9dffz39f//f/0dbtmyhAw88kF70ohcNxv/0008vsmvEel/zmtfQIYccQtPplB7xiEfQ9773veKZPJBT7Edezze+8Q160IMeRJPJhHbs2EFvf/vb6ba3vS297nWvW3eclvTjR8v98JbbDzfS37Hgar/zO79DZ555Jm3fvp323XdfetaznrVuoLhFctxrXvMaOvzww6mqKvqZn/mZIqhvpBAC7dixowi6tojG5KWxvbTfd+89Pec5z6Gf+qmfoqqqigCVkT7xiU/QscceS9PplO573/vSu971riK42llnnUV3uMMdaDKZ0AEHHECPfexj6Tvf+U56/sorr6RTTjmF9tlnH1pZWaGjjz6afu/3fi+N3dlnn12MTeTx3e9+Nx1++OE0mUzopJNOStHq4zPr7dc/+tGP6DGPeQxt2bKFDjroIHrZy15G97jHPeiP/uiP1h3PWyvdaoH3nqaPf/zjVFXVKEjcU9QHUf8v0fOe9zw69thjb2k2dittpk9jQt2PA+2pNXnGGWfQL/zCL+z2epf0403LfXVJP440JiDuDoqRgT/84Q/v9rqXdOun5X64e6kP6G8Ouv7662n79u307ne/+2Zt95amsYOR3UE33HAD7b333kXk9580ulX6eO9Jms/n+N///V885znPwSMf+cgUcXBJe4aiv8+rXvUqPP/5z7+l2dkt9JPYp12ll7zkJTj55JOxdetWnHvuuXjzm9+M17zmNbc0W0u6mWi5ry7p/wX66Ec/ihtuuAHHHnssvvnNb+LpT386DjvssJ9of8UlbZ6W++Gtn0II+M53voOXvvSl2GefffArv/IrtzRLt0r6/Oc/j6985Su4xz3ugR/+8Id43vOeBwB42MMedgtztufoVunjvSfpHe94B3bs2IEf/OAHeNGLXnRLs/MTT2eddRbuete74sQTT0xpGG7t9JPYp12lz3zmMzj55JNx7LHH4nWvex3+8i//chCwZkk/ubTcV5f0/wK1bYtnPetZuNOd7oRTTjkFBxxwAM4///xBZN8l/b9Ny/3w1k/XXXcdDjzwQLz97W/H3/7t324qKNmSSnrJS16C448/HieddBJuvPFGXHDBBbjNbW5zS7O1x0gR7UJs/iUtaUlLWtKSlrSkJS1pSUta0pKWtCYtNd5LWtKSlrSkJS1pSUta0pKWtKQl7UFaAu8lLWlJS1rSkpa0pCUtaUlLWtKS9iAtgfeSlrSkJS1pSUta0pKWtKQlLWlJe5CWwHtJS1rSkpa0pCUtaUlLWtKSlrSkPUgbDsN3/qc+hRBC+u2cAxGhqirUVYXgPeZtC600tNFQSgEEBAow2sAYA6KAEAJ8CPDeQykNpQCjNbQ2UAog4sigzjtozfUYrWFtBe89nPdQAJRS0FpBaQ0KBK01iAghBL5nDEBA0zYAgMpaKKXgvEcIAYECalvDGA0fPACFEAJmsxmMMaiqCkopUAhQWvMzIcBaA2tsqkcBiIy3ziEED61N4sNaCwXAhwAQwVoDKD7vMNoAigAizOcNiICqslBKw3sH7z2Pk1aoLEdFJRBAgPceMS4eEcEYHmOA0xwQEdq2hdYaAEEpDa00CAStNELwCEQwWsMY5peyZ7XisVVKoWkanmtrYSU6a/AeBEBBMU+yJvL1obWG1hp1PYHWCq1z8M4lvuqqSuPuvEtrxgfPbcmcxeh/1lgQAO8dZvM5gnfcL61hjIXWGtZatG2Ltm247zIOBKCuatSTWsZCwXmH+WzO42dN4rdtWyjFdYXgMVudQWuNlS0rUFrDO49mPofSStatKt6VEAK0VgiBoLWCMQbeBxnbACKk9WqMhtEGSvM4NPOG3wlr4ZyDa3m8AEI9mUBBpXkG+F1pXYvKVqgntbxz3K6SsQX4PWibFgTi91HqBxGquoZNvwFjTTefRKjrGsZyH5T0L4QAKF6HIQRQICiloJQq1j7kmjEGzrk0BkZrXl9Q/F7LfsLrlde0kvnwzsF5B0Cl97JpGgBURBINIcAY282HzD1kDwE4jUszn8NUVarLew+jNaqqwqm/+vB198Ix+vgFFyAt1Gw5KCj0bxAoXY/vEO8D3XOUlVdSKlWVLzfqX8ipvEeg9I6lxnL2pI0UbzM12zWchnbQVMZj1tsydCcBahGvGcvx4UH9fIPHD9nYrcNLerRfeTk+/eLFOC1krRtAQjmcqpyosRbG74yuo5ExX0RrLYmR+sbrJtnbe/fj0ugNeywL+T4pqZjGhhwj1zbALwbj2ee4X44Wjll63UbaHR2PrB9jrBZjmfVnUVmF4XrJ209l42s6UtHC9bBWhSCevPy6zBf12+itfyjgfvfd+bRoFGjdeS7Y7Jot/1i4R4xdp2ygey9U/7mxe3uQFnVj7YfimlbrbqWb52PI0djrurtpfBw2Mzqbq6G/ow/3lo22sHbZzZZf6yu+0bo23NhGKtytDfcr3vnZHq1uwcMLb603DiTfvnSPxtgepT4eGKMNA29rLOZuhkCEynbCL4XAYFvzV9YHz8AbCq1vGWQphbqqGbR4D6VUqkMLqG0FIBMRC+iZEN46FroZuHsYbUAghKAA8qCABBwjUI7EoEwzaCNCJYPig0+bmDUVAjH4rioLBQWtFLTRgPDsvQMFQtAKZABjGLyFwH0O3kMpoLIVAwCtYQQ8RlABIjRNw4BCaQTlARC0NvxBVwrxyz6ZTBjIzxj0QQ4ajLEAEW6arQpoDLC2gtY6Ad94YJGAq4xlB+o9fONAIQBGJ0CkNfdJGZ2AlDEGdV1z3ylgPp8jBA9rKxhj0LYtz0kGfAMRtDHQcnDhvQPAByvGGMznDHZR1wwiPaCpA6IkAlMgAoWAEAghOMzBhwFGa2gFQBtAgBMfAtUM2BQQfIAX8O+8g1YGgeKBDyGouGaCAA2T1hsIcL7ll44AogAnwNkK6FVa8RhYAwUGqcZYtG0DHzyMqaF1nBslhzaWD3rkHfDeQxEfTsEB82YGCtwPrRgIGgGWq7NVGM91BBBc2yI4D601ptMpjyMxsAQYPHvnEHyAlr+dd7K2dVoTRpvUp7huAMC1LV8zBj54tLNWgK3h948IwXcA23sP73g9e+9hq0oObnhIIxgn4sOt4By0sZhMJryuZjOEEFDXE8TDoTqOrW9BAagnNaoItOta1pbnwz1j+dDGO1hroTUf9EVJOB5YKShoa2A1r09jDK+x3ZHcIYHUkRvZTt+VUQmgFMBagGV6NK9h0Z4+AsbLD0fHAmPvcVTXtau6R6LQOpBbMxCsVFFV/KUUpd80uDvy/VP51RwZ5GLhuKiUSmedTgAQQ8rnoQ/RFtYFpO+GSv3v80MjH9/sd38sqVdKcR2xUD6CHd/5fWkT5RrpSo0vmjHRp3ymhIf5WOqs9BAgZWOm4gjRYP0mvtQiCNrjtljLOQrje6obklSmWE655KTyN6DsR5zZATIt2ImLKhfe+froaI/Md2x1dDUL34Xg1xsDVfSrfHbsUCX1qvc+xDZU3t9Uz6LVsxOkFv0YmXeZz13flVXXRN697Pyh4GaTnd1j2GQP006N627r7Eag5mYbGZYvXp21mtsds5edMPbfr6LYLre6+UnY6WlL36GdenrjtAebWK/KcbkAw49J/6R5QzUtpg0D7wj6yDkQCNYYeGtBPmA2n6EyDLhAcrJpGawH70UD2Yp2SoRdWajaaJBoWZVoYpXSMAos1BuLoFlr571H8EHAguXNWWnWIgoA1lojEGE+nycNZlCAh4NzXkCR4Y8QEbdrLQvxzkOrDnxEDXHrWgE0/OHSAlaU5pdMGw0yhvdzIvgQELyHNiZp7J0AHdaGc98jEApyWBC12gAlbbq1Fj54UPDQpmahL/AY1HWNQAFam0xTq2Att8HzxICYRCupLVBVDNSD9wCAlhwDJQBKKxht0DrWSnein/BjDJxYFnSWARWPrWgO48GFlXGdN00Cm8ZYVJVF07TJQoEBoofSGlbAZjyMCQI2FVTSlJMAxaSxFMAcgZ2CwnQ6SfMBrYFAfDigNAIFBM8HNVVV83wHQtvw4Y+xFpWueL0LkHdibQGwgMIa3AYhaFm3cY0G2YRJDpG4nzz2BKsMtNLwFBL4RkAmaMkhlqz3OJ/T6Yr0l0FslKC0MVDQ3B/iQw6lNAwZ1sbLWtVKw5iKjS0IYu1gUFU2zYGW8Wdw7FI/KQTWvLsWVuacLVMa5ll3lhQAZC1bAIS2celgJmq/p9MpH4xI/xQp2RcsjLVJWI7aWUUKpCitF210B/ZDgFEGWit42Xu893AttxupbTxIDlSM5r2jbRp4wzyHQDx3O0tjX1tVwiV5lYaP5pc75JXt8xHmRIhVtluIxioX5cXaJRUdgtcE2vIqkiSeca+yPg7AE/XAY478SniLAtBFoT8DeBGdFR/92LX+l684ScjazwFNdw0ZJ325P7dMSNYIC761pDpY2ml6y/6Xlg49jrPhiX/0BbISFqqMx+xqMR2xTFmRyoZolJdYlPL2ch5Vd7/Hd97uws5k9anh1V5NQ50b9UqkFZv+zPjqv2Lx/eujKureha6d8qgBxXjE9dD1tatXpadTfYOxzvjuXc7ntLcki/kdHhN0DxRrkPI3rHdQoyjN8xiLxV6VHQ6MMrhbqOvVqNiard+R5TJafiHtDt53GSDsBoSxu9Tc45VvqvRwz9poLYt3gjVrWNiA2mC5/uWN93ezo57K7zrSXne0dqGyPfHArte5YMw2Ov2bYkEO4Ybfpz27920YeEfzWC0gJlCANRYerGUNYGAZBXNjGDAGCiLUOhaSDYNEIybYBAaqEWDEj0VlK9Yqa4MIRL13rE3XKvGiFOA9gYyY4MYBmkwEpHRa02i2zZpgNmEl4i9VCAHz+SoUNGxdidZMwzkGs1FjX1VW2mGQp7WGEh69D2xirQBlTLICSGazhjV4gVoYY2CVhfMOwRMQAmubxRw+fh6DaOZjx7RWcIEBsNGqMEULgflggCsm2GIaH4JH6xx0ILjQFCagfIjAmmQo0XyKqXQ0eYfMv7EGWkzTjWhIiQh1peG0Ft4Vl1MKXvoUrRu0Vmx6LmBVK50+JpVo7qP2HWBgWdc1WudgQkBV1wmUp/UICC9seq0A1JMJH0a4VkBfz1JD3APYZFohgBKQq0Rjz/WyWbY2lg9Y5OBDgbXqznlUlsGqkwMRH4gPNZQSIA4E8nCNA/kApQ2CZw15VVegQOJaYVK/nW/T4UC0BAjeJRN+fsdMOsTy3gPQmEzkXZVDn+iSYYxGECuGEDyUNmk+2rYFAs8BiWZ+Op1itjpD0zaoKn4f+BAizifSNcgaMNqwq4UItb51aJs5jK2gxELEO3YpmEwnbIqv+FBjMpnAiEWA0hpBDmOMMWx5EgIoBLQUoL0ciDUNbFVjUteAUjCW4Ns2mb97H+RAR94mxW4PlRwyzVb5fa8m7PLQs4veFJX7cve+ljg1lySRviSLwMagoJRKYnZnklHyImubinJdG1ycMgE9F5tKTjqhPStCNGS2/2EioNickvk3ldfTl69fF5W/hecO8RAySIFypGnAzhCVdZCmGx/VtYPevCG/Xh6BlIcY/Zkf0QhjOFw5ja3CNNVjz6+xbONwyWs6GGKo/MLw2VT9eqAmG8ZSGT3WwMhRRTbuyKYqn3qocizjWcDYwQc/0isfq1ZDjlRWaiBwUVZr0bfy6fVJNP+bkDvz32utn8RWZmHQPZzZHERNzsiaKV8N6s3fnqHezjW8ujuaXzBYm56+DVR9czy7J4Zk/XJUvjy3JG0WzN4cLG/wPRkrtafZ+zGYsbVpBPTefE3nX5CNz+HOS4pMGwbeDL5YSIpmmcYaKM1aPmtskq98CGjdDCAxGa0reM9A3BoDY7T4XEO0uSYBIBDgHJunB1LQymf+2yT+vx0o78CzTqDNB89gwTAIcq4FeUom3qxt5/a0Yn/lQARrq0y2FD9WKDZF1+zfLTeTn3grJsmsAfUCPDSggejv6kRTa6QfJJYBVVUxqICC9w7zpmFhWLSyOmqPRbPetg28Y9BhDIPB4D373orZbBQglOJ6KBCC4n5ETWvUwAJIGvO45LQ20j8Sk3Cp25qkkQ9ECMGjETPz4AMm4jtNgaTv3P/KMsiJhyt8kIDkVz5v5ukDHyigdU7GQA4aRCvsBcgmLTvAhwVywOBcC3IOrXOoRGPOVgP8VgeKGmR0awZsdUCB0oGMkfn1XqwnfBDLBpXcBZLmVWtUxoqmmdur6ug/7iW2AccnABFmzRxt6+D9DK1rUdc1tmzZymOF6PctFgywaEObDpeIANd6caPgAxJbsVbZNQ4+MG9xXEF8KMAgXFwtQJjPVtmcfTKBNjVbb1heT65t0TTztJ6ruoIGH67E9U/E7ztRZ6UR37sQfPITjxYoPgSoEBCcB7SCc2I1IPPrPIlGnA/t4jqGMTxWrkUI7H5CoUla8bhu46FAoAANDd+ydYoxRg7lPIxilw8C2EUBfKBhKgsFLYdKapfMzRVYK5/+HinRp1yjWmz/faAMQimt9/5WednsTgKpnSwNFbfZDo0pAEQ519FUutSkde1k1wszYdXdjwcMSV6Lz0S0FH/xfpaxmvrVWfNG1NgBs8hDx0eps0YaM8raUxjar/Y+vSJc0ojPeocIu/FJY5PGO14uUWichdTPgrcS+JWUmcEvEFDWlPniWGXzpDbwXH+uBmbIRfsqYz07jFB5+a5vuXF+vELCZ7ZaizGLzJYrvzPCH7wEMtXZIJTs9CvrrYmi8GCcFG/BVFam8/WaGMjWgcrGkTreCwyPfKS6SBDR+ic33O/vEt0T2V/ZkioOiBaspa6/aqFL9O6jsVp3VaRdRJtFa3uCbql2dwcNeb/Ze7N4C9rztDlsNqRb89Tf3LRgrPbIEObf6nU/Dut8aDexMDcMvKOGu5JgXVHLppSGNTppm4P3cG0LH7i8tVp8XFvxezZi3gvWwrYuacFABFtZ0VApUABI3G6dmLjXVZ0ClGmtoSoLHUzSXEO0jfMQ4AVsRJBZVxWiOXvTcsCwST0BwNbIumLASwK4o4avrqskwLRti0Di0wyNuZujaR1WpkY0gCaNF/nAVsQxSJSA8kldC9bsgqYZrVjDZ60Eb3NsJq5UAq2ta5IJOIi1/6zZZFDvxHQXYKBiLJtWK7AGlAKhdQ3PWSUm74HLx4MVH1hjHQNeVZb9cHmM+OBAgYNaudaxGKA1fCAoePY5N9y/QARrFGxVwc9muOmmm9ic3BoRzATsihVEPJBgc3Gk4GbOe7ZAUN2cTFdW0LYN5vN5Mqc3lUFV11DowAtB3CQg4FzGEkrGWxvoqguslgQixYHTvHeiPfUg5zrhJYKBKO+JebU1Fq1vBZibVNZohZXpFG3r0DRK5pkDjnnn2ALCWsznrnPLUHwgo0jB1hW2bKmTNte5FvPZKgNMrVEZnQ594iGVkXgDcV7Z9J0PMbSxsCbAkwMpmVuQWBO0Mjd8wMU8sl+/VgqubTFvHaytoCoOUudah8mkxsqWrXJooTCxnWUD7xf8HnrvEWYCfsXaoLLxXQ1QHqL9NgycJWAa+4nzQUJtJ+lwx7k2xTeIfvgmHs7I+oxzDglgBwC2qlPQxxj8cKdJ5UIvhv6mecHeX/1/+6U64NHxl0OT0dpzeT/+zG7mYIaB9jgH6f+9W13/Rrin2EKGfhQKc+XuQ5XBsB52AnrtZPwKnBy0XVzL+cvb1V0DKnHQ070lZvJ+5WA8RzQ5QsknnlLxHBbFx1PPVSqNEnoO57kcgZLWFgvyeRoXEoq684OWDPQObAnku62AZHodhzcv3/+742pRT8b7nF9b6H+cutr3hoeMe2w7OwrIrg8ripyOrJF4jqPyM5eRse4dpkWfn0Vzls9Scl3IJkUVDaI7tepXSOhy11Dsa/dsMbaUrTql1tjDdhPlljPAHm5sfM1vuoqfVNpVYLlrj+7eBjfByB4/jvkxXTO7Ybp3f0O7m6mxyS2ujV3MHh7sf93B8dqNrU+bMDWnFCTMZEDbu5D8L6PftDFGfIY7Bq3VIKgugBUU/1YKJKDRz2Zd8CwTBfaQBPbpdJq2T+faFNAsXnStS5rRPIAUbPfBZzNqFD7mrMVkV1sEoG0doFQCnLFPESx6F0Dk2Iy7sp3Zu1aJBycm2kqCZEUhIGrinHOYz2eiceRgUNPpSgLaIBIzcQPt2Uyco7ez6TVA8M5Daa6TQKKlh5ggsxkwe2cDCIEDhkFxMC3Fhw3GVHDOCaBhINg0MyhwoLnJZJK0+U3bprkBVDKFtuILrrRGVdcpEF1LHKBLiTYbijXYoE5OqKoKdc1+3fEwxzsH7yXgHilUte38xedzrM5WJSI60DQNggmJT0DWgWtR1TWM+OyzNreBBmvvrbWAaEGj+bkXH+5oxh3vERHXKfEBqqriQ6EQEERDG2SNaKOhHJI5fDMPmEymIGtEuAlJo8zRzHUSAY21mCo+oDGyJtq24fXonGiE4+sTI5Z7VBVrxQMRlFEwsFA6oLIVfPBo5o2A+QqT6ZT960Ng7bbWKaAZW61ozGaraOcNqrpCXU8ABdT1hA+8iA/M6noCLaC8mcsh1nSCuq7Zv1oOgeq6xnw+lwB7XM9E/O+946CE0YOZA8DJYUh2yGGshlWdL7prmX8r2RR4X2K+vA9iUWNkPowEXFQAKbjQIO6m3rFLhzYGxiisawO6AYo7DVfVE9SLDTuLaF5QTyIGMqF6CEb6v1OLo76cfUF3UW3Dqz04ObTczp7p/G/jxQ4UlDihbLcPeWI7JCBjGKBrbDx6EDpXWkqlmefO8KxF9W/kQD4HVEP+R3sRNfsdMkUCNnn1Pc7zQ4gCVFFZNgG9QmiRH32VZX/683EY6cZ6vetQbFr1g3L9Oe24zxkufX27uSI5mumAaCyzSKYqVn3qM5Vjly/ewjKj1+MSlQ7esbIH5V/SUFeiXLhytfeOFi9c/3iAimcAWmwE06uTiotj+0JeoBzF3S6glydKIzfHi+8eGuyGC+78ONAGR57K+R31ib8Z6RZufs/R6CnUzvf2x3vt/QRTXzZZRP0yxWYfD1IjON/8DG4ceIeQtKspbRfEpFn+hYqphlhzzSa/ckogWjgQgbQGlE7gw3sHS6z1TYHJxNfVB8/+z1pDg7VjWkbPOQfvnWitgNlsFQROG6UrLealArAlUBrA5u9KKzjnYMnKuHIQtOgH7CUonLWigY+CvWMARgCUZ41tG1r2nxaTWyj2Y1bCf0yd5J2Dh0+HGAALAdGf2HsPL/8CyAAMm8hbW6WAXVp3wd0ANuWO6ZSM0Sn41axtJEK7ScHXjOXI6FrpBHidAJ1K7nEkcJ1Mi4NoTAOxRQGPoWYzYtEiV5JeC+LXH7WRLGcqbNu2F2sxxcLBBY6C7VqXTKIV2NKBtabsJ29NxZpkalOgNgoB08kU08lUQL2Cdwy85u08WS1Mp1NE03EFpLRhzrUAlFhB8CA2TZP+DhRkr+18pJUCBxIjPvSAYmWCNgbTis3JY3T96LvuyWN1tspzIOMT3wlAQ45M4J2TtnTHr9Iw4v7ABzVzNjn3rG1m4NuCQgNoBQUNI7x7OahhF4Y5KluL370Vs/UW5AOs0uLawe9HDOQWtcwxQ4ACoW1aNsmvKhjDQfN47jh6O8ABy1wrazsoySrQWQ4o2biM1rCTCQfWk8M8pRSM9wguwDVsmaF7h0hGDkucc9DWQot7ABGgg4YPPEYkfvoWAJFObgOuZQuI6XQFgaRPKnCE/E2eWo7uk9n/C/FVNmoSAT+a1uZRsdOJlIp7+1pi5wZEUrX+d6a8P4Auo8+qsuhAji4x9Zheepz6PRr+HT92cd8b57IvXvOuzxq8DqzkgBpp3Is6MqsZmb00Zx1H8c5IB1IdfbSRA7/+AMjkFwgUC+oogfoQ9lLqQ3lZVqnqNM7lcUUc3zhgC1bQAECpkaLU9anoKP87GK6sTN7jwfoqailGVQplT/etRTLTeYWOt+JdzBd5XB+pTLZ6ojVAYip/fgHjHSMo5nFUiKPeVA9nOV1NbZW7Sr7LdCCtjKfeLbXIP5fO52O3QoTUVoYcs9ciXtuwTLuAva7qvMDugIe7D2Le2sHqrn81bybKBrrAURgf+/XB8UZR3Obo5lgPu6PuH5t1u4iRRYwpjO4G6ZFir924/LIZ2jDw5mjLHAU5+ilHs9c8zZA2BlqTBOpSUBpJ+8wB1giWiNMJxfRTQQEUANFO+phLWkyQIZrrIDmQg4r+pD4NjhZtq4KSPOIcWEuhS2VkxHRb6TjAlAABEVBNatE+K8znczjPmm2tFbzyqc+ABDcTrRqpTnuntIIRLTeBEGZsOlzXdcrVHEHspK5hxc87aud94DqiWbs2ne969Jd3zmE2m7Em2VaY1DWM1gg6RuxWmLdN8reF1tCABNgKACjlloaYcYM4YF5lOYiY9WxK3rQNKluhquo01977znwNEPPyiqNfB4KCBrvpMqxsWweScWOAVUlU64bFAaPgJA0WR6uWAFxicQAASrP2dNu2bUlDH8dJ+UwYVoSVyYqYVrNW13mP+WyWAqXNmzlc28KKf7ACUEtAvRgN3jmfeI4B72zMY04MUPm6A80pmbg7SX1X1WzxUdcRkMpcKg2loxk6yWEIoEXbrJQGFEmUcZPGOWp6a8m5Hd8vLX7VVlUwlUnm7QwoY55riRMgfZlOpwh1LaCdgwK2oU0+1lVVYTqZsgWF+LWz60HTuQWAOFCcAiaTKZC5REApMemXCOwKqOspQnAIgdC2cygAk+kUBD7QUuCDl6qycMojeH5J2UeerQXCPCTwbiVVX2UrtFoz2Ff8XnJARUrpDgEFMpz+rG0bTodnJIK+UbzGwYc7O0tr526MQm93UNZFHM8BHAq5tzRFXuOrMvZnfi0HcX2JQ9pSAKhQoUVANqyv4FN+ZtbbA1lkbGgGl3rSzxhuSSMyMNvNJfb+n2N+w/HJDJT1x3hESx/Newem32vOfQYLizbKNpMfLyIgzsrmk6D6Penqy58ZYri83bzkEBUWNY6uvViOfcdVv08KGVBFNmfDQHPjK46Km9mnptf+IuF4ZB7ZKZs/h716uvHuP62zMjEYoUC4Bet1uI6GLHV1lmupoNyUDxgzekCnecmapayxjKmxWc7HoQ8f4rru5mssbsWuEg1/rvUq7WJLZdULR34TtIeY3UzdqldyZ1naHV3Jzqn2KI29S4tvrUkbX3KLSu1cbzf61E4dQt0ClJ+rFnxuhudd7d+GFkH/izPy3KgQ1a+Chn9j4+tpE+nEeJuOga4YfMc0YEpyDhtUBNFEe8ldXEkUZknblItRoml1jsEpa3B1SulFopXlHNuQwGkh+X4yONcCBlQCBDHPsPMcZCqmnVJK8ncnd06F2XwuvsoMPKfTaTLpnkg08KZtoQjpICD6lGqlEMAabVNpeM2mt/NmjrZtYawW7RsHDTOGzbQJgFEh8YRAqS8xHzcBDNYV5zF3bdv5gxvLkbrBuoB5Mwf7rvPvIH7d03pSmL8rxZHDYyAwmQQ21/UOMXWWrSxUkOjvEqnbSjA8NqHu5peAlAfcWotacmk3bQMtqbb4cIMA16Y81vP5HLPZTABUBSOHDnEtxMjaWus019Za1HWdDiJifnZUnf951FArcRkgAmpbwawoNK0csgQ+5HE+4KabbkJlLerJBIAHiH2EdcUm83yYYmAsa5HbpkltpfRiStK1ASkivVIaTdvAaI3JdCqHTxzwrG0bNj+Xd6DLd00cCV/b5IoQD7VsZRE8a3yjJQmJi0RMy6W1RhM4jV4cJ5Z5Obr6fDZDVVVsgm8kAj7YuiOAA+s18zlCWMVkMk0HQtEVgwjJqiCCcq05CJvSWiK2a3jl4cXcPpD4hlsNOK6kqiq4loMJVtayyb2AdUec1s8FiVcg7i1aDnHSIVoleeKjZYLMPac6UxxEsOE4CfWkBhQQNGHLVj64mc3nsK1DPZnCWj4I4iCEO0/DDVcVG7NC94FSqtN+jwI/AgotzWKUsUCEzECOQvH3WB3pw5maK+ss8bpKfeCbCtHPNwGv7LnRcSlqxcAqOnEhg5abpg8Fzcg0DRVoqhv97MuTCTTrS4vFp3qIFEfaGnk6AiKVPZhJVfk6yANw8c0ShI0CzJ6mtRup/DRinb721tf4CuhxMPAtjm1SyW2+zPvAUMZB9epQg8GMlfQhVJ/FbAX0FosalOu0+/xPvz+9fiJb58X4ro84utLxiEGNrIter6LLSGo+M3lX4z7see/KSyNC56Iyqmxrt1J/UxhrZP3hXFfQLkdzvQY3S7tvZPbIGN+MdLPwP3oytHNVbHQl7Gq/Fnyud4p+HNdIfxzTa72zpyA3C/W/oRjuoT3aTDc2WnbjUc0FqIXQCb/OefHJZHBkbIxKzMDBCWDyQUMrvqeVRuPYHJU1V5WYkSJpiq2kZ2qaRgKSqaRJM9qwts8YVFZSD1FIvqF1VUseZNZ8N/OG/T7FxLgyXY5rPjjQIMvaegCS25rbi0Cf/YvFXxwk5rcM1IOjDIi5pLUMFKADAyBCzZHTJUd5HX2IBdC0EchrhaqqResbkkUAm2+ziXGYBcQUUUCXi9wY1rJG4SoeNqR0aZ4jXKuatd9ty+b82rLmNPoxU+jmQsmhBsVxkVVlJTBYjG4erQ+U8giWrQe01jCqA0sRvEcrSoCBfVVVaJ2DdwAUR5aOBxIxmnocWyD6r8cUWg5tKynmEPNbE0KI65TXjq4s6skWWOfQzluQNrCVhtYW3rVJax3NnimwRluLD3Ytfvzz+TzFOaiqCpVSSXNtDFtKJK156KLH33TTjZJH20hshJoj/NtKDlcU2rZJwd844JwVYK/S77h+AEBpxZH6FVjbrrqI+VrcDaKvvVKKI6ATp7vz0XVDqZS3m4jLcJouHhMVlGiMeW7inCutYEwN79giwjsPbWSNSDwCIxYobGVg2B1AgET0vZ6tzuDgkuUB+3B7Ocir+VBNmxQvgQIL3QSCa1u0TSPxB7hPk3oi0f75EIrTjVkYG1PUKVTTCZyvoLSBaxs+qDMGLoTk/rEzVGr9OlCVKaWkVAe6CvPUcZVTodUagpvYIo26oPVgQeRuFAhH8EFZuX7hTm/fFyko4cgellpwSk+DGlTXE7kofafY8sipcqHp5qv9/N1537uHVQ/UIhuoCIry/hS96q50E8hlFwiHaSqz+hIbGVBNhdKgMJ+l4XBWWRpgyjo5Aq6od28wuf1V0eMlPR6fz8auB2zL9vq31UjbEJDV/aY4OHH598oVayevqj9NBQglqBGf96xBrqxI95cB4qz8oPo4z71uDd6SbuFiMP6xz4mfbm5H1xa6MStGhLDgEKvPUd4P1btHw7kf1LWrNNLAZtsZIKiuHwPQsztR0I8jxbXZfyVvTlrU8MhHKF3aBLMLn6Fs+jfI3xpbRXljrPBG2toE+Ox/ikaX9TrNFOV311rfzMGWGmk7+50n8VhY584C9k2W3/ABzB48QNhEcDU22eTAYT6l8WGQpEVoJVjLWlpjuWoOPMZAySR/UZXSHjnnxLR1giiCBeKc1lAqAcYWrL2KWnFOpc0avUpVon1jUNs6Bx84GJW3HBhM+QAKlDSuTjS2WmlMJ1VqMwW7UkjgTykxVRW/9siHkajpCko09x6VZYBCJIcCYmobKDAo1yqB0TxNTAT1bMpOkkZKJfPD6HceA5xNJlMA1EWgrqoEphREky/B1LTSmE5XxFy4BYzBdDrpwLznSNZas0+tVjw/TsaJA2OplHLJagbU0Ve+rmpYE1hTrji3eFXVkC6JNj6IfzP7f8cgczr6r1c2W2udK4PJ8lFHgBj5BICmaZOgomSXinNpJap7CCFpp2OUe48ArdlEXIvGP6aTsqKFJS28i0aZzdJl/rwXfoL0r5HgYxKZW2tMV1bgW4f5XMr4AKUCjLUcHT9G6VYqRaCPWm72Se6ilPO71fnjO8keECS3+WQyAQWfonprBbQNBxq08i5CKTHTl7GpqtRnHzwsgOl0BXVdw/uAZs5pz4y20BpwwcO1HFQQou228h57Ac1aQaxP+D9b2WQqzsHeHIzmzAYAH5BoOeSK1gRWAueBwO8ZLB+KyLtUVayhbpsGq+0c5ANMJYc8FKCJffK90Wglrzei64pYkdiKDyea+YyHRlIT7jSlj2aeamtEF90DegoYmJF2vt9d+QQHkxAQr2egO6s77kuFkJJAT8ZwuiUHXqMSa8cgnwOodJviX6oEBzqyGBFHj5ExIAGJ3hG7SQLUuJ18dHqU4YY0ZqMnDBn0Vd1DlLWZP5TGm7LnSsSWaoztdlCpbL7PeRmIrG/GW/KZj3M5/aqQ2nLOir7E4YigTHycKetzOe35wUA2FmmsimOmQT/TekAxNZFN5P7GhSCWutKtba36NeRznB085KVGpPni3RmU6+aWyofkH4kPk72nalCom/nMsaQclezVL11LVF6qJxFuxCRaFX92OdH7K6L8eziOkd3BrA2a2WmKVffONwCM7D273szY+vqxpT0g6A+H9JYYiN7HIbs8SiNAPVWzM80X34YNPiPDVLCyyaHbyHvbjxmxOep25Z2e0c2sud7hzmb43l1LevOvyAa47Fe2bqf6C3PztPF0YiHA1DVr3uazBM5yANk0LXxgU2/WyPKengCt6sy02Wx4wtHJiVI+5KaZM4CtKkBLHmgN9rlV4HIi6GuFBP6NiqbmSJpQVCSpphi8sYbOsY91iNHZWesKeS6EgOCD5M4WAUIrzhHsPFoJbKUUm7Nq1XlKaQE2nJfYorIm5QiP2k+lWWvvJB1YDOjGPqhsYhsksFj0CY8acVtZTLUFpI4YrIvEN5vHmzUkWmsY0mKWbpL2uKpYmw4wQPU+pCjsFuzvzPnIuyj2KWd1VfNz3mMeUzJZzrcMIAHvLu94K2myDIKXXOWzOZL2NBCgSfzKTQLbIEnHRmJGD15rRiKbO0m/FTXcWoLXgfiAKJpnx4BhSoGjeM8b3LS6yvOuLZq2xURMr6dqgrlob40c4CjRYsdc7C7Llw5iE++43gAlGlkg+ACrFTQUvEI6zAlyeAUg+UrHvPNB1lISOmXcjcRUaNommeRz0EAlliBykNQ0gIo5yruo6Ua09fEQY2VlC0eEVwx6lVYCfDswocCa7kBBDlDkkCS6RGgjrhMEW9fQ4PeuVS1cM4dvHVsK+CB+6ZPkRpEiuZNP1gHR3QGQAxaJQh4PrhrJ9a7EncTaCnqiUU1q3HDDjcndA0TpAMBWFlVVyyGFY0inSVws+IOnjYGx7OteV7vo450Jp+NauFwQLiWIwpMyywXew7xZhTkIiCAn+wRnv7tnoz9u4Y2dsdmBLSD/wPVAWSagFzi+ZLIHYAoY2PMlHz5RDF3RfzVsM2ujbKWrNdZDuToooe3MN3rRV13lo5QgC18ZwyejUcilcGpXakr383kd9qF/NfHcuzjQ6gIJaKfi6TAXWW/yivLYAzkHw/7yOu1f7AHlNAEZy5S1OHhXcr56FfVRfHwwN4seyFpZozlla6AvZPcF7kXwtxg1JX7ThMwMPHuJZQ/oDCW6Q5PSrHvw0vd47h00jPBEg0OFcqaL0e0dVMT56prKJO5dpTVk1f4hznrVrFd2uH42WPkeoHUBQ39ceg+s+3w8pMxMRBYP9eAFGTS9eKjGn13zmT7oVjs3FYvr34nKNvLo2Gu44IFBSsqNVBz/6m9PI3NeHNaN1tifF/m9K4c5G3hm3Xds+NHCwvW3llgw/KSOPNy/M/IijH0K1urn6L1dP7jaFPD2IaCuKkwlxVAEIdHPO5oDM2uSkzsJGXy1lhzWMSp0VdcsMHsW9DlqNAvL02oqKY4gUZYlJ7j3gEfSIMco11qzhqt1DYJnU+TplOsI4kvLsdIJAUHScqmUI5zN2AEb83/Lx7AyFgFdQC1tNCiQAClKhxAkQDaW0ZrLaQQow6a+0cxdazHvdpw3O1AeKV4CS8WUaEpyRFccKCv6Qsd85yCOyB39pTttqpZDALE4MAam6nJBB/H11lphy5ataY67YHSSdgtRA8+HDMZaWO/S+tNaCYBns2STAZjgGVhqcRGwVcVxAoJH8A5KVVAydlCaNbcU4L24BlScEgska9A7+NbB6S5iNhFYix1E020svzBawbsWwfM60cZgurIi5vGcZz5q+JVSPM+SIi0QgSSYmNEmvWqVaObJQnygA0LQySJBkYKRde+9S+PO86UxqScgRYBT6aBmNpux+bq16VAKEGuAInaBAHMCa5brClpptC1hNpunNH7e82GNtZzj3oXOT9pYdIcFBFhtwUENgeAdHFGyMjGaD2K8dynIYVVV3IaTGAtKwVrJt201tJ7AOz4s8xKcj1P8aVij2W9fKTHpDxK9XsNUFeqJTgcvfCDAfuMctFElLTmxLwG0UphMaqgJC7Ou5Qj6lbUwlRyoqZhpgQMJBqUArdkqQPPcOsdrjXMK7CSNfGBIPnw5KO4Dme6Z4Q7fB8H8I0Ms2Q2KoG7kQ1GK6eUJO6H3R+8Dt/ATM8JGv+lcZi8wU4FWqHxCdZ7t/e7QImZH2h67kJJYFU2qBV9zEg1xwQHSbFL+CGX/9BpWvTL95grzYhQd749f4neAWrPxSD+z9VBoM/v97B1mKCrLZdVT/JTHflJZh1Ii+PdpMOgK6XSqN/1xrXZrixIbqZ4x+9S+ZlwBCx3ys5pVNk5JeM2mgovSsJYeD+k6dc1SMaY9ngfj1L1xSYgfEcS7Xma8Zmsmne0U64aZylke2UKKvnQF4x6G3YK7c9bTuqFunvccLerxYlF+OGuLy/bX1ppl+u/auqQ28YwCxHVT9dbNKC9jL3pxv1cMWcVr0li9vTb6zeWq1DFW831W9W/2vwm9vWC8wpHnyuuF1Urc91TOQrY/IT9CX8TPws4NaESkWKdk3tbYOhnpd1ojalAiFUvV03iZ9doYfWLsfVxvrPpX5WvRS7faHw0ocbldRP0XZM0OrrUHbJw2HtVcNGNaa6iKV54PETSy1ruSCMoRCBlrGExIai6AA4bVVYUWPBhBfMONNSBHYkZdp7RVJIAv+v3GqNqtaD215o3GS2RyEn9iUEDTNsnfGJzgWvzRFQAvLoRBtPAedV2jqqyAYgXnmWcH12lj0QUrM9YAhv1XgxwexKBnwQeQEZPhwJphrSxaMWclAb7GaDjvxEcakjopwKiqE2aUEo0wEMhz8CnP6b8iSGtblwBzm6XwilrrmO4phAAt8UxjTmoFBtM+BCgiiQiOtACj1pwEwMRDCe4v+9WTBDjLfbABhXk7hyIFXYvPuI/mpLzDcjAs9i/XMp5QGnVtU8A3TkHmU9qrmCs8rsv5bIZAIZmIsz8x0th4AWpKQLnoVnke5GCjcQ18y2A05tB2rsVN81UJ/BZQS4DAaLqstEZwHCiu0xrzuo/+yEZS8FVVJQCe4FsOnscA2SWNP8Ba9EDEadIs+847NxfXAh4z9u92EidBSRA55qeqKmgTgIa1m/EdbVvWiEdzdd96NiOXKP6cuo417J1mhg/TOFCgQWVrWece0V0C4vrftmK5UtWoaz4wapSCDXxoFd9lAvuCt5IWzjsP0nxwF6DSgZFRPOaNBF2MPu4cFb1JAdk0olsLW5RA0vVBYlGA+ADDkQeB3wdtNGrLrhqcBcCjbVp+b3YjdabKvN7zDwL1N/vsqe5PJUJvhCMY+UB0Z+2lNjqgW+XjwlchEqnyc62y+6MiSaq4A0KUWsw+0OvJaKme4Zl9v80C9Klh2R4GyXjomTb35d+OhQJMStS+Xm0CjFTeNSXzSdmV8slcZBmWytrry4uqVy7zQc9L9uVi1u6PQIfeuOV95+ZVeUMe6v3s9WNEDl5z5fUb6C6pyIw827kEDMSpYR96wmOX6QJZiS6B2viCU9Ju7xrla3rRm9EDpwrI3UbyQxylxs3a05+jwfIie9T1duSlHtV+yaX+slhL1KX82dHe7jz1MZZaK21dorXK7IRAPOh0fzSKm+iOBwrHC4xOQu/+umPXn0w1cnu0rfHKBp+JtRtmioH8NtrMmm2sx8GCBhZ+F+UZWlRu0RwsoGKfX9DWot/dJ29Qrsujvkl+9giVPK81XqNvz8LXaawv6wH9zdS1+D1Y69mNr3mMd3h0P1jUr/71ze0/GwbeMVJ40zQSUTkkjSi5zgQVYEHXBy8BwrpIwd6HFMk4aUlDAER5GqMzG2MA7yX1GJJAmTTbogEOxHmltTEw0mdPhOA4qFYEStFcu/skSYA0xV6F1loYW6WAZTGQXAzqFdMTRWDZ/askBVgAycFC5L9tOS9zbatkNu0EaCjFGmrvPbZs2ZL6pyV6NWv1A1rXiMZfJS26IdEEU5XM2oEMgDv2oU3m/VrBKJ4XJ1pOpXgulPDrvQfJQQaUwqSq4Hwr0d41dKWTdtJ5l8zjAZd8crvc5DwRPEYa2okpPpFEk5ec31WFyWQifude8jMTVmczKAKmK9O03rT0A8Qm6LayqE2d8pLHwFyV+LnHnOTaaEz0BM65ZBHAUboD6gkHJNMQFwPPIM97j5oCUNeIgbeUAqwyyboi5vvWYkIezdqril0p2obrWVnZIsHTWDMO4kB6SmsYpeChoDT7aMdgeNZatE2TQKAxMTgZIYgWWxG7A1hJZxcPtaLWn9cEp+IylU1+3VXF7XgK0Np3YF8iiGvDcxXER72yFWxlMZlM0xrk942Bceta9r2OWmlwajDXtuKbyfMVg59BLGOIOncBLUK9gpIDLw6MNp/NUx84uCCxy4Ucejkl7g1xH5BDoLhPQEVXAHn/2wY6WFijUh55bSza1mF1tpoOqnaWoiUC/0CJrPuYIX64++WKCjsBiJJANDBoSx/4/kdHLUzC1GOJOoY2Kh6V3+9OA9zdLOsqbsX2+oMi46Gyh7RcT2AhluvjqX6f1uB1cT/i73I+lDzWmcd3msPYHz4UiM8Jg6r0lx8cgfS+08lne1QOVCNcUSEhq964qsh4v0tUcNkNfz6mWf/ytTa6oqj/R+7KEFtaRJRuUz4mqutxAjrx/RpbWAUc6o5fuve56HE3f/LOj2LvwbV8dQxXWNdSfvyD7P3oIH+xLSwA/t1EdM/EOVYZL6lUb6sZHkJINez7N+zeWJeLRtZERJum0ZrUmneRD9awxEaF3u7JvLaxnaHYcxbUtVaRhVrEdfs52tSmy4y+LtnSWrd7sY71Cm6kolQu2+B233Las7TJ85xdrn+XXzXZH/K9fZ3mFTa23tdr85agMbGt98nLvjPyTHZtMdHInzTyUm2krpI2DLwZLOuUOgpggK0sC+xt60TA9uJXzWbOs7mTAFH8kUtm1d4nv+WYGznuDCmwmNbJ/DZSNC/XYG1jFOCN+FNH8p79RJ1rJfIy++FCIflHB4+kEZ5MJ7DGwrmWg0RZ8SkmQpDAWwzSPOcbFt9yQIHIp+BuIdMKcx5tSpG6o6k0+8xqAAzijDEI5GEoBqFj/2WjOHgVlELbtAg+jjtbGbQt5ynXAk7iGLDvtAS3gk1B0ULD5vwxJzoIEvlbgt4p/gRFwJkCX3nWjnbm9V1kcwBo24a1qCGgUuKHLVpzLYcA3nusrq6Ku4AWrSPn7CawtpoPJth33HknJv+SS1p8kGNAMWM5+JuOQon45MdUc855BrjGwAvPOgNx7EPtoa2VFF9tAtBxbUbfca1UOlgI4tcdA1fFA5lK1nAIBvVEAqnpmBIPKYhZDg7jOMf6QZA8013KPBiV3qcY2ZsAMZXmiPS1RA9PbgJQgIy5MQYKSFHXm7bhvSNFYPcADFtqUMBsNpPnLILxHCHdGgblRDAEsb4IYsqtAHETiYH+YtC9ibg1AICbz9P65/eP33FHjg/uQCCqQJJmK66zuLElNwvv2UVCDv6UYp/3NjTp3ZvPVkWg5mCPdV2jricp/ZmHHPaIQD+pazatF6uQnaP865YL+iNIKkm0PTCW3xtLa6RGPigLOBlrkrL/x0IR8PXr7IOufn2D78245Mm3snv9tGBjbcQPZSHz9zuzs0QZUCEU2DRnti8yM/hGOU1AppztA6ZFfKpizAbC+Vhfo4/2WhQ7NQBg/R8oxhZAqWiX9tK67U90OmjoXS8mbAhjxteqGq6RAu2hSOM1kLBSX1XWvfGVP/r32FgB43NJSX+OInL+AJCWAxP/n6/7sT50rVM3afm9bIJi+2uKwQsGfQy0F8yo2FzPZ353Cda9PSy/vrNtrL1P9d/k7qGxIVpbjl7rLRx5uTbTnc0+s8HyizFcfli19oNjAH43rYaSRiodtDUyaRvmZ62F0i+zxrAMPumbmTsp2/dwWvjoJg89Rj+Vi965kfIbppF2B1vZbl4ku7bu4ntJiyvZ2cp34rmN5/E2GtZULJALEGslFRPAwEJbm6XJohT1GwJyiFjbBUTTcQ3nPLxbFf9kk3qhNZJfcQS0zjk08zlsVWEiaZhiyjIj5roTibTsiU25iQgkJrXW2BTMSuvcdznmgY5aXQ9Ev1YAtqowtQbst+sT4PYCcmMk6TivJBq4QOw1qpROZvcUCI1rUQmgiRp8Xdn4MLQ10IH9en28L8HEYvRzrTWmkymbhYMkpZb4wQoIBhSCd2haj0rSLMEjRemOuZEjxokRvCkg5eSOwa6MMSBFcI1PEeWtZauBCHQ1JNiXpBfzkl6Nc0ozb8YaIIC16QkAB6hKo1IaaorOZxuSdztuVkSwVQVjtABMzpsd5zC6H1hbYd60oKaFsR6Vrdh027NbgdYG8/kcIXi02gCKgamNKdh8kGsmHczw+MSI6hJ3QBvJ/RyjvmvUk+4+iR8zB9djYcZoDYKCkfRiMfWYa5kvowxMZVCLNUBcr1Fg4nzpBkp8+V3TwBrOpW1l/TnRltuKfd1jjnECMFtdxerqKqwxmEymmEynMBWDagqEqvIpXz0FwqxZ5Xe/qmCjGX88hAmOU5nVFob49D8EdjlRolWZ1FOOpaAVEGScrZFUZRxPofEN5pL2z9aVuIoYiVXgYSZTENic19oaQXc+6xwoMUZCV2JqL+8qBVTGoKprBPJoGraEqMW6wrVtOsTgLAe7ENU8UvE1VaOXuytlmT5RBCGZaeuYiL9QiIYI0KrzpYzl+ybH6XpW61rG33kQwIU96N3ogMvCIosB2loiQh9l9P5MUdgz/3jVe24jQlChdB3Bpt1cjlgmZLSwJ/kkoZvz8TVSSjwKOe4uwVkOqkfBRt6hrCz1rvWFznHwlLHVWyOjhzqjg61Gfo6/S/33gcqH0t8ponuv7ryH6fnij3hTJf91HldVmpXHynqPjZxbrNO6wuhJkxxILl5VPfPnfA9a8FzOk+qdLK1hTb/TlJb3Hqgbee/lf/3pHl0z8k6vyc4aG8NaS2Dd7m0SVA2ub5K3YjwIg/EZLNL46YnzNYrqerxkL2HxGaTh3pnzPhh/GuF5pN2dBo5Ye/jHulruDcjcc7Kh649vvxIUw5uuD4Brb0CiR9fI0KVHFHpbx1qDkzO34IOc+jeyVhbNZcHXGutlvaWfysV1kH27iu2Dht+lsg3Kfqede1hwZ/einXxuw8C7aVp47RGP/mMOaijWhisoQCPl3QWxCSuDNgtjrYBRghK/2umUzUS9kzy9AtYZCGfaVIkWzebLDKDZl1gCXQVisEFAZWuQYWHaK/ZdhWLDSy2+osp1zvZGzLuJCK1jLW1VVYhpp6AghwVRw4gOvCsFY5SAKSRzaALBBI58zWauLdq2kejprDmPwaPaGMHcRI04A+n5fI62bVOu6wjSojl3VVXQViO0DMwj8I/gPOYWDxRAPsCBTebzgHghhOS37RsvZt2sQY0BsCC8VqaWoFs8JtFUvrIWjWuhoaGNQtPM0TYu+cfXdZ0idlvLhwlBdvQYyEwrnYLjQStAwDTAftNOos3HQF5R+wsC8w8xCScBfpXFdDpB23Q+4gRC6xrMVmdiYSDRrWW+p9MVPngJPs2xEm1/0zao6wnqKvrds68wFGAljRUHRROTaM8+2USBfadlrNumwaSuYSoOghc84H0DJ2upbeYw1mIymUhQN3ZjcE78ymX3cd5DU6dpNzGyPvF4V3UlQcs41oEr3i8jAewIrXegZgZrqqR1N8awebzWcG0rOb8NNPj+fDYXbXcNrdkXPvr/V1WVtM6aOGI/EWv+J/UkjWkXGJAkGKGGtdy34Hme+dDJAYGtMqKLBUBQKqbzM+mrYMQlg10sJDBb3IyJOOc5+CviWrZaoMAHIdpYqECYS2qxnaEiXdGotNDTvXRIICtP3Xcir0MpFFL+GC4ZvyWcycdGRUjYsZA/WCrvVO/fBe0VJrgo/k7V5/6xiaMcKPY+5KOdKfnoPr7yV3aS0DcW6ISmTPJJbZdR3sd6PMaSyhlPEmpZavyb3BMdUj1jknCft5ERHiyL+P9uEOK4JsGNymfK2Ymd6/GXj3OPIyqHr6si/Z27I8RZ6xb6ME95xli0b+8QFT8f359c0B0spuKRkfkYin9dd5mBvASptHKzsovfyyF1/eQ+S5qydLUQKVOlsTWV9aF4v0Z6MExH3k8ZV/4d+9fdVOXyXK9rG6Cs2myN9bkZoWISho+Ue013dcDzCDig7MfoHjTGBzAw46V8b0G5NBd3JiO1xmj06hwt1wcgvX4N613bTH4tyrepjWIWyv4dbJV9HCQLsljDY+1stgMLNvZUjerKqP51Gn9mYWgClW3peZMj9Q/60XsotdX7vqV2snIDS5fsMzE2hqNjKrz3x6JLR1l+Fqg3JmNdGhQYbbwsO1Zf+jvy2S/fi7PSp4VrqP9blZfWX2Kb2yE3HtXcB8lDzEJuXdeijeRPRwgewQWg6qKCw3nAWPHHDknQtSbzQQ7EXIj/tnNtZ8IdPOYN+5FGQTuaqPoQTaXZfJU1rAGYSqogzf62QaKMzxs2Rd2yshWoVAL7EcRxRG4BtJJCyisGAa1znGaqlgjXouUmDUAACxHBKNbQx2jPPgQOTuU413QOfKyt+LBA3hSjWPPqgwP5INq9is2IxbQ3ZEHPGCQbSd2l+UADbDrbCmACBQ66RgFbJ1vQBUSzDDBDF3TNuZaDhGkNayxubG9iMFVX7Bfs2MRbG05JxmmemP/oUx7i4lN5HnI2u/ZiGuxc5ydubZXe2ggM2TqCQZILTsB91Qn4gdjsnri1FNkdCtDoXCEUYC2bintZV2yizgBtpZ5I+4C2vCabJvrgM+COGnutBOxLGrqq4nnxzktKuW6343eB24s+8RzUzcAphdlsFVWogMDB/9jdomY3B3nX4ljEMQQkDzxJXAKlUs7pejKF0Rpt06CJPuBidu+9S1YZRAIsCagF2DvXaeqV4ijkzjk0cuCjAHkXJBq4glhWGDncUeyPDnQHWWJZYkWrHCO9x8MgEB+0RPcKENK4Nm3LvudqAi0R4Hl9aSgQJnUFoJb3XQIcas3B1eQAzwHJlSJ4SntU27A7BLsH8Hqsq4ls4iRWGWv7Ra9HA6Gi2LJHPnl94bH40ud3BnhnTR7yJ7sDgewLWVZdYisMvz1j36b1PuCLhAqVNTLsJUY18ePtxMOEnoSQfbFZSMj9o6lfSVbrMJ8q9YrEAFCD62uJ2ANhY431UKBHjJbrOJVf1J/xBb9kbFQhneeMdiyMzwBlBVRR/8Bcfy0e8voLnvrz2GMzm0tubLiOKC9bNEldZcV67C22kfvMIqV5TibYi4TI/ssxXCzoas6tRcYkPiqvRJ5Bo0J90dyI8J7P2jDXfOkC0d8rVFn45qd196Zy4PMtLyrz+8+MvjkZiOi/ikS9svnDvcryc9XRZ/K+bABA9pdR3OY2MyUbLr9wM19QT3+9x60sv6cwasgxVsWi79Fm+1vwJPUNvksj7S+6tmjLTPPfB6RjH9BembE1OTa2hbUajawH9MYtlll3U874B4YHCflc9urLD5f622n+Ke73US24n/rXq7N/j7Lnx/gs3vm4hqhvy7foZe4zUnxBdtseuGHgbSvLvpsAa0CrGiQ+oTHIFSA+02CNMwvXvCKjJswYBqaeCAasofTOQ2mF6XQlCcpQCl52OjZzt6JlliBhDkmQNtaislUKCuW8gw68Ckzig31ffWCfcw8ks+Q0niL1OeeTdl0poFIdUAf4kEGlHNokQakYMGqtoBWbMvsYiGy6BS50vq38QWBT2RoqnbSRBGHT1mLLSoVAXZ5o570sKpI0U2wRYLMI3DElGptq87NaK1hds1m5D5i7JvkRV5LD2lqLyhnRiEPM9EUrKL7NXRRYBnGVmPZGk2+lNUgOEurJRMaxC34WhQVj45xI/nIQ5g3nzw6B85Erw3OhQQlYWcvrz7kWzbxJabf4gIWB6mQygVJsxg4AjWvS7lBXNeppTEdFMIYH3QcP7whN07JJtrhCEAjBE7QFJtMprDUC9nh9TyYTjiEgEcptxVpjKLaq8M6l2ARK8RqerqzAO8ltXot/NPjgwghg9hIIjiAZACTdno7mwgJEY6YA5xx0XaGqOYhf0zbJpz42Hq0O2raBb72kXDPyZneHDFqzW0DrHCiQBFaboGlbCXZGmK5sYVN/8QP3QSwllMQhIEq+41qzJhziix7dKuJe4jxgKgKcQhsaVHWVHb4B2lhOv0YBrpkDcDDynkeLjXgYBOK4DtrwYUX04XetYzePuuL4B8RWHypw/5WWmAZaix/5zlLa4YcSW0HZVzMX7vtIdPjU4pblqxCFy1IR2P9Kj9c9xA1DAX1NTLGQ4X4pgS5qje5m47P4uyiRp/thmqlLG5anKRoVslNN6/erExqztGQDpjqAlwSDJMjkks2AiU6ayjDiWBUEjE9GqiPOXif5FhrlKJmgO5DIl+vCOY2STD7U2QNjQslA7syZj/tZ+TIAvd/5r0W8EaKkVVYVl8dCBLQBwMP3VTkvqR8jz661qEdV8ll9GxLqRlw7RqRbEraH64QvDFfxGDrZ+BBtjvqzuU4rg4W0YPmjnGqgG/JcEzhopZvetbnK5j8HyzkgXwR2uvp72S3G2lmDxt6BseeLgJRjba25t/bq6w1MYc2yAZ5jpQVgGtvg1pn+AXOj+2yvaO81HX39F7yWi5gpDmCybT3/PeA7a2tgIaQ6a7ixdzs39x+tcxGrcV0vGONiPqSifB/PPnnlo9leSPml3rcrVdXbmvuHhgumr6T+ouw+IyWveZ+yee2P0drOYF0Zrm897kY2qHVo48Bb0vlEE+H5bAYkAZfY9CsK14EAw8G9VCC0AkJiCqAI1oKYhRKI0//ICHnn4MUsup7UEvlYoZHAZ9F/NHUzAKQVoNg8ubYcuRkEMcFmsMr5owPnDK51Z04c+HplKyitEMjDBc++pjKsPnAKsyBacm0MVCVm5qJJhpQDIMBymnIwV6iSFjMBBtFo8kdSpwjNRAApyW0Oye0NAHIAEAKhnpgEniJYC2JR4Dyb11e2QrUiPrNKoQlzOYRgE2tOYdamIF2TSY2qqqEAzJt5SjcWc3ErRFADSdXE4Md5D0SrBPGZJgJWVrbwWCndRbf2Xg4guFzbOMxW2cQ3mkB776A0gymlWWPuQpsEhajRdI7NuH1grXhMm+a8w3zOKaesNbBaQZsK0Oxr751js3LXYjafy7ujksuC1vFQIACBD1UIyNKVebi2Sa4TWoKyxejrreSv5gMLBtA89wqQIGze+bR+ub+A0RpN08B5Tm0XI8nP5jM5TGBNdlVXfFhSTxg0OwdjNaw1KeZAxACubZOQxQdJWnKuz/hQpLKSj5zTtFXWopJ248fSVvE5z+Pm+YaTQIFGGzTNHKurN6HyE+gVhbkEpyMCyHHqPSuZA7wP8ORStH+lwKb+YL/rtu2i4/OBnpb5i64b0ZefDwDms1knVOkKeeo/gGMaaGPQNBzrwRgLF1q0TYOqrqErnVxCdppUNAwujVGH8b1VJhAQuv8jbfNIK6671mX37TSzhWAk/3YfdZa8VCaJ9rVcfVBTcBu/kOl39HHNvqw9/+5RnkbEgmGEb+oBhrVF0cgrF+n6FEe3uI/+/SH1BYXU6xwr9b7wY/A7N7vPgU/ioBjTUpqJB5t9HrtPP43dkLVUSsLlOopjMZTeyrHgeyysqd7VKEDldY4VKNdS5DDxo4ZrMF7P35lSsJODaaJyCuL/VL4WMCAV1xfQm63x8iUv2WUV3z2dPdeV6mOJNBZJ0hyf1bzLY7z0eiL1Z5J/b8HkYDuuwSJbA+Xz3xsAUZLkVhnjb+CuUm/c1n7di2fSmPYeiTNcCskLKhxdJ+UfqZYclPQ3zJFn07auxlvvTWkqnwAMUddHhbR/F8unjzLGupshIpX3K3t2ABYXv0bjL8UiGimn1OJ7m8ArJR8jz+Wm0GN9A7pXQPWur9suurr7/BRJLdbiGb25XK/za+xRQ6JBgaKPI3v1wjZ6l/MqxvadvBxhOLZFXdl4DEzj80oiZdsdUK7h9frTHZKUL+WQ9WzhxHrXVaLsPG08j7cArsicJ04hFP2POZWRl/RSrCmOkb4NTKonaqC9j9pFlVKDtW2bTGhjlGxLFnVVQRkNHTS8kjZsldJ6ReG9bVs0TQMQa7i999CqToMXo1YTOn9hKA0VOEjbfD5jv3GlOp9XbaA0UFcTtE0DbVjTHcABsZRElI5D0zqHeTNHJeb4AIOsVgBdXdf8URQAY0wHTI0xWDEGkgKaxzCEFBQummJrZWB15gPsXdIYwxrU9ST5m3sX4BWbG/vWoXUNjLFYWZkmk35uP6ZyY3P5KoRu7SvWJrJ5+RyTegJTcaC4mE+7aVsYw+bBrWvRti2mKytdnm7hzzsPRw7Oc/5qIsgBgqTqcmyaP5lMsLKyktYKR6b36YXmIGvsm08h8PowOoFm5yTdndGwmjXbrmnRtEipp1R0DQghmfS7llO0ESA5qaNlxyq0BIqzpkIjUbqhO/9rrRWbazdt0sDHnPNNyzEI6kkNayvxre/MydumBRmDqp6g1koOEgKCRMaPgQQBoKIqBV4D2MRfe8AoBSAASsFMrJjIN8mNIh4AzOdzXkcSkA8KEmlfFp3IX3wApmCsRV1XCMEmP+h4cBSD0jnvMJ1ugbZ8KOUdW2low5H23czBbDUgsFm8cw5VXcnBlOKI/JItwVZswQJhhwP9WbTeYT6bQSkeRwqEVdeyht5zZHyEAC/vYlVxfIRokRMPuVjjzu4nSszfYxq4naWY7iiuz4FsHm9QP81RvyJIPeVXK3mBjuVazb8pheDYpb4icOrEgqW+UNlnR/UE795HqLOCGbmdo9dRqS/vgMr63Sc1+DnCRVcy006Wd0u5aAwopXKLbErjQxRHt2ujLwuX/FImkZTzOgSpMr9lN1LhErIgA/s5T92kFP3PFkh/uom65zohOdaWTXB+2FIwWRjAZyz0Ziu9B4vmMQo7MjJjazKhtbUkxmxh5u9Mgmgj71BWqrvXjXjpiZ1NRnw/RQjIuUprss+gAuLpaHd3TAId6V7ap2ngM5pPnSoxtMiRQ14SaFU5J92Lpnpjs8sU3wPqzcV6pIp/xm5hTSCzgUaGc1fWqkYKDvy5s2dGmxwBg3GpDdrfALPpdaHetV57I6twc7SRB9Yrs8tMZM/0F318rfrv9PATku29O8nLpiZqQZmCgc0szmxf2+z47aZXuHgXs622f9AzRvGz0O/64JH+e6TK6wPQvWCe83Uw+KaOnt70+RkTTNJXoCu74Y1sE8C7bVmADxRgJPCVAqAk2naMXg2Ilk0AhakMrK66YE3epxzaUWtqtEYl5qINkAKiVZaFemOtRM52MJqjIleSt1ohpmgihORXzMCF/bdZmI/a2EABvhFf9ah1Fb6i4F9VNaxlMB01f0ohae0oE8y0kn5qiYANSgHmtNNpV4xtpY8NEZQuP4QxajbE59yJyS8F3lW1NmLSzPUGAfpaKUymU5DkK/fBo9JVyqUeg5MRgNpWqCqLejKBQvQV5jYUYsAzj7mknSKy4j/P6cCC91BQqAAxIWf+65r9sL1E8YbUFXM4t2LKzYCXx8S1Xfo5Xg8SrX06hbVV0s4ao1GrGi26XOlxTmV0xfe7ScJPXVkorcXagjhVlNbw3nHQOvKYTlawZcsWuDb62gPaKNiKzZu1mIDHPNhMlNKMIQSxMODAac1sluawlqj73nPE+0CszWVT/S4dlq7YkqRpGsydw3TLCqaTqfTTMmh0GtOVSfJBJoDT93nPQcsoYN6ydp3N3TXIBQRE6wwrB0lBghquSDA7lWIvKMVm8zFlmnMeAB8eaLFyia4TWg4ooi+51grTyRStbjliuTYSWZwBfasbzFZXJSNBLdtVlw0gKDAYbx1m85lYPrClR/TNB7FFjLEVtOL9pppYSRnI61MDaH0DBI59kKK5UwCID9qMuGaQZgE5BI95w7ET1K65eA9pAZgt7qfr8jtFNelJDdQr139+QOVHJdt6hsytWc861H92UFevv4uEprx4prYo+E6/uwujH9yRbvUFrOivPUY5C135tLUk8NhNQx40Kx/5+Et1D4+02qVu6sOH7PkEpPPPfc5c31+4J42rPEFUz9Au462QhlKzC9ZJcqbrOCvvFZ1M/0a4WfabRgpnXcmlrTj+Y4cA+fMD1Wj2PvTfoVR9770Y0XjkMzHGcr/ssI9xTro1M+C34HvYejdd5dqIf8Q1nNofkx2lorHrQ4C+G2lMiE3XFqCl7O4e5GwhrQUmFvGzkM+xPa/3Y00rgM0OgFrw987Wt7M0mLy153pDk73WK7PWs/mh1GZol8eq3+d1KtzUOtgAc/mnIdtSN/r4QhoRWRa1v9lxH9WKL2xn5KOTb4aD53ovR/4t3MCALKxqHdo48HYOdVXB2qmY6XpJtSQ+10SYTqfsb6k64zb2gaXMzJw12yS5ro3kKAZYQz6RIEcxSFbwAaTZJJz9yrv82HF4OO81pwGLAFoH1uZFIG4ta7I5F7Lkug4kOYlZGrDiSxyIMJvPE4iiQBJdnU3kfQgMqEQzF31KV2eraNsGdTVJAJPAebytibm7JbezNlAK6XAgBI48brRGI5p7770cdFgoDXB2KIVoQKAUuH2Ag4hJKrRWzIu1ZlN+CwtYSOA0I6bYcwZUYp4OBQlw1YiPOI/ubD5PwMxWFmYyET9c9rGOKdis5Dz3ns2AJ1vqpEXVWsFYDe9IcqCzb3fbtAiuhbIKymhUlRzicOJrMf0lNPOQ5q6SgHPRogJgVwFSgA6efdMVSe54gtEM6qE1NICJmItzsDrJaY4AcDBzyZUu2tFAmM1WeSwIksaMEhCfTCd8oNI0HLneO7HEMJLSi3PZG2tRG8sRzQ1bWRDxgQgR++tXdc3vu/TbWk43V+sJomCilOaDFfH3jz7lirqc3baq2V3CB3FL4HUK4gj2rWd+bMX51jlCoJdDDLb0cADgHCpbJYAfYyhAcVDEycqUI743c05hp5Deb9es8uFVzfETtFjGxM3JaANY1nIrrWHE3cQCmGAihx0ezrXi/sHWMV4O6ZQcIigNWQtOYhg4KM9jZHTMac8AnpTEQ1AqBWczhg+wnJOxs/XGd84eDQUzBVK5eXhpmqziC5xfLSrpies5hhj5ElG8nplHKcrBTf+Dl3vXdvUtBKsLO90Bqe6r2vlYx2+Zyh9Y0ABlXVeIh0wdAE+PqTxgWl8+K6WJhQAijnUuCKQD1RLQEwBo1WnCB+yP/dW7LhV2wbky6acnK3SK5AjMytRICqp3PtPxO5Cp0kTkXKn0cDcM/XHKaikqHRn1MUk3jlXUysRuFo8tQATphGPsdj6mi9aTrLYev4M+DrTmi6XC/P0dNW0fG4ORWkrBrt969qaovFS/DurxqkYmvXvnotxZypNro5p8ysfOA3YLqW5prs3NglONVM3wybyrtOjGerQ7AMnO0M60u7M87qY+lrvuBttdr83dPu4Zd3vyYGnQ3rCtwfel/1h/4Rasd5tDP2TIonWveuXH6h3wt3aRcZ5H6qCxi+u1NTY9G1yrXd/V4GJmTLW4/gEPg11knQfXpw0D77qqshzSBkrS/3D04S1J+8UpfRhYzOcNmpZTKFUcVQsA5yEO4uea50lm81vWKltjxYeXTXLZv1Zj3jAA1lUFItZW64pNjK1iLVeMypyCVilOwRSBdGVtygseI2wrMKCIQdyUEg12lp4LmaYzAmoCkk8pR6+upC8E0iQaaiD6yMZDhggeiNjHl0QT2oi225NHXdUwAmgVulzjccwiuDdW/jWsaa3IwoeQwHNVV5hMJrxACPCzCFBjWjWSNlSKgB2jPDfzOWA4ovwEU9HOsqlyDOBljIGxlVhCcN9Yo6tljls2CzfShvRVKUgUeS3pxgwkEx1MZRA8YTbn4H1GzMhZiy8pv3yAtRUqyz7PflKjmc0xbxsQuvR0tRzYpGjhRkMrK8G3Wj4Q0Sz4VJIyLKZjqwyDMQV0UfCDh28dQjAgw28xgQO4aWNgrBG/aTZrdk0L5xtMJ5P0otpsDQbRPhP4UCe0HGHeVlailPOBjfcOq6s3gQLPj7UW5Bk011UF7zmAmhLeCQTyAY2fwQcnAfMUbCCASHJzA/NZy+4RtgIsu4KsrGxheU54iVYswUvAMhN99BtY0+XtDhSSQOzl/coPwIzW0NbGpYgYrV+J1UY9qdktwrPGPYCgxTy7Euua+XzOAdikzaZp4rByoMV6kpm6iztCis2g2Rrf2pQH3BjeW3YpqvnIh3xoHj622wsMWiRo9/DHIgG/u5x/bCLE6q7l8Fp1f6YaMlF+4Ue9TzHpVc/oW8BWDh/Xpj6OiXWOtb8AsmW/C9gx2l480EiHI2o83Mro2A4qin+oEmQCyH3YS9A20gt5sOwDBt/+DnNR8Xg6NCACoKHUglFTGS8Jw+WSTW/yJV7AqGDUA098CJT5QucTqPrDSN1EZP0veV0kocYGewcYowKaGqTP61WCot8DHobvb/93D1OjC5qUXddAcuAt0TKi61f/bSr7CRmP/oLI+IgHb/3ujm8kGPZ59G1G/wBn10l1JvH9g51iDkdE6YGZ7bg0PcrxcOjH18568vaeogXbDIA9cxiwG+oaHG6Mf+p2kf8xWLkTFe3OsVtI5eFa/6htDFv3igzXaay52GjKhRstmXpG0OOHZ2scfuzUst/IYYqU639Ko1yi0v97XIzKV2vLBIM+5BfX5LW/t+x+2jDwntTsrzqfN2x2bWzyGTIirBNFEOJB4mhZZamFFCDBlbx8gBV/dwUg+ghyxfe0dS1r20wlEZ+DBDczAoAppf5yrk1ms1psRlkLyFGxSSlJucW+4wADMN96uBAk4JfCbD5D8B71ZIJJXXOEaYL4AEcNbwDgWZ4KAY44KJfRGlXFvrVNM0fTzBnMifZSRxN0pRNIdY79gQMFWM2Bn7QBpnLIQcRgxolfcmUl0FU005VAWC60aT60sfChkYBoSgJsUcp7HrxETxewEaNjTydTVBWbN7u2haksqrqWiO5ODg9ckpM4qJlNftVKAscxYLWIkduVWCKQRE13Lfc55g2vqoqtDFZngGIwzkIwRzk30s8oHAciaFkf8/kc1nLe7mh5oJQSf/YWWrW87iyvAwbMAbq2sMZg5mcSbd8UJuQxInmVDpx4PQX5zzsPHzzmcz480VpjMp2yT7o8q5WCknzcnOLNi1AaTbT5wCFGJJ/P5vDBYyIuG/PVORrdwEjQP601JhWD9/iexOBqnO+8hSYDcmLCrw0HL5SUZ6Zm//KYn72dc3T71rUcWE40wdoaGGWSlQPn755BBZKUdoTWteybL2u30jW08cmlgdeJSWugsryu+IAJAPEBmQJnQnDOYTLl9RfjSRhrua7WpZSCUavrnMd8Nuc4DYBE+ZdDQa1SFHhA3lHvMVtd5QMacR+p6koOryCHMbvJ1nxMJhgBKvm9geAyVknvYzDyeRphZPzznq6qAheONpdDko2A394nP/0YuHgXz8d3Vw5k5FrBD3U6x6KGkQ95Ako0Un5k5Dr/3cUHBGO1dL97ABhUYkmlEm4aRDhK18ZEhwUSQDInGAoJqb34qzgAGJV4yr9V8U+qp4yWP1woMfjeGL6LPAEoXLW6hZH1NVpuZM/06yuv5aizd2+In7MhWbCCE3tdBP6xusbfhT44jgXkJSvma6wyKv9dKAAufuvjQVLHLq8lQjZUYy98zueCmnc3bQA7ZyVpeD6SHsz/Ha29vLRo8xzZR24WWtT3tcZko0BnLVrr+Z2tXy366vTr21nmR78CG6B19tRd4GVRnZsLTziyo4zudztR5ZrretFXfRO0mcd20/Cvx/XoK76O0JRkog1x0KtsE0trw8C7EUG7slYAcrf7seDsk8jSpTNSyRy7jXmgxSQ6+lNHjpXSqCVQWwQ1TdMmM+hoXh6DdUXh1FgLHT80bctgJBAH0LJs3s3+mwqV5tzEzrsUYKuuOABaDNTVzJtMY65TQKqYvggq0/6rTliM5aLGeFJPRDPnYJRBoIZBow+AAYLjQGXRDL6uJsg1EAQBF808gaLKWOgtK5wHnABIADqOxs2HGdEkWENDGZX811mLrZLQw2bgbLZb15NCa2mM5XRoIaCuK2ijYRT71rethpU+kvj2K8XprbQAHM4tbrhdANYazOcNB2KT4HXTyQRVXSV/fms0GhnfQATftsliwSqF1dlMAt11GkJrLBxJcLemQSDOh26NTebbzrUcrM1MOYWVyqK5i90J555mTTWIo+1zhG2HmXeo6wmCD2h9i8rw3NvawtgKTg4tKrGs0FoDVQXfOgnWZ7BlyxZMJxPM5k3SCFN2yESS93xlywpc270nZsJ9gVbJz19ZDUCLm4BPB162MpjqFT7YCuxuAGIz/xTrQOIzaDAYDVpBK6BSFchaNLMZfAiYaA0HtqqoVM2ZAepa0ntVaL0DBYC8R0vsNgKQ+PxHc33IeIovt5iaB1mvbTvneRC/e0BJhgPw32Lqz0HsKB2MaMPR1+NHNBCgrZWDHj4UI4lr0LXH+ej5gJAtDdqGx9laKwdjdiePeYfUN3HNgUEh/MqVCFfKaMXjAn8fjOYtpbLFAzn4LiOTq6zowvBg/XK5w3XBXK9eIB2CjVY7qAdF2UEUby7AazfJ3zJmOZBJzJbDUEKQyMOIcJPyfY4thuG9GDW7760cNQ8FtBuzc0trRe5FwJbhn64Lfb/oUdxdVl20Ect3cxefJ+GvD9a6XlPq+Rh24SU71AmXvaDYjWLV9FtUsVCv/qGdIHWVpd/lYUhuxZC3W/IeZ5XSfI6tlwGPPerznVMe6HD0/rgKqLeEF4Di1JluMfChU3nAUbQwEpwujUPW6Xx4NyhT7h7qybSjHFBvuPuTtdF2euc+G6aNgOVFB0hjfGzker+/sb2xsjvRp6JKVQ7p2PAuGvJF07DofdoZ2rnnxgek3OEWl9sYN/kOszYXo8t8jfL5lfyZtc7KNtLGcB5G5IFNrKe1lmmUefI9dpyX4dekz13xlVD9UlKmv22uN9iUdXW0v4s53ihtPKo5WIhlM+xolslbdUzDY61Nmjw2eeNZb9qWzUmhxK81gj+dfIO5DpOEdxChrtnkPJBo2kKQ4GY+CetRQ6a1SkHfuL5GNLqskY/pxKL5OmsCwXm9tQYC+80aY1CDA3tFgR+KgTwUktaW/adFE+rY5NpnaZiqyQS1pMcylnNoN02DmDbMCf9a6WQy7hxr9qwVsGsMptMpQEgmyRQ6E+/5bM7gzpa+5gQCNAuWleXIzlBRsGJ/89Y7KFIpxzkDI8LqTavwwXNk9PjBlo9GCLwctVEwSidhjXNh+5S3u65qkGbLA7YYYH/qyWSKuq7QOgcoPsCArKsYaKszkaekuQyBrQZijmYQB8VyEhhOyVxUqpsTHfg6A1MGu00z52B0ki/7pvkcgQIm9QSQKOnGGigJ5qUqBoXBezaZlgMRAoPJSrGf9yTUIGLwP5/Pi5RxTcuWB9OVKa/DoCXnOuDaBl7SbvHhg4UyHPVbgfNvV3WNIMK29w433nATWtdiZToFg90qAXlSCt4juQooaLRtg6aZc2A+eT+d5NvO84ZPplNMt2zBfDYXSwSNup4kjX9V1awpV4DVBjAKM+84aKL3UBJXQGmV3ungPWLEMu84f3jTOsxmM7QNZxDgd4QjkIOUROf3nKpNrCes5aCCXgK/VTUfBqzOVtHOGo6ibviQIvrAe+cYpFc1gme+ovsE5/LWad9QitvlLAs7R2OAOLvDWxryIt1HJwnzuTYyV76rfGPvRP88E3IHnvKPVh70i/fjAdZMYESj/HiUnaHOnjy7lwMUnV2LQGP8vD/lXUb2SDZ85cjk7XRAossZOvJxjoOgsw5iRJOdTHIpVc/gseOPBg/l/UcadLWwH+VT/YRHZTkl3BCg8oy/BPTdFRQzl1LYZbJR19aQ7761cApUKQ+XwsZQfOpbtud/F7g4XysJewyzdq+FK8pr5ZPcBMfpp9xRuFgRccTL5/Jh7eqWMRxMuBoMx2KBbAFRBPSLi+QHUV1cg5xzedeV6o1HwWrv93C8ZcAWPMD1drzmxxILiu8OGq13VLTmK/3Tk/gHjT+3ofZ3Fv31qQ9OdnXMNtKdRffWWm/ZPj3W3KLffdrssC3+wmyE9tQC3H3T3680HmD0b/SBZfclGudk/WWweCddv29r2Xmt0cQaxcb4zb/mOdGgxDjlr9YurYT13pfsc7J2ru+dXzUbBt5BACcHrGIhzrkm+RtHH9oIpjnlBX9sG8l5bCsLqy28CkAguOChPJsrq6qGC2zKrg0HAePASwwM28YnH9OmbVIObqWmyUS4slUCv0Z8vDlwWcvmzCGaWBsBqFH7SGhck31wOpNlJdp79q3mYG+slFBQosmLwJn/02n+tOa/tdLQNWt6SQAPuBlAAjw187kEhbLpo0ugzkKNWIMPhZRGKh6CtC0Dqbqq2Fxdaw5OJXwbbTiwlkR3DgLco6xMgaCrLqd4PAyIVgnxehDNpgLPKc+PlYMHKnJ+66BTirh4OKCNhg0GCgx0lIUEteq0jSrOn+ZyRNyua1u4VZciyytZW0ZcGaIJcoywbyTVlYMDFB+8RFNnW1VsiWAtaiMRsMVdIB7EBPChS8zfbgWUQylQ8JyCLXhoiXSnFGt4yXFMAhD7dxvDUc+beQtr+R1hLX8XWNDIWm/bNgUi44MlEusODopWVRWqmte4UppzxVc1B8QTLW5M62bFBSAe5ijLArDz7HcdfcfnYq1iXMt517dyeq1ABEXEbgbo4geEEND6kPz8J5NJekd88KhVJXyzub6S/SB4j0CcBm5FK9SVBWJGANmFAnXxFKI7hK26LALGGHYtaDmSP6gzMecYC5CI7A5VVaV14Z2HouiiAFCM0G4sSIRQ59pd+vpGX8VcYI7VDaJQg4oNPX2kItjrgYVSWO4/1QHHvk95/gmMYCEGXCs/YsPyZSiqDDxlXycqyucdAQrAk7FO3e2CC9W7P8D44DEorAKKCjEcs2KYerPCJx1d1TloJpUA2Bhf+QUVpSulsinKoobnY6Li3IxHkk4VFwgjb7RXoerdyngcBeD9elSvzFrSTL8zYxX31e+9ZwZj2K9/tNphbvF+V3qrfEjrAhMZyEEeIvlb9TvVW9BSxcIARutpOxMzPHPFSu0twKImyp/lC5Txkd8pXqb0d1awGNABst0tlF65ESCykMa2vTXZ2gTPuyrJFxvVonp2cQx37xQk2iNAcwGtD7i6cjcnX+O02QFfo/wml+JGHhrbQ7tDlE02WtBg09rJega750jNi+Hszhz0pO/+LpzqxBFkUWuth3fPfG8YeGvJLewNB27yYubKkYxD0iYr0XBVVcXaSpKgU5MuL3bM6j1REL9Kgg8EBElvJQAqeA+vPZt2S7qxqL3kAGwGRoJYcX5o9jfViAHeRMQkJA25tRw8LQQPoy2q2iB4joIeNbdQSD610cQ8gjatlOQDj2DNJbDAJuUBioDWtfJ9U2glCrbiKGvgIGbcDyOm8ACb6NaVFrNlnsXGO0lDBZDmvoTA6bK2bt2K4El8mZX467qUniuaxMd1pIQXgFIOZoAPU6J/tZExNcakoFVaTzgCNAEhKCijYAQQM4DhvOoA+0E3TYPZfJa0+8ZsRQT0zrHlAlQXibuuJ2LRQAmIRk1sNEUORGxyLWNrDQPUWqJ4AxCNaEiB25q2Qdtyf40E66MQAMsRxlFP4IPkxhZLCy9zbZSGpxj4jHPTs6UAu1AQODig1oSYjz5p7i2v/bZtYWyFqlKiLVbpQMoYw3m+xR/bKI5roJWGrgxWpiscSRkAeYENAnSNZA4wKh6+sC/5rFlF086xYlZYm+19ekc6/ngNNfMm+fDHgwr230YK3Oe9x0033oiqrjFRbFZPiv2hleP83tEfn7XHBkoRXMvvV2XZd907J5YGfDhWV1M4CY5IxO95fEeiFYpSigMCqpgFQA6xtAG7YXD6tHpSQwGcmzwA2hhMJZUgH94xn3Gdex84gJ0P8G6OyXQCayoErbG6urrR7XBASt4vIELS/PMSAeP4J6cfnbnzTcYCwVAnYEgjz+eQtgOwvH7yD2HHUffJLkXuHAzkv5HV0P1dWP0OmeYrNHJfdbwUgF4h9TMmJc5zCkdgovr1Zb0r/hUG+ePaeyKqa0c0nun/sa00iCUoS61l7cjC4LGn3AA9m7+Mbxobx0yd3a2xbjUtEpkGcojKWM9WIn8j+6hupNLcLr3s8GDkC8SXAPgawevGABZQjG//nYiWCaPeD2N1rtHMoF/5O5jxny8AQsl7ETl8bGIys+NYTal87uaZi+WoflGHyr0if9cX97pXBVTvzIHlpjLg2bqId11aFJutv6us+fyusbC44d1Z7+7kc0/wt9kKpT87C4p3djgG62L0ncoKb4qZPTawC9rbaGGs+8B6BxjDq+uFGN0ggzs5kf3Dgk1VsWabNFIk20M30ZAq/s4ThI6V2Nk3oaMNA28fCKGZJx9tEAcnir+tsayJlvzHRICTCNSsBZ+w4C/AWesImgmt82LqyaCgntSpXiLxIW+dfNjYTJSDlQnoTn7ODJSqukJwDq2kMqvrWsoEzjVOUXvrYUkCw4GAwJ8cDrRmxCxV8lITm6JFH/OYPzsCjxjwqZU0YBLpDFUd++LgnfjuGv6vdQ5Ny+A2jyTdNhwojv3MFZrA4E6TlUBWHKwuBJL+VUAA5oH9qK0xopENkidbtLKaDy+srVBXktKrZe1n0EE0rh2IhooaRQl2ZQwIbDKsq4rdAxSnh4pRuuOBQtOw9paBMWv/OXhXm4KRaeHHNWwurLXCbDZnPiSAnArsr2uNRW1rxCBMBNYGz5tG0nyFZNauU8opnmfWkvNhhJdc1Qpgk2Urc09Ih0bkA7ys05i+KgSP1bZBK+Bea43JhM29tRbQ7j0oaHgKCF7ahsQUIMLUVlAGYiWiOPd304hZdoA2BrbiOAQxYBlJXnTvWrSuQVXVgkcoBW4jSFsxyJ3ivPIIXc5x70OKWeCcw0033sAB9AScumglYmzaWNivmq0lblpdRdu0WFmZSjTwACtB4BrJTx7zls/mq3z4JWOnxT+b7SXYCiK6RYBI2mU3AwQ+0IgRftnSw3Za78qKRpxTAobAgeOsxGjguthNZDZjIF1N2Oye9xHhVdwTgg+Y+3mK+7CzVPgnJxDTSSvp/joAJ4HPVGyAEMt/KMLsgplUXC24Hp/t2hxppkRQg/bHBOk0DIV/aVZhBHDohqisvcd5/1BBjfA6Qip7PgepnXk5OkCVxkX1h3ccKKZpHXKg0lO9ec7bGFSaATKZ+FGQUmIseaRYJVxbbF76t0jsKkZyFGCVz3TLOqsvn48R2YRxeQm6x/iJ1mYxu0bHHKX70aS89MmOc5tZGSjKepJNKPWYpHJsusMYlY1JbKfztejj824GqH+xHIzUdle2eCMTj10LxZild0d6G9dDf6+QtvJDhbG89Um4jM+n8xeOmpO/u2vZaGyYxpdhxvcGBNoNCNTF2cdGpf2NCOqLzjLyNsa6kCGPwVlGTrthiDdNG+n3Rvlaoy4au9/73QfZxe/dNTa7Ws9mwedo2Q1WsGHEOjKIvbJrv1k7gag3eaDQtb+28fbOtrfo1YwX1grsOkbjR9q7jzYMvNl8ufPljgG7orYLkMBSrMyEtQZaWdawEpKwbcTMm0EyP+dEu9k0DeqqwgSTpHlz3sG17D9sq4r9lXWMMM1ggn3AHQIhCfEKEfsqCaAm0bQlr7LWdWq79QLEbAUjkcdj3m4Ce5J5RXxfglk5ORQgQHySQ9KeKqWhLWulYgC31hn2g1XijwsgRoGHfNhj5G4rbXjnoY3GysqU+yLpzjhPM2uW2e/ZypiLv7vRbFoLCWQVWOtbVQp1XYkfLR+CtAK+jNHsTy6UACx4PgNxrmvWcrNlQTQHbqgRM3kOtKa0xnS6wmb48dDBhwRYORK9Z99bpTGfrcLais2yJTe8FrPuVrTulcy9E8sKbUzKee08YI3BdGUKBYXV2SofMFSW07mB/55MJmiaBq5tOe2XFnCvIIDaM0gUX3IlafFyqqoaq7NZOpiJAKOqK1hvcJOk+6oqDuZF0SWCohUE+ywTsQXEdDqVduSQqe3SZBkKaF3DBzFZfnZjGZwbOVBpmybFA/Cec803TdNp62VdKMnrDrDJNyRfuoaCqdiP2lqb8rRHTTMviJjnvoIyGkZD3AcMdAhJcOP82DZpm+MuZ6xFkAM0J/7XdT2RQxkep9C0/Du6PMiBAVuVcKC2emI4mr+sNedaWSO87rUcInjnMJ/N0foWe+ntbLIOYN40UMFDW4PJZAVa1ly0vtgl6n30RsXJPg4o7nWCeQcM+4VzA290wjvRoHivSXRPS+VjwuNm+hKvx2kuGpWLqR4qH1rj66fGftBwOMoiakF/sjzm6NWhuqcXtT/66R35JquRX2MyUDrLSBgtH1nqgbMxOZWy2yq7l0EjpYZl8scyHvKL8UtUMLpgufRpTNjpX+96NJy9bj0vkBhThJyOpzw4XpeyLxfxUI5Bn8mxsR5Zlx04z8qlcaGMp0UCcO/tWSvgVrcwU7UsZ1D5Co12SXV1o9f/4kXtP5u/XCJy9uzl1SJ19WZoQRWjl9cCG9n7txbm3ZDYvM5eFBsZvPLxWj7k8fWN+95I3WvyRBieyy5aUhs9UOjVv954brqu/Nmx/Vf1yvfvL6prjMburTV/iz+Ci+vbyMDmfO5OfNY3w1r3Q5TxMKB1mOqPzUbWU18I2NlxWO9FXbRWBjxusMF13u/ezr4Oc/m1nd8TNwy8GbgxWI2myFELpVQXPRvogmU5SfPFAY7YHH1lusIas+hnjGjezFrJFP3YO7RNy6AtUAI6RlIYzZs5B+yqJwJkNODY7DmCz2gOHSSdkhKtKxSbzROA1s0RXBB/WdYW6lYnf2YIQKqrOpnWty37gyvFBwDee5DngG2T6Qqqykrk5lbM2jmidPS7jcHGIohK18VknwM/Ufd91gYabE7b0Aw6RrLWJuUgDhTTi3kET4AKEsxLlpTiQFvOacxmq2nsfAgdODIqRa1Xig8MArHpNhFBieaY+0ts8SAB1BSUaFWB6XTKUbsV+0rHvMzaiM9+CKiqOpnys6bUwTnmUxvJ21xVEkSNQWkruaSnU/brdxLUL8+Nbo3FypYtXdowzV8ydoPgnNGz+QxV4PzqMQhUoIDWOxgZdGsMnJ+zT7/mw4/glYxbxWbVgJieAzTnAxOjDQDPB1UC5NnEmd8PrRhochT8kNwXoPhVbFsHBY26jpYUPH9VVYOAFIwthICbHFsrREsP9oV3Es9MZZYYbAURPB8SkQRJ00pDGSTQ6RwD/GQqK9pqI5YXlOI8AFAc4M57BrghhJSVwMrhV3TTiAHTXOswW52lXOwrKw7btm0DhYDVm24SNw/iqPTWsna/VZiLpQ3EbB3Ebc/nM54/AegqkPQhIIAtGiZ6inoyhQIhqIDJdAVGLDCgtARdm7BWPeyCcJmQZyfcFul6qQNVxXd1IHxnX8EoeDOySB+HaKFRfjDFDDvbNwrW+r8Wfmj7ADkDRv0PtOrXjXUEsYQ20R+rvL9RAM3r48OrsrWyeirAYTE8I33l6xk/uTBPWamelUBej4rtYsx/uauQ/+pLiOWZeiwzkBXHxjgBrG5NpeGUNVB2uI+2VNf11OHMHUIhC1im0lznS0epjAespcXI15sa3MmV5+OCedeHOJrxQD0qcbslFRdLVkca5LioFjSm+tcwOnzFy11UUbpxrIUIEgdxrkaKEeJ6V0PLcWQX8gEcuEnEWzToWjEKBQiibr7TvfzCLtC6VSwY/x4NYmBsFiyNXc5eo+LmGpSW3FrPLFoG2YZRbKljfMbtXvWWdLyX7Udr8rLW9ZyJNfpQxBDI/8331uFyK7b80Xb6dfb3m432o9/morKLDviy50YulT/G+O9/gBbdX3Rt7PqibWtNJsd5SHuulCdSC7e/NXmMt6k3lKP8LN4LR5/bKC1cGFmBtIbKHSMnlZdfWGr30oaBNwGiiQ2YzSSwkuROTumPjBHz3pizWXGwMHQRzK3lCNsctZgF+eA5WFVdVYhAgc2kOYhUVUmUaM8a2ujfy2DBY3U2ExDMgnlV15jUNbQiTunVthJNnf2enfNQYK20NdG8tjNL7vzoGBDGoGxGs6A+l/zH0+mE83trzts9m8854Jaq4ZVnTaOXqN/ie6xFU2eNBZkI8FyxW6WPoYJovlsBCgBCF0W9JeqieCskcKO1gbEM5CmQ5CvmPMpzCYrlJVK3MWy+7uaOI2ErpABlzrUMpo3hvMzEhyRtw+M5nU6gDPviEhFWV1ex6lqsSJom0kCMcl9VVnyAuauVzDXnEGewZy2bG1eSt5qtJDTqiUkvdwgebRtStO0YEdx7Bzfvol7HddlKarh0eCCHLkYbzJsGFDjqPKfbApzn/NcxJRcHBax4/gDRlsdI9BIczBi0zRxt22BluiVFC9cCXON48zvAAcLiwY7WBtraZOZMFNC28ZBC84QDcrBiOZ+6VqhMJQcS7BoBmbemnbM7rInWERIJXrHZNueDt5hMWNOuwNpx37bsjgHCynQLCCSAXKOyHAdBoctswJHmK7F88PAhwGmDWgA3jzeD+zY0klrNYjKdIOazJ3mvOOaBRFn3gd8LcQ2wkuN89cYbUxrDqpqABXBOpzeZTKE1oW0auHkLIgHYteH85uISwddrVOK/Hk3dneQIr+TQb6co+3LlwnfMPV++3COPpvIC0jLJKtZaCPXFx1M+baoT1AeobnGjGHx55etcfKRHn8uuEcpI7H3HW1XoZAGV4nH3KiPpXglsuH8q9StPi9Zni9cUX+26lQMVdBLDmEQ1JjUuwAQqH7P8Ud35HndjmPmoZxJ2B9kyr+24ZAaHDWLyXExMNg5qTHxQWXnq8VT2Jv83jXn2TMcpRtKHdZTWcdYE5T9y/hLvKgOUZV1jc50vWVV0qkwzp7Lnx0ym82Bmo/J4IZX2ee+P3VjNBc4ajL1Sw1dU9f6fR4Qf1t2t87JMJ2mnWVy8DY1eV9n87yqtKyfHQpGREQBC0on8zKxf9xiGW7PJBe/26JytAwYH12nk3oL2FtXVxzIbmY0BHyON9Mds4ThlfS4HpbzfL6uAtSse+7aMdHr08d6gLMK5cRsY9HVscPPK8vrGFpbq1kVRRf7shhfgsEzB41j5jdBYndkfgwOSteYq60u/W8WWPTKmcf/uj8maz3Vb15CHSAt4Lfo58tiPA20iuBoL91p1ubxDCJyyx7K/MGsVWWiOQEEbnSJKBx/gFJvSKoWk8WRAEv1025TOSGuFSS2aviBBrYhTKE3qCZs/Q8Cpdwz8A+dyhlIs2ynO2wziSMrRF/36G69ns/bpFJXhgGucaoxTlwXR1sV0RoCSfMUKk+mUNehKJX93KO6TIojpqgRzk3RdygLes4l1XUsgssDCP4WAoNCZ1scPrtaMqCHRvyvLopn4nQfPQMlojda1cG3LJs/GSnR3B/IBaMV012jUagIFQlhdTRpJgARoOwQZZ7Yc0FAaEn2bxa22ZbPyumJf+bZpAZBEn+d6AB6SqJ3lwxiC0gSjLAL4gCQGHFtZWUGKgu84/zIFEnNwEYoIya3BtS1atKgkiJaTtFXR2iFG2KYQkpVCVU1SwD5luR6rWMupDPubR/9z7x00OMCZFwuCpmlhjQB9AubzBlYbtK5B8BbxRCFq3kghReR2EgDNBw8lhh5t08CHwD7T1sAqi9XV1XTQ1MznvAZkLbQtp0HTAqgJhOAdgmcfe8573ySTbK0457V3HuQcVM0B8mLaN04XRwJkraxxYHU2Q9M2WFlZgbUWzXzOVhhaI0je7dpazGYzzGa8hqZT7gNbbZD0g1BXNYNkpcTNgNKBWT2p4cV1RWmFLVu2sNtCM0/R3vMPvbYWQQ6VOGXZhOMfNHMQ8aEAEXHu9raVeAUTRBQaPFtgxNVprIX3Ac1shvl8xu+0zpHj5qj8eHTazAG46IER5qYDoMOkXrmvZk/ol/Hhj38JYgu/bxmDMSEO6U7He+JV9dpCVqxPw44Obsbx6AOwfqk1P5SRp5gZIpeaVacNjfXlbeXgQ/Ukpm6MRzs1Pg7RMkSkuz5mUD0puyynBv9PQlvemMqf77TSfS6J+rxm3PT6FIWSAnD0Foai/NAiq3tRtTnlAl0G+grBK15RGSO5JDt8TXpt9MS/XiGVz/fYihtZZGqkrrJ85HnATHoPu7nMy5buDmMLJReIkf+dylKhHM+BYBFfIrHU4zWxUlQ66O/g2YH0u/O0qVpGwFysYwB0kqKkbKOwjl+wkSlgVGPcXx7Ur7xftcqWyAgvAxb6ayD/uz91qrhcGkmorh/5NpJ4intKtMgYQTKdLUlvzNai8U/GoJ9xTIr3aNFCiICwX9ca+9Sia31DksES7z03tCpZzPPYPPdf3UElvbnt87IopkfxN0U5GOXB0xgTBbPo5mjwAo0WHfDU2zGGzS5aANnD/XKDZZAN4EaX4WJaJGWsRTuxz/UHZoO08eBq3sE7FuimkwksGABGLVE0NQ+hM6l2zoNAkpqK/YINDBQptK2D8xxESynJWWzZdDmmwQKJL7fsegwOePUSOMAZ5yyuoCT39ryZwwWH0HAaLSP5faM/bjLJtgyWNFTSukWfZmssHDnMGvbzXpEczCA+fGBzYnAKJ+cY7BuDST0FUcB8PsdsPsN0MkUMzBWlWK05MFw0uQ7eo5nPEYjNhquq4lzdot0zRsMDKXo6AKQ8zZavz5s5mvlcfNclEnvU6Inml/Mzd2XYRJ+nn8fIoGmjn32dFtJ0OhXBTku/5phKfufo06415yWvMhPjppnDaAtSIfnQ8gLl1G2xAWMMqsqyD7BnlwDvOf+11hqwFgT2+20aBuXOc9RrTKLPeUBVW5h6IgHcHNycXRwmkynPszHJd9lYI77IPI8upbDjtrySCPfEmn/v57Ipc+AurTW0rJkIEqeTqazJFu2cLRTqLZOkdY/9b3wDpRSmKytQ6FLOGbG88N7BaI2591DEvu5xLWiJpu+9B4G180opVJMJVNDijqHkPfRyMMH9jr7PAGuh5/M5nHOoJxNMJxMYCcq2Iu91HBsijoA+XZmyebzwvGXLCpp5m3gheb/L1IIeBM4GYGyXTi+EABvjAcg1751kDgC07lwuQgjwkj5sujLl/SaQzI+SCO9sSWAMm49PplMA/O5Q4NRnFAKU5QPAZu5R1RPmmzgoopo36b3eWeoEnhjoibKgRoWk3PumRrDJZftRymPqtDJEiQhLWcqpHKRn7vXJJWggTKL3vRiVHBaUHXlEFaW7Et2hwDBbqPQ4PZMibPe+aH2tFKW0XF3LheDUF04zHgem+IXE0ZNi5amx76tKUlCX+jEW6p4eDmYfJnVx5bu2RmSusq7+n/25K6SzXn8LJsZ6lvV5jJnRGjMNP4CBuiahht7vPFJ6LtX1mkzd7SGOfG31H8zfli5yfzlQOUgZlfYKy4geAkpl1LC/A0m0vzj64x7f42Hn87U1mLY+r2Ok8j9ycTl/Nvtj3fneXdQbgw1K2wMwtlbVI2UXgqSRhgZgrD90Kqtv5Hq+Fwx428zYji2P/utFKM+wRraL/B0tXrfeEh3r0+i497fLsT6tNcg5KFurnX49I3tEv1zq0qJ3O38kf6ZXfq0tMJ1RqfH7fSaTh9AYS4sWZX8OMLK9LGh48FpT7/daey16fVdl27G+vrvQmAdXUc8aY1s01b/R+0QReuMwUmwPb2A7TRsG3imqqMSashLgyYkpdkwlZiRlVi3RvOcSNE0rTu9DgdBSKybZEsRKc65dXemUqxpkUuArgxgJmldtTJnFZtgOXimJAA5M6hohEJpkJh1TQVnJ8duikfzUVVV1LmxKJU2iAqCqCrUEaWKzbJ/yTTtySZhl83k2eY+LrnVtWn3sA+5AiDmrqwS4uTuEVlKGxbzF1lQwhrA6W4VzrCGPhxJxHXnvxO/cJk1nLcHMnOQ0rq1EnW4aMW/mNiPAX5lOEf19U2R3iim1GuYFFrFRbQy2rmyFrSzapsFcopFXlaSqIkI1YU34vJnDiw8yW/QGgIhTZVVbeB3IOLXOJVCYAAV1+cqj8GMlunXUmjLAl0j6DiDNIFwrzRHLBUi7tkEIJkX1hsxFCCSHDw3mszmqukY9mcBYoJnPMVudJcAYc2PzWuFgaPN5I9HnDZvwg+eb86qzq4Q1MR85ARQ4HZitEC0XfAiYzRoYw4cKCITZfMba7ECotcKkqjhnurVJyIy5qpMsSBIznAClNSZ1zUA5eDTzBo24aJDRyc3AVhLdntg6IaZIi2BYa83puiQCvTIawTnMZ7Nkwq6NxupNN4kWe5LcG6yt2EdbG7i5A0EhZKn3KPikIedDgC6ivpb9QGkSYE/8DtsK3nvMZ6zltraCrTjAm1LscuBcm1wNvHNiacN9ms/nkqFAMiMYfg/qKfM9b2Yb3Q5HNkiJhZwLPhCz79wGu48z0kes/HoOolWneuTLLV/8rlz39esgBsmHUWXXMfjaDYSWkQ/e4DvfQyyjH7solGR9K2ClvNd5TwfGy8WAUlZuwHkZEEp39ccmKWtPIRO28g4VdvoZr7kmNtabj0/fFzoXTnLK2ixNzHule2x0M1hcQPmA9DPOR+7ikDderJVcs50NnuoS4C16FuglXknrRPXKDQWkZKYe16bquR30mu1M8PtrHL2/usdKTrv1Rejmr3hqRAiFKv33+1YMXLCbx/IgIK9JpXkhZHXkYzYiI6Y5WCA/FrO7QDOt8r/6e0HsW8bH+LxvGBfvPI0NG/Vwarm1rF1XLLjgVn9Y+yBlDZzXXadhAcorU93vfh15uXKtrsEzMJInvqynqD+VUcWet7il8k5aFxlgG7TXf7dH7qV3h1BO3oJ9cnR+1YIbKgN78bdUkl9LFY9VuUafinp785i/cmvNX1G+N+eD13bx6172o/eNGG28f2+NRa16xfIDiEVGL3kClwGL/TXfG6v0derz3udzjPJ1ueZGUBYYVCsLhfuxZkUL+dgZ2jDwjqbjnEI2pvFBSuMUtVfsW2oSAKptJYIuC4mtmJdyoDYGXcF5zNo5qrZFPZ2yb6tik/E44UlgF2AbU3blEdWNpDEzhrVvzbzBfLaKqqpR11VKWRSDUzUSDboSwBqIzawDsQZ765YtWJ3NMJ/NQCBJYyTmyqL5DCFgPncJwIJYQxUkHVqQk4qq5qjcCoqjpjcNgqR7ilHX2czZJ9NhEFCLv3qgkHKLewk4FsI8gZU4vjfddBOnd7MWSumU0mkymSRz7iCRopu25QBspNDMGwCSZon44ARKoZnPOUiY+PAr3aUpixrM6BpQWctzZiA5qn1yO2A/+i7fe9Tg13WNqqrQOocbb7wBzbxBVVdJ6+5cK8HB+FCECJLfWcZaIuk776F8dA2oUNeTpOGOedCrlUryeUsaLRW1wVpyPRN82wKaNamT6ZRjAoglRzRjrwwHtZtOOaidkzzVVVVJDm+NVsz8G+dSQDiFzr2ida0cItQgsI+0Bue918rAijVDXXPe7ta1CRTHKOVRq9XM+dDAU5eHniSAYTy8CJ7z3GutsTJdwYSmnWm+mJB3wej4Wkstm+KDTeOdWG9454pgZFUWKI7AwRfjvAIKoeI0cErVXXo2CmjnMzRNk9LY1XUNbdhIVGkG0fP5HFpxWjcQoKEwmfDaDCHANS18mCdLBCNWAYEITduCQoCVwIheDvu0ZAWIc2ok1dlO7qEA8g+T7Ee9+wUAGHxY5VpoQLP/gWq+CzLboLb8NJRdKT5SBJW+dqNpxAoBTCEHKrnInYBPDk4zSWXh9zkHsusOSu9v4v6n55TqQEVf6kVPy6yQmXaPzdQI79nRvALQ95Mtrc27foFUN9ZZN7qcyzq7TuW9rNmhcX/ZZm7Z0LcMzlnqS4+FS8HIRPWiAZR97P2d+kD9Mhkgz5nIBJQSoKHzu8/6NyroJKEpSuGZ8Ea9WrM/csDKh0qRp570RXnpvM/dtVFxrC9Bpn5xGb6tBvfyY6eO3V49cd2qXrk+Fe9hHlAw09ovkGbTX1RmOOj+6Masb1XTzUfJZ/5S7WQioDVojR1XlX/GJdgHtesK3hvc1Ps9G+3p2Ka4xpAM336UZu3ZzW5PWKOysaa7pTIclpG61vQa6O2f3Tcnu71g6+1vLX0LjTJf/fq0sNjIDVrwY8SqfrF7zSZ4W7T88nlZM43dyFyuSWPrvNfootdgcE311k3+4Egl63mZjN7Ot+I++B65P9ZOHrtyYFaf1zPCe/cFoeEzm+nHKK01sRunjUc1Jxb0ODAV56COWuuqqtkk2vtk/g0gaTGN+GUGCZwVwSAIIEMgrbsgX85BiW8s+7lSZwIuAbUqa+E8axx98KgrznccNdHGsB86BR74GFE9pjMLFDCZTlNEb61jWqY2+bNynnA2wVYrUwar3vFhwmQiOch5aLwExtKSSisCND6QACrLfqdOAn15CRxHREBgDXTUxLJvOIT3uHgUEIBAhLaZpyBzPnD07Kjdda4V8GSKtVZVFftfE/uVR/P3CH6VEV9yyVWuDcQU3sNJFGwOgsZB4OrJBNOVFYCAJqbhkkOBtm0YvAOorKQ+c0ArpuNN0yTT85UtWwEBSRQC6qqGlhReMYc6IQZiQ4qqDYUUNZ7HmIWSup4kYAcowChUxqCuWGttTWfuTABIQGTbMBCfTGoeH3mjo2Y43ldGQ1EQX3BJAUcBRtsUOT+CamMMAjysZhPyGGTOe580r1VdgRAwW52JW4GBtSbNV5DgZtpo6KDQiBUCg1+ftNIK0Wfcy4EFA8tVyVkdc3mzyT2bxltJ8zabzQDFUduVYXeMuDad4yjkSgErW7awz3owmEn6MCuR17u1rqC1AmmdBT/kiAXsJkEyvpA4D0rSxNVsGi+HDDHwmRUNv5H3kUG9lWBofIAxbxsgeJCXKOVKwRkLW9fJokNpI+kQXWYKH5J7RErNp3YxnVhB3Y7fB91FESFqr4f7709gEr4DXa8gBA1a/Raw789CT/dL1jH5c1HDmweS6jfRAZXIiEo/ciDBp9sZMEhIcD3JdmPCeMFrv/s0+KP7qboPaT+qeex33z+uz+2YYMb97fqWK4dJlSyVAawXzOvCcRj3vM8BxCJZcL1Pe9HiAoE4F/S77lK3Fhby3Rcvu0ZGQWuUgvpCVV/IVGMt5hK/Ss+OaZPGfg0ijOcIYQHy7wQ5KuZajU1K+ugg6zyPC2V1LkYmZfOpX2MyXG+88vR//feleJ2BtD/kLC7wNigZyvigOAbF9Pek/J2kBEg2Us2CZan6f/QF9p1lDGtMn+q23rxIArcj+88icFY02bs4DtYWC/qx7sH2uWvTNFgP6xXtNzcYx/43adEaWI//kUEdsKeKfwaU9sH+tTWeGal+vB3K1kQ+ORvdyNcqt9ZCUlgMkPsDlO8H/XuqV2YhPxv7SuV1qN4iHYunkW6rIY/r7h29iei1UhRb+P25GWnjwFtBgotxt7z3mM1XRcOssW3LtuQXqpSCcz4BEGtizmM2TY+CtZHrbevQtJyvOfpkG8Mpl+bNDBEsOefhQ5MAtK0stOfozyEQiJwAYhJeKbVvxExZLE2hCDBKQVv22w0+sHm6aFeV7iSxuqqx6ldZQ66N5ACesqZTTMSb+RzQYk6PqNkEjGUwpZTiKNry1puY6xucwkubDizHfN6Tmq0CgvjRclqyLkBb27KGnAJBVaoA8VpxcKroXxxBTtSYaqNTbmgfXHY4obt51BzoKx4yRD9prbmOEAIsWdw054jeRkvqKaOTMOrmfJBQ1xMEAXX1ZAoQJdN4CiFZBHD/+QDHwMhBS8XaeVkzTdMAig8H+BDEpDGNrxxbDmhANL5d7vjuU9XIIYY2WlLAdVHgteF4Am3r+IBGKVQEzjeuleSel4OOSkvEcZ/aUQCqynJsA88WAlXFIHxlZSVFo/fed5uT4vRy0S/cVPweGInwHQPquUDQ1oD+f+L+LOiWJTsPw76cqmrv/Y9nPufOc997e8TAbjQIgE0SBAeQFElIQUu2wiErHCEypLDpB/nFDx5Cj/aDH0hJlqwQTVuEJCpEgYQINAFi6Llv9+3u233n6czjP+6pqnLww8rMyqpde//7P+c2mEDf8++qrJwra3251vqWaWJ4cyEAzvzhFEUfqMqSCOqEiO8QY0BVk4WKtQal98WfcyBTeXy3ABff0RDL3EOUGOYtcA4E6wDm++N82C+yrKgBb3kBuMi8LyX59UvG/GEBmX9XdTiY0T7ONxEHWmcgpIJjIcSZiuR1IQyftRbgDFySpUzcZB25IMymE2+S7rxLAFnbwPvPP4qPdwOqOxKHa4vo7WdA82BqlFe/Djl+H0ZwMGvB803Sft+fw2y/CDZ6AmAifsACQ7//lcjPCZhEY94KBPk5GMO2PzYNpArCg7/iteguKS8MLEuebEppQcsU5tAH1TGk+rg2KVwbkbTMehngXNBVdoiCWLvfMYWPeAqu0kdYV/NJ16i/6cFE096mzymIbnTbKWlbazlEFNpeB4v4IcxESqPW1Lcw2mzx2bQBrWfSPxYvIrrEhH47JGPQbrlD+/nQlqa1zZ0OPE064HOENZ3c7K6tnkBr/YJ3t6J2hZ2MrDUUrb73Jb/OUo1/XNMnyW59El+P7Om6V1rz1JbQWXdC/Fplic/5MjPWdP7CEo9FtAvtTuhDp/g+dMpqtaezhHvl8zTPWm1ajWwW12Ui6KePp8+4ZGtgi2PXfU/jWk3K6h4s9XbnBFAW5y4p99TD0ml0q+3J2lj63Iry4zvSU89CW5Y0egXeXFrcQlqRKcxLHOp13uUl1x/6FeluAoubQv+9U+w7rf6dtEjiN3VFJevh7xPvn7g2HjatU1bnHTz5seV7yLpVAqcA3kqGcFAMxpLv9Gg4iqasxln6AHmhxTnjQyQ15o9CEHtzCBMVWMODmTgYQ6YUpOBwoDqKvEAga6J2KO8ba6K/L4CoUWzCG5mooaU4xL7tPowZAFS1BqqaWNq9P7eLO2qqpSPgNRgMSSPMOGmQOa1gIpQjf1ulyOw2xjF2BIadtWCsRu3DTDFBmJD65WCqEsxrIjknIGd9vQRkfbMYg4MC4JDnuR9DKieIpRQujczhwRDN0MMCqSvSnApBIbWUo7pm8xkAmoPRaATlfcStc+D+X9JYGxg7j+OZeV/huq6pnaD65iWRvmVZRjGXHSCkxKAYxOUaDi+U9+G1jsAv9wRnQviwUlLE03tjDXg0x2cIgIALitNelaUHo9KbMpOGVElitGdgyDIKEWasRSZ92C0wWG28C4L0vtLKm8sTEC3LEs6GgyMJLoX3G6bDA2fpK6g9KSAdLjFIcB82jeKCC3/QYoxFlilwcDgfw9pYH06vrKIVhdHk6kEhsihed+0jAOR5jizLkfkDCGu9q4EnMLN+3AMoMLaC0bUfhyK6W0gpvX+6i+8EY4D06yBYFmhvJcBAmmjrXBusc3LpsMa7CUiJYjDwa69xcxgOB1AeXBN5YwmtDflde44I48OYRTJFbxkRfMKFENAOEIrDeq11cAGZ13U0dw3jGIgQlcyglIU1GuDcx/p+hJR8rVPQEMjPGAKQYQubfT3dx/0PXscosxhtb8NpA26PkA02AH0Me/97sNPbwO4r4Gor+d4FMEaHCQzw+29TT9fsmPbj5DMRjpGTD3HzrXZJex2aMF3NI+3U9HXph8gDleZnn7QSdpGmj0AY4hCGrJ07jnd6PbQxmtUv5Oifc9bMV/taktzis6zzRxtQxNFPylhsT/NsUj9L6ksZlOAWe8CaWrp+j6nWoXtIEE5Xmu+EWzj4aFsW+O9kPNToEgai3ZfY4Z4xj+9IpyMnSWSuz5+6A+BTc/7lLUXqttY68FgQgDtrN7nUnf9YY/ssY1lvlq6xvmMLvyTit6890d0aGBjrrvukJS2UmebiaE7dPiGJeF3QdkJ1n0iTwrkCW7jU0q51NaTB9zVmcWu25YS+95exXifDevhEUmfNh2Xft0xOqrL3fh/oY0uuo720w/PdcU+3sbUalX4CfQGr8O1iY9ZIPevqpHyt33Ex9jyzRj9dz/gkH8IllZ9QbnqS8lDrrT0osat9YkCo6hSolqa255u47r7zsOmUyPtU4cRCaCzrycYEJzNYxrz/JedeE2o8G3jmwbD/dHhQAjQfRMboGSFI+xvMU7WpyTTY74x5TiG8OCetWD2bgcyHRWRTBhyxcxsCV0qpyF7tnPV+0STAw7lIulbVNWmsGTzbN4VHo7BI8KCeNOwx9q8HmUFDz3zc1uBfGrSwdV2j9lq8EFNaSTKX1TFsGpnBgjEwD4CiFtORRQGxw8voSy6EhODBzNcTZzkHj3/IBJoxCKlguSNf6ej7TfPJWfPV4ZyjKAaeRZ7GSghJ5tPWwjgX+8UEg651ZLCXgjTpuffPb8yOyZ84mCPHPsB5k3ISdugAwUQiujDWqGn8hDeTDpYNWZbFuO7Wmuj7HuY1aC6DNYKSkl5szpHnhdfWCmxubEBbA8/7BsYZspzCrVVVDc4onB0XQFnO8d577+JrX/86PsoyCDfAs6XG0089jmdfeAZnLpz1bP4E6pWjA6EQjzsA9XlZQfpxZ4AnDaM177xZP2M0F1VFMeiFJ87Tnvmb3nIK45VlKh4KMMYglfRkYsRcHw62LBzqkiwFMqVQ+/c6hFsLB0D0nlrUnhAxz3MALL4rpaHY55xzaGuQyQIS8O4IVTwgC/1Vfm6CLzXLcjgLz7VAhwhGN8R0zlkomdOhlqOwbU3sdH+YIAhAUzuI3I/M58mlhczKDXFEJdJMURSAY+DCa/a0ofUNCl33UOQanRRE5UjSuOQjmoIaXU5RO4lpbWCOxhhZC5UXYDiGVDmEyMCOPoAtH8Cd+Sz46DHAW9Sk4ceiPjYVvFNtb2gIW4TjVFAbMAeYFOthaHrnGmDcXPVtYAkocSl0Pk1yrSELPPGhvgZEhQazloTMkn8boSNpR7dJSyS2dCxbj6Tz2mpI78/+OhcuuLYE0hdjxnX/COihqTeM0rLlHPXzqS14y6muB7uFi60F0v6WtxvZlfj8Qz0HPfS7u/K6z/b0p09iTlduwvjfDbfXFfTT2lmaIaCsmM/3IV3/zauGzqV0aha60B6CPgE4PSjwtSVa+caPtgu+0bJSaY1RKum2GkVzkC7r3jF71PQoZfSApb4pXVnpCsCyZBktgpekDW3AsJ70zWLjm7wrnzhpzNbZe9Z8tjs8cXcPIG5J/hYo79lS1mrgug3vvLsAFk3+V2DL7tbdus7aeU7CqKdKq8ByZy9a3H8fIoVtNmy7af0ryo5zuebJViz2NOCzk3fVO7R0HS1JvWO3fHNoP/eQ6bTPrg28tSaiIusofrcUAlJKaG0wr2YQXKAocjgfgkmFGMYeUDDGYBkBxhB6Sdc6sqK7SMDlog+t86zGJCgyWMbgdO0BgY+lLThM2fhmc8YiaRIcEVQBpPEjTSQRe+V5BubNsfMsJwDvtcea4qbFwwCGhgnceVI28unlnh3ba2k5g4Xz5vTaA2rqC/dASMhgSi8BRoBDCIk88zGHA8B0FmAcgpE2sKwrOIfIiA4HOCHihJdl8K1mKMt51GgTAR75FYMBVksCtS4QYRFINdpACuVN1L0frNaondcAcxUJr4Q3nSfw7t0IPHizHjwxTvU4r1XmPrSbNhqz6dT7C9OBSZZlpNlkwRrBwFoq03owLrgAvNk0FwpGG8zqeVyHDEBZljDaQGUykss5z7YORib9AGCtt7bwY1fXFcq6Is19RvGhrTVEi1c5XL13G7/9O7+DH7/5ExwdHGF/tAF59gm8U0kUf/gaNvICT195DM8/8xQuXDyL7bNb2NgZYGN7E8VoAJUrRL9z0MFMOBQIJtuB6C+sY2MNEa3Bs+Qz8v+vyrLR4npXDVOVZH7OiTjMWQvrxWttNLgjk/CqItPzoqCwXJWPOBCsQkjIILb1sqwi033tteouo8MOqcjMm9FgojY6EvwFkG20Bpz1/ttkci6lir7bwrfHlmSpkWU5jDeP11qT64FnOJdSkkUNPM+DEMSE7tn8g0WJtQaCS29GXkW3h3I+h3UOAzlsQDfgzf0djLYQ8tFgN0ulM0ZaZSIwW4wtHZP/YqnBFvjwLHQ9AbMa5mCCYlDCDXK4gYXMQkz4CfS9b8NUL0HsvATGsxZQCMKRZzAggTwF2Q0CXQAErrmcNM0L4/Hj6Gvy/Wl/xzrjFxrT98Fbe6CbBiUYqlVg2u4GindzpRW75s9WJpbk8uCenfitXinEN7Y4PitrwFkjA7EowPfdafUkShQE/Lrxh9cVk+Je5Nr1tfrkfKMSxu42yHfN7QDsWENKF50cWCK0p61cEIyClNjTniWTEA9j/ABSjeEwoF1XWMutRd4S3JvJcd3n/b+OpXR7bdP/Zmq6pvXpS5XU74DuG7SYwuB12hnKSq1sEiuI8FKwpBXd/i6prWEyDn1xSd9+SunEd6zngWUYZg35OmY4UaBfcjO+Hj3FLi3PLbu3xknAmoBm9e2kkBUD1Pd9Wt72vvW/bnsW61k5ft38J10L2+SStrjuxc73oLU2Fj6U6za02dJOwrDLDjyWptO+j2zpj/6sPVnckn4stTJIr/flWXWvJ9s6qXUI0NeHU5R1qnTK+TgF8NaonY7aUgJKpN0iIEnCvbUOxoR4vCR6GG92Hsx5HVzUlsNQ2dYZb8INH2tZQkkFlXnxxbmG4VpKr/218Q0KYauMZ+CWUkQAXtU1lAMYJw05Z8Qk7gDvX0xax+CzGxiQjSGtr/Ng18qgOSfNqvbTGMKEBY0rwHx4KebNpTmGg2H03dXQCWu3jhpu5wgkOjgfy7uC877DAWwbrcnKgBMJGfloU0grqQiQCm+mPvem47BNaKigGTQ2+N8yr/kmbb1SFIu8qqvod8/AwTgghYDWNYymcc2yLDJCkxWA9uz3RNIlpYIJoxQORED+3mAUmc4a4zXNGXSt4ZyGcyxaRxjjD24kma9ba2EcxYEuigLSjwW5NXhWds9SzplAXVeYz2a0iIsCSkk/f0Bla2IC9z7MwZXCOQAWuP7Rx/jqvav46uQOxu+9CzWtUCoJ97nnsbX9BB68fx/bG5s4enAHHx8+wDffehu77DwuZy+iyqaYDqcY7I6wcybDU09fwquvPo3z20NsbWwRIZ4nRgtkcpyxaEJtAuO2ole09ibzxpD/v5IqspsLIaFtjclkHNnrKQ/dp4MTjuFo5N9RG8Gn8MRrShLRW7DaiGG9rIFSdDAyn5dgAMKRhXMEduu6hhAKcBbFYIAiL2BKz4nAMmh/8DMcDKOsH1wKGOfIwsGXd9EIft6VCOHqFCRXcdcklxA/X8mhD/MkjeSCAhhTozTaR1LgqLVBlvHoD+688BsOboILysOlDkJkvIFPXVPa1PzVOchiC9tPfxHja6+D2SmclZjPZ3B6gno+RTHaRFYUUPmALIrG78LVB8Duq+DFWQI4vi9tJm0X9852A5K/nY2HpUGlEQ8KokAU/ghSGEuKaH9xFv1kEz/TzvAGeNIugfX+SX9ztIifutmTOlu3EgFy4Qu5kAkRBLY9wPskrqbgFC638jO22A2GjjK7xzi/pcFv++oGQNvubdLvgO0SIBlup9pTmtvmcKBlEh4PVlhsCh3kpG1OKo8qp3A9mPc7P5zpukyPFIKPcQjp0vB0xFqiJNVUSDV1Q5n1+Ownv13nIKh1GBDXegNY+1wm0rJ6ZS3WjOPivCehyXqk28ZaJrnWMX9v5rKpo72sWafY7uroew2olHggE5dgeoDwryF1Ku68MksfWVleeNZhMfT6Cc+H++Eb1m1Hd0XEOevZftdqZl+DVk2GQ9tAo5svfXadxrD2slrW1iXLbf3EWv+cvOD67q3I32eqvBQId9Zbd7ePWU5zGLLGmDdlpvvlo6fTrLmT8i41bGr1zy19oFXHwubY++MhU8/k/NQQd391J6X1ydUQZAHaepwD+c4yRhpQULxoMm0VUdObZzmyLPO+tWR2rsuKfCsZi6CdO0YM2IZAupL0O7CAW209gzGgMq9Vsw7METmW8D7BUmbeT5aE68FgiGAyZawBBwnCBPYtBCc/ZwLZAkWew1iNuqb4x1lOBwvaGFTjMQGkELLIE7jBAfNyHs2mldcIBlACF/zFycTWVDUABqUkjDaoqjJqninUlYwhrIQgqwBnLFTGohYyxIWelyWUJDNgo43X5ueNv7s3X+aczIaJ+Mp4s14AzsJyTwAH5k16KdZ4WVHZ+aDwwMghc2QWrn1YMBPGgVHsbuccrJRg0L5eCh0VSOHgSHPLA2Eb59C1jizXw9EIDIF12lC/YDAv59GPWCoFCAE4C6Bhl8+ywhNzWZRlSQcQhuJTB60raZU1tGeil1IiUxS/m4OhnM7w8U8+xluvvYEfTG/g2xckyi+9DPdgH/bNazDnt1FsbOFXv/hLOHhpjLf/q9/EWbGFLB+CCwGrHS5nZ2DdZfxk9hHeszkmpYJ5+y7OfufbuOgm+OUv/Qz++l/+qxEA51mBTNHfxlpPFmZ9CC1ynyi1BhiFpQshyYwPyxaICMMhjlQKVpOlQFmVnsxMAYxBZQqSk++79OvG+nnS2mE+J2uJYpDHQys4OiCpdU3uG2WJyhjkWQ4mJIyxqE0NJaQnQiM3ByYVwBtXlLomSxMHQFc05yrz/tyMkfuKUFAA4IhPYDgYgAFxX6CDFR+1wJGLRlVWfv1SPqkkOFMw1qCcT339DqauMNc1al0jywof194hU2RxEMgjHyZFLWl39w2AIGq7GgjivPVQWc4xcQXuVyMMbYmRKsAVR2nnMFqDlXM4Z+AYh3QMUhWw1T3Yu9+A2fkUxNazACMWfWeNBzOiJZzDGTBXQ88eoJxOkA03oLjF8f492HqOrbMXAVlgdnwMNdyFyDdgfTmMZ8lHtJEGnZ+n9jei/bFL4xr3+SCyNFhoVwpeSIkEe5oP6UmCTxeJpGq+NmptPRT6H4WrVgfTue4ArCggpwggrc/FNjWHFqlxfUrmljzX6UJoc6fZrbEIK6TFOuvLbPUm+IAn4HDRDLlvglnnEKONeFiSb5n8tjAXrPN0a2zo4aWy+QlIoQ12wjvbHDwslt9ZjBEo9x0oudiGxVXf9H5RXA1DkPAzJAczi61afDnS9rIkTwjRF2aolZexxbH4k05JtS2jiJOa0ycMLxX2T7zcFNkDhFva0XSNdtqQbpXL7KsWig/lLG6/Pe97s7csDs/i3nrijKYZlmQ+zYpoHUCserCLl9wChjtd6j67DlAKda7Tzod8NRaAaLrWH6qQTvNc+o1Y0emHAI5r5e+s2/WLX9j8Hy31yQoP2+dPKK0NvMkMnHxdjbUkdPu3wVoLJSQgSLgOpGDOktm1ZDL63QYfX2dJiyoEaaLIdxUAiGGYMSKeMoY0x3RY7f07HYUlqusaTkhv+twQkwUythC323mzbR7iWjuLck4mrUY2TMaB4M0mPr/EhG1j26x1EIIl5GmkZRWMrACcdRTqyQN/EQiurCNzcCACDQZivq61xrwsCax4U10pJJygtz/jHBoaAPOkVRTrWwiJjdEGhVqyDvPZNPpdhxeZTHtJy0+aPgIwwecdcBACMIwYu+EQCajyjAB8ILYK4IRYsRmKjYLWrw+zxguqI2gsOScfc+d94onwC5jPazANcCF9qDAP+IRAXuSkqdYUv9kAnoyPkY8uQEAKQAhzFgCsUhlUprwZt43zE4i6PEVdZPkOa5dzDlQWH715Dd/+6g9w4/oDTPP7eOPcHPpGjeKPJhjePMbAMJw50njyOsB+eBO/8jd+BW98+0fY++4PsTmtsTnYAniNe/ZtjPgGXt4Y4QUnMJsB++V91Acz7Awv4wff+RBPXvkePv/Zz1PIMu/TbmodCf6MNQSSvXk8977Y1HHEtgO0TvMsB4Nfr64RzJXKY5z4QMomvMm+1hrz+QxlRQR4wZ0CjMoZDAaNdjgKPxwqJw110JUNh0Na+/5QTmsNSIW8KKLVSTAXr71VRCD8M9qidCVZThiDStOhXJblKPIBWHgnPTN/8GPnzIPuqvKh1ZQnTSP3AsdoTKjNtKfAktVMVdZwjiHL6RDCwflDKrPudtifVnxc2tpYD14cvVez+RTHh3u48+AQ88MDPHl+hHObBflVmwqTmYW2GrU+QD7cRAYHITNI7qCP3iP+hWwHzsyhy2PUtYEodlGMtsFtiXp8B3VVQqCEFBzTB/dx6yd3cebSBQx2r2B8eIT3r76HS89+CtrUuH/1R7j8zCuYzWaweo7NSy+BqS04yDag97ZnqSzoXD8obHuLJhriDnDsypXdT7Dr/bH29PQ/0hF6Fu61UpC0g5lzAFSst5wFiMY6behKlCFTKKgjrDvm4mFF2xy6U3c6lilYW7pAkU5af4ZESmzlOgmcdeYYEcn2IYRwKuH/7mtO9xnG0F6F4eOHJOOSlRTKiuPe/nvhvKk1PwudpCKjr3R3vEP5/tgiPTBYAAeLBy3d+7FxLhyDLK7uDgc7WmPEHMLBUdslJT0kafVsofxTp0eUqdnCH+2i42yfouwA5sNyWYbTYrWd5bPQAIeuh8BajejWn84Ya/2x2LiWgcwyUJEUHLMu2xJcz8+HAcDdcVr5fMjcG8OgyXKKNrTG7TTLt7s1uWTue1JKUfGQQ9QZ2zWQ4Un9Oc04rcr7kAcLa6V/HQD4p9mfNdPawDv4RKtMgYFFc1ZdkymnYMRwHIVKCDDFvFaJmLa1rlEUAzB4siZJAnT60WCcIZd5BNPWkz4xxlHkORzgQRUxnCvPJh4Yzq2z0GXtQSWBwKAR5D7WuNFklioVPUsmvNprdYkoTSoJlWWodQ1nbAQlzBOaBcCXCixZpsAYMJ/PKKQUo/BOSnIfSstEP++6qlCWOvrEhvjmQSitvcVAluWRodp5U1jugYxUKhKSOeuiBpPMdWtoUyNTNGZc+IMBF3zYSXArikE0TRdeK6913QK0QTNoLfnsDgYDwDUAljHyEaf5I7Z7Mj8W8X9wQOlNnoU/CKCxITPfYBFgjYHjNJd1Vfl7ZPocWM+tn0MlJeABWNDgC0VkeYJzUEQwEjDyPEemFBwYjJHkgwygrgwefLiHb//O9/HWO/dQDa+gzgTuH34f5w+Bx1kB9sEYsEAuR9jFLi5uX4J5awL9wzv4T/6D/zVef/MqvvGt9zF9511sTh7A1HPcnt5AXVdQrMBGto3HR2fBdi9irjXEpsIf/dEf4ZVPvUxkb8ZAg+bFAf4wx5tQeyK4LM9RlxUBczhkKiMm8MSkMyTOmrkhn3By6xC+bAcPwj0RWogcQKRnZFoeDrDIb5wOmWq/FrI89+HTCOQbrVHXFaSQGAzJnNyFMG7w/Aog83IGBsEkBOfkk29qCEEAmRjROZQsyCUip5BjtbekCdYettYwoNBqnHHko9y7htA7TNYnRGqnlKI2cA4maTzDgUJVVvHAIVMZWPbwu3EIDxf6sQBO0h+u2euEoDZubu3g4pUncMsYvH/rAY5nBZ48PwIYh3MaZlahGAq42QzWGBQbm2B8BDa8DMcluD6GdRpufgTOJMze+5gfSRQDWjfjcYlq7xaOjvYw2tnFYGsXH7z5HvLBNVx47DLAON55/Vu4/MxzqCqDd7//dTz50is43rsDKRiKzQuoZjMUZ58DU0PqUOdrHQSTRovt4kcuFWDi2PQJhm4RA8XnGBrtTwqY0L6+II8k9SyYHfZ++BfXQWPEz5L+sOR/fR3lC+UsVJPm75jytztKvxvT6869HnvKRqDrlpPmCN9rxG96y+Q8wramfNZ5LkVDbY14p7MBJyZjGJi5Q10p+Fi0HmlGvbsMQrnMj4VzjbVAA2CbVsb2J8u4eWu7UnZAN/4Xa0oJN1r9jQcBzbqhPJ058geJgFvg0Wubsicz0nLzCK8TRztwIJIn2yW054WTJUN4r5L2p4szmZlPLK0l+ybbSJo5AOVlBZyEP/uqSf/ogubQjNb7ytr3Yl0JgO4sm/aetkbjEtEypoXDgXRva31jsHyQ/Xyv9HFPFlh6DpaWucz/f5kvMLB63kLZXZwc57unrmWpb394qOR6ykjbsvSE4ORy07FL3/V2JStSum8lbUotC9I9qbcRS+8t1rNeWidz09j++en5hp1inON34JPdspanh1hgawNvYj5uwm0ZSysyNX2t65q0pVJEoKY9MRaxfpMmOstIO8XAUGkKJQU4TKZTAECuCCTnSkIzhnk5BzT5UmtrfKxtim9svel07f2/A+DgnmU8z8mvN4AH7rWlRuvocy4KAaPJx7Su6ih8lNUcxhDAM9aAaTLFBhDZvwMYgCMfUaEElMoAMA9maBw4Iz9aLngkFWOcx489414CcPDEVPAAU8N5k3iZZzEOclVXXnPIoSsN42xksSaf1tAmb43ggbYUwe+3jOz0WRZAKVkZ1HqOqqown88wHA7BOEc1n0eA7piIca+tI+I0x2mVO2ugbQhlxqLPPfMh0oJbQmBID2PuFxlsRbu7lGS2XFe1J0ejwwhrTGw7ikHUCGcqj7HX6UCHQRtNlhBK+rVCzNvWmzSY2uGDr9/AP/nH/wL3jUN2/lXI2RRsfAM77iy0m4IJie38ErZHZ7G7eQH5cBucF7i1f4zf/3//C2w+dwH7LseDBxWsOoPbozHO6xy7xQjQFWpTorYljsw+Mqlg8wwlDOoZw+uvv44vfunLqOsStioBRkz8SilkXgM8m82hlEIxGCDLFTRn/uCIQXIFbizqusKkmvjDoWatMxCQ1lZDWBHjXltjUftY4yFuuHP03grHfSg+OlwKwFsKGV0YEAR0LxyGua10DeVJBa0x0FVNBweMwdQVaaNB/uvWm38DRG4H0P7CfZSDEBKM+YM76S1ljDbQro7rCqzR5Id9ylmH2pSQgg6nVJZRe3w4MiFoTZBpvvJkgwDYIlB6mLTI8ryQA/R5oHHLswLb2zsQnEFmGe5c+xD37t/BbF7hmUubGOUC4ALzeQUUHFwyTGcWVoygnICbHoDDwGoDkQ/BjQPf2oGZTzCfTVFOjmAqAzYcodzbw9U33sRw+ywYOK59eBPvfXATO7s7mE3HuHnzmzhzZgsMwHtv/ADbZ8/jztWPcPmFTZTHd5HlGcTwDJzchpBF822LZq8BVLVBUtTgLRuaLsbqfpHZkn+X3U8lL9e5n17rCqsLco8DloCglR3oIoWkrG6OCAyDJLYwUMsk2c4AtYSToBkPv7vPpycUAXkl1zonGY2Q6DOkZs5LkEEbCnYl54R7ILncHumkumTSWgJ5IpE1BGpJk0Ls9xSpuPQgoRFcYx/T+YqPNbO1uAJcwgKcjHsKbDpgbmHuGBqJGd28TZ72qnDJ+LBYybJDqAbENS9BS2hf6Kv/uYD2HiEtAI318gOI72cf2Euz9D26KqXda41XqGsFkAz19O5t3T1tVSO6lfc903nNVpW31idonZROe19/1vvULS2jr1Gh3DD2vQYmS1JrW2doD9TC3r6kgCRvK2vf9yj8DFtPd732taNT/nrfk5WZW2t1VRELZaR7Re8r7uV6sNUkar6+hUOSZe0J7/IJ66fvE/7I6RMrKEndz9sJaW3gPZv7+MUgTbP1AixnHE44CkFUO29i7LWagnvfZR/zOMSolhQmrKzm0JUGz3NwLsif07Nqw5E/NEDa0ABOFSMteRD6XSBv8SbQwggK55RlqMrK+5oTHZRgPiCN93uG7wsXHNabjjvGUM1L6Lry4JD6CH9wMJvNkOd51Jo5eKZxS4zMtWeO5t4CgAjBHIRi/oChAZ3aaB+v25vkSyJ5YwwEIJyKfr/WNIRYdV0Tg7s3Y+dSoC7J8iCc5hdFAWcbIriqKr3ZL4XdstZ4E2TnmcjD5mG9HzzigYoQAsVg6OMqGx+v3UH7fjNm4yEAATuvIWfkZ55JYjAXnsSrqj2bNnzYNf92yRiHOrQb3oqAexBNGTnnUCrzTNwM3LtBEBDjyBiP/uFcCFhrMJ+Tr/N0VuLBwT4+uHcbc76JwbsMtlI4n+WQ998F7BhCOhTZU1BSoZCbKOQAhRqAOwlbATMzxWR6jLIaY/7DBzjUGhd2n4Pa0nhNatzeZhg9/yReLg3yH7+P6cEh7kw/xv7sAOKZyzhnd/Dc05/Gv/i938XnP/8FFMMh5iUx3Dvv22wMxbMeDAriU6hqMt0XpAGuKxctOBwjAK2UigR+1lG866G3PtDaRA02YwwSMoLRqqq8Gb8PSeb9wQNTv651whFA77hQKgJszhiEnw8iBTQ+PB+R5hljwbhArQ2cM+A+LFjmmfdrT9inVAbB27oVcjfxYeKk9OHTLGxJIcCyLPfvmmfqZwxSZeBcQpu6iXDAmCdjIy2/UmQtEyxJKPrBo+zIXujG8u968/Fw8REGDpVloKXLwKVEUQywsbmFe7eu4+3rR3j6yg52RgWkY5jPNYwDis3LcE6hEBKwJcYP7mLz4lOQ2QYcAzgsjMowfnAPjkmMx8f48U/eh3ECMr+AwegitnbP4PwLQxpnrVHOphgf7GH/cB/l5Bjz6zdx4eIc587uoH7vXQyLDJzfRmYsTH0Ho4svggkKN8dZ0rNldnepCWvvuGBhCrrfs7SENkRrg5aAfRqwkaQATnyGiE+TfMEKupGQ2sCn2552qzoB1CLYWE7X5tLCWmN2ijXZldJPEnLj/cR8vYXc0nw9Elrz0YiDnI537xFFC8SkUvkSQTNtcVe69e3s17D59viyI/s6a+5GYjnX/N10c9n4daTH0OG0T915iBUmM7+i7PbxQqLL7kifbfP6Zg01XU317kn+iDSXu0j0ApZPIiVDvFa5fZvpJ9ie1AujtTZ7QFJvm9KlmP5eVh/aSyVNLXPxNH9rztdLrXqWgIJ1h79veZxYQHf9uJ52ddvXV273dep8QprICj150bcvLFbZt+zTOerOSe/aSPJE8HnSIlgGzFt71cmT37ftLz1b6PkWnlA83XNL5i55sPeQItxrbXknfJsW9rlO2Seg8MVbp0TFSxv1qGU0aX2NN+jjRGaxNfKcBPXah7kiDShplLnwbM3Ed01m5obCBlmQXyNjDLo2qDyztlIKmcpiuDIHz2jtSCi0znmTbElm72AUUkiXPpZ4CGXkAbuj58kvlPy/jfHCtQcXwVTcWtIAEsM5+Qk7Z8EsgRcuRCT/kpZMtrkX4ClmuYPgDGow8NpCjVpX4FbEcGMM8CbRpJVXSpElAONQGQdqz7jugX5ZUcxlwTgoOFQw+UZyoME8eZvzYaQcLGz0pyetXgO6au8/LLigmNtCEDg3GsbHFJc+5rVMCOJCCCipyAecfOG9Rt5SbHRnLRgn4jQHEJO6ZMhVBmuoDs4FynLuNfIgsiyQLy6ZHZsYp1348GPONWaQnHNwH2ed2NV1jL1tDZGnhUMPBwupyF98WlWoqhm+/+6H+NG1WzjYGaDc2cCXyg1sWYHH1BXU8wNACQh5lsp0OZTLIGwGbjg0GDI3gKsdDqb3ce/oHirMYTKHfOcxPP/Mc/hn3/j7OJ7dg3r50zCvfhpbX/kZHHzz23j7//J/hq2mwFDizNkBzhf7GJ3T+OBDia9+9bfwF//y36BwXHAtFwbGGFSWAQ7xQMsY0zrYKQoilAvmzeR2QOulrmsoP3awFozJSMLGQAdCxFkgo6bYOovcx3M3zgCO1lita+RZRmHqouuDxXw2g65rFEUBIWVjFQMfqs55Db2UgOWAIeZ+5TXaRlOkBGL+H0TLGAdA1zWMsTGEntEaTJK7h1IKuqYQh5AUqq+qS1Q+DBrnDK5um8Eb5uK4wVvw6Kr0bPbwzPgPl1giWEd4FcBJFJ4b6S7dvjk4mMxQFHQ4oKREVuQoRht4cOs6rj8YY64LXNjdhEIJUWxjbhTqcg7G9zDfv4mNMxehtYXMvaVDNcN47x4Ob32IbHMHarCDz/7pv4wz5y9je+dMjCwADzysZze3xpAbzHyKowcPcOPj93D71scwN+7i6WefBGyN3cEGBBzM5A6c3IAankGwFiC/74SZuseHdJF2qvm0xr+as4nWH0GgWTQ965bZeT4WnsxNJ8OCABPrIKmh79PL0rKSQmloFyW2Xnzm/JpplRP2vb5e+YcSia1LYte0vRvzvCvxhT+bv5fRgqUM4i59JtXis47gCjTm0Q4xJFfAfn2CTHo/NUNv6mpfa41Rn/tDq64wOIw4FNI5iX/xBdmwNb5pft/Gbqz6Hnm8c5hDJK8uVeex5B+XtmqFoJrYyS+S3bVK6LSZ/nbty63fP610qqI7UncLKC8pN31teytbIbi3/KSTAlvv4pK9oH/S22Wc1PnuG74sxbe/B5+tUc1COWu1Z0ldC8/03WetfxbunegTn66DLmBFz/VlqXtAkvxeNj8nWg/45xa/NSe3h6Vtal18tNRbxKpvZmeb7S3QLVkDK97J+DhrDiS637WVhzvL2rJGWna40qr0oVNn0k5Z1qnCiYEBkRTJ6MgGnikVNdzWEat0aIyUCpk3r2aMEUCqa28OSybY2ps8D/IcypuhB3BH5GaebEoq1LUHj1KSrzK8T6uicF6CcwhOWk7pwW3QegkpvV8ovSXOuKitNtYiV5n3DSV28rrW/gDAIBPk92qFjEzKDuQ7aq2BA/mqK28mW/p4ywS6acyEJ40TIoStCnHKQcDYaBQiBzjzgN2ACw7Fg/83mbOn/vXOE1UFgK9k5kGA85pOzy7uwQhnZDYvvMZR+z6CkVmyBBHhBQ07Z9wL6R5AMfJLZYz7uOkElMEYFANUUfgXlEzeda0j0ZoU5NsuhUCeFdCmjmOhPCAM1gAAaXHruorjZ/yBDT1DVgicc9RVTebk/jDCWEss99qgsjXKcg7LOD6wFu898TQOBgL/j8//PL7y2JPA3+S4+t6fxg/+4H28+Z03MX5wBGca33vnGAQGGPJNwHJ8b/+HuF5dw+GGwuRTn8MLz76KT48l3nzzRzgwM1hnUX70IfT/9Lt4azYGv3wF8q/9G5h/8A6y7V38mReO8Ve/9CS++eYB/vnNl/E7P34dTzz9NJ5//tP0OlsbyeDCwQj3/sra+3dLJcEcuTsAPpqAf0fICsNBCQnnDOZzAymFJ+Cjd885FzkWguBYliWFw4MD9+tG11U05x4OhpHfAH7tCi6QqQyCCyIvAyBVBgYHXRM3Q1XOaf5tIFekdzuYvNPBDZlJO0tuD1zwGNot8B8YMEymE1hrkWUU6sxai9l8hkFRoDQGuq699piY8oNLzHQ6xXA4ougE0JjPyZWiGAz84Q6RPhpTnW73TFP3uDcR0tI/UnPY8LEnwVpAKuatGsjvO89zjDY2sHfvLg4f3MP8zhyXzw2RZVsenGo8uPkRcsmRjc6iNhqTwwcY5AWuvfltiOltbG8NUE+O4cwZPPnKL2K0udscbjhLRJK+FZxJcCWhsgLDjU1snTmPC48/haP9+7j+0Tu4/sE72BgyqMEmRqMhpK1QHV6HUDlEttECRcE8NVrderWkC38vGb4oq4Rh6gqz3Q9clAJ6xNS+L++CWfQygaEjCXYFw3Ul0BVf/9i1XsSQZu+TWtsLjCWSYxrbmpqxOIjtEQvgrYFjrXnotmWJkNyOX43OfxH3msZEOvzyIDRpWQtEurbXeXt8WKvutEfpUQ7rPNvk64M6buFKs8xYp8b0YKNp5WIt3Tn110PBrq91Cw8sFxhXAa+FZ/rHrD1vi+1ZC4B8Uikd+GSxsvQ+W3xk1f1WuUnxDq0tufNHfwV9vtwhzwLmSspODzZWgve0jp608HnpL+JE8HdCN9v5HnX++wp2q28vNqL9zMLOdlIbu/OVfI5Pk7rnt6ENJw5Rmon1XFvj8fTRU6W0752CumC47yLrPr+w0Fc0zjVFxXei81lb9e5Fw6qeolv5YuP6C4pNPvUA9iy+R0hrA2/uiYrIapNiSEtBvp9gQDmfAyBQFExgYQlIIGgrOYeS3rTa+1dmGfmUOj+qJBAC1mryGYfzPuFFjNesdUUhkoQA98AajMCy9CbtxnII6wCEUFLahwLzfpzevNtaQ8RbIvc+yw5VTeBwOBzA2qDVJyBZexCpVBZNo8uq9ObrFp6zKwITLnhDhOVNZq3XrDNrMZ/PUesqah5rVlN4MCkxn9eRVTqMv1IKxhB40HXt54SBtPje3BYhNrqFkBJ5RiHOal17EjKKAy24gJNeWw2g9uGpuODIC4rnHf2vQaRrZPorIys2acscxaT2YDH42yqpoA2NL4GkOR2MMH/owoX35W184a0xKAaFN8G3gJR0AGIttDNQ0oc28ho0wTlKXYNbAcYIrGVSeOZ1ci8YDob41r19vHZUY/LkGTxTG/zClcdQbBCgffn8GXzqi7s43Ps0rr67h5tX9zA/rqFnNZxWmM0t7r51D0d7N3BQfYxJeRVajDC4qTC4O8P3Dj7C1foO5EYGPdxBzRn4jXdRqS+iePOHuLLlsD+Y4i/+4i7+9uefQMYZvvDcEYavD/B+9gT+4T/7Lfzdf2cXj11+ig40mLd+CEA3uB4zABaecLA5hKHQehpKKQyHFD4Pltax8Ob/YU0SaR/ArfUkbJIsKDy7v5SSDnMQ3kVv7SApHwE+h+PjI3DGkBcDZHlGftW6ogMQLsEFta2udQTRwnMNgJF2mdwT/AGCcz5W+By13xvygtjTS6PBhcDm5hYYI5/wcj73DP4KxpCbCfeHLQZ0gKOU8mHSaC9hIM26c0S2qKSMhxeB8f9RkkvjdafM1CzdthPzTjT4iORLDiaUdyOREEIiywsUw02MNjZx/9YNfHTrCM8MNzHINEQ+gK5qSDnEfHoMo0voySGOju9hmx/i8svPwIEOKHYqBqH3AezGEIPOIf6vxSTrP7icCeTDEc4VA2ydOYfHn3kRV9/5ET786AaefOISrGMY7Z5FfXgV2LwCOdiNEprz6ycm/+UMfW9BG7eoYw0cAmHMwpDF73WPdEmgvrnXIqlemC3WzryQoyNSBamjla0LVdq6zzC3K0pdbBVjyYFD2hn/n1WMSMkz9JqFv9sM6F0ZybWeRwtY05h2kU+f1NkzfgtzFEYpXGmgdgS04T99AupCDxaRSfdIIVxzyex0o0M0RTbXQ/5UGGzNp0taEstrCmx1vSvRhoXaItML5fk5Y+2y1hX3+vI270H3LVscgi7c7hvPR0qdqV0ntd6n9Nkl4Bsr7ne7k679BV/3Fffcwh9J+Yt/trKu3f8Vr3pY+t2l3Ju/b9PpXlsY3JMKXZGWPddXZ3LL9bVhWdn+74VXeJ3BXYGhVo33QgZ/rfl+r1FPXwWtzXZZZf5KF/D2Pr889QLsVY8l21RvvnjvhE52Pzp97VrRBiBp94rM6cg1WU6xgF3yz6r37xH3xfXDiUnhhXIXzUmD1pZMVOlDFXxywQmozecU35oIkyh/MKeVgkyhSagnLWZd15j7sFtkVkts5tZacJBpt9YGQpBZJoDYDoAItaBdNFdmwU9ZCh+zu4Su6+hz7jy7c+7Jm6whsjbmyw0a48pURMrkNbThIEF7JmWeicg6bixZAXDBo0l77TWNQtLhRVVVkYG8rr3/tgc6pQf+3GuxA/gGYxCcQm8FjXpZVZ5tnHxWpaA+h5jO3B+SGKOjdl/4EGGz+cz77zIEc3/unxWCmNil8mbIgDeRp9BUVVmirCpkKkOeZQjEV0EjHeKoa10T8M8yWO//XWtiXLfOIc+LGLaMcTJrJhNjMjsXXEBDEzs7c9Hn3DkCYJU3RwYCERiDMz6cHKdwctenM/zXf/TH2LtyGdMPfow/v/s0BpkPneaFfi6AnXMFtnYu49kXd3B45xB7D45RbA2xtbmJ179a4nv//GsYzGt8VrwKmBzZfo73p1/DHfMAzlbgHMiFQKYUDCT0wR0Mnnocn62O4UyN3/jsCAw1amNxdht49cIefvfxX8ON1/4nvPnGt7G7tY2dnfMRDButUZa0FrI8h1IZ+Xd7U/EQLq2uqmhdQdwLgqwlGVlYhPfWeqZxCuGnMZvNontBXVWYzWcYbWxhUBQIu47RxJgP5rkUnP/aOYayolB/4V0AGKy2mFsKDee8ZQxjnKxLHIHjyXQCBkBmChye9yELewu9j1zQ+2RcE0GAc0FtrzV07Q/MPOGbkCLGbzdae9JH6isXGeazOWpHh1uB42E2ncE5i9HGJq33RwDexDnRbNPBt7QPBEQhHmGYWYKpOAQYmOLgQniXjxyZf4ce3L6B+bxCPSsx2GQwjsMygfnkEJP9eyj37yCfX8fF559EOZ9BZBSBIJcObv8H0KjBt58DmCSw07Kt9FMb/wPQoalAMdhAVgyxsbWLa+/9GHfuXQOXCkrtk199MYLLN8G4ROh5+FI61/1ytllHl8b37gEMzXMOXQliQc5LBLnWc+kTDB6kLjYi1WTGv5YIDQ1ESQFURyZqAbSUZzq0xZcQhek28GtLAqmU2v7XJe1kS/6b/sXi3KdAMNwLqCOVknv0xJ1r7ZXfvhOE7BaAamVrI532iCYPBIDcefcWe4pIeNYiPlwmVYV3srdEl+TpPsfaQmFr2LrSK2v+TW9Hs/xu/V3m+dZDoZNot9zfCwdbnQa3DlR6xqyz+D6xdNrSwnpZeH1jSg9zWgu/DVDDULWX8iLu7GvgwuaCnnXbrqJbXl8Ra6UlOCzdQ5cC8M4zLUDRN5auv10rMXj35uJnb3lK8550MuHat07Cs+s0M/679mmIf2ZZ3m4lnYOClXWcVH/6vB83B7QPhdaoY+GZZXWzIAu0iw/3WLrYT1F/7+VVk5l8NtezulmeaW2ovLSIT2YvPDWruRQSLGFCDjOQCxFJ0GazGaSUjX+zN08OxEwBnFWatFAEyglEc869KSmVZZ0lTXcrlraFdXUE1CGGcAi/Rf6gFpY1IYi4IwE/U8pvhgyKqxawpfukaWSMyNwCwKk1gXXGWQTMwYwcXtNrWUOC5hBMgVkkGwss3gwMxlhIwShcmJAexFDYL+mZv8mXWYNzTyzmSEuvPCkVmew5MC58KDSv2QcBFOZIG88Yg1Jkks+8pjpYHVhrILyWLZBvSUGM4nAgX3DOMS+JSZyBYiJzIZDnOYBA/maiNt1JF836rSUwxDgDHPOHEhmkFJiXJeqqItZrYyLTNgAfCsxBKB7DqXFOBzN1TabVcA4yU5GtO6RyXpIm3TmU1uCFTz+LXzr4AP/dv/hDiOkUn/2NL0MyBqcZqtqinmsc3NjHe996H+9+/wPcvL0HeWGIL/+1n8UXXn4CH3+twq5i+PxLT+Ntto2PPv4WlBPYtU+DWwmWjXD4+NMwl86COQd59Sryo33MfvO/xZP/i1/Df/SXX8V/+T/fwj/57R/gL/3KixgUAowDf/b5Y3z7xgR7f+vfx//wm/8Ad6//Y/z6r/9NXHryCei6xmw2AxyFGRNG+NB5DINiEN8l5xzUcAhjjPfBDzHsieHcGhvdFOqq9m4CpPGVnCwDdF173gVysyCXEONNngeoypIsHSyRDTJwbGxuwphBfC+1pncoMPXXdQ1jbTw00FpDexI/KRXyPPNrxCAXBaQUKOclrLEovI95VVXeukShLEtMpxPkeY5iMEBRFBTKzBgiePPAOctzuGCKr2tMJiWFULM+nrtvj7MW83IO7oj8kSxG1MPsoWFb6v/pBeguaEiFvfBAIEYEc0RwyJnfi7gPj0ekgvVsH4c3P4ABQz0+xpmLlzE9PoBgwN3rH+NTT+Y4vn8Pg60KxdYZsGxIhHa2grn/fdjZXfBznwdXW/BnKi1JrBFwmT+cAhgHBFcYbu7gmZe/gBuDIcYHNyBgsXP2DMxsH85x5DtPAOANOEgRVuer5yJQcIsfd7b4PW8PLlu8mQoBfZLiEmF58WMe2tXcYAv3k6tROlmutWWtR73UkhYTtZ+Ia2a5cOeSvE3BqeYlFa4aE8HOXHQkp8henaYeIbp7pBC7Gw8wlos3XaE3/ZEeYLQaGsZwYf300rfFUpLVfKK8xBZ+Lfah0dC33+GkljYAWhjKPl/wBqKlWu6F9ibm9k14OAekZh2dkYu/k4W0sOQ6bxmL/13x/v1JpGUC+hr3+9vqWmPU++y6MnUf4FjSlrXTJwHKfL7erB2A3ltuZzGnW/dafVkFnmL70vexv9SVYc7QebfSti5bpEvatTDkrl12d4+K2+6yb31PtetoaRfTSYvfF7nke9bfkOTvkP+kNnX6ugDYFwaopw4suZZ+/pI9ky3Ju1DdCsIB6lr7a5IWuzL1zf1PIa2v8fbAuKqr6IPKGQcTzE8QiyzYFBoL3sTUePZkEqDzLIOxDTgQHsRFE3ZLoEsKAdIAkwmzgfcFteSvLDixIVtnyS/UE6txxuB8jGjBuQ9nxBO/RpBPudbIlIJUGQZZ5g8SACkJ0AYCKgdExvHMk7oFbT+RspEpdkNY5UmKLI2DrmuAgUzTPflUVdXIsgyMMw9mAe53TGM0hG8vF8wfCjhkGfmWc0aCOOCgVBbBv2MAM8yPK82VY3TdWgtuDLjXfAZ/b8ZJez2bzVBVJcWUdg6CNfG8dS1QM/qbBbZ6zsF5BueAyXgM5zwjuxDReiEczDBGVgiutlGLGQ4mhsMRHSb4ONxSUrzvWmswn49zG9/M4HrAQWzpNF7eGsCRBhWOiOGyjPo2YQI7F3bx4pc/jS9Mb+O1f/ldfOqpp2FroJoBP/79G/jJ997C3b0HwJDjiZ95HH/zi1/BCy9fQJ4p6DHw9M8J/Bf/19/B5J7Gg8MaZ3Yu4ur+93Bt/zrOD6/gMt9EVjLcHo0weewizJMXwb/3Awyne6h5hvc+fg2//KVfwP/p//gH+PAH1/Bv/bt/Co9dGOGVp3bwM//yf8Q3z/z7uPq3/y6Gv/mfYfDf/g6+/Ct/Fk99/qnI8k9h1CwMp7nmnGM6naKqSgipUOTkFy04Hb6U8zmkB8RVVYFpHsOrASKugbquiChQCOScCMq4JDeAWpPLg7Y6vtvWucRsnACr8+9y8MVmHBCgQzpbhQMosvjIiwGkknCW/MSNJ/1z1gKODt8EF2CCR59xIJiG0hogzb+CNRrVdA4HR8RugdCRscgNUM1qGE0HEion3/OqqsDgOQn8HlFVFVBW8YDi4dMi/VIjCXa3/cQ3NPUFTiRL2hMlGOPxMBCc4/iQYevsEfJc4PqtG5geHSIfDHD3g49Qzcc4OrbY3lYwR8fQdY3B1g5kPoBQA0jGYSfXYMpD8HOfA994nLRJXqqgb5kX6F0we/a/GQMHR54PcfmJ5/DheB+Tcop8MkaWZSgGmzDVGDLbQlAlhTjNLn4okxFwYbTa4nLXi7YzkK1hisJNB8emYK715ILZ77KvfHM9WMW0ndy85nHBlrwPbSe3WdRdIoLvsD5abetKRX2nC8nosVaT0Q80un1Npaok9nZEZS4ZwMZcu5m1pJyWVjX8DjmbeWta0I4ZHvOwTlsQmtGpr2ctLPa1LTW2tcXt9nT/Zr3lhe6x5Dxg+XudXumWGuYr7dfCYUE3sW6vugglRYTdMQ+HBu2c3dpcUknzRF+E8IdMpwIgTWILf/Tejb/SLXdBsE/uLQzhquSaf9bFOadKyx7sW9bdiXRLV01TxuJS7Qel7demvy+tNbKivauudbOcBHqSLTPkOxF3dvrXnf/2nhdf7Ydf78laa1ewInU6sW793Vd/IaQYaz5vPVO70NaFWOndT1vPtb6UAuiFviTrt2U2n7Z92Qbdauyq+tNvycnt/deR1gbeFNOXoarIHznPsygQC0naae3NmYX3/WaMIcuyqOUNZGIkSDZCbjB/FpwIs6whwEpKTQsdCNFAg8oZi2RdWhvMZlMUReE1tUTuxhhDVRP5GwMI6EpEzXj4gBJrt/BtJyEhaOkZGJhgyHkeNeNKSi+QOr+wXTxwAMiHlPinCAwGs+ugwQNAxFDOwnotofL+s4FkzBmLsvLkVHWNqqrJZxkeeAgyHw++YVVdQ2UKUkpo7U12rQWDRZHnvu5wQFBH03TrY38Ph0MiiXMOwq9o6ZmzAVDc8KpCXhQ0z1rHuZdKwdQUr1lXFbTVKIohpBCoLJntA0BRDCAkkbZVFcUgz/PGRSBTdBAR/DmFJ98LvuDw4A8MzW9HYaqCRUEw4Qz+4VrXGA8ZSqdh6in+jZ/7RVx78xqePH8GhlmIDYGX/+JjeP4vXIRSHHlOBFewgD4CZneBwRXg9jsf4wc//B7OnftZfDh+A2pjA0eTI0xnhxjPH0ANL+KKehbP/KTGwcfv4v1sgqOLl5BvPAsjB/jq7/8W/vjrv439t2/j9bdL3L//r/B3/sNfwrNPXsEvviLx0f/n7+POZ76C97eex/Q7byK/ex/335zgma88hu1LW0RACLIIsMbA+Pjxg8HQx0VnjWmuA5m6e+0yAO8HLClkmHOeeKwhFLOWwLfwZGvGEMGbc4D26856XgDGSDNDgIN5TbP1sbjh+QSkJ0fkcY0bS+4OKlMw1qGqSx9DW8FojfHxkec58HG8pYDzxF9CiOjmMJ/PMJtOaY+RxPEA51BXNeq6JsZ1QVYZg8EQo40mhCHFpp976xrXmOUDDc/EQ6Yo0jJEMBYFXpaKvO2nFv50SWnMg23GAaaQMwYmOKTkQFHh3e99A+ODAxzv34WtRhhubeGjSYnJTMFhguHGEBYl6uoeNnZ2ofIacrABJXNwN0V959uws/sQZ14FEzntKVGiovmNvsF+rwNnYOAoBht48qUv4O7Hb2FejbF15jz0fAw9rbB56WU6RA1L0oEOC10YkbY0FMFMB3bwBLLQjQQ4BZPcZPwWBAegrfUFEqCN5EY6N20WcAL2DTBKv+aNrz5r+tGV5KNVWFN3W8vZlTOIaAxJXW3IhJZpPkvHoSW4Lo5PCz51lyPr9C4pkAHxAKV5vCudtetzEe102pHUzz2v+EKDejTZi29P3xvVFsVZUn8zNu0xiGHGWuOePJBI5v3mmYv9C64jfR1o6fPZYr9Cm9vm5IhSKmvl7BxQoNl3UojdNwPtmeygOS8Bt9bBv0YBtrVbLEElbTCWvPQOcUGn70jvdnxS8sur9WgKcjqXWc/fK8teUS7ry9e3la2qbNkn6LRIM4BW19PH9ivY81iyhgNYXLtuX0IY61j/kvXZHZ++v3vGvXO+ugBe1wF/C+Wv6mO61bhkHFelntOHhUe6axXJPCVzvqyq1rJYsp8ttKfn2bWXV2+FPU8uewFPSN34Hqcr4qQBOF06Fat5MHu0lsCTc+TXm+c5aamCxhssITXSXvD1/taGtLp5lkHKArWuI8lT8FElX3AKcSO4QO414+TTauEs+cENBkPyF/YaU7gQFsdF03iAeTNqRECeZ3k0hSfCMGJDD8J4Gu9YSQKozLLoa8oAVD6MF8VbJiZx7c3Rg4YyhA0zxmA+nwGA91vOoh958HkHWGSern3YseDLG/zFOSOfcSLCog9q5s2LSUtgveaPJArnHMpyTmbH3lxbKgVnHWn2nTc/5xysKCiElA8R1sTwljFuNvc+04HJnLTUEk4K8ln34Fl4AjshuA935WNQexbscOjiHAiYM0Q/b+vI99+BSMSsJ6ALoeZoXm1cX1KSiTDjrIn7HojfwOCGGfYmR6jHJdRwF0899xLObg3AlYOUgMgZhlyBgcEaQE+A+R2N+g6w8YIC4PBHv/m7MHWGA7uDiTnEnW0ON3oS2e3byI6OUU0/woflHYzGV3Dl3It4ZVigvHgGV596Du+9/UO89o1jTPfmUFbC1Ro3fnyA//z/+TX87//jv4rP//zT+P1/9sc495M/xo0P70BvvIoPd8/hu7/7T/DCzfP423/n3wSXxPwfSNWEdRgUBQLjPvc++/TRoC+Z9XPMeQ7NyT0BcJ5gsOFGYIwO08i6xPpY1wzccTgOZCyn+WN06FFVFA+dMUBlGaTM6f2RIoYjq3UNxhgkF5iaEoJT2DPOOaqyhHOODsnKCjOrG8Wfc7C1gcozSCExq+aoa29uLjNwSXG7y7IEXIgB78GptfGdFkqAgSxEhDd9DyRx4d13rnGVESL1gX/I1CNwpKa7KaTsE5778Qe9JzxI/BJQzkG4DIPhFehXPofB8F0YozE+3KeDk3wHe0djyGwAN65RV0CeC2i9h42dLeS6ghpuQKghcgHUR+/ClAfg5z4L5GdBJq0kfaQH0iGFA0fGOQbDDWxduIJ7H/wQ27MJpJRQgwzV9AHyjfMtmOCSj2rUErOObjAIyImwE29E4aIHrSwZv/bvIDV3JVfWblyoI85BX/nJqLiOQN9tVEvaS7Tn3bpSYr5UEkPXr7ypN73G0sY6eBDZLiuZjdbRQqv9ji32Ob3dEUCa4U3a49oPMoTDF3RSM+4L05K0sX38sti4VOhvhecKT4cGtBqcFtc1b0c/IEnl+yVyVzKMS1qbri+3cC881Phfh4FL10S33SH5b0A62cl7k2xH6I5SX09amGEtNLBG+gSKWCXJ943lwx4adKuJ20R3WtzJVawPDxbTMozDOpl6jIoeroLVl5utdK3Mq7OsjHe9opDuNtxbSfIOtIH6ssasV/fCCUvf/Z6yFtYTmv63DK+65a/brr7fnef7rD8WxmZVPeu8S+k70Xlm2dpdKJ/1Zex7qK9BLH53etOjvIyfUFobeKvgP2pNDOVDQj6ZYiuuAOaZzwEYRxpixolYTXjW66KQZFoKAkrOOpS6AmME5Jx15KsKIjpjDMgzr2n1PtNKSa8JJ82rlBLBBLfSVWRMDlr3GPaqKmmB+TjYBNA5jCUWbilpOLQxDbjzoaUARJNQS6o4AMxr44KJPYETIURkMiftctBokzk8jVETH9kB4F44sI6Iy6w1cIyY2sM6cSAgzVCg8qHc8iyPZvRlWXrCOJonGUG5BRfKa6rhDxU4lPRkc8FXWCpYZzD1GkXyIw+JedNkIo7LszyynAditxgiTRtUdRX9i5XKMJvPYO0Uw+EQUirM53MAZGZe5NSfGPLNH8DIcNhiLeblDM455MXAE8ARuBNcEIGXZai19r74pAnVzoFvjPD1H7+Bo2qM/YsZ/t0v/nlsbORgDKhLh/F+hWIgIRyHnlpwK/Dgx0dQQ4XdLYWrb9zEd7/+LTz/4q+gzK9gfKeG3d4Aao758HHM6hp8VkNMpxjP9nB3/kPs/NxfwJd//S/i1j/9x3jw0dso1SbccIBqxuC0A69nuP7OAf7BP/hD/Ad/76/hi7/yBF779hxnzuQ4nn6MO+Y17JmPoX/yMQ4ffAWjMzvg/sCLMYaioJBzVhtMZ1OMhiMIKeI8SiEgBgUd4njm+ywj/2Vd68hTYC2xiVM88AEYGAFrY8hlw0vLQUPsrI0HMdPp1K8n8j2WWUYx5BmFjKOQZQpFUWA2m8HUZNlh/WHNcDikd0jD+4JLSMl9WDE6SOIccM7CaAbGNIpMAWDIMn8/aOmdizwB1lgI2cSwn8/nMD5sIXEFkO90XRl6Pw0x/w8Gg8jG//DJeYDB/K/mevpNoY9ex+w1ChTdL4YAg/UuNABzJTbkDAc338fBtfdx86OPUTuJx5+4DD2dY/fcLm5dPYLKa2xvctiqhrGAtoDdP8LGzgYdFhYlVLEBJQvY6i70zT8EznwGbOsZgIm0SW3zvqjBJOb10cYZHG9dwPh4grPndlGNH6CSc6jhLrhQEXHR9paMQgocAtBKEZ9H3+n4LIL0tJHNyKI3n2t+d8FHn8olNCHW34PCWoX0pURK9x1oekj3WACFsfikYz1dYUA0UV5ss6+AtQalU4Y3F/fj0Zw5sMVHuuAzOQhwLl7o7TZzbZPrPm1c89MBrvFxXtTrpk3rr5OEWD+qST3Rb3oFuEhmqCndLebqml13l6DrPNafb9EUPelBT+t68i1VETI0biLN42xVXKoV7cXC74dEsI+Y0qWznszcWWThz1XCeCcFILTg59w3lJ1yl+Gy8HtZG1p9W7bI0j87BbU0tUmhAQCtKHJJI05I3e18Sbt+Oqlnv+pJS1dsuqD6ykgA6sJW3LNHRvDas/TCuonVtV/PxQOTRx2/pOLEELLv67WYOm1berDR3ctZ84ldVfaJXTspA230SzPHJem6F5L8DzW+y3bHh0trA+/RiATl2WxGGmZLfp2BaCvE8uWModZ1NG0OMbWVlHGBcQ+UjTGovYmn8WAXcFAsA8BgnYatLZydwwExfJdlIM27MXGhWxDgU4KYkp2z3pfZh0kSAnVdo6pLFLJIGNfRmNVqTQK6N5UXgqOqa9RVHU3BHRy4kFDezzUI68ITxDmQNs9YA+5ENJ9XXqs2m05RVxXFJvYxl4u8iFpigIGNRmAgQMI5A+DDwxA69xYCotEQQ/pDCQL9EAy6JG1xJhX59nLrDwhsjJ0Nr9E31sBqi1k5gxISo9EIeZYjeOfNyxLMaxgdd/HAJVggZFxFX3RimJYw1kLrEpmSUBmFAAsxpOuafPOVVETy5kn7gjaIDmAsSq2JU0ApIpWqaU1xFoiwHLSp4awj7asQrTdvbDSmnOHbV9/CL//Cn8FX334X/6tPfRZHh3M8uD3HWz+6j8vnh/jcn7oMBgbBGKoDjZtv38XLf/UpOO3w+7/5B6jKbXzq5V/DP/r2P8T8yovYYo9h66NvoT66A8MdDOeolELFJcxwiM995ZcwUwr/0d86h+MHL+DND+7iez95gP1rDNXhELODY6A+xI237uIf/9e/i7/+G7+M17/9rzA+PEY9LvH800/h5vkrOLr1AA9uHGH34gXAeRJCBjI91xq6rimcn/McDBKRWZ9cNZwXtOn9yvMCzjlUZYX5fEYkd0qCaYO6rDzXAvEOcKWQSQUmeGTk15qAKpcCeVF41vUS1lHYPCEkvQeCwv4Fdn0AEIx8yIPgqjWZuyulUEgFzhis1YCQ9O5bGw9uwr6hvSVJw2xPZVn//oV9RPuweCGknkysaYjIkUeQrnWNqq6jhcvDJ+b/v9npm+JY8t/0if7rvWWDDtCkHcPBYXjmMWT3DwBxCzdv7WFaWbzw7BUMN3Zx+SmJw+MJhBlgS1oIzjArnT9QPMJoc4hca5i6RrGxBaFyMGdg916Dmd8HO/sZcLXRfMB6vzk+3Fg+xO6FJ7B/8y2U8xKqGEAKhWp8H4OdK/Rw4hbjMZbvj4vjFr/4vkIORCC+Sj5qZegbzS7QS6Rols5WF9VTAzql9bSA9VzuAW1dqSMYdrfCW/VKNQl4hYd/S+SH9hjxeNUFQBbrbZ7mrTXfLtHBgflxaJ0agMax3yCcWtE1Te9tcLwcrCtYLDvNvJxCbYnMvGo+lkzhCU2Md7qHAs1fbqn2btl7v5ArfX5ZQ+Ih1rJ8fWPu96b1piMChRMz/jRTp39t3jnXeZnXSwvz0zdfHbk6jdd9og+yf76z7WPJz8X2das/DQheUclpp+9EYJ7ueasKP2kNrfNsaxtky59xrX+anYhhcWBPeuG7a+CktnZvr1gnfe9uK18fQl73TC4UwRanZ1Xz0/65zjgt/e4u2WZ676/caLBYyap3+4R5WGjvaXDyie/bqiO106W1gbfx2mcuBAopoX0s6RB+SntQFACQ9CGfWNBEefNtYy2c1wSX3sR1NByBCM2MJ4niMMYiZ3kMU2ashgPFbXYOEJxFTfRkOgHgIisxYyQU6lrDWIrnTGG2GIpiAMChqilkViCNsj6ONBMsErWVXnPHPXFTIFQLmmzmfc3rWkOCRYE+kE05OA9qKaYwGBF/VVWJqiJNZJ7nYN4svK5rHyqNTNSZN/VmjMc4w8H3GSDirABClVIReFpL4NxZBw2yLlD+cKGuKtKGB2Z6AEVegA8aFwIGbyXgXQuI9I7yCkgAdIBAbOmkSa8qAm6ZyOPY5EXuSflIC2t0HcGzENIztMsIugHyJeaMgXsf9KB1Dyzz8CAyXKtrClcmhfRtB0LIqxkzuDW9jz/10mcxMRb5nGNbDfD26/ehlMCXfvkKzp0fAYY0lTAMpjSAcBhekLj/8X18/ff+EE889Sv48bX38Na9d/DMhZdx4T4g8SSQ7aDmYxzlFT7eymFyic1Lj+PXX3kV39U1zrBd/NyTHH/65Q3c/fIlZDxDbbfx/t0jvPaTu3jjnX1cPbiHf/W738IzrxS4+u4RdnYu48rZp3D9zgE2tq7g3rUjPPd5ChsmPbFdrSnkWhwDb8pPhy908AMAzlofo1tG1wtjTHQTgANxFjDiOrC1JV9n28SgD/HojSFWcqM1Bkp6ojwbCcq0JmsUCjVI69waA12TRQMYoDLVvM9GQ1cVhFSAs7AgFwJaZ8TfkBU5pMnIpcCvbSZJA+9sc4AAR7wJYS9yPhxdURSQWU6kfQjkijZyHGjGKDweQ7QUedgUwEIDb3pUDQ3qREvr1fchTj48zq9pZucQvMZk7y6uv/MW3n/3A1y/N0ZpGK7ffIDbt+/iwtktZIoO/u7s7+Hy+W08fXkTQlR+f6lh7RRmpJAbDWdr5Js7UPmIOBMmH8BUh7DnPgcxuhyJ1xbghpdomJAoRlvgaoTp+BhbUsHqCfXTngcTGSJJGwLo8OAslYiiwOpjgCdj24mM3WpGW1vM0oeSvKz1SAdDtlMop+cj3HCDNDcWhOVu20L9sTzWeqC/Ha2FkDQgXWHpLbbYp9iw4DrQlLGYifV2pH0QRRnaoGKJ5OTCPCcXOrm6w0vaurYkygCQC1WSJxa5OHJRl+zSvKkmfVUK7+Xi1dQyJV2zzVpur8lYRtLhvuFfGIvW4C4uQJf+1Rqj5hHXfmShHX23F8tfkelPIq3YhtnCHyek8Lq5ZKy6dbFkfXbez66onZbRCsPurz+aON7zDj9E6razr5xV+x8LS3rVQvmk1kfP1t6zdTxU3Y86livnctnNpH2t7iRrpPV7nTH+k3gX4xYW3oWuC8f6XvMAAQAASURBVNWJqPTEslup93u0bnmdTXVZ6v049xfXuzc8asEr0trAu6qqGOfZei21yhSUzLyPtfYgstm4uOBRA0VkXxTSR3tNmXMOeZZHYqNgBh20t0oF0KlhSkQtHYFY6nzlTWkDoRuFyCJgKoQAlzLG/5ZSQvJAdlZ5n1TjScScN03nqKsaM0Pab+sIUBRZHsGOsQbMEehknINbS2a7DNFHvK5rcCZgGIFnayzA4f3dFQzzpuAArAMQSOREQ9TmnItAWEgRmbqpKkumvFlOmkWvPfe6Iggpo+k8gV8GJECWADwB/jzPYwzjQFgHRjGcrUPDCu+1kCyCHIOiEODB/9voSLxGWkkOeJ9sBHI6peJBgjMWjguAM0guyQKiqijUXF1B1xp5XoAzJD7lCuCA4BR3XUpycaDDBOHXaQZjDSpn8eGtG9jaHOH7H17FZTvE7fcP8dX/+TX8h//xr2JnZwRHrvqRiX9+WKLYJbPnr/3zb6KaK2Sbl/D73/svcIHVGO5/iIwBjlscDh1uFwr3d4aYbg/B5jP8mS/8HH5+exs3j49xr+J4yk5RVTNc2BigHNe4sGXxzNlt/IVXd7BXAUezEj/8xo/wwx+9gzPnCmzrHRRqgP3ZHUihsH1+RP71oiEnpNjs9MWPDPt+vCnUHlmOzOYzMAcMN0YAGLgnNRRSkpWHtXAgFwMpFWpdQZeG2Ow9SR/nZP7v4IBaQ0lFMdn9IZDIJYyuYbXGzJMHAoAy2vMCEAEb4EOM+XebDuMAZzT5sJfaW0IYKJn5EGHhMIsOFjjIn1sKCcstnBOoSuOZ+MnfW3o3jqqqoLWBZAyMkXWEMWQVw4VrDnj8+2ZNw57/aKkVfdlf6WNSboDMAoaIQLEBRM5aoDyAMTMolUMMNqH5CMPdIXbyAXRdYnJ0gDsHB3DOosgzFMUApdrFoTiHDT6FckeAnuFoPEdZ1dgcaVhdw9QVBjvnIYsNyEyB6QPo29+A2XkRYvcFMEGHdo2vbON3yxmDynJsnL2Myb05rLXIiwKVNaim+8g3LyRmxh3JNHbeRQE5fAC7wmxKARXH0aGdsTWGaH9R07xeomhRrbCFEvxzLGboUL2lsDV9ovkrtf1MsW2fRBjHJ7SxR8JsS0LtGpchuz4k2XuzhVbbxUbwwpLnkwOP1vW+d4j5/0+RYVfiTjuQosSG93xB894dKtc5ZADg2jPSbVX8q80037ybYd5Cuxd50dO6OtPG2r3q9nwxpdJ5j/TusPAjLut06aTtiUiqXdzyFiRj0dqpforyf2fN9h1OtNK6+3SY0qTchXBIrp0vHc9uI8L2t1C76+RJl+86bUR77lxy/bSfpAXQ3WnfOnChC7Fac98Zs5XtOEXbWzuAWzEXC5k7je1rc/L8Ipnl4m6UrsGlXWiZQnTq8K9vK2hFsg4XGrqqomUTttDopi+toh7yxWWtb98jpr4BD3+flP/E+2z5rTXTCsv1ExqxztvUn9YnVzNkGipFiJsNKJl59mtAFDnAGIU98mCbWKhJWHbWggkCiOSnWpDJM5gHCvDPVh4cEEAMxE6BHRyAfw5gjNiwgwkphSbjMcSRc8SWzgAISeCQAeCOTNCtpZBUQlAYq/CMc5ZMdS0J45xzaCEhVTCJJa0cHFCW88RX3EEpCeXNu7UPl6S8GawLpr+x/5w07T7edYjV7cAiE3QIe+acpbjOcDHuee7Zn0NILWstiiyDEw5ME7gWCZCvaiKSc4zBaCJ0C0zGwodvg9fU61p7M2EG4+gQIsuI2I2DgSkGLuhQwvk2OVAYNedZ6oM7QDC/L4pBnEcpBGoeQA+ZlnPWaFmrqsJ0OgU4wyCnsFpSURtZbLfXVDoLa+H9womYTXCBsbF44/W3ccwzPPn857G9uQFTMXzpiy9ie3MIU9Lm4knSwQRwtDfGcCfD+MEUX/vqj/DU47+KP/7hf4fD8Y9gJMNEM7hM4FBx7OU5ymEBm4+A6hj86AgvPvk8tDEY372N473rEC9nyEWBQgwgRsDs+BBuI0OuMuxIjt0NjjNffhHff/1jvPDyRRT3HwPj5BpwY/8a7lY3wPinwRlH7UN7qUxF1wegAbQyca1gcD7et6WTHUah8rgU0a2hLEs/T2TZoFQO5kUvrWs4H6pP1953XElYbSJBGr1/ChUjf+sAqhkYuKR5NNbCaU0HPp79PLyf8SAmz+k9lAK5zAnoW8A5A+EPqpxzEJ7osK7mCAc6+aCAkvR+1FUNbQyUkhgMhyjLuY8k4Lx7BYfKlCeTIyse7cnptNZQYu3tsDfRRy/EsGbxXeItINF9Ytl+n8ACBkDPwKBhHcP9ax/i/s0bkJu7eOWJ5zAYjmCtRjmbYj6d0MGltTB1CV2XmFYWfOM8Rvk21PwuRH2M6WwCY0po4zBygNu7i3yzRD7aBZcZMlvBHL4BW+6R6fngDAKBXyp8Btebja0zmB3dw/H4GIOtszDHh2DTPWQDKs8FKYT5senatKH5lHHWlE0CC+vk6hnK7tedpQJRV4BoJMf0Mus+32oVsCBJMgbWYQ1jSGSyrhgUowH0JOYA8I4U0CNdeWQXSEwX+92RbRityabf7TBeTeqE0ovdd75Y5stOWLKDVJxIx/1m7Yvlds3L6WI3f9p/3vS9VU7zH9dzWNEnfLeKTtoTb7DmSgvrLuZsQHiwhuApb0GnSCCxHmk6sNjs9rvvkr8XW9F9qlkvLNbbCWjWJ2gmyziWnVS8NEbyw6YVgKBXpE0vBkxwksCcCPgLQDCZROfac9ots/VYsm4WqmWL+U+Tep87oY/p9pMC9lPV0c2TZOqaRX/iBzAL73Ezr625WHP9naZtfVvCJ50620x/hlXXl/V9yXi0v80rqumuq+67sk5Dl+wjSx/rgu6kbwsHJSvX2xpI+acxmQv1P3xan1zNC79lSSRYWUZM3iGsFPlTuujTyRhH7bVzUimvbeYoq5Lucw7uY3E76302mQdPnMjTGDyIBpG0FXnhWcyJPZx8oDmk8qa1cDC6AYyc2JlQaw0uBKRsNljpWb5hLWptMC/n0HXlTeWlNxUHfVAZw3Q2waAYgnktsjEaZV2TiTXnyPPcm6talNXcgxWQdttr4Ilp28EaDacrTDy4F1KhKkvMphMC2lkGleXIi4LCfvnnGTWHTGKZha4N6prF8RBCwIKI34IZvjEE3rXRkdiKrBZIS88YabbrugZcwwpvPTFXIFkLRGZaG5Se2CyQugVhXGvyI88HufefJbNmIQSc8Gb3jv6FP6yQMhzkGDhHhyzBBJ9Y0PO4BoVn3DYhxrqzsM41Qo//uiqpUNcVrmvg6vc/xjNf/HkUPINhDEezOb74pWfgjANznCI1cZprrsg3Pt+QeOObH8GWV/DxpML9/DzMngEM4IyGMQ6ytDg3EbAHDCbPYYQAE0M8vnUOxgD27od4IrsFZh9HJhS0qaFkBjFifqU6wJF2ejjM8G/9jZ/Hd752E3IuYJ1BlhdgjOP6nVt0OON0PKCxdg4pBayhWPFCCrjaehK6ObIs86R20rOUe1cBv4CMD7+m6xq1s+CeMM0xIJcUJnA6ncJog9LOGwsSqeLByHQ6pRBwIKZzazy4FwLOk7cZo8H9HsEYvfN5VkAbjSzLYZ1p3BQ4j4dxDoDjDtZ4ZnzOQYIkg6mJQE8pYlOX3lTeOcT9J2h5yPLCwhiKjgBO+1Jwf2CeJDJY8gR3mYdJqdlpEJZZBDguftC6TzWpfTcKwYxIKG09Qy4VyskBRLGFjUsv4MqV57F75hy4fyeI7NCPaTnHbHyE6dE+puNDTOdzuEGBjc2nIae3oJhAOZvAHJaoa4PNrQJa78HqGvnmLmQ2hHAAm9+EuXUMvfsy5M7TCMRr9IEnCxvOGbJ8iI3dKxjf+xCHe3dRDEdgroYuj5BJYjincbFecGYNo2vS996QTW7xO+qSe62hTFFZK3eCcJaFuEorbdHNJhWlUu6CNI8kf08/WtJ6J19H2Gj5l4eNHyxmatjQ/ROdcWuDbxcPA0gTExdXkzNYWaXNjRcSkLjQt+RZBjqI6G0F2mPlmiFp2t60gcU+dx/oji5b6HtX8GRAG5T3TGXfcxHILJlO170SbXQ7rUyWEOvrRF+D+6ttl5c+2gpB1/98s1QIrbVk4E5z6YHFsX3k1DeePb/b6w/dqVu7Xa1RWSKvp+ugdStdo2m1afu6r3//5eWpZ39bsnstfXzpQcCyhqxEZEtSssWsY+nbHe/+fSW5kP5mbQDZX/jydi7kY4uX0rnt27GWHurE62zlpKVztvZaWDbRi5tN76dn5Wu1rOyF66uidCxp1zqLvgOww94ax8h17qG7m50mhcV60tPhQwBEQsrex1pfkodqUTetDbxjuB2v5XLWYl6VUFJ69mwO6wmdBOfggqPwfrpGk/kqPCESAEjGPbgm4MC9qS9njACaJ1ojX2TyMyXwR5pUCptFcYeFDx9VVzXm8xnFnM5zinMsKASWSwAHZ8z7F2fUVk7aQmsCe7lnJs8I9HHGYJwnCPNm9QAB+83NLTShieC13DYC6DzP4BhQl3PU8ymqcg44i82tLWSZwM2PrkJmOTa3d8BMhY3hELPpBAf3b6EoRpjOZhiNRhiMNjD0/wMjH2zAwVk6WAg+6iHeNxcCRd6AVgYCRdZSGDMKayZ8iC4OKRjKqoL1vrHEEi9Q6wrhWLUsCUTnBTFml3UFB0A4CtkkvLVBMLd3zjQkcB40Cq+pDeR6zhpwLnyosqC5J5/4QKhGBHYEpAS8RlyXzebMgMCIzqI5tcJsLnD5S1/EE88/BxiHytY4ciW4ApUnAcYbcY8xjmd/4Qo+fP0m3vnWAxhh8OIXc1zfexV7N/45mJ1BGgluyISZOwnGJDCT4KyAyM7jQrYJXTvszmZ4880D/Owzl5FzRQci1sU2Uo2kDy3nNY4PONjWp1Hvc1QOUOefxvFgE28+dRmlrjAqhtDQMawc44C1Gro2nkE/mPJnqGuykggxvp218aClsg6T8RjFoICUzeFV5CVQDSt/iJvunEVVVqirkkC9EBRP3jPZSyFR2RpKZf6dDnsG8Q0oyeNcCkkHccxrPqezGeD5H0JM7aAZZ4yDKQlnDKbTSQTIeV4AjGE4GFIoPCAeMITQagx06JXlBRGygcCHdYaY77mAysgsPViVyEfUeKfSzwI4aAmTYcW5dt7wAQgAJwiE1sLoOYyZA0IBXOLCMy/izIUrUEoCsD70novx66uqxGAwxGA4Qn44wPhwD/MZHaZsbj6BLNtCJm7B1VMcT2cwtsJoM4PDBLqqMdg+g2ywRVYSdoz6/msw83sQ5z8DJjfg0TN9HBkDhMRocxfj/TsoyyOMNnfIaqg8hBvsgHGFEL6t++VtNODMC0JhXJLBdECiSm6KOAkUJVJEN6RSKy0VesIkLJNsm4fTMGxNnb5rPQJObz2xlQk6jADUNe1YKKtfIEhaES1kFrXqTT1pFtdetItt7lTPYj9cso4TANvb/TDv4QAg3HKLkmXMv7wZi78b0+m4jJKMfbJWzBdvNDYCS2tnzZMMbMHcOF13S8vpEd4XsjTD1Gk/6/xeUg5L3oP+ahMNvI80EN+9hxODu8ktm9qTUt96WlFOew/AagSL2GX6u28Ae/f29dPS6nv3nTVE/GWLt3t/2WOs8/uEpvW2/TTzcIoMy9bHwjbMknFdsahX4fy+5pw4xydsh0sLX1XeqpQ0fGX7VtWZ7B2s5/qJz5+U0nJcz7Xkenrmm37Tu03o7+vyRq41d31p5eb+iTzQSuuTq1mDuqq9jykB19lsiiIvoqZJa01a2shqXRMRmuB0zRgYa6EEEY3lqgAAz0wNMEYhiOqazNqdJcHdecmF+5BkVV2BCY5MEaFWAGvBvzuYnRtDrMaknWeeAApeg+cwm80gOPPgtYoaah5CoHlNIAAID3LJJFzBesBI4EZ4v24NxjmUVBCSSKes1ZiMjzA+eEAhkwYDQANVOcdkMsZoY4SjgwPcv3UN5y9eBqzEYFDg6BDY3NokE9qyhNEVjvbvI8sLTKcznLt0BZvbu6QdLgoYb9YeAKQ1FuW89Oa+jYm+EoqAB4gAizkCHiCr+6jhTuNAGx9DWmsyDeZCoDYa5XzuQaSPKe5BmdFEqiWVhFQZqqrCeDKGkAKDQkTyPGO0N3HOoHWNck7WEGVF7R4MBmQ67c2Mc3+QIIzB+LiiMFZSRe08YwJCEtEWcxzn8xG+8NLLsMagshqcOahRhvmxxuZmDsYsad5Aa83MDOzE4trrD/DhT36Enxz+S3zjjbO4vvMMzssc2aykj3/tzfgxAEcOxYZgyLHBn0AuC8BwPPu8wh9+bYav/quP8Ve+8gKUyEmL64gAzcHAGIfrd47w2791iN/9Z28C2y/hN375KxD1ITa2noXan+F4b4rjo30MswGcJXIwxxxMRa4cpKl2Pl46xdMWksDqvJyDDjgIyHLBkWUZBsOhj+tNMxeY7rXWmM9m4EJE7gOtNYx3G5FSkmWErsn821rMatJWh7l3hnkfce0PYCys1jDe+kT7fYQxYuZP31Vd1xQNgPEIAoLFBkCcEfR+cpRl6Q+fiDCwrEowzpDlGYVMA/cHPaLBAkAMK0b7gEFd15hN6XBLquwhtlBKrPUlaac2OROiVLFMCGOdm84T3slBjoM7V+F4jo2tHRR5DiaaCA4uuHkI2pekZ5mXkt7D8eEeppNjHB5PsDHawcbZLbDjG8izMeazMeq9Ocxmhg2ewe3vQVcVitEWRFYg44Aevw9bPgA79wWw0WXfDxLiBefIshwbO+cxuXuM6dE+Ns9cgDElqvF9ZFuXGuAXQXczKi6RtNLIX+m4NIOTXGpldPGLHkc4SGRdLTdDPCztjnfvzPSjOjTgtZn77uyyllR4sliwcDjA+sttci8CvYU8LXN4hsQsowH2LAFkHQF3wTi9JXewDiZj3SFZUJ42M99Z8PEfDyTDveBi1K26VV3P2Dq0Wt6zjNpIjrGee6kpctOC9viEgw36XxPyjSrr+hazbkc60uYioWDQRKX1p63xYeLSspL3YtFSISl/2doO78wnIZT3FP9Qya3/fFja3UOPdevprfsEYLO4lzwkGFgxPTH13ehDmEvmd612ddfSJ7QG2vsb+se7J/9C9eFbkY5TT5u7w9H6diT3F8ro1NU9tHiY1At8l93rPpvuJWseeqxs6jpjv/KUDqutE3qqW/jR2QfDOR/waOO8UPbq1qxRyCf3Eqzv4+39NLOcCJOcdo05NgApZCRXo7BaJOSSL6aOfpXWWGjUkDKHhYXzmjoKWQRYwb2/L4NhFK6KJ+bscJ4szBLQreoq+WYyrw0DiiyHdeTDSWa2jMxeuQAkxRLnnuGYCQaJjA4FlPKH/0ROlXkgro3xPtZAluWROC76ZoPFsEtARh9C6zA7PsbRwQNkgwIP7t7BuXPnKQ45LGbjY6hM4fb1j7G1vYPpdIzZbAoGhq2tDWxsboIxjt2zZ3Hj46twQmB8fIjpeILp++9BZQrbZ8/j4qXLKIoBnD8ccCALAQcH40NOcUHmtcRiDdQ+3JsDxXYeT8YYDofIsozChzEi4wrzGf4XmONzlcEVzodgY9HXWvt5dM6C8cyb+MtoRqykpHkJIZy8n7dzjtwOHMWVFoyI0grv3+2shanJZN4Y49di7v3pDZgPrcYd8yRaFnyqsZExlMbBGQvJOaZDjemBxpiVUFscgjNUkxrVkUaeZcjPCHzqVy7iv/xvvo3D8mOU1z7E7uF7mDxzHvKtElwbcChwlkO6TUg2gkAB7jJsDC5BGwvhgIE4wr/9G6/iP/3Pv4ef/9xjuHx2m+aHMVhnMCtrvPHWAf7oX2xg7/qvYGPzMdx58HX85J7DC8MCXFoYOYS5ehN7L93HxQtXoLj0EQR0NOGXUsJp8t92zhGo5T7MHxzgyMdYQsJog9lkGpnImY8+UNWe3NCbWwsvYQRCxRhijHNk/h13zmE8HsMY42OzS291oCCVjOtLSqrXzGZQWYa6qjGdTFEMC0il4ED+/EbraPbNJEOW5d5ShfgCstHIH/CRtjpsfcYYlFWFsqwwGBRxzdI7QPuPVBLwB3NMEiCvyyqy5heDAYSQkXH/oVPPV6JFxbTwgepI2+GfBIvQwZlGbWpMxzOMts9AuQIqy8B8VAZyP2FwPNTGAMZBZuDcH6ZJKJVDZQUmx4eYjMcwwxG2dp+HmN2Dkvdg52McHc9Q18fY3B5Bm2OYusZwexcyL6CUhDGH0Lf+CHznRfAzrwKyQNAFCqEw3DyD8d4dTKcHGGzOUR4foDijALMDiCHlZAmQQPrhbgNAFm52Rmlh1LrSkuvJ0AdG+r7qgYfDt6Ml2S1mRiONd5Bl9yMdy1oiCiXP9q4R1umUQ6vehhSrL7H4X38064tLUYl/2mu+XfJsI6i69piARfeVheFNm+7LbZtvuu7UdtroBeQUFHf619TZBsJ9beimBrbGBvaX7RvUHBx0RPRYRKci1rm2DNz2LQuGxB1gse+p5QiLZIdoCumgi1Y7Ftq7TDR3Tb5P0sG7F3GsEGRXybgrEUqTJfWscJ37YfzT+Y4Hf8vA3kkpmYbOpZMeOXU1wAJuadKSArvvybLfvQ8tud7b/s74tV7ldEk+Isjqbv/r5O28bmuPf89WsTx1weSKZvYaMnVe526ZJwJm9I9N7yv4sMkl45cWnDa4+w3uSz2i0CM2i4pdt6xPDlOvldYG3kUxAJyLPsCCN6bYJDDLqLmk0FLelNnaGFM4VxmKIgdA3oHz2ZzY0IsceZ5Da+N9h4UPkcRjWLKgteXeh9f5ejkjwrYQGgwgYRyM/NKVorwURon8mIPGi4HywDnUzrNxWwcHIo/LshzBR9w550ORoYkF7Bys1qhrAg1BA2m1gYMFZ0A5H2NjcxNKSoyefobMt8sZ7j94gN1zF7F//y52z1/C5s4ZTMeHccyqyuBH3/sunnjyKehyhHk5x8VLFwFboZpNsbWzjffeeQeH+w/w4O4tPPbEkzh7/lIE4IKRya2JfuclTK2BwQCBEZvMcoG8KCII4VLCaeMFeociy8lP1noNodaAIa2yUoEdm8bWOvhwawZkwltH4TbLsobszcDHfSZAwBlHrXU0JS7ygffbd6i8dtR6f+iqroj5XEoYfxgU4smTBlODcdLY10dzZGcH4I6jtg6CAdcf7KG+vIF/+p/+Id59+3WU1Qy2tCjYCKMzG/gr/5u/iA9/fBtFfhlXRsCD+TVU4zE2XAZ25XHY61fhDOAcg4OAcwrK7mDDncP24FnMYDFUgMQcF0Ylfu1Xn8UfvHYXf/PPDcEcw1yXuH8wxzdem+C739yBvXkZ4+k+Zu4MnJN47a1v4ulf+AousH0UT0uo4gjX71zD8y+8BF1TOC9jrddscn94QbqOYIliHQAuo0DnnPMh5zIEzoIQYiww8DMAWZ5DFnRwFdwDOGMwnMJ7hdjrgCdmi/HuRbwWgJ704e2MJesHY8j3WkqF0caIwL8P82UZWQFwRwRszlpU1sB4CxWVKXBH6zkQ72mtkeU5cQrkuV87Ajoe8pEPd5Y5ZHmjySZXDyozELyJEB5xNj/l9pkm15D0tD5yy78oXU14BHuhEAfA77fOaDhbgg82YPbvQ5x7Fsz/X+sZz/5OwBvRt15ImhOV5VBZjuPDPcyC6fnWReTZCJLfguYHmFUl6gcTbG7mcOAw+i6KjRGK0SYEVwAM7P6PYcsD8POfBx+eJysbBhT5AJtnL+PwxgHGD+5huDlCebwHozVG514E42Rl0zIbbwmpAUQ1EodDABvLx76RAF0y0oHkLskaBZa25EeCQ99Xd8n8MZaAcdfOn9bRpbmOQL5TfKuahju7647QNnpOwFj4zwrBIdyOsDquM5cQwLfhe7B0b0J19cOJpjtNH9MWsjDG8bGeo4LOMC52pwHLLM2YlMOSPOjUwJb81V1ZXUKpeK27FHoFzZPTWjJzV2J2ff2IK7xzp9241irtoqCu3X0vijkNNFmR1i2iM/4nysR95SaD3DdNC3O++NjyijvrNDaBJSCypzknpb7hX3fE+7a1E5/pAMOHTes++0lhmv5XsQfgraiQpQuh8xosf2h5G9Z5dOk6jntvz4Ou+T52z15P04CF7KecjBaQ7Xm2t7i+iTqpbneKpvUdTADotXzqPreiHf1dTGfw0U8G1gbenDPUlUZdV8izHFmmoJz04bwyCMFR1xWsscSuLDgcmI8ba6OQT0RLLJpAUwip2oNZRqG3rI2htgCgKr0ZaZbBWYd5VUJ6k2ilBt5/W6OqicxJCIlaa2+m6jWrjEUNNZmgkx8xd877hzKYmnzRs0wBYLC+7QFUZ5mKPuOkARQwHgwQ4JAQgkPrGtPDfXDuvPn9EFVtoLKMTHqFxOUrT6CuKpy7dBnHh/uYT6e48sRTuHvzNs5fuYT9B3vgKsNkOsEf/fe/jwuXLoMLAeHjce/dv4/LVx4DGMPd27dwuPcAQmZ4+TOfxfbuGagsxNCmRcMZB7y/fFWRmW44vGAMyPMcZVnCzucQQgJWA7rGrbvXUc5KODBs7p5BMdoA48TgrnWNQVHAgIjXpGSwhkN7Qj3Aedb3honeOR87vBhQSDlbI5j3FYMBhXKbTzHkIzDmLS28ZrysKnItMBSKjXEOwSWsNSjrGsZa5J6FezqdI3MWtQNqZ5FBgMHi/dd+hP/D//j3cefgA8xwBOMsNtQGcpZh88ZZ3P2/ldgZXgAExzn3EnbEBTz2sxdx8+ZHuH1/H+7Mp7D34B1Yq2FQQWQC5XAHTz37sxjsXoI5qjBkFrLWOLh/iJtv3MLFF17Cb/3We9jeGeHeA40P9i/g7ujzuIdbKA5v4dzFZ5BXFYbmJdy58To+eO9FPH95gI333gRmtzHGGZjaYDqZwDqH4WAA6329rbXg4NCWSAoZ5558jtZnVZWoaw3BDVSmUGRFjCIQfPrDWq7KEmD0nglJViXGGlRVRWsCAQDR7jgYDP3aR2MOzrjXHDvU4NCm9lwCRCpoLQHdoKnmnCIh1FWF0WgE491W6hjOz0I5Ch9YlxSLWvooAbqq4ZSM5G9VXca6I+maJY6JsG0GLgEplA/DpzGbzXxYuofeR+MapretGw0z0Sb6HN3Pw6qqmQPmZQU1UMDkAVQxAEyVPMvhmAUS4M/hwATR9TNOjO6MSwjlzc+zDOPDfczGExweHGK0uYWNMy9AHF+Hm+7BVFMcHVco5yU2tgc+ZFyJfLQDJjLILIeZ34a7/TXY3VcgzzxPxHhSYDDcxIEY4uj4HqyeYefKszC6gpk9gBxd9GOVgKJIJBbGKoWZAR/2+9gy/zyYP/hIBNa+z+OiBo+0tpHTm7Xb0iDP5dJGnOVUWI75U7bw0JsmtnTargYQ97Asd3+n7Ymy5iog1VNG6CULQSjh104HW/S5RnTReU8NtN83hx+LAmfKAN60fblrd9qhJH9X+gqnCK5z8NJ5vEFKKXgNkl/C/t7XniCB+g7EUroSdq+M1jlwY+0gZX3T5lrrv1ktzXpP/bZZ0qPmuGVR8GbtRsZ+N30MI9w97nio1Ax3skJWpHXBxbI84dXtK3fJ890lsk69S6a4bRyyqp09t1pdX1LG0r1hSToJ95wWI6XPPQoU6bqhrJUSnA2crr2Pkpod/CEexJK9bY3UhX2nLedRxqU1tqdddCHPsgleNnGneO9PdRhwQgpyW/er0JToupkfqqL1ydWC76AYgDEXmbFzlUFI0k7aEBc4I9BV1+QbOigGMEFbxoD5fA44F31OnXVRg2a9Uw6BZU/OlBEI54zDcQvFSKgmH84gZAPCEnEaHQLU0I40t3VdERO1khgUAwBkiswYI40qiKm9qmtwTYRSwb+1qirkWUam5dZGEGmcgfNgIy8K5FkGYwym02M4Y6EkQ5YPyN+bSWTDAT58/x1cunwJda1xuH8dVVnj8WeexfFkgpeffRGz8TFefPUV3Pj4Izz1/PPYuLOBjz/6ENu7Z3H3zh0YY3Hx4nnAGjz17HO4d/cuxodHcNbieDLB7u4ZXPvofXz8wXt4/Kln8NjjT/iY1taDDQq1lWU5wFwk1KK5qsGCH29V4ejgAZyz+PD993H92jUwOFjr8Pwrn8Yrn/08lFKRQTnE9a7qyvsgE8CWsrGCCMvYeDcCxoBMKUynU+9/TxrMTClowyNgl35NCABFXpBVgfcrVz72NAEv7+IQNCuCY7QxxNQCBqQdHk8meOdbf4hcPMCt+XUwDnApMRtIDKcTWAFwDFEbgX12C4eHR7iSPY/ZWwWeKL6EWf0NjMUhzu2+gDuH76F0Y2BHYnJliOLVAxQbAr/ECuzd4ijHFc6c38Grny1x/8Y9fPUf3sZgdAEbn/pZPPOVX0IxyrGXD7H/1k8gnr2C+9NDjOQWtqrHcOuOxuUz57DNz8HmHPowQ11WyBS5MEivvWSMQdca8/kMShCBG0cgrXPIMu4PeyR0TVriWtTNQQijuNd5lgEMKMsqEptZ60PhqQxKZRS2D0SG6JxD5v2hg/ad+VjsAJmlA0CtSyJg4wySNXHjAzs9mZgT67jza1RIgfl87gnkOApPyhbY8J0xMAzRNcHYJq48B0ee5ZjPZrDM+tBpFCqw9j7kXBCbXm0rGGshpUKeC6gs8+vq4VJ4j1wbfbVk2lR4XSQ4SkBmcxEABziD4xlmkwMMpcFoaxd1uQ9sngHz49rS8yX9iOHi4H3khYCQGWSWQakcWXaAyfEhjg8PUQ8G2Nx+GlwMwCe3gWqCyfwYVXWArR0idbT6AbKNHSguIFQBuBJ27wcw8/sQ5z4DoTaQD4bYOHsJ+9cOURmH+dEedF2CZyPwYgdckNVTCyM2CLvxj2UssmT3AdGmvx78xjFvj/TCg51SWFpQKDiVMqIE3fOFTQnJkOZzWLRVZc0zIU8L96RS9aI0GrXfPd9/5m+0Hulgzp7GRzjW7V2frNQqa6mwkfahz7cY3VFqSU7tepNjK4YIqAOMbM9vEJlaBfcjmvCHIxG6LcSy/vmIDydrIh4G+LzLJMAViflyUoDfzEirVYi7RoubIOT2omLavwXk1jeri6iueawZ24YA8dHSTxsYtWwcXN/qW92OdG2zcKG1WJO/W4cIzb1T9bFnyZxqlFcI/6twQetec9b00Kk7PCsz9rRlrao74LXv7Tz1G9gz9nHee+73lr3scGXVs6sauuT6Ath+1NOGNQerVe+pT2Q6cs7KF+8h0knj2IOTl+ZdK/UNxunTKWh8GQCKlWusgfQkTsKHDWJeyzmfzzGbzyEFaSMFpzBhzJCvJpmLkol4AFEGFs6QCWkwg5UQ0MZ4P9+CzJ39IDvnMC9LcMZRVTWyLIukTIF9XfpY2iSgExBhDNB+5oMGT3hG5+CfzRVt1rWuyV8WjARO42BMCckFrCDgAC6QebChtUY5n+Dg/h2cO38Rg2wLtTGYl2PkBcP9m7fw0qc/i3o2A1cSBwf7qM0MVVnjwsUrmI3HEEJgPpthOBqhmkxQlTOcO3cegis8++KLuHv7NmZljeO9PRwe7uPiY0+CC47t3V2AOVz9+EPsnDkDKSTu37uLW9eu4annnsf5CxcjGzwYoCsDXdWR/KoohuBCgDkaj1k19+zwNT7++CoO9/cgGMO9e/dwsL+HwaDAk8+9ACEUQmxaLjhcRfOSZTmBJesiY7w2GkYbcMExLzWM1Shy8vNVSkWfbyEEwLjX1FaQckDuBhY+LBwnEGgshbGyFnlB5ZRzApfOORxxhVvcgWkDJhhmMPjxd7+L8YObOKuAuS7BBgr6+W1sbWzDvvUAQnFk2MD+0T6yzOLs5gW8ef87kOYtPFd8AY9nP4979k3syVu4sP08bk4+hM4E8ru38Y6+ju3pIV5+6mfxP/wn/xRj+R6+8ncLfOZzT2Dv8RKv/84+KnUBL33pl6E2ChxyBvX0ZQx//Rdx/wtPY+/DD3DPvQUuz8OJEh9dfQdnLr+Kj+7OsX97AlsbQJFVhQMwm84o5JoUKMQA5XzuydVkjHsv/GFR0EaDhVj1BM6Ca0bw07QUPJvi1XuXCaM1HEDvpuDRb5oHCxJt4BhQZAXAHOqKGP+F5JFx3HowLwQHHIW+KqvS+2UPMRwSiCerFE1cAn5f0Zr2AFQlsqIAl8Jr4AXyvPCm5yWF2AOi2TvjHMJzTAB0IFcMBmAA7R2ck7WLt1KpfUizR0kNiEDYqFrfqbZxLYtcEilG8wMRS2TMkX92sYHju7egNgd0KKVrMFMDImsEERbMgT00YRT7WHhNMLOer8FbCwlJpucyyzA+OMBsNoM2BttbF5FlQ9iDjyC5QzUf48G9Q5TzAbZ2N6D1Hooti2xIRJIcFnr8MUy5B3b2VajhY9g5cxHz8QGm96/DmfsYDgbgjGN6/30Uu09BZiNEYMXSLgezZ7bwUV34zLlGI9e6u6BqWkwN+OshJuv9CPd/mR1LYWBb6uribtfcir+C9iJYg7kI5CPk7ADAEMO7DTK7mGA98SAdXNfRqrZ62S5nibDTmNp6YJg0sFVm+I73tKPb39aTrNMfD1pbeVyaI9Uks/BKJgc0/f1oTVgXVbWL7y+gdxBdbG+/9UQ6KMlBwyoAHQ774tpL7rcOi9KOdFZjbE/PKgnr9NHxdqfMbupI86sAy4ppS0ta+0ansIVyu0BqDaC78Pyqutd4ZGEZrJlOzL+wMZ3i2WXF9fVpRWEPt7T6JoG1Xs+HxaJLn1sDcK4MfLEqLQPuf0Kp27VHGb++Ah96TFY9GO63Fh1L/ovliyu8YD11dEr4qaW1gTdjBFZJE2khRU5my6YCY8CgGJCWOi8wnc/AmUSmROzktCyhtYaUAsPBKAr5xG4sIYVAWZH/Lhjz4FwgyzJIKQAI77/Lom+pNsTGbYzxhFKebMjSRynPcgDk30pm5qbx6xSk7c0y8hMl7S+F4ap9rGDOOVSegYFFUOicAxMcdVV7IV/AOYvx4R4Ed7j82GMQXgtV1TWcNZhPJ5CS496t6+BCoJxOcenyFWjroMsSFy5dQpbl8eChripPCueQ5TnOnj+H2WSK8+fPwYLjsSefwuvf+Q6m02O89/abGG1uYnN7B6985nOYTaY4Hh/DWYcP3n8PN65dw6c+8xk88fQzBFQ8QOFCIFMUY1wIDhUOEMoZBpnCoWM4ODzE/t4eJpNjDPIcdVXh9u1b+N43v4HzFy5jY3sHzgKaGZiKAG+e5VAqi6Be6xrcWyeAEWu8cw62IoI0ybjnA+CoqgqTyRTa1BgNR95/mcxlwUKIKSLyc4zm2BibMKhLOEvmyT92OV4rx3hhPkPFLb7x9W9h7yevQ832cL+eo35iG5tP7aK4N8dzkxH+1m/8PXzjX/0WBASGbBOy2kAl5njp0ldw4+hjvDl9DRN5jJeKn8GWPYu79VXw4Wfw3hMZMJ/j6IkX8AtntoGDCh/96C2M3RE+eOs+Llzexs7Fs/j8X7mCa5MvQ+5kUDzDviihtke48rf/HL731lswv/d14O5tGJGh/txZXPvWm3hh+1VwPI794x9gOpkh2yDNMxwwm81gjEGeF5FYDXDkylCTX3xda1RV6c2oQ2gwikEP7t0DBJFzldUcpqL8ohYYjTYjiDfW+JBbBGp1IBH0Wui6rgHroDJJe4MiJm1jgpk69+0hNnQC52TNwOCixto5B6cprjwAYrovSwguMBgMkGc5MbY7F/MIb6VRzuc+jBytA2uIm4FbG60mENa6P1iY1TWsqyFEThr3R9ToLLInL27grLu1s8WvdVtkpnBtRTHCAyNRGg6MjzE6ewWmOgJXOcAkIviLHyL/VXIOYA6cSe/m4/yhC8V4F0JBqAxZluPocB/T42Ps7+1hOBpieOZFsKNrUELClhMcj6dwDtg+uw13dAhTa7DtM5BSQQrA1vtwd74Jtv0sBtsv48z5xzA5uI/p9Bj1fA9yax/1wU1I7sC2HweXQ4B3mOSD72/8KDbiAHWtObxIfbI7BuJY5+MZhqrl79eXoVNycrMFLmN7XezK2ucBzeEJa11Me8HBkdpFNGuop9krfq9swDr5V2RgrC8Day/LdnUnVHVCRubz9IBhgqS8LZ6n2Jl1V0qytsC8zX1nAl14rq9dXQSXLi7W+d19zLc51f5HbXZnxn2evkOZpvguYlzS1niLL2btjunD2smm6RGl+pN36HbhvdU9SjdWrf2kzlbW09bnep7t26MYlpa9SFjWpSDslNNXduvi4nNL7yzbT3tSf/OTktM/2eox7StrDby8/KF1y1oHYC7blB+qgZ12db9hJ5XVU+fKKUvzLwtwvqwfbMW9vjzL6u/uS7HB/Qezi7vAkgaE88dFMWxJQ5L0CHO2NvCuqhqcEbGYksrH17bk62tqzGdzMMGhhExMs40nMmtM0ynsBuA8y7AxxsebJgHTSjJFdtaBSdKsMU73gkaAe3b1zGXEZM0pzBB8TGrLKaxOCGukFIUTC6GwUGuKG+5BiLMOnJM2SNfaa4EHqMo5sZ8LCcE5hJLEIOwsDKspxFZNmuNMCuRKwBmL2hGh0+bmFgSArTPncHR4COs03v7RD/H8y5/GZDpFkWWYTo5xaftxjI/HqOs55rMpxpMJZpMJdnZ3MJtMoOsamzs7mE6meOmll/DOj3+MF158HlxmePXzX8DX/uAP8NxgBF3Oce7iWdy8fhXaOow2RjjY38MPvv9dHB3u48WXX8XG5ja1X2sUoxEyT6JFQMpiOj7CG69/D1eefBp7D/awv7+PspxjNptiXhL51I3r1/GD738Hv/Tnfi2adwshKSycDxMWYgrDORiuMZ1OkasMLAPNB5jXVs49ICDQlBcMbm6jOTPjtLotPMEfXEMU59fG4dEhVEagVAgBzgQ+fOMqSrmNSW7w3vV3MPrMs7h3UUGaCbLPPQn+wVWc/+AYv/jYn8X5sxfx1//2n8GtH7+PwaGCNgzb7lMYs+twg21cPPvnke//BAf6Hl6bfQMvss/gyeHncX32NrbuP8Ddpx8DfvhdPPe/+/fwe1/9Bu5OrmLns/exp+/jvd+zeOzp5/HKl1/A1e9paGkw43McKIbBVo73Xv8BDv6zfwS2fwBmKgzObWLrwkWMX30edx7cwaay4CMJbQ22igG4d6OQUkJICsNV1xXKco4syz0xXwVjNEUaKHJkOcX5joKcteAONDc+DJ3WREiorQEzgsIG+ne2nM+hE8LC+WyGeTmHlBJ5RvHASetswaUHwmUZffEdyMxZ1wSYOWPIVA45oL0BDijnpdc4eyDuD+TyDJBKQmUZrStjaN4ZEbUxBjBH7iY8HIwxBms1Ku++wnz/lZQwoKgI2mh6hvHIqM6FwMOmFPa57keilWfxue43xyV3GQM4J+C9uXsZ4+N7cEpjgzMc3HgbO48BauMivU/MJR+iAE4DJCciO8EBxkwSsUD60GOkAZdSYXy4j6PDQ1RFgc2tp6HKDXB+ExAZpuUc1e19bO9uYVAfw9UlBltnoPIhBKM22KMPwatjbAyfxplzF3D1nXtQ9REc3oWeHoCrDKqcwBqHnSc+DyEHUdFH64Etfmj7wG1C0NYaU9bRCveMfXyshXMXc/WTVyUf9x5J0KUok6WZmzYH0MTiekmDQ6UtDNKEiz9D8Q1sbEvMDcB37b72pMWn+zUgD5MW1jZbfp+1/xPb/yhtC2bcrbnwTzEvyTukNihuYeza/hBJG0OeFpLvdpp1GnqyhM7QKS9cY+1csQ9AdMdo9x0dtxdfaHdzWiU8tvr600xNn1tCc8+rEIZ35flHUlZ4fOHQax3Ac9LmsQrpnVT2OvdPl235M53x695aBiTXwUsrLz4CMOmm9hJcUnACokKuh2rCkg/1wn72iIdIj/xeJf2M78Mp63fdTvWB19ZLt16Zaw8LS/5d991Jv6l9AlR/s9ZqxsOXcLp0CnI1TmAVAGmkAW0MxdNlDEplcNZirgmcuZpMkAMwti6QkAmUukTtgYEQFIu7qjWUlN7H17Obe39toy2cJeblutaAj/OtpIxms8aHDQMM+Yoy7n2NndeUMnBGpu+MiMxJYw4OxoHhcAQL52MA0yoTUkZiqNKbojqvyQ3aac4FTD0Hg4MWHJkEDo8OwY8ZGCx2zpzB3t3b2Nw9g6P9+3j+lU/j3q3b2NjahLUW27tncfWjj5AXBcpyjtHGBjgX2NzZhQODyjMUgwEAhnld4/XvfAujzW1YZ2GqEs44/NyXvoS7t27iwf0Z5vMJwIBMcFQlscuPRhuYjCd4/bvfwQsvv4Kd3bNes05m+iEMWV2VkJxha2sL1jncu3uHYibPZ5hZB+YoHBljDB+88y5eeuUzuHD5MWijIYSM32nr/emJoZ2E/KIovPbU+wkzWkdak3VBkReNNYNUmM5mdFDCOMq6JFAtifGdgXkgRn7rm5tbyJSCdY7cD8oa1Rt38GeeeBrvfvQ2zPQIv/T0ixj/6GvQrMLnt2f4yUe3cRHn8Hf/t38H3/nuD6CUwchsYl6XqJmCYxJ8+AQmZ85gXD/A0fwm5q98GnpnB/qt27hiz+L8zqsQkw8wzbfw3N/4OXx9q4Z8/+vAlav4zK9b5MUmbld7+N5X38Jf+9xjGIg3cDi/jAsbl+Ayh9nBIW78v/6/EHfuwnKBp3/hy/j0r/0qvvad76F++8e4/swvYOvtH4FvZTAQmM3mABzKeelZ3ykF8kLrLGAMikFBscurGmVZUWgxT7TGwGFdY/lBbhoUeaAYDCAVxai31mIymSCEKOOCTM9ndY3ZbIbZbIY8L6CNwebGJjjnFANcC3BRA9YiaOyC+T+xi5OmnbEmjjYYo1jTUqCuyQ2lie8LzyVQQycuI+EgLfi6C0HadiE4rPEx673rizEmgnIaMIfJZEz+7XlOTPDGUijCR0ptduWUnClwD/hXpJVvATj5kuK3hXFk+QA7Zy7i4/t3YKsKg+NjCAFYU2K6dwP51iUwJhK8kPh8e5DAGMV8546TWbE/8GKce+23JPNzqYjwcTaBMQZb2+dR5FuQk2uop4eoyxmODqbQQ3rnTH0HxdY2io2zZBkFB1vuQVbHOKM2cbQ9wvtvvIPN6QQb29uY7O/j7M4TKKd3YOf7MOwYMt8AlwMsBIXqO+1GwDuLX/gGGzXG332fzDbITSnwFmenXUFzoNHMZrfs5HpURyTwrtXssFJWfdjTdvl/mX8ymKiHGl3TxrQ93RXWZftuH9ecDGxZqIY1wLDLPL+srICH18E+lJ81477QpmY8+voTP0oLrWraviBP9jSoi0EZ0I497zO4tC0JFTpj3XavIcVSJZ2LqcTZzRs65Zq56PZ/oaN9kKZZHwRa152pR09r1XDK5pzU/JNm4gR8v1D0I4/SOqDjhLQIflb9OvXFn3Jatl5P/fQnnuLwh9e8Z3G09uJlDfoEcVxfFIaFelYMSvdW7xLrfu+6IDl5KGZdslWtlboNWPoSstY//WnJht4zNn9SO93awFtwRn6TXoC1jkyH87zw4IrBMU4xozlHbTQRIWk0YcIYKIaz1gS4qopCgzlHoamU8r6pZF4cfDQdNIE5T5akjUYmCTDAOVhrAEdaNc4Y6rqCY5bY1lVGJqdcoPZ+xtpoFFlOvsLWeNZfqo8r7wcLIM8yClXGqD/zeRUB5FAqzOcz1NUcOzvbmI6PIJTCeHyMzc1N1FWFze1dHO/vIStyHNy/h3OXL2F8eITds7uAY9g5ex63r3+Ec+fP48a1j3H2wgV8/P77eP7llzGdTJEpiZtX7+Klz34WB/fuYpBlGJclnnjyKXzvu99BOZ9hc3sLm5tb2BMZzp4/h8PDQ9y+fQdPP/sMdnbPYjoeQ2TU1uOjIzy4fx8/86e+iMtXHo/gx4H5cG0WWZ7DATjc2wMYQzEcwj64j7IsITmHMRrDusJ4fIzbt2/h3IVLXoqynoFcQGWkOSurErAOXBHBntY1MqUwK+coZ3M4ePKuLAMQfPMlhsMRrRdO2t2qrDAZj1EMBshU5hnmc3BBIcW01nCeCd/AwjqDf/svfR4Hgw38V//otzF0A1y/t4k/d/7T+MHOj/C//HeewP/v3n3c+71j3Hr3GpgSmOwf4fjgCLXZRCUtqtEQ4w0JzTnKww9RjRiu/9yncO6bb+AaG+NWfoTd/AxeuPwMfu74ASZ/8A5u8BqTmz/Gr/8NByNK3L4+RjWvcO7yHIe3ruLl517GH799DU9duoyh5Lh5fwy3f4ThaISX/9a/iTPPPIdvfHgDh3/0NWw99SnsiksozD7kTGM6KTEc0etqnUGmCm/KT24WwrPqwxLYY97PuypLzGfMv7dEDqikRKUrzKZTlHMJqVQ8ABkMht6/2qCuKkipkOUZgVwpMZvNkOU5otZZkOk3AWcZr6usiAz2SklYQ0DcOUsaVikIdNeANQZzraEUaaC58PwRPEfJ5pFMT3AOmVHs7xA6rSorKClRei1/nmferYFhOBxC5Tm0plj2IdyckgqD0Yjqnc/BOEOeF1BJJIVTJ9aCWyCRnkWpr6tNTT/a/QRUBDbI5JVBMIXRxhYuPfkcbn30E+zt7WFTOWR1jcm997HNgGzzEsD44geQASlXNYOAYy4ebpBW3bvqeEsKYj3fw3R8jMPDQ+iNbYx2PwUhroLxuyhnY+ijOYUJ28jh+BhG1xhsnaNQda6CcxoDN8NzlzJcvvTncff+Hq6/+w7AFPiN96F1iVy9DzXawezeu9i89BJ4tuXNz1nPx5a+li0g11J/NdmbvrN48ND8tzPmrHttmYTuAB+mcUEL6dJjl7TJyRe+Oy9JA9os1EA6Xy3iuE4JzL8b7bnuq2GxxlX3F693JiOcJ/hMtOd0AoR1TprSNp4o2LBOKxPNcgvkrOpv2uQ4P6xpgEtHtEey604J2sPbTG3ynvt1S4A1VOL3htbyWBQYWwI9a0LchfUWTOcXO9stq2nP4pFeXweXvAvxvfkTAl/L0MoSuXndInt7wJKxifO1Is+SyvvK/RNNXQDkmiWZbIsPlf7kuvJThjvrAuCHqL77iqyNM7uZ0m1zBZbsbKn9xSWZlq39uE46D5/wBVxo60Kj+vKeclz7DmxPi6uXZWnNV3cQTyzhkzstWd/U3JNfaW3AOfnWcsbABWmr4Bw4o1A9FB5MQeQSWUFa4qoqMZlMUPgP4Hw+gxQKnJNfaogPPJ+T6TEXpBmXXlunrQEMPRtC/zAAjAtoXfswZyQ8M8/kSxpTElw54+CWzFMzF5i1udcWEqCstYbWFJZJqgyGGZQ+vJbKiNDLGiKPCkRMzBmU8wkAQAmBWmaYTMY43NvH8eEhdnZ3ACbgGMO1Dz9EXc5x5amnMR1PsHfvFsqqxnhCwPbWtas4d+kCbl+/itFoA/cfHOPS41fw9g++j82tDeha48VXPo1rH32AwXCI8XiCcl7h4sURdna2cHCwD11X+MznPou9e/fx4M4d5KMBHty6jXt37xFruZJ48Nv/HF/587+Kx598isJvGQNrLA73HqCeTXD/3l2AKVhjoasK2rNHc84hGcN4MgGXGQ4PDskXnnHIPCP/bUuhqcqyhDMUqqwqKwhJZvzWJqfxnuEajPgDjNbeB9nAWIci83HYrQXnDIN8AG01pFSwzkaLA+fIXaAqS3Cv6bxrgP/7O2/guL6NWc1xMHkZP/P8z+HDa0/hm6+9BhQl7mGCb/3T38H9wQ5+/NqHOJjsYbCxgemGw3Qzg2EO0DPYeozjX/gZfA4Of+/v/Hswc4O9g0Ncvf8AP756HbdulWDv3Ue+t48Xf0ZhuC1hywLDUQE8xVHdr3H1zY/xs3/pZ/GNd+5gajS04sD2Jopf+DL+3Fd+Hge3p/jj/T2UH70Du7mN6aXH8PTbR7hw7jk8+VKJYsAiSBwUA1q33PMBaA1tNLa2tpHnOebzGWbTGRijA5V5WWIwGDR7h6PxHm5sYD6dYTIm7W+WZZ6MUJMlCxdwcDA1HazpusZ0OsXGxghSjVBVFZwFynIOhgLGs5k7714QuBdGoxFqQ4R+2tQYDoaYzyxZSnCB2WwW/bwt6EBPl8Zrry2009G6JZRfa3I1kEKgNjWMIb92rUmLXpYlwICiKGLMcs44GBgRgkkidnTeVUapPLb/k0lrbNLLPhjxw9DAAcYZpMqwe/YCjg4e4P7Nj1HlBmq0jXpyhHr2AHU9h9q4BJUNW4AkfnRiuDH/ReaMiNfA/PgjEq9JmSHLMmT5AJPjI0zHxzBmiI2tZyGzDRjzHuq5wfFEQ2tLriXOwpm7KDa3IfMBmKkAoZDzORTbgx46zC49hcuf+lOYHtxGefd93L36Lh574TOoj/dQD28C4h4c48i3nwETCgBHgHPOE68t+HWxBLj2jGkIGx6gR/vDziJw7ILz+HxEROGJHtDKlhGTNRejWd/See887eeJxUIS7vpYWRPxuwcnLqSTxIfl9zv9W0eYWobzHjYlByhr+Ru3Jtsl/viLBfdPC3ENtNjqO5IpTZFrDxpL5swl0u8J7/viqnLJ2uy0o6+QBbvRk4bctf/tcbX4ZMXNboPWQUT9PTgRqiUNXynAu56/V6Ka0zbk9Ck9EEi28RPb0wdYWteSATh1s0/zQPLOdR9aDaqWV9K+0jdRrLe+5Yu378Z6o5HOCUvUzem8naK4xfLDoHSbyNrj1mrPqrauuBHXRfixZtvjvK3aIB62/50foY0hNOAnchT4Uz7rOSmtDbwzRSGHVAhj5P0iq5p8naWQUWMkBCeh2RFwq6raaykz1Jq0aFubW2BcgDOAccoruSSSsbqC1QaSC6/NqskvmRTckDIALvroZVkewb+x3jfVGFjz/2fuz54tSfL7TuzjHvvZ7n7z5p5ZWdW1dVdXA90NNBoLQZAEt8EMR+QYTeSLXvSgJ/0BepfJTKaHMZnJZGPSmEzSjKQRSRsbksYFIAE0uhvopXqpvbJy3+9+1ljdXQ8eESfOcm/ezCqAcrOqvOecCA93D19+39/y/RUorEXNCMumLoRlVdale2qhClwo40JtaiO3ZDqumM9lCQ6la5/pONK2K5kQtVroPLXXCttegWF9Y4M8yxBCMugfofKcbq/HZOJyfHjE3u4uly5dIux0ccr4yzRLQNv49CzPcfyAuzc/Y/PcOe7evs35y1d4ePdzjo6P6R8POH/pMpevXOHx/bsIKa3bbJ4y6A/ZuXKVvSePONg94PzlKzzb3ccvAUjgufzsJz8mjCLOX7iENoZ4MsJzHdx2m7ff+QYfffgBURhyfHxEluXkhbX8J5mCynugzJ+sS+Zs13XRxrqsW/deUaaLmpLfFUWO43hEpfU6K/I6p3NR5HiltVKXhF6B79PpdAmDEITAF4GdkNqgBWUIg0a4HoLyM+AdPGXyb/4t2ZtXOWfWyA+GpPmYb138O/zL//t/Td/p0319E723z5viFX54+w5e4JG0DUnHpXBseIMUkJ27zuHFLX79l7/g3f/ZX2dttYUjdnAcgVKGR0dj/vsf3+atVzZ5sKtIDn5I5Go8DM72OR6q2zy+v0862uPt8xf5aPc+7fVXiY8ekyUTfvoff8Ru4JJtr+B+8BHmnbfoPnmK6Gdc+bVzvPvNK7iBdeV3HMfmny5ypLZW7SRJcF0HVRSMMpteLc2ykpAsJM1TJvGEwPctaK/CL0rA5ZVeBFJO4521UniBi1GKNE1AgNEa3/PK2GoJZWy4FIK8KGpll8GmniuKgiIrmAirBPADH0eVGQGMqMnSEFiLOBZger5PlmalAswhzRTalLHfjoNEokr3dCEEJgc/sHuOQOK5ltPBxrXLWqnneDYfeJqnYKgt+Qg7h9OSw+BlSnP/boqzTXl7ejDPSdrzB988hixlaoHE80MuX/sK0nHYf/ApR/u7yDxmxSiS/kN0PsbdfA3hRrWXUfVIIWxFU+9nAcJBSmxmgtLLxJFOnevbDUL8MGLYPyadTDjKM1qddYLtd+DphxTpEUkuYJTYXOxdhdY5bhARddaQ2u7BjsjZ7nl02iukbkHv1XcQr77Nk9sfcvv9nxJFLqP+EeuXX2Ww/4TtyykmWEGlE1qrl8BvTRUGTcln7hQW87/NfF9hNzsAFWCyNmMNuuIPsK74JaFH7cpdHfwzQH9OBBIzIQWVV0EJjGcShlfzoNHexWY3+jaNW29KPIYZka9qBfPW96ZD+0my0hymrNsyC/gW803X6gohEKYRYlG3zsy8unnBaaYthqkAPe8vW83Z55XmAxrjN62jATQN9eSo3PZnqqjmwZxHy4Lw16y++eXc90vl6Zn3Pqvgmb7fWVrGxfcnFgd3mXBpqi5PwxRm2ZmWdAoznct/6WVxXc13Y2YbeIFqT7p+HkA0n/dXKZe/lIJjvoFLGnzSmz3z80z9vxNqO0sV0/sX3/CyvfQkLcipjTy5nPryv0ARS/98uXpeYAIs23uWldPe2sJ3Z33+8qP35cq8BuGkQ6LxsXnEv1Q5Zaz/qtb72VnNK+KjasPGuq+GZexoRbiW5RlKaZTOaqZi17XkZNo4tTVaymnanzAIS9Crard1IWXtSptlliQpiFpUJ0cVa22MQZU5o43R5IWNHZduaZWX0rZHZbXSII4nBH5Yt8Vgyd+8Mie50SXpVOm2q7UqrfI+AusWn2cZjjC0220KFTA46qP0kPFgwPrmFq5bkCUT8iyl0+vhSJdH9++yur5G/2jA9rlzTCYxz57e5I2vvsvVa9dwPY+nj+5x/uIV7t/+nMlkwvr2NuPxhFa7Q7vdYefCRY7f+wnb5y/iOpJ7t26yc/Ei+/v7bF+8QHZ3wvrWOu///Gdopcscz7CzvcHWznm0gb2nT/ADj5uffEye5Vy4eJEsjSnyjCye8Nmnn3J4cACORxS1ODo6xGBKUi/HspQXik6rZWOEy3zeTcFFiOkWq7UmiWPCKLKeDFlM4IcoXYUgSAqV4TrWZRgDfujjOJKijO9VRVHHf0tHEsexzR2eZdZ9GnB933oxGM2lyxv8na1X+bP+NfqjY/bHd3m8d4tvvPltvj76u/yb2/8d3Xcm/Ozuh3zXXOCrl97mnp6w33KZuMqycwuIdcztN3dw733C6MkD/vX/9If8k3/6ByUQNUgJOysRf+/dq/y0/4Rbdz+jTQxRBH6BEhlaOISrDof7n/L2xYv86Q8/Y895neGf/gD5/s95+rd+j+KVHcT330NvrSFFwcboiG995ypfe/cCjqBmhj8+PiYoc2hLacFnu93G8zziOCYp04q1Wm3cMnQjKMnKMFb0zrKUPMsw2Hh83/cxgjInuwXzRZKgtLJ56B1LviU8D4Go37XNDuDUacOKPKdQijAMyfO8tjynaYrv2xh9KaxSTEiJHwRIIfBcnxxLUqhyjTEl0VopFEdhy8adz/A1SMveLqV1Zdfa8hQUdt8JwxBltA19KHPO1yDKmDJfeUlS5zgYbcosCC9ZmvGec//W38+cgksE5aW7fiUNWuFYIgmjNjsXrzE4fMZnn92k5YPxWriOoK0sw78moLv5CqJkPF9aZy1zOwgj0UbgVp4BNUO8h+sFeH7IqH/EaHDM8PiAvNWmdeFd1P5N8tETyGz7dH9Mlri0OhpdFIS9dRzXQ+sC4Tq0vZSguEPczzGtc5x/5WtsX3yF0e5tdu99jLf3FK+9yrPbH7J+9U2S/h46OSRYvUI6PsQNWnidcyDs+SClh3A8jNFgRBk2ZMOPjLZKPrRBG0U2PsJxQLoBhVYUcR+VjQlba4StiHh4QJ5bvgk36jI+2qNIx7R660gvIh4N8NureOGq9XIyirC9AY7feJfWtX3BeI2ce79TsDMvgM5fNn1fs3OsSv/VvKeeYgs50srvyr9leXHTrXGJgWXmk5j594TrRPMKi5DqO8Xc9Wb5/TNodbZXc4voBNHvpHVU42wry5iyvik2X/RlkHXDZ8dzRh+w8K6bbS6fXoc8LGmeqJo3D4CbEehi7toTiqjUMCdcJBq11S9kce8yjWttO57LL/+CZQkkmBHC5/L/PufW+vsXBULiDH+fVJbgxNPchF+0zEz/k+Z040UtbC/lrdUlMH8GnbU0J+6c+uiMHT5ptc6vtyVPPUv1J1zZVIGe/qyXL2fdf16wvJQWZnkVovmB5pdzf8/P51P6IVg84xYePjdNllY3f/CcKDxV9Va7mziVXPEEneNifafetUz5cwpqf4Fy9hjvMuWWKt2H4ySGMsYTbHx1xY6cpgnSsSmAbBYf655eqKJMdSSRgbXQYgx5kdcbRWXJtunGQKNptds4JbmbQJZ5gG0uX6VsrmFtjGVTFxYMWFAnEY7ENS5xMrE5n4PApjQqcoQUNfu2lA7CkbS8sMwRnZMXecnU7KC0xnUkUjglaVWKKjK0yhkcHdFbXUUVUzb1/uEByhjW2h1UUXB0sEtvZYVebxU/bDMZDgjCgN5Kj5sffYAxmq3tbTa3tjk+7rOysUVePMF1PTxPsbVzHqUU3//jP2J75wLSkRwd7LFxbodf/ORHfP1b3+H2Zx8xGo44ODomzxUrq+v47R5Jodg+dx7X87l8+TKh50KpMPnso1/y9PGD0tqpWFldYaW3wtMnTxnHQySWhVypjCRNWW+tWDd8bWz8t1JlnubcAhvHWsyKPAdhXZrbrbZlO1YaLQxR1KJQ1gXaLd9hNY9M6XXg5A6pSkFC4AcIbB7vrLBux5a93HolhGGI53pkRWHj/YH2Spvf+92vM/4fP+RH2QasbPDg6QO+fiPjK+e+wadPf8b111Imd/b5wf0/4ddWXqWIVpk4CUhd5j42PBJD+r5D+9/+MffaK/zf/sd/ybd+8zd445VNciU4HuX8+5tP+Ofv/Qf27v0Ad/SIjVXBhQsjLmy1cH3BRLp0dzYYZEM2s4/5tRvv8t8eH8CdOzjvvE7xtVfQewc4+0eonW0CHfPaa6u88dYORmniLAZsuIddEwatFA4SF7fOQV/lXo+TGMdxkSUnQjNePkkSsjxHqQLf8zFAlmeQW4u077i110iSJGhPWyBdZiAw2iAda7YrCpuLPQh8tDE2ZVehGI9GNjSgFRGF1sXdZkCwzOFG27zgVUq7LE/RyqYjs6R/luDQ8VxcadujS+8Kx3XruTKejDG63B8ch1zndT+lY+PIi6IgyzK0UkzGY4TjEIahVQSU8elpmtpQlC+Qx/v523HjlFhmXapPsjkRZe4gMlgujajV4dW3foV2b417n/ySo8NjOp0IYQxBJ2N4eI8wDDFOhBd0cNywtFtVbWm22/7iCIk24AiJEI7lDZAurmt5GFzfw/N9RoMj0niMyj2C3nW0dmF8H5EZCkeU5HwTwo4gz5/R6q7it3ugcowDjh7S4RaT/hEjs4p2Ivy1K1zq7ZCN9okHB/SPj1B8TLu3yu6dT2it9WmvrrB77wO6W+fprO0w7h+g85iVc9cRQpJMhmRpQnt1C4FhPNjH5CkGgR91SdO0JoeLuqv4YUTcP+b++z+h02vR277E8LiPSgd0NnZot3pMjg94evcTVrfP47k+Bw8+pbdxjiBqc/jsoU3luHWV4fEBjhfSWb2IAjAFTrACwinhWwWEFmfKbDzt7Iyp7zGLbr+nynfPQdKljmD2WfUPWMv9Evd5sWRqztRh5i5oyjBzwtiSuxcrN40ZW4HkaqzE9JkVQKz7NW/VrsBJqXycxuiLBSw/s0aaioDZjk7rr74UpUA4raQuy9/V/GDONIClA2WaI9XI817FdJ8E7psNeZ7cOK1uBtCdKmi/dFkuPS/ZCWfaMAMmThDWF4DH3O8njdFJwvpJ1z+nGadevwwgLF1XZ6x7vjTB+7yzyHKY+2JAculdJyCs6ddm4ZvnP+F55YS6vix83Sx1p19Ki/Fy5QyPqKd4ObFE852fZRhf5NqztK2xiGfW8vzifpHha/TtrGUxN8aLliUati9YXiDGO7OM30LUVjClbG5lKWV9kIkSbAVBgO9aC3OaWGu2V5GnSZufWwibpsx2xx5ZfuCXbrOKQhf1s3TpZ64xFFqVsd+WTK0SDyxLOaRZat1UXSu0q6Kwqb2kJC8KS8xVxpSqEnjneY5XkkxJIXA9xwIMY8GCJZtKp/HFpUUumUxY29omncSMhn1UlhKsbZCkKRcuXWY0GiC0YXPrHEJI7t+9gzaGza1tJpMJo1FCuxXh+T7xZILr+eRZTHd1nTxdYzIesL61ycHuLu1Om+uvvkar3ebJo4dce+UGtz6/yW/99b/JoD/g6o3X2No+z97eLmHY5o2332H/2ROOjw8YHR/wyo1v8uju5xweHJJkGdqAHwYcHB6SpRk3XnmFbrfD3ZufMBn12djc4eDZM9IkpdNuk7seURTS6nRp91bZ3NpGG2sxDcOwjPm1bPBSSvIsK3kArAWtoMCUZGBSSHBsrnNrCXUw6DJlmI3zzvIUo6nTncWTiZ1rYUgUtRCORJSu0WmW4pZp47Qx5Epx/vo67379Ekffe8ST8QY+FxkcDfC7XX73lX/EJzf/OWrdcOvWPu3+52xc+gZap/jCQQrD7viQO5cE3vf/hK7v4fY69P0W3/t0xOWddY5GBf/993/Bv/nD/yuO+pALlz28LYnjROwdFiTphKtX2ridCOMZMiX57OF9pIj52uiYO//w2zwU6+hJgtAG4UrQKVtC8ua5LZLJ2GYLMDBWuk7xVRR57dYthGUZdx2njMW33huFVsSTmDzP8H2fwK/Gpkq/F9jwkPL7vLDrcJJMkEikK1lZWSEtXdel6+IgmCRjPN+j3WqTiaxOX+aWgFgbZVO9uS5B6KGVtYx7vofUGs/1MUYThJYccTKZWFI+x7JqVy7vWmnyLCczZR5yLFjX2iACa/3XWpNnGQhRW9llabEHUe4ZlUJPEkbW/dqp+BkEqLIOG6rx8qzm81t7LZvXcrWoD8HFc7tCO8sOhQpSNLX2AiFcWt11Lr8SIRyXT9/7c0bjhMAxuO01RoMJq+M+Sh+Sm4zO1g0ULtLrzNYsKsG9ZAEvXYWlkGgpkdKmU5Sui+v5+EFIEEWM+kdMRn0moz7GeOw/PGRr1Wd1dZVESFSiMKTQDhgPjmz6wt4aQhV2/auElnmIxxGHkx77iYNwPPwwwlu/xmZ3i0l/l2F/SFooDm/+gq3Lr+C0VnjwyQf0NndZP3+DeDTmsx/9MRsXLhGsnCOJEw7u/xlhp0tr6zpplpKN9oA9umtbOEGX0XDC8c2PcAMPr71Gb2ODhzc/ZTyO6a1vkWqPn3/vz7h0/TxrmzsUWvDw1udcuPoKiRLsf/gzdq5cx22tceujX3DxekxnbZv9ex8yOXjA6vZl+rt3WNl5lbC7bcOZgjWM8MoxX0S8VUqx5veixmHlB3O6s6+Z/9BEyU3g0fhtAVqX7TDVvGj8NlUEzM/OxlVnQDPLw7NFA8/OS2yi9KBabMP8sNV/irkvzfT7RYC0DGVaxWsN5pvjWT10ZsBnoH/judNQhdnxn2v4ghRqlg9UU3/DLCO+qDgcatWBqOfQzDPKdi0MzWJ3Zr4QLyLtvkxp6h3MonJoabNOKDNKglOUDbUO58SBeE5760qYmT5fqLzIML/gA89ioF4chrkBbCiyvlg5bcAbfBan3n9aI8SSv055/gnN+SuD1ydpXMTz2zAzEmbJdy8BwJ/3zLPQbDQrWLp9PO8VLqvsDNcv7u1zz5xpUGN+zyyQ+U1h2d8vV84MvNPUxkXKklVcSgcpDUJ4CCyxjiOdUsi1REyqKGxub88FBK5rBWIECCkwVVwnlmAtL4paeE7zrI4dtu7eus7HbYypBX7Lim7ZmqsYb12C7jzPUdrmKbbuspboLUlTwiAorWjCgovCWgGFkOTJhCSe0O6tE4U+o+EEtI1DFtIhakU2xdbgCCHXcSYxSiuG/SF+4PHsySO2d87z+OFDLl66iJQucTzhYG+XC5evcri3R5ZlOFKwvr7JZDzEEZLd3WfEcczVG6/y6UcfolTB5avXGA1H9PvHGAwXLl/lYH+PIIzo9lb55q99h2QyYXB8YC3CacrRwR6ojOODXTq9Nrdufsra5jr3b36McRwePnrEV956B5sdTnP//j0e3LvPR++/z9//g/+MwSTh6d4hOCHD8bjO8by+uV7Hvq9trNPprVhWbQSO69qY7bzMne66iEBgythZx3GpUq3keVG7+VbWSa1z8lxZxU2Zhsz3LHu2DW0whGGExuCVjPYS64lhtCHOEsJyNmltWa+lK3jjVy7y6ecDdj94Qt57k91+wU5U0HO2uTL5A/58/58xCp/w09Fn/D3vG4jcIBGgCz5391GHMdGzp5hWh904QG9c408+/JjvvNHhX/yHf8UP3vvv2LmS0b3QwyjN6DDhyaOn5MOYlhyh8y7KePjtDq1MkdIiC2DF/Yxt54DD+Bp9eRHn89vgGtx8wrvtHr4QJElKmma4ZcxyksQkcYJ0LNAMWxGtVgtjDAcHB2AMQRQhpXXFF4Ap47XzLEfpolaSVRZtP7D8CGEY2jVd5rVO0oK8VHpNsgTXcXEdhyAMSoCt8XwPVVhSQiEdy4YtPJSTI7ChHhhIkrhkX5d0Ola5hoFJHJNMYhzXIQojG97hWPZyhS7DGqzyLs8LMNZ7JssyJvHEJnfH1H2l4mkogXzFCB5FLQx2b3DLtZinGWmZ57sCNVoXL72Risb/67/E7AWiIewCDXAzf5Isk44W7SpSOARBi6vX32B9dYM7n33Eg1sfEzx5Sig1h0/u4IQ9VHyIH7UY9Q/QeKycew28CIFEOjbuvYoHr1iTjTRIM/UGchwHz3HxPJ8wCOl2IwJnk/HREw7uH/Dh013u34n5yuuvcOnSORzXJ84NxIowlIh4QpGnRL01G4vvuAjHxTcxm5FBijZPD8cc7j4tme+90iNKIFsB3WCV4WBIlCui1S2O9o7Jss/ora4Rttrc+fB9wt5dOitbSNfj8f17iEcP6a6uEwYBe8/2uHfzM4IworO6hnYDHj9+wmh8l83NVWQU8tlHH1OoD1jbPIcfeXz6wSdI93N2Ll0hT1M+fO8nbF+8ghEOj+7cZOPCVdY3N3h46zN2rimidpu9+zct94D0uP/BD7nylbd59ugBq+cu0916FeVEOF5UujtDZZaYPdanSKB20TOAkDQP/Pmjf1EemUekTeDYqGVmss5bo8RcnugF9HjCs5e0Y0F2aQLBqhkNsDu3Dma+q8Fl87sl4lClsFiOYe2j534UVEKloLZgz61VI6ZW9pqBfL6vdd1ztr3qXZRnG8zBDDG9Y0H4FfN9FDO/Nf9ofqy+mT5jiQJiQZ6cl9Zf3iOoWebf2Xw5dS6dUUivuiIWOjnbjufJz2d53JkByJI6T/r7bA045ccTwOOJ/a01D2cHszC/rl4EiZnZRi0cmI3rTi0nduiMbXlevbN7y2wzxWmXf7HHVnWVr6R23Dmh1PO9Wc2ytnzR9p21nGnhLPluyfR7UWx+gjSFMbNz98VLc418sfIC5GoBQpYpQ8r4atf1rOW4tDIqpXBcl6woSou0PVgc1xKvCSwJU1ESsgkp8VwHnWcY1ThdjSVzy5VlzBZCWIK0kkirSjtVKMrvrLtonuesrKyAa4nTtNaY3BJSGWxas0qGONh/yvHhEb7nce7CBZ48uMkkzlhZWWH3ic27Lf0WB7vP2Ny5yOOHnyOlZGVtnclojOMKJqMRaxvnyIucIGxx7vJVhof7OK7Hw3t32djeIkkzhv19VtfW2Nza5uH9O6RJih+ErK6u0W75SGEtvdn2Dtub20yGQzY2trl77w6j4Zinjx9hAOkF5ElMr9sjiFoURcq9O5/z/i9+ztNHjwlbLRzXw3Ed0jjmwYOHfO2db/A3/t4f8Mv3fkTgSz7+4GPefuddtjY3GY1HfPjBBySTCWmS4oUhn3z8Md/9nb/Go/sPoATB0pH4nk+rFdHqdJCuz5tf/SpRy4YSmFIJYmOwLZiOk8SCZz8o3YlzpGNzNVeuzI7jEIQhTvm969iUVp7nEbktVMlcXYUU2Dzd0xRW2pgyNMHQClsI1zKjW+IwG3PcWW3xjW9e5NGne+z37xJtfwWZDrnghVxsX+VbF/8Bf2T+LQ+Pc4bFGEdKCi25N7zDfrsguH0HIRyGTkT4e3+T4nDI5nbG//6f/+8YHL9Hb6dgcqQ4fvQYUcREzpCNfEze1xTHhtu/3KO1Kdlz1tn5rcsUgceT20Oe3DkiHd9CrTxAvPEdnJufoDY6nB8Nub6zhSqsp4YqFMgJraiNNgbHc9CFIkliBJbp3JF2fPwgQBWK3BSMR0OCIKTb7ZCmKVpNbLyu69oUgMauhby0VkvpkOfWCyLwfRCCLMtqMjvHkTWzPWUccJqllgAvtKEAliDNphCTUpDlGVI6RO22PWdLBRjYlH+qKGi1WoSRBf1JEiMdiVsSnuV5jsHU8cZBffgIPMezhHNCoIxl5TeAKRVvQA0aM8cquYw2KKNsDnidkMUJnmfdp61XzRdgNV8qfc1AqbkD4QTRs0ksVf85K6qK5v+ExAtarJ+7RGd1k7VzF7j5wXuMj3ZZX+vxymuvkcQpD25+yMr2BfYf3cQzE5zuNvHBU9pr2/i982W4h4cXtjCldC9KbxWNQuUjXKlpRQ60IoQ2pMePmSR9XM/lzV/5BvtPnnLr3i7KSC5f2qbbbZNkOVmeErUEYSgwR3u4vk/Y7uL6EcLxcXTKRqhpne+xn64yGCX0jw44OtojS2JMUYAxOI5BygF+EBCGIXuHA3b3Dmi3ItxOj6fP9hBPd3FcSZrD4e4uSZrSXV3F9duM+n2yPKfVahO12igj0LR4fFggpEG2dwhch0R6+J5H58IWymgOY40uXOJUcnDzPqic0aCP8+ldNtd7eI7k4cNHrK6u4LgeRz/7CRevX2c8mrD7+CHd7Sv0954QRh3i8YigvUK4dgXhhCCcJVi4Am9iBi/P2yjr3+Zn3QJOXiYViumFtdlv2YRsXL8gt5i6nTQ+Tr9bIjItPKMSwCsAPgWN07ttxbIGjtO21qtiRqnVaGuD7G4KxKYw1NRfTtsxM0pN2V+ImuFeNPtN4z01FSONNiwTB02z3zRh8bxAP/v9LBxZLgQuqBIa+oDp/Jjnw8dateel9i9ZWH/p6uaH5zSwM7+mTquPFxfuX7acCZ+dBW++YMXNIfvi/Txr45YBldOevljvy+PZGQ3f2Z7/HI+O+fX5VzFn6meYuc+w8PAz4tj/dOUE5dfCOfYFGz17e+P9P2fRTbfjk2bdFwfcVTkz8K6I0myctI0N1UrjOC5CgFfmyc1ym+u6yDK0tizEQgiKvChzRVs3YkvOJsEoBFXcqCTLchwprTsiEkdYgT8vCpwyf3NRCuRRGJY5e8H3fRs/inWfMyW4s3HnGUYrRoO+TcNkFDc/+pDu6iqH4wk3P3iPSZJgEOwfDFhfX2OnyMAY0skQoVMe3L7J5vkLHB8ecO7iJfI0YfvCJbJkQndllTzPePrwAa1WRKEKNncuoPKM4WBgrfBGc/DsCVmScvXaDdI8Y3R8RJYmbO3ssLf7jCzJiFohriuJ2h2OB0dMJiNeff0t3vvxD7l0+SqT8RhlNLrI+MmP/pz7d+5xsL9PHMdE7S6b53bIY+t2fbi/z2effMhgcMxX3voqjx/eZ/vCRQ739xkNB+w9eYhB0m61OX/hPEJ6bF+4wAfvv4/nuRR5xmA0rN39peMStbqsbm7y2lfeQArBeDLm+OiITqdb5uPGAnbXpUhsjm3P86yFW2uMcdHalOmeRAn6rAVWOJLQDS2wBrISRKnS4h4EFgTmuU355vguvh+SZ5m1iJaM6EJKm+e9zCN97Y0Nzl/ZYvdH79MvtvhhMeHv+AEdIbnWu8HrvW+yN3qf4/iYzc4FfnrwIZ/p+5x/87c4unKFZP+A3/h7v893vnqZ/8v/9r/mTvCvCMRTJoMhMs7ohTE3LhQ8/TAnfqbZfZoSpwbXEVy6anj3t1v8+cEOH/5owOjWLd64Ab22z6OJy1iuoy9fg6uPCB/e4e3OCpN+n3gwxHMcPM/mpU7GE/wgKNNKeUghSdK0dtNGWK8Ua2lWOO6UVGxvd5ciz1lZXaPT6aD1hDTLkIja4l0JZFopBqNBTcQkhc3L7rguFMoSqOU5qbRIQGkbn6+E5X7wXM+GiWA9VbIstazpQuK4llAxiWMKpSyxmVuGceR56UrvUCgFWKDcH/VxHKdMbWW9VBzHQUr73jWCMAjJs7xUCNhYeAFIIYmTGK2VJRwryRYrz5yKWG1KuvblbKpN1bjBLJznC5pp5gR8oLKEThmiF++x35Wis/QIWx6vvPZVds5f4vG9W9y7+THv/+Ijokgi8jEX8gLX9bl78xO2rklUlpDc+yVrFxOkF/Ho9kesb++wdu4q8eCQLBnR6q3j+yFCpxw9fcjg+JitK9fI45hk3Kd/PODxvfscDWIUDmtb2zzaj0nFmK/cWGWlHZAngtE4I00yWu2AQE3QeUbUW8ONXITjIVVBi0MuRTHJ2hWSa6+RJAnxaEg8HpFMhsSjPvFkwjhN6A+PrDeT1uwfjjBag+NZLhBjszNcfvMqrU6boNUjbHfpdntEYQvX95GuJY2TQpaioSlTTAr7Mqq/y/E1ZUhSUeTkWUKWTJiMRwyODugfHpDuPuXh4T5pfIRKJjx+dsj5i+cZ3b3LJWx2gKf3b9Lbusj+o08577ukkxFuZ4egcw4jJcswTz05zKz6xkwnwAy4bFw+M0vmPzXvaKZba86r+poFpZJpfGdmkf4S0FDrkpqPMPOwr7Ytz4DEaWvmMGh9YTkmDfBv+2NoXm5J70yjElHfZmbaOP1tsVPLXf1tlbMkbfX7MjNXTes2zTjz6r0u1t6E2rVL+dwgn4TTZgTQOhdPNeZzfTSzz5qORfm8LzPA+zmAeZo9YO76uSoWjLQn4asTnjdf/YkEUGds+sK1Jy2fF7yn2bX6+SfhyrM8g/n2m6ZW6oQ7Gn8/pz+z8/SUul5GwfDCZf4B859P64w45Yq5mTCvYzhp7p5hiJtze/HyM8xAMf/4ZZPllPubOFW82Jx/0TKjRJgH39VPZ1psnNLA+Z1t5smLFc0sslM2oS9QXoBczVoqfccvLcwJeZEThZXbXLmhCwmmmObOLfNwZ6WFS5ag2pQHTqENeWEwRVJ3T5bsvTjWbZjSDdLzPFxHogpBlmQWiJXupVJIWpEFKQJBmsRkacJ42OfZowdk2YTh0QAtBY8fPWN9fZX7Dx6RpAWTTIPjsr21wblr5xmPhhyPCo6SZ/RaEbuf3QLhk2aaqNNlb/cZge+S5210FnOcp0gvANfFSBeTxeTJhMODQ7Z3zhNnCbdufoIjBBevXOf4eJ8sSen0VtjcucDNTz7EkYLtnUskccb9+3dIxkNeffNtHj24z6P7t7l44SIra+v0D/bY2j7Pn//Zn3Ln8zuMRkPyPGc0GvHk2S7D0Yg33nwTgMtXr3D/7h083+P2zY8x2vDqq1/h7p1b3P78cyQwGg65euM6HbnKW197m3gcE+2EdLs9Hjx6RKfTIQwCgigkbLXprKzw7e98lyhqoY0lq1tbWyvTSNlZoLUVQqo86pPxGCElWV7glCeLzSttraKutmA8L2wu9izPLEGfkGhtSMq5gRC1q3ShFPkkwfFsXnX7m4vj2lzzuVL4YQRC0upKvvabl7j5y484+PSn7G5pfrJzg+9uvYovDb9+5ds8Gj7kk9EDeskRz44+4nf+l/+If/r3fpf/zUePeHV7lbeDhP/h+z9i64rmnHrKJ9+/x+GzCWhJ24eN72q2dgR6xWH1ok8y1nRWfV77psdP74b87E+fYjwX6UkKz+fpgYdqrRC2fEbpAHFunbeLCWsZjIYjm87L9wjCyILZZIIzmeC4LmErsq79vo90Spb+MsbZc7xaeFTKssZL6eB5djfWWuO4DsIY4pIEsQK9eclvoArrXRL4AdooihybWUBaJVueZfZ9SIHveQhpLe9ZmtJqt61re25TwWk0RR6TZpYnoAI2ruuWWQxsjLU22AwInnUd1yUnRKVQ84PAEsjFlmjO9b0yD7hXK/Qwxrq8SwchypzUnluSMgq0a/A9D6U1JjOEYWgt5mVGhUqx88WKKOVuO88lEiNMSa81PQAakKF56+w3YlF4Wbb9N/mmHdeju7rJa90Vzl+5zt3PP+XB7U/Y293l4eOf8MYbN8jTmMEH73Hp8hWO9of0Jx9z8eoN2hs73PrkEzaPDjl34QpFXvDBX3wf33fYuXyNqLNKmGV88MM/I4oCvDAkTjKE53Pv4UOkF7G6scHKTogbhRwXLRzZJooCdDYiz0YMhwkqdGm1Jeb4CL8oCDtrOH6ILgocNaQ9/oiwdRVx7qvo81dRqkDromauN2V8f+XlUGdSkE4dCiNcF0e4SMdmTaDMYCEa14OdczXAXIp6G+M8IwHYlHlGKZQqKIqMPM+Ix2OGx/s8fXiP3SePGB0949nuj7hw4RxGa64HLQaHfVqdx6g8Ro4OkRclusjw2+eQjr8MDZYC0BRczggRQswqd5p4rlnB7CW1UGXHZE7AqubeAnCsrhD1t/Mh69PPsv5nITetWLS20mhHBS6nf1ftmk9rtYimRFURc+OwIME13bSba3Oq4qhSodX9nZOzp+1a1g6xQGhV3yuW96Ep5tU4eb6tC5/mRmGZ7qAaQ1HdPcuXPtPOeizm2vhlSN5nkV9Pes7c9/WsXnytpzdhDtyKJng/S/kCSORlh/BEjPqCeOBMz3/p/pmFT/OhIS9a/jLA3pf15OdecZI27Dk3L/3pJZff2e+ZbZhZ8lN1PL40p9zzlBDVcbNM4bBsfTbbsbAhP//x82X5bX85miFhzNlUmd//yU9RpfVSGyv0FEWO5/v4nleSPhUlOZlElrmuq5zCWisc6dTa4cpty6aEcerUYKPJmChsleRrdjQtAVJeu69X4Kta2Nro0rJqre6T/gHHR/t88ov3uHn7Pk4Q0e8PyrzaDmlh2N45z7mdi1y4fIFXXrlBq9XCK91gC6PQRY50vNI11lpDHMcp8/1aS6PKEkLfweiC44NnjCcJaxtbZPGE9Y0tcpWTxSnbly6yt7vL8OgI6QjyNGVlfYMwjHj86D7b5y5xfHBAu9PGaIUb+Hiuz+3bN1lZWSOK2vi+x0qvQ6ENjx8+4Aff+x6PHz7EdT0Oj4442N8nSawV+a2vvs1rX3md0WiEI+HJo0d4vs9//o/+MfF4wIe//AV3bt/j/OWrPHt0n9ff/irf+NVvsr/7jP2DAx7df8Q4mbDS7ZFlKVmSMJzErG1s8pu//TtcvXa9JOmyY2OJrswUAAkxBYDa8NnNT7l27Rqu63F8fEy708H3fcYTGz9uibmcEpDZMc/zDN+zoCvLM4yGNLfs04FvLb95llvmbmkt5Lq0eBsBxghc3yfLFVJIxpOC/+//6c85vv2Y46s77P3Tb/ErzxTfeqTxhMej7IB/9vCPGRfHiP19dv7zP6D47re4ff8Wwf4x4x/9kGj3gEvbEd9+bZ9eeID0DLrQuGpMSxpaXZcw8ogiH2FyMp3z0481/+KHksnqOq9fjQjvP2KgffbHgnzsIZMxeIJzQZuvrVzB5NYF3Fp4fWvRlg6uZ93xHccjjKLS+0TZXOoGolZkSciMwfU8/MAvAYpd3tV3AoHve3iuZ4F2GSNfqJLgzHXBUFuog4oLwdjMBVJK6yauFGEYEYQWrE4mEwDr7ZLnSOng+zY+W0hJlqY2D3gQErVaSCmI4xjPdevN1vIxWGHXcz2kI8jSvExXaPtvDOR5Vucer1ICTiYTPNfD8z0or7Hj6FlLpbIM7KLqb8nS7pV7lyVcE/y9v/U3XmojvXXnzvSDmYUOopZsTU2iNQsImLt39pxZvkFPiZMqS00FQitXKW00usgpsoTR4Ii7n3/KvVuf0D+wLtw3rl/g4uVLHOwfIVXMpeuvYKTLo9s36XRCVta28MKIR3dukcQTgnab0HPJCs2Hv/iQg6MRa+vrnLt4mXNXX6PV6ZHFEwbHh8TjkY3p762w0gkJimNEfoxKRjjkuC602wGeNARRSGt1E8fxsXrb1KaSdNeQ29/E7V7EEkXZE3Y6NhaY2XAj6nhZq7SuYF1jJGcGtAG+F8Z+DklWV9e4d4kVpVaUGyhDorIsYzw4Yu/xHe589gEHjx+ys71Gt9fC9zy2L19BTcZsXH+d+OAxvZ1XcaIthBPgOP7Sts1/t2yeLAjpJ/0gGheYuS9ndQwn32tg9gHTP0+Sk6pP87dOm7OkDQsVzaGnJt21EXNdXoD3jX/MXPPFfEOn77V6nJm7v26zmZ8oc9c1BnEeWS97ibMNmJl3y/aG5cN1kqaA2TGrdg1T/8Ws64T948aNG0ufcpZiTHOsl7S20b7TcPAMcf4LAOba6D/fhBcFEsvW0+KWcXr7aUyVuRtOvP+UuVGXFwBDS0FOdabM1PViGdxNoyHPJUg7qbMvDf7ny2mn6GkPfcEGLNsj56s67dGnVTz3PkTj/8sbUv7+Ql2Y352n+6pofvdlA+8lzV2Ykyfd0/zu+V8tvoaT9sOlFZz0Ipc8+wwm+jNbvJW2+WHzwsZde56L40h0GWNdlK7guTEUusB3AwqlbP5c7EtTWuP7pVVaCIrcuqZ7no9XMhd3O90SWNu7jLFpbqjy7WJKsqUcpXTpLioJfI8sjRn2D/jRn/4RcZLy+a27PNk7tizrrTbdlTWuXX+Fqzde5fqVK/R6XRwpKPKstMIbMBrPkSjjgDD4vle6xlvwnauKJKqLMW0mg0MkmrXtS0RJQjLqW0urIxkfjcARHO89Y2V1ncNnT9jeuUhRpDx4eB+hDefOX+D4aB+BYWNri/F4TJZMGCcDpHCYTCYcH/e5cvkyw+GQ7Z3zHOzvM+j3yVIbn5ulSXm2W5bw3Wd7XLx0BYEhzwqQkv5gwF987z/S6XZ49PAZm+e22X3yiGuvvsbX3vk640Gf3WdP+PD9j3nza2+TxCndXpdnjx/jOJAVBV97912uXLtqydAKy2jvOi5Focqczm5pabQ5doUQPHj8kJWVVRtf7LhlnnRtmeGFoBVFFIUltQqCoBagfS8oGbJtWIJwBB2/YwGT1qUrtKAoFInKiKI2hVLEaUIQhDiuZwm9hACj6XYCfu+//Bbv7rzC//Of/Yz9b73FzXUYfv8Jv/3zAav5GsXuHuNewqX/9R9wPIZn/+f/I+b9D4mP+jAYEucpexsriKu/z59976fAkPXLHS5cbrFzThO1JcpzmBiFQfL4SPLH5i2cv/0K8id/TOfoCTefaiajjMxrEY6OcfWETuRxqbVNGifoQuP7gU1lZ6DQBRKBzCVhEGCw8fNOqaQoVFECY4PjOiVjvyRNEvK8QBUFfuCjlY0Lb7ValtxKCDvny/XrOm5Zl83XrrUmCkP7PqVlTBfK5uA2lB4L5YlglVN2wymKwiqAACGshTsIfAvaHddyNCgFyFqRhbDWeSkkWZZiMGWYiP2+ikH3PJ+iyGvljFIKXSg8z1r/VaEoVEHgByhj3ee1McRJXM4ru326rlcrDIqSzLHKXPDSxUzl1JrFmCn8nhVHGjfVZRk0aLqam9lzQkw/VH/Kqk5TXm0EjuPjtHyCqMPq5nne/savEY+H7O/v8eD2LT6/t0eapYz7x3xy64es9CJc1+GjT+6SFR9x/twWQeixv3/E8fAJq+trrK1v8tZ3fp/NnYt0V9cIwhZgwwqS8Yiw3WV4fMhoNGQynmAMrK5sEroRnnuEykYkyQhIabdCikGfdNSnvbZB1NvE8VsYXaDzY/STPyVPvoa3+TZC+nY8KqBNeVbWoHsag1sl07a4RmNM6Wo8F1JQH6fz0sScDDYLZGs1x6IMIOwLkbhEkU8UtVnfOs/1N7/B8OiQ+59/xMNbHzEZPGA4GrOx2qa49THSMTjhU7zRPkYEdHfessR3DZRQ9bGOdC4t3fPAckE8aMyRRtNPLieB7BNlj+Uo6CRZtLqnIpGr391zBPxZEbq5rsTsx7nrZxHO/BPEyWNRd6t6xtyzZtqx5PZKmKwUJAu52MoGz/sVLy2zoHtmfynrmgeW1X1iBmE2H3AKKmi+89Mb9mLlOaD6zE87TTifn3hi9vqZ/fOkhy2Z/6fpDP6TlWoeVcjsBRQRJ4YPfMH+VbL7C1z8l1hOa8eyHeokMH7C72a2pr/80njKAgA+qa+nIf+Tdufp98vOky9UTltvp2zHC42Z39ZOeY2nzoLGtn72B59xfp9Qzmzx/t5f/Mi6fUrruidKF++sTDfklQRqSqtacK8s327Jal5950gr/GQlc7mUEt+futglSVJbowxTYG9KMq0qjZg2BkdAkowY9Q85Ptzj/Z//nPd+8QlxnOJHLS5de4VXX3uD8xcvsbm5zloJtrMsrYXuJLHWOs8P8Hzftl9pa7X3fQKvEvgESZbZ1FWOi5ASKRSySEgmMX7UIh6P8XyfIks4Oj7G9zy0ztEGRoMha+trluCqtcp4dEyRJRzsHXLh0mXCKEQXObnSPH3yiO1zFxgcH+H6EXmesb7aww9D/vQ//BF3bt8mz3MKpRkMBowGA+I4trHOUvI7f/336PV65EnMYNDn6dMnbG1vMxoM2Tp3ntEk4Td+6zfZXFsDB548esAPv/8XXLp6md0nT7l8/Rr9g0Os9dElTVMuXLnOr3/3t3DK2GFjNIEf2BRtno/r+6A1ReniPBmPePT4EdeuXcf3fOIk5s7t27iOy2tfeb1OJWalaIHrW/fjLMstOESgjPV2ENICzSoXtO/6jMYj2u0ONhrBup8jsC7n0iFOk1qU830fgUMgArxxxMe9Nv+PDXjfGfLrtzK+/Ytj/v2f/gve++iPiC6ukmtDfvcxJplAkYPKkWikBNeVqDRDCEn72lUmzx7R3Qq49rV1Xv96hytXfULP5V9/Jhh9+39Fb/0cH/3bf4b88D0OP9pHBR7G9Yj2n+I7Ba9cuMzVlWsIBFmSkudZmafcKhdMGddcsYhrbZm/gyCwa6CM8fZdS0rW7nStF4LRRFGE7/sNJnK/HHMLqF3Hsd4LdawrNqa7JGyLoog8z0mTGM/3iaKIIAgRUtS5tQM/oN/vI4BWp13fa4zBK/cMDIRhYN+hUsTxhDCMcFyHJLFhBUFo55IqU84JIE5iQj+0xH2eJc9zpIOQVuhM0hQpLdt6XqY8jOMJvh8SRREGY9tTZjcoigKtFK12G9/zS6K/ioci5+///t98qY309p07VDAbmmfBLExobL1UeuUXPxabNTTB/fSzmfk8K4HWRkKtUEVOnufkWUqaxDaEKMtQqgBjvRVc1yWKQsKohRf4JUmih6h4P5S27tZ5TpamJJMRk+Exo8Exw8ExWRLjeT4rK6t0fI1M9jD5gGLSxxGKwIXA0wS+Qxi1CHtreNEqwnFQRWZJMVuX8M/9CjJctS7epYu4MTb+uurfFMLBrBWyUrwss8GUoKoJpmYsqEteyjL57ISTe4r7LN/IZHTMk3ufc+vjnzE+2uWNN1+l3W7RXdnADUL8IMRvb2G8Nn64NvOIuiPz351UTrnAgsEl15wmgVSKjRkAOg/1mnNyXiUwD1ybKqVmE8T8F3W/F+TNJU2cB+lNMDArQs0qCupHzfTCLLZn6V0LnaCptZnWON+K00uFqeb+OHNZNoWn8Og0tcjibzdeeeWFnt0sz7V4n3Rf449qLdVx4OXnZTfU82B+vj1XyGZ+kpzYjufW8RyQsezSE2dUtdYWHt6cY8tY7k9o3gwh4ew9z1tjf+nlL6UBy9beSYjtpDV2CvBetuUtK+Z5c7DcfeaaYMTcLnQq8J7fd1+yVN5DS7bj0+8764Vz1zM3hKdtd6eA7eepT+Z/mH0fyw780++fefaXafG2Fjhjrd5ZVrNSu45DgSmBT1Gy47pMsglaG6Iosm7IqsBog3QEWlmQ5EhZbqKGSTxBCEt8BNQu3lU+bseRZFlBmqY2J7HrWstwf59PP/olh4d9hoMh9x4+pbN6jm9+96u89bWvcunCRYLALQGLQkpBEseltdbGgrqux2Q8Is9znMSyK/t+CFIQT8YAeK6LKt3mnUrwFIIwcGlFbZTSfP7JB/RWVtnauUhRSLJ4Qqd7HseRPLx3C60UcWZYiwLiyZBHDx7y6ltvoxXsPdul2+uA4xD4Hkmc0D8+wg8C1tfW2dg+x8P7d2yKJmGVH47r4XgCbzJBaWVTaxlJmhWMhkN2zp/nydEhWZ4TtVocHRyiheSXv/glv/k73+Vg7xmBCw/u3uP+w4d89etf5+P3P2BrZ5NnDx5gEKxtbZLnipW1Lg/u3eXchYu8+tpX8D3rCum6Tq0M0VohqVxgDU+ePmXn3HmkkDzbfcbnn91kZW2Nq9evkxW5zQHvRDbVmDEkSVq7BYMFaLJ0f1Zlvm6L0QVxmiKlS5rZdGSe5yOMJcxyXQ9d2LRSNt+19bJwhABPk0YD3jHwT/7smP/Dz37Kh//4d/Av+rzz1d/go49/wPizx+g8RwJauAhH0eq08T2B59oYUqMLcB28zRWEGmIcyb3PJzx8MKZzrs3qa5t86Fyhe9Dn6Q//nPHDJ4hv/3305I8QRhE+fYqnNe21FdZXLiBda91yfIMW2FRvwhKMOZ6L0YY8L+dwGbMapwmCKUFdZijjTVWZq9utj2Ht2lz14/EY3/MptMJzfQqVEwYhCOtxEEUtHGFdr13XrTeSMIyQrh1n6Ug7xtoyr6dpUnrA2BReQgpc4ZIkKY62qaGkAKU02lggpbUuXdalJWwrCpsWrHQdL1SBwLquO56DyouSJV2SY7Mi2HXoIoQs06VpnJJV35TtMGUdxhiyLCOOY7I0Jc9ywijE9Tzrgi4tH8XLlgrQmUZ6qOr76TUVmGvCgMYVDdfhsxyVFa6cWgormDC9W1RtqzGPsLm6hSXMdEIPP5w+0FrKTX3tFLs2W2QzClTXSkAIDyEtAaNdsw6O5+F6HqPBMckkpt/vU3S6dNuX8LMDPCEpJn3GSYrSdu5qYowuyLOMsLeJ67dw3IIieUz24Ah3613cletoquwaTYBciZ1ipvf1sJ8IM0w9mDPC+gmXLpVUn5fj2Fih2HEDumvbtHtrXLj2Gg9uf8L9T39J6A+46oS0sIrOPD5AJIcYL0S40ewjGvOndqU3J/TtlH4I8ZxrTioLFtp5xU/V4SXXLZFpapAuKuBgFq+rH7MMWMw+a9qkRQt6JUQ210sldTWrawLk5mpdDhifA8qb7W4C6Prv8mXMIbsmuJy2xszVM/s0Ud5XN3VmKGcGffptE9CZZr9Fg3jPnNzNFy2nCMynXT77fp5zQ3ndDIB5kfbPjd2L3l6XZUBsOXabf/SJW81imd/ZlqKJJXedXPc85PiyXv2Zy5f+wLP2aNku+vxBXNyTTqhWzL2W0xQ8cw9arHq+L+KU3xZ/Oa250wuaskQjq8Tc3WfRN5z+nGlFJ76p+e6dcOSetkOfqk4xjU8Li+MLKC+WlLOnE/MDrLu1tQKbMu5TG+vurY1BGlGmGJNEUUSW5wAlYDIlkZZTC/QGbIofrWoLmiPt77pMWWbdYTUIaxWvrIB5nnH305/z4Qfvc+vOQwbjjJ2LV/mtv/X3+fo7X6fdjpACiiwlSybWOpxlGAyT8ZA0SYjKFE02D7C0bOHKsqRrbZm5lTHoXCEklvDLqVxeFQJwZZvCySnygvOXrhGnOfFkTJrEVgCVVrEwGMVsrK/Qa0ekacbu7lMunD/HwZOHHB0e8dV33uXxwwf4UrL35DFR1CYIAhw/ZG/vGU8eP2Tz3A5SOgyHA7QxteVHCEmn0yFNEtIkQWnDzU8/YWWlZxUmwsF1fQ739lnfOc/6+ir3bt/hza++xfe/92fc+Mrr9FbXMKrg+ivXyFVG4oVsbW/afMlGsbm9SZrn3P38c87t7LC2tm7fXQny8jwjyzJESZx1eHCA57i0Wy0ePHjAs91dbrz2KhsbW4AFQVlilRiVOOG6Dmmq8D0fXcdrGnw/IBOijlcuMhtDLFyJNqBKK3+e5WRpRhi1KIoC17GeCo50KZSiQOE4BUHkMUqGrIQx/9XGV/jqxiY/9QOO9jJ+5Zu/x/d/8K8QxQBVJKy+usM73wrYXPNw3ZzITQgjgfE8HMemOHK4ZNsTuWivxY/vGH72ccJ402P8p3+CNxnxyv/8f8HOufM8uHyBB3du4v74PQI94rf+ScThHxuSNMWVNs7dcUuXbG2QJUlhlsVoY2iFUS2jVWvHcSRaC6DAAFmpGKuUGDY/vQWWnmvHQkgLTB3jUCjL3eBIpwTAE8sM7wf2e9epvVySJEFNFIEf2LzdpadLFSvtuh5ZkeFIlyiK7PrVhtxoXAdMYbkBwiiyqeeyHD8IasbyqnNZlmGwYN3RNm5dpakl1zKqTG1mKPLCxnWX8b9KKTrdHlXqMm20TV9YphZrt9u0Wq06PZ3RpdKvCQpepoi5A2LpSTT7jEWr3lQIbh4UJzrhNuT6eeFL1LWYWs6f3jIFKkY3lQVl3uimYqAG5JLml6L8zwiNkdJWJ2xqSavEsAoqz/Px/IDxYMBkPKrJIHvdDYJ2gDQSmY7I8pRCKwqVoQtNIGJ0sUfQXSFs9/CiNk6RkT/7c7LxE5zNr4PbqsjfS8xQDfrcMVtf04AeC45epvFGpioRO3ZTEFIBuilcLK+tXdjn3wE1GhKNlyAdn87qJm+882ucv3KDmx+8x/0HT9la7yC3bRhO2FslOX6A172A63eYr9jWJm0bxBxgnR2BxTI3R5cKO6UbuKj+Zbksb+o75izDJWgzzSsbxF2mBnvV91VMPiegpZOARBPENm6ZB8lGzLp7V+04DRg9L958yWjU86Z575JuTNelqOfUzBiVgycaN02b3+ysmQ6NqdbvtL469KUJ8mF6TdWGme6I6f+fiypeoDyvihPwwkyLz9iMpd4cZ2jCcy/8kkFh1cyzgZc5dLbs3ZwVJ8wN0BdWMvz/dZkf3bOqFL78kWieMac+Y15ztryiubJ4rn1R5YlY8vfMU6bH7Msrc+fKzDmzrK75V7n0ADvxq+X1nNyaE258eWXkmYF34Pt1A4QGr2QZrtw3VVGghEAoa0F2Xa8GUALrVu65Hm4pxBtj8/O6QpQMyIG12mUZcVKRKVlLnlKGPLPEYZbZVtE/eMqP/vz7PNk9Zm+Q8lt/7W/we3/9d9lYWwWBzYOsCsYjK/AVSjMaDdGqQGlDXmgmcU6hNWiD40jcMmWRLEFeOpnUlkPHdXGKkonZ0QhhyeKyPMPprhBFIaPBMZ7jMDw6ptVdobuyTqe3RpZM+NrXf5Wf/ejP0DiQp8SjEWmnS//oiCAMuHv7JqsrKwyGQ9ywxcbmOfafPUENB1x/5VUG/T7tVpvbn3/KcDAgakVMJjGUXgiBDupYdUcKRqMReVHQ7q0Q5gWjYZ9Wp4NKE7IkZmNznZ/8xY+4fO0G9+89JAx9nj15yrmLO9z6xedcuXaJNC+Q0uXypasc7O3iIOh2O9z57FOK66+wvr6JkJIiS8kLC/rc0v306OiYS5cucef2LeI45uKlC4yGAx7df0CSpQjA83wuX73KuZ3zKKORQhCEAUaDKwQylBbwSYnGYIR1P58kltnaKmEkRggMDo4nKLKcPC8o8qmHhet4OJ4HJdv6ZJKgCkV32+cbfsDhv/uYV1yPn318xDeu/hY3P/kZTybHCCkJNtrc+GaHFfcYT2mkcDCOiyKk0AW5ckiUINMKoQS3n2TcGndJrmwgXv9NIsfh66+/yWonJE1iglaPzXe+Re/aq7T+TUJ6J0NPNNrV5XhL/MCGXSiV1+m5XM+jKHIKVZClqQW8vofAkKUZWZbiuh4Ga2F2Xdfm+paCNLV5tqXjlMRmdk4XuY3/9lyvJkb0HInn+VapphV5bgB/eo8uSkBr8Mu4a6WKOlVX5fpb6NzWQ5VaLgFpFXRFlhE5EVmWUeQ2b3teKLyy/YVSJPGEMGqVLveyBvVSSjCmVBb4ZFmK0tqGfghK0jXrWp5lWQlypE3FVgIIKSR5lpIkKUVuCeZEyVvx8qUSt2X1sSxz2/7MRr3ESjkr6dbi71n29xm3WTN97lIeo4acbwX/qXV7QQCcscbRvNE+UwhA40iJRtu1KyybvnS90jPHx/U8xqMhaRpzmGd0eytE3eu4zmO8vE8aT1AFaASFHkLXYIYGrQqC9or1gtIa1b9JPtlDbL6L6FyCcuSbsK+yfBv0Ury21JV57tfpOIjZS4SYu3Kxouo1Nu8xVHiyustBeg7rWxd499d7PLjzCY9ufoAfjlhZW8HgkEz2KYqE9vZbONKjdnuuX8k0XZ1Z0tEaoz1PeDnhq5oxvnZznHdCNnXqwcqq2zQczDRpjpSs+bZObNBzyxLtwcKqagpJTYAiTpCdltW5WN3ytsjp45YNfkNAnJEdxXQHmZl9Nc6qLOZTJ/iS5gSz4FkwnWFN91Tb3eY7mvXHqd7rdJiqSfa86PsXLPXW9IKQoIE5T53XL9qMJd8/b0aeqeVnFf6r3xoXvWi3Zvf4ubtPxXgnTubll3+B8rIA8IsCx9ky399lqO3Fypmm4Qs0fiGbwMLzpqrORWXaWeqfqawWAab1cWp9Znr4zIzml/F+qrF8obpOH64XrGe+NyeB7i9WzixpGizTsdGaIrd5duu8p6ayagu0UYhSUFbGUBQ5qijqWEwhfLSxQr5NQwaOYy2mQlj3dR/KlECWPAtTpvwpbH7io/0n3PzwPR4+O0a5Pf7Of/G7fPfbv0q3HZGmCaqMHY/jmOOjA46PjsiVJo4zmxItjGhFbTTWQua5Lm7pKttud6jylYvMQWvbVyElQRii8pwkTel0uggBRVGwv39I6Bpa7TZ7e7usbp3jycN7hGGIVopzF3b4/p/8R65cv4EEZKdLu7uKH4YEgc8knlA4DkEY2fFVBY8f3cdzPVZ6q0RRi1uff87a5hZ5XrC2tsGzp0+RGIQUtNtt0jguhSOBIx1G4wmPHj3kypVrTMYjkjih0+txeLBPkmZ88OHHhL7PxvaE27duce2VV9jc2qB/1Oc7v/Pb3P7sJl958yvsPnvGwf4+aZry+MFDjCl49Ogx/+//1/+H/+wf/AO+/evfQQBFkVtQKiUPHz5EOpKPP/6Qvd1dax0X0OutcPWVV/DD0Lo7qwKjNZN4YuONPdfOMWPznme5tXqGQYjjuDiOS5wkuI5LGLVACJLUsp9XTNpB2EJrgxe4OMK6vydpguM4Fmj6lsTP9wMwhqQ1Zu2aw/GTPpeIyR7A77/6X/Cv85yj7IBnP7/PvzIX+Wt/d5WdNSiUQBWGHEWeCzJcUqXQ0qdIQlQgCdsO5tYRMnzApd/7GxwfHXDve++T/exnhHt7nO+tExmDjCPGu12kVKRpakkFdUGWp6XngIc2Blc4uGUu9Dyz68kYmwJLKRvzjBCkeVaGatgYe6UUjrEM8VqXJDtlzHir3SYIZJ1n3RiI4xilNa0oKoU6a/VOytRulRU5CEKUsqnHjFJoVbqNFzaNINic4pmT4XouHa+L4zgkaWKt4J5EaY3WlqhwksToQlG4ORjLbt7pdnFLy3+R56RplfLLJwxDXOOVXg2u5XyQDmmWWiWNzU1WAxQQxJNJSeBWgm9ZJvgq1wzG1GEuL1OEMA2L56xAO795zxycz6u3vueUsoAzmqBm7iCpDsy6WYaGP+lMlVMcM2eNq0F91dcpwrLx/BKBW58DjmPdzj3Px/V8xsNjJuMRx0eH5N0u3c41vOQZkXdMkU5I0hQtDMXRkE4nQys756NOx3pthAEiP6J49n1U/CrO+tvgRhYA2u5TWVMlZfaM+UGaEYoXYcgLiWAzY3qCGFLJRrVQ3ZwhDmGrx/XXvkYYtrn78XsopTkXBeh0jBe10OkAp7WBQDaeQT3Hzcx3jfY/Tyo8g4WxqsPM3NC4soFSFsatWf88ODhRYjMz/8x2SCxeNyN4mnp47PsQU+tJo76GXXq2D1ArCEzjuWLZYyv0vCC5zq3/E8d/qgyo3bkXr5j5q8LYlRJn6ixgG1I1ZZlgDGJ2js55YtSjWZNAmOar/XJR2Hw5FRye8tMSvca8jvAF9E51nYaTsYdZ9uGUNs9fX7+jxm2nD+2SwXneu3jeQC2p+6Qqz/hqzlZMgzHhDJW9HBR+gdqa625pe04CYrMn/IuXM77HmWtOkxteTHmy7Opp1JaYmy3L93zxogvhDOXLIC6cb8mZZKjnXjF/AHyxhp4ZeBdFjue6IATSEaUl2rrECWljQLUxNherY2N+C2Vjsl3Hsg5Xxfd8HCEplCrTjjmABQECYVOWKUVR6DKFkiVTMr5BFwkPbn/KBx/fxu9s8Q/+4X/FjetXkECaxKgiZzwecbi/z1G/z8H+AVK6uL6P50f4HqyurtPtdKnIeexuaJDSMjD7JcFat9u1gr8qaqG6KAq6vZXalVdKiSpyjAOjoyM2N9fJkglb584zPD5GiIL7d++wsblJEERI12Vw8MwqKoDRsM/OpeukSUxndZX337vD6toa586dw/F8dp88YjgaEscxg+GQTq9Xs8J7XsB4NMaPQjzfko0VSjOKrWv9oweP2NrcxnEczl/YYTgYcGAsiVwxGmF8n88+/ohz53cQRpHGKW989R0Onz2m1Wmx9+wpgefh+QFHB/t896/9Lp999AFXr1/nyZNd/tv/5r+h1+3yxptv1YzleZbx7OkTJuMx0vH46te/wdraGr7vkyQJnu+XDOgSIX0L1hzr4qyFwPGdenup8vbGJrZgT1vFiSjZr4V08MpYe+NY5mvHcdGqAGPQGJQxSMcpgWVCnua0Om2ksPNMtSCOU3o7EfE7E568P+Bif4e/e+O/5I/v/GueTGL2f/GMf/ks5Zt/+zKXX4Mkj0mKAmFACwWuR6flMhoL7t8e8/jBGGeQ4935Q3Z/8mOifp9VZQg9n5XOCoEq2fE9D98P0GUKOeE4ZEmMyQuEdCiMJsBBCUMYRPhaofKiBhVGW68RLwyQQlqXcmPBT+VaLUoA7UhTurAbgiCshWhjIEks0Pdcr1b8SMetQapbWt0ncWy9UEriu+O4j+97tFptO6dKcq0gCgkjmyPbdeycqFxWC1VYF3bHR2tFUVq6cT0810dpm47Qzg8L9tMksdeVGRHyQtXKOimsncZ1PdrtDggLsh1H0ooiG2qhTQ3QkySprexKKUsY5rkYA0p/kWO+cmNlKg03tccLFujpd01hcZ4orfr11MPjJKmylB5PtFfNJFduwsElQkZTUqr7MxXYrRLVzkvHgBYVCSdT8O26uJ6H55fW7+GA8XBAkef0Vs7TCtfw3Kf4wZhkPCY3hrwf0y0KWhhMkRC0u/hhC9fzEUqj+h9TJPs4299Ehpu2pzPuxFaMmLr/zoNwUwP2eVbuBSH+pEFuDu6C5FBd8zxhU+D5EeevvIoQcPDoM44PBmyudXA8n/7jj1i7+i1cL2LWej4FUVP5bFZAO7UPYnk/q5GoMF2zrllBvCFAzpGf2WZVgHB+jlV/zM/36RVLHS3mG7+kQ6JZr2g8VczfuQQUN5QMNZ5dWhpgvwlm539bIqRN651DyNUfDZrd+XRZM+9UNOpv1FcrHBshEbOl4QXTrHs+jqBa5zN9+eJlqud7AeF1fhhPaMpJIPdlReUvB06cscw9ZObZL9iQ5cNzGkngX1U5WwsW2l8drXOb1VmA2l/pO3zhclrrzPKPc6LFl9oMmEfU9c/TLWNuz2q8G7FwV+PC5yi0TrrtyygvtnOJJX9/uSqgMwNvrTVK6drSlpbCtOd5CKgtlr4XUhSqTDsmbIqxPMfzPaR00KooD1RrVXY9D+vtXcaqGk2RWMIs6/KtyMq82WEYMBkc8vDhI1Id8rf+7t/l2uULOBjyPCPPEvpHB+zt7bK/f0iaabIC2m2f3sqqFbJdh6jdptuxMXM29ZWxaZJK8i6oYqdt/uNAhkhHEscxYdiy5FFSkiYpSRpj43J92r0uk9HIEriNRnRXV8jjhJ3zl3j8+DGj0TGO45FkOcl4yOHBAUZrVJ6x9+wJG5vbOI7H9vlLHB3tkqdp6R6reftr7/DgwT06nR6rq6uMR0PyQhG1IpTSBGFIq9WmUAY3yyDN2d3b49GjR1y9fp14EvPk8SP8wOfJk2dkaY5SI1qtCIFgOBxx9cZr/OKnP+LixUtMxhMe3b3La2+/TTwa86vf/jU+++gjemvraJXzu7/3O/zhv/sj/vDf/ztee/11m4/ZGN7/+c959OQxr7/xJhevXLGkZ6WruI0RjpHSYTyZ4DouSInnOAityYvC5nxXCj8IcD2PeDzBFT7G2DlYgcBkEqOMJgzbBGGEdGweZlUydGtToJQhCC1zVDpJ8Mr0VUWWIg0kcYI2AomhPx6iRY7eOuIgf4aXSX71/K+wf+NdshXBL3/8A/7inx/y+Gs+N77j01oRuCiQEscP2N8zfPazI4a3j1kdJYTGQRrwjkdEYQvPdfEdjyxLrds81uqqcmsVRkikFLgtS25mCaZSsiKz81KXXhhS2J1LgONbcGwVC5ZZGg0YjRE273ymCnwTIIQFlo5S1jtACFLPMlNbhYWk3eliCMmLwjKOO5LhaEgQhLTb7ZqrAex7dKT1UCiUsinCqgPBGCaTCUWeE4RRCQpsuj4nd8myHN+zTPmi5G4ALNFbaAH7ZDJBK43jOqV1uig9USBPU7RjU6dlWYopCiAhjCIkgiAISJKYIi8QEhxhQ0Iw1KnD8nIMVFEQlSntVJF/we10Cq6acqUVtJds5kLMZCia/12U6KopPp+GAxbrbwC+JsN3dQ7WqGJa6xRwNGHQLDA6ETMgENjQDwkYqRDCs5ZvIaZzzXVxPR/PCxgNj8nSlOPjI8zKGu3eDZzJYwLHJ59MSNMRTHLy/IhWp4VROSrPCNo9nFKZIrJnFI//BLP+Du7aqyDcch8vU1VV7tLG1C69dX9nrP2i7kX1ceYn0xBA6v43x27J4dx43knCf/V+DeA4Lps7lxkPjhgNntHrtSkOdonaPVRybIF31UZhVSrVmps2aZaAbAa8meYzGwBvvkGNrk3JtZ4jNC+4blfzRjB7ZxPMlQ+q75tvbDkyM3HL1a9mOg6mGhPqs3tGQp1pJzOMztP1Zcp12rT0LhG86gkwraHy3luiemg8Zfrd1ENmdg3aqpcg7bk65Ey75/tR3bJsV2HG43/m3TTWfP1XtXZYEgv/MqV6lfN1nVT3Mo3QaZXPVfYyTa6nWmO6zQKP5sVnqO+ED0tvnZ86cJr252ylnK9iplNzG9vzbv8SysyyeZl7l5QXVagszJAZpfgL3/0ll7O/ky+tzJ3jptwbjVboPMPoAlNmhMGA0QrhlPKq6yMcF+narFXN/XtGX1cW01xX1QNPK3MvtxSHZtv+pZa/rPe6WM4MvB3plMCnjL3OizpO25GSJE1Ka5Z1W6X8LQhD/Ao0OBLH8dHa5pu2gpPd+IWxbtN5ljGaWOblVqtlhW1j8IMAz5E8e/KYwbjgW7/xm7zz1lcQRjEqydKODg/Zf/aYo+MBGkkYtVmJIrwgYKW3QqvdIvB9CqWtYqC0Wnuei/Q8PMemY5KCUjD0wBjSzMaC+p5H4AUYYRl4DRrPD2yOYtfD9dtoM8J1fYpsQJH4GOmy++wpyWTIuQsX6e/vsbqxxV5ekJmMeHTA06ePiVptxnFMu93mcH+XJJkgjOHStRv89C9+wJ3PPyUMO3zz175jY5pNGfsYRRRZUUI5gR/YNEqt4ZDBcMSt27dZ29igE/kYbTg+OiZNU9Y21mi3OxRZTpZbwHu0v4fRmrAV8cmHH3L+0kXu37pduuFbcjwhDGmao1XB7//tv0l/MLJEWFrzFz/8AQ8ePOQ73/0uG9vbCESZq1mRpwqkIPIiuyhdy2CfZTlpkjQY8sGUnhSW0du6hmdpRq4KWq22jVUOo5rFGq3J0hRRss0XyhK0uQ5QAn7XdQlcr+QkyMv37+IIQVoUuNIhCD3W1nsofUDcjgm8lO2BIH6m+YZ3A0cb9Md9Prn7GdtfD9l8rYMbBdz/6IijT0bofsqqBukEOL6H63jgSLwwsgomIVBGUejckoUJgyxzlVdrDCnwhASjrFXc2BhObTQabcF56KGUnXtUJGzCephIrHtvpQjRJZu3JbqySqY8t94rFe+C5/v4nofjyDqcA0wdgy0QpPEEz/dptzuoorDxu44kzTKk4+K4Tpmfu0znVpLC+Z6HEZTPt+9SCIkqCsajccmGLgmC0BL0pRme71mXelXQ9tr4QUCe58TjMY5n9580SwlDO5eU0kBBmqQorXBLDweMRueGVFsXfN/3EY6sLfk2bZZmPB5br5pSeP6i5SRZcVH8XpSpKpA0lcmfA3bmTTx1zTROPtGQekwNIOee3BBGGhbyGWvwsqeUyG4qqdcAyJKvlYBNCMt6Xio+HMe3XCB+wGQ4II7H9I8PKTo9up0rONkhLrtIV5BNRmR5QpGP0B2FrxW6yPHaPXzfx/NbeCYn2f8xKt3H3XwXvO6set0w43I8JUVrjFfdv4ZlaA53naitF1i2+Ma9lfKikh/moOZMZTUIkg5B2ObSK2/w6Lai0A7dMMANWkyOn+C1t8r83qYBdBtSjlniHbHw4BmYN3vxMgQ+N0ZTeWiJ2DszyZsgdkkjlt3X6Mfs6DVnbPXuGvB4Bm/OoZslfZsSkDHjOdBURjTnSX1Pc+402lJ5lcw6sDfGbGYZmYXfm0+qxmvR1b2RW2AutKBZFkdX1F9WJHkLe4tpXldes0As9yUJpi+KlpYVsUQQP+G6F3resvq+IN562dub63jmPb1EO5YlUZxW+J+unKAXW/r3iRV8kbl0ZtD9AlXyIk16icbPT6jnDeJZWlECbp3GqMkQkyY2jS5gZEWsasmnhaAM59NIP0S2OritLtIL6n1VLNGunVnxMn+NWf71i5RTVRkvvEC/+D54ZuCttCqFYVUyFeuSyMiSLimtbIxoUdi8waXgX7miSynISnbhiuSoqKziro3nrtyvfT9EVICpdCHWqmD/6SNu3rxF7rR55+23QOXkRUY8GTM8PubJo4c8293HcQM6vQ5Rq0271aLT7RKGIVEU4nounraHnOs4KK0tmZUxNg2TgLTIiaKWjVesyJKQtKIWjuuQpRlaadtOR+CUxE5SdOnpgpsf/oKt7R1WNzZ5/PA+B0d9Vrot3v/ZTznYfcqvfOs7tFptti9e5v0f73L+4lWePXvMZx/8nFa7zcraCtoIjvvHfP7JB6yvrfHsyRMuXd3BAK++9jrHh4e4SUyWF+R5ThLHOK5n2aI9yyi9fe4cShXcufkZ57Y3QQiyLGdjfZ3haIQqCi5cuEAQhlzodnGk5NzONo/v3+fGjVdQWAC3eW6b4/0Drt+4zq1PP2Nj+xxFnpZeBjEP79/hwYNHfPLxx3z3t3+b9Y0NBNaamMYxjueSl4AqUzbNlx+EaEfXVk1jDEmaEPoh0vdRShMnMa7nI4UkmYxxXIc8tYzdCIHn+GCkZbZ2fAvCS3CrlSpZurOa8MsYgXQdpOsS+JZFWxeaoihod1r4gQfSgDAcykO0yZh4Y5zdnPVul0PnGXvqkDBz2P/zIQfvDcEV5KMMYSyJmBv4uEFgrXulWzaBi3YdjBAEfoi1drslmLZ5kE2hEOQ25zSA0RR5hu8HSE+SqQJVssY7CIoyvZ/veuSZjQ8PwqiMaTUl0Rl0wpA0SUqyK8tMHkaRZSt3HAyGwLeEZHYOuQQiZDweEQSRJWMrGc2DMg+3gTqVV55nNp2YdsnyvM4X3iqZ5YOSsbxiEVdKgSlQWhGEId1OB8fzcKRAyMplXtHutG3qr9z2s93p0O05KGXne5ZlpElS75nWg8Z6xlAq6jqdtvWWiYvysFAIrDLHdT3rBl9YwD4cDr+YDFJ6AtSs4DMCuqCZ5GsG7DWAEo2v6zrFovN5EzaZmcO3ebzMHv8NmNGUw+t2n9yI6Xe2jkZ7my7nNVZpcKkLgSg9SowjEEIhhF8qa0vSNd/H80O84TGj4YDhoE+eZXRWVgi6IWL4AFdrVCYYpwlZPmJFKQSCQgGdFm7QRgoH3xPo8T3S+Ah3+xs43UsII9ClhC6qOFhR9mYOWFVFVkzhTclmAXyYxWFq1Du9bDl4XQT7VQUSKSFq91jdusRw7wErvS5qfAwyQOsC6ZThNY3XsPhB1KBkpo+Nti0n8ar6tSg+VvNYlp/mR65uRq2IETPDtaTbcxWYKfiuPTWWXvgc2Wdu7osl+ilRLa9SWSIq5vYm2i37XK/Raj1O1+7i08qxn+v3zEpcmDemXsgz75ImT4WZqWuqo5jOt8U4cQFi9quFHLPLTFNUY9KIl0ecPuRnKS8DkpZpFUzZ7NMunp83L/pscerHU8uyqb4URJ/03Lm21x7/zQc8p0ELP89Mixc46Oam00tecuo9J+0NzfVv5q9t3mzqlTIdmkZd1ffmrA1tbpfVXjmzj5++mZ1tDJp1LJusp1S85OcXfQdzunJ0kVP09zHxEGHKPV7a9MQCg84yTFFgVIE2GsqsU6YE6jrsI9s9vN46ssxUNLMrLttmTurEkuuaasmFek7o/Hx1M8N34hp63kH1nEVwxgVwZuAtyg4rpSxzuVfl0BXkZZzuNAe3jeXL0gyMZZW1cd6BTSNVpjuyTMSitm7Z3NTWkiaFJMsyVGm9NLpg0D9iEBf81m//NqsrbbI4pigKkjjm4GCfw8M+btCi11uj3W2ztrJqY8h9m3u4UDb1kOPYye55Lj4C6Vph3wrvoA3EaYpbOPieh+9ba3FlqcvLfMLCcUpAV8UQGjor65y/eoNnD+/hR23OX75Cq33Ms2ePccMuk/QRP//FL0jHQ157601GoxGFKlhbXydqRTx5/IT7P/gzLl65RKvVo9Veod8fcHx0SBB47O3tsra2Sm9lhSfjcQ24XdcnaEeoLEdEEYkzptVpIwwEnkccT9jbPyTOMuIk4fLFSzieR5ykxOMxG2sr3Ll9l69/+9vs3r5DPJmUbuCC46M+W+e2ee/HP2F9Y4MsHmGEoD8c0T8+5PGjxzx78pjLV69y5cq1qdudsana/MLH9SsCr7EVbIUg8AOyNMGkFhQFvk1BJ6QorbI+QkgKZZU8FcO10YY0TWi3XBzXxgJLxwFhSb3shJUolSOAIi/s5iwE0lhlS6FtSqyCrJLzSqWPR6fTIstipDS02iGDzoBRf8x4P8FMJO2iQ6hC8iJHK40XBdaC73oIz7HuOoAXBig0OA7SD6wVGo0qcrJ00nC/lRgpbAYArcqwDb9Udlm3aoyN0XYchyKzZGlpltpYaEeilbX6+75v16HR5JkdyyAMwIBWqmQGd6gIsQSU11mFmtQ2lVe308VxJUEQoI0hLLMYJElMGEXE8cTGavueHV9hwZDWulRKWYBVKIVOEsCmv8uLoo4jj0o3dIwhyy2/Q1ZkGKOJoha9Xq9eXxVfRJHbMJQwiqxipQwR8TzfEisWOVEU2bk3iTHGekV0ul2U0iRJjCpUuSdZDxc/8KnyfL9sEXLRWt4E2CeJrYuCMA3ZvEZIMwflDDiYeVjzNFkUlWvw0HzUfLBn8+CqDssmMCj/Xx+EtftxAzjYxUblnioqMC5Ly7eQSGGVUk6ZG97z7JkxGg5Jk5hsP6PT6dLpXkeIR7hiD+U4FInD8SAlKzStjgIUrpKErbb1xvIkpCP07o8o0mPcta8gZYCuSLYaAuyM27lp5FCvQXpz5KYW2GrtTN+HqYHc7FA+DyA2L2+8N2E9AlY2dizZXBzTW+mRHB0xPnzM6rlXmIYpNN7bvERRC41NMDknkS7MEVGHkyxrn1j2fWNy1vNDzF5bPaseY6Zn5mxdc20S1cg3n1vN5HmKtKaqq8m9bp8448UgpvfOM7fPMyIsrNHpD422T0vlBj77Y2MtM7v2TFlXvU6Wzh9B8+PMM2fiVZa8z/L35vqftm/KzTB/X6OLS/v5wuULV/AC9U2X9Ys9dpng/pLtrmbt/E4y//tpbTjlq5dqy192eaHnLOBN+4XS1hgCNnONJWK1YW7G2H+VUiBEHR7qlEY0KQSu5+FIe7Y40sqM895FS1/Ol9XbE1HwCyg8nlPm59Hy5z2/DgPoLKE43kWM+0gEuDY9q9YanUwsWXWeofIcpLSgu3Q/l66LkA4qnuCkifXCXd1EulUWrGUL6kV7O+fF9Zx6Zq6da8Xivjz9+2zqlJfuxEw5O/CWwrJ6FyXRmMEC2dLaK4TED2z8rdbaCuNQE5AVqrDAQpSkVsWUAKtQOb4f4DoWuOvMCt9Cyjo10uBwj8e7exR43Lh6CWEMSuVMRkP2dp/SPz7G9XyClrVut1ttpGuBcxSFtFsRRZnP2MarF1THi9QWvLXbNr+vzGSdn1gIGxeaJglZnqJVya4c2tRj1TgUeWaVD47D5vZ5PM9nbWWFm59+xNr6BlevXOHx40dcvngRN4wweos8Lbhw4TxIlwd3btFqt8AYglaLSZwR9hzi0YiLV19hd3ef48EQV0oO9/e58fqbICRPHj0iyzLCVoDvuRyPhrTbPaIownNdxuMRrShk7+AA13N58/orxMkEv9Xi6OCArc113vmV3+C9v/gL2p0WH/3sPf72H/wDbt/8jDyZ8NnNz/jN3/kdolanjL0dE7R6bG1v895Pf8r58xe4f+c23V6Prc0N7ty5jdaaMAgxRuN7AVGnzdraWkmWBdK1lnSgjp1vtzq4nmuts66L59mxDQPrUq6NwShNkiQ2T3wYlt8VOI5LUWQUWQHS5gNXRYYULtpYpY/jOEjHtSnGjC5zkBcgBK12iyzLcB2H0Lfj2GlHTCZj0iRjtLbCaDimtRIyHA5I04Q4GRMnMVmeIl23THcGwnXI8hQwyMCz6fKKHKMLhPRAQ56nyDJGWaGta7YvkY5AFJp0NLLp84zBc120soRyQjqWhb8kZSuKnKKwIRCFtm7oxtiQAF26g2ttQbAUAspc277nlUznGs/3MQKCILDWad8vnwVFoepY7kRrnNLF3/VcjAlq93G7RmxObwDhWBDquS7ScchSm27OWpklOBLXcwjKFF/a2FztRZ5bYB+GFoxnmV2jJWADm6LKgM0NrvMyHMS6uWut6HZ7aFPFolMSyVmPBykEvuczHA7AQCpTWq0WUdRCaf2FNtMK99qDoUasJ99wkoSycCCYaSz2jOluCmqn/3/OITd36tSCuGDu9G70aaFdFmHapjSF+VnQP3Nb/bVAGCsECSGQMqizFTiOi9OI+56MRvSPDkmTiG73Ap4TIiZPrEdI6jAcZRRqbPkgsDHdQRTgeiGuH2KKjPzofVSyi9x4BxltluM4TZpkh9fUIKgaBgmNEICpyUU0B8VM+2bmPk9vEwveuvOvfR6cTsdc4vshUaeLkxekw2OyJMaPrCJKCmkVBOWNFiubpe+xRnPN93zK1JwCzecIig2K7Rrw1rmnm3PBrodp30099ieC2mZ7mvwEjWbZb5t1zKqaZpQn9b3l+55RJM1WWgNjMztcy/5efO482J8C68ZANdZepSSw3y7K7A2gPNNvFq6cL3aNVu9jViEzU0dDMbMYYvHlCJovXJ4nXD+vOY3uNvVTS59RV7p475lLo64FD4vn3PbCI3umGxYH0NQfZ2fQqdW9SONOBJxzl1WT2UCW58TxGIDhaMhwOEJrxWAwtB55ZWicFJIwstlUJpOYLLVpRNvtjs10ErVKzpbcKvhVUcuJvu8TRS1c15nKOaUhRwg5c7QtdL98QVMyRNv26e6xZKLWG8hpg1VdtOzC58ye+cuXbHONrWb2XJq7UKcJxeETnGQEjguOgzYCldmUu8VoYPmVshSD5ZuifC8SKNIc4RRIA6p/jKs1GAjWtpCuVzZEzHnynLFfze/Lybv0kua6/QIaJgENWesFbnyJLfLsruaFKq0VwlqkpWU2LpRCaevaq1SZeqt0TQUsc7mprMI2VrVycS3KxSFLrZQQJSFUnlv24aKwVjPf5eMPfsHB0ZjL114lDENUbt1uh8M+o/6AJLPAxhK2ufhBQLvVxnUlvucTBAGOctHKIB1DK2qVArqdKHlRMCkmCIRN0eRYt9Ykji3QCUIcYdMguWW+36LIEMIhyzLyPMUoTeE6ONIlaLV5srdLnKQUz54wHk/orqyxsrnJ4LiPVgXjUcKtm5/xRqG5ePkK/f6QwWDIeDBge2eH0fExk/GENM8p0gmH+3ucv3iRJ48fMBkPuXT5Cp7n8vTxY7IsJc9s/vMgcMmEBmwqrkePn3D+3A4Hx0eMJiPyrGBru4UnBabI+ZM/+kM++/w2Rlnr4HAw4M233sDxQ954+23OX7zMf/g3/5pXX3+d/vERSTrhwb07XLp8mUIbup0RShXcvnmTG6+/SRC2KJRCGDgaH/Fs9xn3sMz4ruvS7fW4/sqrGGFT1KlClfPI5qSWSuG61opZ5IXNle44CCxRn0QikISRjxACpVW50Riksez4CEGeZ3iuX2tIlTY1436WpmRlmiy7s0AY+viuQ15kpKTWI8L3CVsBvV6HsO3TH3TIspw4njAej0nzFGMMaZGS64K8yPFCzxIFlH02UiKNRmUZSmW40hLB5YWNn/Yi11r5kwRdFLi+h0otGZgfhMiyL75n++s71tLrui6mdLt1HQ9deoy40iHP7BxGQJIkCAErK2sIYRVFCBtX7Yc+AhvuUWk58zy11mRd4Lm9WvlUHdzWIm/q9qRphnXv9gnDCNd1ydKMNEtpt9uoQmG0HWbP80ni2KaOk1abLaWkyDImcWyt6J5Plmc2f7fjkCVZqfm2gEMKqFL+JSoh8C15nOd6uJ5XgjpZh7OoMuVZtYG32h1MGa/kOuWeIR2M90U00laatQRe5bFcua/Wbqy2TAWg5Uf3glB/wkWzAG6xOcuaSDMOuXEiN/Dk0huXtU0s+ctKKRVYKIFY2QdhSuK1coyq9HZCWsWncJwy57cNAxgO+kwmI/I8o9vr0V7rwOABrjNG5wWjeERRjGhlBbqbIc0KhavxWy1cx8MTKfn4PkXWR66/jbvyKka6oJs6iFlLYM22bE9hZl7UCWMsFq6fBXG29gXVyNx91diV80iIcp0FoCwxoxO0qMg2DbOp72pZr3ynzfRiM+1fMu+qdi0lh5vr+Gzc8mwN9TObKKeBehaHcD5tzfJSKXkWmlQJdCcUM/MeRHXL3DX1Cpi5y76bhmqpImMrx1fMXLtQ2ex7Ns0fTdWg2T7SgNgz/bLrafr7tF0Le8pcXVP93+ysq8jn5j0HEI1+Nt9NZZn/ItvjX0Z53uQ54femEoPGJTPexJwOjeZqW7ji1Dl9RoA6W2GFpJZHa78k1pgtL9OuE6qZZ+MHGxdc5DnjyZgsy0iSmP5gQL/fJ80yxqMR3W6XKIpIksR622kLvNM0ZTga0u12wRjyvGD/4ADEHpPJhMuXLxP4geWOmYxRSpeeeiHtdguw4Xe+79PptHFdj263i+d59Ho9orBlvQcrBWFjg6zX3ZkG7zTgdlId84M1f+CfvvDE/OPPuE61UuT9PWSWIIS0wFrb0Md8MrHZmrRGF4WN6S6PdoGxVvAyvbQjPGvxdj3UeGTJbgUEa9vW2FUpiOsF1Ujx2Bzk+XOj7tx0vzzVg2XJntqs8LmqphPrPklD9/LlzMDb9VwEwhJple7fRclQLksX8yAILBu5tORMrnCtJbGM4bU5ubHWJ2ndWGUJZm1OXptWyubW9tDG5vh2S8vd8SjlV379BpUbWZallvDLcVld30BKH9f38QOfIPBLC3aONobBcARoHGlBziTLLKgKQ6tESBO0sQBDKwenFNw9z6sPRMeVSBzIDEpr8qxAiKJ0hVcI18bwep6DdAQg6XZWefjwLv0nu7R6K4RhyEGxj5CSlY1tLk0muF7AL37yY974+je4/pW3KJIJ61tbfP7JRxjpoBFEUcDK6hoHe/u0oi5HxwNu3bqDMYYr168Rj4YWBGU5UkAST3Acl+N+nyRJidOUSxcvWTfdVov9Z8+QUnBue4sPPrlpGeu1IU1SPv7scy5fv0Z23Of8xQv88Ht/TLvTYW1tgyAIcRzJzc8+Z1U6ZGmCMobJYIAqXSHW1tfwyk0QIPA9wjAqc7orsiKnUMq6G0ct2p2OdZ1OY7TWOIUgngxLa6pNm+aENgSh3epg0GAkGOtRkWWWO8Dx7DVKlTHO0i43a2110IUCpev5poqcIPBpRS2Up3GlS+HkeL5rU+YJKLRCSMuajzC02i06PYfBwKXVbVtgLSBJE5IiJStyhGMtUEWeMxoPaTltEJaNuzA2xrzaQXzXWqMdJKb8LwwiZNDGqMIqc3KbEivPMzzHRZW8CgJQRWFJATEkk7GNexfgB3Z9uZ51BTelwkgAuJYDwC0t0vFkQjoes7q6RqEKHNfmsxfCunC5rkeaJoRhhHQctNI23R9lrHYQ1NZuUyrZXN+zbOGqKDMH6HKvEETtMm4cgcoLkrwAjLW+Y6xXhR/ieBXhmcEPQpRWOMKurckktgRznuWScBwbLpImCapUSniBdbtPk5Q4n9iDNijDRsp0iFWbtdG1x80XKmIOXDVAeP1d8/qZzX4RnE1vmhPy513EZ6pcQg7WePjUErgoLNhD3DTqbzy1PHQra/5JAqkRlTN2s19UCIoqd7pxQAiJlhqB9X5xSu+UinhtNDgmmUwYHPfJ2216K6/hxE9x3SNwXPJ0zGgcU+QH6CIj7K5iTI4JW7hegB866GJMvvcT8sku3ta7CK+HqS0CU5OGabyMefll4a1UfDNUwIrpmIkmjLV/N0dk9v1Px2beNd2SQgYUQK4U8fEBvrtm1xKVgkeUkQCmlM2n8+15MtjC1JjPG7YM7C67vhmfPG38KfJk+aU0C0b6E1YJTehtGpc0VUJmrscL1vTq+iboEhWwFcyunGbM6BQILyjFqEDqlEndzPw6B/bn2rT4lsSSFzM/b8RC35Ys8xOLFHOW9ebeNL9PNebRy7pcz5QT5OyFsnRjadzb/G5+Q2W28mVjY5b8+GV0b6Ysx+Qng6OzaKFghkJh9lFTpclJKGR+Z/pC5ZTNpVJuKFUwHI1I4pjxZMxgMLCcPWnMaDhGOpJut4eQgixNGE8mFmMY8DzPKu5VQFFYS/d4MiEoOYDOnTuH73vESUK7VXLKhAF7+3sYqGXOw8MjVldXcF1LplwUygL3/T0mE5uqdm11lY3NDUvC3GrVRoVqOS5TnFYjWlnwX3x9nLZDLzujz4aol90xPxWtFTtGprH1lBQ+RgiKPCePJ+RJisozEBKV5SAFEkuULApAlgS2jotqGFXNZIxME4TrIFyXYGVrdq8SVWuWDFZTN8l0DkHjaD1T/5cRer5M+cvTNJ6d1byMZw7CsMy/W6CKAtd1KYqc8XiMlJJWqw1Ygcr1HILQWuzSNLWxmL4PaFRaMjm7LipN0UqRlu6svufbkXYkgR/gopGOQ4F1AU6SmDQe0e8fEo8tAZMnfBzXupJWuZGLPKuFKUdKoAQs0hLY+J4l2AJKUiYoSm1wXrI5e75nFQJ5UaYTs4vZKxem0cpau43BL91ujbHxo1HUYjQc0GqvcO7yVdIk5mjUJ4vHbO9cwPEsEdXdzz7i+utvMhkMmIyHSKHRKx3a7Rb9w2Pu33qG7/sU6hlFUbCysorrOrglo/qHv/wl/cGAf/iP/zFRFPLDP/0eCMnRcZ+jg0MLgoKQ3uoqn378MVtbW6xvbLDa63F4dGgVItgcy7rMZby7+4zVXg/f85GuT6fd5pOPPuDqjRsYIfB9yWTYZ2V9g+ODPe7fv8fK2hp3Pr9Ju92l0+uRJtYCL7CW5jRNkULili4oUlgQB4o0yxHG4EiJVoV1Uy7zdXuuXwLsCUWhatchpQryokCpAlVoDJ4V3EtvDKUtpZUpFMpYt3X7nkKUm1PkNuTBaGMVPVpbhYmyxHt+r0OeF0ziCdpoLobnS4WBxvdd4jgmyRM8zyMrIibxBEpvkDTL6Pf7+G6Z8kgKeu0OWWHZ/NEarRW+F5DlKaZQOEEEvi5zcTuWTdLVNs+1sG71EkEY2JAPVRR4rkcYhhilLIGakFgughzPt67cfuiUrO2BfSe+h+fZfOpSQrfbq1PSOY4sU7YVpGleZjHwCIIQIexBmisbqqEdhe8HdTowpwofEDb2Ks5tSq/Ktb4o86tborYQt0zvlucZQgocZb0RikIRBgCiJFIrieQ8H+MawDKTu4FPnud2zghRerDYvapQOTKXOEFA1GoR6JBCFWSjMWCIWmVYiRAUZRy4KtQX3lCbRVT/a5x6Zv4khFlJaobWnMZJ2YxmnX9Io5g58aBxxs3jnqrehcNwRmhtPLXqkJn9OPOs5u3Nh9XCSfVjKdBLAUYjPOulIIVTxn671vLteYy8PpPRgNFwgCoKVlYvEkZruKMH5EZT5C7jdAKDGK1y/FYXrXI838cL2jhuiChi1Og2RXGMs/51ROcKIGeMzYvjNmPyWA4+mwM7B7AE1PHSTYF3mQ//fOx0JRo7novwPIg10coGOH7Z2IaLcjltTHX4NJqyAFZnSnPezbe/cdVpVgaYztn5mxb6yuxY1e7eM0h4aTObSgm7XEzd7qk79Wx3lol309/F7BfViAsxnRNUHhtTiXDm/w0FlpirA0yjnpllPNPWJgCdzW3dhFJlW8pvDDR4ZRpXza31ZWUKzWYzq8+rLSolQuWaP+3Xl1NetqaFefycik76+axdef5lL4AIYGYvWXrLieD7hAecsjc1LzphaZ/t9lNrXvKd0WRZxmg8YjKZcHh4yGQy4ejwCMe1QDuJE0bjESsrKwz6fcYTm+Gk123T662gVEGaZozHI9I0YzQaMZnETOKYjfU1fD9gEk8oCo9Wu2WBt1KMx2OuXL1ClqbEcQxCcnhwSL9/xMbGOmtr6xwdHdHptGlFLaRwKIqcfr/Pcf8Y3/dYWVkjCHw21jfodnulXF+dzdNRnO/7IoHYWUZ3Zid4wZGeq2bJ3wvNqbY9rdCDAxyVgetjfN8aeVRClqQobb2RVZViWVhgrbIM4TgIx7Veg3mOznO8VgshJSpL7Ukw9hGejxt1cIPWXF+b7RKLXWvskV9emfrwnDiSJz5w2Sm6ZHN/gXJm4J1nGXlRWGZvxwEDbkn2lGVZmS+5ZFQuydGcMuVQZQXLspRC5TiOZy1OhUKI3FrOy7RiXhmHmpWAyGiNF3hsbW0zMV2iwCNJJhweHjAa9InTFG1EmRvWQ7qSdisiDAPiJLFWA8/FdYOZ9riOZWQuSrcIgaDQluCpImfIC/u5cmfWSlsLp9ZUQmRWWugrwC8AUeY2FgK6Kz2U1hZcGfC1oet4DAZ9VJ5z462vcufmZ9z+/CbrG5ucv3CRxw/vI58+I/B9eqsr5Erz+ee3uHTpMivrazx6/JT19TW0VowGA3b39rh0+QphGDIaDAh8D4V1+W/3eqxvbXF0eMhw0Gdra5OLly4ST8YcHh6yeW6bK1cucfPmLYpyEkVhQCsI+PXf/l1uf/YJRZExGuRcfeVVwiDk3p075FnO/Qf38R484MZrr7J6eMiNN95geDxg9+kThJSEYYgf+CitSZOUPM9RWtFqtclyTTyeoLUharWRJRmG63ikaWZT0flOyS4vrUcB1nouy1AFISBJJjiOY4nStCKexDV7d5EXOMLB830CPwQjLECXGke44AgsRBegNRKQrkshDa6WuL5NdYUwuJ5VvjiOS5Kl+L61uFvrfcFgNMD3PDSGJEvRrkO302Z1ZQWlLAu351ugKIQNZ9CFZej2pIMMvJIUzTAaDRDGrgW3dI2XBkbDvo1DjyJUXpSCvcYpQbplgBf2UBqPSqWSscowYWPdtW/otDs4ru2PdVs1RK0p03nlpu241kqeZmkZ++2glLZeIsLmy5al2zzCxoSnaUJRxpQXeUGhCoxSuEJYhvRSyNNKsXd4SBS1CAIfAxbAC0FLSLI8ZzAYEIahVSwYbdPEuY4lSNM21t1I6yXjCgu4wYL1JEkY9oesbqyXJHA+nnEZDYckSVrHr7uuV3rpWDD/smUxLnZ6yEyF64bAO3PeiAZYWBTkKowyL2g3BeWqDWbuEJ8ClllwYn+VDXfh5lE4FcQRonaVnQXgc/1vgqDZbjXgSrNTVoEC4AiJLoWWKgOG3Q+s0sf1fDzfZzIakKcp/eNjzOo64dobeN59ivEhOnWJ84TsOKaT5Ygix5RhDn7UwfPbNtwjP0bt/QUi2cNZexvjtCqdwMz7QmDj0ctBmxW5Sgg0HwTcAL7NdzKteV4+mpO+G8KGAMt/koxwHUlmBOP+Ee3WeaqQr3kJonpfzactQGJjZuZd05I7tdzOuqpLMfv77ANnezX9qgSfs01kSvY1d/+i1LrkOdPPs4qK6TjOKzBOq0og0MJyOzTjwcXMhYsS7dSbxIJvI8TCtdMwgtkqRHmf/UfMPmymweWz5sfFMH33JRZorsvmLrSMuHHGhX1B2F9i0Vsyp/9KypIFM1V/sDD3T6riyxXeGxVzhsoX9o0vqTwXIMBLQeoXbWBzChlLvDyJxwyG1n08jmMG/QHD0tt0d2+fXrdHEETWpbwVcXx8jOM4dDod4knM8XGfJE1phRHjeEI8jjk6PiIMI3orPdqdTimTwfr6OkmaMBwMmYwndLs98kKRpJYjSkoLmLe2tzk8OGA0GlMoReD5HB0dsrm+iQHL8ZPnjEdjlNbEkycEYcje3j7tts2MtLOzQ7vdtsS01Xn60gN3WjkBhb5Aqe6YnwHTMwCKZALJuA75MoUizzKySVxLF1qAKVQpC9jMUqYkqtNFgSi9H4VrQyuLJMWoAi+KUEmC7B+Rt1o4m5cQ0plvxWKjX6Tbpwz5yT+dArpf9CFfsJwZeI9GI4KS2ViXLpoYrI+/lIiKrEBad4WiyAGDo6e5g5U2IA2h79VK8jzPyxRDNh5cl4zHhVJIx8UrLeqOAFcoGxcrJL4fMJ5MiOOUKOoQRiHdbo8g8AgCa4Hzfc9atrQiTzNUSWhlJRGJKN03fc/mMQZsfmchieMxRaHw/Kpflh1daVUSZOUEQVCmTcprwCKFAGGZ0vM4QxtNt9tFG80oGNnNaRIT5xB5IWjD1Vdew4taZJOYn/3kp2yfP8fu0ydEUUiSpOzuHfK1d75KlmccH+xz6dJF8iwDVXDhyhV2d59xdHREGATcuvmE48GQr7/7LuFX2/zsxz8mTRN+9/d+j3v37mGAQf8Y15F8/Vd/lWdPnvKbv/M7DAYjm+5Nab72ta8ynoz4/OMPWFld55VXX6NQmgd376CzhNfeeovdx09599u/wf/0z/8HfvLjn/LqV15neHRIt9ejf3TAxuY2mZQlGZgPrijBlY3H1drmeRdYpcwknuD7Nse7dOT0PSHq3PHj8RhjrNdEHE+s4iPLcKVVurieJfeT2DR0aFCZZpSMSLzEuk8bSZ4WNrd4luG5DoUAP/TRRlEkilxlaKPQqUEbVRIBatqdqGQTn+B7lo3ZL1wKrVA6p9VqkRcF/eGAVhgycSc277WyIA9h3am0UkTtNYosYzAY2BjkQlnCOQHndy6U80wjpYPnusSTMZ1uF0c4FHlu3bOwGyBY4jCbz9tmFWi1bYolIa31vPIOMCU7aOVV4gibYi0KQtI0RUhBK2zjuR6pk4IQ1nXb8QhCC2A7rXZN7KbLNgaRBeGeZ8GSkILJZIJQkjzL7KElrLKt0+2C0fW71dqmGtPGKtoMxpIDen7t7u44Zbx/WpLCuR6Oa/uZ57YtRVEghGV/b7c7KL+oWdGNtq7uQRhBybrfW1ktlQyWNXUhAPQlylzEsP3/jGw7SzslaMi1ounouvxot2d+E2jMPV+IJd+WVy7cs0TQrgT6RbRwwjE029LlznhMTX01upz2E0QZt0/thi5Kxa1bZsjwfB/PD5iMBqTxhOOjA1rtDp3uDVy3jZnsUsSQJgV6lJNlB3TytHQ9B2FauEELL3QhiVGHH6GSA8Tmu4hoh/pQboBfU1tyTxrRWeBs37MF6/WIzFghT5fEF3CSUQg9QWUjjBFEvVUc16/5VWpP71r4XaZEmW/08vc4nV/VnBWn/n5S/U2wY5ZevfhtQzO1vKamcmgBTZnp9+XnM4xCfWWdOq5ejIuXT3UZzbUNAkklyCwfU9O8eeaXZndmvp7XawgLMqb5t0WzKQtr9aQdrFpyM3oipqSMJ4amnKYQednyItU1p8F0Yp0Cqp8/FvPVN9d8Y/Laf+a2wReFswtvX0yrX1ybZ6isWeYac9Isf5EqX6aoQjEaj9jf3+e4f0QSJzbFcJkpqF2m9dzZ2aHX7QLWu9Jmp7FcTVmacnB0SDxJ8H2ftbXVWpGfpCmTOKbdbrG6soKUkm63Q5Zar8I8z+mUvC0CyNKUILRyShAEIAyb62sUynq0xfGE4eMRd+7dpd1pc05u0+l2kYLa2y/LC4bDAXmRs7e3x8HBASsrPdY3rDu6JYatMpksczN/EYh32lv4YkqvmbCEcjJqo1HjPq4QIG2onjKaLEnQ2o6REE5tlBGOW+8D1mCa2ywuCoRnPTp1mZVGSCuTmqIMQz4+xO2s4rVWTu2haHZVzP277LdTypIT5ksqX16tLxDjbdMQ5UUOhlJg1sRJilKqZiB0HAfX86ybaVJY1mFhY3HDKMIYVVL+24VZFAVR2AJhrWCF0viOQyilzftb5BzvPWJv9xkf/uJzemsbXL64Ywm3XB/ft67eXuYxGg3RuoXjenTabbTWDAb9kvDJWvMm8djGb3jWsmpTVNmFX6jCsuYKhVY2r7TV1GR4nkehC1SuCIKQIPDLZPKiJnrKiwxH2rjUCvxorcjSjFbUtge1BmMTy9I/PuLgk0+5eGGH7fU1OtdvcOXqNY6PjzjKFUpLNs6dJ80sudT6+hqD/oCH9++RZgUXL57jwb0HFEqx3ungOC6D4wEbm9uM44QP3n8fhCQKW7TabdJkwsbmJtub6zzb3WM8HnHr9i02Nzd546036fW6dDsdHt6/x1/7G7+HyhXHR0c8vH+Pu3duc+3adbSM+OSjD5HSZe/HP+Dbv/Yt7t66zdVr13h8/y6PHz4gyRSdlXVa7Tae49HutBHYlGHjMgY3arex7NnG5nHPochszu0obNUKkEk6YTSCqNUCA2mSMBgNkMIyyAdhaF3XjcD3XGQYUKQFk8kEPwhxI4/ABBSq8kywll6bDzoDY+O5VV6QqdTG/QpLJDgZjZGuJIh8ikIiBPiBR8d0rHu70kxi6zURBYElwTOaVhiijbWgt1sRaZriYEnNArdtCd/Q5FKilcJgrdEYGxKhlMKRos4IoJVCttuo3HIo6KIgKK3DXquFEJZBPM8y4rFNodVqdQA7L13Xxfc75P8/5v77WZYky/PDPi5Cpbr6qXoluqqne1pM90zPYAVBGkACoC0AWxoIM5D8Q2lGoxnBJQByd3pWzuz09HR16aff1alCujt/OJ6ZkXnvffVKNHbDrOrlzQzh4eER4d9zvuf7bVtm11PJCNe1qHwWRaSKBwo7QCEe4XVUIs/ynMkwnm8n97Nkw0UrARCbrmVJmqZkeS50/FY8yYfDkURKI+C3VkTVvNdr26+2DaTWoIymqRuC9xLYyvO1sApIsCtNErTRVPE7a230b5fnlI79J5oSnQQM25YksXgfGAyEsbOYLxgMC6lZUnpNc/62yxpW7k7a2c4e7szrtifRvV93LY22Zmp3AIRv0tbVUe788RvtazsTupUZ7b8w+4fr1xPH32Qio9BCXFhbNioj19ikGUmWsZxds5wvWMyuaeqayd4J2d4Ay5cYo2gXCxZVRaCia8/JxyOU93Qd5IOcJBtifUdbvsa//P+iDn6K2fsRkPQYAD36cHjL6U+4pU/X57be1QaLhZ3fV9nFCJB810CQYHS1rAjpmGIwlklPD3GvlMTDGlRv+y+vlb/X53H3eNz9+7ZL96bl5v7UBvCFW8ZE79/t4/QHorr1o+xLbYNm1O0YfnfbsGrbbmV4f1e9yu81w6A/rZNrpVaRj7XtXryGYXNnbEDjbSe7UnnfXmV1iJUY2mrDrUlqbN82AN+c01ZXhHg+ajNON6GEHiJcIdHoAtA79e++fGvEestvX7evbzRZf/Nqm9+2A4dvs+ziB8U32fqOfW7dSt/2bt1ZvmFUoXOO6WzK1eUll5eXnJ6drsVwl4sFRTGIJaeWoshJUVR1Tb2qyfaOEDyffvIZWZ6xv7/PaDSibhqargMfKKuKum549913xbEkzmeur6+5uLjk8uoS7wKz2ZwkSbi+vmRv74C27Xj18hWTyR7FoODi/JzxeMJkIoJqDx89YDZb8PLVC6ZXV5zcO8G1nsPjQ3wM1nsvbMJkmHB5dUVZV8zmS0ajc/IsZ39/X8o+je512ne9Sb7D9dvZy9bS26VvaqhLYYVZREcqCmPDyqLN47sWlMH7jtC28rNWaG1xXYtJNCqAb1tc26Ktic9JYWO6WtHNp7TTS0w+itpfN+c16+dZ/71460l8t+X7eHx9X8tbA28Cazq4vFRCBAhSP2qjCNnKVy/LM8qyoqyqtTWXZIUFsIeoVuiiDZC1hqAUTVNhbRon5Z7lYsFnv/+YX//Vv+WrF5f8eSNZtvlCsseLxRxtJNu5opSGqHpoE0OWpDGSZajbhizWZnddi/MSlanKbi2k0MYBlA8GJGkWrZNihj/+I5N0LVnttgWisJxWNE1L09UkNonZNEPbtAJy8pSqShjv7ZFmOSjFxXngi69eoJUn+E8xCkajIScnxzRdx7MnT/FB8W/+zb9BKXjn8Xvce3if+bzEZhmDQcHJ8REhKKnFHo8wNmU8HDEaDHj47gecvn7Jk6++pFyWpHkh9Pi2YzGd8o/+0T/m7PSMB+8M+Oqzz7FGM5js8/Fvf8twOGJQFNx7+JjR/iHXF+c8e/qEPE249+gxl5eXXJye8vz5M85eveTdDz/ivQ/eJ8ly8mLIsqxRKaLMXtdr+mhd12gjkTKUokKCEWkm9kKLxZyubRmNJwyHA8qyoq0bOi8BnjzJUdqIZZkWNWsB0grtxRs+z3OUUutASKYlIysvLI2x4g1P8GgNdVuJ9VaeYJIsjkcPnliLLPXJwTkIARNrpgeDAVVb0TYds/msl6lVVFUV64075jMRvxMlbxFKq3RFF2n1aZbHuvZVNNhTl6UEuIwhSYaRPt9G6r2ovjdNTVPVEH3mi2FBYiwBqMqSrmsZRU9uYy0hintbq1kJMOkYfMrznLquAMUoMg6kJt/gvcF1jnK5FG2HqGeQJAndirWCCKR1rfiMr1TXtdJ0eNJU7hGCCM+JjViKjr7tbdXigyfL8rWoobWWru3Q0VLMOUeSCrMhBCjLUpQ3vaeJyu8gNiWu6zBR2K2LjAKl1FpYzhgds+WOpKf38K0W1X9P7FRQ7rxLZSK+mc2usk6rX7crRjf73z3e5mPv4OFWOH1js9vWuUEl/prf+/PPTWm62szbYQtlroTyYEM3XtXOKSXMKMEv8gIPOoBW4slqtDznrcUmKTbJWMyuqauKi/OG4XDMcO8j1PwZmbmiKxfMywV1UzNoW/ykIwugVcBkKdZkJLmibWrC+V/Tla8wR3+GSvdX3Yg88NUagMn32+e/niOsgdYKqvX6rEetVv31Y4+uHBD6nRqCo1mcol1JW81ZljPm1y0H7yTrbLRCb65Gz5e6D/S36hJXx1+DQ9UD76z7H5So86+uz/p6htvH8lY/rNbfBnOrUouwNTh6p0yvvOGWyec2MOr7yPeuxV0Ttdu+V9sf1u1VQA/o3hQqVLs7YCsjr3p7W43x7Zt1uxm3gDi180df5u3GSlvdsA3r+i0NvS/6/bi1k91z7F2Gb4g1v99lJy6wbsub0PIbHoL9ef2d4+ZGX93+97dZdpt929/rfv+6A6o3nOg3aM/N3d4iwRaHt/dSDvfq9DV1VXN9fc3zF88pFyXFcEBTN0znc2ySMsgL0jQhSzOWVSkJBqUoq4qu7Xj58uVac+bo8IhAoK5ryqpivliKGG/TiIVrWTKbzbj/4L7QxruOBw8eRL0px5MnT9jb32MymVBWJUmSMpvNUFozGovN8HJZMp/PYulrx4P7j6jrkvOLK1H59h337t0DFyLLckmSJgwHA2xiKcslWkNZVZydn1EUBQ8fPGBvbz+yCG/05Nt1vroxKr/hVbx797uzibacg2tiyawieGibDteKSHGIiQt5a8fQjoqfOwdKyixd3bAOSmodg4Qer8AkCYGA7xzt5TnJ+BA9nHDrcyzGK7fu691lM43YnJu6vXe/8T26G7f62hW/+zV6a+CdRvq2eBIHqVH1Piodi1rgqsYwRNVooYRKZizLcoBoC+Rpuy7WQStccITWR2shE/2IPShFXhS8/6Of89XLS/bvtXz4g3cxRjMYjKjLijQrsElGXhRCeY9exmIJpiLY8DErKXWs3ouqofeeoEP09BZV3SwKVwmY6PAhttGJH7IxQidvmpqVB7FkCVcWZKK87J3QZJx30eIsoetatFEcHOzTNi1GKxJrqJZC8aiqiqYqeX16wfXVjKzIefTe+zRVxWg44vT1K/Isoywbrq8u+N1vf4PrHJPxGKMDl6cvmF1PmeztMZ9do7Th9atnTCZ7WGP42Z/8gtn0gqurKYPxGNd1PP3yC5IsoasUx/dOmM/m3L9/n/OzM/IsZzpb8Lf//v/Jg4cPODw85I9+9BMWyyXWprzz+D0+/vvf8Ktf/Yq//Mtfc31+itEnPH/2nPd+8EOc0zREqrI1pHlOvahp2haUWDukaUYTmrUN1Ip63LY1wYtKtTFiN5QbyfJaY9ZjTZTlNdYYynKBUUYsuLRBBZn8BO9oXSsU6BDQWq51WoxiDR1oldF2kr0uyxqbmDiWNtRn51ycl8nDRpuNDoBNDEWRo1BrH2vvrJw3CRop0XDeU1YVVbWEAMPxkGExkqBCDAwsFk2kpHeUdcXh0fG6tqj0Xmg9RpNlKdaIuZq1Ikrovcd1DmOlFrxrRI3daIPRhsYIq0BrQ5pK/b1WWtquNvepWPuthAiFEZKPiphR7uTvohAv8pVbQUCE0pRk4bSWPqybRmq7Q8B7cMrLM8GmJNFTPNUGH0qsksBcXdfxfjHkeUbTNBK4MQZrU/GE7+LUQGuM1uRarzPl2hjKsiSxCekqKFY3lMslWZ5LvX0jbR0MBoQQqKvqWz9Ib06e1fZvd8+9dybaav0OVmrnJRV6maoNsroBDm5QmrfAfQ8M7DZkfdy73z5b+946xd5kX63XZP1WZUNr7Z/VGoDH6n/5w/cwoSYoHcehjWUHq9pvy/z6mnIxY3p1QTMYMB6/S5aNseYFuk5oqyWLZUPbXTFsOtTeAUk3wuUKazVJluNdhy+f455fo49+jh5/BPFeDGyLZN1QhL7ZBdy8+upWkCCX9+ZoUEBXz6GZ4buWuoN8dEiS3SNJVhRH1cOJu1BK9fpW3ZwmbIGr/rb9Qdr7S23/rvpDoIdXt84i7jf0z1uxqaXeuT3W+1+B/x00tNtNd5Y0yFFvR5phFfTZXmd3CiXWOvKNXgc3tmHI5i7aZqds4cEtWn+vdev4xrak2da+1faH7RKWfls399RqJ9vSAzeDaasxsr2O2uq/7bHyH2i55fn0VqB79fNbtv2bneJbrn3bM/KOrW9897Xz+fCGZnxbMBC2OuwG4wo5ZNu2vHz1kqdPn5AmGVUtddkKxfHJMWmWUpUVo9GQzjnmywXd9RUoKHIp+3TOsb+/D1oz2dvDdY7hcBhZtfIOtzaRssIs5V5xn7Z1nJ3LnLRpGgJQFAO8d8ymU8qyFCZlPqBuGgYDEfRqWrE61drw6vUrjLZcXV2CEg2doR9wfHzCw0ePqMuSV6ev+eyTT7FJwnKx5Ec/+hF5lglGCIGA5/ziAhudYV6+fMV8Pmdvb5+TkxPGo9E66bO5HrdcrPXXt12v7wd037a/rmvpFlPSECBiI48kLnxMRKEVONig4k1gXGmN9x1dU0MAk0bXJx/wyqNNgvMdoXYkeYFrG3S1pJtfYouRsLV2l933x1vcYnfe/n0Q/wdbvubh8xbL21PNbSJKyUki1lMEskyo4xuatY7qxaL6PRwMhLLaNFibiAKx0SQ2pQt+DWRWisllXQNCV6+qWlSKfeDg+IT//v/yfwWEen59fY0xhqIoKKsaZSyd66iqFmVF1CtLU7QROrJDasMlu56RZlLPmyapZOpr8SBOkiT6dCfiOdh1NE0rGTYvfuWJTaJIXIdR8lIeDIYYY/BRAKqpFphEsvzaGIL2aGOj5ZKA9jzPRVRiPMb7wGI+5/zslOm1pmk7Lq+uMMuGL796wXCQs7c35oMPP+T5k6ccP3xAcIHH733AcrHg7PQUrQ2/+du/wwe49/ARru04v7jk5PiIJEn49LPPmM5mZIll//CQum4oigGT4YigIEtTXnz+BfcfPGAwHDIZjzk9fcXV5QVZlvObv/lb/ov/+r/hb//mr3n8/gc8//i3/OIXv+AnP/0ZDx6/xwevz3jx1eckiUQvpxdndMHigvSPtQl2WeJCYDQcY4whSRKp1daKgoLOWjKFZJOzPFqDdeAUaLUGdomVyKIxBmUMbdtGUT9LVdUonWCtKJVro2hcje88WgVQAeUUdduSJqLQr7TCWAnaKK3oqg5tpP651WIz5YOn6xoRFytyjLXR2zAGX1Ske3fygHbOkaWSFW7blrosSdKMPM+pmor9/UN89DpXRjNKhizLBcF7Dvf2aYcSDFop6AtjxEoGnyjO5hzBe3Q8trWWPBf1bt85oeAPhTa1KqNYBbayLBMLj2ZVqx1om0qywWlKYi1VXWG1TEK7rhUmS5GR5fm6bt2srM3SVdAkkKaJBBmco65atBFmg4ovO2NEt6FtW+bzGYNiiE1sPM801muLhoIIwAW8j3ZjzjObXkudeCKTZOc72rrezNljJp1AzKBnUbG+4+rykrH3DIbDyMCRl734yH+HjHfozwp3f2ODUFZ/b83MdkK5W2nF3nYrMafV+iuceuPNFbY+KXZbdfeb6Wuro9Zt3wGTNyaavSP3drlSY96CgmETmRdQJy9npUAHD8rEYI6O5SnR83sFwK1luZhTlyXeOUbjPYrJALt8gbEz6uWcpq3prha4pmY4npCOD0mzoTBcbIrGotoSd/av8eUp+ugXYMeIp3J/IhRuzHvfapp0C4jYfYFvwGpLKE9x1TWL2RXnr17TmiE/+OVPoyPEzX2vgwTcvIbrIMZWY25p9W0ndQfIWQM6tbNBBIt3Arf+oW89nrq53tZKb4umtoHuBkr0UGkcm+tM//qfTQN3QXeIwYM+ANwA2Zt8kdVlXtd7h9Wa/S7Y7ghF/3ptWrB91+y2dGv13t76a2+3aetxdCOA8Gb2y/8qy1Y0ZOe3Nw2DN+HS3U3vOMZ3n1pv717d+OP7WW6Gft5w/Dv3If9/09UOIXB1fc2rVy85OzsVx5m2Ic8KbLJkP01xQTQTmqZhPl9grOHs7CzalmqyNGexWPDq5StGPxuxv7+HMYbT01PRdwmeECTgXxQGbY5IbML19JpyWZEklutK3v2T8Zj5fM7V1ZRnz57x4P4DTk5OyPOUNMuFHh0CBwcH8p4whvv3HwCBosgjUPc8e/acpmk4OjxkUBS8//57NHXLsqx4Wj/j7377Wz786AecnZ6jjSKxlv39AybjMXUUcauqisXiOdPpNePRmNF4xNHBIaPRCmjeCJfd/Li+UN/vPbfNUAp0dQlNBa4RKnk2oHMB37WiFxQCvm3lX+/xrsXXDaiYDI1zJJOl+M7TRV0gFd1IXCvluyiFB3zTYnNoptckkyNsMeL7uLvuAt1vXr7mbvjaJt3x3vwWyzegmsd6UW0pioSm1lFd0FIuVxSSgOs6vPMYI5lioX8mpJHmKRkDHymsNtZ8Cu21yHPx2GWTYa+ris51pDZBKU3VCS08S1OquG1VlWilSbOM1KZYbWhdR1dVGGvIspQsUtAFDNSiFq00aSJibG3bCWhTYgWmFGKdFoIAKi0UX2sNSiVoJwBoBTwkYhQi5T6QpRlZmlE3NR7JftvEcnh4RNPW1FVNluWU5ZLgIS8y2q6lrEr2Dg8YjsdUVYWyCbPrK07PLymKjMODfZSH46NDZvM5B/v7GKV49O57fPr7j3n48D7L5YKryyvqckHXTdjf3+eDD94ThXBjeP7sOcNRwvR6Rtc2HB8fkmQZH/7wQybjPTrvuDg9ZT6TCesXX3xO2zS8fPIUo2FQFIwn+6Asr169Yjm75s/+7JeUsyu++OxTfvanf8rs+pLx/j2W86VQwqP4nNGWoAJluaRcLEjzjMwKy6CpKlzXUVVLVJx8r/yhxdbMkGUjQOjYg3wAWqYqGtEESBKhiJdlTY2Mi6A8eZpjtGRag2pIggblcL6jLhvyQRYDJYrBULzd67oSn/nI7hBgKMDBdS5mr0VArWtbugBJYtGI6FtiRMfAa8f+wT5K6SgSlUTBs5amEis6myTkWSLU9qh/4J2Plg5SM+06x3gstTLC4mioq0bUymM9uNaaVGd0SnQBBoVEntu2wVpLluVrAQ3nPePROFL+pQa/qmus65i3HePJhIDc0wcHBzRNi9EWH4U2imKwthArikLu17qKNl2GgKMYFFH0rpOJaZzkZmkqWe22o0s70iwljarvWZaR5xkheBZ+KceNGX1nPPmgQCsds+shgnMB2VabNTU5K2KNeFNhjTx/JpMJATYMFmvWVoeTyeRbP0hXpSorqvVWTmw3cx1n7UJb3vj/7uyx92/vhaF6wOrWF8UGFAR6lkc76uV9sNA7iRtHX03d161Qq8ne6khh3a7tifp2JnE7U3+zrf2X8Ursaa2wrYK4D8TgjQgGiviasRZtE2yaspzPqMsl11dXdOMJw8mHmOVLUq3pqgVto7ieNTh3RdGU+MkhcECaF+g0RycJqlvi55/j6wvU8Z9hBu/g19cqtu4GCIUVxX8bSIWbfdzr61tZBwS65SXBlbigsPmQfO8Qow/Ii+G65ELGj6x/wzl6dzLHhkp+W7nAbdPCTWK6Vxaw2m2flcFmpG+ttNs/q3Ndx2x22nLbdjtAaHelze7Uzvja0VVYn9DmWFt7UtttX0HarbthPcx3gg0bTnrcLo6PPlVXhU1crneE25klsbe2RM0Ctwkqrksb+v0U2Vhq/VnW27oz1ebDdv34KgKmel21um5fg2b/11i+xfG/yxT/O8GDNz2iv27TtwIAsqzGWCCOmW970N5m288Deb++ePmcZ8+fixDuconRFptY8c5OU66ur0nThMGgYG9vD2MsznUMR6IP8+jBIwn+V5Z33nmMcx2L+ZJpdNrJswznHWVVkkankTwvUEozHA5joN5xfn4ekwVTptMpr1+9YjQSNfPxYCAMXK05u7rkyZMn7O8dkOc5TZz/iDf4AO/h4PCQhw/vx3mWYTpfkCYJzjkGgwHvv/8e89mMZ89e8Omnn/H43cfQdbRtx8X5BQeHB+tkwtHhofiUz6ZMZ1POz885PDjk+PiY4XAkJXa3X8I/7NK7mCFAO7+GriVEZqKI2iLzs+hAE7wkdILzhK5DpwnBBULn15RyX7c09ZKsGBECdE2FTRJAEbRkwpVSmCwn+I7Q1LhqicmHm+dTr42rx863Ps2doX/z3n0DYP5Gx/1+LtxbA++VfY/3Du+EdjCfz8UjN6qdr5SFV1QL5zzDwVAm0OVSwIFSUpsab4Q0SQRkKRFgCyFgdCIKxUgG0iKZdO+9TL5TR1UuI7UkJ8TflVIsy4XQZleWNMaglWG5XGKtFQqKEusDjyfUMonzwUcBJlFVtzah7ZqYwbWSMe86GkQEzlhLnuWUVUlXLtdef0YZTF5EobiOpmloGhGycp2jU+I7nKYJbdtFv+YAteLo+Ig0TVkultjUcnlxwWg8YToacnlxyXK54MXLM7568gxjNF3ToBQ0Tc3nX3yJMYa2aTg7uxDF58GAq6sZZd2QFwO+/Opj8QPvOvYODjm6d48iz3n14hmT8Zi2c4RxYHp1TdCa2fQqWincp1wuefLVZzjX8dVnv+fHP/0ZX375Ob/883/A73/z7/js0884PTtnWZb89jf/XuqzJ0dUZQVcSfay6MQmzBWxFjfgFh1VWUpWMsgDMI32Qavanq5zDIZDisEQ7GbS1rkOPLFcIT5Y6il0Bh9SlE5ofSMe3XiC8iSJomscTesxVuGDwxiNd4EmNOsJapqmFIOCLEupqgq6gNUiALZcLmU+Fa+3dy5SwTuJ8KU5RZbHceZjjXSAKJhmlaKu5Jyz0YjFYoFBkQ9HKKCNCpHzco5CbLbEKm4m9a2xDt5H4Ox8R7WsQBNtvyQ6qQKSRQ8xgBFEi8C5bh0U61yH9gLu1xR6pcnyPK7ryPNsXUqxUpy30carcx6jpZ49TRJRoY6CZ6tgwOq5UVVSu6UqyawrpTA2wYcV1cnjnaOqKxFgtOJKUJaleJRHZwFro4dkcNSlPPCTWArQdS1106wt0NLotuC8J0G8xhXyrPHB05ZNVHO/hQL1TZZdqnVELivLqe0a7tUm2yB4NXvvv0S+tub6Bnbbjequ4cDm1xVWCLtA+JZlDVh68EYRM+/97W9qia7BVZyzr3/t988botBKrSaUxLKPHvhWm+f7ij2TpCmLmaVcLFjMprhuwGj8Dmk6xqonGJPQlUuWy5q2rXDuHFfXhP1jrAOTZthsBHWJb6/g9F/gxj9CH/wEpeUdt5M6vnEtd84g/v+OPt6NeQRw7ZJ2/oJ2ecXi4hUvX7xGj+7xwc//ODpCqNWF2wa9qxrv3uC5mfm+vT193HXzy+31NyB6W4H/zUv/RLf3qbYo5Zsgzm5bbj1GuLsN/etxe76pFwW4ZS0pfOj18UqETSlWImXQ879Wq7OKYSnVPw+1Oa3e+e8G0FbK5bJJ/4xu74BVkKGfl++f9/p63z00b47b3mND9R8u32VW/G2XWw75jZLG30OGeSvoeMuubgPn6+9uexR/L0s/0HYHy+J7Os5iseCTT37P9fQaHzzn5xfghfnWzbpYgucZj0a0bcd8NgdgvpihUBilOb73gLPzM8qy5OT4hOFoSF3VvHj5nPF4zGw2Yz5frOeEJibRVuV9F5eXNE1D17UUeYFNDK9evmY6uyZJM5I8ZTKecHR8RJZlnJ+fU5YV+/sHeB948eoVeZ4xnoxF8Dh4JBjmWCxa5vMFDx7dXwNRrTVXV1fM5nP29/f5wYcfkucZn33+BfPplMn+PsdHB6SZJA+MMQQkkO+jYHTpPF/Nv+LFyxc8evQOx0dHDAYDtFo5IazemL1nQB+J3nXJV+u+9SLPagJ0bU0zm5K1lQDv+D7xwePaRrLZQUqF8YG1IJAP69Lf0DnatqKrakmouC466SSbIJ7eZmSiFaquaK7PSUb7qCT9dmO010c37rt4ml/fN9/XTfjd7rK3Bt5aaUIQa6K2bSJolMmqKB3LwHMui1QDYl2mIQSNBbwSZWilJUqkUELN9azrDBJr403WgVIMioKuczRtI1muTqzKhDKrhVYb3DobvRKzSk1KkgloBiXUUi2Z8hXoFzuqdv3qSiLYbpuGohBaiQDxaFGVZxACdVRxX9HLl/MlNrGilm70Rok6ZvIJkNiMNLGxxsXTNi2BwGg8pmnaSIWNACHalh0dHTMYiI3CZDyRzFwIXF5ccnF5QdtBWZXUlfgWd13L5dVUGAGZUIIfvvOQ99//gMFwzKN33421sR3WaP7dv/0bfvTjHzGbzbAmIS8GmCRjNBnz6e9/T/ASDTs4OOLk/n0++fhjfvmnv+T58+f83W/+PcZY/ud/9j8yHhWUyyX3Hr3DO4/uM5/NOLz/kNPTc4LS64fR1eWFXNPRiCIvRMxKQ2qF3TCeTCQbrCwr1kBQkAWZoK9s6kKAru0YjccUkVoNgNLkgwO87zjKPV2wdN4yGcCidDR1h1HCnNBGQZDx5o3UrrgIoHX0EnZOrMSM1ngQanW0nXOdIx/k5HlB61p850VpPATS1AKapqkx1ght3Vickwx5mgoQ7dqWJE3Z39sTf+llidbRk95a9vb2aNtaGCRKMxgN19ntYjCQAIUV+ng9rJheT9FGk9hEyiY6oVvlmaist6tz8dJfEq3NsFY87YMXWraooAvAVUBVVWvRRB3v9SzPsV1HQJTYq6pkWZYMioI20uPTLKHrXMxyyyTRxkh427UMRyMGIeA6x3K5kOtNILWWrnN0sVY9TdIIzh2+9RKRRUoSBsOhPMhin6Xx3lnVbyul8F6E2KSExBO8p6kbmqZZC+S54NF8F1XzPkzeZKu2a1o3IDwobrLKbnkHqxs/7E60QgSkm/2v1t1kw3aRzCYXS1BbtcB3nduKYivH6wHpDfLc2vOmT1Y/qfXPAjDWkPFG83ZrZrcbJvfuKkCktcFoi7EJSZLF/64p5zOqckHb1gxHEwaHP4LrJ6TmmracUrVL/LSjrc9xbUl2cI/B8AAVEnSWo1rwXY2/Etsxe/wryPYJO1dH+mDnmoT4v5uN3+4ftQ2WvKuppi/wQdG2DpNNCGnF6OQDhqMJxshzZXOM/i43fR7642D7kFvbyvF759O7oHeNhtD7//b+75j1bKUytte5CWZi/XXYEVnrgdfNRqH3c2/9wDbD5I7J7IYlcHtoK/R+WwHm9Wn2ttitEFdbn7f3d3PuvL4L19nKbz6de1No7i1morsXYau/1O3r/sewvAlU94fN1y1f0z1vM03vg/P1c3Hn92+yv5vn9ibY3/v2tp1/2+BDCFxeX/HFl18wvbri9dkpR4eHDKNbUFlWDHJhuT1//kyC8cbgEVbbxcUFjx4+JEnTmHyBR++8w8H+PovFgtenrzk8OCTLMppWAPxwOGCxWLBcLBmMhvIeb1vmsxld51jGZFuaPcRYw4P79/FAYhLyWFteLpcoBY/feRxPQyxhq7LhanrF+fk5iU3I85SXL19SFAOOjg5JbULXinBukmWMxyOWyyXLxQKjDQcHh/zIppydn3FxccHDB/e5OLugGA5YliXX19eAiMYOhkP29/YInSQjz87OuL6+ZjQa8uD+AwaDQcz67oR1vkNwq0fIugFIV0uzmIFvZH2E4eg6J4zBriV08htR8FrF565rGrEIAwHTIZDkoqPkXYfzXhxvsgISje86tLEoq0BrunKJCgFfV3TNci2A22/j1zPsV/MORbilm/r33+3LdwXcb76Rvult9vYZb+dEAAsRWKurmjTLhOLpN9liH2uhtdJrZe/OOQgGrQLOO6rlEpsmsV5N4UMnyspR3di5DptkQFgrH3edAOzgg9RsJCmj8YSm7UjSnCTLJLPpA8GLr7FQhB3W2OjnBygV/aMRyux6khyom5qmkcHXdR113ZFmGQrJrrQxIJAkqfhxlyUKjUkMSZrKQAxeQLk2ECTDmFgbo0EaY9R64uiiCX2SiMJy5xyj0YiqKmmaRujMnWMxn7EYjgTkL5cx82go6yVN2TBfzMXvsCx5cP8+9+7f5+TkCJukTK+v+fyzz/j//L//GeVyyXA0ZDQaRj9ExfOnTxnv7VE3LY/ePeb1i5fMFzOKYkDTtnR1zXw+5+DggOFwwOx6SpoXpJGt0DYVD++f8G/+1b/mwz/6Ib/77W84PDjg8uKCqkZuvFYoR8FL1KxpaqHvty3D4RhPEJX1xZzcF+RZEbUCOlA6Zj8NioDrPD6IDkBdVlID7iXrPxyOwIp3ZFl17A1alhRYAlYrPIYkMShtUZ1cfx1Vr5vWiVCZlvERvMco8YTURq+zsqFp8YIe8M4TtMOgaF1Lmg7Ejs6Jr/fKai4xCY7oI51kdG3DaDikrqo1M2PVP0or2rajjlnhpgkxY+swSPsEXEj/NbWToE8mEV3XuphpVmR5Rts0VHWk3Ae5n7I0wxPEoi0+rpSSbPlGuE7ua8n8DxjpUQx6Oa6vrykGBYNiSCDQNI1YfWW5jJkowDgYio1cXddrID9fzEiTlLoSmvlgOCRJLEoN1oE3rTRZZkWgsKolCJOJw4C1lqqshEViLCoKO5bLpdyragXs1Nr3WxvJlDsndoDGrmr2W0yjcUbKRFxXvu3j8JZlMwnaqri77S2o4nR7NaNf/7T6HLYEktZ7XNn7rF4xW+B68/K99SW03qQvxhY2+7hr2Wmeupliv32DO5b1sXdXuzH/6MGxFVVXrSjTEFZBzVUG3Oj4DtECwlNRPS8XC6ZXF3TDMaO9H0D1GqsUrklo6hJfe1xYkrdPCQcNxeQA22XYrMCYDO1aXPmS9sX/gj78GXryAaC3E9+q14MhrLOfqzPe7he11d9rpnIIdOU5XXVFefWaxXTK2dWSwclHvPP+D0mzfKucYUP17ue8wuYa9QHZLX3dB7bbLb0DbO1ESLYCTQpWpUG32altsS22f7pxOBU7Rd6X/VG8GfO7KHZ3PKntP+LWt43yVcBpc5wVY2D7PukB5R4NW85293z79O1NRrzfB/2rs0qM06Pvr/a8Dem3Tqf3181A3G1r7X4Rtu61neOoVQ/sgMbvOnf9HpbvpQn9Mb01vrfv1bc51ts8ClfP8tvG+80G7X5317p/uCV4z8XFBV89fUIXxdRWLLyqqqjrFqXgcnotPtp1i/OBR48eApIUOTo6BqBcLtk/2GPcyfx1Pp+zWC45ObknIsttx+GR6N20bSfCaollOp3x4vlzDo8OmeztsVgu2ds/AAVdKzXZNkm4vLxkOptxfe0oq4rjoyMODw+jCPKKyZpBgJEb4YPj7OyM5VJzeHjIZG/CoCjkfgiesmo4uziPwfogCS0ncyrnHEeHR0yn1/zu979nb7IHStG1DdYkDEcDmi5lMByhtMF1NUEFZrMp1lrqumI2m3FyfMLJyUlUQX/L6/k1g7H/lLwNgDrXUl6dQ9MSrIa2xnctLilwVYlvG4hZah88oXPr6Unwga5tMLEEAA9EG9zgJUOujQWNqOEH8KEFlWATBUrm2L5tCHVNGGzbI77tkFY7/966vEWs8ZstdzXutqfz2y9vDbwD4jVsjF6/kCQCH1guF6RpJhZdzpEXBYQQs7sdVVmRZunak9cas25zYi0+KhlK1jqJMverWiXpxTRNqesKhWTYfSxcKooBi8VyPeKsFSXl6XRKAAaDIk6qnahQN6KUPMgLXKRPeO/XVJYszURMLWa1CSFaDnU0dbW2RbPWUkeqbjEYYrShaxuCE/uDrm2lxnYwEBprnJA1tYD3taBT06BjZh3v8QTJojatgKwiZzAoGI1LjDFcXlwyHI44OD6mriqquuLq/JKmFVr2n/zJz7g4P+f3H3/M69enLBaLSNluJFhwesbFxSVf8ZTEWh49esifntznR3/8Ey7Ozri8vEBpxdnr1xR5xny+YP/ggGfPnmKNZb5YYLQCo0Woam/Ck6+eUtctL1684o9//kuywYBPPv4MJWX/OOeELqw1CkWW+vX1btuaYiD0H7Fds1wuSgbDIU1UtxzkRayfl4ADQcUMtbwMaie1+VW1JM9TBonn6GDERC8YBmFOTAY5bbPAqBKSggZDG60RUtNw2RgwBXVbxaCN2E81sZ1aqUgpkgyvRuqbvHMkiRVLs06s5bQW0b1ikNNGNoNvPUqLD/igyHHBUxSDaKslWdjWe3y0sQhJQtM0WGMkOOEdTWQ8uE48yfEShGiaWuy/AOc7OicZYOec0NSNjZluz7JcxqCYCBImabrOAgM0TSuK7krHF06g65o1w8Naw2g0EjCbCDtlJRKYJCnWGrp4n5cxSJRl0rY0S0TNFEVZLrFaKONt6+Wedg4fJBChokOBjeB/rWLftnINbEogRME4yXx2XUtwHmNt1Gfw8Zkgj7llWbJYzhkUA0xkx6wdF0Kgbdpv/SDdqvXcxVzrP+Ise+33y1b0tg9rbsOm22ve1gR1Y431PjeFnZvf1OZZvjuB36qbXYOGbUBxE1z2977K4G2/oDaWYqu/49q7mKbX+o1q9yZIpOLLXClwURRRaS0ChFaCuqt/l/MZi/lMrPXGR2QHA+zsKWBw1Zx5WdO0Dd69wnU1+egA29bko30wmfiPNnPc2b/Fl6+xhz9DJeN1Y7dBy51E7luWDQzrqiua+Rld50lHRziVUp094/GDd8mLYXzX9gBBBAphJ2myOdyGgrw9mFYRnRXovjlmeuGROF5X16h/phvguPnzpshT6K+7buj2eNuqI16tcQPl9Ub0Ldn1dc/0z3PTrLvumJ1/WTPUNu1lq093/75tv7fRvVeNWfXprVv2nwVhpXGw80NvX/EAcShsenPTw2rT5zv72eregAQ5goqEik3/7nTnd1+2HzLffvkeJ9e3Ye+7GPi3bbQ1xu5o1y6kvn212wB4/9dbQ2NvXtavpe1n+V2L954nT5/w+edfUNVi41o3Lffv3+P0/JxXL1+xt78Xk3DCYBsfjrCJ5fmz5xwcHLC3v0ee5bRdi9bDqI1DtOKSAN1yuUTHpFPT1BgtIrkeooBzRxUFU9u25d7JCWVdEZzMr5bLBdXVteg7acNgNKJzYkUaQmA0HvHVV084ff2a+VwSSePJnrjjFAPuHR9jjOH68opnT59RlctYbiYsXJskWGMYDAYiWlvkDAcDXOc4Oj7CdR0vnr/g9evXVFXJ0fERjx48pBgOCaFjWYrvdVM3sexW5opVWXF9fc319Jqjo2MJINgVDHszatwdHVtr94I8m783L4Z6MacrlyR4QjA4pcCIGG5XLXFtjfLiAAQRUOuA8jJv1NaCkvml8tA1LTq1sREaZXRkyAqLNcmH4B3tck6aS1lh6Fra5TXJ3iHK9ERC3wbD3uaGcVev3dGN3w0qf69o/hvYiUXfXqUlUwuss42SaVJSf+kDibGUdUWz3NTMrrJnWgsNPUsztFJCcQV8cCRGBrv464pNVNu1QMA51hTc5XIpgM578cOOLxutFGmaCmU2FYuxyWRC2zbUbRNVyEUsLRBEYT1NpE5Da4wRmyKtFDbLRCjOe5qmEpV9LYrpK0s0ow0k0DYNTfBkqdz0qRVKqwpiM5ZGcGONodNyzBACiU4JrYhgBWtkWyQ7p42mrRsgkKYZg+GA2WzO/sEBo/GIxXLJYrFkNp1S5AVtKxny09MLri6v0Dbl0TvvslwuWCwXAgBj5G4VSHj46CE/+tGP6JxnuSz5l3/1a85Ozzg6OuRqes3h3t667vzP/uIvuLq4YDGfcXLygKoWWo81luvFkvvvvMtsseDzz5/SNKIybm2KjiF917aoVDJRdV3Rdi2DoQilzaZTisEgilBYRuNizZpIE1GUb8oGHxx5VtC0UldzdDCisI5lacjyAYk1PDgaMLRLVDNFu5pib4y3Qwie+vqCi9OXKFczGuyTj/bouhpbDMmbKWVyj1nIaT2YeM2yTFTwxZ7OsiyXGCxpnpNHOrqO7AXnBUAaa+g6FydDAe8d8/mU1CYUwwKbZiQxGmuNEXs1RMyvXEaV98GApqrJ8pw0qnJ3bUteFHjvWC6WGCOskOFwuGZQyDhLCHhcFGRL01To1M7FQIFYQIhoW4OxluFohLEW13WUyzJmoVWs79bip1k3krlOrCj7ao1NLL6RDHdTV6TJiMFgQJomVGW1jhQnWok2grZUtdh8yQtHxFqaSmq181xe2KGDNBWhwq7rpAY9MhtWXt9pKpZSCjn34D1JVmCMTFqbZbsRGAKyJMVM9sS5wKxE1SBNJePtw3d/LG/RoPo4AxXppFska3bpZjub7JYUb60D9PH+FsAIvX9vxWXxjz4Vfr1eWEGvGPEOq6zdan8rOL09EdwFoOvzEyQl6+/WSMfs7E4isfdB7wDCgFJ+BcEBjdEi2qmVplsFYqzFpGn0/E5YzKbUVUXbNgzHE0aHPyJJnuLsa9pqTtM1UXjtjLZtKcb7oDW2mJCYDJMn6K7Fzz7H1eeoo19ihu8Q+jZo63Pqn8TdL+yVtknXLOkWLynnp1y+ekFdN6hswqMf/gmHR/fEPg/FKt2/vrZhc4S+dtg2etgZQDciPZudhN5OboCIsAKRPXC8fTJb666/3gHm8v9ew7c27I2ZeKjQ/6k/6HuZ6a2BG5FQ2Oxus0mvbW+chG0da/tM33bydmt33AHm+iNlt42bCedts8ndlqj1fbwdBNl05vq50t9esf3vf2RLgDvLct603Hmt7ti2X+DwTZavXb+3wlu9Yb6jsvVt7XkbyO6cWHZdXl5S1xW+c2hrePzOO5RlhQ+ee/dPmIwndHF+Zq0I5n715Cvm8wWTvQlFLozFZbng8uJKvLZjGd98IcK/eZ6JJsdiySeffMpoNOT+/fvUdYOxhvFoxA9/+EPGozFJYlmWJXVZsVgsGA5H0c1C5mQPHzzAJJbj42OKrEBrqKuaqixRWnPv/kOUVgzygtY5Dg8PGI+EjdfU1XrelqYpSWLJ80ISGnVNkqbUVUmRZ2RpyqyacnU9papKHjx8yIcffcjV1RVPvvqKr776ig8//AHXdSWuKkq0nEBYAPNqweHRETaxXF9PqeuaxXzOvfv3GA6GW8/XO96qd17nm0+pzbbBe6rZ1bqGW57AkhzqOodrKgHdToSxiaWW3nlcV9M2NUpbvO8wNhVLsq7FmphQdA6lEkxiMTFoghOrZpNmBKklFpp6XRO6DkyyfQbfw7Pn5pvk+1jebk/f9HhvDbw77/CNQ7UChr3zVNHLbTgg2mWZqIwsRfXWRuqfsVHNPKyFpqq6kixxJ767Te1oVYMxMuhXNZvee6q6RMUs3coDvK5LAapZKlm+zpEXuUzmlSJNMrQWy4MVXThJUsqypNEtzoslmo3K0ysq4yozKDXsKtJfUxZLqfVYZVWc8yzKBYlNYmZesmdWSf1u8GKF1uKijZl4ODdtS5alAkArUVtOYm2wTwN1XQMwLIY0JgFETbusSsaTMd4FqroUX3DnSJNDrE1o6ioGDTSXlxdS76sVs9mctu0oq5LrqyuMVgyHQz74wQ9YLhd8+fkX/Omf/zkff/w7njx5wnJZcnl1BQSuL6/XgYpBkYNSaG148fJM1LOzjOsnz9Yq2yFOCnVUPqyrUrKPqfS7bQWgGWvxTY3WmtFkDMBK7VoE1nT0XxehrTRJ44MgUFZLEdiwmtxf4pYV7x4fcTBR5JlFhxLfLanKc4Ky+LZcW3a5rkGHDpsX+Lamnl0x2D+hKpfo0JAunnB/cECthrR6yLKzBARcBw91V69t8dIgJQ0QKMuSLM3JizROboRu2DaNjLPgOdjfZzabUs6XBBdi8IUYmMkw2lA3NUkigNVYIwC+Fe/3EMR2T8oYRElfKxGcms1mZGmGTRImaSqiaJ3YfQXvyNIM573UxVoRKUyylFUtqFjkNehEoRIb6+wls1jVldR2E6noVtopXuFi7yQZaanzbpsm0uC7SPGWIFPdiC6E9y4yGwzzaSP3nYa2aUU8KmoktHVNU1eYKFoYnGe+mEMIpHmOjTZy8/k82hFKnzvX0bkQ2TOW+WxOHe378jxHW6kjDyFgYh8tZnNQCCXtOy4b6LCZ1K5fp6pvsxR26J43X5v9l+7q/Hob7Dzsw40Xz0Y5+vZ9rtPMbF7cKxDXl4/aALhNblzF56WMIakT385o9lqiVu1T60llP9e6gvH9DOOa8qv6bY6SVGrVwz2gqDTgsbE0RRmDNhatV5ZjKYvpNWW5YH59hWtHjMbvkiZDTPmKZjGjXc6YLx1tOyM0LapxtOUSRgfoNEMbjTIpvp3iX/8l3d6PsAd/DDrbiiesuuu2KZPqXT+A4B3t4jVtOcN3jqOHH/Dk6XOaLuODxx+QF4OYJdq+Tv2rGXr73QWcmxHZA6NrJLO5NsR+vV184OY+NziwF9q5MysR6CNw1QPHt4L3XnzmBtRU3MCgK/V7euNHjhF5Gf37ZXVuO6e4pparW39er7Np4+0T4rUlXLxHbu5j99OuakC/GzfXfHPv9TfvfXOjjr5/pJUQ4vYad5tQ3fL99zWTVbdc91Wj+sfp/f1tQHd/F1/Xnpt/9yI36mbTvkkbbmuPun2PX7tsPQq/6ea9e/NGvwRJoj1/8Zzr6ylPnjxhOp0yGAwYjUZcz6YYLTXVy+WSs/MLTk9PGY5ErbtrHbPZXMTLigHz+ZyyrkSXZjikaRpG4xF5lmOv5L19dXUFSpiuAOWyZLFYSs1303J5dU0WRWWvri558uQpR8dHHB0d47zj4uJcEm9pymw+p6krTk7uUbe1PFMbKX07GQzpXEcWPcIn+YQQROS5ayqOD/b50Uc/kK70gbIqhQGIoukci8WcQTbBJhnVsqSMGeuHjx4xGg0ARde2PH7vXa4vL/h3f/3XAIxGY+7fv89gOKAuS8aTCVme4boWhaiIWztkOptydn7GvZN7PHjwgDyK8q7GYP+Revvl7s8Fwvb9Ez/X5ZJ2Mce3FUFrAdhtDXZEVy0JTYcPHcFJAkIpSZJ451DWorQBFdDa0jaV4J1Y6hVCENHgVvAbsQwWo0hsjvMt2mY416I6I/XibY3Jim82mL/JWP/+UPcfbHl74N22kWqqydKMqqwYRz/mVV2s0H6rNbjK0gwF1E0tE6aocJymRmonWrEJUoh9mIsDHYRu0jSOuqmjyJJdq9gOBkOxHop07KIoRHU51lWvJvs+BBGC61oSJUCjGBTYSLMNTijteS62Xl3biuBSlsbMuxFw5DqxjIo1rqBYLObix53lJHpFFQcUuC5mMl1D6BQkSsQnrBGKjlrZkAmLQGstIlTeiSI7irZrY+bfRYulXOg5GpKo1GyTROp1nWM2mzKbTgGxSdg/OKRuGrQ5xSQpznuO5wv29sYMBwUvnj3j1evX/Omv/hQFfPH5F+ztHbC3d0jTNuv3enABpaFzUf07xOBECJRlvNbGx8yjjPkkEXVHUcr2klmMU0TxcpdrKeJljmIg9b2L+QIibXglkmVCwBaFWEJ0LYm1gOdoMuJokHB8+IBcOYIXNXGUJfEZ09NnNFdfEVxLcfiY4FKUyeiqBeXiksnhfQZ7R5gkw89eMbt4jU5HdF1H8K+ZTEYYlzNVxzhSQhRis0lKluasprwK1voB5WKBjXUwibWEKL63suFL05SmbnDexXp/HdW+y3gt07VQ3Moyr1yWuM6JKrxyEETgcDKZ4JynKpekWiK1Whs8m4CXUoGqqqUmXmnSJCXPC2bTKVVZkWUp3gWW5ZIsTTFa44LHROXy4AOXlxcMR0OG4/EaADgXrQKbBlBrj840kfbLuNN4D8aKLoRSLW0rQYa6rlFKstneSxZcm5g9jzYWs/mM4WAY6/0lyKOt3crYJ8asPc6rukIHFZ9HmqZpyYucIgYcVvdZVVUYbUmyBHJ5VhhtWCwW4jf+bZcdbuINwvEWkIiTfL35rQdftqbk/d3eRSVff9p54WzXp/YOwPaqgU1e+eak+MZekEztimyuQCnZfoWIwuYct7ZZH15ttb8PDddT0lWKcrXLwLrmNwQZ2ygJCK0EudAr0TXWQchVAMYmYjmWzFIW8ynLhVDPh6M98skAq55jjKVeTnGto7suqeqXjPcnEBw2H5ENJ0CCyVJU2+Cvfo8vz9DHv0Rlx9vTCNU/q1sAZDzbavYSX1+xnJ1z/uI5XltUcsj7H/2JUMy1eKISNqGGdT+qHjQLvX7dug6bg+9e162BsYV09Q4A2d16Z9vNSXP7sgr+bHpizWpeXdut1eP13UEIa3kBtT7TrbHb30uIYH93fPXB1E0vmVZXAAEAAElEQVTM1Rvjt51fH66+seC5R9O/5Ti76952lJtr3LWv+M0m+tJ7mGwCHitexpv3tfWI+IMt3wgv8jVd/YaDvNVmceztxoy+Mdbf3fCWDf5jwAO3xYuc63h9+pqrqyvOzs7WzC+lFVVVi8VrU+GcZz6bUTcNw9GYEBxXV3OGgyHvPH6HB/cfML26YhlgMBowHo9ZzJfkg4IiL1iUSz7//DOqumF//4AsSVmWS45PTkSjJUnY39ujrGtevXqJywd03SnnZ6dYawg+xDki7O3tRbteKaGcTq959eoVR8dHUpZoDMcnx9ENRVGVJScHh5ycHDLIEiweGxOCWoX1s0gpjeg0CAhtuo6mczidcD2dsaxb3nv3XalH74RGbhLD2ekZ4/GIk3sPePXyJVXdsLe3R14MSJJUmL0hkNiEZSmlsc57JpMxi8WS319/wnwx593H7zIebyxet67bakzfCFLtBt82v4UQqGZXuKqErsMbg2sjGNYGVy0JnQhM+6jBFIKXMs6gcF0DjgiuWynpCqwTbSiwSdQfCYHQdTRtTTYY0katLqUUOs1R1oJ3uKYkCfs7N8St4bjtn7ljld0x3dvVtwtx/eGXtxdX6zqpzUYoKSIqIOAwTSSj1jlHUYh6eNd1UWperzOhVklGs6q6qOItite1d2Rpjk602A0R6DoZZdYIoJAMeBIz0gLevPHRe1mo6ZvMdrKmzTqfkKZSp6eVonWdqDQj9aFtI7RryZ5n6ytkrQFEEbmuS4p8QJpmIh7nOkKQjLZzLQRL17WgFEbbSE125IXBdy5m8A0mUvC9E5pO0zQkWUIaM21KKarlEmMsbaR9KCWU4HopwYtArLVPUzIr3tbTa4kOJkdHoMSOqmmESWCMMAwGwwHBOS7OTiVLWOT85Kc/YbK3x+effcH9e/fpomJh14riuut8FJkQerqPlJymrmnbliQxWJuu+0QB2hqUYt1+a62Ig8V9SzAjkMZa+aZpImU5xdiEthZf6hDFy4zRXF1ekCUJg2FBkRs+emfEO0c5SbFHM31NWS+xNsPYuSh2K4cm4LXh8vlnDI/exSnwbcXF6RO++vt/T5YP+MV//n/m4J0/ImBZXp9jkwVNWTI8vM+sXdLUc2wyITn6KTobSZ1SVaHTFIIEH0KkKBuraZ0o1ocgyvRKaRErA1FvN1kUaIu2XcZQDAZU1cYv3DVi9SVZaAlMabWy5PJ0bQfGMJ/P1ir/SZKwWDRkeU5iE6pYX51nYonmnVv3aRfr0FfgHDy+62hhHcxRaY42mqqt2dvfRyvFchntz7JUGB/ekyYiZqi1EqX24KnKjnKxXGsVNHUdvb7VujZcIcCoKEQNXxlhkVhrRY3cGo5PTtZijeJ7rplMxtK/UUUzKCWfnSNLU6lLipoMdbXEdVI/vwr4eR9oqpqqvmY82SONDIJupefwh1xiDeUGgGyWLb2yfqan//f2zra3v+OXmxmW1Qrb0+s+pO7PHzfQZrtdN2sNt/J1m/aHVX5zg6C3MvDxBPvAaytbvG6M2vRFDxCtMugqBIJS6EifCwZWoowqBvK0Ec9vY+XdsJhNqcpSLCKHQ8Z7H2KzU3TymnY5pakWNHVJ13kmLlC4htDWJKMTFAZtEzSKrj7Dvfz/oQ9+ip58SFBma3Ibev9ugzFFVy+guaQrpwQPx+//ER//9mPu/+A++/tH6yBtWJ+n2qL+b/bfn3CFrV92rzARkBLCdn/26NuyWi+wsr60oXeJw/YJfc2yhpfrGeRmzNwOrPrr0GvJdudustC9KA1saqR3zrsvanbbsN36arddbyhF6VPZN/Zeu/3OXQn3N+K23WbuguPdyxD613RnvRunvDPfvfMMw43dfbdl90TviDbcfB59j8e+ebjNXzcxzFvt8s3rft1at4c9tgN6b9GYu3a9uzOg61qePn/K9dU1l1dXzKYzXr1+BUh9sk2kvtkHz3Ixj0DLYlHMZzOKomA0HpHYhLOzM4zRDCJL57e//S1HR8fYxDKfzbi6uooCrB11LFt7cP8BZ2enkd3qKMuKxXLB4cGRBOWNFVsw1/Hq9BUP7j8gz3Mp+dNSgw3w/PnzdV32ol7iOgk1jYYF7zy4x/5owDAVrZuVUWAIAXwH3qNcF29vD0qSiEopBsZSJAnKWA5HBS7AbFlxeT3l5ek5z1+8iPjEUNWNUPMfv8tyMef87JSz01N+8tOfc3hwKJn5WP6XRdZeWZakSUKeZ0ynUz797FOODo94+PChMAnXl2z73brz4eZgiT91dU0zvRTvbudQWjCNNgZlk1jLLRltPJL9RpIfaE1onWj82JQQHHSKqlqQ5ANZxwu+SbJcvNaXNUmWi/6KV3TlArcEk6dkoz3REuo6Ah6169Bx19gOX7/S1qvsjt3s9OLdyxsP9f1A+bev8c5S0lRqSuvO0dQVeVGIvVjXxlpOj9eKNoR1PbGJwFkE0fwm0xZ9e4UGGC24vAhVWW3XKuddJ97PRptIWa3XquI61pa3rdhpGSPRqq6TrJvRUaU6RnG0SQhdoO1aimIg4DqI6nJiE7QCHSmsZVWJkrsV+6mglFiTBQGLaZJFIThQVmETEYBaBRQ61+E7B0psj7wPKC3U9qbraJ1Ej9q6WU8pnOuo65q8EFBSlaWA/SDAaeU1vKrlTYokgh7xE0yUIS0kUzmfz4UFYC1pmpBYy/T6mpN79+laUaFO0pTr62vuPXhAMRyilYhW1HVDmiTM5zOsTejalqoqBZCHQFEMqMqSpm0oikGMjJZ410VRLC02X1auPUHAGYB3ntrVtF2LNc3au1EYAZasyKmqkizLGQyGQilPLfujHKsdf/rjI04OhnivSCw07Yx2dknVVKQf/QOszSmvnrK8OiVJC1yqaTuHSnM6t6Qr5zz66Oc8/OGfMTp6lwB0HnQ6AqWYn39BVbc8+Ojn/P5f/5qkyJkcPeX4nT+itBO69AjtUzqvGAwnOEWscRFg2XWdBA+ShMQmoMC1Un88W1wDYhe2GrdKq40KPqBMR55lOOej2r2JqpLd2pPaREsuow0rxe9VLbX3nuV8znA0oshztLFrlfGAjIvEJuSDMVopZtMZk/09kiShbTqMNWS5RDBFSV5FdkPJYjHHOaFCJVZq2dMYPGmjSJxSGnTAKqmfQinSLKOpheGSpglFUaxp4cVwgDFW7jExI4nMgmQTBPIOF1xkuIgGQgiGgMJ5JzXp1sY6MglgpFkuNfppLlT0WI/vAzEQp/Guk1o1FDZS0b7tsjWp7c2ENyrJff9ONqBh/Y3qbcvm+61Vdl4bvbfNBpj29tVbOwSiIvk2QLnrNaR2P/WiAGpnjRtbxkzjJqCwAtw3YCK3Ai+1OY3QR+WwlfXfWn89KVGoIGMwKEhixkMbYWZoa6Pnd8Z8ek25mLOczfBdx2h8QmoHJPoZyiS40rJcVjT1OeP9JaM9ub/8cJ+sGAFOJhxtQ3f21+jyFHv0C0jGsWs3IHODhURcLoRAs7wguJaq6WjLGRcvX3D08EMevvsRaVZgtGXX7kvtvPdvp/fH4MgtwGs9Lu/ox/4Vu/P3/vje2e9trdkabess9A0Y3T+tuI0m3LnGzkF226t276/N3/1563rvantsroIOWz7zO2Bwd1G9Pt/G6Cu03dvBTvBk1Ufrru3FRbYF2jbQe10a0D+H3nFvm2TePvG8+e2N+/X7BN1/kCXE4MBuWPNrtwJ6p7d5jHyjHawDL9+5n+6CBm8FGd687IzJtu148vRLXr96zbIsYxIE7t+/TyBweXERmXfyDn316jXPnj3lww8/JAR48OABPnhevXol/tzDIVVTo+Jc791335XnXF1zeXVJuax48PABl5dXDIeG6XTKdDrl+vqKohANI2MsTV1TliUqzuUP9g4weY5NpOTz9atXHB0fs1guYyZ9gfOeoig4v7ggyzKKIuWjx4842SsYJikKj4o1zqprCa6DrsPVUS+qrcF50IbgOzAWpS0Yg7IWnWagNUZr9pOU8fE+x5Mhx/tjnr14hVKKi8tLDg+PCMFzdHzE6evXPHv+kuFowsNHD/FOXImapmF/f09Yw5MRi/kC7z17+3ss5nOUUpRVyTuPHjMaDXvP+DcMmTuW5fU5rqoIrsO1LV3wmGqOH++BknP1wUPb4huxUrZpJparVYnvOmyWi70YiqAhzYe4rkVbwWmgxSHGZihjYoLUglGYLMe7Dm1SXNtgfI5rKoJ3KKO3G/9Nomu7kcc78PB/rI+ttwbeNmYKFFAMRKXOJjaCUwEIZRnteOLLNUnTmMlzVHVNlmXkuURDtNZkWSa0XLNSzFNiPaYgsSbScW0E5Z66kX0InXBT82is2Hl1XbemnEKc7qmVH7fUletoJda2bQRG4r8smdwGj2TIrBYFax98zL46vJbsPUGifqDRWoBw3TRChddG1KeViM11MfPbtq0oMgeZdGmlJZsZz00B1iQUg+E6276qXdWsPNQ1dV3jug1dmxBEgCvN8PhIhYfDoyPaCIhQQjVLjlOMjpYQVcX5xQUP33lMuVwyGe9RNw0n9+5RlkuC9+ztTdDGUJUVVSX/rTLfk/0Duk4E25qm4WAwwBjDfDala9voqRyi5VVNksbsTRCKoYuq70mWUsQIqU0s3nlGozHGaA73CzITaOuKj945YW+UcbKXS5azrdAhw4cM7xXHP/7PMEmOb5e0Hbz88lOs1bz3y/+D0Ml9R5KMyEaHOJNz9OAdgraYJCMxnm5+xqJLaLqOH/7xLwjZHscPH3L+6hmf/ZtfUxjP8L2/IBvt45TFeqHer2q4CbHeOZF6/cRYqUVVGp0muNatlcaTqIiPF4aAjewMYwzD4ZCubVEEbIx4ehWZBwSyPCN0Uh4hapmyX+86urZdi3SI0ncMWBlP1UpWvcgHEYh70mLAyT2ptdFoqkTYGE1dr1kaVV2v/bEn4wnOdzEApajrmrZdCa5J7buKQDsELx6YkfqdxYho3TQU+SoLmaG6bj3L8Z0E4rQWsJ9n2dpKsK7EE72OdeRaG1HSjCyKNpaJpFkmIlud3N9pnq2ZFcvFHIEkogzfOE9e5ASIvt/fjWref8ivAci6sHIDnvQq0rt+W/SUvnuT7i34o27/bg1c1wB104rNZF71cMMu5fRmqzegozcBDYG1h/TWEeJ2O+9PiQFsf/9GRd2w80Gtam5vQeW9Vm/6JKzRegC0CoQgwRyUBGC11mgjJR46sRibrlXPq6WUGg3HE4r9D0nsc7Qx1KWhczWz64qmfs3k0JF1Lb6tSIeHkcauUV1NWHxF11yhj36BGT0GpW+EGlZgqetqlCtpF9dYq8GmNC7l4aMPhTUWVcxvTLlu1PJuru+mB9Wb+3rVS/3r06Mlr4HdjX3c3OdtR9kagSu2wg3Q2uuZcHPLlcDa1v63BuQblhsBqN0xvv3Xyn5w+/cbd9vWVrthB/m8CRTcLpTUB/2334H9L29r0aaE4bYogLrRZ3den5uPC3pPrc0x/lAz12+431tX352oq5175et2FvvgNl/gb7p8PWYIW59vF3K7fS+bsfY1u/0G5+G956uvvuDs/JzpbMbl1SXj0UjmMJl4WhsrYqfT6XT9Dp5M9ug68d1OkoSnz55xfXXNoBjw+vVrHj18RMgL8kw0Z7z3XE+ntI1s89vf/pbp9RUhBA72DiibkqPDI5blksPDI5qmJnhP17Qc3z9hOV9QViVpltJETamDqATeNA2///QTCIosk7nSZDLhnQf3+MGDewyNR8d3ow4BXy9QbUe3XNCVi0ittnRdjW8afF2ikgxcQFkNSKBWWYNOE4zNQINOM1SSMCoGfPjwhIfHh5xdz/nsqye8eHWKTSyj0Ygf/fjHDIZDzk9POTs9YzIZc3095eTkhNFozPR6xtnpGcfHR5K0dI7rq2uWywWHh0d8Vn3Co0fvcHBwsMZeX3uRexGkrm1oZlfQiqCZCuBCAOfEhcp76ukloZVMuDKWJNO4rqNrGinVSjNc20IION+RZgXOOZqI9Wye4WuxdjVDKQlcsReJIqc2yzBpijIWZUS76c7Y0tefGLei7K/plm8Usnrrdnz75a2Bd1PXECe5Ok5kRHQqCPCIQFio3zLRrcoSkEygRbKerhPbIqHRib2XNy4KGogncBKzhcaKGiCxbq9pGppa7IuSTLLNzkkNtI0Dc+XZ3UZrobZ1hKbDJpJ5DRCN3x1NU6/Biw+e2XxOmliyvABFzLJ7gjGipuwdeZ6DNZRLGXh5kYtYWFVjjCLLRLypayOdPkbJQoCyWmJ1gk3tWgQuRKquKKU7iJPN1Fp8CFE9EjItWf4iy6kRVXVrE0LwmK5DWb32S07TDGM0eZaxjBPKNgI7pWE0HjObzTk8OpasYF6wWMzJsoRiMCBJLIv5HDsaCf2ea2ySMByPo8Cc1PyXyxLnpf/TJBXA5kVMLrUi/uZdR5PnWG3i+cUlQJJJzXGSSA2167r1xGJUJOAaZlXL3njEw0cPGOgF1ewF2nmSwx+gbYa215BOUEoxP/uCwf498r17PP75f85gtMfw8B51ec3i9AsIHoLj6su/599evOT+O+9z/MNf4duG0xcvGD34iIOjfa4vXzLccxweTXj6yW9Z1IGLFy9R5t+jyhJz8BE6yVjOK5RN15Zp3onmQLlcEqLytncdSZqQpMk6K311eYnznsFggO/ERq5zLtYr2zWjwTuh9q8Au+s62qZZRx9tpOtX0aqtaSTTPhgM1hnutm2i4n1HF4RZUAwKmfw7hwnCSmlaYTk0IeCdOA0EL64DiU0oBgWtEyXLxGhQGqVzAQ0hUC0XtF2HigGrJEnWNPr5bEoWxc2KwSC6GUgtu42sgMV8jjEao0TA0HvRBiDIWFNxDlhX1XpCmOeDdZZ6lSFfaQcUhSj9e+fwSkkwzMfJfIjAvijwwVOWS5q6IS8G3+FRuj1pujOPHAJhlV3bAasqsFaW3p7+7+CNmFoJvS+28UgPYof+N3e1NjZtBRpWgctd7HAjfaZYqaTuIu8bPXGLOvPWVnGC1P9+Y4i1AeO91eJmKoZSetMSFSBoUD5aOUIIFmM1qPj+MhZtklj7nbGcXVOVS6ZXF7SDAePRe5hkyCB5TTW7pm6X+LrDnZ4zmlQEPK3ryIoR6WCMSXIwNV1zRffyn+P3/ojk6GconW+d/qqRbTXHWANpTn11QcBy/8Ofsxcp5tyZuQubPrhVqG7zTX989Dp5J8ixui67QPAmJLh7uQt2vGmSctuA2kFC62DAXUfvHTd+7NPw+2vddty1SCCrbeVbdWNN1s+NLUX2G/dG2Np7/4i9nbz1LPBmiGD3GLu/9JatQApvRobrDuD2/g5xRH0fc85vCRRv7EO9/fY3Tj1sf3/Lo/PmDnZ/2/nujU154znfcg98w6V/O991jfpfe+959eolL1+9YrFcSGKsqvDOM56MqSsRvj05OaYqa5pGKOV5UXB1dSVAPLoOrWxIRYxYk2YiiHZ+eclsOuPk5Jg0kXd8PigwWvPOo8fcf/CAs/Nz2rZhNp1G1p4jy4TxaKyhLivSLGexmHP6/Jy2adibTDg6PmJQDLi8umQ0HBKIeCIEPnp0jw/uHZKoDu0B7wh1SVct8W2Lqyq6upGySrTMdXxUIAoKX4o9GU5qmk0iGjXBO0xiSQYjFAGtAmY0Qg8m5PmAd4/2OJwM+f14wmdffsX19RXeO46PjlAErq+uOT9vOTw6ZDoVG7S9vX2UDtR1Q13VDIYFSZpSLpec+zOKQcHV5SXvPH7M48fvCvb4uhuoVw5Uzq7olst4bgHXVpiQio1Y0xCMiAG7qHdlbU4XcY5rG7Sxkb2wesMq2rpCW0sWmYrWWkxe4DuHClr6NwScTaSEME2xeYFqFcp1UgzWdMIqIFqKbdXf3BLs6z+Sdst9bln95qPu+3jofL/LWwPvJEnXwEFJKo8sUr4757BGMx6PhbLaSb2vDyGKYYmdmOtctDDq1tL9NrHYSFfvIvBa7bNzHZ1b1RkHkf03Nq4jtZ6r+leQGlVjxV+ZIOIFq5pipUV9GRRZpFIIpXzJIAogjMdjmpg5Gw1HGKMpKwE16WAQ7YdCbCfrGuY0SVFjobjWTS01H6nUKyutRLytqgRkWksAyuUSQDzAjYDmjU+qWqu3txEQdV1HAIpcbk4Qpeiu6yLlX0ffdBcp/AZjNcOhKC8GLxPF4D2XkY4zGA0plyVlVaKQcgKjNYOiWD+EHYFiIA837xyLxQKUgKHxZMJitiDJxBquriome3txfMD06goQRkDTxgdcBOpKKfLBUNS4rWUwHFAMhmR5TpZljLJA5zz7e2P+wc8eMTDSX6Gu0XuPaOfnhGIPfMfy9HP+1V//zxy//xN+ePwuIakYTArorrh88pSu7aiXS2ZXL7g6u+TXf/UblDL8J/9ZzoOfDLk6e8V8NuPwB2PcfEZoHYurC+qqwrvAe+/d5/77P2S2mKOf/S1mMWX4wX8i17jtaNtOyh+8QysrddBa6vtFSExqoJVWWJswHEcKefAohHqeK7P26hY18s0EL0vFli3JC6kD75yoksf7MY/U7bRtaZt2TU8XYSkDStpjMxESk5elTCK997RxjPsg95LrvKjJpwOpi9Waghnj5WeoUKOUxTnITEpQwky5PxqRDMYoX7GollQupVGKvb0YmAqBJvpYaqWwUdW9jhoLWV6sVdyXywVd6xiOhgQfpPQgL3Bdx97+QQye6fXztOukZmkwGkl/RAFFqfWu0MVAmARZurZ5S3OhokPAObFrytL02z1FVxeq92k3F7YNbHZn7bCVlV5tHLYJnzJn34wLFW55oayAsIpTyhszzrtzoerGh+0v13TKNXhTt870bgsC3LZztfNvWGW5Yf2i3/RlD6yrm+/f/nltMnWSgdRKEbySCLzSGKUxOkErKWsySUqaJsynVywXMxbzKU1TMxpPKPYGDNJXtPNzyuUc37Z0l3Nc21KManxT0lYl+WiPrBhjsKiuwl9/TFdfYE/+DJWfxBOMvRc8oStxi3O0dyhjWC4Dj07eIYtWPBtZsL56e9gZNqtssnTIjX7a6en172o1jm5ei5vgQvXw4i0BIfpQtf/dBs/dvWyPje0gk+p9t5017w2YrWZug+6V+n2vAWuWnNpkubd/3nzeUURf9f8mk71zb/WKvLcp6j08u2r8zn2+KUfZRXj9/fRRVVSVX/2qVj9vxsGN+1Jtz1/Vbp/DmgBx8yr32/YfeIm3wG7JxeY5uj3NDjfW2yxfPz57K27t9C3beufOvuv0v9eg3bZ9zWYhBF68esFvfvMbjBVRUR8Ck8mE4WAgILCu2dvfo1yKu8pkb4Ixlul0yv7ePoOHD7A24fLqCmMN+/v7LBZL6qblejrl5atXpEnC3t6EqqqE0WgMWZby6PFjijzn/PyMy4szBqMRH7z/wbr8Lk0SHj78Cc+ePUcpTVakFHnGsqok4dN1BA9FnrPMosjzZMJ7Dx/w3tGY/UEU+vIB37aEco6rK7rlAtdWdJ0HIyU8Pnh8UHhkPuE7J/Nc5F4KxuCtRYU4T1oscT6I84p3mLpGz2YkwzF2vMdgMOInH72PMZqvnr+kqmpUkORBXhRUZcXLFy9ZLJfs7+8zGo/Js5y6rri8uuT6WnF8ciKJkyDaJ8ZoXr2Smvv79x8wGo12nkk373OCvGOa2RTahlCWhK5BRYlFXy9h/5DgHPhAcOLT7VyH86KtpKNbU1MuaZYl+b4kt4KX40ppZBANq67DJJnQx1UU2k5SkjwnybPIEvYolULwBNcQuhbSYtPub3xP3YK437z22x1mJyj3HW/UO5e3Bt6rTGbbtmsa66yexahHrGFWntlsJsAXRZZnoFSs7UyFFhoMWZqtxYycd7g1gBbasXMd05mozlprKQpRvc4jjXT1ouqcI6gQLcfqmB00a8/tLM1oOqFMWJNQNxXBeUyQOlmbWAZGqLl1XaONYTgc4V0HKrBYLgRcp+I5l6YZibUxKrXAG/FWrpZim9V2rUQBg2S3E5uS2lQsmWL22/tA5zpsYqnKkroqxbvQGIkqDjN5+DU1xliKwQBjNN1iJfcvIMMYQ5qkEkQIMiEYDAbSP0DbCN19BfySJKHIC+qmphgMGE8m66DEcDRaC+HVVYVSmsnevli+eS8q0GmyVqEnKNq2xmjLeDyRuuLlAtdJn6Ik40mk1SulWC4W64x2VdeRgmzIhyv/bmEVQCDPUow1HExSfvJHHzBJS1TwLC9foZOC8uxTqnLJ4fu/Yrh/zGs0Dz76BY9+/BeUs5dcPvktL558RddWjPcOufeDn9JUT6hLxV//9Rf87/7JPyUtCh5/8BHBtyQ2JQT45JNP+OJ3f8//8H/7H7DZgC9/9zccHO0xn14xvTpjPDng9NUz9OKKdDjBHP8YaxN8WJUeyLlaE6OksW8lCyuRYbGZ89RdK+vpQGalrkkCRlKGULVlvMZJvP88ztWR7SF0dqUVqU6itYOGgaiYT6+uaduGw+MjLCJIiIXgAkFHuwgfpL+VWjMyVpOaJBVPS+daChuY2Gvy6iuq6hXPnjzl7PwFqckxRjMeD8lHE9okYbI3pq3mWJuxZy1tF8gGA/LBiLQY4nVGkzhskjLeP8CplIuZ53IW64iCp7EWRYZLAgSx91AqviQ7h0kSjFLrAFu5XEYxQ7/OfisUbdMym01Jk4wkzaR8xArYDyFQ15I5t/E+striv8ODdK3CTQQoaxApk+Fd2C005J1c5QpQ9mqkt6BDD0jI79t7va2ud6eVvf9vfwdh7QW9XmuNJbZh1hZ46H1/20utD/TWbexlv9XmIHF/q2u4AiObPtjKUG2dxDYiu7EuUpumggghKmXXEwQdaefiOJGR2JT57IqqKrluO/xkj+HoA9JkiE5eUk9nVO2SMOtomguGwwH5WKG8w3eOfHwY69lKXHNJ++KfYw7+GLP/EWgbhQFb2mrGcDDi/PQMbQv27x2uKeYqivvEbtl83gJCfdAa4pgJ/TPu9cR2L6233vX03unR0NcD6F8rtd3jt839V5718UEYv3zzLGYbeG5f//45rY91S/bYh8Cyqnjy1VOMtQwHBScnx5gosKi13gpq9lkWW9PYndKRFZDftHQT5ti0p9/WVV/dHuAIvT82E+mb/XMn3f/Gs2HTxxtNul3UebN8YNPkzW/rNW7EAf5As9BvsOxUr9y1Vvz3DpD7ph3sTrxX639fp34LVvp2O9ks/SDKm3YfgNlsxpMnX4koaoA8y0WXpuu4urpmUS4JPjAajSMjLnB5cUkgcHhwyGg8QqE4Oz+naRqMMqSDlOfPX2Cs4enTp0zGYz54/31JZDUNL1+9YjLZ4+Liki+/+ILjk3tkacr9+w8ZTyRZN53No4d2TggwHo9ikk9zPr3i3skJ89mMtm1ZLpd8Or2S56SCd+8d89P3H5Hg0K6LtPGK7vqa9vocrzQqK/BOknp0fq0yjhaBZ52k8rmVZIgiCHvPBdqmEo2iJBN7V6chTcXn3De49pKkWmL3DknG+/zkw/cZj0b85b/81yitGQ6H2ESCDlfnF2sW4uX5BfsH+xhraNqGi/Nzqqoiy3IODg5o24Zy2QIK13VcX11z7/59Hj58KIzZNwyoplxSX18S6opudo3vWkhzQrUgiTbJXVuJ53Ys2fNNQ/CO0HVSw50movGUtDSLBVkxQumARqG8F0UepYWh5RzBaIILpINc3mOIyrnSGpUm4D2hddsPv34A6U3LevWdErhvtOzOWN68fC+36h3LWwNvlNC4VTSFX/lf100dlY2F1u2cpygG1FFMCRBK+cpGKgomdFGQzdoEbRUu+PVJOudj1Ae0UhgrdNrFfE6W5wyLAV20onJE+rcSKro1RmoRTIZzHVYbtNFCK0foLk3bChhBlPLbtqPpGtlP8NHyScSsMJBasRdbtVuhCZ61/7bzHb6VDDVAmqQSNUqlrjCxKVVd4l1AG8nMEwLj8YTVpV0FAkBq58v4APTeRcVpi1YiSOdDiIrnUsPYNi0BCTa0XUtTN9KOVKjIZSWUf2Lf2yhEtaI7ayXUXmsMg0FB23TR8koeVNqIOjfeMxwM0UbjuoKmqTFaM5/NCCEwGA3WVk7GGu4/eMhseo1CVLqTNEUZTblYkheiE9C2HcWgECpyltO1daTdGz5855CjIfjWoFVGvveQ+uolyfgeIfeo0NFWcz745T9GJUPatqKcnnF99pLf/c3f8f6P/pjjH/wJ46NHXDz9PYvZBQdHh9x7eML+yUOsctSzM66v53z66RN++It3+Cf/7X9Nkg8o6waNJx8fcHp6yXK+4NHjd3n6yRXT+RKb/Bpb1Qwe/UJ8xpUiz4Xe7JzDREFBpaBthZlQlZVYYyUpGgk8ifCeE0XItsGalOViIQrlabqmgCWpXQuPtE0LWtgPOqo2u0hV19pgE1EHr6sGZ4X9gPcEpfDdCtzJ+O2abj0v1lpEARWeUdKRqVMyP6U8O+XV6UvquuTi4pKz84avXjznYCgRzCTNmOyNsKa3b5twdHIM3lE3Lc4HPvrxR+zvT5jNZpx/GRjsnXD06D2ODgPV4owkTciOj7muE37/vKTuDFqB0Ta+hKX0xFpDaDvKckm5XOJ9YDgcStRca7qmAaU4OjoGwlrMbcV8WdkPCYPFiyBIEv0qv/Wymeyt6zDjxHuTwQtbmRqZ725rL/eX3ZdDP9O79f0NeHrLS2UNqndeeH1gtrXTPvCRT32fY3Z+XdfychOChzW42Zkwyo+b/ajNOautI8vWanfb9S/x/2H7961whxJ7FhMDL0oR675VFPLUGCvU8yTLmF1fUS5nTKdXdN2Q4fiEbD8n08/RC0NbL3GVw/mKrn1FN9kji8GUfHRAMthDN6WUiJz/DaF8jT7+BSoZ431LYgLzywu6ekkTUvaPDkmSdA26BUCFbU/nwLpM4fZZw2oG/nWTmVuyZTvU5N2rv7XfNaZXtyh/9/ffP44cbOvabiILN3eyC/pubc+qWYHOOaq2ZVnVfPnVU/76r/89znUcHRzyD//hX8i7pi7ZHxUcHh0DZqef1LaCQdger/275ubYu9nCANEusH+u2wGFna9unOH2Pb7dJTe7R62p8vJnDy1uRwbY7vO7Qf+adh52Gvldlu+wq/4Ta/dZcOsubx1D3+x430usYfX832rk7tXd/e7bL3cBhnK55LPPP6WqarpWrFldtIm9ni4lmTadUpV1fGdKKeXzFy+4vLjgpz/9KVfX1ySJZVAMGI6GTKczrq+v8d5xtHfI3v7+Osk2ny+YL+aUZYUxlqqqeO+99zDGkMe5S9M0vH79muPjYykPiwHz4XCI9475XLy9h1G93HWOsixxvuPh/Qd89O5Dfvj4Edp34DyhbfFlSTuf4VyHTwfiPtN5YdA2Lb7rJMyUJChjUDqhrUvRjIkCbNpY6Cq6piab7EEIAl59R3DCIFVa04YG2gY3KEg7h6lKkoNjHh/t8ed/9ktenp6TZWJ7BvD+Dz7g1cuXfPLJ79nfm2DNIUpr7p3cF5at1hSDAVmWkaYZzibCpDQa5ztOT1+xWMx59913GQ5Ht1x5+Xdx8ZpQLgX4FgOoK1ZPNNc0dHWJQ4uvdteKA1TbYfIcnUiSLTgIiAWs8+Lk5NsOp8RJxxZ5TKAGrLVSD58mKAeua7Fpimsa7LCQrLoWobpwZxnVbaN29XbvJyPuOG219c+tP3/tnXbHPr7v5a2Bt1IapRVZrKVcqRhnWY5zHYlN6LzD+XJd3+pcF+mc8g72SlTCQWpHJUMV1krNYkEmdcCidk30xw5rOrkm+gcrxJDeOZblkjwvyHOxbnK1Y9nOyfNibckCUmutlYoWW540qkFra6CVYzVNAwYSI9TgzGZoLS+1rpM6liQRIbBVzagxCTaxMnHSCoUmySL1Ip5DYlMaLzef68ScPsszuq4jSTOqcokPnjzLRcU9ehZbu63wHiJ9XikRSUsSu57O+OBjrbt8TtKMfFCQptlaaRKg8p5Blq1r9JflgjyKJjjvQIXoCz1Yi3hJwCUljeUFIXgWc4mcTfb2mEz2aLpWfpvOyIoCDRweHUcqfROV50WYK0lTikGO0XLtjTXsT8bgW3yAP3o84sEhaGMx2RhNQAeHtu9gjSUPiuA7qrOX1Is5hz/4FUoFkrzg4N4j/vf/3Ye0KiUfH4O2QEdbVfz4pz/EWkU6mNAsL5idP8F1NY/ff58HD47YPzymXFzTtoEvPv2cweSA4WjEyck96qYjK3K62Zzr81OOxi9w7R/TdJ7UJqAk0MRKjC+KfoVYr50VuVCgCIyGI3EDcPKb6zoUkoEbDEcYq+N9YXB6dd+IPoFTjrqsyKJHIsHTNjWD0RATLMPRkKYRgG7ieGiamuViCVpRFIPNA0bDcjGXIAGeUeqYcIGaP+fq5Zf8q7/7hCwBEPGNoMUf02YDnr044/xiRuNKBsOGIk9IE4NVEhz6+Mkl9+8dMixSAoFf/+XfkOcZR4dDRqMBi8+fof7mN/zo53/K8cOHLC9fUy9mPPjhn5IliqevS8o2p1WWoBIgbET+uo7EJOztH2Cs1IUrEOZMfPELk0bFAJ/Febe2w1NKMRiKOj8O0jSNmhTffpGXQoQWXzNr29BLd2i03IWpbkDJNTC4TapnA2BXx+v/uvXL1jHXk9jV+jfOobfn3m56hHgUakNbDZtMt1Iriu/mDDev1U3715Am9Np4AyiwAe2rsdyvTw0rwbVdIG9AeXQUXlMIW2Tl923W/6XYNGG5mLNYLmiahsFoTDF+j8zm2PKccj6lrDtcV9N257imw++L/2s2GGPTgsQ6XFPhq6e0T15jj3+J03t09Zw0NZjsiOkykGTF+rm+xXjYzUqHXvTlNsDau0SrAEN/B7vjov9hE8BYlRWseq5PX+6jxDUCZ0WND+pmVnVznO1RKmNkwwy5ETLqMzp67Vx9dGHls9tSljVPnr3g/PyKZbnkV3/xKw4P9lnMFnz+5VN+97uPefLkKXujgv/mv/zH/PFPfyliP4HbhvPN4BGbidtWr+zcOFu3/Q1Uvh002eqN3o3ar+K+DWTfDc82YoRbsG7n3DbX+k1T3Z12f9/g+3vcPNz4rG78/taHjKf5vU28bz34bj9+h369Db/fsjjn+Ozzz7i+nm6o5GUpXtuLBZdXlwyHI2xiST00dct4Mub68prJZExeFDjnqaqag4N9bGK5PL/k9elrsizn4YMH7O/vkxUb+nBZLTk/PydJE2azKYPBAGstX37xJSf3TiAEjg4PGQwkaD4cjvjs889IkoTDg0OGoyF1UzMcDHn+/Dmz2YyjoyPSPAOf8tMf/oB3DoZo14DroCxpFzOqi1NRJ9cGj6ItF3RRlDgkKSYfooLHVxXBCt5Q2uDbDlPkch+3LcpoksEQ1zbx0RciDT3aVgImyQlJggvQVDXGOfAOs9fy/r0DsjTj86+esCxL7t+7DyFwcHTEB95xdXbG02fPefDgAUliuXdyj5evXtHNZ4SYFEsju7FtRMi4agUjff7ZZ7zz+DF7e/s33FjauqaZXoN3onHSNoSmljpsDToEtJLkRVuX+Eay3s4Lw6GtpI47EMAFXFtHa99Eat1tgpRwaYi09ECQpFKWgxEWJl2LNpbQRD0A5SAZyHP3TeP4lvF8I2Hx1ss3vLd2n+F/oOUb+XgH7yFaijVNs6Yqr7oizzIUiKBX5/DeURQDiqLA+W6d5VupJHvvMVrHTLJEdZpor7Usl4QQpKY51n+4zmFz8eP1wUVw6qOKcSeiaq2AjSwVz+Gqrum6VuyZIh2cmPlqmkZsrayoECZJwnK5jJZ2fu19HAJU9RKFWgMga83a6iwvclFp1pLZ99EXuPOOxXKBNUJDJsgFtYnFKUXXiWCWNloi8k0DgTWwdSt/8TTDKdYq5Xv7BwQvXtGptfhE/JmX8wXWiGet6zrKzpFmqUj+h0DTNmSpUIu8l/3WdYVRBpSKIFH6VqGomyZORmWSmljJ4HrvWC7mkmkPMByLGqZtGwlcCJ83UvsMreuwPrCf5zRtS5Zn0ec7QPCkmbTZ6o6umnN0vM/DPUi0oi0v0LbA+Ybl9Utssc+8mrE4e8bhu7/k6uwUnY5w3uPakm455eWnv8EM9jj54GeYxFDOz3jxxe94+tVTzk7PefrsFf/HD37GxeVrvvr476mblsPjY44fPMarQGYD1xevefTBD/i//z/+ip//+B7HRwekecrV5ZTXFyXDvKCbPofXf09y/+fyQIiigxiDdx4CVKXYsCVpxqAQn+26rmmaBq1FjM87L4J4No63XAi3bdOQZRlEH/Q0SXHGEZrAaDKRLJ2RjDeAj4Jq2pj4chArD+88KKnhVzHYRYCqEiGRwXBEUy2ZqAXD2Rd09ZKL89fMr+eYxDJbLlHDB1QNnL9+yfVswbL2VJ2hs/t0RtGScllp/MJhNVEsLuHFVy2aigRHG6lMibU8vj/ggwdDskTz23/zz3nvox9y//H7VMs5Lz79Gx788BdUF88p25rB0fu8XnRcVmL3Z4P4p6/o/V0rwoGNq8nTHBcDEdqINRqIOKTrOjwBrY3U73tPXVdMJnvoWKrx7Zcd7WR1c04U+iDxlu1X/6ib365xaF/l+y7FZVl/J0O8BZpuHqEPyvvHXJ1L/4jb0ecIQ9R2XetqE7Wm7KpeO/rtuuUUVuvfBrZvWWez/94sdw30erXfIUQB+RWwtHgVy5xi/bcIh1qMFuGYJM3F83s5Z3p5QTcYMBo+IrEFuXpJvZjSuhZfgXdzUcc/fEhwmrToSIcjbD7ENQHd1bizf4myJxhylLGUV+eo9B7WphIFWwcyuGXOsKE7b/f1BmytAazqA8VVPXO8emGz5QbZhfW13XThOqLRuwI9Bf71DGUDCddDYusa9I6xE0cAtZUYX4NCHY/fy8SvzrLznrrtaJ1ov7TOcT2d8+WXz/n0k88ISthkXdPyP/6z/4nFdC7PxuC5mga+eHnByYNXHIxHItBp8k1bVm1b37/byGkdwIq+6pvu2EZru/XjW8vu9737iJ37JR5qZxIa+6TPUtj+uHs3bC5Fr6/71luCK2QnW8friyJ+F9eHNyxvC+lv7847Qgh3xabeZhd3btTf6VvsOdz88w85n+/34+51ffbsKZeXlzjnmM1mpGlKWZZMZzPmsxlN1VAUHqUMlxenPHrwQOw+i4KiyJnN5yyXpTiiAJ/8/hPmizl5mpONxc626zqOh0PqtqFpWsqyoq5rnj9/Tp7n7O/tMRyOyPOcpq5iJlisw/JBwbNnz7i8vOSdd97BI2VzRmsuzs+4vLykaVoODwODYsCDwz3uDVOMk7mqK5e4xZx2OQOT0bkWF5XLHRDSDJWm4AJdtVjPj4IPmDSjW0yx2YCurgWwKo02abyptOhyEMBD21XYNBUPaxc9sZWia2qSkBOup6RIyeG9gwmvTjOePH3KLDJDm7bl8OCA995/n9///vd89dVXZFnGeDLh3r17TK+vePXqNVpL/Xxd17RNQzEopLbaBxaLOV99+QUn9+5zfHxMYtP4+FBUsyvCSom8ruiqeN2aVtw+rKFraqrrKaHpJHFqUinfq0pc5wBJ3mmImiMepWTupJTCJEm0tLX4tkMnFudEjT54KYk0SRp1vtIotCb4kLaK4mpvc1PszBfuWv/WfdwWhpPvt941/wGWtwfe3sfspIBtG6nRWkvBfYAoBmAYDUegFT7WhNd1FeXlAURpeVPnHWIGWqK1w8Eg2m6JcFTXtWtaurErZfJAYhMCsZaYEGl6Yg5vTUZAKOurl8mqRllrTZHlAtSC0B8CQuFeeVV7HzOM8e27WC6kBtAYuk7suQRciyfsSoF65eGdRQukxjmhaANKa1Fr9DYObKE6plG0jiDU3yyTOhJjTOxTaeOKutxGmrwDdPCbunIPw+Fw7T1ukwSNwiQi5rZcLKKyc1TT9hKz61oJHOR5gcuzdQa261qh/haSCa+WUmvrvGMxn9M1DTZNooK6wQcR81qL3YWoYmk0brYgL0Q0jcVcwHo6Wgcq1rZxwTOcjPngZEiep7imxAVFeX1ONjzEDo85//xvMPmE8aNfkBT7HH3wS3xbsXj1MZ0PGJtz/0d/znJ6yfjkMd53VPMrfvd3n3Hyg19y+fo5P//T/5S2nPHy099wfXHJ89dTLi6mfPCzfwAhcHV5ju8CRZbw4w+PmeztM9o7YnY9JR+NOGkDjQ94r5h+/i/54N2fk4wmOO9o2oYuCpyZqAeQpkIBl+8MiU9YU0lj3f7KR1obE+3BXGRXpGvmiHMdeZZjrcE5mcTKPdJFtkaN0YbFfC4BHa3ovJdIprUk1ohiaZYSgpI2WUOeWo6Ywuvf8PlXn7OoOpqmY1AYBvv3yd/9gMrscfHlF5xV11wsWlrnyYsRg+GAvBA7u4Pj+2SZ1H5bI6r9ZVmymM+oq1JKDZQiBE/ZVPxu2qJDS1st+esXn/Hg5Iqf/egRjx5kzC9ecvTwEZdPP2VsS8YnQz5+cslVl5CkAxFltJF90zQEAkZrqrqMGgjCotBKLN+aVuhRbVXRuoYszyVrniRoI4AniUG1b7PsAsrbIe52xmvn49ccYAMLNjBndez4ba9etQ+WbkI0+shivVbYWWedbI2T/NVh1NaP/Un+zkuy97/1xH7n99283hpErsFf75xW0HCnczc4Uq/xT79vtgWWYilARFBGSdAzeIWyRFuVeK8mFptmJEnKwloW8ynz+Yymrhnt7VHs5yK8triUAFcXcMuKrvuC4cEJQ47xviXNC6wdiA2ha9HugkSnNG4grgBZEesVV/yHXg1xPJktoL1VExxWF3wNvNZXYa2Q3wfHinVt9GoW0wMSG+Dc/233mvUPtBrTtwHNXZR9G8S6mV7sg+DVbj0izth2nZRaSepJmApac3RyxJ/96k94+PA+Zbnk+OSIIs/ZG42ZT+dC1WzlGfnJZ8949PgRrpkzVAuy8QnZ4BiVDPuDaTM560dBVK+VN1DtjbO+8Ry4637v3ztboGl3h7BB2Gr3Xo0tXje11+awcwq7Y2Vrv1sN+4Mvb/UM3F3p69r1Ddr9B8tw3Rpg6X+4caW/0zF2g3Wrx9x8Mefs7Awbg/7D4UjmXVrz8uUriiJHa8315TXGaN7/4H1MYjk9PSX4DbM0TcVhJc9z0jznuChQSgL6B4cHTK+nLMsl3sP5+TmvXr1iMV9weHTIwf4++aDgt3/7Gx4+fMTR4RFnZ2dcXlzy4NEjmqpmNBnz06Ofoo3m8uKSLMu4urxCadib7K3nno/u3+Mn7z0g01pqjZua9vqSrq4IStNpR/AGt9L0iEJhSim8Be1tVDL3hKDAe2w+kDm9NpBlaA9dI6KzaEOIySi0Rme5BPDjHdbM55g0xaQpru3wVS0+4Vqskn/0g/d58eIVZV1T1zVJTDoulkuOjo44PX3NcuEol0vmsxnvvfduTOgJYzcgyUyWFYNhwXK54MWzZxwcHuCdYz6f8eDhI8bDET546qszlO9QNgU/JyjwVY1vKshzsIn0UyUK5kZLos6t6rGNgqCwOkUrEZFOixxQJEWOittqo1FpICnGUqaVy3gwaYoOioCPNmIGZYwozBNQZoSyyXaUaPddcts4v/vh+VY3ylvdYbe9xv4Ay1sD7+FgQFlWsbZBBvFyuRQatJIa7b7quVUm0swVeSaU6qqSOgOxtRLRNZQiTVPyXLJ9dSMewjoCz7VKnhIqoLUGpQQAixgX4kkXs8R1LaJkSmsRo9IaZQxlVRGck0EfRCnZhxAVqW2k5RryIocIIo0x1G2zViK/nk1FjClNUdrE2l3JpiujJBihEsq6WltMrUS1hK4uAYLEBmmntSijUUG8yG2MnGktkzABX/L0rJs62nYlBAVGa5azBdrqdUZdKU0e1ZrLRSl95hN8CLjOMRyN6Vq5+X28Tr5XW++dF3CsFYv5HI2JYmCSXTRaU0UbtRWlKCiplU0SoaGvSgRWHteubRkMRIl9uVySJCnD4VBuXtet7bJ8CCSZ4mCcMcw8+A5tMwyWZJCQDPbJ8ncZHf8RbVuDbzn75F9QL0ruffRLkixlfvGS8ye/pa5q7n/4Zyhl6ZolVxenTO7/gKPDESdHP+a9P/oh9eVzrq5mfPL5a/7m75/xT//7/45ikDG9PGU5m/Li+SsyY3jv3YeMjh4wPHzEl//yf8ETeOfdB7x+/oK0GFDXF8yf/Q17H/1v6ZwTv3SbrG2qjNGAilRuRVPV0eJIWCN19Ea3qWUwGKOcxyYGowSAS4mAYblc0tQ1WZ6JFRKKYCTd4n0gzURATB6gbi2GWNeVlEQEGO+N8TFKZhMDJHjXsm9KzPIzzq5ec3C0RzJf8uTZJWHvPbrRY67Or3n6xd8yn01RNuPk0Xtk+QBlLYdHx4wmE8bjCUobBsOhBIuUFkG0mJWqqwobLSa8cwyKAYHA1fkZL54/I88LTucz/l//+gV7g3Pu7T/lH/7Fj3l474TFxQv2H/8x7x9Zhi+f45JDXlcj2jascYdrWvI8j7W6dm3l13YtRhsGMaC3EoZ03pMmybrcoqmbbz33kRsh/qv6IGlLk5xduuY3Odxtk/n13zszrp159tZeNu0JNyaC6rYGrV6IOw0I6iYAXgPwntr0BkKGG0zlrUOoVfB214V6AwhXWHEHR2++62f548H6h9tkhkMPtCjRPtfyxlVaoyJ7SRuNtolMKuK/8+kVTbXk6qKjG08Yj94jT3JMeUU1X7BowHng8oLQVBR7R+DGhCzDpANUmhCaJcFXJNSM8zHzECcxqO2ASL/vw+513XRmUNyucL+mCMN2qkCt9x1CYLFY8OLFC9q2YVlWlGXJe++9y/HhEUVRrD1kt8dZgN1DrhDMrQMp7JQ79JqzAtlb11Xel9fTGRdX14wPDkiTBB0FlSTxIzamVhuC7/i3//rf8buPf8/l5RUP7t/jn/6f/lv+yT/5r/jb3/wdn3z6Oaevz5hP53zx+RccHR0y/ItfgJ9Tv/6f2H/8S/J7vyDE4M0m3NMLlu2e2h0ztPV59Lq8L2Xwxm16X4Q70ztfv4TeKNmJ0G0HccJGGX1D91//elvL/uNYvn3X3Fj6ff7mfd7yALtrm1u+v3vXdz14v+ELabdpAbqu5cmTr5jNZuu5dl3XBIKw5VBUZcXR0RF10zAoCq6mU9pOMtb7+wf8/vcfkyTi6T0YDEnShIf37zMvS/IIjoui4OL8nKvLS5quZbK3x+XVFfePTzg4OCBNU64urxhPxhCkpDNNM0BwRFVWDAYDrstrLs8vGe+NeXD/PlmW8vzpM4wxjMcj9vf3+fHjhww16K6RrHbbiZgwELpO1MyNgSRHOcnQu7YlOIdJUvGR1how+LYW2yxjSNIM39QE5wlaobMcVy7RVon+i9KssozBdfjI9tFZTvAO1zSYJCFosS/l6pygFaN77/Jnv/wFXz57Rts5urYRK9OuY7lccHZ2xrvvvovWlvPzU169esnx0QlV1XJxeQlBNGyyIsNoTRs8k70JwXtePH3CwdEh8+k1H3z4EYW1hLrGt43MB72nLUu6tpYyXtfFd61YGPvoAKWswSYiqKu8sC296zAxGaFUTpLZtRaK1hqTZSSJxSRW6OZNjckydBAfdG0yea8lFl+W8oyxFlQUA75rWT0zb371DZdv/4C4+1Hw/TwP3xp4r6ijIGA4SQVcSLZSr7PWqwyt954kSbFWuP8WS+c6EX+yEnFyXrJ9SZJgjaGJAlOuk9oE77xcVK3pogCV1K11VHVNkYsPb1WV6xoU8UJOqRsRkAgRwJto5m60WBZpowldrKFWGpUoyZJFn3ITB62JWceyXEpmWxu6zhHocJHWrrSCTqjwTd2sBXvSLJNsdAh431EYqeNru44szyU6pA2d69aeeKGXPffe0zY+UtEl8BAizbbtOlFHx6CM+CQqpdZ18sYmZGmK94FkNalDaqbbRnydV97KYkvWAmEdCbU2iZnWgHMdXdNQdw5jDcWgiLZTjiZS65OBXVvGKSWZ1vlsCiFQjIYUg8HaAm5QDECJ3zVWsv7aaDpXkZpO1L5tDkHRlVcQFG01xbcLnKup5ufQVeik4MEf/xKvgKalnJ7x9NPfcXDyHsqklFfPadqOsy9/x8m9Q4xyfPAn/yn4jrNXz/jqy+f87M9+zoc/eMSv/tGvCK7k7PkX/N1vPmayf8BseUFZOd7/458TQkvTeV6/PuezL1/xqz//KYPJEbOLS2Yvfkd2+D4hP8ZYu7aGS6J9HUSLhjiZJwQRSiOQFzlJlpLEh1kbr8PKbsy3nqZrYs2+ZHi9c7jgyWyOcwHfdegkFdGwKAKYZqJ2n+cZKC0qldowGA7Wqv/Waga2o6iecDW7pixL2mnHQu9h3/tHnM8aXv7db1nOptRNw3D/Hvcff8DB4TFpkUEQT3ilNFmeY628nOu6IkkSEmspyxJrRSyw6zryXET1rLV47zg8OOTBo8dS+6/g6vKC+WzKoq74F397xi//CN67t0c9O2Vv/4Srpx9zb5jR1Uu+nA3RRjKSKCX3g7UxAJVR1dWaTaKUKKHrGGyqq5pgDWmW430XRdq+g6656n+8fcLap3Zu/bIjbLW97LwCVuBFbf+8uwvV32C1DruT69vAWH8f/d97n9WmDX3br63jrne3k6ffAWVq3e4eet9dIuBZAbLVXres1XbauH3M3d316NKIpZxSChUMPmhQMimR8iETmSEWm6QkScJsdk25mDG7vqJrh0zGD0jtgIE+pZxfUzlHO69o6kspf6n3KPZOSF1Hkg8w6VBo577FUDMOLzDdO6h8IOe1ojGvOzlsUZ83eEzdOL3t0aJ6VOjtwRECXE9nfPy7v+df/tVf8fr1KeP9CZPxPucX5/zlrzUffPABJ0dH/PCHf8TDhw+FZRY2lHS1u2+9e/F6Yyeez9a43BrzK3EzYVXN5wv+8te/5ncff8qDx4/5x/+bf0hdS+nToMhRKLFGjFunieWXf/on5EXBxcUF9+/fR6nAb/7u7/E+8F/+V/8Fn37yOb/+y1/TtS3T6RxrE5L8Haqzv2P6xV+RTh5hintSx7lqNj01c7U7lnbE4lYhptAflyu3ge3YRD9usaH09/e2fSVlO7WKftzo5802bMgH64PtPJPU9omsR1EMpm2OtfsU+w8LvnfiB6u/2P3rG8LVrUfg2698ywF3t38rIL9acXdn3+wsbg11hcDTp094/uwZ2vz/ifvPJtuS/LwX+2Xm8tuVrzr+nPa+x2AwGGBgSVgSl6K5EsmrCEYoFPeFdF9J+gz8BrqvxKvQCylIhSQ6kBckQQNgQACDsT097fv08ad8bbt8ZupF5t61q06dnu6ZAe6K6D671l5r7Vxrpfmb5/88itksZzgcslCyaTWXLl9Cty2t1vR6XZrGZT5N6yR9j46PCMOIbrdDlqYMBitkaYqOYuIs4/DgkKquuPPJHcqyYGtri0srq5wMh6RJQhAGrAxW2NvfJ5/NGI+dEpI9OSHLnFxpr9en33eIwdF4RNZJ2dne4WQ4JI5irly9Blh6vR7Xt7e43IuRrQuUG63RxcyxcePkrwwOej5Hu6IC2tnM+wIOZShCx+GkogRrXBmtwUn2Wt2AdcheFScOZu5h18YYdFM7J97LvOq6oplNCDtdXwMdYFWAbgzNeIyIj1jvrrAXhkwnE6b5jIcPH6GUIAoiNre3mUymPP/8CyRxzP7+PpvbW4tkQT6bIqVwJbdSMBlP6PV6qEBxfLDP4d4uq2sbPLp7l61OijAWrKEZTzAYDAIbRNhWo4sSnWVIC+V0QjEaOq1yFSCVI5lTYYgMQzCObC70wde2qb2d1joeFMBq7cjr6hYZKHShIUkJ4oggjLC2xbYtMorcbwQKGQRPHRjnLBJOJ6En7ZMLO784PxyftLieDDD+1W+fq8Y7jn1m2ddjd7s9yrKgaZpFNmm+WWMwQlPXjrY/CB0suWkaxuMxxjqZKhU4R9TB/Ry5mA48RDzEZ0PNIhOstYPcxlG0gHnHkSOZamiom9o7zoowdc5g0zZEYUToB0pd17RV67L1FuLEwWPbpqVtW6qycrAan6WcBwd6vR5hEKGNO87Bqh1DY1XXjvVdOoeyLAqfKQkc3FwoD2t3L98YQ5okblFuQXpJNulZDXXbMp1NGY1H9Ht9H7BQGCxt09B66E/jdZARUBWll4KKCYV7Z23TIIxyhFJSEMUxURRRWzxboSZQAa21BCrEYmia1tVx5zlNVZF2us4ZD4UPIjiSK2McA7wKAifp5GvW89mMJMsYrK45ZnnlShI6nYy6rjFY0ti9s1a3LpBQ1cSRYq0XYXXptJVl4FIbUlJODwmijGr4gGp2jErX6Gxcps73UUowPnhAlU+5+fovUNcVYZphqjEP3v0L9h8/xgpJd7BJlHbIx8fMphP6q6s0xYzLN58lsCWj8Qn3Huzx8KhlODng1pUBKxt98nzGyf5DJuMxcRzw8MEuo8N9OqubHDy6T2cwY+XKx8RXttDWEsWJh3q7icsYS2tOx4dSjmk/TVN0q4mUC6q0rUYZF/SZ61erQLmscaudfqQPCDlyvYKmbkjSFATUdYkUkjRUaKOxUqG8tJkKlNeqbKmaEoslFg2b4TG3v/9NZrOSprUwuE4uNvjo9h2Gh0eURU6c9Vi/fIWN7UtsbG4xWFn1gRo3dsLQ8Qg0TY022o0nqQjCkExK2qah2+17OT29KEnRbUPrdeqN5zJYW99kfWOTosip65K37hxz5/4hz1zp8vyL66xfukwURlxZq9g7OWTarDlZvSWkihDWBSqCkMYTowgfmEJJTO0WjkAGCOE0NkFQ/QTkamcm8zMEUxeApe05k+tTcY6nc+qpr7W0bIil/844WPN6Wucdnfpq85rasy4DS47s3BGet9hlU0+vc/6+Fw7x0oo3Z98WVnBR3vmsEX3eKbtgW0p1n1aan73fU0fb/3sGY/6kdeySwQoh7CIlKefOh+8zUsRunQiW/otigjAinwyZTac0TUOn16XbS+hGGcX0hLaSTMopps1pmwZd56SDTbeWRCky6rj6ONMSWIs5/jbGvITqP4MQ6uwzmztPS0GXJ4InS33urPNx+v28f2iteefdd7j/8CGPH+2ijWGwOkBry7f+4ptMJhN+7a/9Gt/4oz/CGMsbb77Ja6+9zle+/GWiKDzrT1zghzz5Jl0A4An1r6UL6VZTVhUnozGHwxHj8ZhGw9d/6Rcp25Z/82//A48ePuD6tWv85m/8Nfr9HlI41YM5HdmVS9tsb23Stg2zvODf/Nt/z1vf+wG6bfmN3xa88ear5EUO2vDqqy/Q73YJAknvmV/j5K3/N+En/5W1V34XIYIl7ppTJ/rJ7WzAadEjl/ufOD/KTj+cveR5L+3sAYtZQFxgNJ5OPP5VW86evzQullAhT849p+NwEQD6aTvbpzGYz70J3Lk/0p/9cbbl1/2TXvzH9vw/y3EXX3z+bJa/ns2mPHz4wNl7TUtVVTx+/JgrVy6DR6jOyctsWZJlHe7du8vw5ITNrU1sZbh75x43b91kdWWVuqooqoLJeIqxmrIomU4nDIcjpHCkrf3BgF6vSxAEbG5uYI11NdxJwmzqgl29bo+0kzGZTBw8WhvSLGV/f4+yLHnm5i2kFKytr1FXtSNErWuEhbU0RLSOR0jXJc1simlbdO2CB8ZaUAGoYOFTGN2ishQhA0d4rELPUC5p2ilYg7ACGceOOMyCta5+W1jl5IWtQVs8Ale58aENrXG11DKMHD8ShqYYE6YptnUls2o6QoURgdCcnBwzm+U0TcuVq1dd0jIMqeuKsi4ZrKygjeH46IijgwP6KyscHR9T5gVma5PBygqDlRXKoiCVCTs7l3hw7y6He3ukQQCR9Pfm5L5oNbquqGdT10mkJEgyz0OkUUFEW9fIUCKVQQWOzM20DU1VueehG1SSkIQJ2mqHgq1ropUOeF8wjBJUFGFMS5imICW6zEG45yqCEFqHNHiazbNkIf3ocfG0MfbUU8UTh3za+ae21tOv85Nsn93x1pq6amhaF3WOVLRUh+0c4bmD3XpZLXCEXVJItGhBOXhLEIRI6TJQUeCYwKuqXNRhYF3tttYtVVWitXbMxGHoJc3EggHW1YY7OZHZzNUSC+Ec/6Yxrq7ZGkTknqkz/O1CR7r113bQVIGyanG/Uira1pG9xUmCBqq6XOiBGmsIw9izjVtk6LKZTkbKsQSWRbEgX2s8WRoCf77vat5ZDwJHMjZ/fnGS0DMO/m2Buq6wBipT+oymqwtv6tpB1cOQMIwWWajGk6hJHJuhsA5yM8/GG+sg723TOL0+0dLUDXk+c/c4z0xq155gqcY8z0uyTsfD3PH6tC7j2O11sdYjHYLYZ2mNK00QgihNqeuaIneyYnESIyT0YkOsSkTk4Mrl+IB8PGL95pvsvfdn7Lz0i2w+c437P/gvvPUH/5JXful3Gexc5Wj3Nnv3bnNw/y7T4Yxk/TobN1/n4OEdvvGf/pCdW8/z/R/c5h/9H/8Wuhzyyff+mFlRc3wywVh48xd/g9nxPU72HrO+vs5Xun3KKqebdbj8zHM0TcEn797n449us7O9xi/+0peZjcegNW1raGbHTB6+T7T6HDbZcGUHXmex1ZpOp+ORGh7641EFxuvJWguT8eSUfbttPHFfQDFzxCMqcBOeCgKUEB7K7YIl80yqg8lKjG6oqwoZpdRF6QIuUYRG09QVpm0JpWJrYNAnBwRRgigNZFtMgm0++fgTdj0sSoUxndUNNi9dXsiXOcI9RRKn/h075v0gDDy8raUxDaHPINd1jcCSdnooKWiqCo3x9URunMwl0YRwQSkpBN1OnyzrUhU57z46Qcv7vPDsDrqY0hn0ScoPeXx8SDG7TNbtL2qipJRYUVF72ZS00yHP8zOBtCiO0cYQSLXgPpDqJ5ETg/NT+tzhFmfgvqffzf3lJ8/89GueWbPmPuOZHe7Cy9DgJ4189//ldUqc/frUgfAGu13+/mxYGrALXfGzcQD7xG2cae6S43Rm57ltkXk8jSCwuPkn9p+71Pks4RP+1FwbdJ7Nlaca6sIsSp3kXIEhjIiimDhOmI5OKIuc8XBEm3XodbaJRUSgjqitoNYV7STHUtE2j9B1RTLYIbSSKOs5ulFTQdtgjt/CFPuE628gw55DNy09AXthZzlvjC8/zSUHzH9sdcu7777H/++f/3PWV9c4PDxkd3eXX//1v86v/fqvc//uPR4+eMBrr73K9WvX+Wf/7J8yOhnx0YcfcO/OJ/zmb/0WKysrp8GWix67nbfiLGbiTKrXClqtOT4Z8sN33uPhw8f0Bn2OhyM+unPPl8sYnr11kxs3rhEqxa1bz3DrmRsL5J21XnPXzxlzckulAvKiYPfxPr3+CnHs1q3DgwPufHKHYjaj043Y3trAEBCEXdTm60yOP6ZfDpHx+uIeznXCp9pd59z0pfv9EdsThqJ90oNCnIWqX+CjP3lB//vL/fzCbPnTmrU8bu3FY+vH3D7XVc49n6e7nj/h9sR8du73L/pu+fuLvvuxHtenrQif4/1Zy/0H9xiPJ2RZ6iQ8j45IEicTVtctTdugWyfHm3Uy8iJHa8Pq2ioIxfHRMds7O6yurHJ8fOSQoaWg1+lSlCXHx8d0um6emiNie72et88dd1BTt1R1RZaltN5plEKwt7uHEILRaMTWxiZBIOl2ulhjOTw6ZGtzy/MuVYxGI6wx3HplhxgDSExb0U5HWK2pixlWe8bxKPbwaetqs02LDCL/UIxTZsHZSrppFhlvB3wS3sm2jkhaSFeK1rSoNEVYi25rhFdaMqbBti0qiJBhhC4LjHLknE3TEAgJs6lTXYkidtZXOZmWlFXtCJa9nnhd16xvbKK93b++vsa777yLChSDlVWytENezHj0aJeiLFlfcxJko+ExK4MVVlZWKWZTOoFDSTrpxJJmMqaYTsgnE3RVgRAknQ5GCKd0ZA26rTGtRreFm0fbFhEosqy/KK+KksSvRZYwjEEIB9lvHTcRWIxukQ0IpbBt6xx+qx1RKSCDEFSACkJEGHno/mcbD3bx/+Xx8RnGABcFPc8U4fwvtn1mx3u+2KVZ5gwrIaiqamGIywXD+anT6hzT0DuwjWdydlrRczi6sYZimiOFpN/vI5Ukz3Nms5mvJ1ALXXD3+K2HsLtMuLXOwRZzTVYhfb23JY5cDUJZlgtCgyAMfRbSkbXFiWM0bbX2jr6rGzfWyXZFYcfVkuGc5SROXLDBuox3PpuBcNnjeTZPKkXkIes6dQ5p2zYLKLvWHpYxh4urwDmfQlAWhc/iu3tcWVlx7Oe6JY5iisJpcsu5fNKc8M7Lo1lPUtfULuBhhVmQfBmv3S2l8oGJOXTfvWOlFCJ2wRIE9Ho9BzU3evEMnVSTc8JV4N+5r5Gda347WLl2Wo1KUdUVutVO6zp1zlrdOgc/8DXeaRSzmnkCPV1Tzk4oxscgI3RTsHLtNYzRTEaHjA/u8tLX/xvWbrxOfnSH7/77f0ldtvS3b/HF3/xvWN3coho/Yv/hAwbrW7z/wWP+5t/522zvbHP/h/+VRw92STdv8L237vB3/7d/jyxL2f14xL1HQ6p8RiAKNja32b52k6jTZe+H7/PJ/QkiSBgPZ3R6PdJOB6Mbnnv1DYqTh5QnjygOPya5seXKBvx7bhv3LtwDlgTSQaqrqgTrDEbrM7XWukxtZGukkjTWEiURYRAgswzdNAhOSdFaD+vHR2KTJPXlEAEKR8gnIkAI5+DWtdOJ7HZpyxmRHmGlJEoywkZxorbYPzjx2SZNb3WdrDsg7fRI0w7dXo9u1y2scZQ4dvrGlYcoG7pMi4HAZ5qN54PIOl2K2ZR8OvEEa5IgjLxcniOeq0sXIAvCkCTNsNaQph0vsRcjsDw8rtnaNqx0+jR1wfbmGsrscsyIo5OaPE/pDQaub3pG8yiOCVRAt9fHeG16N3ZcPy+LAmsckV2UxD/+THous+V3LT44/3CeEbvgdOsc109dDuypXJDf4d6tXYK8nvGgOWukL2fGWFrMBIu5+yI2ZpdN8aDZc0b/+YXtbLxaLNo9P2DJVTvz6eIA9VkzWyz9z0GeJUjnqCwy7Mu/P88OOq9jsYDP34U71kuKnZL9+/twSBusQaBdzZqUKF9GFIQOEaWimGB0zGwyZjIeUtcJ3d6AKFNEUlLnI6qq5XhU0+0oEEOaVtNd3QEgiDvIIEEYCKWmrXZpdqeo1VdQ3avAXPbMt9hywb26ezH+Xs49ROZ1CBbL3bu3+cM/+i/sPd7j9Vff4NGDB3zlKz/Lb/z236BpajqdLmtr67z//occHh7xa7/212jbhj/8oz/mt3/nt3nrrR/wta/93ILLY7m/nL4fC7gg9NxBn7fVGkvdasq6otGaWdPQX1thY3uTvKj45MEuutEYLLpt+ej2bfYODtnaWuf5Wzd47pmbTmJn8X7dnCOQi7lUG83KYMDf+lu/w+HRMVIIrt+4Sl071NHe3h5vv/Uur7z8MutrKxgg3n6Z8d7bTO5/h/6zf821f96Dn9pJ8eun62d26X4XwYeloNXpUzgzgDj9Vrhz3YA76/gvnyKWR8W8H8xB7WLp+/PzhThzxtnLLsu6nf/dn6KRetHcx9n7ufB4e276+pQgyI+9Ldv25xv0tN86T3TxtOt9rkZ8/hs7nfPcv7PZjPFoSNZJqavG29pw5coV6qYmUIrRaIQKg4W8blVWnAyHGK3p9nq88srLlFXN3t4+k8mY69eu0+/3HC+RdLXZQRDy7LPPMp1OuHf3HuPJmIP9A/I8Z3t7i/X1Dfq9HmVV0e11ETik3srKKqPxkDiO2dnZ4dHjR7RtS5qkjEcTVlbXGB8d8nh3Dwm89OIL7HQilG5c1rSpnboRYK3AYBFhiK4bTF2iLYRh7AjGhBsTuqwxGFQYI6MYGURYDAQJVjvpX2sNSIEQTsGHpsWGAbQtrdEuedXWYLzdLBS6qTFGe/UgV1evggAsaN2iy5J2NiZdSRn0+8jpDBkoxqMxaZpy6dIlptMpIo4IlJM9ffHll9jb3XOa6EajpGQ6nTKdTJlNp/S6PQIVsru7y9Vr18mHIxIMom0xVQG1QxMWoxFVPsMKkMplnrW2jqOqKJFCISJJnVfUeYGQgiSO0HXlGduFD0gLp8kdes6TMAYJQRQ56LiLUxBEsXO+sUjjkkggwLTuACmQYezX1if77SJO/zRb6NPG6OcYJ0/dlq/7lzHH+O0zO95x5ETgy6JEeiKxubEmvSSY8lTznU4X47O9UsqFY8oig+pg5vNFq9PtIv3iVXjHM03TxcJqfRa5bmry6cwZ08EcQuuJpqx1bI21gz5jHDN3EIakqXME66bGlk4WYK5J7WrES4IgcuzpntwqUCHWGurGRfLatqEoCmrl6nftfHWzBt3Oq8JCjAUlPdthW7s6Ei/n1TQ18zc5J5izLUhfI9/Uja/1BqRAG0cWV1XVor47DAOaxtWkTGczV/OWZURJ4mrDrUEasWCCT/xzdOzhwstctcRJDLVFihAZx54BHlcfmyZYYxcMjEJI0jQ7zUb6gEhT1d5ZsS7w4XFgjjgj8rUxbgA1TeMWdJ+liMKI9c0tQhUsWKWzWCCNJp+eUE3GSOFqnh/84L9w7bVfw9iGR+/+Cd1rX2Rt5yoikNz5wR/xnT97h9/+R/+IV7/263T66xzee4uH736Lg8f7PLi/T7qyw9UbNzFtw8HDBxxPDd/5o9/n5dde5uYLLzujeTShsiEP7u+ys5lSlQ37jx/Ry8ekccTXfv5NvvGNv6CbBrR1zcrGNvdu32Zja421y9cZHTxi+vh9gp03EXEPjaVuKoxp0dqR1aVZl5oKBF5GTHuCQeesBoFCG4tQMQqYjUZOUi61DnJuXT13JKQzTk2LNpY4chk4F/gIQGpiGTObzRaLiW4dB2dTO8RClsZEUcLe0R6jkyOmapvj4YTjowOauiGIEvqDVfqrG64WMo6J4sSxtCeJ05QsS7Rv/1yiy3EmhMRx6hadpnWEc1G8SOQ0de3JTVyQyvjIO1hqU7p5wte4ByqgMZo4ybA64pPHJW88t0YUCzrdjCS5yhWV8o0fHJLPmeCjmKyTeVSMk/Wb15RXldOzVIEbb23josxhHBKa6CeYSr3B+5Ss0nnW8wsOePpCI06POf+bF5+7lH9bNpyXFpWFU7C40lJ29IJ2O5iwW4QXP+Hn5tPMOGDFubVrLmV12q6n3MWZD9oYGt0CgiRy5F5niNWWC2/FXMZq6fPyo/DHXfQYTx1Vceb2LU4T3lrhCb2MQw0Jx5MxD66pICQKQoIg9tnvmSut6PXoZFeJfACsqkom0xptKvo2YKofkg42SfsCZROkDBEyQrYF1ubow+/Q5ntE668iVHZ6I2L5Tk5rcu2Ze1l6HUv9cXf3Id//7p/z6OEjJrOcbq/D/+n/8n8m7XT45//8X/Lf/YO/z3M3bwGWP/3TP+UvvvlntNryhS99gcl4xH/6j/+ZX//N3+DDjz7i1VdeQYjzz1SceR9zZ7Q1jmS0aTW1z+pYXLBx0O/R73URQvAn//WbHB7so3WDMW6tuXx5h7ST8eD+Ax49eMD66go3bmSOq0E7dZA5qeucVBEcp8ulS1tsb286JJ5SKCX5+a99BSUMN65fJ45Cr5qiwYDpXGV0589Itl4i6l1luUMsnNlzw3sxPpb+v/yerD9meaSd3xaImPn1xdOOfNKXO9ucU4zBGayBOHd9zu32N3Z6L2JpnrjgxJ/y9lmvvJhXlk84Zxz/yGv54xflPp/RsF6KHf7ocy5+OU9pzOfZ/5RDzk3bWmsePLxHXTvi35PR0JVX6sZJmEYxjx49pKwq1tbWsMbw6PFjjDG0reaZZ26iVMD9Bw+oq5qHD+7R6fYQUlCWJfmJYxxfWV3FWsdgXuQ5YRAwHo8ZnpzQyTp0sg5pknB8csLu3h7GWIo8Zzwasbm9BcDa6ioHB/uMhiNWV1cWJZizyYTd3X2kcLbTta1NYqNd0NHibIamRleubI4wpMlzdKsJsw5Wt+i6Qte1e+WeKNORijon0hqNbZ2clvSEzEZrZOA+oz3cXM0DvcJB0FsNUmBb4xz6usYaQxC6mVgI6X6/dCi7psgJqg7KtLRVyWg8RkhJU9eUVQXWelu/pCpr6sap0yRpzO7jXZI04mD/AG0MSZrw/rvv0s0y3njzTRCSTz7+iKurK0RKYrVBVyXV6Jji+ITp8RFGSnSrUWFLmISIIKCtK9rayZQJH5gIAkVTlrRKIVOPOq4aGlEQd7oIXy6LBNM6UjpdlogkQSUxSkhH6kaAihJkKAmyDCHd8xBoCCOUV/HwS8XS+rXcpU/3zAOLLB9zUWDsxwp0/S+zfWbH2w1iZxwbDa3QCCuIotAziSvCyFHEu7pKV088lx8zWrva2yB0OtPGSW212i16tW4cGZg1PqMaoIRcZGXnDIiyqxaM2VhnHNVN7T87BnQlJVZ4khNPRlbXNUpICL2EFY6ITM3lvgRkaYZJ3KKttQYhUB66EQQBURw7I0yCUiHaaIS1tFVFGLtsma5LqqZBhiEqcob8qdxTTRhGi0yxbt3587bMITrWWpQIsNZQ1TWAc7q0g3bEPsCxsrLqM/0ugyekq1+1Am8YCl+vMs/0W7JO5Ou09WlpQNPSGpfBj6PIOdjawWDy6ZT+YOAg1bohkC6qF0exa19eEEROB73VrsZbBQohlat5EcbLaEnSZEAYOoKrSCryovBagY6YzuoWa2ua8QEqWqG3eYNitE89nWIsFONDot4qtDlKhRSjA3bv3yfp9Rgf7DIbH6Hbmnri6mO6m9f4+b/+DK+88UWsbRgPD3j3hx8wzTWXL6/ws7/yy/SyDidHDzgez/iTP/42R4dDbux0+VLaY3DpGqatXNROQhYprl5bIwwEJycnBFnK4zv3uPbMVYIoBaux1YRaOPh3oAJk5MgEu/0+1lqfdXGSX44wTy1Y4YNAOaIv/1yiOMJGEUmS+LIOR8I317+0FsIgQBvjECLKkbcFyhn6rddynM3G9PsrTt8+jtw7iiNkGBB3euzcfIH75SYif0TlWddXVlZJs4yV1VUH405SkiT2Ul1O2m4uB2iNXZBPYa1b5OYTKy441jQ1GO9oW+vHnsRo4ZijI7fIWaOpK6dGgK0htJjW142rgKoq+Oj+iBdvDMhWt5gdPkTrgq4Ys3s0o6lr+oM1V0vr68PCyJVf1PW8Lj2mqRtiz3cwj/jrn4RcbZ4n+rRIrLVLxEdnrevza8UZA/PzRl7nP3LRQrTsj5/9tU+74MW7F/Xi/s8LsNBzp/t02Vx2wpcPtWAFjbVMqoaqrpnWLdoYtgddZnXLTi9BibOc52fbc77Vywv3osmLDOT5Uxc5QDFf+E8zkVY5ZIGYO+GeR2ROiil9Bnw6OiKfTRkNT6jTlF7nCnHUQVUnVLMpeV3RjgqyxBk+dTGmt3GNIEmxViGCDHSBNDVmepuqPCDc/BIq28YYTVsXKBV5+ORSW5+opV++YeecHu7d55vf/A69lS2i8ID33nmPv/N3/jb5LHdjwaPapJRsbW/x1a9+jcl0wsH+HmVZsLO9wcHeHhLLs888Q5ZlC6fSxaANrXXGe900NK3T2lZepWMRhF566A4i7vgnXnv1Jfb3D3hw/yHGGrZ2LvGrv/p11lcHVHXDdDJlZWWA4JTRHMsicIsQrnzLutHorg2BckG3oqh47tmb3Lp5zRF7YplMZmRZgkWgBlepDv6Co7d/j52v/u8QMl6081NR2udhIEt/PzXe9qOG3BMenuXMHnv2Ek8lQjz/5/n56WntOBerW3bG/0q3edvn7bFnd3/avHjhtPkp8+3TAh3zn7/w9u1TLjjf91N6Zp/Hp5iMXdZ5bnOeHB8zHk8IgoDReMxkMuHk+BgQXL50mZEvc8vSjH6/Jcs6nAxPXLKtrOh0e9Rebzu3lm6vS1U1i8BeWeYuM6oUaZLS6/XZXF9flNhNJxOUVPzw7bdY31inblukDBiPRkzHYwaDAc8+9yz9Xo/jkxPCIMAYy/Xr15ysr9VsDToIXUEQoeuStphh2tbx2KQxuqkJuz2U1iCk434wFpV1AGjzHKTFGo0JXLutxbOZJ1jdYL2S0XzqX/AxeeUTU9UIL89rWscQLrxTC5q2crXgUjki6ciznSdZF1NXiLqk1+1wMs0JwgjZ7WK0pipLOp0OURQBE46PjzzyVtDvdzHacSpdvX6dKIroDVbY2dyk0+3R6w+IwoCwLtFBhDaO3Ez0V9CzgiaMyCdT8vGQrNcnSGKQiuLoiKYoiNKUtqoWdlDU7yMDRVuWSCCIXGLTNA3poIOweI4SVyIsRYAxjYPqR8qh76rGweaTBBqLDEJ3fBgjohh8yd+8Q18Qinzizwv7/Y8YDBd//RlG0F+B4/6ZHe/h6Jg0zVzGy9cwB56hO4ycwb3IzILLZPpst9aaMIoZeKi1xbFnBypw9Rk4Dd3xeEwUx04ySTjoGEK4DLL1mT2vazeHslprQLj6Z4kgjF3na5rG1V17R2EOkbfWkiZex9vrhDvjyRlRc8cfa6irkrZpF51yDgeOVIjBooCqmKGKwuUaqoLUNJT7+7Qrq4j1HaT0IEEhSNIMJeUpY3lTI4RkMpk4goLAOVXgxr7WrtYkUA7KP9faSxJHpiW99qsjyShdXa90kmhKSe/wu0kjSUJQjmxC4NjUG9Ms6rKdJrsPkngd8STJFvroWrfUZYFKM589tCgZEPYin2WMoG4wbUtT1S4T5KEz1hp63Z5z5usaIVytiPJweeGKS8C2EKZYoUgGW7RNhYoyBldfoM0PyU8eUc9GdNe2mRw/YveDP+XhJw9582u/wOVnniXtr6LzEx7deZ8Hd+6y88IGly5dpre+zcHH3+Ho6BhT56ytbbB++SovvPI6ux99m/FoxF/86Xc4OT4hSSLefPMFItXSzo5dTc40x2B47pmrrG6tMxqNuf94zPogZGN7gyovyLodjvb3yGbHZINLPhDiZCVa8DAkpzUtpSAIE5fRb2oXlKoq2sYFhaRSRJFzCquyoG5qsizDWlejn+dTX84hHVxz5lAgcRyDtTTGmQvGGJQU9Pori3HgtOhrjNVEccrWM29w553vMp5MKKsKpSSxSkk6XbJOjzAM3aLg9bmllFRVhRCQJh2XBfTZ6yBQNHVNU7q6pSBwpHq6EY5/IAgx2iBUx2WarcUKTVUWWGOJk4S2qX3wxnELtLpl7txHnYQgCJhUOfsnBTsbG+h8zINPPmL33l1yvULdaoIwoqqcQ5T1+m4BbZyjnSauVETgJN4sDhomPVT1x928P+c+iyUn6Ix1/DRLzL2vp9poF4V5l7NkZ9iQl2xB8eQ1xeL7ZcP8vMN8mvl6IoM/jwjMs2Pi1Dh90vE7NWeXM2fnla3mXxrgW4+GHDeGy6liWhtqYxibGYEI+GQ05PWtDoMkPkeO7i/mIdWnL8KeacX8Nhf3b0/bffYJLrV/sUsgUAjpIO5CSEfuJU9rvwMVuIBUdMxsdMJs5ojXBitrpL0uaXBIXU6oypx2WiKsQpsTTF2QrV8l7q0hvYSLDCWB1WhT0h58lzK9ASqgGO46HfGt58kGO6jABaTtcqbfO+GnwRsXfBuNh7z77sds7+Q8c/Mm3/r2t9nb3ed73/sOP/zBW9z56s/y4osvMsunSKX4nb/5N1BS8I//8T/m8OCQBw8fIWVAFIQcHR7SuXEDKSUnwyEHh8eIwM1bj/f2eeed96jKitXBgK9+7St0u50Fqs2VhM2zwZJ5VfrGxjq/+zd/i5OTIcZa0iyj3+v40pbIfcb3a2NBnKpFGI9wq+rG6w8bNrY2HbGpv74jY3X8Lh3Pfh6EAaXnOYmzNQq1gZwNmZd1nI4Mu+gFnNu36HNzL/spXtKZUXbmO7vojxduc6j6khd/hvjsokF+7tKnw8ye/e4zWJkXqsP9VW1Lc9/yMxAXpcmefupyBOHcl2dPeOKS833npvPzM/lTm/HEe17eeX7e/YzbcluWlwHAGMvJ8ISyLIljV5o4Go1ompZup+slNI1DfMYJ3W6Xo8MjojCirEqKYubGj7XMplMQDlKeZRkbmxu+7rqmriqatmV9bYXRaMh06soG8zwnjiOGk7FDbXokpdaaLMvY3NikaVs63Q55PqWuatKOy4wPx2N0q6nKyvHLKMlwOOTGlcsk1kBTYVXgkjoIZJJhdYPR1mdVDe1siggjZKCwUjiJrLpGRhHGz4rtbOZIf+MEFUYeDu3saaMbTOOSYMIH5LG+nls4+S1rDW1ZYFqHIp0n+Fov/xsK6eroq5JYJC7z3kSoukGJgOHJkKapWF/fIOt0mNQ1ZVlydHzE7qPHpJ0U5ZGg+TTHWnjzS1/kwb0HPLh/nyxLGayu8Xh3lyiMCMKAqix59+4DBLC6vs5sUtHKjGm2SdrZoA0eU9QlfevsteL4ANu01E1FlGZIa5GBIvC+nBUQpwlRlqECxzFlmoogjNHFDKxBxTHG1rj51aExVRhAkvg13iJFAFZjtQbZoLI1VNJ5+oD5SZxeceHHn+L2ecJfn759dqh5knhCmfi0rslamtrpUUulCAMHBw89VX9RFovoryM9ElRlCQJvWBtf/+0M7DByTlxd1YsJ1njiJSFajIeeaq1pmto5fL7IPwodG6ExFoHxzmBI68nf5rDlIAwxeMbvtvULMljrSOG0jxLWXje71+uBEBRl6fTIZ1Oq2QSiFIoxZjQiTFJ4eBt59JD83j2aWy+TPPsSFijyfAGVV0oSBpFj8raGKIqxeBIyD91vfbuMMbR+AmiMq6F1z8MsmNu11qRpwpwUaE7C5pjfnS65QCxY2JVnnp53Hm0NutbEsZdPa0/J7GSSoHXja7rtgrQOXHYz8sR0QjgYObjaRyEVYeCcntCT6DW1iwy2bUNT1yRZAtYQSJ+lN4ZuCqIeYm2H/tU3KE7uMxsekm6+AM2Mw7tvc3z/AzZe+BrCCk4ef8Lx0Qlf/o2/Q6+j2Lj6LNXkkEe33+Zf/NN/x9pGh95wn6T/JXRT8fCjt7l7b5ckVuzv7vLKz36dILAcPrzPcFKxs73Bx4+mfPUXvsKbP/clxgeP0Crm7T/5Pv21VXQ148rOFivbl1Fxwv1vvM1soOh2EpQUGAsySmjyEfnJLp3BFla4Z5SE7hkqT9I05xtotSYIQuLEB1h8vRVSYBG0jWPYt9Y6EkIAISjzkl6/R1W54FOn23GLW6sx0gWUGg8janztfyClc2SxtHVDowIaDQGC/qBLdJwzOTliOp2QpD2iKEYFAbPJxNdcWx+kcpwLYRhSVqVDbxhDkiSApa4rqrICLLZtiOLIcz24Ntbesa5rdwxwWv+8UC3Q0DQu+FWWxFmGbhvvoLuM2uMhDDo1UjkikTfeeIE/e+eAhwf7tK2mv7LmanF9AENKyWw6wbQuYBEn8QINUlc1YRR6+bcfc/M10AtbfCmiu4CpsSS9JU7Pm5txywbuRUbV3NSef3dqti3zWZ914S/M7CwXiV6Qzjlj4C4cgrnhe75hp1JGT7TfBxwv2pYdGuGfwaNJwVujkoPGctIoOlJggAmGV9ZC/tk379NNr9ErDc+vZIvfXTiYi5rupadxwWIszu14woFZOlEsoxT8W0RphJAYqZAycjWPyhFjqiAkDEPiIGIydtDz0ckJuj8gyy4TBccgjmhKyShvSGqDsTX17idkxZjO2mWEcCSkqAxkCOkOUiTotnRBy9hJJdo2x8qUwcZ1BwE8v/m2W2A0PODOJ3cZjcbEcczf/wd/n3ff/4B/8j/9E77yM1/mH/53/5CtSzv8z//u3yGl4M7tT9g/2OeLX/wCG+tb/O//+//eEXIiUFJydHzMlatXefjoMf/63/4+s6Liaz/3s2xubnLv3gP2dg8Iw5CNjc0Fn4u2jp/FGO2uo+SZGmgspGlCkmwv+C6k13u1xs0/zhZw62DgkV+Nbmka99+9e/f54z/6BmkS87Wf/ypXrl6hLBuUdCg7rQ1xFDCb5Qv0TZHndDoZTWsJL3+Z5uEf0hQnJP2U5ZHnZMFOK6GXIeJnEp++P9ulkbns+C20sxd9z4d/nmqMXlSe8TQr82ypx5OHiTNtPdvlz/3K4ssLsRQ/1e28I/vEtnA2/Xzx49i+5wISy8/h6XPDuTaef54/sQ3+2Z7sRfP4E1eyMJ2O2d177Ilzaw4ODokjV5o5zWesb2xwfOwc7ZWVlOl4QqfbYTQaMTw+oZNlJLEjv2rqhoPDQ9bX17l+7TrdTof9/UPK0q37Ozvb9Ho9JpMZxhjW1tbY2tpib3+fJAiI05Th8Qmtbsg6GYOVAUnskJJYw87ODtqjJD/88COuXr1C3Tgm8F6vw3Q6xVrDpY01graCpsEEDbQ1pqlpfcmascJlWJVEJql3pluEkDT51H32yStjFCpJCTx3hjF6MU8a3fjyFeGc7NqVR1rdYrRF6wZdVqAkWjdgLNZ4+bWmwRpNGLmMvBDSEdo2DQ0WFcdo3dLpJqxvrCGF5PDoyAU1Oh12d3d5cP8BVV0RhCHd1R5RHFOVBWVZ8PZbP+Du3bskScLmxgbvvfceGxvrpGnKyaMT4iRmNDwhjlM+vP0JjdYEgSKOE3a2tznpNhTHx+wdTUn39hBHByAMVlvapnZEyliCTgbGEna7SATCCoxuUHFKGMeoKHQZ7CAEvOJTx3FzWSsQgYAlolopBSJIEEpBECLi1BGrfaZefkGPP09w8dPcfnp+9Y/cPrPj7UiV5CLLbI2r611kr4MAGThHeB6BdjIBGXVduRqTtiEMA6IoWdxkGEUeoqxpjMtKBWG0gKdJKZjNcldLoR2zcxQ5CDuCBakYOAM38BJezhmEUM4naZe9KIvcGd5x7KDjON3QOdHb4l5kujDYjbELiTLZFDTTCWJtBzEeYvIZIRrzzrcoP/kQ8+pXid/4WZCKfDp1Ot/WsVVHUeTq25XCGFcXa3TrCRScQ1FVLoKkVEDacdEn6bMCde2yglq3zGZTn4E3i6y3wOmrS6kcbFYIlBAQBtRNRaAdc3SrNWnWIQlSL0fWoMFnDZyzH4ahr21vfWYdgk7H1xlrprMJTVv7TKiiyGdEcUKWuedW1672t/aa4XNiNikVdeUCMo5or4M2mm7o4JuVMcjZEceffAttM3qXX2V6sEe0coVe1TDYuk6dj3l8+21UGDPoOUb0IFthvH+H/Yd7vPvJCS+rgCsqZO3yDSajPabDMUVRcTypee6FZ7l26xlOHn/CcDRidHREJzb8ytee55d/+auMDx8hJDx6sMv3P9ilmx1x41KP55+7isLpSb75xZd4dPs9miqnv33FBXWsIUwzkk4PY1uiMCXCRRjnEi9hHPrnGyFEixSCtpW0pqXNC8dPAOSzGWknI/KQnqquiPyC4oIg7t1L5cacC+g4mbmqLJBC0V9Zpcxn2NaVTcRx6iTHpESomFnTst7v0Vvb4rVU8vD+fYzukbcKA4RhRJplpElKp9dDKgerkh794SZQl+mpqgopHJQ0SVPH6m4tTV0CUJS5q7duHO9BGDv5jbZxuvdhFC8M9DCK3TFhSFUUjhkzdnJrArw6QMjD/RHrsmRjZ5tP7jwir2pXR1pVBEqRdrooFdBqg6lK4jihrkoXrLOnYzHrdFwt108wkc7N84suctaJnWfGLkhTnZHM4vS4c878WTv7XPXTmWJEOK/dc96wPPvFk9c/NTafTvx20b3PneKL0lKnDvOpY26BSas5qjUfzxpmbcCz3djJexnNg7zm8lqPtw6mdANJJAU3B9m5x3gWXP7EEz719p2TviCU87sXBWdL97IUWJg7LdIGWOlKrYSQnvVcuFKPQBFEASoKCZKE6eiYYjr20PMuabZGlCnS4ISqmJG3NdWopJMqEIc01YzB1nXizoDWBJh4gyjKyIsCYS1Jf4cwjKiqGeiGtpwyOWzprl9HKb8OLnUbiytRGh8/5pUXbvJ/+B/+B/7gP/w+/+P/9X9kc2uH3/u93+fjjz7hlVdf4s4nd/jhD9/m8pXL/PEf/xFlVbGxsclf/83fZHg8Yjge8cFHd7h58xpxmnIyGnEyHrO9vUMYhnzyyR3+/Jvfptcf8PwLz7Gzs03b1rz99jtknYRLOzusrq4sFB9Og1HzYJJwRjme3NQjaawFYzVhEJ4J5BRV5SDnloViyOPdPXYf79E0JZcuX2JnZ4fpdEqg1IJLpihyxuOJn4PBmJaTkxPSJCJbuUp1skU1vEs6uHKK+BBL43fuJItl5/tc5z+fDl38fYrKOOPnWnExLP0c4uTUWXxKH5//BMtswGcbdH58n/ktcdF9/AjSx8+9PXm1z+J0w7nYwI/IeC//3Bmb+twU+cTvXHTNCxzxz2+on76Hz5Qt/9GXObPLWMP+wT6T8cQhGwMXpNrc2uLw8IA0yDy6y62tgQo4Hh6zc+kSBwcHrnREKoajEZPxhKPjY65dv+5LHBV13TAaDul0u56cOOD42Otxezukbhr29/dZWVkhyTI++vhjNjY2SGLN1StXvGSvdghIo73ed8rm5ibT6YzZbMa1a1ed7dA0rK+t8+z1K0hbg09gtGVBW1cIFYJSi1psKRVBnNDWNW1durptIVGRQwLqukJ4MmJttA++nLIiODZ0xxJmjSNZs9rxFDEvcfJZb9MadF3TVCVt1RAkEWgDVQlencbVNVuCuO/mOeuI2YbHJ1gchP34+Ai9u+tLExXXX3geLOzv77G2tkG322NtfY0H9+/xxS99mWvXriIEFHnpYP5VSW/Qp9Pp0Os7H22W55zs7WK0YW19nYODA4YnJzRty3u7exxWmp/bHrCWpdimQVhLEDl732rtfAYgiGOXIAsSj1JyZUjSo51RrjTKWJc4clro7nun8hS491LXiCRBRBFBZ/DUYPynDuhzY22BQjn/tz/mR04NP5aT/dObBT+z4902ra9RdfUAc7h0HMeEQeA6pHe2mdd0SkfyNa9dFiLEYmlb7SGoLVr7c/yTkFLRNg2NdZnnbqcL1lDVjcuqCUHdNAvJpXiJyTvwNWrT6dRNMEotIuXWGuIoQeCo9F2GwMFJ5h1NSS/sbgHc/Q5PTkgzx7JsjIYih+kQsb5DdbCLGKyjP/g2xXvfQ197AfmVXyQvCpf18Izp0tfEBkGIFi1NXWPsacCirioPPXd66EZr4iQlkQlhGHgnyxKGLviAtf6ZOQfNQek7i1r6siydLFnh4L5CSK8LPqbX7ztiL6MdGR52oW2edToEbUtdVdRVTRQ7YrU5OVxRFQ4iLFydbCJST0bhIlh1XRGF4YIgryqd0+Wy+s5ZiyKvGxg4fefGw9qN1W7QqpCDH/x7Dh8+5NIX/ya2OCIf7rLz4tforV1ieniXD//8P7C/P+Irv/l3WVlbJ+iskQ8PmI1P2L9/h5//6vN85Re+zPNf+VWa4oTR0RF/+p3bHBxNuXJ9k+1rNzDNjL17t9l7tMvduw/Y3ujz5V/+DVQYMj06RsXw53/2XZTQhDLkyvY6xeSYqi74+P3bDAYJ/VdfQMYZx/u7dLodMIZi/xM6O69hhGQ2GSKQ1LpBG7uos4/jBI32ZBoVSZbS6/U8gsQ5z3GaIKzTOddeqqOsCpc5R1JVTkddWEvZ5hRm5gJSYbCo25/zFASe0X42myy0vNtWczA2ZGFJGGdsZilfvLVKf3OH7318wuOxe2e9Xh+w1FVFlMQIKclnOUI63U6HaHDBmzAIXOBNOK3gIAgQKkRbS1nMPHFhBFiMhaapHK+CtU733kvsNW3rylhosYBuG8Iopmkq118986exksnwgGI6Yzo6cQzyVcXh/h7GGDa3dlxQp9d3/BHaOB4KrBs3UtJ40zQMQ7T5SUKpy8Rjp67lmW1hRD9tWRBLRpVdyuLO2ZLPpWmeaK5dWnSWrm+XVpnPabg+abafae2nnLW8OJ79oXO+MlZA0bTUdc1WJLk7s7w/yomUoh9KksDwYFrx6laXf33ngH/w/Db/+vY+f/u5Ha5048WFzq6lZ4nglv0e9+9pK+ate/I+z8EClh+fcHOitBKBRAQSJQOkCAiUm+tVGBOGEXEUMxoNmc3G1JUj+4yDgKSX0pYjypllnGuqumLQA/YekG5lqJV1qllBURbockpv45ovhzAoFWKEJMBQzQ6YoQmzNeJsHcSy9rvwa6OirsbMRg8YD4/ZPzji1dde56OPPuKTO58QRoo/+A9/wGyWs7a+xgvPP0cYxty8dYt7d+865YnEwRqvXLlCKA1lMWKlE3Lz+g7Dcc53v/s9RqMpVjxCSMmrr79OPpvy/nsfIKTktdde4bd+41fpZMoFObRd1H47yVCHCrI+2DyXW1zIjOIc7LZtnTKGh2jevXef2XTGpcuXODo8ZJrPEGjapuXDDz7k3XfepakqXnr1JZ5//gXyPCeKIoqiJElSBAESzXg0Qg76pGvPekSfhnOEda5fPTm2l+JWi964yD3b+Zd2MSbOjIblSMkTg+pcr1xyqud9/LwNeXqZuYe51Aax9HufaTu9+l9WoulHbucf7pn9T9nOebZng5U/hTadTi3np7enNOSCAM1P8EQXVQ1Ll8/znIP9PSaTCUma0pSFY/42jt9kc3OToiip64aNzQ5VXdHr9Tk5GXJ8dEwYhZRVxfHxMUmS0Ov1mYwn7FzaYTyeYLQhy1IePnjAyuoqRVFw584d1tfXybKMoiy4987dBcmyaTW3bt1CKsl0MiGxCcZoVgZ9pBRYKynygqZpOD45ZjQaE0UxnU5GkiS0TcPa6grdOMZOpi67XVeOwEsFiCCgKWYYg8uoKofqM22LilKaYgbgss1l5RR9Amjb2tUm2xapvPRvVYKXY7X+uRrdYprW1Xhb42S3tKZtauq8cDZrqzHCMD08QirhbX5L2zRk3Q7lZIrFEiQpTT5FZSukWcq9e/cARyJaFAXSJ2OqsuLDDz7k5Vdepj/o07aa6XTGjRs3GY0n7O7uEsfOf+kP+ligrGqKomA4HDIYrPDqa69y5cpVPvjgfQ72D6iqkiRNKIqSqiq5t7vH6qDPi2nKlpSYsiKQiiCMCJNkgWBWYeinJUsYxY6RXEpMU2GEcBxWwvFJqTRxOulz8kyrPUFd6xx0pZzT7XXSnzakL96e9KjPl7/8WMGrz7LvL2n7zI73PEo0l3+q6+aUKEUI8lnuMmJaozz9vTWWoihcFtrrbrdNS9s4AgeXoVbEnoRNCOFJ0yyj0RCE0/qc13tFYbTI2Dr2ZJe1cKRSykuMOcZygdMsFUIQx8midnqh/+2d1GX42pwUzgpLW7WUVeEczDihmE2o85ywLhD5mHY6pcmndJqc5vt/Sqky0l/+m9goJfEwd+HlZ7QfuE5T3AUeZrMpURR5qSm7kDeb61wbbchnU+9oOBgjbYuKYowx9FdWaesSG8ULOSRtzeLZRGFMVZdYYwkiSRR2ve6yWgRQGls7RzBNFgRTLvvuRobxZHrDk6Fn3xZ0e32AhV6q9nBxKSQ2dEaS0ZqqyBELSK9dvP8oCqnDwOk8xskisxEEIVbGqDCiLnOufOE3yQbrHD34LsX4mCof09+4yujgG7zz3e+z9fzPkmUZQewG/PGj24zHQwYbm7z0hS9x+ZkXke2M3Yf3eOvP/ivD4Yxuv8vXf/1v0I1Kjnbv8b1vfYfhSY4F+uuXybIE3TaoSLB5+QZffvOE73//PZ5/4RYvvfYys8PbPPj4Yz758A5bKzGvfOELDMuKw3sP2dzZYToaIYXg+GgXax1KIE67NE1DlKQEQehRD5q21TS1g2IrrwpQVw3aaJI0JbQhVV0hlSKfzbDWkHU6tJ5gzRhDHASuLEEbotjJu4HjS8CyQEMEUXhq0EqBSlOkEJSN5uFhy61LA1cLvr5Jr9fl61+9zO/9l7f9guMMO61bmtpJliVpQlkUDjLu+16367LLAFjLZDJ2rOZ1w+rGBqrbc4QhFqTnFGhqt+gJrQlDzzPgZfhmk5Hjk4hiV35SO6fbEdGFRFFIGhaoMKYo9llb7fOMSMj1kPF4Ql1V5PkUFYYkYYTEwafAzd2BcgidKIwpq9IFK34ScrXllWTZAvY7BDwBJT0HCuXMiuKzrhYW9bDgDS4Pt7LnLL5zXMZPtouzZvxySeoiCS/mDK7uaOszdMsEXmci1j5LtgyfPXV9F009bYY9e8z8n2FR88FEU8qIG5nBAHdmJTf6KW1r0Qi2hSREMC5q4jjiX31yyO/cWOdGP+EiOO7yb4hzu+Y1vGefv1yU0ywOPv9A7dwBF16+Cwc5Fz77LaTjQlAKqUJXgxfFyDBmMjxy68iwdHOebFnvpERpS10VzCoDUqMVBNEaTV2Rjw8ZbF6hKPdoqgLTVqjuKhJXkiGChKQTYHTF9OgObTUjXbmKlMHiPakgpCxbbDPit3824cHtLaKkz7UbN/hfX73Ke+++ywsvvMDe4z2auvbBMPjWt77J7t5DXn35RS7t7PDaF36GX/uln0c2J8jRe4j8EWVRkDWKR/uSk4N9RJiQdrpcv36dF194jtIHoYUQ3Lp1nSRJEJ73RPpgfdNoL/FpIcCx42rjyBfn6Drmx54GrY21jCcTvvlnf0FZV6x8fJs3vvAGjx4+otvrkaQx//EP/hOPHz8mCkNqX2/f6WRYIUjSmNl0gm5bet2OJ/5UtCJakmzz9fPzLuH5ZuTcIZen3WW5DvyU52G583hguTjrxJ4v3zhjU14wfs8qWJ2tOl/+7bNwdMu87M+NyfOjxcvznZuPngp//4m2zxDxu2CbD8153OBTjfcL9v9oJ3l+1Kdd+NzXn8F3vuiKP63HujSdMRydMMtzur0uujUcHBz4gFnC2toaR0fHTCZTOr0OURy5MWQ0w5MhSZZSVTX37t5ldW2V1ZUVToYjlFLcuf0JnW53oawDrg8fHR1Tlo7ktaxK3n3nXY6PjvnSl75Ip9NBKkmn03HjNnFw80uXL5HPZrStZjKdUNc1RZFzcHBEf9BDShfkfPjgEWVVcGV7g8C2mLJApB1kFGFnIMIIUxUIFRJkiXOUm4a2cQ40SqGCCDBOKiyQBNIhLtu6cnrV3hYWQiBD50Ra45xpXfvaZakQXoa3qSpaLx1WV66Uz7QtbV1jtSYwbr1QgSSKEoy1BFGMCiJM22C1Q7QWRcHa+hpaG4dsTRKkUtR1xcH+Ab/8K79MFEWMxmOqsmTaTjk5OWFlZbBIeiaJQw1vb2/T7/dcsiIMSRNXH765ucF0OiUMI46ODsmLYiGVrAJF2OnzUGgGQUgHp+8tA0WcZgRxjPRoIBF69agoduWQUYyJ/VLu5YNloDCVg9cLpUAqpHBynCJyNr3odAhWNhEy/Cn0/XlA8SlX+kzTyzm76694+8yOtzGGfDpFqoAoDImjiLKuaD3ES0g34c/1m4U3RMARmswz3Vo7cjClFHVlF6QwrXb11nO96H5/QNN6nWkpqcrSR9Kc0xdFqa9JrlyG0Nd+u4yA+70kTT0TOzSt0/DUWp/C1HH608pPKLptEVL4bEVIIh1Uu61rQt1C2zrR+XKGGR8jsj7qm/+aqm2Jv/7XkJ0+ZqleWzctVdu4+u4gcFDgtkVKSa/fd+3x2UchWr+gzhdIJ7cyf/ZSN6gwdpDltkFXBbapEIAMYzeZGIsKg8XzT0RCq7UjapACFahFje28tk4b7SSfApfB0Z59XkmFioWXJMtogmahVY4QCOGCAW3rooJCLcny4IIhcZoSheGCJM9JwDjnxkFiZhijydKMujX00pC6yknXbtBZu0RxfJcPvvmHdC+9zLP9LaajfYrplCDtc+vlVwmTDEvA8PGH3H37L9g/KXjtq18lyfp013Z48MNvsHt3l/u379HJAv7WP/g77Nx8hr0Pv829e4/4k2++j7aKq1dW+dpf/3XGR7sU+48pZlP2H++ytrHO2sYaz7zwLFoKVDrg3/2LP2E6q8hev0ZeliBSdveGPPtCQyBx2ayNS2grHUN3XRNEEozrX9rDtauyIk1TzNyAloqmrRfohDzPMdo54WmW0batR0ickvzVZYUKpKsjqis6vR7CQl2WCKWWDK8526+LfmtPJhSGAcNW8uAYrm4oLt16jrYx9Na3+cKLR/zgzmTBiwCCumkRwk3eQRj6MeXea6v1wikWHnLupDgkk/GIKE4IgtDXe7tSkMgz47fW0tQ1bVtTzHLX53WDVJLZzC1Y1hqyrEMUp6RZSig0sRwymkyQQcT05IRGrhLHFVnqEAJSuj6dz6Y0TU2aZljpxmecxCjppMbmmsSR+szT4WfazkOqxBlr7aIMiP9bnO6eG9li2dJcMn7c5c67vPNPF5l8585d/nb+m2e+W2qnOGvmL/bPnW/Ob0/k3s84wfO8uDWWbqTYTAN+MJ7yaNaANgilGDUGEbox87jSXB10eetwzGuXNvjWwZR/eeeY37y+wournacsn/ZC49iK0/t6qsN+7uP5v6XPegtrsdbVBUopHMPvgnRNoTwSJIpCxicR0+ER+XTi6r+Dhl4m2doYoKuSommR4QZmkjM6+IjByiZNWZD2N2kbJ3d5/PgOgVJ01y8ThDFNNUOFEqsbyvEe1jQk/ctEcW8RUTHVDBUIylbw5a/9Is0PD5nNZrzz1vd46wc/5Pbtj/l7f/fvcffOHTa2tvin//T/xd7uLtcvDXjzxQ0ePzqgyYfY/e8ij79DTMFsNAQREsUhz3ciHm+2vPWooixyfvVXfpG11QHfuf0JWZrwyqsvcXln06l2+KfetnNW4JaDwyMO9g+4cvUya6urKKWc1JcxBNKtX/OgI9rJCuWzGY8f73F8csJkMmU8HNNqzfWbN7h+7Sr9Xpc/Kb9B2zibZHN7AyWlI03NNW1do02L1U4CVElJqALSjecRqQBOlRkWI/Yi1Mrin3Pj3S6hVRY7xbmMz5PBnVNn6rx7vBxImqd+FgcznzhOR/0SyeMTcxFPBsLOe7KLUy8I6P2Vb/ZMW4W4aG578pRPG8pPbD/GDT4VNfvERT+jQ3/R0efnL/Fk8NRoze7uY6aTKUKC0TCZTBn0+zRtQ54XjMfjBQ9QVVbM8hmTyQRrLaurqxhjyTod1lbXGI3GBErS6/Vo25bVlRXyWU5VlX5uk45Y2ROqpUnMYLACQpB1uiRxQidLmeUlJydDLJY4iZlMpwjrCE07WYdDcUSRlzz33LNuzfa8C5PJBCEFcRhBU7mFUAWYpkTgk2ZViRXSSYLVFarTJUhSxyfR1IggAOFIwSzSEXx5+9rB8J0Gt8D4gILA6BbdardfKvD+RFu5jLluNWXu1FOqoqSpK6yxDqKNJY5bQh1A2yClQaYgZBcZRhgpGY0nFHlB5RGmmxsb3H9wn9lsxurqGlVdMRwO2dzcot8bOI4obcm9CkOapp60WjAcjphMp2xvbWGsZX1tnaZpF9wZ165fI8sSrl27xsnJMbv7+zx++JAwjHn4+DHXr2xzlK0wiBRxJyWIEgIVECSpYx+XwtlQ/rmpMMRiCJOOC25I3/m0deWMsbNNsS5gYVuNCBTEMcHKJjKIz/b+8xHGzzP+nowbniYQLnCqLcv7n/5Dn2+U/vjbZ7Y0hRQkWbYgRAuUc+KKonBZUiyhiHzW2y2QcwOuLHPSLKOTZQjZ8aRGdkEyZb3x1WpN07bu6YWOybRtWjCQdLoLxzmJYhrtYOqhz9Ba67LrAIGXWJIe0u4cW/dQnfTSlDTLAKjq2jnYSmKVdPBYKV29qpDURe6c78M9R8xWlbR1i52OkPfeweRj7EtfhqyHjSJXAyskrW5I4gRrnaHh2CBLsDiGaqDIZyRpRiglZd5QN7XP/gY+KOCkHKwxmOOH2MCxOMswwdQlthxjkz6mrRHGEkROckkXUwdDjlIE0NSVJzE7Jciay66pwGnzae84YV0GvK2aRZ08QiwFU1xNPDiJNikVQjlt5nw2I4gi4jgmThKXqfQ1JFIpwjjFWvfOZtOJI6JovA6riBAB2NkR6eolmnLE+OAhh7tH7Lx2haPdD1G6dQRZIsY0U2SUkQ93Ge/d497dB6jVGzSN5ea158inJzz8+GN+8PZtDJIXX3meWy9/kXz8mI/ff59vv3WHTqeDknD56g3CJGJ0dEKRT+h3Mow0PH74mJXVAaEUBO2M2hhefuEKb719l7yukSpg0O+ztrFGYwOkjDGmYjY6QsZdJ6NVO6Z/bY3XbFdURelqC61ZSH5YnIRbHIdorYniGO2DNGAR2pOtFaWDM4nTbKT2zPVNXfsgh2PoDOKQtnH9qpxNXeQyCMi6HfwgRGvN8bQlDGCzkzLau01/fZtnr65x+94ho+MjzMCggpCs03V64tpl+oRUvgTDMfw7iQ43dUlf611WJcV0BkKSdjrEQbQI0s11eEMvI9jUDWHsgksyyGibBiEsdds4opG68sGqkG4woZ2eEKVdZnlJfO0riIMGGDk0R10hhVyUfCRp5urHhUQFgWNbBY/AaQi8Y/RT284YtfNtOQc2T93wKbO8vcCymy8h4jRQt3yNpYT5qeFslxamC13n06b+uBb2Rfixp6xi55+BtZZZZQhMy2/tdHlnVPHnB2OKsmJ/VnJttUMvVFQ0XI5jvn884cvCEAYBM2P5/35yzO9aeH3tvPO95JzYpWc/X+OX23eBcXvmHs5/ZzmTQXT7nKwjVhAI6bgJ1KnzrQKFDGMnh9m25LMZRWXYPzjCyoBLW6skSmG7l3h47w5GN6xuXcOYltmspJoVbF6+xvrlHaYne4ClnB758R6SdHrYPKctRhS6oY17xL1tgihFBBmdJGEyzrl1pcNKv8ON69f5z/2Qt3/4DqPhMb//P/8r3nnnfdIs5aWXXmClI7jSmfH9//jPiTrrjNIRm+IhrXWcE1YLlGwZn+RYA1+9BNJkPGg3WF1b5ejwiL/41rcx2jCdTviNX/81B0tUgddEd4H62azgj//4Tzk8OuaF557h67/wc4RRgDWc8rVYzxospJNV81wXN25cY3trk6qsEVbwwTvv0+0/wrYtr7/+Gq++/gaD+/fp9btcvnKF2WyCNU41Qfq6zf5Kj6oqkdZSNzVVKZD99TOZY3Hu9V/QmZ/Y7GKsnjv+iYssXWfpe3G+E17ohZ3bt+jbTw665XDanM/gTPjtgvnqtGmfIbX7l7Gdj1Hapz7uJ8+76Dp/Gd73BT/51P7yl7Tlec7J8QkIqCtHitY2DZPJBG00URxy6fJlhBBMJhOmsxkqUMwmU7a2tymriiCQbG1teTu7ZW11HWMMV69cptt1yD0hBXlRUtc1o9GYldUV2rbl8PCIvChIk5RWu3LKaHWF8WTK8dEhK6urVF7ec3d3l8PDA3rdPv3+gE7WYXV1lV6/x/HRMcfHJySZQ+VdubTluuZ83bLWJbjyGSrtYnF2hJEtGI0VElOWGOtkxSxgtXFZWK/TrXXjytVah0Cd13OjtavzBmQQ0tYVSgXUReHKIquafDKmqRuapqbMc4qioG1baDVpEiFFghQQqNj1Va8iY4WgtZKj8QQVKHTu0Kl/8iffYDgc8gu/+EsIIdjY2GTdS7EVRcHly1cBODw65P69+zx88JDrN27Qas17771HoAKeeeYWK6urTMcTjDVsb+84ebgspdPtMhqN6Ha77AjB9tYWx4dOHz0vSh60mvVBTD+InX55GDkSTCWRUYIIA/fcvRqSq+M2CHM6P5l50KKqwRpkmLi1MQwRQUjQXSXI+qe2zFNso9Mvn7b52fgcb818+1xj7mkMkz8yEf7kPPjjOOufHWouFWES+rorS+XrlLvdLghoG+0cR6/N3bba62gKkiQhS1KatkG3xmf2nPHtNPscYZvWLZHPDge+3tNY62CujVjUZ5Zeo1j767uFXDhHxkdn0ixDyTmUxGVj5873HIorlVzUS9R17QgC/PemdTWyoqmQUeyge1VOU1VoA3p4SOfhD6m3rmAHq4SXbiHDGNHUVFXpGL2FM8QcDBE6na5zgv0kMs+0GeOy2/2VVZqqQCqJUiFYR7Km2xkqimitRQQxwmhEk0PcoW5bgjByHbydwWhELUJsb4MAR4igggDdtliLZ592pGdN05B6crMizx3cWzh98cqzTiepc5arslywyM+h/LppnCxVGNFKSbfX9065e9jueGd81mVFWfo6Xn1KlOO0pZOFkSKEJl65TD16xGR0xGu/8rusbq9DXfDw/gf0Nm/y2tfWufHGLyCE5PjxHb79h/+Jv/j2HZ55Y53NKzdp25rZ0S6Hx2NGo4qf+fmv8tyLL6Gnj3j04bvs7g65cW2H7W3N5SuXWd9YZTY8YTw84IMP7nL10ipJ1qfVlkvbKxR5TtqTBEnG2sY63d4hG6srVNMxaRLz6hsvIsKU6cGYuLNCFUbk0ym9fn9BJDR3Sluvcx5FMdpowiBwDP9+XM1mjpBP+YUiCEOMlkRR7Cb4OFow5Tt/whF5tFovDFStXS1SWRUOak5AkmWOrV4I6rJGCNAWwihBBgHDwsnZrWzt0OQjVrav8tz2xzw6nmC1omgTB1lXrjY/jCOSNEUK6fkSnIE6l/lr22ahczxYW1/As3Tj2fa95AaeFyL2teHC8w5YnJKCxBKEsXfoLVEYEgWwEpaAYnx0QCNSRPcK6vghURwThiGT8YgwCuk1rr47yVJnbEvlybAkVVW7+9Gashp/zqnz07cL15ZP9eYuOnJu7p5bGc6sO8IHUfzOBUR22WmfMyYvO8XL37vrXuw3X3wni2POOAjnb+JMK89cxfqTDRaNJY0DkjDiTx4PkRh+8/KA+7nm28OSO8OCq4OEwgi6QlKpmIeTgm4YcdxoIhny/7x9zO/Ulq/vdAkuavEcLr/cksXaex5Se9aRObuyitNbE4KmbdxcZgHhtWGFxRiHnnJEgdLL+QWUIiTDZT1brTk63Cfp9GmJee/jxzz7wvMMjyfs7x/x4suvOznBaobVlo3t6xDEtMZSVDOK6T691R1U0sO2btyFUhB0N9B1QTXdp8mP6W4+w4uv/Sx/9id7rK48z8ragCw+weiCX/uVr0IzJGLG+to6/+LfCHaPx/zOL73MgEt89P6HGJsxrgxx9ZCyGTIczWhr45QcVEgQSqJQ0s0yfvWVhNnlnyPuduj3+/yNv/HbfPjRRyAs03xGr9tB6xZHJuzsCGstx8cnPLj3kKosefa5m9y8fh0j5vwv82CvIC8Khyiw7r1JBK+//iplUXH3zl2M0ezv7vJobYWtzQ2SOKLX77G9tUGWJkRxRD51GsDaI/Um4xFKCoRHvgkpUFGy6Bvzd788ZBb+4NOGhmUJkXI22PREkGvZBvRBotOA0XJHvOiz//sMhvpcQ8/83HxmWTp1PuM+EQhcRJg+y5T1l7dZ73jNg4oWFgGvp20XGNE/Xjbrp5UD+3zni/N/nGvG8vfWWg6PDmhal6115GUdijynbVvKqmJzY4OidHZpFUUkccIsz0k7Kffv3ePK1StEUUygJHlZOPQoXv1HO9u+rEq2t7cZDUfUdc3R4QGT8Yhbz9xie2uL/f39BaKzaRuOj46ZTh1Muj/o08kyqqZhb3+fQb8PAqIwpLe2SlFWnBwfMxqP2N7eoipLyrIkCgJsXTnn2VpAYrXLZodx4uaFsiQQKW1TYVvtCDBliPUEasajkpxUsEZFkSNNM66UDgvWl7uAcPtxTnNVls5Zbxz3UeO5SPKpk4ositKp+7QarCbAEqoOqhs5hYsoJh2sEiQdptqwe3SEkQFhHPDDt75PUZY898ILlGVJt9Nla2uLLEupysrNQ0Ky+3iXIAi4eu3qgj3eYnnllVfIi5y21XQ7XeIoZjKZopRic3PDlad6NGIcJ6zUFaPRiOtXrzIaDTk+PEBby75K2E4yUiWwvs3WWswc7ehRx3MSUcchYl1WX4WIMFqkFIRy6E4hBDJOkJ0uamUdqZYUY54WzL5o82P9tIxnaf9Tp0TxxAFLFs5Tg6U/eqT/9CbBz+x4zyWs5k7u/O85jNjJZbka07nxPc/aBUHIrMhp69rXtDq9X3wWVghoW+ewBWFAWVSYcA5JdlqATVNQ15VjVGzdwAhDpwtsrUE3hjAIiTudpUyhIyNTgULJwEW6EKd16k290PVUStIaSxI52n/nJCqU1YgyJ4gCbO6IHQgi1O3voOKQOnQSK9HmZawApRIHL1bKyS35uvdQOmiug8NXXme7gbZxzo8AXVfYcob1kbxQCtoqJ5geUrUalQ7QeY4SgFA02qLaGmE14Wwf27aUg6vIMMYWU6rZCBmlhGm6qEnRqZOFa5t6UZtjjJNfq0pH3hV4RvOyLGmaljAIUQFez9xl48uiYDqZEEbhQr4KC1m3yxxqgoCqbjBeBqepGwc39hNbFIQOVdBURFJg24pieED3yg4nj24zfHyPF7/+M0RhwP69H3LvvXd4+Wd/hd6gB1imh/d477vf5N/+wTtsba/w6mvPg1BQjdn9+G0OjqccjBvSwTpJEnDy8DazwjCcVmwGlrYV3HzpNe6/+x2y/gqTac7bH+7y/se7vPjcVd740uvkJ3uIUNCySp7n3Lt/wEvPbnHl5jXuffwJgYIozRySYjKDZJUoSslzhwBwevLG1+WvIKWiaSr33L2zrLxRDoI4SVFK0ngSOzfxu/r/pq5pmhqtNb3BANtqmrYhSVIy78CXVUmVF34BdEgF1wa8brjTWu+vrPhMnKIsCnKtqaqYzW5G35SEccP1qzsc3f0T3nh5k+NK8f17hzRZn0ApitmMPJo6vfE4IghcQKCuSiKvlW1a7T+3VEVFd9BzmX6tkRLquqEpS4w1JFlGU1cOWaHUQmYkDJwjHiXOoRZAV85oJnscPPiEvUeP2Xj+Z7ByDukNCeOY9c1twEHSiqLwaAuYzaaEoWNNT5OUtnbQ9yiO0I3+sSfSU4muuTTSWWtpbnSfmr2n2stn7cNleCieF2nZWJ5f6exvn7qUZ1ejC6o95ydxmss6k2bj9AdO23dK6HS2zaeu++m587q5J37SfckcbmswPrgKtZdyfLafcVTWfDDMGWtJKAQlgvf2x1xZH4C1pGHAW6OKV9ZCpkaSSEHHBvybB0Omjea3rg0IFl7M8o/PYb5Padv8vs45LfNXO+dMmH8t8WNKt0RB6KMQ7vnMJdICT94z1/qOtCAQ1qltSIVQAceH++ztHTJYG/DxvQNEz7I3arhhQzAth7sHDpZ4NaEqZhw+/JAoSUiSAbptEHWJkiHWCpe1MDVtOSaIOoRRRjPbJ+1ssbl5i0d332L76i0e3bmPYkK6eomXX30Wc/IxdXHIP/pfvcaJ3mB1ZYX73/svCGO4t3cA5iHi2eeZzSrqUmNVQJLEWN3SH/So85y6brD5Lm3x7zHJGuuXbvDqK8/z/HM3QEjCpfKTum545933GI/GXLlyhatXL3NyMqK10DSag6MjlBBIz2dRlg5Z5+wBV7YjhQu2dzodVw9uNE1dM5tN2N/bxxjN+++9y97ePh+9L/nyV77AM7duOtI0Hxgu8hlpki0IWpu6piysV5c4Z7yd606L+u/ToX/aj5b9ZnFK0XZmLC4Fflg43Bf80Kc63Rf14OXf+JTjfeTAnvn7vJ93gaf3V7ld9LviMzZn6aCLTebzez/tqueCdk8cO58X/fxsz7+6H92CJwI5y9PQPLZy/uetS6I8evRgYRu0bUuadqjqRzT1jMu9y3x8+/ZCkUSpgI2NdcqiRCrHAaO1Ic+nHB8fk3p75mD/kGvXr3F8fMxsGjGbzTg6OqLb7zmVIq3ZHPRpaoeqS5JkUTMulWQwGFDXNZeuXMUYy9tv/5DBygpXLl92pWZhhFKKfr+PMUPee/ddgjBkMBiQFwVRENA2DW07QxiNVQpbNTSzGbqpkHGKripk6BJyrpZbAdInMwxtWRLEKaZtME0D0kHOTeP+Fh62bY3TG7cY71NYjLY0lUtANW2Dtm4eqoqK2SynbRtmReH8k6qi18Z0ksSVSbUNQdh3yQQVoAXMWku3P2B9Y5N33n2HS5cv0x+ssLKy4jisopCyKCmrirqqmM1mDAYDZrMZVVkyWF1xAZPplLKsWF1bJUlSHj9+zN7+PqsrK6RpQlEWPHxYIKTk+OjIJSB70O91fYK0YXN7GyEEo6NDHp+MWMlSrkcQWxwiUSpMOcG0DTIMUVGM8Bw40gcoTd1AiJMPi1x2ew7jV2mGzHoEq1uoMHkyEMa52XXeyc/1/eU/7fkdTxtMT8yZT7nGRec/dQp4+pk/ztT42TPeQmFx+qJ1XS+yuMrXZFRV5epPlZfNspZABZ6Vb4JS89pUF0ULwog4iWmamjzP3TWV9CyMGiEjQJDEMWmS0erWZc1q5/AvpL4CB8nWRruotXBZwnn2rWka2pmDpVnw2QnrjCXPdIwUSBRK+agYzqmPwwiDoT54TBDH6CpHYNFNQzbbxW7s0OQF6StfQSi3+iqlXGChqV323bpstssguwyIkpLZdOxIHJryFE4snLPtDIgKA8gmx0xHmNXrROWEgBarLbWIiYojqHJUOaTs7GBWLkNdUk1HSBVAEICFoi4XUJtiOqMOa/qDgdPWblqEz9TESeL0/Pzziz3pw5zELvAQXbxzvrLqCIBchj7wzO0BWrcLRlhHwCAWjPFRGDkm7LahKkrnKAUhra7R1hDGMVJatBEk6cBFJHWDUSHahjQGLj/zOsJqPnnvOxSt4nf+N/8ttDkvv/k6KysD9j78FsNxQ9C7xM997RovvPgc5clj3nvvY+48npJlAU1Vc+3Wq4TScvD4IRtXbzKZNlxez0h7PdbWV0nThPzI1UfuPq6QYcZoNKLfXSfNUk6GMx7d3+XlL7xO2Wjibp/jQiDFyElX1SVt25J1OsRxjNaG2XRKECi0bhzKYv5cjfHs8fiSgJYgiGmaxhHX+X6UpClYnEEfhJjCwSMBX6/v4GAIiGLn9DqGXkdM1DaO66CuKh8oc8GUOE5QSnIwa9grWp6XM9I0IMsEJ4dH3HzxZfaO7vF4ltPpdEg7HTf+5iiTwBmgURxRekJFg8vyG2OIksjBz+LIlT1YS1s7hEwcJcRp4uqmvCyZmfMNVNaRrnm20kgakvCYcnJCEEZk/QGys+HKX8LI9R/h6ji1r5E32t17kqUkabYYp03bLBbZJMtQ8dP0JX/0doZYyWeJLgZ1Lyex5q70PJVzznU+m5g9c50zGY+lReEM1ZKY7zmnG2zPWm/zrPUZ50GcXussQ/rS+rbkcJxp3dJ9nYtTn3EELGIhReL8Dsuo1oxag0TQVSBCQ9iNeKusuXs0oVjpcDlU3Ks1z2snX2IsbCQhRgj+cG+CtZq/eWPtNLbAuZ//lND2qfE7fzbL79URWFrrEGDgiGrKsvIIHncNY3B9XBt/nADPeSKjhjQckEYRKoyIsw5Zf8DwaA8URGmXIyPRcY9pbdkbHTM9POZad43hbMrR/fdJ4hArA9p2Sv/qM85Isu55h0lGU04I4hSExDQlMlDkJ3e5eWWVh3didh88YG1tHR1m9GPFnZGiaVNCVWO7V/n2N95nsDrh6o1f5q3v/n+YHo346vMD8tEJpiqoS0PV5ByXFd1+FyMgDBSyKJFRzOzoPp3RXczWJcIwpZOmjmtkzhhsLLu7+/zpf/0mrdbcvXefGzdu8Nf++q8QRSFSCv79v/sP6NZw6dIOb7z5BoPBCmXpWHkFrtaxqWsC5YKJg5UBVVnS1DUCxyUyGo354P0PKKsKheXBvXVu3rzutGW96xyGnndijo6TfgwK9cQIXsRy5sP19AOnnpE76DQgdUHmRSz3syV774L9p6Nv2Slf6tTnrrGYPZb/+VRr8xzR2+Lf82ib5W9/gu1TjdunH37mPSz7wD/SAP88Xz/5XM84v2fCmp81+/Uk6/zTfvKzPJfFtZYezGQ6QWtDp9NlNssZDofEScy6R5pJqSjygjzPuXTpMsZakiRxwfym4dlnn8Vqw3A0Is9L9vcP6fV63Lh5g/v37xOGIUWec3R8TBiG7Fy6RFVWrK6uIFVAXuT0ej0O9vfoeERLknbZ2NigKApOjk84PDwg63SpfElilqXUVc0zzz7DaDhybbtymfFwQlVWVFVJIBN0VWNt66DOukWEMTIsIQjQtXO652NSzjlahAu4s3CqNbpt3MOy1mW0pVM0sp4Xal7jvZAzbBt040pSyrKkrirKPKdpWqq6dooKVUlZVlRNjWkNnSRGqoAgCFBCIY0mzDKIU1qhKGREf6XDwcEhJ8dDToYn7Fy+7JInxtDJOmRpyuHRMU3b0uv1GE/GCOEk4Xb39uh0OmxsbKCCgCRJ2NjcZDAYIKUkTRMePXxM27oATFGUrK6vMR6OaIdDMIas4+zHYpY7JPK1a5SzCQd5TULIlm1cQFhogij2BJ4OkSnnfdS4DHjQ6S6C0VYbTDNzEm5Biuz0CVY2UVFyGoQ/N3V9enc/G4S0ZwfixYf/SM/6guOXtnns8ylI9k9t5uedGj87uRoOGu2Ywj3LudZe5N4u9LiFMejW1XgrqYijiLp2WWWnP1phjctCV1XponC+pjoMQqI4WdyDXmIdV1LSaK+xLeSiA8yhtYFSxFEM3tivqtI5H2FEmqSUdeWZu51zLbzxFAYhdV27juShK9PREGkMIk5IophiMkSmV1FhQFuWtA/eJup1GM8KzOaLqN6Ke0bGLHTC5/JZYRQ5OTNjPfGOpCyczEMQhiTdnpsMlKStCkSVz8tvnbF2eAexcgVRTxGmQtcFNl4lOniHRIHWlmn/GkYoOLpLayyBihC1xSAdE2QYI3wdd+ilAnSrUYEgjiOapl7UurRe1myuJy19LXiRFy7aV7na2ThNyLIOQjiSHBko6qYhL3NM68hsVODqQ1xdiDNyZODqgpWURHHkyDMEWKPQrSFMe5SjXTqr69z40l9ncrLP4fvf4PD2x1y6cZ04cZHBx7ffZjotkWHKF7/yMhs7VxCm5fGH32Hv3n0OjseMjo754m//DYSu+PCHb1M08O0f3uOlWyv0BwO+8IVXuf/Jh5im4qN3fsDu432irMet556jGB5QTceURUHdRHzy8DEHJzOeu77OoJtycjJ1DPNWcPDgHkGcEGVrFLWhrY5dJjhJiBM3eZVlQVVWjlVTO7KouXPTak2d57R1g8UzXaYZM62JwtAtQmFIqzXWaGbTKd1+nzTNnAyXH1tt01AUBZPRiDTNHMKhcSRlTd24zHKauSyycGRF9bik0+1SNxXKBA55oVLuHZQ8s5nx/Kuvc7j7CBV3eOXmOgdvD2nqkCBQpEkGxkXc66ZBIEiylCByCgJt03iIvQsmCAllnrugVpqhpKTMi4UCwpzlv2kbhifHnj9AkXU7BIEikxUDDtHjA4xpKYoZ3SuvQe8a+Th31BDBKdC40+mSZJ0FfHM0PKbfXyGMYidr17YI5jXf7TJH2efezgZZTxeJheO2yL76v89DQT9l4j57yLI7e5HpeNaSWxA0zf93/ibnzVjO1l3QpNN16uzqdsYQPbdiPcmcPP/CYhamrOP4UEISCUGjDaPactJacmMpWk1rNZu9mDsHQ+4AZS+jpyTvTSq6aeoIMa2lHwWUBn7vwYSVOOTrOz2/bn9WK/1JVwProIoWJ3WlhEJbS9U0LkgZKKIwpKxrUs9VYIWg1YaqbUnieBFUMMIigpBQOMZdGUSESUacdUmyPge79zBG0F3bYW+qGVYNs6KhtYqTypKNC8bTguFogjqacO36TVd2VRZORlAphHWEm0YoqtmIMOqQJl2EULTllK98+XXe+v53+MYP3ubKrWfY3lrn4YMhSbbGcChJZoaXX32dR48ece/2Bwx6PXYfVAShYjqeMBnO2Dsagxa0TcOzWcpkNGZ/f0oWSXa2+2xc2qJ+9OccZxukq5cZrKwRKCcxOlcemc1ydNuyu7vP0dEx9x884tbNG3zhi2/y7W9/j+985y1M2/DJygpRHPH8888hheN9icKAuq6ZTacYazk+PGL38S55nqN1S9bJWFkbEIQB3X4XObFsb2/wzDM3SOLYq4xYV6ajnBRq3bQoY4kiF/BUKvBG+Jwkdo5kOd9bLqp+Pu1rTqt+0e1PnevFReYG/1KIam6nLvuBixrXiwbUBTanPe3NZ3nPT4+fTwfL43655U+Ol59ggvwxN3HRH+eH7Plmzqc5exrnOz+XfR6D+bPf9ed7Xhf63J82VZ0715nAlpOTY8ajMUEYMpvNFgmuwWBAHEfcvv0JhwcHRFHMZDKi319xdk3lVG+mkwmPHj5iZWVA27SeF6klTRxS8ujo0AfIY4Iw5NHDRygl2dne4fDoiDiKyPMZURQxncxYX19na3uLKAyZTCaMJmMnyWsshwcHZGnGbDIBIaiqiv39A9IswWpLECp2d3eRSpJE3t8IlNPbFo5AUqgAYQ1BkrryvdIzaguB9RltXRYO/eOZy1UQYLRx7OLGOM1tHBrMer4XhHXJME84bNqWpm2oy5JymjPNZ1RF5aH2JVVVkRclTe2c8bV+13FDKUmUJIRRhExSTBAyLlumMuJ4csTBwQFCCF5//XWCICTLUqcc1LYURUG316PIC6SUTCdTiqKg1x8QeikubQxNXaNCN0f1+wPatmY4HHGwv4cFLl26RBTFZGlKt9Ph4f0HlFXFeDxmY33Dk/VWbG1tsb62Sl2WHM8mdEyDrhsnjaYzZKCQ1kDTQOSl1wQ+EVI7BHQUIVQAMkEmCeFgHTVY95nuC7xbYBmN9llicedNq08dFPyk4cKLJocnZpCfaPvMjvd0Ol7UZc/JsoTPPAspMK1rjGMTdsCqpnHySG7BcM6wtRBFsYOqNA2hrzdxhrD2jOBzjfDIkaR5ki5rDNbXjyhP7ma1Jk1Tn9WztLWD6M4J3qSPsseR48BvapdpXM4Eaq39pKJAC5eNVwpbzBBJCqbx0koCjCGd7aJX+syGJb3nNwnTrs/uuaylVL4mVyr/OxKpoK4c6YNUAWkndNByITBt4wIV/pVqbZBhjKxnBM0MpELlR07qwAiivW8SCk0tEvJki/reeyhTYoMYKyNHVBalhFmGbRtQoY9WObZpKyVt4+p8WxwpVxi5soC6aZyR6euEwyha1CcrKUlTR3pljSPDckET0K2hKopTOTfpnq2D3zvYj64rynxKpzdY1DUbXK14W06pVUOUrWLLKc10hLGCMAqYHu2BUlx59kX6m5d5+MG3+M5//vdEvXXWt7ZJuyvocsqj97/D8bjkaH/I48dHDNbWGXRijh7dpWoEf/Tn71NMhvT7t3jxlZvkoz3aKqfVho/f/oDBoMvNm1dpZkfsbK/xwXu36USaS89c4+F373JlI6WbJfT6XYaHB+w93uWV179OeXxIN4rprGyRy4gkTIiT1JGeWfd8rbEc7O8CsLqx6Zjim2YRxJJCEs2Ztj3B4HwN1sagq4o4iRHCSYhVhdNId5q2DoZeliXWWlY3NnxZiKKuHCJBG+PYJzGLsRnFTl7DWjcuVBpQNy3aaEat5O5+xaXeCll3RBBEJElCMLmH7l6jtJaiKInDGGMN2jiN7Ng61IRTC1CEvjwhCEPHUlw3yMBFotOsg5TKLXBVxZz51NiInSylqRo3PnRDz54QHX/I/v5dZtMZIh4wePGXCFdvUFYtxuZUZclkMiYIQqRy7P6dTp8kSan8sxmZEzqdLnG05sZl4AgH61njURo/7nbB9C7OZibcrnMT+LJjuPx5IVt10WXFBX8v55aX9iyyz0vNtGd3nA0aXGjHnlkvT7NhS/d5/oSlCPT5Zcpyeg33r3NsBlHAs72YQEA5anhcGY7KligQjLQm6nYYDcc8MJarKx0elIarkaV2KWb6SpKFilkc83+/PSSW8DNbvSeUmM/qMT25uC7bCnYevBCC2pcwKaWIw5BGa6ZFSSdNqeqGoq4BgcZxkxxMS7I0QyFojGXWtkxbQ5pGZFIgZID0et9Wxdgg4fGDjykPH7N5/RYzag6mBbrUiELT7J9weP8QScuXv/LzoEKOj46oJgds7FwhCrsUecFkPEZaS29tm7S/TlPnCFMTxjGWki+8/jJYzR/84Z+hhKS7eY2vffUmL738Iv/2936fKNRcv3qVsuny4P49rt64Rhy6gOxkUmBtxKSoqMqWdz84IOuEqCggDkImpaC4s8vOrRTKIU27Sd3UhNYShiFN02KsodvrEEYxSZYSBhGz2YTvfe97gKC/MqDb7XJ0cMj+wRF/8ed/wdbWBtubW7RtQ2MNxWyGEIKqKIjikDt37oCwhIFka3OTbifj+OiIL3/5i4RKcPXqFaeG4GGpLqAiKMuKB/cfg1SsDLp8+N4jfvd3f9sfYxbBI3G+k4uLwl5PennLTqwQFzjIi+tcPH888VHAqZ7Wsud83r1e+ntpLJ4NRLHgm/Hu/5kfXbTrc2apP3V7Wuzr81x/2QH/vO36S40dnAt64qaaRakKFzR3eY1Y7LvoPSx/796YBcqy4MH9e45Lxmdsdi5fcgmtQDKZTiiKHIGgrCrCwDnJK6sDAKLElUYKnJ22trHGwwcPATg5GbKyukZdO0j2xtYmn9y+TZamXL16lSRN6ZQlSRy7zOvKKicnJwRBwP7ePkU3RyjF5sYmeT5jOpkipWRtfRWsUxo6Ojxie2eLB/434zhifWPDlYT5emOja9Ctv2PrWLY9MnWuemSNAU/cqKspYdZxkmDWgtEueGYbFkQNSqFLx4Ej1Dxj61SXEI6Z2wqn3qPbltZohFBMpzOm4wmtaZn5DLjx3D1SQhgqVBS5QGjagTCmLBs+fLTHNOlz/4EjSHvm2WexFqfK1LSMxiOklDx4cJ+s0+XZZ5/xU5Dh+OSELM3Qq4bxeEw+y71euWFaT51SQ9NwdHhIv+/g/dPZjNl0wsHBAc+/8DxvvPEGu3t7pJubFEVB2snY7m1TlxUgkUGI7fY4qUtW8xOSJKHIJwRBtCjdDbOMIIxRKsIKhYgi/zi9ikfaIVrfQvVWETJ4yhw57/Snw2V53rKn3fv0uM+w/ag42nl75Wnb07+6yJW3P/a8+Jkd7yzNPHGay3g7qZ5TqLkRTsIHIYhk5CDhgfRQU0dyIL0TJpWABqfz5qNe2hiyNFtAv/DEK1IIiqJwUHLlGE7jKHa1Xq0np/JQZ8dY6CJZLnvuoRLWooIQBMhE+Wy06xRta0jieCFjgIf+Yg26KqgnAtlbw4yPUFFKU3xCFEuGhyeIK19ExDFWSqRQWOkMtCgK0a2TQHK1ZG7mDeMI2xpUOGdm1USB0ysV1lKXORQTTOAk2tR4D5IBZnZEWB7TlCVZdYzVDZPKMi6GqMk76NJB6ZqmQUcdmrzGnJzAZOZII7Ie0c5Vwuu3CK/dIFrfQqYZuqkJ42TRpaLEEW21tctURrGDszRN45xH73xLpWibFqVaAqX8O3NOU9bp0TS1I2Pz5GlIC8ZS1yWuBKBxTrkxtHVFkc9QaEQQ09Y5gXJ67VU+oRofsrd7wOHhhBe/NqDJx+zf/5hPbt9n7Yrgyouv0R+scPL4fY4fP+C4kDzeO2RjY503fuY1dHlCK2J+8PaH3Lg04Llb27z5xotkScDx/mPuffwJ+8czXnz2CusbG6hQkW70OToe0Ykt69vb3Hl0RFUWXLt8lUDB/UdH5NMZt25eoSoqptMJQShJwq5jlg8iyrp0qArwRGaGrNcHb8Qbo4ljJ88mhcQoQ6AUCIkKXC2zMZbxaOTImrShbZoFR0IcO9b4VtS0rUOCxIlj0gzCiLZ1+8MoRAVe0s06osK2qpFKecd3rkcrPBmiI9Br25YJMcxyLl9+kTofE2c9GN7h9ecv82BSclzFTKox3f6AJHGBm8lovGBylkYspOvq0kkHBaFz9KsipynlIpM/mUyIogjt4fTWQita4kjREyPso29xsL/HeDIl63Tp3XgTMbjBrKh9lN4tvitra44JuaqJ04woilyWPYrIOh1UoAg8H4UQjlSrNYYgCpEiumjq++zbE1Y1S47vucl6vrAs/d8uuaPL3vBZg3r5x9y+ucv9aWvAfDE7k4FbnOXaNK9/drXnpw7nmevYZaP+UyxfcXofYinLvggGePtHIhGYhStfNZpQW65HIQMJh0px2BhaIzjRGtntU47HPFIK201QRU0nCqmsxYYOQpyFihLB/3RnRI3gZzc6xGrZ/ZZL7bkgI+iJeKRwut6OuNMShgF13VI3LUGoCJVCxDF7JyO6acJxUdNLIspGU1uQYciD0YReHAGWXpywX7Z8nDe8PuiQpIGTmNGGaKDoBhHXeyuMxseMUAxFh3rlEpltmYiAIEh5NJwS6BoRd6hNw8neHQdtHE15eP8eVsR0e6vsXL1Bp5PRVM54ErJHW07d+hR3+PLP/BwbWzv84K23GBbwH/7d79Ht9IjjhLKsePfdd2i1ZXXzEl/8+V9i7eRbHH/4HUewo10AbTRtmVUFO6HEFDVtVVNXDXXhMs9bG18g2nrOE3tadOvYhK2x9Hs9vvyVL/HHf/QNRsMRVVmQFzk/fPsH/OxXv8obb77BD3/4Q5Io5qVXXiIOQ6bTMaZtieN5EN2xJO/v7WG0C+I/9+wzDPpdvvXNbzNYHfCbv/XrrK6tEoaKJE6cfq+H/TdVywfv3+bb3/4uzz3/AqPhMW1+Qhi4EjThpRIBr+V91hBcjM5l1YAnB8K5Qcg5zfsLhs65kfVkXfhpr7VL4/N8+G2xVyyfe7p/2eU+L3y27Nv+1JzupWs+Eaj7KW+fOhcCZ+AEn3aBZdvaz5GL7+eOw7k+sQwqsucPvqiNy1+LM988tU3zc/IiRxtDEIaMRkPSLCNUgVuHw4DxeOzQFFKgEGQdp+ajpCPeKouCjc0Notiv+aMxN2/e4PDwkEBJ+v0+ZVkSBIqToyPAwdTv3LnDytoqSZwSRKEn621J04ThcEhROPnftbU18jx3bN1raxwcHDAcnrCzfYlyVNHrphwdH1M3Nasrq6yvr3N0dMTqYIUoCimqCtXvUR/vEzQtWIOMYmztstxaa6QvgZT4JFacuPIlOeeTCRzxmrEuAYFL9mAdknde3w1OXcnSei4mi25qlyk3uPrrpqb2SN+6qSnyAhA0rUZYjxAVAhkoRNahqGo+fHif793bZeP6LZq25dGjh7z62musrK6QFyXGaIeQjCJWVlZJ0pST42OSNCVJEp595tkFx81g4GytIAhdYkc7H2wlyxA4dvvRaERHCdY3NjnYP8BoQ1FXdLsdhJCsra0hpfO74iSmLAvy2YzNzQ2KsXW+TD4j6/bRWKTWBInT8Ub452UcyhklCDo9ZKdLuLqJSrqLjvojh/XyRPeEE/7ZN7t87gVj8keez/k54S9hQjq3fWbHu21d7WigXM1DnCjPXty6zJqvz4yi0DnIde1qM31GzUGcHTFXoAJ0qIEGi5MeilMnCF8UOeAg0EkUUTcNRT4DIcg6gSM8aBvqsuD/8U/+b/y3f/8f0u33CANn3CjlCGvm9VpN6Zw84Z2Tec2pwDngc1F52tbVMfgocluWIAPa8THJ5iVm730b2dvAVjlaWGzaRQBy+5rPGPtsMhatjdPC9u1oqsaRv7Stnwgkuq3BtDT5hDDrUBVT36Fdhh/TEOqKVigox7TTIXEzo5kN2d8/YnYydozrrcZWrdMZbCxVWQMhsjdAr28gwpSoaWjufEj9zT+lsYLuredY+crP0Xn1TfTqKkEcL6TDAg8HP3UMLXEcE/h7y2c5whPTOOk0R4wXxTFBEDCdTqjLEhUoJwHnIfdatCAywiBwNTJVRTEdE0cxUkmCIGV/WLPdcfXAK5efJVYt03xKtHKJpFTksxP06AH79+/xYL/gg4MHvPn1jOHuB5STEe+/f5dw9QpZp8fLr71AL1Hk0xE/fOcBu4dTfvlX32RjPcM2JUeTgkf3H/HxnX263YRbz1yjqFraqmTSOqmIOA54sDfm4cNDdtYy4ihAxAn7773P1uVtLl/b5vGd22S9HlHaZapTquHI17qHaNMiSkkQRgsoofJBitl06oxYXE29FAItoKlLEIJAKfI8J0liWu2yTUWeE8UxaZoRxTEWS68/oCgKyrKkLArmDJRZlgJ+UvEwLO0Z0KsiR0gXHDJCLwhZ5gR7SilXwy0kJ7Oa6XTGa8/2UFazubXJysYGVhyyfzfHhB3Go5GXChSLGnRjDFJJBHhop6EpHbIjCBwSpZgVGDtDSemy6YHLRmmtaZqKRFRsiBGpGTJUkpWNDbL+CtMaTHoJ27Q0lSNyLPMc4yFiWhuatkW1Dr3R6fdRUhFEjvzDaE1Vufr7TrcHYYTPg/3Y2yKTtHBwT/+8MNy6mNutXzhOV59F1Bef+V4+9QLHfm7I2yeOE2cXk4UN7s3BswWri/bM8ykXL17z9tszbTljTD/t5s8bmBYnlexroiPljBZjBdo4kq22MZjGkmjLCpZ9QPUGlKMpxzJgbAVrVrAaKmosg8A9skRJZgT8q0dTCg2vrybspP9/3v4rxrYlze/EfhErlt8+/XH3nHP9Ld/FqrYkm0Szp4fUSBoOOZQwGIgQNBAkvUiPepgH6U2CAEnACBA0MsAIFDQGnJY0JCDOqLvZbFvdVdVlbtX1x5+02++9fEToIVbuk5kn761bVd0Thbonc+/l9s4VsT7zN8HGerSNs15OutvvVGtLbV03w1ceVWPwpLO3UhjmeYWUDd0oYnvQ49l0QWNhOlmwO+ixKiqe5RprGsJCM2ssr3Ut+0nCcVnz/qKg44FnG46NZC9OaIxF+iGpH1Is5pj5nCBOGC8W6HxNhaDpjhgEHsv1msnhYzqhoCmXPHh6yp17b/DWm2+glGQ9PUHoFOn5WJNjdMlqeoauSkY3XkMIn3v3XmN3e8SffvsHTKZzws6QMAzxmobdnT1eff0d3nr7C/hK8Bf/+QOe15qqdEFcqQ2dNGCRa7KiAQmL0xU7Oz0kPmESc/r9/5LSG7F178tYIfGkm/tB6BBvWyOno3F0dEhZVWxvbTMen3F6espXv/ZV4jih24nZGvUIQp+qKNFNvbEntUZT1jX9QY9vfOPrhFHA/sEB3/nWt8nznCh2yUQQ+JuCca2dzZCvFFmWu6C/KtF1znIy54vvvEq3N3DrF86bHfkisnu5r3w5iX7x84X5dkky/0Iq9qIadnFmXVkuPh1ibq/d4fLVXTzuxU3Fpd/a5Fu8mBefnTL+fOMnhrWfUc/bvH/1QJcrFVd/+ClO/jnOe+Xn8ybq5hTihVe73Wzw2Ye8To/iuuHuG9s2tw3Hh88dulN6VEXVemFLqrqiE/gkSUJdNwyripOTE5bLFdvb26xWK8ZnZ0gp2d/fo9frkec5x0dHBIFPp9NlNp+zWMwZ9AcIKVtLTrvhiNdVTRhGKM9nMp0yPj1jMBrgKQfvlp6kamp6vS6+8pjPV2Ati/mSuqx55ZU7zBYLnjx+TF3XdDodrNVk2RprLd20Q9W4mNhTgYttlI/JlzTrFV7SxbRe3iIIafLcfZGtS5JQPqauqNZO88LivKh1XbuCslLYpnE875ZqaRq35hujaera5Ti6oTGGvMgd5bIsyfOSuqnIioKyRX5qZ9WAUBIvSWn8gEdPnvHHP/iIdRRjz06Jk4Sv/cIvIKRgtVoRhhFC+MxnM7TWbG/v4PuKszPHA9/Z38HXIWfPnhFGEb1ej07S4eTsmDCMkVLg+T5PnzlHiCzLmC/mWGHp9HpsbTtvcL00RFHEcjZD+Yp+f0Bd1RxPx0R+gKc8njx5TDftIJIh/uIEmS0Jo4TaWGxLCcSCiiKCTg+ZpKikgz/Ywku7CM/fhAWbNeva+/7CG9cWBX+68dLU+WmO9elT8/JGP/WBP3t87sTbD4KNUIxSnvMNbmoEYIzGD3wCP2gTKyd21pSu0608jziKN53suvWrtq2tkoWWO+w64lo7boWwrpqTdjpOETYIWK1WNHVFtprz0fs/4vd+97/i7/03/lt4obfxEK8rp7AopevGllWBxNlRqFYEpyycsngYhZxXf5USKE9SFk55FU85SHa5JugNXAFAO2GF8JUvsXr6HG+0Sxg58ZimLB0sA4FUrpsXtGJ0npCU1tIUOVZ5LliuK7TRyDDEVjlUBXgB0lqUrdGrM4wXIqsMiiV1sebo8XMq4TN49R2wNVIqjBDIdITs76C9COkpyjxHr5ZkT5+RnxzhvfVVOn/rJiJbsfjB93jwT/9j1P/7t+l/8UsMv/FN4rv3CQZDBBZfKZQfO9GJpqEsm40gjfI8B/8xphXTCpGe4xFbnF2Zt7EdcLeq50mEcJx/IXAWbgJ8NcJqd+8o5SOIyUWAmX6CrkrK5TFNUfDqO19muHVCpzdE9LZ4J9yBZBcZDrn76n1mj/+cyXjJR0+m1E9r/sE//C2297aYHT5iuljz/OkJX/+FNwlkgef3OXn8iFmmefi8ZF5K7t8bAA2+tJydjYnTmChSpL0tnrz/lFFPkcYdoiji+fMTOqlHp5s6HlIY0YkDbLJDaUPKPGM6XpN0uoDrysRpihCOr2y0xiiHGCgLBxf3Ws/rMIpxYiAW67kiiGmf6t1eH3Aq59LznFifsZRFge8HLOYOqhRFzh5PCIkf+DRNgzEG00KoVPsAdVZ2LUxLu4SibLUOqrpGtBCyKOnQVD5PxjV3d312buwTd/ucPfsEkRfMFnlreVcRJylRkuAHPmXm4G1BGFCVlaOPaENVZvh+QNpJ8QIfH0dPwRiUknhCU6yXbMs5qTnj7MEDfvT0KUIqoiQl2Hmd6PYXUemWq14LQZ5lzKdTlssFZZHjK49oMMT3lYOVRzFRHDlkhmn1H9rz6sZ5eNet6NzPMzYBmLiyRIsLG3DlvUtBonuEvAi0z8Nje/lBZnk5ob4StNkL/73+cXF+kefBf9ttb1970aW+8mS61JF58UEvwVSvPgXPr/mcb95m3OdpgbPOswRKEvseyBrNi+DVwxACO1JQactKQJWmzM5miEGfXHjkBmoLFZJYSKwQxJ7HGsufTXJWjWE7LPnCIGEUeojzT77Jfdz1N1ojzik52iXblTbU2jArnA2msYZhHFHUmmfzjEEa04liTrKSiYaTsyXbScRaSz7ODdvKIbEeFhlvdUKeVDW/3Ak4zkoqW7No4JmBd9IuZZ6xbiymI+hawfTsDBtFNFHKRBtWnX16geXJ2YQo7PLuo4dYXbO/vcMrd++CLllMTjFSUayXFOs5deXsgXZvvk60N8D3Q+q6RMUdlB/yq7+S8JWvf5Pdm2+DcJ0w5XubJFNKQe/GW3Q6/4pK1Rzc3mK1LJnOMvxZzul0zc3dDkYKhjvbzE/PyNc50SCmPxzR7w+wOPRbnufMZjOHnmrtPGeTOZ6SjE9P8Xwn5vn08WO+8+3v8NZbrxP59wgD5fRYSueCsJjPW4qOU+vtvf0Gq9UKrGHvxj6jxyPefut1traGLSLOaYsI6e430SJrbt68yd7ODrdvH5Cv5nzjF7/p7mZr27XXIlr0w3mRbONgINrk+UKLU1y5ny5OwMvh24uE+mL6ez5XXsKvXJhD5/P9qlyDPT/YJoNur/ECckVcPd6VXy7OiU8pBfzVjp/1VNdd6ueMk687pbj6w4XiiH3pu7uwRre/iQv7WWucx7Rp2hccKhMhETJASNXCmrl6k1w7zs/YNA3LhfOVPhofgZD0k5jlYsnZ2SmeJzk5PmW5XDrnkzRt2QmGwHduNNPJlKPuEXlr3+UpRVVW7O/vU9UVp8dHrJVPFMfcuXOH+XzGxx9/zMH+Pjdv3aIsS6aTOVVTkRc5SZmws7uD7/s0dcNiNqOqagbDPqbRrikQR9SN5uT0jCzLiJOUVEqyVcaj7DFxEnF2NnafU8qN/pCxGlG7hppKOtA22VAKtMaLY85nTtM69HieQiY9J65mwWiH5jS6cUVJhBMTM7pFYjpkjjUOZai1aX92elPn37+za62cGnxRYRDUTYMKfWQYQZQyGU/5sx+8x0R7hFIRhiHT8ZjBaEi/12e9WiKghdZ3sLZda/uOavPRRx+Rr3I6nR5+EBDHMYEfMFvMmIwn3LhxgzhJqWr3XJJtwWP/4ABrLc+ePqPT6bT0z5CyrEjimLST0uv2CMOA9XqF8n06aZfR9jZlloHRcPMuXjbDVBVBEOAnCVL5BGkXv9PB7w8JB9uopIPw/M0UsRdikZ9UP7s02vXmJUfDS+9/+gFfClE+5f1La+RnHuWvdvxUPt5Ne/O5EE1TlhVp4uDhol018tYDULVy/toYhJTMF/ONp7Fs1cfrpqFuWrVTa6hNu/gL4WB+0n1RTdMALTw38KlrGD99jJev+Jf/6l/RDUP+5m/8FtpaGt3Q6fVaOwK/vfYXnTRjDVHogvCmajZ2aOeQ+BLcjVeVBAiEH1CcPifau0v2/rfRVYGVIdZ6CE/g+YHzbBUOquspZ7cmcRYFVWvbVeqSpnS86vXkBE/5qCBAr5b4YoQIQrKTh/hhii5WqMCjWk7colxl1GXG5JMHFFbS3d/B+gFapfjbtwl37iITN3HDICQIY8enbRq2f9FHVxXZ8RGrBx/SVDn7/+Y/RAYRsw/fZ/qdb3P6H/3f6L35Blu/+jcJD27SGe2gtd5857quaBpn1SIEJEISRgG60eR51gYltKJ6EhGFrYc5rTJ1iR+EVE3heOWtlQQWVOu77EkHQV/XAXE8JJu8S75aMzl8xDt/67/D/mtf4/D9P+HJj79NU5UMkpS3vvlVqGdkyxUPP3pIp9/nK7/4i+ze2Gc8PuMP/+B77O7vMxp12N3dJvQqJscnHJ9OWC+mjI9XREHI3o2bdLsD/vjPfsCff/8hb9weEgUQxhkhJfu7Q9Jel+VixY1b+4x/uKbTiV2BJAjww4Bp02H6/MipaFtLU9cs5s5L2vGtFF7riahUgOUcyup0E6aTCWmnS6fTRXrSJeVCEISRE90ocoK2c17XNWWRt0l3SJQmpN0uYejgQGVZkK3XiFxgtHaFocZBpvz2GpoocsredQXWqTN7ygdPtrYgzv4tjEIQgsnKsjcK6G/tsxwfMT87ZX4yZ677eH5Etloy2t4jSVM3v4LQzemyIlutscISRjFhFOL7AVVVYpqaJPLoqgKvmhHoEqFLalOjqPnkvR/y+OFThttbVNqS7n2ZYP8dGiMwZYHn+e2DsmG1XJCtlpuAORQS4zlUThgFqNY7XPkCax0FxUHzBZPxmKgVQ/nZxwWY+GZc+V1cfe/87asRl7202aabvlFnulpWbo99AQf5eaBWgtbj9Dziu/T0+rR0/aL90EUQ7IWw89POfRHj3n4AKQSeBF9KAs9jGAc8WDU0nsUqD88YPCPACqzx6EiD1lBaqOMYeXLGvB6R9SJ0ositx3aoUNYdW1lYa8MHqwotYh49X3Gv4/PlYUTa1lm8c5V9bZBSUGlnN6UN5FVNJwxIQpeQzooahODpfE1V1ghrKaoGgpBOHHNYGI4mC+aTmioMaSrDgxqOspIbSci36jUflTV/OC/5d7djlhoWCk6rxgWMhCSBx3u55pnXp+r73JCSjmd5vi6Z+wl2NcUn4unxhETErKZr/to372O05Wx6xurkMZ7yaaqM/tY+uzffRNsG33dWmcoPN0VuazSB77EV+TSrQwiHSD+iKptNUqiUR5BsURcFy3lJ2kvopD6Ts5o48khLH60Ft2+NeP7wGTs7HZQ1SF1TTo/It+9hkXi+6wptRCDXGQ8fPAAMpjEYKdjd3+Wtt97k9373d3n29Bnjk2PKfM3X/9rX8IOgtaK0IB2/VSlFGPhIz3OoGd8jjkJ2t4Zs72wRJYm7VYVD4RhjUMrj4eOnTMYTxmdnpJ2Uk+MT3nrzPoPhVhsTSGRLPTuPDy7Pq/b36+aKuKiCcGFOXje3BJeQJy9iwmvC1otFL2EvTf/z476YWhcS7U8LMq++bF9c+yVU/Xmx4S9rfFZE/nlO87m2+ZwZ/OdpZtlL/2x2aKW5rv0zWcA0JdXyiGo9oc5n1OspTZWjG4PWDX6UIoMOyegWUW8XP+pgjUE3JdbUGF2j6wqBbbUgIqQfocIeQnoURUaWOcvS1WrF/sENjDE8ffoUox0fOIoisqJo4we/FV2tW26/oN/vUzc1H7z/Prdu3SJNU/qDAWmasj5Z89rrr3N6esZoNKLRmidPnlAWOdKTTCdTyqrCDxUInzRJGQyHlGXF6ckZd165TVVUFGXO5ExTlAXdbg9Hd3IU0rqu6aQdeoMei/mCyXSGPq3Z2d0lTRKyIkcMhjRFhvUUQZy4ZNxobF0hVUBTFQ4p6sUOBbN2nXVrnUWvF7pYx2i3pkkhMbQOC1qjq9J1vFshTxBo7dCpurUorKvCIXWbmrKq0ca06FvHNc9rjbZOhNMGAVlZ862/+D4Plxk7916nKCuePH3KwwcP+MpXvsJoa4Q1zqL2ow8/pG5qdvf2WS6XTKZTPCm5cePA5SxaE0cxnU6X6XRKkefcvHWLqqwYnz3exC3r1Zowilr7xYIgCLACwjAiL4q22eWxXBjCwNE57927z3Q2Jcsz/EZRliVbW0MqLI3vE85OnJ1YnJCMtgkHW4TDbVTccZbBV+77iz98+pT67Hfsp2zyqVP1SnHMXlhfrom4rs/SP3O52FQ9r72wz7OEXB2fO/GuG2dr5CxSHEQW66T46xa6oTxv0zkSQqCUj2qTk3PxM2M0de1EyIRsIcvaEAb+pjJb1zVBEDrvvKZBIFoVdQchNVrzp9/6Lv2qZkzG937/9/la3CH8xjfxfJ+yKPCUq+id+64aYxCeR1U6SLzylDumcdDvunJwcCklnh9gdUOxmqOMRpc5enZMMhgxf6LRyQh7+DFidMNxQKxBtrAPrHVWT7Qq5+vV5vsQUuBHTmnXNlXrb76iyJaEYQxYqsUJgR9QZzk0DU2zxhjN6vkzSHr0Xnmbzv2votIuVbZCxl0awKsrhOeSvEqUaK0JgsAFkFYT7e3TuXWbuqiopifIKOaV37jPrV//TYrxCZMf/5DpX/w5yXyGePOL+N0ennJCXp7yCCO/5cFKZwdWn3ulOzhelmUI2IgPIaHWuq0aNjTn3DwE5oL/NNbBgqrKQZ6llMzKmOHeq0SjhuVijcGjymZMj58QdrZZPX3A3a98A92UzE+fcHy6giDl3qsJb715h2z6jMMPPuDd957z+OmYX/7rf423v/gOH3z/286OzTYkScw3vnqD2XRGv9/j8NFH6GzB3iCg30/pdRIWkxm3b+8g/YTlfO7g9kJx95VbKAlGN/ihT2kjTuals6gQIJXPYjZrRT9ar/TMkHQ6LJdLwjAiih0iw1XH3X1X5PlGI8AtnCGZbtDGJZc5GVII4jTFAr3+kMY0eEI6frg9h2k3YC2hCjbVYoHz9LbCUuUlXt1s7hEhoK4ajKncAy0InHiRFFSVs/eyxvDg+YJXd3apygxdF9zcCrFLw1leYaylbiqqsmItHHx8tZiDEPSHQ3zlu6BfgtQrOnrGIC5R1ZyzBw84OZ224ks1WEMY+iAUd+7fw3ox3v7XCLfuYowl8CVFUZGtFywWc2aTCdlq3X6eEGshSlKSNHW2g0K65B9DFKcUee6CeK3xlE+n64pWzl7tZxuuE/Kil2Xbru55dLzhU15Y/a8Gci9Gay/IeRJ7/hQ5P8rloHnz7Pis4PhqYk7bd95wWD8t0b7wYNlc0PmrL66La3mrF/a+8DnEld0l4HuSRnn0QsVO7PH+ouBRVrFoDI0BIT0a0xALQaEbTGMxVtCkHcTZmKro8XjUJUlgbgQ7gcTDI5UwMRJbG34wy3m1G/LtRcWPFiVfGUa82Q0JjSZUHp60LKsGKaDSmko7C7/TdYkUlkEcECrBqoYkCkmCgNl8zdHzU/r9HlUnYeBJjvEYvz/FkiBUQzFQZJ7gO9maYRLx/QJOixV/fpbxj/a6LAPJJzUc1zWLuqYC3om6VOR8LAO+Wxvu+pKqa+lEBZ94PkspWN/ZIStzwvLHLAvDw8MTyCbodcZ0POX2/dfojg4oq5xyNSXq9PGDkLzVacknhxjd0BnsYpoKi0Ypj7JYYFSCFE4XpaqgsZJa9Sj9gg8frbm16+wR40DhS0OVZXRT19V69OiEQScmKQv0J9/B330LoVxXr240QiiUr5jN5mSZ60wNhgPSTof79+8TRwG3b93k4w8+pGnsxmaxyDO3BreiZ9I7t7503cPzpHE0GtLrdVthVdtSbZwoa6ACDo+O+ZM/+RZVWRBHAdujHm+99Rqv3n8VY0FbC1a0XWWXfG/S5pfrTJem2IuWzQvEBhfm0Ms7istz47pjtge4dr24tMMFB4MrJxabpLztmF+YgC8KaS+OfbkG919T0v2XeAp+2tN83k55u3YZjIM04xo3om0+mKakKVeUy2PmT3/M6uQBwjiapm0axmcTtBH4Qevwg+DG3ft0RnsgPKp86URuixwpPcK0T9QZYKyhylfUdcPg1lskW3c4OTlDa8NqtSKOY6w1nI3HxHHEauW2nc8WFGXJarnglbt3nc9zljOdTmmahoODA6azmWuSac329hadTsp4POH46Iid3T0ODg7o9Xo8fPQIX/ncvnMHX7nErapKqqpkvlgwGAwIg5DJdOoK7Mon3UoBy2KxoKpqiiInz9Zkec7e3h7LRYG3sduF3Z1dF0dUFZ1u19l1GY0Xxm4+t9a7GINtaoTvoNZI5QTH6saJVVrrGmjQcsFLEOCFEbooEErSFLnTzmkawNFpPeXTWEeBNcZ5eWutaWqN1i4f0U3tYn3p6HuugAx5XpIXBV6ec3b2CWdWIaIOVeOcWlbLJTdv3WJrZ4csK1yRtyrZ3tlxlKYg4MnRMRrreOJtzvP40SNGWyP2911ijoDqqMLzHGqnLEq6vS69fp84jnn+7Bm379zhwYMHzGdz+v0BUYssjqKYs9NTZ4eWZezu7ZCkHYIgIM8yOr0O1rr7s1EpSRTSGw1Jt/cJ+yNUcI1S+bWT5Oqk+py7fMb0+ytbNi7WSP+K16bz8bkTb9UKPkVxjK88yqIkSTv4ysct44aqrp06ed3gB37ryW1aqx5D02ga06CkQkjHs0Lg+L5N43hbbfI1mTioifJ9ktgpRNd15RY4C588fUq/rEBFPJ5NOfpP/xNuD4eot9/BV06gTUrHKc9WTmCml6Qu2a41QjpF6HOl9Dhx4nHZYoapa/y0i8mfYT2FEIry5DHR3itIDGq4z/zdPya59To0FXVTkSYdZGvV1VQlVmuCMCKIks3DzlQlTZHhhzGLySmNWiIQNIsx0c5tjIowywlaCsR6RrWc0jQ1xXRC0UC4v0u0f5dsdoo+foKMErpJF6kCZ4FQLfHCFBVGzs5LCJqqQvnBxqvQj0O84ABrLEIpojAi7t6nc3CDbDGnPHzO8sffJ755h+TmbaI4RkiJJz10+3dFOLuJRmvHo69KqiInSTsIIZ1XtKfa6ptGa+UWKhwtIWgtdpxSvKvoGeNUb601lKKDiBVRuKK/s8385AGD0TanDz9hUSne/PI32L11m8XkhI8+eMi3/uR7pNs3+W//g78L5ZTx0VO+/4NP2B8q3nr7Dq+9epeT50/4+MEzVlkFVpN2Oijf4/atbSyaqpE0Gu4cjNjaGvHujz7hzdtdkjDABgFCDphPZ8yeHSOsJgmF47UIycx2KaqSRtcObuQHSM/DWuf964eh8/FuXCGozHME4PmO61yXpesGJzF5tqbMnZCHVIo4SUiSFCOcfY8KQ8qicMcpC4SUlKakrh0kyFMevlL4gd/aT9gN8vCcCxz0nJd2WRSURUEYxa3fp6sCOw/IjKapSZO05e/7aCvIipJiekrSGaKrE8I6Y6uzix9IYrFGr4/QpkOY9Ag9S2MtpsrZGqVENkfmY+r1GXWxYjle0RQlk8mMxgqeP37KfDKjt3vA9o179LoxXtghvfkl4v4BeVFukCVCVDTaJellkVM3TgAwSlLSJEUqz2kT+Ir1erWxErTtPJBCOk0H67zVpfQ2Ogc/6zhXPr4Y0tpNosqlwHYTJF98iQt9r/NA+0rn6UU1+EKgfg5hv5rbv3yBm+Huh8vH2aTO7YHENcc6t2Z7sd3VY19M8C92xC2IF5ZMLhFovx+BK0oqRRIY3uhHZDVkIkDWhlxr6rKmUIKssVRKEqPRVUVjLDbt4K3X1EXJfHeHwgpKC6mC2gpqK8itJGo0ZlXxSuoz15LfOSv44aLkK72Q3UgRtCKY89IQSog9yboyjHPLLKtYrlf0IkUtnI7HXj9i1ihOmpDHn5xCZenvj5BxSj0ymAc5YtLQMVPevtPjISV0NDcjyemi4chY/vfHa7bThLFsSJOQL3cUD2zDj3SJbw1aKOZC8Ecl1NoQ64Q48slCySgyHNs+Qgp+PJlx4Gmy8SGR1SivQ3/vHtPpgiQOyLICPx2QT09B+CwWE6q8YOvgHnE6wEqo1wuktGizIptP0cEAhKLIVizLhtu/9u+wtc6YnR2RP/8BWh+RFzXdWGGsJltnDIYdunVCb9hntVxz8qPvYW58nc5on8ZYyqpmtLVDU2sHv9U1QeDzzjtvcfvOHT56/30ePXjA3bt3+NKX3ibPc/qDLsr3aPLSwUuFRGAR8lwk1YmVgkt2rXQ6MkKK9j5z3T3fd+9jDbvbA7LFjHt3b/H6G6+zvb3TUnI01vNAOieP8/vVXqB2XMl1X8zRK5Pl4swSL799uajF9cNeOP7FcPbq8c7PtXntyskuCRBtruiaaxEXjnUJy/6XEJX+pQS2n79F/TMl4O2Olzr+F75401TUxZxiccr67BG6zBFBTDzYR1jD+uRjlifPWM7GeMLQ6/cxVjA+OmmT4JqqtoRJyI1bt+l0uyRpSrmc8PzRJyxmC8IkRskAi6XUlsViCVozGg3pb+8QBT7F5DErPUJrQxTHVGXNcrFktVy5Tqe19PsDqqqmrit2dnbodbsslyvi2NnmBkHAYDhgPl9wcOOAg4MbWByi7sMPPyJNU5IkZr1es1wuyPIMT3ns7u1zcnzMcrEkSWJ29/Y4OjrC8yR1XbdxnOWTjz/m4OYNPvjgA6IoYjqdsru35+DRLS9a+T5CSgYD50X9/Nlz9g8OMAYm0xl5tuaNvRE9QSugGGF0jVQh0g+dWGIrfmmbGoxF+qotigC+aONM8+K5Y5x7khECPM+JHuMeZ1VeuKRbaxe7nnO+2/xFCBDWINomote6+nhSMF+tWK4yVuaEDzNDdPMue8Oaw+MTZtPJC/qlEMxmUzzpsbW95WzHpOTw8JjBYEBZlazXa4SUPPjwQ6SQbG1tsbOzQxzFCE9yenJKv99Ha83wYEjaSSnLkrqq2dvbBSF49f59jLEs10t831GBwyii1ppAuxh4vc6pa02auMZfvzcgzzPKsiaOEoowwtu9TbK9c3Xl+xnGT97/PC65ANr5ybteXVeuq0l+3mv41DXq04sJP8u38rkT73N1YmstRStUJtsukfI8Z2Hie5vg3fkGu0l8ngS4brXBj/zWFzBC64bAD9qusEVb0ypqO0EvT7pKdp7njlMc+OTZmvlixg2jibAsy4qszpD/j39C9j/6H1NtbWGxBGFElDghKgfTNQR+gJHaWYo12t2IdbMRlfKUjy0ymuWUoDeiPnuGJ3EVx9PHGHzqYonAEiio8iUISVXlSOXjKw8jJboVZjgPMKV01mtVmVMsZ6goZP34A2QQ4Q92mDz+0FXbygJpDeV8Rr1c0JQFFT621yO9/YbzLvQjgt4QhEeTu0VZ1xUy7hAPdhAC14lHYFs7syDwqeqKUPkgpYMYA+u1q5bWTYNQivDmLYL9G+TjMYsnTxjcvYsKApAeKmiTmFYl+9wGSymfsD+kqWuqsnCdxxDnV64bpFJY6ywavFZ92looS0dLcEmo86t2AYZlXTR0fImf9Dh8/3uY+6+S5RXx9its37rP4vQJH//oe3zrj79NVlh+7RtfR2cTzp59zMcfHeIJyzd/+asEHoyPnzGdjPn40QnPxjmjYZe/95UvsVyumM/mzJaPeff9R7x+e4v+cECe5bx2Z8TuwTYIj8npGUeHY27fv40lQ1pNUxY0SrLUEQ9PCnxVUtSQIliuVlhjHXe4qkm6XbrdHlm2xpOuOBNGoYN8wQYuds4xipOEII5b+JTnIFltRdhpLTje03q1YjAaOa7OakWZ5048RDeoUhGGEUY3dHt9PE+SrzK0ObeIM+Stp7bNM4oib8X0DMZTdLo95tMxQlg6XkHqzQloaOYFk5Mjzp4/5tHjQ37t7/wG+7dvoKRPXTauoFVVlPUZngyotEGwxM6fcfjJ+xweHnI2XZOXDbap8CQEgUdvOODG/de58YUddl/9JVTSRbZJNkK2FWhNlufoRrNerRmfnbBeLihaZdAoSfBVgPA80rTj+O1aE3e67j71nfK7szhz3W1tTdv1d2ryP9e4uuC/jEL8yeOzosWXIu/LD4Hrdr02CT+HZAkuC3lejDbtBdmoT6tGX1AVenGYiz+5/4mrO15K3gHjGN++kFilSAPNOwOfzBi+NbNYI5C+T20MFk0tBAZojI9FgzY0SYxXVojHzyl2t3k2SunEkqWyjDyD8iw+gnFWUWLZjnwya3lSGCZNwe1Q8mYnoCcBbXi4tlTa4GmH0Bova2bzgqPK0ixzmrLmvSBk506PeSM4KxO8RlJ965jQt/hJBKahjALM2KP81pQ4kJwGJUnHspVaphZqJTg+qxBVzcIYft+T2H6E6CpspMBTiLpGGNd5zTyPtZWMCw+fDiZWyP0u/2w95++cPKAjUk7nU25udXj/vU8QjSaOQla2QEwqAl+hwiGDzk2G3ZKwddiQokXfYBCmQZFhi5p4cJcoiEiSlNH2nGefvE9Trln0buAHHyNEzagX0mhLuSp5sqp4860bTE8W9IddPBOjlHTdpCBi0OlTVw4hs7e3wzvvvIVSine+8DZ//Id/yA9+8C69boff+q2/wy/90jdodEO/10NgnZuJdG4m52i58+H7PhutbyldAN4qN9tWIMloV2T76IP3eP/dH9JJI3a3hsxnC6K42zpSWKS2COlE2KS1CHEhE7uqpHXd5Lrm/cuz4sWEOE94N4HmdRPWXp7b1yfdF4pyL71pr6xF11/fZtOra8tLFcCfcXxqdeEnvH/dtp+2/U97jVe+r82lXAnAja4ol6csD99nefqIulhx9MmHzOdLtg5uMNzZxZOSYjXj+PCYpnZ2XvPF2iGuspxnRzOK2hD4Hu/cusFoexs/DJDKJ466bN2EyeI9pscTirxgPF2wWJfc3N/l5u1dVBiim5p8MUWEHTLRJS9yMK6j3Ov38TyPoigpWtHgtFW7RgjCKGS1WpNlGWEQUaiS6WzGarVsxVAFfhA5teswIAh8kjTl+OgYYww3Dg5YrzOeP39O0KpqS89RULV2vt+j0RZ5UfDRhx9y584dsvUapGA8HnP33j2CIGS1WqF8xenJKXmec+f2baSQaO3QKR999DFf//rXqcqcBw8f8eTebb50MEKvl06NHIHwpIOIt9ZiQjonIhVH2MZdj/A8PFzzTXo+0DiAuTWAQ6GalsYqaZEugPQksoXin/O7daNp6mZTuG90g9civbR1otOnsyXTZcYib2iGe9y6eYP3P/yIJ08es1queOWVV3jllbsEQUja6TCfzXn69BlxHFFVrmm5tb3NdDalaTST8cT5fMcRumkoi5J1ltHpOq7+fL6g0+1wNh67/KSs2N7e5mw8xvMki6VTtPekx2Q8YbVcMhyNWC6WhC3iwrlCeRRlSZblmNNT0sQdv6orZtMVnq/o9PoEfvjys/y6yfd50SMXx8uhxktvX//e9YuH+BzbvLTxpbX3s8qkP//4/Im3OYelCoq8cEJkygWvZVUSRBFgnUialE7gqd2vaTtJURihzuHLnreBsSZJQlEWVFWN7ysHFzW25bNWrFZLAt9H+QGe8pme5lRlRSE9wiJn7SlSCfHTR8h/8V+Q/xt/n6g/IAxDhHUPbKN1C4nXgIO0h1HYJsQ+Wjv4vPQFDX3KkyeouIOWTqAqkB6mWDnY72qKF8eYKsfXNUVVETZbICRlWbTq5TU0nhNksOfq7xKJ6+JXdY70FXo9I1eKcjElCEOaRmPqJaIsKPKSvNSIKKYz3MPr72KsxFYrrAqgqRFhgoxSvLiLjFqOy3KN74eoKEFI2Xa7JZ6UNHWDaCHJnnXwmjy3KN8njJINsqB36xaytQqTnkIbjZIKEJvueZ6tiZOUbLnA9338MCROElcxqyrXcQwCrNFUReEg1HnZ+jP6bVfYQWyEAOmFNJXjgVeiS92MidI+cTcm9GN+8V//h4DF1GuKomC+bth95Q08DHdv73D26AfMJnPG84w7r93lzhtf4Dt/+EfYIOPDD56ihOGXvrjL9u4uoRKcrlZMJlM+eTKm3/HZ2dlCoImHKVWl+Ojj5wRtF/7GzV22RkNWUhJEAU2V44UJWRmzWC+RApJIsZqfUWkBVhBEEXXjCg9VWSKEdHBJ5ZNnK5Qf0usP6fUHlGWB1g2ytcxzsZJ1xaE4IooSyrJgtVw4cbQwQvk+eeY8HV8ozdetf73rBkmhKIrcVZcDn0D4rT2IT7ffd0Gf1hvv67rOEUiiOGVvq8O+PCKuD6nXOYXt8sHzE6r1lLwRjJcNTz96j2x2QpHXqDBAKR8rfRZnx6zmE6TX+hUHEX4YI6MOfiLw/TVnx0sGt+6w+/rX6OzeR4ZdlArxIoeysBZkK0S3XmfOpi4vWK/XnBw9Z71ccnZ6TBBEdLqOHx8niVNftc5PXvk+URiifJdUS+mhIr+lxTTOnihy9iPn2/xM46VgW1xe0MWV115Kj18E4pcy94sHsecc02s6zhf2vWyRdR0P+0JysDneZV64y8Ov9uSuPh0F5y3BS3DUtmR9HUT10sc/581KHApBgo8kigJ6wFcsNEbw3goWVUOgBTMjHL9OCqQPK6koGycCqaMA4Qd4p2fYxZLF3jZZJ2Duw8A3IC09KZitK161llHoUSGptKUoLMdNxa6CntUIK1k3cLIoSJAIA17gMytL4k6ParmifF6zOFwguw2qF1BmGhP3WOdr6pMGP6tQ2ZJ6WdFkBj0x9G2DiH36/Yim51F78G5TcyINmZDOo3axgk6C6PvYrgHlY3UDVQ1VCVIg1kuaQx/T7UDq86Mg4TC6z2uLkJvTlOChxCwCVBYQlGd8+5dKTt7e4q9t9fn793YY+Bo/iimzObrOwSpUELv5EVSOZlQVmOwxdSWZT6dMJxOy2SmLk2c8eP9H3Op2oFziedDrdRmfLbCNh/ADsjzDWE3eLEgWU7yogydCyvWCME6ospy00+NXf/UXN5Bso11AWxQ5VV0TRSFB0GnXwXN/XcfJF57HdLbg448+ptPp8Pbbb7ZFOgij2FmWIqjqmroqOTk+4b0fv8eP3/0hs+nEPZvkiNU6J8sKyrKirh01yhrbxuatW8CFaXgxed1MoYvaBdd1ac6FCy+8+GIeuOBdXJxrF2ghm/Xg4hqyee9CMeDq3HpxtAuvvYC/Xz7ulXl6fj2XCmt/eUHnS+PC6S+wYa4Pk8/hW5/vcJ///OfHvBKjW2soF0esp8+YPfuQ5elTlIQPfvweZV7y+ltv0Bn0kcLp+6jBCC+ImY7PmI1neFIxXS343rufsDvq8/qbd9kebaF1w7PHj/E8SRDFpL0hSW/AO1/9Kg8/+oj3f/w+/V6HN998k2E/oTsa4AcxjQG/O0KrLjp37iVVWbOzu0OWZayznOVySb/XczZ9VUnYPgfzonRd9LJESo8bBzeom4pa19RVQ900jLa2ybI1TaNZLVcsFnOCwHedViEoyoIPP/iAXq/LW2+/TVM3jMdnDAdDPM9zyuRY7t69y/bWNsenJ3Q7XSQO4TraGrFcLBiPxwRBwMGNA4eAFJDEEXfu3OH09JTx2RlB6Bp0z8+mfPGVm4g8wzQlKnIe3R4gqgqkoGmF4bAWqxs830cohTEW0TQv3HXqov3XOZ4IrRGeR1OXmLrGWg2NbRN6524khdMAqSrXrBLnlEIpUEqRSo9FUTLNSqZ5QTbqo+KULC/ahkhBr9djtLXFYDBkf3cXIQTrLOP+vbsIIXn8+DF1U+PNFGEQ4g/b5paUKOWRpB0WywXHx0cEwS2SNN00c7R2elvWGLRx9N/JeEIQBnzy4IGLPZG8cucO63VGt9thMBzyF9/9LoGveOcLX3BLq3Hil1I60cuqqgiiiMNnz+j2+ty+ddvldT8JufJSjnrdxheq/1deFedz/8IxLlJuXjrkpXjp6pufc9265nI+ex1xH/JnRdV87sRb4NRALU6VWinlFMOLknPrrsAPnAog1pl2t/Bh5SnqpqGsSscRFhJrLIvlnMB3i0JdV86aQPnUVY3F0NQNjTYEQejslhrnfV3kOU1Vc+oF3DUFC6XoWItva9SPvkd++xXWX/16q7buBKeiKMI02nURlQu6nZ+v45CdWxkJcN7W3QH1YorRBmkMRgX4lcBTkmztrtvkC/R6gvQi6sWUoL/lEvwyR/kR5wCzMs/QTU29mreK4T5SuEDHCo/s5CkqjJkfPaEqClJfoLRhWdRkRU0nUYSjHUzpRCPqxQSV9vDiHsQpVnpI5bkKv5Qk3YHjwekGNgmZ49+GkfPt1o2mMjXyvMtvLF4QEPnJhmtdt8Jw1lqKPKcqSvd3D5wtz7kPdZSkG1hOYyukJ0miCIzdwFmN0eR5Q5nnWKMZbG0BTtk2z9YoT2JxdlS6rvDClCqboYIOcdLHSh+BxvM95s+P0CpEeSFvvLHP/Xe+xMff/n1mk2OMF/LVb3yVm3deo8nGPHx0iPEUHz4+429+/RXeeP0uZyfHzE+PmI5njLZGHI+X3H7lJqfTOcVqxd7eDlZKbt/ZxRoQ0kf5kmI5J0wjrDaoqMOjueJw3qCNQQvJItcoz3VIiqKiKAuCMGIxm6J8RRBEJGkH7/xvhaGuCprGWd5FccJ6uXRokMhVObNszXC0ReVXNHWNNRrfD8j1miAIwRrHP/cDwijEEw4lonXD+GxBfzBENK76W1eVq27mOWEMnnK2f2WRE0VR60rg40nLdrCkZz5Grw4pypKqbCj1iqo0/P6fvsev/tJX+dd+82/Q2NIJL65L5icnTKcLjg6PmEwXjIZd0q09pBfgkSFx1j7dpIeIdtl9+2+Q7r6K8SK08CjLhhDX4asr91mllNStsF+R5ywXC0cRwVWm004X5fvEaQcrBH4YYax1Nhuecp3sc+52K7glgKqqnOtBEBBHYYsq+Pmg5pfHtdH6p66u1/3YhtYvOmTXHOe6Yu1FSNhlt952u0ueWp9ygecx/YVE4So8FV6Ogzfb/aQHMxc+nb1wHCmJhERFkkB6fF0qrM14LCBsLFpKCiEIypq+lkjbsFaKXEqsthhhaXo9ZF7hffKcZtBhsd1nGUtiXzAThi1f8v1Vw10TsBsGeFYwXq2RpeHjShOUFX4Bctkgc4ld1ijdYPExGOZNSRR5RIFmdTSnOYlp/AUm9qiWFqXBYqgwFGWNKTTeKkdWDQaBXOcE8xUq6aBij2/GiqNizSOjmQc+a2PQ0xJzukR0Y/AtmBwvWyDnc/ACTJJANqdjPfZEymvjkq89a3j7SDGqPDyjqfWKD9Mp/2LnI47MDe7WQ/6t11/Fz08Zn03pb+2jmxpjTQuZFCgV0pRrhFQEcYquGzyvZjlZ0xQZeV4iwh57b34de/hndOOAonGF5X4/YitOGJ8umS5q9MIwmWZ0vjQl3btHUZb4vk++XrYCiwW6qfGVoq4bRqM+vW6H7e0tdnZGeEpQ1SVxHGP0C9i47/ssFit+53d+n8PDQ7721a/y+PEzPvr4Y1599R737t3DCKjKiqdPn/KD732fH737rkO1KW8Du/SDwAXn54iaVhzpPKkW19yzn9rtFi/mzMXJtulCb154ucB1SbvhuuLWlX9ccHpNEe1Ka1xcM79fCCm+fNyLl37pZ3F5TfkrG5bPd5bPeSnXBsXXvHgeQp8XWGz7u6kylkcfMHv+AflsyvT0kLST8N6PPiSrDG988UsIYTl8+oT1YomxDpHW7aYIo9FNyfhsSVUb+mlIGHospksCFdDtd0j6A8o8ZzVfcnZ8RtxJnQNHqHjt/m2Onh/T1Bmel7CazEh6Bm1hcnxEuJugPB/Pa2h0BliiOMbzXGwthHMmePDgKds7OwyHLuH1lWK1WnNwsI/wQJeanZ1dppMp+XrNbDZtnUAcpe3k5BRPepieoawqzk7P6Pd7eJ5Las+V0Xd3nCBvlq2Zz6bMZ3MEgtFgyDrP8AOf4+MTlss1j588YrVccueVu0RRRKfbQQrZqq4LptMJjx89YmdvByklT49OWFWaVHoIXLPGVjkY47rX1il6S+U0jdAaK1zzD2vwlL+ZLw4qLtDLOcpToIwTD1MKozV+S/1Tnmph7C3sXGusxaESjUFJiZES5UnyuqRB0BjDSoX0btymkj7f/e53mc2m7O3vYgykaUpZFcwXc4IoYjDou9imKdjd22M2nzlKotacnZ5isNzYPyCOYzzP4+nTJ3Q7HdbrjDTtkOcZ2TpD+YqHDx8ShgGeUpweH5OXBfXU8c+7nS5VVTvEa+CEbU9Ojul0OsxmUz768CM6nZRer4/v+ZycnrI1GpEkCXmWEccRh8+fkiYJW1s7m7VDXFwgfsY5enFavrTmXogs7KeuQC/HTi+L3L54+/OWD8Vnbv3zr4WfO/G2WIoibwWj3AUZaxGeRGgoy4K6dsldFEUuYZUC3w/bbqbXQtUNylOotjpGq0LsqRe+4FZbau2SPt9Xzhe8FW2zxmCbggjLiaf4ZW35UHqEdY5nDbZYMfr2H3K0tcPq4CZxp8O5d7er4tcIzyNqOQ3WWIR3HmUKHDStwu+PaKanjpPd5BCEGE+5BLEuECqCcoVcnkJ3F72YIKKIZPuA0hpUnKLbv5HIM8IwIgwC6myFsaCEbYVmBOnOLSaP3kdXJU1VMl9mBNJS1AYjFF5vC5kMkUFMs5zS5Ble1CHsb6ONRniKMOni+SG6KiiyNX4YoaIEqZRTlheCyFMuebOuw+1Z5zduz2HjOEh9XVVt4qc39ALdNKSdjvt74xJ3QWv/hkZK4TjDpVNTVMoniiPHFa4qEK4wI4SgKgqscYtYGEUEIZimaSuW7l6pqopVDv0oIRrdIpsdumKCaTh6+B6lDnn1jddayNGS2WzO7/zBj7l55xZ/9wtfBduQrebcPuizLjW/9pUDXrn7CsvpKYvplFoLbt/cY7Fc88YrA4bDhDL3aMqcvCqp64Y0HIIvqIsS20BvtMV8scTzQx5NJU8XNWXlksDAV9S1JiscikMKr1XR1Q4GaV5wkf2WWnGuPpmmHaI4carxnuM91VXdBokORp62leSm9Zz0fcebz/MS02jixMf3HYRaSkmjG6cm77vKqWlc0QJcwq2U50RDWsGQqipZLuZs9SIOognp+pC6WFLXDXVjqY3zNu6lHmVt+O3/4nfZGyUc7I1com8MRVlSaoHxe/Tuv4m/dYv+zft0uz2HbmgKdF07DYKkgxUSv7UZ1No4TYJaU1XOFkg3TQsXt2R5RrFetZZBGk85KH2cdBCeRyd1avBBFFPkGVj3/SvfR2vjOom4wlmUJCjj7M0slrppCMPIbfdzjMtL9U+ul27W1osxNJ8SMF7aWXL5oXTNJtfE4pvtroGCiQv/PQ9C21wCKz5l3ysHeRGkt5teCKQv5gAv+n/ucep8ad2O8jwFUR6eJ7nbakrYozWq0JskRQrJRCpiSkRjUI0mM9KBCYXAxhFNGCHyAu+Tp9BPyLa6EPqsak0/8FjahoN5yU7WsFUK+itN3PjY0kM1Bqt9qqwCraCxYBpsbTEa8qWhEJo4EshywWraoKcNlBptHCTaWoMxYBqD9SxSVMiiButhsxImpyAthS8Y7RwQ1TnN8oxindMVAXkU8jCF1e1t1CgmfjSjP6vZrSq+WEtuzCx3ZoK0XuApjfF81t6C78RLvh0/54NowlGv4P5bt/jHX9tnO4SONEzznDBKmZw8pTPcxUO64lgQU1cFy8kxo5tvUVdrorhLkc3Z2TsgTrpY4SHDhPrhjzB1Qdj10IVEW88FqrXm0bMZvV4fT1iMFAjbYNFgXZHVWYOGlJWjFlnr7uc33rhPGHiMtrdI4gipPLxWlE14zmZMSldM++STh5yejdnaGuEHPr/ze/+S/b1dut0upycnPHv2lO//xXc5PjoG67zYw17PFRd8RbfbZXtvn+Fom7TTJQwjfOU0N8654aK9117kqNcVnl4UsT4VKn5xnlxTY7v074WDXOq0X4wlxWU8y4sZdh7H2JcXkZ/QgboUqm6SlP8aku2L42KF8CeMz91pspf++dR9miqnWs8wpkR4PnU2Y/H8fWZHj8jXa4r1Cj8I+PBH77PICl5/802X6ApLz47w/Ij5ZMrh0YRnRxOUEoS+amNAy2jQZWtnmyTtsFouyVZrkm6X4XBIrz+kyDLyfI1uKsqiJssrhO+zXGZE0YreYMBiuqA2hu5AIKoaL1Fk4wzdaBfTearVXoopy5JerwdCcHZ6xvb2NoPBgOl0imqLTwJBWZYcHx2xv79PURbUVYVSik6nx8HBAWVZOYVva5mMJwDs7e07PvaTp/QHfcIgIAhDPCE4OjxCCKjriiSJkZ7HcDCiKHKnE6QN2zvb3LxxE6lcTlDXNUXuKIrr9ZrZYkFWOErZjZs3kUIwrzQdP6DOlviqtWaVEtHqtqg4dRDzuuS8cGa1xlSliyl9haidgK/wJGHadXpSuHW60TVRnFIXOUIpKl1tqK621aiy1jUDy7JCW02tG7SuXXfcGAa7u2y9/WV0nFAt11RVya1btzg4uIGxljh2Ta2z8YQgCNje2SVbZ6zXK3qDHvv7e/jK2bCxu8NiPqfRTRsPl9y7d895sg9HzOYz5os5dV3T7/cpexUYQ5EXxJ0uw61tnjx+TL/bJY4jppMZDz7+hHe+9EWMNTz45AFhELK3u8d8MXdCbVFM0OoRTWcz1llGXZUIbwuxyjg8OqTb7TmR5E+1Lrk4PmOba4B0n7n7TwG4ubyEvIhpPmtxuXo51x3j087ws4zPnXinScpqtQRjnLiJlC2nVGFMs+lih+dw8haypdsbR7SYrbqqwHcQnrqqXafN8xzsQ4hWrA0oXGIfBqGDpHuSwA+oqoqdwZC/uZ/w2w/n1DLkDWvp2gYpLOgGuzyh80f/P5a/9W9SKUUYO39kKT38UFBXtQvwhLO2qms3ydxD2UFupRQkd94g//bvUszOiH1JrStk2EFasLqAfEl58gjPCLzhAdnzB0TbNzDGLVRYTVMULskwBqzG90NWs1Mqo/E7fbKjhxjPp7t/h+fv/jmmrmjyDBt4VI2httLZGeQrRFMyffBjrDWkB3fJFxOCTh+JoMnX6KpsbSo6WOkhWkG0qio2tlRAa//mFF/9IKDInXK6pzyaytmQ6VaNPYqStrvoFuuqLinWGXHi4LzS81w3v26o69qJcBlDnq3JszVR6ILQJIwROPXuMHCiV9Y4BEIQRhjPceKjKMLqEk9KqqjPOJsy6t2g192mmD5j8ux9ot6QSDg4YTZ+gpYJB/fus/3uh2gUdZEhqykfvf8h6dZNypMjIt+nXK/43o+O2N/rk3ZC5os13ThEBz516RQp/aTHep0x6CUsV2vn2TsaIj3JbDIlGQxY1z5PJhmVhbpuqFo+v1LKWdi1QZupa4o8Z71aIqUiCEOMsShfuYe2dBYXdVWyWi4Jo4g4SVkWBbpyIiVRmri/Qds6CYIA3dToumatnSBh2ElRvo/FCQbWVYnzpXcdbYRAeAKtW7EQrV3xTGg6aYcmCCnKgp3UcC98gleeORuwbI3WgqoB3YAVkizLuH97m2ynw2q55nS2AiTLwjCrA4LOFkEwYqBGrCZLZqv36A9Gzqc8DIjCmEBoGr3GWudD3tSNo2QYS15kgKAq8vPeU4u+aPUDmoYwSYjTjlNyF07Z0w+cf7lzMoAk6Tj/Xtk6F3jKVXCLjDzLSNIOxtRuTaprrDpfUH+2cTmIfhFgnyeYF7s5Vxf2TVB9sTP2YrdrxqYHztUnl3ipAv2iZ33Ot770kLnU4rIvjnFxw8+Ici8WGl5QxC8c57yB1ybvwl4A3bZFPQeZtTh987Yhby045DD7acBXti350YpZDbFU9EKB9S1NHOM3DXVecKoFY20ptcYAGIuREUaHeEWNejRF9GJsP6EUNbcXFfdPNNFZhZdpCu1RC+26RkIipEOvGCwSgZyvsJW7VC0sBiiFxHiaqCuJghAdNDDTmNygAWE0NAZdN47LLhvseoKpRdux0SjRwPiY7d6e09MIJf7ZR3AieT0QmFJg7gu6meXLk/uMFgl1MefEG/MkLahMyVIK3ut4/Pm+4TSqMfu3kN4Ov1o94++9MWRUzLl36wZnxw/Y2rrBow//gjDtUxUFtREIcpryOWW2Zv/WKyAFftQBKfHjPsJbkxrN/Xs3+OD73+Xhw++isxmJSgAPhKasGiazM0xtmZxOSLsRgYQqn1NlCzw/3AirNq1jia+cCKvF0umkfPHLX4T2ubTR/5Ae1kJTNZycHpIXJY2BL335i3TShB9+/4cc7O1ysLfLn/zRH/HJRx86VwjfI00cJUcpH9WKiXZ6Xfr9AVvbO2xv7zAYDIkT54CgPNVS5RwHVLaIsXNXEsGLpPziEOe3/eXpe33e+3nHBu7NZe9osZmq12T715zpmiD30+Ldn6Yr9FcyPjOevWYh+oy16dMOdenvYjTL00eMP/lz5qePWS/m9Lb3sKahXCxYzifMpwu01YxPZ5yM53z9l34RpGR8dOJoGkbj+4rRICUOJW6qS+cwUtV4vudiAz8gCBRhMKJuamdzGcaESUrYGeAvpxSrOdEgpj8cYHBaSVo3pP0hcXdIWRQU6yW6qSmKAixMZ1PyLCeMIpIkJkkTpuMpWjfs7u6gPIc6ffr0KVmW0e/3kdJ1jjud7kZBfDp13PLdvT2eP3vCl7/yFTzlMzk7Yz6Zts1kzWQ8ZjqZMOj3gb5TxF5nLNcr5rMZQeBz9949pPQ4PjlGN5q9/T2GwxFVWSGV5NmTZ4y2hg7NYizdXpfj4xP6vR6h71MWeZsPKLJ1xuPDI/bu7TvB2KYmSHvUqwXaNo4a0lRY7RCWMo4B0EWB54dOjE14NLZChT5WW6wfYOuGRnnoonR6SNY4wWdc7mGMdn8jT2LbJkBjDEJ6mKpBAoGnCJVGBSF3XnuduNdnWTmOeZLEYB3/vj8Y0Ot1W8tDxbNnz+n1u3gtdXG9WjMcDB0CseVe7+7ukiQJSZKwXFmeP39O06JR0zQhyzPyrGA8GRPHMdPplDRNuXnjgLqqObhxwHQywfM8kjRlaI3j5/s+91+9T7ZaMxgOuH37NnmROyRQ4NPppM7CNs/RunFNEV9xdnbGcDjkYO/AWbnK6+bY1dXj04KGyxXIi3HDp61CP7nYdn2m/nnWs+trjJ9/9f7chcB2fO7Eu6pKhJQtXFW5gLmpKaty8xByFZ3GVfnbDpNsrccsrstatdBWTymcTZhC62ajWC5i1/MwLVxGG0ueF20VrUZJD683wrv/RW4c/Sl/rgP+Xb0maTSeEjRGI8qM7vED6j/4XZbf/DWqfp/OcIjyPJpGEyWxU0EuS4wx9IcjbZ9iuAABAABJREFUwLoEsnBQ6CjtIJVH7/4XeP57v03x+Iekt+8TJB1E0sdrZhhT4QPl6ROiZMj04x/TfeNrrtJmtOOatA9G0zjFZWsM/a09svmYajnDGsP69BFRb0h3/zbjj9+lqmuy1RIhJH4cga4p5hMmZydINFFvgKlLhPKhrhB+iPJDytUMlXRRfoBQPkWRky2mKE+5rrYKHE/e8zBW01Qa5Xu4hC6kqZv272eIWs/lDfy2fdibRiM9pwh9jg0pywIQxGniChllSaUbOr0+YZxQN/Wm025aH1lrrROxkJ7zRwRsVVIUBdXy1Nna1Ja0t8W6WRNKSbTzGiMhOPrwuwSdDiLoUhmPRx+8x523vsCv/fovM5utieKE1ekpyeg2SRzSDbfJs5IPH54wXZZIMUHrmsj3mfiSbrfD06MZvq9I0oh+L8EK91Cy1rKYLYjThGQ44nBc8PG4ZF00yFbF0xrj0BgWJ4ImXNIrPcf5l7JFexiDpzysMRR5DlY4pf/G6SA473WNJz3K0lleeb5PXc/xhHRc7zhtqQOuiANOJT3PMpIkRXoe6+USpXziJGa5XBCG0SZoDMIA0UKr8iynaRqSKOBmvGKreYDMCvIio6o0lRZo4zkUhGlYrQo+ejrhW++fYnSN8gSNsaxKyBtJGPn41QJ/mTOfjJ24YZwwn4xJWj/dNO0QhjFaO4TDeQJd1xVCSldEapoWGeAKNKbRJN0uUZwgpYObRnFMFCftGmFaxfKGpJOidbQp+Ght0E1NFDuvS91+v0WWOZGZMGztiiza/Bwd70/pDl3XxLmUMF9NtrncWROb7JgX29rLR3xxjsvHcertF9mlL1Lwi69c5m9eeNgIceGVyw+y6x6NroBw8V3BxVbd+eWfa1W5z/fic9iWUyoQKCExwiKFhycEtzqWX95NSCY5H2YaXVtWUnKmDYvGsjJtkSnPEVXjvJG9lvOHwEgPEyrCXHP/eMyXHmcMJzU0FiudWJsT69LgSRyqwFklWjS2MRgNBofOEU2NMhXKNtCUICo0taMyWINsCjwKusqp5+qqwdMFngQZ+MzLKWVV4vsCT4BQFd78E8KtXdZbgulNi05KKrGEfslEjknf6CCDBrlMSEWEXns8rQLetzM+9qfkN25ihts0ShJtx7w1f8p+ccjpk4pymnD3lVcxaM7GR/hhivRjJsfPWec5nhDs3LjD9o27+IELXI0x2CZnvZyhgphksIufNvzyb91j79YdfvAv/gmTw8cEnkPD17VFBDG+qTY6L4tlTpT2wBqqIsOPEpTx0DiutnuWuKKlacUklSfR1uL7AQKBtk7Z+Xd/5/c5Oj7l7bfeZHdvmzBQTowyCnj84CP+4tt/2haLA3rdlCgKieKYJElJ0g5p0iFJnc1gp9Ol0+3R7fbo9ToksRNhVb7Tf5BSOnRXa5m5uU8vx4uX7utNRe18bl0oaF0M4X5ycHZhhbjCH9/Mu8st8Avz8eoF2hdVrwtvv3QdF3ezF95v4xd7dbL/rOPK8vCzDvHSD59+uku/WxDCfR6jK+bPf8Tpx99F1wVNXbNerhjt7uKHAb7ooKiRtmY1X2OakkAJVrMxTV3QHwzwvdgVshtXZC+zHKU80k6HOB0Qxl38Tp9ksAdSofwQGcYopfD8iLC3i+c7KzBTZeSrCXW+dhpBwgMBfpAQdQeoqAPWkC9OOD2bsjzNmM1n5FlBGEVMJmOk9MjWGfP5nLItuve3+jRNzWza+m4rxZPHjxmOhmjdcOPGDQB6vb57bgrJzVs3HdJRN8RJ7JCLdYMrN7rPV9bOJmu5WpHECUZr+v0+TdNQFgWDwQBfKcqyaKkog42ek9um4tmz5wz7rgBoTENRFvT6PXq9nqOh+D5HR4cUecYr2332lUYUDZWUzkVIB65IisDqHOmH7S0vsJ7ENI1rBlW5K+j6sRNlK5wYsrIKKysHO7dOhNEKgZEC40l8TzmhOd8nCnwkDsngewJPOfHGsCjxuwPCfp/GQlGVPPjkE7r9HrP5nCCMGI/HRJGLSfKioD/os16tkVK2323Ner3e0DqLoqDf76M8j7IsefbsGVJK+v0BZel0Pk5PTtCNZrS9TRiG9Hs9kiTl7PSMIs/p9nssF0uMsYzHE5Ryuj9pJ6Xb6WCNdYJtq/WGfttNE+Ikdsm2JwnDiOVyAbiC+OMnjwmCkN3tXcTnXdV+Etzk5Vn68iY/VwXz847Pkz7/5VzI5068izZJTZMOynfJst+KESnfHUZKB80wxjrLAm02XsBFXhDHcStollCXFUEYoHyFHzjoed1yWjEWP3TJWV2VrQqxq5JXleNf3L55mw/3PuFotmDeBPhL1y8RBqS2+KKm9/5fkHiw+JVfB92jzDOqyvFppZRIzyNKEqe8LQTC2lYRtWE9PiboDgi2D9j+lX+d6fyI4pMfIA/uEA22KQ/PSAIPaWua2Zgy/BihfGY/+GPC228gwxpd5eiyQnoKLwho6prA96lrQxin6DhFjPZYjY85/vBdtm/dBeVT6bWzf4kiPIRTqfzkA6Ty2b/3OmGSUOcZYdxFxV08oFqcEXYG4IcU6zlGNzTGdZOM0VRFhgoMMnIJkG0Mynfc9rRNMD3Pa0VsnG96WVZIIWiaGik9impN0zjP9rLICcKAfF22RRKw2gVOaadDXVaYxtlSJUlKjaBpefx1y50JwghrLVXphDH8IHCLZtCh0RVhEqG1JowHNLohL+ek++/QrwxmdYaQoKIOFsnxk6fcvHebe6+9w+z4MT/88RP6o20O9rbIMo3Vmv2hYre7g1INo+1tVJjQNJrFfM6rcUBelFSN3XThqjzHGE130Gdw4z4//PFDHkwtRePU/P3AJwwijNWtgq5DEejGCfgpzydOUpf8WKibCl0VjmrhO3XtunJWYH7LQ7J2TRyndHv9DULBaxOIumpQvuM+CylRwlEAlvPZxmM9CAL6I1dZBtrAsbVuM4bFbEqcdgjqGuV5RLJh3z4nqZ7SlBm6MVS1ptQSY32E0DR1xXyx4pNnc/7gx2ecLi2gNuIiQRiwv9tjMNwiCN3nCqJ4k3i7ApzAao0F4tbWryhydF3R1O77LIsC3w8I45hU9UBKgjBytkPWJWbWWtJulyAMAOFoLbjQ01jDfDajPxhgAd9TNLqk0+m0hRDP/d3a7zrtdsE6ZI6U0lFf/pLHS/6/V9699PPFPPXCs+wlj+2XkvwWEnvxmOIFu/tyU+yyzNN5wu6C/Guv3F3Dhc9xKeG48vMLx/IX13Gxy/biQ57H9Beu4mIjyzqPcem5BC3wQzqxYpD4bJ1mvL+oGFWWW0jWXsAstJzWiuNQsa4cXYm2kBJ4Hn1tuDOreONRxe5xjacjDDHGA0tDY2vA4iGwjUaiwVagC2g00jZou0Z4BmEFja1pgLUEG7skX6kU4zk4/MxLmTYZQjT4IQy6BlWuEMUMGUGZKjLtxHzyVYYwAr0uUeURoQwwwwT6UNcrnp3MUKlCVJJPqmfEecokFxz6IUdpRTNIyXb/GsaXeKLkTlDwK2JC3yuoo5jFckUSBaxWK9IkZHZ2jBUegRFMxqdYEfELv/q3qVdnlOsx5co4v2ztCoKL6YRbb38TFXWxZoEQghuvvUNv63/Gt//5P+Hko78g6Y8wLKCq0EpS5CXWQmc0JNm+iQWStAvSwyCc3grC8WJ9H601fuDjecohJ2q39hlrMI1hPJ4wmy+4e/c2s+kpn3z4Y7TRzKZjyqJA+T5pmlxIthO63R7dXp9ut0uadknihDhO2qJdTBzHxFFEFDkamK+c+4rnOZrDpc73hW43UrjCjrhcOPq0eO18m8up8qf1dMTlf8/rXxfrYZt5dDkIdCZrn9JxujRZL579mosWL7/6c+TI1x/osw546dIu/nL9TpdKgpfWmhfLj2saFOiqwAtidJ2xPv2I+dP3mZ8d0hluUZQlSRJRLcfkTcNivmS9WJCXNUEYcvvOLVarNcvlEm2dFeWt+6/S273tGklNRdNULoZE4gc+6fZt0u17qLCLkM5PHtESauyFT2ctNh6Q9G+0n8T94aW4/LmNNQTpNqV4ypPjH6I8H6k8jDaUZUUURqzzFUkac+PmLcbj8UYbIQgCosgJDa/WK4SwJElKf9CnzAtUy//e3d+jkyYOlt7S5NJOh8V8Tigj4iTmbHzG1vY2IIjjmCiK6A/6HB4+xxhLVdUcHh5R1zV7e/vOhWQ8ZtBzHfKDgwM8T7JerXj6fEmRF+zs7KD1mjiK2N7aZv9gj/FkgqcUy3XGuw+esPP2fbxigakKvKSH8gOa1QLh+XhxCkphqwqsQXgKaYWzF7OWpsg2fG9PSoT0sH6AwGKUwmrn+FIVGUp5LpZROVVRsDMakIQ++7vbDLe36I62CMOQsqmZzpcczzOOpOLho0dMZjOOjo4oq4r9/X3KsqTfH7Barzk9O2N3Z480TTBGs16vWMwXdLsd1qs1ReGSbuX7zOYzF38by7Onz7jzyivstsJsx8eHAFRlQZFnpElMr9dz9s1Nw2g0IooiDm4cgJQsl0t2d3epm5p+r0cURszNnNpahsMh3/nud6iqitdff53FcgUSzo5PeevtNwmDiOFwSBRF6Lrm5OSEra1tFN61s/DFDLRXXv+sWOizx8uFtp+UzV9sTPyE7veVpfDlo11cL+1L7/ws4/N3vNvO3TpbESdJm0zYlsfqOMRBKChKByHzlEumm0pTlCVRHFFW7mHsSdf5CwI3CYr1Ek+5Stv52mmtdZBnrTfiAlJIjFRIBH9zJ2L7Rpf/2Gr+oFD8wlZNtFjiWUGjBarSxL7G++hdUl1y9vVfY9EdkA63Nj7jge/jtyrrVgiwBl1WeEFAlRdk4/dIt/ZRg222fuO/y+Q//V/RPP8A09umaCSpkNTLE0Rekz99n/DVv8bJn/yXbA93CXojpO9DVVEVawIpaeo1Mu04CzJj8MIQLxcMbt1nfviU6fNHKM9HaMu6KElUgKxqymqGsA37t1/BAMuzE4I4JRpsI0xDsZojjKYoM/BjRNrHT/v4WDC69T71Nh3tunIiacIK8ipz/BxPbhJqayGIIoosc+qwxqEVjNYbezesgzXn6wzpOUuxc5sG3WiXXAugtjR1vYFW141TiyyzjHVdO3GrOGkhh8Z5LPohQdd1STDaqXSrAOMpFqtTksE+y9URZj2mKgq277zCJ997l727ryIErCZHmKbi5PiEd9/9mC++sUcYCPb3d5icnWEbj+miIE4kddUwn+cMBilxGtPUBmsFfhQidImUPqdLzbvf/pCTZU1lfITyNvykPM9b30rHlzbWtLZgHlXduG5+Kz7o+77jXzcaU5do47hZQrjilGwTzbquN13fuq7pdfuowMf31aYbU1cVjRAOQhmERJETPHTwtsqhUcoGXWtkxwXRWjftviUSyzA13JRHeOtTGmPQGopaU1a4h74HVdEwma348aMp3/54xqq0dDodh3jAEicxg9GI7Z19htu7bO3sIj1nNeJ53sYSra5qwjDYFLycyFuBJ6VT8/QcJ3s+GYNwvpxRGBK3KBEpWh9zX5Gt1i1FwfmLWxwEzmhDGLguWVNXrFdLlO8TBAF13eAZt16VZYmnPKd+LARVVWFxvOGfdWy6Xte9Z88T0QtQb3Elrrx6oDY5tVe2uZwMX3z9pcj88kVdaHqd/3Dp1EK0p7zmgjaB+Mudr5cC9M3prmmhXbgwwaW29+bhuGnutwcy1oKQSCxJ4HF3K+XGMOXtWcG3Dtf8YFaT5xpfW7Y0dJQi8zxybYmXFfuHGQfPC/qThiBzx9JorKgd3cYaal1iTI01FY3JkKxBanwVIFWM6KQIP0WG25SCltsH2uKQR3WFrXNkY1C+oulCnFqCsIf1Flgvp/Jr6jhEeD3W9ZrCNCyzkmy1QPdqIuUjh5bYE+SyxEsM4/GaumiwnSGmEMw+yhgvMkywxXJrl2LvPtmoj+nGeCZj18z4Yjnnji5R4wIVpsS9hPXymMVyxeT0hOBgz3WqyoylXeB5Efff+TJVsWAxfkq5ntPduolJhwRRgFQNo+5u61ULjZT4UUosHfXom//Nf4fxJ1/ju7/7/2ExX5MVBUnkE0cecSdkVWqE9EB66KZCKsfdPj4+5fHjZ9x+5Taj4YDpbMaNGzc2c1C1AXLdQjx9T7G3t83zxw84Pj7cODkEoU+360Sp0jQlTTsu4e726PX7dLs9krRDEscbFFcQBPhBSOArp/isXBzgKdUm3F6bdF+ElbeJrjhPwC9Nrc24eJtfO78vbgfXpMqXA7wXauftv9dm7G5ybxzPrrugq7W79kCX1pArx375NH9p6Tef2SW7+vJPaESdJ7GXX7iwvbUUixMOf/i7LKcnbL/6VTxhKGeHnDz9GBVELKcTFrM5tip59mjBuiypG1ivVhjhKAtpFKA8j2434eb9V+n2t7j1pV+hv/saQno0dY4xNcrzW4FPhVQR4J3fOpc+hDi/QSxc5O27W0u8XHBtX5eAELJ1FbIs5wv6w4ErHAuBalGLvu+3QqtOOyntdNCNZjad4CmP6XTGOss3Wj6D4YCqKplOxmDh5PSUg4MDpOdxeHjoxHR15VxCpEdVlJydnLCzs+usOsPQdf3LitlsShK7zn6v33M2u0a1zzPDbDal0+mijWG9zohaGl5RZVhj6HRS1quMuq5J0hTdNLz7wce8c+cm+57ElAXaD1Fh7JCf599Oo8Fqh8z0fUTkY3WN9APUeSLYNHhhiDCORhSEEUZ5YGk75J5zlhHQHY0Y7O1hBXh+4ARbW9RdXdcsZgsWecVaVJRGoHXDw4cPGIyGFHnOZDzhnS98AakUvV6fus09nj17Rpym7Gxt0+11WS3XpN0uw9EIhLNfS5MUpGA6HtPtdel0Oy7OMZosK0iShH6vh1I+08mE27fvEEYhaStSd3R0SFHkpN2ua77VFUlbcAzDkOVyhackz58XJIkTVzs6PGJre4thb0TgB+R5TqfTYblc0uiGrdEWq+WS06NniKahrgviTp9uf+SaJOKK88qlReRiYfHTJvuFTS9t8tNBZC5vLV5ey37Cvp9xZX8p43Mn3sPhFgJBWTv+qKd8mqJsq2m0Qb+rtlicajBBQNPUmLqhtE6FGOv8NQNf4UnFcnbE//V/8+/TufkOv/lv/FuMRjs0LWf4vDN9DkGTwvn3edKjvvEabxT/nH8cVvyfMssfb4/4jbJEGI2pDEpZ/MBgmpL4wYfsTMf0fu1vUW5tQ2t75JIgp26umwZjQfoB69kUFUTYJmb28Q+Qumb05i+w+3f/Pea/909gPcPOV8xMj920R20K9Kqg+PA7mCBm/ePvMrr9qvt6JQRphyZbUmuNKXO0dcll0h0Q+D56/WNGO9uMjw7R1NRG02injK185/096PWRUlItpiynE/r7N9FlQXb2DBnESKOxwifoDog6A+oqd1VBP8LzfLSFOHXd1yLPXNXPd92F8yqZEALfd535pq4JwsDBjawrgCjfx/d9stbLWzcNSbfDerHA9wOy9ZowivDDkMTv0NQVdVlgdAOtoJuzTGvwgwCjXTe1qkouqcm2D6Ewiqlr50fJeUc+HNKIAcGtDovnPyJKwY9KvvZr38Q0GcefPGed17z5hXeYTed00whjBVF3RF2W9PsdhFQcn86Y1w1hFDPYGkJTsZqviDsdlHJJ4ao0jNc1Z2uojCUIY/ww2Ng8eC0ssiiKzXenpIduuyS6dslmWZROcVmrlm/o+NZVWW3EfJJWeKMocqqyxGhDFMcU2ZqmFaFTSrX8eN9BLOMEP9SugNR+/8r3N97UUdTFqZv6SFpKgAVPGPb8KXviBL2aURtn+1dVkJUWT0lCJVllGdNpxp++f8bHJyVemNILLMZY8rKk1+sz3Nph98ZNeoMRadpha2cPv+XwL+Zzl1BrQ920MFzp4N5pt08QRM5WTkrHZTPGdfq1QWsneBbHSTv32654WaL8wFE2hiOUcly1qnafL4gipCexVmx8yavWPkVI6YKPumm3MeSFo7goAXWLMPiZxjkfsx1X+c4vhbGXs14uBsHigiqZuPD8Etft2u7+0mPJXnjItQHfS/HsxbevZPebS7jiHXKBdnp9PbsNIq+/UtfF2fC6L13ulQf25nziyjEhUoI3dlPubSV8Y5zzJ49X/Oik4HTZUJ3kREcl3Wc58bhCNtop3voKoSRGGsBgbEmjl9TVgkbnSKWo4w51skOdvEYTJoimwhOQoolMRUBFJA2dyMcKTVHlVFpQaY9Gx+TU6ECgVYnIJ65jnlb4w5IkrihMxaLMKaQhqyqMrKlsRVaumU0KIt8njAMwBmVDilDQ39qm42+xelCht4asqyW5v8NycAsGfXzZ8LpcEx5/zKgYEyD4+//9f4yuCz5678fcvHlAFPs8f/wRh8cnJD3H76zKOSfHJ9x74x3y9YLVrKBazAh7W2ihWJ0dU2Qrtg/usjvax1hNka+wVlMsz5ifPSMZ7DLY2ift9kl3b/Odf/FPmT15H9OU1GVDvi6J0w6eAI1bvz3lc3Q24/f+5R9iLXR7Pf7kj/6U/qDP/v5BCwV2ejHaGtbLFY8ePeZ7f/EXfPTh+9S18yIPwxed7TR1sPHeBjreI+126aRd4jQhDiP3XAoch1Ep5VBHbXPAwdtdYV9e6XRf/r98cRteomG8HF9e6tBc6cK+KIBdSHw/Mwe9MkOvdn0uLDGXuOCbba8GvOeB6OXKwUUxxMvX+3Li/lcyro2txaW3z4uXL967UKA414+w7WpqwWJYT57y+M//GUcf/4jFfEmjNUkaMzt+jm4qmsZw9OQ5woPA9wniiDB1RRrTbG8cXKyFqtYEaZ+DN77Gjde/SdQZba7Rly/sKC/2AjdXeuHzXSpBXqkc2PPqydXvY1OUhMB31CkhZYsYdba459zt6XRGWZR0uz20NlS6ZNDrc3p2SpzGRGFE04qyzbKMIHBF8e2dnRewcmvcfBMhg37fCRwHAXmZs7W9zY2DGzx89BBwyFTimLpu2N3bJYpi5rMpcRwz7PexbYmprhuMNURxxHQy5tad2whge2fH5Q+t97TLEyRhFDCbzqmbhsGwzw8+fsTOF9/AK1fYpkHEnrPRrWtMVWF1jfAUQp0/T0zb0DN4YYwQ55QiQLa2eUKAiJyImtaQpkS45xRSIP0A4bnOP1h0S6csi4rJ8RHzBsT2Hl1dozodvpmkNNownU5QfuDiuMLZoN66dYswDOn1eg6dOBhwdHjIbDrDV4rBYIgf+BR5gfQkJ8cnCCm5dfsW08nEUeWUR9pxlMPAdzD4nd1t953P55vbppN2CPyAXr/PbDhn0O8zHI3I84yzszO2d3Yc7L+uCVTAcGvI8fERTdMwGTve+Gh3F4AkcTS/yfiMpw8f8n/5X/8vydZL/Cilv73N7bt3+eW//uv8wjd/hSS9jBz86VLma3rK51Sdl+KDn/WoL1UhP3WP69+6tvr5U43PbycmnShZ4AebJFhKtyA3lePvAi2k1JJnGevVAuWpCx1ViRd4GKMdhMwaku4WO3fe4Ic//iH/wf/uY/7G3/hbfOObv0yYdJ33sOc5BeZGtx1VS1NXyP07/PbwLb5z9D4ZJf90rXlld4u7x2dQW6gMYlVBN8HDEC2mqN/95wzmpyy+8qsUvW33BfgKpXxMozfFx6g/pF7PQUii3VcoH7/L6R/+Nv2br9L5tX+b6uM/Y3j4kMOHj6m9kMGwg1dXlOPn+LuvkD14l/njrxEOtynOjhnde4syXyKihCZbUI+fof2QYOcGWIh8j63dA8q84OHDR1gMga9YZ84mwmLAk5hGk83GGCsIBttUjcHmC8K4QdQl4c4NwJItJwRhSlAXkPQ3D9S6LBxKwVpo/VK1NsRJQl0VrZez461o3Th1dD/ceG6r1kLC85xHuzGGKIzo9YdYaDu/lrqsXIfcWoIoIvBDPKXaxKsNImxrNaec2I5uavdze6cr5VO3tlLKU5R1hRTO1933A7TqILffQQ5eoTp9gF4+J1AWKzy8KKXM1ygJN29sU+YFi/GM6XyBFILZqsD3JEnawdrSdUB8yWhni7puKBo4XdY8XwjyRuAFPuW6oNPxyGpXRKjbe/6cv61830HyhQDhbDaSNHFiPYlTabetAuc5RcOYZtNVyfNsU02F1g5LSsqmaW181nS7fVdR9FwFfTadkHZS0k6XoigcL813YkVSCIx2UKYoitzcFIJe7HEjPKNTPadeOf6PRpDnDYaIMPKwpmE+W/DkeM4fvXfGolYEcUJVVjRVjQp8By3b2mawvUtvMGL34Ear3u5ESQSKTre78ZHfIBqwBL7vlq4oQhvnjNDrD1s0RkVRlARhgLYGT8hNdb+qazyl6LQPLUFLg/A8180KnSbBObdbCEnkKxCCqiixyqNpHEKnqiqyPKNqCwjWaFex/bmGuBJY2c1rFy0uhH3x+sVod8PUvBwjc+4DfH6EF6JkF8576Wd7IVG95tHRtrbEheu9tOnmQXcxVRAvrv/SbhcCxvPA0F5z/gvnedHEs5tPvdlsA8t11yfc6rdZmwG0NqynOZPnK8YP5gTvT7n9OONg0aAz0LUEDTqUmFhhvQZt55h6RVMtQBi8uIfqbSE6r9KkIc9Cy5EyzH3DJHYJeqeSqEYiREynNqSmYVjnhLrEN4YkDQhUTmznCM9QNJqMEhkJol6ADA0igNxWrFcFRVGyLAsKoBINwuakYUOZ5WhryPKaIpF0bnRZ64bApth1wskPT7GZZG0Ssv5bVMM9bBDhnxzxjWTFP3z1HR7YHc7mlijw+MM/+Jcc7O0gPfjOn/0pv/Qrv8xo6wbD197h6fNHiGJCtjij0YZef0SZrbFNwXQ6ox90WExOqbVi9+A2w8GQKjvDWoGKUsAhdlSUYqoC67si9mj/Dr/6D/89vvf//Y95+INvUVMj0MS9LkXjFJHT3pC0E/DJx49cEHrzJkeHR0xnc77y1S8TBAFVVbNerTg+OuTx40c8eviQ6WTSdr0lUeisjtKk47ig/SG93oBet0u31yVNO67D3YpSBmHgrMN8H185kSvP81At8uY8yZYtDFhI4RLwc2i5fMHvPi8Ku2lzZW6cCxOez9SL7eMrcdpmz81UvZhYX5gTV8aLLvXls79krLtZV8Q1+3/GeGndufzjXxrPe3PUy+PCknCx8vfSpi8+rb386obW0n6TVlMsTzh5/w/IZifEnS61ttimIJ8uKAsnHjo/PHHdYd8VY4IoQnmSMArJVhlpt4MXhIRxl/7BfUZ33qKzdQfPCy9/opZSdn49l//sFz7ElZbaZv27sMZ9eiHmRaPLCRK7WGq1WiKFJI4TirJkOZ8zb4tQeZ5RVRWTqZtHvV6v7V5rTk/HrT6NQ+pVVcWgP6DT7WAwLOYL9g8O2NvbZzqdslgu8H2f9XqFlMIl5E1NmqYUZUWaJhweHrK3u0uv38doy2K5Ik5iRsMh3/ve91mulgyHQ86qktlkytnZGf1Bf/PMTjspjW7wtMeDh49QvofnKcJwlx98+BF3b+7zzk4PnWfoIMBTAUaAkCC8sKWCuHvItHxoAQjPg9Y1xmpntyuCEKyjIiLCjXi0scYVMXDxlmv+KZe3YDFFwXw24fGzQ06TAY/nz1BhyJMnT8BC2u1y48ZNOt0unqfY2d1ha7RFWVWs12u6vS5N0/DwwQMODw8Zjbaoq5rFfI7FUlYFi4XT1Qp9n+dPn9Pr9ej3+8ymU2QkWa3WpCkbO8Y8m7LOMnxfMRgMnO5NKxJ3++ZNhqMhp2eO/900GuUr9ga7zKczRqMRR8fHzGczkiSmzAuiMGQ+Xzh1+hbik6YdikbDYI8v/+LfYHt7l0dPnvAf/If/Z/7v/8//jH/0b/8D/gf/w/8Ju/s3PuUmvm4REZ/+nrg4KX7CAnRhXl1dyj7jDBf2f7HOf/aZrhYyf/rxuRPvsqw2k7yuKozRDrLpeZsOnNEaT4VY8yLpOIfQKq+FwdYuOSmKoq3eKH7tt/4BDx//b3lyfMJ//p/9E+bf+x3e+eovs//VX6Pb+sZZWqGgFtZulc/f/s3f5IfPDnkynoMu+I8axf90b4v0ZIaxEAiXIFopHOy6LPHe/T470zPyN7/Mev8uYvuARiknpGKdv3JTlUgVIDoDquMHqP42tZDMPvkhXjZDSgvdAbuvRXz444+ojaDXjUEZiicfk77yOid/9M945e/+96jGx+SDLYon7yEG+wiroS4pZ2f4pqQyAn34CIKQTr/PoN91lgxSUmlNaFsOnLGsV0sHHfcTqqJgfXrE8NY9/KRDaCL02XOMpwiG+7A8axfTEmOFE/rKM8I4BWMwwolYmaahaRRlWbaK2/4mAVQqaG1x3ARutHYwZSmRnnA+qEIgPY8iWzvurnIdhLosME1DlHSoK2dJAW6fqswdHSHtgHAJYa01Re2stow1NHWNr3zCMHKK19ZSlI4ffV6pBYlWHUz3Ltrrs1weYwOFLE+BGiUraCpUqBjEASoKkFKwbS1KSSrt4MV5rVmvcyZZibaSh6cli0JTNq462lEeSRwzXVX4QUQYBuR5Rt0qj3uegygKFEXxwsKrqeuN/6MUEiPMRkytrqtW+d6poLuCBxsIt4P8Ow9Kp2puNvQN5bVw7Va5PluvSdMOKI8iz1oxQscxc5BzZ3E2UDl3koywmFEWhVuYpU/dSEd7sMbxv1Y5P35wyh++N6ayHlEoKfPC2fB1UtJOjyCM6I922N7dI0o6rSe268or5TnbM2PwPY8oDKmq2qmMey2ssxWmq4uCMPCpypKqFaYLQmdBqCuN9IXjzQkHEbXWFS+McUJ9eZ6htabT6baUF1BeiOfZtmrvFm5nd9igfAeRs+1a4h6M58Um/TMvpMClQOpFGH7NzxcD4isJ6gZ0/VJCfCFsbukJn7b0n295Jb7bDCu4rC5+9TgvQRzFha3Og/9LF3f9bteUBjZpxYVs+3ItWryIvq1LeiQOEmi1ZfJ8wXt//JjD92Y0K4NnFR4eB3GMTZwOQlHWFNmabDYny8c0qsJ0txHbt6D3DnXSY6l8lknJYVTzhJKJL2l8hUHgYbBKUtdwUObslqdUgSLzQ5qmJNU5YZMxQxMoiGRIoAxWV3g6x/M0Jvbxk4RVOacqGzARstTIArTOqJQl8zRaWexOh2DLoT5KLRgvS3RhGBYNi4czbNWjFAnrrbtUu6/grZ4SPPoR3Xtf4Ctv3uXhw4ckoy7v7N1FCUGaxOimob+9zeHxMU+PTlBCc7C/x2tvfIXJyVN++O6PiHzB9OwE3/fIy4rZLEPFK6pswRd/6V9DFzMmR5/Q6fYQyRZlllMspmTLKffe+gp1lSM93z2bpabX6/OV3/xH9Pdu894f/wu8qMfO27/E0+Mz/vBPv0fa7Tubohs3+dIXvkBZVzx98pQvfekL3L5ziw8/eJ8f/fCHHB0+2zxnhIA0jRBCIj1nIdjp9hgMRvT7fXq9Pt1Ol063S5I6DncYRxtkkN/SyVRLb/Fky98WYsO5PS9+SnGhyy3dPS/Og/ZNAe3qjBKX/3sxKX8Be7m8y4Uk/Ty2OS8yvRz1vRy8ftrZL714Phkv5uTnC8s1m29m9nWFOFwx7S8VaX7NeAkoY19czwvhRndh9lOuZ/OxjaVYnTH56E+gKfB8n9V8TpzGiHJNXSwIpc+ycPa3nvLo9gek3ZSoOyLqDh39UUr8zoigs0OY9AniPlKdu8Jc89d5schvVsmXCpzXf/rr/jTXbuZO7NbHc1uwKHIK2s79oyROYno9V5BfLpetuFmf2XTGdDIhjGL2d/eAs1aJPGB3Z5c8z5kv5iRpRLfTdS4qWnN2dorWhtVyhZCCXrfHYrXE8xXL1ZKd3V0WyyUHBweuMOAHG8FCozXPnz3n0cOHTvchdfSxg4MDpJC8ev8+cZRwNh7T7aZMxuOWF92n00lJUyca65wJfP7ke+9y8zf+Or04oV4tEb0BMoxcgtzUUFXYukJEMTIIQCnnLrFxJ5BOb0YpBBYhQ5e1a6fNc653ZI1LzlEKTwgXX1iFQbJerXny6BHHZY3dSki0pWgbTnt7e5RlSVVVhGGIQFLkBR999BHb29vM5nNm8zmj4ZBut8vJ8QmL+ZzBaMjxyTHbO7s0jeHsbIzvK169f58ojpDSI88zjo+PuX33FUajAYeHx5wcH7G7t8f49Iy0kwJwdHxCJ0kIopDhcIiNDLP5DAFEcexsF09PKXJXYDo+OmHQ7+Pdu0eaJCRJyvhsTFEUFEVOGIT0hwOstbz99jt4SrG7tw9CIIKQxsDHj57wf/g//ofM50v+5//+/4LBcHR5Tbk2T33htSLabT4jjLg4wTbrw/l+P3nifPq4urv4zHd//vFTqZoH59Bx46pjeZbRGwzwWkGSuiqdl7Dv/O7CICCKIlarJUXplBdto9FGO1iu55EVOSoI+eY3f5Ev/PC/YkcpytUJZ3/0/+LdP/2XxL/wm/z6r/9ter2+E+H6/xP3X8+2Zdl5H/iby5vt97H3XJumMiuzvIMrWJKgAIGkSDVASpSiKT0qQi966f+ho5/6jREdolrRrW4FCToQFAg6oAoFoCwqK02lu3n9PX775decsx/m2vvsYzLrZlWBvarynm2WmWvtacYY3xjfZ1mUZYll23Ru3OZ/+tIr/NOv/Tnfd11GVcn/x/b4P+8M8EYTFBpHKrRjopEemipZ4J4c0LEF4cP3qB0XefN51PYNilaP2o8Asz+qxh/sUM4nOGWB2L1NPTmmmp4iFyeoLGVns8u9e0+QN68x2OihlSR9chd/6yan3/o/aL30s8ze/Bb+YAO3SrDaGxQP38QpC7InC+qghTzeJ9q7TXF6SOSZWpCT0RgbgZI1Uphsg8rJ0dKgieMnD2j1+vhRB8/xsKYT8ukY+/rLyLrCS2fI4R6u5yO1IcFSskLJCs+PjP61UrhhgNYQtzuG2EapFZIbt1uNLrRBbGtpyggMq7bCDwJEg6y6nkeymBnCNKXwgxBR5AhLEMYtQ/ZWGQIjzw+wAiPfkKeZcU4BWVWIljGAHM9vGMUnWLYxZVwvwLYdHNdGyrphALdIkwVSeEzqCAvJJE85Hc/phxGeSvFdTV1leEFIIRUWmvk843RWsH+6IJU2o0VFUkg81zHSX42snOPaKA21rMkLhWU3WpDCxnFNLXJdmFRzyzLs2FVd4XsBZVWaFH3bMEZ6nt/UgtcEjcQdWiOlatQCTIqlbTurWuTAshtW7nBFMJZnpm6n1e6AoNGfNVqPSyI2q5EPC4IQlGTHG3MnPsUqCopaorSFskKUtsAWiAYFns4zvvXDQ167P8b1Q2LPp5bSkJJYNhvbu0StDp1Oj95giOMFpn6/QZKklOimvttuFksEhrQPkyruua55FoDjuoAJPNiOYxBzWZvFUUmkFPhBYBiQHUP+pzAygK1WC9d1qGqTOaC06ZtFbhjhNZq6kuSZkSZrki1wXAfb8bDtmKoy/ToIjOboj7tdclXXFpsfFW29ys1d3y450OcQsssHadaQH7GGRK8vcFxu14e2U1yBlX3EWnSxXR9iywNirZlLvM9Iwpw5OsY5Qda8/xcPee/rD6hmEl8bXgOhDMokZUW+KFlMJiymJ9i+prM7II6/QOZukNg+qWORuBZpq+Y4mnLPLjjxI2TcR9o2ui4RShJmC/yyIBCS0NNMgi5UFcPjI7pJji4ltgtubKHzlEwqMlFjOSV2JLBsjS4zPFngiRyvrpnNJ0xmCbmAxClJdIFMNanOCboRbhwQR0O60mL2cEyWWcyqXegPkPYGsnUd7TgEPRDjCWLY5698qk91920IPT55Z48vfOEzOI7NGz94nePDAw4Pj3jppU/yy7/2K/xv/+v/yoNHDxkON9C6wGoNGR3c560ffIeXXvk0eVmTFSWLJCHwO+iqYHJ6TBAEzJKc+ePXUMLh2o0XiKM2dV1hu2aM2Y6xC4p0StyKef5Lv8TwxoskpabEQ4+nbH7wlEdPDqhrzXPP3cH3HF77/mvcuLGH58K/+ue/y3h0auYE2yaOIxPEtSxs28H1AuK4Tafbo9Pp0Gp1aLVatOKWSScPo7P6bdczznYzJ9nr6LYQzd8Let3L/5b9rnl/sfb2ytF5RYRrLa505Xg6g0Yvl12cO/jSdtaK1SnEGrqq188tLg3q82/Prr0E6C9fVZ8FAn/69ueV29JdvfDBWTxueX/nPN/zDdRakp7cJ58foRRUVUHQiimTjKpITHAcB8+p6PWHCNum1d9g67nP0t59BSdomwwIME6ZMGWSKy/ifLTw7OXa5/pCEEVcsf8zb2vBCAEmm8Mz6c+bm1vErZjjo6NVhvrm5iaO4/J0/x7TyZSNzQ2qqmZzc4OHDx6iZI3re3S7XfI8J01TEBAEPr5viJHv37/Pzs4OWfPddDJmMOwT+KHhX6rlKm3bb7JKdra38YOAoiiZz+cEZUUct+h1e9x7cI+iKPj0pz/NdDKlKkt2drZRSpMkiQFXspy96zd4+vQJo9EpnutRS4nrukxORww3hxyPxvz5m+/yq5/7JE6RU81GeJ0BwnEQdYXwXEOaZolV2YrlNdlsWp4FvlbBNUO0hu0YFByNFmDZHiyz+JpxXuYpSmkmswnjeUZi+1AYAObuBx+QFzkvv/QyQgiSLCNN0xU/lZQ1k8mkITSOCZrn5PoeT588QVgWnu8zGr3DxsYm167vMZ/PefDwIXFkSGpv3rzJK6++yng64b33HnJyfIqUFVrDbDohbsWMxhOiMCQvcm7dvo3UkqPDI1OGKCXddgepJMcnJwS+z8ZwwM2bN3A9l06nzXQ84fjoiCw3DP3dXg/btiiKgtl0iuf7xHGLd995h+l0ygcffLAqX51M5/zv//if8KWvfIX/8rf/a2zHfoZ+fxXS/VGD4fzcoAWXgnBXBsWe7YyrYf6XGWd8Zsc7DCOyNMXzDHGSYTP2QWsjMq80nushlUG1iyJHa4XveoZRWFjkWXZmjANFWaAaXbrP/Pxf5Q/29xk9eI3nI4dFLfn6B0eM3vnfyN77Di9/+Vd48TNfwut08HzQSlEqRfCrv8U/OJzxzne/yXd7EW9kOf9fz+fv72wSj04RtUbYCu1obGHhuhbVLMF2TnDaHURdYz15hL7zAu7WNYreJlrWVN0NyiBCo3FQ1GUBQQdnK8YOW1SWIEXjOA7XhM3hwyeoZM5ge4CuMnQyoTx4SKo1Ouwyffs1ehtdPEvgakmdThHdLdToMfnJPoUqsZQiXyT4QhE6ggJJmRXYLYcyS6ksjZIKG4dZlhBFIXW+QDkd8qJElRKVzbGFRGjjINoASpKnGRqTilQ2aXsCwz5pN2QyRZogGkc7jCKKPEMpTVUssByDNmR1vaoJT5OKxXRCEEYrRzlNFgSBkdjwgxBV12RlYRYuNHXaEGMFAb4fYDuGiC9LEsMBgFj1j6oqcT2PIs8aVmoH27HJkgTdMIkXRW6kofLC/EbZjNLuUHk27x6fsphL8nxBnqZEkcd4PMe1DclWu9NmsjD1zcs0Q2GbtHqFaKL8DnklAQu7Oa6q6oYMTK5S92tZ4wjwfB+plOEpaJB+w3guKBvJPNWwLVu2jUCQZfkqqCGrGgT4foDreVi2Q5Is6IuhSb8vi4YDoSLxElMfHsUEQUiaLkxAxDekGJZl4ciCF/tTtr0xVBWVlEglEG67kQrTKFkatsrTGf/xtX0eHGcgBLY07VRa0+kN6PaH+EFIf7gJgO+HOJ5PkeXGMSenLAtkVdHp9Vaki3KtdrrVblM384PTlK24rgkcLJ+prGuKoqDIC/xAkGeZeVYCZENgJcuKNMuoayPnJixB4IcgoG405T3PIN92k02glUIjjIOvFYvZDNf3SBYL8iw3LOc/5nbO9r7gaf6oFM3LX59pb19lmJ9HqsXK2jszqvXlnS826BIKpi/vv/z4zHPnnKNx5Z2stVasf7p2mnMOyvmTnQFG5r41MB/NObl/yuT+iP5GQBnXpJOSxemM2XhOXqVUdY3fdrjxc7t88dWfZ7pQPLlXU+sIiUVp19Se4MgteBTBSRih/D5dQEpNoaAWAhsLFXdIIglFQvdoxPDxEXFWIIVPiMKjJBOatHSoghCvZVOSUwOel7PRFjgyJZnMmWYFJ/MxCkG40yaVJ0ynKblSFGWBFTm0uwEhXXSqeXxvTDm3sJMukl1k5wZ20EJXFcIuiVRJ9PwtZpVk/8khL+zdJkmm/OIvfZU7t2+A0gwHQ6IoZjGf89Zbb/L+u+/xxhtvoKRiMBxy9OhtXn3xNm8VC+4/GeFH99na2iYvKianIz7/M19lf/8ROk9RsmY+G6O1zUuf/3lsJFkygUWJ6wfYjsfp8SNQmri3iRuY1MrouReQyuL46BSl4dVXXyEII9qtLqPTE+6+f5c0nZMsThBa4XoOURRgWU39tevi+R5BGBFFbdrtLq1W26SRRyFh1DCUBwG+7+P5Pp7jNKiYvQr8LZ1tcalue/l6zcmGVR33GVu5uGTUXRpPK2/r45p7y1PoJrvjo3zbs1G/QnjWoJ7VcR/hIF8OgF0wYK/Y90eA7x9/+6hHpJdg7vlIhmbp717VKnHu1fKbMpuSz4+RtSSZTyizjFZ3QJok5EmFrCqTTRb5WF5A1O5x/TO/QrzxHJYdrP3+6827HEU8NzVe2MSq3T/ZdjmuY4CUIDAcB5ZtMZvNybKMqBWxvbUFiBWbtpaKjeEGaZbSHwxJ04z5bM7pyQlhGLAxHLJflSwWCWHgM5vPGY1HbG5uEgQBDx89otvpYDs2pyen3Ll9m06n1QQAfF566WWSJCFLU46Oj+j3h1TViPlijtaKojBEYGhTd3xycsLGxoZBXPOcNMtAa1zP4enTfTSwmM9Js5Td568ZAGY+Q6KYTecMNoa8d+8BvTjki3d2sLKMaj7GidsmGCgsLAxRmpa1yfBc1nqj0XVtiI+FoarTjQOOVuY/y5TSqqo0mXHalAVqYbTZ03TBfJrwdDolCTqQJZycjDg9PaXVbnN4dMTm5iatVmtFEh3GITu72wDM5nOKosD3fLI8YzIe0+10DBBV5Mwmpqb9+vXrHO4foGRN+/nnqcqax48fE0YRR8fHPHr0gDRNePnlVxmNRmRZxvHJCffv3Wc4GHD7zh3efPNN2u02nU4HjWI2M6z8aE3YqAoJy2Y8HgOG56bT7bBYLEiTDNdz6YkeR8fHBlS1BeV4jFSK+XzKN/7kT7j7/vsMN7f4zOe/wB/+/r/i+HTMP/0n/4Rf/8/+c3r9wY/s3+LCmw+JaV16t/zkkn31EWPu0vT4DHPl1fPOTxaFfGbH23VdlO8jhCBoGK7B1OIqqXA9t9GuVqtaV0MSZXRzfT/A8Zb7ekhZU5Qm1dx2XGzH5pd+4+/w7353xutHjwi05Nc+ETAtBVF5iPvGv+D7b/0x0fNfYu/TX6I73MR2PWrLgt/4Tb4wOeXO228RRh7fSkv+X52I//b6DXpHhyA1Wkm06+DImkoL5CLFFhaW56EtQf3wA6Qf4Hohoj/EvvsGrhDI67dw4zbu7i1mr/8J5ckBImpjt7dpd/Yo5oc4xXtstn0mT/bJTkbs3NxCZTPS+Yz0eJ/BC6+QnR5THz1i1wKnmJHde4voOU05neLHMXJyjFXUuLlCCZtuHDGeLCjLgsBz0bYmT8HSmqqWCMthfHxE/86nyE4PcWpJeu99ohvPY1MhhYVQknQ2wm/18bwAWVeN5jZUTR2n5/smGqekQUyFheW65BmURYFWmNTwBtU2wRWF5TgEYbhiqLYwdUJeECDU0ojXWLaDZ5sUaNdxkJZFXZXMp2OqsGWIvyybTr+PlsqQj3ke2WLesFi7xO0uVZU3DqtBoqvSIN610rhBTCeIcF2fJOkQJjlda0Kn0+f09JTpbMp8NmM+X+BGHcoyx3YFs6xmZ3uDJM3I88owXLo+rutS14YBvJYGvQUIApPyVBSFaZtjgy3wPCMNAYbjwHV9qqqi3emsUvTzLF/pRRvCEm+FvmitiKPIONZ2icAyTndjKMaxyTzwfd9kjWi9WkSUVKRJQpHnq7FaliWubdP3Cz61cULLzpF1gcZC4qLdCGwfVVVGhiQveffBIX/8+hFpbdHudABTx66AXn9AuzdkuLVDrz/Atl0s2zLqBo6LF5g+VNWmBjuMYtwmxayualSD8GtMmYqsZUPcZ2TOagRKFSuSOqVMMK7X7+H5Pov5giDwkbWkrjLSpFiRwaA1WZbiOR6VbeO5Hn5oNOlt26gkLCXykoVBOvwgMOMhNGyunW6Pcw7jj7ldmqxXjqZuSi7FCq1ZXwCWr8+bkBdeX0TTmgPOG9BLx1ezsuLPOcsXr7h+Dq54v46YXYBuroLJxcV2r7WHpTOjL59g7RwmaKDRCpJZytvfeJ/733tImdaoEvKkJEkWaFcRb3qIDU2vHfPi517g+c/eYpqn3L93RNxt89XfHOKFHouq5nCR8jSredWKmAGzQpOWmnkBE1UzV4KFtkgsm0rVbI7nbDw8QBQ5ymvj2T52PSNzYdrroRxFLBRuvUAJiRUJrvUEHV8wGU2YpXNylZO5FvFGn8HQ573FPouipNsLSJ8miMJH+BHFvAUjxck0JaWHSFuIaYCIIqxMYqdHOC1T1lMnCdMbt8ntmvZuzM2WyxvfHfHDH77LsN9jOpuys71jUIlWTJqnzCYTfuGrX+Vf//6/Is3nvLwXYsmEvZs3SaXHwVQhrBEbm0PyNCPPEzzX4/jkCK9WaCXYuHaDbDEmnR1joYg7Q5QWJLOnqFpy4xNfoEjH2I6N60SoukKqmq2NFlVl9I93t7c4OT3l7vvvM5+N8X0XxzUIm+f5hmnc8/GDcKW3Hcct4tjo04ahQYmCRpZ0uR4t0W2TTr5kJl+XAjM6vyuEe+31sl8av+7s71l/varbXzYN18s7Lo7ni2abccguVGyL8yG2y/6pOHeO5bA5H6Jj1W598QTiqnOe38T6P01wTly0hH/S7Uc04MqvL1nLyyd3/qlooMznJKOnLE4foMqcsqwo8xy30SMeNYGgdq9Fdzg0xIteyNZLP0e0+QJijSDtQ9t+AX3+qPv70Nu97E1/9GUv7GdZNt1ul/FojOO2V3bAYr4gzTLicDl+Ik6Oj0zdblFQFkYfOo5jDg8PiKOQOIq4dfsWaKjqirosQYDnN2V/ZWnW9TDEdd2GeRuiMCLPMrIsYzKZYFkW08mUk+NTXNdlc3OT6WRKu90miiJuXL9OXpYs5nPmsxk72zvIujKa0hojq9VqYQtBFIVMxiO0UrTbbQI/IMtzPN81GWxa8d233iEMPD652cXKFtQLhdXbwLJtdF2ZdcdxTV9uZN4sx0UjzN+GxI26RjgeWA7Cd5qurxHSAkWjeKGpqpyqKMmLgsPTEx6PZtjbLfLJhPfvvsedO88hGy3w0WhEEIb0BwO6vR7j8XiVtu95Plmacnp6iucbgrXhcMB8nnB4eEgURfS6vab2vYdqMhfDSFDkBQeHh0RhRLfb46VPvERRFEhpSglmsznCEmxtb3F6ekKSJLzwwvOkaca7773HdDplONzg+rVdwGSZBr7HfDrFdV2iKKTb7jDo9XjydB+0oqhK/DCgLitarQ6QcXo64tbtO6AtbMvi6OiQX/nlX+HtN37A3fff573373J0tP9Mjvezj40LM1gzL3xY0OujTn3xHOsHffiQ/Iio5lXt+4jtmR1vrRWO4wCCWlZYwqSAObYDliDPM5LFAtB0uz1jQGtNVddGRkwYsiiAsqrIM4Ni+r4hqFCyZjDY4Eu/+lu888e/S09lhPmCr173cEKPUgo2ZMa//8a/5p/9uz/iVz7zMrc/9XkGN+4QdFrkz11j8PgBvzFZsBEE/PtRwj8advhvn3uJzYNHCFWjsZE1OIGDrmpkkhnpEN/HKnLU/ffg9Bieewl7sAGPH2D98R/ib29T3riD96mvUD65x+ThXeqH71ImY1RdIqqKVhziETMdzXn0xj1anQC3FSOzfRazE3ovvsLxDz4gbXm0B12oamZvfM8Mci8gjCNyS+HbGgeBykvajkWVluTJgjgcMpsv8LVAWw6lELiez+jkkHg+ZjDoEwqByGbIXKKwkW6MFfcokhlCa9woxrF9UzONSfkuywplmdrjsNUxNeRNH6rr2mjoej5SGRkDKSsc12ieFmlqJqS6xHIcoigmL3ITWGlY0qVSoDnTTQ9a6LhFtlhQy5qqoqn5NazDRePkmzpzYzjlWYpSdSNXAJbjIvOUui5xHM+g7UoRt3vErS7j8SmWwER8pcTxPDzPx/F8ox+ra6RUaFWT5TUbwwGzeUpV1QjLbtISjfTZEv3QWpPn+Ypd3CC58qx2sJHMcj270TrHBB4sG6FMQMFzXcqyIgijZr6vcR2XoGF6B7Oger6P3bCTgsbzGqk9MGlPTS223TB6B6G/Qol7vR5CS260Ml4aTrFVRi0VSgu0FaNsB6mA2rBZJknGX7x7yHfeG1Fq8F2DqjuuSxjHDDa2iNpdOt0+rU6PdneAHwSAbljDdcPamVGWJWEUrUg9TOCiwqR81zieqdHygwBZS6SqyZIFGoPw11WJkgqn0XM3fc6UNFSVxLYtgjCiruc4DeGJ7dim32UZVlGaABAKSxhJkKqqVxrBcRyDMJkUWutGycBoma6YTn/M7SqE5GwOPkPNVvutOeBnfy9abfqckXcBKl77K1YL0LmzriFH62jausF+8YRXxXaXzsiHoV9rTVg29MI1znsAyystb0mfHYbAyG8fPTrme//6B5w8GlNmNem8oNIVm7d6bO5sk6Um0PTyF1/k9qu3qVXN4dGYMPT5xa9+migM0AjKuqZT1uztdvg8UNaSrKpZlJK00qSloqg186JiniuejhYUj06QWcJsyyUtcpRMwdcs3BbSD+nYFm0bap2TlwXKFnTDHMtaMMkSJmVOUljMUwV1xqvPb/LD+QHzQrLVj5nMJbpWCK0Iwh7VsYeqa/J8A2QfXQ5QHR+nLmB0n3pvk9hKuO5ZjPb3qba6OJs9vnBni+m9h8zmY7793W/xxhuvsXf9Gl/+4hd54fnnOT454Y/+6I+Rdcm/+L3f4/TklO3jCV9+dZcsT+m0A8LApagF+9Ma26vY6PVIkzlB1CatKh7cv8+1G3t4nkeRzsnmM9rDLfYfvofAYrB7iyAIDAquNVo2QV/LlC3ZjsfutW0m0wn/4b23+ODufeq6ph0HeL6PHwaEUUQUGVK0OG4RRXGDbEcEocnmMeSJBt12PaO3vWQntx0bx7KvZCRfvjZjYIlws5pPxRVO9hl/gumQ4lzHX8vqWA0WcWn8iHP7XzUKzo+G9XEEF665NkhW+6whrVeZglfSNKzbhZdQogsO9qW55qfpff+o7UPm0itSh5Ykk0pLkpN7HL33LQ4/eIuw3aO/u2dmGaXBgjI1iLDrmTJH1/cZ3niJ1s5z9Hc/ibC8s9/jChv63M+9MvjXQzEfHby9nPGzNr9/zAJ6I/81gPsPyLMM3/dXkpzz2ZyqqAAaMlyL0Wi0Kg2s68oExJWiKEtkXRO32wSBT5pmhEFFnme4rsPpaEyr3cL3PYZDw+CeZbkJfmrdsF1b9Ad97KnNfLEAYRGFIWVpFAgcx8H1PLZ2tpnNDApeS0mWJliWRRAGgCBNEwbDIZ12i8O3Dul0ezx58pjt7R1c18gGer7Pe+++j+d7zOcLfu8/fJ3i57/M52/uIMqcKpnhd/qmrlsIQ5bm+iYFX9aGaE04DZmakcYVjmuIbpsyvWVJ35K3iqacsMoLaqlIpnOOTk5wWm1uP/8Co+mYt95+h6IqiaKYx48ec+PmDRCC+/fusbO7y2I+59Gjhzi2w7W9PdqtFkW5YDqdGvkvBOPxGK0k3W6XqjIBCqUkOzs7OLZNkqaohuvn8eOHOI5LlmUcHR/j2Da7e9d5/OgRvSYgA5owDHn99TewLNjd3mbv2jVm8zmPHz9mMBjQ7vXoDwaEUcQHd+9hVRX7T/d54YUXuHFjj8l0SlmUyKo2vFxK0Wm3ieIQFLzwwvO88foP+O73vsu/+de/Z+x+y5DTzWazj9WnV337p3LQ5QH8oTPYjwxK/vTnvo+h411R1SVVaVilwyikqkqkbfTv0I2dqI1cEGhqqXEcm6jTXdWsGu1i07H9IMASSy1kQ8r0/HMvMh79Mou7f8pO3+feJGOrqtkeeCSEvHf8lPeOjtnJ99H3vs/42nVEZ0jUH7L7M5/h6Rtvk47n+F7IB7nm/75/wm9ff5nPLE4RkxOU1MYJt210XQAWqqyxWy1UOgfHQT+5j33rRYgiKt9Hv/c2ztPHOFubuJsb+NdvoLmFtjTVYkY+HZFNTrAmIXmuUNWMfJ6Sj1OE41JNZ9jpnEhYPPgPX+f5z93BtiHbP8YOIxbvPYLdAXEYkOUKIWzaYYTTCpkfj1AKVFkgFxnhvKSKQ7JOC1nXLA6e4AB1EGJNZiye3KcU4N15maAzRNsOjuMh67IhlSqNLrYwaca2baG0kWqqFmNsL0LY5rewhNFvD1ttqFil7mmtjHPsuAitkFWFqmsqIXBtp5m0QAIIgSVMxoQQFrIyjpjtuejS1DcrbWqcoUnbbvSa3UYyY1l7vvxOKYUftXF9Q/YWRi0MQlugpGYw3EJrgZqMCSNj/Alh0esPmxTwgkWyQEvDrl8pRRDFhAhqWdNkWDfPymrqcxR1bQjoEBB4Po6tKKsSXWvsJp08SVKCwG/YJI1B4DgG6U6ahcZ1jZSalAplK0Na1kRDwzDGbyTLlkzysjY1TtPphHa7Y2S2omBlLAlh0e50qU6PkdmEL93S7EaLBnXSSKnRThctXESjlV5XJccnE7719jEfHJdYfkCned5SGlmvIO7Q6Q/xgoggirFdpykHgCLPV+nhZZGTJakh4lNmCqtKoxkvLIHrNLreyqBRNBkRSqqm5tugeUbj0iPPMtqdrumbDSHjskzF9fxGYsyiLApcz9Seu56HFwQGpVeKKHIb6TcztwhhIakpS0OmIywLREiepSYVLo6beesn2z7UED73rkFrlgiwPvv8vAF8PqR78dwfZdyf3+/qoz68feff/+j7Wf+iMUfXvekLm25OsLTn1+ITSKl55zvv8Ee/+w0OPjgiL2q2b2zzqV95kfZmxOhkRBy1+fJnvsTW9Q10I09nWYLBYNCoChidVY3GsW2iwG5KOTSWa+HaHoErKeqaUglqpahqj+PTUzaqBbPbLg/HAlVJhu0tcqE5nE7peBZCK+bpCeO6pCLFdisCXyDtjDStGU1TZpXGixzQ8OrGFo/zhNrxCCLFyYni6BTqJMTzQ/w0oD6oKeweehYh/OvooIPIxqgywdroEPcdtnwYypT4+i4fxEMKBf/o/oS/Egbs3hzym7/+6/zL3/sD9p8e8m7rfb72ta/xxhtvsP90n+FwwM2btxBY7Fy7wXiS4IiKUpaEjm6Go2B/lFJUEsu2ULqmFYUcodnauUGRLTjZf0AUd5gcH1FkKTvPfZrB5i6L2RjXDcCycbyIqso5ffoenY3r1EWC67f43Gc/y81b1/nn//xf8dob7+L7PmEUErU6tDs92p0urYaR3KSQh/iNUoHnBXiekUx0ngXdvli3faF2exmI+lHp5GI1Ns9G1blx0Xx33gm/MGJWDu/6WP8IqObiCL44H4jLo3Hp8p855fqKHdYG3Hq71m9mbZ45P7voc036T719WKxguSmtmB+9x+iD7zDdv4ulFXWVMzp4yPTwgHyR4IYBQrjYjsutT32F9tYNovYmQXcL129j2S7n7vwjrP8rv1r/3Z/Bif7RM/KPPkMUtZoSPMvIjwYhs9mcQX8ZHBe0WyFS7XBwcMBgMEC0YqP+UVUrqV6pNU+fPGVre5Nup4dGMRppWq0Wk9kU34uarDunIdvKV/XiR0dHbG1tkiwWBL6HEK0Vc/psNmNaV7RaMfP5nMl0wmI+p9PpELVa1LJmPp7T7nRMuZ2SjEcjqiLD9WwWScbW5ibT6YRer08YhvzwrR/i+h6dTpdkMWO+SHjz3mOCIOSTe5s4pZFFdKK2SSe3tQGTBIbVHGNLW66HcGyQEmHbaMs6Q8CXP47tosoKWRWrcVsVOQeHhyQKbr78Mosk4emTp2xvb3Nt9xppljEcDomjmCiKGY1POT0+QQuNYzkoqciyjMXcAA55buyaxWLBzs72av3a2t7mYP+g8ZN8yqpic3OD8WTCaDym3x9gOw73790jyzPCMOIvvvMdfN9nY2PIfD4jCAI6nS6tTmfFbzMZT5jP5/T6XXa2dwjDgDzLqeqa7e0tDo+OGE8nHB4f0uq08H2fo8NjTkcjXNclbIgru50ux8fHSFnzd//e3yPLMk5OR+zvH7C1scGLL7yArNXH6M0/5li4aKg0c5x5+ewI9H9Kpxs+huONMBIGSqnmr24iLimWZSS/Wh0zOIGm7tLsUxYFGoNgWU3auW5SMvKioKpK0MboR2s+/7kv842s5Lv3vsdXrkcsRjPev5vxB+88YpyWvLgd8NzNNi8MImaLfUQx4rtvv4UftfjNT15n9vZd7ESiasXYdvif777Lb1y/wS/f/gTh3bvIPMfyMI5zOceKfVSRm0G6AFXm1JMRYriF7WDSnB/u4x3vI1oRwa3nqVEoz0bYNnanjWdrAtfBD0JOHtxlcjjBszWiKqglHN4/od92CITFw++9z97LO9iyotg/RGWa0x8eMtgKCFotRvOS4a5D4Dm84Lt8ME6hnaIXGf4sp3M0R+wpCtflVN1HKkV0OqLz8AAdB9Q3buJIRZXMCfqbRhrBtrFsgbZcXM+jbpihdSOTIDD1JXbYoWpI0IIwxnE8Q07jGe1kqRRaKYOKBCZ6jItx8mSN7fpUVUFVVXTCoJGWEih04+yA0Ba2hdFflAovMIit4zjUTc1/kWeo2miHO65LJaUpX2j0xYXl4NpOU+OdkCYJWZ5R5QV+FNDf2DAZF1VFJ46JWi0jh1WXaCDOOsymRusSTK2S43oURY5AkOUZgsWK1Mx1XYrC3JfrOA1abyTFXNelqmvq5j9hxaBNPfhSp9uyLFP7rhQgsGwHy9JYwjLp44imPtFpWL0tlJKGXKSuieMWQjQEZA0JidGiNqnrltDsdOCL1wp6QWkCFFqjtIfw2yhlEHYtJVmy4MH+mK+9ccgoMTrgTmO02o7LoNPBCyLavQG9wSadXh/bdbGEcaId18X1fWxhZNSKIsdv0tDqqkaqmiKZ0x1uGVTatlkkU2MY55aJwjs2YRTi+R5VVeFYNkFgULAgDBvJOpNp4NgOrZZZ0KM4QjWRkaosqWtJ3GqBDtFaUxYltmOCSlVZglZobaGUqfkWmFT8JWIe+IbHwbZt5j9mhPZsuyJnSV/eQ7Bmh1814180rvWZMXhu9x9hHJ65CR+yrK1/ffFcl3yHi6biOc7esyus7ulHLHpXfK01vPmNt/mTf/oNZK34xJde4PnP3KGzG7NYzOl2enzhq58naocm60MZI8m2TP3/EpU/a49pm9WsXVJKpKzN2qQlvi3wbZusrKnKhNDVpJ45fivucc1zmaQLpumCW30bRcmsnOGIFFUnSFHiuBapypkkGaNJgW3BxsAhzUb0BiHaFxRliWXDwXsW89xD1qCVTZ1HlAuFFVos9lug2lhRhA4DdOqi4y0YRrS9jBs26ElGVuX80u6EP1jA0Wyfr3/zj/il524yHAwYDrq04jbvf/AuhwcHnJ6ckGQpe+EeP/szv8Qf/MEf8O0/+xrX/uZvcefWTd597Y+I+9vI6WzV56appHo64RYO7din3+uC0CTzOVLByenIpH1HPVCaZD5G1iWLxQzH8Tg5fEiVzvGjLkHcI6sLbC/CtqEVBfzO7/w2N+98jw8+eIjlesStLnGrY5DuMCJYOt2+IUlz3DN02/DDOGu120Z3+yK6fb5m+/z75SCymr8Xu+FVmSsfArY2+8MVQ+O897ZEJFYfnfXSs0KMi86yWAEa59r4o2zJS99/1CTRfKcvzg//6T3sH3lb6wGLC5+X+ZzF8QeousSxHfzNLnmyYHp0ZPhYggABxL0Orc3rXHv1Fwm7O1jCWWU+fOxNnH9xkavio4/ho2/2GRsQhBGtdouyrEjLhLIqKcrSZPc5Dqenp4RhwKDf4/T0lCiK6Pf6LBZzHMemqqsmNdnCbTLyptOJsWcqw6HSaXcMS3oYMBlPefL0CXEUmXryOGRjY4isa9rdDienp8ynM+Owj8f4QUAURoaIOUsZj04py5I8y3DH4yaoZtb/+WJOGIRMJmOiBgHf3NxguLFBliRsbW3Rbrfp9Xscn5zQ6w14/oUXOD0+JS1K/vBPvon86s/xyq1r+MWCcnaK4/nYQYzt+ysVE2HZWF6AcByTWiUaJLzJTlSAsGxTmqpMsNYOQmSaIVVNVUsOj095fDTiE8+/zPj4CY7rsrG5iev5qCQxdq7GSLIpRasdMxqNyUXOcLjB0eEh27s7BEFIu9M2HDN5QavdZjIeE8UxB/sHKKUYDAcEYcjx0RHvv/c+J6cntNsdKlUyenraaHzfYDIeU5UFsrZ58OABQsDO7jXSNEFpRafTQSlF3G6xublJt9cFIElTsjxnYzjE6XVxPI8ojpnNZhwdHJo6/LJES0V/q4/vG5DEdVyGwyGLxYIwDPnv/vv/nq9//evEoU/ke/R7PcIo/HG79sfeVnHDZ/e1r5pNmn8vhh6fdXv2I57Z8VaybrT0fINqaU1ZlhRFQbttiAHSLDM14EFIVZYU+RmS5AU+URhRywYttaxmIFirFHYtFUFgUmZ+8Rd+kW/YDr/7+l/w1d0+n3g+xvcE6XTOJ2722dzboSxr4n7FyfGMssj52nuH3P6Fl/nC7et84b2HHEnJB67gbd/hGyeP+f6JzV/d2+Oz8wQOj1DUKEujyxIr9JCRh10uIK5Rh8cwHeNubSJDh3L/iHTyGJwI7+Qp1nALPA88CyFLrGKOU2REVckwdhGxxXgiERU4QqNqzdFpTeSArODRW4f0WhpRKqwa6lQzvr8g6JVYls3B5AGD7T67UYB/mnB6MAHXZpaVbLk+G49POSlL6pswnSzY8SeIrMJ56wPqm3eohEv9+ANsIbDbPSzHgbIAzyZdzBFNgEQIgQ1UiymWG1IXBbbrrJi1LduiLiuqqqDIUqTGMJc3hGKWNIRYdVWRJgtsx6WuSsIobtBpieM09bx1RV1WeEGwImgDoDB1uEZ6S1Npw4peFTnJYo7nBzieqTfSukFwq5xaGCcuz3M0JvXbsh0cxyw8YRQz2NAEYYs0SairijxLmU2nTdqiSWGspUZojUbj+yG1rImiiDiODDEaUDepSZZtrwjTAt+g3J7vm/uRyhC1IVbPblX7bFsEQYTtGikyS1iNbm1p9NOXBGxlgd2Q22mMkyssizRNjYOulJHdQmDjmDprJdn1R3zhRo1DRVVJpLRRwkXZASio6wpZS2bjEW/eG/Hn74+plcBxbPzANxqatk2vP6AzGBK3u4RRTKvTx3E9k21Q19h2he26+I0W+VJnXbgGSfJ8H60cHNHI/zXBFttxmsDEnLooiTtdijyjKmssx8Z2bCNvBwblr0wqPdCwyBuCpLIoSBZGF9SxDSlbnmeG+K8hlDOlAcqogwhMjZ/nm37SpMAjLOqqAGGyCYq8WJW9/CTbs2QOrmo5BSsk7ixCC5fMuIs6vZdOeIWHL8QVBnXjWOjzlaGNfb92mXVErpE7aiIEyzMu7+HDItVLh0KsnW+5OJ7djV4z/s2fm6/u8SvRLxN1I+JOiFSSOA5XkjJgyP50c3KxauqZLNO59jSPQQiBYzlYro2jFZWsKaqSebogr0pTPmVbbG5tMATmWUGtFHm5ILcUwhZU2LS1hVYVma5ZqAVSK7RUuK6gF1ekVUltCQpfcHsj4mBRUvkR2almctdFlwoR+DDzIfQQw5KMHlqH0N1B1xWkBSqKsXcDWpFkq+tSyxRlaxaLjA+mJa4QbORj3JNDvrH/hH/y3D9jazjg/bvv82/+zb+hLAu+8IUv8V/93f8Kx/f4o//wb5kvUiopiXubpIsTbLsiL3Ns18ZdMvpr0ErzZH/K3k6fzmDIYj4j8HwkFrPJBGE7XLvzAlpJpuNTVJUjpieMDvcZXLvNYHOPIIxIp4fmZ5UpVSVBVtjC5hd+9ivcuXmTJ4cThBMQRjGBHzTcC8YYd1zXoNrLdPL1VPKlBNgS5W6I0hBrjjZn75f9QCAuOdbW2hi4slxk3ZJbH1PLevALnV8s9+PCiFwNlvVxsz4KxfnBtERQl3PEqjnLgMKZDM+5pl5s0KVtfQTqD9n9/P2uggM/sbP40a1abh8+2+lzz9GUYimK2SFVOkEIje0H2F6InE6oZUWrP0RrhdCa9nCHnU98haA1wLG9S8kJq6sILqT6//TvUXzoN8++2ZbN7u4e8/mCkJCe7FKWNXmRg4atzQ2SxJDwzmczoihgMZ8xGA5otVrcvnWLg/1DPN9nPp9RNqSx4/HEpIn7puSrlpJkkWDZwtiCngEcHj98xEsvfYJCKvK8oCxKsixvkFFjO9V1TTbOieIQ23aIIxfLsVnME/r9AZtbGxw8NU6m53m02x2e7u8zGAxwHNs4/a2ILM+QpzWe57G9tcVkPAKtiOIIWdWMpzO++dobjGZzPn1nj4EfIsuMOkuwwxgnaiMa1RTL81ekawgBtkDXjdyYUqgqR2OjpMJyXaRUIMwcUeQFD/f3saKQ0XjCdDIBYQhby6Kg3W6Dhtlihj40RJdZXlDVNWVV8+TpU3ST4dnudExtfpqyubnJo4cPOTk5RivNYGMT3QQG7t+7j1KKPMsYbBgt8Ddff524bQjujg4PSBaLlU3u2A5RFDEYbnB8csr2zjb3HzwkamQm9/b22NrepK5qTicTwigiSRI838MSEDbAx+nJKWle0G636Pf7dNot4pbx5548fYIGBv0+Wis2tzb5G3/zt+j3esymI9pRxObWzo85Uj5ku2pMNpPFOfqY8zPm5XOcNzk+9BJ/mdszO95VXWM36PWKJEhAEAQN4iZwbJuyKMm1qdMVlqGgD8MIgTH+y8qwPYdRbNAIjOafbTd1s7IGpXFcn5/9/OeJfYf/+N3v8K2y4Od2Bry84WHnJeN793A7bfyt2/zgg5T/8O4RvudwuJgwef4mG/EnufHD+2wejhmokjcjn7eVxf/jnXe52e3yn734Aq8mCc7BMTLPkbJAlJJSlShXIk9PKE+OsA+PsLb7ZI6FVRbIPCXLTxCjeyZqJmTTZkWtNGVlEM1YKYIYFrnFaC6hBldoxhlENswOMuq+jVdqIl9Q2ZoqhfqgxA0FJxXo9IhOGNBKasJ5xcy2oagRucSzLQaPTxhLTdUKOT2ZEQNqYWO9/T6ucFDdLtO3vkewe5tgZw9rPkH1PFSeYIUmUur7HiiBlBVeaGTGbMshz1LjUFlGA9yzAqSUWErhOK5JO7Ysalnj2g54Hk5pWLhNarhmPpuC1gSRQTHq2rBP1438k+O6qxpuu0kX1lozn89AG6KyMI6xG8SjyDNAU5cVqpGF8HxjuMkmNTuMYrI0IctSQ/5mmbrgxWxmkGs/YGM7YD6fE8VtIyXlBxRlgUAYPgKtSBdzqqoibIJFdVXieQHCEihpnN8gMBIVRqO6kdLSCoGgblLJlzqwUkkjFeY4tNsdLMtpnHFDMFiUJa1W20i8uYY0yHHPMkqEMJqdrueRLharuvXQkXx6u+Rmp0DLikpCrT1wTLS1ro2WdZXnjCYJf/L6E97dz0yttW9Q3zwv6A82CFttusMtk2YexWjAsi2iODILxxJlb6THLGGtWN2z2YKw1SYMoyZzIULWNb5rSEzUSnpEo5VGypo8yxr5OUm6yHEcm6CR79CNU1ZVBp1ecgS4TQaA1qaGP4ojVK3I0mS1eBtNemWi+J5v+qtlmM2X/AFGe9iibjIt0iT5iSbdMwdz9cm5P+c+XYVnz1Av4JzhLNaN4nW0Tp85q2c26EVTVZ+16arF6BxMJ1ixCK/vJy7cSxNOPk84fEYpdZFcyhyyNNr1+iFr7sa6x2LO3Rt06Q46KydJaX121ubedcMcZSEuSCUvne/zT0I1pTFL7gGpNVKZcqhu3CaSNdNsjqsV0ypjkc/BdSjKnDAqkDZUUpLkI6bWKUdixIwUdM1Qu8SOzSiZkRQLpBAI6dALQlIFI+EgbMHTv6hQiy6itFAjgRW08HYcsklCOdzD6vmo7hA9PjGPftCCts8mcza1xPcjjvyY+uYu46BPUSzovvAin255WMdHfPvb3+T+3bt8+ctfxnMNAdFsPuOb3/ozvv3tbzOfz/j857/A7/zO3+OVF69z9+0/Y2tnl8NRjZxnK+JH3UCsldY83B8z6LUYdm0cy8FunJ2o1UUIi8ViSplnZMmE3mCHG5/4HBu7txCWIJud4vghjhsgBMgiQ2mB0BLXd7m5t8O163scjRK08HBdQ5a2qtu2l1JgNpYlGlUDsaa/fUU6+bJPXqrbFueG0GWzTJzrjZrL+6ziQ0unfjU+rwhwNeNR6DMEZdn3rwiFmf+vpXqfGyxrDVjaXMvxsx5i0menegbQR1z4e9UuZ/PXxab9/2O7OB0tn6RSkny2b0qohIXjBpRFZtaD2mQ9eb5Pqz+gf+sVerufwHb81WnO12tf3J4BPtPLP2u/xjOjbpfn7I/1pDUMh5uEwQPG4xG27dDr9ZhOp+R5Tq8/JEkPyLOcnd1tOt0uJ8fHFEWJ73srCTHQLOZzkk6bvWt7pFlGkiT4jiE0VVKzf3DA3rVrdPtdhBCG+Kvb5eDgkGt7e0RByMJx6XY7XNvb4/T4hLIssW2bNE0JgoGRAq4qunG8qjFP04wwCmm126RZSl1XDPoD8jzHsiy62x0sx6bVavH06RPKqsJvZI2zPCWKIvwgoNVuMRqN+c50yqMnT/niKy9za7NDbEvqPENVJZYX4rYM4a2wbViWyQqN5XhNeaAEywalsRuACFWA7YBlU5Q1SVGhwpjj4yNGI0MyNplOmc8OuLa3R1EV7O5eQ0lJUZW4jovreLQ322RZxsbmBlEYIZXk9OSELM8aG89iNl/Qabcp8pzJZMzjx4/Y3tlBWBZbO9sc7O/zdP8pVVkxOj0hTRe4jmPIZm2HxWJBluV4vsdiseDzX/gCDx88QKMJ/et88pMv0+l0efpkn8lkwt7eNYIwZH9/HwF0us3zEbC9s8PtKGB0OqKuDUHuwdN9Ot0OQjTk2UqRZjl1LdnY3OSrv/xLvPvDH/KJF19go1G/+bgj4eP0/49zktV4/4g9Ln99Ibj5Iz9/tu2ZHW+rodI3DpFJg0VpLMc2DndZYgmbIIwaGSi/Se+rjZabFihUY6grsnSBJUxKhqkpMym7xhj3TdprEPL5z3yR3eEWf/aD7/MHd+9il4o7Edxu2QzTjO9/4/t87b0JQeDz13/meb74pc+gD98njTP47Ab3v1mjHx9zfZowDgSp4/PeaMrd0wkvbgz41Vdf5LN5hfXoETrPjPNyNKNIcrSCbJ5iCcWDTLBZV8S2RNgCyxJoIdFK4dogqbGkAKXIS01dQJ5ZCBTXIsE815wuNHUtSJUmcjSjU4hsjcwgdDVKQFVDPtO4FhzlNeV4gVaaGEGhayNZIyS6EjhC4D48pPY9RkrhI+j2Ozh/+j3U8Qg17IHtkt8ZUe8/QhQFTvF9gpvXqa+/AHVBqSRZcoodtKlLw/5Z6xKANJmvnGE/CPDDyDhbmFprYdsIyyJv5L6M/JXpD7KqGvI2k/4llVwx4QsEtu0aZvs8M4z5qtGydhw8z6fIDQt4GJva5yI10lGe74NlYWunOd5Edw3pmbtiXwfI8xzPM45yu9tbMW0nizmO6+H5AUWek6WpqZEKQ1zfWyEseZ4RRa0mRdVEPWfTCQiLbq9v7kVYaHI8zzjJi3mKaL73fJ80TUzUttG+XBKz1VKS5TlxZNIrPd+kmxuU3RiWSpnnniYLtFIG6Q0jw7QuJbGe8vM3NX0/p5aSspRUOkA4USOlYcjJyqLg/pMTvvHDUw6mRRPosqhKow/e7nRxg4iN7Wv0Bhv0BoMm6BAbtQJpUuK9wMe2XVzPpSxK0jSh1e4QhzFlnuNYNkWWUQC246LRKx3zuBVT5AWyrsz9lxWiCdx4nkc7jEAbDXPbNfNBmecGNW9Q6uU8E0Zx4ygsmjTURlFBaZNxE4YrxChZLIxjXZYNwZ1PIQ0rfZkXxO0WUtZ4gb8sPf8xN3HFq8vm1GXkaA3dFo3Drde0c9cm+DPj/Tw+vlwwLr5fWuKXgLQ13JpLx3zIDVzczkHk4kKLzjsFq9aJpYFqnOnzjsPafTbOzToKuSy9E6sHc6YBvr6tL53mGOP2OJaDEqZURimJ1IpSVWSyYJrPUUBa5aRVzqyakmQpo2rMpJyTVBWJzFlUKfMyQVU1feHT9mLG8wn7sxlJkWMLl1YrYJTBja7D3anNrHZZfA+ysQc1aFmCa2MNXWSpqdrXsKIuIvfB8RBRgHBKRN/C8iWRBitLEUVNFYY8dQOcYkbgWPywtY1+acCX+R6v/8d/T5pl/Omf/il/7df/Otf39nj3/ffJigLXNvwa1/eu8+TRIyI7w5Yp+BGHxyeGEBENymTsLB1JheDoZMRkLLh1fYt+O2Tr2i6OBdligZaKUmo6gz00ClkrpqcHaFUBishxWGQLXNulKlMsyyHqbOJ4HlWZ4wqL3aGklD7SCs+c7eavEEuEe42RXJw53WDsEtNnzD8rtHutH198v3qtz7OIL4M4F1xuWPa9pYO8GoGs+vUVJ18LTumrU9VXh5wPxJ07hz7bR6w3drnPBcdRL8fZ+VjauXNfCiysNU6sfXZp7vrL8L7XndQlevWMhwoBWtYoWVGkCXWV4QYRlCWt/gZOVBC1+7SHO/T3PkFv9+WV0706x/KfS37v8lkt594f5+bPhRybV88ApT+LD95Mfp7nM9wYMp1NzdoYBlRVQZqmLOYLUIp5NkejSZOEbqfDbDZlOBwynUwYTyaEYUSSppRFwf7+AX7gN+V9FY7j0um2OTpMefToIZ7nsbW1zWKR0Ov1KYqck+Mjdq/t4gfGbp+MJ0xnM8Iwot/rMplOuH/vg0bJRJNlOVJKDg/2UVrRarUbGVaJrIy/MJtN8X2f8WxqJOGSlOlshlaK7a2tRta1wg+8hrS2JI6NMzyZTjg8OuLa9jbP7+3w4t4OXcfB1ZpyNkLLGtsLsRwX4XqIBvRDa3A8LHe5bjQIuJRQV2BZpHlGoTS26xK02rB/AGja7RgLePjwAZ1ul3a7xQ9/+DZpmjAcDtnc2qbX7zObTjk+PjElmJZFvwmUHOzvc+PmjRXBXavVIggCRuMRjm2TFQWTJhNhMZszGY9IJ2OEbfPbf/+/4ctf/hlc1+P09JR/92//Lb/3L/85R4eHLJIFzz33PEIItre2uH/vPr5vzus6rplvHQfHczk8PGSRJJyORty5fQvHcbl//wFlUbB7bdeQYhcl2cERURzhNFlScSuiLEr2D/YZbmzwxa/8DDtbWzjuRygEfIxtPZj/LMPnJ7vSs574J8PGn9nx9oIAx3ZwXYeyaOpIXA9ZV6RpiuPYhGEIQuC6pgbc9TyqyqSGLdOEPc84NnVdG03wujZpxkpSFmoV9Za1YSOWdc1wa5ufe+FFXup12B9PuX+4z5+OJ/TtjL2ux1//9IDRrOS2myKe/gAR+vjXbzM+POZ/nsxwfIt/0HL4P6U106rkHSF4Kww5SVP+97fe4Q99l88OB3w2jNlazHD396kShZIV0pZYScKdXoCYC4OcIalqo4ec5ZpaCyplSGqUBAVYHkSWIk0EsxnYArZCKKVmUQjGBXQcyI1sM3UGLQ9UpbEQ1LWmLQSuhhxIMMZpimahwULjNItjleYIoAQ2D04Y2DZ6uiDoxaRZjbP7FmpzAyVr/KrG63Sp/9bfQQ2GRO0OpRdRS3AFlEWKY1l4ro3jSmwrx3UEssqRlUGVpXCwhKk1Fk3gpCxzPC+griscz0zAWVN6MJ9PSRZzWu02casDGmRdIesa1/NNEENDlqb4ftAQpsW4joPGEFCoukYIU5Zguy5Kaeq0blBu4zwJ26JaGJZO13Hx+wZ9nYxH5IVJ6U6yHCEsoriN7we02m2yNFvVVWdZQpZlBFFMf7iJZVnMZ1NDRiIE/cGGieD6JhrquC5eEJGnhhW9P9gwKeeN02tZNp7nEbfaLKWkyrI0kjieqWl2PR8/8A0ztxAYtk3wnKYGTZtF0yCAijjwuNMv+OItcCmopaQoNUq0QTgmZR+D9KVJwlv3TvjTd0YkhTSkRmGIUhrP9Qjbbbob2/Q3tg1pW9xGYxGELZxmLCIEsq7I8wLpSlzXoMqtdgfPNQGUKDaMo0pKlNaGhM00grxMG3by0MgNeT5Vk/oet1oobUpOyrygrAqEZpVGjtBNG2xDcFeVhpHU99FakiQLPC/AcW20aBx9y8XxnJXEm2rKBJTSFGXRpJbnTc2oS5qmJqOgCdj8pNtVU/ISSXymOV2cOZi6QXkvO7Xnr/dR69GaK9q4ARcteHHuHfoCwrR2ZX0hB3PdnD+X+nrJeV+D45oX627AyntoPKClkS+WrRZr7voVD9g83zM0HDSy4aSQSiK1NFkoyvTPGkmtFUmdUQrFokyZF3PSKiWtC2blnJNqQVrWFEpSqApkRg8bjSDLU07yI4qqxLYs2q0WQeiTlIK4H3I88dl/oHGVT3LaARTaUghfg6OxeiHliY3e2EaIGOWU0HLADxAIRKyJQh+7rlB5QVbneFLink7p9IYIKXk6XfBaNsOtJS+89BI/fPNNnnv+RSxh8Q//4T8kK3JarTZf/YWfZ2fbMAor1+Jv/vY/4M+/9vu8/r1vk+cVYSQMGWVDCKmVAi2bn0STlZr37j7i1s1r7Gz1iSKTSTRfLFjM51RFxu0XPolQNfPxAVob3pDF+BhZVfitDkJqOlvXsRyPMs+wLEFdJFjCxiHB0hLHG2LbnlEaaZzui+g2cPn1mre2+ty8Weupa53yQ8agWPadDyvtWHdQf+Q4Xnq/y2ASF/rtJS+alUO/Piyvus455/CjG3IuQNC06VKc4MLJzwXn1kpB/jL87rNWLtv0UZRI5x+iUpIym1BnCfPjpxR5RmtjCy/qUCuIvYCtFz/H5q3P43pxEznhyvnj4jO9OEdePORikFKsdtJnAYor7PgPM+0vhiHP73z5aazCAZZga3uH8XjM48dPSJOUxWxBr9vFD3yqujA8SmVFnplAv5ISz/NACLqdDgiL3d1doiimKktarSFZmuF7BjiQVc3W1jZlXfH08WOCIEBJSVkUbG5ssr+/31yj5ObNW8StmPF4zHw+w7UtwsBnkSwIl+nmrRZJklAWJVETcA8Cn2SxMJl/bou41aKqKuqq4vj4mG6nw3Q+I/ADDvYPGW4MUVLSjluMxyNOTo5XAdZaSmZJyqPvfo/9oxt8cHDE9a0N9jYGxI7NIA7wpEIXC84ITjVaKxNdsm1sz/AIactCCYHtB3iWg7ZsHh+doBYJ2A5P9/fxQlP25gc+vX7flL4gaMUt2u22CSCMxxweHHL9xnU0mvF4wiJZ8OD+fQLfN0o3+4IsSSjKkuPjI9Ik4bnnn6fb7/PowUOK2pDMjscjJoeHiKoi2tjk5U++2qjJKNqdNsNhHykl0+mMd955hyiM+NSnPkWapMxnC1qtmDiO6XQ6xO2Y6dRopsetmI3hBlErIopjbMeh3+/h2A5hFBIGAXmaIYRRpgqjiKOjIxZJQhRFTGczWu02URhRlAVplhBH8Y/u/M+wrcrUVtlEH32uD7OHPmz4/yTbjxMLeHZyNaWodYkQVoOCGjkm27JotdoIYQx9VasVSlVVVZNeYuqWBRgypDg2kkuNEeU47oqASmBSmzWaOqtxXAeBoH1tD5IZ4XjEi7u7pGrIqFhQC9jUY75yLSG2K1SZY2ExP57wB68d8GQy58Z2B/dv/Rzy8THDe4/52UnKc3nNg6rmQNiM65Jvzqd8Uyn2opgXBpvc6W+ylU5p5QuoFNXhgnFlrSSMpAuuC56jUZUiFDa1lkhhzFEFKFsTBsZJns81shJoIXCFZtsXzEtNKQWJ0rQtUDV0XIEuQQkogMgCFFRoPMDHfF4CNVABBYIKc51Kw7SWtJOMa2nOHI07nxPvnyLLitq3OK4tkqzE/iu/jPuJF4naHTynoB2aNFxLZoRxgBd2SWenVMkEr7NBlSdg2di2YDzNmdq7KN3UAdU1dbVAaM0ky2h1usTtFkWWIawA2/XwPd/otjdsmsuUFqxGu7mp4et0e4agojFEfD+AwOiKF0WBXRtWbmFbOK5hQ3IbyTM/CnEql1rWTXqxRX+4ieeHyCb12ujABqvU09Iu6Xb7LJIFnh+a/ihMwKgsSlrtDsYJUriOIXpLFguyNKGuK4OK15Jut49GEwROg6h7KKnMet8QbtSyot3pkmc5UWzqiOpaUhZG4iqIY5ymXqiqKqIoXrH4CiFohzaf3c251YO6LpDSolIWyvbRwkVVFRpFmWdM5xnfeuuA1x8tsB0P1zETQ5oktNpt/HaHuN2l0xvSanfwg8iksAdGtkfWTc19YfS1gyjEa9By2zb1W7KuabXbaFST2l5RlgVRFDcyYya6DU0ZidZGVi6MGg1xk1LmBwHCAsf2TRp6USAAxzXlLW4giMIIrRV5nuN4Hq7rUjYM+JZlUVcV8+nUyBV1OkSNTIdUypQ2OKaOvqpKoijCth2K3NSkWZb1k0doz5jFzhZ0OEOPLlpret24XRp451mSz9Wdrlmk68bpRR/3kuG6tC3Wr3suhHwGNZ0ZkGe7nV9MBBevsG4qa9Esih9iO17yx1nb4SqLu5lPl7JB5rgrEMQVOChYScHoxuHWilpW1EqaeUpVpKpkVi5IZEFaF+SyIKtTMlWQ1RmlLClVyVTNkKrEV+DUFp7r4to2C1tgaY1jhYzmLrXtMp/bFGVAeVchC59WBLMPOtQJJn0xsAAbexChpIVohYigDXmCZWWoYQfR2kac7ON6GpkXpFVFni4wJIFgta8xj2P+p09f53cf7XOiSv4vf/tvs/23/gbf+c636HT7/P6//n0QFmVZcvPGDbrdLpPpKa+//jp/93f+HhpoDfbo7Y7xehl5Nl/LJJAoIYzfrWXDuyGppeDuvUdMJlOev3OdbtdwuUgleXD3PZ5/+bMsJmPmR08NkVFdILEI/JDWcAfPj7Bsh7ouEY6HVhVKllRlTlVkhO0hKj3GjjewnLghULMN2r2s30awqq1u/jnndK+9PutWV3g9a/39zOfTZ33wXEBpre8ux9D6IPkoU2vVp5u8i9X8cDFUttYOsXbBpiWXseeL1zk7x7nxe/GID2OIW/MeL4b3zrudP23X+7I3/3GMYq0k2fgxZWKQPy8IcL0A2/UJ2g624+HHPSw3PBdA+BEtOv/qGW5Zn00+5hB94TJXPPdL7vxHNO+yk35+a7d73L7zHLPZzJQDOjZZntHr90xZlVYEvkdV1cRxTBgZItLtrS2Ojo5XAWfX9chzkzZs2xa+71HVBUHgkxdVIy3rMZ8nDIcbhGFg5MTshoPG83j8+BGbW1uEjXNeSaNUsjncQNgOx0eHWJZFFIXYloNtCcaTMa1Wi43NIZ5nOG2KMjf63WFokPp+j26vR1kVdNodxuMRQRBwenrCZDI15W11RRTHtOIWSZaYoIHSPD084vHTA+IoIk/mPH/rFv1WzLWdbXSZ4ds2w24bigKrWSVF1mTS2IaoFdtB4TCazXn+1Vd5cnzMaDIx9klZolxFy+uwsTGkrmtmsxkvf/LllcZ5XpiSh8Viwfj0lBs3rvP08VMWaYLtOvT7A+598D79/oDnXnie05MTiqJga2sL23Zod9vUo5okTRmfnkJZIhHM04y/+K5hM0+zjKqqeO0vvtfY1oI8y/nggw/Y3Nzg81/8EgIMmh6GKxnfViuiPxhwenKKsAQ3rt/k+PgYq6oZ9PsrWbDjo2OTsZAX9LpdprMZg/7QkOjFIb1+3xC/labW//DwkFs3b5nsRDhXFvas209z5nmWuWWtku8vdXtmx7uuDdmRUpKiyNFKEcXGsbJsg+rJuqaqa7M4CoM0uU098PIY13UZnZ6cySopY9xHUYzAQsqasioM27XvU5WlQaOiiN5LrxippYePCEvNlohQlqKQG2SpD3KCJRY47pzx8TFfe/0AKTW3o5rW4jHBC5tYn/wcBw/3+T/+8A3cXPHZwCOoJFmhmEjNaJbyPeuYbzo2ke1yTcBzwKYVEAoXCeTzGVmekToWSlRoxyPvDsjzknIyRVPj2hZ+K6ayayw3p9fXLOaKMtfYGhwB2MahrhQkElI0Ugliq0HN0SyMDDY1IBFYaGzAxTjgNuCjkRhUfFnlONGQak2EZlpKvNECD02wEPQdm+TPvk/yxru4v/oFNn/7rzK8sUeeHqO1wOtsMD/Zx/En9HdvczIf4zoCt9Uim51g2RG+lRDojNKJSeYLfN8nmS/o9noIIVYTUlmVhsQs9Ax7vdKUVdk46kXDrlmbOlzHxXIdqoZozXaNw4kwjJSWMA67WWA9k5reaC/mWWrqADFOobBsk51RVWgEtm2jtSKK40bWrnG6i5w0TfGCkFa7g2XbKCk5PT5CA1Ejv1HkBVmWNs6mxLZt4na7Wdg0HcdByZqyKBt5D9Oni6KgKHJQiiiMCZ2IoigJooiyKAjCsElXhzCMKMuiYRe3SZME6QcGVfd92lbCzz9fMwglZampK01ZaZQVII1QJbWW5MmCg6MZf/rDE55MKnwvbGr1jUScsGy6/Q38qMXG1g5+GDf62JJqMUegCYMQiaRq6qmXDnCpGrZwMFFzBHmeo5QijCyyNAVhJAJlLVkkc+J2hzw1qLclBK4nsCyN49o4TmRKCfIcWddYwiLwffymht08D1PSYNmWCQxZ1irDIXJjijwjTVOi2PxWrU4HKSVVWVE3v0kcL2vWHaQ0mQhpmlBXNUEY4Hr+KiPhx9lWNd4XPN6LEdZzjueFwsVL8/3ynCsk7sL1roBhzhGknas9XUO6Vg7GWYv08p81T1tcushaW1aXESzZiM78+TNPYBmZXi1o5/x2sfZ2zfhvCJ3UyqA1f1VzvmWL9BJh0meOuabJPFKSukG6K1lTyZpaVuSyoFAFiyojVyV5XVDKgqROmJUJeVWQVSXvl085KWe8HO3REwFFOaMKFWVVMS1L7k4lTyceixOoUov2dZ/0sUAdlahOF6sdMH5cU2U2hD44FjgexDEqLdDzElo1OBW2UmirwCJFtyNQIXWdUQvBKE3oVhK7rrC04BpT4q7LdujyW7Ggv7fNl2/fQFYVDx8/xrEt/pv/+u9T/Jc53/r2d9jYHPK3/4u/Tbvd5vDAoII/fPN1LEvw/Euv8p1v/Rmu6zdyMya4qJTJNNN1SV0WDQu7RCvJaDwGIdjeLtjZ2kBpxd71m5RlRl2WFIXJ/Olu7hG5Hjs3XkCqGtDkydSMX9fHEi4KGyFs2v1tlKyxbEW9eIqIN/Fam8ZgE2u9ba3PnA2C8861oAn+cMV2MfB14TNz7LLnXuHwiPP99UNRUC4OL0FjxjfG5xmqvE5cJi4eu/pEnHu3bNul2eqCGoK4MMZBN0P/fIr9JUNzVd6xwlV/+j73VduFae7yvHn+/lSdk46eYHshfrtPnsyQSlOMTrCDgNbmTfzW0By5MqivfHKXrrTa61yw5uybi3yW62uH0pdbfv6kl+9zNU+eP4IVn9KHnERggdB0uwM2t7aZzxN2d3cYnY6YTIz86OnJCVmWE7di8iInjloNB4pnGLEn03O8KBaglOJgf5/pbMr2zg5SSvI8p9VqUTZSoXmRY82NrR+FIe1WC40p7ZvN5vQHfY4OD42tFMeouibNMlpxjO961GUjqSold99/j9t3nsO2HZLFgqOjI0NW1u2YrDipGAz7jE7HVKXJltNKU9UVaJMVNx5PuPPcc5SNTnlZlDiuTZGXCAEHR4egNH/x1ttkWUYcRbRaLWbTCde2t+i3WtRpSjf26bViQteh14pwHBsla45PR3z3zbco3YCt7R2m8wWD4ZBaSoP4Tib4nkdRFmaeawCUjeEQy7aYTmccHBxQ1hWHh0f4YYgfBly7tsfp6cg4tWnG0eER167tUlUVUkreefddUyKZpBweHCCLEqEUn/jMZ+nt7PD//F/+F6OwozQIjbAEn/3c52i3O3zjT77OeDzmzTffJIxihhubLJIFe3vX8QOfNEkZDgd4rsvx0RHzeUCWpLQ7beqqxnM9irygLCtkXaGxCMOQJE3p9rpkaUZoh9RSMh9PQUOrFa+exdP9fa5f22OlZ7Y2lM474j9iXF6c+y9sV83XcPVc8qE7LwlkLznfP2rO+PjbMzvedqOVWZYFWkOr1aasKjOBCINWW55H0RAq2JZtGNC1+T7wG3IuyyKKY8wibzTqlFKmbtU2actVZfazLUMYoGpDXqVdF33nRbK4jX96gjVdUM4WeMpGaodaddHljMPxI94czbjhufzaCxFfueajnp5Qno7p7fQRrsM+Fnbs8Fd+4wsMLYv0yYibxwnvfvCUb4wm5JZg4NsI22KsBUJXdG2HzTDg+S/+HFkOB6+9zuL4mF4/4rmf+1nC7T1m773F9HvfwHJsdv/KX6UYnzL91nepFhntGEqhKApw0dgWeJZFKRRpLZgrOFKarjaOdYJJMy8wCLoGgqYT1BjHWwFdzuxl2RzTaY6XTWc91hqv6U2LWtK2BPFswfwP/pwHu9v0/u5NbC/m+ME7XO9vE3b7HL73A/pbOzhRyJP33ubGK59lfLDPfHpMXdW4g5RgxyNsWbh2Td+zcawFtqWxqXBdbeoV0Vi2A8Kg3HPpMrO3UMrGCyJ8S6yIuizLAosm/dwwaQvAdh1D0iON/NWSTM22baRSgKIqJa7nYlkOqizJ0xRhW0hlDA5Z13i+j+8H1LUJIDmux8b2NnZTe12VFb7v47oOeZoRt2KU1sStDnmeMRmNmhpj0x4hLCbjEUHgU1U2dkPk5Ta63I5r2Nttx2nq4XMcx6Xb7jX13yW+72O7Dm5D8JEmc4qioNPtgjZlCq/cDHh1s8aiJktryrxESgspQsqybvS0M5LFgvefTPnzd8ckZaOfboFtW3iBh+eHdPsbtLtDopbRaVRKU+aFIcELu5RFwXQ8wvMNMRlN/b6sa7I0QVgQt7pGb91zCfzAlIcohReELBP1sizBthyE1sRxbCLr2qTa13VNlRY4rksUxWitOD7ap9PtryTIHMchikKqyrCay1oibIswikErirJACtmksZvaPdtxmkiuyRCwlUUYBljOkpjNMMyblHcP23FQWpNnGeFPFOq8fOy6gyAufG6MrbXkbHHuiHP7fWgIdulorx16LkV7/biL5193gq9sPVxcoYzPLs59u37w5WaerayX1rG1RXTdH9IrA7R5NmuOO5g5YpWkrs9/r7SZJZWikdLT1HIpm6gb+TGBUoYR2NYWjmWjpQYpULXmIB/zw9kDOq7PV1vPIxSM86lxKPOKvIYfFhanWcQ8t6hKsANBnkB5IMDvYodt1IFAFh5ELrgetAOsbhcOE9AWOnJMfbLnomMfnUuEzGF+CpFHeDohQuCXBUdhF7TCFw5u4GKFLf79vQPck1NutNu89eYb1JXki1/+CmEY8C//+T/jT7/+x9y6dYtf/eVfptfrUpYmANnqdHGDgB987zukaWb0sR0b13GajDOQskbWRpKwLnPKIqcuS7Qs0VoxHo1J5gkIh3YrJitHFHlGWeRIJagx83K7N6DI5iwmx/hxlyDuM5uc4Nguo6P72LbH3nOvIpUhbkULhONRzPZRZUZreNPog6/6g2i61Id2OEwvWe90H+Ee67N+tvzukjt90eBbS9W+QlJ7dcyHzQbLJl2cD9a9/nNtuGKMr8bNJThoLWpw7vPle3Fp97NzXDYur2Z5/2lt63PLchK6yr1eD1Gc32RlMrEs18dyPWOTaEO66dsB0eA6fqsPWBdKV85eX2yNmXMapYc1h/diUHYd5dbrZ1irjb/Kwr9qijz/5vydLrM9lhGSZWaGWLahuYxj2ya1udUGoSmKgsV8gWUJ9vb2OD4+pt3p8OTxUzY3NlFKk+U5FrCzs827777HfD5HK8Xm5ibDjSGHBwcURUGySEDAbDYjjmK6nTaLJGE2mxHuXWNzY4M8N06yEAZQC4KALMsNd5NtylCruqbb6VCUJUkju2XZFr1enyRZAJowCnE9l0WaMOj3aHfazOcLHNtmNp3j2jbzxQIhIIxCxuMxfhDSbnewrZSqMtwxWiv29q4jpcJ1HSbjCa7vkWRzWi0j4SUEdHs9bM/nO9//Adeu7TIejbEsgWrkaztxTDsMKJM5T54+Jd7cwvdtiryg2+nSHwyo65qiLIiiCIQgDAJ6/QFZmnF6csx8ZrTKd3d38Twf13OpyoKqlmxvbXM6Ntd8+ZVXOD0+ocgzDvYP2NraImi1Gq4LTVUVjE5PkdqUON64c5tvffvbFEVxviMDp6NTfv2v/wY/eO01Fos5i8UCtCaKQkCzWMxNyWUUMp/N8XyPF198AaW1kYCra+Io4vjkGMdxuHbtGtPphDQr8AKfo4N9U5LpB4Zor9MxNl+a4tgG9Kpqk7k4mU3odXogzshQP74bKz7O2491qotfXd22j/7m427P7HgXZYGjHEMOpY026bI2e0kWpaTEcU0qcS0lutS4roclxCo1VUAzMDRKGoIEx3WhSQNcIipFUTYOl0sYtykadmfX89EbmySOgx2N8SOLcDHDThUyL8mkAN3hc27FL93yiHvgBQ5aSWxbUo0P+Y+vjTieZryw6XMtSkELoucC9Ct7FG5N8tqCrgNf3euzUefoRY7CBd+j/wu/TuulT+NLSfypT/Da//sfc7BYYI9LPvnqHr3NbQ7fe5vJk6c8/cf/ghALXUuKXIEStC2BbWuSChwlyGvNojYp4hmaGkGuNR0Mmm2wRfNMFJpTIEbgAH1MqnlyzniAAs0EQQdDyrZA42HqqLvAAfBIabaFppUVyCfH1FVGXS44fPyY669+BS8IcTyf0fEBFjA+fkpveoOo22IxPsB2bEZ3v0v86HVuPn+HVqeN43pY0MiWKhCW0UTEQhcKYVuouuL0/gOc5/4GVhgDGkvYVEWO47rIskTYNk5DuKZqw35Zl4Vx/rQmLzKWyLZUiqoosB0XISRKKhOocR2k1MiyxHIctADHEtgCgtAlzyqoFe3QsO4WhUFWPRccUeJEDlbcMkhvVZPVNlFkENUsy1ZId7JYmDRpaKTJJJYwuphKa2aTMb3hENsyafFZklLVJVmWUZWmTkZKSZVlKE9hMhY0QRAQxS0smfK5vYLnhgadL8qaIi+oKoGwY+NsWA55ljEZj/n+ByO+98EUJSzcBuVPUxOp9oOYdqdLFLWwbKMr7nimBEBpDPkJEISmTWVpNLEX0zlLazGKY5Pmb4GsJbISlCYCgRAC3w+aRcsYEFJJsiTFdh3q2rC+O46LbVtYgY/rGqTZdlyu37yNUorFbEYYxeRNtoCSJj29rkporiHQjcyYRkplMis8ZeRCLNM3VCO9VkuJrzRFXuB5Rmu8rmvTV7RRIViWP/y42yW/eQ0puRR11Y0pvyR0YTmtr2O/583DdW7w87baFQbs2jEXt3N76Iuff7iTctXicv6q6zjehWOusnbPGa3N6zVYXDe/hdKsRaLVuX2XhGs0qeXLz4zGt1qtSZWsqGRFoQoyWVDIglwVJDIjqTMm5YyHsyPemj1EuzWvtHbYsTpM5nOyPMMVmpblc6hc7teKVNoUtU1V2AhHm3rlsQAvBC9Ejiy0cCF0IQjRrRDLdeG4mak3AkSiIHLRkYVoO9DtInSBO5tQBttYtsfmk7tYVYZQjYRiWVBbcHNnwF0dE2ze5n/84ufpBR6v/+AHLNIEheZXf+3X+M3f/A02BkPu3/uAe3ff54+/9sfcee45fuEXf4WyKNnYvsb3v/dNPNfFD/wV94oQoJcp+VVBXXiUvk9VmJTwulF9UFpz/95DOt0Ou9sD5tMpulKUWnNycEh3eM2Md2ECn1JpJof3SdME27K5/vyraFVTFQkCQW0JhGVTlxmqzMnLgrrMaQ1vELUGDfkR5wbaVcGcq2wjrS/tvcq+uOw8XzCwLg0vccWu+uIH641qPrsKZvkQk22VkbLukK5dY3Wqq8brBa/+HG33FfPEudu5auZZfvJMStU/4XbZvV5/DOcQMq0pswmW41KnMzw/JOz00FpgeyH9258i7l9DKxDOerDl4vMx867WNBk2euV4s/Z3rRnA+fl67URX3tPFd8ujzp313EMX54IeS6RerH1wVt5jHooQFv3BgPfeexc/CAFDMjqbThkMBxRlieM4tNstwyze6ZGkKYvFgiLP2d3d5cGD+wRhE8C2bfr9AZ5nwLNer4eWmiD0qap6VbpVlCXdXhcpwbYd5vM5tl0QBiF+oOn3BxRFbvSzLRMgl5WR8KwaOVnbtcmyjKIwa3Ucxzi2KcObjKe02y2yLGM8mqC1ZjabYTvGYa+rGkuUDIdGT1wAJ0eHRj87bnF0dES/38PzfTzf5eGDEe1Wm42tTeyGM0gIQdyKebr/lDiKiKI2RVkwnU44OjmlrkpkWdDr97m+scXRyajRxzZ2Wl3XbG5tEgQ+R4cnYFkkScJ8kbCxucVoPEJOJ2zvbPPCC8/z8OFDJpMJURiRpAlZmmAhODk64v79ewwGQ6Io5v27dxvSYhfXDinrmrIswbIppeJb3/o2Tx4/vrLLHe7v840/+Xpj04jmuU1JU6Mg5G35gCYMIxbJgv39A/q9HmijYGVZguPjE5LFgpu3buJ6LlluskNrWTOdzYijFkezo1Uwcnf3Gq7ncnhwQBRHhvSuKinyzPQH3+csePrR4+RsMHzMWUfzIed/hu1CYPNDd/gpzITP7Hgva61BYNnGiWv1esZ5ro2TnOUZjutiOS5WM7+VZWFSabU0gxiNrCTCMka5UMs0QbFiNDdM2DYajZKyIexyqcsS23GN1JHWyCjmWAIKfGzcusL3LDbaAZtem7xeUBcVWhW4tkbIigdJwhtPczwh+JmhzeKd9yllQ372wvPYVsXhosbfaPPCz30eK0+pK5933niLhydTns9rXnBihA+JsnmYJlhFwfEf/luefP87dDotxg8OKGuBlBo7iti4c4MynSMePqILhLYglhqpIdEmQl+hKRBN/bbgFM0G4CE4BGKgjcBBkwsomt8+AraEoNBwgkYBVeOYF8AxBiV30OSAxOIamrmGuYDUErSTMWHUIhMVqs44uPcmu7c/QVlXpOMTbD9AVgXHDz/AdRRFWXPnU58l8Fzu/uAHhIGFLDeMprRlQ4M8WbaDEub3UWgsC2RRkE9PKI7u4W29iGsLhK4RujboE9oUu9cVQinCpaZ4LXFsibRrHFujZImljEOmHdNfpFViCw2eRlBhueZcWtfGwApqLGEj6wJcibIrhKyw0ChXYlk2SkizELsadI0tXDIt+UDdprICPNsnSRJUXRC121i2TZam2A27u+d4RjpMa1rtNmFgMj0m4xFlkQOCbrdHXVcstKmrEqLC871GJ9siCiMEmt224NM7mm6gqIqaLM2pSkUtPaTd1NaXJVmacDpJ+eY7JzwY1SgEnuPgNVHH/mBAGLUIwhb94TZRu22Y1D3fMA23IqQCYZkxiDD1zlKalK5Oz8gHmYk6XAXOLMuhrCs8zzMSYFpRyoog8HEdB6VShLVULPAATVXVBIE5h5TGmZK1WpWc1LUkaqK8dV0zGY9wXZcojnFct3Gom6CAZVjOy8aokFIZIrZmbEgpydKUuNVuIu4VjuNQ2fWKq8HSNlVZkpcljvvslBeXtitsar10sM851BcMyHNo+Dns5NxJ9YcuB+Lc6/WlYd2QFlfs/VHbVb7ylSc4Z8eKq49Za8A5P+XSRUXDR2XSx4FGWs4cdAmB0pyhU0o3KLchU1sSqdWqppAmpTyvc3KZk8qURKbMZMrD9Ii35vdZqILbnQ3u2Bss5mPmckzgWASxmT3fnpa8MVPUOqTILZSyUbmPJRVVpdGVh+WESOmiwwhRK6hrCAPwfBjP0apC9yNEXiLCAN0NIdBoUWL7GmW7BKMF/khQ97o4TyRug9zb2mjOlkXKe9/6Di9+5lOc4PLuvYd8/hO38aKQf/P7/4ooDHnl1Vd46ZOvcHByguXYvPnmG3z2s59nZ3eXRw8/4PhkxLtvv4mSFVHYIQhCgiDEdV1sy0JrkLKiqkrqMqcqC8o8oywCytw430qZeXK+SAHY3t4ijl3arSFpryCKO2gNRVGRLOYURWXQHj9mc+82juezmIwRdhc/6jZOfo7CQtgeQatPWeSU6SmWBWE8ZFUrfVU/vPD5ZZd02YvWpMWaPne5lONszF44yblvL37+4dsVEblLtef6rC0fVQx5PnrH+WCDXmtcc45VDvO6VXm5BOZHt/8qzPkn2T76XFeFDM89Ra1QdYFlu5TJjCJNiHqb2EEbbfsMbryMsGxsyz2LUaziGUsOiKV8XhPEW3e49fq8c6HVYm2mvTTRXWj8arfzQY2lNNy5Q5q5D9YI2tZn1wvnWm+QEIIwjNjc2ODR4yf0+n1TGlYWjE7HbG/tkKQJnW6HIi+hCxubW2xv73B4eMDTp/sURUmr1cK2bfLMqMV0ul18z6eqKvqDAZZlZE0fP35Mv98nCANCPwAFo9MRViMhHPgBWZoi0PiebwhyleLa7h6z6RTfM6WI49EI1RBgjkenuK7P5sYW/f6Ak+Mjw3oeR0bNpSM52D+gKHIiJ+L4+JgsNZJicbtFVRWMJxVhGLJYGMc4SRfYjkW32zfp7/0+eZGjlSbPM6azGb7vM9zYJI5NSrtUxm6Yz+fUVUW71SYT0BkMDFmnrM2cbNsEUYiSiuPjE7a3t7jz3G1GpyOm8xmWBQcH+8yTOXu7e+R5xmg0wvM8bt2+je96jMcTQFNJSSfs8eqrnyaKYx48uI/juJwcn7C5tcX27i73791Ha83uzhZZXrD/9OmV5XFaaWop+eY3/wzXdQjDwGSDeB5bW+bZbm5v8vZbP2RrewvbspBSMZ5M2NzYYNDrM5qMyPOcdqcNQmDbjinjKEoGwyE3bt5iOpkQhjHXr1/DdRym0ylhGNDt9XBdl6PjE+I0NfZbFHHjxk0jPfwxppGPPecs5399for96Ww/vdDjs7Oaez5FUeDZNjgOdWnqKCzbQtWSsixAGK1dAN/zkarGtTyKPDuHRAlhNxEzw4hcN5GvuN3Caox+Y0gbuS6TZlpjux51nSKlpDfYoMoSbLXFwneZHzyldAJTo1I5eMKlqiLSYoqlFJ6qsLRmVvr81Z7FTuDzicBCLjSlLChRPH7tdb72AyNEH7maaOsaw0//KsIPsT/1fb79f/u/cv/3/yXf/MbX8LyQo8ePqbISD8HO1ga7P/MzPPjzPya3HCxH0b19i5/97/4HPLtm/x//I6oDjV0BQpN2PCZpSdsBa6FxtKnVBvBsG2nB/UrSFxACTwR4wEAJbA1dYVhoPTSFNs52TwimWjNBM0fgAQEmrX3ZB6coWhjke6AE9x24uz/md6RFkRWmbr/UHD++T12WvH/ve7zwmS9Q5Au++/U/5md+5RcpypI8y1CWzSypGB1P0HVlJm3H/P6W1Rg4zeKmhWXYpZVh/JaPvkWLfTr9Dl7gYXkCRwNaomQF0iyI1A2BThMqVkqZWhNtCLu0kiBsE7xpJiFZF8YQl3Wz4NpnCJmq0Uo15ylBC7NfgzKYyDfI2mR0aAXacpGDHXJlUrjiVou6qrCEZTTqhaAoClw/wHNdgiBEw4oszpaS4XCTojKLmW27WLaDX9VUVWmIvRwH3w/QaMLQZy+c8+ndEY7Q5GlJlmQNc7mHtl1EswCURc79/Ql/9u6EeWFk7lzbosgzLBHS7fXpb2zTG27gOD6dXo9Wu2vq6S2DiFdliZQKPwzodk1aqtHirnAcGz8IQOtVxolxUE3Wi0mPFyhl9MKDIMDzTD1/XZtasKIoTKo9wjjrVYnnutRVxbRxrKWUhhAtChtyFxvHsRkMhiit8Dy/YYm3SJOUqjLSc3VZGqehUUhwPQ8hrJVzHkYRYBbZuNUyGuBKNXOSwnYNsWMYBj9RjfdlY33NUGwkj9b3XO6z/vK8QWWtIsnnDOx1pKuprT4fiV0ayWvbJUPwwoq0uo44VxZ+ftFbq+FeP1lj2J8zTlfXOEOB9Kpp5x0arQ3zuNa6Kd8wwV2NXum564ZYbHkvK/sUffY/oUygFklFRalLai1Njb+uKFRJKlMWcsG0mvNBss8Ps4ckTsXNdp9ftW/gppLRYkTbj8CCSldkquKNUc2fHJkyo3nukueKunbQBCg0duSi5g7adcEz84EQNTguCAdqhY6aWdg2SDe+i+54WG4FukZ5IcKDlmfRWoyYdiMYDommE0NEWdfIAoT2yPOUe6+9xvXtbQ5GOxydjFBa8Bu/+Z8zHAwYnZ7yZ3/6p3iui6pLfutv/R00cHhwyA9ef5PHD+8j64rNzQ08LyAII6IgbJRKXIQGqQxfi6xKqiqnLDLKPKPIUvI8NWnlDSdDWdWcjiZY1hDXsWjFHWopcZTN9GSfg6dPuPPJz1NVGQgbVVVkaobSIGyPdD7BcQO05ZOODxhs3wRd4wcBlpDMj94ji0ZEnS28oGPKkdZ7obgsLXcWAjI1sFfExc52F5d69eoMl4JO637zaof1ANf5486dQ3DOqdbrn687w1c5Xcs7EksH2Oy92n/9xRUG51VG6KVAxerellHDi8GBv6ztcuNWt3Qxktc8UKVqZJlSzE8oFmOUFqiqxAk1ftzC8fyGId9hOXctCV1pAnRnDrY56aX5X3PuGaw73StE+lzg9Cpn/Pyjs9Y+0+vfXuhL5ya8ZTxmdSK1/oRYoYjC4tatO2gtePT4kckmtV26vZCyKsnyAiEchKia9bZNkiTYtgmqR2HA4eEhg36fR48eMdgYMghNKvVg0KeWNQ8ePMS1bdrttlE3KQuOj09wXZf5Yk5VViwWc6qyZGNzm+PjI8rSlKgIBEf1IcNB32QRZikaTVWVeK5nCFeVZjIdE8cxjx+ZuvTDoyN63R51VdHrdYni0NybY3M6OqUVxwS+z8bmlkG4BwMcx6PdabP/9CmHB4cMBgMcO6AuS8MfZLukeY5ju+RFyWw+ZzgckhaGMNdxHTzXo9PuMJ3NsCybWmryomwkUF02NjYRmBJG13WwbYv5bEaW50aZxbLoD/pc27vGoN/n4ePHFIUhvLQsi0WT7h6FEePJhMPDAzrtDienp+R5wfZWl62tTZRUPHn0iDA0tpWwLF584QWiKOLR4yfkeX5p/NiWzXAwYHtni8ePnoAwGRCObROGPrPpjO1G6cKyBFLV7Ozs0G530MBsOmNjOMTwIJVMplMWiwXDjSGWZTHo9YnjeMXNk8znHB0fMRgMuHHzBmliAi37T5/iBT6u6zIcbtBpd360K62vHkcfvv+F98vp8cJH53a9Iq76424Xp6hn2Z7Z8V7MZ9i2Q57nuG6D6lUaIa1V9Cedz8nSlN5ggGWphvygxPcMil2VBcpxG0fF1ItGUdSkrCtsy8L2fVQtMb64IU9wHdNMS1gI2yYOe6bxfkjUHRK2e8TtFtOnD1GTCXaqsVKLKHDYUCEOGrvIoCzYKws+ERjGZEqN1gmR41OLitdPCp4uFC3PYsfS5GlBUUmUrtBehLQtkqwiHU14/hNbvPIrv8Z8OmN8MuILf+2v8cpXfoYH+0949Bd/ga0E7eu3KaqUp3/yR+T37qMF5BbUkQ+xR23DZFySWIJKQKU1rmUR9FzeTCqOtcUjrelr6GnILcFjBywNgQbXBstykAJqBbqWtAUMNKSA5TgIx8a2LZNOXUt0WZEDthBk2mJLaiLLpiorxkdPmacLTu9+wBd+9ivcffP7LCYzjp88JIpjhsMuTx88JJ9PefzBPaLIY/9khi5SikVMr9PCde1G271JMWzYxbUQTfaCoqo1eTVm9OR9dq/vEUSBaWtDMqaVMqmmSjVEJaZqWC+lgRqpOaU0QstVGqLGLEmqXrJoS5RuRmGzqKm6MsR/WqGVRFhO43gbmSDLEsY4VJVxKGuJthzeu/ctZqUgCGOGW9sIBBtb2wY1FhadThfbMURvhsndQklTq6ykQasMiZFoJK9s+oMBQhgnWTRBKV0mPN+f89ygRFeSZFGSpSVlUSO1jXAtVF0j64rFYsFr7x/y/YcplVwuAB5uGOJ5Hn4Q0mp3CeMWrh/S6Q0Mqq9NoCdPE1Mn79hEcWQY520bzzN12OvC1kFoaram0ynu/4+4/3ySZEvT/LDfEa5Dps6St66+fbt7untmsDM7NrOzWMCWKwhBLmELEgRpRoIgzWj8X/iNNBKkGQmSIAGQBsFdYEHs7uzsaNXT6upbt0RmVsqQrt3P4YfjERmZVbf7dvcM4GZVFeURLsLDz/H3ed/nfR4t6MceMTVWWIQXU0pNVTuqf5Fljs0iJW3VXQNj8X0PhAPoxhjKPMOYFqUigiDsROW0+w2t6azqFKZ2v3nT1DS1C3r8wCXZCCNHX/U0eZ7TmBbbGMI4IorirhWhYHJ5SZYuCIIIpCSMYyaXl+i2RXVJviAMf4qp8+byMtwVN2fiLpBdheh2Y6uXd7b54HlVCvf6/ZtdqS+D//XuVoH5Gj3fevKsKydf/g3FJujn5rm88r0bBSDbVVIbms7mTXWezE1bk1UZ82JO2qS01qKFR6gDfO3Gl7ASrKA2NY2pyeuCylS0xlA1JVaA52kCqYmDhApLanMWJmVZp8zKJWfpBZ/PjzlqJ3hRxP5owPtixNfFfZpFyqWZMuj1qUVL3takDfzwEn7n3EdIn9NMQi0oWg2tBnxU3KMtLVYqbCQh8BGtBWsQVmIDC7YE0UKkkMa4idsWiDgEW6CqklYnhHXJyBNso9iaT6nHWwylRGBp6wpTFJR5Ru5r8jTl+PiIP/2uTz0542B/h+Pnf8ybbz5C2Jb/6h/+A6Kkz/vvf53PHz9mNl/wwx9+j49/+H2kEhwe3nH2gXFCHCXEcYznO6aKo5u7xF7dNNR1RFVF1EGG34keFnlKVVVOt0U4euLVdM54PMDvb5Evl/QHd1BeSFmUnB0/p6kLvv1Xfp2zpx9RlRnxYEw+Pae2EmFhMbvi9fd/lTDuUxczrJDYtgQpaMsZZ0+OGe2/wWB8r3O9eOkW3bjvVze9vc5N3cKkL4Hq1db2lbEbt8e0e+xc3/OrxO+GtuCrE2C3WkyuN7pZVb4dG17PMdfj/HpKuI3mb32xzanm9jQB3TW69eVesZv/JpfNaevmdbSsLOWadIYfxpTpgmx2hvIDKuVTFzmel2Bsu2Zjme65sy4GmGtQe21DtwGo5YpFcW1nt0n7/tIe+M0b4BVY/gZ1ffV6xdxZIWwhbsybL7s6mNt7BQRBGHHn7l2mswlPvnjC86NjPO2xu7dDFDk1cyGcs0kQ+ghcm50b1x5eV4ne2d1BSElWZEwnM6QUjLe2VkfH932Wy7SrckYd08yyu+dEEU+OT6jrhjt375ClGReXlwwGfc7Pz0l6MUpK57t9dsHW9hjfD7G2paVlMZ0xHI3Y2trm/OIMrTU72zvO/cT33bzTtERRTBzHrse6KBgNR7RNy3Qy4fz8gqoqefjaQ7JslyLP6fX6XdLGYGy9thJVSjHoD7i8vCRJEpazyboNLUuX1GXJeHubs7Nzzs7OmE4n+H7A7t4+aZoBlt3dHWbzBWEQILC8OD0liSP29w94+PChq063Bl9rZ79bVJRVTZqmLJcLDg4OmEynhJ3mlcAV1pq6RirFaDRisVg49fHzcw4P7nCwv0/SS5jNZuRZTtO0DmAHAYPhgNFwwIsXp5RVxd2793j/a1+jbVuUlGAs0ve5uLikbRt6ScLV1YSicOK+n332KQ8fPERIyWAwYD6fU5QFn336GcPxCClga2sHT3trxoJAkGU5T5884/DwgMViiVCKFyenXWxv+cVvf4de0rs9wv8bWf5ij/TznftXBt5hnLiL3NkVaF9T5iVKSLwoBAt+J6AGbniug7Gux9sPnHiUaQ3WGnq9PkpJtPZcoC2cGnLT1EiuJz5jFX4QYI0hiJwPYdvUzs9XKpQfEo0FXhRTp3OmJ0eUsyusrVlMZ4g8x1aOpmasxQpNni9pTEMrKwwFgYazvOFvvT7kcBARe5IPf+938T8/Zm40n3z8IdO8ogF+8a/+On/vf/a/RHmaP/ztf8x//f/9B/zub/9j/uz3/xnPv3iC8TWxElx++Mf89gd/SFiW+LYlGiT0AklgLeW8oDIW0dMkCVxkDSGKtDV8WFRUvrMqy1s4s4JLCztK4AeaUipmdY2xsLMzJEwiFJL5ImN6tcDPKuJhxLknydsWiyHREi08TOmeNInnkYQRNqsR0ylPfvTHlIsTbFXzxfMvePcb72KMIc1zZpMpUeTx+NMveP/b71Mbywc//IhvfftdfCWZzJZEocYY8D2N1sqBW8BaSdu0LlMvFViL9jzCOGJyVfH06XEnHqE6z1a5jktWPsxYkNKB8hUWNIDo6GJSqm4siG5idQ9FRyUTa/YeCEepxmW6m9aArahbS1kW1MZSVjV158NdVq53WPo9LpoMpMfh3fv0BkO2d3YAV9UW3aGX8xnOa1yttQ/KskEKgR/4zve+bVy1t2lBOGXwMnCskEQVfOPugkRl1HnNMi3IspbWCsrS4kU+bWsp84yr6YI/+PCCz88LrIU4iWjqrmdKawbjLfrDLcIoIe73iZP+OpNYZDnxdkIyGLix3XloVkWBHzjwGQQ+nhTEoSKQOSY9p28bkl7E3lbIzlChPB+pAkCzLAzHE8vZ0lAUFXXtKF+iE0JDSFrTopWmrWvX357EhDZ2YoxqJX7WUmS5Y9J0wUbY+Y7Tts52TgqqsnQJJQRxnNDUNUm/h+/51FW9rp6GUUgbuAq8EE64TytF07b0h0NWgnpSOF2Jn3kRfKnYkl3dm5sfvv16HVzeCOW7/4uXKy982bQvbr1/U635NmB2p/Zq79zrxPMqgNz0t305Nt8E3DcrcZaqrZmXKZfLS+bFnKx2NnK+8kjCmFE0ZNTfZk/t07Q102LCi/kpF/kl03pOaWuUUGip8aVPoEIUCiXcvKGEZlZn5G1BsSxJ25JplXKazzgtp8zqBZVp2R6NeJDcZZ8+73p3uW93KPKMSVCQBAMWTcaizrkqSz66bPijy4isjvC1oCw0thJgJTQS3RvSmgSjBSIy4EvQHrZqEK3BRh54FRQFQitsL8A2DQwCZAXWawmthFohrESIALV3n63FFaHvoTxFf2/XAe+mpq0qijwjS1MWiznL+YxPPvqA409+yL39Xf7qr/wyz49foJXk7/7r/33msxnHx0f8+fe+xw/+7I+ZzyZ4vmZntEe/P1j/6SX9jqkSoKRCSAHWBZ5N4yjnVRVQeW4O051AUFm4vkzTWYM2TcNikdPr9wn7McYKgjhh//AuT774nF/5a/8dlvMriiKnLDLqxtCUGU1r+Mav/E0O2gqpBWU2Q2ofgaWpK5p8SV0s8aM+6eVTpBD0Rndcm5G9vvNeHg8ba1+6wV8Bmrp7VawHjH3547fG+Wbiy42jW9u8Ejm+atUtkCY29rxRARKrWulG5XwVZtnVg+gV387eyAKIG/MNiI354cvml/8WkPftw24mLpCYtsZP+mjr4kkhXRuZLTInDmga18rYMdzMmjlzPUHd9ISXHVNv40839zoZE3nrKogbc9xLicwNitBGruWl5VU95ZvrzLpKf6PszUrXYpVgWr0VxRHvvvsubdswm884OTklyzK+/e1vr8+rbVvapiXpJYRhxM6OJssykqTnhJKtJi8KmtqJ2WZpxt7eHvfu3efk+Jgsy52dZ1XhacdQreuaMAi5/+A+QgrHmqsbtnd2uJpMuLy8RGvFYuGqrRboDfquJUBLBoMRWZpiDGgtiZOIA33QVcQ9ZL+P6sTVoijC0x77e/uk6RIpFE1rqMqKyWTCSqg3jmOiOOL05AXWWsZbW0yuJk4AbWuL+Wzu3HZwgpxVVbO7d8DF2anrQ68qyqok8H2G4zFl6fQnsjTj2bNnJEmPOI7o9x9Q1w2z2Yy2Ndy5e4ft8RZ5mfPpJ5+QZRme59Pv97m6vGJ7Z4embUiSmLt37zKbzwiCgGF/yGg8ZjFf8OLFKdHdQx48eOCqztMJw8GQZ0+fcnR85LzXo4jQDzqtmuv5SCrF1dWU07NztNK89ug14q4Q4QcBs/kCKSXb21sY4/rqjXGx+aDfZ3dvnyiO6ff7pGnGbDYlDmOUUGxvb3Pn8JA0TdnZ2aWuK06OcRpIYUhVFWtxUyUU9+/fJ88LlsuUL774gnffedclal4ZwfwM88xmItHy0lD80m1eWlaprlfPoV+2059lZvzqquZC0Ta1C4abToiiy37VdUNdlh1wEmtauaO6uAssOhqj6foP2rbFWqgq1/u9+gZCSFeVsq5SVleuYt40DUW2xPMDlHI+yUXTID3X62qERPgxNIbB4X2WUkBTY6VGNyX1ckZTVORzg2wbRBQz0KBEhRYhnqm5u5UQvv415KNvsrg45dmf/j4fffcPOa8EV1lJZQVIwQ+++yf0/z//L15/712++yd/wvMnnxMqxW/+jb/JX//v/j3++J/+1/yz3/pn6GXFUAuGouVe6DHQliotmeYVNYKhkEyF5TQ3DIREazjSglxYisYitKAfis5DUJJjSFHEYUDfi7HAi7RkqMALQkwSYH1JWlZMTEtrLXEvQdNSVxV1a7C+IEl6DHaGLLKSYCtipx/x4Xf/lO2dhKLMmUxmXJ4eO4suYcjSFKViKmP44pPHbB/sMbmaUeQpYSiZL2vy0lC3FZ5u0arzO1d6ncUVUq17rbXfEquAaW5oTOfhLQ3a8/F8hfYCUBIrQSgFQuMFAUJ5KO0CP4NAKc+ptksPi0VpH6E0Tds6324EVVVjmprZdIYxLXlekM7nLOZTsrxmscgoqob5cuns7Vrj6KNKMxi4PsWt3i53hiP6wzFBELrAMow6pe6gUwFuiON4TW+u69r1K8cJTVO5e79tuueeJU6cdVhdVVjTcKeX8u7WFGVysrQiTSvS1Nk3GBQyjKiqirIoeXZywZ98Pud00TivWwnpYonSmv5gQNIbEfZ6RMmA/mjs7MLCaF1pDjvVSr+rHLStcarCXbAX+bAVg1g8Ry2PscWCIl9Q5gWXec45knuvv0OxKND9A974hV9mMBgx6Ev2Z0s+P6m5SL21b7rn+1SlY7xIqZwAoxBYK9fVWGsdO8Iad88q6Xq8y9J9Z3AK7VK4jDcdc6CpXeY6CLr1XtCpyXu0puXy/II4SZxyugAlNVIKfN+nahrnj06nlr9K4PwMyxoAiI04rKtgvQrWXv/vuvptX3pn89Wth8Hqut3e6Y0q803l8Ftx/cu9i6/4jwvqr0P0n/Yp05iGRb3kPL3iMpuRtxlSCIIoQCJoTMNJespn0y8wGJCCxIvYDsds9fa4M34AAubVjOPlGaf5OWfVjCq7xNCCdNetsZaiqVhWGVlbUtoG6TnwGnoBe9EuO9GALSJeV3c48EZQtsyXM87zCUtZktVLlk1Jbg0fzCS/dzmkqBOE0RRZg20FWA2txPP62CrAthLhaTAFeBKUQLRACCKpsWWB8EEoC6HvQFsiML5PWGdshYZ5z0clHknTst1WHG6P6AUBnieJpETYFozTP6irgjzPWS6WTCZXTC98Fhcv+OCDD/joRz8gTmLnyJAuENZVWILAJwxdQLW1tc3e3h7bOzuMt7YZDIbEUeLE1TpHhlUl1xqL8RoaP8D3KypfuyDb8x2zxg/wipy6zKlr167TdHOssS5JOhr0Sfoxd+7dI4gj0qsTWuPGvg57BPEAPxoglEdb5zRV3TFlKowpXSuO5yNbn7A3wLaGcnlGkU4Z7b2OFyRfArhv//dLblz7pf95aXvxYz6ymTC7KWi2uZG49ZrNyeLVB14Dq9vf6Xo/rxqXm1hQrM/+poa7ZWOGELe2fWlnr0q0/WUv9kaL+s0zt2BbZxuqQ9qoYPbiCL+/TZRsI6RHW9e01qyfL9Zl5J2bgHCCmquYVXauPSuw7dTEV1fnWj/Abp5Ct1wnalYrNt/g5nq7+ZGO5L46jtiYZ+1qDJp1zCCMuR6XXFe8VxR698cVDTzf580338Ia61xTlIexLUfPjzg5ecGDBw+QSgKC0WhEXddkWc7z5895+OAh2tOuT/fstGOr+bSNcxs6ODjg7PyCXtKjrmvOzs5IkoSt7W3nGCI19+/d58Xp6bp4JgQ0Tc3B4SHn5+cslk5UMQgC2qbh8mJJGIb0BwOqskIoyfHREa+9/ojFfMH5xQU729vUjUvol2VJmi6p64qiKJjOJvT7A5bp0vl/hxHL5YIf/vAZ4/GYLF0SRSG93oiZmlGUJX4YYo2hKAqCICBJEqqyZD6bkRUFi8Wc0PeJ4oid/T3CMOLTTz5iNnVCaS7Gq/G9IX/2p38KOIHauq4JAp8Xp6dcXl2ytbXFcrmk1+u7BMTFZccy8CiLiosLl5DY2dnhxekLtvd2iXsxb7/zFp7ns5gvKIqCumkYjUfs7u7y9PlzWmM4PNh3tq5CdDGUExS9OD/n2fMjrLXs7u3x6LXXKMqSKArJsxwpBOPRiDTNODs/I89z3nzzLRbzOWcvThkNh9C1UBZFQRhGRKGjqmd5zunpKXVdA842NwxDsiwDa8nTnLPTc7bGYwbDIXESs7e7C0BZVhyfHHP/3v2XGCNr2PtlCf1XLJaNqfDG1Hprnr015n7cfr9k87/Q5SsDbyEF2dJ5dgtf0FQVnu+7nsvlcq2OrJR0nm9tQxQnyC5bqCInfiQ6QA04GoUU5FlGEEWI1uJp59tsWmcl5ft+Z2EkHWW2q45bawnjHlhDXRcoz8MLQ8KkjzAt8XgbZSBbXEG+gHyOzaZ49QxVl7TpBFXlyKZCihorwAgFg4Ty4ikXf/ZHZPOMb/3tv8/uW+9zfHLEf/7/+Pe5OD+nTZd89w9/n9PTYz795FOWZcPc1Pyn/8V/yWvf/A6/+jf/FpPJJZ9++ikGS+gZCmH4YFKh2pZICLZ8jw+XNReNZWgFhRacWoNUlggIY49w0MNqhUBRNDWRH5DXNUXdYpRmb2eMN53hez6NNbR163o4hj3OrmZ4nkJIaFoIkwRblUS+Jgx8lJR87a0HJHHE+WTG8ekUIRtU6Dy3p1dTeoMRT754QdXOEVJwcLjHi5NzBruGNM84Ojolin3yTgANIagal+lyCZjrAF8YS9N0NC8hkTvv8OCb38KC8+P2NFHcJwhCyqLAmJYgjDoRPoOSgqpL5Hi+z2I2QUlFWZYIJSmylPl8Slu21HWNmRYsFwuKPKepS/KipOiUM51lmSAe7ZNsa8qi4ADXe90bDIiThKQ3oNfvdbZ2mriXuJ7ETgnT9z1H05aCMOx1fec4ZgaQJD1aYyiKfF3RbVtHx5lPpwxGI/cwKha8t1dyN5lSFznLZUGaVi44lR6mMVhcv3K6zPjg6RXfe7KkFZogCFzPmMRlEIXEC2PCXo+dvUOUDgjCGKk0YRQTdqJtLnHuKgShJ0i8mjDIiXVNL7A06SWTF8/JLo8ol0uSrV2EjiiqitL2ePbRD7h4ccm7v/n3iXcOSU3I/OQZSZIwGmzzrV7IZ8+veHxRd/aCmqTXo24c2PI8dz2auqWqS/IiIwhCB6abFlOVKK0QCPzQpy4r0uUSv3s4WgtRkiAQNF2PvFSud21VCS/y3CU6BLSNc05Y9Qy3remoWRVK+0RRyHI+B+/nmGVvBVX2RjD2igeMWHlS/6Q07c2K9Waf9bWGwjUoeHUI/+p9v3TkG6j+OhXwyt29/LVe+tBFesmH55/ydHpM0ZTEQeTmJCWpi5rWNGjpIRRYbSlrJ5R3tDwmrTLKunSuGFKug97KVtRtzbzNaKxACs2ySBHCkoQxcdjjbjRi6PcobEEse9wJ9hh4PfbVFkPZo2lrZtmEF7MzZuWMUpdgG8I4JKskf3iS86fTHgUxWOkoqbZLIFtB4I9oS+cEgK+gbSGUiDjANi1ogwpANhMa30OGzn1AJNL5PWqFJ6Bf1+xIwVbgMxY1d8cxJjOMkpA7noevJVp1HtDWYo2b26qqIktzZoOE6aDPZNBjcTUgn0+oiyVtsSRQAi09tJYEvmI4GHJwcMDB4V129/fZ3tphMOgTRTG+H15b8HWWYo7OajFGo9sWrRWe51gpXhlQ+j51EOD5PmXhUxUOJNNVyUUJC5y1m6dChL1kObukqmtHl8+WRKMdov4Wg/EO2eyMxdULgjDCDiFLZ9TplHS5IBmMuf/mL7hexNYgPY88W3L+7PvE/V362/dQOrhxS74quHoJ9nbj5WWW9sa4fcU9fiOBtd7vRtll3Ye9AozXY/R2cs0lOV4+65dOWthuEhA3QPfthNpL2/74FevVrz6Da/q8fem9v+zlZtpx/VttrJVeiPJDTFtTZXOEtDT5HFOXrkhUFl28gVPVFp3milSuyqqUi0+l6Jh2jkH1yrzNxutVouOnfVrc5EOI9U9587671tsAnJApuGeXcuDKCoswwgFuLEj3PF8BcJczEMRxzDvvvsvW9g7Pnj6ltYYoijk83CeMnI1UWV5XrJVWLJcLvnjyBd/4xjfp9Xr4gc/l5QVN05BmTl+pqisW8wWe9uj3+x04y9g/OCQInGBYVdUkcczV5RVXl1dEYcTV1RWTyRRPO2cR0+n9jLe2WC7mLBdLkjhBa0W6SNHaoypr7hze4ej4hLMzZ4mWJAlbW1vkeUFZFbStoWmcbeTDh69xfHxMXZUo6dxlkqTHxcWFs09TKxcasb4fpJRrVXVrDHv7+9RN7WzpwhDfurbHi7Nzdnf3mM0XvPXWO5RlhTEtW9tbnF+cY4zh8urStRBKyXA8ci2HXZX56uqK1rR4WhOGIYPBgDzLuHh8we7ODgJ48OAB2tdcXlxylaZY4ODwED/wicKIR6+/jud5LNOU58+PmFxN2dnZ7uIiS5plTCYT5rMZBsvOzi5f/8bXieOYvb09TGtIkh5VU5HnBVEccnhwSK/ntj8/O2MynXJ45w79IODps2fEcYzvud7y8dYWYSeUF3QuOBcXF3hak3bnO94a8+L0jNa0XE0mjIZDyrJke3ubKIo4PzsnCAL2dveu2XY/tkr9Y9581aT0U378v43lqwNvIE5iV6Uui2tqaNvgh87vV3uuz0MIOmVEB9SlFKim7fpyXaZOKomvApqqcrRS064f/Kb73IpuE8UJVVkglaO/KCWdIFTTuL6VFrwgcte6LvEDH0nsApBQI4TvPJt1gWxKhM3Q2uDJgDpvyDNDnRcURUn99JRl4cDtYGuf/QevEfdj3h68xxvvfp3zs3/Cozff5N/+d/9dgjjiap7y7/3v/g98/KMfUpQl//yf/GP+h//Wv8m/9Hf+Dn/16pTlyVNknfGDHz7m+dUxCYb7geJF1bIwhrEQhMALaxloaIXFKo1KQpCSojYYYZzit9bsDodUrXEq7OMxu6MRz87OyBYFWmu2Rn20VDRN7fofy4qyaZHAIArpxRFvP7pPmERcXE74/OkLwuGYWZ7zWhizSEs+f3LGOPHZ2howT3OWWUYYeZiOyfDFF0fUjbNnytIcrVyPuBVgraAWAqRACWdL4IIb6fx0rVNznywbsmdPkVKhvE5sLC8Jg4gsWyKFIIp7jvJd5tRlQVNXWGPxgpAiT6nL0gEtrcjSJYvZHHCV0bJwVFblBRjhE/Vjxvt3KfKCOI5Rnkd/MCAIQlRXoY/iBO35JL0e2vOxbbvuh4rixPlOdlZ5vaRHa5quf8xl8jxPd9Zc1qlue13/OFDmBUmvRxhGSCUwraHnt3zrYUVPXFLnFYtFQZZW1I2hNVAUDjjWVcV0nvPdzy/59KzEClelVVK6RBWCIIqJkwFhr0/cGxDGfVeNt5B0SqVaud57X1sGMsVbfohentIbjQn6e5TWZ35+wfziiHz6grNPv4/QCb/wN/51dNDj0+//KRdnLxjff5vp5QyvuaK6mLIoTlmcPWPn0ft8/Me/y9u/9Bu8+WCbXnjGp2c+RodOKKlyIi5t4yr1xrQEQUjS6yOEEyZp2gbTNB1Qb6izDKW0s1bresCjZCXgJ7uEgwtB1w9UJfG7CkBdOxrsqke8KArCKEZphTHd3KQ0SnudnsDPtkhu0zXtNQK/XSqzIOxmHVxsVEeuI7FVNWRd+RHX0d4qwELIdXAM18+bVfBmuySYW9dBqjXadoH1dcVsE4pvnnK3/ep73HiobYbsK8jGWrQo0B59PyBUimWTOnvExiKspGwqClPSSENra1rbOiaVFChfECn3fGlqgVEGpaWrhgd3uRvdoTGGD4pPqRUM/IRI+/hCE6DZ0SNeix/SF10/mRGONt3UpNmc5+fPmTVTbNigtMQIj6NFzu+f13xY7JIJHystKIMzcpRgIfATTOVsDfEkhBq5zLFxiPF9pF2gkwola6KiRIaCaWDRQYA0GY3y0NbymqpJQs29XkAlBHsevK4lyf4YIwWR5zHyNb6UqC6QpmOF1LXracxHPRbbI5b7WyxnhyxnV6SzK6p8CW3dCelE9AcDtrd31lXu0WhEL+mthRCdmrlaV/+6G8wB7y6gV61y1jbac+Db86n8AO27ynfpO/G1uippW0PbtJRU7vmdJAwOHlFmC5L+iIujJ8znc9pnTzn8tXewpmV6cUK+nGKlJj95jO9HlEXFnUffcLGF9imKEq0VTTaDOqdezriYvaCuFgx33yAIe9C5L4iX7s3rf27fvjdgbHedr5nmXV1YXA/NG3sVN0bINZJfV5tvnsnNcb85blbHv30c8Yr83aaC+/UYfnWl6GY27lrv4eb539jv+gP2GgDaHxsZ/6UtduNii43vb5F40Zj0/AltscQPeyghKIuKOr2iKeZo5QQNldAoqVw7m5Lr11JKpx8hJBuX/vp414d79bm96u2fcIlWt8bm1bwJ9lcib9epnFW29YbYZZcgw1qk6VrvhFlT5KWUGCnRvs94a0zbtpydnbtnaZyAtSyXS84+/5z5bMabb73FYDDkzTff5vz8guVyQa/fw5iW4WC4bgPN87yrDsfd89vZq0Zdu5r2XC+y1o5Z1u/3XYuZ1ty/94A0S2nq2rmxFM6HvaoqgjDi9MULPN9bF20GwyFFXnB6fobvac7PZgRh0NmiDUjTjPnceY97vkdRlSyXS4qiQGDZ3t4hihMWizk7OzskSY8oigits7ZyLifOqvDo6LmzFpOK2XwGAobDEUWRcbB/gDGW2WKGbQ2e9siLnO2tbSaTKZPJhDRNOTg4wPd9tre2qaqa6dURg9GQMAwpy5I4iTk7PcWGIcdHzxHcYzafI3AxWpbnNKenxL0eV5MJk6srDg8PuXv3DifHJyyWC6rSWbW9+cYbXFxccHR0xLPnz9f3ygpfad9ne3ubR48e4Xse09mU6WTC1772NYTSZFnK82dPGY7GCKDX63N8fIwVzq61aVr+7M/+lBfHL3jvvXfZPzzA2pY8z3n27Bl37tzB83xenDhr4cVyyTJNaZqGy8srhqMhAvj0k0/5IEu5c3jIeLzF1dUVu7s7nByfMOgPiMKoy152zMCfCMBvDrL19NBlsL5MBf1Vidgfu3zVqe4rFSFeXr4y8M6ypesB007tz1gnnJanGVGSAAJjrFML3hCMSJcLhHD0ZtV55jnKbEGUOLr0SiilruuODuQyj0rrTpCnxQJZlnV+zz5eZ/3TWgMSlPZcgKIlTZmDqdFKIHwPdIynDjFLDZmGPKCag8lT6gas9qCn8ZIErSASMGwFRVFx9E/+70ipmecl2eNj9kPBdj+mmhzjq12GHrz/zhucfPYBEvj8u7/Lv3/5zAnQTC4J25L3vv4uv/pXv4NML8kzJ6oT25ZaWXTtVMmlMZRhgNEuaWE9HxAEoY/SmrKsKfIChSAMfQ729vD8gOOzM4y1jAZ9J7hhDPM8x/cD6qbFU4qhgNjX9PoJw0GfSZpxdXJGlhdIqUjtjKrOuLqcMF3k1HXJxdWEOImIowCswVpBmlcY4TKtizQnS1PSrEBbwbJY2cZdP9AloJUL5lprce3/ApRm+eSI3JwSxrETyvJdb7HqPLzrqkR27Ql+GFFXJUWe0dbOykEq15KwsqGyCIbbeyjlgYSR0t1YkAy33OQqpKA/cBOCHzilRd1dZ9M2nXd1idZOmbzMM+Kkhx+E7l4UgAFb5k5VWynKqqSunMKmo+YUKK0JwgAQ9Pp9lJBUdUWRFwShq1IfDmoe9i7RzZwyK1kuc9K0oq4NVVEhtQIsRV7w7HTCd59kXOUClAetYblcEEURMooYjrfZ3jtEap/xzi5Kuwp3rz/CCkFT19i2wW9n7OsZ8uIj5iePUf0t2qTHYjbFBnuk81OuTj5FKs3r3/xlqlaymBfUdYvQDffefIcqnxIHfd58bRvyF/hhiMpLRoMAqjnnp6c8/ff/t/zGv/G/YH//AE+f8fQyI6slytN4UpGbDAFopZ0nqFJ4vsYPAlSrqIXoVEs9dOzUzz3Pc9SuuiZdLoniuEvkmM6D2+IFHkXnGe55/obPe4UfBkRJQhC6+3Q2m2Lb1tmcCYH2/bUjw8++dEE6G2HTDdC9CWBvz9WrYPcaqN8git9qIL9B09oE7Zvh44Ygz3XH6E0BtZcEgtZc+ZdO8MetXO/XtZo5sL8TbzMI+xwMdrjILljUKZWpKOuKos6RAkLho5SPoaVsC/pej+1wG4kgMymVrYh1wjjYYssf09N9pJB8vPicj7JP2R9sE6oQH4+xGLAtR4zVEE962MZR3Td9vbNqyZOzL1jIOWqoMEpQtoaj2ZJ/fqH5pNzlyviOyCkNQrnrSGsJVAR1RG2so457Eqtc9bqMQpSs2S6OyKKE1rb024Y8VMRBS2QqlsmAQd2gpOWhddWG/cgn9hWpkISJz0EUMfA1RkqUkvR8jRagcPOpMS6BXTchVZ2wVfapijFFXlDlGVWZ01YFwho8pfB8jyiKSHpJ148YO09VP8DzNFrpLjne0WyvcfeauroSPm2VolUapTzHPNKubURrD+X5XYUqpyrLTgDTOP0EWaLiCOF5NNaSjHdorWC8d48qm3Py7DFJv+eYKMsF8WiHxfSSh+9+G2NbpPap6xzPV4BCegnl4govCAi9gDafcnH0A3rjhwy2DhBCXd+PHSh1ibGVNNl17fFGvmkjsXQ9ojags9hIgNEl1zbGm+hAsoAbSbgbUP02gLoxoq4nhs19X4P6VebgxgmDEMiXquqbB7g9xm8f93a0u/qAXW/+5XaGf3nL5vcQt1cg0NEWOhpRLafowDG7pFfRCkk1PXKtaeEQqdxYcve5Xlc5pVA3fmtuH+9LlhuJl59qub3XVxx8FTzZlaMD1/E0qzF5y1pxczs6IdruPSUlQRCyt7eH53vUdcXx8QlK1YzGY5qmoald7HN8fMxisWQ2mzKfTQjCANMa5vM5W1tO4Xpra4vJdIqxhovzc7a2trhz55DpdMbOzg7nFxfs7e9xdnpGGIZcXV0xGrqYy/M0Ozs7nBwf0xv0GYyGXJxfIATEUcTWlhN/VUpxdnqKUoq9vX2Ojp4ThiFbW9sI4fq2p9MpCEej7/X6znWpyB0zx9csFwu0r0mzlDiKaK1lsVgglWIxn3Pnzl3K3LXthFGwTkBmWc44HjlrZGVZLBsevvaQ8/NzmqYmjhLKumZyNcHTHjvb25yfn6O0oq6drWkUR4iyIpeCIi9IEsfMpSuC9Hs9iqIkyzKGwyFhFBIEAU3T8viLx/QGA8IwZDweM97a4tOPP+Xps6fMZzPiOObhwwcY7Jp9mWVOFysIQ6qyJEkShJBs72zz2muvMRyOGI+HlFUnRlqmXJxfIJVmPpsxGrnnR55nGAN13XB0fIQ1hoevPaSuG5RQFHnpHG+UpG0bFvM5V5Mr8rwgS1PysmA4GtNLEvIsY3dvl739Pdq6WdvUXU0mVHVFEicET77gzTfeRGsP+2NnmB8zGm9PW/ZVb3zJR36a47y01c83H35l4L1SVTTG4AUBWZqCEI6C2xrqpqGyjh4uBHh+gO95a29cPwgRdGrJgN8NamMtUsjOZ04ThKGzIlG6o6Q77zghJFEcuqwUoD2PPF2irCKMYjBOpdqaFi0FLYa2WCCrOc3yElkvoVwiTQO+xd8eovUOsXRAsFU+3uAAL95i+fi7XHzyfXRZkqYpZV7gjffYH8Qo22DPH/MH/8V/zvj+Q66uJnzw599nJ1IuK2xaro6foIBESRphefH5Z8xPntPrRVghWNato2YnGisERVajaJFakLbgK6eILZTAViVVXdMi6IUhSegGdlGWPD4+Y1kU+J7G1iVVa6jbBowA2zKIAiIvwPckyk/oDxKeX1xQVg1aghd4aOEj4z5X53OKRuDHMVrNWaY5F1dTJssC2pq9A1imOXmaIT2Pve0BbV1T+NvkpoeULtg2Bpq2oa1bqrIDkNa4XmtjkVoj/RhbTtFaM5tOOkqj2155HsPxtgvmtML3AqqmIQwieoNx1/dbMNre7e4x31nZWRhtb9M0TnzLUcmccJ8Ugt5ggO/7hGFE2zrBs1X/cVlWaC+hqkriJKGuK8rCecorT6N9rxMjabACojCmNa1TB/c9tOfhadfT3DY1QgiiMOoCvZC6qjDWEgQW6oy3tzJ2/TNMkbPIlhS5Ic0ajJE01jo1+Lpiucz4/EXGD09qloWiqgrKInf0pyCgPxigPR8/Shhs7eJ5Duz7fuh6ukyL5wUEfs2BP8M//zNePP6MXPUYDXocPfuCwkje/dW/y3x6wYvPfsDk4pJf+dt/j8FoxLf/2h5FkZKnE6bnz2nyHC0lVng0RoDxaCsIaPAjhayW9Hsep5+ccPzpj6jzBTv33iTwpxxdzLmqelRde4D1faQQSKUoipKicD1Pnheu1VfLzofUeZ0LPK1omoYgjLr+KqcZUFpH5RI49kRTO3E13/fx13OXq3gK4f41rSFOEizOOUEI1/f9My8ChH1FYHXrtdhYfR0wcQ14N8ssm9j99kT/iifIS4+u7mDi+mjr/9v1NjfPdFOs6gaG5xXLrXNYJ9023vCkz93eXfaTffK6YFHNSauMoi4o24plnWEw9II+A29IUWZc5uegJHfDQyKZoIR2fZkNXNVXPM9fUNuad/yHxDbCazykEUjr2hNmzNw5dNfOWovB0pqG59MnzP05IpLUtBRtwUVa8wfzPidGkwqNkS6/5ZwP3PfRhAgxpASQLT2xZKl3nWWgr1GB4bX8Gb2w5UoIlDUM/AED7bG0lhf9bXaoecScQMckOubA14xCj8MkIQw0udTk2mMcaMaeh1QCXyk8KRwlFgDTWSWabr5xln1N3dDUFW1bY9oWgUVJl7wOPJ/A9/E9D6/7s2p/WQFuudnTugKsFmzX1mWkRBnlwLdUnRuCdnOf5+Fpn1J7lF1vaFm4vu8V+0cIgY1ipA2Itw6dG4TnU2RLhK0xBpZZyW5vi3xywujgDYqyolxe4gUBhRcRxX3aek46OUHqgOHoHkZCU2RIqUmvntCWCwa7D/H82N2JQrhxuaZqvzw+rl9vEsbF5hC8Hjs3clwbyaxb40jcHhs3xoV9aYsvq+XYzbUb5/uycsTLoHsF21+1/sbp8GVNJRt7/wvE3T+5dr4SFnv1NsYaLAbphQTjezTZhCqbo4PYVf/LkmJyhG0qdH8XtfsGQvqdjadCCIl8Kflw6wQ33/wqVa2vErP/uGOuP3oNsm//f5UIsxbn/LIC4avXBsfAW/2xrq3KtC5WieOYu/fu0LYtF+cXTKcT9vb32NnZIcudx3RZFezt7VKWBZ9+9Cl3793F83zSNHXgsBME8zzthJV9r7MarqibBmsNl+cX7O7tsFxkxFGE9jTL5ZLBYEiv10Mr1Sl4h2zv7mCathMf8xgOBhRFwc6OO4cgDLDWEATO6rBtGvLc9WQXRUm/P0AIQRD4XF1eMOgPSOKeU87unvtR59jg+z5Hz57TH7jWQK0Vk+mE7a0tej1HRw8DF2uEnVtKnuVUdU2cJGAF4/GY+WKB53ld/OjRH/TJ8gyEpMgLPv74E8ZbY+rGOfq0bct8PqffH3BwcNgB/Mx5lPd7HCaHTGczzs7P6PX7DPoDyqpke3cXrOXx48/ZPzhgPB6hpKI/HDIcDMnGKePxiDTL3D1iDIvlku2uur+/v+d69/PUsYCV5uTkmLpu2N3dI80zlos5p6cv+OijD1HaI8tz7t27y1YwZmd7m6qquDi/pCxdkc7CukCxWCwYDkcc3on56MOP2D88YGd7m+l0ShwFnJy8IIkTF6+Ph4B1FfPnT3lw7wFKK0ajMXu7e048+SeOj58wADfG2Zd98qdPlv3lLF9dXE0qF+y2LbWn1z7bbTrl4s/+MbK3w/DBO3iDHZbLBWGUdL2XCtP5cVd1RTZfoHyPMOwogAIn7qQUGEuVO/GdslgirUUKsG2DsDWiVARdxthWkCjnAy5bQAhMtcTmU6p8jikWNOkVolxiqxShFRKLUMqJOhmnkKylwjQ1Nl+SXRxTLjLSizPy6ZTlbEZVgQx8sukpWbokEBbRlFw9f8rnj7/A9z36vuJg7zUevvUep08+ZvH8CbZtSbQgUBLZVKTTnBbZ9Ssa8rqhbFyCQXiSsjZM8xapXe46CQOsdT7VgRD0opA7+7ss8pLxzg7HF1cUTYNSmnlW0rQtYNgZ9Ag9jW1dAmJ33Ge+LMnbivOTlPl8QVFWjAcxb772OtlkxmXmeosn0zlJFCC169WeLjKu5gWJL7i8mjmlQiGYTrN1NeVFCrWtOtV5l6XNc2czk+WFC+ig82oXeB6QzjuacKdm3lXR/SCk5/nkecFoq0d/tEUcxwRhRBiGzoqruQbwnu9U8kdb267fWbmA0Hk41wS+T9IpYZpO0K8si05gzD0QEAKlXY+414n8xbGjWGEtTVPjaWe7UXQ0Ky+KaIuCunR0du35WAFaK8oipzXOP9hTulM3d8FtJDLe3c3pyRnFcsFitiTPDWlZY63CNg1VVSIsTKYLPjjK+OSsIitqlxHslCutMfh+SDIYs7WzR9wf4fnhWqRldT2VkPT8mnv+BSd/8g8pWs2sAKEr4sNv8/DX/g36/SEnn32XF08/5Z/+o9/mzp0dTj78E57WFcvZlMV8iikz2jonTIbsPfo6lyfHHH/+MX4gUdrHC/uESZ+o32d774Bf/ht/m/OTz/n4D/9L3vjGL/P+r/9rvH4nxD8554tLgdAh1fIKoQIa3CTu+U4ksWkaBIK6qZlPJkjtgHoURR1dvCUMQ9rGJXbCMMRXbj4qywLf91HSAfQ8y0AIlvN5Z5Pmg3B9X6PxqBN3LCmyvLOq27Ro+WmXm7O92AweN6LWl2K461j85rpXvd7cwavP4CUgYNk8prjx/5c2Xu//5ZrObSD+8oqbrxwIdwk106kKa6sZqCFJ0KPSNVVb0vgtVV0zy2Y8zR8jrSDxEjyjKaqCStbozuamtg1VW7JlYpSR2MZiTUNuSlrbguWaRt99X4HoKNOWrFkyUwuIJJVtqEzNIq/5/izmohTMrKISAiVbjOPxg1UoA1oNqCqwShDKggdc8EP/Hkoa5FDyZvGEgSwJtE+/XXIWRvSFR2RK5mGfzINh3TL0fe4mPiaI2ZWGKAgYxzE9X+FrD79jzmjfI9YSLd2f1TwqhHVaExtBtzHuOlhjabt5S4Jro5BOh2LVaiLVqton1tTyTWXnm/eaxViJtQYpXDuONN32rerAu3StGtLrgLiP9lO01hQd+LbGUtU1iMIl6+MEryfIrs7oDcYYKxFSU2Upp8dfcP/RW3hBQFOVPP7wz7n/5nuY5pJUOS2H+cURD979Zaq605yxNUpC26QsL5eU6YTxnXeIe9s3budNMHxd9b6J7l7KnXFzCNrrHWxA2p+cFFvB5FVybL3VVxrfm/DbriuaG7i5SzC9elc3hnT3uZe+5kaSb320VQPyVwGeP8Xyk3bzcsWea5G11Ql1NHgdDNDRiKauEEKjtI9tG04ef8b2vRo9P8eLhgTRw+v7XqzccuxL9/1P8xW/PLh/1Sz8k49wMxl6q6q9rnSzZnGYdTuIS8QZ4yxW27bthE1bV9FunAhYVVVopdnZ3aYsc0eRXjpAnS5TdnZ3GVUNcRIhBJyfXyCEYDabMbmakGYpO9vb3Lt/nziO0WPF+fkFSub0egmz2Zwk6ZGmGb4O6PcVaZZirXD92EW57j2XSvLJRx/j+z73H9ynKErSZcpgMAQh8DzNdJZxdXnJ7t6eE1ktS3q9hMnVBIDRcMh0OnWxZpoRBK7Nz/U7GxbzOX7gKtnWGIaDAeXuDmenp856zBg8z+NqckUQBOzsbLtCW1lwevqCMIp4+PCh0wwylq2tMctlSuD7gO30rCqkFGxv73Sgtnafb4dIIVlmS9TUxarzxYy2bbl77x7z+RytFdkydS4V8yU7OzvUdc1ysSDpJbRNQ2EM+wcHNHWNkIKry0vonmkH+wf4gU9ZFDRNy2Q64eGjR05I1hqSJOb8/IKmbuj3nfbGYply+uKUKI7Z3d3l6uqS58+fMxo7K7Yo9FkulyxfLJlOZ2AMeZ5zcW5preHe3btIpfjiyROkELz19ltcXF5ijCFLM047hwvP07z39jvMlgu+/4Pvk+UZ29vbxFGMQPL48WOiOOKTTz6mlyT0kv7Pkdy7Nd6+ZB5/aZNXPfK6f19+61Vj+dX7+CrLVwbecc/124rSVWDdYHdWA73QMP3Bf8rpdw2it4cY34eH79O78xp+b8vZDtQVUkj80KmSt03tAgLbQrVAtA2mbWjqFF8rfAHC1Ii2hraEaokwDbZt1tk/gXGPT+VhlUaqgBqB9nvo8QH68C0Q0glfmZpm8oxm8hxbZdBW2LYmXxZUWUk2X1BPL2izlKKoQEiCKCTe7dOWOVFr6Q0P6O3dBQS/9Xs/4GiZkpua97/2Ft9873UevvN1+MVf4NM/+X0++71/TiChF4WEoYe1gnlWQNtilcQKS9laLtLc2V9pRRRoWqFQ2nka+56PtBbR1mjbkmeZ86m0gmVWUtUN2lMkievJGCYBbz28R1M3hGHIJ4+fcbbIOZ+n+EoyzTJ6WlHkLaWRXGU5Dw/3+d4f/zlGwNVkwTJ1dBMjNBeTlItpShFqRsOSZZrSGgtSkePx4qwlLZdO9bITr3Jibg70CSx1VQMuS62Uom0atHYUYE941HVDEAT0hyN6wxHDrR08L0ApRZz0GIzGTvhHSrTnEff7YAx1VeEHIUWeOwpxEHTg2n33/nDkPBVF55sdhC5wVApjnc+3aVvAOluNQb97nq+UVN0DTCvl+rW1WvdKL6YzEI514QeBA4F5jlCSpNcjzx1LwijlqqnWcm/c8mhnCeWEdLFkNl2yTCvqepWhbjrP0ZaTsxnfP6o4WTQYJKZt1g/YKI7p9Qf0+kOSwQipA5fNDQJ6/QFJz/VCN03Ddl9zYD/n+ff+OcNH3+L1u2+QbD1EakGzPKHMziiWJYsXnzI5O8eiWRYVv/9P/n+UaYoQhv5gwOvf/lX27tynN97l/ju/zMnjDxnd+SFNW3P0+IcsZnOOT86gNST9PqPxFgcPHnHv3e8wmVzx+M9/i3vv/hL37+7h28dcnX7O8vIZeAl2/6/Q6B5VVZLVtbNd63pTV/7sXhBQdwmTIHCqy1EcEfdc8s4a51+ppeyq2a1TaceyXCyR0mXmZZd9LorcVQybBh0EhGHkNCOqn8NO7PZiu9D+VhZ3PW3fAtqravEm1Xf99isC+RvLxs7Fl6xfRefr978kYr8+1pc8YG5lB14+rZUol+sPbloX+FV1RdUFgHXjEritqalNTd3WGNMyUH0EIIXE056jQ2sPg6VsS7AtId7GtRUYYZHSXTxjDAazrtKuEgBYS2MapvYKGUHb1QFbI/l8GXFZwqKReE3JtrBcmBDTKqdY3gh8b0C9NE69vKo49AsCEmSg2G4zvlY8RmsQShJ5glZopG25owVGB/Ql7LczroKYPRER+D53Bgk+hnGo6YUefV/je5pAKQLlLAm1dPe0lK71akUF3/wN7QqEbFoluTeQwiXh1CbgWPeAblolraqoN35Ftw9rsVZipAPdxkikUUjRdpRd4ZwKuuSX0hrlXVfDy7Kgrur13FwUBRYIg4hg65DWQFXmMO+SbHmO8nzy5dyxe5IBdel82tu64e6jRwyGY6psTpj0HFtO+U6MUng09RLt1yzPH2Oqgt74Dq4yfcvq61XLKqn/ivXXK29eq1eJcd2I3uzNVS/v78s3vXm8m2RMIV6x0SsW8arvvYn4V3Tyl07b/oWXh35ypXvzBO3N+UpcT1Mr4T9rLEIHeIMDqmyOMS1FnqGEQHiKcjFB9gdU8xfY7XtOUBT1Yy7/Rvbh9uqfY/nKeYv1PGxfXucQ9w2q+WbF23T2f6ZLIK8o5HVnB1jXzlWkqiqkEGyNx5i24cWLi3UyO4wifN+jrRukkuzvKzwvIOn1GAyHlHkBAj756GPiXkxVVijlbDhff/11wsAnzVJ6vR5F6fQYwFmmHh4eUtc1SZJ0tG7NYDjg+fMjgiBga3sbIQQXF+f4vqs6P3r0OnmWkS4ztre2mc0dcE2ShKquaJqGMIq4OD8nikKGwyGXl5frz5hu/qrrml6vx9XVFVprHr3xBgKBqeuOTeco+aPtnU6LqGVnd5fpxIH6zz79jO3tLeIk4Y/+4A955913neidtcxnM8IooipLdrZ3EMqtl0LiB76L2bt2y9FwxPvvv898uaRpGvr9Hp99/hm9Xo8kTkizjPl0iu97PNh+wOmLF+Rd9X80HvPdP/1TdnZ32N/fZTgaMZtOWCwXhJFrHdrediJrfhgwny+Yz6YMBgPu3b/P86PnLJapS8a0DR9+8IFTc/d87t+/T7/fQ0rFbD7nxYsXtE3LApfciEKnw+MeF66tbzAYkiQxbWs4fn7sEhvDAZOrK5bpksgPEQKePHnGwd6+U4bf3mE2nyGk5P6DB/hBwDJNeXH6gtcfxSh+9la/W+nML3n/pWn8v7Xlq1e8V56ZHSVTCPDDGKUk0V/5HxBtHbL88LfJL04oLx9z+dE/44UKifYfMHrwNr6nEEEPGSTYsIeK+lipkZ6PFhZjG6ytMG2FLQtaU0MxxeYzJI0D3aYB02Jt21U9fVA+dV2uc8lCSmprqDqwYq1A6gCnn2GRSrjKYp6zuLxkdnZOfjVF2QYpcb1AnkKHAeFoh2KZUjQtXpjQ394lzTKWy5RBoimOa0oraGrXP6hpKfMUZZykf1sbirohwlGTZVFj65a0qJmVhhYoWkttBMK2jloYuF5T3/NRUkDr1B8fDiOWfsB4f4+j00t6cYT2FIHvYxHUxjBbLPjk+QvatmUQRzy4f4cPP/scrGWe5YRBSCRB+IrMwidHp3x+co6NYkRZsCgb/LYhyyvnidi0nE8LzMDn6YsrLmc543GfwgbktXT0zcbZZpVlub5PpFQIT3Re1a6yEgcOHDd15dTokUjVgemkz2C0RX8wIoxikl6PtqPx5Ms5JozY3T8kjCLipNdRwp1fddLvY42hqWsQMBxvdRVuxwZoO7BdN7Wjj/tBpxvQIDpVU98PELi+YiEl1nQ2Jb7seo60A+lCUOY5XleBw3ZJBuW8b5WUzpbKd57zdVUR+ZLXhhkH0ZQ6m5Mvl0yupuSlprPddg8B4yhUT08XfO95yaxw7IGmUzFGgN8B7OHWDuPtPaKkjw5ClNQYLE3bkOU5URQziDQHPGF29DGv/8bfZ7R7n2x6zPTJHzA7/ozZ1Sn3f+FfRpRXFMsJh/fu8PV8ydXlJaZUKNtSFgXzpub4kw8Z7x0y3N5Fa8X2/gGXJ1/wx7/1j/B9SdO0TKcV/USxnC+pahcE6Gef0d/a4fIs5vL4P+KNb/4a2zt7tPmM5WnJ6fPHJLlBP/obrjVAOI/x/rDverw9z/VvW/A7n+GqqdBSdyIgjvHQNs6Srm2cyeFyMcda1+4SdOPJ0Ws1xhpmkyl1UyMFRDpxc1oY0NTyL25m3YxrNyb5l7y+N6txq8U6wLQWUFt97kaZa/X65Qj8VUHlS3WdzRUbwGu9h1dGp9emRNebrsSAuPWOq8i0bUvV1BRlQVEVFFVOWRfUbeP0GboLJKUk9ALCICT0Q9eKY6ExTv1c4bxlV9kJt51FCgsdtVIIF1a3XfbArIAplqVd0AYtSjrxHozgMpcsTEthBVtywaRtedru0toQYSy2FWjtUzcW6yusbQm14Ruc81n4gEOR8+v158SqYBoO2GpyrvweypM8MDWR72E8H6XA15qtKGIbw3Yg2fEVURgz8DWJlkS+h681vpJ4XU+q7nyFlRQIHD3WAfDbv+d1cL7ZuyxtR40WdBZK10DxBth+RalB2E4Nugvuhe16pKXYqHq7OU9JRbMSq1LKJQ2UptI+yvMpi5ymrLpKXEtZ5E6YyQ+otGZ8+JDJ8WPCpE8YJ1jhsZheQFuxe/iA5cL1U4b9MYvZJXU+I0562Dqn1QFNC9ZULOYzhKnZOnxEU9eU2QV1nZJs3Ud74fpeF919cS0kt6ojr67JNcy13Byz19drVVaxL7dn4Eq0rvPabPwiXA+960O/aih2Bd3NbvBN8aHrMHJ1jjf2c+PFrQaUl4qv1wP9erNrNfNXZ/J+tuUnb30zCfBSsLwC3J1vtxPgBeElePGYKr3AD2OaPGVrZ4dyOcMPQ4rZCYuzz1DKIx4dEvXGiM5S7FXHf2nV7RP6aZeXf5gfe1yBE0u77u3emNdvAfAVm2j1p2nbG6C7qiqauqKsnXVpVZZOANE0JEnC3q5hkRZY61yGTNvgB4467vsBWmkW6YKyKKgbt25nb9edWzc/nBwfc3V1xRtvvknTNAyHY6IopGlcS1+vF3fnZxFSuiq1Mbz9zrtrX+uiKJxobb+jiGtJnuWMx9s0jfu9JYKiKOglPYR0ziX9wZimrbm6vOTO3ZB+v0fTuiKF5zm1dq0Vk8mEi/NzriZXHN65y/bWNkWRkyQxi7lLBJed0LNUkizLsbYlCAOyLOW73/0uw8HQWaNWFb1+n7ZpWC6W7Hia7d1tqsIVaK6uroj7A7I8Yz6fE8cxs+mU+PAOxycnruK+NWY0GnN8fEyWpoiOiZemS5Jkn8urSxbLJdYY5rNZJ1TXIwpD2taQLhdrS2ZnT5u4uNTzmEymxHHMcrkAhGvXU5pezyW2414f39PUVU0cRwSB71xeWse4vLy4YP/ggMFggOd5zt6xbUDA3t4uWeZiTKU9zk7PGHQ95EVeONXzMMTXzvu9Lp26vACm0ylHR0cEQYjvaeazKf3+gPPzCw4ODul3hZSXRoa9OWd++Tj7iqD7x+3iK3zmq+/ty5evDLyrukbgrHn8wIkBQGcFpRThG7+KjEbIz/4Yefq4s+uqaM8/4+TkU4wX4wcRPiVamE7J3IIOSLYPUckQ5QVQFyhhwFTYJoMqx7Q1QmmU5zvVPmO67CXdBNpNonblF966QKObLa1MMcYJVeXLlHwxp83zjgJvkR5ILybqRW5S6+wlJpNLPng65ePTjMpIvvPmkntDj/OzGXVhmRVukqvOnpPu9/jd/+Q/QqRT2rLg7t6Ivq+plgtM2xJYQ9pUSKMQrUVZQz8OWVQVtTWEQmIRaO0oglezOZGvUdYSSoFqW55MZsxbw9nllMZA2TaMRyOSOMIYaA0cn09oWsMRE4qqZtDrscwvCQM3ibaVywgf9GOeV4JZVqG9gKpVzDsrrqZuCbOSumnISkiripO5BamZtAbPa/C0eyBopbC0KKlR2omrKa2dsJvnO0ClNUnSJwhDgsAHoekNhs6nvavOu1jGVVj9IOy8wBt8v+d83YULzrMsdb3BXWZyZWnnhUHnDW0A2QX9DU3r+umbunZtBtCpW+fUde16DYVwVluCDrArB8xRNKbprK9qhJDUTYOUkvnUZe6054F2NOaiKOgPOr/stmGnp7mXXBDbCVU6J5svmE0WLAtXjjKmoa5r2roiTTO+uGz5+LRhtnQWWelyiTGmU2H3ifsDhuNtoigmCJ0onbMKiyhLZ6uhjUXQ8NadmPJC8vqv/X2y80/49Lf/EadPn3D25DnZfE5uAt78a/82F5/9Hkr1ePbkY7I8YzqdEng+l1eu97yxJWfTD2mNZXn6hHd+5V9m/7X3Ge/fociWlFnL6emMyVXKv/pv/q9I+iH/4f/5/8rp8SkP37gPpuXyxTNC3+OHv/Nb/M3/yf+a8dY2Z70Bnz75gOT0D3nv3l9B+X0nfjd01kZV7apkUspOcNHR0E1jqWWN1pqmaSjyHHDieFXlkjpCKYSx6wfodHJBvz8kjGOiOCEIQ3SrO/XOGts0aK9Lrvycy+oB4R4Yr6iGvTIKvllFc1h6Bbo3ejTXINyyrsx1wNVh8FcE2psn9SXn+xJsXoOHzfO8AQ86AGdv0FavY9Trv4UQKKHWNpArMUTpVMvQ2sP3AoIuyEMIWtNSNw2NcT2DQoBGg+wCTFhXd1ZMEAAhBdaIrhohXGuGNTSmIVMFfhCBlLS2pahg0hiyWnLgz3mcFjyt7tK0PgpBUxqk8TDawwjXx01Z81aYctA2/DAc8mvlM8aqxkrFMAwJGkMQhfRNS0/5NEoTeR7W0/heyJ6CcRAi/YDd2CfyfXxPEWuFryS+1mglUcpRw5WQqG6uEwLUxu+xCcpW113euKc2fspbYBu4cW/d+K1Xm8trWqugY+WAszHqADhCIExnw7Sy5umSBkp7KO0htYfSPrUuqKqio2G2VGXRCVr6BOGA7Qdfo12eM704xVrRibTlLLPHxGFIHMd4SpAvLynmV0gvBmVR+ZLZbEK5mLB7/23iMEZpJ5jYVCV1fUk6Oyce7jPYvo9UgUsgrKy5bldWN+5wNsD4S1XIFfgWN2vkq8TGhib1+lrf3M/NMX87kbKZGNnc+wZ+X490IV7e7+b5vFRtf8V3vYHnWc0lf7HLl2NXe/PVraKzEDc/sVLbX4Nv4SGCAc3lM9eK1hqqPENpj7Is8ULF09/5j/GjkHD3NbZe+xbbd99Fyh8f/q7P96eMsV8Wz/spruWtxEiHs2+stvDKqnfbOnuutm1p2pq2cbo0dWfT6FxgCve6rp1ollaEgcdstsBYS9O0eGXFaDRkMnXWXysx5efPj9jb3+Nw/xAhBScnJ8znTpwLAR/+6INu/GuGwyHGGJJe4hxjwsgl47XGD9zvcn52RhhGrppelpRlwcVFxcPXXmM2ndF2seFgMKCqKqIkIrvIuOzo1llnb7a3v8d4NKauajxfMD+/ZGENpm3o9fouZlSKXq/HYDB09rRKUZVOSHf/YJ+irOj3+hgz4ujoiCRxdPjRaMSbb77J5eUlWZpz8uKY2XzGYrFgtDXC83yGHW19tDXi4uySpmlIeonzCA9DRuMt50RRFmtf7MnlhLZuefToEc+fP6foLHQfPniIVMpVnOdzwLK9vY2QzglHex6LxRzTGgaDAU3bEAYhp2en9Pt9ptOp6+ePIrZ3dqmrig8/+BCtFL7nMRgOqZqaeOiYROcX2Tqhpz1N4IfcuXuXydUlbV1z7/59pFIcPXtGmPQAweXVJWEQMuj3OTy849pEfZ+mboiikDAIOD87YzzeIi+cvevl5RVN03J6esbB/i7TxZzT4xPeefddkjjh2fNnvPPW22ur6S8fGD/9vGRfevXj9/Hz5Ni+6vKVgXeRuR6KMIrWAa/tfAQFPsaA2n0D3bT4oweYMiMsF9j0ErO8xBQZeT4jrQx1axCmwfcVvkqx2RVaSydmFYQY6XrRhJYgFBYDdUVVOZsAKRQo3QWKjjbsHhyOwi2EoGoayrwkzwpM7fqfA20JpCXo+bRxwCJNqeuG0dYBIkxIFynLyYR4JejQG/EL39pj+qdP+OPPLvivfnDO/+g33+PumweMioZfrL/g4vySu7ElPX4KQtLTkijoMe6FzM7OaZqWfuhRNRB6mta6CnakFa9txUzyhqTLnLfGUOQljXEWak1d0RjDg1Dx6SSjiXs8v5h0yt6G2ghaLCeXU15cTlFS0BiDpxWRp5kvUlfx8wJ6YYhoK5QMOKsagrYly1uKuiHoEhdVY2haENpnUTZIoVG+hxUS4QdOzdBYdAdqg8BRQaVU0GUYXQ+tj+f7RImjr3heQNQf0O8NiBI3IQVhiLF0vXwVvu93yveKXr+P9j1s2xBHEXVTE0cBTeWoh6Hvd5OnXNtSeZ1ivuwyoeli0anfe0jfiXhhjDtX0RDFPRIpOiGurmKtNKZ0waFjErgscNO1SdSd+Jo1Bq09iiKnrR2VXHaekXmW0TYVO1HJ6/0Zop6SLTMWVxOytCAvDY2RtFWGbVvKomSyyPnwrGXexrS4Hs22NSjt4UtJMhgy3NohCBMGo3Gnsu6UhIMgxPN9/LBLGtmWNw9Dhv2Ey0Wfx7//H3D0wfcItu/z8Dt/B9X7Lr3BFkVr0Qquzk5ZXhyTT19weXKOEhFlmdK0BuH7vP766/zCd94n8A3zyTlHH3+P3de+ydb+Hd78+nfILh7zy3/9b/PxBx8itWT3/tv8+r/4q/zRP/9dxnff4e333sYA+WJO1n6Xf/h/+d/zt/6tf5s7B3v86m/8Ks+enZCffcLO279O3TZdwsWitKbJC6yylHlOGEdOKE+6LK9S2iXNOlq9VMr17SvJYOAehEo4cLCzd0BVFNRliac9hBSOimYMTV2B0oTao1pREH6WRVwHabYTc9oMuFfVi5vh9+1wm421N4Phm5Vy2YFv2OSyC3u93a1Tu/Xi+mBfAtNuPXzk9SfX33MDVKyq8xtbCCmQKDztqq1aaXw/IA5j2i5xuv6scBXGpvOIrdvaqbZCJyq2AvnuPKzTHL+mTSMAtT4PuvOzuJ7ns2pGEvadf7hpsMYwrT0K03I3mfNnZzmf5g9pTYSwYJoW2UqU1DRGIkWIRBELy6/oK5pgzNsy5a7KwfeRWmLCgLBR7HiasZAIP0ZLS+h72CCipxzADgKfMHBK5n3Px9MKXyl85eYfLSVKis5vWCCFuwZyE2yLDWGtDSAmN7Mz4uVf8UYF9Qbw3rgPNj7gkjkuKLOqqzR29nXWut/FSInpfJHXatEdS0grjVYeWnvU2kOXirJwFbem6XohjaE1ljCMCUYPGEhNXcxIkh7Pnz2lLTJef/s9PC+gKTPOj75A+wFBnjGfz6iLJbt3XmP7tfecpahQLqFvGuoqpalLynRBOb+gqTLGB287z2958+4X62wZa8CzvtlvZL26l919v/neDRkzcXOMX+9GbPQrb+xww9BZbPyoNxNbNxknN1tQxI19buLm9a7Xc8St5VWJh5+voPPK5ScFs7fB5Svf3Phi11VfS6t7eOOH2MlTrBDIUqD9iLapCFUfL/AplwssT5jUGTQVWw++4VxQftz5ip8QiH+lWP7lD31JypWbJfaNPaxaZ27tdmXbuALj1xVxaE3b6c24Ng/X5tNS180afLvkNgyHzlVgMpmyXC5I06VrWYwjhoMhh4eHxFHIfL5gkaWMhiPefuttFssFZ2dnTK+u6A/6zu9ZK+qyIssz19ZlDMdHx2xvbzMaj3nz7TdZLJYdSwk87TEYDBACZtMpURShtebi/IL5fM5wOCKKQvIiZ2d3l2yZEicxk+mUNE25upowHo8Apx9R9AqWacqdw/ucnp7iBwFV6WI3Y41rYfJdG8xg2CdJekiRE0XOJWd//4DJ5Ir5YkEUxetjfPzRRygtefONtzrVb8toNCKOYz755BO0Vmxt77BcOvAYRiF7wR5RFBFHD5zrU1Vx9/59J7g2nWGt5d79Bzx/+gStNXEc07Qt0+mMuqkZ9gcI6VqvLHB6esb9e/eoqmr9HHR2biFlUdDrJfQHA5RQ3L1/nyzPuLy8II4i4p5jnSa9hKPnz1gulkRRQhg6mvqTJ19w584hBwcH9JIeh4fOl30+n3PvwUPS5ZLlfE5b18yKkv29Paw1+L6HVorA90izjMuLCy7PL9gaj9kaj3n2/BmXF5cIITl7ccLHH35IGDpXoe997/u8+eab/Ev/0t/g7p07DAfDl4fNT1x+mpnlJ3/0Rp70xx7rZ58kfwpVcyjKEuhAgYCyLPD8oPPSFTRWoEb3qLKa2YsXlLNz6jIllj7CQhAqBkGFqQuaRlDVDXljWbTu4Wts6iyotLOU8j2JpwRadWDOczQ2q+yaCmWRVHVDWRnKoqQqK6oyR7QtvV7AcHtM4EfYpqGtKoyFum0pmxZvtEPSHzK7nPDRn3zIx0dXXGUtd3YH/NrXDjB5iqpyfuMbd/jB8ynzrObFomZPRZw8OWIvNCS7Qz4/z9gdWBJfUjclO3e2ySdX0FT4bUU6y8grw7K2NC0MPEvWWnpxyC/cgU/PUmZlTSsg9EN6kcdksaRpDZ6S7CeKP53lxNIjr2rKukULaK0lW6ZcLjNXEekEpLSAXuhhEFStJQhjqrokUpIayULGzJcN87xyCrR1TWs6e4JVMOgZV22Erg+mS4YI6/wxbUtrQAuFH3SKoVohpFOT1lrTH247Cmkc0x+47GAQhmAtUZI4mncQUFbuPHzfJ4yiDqz7WKnw4wRtXC+/lIq2rjFdP3nTNijPPXC0ELRNS2Od9VQQhu6hoiTGOh/Jpm4o8mytei071V8hFW1Tu+xwVaM9Z7PgBT5t46qgvcEAL/ARwlWipXS2Fqsebt8PKMocaSpeG+fsBhPq9Ip8Pmd6NaPIG6pGuJ7GunRCb3nKyaTmR2eW00mKNTNWNEcpXDV9Z2+fKOrRH28x3tnF83zq2iW+4t7Aicx5zv4sLzLubynujSzPvvsPOH36hAfv/irbr30H0zpP9bv3Drl4ccLd93+DxeVz5hcnjA7eJD78Bj/8v/17CFtSVi33HrzOL/7qL5FEkrOTI9TeGzz85te5+OL7nD/9kK07j3jtvV/gd/+z73O4fMG3v/UWgW8wTc2j977D6P7XaduC2eSYy7MzlvM5TZOxWM75w3/0n/FLf+03iXTF0K8ZySuELZDCKcOLrsrnBz7a85CqR90pIyutMY3rzV/MZ2RpihcEDPoDPN8nS1Ma48QcwzCk1+tRFgVKSdqmXfddFXlOr+/YFW3TOiuNnwN4X1egb4fatwKoG1HujdD7pXn8VpjN2hv8FRP+K5mhL32IDST/JTvgGsvfjA5vBYObn9l4f+U7LjuNByE6v3Wl8ayzDTHWroWArDWdQ4BByG5+t2FHtXSJ3bVloDWdoNDGya3B6M3K0iogvSouCXSAUYLC1hgBRQu1EWxHKX9yPuOH2QMq2QfTdruVCKkwaITykV2O4BuDnH0NFyrhfWZo7SO0c99oQ58+Pk0QsOV5WKlJfInyXMUo8TwCrRFK0Q89fE8Rao2npHu+dTRt1fVkKyGR0CUT3Xe7AaY2vq/DgS+jqmsAtwG+X/7J1v++CoTfJDtLVmG+7VgXwtp1/7jpEgZCyjUFXSnX911pjdKdnZMSiFI6XYW2xZZlp8IcE4/uo0UDTc5oOODJ+SnKd77Ds+mEpq5JxgfoIGJy8YK9h++xfXCfKptQFClhlFCXaSeUKDFNg/I9qEqq9IqLZ99n++57eEHvFjNR3Bhb7jbukg4bN/xL40tsjoqbtPM1eO/euxmyiet1wq4/u4bPqw9vAD9xfTJfmly7FlC017fDijq+Gv83qqqbg/z2+P5JAe1f1LIxY9rNc+qWL4tvuznBedu3GG+IHD1C5mdkly9o2wLRPS90GCEkeHEPa2rmRz9CeiHR6A5eEKGUx0pHYeOUbr/8caf/My2vBPUbmRKxcffZ7ju/dEHWLQ+rudVR0Dcr4G3TsYi61rWqctacToytwXQUdU9L6soym89RyqMoS5bLlOFwyHi8hdY+p2en1GVFNRwyGg0d+wVL1Os5AVNrybKU/qDHbDrl+OiI8/Nzrq4uiaKI5XLBvfv3yfOMKE4oStc7PhoN8YOAy4sr9g/2CMIQT3c6EbViPp87FpB2LUeD/qCrDC/wPY+6qlHKiUFmqZsDwjBcJ2i01lTlyqbWEid9zs8vmM8WbG1tuWqxaZnN5rw4PWVrPKKqK2xnk3z//j3oWpq2t3cY9PtOB6iLwcqy5PL8gp29HWf9qyT9Xo+6bhh0XuiffvYpCIHv+fT6ffI0xbaG1994k4uLcy4uzonCmDgK2NnexVj3u006ev54NKZuavIiJ+ntUBQlFxcXSCk5ODhgMBjiBz5ZlvHJRx/R6zvNn9l8TtxLEAKKy5Isy+kNBuRpjh94PHv+nOOjIzyl+cV/4SG+73N+fs729hZffPGEOI461XrLeDxmsUg5OXnB8fExe7u73Ll7D+152Cx318RTnF9e0jaOcfrxRx+xWCzI0ozZbIZUksHA+XifnJwwm0zoJQm/+Eu/hK/96+Tm5rP+q46ozTjnpwTdN17/JU5/Xxl413UF2A7cBTRN42yLPL2mukitiXojiq1DdNynSpek589gcoRIT7FNQ9Ea2tYF1p7vEQjh+vQsCCnQcZ+6SJ01grU0xpIVhmWxdNQYKZBCoaXrnlJS4vuKyJd40iAVxMOE/s6YXj+mKXLyWYqpaqyQlAZUPGJ4ZxdfK6YvjlhcThn3fA62+3w2nXL0eEJt4G995y5VWRP4lu1+iG2hXkw4eDDiwS+9S56mXFzNkJ+fcHk+paTlW4928LMFy2WKNBVNZanqhkE/IZ1kGGPY63lMM8PZyRlxFPH+3SFPLlOepw3jYY/pfMkszRBKc6h88tJwWTYspwsQkjAKyfMcT0rmiyWe0qhVcAb4WhGFAXnVULYWbENdVGTSBXu11lgkUezTtq2ziPNctUIpJ17l+35HsXbVRAtOvdsYgijqrKBcFcvRoXvESYzoKKRx0mNrZxchBWEYI4RwNli+jx8EaM9H4AT6ev2+6yPpKL/9wYi6rrACd7wgwBiL9gPqylGkVlXuVa91WZSUeYYQkqTXR2kHvuuqoshLlPKI4ojIS2iqCqEkbd3QWouHe55pLVFx3FXSXb92EHmUeU6R5+sKG7je9nQxw/N9wiCkLHI8s+TN7YK+mlEsJqTTJdOrKWXV0BpFXpTOIq6tyZYFn59XfDH3WOQF1jpBlLquCcMIFWjGW7uMd/ZJen2CqIeSmjBM6A8DtFK04HpwhKBpGx7uBbx/V/H8u/+Y7//RHzLcf0DVVOxsv4HyAxrTooOYT370KW+O9zn6sz+itD323/waH//+f0lWChp8fv03fpH33j5gObling34xl//e4S+ZXl2zPa9h5SLY3TwPtsHd4kH+3zyvT/n7W98DTPaJ8hzytZSzk+YXV1QEjBbwuMffoiv3Vz2/OkZdx5/xmB3j9DXPP/khzwcv4Ha/QZlVeJ1jgnWOnq51hoR+NSlE0GU2iVNtO6SNUHoLPesRfsaZTRdOaDzMXVUZotF+w54x3Hs5rIwoMhy9zAMwp9rMl3H7a+IzsVGReul2HIjyObLrI+AVTvNq4Ji8YqK2Y1i2K2YepW2FNc72Fi7Ccx+3NPnOomwbntlVZntWnZw7SdWWOSKDG0t1jrG0ipYXAFlsGAsLXZNJV3RSu0GsNlsnl/Ne9c0X3eMuq1pq1O08ihpkFaBaRE0xF7OhxeX/NH0PqW3Bda4hC4WKX2EAX8ckE/cNR8o+LVkTiRi9iUo4dGqoKNUazwdMFIS6/kMfB9f+4S+QnmObu53dPKV/3XodaBbOuCtVlRt6Z6NTnXcgS2JYEVp3qxavxoKii+JOW4mKq5/943fb+POuBHzrK71qp1r9XvJjoYu3LPbGFcBF1LSdv8Kdf1cUUp3QFwhVY6UirouHfW8Kl37QNvghyGBjtk9uIunwNQFWeoYCKgQHQ7I05Ttg/skcUw6u2I5PXWiP9uHKCmoygxjGmxd4PsR3tYOps6Q1Fw+/xHR6JDe8MA5WdhXyK7Z66twY/jcBus3YPnN91dpsxvDdT1GN+H67UWsL754acfXY9be3nCVQHnVATcTM186pK9njhtntwHg/yKWHxfTrqa49X25+msjibFKzAp5DZStcX3NSscEgzvE40sunn2BQdI0huHWFsVihlEeprVI0zA//hDpRTd0XdwY1Dfm0B/7rcVX+dBXXF75u20eZ2MMdsmZa80HsbYGXO1nlWO5roTTJTvBWuFsSzsrwta02LZFCIHWkl4Sk+U5ZVGxmM9ZzOcs505tW0hBURaUZyWtaYnCiAcPX3M2hkLSmpbFYu5sWcuK7d1dHjx8DSHgk48/5vmzZ65ia118N5vPODy8QxzHRFHs5vnWEkfOtszr2JCe73N89JzLi3OqquTg4JD5fMZ4PHRsN+10qJrWUZ9Pjo/RSjHe2qLX71PkBZ7vMV/MWS6XHB7exfMUTdMyGDhhtrzI6SUJTV3T6/VJ4oTlcokfuHkiDEPSNCOJY6LExRDT2YzBcERVFoxGQ7Z3drm6uqIqS7I8Q0rletN7zirs/OIc1cXDvf4+dVUzikY8fPgaT58+oSpLwiDk7v27KKWZTaecnZ05RsBsynA8hCyjrEr29l0/eJalpFnqGKBz154VxzHpcumKWNbi+z62tWt6vmktfkeVf/78Kf3BgLjfJ01TTl+8IM0yWmN49uwpgR/wxltvkfQSppMJUrkkrJTKKbC3DdPJhLIs1+0BxsLRs2fUdc3z58+ZTK4Iw5AgdN7ro/GIMs+ZXF7xR3/yxyjtfqs3Xn/TxXy354BXLjeqAzdXf9k2X7LJy2PxL2/56qrmUUhV1U74KwgQ0om9mLYlXS6hsxwJ44jh9i7pzFGQq+WEthlhmxSlgLZGNQKso8A0rXGWWgIEiiYvqHIXRFvjniLa89jf7ROFPsohTJQSRH6AxZBmBYusJrce2wc7bG31aLI50/Nz6rJGCEVlfSqj2To4QHuSk2fPySaXzq6saQi15juPthmN+/w/f/cJX1zl9A4eIJSiKWp+7WsVZ5OM50dnVMsPGNuGUeCRZSl7acmOtoyTgB3Zkk8c0F9UDY0RGAthFPHuaMD3Pj2mlyToCD44WzCyBZGBBwMPP/Q4n825WuSOltK2jCLBUVaRFg1lbdga9ijyAmEtwrYUZY1VLVIpAqUxraEETq8WCCmYZnnnXW2wSriHlXU+5yuvYyGE86RWytlQdbZcUilC7a37tH0/pCwLer0BYRQ7UbKumqWUxPcDkn7fiVp1IFtpz72vXZ8nwmUPvc72KYpjtHa2W5PFHM/3ugxss/aaxdq1cmRZFpR5xmA0RuDEuCw4EQ3fx7Qti8UM3/epOss4cKJ5bQfWnfWUqzqHYYgXBMhaOrX0usaaLqsKbmJJeuR5zspPMkmSDtQ50S5rW7Z7NXeTHFldkV1dMbm8ZD4vqRtBXbuKqtQKqoaLyZKPzmpOc5+qLqkKB+yVUo4yryRR1CeK+wRRzGhnDz+I0J5H4LkqcNM0hJ7XZX8N+72WN3cyZicX/Bf/0X/I/jf+DuOHb5AvU5588GccvvEO/fEB/sHr/Obf/3fJlxNOPv+QXm+Mri44fPQ2/9P/zS/SGyb0A0FdpvS29vD8kOrqE9Iyp6kK2roiv3rCnXd/jeH2AYdvvsM/+U9+yN5bPvde+yWuLo65PPqcxXSC9Pp4Ycz2rod5/69wfvKMNpuho4Tnz57zVpIQJhFXX1wSf/p7HOy8jZLK0ZaFcMk+a6mKnLZxmWXlaceWQFCUJVEcU1VlZ0MGpnVge9XzbnHCa669IQILXhB2tnMG27T4YUhVVk5A5OddXgG61/++FEyu0SovPWU2kPkN5eRbwe9mYL7a3fVxbkewbrldF918LW6tWSFqhwPE+mvc2Mc6V7BJXRaA6TyUQcjrY9n1Z7rvbtf/u9G3aLrkiV2/f0NmaOPLOrbOJmCw1mKqFulJwNHepXE2WzUtH09e8Ftne2T+LjSAUnhSowNBlTZYz+KFDYVVoAK+MyzZi6AlREuFlRKrPbTy3ZziBfQ6Re/E9wk8j8Dz8LTC0wqtJIFyANTr/qxo5UqqTh3ckead4jid17Do9Equf6FNFvnNu2GjJYDb72/eh9cVtesPrLJC18D9tjDg9a5cBdWsQLjswIBYJQ26Px2QUR3wdswHZ2umlIdWOVXpqlArxlHWpm5O9AO8YER/XDO/OEF5rjIpVMAXH/+IMAp5/1u/grWWq9PHWGvojw64fPGEMpszm17ha8n2/j0GW4cYJH40pK1zFvNLphdHRL0t9l77Br3+LqtEXVfHv76TVr3W1m7c+6sfwHb31u1E1goBb17DzXFobw6gzVzaKrG27kG/Nd7WyPRVYP8abF3//tfHva3+cP1dbv7Mr6wR/QUGoj9pV6v7b7O2u/5eFqRQWGm7+2vlRe+0QFpraHyPaOcB+57P1dkZF8dHVGVNMhziIVDKoJSAZsni5Eekpx4tEj9IEMIQju7S37rn2HA/4Vy/Sgz/4xCAux1uztHXt0zXOiNw4oasNIzcvSEE63hu3XYjRKe34NwGVqwTKV08J7p2EMckclVxl/BywmyrwsbKHQTbILBUZcl0OqUonSWYaQ2D4YDJZMJSL5BK0+v12N7aQnUtNVorBoMhqrOMvbq64uDwDkHos5wv8XyPonJ93Ccnx1xdXvDwtUcUZUGz72K3/mDgYtqiwPc8+r0+VVU6f+osYzadYa1hy1jnL+577O/v86Kjsfd6fYqiIJSSfgcqm6ZFStWJszpKPgIODg/55JOPqJVCa82oU0nvD/qEQeD6usdbBFFEU7fs7uxxdPScdJnir7SGPJ/zszOGwyFzY0jTFGxNWRQUZYkXuILW5eUl4/GYra1toh0Xn2fpksnlFYNhH6kU/V4fhCBbLtna2iJNl8RRRJEXtE3LdDplZ2ePIstZzOZ4nk8Uxfiez9bWFpPphOUyZTq5wlhLXVYgBLt7u9RVzXA0ZDpbUBQF+/sHLBYLJpMrN9d1z6UXJyeAi5+n0ynz2ZSydP7pURyhpGQymfDhjz5gvLXF3bt3uJpM3X1rLHsH+zx98pS/+6/8a/zBH/wOpydnbG2NsQYuLy5dT3gYkC6W/M7v/C6PXn/Ev/M//3ecrdwrgw1etWJz1HzJNhtj8Weey750pz/18tUr3k2DkA7gWGM6ERgNHoBTOq/KnDLPiZOEaDB01XCg2TpA3H2H5dkzyCbYfArlkjqd4YVuf07HUSO0oN8fE4YB2tNIJV1PYNugpUL5CkFDnmacTDKyRtF6A3b2erxxZwx1yuTsgsUspWo1TRtSls42aDBMmJ0d8+c/+ITvP5syqwy7o4h/5RdfpykyWuFxeHcLqY9oheLzowlbPZ+yKNFNzbZquWoN2fGEXWERwjJSBq1AhoJ+ZKgXU2Tj+k5Fl2GUUnB5PuFwb0CoFWnVcGd3xNNFjY9B+YoXixIV+Nzth0yWBVHo8SKtOM1rvpV4+AIMlmWaEvsaIVxAk2BJhcDXHpGvyeuWtKypmhpPS1oraKVHC8tAelQAAQAASURBVBgpmbcKL/QIlROnMsYNMolw1UXPQ/sBXhCQxD20dvRz3w/ww8gFhNrDtA7E9gYDkk50wRhDGIfoTkHRCkiSXjeBW6I4ccfDuN7xtqMVS4X2NHsHB1RlBcY4MYggXEckbe0o5EnSQyknrGaalrp29hg2CLp+rxblaeq6xtNeZ13nkjx5luH7Xkep99bCbGVeOMs5i/N2F15HYetsqnyfKImxrQFh1xR2z1No2XCYpIzUFbaYsZxeML2YMJ9nGBz12RiDkIIyyzidlDxZBJznksV8Rt24nn/HCIjwfJ/eYMRoaxfRUZKaqmIwHAPgBT5hEECXLJlNZ2zHhkejEmsjPvqdf8jTk4xv/qtfZ/+NN3n8u/9v/uS/+k/ZuveI3/x7/xb7r3+LIIoxTcHOm7/M2ePvU7eS8c4uRZ5STp9xNT2nTJfkWUZVFOTpkrqsKArXaiKEZe/Nv8b9d7/D/dce8i/+9/7H3H/jbV48+5CrqysWF1dML87RYUrY69MfjXjjrdd44+2HtC1cnL0gO33C408+Yufu67z26BBbZXz+e/8xe9/423hB1F0TujYAZ6MGLtHnFOyFs6XrBEcQkvl0QhAG7qEaxaysQbSnHVVdCiemZg1N5ZRBnYqpIooTpx7/cy4vg1Y28eEaY9tbW7z8LFjVxOytXs7rZaMIdF0Iv4G9roGu2NjmOk7vgvuu4iduv09XsVsF8Jtl+tvFsw6MXQfyXZDIZu0IVrTa9fpN4Gdv1trWgNyuYPfmZbh9QVZXy/1rjGFZL7ESJB4hoIXHJF/y3fMn/PPnEVfhAaYV2NaghWIrklxlBmEkTShZVBLbGvq+4Zd6M6z2nWOD1GvxMK/7E3qayPcJPE2knTWYVivQ7fq4ded8IJWbu3UHuJWQXdAMQtgNz+6b/65uhFUy4/Zys9K5uiabN88rxNTWn7z9e2y+3rzJxPpf2QFEY21nOyawws11QgqUUbRS0kjZCcQ5ltS6B1yv/niUZe7UlpuGsm1om4bWDwh7dxgFPbSE+dU5QmvyxRWP3vyrFJlT9r04P2O4c4fTo8+wVUncH/Lm+7+M73m0VYb2AqTnWpoQimS4RZXOUEpwdfwJ4g5EvS2XHVq1sIkbV2X9HLIb1/L62q0+9wpvcCFu/QId3Nq4zq+aM1a6B5tbXX/kRkrtxs9juT2Cfww4XJ3GrcPcEFvbvNf+AsH3q5efHNi6eahL7HT3kOcH+L5T766rkrK0RNE+od9jN+wz3NphcuYYEVWRURc5cj4n7g8x6ZLLxx86KnKYIJVF9/YY3/8ae699g3h80AlBbozD26f7M1yXV8/6G2mdVXvA6mEh3NRurIsnDRLRxQ1CurYeacxaZ0EJtW73WCUohHAJMFYgXSqE2PD+bp3yuGvT6BKAwonAFnlOli5pLw1K+4RhsG4V8QKfJE6YTqcsFwsGgz5lWZL0XJGoqmqSXkIYRjx69AipJGVREYY+i2WKVpoXL046pe4GKSSfffopWZ7SS3o8ev0NwjAkCkPGW+O1aG4YhhzeOeTTTz6hamrGwxFx4vzEjTX42hVxZrMpF+cVd+/dI4pCTk+deO1iuUQg2N7aduJuVUW/P0QIuH//PnVTkyQxSRQTHN4hzx9T1xXj8TaXlxc8f/4MrV1hxhhH1bfWCU8uFkvCIGQ+m7O7t9vZKAoODvbRnubk6Jhev0eWpQyHA8LQsSaTJCFJ+rStYblc4nUYan9/n8UiYrlcUhYlJycnHOzvk/QS3nnvvbWyfOA7TY2joyMn0JsuO2tWyfbuNmEQksQx8+WSD3/0IW3bsrWzTRCEHBwesJjNmS8WeFoRxzFxtE2/32O8NeaTjz8mTXP29nY5PDx0LDRjmC8WBGFE0u9zdHyMlJKzszlb29tYY3jvvfdI4phvfuPr/J/+vf8jJ8fuu+d5xnKxWBe05rMpv/1b/5S//pu/yTe/+S3H5nxpmPykOeJmEuuVArdfstyYOX9cxfxGNPWzLV8ZeK8seVYg29n4ONEGz9MdIA/Qnkdb12As2g/pjXedgmldYZRPky3I0gWB7yGWE6RtqOaXyLYAU+MrRas188YQ2wbTOsN6KyyzrCK9qKmtxIt61H6fXl9zfy/GExWnz084u0ypW2hb7aqmonXKx4sZjz+ZoqxhvxcQvjHmH30w4fFZxqfnC379l95nVrWcH73g4V7CB8+m/Af/+M/5K28d8vmHX2Dyigd9j7FpoWjxlGA/UYRBQ+NJrBKYsqasDJkxpI0gt059d7cfYW3LZJazMwyZLgvaMuOtccBymfLFvGA46PP4fEbfq3hjFHKe1QRScNE2fF5YDn3FSds94I1FiJZAgpaQIoh9HzBUbYsXhtSZ65nEChqhEZ7EV5rW2nWfjBWgjLt9jDWEvqNdeZ5PHCVEcULc6+F5nsumxQlxnCCk6yVH0AWJgiiOHRvCdxTyqirpJUO0p529T5eA0FqhtQdSumRNVbnAxQishV5/sPb8jjo6sLGO2i0E+IFP0zi7Ns93Im7GOFDtAjtFGEQQOFEupST9wQAhBCkutpLCZTStda0TRZaifR+tnPBW3fl2K+Wo7FVZ4XmuPxMEbeuywaGqeDhICc0F5XzKcnLOfJozX5SUtaUu5uskQZrmPJsYPp9KlnlK1albrh6CcZJgWkOc9Blv79EfjhltbYEV9Po9wjCkrp0lmla6S4Aodne2eHvbeW6ffPJnnE0brhYVKkmwCM6OHnPw+js8+vZvIPwR6eUT4tEBfhDz9rf+BcIo5E/+yT9kvH/I/oN38HsH7G09cOO7NZR5xuTijMn5C5qLGUVZcXE54eMPfsi9d7/FzoN3aNEcP/5z8nRJUQuWk0uQkqeffYwXRsRxQvD/Z+7PgmzZ8vM+7LfWyjn3XMOpM0/33Hv79nC70Y2ZACgCIgmKlISgHRAZDlm2bMrik8MRfvGj3xzhcITDT36T5bBsP1C2Q6YMmATAJjE0ekJPdz7zVPOuPeWcuZYfVu5du+rUuX17AOWMqHP23jmvzFy5vv//+39fFDLa3KTb32Rr+zJ7ZcPHH3wXqTx6O7dR4Yjxyz1MXVIrBxBk6cLWeEs78KmbhrK1pwjCiKalIKMt9a7T7yOw9PSmqpCOg5KKxXyG61stCoEgWcxJk4ThxiZ1VZGnGXVd/UwBzWUJQvvt3Ew4kwHjdHB7Ng+19pIwa7mo9Yz4WrbsDEY6/y5Y+34hljp3fKvjOjO4XMvInK8fPoPST1XVV2dlxOlyZ85rmZU739ii3c3ahs+gghZYrP28CjyYpcyMaWmVkNYp03qKlIquDMnqjOfJLh/Nn/LhvmbXuUZjHIwGiYPrGMYaNAJ8BxxJox2k0fzqIGczEAgZ4CgXVzq4jofv2do+z3XxPVvDvfxzlcJ1WmuwNtOr5DITbIHpklKuhG0naxfW2n61/apYNv1qPHGOSn4RAF+12qkY25nUIWch5CkgX4Z7LhDoM+aU0bDE3iz1KFjRVzFLGzyBaUU7hTg979MsnLIg3HFQrotyFIVUFGXWClpWqzKDwO+jAo+hF7H74D2GG9tE3R6OEzA9eE66mBN1MoR0CAY9+ltXiLsDFpN9ws6QtKhQtbGZP79DPtkniDpI18fomunefYrukO7mLaTyMOgVkH6lhc6Br2Vt9vJSvE7Beh0ki9Mb93Q7a8usEqDmdO65x21t3ukccdENcX7ceT4Df2EHcV6E769/Wh3WWv91toXWz10ghEIpY1knnmUtNXXdOmAIai0Iom063W2q+T5+3KfKZhgdYGprUVg3Gl2mlGWNE8bMTiZcuX2TskqZPvwW9ew522//Jr3tOyCs6CJGoxz/FIy/cgI/AR4/t+D5N8jyeVu/qkLa4JAQy6RFy/QxEqMU0hjUsqa5td3VulU9X1mO1bbEsGnQ2gJypGX82f1bbR+L+4W1NnQdiiK378ssZTppCIKIIAxRhUNdN1Y1u9fl4PAI37NYoW5qprMJ0/mU0A8xBpQjyfOMycR6WQdBwOXLlymrDQTtGC4IEGKbuqqZTE7AQBiFtmwsCJke7LdjTcGlncs8efwYKQRhFCOlYHoys1lopdi5tMODB/d58eI5nbhrx6SVLWG79+abJOmCsijJstxmzV2PNMt48eIFnutR5Dn9fp8kWTAYDqibCkcpwjCkrEuUFHQ6PQ4ODlGOY2vRHYeyKhiPx2xubaxqzbM0syWSrtPW1LtMJ1N2kxcgJDtXrpAkCUEUEMcxw+GQJEmZTiboRhN3OhR5wXA4xPM9Dg8P0EbT6fUoyoIkSdnYjGiamqKoeeutt3n+/Bmz6ZTpZEKiXGbulCCK0EazsbVFXZWIwApcjoZDhBDMplM63R51XbGxudEGSl36fZfhaAjCnst0OmXQ7+P7Pnu7u+zu7bKzcxnXdWlqW0bq+x6e7+H7Pr/yK7/K//3/9n/l2dNnRHFMlqZUVUUYRTR1xdMnz/jwgw+4d+9NXLfPmX7qx07nUwyf6SlcTa9LcKwt0S7wqaj8M02fGXi7ykW3Gco8z8nSBF3XhJ2YujbUVQ3YOgIc65PsSIcKIM8J4i7K89C9AVGRYXRD6gaUiyn+zog8XVDmBeM8wyxSQtnQBB5F5SFqQVnmKCfAC0KqvKHIaq6OHEInZ383ZV46JGlF3UDkKUxTU2lNliTMpjM6ocOgH2KqCscRvHl5yI/Gmh8+GTOREY0b8OKHP0A5Dr/5hRt88HTCs8M5R+OEOyF8zhXcLgqu+4K4KzC5IU8aDmeCyjdEXU2NYV4ZGmnr3tDYbLJjmKca35X4SlilWyHYP5pQIshLQzFL2Yo9Xk5SjO/SUYbAsZSgj4uGxliRorrRpMbgOg5v+IqnpcFxPKSjKGuBFi6BElQGGqOopcBTDkJK/CAkL3L7UDQNnuNR1ZW9trpBKIXTUpm9Vi07DCOCKG7F7dx2EGcHJt1ez96Owo4cq6qkyA29Qc9uI/ApywKMWSkqpkmCdByEbqiqAoNBNxoV+wSOY18Stc2a1E3FcjBdFmWrWJ5auhSCUhcoR+L7IVVZrjLYTVPTVPVKe6DIMpZqx74fkOfW1kFAm1m1dFR7LM2q3rwqC6S0HXmeppYq6TqgNaMgYyc4gfyQdDZlcnzMdDwjKwx5WVGXBVIqsjRjnjU8mXk8OamYzWatmq+NMouWAiWUYvvqVbZ2rtn9eN5KuND1fFsK4DggbM3T8rHvxw6ROUE7I5598AOczmVcP6QTOHgi49f+w/+UuNtDmgpMYUXx6oJseoQfb3Dp+h06f+/3KRZHIARa5+SLKbouqauCqijwZMnWyKPf6dGJBtQILt25w8nxIS/vf4/xwUv2Hz0gnR/SyJCyqBls7bBINWYxZhYUYDTv/eBDdraHbG5v0h1s0KgOH390nzfdkN7lu+yMYnxXktU1QggarXGMHWzoVtTE2oY1GOyAwGBFBas8twEJZQcmddNQ57mlrJclYRSSt989P0BISVkUlmfj2ijxUkjv5zW90o8vB9ytQM4SXJ3OO5spWytoXpt17sWyRg22X88NVc+j1NVBLZdbgtx1GGZ/P93l+dFwGwAU5vxWVmEE0dapn9nq2nEu6fOnCs/rtbDtEZjTz8ullqTrpdr28rzkmkCVpuE4PcKVLl2nw1F2zIv8Bc/Slzw9Stmvb2HcaLW+qwRO4IBS1NOMptsBrUE7dFTDVzolUtmyGd91CRwX33XwPQ/PtVnuFeB2T6nk7pJKrpTVxFiBbdo+bAm42yp1uQphsKpbN6smP3OdT2s5X53O/2wH8efnvQaonQPdq9tlCbqXdOx2hsX0LQQXpgXfBmlY+X7rNrgopETI6oz6uVQ2OGYzc5bhVqqcsijR2gbarCucIYy6bF57g/HeY8aH+3hBTBh3SCYzav0cZUpuv/UldF0zPnxBU1ekaUI2H1vVXT9kOj7myr13QTo4ri05KdMpxeKQZHJIZ/MmncEOSipWjJBXMs/rYPf8tB4VW3tWL2zU5XdxptXF6vlcNvL67+0GVjfFelRGrO3rQth6wT0jLl7ygkP/mTH4p4xXl4Gg84Pf1+3Tah3YEjjXeGjfJoGapqLIC3RTWhAed/FGt4g2blAmJ9TpCY7rsBi/ZL6/h/I93DBEmobtGzd4+ewF1+7cJV/MkKZhcfiQfHGCIw11kaA1RFu36A6vodzgtFsTP/6YP8sklvRyzt1t7cMvaYeVUqz6VyMkUlrw7Ti2NE+0dd32v3VhyvY3zSq7rZzGZrlNA8j2jrDPo1IG5YDTaJrG2D/dQK2ZTU+YTE5wPY9ut4fv+yyShMGgj9aGg8MD8iwn7sQoKXE9h5PxpNVXcZikU7Isw/OnuI7D9qVL1FWN73vM5wvquuZkckKR58SdLsliwcbWBnt7ewyHQ+tRXtekacrVa1cpCis0trlhhdLmsznGGC7vXGJre5uT8ZiTk/GqDLKp26DAdMrh4SFN3bC5uUndNEynE0ajEQ8fPOT5c8uEe/HyBa7nsbW9ZRMzjkM38AmCkDRJ6fV7HB0eMplMENIKsPUHPT54/wNGGxtsbm7y/Pkz4ihCICiKkm63x8HBPkLY75tbm2xubTIZn3AyHoMxJEmC1lYkr5iUuJ5VOE/TjOPDQxpty3y73Z4dK5dWQG8+n/Pw4QOKPCfNUjY2Nrhx8waHB4c0TcPVK1fI8xzPDzg5mVDke+xcvky3GzOdTlnMF9y+fYs8z5hOpnQ6HWv367mkacrJdML46IimrukNBkgpuHzpsr1PpaDT6zCfzri0vWl1lhzVsg5CjsfH1E2N0Ya8yK1bEILZfM7zFy84PDxo6eafBXB/2gP117z8Tzl9ZuC9HKBq3VCXFVVVWhEr5VI2tkYLY22WXM+DEpqqQDeaznBk1QnLHGUgzxN0VSIdn7o3xDQar5ORzqfUxweEgyGe03opVzVNsSDsDJgcn1AaSeRXdEOHrBIcJaBcD6/rEegxpra14YEw+KLg0WzCw70pJ3nF529s8NblDt1uj2hjxK2dOT94Mub+k33ei118X3GUaD58tse1UcB4kqKaGllA1AhGRuAIQ4lhWhkyYehuSMJQgIZK2qyA6wlMA7UwVtCtMaSNpj/w2T3J2dno0wkUpYEXSUWJpCkSKs+hMZBqTaVh5CvqBo4qDRiUFLhK0kiFRjJ0BQ9qhcBmAwsj8Ts98izF8SOU5xO6XutrC0VZEgQhVkDD0kA914Jmz/dtOwYBnU4XqVyiOKbXH+L6vvXaDa192FIhssgzS8/p9fDbum6BsCA9cm3Wu9vFdT2bxXYcok6HpvXCdl3P1igLW0OilMJRtn65ritoDJW2dl+e7wECp7Y140gLvIQRp+I+TesRLCV1XeEFvh28tsOauiypKluyUFU2alkWufWbbcXUmtp2Wp7rIoRiMZsTRNZmoixLwhpuDDJG/pwyOSGdHnNycMx8lpJm1kMU7ICzyEqO5jkvqhEvThJm06n1Sa9th6McS3EebG7T6Q2Iu0O6vT5BENpOyRhcp63njqKVD7rTlgnUdcMgqFFuh4ff/Tq9a1/h4KMP+f3/6T/l+uUusplSlickB88IeldwwoHNTqH49n/7f2Z4+6vcuPc5wiiGpuTj732b++99n/7GEHTN+GSGMSAVuMq2Yyf2ufulX8WPhjz+/teZHh8ynibM5wkYhWk0ZdUQxTFX79yjmO1SNB5NtcDUDuki4WXZMHv/IVHksz9P6fSe4XY2CJwG0pcEw7dWNm/dnqV+ZVlmfTddF7C+xq7nUpX2XllZwrV0Uce1969AUNclynFp6toGDLVZ+UXWZYkQ9n5p6p+lxvvsqPI0O3MeM6/9cCaVccEmLwr2roNgsw6Tzm7ADtouSB8tB/XnxuGvxorXRsDtiHi1h1UG9XX5vdN9vpo7PXdKKwAnXpm5LG1eii2JFtQtIc9SUG6dXisEpGWOq1w6Tocns8ccFPscZGPG85Rp1kEEIR4OaSnpKEUnlijP4TgFoRXGc6HWiFxwu1Myiqywl+95BK5H7HoEbQ134LWA27H6DCvA3QqnCckq0209zU8DJa0V9tq5L9vM1g2vt+0ZK7C1pntd258R7zp3KU/X/RTkfm759Xmi3ahZ85Bf3+8SDCxr9QUghEToZbbfOiwoZbNbK99v5SAdicodlLK+33XVUJVFm7Fr8L0eve3bpJNDHGXIS+hubPHJez/k13/n7xKEIeODZ5imoapSiiSlO9qmf+kOvu8xvHQLL7RinlK5FOmUpswo0ilFllGmY+r8DYY7byCVy2mA7FyrnxFUONdKF4DbV5S6z3QMr78McHr/v4rwXn1mzvcnF8mlnYLt8wdwwfZWv/7sWZ6LplV3tB5H+nG7afsCKaQNoDhgvBZotuJhRZZSZqVVtq4rwk4Xb3CFYLCD2xmBcGmKByTTCUiFFuC4PvFgg+n4mNHOFasNUqWcHL7gw7/4E+LAMNreIhjdwOvvsHXvV+hv3bQll+dOyqzO7mxg9Px5rC7belctxErXYhVg08uFxOr5E22WeqkNIRA0Ytk3rv8tn7ul7oJzavvX1sfbbQiWwpe6PXqNZUVaDVuJMQIhHYQsgZw8z8jThGQ+p9aGXq9Hkizo9Xo2CCCwIrWex3w+x3EU0+kEx3GtVWxV29KvVnkdYDab0+/1iDqxZRyWBWVe4vou+/uHbG5sEMcd+r0eL168oNvtoJTDxmjEfD6nKEtu377Ng/v38X2fLMsYDob4vo8QksV8ThAGHB7sk2cpSZZhdYQChICT8Zgnjx6xvbXFL/zCVzk5GfPo4aNWvdwhSVMu71xi0Bu0omkxURyRJhlZmoKAMAhRUlFVNcPhCIzBd13iOEJJxdb2NocHB+RFgTbG1puvWE6Sm7du2pruqEM4HtsyGVNxaWdEmibMZjM2RqOWZeqzfWmbjz/+xGa2p1OGoxGXr1zBdRyyLOP582fWbcJx2b60w2Kx4IMP3qc/6LO5uc2Vq1fYfbFLkizI0oSyrJjNjjk+PmZze8sGjoW0Wf6q5Onjp/i+z2i0QbfX4/GDhyjX5fKVy62COxRZRrKY88nHH+Eol6vXrq+Yqo5yGPaHLNIFVV2tRHTDMERgWnHd833vZ5k+pVM930d+Svf3+unTFvrsfeRnBt4YTdPQ1lfUeJ5PulisBLSMAKkcK1jUAjNjNJEXUuY5TZUhBJRNgx/EFELSDWOqsqQuc4rMJfY8on6fKi/xQ0sdasqcPI3IFwvi4QYONko3aaBqSqLYw/UDTF3iBhEmLanrnIPxhMVswmSR8GyS8PSkYqYd/t3f/mUO947YfXCfr10b8kffecqHT4/48Mkh//3ffIdBHCAwZGnBhtb8EvClCoJak2nDYwOJgL4j2BlKhCcQjkEryWEBGZJYGrK8wXUUlYBJWhOFAUZ5lOQczFJuX75E6PuI1FqzaQSLymbyDIJJ1XBVKZDWXk1icKWl6UxqhSttVttIBUaTVpoGCIy1zgiFzV4brEhC1VgLJq0NnudSaytGpVwHP1h6JDsEYUQQxitfaOm4+F6w6qjLIifwQ6qqIu706HtuS0V3KPICEKRpisDgKIdUa1yvIgwDisJmv4s0xfP9FugYC9hdK/JWmcrWeLu2jtxraemWGmUBqxZWkA9hPbuXYn9GG9AN2pg2KyxboGlrg43WKzpa04L7pmlQqsapaxazqa339NzWfsuqZ2eLBGM0fb/h7maJb6Zk42Om4yOmx2PLtCgbytzWmTW6oa4MuwvBXj1imlZMxsekSbIajCrXJe528YMYP4iQymsz2uD5PpFrVTyNNviBh3KsWnxV1hjHnk+jNU51SB2MePbRD/ml3/sn3P7SV6nTQ47v/wWzyYTBlXtsXnsT6pyDB9+jqDTX3/4ae89f8K1vfcy//5/8j+n3e/i+x70vfAHluHzz63+MbFKOpgX7E82v/O4/4u6Xv0qVz7k0ctnaGLD34K94/vABu4dTFqWgnB7S6fboj0bcuDJESfjCFz/Hox9NufLmu/zLP/wTNnsho9EGRWmYLBKOpxmTeU5n95Dh1i5qc4Q3fYyz+XmrMKscay+GbTO3VcRvnIq6acizHN8PkMony1KqomBR5KuBRJkXtquUgtnREZ7v23tB22FFkZf0R0N0Yyl4fhB+5u7wddNP1Hf/2GDuKTpeJrZOy/7sh/Nd/RJyXbhpcX7XZyHTRXs/k+286BzajZnXzBPmHPReOlic58afqcU6i/pWpfIt8BDrYFucPYul+vmiSvhk9hHH2REaQ1E0jBeaxu3hNhF57hIZyVZXIhzBsZY0ixIT+gjXBRrQDV8eOYSewFUuvucTui6R6xJ6LqFrM99eK57mKGU1MZaU8jaguBI9Yg1giCWYXpKabWsv8ZU4e2IXJi/PXPszF+inzBIsb7e1S7L+fdXGy+Nf2+nqWIRdUbTieEt1e5u6F23d6LJG99T7e6V43np/L9Xii6KgLivqpqLJNLpp8P0O/St9VJOQHD9FYLhx+xad/hCNYry/h5SGuNth68pNhBdT1w394YC6SKiKFC+I0HWFcn3qMgPT4LkKKQzZfJemLhns3MXzO2eadwWoVtegte36tFKT17b3eUBvTj+u9te+wFYiDp8+uFveN6exvdM69Quu1ulaK8YIZ5Y6v92fafqUOA+rkA0rIP7jtmXBt9UFQghrd9ra8kmlyJMFZZZSFzllkRPFPcJOjIqGDN74GsFwh8XeA/L5CWWRky8mbFy6zGx8wmI2gzpHSmktmnojkskB/iLHjWY00vD0W/9PhtfeYXDlbTob1xCtiJh9zi141qaxxynlK6dgVv9cEDcRtOU6p+wisFZZS/YIWmNa6vkStNl+5bTEY7msbRuFs6atsNRZUK6Dl1svZiFsWSl50WreaLRzGgkVQiLL0+dWKscC47JClyXT6ZSD/QPCKKTX6xNFEUEYUIRRm1316fa6KCmZz60ta1lWxLHH4eGRZV4KrADvxCPudIlalfHx8TFgkEry0UcfopSi3+uzWCTMZlNb9hN4DAcjwiBkY2uTTtwlCEPyvKDX7SGVbF1yLAOg0ZrLl3Z4/uIFYRC1yamA4cYGQkj29/YQAnzfWzmhDAcDMJZGPx6PyUtb/ialQxTHjMfHmACOj4+YzmZMT07wfJdur0tV1YTdyFqddrtordkYbQA2Mdbv2yzv82fPyPKcq1evMxgOefzokS2DrCpMo1nM5xR5hhSSO3ffsGNpo+n1emRZymQ8Js9zbt28uWJQ6Kbhww/ep9fr4/o+165fpywKjo+PqcqCg709/MC3OK2saOqGKI54+eIl/UGfZLFgf/8Ag7aswRIWyYLxeMxwcwQGZjPLHujEMUmaEAYhnW6XMIqo64qXuy/Y2tzk4aOHHB0fWZFl5YAxxFFIJ4ooy4qjoyNu3bpjGdSfZbrg/XhRn/Gpr8afObb42Vf+zMBbCJsdQtka3bIobAfnKJvhNrQ1vBZwG9PSzz1JEEfopl5llqo8oyhKtOtQFiVeENGNYtLZFIwm7IPnB5R5RplleL0RYbpACqwIWtPQlDmRqQk8l6oqKauSKp2xGB/gUzGIJJeG2wwjl1vXp/wf/uAD8krz4JMndEzObLpgd2/MnY2IyYuEBsE//8aHuI3mqtb8Q214o4KyNMy14SWGEoE0MHIEQx+0FMSxYlI1jPOGk8rQ61jquBbQALWByhg83yPq9ojmGXleMj6eEYUSpgIjFBaCQuTYDLkd3EhmDXhKooSN6NYogjCkqWu0AISi1g2eG1hqv9YIz7PCElpbMAcEjg9AWZakWYLv+QRRiOvamhnluCjX1taq1ibMwKruWUhBkReEoY8fR3iNptPtYjArr8VkMccLArw2ky0dBVqvIrTWXzskCCNo7bOWQjFWQ8CjLm0E0FTW/08Xc8okxelt0NQN0lE0TYM0iia3/o9lWVAVlo6kpKRu65equm4t0CwYP62PkgRh2HoJ65ZC7uFtblIVRZvMEG1duYPRJZd7mpuDDIoT5pMxk6NjppMF2SIlL0o7ONQaiWK6yDlq+uyVEScnE/I8tR6IwlLBGqPpD0f0Bhv0RhtcunzNZp0NOEpR5JlVW3ct9X7JWMjSFAFtHZN9SQZhl0ff/wuG197GU5qDxz9kerjL8No73P7a36BMj8hmYx5851/z/jf/jHf/zj8mXUwo8pzPffWX8Dyf9/7iX3LnC79AFHjcuHuTMPrb/Pkf/xsG5oRFkfDtP/1X3P7SV/ncV75GWB/z7Ef/msPxjE8ePEU6Dp2dOzx5+jHzecrxyYwoDLm8c4mrfsC123cJPMmv/cav892/+Fd8/tabPH9832ampcRzXY4nGdOTI/yoCweP6d0sMcJmEI3R1spNSlzXMhNqbHACaOeXVvVeCPwothR0benmGM1iNrOCVkqBFlYRVynC2GbZsnROXVWtivRPN62A5HrHfmYcLc7MXoKUs++BUyhz/v2wxKUrwa21ddoD4Iza8fIlc+HLRpxdbn2va4PeVZ32+dVfTY+/Mq0DwiUwMSuKOmtA5RQonIpti9WxrI/AV0Pz5TY5BeD2NAy1rrk/+ZgPx+9RNVab4SRL2Jud4Po9dB6T1CEODpf7Nts61VAYgSgqGERoKUEKIjRfGLhEnmd1L1yPwHWIPIewren2XCue5iqb5VayzWpLiRK0me7TU14NyldBA9FWVLIatF80AFiSDMSZf5czXjdiOB9eOX+RXqUZw+mFWAHKc6J6q7ui/X1ZC7raZRslkkvw0Iqv2SqlZQZOIZu6zci1A3mhWgcMd1Xy5Dg5hcqsc0FVk+cZdVPhuQGeHxBv3aYoP6HMFlR5hqAh7HSsO0fQQ7ge2eyIa7ffJp0dk85PcLyQIO5TFDmmKSjSOdKJCDshGoFyPapyznj3PmHUp7d5Hcfx2lM7hbIr6QOxbOfzF421oNSrV2H9EWVtmfNjSLP655XLd7r9c7h4CdlWl+TMA7u8gusrLTOzZ49m9cy+El37GSezdkrtTf/jxsXn14dl92ADX0LI1oHFw3F9XNclTeYUWUq+mFGXJXWVEXZ6+MGA+EqP7uV71EVCmc5Jj58z33uM5ymKLCcMPY53n+GdjNnaGvKD5y+ZpUe8M9jCCzSB61HPn3H48UuynXcYXP28FfJTzhrDRbV2iGb1vJ256J9y0ssyjlWfbGjHArbXk1Ksni+jDSgQ2vYOK5VzaVkljlIoR7UaO87Ke9rzPHzfI/MyPNdq5uRZRuZYX+qiUChVUZUVUtXte1TiutYRp/IqitKnqiryvLClIcaQpimz6QzlKOIoptfvEUcxSMF8OmtBuz0m17UsxspUHB8fopRDGIREcUxRllza3kYgqJuGbrdLr9en1+3z4UcfcHBwYNmB2GTf/u4eRW4tSX0vaF1noKoqFosFV65cxhiYz2eEQWiFgaVYWZdNjyb4vs+tW7fY3t5mNBxyMpmSpCm+H7C5ucVoY8jx8ZjZfN5m360XeZqm1FXN9Rs3mExmuK5VWB8Nh3b/8zkGmM9m9Ps9hLDlblcuX2Y2X9DzPfI8BS24dv0ai0VCFIU8efKYxWJBEPjs7+4SBAGj4bAtrXSIOzGHB4d2zOj5eL7Hw/v3CdKAKAzAQCfu0O33OD48ZD6bkmYZvV6fpm4IoxAvCNjc2mI+mxF1u+jZFM9ziaKQ2WzGbDKlLEu2trcoy9IKwW1s2uU3RswXVnE9XSTMFwvKwpb+LWqbnK3KkrzI2bl8mc9/6Yt8+KEV4pWOfQdHUUi302Vja5Pt7S1OTk7I8+yzA+8L+4cLgoU/a/Dw5zR9ZuBdlBVGN/it2FFdV0hjqb5NVdHt9Wi0ttRdpdAIvCBEStGqJWqMbmytiRCEcQeDoN/p0xaiIAYKYzRNVVmrKuUglWOVjcPo1N9X2qxHtZhy9PQ+Lz75Ec9fvuTJ3piTRcq7N/r8/m9/hdksQdFw9/KI21sdHhzO+S/+4Hv8J79+naIoKI3LoligTYMvBW+g+OWq4U6uEYUhM4apEJQCQiQjYfA9Re0a6lAy2HCopSSrG/YKg1CCwBXsF9auQktBVht6geJokhJ1u1zZGvH45QG78wrf8bg2FLyYF1RCEjqSRa3RQhI5UAmohSSSilgacqFoDGjlgHKRorDZFS+kEbZG0dBSHl0PnecoKWl0W2unDa7n4uLi+T693hAvCHA8H8dx24yrJO50iDvdVkHTgtS6qpASwtBaabmei3SW1jCKxjRsX74CmDZzYam8VVUxHR/ZY/JjiiKnKnJbU9vUlnIUxacZaG3LFcqyoGkKHMej0AmL40PiXt/So1xlPbh1g+/5FEVurcSMQdfWimw2mdId9KnKgrjTpW5shj9PU1zfvmDKssJxBel8Rtf1aFoNg6oscFzP+os3BVdHmoE8op7PmB4fMJ9mzBYZ2SInmyfkRWnbQGsOk5yxvMxhIZhPJqTpwr6AlcLzwPV8ev0Bo+0dXC+g2+sThhG+H+D7PlLa9ldtMKOq7PMU9WKSuUZiWSe+74OukY7Hi/e/wb2v/SYv3vszThY17/zK36euUhbHz3CDiAff/EO++6d/jEAwvHyNbHqIEIIv/9rfZH50n+l0xg+++RfcfecLdOKIzc0Rf+fv/zYf/PCHBA8e8smLE0xyRCSv8+y9P+PD7/8Q/+oXkFEfUS0Yjka852ywOHjOm198g2s3b/Hio++zmC/oDYaMVMSlq5d4592vIlyXOAy59/Y9Hn7yiHmi2T0u+VKj27q1BtmkGLeHMRop1UpEbwm+0ySxqqa+T9NYyr7RZuUv32gNUuAIqIq6LYFg9bKP4hglFWmSMDk+sgOANivw00/LkVH7eW1Abr+/Ovx+bcbyfMZnHUCfH6itQPJr1M/PpCKXv5lXfjoFwaeA90yu+hUAvDaQP/N+O7uf9XXPC7etL28B6fpvYg24vHpSp/Tp022WdcH39r/Do8kDYiemcB3ypmaRVzieS5ELMh0zr1xudO0AcqYNE+3R1AJpHOtD3WaPtl3JTuzgOxLP9Yg8l9CxoDtwFV5LMbfZbonTqgDLZYa7RdvLWu5ldhtB6819JgSyOvVlMJIV1W4NOq2adH2di0HdCsitX4dzxeIXJQpOVbcN5+7SFag+s5fViksBtlOgtgwY2D+FURqhxRlgUMsauaSd12pFiVTKQUm3VUFX5CKjKkv7bqgb6saj8X3irds4jkuSTFHYLN3+7i4bRlKkGRuXLpNMjzh68RCUS38UcLD7iGR6TDo5IF+k3P3iL2KMQrkhgsb6QhcpxycvWUyPuXLnXRw3aMH2MirB6l65qBVXX9e6hbPP43kzr+V1Nq9sYvWPODtvfe3zD7UwZ49h9eXVh/+105kyiJ/ndMGY2P68fu+dm851D2ubAmETBsvnT7ViwK4XkPszsmRBXZWkC0NTa8ogxw8jPN/H68TEGzcZXvsc4+GPmL/8iOTpfQ73TnD9gKPjlziO4o23763e81lZQVERCk2nt0Fx/ICjbErVaIKwixt28Tsjgs4mygtsLbVqtWTWz32t231NM60CL0Ks97vYzLYxSC0wSoOWNigkzMpiTLeZ6Ua1YrHKoXKqVhTSalQUrWBZHmb4aUoaBHh+Rp4XZHlOkeUUylp21k2F6zpUlXUeqN0Gz6soqxLP9Sh9H8+3Y7IiLyjKgmlLf1ZKMRwO6fX7+N5p+WGv28NxrL6DpSkLkjS1+gtFQeB5BFFEJ+5QliVZkdGJOly9do3jo2Ou37jO/U8+YTFfEMUxSZIyPjkhCkP2ZjOm04kVgtaak/GYze0tLu9cxvN8jo4OmUyn1GXF5uYmZVUyGZ8gpWQ8HhMEobWC9H16vR7T2ZQojm3JpTbUdW0TUEKwv3dApxPjOC6L+YwgCHAch9FwSJJlCGyG3WjNw4ePuH37NnGng+f5JIu9U5cWRzGbzuh2e1aPKMvYuXyJNEnRjcZtxSirssIITbpIODg84MaNG62YWYDveTx+/JiT8diKGYtTIWQhBWmWAgLPddmMN3j58iXJYkEUhZwcHwPQ7/dQyuH46Ajf9xkOhxisNfKNmzeZzef0h30cxyGbz+l1O7z55pvs7e1j0IyPx2R5xnh8zKVLl1BKEYQBN65f5xd+4av8+V/8OVEUURQ5gR9QVgU3r18DaAW8f2wvcu5B+ZT560MxfqIu8K9l+uwZb2y0rS4L8izHcV0rlqIcHEeS5RllnlPXNWEUr16sTWNrY5d+nqYBI+x6Xkut1Y2m0TWNtnU2jhfiul4r9mU7BUtBETS6IZtP2fvkB4wf/oj5y0f0nZLtKz5PdjWTrOZbD4/5rS/PCGXD4uQEJRuuDH0+OVyQNJqvf7zPtNAcZQknacUlz+Hfcz3upBU1AuNCGDiECHpCohs7bKgUVJ4ABV7XYS7hcFFxWGi0hJ4PjmsHdBr7AElj6EcueWJ4+OyAq6MuoecyK2qSsqEfh8y0wCs109pQCw+NxleaGkuZ6rgSx9gBlPQ8atfBCzoUZkZXucyc2KrBG1ZquUtGwjISqKSkaCoLQoTECwLrUynVSsSr1x/g+QHN0kMbQRjHeJ6Po2zHKB1FU1UIz9bMlnmG6/nUdU1VlARRiONa5Xs7gHLQvQFCSrIktfW4jrK2X8qxVmDGZiCXarZpkrbU9QyjrUq167ksVTzrpqGuK5Z2UU1dt97fGrelJ7qBb6nqvk9RFvieZ6nugON6VqGzbnBwiDtdpicnBGEItB2bUoQi4Xp/glcfU8ymTI7HTE7mJIuS2cmEqigxWJBXlBVHpcuxvMGLgzF5kjA9Gbe0N7lShQ+jDn4U40cdNja28IOAqirpDYd0Op0WpNvrBTAcbqzE3pqmodKa0HVIkpRhR3Hy7D5FMufk0Q+YJAWbN77I9MX7TMf77Hzut9h7/+s8+uQj8rTCDUI8PyQ9ekqv36e3cYmjJ99H+hu89+0/4+MPnvA3fu8fMZRWQf5zX/wSly5f49e3bvHG136DJ9/+58wyDb2bPP/hX+J3uzSNJHQlV+6+w4Hrcbj7jLu3LjGdnJDkFQiom4q6Lrnx5tsUyYz3P3zM5kafL33tlwg+vs93fnCfWVqzUxU4nQG6zjGqQ17kOI5j2QnGoOtmJfDouLYEQShDnqYEUYTE2viVRU6aJHhBSF3a+41lFlJZkN7QMJ9b2xEHWtbHZ6+8eX0vefFHcwEqfgX3rmaI09/E+RG2uGDlU9C6HCib5Wj/opfRKy+cc2DqlY+v7Ox00xdQbMXacrwyd7nHi6D6OTVlc5rpf+UlbM5+nlczvvPyG0yLCaNgg0IUuPjszp5xUk4ZRRs8TTxeZhE3AkHsKBIEmbFZ1mpRYrwAdAscDdwKFN3At8JcrkvoOramuxVT85RqbTUlrmxF0pY1lcvLJJZBhbXzO8NaOOf9vN6GF4GecyD7olDGK9NnAulnv59es/OlAmv35gX7sZjpVAF9eUZLeyyjJUaeAgNLi11S82tqIdvndEllbQG4s/QgFq3tWIXONU3dWKXjjVuoJkOaisBAXZV8/KPv86t/6+8jlc18Huw+Ie6NKNMp2kjyZMqVm/foDTZp2ncjboSuEkxdUDcGrWuqYs7B0/foblwl7m22gOfcvfopozhx0Ze1AMj6U3K+Fvu1mz1/a5wLQi2vx7nUcLuKOV2F19xDq27mNIjy1zt9llHwRUe8vgWBFArhiLZUyY4/XN/H8yOyZE6eLkhnE/LUwQ8jgk6PMOqAtBng0Y0vEfa3iLduMHn2AZODl4w2NpglGYukZDiI8MOIIk1xw4h0MgMsMFy8+JC9J08IwhApNYHvEW/eYHDzy4Qb1/HDLo4XIJCWgXemH/30lrH9oGBZcmCfyzZ40wqrCWkFDrUxCG37Iy1b8N0Cf+XYNnFc11qqui5e5eEHFb4fEPghYZSRRzlZnpNlGVnWgu+yIM8L6rYmd+n37VYuTmUdYPzSx/d9qjq0ya2ipChLqrKkKArGJ2PGJ2OiMGrdZmA6mRD4AVEc2Zp8v91GVVE3Nfv7B/hhiO+59PoDFrMFWZrT63bZ3tpCa8OVK1eRUpBmOVprZtMpk+mE2WxGURSodnyZRwHf+su/5O3PfY7hcEhRlpRFwWw2RyhJMpsx3Nzg9u07jI+OOdjfxw988rzA932bPS9KkjRFa00UWxtSo7UVDTs6QiqHy5etyNjR0TFN3eB4Lk1j2L60zcMHDymKnOPjY65cuUJZWRedg8ND4lbLp6wqFos5xsCVq1dYLFL6vT5om9g6OthHCsHGxgZlVdLr9vA8j+lkQq/fw3U9O68o6XS7RHHUBiY6ZFlCMl9YhyIhKOuGqiyZTCf0el2uXLnMZDIlz9vxVxAQBCHzxYLpdIrjOFze2bHlRE3rD45hyd0qioyyqiiKAs91maaZFXF2XGazGb1un89/8fMcHR/y5MnTVv+r4MrmFUYbG4ChLPO2NPVTno3z3cEr8c9PWfmiiPO/xemz24m1XoHKdQkiq1JYFPYmR1jrLiOsSFdZ2gygMaYVlhEtrcW39mPGRxhDVVWrF7BqVYUd11qSSWWz38bRSEdRFxXVfEY6Oebw2QP2P/wWPRIu9yRSOETdiF//0lUe//FH5JXhn/3p+/xHX7uM1gXzWc7l2KUfe2SN4U+fZdTa4BjDr2rBb6cVg7rgo1gy8RW/qcEUmqIRNKbGkYKFL2j6LpWQOG5DpjQfnsDAFZQSOj5UCDvAFjYa6TmCwCgqI+lGDvPxnFma0igP5UuSNGOgFI0EHENZG4zr4ilJmSaUQuF6AYGsqYykT0Ph+fiej3AcFqKPMILIi212WtusV93UeP7Sc1bSNBrlnlIm/TDCD8M206zp9votjdfWKA2GI5thFAAW3CIFQRhhTGPBuOshlaRur7Xn+QRRgNGGosiZ5VM63RhHOXT7/ZUqYxhHNHW9ApMn4zF9Y3B9r62TUfi+wvWs8roUktjpUNU1VVUhm4a6bqxfs5AURYEx2kYmW8BsBZgMUtnMv2xVhYUQdHo9m91XkqLtMKVUdPv91hNcoSRciRZs+WPq9IQkWTAZjzmZLFjMMhbzBbosMUaDVORlw0HdZyKGvHjxgsViQZlnKNelaTRVUVqVeD+gv7HFxtaOzWq7HnHbYdr6nRpHSVt+4XtIIZhPp62/urOybFsyArqB4tm3/5I8WfDhD9/n2ptv8vKTD3lRHPG53/wHnDz6DnuP7lPmKVmWk1QSqXx0mRN2rABGOj3hxZNPmMxSqhk03TdxR5+jfPJ1RJVx+4tfY/TGL/PwO/+CZ/c/5smLnH/nH/9T/uv/7Z/A/IRbN3aIPEO31+Pmv/sPWRw+J82ece3GNmnlkaUTnj1+zOZ4zPBgn43tK3zpV/4mf/H1P+bF7p9z750vcmdecDDOeLOpkKbBtMIayzIJ3VjFfY0mT1NrxZblBFGIlMrWbusG3YiWMi7pDSwVy3biFaZpkK2FnGms1V6n26XIczBmNdD/eU/L/r8dIp1DkWKV7TDnMlmvKBizNvvC8fhF8Mlu6FVofHaZ12Pzi+e/GkNYnd36V7vuZ3hpvvb1uGyb9sNScOh03/bzolzwV7vfRAnFKNxEIvGbiE8Wj9lPjtnpjSgqyV4xpO+FDF1BbQRT7eIqRY1Lkc2RnSGFAQeDNpo7HQhcF89RBK5D4Ho2071W1+1K0frmrgHvZdapbYd1BfLTUoGlH/d6nf6nNca5phVnmnnVHBddg9f/tL6RV3kFS4j2KlZfXZjVPb2epT3jQbzEbcsk8an3GNIIoM3MCdVShRtkI1u6vmoFN2VLSXRQSlK0oph1VVOWOU1T0fgBQdAhClxQJwy3LjEabeB4Frin8ynz+YzNa3dpsgVN03Dp9jsMNi+xONnHD2LyssRFUCQJyg3QVCinR9gZkSUT5ASKdEJ/8ybK9Vd4+1TAbL2N12n8a0Gwc4PEi0Dvmb7iTAiknWtOn6v18oszgRuzzgrhzLZWruPnYigX9g3m/O//HU7tA3WmvOSVRQQChVBL7QAX17duOF4YEiSRtb3MEtL5jCLPKaKUIO4SRBFBEBENrxH0LtHZuslw/wHHT97H6CPm5YyTw0Pkzg5BFJMlCZ1ej/H+Lt3BADcI0U1DrWuqvKG3eYlk/AJpSmYvfkR8+R36l+/i+h2Moc1Eys+MK0z7MJ4VshdWvX35GbOy/jPGIIxEClsLrrVsHQSaFd3cc128yqeuK0o/oAytF3re2p0WuQXgeZv5Xn0uCutiU5TUVYVbu9atJdCUpQXmZVlSVdXqz1LXC8rS/p/s7bbjPd+qYMdWT8FxHOsh3elYIdmmRrbjLTmb4QXWmca0pZTHx2NGGyOqqmLQH+C6Dptbm8ymU+q6ocgLyrK0ukAIev0ee3t77O8foHXD0eGRVezudtl98Zw0Tbl96zZBGNDXA9zWG3vpwBN3OkxmUzzXCt8uFgm6qYmiiNncWsi6vo/RmrgT0+lYv+6qqpjN5ijl2AzyaMTR0RFJktDt9djZ2aHIC6QQtrwOcJSD5/v0lUO/3ycIAiustjEiCALSJKHRDf1+f/WemYwn1HXN9vYlAOJOzPh4TFVX3Lp1jcePH9HpdLhy5TIn4xNmE6vg3jRWXDYMQw4PD63wnZ5z4+ZtksSC7itXLzMcDixO6A+J445lvyLY29tjMZ/bOvFWRHhre9va0uYlnThm0OsjBWyMNvA8l42NEckiwWtdQoqyxnFdgiBce9J/DtPaRl5hl5zteF9Z/jNNP+F6P1GNd6MNyrTiC22jGKNJF/NVnexSHVoKQVFVGCXxfN8Kc5UFUkiqoqBuGpSSLahwUVLh+QF1XWGMFdySysXxFU1ZkBwdcPThX/LwR9/mGz96yCQp6QQO//g3biG1YHxwwrap+fLlgK8/mPC9pw2/cbeHVzVME8NOL+IL1xv+8skJBs2mNvyHuead3IqOlQL2HYeO1mRpTdUIamO9sk8ih4Ntn4eV4G5XMplryrzBcwUVEHiSwoCRhtIIGinwXQchJIErKI2D61paR1pDEHhoA5WwvtAd3+NpUeH4Lo5jhcIQDrUxdOIIXWVUKEopuBwoJtp66k1xQFg1Z0c6K+uZIIpXb0ytDUHo2bataqJuFyEEgWctlcI4JEsSVK9L1OkglWqttgRxrwtY723X9UHYTILne4StOjrYB1W3ANb1rep5qTNryVUUSOWAEDiusxIXK7OMPEtBwmI6xQt8HM9DGHA9S/sOo9jW9rUey0YbGmPF4aSw905RZJhGoxuNZzROy5TIFgvLlHAaDFA1zSpj3uh6tb08y1CqbO9RQSArbg1y4mafapGymJ1wcjxlfHzCYp6SzBco2ap/IpgsambhTfaTkuPjl7YjbCqqusb3fJTjEMYdOv0+veEmYae3ClBFUYznWwE8o3XrKV2hpKU2eZ5HXddEnXglDpcXBWHcsZT0QHFyeMilO1/gz/7Fn/DyYM7bX/wS4eASCHj43T/h+9/7BNcPcAOfq29/jbg34DDLcbyIdLJHOh1T5QV1LZhlcw6fP+bqrb+F3vl1vKNvsXHvF3nx4bd4+v5f8eLZHu9992Pe+uV/h0s37rL37BHdXgw652//3v+IePM2733z6zz+xv+FuNujnGQk8xlpkrKYp0zHU9J5wqUrV/mt3/lt/vAP/ojvfvMv6XQ7zLMGpUBJqI31P9Z1RZbnaGOsj3lZ0DQNeZ4jlaSpHLSygw7HcagrK5bn+f6KbVNk1hfY+rxa1VRjNLqyYmrJYmGZOMbYANPPOF2Adzgdii/h6fne2aynBk83dG79n+RtsBJjO3c8qyGrWPvhJ5zOD8bXQfqrC7fLvCYu8BPtlNZSzixBnyEpEj45/oiu1wcEgQo4yPd5kDzm+eSIK71L+I7kaQ6lGnHJUThKcNw4ONJmrme1LUvwA0mRW8syoxuud3x8V+E7Tgu8Fb6rcF2FJxVKilN6ubQD36UZz+oyroQD2tzxqj3WwPiZxvpxDXMRVFub9Rmv52or6/GeC27Lsx+XoO5MmOXcQu15CnkKAjir7CywwNFoEEq2dd9tO2qNFMrWyLaqy1K2WXDptN7fVpAnzzOqoqSpGzKTWe2XJsD3OmxsX2V+vEsyHSM6fbTW5ElKnqQc7z7m7ue+hlKK6fiA+fFLwrhHbSSOEpimQjcNm9fuobwQ13URcZd8MWaWPWYxfsnwylt0B5cQQr7yfJ1+Wo9AXdj6F2Hxi9uWJfA6+yxdXBm9zIqyAt9iBdbPBtM+5W56dbqALfGTTD+fQfSn3eTrLSgQwkEqMFK3AWsrJOtHMVmysLXfWUYym5CnCX4UE8ZdotgKm/rdSwSdEcqL6E4PONl9QjabIBCooEPkeiymU7RwKCqN1imD7U2O9/bZunqdsmjoXrqJLnOiKGDx4gfUiwOMkOgG/M4Qx48JOn2C7hauH114Nq8GT9o3SVs6sqz71isHAes4YPQpCF9q3IhW00YZjZIK7Ti4bkNdu3h1Te37VHVAWFr6eFkUFIXNVi8Bt82CZ+33nDzLKcuSsgXYjlNSN5aJsp4dt2UiFowXZZsNb7PieZ5xcHBgmX5CEkVRq5o9wnVdAt/H9TzGzZhut4sfBNRVBUAQBOzt71PkOcYYLu1cwnVtjfrVq1e5d+8edW1B78nJCZcvX2Gp9N7ohp3Ll6nKytZbX73Ks6fPePH8Od1ej8Ggz+GhpaMv5guGowFNY32rp5Ppyhd873CfwWBEFIUI7FhfScXB3h5xHLMx3KDqVuR5juu5DAZDAt8nWSzIsgyAfn9Ad6vH8+fPGA2HlEVBqjO01oyGA6qq4Oq1q9RPbeJpMpkQxRFoG8RJs4zRcMRisWA6mdHt9VgsFgipVqLXZVkyGAwYjkaAYHNjk6IsmU4nBL5PGIZMJhMWiwVxHNt6/WTBfD6n1+uxMRoxGA4Yj8eWsSolWZYxPjoCBNtb2+zt76McSRhZm9/haMiL5y8IAw8TWseZxWLBV7/6NRrdcHRwxGw2o9/vEkWhFb0dDOi3jIhP7Qp+lukn6vx+vtNnBt5ZnhF3rDVU1XozG21rNISQqyx1kWWURcliOiPqxChlhdfSJLH2P01p7QXCCIMdNGttMFgaqXIchKPQxlBkKZOTfZ784JtMX9wnLsbcCEs+jh0+2Z1xODP86EjyVl+SzWaMBh3evd7nzx9NySvNk6MK0RSMK80nT2ccLCp0Y/jlquE/yAxRpUHYTPVB10UMfe4e5ZRGMBNWRXza83i+HXNi4PqG4vlJTiAMMxS3I0lWVFRaYKTGVw6lsSBASIHGoR+7nOQGV1rafNVU1EgcVxGFAceVAOWw2fM5KgXSsV7Zrh+ghUQ5gsYPaGpNohRBWHNFdpgpn7pubB1MVeO1ll2NNjarHfiAZSWEUdTW21vqehBYSo9UCs8PkcpaZ8lW1CLPs1bYxlmBR6EUpmkQ7UttMZ+DsKBbOS4KjdY1eV7jug6OE7fX056PWY4ajFWUXtbz1E1NukhwPR/lKDzXpShKBOAGQUvvMdRlRRCGq3KDprLq5L4fkCwWhHGHpq7IK6uYbgdoipPxMWEUEUURUlnBOd3uszHW41spxXQy5urQ5Z2NjCY5IMlyskXC8eGYw6Mx6SKnylPAYITNci/oMuvcYv/4hGQ+I5nPrBpkU1mWQWux1umN6Pb7DDe3iFvQ7PkheZYjpSKIY9JkYbP7HTvfBh8awiiy9xO2Xj4M3VaZvaGcHrH7coxyA37rH/4PyKYv6XZCwsEWj77/b/jgwyfMk5pqNuXtL36Zz//a30YJaPKEZ4+ecPnOJ+TpgstvfInxrGGevOSDf/P/4t1f/S2CzTvs3L7GbO9jHv/g2/zg29/HdR2++ktv8cM/+a+58+5X2Xt0nyAMMGXBi0cP+Jf/+/8dn/zVX/DVX3yTG9cvUU9qnuwnDIcjvvj5t3n44BEvnzwhTRIuXb7G3/gbv8o//2//JUV1ghQO6JqyqujHQ2ZpinLs/WoaTTKfW99zIQjjiDCMEUBZlSymU4abm9RVaWvFpBW0ke0oVbrOqi2LoqCuG3RTIcqSTreDkrbkZVUL+zNO60PDFWA5Txk3y3Hs+ZTSRcPTV72VP3Wnawudgv32+2cA3D/PDNcrAK8FBOeSeecOfT0VZ1bLCGPFhEwLwpum4SQ7wXcDfBlQ1gUv0qccF8cczI/ohx18RxE7HpkOGaiAgaM4QuELydDzeGkkoqlxhaDrwNzY/TvA5dhr9TIcm+V2VZvpVjhtllu1nmCnasbL+Ik4cy5LqvmFbbPWFGev26uNaU7vqHMrvv66rbYllqB5uerptk4HOHbp9fLgi7KmZ47vfNFqu5xYZb/hTJpuuQXZAgNl1uq+5QqEL90JrAWSaoF36/2tHJTjkKuMqrA132UbmKsqn9DfINqQzMb7uK4DRjMYbXF08JL+cAs38Jkd7yGFJkumpEmC49us56Wrt+h0Bsznx4hKkTc1tu67RHohdTnn8PFfYW58ge7oKlKeH0aZ039/7IN7fq3z385FLC7Y1Fnwfb6dVx0N6zlis7bo+jU+n0n/a+kLPsP06fv+yY5KCInCRbo2A+64Pn4YU5UFRZaRpwvyVoCtSBeksxA/smVZfhAQ7bxFfPltNu7V0FSYpqTIFuhixvGL+0xePuF49yXK9wk9e49maUp3tMmTjz/m2t3bSDSu73N4/6+Yjw+oak1nMMALYxzXIRrssHnnF4g3b6Mcv60Jtzo5liBi9XaWAbsVy6IVURAClFgmBACjEbIt7zAgjEY3VofISIHU1vtba4XTZo7t2Kixtdx+bQFzbQHzOljO26x3lmfkWWspli+BeWkz4WVpEy9VSdNoK6pcWup4tQT1ZbWioBdFuNpmVVZMp1OKomBvd5cosrT0Qb+PchTT6ZQwDFcq45PJhDCKCMPQ2uoWBcPhqO0TyrZPsLpRV65cwXFUK7hWkiQJdV2DMVRVSVXW3Lh1C601x4dHLMIARzk0Tc2L58/YP9ijLEru3L2L73os0gSEFYNWjmRjY5PpdMrJyQnduEt/MMBxbHJsYzQiK3Libof9vX2MNty6dZvZbIYx9n12eHCAkorj42PquubW7ds8evSIMAws5jLQ7/VQytZ4u6678kU/mYzJ0pxup8v16zd48PCBvW+EYDgccXx8zCeffMygPyCIIvq9LmmW4cuATt3l0s42ddUwmVmauVWG3+Do4IhOJ0brhoPDA5qmbsWQXbTW9Lpd+v0eu89f0uiG2WzaspgVTd0wGPRxHMV8PufqtWvUdcWNmze4c+c2nuPy8OFD/uiP/oiq9PF9D2gYDYd4XvDZO4u2WzDnAtyfuav4CYLWP4/pMwPvXn+Ao2zdraWSOwRBSF7kYGwdrmwsndh1XTC24yiLAoyli2OgKq0ZfVMWFpRha7b8ILB1vMZQJgkPH3zI7kffZ77/BDc7ZtPXKNFgdMPvfukK06zhk90pf/b9xww/3yN2PfYO50Shx5VBwKTQfLA/58UsYa4NWgvcpuH3SsPfSjTCQOMIsgZSaXg88BkuashqMiFopGGvH/B0EFIYycCFWaExEiLfxROCWVXb2jOsv7eUhrwR1stcSXIjGTo+3RA0Bu1ofM+jbASuY5X8Jrmm1C6u4xJKSYm0XtDCQQtD4EeUdY1BI12PJ1LxppPzsQgI+wO0AVmVCNPWBmOp/VVVIaQi6nQQwnY0fmBfJMZAFAS2tmlp6QK4jqWXjza38DyPprXYQgg6vT55lmKMIYwjS8tt/5rG0sCrIkfrhm5/YOuafY+m1hijSZKFraXMM8K4g+e6aAOdqE/c6bU1/GZFd0FacO96lipe19bv0bKq7HLKcVAohhsb7UspaH08l3XaDqIV66jrBtE0rXiF9aN3PQ8JSGG4u9lwvTOhXhyxmExZzBMO9o4Zn8ypigrTVLZmysDJvGThX2bmbLL37IUF3IV96ThKgfAIHB/X9eiPNgnCDv2BtVsIo5i6KDC6YXtnxwqBCYEfhIRRjBL2WVKtzcfypSqFtBYYumExnxGFPnV6xPToEM9z+cW/9/vMjjZ5+qPvMNjcAA1+b0R+mOF7PmVZ8Uf/1f+R6/+r/w11PmufQwdMQ+Qp7r79JsXJPsnuR3z7//Nf8Ru/9z9E1gs++c6fsjeuUUIjZE0cwO17N8mKfZSURHGXuir5zh/+l+y990OGvQ6b1z6P683oDbqEvUvcuH0JNwzY3LlClsyYnMxpmmeMhgPefecWu08fkqYVVWunp3Xdlgxggb0B09AyOkzLTrCDcgdjFc2LEs9zLQUNQVVaZoZSCmEkVVkym0xxPNdGpP1gVSu+tBPL8/Rn6kzFuS9LK63zoOv0t9Pfz+ahPsM+li+KFV/0XIZtbY0VNXz9/zW++BlwtVptLXt0HjwsB+xnXm4ttfaMoJo5PcaluJzh1TZZF4Y7gygviBKYU5hR6gpHOmwG2zyePmA/36M0BfuLY7S2dpWO0AROF2M8rvg+pXDwjcvQU+waBRo6WQ6uR0eAJwwVki4w8q3Fjq8UvnLw2n7dkVa9XLbtuBRSE6LNtC1bYx2PnmlWceZ81ht1/fquspRtH2AVjpfCeOJMW5xu9zwSP237M8ewDqzPrsDqnlk/srZ8Z303p8dxus4ZuLY6f/vbSgVcnq5vlveGNEhj23BJ2V9ZFkkrwmYFEEVr22YDw8pxKdyMIs+pisLWhbZ6GGE4pLfTwfMls6NnjE+OqMuatz7/9yzAKDNOjvbYvnqXqjG4fki312tFQFOqdIH261Um0mhDsThC4BD0NpjsPyCZnbB19R6eH59e0rXrsBTKOwWyr44El7+Lc98vAp8XBWPEMqpywbRit5xb8aJA0OqXFpWLZT/xcwpIftZpSak+Pf9z9+/5nz5tcL26zwUIK/pqwbdngXUYE3W7lHlOniZkyZwynZMtJi2DL8CPOnhhbMdP4QBHKcKRtXGNd+6xcf0++x//Fcd7LwnCAMfxqKqcMkvZ2LnM7sP7cPsuvhegPJfpLGHnxnUC38frDJmfHOIXM3bf+zrxxicEw5v0dt4ErB6J1k1bmkeb5PIQUp2e+tq1XGl8rJhBNkgpkAiFHUO1N400Bt3asCplk126Hb83K8vVmjoIWhC9RhsvC4q8tBnvfO2vyMnSbPXdLl9aMbbA6vFUrZBuWdptlWVJVVbkRd7S0E9rwm1deU7SelfHUWRLWB2HbqdDGNrERFGUSCkIw5BxllFXdZtxt+dweHhoba18n7jTaUsCLHvOWuz6DAcjmqrm+Yvn1EFAr9+jKAuc2AXA8RySJGFvfx8/CAh8zyaKyoJOp8PJyYROHBOFoR3v64bRaITjumRZxiJJmE5n9HpdhoMWT5W2dn5jtEEURcwXc05OJgSOQiqBbjSD/oA8LwhafaY4Dtnf22Nj09K0rWWcaD3DB2ijGR8fUxUl45MxSbyg2+9z5cplnj55ipCwtbGB53t0Oh3m8wXGaA4PDun2umxujLh165bViZKKILD19lprBoOBzfZPZ0wnUzY3t5hMZ9RVyfbOJcqi5OrVq6RZatkPyqGsKrq9Ljdv3qLWml6vS1OV9LobBIGPVIrRxgaj0RClHN586222Ni+t7uUf21+8rhtYdRTi1YU/dcUfM++i6SeMUH72jHeW4Tlea8HUIKVkNj1BCEEUd2kwNHW1iobI1hZESpvhFAh0q2ru+T61Nihh1es836NuGiZ7L3n0/W9yeP97PH36iO/e3yMpKyLP5fe/dpnrwwCjAoqs5G/di3l2OOfwZMIffH/BP/j8BmmpmeUVo8jnMEt572CGcBSNNozqhv+4NLydNxglwHPRtaYxNXsbEVE3ZGM6oUYwcwV7l7d46Sm0sBZfm76mzEqudlyQgmlRYxwHIwyRI9DtCEkj8B1B6LkYrZg3glHoMG8kUe2gachrSYUi8h0mVYlQHkclSCUQjoMjLTU6DGOk59MYQRQGuK2F0gsTclfkPKorjOMRRfEqq5UVOcpx7O0mlbWeUpZiFXU6+L6l7Hqtd7f1S7VUlKWvqm4a6qq0NPWWDu24Lsl81tbMKeq6QQlBmRck5QzP923nrbXtbJPCqoZ6HiCtfZQxDDc2MdpQlsXKq9sIaGpbl17VNXVV4fseKgxt3bW01OyqqtCNxg9DHNejrkobeXMcdNO0wNRSqYwxZG2gwPctTd5xHJqmbEG3zbBTLPjcZUNfjikWU6ZHJ0zGJxwdjJlNE9I0RbUvuUZrZoVk3rnH4byhyI9YzKdkaYpSVtBOe1ZEzY86DEcbOJ5Ht9vHcVzC2Na8dzast6QNHIBQirCteTfCetpmibVhmIzH9hnrdGjqhiDw2yytRBrNG++8xW/8R/85TbJLZ7jDYjZHVwXBYIv9vW+TlTX9bgchBb7vIpR9IRs8am3NjD78xh8zmaS4jiQOfcb3/xyn+T0e/ejPebk34Wv/8H/Jx396jfnjb+BHMcl0xtaVy3zxl7+M73tI09DvWP0GqpKqKih0wmjQ5d4XPodO9sirhvff/5jZ9ASJi2kaHCp0XpOlOZVWLCZjom6P5OA+jO6tLMGaRrdYURLGkY2ct+UHylFEcYciT6lrKyxYNxV5nq2yA03TkKdp+0yIVvvA1o7aGrDc0obbDMNPM51/SYj1weup79AacFr9wxmA2n59dfufsm9aALOcVttaB2FrSOoMkG1/Oxc1WB3Z+gvInF3m/NtpPRe6zHZehDzF6cd2H+vHdnr862DzlRPWtkZaIvl48j5pnaB1w35yyNF8wpubb5LplF7YxRcjhq6LNh6FVtz1XB4bD6kNITVZVtjAVwuiHWDoKoa+i6ccXKWserk8VS+XwoJBaNVi25pua1d9Fl6tAOr5lltLK59JCJ/BzqdtKM4NBs5b2LUtzyuTOAvolyudrr+287Us6PrPp4dx9lotb+8zx3vB/tdOmhX1fA3AW+q5tBTZ9tj0SnTNWo3JFngrpZCOBVGO4+K6nhXMVIlltFQleW6FPH3PtyVf3U36ww2yxYKqqBDS0i7TNCfJK1xH0On0CHsj6qpgMd5FA/3uAK01TZm3gMEw3NpGegG6rjCm4PDFB2xceQvf76xd+2WDndbkrsSxlm3SPvdnrs35uMmZdrzgPjkX/LoQJF8U7FqfeWbDr+t8zMXzft6TOf9h7UY5c0yf8XDWljt9Du17Qko7RvW8AD+M25KwAUWakC6m5GlCnszJFlOEdPECC8LDuIMfRNZNwyg6O5/DH95g8/A5s8OnLI53cbVl51XZnCDucPRyl95wRNUYhjtX2H22y1vvvovrOly6dov55ITBznWkksxf/ABdTGmMoUqnYAxhPEALB+lFeFEPN+wTdkYIpSyoPndZbdlLe9XEst7bYLCCv1rYkj+hNaYVZ5N6WQuucYxBr2fBm4a6siK2NhPegvCyoiis00DRguZ8mQlvlc0tCC9av+9yVf8drGXV66qibMV1yxaMr/8t68lnLfNNScXJ+AQpBXHcodvrWoam7xMGAWliGXNLxmYURxRZhtYNs9mcbq8LgOt6LBYLZrMZge/R7XV54417p4miouSb3/gGURQRx1Yx3HVtgD9JFpRFSZZn+L7PwYH1Lw8Da01mjCaKYmbzGWVRcuXqFQaDIScnE2vrphziKGZ//4CDwwN63S5Ra4dalAWRG+F6ljLveR7Hx8c4yuHo+JjpZELcianKil6vj5SSGzeu8+LFC0YbIxZpauc3NZ1ObIXS4pjAt9pIs/mUoAyZTqcgBFmWWdcprUnTlMFgQJ5lLBbJCvCPRkO6/R7z6Xxlxbu1tcnjJ485OjwiDCNOxmOM0cxmM7I05crVqxweHOC14sbb29v4nk2cWIu1GS+eP+P2rVvEccjVa1e5c+ceQXS27OKzTMsA5+nD/uP7hP+ups8MvIO2plfh4CjXUpq7QTsGMgSeR1LXuJ71TwQ7MNF1Y0FPK7bmBwEYTVlY2ndZ5xw/f8iLH/wp2d4DqsUJQZ3xy3c3cFyHP/zuQ/K05I8fZ/zj229T7X5E6GgGruatyz7ffFSxqEH5MeODI7odn0HHxcwVNIKsKLhaC/5JJbjeaIznYKREeIq6yjjpuCwuDeimGX6pWYQuz29cIu/EdJoKT8BQaagrlA8pGiGhURJXweWOQ1k25I2m1hAog+tIHMcjNFBLl0ZIlOvS7yjGWQ1CUQibxQ08HykUcev/CIasrAmDiE4nBiFwjEQLQZJmSCWpXA/l97hOzXEQo3y/reGATq+P5wXkLcXc9SxYr6rKqpH71sLKYD3X66ZBKtca3FdWAdrSzBUCWz+DANdx6Pa6+C1gbBpDli4AQX9o63DKMscPg1V9th+Eq2sfBD7T8RghFVmW4CiJMZosS9ssMWRJgpDSCqcpZYFzYwdhyXxOdzDA8ZYe8hLPt3YNVV1ZCnptg0IIm53AGIo8I0vmdHr91k7BtKDNsBEKLg9LvPqExfiIyeER4+MJhwfHFGlOWTf2vgWKqmFSh8yjmyRZw+6LZ6uXQpYscD3XRmF7Azq9PlG3TxTFtm28gCDwWwq8stfFdcmz1EYwu138IKQsrWCQ67vMxnOUVHT6fdCGIIqsX3nTEMexBbmNw3DnMhuXbzB++BKCiKrRCC9mvPeAX/2d36Uqpnz/G9/k6q2b3Lj3OZRoaLQBx7P1+FIQbFzGK14wPppwsqj5nf/4f05x/IgPvvPnVMF1nj34IdMq5i+/95RfePce165sc/jyJb3RCC+KqKuCTuzTjRxK03C8f4AJprg7Hul4j17PI0sbDo9PCJSgrEq09lkklrZmr4tmPp/RLyqC8XvQuwnKw/N8qqoEY23UiqIEAVWWtwMSa+0RRh1bPhFY8Ox7tj2VsoMH1/OsdZ7jtrS3Elyr6l/XJcL1W/GVn3Y6F1ldH+SeQWHilWzjqZDScjvry6/9dB6gijMfL9jWuWDA+jJrme8VyGpRwNnDXQPVpwhiDXCd28cFx7Y6dHMelrVQ8Sx+PLfJ9SDBKVA0QNWUHCYHuNKl1CUH6REvJgdcHVwl9jrkRcrN8A6KPg+8nIOy4W7gciJ8lJaYpiYqS8YIpKPoC1AGRKMZBQ6Rq/AcecYyTElbz90yzE+BVgus5DmAfRHgPj3P9fvl7L2zDmhX1+1C4PTKqmcgy2uzphfse4Xdzvx8divrmG99Lxcc1dk9mvPAr03nr9+r0tCSY1n5HgvrRiDEUtVcWYGo1h7JcTwcx7PezUohVUKZS8oipypzmrqkqjziwGFj+wqH9RP2d58y2tgm7AzwOillnqJdDz+KKbI52dzaDwWdDocvH1NmCZ3+iNnxHv1L15F+hJIOftSnTBc0puLl/e8QxH2Gl+4QRv21OMZKyoy1Rrjg4plVM4t1msP61VyC6tcNGl+bmV6yXswFt8/5HuT0mqxf+dfVnf88pjP3qLjgEF83nb8Z14Ut1u/dCza4vJuFUCAVUjo4ysMLIoKoQ9QfUJclRZaQZylFllEVOYvxIcnkuBVsiwiimCAMcdwOgxtfoH/9HdANuilIJwdU2YLF8XMWx3tkeY4QLp4f0x2NuP/RA+689SbSNdSN5ujFMzav30I6ivmLH7CYZzy//2Hb/zTUleDStauE3QHGSLQT07l0myDuousKv7uJF/aJBttI5bK67quTFi34boOFxmCEHYcZWkE2DNKoVvNEo7RGOw6O1mjXMlqXYLyqK+q6sW40a2B8WbtdlKUVr1urD8+Lov0tbxXS67aW3FLC66q2dPSqPK0Nr+z3ss2AL8G+zbrnTCZWANcYiOKQIAgJ/ADHtQJyvV7P9rtNwyJJrHbOSdUG7Vy63Q6OUiRpwiJN2NzYpNPtoZsaFoI3771Jr9/j6Ph45Zs96PWsQns15tatW1RVDQi2trZA0AJQy9pxXQ+BYG93jyzN2N7eJklSu44QbIw2ODw8YO/lLl/80pcYDgbs7u/jONbyra6tds21q1fteKZp2Lhzh3GblNnd3aXb6xL4AcPRAN1m8gVw/fp1hBAUeUGSLIg7ETuXd4jCiMl0Srfb5cXL51Rlxe1bt6kbez2yLOfyzg4Hh4coxyFdJAxHIw4ODqiqiizLkNLeJ71ej7oqKeuCk5NjDKzcJjzX487du3i+S6/bY3Iypu50uHXrJpPJhD/8gz8E4Gtf+yp3797lc597h52dyyipPnM/sD5+WD36r8tYL99vF/Uzf31d3CvTZx5p6ka3SuW6BW3WEqGsSkxjvbfLPEc6DlVZIqTE8zwcZVW1dZtpWmbzssWMF5+8z5P3v8v3f/A9XhwccWuzw+98/hJl3pBMjnh3J0Z+5Rp/8P1d3ntywD/74z/nd9/ZJEkMdVXzzqUe33+eYYRkkuREvstBWfLDaUlhBHlRcqsy/KfaYUcKTJvxE3WDrmsyJdm9tonjOAySkqoX8vLGNXLXRaBxlaTjKlvngouUNQ41ZaNRnuZm39J/jNNQ1gUdV+FKaLBZS7RAK8lCu3Q8S5tzGxdHC7R0qBtr2+Q5Hq6RCNdSV7zQ0AhJUlQ2MuY6llLePsQaWOBivNhmgvWyrtzBcT0QlqKr2o4FrG+653vEcdfWv7ZK5EmS4EhJXdsaaoMtB5DCBwwaK8Jho441QpY4WlNXNd1eH4FVDxdC4Hm+rZVz3VbdvqApC4LIijQIJUnTxKpRC0GaLKjLCjy3BVgVtBnr6ckEY1o1a93ghwES8D2PStaURWHF7Mpidd8taYllVqBa2rEfhihpM+5VXdPUDQrN5XjB1TClTsZMTo453t9lfDxjNk1J0gxd1yAlWguSvCSPrjILLrH78gVlYSO3eZph2totKRX94YjuYMRwtIFyXAajDaKo0x5/aH0dlWOP3RhLf6dmNhnT649oWjZJUdg2bbQhiiI0lo4eRpE9LgRKuTQmJF+cYIwg3rzBZO+ppaIpycvHj/jK3/4lbr/1WyyOx4y2LxEFEkyDI6HM5isngpv33uEX/94/4pv/zX/BzS/8Km98/ot85w//S3Zf7PPs0beo/sX/g7yyL+dHD59yaXtE1BsxGZ8wOZlw+eYdNrZdXN+lmKU8/dFf0H3nOqYYEIUOrhDsT3I6vkdd50SBIoo8FrmmzCuUchlEHkleUuQpRy8fc+nqMbVzlbKsGB8dEkUxUgqmkwlhGNHpdFZj0UbbY5NSWFs5bdk3eZquNCiWZS1pklCVVjyQMMBxPXuNWtvDn34SF3yyk30ZnP5qxAWD6bUx4hIYCcMpddu8fvsXH8XF01lwu7bNdaT32u2egvAz52TOr3rRdk7fdKdnfwoUzNklL1h7bek2eNHoGoRmXs+YZBNeTPYZhEO2Y0tT2/a2uRJdJyk0ja7YdB1iz2ehFZ427FDzojJUjoOSgpGjcKipGs3VILB2YVLZjLdUOFKghGhBt1yd0unfKXh5BV+9pl3Pz7+oHV43hrhoMhd8XolyrWWzf9z6p2BoDXK9cp2Xx/XjNroGINvjWKmhm1PrMYsNLQCndekwSliLN0cg9LLee/l/jSOdNgPutmI/jrW+lIIit5TTvMmoa4fQ7zK4co90ckxjaIWgPI4O93jrna9QVyWT4z3KdEbcHbEYH+GFAY4foZFceeOLCOmiNXiugsaWP9VlQVUWlOmYKltw6faX8QPLMpKItQDK6fOzaowzvyxHhcsAmDl77dv7flWbL87eG+e3e54EcybA8mnTerCoPZ6/zjHpRds+73B+8RPyykpnF3udOfYFByBaEC6NQnkOruujQ03UsQDMltIVFHlqbcmSBfN0wXwMrh/iBiF+EFlA7vsoxyUYXGGwE7F95ysUyYT54RP2HrxPke3hegHNLOXg+XNG21v4YYenH/0IBPRGIxASibVMlRLGR3P6gx5FmpOnLxhtjpg++YjZo28T9TcIR9tkjkddN8RX3mH77d/CCIGjnJZwcvo0Cwx67R4CC6Bs5nv5B1IKjFEYrS1LTlntFK2turenrQhuo21d+IqaXq/Vh7eZ6iV1PM+tGnqeW1G2oizIspSyrFaK59amrF5lx5vWMnZJTbfgvmgp70sBuIK8yEnTjLKoOKoOUdLBYMdQruO0ntkeZWk1hIyAKIzI8wxjLPvWcRxevnxpxW/DkP6gjzEQxzFFUZEVGXEUtfXiV9na2kYqybOnzxiNRhgMoR9y+/Ydqqqm2+/hKMVsOuHly108zyMMQ6RSzGZT9vd3OTg4sEBZQFHm3L5zlygIkQKSZEFVVZyMTyjynCvXrhLHHY6PjmxWPIoYDIbMZlPef/A+ZZlTlRVvff7zSLAU+063VXrf4sP33+O9H/6Q/mDI1atXkUox6A+p65rd/X10XbO5tcl0MmbQ7zGbzajKitHGkCRNCQJrN2xtXiPSLOV7f/Vdrly9iltrti9d4ujouKXu95nP50zGY27dvY3reWxubjAcDMiyjG/8xTeI45AojJhOZ9y5e5e7b7yJQJ7Gz9aJOD+mE1r1gebHL742Gnvll38b02cG3ksauZJW9KTR2MxRUaAcrxUw8cmyzMrvt/ZU2hiqqkYbg+sKdFly+PIZx88eMN19xJYcE3uCk0XB8Tyn1oLf/dJlSiMos5y3hpLpmzv8q/df8NF+zrtXG3xqOkEARU4vdGkaSPKah1rx/rwgLyqaPOfN2vBPjEvfD2xdmaPQVWGj4Fqwf22bcHODTl4gtzcYX7+FEJKuMTaCU1akwtaHx0FAIAV1mRPRcCUEXVXUoiFva0RCx9ZkaGNwlYcUhgpDIh36jsukNsSxQ9FIhHKZZSXSGBo3IBAO0rUUfKkExkCgXFvb5nn4QQQYGiNWXt3CdYk6VqBMtfVuUkikshkC69Mt8YOANE1sRthoC96NBgS+H9DUFd3BCL/19q7LEiFphYV8wKqG9wdDFov5KgBT5rlVKtcGLWnt4Fxbd+Naq4pCSazQh0PU6VK29hNq6fHdNERxbGmDrgWldV3j+yGNqVqfbyviBpClKdpYL+c8TaiqEtfz2gCDBSoaTeh5LbOiwA1DGt1QFSXdAO5slMScUM0XTI/2mU0mHB3NmM1S5tN5WweuMBpmuabZfJe5CRgf7pMkCVmaUNU1ju9htMb1fMI4pj/aoNcfMtrYxFEudTswi+OotTpz2uih9RP14w79/og8TykrSycKfN+qnrf+o1IIdFlTlCUgaOqKfn9AnueUbo8yPeH54wfceeMOk8M9Gi05fHqf4/1DDvYO+cIv/hq//rv/gNnRLqIuEcYKGnaGG5R5gjGw9/hHvPt3/jF/9z//X7O1tcXuD/8lTz6+z41f+B2uf0Xy+P4Dnrz/DS4NA65e3WE2PuTkeExntI1UcP9HP2D72jX+5t//9/nW1/+EdHpMkRdkyZww7lJVBcIN8X2JbiSOI7l58yoPH+3ycr9iFLtUwqEsa6qqYX//JVc+v0sqN9p71KeuapRrqaVKKRbzOULQ1nIH+IHflhpYBXStrbDf0u/d830wVsU+jCJrTaJsUE0Yswpo/dTTMkO3/Hpm3qckqdr/X1nPrIbXr7wTXrsOP93r40yC6KdY92defgXGzi7z4w6nbmqK2tIQDxfHxF6XQTTkWnyV/fken9v4PIEKyZV9dq5FPgeNpVRu6pLHlQGpODGSvhQEwv4Jo3mr6+GuqOXLTLdoU93Yeu6WzinAMgPWavpX16ZtWLP89zUNdj7rd1GDvS57fSapddG2xdmZP+lQ42wGfG3ddeR3IQQU6wuvfl/ZjZ35fBrFWCqhG4VVPJftcqL1JG4Vz4U8pZ9LpVpLQFvDK5QVZMvztFVTLtBNTRiE9C7dxiiJ8iBPnhG0gEkbydHuC4TQra2pRzKbIYWgv3HF9vktA8IYEMqyZer0hDKb43dHNFXKwbP36AyvMNi8tipTOn+NzHqbnUvKrogd6+G6C+4bc+7eOL3p1jK/Zy7Ya67UGWB2bvGziP7f2nTxvXn+hFk+fPxcDnIZGxLSAl7ASBdcg+drTGxFwupqg7LIyZM56WJGWRRksxOroC8Ejufh+hF+GFOEBV4QEEQDtu5sMbr+BfLpAce79+kevKCpUhZJgVec0N/Y4MUnH1Bdu87m5Wt4VYMT+HQHG9z70ldJphO8TpdkcsL+y32u3n6Tsh1/+2HA5MVT8sKC3e6Vt+lsXLPsibV6IWFrYZDLjorT4K5YBolW4LsVa5MSaax2jjFWLV07GqMdC8Jb7Zl1WnrTNKdCbWt08noFoK0AW7FGR1+C53UBt1NLsrIVrm0F0+qaumlWv9tnvLIZ9SJH5fIUlJc5ZV6AkDiupXfHsWUdLlpGqOvZkhXXtVlm5TqcAMPBAM8PmM0XVuy1VBSlHXOGTkCSJJRVRRAEls2LaTPCOZ1uzPTkhG63w2i0gQUhmjCM2NjYZDTcYHwyZjAcEgQhDx8+pBN3SBYLJpNJyxywpaNlWXB4dIBUijt37rC1vcXLly+ZzmY8e/aUly9esrG1SaMNSZby8sVzOnGXptEcHx3iuR5CCI6Pjzk62OfS5Ss8f/6Mu2/cw/d8qroiz1IODw7Ii5ym1lR1zaDfp6wq4igmTRK2t7dwXactwXRI04TBYIhAsjEa0mjN5ctXWCzmuMoyao8XCwa9PpujEdPJlL3dPR49fMDW1jaXtrfo9wd0uh22NretdtQ6cF5/d30G8L2cXg3YXTTv1cf/3xb8/szAu25qwjBiNjmx9bHKej47ysH3PYyxtaOu5xGGkRUXaHSbCXWZT8Z89MPv8cPvfot8vMs7Wz6B75BnJ/zGtYChs80//9E+P3h6xKZfc7cnKLRVfL4cpSgBrhcwLiQDCUVZoKRgELoUODzyYh50Ysj2UCbhTSH5nyiXUa9L4ymoNY0U6NkcIxWLSwPUV9+lU2SAJO1vIIwhKiuktP7UZW5l/mlqmrIkMw2duEskatKyxBeaXLkYmRNKg68gNzVCGxASz5HUUlIrl8ZxcT0f3TQMHUHW2WQU5aRpQSU8PFdRakHdjj8cadVztTEIZVWXHddDNAahFI7nEYQxUdyhMeC4jo3YSpvdE0rSVDWdfp+yLBkGVtpfCEXQqkHWtRUy8+IIJSV1ZQcnfhAgBCxmM3pDD6MNUirqpkIKG4SxYmYgpKDMs5Z6FSCloK5LaxUmIO50KYucpixAYIMxtaaoclvLHQRobSiLdJWdj+KIum4I3A5lVeK2CuvLaGwxX+CFEcpzMbppfcg96qZGN7YOKV0skEpitGYxn+G6Dlux5u4oQdUzkumE+fERhweHTCcJJyfW0xPsOWVlzYIu4tI77B5PORm/IE9TFsmijQBDUxc2MtrpMtjcoj/cpBPH+EGE5weEcYjnWu90KSXz6RTje3QHA2vhJiXaNMTdHsKYlYppU1U4nku6WLTBiZr5bEaeJPQGAzzPai2UdOn3u/z5v/gDbt75p1y69Taf/8Vf5Lv/339GkeU0RUalG7Zufx5dzbn/g/eIL70J0ueNL38NN4rJ0xzqkmQ25ubbXyLbe48f/eWfkpmA65//NYxpOHr6ATe2YoYbIxqtefbkJVlW8tGDXd559ytc2dli/+lT4kGP3/kP/i4P3vuIl4/uk6Up/qBHoT1+9O1vcmXgkAmDlPZeLrIMjeDSVofH+ylROKBEocsSGvvCsXX5dfsYVtRliQkCPM+zjImqIopi8qJgNpkQhqGl/gc+GCui1tQ1SEHTaIKwtSWra4osww8jhJToMv/Zxm4XFmGf78qX35f52/WB5DIlyZlBpWgXW6cdf9bDvGjc/ZnXX771Pk1Y6Xx69TVvrYu2IM5/Wwer5wCqWOb+2kHhMh8mpESbmoPkEN0ILvW22Qg3Gfh9jNZshltIJIU29Fxl761G47SZ1EI6jGjAUSQItBB0HGvtdzNwWmr5KfC21PIlsD4HutvTkKuzWx7v2dj6mYTcGjBdArFXc5JrAmqvbONMi13YpqsUwKdM6+u+fnxz5ohfc7+f7uvskZ2d1kG7tR4Tq7XsJ7m61kIaDFYASrRBESlcm/1uVc+XlmOqtRyzomttkM5xLFU4t7aCi8T66dZhRBx2uXLzHuPDXRpt6bee77L34hmj7avsP3/AlRv36IwuoYVgcbyLkJJLV++SLOY4yl77sm7woz6DjcvUZUGja9LpLqYp6Y2u4q3bRK3f2uvI90zjfNbpZwWcS2aGwaxdgU/f3///TGcCEz9FG14YZDizkaUOgk02GQyetMkIP4iIOl16G9s0VUlZZFYhPUuoipx8MSWdWSDu+gGuHxLFfTv2ikZs3Pkal+79Erq2Aepk/wHJwUOapmY+mVLXGi8M2bh6m8NnT4j6AzqDLXSZ0Bv06Q6HGF0hnAApanYfPiCZz9m4epuyNqSLOd3R8kzWXx7rgp/rP58GXk7rwVt3g3bctdQxQrYJDmOdjeznNgFjLPheCrU1Wlt6eit4WDcNTd1QNxaIV0uRtbZsr2jrwE/BeE6WFRRF1tqa5avMd12fbqOq6jM14VbroTpDebfbL5nNZkynU1srrhSu4+L7FnQ7rkPgB8Rxx4qiteWd/V4P5booabV8TsqKqhVpa6qavCxarNMKCquGJEnBQJrmpGlOHEerY9PGljxWdbPSqXjrrbcpipI4jgjCiBfPn7PR3WzFgC3jOEkS9vf3rFd6UzMej3Fdlxs3b+J6Htvb29YBKUvo9fpo3dhklW7wXQ/fC9jZucwiSXjj7j2rcm8gGZ9gjKHT7bKYzzDa8OzJY+7efYOj8TFPnz1jPpvy6OF9er0BW1tbHB8fMR6PuXPnDlVRUtUVnTjm+HiM43rMF3OqouTy5R32Dw/Y39+nbAXfPNfhnXfeodftcvPWbS7t7DAcrm7YV5/rzyjweAasf8p0+o4TZzqCf1vg+zMDb9fxOD7Yby2sHMI4pq4q3CC0inplQVNXrfCVrUNoyoqDpw+ZPnmPvU++wze/9wHfezqlQXBwbYN/7yu3kEGXuphxKzaMQpeTrGIvlbz1zldo5mOK6S4Kw9aoTyfu4KIpq4ayNjhCsN3xKW6/yf6wh/PoAUWecq0y/M9w2Br20YEPvkd9PKZeFDSuSz3qUv3N38LrRjQIC+KKAlWVmEWCkoIiyzDSsfYRBhzPw5MCrUuSRrIdeCwKg5K2vr3nCjy0reduGrRRlm6EIHQdxiJA+JH1oC5yW8ejunj+AJPMcPyAKIjIypIGicEGLIzWVI3NzOR5ifJ8OlGMwUb30zSzXnmbW1ZhXSr8wMfzA4qioCpKS1VvtKWSG2PrmoS1rPJ8nyCw2WTRgmprA6bwfEsdj+MYMFR5BVIQRTGi9dFu6gakstHOoqAuCrRpbcSMZj6d4PsBqq3prqui7egidKvoLaVshS80vu/T1FaVPMusyrTreZRlgev6NtqpbWmDVAovsAJsyWKxUj43bWff7w9RoURRc71Xsu2PqdIp6SLhaPcl46MxJydzknlmVeCVtT8oGk3Vu00R7HC4d8D+7i5101DmVq1zyebo9Pr4YUwQRkghcZQi7vZtjb/rEAYxSkJZFijl0B8O7cPdaLwgwHOtZZhqrGKkwILRRmucxtZOlaUF92EcW+uITscO+hwXrSK6G5f4zjef8PTJE65fv8ytd3+VJx+9T/6Dx7z/ja/z7m/8DttXLrNx40scPnuB1iWO51PMppwIyXiSgtYcPXqPGzd2uP9Xf0pSeXzht3+flx99m8ff+gOymRU9O5nMOTo+YTrL0UKhaEjmUzpvXGE+njAfz0hmf8XN2/e4fOMq93/4A9xW8K7XCXGcClfZbP54llJVDb/w1oit7W1Kv6CsJwgUAkNeNDZi3QbCmqax1HwpcVpA3lF96tLWjTmuYwNGSpEnCV4Yoo22NDJtI+O2jQVBFDDP8/bFVxPGMaZpWo2Fn206+3o4HfCcF8Za0m/Xa6jPvGzOIaDzGbOf1/TabX3Ke259oGoP07R1qT/Bfj4FM4i1+SvQfX4FAcZopvmYk8WcNzfu4Xsx72y+jdYNl0aXcKUt15BIekrxMG1IKs1OqPiTmWSTmlIaGiEwSEoEsbTZj2uRHWTZLGprbSU4VTDHHtvpAHa9lvf8eZ/Cz7OnvJ7xXB8Jv5qTvqh114VkXtXGOh+84Oy9dH7eK8d8/vtrgM1rgy1rxQEXpczPzDgfTFhTbl9Osm0jbYmy9r2xVD4Xbf23WP0JKdssuGV+SaXIs3Q1GK8bQ1U3xFEXN0o4OXqJ5BJ+2KGpS3703b9kZ2eHwcYlsnRBkqVMDl9y6cYbvHz6MY4QVGXG+GCX629+kbi3ga6sPaTRDVU6YTw94PjFx+zc/QX6w8vnstLtl9c+B6f9wqopzgdRzowyl219rsMwZ79+2nR2j2fXv+DLTzwZOCsO91NvqL0jf8btLJv01aft/BLLb6cg3FraebiezQRHurYifLUVCKvKgjxLyRY2K54lCfPxkb0nHRcvCAnjLq4fEsRDgru/SLx9h97RY8bPP2B2MqaoapqqoChrHnzwMXc/97bVesnn+K5DmhWEcYcyq5hN5+QiRB+OEV7AzaBz2lzrbSVOn+d1B4rTbsy+k5ZMFCNs5lssRUFX2fD2SW2FbOXa70YpTJt4W9aKN6347ZKa3jTLv7qtE6/XPL+rlS94WRYWhLfq6VmWrQm2WRX0qrRZ/nW1dEtTr9r68BbYVyVFWVEVS4CeU7QllPNWtG3ZRta1KSCKIsIgIAhDer0evufjeq5VVlcOjuucbjsvWvBo5wWhFXZUUuG6Dlme4jou8/mMKO7S6cT0e12UkiSLxCqoux4g2N7assmwxjIEksWCXq+L79nadc/zrNe871HXmsViTlnaRFZTVRyPM+azGcPBgCLP8YPACiM7DpGIrSNVnhF3YoQQhGFAGAY04zF333iDxXzObD6nqmtGwxHTkwlRGOH5HlIK620+maDaz2VZ8MP3fojruXTiDlHU2gkrQRCEHB8eUtUaz3W4d+8eOzuX2N7e5s7dN9jZuYzv+a/vF9q+0rT33zowfw2p5+LNnOkLTSsx8pp37E+y4Z9i+swjTSElQaeLaiN/dWkHsq7joI0FTAKB73rUZcnj+x/x0bf/FPY/pKNqyiThS9e6hI7h659M+Pj5IV/akvQ8CShM0GUwEty7doev/sKvULz4gMoY3J27vPzoMf2u4o2tGN8k5CXklaGSLs29LzLt+sjdJwR5TpCW/GdCcWUQo4Y9atejdhVmMqdyNMG771DcvoPZ3KLT7eA4LgZDkdpIpR93yRYLPNenKHKqlgpTlTkoRd0ohi6kusIJOjhVholcfEcg6xxfSOtTqA1dPyCva9ygR+w4JEGMDjpkIqUvazIV05QlXqdLUdSUWYYTxniOjR4CaKnwPYkbRARNA0Lhtg+XAQvgfN8+hEFg/R9d63MaRIq6qjBNQ1WWGN+lKkr8IERrbNQ2Cq0KeCtApbVLVVsvbtftWkpfO+CsCmsjVxZWkdzxXLIkIQh8qxLtehRmCSYMjmuBvut7K/AUBtZrUUpJ3ei2XlLQH43QrU/zsl7I1ufaWr2gpcRLKekOBqCtrVTd1JimwWBObSLqCs+z1miB0tzuZ4T1PsV0xuR4zNHePtPJgulsQTLP0aZBKkldNSwqgXPtqyQ6ZHx0yNHhAWma2AhlWWIA5XoEYUh/uGGF1KIOvcGAMLI1977noZTNfSnHI2hLLuyLprJDUq0xnocjW1qbFDTGUDc1nh+gjSaIbOZc6wa0RsQdHEe19GkoG+hsXIf8+xzsHeCYjO1rt/gb/71/wjSR/Ok//3/zr//Z/4l/8J/9LwiiEbe//KvU6Qzlejz95DvsvTxifHTClWsjbr15j/373+fJo2dc+/Lf5eC9P2b69PvoLMX3fY6PT5jNU4qywmjJ5qUNTo6PSdOUuNPl8PAQ14/wPEs9v3z9Gu98+V2SJOHkwT5/8+//Lg+/9XWKZE7oe8zHKTdvbnPr7k16V9/l/p/8GXU+pqkqerFHiU9dlqsX/VLEKstSeoM+YG3XUAqpDY7r4SNIkxRtDPPZDMd1EYDv+0jHpamtSFtdN+293rSBngalJHX1s9R4/5j++Xwo9tz3ZY31mZ8/Da22y58CPvPKYq8kcLjg9wvmn05rg07RwqELVJnPhagv3t/57b8yAF8f/r56PivdpLVZgRfgCI+vXvkyt0Zv0PE6xI5lWxltF9NaW0G/BppGs+FJfpBaJta7keS/mRuE42KQpA30HIginw1XoaS0dPP/H3P/9WzLkuf3YZ905Zbb7tjrb9/b3be7Z7p7GuMHMxA1wBAgCEKiQ4hSKILBJ4VC/4beFHrSkyIYCkZQYggMkjKUSIoAiAE4BhjfZrpvm9vXHLfPNsuWzUw9ZNVatdZee599TncPWRHn7LVqZWVmZWVl/uz3KwWqB6gmoQ0t73dR9KTbvsXEb4/5Ggmmd7IfC75H6b4yrHuVNbFutjOGXFG6dlytW1b/fUrV7u30Ncfd4leAwDYC/9WatwV+3zu/RsPv9pH1e9GCQsk2J9w7nNvwfkuhWsaC1vutVMBH0YE6SmmD1gHQsi6D0c2ugvAfp3eIlCVfzWiqJaPRiLKCN9/7gIvpGflsSr6cMTm+Q50vyYYHLC6ecnB8jwdvfoGmybF1iYlGeBEhFcQmxk3PqeuKi8ffRwjBcHxnjQ2wC2i3PezbJ3z7tDpqsg0oXd/nu6NwdwPbe1zb72GvwJX56TcXiN13/tWP7Xt9Bcn21pfsXcxuLPniBnsX9N4j0SriKI1SHmNSfBqM/0Pb0DQVdVkGutF8RZkvqMqCfH7J/PxZwEKKE0wyIM2GZA8+QE1e4z4N02efUs5OMckTVss5T5485+6dCZFRNMKgjaNeXPLxD36MSDJEbanqkpPXP086OuzdwzVryTrUfHOvQm7mQ2c864DX1oo3PU84rBXuzW8hXNgpv/aCK9eFqbvgpHGuTcvsuMM3Sng/Z3xNYdbmfBd5vqYYK4s2tzsvWOWrFnCtWCvx4dpe3nkPMX2t3K9R04Ny3uWlN60HfbVahTRagje7ywMXQpBlgbXHWctRS2k7m01bpVxRN4M1gndZdkbasE8s5nNmsxlZljKZHKCjiMOjowDAB0RxzMPXXgvUXZeXPH36NAAJ2xqHZzgYsFyuuDgP6Oaj0RiE4Oz0jDRLODk5QUlFWZfIqiJNU7QO4M1KGUajEdbZ4JWumlZelmRJRlmU3L13nyQdIJXgwYMHHBwcoJTC2obL6ZSqqnnttdcpypyPf/xj7ty5yxtvvsXF+Tmvv/EGi/mcpmm4c+cE5xxlUXJyckIUGd5++y3Gkwmvv/4mx8cBY+tGssUtWWPz8XbL0fYut/sq32B1+5ket1a8q6pEeI+OFJ42x1iIgKbtHEVRYPMFz3/4Cf/Jf/pf8Kd//hcs5nN+5d0TjlKBdw2REBgFo0HGyZ0TquN3kQ8eYJIUTMzv/LUBsqlYPPkB8eiYKE559PGHrJzijTsZsS84X1TkVjK+8wYH737A96ePWf3wI+rFguLpOf8bEfPuwRAxHgQ6kvfepP7Tb1HFBv1rv0Jx9wT18HWO05TZxTm2KIJVzjqs9SymU6qyQEkZFO7GYiJDEgck77qsaGQLttMUmNEIUIhyQeMlSWqQjaWsK2Q2xBQVEkiTjFU2oXKeeHJAvZhxFCueEeMayzAbBS+xlCyXS6y1gb/aBZALZ23Lw52C1IETsbZkg2EIsW69eyBI0jTQGUBYVdt8YaE1NmnCZwFKS/K8wDU1URIsTiEPx4L36DhGEIDUlJAB+dw2KBm810qGc0KAd8Fbnq9WSBWEIBPFNE1NWYGWYbGSkUa6oIRGkQn8723Yet0qtq61hpZlAYgQyhWVLdVCAOgziWG1mAcKrzYMx9uQY27rmrrMef044kF6Aflz5pdPuXx+zsXZjNPTM5aLgiKvaJxDK0lZWhZyRPTWL3A6XfDpx99muVzSNDVFEcLihZQMRmOG4wPGkwmjyRFHJyeBd91DWRSYKAoea2OwdUktaNMyYDGdESUR48lhyGUvSkQSYTAUZUEchzwhEwcAC2sDt7mJDJIwyN570jSjLIqgzKcPOBzAxbNnMP+Y009+xFd+4+/w9/6D/y1H945ZPf0Bv/sf/x/5hd/5dzk4uUeNQ0cR+fycz33wLvW3f8TR218jyTL+5J/+f3j86Amffvh/wlVz0jRmWTSsVivKxiGV5v7DE5LYML04JzUaJTzWe0bDlI8+fkYURzy4d8CnH33C5HjF0f0HvPHWa9y/M+GTOMJoiXUNx0cJ733weeLRHb7/aManP/wub97NWEwvee3hIVYNg8dgtQrgKMa0z9oyvbhASslwcrAG/auamvnlFG02gIIh2sOAVOA9UmqqMqQTKBUQzRHBCFIWYbN+1UOsN4c9Smh39MOltpCP/BUZT4irHontaKse3Rasd6JQbruyKyBoV8K2+h2+3tva6fprNWifN60TTH17D51A11P2Nk1tLAT7lI2uPuFFAKTzPa5oL0h0yjde/2UiHaOlCTqED3zQToRQSEQAh6wtGClBeH5cOP7WgWSaW04bELFGuIYCxShSfPUgZmAkSonwTwZcjU5n6sa5n8vdp5PbeiZ7xrgTajtcgLbKVhcXvUv7ytbO+NKjA2vHcNumIdhnJ1l3zXd6Va/+fn1+X5v9G975qWtzrbD1xmHHzSkQ/VvfPr/z+uylHgO8CGRJ4T40VoQ9pBEd8Fr7T5mW8zvCtHnb+XJBWeRtLmYIPa8iQyQ9h8cPWEwvKctgbJXt/rNaLpncfQMpPBfPPuPem+8TxTFFuQDnSEZHIf2oztFxhhcKoTVxlCAFTJ98yGr2nMO77xBF6caDv344+x5ya4DoDfj6ufY+9wZrU9/OWtMpTev6Re8Z7z6ILQPN1Y//wx03rK03XQNcp4RfL3ffoLSL7Z+33mwBAoVvkdKNSUjT4BV3rlUu65q6yimWS4rVgtX8ksX5U6bPPkUojdIx6XBMdPg66Z23OHlfgnM413Dx8bfw1QV1nqPjCJ0OmRxNcCpmvihwSB68+/Nr7vl1/25QMNYKYVesMxa25zdrRH8192uveJcq4p3bKid8oCqTPqSKdGwFQQl3LUd5q4T3QtO73PB1mHobnh5Q1Nuw8pbCrGq9zavVijzPA4d4GQDGAo948JzXZRUo0ZpmyxPeNB1i+kbprnsgbp3S333O83wTWek9WgU8q9Pnz9fAaUmbCje9nCK1YjQcgJDr/HFtQtpkWZbMZlOePX3G5OAg0JGlGVor5vN5iOobDlFCcvfuXcqy4PJyCj44D6bTKcvlkrPzizYqVXByfEx5WeKdx8SGSBsmrdJ8eXGBiSMiE7FcOZ6fhj57PFVVkw0ylFFcXFxinSdNEpx1fPrZIyaTEXVVE8cxd+7cY3pxwWw2XSO9a21IkpQPPviA+WxOlqXEccLR0SFpmjAcDrl75y5HxyccHBySZQH3SLasQc47hN8YJfdJA/ve5FsvB72L/G1AF3f2y5/28RLgag0ez2w6JUkyvFYIIfnhd7/Fn/2zf0S9vOS1keTocMBr45j4l3+Fewcp794/AGnwOiMva4Qx/PJwjIpTmmLBanYZeAWbhunzZ0RRTOElzz76kKrxODHitftpeKH0kLtvfYnX3nyXpav5ve/8OU/PT7HLFedPzvl3heLnjk9QkUAYSRHFiHsPUfr7JL/wZYovfIAzCTrN8MIhhGS5uMTbZs0XOhqPsWXCYjknSmJErfBNCDcvqgplDDLS+HxJkg4QcYzwFuMjaGwAxCobclczrxrS4RhtGxpC/SiBGR7g0yF2NiUbjNBxghOSfBGsRCYKdTkEo4MJURx49OI0RUiFieLg6Y7i9UKgjCaKEqSUVFWFx6/BIqxtkbdVoAqDjns2cKvXZRlCwdtnHSVxyKnzMJtNEUKQJDHloiBNM+I0pXF2HcJX5qs1mNXx3XtcPD9FaxPQ0ZXEVjWWiiTNgvCsAkJwl4PXtNZGIQW2qlsOeEWR5wxGQ7xLg4e9tbh551taKREoopSibsNxsuEAoyzvnsCBeYZdnTN//oTL5895+viU2SxnvlhSlQ1CBkq1VVFTj99C3/kiT8/OWc6mTKeXIcypLBFCkgwGmCjh8PiE8fiAdDAIFkEhMUnM+GCCMYY0HVCWOUpAWZbEWYYUiroqGU4mpFkKiMC17h11UQVkdwFWB7R9HQUgDO82AHaqBRISBF5qpRV1XjK3I+7dP+L89BEHDxJ+9N3v8NYHP8/k3rv81t//Bzz58I/47Mcf8+Nv/h7ZL/8NRBSH8LY04c3X7vHg3pC3v/F3+e4f/y4f/eAjZs+f8vz0HC8UWVIyHqYMRynv3L2LAC7Oz/C24eggpYgrIumw9QolPOOjA/7gTz7hYjrn/vEILwyL2YoHbz6EpuTh62+yOHvEG+++x5uf/yCkT0QP+IPf/4e4pqK2Ga5eMjj+MksxxGiNT9LAQx/FIVxvOm05LEc4L4JxRgQO7iRNqaqS1bIK4eMEAMiqKChWS5TRpGlKsSooXBEMGlK1VvvmJ1pI18Kx2BHZ+gLPFQE3CC27HOCbSvdLSVtng4Szfb732fc3D7Hdz1u32bYTZPRtBWB/Hf3qxNYPa4cvvb50f7aE2U39m/T3Tc6llwIjApXUpose6dtQ5BZ5l9ZAbIzhYQT/+bOS9yPLsYTv5JaphUjASEAD3I00XxvHxFoRK4USwavVz+eWreDZKeHX3n5P+RTs2Dt6xoZ1dPAVPaf3//Yw7jux93GIq8Xa89uGgCsFdyu7QQAR13zZyC57xqg/J/2+z71r+uBr64pF8H77doxFoHITUgXvd08B10qjlGm93mFPzJcR+WpJUeQtjWNFbQwIy+TO6yxm36EsctJsgNCaqqpZTKcIX3N45z5gmV2eUi+nxMND8qefYoxGRyn581PyxSWvvf1FhA6e9zJfks+fsTh/xODwIccPPkccZxsDy9oYsblJzyZ67BpTWu8970uWO+PH5hlv4aT3Xr3tS3ctIj9F6XP94r9EnXuVxusNhP7KmZep93bdEdx8G2LrxzY8XQalOoo8PhsxGLWKZFlQ5kuW80vy+ZTVcsH0+ROqMm9BQDXaxKSDEfrgLSgOaC4/RtqC2sPx3bvMZzMWOO6+/WWOHn5uvU5esem84Ma6laSbl+v76W5qx/NNN29hHamyDjkXal3Oy+4a1ijp+KBwdZ7wLiw9KOIBKHUTkt4DbOsU6LrHKV5Va97wjr6sKANyelW13OJFufZ0r3PEu9zzul7Xa9vw7qbehLCvr+t5xeu6Weevz+az9TolW9k2MhFRbDhrgXWjVq5TbQpMmqYkaRo8/y5wfhd5EbjI0wRrLfPZHCkJIetKk+cF1lnyoqAsK6zzIRq0jaY8O3uOkJLDg0NWiwUL5xiORoyGY4qyDHni5YKyCvzjSmsGgwHeOZqqbp+B5fGjR6RZyuHBAcvlgrOzM4yWJElKmqY0TcPB0SGHx0c452iahiyNybKUe/fucnx8wuHRMWmacDA5IMvSQKsmN/MyTJdglAqWSAdesmWu3xWZevPxlQNx2uu86K69ZiEQ25f8NHXwWyvegR6qQghBvlowGI7IBilvv/95Hr7+MFj3ohhjIr5kLV7AKs+ZX15Q5CuKPMe04Ffnp08oiidERmMbi/cO4RpsXeNMxMH9N0gnxyTSUueLIDCPT3Ay8Esv85zf++5fMmsabFVz9vSMX648f/P+EXqYgLOUzuO/9ovk3/oO6S9+g9Uv/CKiaaCxa6UnHU2o8hWuEWHSSRDeM1/OkUBZ1gEoouMTbBoOx0Mi0ZAOMtzhHWJfY4TAK4m0DrRBZhnx6pJKaJJ0QF3XRHiSw2O8MswvLknShJUOIGJCWxpbIZQKSnaLBqh1yMEuq5oky5BKY6IIj1xbzDyeqiwZp1krWAS0cCBYA8s8gHspgVIJTRVQNoNCbhFIagHz2QxjTOCkbJGjvQ9husaEHOvBaASAw7Ugaw11VdFUFSaKsU3NfDYlyVLAE0VRCE/XmiwbBPTP1bLl/3PBa+sc89klw9EYIyOGozFlC1QxOTwMKIdAXZU03hPFEflyiYli4jhwjHtrSdLA6R35Be/fb4jsOeXFc2anTzl/fsb52YLZvGA2XVA3Nrxx1jMrHfrhN6jUhLNPP+Xy4oLFbLqmvggI/RFJNuDg6Dig1ApBNhyRZgOM1mtvuxWCpqpIkjTkNR8OKFZLvHMBAC6OMDp4+Z1tiKIkoIPikUqFsCalqOsGo1sU+KJACBXC861Fao2WirqukUrgzJB7b3+es2/+mHz8kE9+9H1OT89o6orj17/Ag/e+yiDVeD0GV2KSMWY4ZnxwyId/9oe897Vf4elH3+F73/wmZb5geHjA07Mlw0QAjtHhAQ9OJvzo+z+mcIqD197lrNLkRYXxp2jv0Qpqa7nz4AG/kR3yB3/4Z3jreC9NwUh+8Jff4/L8jMFowq/+zb9DmiVYLyjymu995w+5PD8llYpFXvL5d1+jGb1HUXm0C7RrwgWOzdVqiRCCyAT0/gC0ElDNG+eCNbmlFZpPL/HOEScJy/k8oJoDS7vEthgGZRHQU+uqwllHOhy8+kraVxz7esL1eixbcbi7AtKO7nOdjBjkq+D96wp1Ov61m8UNOs2+fnb3cVWU35XmNudeJNNeOe83f/bpfGtPu994ekNUwHZPuw1byoDcq6TDaMVRZvhk2WC95LcPJb9/VnBeOorG4Rw46dECPjdMeDM1GCkxSqClCKjZHZo5PaF73b99Y3H1Pvcqm9352wjGeydJe8de7FSxU6HoPZ1btXWLY+dhXbmNnfu8to6tz9sdvWoUCGU64cy3qW/CB3A8OkRqqZAysGfU61zvkF+7ZgDRCqEkeQe41vIHZ1nKnbe/EtaNNGCR6CTm+9/+cw6OTnjt7c8xe/6Y2eyScjlndGSR0hPpGJPUzOcLHr7+NlqHNpEC11Q4B96WXDz+LsvLp7z+hV8kSSetWafPZ9+ZK7ajHdaeR3ZnQg8roj9uPRF246kUW8O+Nb7er0vsj4z5KRyiMzO84OgLx71zNwUUvbDhdT23Vs331OGv9uslrl97ioVCC4nWMXGcMRgdcHByH9vU1HWgpytXyzZHfMpqMWN+/oymKhHC4xtQVYHxBZGwzOY55vAN3vrab6HiwNrh1w/yqgLzwp7ubF7rfaKdX741SIaUkLYMtOf7oegberJ1XrgQyLZM8IY7aIGE8R7nPB6Hs22kQJum51pv+JZXvFXIO6V5nc+9E0be5Yf3c8PLNte7w3rpoj2bug656C2NnHVuHbreAbuVZdnzxFc0jd0CfGuahjxfsVwFT75q76+L8EySBCVVK7cakiRmkA3W8jYSkiTBaB3SeKVcrwcBOK6hLIswdlIyHo1ZrpYopSjLkmfPniBlYHPJVznzWWCBqYxhMV+wXAX8pOenz3j2xAaHWhyBEG1/JcYo8nxBlgU5O0uCjD0aj9Eq0LPJFnMnGww4Pj5mMAiUbcPhkDgOzkFjNFLpEEHku7sQO4tYu3/3ItvCStitZNcft7Kf7Wyd4U3e0ayvqeT6VfDWG/eV4/Y83s63oGomTKy6ZrGYo3VEMjxsXypPnq8oVktm8zm2DopYngd08DJfcH56GsKQtaESiqxVTKUUZPGQyckdcD7QW8WGunGtkhLjERij+Naf/gELV7CaL6gKy1tO8r84GDNKYnxdY71nmY3Q4wPEm28jfvtfRRYFbjEnTTKUgOV8Di6Eby+m4WWN04yqrpA6pixzoiQDZ3FNjZcVUSZJBwMi5cjiiJXQWJWR2CUyTVFSYKTExzH16Tl2eMLq8oJoNMFUSxKpWUiDGYxwzjI+uUNZB2/64vyMweSAfJVDU5AdHFHmBQ2COBvQNBYtFb4NQa2qCu88UZJwcHRMFCcoLUEqFILVfIr3kGVDopbmQAqJxWKrEmdDnmte5AghEVIQJylFG3qfZIOQT1wLrA10AiYygWu7lfFt02AisxYcTRQjZOifFALnPOCxTRNQvLUJXm3XtKs04D3jyQF1VYMPY+GsxePQ2mCFxDZVyMOVElvXmCQOOe7eI6Wkrkqkt9wbWV4b5fj8nNXslMvTp5w+PePiYsXlxTzk3tsGD7jGU+gJgy/+CpfLiseffczscspiPqNqF+COL9ZEEYPBkHQwCvnt2oRFLQphjIdHx2itieMEIUP6hRCCuqoxcYzRBtnmfEujwQYKMiEgzwPae75aYLTBeo0UGUKI9T3OLy9obE1kDHE6wCpLkYdQSK0Uycl7VLN/TlHeY3Vxyj/+T/7PHD54gy/90l/ni1//JYYPv8LzD/85xfSMaHSHNDvi8PiAy6ePOTu7ZPajj3n06SNsVZKmCYcHGWcXM47GA+48uMcPv/tDlqPPc/Der/BsesE8f8ayuKTMB7zz3glSeoS3zJ8/Y3oxY5JJjk+OOTufMWwaXn/nbe48fJMkjaiKJVXjQMc8PTvjRz/6iHz6nGSUMUw1B6+9z6NFhNeL1lOlOHt+GnKltGqjPkJI0mqxwAGr5ZIoiVnagDWRpCmD0SiE5wtBVRRURY5Ms+AVUwrnoSyLkE4hJM7VtxEHbzg2i/B+IWdXbfVrPWN/MdGrtlNAbqp3jyS4q2XtVtITmq7W6dkuLdg5ceXXLSXhSpU96ey6zaq7hS2vmG89Mf0wZLG2lu9T1YWXOOHX4FuRUnxukvIvLi757WPJRWlZlJbHObxl4AfO4RDEUvCVcUqqFUaH/G4lgye1603wfHfPuT8mvZ39yu3tjFM/v3ur7377GnG1rnV6gd+UW8vXV9rcvb49t1de2KPU0xkFtvmk/e4lW8+WdRuiE6Su3Gev3Ppjl0bQb2hPR3vhrV1ItZC+FepAiPCuyHZP64DW1Dr/O3jBlTYtQFbI5V8tl1Qtj7BtarLBgNHgiLp2CAHFfMZrb3+Ou/cfoqQK3rTFnLK2DJ0nO7jLxdljHhzd5e4b77M4f0JTOvADZJQRZWOmp5/QVCVSJzhXM332EeXgmIOT1xBC9sB/dt66Tqnuzl0xePR+v/KOb1tGbhQTe+107fbzf39mx03uJL/5ebsXV1amre/X9fhWQvoLLnjpXWKPgQo6JRUECi8UUhq0ToiTYVBYD10A6muqELW1nDO/POPy+WOmzx4xm9fUixkSx6q0HA0MTx8/pnaKydEJURQhpUYK9bI9vnLb61dc0AJp+p0fwmeBx/l2jWxf6A6rYZ0L3sqwvlvDW4OS9O17LD0ehVcb73nnCff9sPQ+YFsPrM06t8Up3inMVVWuQdDWCOqt8p0XeUtla3sUZnXrJOnC3FtP+xqlvXPI1dj2b9M0IVS993vXB9vSqlm3oUSbzacIApCy8wQlNstIkoQ0TYnimMgYTBQFFhzn1oCzkTFUdeA6Pz8LwH1SSmwT6JuTNMFNpzjvUa0DK4pMm8rqSNqU0yRJqKocpSCNY+Is4/DwgMEgZTgakrbc9H2nnJAhdL5DcjcmUOEaYzBGr73gnePMOx9YhvyGjcI7UDJwyndxZWulnDYScCs9ZnuOrt+jl3mp+9P1lqvBq6vX1x8vRSemhMA1NoT2eo/RgQ96tVi2kPc2UHesVhSrAHKkTUyKxNkaE59wcHiCkzJs5rZBaoMi8FObKITqNrbGqICarSICaFWSIqXgybPP+OjiMc4J3njnfZ78iz/k344M95IYJQV11VAgcF//ZZrzC7K/828wW8zJF/MAMuYdtOjFXW669wEtebmYt5a5wHFdV2VQqAYjsomiKEp8HKEGCVYqfGNpUFAtUDpCRxnSlhiV4bKM508fE7/+Fl55bF0hyhXZySFiNKGua1zTYIsCFSXcefg6dWNRUQCVKvMQYuzqCi8FcZpiogQvBGmarbmmTRS3lEs25JxIidSKydFx8JorCc4jpCJfzgNXdxyHBc1abF2Tr5YcHp8gdXhpBCLkd7iA8qikQhmN99DUDVI5wK/bTrLBOge6HcDgdffg2pznjns8UHx5ijY8XUpBHKcYE68XaxkZEKJFoC1o6pLBYIi1Fm0UvlVcA82UZxArXhvmHOop5fSM5cUpZ08fc3m+4vxizmy2ar3cIKQmLxvk8edI73+ZTx89Yj694PL8gsVqyWI2p27qgGaZDhkMR8HYkqSkacZkcsTh8RFRFIeQ+6rEO4fWGmdrlDCtIUOCqFFCoiODBOoqLNDWNoEaTobxaJxjMDrAu4ZylWNtg2jTAEJUQUE2HAGipV8LCmZYABV18xr37x3z7T/7E5y1/Oi7f8n09DP+9L//Pf61//W/zy/+5m9z74O/wZPv/R6Pv/17HL/71xgdHAGCv/yjP+Lw5Ij5xZRlUfDwwR3iSPH6g0PSQUblNOL9v42Jjji/PKNaXlLnS5JI8ytfeodf/2CCtZZhYnj+yRO+++GM0eGEtz73Dnfv3uf4zgFFseLi+RPOHheoeMDj50ueP3vOO1//V/js0T8j1QJrKx4+eIPnPGBVOVRTYHXwVDV1g3OB6qKj5KOd58VigTaGYrlEtPRPRRtajBBIqRiMJ+CDIQcZQo+rsiRpw7lMFJENh8Hi/hMde5TfG8p23oMrdVwp2lv6dzWsHcPx1atv6lNfcbzajz1633ZVe/et/Qrc9s+9DXVvOP2OWC023pJOmPME455fXx/q7GhrRSvwBYopz8hoDozkqwPPP3kamBsunOavDR0flzXCKN5KNCeJJtaSSCuUbPU76emHm4dudGO3a67ZfOsrkWKrxP573kL07l+4q3tuN8Oer+uzVwTorWe+/5qrp3r3tMdmsGMu2Cp/szFrV2Hcvt2+123Tbie494R7AdKBp/V+K4HzDoQOa7Ho+L5buk0VWBFkixeidIiUWS4XFKuA6xFYMmrKKmGUHXN0N+fxZ5/x2htvUbbG56LMGU/uEQ0GLC+fMj44QUUJy/kFjbU4OWa+WKB1jpQKkEzuvBlAQ2WIYlpMH+PxjA/vo5TZHppXeu47/uTO1rLzrDbPcO+KQf/nn5bu7eney2s00XWh/ddvGWZuuOTarna64O5g7G3zupZe/thUv990cHVJb73JqgVtMzFJMmA4PmByco+T194kn18yO3vK/PxJkBGFYnzykKMHrzMYHWCMQckw/3+aRzcH19FH9N7f9kOIJhZrpXrbACig82qule6Nh1zQKuNdWHr3WcngCe/C2Hu54d1n24Zsuz5qequE2x6nePevQ07vmA6qKlCP1VUHvNZ6xcuydcYEBb7ztAfve/C2u44irWX66TjG67oLWW9aCrUOvX3jVXct4JxtAeaWiwXz2ax14vg1la61TQsaabaUWq2DYUUrjRCgtCZNM/CesixaJTso70lLFWyMwVkZ5Egp0EaH9E8liSPTRvpayrxsQ+Hb/HytMW3UUABAVpvog6ZegwPHUU0ch3GKjMW2qaJKKaSSKN8aCQhpr16C9I528tC9NVt2Hb93udq8ujcZ8PaV3z15w+v90sa2Fxy3Vry9tRS2ITKBUw4feOg8gbvbeU/dNCAk2fiAKM1YLhbEUZtb1YaHOm/b/FzZWnlCOIkQktViThLHWOeDlbpVPjoeZCHhH//hP+Pe8WvcPTrh9/+b/5JvLJb8XJZhhine1jTeU9y9T/rGG/h792m0Jk5Tps+fU65WreVFBMtYVeK9oHGeyjoWsxVRYlrQB482Bi8kQmmyw0N03Xq/45QKiDKDchVEh8h8jspGCF8jvUNPJvC9j4l//W8hfvBn5PGQdLWiSVKkNniZ00hFPBhR5qu1dUxqhY5jlPPUdYVUIVzdAcvFHBMFy1OWZVRlGV60lqJCiMC9nQ6H65Bu2jCPqFWQozimbCmxijzH+YAOvlosSLIMExmMNpRlEYQSFXKw89US09InaK2pq7JFh9UIwstuy5IkDSjpdVlR11X7YnqU0VR1jm7D5aMoXoOnNW0utTYR1jYkaRooN1rgBylojTimtXgGlHak5+5Y89pgganOWJ0/Z352ytnTpzx/PmW+qFksV+F9lIK6cuROEb35y8jRfZ48ecT582ecPnsWQomqYC2NkxQTxQxGYw6OjhmOD9DGcHB4hNYRcRKHea0Mw8NBoKGQAttYXFVQFBUmjnDOMcwGYUNRmiSLUFIxn17SNBXCSoRrSLRGGU2R14wPJgHMTmlmlxeYKEKbCClDfvv0/GyteBfOE8cxjUt58O7n+fa3/r/cf+9LPP+Xf0TjDinzOX/8j/5bjF3x1d/613n4xb+OSQ/58Z//Pun4kNE44+OPn/H5X/gaZBM+/M6HPD895+BgQFNZoiiCwQOi5HXmz0+pyyXL5ZxIwVt3Y37pcxn5+WPS8YTRZMTb0YQv/dJD7j64h20KZufP+fDbn7Fc5SilGE4OSQ7ukf/wjzFG8F/9P/4z3OqcNx8cUzclNr3Pk7lGm5qyXiGUZDAcUeRLsuGYIs8p8hwdRRR5DiLQPQVaOoLVWbXhZsZQlUUABKxCznfTpplI2W5etmk3v7De/MSyZadgXdFEdhfTvrCyqwhtRONtYXIjde4TwK9r6qbzL+7o9W1dW8daydwWNa9UsWcHvcn+vA67xW/ZITaK3rZfTBKUci+D8q2V4leOM/IiJ8IxbYJC/kB5oqpimAz45YOEVCsirYK3W8kQIUFQ8q5m1t6wiV/3G9eP+sts7rc18l8z/V587ZZ7dfuZXnvxFg/9ixT8F1S1+72n7fst5S0I8cggyG8QloPHJNDABQOckk0vDL2bFxrdYqHoKHhs8tWSqijIVyuqsqIoEsaDu9x9KFmtgqFPiGAwPn3yCd6V3H/zfaI4ZXr2iKZYEiUZ54++D0KSpAOKskKoBKUj7j18h7KucUKRTy+p8jnnj79POr7LycPPoU28pufbqyD31wq/e3o35eCaS/eM+vWPSrzc5Ly+lmvXLb9VaP/Ft5Sre8fOTN+dkjcK2y+QxF/ieFGYd3/1uu73bq+TKlCxjiYnnDx4C2trvHPBoKQNUpqesi2ub393EXiFW93CAuiHnO9SZbY3Jzqdu73j/hKzNoj0DLLdDtKFqUvR5Yh7vGzzxGlD090mLN07j3V2ky/eC0/fyhlfc4y3Hu023aRpgnxZ1zVVC7xWliV1UwfA5Q4BvUVJ78DaOi+vbeWKEKYenCdNHRTzrg+NbVpFexvN3TYbALjgYa/XHvbGdkpu1d5j00YDKLz31CIo31SCuiyg1Q2klDjb0FSeSjiUCmwvHfhpWCcFtagxWlO17C6+DftvrA3gydZhYoMzlpgkOCh9N64NVVlS1w1xXdPEScsu1NCYiihKWswpjfIhmkGhgqlUSCQyiEIOEK6dmOFvH+fiRkPu7k99i9DW4tOv74brX3i8+vpwa8V7PJmwmM8BAvq2CJNH6ZD/iwgo1koFNO0siclGI3xTYxtLlGWYlgh+SACXct63uU9+jQRtTIwUoIyhqSq0Cjm1Sin+xZ/+Pm+8/pDPP3zI//Uf/sdkz075u9mA1EhEqqmfL2gGQ7K//29j79+DKKapa5yHg7v3uHj+HFdX2KpicTljcXnBYDxGm5j5fI4XwTNclXl42MowHE7weJyXgeJpfEhsDN47skFK7CqaXKGkoFEJiYqp8xXJ6ICIj/Effx/1xudQyzl1kWPzFSJJgkWpNSp475lenGOSlChOaTzEWUwqdaBMkgHUQEcJtglh2svFDBCMWp6+wKtdIXVQ0LLBINhNpcCYmDhNQAiK5QJna7ySRHFAWKzKMuSTxPGaWxvnaKqSbDAKm15L06V06JPUgb/QlSVN0xCnCcVqRVXkZIMhUikGo1HIK6mq1rBSYoZmTfkyn89CyLvRGGPWoeNSSpoqUJbFURwon+qA3A6he1JYHo7gteGUZvGY5eyc2cUZz5885eIieLrr2rYOEUleVDTpCePP/QoX84LP/vI7TKcXzGczVstVmzMtieOI4eSQ8XhCkg44ufuA4WgcPN9ZhnMWoSSJiZBtCLnznqqo6JSCuM2hXy0XeHygpity6loSt9zpVVmCsHhb47UJHt3xOGygbR552oJeVC06txDQ1BVJkgRjilJEUURVVZgH3+DBnX/CO7/4O9z/wq/zz/6L/5A33n6NyUHGD/7s91jMVvza3/13uPPWlzFRypPv/xnxIGNykPHJD35EkiX83M+/y5PHpzx+fE6WKFxd8umf/1Mmb5xxmBwzlytGB5p3TiK+/oV7JLHG3H0NLzTvDl7j8vyUTz5+xO9/79tYF8LnXFNzcf6cpm64f2/Ce9mAe++8Q7laMZ+tyO494MHnvs6Hf/Ev+WwRI+SCQZYRxfGauk4qSV0HXtSqKhkfHJHnOZJApxe3/5y1IW/eOaqqQGuDiWKywZDZdIaJDMpoVos5ZVkynkyYHB6uNzvX8Xi+4nEdR+0+RWct7/SEaHFd4X3V3lbzuukicfWXcPoGb3S/vNi9p/4A9BW223V0MxStit0L8e7a6tDAO9qbTX/WBdZdlzLwc0sFsVK8M0r4blUxUfDjRnBi4FArtC/4a5OYt9OIJAqKt1ai5e2mVbzFejzW2/feW9qvVu99XPv27etM+vvi6XaU4y1jR0/W2IrIv3He3NBG18660pvqueZZX3fNVdvNjUeQz7vZIDbj0wv9l17gvUAIjxMCIV2ItOtxfysVomZUy4SgTYwxgZpztQje77qqmJUlVZUyHk2CMyHNkDoCrbk4e8rxnRPiOOLy8pTZxTOafMFgNCbKRsTZmDpfMBrfIc4ysjSlahqEUvi6wjUlHklTLZlfPKWpcw7vvMlgdNKG6F7/Bm3sGztK4hVl/MoIbv/wAnvJT6Cb/cTHlser93f9+75rep9v8n7vsaNdrek2CMh72nmRMr2npZcoFwxJUii0jnsN3aBo3/bY8/xv26c1JJbYfO8ijyC8oiFUveMKZ9d2FMZto8Gv32HfnhcevAoVybaMk36Nmt6BuzmvN8p4F57ubFBY1zRmwTvegblZ23nL3VqJ7uhpA4hbyP/uA7N1ynkH9LZGWq/qAPrWoqVfAYnr2t8Kme/lr6/btWuDwDr0veWLd+39dXPNO7+9V7TjKOXGAGmMDmHiWgfvtTEtq1CYUyEP3aJkoHgrq+AkFaJqywhsizGk2jGWSrXMQnqd+x7GKSZJGmyctF59i3MRxhmcbj3oSiE7O1ELHix7b09ni+lNiSvze02OcstN9sY3+pXkqpc/bq14r1Yr4iwLE7flSV4tK0IIp8J68C5YebQJgFR4T5RmYbK0aN11XaNVQKxGqvVLGJng/Zaqtc6sllRlQZxkGKOZ5Uvm+YovvfkW//D/+Z+wfPyMfzcyvH58yHJ6CXlJIwXid/41xFtvs5xeUs/m0OYZ10WFcDbkyiwWFHnBYDyiKUuk1hgTka9yLs7OieOIbDjCpAN0liJURDIakUkREKWjmEgrBgcT/HKOUoqqrlA6obYlcTZGxGfEd06Y/Yv/nnv/wf8O89mHzMQQc/4ce/dh8M6VZfBam4jxyR2kMkgpGJgYaQy2saTDYQjDVsGqpaREKh3CrD3B4BHHNHVFlCQkcUKer6jLECYf6YTG1lCAUrIN8Ya6zIOgERl8HCGUAtr6W45tHUfBMyslrgmLwtnjR4wPDxkMRuF5FqGd5WIRUKidZ7mYo5QmHQ6xba60bQomB4fhxW2a8Px9yC/RxuBs03IrO6YXFzhnGY3HQFi4lQoImUprjKh5847gQM0ppo+Znj1hfnnJxbNzLi4XzKYhj1hphXee2aoifvgVBg+/zPnFJZ/8+CMW8ynz2YzlcoVzwYiURBmTwxPGR0eMxwckSdaCqAWwkqYqMXEc+MK9I47SAFLnmkDlZj1Jkq4xDIQY0dQNnhLwGKMoypy6KEOEglIMD0+o6ga8p1gtGY0PQEBTB0oIASRpQl2VKK0ZjsdEUUJVlWFc202liO7z5ue/zO//v/4j5OguR5OUH37/hxwfTfjCL/8q//V//B9x9vySv/vv/S+J04zDh2/z3s/XzKY5f/zHH6K1ZzJJeP/9N/j61z5gMBkzyEYh78gYoiRBaYUxKc7WlGXDxWzOxY9/wLPHT7i8nLLMaxyKJDZcLkr+4ruPeO1QMkol8xzmPzplVvwhh3ceUhdL3ro/5rW33+fxWcFgfMgTazD5kmI5RxvD5OCYKI5o6mBxDVQfFYvZJVIZnBBcPD9lcnTMcrFkMBhQupw4SciGozY8yyGka8e0Dij8VQUi5HiLhSROUryzwYD4Exxb8s9Nq7vY/uiv+xGuSiVb7vJesT268u1U3b7A3ncj33T0+rBu8Job7oR68UJ79a0k+zC+26L1Wij3oYUQkuoRSKTwKAFaSRqluJMlDJKClaz5RgRHieavTQb8/eOUoVGkRhO3VGJStOirrdX9at97WkHv69Ub2lHA/QuG+MplVwfmNnLBRvBfP4T1tZ4buk3ngur/ehsF5CWk9h3rQGtqYdPDPcV7v6yFe98J+6LbDjeSWFteSIlbe8C3c72lDOGPWql1yLk2wQuudTCelkWItLGNZThMWK1KRqMx6vQRg/Eh9978HGWxQmFZzS6J0wwdDzFRxumjT3nj3S8wOb7PcvocdEy+nOO8DWtOmaO1xNY5aTqgzmecfvwt1DtfJckO1krLfgWz/3R797zzfnQje126/XXL1ZUx31Pmp3HsnVl7+rivD9cpt7tmv2sbvP7iPYv59Qv7+i3ZKbqek9f1Y7f6Fxy+eydvyH291bF7zU/6cHfeu3Bq07cu7Fz08CK6vot9w7rGcaBdjnqUZp51frz3HuU3qOmdF1x5NmHpvX9uRxn3fsM13tEKO9/lkocQ685r3nnQrQ1K5CbcfONVb1pH4xrora7WCnunQFet8m5bj3aHjl7X9SZP3QbKtTXqerMBkwt9CJ+7seqU9t176LDnu6V2rYS3jD4BYV1iTLT5LhUIifPgHG3fLFI06/TG7jkIrUGAlHrtuHDtOKxR4NOaJmmImwYbh/4b5/A+pK8G1SMYTaSUONkp3w7n5Ca6SXSgmreQIwS9YMJrd+4rl1332y2ae6njJcDVHIvZlCrPGR0c4b0nHQxx1gU+ZheUogEe24QHEEIUggdVR3HLcxxTW0sUB2+0iRLKqmR6foZOQhJ/VRSYJCFu85OTNGU6PeML91/jP/2v/1MsEe8vC37ljddwVYH3UC8L5G/9T1h9+evUs3nIeXaOpiixZUG5WlGVBXUTrE1xHFPVJUVe0DR1mHRSMjk6DgrO4REoE4wMxuAgAI6ZgCrd1DVRkuEFIRe1LBBVCNFYVpbMawYPHrL49E+x3/s28XtfIDp9TL3KUU0A3UIqTJoFYDEZwkjqylI3DtVYiuWCbDSmsT54hPE0QJoOiJKEuiyRApIsw9qQI+29ZzAYtMKGD57wfIX1HrwmXy4ZTg7RWoeJrjVJG8IdxQllGQwTcZJgtAl0K3mFkoGK7ODoCKk0jW3asPEYqhIlRMvpHUBthAjoiFVd0pQVg9GQfLUiTpMgAHUgEEat0fLLxYLhaESchDAV5xxJkoYFpl1oB2LF20cViV+yunjG+bNPmV9OOT8959mTC4qiCaiZStHUjqWVDN7/LdTkAZ9+9gmzy3POTp+SFwV5HkAmtIkYpRmD4YjRwSGj8YTR5JDx5GDtaY7jeH1PxkQtv/eKupYhLH04QivZ8sG3/RUEL6tSFHmFdWW7jwROTiM1zjbEURS8upVktZzjkbgmgH4kaYq1jjJfMTk4CGAp2iBtoIHTxmAig/MRg3d+jYM//3P+4i+/yQdffI2DVcnv/u6/5I0vfYMvfe3nSEYZn334F9TFDIlFuBXvf/mLHJ8cMjs/x/mCwSAF31AXFWfFJRAiFuqyYrFYsFwsWS5zqrqhtp7ERFjXcHB4wKPHH5NEEuVSPvzeZ7jaMcompMMBP7ws0L7CfnqKRvDOm8fcuf8m33s04+yj79CcfJE0mVAWJZEJ6Qvz+RQxFzjvGY1GICQ6SgLqe5vnqaOYIs9Js4z5bIrWAdE8RLJEOA/5+RkmihiNRgEgcjjCViVSGxaLOXlRkGWDNo/+1Y6bFt4XLcpBn75eabmyIVxRyDaSi+95GK7WdX1PXmgAuKam9TX90FjRL9ErtY45vFrfts4vQHjETVvaFTfJ7sewSUsRPJuGYM0/TBMejjN+47DmlweGk0jzHxwp7g0SxklEGuk1mnlXy346rOvGZ89990QFf13R3iVr0bRFk9s43Xak9KsTolf3pty102BX2N2VgHsX+t5F69zqdbHbiB4bJfta9+ArHFspCL3KgkdNrHNEZScorsPPZat0bwDWpAwygNK6dR4YVBSh5zPy1ZK6rpheNlRljZCKg3tvcHE5ZTmbYbSkKHPKusGLhsTBxaNPefjW54iTjNn5U7wtKQRE6QRXVyglGR/dYzF9ThwlDA6PWS4X2MZy9uh7HN5/j3RwsGYpuX4M2hHeq1Duvpf7K/hZKdV/Fcc1S8H1x60W5Jta2veOX3Op6JXyL3gO1zXRnfB+u5Doz/6Xe4avqmTcdOwaw3YNGJt1Y+MN73q+jV4fynkIivpO5FPIDw/XrinMaJXw9vrue+cF9q0Dzrfra18Zp1PI6ULWg3LuW0W+q2d9bpcOrVPcu9B2t/Go29aD3inK63Odct7xireAbOvPdesx7zzsLeBvlxPeNEGBBY+zfu25t2vjgF0bD7p72pUXgre5Z4hUwROutW6xMAzaKGT7PXBuB7nL+ZAG7LxC+BCGrlrPN3isFeuIgu5+bWKxrSHDOkvko9A3Y6Dl84YAYOqkXCvfCNm+O51BeHf/Ye+Lv/uWij3nYUOHJ4TfMAHc8oV6xW3r9oq3Vu3gCxkikZvAe1s3DcVqiUlShBBU+RIdJ0gEkRI03oek/16YV9yG0nohaeoqwOkPBnhEazUJCNzJYIR3NoBrxQn/+X/2HzFLDPl3H/FvjsccxRHF5QVECfz812l+++9hlKJazMA6qjynKUuasgj5zM7RlCWrxYJ8uURKQVUHYDWjNJOjOzgBJh1Ayy+tohSTJERxEkKhlSYeDImcZbFckCYxRAlmMMKqAuSIBE3+2UcYEzH53Ptc/u4/5uTzXyI+ucfi448QTx4Rv/kOUmk8AmlECBeOY6IkQxkNCNI0JR0Mw8RQgfJMKomOAndtnA0C5zedcTDMFG0C2rZvJ20URYAPIGp37gYLWWNDqHlLXeZs8EzHcUpk4gD61TQhrE4E5HATGZzTLGbTNqw3oBnGaYo2R2gVPAhCCrzz1E1NEicQB1T1vuJqIkM2HARPNlCVJbZpgtI0HJG1YdZ1VQYORAV3R5a7SY6v58wvnnL2+BFnz8+5OJ9yeT4PnuM2PG+Zl9jkmPRzv8isdJx+9zssZjNmswtWqyVVVaG0Ic0i4iQjTgfEScL48IQ0zVpwnjCeTd1wcJiFnHPvef7sSdvH4drIIKXEuc3rWBQrItNyDMtAF+OaGq0VcZyio6i1UnpsGcLzozhwfJdFgfOO0cEhURzAxGydsFquGIwVVZGHEKeyoK7LNgdc40fv8M7n3+XHn8356q/8Jr82HvBf/t/+75w+ecJv/e1/wPPTJzz66EP+7A//gFUT8eUvv005PcXXJXfun6CThLoouHh+ymz2CbN5GYw32pCXFa5xSOFQSUKkNQeJ4eQo5clnjxF+iMJy9+4Jf/7tTzA47k0USQSf+9JXee9rQ558/CGjyHH/3hE6GfMvf7jgyQ+/xZ27d1mqY0y7OYeQqorR6CAgj1YlTV0zHE/Wm0tVVwjARAlJi9rvnaUsLYHyMMc2M+JsEOgAveOyqVAqCikD1iJE8HYbE1EWRUjj+GkdP5FEs0csWu8FVyve9Xi9sO7OawJbG1i3g714z3kJSb1rZqOhX/3xhuPa0leUTn9FAe8+SEBLidcaF8F744x//81gzRdCksSa40HCwSAKhiPZjfJG6e633Ve6u6HcfyfXmDNeem6IK5+vPGm/KbUJQhD9P1dq2RHjN3d64yPaPuHbBm4qdr2QvyM83TAwNw1ZGNMOzGmjjG9yR8E7cB1qrvQIWuC1NswyeIGaVvEOQEQmijFRQmQMOopYzmfURcFqlfPpZ08ZDROGh3e5ODvl5P5d6nLFaHLEt/70T8geP+Wtd95hND6kaSqm50+JkiHaF1w8/phoMEHHGcXykjiJGRwcYb0gjWOms8eUJuHZJ98CEfHw7S+RDg5uGIFuHHbm5c4Y7Vsd/odRuH+CVl+wOL2qILy/ll1R/RYj2F9OxdVrunm5Viy7a4Km+FK98r1v16wKt6vop3zsLD/hXKvQhFtveetb72UHqhau6aFZe7/2bG/tW4KNAk6vTOdN7xRqITb84WJjmKZTvHv1rinP6JTx/ufNb13d67I9pbZT0jfh7m0L61D3HjL7+p9vsWbsGgDOORtC120PDK7efLbWtkp6s0Zvr+vgqKrrZq3kd0BvnYe/m2f9KCEhW9Nyi3khpFgbIgN3+IaKUSuNkiqsm0IgZdBTREvhCAEMTsrQkHOBfcnZcM/WtdEBrZGgHc71oXsrmGzzz4Oy6QNjan9929a9tyfezvGit+Lq7tq78Gf0jtxa8Rb4VtHSWOsCYImS1Na2D03gXeB/s8WS2gukMcHqoS1KKqRWNI3Fu4DaXNU1cWSoqoYsG9DYQMOksyEmjrBNHTifgT/6w3/Ch/WC6rLki6uKL929Q315QS40vPEu/n/+76EHQ5piRZJmrJogWC/OztFRqKMuS4rVitVyFbistSLyYD0MBhlOKOIkxXnP+OgEHWmqKlBmWWvRccJgPAne/ixD6wghWkqrfIkyIeRDjsbIOEP6hoMvvs/0O9+i+PM/If3GL+IHwwCUtVrh45j5dMpyuWB0cBRoqpQOXl5niUwSaAR0EBLkUFGVRdAtpVhb4PLVAikk2XCIrSoEHmUU3iuEEIECDLB1TZIFNO04CTRZZVFQlwVSSaqiQClFlIRccu9c8BYNhjAc4mxD0ZQkadZ6GQdoJUNINwpX10itQUjKuqCpqxDG14LjOROer/esQ2YEnihNUVKSpCn5cs5iNmUwGuO9Q0lBLBvemdQMmGLzFYuLpzz95MdcnE15fnbBfJ7jnWgFLM+qqFB3vsDB29/g2fPnPP7sU+azKefPn9PYNswdiLVBRymD0QGjg0O0DjnlXXg53pGmKbrlPM/zFWmWkbZGJu8d4/Ehy8WSi+en1HXNYDgkTlJGk8OQ99KOfzYat6BpgYO9KkvwHp0mFKsKN5+iTEy+mCOURknNYjqlLFaMD4/WKQZCSJCSfLVCymBVrMoFos0JT976Vd59/UecfvaID776t/mdf+vv8/TJJWZ0gH/8I370/R/wj//gB7z52j0+/6WvUKqG82efcIBEFzl1mRMpw/37KQ8fSOazJRfTBfm84NmTS7SWnBxnfDazfO7d17k8u6BpGlaLBXfvTPjhj0+ZzXIiI5hkmqq0fOeP/yVvvPWQNx4cMp4c8enpgj/+86c00094MFZcmIfUTb22vtZ1ha0bzs+fo4zB1g1lWZLnASgvimKUNozHI5SU5HmOXwUQvsnhEXle4FzABKiLHBMnFHmOMYoyLwPgn7OM0qSlLxFtZMKtl8Mrx5U1+gXCzf6f/dVdQlwjNO/R+PZy9F459ihO/fp2iuwV4DcOl73Vb9rfIypet5G9QMG76QjjE/7fEkbbKiRsDGQCIGmjKgRGSSKjiI3CaNUqYaGOfQjgV1TM3i2+nEmhX8EeCaL3aHcYoa60u//oP0Sx81y2/94omOx079XlkKtz84pgfptjd5C36tgYGkSrhK9DrIUI4Eyd91sIRBt+LmFNcaNawTMYkQ1SteHnUYw2htVsSr5aUhQFZVmRZgnSVgzyiqqsWeUFb73zNq+/80WUhLLMKVZznj95wvGD11lOT9FRSrVc8PhH36ZcrfjqX//bFPmcxAyp65rJ3Tcp8xWXTz8mGY749MMlb37x14iTwc5QbEZzS4F7wRPo3pEtvXDP+P/slPLepFprordo8VpB+NXevCvDtdWX6664qR83XENPEfe9hv2esnuWmDDVt3+4ZiW/erQh4FcpB3+CY60H70kP6XmoO0V3c2JTdpO3u9lQtnjC+6Hp4aXuPaJuLxF7p/069cQTsJu6etfd6Snfvrul7kNPge/fcqdU757rxsP7NQ4Rvbq9d61dpae4t78531fCt/91TEW74ezdX7umNbNrULi+Ih7yzANKexca7+0GCb5P6dalaPWfTdgvg54nRcAekkquw9G7VJ32tgJ9sGCNE9WtwbWvcbmjrm3gZm+V7nUaTdtW9w/h20gHT6d7dzFjexXu/rEzEa4PJNxDJvYKL8jt9+Pt4/ah5t5h2lzf5XxOkqakUUaWqR5Nh6KsNFW+xAhJWdZhlEwrRTiPtfXaiwmeIi8QQtA0UFcV3kMyyFovX42VlvNnn/JnH32bShnqywt+Lc6Y1AXT2Qr15lu4f/C/otAaihXee+qypKkrvBDEoyHFfEZdd94s3yr1Ia/WOk82HoOOGB8e4QjQ+iiJMoYsioiSlCTNAL+2kFvniNKkDcsQCKFo8gXSebxQqPEBdnZJNBwyfO0NLv7b/xr97vuYOCMvS2SZY1pv792Hr4MIHOEekEpRFDlKR8imIYpjpNYoEfKwy6JAyUAxoGTIz4iSJHib4xilFbYOXIEQ0MCdd0gdQkF0m8cdUMItcXxEka82CIjOhTHwDm8djbeBl9u7kAsXJ21OvsMBtizXL421Fh0pojgiXy7Q2WBtAYta8DbbUnuFPmjKVUE2HIQw+dE4hNO0oTJ3x4aHwxXKLihWlyzPn/Lss894fjZlNluyXIY5gwTvJYsGhu/9Jrme8KOPPmJ6cc6zp09YLBbYlkJCG8NockCSDkjSAWk2YHJwRJZlAfVaa/LlguO7d4mThCTNiOOI2cU5SUsrFsXh2Tvn8N4GD/5giNSa1XwOvuVCjyKsd5RFjqstpQ0ciwGxXZAvly2YRZiT3bukW4NLNhwG6jHhsE3gmk0yDTikipBCMhpPmM9nlEWBPPoSb/381/nku9+jlmO+8hv/Bumf/LecfvQtPvvhX/JsJsnGJ6zKhvPHPwYdo+Ihj59egqvQWjAYZkwLh/I1tnIonfDgYcaD1+7ibIiWUGmNEDWnp3Ost2QUVI0nzyvu3Zvw8O4xh0cZR3fuYJIhTkRUleeff/MJP3yaExdPuZsWrLKvMC8EdTPFphkmioNHOgkeft/mM0mpqeuSpm6wdeDZnc9maBO11GoSh2B2eRkQ+bVGCYHVhrpuSLIscFtmEWWxQnhoWiPbvKrJl4sA2PWzPq6u9u33nlC3d5vYLt6v5qV7fYOAvhbG+2Vu2UBfKd+6cKuTu5WFxm6tj7+offpDKTZAPoS/SmiMVoyztdy2zt8O3/f4uH2nu4orHXpx/25zBz1Txb7ie57D1nx4QRPXKtidMr5rD9lzgdg5ufuYb5oqVxS8n/Fr1j0/3/3f5op29Hgd5Zx3m7Qo0QKuNVIhZb0OrdRaYXSE7vK/tUEZw7JdbxeLFXGccLlwjEdHVOVjHj095c33v4zAUqyWzC4uKYsqOCZMwnJ6jokSPvjG38B7S51PQQbcFoHAC8Vy9izs+SZFSjj77C+JB4dEyZDB6KilJ+sP7raEedMQ/xWsctcfu/P1ZV/wro5XuX5PV8KHfQbBGyp+hTY374BYvy8vMnz0O7le115yTd7pwfrjVez7lzs2V9/ckeC83jy4NXQE7Zrig0wNrBXBTQSNX6/HawXKr+0IW/1YpwF3jfb66Z0P1te+0rze4/zWPPD9K3fa6Pq4tX36bWV+MyY7ynuv7r5HnbZ/u+HvW3nnzoPfKMxd7vk67N1uFPUtKrWeF30L2G1Lsd/ksa/zx51d57T3b7avHKtWGRdyY8DzBHaqgEAfnsN6L3Ye5xqapqJpNHVTo2vT0vD6Ndia3zsvX5DTfcNx8zq4T/a6xYU/hePWireSmsVsjlaQDTKaKuQaGK0CqMCaL8+houDBNOkAiacqC4rVCh0lRJEBIQMSNw4TRSznC7LBYE3TIQDb5jcIHN/+iz/imXOoxjF58pwvfu7z1I9/SG4h+Z1/nebwGOXBeku+WNBUBfPnz2mahnK1pKpqynxJlVcIFZD5UCGMLDGaeDgiSVOiJMPEEUVZBk+bDTQNTV3TmDoI79ayXCzQkSGKYqRWCAQyilAuDWHbxYr4+B7Tywt0WTJ8/33Ov/t9pv/sd8n+5r9K5WeB/69pSAcDpAoKFkIErmEhGY7HNLWjKguauiEbDXFSoSNDfHBIsVqGkDlBW4cCL6jLAC6FD2BdMoqoygIpFVGcBiovE2GdpSoLlAxKslJh8avrmmK1RLdI3HGShHxpa9EmwvsaW1VtDotde26FFBilkUm6zkcZTQ5aJEkXAOEay2oxI+Rs+DYcHuI05Psro1uDjCCS8HBkuZNc0uQX5MsFZ08+4dmTZ8xmOYv5kuWqCOi0WpMXJYU+4PArv8GstEwvL/n4ox+xWi5YzOfUVYX1juFoRJoNSbIRWisODg8ZHxyRJCmuffMirTh48ABaui5jDNlg0HJK10GQd5bVssDZBu9hOJqsfx+MAvBc0zSh3aaizENetFLBqtsIiVCSOM1I2xdfSUmSZXhnmU8vwUM2GFIWOR3lnpSK1XKxjo4wWlMWRTAMOYdHEL/zr/BGXfL9P/0DvvE7/w5v/fxv8dGf/Xd879vf5U/+5fcpLcwaR1485P6xQB7fY/btS+aXcz788BOGg4h7d8ccHQ6RUpOmKZX1OC+oa0ccxRweRqRRxPHRHbzwZMMhkVHoKEGoqF10FUVV8/TZBY8fn/JorpjWCrN6wkCtqIfvMLcZzofw8KLIsc4ym10GCkGliVre7tVygRC09D+qpd+rEVXBch7yi5I0IxuOA5OBc9R4KEuyNGO1cOgowtuGuqoZjkesVisGbV53kg6oquKVF9KesZi1lA+3WMxvJ4X2c2xvLH5VN7ymvtsdW7Le7kUvsj5fafTFWsEr733imv60kt76vAC5R1ndBrHqDeL62msGdkcZfnlx9ubB2DPkV67qyfPXV33thXu6sE9D3/t5TyV7NfkX1PQyysRtJ4Xo5OrwVILwLtYhr+DDRPAe6dtwSyER0rXpUhuHQudUUDpQoyoToXRQvovlkiJf0dQVghGTk4dcXlyymM44PD4MYaR1hYojHJInP/6Qxjq+8Rt/C+strlqyWswZnbwO3iKkQXjP4vyCwfEDxscPyZcXVNWK6fNHOCAdTLj/9s+TDiadReglxnCt0rzUcP7Ujldo8Epf+/d7RSP9Cfr0sx6MK2vKDe3urFFbRV64j2xXs7/EPivGDcfuQ3iJsRL9B7cbTr/WxP1a9u8ME509pFNctwyfVx752sy2qbr7K/sl+gulx7t1Ygpe+KCfr3/2vev2NNdX01+wlu5VKPvKd+c2bo912Dqt4aCtYx9g3AY4rgtl38059zhvW9q1Xg56T2l3rQe8854767B+G1RukxNPkOv7Bqte34OxU9IRj6jWS65bEOsoDtRi2oTQ9Y0HXW6U+24nFtsbWzeXXih39H6/tZHrdkVe2N5tjlsr3nVdEacJRklsUxGlUcjZRVCVBc570jRF4JBSt7H9TQtbHxGnsuWzAxNpGgmgcC4I7UmSIjvlryyQAqI4QfqGH188o1AJFz/4Pn99WRIdjjn/boH/0peYvfke9uIicPMaE2ihhEYZw/zyknw+Z7lcYetyjUieDEdEJg6KkrNEwwC6JI1BGoNBILVmOBhQ12WYHCZ4N60nUKOZmCjSxHFC1ebkIgQ6Mgir8YfHiPEBTVWj794nOjjg8g//EPWlr4TvWtPYCueDl3h2ccHw4AjrPE29CvmnadoqdeNwb1UVuMilDOB0VbWmW9Iq8BILKVjOZiAEURIHEDNAtzRW3nnKIgfcmnqpA0RoaosxgXJpfnkJLUBbpA1hlQiLY1FVgQPVRCjjUUrTce0F1MMgxNimRklFXVcIazFxgiAYXWgtYtlggGznV75YAJ6DRPL2SUPCgmo5YzU9ZXr6jKePnzGdLlmtSsoqANR565itctTJFxnd/4CPPvmM2XzKcjFnuZgzm82CUKUksQqc2IPhmOFoQpKlJGnKweEh2XCEUhrnLHEchB/rPHGa0dQ1y8Uy0I21tFW0od1VWbYgc4GWocDTVDW2rkAoGlvjnUdHBkcwNixnM0wUIXzgZpdC4m1NnGRESULaUsFVVYm1DcaEHP1gGRUYE1GXBUoKiqJASbg8O2c0PsBoA6PXST7/O5x987/h//cP/0N+7e/827zz9d/kf3b/dd7/yj/le3/x5zx+dMrH3/8+iXqTuHI8eP0hVVXzzrtvMZpMeOONu9y7M0KbGNvUlPmCVV5wdplTW09V5lTA5WqBVopVJYjiCKioLCwXS6p8xWqxoMLw1B6B1MjpdxlENXL8JhfuEO9qdBQiL6IoDu+hqKE12HkXqHe0MeCCRRcfUhXSbMhqOW/HRwAiABUC1nuM0mGtcRYlAG9wDqQSlHmOVAqlNVGUUFdVGLtXPDwbq/uNheBm5Xhnh/frk7u/h11t7UwQV0ruNrm/nVsIgb2im/7vyhFXJMPbCXTX41jvK9v/4nvGji7cMPy/X2/chDGGfLT9lvVN98PArMPOb7mxboptC4I36bv979fJ1FdGdM8D9b1PG+ExnBHd6PhbtNddeq3yfNun1p+3Vw1Re0PoX/XYM9Dda0JP+e4KhNxvsRFcBQjXQz7v8X0HxTuAsBkTDO5RFGHimGUUsZrPqKuSs/MpjR1x560vkK+WjMZDrAOTJHzyzW9ydvqMk8Mj3vvSF2nqnGo1Y3nxDBVl1GWOJEXGKc5Z7r/7JaJ01KbJaFSUocwKrRSPfvhNZpdnvP/Vv0E2POjf6ItH8ydQoH52x4s78eKZ9pP4cPe9ifuOvrfz6jUvVmU7D1vverHn950Nol/8Ns+r24v29qPdo9bR3eu2/FaRftu+3+093XgpZaXzYMPWGu53xq+LVFrXu35X12/wnk3oar+6a/u/dV5rkC01WRfCLraGfn3vYs/n3Yb7mrfwV+wL/V5t/9Zt4n6nFOt+dX3ut9UPV78Syt614XvgcFve9BAO73YUdOhSQH2rmNu1Er9WvJ1rw8r7yPBu3a5ArMPBuxxyKQJgpVKqBW9rMTRaILeASdUHuQzrrpCyZRYRXYX7nu4LRA2xW/rKb1e+dsLOz3BtvLXiLaUKHu3GY+IE6zy6DRERArwNFpcyz1FKI1WYoVVVQVmiTYuIhydf5eHhNTVJNiBOYmQrDAU0vgAukA4GzKYzxOEA9/yc7GzGV6KI6N59zr0i/u2/TTwcsFwssM5BXVHMZ3jrWM1mlHlBWRY0ZRE8zA5c49EOfF2BSjFxHEJ58ShjcD5QXFnXhUOrFjJf0dgGIxUmyVoe2QDopI1ByhG1LBBK0jSWqvY4qakvLmhOMqL33kX8wR+z/Kf/hOG/9Q9omhra+0yGY5LhKCjuWlMWOUKE8YyTZK1AeyGoq6Box1EUUARbkCjnAlJ70zT4NrRbqECnZSITPMpphvUBRE3pGCk7Q0eJ98G767xDC8XB0QnOWpRWbf0WLwSurhlNQr6yVJJytVpTsjgb6BKkVzgbwqYFgjgOChXCI7UkIoCyNdbStLRYIEjTmDtZxWvZAl+vKFcL5s8/4+zpU56fXrJcVSxmS4SSmMhQlhWzSpC9/RtU5oDv/+AHLOZzLs6eU1YVRVG0hglFmg0YjieMJ4ekgwGHRycMhiPiJCZJUwZZFu6vqbFNwBzwQlDXDVWxohGSOBuQJhlWNrjG0viAht8tDPNpCHMejQ9o6oo8X5IkKat8QV3XwWvtLKPRiOF4Qt3UqEa1odHBsLJaLijzJSaO2zkZBauhVFR1iW+qFqlTQBMMLWVZMjo4BOep64okzZDH7zP56pBP/ui/4v/yf/jf87Vf/03e+9Ln+fpv/z2+/Ku/xcWTTzl7+in1chqQxNMB0/MhR0djfunXfxUtLNOzU2YXZ8xnM+bL8M6WhWVZFMTZmMOTu5w/P+Pjz05Z5k/xFoqyJIk1g8hzMjJgRnw8zygXz0jqMw5ShTx6lylHKG0AgbeO8eQAISRFUWCiiKLIiaN4vfZppaisZTw5IC8K6qoKaRG2YdkqzVVRMRgHA4q3Qdl2jWyBTTyCAqE1gyyjscESXOQ5s8sLxgeH64iHVzk6+X6vgtsdfufv7u+3aqePY7tbX0/lEn1RMBTqBMZdv4BnZ0/bamGP8tQTPfYLXZvfrujju33u7Z17FcEbxqoTPDphSLQC5W4d66Z6D6nL/V3rLFea6IlfW/t7D6G8G+o9+/+tg+N643DFltENc/uAxNYFXR93Qh+36uzfWFuqbz1pC/tOOb9OuPc7bV77UPcce57zX/Wxjljw4S3Y5AxuZqhA4mUQgrvcxi78fE071nlnVDDsmygJ4GtRxGI2pVgtOT+/YK40w0HM5cUl48kQgeX45IhsMOLNz72LEFDmS/LFjPOzMx587gFlWTCfnuOqFVpKhnfeIBkqinwRIszsFOfh2UffZzW7DGlP2SGf/+pfb2WD28+5l1KU/kdw3NjPv+J59bNr55qa1/d3exPlbomeU3nd1NU1chMy3C2T2+vFT3fehDY2a9IVpX7H0rnO7e0pR+Ht7T7sV6u66/qGSLFeWDvDw7Zydt2W029LdJ3qSqwLtr0SXTv9m+jvwtvjuelfa3Dole33fX126/OenPS+At59991Hv3PtBiRuHdrOC7zrnRLOTp2O3n68AR4WUrbgbeGvUiGiqKMv7v6J7i9ikwYktle2fi56/83Y98y2pJA98oPYLvFXtibeWvFu2txU7xxFGUCi7LwK+ZhNQ5oNAke3DvRJ3eAnWbyh9qpq6tUqAB85T5JmaKUCYl9dBW+i82gTqLYCoNWCs6KhfHzO+1XNJEvJqxr3wQfI974YKMsIL9D07DnL2RzpHLPplLoKIc9OSLLxJISSKI11nuHhMdlotLYkKWMQSpMkEXjQKkEIGB8c0DgfeMnbR6xa6PumaaCxQdx1nng4IIpT6myIWixwTz+lQDAYH5F98UtUH33C4vsfor79F7j3Ph9CO1zoE4DSmjgOeadJkqAjQwgZX4WNXhukDpzXdV3iHSgTBauSDyAMdZGHkP4kpamrYKGPQ3RCWazQrbXeWrtZyjsFZrXEWRdC/oG4DRt3zhHF8doKaa1roxc8ZVkipaLxFiFCuO98ehFyONpQPWMMkZQh/1tpytUKIRW2qahLSxwnJEbx5qTgyCyoV1OW0wsWF6ecPjvj/GzGallQVjXSGPCOVV5SmBOOv/rrfPr0lPnTjzk/e85iPidfLZnPFwDIFkU8HQwZDMckacbBwVH4nMQtwnocvPJCoLRpgf4sSZLgPDQyhBk6Z1nlS+oicLpqo5BCMRyNWM0XIfXCRIGbvV08irIkTjOEWGGtw8QRSRTjvSOKIhqlGQyHXJ6fIZVilAXkbikVVd2gpacq8jatwbCcr7B1TZSklHmOc5ZsMMY1DmdrhpNDBEFgrNWQu7/w9zgsVjw6O+XJH37IySRhENVkccSd195BC8JzKHLy0vGD736f3/vvfpdYVSzmgfM9ieMW2bzhyekln53O+aVv3KXJl6zmS06fPCbWGikEQwWHmWQ8zjgtDE9mQP4px3JFNMpwo7dY6jvggsGoU4bKolynmmgd4T00LSq+FyJ4vbVhPpvSNHbN042QGB3mVhSFiJeLi3OMNnjvGY0nDMdjtDYBl0FKVqvVeuO0NsLEKUWeEyXRKy+kHq73ePutP9tF9l1zRZO6+jOwxyPZl5Lad3tdRmwV29SxFl9e1Cwgek6GvvrueyXaMy8wLryKANuTufZXSqd8X72T3WHY3Wiv9OdaJXRPo9c9d3ZsFddUube+tuD1ZTuJfJ9QcZ0KJq6MQ1/j331k++aDaNvsDBZX2tkSivYIlztawE7x7Yo6QfLGcbj+2P/oNsaKztjgYZ1bKr3DOxmM1+ucRoloqcfkWvHWGJNgopg4TjBRymJ6wXJ2SVEU5HnOamECkGO9Ah8M1gCr2QXee06fPMJ5wfmzz1hcPOfo3pt87ku/EO5bQpUvWC0vWS3mZIMJ589OefS9P+a9t+9gkozVk+9hv/wr6J6B8sWD8oIX82d93PCuXFuenWteVft9YdvXvTU3rzm71a8NcdcuKi9/3OrKnUW8M0ReuX5X67tSSffD5l3xL7juJ3o/RU/x7xkCt/aVdqHYVrfaPnbjzdVpst6Tu1DunQVwW+naP0E2ypnYwoFxzu/dXz1+47FfPwOB2EpEp7eB70yUPRNodyr1PeCbz73+95Xw3e9bn9se73jRu/tY17/1+9W/varoBIyQvtN6rBEtFpZYh5TTGThbI+cm2qi39m6Ny76d7cUzT/Qs5Luzuz8M+0Sln9Vxe1RzKYh1jJaCumkwUYSzMQiBa+HuhaD1bgbOVEEY5MViETyjSpMkSQjfhtaTKwISt5CB6802GC3BWmpbU9Yl09PnFI8e84GJUDqiWJVk/8a/iTSGpsyxLc9dVZRURUm+mDGfzihWOUkSMRiPEMpg0gHpYABKI5RkeHBIkmVUZRleFmVIswFNXQdQrMaitEELgdIqcHS2D1EKwWAwpKrK1rNLCxRQBxABWxOd3MWZBFdX+PEh8de+Sv6P/hHL/+4fYU7uIg8OScfjFrwtoGc3tmE4HNI0FmPiNj8lgBisFosQpk0I9dBKMZi0FEtNCMkVSlKuCvLFPCh8UgIyKB5NAC0LZqlwH3VdU+QrkjQJYeZJAG2ryhDmHEURVVVhW/5zvMM2ITJBKcn44GDt8e7AHbTWxElKXVet8h9RN00A3ysWeO9wdU1VlQjvGWaC9+94Ir+gmF+wvDjn4vQJT588YzqryIsKZ20w0jjHqrDoBz/Hnde/yvPnZ1yen3F++oxVviLPc6o23DiK4pBDPRhycHjMaDwBPNY6ojgo3FIKkiRe84VLJVFCEidxSFtQiqM7d4JBSCkW0ylJNkAQQlbrsiRfLvEi8Jh3VruiyBlOJkgpAp+0HVJXNWWZry15ANKH/owPDsFDU5fgHa6pSdNBG7VgyVeLIPgJgUkzqqokimPiJGsXM4EQIe0BAkCfMgbvPMPju8Tjo0A1AXxy/gx5+hiTP8JVM/BQFCXgmRwdcPrkObkA4cN7cjFbMp3OmM5zlPJ87Qtv8uDehNVixZPPHmOkYJBFDAcxRitWVvPNxw22WZLJijSNEdnrzMQRmEnI85EyUK5FEWVZYMsCbTRShdAjYzoDnqBYLcN72FLXNU1NFMUANHW+NgxVTRXyxQGEII4C33pTVzQt/Z23jjhN10CC1rqAol5Va7T7Vzmu366vKfwi4XFH0Lj5t/4GvSMA9Hq3+WXPhr8lUG23en0fNl+2Fe19wv2O9f5Kn24xVrtyy1bBzdYpRHvnrRBw7T56rTIs9ip7+0dxVyzffL9Wtu13+abfEXv06u3SL9RL2u6sBdCbhIpeZf3ivsuT79V362Nbuu6du1H637n8GgF1bzs7Rfc+4+2okU4wDNRECk8IoZR+IzxKobBtHmLw2MiAN9HmLZooCrzfWjG/DJgk0/mK4gePuHtnTDYaM59dMDo8wNuGoigoipooHeBlxL033uPeG+8jdMRqfo4xEaef/ZC6aRBe8qNP/hxfL/jNv/k/JZYV3hVYq1idfovh/a+gdHz92LxgqP7HfvRjdX6i47qX5AXVvsx07ysuu02/yKh583GLexe9vz3l+9pO9BTWrXffc8WKsFE+r97Fyz2V65XbTrvfayxmWym8QunIBrtjfStrzZtNuPJL9LILSd/1XHd3oNT+3UvsztT1F7ldwzVo/usRuqG/a9X5mkm1UYgFfZ9w30m/m1e+Pte7fldBv1p//9pN3f3+d5/7xpMNgnn3Xe6kdW1m3L69uH/f/d1hd4/elXE2Q9rd5z456me/Qt5a8Y6jCFpUbRPHKCnx1lKWJXVdY60NygvQ0bY0NgB84RxxEodNKgqhvd5ZlNZY58hXK7zw2KYhjmJwQUn03lIVBfXHP2Ig4K3YIKMYd+cO0RtvgbfMZzOa5YIyD8J7Pp9SFRV1VSGkQpgYlWQkwxHSRERZim0s8WCIjmLqqmZ8eEiR54GuSQqS4RDbWLJxBgS+V1zIOQcPbShGWVUYpciLHCkkq+UCKaAqSmxdYSZHCEtAUF/MUG+8RfLlDzj7s2+S//N/yuRv/R3wgqZpUFGMiQxVVbe5xSG3VrV83NIHI8bB8Qneh5D+OIlbXr0weco8hAOPDw9pqpK6rlisFowODqmKQEOW6KzlgK4DNRptLr1QIZqh7hQg1fIBNgHwrQ2rL/KcdDAkjmOsayjLAhBrRUYjqKsw/lGcEGXDkNvRhrubKHBOKyVJk4g7yYI3D2t8vSSfXnB5+hnTy0vmsxXTWcFiFULRpdaUVc3SGg4++BtUeszHH/+YxXzG9PKCxlpWyyVlWbYh+inpcMTBwSHpYBxowAZDxgcHCB+Q3pMkRGMIAme5rWsGwwFeBkT7EPpvaKoGISWNqwNFVdXSpGlJlISQ/YuzM5qmDkplmhJFIT2gLgu895gowtpA11ZUFaKq1lQMcZoE7nQpWcynIVfaC4o8p6wKlJRESRrC26ucRjZ479p8eom3DQ5JU9WYOKKpCpo68LmbOEapQAFYFTllWbIqaxo7Yjodszy7xM0eo5oFZ8/PGUyOef3eAYvZnLKqefzZx2jpGKQxo8Rx784x7777BlVdcHlxwTAV3LtzROMl5wvL5bSm8SEHXumUVXSHIjtCxyPyskBVVTBi6JDHM5tN0UrjvKduLMp5XGxbOoyq3fw8uqV/kkq3vN4VQuk1LkSR58EimyQAbahoyAu7vLggMgaGw3ZtdUitA/d5S4fo22t+ouPK5XvErT3C0dbR7adXFK4rRXo6zUba2DWW7xVz1m2LK9248v2GIblW2Wt/FDfRc+wKHeuv10iH63aujuf2Ztvf7HfU7l1B4Urtt8jl3tIXxTUDfG33b6xwo15cnRi7yseugL/XYv8CjWFryNkenr58cgud9tof+j6Ga8Ttv7pjW5Zj7bmnB77mPUjZ83z7Ne1YyAFvcxD73m+tg2FQh/VWRzH64ozFbMpyseQH8xVHxyOOJzF1WeGlwYsGoTUffvvbjA6P+PI3fgkTRcwvnrCYXmBMxGJ6jtSG08dPGCWWb/zar2OyMWBpVmdIvyB/+idUy1Mmb/0qUTK5cstXH8nLWk5+ysdtH/ltuij2f91RD9ibA7T1/ScRuP9HZMboLwJXtZHt8zvvQj9IKHy8uj7/pEjo2x14+evW+hht/3YNl7t7Zrf+v9A62VvkfAfA6HsKd39B21bVdpq+5d3dXPo2dYgrN7s5brSx+s2zXSvKvc1k+9S2R7tff9e0v2JpElefQ0957uSsrbHdp1iL/TvGNbe0dd2+F+G6bbrHffFqM/MVl45bK97WeZR0SAF10+BE4DGWQnBwcIS1Ae3ZViVaaxprqesmeOiUxBM8wgICbUercDrn0VGMlFBTEEAUgjCgTMLFxXPyyxnGNUykJhqNiH/111k2NavZJbYOwnmd5yxnl+RFSdNYhgcHOKGQ2hCPRqgkJTKasmzQWqCjGBXFCO+w1pONhtRl4L5LTNRaZBRSgJAExcs21FWJjhKM0fjGYn3gx7ZCtijdnvHRsDUceJLxAcvFgtTdI59eQJKyNBnTzz5j9oMPSb72i4wmE0ajEUW+ItKKKElbkLjwknjvcUjSNMMT0K/jg8MQ9iIEtrbkqxVaa3xTUywXYSK13NjeuTaMWoMQCKlJhzHgSJKkjcAJXmxXF7iWA72uAx1ZAJaRaDRxGowPZRmonTpaMq0NSooQYt6On22a8Kytx+YN+WrVKpUN2ls+f1cxNpZqdUk+veD5Zx9zdjFlvsiZz1YUZfA4a6VYrkrs4AFH7/4ytZc8/vjHwdN9dsp8NqOu6zYnXZPGCaPJAdlgzOTggCjJmEwmxG1kQVUGFHATx4yTDB0ZynxFNgwh5pFUeGvXoDrpIKIsSmxrOMD7Nrc7vHi2aVqKOouJBDhHbS0QFpmmrtfIlELINRhYXRUIFHVRBAA7KTk4OsY7T1EU1HkIaU/HgUO9qmtUkoJtODi6G6yyzoNWXJ49Jx2OqFtDmInjAGYnBFUZIgaausY2NUZLpudzVpWlie/wxFrqpeTzD2KmTz/jNJI8PBmxmEtO7t7l00+ecj5fMc4Udx4+wHvB5dkFH//wx0SxZlbAnz1qqImQMkKoCKENcZygUOjSEREQ9JXW2HZsmnaudPlHzjqWizmNcyRxgtHBEGZaI0aR50gbQvS99xSrRTBatekBeZHjrEMS6P5sa0zr5n3TWBBVa9DSpNkQE0es5gvqNYjdqx094/q1Qt5eBbhfwXX13tDmrvCx3kx7UkpXbn1qj7F8Szbr/7Dbr+u0sK0iYk+ZViFey0E3Z2ZtndvRza8tfsMh2sb7FnyxU+CFVfYMCWvDyLY02D/JOifwuv72hD2/9aB7wq7oK+U39Y3t4XoFGVms+bbYHqt+Gc+2ceJFEss++0on8O+bgFtDtvtQbqc0dtPt2m713pFQo1grFmvlW8g16jm+RT7v5SzKNv+7y1XUa+CgQD0WRRFRFDPXF8ynM548OePyXCKk5uTeA6rZAmViXnv7LV57+3O89tb7FMWci2ePkL6hrgzz6ZRytST2K7749V9B6JBmJaVERWOcThDVkmr2MWffvWT02i8yOHpnTc+0GdA9Gsn/UMcrCartOrb7zu7Mvb1T8VV1xVeWxLdn6U9zpF84+29aY15UYX/P6BYQv+dd2t1Edo+XvnGx9+OVarcUvL5a2cpV7YLSfeuUxf7yfH0X9q3rYuv+t/bZdcL5yx7bAy62b6Hf7e2T7WTc2KT9en0WveL71r3dJvrntu0K2/fTr3u3J1uF1mf2vzDXRQvslLrh29XmtrbcW77f2/fit/f/9Zj9BG/rS877WyveELxHUmtwTaCI0jrQ/BByaT0Bwa6qasrVIuQuK4X1Aq0lUkch97Qq8e1EsjZwxXU0TSqKMMYgiEmzIZezM/Kq5oGJSaIU3ngdm2bYVU5V18jGUizmLGdTzs7OKfKWAitOWt7MiOnlJZNjTZxmGELYuLeWuixoqorDkwTXhFzxeDDExDHlcom1TaBJcxZJoDvCe1TdIAYDijZXtPOUW2+Jo5jG2nVumDKGcZJQ5QVEMXowRsQJ8ZOnzKuGbDwmyjLy1TKEOusQbpsYg2o5tYHA5Q0BxVor6ipwZyMltq5xdYmMNDoJobjnp88CRZqJQt55vURKEcYmkOzhfQCUq9oQ27IsA8Cac2itQQpsXQcPNj6E1OmoBWfTFKu8fXk9zlmqMlCuoRVKasoyR2kdvMlNQ5omSCE4HkveOgTZXLK6uGBx8ZTTR494fj5jtWpY5iVSyOC9bRrOZgXpaz/H4O7neXJ6yumTJ8znU+bTS2azWQDw8wH0L00y0ixjOJ4wGI0ZHhxiVIgKyIZD0jQjHQwwJsI29TrfJElSTGSweUHjLFqKgMhelVRVjhABXFAKicehlCTPS6oiRxlDnGRBQOsA75RmMbtEKUUcJ2gV8n6S4ZDVYh484QikDuBf89mUqixIs0EId1caHcUs5jOa1gASaBgMZV0B4Z1xTYM2hqjFWojimHwV8tVz79deGKUkaRYQcyWaNM2YXV5QliVZlnKeZ3xcDfjcWymLZx9zmadMxmO8s0ilmF7OSKXl9HTGYlGSz+e89859zivN7353SiNS4iTgIjjnAxd7U6FEhNAK29R46Wja9AXZoldGcdwCegSaPiVVmHNtvn2kTFCcEcQm0JQ1TRPo7VoUcmMMzjmSdAD49VoSxUnwUClFlCSULdheOhyipKLIc4o8x8QxiTGvLKOF1XFzbG8uvd30BYtyTz+9tby0de3O9deVuUl43duna/pwXZ27m+GV9mC962/t3ev+XLV1b+SRXp5fu/bcdAsv3AdfoHBvHp24flDFZhivPHv6PvoNsnr46fYb/Yus/31l+4oOf40hZZ/s2OU6b5XrXbor7F3tBNuDgdg69TLHzULa9YL/1tDe4qXeDFEbbSA6dTzgt3vh8J1xhB7gz1oJb+lwVPCE66gLPW+931GMvDhjMZ/xve9/gokyTu7dp/70I8z4kKOTB8wvT7HFnOX0lHy5IhkMOXv+nNjO+dJXv4SrVixOPyE7vI+OA+uFVBk+STCEPPLpj/4J5ewzJq9/Ax0Nwh3159jPQht8meNlFtid59c9o74i9OLbENsXXnvBnh925/Itj31K94tue3fvuO7Yawi75bU3qMqberY6ftO82T6x+eZvbOjWQ7mv4Ja2FT4GTu9tNfP65vc8jRsee/dFXPlBXB2fbprt7nu9JjdBFzc32inX3X2F57JnMPbc5NVnuPm+pWTvRoD47Y+iLb9vubh+Pb7h6D2UrRkjNuvTde/M1j31f7ty/+3a7fsX3bB7vOT++9M+bq14J0lCVQaU8KIMecjeW6QyLPMcJSAdjtBGg5AtirUMHN91hdIpZb4KSMa+RUAvVtRVTZwNGAzHyDa3qi5D7u8qzzl99oiq9igtKYQk+eIHKGPQpkFrTVUsqcuSxjuEUlgETgXqr2gwxKRDBBCnKcPRCKkVeIiiEPburKOoGnANQipSKcEHoLOmrtfGhrLIGR0cUSyXJFmKUpLh5IAqX2GdQ5sooHy3XrYoiVFah6wOIZHGENUpRZIwODzkrrWs8hKdxMFD2tTE6WBN59W0lF3BOGGpihzrPGWxwhOiBrz1xIMBUkrGh4fkqxwVB7j+0cEhdVUF4BUhyAZDBIJiFajEnCd4+FygUqnrBgEUxYoqL1Am0I+5pkZGEXGS4F2bD9s0KGNC/mxTY7TG2iYg39chLNs2JXVVhxBgZ1FKEemENw48x3FFs7pkdvmE1fSCZ48ec3G5IC8tzougAFtHkVcsG83ki38dnxzw5MljHn/2KdPpjOV8SlnkwZvrXUgrkIooiRmMJ9y5/xrZYBBSBlqedt16UH3laEQT5oOQLV88VIuKuqkpVyuyYRY8olrjXADEMUmKtxYdRcymM0wch/QCbfDOkWYZovWUREZzcv8+VV6QpCllWWKdZ3p+BlKClGip8N7SNCFNwxPoxZQOQGI6ijm6e4+6KsmyEWW5DIB1g8Haw1/XFcv5DGk0o/EE21gG4wllXiCVpqkroiTBWhvC/JWkWK5QSjIYDhFSMXAj4iRlPp/zg2WMjhR/8p2nKAlfezPj5FBzcjQGHHWZY7SGwzt859GCb302Q8jAda51yHEEEd6/KKKuKrxzKBPhnKOpqpYfPkZKHYwfLVih1hoVB2C1qqpR1rJczolMzGgywbqmzfIQLWc5NI3F2gKEx7kwp4UQ5KslrokYjEbYumJZlSE3XypwjsV0FoDphsNgOIkTmrp55YW01ReBq+LI/l1uj2S5ddxC0dop5r1v89qubvFbhvR9guvNu+u1QqQQ2wLHXqFnnxDdCU83XderdJ+80N3lVSs87KOa6V9z3e97Sm53pdeHvhzaGSC7Pd+3N7dl8d+1jvS+rmWTPQLFPglI7Px0rRLcffZ7ylw5blDvu/u+rfLUnxO9AVsP+5ZyfkN7L6j7OgFyL9DhDa/UZh5unnt41h6BxOPxLfNK8Lz2aMeE2njApWpzv02gHI0D0GnI/9YsZzO+/a3v8f77byGBy7PTAG5aLCmqKqTuVRUqaijzJYmfkl8+RSlIBhMuH/+QweE9ktFJaNskAdy0kPhyQfX8W5zOPmXy1q+TTt4I3u++4L9vvP3Wnyt6xU/r2Mv6cItjS2fYOXfrCvwrXblT/LqJt3tmSxV9peb2vc4vo5S/qPyVBvedvlLJi1brrQ1wZ7R6EU63eRQ768OVXPX2774qRHeB7zXVV2hvtABsOrf1Hvid+9m3R+6M16aZK7tI78c9C9ja8LxPNtg0Ina+3+rwO393t6LdPeYlq92nz15dT27fyO420SJx9D73q7lmLLrT63m5f099peMVr799qLltcECel0htqOsqeJCaisbC8GBMlmbUTU1dBaCmDqxKANpEVDYniqLgEZQSLyVx3OAReB/oqqqqQiFBgmwaZhfnCCcwwyNsljH4+lcpnKOuSoyUFAiqxlJWDXVdo6KIOB2CVmSjEcZExMMRo9GIOI7RWoFUpHFE4xxSSCwCJUIebhQZyrIMXnFAa8VyPgfvqMuC4XgYEK67HFuRBWJ67wK38HDUelIVrmmwrYLQhdlGSUJdViAk2Si011iPMTFSBS5kKSXRcNACnFkCT5/E1hVRFAcqJaGQscLVdVCKESRp2houCqIoIkliirxACB+MDM6hlcT7AIhX5DlKK4p8BT5wcEcmULtFURy87CqgRyshmc2mVGUZQNLmC6KW6sw1FtvUOATOexSBgzkouiBlxCQRvD5aMVAF1eyS2fNHLKaXnD17znReUlQe50EKsLVlviyo07scf+XXmK9KLp885vTRZzx7+piyKILi6UK0RJqNOLl7H6U0STbk6OQOR8cnQRhSksFghDZhrMO7Erz31lq8CDnbAk82GJKpIW4yYTmdBtA8KVBCkWQZUZxibTA6RCZCKI0/9iipGIyGVFUVEOPbNANbNzRVSY4P/NJt7nlTV8EAIATSSaJIB6C0OA55ftNLyqJkMBQMxuG9KsqKKEpZLWYUeeDw7p63iZPgUUZg4jjk0CACd/toFIAPZfD8SqlJswwTJwGjwERYHwwzgfIswfkx8cJxcXHB739Uc3ekOEltoH9zCU+nKy5WFi80OgqCXxwngRPSBsyAEOlQB8R9FaIFtNEIEeHxAbW9ChzojW1QWgfPtfdIAc5VATciTTBRRFPXNE0wDlnn0EpTlGVId5HhfRSEVJiqqdFS4/GURTAW0ub1Kx2h4gSEJB0c0jQNy/k8vBvm1Xm8d0WSa5Xaa1fq7c12r+H2BoVjaz/17BcMdr/cYr/e1dk2iuXm+xWZSLDl5N+Vc7a++6vduH6EWCubwrMOPd3Vba8KO73erpWMjRa4T4S+SS7qC3z957RfntsR5PYd1xkhxA3jxs4z6V38wrl4jfC1O9Wu9Pe2D2qnzEsLcLe6Zout/NWVsu0qe89y4/32dEK1QEofvN/CbXi/19yzbc63lCgdosJ0FIVIpSgmTlKmyXMWs0s+/P6PuXvnmHQ0YT49RyuBs4E3dz6dkY4mLJ4/4813xzR1ST49RfgGnY6p8gVSRZhsjM/nmGSITo/wLeVnvTrl/Lv/bwb3v874ta+hTLoZk+s0lS2rz08qjV47vK9+9HWQl1Hc+hf/VI+rndhtZXe5vo3ivDuPd1/V/nG1zusHZm/bu8Wv1WKvP3llndi56V01cf8mdovns2dBeuE02P2he79v1Lt6i+Gea/f39LpV2W/Wjq06e5VdJxt0F+7d//fc2L7Nqb8ZXCnXVS16Ve6bef32dq7doz//tN60a+dNb+jWqSdbz3NnDHereMGLeKNN5qd43F7xbhrKlmNXSBWE38jgvSNLg/C9zFc0VYlSmsZ5ImOCt7YqkcsFaZYFBOOmzf2ODF4EgLWyBSRrrEVJhTEa6wLidmM96fE9eHiCODjGLYOX++L5M+q85Pn5OVVRItMBiZCkkzHpaEJTVkRpSpamaBNhbY2JQlhqVVU4T/DQeQ9aUZUldRk4gp3zLQBTyAcXUoX7JQj+3lqaukJKTZGvMCbQF9mmwbvwV2pDXSxw3pONRggETVW3BomgiDsflPsoCZzG1lqEgKoI5UKut8BL0CaAuERxHBR7F0jura0RUuOdZ7GY4ZuG4eQAbQwDFXimvfeo1iPvBFRFEZTxylJXgXZMab3Oaw71WpqmDhPcu4Dy3vZbSkkUB+W8LPKQh0ugnVsuZti6QivNcDTizsDy+qhEuhXF7IzZ6WdcnF8yvZgxna+wXoc5VTmKumRRNCSvf4No8hrPzi549tlnXFxesFzMmU5n2LoGPEk2YDg5JI5T4iQjSVPSbBCA/7wjjrO1QlfXFXVVsJhdcnh0hE4zPJ58GWjAsuEgKOCxxESaJEuJWvA/7x1Km3X4snOWylqkcy3ImqYqQxizEALbek6VbO28QrRUWSE8vMxzoiRe01x556jqkqoOoHTKGOI0DWkbZUnTNEgBZV3hpUQbRZKkKBNhBDhrSQcDXGNRQlBUIc+7riogeH+1VlRVzWhywOXFeUBPd66lOgvgcpPDQ8qiJM9z7t1/yMnJXfJ8SVUW/GCes1rlIY9eJkgdUO27UEsPgQtSeKq6Qntaiq+Qjx084bSc9yFsM00Carl3jqoqWy7JEI0xGo9xNhizLs7PiYxBaYNt6hBVkCRoHd4BIQVN2YR5DoGCDI+SMjzHJAnMB03TetMNCFhMp63HW5MvF6TZ4CdbTbu94YrGdtOxK01sNsKbFbnta3eVzRfqSPskj2su2sd3va+6/m2vrf/dRtkTnHfl4b5S0Ffyt+SGPZv8tTLhlY1V7N9s/fZ4rS/dK0mGGxGerQez5eXouf9v+/T7ivveH3u3sE/c2lteXPP7bpnrNIUd2W2romuBDG7o2HWX9JW9K/R31xxbfRY96atrYOemXuU1XM/Z7q1qyYva8yH5K0Q2SQdC9RRwIdd7qFKm3R8MOopI0oxkMCA5y5hdnPH09AzhGi4uZnzwlQ8QQlDXDYu8QDw5RboCW2XUVcDCWF6ckVhLMtaUi3PwHpMOKJfnaBNjBnewq7OAK1LlLB/9C4rZpxy+85sko7u3GIy/ApHzZXX7F2ms1zWwu3DsCt8vUdXe+fWSEvrVW9i/jr+MdWu/0n1dey84ruvOdWVfeO89yr6t86K3oVzTcG9BvHG67Fy6T8+67ppr15l+1/asuVe7u6sYrtXCrTtfF73tg9na5Hbr7poU23Ve6eyekVgvmaIdM78zFtu76wYR/eaO95fy3enc31Ov3T6uqX7z6vV7sSvtbOrekkN21vSughse/V/ZcWvFe7lcMRgMgtLRNBgliZMBw8EQDxRlxezyEm0MqY5I4uDlUkpxcucuUqlwnTFhYxKB8mhVFhRVRaQlUhvSJMHVTVCMqbk4n1HVwWMb/dxXefb8GdPzC+aXl5w9e8oqL1leXmKSjMPxhDiOieKIydERSmsSE/J74yQGH+EcgbO5DY81cdxSlzUhv9Q2mCRde+LiLCP2DkEAUinLIqCWN3WwbBtPVRRBwYvioJjKkLdbVhVCa7QICq9tbEuX1IZ5S7n2lnsf8rgTJVtU8BVNEwCfhBSYziPoPHVVUuQr4jQly0bYpqLMC7wPFGPIgD7fVHXwCLY5zkIEhdQ5CLmyNYPhCG0MvmmCYh1rPA6pAmjafHoZxj6KGMcJZZEHI4SSVFVF0zTkqyWyBc1aTC+JkoThcISyJe8cFBwlJbZYsFyeM33+mPNnz7mcLsgLT2NlyHN2nqZuyEkZffGXUINjPvv0Y2bTS549fcLF2TmNrWmqiqalj4rSjJO7DwIVFxKtNYPhiDSNSbIMpSRRO9+SFhRuMMiCouhcUNTTLFCjtUqcrRuMGYRIiMaiIxOiJJB45/D4EN7uHDqK8LWnaULI9yBL6RC4lQptxEnI8atbo473nsFoGJRHZynLkP++mE0ZjickSUqmhnhCxMhqNiMbjdAqGCeauiIZjkDIEMFhgvFLtLz3xSpgE4wmB+SrFVVZYCINXiKp19zu2WiM8BZpIuqqoSpyBlmGQLSGirpNtbBoCcPRmLIsKcsCrTTeO6qyWBuvqqIMxoCWAgwETSNI04yy5TWHgBK/WsxDOkXdEKcpVV3TtNR9AcRRBCOfidaKdCfMSilbUEaH9yFKIV8tkV2+pVIorVjMZ+HZOmjqhshELUq/oyhyoiQB75hdXqwNKmyBEr3CsW+jfvWqdj6w2WRv0sB8bxPct/FsF9387K+eF9v79daXtTzQ9mlLsNiVI3fkg91K+5f0N+a+ALR3V9xzT2K34zvCR2hLbPe7X3qv0n1zm9tt927Ks5WDeENq3VYlYqfA1vPoVfBSOsRamez3dU//9/Tn5gIvebyajrG/nlaiEy3YUWf32DKivGS1W010j9JvolG8EEjCc/Xet3+DfLDJ+1YhIq0FXjM6REglSRqU72xIkg2Znp0yv3jG+dlzHn3yMUeHE6RJGB8c8t1vfouHQ0EyHGMbS77KSQcDyhbzI0qGYGuwB0gT09gG1zSY7A5Uc7wwyMhSr055/pf/JaPX/hqjex+EFMBrxvOvQO1+tWPf+saed/dWFWzXdWspfNeyeOPxciPZn6+7n39mx96GrtG+b3iRruhPt5pH+wpdc1Xbx73bXm+/uWmsXvSIt7aXFyjHXeSLF5u0ohsbuakzP+kLJ3b+vmIVN70GV7bybs3duvk9h98/lNe1deO5PfLDbqbYbYdi65mLzee/6uPWircUgrIqaYoVwkRUXiHLAmd0EJYFZGkIMY3jCOs8UgX0Zx2ZdQ5x09RUZUWaZWvvbqQDNRMuIGhrpfHCs1wuKfISvGQ4zIhff4uGwD19cfac6eUUIQ3HD18jHYyQAnQUwp+1MSEzSyiiKCjYSZrR2Aa8CyHDIlAxRUmCsiYAZbW8nI21eGspViu0Djzd1irqOgj9SuuWMkqQDoZBabNNGxEgcVXIfdZJSlHmWOeIohipA0J4WZbYsgQgyTKausLWNVIGz7k2BuehLFYh/1eIVkmnBe8KHNICEK1F3Rgd2heiVfKDZzTwaddrMLVIa6QctoCtLhhDjMG6oKTXdUW5ynEe0sEQCTgXnlVZFG0OOq2wIxiMRlRloNgajMaAZxI73j1RxCypl0uW06dcnj7m4nzKfNGFlgu8VDRNw3w2Rx6+zfiNr/P09JzFo+9x9vyUi/Nzlos5ta2pqxplDKnSjCYHTA5PGEwOQz5dFBPHCZPDQ/BhrLPBoEUfF8FgICAbTlBKky/nwXsLRMa0CqzHGktVFaEtpamrmny5IE5TOjqrOEmp8hzbNNR1SV1WIZICgW7BwYrVEuccJjKIVR7C3KUijoNSXlclZcsfX5d18LZ6sDhW8yVKCHScECcBUM02DU1ZhHmtNVIoqiLH4VEInG9oqirgC0Qx1jmcbTBRQmQMRVGhTODMLvMVw/GEg6Nj8jxHeI9rNJPJISYKod1VWTI9f45zDd6mICWD4YiyyAP9H/9/7v7syZFlz/PDPr7FCiCzqs65S9/pvt3kcNjdbI7I4Yy4iEbTA01mepXpSY/6G6UH0STTZhozDm1Izj7kdE933+5771mrMhNAbL7p4RdAIpEAEllV58wY3eycSgAR7h4eHh7+/S3fr0RgjKN4qo0V446dQbtS0LTLOcd1xDrHNAxM4zBHmSBpFUpT1y25zjCnb6SUGKdB9OWrBm0k9WHyoicfQxBv+kzMZqzd32dnZXxXq1tSDEQ/kYInzp6nOBMH5pwpi4Jx8ohsRiZF/xmW1Ncv41diyvmHFzZEB2+oZy/VQ1B7qZsHWPXJfvO4veN/LwGpCxuaa176n2uTsreKH248DwwJ+9zz16Da13Xh8fMe2F1uak90c7QLehLCeGLncQxAz5bjHejFa7/Q00u7qmsn+Ss2svu8TdTB5T82JuRLl7u7O+5Jm0efH4mG1JN5o+bnKGdNVhnSI/hWWqHjTvdbQtCtc1hXyZpcSO53UZV8/9u/4k//5z/jT/72n7B8847oR372858zff9nM8lmQU6ecbNBF45KwQSkaWBYf0+9fEfR3sjczQlTthRtRfAD6II4rln/6u8zPvyaN7/8z3D17ctj8vLwf1T52Dzvk/06jDV9Vk638qz9/PhsKU48i8+Q5fzdVRdxaTTPV5Bf+P30c3b4kF15F4+MVMcO1YunHgz94XP0eDs+Bok+//4RIB0bBR4tMPnJz7vFW9aCvOsssA9ZefLdQX1H1e/KIe/D/pDdRDo5P34MKPfsbXLlsY9s6jKsFyQ0Dx8MjiMXTrRw8P44O43UudfE4cbl+a8n7v6z5/hcv/bzQz2f44dr/7N90q6CH6BcDbyNyqScReoqSs5vVsKyXbcLyIm6lvzgsd8yTIFmJpuaJk/ynqw1lXMwgyhlhO3Y+wlUjbFWgJ02+EnIozSgXc1P/8Yv0FVF9+E9PiSUKShr8cC7OR+5KBxVu2R5c0tZV/hxoGoWWCO5vjEKC7Rs1GdjgFJ8/+13RO958+4taiZms64g58Tm/p6bt+/YyTKNQw85gNJUdUNOmbKqKMoSH4KEHA8DRd2SvCfFRIoydnEaaWvJ8RUiKkeMYb67Mk5KwTSO8wtbY5uGcRgZpxGjNPViiXaO5ANx9pTHaTzILU4oJaH6KSVCkJD1aRwJMaBRpJhwZck054JbJ1JYOSUJZ87i8SblWdvb45yA0Ha5ZCcVlqIc51wFeZYS0/DLLwp+sfLk8Z5ue8/m/Vd8/9VXvH//wOhh3GmZZgn5n0Km+L2/R/mTf4+7uzvWDw989803fPvNV3gf6GYvrrFOQFpKGCNyLTlGmrfvqMqSnDJKa8aupygrrLWkmBjHHjtb+lPOaDJl0wgQ3GtyW5rFQrzqITIOHd16gysrlqsVzCGEkElzDnfwAWcLbt++YxpGCiOyVdlYyrplff8BrUBbULrCqPwYJbDdEGPk5s1bylXNOA744Bm2a4ZhoHAFtUIMStqgCsOqkHumtBEj2CRpAFQVfpqIfiIOafZqSJ79NHnMzS05J8ZpnCMWNNuHO5TWlDPzty1KYQM3hjBNWKOFBX6xRAHb7Zaqrhj6gaHb8v3777C2xhhhni8RjoAQE0YbifZQir7r0FrvATIZiqoixyTGBAJ13YrSQRZm8hgDscuUTYlCYbQixiTrgdYwS7mNw8BmvRYDmIJyJpFDSUqHUoq6cFSrG4Zx5qSYJtqFop7TR9rGSQ75/Ex8ermAWq7ZD71moT869sne49QhJzZWp7DbueP3x7zGbH2uvmvG4KNfehc6dwCyn/RJ8WTcXrV5vNTcobHj3KEnwPIz8HepzTPdPDWMh2y1Zw9+FVLnOjBy9rdX3uTjOX8CyO3zs3kc/1P74ZMtv2QcUogHXM3h5we7Ta0TOWsevd+imCGEa0ai4YzDuWImXav2e4fvv/4N//rP/5rf+71EDoGcPHfrER8zTVlDLlAp4MeBMWfMNKLbJSlOqPnd2958QSITw4irl9jqBu1qjKvw/T3T+z/jm83XrH7vP2fxxb8rRJOH1/wj4IUXmzjTl8PxV08OPD79RAvXGoQuge5XgNJDY95rTnne6HXtPFZwvLhdKHPn1PGXV67rr4F9n1pOYdtHIJifhhg/HgDq5Gw4W+9+PHZr2ZMxvXTi2S8/Uzmx2B/f+5eaf+mYl9aA4yn1wvHq5Px6tHe8uBjv7umZ999rbBuKIwPsuXfqj1iuBt5F3ZBTlLxhW5BznMNCxcNnnSNph7YOFRJVLbrEOwa5qIAYCcaAduLd1hpd1/hhoB9GmlrP+c+WZrkihBGU4fb2Hf/Bn/wx4zTy8P57vvn1XzEMI0XVYIyhWiy5efsWFLTtAmcNavbkFoUTOSHn8DGQcqafw3e1NqCQOpqaBJTOoZXCGo2zrYT4KgFstnBkJaG1OWeXViCnAAEAAElEQVSRJmoXhGlgmiQ/O6WMUpoYZxCcEkVh0aqk6yQ3XXLNS5wtMNYKKJ5GyrphmgQgMPdBQtglr37sO+7v3lMW4unTRuTK0jTg57xiP3ue9czKHrstMSZhtE5KmKZD2ns6u81GJOKyyDDVTYsuCvrNBm0SKEuKke3Qs1iuUKg98CyrkhQjMSdS9JTa84e/A2+qLVO3prv/lofvv+L9t99zv00MoxCKKevwU8D7wKhq3O//XWKx5K9+9Su26zX3d3d8+83XdF0neffTSLtY0q5uqVuRBGtXb1itVoxDT13V1IuWfr2hX69Z3KwonBMSM6WxxpBywllhzN7pOBdlhTGGyXu67XZPAmbm8MC3X36JsZJvHYJnmnPL/SCRCq4sKZ2j32yxzhJTJAXZhKWcWN3cMAwCkEc97I0hWmvJz7bCBeAKh1YaZRX1csWbL3+K956yKNis11ijRe/aGMqqwk+eMI4UZSVs8kGiGRarW1JOwoswiSZ9USi6zUaUALTB1A0xeOq2JcaE1gbrRrquo1ndSBRKO0ejvAc3E/fdvHlD9IG6mvDLFWVV048j0zhKVIs1jOM0G3kG6jn3WuaxJWfQpZojXxQ+evLupZklX99ZiX4wpkChKKoSP42SK2klCiSjRBs+RMoq7yM5pknSLSSMWFG3jWyQdUFMCQXcvH2DQqOQ9AAh/zPYoqQtHCGmz77APiknNnpP8N8JsHaxHFlmT73krgHTh009Cb86BC2fwwp8zS70I3DYMYa++sSPbPNZubSpOejUa5o5t+c7+PWVNR506bi/n7qLvohgPw1Yf+zhj17wy3JzH1PUDnzP9e9YiLOS9KJsMiRm1nNF0uYxDcZY7GzoFebzgrKqqeoF77/9ir/8q6+weDIGryv+n3//n/Of/d0/5uc/fwtRo51E5/hpIsX3FPWCFB9Q/QNaKerVT8haMW0fyAlM2aALi0GjbcnUfceHP/1v6O/+mDe//E9xZfs4Rz/3QJ0p13qrgJNO7RcNgMdFnfjzcG3jABC8VM/R+n0ZqL52MF/5zJx9hk+df6Lu039e3a2LvX3NO+w0sn7SuacZ04eHqT1OfnLCE7emOj7p6Py8e4xnPqOj2l4DbBXXz8v95Lvy+CedemX9HPy7fyGpZ4eebeOJgefZruHEOY+rr3Th8djzXT8wKpzqz2Ffj7twoitXlVML0Vnr9Ocr14eaayuMmSEw7bxa3Vo8ZdNIjJXkbHcdOYlEknGOPHsqlTKEOKJ20k1+Iow9wzRRVTWltZRVRSgcfhgJCpSxLJoFX/ze3+T29paHzUAC0b2eIs3NG5GJ0ko8lqsbrFZ7T1tZVRIqXpaQZ/bqmRRs7DrqdoEfR6yzOCsAPcZIAox2IoNVSAjytO0o5nDumKBpGsZhoN+uJfe76yCJHFpdizRUyOxfksoYjNEooyldzTQIAZa1lhhGUvTE4Oe81ky/3WKqxSMp2jQJK7TPDN2GshJvY7dek5LHFRV+GiX8NmVSSATv6bsOo0XHOKUk4CJH2mYxh48rus1aGNnbBTknSJl2sWCzXpNzxjlH3dSMwzBLFsnLfyfP5ceRWm35Wz8Bx8iw2bL+/q+5++5r7u47NmvPOEYBQErhQ6QfJtLql7if/CHbfmT74Wu+//YbPrz/nm6z4e79e8iJerGgWYjc1dt3X4rsnHM0bcvtm7copSRv2TrMzQ1WazG2WDlGzW/VlDMxJGLwEj1gDc4VGC251svVisl7pqGnKAtcWYoRQike1g8YLTntVSlEfbtc7gxitAkzc7gSr7ufx925AlM1QGK7Fm+2yHoJh0DK8HB/R06JZrGgKAtSynO4eKRpGhKZsR+EuTxntBEyMOb8QuMkbxp2a6lGW+lbs7zB+5EQPW2zZBp7yT/PimkchRXdaFIM6Jx4+8UX9NvtnnfAOCfeYzPnVm+2NMsVddty/+GOvu+IwVPVNc55ttsNWtekDDkmWm0kf3+ayEgudY6RlJjDL0vKquLuwwesFX4E5rQKhyJlhVEi22NdMeeDe6ZJjCpaKdCaN+/eEbzfR7+kHTGhmdCmnPMw5zz9me1+Ggb8OFD//OdY44D4GZbUF3Yn175gr33RX/GCOPVSPXnqCaD35N37KZvyU0aA4wOetXi5HA7pMW54vLYfwZ33UtXnNpYnztvvw374d//z/uw68PyLowNfYxG6rpyfyq+z+DyXzTqS5jnV2kcYYZ7mf0uut8qA1qicUVoUPvI+9FyjdsznWu+5YLTRGOvQ2oFS+Gnir//8X/Pdb3/NsLlnmib+r//3f8h/8Xf/kD/5o19KRN40zalHkZw7tLbY0vHw3W8kouftz9HG4fsNKUVMUWOLlpgzRX3Ld7/+C/78X/5f0P/kn/K//t//nymbxf66fwTcfV0bZzbfj/v+51Dr1Z04NeFeA2heOPYVS/TrypMJ/hnu2EuP++Hng+s+NmLsvzxnnDhlIX1F9w+r2htjzoHW/QvhXAM7wL2D3+wXj5wPgstPTcBLxpZD2bqXrvG1e4Jz5ZxV/GT9p2fjq9/tLxp6eDJ8j3j8lQ0dj/elfcQFo9hrcPmrpuZHPoZXA+9MBq0xZIZxImohAYuzJJcPAbyfvXNOiLq0El3vnOn7TvI3c0bDrHFtaFyx95hNfce260QPOWeW7YJl0/K3//YfUdU13eCJCSbvqdoFRVny7ou3lGVFnlnUJdw0kVOiKGuM1Shl9szckvdlcE60OVHs2YwFsArRlg8RO4PEcRhIKRMFTRB9oEtRNKpzxLqSulng/cjUdRhrqZt2r2Us7OOiMz71ku+dlaaeJaSU0gTrJffXSxj+YrHEFiItJoBPyOAAxknyVnOWHOIUIAVhPjXG4RpHmonBdgRtZVEI8ZrSAqCTgAytFUVVYY0Vlvkkocpdt0VpxTSIDrUC2qZi3N4Ts0JpR46RHAK/s/D8fAnZd2zX73n4/jfcf/cd20lxf9fJ3EDP9U2sp0Tz+/8bWPyMzfqBX//qL1k/3LPdbHh4eGAcR1wheu+L5a14JBdLqqpmnCZuFwuMljz5d198gbNGDDBOGOvZLagz6Aaoa2E436wfqKrV7N1f4AqLsYbtekNZNyxm+S2twY8jwzgSgmf5xZfoYSDFQFmVVHXFNIyMg/S1akRWLszguKprtNb4aZy9tnr20pa4ohL9b63RRrFY3QjjeQhM40SlDQ/bDTlGilliK4RATlGk4sqK5e0N1pZMk3iZl7dvGPuefruhWawoypI3b95inaEoF6SYMFazKFeEEGWDqBSbzYauHyirGqWEy6FZLokhUtRzmkM/0FQ1oFksNeuHB0KMNMsVVS0Sa0M/ULeSthD8RMyZcRgpqVg/3FPVDcY6pmlEOUe7WhH8BFkRU6Cqqj0xm9YSrqmVFgCN2hOt9duOxWo5h61nUR/YsQhr8WBvN1vqqqJuGmGaR1I7iqpk3HayTtQ1dVkRU0Abx2azvs7rcXZ9PLP2XlqQ1ZmP1+KaC5bywz3HOcnqJ+295PG66m33QscvbZQ+opx6551wlpz9/kcDtsflhG3mUPF3BxWPVUrPVvDC1yePe7aLvf60H6J8dL1HQ3T+Uj5y8r0AFsRAsiN4O/xRoVUiZyPgO8lmXisNSs9AXHLBjTZCtpkTKQRSjDij+PCtFaO49/z//uH/xHf3Hf/Zf/KH1JU4OaaxYwoRpURa0yQN3RoFVKt3uKpl3N5TRI8ta/zo+Z//8T/m+6+/E3BxP/Ldb/+cX/y7f8IxUNgBXHV0vT96ObFG5jO/fUzdOwx7sqoTS9t+iK7ccH+25+WT183LJ1+s/hKYfqmJY6PWwdbsWQde7ua1h5w/6qQl5/G3FwH9S0aP4+t9qVwY+OMl5wdZez8KcL/mpI+YtNcYZs7dm6PxfN34nUDqP1C5Gnj32/UMqAPKGEiJqmlJITAF8ZaRFT4CKMpS5KmGYcRojZs9cDuZI2ct0RpImfv7B4w1lNZQNe1MRKYJMbFavOH3/+D3iSkzTBOKxM3bLwkxochMk6dql1iFkCalSFWVWJ3R1s1Eb8LKbqx4rFNWlIXF+0BZlnsW8Bgmcs6M24EMOGMwrqCqKsZhRJNBG1QhIKpuH+WHMjN5mpHQ+5wzOUX6bkNKoK0jxTB7mWU8Ji9e7Jwk5DfEyMPdB5qmwczyXtM4Su42ipRHpkFC0l0pLN05ZVxZo5WSnO+USD4wzgCtqkqCD0zDIPJmYz97AxUxJeIk4fI7ibBEFvk3o3HW4WYJp6aY+BvLkWo1MkXFh84w5oafftFSpw3T9j3bh+95+P5rHu7X3N1v6bcjotGuCcHTdT2ju+X2j/5LtkHz9V/8OXfff8uHD++5+/47MgofJrTWlFWNKwrqdsHNm3cUZUn0nrdffslytaJq2j2HgDVCemespWkXEmoPDOOIIs2eeU0MYtBISrFYWsmdVnlmMA+iQ50hRU+36cUzrYXNvttuiTHirCGnyGazoa2bWXs1Mg7CKB+miR3pnDDcWoawZRxF0z6lNHuUhdleG0tVSch48J5us0EpRZ4VAJTSOKMeSXbI+KHHew9MNI2wgmulZ73rkqaVcYnG4McBM4d6V5VIoeUYJdKjLLHDQFlKKoMik2LEWofSRqIflOZ2Zmiv2wUoxeLmljB51psNm/sPoBy3b1tyhoeHe+rFErKw/fdDT1XVxCTGgxg8RelYrN7wcH9PURRs12vevlui5lxwEpIWkpKwqQ/jLM0Gt2/fSN6kMZSFI+7DwhRWy5xumobV7VtCTCxWt+QUCD7ih1E0zJ2jrivxvudMP0yzhvfHv9oe1+kTJtqjI47PO+dYuKKxlw+5Fhcfd1md6dyTSl/xdnrx0OcHXALUn6PJl1/Gx9f59PM1VvRrXvin+3nNTvf42I8Fla8E+Ae/5pMT7IfYtXzcRvopHr5mV/xxa8Auq3yn/Y0ClcW4n9WjkX8HuOOOdG3n/baWsqxoFktu3/2EX/ze77P+8B3377/j4ftvufvwnr/8zQc+3P1D/qv/9I9497ahcjf4YWCaRhIaFT1+uyGFQJgGqnaJaW6Yhsz2w7f8T//kn7BZrzFFidGaol3OcplPr31v+PmxAPepB+nS7VGnV9knS9ZrgepLADqfWRLPtnPimfqUhf1wjH6A+7InnTqu/5pH5nXLxsskbi/g2at+uGqM1OPlqqfff/4xPrPGXmjnmpXo6aPz9L10quVjorhPA/NnLFOfY+yureMHeB5+MCPHQbmeXM1YxnEgpwwp47ShbFtUijjvmcaBkDJ1VaGsMIp779HANI2M44R2jtIaXFliFGw3G7Qx1G1LWTh08gxTmImXNM5Z/s5/8h/zxZfvCAmR1SorhvWWxWpFvVhRlQ7rCgkx12rWCBZytJQFcHsvRG3VciWay0HIxmKKgKZqSvHUG4t1M9hNicI6fIwUhcWVsySU9ygUTdOgjaLfbEELY3K3XUNWs1yU9HXse8ahxzohcqvbBTF4sVhvR9brtYT1xkieQ7xTSgzDQO56IaDzE9M04opKQmVTYgoBP45UtYSYN+0CFYVwys963d1mS7NoMa4gKyQf2BjJkfUetKKsG9rFUu5fhBQD5EzhCiEPI/IH7wLvyg06e6KfMEnxho5x6jHdLxjixMM3v+Luu/fcr3vW93eMw4S2JTlrprFn0/W4n/wxX/z+3+HD3T1f/eZXfP3b37DdbBiGnoeHB5TWuNkbefP2HcvVraQQNA2rm1sJRTYiGeacxWglxolpxBWlgL1xoKor+nHah28LgZ/Hzh5oYaGPjOMohGJa0/c9VSVAOsQkBpoYUNpSzqHLzLI1IUa69Yb1+/e0yyVNu6Sb9cCNc8JY7yfa5Ur03LV4bnOGoqpFbi5l4RjIae6DEUkrZHNWFBVlXckmSCtCCCIj13UUVU1dCSdBSBFQuLIkpIS2lvX9vXhSbMH9+/cs37yhtJYQZA5GFChFCp7V7Q3jOCHM3hB9IHkxfiilKVxBiomufxAWeOtQgJ8ErC5vbkkpY8uKoetZLAIqZ3yI1E3LcnXDtttgjGXYblBEFosFGUNVSEi/kNsVWCtgPyPpDylFUKKfro3FzOH5QrYWQQmJnMqJsiwoy5IQIpnMOCsUTFOcU0QMIUyEGFDWovVM0DZO+zz1oqo//wq7L+ffEM/3Lc9f0p/0onzNxuicy/jqys9Vpq6s73xXjn9RB6NyuJ14et7jL5/be/BS/w7v5Mvl3E36gbYAHwUAns/H68byNKK67go/0mOy+/Oq0+cenGJquwZ4PDlezR7UxzBzckbpDFmRZ+4LxaHmt5GIM1dQVhXt4obxi58wbDd0mwfWH77jwzdf8c1vfsX3X/2G/+b/84/4u/+rf4e/9Qc/p6iqvVE5eDHqp25LGDpymLDjFu0W/M//7F/Qd51EAVYV0Uds3bK4/YJHrd7HAfvBMPe1hsAL56nDz58EvF5RR55XcMXHKR5cmuQvXcuuvSftXtGBc9aJ43I8x49tfsfnf8rkOGebu7rOjzQynqlGXTM+B+UQvD797vDTiYYu1HfdkafX2msA9+Hnq5awE3Vc/c68mqDminKNPfjCb6d+ungNh2vMtd3/yMu8PsfbGlQwlE0pDMMpMQ0DTdOQlcbNIdM5BvphQCFSVCEGbHSMfUeOCl2WVGVJzImbN29EmzhGyX/WBc4UFFnYtFNK/Pv/yX/JNBM2Tf0WrGN5c0NRFrTLlsJaUvDossZqCdfSSpGyAP4YPFlplqsblBbPq84KbQ0aQxhH0jQQnKMoCmDOx4qRyY+YmeXcaCHjss6JlnT0mGwIwZPiCAqMNhImr4UkTvQ2pU7jLGmWQRJGdT8zrUcJAzaGZtmy3az3XtSiFGmn5OzsbbX4lIT53SiahehBpzjQdZ14XINHZxlTawwP9/dY54RArq7ZPjzgCvFiV81C9MSzkErt5t2O3G1Vwu8uOhamI8WJMA3EmBiHDb67ZxxHvv7rv8Q1b9k8PLBZbwnjhA8K5WpySnTdli4YVn/4X9PrBR8+3PP+/fd8eP8993fv6buOlMHOTK/WOtrlgqIo5rxhx82bt9y+eYM2wg5rjaWsK6zRZGVomlbSA6ZpDr0WaS1XODEw5ITkeScyMM4GlG67ZhxGFisByOPYU1BSOLsn5drpQ4ecKeuKGGWDY1xB1TR0XYcpKtb3dyxubgg+Ya0hBk3OME2eEEZSkvDzHaGeNlpI0nxinHkR6sVS0hPKUmSzZrCfZnAuBpxiL8MVhoGh6xmGgc36AaUUq5sbUIoYhcTs5u0bQvCUy4VIjdUVebth8MKYKzmHZtblHnFOmNndrJ099iLD1i6WhBDQWoHSlHVJhWKzhbIomMYJPw2QwbiCoikYug5jDW+/+JJ+u6VZ/Iyb8KV4262hbmr6Xkj7/DTR9wOKjKtKVm/e0nUdzhqadsnm4X42vBiU1rSFRGOEDHn24ruypqjEMKK8xxkhQcwobFWwKldEFOUs+TeOwoGgrUOpH0pO7HNvYV+5c31pM/fS8Wc8TE/7cu5V/fjX9eDsOVA+9/trSj7463Czcrn80PfymnLZWHP9tXye8ryl69p+vJ/PZ9JL8+OlFvLB/z/aTbvvwGu2wnK8yvnp7FSyOVf5aZ80kOeoHqWYI6Jm0jUrcmOucJRliZ8apsWKdnVLu3pDu3rD4uYNN7dv+O6rv+a//2d/xfsPW/7ef/w3KeuW4D1h6PHDiC0cZMWwWeNS4uuv/opuuxV5DWP5p//wnzDefcd/9X/4P9Gu3j65JWo/mpflg36Qsltnjj2v+eCfHfA9UR5P+YzgbDcupzb7h4D41G+c+B7gWVrCC+VjLYWnqr+Evl5nTftfXnmFLePjjLjHZz59Lz3txvHv1/Xt8cyPL6faPDXVP6ql1xitXrN3+Yye789oOjhbrgbe3gdSDIRUzGydWcI0+wGlYBh6tNJM0wjaCpmRLTBakYDlsgXtUKZAG0OOkLXCKlDK4aeBFBNV3TBOoxA7KUVIIrellGj5Gqcoy5LJS35x0TYwE6AlwBg3A484e8mEQVQbjfeeHMOcqy7a3EK8lkkx7a+xHwas1gQfyGrCaCjLCpS8NNfrB7S2VHVFu7plc/+BnLOErgJx9qJaK5rk0Qn7svcelzJJe/ESOifs2yFQ1vUcJi/hryklHu4+ULethP4qxdh3pJkB2s45riLFJuypIYRZh1ZJbnBKVLXItKWUUdqwvL0lp0yMXsjgUmSz3RJmWS3vPWEa+Gmb+BurLQ5P8gMhTDPgXjNst3R9xzRF7j7cM4UPJAxxmlCuwhQ10zCwWW9I7c/5yX/0X/H+fkO/3fLbX/+a+w8fuL97z3azYZomcsrYQoBsXTcYY2kWS6q64d0XX+Kcm/OkLc3NjYCv2fSqZyMHWrTLd1JSxhicK2fwKhEQ1jmCH7l//37OEVbUc7i4MoZpHJlmo4OxlrKuiSnJvZ+91nEKuKqkLIp5PsCw3c4SXWtWt2/23vHNwx0ZhXEOtYuyCIGyaYnjiEI2YGVdUzghIFQk8nxfh6GjKEpSzpCFuM/HCF0Pc3oCxrJY3TKNIyBM+9q4PXlYUYkxI2WEAG4chaDNWoiJsd8yDiNV08zP+YS1BVopnHV4I1J1WmliSpATwzSSYgZjKArHttty9/13KDJNXVOUBcoY2p/8hPu7O4ZhZPKBEITRPeZIaQuMqYXdvygZe9GNZ84Nb5YLFssV77/9Gm0s7XI5a5RLfnzOM3lRyvNGds4NV4pxu53Z2i3BR8pCjAnWWVRSxODZbtYzN0MrffNB5s+/FUU9/5if/flZm3jd4ed2l6fPunzUx+0on+51T/fucu2HolPXlMON0Uvn/fCQ5bWj9nTefNzW4lSbOwfYuf4cmlFOzZpzG9jL8/wQ6H6GHdhHDMcxyDvGUbvrzvP/88x8jgISoISIM6ddSpLBmIg1lmAtzonMoZBFVpR1Q71Ysri5ZfXmC7797a/45re/5v/19/8l//nf+yNuVi3GWvxgCOMgcqCFZv3t93x4/4BxJaZq+dM/+xX/8l/+KX/07/6Cn/2tv4u27hHsKvY69j9oeW39u37BoyEwv2Bjeek6ju/5JaB8qZydtOo0ODjVP3WiS+f6f8Ig8YOAkHPj8rF1fzY081EP6cvVHI/rhWYurXVPjID71fF15Zw5+5Tt53y/nr6vTh/zvL5L75Vzj8i1YFwB+XPNy2u//xx1/wDl+hzvYYAYMUWmXbaEaWQcJtARlRMhRIpCS7isEk+T2rF2kvHJ8rDuaBeKEBVD30OGuhXPaEgZW5QM44Afx1lio9qHg2+TMDSTAv0wcnN7QxhHkXYymqqoyDkJgKwbpmkQ4K2RUOGUKAtHpkArwxQ84ziiPbiyENK1lPfkbykrlLHUTcPYS7i6dQXjMFA1rTBnz2yly9WKcZwIwTMOHjI0ywUKTZg91TGIPJSEn0n+eMyRsl1g/DRrU2uUcnuPuO07FMLY7qwjhGkeNwlFL8tizpd5zCkW4q+Goe8kLUAr2sWCoR+EMZo8R9QlMnnW/s77MP2mNPziC8sX5QPkQAwT09gTppF+/T2bu+8Zp8QwRLabLSEZIoYpJKwWUr04Day3PeXv/j2Wv/sn3D/c8/233/L+/ff8+ld/ycPdB2ISL64xlrptqdslq5s3VHMO7s9/8bsURUHdCrO4McJG3202tIsFi8UCtHgKgvfEnPDTSFmVFI0QpIUQaZqCcRzpNmu0NpRVSd0KN0FdN6SchP2+77ELidDY6a73mw1VVZJzpChKyZHOifX9A9ZqtLY0ixY/jjRNTVaKYmaXjylisQxDT1HXTHkUY8yc0pBjRGnN7mn3PqCNcBaEydPeLLHasF2v5b7WLa4QUh0/ebr+Yb8xW715S7dZM/adkJ0lYVwfh54Uguhmp0RIcs9lninKqkLrmswaP3k22zV1s2QaJxLgpzuZa91I3TbELJEZrqhE/kxbUllSFAU3qxsx9FQFPgTJY0+J5c0N333zDWVZSk49AImcBQj7riNn2Zavbm4YxhE/Pw/WSY4/KYv2bVGitKKqKoZuiyoKnALjNDnlmedAPEuLZUtR1lRVxWb9gLVmlg/MaOWo64aYMykGyfd2DlMUn7aaZp5Gq+53tMflI4CB4mD3+RF1fGy52MwP14dTL/lLHvDry9Pt0ccYMv7NOoMuXff53y6Lan3m+/hkUA+8vgfGo+Ny3Lvr7+5rDSgnWrzAfPy8R0e/XOqo2j22cw+VbMSVznMkTibrLL/nXei5wSSLs2GWG3MUrsCVlRBCtkua5Q3tzQ3tcsU3f/0r/h//33/E3/nbf5Nf/o13FO0SpS1x6pmGiWEUCdiqacjG0t194Je/9xN++R/8CT/9gz+eVR4USWV01o/3Tn1WJ9LTcrbifGDAuAAWLoDufGmSHZeDY5516ajRk10+GKtTxx8fsjcYXOjfHnRfaviKyX74qngxp/pkBUdT+2PB9mvaPrUInPr8hJ3tQsfOAelTY/oM1e4G4IQB5aiqU3+fv2x19OkpVH9poE+vSDvj37lzzz/Jh2ltz3cX+dn3z+vl5Pp76vr3RsiDFj6p/AAL1I9hd4RXAO+6cPhgyH5g2GaKqqZwouUdc5aQ6CwERqvbN5JPmjPjOOGcI6MpnCNnZkbpRljRQ5y9kw5yZOz7vf42gNZmDs9WKF1DTsQsQCxZyzCOQpQ2yw2VdU23eSCOPbpusbYgRmH2VkoJuZsfUdpQ1RVW6z2xmTGKFIUlWXJJtYQAN43kLY8TpAhGWNizNhRaMYaMcW4PzECRUiKGQNcP5JQo51zWKUaUlrAynfNMbiLTtOt6Ukwz07lisViC0vucbMkDLoFMObOlD9stxgjD/C503Icwg4wkIbXDgDFa8meTeFmLqhJtZa3pu63ckyLzy9WWhRmJYcT7gThuGYeBobtn2G7YbgeGIdN1I145ctLEDChLzoFhu2HjNTd/9L/Drn7G999/z9dffcW3X/2Gb77+mr7bioxVyhRVSbu85c0XX1LXDVXdsLy5oaxqCSsvChaLJcZKeLnSoiHetAumcRQN8zlNIYwjZSmeWq007WKJms34yihUbvZ67FobcEI4lkJGWyPkWikx9gP1Yimh+aUwwecMzhp88MQQaRcLwjTOJHTCxB2TvAxC8CitWCwXTJPo28c5kqPbrsnJknIvOcVFIYRjMZCix0/QLBZs05puvUEZQ13XM9t5T7u8oe8HrHO8/eInhCih38MsjVc4S0ZRFmb2ojjGKTCFDWEcaW9E55sApMQ0DXNutZMwc6Np6hJXFMSUaduWqe+p65n1f2beVQpMUzGNEzFmjJX2pmEkK401YIuKmBJDP1CVFU27IJOw2koIv1ZkFO1iRQxxJiLscEazXC1JWSJemmbJtttKbvaUaRZL2kVLVxbEKISIWltCjlhnSZPny5//jJxg8oH7u3sxyIVE2YpUYVWVZKVRKlPoAoylrEpJS/jEoo4+fMye51TZk/R/hrqeV87RDvHM7z9IObyq6xp5etQOUp7eqF9jyb+0XXl61L9ZyH22qEf7zuUefvqN3G3iT3o/dtg6n2lFPQKCXT37Ud0BvSPA/uTeXgA4Rz15oeSDf1/YVX+Gea8Okcuc+qL0HKL+5D/2ADxpi9biAbfGSXRcUVIWFWVVU9YtVbukbld8/Vd/zj/4H/+Ub779wN/+41/iSsn7ZhpJcZzJMhU5jJSF4+e/+zf4k//t/5GiWYljQ2kMiqweA8z/jc72o+XgtcDvWd8P7+Pu76PP6mBu7p6n/ceD759MiQuATh18foLpFGKdPVHnE9D9mnl33I+j/h8OxpNn7IU2PhpwX6rwGvvf4YC8BJIvlXNtXXv+J1z7a2wNh40dzt1TwPUTu/XCc30I4K9cIy/WdPSFun5cXqz0B0THPwb4vhp4F2VF1WiC93tdXuMc2XsKVzJNHmUczdsWrS0pBeK4ISuHmgF304jEkjUaPwPuaRhIRUFdN/ixp64rbFGK12wYKIsCba0wTPvA5COL5ZJhHDHGsFyWGKPlhZYDOWmadoFuW7SV/GylNRkkrFmJZ1erJCHBppqJxoTwKmUoqobGGmKYGLqeLkSxRBclTqs5h1tYl8UDLS/TzWYjoA5EZ5lMVTdM08Qw9BjrJAxXqb2cl0zyhPeeGMKcZyuhwrvinMOPAzGJVjQZYkwYa2iXKyHryom+6wgzc3fUEHxiWK8xViIQmtUNWotWqPdhr8NtjeZWD/zydsIp8XIHP+D7DePYM6zf020eGPrAZusZpkxMlmHyEjocIkVVs95siO3v8uXf/M8ZQuKb3/6av/rLv+TD++/ZrB9YP9wRQ6AoqxlYl9y8ece7L36KKyScvizLmZlcPMMxRawyZETbenlzi55JuJxzc458pKybfd6csGMnGVuVMUozBS+Gl3HClSUoiSSwzqEyosUeA/ViQfQeo7UwpRtLjBPjkCR3GAheY60DhRhYZq16oxVDtwWlMFqiHbabDVVVoZOMk8+BRSFGgfXDHcZYlFIUZS0pAGEmwLNWQttnErackbEwWtjNQyD6QLYGpSD6CZSE+4/DIFJdCoIfaVc3dJsNth9w5Y3kfNcN0QvoV1qxenNLmCbi1OOngaJeAoqiKgkxk3Pi7u5evOQ50Q89U99RVTVoR902krOeQMVARmT6cs7k6HGFm1nkIQ/TfiwzEGfStsVqhTGGECRaQWlNkZNEEjjhiahmUrl2dTs/BxMxiDFpmiaWqxVKZaYQRUPdStRLSjJf6rKgKAqMNhLF0fcURs/SZp+wkp5drR91hC9Wf+L8Y9B4TOry8X06Kuro33O//2Dl+abjORw/FVB+bPN/Xk59f2ihfzJEB1WeO++43peB/et2C+fsHhfL/oDHs0+ZM671qhwfddpzIf9mOO8wZv4xc3ountplXp4EezQgIOV5yPlJg8CJcvWjvr/YY0vBcZ9OlRMjf0Afnfef5T2lspoB+I58TTgtjBHFDkldM7MnvBDnR9VSNQvqdsFf/eov+Pq7/5H/4u/+ETfLGo0msRYDdYK+H/md3/0dfvaHf483f+MP8X6aI90sKH0Kv33+8tpKr72hBwc8u7dH5+yv82gReILz1NEx55p/aY291rZzbkKeA52XJvDOuLX736FB4Yo+nWz32nJY9+G/r0GNx5Pw2FhyTSWfDNLOTZqXzzr9nr5uAK5Zl06/m85/89ptzfN14Plg7pb04xbPtvWae3epjo+0B1w7BtcYJo4b/hiDyNXAe/ITtWmwRcnm/g6toDIrhr4X/V0Ubb0gpoQRlyq6ahmmSXSIp4moFYt2wehFlslo0KYBFCl6iqoW0rZpEiInBV0/UDjJ1S7rGmaCtGIG484akUJSiCRUyhjrRPYpz6A7y7DsPNk+ZuqqxHvJS0cncorYSvJpc4xEBWDIiJfVFAUqJ9COTJ7zUcUiHXMWJuduQClDIqONZrlcMgUBhsvVDcY60Jqp3+K9hNJOfS83Ys7p2t0853aeejEe2LKEWarKFRIavSPyCglIURhSreS45ygh1EYrtpstrijkGCtay8LeHakV/Lzd8kW5gRQJU8CPW7wPDNs1/cP3DH1H10e63jMMmYgSVm5jICmUyUTfU/7s38e/+w/59u4eP428/+47Hu7vBHSv14R5LLSx1Islq9u31E2DNuKhJmeWixX1ciFgVUt+PgrCNLG+f8A5S7fdUpYF0zhSVJVon6Y0e3mFmTsDMXiMFW13peJscBj3JG0ZMQtvNxuGvkcbQ8qJaRip6wqlFF0v2u/aROkjCqMV3k8zgVslBptJtO37Obwb1CN/QAY/jKScaeoGV5SEaUJrIYbruk6IyGZZNG0UOe1y+cW4UNVGcrxzgNkzm1KgX29RShGqSli/fcD7EWcsVbtgtbph9IEvfvpTgg/06zVF4TBKYaqSfusl3N7WlFVD8A1T3xH9gKJiGHpyEuIyZw1GKVKCoet5++VP8OPANHkJidcaqyFlQ1UZmramcJY7pQTk53FOeVC4ZkFVVmy2Wx7uP8wSaIayWVBlWQNA8uPLsma73VJUNfd370X33Bgm71FZc/NmKUzlSgvj/zQS/D1NU+Inj5l5FJy1aCsRCqYoGKeJxe0tYLBWs932r1g6j8oLq+6Tl/FJEHJ++Vbz77t17FzzFzecn8WMe80r5uMaOXXWSzW9Fkg+Pe94R60+yvDymj3sVdUf3cjn55xu7WXviDrbj+dzZ7dzV48e6vnb3eerr+VcOdhMPwNBL5z6tAePG8Hz5zxHylcB8mcHHT9QFxs9cZ6co5QScK124Fse/7xjQGfmrjAzCFcabYQvRoC4wViDmfcMZV1TNgvKZsFf//mf8X/7f/9j/tO/87f43Z+/pVquCD4w9iP9MPF+ULytvmS9fsAWEu2EkbD3R0m0U+P5WRaQK8fpQvmUbuwAdn7ycZ93+gTbZfZpQ6dsQ8/K4Ynnj3r20zOG9LnBfNTXUwD8aj3sc+D9c5VjsP0xSGRf0YkTP6a/T875RBT+ivfC618h1/Xn1Pt9t/LlJ59eU+vhkecn2vng8Gvf0J95wil4Ep5y5pAX+/FsWuyu84de514BvK0tGMcB6yyuqtl5ecuyxMdIXdWE4IlRQGzOEuoZk+RRVnVJUzegFCpGCYcuRMNaGUdWiETWNHH//bc0i6WE487511prpnEk5yRAeGbqHoeBsm6YvORSpzCRpgljKvEg5kyYPH2/JSvRzez7kRwTyii6tViEtVLc399LrrUtYOwhK+yBxrE1Wq4vZ2IM9F0veto5U1aS266Noq6aeWHMGK33+sdCgBYpq5oQIigJfd5peeaU5xBYCVnOKbHd6XjnTFlJCLqfJmHYngm7RINYMU0TOoQ5d1qTJ09VliLTVZTkLCRv0yhh0m8q+JtvJxojIe4hTPhhZBg6xu0d3d13DIOnGxLD4PE+kWJGuVoeRz+iciJOPbiCfpi4//Y7vv3tX/Pt17+FnHi4v6cfJ/w0idexKKmalnax4ubNOxbLG1IKc4675GwX1mFnNvlx6FGIx3KxWpGRa2gWy1krXQC3LUoUQkoX/IBxIjOn51D7oe8gJwnTq0QaLowSdZBzol0syMA49MJurzTddoOraowCizCkl3VD321JQXLnUopCDJgSxljKsia5RJql21RVSX5z24hBJ0bCNLHdrNFas8kblqsV93cfxMAQPaEPmMIR1mtcWRGnibIsyUoR/MQwjVhXkGHOf7fUTSsLhhrFiwEze32YtbmNkBMuliTvyVpY1bV1EAMheMwMSqvFiuCFhC2FCecsfhrECKIVxjjelO8Yh4kUE4MPFJaZxb9CuQKjEnEcxBiSk7D6Z4MJkdIZrE54P8rzUbcwG3P6bcfYbagXCwHRfqKuZL71XUfhVsSYiGkk79QO/IQpCjRQt+1skKkxxmFbxzhKdI7KiZwEdMcQRGXAOmIcGfoojO6fUo42UWeX7yvX9XOOtVMv9xdf+D/8u+STyrHv8vDaT/39+A3Pvjm8DefPfdaBk+XZeScqulTvtfnHJyDdCx25+PVVfXuxvTPHqHPHHQOBowl8COLPtpcPvnip8zvAer47R4/kMcrZmWBOjf6Fzf9ZQH7K/HFmY6jm8+ZB2YE8xWzs0LvfMkqD2vO4zPrfxogB2VqsK3BlQ9kuqBY3/OYv/pT/9n/4U37z9Rv+4Bdfst1s+eqr7/nzr9f87G/9h6Ss9jwaO+Pzj4Krry0nHt5P2sKfWUhPYcWLoPiwut1c/1xjdjzdztT75FI+R9ufCxtdmj8vvbA+Caz/SOWT+3ViQT1R57n1PJ/47dzfZ1o8efTh2vcI3dWTI46Pfc1t/qzl5EJ/3cJ1clxf3BScOuHaby+X6z3eY4/WimkIuLKi2HlnbU0eRjbbLdYanLGidT0TYrnCMg0JP3ne99+T0hxyDgxZCJESkpO8094tipK6bjFm1sEEcgLnLDFBWZTCEO5EUzhME9o60RjWEg4/TZ4UPCEk2tWCYeiIIRAyaBJ931FVFSipd5o8WimmECkcmKqm225RIWKco7ASQp7mUF5tNGVVClP2OGI0VLWAcKMl5zfnzOQ9wU/0IeGqWUotQUxJSNOKUkihkli6m1bCc7vthhwTKC1EcUrN2uSSAx685MUGL9EBMXjImXaxFIkko7HOorWhKASUG60l1NtZfmcV+d3FPSZPhJAIfsRPE8Pmjv7hW4ZhotsM9JMSL3dSoAtyDqTg55ePEJpFFGPX89ff/5rv08CH774h+EnC34eeoRuompqyqlmubrCupKwbFqsbyWNHdMPbpeSoC5idsM6KtJgrGPqesqrYbtbcvHkrIBAljORK+pLI+1BzpSR8XHLoJfKhrhuKUrSxhzzQbbbEGKnbhs16TbNYUpYVwYjsmyuLef8zb36Uou+23L9/Lzn62tAuFox9hzZuP3etEy/95kGk4QDGmf3fWsswDLOxRF6wD/f3BB+o2ltMUdOtH8g5Q47cf/8tRVlijehyLxYrYphQSngEJukYoIhpBvxzKsTQ9TNLvXj5q6bFgBDRbXumoaOYoyJSjPhxRBs9k6Q1TN5TLVYYYymAFL2Ev8eMcRabM9koWi3PgjZG+ApyAjIPd+9RKWF0lmc3g9EtSoukV8oKpQ1aSXRCjJHRR27evpmJ6KxooodAVdcoLeHkdo7syCiGjaaoKkbvhSBNQTUbqIx5lAh0zmKsIWV5LmKImBuNn4LIzOVICB+xgh6Wi+/WQzfLqaX6iuX71S+K495caufS76feeD/cTuk1m4tdOY2LHvv4fCNxebd3FmddPf6nQN1x+0+/3U+LU/f5TLufb7Pz6NV4utV6wXCQeeIxPPxjD7RP7Y3OTZtr5/gletxTdey/+7h5+xyaX9HRZ9d9ynR0AuCrOf979oBnZK03We21v/f639ZKhN+egG1BvVhRtwv+/F/9C/7pP/9Tpsnj6iX/3n/4H/EHf/jHvHn7bia7lHxyJTlaT67z8hPxI5cDg83Z5fP44CcI+uUm9kD2hVt61ui0M2Aonj4QmcsM7OrMNDkuB5f15B4dn3zKhnT4/bXT/zWGmJee7d24nmt739YPNcdOvJgv9uPy6afL80ovTqf9ZD489TAd7TzIu8IeeWUPL/1+PGGeH/dp5RP2D/tF4DO9/Y5fXD9Cud7FkyMpW5ydT1GaaRyIKZJ2zNhKo4yZGbsz0Qf8uJFNsNb42RsevKWY83Mxhn67JfkJpTLN6i3tcjnr7E5zGFbGVRXWGlSSMVdzPnRR1TOwgmkaBIT6kbpZkIyhmPPIjYIQR1zRYI1BGSvyQhlSjKIFbgx106JVJoRA0zSSww0MM7nZNE2kHOnWW9p2QQpRwr6MBZXIUeS5vI/kMIHS1E2DKQ0+RNab7X58mtYcGBaSjGWMIgNVloxpIviRpq4ZhmHWHLZ8eP8dKUZu374TCbOcUDlgi5pMZrFomEaxaA/juCeJwxhqp/l5u+Zd2UESojk/9kzTSL/+wLD5QLdZMwyw6SPjILn7OxbWxMyMHj0xeWLOjP3A+43nX3410scPslnIea+jro2lLCtcWVE1CxarW6q6oaxqUs4slkuctdR1jdKGuqpJSQjxrBHZtOVyiY+iq52zhBMbbYTUbGbb3mmnpxQhOxRi0JDwPCfcBCHMBHoiN5Vz4uHDB3JOvHn7bs/wbrOlVJVIrsVICIG+78ko6ral7zoUnvVDYLFa7dMdcoaUIkoJwOu3a/Ssc6+1ZpxlxCRaRNi0d7rbOw1vhYB5cubmZkUKEdTMHdBtKQoJnTbGUtViILq/u2OxXGJtQfAjzhaiKT+OIr83jZAz3XpNzgltrPQ3ZVwpTOwpJ6Z+Ep3xmQuhKEo2Dw/YWTcbDCl7xlEMI9YUuEKeSW3M/hqDD9TtDZvNA8FHbt6uQCnGvmezXmOLikwWo5VzFKXIqbUps91uCIPo0leLJUVRkHOmbupZmmxi7Ht8kHtTqMxidSNGL2tRZKy1hBBEucAYmrYV44eRdWXoOpq2wVqNMTWNdfhPRt5nls6PPO9wv3Iu/On86/HTy/Gr7fKr7pzf8TWtve7oc315iTf18dyjUb0GI3/CQL+0VZDQ0XwQ9f7pG4HXzo/dniYf4IhzdSmOviA/BR7Hfblms/+k4iuK4rlwwAFQ2zWcd8fuANLZhh63nefG7HD+PLXjHHZGnbmOY6D9OBPFLrBj9j44LkvEnzjAM2aWTtTaorXFzClZxhWi5NC01IsV7c1bvvn1X2JcyZe/+CVf/OwX3L77Cbdv3tG0S4piVpqYpc5evD8/djmYcNftjT99FcwnbtvF23g8Z88ZyI4epqthw9GBh82ooy+e9f3w3Bcm81nw/7nsq8d9+Zj69kD16glxuS8/xEvzoBzj6pP9OPz3ivrOV/Kaok5M28d6nt4edbbtl+ewel7hx977g+oufHH6nNcYaX+kNfBq4G2KkqKoGIcBNPuNumx2Hat2CTnRD6MwnMc4k4CIhJDShhA93iuKQvJuJR9VAFZRV4zTBEmkySbfUTiHNobSFUzeM6REmESLO5GxxuKnCRBitByjAHrk2bTWihZ4TOJlrWqRoCJJjrbSKK1AK2wSjWBjxGM+bjfoqp4/T+JJnwnSjNHUdUWMnmnsqZpWjAgg4dyTxxUFpixgZnwPfsKPA66qUGRCN7CNXkhTigrIDF3Hdv2AK0qmaaJsGhblAqUUN3WDn0OMq1LYsHdEdSmBqRuRHIui91yUpRgKRmFVH7Ydb5otf/DW05iBGMQTH/zEuH2gv/+OvtuKp7tL9GPGe9CuRB5ULeQvccIPveiiD55+HPnqfcc/+23gfZeEKMuI5rXSmqZpqRdLbm7f0i5XlJUYPoqyZLFYkkFCtOuaaQpM45px6CX0exold38O0a+qUsL2MyiV8ONEjFGuL2fR+W4WpChScX3fk2KYgeUk4cSFw2iDK2U+FE7mtdYi8xbn0PUUEqaQiIFsLcaJxFtZluSUSFWSnOFZO1obI/3oO+E9sFrmdVlTNQKOQcAsSQw02TlikhBrlBIN10lUAKxZ7iX5iAk1e+9Vinz1m7+mKCuctXN6guyUN/cfKOuGsmpQWuEnT79ZU9Ut1RzOnfMocENpbFGhtCLEyOSjhIkb8FPYh/FrK2Rk4hVOKGMprWH9cC9M64sFPkTKqiLHSNd1YAw5BWKEplmhUYzDSEKhlGF180a0ycmolPDJk6KExofgsdZSOosiUVot6QwZxlGiFJQ2xBAgQ7tc4r2HaRDme1XQrde4ssRPIxkoq4oPH+6QVIRM3w+UhQMyVV0RfCSmgDU/5KqrnvxzTbmID46POfvNtQ1eOk7eTse+us9bzu1gT7dzzfv7B9tXHXXx8d19fmv0uM879Cec93B8Lnv+4ThdxgYHG6+XEedJQP7s8M85RY6nw4WLeXrowQHHnkjOB0++tBfbD8UB2Hm+vztENJfMQHLsDnDL+Oc5jP6AAZ3dWg85a8n93nm+jcUYi7MFRVnSLFa8+9kv+PeGvwcZbFFQ1Q11u6BpFtRNi3PlXi1EruPfNuTN09u3+99runkKkJ5oYj+tjubZ9bhTPZ2Tu/OPDEMZnhuKXmrrROOncr/PhcbvjztsNx+199K515TPBdJf29alvh+DvlPlsyy2599TP0Z5zSWcA9GnjnnpubmqHL8sjhfOXX3587wzftjp93km+dXAW2vxHDdNgw8BRWYcBwpbYqxmmibGYSTGiRzFc9Te3OJjwBlDt1ljtUVVBcY6hkF0rzcP99RNi1HlzOwsubJTt6VbP7Bc3Ug+rRVNZDI4oyms22/eJz+JTFkMlBlSTGiVqFwhpG4xSk5sivhxQlUlMXqBk1bYtNvFipQTzggDOIj0WFVVuEKGu6rFu661MC+nMGEWLWCo6prgPQFY3tzMIfTM+dYGVEFjHDEnCmtQxog3ddYSRsFiuaJuGu6++xa0liiBEIghUtZCktY0mbKu0cZSFEKmlVOialrxmBrEIBLTbPywaAW//4XidxYdKnuRp+ruCTEybD4wrL+j33Z0/cQwZsYxM04RW1SkzOz9yISpI8+M1X3Xsd4M/Pm3A//0r7cMUZjatTbkpIgza3WsKsqixM653dpYFssVb96+w5WVhP9GkZHTRkKIi6IQRuuyEGOCHxmGkaHfzjnaNSEkMoi8mlLEmCjKgugDCkW7WrG5u9/n7ztnIScBzcDd+++wtqAsRStckem6bk6nGOacabnvwft5A2RmA4uwjztrJUcfjR+FPG33u3ijG2HQnvPV/eTnPnqqVUk2BX4cxXseI2jN6vYNMSSUko1YUZTiue0l2sEYy5svfjKHfEfqRiTsUpZw/bqppa9K0c+EbdqIF1prjdKaYbvGVQ0ZhVWacbOW56vb4gpHip4pJ9pFi1aaqMCHILwN/ZaiLGkXK1BCmmhncsWQJHKkLAr6SRamECQNBKXBjxKB4Qq0Ngz9FuecKAk4J9wQKbJoF0zTQIqBYZzmZ0kJKV9TY4yhXS2El2CaiH6kmnVuvQ9YZ3BGY6qKlDPTOArrOwplDEVVSih+L/n81hViAPCf6PGeX/KH7/lTG/jTL6wXFvSPfoOfrvc5WDwFPg5/O+cmuMZs/8PuyJ7X/ryd499O9mTepLNzqBxfsno88WlucT6x8Tl9redG8aXzTpfrxvWlqXNqXqpTX54A3XCIZ19vXDrZkSeVn/jqVD+e3YCnaOfJ/T8GQqebOdmFU3vIS/f9es6+AwCu9jD80XigZnCcIWPYsaCjxJiptUEbi7OOsixom5YY35KSvCd3XCDOFbiipHCV7A1mw7E62PGqH9L1/WQwr1wXDp65fR0nT3n65c4xuntWz13W/utjMHfw4zPArJ7/vv943M7BRNmR2O/m5M6Y8KJ95vC6Do49Pu9JPw/Wq2d9PbqWs82+sj8/eDm8kQcP35Mp8RHGmaf1v+Kc43Xy3Bqmznw+NXFe2/8TVR4397yx3dv/3L7g1PlXdmRX0aVyorN7A9C581+s+8Skzk+++dEMIS+Vq4G3mheNsesYQyRFkVzCKJES04qcPHXdCAlZEICXUiIiHnOdIg54uL/DOSGw0tpQlBVkGPotRmmUsVRlQVkVKDQxJ6Y59Na6QhacDEPfzznUXsLTs0gTjeNA7jrIC4wRUqlx7PFzLvj6YS1hzcagrSH6icELGRbWMo0jIUUIQfKpY9wTa1nrxOuPAmVICbTOAmL7jmmcKEqR+tA5AZBzZrvdMnQdVd0ypTBrGWuKoqAsS1JOjH7EKE1RV6ANKYqMyHa9Zug7gp+wM5upJuMnL7rJrmAaJ1RO+BhxzjGNEha9rC2/06y5LXrIAe8HYVUfB4b1N3QP9wwT9N3EMCTGCWLWIhWHjCc5EaMQafkQGMeJD+uBf/yXG/76LoAyWKuQIYsUhcPaErQR76uxe63029WthDlrTV0VuKJgvdlIzvOehCvv889STpjguVmtmKLokG/W9yJJpg0ojbWWadiwnSaqpsZPHpSA7/7hDlMU5CzjgtKzV16k3Xa51nrWkh+HHh8CabMV77RSxBBRRnKpnXNUdc3yxkGGcRwlHz9LDkRGYVyJ94EP339Hu1gKoMvieQXIaDIGP4qEldFCalYUTjzv2grhnjX4SeT7rHOEGGHWe9VaE6aRED3Lmzek4IXVPASmyZNzottuKZyj77ZYaxnHiaKsKBuJopgmL4axohB2+BixRubWPrRxjgQwRhNTpm4XpChEZEPfU1QVfhCSPaMVpmmE1NAHNtstzslmj9lbHmPPNIpG+fr9t/zO7/2StmnRWnN39wHnLJlM0y4IXlIZ/DCgSGhtWSxv2HYbqqIgRTHSVVVJymIcTHGgbhdYLUBddNIjlWtkDbKG7b0w+jZNwzDK+rG+v2e5XH7aanrmvfky2HqhyledeN3r5Sno/rHKpR3Fqd3q62u/5rfjEVJPjjj4NR/146j76ujww9pOtX3JHPDsK/X03r/uTj0efSpH++wMOTXkh4aIc+d8xEbx6nJN/erEx/1Fvnzia5+C1xyfD9p59sOFrj3Zh+8NDY8ISSEGD5MNWWWRSNVmD65TiqSU9g2J4of8rrXdpwXtQLd62vKPWi6uWFd158wTpngSlXxxyE8ZeI6qfs3qdfL7E8j9GH9dXY7AyuFUv+px+SgUcsoyMc/La8rnnloH3VDH35/7+5yl7aNvxIl2XvrtXP+uqetCOdX105dz/iI/CXRfW86t6S/NySuMHD/86vV5WrgaeJdlSUyJcRzou414HutGyKeUpiglX6gshT1bG/FUaiv5wc4aUjZ063tA2KrLqqFt25koKuzZwKMXFmKYyZe0Zusf2K4fuH33JShNyuIdEyZsIYSq65pp6LFG46oKbRxaQ86Jse+xhUNrTVVXaKNxMzN64VrGcWDsenJVzd5Bi7Ei6yTs1Yl+84CZc9xdWeOcJewYplMU8FRVsweQmU28QKEwSlG4AkXee/iU1pJnZTRxCoRxYNP1ZDJvvviJzMOcWSwWwpAtFyMs3s5JjnshQNVaR46B0gr7trWO1gz8wfKOUgtoDuMG7yfGvqNff8uw3rDtA9MYGfrIMGVMUWNQ+GlEZQmfTynOOdsT237kN98P/Pd/fsfDKBsBZzVlUc5h2pN4+S3UZY11xcy6WsFMprXTc98BbGeNGFTIs1apkMAlZ4kBQoQqplnbWa596DpWt7cSHTGNck9cgUaIwoYQWCxXLN++IyVwyQuBWd3Q9wO28OTwKEFmZu913bRCeleWYgwahLG8bhdsN+s5jNyKhnjw81woMVozTSOT93SbLTkl3rx5wziKFn2IibKuebi/JwVPTGJU0cbRLpdiZJmjLVIMKOsIgxB/hckDEmbYdZ2kX5TlntndDyPaGbpuA4jc3jgOAExzeDxK07S1GMuUQqFIMYlHJCXCFPba8cZaYvD4vsdakTGLfmIKmZvbFX4Kkt/tHDmrmVSuQBtDDJGsNNX8PBhrKKuKlCRU3MxpBq4oCN2GftsxFVHC2UPEFZqb2xtiSkJ0FwN+9szEnNk8PJBTws9RMEaLpJ0QHErevDVmL883zsaNcegoq4puu8XVFXVZ03VCrleWFT/56U+wM4HipxV5o5wyap88lN2Br1/QX947XVvnDweGP6acv66jN+6LA3D6DX3x3X4W6M6+gUf66cdfjvJxP6bsazhltZn/ORkCfjDJzg3HxXmoXnYkqMM/fpSd2dOqjz2Vl4iqnny4MCD5hBzNNVjk3O/q5DEvIevnH08xrCv1mP8tn+czdnHMSs2s5+LI2KUF7XTOFfLbI/HoLqdbHXm31cUuf9ZysOZd9Qh/WhMXp+6T+X3893Os/AyQX4EHXlxKn3y8UFE++ONE1+SnU0aCU3WeA+rHx56zWl6aK5/VGLdD1xcqPDcYx+VU/3dWcXXw+zlweE0b5/p2lQXohXM/qbxUyYnGLvX1HHh+TVfOnv8Cus4v3fDPMQE/6+A/KVcD7xgjKYPRmrZdUDgnnlXvCSkQu0hT1yI5ZgTETEOPqwDrIIH3HluU1O2S7WaD0TK7Y4poRIdbF5aun/W1lZbcURJdL+G/Q98LUI+RqhGJJh8COUaRQII5t7YkowghAAlXVeSYJAxdQ0qalCJTN2KtkJPkqiLOYfQ7uTJbWJQWr/k09Hg/YYuCoqjouw3DdkO9YNbGToQYiVqjvZe+Z8U4DriixM055EYbJi+5vN12ix4nIX8yDuuENKvbbjHWzGylIn+llMZqQ4yBceipqorJT8QpULataIrHiM6Bn9Rbflp3GCbC2BPDyNBtmMaB/uGOvuvoe88wJqYxMPlMypocJN9VKU1OmRC8ePxj5m7d889/dc8//as1AUNTVbOEXJyjAGSi+hBIGWyIDMNEuxRG7nbR4qyd1zfJaa+rCo2a2cMFQPmcaRcLSDAMPVprhmEQT2rwoMAazWb9gCsrcooUZUlKWQiyFBIinSLZi5e+KCSlQMbdSi6+9njvGcaRFNZYJ6zk7XLJNE14HyiregbEiXdffEm/FVB99/57Sb1oW2HUNxpQhHFitVqCUsScxHNdFKCjGBqMJgYB9NFPaOMEiM4Efd5PbO/vWL15S1aK9f0d7WIp8nEzH0JOIiOH0hilGMeReuYZ8H6krCtscNRtuyecyylRlBJ67acJYwwpRYZhJPgJssz51e0t1lpiHMmoOZ1Dix49mvsP95RVIQA7Qxg7lDHCsUDCx0CYJkkl8X6/SbRG4bUSlnrr6KdRyO1SwOmacZwIMbGoKvptR1nXuMpho2GX/JN8pKob0VEPkaETVnaywlrL+v6BZrmgbkT3O6SIyhlI1E0jkQPW0nVbcoj0XYdzhhgtxpRk/YnAe7/Wn1mwP+VdcAYVvHrDf/bsz1DOIpfP8wJ8RmR6sq1LL8tLQPxQxuwV/b3w/j/e4/2AGPWo/usMDbupmg+/ODg4H31+lpt6orxuq3L+6KvG6tTpx9+d+O2YreD48Gvv2TXXeNHocdS1w33/yXp2RKwz8tutrXKOgHAF6KyfAHXUU5/kjsX8qcnh8a/PipleU17aS58dzPO9PbyW48j2Z2dlnmt6XzEQx0D8uN1r+vmkDxeOPDQSqMNjj8D2a+7fVccqIKun0RfnyscudB+Dc66yqJxu6snPP9QCfa5vVz9k+fJEPFPP80t5xeBee+hrDC+fe1F5tlCduHkv3s/9ZObzdu7lcjXwHoZ+9s5a6qYgRQlpLcoSlSNj1xGsYfKBMU+kEGgWC/EwlRnnCmEWz0ImJeQe4llLUfQkFZnkvXi/dp5mwA9ChmTmnFwBrBvCNFE1DSl5jHWiBzwTTSmtCMGjjSInjTECCOu6Zhg6SJBzwg8D1A1DP1BWNW4O941RdLYFfEk4eohBiLCyeNB2Ie4hBLbjKHJGzmGN5ICjNOPQE6aJpm2Ic+hsMsK+3HcdkCmqghgDddNQ1xV+ztvevUitc/hxEkAUPH4aKYqCEKOElelADoFoNGlY8++8nbhxW1IY8WEi+AHfP7C9/55+2zEMnmlSbDvP2EeULcC42dPdC5AKgRgDfhwZpsD3DyP/3b/6nr/8XryWzjl8mPaW9JREEk4rRdMuKKsGV1QsVresblbUTS1gGhinUV5sWjP0PRnwk6dZtBROGMNtUTCNAypnqqJg9MKi7cee1e0btBbv7ND3xCh58NpYlqvlzGouxiJRt4pME4QQyKNwCyil0cZy/+E9RVUz9ltUhqpuGOuawlmRENMKVwhD+jj0kitnHSwWe1A+9B3ru44vf/ozjNGUZQXGYJRiu34gxURMSTzIxjBE0ZzfSV2FaSQFxerNG1xRoK1BKSPz6c07iqrEFSXddjNLfomOa7/eQFVSljXTMBBipKprqrohJ8nrS1q8IH3fs11/Rd22lHN4f99taZYr2qUQI0r0RyFkg66QzVwWJnhXFGzu7yTkPWhCyBSl8DXEmPB+YhpHUlYEP0oYuNVC0pYzad7VWOtAK+qywn75E1CKwpXYyROnYSbLi4heuyWEJKkXMeNnSQNtHFbNkS4hEuKsD5+FYPH+7h7rHCpLSH7TNoCA9eCRHMeioGnqvZLATn7uk8p+AX/qt9q/09XR+/2Va/0xIPghgdxlt8dxUUeHfJqlOO//fwQgzxgeTu+ZzoOr078e7/ufE5+des8/qSfvsNHuR8XR4Z9WDi50B7ryqX487dLZT/KNevbzs9t48PHUWKtTA/s59zGfq84T5z/n93162I7h/Vy3LnfpWJ3+xB04qOQQfMvn08/eo8f7sQVJAT+6mhPe7LP93f2W91X/MFvRw0s6buAS6j973mW0fmlffqqK/fw+ehYOAe9xS/no/FeXHYBWj0vGqas6N2THfbqIhzJnHcdn7/nhunPqpHONXpxEr3moXzjmcN4cDty5Jo6fg9cAyNeWkzeM640F58Dr53r5P7m+kwvkiQX/6Nxzff9YS9DFPh504ozB4ZJR6KJ95dlre7eifv6X2vXkarNOsZ8mYhiIScKbY0rYskEbh3MW7RJ+mhhT3pM/5awkfNZorNNziK4mBNGpNlaRsiLPGsTGgCtnxvKcaBZLrJEwax8jIWWULahLyZF2bg45Dp5pGHFGU7et5KVmhR8HwIArUMbSNi0+SM6vUeCDaB0rrdDKoDUzQDBYY4ghMYwDVmsxBHgv+eIqk7IT8Il4NFMIDMOIRvH9+/fkGFkul+IdzCLDNW2F+b0oS8qiQCmN9xNDtyX6QFlJKK6dw5/TbASYhgFXltSNY/ITlbOkGGialpwSren4xU89BVsB6MMWP/YM3R1Tv6Hfbhk6TzcEhiEwThFthUBNcsIiWWvCOJByFtmlYeJX3274b//0A/d9xhpHWTgJr0yKqiiJKeK9AC0hP2so65YYErfvvqCe2ctDjCyWK5y1LBbt/o0VvaddtNSVRCk07UI2IMpQVJKHXxmZRzdv32GtReUsxGbOSR64MWgr+dsxBrrNhmaxIIbMOAwzoYxjHCdiCJIXnRI3t28pqwrf1IhEnuhGZye5cK4oxYPbdyhm1vPwSNiXYqJpWrSCcexY3t4KH4Ar2HZbyW83BoJnGnq0MSxWK8hZgLu1bDdrMmC0RWtFrWpSylRViTdGcvWyyKJRllhjGCdPvWhwtiCkSNk06HFAIVrXi9WKnDJpHPju/XvQmuViiZpztaumlueNTFHVdOsNrrCEGLHKoo0sDUo/EgG2iwXaFqA1GkX0I+MUcGVJDmG/S9XaolWUOdqUlKUY0oIN9NvNnBbSUFUtSrGPUmiXS2LwVE2DsWLcc4UmeU8is3l4jzWOoe+Ik0dbgzOSE++soR/EiOUQwC2efYdxBZMXUkBrDcZoclIM3pPJeB/QU0APw2dbWPOl5f/Kl/m59+Gp9+APC8J/7DKDif2nlwfrEShf3r1fO07PQfjz+/naMd9zAp068VK3T5yzlx7b9/DkWQeVn//13OdTZzy9M8/LkankYvvX1/OKgy4YAZ6AYMV5RP0JD1Q++P9LVSt2m7yDHd8J4HU4OZ4YhHZge48U543ii+Ro6jUff9xyCXjwCIpPXeLF23bhovb34wSoPlnViQX4mjG7Ztl/dg2ZJ9ktl3DS2aftyYQ7f+DZeg/H+6Xn4hKgvHYAPrV8ygvx1QaEz9jOSweqo6+vusYX1uCX1tBTv13T7ieN11GfLxkfLhkNXiyPk/K66fL5APjVwNtPI8GPGCekWcwEYeN6jdaKsqoYRk8KAaU1ZVWSY8TMOcfC6pyYvHgbm7YlJ5GcQitCDIz9QF3XM0lUpKpbYvRYY8QLnLPIX8Weum3IOTNOnhg8kw8iY5UyRlmGrt+TK3k/SQ70vHxpY9FJgFuc5dCM0YQQsYXFlQVGaUKSXHTvJ9HlLkQLWpGIMTGFCZ9kZazKkmkcGfoOozVNu6CwGj/LU01zzm0M4kVeLBZkpfA+CGt2joQQCcFT5GIvYRaDl7Di+W2zvr8XWaumIfqJFCO5KvnJEn6x7FChm8PPB6Z+w7C5o1t/YBo8XdcxDJl+yqSkyIgOds6ZGMVjHfwMuoeR+03Pv/ire/7RX9wRsqGqK0DNuebCpB1iQGtF0zaUZY2xjuXNLU27mgGUEx1260TrHHAzadjqZoU2CmtXpOBnYraCmCV/N+fE0PcMw4izBqUtCkdAGMW364d5noiXd7N5oO86mqaWHGzvSUgaQIiSH640rG7eoK0hTwljhczLWIv3Ae89xhUobej7jjCzdLuioCwrttsN3nvxyjuPdYWwsRv7OLcyIr0FFFUlEm8pU7ULVM4oo1EZ/Djnphcl3fqB+65jsVphXYGx0PcSfaCVRG8ImZ4sFCl6tHUzgWHApygAOGfKUsjd1vf3hOBxZYG1jrIWTW7tCtq6pmpbmZM+iDLBOJLGaZZ7E2+8AtFGzxL9YRGPflZKwvyZ8EPHNE0s37yFFGUclMIF4TeYhp2eu6apG2xZ0m87MpmcFdZZYgyYOa2CnEkpzXwKSgx1Cdq6QqvMm/YLxmlk6rcoo1ksRCO8Khwh5ZkUz8jaM42kDNuHB4qyoK4rMjCME2oKGOsYp4kxeL748stPW033LwL19OPu33kT9ep1+8Jb4Un96jyWeG15fL8e++Gu6NSPtHV/Fnp+sf3LnrFTdUj1+cltzZmnsj35+bt/byg46N+TPdP8/bOuH4Ku/GQqnSzn7sDzYTne/V5bXt5o7O/B0dzbb2U+x17lY869anOoTh+XT3d7fz+uAB+HxMvPmj3z9+XrVCcOms0gc8f2RpF8dNYR6LnY/R/60f2Y+XBwzks2hatm+PFScDAux3v6l7p53sR3xfFHj8ipOjJP15uXMNHFH06B4tfe7xcG+Mnt3a2Xp9o+1/DHrhenDDa75/iKtfTsg5o/AXe/5sSPxY0/Rjk2mpwzovwQBooXyvHz8fxXTv94ZG17Mq3PAvrPtLniFcC7qmtcUTBNIykw63BP+/zUnCNdP2C1orQlMYuEk7WWuml5uPsguddVTVWWDOMorMdliY4TMWXa5RJnhQCtyKIJrZViHAZiiihthQE8iXaz0ZpxBp9VWc5eaFDKiGEvJwEoSgC5NpqMMKPHFOk3a2FDtlC4Aq0lZBYUwWghGJvrKkrxTMepJ6VEWdWoWrTIM4phFDIyax23b94QY2RVvNl7K7WZdTe9h5wlrziGmYjMzTqchhQNeZYJ0VqjrGWc87uN0hRViZ8G8erXNYSeX94MvG0i2ffi5Z4mxmHD2D3Qre/ZPmwZh8S2C/ioycrsJ1ecBrQtBdz5gZQifT/xm2/v+Ud/cc9fftdjXYXNCas1kDEqg3X4nJgmP8+Nmnqxom4WNIsFTdOyWN5gioK2XYiHvyipqpKyKgUU5wRRk1WSCIN5km8eHrDWUlQlVS1hwtM4oJQYCIRoPclvSmGNRilw1tLMuc5FVaIR72/IIr2lFHMagKecGcyVkbmrtcEVJQ93dxhrGfteIjiUpEaklNg83ANQFAXL1c/Zrjczm38EYyAKi73WZjYwKYa+F0CcImEmAByHXiIanLRTNy3tQowQIv8i0i/azOR2ORO9ePI39w8sb29oF6t5XgBJtggpCtlgP4yQM2O3FeLDOb9dhYG6LjCmZBxHIeBJCTK4UvL1LUoI+5J42DMip1bVDZqaoqyJKdDd3wu4rmtu337BMM2GIiCnDCSMK/bEcsYaMSylSOg6yrray+kppanqhhgTYRqYvMdoiytqus2WonBC4HdzS0pROBmKFmMNKmesk6gAWzRo7/HjyGZ9x82bN/icCaNEmMQQUa0VLoSsqKsKtMU5i1Gg7dXL4fPybPF/vug/8W698IZ6aTO3A8X54NfP887LB/8//O7UzubzlKfe6sd/84Wx2hsy9p9f7s+JO3LU7lH9T/ryWMGTTfkB4Dx5+w+/urTDvrKc20OfbvolGHJ8lS/VfWEDc+Lr1+6FzlZ07fHXOndOHHfy1GOwc0Ufnm7wr30m1f6pe3wCDmfp836e6tB+pl6JBK+ZS5+1XL0xP38jn1dxepx2Xxzur696Tg+WoD1oPARxPJ0P5+bUMYjfn3Lw4VR/d33dH3/8YH7isvsqpbi5vWfnnPt8YuqdN44etHGqzuPD8kf0/fCPUwSYh/Wdq1u9YsiP78+JufDjl8/U8qU58JJB49VdODrh8H6pg7fa7t6cOPxFH/aF9fDHKK/Q8VYMw4gxipSF4dsZS9mUQq7VbyS3NSPMmVHCwkXSQuGqGnLGagEjShvJ+VUarUuiD2gkjFuRiUlAc7/ZoFxBUwmLuCscwzAwjn72RAeqpuH+4Y6qqihdQSahsugXV3XNdtOhtNwODULsZi1oKyHW00SYPCGKd7msG5wrGLYbbCVM0EM/4ArHOA5oYxhm8NRtt0CmbltQlmZRobTGKnmNlmXJZr1GK0VEDAkoRHO7EA3qoRuY1IDRkr8aY0AjpFJFWQp4nCYwirHrsNahtKJb3/Ef/PKG2+USlRMhfsMU7ug33zH1Hf3mnm7bs+0z3XbCJ72XPwMgCft8jh7vR0LKPKy3/Ovfrvkf/uKBzSRGirIsyCntvZYhKpIfMK6gqkWLu6wb2nbJYnVDvVhSWEezXFBpqNuKbAo0SGi/D0KQ5QMxeKy1LBYrtMoMwzBLWSU0s+SVnxj7ntXtinmCiRzK/G/ynpQDRothRRvJLU5KUgM2DxtSjBSVSLeJtFmQ8GwtgDVn6PuOumnZPNyLd7Ztca5AGUO/3QoTvrUwa3I7V1Dt84QFwOacuf/wnhQjab4GV0ibO/K3se8hZ5IrKOsG7yeUmaMPYibGcS9T1jQN/XZD0y5R2vDmyy9QZIIPsJPqs45msYQc+earrwhBcv9t4UR6zBiqumTc3GPbW5EOU4rvv/6KxWJJDn6OyJD7vQPeikDf99Rti5v1yNf395IPbQ2ucEAWdvAsnnEVR+r2FuXEqx1DEDLBaZz10MWIB4oQE9ZJW2PfUdW1gOw5JSDFiNGSF+79RBBnOlaJlB5AJtFtOtEfz0GMXBrattmT7hlrCQ+BTObu2/eUbY2bw8+N0yhtyEqhTPHxK+krF/Ln+5Gnu4FL76uzr5QfbQf9OcspqqsL0G/eEOdnx7702rywQT844tQ+/HmmLk824lz67ppycAmH4PzkHDg65vpyeXxOV3mdMeOJPWn/gzqwTvwbKK9t9+jmnwRUp/4+PiE/wufPcemn7stlwPyyseUplnsCC19o+dPKQST968r+gmVks1IXvFy7Y3mySX92NS8M06lw8sOKjpt/ArSPwfXhdR+Cah7B/a7qq/gSDteLw75evwSeLfngQtTxnP/IOq++6bv2PhUUnXuOX3o0rqnzIybwSUPPqXo+oY0fpJ6PqfPU/Tv3rF56iI7bvGSwulheMQhnD/38a+HVwHu77WYpoIUwgytN0bRUZUnfbamaBVprxmEkhIR1htrVxOjpthuUltzK0ScSmdgPoDXWlUSgsBJSO44DWkvekrMO2gWurESnUil8CDw8PDBNgaquMUbuitXiNSucmVm6J2JOaMRoUDW1AIEoIfM5JckVVgZbOmKM3K6EDC6ljDUGYy1GibRWThGtSwm3zhptFMZo3r57K1EAKYmMU04kZyV83ZrZ0y4a1n4YyEqRk4S4+jBRliV1KeBVa8m/JUe2my33fgYXMVAUJRlYrJZYY0Fr3r0pWSyXBO1EVqkx+A/f0D98YPuwpg+KzV1PPwRiNGRjMUahlGQshuDJYSLGwDRNfFhP/JO//MC/+noAU1CW4kUmCyu81hB8JOZE6QpcUVGWJYvljegit0vqpqVwjsViQVM1LBaSOqBchTHiZQwh0HUdMUaWyxatNCEGnLYUzrFaLhjHcTZOOMqqpGkqcspM40BZt7L5VvK66vpOJMAKCynzsF5jjJCnaaOJMbBYLijKCqUUMQSG7RY1G0CU0oQg0QfTNNEuF8QQsM5hZ894s1gIc/nMBqu1pCZsNxtyltzhlESvPSfR214slrME30RRWFTKlEWJXq3IUUi9vPcif7ZeCw9A9JRFBSrPUnmWoizxfhKjxzShlKJuK4o5ciB4SbfQ2lDUDXH9QF3XxJyI00hTV8RxS7V6g1aWbrMFoGlabOEo65o8R5dkMmiNAqZxwBrhXvB+Dr1Vcu2FkzB3tEZpTfbChG9cRT+NqBC4v78jhUC9aAmTp6gqmrp+JLObQYTWiqqt0SiMLefUioTKiZzEOKOVIYQRY2diuWlDVoqqrIgmQlaS0+1Eok0yQJSQEIZA09ZMk6Vp5RkzRkOOJD9JCo1xJJM+fiU95Q458fPxxvf41+sbelrTyc3lR5WPeVt/7Jv++l3dqU3tqZpebuuwtnP36GDDfHzahYaOr+aJx/6Kc0+xhu88PadH6BiCnTrqkqvicnktDDvuzaHHcN/Da4DFmU48Hn4Jjr5QzlzIsefkHBw9XccL9/nSJvvJIefmY55/PXjed+CIlwwx58flsdYfx2L3cTPvRLkEmJmfu4N1eD8vj4Hdlc3sDz/3DM91PsEN87GfhIVfeHT3IPmFfpHZK89dbDSfqOo1S/q1gOul+k+dd40R4GS9Rw/1R4DvfMV9PNuvHwIEX9WPUxbcT+jES/f21HU++S7vX6rPDEzn2lAXfrvQTdkLqRd4CQ7H56DyF9v5PDfyauCdZ3kwhegW1+0SZ4zIPM0SRTmD0sKIreaNeYwJZRw5TmhXY4qKlBOqrAnTQEySJ4vWJCUhtj54yQOOEnabU8J7kSpKMeCnkbIoWS2XMIN0TYuyhmnywmqNmnNoM2MvecsKRZxDaxNgnCOOA0k5lLGzd9BSFJJPXDctIAzpKUb8OFJUAiS1FpbsYRzR2lA1FcQ0h/8q+s0aW5Siwa00piqIc262VjB58c4FHyisY5p28mMJ0BRVzbBdEzOkGCS81pRiLCCTUmBVWbZ33zFtN6K/rTMxBMZxYL0ZGMZE13lCshhXSuhwmkng/ECKgRgj/Tjy2+83/IM//cDXD5I/7QBbOIzSxJCEHE85bOFwKLQrqKoaW5S0qxuapsG5kuXNjYCsuqGqK3xCAPFcUoooJZEA1hrROVdKgJ8W3WeUoqxqpnHEOQdGQyrpt1uKqpk1mp28KJRisbrB+1HYtaeJ5WrFNE7EFNk+rLm5vSUjmtXi/a0ZhwGFaKLHmDBWpN3KouD2zRt8EO3ywrm91mkGrJbwa2HyFoZ5kejSbNf31E3LFKJ4icuS0PcUpSP4CefK2RhjmebQd0nXqGjtkm6zZlEtQcncVMg45QwxRJzVZBPnHHAr0SRKidxbt6Xf9qxubrA3N+SZW6BqF0BCuZIYM6PvGbsNWmvRD095jsZQKFugsoBPFeX6jSuwRTmHwAMp0g09HVDMINe4gu3mgeXNLdYaso/03YbCOmwlOdU5BuI0sZkmFjsSu5RRWuZ2jIGyrInz8+unCR8jpIyKiZREUUBnzWbdkZVEH+yk+sq6xvuR7cMDWRuWi5aMQhuDSgmrDdYWbLtO1gVvWN9/oLCW5XJJSImq+ARW82vW4/y4ecvHL6ij859Vd/El8r+08hrTxKWB/8RdT97dGvXkfp0xxJ8tx/ugC82d/HSKefxiW1c1t4NdR/UfHXGJ1ftsBz7rHM0HA/6Zd6/HlpbPVP3+bXFsdXj88WJ3XtyTHq0dHzvkz81ax7/+G1xsTtyPvaf78Odr79uJcbq4H7+izkM8s/fA5xPT9KCPmaNjdgaUV8y/feTAU/vCvoHd7yervMLI9dGPwTNLxQu/X3PN54xYzyyVJzr/GnvkuXaPDRuv6fPu3HPtnurfRw3+hQXmNf1+qYlLxo9X1L2n1rhkDLp2nK99Zs59cbVF5XMNpJSrgXdZltimJaVADp6YhY16mgZyTlRVQ5qlg4w19N0W6yx2DnM1uiEGT0wJZw3DOOJ9pKxLcvBsthtQBlsUxOCFwCtFCTmd82utc9R1Q123FEUhAL0o0ApiDAwPnUgtWYufOmIW+8p6vZ5Be2J5+1ZYjb2n77aEBH7bs1jdkFFUVUkKgX67Rs+s4n6SfN2QIn6zRgHt6oZxmlA5SY6zMfgsnuxpZmvPKLq+J0YBLXmWODPGSp0kus0WM+eExxDRmr2kknUOjUarau/9BBiHfgYnBdtf/VOG7RrX3FA1DWH9Nfff3/Hd1+9JqiYph7ZOpNPGAVuUhMmTUiD4iU038a9+88D/8K/v2EyJuipRQFE4IbOLfm9ByjFByNRtQ7NYUlUNi5tb3nzxJW3ToLUhK027WFJXFf0wyLW6AhUn1JRQKROjx5Ule1t7RkB2WbJZbwSIp8w49pSVyJDlJG8Zow3T0DONA272YO+A+q6Mg0RNhGmiWSxwZck0jAQfSENPvyPvMhaURFKkHHn77h1aKYauByNzONbNXt7OuYJxEP3pcegIIeGKAj+OZISjwDrHerMGpUUGK0WC97iiRCnF/d0dYZpY3qzo+2GOKMgi1XdjRDUgJrKf8KOn326YpkmI2UKa9emhWz8QEyij92RvY9ehyUIOWJQYeHzmqlpI0lJgcXODMaKNrhAP8DgOxO0GV5YYY2naFoa8H7cQRVKsqhusFcm9ohL2dZSibYXZ3fvANAygFNYZjLYYa6j29yeTgoyHSQllLDEEyqKEnBj7nqIsWd2+4f7+DgVs1muadkG37RjGO1SOGKOpqor25mZvUDLG0SxWTNPEppsjJqyjLAumGPBjDzHI+SiRdCsrwiw/139GVvN5Wj/Zg+ylYq7YDZ3FGke7x+u3xj+k6f2lHdZL553bOJzbdX1MOWzrTP92wrhH3x/u8a4Z78f7Ljcrn3lZP9k05wt1n/nhcSo8HceXWeCfnnXFoVcdsp/rJzZoJ9u6xmZy/N0O6cCjReDcRv9cm5euaVflscHhFGh4UumZeo/mzTOgdO48Di/15Qs7t5/Pz775t7zkg9t89NjswOlZIwQvT9dTygKHHHtPRmpu74nHUz2fdodt78+70Mdd+5dkvZ704fCX3YvkyTgcTrLna9fTPz6hHHTsbP+vXfKvOfYS6D7qz6nT1OEXrwXQl9aJw3NfYfjJHKyNL43dcRuXfj+s//g4dXTKYT+uKQfHP3kODvcgx8/kyaIex/TYYLR7JZ9cWy9XeTwOV8Pi43ZfbPOFyfPKJfYVbEJZclaNY+i2mBnMaG3I2ZBzxlq7l8Cy1hGz5GoXiC52zkLU5P1ETtB1HbYo0a7E+Cih3dZSWEOICWWM5OQaRd02FEUlIMtacs6SAzxO4pEOfp5VIulVlIWAGB+pm2bOUTVYayiLEu8tzop3fbtORO8ZchLPsrUoaxi6jqKuJVe0KlnerAh+wntP8CNVVVHXtXwOfr5uTfYJnNu/9MZ+i9aaspac4jD3b+o7XGHZ3N9RlCUDUBZOiKKcw8xs7tPQE7UWbWKV0c6RvaLvevr1e1LWqO0HuuGOnDND7/nuuw5c5ubNG2Ga1hrtxOse/cA0ed6vO/7HP/vAv/j1GuMK2rbBGIuddcwn70WXuijQRowetihZLFeUzYKf/vRnkg9flDRNLQA/JZy1aKPwfmKxXMzGl0pCuYcJVxVUdY1C4b1Ha6jbBmOMeP1nMi7rDBkJIS6rmrptJN991hdX2lBUJVMv+fHOFbiqJjHgnEQw7N6SyiiscRRNQz90TIPk6pdFCVpyybNSTNPEOPQUVSWa210nRhDr6LueHIOQcWlN0ZQorWkXC6ZxIoTIOA7klCXcPGWRx9OaFIWpPcUkjP5ZvPyb7RY9h5PHlNjc3dEsl5RVRVEKE3xZVZAVGSEcrKoSY1qij3M4tWOaRt7+5EvqupGNvjZMfS/h8oUhAe1yRd91QowYAu1iKTrqwaMAbTSbuztu375lHAaKopqfcYVOYLSWsPQsLPDTOEmutyvF6DONQsA3h8ArpSR/OkX6fktZ15SFkCN2XUfVtGIMiIlsLVlJeHtMmXZmxC+Lisxm9qivuNGGGCJDv6Ga772qSrl/ObN+uGcYPVZrvB9ZLFeEWEq0SiGSiIvVjZAdaiXEi0qTc6Q+MN587nK8V3jdWQc7w4M35ufYS53epRyWT9msf6x1+LNc2RVtHwH+/OTT/oiztRy8+I+B+bUAHR7Pffr+v9z3JwadE3VeLieQ8YWdwxUY8fG3C4Di48ojclfHG7RzrORXVvmq40/d2Iv1HBt1Xq5fHfz9rN1z1Z20QahnXT1XPvYJ/ZRyDeY5iWqP/774YL781cmv5zHPZx7kHbZ99kyc+O4ctjp7X47XgeO9/dzGxefroB/Hx736+XzpRn0MWLqmfnVw/Yfz+miNPUdd+AzUnerfNWPwuPScB/PHdR8D/OPqrl4/jo6b1749yH3yglJPUwlOVP5Ma37u/w6MnwWv5+bTNUDzFKg+GJsnbR9+d/zbpXLlGD6/VUcP7KvQ+lH5iLl/NfAehx7jRGJonEbCZoNxjrppsdaglGL98EBR1cQkHjvrClxZMIwj0zSSYyLEJDnWGYqZodxiadqFyBkFv8+LlpzxAZMMWlvi7OEGhZ1D2VNOjJuBsipJfmJMI6W1JG2wriCnTKE1VdNijQGlEe1wYYsuSskdHvuBMHqGPgso1Hr2nHtWt28A8NPE5D3OCqhLMyszORMykGDbbbHOEYPIrWmtaRYLptHTb7dE7zFluc+RLaoaB+QU8V40poOfqJsGgOBH6ralLEuGrUQRKKUpq4reTyLvFgLGVRJ+HAfQJR/WI+hE2bQ0tXgTwyj5xNM48ZvvO/7Bn77nNx8GGQ89e7RNJmVQWkteeRTQWdTtPpd79eYL3r37QgwazhGzmnP4DZlM129p2oVIpqVMP3as7+9pFktyTkzdhqaqMNZRLFq26w2BiLUSHu1TJPlEUdZ0mw1JA0qhtBIWeGPJLkJKWGMJ1jJNgbu7e8qyEA+tD6Qouu6N2oH3CmU0WksqQQyBLkZsUaFAIgxm4jUyaG3Fa2sLYgyMQz8Tg8lv2poZWGbGoafbdkzTSN00lGWB1mYfgTEOA0O3xdqCmBLB95KrvVwy9N3MSG559+VPcKWTMQlRIg/mp3uxEI96SonJB8auo1ku0RjaRqTcJPS6oHQWv/UUTowVKUW81mRgmjxGCaG5NpK6kHPCupLbL2qMMSgU2+1MmjZNuKKc001Ggp9mEjzFNGUhYtOykHXbDdaVNIvlzAgfmSZRP7DGCJnhTJITgsePA3GWcbNOxtRajQ+iRjBNA3GaqOuKoiiwTggNq6qiKGu5Lh+oqpqu7yhrIUNUxmCDeNuHbjsT5YlMYfCBwlS4qmEaR1kjjEU79/oV9HOUS5vCK3fGz18sV5pgP2HXfZWh+OqaXvP952rr9G7sAt55UvZgJz8e/Un70H3D53dn6vjDR9+A55m956p7MhbXtHm4ezo1f6/dtF1TjjaMrzn1Yn3HlR3fm2N0mE8DsCflVAc/pdOH5z7p99P5c+mWXW726ZmfC6RfXNdeaGDvaT1zQdc+Ek9uwc54tvt85Xknwdep/h9d6zV9PAmaDwwqT6ffhbzZ+bhn6SL5aJa8cC+O29zfg0uT4tQydgzCzrT5bMmY788T4rpLz9qZep/1i+sfwZNr73G9Jyp7NkRH9+PFyIGDe3UsZfnk8Av34TCC58mykZ8e96ztM/UdHnduKM5WszvvGHS/NCY/dHnx+j/LGwZ4BfC2rpDwaK1pFyuGcUArJWAPCSHOKbJdP1A1LWVVUrULhu2WYRox1grL8uQhKYq6FeAaPN0M0o2xWKOJXvJzQ0L0j6uSetFAykzTxDSJXnaIkZQVKicKt6AsC/rNhqIshbjNOMmvtlYAYgzklJh8RALBwdoKbzzaihfTKfH2lq4iF8LerLTGGEPwnuViyTD0kp/sHDvN4V0cRt22EjZdVZiY0FrGLwbRIzdGU5QVRTmzhc/3c5oGyIqiKuk6yCljncUrTYgR4wOmsORZp9hPI+iC1ern6O7942TImXJ5izGGTT8RfEQ1Gp9ES73vB/78qzX/3Z99YDtlbm5uCDGQgjDQ5xhJs5e2KAussVRNy+27n7BYrVBKs1jeslguQSnGcWKxXNG2rRDG+QFt1Oz1NpASN29uiUFC52OMqOipq4qsNc4VmFvNsO1mXXAB3zmL93N1e0OKCVsUkMWbHmOkaAXYTePA0HUM/YBWSCixNrQzyDfGUpYVReHYPmzQcwh5Sgk3RxWkGBg6iUrISEi5s07k80Jgu1lTuGI2NBSSxw+YnJnGSRAsEp7ftq2APufot53kyWtDs1gw9r2wnceINprCFUzes7y5mT3jojuvmHPdybJgzrnc4zQyzEBxGAb82FO1DXVVk1K7z+sWi32mapcio5USs84XZVGgtJbQ9KEXTW6ypGzMgFUbMSZIXj9UTS0SdV689q6sqJ1jmib69Zr18F5Y2WMi54zSmm6zwRYFfbdFI9rvOSu0hhwiVV0Jj0JZSD5+DOQMfvJUVSvRI3Payer2lozCjwN+BuFhzMKirhQ5Jzbre3ISWbT6Tcv6wwdUFjK7uhZjYFnWZAamvmezfkBbQwxJonLaCqc/52r/dJE+lMySd80r0cfpP88a8p9v7D4Cybxw1Osx3/E1H34+hSI+R7k0zqfbOXddF6/3yYB85LUcNXDqXu5rVs9/ew2MOr5/11Bs7d5V12CNjy4/SKUf0f6TLrxwY67o61WXc6q+U5PgxL2/NLtfTj14qUOfD3QfVXv14U/A9uG/JwHqFXXuQNshkJv/d3JtOwE0MwIeLo/9aRD9okQXT6/7MLjj5H146QYdAtprBukIFKnDpex44l0zOT5xAl2/ep9oM/PsevZg77iyU2OjDu5X5rnX+YVyboiObRFPjjlnrDmeS2cMFU+q2c2hw7rOnaB4mYTvcEx3X10Yi7MGEnW6f68vJ1bCayt7cq2nLvzlRf5V3GxzuRp4x7jzvikJKzcaP44ErVFKkcJEVTpsWYPSInU0TaL/qzUheLqHB6p2gStuhEypkHDwafL4KVBWJePsxTTW0vUDdbPAFQ6jLXEmfrJWQnDVODIMo7Bye48rHO3qBj9LO22325khPeOngaKs2WzXsgFXgBLd5MI5EgpyYui2xFHCZK0xkiebIsM4Mmw33N3fYZSmWbSMU0DlxDj0AtxniSulkDDfooAsIHJ5sxLglRGvtpF89jBK/7U2qEIIs6wVY0H04vUrXEGcc82TH0lZPL/L5YLOvKE2a3SOoBS2WnLzheLdl+/ofvOekBLTnIu/3vT8s1/d88//+p6s5vuWoshyWQkNd65AG40xjrIscWVJVS9wZQ3KUlUVRiuMNhKObT11LdrsPiaqqua+67C2oq5bgh9x1lEUpUQsVBXjkJl82LN1T4PwBDgj+uLj7PU3xswEYohc2p7MTrPZbMT7nAQ09tstrmkgJ4L3bNcPhBhROaG0EuBfCGg3WqO0RFkYa9is1xRFQU6Rpl1ICLIxbDcbirLm5s0brLagxEubk0QSpCjs5TEnwkzGFlPCD+Mst1VgjRHm8pwoqoroPfcfPuCqiqATU9/TNPUMMoPolIcsevVOtM+Nc3JNM2P60PdYa1mublCovQSbtiLNFWOg32xpFi1x1uBqmlYAtrVMkwcydSM52pIuIm/3yQd0ELZ1rQxNXaOtJcaAndM49Oz1TyEIkVzTiE736Ik5o7Sw+KvgGbuthLm3LTEGjHYslkv85MlaYRQUVUmYDJOfqKsKP/bYohId8pQI8zyo6oYUA33XQ0o0iyXGaPq+w2vJj/fB882vvma5bKjbBdparDVM6w1d3mKtxThLbUVusO96Ugzy7JpPIFd7Rblm36OO/jiVV/i0njz/f+d1PbFT/Ojy+PI58c69+tyPae+wfBzY/zzlMuhWLx9zRVEHfz29c883Fh8jKXYKwB/Om+f9ePx8Zj/6ZC7sN49HwPDZnT/+4tx+5wpwcniCevmg15WL7atnv53bXD6il9PnncsZvro8swt8jjH4YUD3SdD5ivJRp+ZH0KKOvj9X4ZNc8EPE8pq+5NPXepjn+5rV8Rgb7qbWHkvmp3W/1NcXMzWuBPNPFgh19Psx+L3UoXNtHBvDds/MKcR63I8DgH3qkGfz4fjcE2vAya6fubZdFcdRGk+G6cC4cQp877t1bV9Od2U/VvncAa98uC7N3VPXd2pcj/c5n63Vj1m0PuOr46VyNfD244iuSsqqBRIpZhJyfcvlkhQm0eP1nsJZjFaib5wiZVVhk0MrhStLypnoKSNkTIvFcvZGR5yRMNMwsyqH6CmUExbkGBj6Hu8jRS2hyk2jCX4SAitnyTEQxoGMpq5LirImhomxn0O464YYJefZFSIVpY1l7LYCBgtHvSxQSHh9WdUSLr15IKdMXdcSsp4zOSbGaSAGT1UUFLN3USsNWWSXUs7onMkxghJvflkW5BgJc17q0HV0M6nUm9s3pJkIzocoEQGbNa4qRUoqJwmbt5b3H95Tl453uqVlMy9ukmt/8+aWb95vSGjGyfPb7zb8g//pG766HynLAqM1KPHEl0WJD4GqqkTf2jUsbm64ub2lKCpSSjRtg7OOm9s3EjbsLBpYrZZYa0VnOURwVvLZy4K6rgnOEFJi0TaMgxgdRM/ak8jUlQA7lTR51s3JKVJVFdZKOPgu7H67EeK1pBTOuVkHHcZhYnlzQ46BlDWusFR1g/ejGC1CwJYlAJlMVdUkoO+2FIX002iJLMhI6PTQ94RZP71ZLIg57A08Rov82DgMxBjw04S1lqZZkAHnHH3XoRBiMBBywmEYqOqKZrnCTyPWGOq3byTfO2eZNzMjv7GONI37Z2EaB6Y5yuT23Ruijyit2XadkIQVBTF6wEgoe4xE70lZ2Okf7u8x/3/m/nxHkhxb8wR/XGVRVTP3iMy8UwvQGGDm/V9j3qAb6AEG6KrqW/dWRribmaps3OaPI6qmi+hi5u55k0B4mIpQSApJIfmd5TvOUfmKoqSN2hiU0jODeBQyw66T8GfO4KuKt23CWYsyBtd4VAFjDNMwobShbWqmKaCBohTOOkqWvtHF06w3wq3gDCUV8c+PEeUsw+sr2ooAJMREmgLaaiovccafvnxh7DtAMQ69RFXQhqbxjH1PioFhSBLKrUg8+DANrNuK56cnUBqlodt1JAo6z6z//UjRis36iWplcG6SmPJXxcCfS6cb/c8t+xx0l5O/PwNRHzsCPlbq+RHn/Prn0nUrgVsnumt1fkYgsHDG/MFhvVrGuUZmof6flaTM97cqlKOD4uNw9gIAPJg+cx6/WsiPfmbX0TMnp9bzU/uP1n8X5Bx/4TcyKkSIyvvIHYbyakffHoGfuXL91O55oLA90D903xHIOf69VO7R9D/Nu/QR3kV1y/mXirp41yvlHEDd/vePgFm4WHOuFncPaR2371xo8ZnFawEcHvB2eZ/bF2vp0d/H4E/xAcHlowDu+P2OgeW1T+uKoGAJiH7Yt/pW+9TF8D883/aZT15pX9gt4el5ZbcKP/l53AlXsl6Zd9ceu0gn7VWnfXEyoW722IfTw8D76ctXISLKmTFMxFTwVUuOgRAC3nkohdo6rFaUnEgFjJE4vylEcpnDSZGp6pqh2xLnOMZV0+D9ipgiiYm+76i8p/IVKSbGoSflzDT0jGNgt31ltVrjKj/HEAdjvZhyoyja4J2VhUlbsI6p73F1zeygfQgjlVJGIWGVjFIHYjhM5vX1ldXmCYoixMDae4QJG5ROuGpDiqIxs7OPcwxBQFIwaGuYYiJYS06JulnhvCPFiaHv0NowhkhOCe8cKWdSSIxTD8A4DlAytVlhWo9Rc/inGHjabBjHiT/V7yhlqfIL3ltKghAlTjnA//nfv/H/+T/+J92UaZpmDo8WscbirZXQZoBzFqcMddtQ1S2rzVfq2SVg/fSEtY6qqXn++puE88piXp1iwFrDauXwdc1f//Y3ATjThPNezPvH8WCG3DQ11pr5Iy4ipCma3esLaMPT8zMAIUbIhbfXVwlBVtczo7ZmCuJvr0ohRYlxrZTEXZ8NI1itNzPr+MD2bYsxeiYbqwhToHqqiCGglYSXc96LZcZux2q9AZDY8UHIx7bbN0ouVFVNSFnit4fAl99+w1f1HJd7ImfF89evYnq922KsE412SoxdLySCpiHGyNuLvJsQ1/k5Zr34J2hj6bZbfFVL/PKqwlcepQy5RLrv32lWEp7OGEMpmjCNhEFCbMUk4dOmmCgl023fKG2SOPXWkoaJqqmJMUCBYZrQxrD58kRV1fTdDmLENbXETc9F4p2nNL9LR7NeURkjQjWJBMcwTKzXMm+M0hSlxYx8HPCzpYCaTcn1zPTvjIV1CzljvENFiQvu6hZVCq6qGPteQvzJR01JGecsyljqRjF2HeunL0xhQhk3b1hK5kbJc5hBg6uE++H15Ts5F7wzbJ42OKN/aDE9TdcPsA8B8TPh7QNPHPLcOh/eT5ebyucj/d5r8eVJ5Ngk/4fTj++Ph3TlPPeurVTHG7a61CxcTfuMp4eMY5+8Yy/dpbG9Nc7358CVE/FZ4Sdal7ND+1L5xzG876ZrnftoukBIvyh9WjB3exQ+pQG+VtytwT4a6l8hwPlVaQmMfCSdAMCFgtXCt7p0eD8XuF0QVl1p42EZurIeLTXt6tJVzuo6WSduFHhe+HFZZ/kf/m7vFH9RRDkbi1svuVzCezpal65GILhWtOKm4OVYSPXhbjh/4N6H9lkA/Wg66+O9sELBiQn4occf7PqHhC/nQpc75R6nwzw5FpgdV3whKVhA/4/uwWcCp8dbuNCUB9MHWM0hzr6UFCSUUApYqyUUllbkXJhCACeH3pwL09SL+WsuWGvwvmKcAsYUUhhRKuOqhhQDfUoowGqFNwprFCkFQhBT7NoL8HgChq4Thu+Z7VsrmKaRpEWLl8PEVLJMMi3xH60X1m1tNKmU2a9ZgJdxnqqxhBDYbbeMIcyH+wgpUjU1KUW63U66PGchcioK4yx1JffH3UCJEW3Fv3kaE1hDmTWQvilMYy8xw62lf3shDD110wISk3kYR0qM5FJ4ev4q7Z1GtFEopYGCb1aQI0aDUppt9nx/7ahe/yT0b/z5smPImv/9//vv/B//7Q+0dnhnZib3iEJjjZCDOaepake7fp4BZGHz9IxznqpucN7zl7/+C9YZ/KwpLaUcwHCKEn5KxlzMrwsK7yzb7ZuY7c9+9gqo6gqUwlkvccuVwtiZhG4SkCvg1DJNEzqn2eRcQmBpa3n7/o1mtSZME9570VbnzKpdgVKEGPn+x9/xdUM7m0KHEOfDk1hjlGk2EXcCCp9++40QAs5JPPUYA9Z7ckzzyVrR9+If3qw3TMOIc5ZxHGHmEpjGEWZOACj4upawZUgs7jCNmCixzFOKMi9yJteidc95ZpO3hrfXVyrvyTmjlOHb3//Ol99/x1cNlffYL09oLf0aY6Tv+7lvmQnVMlUlfe0rL0SCpdAPO6lDQXgTQj/vK7TiEKJt6Hu2L2947/jzjz+omgZdCtZbNBpKwVcerQwhjowpY5SSuN91NUcbMMQ8UXmHwtJuNoBinKbZh38UX3drRVvuPc45uq4TIsKYRHBTCijN2O3o+kEsY7wQ4TVNwzQM1HWNXq3EiiQlxnGk8o7drsM6h29aUpiAgspQP62FJHKYUAqMdQzT9Ikl9PH0sw66l+UolCoiHDna7cpPqvExf/TL9JnD/WMg8QMN+GQ638oXiz5/waOHfoZC5Vz8cfrc6ZhcA+Ll6Imlcm7Xqy7xeLnMe3OcHxB+/HTIfP6qNwfxwbKWrp93+mfSh5+/D0aW6NROrlwO67vwYhG9/RNA9Ef76Wy+HWscj9//uKhLRujb1Zz3xr0mHUD38f+vAPRr5S0JAhZ/n4Pbqw26Xv6NbA+l47XzHB9d4KWFdtwSUpw8rs7+LkcCv0NpZxXdA8bqdF78lPTTF7iFdGXy7IH2oSnnn/Xxt/CoVPfmBHy0jbc/6KtCpKX99moxH1tclbol9L/XKR9LDwPvXArDMIpmzjms9cQwMo0S+zbELObLM0GWMVq0bd2OkuHLb78Rwog2hoxCay1xtmfAYk0lHFUK8a22mhSzxNmNAnagYJSYSDfrtZBrTRNhGrAzO3OMAa1F+6yNZZpGVCqkEDH7EF0xMnQDvq6Zhg6tFKvNehYOOKZxIIeJZrOh2rQobUhToFm1hGkOJ2U0Q9dhnac2jZivFyiliCmvlTjYXfdCZVcYrTGVhDXKaSJlaJuG1dMzVd2ilEJrc9Aeu6YWs14nft+qJDSGcQo0lcco0FWFrj0piJ/2S/s3/q9/7fi3/+v/5v/3byP/+r9eeNkNtG1LQXzWldL4WWuf4kQqYka+Xm9o1s98/e0vGF+h0dRtg29aNusNzlth6dazH6xQYtPtOpzTxGnC1xVVVaG0gXESX2jvqOoGbQ3OiEm6kHdluqETrbtzjH1PCIHt6xvlS+b569c5lJWwkKeZwKxuanY7mQ9Ga5jHtMRMKns2fPHXdbPf/T6UnZiuJ4bXHqU11lpiiDQrseaIo4S2UkYLKZ8xpJnsTCtFXTdUVY2zjpiF8yBME9E76DpSKVTe03U7hmFg1QjTOCkTQ8Q6i3MStzulxNv3N37721/ZW1OEEEgxEKMImuws5ABFu15R5pjxenaTKEUA7jQFhnHAao1zFcZaCuKCIf748o2NU0ApqJyTCAM5s9ps6Lc7mKMIDLtO/KKdaL6NEb/4qR+JStaA9Zdn8QtHTFKd9zggjBPr1YpMIdXCfUAWtwBXVaQkMY211nS7DqU1bg4/6L0nhUiMSQjPYjjE5y7zburrWnzfk/hye+/EH1/D2+sL7fqJVGAaJrAOpRS+rtGIoM56cU/IKTJNkbpuCcYRx1FM11P8ocX0WlrCZxyu3T+JHYhtbqz1ZfGUsHR4fgzm/Vjav9lnynzkZL3P9yMb4e16PqqkOD3w3X/2WF5+MTfKx44Mnz0CqJO/1WKbHsU5P/VseafyIxL5SwHIz8CI1+q/B+RvlncuOLks9lYbzr+l9wN0WSz/Vn2L6R8BDo7TCSL4ZOVLj3+wqKVpoxYuXMgl9hkfANQ3r589ty/y6mvcW+a5BbqPKluo9zgHhQuN6FK/3GrmyZy7B6Tn+2r/9IPjeN7tiiPAXI768xFBxNI8miu4EM58dMp+5tkHhA9LhZ/0uzr61I4m1/F+dY6DT6o9F7yeNekhy6Zr77Ekmbn3zHnWqwj9OOuxCmIh3w25zGmDzhv3+c3mA+RqGVUyzgt5WowRa534dY8TVisCzGzWmnGc0FpRNysoCAs3YqoKc1xv5XGVgK7JWEBMuKOCqqmxTt5Na9GYqhmYeusJKZEyEje6bsgFjNXoAv0wkJKGURjQnXcMfY+LnmIscRrJYcJ6J4zSSjNNowCtHHFVTQEq50m5QE7ESfLvwVicMqUIEBTz4kyYJpy1oCzeVkxRGKuncaAoiEFMtmO0qBDo+x6lCikmfFVRchHfW6XQxqIVpJzFnL6pef32J0VrQiqkHFAh0LStaCGV4utf/4WmXdE+feXpb/+Jv/2P/85/+2//g+/fX5hSni0QRBNpnaOqPM571s9fef7yO3XdsHp6wvt6JsSLNHVN3TQSQg0FJTP2PeunZ9rVmpSCEG5Z0YKnlCGlOfa5sLfnIgReKSWUMsSYmIYBX1XEmNi+vlLXFdoYfvvLb/z59z9IMVJVFXZPymYtvvKkJLGsxbRa4p2XkqnbVsJVFQltpYv416dcGLqet5fvtJs1vqppVu3hAxr6nrHvBOway+71hXazZioiaMjTJMDQOYwWYRJKEfoe7/3B/1lrQ57n2zQOlAK7nIRc0GimoQcECKYp4NuWf/mv/3U2T49zLPcGV1dY59i9vVG1K0BCyqEq1k/PFIrEeNeWKUz0Q0+JUQRe+zFraoYwYa0hhoR1lunIHUIhOpGYEn3X0XUdajSz28BEIWNtI6HSKAfyvTRFrNWMu04At7NsX19RWrHePNGu12hlKHEiTEEWGOdRzrPrBnJJWCMEjbIeMDO2C8mZ1sL4v+skzvpqvaFu1wzdFqUkn6scYSxUWgRr3lc4X+GcJ5UsXAMlYmJmN+5QyvD1999oV8/EmMmziwlA1wmpX4oJnEb76tML6Xta3jHuYoOLHW0hz48LWj+Zfv6p/D/sVX4wnfTED7zAtfd/vMjrJ/klqf0F4dZCA5bNL/elqbMrRzmOD6XXpEwLz94EnD+aboGxj9RxT3Z178D5aPogBl3MvlDfY01QV1atX5w+U+keODzw6EEecSzA5P13OR/H/fXyfum4zov6bn3AHwAiZf7nKjg8F+qdz8njb+4j/bkkqzmq65hY7nxpeKSaq20+T0cb44WA4wwwcn59oRjFHhCehVlbmjRHY3jMEL4fk/3zSwKI9/zXhF9cMHovgtolsPuRDj9fx6+dJY7yXQXLR/Nw/+6PyEwOxZ239+qhR12ft59djC7KkgoeXoav9vWP7dRL6WHgrWfQEWMClbFGNGrTOEhYLQVN087+n4VxGHDOEAsYxNQ1p8QwCUFVCQHvK5r1Gqs0/XaLsQbtLQpNmAIhRjH1LoWEZrVekaz4hWqbCSFQzTG38xTQzqFI5DiSQczT65oURUuNVlTe4nUmqIz3VgjLrEUpIRpT2uCMon56ph9Em69nMi9tLKpIWQcT2AJGG2G4VqLJz7kQcxI272pFqCuGvkc5IdHKueCrClNLjHNhaCwM3Y7qt99JSnxNlRZz9e3LC76uKUis7akkDJmiFHqcsEZYxrV1uHbDf/7f/t88//4v/Paf/jf++l/+O//+P/4bf3z7TtePFBTjOFIwWGtZb57ZfPlK066oqxpfVThnWa3WFOb45tPI02aDdRa0YbNZz2a+FTEJAZp1nhgmxkEEHUqJwCSGxDAMxGlCW0fTtmy/v0jcauvQRlNSoqolJnNdVfw//ktNjBL6TcKmKYSBvvD2+kKKwso9jiPj8CLkawW6acJWHmccb2+v5JwkdJsTIcbQ9zhfgdJQhPm+qhvMEcCsak8MQQRNWhNDZAoTY99TNQ3jMNB3HW3TYtqaDKRh5OnLM1opdm9vuKrC7vkGqop+BvZKiVuG0oo0TbimBm1wTvGXf/lPKETQknMSy5CceBsGSgyYcZpZ8mHY7TDW0awacgyoWUhjnZMwezHMvuIOiOy2O3zlGMOEblsUmW4nIdjqpkGVDBmcaajWDucrtLV02zf6bkCpAmjWz89i1h/CwQqjXa+pqopSEmGcpP0pzdEEZAULU2Dsd1hX0Q87YkrEGGiqhhzFvHsaR9rVk7RdF1y7whmDQrTyWinCOJJSwXlHzhE9kxWGWWBFkvmTQpCQghSs1oz9QEGEWd1uxzh0rDfP9LsdxhpyCJQcyeMPHD2vHASWSzxe4c9ONkvPnhV0Cwtcbgd3oc4PpPPaLsTQDz/5eLqGen7euz1+5rnOpH1tPM5H/tF+WM53GxSf+2OfIJBrZ4m5YeL3Wm6eAcvRcycH5nMp07XD7q0hu3HvJLTPcfYrB8mLMu9NlR+ZStfe7Wws1JXr1w6iJ/lPHjnNeDkjHkv/MNB9NMceXScu1rsHP5rzPlu6f/69HFtTnIzXNTBzK92aC0dZ7gL887zHDz06cOeI9kafnPw4Ap/7a9c+M3V+AWB2gVJLUoXy/tzi87cA83z9sJSd/f3+CkeDsFTG0TjvhYdlvqbmvy+0w/v7ZQHWXftuz1/j2txYAKqLr3/ccUtniKX5ubBdHspeGIT9kJ0T0x2qKEdNKGdChqVG7zOfz9/za/uyr43XnXS8G743XR2adfLHxYT/YHpkL1tIDwNv572Y5sZAmCZUVZFjwFlN3dSixZwC3W4LJQt7eM7oGGcgIGGq0jCQYpxjBmuG3Q7rHVXb0tQVSmtQEi6rqUWbTFGzyXAkJtGYUxIKJfGDcxZzdWdJUYlmMKXZTFZMkI1S5CJaX1e12KoR7XoWDW2zWkm4qmEgjkIKZvcaToSt3WhDvWlRGqZhJEWJ/7x5+kLKEY2YBVtdiHNcbFc5lLbkAmhL3+3mRcgglvXlYPr99fe/EnNGIRYCxTpCDNRNQ1Ya4zx56KkbCX1V1bWAsZQoOTL1HQWom4anr38hpMJ/cTXN6om/DRIDuRTF63eJ+12Uxjoh4frt99+pm5Z2tRKSOWMYx5H1ekMIk8Qdn1nbjXbCcu09KgYB1UrRd0IWV8Y0h0VzUCSmt7VWGLTHEV9Vs5uA+Eib2e+8riuJgW0sZmYjL2NPSoVut6NpJFSVNYVhGtA6Y7VGoYWoywi5WNZaYqBPonl3Vct684RxhrppiCFg7AzGhx7XtDhfk2IiTkIcNgwDta9xTc0wCCFaGMVq47fff5+tMBwxpZnsbmbKbxq2Ly+UKqPrRgjWcsY4hyqFZmZrjzEeNvlSMs5Xook9+LMr4hw6bf31N0D80LPiQFgXs2i/x2GEksViwnuK0vzb//0/sMawfv4isct3O6z3IsyaNwytxZ1jNcdnn8YBlUS4ZrxHG/Efd07izY9joKobrJfdyFhLzolxisQQSSlgjSYnaX8BvPcYZYhVTU7Ci5B3O5q2JQwjz1++gNHzNw0xTIRJYnTXbYuxFudEQDVNE21V0a5WpCT+8qVkvDEyPkWsb6ahY/O0pmnXDEPPn//6r6i3NzabDSkMIhgiUdeOYZgYwsTGtdQ/ovG+d8qTkV6+dOWRRwS/t0Dij8lk76RHGvcfmj65I54/fdahN4HtwsFv34LFsZgPKvcYdpfEG9cA/f6IUY7KPc5/8IU8KutmZVcuXwXhtxq5VN+HM9xIjwz5rTxLIP04/z0BwsX7Hp+eH/zIPwoq9z/Onivzv+rw72fTL11F7qbFZXVBEHEtLS5T6vTPc2B1FdwvzY9bec6vLTxzAhzvtffW3L22KBwLEw5llAPx6Hne83KW1p7zdBJybaGPbg3RCejb/35kyu3Xsf0Zav+POrm9WOaST/+V4t+fP07HQLQsPHBcyLWfC/dOxv54ji4A0/PnD7fvAe5bt85e+p771NX5elbwtfdarHd/6aNL1pWG3F1aHzomXF3gP50eBt7DzM7MbPZtjSFl0RSOo8RqziXNRE2eohQFhfNOfJbnWMTOi/+oUkU0mmHCVwK0coESJXRS3a5E0Ymm696EpCpHhq7DNw3GWIZ+wBhNzFB5MSEPMQnRl4FYykySJURlzhiJ+9t1aK0l3FUGSmYcJzGVnqaD+bv3Yr5qjZP42ynhSiYGMQ9OQcDArpeQTv1uJyRRM9kVRTR5uRSck/bpWXhRcj5ct0biJE8xkuZ7WmsKopHdbbeklNBagCUzy7O1hhImrHUUXaG0FSCoDdM0UbdrfNWAtnTdbtbqR9rVCoxFI4zdrhKzYtmEspCFofC+Es28MeQYKd5LG2LAlELfbYkxEqaAr2vWmw3aGIw2dLsdoOj7TjTlSnxxu+0rzXqDdY7Vak3XbanbRkJpocgpkwuoGZiXovC1gK2xH0TDO/s4a60p1oAWUIrSOF8xjiPGWOovLd4J2djqeX6HHGd2cysa7iKrd5j9n/fzZZom4Rfoe56enwgxYrTG+wrrPdMwkFNk7Ad2XU/lLFVdo60T4YUx+MozTZOQyikDqtD3HUYLUNy9fSOljKsqCordbPUh/sYJYw2VFWGNdg4tkiK0sYQ0Mg0DxhiatkVpJWzr48QwDjOXQmH7+sL66ZlcYOhlXEOMTMNESSIAywl87dg0Xxi7njANTP2A2s/BqqI2sPvj3/jz5V/xX/4Tddsy9SPjNJKnScLQVR6lLSkkShH3iZgzaRpJKZGjsPcbYygKms2GAuSUiCEKCZ1SWCNCju7tFeNrjLPEYSLlQPj2jThNoBRKK8I4YasKnQrbty2Kwm9/+QvjMACaMQhBXhh64mzRohASOHTCKyNm6jmQZ76KH0vvot8bZ5H3a49sMAub7P7yx7aCHwOjPyddO73+o9r2QXR2q4M/wL6zmLNcjuX5/Lh3/r+W51orrh6mFibrkt705hvvX+AKAHiokEdO+gv5TqpYase9cm58Y5cV3Ekf/TCPyz5ITc7KuVF3me+rw9iWy/nzz/Dp79NDbXkXHZxPnaIeH45FAHY819VZkx4AZeffyrGJ8lVAtP+9B5hH73D8nDp/+PwlrhW8UPc+3+Hxo3e7+F4Wilb7rewBkHrr7oUmdOGhS+ucB5I6GlOO3ukctJ5fOyvj/Fvbl6GW8hy/w2Vpy+Py4IZ90vbjZx5Avydz+7g/Pvq9L/XTtTLO+v9e3ot0rS8+0uZba8mRZddFVRdjcb4x3mvEZybse/oQuZouCYVFaYXRCl1VEmpJG+Jsah3DiK9qFEh83Zzx3pNTFBPVKIDCe/H9RWusdVhj6HsBEtZaAVLWEuOE8w5nLNoYbFXTNi3TKHHA02yynHOmZDnU55TRGkpKdLtOTNjnL9sYQwhBzNW1loVdK16+f+fpyxdWm42ESlIDuWTImqIzVd2AEmbqbvuGccLQ7n1NyknYt62Bokil0O96MmBUoaprqtUa7zzjMFBVtbSziCx6GAZQ4LXBOk/f7XBaC8t1EtN2X3nqqoanDSEmCpmUmUOZWYnhPLNn77qOEAIlC9nY+ukL7fqJse9BK1ZPEp97mkamoafvB3IpmFm72bbtgbxsz0YOvIfjSnY2j88464lTYhhG6qqCUkh5Dhc3DijKIRZ2Ton15kmsGkohpkS7WjMOIylnhmFAIb7H1grZlzEGY0SznIqMcQgTvq55fv4KJdPtdnMsZ0WIiW63o6pr0Ip+11HX0t9VVaG1YpwC0zhBgZyimJCPo2jlgdV6/e4OoPVsulwfwt5N4ziHyXOsrJtB9mzabD3BKMZhIL5NtJu1sLcDRmu28RU3xxS3xuK9mUnBMlXlZ2JAIR9rVyuMtQxdBzlTz1YZaDUTl2W0NlSrFf2uZ+hH1PxNbdabeb5GCoVpGLFVhQI00GzWkJIIBWbuhBgC7WZFjvKuOeeZO8FQciCnUcKMl8K//8//SdOuGLsd2mhWqxZfN4QpkMIkZvpTACOWKXuBHCicryg5oJQAe6c0JWcRBFhLuxYWeuvcQUhlvCV0AV9ZYpQY5mkSs/2pl1CDYZpo6wptLHVrKTlTe4f78oWcN2KhECNp5jtwzjENEzkl2nb181hMD+l8V1jauZd3jkeacmlItVz/Ujiq+229keWQ7WPA85Ft6nPb2WXbz3vkowzvn5oKdzQEJ4WX0z9PznVnnaBOxu46gr0AKO/NWmzCo2np3PjImf9uYT8hnYcSunsGvHeI3UtDlhaDa2jvTIByU6q2MMGPTVtPHj+eT1fqPi3qvQR1+Pes+R8RIJyV+VPSj51X39PCQJ+09Oz+4b0XXkfBhS/uRTpBz5e3Cryb3u7bc6XM893gZp3XBFGPjOHRu54CW3UKSM9B8PHfjy7aR+kc8C0C3YXnFue3eqANZ+N/ek8d2nToh/PMj/TpUv4lMP4IXnsk3QKq9/rtbD35dFLv1T78bVy7fSYUOMy7j64FDwgxzrupLD70md45HuxyeumD6WHg3bYroAh5UQiUJICvlMI0ih+w857Vao0yAphiTJSU6GPEaEMKI7mA1RXaOkpMOCOhp6ZJQv0YawR4WsPb2w49x9XWRrS5VVUzjBM5RbSGqmqJQbSKOWmmENnttqQUDzGmBTiIKXzKEsaMmcAKrQmjAL4cV4yod3IzbZhCECbyUqgqz24OdzV1Aev8TMiWmYYRYw2uqpmGAUWirmqM0biqJpdCKWLe65wX39I5FrTRmrppCEE0+kaLWbZokycBoIiQYb1q8Smz274xdVtKgS5nIfkqSeIsx8g4a0PLNOKbZtaiK1zdUKtWfM2L+BSvZ0bpNGvhYwhorWlXawHHbcs4DEzjiEJRtQ277Y5pHMUvva5RJdN1W0qSfm5WMl9iiCgKL9++sXp6ol6tUVoYsAXYiR/0OI102y2711ea9Yq2XZFVwViI4yD5EWKsVdvifCVCncrjQhC28qFnt32l5MRmsyHM/ubWzfN0DrEVQ2AYBsI4UtWVCEiMZvu2xc0Eeu16zdD1lJzIKVKA9dOTWAOUIjGtw4D3NU3bEGLCGisCFF/RbXfCaxAzpjISPiwm9MysDmCrClD0QyfCnr7HGGEUzCkxDiP1ylA1LUPf8fryXfq/aWlWK3IuhHEghcg4idXJNPT4umHsO9TMYt/6iqfnDSllpikwDT2uFhBuvKcEsSLwlZN5l8MBsOcM4ziijMX+9v9k7HbCMJ8S49CjlXAgjGNgHCNKK1zlpZwQCONAP/R456nqGqMNU5iI48j6aUO33TKFCY2ibhuccwcm/wzUbcvb91exeLEahWK7k3nvrKGuG4rSeCfCA+Ms6/WaYRwPceDFCiDw+1/XEgVh7KmqSr7tSb6TaRpxVfO5VXSfzg93c1oGQZ/fFq/tOde2lNO9Tp1c+3DFH8x+vR51so3xI+36UFtuv4Q6yXPpM7vUt5/CMVfSrXHdP37tLP5Q9ddw1PHcvQA1x8hS3Rik8p7n7NLFz3sKhocOsPPoHLMBHT/7s0DeUrpR7s2j3b0BfqTOM1BTFn98HBP86nT/W9kLmK5/bxfXrk+19yl9TfJ01DEnAo5H0zVweVb/ySMfHYSzjljsw3MAeAu07bPcyn8uZHhgzVrCpVfTB/pA3ct/a3IfNUwtXFv8/WA6CPvKA208uffJr/FXrWPX6roztx8uQsGpmdXRh1euVPLRMbmWdy5HnQlGL7pSLd35tR3+MPD+13/9H3z9/W8YY3CVEdPVcTj4p+akJB5uVTH0g5iLNy0pR6Yp4bxl9fREGHpKiYyDMFKP40A9hzJqq0pI3NqWYejRxtBUjZih50SKgaHf0qw2KCWArKoqnBe26xADr9+/kWJg8+UrddNAgRAjTdOgrccazdT3lKKp25qh2+GdhJ8ahx6tLUprhn4gJQnr1K5aUIpp6AXIrFZ0nZhQl5QY+45pmGjXrWhQ64r1ukUBfbfFaoW1BpCwW7u3F7SxVN7PYcZqYkqzaXVBGz3HpS74WsivpnGgahre3ra4qmL99ETfSVgtbR1oTSlZTNtnbbUC8RFXa/y6od8Vxr6b4zlLHOmqbiglY2o1k4/VEns9JXa7LW3TEsLEerNh6HtSyoxdjyqiyTdGizVBKWjrwYrnuphMD1hraZqGEAJxFjKAkPV5b+j7+Z1RrFYrKFmA0DigULiZHE8rRQ6J1WbDerOZxzWwfXmh7/uZtC6hFXz5y9+wdYWva3Iu9P0gsebDxGq9ZppGvPdUlZ/9kWH3tkUZTYqRlOIcO15Tr1YSW3q3k3BfKYnJeV1ThoHt6wt5Nk9frTYoJZr8569fZsFK4PXlFdO9UVxFF4V4zFiDMZ4YJyjCkN+9bbFeCOh8XYs7QhKTbWstpUhfC3GZCAuGVHCVZvP0JDHtZ0uBPk4yB7RBUdDWkskYEqv1avZxV2jnmdI4m4krrHfklCTcWBLze1e5OdTfTGJWCtYZnLesNxsB59NwsCrxzktoNWPE8qAk8dc24oKglMI6YTL3sxWCjLGWNhhLVKLVj1PAeU+OkafNM8M40aw2hKHHzgzzYjGjxD8+ZV5eX7HW0L++gpb44NO3N/pdjfeeuq5FaOMcbNaUGClaMey2n15Il6X813aEDx1TPrxXv+9bp/Xc3kp+/rH8WMi9JHAoF7nl/vkefLuG0zL3189B9p7J/94Z/GZXnxVw/MwiGLjW+HJ6e+nZYw1VOQeWD6aLd1iYdgdN0L1y1NnfJ235QMPu9c1n0zUFxEMA/vLCzccW2v7hr+eoD+/JHG5//kd67cNB9j8aYt9IDxyq77b+ZNJeyX104L4Kds/m4qLJ+LUF40Z5x4/ee92PYIyH8l2TVCwJZMrROvPIVnUjnQCtf2S6AboeWgs+2d5re8nn0o0v/LOV/Og4fGRi3jufLB13bj1zC0jv/zxeO2+19WSNlYyHsVsUiDyefkSg+bipeUwM2xdW6w2Z2aeWQl3XUArFWJyv6McBpfQcesyhKLOJaianjPU1MQtRdSkZ44Ql2VqDaVtyjBLCCyVm3KVgkRjMxlrsKIDBeI+1X8S/eY4dTlFsvjyTYqJpVpQCMUsb1RwM92234/XllfXzMzGKmbvzllwUaiYVU0pBSTw9bd4JtLRhmmOJTzGgZgI4rYSAbfP8BFrTNg1aI6CirmnWz3N55SCZ19rM2k0jwBdmbaH4wmtrSIOAQ60hxMLm+Vk04CEIK7QVlvaUEyUqYhFCq/3zYZyom1ZCZxVhQ6+bFjdbD0zTSE6R9fqJlAK77RvGGLQqNM2Kqq4pBVKOhHFk1IYwBbTR5AwxRuqmZbNez/7sBaU0/TDyvNoABWstOSYKhbppUUpcFszMUF6ywIFcMm9vb7TrDVXVzBYTlhSDWAQoTdW0MxmYEHnllBjHgUKhqmpyEQb0L//yn8mqoJFwYkYbptwzzX79YRolPJpzFMA5j1IKVRS+EZBvrYTJEr/kQB8jVSPkayEEstZi6q2UxNq2hjhNVHXNNA6kEBj7gdXTE0ZreaYkQpwwxlMQU+dheBNwX1USFssZpjGIL7ISS4D3MHNW3DqMxL0O44QyBuPEFL1uW4ZeNLkAv/3+V1ASv3qaJtSkqJoG5yz9bourWiQOt8ZYQ0wFUiKE2WXDGZQ1VN4D8Pb2Rt+JSbfzFb6qqVvRNscw0DYNylqMNrISpSycCaXg65q6XQPg59jk2ojmP4aANo56/QQ5k1Nku9vNPveGnCIoI4z2/UAYBrquZ9XWFCPzTdHjnD/E7O4HET41qxVKK9CG569fyTkRAlgrYfQ0hWQcY0i09Zp2bT6xhEpaOiw/cAa6mhb94uYC3k1rT5H+sWT3R9Pj4Pd2egw4f6zOpT6816/37i2fs9X1vfm4wmt/H+fdP39exlm5S9o5dZxHLeR5eFJdadfddAr8l0LkAPM+d1b2meTl8Nyj6PKuJORG+sjJ6DjvuWPotXIWOuGhKk8G9Cj3yTzaf8wLgoCTxeX4oaXJdTY9/gMx+f2qj3KcoOCjdI+J8Djr/PixX/W9hWDJIuQRDHKS57i+Gw9cHv4fSEvf8Pl7LX6cp/Uet/Wumf21so5+PiJk+OF0vlDfEho8AN5O8l1Z226l+4KGow57KOD1ciXHLgw/3L/3+o0b1y8KOsp81o+nc/+swHL2t1r4eWchfQfS1zO9F326Rr5X/yO9+WOil4eB92+/f6WkKFptbVB1w2otmudxHNFWAFqaJqo5lNHQ9WgtsYu11hLrePbF3vU97WpNGEcxk/aeOAXGcZxDEmXQBXJiGBLeV6BFw0XOMJtjg5i/l1zE/FwpMZGOkRwiRkn4LGsMQ9/TDQPKKFKYMI3ETQ7TSNf1c7uVaLXXQhTGTL5lnWXz/IVxHJn6gaI0bduglEZbSymZ73/+iVbQrjdYX82xqwUoVt5JvO+UsM7jKs80jMSY8N6jEA1mP/RUvmLsB4y1qCxhlAYFKE3Oicp50dCPA9b5OZaypqlrfF2jtJ7PDgVrxYc3jqN8xEoJAZoxrNdrQhBGbGs1TbsmRdH27sOhpQRV21KyxMd+e32h3YcWY2bHVKKBncbhAIadk7js3ntCDIQwUTU1Tml22y1lFkR8/+MP1k/PVHXN9u0Nyhz7Wmusr+h3HdV6TYziZx3VJJYHsza1226xTov1gDEHcrt+t6PM2mJjDO1qRc4JBRLvO4vp/5B6vKuw3pFiZBpHUk74pqHfbkGJmf80TkzDQMoFU1Wg1Kyh1VSupnJOWPyNnrXZhjQFckpiZt5sJBZ6Ed/xmBJVXREmEbjkIgIPbcSUve8FwL/++ae4NDjDFCIqic90TplxuyPmKObWIIKctmXse/744w+enjakAiWJifs09CgrTP9//PF3UkzU7YoSZ822l7lijCLGIuA4ZSEY1AYqYV433pNmvgaUxtXy3Uh8bE2KgXEKOK0Zx5GmXdFthZxRW8/Qdyg1sdtuUcZQV5WA9Cykhc5Y+r6joDDWsn39PvvbF6Z+x6quaNqaXBTsXT+GHq3UIVJC09RQ1zhfMw49tq7JQRj4wzQRJ0sGnKvIWULNGec/vZA+mj61VN9Y4w8b0AHYXG4m52fJW5jxV6al8+G1/rh17729y7nUWc5yUeL1Nz1/dn916cmTw8WNQ8wxVjjGS3f7+9bB6Pzwci/PB6s4KXsua/G8tD8RXmsH/6D59Zlz0N2G3Tqd/sBb7YtdAHlLaXHmXoBvufiP+pY/k5YP1Z+Ujixkv1hfji4cd1Xh8voNLLB87RiwLj2w9DplIcsjr39LDnT2XV4Us7Do7p9Tx78fqftGxiUSs8Vy7uVZEhocP3OU58SC+cqnWo7btpTtCn48FowsNvejH9hJweeV3Sjs6B1/yjd9D3QvNeFcIHXegSeBv7m/Dt+QSC2yxN9aLx9e9+996fcKuuy4HxmPh4F3VdWYmZxor0ncvm2x3gvbt7XE3FFEjQlKY71l6js2zytCCLy+vGCcRylYrzdCCIaEv1LaME4TOYsWrGlb8dsuhZIyw8w4LAzeCBlT3purjyil51jbhhiC+DhbC1pjipJykNBWTgtTtjZaTGGBlbEYbfHekYv4Q+fCgYRJARhD1TQzcdgoIMQwmwEXvv72GzFEvn/7cw4PlTBGS5zvnIk5Y51FGUVdN2itmcZJzGuV4e3lm2idjaGQ2b294pyf+8IzjiPeV5SSCSHOfrRRwopZd2AHTznP+Yr4yqYg2vGUCONIXdfopiWlRClCOEfODOMoJshGQrNZK++tlKaYgraGJ/MVX1UHk/YUCykm+r5nGgeMNXz/4+88PX+hbmrQGm8MZfaLftu9oq2FGEFp1k9POOvoh462kVBbvqoOi976eSZjm03xldJzzO+MNZaqEVN55zzTJCRrikJdV3MIszluuS3UVS3EerOJ8jgOvH1/QRsRtCilGfseHfbEeZ4QIt1uSwgTddvQNitQCqsdzjtevn1HaU3btqRhxO211EXI1IpSECbaVSvx3ZMQ0ZUiJtnOyeerlSKkJNcqR900GGPYbd8oIPPFiIBpu32T0HZtAxSmEIQ4DwEjJWfquqLbbWnaNc3zRqwEYpCwds5jnafbvpGjaOfbVcP37y9iWaIERK++PNNtdzjvCdNECoHV04ahH8T8uxRUyoSYMHVNSpHt6zfybKqfc8YYxTD0WK0lJNxqLe4RRlNyRJEJY4aSAMVuK5YPOYomXFMgTehi8cbgNk9gLK5qGbotJQZs7dGV1B/GEVfV7EMS7hd5pYQYUBshMIwxkMeJcZrEhSBMmDD9wFJ6mj6/KF8RvR/9fU4odfTUvVI/0oKLDf/z8t3lct//fry3zvfjPfxerOeahGGhzMun76R7GoOPdNY1ZHV88ls62N894CyUdZzODzSPlr0EIG5euHL9kUP9o1PjmvDh0fQo7jtv69n82nfd+Rn04hsq6l1wsQjS7jXo+te0ePdhdPXr0uF9j5HvSRuOemnOvIjRD7mXhWMXpZ691qEdvIM3BQe2dI7uXxz+j8s5uqZOm3LZiOUizrPcBrALL3jRi0tA8dZ3t3Dr5PL5XH/0Ozl/dh73u/j7HAsttqXMy6J6aOqeLKNL4PBkcp0Cq3KvwWfT9jTflUY9ur5+CnE/MLgfKe8B4cbFvUeWrkf2zrt7grraj9ebees0c21gPjUQN9PDwLsUCCHAbNatVabvd6S3LFo/75iGgWmc0MZhvcKqIozk3hNjwjnL7vXv+GYNVYUqhdVqJUzeQ4dWWnyGTYU1Eu4pzmbdKWW67ZsQSzU1MUoYrW63o6RMJoF1WC9xqY21jP1ABCpjJOSQVvjKo4oAU2stXdfJO4XIlCZ2OxEiVFUFSotWzBiJu4wAeFWymPXWFbooSkqHCR1TQlHYfv+DagYnT8+/YYwjDePsg2t4e3sjxzSbwmqMUrTtSjTGWUJGWa3IQIyJlEUjb609DMi3P/+cSdBWeDWShjeCrpjGCT9rL8M0CrumtnjvaZoGpTUhTAydhJ1SlcI7h5lB6d6EPkyT+NKHICG7piC+1rNAYRwnXl+/E6YJFBLru2rEdFtDShGSQu+J5MYR7xyubsgpsHvb4qtKNNEl472dfc8zVVMT51Bp1lp227eZaM/AzJid5wCSIUSKmti9vc0WGcICb4yQdGmlyCimIIKecRglJJnz/PbXv/H6/U/CNFLVDVXd8Pb6HUrBzrHr15vNHGZMmL6nacJYh3ESXi+GCVgJE7d1GGPna0XMtmdf/lwiORemceTbv/13nn77G8pYqtqTi8QV33YjbnRUdcM09QjrvmG73ZFLYRwGdm8vOF9J/QpKSuQkAoeu28mGpBSg6bZv9H1P3YoVgWibFUVbqrohxEgumTEEKIkcC64Wc/9h+zabsTdYLxYJRSkJeacgjSPjMLB+fhbfbURI5ZsW7yPDOMr7Dj3JOVSB3W7HNAwzGWKiqSu0r2iblt12S9tUeKvB2ZnRNNO2DbZy+KohxsSf377z8u2Vr1+/4Co/j5Oj7wJpHMjWkJKRcIRK0Q+DhHAzGj0TQoaYGPodKGFZTyExpfjTFtaTtfPhu3cW9qMN6fQ8dHsnv7/H7/9dqP/sLPyj4Htf2x5wX8PFx+eDcvLkcaOuNnXxOmp/kJpbcHQYXCxxfxrf37+1Z19rDJcH/xNk9iNpf+4oZ+271p7ygUqvTYVDHz72zPvlD8zzX53ujt81ZH2W5whILLkInLDT3/p4zgUQC/nOz5oXPVjOHzv+si/7e2kqn1b9i8H4p4qdZ5I6Bb330knWBYnI4f7RsnD80NLne2Ht8oE1YbE9+/+dDUzZb+VL7dmvyUf1XwDLpT66ASBvdum155Z+X6lXLeV7FBAujfmdZy7WxUcx1L6vH/kMjts/r+2XApCzAg6T7Pje2dd4d47fWFPPn/3sxn21/qMb5WiyHm7d6LiH7l/+PtG832njUteddsF5n3/0dPNjJ6HHfbxzwvmKoduJhssKOAAJsSWET4pqtRL/z1ricmvg9fWVnDLtaoNzBmVEWzjlTEyJ8e0VSqZdrSgoUsz040S320qon9UKV9W0paCMAQrOexmFhJBolfd2WmOo65oSA4RIjAFthYAq5Szs6kkYsnNMpByJUUjSrHFi1mqtgJ6cQIs5+dB1FDQpjGgUTdPKwb7rACGYstpSrxpq7w5xg19fX2citowxNUPfzT7SDWM/oPfvNYdWU8Bf/vY3mBnjxZ95xDlHCJk4BWKM4gPsxUe5KM00DZiqwjpLmibUHLYtpYSrPP12i/UVOSVSTqQcmMaBkgLBuQMLeCkCTmMKVFXNOLN/V01NCsJ4LSbJBu28tCtG8aGdBipfYZ343vfdjnbzBEXGxThPt9sKAd1mI/G6RwFi1otLQklZWNZjouTM1Hfi86sVOWccEuPbWgHWpcDbt2/4usI7iZ2dkyYlsTTouo7ddsfTl+c5xraEqRNwrKibFTHKe4VpYvP0TN3UTFOglELlK7rdDqMVT1+/MHS9hKWKE89P4nvf7bYUFOMwEXMizu2z87hGrQhTkBBhxtA0/6+ZQC+hlaZuav789/8pobbm9ys58/b6grUeX3s0ispb1GpFKgJbrHWoPU+Cc4RpxGgJn5eyhAtzVszeQxHh19D3rDcVGYMaIRiFKoW2XUnIL6NxVlwEVps1RgkhXtqIcKFtG7yvSHU9WxVomqYB8szIr/C+kX4OE1bD0A/EnKmaFj+7Kaw2G4kprzUZhfUeo2u0czTtWsgCQ2S1WWONCMm2b29Yo9ClULIQsnVvO+rVirpd06zWxDnyQoyR3W4n34gWED50HW9vb9SV4+lZ1oNSoKprdtvdDy2m+3TY7G9IY8viryNIenvXeAddh3s//4Bc4MTV9GS/XMx9aNrhr1tY45Fz1nFa8tFc3oel3suY1Qp1Znd43PP7Z++mR7v6I9jlIs+dU3C5/Hl6nllAc492+NLB/VyQ8DNw9NLZ55PpPCTXh+o/Lekywwfbdeiec4HIPWBzUcDl9ff5qhbB/nI67eBrMPxnCNXuteIdMD/SE8eiQHU6HA+WoViQN10B1RelXVmkzoE5HGGO0+VFrp3VdSJuvDPFbq65S1juuG2/ekDPKt7388Nz/PjvDwkiH0Rad++/j8N5vPhbVZ08ff6+h/FcWNQefccfGruFCfWhvl3aS+6U8fOPHsvV3ALdC+27OuR3h2RBEPKT5ZAPA2+lnWjFjEXlgrUaZvIja8U0uUHcr8MU6LstVd1IfOCUaJp2ZkBfkefDgK0bXIHd9o2YCn0nhFXf/vwDpTR10wo78XYrGjuj0YhpdIqFXKLE800JcsZVNQXYvX6natvZ/HpDTBKjOBfxHU8xixZYQdU0wqCeEn/5279QKAfm7RgCMUTxYdeGYegxxrKa4ynHaZp9iqvZ/LMw9D30Gu891njGaQClCCHQNDXjMEAp1FWFns1f+75jtVpTeUd2ouGVWOSiobPWYryHWVsKQuqV8zuhWi4WXdk57rFoHsXSIKLUe4i0oe/m8fKiYZ/Z43e7HdUM0pWazYOdo+86Ypjw3hLGSMmZcRjYPH3BzQRsWntckhBZOWchrHOWvutQKFIIdF0nzNvGoLUiTpMQXCmN0pbN1xUxJHHhJ/P2/ZWqEZPyMI4oIyR2zku87Ha1lrbGyG67xXmPdxIarCBWAtpESko0bSsM7NaRUubl5QWFxLiu6lrcDrJBG41SAu67nWiGBdyLoMdXlfj0G8P49ka93pByph96SkpCAGc0uz+/k0umJIueXQrGQYB4mVdqbR2reX6HaaLyFX/7z/9VQoPNbhW5FJ6enimIO8Zuu0MZKxYDszAkjIFmXUsc8cIcS16ESpRMDBO5OMIUhGcgKtrnrxTynN+xeX5GaYVSRuLCa81u+4Zznqz1gVrh97/+jZjEJDznBNoQYsRrRVGw2qxxRlxGpnFit+sk1JxxPP9lg3fCap6suFPsXl9oN08Y48g54StPGCes0owxEoYBX0tItu0ozPWbp9WBOBClRYDU9ygKYRIuB6UMdW0ZhgGjDL6V6AapFFwphNc3otGECDGLRUsBmtX6Jyyp6v2Aebgi6f5een1VP9FqLDy2yAf1/s9FK25H/r7+e8nM/VZawnDn9289e2jDwkFbHf9x63Ba3u+Xowt3QXa50+9XnrnIfATwleLBgq6UdZb2IHhZ0HBeyL4jrhS4dPkaGP/IAeQaSPrJh5nFtFSHWsqwv6EoZ9dOhnI/F+Y+uDigzr8Xq1DLlxa74XzRuKp6K/O/t07FS0Kx9/Yvf88/f1AU3CGIenBh+SCQOHnmkQVn//et5iyN5VG7TsDLOQg6L3//DR9jpvN23loDlpp6KVG8TMdte7A/T4UnnLzv5Zy9UsD+3kfrP39m/vuki4/Kf9eQHn+QyyukOvn1YIPufsQsvuP73FAn73QiZroz/2Stv/Ix/aDg5bKrzvrv/IBz9h5nD7PcOT95fVlciPe1Xwq63tt6LCK58dEtHbp+QLL1MPB+e3sRAFZVwkQeM5SIrzwoYZC21s6mrJoya0CV1jzPRGUKidcdY+B719OuJORWmAaaukVrxfb1O3VV4+uGtq2JMTCqgnMGihBIaW3IOqJCFJ4145lixCJs267ycgC3ghhCH0gpkrY7UOIva+awSykL83OOEe8rVqsV4zQChhCm2Wx6JtGqa4lL3u1AGRRI6K+SZj9wIS6bpoGXbzNpmK8kDrQyvL68opWibhqmENApi9a7283xhC0oLSAhBbxfgVaMw8A4DNR1NfuxC3HZ0HeMw0jf9zD7OQ99z/OX55ntXSZIjJG31/+Fr2qaZjWzWCcoUNUNwzCIb7H3DMOAsw7UQJxEc9is1yhjsErjnRMwpc3JYTbGJD67SuNmAGq9p57nqHdOyOZioGlXpBgZhoEcI1CwzomG1tfiluA9fbc7sG6jJNa5mNoXpmEipkTXbWfiLS1m0Uis6BAmETogsaBzTmy/fSdMI8M40K5WKCVCm93blmkcWT8/C/N5CDjnGIeeYCx121BVFSEkYthSFLjKk1Ng6iUGdrNai0AoRYxRGOVoWmHW73fizpBzpqDE/B9F8n52aYC+71DaCNN3EuuAnCJFW3LJmBCl/HFEG81qvebp+Zlp6CW/84x9j/eeodsRQwRtcLO/vC+JFHtstRbT9JLot1uMc1RVLbG5y4TSBmsrmhVMXYf3lZhnAwmJ+759fcMoJRYaRrgVmpUQ8xnjMNZjiqK8bXn980+azZrXl+8451itN8Kq7iuenr8wdh1Vu5rD0lmojQiPQmDYbSm5AQXdqxCxGWskHB56ZjzXrL9sKCmJEMhLiDhtLdZ7dq9v5A60VnTbDuMcvqkJ48CQE1OIOGulb2fN/q9Jx9L18w3iCqA8T8tn7kVJ8BLD+fm56BJ0PFDt1b3m8uhy/uxSe8rJH3dKPrzT/ohytjGWs61XnYKKW3vnybni+Nq9vfUCGMmT5bzcn53uAoiz0f4oyL2cLP/UaQ/qfg6Wv4KQb1bO5cS/IsUqx3keacqRvEQdn25vjtEDh9wl4PAL0+eqWFrcfuDx2/KL28DpXhW3zuknN9RypgNQ/Hi6aMOdzl4W0t1JR/P3fE/Yf3+L94+vPdrv19L5tN732flW+khfFnXYf661+aiaxXdebNfxhYvrR3vXoc1l/p86tOWk4qM27i8/NE8eyXNtfi8IOK6Wu3yMWe60W41aElI9Oi9OlsXHHvzUenQieeXj83dODwNv5yxN22KNoe922LpBaYe1jlQKeZoYhw5nLbZqQAkwc87hfUXOWTR5Ss1ackMME1ppnp6/AMKIrIzBaYurPKUUOVhvntHGMk4j4zShYqQUMbMtIeCcxcykWpRMVbcYLf7hKQXqpkLR0PXdIVyYr2rRaI8jq80GjbQ1psg0jGK6rjXVTCSGsTRNCyXT7XbiY+68hBmbBrTKEpPYGEpJAk6VIWVpqxA9wWq9EsCfEnb2Jc+zb2nf99RNK9rSXJjCxN///d+FTKwIsBDT6iih1ZzDGEM/dPMEKHz9+pUChJAOjOrOyxiIebueQ1NNvLy9SXxla0WIoIV4yljL169fiSnT73akIH2yeX4Ws+i6EaAGQkY2m0prpdjttoxDL0RwM6O4+McLR8DbywurjZBnhRAP82oYesa+59sf/4uvv/2Fum5o12tiSqRpmrXdQvJljJHwYWFkvX7CWjPHLpeY65mCGvVM6Jbouh3WebwXM+sck5hjzxr0brc9tNU4h03p0L/9PgxZTBirxcogF7SW9qQZ3KMEUGljaDdPTOOEcZYUM67yc9g98XtXzovrQS7EFEmzn/UeOAiAlbHQ1uKsE6sFY+i7TjgAYiBM7SwE6w9cCKUUmtWa4Y8/iNPEOE48ff3K89evvH3/RlFK4pfnjHmWdwzjhDUKX7V0s3DKWI/1iawUbiYHzCnz/dt3wjjy/PVJzMq1WC10ux39rqdQWG82hBip25a2bemHAXIiRUE1Rc1zvRaBSckZjKHv+9niQBjVfdvMscg1zdOKum4Zuy39bofSirZt0UbhfH2IXT/O1gMlCelfKQmyxvga5wxZAcaRyoTWhvWmFqGhsSJM/Alp6RD0kUX+IFC+dl4+3/QfXvwv41t/qD0L6d6Z/aH3PgPHRziD/QmnXCObOgfKd9K1M8OhGeXs9/yXUudUTqfpYGZ5jnfn89XNtl2TkFx2xnI6O6DdLPvRdNT+QzU/emheevba9Qfm8kMZ75ZzmWFxTh+ftwqHj3BBBHTZvFvvqhb69dG08N0s3Jrv35i9S+37yKH3wbRU3EfWj1vT/OF6b1V4jLDOhXifSR/sw8WsDwn+yrzOqMMadg8DXWhUj977olHH8+zGGqDm33sB2El9ZxfKUhuW0iNr33m+/dq174drH5g6LvqyITebd/YuwGP+4NfSgjztfAI+DCU/sqBcy3oxWR54cHHQP5Bubc4fffbk1nWXt0M6n/9qeU29NV8+kh4G3lprKIUwxzmeJmHALjmhtJlJnQpjCITCwaRZG80wjnS7HXkaqVcNxliMEahtjSEDRmleX17wtZBACRM2lJxJpTD2nRxutCaFQEyZUjze18QpknLGGmGSRgmbdM4SfzuXAjlhvSdOk5B+KQH5wyRhiMiFVLIQh1mDRs/a60Bdi9/tOE4MXcdqvcLaJKzgMzFY3+34/u2baLmbBmKmyJvRNA3b7Za2bWdf74JxEpLp7fUVM4c6a1YtADFFzMxC7n2FMYratWhrSWOiKISxXSlev78QYsBZAyh8VXj59v0QMkxI0EaMd4QuMHQ7UvJoLZrkvuuEAM+JoMNoPU8wRY4R4xxNK6RW1lnGcRDNdU7UdU3dNBQEuKRcsK6i73fsNbzTrNkGGIaBUgrdboudw275qhaz/RBo1i0r1lR1M4enSoRhQOsZ8CKgNCVhUQ/TNJsTSWgv5yzb7ZsAVqWpmopcCtMwUoDX1zdQsHl+xjpH264opeC+fmUcekqKxCSfVo6B3esr681GQmrFQI56JnSDvuuw00RVVVR1jdaGmCLddsdqsyHGTClisq+VIowTBZimEWcsoWT6riOlyGq9od1sxCe+H7BOhAbDOGJjIpogoD0KYZlS8PT0JCRlVSWa8hQJMZFiJ0zdMTHN7hD56Zk//viTtqkxRgRSZAkDGEJg6Dr5BkC0zTshHdNWU1k3b5Kavu+gZOrK0dQtVdPw8u1PktI4Y8hppKoqhm6LKMQdxdrZtcHNDPMDaYqUkilVxTAM9P1ANYdzq+oatGKYrQRWT5tDyC9tLKunZwqv5JiYYoJJSPq0NZSccdaSYkRrze7tjbqtSVOk70eMc6iUmPodOU2MQ0QZQ06J7dtO2NB/RbrYwX/OifajB7UPHi14180vbVxl/vfz73INb17PrW4e1g4b40JBp8IQdQC0989K5d1c8VrBJ2BdDNaOQ1rDI/V8LpW7Basrf/PrGvWR9B9Z9410Ml8W5ttBSHMkJLk4gC8VWu6DzTstupnjfEjPZ+3S74v0Y2fKB9Lp8fW93UstO5VgHMQcPyIYuCUBOBN63apisQlLF++19dZ3+Mg7qv2cmwWE6rLaxWKuLQ0fBUBnzy4ul2dlqCvX948ebj06xgt1fYSAb7GgYzB8q6yL8bsh5LpW51K1/6RrI7A8p2/uQb98UflAKnOXq/2vw+XbAB6u+tp9cC99nNU8FwoJbR2+rsR3e2Z5tkYIlbQW/13xPe2xczzjkjPOWbSTUFDjFKEUvBWTYK01/STApNtuWT19IcQ0hyRKqEkAwkEraSyuFtZzeWeJs220mFUPncRfphS67XdcVYtPOhJX2jrxtc1BYgAbY6hXNc55drsdBfB1RRgGKifm36UIiZyxhnEYWK03gGgwJS5zwPYD0zASQoSShQFdm5klvaZqGqwxdNstGmA+9Nd1LUAkg6ss4zCCMRgrjOXj2M9aPoNxlhwTevY/B1itWoauJ+SJnFuY/ZS10VjnmcYBrRRPz8/EKMRfzlfkVASAzxpkXURo0e06FALCtTESwquyGGtnzX3BzEDbWUsuRUByFEHEIQSbNvS7FzKFpmkZRwGKQ9+jteN580SZx0nNgp1cCru3V/kwlIKcUM4fNKPG2JlV3AoxV8lQFFXTCCgv4uVd1RXaWuI4iTtAjKzXa7TR9F1PyYVxGgXUpywbVi5iOaE00xyPvhSxhNBazxplIfDz3gvvgJI5N00d0zTNAiIZ+9d//44yms3zE0YZXr7/KdwFzzU5Jpx3VMpjtKKUzDhOjENPTmKS/vbynecvX7BNTZ612cboue6GoR/E+sQ5tq+v1HsT/m4nGuemmcc8ksNEsJaMp9v2xDBiUKQspGx1XeGM5TUEQtezXq+obC0m9TkxhYDzjq/175AKf/z9T5R1kCNV3eAqx/PvfxXrjQIxThJmbbNm7DshMkziF/705Zl+15Hmb1yXTFXV4JOA34JYzhhLKYivuDVM/QDW4KqGqCcUMOVImd0QcgwYLbHchY1/IgbPlBLj23faVUPVtDw9P0mfpIxxnpIz1bwu/NR0Ij19QOp6p5i5qMt0BU9d3vzYye6aEuQj6eT5hcJOcPAxciknx+wzobs6/O+8bSd758Pnn4WNeGEjXYIF6vxuOX3fh/bhW5luHEyvteTD6V01dFHvxZA90KcPtehIPfSpg/Y/8lR63glHv2/iqSWw8ZlmX3vmyvf0fum4Zz/fX7+ip4/beel6M39E6n3VvC9g+mA6O2QfMybvbx2vOafw/0ZTzpfbO4tnOa5n4eB/DQs8ImT8VFd99KEbc/OzspGfNt9+tKB7W+f+1sI+9Fihn83x85/89VX9A9p2EEDdrv14fXzH0JftuyhqqWx1496N9PBJcxh7DIrNc03MilwUCiE3yinRNA0himZu6DrWm6dZy6UpcxxtqzUxij8vpYi21VpSymLmqSxj6Jj+/IM4Seipp+cn0E58NmciKu89ej5YT5PE4jUSO4imblA5kUn4qma13hBzxHvRzDrvUDnPIccMVVXjfMU+FFOKiaptMdqQtBYz4DCRUhYfU2UYug6YQ5MpRUyZyteUolBqIMQosbetJeSMd6I1jFMgo0RzHQKgWD+tMUo063mOz51LQRck7Nc0EEOkUKQdWQjIhr7DOku7amZf5x3W2lmLmrDO4ZwDlGizs2xvKWeGYRSBgLMoLQKTUmCapoMJcAhiHr5uW4pSs6WAEL6hpF9ijOQYGYaepm3FdDpGnPOAaGjblWiVJda1kNJZa1itNweAVADnPX/8+78RhhG0MMZXtbC2m3mOhH0MeWOYppFuu8U4S+Ur0aTGSClKYlR3Hb6qDjHmldaM44B3NdZMwhZfsuy4867brtaYmSldSMlEMJFmoU/eowMlTPopJWIp5FxIIcx+xoWh66iamqoRAj2Fou97mnZF3cyRAJQw5eeZ1bzkLKzvxgKRYdfhjCbnRLfr8LMmuGlXNG3D7m2Ha2pMLuQidIXT2BOmwDSO1G0jpHs5Q84oJWG94m4nfugKtLUoNG3TYisvgiWZJTgr4bhSSsQQUVpiz4cQedm9CCt8v6Vt16QwkCY1uw44hl6sFJwxjMNslaA13nlSzihlUFpRcsQq2E0T4zCgjSbnAT+HDlNa8/byKkI+I3POljTHsC8SXq8UiIlx16G0pjjNNJPToTQhBond7i2uqsV6ZtcfLCXSNJBSoK0rlP1Fm8PhkP64qfeFRu34+gKguxTC/pyT6vX95LFyT0IqXRMUH066898n+Y78pdX7+99q3HtfHIG7o4P14jPnFw5tefcDXAJfx+1ZOmffA8lXzeSvHPovkrp2773gPdfHAVhc9M95l77P0nchxKXg6LTO6638oZl467CzcPlTQqIruA/1Lvo5z/IZF79zQdaPfJnHI6dOPprH1pbj91gkV7uYfw/x/T+YCjctZY7XC7UfgVkQd7QW/FBa+L5OhCk30O69qm8Jki7m0dLzR5muNeP0b/Vwf3wWDH8m/QNh4JV0+5s47YuFPHf2jON0EsbtPP7bB9NHnvxHjufj6WdLyT5a7T1JiDoSNh8/XuZxvLHBXC3yY7vPw8C7qmq00oyTmI1b72laCQ2k5vjKahSw5Zs1MSf6YUAjsblLzkxRfJTrumaaRvpuy6g1VdMyjv0crzrireP5+QlXNxgl4EMYxgNaa2IqeOtApcPOkZL4eBpjqZtaDv9G43SFioZx6MTsve9wzjGNI027OmhrKTBNibe3F35znpKigGTn8UreSxkDReGsQxuJZx1jRM9aWPlQxQd6mqbZFLrQv72itJC5NY0AIu+EyXkap5ng65VcRLuKgqZuSX1/WIX9zFBe1y1oaY/VwkJdcuLpy1emSeJeV3U9s4vbGeiomdG94KzBffkimu45njfKUEohJebYx5pmteLlz2/knGmalmEYsHOcau9nrXwpYtZrDOM4za4HoI1ohSnCvr43JR+GgappSTHw9vrC169/IatMmf2H67qFXITwq21xxkqD5nkdxkBOUlbJhVISOSqSEbP/OAV8I++eYmQXJuwcz11bIfcL0yTlN62YZ08TVS0+wdM44KlRWmGMwc0uANMkPv++qih7H/9pYr15AgpDJ6bZ3W4nlg2Vx84EeMI9AN7JWKAUYz8QppH104ZcCjEnKrMiqkAKAWUMVdvg/TNu9jOfwiRxzLWw/BdgeNtKiLeZpE8bITVcbzagNMYZVFbg7WHV0DGiqhqQWPZaQ8llNt2ObNYrartGNRu+f3+VqAFR5lXJs496yXz57TcJv+crnPOknIT1HbBO/KWHocfoEe89pUA3dZRcoIiGmSJmvMZJpISYEqunDWGaiNMo317tyVNPCiIsiiljfUWKE0ppQoji4qAVdS2WJ5SE9zWUzNgPNE2Fq9ZioTGPg1aKOIzEMM2REQwquocXzltpAZ/90vTryv8J0OCab/ZS9kPW48P2UZaf8KLvQPI4qftnhduIevnSOdI6XNoDj2PZ+wKwOW7TGch/vz+L+ct7yftnHu8/tRxy6aS+owx3hvTk9lk57z9vHnk/nR4UaT2Qs5z+tSQZOStKzZkvajg+B54vDnea9mNH2ONevlLSHdB9Ljf7NUfps1LPpRrHWfbLyhko+lA/nb3IBUBe6JPFb/NOZcdZbg37Ie+CEGS/Xqnz9h4JIB5aFs8EFvea/6GxvvFy533wofmz8MDPnIOLLPQs9/n9ws4bd5via+ne4Vo5kTndTx9dJH7Rh7xY7JUxBJb9/n+oBWccBzcPX/KxvWd5//fCrWD+fb533REf3kyPa7z7nnbzxNh1hJRorGit9ewrnXIBZbGVxHQeuh0xBryrqZ6fmaaeadeJ1nAOb1XXDQVYrzZoLYDk+ekJV1dQClOITCmRY3xnww4TYRzwXnxrC0gMM61QKNHgWo9v1FyGkJpVs6bRuoJSmhgT0zSRYoCq4H2Fc0ZAc5jw1szs2o4yH+rHYUTNpvQFiEzonGdz+gKq0DTNgQV96DqqqsF5J4RcRvywUxRCraatGcdAypkQA0UZ1jNoRmuatkWpguo6cpR45loppiCAsXl6FhABGCcxu2MIKCUaZKUNMQ4H/9oYJITXNE2gwDtPTPmwRMQYSakQJhEGtO2KqhG/V+cs1nqmUVjUh24nMa+//sbu5U3ercDz7C9tzOwH3rboGcCmKGRq/stXSoEQJ5yveH39zmq9wThL+/REjlHApjFUM2HdNI4owBh7CN325cvf6GbhRCkFV9fkGOmmgDWiYdfG8Pb6ShyFBE3PVgy+8qRkWK8FkJWSGbqOod+RS6GuG7QxIuyxhnHoUUozdNsDWE8pUTcS5zznTFV5nr8IM7q2Fj0ThZWcmEYJbVVKJpN5+vpVxmIcKSEyTqPEH88R7y1aWWIUM+6qqqSNORKHHc1qg29awjQdfLOVEgK0qm1FCBEjVlu0t7O/8w6MIYwTTVMLsDWG2lQkXVAaCPC63ZHaljy9EcPEavOE9g5lNCnl2Sy9QWmDsp5xELI7ihzeEzAMI3VdUzUSU9tYS54C3jmG3Y66qWma+rDI5ZJlzsSIq1tSgZIyYZxQRmOrGmcNWWnCFHj59id1VTFqi68c/dsr9dMT2hoq58lFEWLGGI9roF2vRUuOCBCNcUwxkHNiU1uKMlR1Q9d1n1xGj9NP2tX2O0K5vHz1wH6Z8yemf5AU+2iz/HGcvXCaXkZR77nP6r4vOCl3hQuPCl9OYgEfAbjFh8vp30ugvxxqv9WypTKlxeXi2lG+I5CgziflNdB+dkC/0PbfaOrVON1LA3UO0s5By610jHxuDdojgOcow3G9d7/hk+46/lcd/j3tswWgui+q3O/fR+fnPzYtDTZwJFHar7SfWpluPfQoIr2y1C+c2Q9flLrIcfydl8vJcT4wjwgFbj1yVMV+/z3Di4d8h2/uCghZBI5LQo0boP99DTkdkHIl//mSchuk3umZK+vC4ve5kM4Jrpf65XgNVzfae7Y0Pvw9HmvaL9qzWMPy3PsZQoB32cP74ngyJ47+v2eiv2fd8Vg6772jeh9J6rTPL5Th6uh/x3vtIxvXlfQw8DZWQ05AZN02otHEkinEYQZV3jN1IyklYRtWFTGlOXyQaGLHcRIWYy1wz1lHP3TCUu5rXFWRk2gwc4x8//aNpmmFFCpJmKnh9YW3l8j66Yv4GhuDncMiTdMosam1JoyDkGGVQjIWYxxa5Tn0mcFoxW7qKcVjjCInxWolQDHnjCKR4kRBofIcDioJ+zpawPvQ9dQNKGvJGaaSsEYGqF2vxR/dSgzwHGcf1qSIKc0+8lHiZ6/XYs6uFc440QIW0YPs/ZGtdcQcZ+2+gZkILcU0k8pJHOdxHA+a2rZdiVbReEqZeH17JYRAXdWEaeLLl98Yhp631+/iF64NIQRUlNBgmUJOGRAT9TiOKGNoVyuCNcRxpG5bwjTO7O0TQ99jZ6b03W6Hsw5XeQljFgJmDjXmjGjDm9WKrtuRs7CND30veWYG+jKzvCsjwMtaQ9s0jONEnALaaold7h2m8oQoFhJ9J4KefexxpSScnVbiE15yFsGFMWhlSF784a0WP3BXVWLiDUxpZOh3oIWsTlsLOkmscq0xSpGVni0HJKa5axriOBJD4PX1lfXTE6DQxvH2tiWFgHEObTQpZlKY0FrmqakNWmmmcSSXTOUrnF8TmhU5J/phFE3zTBTYdTu0deRZ4z2OA8Mw0qydWCfkhDEKWzl8VeG9xIqPOVNyIgRhiVdKM4Yorgp7l46UiMNw6Ks4Ww2M3U5CexWYwkRJSeYtYo0Q4zTH/UaETyjs8zM5F8YpyHeuFLWxGOuomhXTOBLGkZIK2WoqZVDe43zFGAIpjXPbIzklfO3ZfP2LREAo4n4SxpGMFm1+KXSdEPH5yhNCkLwxSAhDa+eY4zu0+Tka7/P0vqirn3PI/Q89JT96zP3ENvoL3+v4ALj//cCZ9h72OpaTL2c6QQfveeTS8unrvM6TXOXSXeGUP+D2HLt3773+819n8v1jfHoB7hbQ8DnIPkt7kH+ixfwAMF8Crsv3b5zwLjrmHSZdzJUbnfjQge9srI/n0uLceCRdNHQemHIC1e8WcdyufyZAfjhSFygLSOBy2JckEUen6LL40EKp3Mt0Nb0/tbRSnH/Jl8D1vZ1XRuJGYPTDF1veXfrOpRVL81qpd5DI2TOLZSgx0T2NK30Jho6vXljYnLSbk24/XVL2jXgHd4f7e6C+X4+Ofp+3ZF/ERXPV2d+XLWO/Gi6+28Kz6ujeMRA/yXNy4/T9zkHrxbXjKh9ZI8+fuQKuT2SQ1549S+cx3s/XwsUy921beKelfeGnpLlh5WgNeJ9ntxeFn9GUh4H309MXUkokV0tILjJj1wkAKRltJbQVVUUeRyE6shKD2DqLUZopZTZPTwCMwwAlE1PE2wpKBiUmyhIyylJVnrry5DgBFhXEPNpVFc7Jgb9QcFUFuYjmEXCHHVuz93/KSUi/tFKoApX35JJ5/vo7KIX3Dq0V0yTm7BpQzpJSxPmKEALWGJyrBJDOmu5m1ZJzESCQwRmN1hrrDFoptFFzWCsBhrmIxt1oRZgC2hi6TsinjBGgZa3DWMuu2+KdxXmP955vf/5dGN9dJX7PQbSIUSvGYSRE8UWum5ZpGqmt+NKmmOi6F6ZxFLbwaSIZi3OWfQgsNfvk+qoCQOt9THYk5jWFlDM9hbpp8ZVndI7X799ZNS2Vr4hJzM6bppFQZ9YyTUEY8LXGVTW+qsQHtxQKhbHv6WdBiXMOrRTrpyfaZsUU5/jrIZJSpETRvK9WK5S14r9dPEPXifbXaLTxKESD65wQyKWc6P/8g/VamMPr1epguq+1kP9p5+dQbQVX1TgPXdcxZhEm1E1ziGlttMZYe3ALGHYdaMXz77/jfUXKAcUcMk5rslasN09obcmFg8ZdVU7IxYCUJqzT+L2/erKUUti9vMlZ4WlDN/RY6yVs23YrbfUeYwxV5RnHSULRAU3TirZ+ntsqR5q2miMDKEpxDN0bqR9Yb9aknNg8PQsT/TQJYaLWc4QBL4tfTPR9R922WOexVuJ7W2uJYSKVBEiYNefFZN8qIeAzzqEKTHuXESU+6Skk3NOeqDDNURMcoUzkMfC26+VbcyPOWZx3NKuaPBMvhkn89a1z1I2EF8shSEgT62hWKxRQVQ7rqjnOuiHkiK/W7LYSjcFbM3Mi/Jr04cX62jnrrKxfdjA+qeif6fj9eDo9O56D3h9P98q41n1LZ7wfqef8THYr/2fv3UvH58bD4fZOZecg79ozhbMD3DEIYLmc4/yPfHuX1V77ypZJEm+961UZwMW1spDpCsC89lJnk+paiKN7ePNnfSO30vVD9SkqPMbKt9L7uX15Ep18J+XainDtzW+vvO9PLz9/ijHuiVVu1HWMx24M0GILZrR5LNA6B5DqPPs+31mB6jBE5+jpsh3HQqUD4t2XswfHiz139tGr06vLQpbTyXJvv1zk2Fj4aE7FnccPHb3hvn/n8T3u0SMsegq+r6STNpfzd+WSs+TWB3uOcGfekiWt+yn4PG8J79/rxeJb5jKP+r+cPTjfL0fvdGjHDICP1/r9VDkRvCw06dpgL65xjyxst8pYEA4ctfbh9DDwDkE0WClljHXCBowAR3KhHwbGoaepBdQkZ4UsKhi09QxDJ4zXpTCFTEyJgkZpS4gJVZhJs4QcityhtGhWQcistNJMIeB9JaB6lqikXNBaMfQd3lVkbcgh4qqaUgpaiW/50HWEXFBakce9dk76cJoP8jFmQhypvKOqK5SSmNVhDmE2DluMlbBa1tjDxDEzg3vOZY5HDN47KIigoKrEtDsGvPNoDeMw4pwT09fZ9FdpCYs1dD0lZwLgK4PRhvVqQ0xR/Ldhjt1syEXh6ppatygl7Nsr7wlBQImmEKcJKFjrqeoaax11XROTaDefv/zONA6M0yhhxIzBeUsYw8zmLvGkq6aRMZwCKMP66Uk0l0WAUwoTKIXJhjCKXy4ziZqvqgN7eM4SZiyGiHOGFNI8fcV3vhvEH7h7e6VQqHyFNsKKb6yTEGXaoLRhtXli6Dug8Pb6yub5WdjHZ2mw0Ya2XfP8228zmV9hGif2JuoU6DshDHNWYtMLqBZStKp+wnsvBGBJwN44DDTtGmWEbM7ObOgxJiHtmmOo55IoGbquZx9n2ziLwzNOE9Mo7NzOWZ6/fCWlzJ6R3zrRYIdpIoZA1/XA7ANf14RxpNu+4b2Hpp7jnI+M40TbtnhXY7QmJ2g2TzjrDhvINE7ELG0NMQrDeEyMw0hdeb59+xPrPPVMUOebGlt5Gq0oKRGnaSY8tGgFxjlWz88YpYjTSEqipc+ze0UMkRAC2xcJdWedR2nL6lni2nvvCVMgDD1FybdU1QYTIIVI04oLizZeLFeMQ6tCv+sw1pFCIjpZV1KWNco7NwukIlYrVq6iKGG4V1pDzsJRoUVwEH/BSfMcFN1NF6ec5YP3D6dytOlxupdcbMr3TulXyocrz10t7/Z7fzRdPYfcfeIjdV/Pe2+cfmwcT9t6v6zPvNvx0x977r097wfqJWjzaKkLZ7hrPy+uqVJO2sPZQfb0+eXj/K267nX+8u3Lub4kQPhMeu9fdfY6C0DmkUPorYP4p9M8o84dtc9+nj6xP6Sf+q6fAujr6eqXcoJs3u9fzFd1DL4+n64+fzFWc83HuO6oUSdg9nyoj4pRxyj6uCpVTl/7ZAIev+v7jUN+dQrgj5993z/20P7aV3Q6AY/f77xdx64me1/4A1PG0T1p2jkIP/TUUUWK83CZV4VZN7alSyB+/th+cAqqqJNn3lt5XMqcZ3ExuIT/i6DwuC1lufnn4FntB/OkjeepHNbxE0aShfVYHebte3i7C56CkwbMxx41A/jyXpiMjXrPfmZTf3LGKgvjeOuDff+44ajNp8/97LXvA8B7GgeUBo2QUIVoGceId4aSAikG0XylgHcNsR/RHoknrRTeVSSl2L69sHsTTbk2Fh0ibV2jjSKESYDGOFA3Ag6VEqKrytfkXITFOQW0FhATxolxHGYtfEGRsbZi6AemYaCqxNfZoHBVRZzDlqVcMECMUUJWKY2vK3IJWF+BUkyjaFy1NqQYUNqye3vDWIlNvtpsZiCXsMaIxm4Sk1vvPLZt6PteQlPV8jcl0w89VhniNBLChDVOfH9zwWrH2PWH6/s+sNYyzURQ3jkhTUOBTsKAXXuKtoRpwjpL07TklEV7ONevtIRTS0mIsFzlSSHy9vaKtf5gput8JeRXGooXkGa0pm5qphDEJB0lhF/WEUKP1gY7g1DnxQJBWyG/0rN2OMWItgZXe3ZvWxnb2YIhqBGQsRZ/akVd1zjz5eAakGJCadjutrx8+ybtnN+jqipKKTPruSVqIWITYc6EtWIivS937IWALYXANE1M0ygxxVOkDANKi0imrhqMM5SUsdbgvCNGMZPe7d7wlTCXl1IYB2FWX23W5JQIQUjZ+p2w5RcFvmmIU+B1ClitCGOP8R6Hpet6Me0PE845CopmvcbHyNj3KA05RkqS8HauqmZrjJGSIRSZq9PQQRQmfDU7jDrXkHKk7weUNoc5qowsemoOczeFQNPUrDZrti8vMqYw8yxESi5zbOxAnAkEtdaUmEghUoxBGSd8bgio986R4kSOgc1mI5YFKOqmmhnPDcMY6LqOkgqKSMqJpDUKTdU2oq3OhZIz27dXMW2zjjRO9P2AsZrt3/8X3juseIKQYpRoBxo0mTyNdMNImMQ65du3f8M5EQI4Z9Ap/7SF9fF0tqj/TPB/tuOeS86P95WLLWW/Ie1POsdF7vOfHQ4OVo9noP6iTeeHip+3n/2C9M/WyGvw7BNCi5MTy7XHPv7e5yD91pT+mdP9I3WUizv3Tme/Pj0q3LhrdvkIoJYK7/fArx6g4/VgPlurMgMVdTlFZV25gByH9A4byvsD+/9Jsadr14NNvPh9djY/XSEvv9Hz6hZB692aj9P8MleyLBWp9ovz4gPlPOOFcKNwuoeoGyr3y+/rCqa50d53kFk4/jr2ZuonfX+lDxf3O3UuDjhfANXFd3Guj78+fW6Mmbrsj/dn1Omjh733rDWPfttHbTl2Rbr8cpYkHReZLqtc6NiLb2E+LLyP4OGVTp459P65mdLC0WjJyP+0jPOGHlV88hLzHC8ccYBcPrxoPXN1n3x8sXwYeMcUqWxF0RYy7F5fhOW6XuGbjfjPhoBRijCNNHUt/p0ls3t7xVc1GAdlpF21uKqi24mWMoQJlZCwV0rx3LY470lhwlgPSjGMI2GagDyDaQulMJHRSmJR70d070estICuAnS7Hc4K43nOIBNCSUxmVwEFjWib4zSBsSSFhKEaeqxzaG14/u0rJUvYqL0kvW5aYd0eRlZti7FijrwPo1VSQlGIcQIk7NIQemIMs7lwolmtcM4RU0LrjHMerRV5JoHSSlNXDUUrSk5U3s7kb+ITb7T4wBujhSE9JcZxoPI14zBgrMXPJGtC2qbJUUjdtBLGdeMsLko89FwylWtompapFhbxcRgYegGlpRSUMhTEGsB5RTVrtKdJtLjeCSitKj+/l0EDY4jzGBl8VVNVFVXlxcd9ZmA3WkyWg1IwhxGr24YUooTHAkpJlJLpu272hx4O0cFSiPhamLONMWRnUUYLGdn8sYUoMazHcZDQVFYsFEIM7N7eSEk0/SaJRjtn8TX3Tc1684TSinEUToOCEBD2u+1s7VDRtCthjS+t+JhrQ9W0TOPAsOvIztGs1tS1EP+9vbzgqorK2Zkxv2fYveF8JeHBanFz+P73/wVFfO6dr3G+pu86mqZi8/wsfvBGGO/FdNsSQqDywjoexgFSIgGVb8SPPBeqtiG9Rl5fX1ltVviqQVuJYR5zpt/teHreoNBgLd3LC9o62lULcyg75yzOWorWTKOECzTWwqgxzlFyEaJDpSkoxrFnmHaUlAiTCLO0knm/j6UeQqAgYeKKUrgs/Ai711eMKgi9gcI7h69qcXNxHu38HNquEPpehELWSAi8fsvUd+SmxajCbkz4un144fxUun1WOs34iRPviaR46eZSqeW8tv2R6fQIe6xEONlYjzah8+NmOcq3BBeXDkxSpLooe39zPpOfFvgDuOj08cXGfKCkK+X8SNq/88WNx+p4f4UFlt3FU+nRg/vrn+rj62j+HwG2T9NxG/5ZBCmPj9/JE9fG6aEi7whElj7SX5Kuf2ePALL3h25cu/YOt/robGpcy3q/ez7fgVdnp1qexffaevz89YuPtfchgYU62k8usfzZWnv5tpcgrJxafpc9CH6kMQu9uQeDi0Wc7n2Ha7e24yOJjjqr8vKtONw8s8GZ26QWJTL7vjy893mHnBS90Jdnb7QwGEeNXn7R01CoxzB6f7A4ur6Afi+EDA8JMea7e3Cs3gU/Zd9Vt/aw41puLBdL3XCcR4b4VjjFj2+QDwPv2lfkXBjGgTiNtLUTQjQrZFxaKwFvxuKNpu93hHGiFDH/VFrNZGKZqhZz5RQmnBPm5ZKLHLSb1QxwMs5X8kwpGKNn7ZVnf4jIucygYsJ4R9O0El86CFjNQfyLvZNY0HufY20s5CTxjpvm0Ltv21fImTBFjAlkkPBZVUUMAeYwU2MIB6K4qmqEYbxtQU1sX9+oV434kU7iY5uBeAj3AtZqwpRZrdeEMPH2+gJak2ICioQaGweMdrOpvBJW8JQOIcekr8WEPqdISmCNaGOFXGsmWBsn/KyltdaitGjmlSrEEBjHAZRCZy2gVmlc5ckp8/LnHzTrNc55YprdDJzEUx/7gWJAlyI+/M4xTYGSk4QWM6KhjjFijCalyBhEgDENw2G+9n0nmmnrRFOuysySXei7nhjErNscMcIbaw9hyvq0E595Z4lziCmjW7JKpJhISkKVGW1nQUvm+2xGrWeW77ZZYb24AYxjEOb1yh/qffv2nfZpI6A1RnLKZAekJNp5rckRut2WGAJ//Pu/0z5tWK3WEiotZ0LMaAvx5QXnHZsvX4ghzGzxlmEcKDmSJ/DrNTFFHFBXX0VQpBTWOvq+F416KdRNQxgGht0EgNYrXr+/CIGcFkb43dsLzldYK/7lWlusUxST0CWTQuDt5QWA3U7TNDUpBvptD1rx/c8/qeoKoxUlRWHBj0EEK97NQizN9vUFKFgj/v/TNElYNitWGIUioeKMxnnP68sbKWdymshToH16FpK+ceRl+0ZVVTR1wziOElkgZRLxwB4fpy3eKur1Bkpm+/pGSvnwnVgvcckpk/Aa5ESK4gbiXMW6XROsJ5UMFIwq1F5/aPFcStf26GOh6jv4Ps55jiZPnrpMR9kv/L1OGrNU1n4LvYScxwB8b9Z5nOH88HUqaS5HP+Y6ynuU4WNfLlSZAeXpzrlv8vH/D3Wp03Ye/l54/wuG18uzz8l7L3b5tZPmlbKWM30C4F0RaNzOpxaucTg43LDoXQTXJ+ab51Pn7it9Fnj8o0HxfzT45m5/PgJEb/baLdBwrSkfeOZz6QbSPlx7/HD+i0ReV9PPqOPHyaIeHCB1bTAf7dHP5bn/eud73/zU0T5yXVawBDwfmbSPTuqFeXfzhT76sdzIvyAIeP/1iE3Me39c276ut+MaOlVHTVsat1t/P5Kugf33+k/PGfvRXtjcPpIeeky91/X4Q3fTw8C7KHC+wniPYS0EZE4YmIdhgJzJKVF0Es1e3ZALtHVNQc8s3ruD1jDuicDaRsDzOImWtt8JsRkVeWYcjmHit7/8haRFWxxzQeeI1hplLe36iTBOBBsohTl8Fox5Yv30zNR3hKEDX9M0K1JJov0shkzBUAgpiq96L77Vxq9Qswa2KEVRmf7tjdXTE9ZZrK5xrqLvu5k5XBjRBy2a76gUrq6oqvrA0C6m4YoYEtVMYma9Z7XeYK2ERlJKUc1s1eM4sVqv0EoTVSKmxDj2FDLaCBGcMQatjcRx1ppuJwILCqRaQGrdtChEIxvGEYC6aRinkT0TZckF6x06itk9GprNBqONaNKto5TAPnbyNI04pXBti9IQQyLF2Qxdz+X3Hc1qRUpJmMC1+EGX2X/ZOkc9CwKGYaDb7TBWwmhJKDBN2E2M04R3jmmcCNOIqyqsc6w3G8RoWQC1nn2tU4z4WkjKShYCt2a1IoRASsJEXrcyT0KYCCFgRolHr63FGMO3P/7EWsNz0+BrMZ033omZvnPkSRi727Zl6Gfhhda0mw0KMWePMTL0A9YZrBXivFIyVeUoOeOrmpQCIQaauqauKnKOGCfzUilF07T03Y6QElYptBbNuy5J3qttqVYtqgiDfA6Bt9dXmrYhx5qqrqmblkLBKAGgYZLQbDEErHVsnjfEKcyCBk3TrOk6mddhGHDW4euanN8YuoHoE03lBNzmwvbtjZfvf+K9p6o8wxTEYsEK90HJWQQ7OSOkcxlIGA3W1qzXG4ZxZLft0RosBVUyKQXEUiRiq4qcs7iGlMLzb1/F9B01k8BZUgziEqMs0zCKxY0C6zxGSWSCpnaQM6WyxDThlJjGV3WDMv6HF9T9NnECbX/qafBUTFuOJOEXYIoj3HzjfFKOn9n7eB1J3s+bf7IFLeyZ52D2+Bx9joNv4Y4TkH0BiN//vtq95+96/vt8Lz2q8OTWUV9fb9ePD/Isj7h6lLw4k13KUk5CAJ3326KQoRwVdXz+LVfG6MOv+XFtwHIZn6r8l6TFN/rMa370mTPhyeK1s+/xWIF1cmsJpxQOFiWXn/XP7vt7L7+wsBwLG3k/Ep/eO32xj0DIn86c/Jn006f6tR74pZKVT6bPx0W+/j7nO8799KE2PCIZ+9E6rj6wn/SX8/3Er/+H96gH+u6qAO3axYXNaynr0qJ2lkei6PzEs9bx0vSLhJAPA29rLNM4kmZQpJyYFY8zOVScYzQrieFDjHGO66tQSg7HpUBV16QUhYzKV7NvNsKObjSNrxgHic08TaMA0bYlhMA0yUFcKUUqhd32DWsdGQ6m3WGayFqjlMZZIbDS1vP0pWacJrq+p+TI5vkZO7Oh7ynlxxgBIY2wxjLlIOG9nDCeh0nuW2MZxlHMjquKgpB0lZSF+KkRhnfvPTkXnPN0uy3jMFByJubI85evEp85Jdp2LWDRO1CaYRgQ01pHLsys4AKgXV3N/sPiS16MxB43ykhIKaX49vc/cN7x9PxFBCTaAqLBGvtBNL1hwmphPc8FhmGgXa0w9QwKfYXWir7rBfxMiaqu6bteNKDrDcMwsH19mS0TRFvuKyEmi0G03zlGYbfeE5YBvq6YpkAMCVNJvHQJ91QRY5hdEAQYT9OEtgZtpc0pZ8aXF6q6ptt1pJJRpdCu12LyjELXimGQWO++8jOZl2P3+kpOkbpdEaaJ3W4rfTAf3t3MKN5td+zeXvj9r3/j9fsL2hj2BG/CsD2HzOpH+n4Uiw+lD3Hlc4ZhFC20sZaqrkBN+LoWActczm67BQoByDFRVTUo0Uwba9i9bklFxizGmUfBGJQy+LqhWW8Y+w4yWGdxxmAqN3+H4o9eMAxTEncHJVroMA7kXKAUUopUdUu7rkkpz31Wi4AERbt5Yvf2Jr7a3pNLIo4Dycq3HgaJ6d42Fb5uKAq611cJH6cNMXGYrylnUuxo2hZf1xKiryhetx05TpQ0MoXC8/MXpjlk2WqzYZz90sM40HedaLxLoa5qUkxkIl3X47zn61/+xrDrmHKiLszCqpGIoqkqAeHW4pynrhshBEyJvh+oZpP/H0sfkbz/2C5RDkUcAf171e/R2Pnh+gJ83kK0p/W/V3mKiJceP9clHAsEzgHn+Tn60LTyfm8pdmk5zszpPnoo7/zMsgBq1Pmz53+fgIClys7SmYD+uAnH7bsQoCxNl4UD3zGAuAlrzubIBV65Ad5gL5u5RWr18cPuPy794MnsaO4dXwPeTR/LB0DcVcnKQqYLCdRZGUsCqaOD67Gp6vl3+HhDfyY6/USZhz7ev8u7oEAd/lku8p8BV19NR80u3O+ZX/kunx7p8wd/ViPvfBf323u+2P2zpuX18naTb62xV6Rrh3vn6/QvQpmL9R+nG/Wqk/8t3pO/y9Ged5m7HP3xMEfG0f9vGZqf1fBQehh4K63x3pFUoe+22BjFHNZ5au/ASkzpqR9wlYMimmRdIMRJNKrG4qtKDv4xMsWR4B2rZoUxCm/k5YrSGFeRw0Bd1wcW7RQDKYzCbt2sUcahrZipZwrTMJJzwlaeoRcCN6XfqQVyiuSS2Ww2QMZgeH17o6AIU892Kz6q6/Va2J5HiatcCOLj3rQUYJxGwjRiMCg3A5yZLd1gULPWdeh7YZ8OI8NsUu2bGhXNbJYNlIK2mTgFSs7iezxOElc8C8s0JWMrAyWTYz7EOe92O9pcUD4zAaVkQLHarKHs41MrSklM04SzltVmMwsWFN+3Epe67zussXOYq4oxTIQpsNuKf3HT1Affba01MUSsd9iZtV2ELIWmXVF5K+BUK8ahZ5wGckw07YpuJ+HJYhTG70IhRYm9XnJmyJmcE81qjfMVSsHm+ZnXlxco4Lyw0OsjU+YwBarZFSAlcXXY9h1xJpnTMxldLiIAqTcbtDFst2/UtYDNumpF8z2DW2M0X77+TrNqZ3PlMgsrAsCcN84h7AbxV7cGbSztqmUYRoZ+oHt7oW7X4s5wQClaSMRKJscwm7uP5JTZ7bZiIZCdWB0A3dsbWkFJkapqMM5SZtb5UsrBjcI6Kz7cWuK4j/1AiIHX12/02x2///V3mrqZGfgt3hhKjIwz8VzXbck5z2bnr2ijxU2hrkFpsWbJFms9JUnoulzE3WO1almt2zmcGdRtQ8mFkhIv34TF3DnDNIykFHBG46tnumkgjgPdbku7bnHO4WYf/2noyVnI3FKIZCJo8fN23kv5FLEYCAFrwBoR7rWbNarviFPAzSHvtLUMJCjVDLYz0zCQciEjZIvCGv/j6eZBYOnmxZp9DzkdP6co52y7x2B1D/L2gOC8CHW57Z0DizP9/dF2fefIc3L5KO8Cpl/a+vdtO98oj83HL0DQ+c9rFZ3fP23e5d/nr3rlLPee7Zok4R0kHM6ntw4XvL/jtSmxRNSkzus7f0Yt9N0CIL+I0V1Oyz4t9LzRH03/aJhxpcG3+u4YGZ0BjEMYnEc+32Msfac5J4/dmIe3Hj15bEFwtZT3agU/JS0ftt9f69qHtny0/tTMWfhu/inTg0h4L2P54ff5FTIWfsLy8MO1/tzS/jHvsPSdXAOqp/zotzfCf7Z0Y0G7+sTlznt33X20G862g48+fi89DLxFIyQEaU5pUpooKZOVIs7+2ylNGOsIY6Bo0XJ3c4xjawy5FHIqYBw5RKZxi6Wlp+B9hdFi+uutQwFN3crhJycUAhjquhIW5QLOit9uSRJ7O8aIqzzaOpxLEupMa1SBYRyhiP90COLnPQUJexTCQBhHKi/mxm4mlKKIn3NRCm8dT1+/UNU1zvmZwXykMSuUNmgFYRJG9lwyMSZhFs/SDus9MSi0crha4oAPgwggYoqs1mumIKbUKSZhH8/54MtsnKPfvqGUsLeHGGnXa3LKYmqrwHsvfTOb5OaU8HU9Ww5IKDatZk06eo4hvqNpG5q6FWKzLD6vKUWU0VhnGceAsYrubYu1FucryjSii8LXNckmYoqM3Q5NAwh7+ubpCylnhn5HRkzZh91OTMeriqkfCHqirmuGrp/71lEU9FsBp/VsRi3aYLBeE2OAENHKsHluMLPWeujEv1dphTH2EDKsaZqZjVvM8VNK1HUjWtgQmECILXKmaVfv8d6VRhtw1s2h2UZxb1BFQrTlJOHgtEZp6SsJbacxVjTglff0Xc80DLTrFkqGIu4WuWSUsvM6mnDe4p2w5GcyU0zklNAajLEYq2EOTeZCpGkaCSmnNdNMOtd1b+SiiCkyjYGh72naGm0ddbuS02Hfo62jKMO6qqma5iBIMEbDHPKNymEUoDIZRcpZiO2UxmgDORJSYPX8BazHZs3QdxhtmXIkpEQ9RywwM79AmOfybvvGy/fvNE3N169f5HvSYjKUCvimlvls5Zvsu/4gyMpFoRVzhAIlrhRaMXQ9gxKz/zBO5JwoubBqJEoBWmOsmS0CFNkadBK3iWnoGbvdT1pWr52a3+8fH9I/UsTl0XQRVh3lPTqonoMs9lv4MXC/KRG42sQjL6wzcHB6cL6Jg4u0Rdr0wBb3CMC5dv/qyf3KqVO9A6tr3VI4FhLsDwLqROBxAmKXyjqr/pFD9L33exSkAFf96E8eLkcZrwkyjn9cdOlCHz8ykD+YPlzFQTvyPpaLQpCzPni4juM5dS/jIwfSa8qk83E4vXl6pZyuBr8sLXbgMYfwHQnDwlr2K9MvwqLv6WiIjy1i/mPA6ifTklCKXyC6+eWD8Q+thsOCsiQZXxRJL/3mg7/3g3R+7T8i3Xuv83Q80Th0WzmbcyddeU12cU+YdWMzvMqt80B6GHjv3l5IKVLPMaBTyhTSzNQti3WKaSbuUpQEvqoZpx1pGmk3zxILOwSsdxjvqLKQWu2BtIDjTEoJ5zzTKKzf8l+matqDCbsxlhAl7m9Vi594SsJ2HONO4vNS2H7/LqbBxlI1DYoi/sglUErGe01TrxmdxBtXWs/m1wXvLc5pSlFo6wCJBe59TdO0VHWN9zVQyDmTksSmVlqhlcI4MXHuux7rPGGK9H3HerMhZYR8zWrWzVrAntEY68i58PbyXcx1nRHfY60kbJVSOGup6hozM0lrY6i8p+u6AyFSCBPOi5AhxkyM4UCCh4ISxIy+nsNhTeOAtpacpH+3b1vyQeDhqFzL5tkdiOuGfmQcR56fv4gJ+ziiZxN85zxd39N3HShxA7BGE1PBVtWhj1HgvJd40a2aSdiEqd1YgyoiRNBGEaJo2ysvIc2adgUUVu0GpWG33eK8I4+ZXAqb9Yrdm4DElAvjNDIOo2iHjbDb+6oiTgFlNP12R900oq23whHQdxIjGoRUbxqnOV67AHGtlfiRU2ibllKYzc7BO0/TrrBas3t7wWghxXNO4lX3XY+1hpLi7PNeiRCjSPxxY40IGHKmxIxyjrpuBIB2W6gqnFESe3ucwBhSFHKx7fYVrQ3OaJzOWGOESX8YqKoKaxxD388h6jJ0HVM/UDUNVeUJKaGVotttKdrgrCPnzLR7wzQVdbNGawNZs/ki5t6lBLrujbETV4YcI8M0UDctq80GSkGrCWMtpUDTNKRJSPammDHG4izkUig5YowlAWkKlBRpmopVK+4FY7elaVcUpeh3O6xzs5vJiC8JY72QG44zk3nb0r++4p0T8seZtA3ku35eb0D9ydvs4vAjaYE7GuBUK3mc/0jz9egx67AV3wIy56D3ZEO7t1uc7lLqyvV75ZxuTAt9Ml++gPpK+vGCXXahnA/ve/c22ltZLtDznaIXBA33H7o+Ru9ngGPO+evt+Ux6CM8sYaFrmR9Bu4VHPBseTteqvN6jVy5fyl6Wn/xMw+/MpUO6/NjPrnP5WV6r7pBXylrSdl958rFKPpIW3/uWEPFGOpuHD4PVfzYke+27erCd14R0HwWQv8rX/Vd19z9yGH9NXY98Ww8C5KuDvfTMfxTQvtKEizPLIw+dXb6xJZ7M6werecfnlx37I3PhYeDtvUMzs5hPkWEQRnLnReNnjKHP3exzWaiqGqM1682a7cuEUlA1DSlMM5hyjKPETm7aljS/pJ81vLu3VzGlrWpiEFKvNGuHU4hY41BIKKFh7PF1JZpHo3HO0TY1KSdiDGht0c7hrBXNtAukkAREWcs4jtSNIZWMc9Xsnyxm27ZqsVa6SbTBmZQjzjlyEbC9fXuZ38mKmbcSP3BrLf2uJ8RI3+3oux3tqp1ZnhUxBdqqncM5CTiu6hnQ1xVT3zMNI1opUgiYffzuvWBgtgDYhwnz3rPbvqFQOO+pqpphGGb28hHnPW3bUkqh73YoralXLUYbAclZAFBKidV6hTEW6wVsj/3Av/3b/+T33/8qLOkHbabBWREwFCW7xNgLI7arPBoB11opYhRQ7p0jxMjz1y+4qkKhxO/fSTzzfrfFOydl+ErY6mehSsqJVdNivfiiD0OHMeJuoI0Rq4xcmMaJqmnYvb2htBZhwFbI/XRtiDEKG3nJkBKrpw3e+zl+tsQuH/qO1frpMMf3AqGX798O7NqrdUuYAtMw0A89ZjY3d3Uj89eLmbt1nrZpSCnO31LDMIwY40Tw0LQM48DLn9+wzotmN2fC2M+h0RqqYUQbw5e//BVVilgzbLdoo4XNOwSqytH89a9oY0hhxNovWF/x9rql73YSWi1Fnr58EWFLlgVFa0Xoe4zR+KoixSRCjmk6kJs9/f4XvPcHkjptDKUoYoxU/v/f3pmut4101/qtEQMH2f0luf/bOydJtyWRGGrMjw3JEkVKlCy73QnW87RbJIGqwlSoPa2laZteeBUmKQfJMRN04O7uQAyRthPegJoLKE273VNjZJpHajZ0fS9pmrlinOVwd0+32YAVhvYCbIxlPEjWhDg9M1RDDAFjJN0+lkopFeM83sv1bNuWzX4vXBNFmO5rTqAUKRWRZRt+3PA+XTrWx39exzOP7Yfeh+fMgnNv4eevi4fo1utGyTWcnmdeSFe8mU4NGFW/v+quNpQ+EQ9kk5/V2mu4GFF+s9XlHL1l1H5WFPl6S/bKbV5atA9rrg8Z4E+DN8/2/9GY1a+LeV3E+3xkb+Ltza+05H8ZnlzY/6P4xx35Kw6rfxJ+6bh/uLNrn9vf9fn+hJaeON1OZ+5n3DdPnFkve38WCfn+rVJCOHuVZ/ptXG1495stKL2klEem8cA8Qr/d07YtMU6A1Gd675forcbbXrR1i6RHKzwpJnIpbHd7cimEEGnajhwjx2FAVYkGt32HcY641BaXLOm0zoku8ZevX5kmRy4Vt2w3L6zkKQqzctttlpexEj3gXCTFt1ZCKKRhwrcN2mpqSiKBVTJN47E0mEVOrBZJt31gYqvIGEFIuRTCnkyFEKU2/P7+IBFpLaRH2+2Wvt+QS6ECfdeRYljOX6S1PW0v0bgYI0oboEpadIg0ffcoL6YXI0NrTQizGGdNI9HGUpjDTEWYzLUxYpQ2LSgxjIV8zmGMZ5pGhuNRDJ9ahZ26lW0VmrjU5v7x9Q8UIvW229+QHwwwraklPxKszfOE955+u30knBkOR4bhSNP3aCRFvaTEOIz4pqGkxGGe2e72khKuFDllrLWkLCnD1kr0/3B/T9t0aKuX+0pY9SXK76lUhvsDbdeKk2GJivpWjHxjDdZYCpXtzVchLNNaIu7pe1nDl6//Wq6douRCmGZqrWy2IhOmtcG3DSndMx0HnJMSifF4IMWE8w2Hhck8xsQ4TWgQQkHrqHUgJcV2v5d7XGl2N3uEpT3htabdbZnGkYrir7/+whpN23U0zlJzwmkt7O9zQDcNTftgLMfH6w2KpvEY77DWLNwHBt90pBTISzS832ygVo7394/RkLbrqCWhtMU6L89q4xcde0iTGNUxilxdLVKmkKtaJN7ScmyVqWZ2N19wXjMcDovDznLzx7+Yx5GUM8NxFAbzeZbsioW8zziHUXpxFnUcbu8QLXANSu7tB9mw+TgR4ywOjKah7XqGUkmlimKBquRccE1LmCaG48AwHPHd5uMz6SOuMU3P//idjeKaHq6zep6/gK41ZU+/fecb5sqoy2Wb7KNW3hn8bWv2tzs9H5S4zvKsy3bqk6PFZ3FV4z9opF7ICHl1+8dV1cuv/ln44Ln7wTXr6e7fF6nqaofhj+FSftDT0TxdSl+YF559/SPM2Jfxdxhhn9XnP8uA/E36+MfhvRPob4IPjfdkbjjx9F+ctl5Zl1z6XvHdoK+vanm/D1cb3nMMdEtKt1aVr19viDFhrGaeB6gsTN4Nvm1wVpi04xwpOYsGeAw4rbHGCAuzcxyHgWkYKSlTSiKEQL/Z0vTCMHy8vyPHRNtJ5HkaB9CGWir3d/eM4xHrWioKpYykBmuzpDNrQoh0mx6toFQY5wPzPGOUlvrpnChjpVmcBfMsTOp10UlOKS21wwWjtdT1Gsvh/ht9vyNmkWRSCNGUUjAej+ScJcXaCsu7GDotSltymlCAa1rQmpAqYDBO0sxjjFDF0FdASIG263DWMg0DUwgoaxmOR/Z7MdSkvtuw298QQ+D+/o7heIRa6PoebaQGPk4S6bUPBluccdax2W3xvpHI94KcMrVESi00TUtKWYyblHDGo7WQynnviLGisqLre5z35CSZDXMIhBCJ88Q0HEFL/bDznlrFyC0p0W165mFiHAfaxi81wXL9yhJdlXr3QEmJ0hYaJw6PdJwpQNd3aGUIOQoLekrkpa7fGIMzDt94SspUeNQGN1qJswOkJl8piRiXAsqQJmHzTyniGkcuGa0MxhrGcSSlSNuKxBnqqXetLtrpkGIihhltRSoMkGuVIm7REC+LBn3bdThlyVZq9Xc3X7j9dkcOAb/pscaQYsE4g+96YTM3muFwJIaI0UqI7rqecZokEwSIKT9mJeQUySmRc3mUZnNeWPSts+QQ8Y2n23SiW448E8Zacq6P0l6uSDQ5RJH3c43FOk3JwmEwDQf2f3zFawta+q3F0Pc9CmE1F4J1BbWgVSHPQpLYtg3Oe0rOpBCJNROmCFrR9j0lJY7jxHh3izGG/X7HMM7i+HGaxktWTJwlxX6eJPVcO8s8Hpi1TH8pzMKq7q+eDt/AdYqbL/f6fHyOP/kktXn5jhffXRrBu7v73AXEb2aRfcZwnjkw/pbje4gMfJLVf00bbxynUqflDT+AypmGfoMI+FOcPNiXjru++OP0Y13+PVlY/oLg2Nkhf9r9/LtF91Z8Lv4vX99Tx9Q129d3bP+r8JHxvGcefnehxcl+b+/9kenqHanmjdRVx4zWBtt0NO2D8SaLeCGjMljnl1RXMSBzzSKlpTSpVqxVVBTDMHK8u4VaKbV/jKrXCl3bcVzIjkQWqVKVYjgecT6z//KFWgtaK0JMpHmi1oJzjtY5CpVc6sKEHtDOSVQ2Jtq2W1ilE86YRVYsQ1VsNhtizKhSmaZJZMWmkWmeMUqJprMBVSupiMTSOAiB3KbbUGqh67qllrXy9eu/KLUwTTMlZ+6He4yStHapIaiUFFDGAJZSJJ3ZOUsBVC3SVoFUKtZ7pnmiThMpC8EWKNCGOcxSC64N3jVUW9DWijHnOqiVWrIYiChKlgW1tprW9IQQmOfANAmbfNNIurPIfCXEkBTjsmmEsCqGQCqFWiUtXGktGuNTptZALaLXnJUYxg8Sa7lkrLFoDTlLpLws7VvfEsMsWtKbDVAouZIVqKrYf/1KrRBCkAwAa+ia7tEQdtYyay31+I3HNx6UOECmcUIrwzQe0daw2e5AKYx3zMOwGI+NZBHkiPMOaub+eJBItTZYrRnnmVwLh29/4azHdw1KaZpeosYpCuu6UoZaCzkVYeRuGmKcCcMImw3bmy+SFm090zRDKWhAO0cIA8fDEdsKW7mxIunn2444TzBVqIW262jbnqatjGFmt78BrcTxkDKxRBQwHY9QEm3fY43cA7WIYoDSwsqeQpA0877HWkdVllQmxuNA23fEOZBKJSxGbMmFvm8J80hOhqb5QtM4xmkixcxuv19KIjqJijuPMZKpUFHyTE5B7hs02oi+eymJxrdkKuM0Y3TEOUvNkTBF1HbDFIRAzSowWhwKRlW6tsFZD8YChjDPGNegbUuKM9OcMb4lx2kpiZE5xrnmndPnOdTHZeyHV48fWE+81tvL708bv9agvqZO7Pzg1Zm/zuLT1gXLVVBv5we850pd8xq/pr1PtS0+8ts1l/mag7jU2ZkDfNcxX50i8RzqlU+Xunl1q6cbXDgvz9r40IX9eYb8u5JP/w7nzTkHx9Mp5Covyrmj/E2cIyt+In43I/JX4r3H/r/xXJ1OyOcCBO9vsz62cd05+0hv18uJVTFi08IIboylLHJOtVRyFSkxpQpbBco3pJIx1kJOaGtwrqEUZJEeE9poke7SEvEqOQm7NGL0DseB3f4GrRXeOaZ5pN9Iqja1LPrACUDYmNG4tsM4Q12kn5pGouEpV0IMoimuFFSFdZZpmvFtj2s8qWRAM85HiaI5qYlOssJVnAAAF5VJREFUOWGMaFF3XYe2ls0XI4YsYJxFK83d7TfarsN6YT1PIaI11CW6btoGrSHFSNd2uKV+/Xi4I6UMux3zNOMXAxYqzlqscVQDaamPF51lRYwz8zRQqyww4zyz2Wyppkrdu2/QShFSZDwWcpGI5fF4lNRco5iGCesdOT4Q2ZUlcit1175pJMVCaUKZKEUYuadxpOla4hwWg0rOgVKiv912PTFFjFLkNEkavNYLy3pAw0JkZslZcX93t7DJK4bDgfvDPU3TEOZpYfQ2pJgZBkkbV8s1F7IwQ4VFoq0QY8QYMaqNMVSEmC6lgHOOMM+0fSf+/VoYj4OMdZHoYjne7XYn9ctNy2Yn8lPjcJR0bRSqKuEo2PRopRmPx0XHHqlPV5a0kNWVWpjHgRSF1KvpOmouDMcj9YGY73AnfAcpoa1DaUXTOIbDAW0M3ltKStSc8I3HWieM7lquv7Yak4S93llLDDPjcIQqWvAlF1CQUiEjWvXTcCSEmb7foLRoiGutFvbyhwwAjaZKHbXVkCt+uxUt9DgQwixzRJEyhCmMHO/vRZptI/X5c4y0TbPwC4wiBwfEaaRUkQDTRjgYSo5Y1zCnTE2REgN+s6HpOtqu5Xg4MBwOKBTWO7q2xWjJUMjLCjLEmenuVvTCleb4n3/SbDrCNGG1pmkcShtSUcRqpLylfGAGPZ0neTJdq+ffw/cfL9kVP5bi+SMRubeSrn60nae/P1tZf7D96/dTLPnY3784+f2JvfTyXf5olFzX44Xo4Ul/T7d9/ObNDj4p4vqUdv7p/fjmMT5LwHseRDnd8Zxx+p7hX+M0uDCsV/c5c13PDetsIF+dv3Knz/Zr4348VX9HMP3SWvK1FMqfHVS83lv4CY2+A79ZcsOKFStew2tBgesf4of11+VgxedMCNcb3saIZNdwJE0zm01PLQqURms4HgaUNuxv9lhjGUeJVivb0DY9Wk3M00yYZ3a7PcooIQ6zlq7rGOcJhSLlwjyPEgXUBqUV3jeUnIVEy4NTEjHPMaJ1xVovabJFFlhaG4zJIidUM2EOlFKFJMpaKnUh56psdg2lJL59u8U17SKTtETUtcEb0Y7uNptFnqhQk4zFWgfUpca5ojY9qlRKisQQ6fpeyLWQVONpGslZSJ+sbxcmdiE0a/sNxjpJy86JTmuRhIqRUgpt04r+8FJLiwLvmoXkSktE1OjFgazxS32rMYZS2uUcyLaPdepF1hJhmig547xns92ildQRhzBjjCEVYcuGSqlVNLKthQJoRdM2j5raeZI07pzyksqc6PpeUpNj5ng44rynXUoJUhByrTDO0AuXcZyDsMIbYRBXSpwet/d/LQZhRaOgSgbAQyS7LtF5rTXlODCHiVrsEsFtaFpP41uaRo6tAjlL1kTfdRjvsc5KartvyFWM+Fqh8R5rRSNbGNWRqH2W440piVzWHGApVciuYBRMCymadVJj7X2DaxqmYRBNdGvIuXLz5SvVGDabLSlGYVF3li9/fBVHgxWNdOu8GNqIYVxiwVpEMqzTi4Os4rzDB8c8TUK81raL86tIyUatWGfZbLYYa/jrz/8kx4z3jma7oTeWFEZc09Lt9sI7UCs5TbS7naSBUwnzDBVs4yWDQht2X79SYmQOE844xsNAKSLt5hbH1G6/Yxgcx7uDXEeg7fulBOVIDkKm6NoOrTRziBjryFXTb3dYawhTwFgvRIcpYRDHiciGbWnalnGc6DctxjdYrfDOYL1ouP/5X39iraHtGlr745PqaYr51ZHUi/P62xN+fbbd6d9v9Lv8+3ZU/DPxylhfWCWXjv/pfqchye/fvzSVnvz1NMx3VtdJPTdGlwwl+eny9Xg2+rc3P8HT433PNXh+nl58WiKLzw75UlPnzsUZw/X1Rk6aPLvtDy5mnhj831uqTz6ol3fJ84t/sW/14o8z3Z428dolq883ffblRU3BN7AMRL3Vzrs8Hr8qNva9l/fd6ZduxNNtVov5o3j2zLzLMfdj/f3qK/ai3+W2Ofdm+Wl9/kI86/ul7/Vqn+XnDuQz9jm9Yq95Zi99Xy87RX+C9/FqwzvMEedFw9nksrAuV6ZZjIq2bVDainZwHkQqyzXUIhrSYrQVqMJYXHKmVmHoDjEsaeyBeRJ2Zu89KQVqikSlOR4PGG2w1jwSNnWbnhwT8xxwTQMqk2IgaElhDfMsUa0YaNpWIrmlQKkyvqXW2WooOZFTAl3ZbDboRXN4OB4oOUtdtJVIcMwZXQpzTGLMOys65UXk1bSWyGlKiRBEwstayzyOcv6MIaVMzBmrxVgep4nNxrLZ7ZjGkePxjnmY2O73KODu/hal9EIEJkzSFdDGUmqmazwa/XjnVF8XtvRKVZKW7ZxkKrSt6CqnJOm7DzJaOUktvnYik6VYDMgUH4nLUoqkEEleam2ttmgjNdI5Cxna7V/fyDmJ4W8tKE2MgWmaOdx9k3p0Jczr4zjSNq2QxM0zx/uZtmvpNz21VGpF7oOljCBMozCPl8rxcAAtLPYpRcZRpOWMFfK9frNZSPky1jfioDCGlKSOXwNYtWh7C7u5c55xOJBCQFsr0WYtMm/aGEAzzUHI+4wWeTLrSDUxhCPeCZP7cRzolLD1S2aG1KrXRWe+xkgoFVUrh+ORvm1p2kYitzkzHI7keUKrhrQ4SMYQAUWTK5WKb71kJNTKNAyEEBf+g4w1Ba8gBkl5N9biGy8EZnffcFax//f/IIXIOAqrfrfpCeOI1Zo0jmRroRSm4xHXOMZhAio1Brb7PWhonBdugMM9AClnIThUiqZr2TghopunicO3GWsseZEMDDFRELK38SDEe857UgXnPCUmpmEiRY3dbonDTMqF4/FA03h2X76ijCbOEykGjJKU/aoNVotUmG0aPBrnGmpNYGW8FeGmsE7jVCXFxGDip02sb73ELuLiBt+Np3N9/ZrF8hn8tFVKPfmz8kAU+D1cufy8rA5f9VKrl+6Js309W88/tZbUyTaXDvx7Ay/7OudQuDSq167oawuNl86HZz+p89u+6up4MZQz/T9ckmeL83M3xFvHekWo+9kmioti2A/MnueaVKdn7vLi7OHuenBiqLfGWZ/sp9QyvO8emItK2e99fl4c2gWD843J4WJdfP075pY3VuWVi7QCz4/+B0d9cYH/gUnuqVf1rPPpEybOc/f6sznycQQrXsOVl+P9tuN7rvMvdBy940A+fVT18n35cWP/9ef+xePx5KDOOkUft3ltJK85Ac/jasN7PN6TkxcmYuuY5/gYbX1gfs5ZaqbrYlR3XYumEuZJmI5TfoyKiRTZnpgi8zBinUgY+bal3d9QcoTiabtG9J8Xhm+toChwXQtVkUohhIAZR6xzxDDjrETnS5EIcde1QgAVAtMij9Q0QtykagVt6boNxjcYrRb95Aq1LkRZFWcNMQWpubWOfrMhhknYt7PCGQNKLyRTFWUMd7e3dP1GCL/mwHb/hVoL8zCgrEZrS1UI0ZQRnfBSCtYIIVf3tRemdIWkQNeKtlZS0ZmIOeEbR05SH5uL6KjPcxBnwcK607UdUKCAsxbT+OVdUBeZLGF5fpCOMtZglBa296wZh5EUE8YYuqZFL/JeRilCmHF4vJf/ShbnQ9M0tH1HKUibxtC1nrb5D6wVDfYHtnelYL/bc19u6fsOaw3GedQiEVaLleiytThvpTbdaNEANwatFG3bMhyO/Pn//x9//Pu/0/e91O16T4xBDOuypHbXilm4CGKYJc2/VsI4Ss16iKQYsAvBHohOuMjASflC0zaUnPCuBZYyicXZ4r2j8VKn7xq57463d1KHXCspJw7398SUKUn6KwViRTIqcqFkITcT4jIxuOMocmDGi/Fas0T652lcSNw0t9++oWpZ6pz3dF0rLPRbkUub5gldszhwlMUY2O0layCmQrvZUXNGK1BKy70yz0zjRI4B5wxu0z/eL9ZahsMR33WoWiVDxcn1rTmjvUcZSyoVrS05SyS/aRtqTsyHAymnhc29MN7fMwwD1nsqPPILpJTxTQPzgDeKtvHEacA3Pd5blOrJYSRMQeYYpTFaEQaROOw3PUYjZSZK0tKbnIiTwRiFMpYwTVdPnG/hPZxT71siPjUgT42Bt/u49IqoT/54Ldp3yUZ603Q6GcDjx7Nv2Etn5PUzdfll/T1s89Kjfc5AvoBLBt5rLZwa8qfn7Ls99lbnz/t5cQJPv78wznfYuK+uNx6Gc0YM+vwlrVc+EOfO5RXG+Bu/P+Y+PLu5K/LsXHKIPP361Oh/bRl64tx4dunOtH+GHU++ef1ifbqf65cZZy9noasNfPUw470c5+ct3i92/rzhS40/+/1t187n4RNMo7Nz5C8ay5kT9PJUv6Pdn3/C34HzZt35of0OA/7Aybv4Lv+E4/lIE4/jee4qfTbrXFzsnMzhn3xNrja8nRO9YWsdOWemaZTomLd0bYvSCirkUjC+xXpJgzVWyJKc8xifsdqilGxXawU0Td/jrUdFzThNHIcDfdeTgXEKwpbeCvFRyQnn7GIUV6ZxoGkbWMjS2q4jlUIOgbbrUcbQei+pxbWitaKUyv3hSEiJrm3wS1rxdDjgnV3YsxO3S91xzoVxivjGLRJVLNFjkd9SSjGOA7d3txht6LoNFTHW52lkVop5Gtl//QNV4TAM3NzckFMGLWniMSXsEp2tNaGrGOWlSD1vzkISNd7eYr2nbTxhnqUmvPHkIjJuqopzwlgradOlMo4jKIQNO0W0lihpKYWKIsXINI0LUZaQYFUFORUCQszVdg3W2CXCv0isVUQnPIkesncO37b869/+TSLdtTItTgBjDP12S5gCx2FYmMQjxlicE8Pat3JufdMCSK1+KeRaadqeUkUmbhoPC4O+J6SEXmr9q0KkyLTGLvrZ0zRRamW725HmmRCCRFvnmaAn5hCwzpNTxPuG6XAvRpvRj9tR5Z5p+455moT1O2eJqBvLNIzEGDBWyh1yLeRSSLngqMt4hBiNnCizPPCGQl3S+ufhwP23P7HOcnNzgwJ8v8E1noximCJKGdq+o+07pmlanh9J7Z5jZjgcmMcjfddineH+7h7jHOPxyGa3I5WC1hpMyxAmbD7SdA2NbzgOw0Ls53CNx1iJvB/u7lBKc3t7hzMK3+yxTSfnMQZq0RgtjjFtjWRM+IYSouhjt6Ju4IwQ8ZXS4bylaTvGcWT7ZUeJiZQzORdKijircd6KOkCVtUstmYqhoun3N/TbDSkEwhyoWpx/xyEtdeSKskjAzSFgjSGMA912s2QPVHKuxFzZ3NxAKiSt2LTbT5hSz4SPXiy6H14EJ5GQenan73ix7vvxWNT7fbUXGvgI3orEPXRxrg/18s/6cpX2OpZzfnbTs5Gp9+GtIVxtbFzo/7WlkTrd6sGouqLDF+O68ly+vJfOOAU+dL+8PWh1+uHUoXHhwL8v5l9p+9LP5354chJejOnln0/wvK2LzqOzW7zW4ukC8hSXL0j94cnhYstnPp/OmaeOju8frrlmvw/e8sC+8UBca/+cmyTP3XOfOUe+ueNnb/n+rd/V0jscTx8dxU97pN7R9+mHt4x/BW/fhxcP6Mr78u29rsPTcZ7c9D+2xnlt7/e3fLXh3fUbYkpoY5jGkZwizgkBlXYWrUXqS+quRSpsngOxBhSK43Ck7zeglEQYFbimIadErYpxHoUM6nDAOc+kRnzTAZVaJWqutaFSKEWz3e2FrVgbfNNI5BqJUOeUiPPEPE90my2UQpgnSUn3nrZtGKcRhdROxywyZiBM0FobjqOQTu12W3K2xHkkzBLt3t/cLCzfYIyFJWX+y9c/HgmsVIViDWmeKUmI4EoSbXFFZZ4naqn8dX8rYwTiEtV3TiSrGtfJsSQxXJ21TOOReAwYvaPkgtJ6SRMX0iiUwjlPXdL6tVY467i9/Ubbt9hqATFU7u7umMcZY0R6TauHtO3EHALFZggVlKbrOv766y800HU9w3AkpUTJBWsNjfcYJ/XYpVS8thJdVYpxGun6DdM48d9//jdGGfY3O8ZhEhZ21ZFyktR7pTkcIkZp2n5DLpnxeBSDUWvqIn8VY2Sag9TWG9EJ3+9vKDULmVmp3N1+o+t6Sskcl2s1TTMpRfq+lwdgkWgTKZrKOE5sNhv6fsPd3R1hDtzf3XFzs8f5hmEYCTFijeHPP/9iu93y7c//xhiD1lJuoGYldeJak50X4jbr2CxEcJJZoDG+YZol/do5g6bQb3e0nVx3rRSHw5HxeGQeB4zWwIaYMgDHwz3dZkuKieP9Pd4bdNfgm4am64GJMA3M44FN3xFqxllHSSK3Jen2ipgicZpw3uK9x3hPCIn7uwOqFtrG0Xq5P8I8MYelXtzK9gEWLgWRqhuGgbZtiDHz53/9F03jaduO7d6itGWepkepuzEGNtstKiZKmcVhgyXlSioIr0EKNH0nrPlKkWPh27c7KdmYRg6He4w1aAUBKZdouk4yDBZpuabrlqDb4owpheH+joqQ1g2HA5v9/t0T6Dk8i5+9Mie/+Omt6MmZza9el11q8omBcl3s8Pv2l9p/bQBPA7/15LdXdntzTOrk/xc3/Am41p48t+Z9NdL9lmV90SC8YjDvwaXF+oV+zg7t7wjiXNvnRx6iz+r7B/F06Of+frbhK2Oqj/+c3JO/wEL4WV2cGjnvuSTvtU9fHcA72/mUvn9ClO6jeM/5f+3Yz/72u0S1P+ei/fPxux77tY6DXzT+d8iJ+UdjwBiNMc2ipZtJIaG1SGtls8j3GKk1DTHT9b2QKi0aysM0Yo1dUn1FvqhS6fqN1H0rgwKmcaBvG5RajGvEqB6nSQjNakVb0d6WVG9FYw2bjScsrM7qoZ70MeItxqXxDmsMaAWZR53mWoVJfLf7Qtv2aKUkLVwJE7JemKQTomltFskoaAkpkWuGUsQxYT39ZksB5nGkaZrH2mZj1JK+LYayNuKsiDGitBLJs1KoOZFjlBpd7/ny5Q9iCmhlaBZDIaWIbRZjW2nGccQ6iRg67+na7jFl2hip7W68x1lL8Zm+69FGtM+11lhrsLZfMhUc0zQKs/tCdNd1LXOMIgdmNF3XL6UGhVBE4iukyHA4YKxlt7+RWvGc8dYSU8IYS9eLNrnSmge5uL7rUUjqdgwTMSbiPNN0EgWPMVCyJhXRVR/DyH5zQ9t0mEWrfByHhaG9yjWFRRudR43tWoUobhyF/b1ftM6VVqLzXUEtZRR1IVk7Ho8459nu9hilUMORHANfvn6laVohzwuBuLC1e+/FwYJwCriFQOzu/h5yBm25udkzDgO+3dD2O4qGpuvIuSxEZondfsdu0zOFmRhntILGt7Rtg6bS9R05RbrtFkoWx1jONG2L957NbofWRgxtY0Rf3jekHIlzoKZEv9mKbJ+COM/c/vknZSHz801Ht7shhsima4g5y7YpM8+Sqq8QJ4ZfuBRSiChdyWHmMB7BOnabDc5B2/ZM0/DIIl9LWTIsspQYaGCeIEm0WuTl4pK9MaDRlBSw2w39dkupIm9WKqANRoFBYX0LykBKzCHS9R2lVqhCLpdSQqnCcBgXg/7HU83FOfoeE3b5/I7V5xW2z6tdqwsNqJPPp0u3098/gqdtfIad82x8r+3wUdf6FevXq07JW4GvDzT8VibAR9o82/YHrvnfvv76TCP5bz+Yc3inufoej9RJsLk++f/POxUn4zjb0cdM9HeN+WQBfnbfV+bIczu997xdiky+D694xd6722fM+VdueLrttQ7dH+v4R/DOq3vOuXXy208Z948+vP+UOfLa9/zZxc2P49rTrGp9c5W4YsWKFStWrFixYsWKFStWrPgg9N89gBUrVqxYsWLFihUrVqxYseJ/M1bDe8WKFStWrFixYsWKFStWrPiJWA3vFStWrFixYsWKFStWrFix4idiNbxXrFixYsWKFStWrFixYsWKn4jV8F6xYsWKFStWrFixYsWKFSt+IlbDe8WKFStWrFixYsWKFStWrPiJWA3vFStWrFixYsWKFStWrFix4idiNbxXrFixYsWKFStWrFixYsWKn4jV8F6xYsWKFStWrFixYsWKFSt+Iv4HgyqcDKcbt94AAAAASUVORK5CYII=\n", + "text/plain": [ + "
    " + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# Display the results\n", + "display_product_images(multimodal_matches)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "cleanup_md" + }, + "source": [ + "---\n", + "\n", + "## Cleaning Up\n", + "\n", + "To clean up all Google Cloud resources used in this project, you can [delete the Google Cloud project](https://cloud.google.com/resource-manager/docs/creating-managing-projects#shutting_down_projects) you used for the tutorial.\n", + "\n", + "Otherwise, you can delete the individual resources you created in this tutorial:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "cleanup_code" + }, + "outputs": [], + "source": [ + "# Delete the BigQuery table\n", + "! bq rm --table -f cymbal_pets.products\n", + "\n", + "# Delete the remote models\n", + "! bq rm --model -f cymbal_pets.text_embedding_model\n", + "! bq rm --model -f cymbal_pets.mm_embedding_model\n", + "\n", + "# Delete the remote connection\n", + "! bq rm --connection --project_id=$PROJECT_ID --location=us cymbal_conn\n", + "\n", + "# Delete the BigQuery dataset\n", + "! bq rm -r -f $PROJECT_ID:cymbal_pets" + ] + } + ], + "metadata": { + "colab": { + "name": "bigquery_embeddings_vector_search.ipynb", + "toc_visible": true + }, + "kernelspec": { + "display_name": "Python 3", + "name": "python3" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} diff --git a/gemini/use-cases/applying-llms-to-data/bigquery_generative_ai_intro.ipynb b/gemini/use-cases/applying-llms-to-data/bigquery_generative_ai_intro.ipynb new file mode 100644 index 0000000..73d63c1 --- /dev/null +++ b/gemini/use-cases/applying-llms-to-data/bigquery_generative_ai_intro.ipynb @@ -0,0 +1,6907 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "cMQCs0oQf5Jo" + }, + "outputs": [], + "source": [ + "# Copyright 2026 Google LLC\n", + "#\n", + "# Licensed under the Apache License, Version 2.0 (the \"License\");\n", + "# you may not use this file except in compliance with the License.\n", + "# You may obtain a copy of the License at\n", + "#\n", + "# https://www.apache.org/licenses/LICENSE-2.0\n", + "#\n", + "# Unless required by applicable law or agreed to in writing, software\n", + "# distributed under the License is distributed on an \"AS IS\" BASIS,\n", + "# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n", + "# See the License for the specific language governing permissions and\n", + "# limitations under the License." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "iR0jdheRGG89" + }, + "source": [ + "# Introduction to Generative AI functions in BigQuery" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "td9kx9LVgSve" + }, + "source": [ + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
    \n", + " \n", + " \"Google
    Open in Colab\n", + "
    \n", + "
    \n", + " \n", + " \"Google
    Open in Colab Enterprise\n", + "
    \n", + "
    \n", + " \n", + " \"Vertex
    Open in Vertex AI Workbench\n", + "
    \n", + "
    \n", + " \n", + " \"BigQuery
    Open in BigQuery Studio\n", + "
    \n", + "
    \n", + " \n", + " \"GitHub
    View on GitHub\n", + "
    \n", + "
    \n", + "\n", + "
    \n", + "\n", + "Share to:\n", + "\n", + "\n", + " \"LinkedIn\n", + "\n", + "\n", + "\n", + " \"Bluesky\n", + "\n", + "\n", + "\n", + " \"X\n", + "\n", + "\n", + "\n", + " \"Reddit\n", + "\n", + "\n", + "\n", + " \"Facebook\n", + "" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "5h3O5b6P8WEx" + }, + "source": [ + "| Author |\n", + "| --- |\n", + "| [Alicia Williams](https://github.com/aliciawilliams) |" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "intro_md" + }, + "source": [ + "## Overview\n", + "\n", + "This tutorial will guide you through powerful generative AI capabilities available in BigQuery. You'll get hands-on experience using the suite of generative **`AI.*` functions** that integrate directly with powerful models like Gemini. This allows you to perform sophisticated AI-driven analysis on your data right within your familiar SQL environment.\n", + "\n", + "While the examples in this notebook will focus on **text inputs** to the generative AI models, many of these capabilities extend to **multi-modal analysis**." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "SwQLISjqGTHd" + }, + "source": [ + "### Objectives\n", + "\n", + "We'll cover how to:\n", + "\n", + "* **Prompt models to generate text and structured data** with `AI.GENERATE`, `AI.GENERATE_TABLE`, and `AI.GENERATE_TEXT`.\n", + "* **Perform powerful, row-level analysis** using scalar functions like `AI.GENERATE_BOOL`, `AI.GENERATE_DOUBLE`, and `AI.GENERATE_INT`.\n", + "* **Forecast future trends** with time-series data using `AI.FORECAST`.\n", + "\n", + "**Note**: This notebook focuses on the [general-purpose AI functions](https://docs.cloud.google.com/bigquery/docs/generative-ai-overview#general_purpose_ai) available in BigQuery. For more information on the [managed AI functions](https://docs.cloud.google.com/bigquery/docs/generative-ai-overview#managed_ai_functions) such as `AI.IF`, `AI.SCORE` and `AI.CLASSIFY`, please head to [Semantic Analysis in BigQuery with AI Functions](https://github.com/GoogleCloudPlatform/generative-ai/blob/main/gemini/use-cases/applying-llms-to-data/bigquery_ai_operators.ipynb)." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "costs_md" + }, + "source": [ + "### Services and Costs\n", + "\n", + "This tutorial uses the following billable components of Google Cloud:\n", + "\n", + "* **BigQuery**: [Pricing](https://cloud.google.com/bigquery/pricing)\n", + "\n", + "* **BigQuery ML**: [Pricing](https://cloud.google.com/bigquery/pricing#bqml)\n", + "\n", + "* **Vertex AI**: [Pricing](https://cloud.google.com/vertex-ai/generative-ai/pricing)\n", + "\n", + "You can use the [Pricing Calculator](https://cloud.google.com/products/calculator) to generate a cost estimate based on your projected usage." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "nkoCFoFVSPii" + }, + "source": [ + "---\n", + "\n", + "## Before you begin" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "setup_md_1" + }, + "source": [ + "### Set up your Google Cloud project\n", + "**The following steps are required, regardless of your notebook environment.**\n", + "\n", + "1. [Select or create a Google Cloud project](https://console.cloud.google.com/cloud-resource-manager). When you first create an account, you get a $300 free credit towards your compute/storage costs.\n", + "\n", + "2. [Make sure that billing is enabled for your project](https://cloud.google.com/billing/docs/how-to/modify-project).\n", + "\n", + "3. [Enable the BigQuery, BigQuery Connection, and Vertex AI APIs](https://console.cloud.google.com/flows/enableapi?apiid=bigquery.googleapis.com,bigqueryconnection.googleapis.com,aiplatform.googleapis.com).\n", + "\n", + "4. If you are running this notebook locally, you need to install the [Cloud SDK](https://cloud.google.com/sdk)." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "YQ3g-h7uTaSf" + }, + "source": [ + "### Set your project ID" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "set_project_id" + }, + "outputs": [], + "source": [ + "PROJECT_ID = \"\" # @param {type:\"string\"}\n", + "\n", + "# Set the project id\n", + "! gcloud config set project {PROJECT_ID}" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "auth_md" + }, + "source": [ + "### Authenticate to your Google Cloud account\n", + "\n", + "Depending on your Jupyter environment, you may have to manually authenticate. Follow the relevant instructions below." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "V6NjZRCXU5Ro" + }, + "source": [ + "**1. Colab Enterprise or BigQuery Studio Notebooks**\n", + "* Do nothing as you are already authenticated." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "l0dV1hvAU1ed" + }, + "source": [ + "**2. Colab, uncomment and run:**" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "auth_code" + }, + "outputs": [], + "source": [ + "from google.colab import auth\n", + "\n", + "auth.authenticate_user()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "4Pp4LAJ3UyRP" + }, + "source": [ + "**3. Local JupyterLab instance, uncomment and run:**\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "AzU7S3fMVDkW" + }, + "outputs": [], + "source": [ + "# ! gcloud auth login" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "conn_md" + }, + "source": [ + "### Create BigQuery Cloud resource connection\n", + "\n", + "You will need to create a [Cloud resource connection](https://cloud.google.com/bigquery/docs/create-cloud-resource-connection) to enable BigQuery to interact with Vertex AI services." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "conn_code" + }, + "outputs": [], + "source": [ + "!bq mk --connection --location=us \\\n", + " --connection_type=CLOUD_RESOURCE test_connection" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "perms_md" + }, + "source": [ + "### Set permissions for Service Account\n", + "\n", + "The resource connection service account requires certain project-level permissions to interact with Vertex AI." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "6OqpZNY953xR" + }, + "outputs": [], + "source": [ + "SERVICE_ACCT = !bq show --format=prettyjson --connection us.test_connection | grep \"serviceAccountId\" | cut -d '\"' -f 4\n", + "SERVICE_ACCT_EMAIL = SERVICE_ACCT[-1]\n", + "print(SERVICE_ACCT_EMAIL)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "perms_code" + }, + "outputs": [], + "source": [ + "import time\n", + "\n", + "!gcloud projects add-iam-policy-binding --format=none $PROJECT_ID --member=serviceAccount:$SERVICE_ACCT_EMAIL --role='roles/bigquery.connectionUser'\n", + "!gcloud projects add-iam-policy-binding --format=none $PROJECT_ID --member=serviceAccount:$SERVICE_ACCT_EMAIL --role='roles/aiplatform.user'\n", + "\n", + "# wait 60 seconds, give IAM updates time to propagate, otherwise, following cells will fail\n", + "time.sleep(60)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "generate_md_1" + }, + "source": [ + "---\n", + "\n", + "## Generate text and structured data with `AI.GENERATE` and `AI.GENERATE_TABLE`\n", + "\n", + "These functions allow you to leverage the power of large language models (LLMs) directly within BigQuery to generate new text content, including creating content with a specified schema. Both functions work by sending requests to your choice of a [generally available](https://cloud.google.com/vertex-ai/generative-ai/docs/models#generally_available_models) or [preview](https://cloud.google.com/vertex-ai/generative-ai/docs/models#preview_models) Gemini model, and then returning that model's response." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "generate_text_md" + }, + "source": [ + "### Using `AI.GENERATE`: Extract keywords from reviews\n", + "\n", + "Let's use the [`AI.GENERATE`](https://cloud.google.com/bigquery/docs/reference/standard-sql/bigqueryml-syntax-ai-generate) function to extract keywords from the movie reviews in the `bigquery-public-data.imdb.reviews` table." + ] + }, + { + "cell_type": "code", + "execution_count": 44, + "metadata": { + "id": "generate_text_code" + }, + "outputs": [ + { + "data": { + "text/plain": [ + "Query is running: 0%| |" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + "Downloading: 0%| |" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/vnd.google.colaboratory.intrinsic+json": { + "summary": "{\n \"name\": \"get_ipython()\",\n \"rows\": 5,\n \"fields\": [\n {\n \"column\": \"review\",\n \"properties\": {\n \"dtype\": \"string\",\n \"num_unique_values\": 5,\n \"samples\": [\n \"Remove yourself from the Kirk Douglass aspects of the casting. It is essential to your enjoying the film. There is a beautiful young woman playing double roles - and in the photos from the 1800's, I can't believe how smooth and white her skin is. Also, there is an excellent degrading of the film stock which chills the mind if you like faded greys and yellows as I do. This film is played on TNT from time to time so see it.\",\n \"Jules Verne wrote about 80 novels as well as plays and short stories in his career. He began writing in 1854 with a short story called \\\"Master Zacharias, or the Clockmaker's Soul\\\". It was the first time he talked of the negative side of progress - the evil that results from some discoveries or inventions when they fall into the wrong hands. This becomes a running theme in his novels: Captain Nemo in TWENTY THOUSAND LEAGUES UNDER THE SEA, or Robur (from ROBUR THE CONQUEROR and it's sequel, THE MASTER OF THE WORLD) are two of his best examples of this them. Kongre, in THE LIGHTHOUSE AT THE EDGE OF THE WORLD, is another.Verne was so prolific that when he died in 1905 he left a dozen unpublished novels and stories that were not fully published until 1910. They include some of his best writing, such as THE BARSAC MISSION (partly written by Verne's son Michael), THE SURVIVORS OF THE \\\"JONATHAN\\\", THE PURSUIT OF THE METEOR, THE DANUBE PILOT. All of these dealt with science, but also dealt with political systems, and economics, for Verne was interested in all the problems facing modern man. THE LIGHTHOUSE AT THE EDGE OF THE WORLD was the last novel that was published in Verne's lifetime. It does not deal with the political questions or economic ones that perplexed him, but seems to go back to his potboiler period, when he was turning out stories for money while considering better stories for later publication. But nothing Verne wrote is without interest. Rereading THE LIGHTHOUSE one sees what the subtle point is in it. It is the study of how the ego of a villain can prevent him from escaping retribution.Kongre (renamed Jonathan Kongre) is one of the last pirates in the world of 1900. He and his gang find a damaged boat and repair it. They sail it across the Pacific, and reach Staten Island, a small island in the Straits of Magellan controlled by Chile. There they find a lighthouse with a crew of three men. They manage to kill two of them, but the third one (named Vasquez - he's from Chile, remember), hides on the island. Kongre and his men decide that they should prepare to leave the island shortly, before the Chilean Naval relief boat returns in three months to pick up the lighthouse crew. But first they will wreck any boat that comes to the passage, and increase their ill-gotten gains. But the key to the novel (and it is not in the movie) is that Kongre's right hand men (Carcante and Vargas) keep urging him to pack up his supplies and wealth and head to Asia where the money can be divvied up and everyone separate in safety. And each time Kongre won't do it. Initially it is pure greed. He wrecks a boat, and massacres the crew (a scene that is done in the film). The sole survivor is an American, John Davis (the name became Denton in the film, except that it was given to the character of Vasquez). Now with an ally (and not a drunken one, as in the film), Vasquez starts sabotaging Kongre's activities on the island. Carcante keeps suggesting leaving, but Kongre (unused to someone annoying him successfully) keeps delaying in order to catch Vasquez and Davis. The end result is that when he thinks he has them cornered, the Chilean boat appears to sink his craft, kill most of his crew, and confront him. Kongre commits suicide to avoid capture.Much of the mayhem of the movie (with Denton picking off crew members one at a time) is not in the book. Nor is there any female character in the novel (a rarity in most of Verne's stories - he could be quite a feminist when he wished). The egotism of \\\"Jonathan\\\" Kongre is well shown by Yul Brynner's performance, but the subtlety of that trait is lost. The writers presumably did not think the audience could appreciate it. Kirk Douglas does well enough as Denton, but his singlehanded success (Vasquez and Davis work together well to the end of the story, unlike Denton's ally who is killed by the pirates) seems unlikely. The bestiality of the pirates is well shown in the movie, particularly a singularly tall actor who in one scene wears women's clothing to particularly unsettling effect. The film is not a bad minor adventure film, but it could have been better if they had stuck to Verne's theme.\",\n \"Surprising that Jules Verne would write such a story. Even more surprising that Hollywood would produce it. Yul Brynner is unbelievably good as a man freed of all bounds of convention, free to indulge his taste for cruelty and domination. Douglass is an excellent counterpoint, a courageous individual who's chosen simple solitude as a way to deal with the complications and turmoil society had imposed on him. And Samantha Egger's character is the capper, a woman willing to sacrifice far too much for comfort and safety. Inotherwords, everyman. The movie does show its age and is limited by the conventions of time and place and technology of the time. If you're looking for special effects and action as substitutes for thought, look elsewhere.\"\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"keywords\",\n \"properties\": {\n \"dtype\": \"string\",\n \"num_unique_values\": 5,\n \"samples\": [\n \"Here are the keywords from the text:\\n\\n* Film\\n* Casting\\n* Kirk Douglass\\n* Double roles\\n* 1800s\\n* Film stock degradation\\n* Faded colors (greys and yellows)\\n* TNT\",\n \"Here are the keywords extracted from the text:\\n\\n* Jules Verne\\n* Novels\\n* Short stories\\n* Plays\\n* \\\"Master Zacharias, or the Clockmaker's Soul\\\"\\n* Negative side of progress\\n* Discoveries\\n* Inventions\\n* Captain Nemo\\n* TWENTY THOUSAND LEAGUES UNDER THE SEA\\n* Robur\\n* ROBUR THE CONQUEROR\\n* THE MASTER OF THE WORLD\\n* Kongre (Jonathan Kongre)\\n* THE LIGHTHOUSE AT THE EDGE OF THE WORLD\\n* Science\\n* Political systems\\n* Economics\\n* Ego of a villain\\n* Retribution\\n* Pirates\\n* Staten Island\\n* Straits of Magellan\\n* Chile\\n* Lighthouse\\n* Vasquez\\n* John Davis\\n* Carcante\\n* Vargas\\n* Movie adaptation (of The Lighthouse)\\n* Yul Brynner\\n* Kirk Douglas\",\n \"Here are the keywords from the text:\\n\\n* Jules Verne\\n* Hollywood\\n* Yul Brynner\\n* Douglass\\n* Samantha Egger\\n* Cruelty\\n* Domination\\n* Solitude\\n* Sacrifice\\n* Convention\\n* Everyman\\n* Movie\\n* Age (of movie)\\n* Special effects\\n* Action\\n* Thought\\n* Technology\"\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n }\n ]\n}", + "type": "dataframe" + }, + "text/html": [ + "\n", + "
    \n", + "
    \n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
    reviewkeywords
    0The Light At The Edge of the World marks Kirk ...Here are the keywords extracted from the text:...
    1Remove yourself from the Kirk Douglass aspects...Here are the keywords from the text:\\n\\n* Fi...
    2Surprising that Jules Verne would write such a...Here are the keywords from the text:\\n\\n* Ju...
    3it is a quite slow pace face-to-face Brynner/D...Here are the keywords extracted from the text:...
    4Jules Verne wrote about 80 novels as well as p...Here are the keywords extracted from the text:...
    \n", + "
    \n", + "
    \n", + "\n", + "
    \n", + " \n", + "\n", + " \n", + "\n", + " \n", + "
    \n", + "\n", + "\n", + "
    \n", + " \n", + "\n", + "\n", + "\n", + " \n", + "
    \n", + "\n", + "
    \n", + "
    \n" + ], + "text/plain": [ + " review \\\n", + "0 The Light At The Edge of the World marks Kirk ... \n", + "1 Remove yourself from the Kirk Douglass aspects... \n", + "2 Surprising that Jules Verne would write such a... \n", + "3 it is a quite slow pace face-to-face Brynner/D... \n", + "4 Jules Verne wrote about 80 novels as well as p... \n", + "\n", + " keywords \n", + "0 Here are the keywords extracted from the text:... \n", + "1 Here are the keywords from the text:\\n\\n* Fi... \n", + "2 Here are the keywords from the text:\\n\\n* Ju... \n", + "3 Here are the keywords extracted from the text:... \n", + "4 Here are the keywords extracted from the text:... " + ] + }, + "execution_count": 44, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "%%bigquery --project {PROJECT_ID}\n", + "\n", + "SELECT\n", + " review,\n", + " AI.GENERATE(('Extract the keywords from the text below: ', review),\n", + " connection_id => 'us.test_connection',\n", + " endpoint => 'gemini-2.5-flash').result AS keywords\n", + "FROM\n", + " `bigquery-public-data.imdb.reviews`\n", + "WHERE movie_id = \"tt0067345\"\n", + "LIMIT 5\n", + ";" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "ZiVKjzbVcZ2x" + }, + "source": [ + "You can see that BigQuery has prompted the specified model for each row of the data and returned the response." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "Ulkyyt6pUpfQ" + }, + "source": [ + "#### Using arguments to adjust model configuration\n", + "In addition to the `prompt`, `connection_id`, and `endpoint`, there are additional arguments available in `AI.GENERATE` at your disposal for customizing your generation request." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "bY5ksh35WMnd" + }, + "source": [ + "##### The `model_params` argument\n", + "There are several parameters you can specify within the `model_params` argument. You can read more about which specific parameters are available for `AI.GENERATE` in the [documentation](https://cloud.google.com/bigquery/docs/reference/standard-sql/bigqueryml-syntax-ai-generate#arguments). The next query adds custom `temperature` and `maxOutputTokens` parameters.\n", + "\n", + "* The `temperature` parameter controls the randomness of the response, where a lower value makes the output more predictable and focused, while a higher value encourages more creative and diverse results.\n", + "\n", + "* The `maxOutputTokens` parameter sets the maximum length of the model's response by limiting the total number of tokens (pieces of words) it can generate.\n", + "\n", + "You can read more about the various parameters [here](https://cloud.google.com/vertex-ai/generative-ai/docs/model-reference/inference#generationconfig), and the parameters' default values are documented in the model card in the [model documentation for Vertex AI](https://cloud.google.com/vertex-ai/generative-ai/docs/models)." + ] + }, + { + "cell_type": "code", + "execution_count": 45, + "metadata": { + "id": "61nesgOtWkWs" + }, + "outputs": [ + { + "data": { + "text/plain": [ + "Query is running: 0%| |" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + "Downloading: 0%| |" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/vnd.google.colaboratory.intrinsic+json": { + "summary": "{\n \"name\": \"get_ipython()\",\n \"rows\": 5,\n \"fields\": [\n {\n \"column\": \"review\",\n \"properties\": {\n \"dtype\": \"string\",\n \"num_unique_values\": 5,\n \"samples\": [\n \"it is a quite slow pace face-to-face Brynner/Douglas...i found there was some pretty heavy violence/gore but you know, seventies giallo style : (maybe some spoiler ahead) a little bit phony and fake but the image is there : the monkey torn apart, the flesh torn from the mechanic, etc...i don't remember if it is alike the novel...but overall i think it is a OK action flick, hero flick : he stands all alone at the end, all the bad guys are out..........and nice images too : a small rocky island, the sun over the seabut not a typical Verne's story-on-cinema : subs, sci-fi; but the adventure seen in a lot of Verne's novels tough\",\n \"Surprising that Jules Verne would write such a story. Even more surprising that Hollywood would produce it. Yul Brynner is unbelievably good as a man freed of all bounds of convention, free to indulge his taste for cruelty and domination. Douglass is an excellent counterpoint, a courageous individual who's chosen simple solitude as a way to deal with the complications and turmoil society had imposed on him. And Samantha Egger's character is the capper, a woman willing to sacrifice far too much for comfort and safety. Inotherwords, everyman. The movie does show its age and is limited by the conventions of time and place and technology of the time. If you're looking for special effects and action as substitutes for thought, look elsewhere.\",\n \"The Light At The Edge of the World marks Kirk Douglas's second filming of a Jules Verne novel. The first of course was one of his most popular films 20,000 Leagues Under The Sea. But this film is far more serious and has far more adult themes than Walt Disney's film aimed for the kid trade.This was the last novel Jules Verne had published during his lifetime and it's a story of survival against almost impossible odds. In the original novel Kirk Douglas's character was named Vasquez which certainly was more in keeping with someone assigned to lighthouse duty on Cape Horn. But in giving Douglas's character an Anglo name it better explains his presence on the island and it certainly is in keeping with the international tradition of Jules Verne's writings.Cape Horn is one of the loneliest parts of the globe and the geography of the southern tip of South America. Look on a map of the many islands and rocks in that part of the globe and imagine how rough the sea is because it has only limited space. It's not without reason that sailors in all cultures say that no one is really a true sailor until they've made a voyage crossing from the Atlantic to the Pacific Ocean in that area. Remember also this is 1865 as Yul Brynner identifies the year and the Panama Canal had not been built.Which makes the lighthouse at Cape Horn an international concern which was something that is ever present in Jules Verne's writings. But then as now there are malevolent forces in the world and they are in this story Yul Brynner and his pirate crew.On one desultory like any other down there, Yul Brynner's ship docks at the island and kills lighthouse keeper Fernando Rey and his young assistant Massimo Ranieri. By sheer dumb luck Douglas is not at the lighthouse when this happens, but he becomes a hunted man by Brynner and his pirate crew who want to set up headquarters there and use the light to pile up as many wrecks as they can plunder. Also they want to eliminate Douglas who's now the only witness to their crimes.I did like this film very much both when first seeing it in the theater and now on VHS. One thing of interest I found here is that there is no ambiguity, no shadings of character. Kirk Douglas is a good guy and Yul Brynner a bad one, no one is going to walk away thinking anything else. In fact Yul Brynner's pirate captain Jonathan Kongre is the most unredeemable villain we've seen on screen since Lee Marvin as Liberty Valance.Definitely fans of Kirk Douglas, Yul Brynner and Jules Verne should earmark this film for their collection.\"\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"keywords\",\n \"properties\": {\n \"dtype\": \"string\",\n \"num_unique_values\": 5,\n \"samples\": [\n \"Here are the keywords extracted from the text:\",\n \"Here are the keywords from the text\",\n \"Here are the keywords extracted\"\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n }\n ]\n}", + "type": "dataframe" + }, + "text/html": [ + "\n", + "
    \n", + "
    \n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
    reviewkeywords
    0Jules Verne wrote about 80 novels as well as p...Here's a list
    1it is a quite slow pace face-to-face Brynner/D...Here are the keywords extracted from the text:
    2The Light At The Edge of the World marks Kirk ...Here are the keywords extracted
    3Remove yourself from the Kirk Douglass aspects...Here are the keywords from the text:\\n\\n*
    4Surprising that Jules Verne would write such a...Here are the keywords from the text
    \n", + "
    \n", + "
    \n", + "\n", + "
    \n", + " \n", + "\n", + " \n", + "\n", + " \n", + "
    \n", + "\n", + "\n", + "
    \n", + " \n", + "\n", + "\n", + "\n", + " \n", + "
    \n", + "\n", + "
    \n", + "
    \n" + ], + "text/plain": [ + " review \\\n", + "0 Jules Verne wrote about 80 novels as well as p... \n", + "1 it is a quite slow pace face-to-face Brynner/D... \n", + "2 The Light At The Edge of the World marks Kirk ... \n", + "3 Remove yourself from the Kirk Douglass aspects... \n", + "4 Surprising that Jules Verne would write such a... \n", + "\n", + " keywords \n", + "0 Here's a list \n", + "1 Here are the keywords extracted from the text: \n", + "2 Here are the keywords extracted \n", + "3 Here are the keywords from the text:\\n\\n* \n", + "4 Here are the keywords from the text " + ] + }, + "execution_count": 45, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "%%bigquery --project {PROJECT_ID}\n", + "\n", + "SELECT\n", + " review,\n", + " AI.GENERATE(('Extract the keywords from the text below: ', review),\n", + " connection_id => 'us.test_connection',\n", + " endpoint => 'gemini-2.5-flash',\n", + " model_params => JSON '{\"generationConfig\":{\"temperature\": 0.5, \"maxOutputTokens\": 250}}'\n", + " ).result AS keywords\n", + "FROM\n", + " `bigquery-public-data.imdb.reviews`\n", + "WHERE movie_id = \"tt0067345\"\n", + "LIMIT 5\n", + ";" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "FnSyQrNzb_4A" + }, + "source": [ + "Why are some of the responses truncated or cut off? By setting the `maxOutputTokens` value to 250 (much lower than the default 65,535 which was used in the first queries), you are seeing this limit in action. Gemini 2.5 Flash has [thinking capabilities](https://cloud.google.com/vertex-ai/generative-ai/docs/thinking), which means the model goes through a \"thinking process\" before determining and returning its response, and this thinking process consumes output tokens. In this case, the model is consuming most of the `maxOutputTokens` value before it moves from its thinking process to returning the actual response.\n", + "\n", + "You can resolve this issue by increasing the `maxOutputTokens` value and/or setting a `thinking_budget` to limit the amount of tokens that can be consumed by the thinking process." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "aBBpEQIYJ2Dt" + }, + "source": [ + "##### Setting a `thinking_budget`\n", + "\n", + "You can set a [`thinking_budget`](https://cloud.google.com/bigquery/docs/reference/standard-sql/bigqueryml-syntax-ai-generate#thinking-budget) to manage how many output tokens are allocated to the thinking phase of the model's response development.\n", + "\n", + "The next query shows how to set the `thinking_budget` as an additional parameter in the `model_params` argument." + ] + }, + { + "cell_type": "code", + "execution_count": 46, + "metadata": { + "id": "L2fNyr7aGbDP" + }, + "outputs": [ + { + "data": { + "text/plain": [ + "Query is running: 0%| |" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + "Downloading: 0%| |" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/vnd.google.colaboratory.intrinsic+json": { + "summary": "{\n \"name\": \"get_ipython()\",\n \"rows\": 5,\n \"fields\": [\n {\n \"column\": \"review\",\n \"properties\": {\n \"dtype\": \"string\",\n \"num_unique_values\": 5,\n \"samples\": [\n \"The Light At The Edge of the World marks Kirk Douglas's second filming of a Jules Verne novel. The first of course was one of his most popular films 20,000 Leagues Under The Sea. But this film is far more serious and has far more adult themes than Walt Disney's film aimed for the kid trade.This was the last novel Jules Verne had published during his lifetime and it's a story of survival against almost impossible odds. In the original novel Kirk Douglas's character was named Vasquez which certainly was more in keeping with someone assigned to lighthouse duty on Cape Horn. But in giving Douglas's character an Anglo name it better explains his presence on the island and it certainly is in keeping with the international tradition of Jules Verne's writings.Cape Horn is one of the loneliest parts of the globe and the geography of the southern tip of South America. Look on a map of the many islands and rocks in that part of the globe and imagine how rough the sea is because it has only limited space. It's not without reason that sailors in all cultures say that no one is really a true sailor until they've made a voyage crossing from the Atlantic to the Pacific Ocean in that area. Remember also this is 1865 as Yul Brynner identifies the year and the Panama Canal had not been built.Which makes the lighthouse at Cape Horn an international concern which was something that is ever present in Jules Verne's writings. But then as now there are malevolent forces in the world and they are in this story Yul Brynner and his pirate crew.On one desultory like any other down there, Yul Brynner's ship docks at the island and kills lighthouse keeper Fernando Rey and his young assistant Massimo Ranieri. By sheer dumb luck Douglas is not at the lighthouse when this happens, but he becomes a hunted man by Brynner and his pirate crew who want to set up headquarters there and use the light to pile up as many wrecks as they can plunder. Also they want to eliminate Douglas who's now the only witness to their crimes.I did like this film very much both when first seeing it in the theater and now on VHS. One thing of interest I found here is that there is no ambiguity, no shadings of character. Kirk Douglas is a good guy and Yul Brynner a bad one, no one is going to walk away thinking anything else. In fact Yul Brynner's pirate captain Jonathan Kongre is the most unredeemable villain we've seen on screen since Lee Marvin as Liberty Valance.Definitely fans of Kirk Douglas, Yul Brynner and Jules Verne should earmark this film for their collection.\",\n \"Surprising that Jules Verne would write such a story. Even more surprising that Hollywood would produce it. Yul Brynner is unbelievably good as a man freed of all bounds of convention, free to indulge his taste for cruelty and domination. Douglass is an excellent counterpoint, a courageous individual who's chosen simple solitude as a way to deal with the complications and turmoil society had imposed on him. And Samantha Egger's character is the capper, a woman willing to sacrifice far too much for comfort and safety. Inotherwords, everyman. The movie does show its age and is limited by the conventions of time and place and technology of the time. If you're looking for special effects and action as substitutes for thought, look elsewhere.\",\n \"Jules Verne wrote about 80 novels as well as plays and short stories in his career. He began writing in 1854 with a short story called \\\"Master Zacharias, or the Clockmaker's Soul\\\". It was the first time he talked of the negative side of progress - the evil that results from some discoveries or inventions when they fall into the wrong hands. This becomes a running theme in his novels: Captain Nemo in TWENTY THOUSAND LEAGUES UNDER THE SEA, or Robur (from ROBUR THE CONQUEROR and it's sequel, THE MASTER OF THE WORLD) are two of his best examples of this them. Kongre, in THE LIGHTHOUSE AT THE EDGE OF THE WORLD, is another.Verne was so prolific that when he died in 1905 he left a dozen unpublished novels and stories that were not fully published until 1910. They include some of his best writing, such as THE BARSAC MISSION (partly written by Verne's son Michael), THE SURVIVORS OF THE \\\"JONATHAN\\\", THE PURSUIT OF THE METEOR, THE DANUBE PILOT. All of these dealt with science, but also dealt with political systems, and economics, for Verne was interested in all the problems facing modern man. THE LIGHTHOUSE AT THE EDGE OF THE WORLD was the last novel that was published in Verne's lifetime. It does not deal with the political questions or economic ones that perplexed him, but seems to go back to his potboiler period, when he was turning out stories for money while considering better stories for later publication. But nothing Verne wrote is without interest. Rereading THE LIGHTHOUSE one sees what the subtle point is in it. It is the study of how the ego of a villain can prevent him from escaping retribution.Kongre (renamed Jonathan Kongre) is one of the last pirates in the world of 1900. He and his gang find a damaged boat and repair it. They sail it across the Pacific, and reach Staten Island, a small island in the Straits of Magellan controlled by Chile. There they find a lighthouse with a crew of three men. They manage to kill two of them, but the third one (named Vasquez - he's from Chile, remember), hides on the island. Kongre and his men decide that they should prepare to leave the island shortly, before the Chilean Naval relief boat returns in three months to pick up the lighthouse crew. But first they will wreck any boat that comes to the passage, and increase their ill-gotten gains. But the key to the novel (and it is not in the movie) is that Kongre's right hand men (Carcante and Vargas) keep urging him to pack up his supplies and wealth and head to Asia where the money can be divvied up and everyone separate in safety. And each time Kongre won't do it. Initially it is pure greed. He wrecks a boat, and massacres the crew (a scene that is done in the film). The sole survivor is an American, John Davis (the name became Denton in the film, except that it was given to the character of Vasquez). Now with an ally (and not a drunken one, as in the film), Vasquez starts sabotaging Kongre's activities on the island. Carcante keeps suggesting leaving, but Kongre (unused to someone annoying him successfully) keeps delaying in order to catch Vasquez and Davis. The end result is that when he thinks he has them cornered, the Chilean boat appears to sink his craft, kill most of his crew, and confront him. Kongre commits suicide to avoid capture.Much of the mayhem of the movie (with Denton picking off crew members one at a time) is not in the book. Nor is there any female character in the novel (a rarity in most of Verne's stories - he could be quite a feminist when he wished). The egotism of \\\"Jonathan\\\" Kongre is well shown by Yul Brynner's performance, but the subtlety of that trait is lost. The writers presumably did not think the audience could appreciate it. Kirk Douglas does well enough as Denton, but his singlehanded success (Vasquez and Davis work together well to the end of the story, unlike Denton's ally who is killed by the pirates) seems unlikely. The bestiality of the pirates is well shown in the movie, particularly a singularly tall actor who in one scene wears women's clothing to particularly unsettling effect. The film is not a bad minor adventure film, but it could have been better if they had stuck to Verne's theme.\"\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"keywords\",\n \"properties\": {\n \"dtype\": \"string\",\n \"num_unique_values\": 5,\n \"samples\": [\n \"Here are the keywords extracted from the text:\\n\\n* The Light At The Edge of the World\\n* Kirk Douglas\\n* Jules Verne\\n* 20,000 Leagues Under The Sea\\n* Cape Horn\\n* Yul Brynner\\n* lighthouse\\n* pirates\\n* survival\\n* novel\\n* film\\n* Jonathan Kongre\\n* Fernando Rey\\n* Massimo Ranieri\\n* Atlantic to Pacific Ocean\\n* 1865\\n* Panama Canal\\n* villain\\n* hero / good guy\\n* bad guy\",\n \"Here are the keywords extracted from the text:\\n\\n* Jules Verne\\n* Hollywood\\n* Yul Brynner\\n* Cruelty\\n* Domination\\n* Douglass\\n* Solitude\\n* Samantha Egger\\n* Sacrifice\\n* Comfort\\n* Safety\\n* Everyman\\n* Movie\\n* Conventions (of time, place, technology)\\n* Thought (as opposed to special effects/action)\",\n \"Here are the keywords extracted from the text:\\n\\n* Jules Verne\\n* Novels\\n* Short stories\\n* Master Zacharias, or the Clockmaker's Soul\\n* Negative side of progress\\n* Evil of discoveries/inventions\\n* Captain Nemo\\n* Twenty Thousand Leagues Under the Sea\\n* Robur the Conqueror\\n* The Master of the World\\n* Kongre\\n* The Lighthouse at the Edge of the World\\n* Unpublished novels\\n* The Barsac Mission\\n* The Survivors of the \\\"Jonathan\\\"\\n* The Pursuit of the Meteor\\n* The Danube Pilot\\n* Science\\n* Political systems\\n* Economics\\n*\"\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n }\n ]\n}", + "type": "dataframe" + }, + "text/html": [ + "\n", + "
    \n", + "
    \n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
    reviewkeywords
    0it is a quite slow pace face-to-face Brynner/D...Here are the keywords extracted from the text:...
    1The Light At The Edge of the World marks Kirk ...Here are the keywords extracted from the text:...
    2Jules Verne wrote about 80 novels as well as p...Here are the keywords extracted from the text:...
    3Remove yourself from the Kirk Douglass aspects...Here are the keywords extracted from the text:...
    4Surprising that Jules Verne would write such a...Here are the keywords extracted from the text:...
    \n", + "
    \n", + "
    \n", + "\n", + "
    \n", + " \n", + "\n", + " \n", + "\n", + " \n", + "
    \n", + "\n", + "\n", + "
    \n", + " \n", + "\n", + "\n", + "\n", + " \n", + "
    \n", + "\n", + "
    \n", + "
    \n" + ], + "text/plain": [ + " review \\\n", + "0 it is a quite slow pace face-to-face Brynner/D... \n", + "1 The Light At The Edge of the World marks Kirk ... \n", + "2 Jules Verne wrote about 80 novels as well as p... \n", + "3 Remove yourself from the Kirk Douglass aspects... \n", + "4 Surprising that Jules Verne would write such a... \n", + "\n", + " keywords \n", + "0 Here are the keywords extracted from the text:... \n", + "1 Here are the keywords extracted from the text:... \n", + "2 Here are the keywords extracted from the text:... \n", + "3 Here are the keywords extracted from the text:... \n", + "4 Here are the keywords extracted from the text:... " + ] + }, + "execution_count": 46, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "%%bigquery --project {PROJECT_ID}\n", + "\n", + "SELECT\n", + " review,\n", + " AI.GENERATE(('Extract the keywords from the text below: ', review),\n", + " connection_id => 'us.test_connection',\n", + " endpoint => 'gemini-2.5-flash',\n", + " model_params => JSON '{\"generationConfig\":{\"temperature\": 0.5, \"maxOutputTokens\": 250, \"thinking_config\": {\"thinking_budget\": 100}}}'\n", + " ).result AS keywords\n", + "FROM\n", + " `bigquery-public-data.imdb.reviews`\n", + "WHERE movie_id = \"tt0067345\"\n", + "LIMIT 5\n", + ";" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "edQ8hdPb1QMm" + }, + "source": [ + "##### The `output_schema` argument\n", + "\n", + "As you can see in the last query, the model's responses were not provided in a standard structure. You can use the **`output_schema`** argument to define the format of the responses.\n", + "\n", + "First, you'll specify the output as a string of key words by adding `output_schema => 'keywords ARRAY'` to the `AI.GENERATE` request." + ] + }, + { + "cell_type": "code", + "execution_count": 47, + "metadata": { + "id": "7w-oUv8UimxT" + }, + "outputs": [ + { + "data": { + "text/plain": [ + "Query is running: 0%| |" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + "Downloading: 0%| |" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/vnd.google.colaboratory.intrinsic+json": { + "summary": "{\n \"name\": \"get_ipython()\",\n \"rows\": 5,\n \"fields\": [\n {\n \"column\": \"review\",\n \"properties\": {\n \"dtype\": \"string\",\n \"num_unique_values\": 5,\n \"samples\": [\n \"Remove yourself from the Kirk Douglass aspects of the casting. It is essential to your enjoying the film. There is a beautiful young woman playing double roles - and in the photos from the 1800's, I can't believe how smooth and white her skin is. Also, there is an excellent degrading of the film stock which chills the mind if you like faded greys and yellows as I do. This film is played on TNT from time to time so see it.\",\n \"Jules Verne wrote about 80 novels as well as plays and short stories in his career. He began writing in 1854 with a short story called \\\"Master Zacharias, or the Clockmaker's Soul\\\". It was the first time he talked of the negative side of progress - the evil that results from some discoveries or inventions when they fall into the wrong hands. This becomes a running theme in his novels: Captain Nemo in TWENTY THOUSAND LEAGUES UNDER THE SEA, or Robur (from ROBUR THE CONQUEROR and it's sequel, THE MASTER OF THE WORLD) are two of his best examples of this them. Kongre, in THE LIGHTHOUSE AT THE EDGE OF THE WORLD, is another.Verne was so prolific that when he died in 1905 he left a dozen unpublished novels and stories that were not fully published until 1910. They include some of his best writing, such as THE BARSAC MISSION (partly written by Verne's son Michael), THE SURVIVORS OF THE \\\"JONATHAN\\\", THE PURSUIT OF THE METEOR, THE DANUBE PILOT. All of these dealt with science, but also dealt with political systems, and economics, for Verne was interested in all the problems facing modern man. THE LIGHTHOUSE AT THE EDGE OF THE WORLD was the last novel that was published in Verne's lifetime. It does not deal with the political questions or economic ones that perplexed him, but seems to go back to his potboiler period, when he was turning out stories for money while considering better stories for later publication. But nothing Verne wrote is without interest. Rereading THE LIGHTHOUSE one sees what the subtle point is in it. It is the study of how the ego of a villain can prevent him from escaping retribution.Kongre (renamed Jonathan Kongre) is one of the last pirates in the world of 1900. He and his gang find a damaged boat and repair it. They sail it across the Pacific, and reach Staten Island, a small island in the Straits of Magellan controlled by Chile. There they find a lighthouse with a crew of three men. They manage to kill two of them, but the third one (named Vasquez - he's from Chile, remember), hides on the island. Kongre and his men decide that they should prepare to leave the island shortly, before the Chilean Naval relief boat returns in three months to pick up the lighthouse crew. But first they will wreck any boat that comes to the passage, and increase their ill-gotten gains. But the key to the novel (and it is not in the movie) is that Kongre's right hand men (Carcante and Vargas) keep urging him to pack up his supplies and wealth and head to Asia where the money can be divvied up and everyone separate in safety. And each time Kongre won't do it. Initially it is pure greed. He wrecks a boat, and massacres the crew (a scene that is done in the film). The sole survivor is an American, John Davis (the name became Denton in the film, except that it was given to the character of Vasquez). Now with an ally (and not a drunken one, as in the film), Vasquez starts sabotaging Kongre's activities on the island. Carcante keeps suggesting leaving, but Kongre (unused to someone annoying him successfully) keeps delaying in order to catch Vasquez and Davis. The end result is that when he thinks he has them cornered, the Chilean boat appears to sink his craft, kill most of his crew, and confront him. Kongre commits suicide to avoid capture.Much of the mayhem of the movie (with Denton picking off crew members one at a time) is not in the book. Nor is there any female character in the novel (a rarity in most of Verne's stories - he could be quite a feminist when he wished). The egotism of \\\"Jonathan\\\" Kongre is well shown by Yul Brynner's performance, but the subtlety of that trait is lost. The writers presumably did not think the audience could appreciate it. Kirk Douglas does well enough as Denton, but his singlehanded success (Vasquez and Davis work together well to the end of the story, unlike Denton's ally who is killed by the pirates) seems unlikely. The bestiality of the pirates is well shown in the movie, particularly a singularly tall actor who in one scene wears women's clothing to particularly unsettling effect. The film is not a bad minor adventure film, but it could have been better if they had stuck to Verne's theme.\",\n \"Surprising that Jules Verne would write such a story. Even more surprising that Hollywood would produce it. Yul Brynner is unbelievably good as a man freed of all bounds of convention, free to indulge his taste for cruelty and domination. Douglass is an excellent counterpoint, a courageous individual who's chosen simple solitude as a way to deal with the complications and turmoil society had imposed on him. And Samantha Egger's character is the capper, a woman willing to sacrifice far too much for comfort and safety. Inotherwords, everyman. The movie does show its age and is limited by the conventions of time and place and technology of the time. If you're looking for special effects and action as substitutes for thought, look elsewhere.\"\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"keywords\",\n \"properties\": {\n \"dtype\": \"object\",\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n }\n ]\n}", + "type": "dataframe" + }, + "text/html": [ + "\n", + "
    \n", + "
    \n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
    reviewkeywords
    0The Light At The Edge of the World marks Kirk ...[Kirk Douglas, Jules Verne, The Light At The E...
    1Remove yourself from the Kirk Douglass aspects...[Kirk Douglass aspects, casting, film, beautif...
    2Surprising that Jules Verne would write such a...[Jules Verne, Hollywood, Yul Brynner, cruelty ...
    3it is a quite slow pace face-to-face Brynner/D...[slow pace, face-to-face, Brynner/Douglas, hea...
    4Jules Verne wrote about 80 novels as well as p...[Jules Verne, novels, plays, short stories, Ma...
    \n", + "
    \n", + "
    \n", + "\n", + "
    \n", + " \n", + "\n", + " \n", + "\n", + " \n", + "
    \n", + "\n", + "\n", + "
    \n", + " \n", + "\n", + "\n", + "\n", + " \n", + "
    \n", + "\n", + "
    \n", + "
    \n" + ], + "text/plain": [ + " review \\\n", + "0 The Light At The Edge of the World marks Kirk ... \n", + "1 Remove yourself from the Kirk Douglass aspects... \n", + "2 Surprising that Jules Verne would write such a... \n", + "3 it is a quite slow pace face-to-face Brynner/D... \n", + "4 Jules Verne wrote about 80 novels as well as p... \n", + "\n", + " keywords \n", + "0 [Kirk Douglas, Jules Verne, The Light At The E... \n", + "1 [Kirk Douglass aspects, casting, film, beautif... \n", + "2 [Jules Verne, Hollywood, Yul Brynner, cruelty ... \n", + "3 [slow pace, face-to-face, Brynner/Douglas, hea... \n", + "4 [Jules Verne, novels, plays, short stories, Ma... " + ] + }, + "execution_count": 47, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "%%bigquery --project {PROJECT_ID}\n", + "\n", + "SELECT\n", + " review,\n", + " AI.GENERATE(\n", + " ('Extract the keywords from the text below: ', review),\n", + " connection_id => 'us.test_connection',\n", + " endpoint => 'gemini-2.5-flash',\n", + " output_schema => 'keywords ARRAY< STRING>').keywords\n", + "FROM\n", + " `bigquery-public-data.imdb.reviews`\n", + "WHERE movie_id = \"tt0067345\"\n", + "LIMIT 5\n", + ";" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "AoF8XxEa1HsW" + }, + "source": [ + "Now the responses are arrays of keywords - a more useful format for this use case!\n", + "\n", + "Next, you'll adjust the prompt to the model to extract the sentiment as well, and adjust the `output_schema` accordingly." + ] + }, + { + "cell_type": "code", + "execution_count": 48, + "metadata": { + "id": "f1wfmGVZkA1x" + }, + "outputs": [ + { + "data": { + "text/plain": [ + "Query is running: 0%| |" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + "Downloading: 0%| |" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/vnd.google.colaboratory.intrinsic+json": { + "summary": "{\n \"name\": \"get_ipython()\",\n \"rows\": 5,\n \"fields\": [\n {\n \"column\": \"review\",\n \"properties\": {\n \"dtype\": \"string\",\n \"num_unique_values\": 5,\n \"samples\": [\n \"Remove yourself from the Kirk Douglass aspects of the casting. It is essential to your enjoying the film. There is a beautiful young woman playing double roles - and in the photos from the 1800's, I can't believe how smooth and white her skin is. Also, there is an excellent degrading of the film stock which chills the mind if you like faded greys and yellows as I do. This film is played on TNT from time to time so see it.\",\n \"it is a quite slow pace face-to-face Brynner/Douglas...i found there was some pretty heavy violence/gore but you know, seventies giallo style : (maybe some spoiler ahead) a little bit phony and fake but the image is there : the monkey torn apart, the flesh torn from the mechanic, etc...i don't remember if it is alike the novel...but overall i think it is a OK action flick, hero flick : he stands all alone at the end, all the bad guys are out..........and nice images too : a small rocky island, the sun over the seabut not a typical Verne's story-on-cinema : subs, sci-fi; but the adventure seen in a lot of Verne's novels tough\",\n \"Surprising that Jules Verne would write such a story. Even more surprising that Hollywood would produce it. Yul Brynner is unbelievably good as a man freed of all bounds of convention, free to indulge his taste for cruelty and domination. Douglass is an excellent counterpoint, a courageous individual who's chosen simple solitude as a way to deal with the complications and turmoil society had imposed on him. And Samantha Egger's character is the capper, a woman willing to sacrifice far too much for comfort and safety. Inotherwords, everyman. The movie does show its age and is limited by the conventions of time and place and technology of the time. If you're looking for special effects and action as substitutes for thought, look elsewhere.\"\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"full_response\",\n \"properties\": {\n \"dtype\": \"string\",\n \"num_unique_values\": 5,\n \"samples\": [\n \"{\\\"candidates\\\":[{\\\"avg_logprobs\\\":-1.3310155073801677,\\\"content\\\":{\\\"parts\\\":[{\\\"text\\\":\\\"{\\\\\\\"keywords\\\\\\\": [\\\\\\\"Kirk Douglass aspects\\\\\\\", \\\\\\\"casting\\\\\\\", \\\\\\\"beautiful young woman\\\\\\\", \\\\\\\"double roles\\\\\\\", \\\\\\\"smooth white skin\\\\\\\", \\\\\\\"degrading film stock\\\\\\\", \\\\\\\"faded greys and yellows\\\\\\\", \\\\\\\"TNT\\\\\\\"], \\\\\\\"sentiment\\\\\\\": \\\\\\\"Positive\\\\\\\"}\\\"}],\\\"role\\\":\\\"model\\\"},\\\"finish_reason\\\":\\\"STOP\\\",\\\"score\\\":-63.88874435424805}],\\\"create_time\\\":\\\"2026-01-06T22:58:03.610034Z\\\",\\\"model_version\\\":\\\"gemini-2.5-flash\\\",\\\"response_id\\\":\\\"e5NdafKdJb2mlu8Po5XtwQo\\\",\\\"usage_metadata\\\":{\\\"billable_prompt_usage\\\":{\\\"text_count\\\":447},\\\"candidates_token_count\\\":48,\\\"candidates_tokens_details\\\":[{\\\"modality\\\":\\\"TEXT\\\",\\\"token_count\\\":48}],\\\"prompt_token_count\\\":123,\\\"prompt_tokens_details\\\":[{\\\"modality\\\":\\\"TEXT\\\",\\\"token_count\\\":123}],\\\"thoughts_token_count\\\":279,\\\"total_token_count\\\":450,\\\"traffic_type\\\":\\\"ON_DEMAND\\\"}}\",\n \"{\\\"candidates\\\":[{\\\"avg_logprobs\\\":-1.9722009741741677,\\\"content\\\":{\\\"parts\\\":[{\\\"text\\\":\\\"{\\\\n \\\\\\\"keywords\\\\\\\": [\\\\n \\\\\\\"slow pace\\\\\\\",\\\\n \\\\\\\"heavy violence\\\\\\\",\\\\n \\\\\\\"gore\\\\\\\",\\\\n \\\\\\\"seventies giallo style\\\\\\\",\\\\n \\\\\\\"phony\\\\\\\",\\\\n \\\\\\\"fake\\\\\\\",\\\\n \\\\\\\"action flick\\\\\\\",\\\\n \\\\\\\"hero flick\\\\\\\",\\\\n \\\\\\\"rocky island\\\\\\\",\\\\n \\\\\\\"sea\\\\\\\",\\\\n \\\\\\\"Jules Verne\\\\\\\",\\\\n \\\\\\\"adventure\\\\\\\"\\\\n ],\\\\n \\\\\\\"sentiment\\\\\\\": \\\\\\\"Neutral\\\\\\\"\\\\n}\\\"}],\\\"role\\\":\\\"model\\\"},\\\"finish_reason\\\":\\\"STOP\\\",\\\"score\\\":-181.44248962402344}],\\\"create_time\\\":\\\"2026-01-06T22:58:02.628472Z\\\",\\\"model_version\\\":\\\"gemini-2.5-flash\\\",\\\"response_id\\\":\\\"epNdafitJrjFvPEP5p7G0Ao\\\",\\\"usage_metadata\\\":{\\\"billable_prompt_usage\\\":{\\\"text_count\\\":622},\\\"candidates_token_count\\\":92,\\\"candidates_tokens_details\\\":[{\\\"modality\\\":\\\"TEXT\\\",\\\"token_count\\\":92}],\\\"prompt_token_count\\\":179,\\\"prompt_tokens_details\\\":[{\\\"modality\\\":\\\"TEXT\\\",\\\"token_count\\\":179}],\\\"thoughts_token_count\\\":625,\\\"total_token_count\\\":896,\\\"traffic_type\\\":\\\"ON_DEMAND\\\"}}\",\n \"{\\\"candidates\\\":[{\\\"avg_logprobs\\\":-1.2379138734605577,\\\"content\\\":{\\\"parts\\\":[{\\\"text\\\":\\\"{\\\\\\\"keywords\\\\\\\": [\\\\\\\"Jules Verne\\\\\\\", \\\\\\\"Hollywood\\\\\\\", \\\\\\\"Yul Brynner\\\\\\\", \\\\\\\"cruelty and domination\\\\\\\", \\\\\\\"Douglass\\\\\\\", \\\\\\\"simple solitude\\\\\\\", \\\\\\\"Samantha Egger\\\\\\\", \\\\\\\"sacrifice\\\\\\\", \\\\\\\"everyman\\\\\\\", \\\\\\\"movie age\\\\\\\", \\\\\\\"conventions of time\\\\\\\", \\\\\\\"technology\\\\\\\", \\\\\\\"special effects\\\\\\\", \\\\\\\"action\\\\\\\", \\\\\\\"thought\\\\\\\"], \\\\\\\"sentiment\\\\\\\": \\\\\\\"Positive\\\\\\\"}\\\"}],\\\"role\\\":\\\"model\\\"},\\\"finish_reason\\\":\\\"STOP\\\",\\\"score\\\":-89.12979888916016}],\\\"create_time\\\":\\\"2026-01-06T22:58:02.624833Z\\\",\\\"model_version\\\":\\\"gemini-2.5-flash\\\",\\\"response_id\\\":\\\"epNdacGRJpHplu8P_-zfqQw\\\",\\\"usage_metadata\\\":{\\\"billable_prompt_usage\\\":{\\\"text_count\\\":728},\\\"candidates_token_count\\\":72,\\\"candidates_tokens_details\\\":[{\\\"modality\\\":\\\"TEXT\\\",\\\"token_count\\\":72}],\\\"prompt_token_count\\\":176,\\\"prompt_tokens_details\\\":[{\\\"modality\\\":\\\"TEXT\\\",\\\"token_count\\\":176}],\\\"thoughts_token_count\\\":288,\\\"total_token_count\\\":536,\\\"traffic_type\\\":\\\"ON_DEMAND\\\"}}\"\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n }\n ]\n}", + "type": "dataframe" + }, + "text/html": [ + "\n", + "
    \n", + "
    \n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
    reviewfull_response
    0The Light At The Edge of the World marks Kirk ...{\"candidates\":[{\"avg_logprobs\":-0.587079347069...
    1Remove yourself from the Kirk Douglass aspects...{\"candidates\":[{\"avg_logprobs\":-1.331015507380...
    2Surprising that Jules Verne would write such a...{\"candidates\":[{\"avg_logprobs\":-1.237913873460...
    3Jules Verne wrote about 80 novels as well as p...{\"candidates\":[{\"avg_logprobs\":-0.702408254963...
    4it is a quite slow pace face-to-face Brynner/D...{\"candidates\":[{\"avg_logprobs\":-1.972200974174...
    \n", + "
    \n", + "
    \n", + "\n", + "
    \n", + " \n", + "\n", + " \n", + "\n", + " \n", + "
    \n", + "\n", + "\n", + "
    \n", + " \n", + "\n", + "\n", + "\n", + " \n", + "
    \n", + "\n", + "
    \n", + "
    \n" + ], + "text/plain": [ + " review \\\n", + "0 The Light At The Edge of the World marks Kirk ... \n", + "1 Remove yourself from the Kirk Douglass aspects... \n", + "2 Surprising that Jules Verne would write such a... \n", + "3 Jules Verne wrote about 80 novels as well as p... \n", + "4 it is a quite slow pace face-to-face Brynner/D... \n", + "\n", + " full_response \n", + "0 {\"candidates\":[{\"avg_logprobs\":-0.587079347069... \n", + "1 {\"candidates\":[{\"avg_logprobs\":-1.331015507380... \n", + "2 {\"candidates\":[{\"avg_logprobs\":-1.237913873460... \n", + "3 {\"candidates\":[{\"avg_logprobs\":-0.702408254963... \n", + "4 {\"candidates\":[{\"avg_logprobs\":-1.972200974174... " + ] + }, + "execution_count": 48, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "%%bigquery --project {PROJECT_ID}\n", + "\n", + "SELECT\n", + " review,\n", + " AI.GENERATE(\n", + " ('Extract the keywords and sentiment (Positive, Negative, or Neutral) from the following review: ', review),\n", + " connection_id => 'us.test_connection',\n", + " endpoint => 'gemini-2.5-flash',\n", + " output_schema => 'keywords ARRAY< STRING>, sentiment STRING').full_response\n", + "FROM\n", + " `bigquery-public-data.imdb.reviews`\n", + "WHERE movie_id = \"tt0067345\"\n", + "LIMIT 5\n", + ";" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "KmpYd5Dg2AJy" + }, + "source": [ + "This time, the response is in a JSON format. Why?\n", + "\n", + "When using `output_schema` with [`AI.GENERATE`](https://cloud.google.com/bigquery/docs/reference/standard-sql/bigqueryml-syntax-ai-generate), you can specify in the SQL which field you want to return. In this case the SQL specifies the `.full_response` which will provide a JSON object containing the various text response elements. You may alternatively specify to return any one of the schema columns you defined, such as `.keywords` (as was used in the first query of this section) or `.sentiment`. However, you cannot specify to return multiple columns.\n", + "\n", + " You could use SQL to parse the JSON provided in the `.full_response`, but for ease of use, let's look at the [`AI.GENERATE_TABLE`](https://cloud.google.com/bigquery/docs/reference/standard-sql/bigqueryml-syntax-generate-table) function." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "generate_table_md" + }, + "source": [ + "### Using `AI.GENERATE_TABLE`: Extract keywords and sentiment from movie reviews\n", + "\n", + "The [`AI.GENERATE_TABLE`](https://cloud.google.com/bigquery/docs/reference/standard-sql/bigqueryml-syntax-generate-table) function provides expanded functionality for turning unstructured text into a structured table. Here, you'll perform the same analysis to extract keywords and overall sentiment from the `bigquery-public-data.imdb_reviews` table and return the response as multiple columns in one query." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "prepare_md" + }, + "source": [ + "#### Create a BigQuery Dataset\n", + "\n", + "The syntax for using [`AI.GENERATE_TABLE`](https://cloud.google.com/bigquery/docs/reference/standard-sql/bigqueryml-syntax-generate-table) differs from [`AI.GENERATE`](https://cloud.google.com/bigquery/docs/reference/standard-sql/bigqueryml-syntax-ai-generate), and it requires that you first create a remote model in BigQuery that connects to the one of the [generally available](https://cloud.google.com/vertex-ai/generative-ai/docs/models#generally_available_models) or [preview](https://cloud.google.com/vertex-ai/generative-ai/docs/models#preview_models) Gemini models.\n", + "\n", + "Running the following query will create a BigQuery dataset called **`bq_ai_tutorial`** to house the remote model:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "create_dataset_code" + }, + "outputs": [], + "source": [ + "%%bigquery --project {PROJECT_ID}\n", + "\n", + "CREATE SCHEMA\n", + " `bq_ai_tutorial` OPTIONS (location = 'US');" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "create_model_md" + }, + "source": [ + "#### Create a remote model for Gemini 2.5 Flash\n", + "\n", + "Now you'll create the remote model, which is simply a pointer to the model endpoint. In this example, you'll continue to use the `gemini-2.5-flash` model endpoint.\n", + "\n", + "Once you create this remote model, it is saved in your `bq_ai_tutorial` dataset so that you can use it in any future analysis." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "create_model_code" + }, + "outputs": [], + "source": [ + "%%bigquery --project {PROJECT_ID}\n", + "\n", + "CREATE OR REPLACE MODEL\n", + " `bq_ai_tutorial.gemini_2_5_flash`\n", + "REMOTE WITH\n", + " CONNECTION `us.test_connection`\n", + " OPTIONS (ENDPOINT = 'gemini-2.5-flash');\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "SBiwwjzhendG" + }, + "source": [ + "#### Run analysis using the remote model" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "CmVE4UxdnoqB" + }, + "source": [ + "Now that the remote model has been created, you can prompt it with [`AI.GENERATE_TABLE`](https://cloud.google.com/bigquery/docs/reference/standard-sql/bigqueryml-syntax-generate-table) to analyze data and specify the output schema with multiple columns returned." + ] + }, + { + "cell_type": "code", + "execution_count": 50, + "metadata": { + "id": "generate_table_code" + }, + "outputs": [ + { + "data": { + "text/plain": [ + "Query is running: 0%| |" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + "Downloading: 0%| |" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/vnd.google.colaboratory.intrinsic+json": { + "summary": "{\n \"name\": \"get_ipython()\",\n \"rows\": 5,\n \"fields\": [\n {\n \"column\": \"review\",\n \"properties\": {\n \"dtype\": \"string\",\n \"num_unique_values\": 5,\n \"samples\": [\n \"Surprising that Jules Verne would write such a story. Even more surprising that Hollywood would produce it. Yul Brynner is unbelievably good as a man freed of all bounds of convention, free to indulge his taste for cruelty and domination. Douglass is an excellent counterpoint, a courageous individual who's chosen simple solitude as a way to deal with the complications and turmoil society had imposed on him. And Samantha Egger's character is the capper, a woman willing to sacrifice far too much for comfort and safety. Inotherwords, everyman. The movie does show its age and is limited by the conventions of time and place and technology of the time. If you're looking for special effects and action as substitutes for thought, look elsewhere.\",\n \"Jules Verne wrote about 80 novels as well as plays and short stories in his career. He began writing in 1854 with a short story called \\\"Master Zacharias, or the Clockmaker's Soul\\\". It was the first time he talked of the negative side of progress - the evil that results from some discoveries or inventions when they fall into the wrong hands. This becomes a running theme in his novels: Captain Nemo in TWENTY THOUSAND LEAGUES UNDER THE SEA, or Robur (from ROBUR THE CONQUEROR and it's sequel, THE MASTER OF THE WORLD) are two of his best examples of this them. Kongre, in THE LIGHTHOUSE AT THE EDGE OF THE WORLD, is another.Verne was so prolific that when he died in 1905 he left a dozen unpublished novels and stories that were not fully published until 1910. They include some of his best writing, such as THE BARSAC MISSION (partly written by Verne's son Michael), THE SURVIVORS OF THE \\\"JONATHAN\\\", THE PURSUIT OF THE METEOR, THE DANUBE PILOT. All of these dealt with science, but also dealt with political systems, and economics, for Verne was interested in all the problems facing modern man. THE LIGHTHOUSE AT THE EDGE OF THE WORLD was the last novel that was published in Verne's lifetime. It does not deal with the political questions or economic ones that perplexed him, but seems to go back to his potboiler period, when he was turning out stories for money while considering better stories for later publication. But nothing Verne wrote is without interest. Rereading THE LIGHTHOUSE one sees what the subtle point is in it. It is the study of how the ego of a villain can prevent him from escaping retribution.Kongre (renamed Jonathan Kongre) is one of the last pirates in the world of 1900. He and his gang find a damaged boat and repair it. They sail it across the Pacific, and reach Staten Island, a small island in the Straits of Magellan controlled by Chile. There they find a lighthouse with a crew of three men. They manage to kill two of them, but the third one (named Vasquez - he's from Chile, remember), hides on the island. Kongre and his men decide that they should prepare to leave the island shortly, before the Chilean Naval relief boat returns in three months to pick up the lighthouse crew. But first they will wreck any boat that comes to the passage, and increase their ill-gotten gains. But the key to the novel (and it is not in the movie) is that Kongre's right hand men (Carcante and Vargas) keep urging him to pack up his supplies and wealth and head to Asia where the money can be divvied up and everyone separate in safety. And each time Kongre won't do it. Initially it is pure greed. He wrecks a boat, and massacres the crew (a scene that is done in the film). The sole survivor is an American, John Davis (the name became Denton in the film, except that it was given to the character of Vasquez). Now with an ally (and not a drunken one, as in the film), Vasquez starts sabotaging Kongre's activities on the island. Carcante keeps suggesting leaving, but Kongre (unused to someone annoying him successfully) keeps delaying in order to catch Vasquez and Davis. The end result is that when he thinks he has them cornered, the Chilean boat appears to sink his craft, kill most of his crew, and confront him. Kongre commits suicide to avoid capture.Much of the mayhem of the movie (with Denton picking off crew members one at a time) is not in the book. Nor is there any female character in the novel (a rarity in most of Verne's stories - he could be quite a feminist when he wished). The egotism of \\\"Jonathan\\\" Kongre is well shown by Yul Brynner's performance, but the subtlety of that trait is lost. The writers presumably did not think the audience could appreciate it. Kirk Douglas does well enough as Denton, but his singlehanded success (Vasquez and Davis work together well to the end of the story, unlike Denton's ally who is killed by the pirates) seems unlikely. The bestiality of the pirates is well shown in the movie, particularly a singularly tall actor who in one scene wears women's clothing to particularly unsettling effect. The film is not a bad minor adventure film, but it could have been better if they had stuck to Verne's theme.\",\n \"The Light At The Edge of the World marks Kirk Douglas's second filming of a Jules Verne novel. The first of course was one of his most popular films 20,000 Leagues Under The Sea. But this film is far more serious and has far more adult themes than Walt Disney's film aimed for the kid trade.This was the last novel Jules Verne had published during his lifetime and it's a story of survival against almost impossible odds. In the original novel Kirk Douglas's character was named Vasquez which certainly was more in keeping with someone assigned to lighthouse duty on Cape Horn. But in giving Douglas's character an Anglo name it better explains his presence on the island and it certainly is in keeping with the international tradition of Jules Verne's writings.Cape Horn is one of the loneliest parts of the globe and the geography of the southern tip of South America. Look on a map of the many islands and rocks in that part of the globe and imagine how rough the sea is because it has only limited space. It's not without reason that sailors in all cultures say that no one is really a true sailor until they've made a voyage crossing from the Atlantic to the Pacific Ocean in that area. Remember also this is 1865 as Yul Brynner identifies the year and the Panama Canal had not been built.Which makes the lighthouse at Cape Horn an international concern which was something that is ever present in Jules Verne's writings. But then as now there are malevolent forces in the world and they are in this story Yul Brynner and his pirate crew.On one desultory like any other down there, Yul Brynner's ship docks at the island and kills lighthouse keeper Fernando Rey and his young assistant Massimo Ranieri. By sheer dumb luck Douglas is not at the lighthouse when this happens, but he becomes a hunted man by Brynner and his pirate crew who want to set up headquarters there and use the light to pile up as many wrecks as they can plunder. Also they want to eliminate Douglas who's now the only witness to their crimes.I did like this film very much both when first seeing it in the theater and now on VHS. One thing of interest I found here is that there is no ambiguity, no shadings of character. Kirk Douglas is a good guy and Yul Brynner a bad one, no one is going to walk away thinking anything else. In fact Yul Brynner's pirate captain Jonathan Kongre is the most unredeemable villain we've seen on screen since Lee Marvin as Liberty Valance.Definitely fans of Kirk Douglas, Yul Brynner and Jules Verne should earmark this film for their collection.\"\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"keywords\",\n \"properties\": {\n \"dtype\": \"object\",\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"sentiment\",\n \"properties\": {\n \"dtype\": \"string\",\n \"num_unique_values\": 3,\n \"samples\": [\n \"Positive\",\n \"Neutral\",\n \"Negative\"\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n }\n ]\n}", + "type": "dataframe" + }, + "text/html": [ + "\n", + "
    \n", + "
    \n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
    reviewkeywordssentiment
    0Remove yourself from the Kirk Douglass aspects...[Kirk Douglass casting, beautiful young woman,...Positive
    1Surprising that Jules Verne would write such a...[Jules Verne, Hollywood, Yul Brynner, cruelty,...Positive
    2The Light At The Edge of the World marks Kirk ...[The Light At The Edge of the World, Kirk Doug...Positive
    3it is a quite slow pace face-to-face Brynner/D...[slow pace, Brynner/Douglas, violence, gore, s...Neutral
    4Jules Verne wrote about 80 novels as well as p...[Jules Verne, The Lighthouse at the Edge of th...Negative
    \n", + "
    \n", + "
    \n", + "\n", + "
    \n", + " \n", + "\n", + " \n", + "\n", + " \n", + "
    \n", + "\n", + "\n", + "
    \n", + " \n", + "\n", + "\n", + "\n", + " \n", + "
    \n", + "\n", + "
    \n", + "
    \n" + ], + "text/plain": [ + " review \\\n", + "0 Remove yourself from the Kirk Douglass aspects... \n", + "1 Surprising that Jules Verne would write such a... \n", + "2 The Light At The Edge of the World marks Kirk ... \n", + "3 it is a quite slow pace face-to-face Brynner/D... \n", + "4 Jules Verne wrote about 80 novels as well as p... \n", + "\n", + " keywords sentiment \n", + "0 [Kirk Douglass casting, beautiful young woman,... Positive \n", + "1 [Jules Verne, Hollywood, Yul Brynner, cruelty,... Positive \n", + "2 [The Light At The Edge of the World, Kirk Doug... Positive \n", + "3 [slow pace, Brynner/Douglas, violence, gore, s... Neutral \n", + "4 [Jules Verne, The Lighthouse at the Edge of th... Negative " + ] + }, + "execution_count": 50, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "%%bigquery --project {PROJECT_ID}\n", + "\n", + "SELECT\n", + " review,\n", + " keywords,\n", + " sentiment\n", + "FROM\n", + " AI.GENERATE_TABLE(\n", + " MODEL `bq_ai_tutorial.gemini_2_5_flash`,\n", + " (\n", + " SELECT\n", + " review,\n", + " ('Extract the keywords and sentiment (Positive, Negative, or Neutral) from the following review:',review) AS prompt,\n", + " FROM\n", + " `bigquery-public-data.imdb.reviews`\n", + " WHERE movie_id = \"tt0067345\"\n", + " LIMIT 5\n", + " ),\n", + " STRUCT(\n", + " 'keywords ARRAY< STRING>, sentiment STRING' AS output_schema\n", + " )\n", + " );" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "WkdFwoOp1cRm" + }, + "source": [ + "#### Saving results in a BigQuery table\n", + "Wrapping any `SELECT` statement with the [`CREATE TABLE` statement](https://cloud.google.com/bigquery/docs/reference/standard-sql/data-definition-language#create_table_statement) allows you to create a permanent table from your query results. You can run the next two queries to see that in action." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "I0yvijK71Zbm" + }, + "outputs": [], + "source": [ + "%%bigquery --project {PROJECT_ID}\n", + "\n", + "CREATE OR REPLACE TABLE `bq_ai_tutorial.reviews_keywords_sentiment` AS (\n", + " SELECT\n", + " review,\n", + " keywords,\n", + " sentiment\n", + " FROM\n", + " AI.GENERATE_TABLE(\n", + " MODEL `bq_ai_tutorial.gemini_2_5_flash`,\n", + " (\n", + " SELECT\n", + " review,\n", + " ('Extract the keywords and sentiment (Positive, Negative, or Neutral) from the following review:',review) AS prompt,\n", + " FROM\n", + " `bigquery-public-data.imdb.reviews`\n", + " WHERE movie_id = \"tt0067345\"\n", + " LIMIT 5\n", + " ),\n", + " STRUCT(\n", + " 'keywords ARRAY< STRING>, sentiment STRING' AS output_schema)));" + ] + }, + { + "cell_type": "code", + "execution_count": 30, + "metadata": { + "id": "JW88rus93d9D" + }, + "outputs": [ + { + "data": { + "text/plain": [ + "Query is running: 0%| |" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + "Downloading: 0%| |" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/vnd.google.colaboratory.intrinsic+json": { + "summary": "{\n \"name\": \"get_ipython()\",\n \"rows\": 5,\n \"fields\": [\n {\n \"column\": \"review\",\n \"properties\": {\n \"dtype\": \"string\",\n \"num_unique_values\": 5,\n \"samples\": [\n \"Jules Verne wrote about 80 novels as well as plays and short stories in his career. He began writing in 1854 with a short story called \\\"Master Zacharias, or the Clockmaker's Soul\\\". It was the first time he talked of the negative side of progress - the evil that results from some discoveries or inventions when they fall into the wrong hands. This becomes a running theme in his novels: Captain Nemo in TWENTY THOUSAND LEAGUES UNDER THE SEA, or Robur (from ROBUR THE CONQUEROR and it's sequel, THE MASTER OF THE WORLD) are two of his best examples of this them. Kongre, in THE LIGHTHOUSE AT THE EDGE OF THE WORLD, is another.Verne was so prolific that when he died in 1905 he left a dozen unpublished novels and stories that were not fully published until 1910. They include some of his best writing, such as THE BARSAC MISSION (partly written by Verne's son Michael), THE SURVIVORS OF THE \\\"JONATHAN\\\", THE PURSUIT OF THE METEOR, THE DANUBE PILOT. All of these dealt with science, but also dealt with political systems, and economics, for Verne was interested in all the problems facing modern man. THE LIGHTHOUSE AT THE EDGE OF THE WORLD was the last novel that was published in Verne's lifetime. It does not deal with the political questions or economic ones that perplexed him, but seems to go back to his potboiler period, when he was turning out stories for money while considering better stories for later publication. But nothing Verne wrote is without interest. Rereading THE LIGHTHOUSE one sees what the subtle point is in it. It is the study of how the ego of a villain can prevent him from escaping retribution.Kongre (renamed Jonathan Kongre) is one of the last pirates in the world of 1900. He and his gang find a damaged boat and repair it. They sail it across the Pacific, and reach Staten Island, a small island in the Straits of Magellan controlled by Chile. There they find a lighthouse with a crew of three men. They manage to kill two of them, but the third one (named Vasquez - he's from Chile, remember), hides on the island. Kongre and his men decide that they should prepare to leave the island shortly, before the Chilean Naval relief boat returns in three months to pick up the lighthouse crew. But first they will wreck any boat that comes to the passage, and increase their ill-gotten gains. But the key to the novel (and it is not in the movie) is that Kongre's right hand men (Carcante and Vargas) keep urging him to pack up his supplies and wealth and head to Asia where the money can be divvied up and everyone separate in safety. And each time Kongre won't do it. Initially it is pure greed. He wrecks a boat, and massacres the crew (a scene that is done in the film). The sole survivor is an American, John Davis (the name became Denton in the film, except that it was given to the character of Vasquez). Now with an ally (and not a drunken one, as in the film), Vasquez starts sabotaging Kongre's activities on the island. Carcante keeps suggesting leaving, but Kongre (unused to someone annoying him successfully) keeps delaying in order to catch Vasquez and Davis. The end result is that when he thinks he has them cornered, the Chilean boat appears to sink his craft, kill most of his crew, and confront him. Kongre commits suicide to avoid capture.Much of the mayhem of the movie (with Denton picking off crew members one at a time) is not in the book. Nor is there any female character in the novel (a rarity in most of Verne's stories - he could be quite a feminist when he wished). The egotism of \\\"Jonathan\\\" Kongre is well shown by Yul Brynner's performance, but the subtlety of that trait is lost. The writers presumably did not think the audience could appreciate it. Kirk Douglas does well enough as Denton, but his singlehanded success (Vasquez and Davis work together well to the end of the story, unlike Denton's ally who is killed by the pirates) seems unlikely. The bestiality of the pirates is well shown in the movie, particularly a singularly tall actor who in one scene wears women's clothing to particularly unsettling effect. The film is not a bad minor adventure film, but it could have been better if they had stuck to Verne's theme.\",\n \"Remove yourself from the Kirk Douglass aspects of the casting. It is essential to your enjoying the film. There is a beautiful young woman playing double roles - and in the photos from the 1800's, I can't believe how smooth and white her skin is. Also, there is an excellent degrading of the film stock which chills the mind if you like faded greys and yellows as I do. This film is played on TNT from time to time so see it.\",\n \"Surprising that Jules Verne would write such a story. Even more surprising that Hollywood would produce it. Yul Brynner is unbelievably good as a man freed of all bounds of convention, free to indulge his taste for cruelty and domination. Douglass is an excellent counterpoint, a courageous individual who's chosen simple solitude as a way to deal with the complications and turmoil society had imposed on him. And Samantha Egger's character is the capper, a woman willing to sacrifice far too much for comfort and safety. Inotherwords, everyman. The movie does show its age and is limited by the conventions of time and place and technology of the time. If you're looking for special effects and action as substitutes for thought, look elsewhere.\"\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"keywords\",\n \"properties\": {\n \"dtype\": \"object\",\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"sentiment\",\n \"properties\": {\n \"dtype\": \"category\",\n \"num_unique_values\": 2,\n \"samples\": [\n \"Positive\",\n \"Neutral\"\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n }\n ]\n}", + "type": "dataframe" + }, + "text/html": [ + "\n", + "
    \n", + "
    \n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
    reviewkeywordssentiment
    0it is a quite slow pace face-to-face Brynner/D...[slow pace, face-to-face, Brynner, Douglas, he...Neutral
    1Jules Verne wrote about 80 novels as well as p...[Jules Verne, novels, plays, short stories, Ma...Neutral
    2Surprising that Jules Verne would write such a...[Jules Verne, Hollywood, Yul Brynner, cruelty,...Positive
    3The Light At The Edge of the World marks Kirk ...[The Light At The Edge of the World, Kirk Doug...Positive
    4Remove yourself from the Kirk Douglass aspects...[Kirk Douglass casting, beautiful young woman,...Positive
    \n", + "
    \n", + "
    \n", + "\n", + "
    \n", + " \n", + "\n", + " \n", + "\n", + " \n", + "
    \n", + "\n", + "\n", + "
    \n", + " \n", + "\n", + "\n", + "\n", + " \n", + "
    \n", + "\n", + "
    \n", + "
    \n" + ], + "text/plain": [ + " review \\\n", + "0 it is a quite slow pace face-to-face Brynner/D... \n", + "1 Jules Verne wrote about 80 novels as well as p... \n", + "2 Surprising that Jules Verne would write such a... \n", + "3 The Light At The Edge of the World marks Kirk ... \n", + "4 Remove yourself from the Kirk Douglass aspects... \n", + "\n", + " keywords sentiment \n", + "0 [slow pace, face-to-face, Brynner, Douglas, he... Neutral \n", + "1 [Jules Verne, novels, plays, short stories, Ma... Neutral \n", + "2 [Jules Verne, Hollywood, Yul Brynner, cruelty,... Positive \n", + "3 [The Light At The Edge of the World, Kirk Doug... Positive \n", + "4 [Kirk Douglass casting, beautiful young woman,... Positive " + ] + }, + "execution_count": 30, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "%%bigquery --project {PROJECT_ID}\n", + "\n", + "SELECT *\n", + "FROM `bq_ai_tutorial.reviews_keywords_sentiment`" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "T0Hz06RiecoI" + }, + "source": [ + "#### Using arguments to adjust model configuration\n", + "Similar to `AI.GENERATE`, [`AI.GENERATE_TABLE`](https://cloud.google.com/bigquery/docs/reference/standard-sql/bigqueryml-syntax-generate-table) also allows you to customize the parameters for your generation request. The placement of these parameters is within the `STRUCT` value. The [documentation page](https://docs.cloud.google.com/bigquery/docs/reference/standard-sql/bigqueryml-syntax-generate-table) contains the available fields you can define within the `STRUCT`.\n", + "\n", + "Next you'll take the same query and now define values for `temperature` and `max_output_tokens`." + ] + }, + { + "cell_type": "code", + "execution_count": 31, + "metadata": { + "id": "SyGREvkxeXzh" + }, + "outputs": [ + { + "data": { + "text/plain": [ + "Query is running: 0%| |" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + "Downloading: 0%| |" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/vnd.google.colaboratory.intrinsic+json": { + "summary": "{\n \"name\": \"get_ipython()\",\n \"rows\": 5,\n \"fields\": [\n {\n \"column\": \"review\",\n \"properties\": {\n \"dtype\": \"string\",\n \"num_unique_values\": 5,\n \"samples\": [\n \"The Light At The Edge of the World marks Kirk Douglas's second filming of a Jules Verne novel. The first of course was one of his most popular films 20,000 Leagues Under The Sea. But this film is far more serious and has far more adult themes than Walt Disney's film aimed for the kid trade.This was the last novel Jules Verne had published during his lifetime and it's a story of survival against almost impossible odds. In the original novel Kirk Douglas's character was named Vasquez which certainly was more in keeping with someone assigned to lighthouse duty on Cape Horn. But in giving Douglas's character an Anglo name it better explains his presence on the island and it certainly is in keeping with the international tradition of Jules Verne's writings.Cape Horn is one of the loneliest parts of the globe and the geography of the southern tip of South America. Look on a map of the many islands and rocks in that part of the globe and imagine how rough the sea is because it has only limited space. It's not without reason that sailors in all cultures say that no one is really a true sailor until they've made a voyage crossing from the Atlantic to the Pacific Ocean in that area. Remember also this is 1865 as Yul Brynner identifies the year and the Panama Canal had not been built.Which makes the lighthouse at Cape Horn an international concern which was something that is ever present in Jules Verne's writings. But then as now there are malevolent forces in the world and they are in this story Yul Brynner and his pirate crew.On one desultory like any other down there, Yul Brynner's ship docks at the island and kills lighthouse keeper Fernando Rey and his young assistant Massimo Ranieri. By sheer dumb luck Douglas is not at the lighthouse when this happens, but he becomes a hunted man by Brynner and his pirate crew who want to set up headquarters there and use the light to pile up as many wrecks as they can plunder. Also they want to eliminate Douglas who's now the only witness to their crimes.I did like this film very much both when first seeing it in the theater and now on VHS. One thing of interest I found here is that there is no ambiguity, no shadings of character. Kirk Douglas is a good guy and Yul Brynner a bad one, no one is going to walk away thinking anything else. In fact Yul Brynner's pirate captain Jonathan Kongre is the most unredeemable villain we've seen on screen since Lee Marvin as Liberty Valance.Definitely fans of Kirk Douglas, Yul Brynner and Jules Verne should earmark this film for their collection.\",\n \"Jules Verne wrote about 80 novels as well as plays and short stories in his career. He began writing in 1854 with a short story called \\\"Master Zacharias, or the Clockmaker's Soul\\\". It was the first time he talked of the negative side of progress - the evil that results from some discoveries or inventions when they fall into the wrong hands. This becomes a running theme in his novels: Captain Nemo in TWENTY THOUSAND LEAGUES UNDER THE SEA, or Robur (from ROBUR THE CONQUEROR and it's sequel, THE MASTER OF THE WORLD) are two of his best examples of this them. Kongre, in THE LIGHTHOUSE AT THE EDGE OF THE WORLD, is another.Verne was so prolific that when he died in 1905 he left a dozen unpublished novels and stories that were not fully published until 1910. They include some of his best writing, such as THE BARSAC MISSION (partly written by Verne's son Michael), THE SURVIVORS OF THE \\\"JONATHAN\\\", THE PURSUIT OF THE METEOR, THE DANUBE PILOT. All of these dealt with science, but also dealt with political systems, and economics, for Verne was interested in all the problems facing modern man. THE LIGHTHOUSE AT THE EDGE OF THE WORLD was the last novel that was published in Verne's lifetime. It does not deal with the political questions or economic ones that perplexed him, but seems to go back to his potboiler period, when he was turning out stories for money while considering better stories for later publication. But nothing Verne wrote is without interest. Rereading THE LIGHTHOUSE one sees what the subtle point is in it. It is the study of how the ego of a villain can prevent him from escaping retribution.Kongre (renamed Jonathan Kongre) is one of the last pirates in the world of 1900. He and his gang find a damaged boat and repair it. They sail it across the Pacific, and reach Staten Island, a small island in the Straits of Magellan controlled by Chile. There they find a lighthouse with a crew of three men. They manage to kill two of them, but the third one (named Vasquez - he's from Chile, remember), hides on the island. Kongre and his men decide that they should prepare to leave the island shortly, before the Chilean Naval relief boat returns in three months to pick up the lighthouse crew. But first they will wreck any boat that comes to the passage, and increase their ill-gotten gains. But the key to the novel (and it is not in the movie) is that Kongre's right hand men (Carcante and Vargas) keep urging him to pack up his supplies and wealth and head to Asia where the money can be divvied up and everyone separate in safety. And each time Kongre won't do it. Initially it is pure greed. He wrecks a boat, and massacres the crew (a scene that is done in the film). The sole survivor is an American, John Davis (the name became Denton in the film, except that it was given to the character of Vasquez). Now with an ally (and not a drunken one, as in the film), Vasquez starts sabotaging Kongre's activities on the island. Carcante keeps suggesting leaving, but Kongre (unused to someone annoying him successfully) keeps delaying in order to catch Vasquez and Davis. The end result is that when he thinks he has them cornered, the Chilean boat appears to sink his craft, kill most of his crew, and confront him. Kongre commits suicide to avoid capture.Much of the mayhem of the movie (with Denton picking off crew members one at a time) is not in the book. Nor is there any female character in the novel (a rarity in most of Verne's stories - he could be quite a feminist when he wished). The egotism of \\\"Jonathan\\\" Kongre is well shown by Yul Brynner's performance, but the subtlety of that trait is lost. The writers presumably did not think the audience could appreciate it. Kirk Douglas does well enough as Denton, but his singlehanded success (Vasquez and Davis work together well to the end of the story, unlike Denton's ally who is killed by the pirates) seems unlikely. The bestiality of the pirates is well shown in the movie, particularly a singularly tall actor who in one scene wears women's clothing to particularly unsettling effect. The film is not a bad minor adventure film, but it could have been better if they had stuck to Verne's theme.\",\n \"Remove yourself from the Kirk Douglass aspects of the casting. It is essential to your enjoying the film. There is a beautiful young woman playing double roles - and in the photos from the 1800's, I can't believe how smooth and white her skin is. Also, there is an excellent degrading of the film stock which chills the mind if you like faded greys and yellows as I do. This film is played on TNT from time to time so see it.\"\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"keywords\",\n \"properties\": {\n \"dtype\": \"object\",\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"sentiment\",\n \"properties\": {\n \"dtype\": \"string\",\n \"num_unique_values\": 3,\n \"samples\": [\n \"Neutral\",\n \"Positive\",\n \"Negative\"\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n }\n ]\n}", + "type": "dataframe" + }, + "text/html": [ + "\n", + "
    \n", + "
    \n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
    reviewkeywordssentiment
    0it is a quite slow pace face-to-face Brynner/D...[slow pace, heavy violence/gore, seventies gia...Neutral
    1The Light At The Edge of the World marks Kirk ...[Kirk Douglas, Jules Verne, The Light At The E...Positive
    2Remove yourself from the Kirk Douglass aspects...[beautiful young woman, double roles, film sto...Positive
    3Surprising that Jules Verne would write such a...[Jules Verne, Hollywood, Yul Brynner, cruelty ...Positive
    4Jules Verne wrote about 80 novels as well as p...[Jules Verne, novels, themes, negative side of...Negative
    \n", + "
    \n", + "
    \n", + "\n", + "
    \n", + " \n", + "\n", + " \n", + "\n", + " \n", + "
    \n", + "\n", + "\n", + "
    \n", + " \n", + "\n", + "\n", + "\n", + " \n", + "
    \n", + "\n", + "
    \n", + "
    \n" + ], + "text/plain": [ + " review \\\n", + "0 it is a quite slow pace face-to-face Brynner/D... \n", + "1 The Light At The Edge of the World marks Kirk ... \n", + "2 Remove yourself from the Kirk Douglass aspects... \n", + "3 Surprising that Jules Verne would write such a... \n", + "4 Jules Verne wrote about 80 novels as well as p... \n", + "\n", + " keywords sentiment \n", + "0 [slow pace, heavy violence/gore, seventies gia... Neutral \n", + "1 [Kirk Douglas, Jules Verne, The Light At The E... Positive \n", + "2 [beautiful young woman, double roles, film sto... Positive \n", + "3 [Jules Verne, Hollywood, Yul Brynner, cruelty ... Positive \n", + "4 [Jules Verne, novels, themes, negative side of... Negative " + ] + }, + "execution_count": 31, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "%%bigquery --project {PROJECT_ID}\n", + "\n", + "SELECT\n", + " review,\n", + " keywords,\n", + " sentiment\n", + "FROM\n", + " AI.GENERATE_TABLE(\n", + " MODEL `bq_ai_tutorial.gemini_2_5_flash`,\n", + " (\n", + " SELECT\n", + " review,\n", + " ('Extract the key words and sentiment (Positive, Negative, or Neutral) from the following review:',review) AS prompt,\n", + " FROM\n", + " `bigquery-public-data.imdb.reviews`\n", + " WHERE movie_id = \"tt0067345\"\n", + " LIMIT 5\n", + " ),\n", + " STRUCT(\n", + " 'keywords ARRAY< STRING>, sentiment STRING' AS output_schema,\n", + " 0.5 AS temperature,\n", + " 1000 AS max_output_tokens\n", + " )\n", + " );" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "tN51GtTMYEoF" + }, + "source": [ + "## Generate text with partner and open models with `AI.GENERATE_TEXT`\n", + "\n", + "The tutorial thus far has performed analysis using the [Gemini family of models](https://cloud.google.com/vertex-ai/generative-ai/docs/models#generally_available_models). However, the generative analysis capabilities of BigQuery extend to several [partner models](https://cloud.google.com/bigquery/docs/generate-text#enable-model), including Anthropic Claude, Llama, and Mistral AI, as well as [open models](https://cloud.google.com/bigquery/docs/generate-text#deploy_an_open_model) from the [Vertex AI Model Garden and Hugging Face](https://cloud.google.com/bigquery/docs/reference/standard-sql/bigqueryml-syntax-create-remote-model-open#supported_open_models).\n", + "\n", + "When choosing a partner or open model, you will use the [`AI.GENERATE_TEXT` function](https://docs.cloud.google.com/bigquery/docs/reference/standard-sql/bigqueryml-syntax-ai-generate-text) to perform your analysis. The syntax for the `AI.GENERATE_TEXT` function is similar to `AI.GENERATE_TABLE`. Let's walk through an example analysis using a partner model." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "RB90JcraZrwf" + }, + "source": [ + "### Enabling a partner model\n", + "\n", + "You must enable partner models in Vertex AI before you can use them in BigQuery.\n", + "\n", + "**BEFORE MOVING AHEAD**, enable the [**Anthropic Claude 3 Haiku** model](https://console.cloud.google.com/vertex-ai/publishers/anthropic/model-garden/claude-3-haiku) by following the [instructions in the documentation](https://cloud.google.com/bigquery/docs/generate-text#enable-model)." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "plOQu7eIjeEs" + }, + "source": [ + "### Create a remote model for Anthropic Claude 3 Haiku\n", + "\n", + "Now you'll create a new remote model, pointing to the `claude-3-haiku@20240307` model endpoint." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "wrYOI_HGjQT2" + }, + "outputs": [], + "source": [ + "%%bigquery --project {PROJECT_ID}\n", + "\n", + "CREATE OR REPLACE MODEL\n", + " `bq_ai_tutorial.claude_3_haiku`\n", + "REMOTE WITH\n", + " CONNECTION `us.test_connection`\n", + " OPTIONS (ENDPOINT = 'claude-3-haiku@20240307');" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "vtJ5nuMbmY0C" + }, + "source": [ + "### Run analysis using the partner model\n", + "\n", + "Now you are ready to use the remote model with an [`AI.GENERATE_TEXT` function](https://docs.cloud.google.com/bigquery/docs/reference/standard-sql/bigqueryml-syntax-ai-generate-text). The following query repeats your previous analysis done to extract key words and sentiment from the reviews, now using Anthropic Claude 3 Haiku." + ] + }, + { + "cell_type": "code", + "execution_count": 52, + "metadata": { + "id": "HaPTzBYrZnr4" + }, + "outputs": [ + { + "data": { + "text/plain": [ + "Query is running: 0%| |" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + "Downloading: 0%| |" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/vnd.google.colaboratory.intrinsic+json": { + "summary": "{\n \"name\": \"get_ipython()\",\n \"rows\": 5,\n \"fields\": [\n {\n \"column\": \"review\",\n \"properties\": {\n \"dtype\": \"string\",\n \"num_unique_values\": 5,\n \"samples\": [\n \"Remove yourself from the Kirk Douglass aspects of the casting. It is essential to your enjoying the film. There is a beautiful young woman playing double roles - and in the photos from the 1800's, I can't believe how smooth and white her skin is. Also, there is an excellent degrading of the film stock which chills the mind if you like faded greys and yellows as I do. This film is played on TNT from time to time so see it.\",\n \"it is a quite slow pace face-to-face Brynner/Douglas...i found there was some pretty heavy violence/gore but you know, seventies giallo style : (maybe some spoiler ahead) a little bit phony and fake but the image is there : the monkey torn apart, the flesh torn from the mechanic, etc...i don't remember if it is alike the novel...but overall i think it is a OK action flick, hero flick : he stands all alone at the end, all the bad guys are out..........and nice images too : a small rocky island, the sun over the seabut not a typical Verne's story-on-cinema : subs, sci-fi; but the adventure seen in a lot of Verne's novels tough\",\n \"Surprising that Jules Verne would write such a story. Even more surprising that Hollywood would produce it. Yul Brynner is unbelievably good as a man freed of all bounds of convention, free to indulge his taste for cruelty and domination. Douglass is an excellent counterpoint, a courageous individual who's chosen simple solitude as a way to deal with the complications and turmoil society had imposed on him. And Samantha Egger's character is the capper, a woman willing to sacrifice far too much for comfort and safety. Inotherwords, everyman. The movie does show its age and is limited by the conventions of time and place and technology of the time. If you're looking for special effects and action as substitutes for thought, look elsewhere.\"\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"result\",\n \"properties\": {\n \"dtype\": \"string\",\n \"num_unique_values\": 5,\n \"samples\": [\n \"Keywords:\\n- Kirk Douglass\\n- double roles\\n- 1800's\\n- film stock\\n- faded greys and yellows\\n- TNT\\n\\nSentiment:\\nPositive\\n\\nThe review overall has a positive sentiment. The reviewer praises the casting, the performance of the young woman playing double roles, and the excellent degrading of the film stock. The reviewer also recommends watching the film on TNT, indicating a positive recommendation.\",\n \"Keywords:\\n- slow pace\\n- face-to-face Brynner/Douglas\\n- heavy violence/gore\\n- seventies giallo style\\n- phony and fake\\n- monkey torn apart\\n- flesh torn from the mechanic\\n- not alike the novel\\n- OK action flick\\n- hero flick\\n- stands all alone at the end\\n- bad guys are out\\n- small rocky island\\n- sun over the sea\\n- not a typical Verne's story-on-cinema\\n- subs, sci-fi\\n- adventure seen in a lot of Verne's novels\\n\\nSentiment:\\nThe review has a mix of positive, negative, and neutral sentiments.\\n\\nPositive:\\n- \\\"nice images too: a small rocky island, the sun over the sea\\\"\\n- \\\"OK action flick, hero flick\\\"\\n\\nNegative:\\n- \\\"quite slow pace\\\"\\n- \\\"heavy violence/gore\\\"\\n- \\\"a little bit phony and fake\\\"\\n\\nNeutral:\\n- \\\"not alike the novel\\\"\\n- \\\"not a typical Verne's story-on-cinema\\\"\\n- \\\"subs, sci-fi; but the adventure seen in a lot of Verne's novels tough\\\"\",\n \"Keywords:\\n- Jules Verne\\n- Hollywood\\n- Yul Brynner\\n- Douglass\\n- Samantha Egger\\n- society\\n- special effects\\n- action\\n- thought\\n\\nSentiment:\\nThe overall sentiment of the review is Positive. The reviewer praises the performances of the actors, particularly Yul Brynner, and the complexity of the characters and themes in the movie. However, the reviewer also notes that the movie \\\"shows its age\\\" and that it is \\\"limited by the conventions of time and place and technology of the time,\\\" suggesting a slightly Negative sentiment towards some aspects of the film.\"\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n }\n ]\n}", + "type": "dataframe" + }, + "text/html": [ + "\n", + "
    \n", + "
    \n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
    reviewresult
    0The Light At The Edge of the World marks Kirk ...Keywords:\\n1. Jules Verne\\n2. Kirk Douglas\\n3....
    1Remove yourself from the Kirk Douglass aspects...Keywords:\\n- Kirk Douglass\\n- double roles\\n- ...
    2Surprising that Jules Verne would write such a...Keywords:\\n- Jules Verne\\n- Hollywood\\n- Yul B...
    3Jules Verne wrote about 80 novels as well as p...Keywords:\\n- Jules Verne\\n- Novels\\n- Progress...
    4it is a quite slow pace face-to-face Brynner/D...Keywords:\\n- slow pace\\n- face-to-face Brynner...
    \n", + "
    \n", + "
    \n", + "\n", + "
    \n", + " \n", + "\n", + " \n", + "\n", + " \n", + "
    \n", + "\n", + "\n", + "
    \n", + " \n", + "\n", + "\n", + "\n", + " \n", + "
    \n", + "\n", + "
    \n", + "
    \n" + ], + "text/plain": [ + " review \\\n", + "0 The Light At The Edge of the World marks Kirk ... \n", + "1 Remove yourself from the Kirk Douglass aspects... \n", + "2 Surprising that Jules Verne would write such a... \n", + "3 Jules Verne wrote about 80 novels as well as p... \n", + "4 it is a quite slow pace face-to-face Brynner/D... \n", + "\n", + " result \n", + "0 Keywords:\\n1. Jules Verne\\n2. Kirk Douglas\\n3.... \n", + "1 Keywords:\\n- Kirk Douglass\\n- double roles\\n- ... \n", + "2 Keywords:\\n- Jules Verne\\n- Hollywood\\n- Yul B... \n", + "3 Keywords:\\n- Jules Verne\\n- Novels\\n- Progress... \n", + "4 Keywords:\\n- slow pace\\n- face-to-face Brynner... " + ] + }, + "execution_count": 52, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "%%bigquery --project {PROJECT_ID}\n", + "\n", + "SELECT\n", + " review,\n", + " result\n", + "FROM\n", + " AI.GENERATE_TEXT(\n", + " MODEL `bq_ai_tutorial.claude_3_haiku`,\n", + " (\n", + " SELECT\n", + " review,\n", + " CONCAT('Extract the keywords and sentiment (Positive, Negative, or Neutral) from the following review:',review) AS prompt,\n", + " FROM\n", + " `bigquery-public-data.imdb.reviews`\n", + " WHERE movie_id = \"tt0067345\"\n", + " LIMIT 5\n", + " ),\n", + " STRUCT(\n", + " 500 AS max_output_tokens\n", + " )\n", + " );" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "scalar_md" + }, + "source": [ + "## Perform row-level AI with scalar functions `AI.GENERATE_BOOL`, `AI.GENERATE_DOUBLE`, and `AI.GENERATE_INT`\n", + "\n", + "BigQuery's scalar AI functions ([`AI.GENERATE_BOOL`](https://cloud.google.com/bigquery/docs/reference/standard-sql/bigqueryml-syntax-ai-generate-bool), [`AI.GENERATE_DOUBLE`](https://cloud.google.com/bigquery/docs/reference/standard-sql/bigqueryml-syntax-ai-generate-double), and [`AI.GENERATE_INT`](https://cloud.google.com/bigquery/docs/reference/standard-sql/bigqueryml-syntax-ai-generate-int)) bring the power of LLMs for AI-driven filtering, classification, and data extraction directly within your SQL queries.\n", + "\n", + "On the surface, these functions perform tasks similar to that of using `AI.GENERATE` with a defined `output_schema` of data type of `BOOL`, `FLOAT64`, or `INT64`. However, these functions aren't only available for use in a `SELECT` clause, but also in a `WHERE` clause, giving you the power to do complex filtering with natural language. The next few examples focus on the filtering use case.\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "generate_bool_md" + }, + "source": [ + "### Using `AI.GENERATE_BOOL`: Filtering for drought tolerant tree species\n", + "\n", + "Let's run the next query to take a look at the `bigquery-public-data.new_york_trees.tree_species` table." + ] + }, + { + "cell_type": "code", + "execution_count": 36, + "metadata": { + "id": "YuKFqCK3oHXg" + }, + "outputs": [ + { + "data": { + "text/plain": [ + "Query is running: 0%| |" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + "Downloading: 0%| |" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/vnd.google.colaboratory.intrinsic+json": { + "summary": "{\n \"name\": \"get_ipython()\",\n \"rows\": 57,\n \"fields\": [\n {\n \"column\": \"species_scientific_name\",\n \"properties\": {\n \"dtype\": \"string\",\n \"num_unique_values\": 57,\n \"samples\": [\n \"Quercus phellos\",\n \"Carpinus caroliniana\",\n \"Prunus cerasifera\"\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"species_common_name\",\n \"properties\": {\n \"dtype\": \"string\",\n \"num_unique_values\": 57,\n \"samples\": [\n \"Willow Oak\",\n \"American Hornbeam\",\n \"Purpleleaf Plum\"\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n }\n ]\n}", + "type": "dataframe" + }, + "text/html": [ + "\n", + "
    \n", + "
    \n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
    species_scientific_namespecies_common_name
    0Quercus phellosWillow Oak
    1Cotinus sp.Smoke Tree
    2Prunus sargentiiSargent Cherry
    3Prunus padusEuropean Birdcherry
    4Prunus serrulata 'Kwanzan'Japanese Flowering Cherry
    5Carpinus carolinianaAmerican Hornbeam
    6Acer ginnalaAmur Maple
    7Fraxinus 'Leprechaun'Leprechaun Green Ash
    8Amelanchier sp.Serviceberry
    9Malus sp.Crabapple
    10Lagerstroemia indicaCrapemyrtle
    11Eucommia ulmoidesHardy Rubber Tree
    12Ostrya virginianaAmerican Hophornbeam
    13Acer truncatumShantung Maple
    14Acer campestreHedge Maple
    15Maackia amurensisAmur Maackia
    16Quercus imbricariaShingle Oak
    17Quercus rubraNorthern Red Oak
    18Gymnocladus dioicusCoffeetree
    19Tilia x euchloraCrimean Linden
    20Tilia tomentosaSilver Linden
    21Celtis occidentalisHackberry
    22Tilia americanaAmerican Linden
    23Quercus bicolorSwamp White Oak
    24Quercus palustrisPin Oak
    25Platanus x acerifoliaLondon Plane
    26Gleditsia triacanthos var. inermisHoneylocust
    27Prunus 'Okame'Okame Cherry
    28Prunus x yedoensisYoshino Cherry
    29Cornus masCornelian Cherry
    30Prunus cerasiferaPurpleleaf Plum
    31Crataegus sp.Hawthorn
    32Cercis canadensisEastern Redbud
    33Syringa reticulataJapanese Tree Lilac
    34Ulmus parvifoliaChinese Elm
    35Cercidiphyllum japonicumKatsura Tree
    36Acer rubrumRed Maple
    37Quercus acutissimaSawtooth Oak
    38Styphnolobium japonicumScholar Tree
    39Koelreuteria paniculataGoldenraintree
    40Pyrus calleryanaCallery Pear
    41Quercus spp. 'Fastigiata'Fastigiata Oak
    42Ginkgo bilobaGinkgo
    43Quercus roburEnglish Oak
    44Carpinus betulusEuropean Hornbeam
    45Liriodendron tulipiferaTulip Tree
    46Liquidambar styracifluaSweetgum
    47Tilia cordataLittleleaf Linden
    48Fraxinus pennsylvanicaGreen Ash
    49Fraxinus americanaWhite Ash
    50Metasequoia glyptostroboidesDawn Redwood
    51Taxodium distichumBaldcypress
    52Prunus virginiana 'Schubert'Schubert Cherry
    53Nyssa sylvaticaBlack Gum
    54Corylus colurnaTurkish Filbert
    55Ulmus americanaAmerican Elm
    56Zelkova serrataJapanese Zelkova
    \n", + "
    \n", + "
    \n", + "\n", + "
    \n", + " \n", + "\n", + " \n", + "\n", + " \n", + "
    \n", + "\n", + "\n", + "
    \n", + " \n", + "\n", + "\n", + "\n", + " \n", + "
    \n", + "\n", + "
    \n", + "
    \n" + ], + "text/plain": [ + " species_scientific_name species_common_name\n", + "0 Quercus phellos Willow Oak\n", + "1 Cotinus sp. Smoke Tree\n", + "2 Prunus sargentii Sargent Cherry\n", + "3 Prunus padus European Birdcherry\n", + "4 Prunus serrulata 'Kwanzan' Japanese Flowering Cherry\n", + "5 Carpinus caroliniana American Hornbeam\n", + "6 Acer ginnala Amur Maple\n", + "7 Fraxinus 'Leprechaun' Leprechaun Green Ash\n", + "8 Amelanchier sp. Serviceberry\n", + "9 Malus sp. Crabapple\n", + "10 Lagerstroemia indica Crapemyrtle\n", + "11 Eucommia ulmoides Hardy Rubber Tree\n", + "12 Ostrya virginiana American Hophornbeam\n", + "13 Acer truncatum Shantung Maple\n", + "14 Acer campestre Hedge Maple\n", + "15 Maackia amurensis Amur Maackia\n", + "16 Quercus imbricaria Shingle Oak\n", + "17 Quercus rubra Northern Red Oak\n", + "18 Gymnocladus dioicus Coffeetree\n", + "19 Tilia x euchlora Crimean Linden\n", + "20 Tilia tomentosa Silver Linden\n", + "21 Celtis occidentalis Hackberry\n", + "22 Tilia americana American Linden\n", + "23 Quercus bicolor Swamp White Oak\n", + "24 Quercus palustris Pin Oak\n", + "25 Platanus x acerifolia London Plane\n", + "26 Gleditsia triacanthos var. inermis Honeylocust\n", + "27 Prunus 'Okame' Okame Cherry\n", + "28 Prunus x yedoensis Yoshino Cherry\n", + "29 Cornus mas Cornelian Cherry\n", + "30 Prunus cerasifera Purpleleaf Plum\n", + "31 Crataegus sp. Hawthorn\n", + "32 Cercis canadensis Eastern Redbud\n", + "33 Syringa reticulata Japanese Tree Lilac\n", + "34 Ulmus parvifolia Chinese Elm\n", + "35 Cercidiphyllum japonicum Katsura Tree\n", + "36 Acer rubrum Red Maple\n", + "37 Quercus acutissima Sawtooth Oak\n", + "38 Styphnolobium japonicum Scholar Tree\n", + "39 Koelreuteria paniculata Goldenraintree\n", + "40 Pyrus calleryana Callery Pear\n", + "41 Quercus spp. 'Fastigiata' Fastigiata Oak\n", + "42 Ginkgo biloba Ginkgo\n", + "43 Quercus robur English Oak\n", + "44 Carpinus betulus European Hornbeam\n", + "45 Liriodendron tulipifera Tulip Tree\n", + "46 Liquidambar styraciflua Sweetgum\n", + "47 Tilia cordata Littleleaf Linden\n", + "48 Fraxinus pennsylvanica Green Ash\n", + "49 Fraxinus americana White Ash\n", + "50 Metasequoia glyptostroboides Dawn Redwood\n", + "51 Taxodium distichum Baldcypress\n", + "52 Prunus virginiana 'Schubert' Schubert Cherry\n", + "53 Nyssa sylvatica Black Gum\n", + "54 Corylus colurna Turkish Filbert\n", + "55 Ulmus americana American Elm\n", + "56 Zelkova serrata Japanese Zelkova" + ] + }, + "execution_count": 36, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "%%bigquery --project {PROJECT_ID}\n", + "\n", + "SELECT\n", + " species_scientific_name,\n", + " species_common_name\n", + "FROM\n", + " `bigquery-public-data.new_york_trees.tree_species`" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "f8om0-IQoOiX" + }, + "source": [ + "Next, let's use [`AI.GENERATE_BOOL`](https://cloud.google.com/bigquery/docs/reference/standard-sql/bigqueryml-syntax-ai-generate-bool) to return only tree species that are drought tolerant." + ] + }, + { + "cell_type": "code", + "execution_count": 37, + "metadata": { + "id": "generate_bool_code" + }, + "outputs": [ + { + "data": { + "text/plain": [ + "Query is running: 0%| |" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + "Downloading: 0%| |" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/vnd.google.colaboratory.intrinsic+json": { + "summary": "{\n \"name\": \"get_ipython()\",\n \"rows\": 39,\n \"fields\": [\n {\n \"column\": \"species_scientific_name\",\n \"properties\": {\n \"dtype\": \"string\",\n \"num_unique_values\": 39,\n \"samples\": [\n \"Lagerstroemia indica\",\n \"Gymnocladus dioicus\",\n \"Quercus bicolor\"\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"species_common_name\",\n \"properties\": {\n \"dtype\": \"string\",\n \"num_unique_values\": 39,\n \"samples\": [\n \"Crapemyrtle\",\n \"Coffeetree\",\n \"Swamp White Oak\"\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n }\n ]\n}", + "type": "dataframe" + }, + "text/html": [ + "\n", + "
    \n", + "
    \n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
    species_scientific_namespecies_common_name
    0Koelreuteria paniculataGoldenraintree
    1Corylus colurnaTurkish Filbert
    2Ostrya virginianaAmerican Hophornbeam
    3Cornus masCornelian Cherry
    4Quercus bicolorSwamp White Oak
    5Crataegus sp.Hawthorn
    6Styphnolobium japonicumScholar Tree
    7Prunus virginiana 'Schubert'Schubert Cherry
    8Cercis canadensisEastern Redbud
    9Quercus spp. 'Fastigiata'Fastigiata Oak
    10Liquidambar styracifluaSweetgum
    11Platanus x acerifoliaLondon Plane
    12Prunus 'Okame'Okame Cherry
    13Ulmus parvifoliaChinese Elm
    14Fraxinus pennsylvanicaGreen Ash
    15Zelkova serrataJapanese Zelkova
    16Cotinus sp.Smoke Tree
    17Acer ginnalaAmur Maple
    18Amelanchier sp.Serviceberry
    19Acer campestreHedge Maple
    20Gleditsia triacanthos var. inermisHoneylocust
    21Acer truncatumShantung Maple
    22Celtis occidentalisHackberry
    23Ginkgo bilobaGinkgo
    24Fraxinus americanaWhite Ash
    25Carpinus carolinianaAmerican Hornbeam
    26Eucommia ulmoidesHardy Rubber Tree
    27Quercus rubraNorthern Red Oak
    28Tilia tomentosaSilver Linden
    29Prunus cerasiferaPurpleleaf Plum
    30Syringa reticulataJapanese Tree Lilac
    31Quercus acutissimaSawtooth Oak
    32Nyssa sylvaticaBlack Gum
    33Lagerstroemia indicaCrapemyrtle
    34Maackia amurensisAmur Maackia
    35Quercus imbricariaShingle Oak
    36Gymnocladus dioicusCoffeetree
    37Pyrus calleryanaCallery Pear
    38Quercus roburEnglish Oak
    \n", + "
    \n", + "
    \n", + "\n", + "
    \n", + " \n", + "\n", + " \n", + "\n", + " \n", + "
    \n", + "\n", + "\n", + "
    \n", + " \n", + "\n", + "\n", + "\n", + " \n", + "
    \n", + "\n", + "
    \n", + "
    \n" + ], + "text/plain": [ + " species_scientific_name species_common_name\n", + "0 Koelreuteria paniculata Goldenraintree\n", + "1 Corylus colurna Turkish Filbert\n", + "2 Ostrya virginiana American Hophornbeam\n", + "3 Cornus mas Cornelian Cherry\n", + "4 Quercus bicolor Swamp White Oak\n", + "5 Crataegus sp. Hawthorn\n", + "6 Styphnolobium japonicum Scholar Tree\n", + "7 Prunus virginiana 'Schubert' Schubert Cherry\n", + "8 Cercis canadensis Eastern Redbud\n", + "9 Quercus spp. 'Fastigiata' Fastigiata Oak\n", + "10 Liquidambar styraciflua Sweetgum\n", + "11 Platanus x acerifolia London Plane\n", + "12 Prunus 'Okame' Okame Cherry\n", + "13 Ulmus parvifolia Chinese Elm\n", + "14 Fraxinus pennsylvanica Green Ash\n", + "15 Zelkova serrata Japanese Zelkova\n", + "16 Cotinus sp. Smoke Tree\n", + "17 Acer ginnala Amur Maple\n", + "18 Amelanchier sp. Serviceberry\n", + "19 Acer campestre Hedge Maple\n", + "20 Gleditsia triacanthos var. inermis Honeylocust\n", + "21 Acer truncatum Shantung Maple\n", + "22 Celtis occidentalis Hackberry\n", + "23 Ginkgo biloba Ginkgo\n", + "24 Fraxinus americana White Ash\n", + "25 Carpinus caroliniana American Hornbeam\n", + "26 Eucommia ulmoides Hardy Rubber Tree\n", + "27 Quercus rubra Northern Red Oak\n", + "28 Tilia tomentosa Silver Linden\n", + "29 Prunus cerasifera Purpleleaf Plum\n", + "30 Syringa reticulata Japanese Tree Lilac\n", + "31 Quercus acutissima Sawtooth Oak\n", + "32 Nyssa sylvatica Black Gum\n", + "33 Lagerstroemia indica Crapemyrtle\n", + "34 Maackia amurensis Amur Maackia\n", + "35 Quercus imbricaria Shingle Oak\n", + "36 Gymnocladus dioicus Coffeetree\n", + "37 Pyrus calleryana Callery Pear\n", + "38 Quercus robur English Oak" + ] + }, + "execution_count": 37, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "%%bigquery --project {PROJECT_ID}\n", + "\n", + "SELECT\n", + " species_scientific_name,\n", + " species_common_name\n", + "FROM\n", + " `bigquery-public-data.new_york_trees.tree_species`\n", + "WHERE\n", + " AI.GENERATE_BOOL(\n", + " ('Is this tree species drought tolerant?', species_scientific_name),\n", + " connection_id => 'us.test_connection',\n", + " endpoint => 'gemini-2.5-flash').result = true" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "generate_double_md" + }, + "source": [ + "### Using `AI.GENERATE_DOUBLE`: Filtering for tree species with a minimum average lifespan\n", + "\n", + "With [`AI.GENERATE_DOUBLE`](https://cloud.google.com/bigquery/docs/reference/standard-sql/bigqueryml-syntax-ai-generate-double), we can get a numerical response of type `FLOAT64`, such as average lifespan in years of each tree species. Again, we'll use `bigquery-public-data.new_york_trees.tree_species` table, this time filtering for species that meet a minimum average lifespan requirement of 19 years." + ] + }, + { + "cell_type": "code", + "execution_count": 38, + "metadata": { + "id": "xT9vakyw1PZ2" + }, + "outputs": [ + { + "data": { + "text/plain": [ + "Query is running: 0%| |" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + "Downloading: 0%| |" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/vnd.google.colaboratory.intrinsic+json": { + "summary": "{\n \"name\": \"get_ipython()\",\n \"rows\": 57,\n \"fields\": [\n {\n \"column\": \"species_scientific_name\",\n \"properties\": {\n \"dtype\": \"string\",\n \"num_unique_values\": 57,\n \"samples\": [\n \"Ostrya virginiana\",\n \"Cornus mas\",\n \"Acer rubrum\"\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"species_common_name\",\n \"properties\": {\n \"dtype\": \"string\",\n \"num_unique_values\": 57,\n \"samples\": [\n \"American Hophornbeam\",\n \"Cornelian Cherry\",\n \"Red Maple\"\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n }\n ]\n}", + "type": "dataframe" + }, + "text/html": [ + "\n", + "
    \n", + "
    \n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
    species_scientific_namespecies_common_name
    0Ostrya virginianaAmerican Hophornbeam
    1Carpinus betulusEuropean Hornbeam
    2Metasequoia glyptostroboidesDawn Redwood
    3Prunus serrulata 'Kwanzan'Japanese Flowering Cherry
    4Prunus x yedoensisYoshino Cherry
    5Cornus masCornelian Cherry
    6Liriodendron tulipiferaTulip Tree
    7Cotinus sp.Smoke Tree
    8Acer ginnalaAmur Maple
    9Amelanchier sp.Serviceberry
    10Acer campestreHedge Maple
    11Gleditsia triacanthos var. inermisHoneylocust
    12Tilia cordataLittleleaf Linden
    13Platanus x acerifoliaLondon Plane
    14Prunus 'Okame'Okame Cherry
    15Ulmus parvifoliaChinese Elm
    16Cercidiphyllum japonicumKatsura Tree
    17Fraxinus pennsylvanicaGreen Ash
    18Zelkova serrataJapanese Zelkova
    19Carpinus carolinianaAmerican Hornbeam
    20Eucommia ulmoidesHardy Rubber Tree
    21Quercus rubraNorthern Red Oak
    22Tilia tomentosaSilver Linden
    23Prunus cerasiferaPurpleleaf Plum
    24Syringa reticulataJapanese Tree Lilac
    25Quercus acutissimaSawtooth Oak
    26Nyssa sylvaticaBlack Gum
    27Acer truncatumShantung Maple
    28Celtis occidentalisHackberry
    29Tilia americanaAmerican Linden
    30Acer rubrumRed Maple
    31Ginkgo bilobaGinkgo
    32Fraxinus americanaWhite Ash
    33Malus sp.Crabapple
    34Quercus bicolorSwamp White Oak
    35Quercus palustrisPin Oak
    36Crataegus sp.Hawthorn
    37Styphnolobium japonicumScholar Tree
    38Prunus virginiana 'Schubert'Schubert Cherry
    39Koelreuteria paniculataGoldenraintree
    40Corylus colurnaTurkish Filbert
    41Prunus sargentiiSargent Cherry
    42Cercis canadensisEastern Redbud
    43Quercus spp. 'Fastigiata'Fastigiata Oak
    44Liquidambar styracifluaSweetgum
    45Taxodium distichumBaldcypress
    46Quercus phellosWillow Oak
    47Prunus padusEuropean Birdcherry
    48Fraxinus 'Leprechaun'Leprechaun Green Ash
    49Lagerstroemia indicaCrapemyrtle
    50Maackia amurensisAmur Maackia
    51Quercus imbricariaShingle Oak
    52Gymnocladus dioicusCoffeetree
    53Tilia x euchloraCrimean Linden
    54Pyrus calleryanaCallery Pear
    55Quercus roburEnglish Oak
    56Ulmus americanaAmerican Elm
    \n", + "
    \n", + "
    \n", + "\n", + "
    \n", + " \n", + "\n", + " \n", + "\n", + " \n", + "
    \n", + "\n", + "\n", + "
    \n", + " \n", + "\n", + "\n", + "\n", + " \n", + "
    \n", + "\n", + "
    \n", + "
    \n" + ], + "text/plain": [ + " species_scientific_name species_common_name\n", + "0 Ostrya virginiana American Hophornbeam\n", + "1 Carpinus betulus European Hornbeam\n", + "2 Metasequoia glyptostroboides Dawn Redwood\n", + "3 Prunus serrulata 'Kwanzan' Japanese Flowering Cherry\n", + "4 Prunus x yedoensis Yoshino Cherry\n", + "5 Cornus mas Cornelian Cherry\n", + "6 Liriodendron tulipifera Tulip Tree\n", + "7 Cotinus sp. Smoke Tree\n", + "8 Acer ginnala Amur Maple\n", + "9 Amelanchier sp. Serviceberry\n", + "10 Acer campestre Hedge Maple\n", + "11 Gleditsia triacanthos var. inermis Honeylocust\n", + "12 Tilia cordata Littleleaf Linden\n", + "13 Platanus x acerifolia London Plane\n", + "14 Prunus 'Okame' Okame Cherry\n", + "15 Ulmus parvifolia Chinese Elm\n", + "16 Cercidiphyllum japonicum Katsura Tree\n", + "17 Fraxinus pennsylvanica Green Ash\n", + "18 Zelkova serrata Japanese Zelkova\n", + "19 Carpinus caroliniana American Hornbeam\n", + "20 Eucommia ulmoides Hardy Rubber Tree\n", + "21 Quercus rubra Northern Red Oak\n", + "22 Tilia tomentosa Silver Linden\n", + "23 Prunus cerasifera Purpleleaf Plum\n", + "24 Syringa reticulata Japanese Tree Lilac\n", + "25 Quercus acutissima Sawtooth Oak\n", + "26 Nyssa sylvatica Black Gum\n", + "27 Acer truncatum Shantung Maple\n", + "28 Celtis occidentalis Hackberry\n", + "29 Tilia americana American Linden\n", + "30 Acer rubrum Red Maple\n", + "31 Ginkgo biloba Ginkgo\n", + "32 Fraxinus americana White Ash\n", + "33 Malus sp. Crabapple\n", + "34 Quercus bicolor Swamp White Oak\n", + "35 Quercus palustris Pin Oak\n", + "36 Crataegus sp. Hawthorn\n", + "37 Styphnolobium japonicum Scholar Tree\n", + "38 Prunus virginiana 'Schubert' Schubert Cherry\n", + "39 Koelreuteria paniculata Goldenraintree\n", + "40 Corylus colurna Turkish Filbert\n", + "41 Prunus sargentii Sargent Cherry\n", + "42 Cercis canadensis Eastern Redbud\n", + "43 Quercus spp. 'Fastigiata' Fastigiata Oak\n", + "44 Liquidambar styraciflua Sweetgum\n", + "45 Taxodium distichum Baldcypress\n", + "46 Quercus phellos Willow Oak\n", + "47 Prunus padus European Birdcherry\n", + "48 Fraxinus 'Leprechaun' Leprechaun Green Ash\n", + "49 Lagerstroemia indica Crapemyrtle\n", + "50 Maackia amurensis Amur Maackia\n", + "51 Quercus imbricaria Shingle Oak\n", + "52 Gymnocladus dioicus Coffeetree\n", + "53 Tilia x euchlora Crimean Linden\n", + "54 Pyrus calleryana Callery Pear\n", + "55 Quercus robur English Oak\n", + "56 Ulmus americana American Elm" + ] + }, + "execution_count": 38, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "%%bigquery --project {PROJECT_ID}\n", + "\n", + "SELECT\n", + " species_scientific_name,\n", + " species_common_name\n", + "FROM\n", + " `bigquery-public-data.new_york_trees.tree_species`\n", + "WHERE\n", + " AI.GENERATE_INT(\n", + " ('What is the average life in years of this tree species?', species_scientific_name),\n", + " connection_id => 'us.test_connection',\n", + " endpoint => 'gemini-2.5-flash').result > 19" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "generate_int_md" + }, + "source": [ + "### Using `AI.GENERATE_INT`: Filtering for tree species of specific average mature height\n", + "\n", + "[`AI.GENERATE_INT`](https://cloud.google.com/bigquery/docs/reference/standard-sql/bigqueryml-syntax-ai-generate-int) is similar to [`AI.GENERATE_DOUBLE`](https://cloud.google.com/bigquery/docs/reference/standard-sql/bigqueryml-syntax-ai-generate-double), except that it returns data of type `INT64`, instead of `FLOAT64`. Let's use it to return tree species with an average mature height of 10-20 meters.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 39, + "metadata": { + "id": "generate_int_code" + }, + "outputs": [ + { + "data": { + "text/plain": [ + "Query is running: 0%| |" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + "Downloading: 0%| |" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/vnd.google.colaboratory.intrinsic+json": { + "summary": "{\n \"name\": \"get_ipython()\",\n \"rows\": 26,\n \"fields\": [\n {\n \"column\": \"species_scientific_name\",\n \"properties\": {\n \"dtype\": \"string\",\n \"num_unique_values\": 26,\n \"samples\": [\n \"Quercus bicolor\",\n \"Quercus phellos\",\n \"Koelreuteria paniculata\"\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"species_common_name\",\n \"properties\": {\n \"dtype\": \"string\",\n \"num_unique_values\": 26,\n \"samples\": [\n \"Swamp White Oak\",\n \"Willow Oak\",\n \"Goldenraintree\"\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n }\n ]\n}", + "type": "dataframe" + }, + "text/html": [ + "\n", + "
    \n", + "
    \n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
    species_scientific_namespecies_common_name
    0Koelreuteria paniculataGoldenraintree
    1Corylus colurnaTurkish Filbert
    2Ostrya virginianaAmerican Hophornbeam
    3Carpinus betulusEuropean Hornbeam
    4Quercus spp. 'Fastigiata'Fastigiata Oak
    5Ulmus parvifoliaChinese Elm
    6Cercidiphyllum japonicumKatsura Tree
    7Fraxinus pennsylvanicaGreen Ash
    8Quercus bicolorSwamp White Oak
    9Quercus palustrisPin Oak
    10Crataegus sp.Hawthorn
    11Styphnolobium japonicumScholar Tree
    12Prunus x yedoensisYoshino Cherry
    13Acer truncatumShantung Maple
    14Celtis occidentalisHackberry
    15Acer rubrumRed Maple
    16Quercus phellosWillow Oak
    17Prunus padusEuropean Birdcherry
    18Quercus imbricariaShingle Oak
    19Tilia x euchloraCrimean Linden
    20Eucommia ulmoidesHardy Rubber Tree
    21Quercus acutissimaSawtooth Oak
    22Nyssa sylvaticaBlack Gum
    23Acer campestreHedge Maple
    24Gleditsia triacanthos var. inermisHoneylocust
    25Tilia cordataLittleleaf Linden
    \n", + "
    \n", + "
    \n", + "\n", + "
    \n", + " \n", + "\n", + " \n", + "\n", + " \n", + "
    \n", + "\n", + "\n", + "
    \n", + " \n", + "\n", + "\n", + "\n", + " \n", + "
    \n", + "\n", + "
    \n", + "
    \n" + ], + "text/plain": [ + " species_scientific_name species_common_name\n", + "0 Koelreuteria paniculata Goldenraintree\n", + "1 Corylus colurna Turkish Filbert\n", + "2 Ostrya virginiana American Hophornbeam\n", + "3 Carpinus betulus European Hornbeam\n", + "4 Quercus spp. 'Fastigiata' Fastigiata Oak\n", + "5 Ulmus parvifolia Chinese Elm\n", + "6 Cercidiphyllum japonicum Katsura Tree\n", + "7 Fraxinus pennsylvanica Green Ash\n", + "8 Quercus bicolor Swamp White Oak\n", + "9 Quercus palustris Pin Oak\n", + "10 Crataegus sp. Hawthorn\n", + "11 Styphnolobium japonicum Scholar Tree\n", + "12 Prunus x yedoensis Yoshino Cherry\n", + "13 Acer truncatum Shantung Maple\n", + "14 Celtis occidentalis Hackberry\n", + "15 Acer rubrum Red Maple\n", + "16 Quercus phellos Willow Oak\n", + "17 Prunus padus European Birdcherry\n", + "18 Quercus imbricaria Shingle Oak\n", + "19 Tilia x euchlora Crimean Linden\n", + "20 Eucommia ulmoides Hardy Rubber Tree\n", + "21 Quercus acutissima Sawtooth Oak\n", + "22 Nyssa sylvatica Black Gum\n", + "23 Acer campestre Hedge Maple\n", + "24 Gleditsia triacanthos var. inermis Honeylocust\n", + "25 Tilia cordata Littleleaf Linden" + ] + }, + "execution_count": 39, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "%%bigquery --project {PROJECT_ID}\n", + "\n", + "SELECT\n", + " species_scientific_name,\n", + " species_common_name\n", + "FROM\n", + " `bigquery-public-data.new_york_trees.tree_species`\n", + "WHERE\n", + " AI.GENERATE_DOUBLE(\n", + " ('What is the average height in meters of this tree species at full maturity?', species_scientific_name),\n", + " connection_id => 'us.test_connection',\n", + " endpoint => 'gemini-2.5-flash').result BETWEEN 10 AND 20" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "LXSfuNKs521-" + }, + "source": [ + "You can also combine `AI.GENERATE_DOUBLE`, `AI.GENERATE_INT`, and `AI.GENERATE_BOOL` scalar functions within one query. Here's an example combining the AI filters of the last three queries into one `WHERE` clause:" + ] + }, + { + "cell_type": "code", + "execution_count": 40, + "metadata": { + "id": "9mpFmVGG5yrG" + }, + "outputs": [ + { + "data": { + "text/plain": [ + "Query is running: 0%| |" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + "Downloading: 0%| |" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/vnd.google.colaboratory.intrinsic+json": { + "summary": "{\n \"name\": \"get_ipython()\",\n \"rows\": 12,\n \"fields\": [\n {\n \"column\": \"species_scientific_name\",\n \"properties\": {\n \"dtype\": \"string\",\n \"num_unique_values\": 12,\n \"samples\": [\n \"Tilia x euchlora\",\n \"Gymnocladus dioicus\",\n \"Corylus colurna\"\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"species_common_name\",\n \"properties\": {\n \"dtype\": \"string\",\n \"num_unique_values\": 12,\n \"samples\": [\n \"Crimean Linden\",\n \"Coffeetree\",\n \"Turkish Filbert\"\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n }\n ]\n}", + "type": "dataframe" + }, + "text/html": [ + "\n", + "
    \n", + "
    \n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
    species_scientific_namespecies_common_name
    0Corylus colurnaTurkish Filbert
    1Ostrya virginianaAmerican Hophornbeam
    2Ulmus parvifoliaChinese Elm
    3Fraxinus pennsylvanicaGreen Ash
    4Styphnolobium japonicumScholar Tree
    5Eucommia ulmoidesHardy Rubber Tree
    6Quercus spp. 'Fastigiata'Fastigiata Oak
    7Acer campestreHedge Maple
    8Gleditsia triacanthos var. inermisHoneylocust
    9Gymnocladus dioicusCoffeetree
    10Tilia x euchloraCrimean Linden
    11Pyrus calleryanaCallery Pear
    \n", + "
    \n", + "
    \n", + "\n", + "
    \n", + " \n", + "\n", + " \n", + "\n", + " \n", + "
    \n", + "\n", + "\n", + "
    \n", + " \n", + "\n", + "\n", + "\n", + " \n", + "
    \n", + "\n", + "
    \n", + "
    \n" + ], + "text/plain": [ + " species_scientific_name species_common_name\n", + "0 Corylus colurna Turkish Filbert\n", + "1 Ostrya virginiana American Hophornbeam\n", + "2 Ulmus parvifolia Chinese Elm\n", + "3 Fraxinus pennsylvanica Green Ash\n", + "4 Styphnolobium japonicum Scholar Tree\n", + "5 Eucommia ulmoides Hardy Rubber Tree\n", + "6 Quercus spp. 'Fastigiata' Fastigiata Oak\n", + "7 Acer campestre Hedge Maple\n", + "8 Gleditsia triacanthos var. inermis Honeylocust\n", + "9 Gymnocladus dioicus Coffeetree\n", + "10 Tilia x euchlora Crimean Linden\n", + "11 Pyrus calleryana Callery Pear" + ] + }, + "execution_count": 40, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "%%bigquery --project {PROJECT_ID}\n", + "\n", + "SELECT\n", + " species_scientific_name,\n", + " species_common_name\n", + "FROM\n", + " `bigquery-public-data.new_york_trees.tree_species`\n", + "WHERE\n", + " AI.GENERATE_DOUBLE(\n", + " ('What is the average height in meters of this tree species at full maturity?', species_scientific_name),\n", + " connection_id => 'us.test_connection',\n", + " endpoint => 'gemini-2.5-flash').result BETWEEN 10 AND 20 AND\n", + " AI.GENERATE_INT(\n", + " ('What is the average life in years of this tree species?', species_scientific_name),\n", + " connection_id => 'us.test_connection',\n", + " endpoint => 'gemini-2.5-flash').result > 19 AND\n", + " AI.GENERATE_BOOL(\n", + " ('Is this tree species drought tolerant?', species_scientific_name),\n", + " connection_id => 'us.test_connection',\n", + " endpoint => 'gemini-2.5-flash').result = true" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "forecast_md" + }, + "source": [ + "---\n", + "\n", + "## Forecast time series with `AI.FORECAST`\n", + "\n", + "The Google Research [`TimesFM`](https://cloud.google.com/bigquery/docs/timesfm-model) model is a foundation model for time-series forecasting that has been pre-trained on billions of time-points from many real-world datasets, so you can apply it to new forecasting datasets across many domains.\n", + "\n", + "The [`AI.FORECAST`](https://cloud.google.com/bigquery/docs/reference/standard-sql/bigqueryml-syntax-ai-forecast) function in BigQuery leverages the model to provide high-quality time series forecasting without the need to create and train your own model.\n", + "\n", + "For this example, we'll use the `bigquery-public-data.san_francisco_bikeshare.bikeshare_trips` data to forecast hourly bikeshare trips by subscriber type." + ] + }, + { + "cell_type": "code", + "execution_count": 41, + "metadata": { + "id": "forecast_code" + }, + "outputs": [ + { + "data": { + "text/plain": [ + "Query is running: 0%| |" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + "Downloading: 0%| |" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "%%bigquery forecast_results --project {PROJECT_ID}\n", + "\n", + "SELECT *\n", + "FROM\n", + " AI.FORECAST(\n", + " (\n", + " SELECT\n", + " TIMESTAMP_TRUNC(start_date, HOUR) as trip_hour,\n", + " subscriber_type,\n", + " COUNT(*) as num_trips\n", + " FROM `bigquery-public-data.san_francisco_bikeshare.bikeshare_trips`\n", + " WHERE start_date >= TIMESTAMP('2018-01-01')\n", + " GROUP BY TIMESTAMP_TRUNC(start_date, HOUR), subscriber_type\n", + " ),\n", + " horizon => 720,\n", + " confidence_level => 0.95,\n", + " timestamp_col => 'trip_hour',\n", + " data_col => 'num_trips',\n", + " id_cols => ['subscriber_type']);" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "forecast_text_md" + }, + "source": [ + "The results of this query were saved as a pandas DataFrame. Running the next cell will display the DataFrame, including the forecasted number of trips by subscriber type (annually/monthly subscriber, or short-term customer), for the next 720 hours (30 days), along with the prediction interval." + ] + }, + { + "cell_type": "code", + "execution_count": 42, + "metadata": { + "id": "9rkQpI4KQpB5" + }, + "outputs": [ + { + "data": { + "application/vnd.google.colaboratory.intrinsic+json": { + "summary": "{\n \"name\": \"forecast_results\",\n \"rows\": 1440,\n \"fields\": [\n {\n \"column\": \"subscriber_type\",\n \"properties\": {\n \"dtype\": \"category\",\n \"num_unique_values\": 2,\n \"samples\": [\n \"Customer\",\n \"Subscriber\"\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"forecast_timestamp\",\n \"properties\": {\n \"dtype\": \"date\",\n \"min\": \"2018-05-01 00:00:00+00:00\",\n \"max\": \"2018-05-30 23:00:00+00:00\",\n \"num_unique_values\": 720,\n \"samples\": [\n \"2018-05-15 04:00:00+00:00\",\n \"2018-05-13 02:00:00+00:00\"\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"forecast_value\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": 144.02612265291813,\n \"min\": -55.49763488769531,\n \"max\": 751.3600463867188,\n \"num_unique_values\": 1440,\n \"samples\": [\n -6.396148681640625,\n -29.988113403320312\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"confidence_level\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": 2.5432939679420995e-14,\n \"min\": 0.95,\n \"max\": 0.95,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.95\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"prediction_interval_lower_bound\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": 124.33868160606646,\n \"min\": -361.1392152019482,\n \"max\": 505.0838247829274,\n \"num_unique_values\": 1440,\n \"samples\": [\n -102.71066180632344\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"prediction_interval_upper_bound\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": 221.76368830950986,\n \"min\": -9.185602128762568,\n \"max\": 1198.4913343334024,\n \"num_unique_values\": 1440,\n \"samples\": [\n 89.9183644430422\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"ai_forecast_status\",\n \"properties\": {\n \"dtype\": \"object\",\n \"num_unique_values\": 1,\n \"samples\": [\n \"\"\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n }\n ]\n}", + "type": "dataframe", + "variable_name": "forecast_results" + }, + "text/html": [ + "\n", + "
    \n", + "
    \n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
    subscriber_typeforecast_timestampforecast_valueconfidence_levelprediction_interval_lower_boundprediction_interval_upper_boundai_forecast_status
    0Subscriber2018-05-01 00:00:00+00:000.6049190.95-9.78078510.990624
    1Subscriber2018-05-01 01:00:00+00:0021.5181580.95-5.28409948.320415
    2Subscriber2018-05-01 02:00:00+00:00-12.4725800.95-32.1329717.187812
    3Subscriber2018-05-01 03:00:00+00:00-8.9191590.95-59.02294841.184630
    4Subscriber2018-05-01 04:00:00+00:0012.5113220.95-32.00543257.028076
    \n", + "
    \n", + "
    \n", + "\n", + "
    \n", + " \n", + "\n", + " \n", + "\n", + " \n", + "
    \n", + "\n", + "\n", + "
    \n", + " \n", + "\n", + "\n", + "\n", + " \n", + "
    \n", + "\n", + "
    \n", + "
    \n" + ], + "text/plain": [ + " subscriber_type forecast_timestamp forecast_value confidence_level \\\n", + "0 Subscriber 2018-05-01 00:00:00+00:00 0.604919 0.95 \n", + "1 Subscriber 2018-05-01 01:00:00+00:00 21.518158 0.95 \n", + "2 Subscriber 2018-05-01 02:00:00+00:00 -12.472580 0.95 \n", + "3 Subscriber 2018-05-01 03:00:00+00:00 -8.919159 0.95 \n", + "4 Subscriber 2018-05-01 04:00:00+00:00 12.511322 0.95 \n", + "\n", + " prediction_interval_lower_bound prediction_interval_upper_bound \\\n", + "0 -9.780785 10.990624 \n", + "1 -5.284099 48.320415 \n", + "2 -32.132971 7.187812 \n", + "3 -59.022948 41.184630 \n", + "4 -32.005432 57.028076 \n", + "\n", + " ai_forecast_status \n", + "0 \n", + "1 \n", + "2 \n", + "3 \n", + "4 " + ] + }, + "execution_count": 42, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "forecast_results.head(5)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "WGJwVQbcRRcv" + }, + "source": [ + "We can view the results as a chart using python." + ] + }, + { + "cell_type": "code", + "execution_count": 43, + "metadata": { + "id": "fVwumTBNUZSO" + }, + "outputs": [], + "source": [ + "import matplotlib.pyplot as plt\n", + "\n", + "# Separate the data by subscriber type\n", + "customer_df = forecast_results[forecast_results[\"subscriber_type\"] == \"Customer\"]\n", + "subscriber_df = forecast_results[forecast_results[\"subscriber_type\"] == \"Subscriber\"]\n", + "\n", + "# Create a figure and axes for the plot\n", + "fig, ax = plt.subplots(figsize=(15, 7))\n", + "\n", + "# Plot the forecast for 'Customer'\n", + "ax.plot(\n", + " customer_df[\"forecast_timestamp\"],\n", + " customer_df[\"forecast_value\"],\n", + " label=\"Customer Forecast\",\n", + ")\n", + "ax.fill_between(\n", + " customer_df[\"forecast_timestamp\"],\n", + " customer_df[\"prediction_interval_lower_bound\"],\n", + " customer_df[\"prediction_interval_upper_bound\"],\n", + " color=\"blue\",\n", + " alpha=0.1,\n", + " label=\"Customer Confidence Interval\",\n", + ")\n", + "\n", + "# Plot the forecast for 'Subscriber'\n", + "ax.plot(\n", + " subscriber_df[\"forecast_timestamp\"],\n", + " subscriber_df[\"forecast_value\"],\n", + " label=\"Subscriber Forecast\",\n", + ")\n", + "ax.fill_between(\n", + " subscriber_df[\"forecast_timestamp\"],\n", + " subscriber_df[\"prediction_interval_lower_bound\"],\n", + " subscriber_df[\"prediction_interval_upper_bound\"],\n", + " color=\"orange\",\n", + " alpha=0.1,\n", + " label=\"Subscriber Confidence Interval\",\n", + ")\n", + "\n", + "\n", + "# Set the title and labels\n", + "ax.set_title(\"Bikeshare Trips Forecast\")\n", + "ax.set_xlabel(\"Date\")\n", + "ax.set_ylabel(\"Number of Trips\")\n", + "ax.legend()\n", + "ax.grid(True)\n", + "\n", + "# Rotate the x-axis labels for better readability\n", + "plt.xticks(rotation=45)\n", + "\n", + "# Show the plot\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "tc_YRVNIfS7Q" + }, + "source": [ + "The forecast shows usage patterns you might expect:\n", + "\n", + "* **Subscribers** are typically commuters who use the service for regular, predictable trips, such as going to and from work. Their forecast shows expected weekday peaks during morning and evening commute hours, and lower weekend usage.\n", + "\n", + "* **Customers** are usually tourists or casual riders and you might expect them to have higher usage on weekends and holidays for leisure activities, and more trips during the middle of the day, rather than concentrated during traditional commute times.\n", + "\n", + "An interesting start to an analysis without any model training!\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "cleanup_md" + }, + "source": [ + "# Cleaning Up\n", + "\n", + "To clean up all Google Cloud resources used in this project, you can [delete the Google Cloud project](https://cloud.google.com/resource-manager/docs/creating-managing-projects#shutting_down_projects) you used for the tutorial.\n", + "\n", + "Otherwise, you can delete the individual resources you created in this tutorial:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "cleanup_code" + }, + "outputs": [], + "source": [ + "# Delete BigQuery dataset, including the BigQuery remote model you just created, and the BigQuery Connection\n", + "! bq rm -r -f $PROJECT_ID:bq_ai_tutorial\n", + "! bq rm --connection --project_id=$PROJECT_ID --location=us test_connection" + ] + } + ], + "metadata": { + "colab": { + "name": "bigquery_generative_ai_intro.ipynb", + "toc_visible": true + }, + "kernelspec": { + "display_name": "Python 3", + "name": "python3" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} diff --git a/gemini/use-cases/applying-llms-to-data/gemini-and-documentai-for-entity-extraction/README.md b/gemini/use-cases/applying-llms-to-data/gemini-and-documentai-for-entity-extraction/README.md new file mode 100644 index 0000000..fa70d38 --- /dev/null +++ b/gemini/use-cases/applying-llms-to-data/gemini-and-documentai-for-entity-extraction/README.md @@ -0,0 +1,105 @@ +# Document AI and Gemini: Unlocking Entity Extraction Power + +This repository contains a Python script that demonstrates how to harness the combined capabilities of Google Cloud's Document AI and Gemini API to extract entities from PDF documents. Explore the strengths and nuances of each API through a side-by-side comparison of their outputs, gaining valuable insights into their performance. + +Code sample leverages official documentation to send an [online processing request](https://cloud.google.com/document-ai/docs/samples/documentai-process-document), [batch processing request](https://cloud.google.com/document-ai/docs/samples/documentai-batch-process-document#documentai_batch_process_document-python) and [handle the processing response](https://cloud.google.com/document-ai/docs/handle-response). + +## Why Compare Document AI and Gemini? + +In the realm of extracting valuable information from documents, Document AI and Gemini emerge as two powerful tools, each with its unique strengths and specialized capabilities. + +### Document AI + +Document AI is a specialized at extracting data from a wide variety of documents, including invoices, receipts, contracts, and more. It leverages advanced optical character recognition (OCR) and machine learning models to accurately identify and classify key information, transforming unstructured text into structured data. + +#### Document AI Use Cases + +- **Automating Invoice Processing**: Document AI can efficiently extract invoice details like vendor information, invoice number, line items, and total amounts, streamlining processes and reducing manual errors. +- **Streamlining Contract Analysis**: By identifying key clauses, dates, and parties involved in contracts, Document AI enables faster and more accurate legal document review. +- **Digitizing Medical Records**: Document AI can extract patient information, diagnoses, medications, and other crucial details from medical forms and records, facilitating efficient data management and analysis in the healthcare sector. + +### Gemini + +Gemini is a family of large language models known for its capabilities in natural language understanding and generation. It excels at comprehending the nuances of human language, enabling it to perform tasks such as summarization, translation, question answering, and even creative writing. + +#### Gemini Use Cases + +- **Creating Chatbots and Virtual Assistants**: Gemini's natural language processing prowess empowers it to engage in dynamic conversations, providing personalized customer support or acting as an intelligent assistant across various applications. +- **Generating Content**: Whether it's drafting emails, writing articles, or composing creative stories, Gemini can generate high-quality text based on given prompts or contexts. +- **Summarizing Complex Documents**: Gemini can distill lengthy documents into concise summaries, saving time and facilitating efficient information consumption. + +#### Comparing Document AI and Gemini: Synergy and Specialization + +While both tools offer entity extraction capabilities, their focus and strengths differ significantly. + +- **Document AI**: Ideal for extracting data from specific document types with predefined schemas. +- **Gemini**: Best suited for extracting entities from unstructured text and understanding the nuances of natural language. + +By comparing their results in entity extraction tasks, you gain insights into their unique approaches and potential trade-offs, enabling you to choose the right tool for specific needs. +Moreover, combining the power of Document AI and Gemini can lead to innovative solutions. For instance, you can use Document AI to extract structured information from documents and then leverage Gemini to generate natural language summaries or insights based on the extracted data. + +## What You'll Gain + +- Hands-on Experience: The included Python script provides a practical example of integrating Document AI and Gemini, allowing you to experiment with both APIs and explore their capabilities firsthand. +- Deeper Understanding: The comparison of API results will shed light on the strengths and potential trade-offs of each tool, helping you make informed decisions for future projects. +- Customization: The code serves as a foundation that you can adapt and extend for your own entity extraction needs, tailoring it to specific document types and entity types. + +## Key Features + +- Document AI Integration: The script utilizes Document AI's robust entity extraction capabilities, showcasing its ability to identify and classify key information within PDF documents. +- Gemini API Interaction: The script leverages Gemini's natural language understanding to extract entities based on a carefully crafted prompt, demonstrating its versatility. +- Side-by-Side Comparison: The output highlights the strengths and potential differences in entity extraction results from both APIs, providing valuable insights into their performance. + +## Setup + +1. **Install Dependencies** + + ```bash + pip install --upgrade google-cloud-aiplatform + pip install -q -U google-generativeai + pip install -r requirements.txt + + ``` + +2. **Assumption** + +This repository assumes, that this [codelab](https://www.cloudskillsboost.google/focuses/67855?parent=catalog) has been completed, that a dataset with the test documents is available and there exists a Document AI extractor. + +Once it has been completed, additionally create two buckets, for batch processing, namely, temp and output. + +```bash +TEMP_BUCKET_URI = f"gs://documentai-temp-{PROJECT_ID}-unique" + +gcloud storage buckets create {BUCKET_URI} --project {PROJECT_ID} --location {LOCATION} + +OUT_BUCKET_URI = f"gs://documentai-temp-{TEMP_BUCKET_URI}-unique" + +gcloud storage buckets create {OUT_BUCKET_URI} --project {PROJECT_ID} --location {LOCATION} +``` + +## Diagram + +![alt text](diagram.png) + +## Code Overview + +- `test_doc_ai.py`: This script orchestrates the entire process: + + - It first uses the Document AI API to process the PDF and extract entities. + - Then, it utilizes the Gemini API with a tailored prompt to extract entities from the same PDF. + - Finally, it compares the results from both APIs and prints a summary. + +- `extractor.py`: Contains classes for interacting with the Document AI API for both online and batch processing. + +- `entity_processor.py`: Defines classes for extracting entities from the Document AI output and the Gemini API response. + +- `prompts_module.py`: Provides functions to generate prompts for entity extraction and comparison tasks for the Gemini API. + +- `temp_file_uploader.py`: Handles uploading files to a temporary Google Cloud Storage location for processing. + +## Notes + +- Ensure that your Google Cloud project has the necessary APIs enabled (Document AI, Vertex AI, etc.). +- The script is configured to process a single PDF file. You can modify it to process multiple files or handle different input sources. +- The accuracy and performance of entity extraction may vary depending on the document complexity and the chosen API parameters. +- This script is intended for demonstration purposes. You can adapt and extend it to suit your specific use case and integrate it into your applications. diff --git a/gemini/use-cases/applying-llms-to-data/gemini-and-documentai-for-entity-extraction/diagram.png b/gemini/use-cases/applying-llms-to-data/gemini-and-documentai-for-entity-extraction/diagram.png new file mode 100644 index 0000000..9fbb3b4 Binary files /dev/null and b/gemini/use-cases/applying-llms-to-data/gemini-and-documentai-for-entity-extraction/diagram.png differ diff --git a/gemini/use-cases/applying-llms-to-data/gemini-and-documentai-for-entity-extraction/entity_processor.py b/gemini/use-cases/applying-llms-to-data/gemini-and-documentai-for-entity-extraction/entity_processor.py new file mode 100644 index 0000000..771c96f --- /dev/null +++ b/gemini/use-cases/applying-llms-to-data/gemini-and-documentai-for-entity-extraction/entity_processor.py @@ -0,0 +1,57 @@ +from abc import ABC, abstractmethod +import json +import mimetypes +from typing import Dict + +from google.cloud import documentai +from vertexai.generative_models import GenerationConfig, GenerativeModel, Part + + +class EntityExtractor(ABC): + """Abstract Base Class for entity extraction.""" + + @abstractmethod + def extract_entities(self) -> Dict: + """Abstract method to extract entities.""" + + +class DocumentAIEntityExtractor(EntityExtractor): + """Class for Document AI entity extraction""" + + def __init__(self, document: documentai.Document) -> None: + self.document = document + + def extract_entities(self) -> Dict: + entities = {} + for entity in self.document.entities: + entities[entity.type_] = entity.mention_text + return entities + + +class ModelBasedEntityExtractor(EntityExtractor): + """Class for Gemini entity extraction""" + + def __init__(self, model_version: str, prompt: str, file_path: str) -> None: + self.config = GenerationConfig( + temperature=0.0, + top_p=0.8, + top_k=32, + candidate_count=1, + max_output_tokens=2048, + response_mime_type="application/json", + ) + self.model = GenerativeModel(model_version, generation_config=self.config) + self.prompt = prompt + mime_type = mimetypes.guess_type(file_path)[0] + if (mime_type is None) or (mime_type != "application/pdf"): + raise ValueError("Only PDF files are supported, aborting") + self.file_path = file_path + + def extract_entities(self) -> Dict: + pdf_file = Part.from_uri(self.file_path, mime_type="application/pdf") + contents = [pdf_file, self.prompt] + response = self.model.generate_content(contents) + + cleaned_string = response.text.replace("```json\n", "").replace("\n```", "") + entities = json.loads(cleaned_string) + return entities diff --git a/gemini/use-cases/applying-llms-to-data/gemini-and-documentai-for-entity-extraction/extractor.py b/gemini/use-cases/applying-llms-to-data/gemini-and-documentai-for-entity-extraction/extractor.py new file mode 100644 index 0000000..19db59b --- /dev/null +++ b/gemini/use-cases/applying-llms-to-data/gemini-and-documentai-for-entity-extraction/extractor.py @@ -0,0 +1,153 @@ +import re +from typing import Any, Optional + +from google.api_core.client_options import ClientOptions +from google.api_core.exceptions import InternalServerError, RetryError +from google.cloud import documentai, storage +from temp_file_uploader import TempFileUploader + + +class DocumentExtractor: + """Abstract base class for document extraction.""" + + def __init__( + self, + project_id: str, + location: str, + processor_id: str, + processor_version_id: Optional[str] = None, + ): + self.project_id = project_id + self.location = location + self.processor_id = processor_id + self.processor_version_id = processor_version_id + self.client = documentai.DocumentProcessorServiceClient( + client_options=ClientOptions( + api_endpoint=f"{location}-documentai.googleapis.com" + ) + ) + self.processor_name = self._get_proccessor_name() + + def _get_proccessor_name(self) -> Any: + if self.processor_version_id: + return self.client.processor_version_path( + self.project_id, + self.location, + self.processor_id, + self.processor_version_id, + ) + return self.client.processor_path( + self.project_id, self.location, self.processor_id + ) + + def process_document(self, file_path: str, mime_type: str) -> documentai.Document: + """abstract function for document processing""" + raise NotImplementedError + + +class OnlineDocumentExtractor(DocumentExtractor): + """ + Processes documents using the online Document AI API. + """ + + def process_document( + self, file_path: str, mime_type: str = "application/pdf" + ) -> documentai.Document: + with open(file_path, "rb") as image: + image_content = image.read() + + request = documentai.ProcessRequest( + name=self.processor_name, + raw_document=documentai.RawDocument( + content=image_content, mime_type=mime_type + ), + ) + + result = self.client.process_document(request=request) + return result.document + + +class BatchDocumentExtractor(DocumentExtractor): + """ + Processes documents using the batch Document AI API. + """ + + # pylint: disable=too-many-arguments + def __init__( + self, + project_id: str, + location: str, + processor_id: str, + gcs_output_uri: str, + gcs_temp_uri: str, + processor_version_id: str, + timeout: int = 400, + ): + super().__init__(project_id, location, processor_id, processor_version_id) + self.gcs_output_uri = gcs_output_uri + self.timeout = timeout + self.storage_client = storage.Client() + self.temp_file_uploader = TempFileUploader(gcs_temp_uri) + + def process_document(self, file_path: str, mime_type: str) -> documentai.Document: + gcs_input_uri = self.temp_file_uploader.upload_file(file_path) + document = self._process_document_batch(gcs_input_uri, mime_type) + self.temp_file_uploader.delete_file() + return document + + # pylint: disable=too-many-locals + def _process_document_batch( + self, gcs_input_uri: str, mime_type: str + ) -> documentai.Document: + gcs_document = documentai.GcsDocument( + gcs_uri=gcs_input_uri, mime_type=mime_type + ) + gcs_documents = documentai.GcsDocuments(documents=[gcs_document]) + input_config = documentai.BatchDocumentsInputConfig(gcs_documents=gcs_documents) + + gcs_output_config = documentai.DocumentOutputConfig.GcsOutputConfig( + gcs_uri=self.gcs_output_uri + ) + output_config = documentai.DocumentOutputConfig( + gcs_output_config=gcs_output_config + ) + + request = documentai.BatchProcessRequest( + name=self.processor_name, + input_documents=input_config, + document_output_config=output_config, + ) + + operation = self.client.batch_process_documents(request) + try: + print(f"Waiting for operation ({operation.operation.name}) to complete...") + operation.result(timeout=self.timeout) + except (RetryError, InternalServerError) as e: + print(e.message) + + metadata = documentai.BatchProcessMetadata(operation.metadata) + if metadata.state != documentai.BatchProcessMetadata.State.SUCCEEDED: + raise ValueError(f"Batch Process Failed: {metadata.state_message}") + + # Retrieve the processed document from GCS + for process in list(metadata.individual_process_statuses): + matches = re.match(r"gs://(.*?)/(.*)", process.output_gcs_destination) + if not matches: + print( + "Could not parse output GCS destination:", + process.output_gcs_destination, + ) + continue + + output_bucket, output_prefix = matches.groups() + output_blobs = self.storage_client.list_blobs( + output_bucket, prefix=output_prefix + ) + for blob in output_blobs: + if blob.content_type == "application/json": + print(f"Fetching {blob.name}") + return documentai.Document.from_json( + blob.download_as_bytes(), ignore_unknown_fields=True + ) + + raise FileNotFoundError("Processed document not found in GCS.") diff --git a/gemini/use-cases/applying-llms-to-data/gemini-and-documentai-for-entity-extraction/prompts_module.py b/gemini/use-cases/applying-llms-to-data/gemini-and-documentai-for-entity-extraction/prompts_module.py new file mode 100644 index 0000000..1a320b3 --- /dev/null +++ b/gemini/use-cases/applying-llms-to-data/gemini-and-documentai-for-entity-extraction/prompts_module.py @@ -0,0 +1,88 @@ +def get_extract_entities_prompt() -> str: + """Definition of prompt to extract data from a W-2 Tax Form""" + return """ + Task: Extract W-2 Tax Form Information + + Objective: + Accurately identify and extract the following fields from the provided document, which is expected to be a W-2 tax form or a representation thereof: + + * Employee's Social Security Number + * Employer Identification Number (EIN) + * Employee's Name + * Employer's Name + * Employer's Address + * Control Number (if present) + * Wages, Tips, and Other Compensation (Box 1) + * Federal Income Tax Withheld (Box 2) + * Social Security Wages (Box 3) + * Social Security Tax Withheld (Box 4) + * Medicare Wages and Tips (Box 5) + * Medicare Tax Withheld (Box 6) + * Social Security Tips (Box 7) + * Allocated Tips (Box 8) + * Dependent Care Benefits (Box 10) + * Nonqualified Plan Contributions (Box 11) + * State and Local Information (Boxes 15-20): + * State + * Employer's State ID Number + * State Wages, Tips, Etc. + * State Income Tax Withheld + * Local Wages, Tips, Etc. + * Local Income Tax Withheld + * Locality Name + + Guidelines: + + * Prioritize accuracy. If a field cannot be confidently extracted, indicate it as "Not Found" or a similar placeholder. + * Handle variations in document formatting and layout. + * If the document contains multiple W-2 forms, extract information for each one separately. + * Format the extracted data in a structured manner, such as a JSON object or a table, for easy further processing. + + Example Output (JSON): + + ``` + { + "Employee's Social Security Number": "***-**-****", + "Employer Identification Number (EIN)": "**-*******", + "Employee's Name": "John Doe", + "Employer's Name": "Acme Corporation", + "Employer's Address": "123 Main Street, Town, USA", + "Control Number": "12345", + "Wages, Tips, and Other Compensation (Box 1)": "50000.00", + "Federal Income Tax Withheld (Box 2)": "5000.00", + "Social Security Wages (Box 3)": "45000.00", + "Social Security Tax Withheld (Box 4)": "2800.00", + "Medicare Wages and Tips (Box 5)": "50000.00", + "Medicare Tax Withheld (Box 6)": "725.00", + "Social Security Tips (Box 7)": "0.00", + "Allocated Tips (Box 8)": "0.00", + "Dependent Care Benefits (Box 10)": "0.00", + "Nonqualified Plan Contributions (Box 11)": "0.00", + "State": "CA", + "Employer's State ID Number": "123456789", + "State Wages, Tips, Etc.": "50000.00", + "State Income Tax Withheld": "2000.00", + "Local Wages, Tips, Etc.": "0.00", + "Local Income Tax Withheld": "0.00", + "Locality Name":"" + } + """ + + +def get_compare_entities_prompt() -> str: + """Definition of prompt to compare output from DocumentAI and Gemini""" + return """ + **Analyze and compare the following two outputs, one from DocumentAI and the other from Gemini. Identify and list the following:** + + * **Similarities:** Entities or data points that are present and have the same values in both outputs. + * **Differences:** + * Entities present in one output but missing in the other. + * Entities present in both but with differing values. + + **DocumentAI output:** + ```{docai_output}``` + + **Gemini output:** + ```{gemini_output}``` + + """ diff --git a/gemini/use-cases/applying-llms-to-data/gemini-and-documentai-for-entity-extraction/requirements.txt b/gemini/use-cases/applying-llms-to-data/gemini-and-documentai-for-entity-extraction/requirements.txt new file mode 100644 index 0000000..dd2b2a2 --- /dev/null +++ b/gemini/use-cases/applying-llms-to-data/gemini-and-documentai-for-entity-extraction/requirements.txt @@ -0,0 +1,3 @@ +google-cloud-aiplatform +google-cloud-documentai +google-cloud-storage \ No newline at end of file diff --git a/gemini/use-cases/applying-llms-to-data/gemini-and-documentai-for-entity-extraction/temp_file_uploader.py b/gemini/use-cases/applying-llms-to-data/gemini-and-documentai-for-entity-extraction/temp_file_uploader.py new file mode 100644 index 0000000..1bdc422 --- /dev/null +++ b/gemini/use-cases/applying-llms-to-data/gemini-and-documentai-for-entity-extraction/temp_file_uploader.py @@ -0,0 +1,40 @@ +import uuid + +from google.cloud import storage + + +class TempFileUploader: + """Class with methods to work with files in GCS""" + + def __init__(self, gcs_temp_uri: str) -> None: + path_parts = gcs_temp_uri.replace("gs://", "").split("/") + self.temp_bucket_name = path_parts[0] + self.temp_file_path_gcs = "/".join(path_parts[1:]) + self.storage_client = storage.Client() + self.destination_blob_name = "" + + def _get_destination_blob_name(self, file_path: str) -> str: + file_id = str(uuid.uuid4()) + file_extension = file_path.split(".")[-1] + destination_blob_name = f"{self.temp_file_path_gcs}{file_id}.{file_extension}" + return destination_blob_name + + def upload_file(self, file_path: str) -> str: + """Function to upload file to GCS""" + self.destination_blob_name = self._get_destination_blob_name(file_path) + + bucket = self.storage_client.bucket(self.temp_bucket_name) + blob = bucket.blob(self.destination_blob_name) + blob.upload_from_filename(file_path) + + gcs_destination_uri = ( + f"gs://{self.temp_bucket_name}/{self.destination_blob_name}" + ) + return gcs_destination_uri + + def delete_file(self) -> None: + """Function to delete the temporary file from GCS""" + if self.destination_blob_name: + bucket = self.storage_client.bucket(self.temp_bucket_name) + blob = bucket.blob(self.destination_blob_name) + blob.delete() diff --git a/gemini/use-cases/applying-llms-to-data/gemini-and-documentai-for-entity-extraction/test_document_ai_gemini.ipynb b/gemini/use-cases/applying-llms-to-data/gemini-and-documentai-for-entity-extraction/test_document_ai_gemini.ipynb new file mode 100644 index 0000000..1c00635 --- /dev/null +++ b/gemini/use-cases/applying-llms-to-data/gemini-and-documentai-for-entity-extraction/test_document_ai_gemini.ipynb @@ -0,0 +1,163 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "id": "88c355af", + "metadata": { + "id": "2eec5cc39a59" + }, + "outputs": [], + "source": [ + "# Copyright 2024 Google LLC\n", + "#\n", + "# Licensed under the Apache License, Version 2.0 (the \"License\");\n", + "# you may not use this file except in compliance with the License.\n", + "# You may obtain a copy of the License at\n", + "#\n", + "# https://www.apache.org/licenses/LICENSE-2.0\n", + "#\n", + "# Unless required by applicable law or agreed to in writing, software\n", + "# distributed under the License is distributed on an \"AS IS\" BASIS,\n", + "# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n", + "# See the License for the specific language governing permissions and\n", + "# limitations under the License." + ] + }, + { + "cell_type": "markdown", + "id": "4a87f86f", + "metadata": { + "id": "31ab8a519de3" + }, + "source": [ + "| | |\n", + "|-|-|\n", + "|Author(s) | [Valentin Huerta](https://github.com/valentinhuerta1996) |\n", + "|| [Ulises Jimenez](https://github.com/ulises-jimenez07) |" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "6351524b-956c-4745-b651-3052f0181357", + "metadata": { + "id": "fec5d5b34a0a" + }, + "outputs": [], + "source": [ + "from entity_processor import DocumentAIEntityExtractor, ModelBasedEntityExtractor\n", + "from extractor import OnlineDocumentExtractor\n", + "from prompts_module import get_compare_entities_prompt, get_extract_entities_prompt\n", + "from temp_file_uploader import TempFileUploader\n", + "from vertexai.generative_models import GenerativeModel" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "a392e37a-e731-44ce-b81c-0db71395636a", + "metadata": { + "id": "06e3d60cf1f2" + }, + "outputs": [], + "source": [ + "project_id = \"project-id\"\n", + "location = \"us\" # Or other supported locations like 'eu'\n", + "processor_id = \"processor-id\"\n", + "processor_version_id = \"processor-version-id\" # Optional for batch processing\n", + "# File to process\n", + "file_path = \"test_file.pdf\"\n", + "mime_type = \"application/pdf\"\n", + "\n", + "gcs_output_uri = \"gs://bucket-output\" # GCS URI for output\n", + "gcs_temp_uri = \"gs://bucket-temp\" # GCS URI for output" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "3a2815a0-1f57-4ca1-9818-19fa3a384300", + "metadata": { + "id": "b5253c7b3285" + }, + "outputs": [], + "source": [ + "online_extractor = OnlineDocumentExtractor(\n", + " project_id=project_id,\n", + " location=location,\n", + " processor_id=processor_id,\n", + " # processor_version_id=processor_version_id\n", + ")\n", + "online_document = online_extractor.process_document(file_path, mime_type)\n", + "\n", + "docai_entity_extractor = DocumentAIEntityExtractor(online_document)\n", + "docai_entities = docai_entity_extractor.extract_entities()" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "fd4b0dd2-325d-4a13-b741-b1063098c2f3", + "metadata": { + "id": "9788574b815b" + }, + "outputs": [], + "source": [ + "docai_entities" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "5768a5dc-0ebf-4891-992f-6fbdea315b97", + "metadata": { + "id": "06c441a872d9" + }, + "outputs": [], + "source": [ + "temp_file_uploader = TempFileUploader(gcs_temp_uri)\n", + "gcs_input_uri = temp_file_uploader.upload_file(file_path)\n", + "\n", + "prompt_extract = get_extract_entities_prompt()\n", + "model_extractor = ModelBasedEntityExtractor(\n", + " \"gemini-2.0-flash\", prompt_extract, gcs_input_uri\n", + ")\n", + "gemini_entities = model_extractor.extract_entities()\n", + "\n", + "temp_file_uploader.delete_file()" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "02e0a0d3-11da-4673-82dc-bed9efac27f8", + "metadata": { + "id": "aebb108dc010" + }, + "outputs": [], + "source": [ + "compare_prompt = get_compare_entities_prompt()\n", + "compare_prompt = compare_prompt.format(\n", + " docai_output=str(docai_entities), gemini_output=str(gemini_entities)\n", + ")\n", + "\n", + "model = GenerativeModel(\"gemini-2.0-flash\")\n", + "docai_gemini_response_analysis = model.generate_content(compare_prompt)\n", + "print(docai_gemini_response_analysis.text)" + ] + } + ], + "metadata": { + "colab": { + "name": "test_document_ai_gemini.ipynb", + "toc_visible": true + }, + "kernelspec": { + "display_name": "Python 3", + "name": "python3" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} diff --git a/gemini/use-cases/applying-llms-to-data/multimodal-analysis-bigquery/analyze_multimodal_data_bigquery.ipynb b/gemini/use-cases/applying-llms-to-data/multimodal-analysis-bigquery/analyze_multimodal_data_bigquery.ipynb new file mode 100644 index 0000000..7ef8f6e --- /dev/null +++ b/gemini/use-cases/applying-llms-to-data/multimodal-analysis-bigquery/analyze_multimodal_data_bigquery.ipynb @@ -0,0 +1,5210 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "cMQCs0oQf5Jo" + }, + "outputs": [], + "source": [ + "# Copyright 2025 Google LLC\n", + "#\n", + "# Licensed under the Apache License, Version 2.0 (the \"License\");\n", + "# you may not use this file except in compliance with the License.\n", + "# You may obtain a copy of the License at\n", + "#\n", + "# https://www.apache.org/licenses/LICENSE-2.0\n", + "#\n", + "# Unless required by applicable law or agreed to in writing, software\n", + "# distributed under the License is distributed on an \"AS IS\" BASIS,\n", + "# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n", + "# See the License for the specific language governing permissions and\n", + "# limitations under the License." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "iR0jdheRGG89" + }, + "source": [ + "# Analyze Multimodal Data in BigQuery" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "td9kx9LVgSve" + }, + "source": [ + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
    \n", + " \n", + " \"Google
    Open in Colab\n", + "
    \n", + "
    \n", + " \n", + " \"Google
    Open in Colab Enterprise\n", + "
    \n", + "
    \n", + " \n", + " \"Vertex
    Open in Vertex AI Workbench\n", + "
    \n", + "
    \n", + " \n", + " \"BigQuery
    Open in BigQuery Studio\n", + "
    \n", + "
    \n", + " \n", + " \"GitHub
    View on GitHub\n", + "
    \n", + "
    \n", + "\n", + "
    \n", + "\n", + "Share to:\n", + "\n", + "\n", + " \"LinkedIn\n", + "\n", + "\n", + "\n", + " \"Bluesky\n", + "\n", + "\n", + "\n", + " \"X\n", + "\n", + "\n", + "\n", + " \"Reddit\n", + "\n", + "\n", + "\n", + " \"Facebook\n", + "" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "EiKsD6FM5i0m" + }, + "source": [ + "| Author |\n", + "| --- |\n", + "| [Jeff Nelson](https://github.com/jeffonelson) |" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "intro_md" + }, + "source": [ + "## Overview\n", + "\n", + "This notebook provides a hands-on example of BigQuery's powerful multimodal capabilities. You'll learn how to perform sophisticated, AI-driven analysis on your data - both structured and unstructured - right from your familiar SQL environment.\n", + "\n", + "Organizations typically store data in separate locations:\n", + "* There's **structured data** stored in neat rows and columns, like in [BigQuery tables](https://cloud.google.com/bigquery/docs/introduction)\n", + "* And there's **unstructured data**, which includes images, audio, video, and more. This generally lives in a cloud object store like [Google Cloud Storage (GCS)](https://cloud.google.com/storage/docs/introduction).\n", + "\n", + "The difficulty is querying both types of data together. For example, asking a question about customer satisfaction by analyzing the *audio* of a support call alongside customer support history.\n", + "\n", + "Fortunately, [BigQuery's multimodal](https://cloud.google.com/bigquery/docs/analyze-multimodal-data) and [generative AI capabilities](https://cloud.google.com/bigquery/docs/reference/standard-sql/bigqueryml-syntax-create-remote-model) cover these use cases, making it possible to analyze both structured in unstrctured data in a single query." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "SwQLISjqGTHd" + }, + "source": [ + "### Objectives\n", + "\n", + "You will learn to:\n", + "\n", + "* Understand what an [`ObjectRef`](https://cloud.google.com/bigquery/docs/analyze-multimodal-data#objectref_values) is, and how it bridges the gap between data in BigQuery and files in Google Cloud Storage (GCS).\n", + "* Create `ObjectRef`s in two different ways.\n", + "* Combine structured tables with unstructured files to create a single, unified view.\n", + "* Run generative AI models over your new multimodal tables to extract insights." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "costs_md" + }, + "source": [ + "### Services and Costs\n", + "\n", + "This tutorial uses the following billable components of Google Cloud:\n", + "\n", + "* **BigQuery**: [Pricing](https://cloud.google.com/bigquery/pricing)\n", + "\n", + "* **BigQuery ML**: [Pricing](https://cloud.google.com/bigquery/pricing#bqml)\n", + "\n", + "* **Vertex AI**: [Pricing](https://cloud.google.com/vertex-ai/generative-ai/pricing)\n", + "\n", + "You can use the [Pricing Calculator](https://cloud.google.com/products/calculator) to generate a cost estimate based on your projected usage." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "nkoCFoFVSPii" + }, + "source": [ + "---\n", + "\n", + "## Before you begin" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "setup_md_1" + }, + "source": [ + "### Set up your Google Cloud project\n", + "**The following steps are required, regardless of your notebook environment.**\n", + "\n", + "1. [Select or create a Google Cloud project](https://console.cloud.google.com/cloud-resource-manager). When you first create an account, you get a $300 free credit towards your compute/storage costs.\n", + "\n", + "2. [Make sure that billing is enabled for your project](https://cloud.google.com/billing/docs/how-to/modify-project).\n", + "\n", + "3. [Enable the BigQuery, BigQuery Connection, and Vertex AI APIs](https://console.cloud.google.com/flows/enableapi?apiid=bigquery.googleapis.com,bigqueryconnection.googleapis.com,aiplatform.googleapis.com).\n", + "\n", + "4. If you are running this notebook locally, you need to install the [Cloud SDK](https://cloud.google.com/sdk)." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "YQ3g-h7uTaSf" + }, + "source": [ + "### Set your project ID" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "set_project_id" + }, + "outputs": [], + "source": [ + "PROJECT_ID = \"YOUR-PROJECT-ID\" # @param {type:\"string\"}\n", + "\n", + "# Set the project id\n", + "! gcloud config set project {PROJECT_ID}" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "auth_md" + }, + "source": [ + "### Authenticate to your Google Cloud account\n", + "\n", + "Depending on your Jupyter environment, you may have to manually authenticate. Follow the relevant instructions below." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "V6NjZRCXU5Ro" + }, + "source": [ + "**1. Colab Enterprise in BigQuery Studio or Vertex AI**\n", + "* Do nothing as you are already authenticated." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "l0dV1hvAU1ed" + }, + "source": [ + "**2. Colab Consumer - uncomment and run the following:**" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "auth_code" + }, + "outputs": [], + "source": [ + "from google.colab import auth\n", + "auth.authenticate_user()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "4Pp4LAJ3UyRP" + }, + "source": [ + "**3. Local JupyterLab instance, uncomment and run the following:**\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "AzU7S3fMVDkW" + }, + "outputs": [], + "source": [ + "# ! gcloud auth login" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "conn_md" + }, + "source": [ + "### Create BigQuery Cloud resource connection\n", + "\n", + "You will need to create a [Cloud resource connection](https://cloud.google.com/bigquery/docs/create-cloud-resource-connection) to enable BigQuery to interact with Vertex AI services." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "conn_code" + }, + "outputs": [], + "source": [ + "!bq mk --connection --location=us \\\n", + " --connection_type=CLOUD_RESOURCE test_connection" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "perms_md" + }, + "source": [ + "### Set permissions for Service Account\n", + "\n", + "The resource connection service account requires certain project-level permissions to interact with Vertex AI and Google Cloud Storage." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "6OqpZNY953xR" + }, + "outputs": [], + "source": [ + "SERVICE_ACCT = !bq show --format=prettyjson --connection us.test_connection | grep \"serviceAccountId\" | cut -d '\"' -f 4\n", + "SERVICE_ACCT_EMAIL = SERVICE_ACCT[-1]\n", + "print(SERVICE_ACCT_EMAIL)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "perms_code" + }, + "outputs": [], + "source": [ + "import time\n", + "\n", + "!gcloud projects add-iam-policy-binding --format=none $PROJECT_ID --member=serviceAccount:$SERVICE_ACCT_EMAIL --role='roles/storage.objectViewer'\n", + "!gcloud projects add-iam-policy-binding --format=none $PROJECT_ID --member=serviceAccount:$SERVICE_ACCT_EMAIL --role='roles/aiplatform.user'\n", + "\n", + "# Wait ~60 seconds, to give IAM updates time to propagate. Otherwise, subsequent cells may fail.\n", + "time.sleep(60)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "-fhqCNYQOLCq" + }, + "source": [ + "#### Create a BigQuery Dataset\n", + "\n", + "Running the following query creates a [BigQuery dataset](https://cloud.google.com/bigquery/docs/datasets-intro) called **`bq_mm_tutorial`** to house any tables or remote models for this tutorial:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "ZR9S5lgpOGDr" + }, + "outputs": [], + "source": [ + "%%bigquery --project {PROJECT_ID}\n", + "\n", + "CREATE SCHEMA `bq_mm_tutorial` OPTIONS (location = 'US');" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "VfM1EL2NOTVR" + }, + "source": [ + "### Set Colab display options\n", + "\n", + "Colab includes the `google.colab.data_table` package that can be used to display large pandas dataframes as an interactive data table. It can be enabled with:" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": { + "id": "iSYRP39jOQ7R" + }, + "outputs": [], + "source": [ + "%load_ext google.colab.data_table" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "generate_md_1" + }, + "source": [ + "---\n", + "\n", + "## What is an `ObjectRef`?\n", + "\n", + "[`ObjectRef`](https://cloud.google.com/bigquery/docs/analyze-multimodal-data#objectref_values) is the foundation of multimodal analysis in BigQuery. Think of it as a secure pointer to a file (image, PDF, video, etc.) in GCS. It doesn't hold the file's data itself, but it contains the information BigQuery needs to find and access the file during a query.\n", + "\n", + "An `ObjectRef` is stored as a `STRUCT` and contains details like the file's URI path (i.e. its direct GCS \"address\"), a secure authorizer, and other metadata. By using `ObjectRef` columns in your BigQuery tables, you can effectively query against GCS objects right alongside your structured data.\n", + "\n", + "### Creating ObjectRefs:\n", + "\n", + "There are two primary ways to create tables with `ObjectRef` columns. We'll detail them in two short demo scenarios.\n", + "* Scenario 1: Use an [object table](https://cloud.google.com/bigquery/docs/object-table-introduction) to automatically generate an `ObjectRef` for every file in a GCS bucket\n", + "* Scenario 2: Use [built-in SQL functions](https://cloud.google.com/bigquery/docs/reference/standard-sql/objectref_functions) like `OBJ.MAKE_REF()` to create `ObjectRef`s programmatically from URIs in an existing table" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "K9_iiZJWTQXG" + }, + "source": [ + "---\n", + "\n", + "## Scenario 1: Analyze Customer Service Calls" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "a2xG-u0qaLuG" + }, + "source": [ + "### 1. Load structured data\n", + "\n", + "Let's begin with a practical example. Imagine we have a standard BigQuery table with details about customer support calls. The table contains a `call_id` column, which is a unique identifier for a call. Let's load this sample data into BigQuery." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 154, + "referenced_widgets": [ + "b8ef66e84d794d77916d9836532ab1f1", + "2f3fb9fa82cc46c4950c8b64f5826446", + "937cf5ab5ad24b9bb8e4b8b1202af2b8", + "f50b56de18eb421191c7ce50637fb8d4", + "61f68bac11794c5a80f523d61b427ca8", + "e57e801c23ab4f56854c349e1d5f9dc5", + "bc55e1a7c8814b668b52bab40537fc61", + "b39b6fabac0143eb9cbab48dc650e804", + "90c29b09df5347a08df528b120b65a6a", + "a6286c3f439c400dbca934df88ff4c5b", + "e1ba640e598b4e849c78aa735b168694" + ] + }, + "id": "1JY3WhVBKT8c", + "outputId": "ae845812-8639-4f89-ea3b-938a46268baf" + }, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "b8ef66e84d794d77916d9836532ab1f1", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Query is running: 0%| |" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/vnd.google.colaboratory.intrinsic+json": { + "summary": "{\n \"name\": \"get_ipython()\",\n \"rows\": 0,\n \"fields\": []\n}", + "type": "dataframe" + }, + "application/vnd.google.colaboratory.module+javascript": "\n import \"https://ssl.gstatic.com/colaboratory/data_table/9e65f7085e7ffcb7/data_table.js\";\n\n const table = window.createDataTable({\n data: [],\n columns: [[\"number\", \"index\"]],\n columnOptions: [{\"width\": \"1px\", \"className\": \"index_column\"}],\n rowsPerPage: 25,\n helpUrl: \"https://colab.research.google.com/notebooks/data_table.ipynb\",\n suppressOutputScrolling: true,\n minimumWidth: undefined,\n });\n\n function appendQuickchartButton(parentElement) {\n let quickchartButtonContainerElement = document.createElement('div');\n quickchartButtonContainerElement.innerHTML = `\n
    \n \n\n\n\n \n
    `;\n parentElement.appendChild(quickchartButtonContainerElement);\n }\n\n appendQuickchartButton(table);\n ", + "text/html": [ + "\n", + "
    \n", + "
    \n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
    \n", + "
    \n", + "
    \n", + "\n", + "
    \n", + " \n", + "\n", + " \n", + "\n", + " \n", + "
    \n", + "\n", + "\n", + "
    \n", + "
    \n" + ], + "text/plain": [ + "Empty DataFrame\n", + "Columns: []\n", + "Index: []" + ] + }, + "execution_count": 16, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "%%bigquery --project {PROJECT_ID}\n", + "\n", + "LOAD DATA OVERWRITE bq_mm_tutorial.calls\n", + "FROM FILES (\n", + " uris = ['gs://sample-data-and-media/customer-support/tables/calls'],\n", + " format = 'PARQUET'\n", + ");" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "F_bbC8thoHGU" + }, + "source": [ + "Taking a peek at the table, we can see the `call_id` column alongside other call attributes." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 293, + "referenced_widgets": [ + "d7090bcee20e409cb2d7413f37fd9912", + "2c5f64dc17024b20ad5e74bb3925dab9", + "c8cbc42966a04facb2b21b5073c6b2c2", + "6ba1b399479d49db88bc42a740fa7bbe", + "14d5e40d69f54c3c93c86bafb24246d8", + "45399fa005eb4628aabeec06da3b8bfa", + "d3f1e33c657347e5ab9b0aac598b9b3b", + "a5ac996c9e9c4b2190cba1c8b548d8d9", + "90a436b5e0b9413fa477652753d5b4b2", + "46816b0077744ddf827985ba2a286ca9", + "460a8a94caf14c4892c6bfe23f8c514f", + "c38f9bedc9784442a793f069221dfb3e", + "6f27f2c33d754f1f9532cc0001e83409", + "cbc61c8d15c543609f0f81a331ba033a", + "e5de7b2893e14d21963cd45a05123db1", + "4fb6c5de06514f838cd552d5d1a14ec6", + "644cdd45b8a2496fb2f67f5165393508", + "5797e98f6e3648b1a03d27563a578d97", + "57363affda8f469099e70e9195ee8af8", + "65142791887e438e94f4c2743dfb9a6d", + "d1a4a1a201d440b79f9525ef50cf0ec6", + "d89fbda5f4284a36b833bef145f8d1e7" + ] + }, + "id": "YLqRk757KT3O", + "outputId": "0ba6a364-583b-4f51-e15a-d18b8adf2862" + }, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "d7090bcee20e409cb2d7413f37fd9912", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Query is running: 0%| |" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "c38f9bedc9784442a793f069221dfb3e", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Downloading: 0%| |" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/vnd.google.colaboratory.intrinsic+json": { + "summary": "{\n \"name\": \"get_ipython()\",\n \"rows\": 5,\n \"fields\": [\n {\n \"column\": \"call_id\",\n \"properties\": {\n \"dtype\": \"string\",\n \"num_unique_values\": 5,\n \"samples\": [\n \"call_3_20250411045541\",\n \"call_4_20250411045544\",\n \"call_5_20250411045547\"\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"company_name\",\n \"properties\": {\n \"dtype\": \"category\",\n \"num_unique_values\": 2,\n \"samples\": [\n \"Synergy Corp\",\n \"Global Tech Enterprises\"\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"company_revenue\",\n \"properties\": {\n \"dtype\": \"Int64\",\n \"num_unique_values\": 5,\n \"samples\": [\n 9800000,\n 20000000\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"customer_name\",\n \"properties\": {\n \"dtype\": \"string\",\n \"num_unique_values\": 5,\n \"samples\": [\n \"Robert Jones\",\n \"Emily Carter\"\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"product_name\",\n \"properties\": {\n \"dtype\": \"string\",\n \"num_unique_values\": 5,\n \"samples\": [\n \"Global Secure VPN\",\n \"Synergy Project Manager\"\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"call_reason\",\n \"properties\": {\n \"dtype\": \"string\",\n \"num_unique_values\": 5,\n \"samples\": [\n \"VPN service disconnecting frequently\",\n \"Praise for excellent customer support\"\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n }\n ]\n}", + "type": "dataframe" + }, + "application/vnd.google.colaboratory.module+javascript": "\n import \"https://ssl.gstatic.com/colaboratory/data_table/9e65f7085e7ffcb7/data_table.js\";\n\n const table = window.createDataTable({\n data: [[{\n 'v': 0,\n 'f': \"0\",\n },\n\"call_1_20250411045535\",\n\"Global Tech Enterprises\",\n{\n 'v': 12500000,\n 'f': \"12500000\",\n },\n\"Sarah Miller\",\n\"Global Shield Antivirus\",\n\"Antivirus subscription activation issue\"],\n [{\n 'v': 1,\n 'f': \"1\",\n },\n\"call_3_20250411045541\",\n\"Global Tech Enterprises\",\n{\n 'v': 9800000,\n 'f': \"9800000\",\n },\n\"Robert Jones\",\n\"Global Secure VPN\",\n\"VPN service disconnecting frequently\"],\n [{\n 'v': 2,\n 'f': \"2\",\n },\n\"call_5_20250411045547\",\n\"Global Tech Enterprises\",\n{\n 'v': 12000000,\n 'f': \"12000000\",\n },\n\"Michael Brown\",\n\"Global Protect Data Backup\",\n\"Need to upgrade data backup plan\"],\n [{\n 'v': 3,\n 'f': \"3\",\n },\n\"call_2_20250411045538\",\n\"Synergy Corp\",\n{\n 'v': 18000000,\n 'f': \"18000000\",\n },\n\"David Martin\",\n\"Synergy Cloud Storage\",\n\"Inquiry about enterprise cloud storage pricing\"],\n [{\n 'v': 4,\n 'f': \"4\",\n },\n\"call_4_20250411045544\",\n\"Synergy Corp\",\n{\n 'v': 20000000,\n 'f': \"20000000\",\n },\n\"Emily Carter\",\n\"Synergy Project Manager\",\n\"Praise for excellent customer support\"]],\n columns: [[\"number\", \"index\"], [\"string\", \"call_id\"], [\"string\", \"company_name\"], [\"number\", \"company_revenue\"], [\"string\", \"customer_name\"], [\"string\", \"product_name\"], [\"string\", \"call_reason\"]],\n columnOptions: [{\"width\": \"1px\", \"className\": \"index_column\"}],\n rowsPerPage: 25,\n helpUrl: \"https://colab.research.google.com/notebooks/data_table.ipynb\",\n suppressOutputScrolling: true,\n minimumWidth: undefined,\n });\n\n function appendQuickchartButton(parentElement) {\n let quickchartButtonContainerElement = document.createElement('div');\n quickchartButtonContainerElement.innerHTML = `\n
    \n \n\n\n\n \n
    `;\n parentElement.appendChild(quickchartButtonContainerElement);\n }\n\n appendQuickchartButton(table);\n ", + "text/html": [ + "\n", + "
    \n", + "
    \n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
    call_idcompany_namecompany_revenuecustomer_nameproduct_namecall_reason
    0call_1_20250411045535Global Tech Enterprises12500000Sarah MillerGlobal Shield AntivirusAntivirus subscription activation issue
    1call_3_20250411045541Global Tech Enterprises9800000Robert JonesGlobal Secure VPNVPN service disconnecting frequently
    2call_5_20250411045547Global Tech Enterprises12000000Michael BrownGlobal Protect Data BackupNeed to upgrade data backup plan
    3call_2_20250411045538Synergy Corp18000000David MartinSynergy Cloud StorageInquiry about enterprise cloud storage pricing
    4call_4_20250411045544Synergy Corp20000000Emily CarterSynergy Project ManagerPraise for excellent customer support
    \n", + "
    \n", + "
    \n", + "\n", + "
    \n", + " \n", + "\n", + " \n", + "\n", + " \n", + "
    \n", + "\n", + "\n", + "
    \n", + " \n", + "\n", + "\n", + "\n", + " \n", + "
    \n", + "\n", + "
    \n", + "
    \n" + ], + "text/plain": [ + " call_id company_name company_revenue \\\n", + "0 call_1_20250411045535 Global Tech Enterprises 12500000 \n", + "1 call_3_20250411045541 Global Tech Enterprises 9800000 \n", + "2 call_5_20250411045547 Global Tech Enterprises 12000000 \n", + "3 call_2_20250411045538 Synergy Corp 18000000 \n", + "4 call_4_20250411045544 Synergy Corp 20000000 \n", + "\n", + " customer_name product_name \\\n", + "0 Sarah Miller Global Shield Antivirus \n", + "1 Robert Jones Global Secure VPN \n", + "2 Michael Brown Global Protect Data Backup \n", + "3 David Martin Synergy Cloud Storage \n", + "4 Emily Carter Synergy Project Manager \n", + "\n", + " call_reason \n", + "0 Antivirus subscription activation issue \n", + "1 VPN service disconnecting frequently \n", + "2 Need to upgrade data backup plan \n", + "3 Inquiry about enterprise cloud storage pricing \n", + "4 Praise for excellent customer support " + ] + }, + "execution_count": 17, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "%%bigquery --project {PROJECT_ID}\n", + "\n", + "SELECT * FROM bq_mm_tutorial.calls;" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "CA6lCP22oTj8" + }, + "source": [ + "### 2. Create an Object Table\n", + "\n", + "We also have audio files stored in GCS corresponding to these customer support calls. The following creates an [object table](https://cloud.google.com/bigquery/docs/object-table-introduction) over a bucket containing customer service calls.\n", + "\n", + "The object table is a read-only BigQuery table that mirrors the contents of a GCS directly and [automatically generates an `ObjectRef`](https://cloud.google.com/bigquery/docs/analyze-multimodal-data#object_tables) for each file." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 154, + "referenced_widgets": [ + "805b7cdd6292460b849297f22d2a049b", + "4f99d69cf78d480abe044073bece8e3c", + "0d0827342e45461bbcd954ed8a4496eb", + "23388c42f6e24c85bbd993139c71d53b", + "199b650704f64310a2e954b03340d4bb", + "c38322312b6349b68c6e55fa1427c590", + "eafb59d7bf99480a9de7902fb2f5b020", + "c6ba2126765c44f6b9fc5c599f508a02", + "a7aa3d31bb5f475db331d2a0f47bf1b6", + "490d2c50a16348c59d667dcdd733b058", + "4eddcc005af241f19ee910296bc16217" + ] + }, + "id": "stlUACEaLx73", + "outputId": "90ee7882-6975-49db-9950-4aaa0485592f" + }, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "805b7cdd6292460b849297f22d2a049b", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Query is running: 0%| |" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/vnd.google.colaboratory.intrinsic+json": { + "summary": "{\n \"name\": \"get_ipython()\",\n \"rows\": 0,\n \"fields\": []\n}", + "type": "dataframe" + }, + "application/vnd.google.colaboratory.module+javascript": "\n import \"https://ssl.gstatic.com/colaboratory/data_table/9e65f7085e7ffcb7/data_table.js\";\n\n const table = window.createDataTable({\n data: [],\n columns: [[\"number\", \"index\"]],\n columnOptions: [{\"width\": \"1px\", \"className\": \"index_column\"}],\n rowsPerPage: 25,\n helpUrl: \"https://colab.research.google.com/notebooks/data_table.ipynb\",\n suppressOutputScrolling: true,\n minimumWidth: undefined,\n });\n\n function appendQuickchartButton(parentElement) {\n let quickchartButtonContainerElement = document.createElement('div');\n quickchartButtonContainerElement.innerHTML = `\n
    \n \n\n\n\n \n
    `;\n parentElement.appendChild(quickchartButtonContainerElement);\n }\n\n appendQuickchartButton(table);\n ", + "text/html": [ + "\n", + "
    \n", + "
    \n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
    \n", + "
    \n", + "
    \n", + "\n", + "
    \n", + " \n", + "\n", + " \n", + "\n", + " \n", + "
    \n", + "\n", + "\n", + "
    \n", + "
    \n" + ], + "text/plain": [ + "Empty DataFrame\n", + "Columns: []\n", + "Index: []" + ] + }, + "execution_count": 29, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "%%bigquery --project {PROJECT_ID}\n", + "\n", + "CREATE OR REPLACE EXTERNAL TABLE `bq_mm_tutorial.object_table`\n", + "WITH CONNECTION `us.test_connection`\n", + "OPTIONS (\n", + " object_metadata = 'SIMPLE',\n", + " uris = ['gs://sample-data-and-media/customer-support/calls/*.mp3']\n", + ");" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "ybC6djJipNoV" + }, + "source": [ + "Let's check the table contents. Notice the `ref` column, which is an `ObjectRef` that we can use." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 471, + "referenced_widgets": [ + "1bca622aa4ca4b9998751522fa0eeb16", + "83b86260252b42ee9acaf67b76eb1b18", + "22bbe4e1883840248af78f020fb1b12f", + "57c3e0e7cef944438a54edae5042a801", + "8798189b79c541008e96c9d4e343d98d", + "11439efc9f114571979f55a797de0246", + "28136705f7cb4fae983962fee6e11f85", + "06f75175ed854d11bcdd2942901e96fc", + "fdc57743864d4be5ab270ff8a41f54b1", + "d04d3060f6b8465e93ede0a2bb86e680", + "851c27b84a4b4fe28d6984a0df223043", + "65f74e6fb5274cbf8d0da8c8d8d9f7a1", + "15da5dbcba9049a3bf3429484e2ff861", + "16b9dfdff73e47c58e06b2ddd2d9bdb9", + "d5227fb6f9a54e75b6f3e7e00146f0ed", + "c418f8417d6740b9b2859a38185db8d8", + "9e96f19df9e84cdea61774119614257f", + "9fbf11468b694b29bf7b7e1ae4ef78f9", + "fbf8cbe3140c4dd588261b6203560030", + "2e360f2805884a9d96121412920e62cc", + "9db849c8e85d416bb2508612148c6550", + "d5c25f3cbacb4ef187ce6da624959245" + ] + }, + "id": "GD-DhrXiKT0p", + "outputId": "dd044ab1-9ed0-457c-da93-273df96ab10a" + }, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "1bca622aa4ca4b9998751522fa0eeb16", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Query is running: 0%| |" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "65f74e6fb5274cbf8d0da8c8d8d9f7a1", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Downloading: 0%| |" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/vnd.google.colaboratory.intrinsic+json": { + "summary": "{\n \"name\": \"get_ipython()\",\n \"rows\": 5,\n \"fields\": [\n {\n \"column\": \"uri\",\n \"properties\": {\n \"dtype\": \"string\",\n \"num_unique_values\": 5,\n \"samples\": [\n \"gs://sample-data-and-media/customer-support/calls/call_2_20250411045538.mp3\",\n \"gs://sample-data-and-media/customer-support/calls/call_5_20250411045547.mp3\",\n \"gs://sample-data-and-media/customer-support/calls/call_3_20250411045541.mp3\"\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"generation\",\n \"properties\": {\n \"dtype\": \"Int64\",\n \"num_unique_values\": 5,\n \"samples\": [\n 1753936794657505,\n 1753936795493499,\n 1753936794931315\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"content_type\",\n \"properties\": {\n \"dtype\": \"category\",\n \"num_unique_values\": 1,\n \"samples\": [\n \"audio/mpeg\"\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"size\",\n \"properties\": {\n \"dtype\": \"Int64\",\n \"num_unique_values\": 5,\n \"samples\": [\n 65376\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"md5_hash\",\n \"properties\": {\n \"dtype\": \"string\",\n \"num_unique_values\": 5,\n \"samples\": [\n \"c8c575ce7cf437c840c28cbb84174e43\"\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"updated\",\n \"properties\": {\n \"dtype\": \"date\",\n \"min\": \"2025-07-31 04:39:54.327000+00:00\",\n \"max\": \"2025-07-31 04:39:55.523000+00:00\",\n \"num_unique_values\": 5,\n \"samples\": [\n \"2025-07-31 04:39:54.689000+00:00\"\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"metadata\",\n \"properties\": {\n \"dtype\": \"object\",\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"ref\",\n \"properties\": {\n \"dtype\": \"object\",\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n }\n ]\n}", + "type": "dataframe" + }, + "application/vnd.google.colaboratory.module+javascript": "\n import \"https://ssl.gstatic.com/colaboratory/data_table/9e65f7085e7ffcb7/data_table.js\";\n\n const table = window.createDataTable({\n data: [[{\n 'v': 0,\n 'f': \"0\",\n },\n\"gs://sample-data-and-media/customer-support/calls/call_1_20250411045535.mp3\",\n{\n 'v': 1753936794296133,\n 'f': \"1753936794296133\",\n },\n\"audio/mpeg\",\n{\n 'v': 65952,\n 'f': \"65952\",\n },\n\"1e99258238c0830c9a59a1a45d5dd095\",\n\"2025-07-31 04:39:54.327000+00:00\",\n\"[]\",\n\"{'uri': 'gs://sample-data-and-media/customer-support/calls/call_1_20250411045535.mp3', 'version': '1753936794296133', 'authorizer': 'data-demo-n25.us.test_connection', 'details': '{\\\"gcs_metadata\\\":{\\\"content_type\\\":\\\"audio/mpeg\\\",\\\"md5_hash\\\":\\\"1e99258238c0830c9a59a1a45d5dd095\\\",\\\"size\\\":65952,\\\"updated\\\":1753936794327000}}'}\"],\n [{\n 'v': 1,\n 'f': \"1\",\n },\n\"gs://sample-data-and-media/customer-support/calls/call_2_20250411045538.mp3\",\n{\n 'v': 1753936794657505,\n 'f': \"1753936794657505\",\n },\n\"audio/mpeg\",\n{\n 'v': 65376,\n 'f': \"65376\",\n },\n\"c8c575ce7cf437c840c28cbb84174e43\",\n\"2025-07-31 04:39:54.689000+00:00\",\n\"[]\",\n\"{'uri': 'gs://sample-data-and-media/customer-support/calls/call_2_20250411045538.mp3', 'version': '1753936794657505', 'authorizer': 'data-demo-n25.us.test_connection', 'details': '{\\\"gcs_metadata\\\":{\\\"content_type\\\":\\\"audio/mpeg\\\",\\\"md5_hash\\\":\\\"c8c575ce7cf437c840c28cbb84174e43\\\",\\\"size\\\":65376,\\\"updated\\\":1753936794689000}}'}\"],\n [{\n 'v': 2,\n 'f': \"2\",\n },\n\"gs://sample-data-and-media/customer-support/calls/call_3_20250411045541.mp3\",\n{\n 'v': 1753936794931315,\n 'f': \"1753936794931315\",\n },\n\"audio/mpeg\",\n{\n 'v': 75744,\n 'f': \"75744\",\n },\n\"de6342b8cea147fdba8a3e3d1edcdeee\",\n\"2025-07-31 04:39:54.963000+00:00\",\n\"[]\",\n\"{'uri': 'gs://sample-data-and-media/customer-support/calls/call_3_20250411045541.mp3', 'version': '1753936794931315', 'authorizer': 'data-demo-n25.us.test_connection', 'details': '{\\\"gcs_metadata\\\":{\\\"content_type\\\":\\\"audio/mpeg\\\",\\\"md5_hash\\\":\\\"de6342b8cea147fdba8a3e3d1edcdeee\\\",\\\"size\\\":75744,\\\"updated\\\":1753936794963000}}'}\"],\n [{\n 'v': 3,\n 'f': \"3\",\n },\n\"gs://sample-data-and-media/customer-support/calls/call_4_20250411045544.mp3\",\n{\n 'v': 1753936795174045,\n 'f': \"1753936795174045\",\n },\n\"audio/mpeg\",\n{\n 'v': 65184,\n 'f': \"65184\",\n },\n\"960ca9e91bca3335b18c2ec1e37c84f2\",\n\"2025-07-31 04:39:55.204000+00:00\",\n\"[]\",\n\"{'uri': 'gs://sample-data-and-media/customer-support/calls/call_4_20250411045544.mp3', 'version': '1753936795174045', 'authorizer': 'data-demo-n25.us.test_connection', 'details': '{\\\"gcs_metadata\\\":{\\\"content_type\\\":\\\"audio/mpeg\\\",\\\"md5_hash\\\":\\\"960ca9e91bca3335b18c2ec1e37c84f2\\\",\\\"size\\\":65184,\\\"updated\\\":1753936795204000}}'}\"],\n [{\n 'v': 4,\n 'f': \"4\",\n },\n\"gs://sample-data-and-media/customer-support/calls/call_5_20250411045547.mp3\",\n{\n 'v': 1753936795493499,\n 'f': \"1753936795493499\",\n },\n\"audio/mpeg\",\n{\n 'v': 63552,\n 'f': \"63552\",\n },\n\"4b846ecfda4b7e57761b55ed4b8b1e2b\",\n\"2025-07-31 04:39:55.523000+00:00\",\n\"[]\",\n\"{'uri': 'gs://sample-data-and-media/customer-support/calls/call_5_20250411045547.mp3', 'version': '1753936795493499', 'authorizer': 'data-demo-n25.us.test_connection', 'details': '{\\\"gcs_metadata\\\":{\\\"content_type\\\":\\\"audio/mpeg\\\",\\\"md5_hash\\\":\\\"4b846ecfda4b7e57761b55ed4b8b1e2b\\\",\\\"size\\\":63552,\\\"updated\\\":1753936795523000}}'}\"]],\n columns: [[\"number\", \"index\"], [\"string\", \"uri\"], [\"number\", \"generation\"], [\"string\", \"content_type\"], [\"number\", \"size\"], [\"string\", \"md5_hash\"], [\"string\", \"updated\"], [\"string\", \"metadata\"], [\"string\", \"ref\"]],\n columnOptions: [{\"width\": \"1px\", \"className\": \"index_column\"}],\n rowsPerPage: 25,\n helpUrl: \"https://colab.research.google.com/notebooks/data_table.ipynb\",\n suppressOutputScrolling: true,\n minimumWidth: undefined,\n });\n\n function appendQuickchartButton(parentElement) {\n let quickchartButtonContainerElement = document.createElement('div');\n quickchartButtonContainerElement.innerHTML = `\n
    \n \n\n\n\n \n
    `;\n parentElement.appendChild(quickchartButtonContainerElement);\n }\n\n appendQuickchartButton(table);\n ", + "text/html": [ + "\n", + "
    \n", + "
    \n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
    urigenerationcontent_typesizemd5_hashupdatedmetadataref
    0gs://sample-data-and-media/customer-support/ca...1753936794296133audio/mpeg659521e99258238c0830c9a59a1a45d5dd0952025-07-31 04:39:54.327000+00:00[]{'uri': 'gs://sample-data-and-media/customer-s...
    1gs://sample-data-and-media/customer-support/ca...1753936794657505audio/mpeg65376c8c575ce7cf437c840c28cbb84174e432025-07-31 04:39:54.689000+00:00[]{'uri': 'gs://sample-data-and-media/customer-s...
    2gs://sample-data-and-media/customer-support/ca...1753936794931315audio/mpeg75744de6342b8cea147fdba8a3e3d1edcdeee2025-07-31 04:39:54.963000+00:00[]{'uri': 'gs://sample-data-and-media/customer-s...
    3gs://sample-data-and-media/customer-support/ca...1753936795174045audio/mpeg65184960ca9e91bca3335b18c2ec1e37c84f22025-07-31 04:39:55.204000+00:00[]{'uri': 'gs://sample-data-and-media/customer-s...
    4gs://sample-data-and-media/customer-support/ca...1753936795493499audio/mpeg635524b846ecfda4b7e57761b55ed4b8b1e2b2025-07-31 04:39:55.523000+00:00[]{'uri': 'gs://sample-data-and-media/customer-s...
    \n", + "
    \n", + "
    \n", + "\n", + "
    \n", + " \n", + "\n", + " \n", + "\n", + " \n", + "
    \n", + "\n", + "\n", + "
    \n", + " \n", + "\n", + "\n", + "\n", + " \n", + "
    \n", + "\n", + "
    \n", + "
    \n" + ], + "text/plain": [ + " uri generation \\\n", + "0 gs://sample-data-and-media/customer-support/ca... 1753936794296133 \n", + "1 gs://sample-data-and-media/customer-support/ca... 1753936794657505 \n", + "2 gs://sample-data-and-media/customer-support/ca... 1753936794931315 \n", + "3 gs://sample-data-and-media/customer-support/ca... 1753936795174045 \n", + "4 gs://sample-data-and-media/customer-support/ca... 1753936795493499 \n", + "\n", + " content_type size md5_hash \\\n", + "0 audio/mpeg 65952 1e99258238c0830c9a59a1a45d5dd095 \n", + "1 audio/mpeg 65376 c8c575ce7cf437c840c28cbb84174e43 \n", + "2 audio/mpeg 75744 de6342b8cea147fdba8a3e3d1edcdeee \n", + "3 audio/mpeg 65184 960ca9e91bca3335b18c2ec1e37c84f2 \n", + "4 audio/mpeg 63552 4b846ecfda4b7e57761b55ed4b8b1e2b \n", + "\n", + " updated metadata \\\n", + "0 2025-07-31 04:39:54.327000+00:00 [] \n", + "1 2025-07-31 04:39:54.689000+00:00 [] \n", + "2 2025-07-31 04:39:54.963000+00:00 [] \n", + "3 2025-07-31 04:39:55.204000+00:00 [] \n", + "4 2025-07-31 04:39:55.523000+00:00 [] \n", + "\n", + " ref \n", + "0 {'uri': 'gs://sample-data-and-media/customer-s... \n", + "1 {'uri': 'gs://sample-data-and-media/customer-s... \n", + "2 {'uri': 'gs://sample-data-and-media/customer-s... \n", + "3 {'uri': 'gs://sample-data-and-media/customer-s... \n", + "4 {'uri': 'gs://sample-data-and-media/customer-s... " + ] + }, + "execution_count": 30, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "%%bigquery --project {PROJECT_ID}\n", + "\n", + "SELECT * FROM `bq_mm_tutorial.object_table`;" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "odlYhyPmpToE" + }, + "source": [ + "### 3. Create a multimodal table\n", + "\n", + "We'll first join the `calls` table that contains structured attributes to the `object_table` table, which contains the `ObjectRef` column, `ref`. We'll refer to this as a \"multimodal table\" because it contains structured and unstructured fields." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 154, + "referenced_widgets": [ + "8ede8c802af8468fb5c3be63f42ff3e1", + "ccc6ce2603e84141ac601b0d5c49a380", + "2ceec39a87b547188784d7b871ff9564", + "9e8609071a1c408b8d3f590233e54ccf", + "9e5ffe49d77d4b4f9aa791d6ae71b0c8", + "c47579c600474f0bab1d89cc29bfe6c2", + "69f9961815944587b9aa12ed08f191c2", + "42b50ebce8954111b3980eef454d8fe8", + "c29db691f5124d6ca9e7ab6c85375565", + "cde40bfcf9964bb3b4814b43f70294ba", + "1cd3e783ca604bb894fb09c21fb80b05" + ] + }, + "id": "wPepSxh2KTs1", + "outputId": "ea086dcf-2e95-4972-ce81-417ab5304d7f" + }, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "8ede8c802af8468fb5c3be63f42ff3e1", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Query is running: 0%| |" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/vnd.google.colaboratory.intrinsic+json": { + "summary": "{\n \"name\": \"get_ipython()\",\n \"rows\": 0,\n \"fields\": []\n}", + "type": "dataframe" + }, + "application/vnd.google.colaboratory.module+javascript": "\n import \"https://ssl.gstatic.com/colaboratory/data_table/9e65f7085e7ffcb7/data_table.js\";\n\n const table = window.createDataTable({\n data: [],\n columns: [[\"number\", \"index\"]],\n columnOptions: [{\"width\": \"1px\", \"className\": \"index_column\"}],\n rowsPerPage: 25,\n helpUrl: \"https://colab.research.google.com/notebooks/data_table.ipynb\",\n suppressOutputScrolling: true,\n minimumWidth: undefined,\n });\n\n function appendQuickchartButton(parentElement) {\n let quickchartButtonContainerElement = document.createElement('div');\n quickchartButtonContainerElement.innerHTML = `\n
    \n \n\n\n\n \n
    `;\n parentElement.appendChild(quickchartButtonContainerElement);\n }\n\n appendQuickchartButton(table);\n ", + "text/html": [ + "\n", + "
    \n", + "
    \n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
    \n", + "
    \n", + "
    \n", + "\n", + "
    \n", + " \n", + "\n", + " \n", + "\n", + " \n", + "
    \n", + "\n", + "\n", + "
    \n", + "
    \n" + ], + "text/plain": [ + "Empty DataFrame\n", + "Columns: []\n", + "Index: []" + ] + }, + "execution_count": 31, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "%%bigquery --project {PROJECT_ID}\n", + "\n", + "CREATE OR REPLACE TABLE `bq_mm_tutorial.calls_combined` AS\n", + "SELECT\n", + " c.*,\n", + " o.ref\n", + "FROM\n", + " `bq_mm_tutorial.calls` AS c\n", + "LEFT JOIN\n", + " `bq_mm_tutorial.object_table` AS o\n", + "ON\n", + " c.call_id = REGEXP_EXTRACT(o.uri, r'calls/([^.]+)')" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "Q2qhFXw1pqpi" + }, + "source": [ + "We now have a single table, `calls_combined` that contains both traditional structured data like `company_revenue` and `product_name` alongside a reference to unstructured customer support calls.\n", + "\n", + "We can run a single query against both data types." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "IeP5EoJZt05z" + }, + "source": [ + "### 4. Run a [multimodal query](https://cloud.google.com/bigquery/docs/analyze-multimodal-data#generative_ai_functions)\n", + "\n", + "Since `calls_combined` contains structured data and pointers to audio in GCS, we can filter the table on both conditions.\n", + "\n", + "In this case, we'll look for \"high value\" companies with audio indicating they're looking to purchase something USING [`AI.GENERATE_BOOL`](https://cloud.google.com/bigquery/docs/reference/standard-sql/bigqueryml-syntax-ai-generate-bool)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 229, + "referenced_widgets": [ + "686da3cfdc4048e9acf5831fa5a2aad8", + "28342d3501f04f12bb63a12ab74acacf", + "034d032bedd14f3aba2446f6742af293", + "624dee9d1e3b491bb039bbbe59b304e8", + "2abe7e6721014b32a886ac517e67e929", + "1c59d05c23f7442dbb54457624f3d17a", + "63ed9bea712c4f3fab1cbe94bf409bd1", + "fab215c996c44201a1f62ea3f3b90642", + "b19ba465cbdc45259320be3c80b44e83", + "36a7136ce005462097fcdee4621456c8", + "460bace6250d463fa779b364bb84c210", + "ef01b1a0839a4fd2ac76c910fea8fb76", + "bc95da919c2e40b2a5d1397c3ccba0b4", + "d8599a119a244cefbd8fffb0eaded349", + "97036a798f164c30a8d3e6fa6da1807c", + "9286b683ed67409798cb40a279b1c41f", + "95d9aa5a25814f26bad5d4f806e401e8", + "d0b84a1a224a46f29cf238d1060ae0c6", + "75ac6046214b4066a73724ea897c7a23", + "ffd831e5406646c99916defe41d5f887", + "3066e0f0e70c4ec8b41def853d25c5fe", + "e0032930338440f4a01ddf44f45e791e" + ] + }, + "id": "QxrWwWiGtfud", + "outputId": "e30569ff-f8f5-4232-cfe6-28846d14be2c" + }, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "686da3cfdc4048e9acf5831fa5a2aad8", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Query is running: 0%| |" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "ef01b1a0839a4fd2ac76c910fea8fb76", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Downloading: 0%| |" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/vnd.google.colaboratory.intrinsic+json": { + "summary": "{\n \"name\": \"get_ipython()\",\n \"rows\": 2,\n \"fields\": [\n {\n \"column\": \"company_name\",\n \"properties\": {\n \"dtype\": \"string\",\n \"num_unique_values\": 1,\n \"samples\": [\n \"Synergy Corp\"\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"customer_name\",\n \"properties\": {\n \"dtype\": \"string\",\n \"num_unique_values\": 2,\n \"samples\": [\n \"Emily Carter\"\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"product_name\",\n \"properties\": {\n \"dtype\": \"string\",\n \"num_unique_values\": 2,\n \"samples\": [\n \"Synergy Project Manager\"\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n }\n ]\n}", + "type": "dataframe" + }, + "application/vnd.google.colaboratory.module+javascript": "\n import \"https://ssl.gstatic.com/colaboratory/data_table/9e65f7085e7ffcb7/data_table.js\";\n\n const table = window.createDataTable({\n data: [[{\n 'v': 0,\n 'f': \"0\",\n },\n\"Synergy Corp\",\n\"David Martin\",\n\"Synergy Cloud Storage\"],\n [{\n 'v': 1,\n 'f': \"1\",\n },\n\"Synergy Corp\",\n\"Emily Carter\",\n\"Synergy Project Manager\"]],\n columns: [[\"number\", \"index\"], [\"string\", \"company_name\"], [\"string\", \"customer_name\"], [\"string\", \"product_name\"]],\n columnOptions: [{\"width\": \"1px\", \"className\": \"index_column\"}],\n rowsPerPage: 25,\n helpUrl: \"https://colab.research.google.com/notebooks/data_table.ipynb\",\n suppressOutputScrolling: true,\n minimumWidth: undefined,\n });\n\n function appendQuickchartButton(parentElement) {\n let quickchartButtonContainerElement = document.createElement('div');\n quickchartButtonContainerElement.innerHTML = `\n
    \n \n\n\n\n \n
    `;\n parentElement.appendChild(quickchartButtonContainerElement);\n }\n\n appendQuickchartButton(table);\n ", + "text/html": [ + "\n", + "
    \n", + "
    \n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
    company_namecustomer_nameproduct_name
    0Synergy CorpDavid MartinSynergy Cloud Storage
    1Synergy CorpEmily CarterSynergy Project Manager
    \n", + "
    \n", + "
    \n", + "\n", + "
    \n", + " \n", + "\n", + " \n", + "\n", + " \n", + "
    \n", + "\n", + "\n", + "
    \n", + " \n", + "\n", + "\n", + "\n", + " \n", + "
    \n", + "\n", + "
    \n", + "
    \n" + ], + "text/plain": [ + " company_name customer_name product_name\n", + "0 Synergy Corp David Martin Synergy Cloud Storage\n", + "1 Synergy Corp Emily Carter Synergy Project Manager" + ] + }, + "execution_count": 35, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "%%bigquery --project {PROJECT_ID}\n", + "\n", + "SELECT company_name, customer_name, product_name\n", + "FROM `bq_mm_tutorial.calls_combined`\n", + "WHERE company_revenue > 15000000\n", + "AND\n", + " AI.GENERATE_BOOL(\n", + " prompt => (\"Wants to buy something\", ref),\n", + " connection_id => \"us.test_connection\").result\n", + ";" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "qslXv2wXt2zs" + }, + "source": [ + "### 5. Run a multimodal query on an ARRAY of `ObjectRef`s\n", + "\n", + "Because an `ObjectRef` is a `STRUCT` data type, it can also be nested as an array and passed to Gemini for inference.\n", + "\n", + "In this example, we generate high-level themes customers are calling about, grouped by `company_name` using [`AI.GENERATE`](https://cloud.google.com/bigquery/docs/reference/standard-sql/bigqueryml-syntax-ai-generate)." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 245, + "referenced_widgets": [ + "5cad28074be84d29a4cde869bec779a2", + "65f107bf6c4948f0bded28f268f99308", + "343d1a19bc3c427e80f1c5af008cc88c", + "8f67587019714bfea93695c847f36a53", + "2198e1de24dd4ceabcbe3517adeba7ac", + "880a9a5982c94b62b9e460539b605883", + "1dd44c53f6b5478bb266f2ed2b20b4da", + "4ae1c2064441402eb2f5a561afcb8f5e", + "3c0f0169c5484d29879ec229e605ac17", + "ad003a8f494d48c29bf1b27d163fdd1d", + "c85a1f6b529f4b4f811d2e82a1f14d60", + "20df373ee81c4a5cbcea085b982679fd", + "f17e1b9f1fd34a9186cf7d5228519014", + "061ed8babda04387bff0bf6c829d565b", + "3fbabbc07fee4ae49ba768d98d2bd8ab", + "1773cfd4048f4e54ae50161292e7bd30", + "b6c900858dff4fe8bdd2e32589adabbd", + "708dc79209a6490ca8c3a6abef3d445a", + "b9d86186597d47a99cf1a938a107255e", + "77bb4dbef9944074ae456bc597136aed", + "d86f828be9ac4646866ccf66f28e6bcb", + "581b246d5e6449679873a67a9cad9589" + ] + }, + "id": "DFmwwe1yuL6F", + "outputId": "ca69a3e2-fec0-47ac-83a2-47d1174889a5" + }, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "5cad28074be84d29a4cde869bec779a2", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Query is running: 0%| |" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "20df373ee81c4a5cbcea085b982679fd", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Downloading: 0%| |" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/vnd.google.colaboratory.intrinsic+json": { + "summary": "{\n \"name\": \"get_ipython()\",\n \"rows\": 2,\n \"fields\": [\n {\n \"column\": \"company_name\",\n \"properties\": {\n \"dtype\": \"string\",\n \"num_unique_values\": 2,\n \"samples\": [\n \"Global Tech Enterprises\",\n \"Synergy Corp\"\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"themes\",\n \"properties\": {\n \"dtype\": \"object\",\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n }\n ]\n}", + "type": "dataframe" + }, + "application/vnd.google.colaboratory.module+javascript": "\n import \"https://ssl.gstatic.com/colaboratory/data_table/9e65f7085e7ffcb7/data_table.js\";\n\n const table = window.createDataTable({\n data: [[{\n 'v': 0,\n 'f': \"0\",\n },\n\"Synergy Corp\",\n\"['Customer Service' 'Cloud Storage']\"],\n [{\n 'v': 1,\n 'f': \"1\",\n },\n\"Global Tech Enterprises\",\n\"['VPN' 'Disconnect' 'Refund' 'Data Backup' 'Storage' 'Upgrade'\\n 'Website Issue' 'Antivirus' 'Activation' 'Pop-ups']\"]],\n columns: [[\"number\", \"index\"], [\"string\", \"company_name\"], [\"string\", \"themes\"]],\n columnOptions: [{\"width\": \"1px\", \"className\": \"index_column\"}],\n rowsPerPage: 25,\n helpUrl: \"https://colab.research.google.com/notebooks/data_table.ipynb\",\n suppressOutputScrolling: true,\n minimumWidth: undefined,\n });\n\n function appendQuickchartButton(parentElement) {\n let quickchartButtonContainerElement = document.createElement('div');\n quickchartButtonContainerElement.innerHTML = `\n
    \n \n\n\n\n \n
    `;\n parentElement.appendChild(quickchartButtonContainerElement);\n }\n\n appendQuickchartButton(table);\n ", + "text/html": [ + "\n", + "
    \n", + "
    \n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
    company_namethemes
    0Synergy Corp[Customer Service, Cloud Storage]
    1Global Tech Enterprises[VPN, Disconnect, Refund, Data Backup, Storage...
    \n", + "
    \n", + "
    \n", + "\n", + "
    \n", + " \n", + "\n", + " \n", + "\n", + " \n", + "
    \n", + "\n", + "\n", + "
    \n", + " \n", + "\n", + "\n", + "\n", + " \n", + "
    \n", + "\n", + "
    \n", + "
    \n" + ], + "text/plain": [ + " company_name themes\n", + "0 Synergy Corp [Customer Service, Cloud Storage]\n", + "1 Global Tech Enterprises [VPN, Disconnect, Refund, Data Backup, Storage..." + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "%%bigquery --project {PROJECT_ID}\n", + "\n", + "SELECT\n", + " company_name,\n", + " AI.GENERATE(\n", + " ('Give 1-2 word themes customers are calling about', refs),\n", + " connection_id => 'us.test_connection',\n", + " endpoint => 'gemini-2.5-flash',\n", + " output_schema => 'themes ARRAY').themes\n", + "FROM (SELECT company_name, ARRAY_AGG(ref) AS refs FROM `bq_mm_tutorial.calls_combined` GROUP BY company_name);\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "JxM1hI7X5RXU" + }, + "source": [ + "### Recap\n", + "\n", + "In this scenario we:\n", + "* Began with a table called `calls` containing structured data\n", + "* Created an object table over audio files in GCS. This contained an `ObjectRef` column\n", + "* Created a multimodal table called `calls_combined`\n", + "* Ran a query against the `calls_combined` table using structured *and* unstructured data in a `WHERE` clause\n", + "* Used an array of `ObjectRef`s to understand key themes by `company_name`\n", + "\n", + "This scenario used an object table to create our `ObjectRef` column. In the next scenario, we'll create it programmatically." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "v6ibKGHPuayE" + }, + "source": [ + "\n", + "\n", + "---\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "nmHS_hmFuckJ" + }, + "source": [ + "## Scenario 2: City 311 Response\n", + "\n", + "In this scenario, we'll work with a common public sector dataset: [311 reports](https://en.wikipedia.org/wiki/311_(telephone_number)). Citizens submit issues (like potholes or graffiti) that include text descriptions and often upload supporting media like photos, audio recordings, or videos.\n", + "\n", + "Our goal is to use multimodal analysis to automatically triage these reports, assess their urgency, and route them to the correct city department." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "R0jTjJfC-NfD" + }, + "source": [ + "### 1. Load structured data\n", + "\n", + "We'll first begin by adding two tables to our BigQuery environment:\n", + "* **`reports`** contains 311 report tabular data, like the `ticket_id`, `location`, `text_description` and more.\n", + "* **`media`** contains `uri` fields corresponding to any media associated with a 311 ticket (e.g. images, audio)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 154, + "referenced_widgets": [ + "18ef8fc44f44409dbf8e8aa84c3f52ad", + "978e297b1e00476384c974d274b7e735", + "c5487ec63239476cbe0b4c2eafbca798", + "a4fa0932219b4ed2aedd1bfb2c30e913", + "f6a9ed3959a7477e8512e030ac80790d", + "f859be1f377549c2a2e5581677316a05", + "f5ce1137975c4abba2b14db8130ef1ed", + "5b0739ef14da4091b45bfb914c906df4", + "0848d04abdce4976a4a138ebedf0289b", + "b18bdf1e286144079cf4b24d2a6b3234", + "a73f21ae668f46be995e3703c6f1835b" + ] + }, + "id": "42xL93VL1Buh", + "outputId": "f1349304-03e9-434a-9dd9-b840a01e11b2" + }, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "18ef8fc44f44409dbf8e8aa84c3f52ad", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Query is running: 0%| |" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/vnd.google.colaboratory.intrinsic+json": { + "summary": "{\n \"name\": \"get_ipython()\",\n \"rows\": 0,\n \"fields\": []\n}", + "type": "dataframe" + }, + "application/vnd.google.colaboratory.module+javascript": "\n import \"https://ssl.gstatic.com/colaboratory/data_table/9e65f7085e7ffcb7/data_table.js\";\n\n const table = window.createDataTable({\n data: [],\n columns: [[\"number\", \"index\"]],\n columnOptions: [{\"width\": \"1px\", \"className\": \"index_column\"}],\n rowsPerPage: 25,\n helpUrl: \"https://colab.research.google.com/notebooks/data_table.ipynb\",\n suppressOutputScrolling: true,\n minimumWidth: undefined,\n });\n\n function appendQuickchartButton(parentElement) {\n let quickchartButtonContainerElement = document.createElement('div');\n quickchartButtonContainerElement.innerHTML = `\n
    \n \n\n\n\n \n
    `;\n parentElement.appendChild(quickchartButtonContainerElement);\n }\n\n appendQuickchartButton(table);\n ", + "text/html": [ + "\n", + "
    \n", + "
    \n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
    \n", + "
    \n", + "
    \n", + "\n", + "
    \n", + " \n", + "\n", + " \n", + "\n", + " \n", + "
    \n", + "\n", + "\n", + "
    \n", + "
    \n" + ], + "text/plain": [ + "Empty DataFrame\n", + "Columns: []\n", + "Index: []" + ] + }, + "execution_count": 37, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "%%bigquery --project {PROJECT_ID}\n", + "\n", + "LOAD DATA OVERWRITE bq_mm_tutorial.reports\n", + "FROM FILES (\n", + " uris = ['gs://sample-data-and-media/311-demo/tables/city_311_reports'],\n", + " format = 'PARQUET'\n", + ");\n", + "\n", + "LOAD DATA OVERWRITE bq_mm_tutorial.media\n", + "FROM FILES (\n", + " uris = ['gs://sample-data-and-media/311-demo/tables/city_311_media'],\n", + " format = 'PARQUET'\n", + ");" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "YIE0ZeOzukua" + }, + "source": [ + "### 2. Programmatically create an `ObjectRef`\n", + "\n", + "In **Scenario 1**, you learned to create an `ObjectRef` using [object tables](https://cloud.google.com/bigquery/docs/analyze-multimodal-data#object_tables).\n", + "\n", + "In this scenario, you'll use a more dynamic workflow, where you create `ObjectRef`s using a set of built-in SQL functions.\n", + "\n", + "* [`OBJ.MAKE_REF`](https://cloud.google.com/bigquery/docs/reference/standard-sql/objectref_functions#objmake_ref): This function takes the string from the `uri` column and converts it into an `ObjectRef`\n", + "\n", + "* [`OBJ.FETCH_METADATA`](https://cloud.google.com/bigquery/docs/reference/standard-sql/objectref_functions#objfetch_metadata) populates the `ObjectRef` with important file metadata from GCS\n", + "\n", + "Here's an example with `OBJ.MAKE_REF` that references a single object with a connection:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 207, + "referenced_widgets": [ + "65a79b101e774d3ebc70b11595c56b17", + "c79d4b6e13294795a7586e14a63255ea", + "0e2cf603458e486db98de941df3af34b", + "53ffd2a507dd4cfe8626c72736c6b35f", + "6e0eae13e8f9405dab2142a28b4143a7", + "7c33beb9ba2d4b4399db4ef2457a6e3b", + "68b06a2c95314dd7bbee7497a455ea4f", + "e7f0a26b0f5d4f78bd62c8af876e1b22", + "860d1619667a4532971f5430d811b827", + "9fd692dadc6d435da2698afb073758c4", + "47b24af3837f4ec89e5b608b5d6c58b9", + "e553f3c326104035916463e9dd94fbf2", + "ebaa2ccf7dcc47c88b80d3448918a25e", + "57ca18529ebc49d09c7fff98326816b6", + "ef974197c9864aad82d0ad78f0388d6a", + "86990d96210f4d5eb9dadcffc6eb8c0c", + "5389deee286c474f865a6f47ab995bd2", + "3bf11f2792c744f69f47e753337a52f7", + "d35cf9b20cb343a0b89299d71d586e67", + "7fff81ab90cf45798ab3d9fbe21e88e8", + "79411d421c014f4eb23dac0084a5c2b0", + "ba29c683005e4eeb9f50dacbf1983a6d" + ] + }, + "id": "QHdnc7hcu0k2", + "outputId": "c019c905-0fee-4f1e-ee39-5636aa33af3c" + }, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "65a79b101e774d3ebc70b11595c56b17", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Query is running: 0%| |" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "e553f3c326104035916463e9dd94fbf2", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Downloading: 0%| |" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/vnd.google.colaboratory.intrinsic+json": { + "summary": "{\n \"name\": \"get_ipython()\",\n \"rows\": 1,\n \"fields\": [\n {\n \"column\": \"image_ref\",\n \"properties\": {\n \"dtype\": \"object\",\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n }\n ]\n}", + "type": "dataframe" + }, + "application/vnd.google.colaboratory.module+javascript": "\n import \"https://ssl.gstatic.com/colaboratory/data_table/9e65f7085e7ffcb7/data_table.js\";\n\n const table = window.createDataTable({\n data: [[{\n 'v': 0,\n 'f': \"0\",\n },\n\"{'uri': 'gs://sample-data-and-media/311-demo/images/311-20250409-1515EF.png', 'version': None, 'authorizer': 'data-demo-n25.us.test_connection', 'details': None}\"]],\n columns: [[\"number\", \"index\"], [\"string\", \"image_ref\"]],\n columnOptions: [{\"width\": \"1px\", \"className\": \"index_column\"}],\n rowsPerPage: 25,\n helpUrl: \"https://colab.research.google.com/notebooks/data_table.ipynb\",\n suppressOutputScrolling: true,\n minimumWidth: undefined,\n });\n\n function appendQuickchartButton(parentElement) {\n let quickchartButtonContainerElement = document.createElement('div');\n quickchartButtonContainerElement.innerHTML = `\n
    \n \n\n\n\n \n
    `;\n parentElement.appendChild(quickchartButtonContainerElement);\n }\n\n appendQuickchartButton(table);\n ", + "text/html": [ + "\n", + "
    \n", + "
    \n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
    image_ref
    0{'uri': 'gs://sample-data-and-media/311-demo/i...
    \n", + "
    \n", + "
    \n", + "\n", + "
    \n", + " \n", + "\n", + " \n", + "\n", + " \n", + "
    \n", + "\n", + "\n", + "
    \n", + "
    \n" + ], + "text/plain": [ + " image_ref\n", + "0 {'uri': 'gs://sample-data-and-media/311-demo/i..." + ] + }, + "execution_count": 38, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "%%bigquery --project {PROJECT_ID}\n", + "\n", + "SELECT OBJ.MAKE_REF('gs://sample-data-and-media/311-demo/images/311-20250409-1515EF.png', 'us.test_connection') AS image_ref;" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "nTaOjfIb06TI" + }, + "source": [ + "We can then get [GCS metadata](https://cloud.google.com/storage/docs/metadata) for the object by wrapping the `OBJ.FETCH_METADATA` function around the prior query.\n", + "\n", + "This provides additional metadata, like a GCS object version, the object type, when it was last updated, and more." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 224, + "referenced_widgets": [ + "ba71adb40d0c4f93ba4d24293c3837dc", + "b0d4be6ee7064916b807b54e53bd233d", + "89711047bae74967860f75e348f058a8", + "df00a456ccdf410fab7f06f3ad845c98", + "6341733d9b4b47629d4ea14b210dcf3b", + "561d86e8001549138dc5aa4b2cc1e6e9", + "00fd5605c58746c2a8b7eac315062e2a", + "280b9c31d968456fa8bf49962a2141fc", + "1934aa5c02e346b98d75aa8187f16880", + "9ef6d136f75a4a4e8038f264a2ee252d", + "05f76663c67140c584189ea0be14cb4c", + "0e43461dda8447e68bcac400a2e3b5f7", + "c2df60b0eac349a8b6a7fbf5ca6300ad", + "d260ec4e8df94523a0ed165f015cbb63", + "8ddb8b981e7c4f90804d295ef64227dd", + "a4a975511e4b45aa9239f9aae0373a54", + "59145f4a708448819dec5c4513efdee9", + "1f774daa4b3c4467bc9aaba52f1a0894", + "44ea649320774071b52bfac20931ade6", + "b5d63777f49745c7b82953000f4ca854", + "8600cf07284749fd86c915731d5a0ce5", + "b1d86680f91a44c4a2d3cbd38ff6c0fe" + ] + }, + "id": "wkolFoSXu0Xz", + "outputId": "323501b1-44f5-4930-eee0-aac6c55ec833" + }, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "ba71adb40d0c4f93ba4d24293c3837dc", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Query is running: 0%| |" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "0e43461dda8447e68bcac400a2e3b5f7", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Downloading: 0%| |" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/vnd.google.colaboratory.intrinsic+json": { + "summary": "{\n \"name\": \"get_ipython()\",\n \"rows\": 1,\n \"fields\": [\n {\n \"column\": \"image_ref\",\n \"properties\": {\n \"dtype\": \"object\",\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n }\n ]\n}", + "type": "dataframe" + }, + "application/vnd.google.colaboratory.module+javascript": "\n import \"https://ssl.gstatic.com/colaboratory/data_table/9e65f7085e7ffcb7/data_table.js\";\n\n const table = window.createDataTable({\n data: [[{\n 'v': 0,\n 'f': \"0\",\n },\n\"{'uri': 'gs://sample-data-and-media/311-demo/images/311-20250409-1515EF.png', 'version': '1753891211822957', 'authorizer': 'data-demo-n25.us.test_connection', 'details': '{\\\"gcs_metadata\\\":{\\\"content_type\\\":\\\"image/png\\\",\\\"md5_hash\\\":\\\"6b50c5266a530079e836bc32c43b5c0e\\\",\\\"size\\\":2217041,\\\"updated\\\":1753891211000000}}'}\"]],\n columns: [[\"number\", \"index\"], [\"string\", \"image_ref\"]],\n columnOptions: [{\"width\": \"1px\", \"className\": \"index_column\"}],\n rowsPerPage: 25,\n helpUrl: \"https://colab.research.google.com/notebooks/data_table.ipynb\",\n suppressOutputScrolling: true,\n minimumWidth: undefined,\n });\n\n function appendQuickchartButton(parentElement) {\n let quickchartButtonContainerElement = document.createElement('div');\n quickchartButtonContainerElement.innerHTML = `\n
    \n \n\n\n\n \n
    `;\n parentElement.appendChild(quickchartButtonContainerElement);\n }\n\n appendQuickchartButton(table);\n ", + "text/html": [ + "\n", + "
    \n", + "
    \n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
    image_ref
    0{'uri': 'gs://sample-data-and-media/311-demo/i...
    \n", + "
    \n", + "
    \n", + "\n", + "
    \n", + " \n", + "\n", + " \n", + "\n", + " \n", + "
    \n", + "\n", + "\n", + "
    \n", + "
    \n" + ], + "text/plain": [ + " image_ref\n", + "0 {'uri': 'gs://sample-data-and-media/311-demo/i..." + ] + }, + "execution_count": 39, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "%%bigquery --project {PROJECT_ID}\n", + "\n", + "SELECT OBJ.FETCH_METADATA(OBJ.MAKE_REF('gs://sample-data-and-media/311-demo/images/311-20250409-1515EF.png', 'us.test_connection')) AS image_ref;" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "UsymB7TvAliK" + }, + "source": [ + "### 3. Create a multimodal table\n", + "\n", + "To create our multimodal table, `reports_mm` we first join the `reports` and `media` table and also convert the `audio_uri`, `image_uri`, and `video_uri` fields to `ObjectRef` fields using the `OBJ.` functions.\n", + "\n", + "Note that a multimodal table can have multiple `ObjectRef` columns and you can alias these columns." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 867, + "referenced_widgets": [ + "8906a505d0764a308693e43a6e74d0b1", + "33ff4ba94a4f49cbb41317168773bb63", + "54eadb7847784de28baee69c04ec2730", + "a93a9fa0561e43daa15fa9d0cc1dac47", + "d5595568695e4649b1f65bb113841520", + "46941be814dd4bc08ee831b686ecbebe", + "1a6a53a23c11474ea1490f044e00fc90", + "ca4104787ba94939b7e0a56383353b1a", + "d98a1e7bc53e4085b9f752520cb29d54", + "f3ec2581dfed4739a2970d6ef7ec8a16", + "049e79c247d04bb7b84a99436a380f70", + "cbfd84f13b024610b76d58edd9a4859d", + "adda6f62d05e4a019e085a6f88b45397", + "1aef208b7fcb4f98a5af24f35ca7b60f", + "c0be490ef6e940658e0c5481ebc696b4", + "cc4a0f778a3b4291b8e182d3128e93ce", + "a833cdda5d3b4d94852ef4f4bde7c9b3", + "6492cc2410b14a38a3144ad27c47baf4", + "ba5b6d8fbdce456cab1de49f916e9651", + "dc1b836b4d73400aa7a256d439fe8d8c", + "719b3da5b8a74cf58c5b8e8352d29b60", + "f47785db50134dfb96eb7532f8fe7ebc" + ] + }, + "id": "FbnyBYuRAmdP", + "outputId": "f165ee31-d734-4e8c-81c0-42102844fffb" + }, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "8906a505d0764a308693e43a6e74d0b1", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Query is running: 0%| |" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "cbfd84f13b024610b76d58edd9a4859d", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Downloading: 0%| |" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/vnd.google.colaboratory.intrinsic+json": { + "summary": "{\n \"name\": \"get_ipython()\",\n \"rows\": 5,\n \"fields\": [\n {\n \"column\": \"ticket_id\",\n \"properties\": {\n \"dtype\": \"string\",\n \"num_unique_values\": 5,\n \"samples\": [\n \"311-20250409-A57DB4\",\n \"311-20250409-60290D\",\n \"311-20250409-59911E\"\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"timestamp\",\n \"properties\": {\n \"dtype\": \"date\",\n \"min\": \"2025-04-09 17:16:21+00:00\",\n \"max\": \"2025-04-09 17:35:21+00:00\",\n \"num_unique_values\": 4,\n \"samples\": [\n \"2025-04-09 17:29:21+00:00\",\n \"2025-04-09 17:16:21+00:00\",\n \"2025-04-09 17:28:21+00:00\"\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"reporter_id\",\n \"properties\": {\n \"dtype\": \"string\",\n \"num_unique_values\": 5,\n \"samples\": [\n \"Citizen-77E1\",\n \"Citizen-4B3B\",\n \"Citizen-69DD\"\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"location\",\n \"properties\": {\n \"dtype\": \"string\",\n \"num_unique_values\": 5,\n \"samples\": [\n \"POINT(-118.33 34.09)\",\n \"POINT(-118.35 34.055)\",\n \"POINT(-118.385 34.06)\"\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"location_description\",\n \"properties\": {\n \"dtype\": \"string\",\n \"num_unique_values\": 5,\n \"samples\": [\n \"Leaking fire hydrant on Highland Ave near the movie theater west of Hollywood Blvd\",\n \"Large sinkhole on the sidewalk near apartment buildings on La Brea Ave south of Wilshire Blvd\",\n \"Car parked on the sidewalk on Beverly Hills Blvd near the cafe east of La Cienega Blvd\"\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"category_reported\",\n \"properties\": {\n \"dtype\": \"string\",\n \"num_unique_values\": 5,\n \"samples\": [\n \"Leaking Fire Hydrant\",\n \"Major Sinkhole\",\n \"Abandoned Vehicle\"\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"description\",\n \"properties\": {\n \"dtype\": \"string\",\n \"num_unique_values\": 5,\n \"samples\": [\n \"A leaky fire hydrant that has been creating a puddle\",\n \"A huge sinkhole that looks like it will eat a car. FIX SOON\",\n \"Someone drove their car on the sidewalk and abandoned it. It needs to be towed. Its in the way of pedestrians!\"\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"status\",\n \"properties\": {\n \"dtype\": \"category\",\n \"num_unique_values\": 1,\n \"samples\": [\n \"Open\"\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"district\",\n \"properties\": {\n \"dtype\": \"category\",\n \"num_unique_values\": 2,\n \"samples\": [\n \"South\"\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"audio_ref\",\n \"properties\": {\n \"dtype\": \"object\",\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"image_ref\",\n \"properties\": {\n \"dtype\": \"object\",\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"video_ref\",\n \"properties\": {\n \"dtype\": \"object\",\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n }\n ]\n}", + "type": "dataframe" + }, + "application/vnd.google.colaboratory.module+javascript": "\n import \"https://ssl.gstatic.com/colaboratory/data_table/9e65f7085e7ffcb7/data_table.js\";\n\n const table = window.createDataTable({\n data: [[{\n 'v': 0,\n 'f': \"0\",\n },\n\"311-20250409-52F19E\",\n\"2025-04-09 17:28:21+00:00\",\n\"Citizen-4A2E\",\n\"POINT(-118.245 34.052)\",\n\"Large trash pile in the alley behind the building on West 2nd St between Main St and Hope St\",\n\"Illegal Dumping/Trash Pile\",\n\"There is a huge pile of trash in the alley and I cannot walk through it. It gets bigger every day.\",\n\"Open\",\n\"North\",\n\"{'uri': 'gs://n25-311-demo/calls/311-20250409-52F19E.mp3', 'version': '1744220287159290', 'authorizer': 'data-demo-n25.us.test_connection', 'details': '{\\\"gcs_metadata\\\":{\\\"content_type\\\":\\\"audio/mpeg\\\",\\\"md5_hash\\\":\\\"4e19319d8ada9401fedd4dbfc471b435\\\",\\\"size\\\":48576,\\\"updated\\\":1744220287000000}}'}\",\n\"{'uri': 'gs://n25-311-demo/images/311-20250409-52F19E.png', 'version': '1744223317884648', 'authorizer': 'data-demo-n25.us.test_connection', 'details': '{\\\"gcs_metadata\\\":{\\\"content_type\\\":\\\"image/png\\\",\\\"md5_hash\\\":\\\"eb8eb813edda4f3faaa9331410577977\\\",\\\"size\\\":1692711,\\\"updated\\\":1744223317000000}}'}\",\n\"{'uri': 'gs://n25-311-demo/videos/311-20250409-52F19E.mp4', 'version': '1744238753544507', 'authorizer': 'data-demo-n25.us.test_connection', 'details': '{\\\"gcs_metadata\\\":{\\\"content_type\\\":\\\"video/mp4\\\",\\\"md5_hash\\\":\\\"eb537faabffdd3c198234ed47d25d0aa\\\",\\\"size\\\":4205301,\\\"updated\\\":1744238753000000}}'}\"],\n [{\n 'v': 1,\n 'f': \"1\",\n },\n\"311-20250409-A57DB4\",\n\"2025-04-09 17:29:21+00:00\",\n\"Citizen-77E1\",\n\"POINT(-118.33 34.09)\",\n\"Leaking fire hydrant on Highland Ave near the movie theater west of Hollywood Blvd\",\n\"Leaking Fire Hydrant\",\n\"A leaky fire hydrant that has been creating a puddle\",\n\"Open\",\n\"North\",\n\"{'uri': 'gs://n25-311-demo/calls/311-20250409-A57DB4.mp3', 'version': '1744220289163179', 'authorizer': 'data-demo-n25.us.test_connection', 'details': '{\\\"gcs_metadata\\\":{\\\"content_type\\\":\\\"audio/mpeg\\\",\\\"md5_hash\\\":\\\"d961d77277ede4c52a7fcbe53777fcd3\\\",\\\"size\\\":40032,\\\"updated\\\":1744220289000000}}'}\",\n\"{'uri': 'gs://n25-311-demo/images/311-20250409-A57DB4.png', 'version': '1744239339712496', 'authorizer': 'data-demo-n25.us.test_connection', 'details': '{\\\"gcs_metadata\\\":{\\\"content_type\\\":\\\"image/png\\\",\\\"md5_hash\\\":\\\"00a987cfa97a9b09879bdd0d373a95c8\\\",\\\"size\\\":1600367,\\\"updated\\\":1744239339000000}}'}\",\n\"{'uri': 'gs://n25-311-demo/videos/311-20250409-A57DB4.mp4', 'version': '1744239317334568', 'authorizer': 'data-demo-n25.us.test_connection', 'details': '{\\\"gcs_metadata\\\":{\\\"content_type\\\":\\\"video/mp4\\\",\\\"md5_hash\\\":\\\"f92f5ff69a6228c21dd3c18ff15a8f9a\\\",\\\"size\\\":1335816,\\\"updated\\\":1744239317000000}}'}\"],\n [{\n 'v': 2,\n 'f': \"2\",\n },\n\"311-20250409-59911E\",\n\"2025-04-09 17:28:21+00:00\",\n\"Citizen-69DD\",\n\"POINT(-118.385 34.06)\",\n\"Car parked on the sidewalk on Beverly Hills Blvd near the cafe east of La Cienega Blvd\",\n\"Abandoned Vehicle\",\n\"Someone drove their car on the sidewalk and abandoned it. It needs to be towed. Its in the way of pedestrians!\",\n\"Open\",\n\"North\",\n\"{'uri': 'gs://n25-311-demo/calls/311-20250409-59911E.mp3', 'version': '1744220291265142', 'authorizer': 'data-demo-n25.us.test_connection', 'details': '{\\\"gcs_metadata\\\":{\\\"content_type\\\":\\\"audio/mpeg\\\",\\\"md5_hash\\\":\\\"cea743e87b873554e6faf55c89db2aad\\\",\\\"size\\\":41856,\\\"updated\\\":1744220291000000}}'}\",\n\"{'uri': 'gs://n25-311-demo/images/311-20250409-59911E.png', 'version': '1744240096289336', 'authorizer': 'data-demo-n25.us.test_connection', 'details': '{\\\"gcs_metadata\\\":{\\\"content_type\\\":\\\"image/png\\\",\\\"md5_hash\\\":\\\"0e53d8ffe485532436822e9c5e8159ce\\\",\\\"size\\\":1945756,\\\"updated\\\":1744240096000000}}'}\",\n\"{'uri': 'gs://n25-311-demo/videos/311-20250409-59911E.mp4', 'version': '1744240085395689', 'authorizer': 'data-demo-n25.us.test_connection', 'details': '{\\\"gcs_metadata\\\":{\\\"content_type\\\":\\\"video/mp4\\\",\\\"md5_hash\\\":\\\"147422cb23771f97b9fad4f4ef440227\\\",\\\"size\\\":1689108,\\\"updated\\\":1744240085000000}}'}\"],\n [{\n 'v': 3,\n 'f': \"3\",\n },\n\"311-20250409-1515EF\",\n\"2025-04-09 17:35:21+00:00\",\n\"Citizen-5DFC\",\n\"POINT(-118.341 34.063)\",\n\"Large pothole on Sunset Blvd near the bus stop east of La Brea Ave\",\n\"Pothole\",\n\"There is a small pothole near the bus stop that should be filled. It is a little bit of a hazard.\",\n\"Open\",\n\"South\",\n\"{'uri': 'gs://n25-311-demo/calls/311-20250409-1515EF.mp3', 'version': '1744220284822352', 'authorizer': 'data-demo-n25.us.test_connection', 'details': '{\\\"gcs_metadata\\\":{\\\"content_type\\\":\\\"audio/mpeg\\\",\\\"md5_hash\\\":\\\"0e1e2551947e47dc603dea1b144b6b09\\\",\\\"size\\\":38592,\\\"updated\\\":1744220284000000}}'}\",\n\"{'uri': 'gs://n25-311-demo/images/311-20250409-1515EF.png', 'version': '1744238597874740', 'authorizer': 'data-demo-n25.us.test_connection', 'details': '{\\\"gcs_metadata\\\":{\\\"content_type\\\":\\\"image/png\\\",\\\"md5_hash\\\":\\\"6b50c5266a530079e836bc32c43b5c0e\\\",\\\"size\\\":2217041,\\\"updated\\\":1744238597000000}}'}\",\n\"{'uri': 'gs://n25-311-demo/videos/311-20250409-1515EF.mp4', 'version': '1744238755038594', 'authorizer': 'data-demo-n25.us.test_connection', 'details': '{\\\"gcs_metadata\\\":{\\\"content_type\\\":\\\"video/mp4\\\",\\\"md5_hash\\\":\\\"f190671c4c52dee225ecc89f6a8b97e5\\\",\\\"size\\\":8157657,\\\"updated\\\":1744238755000000}}'}\"],\n [{\n 'v': 4,\n 'f': \"4\",\n },\n\"311-20250409-60290D\",\n\"2025-04-09 17:16:21+00:00\",\n\"Citizen-4B3B\",\n\"POINT(-118.35 34.055)\",\n\"Large sinkhole on the sidewalk near apartment buildings on La Brea Ave south of Wilshire Blvd\",\n\"Major Sinkhole\",\n\"A huge sinkhole that looks like it will eat a car. FIX SOON\",\n\"Open\",\n\"South\",\n\"{'uri': 'gs://n25-311-demo/calls/311-20250409-60290D.mp3', 'version': '1744220293329858', 'authorizer': 'data-demo-n25.us.test_connection', 'details': '{\\\"gcs_metadata\\\":{\\\"content_type\\\":\\\"audio/mpeg\\\",\\\"md5_hash\\\":\\\"87194fd410abd637e86f0fb822bb459c\\\",\\\"size\\\":41184,\\\"updated\\\":1744220293000000}}'}\",\n\"{'uri': 'gs://n25-311-demo/images/311-20250409-60290D.png', 'version': '1744239339727794', 'authorizer': 'data-demo-n25.us.test_connection', 'details': '{\\\"gcs_metadata\\\":{\\\"content_type\\\":\\\"image/png\\\",\\\"md5_hash\\\":\\\"1a9369188045c5475cc14aa9b7e6af47\\\",\\\"size\\\":1825501,\\\"updated\\\":1744239339000000}}'}\",\n\"{'uri': 'gs://n25-311-demo/videos/311-20250409-60290D.mp4', 'version': '1744239208881322', 'authorizer': 'data-demo-n25.us.test_connection', 'details': '{\\\"gcs_metadata\\\":{\\\"content_type\\\":\\\"video/mp4\\\",\\\"md5_hash\\\":\\\"d51a19210029014d075fb9197eaf27f9\\\",\\\"size\\\":5407865,\\\"updated\\\":1744239208000000}}'}\"]],\n columns: [[\"number\", \"index\"], [\"string\", \"ticket_id\"], [\"string\", \"timestamp\"], [\"string\", \"reporter_id\"], [\"string\", \"location\"], [\"string\", \"location_description\"], [\"string\", \"category_reported\"], [\"string\", \"description\"], [\"string\", \"status\"], [\"string\", \"district\"], [\"string\", \"audio_ref\"], [\"string\", \"image_ref\"], [\"string\", \"video_ref\"]],\n columnOptions: [{\"width\": \"1px\", \"className\": \"index_column\"}],\n rowsPerPage: 25,\n helpUrl: \"https://colab.research.google.com/notebooks/data_table.ipynb\",\n suppressOutputScrolling: true,\n minimumWidth: undefined,\n });\n\n function appendQuickchartButton(parentElement) {\n let quickchartButtonContainerElement = document.createElement('div');\n quickchartButtonContainerElement.innerHTML = `\n
    \n \n\n\n\n \n
    `;\n parentElement.appendChild(quickchartButtonContainerElement);\n }\n\n appendQuickchartButton(table);\n ", + "text/html": [ + "\n", + "
    \n", + "
    \n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
    ticket_idtimestampreporter_idlocationlocation_descriptioncategory_reporteddescriptionstatusdistrictaudio_refimage_refvideo_ref
    0311-20250409-52F19E2025-04-09 17:28:21+00:00Citizen-4A2EPOINT(-118.245 34.052)Large trash pile in the alley behind the build...Illegal Dumping/Trash PileThere is a huge pile of trash in the alley and...OpenNorth{'uri': 'gs://n25-311-demo/calls/311-20250409-...{'uri': 'gs://n25-311-demo/images/311-20250409...{'uri': 'gs://n25-311-demo/videos/311-20250409...
    1311-20250409-A57DB42025-04-09 17:29:21+00:00Citizen-77E1POINT(-118.33 34.09)Leaking fire hydrant on Highland Ave near the ...Leaking Fire HydrantA leaky fire hydrant that has been creating a ...OpenNorth{'uri': 'gs://n25-311-demo/calls/311-20250409-...{'uri': 'gs://n25-311-demo/images/311-20250409...{'uri': 'gs://n25-311-demo/videos/311-20250409...
    2311-20250409-59911E2025-04-09 17:28:21+00:00Citizen-69DDPOINT(-118.385 34.06)Car parked on the sidewalk on Beverly Hills Bl...Abandoned VehicleSomeone drove their car on the sidewalk and ab...OpenNorth{'uri': 'gs://n25-311-demo/calls/311-20250409-...{'uri': 'gs://n25-311-demo/images/311-20250409...{'uri': 'gs://n25-311-demo/videos/311-20250409...
    3311-20250409-1515EF2025-04-09 17:35:21+00:00Citizen-5DFCPOINT(-118.341 34.063)Large pothole on Sunset Blvd near the bus stop...PotholeThere is a small pothole near the bus stop tha...OpenSouth{'uri': 'gs://n25-311-demo/calls/311-20250409-...{'uri': 'gs://n25-311-demo/images/311-20250409...{'uri': 'gs://n25-311-demo/videos/311-20250409...
    4311-20250409-60290D2025-04-09 17:16:21+00:00Citizen-4B3BPOINT(-118.35 34.055)Large sinkhole on the sidewalk near apartment ...Major SinkholeA huge sinkhole that looks like it will eat a ...OpenSouth{'uri': 'gs://n25-311-demo/calls/311-20250409-...{'uri': 'gs://n25-311-demo/images/311-20250409...{'uri': 'gs://n25-311-demo/videos/311-20250409...
    \n", + "
    \n", + "
    \n", + "\n", + "
    \n", + " \n", + "\n", + " \n", + "\n", + " \n", + "
    \n", + "\n", + "\n", + "
    \n", + " \n", + "\n", + "\n", + "\n", + " \n", + "
    \n", + "\n", + "
    \n", + "
    \n" + ], + "text/plain": [ + " ticket_id timestamp reporter_id \\\n", + "0 311-20250409-52F19E 2025-04-09 17:28:21+00:00 Citizen-4A2E \n", + "1 311-20250409-A57DB4 2025-04-09 17:29:21+00:00 Citizen-77E1 \n", + "2 311-20250409-59911E 2025-04-09 17:28:21+00:00 Citizen-69DD \n", + "3 311-20250409-1515EF 2025-04-09 17:35:21+00:00 Citizen-5DFC \n", + "4 311-20250409-60290D 2025-04-09 17:16:21+00:00 Citizen-4B3B \n", + "\n", + " location location_description \\\n", + "0 POINT(-118.245 34.052) Large trash pile in the alley behind the build... \n", + "1 POINT(-118.33 34.09) Leaking fire hydrant on Highland Ave near the ... \n", + "2 POINT(-118.385 34.06) Car parked on the sidewalk on Beverly Hills Bl... \n", + "3 POINT(-118.341 34.063) Large pothole on Sunset Blvd near the bus stop... \n", + "4 POINT(-118.35 34.055) Large sinkhole on the sidewalk near apartment ... \n", + "\n", + " category_reported \\\n", + "0 Illegal Dumping/Trash Pile \n", + "1 Leaking Fire Hydrant \n", + "2 Abandoned Vehicle \n", + "3 Pothole \n", + "4 Major Sinkhole \n", + "\n", + " description status district \\\n", + "0 There is a huge pile of trash in the alley and... Open North \n", + "1 A leaky fire hydrant that has been creating a ... Open North \n", + "2 Someone drove their car on the sidewalk and ab... Open North \n", + "3 There is a small pothole near the bus stop tha... Open South \n", + "4 A huge sinkhole that looks like it will eat a ... Open South \n", + "\n", + " audio_ref \\\n", + "0 {'uri': 'gs://n25-311-demo/calls/311-20250409-... \n", + "1 {'uri': 'gs://n25-311-demo/calls/311-20250409-... \n", + "2 {'uri': 'gs://n25-311-demo/calls/311-20250409-... \n", + "3 {'uri': 'gs://n25-311-demo/calls/311-20250409-... \n", + "4 {'uri': 'gs://n25-311-demo/calls/311-20250409-... \n", + "\n", + " image_ref \\\n", + "0 {'uri': 'gs://n25-311-demo/images/311-20250409... \n", + "1 {'uri': 'gs://n25-311-demo/images/311-20250409... \n", + "2 {'uri': 'gs://n25-311-demo/images/311-20250409... \n", + "3 {'uri': 'gs://n25-311-demo/images/311-20250409... \n", + "4 {'uri': 'gs://n25-311-demo/images/311-20250409... \n", + "\n", + " video_ref \n", + "0 {'uri': 'gs://n25-311-demo/videos/311-20250409... \n", + "1 {'uri': 'gs://n25-311-demo/videos/311-20250409... \n", + "2 {'uri': 'gs://n25-311-demo/videos/311-20250409... \n", + "3 {'uri': 'gs://n25-311-demo/videos/311-20250409... \n", + "4 {'uri': 'gs://n25-311-demo/videos/311-20250409... " + ] + }, + "execution_count": 40, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "%%bigquery --project {PROJECT_ID}\n", + "\n", + "CREATE OR REPLACE TABLE `bq_mm_tutorial.reports_mm` AS\n", + "SELECT\n", + " r.*,\n", + " OBJ.FETCH_METADATA(OBJ.MAKE_REF(m.audio_uri, 'us.test_connection')) as audio_ref,\n", + " OBJ.FETCH_METADATA(OBJ.MAKE_REF(m.image_uri, 'us.test_connection')) as image_ref,\n", + " OBJ.FETCH_METADATA(OBJ.MAKE_REF(m.video_uri, 'us.test_connection')) as video_ref\n", + "FROM `bq_mm_tutorial.reports` r\n", + "LEFT JOIN `bq_mm_tutorial.media` m\n", + "ON r.ticket_id = m.ticket_id;\n", + "\n", + "SELECT * FROM `bq_mm_tutorial.reports_mm`;" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "xqzgf0fASE6U" + }, + "source": [ + "### 4. Run a multimodal query with on `ObjectRef`\n", + "\n", + "Our `reports_mm` table has column called `description`, which provides a text description of a reported incident.\n", + "\n", + "However, there may be additional, secondary issues that AI can help infer from provided media.\n", + "\n", + "In this example, we pass the text `category_reported` alongside a prompt and then append `image_ref`. In this way, the [`AI.GENERATE_TABLE`](https://cloud.google.com/bigquery/docs/reference/standard-sql/bigqueryml-syntax-generate-table) function passes text **and** an image in the same Gemini model call before returning the results.\n", + "\n", + "But for BigQuery's `AI.GENERATE_TABLE` to call Gemini to parse unstructured data, we first need to create a [Remote Model](https://cloud.google.com/bigquery/docs/reference/standard-sql/bigqueryml-syntax-create-remote-model). We'll create that and move to our multimodal query." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 154, + "referenced_widgets": [ + "efa7bed09a614513949c89bcc01764ff", + "40ae635e06e743e4bfbd852203da640d", + "d317ebcf563546209b6bdbb07247b50b", + "1606ec0a800549b7b0ac80bae1490ca7", + "54d5e3f9c2fd46059cbbdadc0fbb6397", + "f897fc1656854a5ab3a0a6dac941d3cf", + "63a16a5138104e3699980274d1a6a346", + "d09e04b34451415eb2083ac4a399efd0", + "068c95f20fb7448e8c2330227b24afed", + "18f60ba1083345b7a78518d77e7fe45d", + "80c473eebb5f40e3a66644d2c214055d" + ] + }, + "id": "g-1Lny9oeN9D", + "outputId": "06a8a033-c9af-47b9-f920-5d3aec1dccc0" + }, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "efa7bed09a614513949c89bcc01764ff", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Query is running: 0%| |" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/vnd.google.colaboratory.intrinsic+json": { + "summary": "{\n \"name\": \"get_ipython()\",\n \"rows\": 0,\n \"fields\": []\n}", + "type": "dataframe" + }, + "application/vnd.google.colaboratory.module+javascript": "\n import \"https://ssl.gstatic.com/colaboratory/data_table/9e65f7085e7ffcb7/data_table.js\";\n\n const table = window.createDataTable({\n data: [],\n columns: [[\"number\", \"index\"]],\n columnOptions: [{\"width\": \"1px\", \"className\": \"index_column\"}],\n rowsPerPage: 25,\n helpUrl: \"https://colab.research.google.com/notebooks/data_table.ipynb\",\n suppressOutputScrolling: true,\n minimumWidth: undefined,\n });\n\n function appendQuickchartButton(parentElement) {\n let quickchartButtonContainerElement = document.createElement('div');\n quickchartButtonContainerElement.innerHTML = `\n
    \n \n\n\n\n \n
    `;\n parentElement.appendChild(quickchartButtonContainerElement);\n }\n\n appendQuickchartButton(table);\n ", + "text/html": [ + "\n", + "
    \n", + "
    \n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
    \n", + "
    \n", + "
    \n", + "\n", + "
    \n", + " \n", + "\n", + " \n", + "\n", + " \n", + "
    \n", + "\n", + "\n", + "
    \n", + "
    \n" + ], + "text/plain": [ + "Empty DataFrame\n", + "Columns: []\n", + "Index: []" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "%%bigquery --project {PROJECT_ID}\n", + "\n", + "CREATE OR REPLACE MODEL `bq_mm_tutorial.gemini`\n", + "REMOTE WITH CONNECTION `us.test_connection`\n", + " OPTIONS(ENDPOINT = 'gemini-2.5-flash');" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 293, + "referenced_widgets": [ + "bc3ccd30484e495ca31411dad7ef0a58", + "668aa95aae7443c197c0b0a5b5eb1245", + "01cea4f298a9425190fae5ccfac104d5", + "d7794f774ff84cfd8b25cf362baeb4ac", + "f805e0d5073941a78d4e1f8ec210a485", + "b56b5e01b7d940c5b1395a0644c1caca", + "ee6eb5747c0c4fc9beca5271f371ac90", + "a6b8e054c09b4747a84a666327348783", + "7e9557b4b0b443149dbe2255e1b3c73c", + "28f99e97ed584ec685e16a5c43261f81", + "cd73d9917e6c4d83943cb03f7438aec4", + "93129eed185c45aeb80d6eabe320a585", + "470bc1aa7b44486b9e80f5aa762efdaa", + "eace7c28638e478ca9397676d9c24e04", + "06f81c65251f4ec2b66913ba4039cf41", + "e770eb58bfa144888fabcaf30738756c", + "b52d7c0f6b654c78a7757fe137b2b189", + "2f5c17ffe1d14b719f36672eb35806fc", + "20c3ce0e41fa44eaad482c6b7418a7ce", + "eaa9ea3a207e487b831fd7bb84e26178", + "e9d84d732ba74ba487668cf9c85accab", + "9fb87c004c494d7ba38462afc7db4f39" + ] + }, + "id": "wdayCH4ISWts", + "outputId": "e5906084-b5ec-4458-a2eb-cb3d6fdd1726" + }, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "bc3ccd30484e495ca31411dad7ef0a58", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Query is running: 0%| |" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "93129eed185c45aeb80d6eabe320a585", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Downloading: 0%| |" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/vnd.google.colaboratory.intrinsic+json": { + "summary": "{\n \"name\": \"get_ipython()\",\n \"rows\": 5,\n \"fields\": [\n {\n \"column\": \"ticket_id\",\n \"properties\": {\n \"dtype\": \"string\",\n \"num_unique_values\": 5,\n \"samples\": [\n \"311-20250409-60290D\",\n \"311-20250409-A57DB4\",\n \"311-20250409-52F19E\"\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"category_reported\",\n \"properties\": {\n \"dtype\": \"string\",\n \"num_unique_values\": 5,\n \"samples\": [\n \"Major Sinkhole\",\n \"Leaking Fire Hydrant\",\n \"Illegal Dumping/Trash Pile\"\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"secondary_category\",\n \"properties\": {\n \"dtype\": \"object\",\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"description\",\n \"properties\": {\n \"dtype\": \"string\",\n \"num_unique_values\": 5,\n \"samples\": [\n \"A huge sinkhole that looks like it will eat a car. FIX SOON\",\n \"A leaky fire hydrant that has been creating a puddle\",\n \"There is a huge pile of trash in the alley and I cannot walk through it. It gets bigger every day.\"\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n }\n ]\n}", + "type": "dataframe" + }, + "application/vnd.google.colaboratory.module+javascript": "\n import \"https://ssl.gstatic.com/colaboratory/data_table/9e65f7085e7ffcb7/data_table.js\";\n\n const table = window.createDataTable({\n data: [[{\n 'v': 0,\n 'f': \"0\",\n },\n\"311-20250409-59911E\",\n\"Abandoned Vehicle\",\n\"['Damaged Tree']\",\n\"Someone drove their car on the sidewalk and abandoned it. It needs to be towed. Its in the way of pedestrians!\"],\n [{\n 'v': 1,\n 'f': \"1\",\n },\n\"311-20250409-60290D\",\n\"Major Sinkhole\",\n\"['Widespread pavement cracks']\",\n\"A huge sinkhole that looks like it will eat a car. FIX SOON\"],\n [{\n 'v': 2,\n 'f': \"2\",\n },\n\"311-20250409-52F19E\",\n\"Illegal Dumping/Trash Pile\",\n\"['Graffiti']\",\n\"There is a huge pile of trash in the alley and I cannot walk through it. It gets bigger every day.\"],\n [{\n 'v': 3,\n 'f': \"3\",\n },\n\"311-20250409-1515EF\",\n\"Pothole\",\n\"['Cracked Road']\",\n\"There is a small pothole near the bus stop that should be filled. It is a little bit of a hazard.\"],\n [{\n 'v': 4,\n 'f': \"4\",\n },\n\"311-20250409-A57DB4\",\n\"Leaking Fire Hydrant\",\n\"['Slipping Hazard']\",\n\"A leaky fire hydrant that has been creating a puddle\"]],\n columns: [[\"number\", \"index\"], [\"string\", \"ticket_id\"], [\"string\", \"category_reported\"], [\"string\", \"secondary_category\"], [\"string\", \"description\"]],\n columnOptions: [{\"width\": \"1px\", \"className\": \"index_column\"}],\n rowsPerPage: 25,\n helpUrl: \"https://colab.research.google.com/notebooks/data_table.ipynb\",\n suppressOutputScrolling: true,\n minimumWidth: undefined,\n });\n\n function appendQuickchartButton(parentElement) {\n let quickchartButtonContainerElement = document.createElement('div');\n quickchartButtonContainerElement.innerHTML = `\n
    \n \n\n\n\n \n
    `;\n parentElement.appendChild(quickchartButtonContainerElement);\n }\n\n appendQuickchartButton(table);\n ", + "text/html": [ + "\n", + "
    \n", + "
    \n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
    ticket_idcategory_reportedsecondary_categorydescription
    0311-20250409-59911EAbandoned Vehicle[Damaged Tree]Someone drove their car on the sidewalk and ab...
    1311-20250409-60290DMajor Sinkhole[Widespread pavement cracks]A huge sinkhole that looks like it will eat a ...
    2311-20250409-52F19EIllegal Dumping/Trash Pile[Graffiti]There is a huge pile of trash in the alley and...
    3311-20250409-1515EFPothole[Cracked Road]There is a small pothole near the bus stop tha...
    4311-20250409-A57DB4Leaking Fire Hydrant[Slipping Hazard]A leaky fire hydrant that has been creating a ...
    \n", + "
    \n", + "
    \n", + "\n", + "
    \n", + " \n", + "\n", + " \n", + "\n", + " \n", + "
    \n", + "\n", + "\n", + "
    \n", + " \n", + "\n", + "\n", + "\n", + " \n", + "
    \n", + "\n", + "
    \n", + "
    \n" + ], + "text/plain": [ + " ticket_id category_reported \\\n", + "0 311-20250409-59911E Abandoned Vehicle \n", + "1 311-20250409-60290D Major Sinkhole \n", + "2 311-20250409-52F19E Illegal Dumping/Trash Pile \n", + "3 311-20250409-1515EF Pothole \n", + "4 311-20250409-A57DB4 Leaking Fire Hydrant \n", + "\n", + " secondary_category \\\n", + "0 [Damaged Tree] \n", + "1 [Widespread pavement cracks] \n", + "2 [Graffiti] \n", + "3 [Cracked Road] \n", + "4 [Slipping Hazard] \n", + "\n", + " description \n", + "0 Someone drove their car on the sidewalk and ab... \n", + "1 A huge sinkhole that looks like it will eat a ... \n", + "2 There is a huge pile of trash in the alley and... \n", + "3 There is a small pothole near the bus stop tha... \n", + "4 A leaky fire hydrant that has been creating a ... " + ] + }, + "execution_count": 41, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "%%bigquery --project {PROJECT_ID}\n", + "\n", + "SELECT\n", + " ticket_id,\n", + " category_reported,\n", + " secondary_category,\n", + " description\n", + "FROM AI.GENERATE_TABLE(\n", + " MODEL `bq_mm_tutorial.gemini`,\n", + " (\n", + " SELECT (\n", + " category_reported, ' is the primary issue.'\n", + " 'Note a secondary issue, only if you find something severe needing attention.'\n", + " 'Inspect the background and in the object.'\n", + " , image_ref\n", + " ) AS prompt,\n", + " ticket_id,\n", + " category_reported,\n", + " description\n", + " FROM `bq_mm_tutorial.reports_mm`\n", + " ),\n", + " STRUCT(\n", + " \"secondary_category ARRAY\" AS output_schema\n", + " )\n", + ");" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "BzbLQ7fuVAsz" + }, + "source": [ + "### 5. Run a multimodal query with multiple `ObjectRef`s\n", + "\n", + "We can pass **multiple** `ObjectRef` fields in the same call to a Gemini model. In this example, we pass:\n", + "* The text description a user reported\n", + "* An image (`image_ref`)\n", + "* Call audio (`audio_ref`)\n", + "* Video (`video_ref`)\n", + "\n", + "Providing all of this multimodal data in the same function call to the Gemini model allows it to perform a complete analysis that takes into account all four pieces of information at once. The output helps a 311 Operator prioritize which tickets are highest urgency and need immediate assistance." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 624, + "referenced_widgets": [ + "1ade78ecd6f649bfbc1aa07232920c8f", + "226564842aa34efe8e9e8fe7ddcddb30", + "d141ac9dc37843179c12eec324e9ab02", + "4a142df34b4b44a7a32ea954780fe6e4", + "50f7dbade42141c3a3db985b492d358f", + "c17c2fd2dd634300b492636fea3d0c22", + "689573d49df0476cb21e3bb748444189", + "0cd8da74b1c14958be15fe32ac68e1dc", + "be18dd05a2734d5e8b6b9f40ebe0d3ec", + "0e6f6b1cdcc44b6d99fc5ca5ce2815d1", + "76dda31c62e642eabe81d86e8d78e898", + "c0cfa49f540a498e91b196e83a9157d1", + "105b1f83272f4dbe98c16e97d4c252d7", + "1cdaa139b96c407280876aee31d240dc", + "0dad6963c40c4aa3b86c5083c9f8f3dd", + "c9f8549fc54640f3985f2c8d1f897b84", + "094e5a41b51544dd82d0c95cc324336e", + "bf0fa1e09e8b4f1d8eda6b1a6b0f1af4", + "aaf378ff4188447fb6bfeb34c7fc2e24", + "7e0e4c90626e4bec9d7cfff2c58d8641", + "90fc08c4e95d4ba688c893814c0f26f6", + "d9c37d7f1b714c4ca1153436d414c7e3" + ] + }, + "id": "A-PzHlJ1U1zo", + "outputId": "9d7b1a81-7684-4202-8323-3fa01542aed4" + }, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "1ade78ecd6f649bfbc1aa07232920c8f", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Query is running: 0%| |" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "c0cfa49f540a498e91b196e83a9157d1", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Downloading: 0%| |" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/vnd.google.colaboratory.intrinsic+json": { + "summary": "{\n \"name\": \"get_ipython()\",\n \"rows\": 5,\n \"fields\": [\n {\n \"column\": \"ticket_id\",\n \"properties\": {\n \"dtype\": \"string\",\n \"num_unique_values\": 5,\n \"samples\": [\n \"311-20250409-60290D\",\n \"311-20250409-A57DB4\",\n \"311-20250409-1515EF\"\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"issue\",\n \"properties\": {\n \"dtype\": \"string\",\n \"num_unique_values\": 5,\n \"samples\": [\n \"Massive sidewalk sinkhole\",\n \"Leaking fire hydrant\",\n \"Deep road pothole\"\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"secondary_issue\",\n \"properties\": {\n \"dtype\": \"string\",\n \"num_unique_values\": 4,\n \"samples\": [\n \"Extensive pavement cracking\",\n \"Abandoned vehicle\",\n \"Graffiti, fire hazard\"\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"original_description\",\n \"properties\": {\n \"dtype\": \"string\",\n \"num_unique_values\": 5,\n \"samples\": [\n \"A huge sinkhole that looks like it will eat a car. FIX SOON\",\n \"A leaky fire hydrant that has been creating a puddle\",\n \"There is a small pothole near the bus stop that should be filled. It is a little bit of a hazard.\"\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"ai_summary_description\",\n \"properties\": {\n \"dtype\": \"string\",\n \"num_unique_values\": 5,\n \"samples\": [\n \"A substantial and rapidly expanding sinkhole has formed on a paved urban sidewalk, characterized by a deep cavity with visible rubble and extensive radial cracking of the surrounding concrete. The accompanying audio confirms the location on La Brea Avenue near an apartment, emphasizes its growing size, and expresses concern about potential falls, highlighting an immediate safety hazard.\",\n \"A red fire hydrant located on Highland Avenue near a movie theater is actively leaking water onto the sidewalk, creating a growing puddle. The leak has reportedly been ongoing for several hours, causing significant water waste and potentially creating a slip hazard for pedestrians.\",\n \"A significant and deep pothole is present in the middle of a busy residential street, surrounded by radiating cracks and a longer central crack, posing a considerable traffic hazard as evidenced by passing vehicles swerving to avoid it. Its location near a bus stop further increases safety concerns for both drivers and pedestrians.\"\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"recommended_action\",\n \"properties\": {\n \"dtype\": \"string\",\n \"num_unique_values\": 5,\n \"samples\": [\n \"Dispatch emergency crews immediately to cordon off the area, assess the full extent of the damage, and initiate urgent repairs to the sidewalk sinkhole.\",\n \"Dispatch a crew to Highland Avenue to address a leaking fire hydrant that is creating a significant puddle.\",\n \"Dispatch a crew to immediately repair the large and hazardous pothole on Sunset Boulevard near the bus stop to prevent accidents and further road damage.\"\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"urgency_score\",\n \"properties\": {\n \"dtype\": \"Int64\",\n \"num_unique_values\": 3,\n \"samples\": [\n 10,\n 9,\n 7\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"city_response_department\",\n \"properties\": {\n \"dtype\": \"string\",\n \"num_unique_values\": 5,\n \"samples\": [\n \"Public Works\",\n \"Water Department\",\n \"Roads\"\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n }\n ]\n}", + "type": "dataframe" + }, + "application/vnd.google.colaboratory.module+javascript": "\n import \"https://ssl.gstatic.com/colaboratory/data_table/9e65f7085e7ffcb7/data_table.js\";\n\n const table = window.createDataTable({\n data: [[{\n 'v': 0,\n 'f': \"0\",\n },\n\"311-20250409-52F19E\",\n\"Alleyway blocked by trash\",\n\"Graffiti, fire hazard\",\n\"There is a huge pile of trash in the alley and I cannot walk through it. It gets bigger every day.\",\n\"The scene depicts an alleyway completely obstructed by an enormous accumulation of diverse refuse, including large furniture pieces, numerous black garbage bags, and various discarded items. This significant trash pile prevents passage and is reported to be growing, attracting pests, and emitting foul odors. Critically, during the observation, an active fire has ignited within the trash, posing an immediate danger and escalating the severity of the situation significantly. The alley walls also show signs of graffiti.\",\n\"Dispatch Sanitation and Fire Departments immediately to clear the massive trash pile and extinguish the active fire in the alley.\",\n{\n 'v': 10,\n 'f': \"10\",\n },\n\"Fire\"],\n [{\n 'v': 1,\n 'f': \"1\",\n },\n\"311-20250409-60290D\",\n\"Massive sidewalk sinkhole\",\n\"Extensive pavement cracking\",\n\"A huge sinkhole that looks like it will eat a car. FIX SOON\",\n\"A substantial and rapidly expanding sinkhole has formed on a paved urban sidewalk, characterized by a deep cavity with visible rubble and extensive radial cracking of the surrounding concrete. The accompanying audio confirms the location on La Brea Avenue near an apartment, emphasizes its growing size, and expresses concern about potential falls, highlighting an immediate safety hazard.\",\n\"Dispatch emergency crews immediately to cordon off the area, assess the full extent of the damage, and initiate urgent repairs to the sidewalk sinkhole.\",\n{\n 'v': 9,\n 'f': \"9\",\n },\n\"Public Works\"],\n [{\n 'v': 2,\n 'f': \"2\",\n },\n\"311-20250409-1515EF\",\n\"Deep road pothole\",\n\"Extensive road cracking\",\n\"There is a small pothole near the bus stop that should be filled. It is a little bit of a hazard.\",\n\"A significant and deep pothole is present in the middle of a busy residential street, surrounded by radiating cracks and a longer central crack, posing a considerable traffic hazard as evidenced by passing vehicles swerving to avoid it. Its location near a bus stop further increases safety concerns for both drivers and pedestrians.\",\n\"Dispatch a crew to immediately repair the large and hazardous pothole on Sunset Boulevard near the bus stop to prevent accidents and further road damage.\",\n{\n 'v': 9,\n 'f': \"9\",\n },\n\"Roads\"],\n [{\n 'v': 3,\n 'f': \"3\",\n },\n\"311-20250409-59911E\",\n\"Car blocking sidewalk\",\n\"Abandoned vehicle\",\n\"Someone drove their car on the sidewalk and abandoned it. It needs to be towed. Its in the way of pedestrians!\",\n\"A silver car is illegally parked on a bustling urban sidewalk next to an outdoor cafe, significantly obstructing pedestrian flow. The vehicle appears to be abandoned, as it has been stationary for an extended period, creating a nuisance and potential safety hazard in a high-traffic area.\",\n\"Dispatch a tow truck to remove the illegally parked and abandoned vehicle from the sidewalk.\",\n{\n 'v': 7,\n 'f': \"7\",\n },\n\"Police\"],\n [{\n 'v': 4,\n 'f': \"4\",\n },\n\"311-20250409-A57DB4\",\n\"Leaking fire hydrant\",\nnull,\n\"A leaky fire hydrant that has been creating a puddle\",\n\"A red fire hydrant located on Highland Avenue near a movie theater is actively leaking water onto the sidewalk, creating a growing puddle. The leak has reportedly been ongoing for several hours, causing significant water waste and potentially creating a slip hazard for pedestrians.\",\n\"Dispatch a crew to Highland Avenue to address a leaking fire hydrant that is creating a significant puddle.\",\n{\n 'v': 7,\n 'f': \"7\",\n },\n\"Water Department\"]],\n columns: [[\"number\", \"index\"], [\"string\", \"ticket_id\"], [\"string\", \"issue\"], [\"string\", \"secondary_issue\"], [\"string\", \"original_description\"], [\"string\", \"ai_summary_description\"], [\"string\", \"recommended_action\"], [\"number\", \"urgency_score\"], [\"string\", \"city_response_department\"]],\n columnOptions: [{\"width\": \"1px\", \"className\": \"index_column\"}],\n rowsPerPage: 25,\n helpUrl: \"https://colab.research.google.com/notebooks/data_table.ipynb\",\n suppressOutputScrolling: true,\n minimumWidth: undefined,\n });\n\n function appendQuickchartButton(parentElement) {\n let quickchartButtonContainerElement = document.createElement('div');\n quickchartButtonContainerElement.innerHTML = `\n
    \n \n\n\n\n \n
    `;\n parentElement.appendChild(quickchartButtonContainerElement);\n }\n\n appendQuickchartButton(table);\n ", + "text/html": [ + "\n", + "
    \n", + "
    \n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
    ticket_idissuesecondary_issueoriginal_descriptionai_summary_descriptionrecommended_actionurgency_scorecity_response_department
    0311-20250409-52F19EAlleyway blocked by trashGraffiti, fire hazardThere is a huge pile of trash in the alley and...The scene depicts an alleyway completely obstr...Dispatch Sanitation and Fire Departments immed...10Fire
    1311-20250409-60290DMassive sidewalk sinkholeExtensive pavement crackingA huge sinkhole that looks like it will eat a ...A substantial and rapidly expanding sinkhole h...Dispatch emergency crews immediately to cordon...9Public Works
    2311-20250409-1515EFDeep road potholeExtensive road crackingThere is a small pothole near the bus stop tha...A significant and deep pothole is present in t...Dispatch a crew to immediately repair the larg...9Roads
    3311-20250409-59911ECar blocking sidewalkAbandoned vehicleSomeone drove their car on the sidewalk and ab...A silver car is illegally parked on a bustling...Dispatch a tow truck to remove the illegally p...7Police
    4311-20250409-A57DB4Leaking fire hydrantNoneA leaky fire hydrant that has been creating a ...A red fire hydrant located on Highland Avenue ...Dispatch a crew to Highland Avenue to address ...7Water Department
    \n", + "
    \n", + "
    \n", + "\n", + "
    \n", + " \n", + "\n", + " \n", + "\n", + " \n", + "
    \n", + "\n", + "\n", + "
    \n", + " \n", + "\n", + "\n", + "\n", + " \n", + "
    \n", + "\n", + "
    \n", + "
    \n" + ], + "text/plain": [ + " ticket_id issue \\\n", + "0 311-20250409-52F19E Alleyway blocked by trash \n", + "1 311-20250409-60290D Massive sidewalk sinkhole \n", + "2 311-20250409-1515EF Deep road pothole \n", + "3 311-20250409-59911E Car blocking sidewalk \n", + "4 311-20250409-A57DB4 Leaking fire hydrant \n", + "\n", + " secondary_issue \\\n", + "0 Graffiti, fire hazard \n", + "1 Extensive pavement cracking \n", + "2 Extensive road cracking \n", + "3 Abandoned vehicle \n", + "4 None \n", + "\n", + " original_description \\\n", + "0 There is a huge pile of trash in the alley and... \n", + "1 A huge sinkhole that looks like it will eat a ... \n", + "2 There is a small pothole near the bus stop tha... \n", + "3 Someone drove their car on the sidewalk and ab... \n", + "4 A leaky fire hydrant that has been creating a ... \n", + "\n", + " ai_summary_description \\\n", + "0 The scene depicts an alleyway completely obstr... \n", + "1 A substantial and rapidly expanding sinkhole h... \n", + "2 A significant and deep pothole is present in t... \n", + "3 A silver car is illegally parked on a bustling... \n", + "4 A red fire hydrant located on Highland Avenue ... \n", + "\n", + " recommended_action urgency_score \\\n", + "0 Dispatch Sanitation and Fire Departments immed... 10 \n", + "1 Dispatch emergency crews immediately to cordon... 9 \n", + "2 Dispatch a crew to immediately repair the larg... 9 \n", + "3 Dispatch a tow truck to remove the illegally p... 7 \n", + "4 Dispatch a crew to Highland Avenue to address ... 7 \n", + "\n", + " city_response_department \n", + "0 Fire \n", + "1 Public Works \n", + "2 Roads \n", + "3 Police \n", + "4 Water Department " + ] + }, + "execution_count": 42, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "%%bigquery --project {PROJECT_ID}\n", + "\n", + "SELECT\n", + " ticket_id,\n", + " issue,\n", + " secondary_issue,\n", + " description AS original_description,\n", + " ai_summary_description,\n", + " recommended_action,\n", + " urgency_score,\n", + " city_response_department\n", + "FROM AI.GENERATE_TABLE(\n", + " MODEL `bq_mm_tutorial.gemini`,\n", + " (\n", + " SELECT (\n", + " 'Describe the primary issue in the photo in 2-3 words. Be descriptive.'\n", + " 'Only if you notice a secondary issue, list it too. Inspect the background and everywhere.'\n", + " 'Rate the urgency score for the city to respond from 1-10 where 1 is low, 10 is absolutely critical to safety.'\n", + " 'Write an AI generated description of the issue taking into account the text, image, audio, and video.'\n", + " 'Write a 1 sentence description to city dispatch for a recommended action.'\n", + " 'Assign a single city response department (e.g. Roads, Sanitation, Parks, Fire)'\n", + " , description, image_ref, audio_ref, video_ref\n", + " ) AS prompt,\n", + " ticket_id,\n", + " description\n", + " FROM `bq_mm_tutorial.reports_mm`\n", + " ),\n", + " STRUCT(\n", + " \"issue STRING, secondary_issue STRING, urgency_score INT64, ai_summary_description STRING, recommended_action STRING, city_response_department STRING\" AS output_schema\n", + " )\n", + ")\n", + "ORDER BY urgency_score DESC;" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "WL-v0ztHV59I" + }, + "source": [ + "### 6. Run a multimodal query on an ARRAY of `ObjectRef`s\n", + "\n", + "Of course, we can also aggregate `STRING` and `ObjectRef` fields as an array and pass them to a Gemini model too.\n", + "\n", + "In this example, we'll return the top issues that need attention in the North and South districts of the city. This maybe useful for citywide reporting." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 245, + "referenced_widgets": [ + "3fe1652ecc8647e791bb92b359083c7b", + "2333507379c94604b0085c82d1dee027", + "c656c3e7e3674cfcbd6da8aedd6ab8ea", + "9f5f022f32354990a37d8792e9e5835c", + "33c78c57e7af4206a719a36fc40dcd51", + "5255edd87c294e76833fcf92b435fe14", + "e1e41d0228a44c1f863c148b1767d884", + "16c0578e35ca41598b32ce04cb41ec7e", + "606a489da1fc463583afe11c0507ef23", + "56d9703bee29498992ae0ab38971cef5", + "b55dd25b8cad49b999700f012dbeb204", + "4c6e8534504d45dcb9725ebd4930030b", + "498b73aebe394b6082564b8d7142ffad", + "d4b3eb88a9b6401ab0bf3715dc8dda0f", + "2d91ea4991f243af8f3df17b68067bcd", + "bdb7e257379c4d589775f8d70b8e3256", + "11c3c34bc07940f6bb3072a79cc9631f", + "a20b904217474868bdbadd00fd44c608", + "ad8c27470b924b47bda49ce9a7108bbe", + "6fe605d131a14af3b688dd830dd27cfe", + "758c1d812bca41b186c2e78cb808ee75", + "f765af49ffa14f98a3c23da874ac1994" + ] + }, + "id": "jotBZrmyV7TD", + "outputId": "5216d7f4-2871-47d0-c789-cfbd4e285a85" + }, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "3fe1652ecc8647e791bb92b359083c7b", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Query is running: 0%| |" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "4c6e8534504d45dcb9725ebd4930030b", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Downloading: 0%| |" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/vnd.google.colaboratory.intrinsic+json": { + "summary": "{\n \"name\": \"get_ipython()\",\n \"rows\": 2,\n \"fields\": [\n {\n \"column\": \"district\",\n \"properties\": {\n \"dtype\": \"string\",\n \"num_unique_values\": 2,\n \"samples\": [\n \"North\",\n \"South\"\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"summary\",\n \"properties\": {\n \"dtype\": \"object\",\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n }\n ]\n}", + "type": "dataframe" + }, + "application/vnd.google.colaboratory.module+javascript": "\n import \"https://ssl.gstatic.com/colaboratory/data_table/9e65f7085e7ffcb7/data_table.js\";\n\n const table = window.createDataTable({\n data: [[{\n 'v': 0,\n 'f': \"0\",\n },\n\"South\",\n\"['A small pothole near the bus stop needs to be filled as it poses a hazard.'\\n 'A huge sinkhole on the road, described as potentially car-eating, needs immediate repair.'\\n 'A really big sinkhole is growing on the sidewalk near an apartment, raising concerns about people falling in.']\"],\n [{\n 'v': 1,\n 'f': \"1\",\n },\n\"North\",\n\"['Massive pile of trash in the alley causing obstruction and attracting pests'\\n 'Leaky fire hydrant causing a puddle'\\n 'Abandoned car blocking the sidewalk and pedestrian access']\"]],\n columns: [[\"number\", \"index\"], [\"string\", \"district\"], [\"string\", \"summary\"]],\n columnOptions: [{\"width\": \"1px\", \"className\": \"index_column\"}],\n rowsPerPage: 25,\n helpUrl: \"https://colab.research.google.com/notebooks/data_table.ipynb\",\n suppressOutputScrolling: true,\n minimumWidth: undefined,\n });\n\n function appendQuickchartButton(parentElement) {\n let quickchartButtonContainerElement = document.createElement('div');\n quickchartButtonContainerElement.innerHTML = `\n
    \n \n\n\n\n \n
    `;\n parentElement.appendChild(quickchartButtonContainerElement);\n }\n\n appendQuickchartButton(table);\n ", + "text/html": [ + "\n", + "
    \n", + "
    \n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
    districtsummary
    0South[A small pothole near the bus stop needs to be...
    1North[Massive pile of trash in the alley causing ob...
    \n", + "
    \n", + "
    \n", + "\n", + "
    \n", + " \n", + "\n", + " \n", + "\n", + " \n", + "
    \n", + "\n", + "\n", + "
    \n", + " \n", + "\n", + "\n", + "\n", + " \n", + "
    \n", + "\n", + "
    \n", + "
    \n" + ], + "text/plain": [ + " district summary\n", + "0 South [A small pothole near the bus stop needs to be...\n", + "1 North [Massive pile of trash in the alley causing ob..." + ] + }, + "execution_count": 43, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "%%bigquery --project {PROJECT_ID}\n", + "\n", + "SELECT\n", + " district,\n", + " summary\n", + "FROM AI.GENERATE_TABLE(\n", + " MODEL `bq_mm_tutorial.gemini`,\n", + " (\n", + " SELECT (\n", + " 'Describe the primary issues that need to be fixed in this district'\n", + " , ARRAY_AGG(description), ARRAY_AGG(image_ref), ARRAY_AGG(video_ref), ARRAY_AGG(audio_ref)\n", + " ) AS prompt,\n", + " district\n", + " FROM `bq_mm_tutorial.reports_mm`\n", + " GROUP BY district\n", + " ),\n", + " STRUCT(\n", + " \"summary ARRAY\" AS output_schema\n", + " )\n", + ");" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "izKiUVyzbsvA" + }, + "source": [ + "### Recap\n", + "\n", + "In this scenario we:\n", + "* Began with two tables, `reports` and `media` containing structured data and `uri` columns pointing to GCS objects\n", + "* Explored how to programmatically create an `ObjectRef` using `OBJ.` functions.\n", + "* Created a multimodal table called `reports_mm`\n", + "* Ran several queries against the `reports_mm` table using structured data and one *or more* `ObjectRef` columns\n", + "* Ran a multimodal query against an ARRAY of `ObjectRef`s to produce aggregated insights" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "q_A4zzWousGx" + }, + "source": [ + "\n", + "---\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "cleanup_md" + }, + "source": [ + "# Cleaning Up\n", + "\n", + "To clean up all Google Cloud resources used in this project, you can [delete the Google Cloud project](https://cloud.google.com/resource-manager/docs/creating-managing-projects#shutting_down_projects) you used for the tutorial.\n", + "\n", + "Otherwise, you can delete the individual resources you created in this tutorial:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "yRzpOQf_Wqqi" + }, + "outputs": [], + "source": [ + "# Delete the BigQuery tables\n", + "! bq rm --table -f bq_mm_tutorial.object_table\n", + "! bq rm --table -f bq_mm_tutorial.calls\n", + "! bq rm --table -f bq_mm_tutorial.calls_combined\n", + "! bq rm --table -f bq_mm_tutorial.media\n", + "! bq rm --table -f bq_mm_tutorial.reports\n", + "! bq rm --table -f bq_mm_tutorial.reports_mm\n", + "\n", + "# Delete the remote model\n", + "! bq rm --model -f bq_mm_tutorial.gemini\n", + "\n", + "# Delete the remote connection\n", + "! bq rm --connection --project_id=$PROJECT_ID --location=us test_connection\n", + "\n", + "# Delete the BigQuery dataset\n", + "! bq rm -r -f $PROJECT_ID:bq_mm_tutorial" + ] + } + ], + "metadata": { + "colab": { + "collapsed_sections": [ + "td9kx9LVgSve" + ], + "provenance": [], + "toc_visible": true + }, + "kernelspec": { + "display_name": "Python 3", + "name": "python3" + }, + "language_info": { + "name": "python" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} diff --git a/gemini/use-cases/applying-llms-to-data/semantic-search-in-bigquery/stackoverflow_questions_semantic_search.ipynb b/gemini/use-cases/applying-llms-to-data/semantic-search-in-bigquery/stackoverflow_questions_semantic_search.ipynb new file mode 100644 index 0000000..c3208cc --- /dev/null +++ b/gemini/use-cases/applying-llms-to-data/semantic-search-in-bigquery/stackoverflow_questions_semantic_search.ipynb @@ -0,0 +1,547 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "gNz_7idNEdlE" + }, + "outputs": [], + "source": [ + "# Copyright 2024 Google LLC\n", + "#\n", + "# Licensed under the Apache License, Version 2.0 (the \"License\");\n", + "# you may not use this file except in compliance with the License.\n", + "# You may obtain a copy of the License at\n", + "#\n", + "# https://www.apache.org/licenses/LICENSE-2.0\n", + "#\n", + "# Unless required by applicable law or agreed to in writing, software\n", + "# distributed under the License is distributed on an \"AS IS\" BASIS,\n", + "# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n", + "# See the License for the specific language governing permissions and\n", + "# limitations under the License." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "IXbw-R0ZGiWf" + }, + "source": [ + "# Performing Semantic Search in BigQuery\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
    \n", + " \n", + " \"Google
    Open in Colab\n", + "
    \n", + "
    \n", + " \n", + " \"Google
    Open in Colab Enterprise\n", + "
    \n", + "
    \n", + " \n", + " \"Vertex
    Open in Vertex AI Workbench\n", + "
    \n", + "
    \n", + " \n", + " \"BigQuery
    Open in BigQuery Studio\n", + "
    \n", + "
    \n", + " \n", + " \"GitHub
    View on GitHub\n", + "
    \n", + "
    \n", + "\n", + "
    \n", + "\n", + "Share to:\n", + "\n", + "\n", + " \"LinkedIn\n", + "\n", + "\n", + "\n", + " \"Bluesky\n", + "\n", + "\n", + "\n", + " \"X\n", + "\n", + "\n", + "\n", + " \"Reddit\n", + "\n", + "\n", + "\n", + " \"Facebook\n", + " " + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "4qTZ1THsriwh" + }, + "source": [ + "| | |\n", + "|-|-|\n", + "|Author(s) | [Jaideep Sethi](https://github.com/sethijaideep) |" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "_cGtn8TvG7SB" + }, + "source": [ + "## Overview\n", + "\n", + "The objective is to demonstrate how to perform semantic search in BigQuery using Vector Search, including:\n", + "\n", + "\n", + "* Completing setup steps for accessing Vertex AI from BigQuery\n", + "* Creating a remote model in BigQuery\n", + "* Generating text embedding using the remote model\n", + "* Creating a vector index to optimize the semantic search\n", + "* Performing semantic search using `VECTOR_SEARCH` function in BigQuery\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "sf3ioTDBzuHR" + }, + "source": [ + "## About the dataset\n", + "\n", + "We are going to use Stack Overflow public dataset available in BigQuery. The data is an archive of Stack Overflow posts, votes, tags and badges.\n", + "\n", + "The dataset can be accessed [here](https://console.cloud.google.com/bigquery(cameo:product/stack-exchange/stack-overflow))." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "E8sI7ir1fVgI" + }, + "source": [ + "## Services and Costs\n", + "\n", + "This tutorial uses the following Google Cloud data analytics and ML services, they are billable components of Google Cloud:\n", + "\n", + "* BigQuery & BigQuery ML [(pricing)](https://cloud.google.com/bigquery/pricing)\n", + "* Vertex AI API [(pricing)](https://cloud.google.com/vertex-ai/pricing)\n", + "\n", + "Use the [Pricing Calculator](https://cloud.google.com/products/calculator/) to generate a cost estimate based on your projected usage.\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "P10XEmXlzeOF" + }, + "source": [ + "# Setup steps for accessing Vertex AI models from BigQuery" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "6xxnNycUFs8Z" + }, + "source": [ + "## Enable the Vertex AI and BigQuery Connection APIs" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "52cf2e9fc5ec" + }, + "outputs": [], + "source": [ + "!gcloud services enable aiplatform.googleapis.com bigqueryconnection.googleapis.com" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "DyOBjt1yFuKA" + }, + "source": [ + "## Create a Cloud resource connection\n", + "You can learn more about Cloud resource connection [here](https://cloud.google.com/bigquery/docs/create-cloud-resource-connection)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "65046bc876c6" + }, + "outputs": [], + "source": [ + "!bq mk --connection --location=us \\\n", + " --connection_type=CLOUD_RESOURCE vertex_conn" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "oo3okAPNF7QD" + }, + "source": [ + "## Grant the \"Vertex AI User\" role to the service account used by the Cloud resource connection\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "d443f951c280" + }, + "outputs": [], + "source": [ + "SERVICE_ACCT = !bq show --format=prettyjson --connection us.vertex_conn | grep \"serviceAccountId\" | cut -d '\"' -f 4\n", + "SERVICE_ACCT_EMAIL = SERVICE_ACCT[-1]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "a7d37cb9eb65" + }, + "outputs": [], + "source": [ + "import os\n", + "\n", + "PROJECT_ID = os.environ[\"GOOGLE_CLOUD_PROJECT\"]\n", + "!gcloud projects add-iam-policy-binding --format=none $PROJECT_ID --member=serviceAccount:$SERVICE_ACCT_EMAIL --role=roles/aiplatform.user" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "xCbjG3fb3def" + }, + "source": [ + "# Create the remote model in BigQuery ML" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "fm840uHo4kHP" + }, + "source": [ + "## Create a new dataset named `'bigquery_demo'`" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "fdSiqoR04jeQ" + }, + "outputs": [], + "source": [ + "%%bigquery\n", + "CREATE SCHEMA\n", + " `bigquery_demo` OPTIONS (location = 'US');" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "diwD_Bv0F_Sa" + }, + "source": [ + "## Create the remote model for Text Embedding in BigQuery ML\n", + "Text embeddings model converts textual data into numerical vectors.These vector representations are designed to capture the semantic meaning and context of the words they represent.To generate embeddings we are using `text-embedding-005` model, which is one of the text embedding models available on Vertex AI platform.\n", + "\n", + "You can learn more about Embeddings APIs [here](https://cloud.google.com/vertex-ai/generative-ai/docs/embeddings)\n", + "\n", + "Note: If you encounter a permission error while accessing or using the endpoint for the service account, please wait a minute and try again." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "KxAuo3YyGHp2" + }, + "outputs": [], + "source": [ + "%%bigquery\n", + "CREATE OR REPLACE MODEL `bigquery_demo.text_embedding_005`\n", + "REMOTE WITH CONNECTION `us.vertex_conn`\n", + "OPTIONS (endpoint = 'text-embedding-005')" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "JcYVG4qNKAuY" + }, + "source": [ + "# Prepare the dataset for semantic search\n", + "Semantic search is a technology that interprets the meaning of words and phrases." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "KCZ-nme8GjTN" + }, + "source": [ + "## Generate text embeddings for title and body associated with Stack Overflow questions\n", + "\n", + "For our use case we are going to use `title` and `body` fields from the Stack Overflow `posts_questions` table to generate text embeddings and perform semantic search using the `VECTOR_SEARCH` function.\n", + "\n", + "Note: To limit costs for this demo, we'll use the top 10,000 iOS-related posts." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "1VPgFMlBGi_5" + }, + "outputs": [], + "source": [ + "%%bigquery\n", + "CREATE OR REPLACE TABLE\n", + " `bigquery_demo.posts_questions_embedding` AS\n", + "SELECT\n", + " *\n", + "FROM\n", + " ML.GENERATE_EMBEDDING( MODEL `bigquery_demo.text_embedding_005`,\n", + " (\n", + " SELECT\n", + " id,\n", + " title,\n", + " body,\n", + " CONCAT (title, body ) AS CONTENT\n", + " FROM\n", + " `bigquery-public-data.stackoverflow.posts_questions`\n", + " WHERE\n", + " tags LIKE '%ios%'\n", + " ORDER BY\n", + " view_Count DESC\n", + " LIMIT\n", + " 10000 ),\n", + " STRUCT ( TRUE AS flatten_json_output,\n", + " 'SEMANTIC_SIMILARITY' AS task_type ) );" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "f324515ea4d6" + }, + "source": [ + "Let's now check the new table containing the embedding fields." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "8bcyGsNWHPeZ" + }, + "outputs": [], + "source": [ + "%%bigquery\n", + "SELECT * FROM `bigquery_demo.posts_questions_embedding` LIMIT 100;" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "ceUN_Mgm0vQa" + }, + "source": [ + "## Create Vector Index on the embeddings to help with efficient semantic search\n", + "A vector index is a data structure designed to let the `VECTOR_SEARCH` function perform a more efficient vector search of embeddings.You can learn more about vector index [here](https://cloud.google.com/bigquery/docs/vector-index)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "Gi1WMtnp1Tvh" + }, + "outputs": [], + "source": [ + "%%bigquery\n", + " CREATE OR REPLACE VECTOR INDEX ix_posts_questions\n", + " ON\n", + " `bigquery_demo.posts_questions_embedding` (ml_generate_embedding_result) OPTIONS(index_type = 'IVF',\n", + " distance_type = 'COSINE',\n", + " ivf_options = '{\"num_lists\":500}');" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "M4swkWSH08yE" + }, + "source": [ + "## Verify vector index creation" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "v-MJ0Gtui_BZ" + }, + "source": [ + "Note: The vector index is populated asynchronously.You can check whether the index is ready to be used by querying the `INFORMATION_SCHEMA.VECTOR_INDEXES` view and verifying that the `coverage_percentage` column value is greater than 0 and the `last_refresh_time` column value isn't `NULL`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "Znj6z0nstGBy" + }, + "outputs": [], + "source": [ + "%%bigquery\n", + "SELECT\n", + " table_name,\n", + " index_name,\n", + " index_status,\n", + " coverage_percentage,\n", + " last_refresh_time,\n", + " disable_reason\n", + "FROM\n", + " `bigquery_demo.INFORMATION_SCHEMA.VECTOR_INDEXES`;" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "Vj7gGTPy2qpQ" + }, + "source": [ + "# Perform semantic search\n", + "\n", + "Using text embeddings to perform similarity search on a new question" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "1SZ1OmVc2IN5" + }, + "source": [ + "## Match input question text to existing question's using vector search\n", + "Now let's perform a semantic search using the `VECTOR_SEARCH` function to find the top 5 closest results in our `posts_questions_embedding` table to a given question." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "N4pBoQPP2Y3k" + }, + "outputs": [], + "source": [ + "%%bigquery\n", + "SELECT\n", + " query.query as input_question,\n", + " base.id matching_question_id,\n", + " base.title as matching_question_title,\n", + " base.content as matching_question_content ,\n", + " distance,\n", + "FROM\n", + " VECTOR_SEARCH( TABLE `bigquery_demo.posts_questions_embedding`,\n", + " 'ml_generate_embedding_result',\n", + " (\n", + " SELECT\n", + " ml_generate_embedding_result,\n", + " content AS query\n", + " FROM\n", + " ML.GENERATE_EMBEDDING( MODEL `bigquery_demo.text_embedding_005`,\n", + " (\n", + " SELECT\n", + " 'Why does my iOS app crash with a low memory warning despite minimal memory usage?' AS content) ) ),\n", + " top_k => 5,\n", + " OPTIONS => '{\"fraction_lists_to_search\": 0.10}')\n", + "ORDER BY\n", + " distance ASC ;" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "835f45a9fdac" + }, + "source": [ + "Summary: The results demonstrate that `VECTOR_SEARCH` effectively identified the top 5 most similar questions.You can use this same approach to implement semantic search in BigQuery on any dataset." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "UB8VKTHJJlKx" + }, + "source": [ + "# Cleaning up\n", + "\n", + "To clean up all Google Cloud resources used in this project, you can [delete the Google Cloud project](https://cloud.google.com/resource-manager/docs/creating-managing-projects#shutting_down_projects) you used for the tutorial.\n", + "\n", + "Otherwise, you can delete the individual resources you created in this tutorial by uncommenting the below:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "nSF-ZhhPMjfs" + }, + "outputs": [], + "source": [ + "#\n", + "# !bq rm -r -f $PROJECT_ID:bigquery_demo\n", + "# !bq rm --connection --project_id=$PROJECT_ID --location=us vertex_conn\n", + "#" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "gFWBsvpbhGtE" + }, + "source": [ + "#Wrap up\n", + "\n", + "In this you have seen an example of how to integrate BQML with Vertex AI LLMs, how to generate embeddings with `ML.GENERATE_EMBEDDING` and perform semantic search using `VECTOR_SEARCH` in BigQuery.\n", + "\n", + "Check out our BigQuery ML documentation on [generating embeddings](https://cloud.google.com/bigquery/docs/generate-text-embedding) and [vector search](https://cloud.google.com/bigquery/docs/vector-search) to learn more about generative AI in BigQuery." + ] + } + ], + "metadata": { + "colab": { + "name": "stackoverflow_questions_semantic_search.ipynb", + "toc_visible": true + }, + "kernelspec": { + "display_name": "Python 3", + "name": "python3" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} diff --git a/gemini/use-cases/applying-llms-to-data/using-gemini-with-bigquery-remote-functions/README.md b/gemini/use-cases/applying-llms-to-data/using-gemini-with-bigquery-remote-functions/README.md new file mode 100644 index 0000000..f9964c3 --- /dev/null +++ b/gemini/use-cases/applying-llms-to-data/using-gemini-with-bigquery-remote-functions/README.md @@ -0,0 +1,203 @@ +# Analyzing images and text with Gemini using BigQuery & Remote Functions + +| | | +| --------- | --------------------------------------------- | +| Author(s) | [Shane Glass](https://github.com/shanecglass) | + +## Overview + +This repo provides an example of how to use [Gemini](https://blog.google/technology/ai/gemini-api-developers-cloud/), Google's largest and most capable AI model, to analyze your BigQuery data. BigQuery and [Remote Functions](https://cloud.google.com/bigquery/docs/remote-functions) can be used to analyze images and text input using the [Gemini API in Vertex AI](https://cloud.google.com/vertex-ai/docs/generative-ai/start/quickstarts/quickstart-multimodal) on Google Cloud. The following instructions should help you get started. + +## Calling Gemini using SQL + +This repo allows you to pass requests to Gemini using SQL and get the results back like you would for any other BigQuery query. This approach has several advantages: + +- It allows users who are familiar with SQL to leverage the power of Gemini without needing to write additional code +- You can more easily analyze a large batch of data rather than having to make individual requests for each image or text prompt +- You don't need to export your data from BigQuery before you can analyze it with Gemini + +## About this demo + +We've created a Terraform module that deploys all the necessary resources to call the [Gemini API in Vertex AI](https://cloud.google.com/vertex-ai/docs/generative-ai/start/quickstarts/quickstart-multimodal) using SQL in BigQuery. + +After the module is deployed, you will have access to two [BigQuery Remote Functions](https://cloud.google.com/bigquery/docs/remote-functions): + +- Analyze images and text (multimodal input) using `gemini_bq_demo_image`: This remote function takes an image from GCS as an input and prompts the Gemini model to create a brief description of the image +- Analyze text using `gemini_bq_demo_text`: This remote function takes text in a BigQuery table and uses the text directly as prompts for the Gemini model to return a response + +You will also have access to two BigQuery stored procedures which allow you to easy test each remote function using real image and text data: + +- `image_query_remote_function_sp`: This SQL query uses the `gemini_bq_demo_image` remote function to send a list of image URIs stored in an object table (deployed as part of the module) along with a prompt to Gemini +- `text_query_remote_function_sp`: This SQL query uses the `gemini_bq_demo_text` remote function to send a sample BigQuery table with pre-written text prompts (deployed as part of the module) to Gemini + +## Getting Started + +### Deploying the infrastructure + +**Note:** Though using a new Google Cloud project for this example is not a requirement, it might be easiest to use a new Google Cloud project for this. This makes cleanup much easier, as you can delete the whole project to ensure all assets are removed and it ensures no potential conflicts with existing resources. You can also remove resources by running `terraform destroy` after you deploy the resources, but it will also disable the associated APIs. + +#### 1. Clone this repo in Cloud Shell + +You'll need to set your Google Cloud project in Cloud Shell, clone this repo locally first, and set the working directory to this folder using the following commands. + +```shell +gcloud config set project +git clone https://github.com/GoogleCloudPlatform/generative-ai/ +cd ./generative-ai/gemini/use-cases/applying-llms-to-data/using-gemini-with-bigquery-remote-functions +``` + +#### 2. Enable the Cloud Resource Manager API + +Check to make sure the [Cloud Resource Manager API](https://console.cloud.google.com/apis/library/cloudresourcemanager.googleapis.com) is enabled + +#### 3. Initialize Terraform + +First, initialize Terraform by running + +```shell +terraform init +``` + +#### 4. Review resources + +Review the resources that are defined in the configuration: + +```shell +terraform plan +``` + +#### 5. Deploy the Terraform script + +```shell +terraform apply +``` + +When you're prompted to perform the actions, enter `yes`. Terraform will prompt you to provide your project ID and region. This sample has been tested using region `us-central1`. Terraform will display messages showing the progress of the deployment. + +After all the resources are created, Terraform displays the following message: + +```shell +Apply complete! +``` + +The Terraform output also lists the following additional information that you'll need: + +- The link to open the BigQuery editor to invoke the `image_query_remote_function_sp` stored procedure that analyzes the sample images provided +- The link to open the BigQuery editor to invoke the `text_query_remote_function_sp` stored procedure that analyzes the sample text prompts provided + +If you need to see your Terraform outputs again, simply enter `terraform output` into your command line. + +## Analyzing sample data with your new deployment + +Now that the resources have been deployed, your BigQuery Remote Functions are ready to be used in SQL queries. + +### 1. Analyze text and images (multimodal) + +Gemini will analyze the sample images when you invoke the `image_query_remote_function_sp` stored procedure. Simply click `Invoke stored procedure` for the stored procedure, then click `Run` for the resulting query to get the image descriptions generated by Gemini through your Remote Function. + +

    + A demo of how to analyze text prompts in the console +

    + +### 2. Analyze text only + +Gemini will analyze the prewritten text prompts when you invoke the `text_query_remote_function_sp` stored procedure. Simply click `Invoke stored procedure`, then click `Run` in the resulting query to get the responses generated by Gemini through your Remote Function. + +

    + A demo of how to analyze text prompts in the console +

    + +## How it works + +### Architecture Diagrams + +#### Image analysis + +

    + Architecture Diagram for analyzing images +

    + +
      +
    1. Sample images are uploaded to a Cloud Storage bucket and a GCS object table is created in BigQuery
    2. +
    3. Cloud Workflows creates a stored procedure in BigQuery that contains the sample query, which references the object table created in step 1 to pass images to the remote function for analysis using the Gemini API in Vertex AI
    4. +
    5. The stored procedure from step 2 is used to invoke the Cloud Function through a BigQuery connection
    6. +
    7. The Cloud Function analyzes the sample images by passing them to the Gemini API in Vertex AI (step 5) to get a brief description of the sample images and returns results from the Gemini API in Vertex AI as query results
    8. +
    + +#### Text analysis + +

    + Architecture Diagram for analyzing images +

    +
      +
    1. Cloud Workflows creates a stored procedure in BigQuery that contains the sample query and provisions the sample_text_prompts table, which contains sample text prompts to describe various landmarks. The sample query passes these prompts to the remote function for analysis using the Gemini API in Vertex AI
    2. +
    3. The stored procedure from step 2 is used to invoke the Cloud Function through a BigQuery connection
    4. +
    5. The Cloud Function analyzes the text input from the sample text by passing them to the Gemini API in Vertex AI (step 4) to get generate a text response to each prompt and returns results from the Gemini API in Vertex AI as query results
    6. +
    + +## Pricing Estimation + +Installing this demo (by running `terraform apply` in the Cloud Shell CLI) and running the stored procedures for text and image analysis 4 times each day will cost approximately $0.06 USD per month. See the [Google Cloud Pricing Calculator](https://cloud.google.com/products/calculator/#id=d3f64c61-9afb-4467-a6af-2bdd9540d489) for more details. The total monthly cost will vary as your usage varies, including how often you deploy and teardown this demo. + +**NOTE**: Charges for Multimodal model consumption in Vertex AI take effect on January 15, 2024. We estimate running this demo 4 times per day will increase the total cost by _$3.73 USD per month_. See the [Vertex AI pricing page for Generative AI](https://cloud.google.com/vertex-ai/pricing#generative_ai_models) for full details. Below is a breakdown of this estimate: + +- Text analysis + + - $0.02/month for Text Input + - $0.23/month for Text Output + +- Image analysis + - $3.30/month for Image Input + - $0.18/month for Text Output + +Keep in mind that generative AI models like Gemini are non-deterministic, so associated costs will vary based on the output length and cannot be definitively estimated. + +## Make it your own + +You can adapt this demo for your own use case! Check out the instructions below for image and text analysis. + +### Analyze images + +You can get started analyzing any images you have uploaded to Cloud Storage by: + +1. [Create a Cloud Storage object table](https://cloud.google.com/bigquery/docs/object-tables) + + If your bucket is stored in the same region that you deployed this demo, you should be able to reuse the existing BigQuery connection. If not, you will likely have to create a new one. + +1. Modify the `context` variable on [line 40](https://github.com/shanecglass/gemini-remote-function/blob/0892e365feeec8dac6c6c3b3d67038e7964c3e65/function/image/main.py#L40C1-L41C1) of the Cloud Function + + You can do this by editing the `gemini-bq-demo-image` that was deployed. Click the Edit button at the top of the Function Details page, then click Next to see the in-line editor. Change the value of the `context` variable to provide tell Gemini what it should do with your images, then click Deploy. + + Check out [this sample notebook](https://github.com/GoogleCloudPlatform/generative-ai/blob/main/gemini/use-cases/intro_multimodal_use_cases.ipynb) for inspiration and ideas of what you can ask Gemini to do with an image. + +1. Update the `image_query_remote_function_sp` stored procedure and run + + Update line 5 of the stored procedure to reference the object table you created in step 1 and run the query. + +### Analyze text + +You can get started by analyzing your own text inputs without having to modify the existing Cloud Function. Simply replace the `text_prompt` in line 3 of the `text_query_remote_function_sp` stored procedure with whatever text you want to analyze. You can enter a single string of text or you can reference a column of text prompts from a BigQuery table. If you are using a column from a BigQuery table, be sure to update the table reference on line 5 of the stored procedure. + +## Clean up + +After you are finished with the demo, you can delete all the resources you created with the following steps: + +1. Update the `variables.tf` file + + Change the default value for the `force_destroy` variable from `false` to `true`. Change the default value for the `deletion_protection` variable from `false` to `true`. + +1. Run `terraform apply` + + This applies the changes you made in step 1 to your resources so they can be easily deleted. + +1. Delete the BigQuery dataset + + Run the following command in your CLI to delete the BigQuery dataset created: + + ```shell + bq rm -r -f -d gemini_demo + ``` + +1. Run `terraform destroy` + + This deletes all the remaining resources you created. diff --git a/gemini/use-cases/applying-llms-to-data/using-gemini-with-bigquery-remote-functions/bigquery.tf b/gemini/use-cases/applying-llms-to-data/using-gemini-with-bigquery-remote-functions/bigquery.tf new file mode 100644 index 0000000..a113c24 --- /dev/null +++ b/gemini/use-cases/applying-llms-to-data/using-gemini-with-bigquery-remote-functions/bigquery.tf @@ -0,0 +1,160 @@ +/** + * Copyright 2023 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#Create dataset to host the GCS object table +resource "google_bigquery_dataset" "demo_dataset" { + project = module.project-services.project_id + dataset_id = "gemini_demo" + location = var.region + depends_on = [time_sleep.wait_after_apis] +} + +#Create BigQuery connection for Cloud Functions and GCS +resource "google_bigquery_connection" "function_connection" { + connection_id = var.connection_id + project = module.project-services.project_id + location = var.region + friendly_name = "Gemini connection" + description = "Connecting to the remote function that analyzes images using Gemini" + cloud_resource {} + depends_on = [time_sleep.wait_after_apis] +} + +locals { + bq_roles = [ + "roles/run.invoker", // Service account role to invoke the remote function + "roles/cloudfunctions.invoker", // Service account role to invoke the remote function + "roles/storage.objectViewer", // View GCS objects to create object tables + "roles/iam.serviceAccountUser" + ] +} + +#Grant the connection service account necessary permissions +resource "google_project_iam_member" "functions_invoke_roles" { + count = length(local.bq_roles) + project = module.project-services.project_id + role = local.bq_roles[count.index] + member = format("serviceAccount:%s", google_bigquery_connection.function_connection.cloud_resource[0].service_account_id) + + depends_on = [google_bigquery_connection.function_connection, google_project_iam_member.function_manage_roles] +} + +#Create GCS object table for your images. This will be the input table for the remote function +resource "google_bigquery_table" "object_table" { + project = module.project-services.project_id + dataset_id = google_bigquery_dataset.demo_dataset.dataset_id + table_id = "image_object_table" + deletion_protection = var.deletion_protection + + external_data_configuration { + autodetect = false + connection_id = google_bigquery_connection.function_connection.id + source_uris = ["${google_storage_bucket.demo_images.url}/*"] + object_metadata = "Simple" + } + + depends_on = [google_project_iam_member.functions_invoke_roles, google_storage_bucket.demo_images] +} + +# Create a series of stored procedures to connect to the remote function and call it +## Create the image remote function. This stored procedure will be called by the workflow +resource "google_bigquery_routine" "image_create_remote_function_sp" { + project = module.project-services.project_id + dataset_id = google_bigquery_dataset.demo_dataset.dataset_id + routine_id = "image_remote_function_sp" + routine_type = "PROCEDURE" + language = "SQL" + definition_body = templatefile("${path.module}/src/sql/image/provision_remote_function.sql", { + project_id = module.project-services.project_id, + dataset_id = google_bigquery_dataset.demo_dataset.dataset_id, + bq_function_name = var.image_function_name, + region = var.region, + bq_connection_id = var.connection_id, + remote_function_url = google_cloudfunctions2_function.image_remote_function.url + } + ) +} + +#Sample query to call the image remote function +resource "google_bigquery_routine" "image_query_remote_function_sp" { + project = module.project-services.project_id + dataset_id = google_bigquery_dataset.demo_dataset.dataset_id + routine_id = "image_query_remote_function_sp" + routine_type = "PROCEDURE" + language = "SQL" + definition_body = templatefile("${path.module}/src/sql/image/query_remote_function.sql", { + project_id = module.project-services.project_id, + dataset_id = google_bigquery_dataset.demo_dataset.dataset_id, + bq_function_name = var.image_function_name, + object_table_id = google_bigquery_table.object_table.table_id + } + ) + depends_on = [ + google_bigquery_routine.image_create_remote_function_sp + ] +} + +## Create the sample text input table. This stored procedure will be called by the workflow +resource "google_bigquery_routine" "provision_text_sample_table_sp" { + project = module.project-services.project_id + dataset_id = google_bigquery_dataset.demo_dataset.dataset_id + routine_id = "provision_text_sample_table_sp" + routine_type = "PROCEDURE" + language = "SQL" + definition_body = templatefile("${path.module}/src/sql/provision_text_sample_table.sql", { + project_id = module.project-services.project_id, + dataset_id = google_bigquery_dataset.demo_dataset.dataset_id + } + ) +} + +## Create the image remote function. This stored procedure will be called by the workflow +resource "google_bigquery_routine" "text_create_remote_function_sp" { + project = module.project-services.project_id + dataset_id = google_bigquery_dataset.demo_dataset.dataset_id + routine_id = "text_remote_function_sp" + routine_type = "PROCEDURE" + language = "SQL" + definition_body = templatefile("${path.module}/src/sql/text/provision_remote_function.sql", { + project_id = module.project-services.project_id, + dataset_id = google_bigquery_dataset.demo_dataset.dataset_id, + bq_function_name = var.text_function_name, + region = var.region, + bq_connection_id = var.connection_id, + remote_function_url = google_cloudfunctions2_function.text_remote_function.url + } + ) +} + +#Sample query to call the image remote function +resource "google_bigquery_routine" "text_query_remote_function_sp" { + project = module.project-services.project_id + dataset_id = google_bigquery_dataset.demo_dataset.dataset_id + routine_id = "text_query_remote_function_sp" + routine_type = "PROCEDURE" + language = "SQL" + definition_body = templatefile("${path.module}/src/sql/text/query_remote_function.sql", { + project_id = module.project-services.project_id, + dataset_id = google_bigquery_dataset.demo_dataset.dataset_id, + bq_function_name = var.text_function_name, + sample_prompt_table = "sample_text_prompts" + } + ) + depends_on = [ + google_bigquery_routine.text_create_remote_function_sp, + google_bigquery_routine.provision_text_sample_table_sp + ] +} diff --git a/gemini/use-cases/applying-llms-to-data/using-gemini-with-bigquery-remote-functions/function.tf b/gemini/use-cases/applying-llms-to-data/using-gemini-with-bigquery-remote-functions/function.tf new file mode 100644 index 0000000..b936e78 --- /dev/null +++ b/gemini/use-cases/applying-llms-to-data/using-gemini-with-bigquery-remote-functions/function.tf @@ -0,0 +1,131 @@ +/** + * Copyright 2023 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +#Create a service account to manage the remote function and its permissions +resource "google_service_account" "cloud_function_manage_sa" { + project = module.project-services.project_id + account_id = "gemini-demo" + display_name = "Cloud Functions Service Account" + + depends_on = [ + time_sleep.wait_after_apis, + ] +} + +locals { + cloud_function_roles = [ + "roles/cloudfunctions.admin", // Service account role to manage access to the remote function + "roles/run.invoker", // Service account role to invoke the remote function + "roles/storage.objectAdmin", // Read/write GCS files + "roles/bigquery.admin", // Create jobs and modify BigQuery tables + "roles/aiplatform.user", // Needs to predict from endpoints + "roles/aiplatform.serviceAgent", // Service account role + "roles/iam.serviceAccountUser" + ] +} + +resource "google_project_iam_member" "function_manage_roles" { + count = length(local.cloud_function_roles) + project = module.project-services.project_id + role = local.cloud_function_roles[count.index] + member = "serviceAccount:${google_service_account.cloud_function_manage_sa.email}" + + depends_on = [google_service_account.cloud_function_manage_sa] +} + +## Create a Cloud Function to serve as the remote function for image analysis +resource "google_cloudfunctions2_function" "image_remote_function" { + name = "gemini-bq-demo-image" + project = module.project-services.project_id + location = var.region + description = "A Cloud Function that uses the Gemini Generative Model to analyze and describe images." + + build_config { + runtime = "python311" + entry_point = "run_it" + + source { + storage_source { + bucket = google_storage_bucket.function_source.name + object = google_storage_bucket_object.image_source_upload.name + } + } + } + + service_config { + max_instance_count = 10 + # min_instance_count can be set to 1 to improve performance and responsiveness + min_instance_count = 0 + available_memory = "2Gi" + timeout_seconds = 300 + max_instance_request_concurrency = 1 + available_cpu = "2" + ingress_settings = "ALLOW_ALL" + all_traffic_on_latest_revision = true + service_account_email = google_service_account.cloud_function_manage_sa.email + environment_variables = { + "PROJECT_ID" : module.project-services.project_id, + "REGION" : var.region } + } + depends_on = [time_sleep.wait_after_apis] +} + +## Create a Cloud Function to serve as the remote function for image analysis +resource "google_cloudfunctions2_function" "text_remote_function" { + name = "gemini-bq-demo-text" + project = module.project-services.project_id + location = var.region + description = "A Cloud Function that uses the Gemini Generative Model to analyze and generate text." + + build_config { + runtime = "python311" + entry_point = "run_it" + + source { + storage_source { + bucket = google_storage_bucket.function_source.name + object = google_storage_bucket_object.text_source_upload.name + } + } + } + + service_config { + max_instance_count = 10 + # min_instance_count can be set to 1 to improve performance and responsiveness + min_instance_count = 0 + available_memory = "512Mi" + timeout_seconds = 300 + max_instance_request_concurrency = 1 + available_cpu = "2" + ingress_settings = "ALLOW_ALL" + all_traffic_on_latest_revision = true + service_account_email = google_service_account.cloud_function_manage_sa.email + environment_variables = { + "PROJECT_ID" : module.project-services.project_id, + "REGION" : var.region } + } + depends_on = [time_sleep.wait_after_apis] +} + +# Wait until after the function is created to deconflict resource creation +resource "time_sleep" "wait_after_functions" { + create_duration = "10s" + depends_on = [ + google_cloudfunctions2_function.image_remote_function, + google_cloudfunctions2_function.text_remote_function, + ] +} diff --git a/gemini/use-cases/applying-llms-to-data/using-gemini-with-bigquery-remote-functions/function/image/main.py b/gemini/use-cases/applying-llms-to-data/using-gemini-with-bigquery-remote-functions/function/image/main.py new file mode 100644 index 0000000..ccdfc03 --- /dev/null +++ b/gemini/use-cases/applying-llms-to-data/using-gemini-with-bigquery-remote-functions/function/image/main.py @@ -0,0 +1,63 @@ +# Copyright 2023 Google LLC + +# 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 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import json +import os + +import functions_framework +import vertexai +from vertexai.generative_models import GenerativeModel, Part + + +@functions_framework.http +def list_url(request) -> str | tuple[str, int]: + print(request) + try: + request_json = request.get_json() + calls = request_json["calls"] + for call in calls: + image_url = str(call[0]) + print(image_url) + return image_url + except Exception as e: + return json.dumps({"errorMessage": str(e)}), 400 + + +def analyze_image(image_file) -> str | None: + gemini_pro_vision_model = GenerativeModel("gemini-2.0-flash") + print(gemini_pro_vision_model) + image = Part.from_uri(image_file, mime_type="image/jpeg") + print(image) + context = """Describe and summarize this image. + Use no more than 5 sentences to do so""" + prompt = [context, image] + print(prompt) + response = gemini_pro_vision_model.generate_content(prompt, stream=False) + print(response) + output = " ".join(response.text.strip().split("\n")) + print(output) + return output + + +def run_it(request) -> str | tuple[str, int]: + try: + project_id = os.environ.get("PROJECT_ID") + region = os.environ.get("REGION") + vertexai.init(project=project_id, location=region) + file_to_analyze = list_url(request) + image_description = analyze_image(file_to_analyze) + result = image_description or "Unable to generate description" + return json.dumps({"replies": [result]}) + except Exception as e: + return json.dumps({"errorMessage": str(e)}), 400 diff --git a/gemini/use-cases/applying-llms-to-data/using-gemini-with-bigquery-remote-functions/function/image/requirements.txt b/gemini/use-cases/applying-llms-to-data/using-gemini-with-bigquery-remote-functions/function/image/requirements.txt new file mode 100644 index 0000000..1732265 --- /dev/null +++ b/gemini/use-cases/applying-llms-to-data/using-gemini-with-bigquery-remote-functions/function/image/requirements.txt @@ -0,0 +1,3 @@ +functions-framework +google-cloud-aiplatform +vertexai diff --git a/gemini/use-cases/applying-llms-to-data/using-gemini-with-bigquery-remote-functions/function/text/main.py b/gemini/use-cases/applying-llms-to-data/using-gemini-with-bigquery-remote-functions/function/text/main.py new file mode 100644 index 0000000..c0deb36 --- /dev/null +++ b/gemini/use-cases/applying-llms-to-data/using-gemini-with-bigquery-remote-functions/function/text/main.py @@ -0,0 +1,57 @@ +# Copyright 2023 Google LLC + +# 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 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import json +import os + +import functions_framework +import vertexai +from vertexai.generative_models import GenerativeModel + + +@functions_framework.http +def list_text_input(request) -> str | tuple[str, int]: + print(request) + try: + request_json = request.get_json() + calls = request_json["calls"] + for call in calls: + text_prompt = str(call[0]) + print(text_prompt) + return text_prompt + except Exception as e: + return json.dumps({"errorMessage": str(e)}), 400 + + +def generate_text_from_prompt(text_string) -> str | None: + # this is the text-to-text model + text_model = GenerativeModel("gemini-2.0-flash") + responses = text_model.generate_content(text_string, stream=False) + print(responses) + output = " ".join(responses.text.strip().split("\n")) + print(output) + return output + + +def run_it(request) -> str | tuple[str, int]: + try: + project_id = os.environ.get("PROJECT_ID") + region = os.environ.get("REGION") + vertexai.init(project=project_id, location=region) + text_to_analyze = list_text_input(request) + text_output = generate_text_from_prompt(text_to_analyze) + result = text_output or "Unable to generate description" + return json.dumps({"replies": [result]}) + except Exception as e: + return json.dumps({"errorMessage": str(e)}), 400 diff --git a/gemini/use-cases/applying-llms-to-data/using-gemini-with-bigquery-remote-functions/function/text/requirements.txt b/gemini/use-cases/applying-llms-to-data/using-gemini-with-bigquery-remote-functions/function/text/requirements.txt new file mode 100644 index 0000000..1732265 --- /dev/null +++ b/gemini/use-cases/applying-llms-to-data/using-gemini-with-bigquery-remote-functions/function/text/requirements.txt @@ -0,0 +1,3 @@ +functions-framework +google-cloud-aiplatform +vertexai diff --git a/gemini/use-cases/applying-llms-to-data/using-gemini-with-bigquery-remote-functions/main.tf b/gemini/use-cases/applying-llms-to-data/using-gemini-with-bigquery-remote-functions/main.tf new file mode 100644 index 0000000..ecea17d --- /dev/null +++ b/gemini/use-cases/applying-llms-to-data/using-gemini-with-bigquery-remote-functions/main.tf @@ -0,0 +1,97 @@ +/** + * Copyright 2023 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +module "project-services" { + source = "terraform-google-modules/project-factory/google//modules/project_services" + version = "16.0" + disable_services_on_destroy = false + + project_id = var.project_id + enable_apis = var.enable_apis + + activate_apis = [ + "aiplatform.googleapis.com", + "bigquery.googleapis.com", + "bigqueryconnection.googleapis.com", + "bigquerystorage.googleapis.com", + "cloudapis.googleapis.com", + "cloudfunctions.googleapis.com", + "config.googleapis.com", + "dataflow.googleapis.com", + "dataform.googleapis.com", + "logging.googleapis.com", + "notebooks.googleapis.com", + "run.googleapis.com", + "serviceusage.googleapis.com", + "storage.googleapis.com", + "storage-api.googleapis.com", + "workflows.googleapis.com", + ] + + activate_api_identities = [ + { + api = "workflows.googleapis.com" + roles = [ + "roles/workflows.viewer" + ] + api = "cloudfunctions.googleapis.com" + roles = [ + "roles/cloudfunctions.invoker" + ] + api = "run.googleapis.com" + roles = [ + "roles/run.invoker" + ] + } + ] +} + +# Create random ID to be used for deployment uniqueness +resource "random_id" "id" { + byte_length = 4 +} + +# Define/create zip file as a source for the image analysis Cloud Function +data "archive_file" "create_image_function_zip" { + type = "zip" + output_path = "${path.root}/tmp/image_function_source.zip" + source_dir = "${path.root}/function/image/" +} + +# Define/create zip file as a source for the image analysis Cloud Function +data "archive_file" "create_text_function_zip" { + type = "zip" + output_path = "${path.root}/tmp/text_function_source.zip" + source_dir = "${path.root}/function/text/" +} + +# Wait until after the APIs are activated to being setting up infrastructure +resource "time_sleep" "wait_after_apis" { + create_duration = "30s" + depends_on = [module.project-services] +} + +data "google_client_config" "current" { + depends_on = [time_sleep.wait_after_apis] +} + +# Wait until the Cloud Workflow has finished to complete setup +resource "time_sleep" "wait_after_workflow" { + create_duration = "30s" + depends_on = [ + data.http.call_workflows_setup + ] +} diff --git a/gemini/use-cases/applying-llms-to-data/using-gemini-with-bigquery-remote-functions/outputs.tf b/gemini/use-cases/applying-llms-to-data/using-gemini-with-bigquery-remote-functions/outputs.tf new file mode 100644 index 0000000..5afb9b0 --- /dev/null +++ b/gemini/use-cases/applying-llms-to-data/using-gemini-with-bigquery-remote-functions/outputs.tf @@ -0,0 +1,30 @@ +/** + * Copyright 2023 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +output "image_bucket" { + value = google_storage_bucket.demo_images.name + description = "Raw bucket name" +} + +output "bigquery_editor_image_sp" { + value = "https://console.cloud.google.com/bigquery?project=${module.project-services.project_id}&ws=!1m5!1m4!6m3!1s${module.project-services.project_id}!2s${google_bigquery_dataset.demo_dataset.dataset_id}!3simage_query_remote_function_sp" + description = "The URL to launch the BigQuery editor to invoke the image analysis procedure opened" +} + +output "bigquery_editor_text_sp" { + value = "https://console.cloud.google.com/bigquery?project=${module.project-services.project_id}&ws=!1m5!1m4!6m3!1s${module.project-services.project_id}!2s${google_bigquery_dataset.demo_dataset.dataset_id}!3stext_query_remote_function_sp" + description = "The URL to launch the BigQuery editor to invoke the image analysis procedure opened" +} diff --git a/gemini/use-cases/applying-llms-to-data/using-gemini-with-bigquery-remote-functions/src/sql/image/provision_remote_function.sql b/gemini/use-cases/applying-llms-to-data/using-gemini-with-bigquery-remote-functions/src/sql/image/provision_remote_function.sql new file mode 100644 index 0000000..0b5e6bd --- /dev/null +++ b/gemini/use-cases/applying-llms-to-data/using-gemini-with-bigquery-remote-functions/src/sql/image/provision_remote_function.sql @@ -0,0 +1,22 @@ +# Copyright 2023 Google LLC + +# 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 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +-- noqa: disable=PRS +CREATE OR REPLACE FUNCTION + `${project_id}.${dataset_id}.${bq_function_name}`(gcs_uri STRING) RETURNS STRING + REMOTE WITH CONNECTION `${project_id}.${region}.${bq_connection_id}` + OPTIONS ( + endpoint = '${remote_function_url}', + max_batching_rows = 1 + ); diff --git a/gemini/use-cases/applying-llms-to-data/using-gemini-with-bigquery-remote-functions/src/sql/image/query_remote_function.sql b/gemini/use-cases/applying-llms-to-data/using-gemini-with-bigquery-remote-functions/src/sql/image/query_remote_function.sql new file mode 100644 index 0000000..9e6e8f4 --- /dev/null +++ b/gemini/use-cases/applying-llms-to-data/using-gemini-with-bigquery-remote-functions/src/sql/image/query_remote_function.sql @@ -0,0 +1,20 @@ +# Copyright 2023 Google LLC + +# 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 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +SELECT + uri AS image_input, + `${project_id}.${dataset_id}.${bq_function_name}`(uri) AS image_description +FROM + `${project_id}.${dataset_id}.${object_table_id}` +; diff --git a/gemini/use-cases/applying-llms-to-data/using-gemini-with-bigquery-remote-functions/src/sql/provision_text_sample_table.sql b/gemini/use-cases/applying-llms-to-data/using-gemini-with-bigquery-remote-functions/src/sql/provision_text_sample_table.sql new file mode 100644 index 0000000..a9956e1 --- /dev/null +++ b/gemini/use-cases/applying-llms-to-data/using-gemini-with-bigquery-remote-functions/src/sql/provision_text_sample_table.sql @@ -0,0 +1,78 @@ +# Copyright 2023 Google LLC + +# 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 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +CREATE OR REPLACE TABLE +`${project_id}.${dataset_id}.sample_text_prompts` + +( + landmark_name STRING, + text_prompt STRING, + image_file STRING +) + +AS + +SELECT + "Wasserturm Radebeul" AS landmark_name, + """Describe the historic landmark + Wasserturm Radebeul in 5 sentences or less""" AS text_prompt, + "0015ff7be758af7f.jpg" AS image_file +UNION ALL +SELECT + "USS Texas" AS landmark_name, + """Describe the historic landmark + USS Texas in 5 sentences or less""" AS text_prompt, + "000952662701bc5d.jpg" AS image_file +UNION ALL +SELECT + "Erzsebet Bridge" AS landmark_name, + """Describe the historic landmark + Erzsébet Bridge in 5 sentences or less""" AS text_prompt, + "00454ad2434ee811.jpg" AS image_file +UNION ALL +SELECT + "Post Office, Meriden, Conn." AS landmark_name, + """Describe the historic landmark + Post Office, Meriden, Conn. in 5 sentences or less""" AS text_prompt, + "00041b7cefb3f517.jpg" AS image_file +UNION ALL +SELECT + "Saxtead Green Windmill" AS landmark_name, + """Describe the historic landmark + Saxtead Green Windmill in 5 sentences or less""" AS text_prompt, + "003ef0c91fa27a19.jpg" AS image_file +UNION ALL +SELECT + "Double-Span Metal Pratt Truss Bridge" AS landmark_name, + """Describe the historic landmark + Double-Span Metal Pratt Truss Bridge in 5 sentences or less""" AS text_prompt, + "0030d6b693209987.jpg" AS image_file +UNION ALL +SELECT + "Grand Canyon" AS landmark_name, + """Describe the historic landmark + Grand Canyon in 5 sentences or less""" AS text_prompt, + "004f71969864e68a.jpg" AS image_file +UNION ALL +SELECT + "Bamburgh Castle" AS landmark_name, + """Describe the historic landmark + Bamburgh Castle in 5 sentences or less""" AS text_prompt, + "004feb14c8606fd9.jpg" AS image_file +UNION ALL +SELECT + "Alcatraz Island" AS landmark_name, + """Describe the historic landmark + Alcatraz Island in 5 sentences or less""" AS text_prompt, + "000ded10aa81024f.jpg" AS image_file; diff --git a/gemini/use-cases/applying-llms-to-data/using-gemini-with-bigquery-remote-functions/src/sql/text/provision_remote_function.sql b/gemini/use-cases/applying-llms-to-data/using-gemini-with-bigquery-remote-functions/src/sql/text/provision_remote_function.sql new file mode 100644 index 0000000..75b7858 --- /dev/null +++ b/gemini/use-cases/applying-llms-to-data/using-gemini-with-bigquery-remote-functions/src/sql/text/provision_remote_function.sql @@ -0,0 +1,22 @@ +# Copyright 2023 Google LLC + +# 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 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +-- noqa: disable=PRS +CREATE OR REPLACE FUNCTION + `${project_id}.${dataset_id}.${bq_function_name}`(input_text STRING) RETURNS STRING + REMOTE WITH CONNECTION `${project_id}.${region}.${bq_connection_id}` + OPTIONS ( + endpoint = '${remote_function_url}', + max_batching_rows = 1 + ); diff --git a/gemini/use-cases/applying-llms-to-data/using-gemini-with-bigquery-remote-functions/src/sql/text/query_remote_function.sql b/gemini/use-cases/applying-llms-to-data/using-gemini-with-bigquery-remote-functions/src/sql/text/query_remote_function.sql new file mode 100644 index 0000000..c3188c5 --- /dev/null +++ b/gemini/use-cases/applying-llms-to-data/using-gemini-with-bigquery-remote-functions/src/sql/text/query_remote_function.sql @@ -0,0 +1,21 @@ +# Copyright 2023 Google LLC + +# 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 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +SELECT + text_prompt AS text_input, + `${project_id}.${dataset_id}.${bq_function_name}`(text_prompt) + AS landmark_description +FROM + `${project_id}.${dataset_id}.${sample_prompt_table}` +; diff --git a/gemini/use-cases/applying-llms-to-data/using-gemini-with-bigquery-remote-functions/storage.tf b/gemini/use-cases/applying-llms-to-data/using-gemini-with-bigquery-remote-functions/storage.tf new file mode 100644 index 0000000..3a9025f --- /dev/null +++ b/gemini/use-cases/applying-llms-to-data/using-gemini-with-bigquery-remote-functions/storage.tf @@ -0,0 +1,51 @@ +/** + * Copyright 2023 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#Create a GCS bucket to store the image analysisremote function source code +## Use a random string for the bucket name to avoid conflicts +resource "google_storage_bucket" "function_source" { + name = "gemini-bq-demo-${random_id.id.hex}" + project = module.project-services.project_id + location = var.region + uniform_bucket_level_access = true + force_destroy = var.force_destroy + depends_on = [time_sleep.wait_after_apis] +} + +##Upload the image analysis function source code to the bucket +resource "google_storage_bucket_object" "image_source_upload" { + name = "image_function_source.zip" + bucket = google_storage_bucket.function_source.name + source = data.archive_file.create_image_function_zip.output_path +} + +##Upload the text analysis function source code to the bucket +resource "google_storage_bucket_object" "text_source_upload" { + name = "text_function_source.zip" + bucket = google_storage_bucket.function_source.name + source = data.archive_file.create_text_function_zip.output_path +} + +# Create a GCS bucket to upload demo images +## Use a random string for the bucket name to avoid conflicts +resource "google_storage_bucket" "demo_images" { + name = "gemini-bq-demo-images-${random_id.id.hex}" + project = module.project-services.project_id + location = var.region + uniform_bucket_level_access = true + force_destroy = var.force_destroy + depends_on = [time_sleep.wait_after_apis] +} diff --git a/gemini/use-cases/applying-llms-to-data/using-gemini-with-bigquery-remote-functions/templates/workflow.tftpl b/gemini/use-cases/applying-llms-to-data/using-gemini-with-bigquery-remote-functions/templates/workflow.tftpl new file mode 100644 index 0000000..5970594 --- /dev/null +++ b/gemini/use-cases/applying-llms-to-data/using-gemini-with-bigquery-remote-functions/templates/workflow.tftpl @@ -0,0 +1,106 @@ +# Copyright 2023 Google LLC +# +# 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 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Runs the setup steps for the data warehouse setup: +# 1. Copies data from the public bucket into the staging buckets +# 2. Executes a set of stored procedures to stage the remaining data needed + +main: + params: [] + steps: + - sub_copy_objects: + call: copy_objects + result: output + - sub_create_tables: + call: create_remote_functions + result: output1 + +# Subworkflow to copy initial objects +copy_objects: + steps: + - init: + assign: + - source_bucket: "github-repo" + - dest_bucket: ${sample_bucket} + - copied_objects: [] + - list_objects: + call: googleapis.storage.v1.objects.list + args: + bucket: $${source_bucket} + prefix: "bigquery-remote-functions" + result: list_result + - start_counter: + assign: + - copied_objects: 0 + - copy_objects: + for: + value: object + index: i + in: $${list_result.items} + steps: + - step1: + try: + steps: + - copy_object: + call: googleapis.storage.v1.objects.copy + args: + sourceBucket: $${source_bucket} + sourceObject: $${text.url_encode(object.name)} + destinationBucket: $${dest_bucket} + destinationObject: $${text.url_encode(object.name)} + result: copy_result + - save_result: + assign: + - copied_objects: $${copied_objects + 1} + except: + as: e + raise: + exception: $${e} + sourceBucket: $${source_bucket} + sourceObject: $${object.name} + destinationBucket: $${dest_bucket} + - finish: + return: $${copied_objects + " objects copied"} + +# Subworkflow to create BigQuery remote functions +create_remote_functions: + steps: + - assignStepTables: + assign: + - results: {} + - dataset_id: ${dataset_id} + - project_id: $${sys.get_env("GOOGLE_CLOUD_PROJECT_ID")} + - map: + 1: $${"CALL `"+project_id+"."+dataset_id+".image_remote_function_sp`();"} + 2: $${"CALL `"+project_id+"."+dataset_id+".text_remote_function_sp`();"} + 3: $${"CALL `"+project_id+"."+dataset_id+".provision_text_sample_table_sp`();"} + - loopStepTables: + for: + value: key + in: $${keys(map)} + steps: + - runQuery: + call: googleapis.bigquery.v2.jobs.query + args: + projectId: $${sys.get_env("GOOGLE_CLOUD_PROJECT_ID")} + body: + useLegacySql: false + useQueryCache: false + location: $${sys.get_env("GOOGLE_CLOUD_LOCATION")} + timeoutMs: 600000 + query: $${map[key]} + result: queryResult + - sumStep: + assign: + - results[key]: $${queryResult} diff --git a/gemini/use-cases/applying-llms-to-data/using-gemini-with-bigquery-remote-functions/variables.tf b/gemini/use-cases/applying-llms-to-data/using-gemini-with-bigquery-remote-functions/variables.tf new file mode 100644 index 0000000..43abec6 --- /dev/null +++ b/gemini/use-cases/applying-llms-to-data/using-gemini-with-bigquery-remote-functions/variables.tf @@ -0,0 +1,61 @@ +/** + * Copyright 2023 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +variable "project_id" { + type = string + description = "Google Cloud Project ID" +} + +variable "region" { + type = string + description = "Google Cloud Region" +} + +variable "connection_id" { + type = string + description = "Default ID for the BigQuery connection that is created" + default = "gcf-connection" +} + +variable "image_function_name" { + type = string + description = "Name of the BigQuery remote function for image analysis" + default = "gemini_bq_demo_image" +} + +variable "text_function_name" { + type = string + description = "Name of the BigQuery remote function for text analysis" + default = "gemini_bq_demo_text" +} + +variable "enable_apis" { + type = string + description = "Whether or not to enable underlying apis in this solution." + default = true +} + +variable "force_destroy" { + type = string + description = "Whether or not to delete BigQuery resources when solution is modified or changed." + default = true +} + +variable "deletion_protection" { + type = string + description = "Whether or not to protect GCS resources from deletion when solution is modified or changed." + default = false +} diff --git a/gemini/use-cases/applying-llms-to-data/using-gemini-with-bigquery-remote-functions/versions.tf b/gemini/use-cases/applying-llms-to-data/using-gemini-with-bigquery-remote-functions/versions.tf new file mode 100644 index 0000000..4286fa6 --- /dev/null +++ b/gemini/use-cases/applying-llms-to-data/using-gemini-with-bigquery-remote-functions/versions.tf @@ -0,0 +1,45 @@ +/** + * Copyright 2023 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +terraform { + required_providers { + google = { + source = "hashicorp/google" + version = "< 7" + } + random = { + source = "hashicorp/random" + version = ">= 2" + } + archive = { + source = "hashicorp/archive" + version = ">= 2" + } + time = { + source = "hashicorp/time" + version = ">= 0.9.1" + } + http = { + source = "hashicorp/http" + version = ">= 3.4.1" + } + } + required_version = ">= 0.13" + + provider_meta "google" { + module_name = "blueprints/terraform/terraform-google-bigquery:data_warehouse/v7.0.0" + } +} diff --git a/gemini/use-cases/applying-llms-to-data/using-gemini-with-bigquery-remote-functions/workflows.tf b/gemini/use-cases/applying-llms-to-data/using-gemini-with-bigquery-remote-functions/workflows.tf new file mode 100644 index 0000000..e5b4adc --- /dev/null +++ b/gemini/use-cases/applying-llms-to-data/using-gemini-with-bigquery-remote-functions/workflows.tf @@ -0,0 +1,101 @@ +/** + * Copyright 2023 Google LLC + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +# Set up the Workflow +## Create the Workflows service account to manage permissions +resource "google_service_account" "workflow_service_account" { + project = module.project-services.project_id + account_id = "cloud-workflow-sa-${random_id.id.hex}" + display_name = "Service Account for Cloud Workflows" + depends_on = [time_sleep.wait_after_apis] +} + +locals { + workflow_roles = [ + "roles/workflows.admin", + "roles/run.invoker", + "roles/cloudfunctions.invoker", + "roles/iam.serviceAccountTokenCreator", + "roles/storage.objectAdmin", + "roles/bigquery.connectionAdmin", + "roles/bigquery.jobUser", + "roles/bigquery.dataEditor", + ] +} + +## Grant the Workflow service account access needed to execute its tasks +resource "google_project_iam_member" "workflow_service_account_roles" { + count = length(local.workflow_roles) + project = module.project-services.project_id + role = local.workflow_roles[count.index] + member = "serviceAccount:${google_service_account.workflow_service_account.email}" + depends_on = [google_project_iam_member.functions_invoke_roles] +} + +## Create the workflow +resource "google_workflows_workflow" "workflow" { + name = "setup-workflow" + project = module.project-services.project_id + region = var.region + description = "Runs post Terraform setup steps for Solution in Console" + service_account = google_service_account.workflow_service_account.id + + source_contents = templatefile("${path.module}/templates/workflow.tftpl", { + sample_bucket = google_storage_bucket.demo_images.name, + dataset_id = google_bigquery_dataset.demo_dataset.dataset_id + }) + + depends_on = [ + google_service_account.workflow_service_account, + google_bigquery_connection.function_connection, + google_bigquery_routine.image_create_remote_function_sp, + google_bigquery_routine.image_query_remote_function_sp, + google_bigquery_routine.provision_text_sample_table_sp, + google_bigquery_routine.text_create_remote_function_sp, + google_bigquery_routine.text_query_remote_function_sp, + google_bigquery_table.object_table, + google_cloudfunctions2_function.image_remote_function, + google_cloudfunctions2_function.text_remote_function, + google_storage_bucket.demo_images, + google_project_iam_member.functions_invoke_roles, + time_sleep.wait_after_functions, + ] +} + +## Trigger the execution of the setup workflow +data "http" "call_workflows_setup" { + url = "https://workflowexecutions.googleapis.com/v1/projects/${module.project-services.project_id}/locations/${var.region}/workflows/${google_workflows_workflow.workflow.name}/executions" + method = "POST" + request_headers = { + Accept = "application/json" + Authorization = "Bearer ${data.google_client_config.current.access_token}" } + depends_on = [ + google_workflows_workflow.workflow, + google_bigquery_connection.function_connection, + google_bigquery_routine.image_create_remote_function_sp, + google_bigquery_routine.image_query_remote_function_sp, + google_bigquery_routine.provision_text_sample_table_sp, + google_bigquery_routine.text_create_remote_function_sp, + google_bigquery_routine.text_query_remote_function_sp, + google_bigquery_table.object_table, + google_cloudfunctions2_function.image_remote_function, + google_cloudfunctions2_function.text_remote_function, + google_storage_bucket.demo_images, + google_project_iam_member.functions_invoke_roles, + time_sleep.wait_after_functions, + ] +} diff --git a/gemini/use-cases/code/analyze_codebase.ipynb b/gemini/use-cases/code/analyze_codebase.ipynb new file mode 100644 index 0000000..fb68c39 --- /dev/null +++ b/gemini/use-cases/code/analyze_codebase.ipynb @@ -0,0 +1,951 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "bCIMTPB1WoTq" + }, + "outputs": [], + "source": [ + "# Copyright 2024 Google LLC\n", + "#\n", + "# Licensed under the Apache License, Version 2.0 (the \"License\");\n", + "# you may not use this file except in compliance with the License.\n", + "# You may obtain a copy of the License at\n", + "#\n", + "# https://www.apache.org/licenses/LICENSE-2.0\n", + "#\n", + "# Unless required by applicable law or agreed to in writing, software\n", + "# distributed under the License is distributed on an \"AS IS\" BASIS,\n", + "# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n", + "# See the License for the specific language governing permissions and\n", + "# limitations under the License." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "pPmcMNgZpo9V" + }, + "source": [ + "# Analyze a codebase with Gemini in Vertex AI\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
    \n", + " \n", + " \"Google
    Open in Colab\n", + "
    \n", + "
    \n", + " \n", + " \"Google
    Open in Colab Enterprise\n", + "
    \n", + "
    \n", + " \n", + " \"Vertex
    Open in Workbench\n", + "
    \n", + "
    \n", + " \n", + " \"GitHub
    View on GitHub\n", + "
    \n", + "
    \n", + " \n", + " \"Google
    Open in Cloud Skills Boost\n", + "
    \n", + "
    \n", + "\n", + "
    \n", + "\n", + "Share to:\n", + "\n", + "\n", + " \"LinkedIn\n", + "\n", + "\n", + "\n", + " \"Bluesky\n", + "\n", + "\n", + "\n", + " \"X\n", + "\n", + "\n", + "\n", + " \"Reddit\n", + "\n", + "\n", + "\n", + " \"Facebook\n", + " " + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "1EExYZvij2ve" + }, + "source": [ + "| | |\n", + "|-|-|\n", + "|Author(s) | [Eric Dong](https://github.com/gericdong), [Holt Skinner](https://github.com/holtskinner), [Aakash Gouda](https://github.com/aksstar)|" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "7yVV6txOmNMn" + }, + "source": [ + "## Overview\n", + "\n", + "Gemini features a breakthrough long context window of up to 1 million tokens that can help seamlessly analyze, classify and summarize large amounts of content within a given prompt.\n", + "\n", + "With its long-context reasoning, Gemini can analyze an entire codebase for deeper insights.\n", + "\n", + "In this tutorial, you learn how to analyze an entire codebase with Gemini 3 and prompt the model to:\n", + "\n", + "- **Analyze**: Summarize codebases effortlessly.\n", + "- **Guide**: Generate clear developer getting-started documentation.\n", + "- **Debug**: Uncover critical bugs and provide fixes.\n", + "- **Enhance**: Implement new features and improve reliability and security.\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "hdMGtr18rFdL" + }, + "source": [ + "## Getting Started" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "No17Cw5hgx12" + }, + "source": [ + "### Install Google Gen AI SDK for Python and other libraries\n", + "\n", + "In addition to the [Google Gen AI SDK for Python](https://cloud.google.com/vertex-ai/generative-ai/docs/sdks/overview), we will be using [Gitingest](https://gitingest.com/) to load the repository into the prompt.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "tFy3H3aPgx12" + }, + "outputs": [], + "source": [ + "%pip install --upgrade --quiet google-genai gitingest gitpython PyGithub" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "R5Xep4W9lq-Z" + }, + "source": [ + "### Restart runtime (Colab only)\n", + "\n", + "To use the newly installed packages in this Jupyter runtime, you must restart the runtime. You can do this by running the cell below, which restarts the current kernel.\n", + "\n", + "The restart might take a minute or longer. After it's restarted, continue to the next step." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "XRvKdaPDTznN" + }, + "outputs": [], + "source": [ + "import sys\n", + "\n", + "if \"google.colab\" in sys.modules:\n", + " import IPython\n", + "\n", + " app = IPython.Application.instance()\n", + " app.kernel.do_shutdown(True)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "SbmM4z7FOBpM" + }, + "source": [ + "
    \n", + "⚠ The kernel is going to restart. Please wait until it is finished before continuing to the next step. ⚠\n", + "
    \n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "dmWOrTJ3gx13" + }, + "source": [ + "### Authenticate your notebook environment (Colab only)\n", + "\n", + "If you are running this notebook on Google Colab, run the following cell to authenticate your environment.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "NyKGtVQjgx13" + }, + "outputs": [], + "source": [ + "import sys\n", + "\n", + "if \"google.colab\" in sys.modules:\n", + " from google.colab import auth\n", + "\n", + " auth.authenticate_user()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "DF4l8DTdWgPY" + }, + "source": [ + "### Set Google Cloud project information and create client\n", + "\n", + "To get started using Vertex AI, you must have an existing Google Cloud project and [enable the Vertex AI API](https://console.cloud.google.com/flows/enableapi?apiid=aiplatform.googleapis.com).\n", + "\n", + "Learn more about [setting up a project and a development environment](https://cloud.google.com/vertex-ai/docs/start/cloud-environment)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "Nqwi-5ufWp_B" + }, + "outputs": [], + "source": [ + "import os\n", + "\n", + "# fmt: off\n", + "PROJECT_ID = \"[your-project-id]\" # @param {type: \"string\", placeholder: \"[your-project-id]\", isTemplate: true}\n", + "# fmt: on\n", + "if not PROJECT_ID or PROJECT_ID == \"[your-project-id]\":\n", + " PROJECT_ID = str(os.environ.get(\"GOOGLE_CLOUD_PROJECT\"))\n", + "\n", + "LOCATION = os.environ.get(\"GOOGLE_CLOUD_REGION\", \"us-central1\")\n", + "\n", + "from google import genai\n", + "\n", + "client = genai.Client(enterprise=True, project=PROJECT_ID, location=LOCATION)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "vbozY-XKee95" + }, + "source": [ + "### Import libraries" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": { + "id": "NSCFmvOWBas9" + }, + "outputs": [], + "source": [ + "import os\n", + "import shutil\n", + "\n", + "import git\n", + "from IPython.core.interactiveshell import InteractiveShell\n", + "from IPython.display import Markdown, display\n", + "from github import Github\n", + "from gitingest import ingest\n", + "\n", + "InteractiveShell.ast_node_interactivity = \"all\"\n", + "\n", + "import nest_asyncio\n", + "from google.genai.types import CreateCachedContentConfig, GenerateContentConfig\n", + "\n", + "nest_asyncio.apply()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "WNoOTMp2fe33" + }, + "source": [ + "## Cloning a codebase\n", + "\n", + "You will use the repo [Online Boutique](https://github.com/GoogleCloudPlatform/microservices-demo) as an example in this notebook.the Online Boutique is a cloud-first microservices demo application. The application is a web-based e-commerce app where users can browse items, add them to the cart, and purchase them. This application consists of 11 microservices across multiple languages." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": { + "id": "GlDOs49qgStM" + }, + "outputs": [], + "source": [ + "# The GitHub repository URL\n", + "# fmt: off\n", + "repo_url = \"https://github.com/GoogleCloudPlatform/microservices-demo\" # @param {type:\"string\"}\n", + "# fmt: on\n", + "\n", + "# The location to clone the repo\n", + "repo_dir = \"./repo\"" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "PAm1ly9pfIEX" + }, + "source": [ + "#### Define helper functions for processing GitHub repository" + ] + }, + { + "cell_type": "code", + "execution_count": 32, + "metadata": { + "id": "stNia6UaHau2" + }, + "outputs": [], + "source": [ + "def clone_repo(repo_url: str, repo_dir: str) -> None:\n", + " \"\"\"Shallow clone a GitHub repository.\"\"\"\n", + " if os.path.exists(repo_dir):\n", + " shutil.rmtree(repo_dir)\n", + " os.makedirs(repo_dir, exist_ok=True)\n", + " git.Repo.clone_from(repo_url, repo_dir, depth=2)\n", + "\n", + "\n", + "def get_github_issue(owner: str, repo: str, issue_number: int) -> str | None:\n", + " \"\"\"Fetch the contents of a GitHub issue.\n", + "\n", + " Args:\n", + " owner (str): The owner of the repository.\n", + " repo (str): The name of the repository.\n", + " issue_number (int): The issue number to retrieve.\n", + "\n", + " Returns:\n", + " str | None: The issue body if found, otherwise None.\n", + "\n", + " Raises:\n", + " Exception: If an error occurs while fetching the issue.\n", + " \"\"\"\n", + " g = Github()\n", + "\n", + " try:\n", + " repository = g.get_repo(f\"{owner}/{repo}\")\n", + " issue = repository.get_issue(number=issue_number)\n", + " return issue.body\n", + " except Exception as error:\n", + " print(f\"Error fetching issue: {error}\")\n", + " return None\n", + "\n", + "\n", + "def get_git_diff(repo_dir: str) -> str:\n", + " \"\"\"Fetches commit IDs from a local Git repository on a specified branch.\"\"\"\n", + " repo = git.Repo(repo_dir)\n", + " branch_name = \"main\"\n", + "\n", + " # A list of commit IDs (SHA-1 hashes) in reverse chronological order (newest first)\n", + " commit_ids = [commit.hexsha for commit in repo.iter_commits(branch_name)]\n", + " if len(commit_ids) >= 2:\n", + " return repo.git.diff(commit_ids[0], commit_ids[1])\n", + " return \"\"" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "2947e8614485" + }, + "source": [ + "### Create an index and extract content of a codebase\n", + "\n", + "Clone the repo and create an index and extract content of code/text files.\n", + "\n", + "Gitingest will extract all of the contents of the files into a long string and create a directory tree of the files." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "4eb417d380c9" + }, + "outputs": [], + "source": [ + "clone_repo(repo_url, repo_dir)\n", + "\n", + "_, tree, content = ingest(repo_dir)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "JiVQB5SKekS0" + }, + "source": [ + "## Analyzing the codebase with Gemini\n", + "\n", + "With its long-context reasoning, Gemini can process the codebase and answer questions about the codebase." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "BY1nfXrqRxVX" + }, + "source": [ + "### Load the Gemini model\n", + "\n", + "Learn more about the [Gemini API models on Vertex AI](https://cloud.google.com/vertex-ai/generative-ai/docs/learn/models#gemini-models)." + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "metadata": { + "id": "vB9gY3WruzK9" + }, + "outputs": [], + "source": [ + "MODEL_ID = \"gemini-3.5-flash\" # @param {type:\"string\"}" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "59f3c0bbef54" + }, + "source": [ + "### Create a context cache\n", + "\n", + "We will create a [context cache](https://cloud.google.com/vertex-ai/generative-ai/docs/context-cache/context-cache-overview) of the codebase so we don't have to send the entire context with every request, saving processing time and cost.\n", + "\n", + "**Note**: Context caching is only available for stable models with fixed versions (for example, `gemini-3.5-flash`). You must include the version postfix (for example, the `-001`).\n", + "\n", + "For more information, see [Available Gemini stable model versions](https://cloud.google.com/vertex-ai/generative-ai/docs/learn/model-versioning#stable-versions-available)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "ee211faaf336" + }, + "outputs": [], + "source": [ + "system_instruction = \"You are a coding expert. Your mission is to answer all code related questions with given context and instructions.\"\n", + "\n", + "contents = [\n", + " \"\"\"\n", + " Context:\n", + " - The entire codebase is provided below.\n", + " - Here is directory tree of all of the files in the codebase:\n", + " \"\"\",\n", + " tree,\n", + " \"\"\"\n", + " - Then each of the files are concatenated together. You will find all of the code you need:\n", + " \"\"\",\n", + " content,\n", + "]\n", + "\n", + "cached_content = client.caches.create(\n", + " model=MODEL_ID,\n", + " config=CreateCachedContentConfig(\n", + " contents=contents,\n", + " system_instruction=system_instruction,\n", + " ttl=\"3600s\",\n", + " ),\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "g3OtaszvJt9L" + }, + "source": [ + "### 1. Summarizing the codebase\n", + "\n", + "Generate a summary of the codebase." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "uMexx1Qtf1ML" + }, + "outputs": [], + "source": [ + "question = \"\"\"\n", + " Give me a summary of this codebase, and tell me the top 3 things that I can learn from it.\n", + "\"\"\"\n", + "\n", + "# Generate text using non-streaming method\n", + "response = client.models.generate_content(\n", + " model=MODEL_ID,\n", + " contents=question,\n", + " # Use the cached content\n", + " config=GenerateContentConfig(\n", + " cached_content=cached_content.name,\n", + " ),\n", + ")\n", + "\n", + "# Print generated text and usage metadata\n", + "display(Markdown(response.text))\n", + "print(response.usage_metadata)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "aCshJHPCYoxI" + }, + "source": [ + "### 2. Creating a developer getting started guide\n", + "\n", + "Generate a getting started guide for developers. This sample uses the streaming option to generate the content." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "e6Kns7vCYm1P" + }, + "outputs": [], + "source": [ + "question = \"\"\"\n", + " Provide a getting started guide to onboard new developers to the codebase.\n", + "\"\"\"\n", + "\n", + "\n", + "# Generate text using streaming method\n", + "responses = client.models.generate_content_stream(\n", + " model=MODEL_ID,\n", + " contents=question,\n", + " config=GenerateContentConfig(\n", + " cached_content=cached_content.name,\n", + " ),\n", + ")\n", + "\n", + "for response in responses:\n", + " print(response.text, end=\"\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "hXurINu-jelb" + }, + "source": [ + "### 3. Finding bugs\n", + "\n", + "Find the top 3 most severe issues in the codebase." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "fy3AWPRgNhu_" + }, + "outputs": [], + "source": [ + "question = \"\"\"\n", + " Find the top 3 most severe issues in the codebase.\n", + "\"\"\"\n", + "\n", + "responses = client.models.generate_content_stream(\n", + " model=MODEL_ID,\n", + " contents=question,\n", + " config=GenerateContentConfig(\n", + " cached_content=cached_content.name,\n", + " ),\n", + ")\n", + "\n", + "for response in responses:\n", + " print(response.text, end=\"\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "HCilrR6FjmfB" + }, + "source": [ + "### 4. Fixing bug\n", + "\n", + "Find the most severe issue in the codebase that can be fixed and provide a code fix for it.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "dwjDh0xGKE2r" + }, + "outputs": [], + "source": [ + "question = \"\"\"\n", + " Find the most severe bug in the codebase that you can provide a code fix for.\n", + "\"\"\"\n", + "\n", + "responses = client.models.generate_content_stream(\n", + " model=MODEL_ID,\n", + " contents=question,\n", + " config=GenerateContentConfig(\n", + " cached_content=cached_content.name,\n", + " ),\n", + ")\n", + "\n", + "for response in responses:\n", + " print(response.text, end=\"\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "8w2pCULT_xKE" + }, + "source": [ + "### 5. Implementing a feature request using Function Calling\n", + "\n", + "Get the feature request text from a GitHub Issue URL.\n", + "\n", + "We will use [Function Calling](https://cloud.google.com/vertex-ai/generative-ai/docs/multimodal/function-calling) to extract the feature request data from the prompt, then call the GitHub API to retrieve the contents.\n", + "\n", + "Note: We can't use the previously created cached content, because tools cannot be added at runtime when using cached content, and the other prompts in this notebook do not need this tool." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "mMjOy0gJ1_xx" + }, + "outputs": [], + "source": [ + "FEATURE_REQUEST_URL = (\n", + " \"https://github.com/GoogleCloudPlatform/microservices-demo/issues/2205\"\n", + ")\n", + "\n", + "question = f\"What is the feature request of the following {FEATURE_REQUEST_URL}\"\n", + "\n", + "response = client.models.generate_content(\n", + " model=MODEL_ID,\n", + " contents=question,\n", + " # Use the function as a tool\n", + " config=GenerateContentConfig(\n", + " tools=[get_github_issue],\n", + " ),\n", + ")\n", + "\n", + "issue_description = response.text\n", + "display(Markdown(f\"# Feature Request\\n{issue_description}\"))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "19455545f4c9" + }, + "source": [ + "Use the GitHub Issue text to implement the feature request." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "78e6df259be8" + }, + "outputs": [], + "source": [ + "# Combine feature request content and cached code content\n", + "question = f\"\"\"Implement the following feature request\n", + "{issue_description}\n", + "\"\"\"\n", + "\n", + "response = client.models.generate_content(\n", + " model=MODEL_ID,\n", + " contents=question,\n", + " config=GenerateContentConfig(\n", + " cached_content=cached_content.name,\n", + " ),\n", + ")\n", + "\n", + "# Generate code response\n", + "display(Markdown(response.text))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "TOk_Qe35b_cJ" + }, + "source": [ + "### 6. Creating a troubleshooting guide\n", + "\n", + "Create a troubleshooting guide to help resolve common issues." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "DKn85LS-v0iw" + }, + "outputs": [], + "source": [ + "question = \"\"\"\n", + " Provide a troubleshooting guide to help resolve common issues.\n", + "\"\"\"\n", + "\n", + "responses = client.models.generate_content_stream(\n", + " model=MODEL_ID,\n", + " contents=question,\n", + " config=GenerateContentConfig(\n", + " cached_content=cached_content.name,\n", + " ),\n", + ")\n", + "\n", + "for response in responses:\n", + " print(response.text, end=\"\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "h23z0sTsj5pL" + }, + "source": [ + "### 7. Making the app more reliable\n", + "\n", + "Recommend best practices to make the application more reliable.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "yOBSulTPLUAo" + }, + "outputs": [], + "source": [ + "question = \"\"\"\n", + " How can I make this application more reliable? Consider best practices from https://www.r9y.dev/\n", + "\"\"\"\n", + "\n", + "responses = client.models.generate_content_stream(\n", + " model=MODEL_ID,\n", + " contents=question,\n", + " config=GenerateContentConfig(\n", + " cached_content=cached_content.name,\n", + " ),\n", + ")\n", + "\n", + "for response in responses:\n", + " print(response.text, end=\"\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "uf1jNDpJj8u0" + }, + "source": [ + "### 8. Making the app more secure\n", + "\n", + "Recommend best practices to make the application more secure." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "Hy_mCyFVLlXU" + }, + "outputs": [], + "source": [ + "question = \"\"\"\n", + " How can you secure the application?\n", + "\"\"\"\n", + "\n", + "responses = client.models.generate_content_stream(\n", + " model=MODEL_ID,\n", + " contents=question,\n", + " config=GenerateContentConfig(\n", + " cached_content=cached_content.name,\n", + " ),\n", + ")\n", + "\n", + "for response in responses:\n", + " print(response.text, end=\"\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "GFfwMOb6kYfw" + }, + "source": [ + "### 9. Learning the codebase\n", + "\n", + "Create a quiz about the concepts used in the codebase." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "l7jQIUwsNRH4" + }, + "outputs": [], + "source": [ + "question = \"\"\"\n", + " Create a quiz about the concepts used in the codebase to help me solidify my understanding.\n", + "\"\"\"\n", + "\n", + "responses = client.models.generate_content_stream(\n", + " model=MODEL_ID,\n", + " contents=question,\n", + " config=GenerateContentConfig(\n", + " cached_content=cached_content.name,\n", + " ),\n", + ")\n", + "\n", + "for response in responses:\n", + " print(response.text, end=\"\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "Rjo1UrZwGLan" + }, + "source": [ + "### 10. Creating a quickstart tutorial\n", + "\n", + "Create an end-to-end quickstart tutorial for a specific component.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "FRwmRyDDFRMB" + }, + "outputs": [], + "source": [ + "question = \"\"\"\n", + " Please write an end-to-end quickstart tutorial that introduces AlloyDB,\n", + " shows how to configure it with the CartService,\n", + " and highlights key capabilities of AlloyDB in context of the Online Boutique application.\n", + "\"\"\"\n", + "\n", + "responses = client.models.generate_content_stream(\n", + " model=MODEL_ID,\n", + " contents=question,\n", + " config=GenerateContentConfig(\n", + " cached_content=cached_content.name,\n", + " ),\n", + ")\n", + "\n", + "for response in responses:\n", + " print(response.text, end=\"\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "OAJ-kBCZnlH_" + }, + "source": [ + "### 11. Creating a Git Changelog Generator\n", + "\n", + "Understanding changes made between Git commits and highlighting the most important aspects of the changes." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "jdeWO8crnlH_" + }, + "outputs": [], + "source": [ + "diff_text = get_git_diff(repo_dir)\n", + "question = f\"\"\"\n", + " Given the below git diff output, Summarize the important changes made.\n", + "```diff\n", + "{diff_text}\n", + "```\n", + "\"\"\"\n", + "\n", + "responses = client.models.generate_content_stream(\n", + " model=MODEL_ID,\n", + " contents=question,\n", + " config=GenerateContentConfig(\n", + " cached_content=cached_content.name,\n", + " ),\n", + ")\n", + "\n", + "for response in responses:\n", + " print(response.text, end=\"\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "7kUeIBfGyoX7" + }, + "source": [ + "## Conclusion\n", + "\n", + "In this tutorial, you've learned how to use Gemini to analyze a codebase and prompt the model to:\n", + "\n", + "- Summarize codebases effortlessly.\n", + "- Generate clear developer getting-started documentation.\n", + "- Uncover critical bugs and provide fixes.\n", + "- Implement new features and improve reliability and security.\n", + "- Understanding changes made between Git commits" + ] + } + ], + "metadata": { + "colab": { + "name": "analyze_codebase.ipynb", + "toc_visible": true + }, + "kernelspec": { + "display_name": "Python 3", + "name": "python3" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} diff --git a/gemini/use-cases/code/code_retrieval_augmented_generation.ipynb b/gemini/use-cases/code/code_retrieval_augmented_generation.ipynb new file mode 100644 index 0000000..dfd566f --- /dev/null +++ b/gemini/use-cases/code/code_retrieval_augmented_generation.ipynb @@ -0,0 +1,780 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "5norOZI0mA6s" + }, + "outputs": [], + "source": [ + "# Copyright 2023 Google LLC\n", + "#\n", + "# Licensed under the Apache License, Version 2.0 (the \"License\");\n", + "# you may not use this file except in compliance with the License.\n", + "# You may obtain a copy of the License at\n", + "#\n", + "# https://www.apache.org/licenses/LICENSE-2.0\n", + "#\n", + "# Unless required by applicable law or agreed to in writing, software\n", + "# distributed under the License is distributed on an \"AS IS\" BASIS,\n", + "# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n", + "# See the License for the specific language governing permissions and\n", + "# limitations under the License." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "XNPE46X8mJj4" + }, + "source": [ + "# Use Retrieval Augmented Generation (RAG) with Gemini API\n", + "\n", + "\n", + "\n", + " \n", + "\n", + " \n", + " \n", + " \n", + "
    \n", + " \n", + " \"Google
    Open in Colab\n", + "
    \n", + "
    \n", + " \n", + " \"Google
    Open in Colab Enterprise\n", + "
    \n", + "
    \n", + " \n", + " \"GitHub
    View on GitHub\n", + "
    \n", + "
    \n", + " \n", + " \"Vertex
    Open in Workbench\n", + "
    \n", + "
    \n", + "\n", + "
    \n", + "\n", + "Share to:\n", + "\n", + "\n", + " \"LinkedIn\n", + "\n", + "\n", + "\n", + " \"Bluesky\n", + "\n", + "\n", + "\n", + " \"X\n", + "\n", + "\n", + "\n", + " \"Reddit\n", + "\n", + "\n", + "\n", + " \"Facebook\n", + " " + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "VrLtlKPFqSxB" + }, + "source": [ + "| | |\n", + "|-|-|\n", + "|Author(s) | [Lavi Nigam](https://github.com/lavinigam-gcp), [Polong Lin](https://github.com/polong-lin) |" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "zNAEdYNFmQcP" + }, + "source": [ + "### Objective\n", + "\n", + "This notebook demonstrates how you augment output from Gemini API by bringing in external knowledge. An example is provided using Code Retrieval Augmented Generation(RAG) pattern using [Google Cloud's Generative AI github repository](https://github.com/GoogleCloudPlatform/generative-ai) as external knowledge. The notebook uses [Gemini API in Vertex AI](https://ai.google.dev/gemini-api), [Embeddings for Text API](https://cloud.google.com/vertex-ai/docs/generative-ai/embeddings/get-text-embeddings), FAISS vector store and [LangChain đŸŠœïžđŸ”—](https://python.langchain.com/en/latest/).\n", + "\n", + "### Overview\n", + "\n", + "Here is overview of what we'll go over.\n", + "\n", + "Index Creation:\n", + "\n", + "1. Recursively list the files(.ipynb) in github repo\n", + "2. Extract code and markdown from the files\n", + "3. Chunk & generate embeddings for each code strings and add initialize the vector store\n", + "\n", + "Runtime:\n", + "\n", + "4. User enters a prompt or asks a question as a prompt\n", + "5. Try zero-shot prompt\n", + "6. Run prompt using RAG Chain & compare results.To generate response we use **gemini-2.0-flash**\n", + "\n", + "### Cost\n", + "\n", + "This tutorial uses billable components of Google Cloud:\n", + "\n", + "- Gemini API in Vertex AI offered by Google Cloud\n", + "\n", + "Learn about [Vertex AI pricing](https://cloud.google.com/vertex-ai/pricing) and use the [Pricing Calculator](https://cloud.google.com/products/calculator/) to generate a cost estimate based on your projected usage.\n", + "\n", + "**Note:** We are using local vector store(FAISS) for this example however recommend managed highly scalable vector store for production usage such as [Vertex AI Vector Search](https://cloud.google.com/vertex-ai/docs/vector-search/overview) or [AlloyDB for PostgreSQL](https://cloud.google.com/alloydb/docs/ai/work-with-embeddings) or [Cloud SQL for PostgreSQL](https://cloud.google.com/sql/docs/postgres/features) using pgvector extension." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "2cab0c8509c9" + }, + "source": [ + "## Get started" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "b56b5a5d28c1" + }, + "source": [ + "### Install Vertex AI SDK for Python and other required packages\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "QHaqV20Csqkt" + }, + "outputs": [], + "source": [ + "%pip install --upgrade --user -q google-cloud-aiplatform \\\n", + " langchain \\\n", + " langchain_google_vertexai \\\n", + " langchain-community \\\n", + " faiss-cpu \\\n", + " nbformat" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "-VUWOgz6M1rZ" + }, + "source": [ + "### Restart runtime (Colab only)\n", + "\n", + "To use the newly installed packages, you must restart the runtime on Google Colab." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "BIS8EYgkMy8T" + }, + "outputs": [], + "source": [ + "import sys\n", + "\n", + "if \"google.colab\" in sys.modules:\n", + " import IPython\n", + "\n", + " app = IPython.Application.instance()\n", + " app.kernel.do_shutdown(True)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "0af13c10a26a" + }, + "source": [ + "
    \n", + "⚠ The kernel is going to restart. Wait until it's finished before continuing to the next step. ⚠\n", + "
    \n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "uZcP9WBENG0e" + }, + "source": [ + "### Authenticate your notebook environment (Colab only)\n", + "\n", + "Authenticate your environment on Google Colab.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "1S_HgQXQNcbz" + }, + "outputs": [], + "source": [ + "import sys\n", + "\n", + "if \"google.colab\" in sys.modules:\n", + " from google.colab import auth\n", + "\n", + " auth.authenticate_user()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "rVmxMr43Nhoo" + }, + "source": [ + "### Import libraries" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "L-Tljm5asMBc" + }, + "outputs": [], + "source": [ + "import time\n", + "\n", + "from google.cloud import aiplatform\n", + "from langchain.chains import RetrievalQA\n", + "from langchain.prompts import PromptTemplate\n", + "from langchain.schema.document import Document\n", + "from langchain.text_splitter import Language, RecursiveCharacterTextSplitter\n", + "from langchain.vectorstores import FAISS\n", + "\n", + "# LangChain\n", + "from langchain_google_vertexai import VertexAI, VertexAIEmbeddings\n", + "import nbformat\n", + "import requests\n", + "\n", + "# Vertex AI\n", + "import vertexai\n", + "\n", + "# Print the version of Vertex AI SDK for Python\n", + "print(f\"Vertex AI SDK version: {aiplatform.__version__}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "4f872cd812d0" + }, + "source": [ + "### Set Google Cloud project information and initialize Vertex AI SDK for Python\n", + "\n", + "To get started using Vertex AI, you must have an existing Google Cloud project and [enable the Vertex AI API](https://console.cloud.google.com/flows/enableapi?apiid=aiplatform.googleapis.com). Learn more about [setting up a project and a development environment](https://cloud.google.com/vertex-ai/docs/start/cloud-environment)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "eNGEcBKG0iK-" + }, + "outputs": [], + "source": [ + "# Initialize project\n", + "# Define project information\n", + "PROJECT_ID = \"YOUR_PROJECT_ID\" # @param {type:\"string\"}\n", + "LOCATION = \"us-central1\" # @param {type:\"string\"}\n", + "\n", + "vertexai.init(project=PROJECT_ID, location=LOCATION)\n", + "\n", + "# Code Generation\n", + "code_llm = VertexAI(\n", + " model_name=\"gemini-2.0-flash\",\n", + " max_output_tokens=2048,\n", + " temperature=0.1,\n", + " verbose=False,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "o537exyZk9DI" + }, + "source": [ + "Next we need to create a GitHub personal token to be able to list all files in a repository.\n", + "\n", + "- Follow [this link](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens) to create GitHub token with repo->public_repo scope and update `GITHUB_TOKEN` variable below." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "Bt9IVDSqk7y4" + }, + "outputs": [], + "source": [ + "# provide GitHub personal access token\n", + "GITHUB_TOKEN = \"YOUR_GITHUB_TOKEN\" # @param {type:\"string\"}\n", + "GITHUB_REPO = \"GoogleCloudPlatform/generative-ai\" # @param {type:\"string\"}" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "dqq3GeEbOJbU" + }, + "source": [ + "# Index Creation\n", + "\n", + "We use the Google Cloud Generative AI github repository as the data source. First list all Jupyter Notebook files in the repo and store it in a text file.\n", + "\n", + "You can skip this step(#1) if you have executed it once and generated the output text file.\n", + "\n", + "### 1. Recursively list the files(.ipynb) in the github repository" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "eTA1Jt0uOX8y" + }, + "outputs": [], + "source": [ + "# Crawls a GitHub repository and returns a list of all ipynb files in the repository\n", + "\n", + "\n", + "def crawl_github_repo(url: str, is_sub_dir: bool, access_token: str = GITHUB_TOKEN):\n", + " ignore_list = [\"__init__.py\"]\n", + "\n", + " if not is_sub_dir:\n", + " api_url = f\"https://api.github.com/repos/{url}/contents\"\n", + "\n", + " else:\n", + " api_url = url\n", + "\n", + " headers = {\n", + " \"Accept\": \"application/vnd.github.v3+json\",\n", + " \"Authorization\": f\"Bearer {access_token}\",\n", + " }\n", + "\n", + " response = requests.get(api_url, headers=headers)\n", + " response.raise_for_status() # Check for any request errors\n", + "\n", + " files = []\n", + "\n", + " contents = response.json()\n", + "\n", + " for item in contents:\n", + " if (\n", + " item[\"type\"] == \"file\"\n", + " and item[\"name\"] not in ignore_list\n", + " and (item[\"name\"].endswith(\".py\") or item[\"name\"].endswith(\".ipynb\"))\n", + " ):\n", + " files.append(item[\"html_url\"])\n", + " elif item[\"type\"] == \"dir\" and not item[\"name\"].startswith(\".\"):\n", + " sub_files = crawl_github_repo(item[\"url\"], True)\n", + " time.sleep(0.1)\n", + " files.extend(sub_files)\n", + "\n", + " return files" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "5vaKaxcGO_R6" + }, + "outputs": [], + "source": [ + "code_files_urls = crawl_github_repo(GITHUB_REPO, False, GITHUB_TOKEN)\n", + "\n", + "# Write list to a file so you do not have to download each time\n", + "with open(\"code_files_urls.txt\", \"w\") as f:\n", + " for item in code_files_urls:\n", + " f.write(item + \"\\n\")\n", + "\n", + "len(code_files_urls)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "c5hoNYJ5byMJ" + }, + "outputs": [], + "source": [ + "code_files_urls[0:10]" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "mFNVieLnR8Ie" + }, + "source": [ + "### 2. Extract code from the Jupyter notebooks.\n", + "\n", + "You could also include .py file, shell scripts etc." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "ZsM1M4hn4cBu" + }, + "outputs": [], + "source": [ + "# Extracts the python code from an ipynb file from github\n", + "\n", + "\n", + "def extract_python_code_from_ipynb(github_url, cell_type=\"code\"):\n", + " raw_url = github_url.replace(\"github.com\", \"raw.githubusercontent.com\").replace(\n", + " \"/blob/\", \"/\"\n", + " )\n", + "\n", + " response = requests.get(raw_url)\n", + " response.raise_for_status() # Check for any request errors\n", + "\n", + " notebook_content = response.text\n", + "\n", + " notebook = nbformat.reads(notebook_content, as_version=nbformat.NO_CONVERT)\n", + "\n", + " python_code = None\n", + "\n", + " for cell in notebook.cells:\n", + " if cell.cell_type == cell_type:\n", + " if not python_code:\n", + " python_code = cell.source\n", + " else:\n", + " python_code += \"\\n\" + cell.source\n", + "\n", + " return python_code\n", + "\n", + "\n", + "def extract_python_code_from_py(github_url):\n", + " raw_url = github_url.replace(\"github.com\", \"raw.githubusercontent.com\").replace(\n", + " \"/blob/\", \"/\"\n", + " )\n", + "\n", + " response = requests.get(raw_url)\n", + " response.raise_for_status() # Check for any request errors\n", + "\n", + " python_code = response.text\n", + "\n", + " return python_code" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "WCRp5Xtb48is" + }, + "outputs": [], + "source": [ + "with open(\"code_files_urls.txt\") as f:\n", + " code_files_urls = f.read().splitlines()\n", + "len(code_files_urls)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "4Y9SMO7H4xgF" + }, + "outputs": [], + "source": [ + "code_strings = []\n", + "\n", + "for i in range(0, len(code_files_urls)):\n", + " if code_files_urls[i].endswith(\".ipynb\"):\n", + " content = extract_python_code_from_ipynb(code_files_urls[i], \"code\")\n", + " doc = Document(\n", + " page_content=content, metadata={\"url\": code_files_urls[i], \"file_index\": i}\n", + " )\n", + " code_strings.append(doc)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "T1AF3fhBSLOm" + }, + "source": [ + "### 3. Chunk & generate embeddings for each code strings & initialize the vector store\n", + "\n", + "We need to split code into usable chunks that the LLM can use for code generation. Therefore it's crucial to use the right chunking approach and chunk size." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "Rj1cCA2fqx64" + }, + "outputs": [], + "source": [ + "# Utility functions for Embeddings API with rate limiting\n", + "\n", + "\n", + "def rate_limit(max_per_minute):\n", + " period = 60 / max_per_minute\n", + " print(\"Waiting\")\n", + " while True:\n", + " before = time.time()\n", + " yield\n", + " after = time.time()\n", + " elapsed = after - before\n", + " sleep_time = max(0, period - elapsed)\n", + " if sleep_time > 0:\n", + " print(\".\", end=\"\")\n", + " time.sleep(sleep_time)\n", + "\n", + "\n", + "class CustomVertexAIEmbeddings(VertexAIEmbeddings):\n", + " requests_per_minute: int\n", + " num_instances_per_batch: int\n", + " model_name: str\n", + "\n", + " # Overriding embed_documents method\n", + " def embed_documents(\n", + " self, texts: list[str], batch_size: int | None = None\n", + " ) -> list[list[float]]:\n", + " limiter = rate_limit(self.requests_per_minute)\n", + " results = []\n", + " docs = list(texts)\n", + "\n", + " while docs:\n", + " # Working in batches because the API accepts maximum 5\n", + " # documents per request to get embeddings\n", + " head, docs = (\n", + " docs[: self.num_instances_per_batch],\n", + " docs[self.num_instances_per_batch :],\n", + " )\n", + " chunk = self.client.get_embeddings(head)\n", + " results.extend(chunk)\n", + " next(limiter)\n", + "\n", + " return [r.values for r in results]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "oae37l-pvzZ6" + }, + "outputs": [], + "source": [ + "# Chunk code strings\n", + "text_splitter = RecursiveCharacterTextSplitter.from_language(\n", + " language=Language.PYTHON, chunk_size=2000, chunk_overlap=200\n", + ")\n", + "\n", + "\n", + "texts = text_splitter.split_documents(code_strings)\n", + "print(len(texts))\n", + "\n", + "# Initialize Embedding API\n", + "EMBEDDING_QPM = 100\n", + "EMBEDDING_NUM_BATCH = 5\n", + "embeddings = CustomVertexAIEmbeddings(\n", + " requests_per_minute=EMBEDDING_QPM,\n", + " num_instances_per_batch=EMBEDDING_NUM_BATCH,\n", + " model_name=\"text-embedding-005\",\n", + ")\n", + "\n", + "# Create Index from embedded code chunks\n", + "db = FAISS.from_documents(texts, embeddings)\n", + "\n", + "# Init your retriever.\n", + "retriever = db.as_retriever(\n", + " search_type=\"similarity\", # Also test \"similarity\", \"mmr\"\n", + " search_kwargs={\"k\": 5},\n", + ")\n", + "\n", + "retriever" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "Q_gn89IyuHIT" + }, + "source": [ + "# Runtime\n", + "### 4. User enters a prompt or asks a question as a prompt" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "1vrvTkO7uFNi" + }, + "outputs": [], + "source": [ + "user_question = \"Create a Python function that takes a prompt and predicts using langchain.llms interface with Vertex AI Gemini model\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "azbvOUFRvEp5" + }, + "outputs": [], + "source": [ + "# Define prompt templates\n", + "\n", + "# Zero Shot prompt template\n", + "prompt_zero_shot = \"\"\"\n", + " You are a proficient python developer. Respond with the syntactically correct & concise code for to the question below.\n", + "\n", + " Question:\n", + " {question}\n", + "\n", + " Output Code :\n", + " \"\"\"\n", + "\n", + "prompt_prompt_zero_shot = PromptTemplate(\n", + " input_variables=[\"question\"],\n", + " template=prompt_zero_shot,\n", + ")\n", + "\n", + "\n", + "# RAG template\n", + "prompt_RAG = \"\"\"\n", + " You are a proficient python developer. Respond with the syntactically correct code for to the question below. Make sure you follow these rules:\n", + " 1. Use context to understand the APIs and how to use it & apply.\n", + " 2. Do not add license information to the output code.\n", + " 3. Do not include Colab code in the output.\n", + " 4. Ensure all the requirements in the question are met.\n", + "\n", + " Question:\n", + " {question}\n", + "\n", + " Context:\n", + " {context}\n", + "\n", + " Helpful Response :\n", + " \"\"\"\n", + "\n", + "prompt_RAG_template = PromptTemplate(\n", + " template=prompt_RAG, input_variables=[\"context\", \"question\"]\n", + ")\n", + "\n", + "qa_chain = RetrievalQA.from_llm(\n", + " llm=code_llm,\n", + " prompt=prompt_RAG_template,\n", + " retriever=retriever,\n", + " return_source_documents=True,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "3NBaObAQSlIv" + }, + "source": [ + "### 5. Try zero-shot prompt" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "1svTVwtBS0zP" + }, + "outputs": [], + "source": [ + "response = code_llm.invoke(input=user_question, max_output_tokens=2048, temperature=0.1)\n", + "print(response)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "JPm8qdxzwPM0" + }, + "source": [ + "### 6. Run prompt using RAG Chain & compare results\n", + "To generate response we use code-bison however can also use code-gecko and codechat-bison" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "ZMz3nPMyVoj_" + }, + "outputs": [], + "source": [ + "results = qa_chain.invoke(input={\"query\": user_question})\n", + "print(results[\"result\"])" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "HF3lVWK1wjxe" + }, + "source": [ + "### Let's try another prompt" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "jel0ON68XiU7" + }, + "outputs": [], + "source": [ + "user_question = \"Create python function that takes text input and returns embeddings using LangChain with Vertex AI text-embedding-005 model\"\n", + "\n", + "\n", + "response = code_llm.invoke(input=user_question, max_output_tokens=2048, temperature=0.1)\n", + "print(response)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "G9bIkqE8sO6P" + }, + "outputs": [], + "source": [ + "results = qa_chain.invoke(input={\"query\": user_question})\n", + "print(results[\"result\"])" + ] + } + ], + "metadata": { + "colab": { + "name": "code_retrieval_augmented_generation.ipynb", + "toc_visible": true + }, + "kernelspec": { + "display_name": "Python 3", + "name": "python3" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} diff --git a/gemini/use-cases/code/code_scanning_and_vulnerability_detection.ipynb b/gemini/use-cases/code/code_scanning_and_vulnerability_detection.ipynb new file mode 100644 index 0000000..f19174f --- /dev/null +++ b/gemini/use-cases/code/code_scanning_and_vulnerability_detection.ipynb @@ -0,0 +1,901 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "nUWIDIzvODfw", + "metadata": { + "id": "nUWIDIzvODfw" + }, + "source": [ + "# Code Vulnerability Scanning & Automated Remediation using Gemini API in Vertex AI (Gemini 2.0)\n", + "\n", + "---\n" + ] + }, + { + "cell_type": "markdown", + "id": "xHjHLyn8m2WF", + "metadata": { + "id": "xHjHLyn8m2WF" + }, + "source": [ + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
    \n", + " \n", + " \"Google
    Open in Colab\n", + "
    \n", + "
    \n", + " \n", + " \"Google
    Open in Colab Enterprise\n", + "
    \n", + "
    \n", + " \n", + " \"Vertex
    Open in Workbench\n", + "
    \n", + "
    \n", + " \n", + " \"GitHub
    View on GitHub\n", + "
    \n", + "
    \n", + " \n", + " \"Google
    Open in Cloud Skills Boost\n", + "
    \n", + "
    \n", + "\n", + "
    \n", + "\n", + "Share to:\n", + "\n", + "\n", + " \"LinkedIn\n", + "\n", + "\n", + "\n", + " \"Bluesky\n", + "\n", + "\n", + "\n", + " \"X\n", + "\n", + "\n", + "\n", + " \"Reddit\n", + "\n", + "\n", + "\n", + " \"Facebook\n", + " \n" + ] + }, + { + "cell_type": "markdown", + "id": "zBrMoWSrXFI6", + "metadata": { + "id": "zBrMoWSrXFI6" + }, + "source": [ + "| | |\n", + "|-|-|\n", + "|Author(s) | [Souvik Mukherjee](https://github.com/talktosauvik)" + ] + }, + { + "cell_type": "markdown", + "id": "R2__rS9iZeVx", + "metadata": { + "id": "R2__rS9iZeVx" + }, + "source": [ + "## Background\n", + "\n", + "In today's digital landscape, software security is paramount. With the increasing sophistication of cyber threats, it's more important than ever for developers to proactively identify and address vulnerabilities in their code. Vulnerabilities can lead to data breaches, financial losses, and reputational damage. By harnessing the power of Gemini 2.0, we can help transform code vulnerability detection and remediation, and build a software vulnerability scanning mechanism" + ] + }, + { + "cell_type": "markdown", + "id": "6j_ed1PcwS3f", + "metadata": { + "id": "6j_ed1PcwS3f" + }, + "source": [ + "## Overview\n", + "\n", + "Gemini 2.0, a member of Google Gemini family, is a generative AI model purpose-built for diverse multimodal applications. It's proficiency in understanding and generating content across text, code, and images makes it a powerful asset for intricate codebase analysis. With its expansive 2M token context window, Gemini 2.0 efficiently processes large code volumes in a single call, streamlining large-scale code scanning.Gemini 2.0's deep comprehension of programming languages and security best practices enables it to identify potential vulnerabilities and suggest helpful and contextual modifications. Learn more about [Gemini 2.0](https://deepmind.google/technologies/gemini/pro/).\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "This experimental approach aims to efficiently scan large codebases, analyze multiple files in a single call, and delve deeper into complex code relationships and patternsThe model's deep analysis of code can help ensure comprehensive vulnerability detection, going beyond surface-level flaws. By using this approach, we can accommodate code written in several programming languages. Additionally, we can generate the findings and recommendations as JSON or CSV reports, which we would hypothetically use to make comparisons against established benchmarks and policy checks. With this tutorial, you learn how to use the Gemini API in Vertex AI, Google Cloud Storage API and the Vertex AI SDK to work with the Gemini 2.0 model to build a step by step code vulnerability scanning approach using Gemini 2.0 with the following steps:\n", + "\n", + "\n", + "* Read Python files from a GCS bucket and combining them into a single string\n", + "* Prompt engineering by crafting a clear and comprehensive prompt for Gemini 2.0, providing instructions for code analysis and output formatting\n", + "\n", + "* Submit the consolidated code string to Gemini 2.0 for analysis\n", + "* Extract vulnerability information, recommendations, and code snippets from the model response\n", + "\n", + "* Generate CSV and JSON output reports for further analysis, benchmarking and integration with security tools\n" + ] + }, + { + "cell_type": "markdown", + "id": "TpJD9T2_PvPp", + "metadata": { + "id": "TpJD9T2_PvPp" + }, + "source": [ + "### Getting Started" + ] + }, + { + "cell_type": "markdown", + "id": "k9uwJYsyP7rv", + "metadata": { + "id": "k9uwJYsyP7rv" + }, + "source": [ + "### Install Vertex AI and Google Cloud Storage SDKs for Python\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "N75LKYKXbuHp5cpzkJiq89Sb", + "metadata": { + "id": "N75LKYKXbuHp5cpzkJiq89Sb" + }, + "outputs": [], + "source": [ + "%pip install -q --upgrade --user google-cloud-aiplatform google-cloud-storage" + ] + }, + { + "cell_type": "markdown", + "id": "Xb3No6mSQFoe", + "metadata": { + "id": "Xb3No6mSQFoe" + }, + "source": [ + "### Restart the Kernel\n", + "\n", + "To use the newly installed packages, you must restart the current runtime." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2l4Q9oy8f2KR", + "metadata": { + "id": "2l4Q9oy8f2KR" + }, + "outputs": [], + "source": [ + "# restart the current runtime to be able to access the downloaded packages\n", + "import IPython\n", + "\n", + "app = IPython.Application.instance()\n", + "app.kernel.do_shutdown(True)" + ] + }, + { + "cell_type": "markdown", + "id": "IIyao4PXQkAm", + "metadata": { + "id": "IIyao4PXQkAm" + }, + "source": [ + "### Authenticate the notebook environment (Colab only)\n", + "\n", + "If you are running this notebook on Google Colab, run the following cell to authenticate your environment. This step is not required if you are using [Vertex AI Workbench](https://cloud.google.com/vertex-ai-workbench)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "pX9rkTlff8l-", + "metadata": { + "id": "pX9rkTlff8l-" + }, + "outputs": [], + "source": [ + "from google.colab import auth\n", + "\n", + "auth.authenticate_user()" + ] + }, + { + "cell_type": "markdown", + "id": "x4A6vhaKQqKT", + "metadata": { + "id": "x4A6vhaKQqKT" + }, + "source": [ + "### Set Google Cloud project information and initialize Vertex AI SDK\n", + "\n", + "\n", + "To get started using Vertex AI, you must have an existing Google Cloud project and [enable the Vertex AI API](https://console.cloud.google.com/flows/enableapi?apiid=aiplatform.googleapis.com).\n", + "\n", + "Learn more about [setting up a project and a development environment](https://cloud.google.com/vertex-ai/docs/start/cloud-environment).\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "-c8ZjhqIgA7K", + "metadata": { + "id": "-c8ZjhqIgA7K" + }, + "outputs": [], + "source": [ + "# initialize variables\n", + "\n", + "PROJECT_ID = \"[your-project-id]\" # @param {type:\"string\"}\n", + "REGION = \"us-central1\" # @param {type:\"string\"}\n", + "BUCKET_NAME = \"your-bucket-name\" # @param {type:\"string\"}\n", + "PREFIX = \"your prefix folder-name/\" # @param {type:\"string\"}" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "TwEE78OKgTRB", + "metadata": { + "id": "TwEE78OKgTRB" + }, + "outputs": [], + "source": [ + "# import and initialize Vertex AI\n", + "import vertexai\n", + "\n", + "vertexai.init(project=PROJECT_ID, location=REGION)" + ] + }, + { + "cell_type": "markdown", + "id": "zrgeiLeZRB8m", + "metadata": { + "id": "zrgeiLeZRB8m" + }, + "source": [ + "## Process Python files in batch\n", + "\n", + "This block of code reads Python files from the GCS bucket, combines their content, and adds respective `filename` as separator for LLM to better identify each file.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "lZNJhxiignA5", + "metadata": { + "id": "lZNJhxiignA5" + }, + "outputs": [], + "source": [ + "from google.cloud import storage\n", + "\n", + "\n", + "def process_py_files(bucket_name, prefix):\n", + " \"\"\"\n", + " Reads .py files from a GCS bucket and combines them into a single string.\n", + " Returns:\n", + " A string containing the combined content of all .py files.\n", + " \"\"\"\n", + "\n", + " storage_client = storage.Client()\n", + " bucket = storage_client.get_bucket(BUCKET_NAME)\n", + " blobs = bucket.list_blobs(prefix=PREFIX)\n", + "\n", + " combined_text = \"\"\n", + " for blob in blobs:\n", + " if blob.name.endswith(\".py\"):\n", + " file_content = blob.download_as_string().decode(\"utf-8\")\n", + " combined_text += f\"### File: {blob.name} ###\\n{file_content}\\n\"\n", + "\n", + " return combined_text\n", + "\n", + "\n", + "combined_string = process_py_files(BUCKET_NAME, PREFIX)\n", + "print(combined_string)" + ] + }, + { + "cell_type": "markdown", + "id": "ODdfYDIdRzzX", + "metadata": { + "id": "ODdfYDIdRzzX" + }, + "source": [ + "### Import Generative model library from Vertex AI" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "WCIOjVZYh28Q", + "metadata": { + "id": "WCIOjVZYh28Q" + }, + "outputs": [], + "source": [ + "from IPython.display import display\n", + "from vertexai.generative_models import GenerationConfig, GenerativeModel" + ] + }, + { + "cell_type": "markdown", + "id": "zyh4WLTZR42A", + "metadata": { + "id": "zyh4WLTZR42A" + }, + "source": [ + "### Initiate Gemini 2.0" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "sjA145qSh9O8", + "metadata": { + "id": "sjA145qSh9O8" + }, + "outputs": [], + "source": [ + "model = GenerativeModel(\"gemini-2.0-flash\")" + ] + }, + { + "cell_type": "markdown", + "id": "QHjEFzCBR_n9", + "metadata": { + "id": "QHjEFzCBR_n9" + }, + "source": [ + "### Setting up model configuration & Prompt template\n", + "\n", + "This piece of code sets up the model configurations & prompt template with 1 shot inference. For this specific notebook, the safety filters have not been imported.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "rX2dng4piDHd", + "metadata": { + "id": "rX2dng4piDHd" + }, + "outputs": [], + "source": [ + "# define the prompt template to be passed to gemini.\n", + "context = combined_string\n", + "\n", + "my_prompt = f\"\"\"You are an expert code assistant. Review the following code for vulnerabilities and provide recommendations:\n", + "{context}\n", + "\n", + "Please format your response using markdown and Display with the following structure:\n", + "\n", + "file_name: Name of the code file\n", + "Vulnerability Name: Name of the identified vulnerability\n", + "Vulnerability: Description of the vulnerability and its potential impact.\n", + "Recommendations: List of actionable steps to mitigate the vulnerability.\n", + "Recommended code: Recommended code snippet to remove the suspected vulnerability\n", + "\n", + "I am also providing a sample response output that you should follow-\n", + "-------------------------------------------------\n", + "**file_name: bulk/example_1.py**\n", + "\n", + "**Vulnerability Name:** Information Exposure\n", + "\n", + "**Vulnerability:** The `server_bad` function returns the entire traceback when an exception occurs\n", + ".This can leak sensitive information about the application's internals, such as file paths, variable names, and even the type of exception raised. Attackers could\n", + " exploit this information to gain a deeper understanding of the system and potentially launch further attacks.\n", + "\n", + "**Recommendations:**\n", + "\n", + "*Catch specific exceptions instead of using a broad `Exception` clause.\n", + "*Return a generic error message to the user without revealing internal details.\n", + "*Log the full traceback for debugging purposes, but do not expose it to the user.\n", + "\n", + "**Recommended code:**\n", + "\n", + "```python\n", + "sample code\n", + "```\n", + "\"\"\" # try your own prompt\n", + "\n", + "\n", + "generation_config = GenerationConfig(\n", + " temperature=0.5,\n", + " top_p=0.4,\n", + " top_k=24,\n", + " candidate_count=1,\n", + " max_output_tokens=8192,\n", + ")\n", + "responses = model.generate_content(\n", + " contents=my_prompt,\n", + " generation_config=generation_config,\n", + " stream=True,\n", + ")\n", + "\n", + "for res in responses:\n", + " print(res.text)" + ] + }, + { + "cell_type": "markdown", + "id": "cnuNp-heSUMD", + "metadata": { + "id": "cnuNp-heSUMD" + }, + "source": [ + "## Capture the model response into a single variable\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "M_gJqJWAhALS", + "metadata": { + "id": "M_gJqJWAhALS" + }, + "outputs": [], + "source": [ + "response_text = r\"\"\"\n", + " Security Analysis of Code Snippets\n", + "\n", + "### file_name: Bulk/example_1.py\n", + "\n", + "**Vulnerability Name:** Information Exposure Through Exception Handling\n", + "\n", + "**Vulnerability:** The `server_bad` function returns the entire traceback when an exception occurs. This can leak sensitive information about the application's internals, such\n", + " as file paths, variable names, and even the type of exception raised. Attackers could exploit this information to gain a deeper understanding of the system and potentially launch\n", + " further attacks.\n", + "\n", + "**Recommendations:**\n", + "\n", + "*Catch specific exceptions instead of using a broad `Exception` clause.\n", + "*Return a generic error message to the user without revealing internal details.\n", + "*Log the full traceback for debugging purposes, but do not expose it to the user.\n", + "\n", + "**Recommended code:**\n", + "\n", + "```python\n", + "@app.route('/bad')\n", + "def server_bad():\n", + " try:\n", + " result = do_computation()\n", + " return\n", + " result\n", + " except SpecificException as e:\n", + " # Log the error for debugging\n", + " app.logger.exception(\"An error occurred during computation\")\n", + " return \"An error occurred. Please try again later.\"\n", + "```\n", + "\n", + "### file_name: Bulk/example_10.py\n", + "\n", + "**Vulnerability Name:** Insecure Temporary File Creation\n", + "\n", + "**Vulnerability:** The `mktemp()` function creates temporary files with predictable names, which could allow attackers to guess the file names and access sensitive data. Additionally, the file permissions might be insecure, allowing unauthorized access.\n", + "\n", + "**Recommendations:**\n", + "\n", + "*Use the `tempfile .mkstemp()` function to create temporary files with more secure permissions and unpredictable names.\n", + "*Ensure that the temporary files are deleted after they are no longer needed.\n", + "\n", + "**Recommended code:**\n", + "\n", + "```python\n", + "import tempfile\n", + "\n", + "def write_results(results):\n", + " with tempfile.NamedTemporaryFile(mode=\"\n", + "w+\", delete=False) as f:\n", + " filename = f.name\n", + " f.write(results)\n", + " print(\"Results written to\", filename)\n", + " # ... process the file ...\n", + " os.remove(filename) # Delete the file when done\n", + "```\n", + "\n", + "### file_name: Bulk/example_100.py\n", + "\n", + "**Vulnerability Name:** Weak Key Generation Algorithm\n", + "\n", + "**Vulnerability:** The `DSA.generate(1024)` function generates a DSA key with a length of 1024 bits, which is considered weak and vulnerable to modern cryptanalysis techniques.\n", + "\n", + "\n", + "**Recommendations:**\n", + "\n", + "*Use a stronger key generation algorithm, such as RSA or ECC, with a key length of at least 2048 bits.\n", + "*Consider using libraries that provide secure defaults for key generation.\n", + "\n", + "**Recommended code:**\n", + "\n", + "```python\n", + "from Crypto.PublicKey import RSA\n", + "\n", + "\n", + "def generate_private_key():\n", + " key = RSA.generate(2048)\n", + " return key.export_key('PEM') # Export in a standard format\n", + "```\n", + "\n", + "### file_name: Bulk/example_101.py\n", + "\n", + "**Vulnerability Name:** Insecure Encryption Mode and Static IV\n", + "\n", + "**Vulnerability:** The code uses AES in CBC mode with a static initialization vector (IV). Using a static IV weakens the encryption and makes it vulnerable to certain attacks, especially when encrypting multiple messages with the same key.\n", + "\n", + "**Recommendations:**\n", + "\n", + "*Use a random IV for each encryption operation.\n", + "*Consider using more secure modes of operation, such as GCM or CTR, which provide authenticated encryption.\n", + "\n", + "**Recommended code:**\n", + "\n", + "```python\n", + "from Crypto.Cipher import AES\n", + "from Crypto.Random import get_random_bytes\n", + "\n", + "def getEncryptionKey(data, key):\n", + " iv\n", + " = get_random_bytes(AES.block_size)\n", + " cipher = AES.new(key, AES.MODE_GCM, iv)\n", + " ciphertext, tag = cipher.encrypt_and_digest(data)\n", + " return iv, ciphertext, tag\n", + "```\n", + "\n", + "### file_name: Bulk/example_102.py\n", + "\n", + "**Vulnerability Name:** Insecure FTP Usage and Hardcoded Credentials\n", + "\n", + "**Vulnerability:** The code uses FTP, which transmits data in plaintext, including usernames and passwords. Additionally, the credentials are hardcoded in the script, making them easily discoverable.\n", + "\n", + "**Recommendations:**\n", + "\n", + "\n", + "*Use a secure file transfer protocol, such as SFTP or FTPS, which encrypts the communication.\n", + "*Avoid hardcoding credentials and instead use environment variables or a secure credential storage mechanism.\n", + "\n", + "**Recommended code:**\n", + "\n", + "```python\n", + "import pysftp # Assuming you have the pysftp\n", + " library installed\n", + "\n", + "def downloadFile(hostname, username, password, sensitive_file):\n", + " with pysftp.Connection(hostname, username=username, password=password) as sftp:\n", + " sftp.get(sensitive_file, preserve_mtime=True)\n", + "```\n", + "\n", + "### file_name:Bulk/example_103.py\n", + "\n", + "**Vulnerability Name:** Potential Issues with Password Hashing\n", + "\n", + "**Vulnerability:** While PBKDF2 is a good choice for password hashing, the code uses a static salt, which reduces its effectiveness against rainbow table attacks.\n", + "\n", + "**Recommendations:**\n", + "\n", + "*Use a unique salt for each password. This can be achieved by generating a random salt for each user and storing it securely alongside the hashed password.\n", + "\n", + "**Recommended code:**\n", + "\n", + "```python\n", + "import os\n", + "\n", + "def getHash(password):\n", + " salt = os.urandom(16) # Generate a random\n", + " salt\n", + " hash = pbkdf2_hmac('sha256', password, salt, 100000)\n", + " return salt, hash # Store the salt with the hash\n", + "```\n", + "\n", + "### file_name: Bulk/example_104.py\n", + "\n", + "**Vulnerability Name:** Hardcoded and Weak Admin Password\n", + "\n", + "**Vulnerability:** The code uses a hardcoded and easily guessable password for the admin user. This makes the system extremely vulnerable to unauthorized access.\n", + "\n", + "**Recommendations:**\n", + "\n", + "*Never hardcode passwords, especially for privileged accounts.\n", + "*Enforce strong password policies that require a minimum length, complexity, and regular password changes.\n", + "*Consider using multi-factor authentication for additional security.\n", + "\n", + "**Recommended code:**\n", + "\n", + "```python\n", + "# Implement a secure password storage and verification mechanism, such as bcrypt or Argon2\n", + "# and avoid hardcoding passwords.\n", + "```\n", + "\n", + "### file_name: Bulk/example_105.py\n", + "\n", + "**Vulnerability Name:** Open Redirect Vulnerability\n", + "\n", + "**Vulnerability:** The `flask_redirect` function redirects the user to any URL provided in the `next` parameter without validation. This can be exploited by attackers to redirect users to malicious websites.\n", + "\n", + "**Recommendations:**\n", + "\n", + "*Validate the `next` parameter to ensure it points to a trusted domain or a specific set of allowed URLs.\n", + "\n", + "**Recommended code:**\n", + "\n", + "```python\n", + "from urllib.parse import urlparse\n", + "\n", + "@app.route('flask_redirect')\n", + "def flask_redirect():\n", + " url = request\n", + ".args.get(\"next\")\n", + " parsed_url = urlparse(url)\n", + " if parsed_url.netloc in allowed_domains: # Replace with your allowed domains\n", + " return redirect(url)\n", + " else:\n", + " return \"Invalid redirect URL\"\n", + "```\n", + "\n", + "### file_name: Bulk/example_106.py\n", + "\n", + "**Vulnerability Name:** Potential XXE (XML External Entity) Vulnerability\n", + "\n", + "**Vulnerability:** The code parses XML data from the request without disabling external entity processing. This could allow attackers to inject malicious entities that can lead to information disclosure, denial of service\n", + ", or server-side request forgery (SSRF) attacks.\n", + "\n", + "**Recommendations:**\n", + "\n", + "* Disable external entity processing when parsing XML data.\n", + "* Use a library that provides secure XML parsing options.\n", + "\n", + "**Recommended code:**\n", + "\n", + "```python\n", + "from defusedxml import lxml\n", + "\n", + "@app.post(\"/\n", + "upload\")\n", + "def upload():\n", + " xml_src = request.get_data()\n", + " parser = lxml.XMLParser(resolve_entities=False)\n", + " doc = lxml.fromstring(xml_src, parser=parser)\n", + " return lxml.tostring(doc)\n", + "\n", + "```\n", + "\n", + "### file_name: Bulk/example_107.py\n", + "\n", + "**Vulnerability Name:** Potential XXE and Insecure Password Handling\n", + "\n", + "**Vulnerability:** Similar to example_106.py, this code is vulnerable to XXE attacks due to insecure XML parsing. Additionally, the `check\n", + "Password` function is not implemented, and it's unclear how passwords are stored and verified.\n", + "\n", + "**Recommendations:**\n", + "\n", + "*Address the XXE vulnerability as described in example_106.py.\n", + "*Implement a secure password storage and verification mechanism, such as bcrypt or Argon2.\n", + "*Avoid storing passwords in plaintext or using weak hashing algorithms.\n", + "\n", + "**Recommended code:**\n", + "\n", + "```python\n", + "# Implement secure XML parsing as shown in example_106.py\n", + "# Implement a secure password storage and verification mechanism\n", + "```\n", + "\"\"\"" + ] + }, + { + "cell_type": "markdown", + "id": "1tK5AtEdIeep", + "metadata": { + "id": "1tK5AtEdIeep" + }, + "source": [ + "### Parse the response and export it into a csv for further analysis.\n", + "\n", + "To parse the response we have used regular expression pattern matching. This is easy to configure and maintain\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "yVethA0vhsiv", + "metadata": { + "id": "yVethA0vhsiv" + }, + "outputs": [], + "source": [ + "import re\n", + "\n", + "import pandas as pd\n", + "\n", + "\n", + "def extract_vulnerability_data(text):\n", + " # Regular expressions for extracting data\n", + " file_pattern = r\"###\\s*file_name:\\s*(.*\\.py)\" # Adjusted for \"file_name\"\n", + " vulnerability_name_pattern = r\"\\*\\*Vulnerability Name:\\*\\*\\s*(.*)\"\n", + " vulnerability_pattern = r\"\\*\\*Vulnerability:\\*\\*\\s*(.*?)(?=\\*\\*Recommendations)\"\n", + " recommendation_pattern = r\"\\*\\*Recommendations:\\*\\*\\s*((?:\\*.*\\n)+)\"\n", + " code_pattern = r\"```python(.*?)```\" # Added pattern for recommended code\n", + "\n", + " data = []\n", + "\n", + " # Iterate through each vulnerability report\n", + " for match in re.finditer(r\"###.*?(?=###|$)\", text, re.DOTALL):\n", + " report = match.group(0)\n", + "\n", + " file_name = re.search(file_pattern, report).group(1)\n", + " vulnerability_name = re.search(vulnerability_name_pattern, report).group(1)\n", + " vulnerability = (\n", + " re.search(vulnerability_pattern, report, re.DOTALL).group(1).strip()\n", + " )\n", + " recommendations = (\n", + " re.search(recommendation_pattern, report, re.DOTALL).group(1).strip()\n", + " )\n", + "\n", + " # Extract recommended code, handling potential absence\n", + " code_match = re.search(code_pattern, report, re.DOTALL)\n", + " recommended_code = code_match.group(1).strip() if code_match else \"N/A\"\n", + "\n", + " data.append(\n", + " {\n", + " \"File Number\": file_name.split(\"_\")[-1].split(\".\")[0],\n", + " \"File Name\": file_name,\n", + " \"Vulnerability Name\": vulnerability_name,\n", + " \"Description\": vulnerability,\n", + " \"Recommendations\": recommendations,\n", + " \"Recommended Code\": recommended_code, # Include recommended code\n", + " }\n", + " )\n", + "\n", + " return pd.DataFrame(data)\n", + "\n", + "\n", + "# Extract data and create DataFrame\n", + "df = extract_vulnerability_data(response_text)\n", + "\n", + "# Display DataFrame in Colab\n", + "display(df)\n", + "\n", + "# Save DataFrame to CSV file\n", + "df.to_csv(\"vulnerability_report_BULK.csv\", index=False)\n", + "\n", + "print(\"CSV file 'vulnerability_report_BULK.csv' created successfully.\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ipY3TXNUieVo", + "metadata": { + "id": "ipY3TXNUieVo" + }, + "outputs": [ + { + "data": { + "application/javascript": "\n async function download(id, filename, size) {\n if (!google.colab.kernel.accessAllowed) {\n return;\n }\n const div = document.createElement('div');\n const label = document.createElement('label');\n label.textContent = `Downloading \"${filename}\": `;\n div.appendChild(label);\n const progress = document.createElement('progress');\n progress.max = size;\n div.appendChild(progress);\n document.body.appendChild(div);\n\n const buffers = [];\n let downloaded = 0;\n\n const channel = await google.colab.kernel.comms.open(id);\n // Send a message to notify the kernel that we're ready.\n channel.send({})\n\n for await (const message of channel.messages) {\n // Send a message to notify the kernel that we're ready.\n channel.send({})\n if (message.buffers) {\n for (const buffer of message.buffers) {\n buffers.push(buffer);\n downloaded += buffer.byteLength;\n progress.value = downloaded;\n }\n }\n }\n const blob = new Blob(buffers, {type: 'application/binary'});\n const a = document.createElement('a');\n a.href = window.URL.createObjectURL(blob);\n a.download = filename;\n div.appendChild(a);\n a.click();\n div.remove();\n }\n ", + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/javascript": "download(\"download_aab82d58-53a1-40c6-be64-e5bb8ff671f5\", \"vulnerability_report_BULK.csv\", 9660)", + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# Display the created CSV file (assuming you have the 'google.colab' library installed)\n", + "from google.colab import files\n", + "\n", + "files.download(\"vulnerability_report_BULK.csv\")" + ] + }, + { + "cell_type": "markdown", + "id": "7-kVfA_VIVv8", + "metadata": { + "id": "7-kVfA_VIVv8" + }, + "source": [ + "## Parse the response and export it into JSON output.\n", + "\n", + "NOTE: The following process is just a way to capture the response text and parse it further with regular expression. With Gemini 2.0, we can force the model to respond in JSON structure as well." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "F3hgDQcbn5YO", + "metadata": { + "id": "F3hgDQcbn5YO" + }, + "outputs": [], + "source": [ + "# Extract data and write to JSON file (assuming you have 'response_text' defined)\n", + "data = extract_vulnerability_data(response_text)\n", + "\n", + "data.to_json(\"vulnerabilities.json\", indent=4)\n", + "\n", + "print(\"Vulnerability data extracted and saved to vulnerabilities.json\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "u-jhc3_Qpx9c", + "metadata": { + "id": "u-jhc3_Qpx9c" + }, + "outputs": [ + { + "data": { + "application/javascript": "\n async function download(id, filename, size) {\n if (!google.colab.kernel.accessAllowed) {\n return;\n }\n const div = document.createElement('div');\n const label = document.createElement('label');\n label.textContent = `Downloading \"${filename}\": `;\n div.appendChild(label);\n const progress = document.createElement('progress');\n progress.max = size;\n div.appendChild(progress);\n document.body.appendChild(div);\n\n const buffers = [];\n let downloaded = 0;\n\n const channel = await google.colab.kernel.comms.open(id);\n // Send a message to notify the kernel that we're ready.\n channel.send({})\n\n for await (const message of channel.messages) {\n // Send a message to notify the kernel that we're ready.\n channel.send({})\n if (message.buffers) {\n for (const buffer of message.buffers) {\n buffers.push(buffer);\n downloaded += buffer.byteLength;\n progress.value = downloaded;\n }\n }\n }\n const blob = new Blob(buffers, {type: 'application/binary'});\n const a = document.createElement('a');\n a.href = window.URL.createObjectURL(blob);\n a.download = filename;\n div.appendChild(a);\n a.click();\n div.remove();\n }\n ", + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/javascript": "download(\"download_7ee96007-491b-4a8f-b52a-6b7d5e63e4b7\", \"vulnerabilities.json\", 11541)", + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "from google.colab import files\n", + "\n", + "files.download(\"vulnerabilities.json\")" + ] + }, + { + "cell_type": "markdown", + "id": "qbk8xpGJUASo", + "metadata": { + "id": "qbk8xpGJUASo" + }, + "source": [ + "## Conclusions\n", + "\n", + "In this notebook we have successfully leveraged Gemini 2.0's code scanning and code generation capability to\n", + "1. Analyze multiple python files for potential vulnerabilities\n", + "2. Used Gemini 2.0 to provide recommendations (both in wordings and in the form of code)\n", + "3. Export the details into csv and json for further analysis\n", + "\n", + "The scope of this experiment is limited to identifying issues and providing helpful and contextual modification. Automating remediations or fitting the findings into a review workflow would exist in a more mature tool, and hasn't been considered as part of the experiment. While Gemini 2.0 demonstrates promising capabilities in code analysis, it's important to note that this approach is still experimental. We believe that it is important to explore the potential of this technology for vulnerability detection, and continue development and validation efforts before it can be considered a robust security tool" + ] + } + ], + "metadata": { + "colab": { + "name": "code_scanning_and_vulnerability_detection.ipynb", + "toc_visible": true + }, + "kernelspec": { + "display_name": "Python 3", + "name": "python3" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} diff --git a/gemini/use-cases/code/refactor_legacy_code.ipynb b/gemini/use-cases/code/refactor_legacy_code.ipynb new file mode 100644 index 0000000..cc97429 --- /dev/null +++ b/gemini/use-cases/code/refactor_legacy_code.ipynb @@ -0,0 +1,1115 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "f705f4be70e9" + }, + "outputs": [], + "source": [ + "# Copyright 2025 Google LLC\n", + "#\n", + "# Licensed under the Apache License, Version 2.0 (the \"License\");\n", + "# you may not use this file except in compliance with the License.\n", + "# You may obtain a copy of the License at\n", + "#\n", + "# https://www.apache.org/licenses/LICENSE-2.0\n", + "#\n", + "# Unless required by applicable law or agreed to in writing, software\n", + "# distributed under the License is distributed on an \"AS IS\" BASIS,\n", + "# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n", + "# See the License for the specific language governing permissions and\n", + "# limitations under the License." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "j1cu3boIga_X" + }, + "source": [ + "# Reduce Tech Debt with Gemini 3 Pro\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
    \n", + " \n", + " \"Google
    Open in Colab\n", + "
    \n", + "
    \n", + " \n", + " \"Google
    Open in Colab Enterprise\n", + "
    \n", + "
    \n", + " \n", + " \"Vertex
    Open in Vertex AI Workbench\n", + "
    \n", + "
    \n", + " \n", + " \"BigQuery
    Open in BigQuery Studio\n", + "
    \n", + "
    \n", + " \n", + " \"GitHub
    View on GitHub\n", + "
    \n", + "
    \n", + "\n", + "
    \n", + "\n", + "

    \n", + "Share to:\n", + "\n", + "\n", + " \"LinkedIn\n", + "\n", + "\n", + "\n", + " \"Bluesky\n", + "\n", + "\n", + "\n", + " \"X\n", + "\n", + "\n", + "\n", + " \"Reddit\n", + "\n", + "\n", + "\n", + " \"Facebook\n", + "\n", + "

    " + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "83f287773529" + }, + "source": [ + "| Authors |\n", + "| --- |\n", + "| [Dennis Kashkin](https://github.com/dkashkin) |\n", + "| [Holt Skinner](https://github.com/holtskinner) |" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "308adc2720c1" + }, + "source": [ + "## Overview: Escaping \"Legacy Land\"\n", + "\n", + "We all know the code. The ten-year-old library that nobody quite understands anymore. You don't want to be the one who breaks it, but you can't ignore it forever. The struggle is so relatable that it inspired [Forrest, Amanda, and Holt to sing about it at Next '23](https://goo.gle/legacy-land).\n", + "\n", + "But while the fear is real, so is the need for modernization. Security, performance, and scalability demand that we refactor.\n", + "\n", + "**About this Notebook**\n", + "We are going to look at a new approach to this old problem. utilizing **Gemini 3 Pro on Vertex AI**. We will look at how to leverage the model not just for faster coding, but for analyzing dependencies and executing safer, more reliable code modernization." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "VbTqt86rrbWQ" + }, + "source": [ + "## Prerequisites\n", + "\n", + "Before starting, ensure you have the necessary dependencies and environment set up.\n", + "\n", + "*(Note: In a Jupyter environment, you may need to restart the kernel after installing new packages.)*" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "OzilzKKFrd2m" + }, + "outputs": [], + "source": [ + "%pip install -q --upgrade google-genai pyparseit" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "XRGl-_glga_Z" + }, + "source": [ + "### Authenticate your notebook environment\n", + "\n", + "If you are running this notebook in **Google Colab**, run the cell below to authenticate your account." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "rK-IZVyzga_Z" + }, + "outputs": [], + "source": [ + "import sys\n", + "\n", + "if \"google.colab\" in sys.modules:\n", + " from google.colab import auth\n", + "\n", + " auth.authenticate_user()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "b6d46b3a5abf" + }, + "source": [ + "### Set Google Cloud project information\n", + "\n", + "To get started using Vertex AI, you must have an existing Google Cloud project and [enable the Vertex AI API](https://console.cloud.google.com/flows/enableapi?apiid=aiplatform.googleapis.com).\n", + "\n", + "Learn more about [setting up a project and a development environment](https://cloud.google.com/vertex-ai/docs/start/cloud-environment)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "05dab3326853" + }, + "outputs": [], + "source": [ + "import os\n", + "\n", + "from google import genai\n", + "\n", + "# fmt: off\n", + "PROJECT_ID = \"[your-project-id]\" # @param {type: \"string\", placeholder: \"[your-project-id]\", isTemplate: true}\n", + "LOCATION = \"global\" # @param {type: \"string\", placeholder: \"[your-project-id]\", isTemplate: true}\n", + "# fmt: on\n", + "\n", + "if not PROJECT_ID or PROJECT_ID == \"[your-project-id]\":\n", + " PROJECT_ID = str(os.environ.get(\"GOOGLE_CLOUD_PROJECT\"))\n", + "if not LOCATION:\n", + " LOCATION = os.environ.get(\"GOOGLE_CLOUD_REGION\")\n", + "\n", + "client = genai.Client(vertexai=True, project=PROJECT_ID, location=LOCATION)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "935c5cca7fb3" + }, + "source": [ + "### Load model" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "6178371d0b7a" + }, + "outputs": [], + "source": [ + "MODEL_ID = \"gemini-3.1-pro-preview\" # @param {type:\"string\"}" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "bc778f7deef4" + }, + "source": [ + "### Import libraries and create helper functions" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "778300e059d9" + }, + "outputs": [], + "source": [ + "import base64\n", + "import logging\n", + "import re\n", + "\n", + "import markdown\n", + "from IPython.display import HTML, display\n", + "from google.genai import types\n", + "from pyparseit import parse_markdown_string\n", + "\n", + "logging.disable(level=logging.WARNING)\n", + "\n", + "\n", + "def call_llm(prompt: str) -> str:\n", + " \"\"\"Simple wrapper for model inference.\"\"\"\n", + " print(f\"Calling {MODEL_ID} with {len(prompt)} chars...\")\n", + " response = client.models.generate_content(\n", + " model=MODEL_ID,\n", + " contents=prompt,\n", + " config=types.GenerateContentConfig(\n", + " temperature=1.0,\n", + " thinking_config=types.ThinkingConfig(thinking_budget=16384),\n", + " ),\n", + " )\n", + " # Basic cleanup of markdown code blocks if present\n", + " snippets = parse_markdown_string(response.text, language=\"python\")\n", + " return snippets[0].content\n", + "\n", + "\n", + "def generate_and_save(template_path: str, output_path: str, replacements: dict) -> str:\n", + " \"\"\"Helper to load a prompt template, inject variables, call LLM, and save the result.\"\"\"\n", + " with open(template_path) as f:\n", + " prompt = f.read()\n", + "\n", + " for key, value in replacements.items():\n", + " prompt = prompt.replace(f\"{{{key}}}\", value)\n", + "\n", + " print(f\"Generating from {template_path}...\")\n", + " content = call_llm(prompt)\n", + "\n", + " with open(output_path, \"w\") as f:\n", + " f.write(content)\n", + " return content\n", + "\n", + "\n", + "def render_markdown_with_mermaid(markdown_string: str) -> None:\n", + " \"\"\"Renders a string containing markdown and mermaid diagrams inline.\"\"\"\n", + " # Placeholder and storage logic is identical to the cell magic\n", + " mermaid_placeholder = \"---MERMAID_PLACEHOLDER_{}---\"\n", + " mermaid_blocks = re.findall(r\"```mermaid\\n(.*?)\\n```\", markdown_string, re.DOTALL)\n", + "\n", + " temp_md = markdown_string\n", + " for i, block in enumerate(mermaid_blocks):\n", + " temp_md = temp_md.replace(\n", + " f\"```mermaid\\n{block}\\n```\", mermaid_placeholder.format(i), 1\n", + " )\n", + "\n", + " html_output = markdown.markdown(\n", + " temp_md, extensions=[\"extra\", \"fenced_code\", \"tables\"]\n", + " )\n", + "\n", + " for i, block in enumerate(mermaid_blocks):\n", + " encoded_mermaid = base64.b64encode(block.encode(\"utf-8\")).decode(\"utf-8\")\n", + " mermaid_html = f\"\"\"\n", + "
    \"\"\"\n", + " html_output = html_output.replace(mermaid_placeholder.format(i), mermaid_html)\n", + "\n", + " display(HTML(html_output))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "2At_vpZvga_b" + }, + "source": [ + "### Create necessary directories." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "gQhCIhZCYxs2" + }, + "outputs": [], + "source": [ + "import os\n", + "\n", + "DIRS = [\n", + " \"original_code\",\n", + " \"original_tests\",\n", + " \"output/tests\",\n", + " \"output/specs\",\n", + " \"output/design_docs\",\n", + " \"output/code\",\n", + " \"prompts\",\n", + "]\n", + "for d in DIRS:\n", + " os.makedirs(d, exist_ok=True)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "uTlfZ5Grga_b" + }, + "source": [ + "#### Load Prompts\n", + "\n", + "Load the specialized prompts for each stage of the pipeline." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "dOoN54WMga_b" + }, + "outputs": [], + "source": [ + "%%writefile prompts/create_tests.md\n", + "\n", + "You are a world-class software engineer specialized in writing comprehensive unit tests for specific Python modules.\n", + "\n", + "\n", + "\n", + "Your goal is to write a robust set of \"Passing Tests\" for the Python module provided in the `source_code` tag below.\n", + "\"Passing Tests\" are unit tests that cover existing, valid code paths and are expected to pass.\n", + "Do not write any \"Failing Tests\" that are expected to result in errors based on the current version of source code. For example, if you see insufficient validation of input parameters or lack of exception handling, do not write any tests that target such known gaps.\n", + "\n", + "\n", + "\n", + "You must follow these instructions:\n", + "- You must use Python's standard framework \"unittest\"\n", + "- You must write a complete Python module including all required import statements\n", + "- Wrap all unit tests into one test class inherited from unittest.TestCase\n", + "- The source code of the tested module must be imported using the following exact code:\n", + "\n", + "```python\n", + "import importlib\n", + "import os\n", + "import sys\n", + "\n", + "sys.path.insert(0, os.getcwd())\n", + "PACKAGE_PREFIX = os.environ.get('PACKAGE_PREFIX', '{code_dir_path}')\n", + "module = importlib.import_module(f'{PACKAGE_PREFIX}.{module_name}')\n", + "```\n", + "\n", + "- Include the following code at the very end of the Python module with the unit test class:\n", + "\n", + "```python\n", + "if __name__ == '__main__':\n", + " unittest.main()\n", + "```\n", + "\n", + "\n", + "\n", + "\n", + "Format your response as a single Markdown code block with the full source code of the Python module containing all unit tests.\n", + "\n", + "\n", + "\n", + "# A Comprehensive Guide to Unit Testing Python Modules\n", + "\n", + "Writing effective unit tests is a critical skill in software engineering. When testing a Python module with multiple classes and functions, the scope expands from testing individual functions in isolation to verifying the collaborative behavior of those methods and managing the state of class instances. A key metric for a thorough test suite is code coverage, which measures the percentage of your code executed by your tests. While aiming for 100% coverage is an excellent goal that builds confidence and reduces bugs, remember that it is a tool, not the ultimate goal. A meaningful test that verifies the class's public behavior is always more valuable than a test that simply executes a line of code.\n", + "\n", + "## 1. Understand the Role and Public Interface of every class and function\n", + "\n", + "Before writing a single test, you must thoroughly understand the classes and functions you intend to test. This involves looking at the bigger picture of how each class and its methods are intended to be used.\n", + "\n", + "* **Analyze the Responsibilities**: Read the code, docstrings, and any related specifications to understand the purpose, the public methods (the intended interface), and the interactions with other components.\n", + "* **Focus on Public Behavior**: Prioritize testing the public methods of classes, as these define how other parts of your code will interact with it. While it's possible to test private methods in Python, it's often a sign that your class may have too many responsibilities and could be a candidate for refactoring. Testing through the public interface helps ensure your tests aren't tightly coupled to implementation details, making them less brittle to future code changes.\n", + "* **Identify Method Interactions and State Changes**: Map out how the methods within each class interact with each other. Does one method's execution affect the state of the object, which in turn influences the outcome of another method? These interactions are crucial to test.\n", + "* **Map Out All Code Paths**: For each public method, and any critical private methods that contain complex logic, you must identify and test all execution paths created by conditional logic.\n", + " * **Conditionals**: Every `if`, `elif`, and `else` statement creates a branch that needs to be tested.\n", + " * **Loops**: Test scenarios where a loop runs zero, one, or multiple times. Also, test any `break` or `continue` statements that alter the loop's flow.\n", + " * **Exception Handling**: Ensure your tests trigger each `try` block's success path and every corresponding `except` block's failure path.\n", + " * **Early Returns**: Functions with guard clauses or multiple return statements have exits that must all be tested.\n", + "\n", + "## 2. Design Test Cases Systematically\n", + "\n", + "A structured approach to test case design ensures that you cover all expected behaviors, boundaries, and failure scenarios.\n", + "\n", + "* **Equivalence Partitioning**: Divide possible inputs for each method into groups that should be handled similarly. For example, a method processing a user's age might have partitions for valid ages (e.g., 18-65), underage (e.g., 0-17), and invalid inputs (e.g., negative numbers or non-integer types). Test one representative from each group.\n", + "* **Boundary Value Analysis**: Pay special attention to the edges of your input ranges. For an age range of 18-65, you should test the values 17, 18, 65, and 66.\n", + "* **Test the \"Happy Path\"**: Start with at least one test for the normal, expected use case of each public method with valid inputs. For a class, this may involve a sequence of method calls that represent a typical interaction.\n", + "* **Cover Edge Cases and Invalid Inputs**: This is where many bugs hide. Your tests should cover:\n", + " * **Empty or Null Inputs**: `None`, empty strings (`\"\"`), and empty collections (`[]`, `{}`).\n", + " * **Numeric Boundaries**: `0`, `-1`, and very large or small numbers.\n", + " * **Invalid Types**: Pass a string where an integer is expected to ensure proper error handling.\n", + "* **Test State Transitions**: For stateful classes, design tests that specifically check the transition of the object's state after a method is called. For instance, if a `user.deactivate()` method is called, a subsequent call to `user.is_active()` should return `False`.\n", + "\n", + "## 3. Leverage Mocking\n", + "\n", + "Classes and functions often depend on external systems like databases, APIs, or the file system. To test your logic in isolation, you must use **mocking**. Mocking involves replacing these external dependencies with controlled objects that simulate their behavior.\n", + "\n", + "* **Why Mock?**: Mocking makes your tests faster, more reliable, and independent of external systems. It allows you to simulate success, failure (e.g., an API timeout), and edge case responses from dependencies without making real network or disk operations.\n", + "* **How to Mock**: Python's `unittest.mock` library provides tools like `@patch` to replace objects within a test's scope. When mocking, it's a good practice to use `autospec=True` to ensure your mock has the same interface as the object it's replacing.\n", + "* **Dependency Injection**: A powerful technique to make your functions and classes more testable is dependency injection. Instead of a class creating its own dependencies internally, you provide them as arguments to the constructor. This makes it easy to substitute a real dependency with a mock during testing.\n", + "\n", + "**Example: Mocking an API Call in a Class Method**\n", + "\n", + "```python\n", + "from unittest.mock import patch, Mock\n", + "\n", + "# Assuming 'user_service.get_user_data' calls 'requests.get'\n", + "@patch('my_app.user_service.requests.get')\n", + "def test_get_user_data_success(mock_get):\n", + " # Configure the mock to return a successful response\n", + " mock_response = Mock()\n", + " mock_response.json.return_value = {'id': 1, 'name': 'Jane Doe'}\n", + " mock_get.return_value = mock_response\n", + "\n", + " # Instantiate the service and call the method\n", + " service = UserService()\n", + " user_data = service.get_user_data(1)\n", + "\n", + " # Assert the result\n", + " assert user_data['name'] == 'Jane Doe'\n", + "```\n", + "\n", + "## 4. Recommended Workflow\n", + "\n", + "1. **Write Initial Tests**: Begin by writing tests for the primary functionality, focusing on the public methods and their expected outcomes (the \"happy path\").\n", + "2. **Manage Test State with `setUp` and `tearDown`**: Use the `setUp` method in your test class to create a clean instance before each test method runs. This ensures that your tests are independent and don't influence each other. The `tearDown` method can be used to clean up any resources after each test.\n", + "3. **Identify Gaps**: Review the code to find uncovered lines, branches, and method interactions.\n", + "4. **Write Targeted Tests**: For each missed scenario, determine the specific inputs, method call sequences, or mock behaviors needed to execute that code path. Write a new test case targeting that exact situation.\n", + "5. **Refactor for Clarity**: As you add tests, keep them clean and readable. Use descriptive names (e.g., `test_user_deactivation_sets_is_active_to_false`) and consider using parameterized tests to avoid duplicating test logic when testing similar inputs.\n", + "\n", + "## 5. Advanced Techniques and Best Practices\n", + "\n", + "1. **Parameterize Tests**: To test a method with multiple inputs using the same logic, use a loop with `subTest` or a parameterized testing library like `pytest`. This keeps your test suite DRY (Don't Repeat Yourself).\n", + "2. **Verify State and Side Effects**: A test should not only check a method's return value but also verify any intended side effects, such as changes to the object's attributes or calls to a mocked dependency.\n", + "3. **Test for Exceptions**: Use `assertRaises` (as a context manager or method) to confirm that a method correctly raises an exception with invalid input.\n", + "4. **Keep Tests Independent**: Each test case should be able to run on its own and in any order. Avoid creating dependencies between tests where one test relies on the state left by a previous one.\n", + "5. **Use Descriptive Naming**: Test methods should have long, descriptive names that clearly state what they are testing. This makes test results easier to understand and serves as a form of documentation.\n", + "\n", + "\n", + "\n", + "{source_code}\n", + "" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "I7r4Di-Xga_c" + }, + "outputs": [], + "source": [ + "%%writefile prompts/create_spec.md\n", + "\n", + "\n", + "You are a world-class software architect specialized in designing comprehensive specifications intended for junior developers.\n", + "\n", + "\n", + "\n", + "Review the source code of the Python module provided in the `source_code` tag below and write a comprehensive Software Requirements Specification for this module by strictly following the process defined in the \"process\" tag below.\n", + "\n", + "\n", + "\n", + "1. The specification must be written in plain English language, without any Python code snippets.\n", + "2. You can include diagrams in Mermaid format.\n", + "3. You must follow the Specification Design Process provided in the `process` tag below.\n", + "\n", + "\n", + "\n", + "## Guiding Principles\n", + "\n", + "1. **Strict Contract**: The resulting SRS is a non-negotiable contract. Deviations in the final implementation, even for technical improvement, are considered defects.\n", + "2. **Independent Resolution**: You MUST resolve all ambiguities autonomously based on currently available information (code, comments, tests). Do not rely on external clarification. Make reasonable engineering assumptions based on the simplest non-crashing behavior or standard patterns to ensure deterministic outcomes.\n", + "3. **Absolute Completeness**: No placeholders (e.g., \"etc.\", \"such as\", \"e.g.\"). Every list, enum, dictionary key set, attribute, method, and constant MUST be exhaustively enumerated.\n", + "4. **Precision and Clarity**: Avoid vague terms. Define any domain-specific terms used. Specifications MUST be unambiguous and specific.\n", + "\n", + "## Phase 1: Scope Definition and Strategic Planning\n", + "\n", + "Establish the boundaries of the documentation effort, ensuring no part of the exposed surface area is omitted.\n", + "\n", + "1. **Define Core Objective**: Produce a complete SRS for all components within the target Python module.\n", + "2. **Identify the Complete Module Namespace**: The scope is strictly defined by the *entire* module namespace. This includes ALL publicly accessible components, protected components, and any private components accessed by the test suite:\n", + " * Classes and their full inheritance hierarchies.\n", + " * Module-level functions and helper utilities.\n", + " * Exceptions, constants, type variables, and protocol definitions.\n", + " * Any internal component (even those prefixed with `_`) if it is accessed by the existing test suite, treating such access as defining a \"hidden\" public contract.\n", + " * Required module-level imports, including from `__future__`.\n", + "3. **Conditional Features**: Identify features or classes available only under certain conditions (e.g., OS, Python version, presence of other modules). The SRS MUST specify exactly how their absence is reflected in the module namespace. These components SHALL NOT be defined in the namespace if the condition is not met.\n", + " * *Example:* \"`KqueueSelector` class SHALL be present in the module namespace if and only if `select.kqueue` is available.\"\n", + "\n", + "## Phase 2: Static Analysis and Architectural Deconstruction\n", + "\n", + "Examine the source code without execution to map its structure, contracts, and dependencies with absolute precision.\n", + "\n", + "1. **Namespace and Hierarchy Cataloging**:\n", + " * Create a definitive list of every name exported by the module.\n", + " * Formalize all class hierarchies, explicitly documenting required naming conventions and exact inheritance trees.\n", + " * List all required module-level constants with their types and expected values.\n", + " * *Example:* `DEFAULT_TIMEOUT: final[int] = 10`\n", + " * List all required imports, including from `__future__`. Verify the existence and exact names of any functions or attributes assumed to be imported from other modules.\n", + " * *Example:* \"SHALL import `annotations` from `__future__`.\"\n", + " * *Constraint Check*: Ensure NO \"etc.\" or \"e.g.\" is used. All items MUST be listed.\n", + "\n", + "2. **Comprehensive API Extraction**:\n", + " * **Classes**:\n", + " * List all attributes (public, protected, private): Specify name, type, access level (public, protected, private), and if it's a property or a plain instance/class attribute.\n", + " * *Example:* `_is_open: bool` - Instance Attribute, Private, Read/Write.\n", + " * *Example:* `@property max_size(self) -> int` - Property, Public, Read-Only.\n", + " * List all methods (public, protected, private): Provide the full signature, including parameter types, return types, and decorators (`@staticmethod`, `@classmethod`). Pay close attention to dunder methods, ensuring correct parameters (e.g., `self`).\n", + " * *Example:* `def _process_data(self, data: bytes, config: Dict[str, Any]) -> bool:`\n", + " * *Example:* `def __len__(self) -> int:`\n", + " * **Module-Level Callables (Functions)**: Extract the *exact* signature for every callable, including type hints for all parameters and return values. Explicit documentation for `*args` and `**kwargs` handling is mandatory.\n", + " * **Data Structures**: Deconstruct all Data Transfer Objects (DTOs), enums, and configuration dictionaries. Every allowed key or member MUST be enumerated. Use type hints to define the structure of complex types.\n", + " * *Example:* Config dict structure: `{\"timeout\": int, \"retries\": int, \"protocol\": Literal[\"tcp\", \"udp\"]}`\n", + " * Specify mutability where important (e.g., `set` vs. `frozenset`, `List` vs. `Tuple`).\n", + "\n", + "## Phase 3: Behavioral Analysis and Protocol Reverse-Engineering\n", + "\n", + "Understand dynamic behavior, state transitions, and implicit Python protocols through rigorous examination of code and tests.\n", + "\n", + "1. **Test Suite Deconstruction as Source of Truth**:\n", + " * Analyze every test case to extract hard input-output scenarios and edge case handling.\n", + " * Treat mocked interactions in tests as rigid requirements for external dependencies.\n", + " * Identify \"hidden\" public contracts: If a test accesses an attribute named `_internal_cache`, that attribute MUST be documented as part of the specification as if it were public for testing purposes.\n", + "\n", + "2. **Implicit Protocol Identification**:\n", + " * Identify all implemented \"dunder\" methods (e.g., `__add__`, `__iter__`, `__call__`, `__getstate__`, `__str__`, `__repr__`).\n", + " * Document these not just as methods, but as formal adherence to Python protocols (e.g., \"Implements the `Iterable` protocol via `__iter__`\").\n", + "\n", + "3. **Logic, Side Effect, and Algorithm Mapping**:\n", + " * Use clear, sequential steps (e.g., numbered lists) and imperative language (\"SHALL\", \"MUST\") to describe method/function behavior.\n", + " * Specify the order of operations, especially when interactions or side effects are involved.\n", + " * *Example:* \"1. SHALL validate input `config`. 2. SHALL acquire lock. 3. SHALL call `_internal_method()` *before* releasing lock.\"\n", + " * Enumerate all known edge cases and specify exactly how they MUST be handled.\n", + " * *Example:* \"If input is None, SHALL raise `TypeError`. If input list is empty, SHALL return an empty `Dict`.\"\n", + " * Provide concrete examples of inputs and expected outputs, especially for parsing, formatting, or complex transformations.\n", + " * Document ALL observable side effects exactly (e.g., specific log message templates, exact file system paths utilized, mutations to input objects if applicable).\n", + " * **String Formats**: For methods returning strings (`__str__`, `__repr__`, custom formatting), provide the exact format string or a template.\n", + " * *Example:* `__repr__` SHALL return `f\"MyClass(id={self.id!r})\"`\n", + " * **Regular Expressions**: Include any required regular expressions in their entirety, with flags. Explain capture groups.\n", + " * *Example:* `VERSION_REGEX = re.compile(r\"V(?P\\d+)\", re.IGNORECASE)`\n", + " * **Algorithms/Formulas**: If a specific algorithm is required, provide step-by-step pseudo-code. Explicitly state any mathematical formulas. Detail data transformations like sorting, stripping, encoding, or type conversions.\n", + " * *Example:* \"Output from command SHALL be decoded as UTF-8, stripped of leading/trailing whitespace, and split by newline characters.\"\n", + "\n", + "## Phase 4: Specification Synthesis\n", + "\n", + "Consolidate findings into a formal SRS document. This phase requires making definitive decisions to resolve all ambiguities without external input.\n", + "\n", + "1. **Ambiguity Resolution and Assumption Locking**:\n", + " * Explicitly document every ambiguity encountered during analysis.\n", + " * Resolve each ambiguity by making and documenting a reasonable engineering assumption. Base assumptions on promoting robustness, simplicity, deterministic behavior, or adherence to common Python idioms.\n", + " * *Example Resolution*: \"The code handles `None` for `user_id` inconsistently. SRS mandates `user_id` MUST NOT be None and SHALL raise `ValueError` if None is provided.\"\n", + " * Avoid vague terms. Define any domain-specific terms used.\n", + "\n", + "2. **SRS Structural Requirements**:\n", + " The SRS MUST include standard sections (Introduction, Overview, NFRs) AND specifically mandated sections:\n", + " * **Module Namespace Reference**: A dedicated section defining every exposed name (classes, functions, exceptions, constants), including conditional components as defined in Phase 1.\n", + " * **Assumption Log**: A record of all independent decisions made to resolve established ambiguities, with justifications.\n", + "\n", + "3. **Detailed Component Specification Rules**:\n", + " Every component MUST be documented using the following strict formats:\n", + "\n", + " * **For Classes**:\n", + " * **Inheritance**: The exact parent class(es).\n", + " * **Attributes**: A complete table of ALL public, protected, and private attributes (Name, Type, Access Level, Description, Property/Instance/Class).\n", + " * *Example:* `is_initialized: bool` | Instance Attribute | Private | Tracks initialization state.\n", + " * **Methods**: Detailed specification for each method including dunder methods (see Callables below).\n", + " * **Protocol Support**: Explicit list of supported Python protocols.\n", + "\n", + " * **For Callables (Methods/Functions)**:\n", + " * **Full Signature**: MUST include all parameters with type hints and default values. Decorators MUST be listed.\n", + " * *Example:* `@classmethod def from_config(cls, config: Dict[str, Any]) -> 'MyClass':`\n", + " * **`*args`/`**kwargs` definition**: Explicit explanation of how variadic arguments are processed, including expected types and structure if applicable.\n", + " * **Preconditions**: What MUST be true before execution.\n", + " * *Example:* \"Parameter `config` MUST not be None.\"\n", + " * **Behavior**: Detailed step-by-step description using imperative language, including order of operations, side effects, algorithms, and data transformations as per Phase 3.3.\n", + " * **Postconditions**: What is guaranteed to be true after successful execution.\n", + " * **Return Value**: Exact type and structure. For complex types, provide a breakdown.\n", + " * *Example:* Returns `Dict[str, List[Tuple[int, str]]]` where each tuple is `(index, value)`.\n", + " * **Error Handling**: Exact exception types and their *literal* error message strings (use f-string like placeholders for dynamic parts) for every error condition.\n", + " * *Example:* SHALL raise `ValueError` with message `f\"Invalid ID: {id}\"` if ID is not positive.\n", + " * *Example:* SHALL raise `TypeError` with message \"Configuration must be a dict.\"\n", + "\n", + " * **For Collections/Enums/Constants**:\n", + " * **Complete Enumeration**: ABSOLUTELY NO use of \"e.g.\" or \"etc.\". Every member, key, or value MUST be listed.\n", + " * **Type and Value**: For constants, specify type and exact value.\n", + "\n", + "## Phase 5: Verification and Validation\n", + "\n", + "Ensure the SRS is accurate, complete, and actionable based *solely* on the generated document.\n", + "\n", + "1. **Canonical Test Suite Definition**:\n", + " * Translate identified behavioral scenarios, edge cases, and input/output examples from Phase 3 into a required acceptance test suite definition within the SRS.\n", + " * Ensure these tests cover the \"hidden contracts\" (e.g., `_private` attributes accessed by legacy tests) as specified.\n", + "\n", + "2. **Completeness Review**:\n", + " * Verify that NO placeholders (\"etc.\", \"e.g.\") exist anywhere in the document.\n", + " * Validate that every name in the module namespace is documented.\n", + " * Confirm that every assumption made in Phase 4 is clearly documented in the Assumption Log and reflected as a firm requirement in the component specifications.\n", + " * Ensure all types, string formats, error messages, and behaviors are specified with an absolute level of detail.\n", + "\n", + "\n", + "\n", + "{source_code}\n", + "" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "EHGn7iVrga_d" + }, + "outputs": [], + "source": [ + "%%writefile prompts/create_dd.md\n", + "\n", + "\n", + "You are a world-class software architect specialized in Python.\n", + "\n", + "\n", + "\n", + "You will be given a Technical Specification located in the `technical_specification` tag below (\"the Spec\"). This Spec provides detailed requirements for designing one Python module.\n", + "Your goal is to write a Technical Design Document (TDD) that suggests an optimal design for this Python module.\n", + "\n", + "\n", + "\n", + "- The design must strictly adhere to all requirements defined in the Spec, without adding any out-of-scope features.\n", + "- The target runtime environment must be Python 3.11 on Linux.\n", + "- The TDD must start with a brief overview of multiple viable design alternatives and compare their strengths and weaknesses.\n", + "- The TDD must recommend a single design option and justify the choice with a clear trade-off analysis relative to the requirements.\n", + "- The TDD must document the recommended design option in full details describing the system's structure and functionality using natural language, Mermaid diagrams, and short code snippets.\n", + "- A Mermaid class diagram is recommended if the module design involves multiple related classes.\n", + "- A Mermaid sequence diagram is recommended for non-trivial call chains or complex interactions.\n", + "- The TDD must include a dedicated section outlining relevant security implications and proposed mitigations.\n", + "\n", + "\n", + "\n", + "- Write the TDD in Markdown format.\n", + "\n", + "\n", + "\n", + "{spec}\n", + "" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "ZCoVBy1Vga_d" + }, + "outputs": [], + "source": [ + "%%writefile prompts/write_new_code.md\n", + "\n", + "\n", + "You are a world-class software engineer specialized in implementation of Python modules.\n", + "\n", + "\n", + "\n", + "You are given two documents:\n", + "1. Software Requirements Specification located in the `software_requirements_specification` tag below.\n", + "2. Technical Design Document located in the `technical_design_document` tag below.\n", + "Your goal is to develop a new Python module based strictly on these two documents.\n", + "\n", + "\n", + "\n", + "- You can leverage all Standard Python Libraries available in Python 3.11, but you are not allowed to import any PyPi packages.\n", + "- Before submitting your response perform a thorough code review and focus on the following 3 critical aspects:\n", + " 1. Your code implementation must implement all requirements found in the specification and design doc, without any gaps.\n", + " 2. Your code must be bug free.\n", + " 3. Your code must not introduce any extra functionality that is not specifically requested in the design doc.\n", + "\n", + "\n", + "\n", + "Your response must be a single Markdown code block with the full code of the Python module that you implemented. Do not include any unit tests or code samples that show how to use this module.\n", + "\n", + "\n", + "\n", + "{spec}\n", + "\n", + "\n", + "\n", + "{design_doc}\n", + "" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "lBPtZNR0ga_d" + }, + "source": [ + "### Step 1: Select and load Target Module\n", + "\n", + "We will use `twisted.python.zipstream` and download the raw file directly from GitHub.\n", + "\n", + "https://github.com/twisted/twisted/blob/master/src/twisted/python/zipstream.py\n", + "\n", + "* **Source**: `original_code/zipstream.py`\n", + "* **Target**: `output/zipstream.py`" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "ODxstIc6ga_d" + }, + "outputs": [], + "source": [ + "MODULE_URL = \"https://raw.githubusercontent.com/twisted/twisted/master/src/twisted/python/zipstream.py\"\n", + "TARGET_MODULE = \"zipstream\"\n", + "CODE_DIR_PATH = \"original_code\"\n", + "CODE_PATH = f\"{CODE_DIR_PATH}/{TARGET_MODULE}.py\"\n", + "\n", + "!wget -q -O {CODE_PATH} {MODULE_URL}" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "59V3ymjRzSq-" + }, + "source": [ + "### Step 2: Generate and Verify Unit Tests\n", + "\n", + "If the legacy code lacks tests, we must generate them to establish a baseline of correctness.\n", + "\n", + "#### 2.1 Generate Tests\n", + "\n", + "We read the legacy code and use a specific prompt to generate high-coverage `unittest` cases." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "k5dYJPLRoMI3" + }, + "outputs": [], + "source": [ + "# Load code\n", + "with open(CODE_PATH) as f:\n", + " source_code = f.read()\n", + "print(f\"Loaded {TARGET_MODULE} ({len(source_code)} bytes)\")\n", + "\n", + "TEST_PATH = f\"output/tests/test_{TARGET_MODULE}.py\"\n", + "\n", + "generated_tests = generate_and_save(\n", + " template_path=\"prompts/create_tests.md\",\n", + " output_path=TEST_PATH,\n", + " replacements={\n", + " \"source_code\": source_code,\n", + " \"module_name\": TARGET_MODULE,\n", + " \"code_dir_path\": CODE_DIR_PATH,\n", + " },\n", + ")\n", + "print(f\"Tests saved to {TEST_PATH}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "7U_0eSFUuMMz" + }, + "source": [ + "#### 2.2 Verify Baseline (Run Tests)\n", + "\n", + "We must ensure these newly generated tests actually pass on the *legacy* code before moving on. We use `subprocess` to isolate the test execution." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "u_fabIYiuOhO" + }, + "outputs": [], + "source": [ + "import os\n", + "import subprocess\n", + "import sys\n", + "\n", + "from IPython.display import Markdown\n", + "\n", + "def run_tests(test_dir: str, code_dir: str, test_file: str) -> None:\n", + " \"\"\"Runs tests in a separate process and displays a rich Markdown table report.\"\"\"\n", + " env = os.environ.copy()\n", + " # Ensure the module under test can be imported\n", + " env[\"PYTHONPATH\"] = f\"{code_dir}\" + os.pathsep + env.get(\"PYTHONPATH\", \"\")\n", + "\n", + " # Use -v to get detailed output for each test\n", + " cmd = [sys.executable, \"-m\", \"unittest\", \"-v\", os.path.join(test_dir, test_file)]\n", + "\n", + " display(Markdown(f\"### Running `{test_file}` against `{code_dir}`...\"))\n", + " result = subprocess.run(cmd, check=False, env=env, capture_output=True, text=True)\n", + "\n", + " table_lines = []\n", + " table_lines.append(\"| Test Name | Description | Status |\")\n", + " table_lines.append(\"| :--- | :--- | :---: |\")\n", + "\n", + " lines = result.stderr.splitlines()\n", + " pending_test_name = None\n", + " passed = 0\n", + " failed = 0\n", + " skipped = 0\n", + "\n", + " for line in lines:\n", + " line = line.strip()\n", + " if not line:\n", + " continue\n", + "\n", + " if \" ... \" in line:\n", + " parts = line.split(\" ... \")\n", + " if len(parts) >= 2:\n", + " desc_part = parts[0].strip()\n", + " status_part = parts[1].strip()\n", + "\n", + " if pending_test_name:\n", + " test_name = pending_test_name\n", + " description = desc_part\n", + " pending_test_name = None\n", + " else:\n", + " # Fallback parse if no header line preceded\n", + " test_parts = desc_part.split(\" \")\n", + " test_name = test_parts[0]\n", + " description = (\n", + " \" \".join(test_parts[1:]).strip() if len(test_parts) > 1 else \"\"\n", + " )\n", + " # Handle trailing parens if present\n", + " if \"(\" in description:\n", + " description = description.split(\"(\")[0].strip()\n", + "\n", + " if status_part == \"ok\":\n", + " status_emoji = \"✅ PASS\"\n", + " passed += 1\n", + " elif status_part.startswith(\"FAIL\"):\n", + " status_emoji = \"❌ FAIL\"\n", + " failed += 1\n", + " elif status_part.startswith(\"ERROR\"):\n", + " status_emoji = \"🛑 ERROR\"\n", + " failed += 1\n", + " elif status_part.startswith(\"skipped\"):\n", + " status_emoji = \"⚠ SKIP\"\n", + " skipped += 1\n", + " else:\n", + " status_emoji = f\"❓ {status_part.split()[0].upper()}\"\n", + "\n", + " table_lines.append(\n", + " f\"| `{test_name}` | {description} | {status_emoji} |\"\n", + " )\n", + "\n", + " elif line.startswith(\"test_\") and \"(\" in line and \")\" in line:\n", + " pending_test_name = line.split(\" \")[0]\n", + " elif not line.startswith(\"test_\") and line.strip() != \"\":\n", + " pending_test_name = None\n", + "\n", + " display(Markdown(\"\\n\".join(table_lines)))\n", + " display(\n", + " Markdown(f\"\\n**Summary:** {passed} passed, {failed} failed, {skipped} skipped.\")\n", + " )\n", + "\n", + " if result.returncode == 0 and failed == 0:\n", + " display(Markdown(\"\\n### ✅ **OVERALL SUCCESS: All tests passed.**\"))\n", + " return True\n", + " display(Markdown(\"\\n### ❌ **OVERALL FAILURE: Some tests failed.**\"))\n", + " if failed > 0 or result.returncode != 0:\n", + " print(\"\\nDetailed error output:\")\n", + " print(result.stderr)\n", + " return False\n", + "\n", + "\n", + "def verify_module(test_dir: str, code_dir: str, module_name: str) -> bool:\n", + " \"\"\"Wrapper to check file existence and run tests.\"\"\"\n", + " code_path = os.path.join(code_dir, f\"{module_name}.py\")\n", + " test_file = f\"test_{module_name}.py\"\n", + " if os.path.exists(code_path) and os.path.exists(os.path.join(test_dir, test_file)):\n", + " success = run_tests(test_dir, code_dir, test_file)\n", + " if success:\n", + " print(f\"\\nVerified! Valid module found at: {code_path}\")\n", + " return success\n", + " print(\"Cannot run verification due to missing files.\")\n", + " return False" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "8JWqlqSGg8qK" + }, + "outputs": [], + "source": [ + "# Run tests on original code\n", + "verify_module(\"output/tests\", CODE_DIR_PATH, TARGET_MODULE)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "eJPdp09luTJ9" + }, + "source": [ + "### Step 3: Generate Specification\n", + "\n", + "Create a detailed Markdown specification derived strictly from the legacy code's behavior." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "suCuBQy0uXRO" + }, + "outputs": [], + "source": [ + "SPEC_PATH = f\"output/specs/{TARGET_MODULE}.md\"\n", + "spec_content = generate_and_save(\n", + " template_path=\"prompts/create_spec.md\",\n", + " output_path=SPEC_PATH,\n", + " replacements={\"source_code\": source_code},\n", + ")\n", + "display(Markdown(spec_content))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "s4h4KxrYzVY_" + }, + "source": [ + "### Step 4: Generate Design Document\n", + "\n", + "Using *only* the specification from Step 3, generate a design document. This ensures the new design is based on desired behavior, not just copying the old implementation details." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "5cm70qIBga_e" + }, + "outputs": [], + "source": [ + "DD_PATH = f\"output/design_docs/{TARGET_MODULE}.md\"\n", + "dd_content = generate_and_save(\n", + " template_path=\"prompts/create_dd.md\",\n", + " output_path=DD_PATH,\n", + " replacements={\"spec\": spec_content},\n", + ")\n", + "render_markdown_with_mermaid(dd_content)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "QJABk09Yz1s5" + }, + "source": [ + "### Step 5: Generate New Code\n", + "\n", + "Generate the refactored code using the Specification and Design Doc. The legacy code is *not* used in this input, preventing contamination from old anti-patterns." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "-RnB1sdnga_e" + }, + "outputs": [], + "source": [ + "NEW_CODE_PATH = f\"output/code/{TARGET_MODULE}.py\"\n", + "new_code = generate_and_save(\n", + " template_path=\"prompts/write_new_code.md\",\n", + " output_path=NEW_CODE_PATH,\n", + " replacements={\"spec\": spec_content, \"design_doc\": dd_content},\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "50PZ7q9v0J7J" + }, + "source": [ + "### Step 6: Final Verification\n", + "\n", + "Run the tests generated in Step 2 against the new code generated in Step 5." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "1W5MjFSz0Ljj" + }, + "outputs": [], + "source": [ + "verify_module(\"output/tests\", \"output/code\", TARGET_MODULE)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "yf-gr2S40M3o" + }, + "source": [ + "## 7. Introduce new Requirements for the code\n", + "\n", + "Delete and rewrite the code, show that the added requirements are successfully incorporated.\n", + "\n", + "We will update the specification with new requirements, then re-generate the Design Doc and code based upon that specification." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "hwxPIKBFga_f" + }, + "outputs": [], + "source": [ + "# Add new requirements to specification\n", + "new_requirements = \"\"\"## Coding style requirements\n", + "1. All input parameters and return values must have type hints.\n", + "2. All methods and classes must have Google-style Docstrings.\n", + "\"\"\"\n", + "\n", + "with open(SPEC_PATH, \"a+\") as f:\n", + " f.write(new_requirements)\n", + " f.seek(0)\n", + " spec_content = f.read()\n", + "\n", + "# Regenerate Design Doc & Code\n", + "dd_content = generate_and_save(\"prompts/create_dd.md\", DD_PATH, {\"spec\": spec_content})\n", + "new_code = generate_and_save(\n", + " \"prompts/write_new_code.md\",\n", + " NEW_CODE_PATH,\n", + " {\"spec\": spec_content, \"design_doc\": dd_content},\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "HvnpOMFHa6or" + }, + "outputs": [], + "source": [ + "verify_module(\"output/tests\", \"output/code\", TARGET_MODULE)" + ] + } + ], + "metadata": { + "colab": { + "name": "refactor_legacy_code.ipynb", + "toc_visible": true + }, + "kernelspec": { + "display_name": "Python 3", + "name": "python3" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} diff --git a/gemini/use-cases/customer-support/customer_support_gemini_genai_sdk.ipynb b/gemini/use-cases/customer-support/customer_support_gemini_genai_sdk.ipynb new file mode 100644 index 0000000..22901d1 --- /dev/null +++ b/gemini/use-cases/customer-support/customer_support_gemini_genai_sdk.ipynb @@ -0,0 +1,1081 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "8RopCue2ab0K" + }, + "outputs": [], + "source": [ + "# Copyright 2025 Google LLC\n", + "#\n", + "# Licensed under the Apache License, Version 2.0 (the \"License\");\n", + "# you may not use this file except in compliance with the License.\n", + "# You may obtain a copy of the License at\n", + "#\n", + "# https://www.apache.org/licenses/LICENSE-2.0\n", + "#\n", + "# Unless required by applicable law or agreed to in writing, software\n", + "# distributed under the License is distributed on an \"AS IS\" BASIS,\n", + "# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n", + "# See the License for the specific language governing permissions and\n", + "# limitations under the License." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "VyPmicX9RlZX" + }, + "source": [ + "# Get hands-on with a customer support use case using Gemini and Gen AI SDK\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + "
    \n", + " \n", + " \"Google
    Open in Colab\n", + "
    \n", + "
    \n", + " \n", + " \"Google
    Open in Colab Enterprise\n", + "
    \n", + "
    \n", + " \n", + " \"Workbench
    Open in Workbench\n", + "
    \n", + "
    \n", + " \n", + " \"GitHub
    View on GitHub\n", + "
    \n", + "
    \n", + "\n", + "
    \n", + "\n", + "

    \n", + "Share to:\n", + "\n", + "\n", + " \"LinkedIn\n", + "\n", + "\n", + "\n", + " \"Bluesky\n", + "\n", + "\n", + "\n", + " \"X\n", + "\n", + "\n", + "\n", + " \"Reddit\n", + "\n", + "\n", + "\n", + " \"Facebook\n", + "\n", + "

    " + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "fI7REy_PauX_" + }, + "source": [ + "| Author |\n", + "| --- |\n", + "| [Eric Dong](https://github.com/gericdong) |" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "hXLnBuhYa15O" + }, + "source": [ + "## Overview\n", + "\n", + "This tutorial provides you hands-on experience building your own Gemini-powered applications. It guides you through practical examples and code snippets, focusing on a customer support use case, to show you how to start creating AI applications using Gemini and Google Gen AI SDK." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "EAICO794dAmy" + }, + "source": [ + "## Getting Started" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "CHRZUpfWSEpp" + }, + "source": [ + "### ⚙ **Setup**: Install Gen AI SDK\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "sG3_LKsWSD3A" + }, + "outputs": [], + "source": [ + "%pip install --upgrade --quiet google-genai" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "LymmEN6GSTn-" + }, + "source": [ + "#### Authenticate your notebook environment" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "12fnq4V0SNV3" + }, + "outputs": [], + "source": [ + "import sys\n", + "\n", + "if \"google.colab\" in sys.modules:\n", + " from google.colab import auth\n", + "\n", + " auth.authenticate_user()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "nanp0kdisICh" + }, + "source": [ + "#### Set Google Cloud project and location" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "Gz3Bq7dwq3K1" + }, + "outputs": [], + "source": [ + "import os\n", + "\n", + "# fmt: off\n", + "PROJECT_ID = \"[your-project-id]\" # @param {type: \"string\", placeholder: \"[your-project-id]\", isTemplate: true}\n", + "# fmt: on\n", + "if not PROJECT_ID or PROJECT_ID == \"[your-project-id]\":\n", + " PROJECT_ID = str(os.environ.get(\"GOOGLE_CLOUD_PROJECT\"))\n", + "\n", + "LOCATION = os.environ.get(\"GOOGLE_CLOUD_REGION\", \"us-central1\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "37CH91ddY9kG" + }, + "source": [ + "#### Create a product catalog" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "c5P-OweZpuNO" + }, + "outputs": [], + "source": [ + "from google.cloud import storage\n", + "\n", + "storage_client = storage.Client()\n", + "bucket = storage_client.bucket(\"cloud-samples-data\")\n", + "blobs = bucket.list_blobs(prefix=\"generative-ai/image/retail\")\n", + "\n", + "id = 0\n", + "product_catalog = []\n", + "for blob in blobs:\n", + " if blob.name.endswith(\".png\"):\n", + " item = {\n", + " \"id\": id,\n", + " \"inventory\": \"In stock\",\n", + " \"price\": 39.99,\n", + " \"image_url\": blob.public_url,\n", + " }\n", + " product_catalog.append(item)\n", + " id += 1" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "EdvJRUWRNGHE" + }, + "source": [ + "### 1ïžâƒŁ Import libraries\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "qgdSpVmDbdQ9" + }, + "outputs": [], + "source": [ + "import numpy as np\n", + "from IPython.display import HTML, Audio, Image, Markdown, display\n", + "from google import genai\n", + "from google.genai.types import (\n", + " Content,\n", + " FunctionDeclaration,\n", + " GenerateContentConfig,\n", + " GoogleSearch,\n", + " LiveConnectConfig,\n", + " Part,\n", + " Tool,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "E_0XTXckBAmy" + }, + "source": [ + "### 2ïžâƒŁ Create a client" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "zpIPG_YhSjaw" + }, + "outputs": [], + "source": [ + "client = genai.Client(vertexai=True, project=PROJECT_ID, location=LOCATION)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "a07vlxdfdP5l" + }, + "source": [ + "## Customer support use case\n", + "\n", + "Imagine a customer is buying a chair. He searched the internet and got a picture of a chair that he likes from the web, then he is reaching out online to his favorite furniture store for assistance.\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "KGAKijPSC73g" + }, + "source": [ + "#### đŸ€” **Example question 1**: Do you have a chair similar to the one in this picture, but in red?\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "yAyk3cWpC4jw" + }, + "outputs": [], + "source": [ + "customer_query = \"Do you have chairs similar to the one in this picture, but in red?\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "sae1XmAohTm0" + }, + "outputs": [], + "source": [ + "customer_chair_image_url = (\n", + " \"https://storage.googleapis.com/cloud-samples-data/generative-ai/image/armchair.png\"\n", + ")\n", + "\n", + "display(Image(url=customer_chair_image_url, width=300))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "uFDdiSuqR6w4" + }, + "source": [ + "### 3ïžâƒŁ Send requests to the model to generate content\n", + "\n", + "The request may contain:\n", + "- `model`: The model ID\n", + "- `contents`: Multimodal input (text, PDFs, images, audio, video)\n", + "- `config`: Configuration for generation\n", + "\n", + "Learn more about [Google models](https://cloud.google.com/vertex-ai/generative-ai/docs/learn/models)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "SNtTH-4EFwFy" + }, + "outputs": [], + "source": [ + "MODEL_ID = \"gemini-3.5-flash\"" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "KF4I1qrkfdAG" + }, + "source": [ + "Use `contents` to construct multimodal input including text, PDFs, images, audio, video, and code all together.\n", + "\n", + "Here, you can leverage the long context window that helps seamlessly analyze large amounts of information." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "uRYbBLDffv9h" + }, + "outputs": [], + "source": [ + "product_catalog_parts = []\n", + "for product in product_catalog:\n", + " product_catalog_parts.append(f\"Chair (id={product['id']}):\")\n", + " product_catalog_parts.append(\n", + " Part.from_uri(file_uri=product[\"image_url\"], mime_type=\"image/png\")\n", + " )\n", + "\n", + "\n", + "contents = [\n", + " customer_query,\n", + " Part.from_uri(file_uri=customer_chair_image_url, mime_type=\"image/png\"),\n", + " \"catalog:\",\n", + " product_catalog_parts,\n", + "]" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "mUu7lubTfYXK" + }, + "source": [ + "Use `system_instruction` to give the model additional context to understand the task, provide more customized responses, adhere to specific guidelines over the full user interaction with the model.\n", + "\n", + "Gemini 2.0 models were trained to have conversational responses that are short, to the point and cost-effective for serving. If you need richer, more verbose answers use the following system instruction:\n", + "\n", + "\n", + "> All questions should be answered comprehensively with details,\n", + "unless the user requests a concise response specifically.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "ye5-5gSIfRjK" + }, + "outputs": [], + "source": [ + "system_instruction = \"\"\"\n", + "You are an expert sales assistant specializing in furniture recommendations.\n", + "\"\"\"" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "bcWXLz8ef1k1" + }, + "source": [ + "Use `client.models.generate_content` to send a request to the model with `model`, `contents`, `config`, `tools`, etc.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "Dl8b5_yWwZWg" + }, + "outputs": [], + "source": [ + "%%time\n", + "\n", + "response = client.models.generate_content(\n", + " model=MODEL_ID,\n", + " contents=contents,\n", + " config=GenerateContentConfig(\n", + " system_instruction=system_instruction,\n", + " ),\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "_8DNdQ9UEpNy" + }, + "source": [ + "### 4ïžâƒŁ Read model responses\n", + "\n", + "The response may contain:\n", + "- Multimodal output (text, code, images, audio, embeddings)\n", + "- Response metadata" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "cT0fruYTMpcC" + }, + "outputs": [], + "source": [ + "response" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "oY8RMOm3Npf9" + }, + "source": [ + "You can use `Markdown` to display the formatted text.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "6o77sT_SNs1H" + }, + "outputs": [], + "source": [ + "display(Markdown(response.text))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "qTej417fIEOw" + }, + "source": [ + "### 🚀 **Feature**: Structured output (controlled generation)\n", + "\n", + "When you want the model response to a prompt to be returned in a structured data format, particularly if you are using the responses for downstream processes, such as downstream modules that expect a specific format as input, you use the controlled generation to ensure that model outputs comply with a specific structured format.\n", + "\n", + "You define a response schema for the model output using these two options:\n", + "\n", + "- **Option 1**: Using a Pydantic object" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "m3GoA5HzJQxA" + }, + "outputs": [], + "source": [ + "from pydantic import BaseModel, RootModel\n", + "\n", + "class MatchedFurnitureInfo(BaseModel):\n", + " id: int\n", + " match_score: int\n", + " match_reason: str\n", + "\n", + "\n", + "class MatchedFurnitureList(RootModel):\n", + " root: list[MatchedFurnitureInfo]\n", + "\n", + " def __iter__(self):\n", + " return iter(self.root)\n", + "\n", + " def __getitem__(self, item):\n", + " return self.root[item]" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "-NwKorrbJbsm" + }, + "source": [ + "- **Option 2**: Using a dictionary as in OpenAPI schema.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "aFDZntCKJcCu" + }, + "outputs": [], + "source": [ + "response_schema = {\n", + " \"type\": \"ARRAY\",\n", + " \"items\": {\n", + " \"type\": \"OBJECT\",\n", + " \"properties\": {\n", + " \"id\": {\"type\": \"INTEGER\"},\n", + " \"match_reason\": {\"type\": \"STRING\", \"nullable\": True},\n", + " },\n", + " },\n", + "}" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "ybFEGPYkJ8H3" + }, + "source": [ + "This example uses the OpenAPI schema option, then sets the defined schema in the `response_schema` in the `GenerateContentConfig`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "qItf6-QJlpR3" + }, + "outputs": [], + "source": [ + "response = client.models.generate_content(\n", + " model=MODEL_ID,\n", + " contents=response.text,\n", + " config=GenerateContentConfig(\n", + " system_instruction=\"Convert the given text to JSON\",\n", + " response_mime_type=\"application/json\",\n", + " response_schema=response_schema,\n", + " ),\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "2Sy-1fxWw_Yz" + }, + "outputs": [], + "source": [ + "response.parsed" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "Coyc8ZqEl_5_" + }, + "outputs": [], + "source": [ + "matching_items: dict = response.parsed\n", + "\n", + "display(HTML(\"\"))\n", + "display(HTML(\"Original: \"), Image(url=customer_chair_image_url, width=200))\n", + "display(HTML(\"Recommended: \"))\n", + "\n", + "matching_items_urls = []\n", + "for item in matching_items:\n", + " url = product_catalog[item[\"id\"]][\"image_url\"]\n", + " display(Image(url=url, width=200))\n", + " matching_items_urls.append(url)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "CIORDrCCassP" + }, + "source": [ + "### đŸ€” **Example question 2**: Would the chair fit in my room?\n", + "\n", + "- Reasoning across image and text" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "sQByglSCPUHt" + }, + "outputs": [], + "source": [ + "customer_query = \"Would the chair fit in my room?\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "TWOQErQDfgrx" + }, + "outputs": [], + "source": [ + "customer_room_image_url = \"https://storage.googleapis.com/cloud-samples-data/generative-ai/image/living-room-2.png\"\n", + "\n", + "display(Image(url=customer_room_image_url, width=400))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "wVkO1iA8PGv1" + }, + "source": [ + "### Send request to the model\n", + "\n", + "Here, you leverage the multimodal understanding to reason across the images of both the chair and the room." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "Kku6akon9d-u" + }, + "outputs": [], + "source": [ + "system_instruction = \"\"\"\n", + " You are an interior designer.\n", + " Your mission is to help customers to create living spaces that balance functionality\n", + " and beauty through personalized service and sustainable design practices.\n", + "\"\"\"\n", + "\n", + "contents = [\n", + " \"Chair:\",\n", + " Part.from_uri(file_uri=matching_items_urls[0], mime_type=\"image/png\"),\n", + " \"Living room:\",\n", + " Part.from_uri(file_uri=customer_room_image_url, mime_type=\"image/png\"),\n", + " customer_query,\n", + "]\n", + "\n", + "response = client.models.generate_content(\n", + " model=MODEL_ID,\n", + " contents=contents,\n", + " config=GenerateContentConfig(\n", + " system_instruction=system_instruction,\n", + " ),\n", + ")\n", + "\n", + "display(Markdown(response.text))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "0KHz3YMFPgb7" + }, + "source": [ + "### 🚀 **Feature**: Image generation\n", + "\n", + "You also generate an image with the chair being placed in the living room, showcasing the Gemini's ability to generate images natively." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "Azo_HLmpPde2" + }, + "outputs": [], + "source": [ + "response = client.models.generate_content(\n", + " model=\"gemini-3.1-flash-image\",\n", + " contents=[\n", + " contents,\n", + " \"Create an image with the chair be integrated in the living room\",\n", + " ],\n", + " config=GenerateContentConfig(\n", + " response_modalities=[\"TEXT\", \"IMAGE\"],\n", + " ),\n", + ")\n", + "\n", + "for part in response.candidates[0].content.parts:\n", + " if part.text:\n", + " display(Markdown(part.text))\n", + " if part.inline_data:\n", + " display(Image(data=part.inline_data.data, width=400))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "NeP2E49KWzos" + }, + "source": [ + "### đŸ€” **Example question 3**: Is this chair available at a store near me? I am at Google Cloud Next!\n", + "\n", + "Here you use the function calling and native tool use to enable models to connect to built-in or external tooling and systems, and fetch real-time or business data.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "kioI4ZD6StdN" + }, + "outputs": [], + "source": [ + "customer_query = \"Do you have the chair available in a store near me? I am at Google Cloud Next 2025.\"" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "nK-ZjxDvS2QF" + }, + "source": [ + "### 🚀 **Feature**: Function calling\n", + "\n", + "Define a `FunctionDeclaration` with the name of a function that matches the description and the arguments to call it with. You also can define a Python function for automatic function calling, which will run the function and return the output in natural language generated by the model." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "n9MytQ-FPXIz" + }, + "outputs": [], + "source": [ + "get_product_info_function = FunctionDeclaration(\n", + " name=\"get_product_info\",\n", + " description=\"Get the stock amount and identifier for a given product\",\n", + " parameters={\n", + " \"type\": \"OBJECT\",\n", + " \"properties\": {\n", + " \"product_name\": {\"type\": \"STRING\", \"description\": \"Product name\"}\n", + " },\n", + " },\n", + ")\n", + "\n", + "get_store_location_function = FunctionDeclaration(\n", + " name=\"get_store_location\",\n", + " description=\"Get the location of the closest store\",\n", + " parameters={\n", + " \"type\": \"OBJECT\",\n", + " \"properties\": {\"location\": {\"type\": \"STRING\", \"description\": \"Location\"}},\n", + " },\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "Q7JB8rbwSddI" + }, + "source": [ + "Define a `Tool` that allows the model to select from a set of defined functions." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "xhhcUJjaPbwV" + }, + "outputs": [], + "source": [ + "retail_tool = Tool(\n", + " function_declarations=[\n", + " get_product_info_function,\n", + " get_store_location_function,\n", + " ],\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "XKCqiOHJSVLZ" + }, + "source": [ + "Use function calling in a `chats` session to answer user's questions about the products." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "iU0AOZs6gcvN" + }, + "outputs": [], + "source": [ + "chat = client.chats.create(\n", + " model=MODEL_ID,\n", + " config=GenerateContentConfig(\n", + " tools=[retail_tool],\n", + " ),\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "2T7C0tdPTI3E" + }, + "source": [ + "The model generates the function calls that you can use to connect to an external system to get real-time or business data." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "NfuSOgW8Pjjx" + }, + "outputs": [], + "source": [ + "response = chat.send_message(customer_query)\n", + "\n", + "response.function_calls" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "teVbr4RD9wZH" + }, + "source": [ + "### 🚀 **Feature**: Google Search\n", + "\n", + "Using `GoogleSearch` in Tool, Gemini will automatically do a Google search and incorporate information from the web into its responses. It also provides direct citations so you can easily fact-check sources." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "-w03L_82P0qV" + }, + "outputs": [], + "source": [ + "def get_store_location(location: str):\n", + " google_search_tool = Tool(google_search=GoogleSearch())\n", + "\n", + " prompt = f\"What is the location for {location}?\"\n", + "\n", + " response = client.models.generate_content(\n", + " model=MODEL_ID,\n", + " contents=prompt,\n", + " config=GenerateContentConfig(tools=[google_search_tool], temperature=0),\n", + " )\n", + "\n", + " return {\"store\": response.text}\n", + "\n", + "\n", + "def get_product_info(product_name: str):\n", + " return {\"id\": \"3\", \"in_stock\": \"yes\"}" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "F8i3Ux3PTuPE" + }, + "source": [ + "This is where you use the model response to connect to external systems to get the real time or business data." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "H1ZHlsiqfchF" + }, + "outputs": [], + "source": [ + "function_response_parts = []\n", + "\n", + "for function_call in response.function_calls:\n", + " if function_call.name == \"get_store_location\":\n", + " function_response = get_store_location(**function_call.args)\n", + " elif function_call.name == \"get_product_info\":\n", + " function_response = get_product_info(**function_call.args)\n", + " else:\n", + " raise ValueError(f\"Unknown function: {function_call.name}\")\n", + "\n", + " print(function_call.name)\n", + " print(function_response)\n", + "\n", + " function_response_part = Part.from_function_response(\n", + " name=function_call.name,\n", + " response=function_response,\n", + " )\n", + "\n", + " function_response_parts.append(function_response_part)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "Uc5d9Q3ZT5Ko" + }, + "source": [ + "The mode incorporates the data from external systems, and returns the output in natural language." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "psVYQ2PmiCbY" + }, + "outputs": [], + "source": [ + "response = chat.send_message(function_response_parts)\n", + "\n", + "display(Markdown(response.text))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "xN4JrBBVeD_0" + }, + "source": [ + "### 🚀 **Feature**: Live API\n", + "\n", + "#### **Example**: Text-to-audio conversation\n", + "\n", + "**Step 1**: You set up a conversation with the API that allows you to send text prompts and receive audio responses.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "AuPizI-nedBB" + }, + "outputs": [], + "source": [ + "config = LiveConnectConfig(\n", + " response_modalities=[\"AUDIO\"],\n", + " tools=[Tool(google_search=GoogleSearch())],\n", + ")\n", + "\n", + "\n", + "async def main() -> None:\n", + " async with client.aio.live.connect(\n", + " model=\"gemini-live-2.5-flash-native-audio\", config=config\n", + " ) as session:\n", + "\n", + " async def send() -> bool:\n", + " text_input = input(\"Input > \")\n", + " if text_input.lower() in (\"q\", \"quit\", \"exit\"):\n", + " return False\n", + " await session.send_client_content(\n", + " turns=Content(role=\"user\", parts=[Part(text=text_input)])\n", + " )\n", + "\n", + " return True\n", + "\n", + " async def receive() -> None:\n", + "\n", + " audio_data = []\n", + "\n", + " async for message in session.receive():\n", + " if (\n", + " message.server_content.model_turn\n", + " and message.server_content.model_turn.parts\n", + " ):\n", + " for part in message.server_content.model_turn.parts:\n", + " if part.inline_data:\n", + " audio_data.append(\n", + " np.frombuffer(part.inline_data.data, dtype=np.int16)\n", + " )\n", + "\n", + " if message.server_content.turn_complete:\n", + " display(Markdown(\"**Response >**\"))\n", + " display(\n", + " Audio(np.concatenate(audio_data), rate=24000, autoplay=True)\n", + " )\n", + " break\n", + "\n", + " while True:\n", + " if not await send():\n", + " break\n", + " await receive()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "94IeUUb3e90M" + }, + "source": [ + "**Step 2** Run the chat, input your prompts, or type `q`, `quit` or `exit` to exit.\n", + "\n", + "Sample questions:\n", + "- Where is the Google Cloud Next? Give me the address.\n", + "- How is the weather today?" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "2UvgUDIYJqfw" + }, + "outputs": [], + "source": [ + "await main()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "KAtlZrA7ULlj" + }, + "source": [ + "# 🎉 What's Next?\n", + "\n", + "🚀 Learn more at the Google Cloud Gen AI sample repository 👇\n", + "\n", + "[github.com/GoogleCloudPlatform/generative-ai](https://github.com/GoogleCloudPlatform/generative-ai)\n" + ] + } + ], + "metadata": { + "colab": { + "name": "customer_support_gemini_genai_sdk.ipynb", + "toc_visible": true + }, + "kernelspec": { + "display_name": "Python 3", + "name": "python3" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} diff --git a/gemini/use-cases/document-processing/doc_parsing_with_llamaindex_and_llamaparse.ipynb b/gemini/use-cases/document-processing/doc_parsing_with_llamaindex_and_llamaparse.ipynb new file mode 100644 index 0000000..077430d --- /dev/null +++ b/gemini/use-cases/document-processing/doc_parsing_with_llamaindex_and_llamaparse.ipynb @@ -0,0 +1,861 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "id": "b2c3c554daa1" + }, + "source": [ + "# Comparing LlamaIndex and LlamaParse for Dense Document Questioning Answering on Vertex AI\n", + "\n", + " \n", + " \n", + " \n", + " \n", + "
    \n", + " \n", + " \"Google
    Open in Colab\n", + "
    \n", + "
    \n", + " \n", + " \"Google
    Open in Colab Enterprise\n", + "
    \n", + "
    \n", + " \n", + " \"Vertex
    Open in Vertex AI Workbench\n", + "
    \n", + "
    \n", + " \n", + " \"GitHub
    View on GitHub\n", + "
    \n", + "
    \n", + "\n", + "
    \n", + "\n", + "Share to:\n", + "\n", + "\n", + " \"LinkedIn\n", + "\n", + "\n", + "\n", + " \"Bluesky\n", + "\n", + "\n", + "\n", + " \"X\n", + "\n", + "\n", + "\n", + " \"Reddit\n", + "\n", + "\n", + "\n", + " \"Facebook\n", + " " + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "1bc291e87bd1" + }, + "source": [ + "| | |\n", + "|-|-|\n", + "| Author(s) | [Noa Ben-Efraim](https://github.com/noabenefraim/) |" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "648701a7bf44" + }, + "source": [ + "## Overview\n", + "There are many ways to customize RAG pipelines by choosing how to ingest, parse, chunk, and retrieve your data. This notebook focuses on comparing different document parsing capabilities offered by LlamaIndex.\n", + "\n", + "We will compare document parsing with LlamaIndex and LlamaParse on a 10-Q financial document, which is heavily populated with complex tables.\n", + "\n", + "### Objectives\n", + "This notebook compare using LlamaIndex and LlamaParse for ingesting and indexing a complex document. \n", + "\n", + "You will complete the following tasks:\n", + "1. Ingest and parse document using LlamaIndex SimpleDataReader, LlamaIndex LangchainNodeParser, and LlamaParse Parser using Gemini models.\n", + "2. Index your parsed document in a VectorStore.\n", + "3. Create a a query agent for each parsing technique that can answer questions against the input document.\n", + "4. Compare results across LlamaIndex and LlamaParse.\n", + "\n", + "### LlamaIndex\n", + "LlamaIndex is a foundational data framework for building LLM applications. A few of its main capabilities are:\n", + "\n", + "+ Data Ingestion: Loads your data from various sources (documents, databases, APIs).   \n", + "+ Indexing: Structures your data into efficient formats for LLM retrieval (e.g., vector stores, tree structures).   \n", + "+ Querying: Enables you to ask questions or give instructions to the LLM, referencing your indexed data for answers.   \n", + "+ Integration: Connects with various LLMs, vector databases, and other tools.   \n", + "  \n", + "\n", + "### LlamaParse\n", + "LlamaParse is a tool within the LlamaIndex ecosystem, focused on parsing complex documents:\n", + "\n", + "+ PDFs: Handles PDFs with tables, charts, and other embedded elements that can be challenging for standard parsing.  \n", + "+ Semi-structured Data: Extracts structured information from documents that aren't fully formatted databases.   \n", + "+ Enhanced Retrieval: Works seamlessly with LlamaIndex to improve retrieval accuracy for complex documents." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "87008e44295a" + }, + "source": [ + "## Getting Started\n", + "\n", + "### Authenticate your notebook environment\n", + "\n", + "This notebook expects the following resources to exists:\n", + "+ Initialized Google Cloud project \n", + "+ Vertex AI API enabled\n", + "+ GCS Bucket and Vertex AI Search Index and Endpoint\n", + "+ A LlamaParse API Key [request a key here](https://docs.cloud.llamaindex.ai/llamacloud/getting_started/api_key)" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": { + "id": "516b8d774169" + }, + "outputs": [], + "source": [ + "PROJECT_ID = \"\" # @param {type:\"string\"}\n", + "REGION = \"\" # @param {type:\"string\"}\n", + "GCS_BUCKET = \"\" # @param {type:\"string\"}\n", + "VS_INDEX_NAME = \"\" # @param {type:\"string\"}\n", + "VS_INDEX_ENDPOINT_NAME = \"\" # @param {type:\"string\"}\n", + "DATA_FOLDER = \"./data\" # @param {type:\"string\"}" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "205f199d8a53" + }, + "source": [ + "### Set Google Cloud project information and initialize Vertex AI SDK\n", + "\n", + "To get started using Vertex AI, you must have an existing Google Cloud project and [enable the Vertex AI API](https://console.cloud.google.com/flows/enableapi?apiid=aiplatform.googleapis.com).\n", + "\n", + "Learn more about [setting up a project and a development environment](https://cloud.google.com/vertex-ai/docs/start/cloud-environment)." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": { + "id": "dd11490e3764" + }, + "outputs": [], + "source": [ + "PROJECT_ID = \"\" # @param {type:\"string\"}\n", + "LOCATION = \"\" # @param {type:\"string\"}\n", + "\n", + "import vertexai\n", + "\n", + "vertexai.init(project=PROJECT_ID, location=LOCATION)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "f818f7127a12" + }, + "source": [ + "### Setting up the Environment\n", + "Install dependencies" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "c76df03a88f3" + }, + "outputs": [], + "source": [ + "%pip install google-cloud-aiplatform \\\n", + " llama-index \\\n", + " langchain-community \\\n", + " llama-index-embeddings-vertex \\\n", + " llama-index-llms-vertex \\\n", + " termcolor \\\n", + " llama-index-core -q" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "44d71c554004" + }, + "source": [ + "Set up imports" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": { + "id": "fb7e65420d45" + }, + "outputs": [], + "source": [ + "from langchain.text_splitter import RecursiveCharacterTextSplitter\n", + "from llama_index.core import Settings, SimpleDirectoryReader, VectorStoreIndex\n", + "from llama_index.core.base.response.schema import Response\n", + "from llama_index.core.extractors import KeywordExtractor, QuestionsAnsweredExtractor\n", + "from llama_index.core.ingestion import IngestionPipeline\n", + "from llama_index.core.node_parser import LangchainNodeParser\n", + "from llama_index.embeddings.vertex import VertexTextEmbedding\n", + "from llama_index.llms.vertex import Vertex\n", + "from llama_index.vector_stores.vertexaivectorsearch import VertexAIVectorStore\n", + "from llama_parse import LlamaParse\n", + "from termcolor import colored" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "8d7a288c44bb" + }, + "source": [ + "Generate credentials" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": { + "id": "5e5cb3d58df5" + }, + "outputs": [], + "source": [ + "import google.auth\n", + "import google.auth.transport.requests\n", + "\n", + "# credentials will now have an api token\n", + "credentials = google.auth.default(quota_project_id=\"genai-noabe\")[0]\n", + "request = google.auth.transport.requests.Request()\n", + "credentials.refresh(request)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "0e9455150d49" + }, + "outputs": [], + "source": [ + "embedding_model = VertexTextEmbedding(\"text-embedding-005\", credentials=credentials)\n", + "llm = Vertex(model=\"gemini-2.5-flash\", temperature=0.0, max_tokens=5000)\n", + "\n", + "Settings.embed_model = embedding_model\n", + "Settings.llm = llm" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "f5822037c828" + }, + "source": [ + "Set up LlamaIndex settings to point to Gemini models." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "a6c832d2780c" + }, + "outputs": [], + "source": [ + "import IPython\n", + "\n", + "app = IPython.Application.instance()\n", + "app.kernel.do_shutdown(True)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "8daf430888fc" + }, + "source": [ + "### Authenticate your notebook environment (Colab only)\n", + "\n", + "If you're running this notebook on Google Colab, run the cell below to authenticate your environment." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": { + "id": "885a3c84ddac" + }, + "outputs": [], + "source": [ + "import sys\n", + "\n", + "if \"google.colab\" in sys.modules:\n", + " from google.colab import auth\n", + "\n", + " auth.authenticate_user()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "e9a64d5bf6c3" + }, + "source": [ + "### Download sample data\n", + "\n", + "For the remainder of the notebook we will examine Alphabet Inc. 10Q document. A 10Q is a financial document that is dense with tables with financial figures. This document is a great candidate to to investigate document parsing capabilities." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "e0c896dfda70" + }, + "outputs": [], + "source": [ + "!mkdir {DATA_FOLDER}\n", + "!wget \"https://abc.xyz/assets/ae/e9/753110054014b6de4d620a2853f6/goog-10-q-q2-2024.pdf\" -P {DATA_FOLDER}" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "ece6a448d7e0" + }, + "source": [ + "## Document Parsing with LlamaIndex\n", + "\n", + "This section will ingest and parse the 10Q using LlamaIndex tools, specifically focusing on SimpleDirectoryReader and LangChainNodeParser.\n", + "\n", + "### Option 1: `SimpleDirectoryReader`\n", + "The SimpleDirectoryReader is the core data ingestion tool in LlamaIndex. It's designed to load data from a variety of sources and convert it into a format suitable for further processing and indexing by LlamaIndex." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "7b343b4be5d9" + }, + "outputs": [], + "source": [ + "reader = SimpleDirectoryReader(\"./data\")\n", + "documents = reader.load_data(show_progress=True)\n", + "print(documents[0])" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": { + "id": "f5e3def62a59" + }, + "outputs": [], + "source": [ + "# Index the parsed document\n", + "simpledirectory_index = VectorStoreIndex.from_documents(documents)\n", + "\n", + "# Generate a query engine based on the SimpleDataReader\n", + "simple_query_engine = simpledirectory_index.as_query_engine(similarity_top_k=2)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "ec35c0de8639" + }, + "source": [ + "### Option 2: `LangChainNodeParser` with LlamaIndex\n", + "The LangChainNodeParser is a part of LlamaIndex. It is a specialized parser designed to extract structured information from text documents using the power of LangChain.\n", + "\n", + "Key Features:\n", + "+ LangChain Integration: Leverages LangChain's powerful language models and tools to parse text.\n", + "+ Node-Based Output: Converts unstructured text into a structured format based on a defined schema, represented as a hierarchy of nodes. This enables more sophisticated querying and analysis of the extracted information.\n", + "+ Customization: Supports defining custom parsing schemas to match the structure of your specific documents.\n", + "+ Flexibility: Can be used in combination with other LlamaIndex components, such as the SimpleDataReader, to process and index the extracted structured data." + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": { + "id": "c94da573044a" + }, + "outputs": [], + "source": [ + "parser = LangchainNodeParser(RecursiveCharacterTextSplitter())\n", + "langchain_nodes = parser.get_nodes_from_documents(documents)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "e8e2f0b882ce" + }, + "outputs": [], + "source": [ + "# An example node that was generated using the LangChainNodeParser and the associated metadata\n", + "langchain_nodes[0]" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": { + "id": "a88552f52124" + }, + "outputs": [], + "source": [ + "# Index the document based on the LangChain nodes generated above\n", + "langchainparser_index = VectorStoreIndex(nodes=langchain_nodes)\n", + "\n", + "# Create a query engine based off the LangChainNodeParser\n", + "lg_query_engine = langchainparser_index.as_query_engine(similarity_top_k=2)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "adc97e363bac" + }, + "source": [ + "## LlamaParse\n", + "\n", + "LlamaParse Parser is a powerful tool for extracting structured data from unstructured or semi-structured text, offering flexibility, customization, and seamless integration within the LlamaIndex framework.It can take an unstructured or semi-structured text document and, using a defined schema, extract structured information from it. This structured output is represented as a nested hierarchy of nodes, facilitating further processing and analysis.\n", + "\n", + "A few key features include:\n", + "\n", + "+ JSON Schema: Leverages the standardized JSON Schema format for more complex schemas.\n", + "+ Prompt Templates: Allows you to craft custom prompts to guide the language model's parsing behavior, offering greater control and adaptability.\n", + "+ LLM Selection: You have the flexibility to choose the specific LLM you want to use for parsing, enabling you to tailor the performance to your specific needs and budget.\n", + "+ Node-Based Output:\n", + " + Structured Representation: The parsed output is organized into a hierarchy of nodes, each representing a piece of extracted information.\n", + " + Nested Structure: Nodes can contain other nodes, allowing for the representation of complex relationships and nested data structures within the document.\n", + " + Metadata: Nodes can also include additional metadata, such as confidence scores or source information, enriching the extracted data.\n", + "+ Integration with LlamaIndex: The structured output from parser() seamlessly integrates with other LlamaIndex components, such as indexing and querying, facilitating efficient retrieval and analysis of the extracted information.\n", + "\n", + "#### Define a Parser\n", + "\n", + "Here we will define a LlamaParse() parser with specific parsing instructions, and ingest the data." + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": { + "id": "85ec700afd80" + }, + "outputs": [], + "source": [ + "parser = LlamaParse(\n", + " parsing_instruction=\"You are a financial analyst working specifically with 10Q documents. Not all pages have titles. Try to reconstruct the dialogue spoken in a cohesive way.\",\n", + " api_key=\"\",\n", + " result_type=\"text\", # \"markdown\" and \"text\" are available\n", + " language=\"en\",\n", + " invalidate_cache=True,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "23fd5739fa74" + }, + "source": [ + "### Option 1 - LlamaParse with SimpleDirectoryReader\n", + "\n", + "This is the apples to apples comparison with LlamaIndex. We are using the SimpleDirectoryReader with the LlamaParse file extractor, and then loading the data directly from documents to a Vector Store for retrieval." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "688898e8b334" + }, + "outputs": [], + "source": [ + "import nest_asyncio\n", + "\n", + "nest_asyncio.apply()\n", + "file_extractor = {\".pdf\": parser}\n", + "documents = SimpleDirectoryReader(\n", + " input_files=[\"./data/goog-10-q-q2-2024.pdf\"], file_extractor=file_extractor # type: ignore\n", + ").load_data()" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": { + "id": "d215261053fb" + }, + "outputs": [], + "source": [ + "lp_simple = VectorStoreIndex.from_documents(documents)\n", + "lp_simple_engine = lp_simple.as_query_engine(similarity_top_k=2)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "906057ddf157" + }, + "source": [ + "### Option 2 - LlamaParse and Vertex AI Vector Search\n", + "\n", + "This approach is a more customized approach by defining the Vector Search mechanism through Vertex AI and extracting metadata that will be embedded and stored in the search index. \n", + "\n", + "Using metadata in Retrieval Augmented Generation (RAG) improves accuracy and context by focusing searches and providing additional information. This leads to efficient filtering, ranking, and personalized responses tailored to user needs and history. Metadata also facilitates handling complex multi-criteria queries, making RAG systems more versatile and effective.\n", + "\n", + "The following section will:\n", + "+ Parse the documents using LlamaParse\n", + "+ Extract metadata from documents returned from LlamaParse\n", + "+ Create metadata embeddings attached to each document\n", + "+ Create index in Vertex AI Vector Store\n", + "+ Query against the Vector Store" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "0ab41643425c" + }, + "source": [ + "#### Parse data using LlamaParse" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "d4d5d47ad69e" + }, + "outputs": [], + "source": [ + "documents = parser.load_data(\"./data/goog-10-q-q2-2024.pdf\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "e5bde318580a" + }, + "source": [ + "#### Create Metadata from Nodes\n", + "\n", + "Using extractors we will generate meta-data for each node. The metadata is generated using Gemini-Pro and focuses on what questions can this text answer and what key words are meaningful in this section. Each metadata piece will be embedded with Gemini text-embedding model. \n", + "\n", + "Creating metadata can be useful for another lookup criteria during RAG based search." + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "metadata": { + "id": "ea18fa35d496" + }, + "outputs": [], + "source": [ + "extractors = [\n", + " QuestionsAnsweredExtractor(questions=3, llm=llm),\n", + " KeywordExtractor(keywords=10, llm=llm),\n", + "]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "fd57de02eb9b" + }, + "outputs": [], + "source": [ + "# Run metadata transformation pipeline.\n", + "pipeline = IngestionPipeline(\n", + " transformations=extractors, # type: ignore\n", + ")\n", + "nodes = await pipeline.arun(documents=documents, in_place=False)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "8929ab07f054" + }, + "source": [ + "Example metadata that was generated:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "b41541f14b77" + }, + "outputs": [], + "source": [ + "print(nodes[1].metadata)" + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "metadata": { + "id": "476642ff9305" + }, + "outputs": [], + "source": [ + "# Generate embeddings for each metadata node\n", + "for node in nodes:\n", + " node_embedding = embedding_model.get_text_embedding(\n", + " node.get_content(metadata_mode=\"all\")\n", + " )\n", + " node.embedding = node_embedding" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "6e52150147f8" + }, + "source": [ + "#### Load Nodes into Predefined Vector Store\n", + "\n", + "This following section required a preexisting Vertex AI Vector Store. Vector stores contain embedding vectors of ingested document chunks.\n", + "\n", + "For information to create a vector store, refer to this link https://docs.llamaindex.ai/en/stable/examples/vector_stores/VertexAIVectorSearchDemo/" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "f8cdc3678a71" + }, + "outputs": [], + "source": [ + "vector_store = VertexAIVectorStore(\n", + " project_id=PROJECT_ID,\n", + " region=REGION,\n", + " index_id=\"\", # Add in your Vertex AI Vector Search Index ID\n", + " endpoint_id=\"\", # Add in your Vertex AI Vector Search Deployed Index ID\n", + " gcs_bucket_name=GCS_BUCKET,\n", + ")\n", + "\n", + "# Only need to run once\n", + "vector_store.add(nodes)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "d22ef6b3a7e1" + }, + "source": [ + "#### Create a search index and search and query the Vector Store" + ] + }, + { + "cell_type": "code", + "execution_count": 26, + "metadata": { + "id": "c135cf0ee925" + }, + "outputs": [], + "source": [ + "lp_index = VectorStoreIndex.from_vector_store(vector_store)\n", + "lp_query_engine = lp_index.as_query_engine(similarity_top_k=2)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "7064bdd2c434" + }, + "source": [ + "## Query Comparison between LlamaIndex and LlamaParse\n", + "Below are queries that responses can be found in the 10Q document within complex tables. Let's see how each approach compares." + ] + }, + { + "cell_type": "code", + "execution_count": 27, + "metadata": { + "id": "bef0f00ab246" + }, + "outputs": [], + "source": [ + "queries = [\n", + " \"What are the total cash, cash equivalents, and marketable securities as of Dec 23 2023\",\n", + " \"Total investments with fair value change reflected in other comprehensive income as of Dec 23 2023\",\n", + " \"What is the corporate debt securities unrealized loss as of Dec 31 2023 for 12 months or greater?\",\n", + " \"What is the coupon rate for total outstanding debt\",\n", + " \"Provide the table of share repurchases\",\n", + "]" + ] + }, + { + "cell_type": "code", + "execution_count": 28, + "metadata": { + "id": "a0c410e45aea" + }, + "outputs": [], + "source": [ + "def print_output(response: Response):\n", + " print(\"Response:\")\n", + " print(\"-\" * 80)\n", + " print(colored(response.response, color=\"red\"))\n", + " print(\"-\" * 80)\n", + " print(\"Source Documents:\")\n", + " print(\"-\" * 80)\n", + " for source in response.source_nodes:\n", + " print(f\"Sample Text: {source.text[:100]}\")\n", + " print(f\"Relevance score: {source.get_score():.3f}\")\n", + " print(f\"File Name: {source.metadata.get('file_name')}\")\n", + " print(f\"Page #: {source.metadata.get('page_label')}\")\n", + " print(f\"File Path: {source.metadata.get('file_path')}\")\n", + " print(\"-\" * 80)\n", + "\n", + "\n", + "def run_query(query_idx: int):\n", + " query = queries[query_idx]\n", + " print(\"Query: \" + query)\n", + " print(colored(\"LlamaIndex SimpleDirectoryReader response....\\n\", color=\"blue\"))\n", + " print_output(simple_query_engine.query(query))\n", + "\n", + " print(\n", + " colored(\n", + " \"LlamaIndex LangChainNodeParser on LlamaIndex response....\\n\", color=\"blue\"\n", + " )\n", + " )\n", + " print_output(lg_query_engine.query(query))\n", + "\n", + " print(colored(\"LlamaParse Simple response....\\n\", color=\"blue\"))\n", + " print_output(lp_simple_engine.query(query))\n", + "\n", + " print(colored(\"LlamaParse on Vertex AI response....\\n\", color=\"blue\"))\n", + " print_output(lp_query_engine.query(query))\n", + " print(\"###################################################\\n\\n\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "3a90e4c451cf" + }, + "outputs": [], + "source": [ + "run_query(query_idx=0)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "8095a2092eab" + }, + "outputs": [], + "source": [ + "run_query(query_idx=1)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "fe4cac0c796c" + }, + "outputs": [], + "source": [ + "run_query(query_idx=2)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "e4deb4eeaa0d" + }, + "outputs": [], + "source": [ + "run_query(query_idx=3)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "c837ac4f41f5" + }, + "outputs": [], + "source": [ + "run_query(query_idx=4)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "ceb7a1bbb11e" + }, + "source": [ + "## Observations\n", + "\n", + "### Answer Key\n", + "| Query | Answer | Citation page |\n", + "|------------------------------------------------------------------------------------------------------|------------------|---------------|\n", + "| \"What are the total cash, cash equivalents, and marketable securities as of Dec 23 2023\" | $110,916 million | 5 |\n", + "| \"Total investments with fair value change reflected in other comprehensive income as of Dec 23 2023\" | $78,917 million | 13 |\n", + "| \"What is the corporate debt securities unrealized loss as of Dec 31 2023 for 12 months or greater? | 592 million | 15 |\n", + "| \"What is the coupon rate for total outstanding debt\" | 0.45-2.25% | 22 |\n", + "| \"Provide the table of share repurchases\" | Table | 27 or 49 |\n", + "\n", + "### Generated Answers\n", + "| Document Parsing Technique | Query 1 | Query 2 | Query 3 | Query 4 | Query 5 |\n", + "|------------------------------------------|---------|---------|---------|---------|---------|\n", + "| LlamaIndex - SimpleDirectoryReader | (✓) | (✓) | (✓) | (✓) | (✓) |\n", + "| LlamaIndex - LangChainNodeParser | (✓) | (✓) | (✓) | (✓) | (✓) |\n", + "| LlamaParse - SimpleDirectoryReader | (✓) | (✓) | (✓)| (✓) | (✓) |\n", + "| LlamaParse - Vertex AI Vector Search | (✓) | (✓) | (✓)| (✓) | (✓) |\n", + "\n", + "## Conclusion\n", + "\n", + "There are many ways to customize your data ingestion and retrieval pipelines for custom RAG applications. This notebook was an overview to a handful of options that work in combination with Google Gemini models. " + ] + } + ], + "metadata": { + "colab": { + "name": "doc_parsing_with_llamaindex_and_llamaparse.ipynb", + "toc_visible": true + }, + "kernelspec": { + "display_name": "Python 3", + "name": "python3" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} diff --git a/gemini/use-cases/document-processing/document_processing.ipynb b/gemini/use-cases/document-processing/document_processing.ipynb new file mode 100644 index 0000000..b359263 --- /dev/null +++ b/gemini/use-cases/document-processing/document_processing.ipynb @@ -0,0 +1,1355 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "ijGzTHJJUCPY" + }, + "outputs": [], + "source": [ + "# Copyright 2024 Google LLC\n", + "#\n", + "# Licensed under the Apache License, Version 2.0 (the \"License\");\n", + "# you may not use this file except in compliance with the License.\n", + "# You may obtain a copy of the License at\n", + "#\n", + "# https://www.apache.org/licenses/LICENSE-2.0\n", + "#\n", + "# Unless required by applicable law or agreed to in writing, software\n", + "# distributed under the License is distributed on an \"AS IS\" BASIS,\n", + "# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n", + "# See the License for the specific language governing permissions and\n", + "# limitations under the License." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "VEqbX8OhE8y9" + }, + "source": [ + "# Document Processing with Gemini\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
    \n", + " \n", + " \"Google
    Run in Colab\n", + "
    \n", + "
    \n", + " \n", + " \"Google
    Run in Colab Enterprise\n", + "
    \n", + "
    \n", + " \n", + " \"GitHub
    View on GitHub\n", + "
    \n", + "
    \n", + " \n", + " \"Vertex
    Open in Vertex AI Workbench\n", + "
    \n", + "
    \n", + " \n", + " \"Google
    Open in Cloud Skills Boost\n", + "
    \n", + "
    \n", + "\n", + "
    \n", + "\n", + "Share to:\n", + "\n", + "\n", + " \"LinkedIn\n", + "\n", + "\n", + "\n", + " \"Bluesky\n", + "\n", + "\n", + "\n", + " \"X\n", + "\n", + "\n", + "\n", + " \"Reddit\n", + "\n", + "\n", + "\n", + " \"Facebook\n", + " \n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "fb49ff2efb96" + }, + "source": [ + "| Authors |\n", + "| --- |\n", + "| [Holt Skinner](https://github.com/holtskinner) |\n", + "| [Renato Leite](https://github.com/leiterenato) |" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "CkHPv2myT2cx" + }, + "source": [ + "## Overview\n", + "\n", + "In today's information-driven world, the volume of digital documents generated daily is staggering. From emails and reports to legal contracts and scientific papers, businesses and individuals alike are inundated with vast amounts of textual data. Extracting meaningful insights from these documents efficiently and accurately has become a paramount challenge.\n", + "\n", + "Document processing involves a range of tasks, including text extraction, classification, summarization, and translation, among others. Traditional methods often rely on rule-based algorithms or statistical models, which may struggle with the nuances and complexities of natural language.\n", + "\n", + "Generative AI offers a promising alternative to understand, generate, and manipulate text using natural language prompting. Gemini on Vertex AI allows these models to be used in a scalable manner through:\n", + "\n", + "- [Vertex AI Studio](https://cloud.google.com/generative-ai-studio) in the Cloud Console\n", + "- [Vertex AI REST API](https://cloud.google.com/vertex-ai/docs/reference/rest)\n", + "- [Google Gen AI SDK for Python](https://cloud.google.com/vertex-ai/generative-ai/docs/sdks/overview)\n", + "\n", + "For more information, see the [Generative AI on Vertex AI](https://cloud.google.com/vertex-ai/docs/generative-ai/learn/overview) documentation.\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "DrkcqHrrwMAo" + }, + "source": [ + "### Objectives\n", + "\n", + "In this tutorial, you will learn how to use the Gemini API in Vertex AI with the Google Gen AI SDK for Python to process PDF documents.\n", + "\n", + "You will complete the following tasks:\n", + "\n", + "- Install the SDK\n", + "- Use the Gemini 3 Flash model to:\n", + " - Extract structured entities from an unstructured document\n", + " - Classify document types\n", + " - Combine classification and entity extraction into a single workflow\n", + " - Answer questions from documents\n", + " - Summarize documents\n", + " - Extract Table Data as HTML\n", + " - Translate documents\n", + " - Compare and contrast similar documents\n", + " - Identify and extract relevant pages from a PDF" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "C9nEPojogw-g" + }, + "source": [ + "### Costs\n", + "\n", + "This tutorial uses billable components of Google Cloud:\n", + "\n", + "- Vertex AI\n", + "\n", + "Learn about [Vertex AI pricing](https://cloud.google.com/vertex-ai/pricing) and use the [Pricing Calculator](https://cloud.google.com/products/calculator/) to generate a cost estimate based on your projected usage.\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "r11Gu7qNgx1p" + }, + "source": [ + "## Getting Started\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "No17Cw5hgx12" + }, + "source": [ + "### Install Google Gen AI SDK for Python\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "tFy3H3aPgx12" + }, + "outputs": [], + "source": [ + "%pip install --upgrade --quiet google-genai pypdf pydantic" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "dmWOrTJ3gx13" + }, + "source": [ + "### Authenticate your notebook environment (Colab only)\n", + "\n", + "If you are running this notebook on Google Colab, run the following cell to authenticate your environment. This step is not required if you are using [Vertex AI Workbench](https://cloud.google.com/vertex-ai-workbench).\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "NyKGtVQjgx13" + }, + "outputs": [], + "source": [ + "import sys\n", + "\n", + "# Additional authentication is required for Google Colab\n", + "if \"google.colab\" in sys.modules:\n", + " # Authenticate user to Google Cloud\n", + " from google.colab import auth\n", + "\n", + " auth.authenticate_user()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "DF4l8DTdWgPY" + }, + "source": [ + "### Set Google Cloud project information and create client\n", + "\n", + "To get started using Vertex AI, you must have an existing Google Cloud project and [enable the Vertex AI API](https://console.cloud.google.com/flows/enableapi?apiid=aiplatform.googleapis.com).\n", + "\n", + "Learn more about [setting up a project and a development environment](https://cloud.google.com/vertex-ai/docs/start/cloud-environment)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "Nqwi-5ufWp_B" + }, + "outputs": [], + "source": [ + "import os\n", + "\n", + "from google import genai\n", + "\n", + "# fmt: off\n", + "PROJECT_ID = \"[your-project-id]\" # @param {type: \"string\", placeholder: \"[your-project-id]\", isTemplate: true}\n", + "# fmt: on\n", + "if not PROJECT_ID or PROJECT_ID == \"[your-project-id]\":\n", + " PROJECT_ID = str(os.environ.get(\"GOOGLE_CLOUD_PROJECT\"))\n", + "\n", + "LOCATION = os.environ.get(\"GOOGLE_CLOUD_REGION\", \"us-central1\")\n", + "\n", + "client = genai.Client(enterprise=True, project=PROJECT_ID, location=LOCATION)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "jXHfaVS66_01" + }, + "source": [ + "### Import libraries\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "lslYAvw37JGQ" + }, + "outputs": [], + "source": [ + "import json\n", + "from datetime import date\n", + "from enum import Enum\n", + "\n", + "import pypdf\n", + "from IPython.display import HTML, Markdown, display\n", + "from google.genai.types import GenerateContentConfig, Part\n", + "from pydantic import BaseModel, Field\n", + "\n", + "PDF_MIME_TYPE = \"application/pdf\"\n", + "JSON_MIME_TYPE = \"application/json\"\n", + "ENUM_MIME_TYPE = \"text/x.enum\"" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "FTMywdzUORIA" + }, + "source": [ + "### Load the Gemini model\n", + "\n", + "Gemini is a multimodal model that supports multimodal prompts. You can include text, image(s), and video in your prompt requests and get text or code responses.\n", + "\n", + "Learn more about all [Gemini models on Vertex AI](https://cloud.google.com/vertex-ai/generative-ai/docs/learn/models#gemini-models)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "e771399cfc79" + }, + "outputs": [], + "source": [ + "MODEL_ID = \"gemini-3.5-flash\" # @param {type: \"string\"}" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "Wy75sLb-yjNn" + }, + "source": [ + "## Entity Extraction\n", + "\n", + "[Named Entity Extraction](https://en.wikipedia.org/wiki/Named-entity_recognition) is a technique of Natural Language Processing to identify specific fields and values from unstructured text. For example, you can find key-value pairs from a filled out form, or get all of the important data from an invoice categorized by the type." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "7a75f6e4bd54" + }, + "source": [ + "### Extract entities from an invoice\n", + "\n", + "In this example, you will use a sample invoice and get all of the information in a structured format.\n", + "\n", + "This is the prompt to be sent to Gemini along with the PDF document. Feel free to edit this for your specific use case." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "0841cb312d46" + }, + "outputs": [], + "source": [ + "entity_extraction_system_instruction = \"\"\"You are a document entity extraction specialist. Given a document, your task is to extract the text value of the entities provided in the schema.\n", + "- The values must only include text found in the document\n", + "- Do not normalize any entity values.\n", + "\"\"\"" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "802016a08f79" + }, + "source": [ + "We will use [Structured output](https://cloud.google.com/vertex-ai/generative-ai/docs/multimodal/control-generated-output) to tell the model which fields need to be extracted.\n", + "\n", + "The response schema is specified in the `response_schema` parameter in `config`, and the model output will strictly follow that schema.\n", + "\n", + "You can provide the schemas as [Pydantic](https://docs.pydantic.dev/) models or a [JSON](https://www.json.org/json-en.html) string and the model will respond as JSON or an [Enum](https://docs.python.org/3/library/enum.html) depending on the value set in `response_mime_type`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "608a06507932" + }, + "outputs": [], + "source": [ + "class Address(BaseModel):\n", + " \"\"\"Represents the geographic location details of a person or organization.\"\"\"\n", + "\n", + " street: str | None = Field(\n", + " None, description=\"The street address, e.g., 123 Main St\"\n", + " )\n", + " city: str | None = Field(None, description=\"The city, e.g., Springfield\")\n", + " state: str | None = Field(None, description=\"The state or province code, e.g., IL\")\n", + " postal_code: str | None = Field(\n", + " None, description=\"The postal or ZIP code, e.g., 62704\"\n", + " )\n", + " country: str | None = Field(None, description=\"The country, e.g., USA\")\n", + "\n", + "\n", + "class LineItem(BaseModel):\n", + " \"\"\"Represents an individual product or service entry listed in a document.\"\"\"\n", + "\n", + " amount: float = Field(..., description=\"The total amount for this line item\")\n", + " description: str | None = Field(\n", + " None, description=\"Description of the product/service, e.g., Laptop\"\n", + " )\n", + " product_code: str | None = Field(\n", + " None, description=\"The SKU or product code, e.g., LPT-001\"\n", + " )\n", + " quantity: int = Field(..., description=\"The number of units\")\n", + " unit: str | None = Field(None, description=\"Unit of measure, e.g., pcs\")\n", + " unit_price: float = Field(..., description=\"Price per single unit\")\n", + "\n", + "\n", + "class VAT(BaseModel):\n", + " \"\"\"Represents Value Added Tax details, including rates and calculated amounts.\"\"\"\n", + "\n", + " amount: float = Field(..., description=\"Taxable amount\")\n", + " category_code: str | None = Field(None, description=\"Tax category code, e.g., A\")\n", + " tax_amount: float | None = Field(None, description=\"The calculated tax value\")\n", + " tax_rate: float | None = Field(\n", + " None, description=\"Percentage as a float, e.g., 10 for 10%\"\n", + " )\n", + " total_amount: float = Field(..., description=\"Total amount including tax\")\n", + "\n", + "\n", + "class Party(BaseModel):\n", + " \"\"\"Represents the contact and identification details of a transacting entity.\"\"\"\n", + "\n", + " name: str = Field(..., description=\"Name of the entity, e.g., Google\")\n", + " street: str | None = Field(None, description=\"Street address\")\n", + " city: str | None = Field(None, description=\"City name\")\n", + " state: str | None = Field(None, description=\"State or province\")\n", + " postal_code: str | None = Field(None, description=\"Postal code\")\n", + " country: str | None = Field(None, description=\"Country name\")\n", + " email: str | None = Field(None, description=\"Contact email address\")\n", + " phone: str | None = Field(None, description=\"Contact phone number\")\n", + " website: str | None = Field(None, description=\"Entity website URL\")\n", + " tax_id: str | None = Field(None, description=\"Tax identification number\")\n", + " registration: str | None = Field(None, description=\"Business registration number\")\n", + " iban: str | None = Field(None, description=\"International Bank Account Number\")\n", + " payment_ref: str | None = Field(\n", + " None, description=\"Payment reference or invoice number\"\n", + " )\n", + "\n", + "\n", + "class Invoice(BaseModel):\n", + " \"\"\"Represents the comprehensive structured data extracted from an invoice document.\"\"\"\n", + "\n", + " invoice_id: str = Field(..., description=\"The unique identifier of the invoice\")\n", + " invoice_date: str = Field(\n", + " ..., description=\"The date the invoice was issued (YYYY-MM-DD)\"\n", + " )\n", + " supplier: Party\n", + " receiver: Party\n", + " line_items: list[LineItem]\n", + " vat: list[VAT]" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "91dcaf17c2ce" + }, + "source": [ + "For this example, we will download a PDF document to local storage and send the file bytes to the API for processing.\n", + "\n", + "You can view the document [here](https://storage.googleapis.com/cloud-samples-data/generative-ai/pdf/invoice.pdf)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "42b044f767e3" + }, + "outputs": [], + "source": [ + "# Download a PDF from Google Cloud Storage\n", + "! gsutil cp \"gs://cloud-samples-data/generative-ai/pdf/invoice.pdf\" ./invoice.pdf" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "KzqjpEiryjNo" + }, + "outputs": [], + "source": [ + "# Load file bytes\n", + "with open(\"invoice.pdf\", \"rb\") as f:\n", + " file_bytes = f.read()\n", + "\n", + "# Send to Gemini API\n", + "response = client.models.generate_content(\n", + " model=MODEL_ID,\n", + " contents=[\n", + " \"The following document is an invoice.\",\n", + " Part.from_bytes(data=file_bytes, mime_type=PDF_MIME_TYPE),\n", + " ],\n", + " config=GenerateContentConfig(\n", + " system_instruction=entity_extraction_system_instruction,\n", + " response_schema=Invoice,\n", + " response_mime_type=JSON_MIME_TYPE,\n", + " ),\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "131e8044cf70" + }, + "source": [ + "We can load the extracted data as an object using the `response.parsed` field." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "63f7f16fabc7" + }, + "outputs": [], + "source": [ + "invoice_data = response.parsed\n", + "print(\"\\n-------Extracted Entities--------\")\n", + "print(invoice_data)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "c82b9d10e9d1" + }, + "source": [ + "Or the response can then be parsed as JSON into a Python dictionary for use in other applications." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "ce9731cb0a84" + }, + "outputs": [], + "source": [ + "json_object = json.loads(response.text)\n", + "print(json_object)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "c7cdda6aa720" + }, + "source": [ + "You can see that Gemini extracted all of the relevant fields from the document." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "4dca9fa02c05" + }, + "source": [ + "### Extract entities from a payslip\n", + "\n", + "Let's try with another type of document, a payslip or paystub.\n", + "\n", + "In this example, we will use a document hosted on Google Cloud Storage and process it by passing the URI.\n", + "\n", + "You can view the document [here](https://storage.googleapis.com/cloud-samples-data/generative-ai/pdf/earnings_statement.pdf)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "3ca20cd3f738" + }, + "outputs": [], + "source": [ + "class Payslip(BaseModel):\n", + " \"\"\"Represents the structured payroll, tax withholding, and employment details extracted from a payslip.\"\"\"\n", + "\n", + " employee_id: str = Field(..., description=\"Unique identifier for the employee\")\n", + " employee_name: str = Field(..., description=\"Full name of the employee\")\n", + " pay_period_start: date = Field(..., description=\"Start date of the pay period\")\n", + " pay_period_end: date = Field(..., description=\"End date of the pay period\")\n", + " gross_income: float = Field(..., description=\"Total income before deductions\")\n", + " federal_tax: float = Field(..., description=\"Federal tax deduction amount\")\n", + " state_tax: float | None = Field(\n", + " 0.0, description=\"State tax deduction amount, if applicable\"\n", + " )\n", + " social_security: float = Field(..., description=\"Social Security deduction amount\")\n", + " medicare: float = Field(..., description=\"Medicare deduction amount\")\n", + " other_deductions: float | None = Field(\n", + " 0.0, description=\"Other deductions (e.g., health insurance, retirement)\"\n", + " )\n", + " net_income: float = Field(..., description=\"Income after all deductions\")\n", + " payment_date: date = Field(..., description=\"Date the payment was issued\")\n", + " hours_worked: float | None = Field(\n", + " None, description=\"Total hours worked in the pay period\"\n", + " )\n", + " hourly_rate: float | None = Field(\n", + " None, description=\"Employee's hourly rate, if applicable\"\n", + " )" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "06d34a6f08d9" + }, + "outputs": [], + "source": [ + "response = client.models.generate_content(\n", + " model=MODEL_ID,\n", + " contents=[\n", + " \"The following document is a Payslip.\",\n", + " Part.from_uri(\n", + " file_uri=\"gs://cloud-samples-data/generative-ai/pdf/earnings_statement.pdf\",\n", + " mime_type=PDF_MIME_TYPE,\n", + " ),\n", + " ],\n", + " config=GenerateContentConfig(\n", + " system_instruction=entity_extraction_system_instruction,\n", + " response_schema=Payslip,\n", + " response_mime_type=JSON_MIME_TYPE,\n", + " ),\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "230b3ae51289" + }, + "outputs": [], + "source": [ + "print(\"\\n-------Extracted Entities--------\")\n", + "print(response.parsed)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "Uhtahn_jTZKC" + }, + "source": [ + "## Document Classification\n", + "\n", + "Document classification is the process for identifying the type of document. For example, invoice, W-2, receipt, etc.\n", + "\n", + "In this example, you will use a [sample tax form (W-9)](https://storage.googleapis.com/cloud-samples-data/generative-ai/pdf/w9.pdf) and get the specific type of document from a specified `Enum`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "d797c2bfb490" + }, + "outputs": [], + "source": [ + "classification_prompt = \"\"\"You are a document classification specialist. Given a document, your task is to find which category the document belongs to from the document categories provided in the schema.\"\"\"\n", + "\n", + "\n", + "class DocumentCategory(Enum):\n", + " \"\"\"Specifies the supported classifications for financial, tax, and identification documents.\"\"\"\n", + "\n", + " TAX_1040_2019 = \"1040_2019\"\n", + " TAX_1040_2020 = \"1040_2020\"\n", + " TAX_1099_R = \"1099-r\"\n", + " BANK_STATEMENT = \"bank_statement\"\n", + " CREDIT_CARD_STATEMENT = \"credit_card_statement\"\n", + " EXPENSE = \"expense\"\n", + " TAX_1120S_2019 = \"form_1120S_2019\"\n", + " TAX_1120S_2020 = \"form_1120S_2020\"\n", + " INVESTMENT_RETIREMENT_STATEMENT = \"investment_retirement_statement\"\n", + " INVOICE = \"invoice\"\n", + " PAYSTUB = \"paystub\"\n", + " PROPERTY_INSURANCE = \"property_insurance\"\n", + " PURCHASE_ORDER = \"purchase_order\"\n", + " UTILITY_STATEMENT = \"utility_statement\"\n", + " W2 = \"w2\"\n", + " W9 = \"w9\"\n", + " DRIVER_LICENSE = \"driver_license\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "7dcab4a008a5" + }, + "outputs": [], + "source": [ + "response = client.models.generate_content(\n", + " model=MODEL_ID,\n", + " contents=[\n", + " \"Classify the following document.\",\n", + " Part.from_uri(\n", + " file_uri=\"https://storage.googleapis.com/cloud-samples-data/generative-ai/pdf/w9.pdf\",\n", + " mime_type=PDF_MIME_TYPE,\n", + " ),\n", + " ],\n", + " config=GenerateContentConfig(\n", + " system_instruction=classification_prompt,\n", + " response_schema=DocumentCategory,\n", + " response_mime_type=ENUM_MIME_TYPE,\n", + " ),\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "200922ddac39" + }, + "outputs": [], + "source": [ + "print(\"\\n-------Document Classification--------\")\n", + "print(response.text)\n", + "print(response.parsed)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "d99b968e9faa" + }, + "source": [ + "You can see that Gemini successfully categorized the document." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "9c41c7273b66" + }, + "source": [ + "### Chaining Classification and Extraction\n", + "\n", + "These techniques can also be chained together to extract any number of document types.\n", + "\n", + "For example, if you have multiple types of documents to process, you can send each document to Gemini with a classification prompt, then based on that output, you can write logic to decide which extraction prompt to use.\n", + "\n", + "These are the sample documents:\n", + "\n", + "- [US Driver License](https://storage.googleapis.com/cloud-samples-data/documentai/SampleDocuments/US_DRIVER_LICENSE_PROCESSOR/dl3.pdf)\n", + "- [Invoice](https://storage.googleapis.com/cloud-samples-data/documentai/SampleDocuments/INVOICE_PROCESSOR/google_invoice.pdf)\n", + "- [Form W-2](https://storage.googleapis.com/cloud-samples-data/documentai/SampleDocuments/FORM_W2_PROCESSOR/2020FormW-2.pdf)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "69fd5883a812" + }, + "outputs": [], + "source": [ + "class W2Form(BaseModel):\n", + " \"\"\"Represents the wage and tax information extracted from a standard US W-2 form.\"\"\"\n", + "\n", + " control_number: str | None = Field(\n", + " None, description=\"The employer's internal control number, if available\"\n", + " )\n", + " ein: str = Field(..., description=\"Employer Identification Number (EIN)\")\n", + "\n", + " employee_first_name: str = Field(\n", + " ..., description=\"Legal first name of the employee\"\n", + " )\n", + " employee_last_name: str = Field(..., description=\"Legal last name of the employee\")\n", + " employee_address_street: str = Field(\n", + " ..., description=\"Employee's residential street address\"\n", + " )\n", + " employee_address_city: str = Field(..., description=\"Employee's residential city\")\n", + " employee_address_state: str = Field(\n", + " ..., description=\"Employee's residential state abbreviation\"\n", + " )\n", + " employee_address_zip: str = Field(\n", + " ..., description=\"Employee's residential postal code\"\n", + " )\n", + "\n", + " employer_name: str = Field(..., description=\"Legal name of the employing entity\")\n", + " employer_address_street: str = Field(\n", + " ..., description=\"Employer's business street address\"\n", + " )\n", + " employer_address_city: str = Field(..., description=\"Employer's business city\")\n", + " employer_address_state: str = Field(\n", + " ..., description=\"Employer's business state abbreviation\"\n", + " )\n", + " employer_address_zip: str = Field(\n", + " ..., description=\"Employer's business postal code\"\n", + " )\n", + " employer_state_id_number: str | None = Field(\n", + " None, description=\"The employer's state-specific identification number\"\n", + " )\n", + "\n", + " wages_tips_other_compensation: float = Field(\n", + " ..., description=\"Box 1: Total taxable wages, tips, and other compensation\"\n", + " )\n", + " federal_income_tax_withheld: float = Field(\n", + " ..., description=\"Box 2: Total federal income tax withheld\"\n", + " )\n", + " social_security_wages: float = Field(\n", + " ..., description=\"Box 3: Total wages subject to Social Security tax\"\n", + " )\n", + " social_security_tax_withheld: float = Field(\n", + " ..., description=\"Box 4: Total Social Security tax withheld\"\n", + " )\n", + " medicare_wages_and_tips: float = Field(\n", + " ..., description=\"Box 5: Total wages and tips subject to Medicare tax\"\n", + " )\n", + " medicare_tax_withheld: float = Field(\n", + " ..., description=\"Box 6: Total Medicare tax withheld\"\n", + " )\n", + "\n", + " state: str | None = Field(\n", + " None, description=\"Box 15: State abbreviation for state-level reporting\"\n", + " )\n", + " state_wages_tips_etc: float | None = Field(\n", + " None, description=\"Box 16: Total wages subject to state income tax\"\n", + " )\n", + " state_income_tax: float | None = Field(\n", + " None, description=\"Box 17: Total state income tax withheld\"\n", + " )\n", + "\n", + " box_12_code: str | None = Field(\n", + " None,\n", + " description=\"Box 12: Alpha code for specific compensation types (e.g., D, J, AA)\",\n", + " )\n", + " box_12_value: str | None = Field(\n", + " None, description=\"Box 12: The dollar amount associated with the alpha code\"\n", + " )\n", + "\n", + " form_year: int = Field(\n", + " ...,\n", + " description=\"The specific tax year for which the W-2 was issued (e.g., 2023)\",\n", + " )\n", + "\n", + "\n", + "class DriversLicense(BaseModel):\n", + " \"\"\"Captures personal identity and licensing details extracted from a government-issued driver's license.\"\"\"\n", + "\n", + " address: str = Field(\n", + " ..., title=\"Address\", description=\"The address of the individual.\"\n", + " )\n", + " date_of_birth: date = Field(\n", + " ..., title=\"Date of Birth\", description=\"The birthdate of the individual.\"\n", + " )\n", + " document_id: str = Field(\n", + " ...,\n", + " title=\"Document ID\",\n", + " description=\"The unique document ID for the driver's license.\",\n", + " )\n", + " expiration_date: date = Field(\n", + " ...,\n", + " title=\"Expiration Date\",\n", + " description=\"The expiration date of the driver's license.\",\n", + " )\n", + " family_name: str = Field(\n", + " ...,\n", + " title=\"Family Name\",\n", + " description=\"The family name (last name) of the individual.\",\n", + " )\n", + " given_names: str = Field(\n", + " ...,\n", + " title=\"Given Names\",\n", + " description=\"The given names (first and middle names) of the individual.\",\n", + " )\n", + " issue_date: date = Field(\n", + " ..., title=\"Issue Date\", description=\"The issue date of the driver's license.\"\n", + " )\n", + "\n", + "\n", + "# Map classification types to schemas\n", + "classification_to_schema = {\n", + " DocumentCategory.INVOICE: Invoice,\n", + " DocumentCategory.W2: W2Form,\n", + " DocumentCategory.DRIVER_LICENSE: DriversLicense,\n", + "}" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "2c806b4d757e" + }, + "outputs": [], + "source": [ + "gcs_uris = [\n", + " \"gs://cloud-samples-data/documentai/SampleDocuments/US_DRIVER_LICENSE_PROCESSOR/dl3.pdf\",\n", + " \"gs://cloud-samples-data/documentai/SampleDocuments/INVOICE_PROCESSOR/google_invoice.pdf\",\n", + " \"gs://cloud-samples-data/documentai/SampleDocuments/FORM_W2_PROCESSOR/2020FormW-2.pdf\",\n", + "]\n", + "\n", + "for gcs_uri in gcs_uris:\n", + " print(f\"\\nFile: {gcs_uri}\\n\")\n", + "\n", + " # Send to Gemini with Classification Prompt\n", + " classification_response = client.models.generate_content(\n", + " model=MODEL_ID,\n", + " contents=[\n", + " \"Classify the following document.\",\n", + " Part.from_uri(file_uri=gcs_uri, mime_type=PDF_MIME_TYPE),\n", + " ],\n", + " config=GenerateContentConfig(\n", + " system_instruction=classification_prompt,\n", + " response_schema=DocumentCategory,\n", + " response_mime_type=ENUM_MIME_TYPE,\n", + " ),\n", + " )\n", + "\n", + " print(f\"Document Classification: {classification_response.text}\")\n", + "\n", + " # Get Extraction schema based on Classification\n", + " extraction_schema = classification_to_schema.get(classification_response.parsed)\n", + "\n", + " if not extraction_schema:\n", + " print(\"Document does not belong to a specified class. Skipping extraction.\")\n", + " continue\n", + "\n", + " # Send to Gemini with Extraction Prompt\n", + " extraction_response = client.models.generate_content(\n", + " model=MODEL_ID,\n", + " contents=[\n", + " f\"Extract the entities from the following {classification_response.text} document.\",\n", + " Part.from_uri(file_uri=gcs_uri, mime_type=PDF_MIME_TYPE),\n", + " ],\n", + " config=GenerateContentConfig(\n", + " system_instruction=classification_prompt,\n", + " response_schema=extraction_schema,\n", + " response_mime_type=JSON_MIME_TYPE,\n", + " ),\n", + " )\n", + "\n", + " print(\"\\n-------Extracted Entities--------\")\n", + " print(extraction_response.parsed)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "322abdb6d63d" + }, + "source": [ + "## Document Question Answering\n", + "\n", + "Gemini can be used to answer questions about a document.\n", + "\n", + "This example answers a question about the Transformer model paper [\"Attention is all you need\"](https://arxiv.org/pdf/1706.03762), we will be loading the PDF file directly from the source on [arXiv](https://arxiv.org)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "f47a8b63ce13" + }, + "outputs": [], + "source": [ + "qa_system_instruction = \"You are a question answering specialist. Given a question and a context, your task is to provide the answer to the question based on the context provided. Give the answer first, followed by an explanation.\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "636f158c24fb" + }, + "outputs": [], + "source": [ + "# Send Q&A Prompt to Gemini\n", + "response = client.models.generate_content(\n", + " model=MODEL_ID,\n", + " contents=[\n", + " \"What is the attention mechanism?\",\n", + " Part.from_uri(\n", + " file_uri=\"gs://cloud-samples-data/generative-ai/pdf/1706.03762v7.pdf\",\n", + " mime_type=PDF_MIME_TYPE,\n", + " ),\n", + " ],\n", + " config=GenerateContentConfig(\n", + " system_instruction=qa_system_instruction,\n", + " ),\n", + ")\n", + "\n", + "display(Markdown(response.text))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "d5881bdeb3b0" + }, + "source": [ + "## Document Summarization\n", + "\n", + "Gemini can also be used to summarize or paraphrase a document's contents. Your prompt can specify how detailed the summary should be or specific formatting, such as bullet points or paragraphs." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "85b23b916ffa" + }, + "outputs": [], + "source": [ + "summarization_system_instruction = \"\"\"You are a professional document summarization specialist. Given a document, your task is to provide a detailed summary of the content of the document.\n", + "\n", + "If it includes images, provide descriptions of the images.\n", + "If it includes tables, extract all elements of the tables.\n", + "If it includes graphs, explain the findings in the graphs.\n", + "Do not include any numbers that are not mentioned in the document.\n", + "\"\"\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "01c2c8c947e0" + }, + "outputs": [], + "source": [ + "# Send Summarization Prompt to Gemini\n", + "response = client.models.generate_content(\n", + " model=MODEL_ID,\n", + " contents=[\n", + " \"Summarize the following document.\",\n", + " Part.from_uri(\n", + " file_uri=\"gs://cloud-samples-data/generative-ai/pdf/fdic_board_meeting.pdf\",\n", + " mime_type=PDF_MIME_TYPE,\n", + " ),\n", + " ],\n", + " config=GenerateContentConfig(\n", + " system_instruction=summarization_system_instruction,\n", + " ),\n", + ")\n", + "\n", + "display(Markdown(\"### Document Summary\"))\n", + "display(Markdown(response.text))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "85015f00a36f" + }, + "source": [ + "## Table parsing from documents\n", + "\n", + "Gemini can parse contents of a table and return it in a structured format, such as HTML or markdown." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "b780755d42e0" + }, + "outputs": [], + "source": [ + "table_extraction_prompt = \"\"\"What is the HTML code of the table in this document?\"\"\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "2ad318a19c6a" + }, + "outputs": [], + "source": [ + "# Send Table Extraction Prompt to Gemini\n", + "response = client.models.generate_content(\n", + " model=MODEL_ID,\n", + " contents=[\n", + " table_extraction_prompt,\n", + " Part.from_uri(\n", + " file_uri=\"gs://cloud-samples-data/generative-ai/pdf/salary_table.pdf\",\n", + " mime_type=PDF_MIME_TYPE,\n", + " ),\n", + " ],\n", + ")\n", + "\n", + "display(Markdown(response.text))\n", + "display(HTML(response.text.removeprefix(\"```html\").removesuffix(\"```\")))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "1ebe7318abf6" + }, + "source": [ + "## Document Translation\n", + "\n", + "Gemini can translate documents between languages. This example translates meeting notes from English into French and Spanish." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "c03f55376e76" + }, + "outputs": [], + "source": [ + "translation_prompt = \"\"\"Translate the first paragraph into French and Spanish. Label each paragraph with the target language.\"\"\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "0e22d1c06508" + }, + "outputs": [], + "source": [ + "# Send Translation Prompt to Gemini\n", + "response = client.models.generate_content(\n", + " model=MODEL_ID,\n", + " contents=[\n", + " translation_prompt,\n", + " Part.from_uri(\n", + " file_uri=\"gs://cloud-samples-data/generative-ai/pdf/fdic_board_meeting.pdf\",\n", + " mime_type=PDF_MIME_TYPE,\n", + " ),\n", + " ],\n", + ")\n", + "\n", + "display(Markdown(\"### Translations\"))\n", + "display(Markdown(response.text))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "0e8111f438db" + }, + "source": [ + "## Document Comparison\n", + "\n", + "Gemini can compare and contrast the contents of multiple documents. This example finds the changes in the IRS Form 1040 between 2013 and 2023.\n", + "\n", + "Note: when working with multiple documents, the order can matter and should be specified in your prompt." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "62bd15c5553f" + }, + "outputs": [], + "source": [ + "comparison_prompt = \"\"\"The first document is from 2013, the second one from 2023. How did the standard deduction evolve?\"\"\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "e5f07456ed8d" + }, + "outputs": [], + "source": [ + "# Send Comparison Prompt to Gemini\n", + "response = client.models.generate_content(\n", + " model=MODEL_ID,\n", + " contents=[\n", + " comparison_prompt,\n", + " Part.from_uri(\n", + " file_uri=\"gs://cloud-samples-data/generative-ai/pdf/form_1040_2013.pdf\",\n", + " mime_type=PDF_MIME_TYPE,\n", + " ),\n", + " Part.from_uri(\n", + " file_uri=\"gs://cloud-samples-data/generative-ai/pdf/form_1040_2023.pdf\",\n", + " mime_type=PDF_MIME_TYPE,\n", + " ),\n", + " ],\n", + ")\n", + "\n", + "display(Markdown(\"### Comparison\"))\n", + "display(Markdown(response.text))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "1f99e3fb7a97" + }, + "source": [ + "## Document Page Extraction\n", + "\n", + "This example uses Gemini to identify relevant pages and creates a new, focused PDF." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "ee236963ec3d" + }, + "outputs": [], + "source": [ + "PROMPT_PAGES = \"\"\"\n", + "Return the numbers of all pages in the document above that contain information related to the question below.\n", + "\n", + " - Use the document above as your only source of information to determine which pages are related to the question below.\n", + " - Return the page numbers of the document above that are related to the question. When in doubt, return the page anyway.\n", + " - The page numbers should be in the format of a list of integers, e.g. [1, 2, 3].\n", + "\n", + "\n", + " - The document above is a financial report with various tables, charts, infographics, lists, and additional text information.\n", + " - Pay CLOSE ATTENTION to the chart legends and chart COLORS to determine the pages. Colors may indicate which information is important for determining the pages.\n", + " - The color of the chart legends represents the color of the bars in the chart.\n", + " - Use ONLY this document as context to determine the pages.\n", + " - In most cases, the page number can be found in the footer.\n", + "\n", + "\n", + "{question}\n", + "\n", + "\"\"\"\n", + "\n", + "\n", + "def pdf_slice(input_file: str, output_file: str, pages: list[int]) -> None:\n", + " \"\"\"Extracts specific pages from a PDF and saves them to a new file.\"\"\"\n", + " pdf_reader = pypdf.PdfReader(input_file)\n", + " pdf_writer = pypdf.PdfWriter()\n", + " for page_num in pages:\n", + " if 1 <= page_num <= len(pdf_reader.pages):\n", + " pdf_writer.add_page(pdf_reader.pages[page_num - 1])\n", + " pdf_writer.write(output_file)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "5a8fad605e6a" + }, + "source": [ + "Include your question and the path to your PDF from a URL." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "ffcad6183966" + }, + "outputs": [], + "source": [ + "# fmt: off\n", + "question = \"From the Consolidated Balance Sheet, what was the difference between the total assets from 2022 to 2023?\" # @param {type: \"string\"}\n", + "pdf_path = \"https://storage.googleapis.com/github-repo/generative-ai/gemini/use-cases/document-processing/CymbalBankFinancialStatements.pdf\" # @param {type: \"string\"}\n", + "# fmt: on\n", + "local_pdf = os.path.basename(pdf_path)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "25ba38644e57" + }, + "source": [ + "Extract the relevant pages using Gemini and print them." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "31408efe4118" + }, + "outputs": [], + "source": [ + "response = client.models.generate_content(\n", + " model=MODEL_ID,\n", + " contents=[\n", + " \"\",\n", + " Part.from_uri(file_uri=pdf_path, mime_type=PDF_MIME_TYPE),\n", + " \"\",\n", + " PROMPT_PAGES.format(question=question),\n", + " ],\n", + " config=GenerateContentConfig(\n", + " response_mime_type=JSON_MIME_TYPE,\n", + " response_schema=list[int],\n", + " ),\n", + ")\n", + "pages = response.parsed\n", + "print(pages)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "6d2a92e816d4" + }, + "source": [ + "Download the PDF file to local storage." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "33474e737de4" + }, + "outputs": [], + "source": [ + "!wget {pdf_path} -O {local_pdf}" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "bde5fbbbbe08" + }, + "source": [ + "To ensure we find the answer to the question, we will also retrieve the page immediately after the selected page." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "3bc18fd65be1" + }, + "outputs": [], + "source": [ + "pages" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "c05346c2724e" + }, + "outputs": [], + "source": [ + "# This includes the page and its successor (e.g., [1, 5] -> {1, 2, 5, 6})\n", + "expanded_pages = sorted({p for page in pages for p in (page, page + 1)})\n", + "\n", + "pdf_slice(local_pdf, \"sample1.pdf\", expanded_pages)" + ] + } + ], + "metadata": { + "colab": { + "name": "document_processing.ipynb", + "toc_visible": true + }, + "kernelspec": { + "display_name": "Python 3", + "name": "python3" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} diff --git a/gemini/use-cases/document-processing/patents_understanding.ipynb b/gemini/use-cases/document-processing/patents_understanding.ipynb new file mode 100644 index 0000000..4bd1826 --- /dev/null +++ b/gemini/use-cases/document-processing/patents_understanding.ipynb @@ -0,0 +1,906 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "ur8xi4C7S06n" + }, + "outputs": [], + "source": [ + "# Copyright 2024 Google LLC\n", + "#\n", + "# Licensed under the Apache License, Version 2.0 (the \"License\");\n", + "# you may not use this file except in compliance with the License.\n", + "# You may obtain a copy of the License at\n", + "#\n", + "# https://www.apache.org/licenses/LICENSE-2.0\n", + "#\n", + "# Unless required by applicable law or agreed to in writing, software\n", + "# distributed under the License is distributed on an \"AS IS\" BASIS,\n", + "# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n", + "# See the License for the specific language governing permissions and\n", + "# limitations under the License." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "JAPoU8Sm5E6e" + }, + "source": [ + "# Patents Document Understanding with Gemini\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
    \n", + " \n", + " \"Google
    Open in Colab\n", + "
    \n", + "
    \n", + " \n", + " \"Google
    Open in Colab Enterprise\n", + "
    \n", + "
    \n", + " \n", + " \"Vertex
    Open in Vertex AI Workbench\n", + "
    \n", + "
    \n", + " \n", + " \"BigQuery
    Open in BigQuery Studio\n", + "
    \n", + "
    \n", + " \n", + " \"GitHub
    View on GitHub\n", + "
    \n", + "
    \n", + "\n", + "
    \n", + "\n", + "Share to:\n", + "\n", + "\n", + " \"LinkedIn\n", + "\n", + "\n", + "\n", + " \"Bluesky\n", + "\n", + "\n", + "\n", + " \"X\n", + "\n", + "\n", + "\n", + " \"Reddit\n", + "\n", + "\n", + "\n", + " \"Facebook\n", + "" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "84f0f73a0f76" + }, + "source": [ + "| Author |\n", + "| --- |\n", + "| [Holt Skinner](https://github.com/holtskinner) |" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "tvgnzT1CKxrO" + }, + "source": [ + "## Overview\n", + "\n", + "Back in 2019, I wrote a [Google Cloud Blog post](https://cloud.google.com/blog/products/ai-machine-learning/building-a-document-understanding-pipeline-with-google-cloud) in collaboration with [Michael Munn](https://github.com/munnm) and [Michael Sherman](https://github.com/michaelwsherman) which illustrates how to build a Document Understanding Pipeline using [AutoML](https://cloud.google.com/automl).\n", + "\n", + "This example showed how to train custom machine learning models for the following tasks:\n", + "\n", + "- [Image Classification](https://cloud.google.com/vision/automl/docs/beginners-guide)\n", + "- [Entity Extraction](https://cloud.google.com/natural-language/automl/entity-analysis/docs/)\n", + "- [Text Classification](https://cloud.google.com/natural-language/automl/docs/predict)\n", + "- [Object Detection](https://cloud.google.com/vision/automl/object-detection/docs/)\n", + "\n", + "In today's world of Generative AI models like [Gemini](https://blog.google/technology/ai/google-gemini-ai/), it's possible to create the same document processing pipeline without training custom models. This significantly simplifies the process and reduces the time & resources required to automate these workflows.\n", + "\n", + "In this notebook, we'll create a document understanding pipeline on a public dataset of [patents PDFs](https://console.cloud.google.com/marketplace/details/global-patents/labeled-patents) stored in BigQuery and use [Batch Prediction for Gemini 3 in Vertex AI](https://cloud.google.com/vertex-ai/generative-ai/docs/multimodal/batch-prediction-gemini) to:\n", + "\n", + "- Classify the patent granter (US or EU).\n", + "- Classify the invention type (Medical Tech, Computer Vision, Cryptography, Other).\n", + "- Extract key entities like publication date, application number, etc.\n", + "- Detect bounding boxes for figures in the document." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "61RBz8LLbxCR" + }, + "source": [ + "## Get started" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "No17Cw5hgx12" + }, + "source": [ + "### Install Google Gen AI SDK for Python\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "tFy3H3aPgx12" + }, + "outputs": [], + "source": [ + "%pip install --upgrade --quiet google-genai google-cloud-bigquery pandas-gbq" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "R5Xep4W9lq-Z" + }, + "source": [ + "### Restart runtime\n", + "\n", + "To use the newly installed packages in this Jupyter runtime, you must restart the runtime. You can do this by running the cell below, which restarts the current kernel.\n", + "\n", + "The restart might take a minute or longer. After it's restarted, continue to the next step." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "XRvKdaPDTznN" + }, + "outputs": [], + "source": [ + "import IPython\n", + "\n", + "app = IPython.Application.instance()\n", + "app.kernel.do_shutdown(True)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "SbmM4z7FOBpM" + }, + "source": [ + "
    \n", + "⚠ The kernel is going to restart. In Colab or Colab Enterprise, you might see an error message that says \"Your session crashed for an unknown reason.\" This is expected. Wait until it's finished before continuing to the next step. ⚠\n", + "
    \n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "dmWOrTJ3gx13" + }, + "source": [ + "### Authenticate your notebook environment (Colab only)\n", + "\n", + "If you're running this notebook on Google Colab, run the cell below to authenticate your environment." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "NyKGtVQjgx13" + }, + "outputs": [], + "source": [ + "import sys\n", + "\n", + "if \"google.colab\" in sys.modules:\n", + " from google.colab import auth\n", + "\n", + " auth.authenticate_user()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "C8aMIcn9mEWt" + }, + "source": [ + "### Import libraries\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "DF4l8DTdWgPY" + }, + "source": [ + "### Set Google Cloud project information and create client\n", + "\n", + "To get started using Vertex AI, you must have an existing Google Cloud project and [enable the Vertex AI API](https://console.cloud.google.com/flows/enableapi?apiid=aiplatform.googleapis.com).\n", + "\n", + "Learn more about [setting up a project and a development environment](https://cloud.google.com/vertex-ai/docs/start/cloud-environment)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "Nqwi-5ufWp_B" + }, + "outputs": [], + "source": [ + "import os\n", + "\n", + "import pandas_gbq\n", + "from google import genai\n", + "from google.cloud import bigquery\n", + "\n", + "# fmt: off\n", + "PROJECT_ID = \"[your-project-id]\" # @param {type: \"string\", placeholder: \"[your-project-id]\", isTemplate: true}\n", + "# fmt: on\n", + "if not PROJECT_ID or PROJECT_ID == \"[your-project-id]\":\n", + " PROJECT_ID = str(os.environ.get(\"GOOGLE_CLOUD_PROJECT\"))\n", + "\n", + "LOCATION = os.environ.get(\"GOOGLE_CLOUD_REGION\", \"us-central1\")\n", + "\n", + "client = genai.Client(enterprise=True, project=PROJECT_ID, location=LOCATION)\n", + "bq_client = bigquery.Client()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "e43229f3ad4f" + }, + "source": [ + "### Load the Gemini 3 Flash model\n", + "\n", + "To learn more about all [Gemini models on Vertex AI](https://cloud.google.com/vertex-ai/generative-ai/docs/learn/models#gemini-models)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "cf93d5f0ce00" + }, + "outputs": [], + "source": [ + "MODEL_ID = \"gemini-3.5-flash\" # @param {type: \"string\"}" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "EdvJRUWRNGHE" + }, + "source": [ + "## The Prediction Pipeline\n", + "\n", + "Now, let's build the pipeline for processing the patent documents. We will:\n", + "\n", + "1. Fetch the PDF URIs from BigQuery into a Pandas DataFrame.\n", + "2. Create a detailed prompt for Gemini to extract the information.\n", + "3. Process each PDF with the prompt and save the results in a Pandas DataFrame\n", + "4. Save the structured output to a new BigQuery table" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "aa191bc13022" + }, + "source": [ + "### Get PDF URIs from BigQuery\n", + "\n", + "We'll query BigQuery to get a list of Google Cloud Storage URIs for the patent PDF files.\n", + "\n", + "NOTE: This query limits to only 5 documents to save on processing time for this tutorial." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "mzVYcJ5CYgZ3" + }, + "outputs": [], + "source": [ + "query = \"\"\"\n", + "SELECT\n", + " COALESCE(t1.gcs_path, t2.gcs_path, t3.gcs_path) AS gcs_path\n", + "FROM\n", + " `bigquery-public-data.labeled_patents.extracted_data` AS t1\n", + "FULL OUTER JOIN\n", + " `bigquery-public-data.labeled_patents.figures` AS t2\n", + "ON\n", + " t1.gcs_path = t2.gcs_path\n", + "FULL OUTER JOIN\n", + " `bigquery-public-data.labeled_patents.invention_types` AS t3\n", + "ON\n", + " COALESCE(t1.gcs_path, t2.gcs_path) = t3.gcs_path\n", + "LIMIT 5\n", + "\"\"\"\n", + "\n", + "df = bq_client.query(query).result().to_dataframe()\n", + "df" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "7ea36f19d9f7" + }, + "source": [ + "### Define the Gemini prompt\n", + "\n", + "Here's the prompt we'll use with Gemini to extract the information we need. It's a detailed instruction that specifies the output format as JSON and also includes a JSON Schema." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "jYxX_rCGe0hC" + }, + "outputs": [], + "source": [ + "PATENTS_PROMPT = \"\"\"\n", + "Given a patent document, please extract the following information and output it as a JSON object with the specified keys.\n", + "\n", + "**JSON Output Format:**\n", + "\n", + "```json\n", + "{\n", + " \"granter_classification\": \"United States or European Union\",\n", + " \"invention_type\": \"med_tech/computer_vision/crypto/other\",\n", + " \"entities\": {\n", + " \"issuer\": \"string\",\n", + " \"language\": \"string\",\n", + " \"publication_date\": \"YYYY-MM-DD\",\n", + " \"class_international\": [\"string\", \"string\", ...],\n", + " \"class_us\": [\"string\", \"string\", ...],\n", + " \"application_number\": \"string\",\n", + " \"filing_date\": \"YYYY-MM-DD\",\n", + " \"priority_date_eu\": \"YYYY-MM-DD\",\n", + " \"representative_eu\": [\"string\",\"string\",...],\n", + " \"applicant\": [\"string\", \"string\", ...],\n", + " \"inventor\": [\"string\", \"string\", ...],\n", + " \"title\": \"string\",\n", + " \"patent_number\": \"string\"\n", + " },\n", + " \"image_detections\": [\n", + " {\n", + " \"label\": \"figure_1\",\n", + " \"bounding_box\": [x1, y1, x2, y2]\n", + " },\n", + " {\n", + " \"label\": \"figure_2\",\n", + " \"bounding_box\": [x1, y1, x2, y2]\n", + " }\n", + " ]\n", + "}\n", + "```\n", + "\n", + "**Detailed Instructions:**\n", + "\n", + "1. **Granter Classification:** Determine if the patent was granted by the \"United States\" or \"European Union\". Populate the `granter_classification` field accordingly.\n", + "\n", + "2. **Invention Type Classification:** Classify the type of invention described in the patent as one of the following and populate the `invention_type` field with the corresponding code:\n", + " * Medical Tech: `med_tech`\n", + " * Computer Vision: `computer_vision`\n", + " * Cryptography: `crypto`\n", + " * Other: `other`\n", + "\n", + "3. **Entity Extraction:**\n", + " * Extract the following entities and populate the corresponding fields within the `entities` object.\n", + " * If multiple entities exist for a field (e.g., multiple inventors, EU representatives, multiple US and international classes), represent them as a JSON array of strings.\n", + " * Use the `YYYY-MM-DD` format for dates.\n", + " * If a field is not present in the document, leave the value as null or an empty array as appropriate\n", + "\n", + "4. **Image Object Detection:**\n", + " * Detect the bounding boxes of all image figures in the patent document.\n", + " * Represent each bounding box as a JSON array with the format `[x1, y1, x2, y2]` where:\n", + " * `x1` and `y1` are the coordinates of the top-left corner of the bounding box.\n", + " * `x2` and `y2` are the coordinates of the bottom-right corner of the bounding box.\n", + " * Include a label (e.g., \"figure_1\", \"figure_2\", etc.) for each detected bounding box.\n", + " * Populate the `image_detections` array with each detected bounding box. If no figures are detected, the image_detections array should be empty.\n", + "\n", + "**Input:**\n", + "\n", + "Provide the complete patent document text as input to this prompt.\n", + "\n", + "**Output:**\n", + "\n", + "The output should be a single JSON object following the specified format, containing all the extracted information.\n", + "\n", + "**Example (Illustrative, Not Complete):**\n", + "\n", + "```json\n", + "{\n", + " \"granter_classification\": \"United States\",\n", + " \"invention_type\": \"med_tech\",\n", + " \"entities\": {\n", + " \"issuer\": \"United States Patent and Trademark Office\",\n", + " \"language\": \"en\",\n", + " \"publication_date\": \"2023-10-26\",\n", + " \"class_international\": [\"A61K 31/4709\", \"A61K 9/00\"],\n", + " \"class_us\": [\"514/250\"],\n", + " \"application_number\": \"17/744,843\",\n", + " \"filing_date\": \"2022-05-24\",\n", + " \"priority_date_eu\": null,\n", + " \"representative_eu\": [],\n", + " \"applicant\": [\"ABC Pharmacy Inc\"],\n", + " \"inventor\": [\"John Smith\", \"Jane Doe\"],\n", + " \"title\": \"Novel Formulation\",\n", + " \"patent_number\": \"US12345678\"\n", + " },\n", + " \"image_detections\": [\n", + " {\n", + " \"label\": \"figure_1\",\n", + " \"bounding_box\": [100, 50, 300, 250]\n", + " },\n", + " {\n", + " \"label\": \"figure_2\",\n", + " \"bounding_box\": [400, 100, 600, 300]\n", + " }\n", + " ]\n", + "}\n", + "```\n", + "\n", + "\"\"\"" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "01048a7ef243" + }, + "source": [ + "### Create BigQuery Dataset and Table for Batch Prediction\n", + "\n", + "NOTE: The location of the BigQuery dataset must be the same as the location for Vertex AI. (e.g. `us-central1`, not `us`)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "d-KVqc67ajwf" + }, + "outputs": [], + "source": [ + "DATASET_NAME = \"patents_data_batch\"\n", + "TABLE_NAME = \"patents_table\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "e6de4b4be476" + }, + "outputs": [], + "source": [ + "!bq --location={LOCATION} mk --dataset \"{PROJECT_ID}:{DATASET_NAME}\"" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "c403e052d071" + }, + "source": [ + "Load `GenerateContentRequest` JSON into BigQuery table.\n", + "\n", + "This request takes a Google Cloud Storage path to a PDF, processes it with the prompt above, and returns a structured dictionary using [Controlled Generation](https://cloud.google.com/vertex-ai/generative-ai/docs/multimodal/control-generated-output)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "bbba7e8861af" + }, + "outputs": [], + "source": [ + "import json\n", + "\n", + "\n", + "def create_request_json(row) -> str:\n", + " return json.dumps(\n", + " {\n", + " \"contents\": [\n", + " {\n", + " \"role\": \"user\",\n", + " \"parts\": [\n", + " {\"text\": PATENTS_PROMPT},\n", + " {\n", + " \"fileData\": {\n", + " \"fileUri\": row[\"gcs_path\"],\n", + " \"mimeType\": \"application/pdf\",\n", + " }\n", + " },\n", + " ],\n", + " }\n", + " ],\n", + " \"systemInstruction\": {\n", + " \"parts\": [{\"text\": \"You are an expert at analyzing patent documents.\"}]\n", + " },\n", + " \"generationConfig\": {\n", + " \"responseMimeType\": \"application/json\",\n", + " \"responseSchema\": {\n", + " \"type\": \"OBJECT\",\n", + " \"properties\": {\n", + " \"granter_classification\": {\n", + " \"type\": \"STRING\",\n", + " \"enum\": [\"United States\", \"European Union\"],\n", + " },\n", + " \"invention_type\": {\n", + " \"type\": \"STRING\",\n", + " \"enum\": [\"med_tech\", \"computer_vision\", \"crypto\", \"other\"],\n", + " },\n", + " \"entities\": {\n", + " \"type\": \"OBJECT\",\n", + " \"properties\": {\n", + " \"issuer\": {\"type\": \"STRING\"},\n", + " \"language\": {\"type\": \"STRING\"},\n", + " \"publication_date\": {\n", + " \"type\": \"STRING\",\n", + " \"format\": \"date\",\n", + " },\n", + " \"class_international\": {\n", + " \"type\": \"ARRAY\",\n", + " \"items\": {\"type\": \"STRING\"},\n", + " },\n", + " \"class_us\": {\n", + " \"type\": \"ARRAY\",\n", + " \"items\": {\"type\": \"STRING\"},\n", + " },\n", + " \"application_number\": {\"type\": \"STRING\"},\n", + " \"filing_date\": {\"type\": \"STRING\", \"format\": \"date\"},\n", + " \"priority_date_eu\": {\n", + " \"type\": \"STRING\",\n", + " \"format\": \"date\",\n", + " \"nullable\": True,\n", + " },\n", + " \"representative_eu\": {\n", + " \"type\": \"ARRAY\",\n", + " \"items\": {\"type\": \"STRING\"},\n", + " },\n", + " \"applicant\": {\n", + " \"type\": \"ARRAY\",\n", + " \"items\": {\"type\": \"STRING\"},\n", + " },\n", + " \"inventor\": {\n", + " \"type\": \"ARRAY\",\n", + " \"items\": {\"type\": \"STRING\"},\n", + " },\n", + " \"title\": {\"type\": \"STRING\"},\n", + " \"patent_number\": {\"type\": \"STRING\"},\n", + " },\n", + " \"required\": [\n", + " \"issuer\",\n", + " \"language\",\n", + " \"publication_date\",\n", + " \"class_international\",\n", + " \"class_us\",\n", + " \"application_number\",\n", + " \"filing_date\",\n", + " \"applicant\",\n", + " \"inventor\",\n", + " \"title\",\n", + " \"patent_number\",\n", + " ],\n", + " },\n", + " \"image_detections\": {\n", + " \"type\": \"ARRAY\",\n", + " \"items\": {\n", + " \"type\": \"OBJECT\",\n", + " \"properties\": {\n", + " \"label\": {\"type\": \"STRING\"},\n", + " \"bounding_box\": {\n", + " \"type\": \"ARRAY\",\n", + " \"items\": {\"type\": \"NUMBER\"},\n", + " \"minItems\": 4,\n", + " \"maxItems\": 4,\n", + " },\n", + " },\n", + " \"required\": [\"label\", \"bounding_box\"],\n", + " },\n", + " },\n", + " },\n", + " \"required\": [\n", + " \"granter_classification\",\n", + " \"invention_type\",\n", + " \"entities\",\n", + " \"image_detections\",\n", + " ],\n", + " },\n", + " },\n", + " }\n", + " )" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "29afb3b3fdcb" + }, + "outputs": [], + "source": [ + "df[\"request\"] = df.apply(create_request_json, axis=1)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "271aeda2bdaa" + }, + "outputs": [], + "source": [ + "pandas_gbq.to_gbq(df, f\"{DATASET_NAME}.{TABLE_NAME}\", project_id=PROJECT_ID)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "XKpdIpbw6Ycv" + }, + "source": [ + "### Batch Process Patent Documents with Gemini\n", + "\n", + "Batch Processing in Vertex AI will take in the BigQuery table with the requests and return the output in a new BigQuery table in the same dataset." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "c069acc57089" + }, + "outputs": [], + "source": [ + "batch_job = client.batches.create(\n", + " model=MODEL_ID,\n", + " src=f\"bq://{PROJECT_ID}.{DATASET_NAME}.{TABLE_NAME}\",\n", + ")\n", + "batch_job" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "e67c90f1117e" + }, + "source": [ + "Run the following block to get the latest status of the batch job." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "cf230deeb013" + }, + "outputs": [], + "source": [ + "batch_job = client.batches.get(name=batch_job.name)\n", + "batch_job" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "bf44c2b0bd92" + }, + "source": [ + "Run the following block to continuously poll the status of the batch job until it completes." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "defb5eb0b1c1" + }, + "outputs": [], + "source": [ + "import time\n", + "\n", + "# Refresh the job until complete\n", + "while batch_job.state == \"JOB_STATE_RUNNING\":\n", + " batch_job = client.batches.get(name=batch_job.name)\n", + " # print(batch_job)\n", + " time.sleep(5)\n", + "\n", + "# Check if the job succeeds\n", + "if batch_job.state == \"JOB_STATE_SUCCEEDED\":\n", + " print(\"Job succeeded!\")\n", + "else:\n", + " print(f\"Job failed: {batch_job.error}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "acd872565dc0" + }, + "source": [ + "Load the results from the destination BigQuery table." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "9ef9a49490eb" + }, + "outputs": [], + "source": [ + "results_df = pandas_gbq.read_gbq(\n", + " batch_job.dest.bigquery_uri.replace(\"bq://\", \"\"), project_id=PROJECT_ID\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "ed26cf628fc3" + }, + "source": [ + "Extract the fields from the controlled generation response." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "5ac2c0a3d98c" + }, + "outputs": [], + "source": [ + "import pandas as pd\n", + "\n", + "\n", + "def flatten_response(response) -> dict | None:\n", + " try:\n", + " parsed_json = json.loads(\n", + " response[\"candidates\"][0][\"content\"][\"parts\"][0][\"text\"]\n", + " )\n", + "\n", + " entities = parsed_json.get(\"entities\", {})\n", + " additional_fields = {\n", + " \"granter_classification\": parsed_json.get(\"granter_classification\"),\n", + " \"image_detections\": parsed_json.get(\"image_detections\"),\n", + " \"invention_type\": parsed_json.get(\"invention_type\"),\n", + " }\n", + "\n", + " return {**entities, **additional_fields}\n", + " except (KeyError, IndexError, json.JSONDecodeError) as e:\n", + " print(f\"Error processing response: {e}\")\n", + " return None\n", + "\n", + "\n", + "results_df = results_df.join(\n", + " pd.json_normalize(results_df[\"response\"].apply(flatten_response))\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "da7bda40a26e" + }, + "outputs": [], + "source": [ + "results_df" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "UkIB1pek6_Y7" + }, + "source": [ + "### Compare Results with Ground Truth\n", + "\n", + "We can pull the ground truth data from BigQuery into a DataFrame to compare the results. Note that the data is in a slightly different format, so a direct comparison is not straightforward." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "MzDxgh74mKVy" + }, + "outputs": [], + "source": [ + "query = \"\"\"\n", + "SELECT\n", + " COALESCE(t1.gcs_path, t2.gcs_path, t3.gcs_path) AS gcs_path,\n", + " t1.issuer,\n", + " t1.language,\n", + " t1.publication_date,\n", + " t1.class_international,\n", + " t1.class_us,\n", + " t1.application_number,\n", + " t1.filing_date,\n", + " t1.priority_date_eu,\n", + " t1.representative_line_1_eu,\n", + " t1.applicant_line_1,\n", + " t1.inventor_line_1,\n", + " t1.title_line_1,\n", + " t1.number,\n", + " t2.x_relative_min,\n", + " t2.y_relative_min,\n", + " t2.x_relative_max,\n", + " t2.y_relative_max,\n", + " t3.invention_type\n", + "FROM\n", + " `bigquery-public-data.labeled_patents.extracted_data` AS t1\n", + "FULL OUTER JOIN\n", + " `bigquery-public-data.labeled_patents.figures` AS t2\n", + "ON\n", + " t1.gcs_path = t2.gcs_path\n", + "FULL OUTER JOIN\n", + " `bigquery-public-data.labeled_patents.invention_types` AS t3\n", + "ON\n", + " COALESCE(t1.gcs_path, t2.gcs_path) = t3.gcs_path;\n", + "\"\"\"\n", + "\n", + "query_job = bq_client.query(query)\n", + "ground_truth_df = bq_client.query(query).result().to_dataframe()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "2a4e033321ad" + }, + "source": [ + "## Conclusion\n", + "\n", + "This notebook demonstrates the capabilities of using Gemini to extract structured information from complex documents, simplifying document understanding pipelines without needing to train custom models.\n", + "\n", + "**Key Improvements:**\n", + "\n", + "- **Comprehensive Extraction**: Unlike AutoML, which often limited field extraction to the first line (e.g., applicant, inventor, class US), Gemini accurately extracts full text and all listed values for these fields.\n", + "- **Simplified Workflow**: AutoML required four separate models and four requests per document to complete the tasks. Gemini consolidates this into a single request.\n", + "- **No Custom Model Training**: Gemini is a pre-trained model, eliminating the need for use case-specific training, saving both time and resources." + ] + } + ], + "metadata": { + "colab": { + "name": "patents_understanding.ipynb", + "toc_visible": true + }, + "kernelspec": { + "display_name": "Python 3", + "name": "python3" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} diff --git a/gemini/use-cases/document-processing/sheet_music.ipynb b/gemini/use-cases/document-processing/sheet_music.ipynb new file mode 100644 index 0000000..1992971 --- /dev/null +++ b/gemini/use-cases/document-processing/sheet_music.ipynb @@ -0,0 +1,517 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "ijGzTHJJUCPY" + }, + "outputs": [], + "source": [ + "# Copyright 2024 Google LLC\n", + "#\n", + "# Licensed under the Apache License, Version 2.0 (the \"License\");\n", + "# you may not use this file except in compliance with the License.\n", + "# You may obtain a copy of the License at\n", + "#\n", + "# https://www.apache.org/licenses/LICENSE-2.0\n", + "#\n", + "# Unless required by applicable law or agreed to in writing, software\n", + "# distributed under the License is distributed on an \"AS IS\" BASIS,\n", + "# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n", + "# See the License for the specific language governing permissions and\n", + "# limitations under the License." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "VEqbX8OhE8y9" + }, + "source": [ + "# Sheet Music Analysis with Gemini\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
    \n", + " \n", + " \"Google
    Run in Colab\n", + "
    \n", + "
    \n", + " \n", + " \"Google
    Run in Colab Enterprise\n", + "
    \n", + "
    \n", + " \n", + " \"GitHub
    View on GitHub\n", + "
    \n", + "
    \n", + " \n", + " \"Vertex
    Open in Vertex AI Workbench\n", + "
    \n", + "
    \n", + " \n", + " \"Google
    Open in Cloud Skills Boost\n", + "
    \n", + "
    \n", + "\n", + "
    \n", + "\n", + "Share to:\n", + "\n", + "\n", + " \"LinkedIn\n", + "\n", + "\n", + "\n", + " \"Bluesky\n", + "\n", + "\n", + "\n", + " \"X\n", + "\n", + "\n", + "\n", + " \"Reddit\n", + "\n", + "\n", + "\n", + " \"Facebook\n", + " \n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "5ad877ea09dd" + }, + "source": [ + "| Author |\n", + "| --- |\n", + "| [Holt Skinner](https://github.com/holtskinner) |" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "CkHPv2myT2cx" + }, + "source": [ + "## Overview\n", + "\n", + "[Sheet Music](https://en.wikipedia.org/wiki/Sheet_music) is the primary form of music notation used by composers and performers across the world. These pages contain information about the lyrics, pitches, rhythms, composer, text author, composition date, among others.\n", + "\n", + "This notebook illustrates using Gemini to extract this metadata from sheet music PDFs.\n", + "\n", + "These prompts and documents were demonstrated in the [Google Cloud Next 2024 session \"What's next with Gemini: Driving business impact with multimodal use cases\"](https://www.youtube.com/watch?v=DqH1R9Pk5RI).\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "r11Gu7qNgx1p" + }, + "source": [ + "## Getting Started\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "No17Cw5hgx12" + }, + "source": [ + "### Install Google Gen AI SDK for Python" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "tFy3H3aPgx12" + }, + "outputs": [], + "source": [ + "%pip install --upgrade -q google-genai pypdf" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "dmWOrTJ3gx13" + }, + "source": [ + "### Authenticate your notebook environment (Colab only)\n", + "\n", + "If you are running this notebook on Google Colab, run the following cell to authenticate your environment. This step is not required if you are using [Vertex AI Workbench](https://cloud.google.com/vertex-ai-workbench).\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "NyKGtVQjgx13" + }, + "outputs": [], + "source": [ + "import sys\n", + "\n", + "# Additional authentication is required for Google Colab\n", + "if \"google.colab\" in sys.modules:\n", + " # Authenticate user to Google Cloud\n", + " from google.colab import auth\n", + "\n", + " auth.authenticate_user()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "DF4l8DTdWgPY" + }, + "source": [ + "### Set Google Cloud project information and create client\n", + "\n", + "To get started using Vertex AI, you must have an existing Google Cloud project and [enable the Vertex AI API](https://console.cloud.google.com/flows/enableapi?apiid=aiplatform.googleapis.com).\n", + "\n", + "Learn more about [setting up a project and a development environment](https://cloud.google.com/vertex-ai/docs/start/cloud-environment)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "Nqwi-5ufWp_B" + }, + "outputs": [], + "source": [ + "# Use the environment variable if the user doesn't provide Project ID.\n", + "import os\n", + "\n", + "from google import genai\n", + "\n", + "# fmt: off\n", + "PROJECT_ID = \"[your-project-id]\" # @param {type: \"string\", placeholder: \"[your-project-id]\", isTemplate: true}\n", + "# fmt: on\n", + "if not PROJECT_ID or PROJECT_ID == \"[your-project-id]\":\n", + " PROJECT_ID = str(os.environ.get(\"GOOGLE_CLOUD_PROJECT\"))\n", + "\n", + "LOCATION = os.environ.get(\"GOOGLE_CLOUD_REGION\", \"global\")\n", + "\n", + "client = genai.Client(vertexai=True, project=PROJECT_ID, location=LOCATION)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "jXHfaVS66_01" + }, + "source": [ + "### Import libraries\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "lslYAvw37JGQ" + }, + "outputs": [], + "source": [ + "import json\n", + "\n", + "import pypdf\n", + "from IPython.display import Markdown, display\n", + "from google.genai.types import (\n", + " GenerateContentConfig,\n", + " GoogleSearch,\n", + " HarmBlockThreshold,\n", + " HarmCategory,\n", + " Part,\n", + " SafetySetting,\n", + " Tool,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "FTMywdzUORIA" + }, + "source": [ + "### Load the Gemini model\n", + "\n", + "Gemini is a multimodal model that supports multimodal prompts. You can include text, image(s), PDFs, audio, and video in your prompt requests." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "7b76801cc9e4" + }, + "outputs": [], + "source": [ + "MODEL_ID = \"gemini-3.5-flash\" # @param {type: \"string\"}\n", + "\n", + "config = GenerateContentConfig(\n", + " safety_settings=[\n", + " SafetySetting(\n", + " category=HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT,\n", + " threshold=HarmBlockThreshold.BLOCK_ONLY_HIGH,\n", + " )\n", + " ],\n", + " system_instruction=\"You are an expert in musicology and music history.\",\n", + " tools=[Tool(google_search=GoogleSearch())],\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "Wy75sLb-yjNn" + }, + "source": [ + "## Extract Structured Metadata from Sheet Music PDF\n", + "\n", + "For this example, we will be using the popular classical music book [24 Italian Songs and Arias of the 17th and 18th Centuries](https://imslp.org/wiki/24_Italian_Songs_and_Arias_of_the_17th_and_18th_Centuries_(Various)), and extracting metadata about each song in the book." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "0ed417af1e5c" + }, + "outputs": [], + "source": [ + "sheet_music_pdf_uri = \"gs://github-repo/use-cases/sheet-music/24ItalianSongs.pdf\"\n", + "\n", + "sheet_music_extraction_prompt = \"\"\"The following document is a book of sheet music. Your task is to output structured metadata about every piece of music in the document.\n", + "\n", + "Correct any mistakes that are in the document and fill in missing information when not present in the document.\n", + "\n", + "Include the following details:\n", + "\n", + "Title\n", + "Composer with lifetime\n", + "Tempo Marking\n", + "A description of the piece\n", + "\"\"\"\n", + "\n", + "# Send to Gemini\n", + "response = client.models.generate_content(\n", + " model=MODEL_ID,\n", + " contents=[\n", + " sheet_music_extraction_prompt,\n", + " # Load file directly from Google Cloud Storage\n", + " Part.from_uri(\n", + " file_uri=sheet_music_pdf_uri,\n", + " mime_type=\"application/pdf\",\n", + " ),\n", + " ],\n", + " config=config,\n", + ")\n", + "\n", + "# Display results\n", + "display(Markdown(response.text))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "29b40b5e0cb0" + }, + "source": [ + "You can see that Gemini extracted all of the relevant fields from the document." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "db3765c7645d" + }, + "source": [ + "### Song Identification with Audio\n", + "\n", + "Now, let's try something more challenging, identifying a song being performed based on the sheet music. We have an audio clip of Holt Skinner performing one of the songs in the book, and we will ask Gemini to identify it based on the sheet music PDF." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "61ea3f2f1c4a" + }, + "outputs": [], + "source": [ + "song_identification_prompt = \"\"\"Based on the sheet music PDF, what song is in the audio clip? Explain how you made the decision.\"\"\"\n", + "\n", + "# Load PDF file\n", + "pdf_part = Part.from_uri(\n", + " file_uri=sheet_music_pdf_uri,\n", + " mime_type=\"application/pdf\",\n", + ")\n", + "\n", + "audio_part = Part.from_uri(\n", + " file_uri=\"gs://github-repo/use-cases/sheet-music/24ItalianClip.mp3\",\n", + " mime_type=\"audio/mpeg\",\n", + ")\n", + "\n", + "# Send to Gemini\n", + "response = client.models.generate_content(\n", + " model=MODEL_ID,\n", + " contents=[pdf_part, audio_part, song_identification_prompt],\n", + " config=config,\n", + ")\n", + "\n", + "# Display results\n", + "display(Markdown(response.text))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "9730e8a5628b" + }, + "source": [ + "### Edit PDF Metadata\n", + "\n", + "Next, we'll use the output from Gemini to edit the metadata of a PDF containing one song, which can make it easier to organize this file in sheet music applications.\n", + "\n", + "We'll adjust the prompt slightly and set the [`response_mime_type`](https://cloud.google.com/vertex-ai/generative-ai/docs/model-reference/gemini#:~:text=in%20the%20list.-,responseMimeType,-(Preview)) to get the response in JSON format." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "97e2a06cc762" + }, + "outputs": [], + "source": [ + "sheet_music_pdf_uri = \"gs://github-repo/use-cases/sheet-music/SebbenCrudele.pdf\"\n", + "output_file_name = \"SebbenCrudele.pdf\"\n", + "\n", + "sheet_music_extraction_prompt = \"\"\"The following document is a piece of sheet music. Your task is to output structured metadata about the piece of music in the document. Correct any mistakes that are in the document and fill in missing information when not present in the document.\n", + "\n", + "Output the data in the following JSON format:\n", + "\n", + "{\n", + " \"/Title\": \"Title of the piece\",\n", + " \"/Author\": \"Composer(s) of the piece\",\n", + " \"/Subject\": \"Music Genre(s) in a comma separated list\",\n", + "}\n", + "\n", + "\"\"\"\n", + "\n", + "# Load file directly from Google Cloud Storage\n", + "file_part = Part.from_uri(\n", + " file_uri=sheet_music_pdf_uri,\n", + " mime_type=\"application/pdf\",\n", + ")\n", + "\n", + "config.response_mime_type = \"application/json\"\n", + "\n", + "# Send to Gemini\n", + "response = client.models.generate_content(\n", + " model=MODEL_ID,\n", + " contents=[sheet_music_extraction_prompt, file_part],\n", + " config=config,\n", + ")\n", + "\n", + "# Display results\n", + "display(Markdown(response.text))\n", + "\n", + "new_metadata = json.loads(response.text)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "8e997cb6affc" + }, + "source": [ + "Next, we'll download the PDF from the GCS Bucket and edit the metadata using the [`PyPDF2`](https://pypdf2.readthedocs.io/en/3.x/) library." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "879f827c537a" + }, + "outputs": [], + "source": [ + "! gcloud storage cp {sheet_music_pdf_uri} {output_file_name}" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "e81759999d78" + }, + "outputs": [], + "source": [ + "def edit_pdf_metadata(file_path: str, new_metadata: dict) -> None:\n", + " \"\"\"Edits metadata of a PDF file.\n", + "\n", + " Args:\n", + " file_path (str): Path to the PDF file.\n", + " new_metadata (dict): Dictionary containing the metadata fields (e.g., {'/Author': 'John Doe', '/Title': 'My Report'})\n", + " \"\"\"\n", + " # 1. Read the existing PDF\n", + " reader = pypdf.PdfReader(file_path)\n", + " writer = pypdf.PdfWriter()\n", + "\n", + " # 2. Copy all pages and structure (bookmarks, etc.) from reader to writer\n", + " writer.append(reader)\n", + "\n", + " # 3. Add/Update metadata\n", + " # Note: Keys usually start with a slash, e.g., \"/Title\"\n", + " writer.add_metadata(new_metadata)\n", + "\n", + " # 4. Save the result\n", + " # We open the file in write-binary mode to save the changes\n", + " with open(file_path, \"wb\") as f:\n", + " writer.write(f)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "jtbQo-df1TTu" + }, + "outputs": [], + "source": [ + "# View current metadata\n", + "reader = pypdf.PdfReader(output_file_name)\n", + "print(reader.metadata)\n", + "\n", + "edit_pdf_metadata(output_file_name, new_metadata)\n", + "\n", + "# View updated metadata\n", + "reader = pypdf.PdfReader(output_file_name)\n", + "print(reader.metadata)" + ] + } + ], + "metadata": { + "colab": { + "name": "sheet_music.ipynb", + "toc_visible": true + }, + "kernelspec": { + "display_name": "Python 3", + "name": "python3" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} diff --git a/gemini/use-cases/document-processing/summarization_large_documents_langchain.ipynb b/gemini/use-cases/document-processing/summarization_large_documents_langchain.ipynb new file mode 100644 index 0000000..3f62d10 --- /dev/null +++ b/gemini/use-cases/document-processing/summarization_large_documents_langchain.ipynb @@ -0,0 +1,929 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "uxCkB_DXTHzf" + }, + "outputs": [], + "source": [ + "# Copyright 2023 Google LLC\n", + "#\n", + "# Licensed under the Apache License, Version 2.0 (the \"License\");\n", + "# you may not use this file except in compliance with the License.\n", + "# You may obtain a copy of the License at\n", + "#\n", + "# https://www.apache.org/licenses/LICENSE-2.0\n", + "#\n", + "# Unless required by applicable law or agreed to in writing, software\n", + "# distributed under the License is distributed on an \"AS IS\" BASIS,\n", + "# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n", + "# See the License for the specific language governing permissions and\n", + "# limitations under the License." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "Hny4I-ODTIS6" + }, + "source": [ + "# Text Summarization of Large Documents using LangChain 🩜🔗\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + "
    \n", + " \n", + " \"Google
    Open in Colab\n", + "
    \n", + "
    \n", + " \n", + " \"Google
    Open in Colab Enterprise\n", + "
    \n", + "
    \n", + " \n", + " \"Vertex
    Open in Vertex AI Workbench\n", + "
    \n", + "
    \n", + " \n", + " \"GitHub
    View on GitHub\n", + "
    \n", + "
    \n", + "\n", + "
    \n", + "\n", + "Share to:\n", + "\n", + "\n", + " \"LinkedIn\n", + "\n", + "\n", + "\n", + " \"Bluesky\n", + "\n", + "\n", + "\n", + " \"X\n", + "\n", + "\n", + "\n", + " \"Reddit\n", + "\n", + "\n", + "\n", + " \"Facebook\n", + "" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "8e71fd522e56" + }, + "source": [ + "| Authors |\n", + "| --- |\n", + "| [Polong Lin](https://github.com/polong-lin) |\n", + "| [Holt Skinner](https://github.com/holtskinner) |" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "-nLS57E2TO5y" + }, + "source": [ + "## Overview\n", + "\n", + "Text summarization is an NLP task that creates a concise and informative summary of a longer text. LLMs can be used to create summaries of news articles, research papers, technical documents, and other types of text.\n", + "\n", + "Summarizing large documents can be challenging. To create summaries, you need to apply summarization strategies to your indexed documents. You have already seen some of these strategies in the previous notebooks. If you haven't completed it, it is recommended to do so to have a basic understanding of how to summarize large documents.\n", + "\n", + "In this notebook, you will use LangChain, a framework for developing LLM applications, to apply some summarization strategies. The notebook covers several examples of how to summarize large documents." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "iXsvgIuwTPZw" + }, + "source": [ + "### Objective\n", + "\n", + "In this tutorial, you learn how to use LangChain with Gemini to summarize large documents by working through the following examples:\n", + "\n", + "- Stuffing method\n", + "- MapReduce method\n", + "- Refine method" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "skXAu__iqks_" + }, + "source": [ + "### Costs\n", + "\n", + "This tutorial uses billable components of Google Cloud:\n", + "\n", + "- Vertex AI\n", + "\n", + "Learn about [Vertex AI pricing](https://cloud.google.com/vertex-ai/pricing), and use the [Pricing Calculator](https://cloud.google.com/products/calculator/) to generate a cost estimate based on your projected usage." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "mvKl-BtQTRiQ" + }, + "source": [ + "## Getting Started" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "PwFMpIMrTV_4" + }, + "source": [ + "### Install Vertex AI SDK & Other dependencies" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "8aP6JVlZkS-m" + }, + "outputs": [], + "source": [ + "!sudo apt -y -qq install tesseract-ocr\n", + "!sudo apt -y -qq install libtesseract-dev\n", + "!sudo apt-get -y -qq install poppler-utils #required by PyPDF2 for page count and other pdf utilities\n", + "!sudo apt-get -y -qq install python-dev libxml2-dev libxslt1-dev antiword unrtf poppler-utils pstotext tesseract-ocr flac ffmpeg lame libmad0 libsox-fmt-mp3 sox libjpeg-dev swig" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "oDmNq5__Trl4" + }, + "outputs": [], + "source": [ + "%pip install --upgrade --quiet pytesseract pypdf PyPDF2 langchain langchain-core langchain-google-vertexai google-cloud-aiplatform langchain-community" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "jWwtjLV5TY6H" + }, + "source": [ + "**Colab only**: Run the following cell to restart the kernel. For Vertex AI Workbench you can restart the terminal using the button on top." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "XRvKdaPDTznN" + }, + "outputs": [], + "source": [ + "# Automatically restart kernel after installs so that your environment can access the new packages\n", + "import IPython\n", + "\n", + "app = IPython.Application.instance()\n", + "app.kernel.do_shutdown(True)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "opUxT_k5TdgP" + }, + "source": [ + "### Authenticating your notebook environment\n", + "\n", + "- If you are using **Colab** to run this notebook, run the cell below and continue.\n", + "- If you are using **Vertex AI Workbench**, check out the setup instructions [here](https://github.com/GoogleCloudPlatform/generative-ai/tree/main/setup-env)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "vbNgv4q1T2Mi" + }, + "outputs": [], + "source": [ + "import sys\n", + "\n", + "if \"google.colab\" in sys.modules:\n", + " from google.colab import auth\n", + "\n", + " auth.authenticate_user()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "n5fXfvzhTkYN" + }, + "source": [ + "### Import libraries" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "GjSsu6cmUdEx" + }, + "outputs": [], + "source": [ + "# Use the environment variable if the user doesn't provide Project ID.\n", + "import os\n", + "\n", + "# fmt: off\n", + "PROJECT_ID = \"[your-project-id]\" # @param {type: \"string\", placeholder: \"[your-project-id]\", isTemplate: true}\n", + "# fmt: on\n", + "if not PROJECT_ID or PROJECT_ID == \"[your-project-id]\":\n", + " PROJECT_ID = str(os.environ.get(\"GOOGLE_CLOUD_PROJECT\"))\n", + "\n", + "REGION = os.environ.get(\"GOOGLE_CLOUD_REGION\", \"global\")\n", + "\n", + "import vertexai\n", + "\n", + "vertexai.init(project=PROJECT_ID, location=REGION)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "cRkcfnQMT9vD" + }, + "outputs": [], + "source": [ + "import urllib\n", + "import warnings\n", + "from pathlib import Path as p\n", + "\n", + "import pandas as pd\n", + "from langchain import PromptTemplate\n", + "from langchain.chains.summarize import load_summarize_chain\n", + "from langchain.document_loaders import PyPDFLoader\n", + "from langchain_google_vertexai import VertexAI\n", + "\n", + "warnings.filterwarnings(\"ignore\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "DAGaTjPVTmhP" + }, + "source": [ + "### Import models\n", + "\n", + "You load the pre-trained text generation model Gemini 3 Flash." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "ITUmZiNZcMUW" + }, + "outputs": [], + "source": [ + "vertex_llm_text = VertexAI(model_name=\"gemini-3.5-flash\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "cKG-ZTJ_02wq" + }, + "source": [ + "## Summarization with Large Documents" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "HZkLDRTjTcfm" + }, + "source": [ + "### Preparing data files\n", + "\n", + "To begin, you will need to download a few files that are required for the summarizing tasks below." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "7H0zINHpTaSu" + }, + "outputs": [], + "source": [ + "data_folder = p.cwd() / \"data\"\n", + "p(data_folder).mkdir(parents=True, exist_ok=True)\n", + "\n", + "pdf_url = \"https://services.google.com/fh/files/misc/practitioners_guide_to_mlops_whitepaper.pdf\"\n", + "pdf_file = str(p(data_folder, pdf_url.rsplit(\"/\", maxsplit=1)[-1]))\n", + "\n", + "urllib.request.urlretrieve(pdf_url, pdf_file)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "JELITHdBhnf0" + }, + "source": [ + "### Extract text from the PDF\n", + "\n", + "You use an `PdfReader` to extract the text from our scanned documents." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "x3INtovxreI_" + }, + "outputs": [], + "source": [ + "pdf_loader = PyPDFLoader(pdf_file)\n", + "pages = pdf_loader.load_and_split()\n", + "print(pages[3].page_content)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "ZDVwBFSjZ7ws" + }, + "source": [ + "## Method 1: Stuffing\n", + "\n", + "Stuffing is the simplest method to pass data to a language model. It \"stuffs\" text into the prompt as context in a way that all of the relevant information can be processed by the model to get what you want.\n", + "\n", + "In LangChain, you can use `StuffDocumentsChain` as part of the `load_summarize_chain` method. What you need to do is setting `stuff` as `chain_type` of your chain." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "uhEi-XqKnv2v" + }, + "source": [ + "### Prompt design with `Stuffing` chain" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "B-ljajUen1YO" + }, + "outputs": [], + "source": [ + "prompt_template = \"\"\"Write a concise summary of the following text delimited by triple backquotes.\n", + " Return your response in bullet points which covers the key points of the text.\n", + " ```{text}```\n", + " BULLET POINT SUMMARY:\n", + " \"\"\"\n", + "\n", + "prompt = PromptTemplate(template=prompt_template, input_variables=[\"text\"])" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "N5aVrDWkJs3Y" + }, + "source": [ + "### Retrying\n", + "Initiate a chain using `stuff` method and process three pages document." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "N_hoizIgObe9" + }, + "outputs": [], + "source": [ + "stuff_chain = load_summarize_chain(vertex_llm_text, chain_type=\"stuff\", prompt=prompt)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "Q1_zwxwgTnlV" + }, + "outputs": [], + "source": [ + "three_pages = pages[:3]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "0jEUfOn7UFI2" + }, + "outputs": [], + "source": [ + "three_pages" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "QnXUwWxkrLu4" + }, + "outputs": [], + "source": [ + "try:\n", + " print(stuff_chain.run(three_pages))\n", + "except Exception as e:\n", + " print(\n", + " \"The code failed since it won't be able to run inference on such a huge context and throws this exception: \",\n", + " e,\n", + " )" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "xKb_fBEedZqu" + }, + "source": [ + "As you can see, with the `stuff` method, you can summarize the entire document content with a single API call passing all data at once.\n", + "\n", + "Depending on the context length of LLM, the `stuff` method would not work as it result in a prompt larger than the context length." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "hqZrKM32h-o2" + }, + "source": [ + "### Considerations\n", + "\n", + "The `stuffing` method is a way to summarize text by feeding the entire document to a large language model (LLM) in a single call. This method has both pros and cons.\n", + "\n", + "The stuffing method only requires a single call to the LLM, which can be faster than other methods that require multiple calls. When summarizing text, the LLM has access to all the data at once, which can result in a better summary.\n", + "\n", + "But, LLMs have a context length, which is the maximum number of tokens that can be processed in a single call. If the document is longer than the context length, the stuffing method will not work. Also the stuffing method is not suitable for summarizing large documents, as it can be slow and may not produce a good summary.\n", + "\n", + "Let's explore other approaches to help deal with having longer text than context length limit of LLMs." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "RM3V1JARZ9-k" + }, + "source": [ + "## Method 2: MapReduce\n", + "\n", + "The `MapReduce` method implements a multi-stage summarization. It is a technique for summarizing large pieces of text by first summarizing smaller chunks of text and then combining those summaries into a single summary.\n", + "\n", + "In LangChain, you can use `MapReduceDocumentsChain` as part of the `load_summarize_chain` method. What you need to do is setting `map_reduce` as `chain_type` of your chain." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "lagLXEamlPY2" + }, + "source": [ + "### Prompt design with `MapReduce` chain\n", + "\n", + "In our example, you have a 32-page document that you need to summarize.\n", + "\n", + "With LangChain, the `map_reduce` chain breaks the document down into 1024 token chunks max. Then it runs the initial prompt you define on each chunk to generate a summary of that chunk. In the example below, you use the following first stage or map prompt.\n", + "\n", + "```Write a concise summary of the following text delimited by triple backquotes. Return your response in bullet points which covers the key points of the text.\n", + "'''{text}'''. BULLET POINT SUMMARY:```\n", + "\n", + "Once summaries for all of the chunks are generated, it runs a different prompt to combine those summaries into a single summary. In the example below, you use the following second stage or combine prompt.\n", + "\n", + "```Write a summary of the entire document that includes the main points from all of the individual summaries.```" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "R6oHEtdSmsTn" + }, + "outputs": [], + "source": [ + "map_prompt_template = \"\"\"\n", + " Write a summary of this chunk of text that includes the main points and any important details.\n", + " {text}\n", + " \"\"\"\n", + "\n", + "map_prompt = PromptTemplate(template=map_prompt_template, input_variables=[\"text\"])\n", + "\n", + "combine_prompt_template = \"\"\"\n", + " Write a concise summary of the following text delimited by triple backquotes.\n", + " Return your response in bullet points which covers the key points of the text.\n", + " ```{text}```\n", + " BULLET POINT SUMMARY:\n", + " \"\"\"\n", + "\n", + "combine_prompt = PromptTemplate(\n", + " template=combine_prompt_template, input_variables=[\"text\"]\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "JXoz0uLDMoWD" + }, + "source": [ + "### Generate summaries using MapReduce method\n", + "\n", + "After defining prompts, you initialize the associated `map_reduce_chain`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "VRGJcBZeVdEa" + }, + "outputs": [], + "source": [ + "map_reduce_chain = load_summarize_chain(\n", + " vertex_llm_text,\n", + " chain_type=\"map_reduce\",\n", + " map_prompt=map_prompt,\n", + " combine_prompt=combine_prompt,\n", + " return_intermediate_steps=True,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "-6fekDDr0hrJ" + }, + "source": [ + "Then, you generate summaries using the chain. Notice that LangChain use a tokenizer (from transformer library) with 1024 token limit by default." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "uSC6w2TBV35q" + }, + "outputs": [], + "source": [ + "map_reduce_outputs = map_reduce_chain({\"input_documents\": pages})" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "meH2ELuz2H46" + }, + "source": [ + "After summaries are generated, you can validate them by organize input documents and associated output in a Pandas DataFrame." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "r6FRSR7xRLew" + }, + "outputs": [], + "source": [ + "final_mp_data = []\n", + "for doc, out in zip(\n", + " map_reduce_outputs[\"input_documents\"],\n", + " map_reduce_outputs[\"intermediate_steps\"],\n", + " strict=False,\n", + "):\n", + " output = {}\n", + " output[\"file_name\"] = p(doc.metadata[\"source\"]).stem\n", + " output[\"file_type\"] = p(doc.metadata[\"source\"]).suffix\n", + " output[\"page_number\"] = doc.metadata[\"page\"]\n", + " output[\"chunks\"] = doc.page_content\n", + " output[\"concise_summary\"] = out\n", + " final_mp_data.append(output)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "dA9cnh8YaNbF" + }, + "outputs": [], + "source": [ + "pdf_mp_summary = pd.DataFrame.from_dict(final_mp_data)\n", + "pdf_mp_summary = pdf_mp_summary.sort_values(\n", + " by=[\"file_name\", \"page_number\"]\n", + ") # sorting the dataframe by filename and page_number\n", + "pdf_mp_summary.reset_index(inplace=True, drop=True)\n", + "pdf_mp_summary.head()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "yA0eM1K3cvH2" + }, + "outputs": [], + "source": [ + "index = 3\n", + "print(\"[Context]\")\n", + "print(pdf_mp_summary[\"chunks\"].iloc[index])\n", + "print(\"\\n\\n [Simple Summary]\")\n", + "print(pdf_mp_summary[\"concise_summary\"].iloc[index])\n", + "print(\"\\n\\n [Page number]\")\n", + "print(pdf_mp_summary[\"page_number\"].iloc[index])\n", + "print(\"\\n\\n [Source: file_name]\")\n", + "print(pdf_mp_summary[\"file_name\"].iloc[index])" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "ROrE1-HKpg7y" + }, + "source": [ + "### Considerations\n", + "\n", + "With `MapReduce` method, the model is able to summarize a large paper by overcoming the context limit of `Stuffing` method with parallel processing.\n", + "\n", + "However, the `MapReduce` requires multiple calls to the model and potentially losing context between pages.\n", + "\n", + "To deal this challenge, you can try another method to summarize multiple pages at a time." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "lxdB-5PqgCf-" + }, + "source": [ + "## Method 3: Refine\n", + "\n", + "The Refine method is an alternative method to deal with large document summarization. It works by first running an initial prompt on a small chunk of data, generating some output. Then, for each subsequent document, the output from the previous document is passed in along with the new document, and the LLM is asked to refine the output based on the new document.\n", + "\n", + "In LangChain, you can use `RefineDocumentsChain` as part of the `load_summarize_chain` method. What you need to do is setting `refine` as `chain_type` of your chain." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "pjj2UZilDF4Q" + }, + "source": [ + "### Prompt design with `Refine` chain\n", + "\n", + "With LangChain, the `refine` chain requires two prompts.\n", + "\n", + "The question prompt to generate the output for subsequent task. The refine prompt to refine the output based on the generated content.\n", + "\n", + "In this example, the question prompt is:\n", + "\n", + "```\n", + "Please provide a summary of the following text.\n", + "TEXT: {text}\n", + "SUMMARY:\n", + "```\n", + "\n", + "and the refine prompt is:\n", + "\n", + "```\n", + "Write a concise summary of the following text delimited by triple backquotes.\n", + "Return your response in bullet points which covers the key points of the text.\n", + "```{text}```\n", + "BULLET POINT SUMMARY:\n", + "```\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "XiZX45Z5VTwS" + }, + "outputs": [], + "source": [ + "question_prompt_template = \"\"\"\n", + " Please provide a summary of the following text.\n", + " TEXT: {text}\n", + " SUMMARY:\n", + " \"\"\"\n", + "\n", + "question_prompt = PromptTemplate(\n", + " template=question_prompt_template, input_variables=[\"text\"]\n", + ")\n", + "\n", + "refine_prompt_template = \"\"\"\n", + " Write a concise summary of the following text delimited by triple backquotes.\n", + " Return your response in bullet points which covers the key points of the text.\n", + " ```{text}```\n", + " BULLET POINT SUMMARY:\n", + " \"\"\"\n", + "\n", + "refine_prompt = PromptTemplate(\n", + " template=refine_prompt_template, input_variables=[\"text\"]\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "-USlaSPbM0rs" + }, + "source": [ + "### Generate summaries using Refine method\n", + "\n", + "After you define prompts, you initiate a summarization chain using `refine` chain type." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "_-Sv3HO1U3hi" + }, + "outputs": [], + "source": [ + "refine_chain = load_summarize_chain(\n", + " vertex_llm_text,\n", + " chain_type=\"refine\",\n", + " question_prompt=question_prompt,\n", + " refine_prompt=refine_prompt,\n", + " return_intermediate_steps=True,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "f9EZCDK-MQJH" + }, + "source": [ + "Then, you use the summarization chain to summarize document using Refine method." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "KHwwab7vXNa1" + }, + "outputs": [], + "source": [ + "refine_outputs = refine_chain({\"input_documents\": pages})" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "eUqpki5EMYEr" + }, + "source": [ + "Below you can see the resulting summaries." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "7j5cUGStZ5WF" + }, + "outputs": [], + "source": [ + "final_refine_data = []\n", + "for doc, out in zip(\n", + " refine_outputs[\"input_documents\"],\n", + " refine_outputs[\"intermediate_steps\"],\n", + " strict=False,\n", + "):\n", + " output = {}\n", + " output[\"file_name\"] = p(doc.metadata[\"source\"]).stem\n", + " output[\"file_type\"] = p(doc.metadata[\"source\"]).suffix\n", + " output[\"page_number\"] = doc.metadata[\"page\"]\n", + " output[\"chunks\"] = doc.page_content\n", + " output[\"concise_summary\"] = out\n", + " final_refine_data.append(output)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "N_7Mm9cEmGOV" + }, + "outputs": [], + "source": [ + "pdf_refine_summary = pd.DataFrame.from_dict(final_refine_data)\n", + "pdf_refine_summary = pdf_mp_summary.sort_values(\n", + " by=[\"file_name\", \"page_number\"]\n", + ") # sorting the dataframe by filename and page_number\n", + "pdf_refine_summary.reset_index(inplace=True, drop=True)\n", + "pdf_refine_summary.head()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "jvLVCs8Gbwbw" + }, + "outputs": [], + "source": [ + "index = 3\n", + "print(\"[Context]\")\n", + "print(pdf_refine_summary[\"chunks\"].iloc[index])\n", + "print(\"\\n\\n [Simple Summary]\")\n", + "print(pdf_refine_summary[\"concise_summary\"].iloc[index])\n", + "print(\"\\n\\n [Page number]\")\n", + "print(pdf_refine_summary[\"page_number\"].iloc[index])\n", + "print(\"\\n\\n [Source: file_name]\")\n", + "print(pdf_refine_summary[\"file_name\"].iloc[index])" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "7dwgbRTrM5Cb" + }, + "source": [ + "### Considerations" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "1H0Y5pPcXbgm" + }, + "source": [ + "In short, the Refine method for text summarization with LLMs can pull in more relevant context and may be less lossy than Map Reduce. However, it requires many more calls to the LLM than Stuffing, and these calls are not independent, meaning they cannot be parallelized. Additionally, there is some potential dependency on the ordering of the documents. Latest documents they might become more relevant as this method suffers from recency bias." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "HAaWXncPMhv4" + }, + "source": [ + "## Conclusion\n", + "\n", + "\n", + "In this notebook you learn about different techniques to summarize long documents with LangChain and PaLM API. What you have seen in this notebook are only some of the possibilities you have. For example, there is another method called the Map-Rerank method which involves running an initial prompt on each chunk of data, which not only tries to complete a task but also gives a score for how certain it is in its answer. The responses are then ranked according to this score, and the highest score is returned.\n", + "\n", + "With that being said, it is important to highlight that depending on your needs you may consider to use pure Foundational model with a custom framework to build generative ai application.\n", + "\n", + "Here are some of the benefits of using a foundational model with a custom framework:\n", + "\n", + " - More flexibility to implement your application with different LLMs, prompting templates, document handling strategies and more.\n", + "\n", + " - More control to customize your generative applications based on your scenario.\n", + "\n", + " - Better performance to improve latency and scalability of your application.\n" + ] + } + ], + "metadata": { + "colab": { + "name": "summarization_large_documents_langchain.ipynb", + "toc_visible": true + }, + "kernelspec": { + "display_name": "Python 3", + "name": "python3" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} diff --git a/gemini/use-cases/document-processing/tax_automation.ipynb b/gemini/use-cases/document-processing/tax_automation.ipynb new file mode 100644 index 0000000..20034ef --- /dev/null +++ b/gemini/use-cases/document-processing/tax_automation.ipynb @@ -0,0 +1,679 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "ur8xi4C7S06n" + }, + "outputs": [], + "source": [ + "# Copyright 2025 Google LLC\n", + "#\n", + "# Licensed under the Apache License, Version 2.0 (the \"License\");\n", + "# you may not use this file except in compliance with the License.\n", + "# You may obtain a copy of the License at\n", + "#\n", + "# https://www.apache.org/licenses/LICENSE-2.0\n", + "#\n", + "# Unless required by applicable law or agreed to in writing, software\n", + "# distributed under the License is distributed on an \"AS IS\" BASIS,\n", + "# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n", + "# See the License for the specific language governing permissions and\n", + "# limitations under the License." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "JAPoU8Sm5E6e" + }, + "source": [ + "# Automating Income Taxes with Gemini\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
    \n", + " \n", + " \"Google
    Open in Colab\n", + "
    \n", + "
    \n", + " \n", + " \"Google
    Open in Colab Enterprise\n", + "
    \n", + "
    \n", + " \n", + " \"Vertex
    Open in Vertex AI Workbench\n", + "
    \n", + "
    \n", + " \n", + " \"BigQuery
    Open in BigQuery Studio\n", + "
    \n", + "
    \n", + " \n", + " \"GitHub
    View on GitHub\n", + "
    \n", + "
    \n", + "\n", + "
    \n", + "\n", + "Share to:\n", + "\n", + "\n", + " \"LinkedIn\n", + "\n", + "\n", + "\n", + " \"Bluesky\n", + "\n", + "\n", + "\n", + " \"X\n", + "\n", + "\n", + "\n", + " \"Reddit\n", + "\n", + "\n", + "\n", + " \"Facebook\n", + "" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "84f0f73a0f76" + }, + "source": [ + "| Author |\n", + "| --- |\n", + "| [Holt Skinner](https://github.com/holtskinner) |" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "tvgnzT1CKxrO" + }, + "source": [ + "## Overview\n", + "\n", + "Back in 2022, I wrote a [Google Cloud Blog post](https://cloud.google.com/blog/topics/developers-practitioners/automating-income-taxes-document-ai) about automating income tax preparation using [Document AI](https://cloud.google.com/document-ai/docs/overview).\n", + "\n", + "This demo used the [Lending processors](https://cloud.google.com/blog/products/ai-machine-learning/lending-docai-fast-tracks-the-home-loan-process) to extract data from W-2 and 1099 PDFs and calculate the total tax owed.\n", + "\n", + "In the world of Generative AI models like [Gemini](https://blog.google/technology/ai/google-gemini-ai/), it's possible to create the same document processing pipeline in a more efficient manner.\n", + "\n", + "In this notebook, we'll create a document understanding pipeline on some sample tax documents to:\n", + "\n", + "- Classify the type of document (W-2, 1099-DIV, 1099-INT, 1099-MISC, 1099-NEC)\n", + "- Extract key fields based on the document type.\n", + "\n", + "These are the sample documents we will use:\n", + "\n", + "- [2020 Form 1099-DIV](https://storage.googleapis.com/cloud-samples-data/documentai/LendingDocAI/1099-DIV%20Parser/2020%20Form%201099-DIV%20-%20Anastasia%20Hodges.pdf)\n", + "- [2020 Form 1099-INT](https://storage.googleapis.com/cloud-samples-data/documentai/LendingDocAI/1099-INT%20Parser/2020%20Form%201099-INT%20-%20Anastasia%20Hodges.pdf)\n", + "- [2020 Form W-2](https://storage.googleapis.com/cloud-samples-data/documentai/LendingDocAI/W2Parser/2020/2020%20Form%20W-2%20-%20Anastasia%20Hodges.pdf)\n", + "\n", + "> Disclaimer: This is **NOT** financial advice, for educational purposes only!" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "61RBz8LLbxCR" + }, + "source": [ + "## Get started" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "No17Cw5hgx12" + }, + "source": [ + "### Install Google Gen AI SDK for Python\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "tFy3H3aPgx12" + }, + "outputs": [], + "source": [ + "%pip install --upgrade --quiet google-genai" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "dmWOrTJ3gx13" + }, + "source": [ + "### Authenticate your notebook environment (Colab only)\n", + "\n", + "If you're running this notebook on Google Colab, run the cell below to authenticate your environment." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "NyKGtVQjgx13" + }, + "outputs": [], + "source": [ + "import sys\n", + "\n", + "if \"google.colab\" in sys.modules:\n", + " from google.colab import auth\n", + "\n", + " auth.authenticate_user()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "DF4l8DTdWgPY" + }, + "source": [ + "### Set Google Cloud project information and create client\n", + "\n", + "To get started using Vertex AI, you must have an existing Google Cloud project and [enable the Vertex AI API](https://console.cloud.google.com/flows/enableapi?apiid=aiplatform.googleapis.com).\n", + "\n", + "Learn more about [setting up a project and a development environment](https://cloud.google.com/vertex-ai/docs/start/cloud-environment)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "Nqwi-5ufWp_B" + }, + "outputs": [], + "source": [ + "import os\n", + "\n", + "from google import genai\n", + "\n", + "PROJECT_ID = \"[your-project-id]\" # @param {type: \"string\"}\n", + "if not PROJECT_ID or PROJECT_ID == \"[your-project-id]\":\n", + " PROJECT_ID = str(os.environ.get(\"GOOGLE_CLOUD_PROJECT\"))\n", + "\n", + "LOCATION = os.environ.get(\"GOOGLE_CLOUD_REGION\", \"us-central1\")\n", + "\n", + "client = genai.Client(vertexai=True, project=PROJECT_ID, location=LOCATION)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "C8aMIcn9mEWt" + }, + "source": [ + "### Import libraries\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "rrgrbhPmmEWt" + }, + "outputs": [], + "source": [ + "from enum import Enum\n", + "\n", + "import pandas as pd\n", + "from IPython.display import display\n", + "from google.genai.types import GenerateContentConfig, Part\n", + "from pydantic import BaseModel, Field\n", + "\n", + "pd.set_option(\"display.max_colwidth\", None)\n", + "PDF_MIME_TYPE = \"application/pdf\"\n", + "JSON_MIME_TYPE = \"application/json\"\n", + "ENUM_MIME_TYPE = \"text/x.enum\"" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "e43229f3ad4f" + }, + "source": [ + "### Load the Gemini 3 Flash model\n", + "\n", + "To learn more about all [Gemini models on Vertex AI](https://cloud.google.com/vertex-ai/generative-ai/docs/learn/models#gemini-models)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "cf93d5f0ce00" + }, + "outputs": [], + "source": [ + "MODEL_ID = \"gemini-3.5-flash\" # @param {type: \"string\"}" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "53d3d02d82b0" + }, + "source": [ + "Create a pandas DataFrame to contain the data." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "43c76a9f1ea4" + }, + "outputs": [], + "source": [ + "tax_documents = pd.DataFrame(\n", + " {\n", + " \"file_uri\": [\n", + " \"https://storage.googleapis.com/cloud-samples-data/documentai/LendingDocAI/1099-DIV%20Parser/2020%20Form%201099-DIV%20-%20Anastasia%20Hodges.pdf\",\n", + " \"https://storage.googleapis.com/cloud-samples-data/documentai/LendingDocAI/1099-INT%20Parser/2020%20Form%201099-INT%20-%20Anastasia%20Hodges.pdf\",\n", + " \"https://storage.googleapis.com/cloud-samples-data/documentai/LendingDocAI/W2Parser/2020/2020%20Form%20W-2%20-%20Anastasia%20Hodges.pdf\",\n", + " ]\n", + " }\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "EdvJRUWRNGHE" + }, + "source": [ + "## Classify Documents\n", + "\n", + "First, we need to classify each of our documents.\n", + "\n", + "We will create an `Enum` class including each type of document." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "955ffce857e3" + }, + "outputs": [], + "source": [ + "class DocumentType(Enum):\n", + " W_2 = \"W-2\"\n", + " _1099_DIV = \"1099-DIV\"\n", + " _1099_INT = \"1099-INT\"" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "d3bd3714f764" + }, + "source": [ + "Next, we will send each document to the Gemini model with a classification prompt." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "649cb3dce660" + }, + "outputs": [], + "source": [ + "def classify_document(file_uri: str) -> Enum:\n", + " response = client.models.generate_content(\n", + " model=MODEL_ID,\n", + " contents=[\n", + " \"Classify the following document.\",\n", + " Part.from_uri(\n", + " file_uri=file_uri,\n", + " mime_type=PDF_MIME_TYPE,\n", + " ),\n", + " ],\n", + " config=GenerateContentConfig(\n", + " system_instruction=\"\"\"You are a document classification specialist. Given a document, your task is to find which category the document belongs to from the document categories provided in the schema.\"\"\",\n", + " temperature=0,\n", + " response_schema=DocumentType,\n", + " response_mime_type=ENUM_MIME_TYPE,\n", + " ),\n", + " )\n", + " return response.parsed\n", + "\n", + "\n", + "tax_documents[\"classification\"] = tax_documents[\"file_uri\"].apply(classify_document)\n", + "display(tax_documents)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "843b123600c7" + }, + "source": [ + "## Extract Data\n", + "\n", + "In order to extract the fields from each of these document types, we will need to create Pydantic classes containing the fields to extract for each type. Then we will create a mapping of the classification `Enum` to the Pydantic classes.\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "9c8d93b1ee61" + }, + "source": [ + "### Create Pydantic classes\n", + "\n", + "> Note: These Pydantic models were created using Gemini with the following prompt:\n", + "> \n", + "> `Create a Pydantic class from BaseModel to contain values to extract from a [Document Type]`" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "fb3fd97752c4" + }, + "outputs": [], + "source": [ + "class FormW2(BaseModel):\n", + " \"\"\"Pydantic class to represent data extracted from a Form W-2 (Wage and Tax Statement).\"\"\"\n", + "\n", + " employee_ssn: str = Field(..., description=\"Employee's Social Security Number\")\n", + " employer_ein: str = Field(\n", + " ..., description=\"Employer's Employer Identification Number\"\n", + " )\n", + " control_number: str | None = Field(\n", + " None, description=\"Employer's Control Number (Optional)\"\n", + " )\n", + " wages_tips_other_compensation: float = Field(\n", + " ..., description=\"Total Wages, tips, and other compensation\"\n", + " )\n", + " federal_income_tax_withheld: float = Field(\n", + " ..., description=\"Federal income tax withheld from wages\"\n", + " )\n", + " social_security_wages: float = Field(..., description=\"Social Security wages\")\n", + " social_security_tax_withheld: float = Field(\n", + " ..., description=\"Social Security tax withheld\"\n", + " )\n", + " medicare_wages_and_tips: float = Field(..., description=\"Medicare wages and tips\")\n", + " medicare_tax_withheld: float = Field(..., description=\"Medicare tax withheld\")\n", + " dependent_care_benefits: float | None = Field(\n", + " None, description=\"Dependent care benefits (Box 10)\"\n", + " )\n", + " nonqualified_plans: float | None = Field(\n", + " None, description=\"Nonqualified plans (Box 11)\"\n", + " )\n", + " box_12a_code: str | None = Field(None, description=\"Code for amount in Box 12a\")\n", + " box_12a_amount: float | None = Field(None, description=\"Amount for Code in Box 12a\")\n", + " box_12b_code: str | None = Field(None, description=\"Code for amount in Box 12b\")\n", + " box_12b_amount: float | None = Field(None, description=\"Amount for Code in Box 12b\")\n", + " box_12c_code: str | None = Field(None, description=\"Code for amount in Box 12c\")\n", + " box_12c_amount: float | None = Field(None, description=\"Amount for Code in Box 12c\")\n", + " box_12d_code: str | None = Field(None, description=\"Code for amount in Box 12d\")\n", + " box_12d_amount: float | None = Field(None, description=\"Amount for Code in Box 12d\")\n", + " statutory_employee: bool = Field(\n", + " False, description=\"Indicates if Statutory Employee\"\n", + " )\n", + " retirement_plan: bool = Field(False, description=\"Indicates if Retirement Plan\")\n", + " third_party_sick_pay: float | None = Field(\n", + " None, description=\"Third-party sick pay (Box 13)\"\n", + " )\n", + " other: str | None = Field(None, description=\"Other (Box 14)\")\n", + "\n", + " employer_name: str = Field(..., description=\"Employer's Name\")\n", + " employer_address: str = Field(..., description=\"Employer's Address\")\n", + " employer_city: str = Field(..., description=\"Employer's City\")\n", + " employer_state: str = Field(..., description=\"Employer's State (abbreviation)\")\n", + " employer_zip: str = Field(..., description=\"Employer's Zip Code\")\n", + "\n", + " employee_name: str = Field(..., description=\"Employee's Name\")\n", + " employee_address: str = Field(..., description=\"Employee's Address\")\n", + " employee_city: str = Field(..., description=\"Employee's City\")\n", + " employee_state: str = Field(..., description=\"Employee's State (abbreviation)\")\n", + " employee_zip: str = Field(..., description=\"Employee's Zip Code\")\n", + "\n", + " state: str | None = Field(None, description=\"State (if applicable)\")\n", + " state_employer_id: str | None = Field(\n", + " None, description=\"State Employer ID (if applicable)\"\n", + " )\n", + " state_wages: float | None = Field(None, description=\"State Wages (if applicable)\")\n", + " state_income_tax: float | None = Field(\n", + " None, description=\"State Income Tax (if applicable)\"\n", + " )\n", + "\n", + "\n", + "class Form1099DIV(BaseModel):\n", + " \"\"\"Pydantic class representing data extracted from Form 1099-DIV (Dividends and Distributions).\"\"\"\n", + "\n", + " payer_name: str | None = Field(\n", + " None, description=\"Name of the payer (company distributing dividends).\"\n", + " )\n", + " payer_street_address: str | None = Field(\n", + " None, description=\"Payer's street address.\"\n", + " )\n", + " payer_city: str | None = Field(None, description=\"Payer's city.\")\n", + " payer_state: str | None = Field(None, description=\"Payer's state.\")\n", + " payer_zip: str | None = Field(None, description=\"Payer's zip code.\")\n", + " payer_telephone: str | None = Field(None, description=\"Payer's telephone number.\")\n", + " payer_tin: str | None = Field(\n", + " None,\n", + " description=\"Payer's Taxpayer Identification Number (TIN).\",\n", + " alias=\"payer_id\",\n", + " )\n", + "\n", + " recipient_name: str | None = Field(None, description=\"Recipient's (your) name.\")\n", + " recipient_street_address: str | None = Field(\n", + " None, description=\"Recipient's street address.\"\n", + " )\n", + " recipient_city: str | None = Field(None, description=\"Recipient's city.\")\n", + " recipient_state: str | None = Field(None, description=\"Recipient's state.\")\n", + " recipient_zip: str | None = Field(None, description=\"Recipient's zip code.\")\n", + " recipient_identification_number: str | None = Field(\n", + " None,\n", + " description=\"Recipient's Taxpayer Identification Number (TIN) (usually your SSN).\",\n", + " alias=\"recipient_id\",\n", + " )\n", + " account_number: str | None = Field(\n", + " None, description=\"Recipient's account number (if applicable).\"\n", + " )\n", + "\n", + " # Box Values\n", + " box_1a_total_ordinary_dividends: float | None = Field(\n", + " None, description=\"Box 1a: Total Ordinary Dividends.\"\n", + " )\n", + " box_1b_qualified_dividends: float | None = Field(\n", + " None, description=\"Box 1b: Qualified Dividends.\"\n", + " )\n", + " box_2a_total_capital_gain_distributions: float | None = Field(\n", + " None, description=\"Box 2a: Total Capital Gain Distributions.\"\n", + " )\n", + " box_2b_unrecaptured_section_1250_gain: float | None = Field(\n", + " None, description=\"Box 2b: Unrecaptured Section 1250 Gain.\"\n", + " )\n", + " box_2c_section_1202_gain: float | None = Field(\n", + " None, description=\"Box 2c: Section 1202 Gain.\"\n", + " )\n", + " box_2d_collectibles_28_percent_rate_gain: float | None = Field(\n", + " None, description=\"Box 2d: Collectibles (28%) Rate Gain\"\n", + " )\n", + " box_3_nondividend_distributions: float | None = Field(\n", + " None, description=\"Box 3: Nondividend Distributions.\"\n", + " )\n", + " box_4_federal_income_tax_withheld: float | None = Field(\n", + " None, description=\"Box 4: Federal Income Tax Withheld.\"\n", + " )\n", + " box_5_section_199A_dividends: float | None = Field(\n", + " None, description=\"Box 5: Section 199A Dividends.\"\n", + " )\n", + " # Note Box 6 is not needed as it only notes if its a section 199A distribution\n", + "\n", + " foreign_tax_paid: float | None = Field(\n", + " None,\n", + " description=\"Foreign tax Paid (If any is marked by a boolean in the additional box section)\",\n", + " )\n", + "\n", + " foreign_country: str | None = Field(None, description=\"Name of Foreign Country\")\n", + "\n", + "\n", + "class Form1099INT(BaseModel):\n", + " \"\"\"Pydantic class representing data extracted from a Form 1099-INT (Interest Income).\"\"\"\n", + "\n", + " payer_name: str = Field(..., description=\"Name of the payer (bank, institution)\")\n", + " payer_tin: str = Field(\n", + " ...,\n", + " description=\"Payer's Taxpayer Identification Number (TIN)\",\n", + " alias=\"payer_tax_id\",\n", + " ) # Added alias\n", + " recipient_name: str = Field(..., description=\"Recipient's Name\")\n", + " recipient_tin: str = Field(\n", + " ...,\n", + " description=\"Recipient's Taxpayer Identification Number (TIN)\",\n", + " alias=\"recipient_tax_id\",\n", + " ) # Added alias\n", + " recipient_address: str = Field(..., description=\"Recipient's Address\")\n", + " recipient_city_state_zip: str = Field(\n", + " ..., description=\"Recipient's City, State, and Zip Code\"\n", + " )\n", + "\n", + " box_1_interest_income: float = Field(..., description=\"Box 1: Interest Income\")\n", + " box_2_early_withdrawal_penalty: float | None = Field(\n", + " None, description=\"Box 2: Early Withdrawal Penalty\"\n", + " )\n", + " box_3_interest_us_savings_bonds_treas_obligations: float | None = Field(\n", + " None,\n", + " description=\"Box 3: Interest on U.S. Savings Bonds and Treasury Obligations\",\n", + " )\n", + " box_4_federal_income_tax_withheld: float | None = Field(\n", + " None, description=\"Box 4: Federal Income Tax Withheld\"\n", + " )\n", + " box_5_investment_expenses: float | None = Field(\n", + " None, description=\"Box 5: Investment Expenses\"\n", + " )\n", + " box_6_foreign_tax_paid: float | None = Field(\n", + " None, description=\"Box 6: Foreign Tax Paid\"\n", + " )\n", + " box_7_foreign_country_or_us_possession: str | None = Field(\n", + " None, description=\"Box 7: Foreign Country or U.S. Possession\"\n", + " )\n", + "\n", + " account_number: str | None = Field(\n", + " None, description=\"Account Number (may be truncated)\"\n", + " )\n", + " form_year: int | None = Field(None, description=\"Year the form applies to\")\n", + " payer_street_address: str | None = Field(None, description=\"Payer's Street Address\")\n", + " payer_city_state_zip: str | None = Field(\n", + " None, description=\"Payer's City, State, and Zip Code\"\n", + " )\n", + "\n", + "\n", + "document_mapping: dict[DocumentType, BaseModel] = {\n", + " DocumentType.W_2: FormW2,\n", + " DocumentType._1099_DIV: Form1099DIV,\n", + " DocumentType._1099_INT: Form1099INT,\n", + "}" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "7ea36f19d9f7" + }, + "source": [ + "### Define the Gemini prompt\n", + "\n", + "Here's the prompt we'll use with Gemini to extract the information we need." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "281b1ff1b0b1" + }, + "outputs": [], + "source": [ + "def extract_document(row: pd.Series) -> dict:\n", + " response = client.models.generate_content(\n", + " model=MODEL_ID,\n", + " contents=[\n", + " f\"Extract from the following {row['classification'].value} document.\",\n", + " Part.from_uri(\n", + " file_uri=row[\"file_uri\"],\n", + " mime_type=PDF_MIME_TYPE,\n", + " ),\n", + " ],\n", + " config=GenerateContentConfig(\n", + " system_instruction=\"\"\"You are an expert in United States Tax Forms. Given a document, extract fields for income tax filing.\"\"\",\n", + " temperature=0,\n", + " response_schema=document_mapping.get(row[\"classification\"]),\n", + " response_mime_type=JSON_MIME_TYPE,\n", + " ),\n", + " )\n", + " print(row[\"file_uri\"])\n", + " print(response.parsed)\n", + " return response.parsed.model_dump()\n", + "\n", + "\n", + "tax_documents[\"extraction\"] = tax_documents.apply(extract_document, axis=1)\n", + "\n", + "# Normalize and flatten the extracted fields\n", + "extracted_df = pd.json_normalize(tax_documents[\"extraction\"])\n", + "\n", + "# Merge the extracted fields back into the original dataframe\n", + "tax_documents = tax_documents.drop(columns=[\"extraction\"]).join(extracted_df)\n", + "\n", + "display(tax_documents)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "8f9bd6bce1c9" + }, + "source": [ + "Now, we'll load the data to a CSV for further processing and tax calculation." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "89808e81a5c9" + }, + "outputs": [], + "source": [ + "tax_documents.to_csv(\"tax_data.csv\")" + ] + } + ], + "metadata": { + "colab": { + "name": "tax_automation.ipynb", + "toc_visible": true + }, + "kernelspec": { + "display_name": "Python 3", + "name": "python3" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} diff --git a/gemini/use-cases/education/use_cases_for_education.ipynb b/gemini/use-cases/education/use_cases_for_education.ipynb new file mode 100644 index 0000000..905a22f --- /dev/null +++ b/gemini/use-cases/education/use_cases_for_education.ipynb @@ -0,0 +1,1229 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "ijGzTHJJUCPY" + }, + "outputs": [], + "source": [ + "# Copyright 2024 Google LLC\n", + "#\n", + "# Licensed under the Apache License, Version 2.0 (the \"License\");\n", + "# you may not use this file except in compliance with the License.\n", + "# You may obtain a copy of the License at\n", + "#\n", + "# https://www.apache.org/licenses/LICENSE-2.0\n", + "#\n", + "# Unless required by applicable law or agreed to in writing, software\n", + "# distributed under the License is distributed on an \"AS IS\" BASIS,\n", + "# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n", + "# See the License for the specific language governing permissions and\n", + "# limitations under the License." + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": { + "id": "VEqbX8OhE8y9" + }, + "source": [ + "# Using Gemini in Education\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
    \n", + " \n", + " \"Google
    Run in Colab\n", + "
    \n", + "
    \n", + " \n", + " \"Google
    Run in Colab Enterprise\n", + "
    \n", + "
    \n", + " \n", + " \"Vertex
    Open in Vertex AI Workbench\n", + "
    \n", + "
    \n", + " \n", + " \"GitHub
    View on GitHub\n", + "
    \n", + "
    \n", + " \n", + " \"Google
    Open in Cloud Skills Boost\n", + "
    \n", + "
    \n", + "\n", + "
    \n", + "\n", + "Share to:\n", + "\n", + "\n", + " \"LinkedIn\n", + "\n", + "\n", + "\n", + " \"Bluesky\n", + "\n", + "\n", + "\n", + " \"X\n", + "\n", + "\n", + "\n", + " \"Reddit\n", + "\n", + "\n", + "\n", + " \"Facebook\n", + " \n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "3cf4e0393128" + }, + "source": [ + "| Author |\n", + "| --- |\n", + "| [Laurent Picard](https://github.com/PicardParis) |" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "VK1Q5ZYdVL4Y" + }, + "source": [ + "## Overview\n", + "\n", + "In this notebook, you will explore a variety of use cases enabled by Gemini in the context of education.\n", + "\n", + "### Gemini\n", + "\n", + "Gemini is a family of generative AI models developed by Google DeepMind.\n", + "\n", + "The Gemini models are built for multimodality from the ground up:\n", + "\n", + "- Supported inputs: Text, code, images, audio, video, video with audio, and PDF\n", + "- Generated output: Text\n", + "\n", + "### Gemini API in Vertex AI\n", + "\n", + "The Gemini API in Vertex AI provides a unified interface for interacting with the Gemini models.\n", + "\n", + "For more information, see [Gemini models](https://cloud.google.com/vertex-ai/generative-ai/docs/learn/models#gemini-models)." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "uVL_vGs4q3pg" + }, + "source": [ + "### Objectives\n", + "\n", + "The main objective of this notebook is to demonstrate a variety of educational use cases that can benefit from Gemini\n", + "\n", + "The steps performed include:\n", + "\n", + "- Installing the Python SDK\n", + "- Loading Gemini\n", + "- Reasoning at different levels\n", + "- Reasoning on text\n", + "- Reasoning on numbers\n", + "- Reasoning on a single image\n", + "- Reasoning on multiple images\n", + "- Reasoning on a video" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "bRdtKLfTsQ27" + }, + "source": [ + "### Costs\n", + "\n", + "This tutorial uses billable components of Google Cloud:\n", + "\n", + "- Vertex AI\n", + "\n", + "Learn about [Vertex AI pricing](https://cloud.google.com/vertex-ai/pricing) and use the [Pricing Calculator](https://cloud.google.com/products/calculator/) to generate a cost estimate based on your projected usage.\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "QDU0XJ1xRDlL" + }, + "source": [ + "## Getting Started\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "SBGrQE22sVrt" + }, + "source": [ + "### Install Google Gen AI SDK\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "-hqq5vomsW_P" + }, + "outputs": [], + "source": [ + "%pip install --upgrade --quiet google-genai" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "N5afkyDMSBW5" + }, + "source": [ + "### Authenticate your notebook environment (Colab only)\n", + "\n", + "If you are running this notebook on Google Colab, run the following cell to authenticate your environment. This step is not required if you are using [Vertex AI Workbench](https://cloud.google.com/vertex-ai-workbench).\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "Ab4Y6eSIUknb" + }, + "outputs": [], + "source": [ + "import sys\n", + "\n", + "# Additional authentication is required for Google Colab\n", + "if \"google.colab\" in sys.modules:\n", + " # Authenticate user to Google Cloud\n", + " from google.colab import auth\n", + "\n", + " auth.authenticate_user()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "kc4WxYmLSBW5" + }, + "source": [ + "### Define Google Cloud project information and initialize Vertex AI\n", + "\n", + "Initialize the Gen AI SDK for Python for your project:\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "YmY9HVVGSBW5" + }, + "outputs": [], + "source": [ + "# Use the environment variable if the user doesn't provide Project ID.\n", + "import os\n", + "\n", + "from google import genai\n", + "\n", + "# fmt: off\n", + "PROJECT_ID = \"[your-project-id]\" # @param {type: \"string\", placeholder: \"[your-project-id]\", isTemplate: true}\n", + "# fmt: on\n", + "if not PROJECT_ID or PROJECT_ID == \"[your-project-id]\":\n", + " PROJECT_ID = str(os.environ.get(\"GOOGLE_CLOUD_PROJECT\"))\n", + "\n", + "LOCATION = os.environ.get(\"GOOGLE_CLOUD_REGION\", \"us-central1\")\n", + "\n", + "client = genai.Client(vertexai=True, project=PROJECT_ID, location=LOCATION)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "kdINrwJZsj1d" + }, + "source": [ + "### Import libraries\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "stNmWCsRsotM" + }, + "outputs": [], + "source": [ + "from IPython.display import Markdown, display\n", + "from google.genai.types import GenerateContentConfig, Part" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "0v63w6fWs9Dx" + }, + "source": [ + "### Define helper functions\n", + "\n", + "Define some helper functions to load and display images.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "OGvJLH4DmZfw" + }, + "outputs": [], + "source": [ + "def generate_content(\n", + " model_id: str,\n", + " contents: list | str,\n", + ") -> str:\n", + " \"\"\"Call the Gemini API in Vertex AI.\n", + "\n", + " The default temperature (randomness/creativity) is set low for more consistent responses.\n", + " \"\"\"\n", + " return client.models.generate_content(\n", + " model=model_id,\n", + " contents=contents,\n", + " config=GenerateContentConfig(max_output_tokens=8192),\n", + " ).text" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "s9R3nV5bE22Z" + }, + "source": [ + "## Loading Gemini Model\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "UnUmflwsE22a" + }, + "outputs": [], + "source": [ + "MODEL_ID = \"gemini-3.5-flash\"" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "9fm71OTvpyqD" + }, + "source": [ + "## Reasoning at different levels\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "zgf3t9odFlIj" + }, + "source": [ + "You can ask for direct answers:\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "tBpDHLmMv2un" + }, + "outputs": [], + "source": [ + "contents = \"\"\"\n", + "What happened to the dinosaurs? When?\n", + "Explain simply in one sentence.\n", + "\"\"\"\n", + "\n", + "display(Markdown(generate_content(MODEL_ID, contents)))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "Rfs1qLAoFrZu" + }, + "source": [ + "
 as well as for more nuanced answers:\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "AcVfT9bowUKW" + }, + "outputs": [], + "source": [ + "contents = \"\"\"\n", + "Are we 100% sure about what happened to the dinosaurs?\n", + "If not, detail the current main hypotheses.\n", + "\"\"\"\n", + "\n", + "display(Markdown(generate_content(MODEL_ID, contents)))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "-KPNK1hNGCk4" + }, + "source": [ + "You can ask for simple answers:\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "SPoEA7UUqlvy" + }, + "outputs": [], + "source": [ + "contents = \"\"\"\n", + "Explain why it's summer here in France and winter in Australia.\n", + "I'm a kid. Answer in simple key points.\n", + "\"\"\"\n", + "\n", + "display(Markdown(generate_content(MODEL_ID, contents)))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "gMlbj1-pGKkA" + }, + "source": [ + "
 or for more detailed answers:\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "LdiaJBtbpXjM" + }, + "outputs": [], + "source": [ + "contents = \"\"\"\n", + "Explain why we have tides.\n", + "I'm a scientist. Provide a detailed answer using bullet points.\n", + "\"\"\"\n", + "\n", + "display(Markdown(generate_content(MODEL_ID, contents)))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "hV6ULv8KHkxO" + }, + "source": [ + "You can ask closed questions:\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "dGk1uEPN0XXq" + }, + "outputs": [], + "source": [ + "contents = \"\"\"\n", + "When were the previous and penultimate leap years?\n", + "List 3 international competitions that took place during the penultimate one.\n", + "Detail dates, cities, and venues.\n", + "\"\"\"\n", + "\n", + "display(Markdown(generate_content(MODEL_ID, contents)))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "SzmiMEq_HsNF" + }, + "source": [ + "
 as well as questions that are more open:\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "HqcYe_w5BvLT" + }, + "outputs": [], + "source": [ + "contents = \"\"\"\n", + "What came first, the chicken or the egg? Explain from 3 different perspectives.\n", + "What do we call a \"chicken and egg\" problem? Give 1 example that can occur in education.\n", + "\"\"\"\n", + "\n", + "display(Markdown(generate_content(MODEL_ID, contents)))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "BoqqOTjfL6HX" + }, + "source": [ + "## Reasoning on text\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "1dEt9vXqmGN5" + }, + "source": [ + "You can summarize and translate text:\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "jAbIQ5U3mGN5" + }, + "outputs": [], + "source": [ + "contents = \"\"\"\n", + "Summarize the following text in English, in 3 short bullet points, only using the text.\n", + "\n", + "TEXT:\n", + "- Les hommes naissent et demeurent libres et Ă©gaux en droits. Les distinctions sociales ne peuvent ĂȘtre fondĂ©es que sur l'utilitĂ© commune.\n", + "- Le but de toute association politique est la conservation des droits naturels et imprescriptibles de l'homme. Ces droits sont la libertĂ©, la propriĂ©tĂ©, la sĂ»retĂ© et la rĂ©sistance Ă  l'oppression.\n", + "- Le principe de toute souverainetĂ© rĂ©side essentiellement dans la Nation. Nul corps, nul individu ne peut exercer d'autoritĂ© qui n'en Ă©mane expressĂ©ment.\n", + "- La libertĂ© consiste Ă  pouvoir faire tout ce qui ne nuit pas Ă  autrui : ainsi, l'exercice des droits naturels de chaque homme n'a de bornes que celles qui assurent aux autres membres de la sociĂ©tĂ© la jouissance de ces mĂȘmes droits. Ces bornes ne peuvent ĂȘtre dĂ©terminĂ©es que par la loi.\n", + "La loi n'a le droit de dĂ©fendre que les actions nuisibles Ă  la sociĂ©tĂ©. Tout ce qui n'est pas dĂ©fendu par la loi ne peut ĂȘtre empĂȘchĂ©, et nul ne peut ĂȘtre contraint Ă  faire ce qu'elle n'ordonne pas.\n", + "- La loi est l'expression de la volontĂ© gĂ©nĂ©rale. Tous les citoyens ont droit de concourir personnellement ou par leurs reprĂ©sentants Ă  sa formation. Elle doit ĂȘtre la mĂȘme pour tous, soit qu'elle protĂšge, soit qu'elle punisse. Tous les citoyens, Ă©tant Ă©gaux Ă  ses yeux, sont Ă©galement admissibles Ă  toutes dignitĂ©s, places et emplois publics, selon leur capacitĂ© et sans autre distinction que celle de leurs vertus et de leurs talents.\n", + "- Nul homme ne peut ĂȘtre accusĂ©, arrĂȘtĂ© ou dĂ©tenu que dans les cas dĂ©terminĂ©s par la loi et selon les formes qu'elle a prescrites. Ceux qui sollicitent, expĂ©dient, exĂ©cutent ou font exĂ©cuter des ordres arbitraires doivent ĂȘtre punis ; mais tout citoyen appelĂ© ou saisi en vertu de la loi doit obĂ©ir Ă  l'instant ; il se rend coupable par la rĂ©sistance.\n", + "- La loi ne doit Ă©tablir que des peines strictement et Ă©videmment nĂ©cessaires, et nul ne peut ĂȘtre puni qu'en vertu d'une loi Ă©tablie et promulguĂ©e antĂ©rieurement au dĂ©lit, et lĂ©galement appliquĂ©e.\n", + "Tout homme Ă©tant prĂ©sumĂ© innocent jusqu'Ă  ce qu'il ait Ă©tĂ© dĂ©clarĂ© coupable, s'il est jugĂ© indispensable de l'arrĂȘter, toute rigueur qui ne serait pas nĂ©cessaire pour s'assurer de sa personne doit ĂȘtre sĂ©vĂšrement rĂ©primĂ©e par la loi.\n", + "- Nul ne doit ĂȘtre inquiĂ©tĂ© pour ses opinions, mĂȘme religieuses, pourvu que leur manifestation ne trouble pas l'ordre public Ă©tabli par la loi.\n", + "\"\"\"\n", + "\n", + "display(Markdown(generate_content(MODEL_ID, contents)))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "Ri8Yx5vtE22b" + }, + "source": [ + "
 as well as ask for ideas:\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "k5pXqmNJmGN5" + }, + "outputs": [], + "source": [ + "contents = \"\"\"\n", + "Provide an outline in 5 key points for a \"chocolate in the world\" presentation.\n", + "One part must be about its origin in Mexico (my teacher has family there).\n", + "The last one will be a tasting with everybody in the classroom.\n", + "\"\"\"\n", + "\n", + "# For more creative responses, let's increase the level of randomness with a higher temperature.\n", + "# Successive requests will likely return different answers.\n", + "display(Markdown(generate_content(MODEL_ID, contents, temperature=1.0)))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "zf25rsTSkdXu" + }, + "source": [ + "You can also ask for text corrections:\n", + "\n", + "Below, examples are provided to help the model generate responses with the expected structure and formatting. This is also called few-shot prompting.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "jgKEDKVkL8Ub" + }, + "outputs": [], + "source": [ + "contents = \"\"\"\n", + "I'm a non-native English speaker.\n", + "Check whether the following sentences are correct.\n", + "When incorrect, provide a correction and an explanation.\n", + "Use the same structure as in the given examples.\n", + "\n", + "EXAMPLES:\n", + "- **Hi!**\n", + " - Status: ✔\n", + "- **Your my best freind!**\n", + " - Status: ❌\n", + " - Correction: **You're my best friend!**\n", + " - Explanation:\n", + " - \"**Your**\" is incorrect. It seems that you meant \"You're\", which is the short form of \"You are\".\n", + " - \"**freind**\" is misspelled. The correct spelling is \"**friend**\".\n", + "\n", + "SENTENCES:\n", + "- They're twins, isn't it?\n", + "- I assisted to the meeting.\n", + "- You received important informations.\n", + "- I digged a hole in the ice and saw lots of fishes.\n", + "- That's all folks!\n", + "\"\"\"\n", + "\n", + "display(Markdown(generate_content(MODEL_ID, contents)))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "ach9JtNikxVz" + }, + "source": [ + "
 as well as ask for elaborate tasks on text and languages:\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "P0xdsfQDE22b" + }, + "outputs": [], + "source": [ + "contents = \"\"\"\n", + "Translate the text into the following languages.\n", + "\n", + "TEXT:\n", + "Hello folks! I hope you're all doing well. Let's get this workshop started!\n", + "We'll stick to English because, actually, I can't speak all those languages.\n", + "\n", + "LANGUAGES:\n", + "German, French, Greek, Bulgarian, Japanese\n", + "\"\"\"\n", + "\n", + "display(Markdown(generate_content(MODEL_ID, contents)))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "LSNoxDSeZWw-" + }, + "outputs": [], + "source": [ + "contents = \"\"\"\n", + "I'm a non-native English speaker and made mistakes in the following sentences.\n", + "Guess my native language (if there are several possibilities, here is a hint: I like cheese).\n", + "Explain why these are typical mistakes.\n", + "\n", + "SENTENCES:\n", + "- They are twin sisters, isn't it?\n", + "- I assisted to the meeting.\n", + "- I saw lots of fishes.\n", + "\"\"\"\n", + "\n", + "display(Markdown(generate_content(MODEL_ID, contents)))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "AIl7R_jBUsaC" + }, + "source": [ + "## Reasoning on numbers\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "hm61coMZJX-o" + }, + "source": [ + "> Note: Depending on inputs and parameters, large language models can hallucinate and generate inaccurate outputs, including math operations. As a best practice, consider using prompts with step-by-step instructions to reduce hallucinations, or use a calculator library for more advanced math. You can also ask Gemini to generate problem-solving code (see [Doing Math with Large Language Models](https://medium.com/google-cloud/doing-math-with-large-language-models-69d94c8b0590)).\n", + "\n", + "You can ask about real life problems:\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "nmvIJfDUmGN6" + }, + "outputs": [], + "source": [ + "contents = \"\"\"\n", + "Patricia is a good runner and runs at an average 12 km/h.\n", + "- On Monday, she ran for 1.5 hour. What distance did she run?\n", + "- On Tuesday, she ran for 21 km. How long did she run?\n", + "- On Wednesday, she ran for 150 minutes. What distance did she run?\n", + "- Next, she plans to do a marathon (42 km). How long should it take?\n", + "- To complete a marathon in 3 hours, how much faster does she need to run?\n", + "\n", + "Detail the answers step by step.\n", + "\"\"\"\n", + "\n", + "display(Markdown(generate_content(MODEL_ID, contents)))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "_nZwoSe0mGN6" + }, + "source": [ + "
 or about classical problems:\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "R6dmn9_Q0Be0" + }, + "outputs": [], + "source": [ + "contents = \"\"\"\n", + "I just borrowed 1,000 EUR from a friend.\n", + "We agreed on a 4.5% simple interest rate, based solely on the initial amount borrowed.\n", + "I want to know how much I'll have to refund in 1, 2, or 3 years.\n", + "Present the results in a recap table.\n", + "\"\"\"\n", + "\n", + "display(Markdown(generate_content(MODEL_ID, contents)))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "dvdNYQKJE22c" + }, + "source": [ + "## Reasoning on a single image\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "UFW-KLIjmGN7" + }, + "source": [ + "You can ask for an image description:\n", + "\n", + "![Abacus](https://unsplash.com/photos/cHhbULJbPwM/download?w=600)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "aFvAEetIcSA7" + }, + "outputs": [], + "source": [ + "prompt = \"Describe this image in a short sentence:\"\n", + "\n", + "# Image by Crissy Jarvis on Unsplash: https://unsplash.com/photos/cHhbULJbPwM\n", + "image_abacus = Part.from_uri(\n", + " file_uri=\"https://images.unsplash.com/photo-1548690596-f1722c190938?q=80&w=3548&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D\",\n", + " mime_type=\"image/jpeg\",\n", + ")\n", + "\n", + "contents = [prompt, image_abacus]\n", + "\n", + "display(Markdown(generate_content(MODEL_ID, contents)))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "jjqqXuauE22c" + }, + "source": [ + "
 or ask specific questions:\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "H0SOqKjyi1tH" + }, + "outputs": [], + "source": [ + "prompt = \"\"\"\n", + "Answer the following questions about this image.\n", + "Return the results as a JSON list containing \"question\" and \"answer\" key pairs.\n", + "\n", + "QUESTIONS:\n", + "- What does the image show?\n", + "- How does it work?\n", + "- When was it invented?\n", + "- What's the name of this object in French, Italian, Spanish, Dutch, and German?\n", + "- What are the most prominent colors in the image?\n", + "\"\"\"\n", + "\n", + "contents = [prompt, image_abacus]\n", + "\n", + "display(Markdown(generate_content(MODEL_ID, contents)))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "I9SIjHnbmGN8" + }, + "source": [ + "Your specific questions can have follow-up questions:\n", + "\n", + "![Tiles](https://unsplash.com/photos/E1por_SGvJE/download?w=600)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "bGIsUZFAmGN8" + }, + "outputs": [], + "source": [ + "# Image by Brett Jordan on Unsplash: https://unsplash.com/photos/E1por_SGvJE\n", + "image_tiles = Part.from_uri(\n", + " file_uri=\"https://images.unsplash.com/photo-1612174636808-fdb8c5d80780?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&w=600\",\n", + " mime_type=\"image/jpeg\",\n", + ")\n", + "\n", + "prompt = \"\"\"\n", + "- What expression can be read in this image? How is it presented?\n", + "- What is the opposite expression?\n", + "- What is a recommendation, starting with this expression, a teacher could give his students for an exam?\n", + "- With the opposite expression?\n", + "\"\"\"\n", + "\n", + "contents = [image_tiles, prompt]\n", + "display(Markdown(generate_content(MODEL_ID, contents)))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "trT6xm249rqo" + }, + "source": [ + "Information can have multiple forms. It can be objects, printed text, handwritten text, and more:\n", + "\n", + "![Euler](https://storage.googleapis.com/cloud-samples-data/generative-ai/image/math_beauty.jpg)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "VKsArAoJmGN8" + }, + "outputs": [], + "source": [ + "prompt = \"\"\"\n", + "Follow the instructions.\n", + "Write math expressions in LaTex.\n", + "Use a table with a row for each instruction and its result.\n", + "\n", + "INSTRUCTIONS:\n", + "- Extract the formula.\n", + "- What is the symbol right before Pi? What does it mean?\n", + "- Is this a famous formula? Does it have a name?\n", + "- Why is it special?\n", + "- Extract the caption.\n", + "- What color is it?\n", + "- What color is the formula?\n", + "- What's the object in the bottom?\n", + "- What can you conclude about the object?\n", + "\"\"\"\n", + "\n", + "image_euler = Part.from_uri(\n", + " file_uri=\"https://storage.googleapis.com/cloud-samples-data/generative-ai/image/math_beauty.jpg\",\n", + " mime_type=\"image/jpeg\",\n", + ")\n", + "\n", + "contents = [prompt, image_euler]\n", + "display(Markdown(generate_content(MODEL_ID, contents)))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "XHHKFGbMU-Re" + }, + "source": [ + "You can also ask for interpretations and suggestions:\n", + "\n", + "![Classroom](https://unsplash.com/photos/4ApmfdVo32Q/download?w=600)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "cJ0TsH-7unIE" + }, + "outputs": [], + "source": [ + "prompt = \"\"\"\n", + "Answer the following questions about the image.\n", + "Present the results in a table with a row for each question and its answer.\n", + "\n", + "QUESTIONS:\n", + "- What is visible?\n", + "- What are the reasons it's funny?\n", + "- What could be a fun caption?\n", + "- What could happen next?\n", + "- How would you alter the image? Would it still be funny and why?\n", + "- How would you make it funnier?\n", + "\"\"\"\n", + "# Image by Elimende Inagella on Unsplash: https://unsplash.com/photos/4ApmfdVo32Q\n", + "image_classroom = Part.from_uri(\n", + " file_uri=\"https://images.unsplash.com/photo-1613905780946-26b73b6f6e11?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&w=600\",\n", + " mime_type=\"image/jpeg\",\n", + ")\n", + "\n", + "contents = [prompt, image_classroom]\n", + "display(Markdown(generate_content(MODEL_ID, contents)))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "qU5Tm-FQE22c" + }, + "source": [ + "## Reasoning on multiple images\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "Vbf1HALvE22d" + }, + "source": [ + "You can also use multiple images:\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "sLUesezCE22d" + }, + "outputs": [], + "source": [ + "prompt = \"\"\"\n", + "Answer the following questions for each image.\n", + "Present the results in a table with a row for each image and a column for each question.\n", + "\n", + "QUESTIONS:\n", + "- What can we see in the image?\n", + "- Where does it take place? (answer in one word)\n", + "\"\"\"\n", + "caption_b1 = \"Image 1:\"\n", + "caption_b2 = \"Image 2:\"\n", + "caption_b3 = \"Image 3:\"\n", + "\n", + "# Photo by Deleece Cook on Unsplash: https://unsplash.com/photos/zzjLGF_6dx4\n", + "image_b1 = Part.from_uri(\n", + " file_uri=\"https://images.unsplash.com/photo-1542587227-8802646daa56?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&w=600\",\n", + " mime_type=\"image/jpeg\",\n", + ")\n", + "# Photo by Natasha Kapur on Unsplash: https://unsplash.com/photos/ndAHi2Wxcok\n", + "image_b2 = Part.from_uri(\n", + " file_uri=\"https://images.unsplash.com/photo-1498669374702-58e97ebbede3?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&w=600\",\n", + " mime_type=\"image/jpeg\",\n", + ")\n", + "# Photo by Roman Mager on Unsplash: https://unsplash.com/photos/5mZ_M06Fc9g\n", + "image_b3 = Part.from_uri(\n", + " file_uri=\"https://images.unsplash.com/photo-1453733190371-0a9bedd82893?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&w=600\",\n", + " mime_type=\"image/jpeg\",\n", + ")\n", + "contents = [prompt, caption_b1, image_b1, caption_b2, image_b2, caption_b3, image_b3]\n", + "display(Markdown(generate_content(MODEL_ID, contents)))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "e0968313024e" + }, + "source": [ + "
 or make comparisons between images:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "d1a33383a717" + }, + "outputs": [], + "source": [ + "prompt = \"\"\"\n", + "Answer the following questions about the images, with a short answer and a detailed reason for the answer.\n", + "\n", + "QUESTIONS:\n", + "- What do the images have in common?\n", + "- Which one would be of interest to a mathematician?\n", + "- Which one indicates it's the end of vacation?\n", + "- Which one suggests we may get a coffee there?\n", + "\"\"\"\n", + "\n", + "contents = [prompt, caption_b1, image_b1, caption_b2, image_b2, caption_b3, image_b3]\n", + "display(Markdown(generate_content(MODEL_ID, contents)))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "uqFSjI4_mGN9" + }, + "source": [ + "You can use Gemini's level of language and visual understanding to work with concepts or even get suggestions on new images:\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "QjbP2y26mGN9" + }, + "outputs": [], + "source": [ + "prompt = \"\"\"\n", + "Answer the following questions about the images, with a short answer and a detailed reason for the answer.\n", + "\n", + "QUESTIONS:\n", + "- What does the first image represent?\n", + "- What does the second image represent?\n", + "- What could be the next logical image?\n", + "\"\"\"\n", + "caption_w1 = \"Image 1:\"\n", + "caption_w2 = \"Image 2:\"\n", + "# Photo by Diego Ballon Vargas on Unsplash: https://unsplash.com/photos/TA5bUTySOrg\n", + "image_w1 = Part.from_uri(\n", + " file_uri=\"https://images.unsplash.com/photo-1502251590879-9b45a5d29c7f?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&w=600\",\n", + " mime_type=\"image/jpeg\",\n", + ")\n", + "# Photo by Reza Shayestehpour on Unsplash: https://unsplash.com/photos/Nw_D8v79PM4\n", + "image_w2 = Part.from_uri(\n", + " file_uri=\"https://images.unsplash.com/photo-1428592953211-077101b2021b?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&w=600\",\n", + " mime_type=\"image/jpeg\",\n", + ")\n", + "\n", + "contents = [\n", + " caption_w1,\n", + " image_w1,\n", + " caption_w2,\n", + " image_w2,\n", + " prompt,\n", + "]\n", + "display(Markdown(generate_content(MODEL_ID, contents)))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "dJdBiddamGN9" + }, + "source": [ + "This is really up to your imagination:\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "iFhubVwFmGN9" + }, + "outputs": [], + "source": [ + "prompt = \"\"\"\n", + "Answer the following questions, with a short answer and a detailed reason for the answer.\n", + "\n", + "QUESTIONS:\n", + "- What theme do these images illustrate?\n", + "- What could be another image to replace the first one?\n", + "- What other image could replace the second one?\n", + "- What would be an alternative to the third image?\n", + "\"\"\"\n", + "caption_s1 = \"Image 1:\"\n", + "caption_s2 = \"Image 2:\"\n", + "caption_s3 = \"Image 3:\"\n", + "\n", + "# Photo by Tomoko Uji on Unsplash: https://unsplash.com/photos/eriuKJwcdjI\n", + "image_s1 = Part.from_uri(\n", + " file_uri=\"https://images.unsplash.com/photo-1522748906645-95d8adfd52c7?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&w=600\",\n", + " mime_type=\"image/jpeg\",\n", + ")\n", + "\n", + "# Photo by Todd Trapani on Unsplash: https://unsplash.com/photos/QldMpmrmWuc\n", + "image_s2 = Part.from_uri(\n", + " file_uri=\"https://images.unsplash.com/photo-1598920710727-e6c74781538c?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&w=600\",\n", + " mime_type=\"image/jpeg\",\n", + ")\n", + "\n", + "# Photo by Olivia Hutcherson on Unsplash: https://unsplash.com/photos/rN3m7aTH3io\n", + "image_s3 = Part.from_uri(\n", + " file_uri=\"https://images.unsplash.com/photo-1538580619159-6c19131e1062?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&w=600\",\n", + " mime_type=\"image/jpeg\",\n", + ")\n", + "\n", + "contents = [\n", + " prompt,\n", + " caption_s1,\n", + " image_s1,\n", + " caption_s2,\n", + " image_s2,\n", + " caption_s3,\n", + " image_s3,\n", + "]\n", + "display(Markdown(generate_content(MODEL_ID, contents)))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "JkjzQsgKGS7o" + }, + "source": [ + "## Reasoning on a video\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "_irp8pd4GS7o" + }, + "source": [ + "And you also can extract information from a video:\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "o9dOaKJoUknq" + }, + "outputs": [], + "source": [ + "prompt = \"\"\"\n", + "Answer the following questions using the video only.\n", + "Present the results for each question and its answer, as well as the timestamps where the answer can be found and whether the info source comes from \"Image\", \"Text\", and/or \"Speech\".\n", + "\n", + "QUESTIONS:\n", + "- Where was the video likely shot?\n", + "- What real animals are first visible as a group?\n", + "- What animals are cartoon characters doing a close-up selfie?\n", + "- What does the electronic device let real animals do?\n", + "- What is the veterinarian full name?\n", + "- Where does he work?\n", + "- What is Courtney's job position?\n", + "- What's her full name?\n", + "- Which famous brand is first visible?\n", + "- Which famous brand is last visible?\n", + "- What happens at timestamp 0:36?\n", + "- What happens at timestamp 1:05?\n", + "\"\"\"\n", + "video = Part.from_uri(\n", + " file_uri=\"gs://cloud-samples-data/video/animals.mp4\",\n", + " mime_type=\"video/mp4\",\n", + ")\n", + "\n", + "contents = [prompt, video]\n", + "display(Markdown(generate_content(MODEL_ID, contents)))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "806cbfafe417" + }, + "source": [ + "Some remarks:\n", + "\n", + "- Gemini uses both video and audio information.\n", + " - Video files are sampled at 1 frame per second (1fps).\n", + " - Video and audio samples are analyzed with their timestamps.\n", + " - Text (typewritten or handwritten) can be detected in video frames, like when reasoning on images.\n", + " - This explains why the extracted samples can give you three possible sources of information:\n", + " - image (video frame)\n", + " - text (text in the video frame)\n", + " - speech (audio sample)\n", + "- Using timestamps, you can also ask questions about specific video locations.\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "jWT14Lw6Uknq" + }, + "source": [ + "## Conclusion\n", + "\n", + "In this tutorial, you saw examples of how to use Gemini in education to reason on text, images, and videos.\n", + "\n", + "For more information, see the following documentation pages:\n", + "\n", + "- [Image understanding](https://cloud.google.com/vertex-ai/generative-ai/docs/multimodal/image-understanding)\n", + "- [Video understanding](https://cloud.google.com/vertex-ai/generative-ai/docs/multimodal/video-understanding)\n", + "- [Audio understanding](https://cloud.google.com/vertex-ai/generative-ai/docs/multimodal/audio-understanding)\n", + "- [Document understanding](https://cloud.google.com/vertex-ai/generative-ai/docs/multimodal/document-understanding)\n", + "\n", + "You may also want to explore other tutorials that focus on different domains or specificities of the Gemini API in Vertex AI." + ] + } + ], + "metadata": { + "colab": { + "name": "use_cases_for_education.ipynb", + "toc_visible": true + }, + "kernelspec": { + "display_name": "Python 3", + "name": "python3" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} diff --git a/gemini/use-cases/entity-extraction/Dockerfile b/gemini/use-cases/entity-extraction/Dockerfile new file mode 100644 index 0000000..8eb292e --- /dev/null +++ b/gemini/use-cases/entity-extraction/Dockerfile @@ -0,0 +1,26 @@ +# Dockerfile + +# Use an official Python runtime as a parent image +FROM python:3.11-slim + +# Set the working directory in the container +WORKDIR /app + +# Set environment variables +ENV PYTHONUNBUFFERED True + +# Copy the dependencies file and install them +COPY requirements.txt requirements.txt +RUN pip install --no-cache-dir -r requirements.txt + +# Copy the rest of the application code into the container +COPY . . + +# Get the port number from the environment variable +ENV PORT 8080 + +# Expose the port the app runs on +EXPOSE $PORT + +# Define the command to run the application using Gunicorn +CMD ["gunicorn", "--bind", "0.0.0.0:$PORT", "main:app"] diff --git a/gemini/use-cases/entity-extraction/README.md b/gemini/use-cases/entity-extraction/README.md new file mode 100644 index 0000000..f6ea00e --- /dev/null +++ b/gemini/use-cases/entity-extraction/README.md @@ -0,0 +1,325 @@ +# Entity Extraction with Gemini + +## About + +This project provides a flexible solution for classifying and extracting structured +information from documents using Gemini family of models via the Vertex AI API. It +exposes this functionality through a simple Flask-based web server running on Cloud Run. + +### When to use this solution + +The use of Gemini API for document classification and entity extraction is especially +useful when the structure of the document varies from one document type to another or +when it is unknown. For more structured documents like complex forms, we recommend +taking a look into [Document AI](https://cloud.google.com/document-ai/docs/overview), +that provides powerful mechanisms for entity extraction and layout parsing. + + +## Overview + +The core of this project is a Python script that takes a document and a configuration +ID as input. The configuration specifies which Gemini model to use, the name of the +document type, and a JSON schema of the fields to extract. The script then prompts the +Gemini model to extract the requested information from the document and return it as a +JSON object. It also allows you to classify a document based on a description of the +document type. + +This is wrapped in a Flask web application, allowing you to easily integrate document +extraction capabilities into your own services via an HTTP API. We provide a script for +deployment to a service in Cloud Run. + +## Features + +- **AI-Powered Extraction**: Leverages the multimodal capabilities of Gemini models to + understand, classify and extract data from documents. +- **Configurable Schemas**: Easily define different extraction schemas for various + document types (e.g., reports, legal documents) in a central configuration file. +- **JSON Output**: The model is prompted to return structured data in JSON format, + making it easy to parse and use in downstream applications. +- **Web API**: A simple Flask server exposes the extraction functionality through a + `/extract` endpoint. +- **Google Cloud Integration**: Built to run on Google Cloud, utilizing Vertex AI for + scalable and managed access to Gemini models. + +## Architecture + +The current solution includes the ability to classify documents and extract entities +based on the specific document type and the fields specified in the configuration using +the Gemini API (online). + +![Current Architecture](./images/current_architecture.png) + +## Prerequisites + +- A Google Cloud Project with the Vertex AI API enabled. +- Google Cloud SDK installed and authenticated. +- A Google Cloud Storage bucket for hosting the documents to be processed. + +## Setup and installation + +1. **Clone the repository:** + + ```bash + git clone https://github.com/GoogleCloudPlatform/generative-ai.git + cd generative-ai/gemini/use-cases/entity-extraction + ``` + +2. **Create and activate a Python virtual environment:** + + ```bash + python3 -m venv venv + source venv/bin/activate + ``` + +3. **Install dependencies:** + + ```bash + pip install -r requirements.txt + ``` + +4. **Configure Google Cloud settings:** + Open `.env` and update the following constant with your Google Cloud project + details: + ```bash + GEMINI_PROJECT_ID="project-id-for-gemini-api" + ``` + To store evaluation results in Vertex AI Experiments, you can also define an evaluation destination bucket: + ```bash + EVAL_DEST="gs://your-bucket-name/evaluations" + ``` + You can also update the other constants if needed. + +## Testing + +A simple test case is provided in `document_processing_test.py`. + +To run the provided test: + +```bash +python document_processing_test.py +``` + +This will call the relevant functions from `document_processing.py` with sample +documents and assert that the outputs match the expected JSON. + +**Note:** Running the test will make live calls to the Vertex AI API and may incur +costs. + +## Usage + +### Deploying to Cloud Run + +Open `.env` and update the following constants with your Google Cloud project details: + +```bash +CLOUD_RUN_PROJECT_ID="project-id-for-cloud-run-deployment" +``` + +You can also update the other constants if needed. + +To deploy the Flask application to Cloud Run, run: + +```bash +sh deploy.sh +``` + +### Sending an Extraction Request + +You can send a `POST` request to the `/extract` endpoint with a JSON payload +containing the `extract_config_id` and the `document_uri` (a GCS URI for the PDF). + +Here is an example using `curl`. Replace with the URL that you get after you deploy the +service to Cloud Run, + +```bash +curl -X POST https://YOUR-CLOUD-RUN-URL/extract \ +-H "Content-Type: application/json" \ +-H "Authorization: Bearer $(gcloud auth print-identity-token)" \ +-d '{ + "extract_config_id": "form_10_q", + "document_uri": "gs://cloud-samples-data/gen-app-builder/search/alphabet-investor-pdfs/2021Q1_alphabet_earnings_release.pdf" +}' +``` + +**Expected Response:** + +```json +{ + "year": "2021", + "quarter": "Q1", + "company_name": "Alphabet Inc.", + "ceo": "Sundar Pichai", + "net_income_millions": "17930" +} +``` + +### Sending a Classification Request + +You can send a POST request to the /classify endpoint with a JSON payload containing +the document_uri (a GCS URI for the PDF). +Here is an example using curl. Replace with the URL that you get after you deploy the +service to Cloud Run, + +```bash +curl -X POST https://YOUR-CLOUD-RUN-URL/classify +-H "Content-Type: application/json" +-H "Authorization: Bearer $(gcloud auth print-identity-token)" +-d '{ + "document_uri": "gs://cloud-samples-data/gen-app-builder/search/alphabet-investor-pdfs/2021Q1_alphabet_earnings_release.pdf" +}' +``` + +**Expected Response:** + +```json +{ + "class": "form_10_q" +} +``` + +### Sending a Classification and Extraction Request + +You can send a POST request to the `/classify_and_extract` endpoint with a JSON payload +containing the document_uri. The service will first classify the document and then use +the corresponding extraction configuration. + +```bash +curl -X POST https://YOUR-CLOUD-RUN-URL/classify_and_extract +-H "Content-Type: application/json" +-H "Authorization: Bearer $(gcloud auth print-identity-token)" +-d '{ + "document_uri": "gs://cloud-samples-data/gen-app-builder/search/alphabet-investor-pdfs/2021Q1_alphabet_earnings_release.pdf" +}' +``` + +**Expected Response:** + +```json +{ + "year": "2021", + "quarter": "Q1", + "company_name": "Alphabet Inc.", + "ceo": "Sundar Pichai", + "net_income_millions": "17930" +} +``` + +## Configuration of entities for classification, extraction and prompts + +### Entities + +The classification and extraction behavior is controlled by the configuration file +`config.json`, which holds the configurations for different document types and the +fields to extract. To add a new document type or field, simply add new key-value pairs. + +```json +"your_config_id": { + "document_name": "A descriptive name for your document type", + "document_mime_type": "application/pdf", + "model": "gemini-2.5-flash", + "fields": { + "field_key_1": "Description of what to extract for this field", + "field_key_2": "Another field description" + } +}, +``` + +Note that you can place the configuration file either locally or in a bucket. You can +define the location of the `CONFIG_PATH` variable in the `.env` file. If the constant +starts with `"gs://"`, the solution will look for the file in GCS, otherwise it will +look for it locally. Example: + +```bash +CONFIG_PATH="config.json" +``` + +For classification, you can define the classes under the classification_config key. +The model will use the descriptions to classify the document. + +```json +"classification_config": { + "document_mime_type": "application/pdf", + "model": "gemini-2.5-flash", + "classes": { + "class_name_1": "Description of the first document class", + "class_name_2": "Description of the second document class" + } +} +``` + +### Prompt + +The constants `EXTRACT_PROMPT_TEMPLATE` and `CLASSIFY_PROMPT_TEMPLATE` in +`document_processing.py` are the templates for the prompts sent to the Gemini model. +You can customize it to improve extraction accuracy for your specific use case. + +```python +EXTRACT_PROMPT_TEMPLATE = """\ + Based solely on this {document_name}, extract the following fields. + If the information is missing, write "missing" next to the field. + Output as JSON. + + Fields:\n + {fields} +""" +``` + +## Evaluation + +You can assess the quality and accuracy of the document classification model using the Vertex AI Evaluation Service. The `evaluate.py` script computes the `exact_match` metric comparing the model's prediction against a ground-truth dataset. The evaluation dataset is a subset of images from the RVL-CDIP dataset converted to png. + +We also provide a Python notebook `evaluate.ipynb` for interactive experimentation and visualization of the evaluation process. + +To run the evaluation script: + +```bash +python evaluate.py +``` + +If you configured `EVAL_DEST` in your `.env` file, the evaluation results and metrics will be persisted to your GCS bucket and viewable in the Google Cloud Console under the Vertex AI Experiments page. + +## Further development + +We are planning to add the following functionalities to this project: + +- Support for Gemini Batch API to enable large-scale asynchronous processing of + document extraction tasks. This will allow users to submit a batch of documents and + receive the results when they are ready, which is ideal for high-volume, offline + workflows. + +Future architecture: +![Future Architecture](./images/future_architecture.png) + +## Dataset Attribution + +This project utilizes the **RVL-CDIP** (Ryerson Vision Lab Complex Document Information Processing) dataset. + +If you use this project or build upon it, please ensure you appropriately credit the dataset creators and the original document sources: + +* **RVL-CDIP Dataset:** Created by Adam W. Harley, Alex Ufkes, and Konstantinos G. Derpanis. +* **Parent Collection:** The dataset is a subset of the IIT-CDIP Test Collection 1.0. +* **Original Source:** The documents were originally collected and are maintained by the [UCSF Industry Documents Library](https://www.industrydocuments.ucsf.edu/) (formerly the Legacy Tobacco Document Library). + +### Citation + +If you are using this code for academic or research purposes, please cite the original RVL-CDIP paper: + +```bibtex + @inproceedings{harley2015icdar, + title = {Evaluation of Deep Convolutional Nets for Document Image Classification and Retrieval}, + author = {Adam W. Harley and Alex Ufkes and Konstantinos G. Derpanis}, + booktitle = {International Conference on Document Analysis and Recognition (ICDAR)}, + year = {2015}, + pages = {991--995} +} +``` + +## Authors + +- **Author:** [Ariel Jassan](https://github.com/arieljassan) +- **Contributor:** [Ben Mizrahi](https://github.com/benmizrahi) + +## Disclaimer + +This repository itself is not an officially supported Google product. The code in this +repository is for demonstrative purposes only. diff --git a/gemini/use-cases/entity-extraction/config.json b/gemini/use-cases/entity-extraction/config.json new file mode 100644 index 0000000..4527870 --- /dev/null +++ b/gemini/use-cases/entity-extraction/config.json @@ -0,0 +1,261 @@ +{ + "extraction_configs": { + "scientific_report": { + "document_name": "Research findings document", + "document_mime_type": "application/pdf", + "model": "gemini-2.5-flash", + "fields": { + "title": "Title of the report", + "authors": "List of authors", + "publication_date": "Date of publication", + "abstract_summary": "Summary of the abstract or executive summary", + "keywords": "Key topics or keywords mentioned" + } + }, + "resume": { + "document_name": "CV or work history", + "document_mime_type": "application/pdf", + "model": "gemini-2.5-flash", + "fields": { + "candidate_name": "Full name of the candidate", + "email": "Email address", + "phone": "Phone number", + "education": "Highest degree and university", + "skills": "List of technical or professional skills", + "last_job_title": "Most recent job title" + } + }, + "memo": { + "document_name": "Internal office brief", + "document_mime_type": "application/pdf", + "model": "gemini-2.5-flash", + "fields": { + "sender": "Name or department sending the memo", + "recipient": "Name or department receiving the memo", + "date": "Date of the memo", + "subject": "Subject line", + "action_items": "List of required actions or requests" + } + }, + "file_folder": { + "document_name": "Folder cover or label", + "document_mime_type": "application/pdf", + "model": "gemini-2.5-flash", + "fields": { + "label_text": "Text written on the folder tab or cover", + "department": "Department or category associated with the folder", + "year": "Year associated with the folder contents", + "color": "Visual color of the folder if distinguishable" + } + }, + "specification": { + "document_name": "Design requirements", + "document_mime_type": "application/pdf", + "model": "gemini-2.5-flash", + "fields": { + "project_name": "Name of the project or product", + "spec_id": "Specification ID or document number", + "version": "Version number", + "requirements_summary": "Summary of main technical requirements", + "status": "Status (e.g., Draft, Approved, Deprecated)" + } + }, + "news_article": { + "document_name": "News report", + "document_mime_type": "application/pdf", + "model": "gemini-2.5-flash", + "fields": { + "headline": "Main headline of the article", + "publication_source": "Name of the newspaper or website", + "date": "Date of publication", + "author": "Name of the journalist or author", + "topic": "Main topic or category (e.g., Politics, Sports, Tech)" + } + }, + "letter": { + "document_name": "Written correspondence", + "document_mime_type": "application/pdf", + "model": "gemini-2.5-flash", + "fields": { + "sender_name": "Name of the sender", + "sender_address": "Address of the sender", + "recipient_name": "Name of the recipient", + "recipient_address": "Address of the recipient", + "date": "Date of the letter", + "subject": "Subject or reference line" + } + }, + "form": { + "document_name": "Fill-in-the-blank document", + "document_mime_type": "application/pdf", + "model": "gemini-2.5-flash", + "fields": { + "form_title": "Title or header of the form", + "form_id": "Form ID number", + "person_name": "Name of the person filling the form", + "submission_date": "Date the form was filled or signed", + "is_signed": "Boolean indicating if a signature is present" + } + }, + "budget": { + "document_name": "Financial plan", + "document_mime_type": "application/pdf", + "model": "gemini-2.5-flash", + "fields": { + "period_start": "Start date of the budget period", + "period_end": "End date of the budget period", + "total_revenue": "Total projected revenue", + "total_expenses": "Total projected expenses", + "department": "Department or project the budget belongs to" + } + }, + "handwritten": { + "document_name": "Non-typed text", + "document_mime_type": "application/pdf", + "model": "gemini-2.5-flash", + "fields": { + "transcription": "Full text transcription of the handwriting", + "language": "Language of the text", + "sentiment": "Sentiment of the content (Positive, Negative, Neutral)", + "writer_name": "Name of the writer if identifiable" + } + }, + "email": { + "document_name": "Digital message", + "document_mime_type": "application/pdf", + "model": "gemini-2.5-flash", + "fields": { + "from": "Sender email address", + "to": "Recipient email address", + "cc": "CC email addresses", + "date": "Date and time sent", + "subject": "Subject line", + "body_summary": "Summary of the email body content" + } + }, + "invoice": { + "document_name": "Request for payment", + "document_mime_type": "application/pdf", + "model": "gemini-2.5-flash", + "fields": { + "invoice_number": "Unique invoice identifier", + "vendor_name": "Name of the vendor or seller", + "invoice_date": "Date of issue", + "total_amount": "Total amount due", + "currency": "Currency code (e.g., USD, EUR)", + "due_date": "Payment due date" + } + }, + "presentation": { + "document_name": "Slide deck", + "document_mime_type": "application/pdf", + "model": "gemini-2.5-flash", + "fields": { + "title": "Title of the presentation", + "presenter": "Name of the presenter or author", + "slide_count": "Total number of slides", + "event_name": "Name of the event or meeting context", + "key_takeaways": "List of key points or summary" + } + }, + "scientific_publication": { + "document_name": "Scholarly article", + "document_mime_type": "application/pdf", + "model": "gemini-2.5-flash", + "fields": { + "article_title": "Title of the article", + "journal_name": "Name of the academic journal", + "volume_issue": "Volume and issue number", + "authors": "List of authors", + "doi": "Digital Object Identifier (DOI)", + "abstract": "Full text of the abstract" + } + }, + "questionnaire": { + "document_name": "Survey instrument", + "document_mime_type": "application/pdf", + "model": "gemini-2.5-flash", + "fields": { + "survey_title": "Title of the questionnaire", + "target_audience": "Intended audience description", + "number_of_questions": "Count of questions", + "response_type": "Type of responses (e.g., Multiple Choice, Open text)", + "instructions": "Instructions for the respondent" + } + }, + "advertisement": { + "document_name": "Promotional content", + "document_mime_type": "application/pdf", + "model": "gemini-2.5-flash", + "fields": { + "brand_name": "Name of the brand", + "product_name": "Name of the product or service", + "headline": "Main promotional headline", + "offer_details": "Details of any discount or offer", + "call_to_action": "The primary action requested (e.g., Buy Now, Visit Website)", + "contact_info": "Phone number or website listed" + } + }, + "form_10_k": { + "document_name": "Annual report of a company", + "document_mime_type": "application/pdf", + "model": "gemini-2.5-flash", + "fields": { + "year": "Year of the report", + "company_name": "Name of the company", + "ceo": "Name of the CEO of the company", + "net_income_millions": "Net income in million dollars" + } + }, + "form_10_q": { + "document_name": "Quarter report of a company", + "document_mime_type": "application/pdf", + "model": "gemini-2.5-flash", + "fields": { + "year": "Year of the report", + "quarter": "Quarter of the report (Q1, Q2, Q3, Q4)", + "company_name": "Name of the company", + "ceo": "Name of the CEO of the company", + "net_income_millions": "Net income in million dollars", + "net_income_previous_year_millions": "Net income of previous year in million dollars" + } + }, + "other": { + "document_name": "Unclassified Document", + "document_mime_type": "application/pdf", + "model": "gemini-2.5-flash", + "fields": { + "summary": "Brief summary of the document content", + "suggested_category": "A suggested category if none of the above fit", + "language": "Dominant language of the document", + "entity_names": "List of major entities (people, orgs) found" + } + } + }, + + "classification_config": { + "document_mime_type": "image/png", + "model": "gemini-2.5-flash", + "classes": { + "handwritten": "Document entirely handwritten. Process this before other classes", + "form_10_k": "Annual report of a company", + "form_10_q": "Quarter report of a company", + "resume": "CV or work history", + "file_folder": "Folder cover or label", + "budget": "Financial forecasts, spreadsheets, or internal expenditure authorizations. IF the document is a fill-in-the-blank form requesting or authorizing money (e.g., 'Contribution Request', 'Account Code', 'Amount: $'), classify as budget. EXCLUDE: Do NOT classify as invoice unless it is a final bill from an external vendor.", + "invoice": "Definitive demand for payment for goods/services already delivered. Look for 'Remit To', 'Net 30', 'Terms', 'Tax ID', or 'Please Pay'. If it is an external bill, it is an invoice, even if it has form-like boxes. EXCLUDE: Do NOT classify as budget.", + "scientific_publication": "Scholarly article", + "scientific_report": "Research findings document", + "specification": "Detailed technical, engineering, or architectural requirements. Look for section numbers (e.g., 1.1, 1.2), blueprints, materials lists, or strict technical constraints. IF it is a technical checklist or engineering form, classify as specification, NOT form.", + "questionnaire": "Survey instrument", + "email": "Digital message", + "memo": "Internal office correspondence. Look for a strict, block-style header at the top containing 'TO:', 'FROM:', 'DATE:', and 'SUBJECT:' (or 'RE:'). EXCLUDE: If the document starts with a conversational salutation like 'Dear [Name]', it is NOT a memo. Classify as letter instead.", + "presentation": "Official corporate communications or media press releases", + "news_article": "Clipping from a newspaper, magazine, or journal", + "advertisement": "Promotional content", + "letter": "Typed correspondence, usually external. Visually characterized by formal salutations ('Dear...', 'To Whom It May Concern') and sign-offs at the bottom ('Sincerely', 'Cordially', 'Best Regards'). EXCLUDE: If the document uses a rigid 'TO / FROM / SUBJECT' block header, classify as memo instead.", + "form": "Administrative data collection documents featuring blank lines, checkboxes, or fax cover sheets. EXTREME EXCLUSION: If the document contains prices, 'Account Code', authorizes spending, or contains technical engineering data, do NOT classify as form. Classify as budget, invoice, or specification instead.", + "other": "None of the above" + } + } +} diff --git a/gemini/use-cases/entity-extraction/deploy.sh b/gemini/use-cases/entity-extraction/deploy.sh new file mode 100644 index 0000000..f57759d --- /dev/null +++ b/gemini/use-cases/entity-extraction/deploy.sh @@ -0,0 +1,47 @@ +#!/bin/bash + +# Copyright 2025 Google LLC +# +# 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 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# +# Builds and deploys the entity extraction service to Cloud Run. +# + +# Exit immediately if a command fails, treat unset variables as an error, and cause +# pipelines to fail if any command within them fails. +set -euo pipefail + +# Load environment variables from .env file. +if [ -f .env ]; then + echo "Loading environment variables from .env" + set -o allexport + source .env + set +o allexport +else + echo ".env file not found. Please create one." + exit 1 +fi + +# Enable APIs. +gcloud services enable run.googleapis.com +gcloud services enable artifactregistry.googleapis.com + +# Submit build to Artifact Registry. +gcloud builds submit --tag gcr.io/$CLOUD_RUN_PROJECT_ID/$SERVICE_NAME + +# Deploy service. +gcloud run deploy $SERVICE_NAME \ + --image gcr.io/$CLOUD_RUN_PROJECT_ID/$SERVICE_NAME \ + --platform managed \ + --region $CLOUD_RUN_REGION diff --git a/gemini/use-cases/entity-extraction/document_processing.py b/gemini/use-cases/entity-extraction/document_processing.py new file mode 100644 index 0000000..cd7a52b --- /dev/null +++ b/gemini/use-cases/entity-extraction/document_processing.py @@ -0,0 +1,141 @@ +# Copyright 2025 Google LLC +# +# 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 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Main logic for classification and entity extraction.""" + +import json +import os + +import dotenv +from google import genai +from google.genai import types + +import utils +import document_sanitizer + +EXTRACT_PROMPT_TEMPLATE = """\ + Based solely on this {document_name}, extract the following fields. + If the information is missing, write "missing" next to the field. + Output as JSON. + + Fields:\n + {fields} +""" + +CLASSIFY_PROMPT_TEMPLATE = """\ + Analyze the intent, visual layout, text content, and structural elements of the document. + Classify it into exactly one of the following classes based on its distinguishing features. + Output as JSON in the following format: + + "reasoning": "Brief explanation of the key visual cues and keywords found that justify the class", + "class": "class_name" + + + Classes:\n + {classes} +""" + +# Load environment variables. +dotenv.load_dotenv() +project_id = os.environ.get("GEMINI_PROJECT_ID") +if not project_id: + raise ValueError("GEMINI_PROJECT_ID environment variable must be set.") +location = os.environ.get("GEMINI_LOCATION", "global") +config_path = os.environ.get("CONFIG_PATH", "config.json") + +# Initialize Gemini client. +client = genai.Client(vertexai=True, project=project_id, location=location) +CONFIGS = utils.load_app_config(config_path) + +def extract_from_document(extract_config_id: str, document_uri: str) -> str: + """Extract entities from a document.""" + extract_config = CONFIGS["extraction_configs"][extract_config_id] + + prompt = EXTRACT_PROMPT_TEMPLATE.format( + document_name=extract_config["document_name"], + fields=json.dumps(extract_config["fields"], indent=4), + ) + + response = client.models.generate_content( + model=extract_config["model"], + contents=[ + types.Part.from_uri( + file_uri=document_uri, + mime_type=extract_config["document_mime_type"], + ), + prompt, + ], + config={ + "response_mime_type": "application/json", + }, + ) + return response.text + +def classify_document(document_uri: str) -> str: + """Classify a document.""" + classification_config = CONFIGS["classification_config"] + + prompt = CLASSIFY_PROMPT_TEMPLATE.format( + classes=json.dumps(classification_config["classes"], indent=4), + ) + + response = client.models.generate_content( + model=classification_config["model"], + contents=[ + types.Part.from_uri( + file_uri=document_uri, + mime_type=classification_config["document_mime_type"], + ), + prompt, + ], + config={ + "response_mime_type": "application/json", + }, + ) + return response.text + +def classify_and_extract_document(document_uri: str) -> str: + """Classify a document and extract entities from it.""" + classification_response = classify_document(document_uri) + classification_result = json.loads(classification_response) + document_class = classification_result.get("class") + if not document_class or document_class not in CONFIGS["extraction_configs"]: + raise ValueError("Document classification failed.") + + return extract_from_document(document_class, document_uri) + +def evaluate_quality_and_extract(extract_config_id: str, document_uri: str): + image_quality = document_sanitizer.evaluate_document_quality( + document_uri=document_uri + ) + print(f"image_quality: {image_quality}") + + if image_quality == "good": + data = ( + extract_from_document( + extract_config_id=extract_config_id, + document_uri=document_uri + ) + ) + + if image_quality == "bad": + # TODO: Process multiple pages if needed, not only the first one. + enhanced_document_path = document_sanitizer.preprocess_file(document_uri)[0] + data = ( + document_sanitizer.extract_data_from_low_quality_document( + extract_config_id=extract_config_id, + document_path=enhanced_document_path + ) + ) + return data diff --git a/gemini/use-cases/entity-extraction/document_processing_test.py b/gemini/use-cases/entity-extraction/document_processing_test.py new file mode 100644 index 0000000..ec26771 --- /dev/null +++ b/gemini/use-cases/entity-extraction/document_processing_test.py @@ -0,0 +1,146 @@ +# Copyright 2025 Google LLC +# +# 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 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Tests for entity extraction.""" + +import json + +import document_processing + +FORM_10_Q_URI = "gs://cloud-samples-data/gen-app-builder/search/alphabet-investor-pdfs/2021Q1_alphabet_earnings_release.pdf" +FORM_10_K_URI = "gs://cloud-samples-data/gen-app-builder/search/alphabet-investor-pdfs/2021_alphabet_annual_report.pdf" +FORM_10_Q_DIRTY_URI = "gs://arielj-argolis-1-docs/alphabet/augmented_output_3.pdf" + +def test_extract_from_document_10_q() -> None: + extract_config_id = "form_10_q" + document_uri = FORM_10_Q_URI + expected_response = """\ + { + "year": "2021", + "quarter": "Q1", + "company_name": "Alphabet Inc.", + "ceo": "Sundar Pichai", + "net_income_millions": "17930", + "net_income_previous_year_millions": "6836" + } + """ + + response = document_processing.extract_from_document( + extract_config_id=extract_config_id, document_uri=document_uri + ) + print(response) + assert json.loads(response) == json.loads(expected_response) + +def test_extract_from_document_10_k() -> None: + extract_config_id = "form_10_k" + document_uri = FORM_10_K_URI + expected_response = """\ + { + "year": "2021", + "company_name": "Alphabet Inc.", + "ceo": "Sundar Pichai", + "net_income_millions": "76033" + } + """ + + response = document_processing.extract_from_document( + extract_config_id=extract_config_id, document_uri=document_uri + ) + print(response) + assert json.loads(response) == json.loads(expected_response) + +def test_classify_document() -> None: + document_uri = FORM_10_Q_URI + expected_response = """\ + { + "class": "form_10_q" + } + """ + + response = document_processing.classify_document(document_uri=document_uri) + print(response) + assert json.loads(response) == json.loads(expected_response) + +def test_classify_and_extract_document() -> None: + document_uri = FORM_10_Q_URI + expected_response = """\ + { + "year": "2021", + "quarter": "Q1", + "company_name": "Alphabet Inc.", + "ceo": "Sundar Pichai", + "net_income_millions": "17930", + "net_income_previous_year_millions": "6836" + } + """ + + response = ( + document_processing.classify_and_extract_document(document_uri=document_uri) + ) + print(response) + assert json.loads(response) == json.loads(expected_response) + +def test_evaluate_quality_and_extract_high_quality_document() -> None: + document_uri = FORM_10_Q_URI + extract_config_id = "form_10_q" + expected_response = """\ + { + "year": "2021", + "quarter": "Q1", + "company_name": "Alphabet Inc.", + "ceo": "Sundar Pichai", + "net_income_millions": "17930", + "net_income_previous_year_millions": "6836" + } + """ + response = ( + document_processing.evaluate_quality_and_extract( + extract_config_id=extract_config_id, + document_uri=document_uri + ) + ) + print(response) + assert json.loads(response) == json.loads(expected_response) + +def test_evaluate_quality_and_extract_low_quality_document() -> None: + document_uri = FORM_10_Q_DIRTY_URI + extract_config_id = "form_10_q" + expected_response = """\ + { + "year": "2021", + "quarter": "Q1", + "company_name": "Alphabet Inc.", + "ceo": "Sundar Pichai", + "net_income_millions": 17930, + "net_income_previous_year_millions": 6836 + } + """ + response = ( + document_processing.evaluate_quality_and_extract( + extract_config_id=extract_config_id, + document_uri=document_uri + ) + ) + print("response from model") + print(response) + assert json.loads(response) == json.loads(expected_response) + +if __name__ == "__main__": + test_extract_from_document_10_q() + test_extract_from_document_10_k() + test_classify_document() + test_classify_and_extract_document() + test_evaluate_quality_and_extract_high_quality_document() + test_evaluate_quality_and_extract_low_quality_document() + print("All tests passed!") diff --git a/gemini/use-cases/entity-extraction/evaluate.ipynb b/gemini/use-cases/entity-extraction/evaluate.ipynb new file mode 100644 index 0000000..8662128 --- /dev/null +++ b/gemini/use-cases/entity-extraction/evaluate.ipynb @@ -0,0 +1,919 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "id": "948123f6", + "metadata": {}, + "outputs": [], + "source": [ + "# Copyright 2025 Google LLC\n", + "#\n", + "# Licensed under the Apache License, Version 2.0 (the \"License\");\n", + "# you may not use this file except in compliance with the License.\n", + "# You may obtain a copy of the License at\n", + "#\n", + "# http://www.apache.org/licenses/LICENSE-2.0\n", + "#\n", + "# Unless required by applicable law or agreed to in writing, software\n", + "# distributed under the License is distributed on an \"AS IS\" BASIS,\n", + "# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n", + "# See the License for the specific language governing permissions and\n", + "# limitations under the License." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b784874a", + "metadata": {}, + "outputs": [], + "source": [ + "import dotenv\n", + "import os\n", + "\n", + "import matplotlib.pyplot as plt\n", + "import pandas as pd\n", + "import seaborn as sns\n", + "\n", + "import document_processing\n", + "import evaluate\n", + "\n", + "# Reload modules to reflect recent edits.\n", + "import importlib\n", + "importlib.reload(document_processing)\n", + "importlib.reload(evaluate)\n", + "\n", + "\n", + "# Load environment variables.\n", + "dotenv.load_dotenv(dotenv_path=\".env\", override=True)\n", + "PROJECT_ID = os.environ.get(\"GEMINI_PROJECT_ID\")\n", + "if not PROJECT_ID:\n", + " raise ValueError(\"GEMINI_PROJECT_ID environment variable must be set.\")\n", + "LOCATION = os.environ.get(\"GEMINI_LOCATION\", \"global\")\n", + "IMAGE_PATHS = os.environ.get(\"IMAGE_PATHS\", \"\")\n", + "IMAGE_PREFIX = os.environ.get(\"IMAGE_PREFIX\", \"\")\n", + "\n", + "# Set evaluation parameters.\n", + "EVAL_MODEL = \"gemini-2.5-flash\"\n", + "RANDOM_STATE = 42\n" + ] + }, + { + "cell_type": "code", + "execution_count": 48, + "id": "a4468715", + "metadata": {}, + "outputs": [], + "source": [ + "def plot_confusion_matrix(df, title='Confusion Matrix'):\n", + " # Create a confusion matrix that is ordered by the average exact match score\n", + " # for each class.\n", + "\n", + " # Extract the ordered list of classes.\n", + " avg_df = df.groupby('reference')['exact_match'].mean().reset_index()\n", + " avg_df = avg_df.sort_values(by='exact_match', ascending=True)\n", + "\n", + " ordered_classes = avg_df['reference'].tolist()\n", + " extra_preds = [cls for cls in df['response'].unique() if cls not in ordered_classes]\n", + " full_order = ordered_classes + extra_preds\n", + "\n", + " # Create and order the confusion matrix.\n", + " cm = pd.crosstab(df['reference'], df['response'])\n", + " cm_ordered = cm.reindex(index=full_order, columns=full_order, fill_value=0)\n", + "\n", + " # Plot the confusion matrix.\n", + " plt.figure(figsize=(10, 8))\n", + " ax = sns.heatmap(cm_ordered, annot=True, fmt='d', cmap='Blues')\n", + "\n", + " ax.xaxis.tick_top()\n", + " ax.xaxis.set_label_position('top')\n", + " plt.xticks(rotation=90)\n", + " plt.title(title, pad=20)\n", + " plt.ylabel('Actual (Reference)')\n", + " plt.xlabel('Predicted (Response)')\n", + "\n", + " plt.show() " + ] + }, + { + "cell_type": "code", + "execution_count": 67, + "id": "ee614cc6", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + " Analyze the intent, visual layout, text content, and structural elements of the document.\n", + " Classify it into exactly one of the following classes based on its distinguishing features.\n", + " Output as JSON in the following format:\n", + " \n", + " \"reasoning\": \"Brief explanation of the key visual cues and keywords found that justify the class\",\n", + " \"class\": \"class_name\"\n", + " \n", + "\n", + " Classes:\n", + "\n", + " {\n", + " \"handwritten\": \"Document entirely handwritten. Process this before other classes\",\n", + " \"form_10_k\": \"Annual report of a company\",\n", + " \"form_10_q\": \"Quarter report of a company\",\n", + " \"resume\": \"CV or work history\",\n", + " \"file_folder\": \"Folder cover or label\",\n", + " \"budget\": \"Financial forecasts, spreadsheets, or internal expenditure authorizations. IF the document is a fill-in-the-blank form requesting or authorizing money (e.g., 'Contribution Request', 'Account Code', 'Amount: $'), classify as budget. EXCLUDE: Do NOT classify as invoice unless it is a final bill from an external vendor.\",\n", + " \"invoice\": \"Definitive demand for payment for goods/services already delivered. Look for 'Remit To', 'Net 30', 'Terms', 'Tax ID', or 'Please Pay'. If it is an external bill, it is an invoice, even if it has form-like boxes. EXCLUDE: Do NOT classify as budget.\",\n", + " \"scientific_publication\": \"Scholarly article\",\n", + " \"scientific_report\": \"Research findings document\",\n", + " \"specification\": \"Detailed technical, engineering, or architectural requirements. Look for section numbers (e.g., 1.1, 1.2), blueprints, materials lists, or strict technical constraints. IF it is a technical checklist or engineering form, classify as specification, NOT form.\",\n", + " \"questionnaire\": \"Survey instrument\",\n", + " \"email\": \"Digital message\",\n", + " \"memo\": \"Internal office correspondence. Look for a strict, block-style header at the top containing 'TO:', 'FROM:', 'DATE:', and 'SUBJECT:' (or 'RE:'). EXCLUDE: If the document starts with a conversational salutation like 'Dear [Name]', it is NOT a memo. Classify as letter instead.\",\n", + " \"presentation\": \"Official corporate communications or media press releases\",\n", + " \"news_article\": \"Clipping from a newspaper, magazine, or journal\",\n", + " \"advertisement\": \"Promotional content\",\n", + " \"letter\": \"Typed correspondence, usually external. Visually characterized by formal salutations ('Dear...', 'To Whom It May Concern') and sign-offs at the bottom ('Sincerely', 'Cordially', 'Best Regards'). EXCLUDE: If the document uses a rigid 'TO / FROM / SUBJECT' block header, classify as memo instead.\",\n", + " \"form\": \"Administrative data collection documents featuring blank lines, checkboxes, or fax cover sheets. EXTREME EXCLUSION: If the document contains prices, 'Account Code', authorizes spending, or contains technical engineering data, do NOT classify as form. Classify as budget, invoice, or specification instead.\",\n", + " \"other\": \"None of the above\"\n", + "}\n", + "\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/Users/arieljassan/Documents/projects/generative-ai/gemini/use-cases/entity-extraction/evaluate.py:103: FutureWarning: DataFrameGroupBy.apply operated on the grouping columns. This behavior is deprecated, and in a future version of pandas the grouping columns will be excluded from the operation. Either pass `include_groups=False` to exclude the groupings or explicitly select the grouping columns after groupby to silence this warning.\n", + " df = df.groupby(\"reference\", group_keys=False).apply(\n", + "Gemini Inference: 100%|██████████| 120/120 [00:24<00:00, 4.88it/s]\n", + "Computing Metrics for Evaluation Dataset: 100%|██████████| 120/120 [00:04<00:00, 24.97it/s]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[AggregatedMetricResult(\n", + " mean_score=0.7916666666666666,\n", + " metric_name='exact_match',\n", + " num_cases_error=0,\n", + " num_cases_total=120,\n", + " num_cases_valid=120,\n", + " stdev_score=0.407819232762071\n", + ")]\n" + ] + }, + { + "data": { + "image/png": "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44k1jvg+TPs2TlTg5A1OhGDPRDy3UUHH8xXjYFTxsVcoqpR47hhbxqAajOcOxh3bY6DqHJ7bIiKKNJHW95SIiEyD9vdt27a1ZcmSxRYnThxbwoQJ1TYBU6ZMUdsqGIKCgtT2C1mzZrXFjh3bljFjRtuAAQOCfU9oWxu42kIhtK0s4LfffrPly5dPxePj46O2RHDeLmDz5s1qK4506dKp78P/jRs3DtbO39VWFvDHH3+o3zFevHiqtX/NmjVtJ0+eDPY9xv05b5WB28L1uO3wbmURmtC2ssCWH2nTplXxIc7du3e73IJizZo1tjx58qhtHBx/T3xf3rx5Xd6n4+1gSwk8XoULF1aPr6MePXqo7T1w32EJ7fG+ffu2rWXLlrYUKVKoxyd//vwhHoewngPODh48qL538ODBoX7P5cuX1fcg9rfZysJxO4rx48fbihYtqrbgwO+RI0cOW5cuXWznz5+3f5+r+1m7dq2tQIECNm9vb/XaGjNmjG3OnDnBnjt///23es5mypRJbVWCLVawTceBAwfst7Ny5UpbpUqV1Ndw//jedu3a2W7duhXs/v7991/1esyePbv6Pox7iRIlVPzYRiQit0VEFBm88E/kpZpERERERETkjrjmkIiIiIiIiJgcEhEREREREZNDIiIiIiIiYnJIRERERERETA6JiIiIiIhIYUMaIiIiIiIiYnJIRERERERETA6JiIiIiIiIySERERERERExOSQiIiIiIiKFDWmIiIiIiIiIySERERERERExOSQiIiIiIiImh0RERERERMTkkIiIiIiIiBQ2pCEiIiIiIiImh0RERERERMTkkIiIiIiIiJgcEhEREREREZNDIiIiIiIiUtiQhoiIiIiIiJgcEhEREREREZNDIiIiIiIiYnJIRERERERETA6JiIiIiIhIYUMaIiIiIiIiYnJIRERERERETA6JiMjDtWjRQmrXrm3/vGzZstK9e3fT4/jzzz/Fy8tLHj16FOb3bd68WXLnzi0vX740LTZ3cO/ePUmVKpVcv37d6lCIiNwWp5USEZGWCRsSJVzixIkj2bNnl6+++kpevHgR5fe9atUqGT58eKQmdJGpb9++MmjQIIkZM6b6fN68efaxihEjhqRNm1Y++eQTuXr1qniSFClSSLNmzWTIkCFWh0JE5LaYHBIRkZaqVKkit27dknPnzkmvXr1k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XLmyBAQESMGCBdV1R44cUYnipk2bJG/evHyEiIiITMDkkIiItJhaumjRIjl9+rT6PHfu3NKkSROJFy+e1aERERF5DCaHREREREREJLE4BkREZLa1a9dK1apVJXbs2OrjsHz88cemxUVEROTJWDkkIiLTxYgRQ/755x/VkRQfh8bLy0tevnxpamxERESeiskhERERERERcZ9DIiKyFrawQKdSZ4GBgeprREREZA5WDomIyFIxY8aUW7duqSmmju7fv6+u47RSIiIic4S+0IOIiMgENptNrS10dv36dUmcODEfAyIiIpOwWykREVnC19dXJYW4lC9fXmLF+t+fJFQLL126JFWqVOGjQ0REZBImh0REZInatWur/w8fPiyVK1eWBAkS2L8WJ04cyZIli9SrV4+PDhERkUm45pCIiCw1f/58+eSTT8Tb25uPBBERkYWYHBIRERERERGnlRIRkbWwvnDixImyfPlyuXr1qtrCwtGDBw8si42IiMiTsFspERFZatiwYfLNN9+oqaWPHz+Wnj17St26dSVGjBgydOhQPjpEREQm4bRSIiKyVLZs2WTy5MlSvXp1SZgwoWpQY1y3Z88eWbx4MR8hIiIiE7BySERElvrnn38kf/786mN0LEX1EGrUqCG//vorHx0iIiKTMDkkIiJLZciQQW7duqU+RsXwt99+Ux/v379f4saNy0eHiIjIJEwOiYjIUnXq1JHNmzerj7t06SKDBw+WHDlySLNmzaRVq1Z8dIiIiEzCNYdERKQVrDPctWuXShBr1qxpdThEREQeg8khEVEUihkzppoymSpVqmDX379/X12HbRyIiIiIdMBppUREUchms7m8PiAgQOLEicOxF5HRo0fLnDlzQowFrhszZgzHiIiIyCSxzLojIiJPgm0YwMvLS2bNmqW6cBpQLdy+fbvkypXLwgj1MWPGDJfbVeTNm1caNWok/fr1syQuIiIiT8PkkIgoCkycONFeOZw+fbqaXmpAxTBLlizqevpvK4u0adOGGIqUKVPau5gSERFR1GNySEQUBS5duqT+/+ijj+Tnn3+WJEmScJxDkTFjRtm5c6dkzZo12PW4Ll26dBw3IiIikzA5JCKKIkFBQXL16lVV/WJyGLq2bdtK9+7d1XiVK1dOXYetLfr27Su9evXi85OIiMgkTA6JiKJI7Nixxd/fn+P7Gn369FHd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K88ePZC478SXlBmzSsO+oyVr/vfEClvW/9ey+uv+/2v6A627D1atya1SpWo1efjggUz9brLcu3dXfHLllqkzZklyi6a76BaPjjHpFo+OMekWj44x6RaPjjHpFo+Of0d0GyPd4tExprOnT0i/Lm3sn8+c8l9xoELVj6X3oOES7Xh5zmRLL5tZR9lE/w+dSqdPny7Xrl0zZUzm7L+q3djnTmrd1hOu+GZJYnUIREQUTocu/2+fYB3wb4h7uvXI2llHzrKm+F+DQ93EK/GFZfftt2uUqffHyiFFualTp6qOpWi5vHPnTrWtheOUUSIiIiIish6TQ4pyWEM4YsQIefDggWTKlElNMUXjGCIiIiIi7Xn9b9u36I7JIUW5iRMnqgsREREREemLySEREREREVFoPKghjef8pkRERERERBQqVg6JiIiIiIhC40FrDlk5JCIiIiIiIiaHRERERERExGmlREREREREoWNDGiIiIiIiIvIkbEhDREREREQUGlYOiYiIiIiIyJOwWykRERERERFxWilFf7mTJhLdDFx/SnSyvmNxq0MgIqJw8s2ShGNFby1tEm+OYnjF4D6HRERERERE5EHYkIaIiIiIiCg0bEhDREREREREnoSVQyIiIiIiotB4cc0hEREREREReRBuZUFEREREREScVkpERERERBQqNqQhIiIiIiIiT8KGNERERERERKFhQxoiIiIiIiLyJGxI48HKli0r3bt3N+3+5s2bJ0mSJDHt/oiIiIiIKPyYHHqwVatWyfDhw027v08++UTOnj0r7mTd8nkyrHsLaV//I+nyaRWZNLyP3Lp+xdKYmn+QQbZ0LR7sMu+zQmK1pYsXSdWK5aSob35p0qiBHDt6lPFwjPg84muN70cavj/qGBPj4Rhp35DGy6KLyZgcerBkyZJJwoQJTbu/ePHiSapUqcSdnD52SMpVry+DJ8yWPiMmy8sXL2T8oK4S4O9naVyX7j+XerMO2C9dVx63NJ6NG9bL+LGjpV3HTrJ0xc/i45NLOrRrLffv32c8HCM+j/ha4/uRRu+PfM92v/HRMSbd4qHIw+TQgzlOK82SJYuMGjVKWrVqpRLGTJkyycyZM+3fW6JECenXr1+wn797967Ejh1btm/frj5/+PChNGvWTJImTSrvvPOOVK1aVc6dOxfmtNJffvlFihYtKt7e3pIiRQqpU6eO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IMr8WP+HSuWbQudQ3D4SOEwRwPo/4/7atWunqp+ffPKJqogiXnwfps0a2rZtq7qvFilSRE2N3bp1q33bDEP69Oll/fr1Km4koNiqwkiSdVOlajV5+OCBTP1usty7d1d8cuWWqTNmSXKLpk7oFo+OMekWz9nTJ6Rflzb2z2dOGa/+r1D1Y+k9aLjp8aRIEEcGVc4hieLFksd+QXLs5r/Sefkxeexn7XYWuj1uusWjY0y6xaNjTLrFo2NMur1H6jY+OsakWzxRzcuDGtJ42Rzn9ZGpsO/go0ePZPXq1Rz5KKTB9m0UDdx65C+6ab1Yr5bh6zv+11WZiDyLju+PaS1cQ01vxlvjktUHo10vtzLD3gGhN4mMCho/DERERERERNby8pzCIbuVEhERERERESuHlpo3bx6fg0REREREGvPyoNIh9zkkIiIiIiIiJodERERERETEaaVERERERESh8qBZpawcEhERERERESuHREREREREoWJDGiIiIiIiIvIo7FZKREREREREEotjQERERERE5Bob0hAREREREZFHYeWQiIiIiIgoFJ7UkIbJIRGRm1rfsbjoJHefX0Unp8ZVF93ceuQvOkmbxNvqECga4POIKPpgckhERERERBQKT6ocslspERERERERMTkkIiIiIiIiTislIiIiIiIKlQfNKmXlkIiIiIiIiFg5JCIiIiIiChUb0hAREREREZFHYbdSIiIiIiIiYnJIREREREQUVkMaqy7hNXToUDX91fGSK1cuiahYEf4JIiIiIiIi0krevHnljz/+sH8eK1bEUz0mh0RERERERG7ekAbJYJo0aTxvzSEeoNWrV0f5/cybN0+SJEkS7LqZM2dKxowZJUaMGPLtt9+qEm6hQoVEF65iduYcc4sWLaR27drR5nGLCksXL5KqFctJUd/80qRRAzl29Cjj4RiF27HDB2VI3y7y6ccVpErJgrJr+xZLnz+6Pq9TJ44rE5sUkr9HVJRTY6rIhj6lJX/GxJbGpNMY8Xnkno+bjvHoGBPj4RjR2zt37pykS5dO3n33XWnSpIlcvXrVM5LDW7duSdWqVSP1NrNkyaKSPUeffPKJnD171v75kydPpHPnztKvXz+5ceOGfP7559K7d2/ZvHmzuLNJkyappDKyhJYwR8XjZoaNG9bL+LGjpV3HTrJ0xc/i45NLOrRrLffv32c8HKNw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", + "text/plain": [ + "
    " + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# Run evaluation for document classification task. It will run on the specified \n", + "# sample size and return the evaluation results and a DataFrame with detailed \n", + "# information about each evaluated sample.\n", + "importlib.reload(document_processing)\n", + "importlib.reload(evaluate)\n", + "\n", + "SAMPLE_SIZE = 120\n", + "\n", + "\n", + "result, df = (\n", + " evaluate.run_evaluation(\n", + " project_id=PROJECT_ID, \n", + " location=LOCATION, \n", + " csv_path=IMAGE_PATHS,\n", + " image_prefix=IMAGE_PREFIX,\n", + " eval_model=EVAL_MODEL,\n", + " sample_size=SAMPLE_SIZE,\n", + " random_state=RANDOM_STATE,\n", + " stratify=True,\n", + " )\n", + ")\n", + "\n", + "print(result.summary_metrics)\n", + "plot_confusion_matrix(df, title='Confusion Matrix for All Classes')" + ] + }, + { + "cell_type": "code", + "execution_count": 68, + "id": "dff50ab7", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + " Analyze the intent, visual layout, text content, and structural elements of the document.\n", + " Classify it into exactly one of the following classes based on its distinguishing features.\n", + " Output as JSON in the following format:\n", + " \n", + " \"reasoning\": \"Brief explanation of the key visual cues and keywords found that justify the class\",\n", + " \"class\": \"class_name\"\n", + " \n", + "\n", + " Classes:\n", + "\n", + " {\n", + " \"budget\": \"Financial forecasts, spreadsheets, or internal expenditure authorizations. IF the document is a fill-in-the-blank form requesting or authorizing money (e.g., 'Contribution Request', 'Account Code', 'Amount: $'), classify as budget. EXCLUDE: Do NOT classify as invoice unless it is a final bill from an external vendor.\",\n", + " \"invoice\": \"Definitive demand for payment for goods/services already delivered. Look for 'Remit To', 'Net 30', 'Terms', 'Tax ID', or 'Please Pay'. If it is an external bill, it is an invoice, even if it has form-like boxes. EXCLUDE: Do NOT classify as budget.\",\n", + " \"specification\": \"Detailed technical, engineering, or architectural requirements. Look for section numbers (e.g., 1.1, 1.2), blueprints, materials lists, or strict technical constraints. IF it is a technical checklist or engineering form, classify as specification, NOT form.\",\n", + " \"form\": \"Administrative data collection documents featuring blank lines, checkboxes, or fax cover sheets. EXTREME EXCLUSION: If the document contains prices, 'Account Code', authorizes spending, or contains technical engineering data, do NOT classify as form. Classify as budget, invoice, or specification instead.\"\n", + "}\n", + "\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/Users/arieljassan/Documents/projects/generative-ai/gemini/use-cases/entity-extraction/evaluate.py:103: FutureWarning: DataFrameGroupBy.apply operated on the grouping columns. This behavior is deprecated, and in a future version of pandas the grouping columns will be excluded from the operation. Either pass `include_groups=False` to exclude the groupings or explicitly select the grouping columns after groupby to silence this warning.\n", + " df = df.groupby(\"reference\", group_keys=False).apply(\n", + "Gemini Inference: 100%|██████████| 60/60 [00:22<00:00, 2.70it/s]\n", + "Computing Metrics for Evaluation Dataset: 100%|██████████| 60/60 [00:01<00:00, 39.18it/s]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[AggregatedMetricResult(\n", + " mean_score=0.8,\n", + " metric_name='exact_match',\n", + " num_cases_error=0,\n", + " num_cases_total=60,\n", + " num_cases_valid=60,\n", + " stdev_score=0.4033755872716886\n", + ")]\n" + ] + }, + { + "data": { + "image/png": "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NG3a1GSEmjVrZp6nAbfue/03oN+tfp8aKGhg8c8//9zxe9L9rt+R/rvQIUOBgYFmX9SuXVt+/fVXE3ArrfPRyQH0M+gxq8G7Pt/Vdxr/pFqDH+1fxowZ5W5oYK7HpQY9enJuH6qoP9evXx8XyOtxrtkM3d+addJjUAMd3T+6b3S4owYnepGhS5cu5rU0qNOskR4XGjQpzSBpkKP/9vTvjmYj9d+oBr/btm0zfzuS+loAIDYAd+XixYs2EbE1adIkSc/fvn27ef5LL73k1N6rVy/T/vPPP8e1FSxY0LStWbMmru3UqVO20NBQ25tvvhnXFhkZaZ733nvvOb1mu3btzGvEN2jQIPN8uw8++MDcP336tMt+299jxowZcW3ly5e35cyZ03b27Nm4th07dtgCAwNtbdu2TfB+L774otNrNmvWzJYtWzaX7+n4OdKnT29uP/3007Y6deqY2zExMbbcuXPbhgwZkug+uHHjhnlO/M+h+2/o0KFxbZs2bUrw2exq1qxpHps8eXKij+nmaPny5eb57777ru3gwYO2DBky2Jo2bWpLCv2unnzySae28ePHm9f74osv4tqio6Nt1apVM6996dKluM+lz8uUKZM5Ru4kNjY27rPlypXL1qZNG9vHH39sO3z4cILnduzY0ZYnTx7bmTNnnNqfeeYZW3h4uO3atWtOfXDcj/pdlSlTxnwXju9dvXp1W7FixeLaBg4caH538eLFifb1dt+TPq6vVa9evbjnKu1X4cKFbY8//nhcm34XadOmdfqce/bssQUFBTn9m0jM119/bZ6j/16SIrH9Yd9XjubOnZvg37nu19dff93la2/bts38zoIFC1w+59ChQ+ZzDR8+3Kl9165dtjRp0sS1J+W1AEAxJAm4S3qFVSX1iqMWRqqePXs6tduvKsevddCri/arqUqvjOpwIb1K6Sn22oevv/7ajHlPihMnTpjZavRKt+NVbM1S6NAJ++d0pFdNHenn0iun9n2YFDr0SIennDx50lxV1p+uCkz1CrteaVYxMTHmvezDrRyHdtyJvo5eEU4KvXqthbB6NVwzIjq8w50hL/HpftQrvpo9stOhMF27djVX5VevXu30fB1WosfIneiVbB1v/+6775rhUlobovU2mnlo3bp13LAvm81mMgE6A5De1qv+9k2vSuuQFVf7UoeE6XekV7d1iI399/R7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", + "text/plain": [ + "
    " + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# Run evaluation for a specified set of target classes. This will allow us to \n", + "# focus the evaluation on a subset of classes that are of particular interest.\n", + "importlib.reload(document_processing)\n", + "importlib.reload(evaluate)\n", + "\n", + "SAMPLE_SIZE = 60\n", + "\n", + "\n", + "target_classes = [\n", + " \"budget\", \n", + " \"specification\",\n", + " \"form\",\n", + " \"invoice\"\n", + "]\n", + "selected_result, selected_df = (\n", + " evaluate.run_evaluation(\n", + " project_id=PROJECT_ID, \n", + " location=LOCATION, \n", + " csv_path=IMAGE_PATHS,\n", + " image_prefix=IMAGE_PREFIX,\n", + " eval_model=EVAL_MODEL,\n", + " sample_size=SAMPLE_SIZE,\n", + " random_state=RANDOM_STATE,\n", + " stratify=True,\n", + " classes=target_classes,\n", + " )\n", + ")\n", + "\n", + "print(selected_result.summary_metrics)\n", + "plot_confusion_matrix(selected_df, title='Confusion Matrix for Selected Classes')" + ] + }, + { + "cell_type": "code", + "execution_count": 45, + "id": "cdf32ba3", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
    \n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
    img_pathresponsereferenceexact_match
    0gs://github-repo/generative-ai/gemini/use-cases/entity-extraction/images/budget/2045467868.pngbudgetbudget1.0
    1gs://github-repo/generative-ai/gemini/use-cases/entity-extraction/images/budget/ti16352375_2377.pngspecificationbudget0.0
    2gs://github-repo/generative-ai/gemini/use-cases/entity-extraction/images/budget/tcal0288700.pnginvoicebudget0.0
    3gs://github-repo/generative-ai/gemini/use-cases/entity-extraction/images/budget/11006870.pngformbudget0.0
    4gs://github-repo/generative-ai/gemini/use-cases/entity-extraction/images/budget/0000079198.pngbudgetbudget1.0
    5gs://github-repo/generative-ai/gemini/use-cases/entity-extraction/images/budget/2048693171.pngbudgetbudget1.0
    6gs://github-repo/generative-ai/gemini/use-cases/entity-extraction/images/budget/ti31139030.pngbudgetbudget1.0
    7gs://github-repo/generative-ai/gemini/use-cases/entity-extraction/images/budget/11003957.pnginvoicebudget0.0
    8gs://github-repo/generative-ai/gemini/use-cases/entity-extraction/images/budget/tob07809.99.pnginvoicebudget0.0
    9gs://github-repo/generative-ai/gemini/use-cases/entity-extraction/images/budget/24010475.pngformbudget0.0
    10gs://github-repo/generative-ai/gemini/use-cases/entity-extraction/images/budget/tcal0289181.pnginvoicebudget0.0
    11gs://github-repo/generative-ai/gemini/use-cases/entity-extraction/images/budget/0000167320.pngbudgetbudget1.0
    12gs://github-repo/generative-ai/gemini/use-cases/entity-extraction/images/budget/tnwl0028229.pngformbudget0.0
    13gs://github-repo/generative-ai/gemini/use-cases/entity-extraction/images/budget/2045814412_4416.pngbudgetbudget1.0
    14gs://github-repo/generative-ai/gemini/use-cases/entity-extraction/images/budget/2076879244.pngbudgetbudget1.0
    15gs://github-repo/generative-ai/gemini/use-cases/entity-extraction/images/form/524557796+-7813.pngspecificationform0.0
    16gs://github-repo/generative-ai/gemini/use-cases/entity-extraction/images/form/2074666677.pngformform1.0
    17gs://github-repo/generative-ai/gemini/use-cases/entity-extraction/images/form/2074551212.pngformform1.0
    18gs://github-repo/generative-ai/gemini/use-cases/entity-extraction/images/form/2072514136.pngformform1.0
    19gs://github-repo/generative-ai/gemini/use-cases/entity-extraction/images/form/2029026843.pngformform1.0
    20gs://github-repo/generative-ai/gemini/use-cases/entity-extraction/images/form/2064200721.pngformform1.0
    21gs://github-repo/generative-ai/gemini/use-cases/entity-extraction/images/form/2024471389.pngformform1.0
    22gs://github-repo/generative-ai/gemini/use-cases/entity-extraction/images/form/2077753550.pngformform1.0
    23gs://github-repo/generative-ai/gemini/use-cases/entity-extraction/images/form/2028711777.pngformform1.0
    24gs://github-repo/generative-ai/gemini/use-cases/entity-extraction/images/form/2084607707.pngformform1.0
    25gs://github-repo/generative-ai/gemini/use-cases/entity-extraction/images/form/509609620.pngformform1.0
    26gs://github-repo/generative-ai/gemini/use-cases/entity-extraction/images/form/2501097632.pngbudgetform0.0
    27gs://github-repo/generative-ai/gemini/use-cases/entity-extraction/images/form/2030772375.pngformform1.0
    28gs://github-repo/generative-ai/gemini/use-cases/entity-extraction/images/form/509189296_509189297.pngspecificationform0.0
    29gs://github-repo/generative-ai/gemini/use-cases/entity-extraction/images/form/507038881.pngformform1.0
    30gs://github-repo/generative-ai/gemini/use-cases/entity-extraction/images/invoice/0001219658.pngbudgetinvoice0.0
    31gs://github-repo/generative-ai/gemini/use-cases/entity-extraction/images/invoice/2075573314.pngforminvoice0.0
    32gs://github-repo/generative-ai/gemini/use-cases/entity-extraction/images/invoice/2023921600.pnginvoiceinvoice1.0
    33gs://github-repo/generative-ai/gemini/use-cases/entity-extraction/images/invoice/2081562369_2371.pnginvoiceinvoice1.0
    34gs://github-repo/generative-ai/gemini/use-cases/entity-extraction/images/invoice/2080690649.pnginvoiceinvoice1.0
    35gs://github-repo/generative-ai/gemini/use-cases/entity-extraction/images/invoice/506077664+-7664.pngbudgetinvoice0.0
    36gs://github-repo/generative-ai/gemini/use-cases/entity-extraction/images/invoice/2028714926.pnginvoiceinvoice1.0
    37gs://github-repo/generative-ai/gemini/use-cases/entity-extraction/images/invoice/2063321228.pnginvoiceinvoice1.0
    38gs://github-repo/generative-ai/gemini/use-cases/entity-extraction/images/invoice/CTRCONTRACTS024759-4.pnginvoiceinvoice1.0
    39gs://github-repo/generative-ai/gemini/use-cases/entity-extraction/images/invoice/88132033.pnginvoiceinvoice1.0
    40gs://github-repo/generative-ai/gemini/use-cases/entity-extraction/images/invoice/00922266_00922271.pnginvoiceinvoice1.0
    41gs://github-repo/generative-ai/gemini/use-cases/entity-extraction/images/invoice/2074660865.pnginvoiceinvoice1.0
    42gs://github-repo/generative-ai/gemini/use-cases/entity-extraction/images/invoice/2028720753_2028720754.pnginvoiceinvoice1.0
    43gs://github-repo/generative-ai/gemini/use-cases/entity-extraction/images/invoice/2001207874.pnginvoiceinvoice1.0
    44gs://github-repo/generative-ai/gemini/use-cases/entity-extraction/images/invoice/515618928+-8930.pngforminvoice0.0
    45gs://github-repo/generative-ai/gemini/use-cases/entity-extraction/images/specification/2069720832.pngspecificationspecification1.0
    46gs://github-repo/generative-ai/gemini/use-cases/entity-extraction/images/specification/2057163994_2057163995.pngformspecification0.0
    47gs://github-repo/generative-ai/gemini/use-cases/entity-extraction/images/specification/2055275453.pngspecificationspecification1.0
    48gs://github-repo/generative-ai/gemini/use-cases/entity-extraction/images/specification/2069730436.pngspecificationspecification1.0
    49gs://github-repo/generative-ai/gemini/use-cases/entity-extraction/images/specification/530542237+-2239.pngspecificationspecification1.0
    50gs://github-repo/generative-ai/gemini/use-cases/entity-extraction/images/specification/2057410449_2057410451.pngspecificationspecification1.0
    51gs://github-repo/generative-ai/gemini/use-cases/entity-extraction/images/specification/2057433890.pngspecificationspecification1.0
    52gs://github-repo/generative-ai/gemini/use-cases/entity-extraction/images/specification/0011572293.pngspecificationspecification1.0
    53gs://github-repo/generative-ai/gemini/use-cases/entity-extraction/images/specification/505957777.pngspecificationspecification1.0
    54gs://github-repo/generative-ai/gemini/use-cases/entity-extraction/images/specification/2054772976_2054772982.pngspecificationspecification1.0
    55gs://github-repo/generative-ai/gemini/use-cases/entity-extraction/images/specification/2069736584.pngspecificationspecification1.0
    56gs://github-repo/generative-ai/gemini/use-cases/entity-extraction/images/specification/504804068+-4068.pngspecificationspecification1.0
    57gs://github-repo/generative-ai/gemini/use-cases/entity-extraction/images/specification/0012229304.pngspecificationspecification1.0
    58gs://github-repo/generative-ai/gemini/use-cases/entity-extraction/images/specification/0000225206.pngspecificationspecification1.0
    59gs://github-repo/generative-ai/gemini/use-cases/entity-extraction/images/specification/512383380_512383389.pngspecificationspecification1.0
    \n", + "
    " + ], + "text/plain": [ + " img_path \\\n", + "0 gs://github-repo/generative-ai/gemini/use-cases/entity-extraction/images/budget/2045467868.png \n", + "1 gs://github-repo/generative-ai/gemini/use-cases/entity-extraction/images/budget/ti16352375_2377.png \n", + "2 gs://github-repo/generative-ai/gemini/use-cases/entity-extraction/images/budget/tcal0288700.png \n", + "3 gs://github-repo/generative-ai/gemini/use-cases/entity-extraction/images/budget/11006870.png \n", + "4 gs://github-repo/generative-ai/gemini/use-cases/entity-extraction/images/budget/0000079198.png \n", + "5 gs://github-repo/generative-ai/gemini/use-cases/entity-extraction/images/budget/2048693171.png \n", + "6 gs://github-repo/generative-ai/gemini/use-cases/entity-extraction/images/budget/ti31139030.png \n", + "7 gs://github-repo/generative-ai/gemini/use-cases/entity-extraction/images/budget/11003957.png \n", + "8 gs://github-repo/generative-ai/gemini/use-cases/entity-extraction/images/budget/tob07809.99.png \n", + "9 gs://github-repo/generative-ai/gemini/use-cases/entity-extraction/images/budget/24010475.png \n", + "10 gs://github-repo/generative-ai/gemini/use-cases/entity-extraction/images/budget/tcal0289181.png \n", + "11 gs://github-repo/generative-ai/gemini/use-cases/entity-extraction/images/budget/0000167320.png \n", + "12 gs://github-repo/generative-ai/gemini/use-cases/entity-extraction/images/budget/tnwl0028229.png \n", + "13 gs://github-repo/generative-ai/gemini/use-cases/entity-extraction/images/budget/2045814412_4416.png \n", + "14 gs://github-repo/generative-ai/gemini/use-cases/entity-extraction/images/budget/2076879244.png \n", + "15 gs://github-repo/generative-ai/gemini/use-cases/entity-extraction/images/form/524557796+-7813.png \n", + "16 gs://github-repo/generative-ai/gemini/use-cases/entity-extraction/images/form/2074666677.png \n", + "17 gs://github-repo/generative-ai/gemini/use-cases/entity-extraction/images/form/2074551212.png \n", + "18 gs://github-repo/generative-ai/gemini/use-cases/entity-extraction/images/form/2072514136.png \n", + "19 gs://github-repo/generative-ai/gemini/use-cases/entity-extraction/images/form/2029026843.png \n", + "20 gs://github-repo/generative-ai/gemini/use-cases/entity-extraction/images/form/2064200721.png \n", + "21 gs://github-repo/generative-ai/gemini/use-cases/entity-extraction/images/form/2024471389.png \n", + "22 gs://github-repo/generative-ai/gemini/use-cases/entity-extraction/images/form/2077753550.png \n", + "23 gs://github-repo/generative-ai/gemini/use-cases/entity-extraction/images/form/2028711777.png \n", + "24 gs://github-repo/generative-ai/gemini/use-cases/entity-extraction/images/form/2084607707.png \n", + "25 gs://github-repo/generative-ai/gemini/use-cases/entity-extraction/images/form/509609620.png \n", + "26 gs://github-repo/generative-ai/gemini/use-cases/entity-extraction/images/form/2501097632.png \n", + "27 gs://github-repo/generative-ai/gemini/use-cases/entity-extraction/images/form/2030772375.png \n", + "28 gs://github-repo/generative-ai/gemini/use-cases/entity-extraction/images/form/509189296_509189297.png \n", + "29 gs://github-repo/generative-ai/gemini/use-cases/entity-extraction/images/form/507038881.png \n", + "30 gs://github-repo/generative-ai/gemini/use-cases/entity-extraction/images/invoice/0001219658.png \n", + "31 gs://github-repo/generative-ai/gemini/use-cases/entity-extraction/images/invoice/2075573314.png \n", + "32 gs://github-repo/generative-ai/gemini/use-cases/entity-extraction/images/invoice/2023921600.png \n", + "33 gs://github-repo/generative-ai/gemini/use-cases/entity-extraction/images/invoice/2081562369_2371.png \n", + "34 gs://github-repo/generative-ai/gemini/use-cases/entity-extraction/images/invoice/2080690649.png \n", + "35 gs://github-repo/generative-ai/gemini/use-cases/entity-extraction/images/invoice/506077664+-7664.png \n", + "36 gs://github-repo/generative-ai/gemini/use-cases/entity-extraction/images/invoice/2028714926.png \n", + "37 gs://github-repo/generative-ai/gemini/use-cases/entity-extraction/images/invoice/2063321228.png \n", + "38 gs://github-repo/generative-ai/gemini/use-cases/entity-extraction/images/invoice/CTRCONTRACTS024759-4.png \n", + "39 gs://github-repo/generative-ai/gemini/use-cases/entity-extraction/images/invoice/88132033.png \n", + "40 gs://github-repo/generative-ai/gemini/use-cases/entity-extraction/images/invoice/00922266_00922271.png \n", + "41 gs://github-repo/generative-ai/gemini/use-cases/entity-extraction/images/invoice/2074660865.png \n", + "42 gs://github-repo/generative-ai/gemini/use-cases/entity-extraction/images/invoice/2028720753_2028720754.png \n", + "43 gs://github-repo/generative-ai/gemini/use-cases/entity-extraction/images/invoice/2001207874.png \n", + "44 gs://github-repo/generative-ai/gemini/use-cases/entity-extraction/images/invoice/515618928+-8930.png \n", + "45 gs://github-repo/generative-ai/gemini/use-cases/entity-extraction/images/specification/2069720832.png \n", + "46 gs://github-repo/generative-ai/gemini/use-cases/entity-extraction/images/specification/2057163994_2057163995.png \n", + "47 gs://github-repo/generative-ai/gemini/use-cases/entity-extraction/images/specification/2055275453.png \n", + "48 gs://github-repo/generative-ai/gemini/use-cases/entity-extraction/images/specification/2069730436.png \n", + "49 gs://github-repo/generative-ai/gemini/use-cases/entity-extraction/images/specification/530542237+-2239.png \n", + "50 gs://github-repo/generative-ai/gemini/use-cases/entity-extraction/images/specification/2057410449_2057410451.png \n", + "51 gs://github-repo/generative-ai/gemini/use-cases/entity-extraction/images/specification/2057433890.png \n", + "52 gs://github-repo/generative-ai/gemini/use-cases/entity-extraction/images/specification/0011572293.png \n", + "53 gs://github-repo/generative-ai/gemini/use-cases/entity-extraction/images/specification/505957777.png \n", + "54 gs://github-repo/generative-ai/gemini/use-cases/entity-extraction/images/specification/2054772976_2054772982.png \n", + "55 gs://github-repo/generative-ai/gemini/use-cases/entity-extraction/images/specification/2069736584.png \n", + "56 gs://github-repo/generative-ai/gemini/use-cases/entity-extraction/images/specification/504804068+-4068.png \n", + "57 gs://github-repo/generative-ai/gemini/use-cases/entity-extraction/images/specification/0012229304.png \n", + "58 gs://github-repo/generative-ai/gemini/use-cases/entity-extraction/images/specification/0000225206.png \n", + "59 gs://github-repo/generative-ai/gemini/use-cases/entity-extraction/images/specification/512383380_512383389.png \n", + "\n", + " response reference exact_match \n", + "0 budget budget 1.0 \n", + "1 specification budget 0.0 \n", + "2 invoice budget 0.0 \n", + "3 form budget 0.0 \n", + "4 budget budget 1.0 \n", + "5 budget budget 1.0 \n", + "6 budget budget 1.0 \n", + "7 invoice budget 0.0 \n", + "8 invoice budget 0.0 \n", + "9 form budget 0.0 \n", + "10 invoice budget 0.0 \n", + "11 budget budget 1.0 \n", + "12 form budget 0.0 \n", + "13 budget budget 1.0 \n", + "14 budget budget 1.0 \n", + "15 specification form 0.0 \n", + "16 form form 1.0 \n", + "17 form form 1.0 \n", + "18 form form 1.0 \n", + "19 form form 1.0 \n", + "20 form form 1.0 \n", + "21 form form 1.0 \n", + "22 form form 1.0 \n", + "23 form form 1.0 \n", + "24 form form 1.0 \n", + "25 form form 1.0 \n", + "26 budget form 0.0 \n", + "27 form form 1.0 \n", + "28 specification form 0.0 \n", + "29 form form 1.0 \n", + "30 budget invoice 0.0 \n", + "31 form invoice 0.0 \n", + "32 invoice invoice 1.0 \n", + "33 invoice invoice 1.0 \n", + "34 invoice invoice 1.0 \n", + "35 budget invoice 0.0 \n", + "36 invoice invoice 1.0 \n", + "37 invoice invoice 1.0 \n", + "38 invoice invoice 1.0 \n", + "39 invoice invoice 1.0 \n", + "40 invoice invoice 1.0 \n", + "41 invoice invoice 1.0 \n", + "42 invoice invoice 1.0 \n", + "43 invoice invoice 1.0 \n", + "44 form invoice 0.0 \n", + "45 specification specification 1.0 \n", + "46 form specification 0.0 \n", + "47 specification specification 1.0 \n", + "48 specification specification 1.0 \n", + "49 specification specification 1.0 \n", + "50 specification specification 1.0 \n", + "51 specification specification 1.0 \n", + "52 specification specification 1.0 \n", + "53 specification specification 1.0 \n", + "54 specification specification 1.0 \n", + "55 specification specification 1.0 \n", + "56 specification specification 1.0 \n", + "57 specification specification 1.0 \n", + "58 specification specification 1.0 \n", + "59 specification specification 1.0 " + ] + }, + "execution_count": 45, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "pd.set_option('display.max_colwidth', None)\n", + "selected_df\n", + "\n" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": ".venv-1 (3.13.7)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.13.7" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/gemini/use-cases/entity-extraction/evaluate.py b/gemini/use-cases/entity-extraction/evaluate.py new file mode 100644 index 0000000..55877cd --- /dev/null +++ b/gemini/use-cases/entity-extraction/evaluate.py @@ -0,0 +1,207 @@ +# Copyright 2025 Google LLC +# +# 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 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Evaluation code for document classification use case. This code reads in a +CSV file with image paths and labels, prepares the data for evaluation, runs +inference using a specified Gemini model, and then evaluates the predictions +against the reference labels using the exact match metric in the Generative AI +Evaluation framework. +""" + +import dotenv +import json +import os +import pandas as pd + +from google.genai import types +import vertexai + +import document_processing + + +# Load environment variables. +dotenv.load_dotenv() +PROJECT_ID = os.environ.get("GEMINI_PROJECT_ID") +if not PROJECT_ID: + raise ValueError("GEMINI_PROJECT_ID environment variable must be set.") +LOCATION = os.environ.get("GEMINI_LOCATION", "global") +IMAGE_PATHS = os.environ.get("IMAGE_PATHS", "") +IMAGE_PREFIX = os.environ.get("IMAGE_PREFIX", "") +EVAL_DEST = os.environ.get("EVAL_DEST") + +# Other default constants. +EVAL_MODEL = "gemini-2.5-flash" +SAMPLE_SIZE = 10 + + +def load_eval_data(csv_path: str, image_prefix: str) -> pd.DataFrame: + """Reads eval data from CSV, formats paths, and prepares labels.""" + df = pd.read_csv(csv_path) + df = df[["img_path", "label"]] + df["img_path"] = f"{image_prefix}/" + df["img_path"] + df = df.rename(columns={"label": "reference"}) + return df + +def prepare_eval_df( + csv_path: str, + image_prefix: str, + sample_size: int = None, + random_state: int = None, + stratify: bool = False, + classes: list[str] = None +) -> pd.DataFrame: + """Prepares the eval_df based on the data from csv file with image paths.""" + config_classes = ( + document_processing.CONFIGS["classification_config"]["classes"] + ) + + if classes is None: + prompt_classes = config_classes + filter_classes = list(config_classes.keys()) + else: + prompt_classes = { + k: v for k, v in config_classes.items() if k in classes + } + filter_classes = classes + + prompt = document_processing.CLASSIFY_PROMPT_TEMPLATE.format( + classes=json.dumps(prompt_classes, indent=4) + ) + print(prompt) + + df = load_eval_data(csv_path, image_prefix) + + # Filter the DataFrame to only include the requested classes + df = df[df["reference"].isin(filter_classes)].reset_index(drop=True) + + requests = [] + for uri in df["img_path"]: + image_part = types.Part.from_uri( + file_uri=uri, + mime_type="image/png" + ) + requests.append([image_part, prompt]) + + df["request"] = requests + + if sample_size and sample_size < len(df): + if stratify: + # Proportional stratified sampling per class + fraction = sample_size / len(df) + df = df.groupby("reference", group_keys=False).apply( + lambda x: x.sample( + n=max(1, int(round(len(x) * fraction))), + random_state=random_state + ) + ) + # Correct any slight oversampling due to rounding + if len(df) > sample_size: + df = df.sample(n=sample_size, random_state=random_state) + df = df.reset_index(drop=True) + else: + df = df.sample( + n=sample_size, + random_state=random_state + ).reset_index(drop=True) + + return df + +def extract_class(response_str): + """Extract the class from the JSON response.""" + try: + return json.loads(response_str).get("class") + except (json.JSONDecodeError, AttributeError): + return response_str + +def run_evaluation( + project_id: str = PROJECT_ID, + location: str = LOCATION, + csv_path: str = IMAGE_PATHS, + image_prefix: str = IMAGE_PREFIX, + eval_model: str = EVAL_MODEL, + sample_size: int = SAMPLE_SIZE, + random_state: int = 42, + stratify: bool = False, + classes: list[str] = None, + eval_dest: str = EVAL_DEST +): + client = vertexai.Client(project=project_id, location=location) + + eval_df = prepare_eval_df( + csv_path=csv_path, + image_prefix=image_prefix, + sample_size=sample_size, + random_state=random_state, + stratify=stratify, + classes=classes + ) + + eval_dataset = client.evals.run_inference( + model=eval_model, + src=eval_df, + config={ + "generate_content_config": { + "response_mime_type": "application/json", + "temperature": 0 + }, + "dest": eval_dest if eval_dest else None + } + ) + + if hasattr(eval_dataset, "eval_dataset_df"): + eval_dataset = eval_dataset.eval_dataset_df + + eval_dataset["predicted_class"] = ( + eval_dataset["response"].apply(extract_class) + ) + + # The evaluate function expects 'prompt', 'response', and 'reference' + # columns, even though the comparison is done between 'response' and + # 'reference' only. + eval_input_df = eval_dataset.copy() + eval_input_df["response"] = eval_input_df["predicted_class"] + eval_input_df["prompt"] = "Multimodal classification prompt" + eval_input_df = ( + eval_input_df[["img_path", "prompt", "response", "reference"]] + ) + + eval_result = ( + client.evals.evaluate( + dataset=eval_input_df, + metrics=[vertexai.types.Metric(name='exact_match')], + config={"dest": eval_dest} if eval_dest else None + ) + ) + + exact_match_scores = [ + case.response_candidate_results[0].metric_results["exact_match"].score + for case in eval_result.eval_case_results + ] + + # Include the original request and the exact match score back into the + # input DataFrame. + + eval_input_df["exact_match"] = exact_match_scores + eval_input_df["request"] = eval_dataset["request"] + + # Select and reorder columns for the final results table + results_df = eval_input_df[ + ["img_path", "response", "reference", "exact_match"] + ] + + return eval_result, results_df + +# if __name__ == "__main__": +# run_evaluation() diff --git a/gemini/use-cases/entity-extraction/images/current_architecture.png b/gemini/use-cases/entity-extraction/images/current_architecture.png new file mode 100644 index 0000000..f80cabb Binary files /dev/null and b/gemini/use-cases/entity-extraction/images/current_architecture.png differ diff --git a/gemini/use-cases/entity-extraction/images/future_architecture.png b/gemini/use-cases/entity-extraction/images/future_architecture.png new file mode 100644 index 0000000..74b2f7e Binary files /dev/null and b/gemini/use-cases/entity-extraction/images/future_architecture.png differ diff --git a/gemini/use-cases/entity-extraction/main.py b/gemini/use-cases/entity-extraction/main.py new file mode 100644 index 0000000..409ef1b --- /dev/null +++ b/gemini/use-cases/entity-extraction/main.py @@ -0,0 +1,140 @@ +# Copyright 2025 Google LLC +# +# 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 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Main file for web server.""" + +import json +import logging +import os + +import document_processing +from flask import Flask, jsonify, request + +app = Flask(__name__) + + +@app.route("/extract", methods=["POST"]) +def handle_extraction(): + """HTTP endpoint to handle entity extraction requests.""" + data = request.get_json() + logging.info(f"Received handle_extraction request.") + + if not data or "extract_config_id" not in data or "document_uri" not in data: + logging.warning("Invalid request data received. Missing required fields.") + return ( + jsonify( + {"error": "Request must include 'extract_config_id' and 'document_uri'"} + ), + 400, + ) + + try: + result_text = document_processing.extract_from_document( + extract_config_id=data["extract_config_id"], + document_uri=data["document_uri"], + ) + logging.info(f"Entity extraction result: {result_text}") + return jsonify(json.loads(result_text)), 200 + + except KeyError: + error_msg = f"Configuration with data '{data}' not found." + logging.info(error_msg) + return jsonify({"error": error_msg}), 404 + except json.JSONDecodeError: + error_msg = f"Failed to decode JSON from model response: {result_text}" + logging.info(error_msg) + return jsonify( + {"error": "Failed to parse response from the extraction model."} + ), 500 + except Exception as e: + logging.info(f"An unexpected error occurred: {e}") + return jsonify({"error": "An internal error occurred."}), 500 + + +@app.route("/classify", methods=["POST"]) +def handle_classification(): + """HTTP endpoint to handle entity classification requests.""" + data = request.get_json() + logging.info(f"Received data request: {data}") + + if not data or "document_uri" not in data: + logging.warning("Invalid request data received. Missing required fields.") + return ( + jsonify({"error": "Request must include 'document_uri'"}), + 400, + ) + + try: + result_text = document_processing.classify_document( + document_uri=data["document_uri"], + ) + logging.info(f"Document classification result: {result_text}") + return jsonify(json.loads(result_text)), 200 + + except KeyError: + error_msg = f"Configuration with data '{data}' not found." + logging.info(error_msg) + return jsonify({"error": error_msg}), 404 + except json.JSONDecodeError: + error_msg = f"Failed to decode JSON from model response: {result_text}" + logging.info(error_msg) + return jsonify( + {"error": "Failed to parse response from the classification model."} + ), 500 + except Exception as e: + logging.info(f"An unexpected error occurred: {e}") + return jsonify({"error": "An internal error occurred."}), 500 + + +@app.route("/classify_and_extract", methods=["POST"]) +def handle_classification_and_extraction(): + """HTTP endpoint to handle entity classification and extraction requests.""" + data = request.get_json() + logging.info(f"Received data request: {data}") + + if not data or "document_uri" not in data: + logging.warning("Invalid request data received. Missing required fields.") + return ( + jsonify({"error": "Request must include 'document_uri'"}), + 400, + ) + + try: + result_text = document_processing.classify_and_extract_document( + document_uri=data["document_uri"], + ) + logging.info(f"Document classification and extraction result: {result_text}") + return jsonify(json.loads(result_text)), 200 + + except KeyError: + error_msg = f"Configuration with data '{data}' not found." + logging.info(error_msg) + return jsonify({"error": error_msg}), 404 + except json.JSONDecodeError: + error_msg = f"Failed to decode JSON from model response: {result_text}" + logging.info(error_msg) + return jsonify( + {"error": "Failed to parse response from the classification model."} + ), 500 + except ValueError as e: + error_msg = f"Document classification or extraction failed: {e}" + logging.info(error_msg) + return jsonify({"error": error_msg}), 400 + except Exception as e: + logging.info(f"An unexpected error occurred: {e}") + return jsonify({"error": "An internal error occurred."}), 500 + + +if __name__ == "__main__": + app.run(debug=True, host="0.0.0.0", port=int(os.environ.get("PORT", 8080))) diff --git a/gemini/use-cases/entity-extraction/requirements.txt b/gemini/use-cases/entity-extraction/requirements.txt new file mode 100644 index 0000000..542d40e --- /dev/null +++ b/gemini/use-cases/entity-extraction/requirements.txt @@ -0,0 +1,10 @@ +dotenv +flask +gcsfs +google-cloud-aiplatform +google-cloud-storage +google-genai +gunicorn +pymupdf +opencv-python +numpy diff --git a/gemini/use-cases/entity-extraction/utils.py b/gemini/use-cases/entity-extraction/utils.py new file mode 100644 index 0000000..2a7e297 --- /dev/null +++ b/gemini/use-cases/entity-extraction/utils.py @@ -0,0 +1,65 @@ +# Copyright 2025 Google LLC +# +# 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 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Utilities for entity extraction.""" + +import json +import logging + +from google.cloud import storage + + +def load_config_from_gcs(bucket_name: str, file_name: str) -> dict: + """Downloads a file from GCS and parses it as JSON.""" + logging.info(f"Loading config from GCS: gs://{bucket_name}/{file_name}") + try: + client = storage.Client() + bucket = client.bucket(bucket_name) + blob = bucket.blob(file_name) + config_data = blob.download_as_string() + return json.loads(config_data) + except Exception as e: + logging.error(f"ERROR: Failed to load GCS config: {e}") + raise + + +def load_config_from_local(file_path: str) -> dict: + """Loads a config file from the local filesystem.""" + logging.info(f"Loading config from local file: {file_path}") + try: + with open(file_path) as f: + return json.load(f) + except Exception as e: + logging.error(f"ERROR: Failed to load local config {file_path}: {e}") + raise + + +def load_app_config(config_path: str) -> dict: + """Loads entity extraction configurations from GCS or local based on path prefix.""" + if config_path.startswith("gs://"): + try: + # Parse the GCS path: "gs://[BUCKET_NAME]/[FILE_PATH]" + path_parts = config_path.replace("gs://", "").split("/", 1) + if len(path_parts) < 2 or not path_parts[0] or not path_parts[1]: + raise ValueError( + "GCS path must be in gs:/// format." + ) + bucket_name, file_path = path_parts + return load_config_from_gcs(bucket_name, file_path) + except Exception as e: + logging.error(f"ERROR: Could not parse GCS path '{config_path}': {e}") + raise + else: + # Treat it as a local file path + return load_config_from_local(config_path) diff --git a/gemini/use-cases/graphrag/agentic_graph_rag_neo4j.ipynb b/gemini/use-cases/graphrag/agentic_graph_rag_neo4j.ipynb new file mode 100644 index 0000000..ed3ebcd --- /dev/null +++ b/gemini/use-cases/graphrag/agentic_graph_rag_neo4j.ipynb @@ -0,0 +1,1265 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "id": "Header_01" + }, + "source": [ + "# đŸ›Ąïž Agentic GraphRAG: Cybersecurity Threat Intelligence\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + "
    \n", + " \n", + " \"Google
    Open in Colab\n", + "
    \n", + "
    \n", + " \n", + " \"Google
    Open in Colab Enterprise\n", + "
    \n", + "
    \n", + " \n", + " \"Vertex
    Open in Vertex AI Workbench\n", + "
    \n", + "
    \n", + " \n", + " \"GitHub
    View on GitHub\n", + "
    \n", + "
    \n", + "\n", + "
    " + ] + }, + { + "cell_type": "markdown", + "source": [ + "| Author | Architecture |\n", + "| --- | --- |\n", + "| [Aniket Agrawal](https://github.com/aniketagrawal2012) | **Vertex AI Agent Engine + Google ADK + Neo4j** |" + ], + "metadata": { + "id": "OFzR-URUlNf0" + } + }, + { + "cell_type": "markdown", + "metadata": { + "id": "Share_Buttons" + }, + "source": [ + "Share to:\n", + "\n", + "\n", + " \"LinkedIn\n", + "\n", + "\n", + "\n", + " \"Bluesky\n", + "\n", + "\n", + "\n", + " \"X\n", + "\n", + "\n", + "\n", + " \"Reddit\n", + "\n", + "\n", + "\n", + " \"Facebook\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "Overview_02" + }, + "source": [ + "## Overview\n", + "\n", + "This notebook builds a **Cybersecurity Threat Analysis Agent**. Unlike standard RAG which retrieves documents, this agent uses **GraphRAG** to understand the complex relationships between Threat Actors, Malware, and Vulnerabilities (CVEs).\n", + "\n", + "### The \"Neo4j Flavour\" + ADK Upgrade\n", + "We are evolving the architecture from Dialogflow CX to the **Google Agent Development Kit (ADK)** and **Vertex AI Reasoning Engine**.\n", + "\n", + "**System Components:**\n", + "1. **Graph Database (Neo4j):** Stores the threat landscape (e.g., `(APT28)-[:USES]->(Zebrocy)`).\n", + "2. **Reasoning Engine (Vertex AI):** A managed service to deploy the agent logic.\n", + "3. **Framework (Google ADK):** Defines the agent's persona, tools, and routing logic programmatically.\n", + "\n", + "\n", + "### Objectives\n", + "In this tutorial, you will:\n", + "* Install the necessary libraries (Google ADK, LangChain, Neo4j drivers).\n", + "* Set up your Google Cloud environment and authenticate.\n", + "* Connect to a Neo4j database and seed it with cybersecurity threat data.\n", + "* Define a GraphRAG tool using LangChain to query the knowledge graph.\n", + "* Build an agent using the Google ADK that utilizes the GraphRAG tool.\n", + "* Test the agent locally to verify its reasoning capabilities.\n", + "* Deploy the agent to the Vertex AI Agent Engine as a managed service.\n", + "* Visualize the threat graph interactively within the notebook.\n", + "* Clean up resources to avoid incurring costs.\n", + "\n", + "## Before you begin\n", + "\n", + "1. In the Google Cloud console, on the project selector page, select or [create a Google Cloud project](https://cloud.google.com/resource-manager/docs/creating-managing-projects).\n", + "2. [Make sure that billing is enabled for your Google Cloud project](https://cloud.google.com/billing/docs/how-to/verify-billing-enabled#console).\n", + "3. [Make sure the Vertex AI API is enabled](https://console.cloud.google.com/flows/enableapi?apiid=aiplatform.googleapis.com).\n", + "\n", + "### Required roles\n", + "To get the permissions that you need to complete the tutorial, ask your administrator to grant you the [Vertex AI User](https://cloud.google.com/iam/docs/understanding-roles#aiplatform.user) (`roles/aiplatform.user`) IAM role on your project. For more information about granting roles, see [Manage access](https://cloud.google.com/iam/docs/granting-changing-revoking-access)." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "Install_03" + }, + "source": [ + "### 1. Installation and Prerequisites\n", + "We install the **Google ADK**, **LangChain**, and **Neo4j** drivers." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "PipInstall_04" + }, + "outputs": [], + "source": [ + "%pip install --quiet google-adk>=1.0.0\n", + "%pip install --quiet google-cloud-aiplatform>=1.97.0\n", + "%pip install --quiet langchain-google-vertexai\n", + "%pip install --quiet langchain-community neo4j" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "DoK1gW9dsRxB" + }, + "source": [ + "#### Authenticating your notebook environment\n", + "* If you are using **Colab** to run this notebook, uncomment the cell below and continue.\n", + "* If you are using **Vertex AI Workbench**, check out the setup instructions [here](https://github.com/GoogleCloudPlatform/generative-ai/tree/main/setup-env)." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": { + "id": "os3H39sGXugN", + "outputId": "6c8c5162-4c4b-44d4-ac18-7907efa27051", + "colab": { + "base_uri": "https://localhost:8080/" + } + }, + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": [ + "3.12.12 (main, Oct 10 2025, 08:52:57) [GCC 11.4.0]\n", + "✅ Authenticated\n" + ] + } + ], + "source": [ + "import sys\n", + "print(sys.version)\n", + "\n", + "if \"google.colab\" in sys.modules:\n", + " from google.colab import auth\n", + " auth.authenticate_user()\n", + " print(\"✅ Authenticated\")" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": { + "id": "Config_05" + }, + "outputs": [], + "source": [ + "import os\n", + "\n", + "# --- CONFIGURATION ---\n", + "PROJECT_ID = \"your-project-id\" # @param {type:\"string\"}\n", + "REGION = \"us-central1\" # @param {type:\"string\"}\n", + "NEO4J_URI = \"neo4j+s://your-instance.databases.neo4j.io\" # @param {type:\"string\"}\n", + "NEO4J_USER = \"neo4j\" # @param {type:\"string\"}\n", + "NEO4J_PASSWORD = \"your-password\" # @param {type:\"string\"}\n", + "\n", + "# Set Environment\n", + "os.environ[\"GOOGLE_CLOUD_PROJECT\"] = PROJECT_ID\n", + "os.environ[\"GOOGLE_CLOUD_LOCATION\"] = REGION\n", + "os.environ[\"NEO4J_URI\"] = NEO4J_URI\n", + "os.environ[\"NEO4J_USER\"] = NEO4J_USER\n", + "os.environ[\"NEO4J_PASSWORD\"] = NEO4J_PASSWORD\n", + "\n", + "import vertexai\n", + "vertexai.init(project=PROJECT_ID, location=REGION)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "Seeding_06" + }, + "source": [ + "### 2. Hydrate the Knowledge Graph\n", + "We will seed the database with a sample **Cybersecurity Schema**. This creates nodes for `ThreatActor`, `Malware`, and `Vulnerability`." + ] + }, + { + "cell_type": "code", + "execution_count": 31, + "metadata": { + "id": "SeedCode_07", + "colab": { + "base_uri": "https://localhost:8080/" + }, + "outputId": "33c8a9c9-0406-41ee-f755-026b69dfb010" + }, + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": [ + "đŸŒ± Seeding Cyber Threat Data...\n", + "✅ Database populated with Threat Intel graph.\n" + ] + } + ], + "source": [ + "from langchain_community.graphs import Neo4jGraph\n", + "\n", + "def seed_database():\n", + " print(\"đŸŒ± Seeding Cyber Threat Data...\")\n", + " graph = Neo4jGraph(url=NEO4J_URI, username=NEO4J_USER, password=NEO4J_PASSWORD)\n", + "\n", + " cypher = \"\"\"\n", + " MERGE (a:ThreatActor {name: 'APT29', alias: 'Cozy Bear'})\n", + " MERGE (m:Malware {name: 'WellMess'})\n", + " MERGE (v:Vulnerability {cve: 'CVE-2023-1234', severity: 'High'})\n", + " MERGE (t:Target {sector: 'Pharmaceuticals'})\n", + "\n", + " MERGE (a)-[:USES]->(m)\n", + " MERGE (m)-[:EXPLOITS]->(v)\n", + " MERGE (a)-[:TARGETS]->(t)\n", + " \"\"\"\n", + " graph.query(cypher)\n", + " print(\"✅ Database populated with Threat Intel graph.\")\n", + "\n", + "if \"your-password\" not in NEO4J_PASSWORD:\n", + " seed_database()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "ToolDef_08" + }, + "source": [ + "### 3. Define the GraphRAG Tool\n", + "We define a Python function `query_threat_graph`. The **Vertex AI Agent** will call this function when it needs to answer questions about security threats. It uses **LangChain** to translate the question into Cypher." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": { + "id": "ToolCode_09" + }, + "outputs": [], + "source": [ + "from langchain_google_vertexai import VertexAI\n", + "from langchain_community.chains.graph_qa.cypher import GraphCypherQAChain\n", + "\n", + "def query_threat_graph(question: str) -> str:\n", + " \"\"\"Queries the cybersecurity knowledge graph to answer questions about actors, malware, and CVEs.\"\"\"\n", + " try:\n", + " # Re-initialize inside function for serialization contexts\n", + " import os\n", + " from langchain_community.graphs import Neo4jGraph\n", + " from langchain_google_vertexai import VertexAI\n", + " from langchain_community.chains.graph_qa.cypher import GraphCypherQAChain\n", + "\n", + " # Credentials must be explicit for remote execution\n", + " neo4j_uri = \"neo4j+s://7d50da77.databases.neo4j.io\"\n", + " neo4j_user = \"neo4j\"\n", + " neo4j_password = \"nZasSfGzac_mgTApAqrZd_Yqty9I4HOXKyj8qeNKdYg\"\n", + "\n", + " graph = Neo4jGraph(\n", + " url=neo4j_uri,\n", + " username=neo4j_user,\n", + " password=neo4j_password\n", + " )\n", + "\n", + " llm = VertexAI(model_name=\"gemini-2.0-flash\", temperature=0)\n", + "\n", + " chain = GraphCypherQAChain.from_llm(\n", + " llm=llm,\n", + " graph=graph,\n", + " verbose=True,\n", + " allow_dangerous_requests=True\n", + " )\n", + "\n", + " result = chain.invoke(question)\n", + " return result['result']\n", + "\n", + " except Exception as e:\n", + " # Catch ALL errors (imports, init, connection) to prevent Engine crash\n", + " return f\"DEBUG ERROR in tool: {str(e)}\"" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "AgentDef_10" + }, + "source": [ + "### 4. Create the ADK Agent\n", + "Using the **Agent Development Kit**, we define the `CyberSecurityAgent`. We give it instructions to always check the graph first before assuming answers." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": { + "id": "AgentCode_11", + "colab": { + "base_uri": "https://localhost:8080/" + }, + "outputId": "ad350b95-d685-433e-e92d-83ec842b9089" + }, + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": [ + "✅ Agent Definition Created\n" + ] + } + ], + "source": [ + "# Install missing dependency required by google-adk\n", + "%pip install --quiet deprecated\n", + "\n", + "from google.adk.agents import Agent\n", + "# from google.adk.tools import google_search <-- Removed to avoid API conflict\n", + "\n", + "cyber_agent = Agent(\n", + " name=\"CyberThreatIntel\",\n", + " model=\"gemini-2.0-flash\",\n", + " description=\"An expert in cybersecurity threat intelligence and graph analysis.\",\n", + " instruction=\"\"\"\n", + " You are a Cybersecurity Analyst.\n", + " 1. If the user asks about Threats, Actors, or CVEs, ALWAYS use the 'query_threat_graph' tool first.\n", + " 2. Be concise and actionable in your reporting.\n", + " \"\"\",\n", + " tools=[query_threat_graph] # Removed google_search\n", + ")\n", + "\n", + "print(\"✅ Agent Definition Created\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "LocalTest_12" + }, + "source": [ + "### 5. Local Testing\n", + "We instantiate the **ADK App** locally to test the reasoning loop." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "TestCode_13" + }, + "outputs": [], + "source": [ + "from vertexai.preview import reasoning_engines\n", + "\n", + "# Initialize Local App\n", + "app = reasoning_engines.AdkApp(\n", + " agent=cyber_agent,\n", + " enable_tracing=True\n", + ")\n", + "\n", + "print(\"💬 Querying Agent: 'Which malware does APT29 use?'\")\n", + "print(\"-\" * 40)\n", + "\n", + "if \"your-password\" not in NEO4J_PASSWORD:\n", + " # Streaming response\n", + " for event in app.stream_query(user_id=\"analyst_01\", message=\"Which malware does APT29 use?\"):\n", + " if 'content' in event and 'parts' in event['content']:\n", + " part = event['content']['parts'][0]\n", + " if 'text' in part:\n", + " print(part['text'], end=\"\")\n", + " elif 'function_call' in part:\n", + " print(f\"\\n[đŸ› ïž Tool Call: {part['function_call']['name']}]\", end=\"\\n\")\n", + "else:\n", + " print(\"⚠ Neo4j credentials missing. Skipping test.\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "Deploy_14" + }, + "source": [ + "### 6. Deploy to Vertex AI Agent Engine\n", + "Finally, we package and deploy the agent as a scalable, managed service on Google Cloud." + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": { + "id": "DeployCode_15", + "colab": { + "base_uri": "https://localhost:8080/" + }, + "outputId": "cc4a242c-7b87-4084-81dc-6ec5cb106afc" + }, + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": [ + "🚀 Deploying to Vertex AI Agent Engine... (Bucket: gs://aniket-personal-vertex-staging)\n" + ] + }, + { + "output_type": "stream", + "name": "stderr", + "text": [ + "INFO:vertexai.reasoning_engines._reasoning_engines:Using bucket aniket-personal-vertex-staging\n", + "INFO:vertexai.reasoning_engines._reasoning_engines:Writing to gs://aniket-personal-vertex-staging/reasoning_engine/reasoning_engine.pkl\n", + "INFO:vertexai.reasoning_engines._reasoning_engines:Writing to gs://aniket-personal-vertex-staging/reasoning_engine/requirements.txt\n", + "INFO:vertexai.reasoning_engines._reasoning_engines:Creating in-memory tarfile of extra_packages\n", + "INFO:vertexai.reasoning_engines._reasoning_engines:Writing to gs://aniket-personal-vertex-staging/reasoning_engine/dependencies.tar.gz\n", + "INFO:vertexai.reasoning_engines._reasoning_engines:Creating ReasoningEngine\n", + "INFO:vertexai.reasoning_engines._reasoning_engines:Create ReasoningEngine backing LRO: projects/551887116707/locations/us-central1/reasoningEngines/8343273451959615488/operations/5496245304816566272\n", + "INFO:vertexai.reasoning_engines._reasoning_engines:ReasoningEngine created. Resource name: projects/551887116707/locations/us-central1/reasoningEngines/8343273451959615488\n", + "INFO:vertexai.reasoning_engines._reasoning_engines:To use this ReasoningEngine in another session:\n", + "INFO:vertexai.reasoning_engines._reasoning_engines:reasoning_engine = vertexai.preview.reasoning_engines.ReasoningEngine('projects/551887116707/locations/us-central1/reasoningEngines/8343273451959615488')\n" + ] + }, + { + "output_type": "stream", + "name": "stdout", + "text": [ + "✅ Deployed! Resource Name: projects/551887116707/locations/us-central1/reasoningEngines/8343273451959615488\n" + ] + } + ], + "source": [ + "from vertexai.preview import reasoning_engines\n", + "import vertexai\n", + "\n", + "STAGING_BUCKET = f\"gs://{PROJECT_ID}-vertex-staging\"\n", + "\n", + "# Configure the staging bucket globally\n", + "vertexai.init(project=PROJECT_ID, location=REGION, staging_bucket=STAGING_BUCKET)\n", + "\n", + "print(f\"🚀 Deploying to Vertex AI Agent Engine... (Bucket: {STAGING_BUCKET})\")\n", + "\n", + "# --- Wrapper to fix API registration and Context issues ---\n", + "class AgentWrapper:\n", + " \"\"\"Wrapper to expose stream_query and pass through all arguments.\"\"\"\n", + " def __init__(self, app, project_id: str, location: str):\n", + " self.app = app\n", + " self.project_id = project_id\n", + " self.location = location\n", + "\n", + " def stream_query(self, **kwargs):\n", + " # 1. Re-initialize Vertex AI context inside the remote execution\n", + " import vertexai\n", + " vertexai.init(project=self.project_id, location=self.location)\n", + "\n", + " # 2. Delegate to the app with all arguments (e.g. message, user_id)\n", + " return self.app.stream_query(**kwargs)\n", + "# ----------------------------------------------\n", + "\n", + "# Deployment\n", + "# Note: This requires the staging bucket to exist.\n", + "try:\n", + " # 1. Create a FRESH app instance\n", + " clean_app = reasoning_engines.AdkApp(agent=cyber_agent, enable_tracing=False)\n", + "\n", + " # 2. Wrap the clean app with explicit project/location\n", + " wrapped_app = AgentWrapper(clean_app, project_id=PROJECT_ID, location=REGION)\n", + "\n", + " remote_app = reasoning_engines.ReasoningEngine.create(\n", + " wrapped_app,\n", + " requirements=[\n", + " \"google-adk>=1.0.0\",\n", + " \"langchain-community\",\n", + " \"langchain-google-vertexai\",\n", + " \"langchain\",\n", + " \"neo4j\",\n", + " \"google-cloud-aiplatform>=1.97.0\"\n", + " ],\n", + " )\n", + " print(f\"✅ Deployed! Resource Name: {remote_app.resource_name}\")\n", + "except Exception as e:\n", + " print(f\"â„č Deployment skipped (Ensure GCS bucket exists): {e}\")" + ] + }, + { + "cell_type": "code", + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "678d36ef", + "outputId": "79de1914-05f5-488c-86ff-9d4403dcdd48" + }, + "source": [ + "import inspect\n", + "\n", + "# Inspect the 'app' object to see its methods and if they are async\n", + "print(\"App Type:\", type(app))\n", + "print(\"Methods in app:\", dir(app))\n", + "\n", + "if hasattr(app, 'query'):\n", + " print(\"Is 'query' async?\", inspect.iscoroutinefunction(app.query))\n", + "\n", + "if hasattr(app, 'stream_query'):\n", + " print(\"Is 'stream_query' async?\", inspect.iscoroutinefunction(app.stream_query))" + ], + "execution_count": 16, + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": [ + "App Type: \n", + "Methods in app: ['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getstate__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__slotnames__', '__str__', '__subclasshook__', '__weakref__', '_convert_response_events', '_init_session', '_telemetry_enabled', '_tmpl_attrs', '_tracing_enabled', '_warn_if_telemetry_api_disabled', 'agent_framework', 'async_add_session_to_memory', 'async_create_session', 'async_delete_session', 'async_get_session', 'async_list_sessions', 'async_search_memory', 'async_stream_query', 'bidi_stream_query', 'clone', 'create_session', 'delete_session', 'get_session', 'list_sessions', 'project_id', 'register_operations', 'set_up', 'stream_query', 'streaming_agent_run_with_events']\n", + "Is 'stream_query' async? False\n" + ] + } + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "4aeb6e9d" + }, + "source": [ + "### 7. Query the Deployed Agent\n", + "Now that the agent is running on Vertex AI, we can send it queries.\n", + "\n", + "**Note:** If you restart your session, you can reconnect to this agent using:\n", + "```python\n", + "remote_app = reasoning_engines.ReasoningEngine('YOUR_RESOURCE_NAME')\n", + "```" + ] + }, + { + "cell_type": "code", + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "e9cf1704", + "outputId": "bbb6b5be-ebe8-4ae1-ed0e-0b83f5f26b72" + }, + "source": [ + "print(\"💬 Querying Remote Agent: 'What CVEs are associated with WellMess?'\")\n", + "print(\"-\" * 40)\n", + "\n", + "# Query the deployed agent\n", + "# We provide user_id as AdkApp often requires session context\n", + "response = remote_app.stream_query(\n", + " message=\"What CVEs are associated with WellMess?\",\n", + " user_id=\"remote_analyst_01\"\n", + ")\n", + "\n", + "for chunk in response:\n", + " print(chunk)" + ], + "execution_count": 17, + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": [ + "💬 Querying Remote Agent: 'What CVEs are associated with WellMess?'\n", + "----------------------------------------\n", + "{'model_version': 'gemini-2.0-flash', 'content': {'parts': [{'function_call': {'id': 'adk-9b45e639-f6cb-473c-a12b-4ebc87dc4d2a', 'args': {'question': 'What CVEs are associated with WellMess?'}, 'name': 'query_threat_graph'}}], 'role': 'model'}, 'finish_reason': 'STOP', 'usage_metadata': {'candidates_token_count': 15, 'candidates_tokens_details': [{'modality': 'TEXT', 'token_count': 15}], 'prompt_token_count': 119, 'prompt_tokens_details': [{'modality': 'TEXT', 'token_count': 119}], 'total_token_count': 134, 'traffic_type': 'ON_DEMAND'}, 'avg_logprobs': -4.4566400659581026e-05, 'invocation_id': 'e-e66d3e09-f7ef-488e-9a39-21fbc5133012', 'author': 'CyberThreatIntel', 'actions': {'state_delta': {}, 'artifact_delta': {}, 'requested_auth_configs': {}, 'requested_tool_confirmations': {}}, 'long_running_tool_ids': [], 'id': '067a91ca-e1da-4c6b-a9c3-c66b7c5821b3', 'timestamp': 1770299736.027741}\n", + "{'content': {'parts': [{'function_response': {'id': 'adk-9b45e639-f6cb-473c-a12b-4ebc87dc4d2a', 'name': 'query_threat_graph', 'response': {'result': 'CVE-2023-1234 is associated with WellMess.\\n'}}}], 'role': 'user'}, 'invocation_id': 'e-e66d3e09-f7ef-488e-9a39-21fbc5133012', 'author': 'CyberThreatIntel', 'actions': {'state_delta': {}, 'artifact_delta': {}, 'requested_auth_configs': {}, 'requested_tool_confirmations': {}}, 'id': '6bbc55c1-5bc8-42bb-95bd-98f8ad77122b', 'timestamp': 1770299742.142327}\n", + "{'model_version': 'gemini-2.0-flash', 'content': {'parts': [{'text': 'CVE-2023-1234 is associated with WellMess.\\n'}], 'role': 'model'}, 'finish_reason': 'STOP', 'usage_metadata': {'candidates_token_count': 18, 'candidates_tokens_details': [{'modality': 'TEXT', 'token_count': 18}], 'prompt_token_count': 158, 'prompt_tokens_details': [{'modality': 'TEXT', 'token_count': 158}], 'total_token_count': 176, 'traffic_type': 'ON_DEMAND'}, 'avg_logprobs': -0.0005405148387783103, 'invocation_id': 'e-e66d3e09-f7ef-488e-9a39-21fbc5133012', 'author': 'CyberThreatIntel', 'actions': {'state_delta': {}, 'artifact_delta': {}, 'requested_auth_configs': {}, 'requested_tool_confirmations': {}}, 'id': '5252ebca-08fd-440c-bfd5-124d7ff93e1a', 'timestamp': 1770299742.269534}\n" + ] + } + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "2dd883a0" + }, + "source": [ + "### 8. Next Steps & Cleanup\n", + "\n", + "**1. Try a Complex Query**\n", + "Let's see if the agent can traverse multiple hops in the graph (Actor -> Target)." + ] + }, + { + "cell_type": "code", + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "b9d05b0a", + "outputId": "570f4f9e-d987-4379-b596-078112770895" + }, + "source": [ + "# Ask about targets\n", + "response = remote_app.stream_query(\n", + " message=\"Which threat actors target the Pharmaceuticals sector?\",\n", + " user_id=\"analyst_02\"\n", + ")\n", + "\n", + "for chunk in response:\n", + " print(chunk)" + ], + "execution_count": 18, + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": [ + "{'model_version': 'gemini-2.0-flash', 'content': {'parts': [{'function_call': {'id': 'adk-3c052b1b-5333-4aa9-9a9a-02463d302cec', 'args': {'question': 'Which threat actors target the Pharmaceuticals sector?'}, 'name': 'query_threat_graph'}}], 'role': 'model'}, 'finish_reason': 'STOP', 'usage_metadata': {'candidates_token_count': 14, 'candidates_tokens_details': [{'modality': 'TEXT', 'token_count': 14}], 'prompt_token_count': 118, 'prompt_tokens_details': [{'modality': 'TEXT', 'token_count': 118}], 'total_token_count': 132, 'traffic_type': 'ON_DEMAND'}, 'avg_logprobs': -9.134594750191485e-05, 'invocation_id': 'e-aa0661ca-89d6-4386-8013-ce2324f5e69a', 'author': 'CyberThreatIntel', 'actions': {'state_delta': {}, 'artifact_delta': {}, 'requested_auth_configs': {}, 'requested_tool_confirmations': {}}, 'long_running_tool_ids': [], 'id': '12305720-9f31-4f22-b299-b0212c7091db', 'timestamp': 1770299883.442706}\n", + "{'content': {'parts': [{'function_response': {'id': 'adk-3c052b1b-5333-4aa9-9a9a-02463d302cec', 'name': 'query_threat_graph', 'response': {'result': 'APT29 targets the Pharmaceuticals sector.\\n'}}}], 'role': 'user'}, 'invocation_id': 'e-aa0661ca-89d6-4386-8013-ce2324f5e69a', 'author': 'CyberThreatIntel', 'actions': {'state_delta': {}, 'artifact_delta': {}, 'requested_auth_configs': {}, 'requested_tool_confirmations': {}}, 'id': '85c3667b-1736-43a4-8e5e-31b643c8df17', 'timestamp': 1770299891.266381}\n", + "{'model_version': 'gemini-2.0-flash', 'content': {'parts': [{'text': 'APT29 targets the Pharmaceuticals sector.\\n'}], 'role': 'model'}, 'finish_reason': 'STOP', 'usage_metadata': {'candidates_token_count': 9, 'candidates_tokens_details': [{'modality': 'TEXT', 'token_count': 9}], 'prompt_token_count': 147, 'prompt_tokens_details': [{'modality': 'TEXT', 'token_count': 147}], 'total_token_count': 156, 'traffic_type': 'ON_DEMAND'}, 'avg_logprobs': -0.0003546736358354489, 'invocation_id': 'e-aa0661ca-89d6-4386-8013-ce2324f5e69a', 'author': 'CyberThreatIntel', 'actions': {'state_delta': {}, 'artifact_delta': {}, 'requested_auth_configs': {}, 'requested_tool_confirmations': {}}, 'id': 'f247f585-53d4-47df-9419-502c8d750aa0', 'timestamp': 1770299891.383679}\n" + ] + } + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "af07d9a5" + }, + "source": [ + "**2. How to Connect from Another App**\n", + "Use the `resource_name` printed in the deployment step to connect to this agent from any Python script." + ] + }, + { + "cell_type": "code", + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "6fe565a1", + "outputId": "85b5694f-b2b4-4a19-a572-cd66ae79b2f8" + }, + "source": [ + "# Example of connecting to an existing agent\n", + "# Replace RESOURCE_NAME with the output from DeployCode_15 (starts with projects/...)\n", + "agent = reasoning_engines.ReasoningEngine(remote_app.resource_name)\n", + "\n", + "# We must use stream_query and PROVIDE A USER_ID for session context\n", + "response = agent.stream_query(\n", + " message=\"Hello\",\n", + " user_id=\"external_user_01\" # Required by AdkApp\n", + ")\n", + "\n", + "for chunk in response:\n", + " print(chunk)" + ], + "execution_count": 23, + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": [ + "{'model_version': 'gemini-2.0-flash', 'content': {'parts': [{'text': \"Hello, I'm ready to assist you with cybersecurity threat intelligence. What threats, actors, or CVEs are you interested in today?\\n\"}], 'role': 'model'}, 'finish_reason': 'STOP', 'usage_metadata': {'candidates_token_count': 29, 'candidates_tokens_details': [{'modality': 'TEXT', 'token_count': 29}], 'prompt_token_count': 111, 'prompt_tokens_details': [{'modality': 'TEXT', 'token_count': 111}], 'total_token_count': 140, 'traffic_type': 'ON_DEMAND'}, 'avg_logprobs': -0.10933220797571643, 'invocation_id': 'e-e5e6ee44-e3ec-43bc-9325-0e8a1028d372', 'author': 'CyberThreatIntel', 'actions': {'state_delta': {}, 'artifact_delta': {}, 'requested_auth_configs': {}, 'requested_tool_confirmations': {}}, 'id': 'e174f6b3-e314-47f2-8a0d-c4baecec0ceb', 'timestamp': 1770300079.152798}\n" + ] + } + ] + }, + { + "cell_type": "markdown", + "source": [ + "**3. Interactive visualization:**\n", + "Following code block does the following:\n", + "\n", + "* Installs the `pyvis` visualization library.\n", + "* Creates a function to query the raw graph data.\n", + "* Renders an interactive network diagram of APT29 and its connections right here in the notebook.\n", + "\n", + "Graph Visualization Active! đŸ•žïž\n", + "\n", + "You can now see the interactive network of threats directly in your notebook.\n", + "\n", + "* **Red Nodes:** The Threat Actor (e.g., APT29).\n", + "* **Blue Nodes:** The connected entities (Malware, CVEs, Targets).\n", + "* **Edges:** The relationships (e.g., USES, EXPLOITS, TARGETS)." + ], + "metadata": { + "id": "OIk5tHj_IhgQ" + } + }, + { + "cell_type": "code", + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "4a2119f4", + "outputId": "ae1089e8-7403-4283-99c4-e9a6c671f9a2" + }, + "source": [ + "%pip install --quiet pyvis\n", + "print(\"✅ PyVis Installed\")" + ], + "execution_count": 24, + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": [ + "\u001b[?25l \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m0.0/756.0 kB\u001b[0m \u001b[31m?\u001b[0m eta \u001b[36m-:--:--\u001b[0m\r\u001b[2K \u001b[91m━━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[90mâ•ș\u001b[0m\u001b[90m━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m399.4/756.0 kB\u001b[0m \u001b[31m11.7 MB/s\u001b[0m eta \u001b[36m0:00:01\u001b[0m\r\u001b[2K \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m756.0/756.0 kB\u001b[0m \u001b[31m12.1 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m\n", + "\u001b[?25h\u001b[?25l \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m0.0/1.6 MB\u001b[0m \u001b[31m?\u001b[0m eta \u001b[36m-:--:--\u001b[0m\r\u001b[2K \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m1.6/1.6 MB\u001b[0m \u001b[31m48.8 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m\n", + "\u001b[?25h✅ PyVis Installed\n" + ] + } + ] + }, + { + "cell_type": "code", + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 575 + }, + "id": "3dc67aea", + "outputId": "c82b16cd-4220-4d73-83c8-bfa5924129f6" + }, + "source": [ + "from pyvis.network import Network\n", + "from IPython.display import HTML, display\n", + "from neo4j import GraphDatabase\n", + "\n", + "def visualize_attack_graph(root_node_name):\n", + " \"\"\"Queries Neo4j and creates an interactive graph visualization around a node.\"\"\"\n", + " # 1. Direct Connection (using the same credentials as before)\n", + " uri = \"neo4j+s://7d50da77.databases.neo4j.io\"\n", + " user = \"neo4j\"\n", + " pwd = \"nZasSfGzac_mgTApAqrZd_Yqty9I4HOXKyj8qeNKdYg\"\n", + "\n", + " driver = GraphDatabase.driver(uri, auth=(user, pwd))\n", + "\n", + " # 2. Setup PyVis Network\n", + " net = Network(notebook=True, cdn_resources='in_line', height=\"500px\", width=\"100%\", bgcolor=\"#222222\", font_color=\"white\")\n", + "\n", + " cypher = f\"\"\"\n", + " MATCH (n)-[r]-(m)\n", + " WHERE n.name = '{root_node_name}' OR n.alias = '{root_node_name}'\n", + " RETURN n, r, m\n", + " LIMIT 50\n", + " \"\"\"\n", + "\n", + " with driver.session() as session:\n", + " result = session.run(cypher)\n", + " for record in result:\n", + " src = record['n']\n", + " dst = record['m']\n", + " rel = record['r']\n", + "\n", + " # Helper to get a label for the node (handling different schemas)\n", + " def get_label(node):\n", + " return node.get('name') or node.get('cve') or node.get('sector') or 'Unknown'\n", + "\n", + " # Add Nodes\n", + " # We use element_id to uniquely identify nodes in the visualizer\n", + " net.add_node(src.element_id, label=get_label(src), title=str(src.labels), color='#ff4b4b') # Red for Source\n", + " net.add_node(dst.element_id, label=get_label(dst), title=str(dst.labels), color='#4b94ff') # Blue for Target\n", + "\n", + " # Add Edge\n", + " net.add_edge(src.element_id, dst.element_id, title=rel.type, label=rel.type, color='white')\n", + "\n", + " driver.close()\n", + "\n", + " # 3. Render\n", + " net.show('threat_graph.html')\n", + " return HTML('threat_graph.html')\n", + "\n", + "print(\"✹ Visualization Tool Created. Rendering graph for 'APT29'...\")\n", + "visualize_attack_graph(\"APT29\")" + ], + "execution_count": 25, + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": [ + "✹ Visualization Tool Created. Rendering graph for 'APT29'...\n", + "threat_graph.html\n" + ] + }, + { + "output_type": "execute_result", + "data": { + "text/plain": [ + "" + ], + "text/html": [ + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
    \n", + "

    \n", + "
    \n", + "\n", + "\n", + " \n", + " \n", + "\n", + "\n", + "
    \n", + "

    \n", + "
    \n", + " \n", + " \n", + "\n", + "\n", + " \n", + "
    \n", + " \n", + " \n", + "
    \n", + "
    \n", + "\n", + " \n", + " \n", + "\n", + " \n", + " \n", + "" + ] + }, + "metadata": {}, + "execution_count": 25 + } + ] + }, + { + "cell_type": "markdown", + "source": [ + "This feature turns the \"black box\" of the graph database into a clear, visual map for analysts.\n", + "\n", + "Mission Accomplished! We have:\n", + "\n", + "* Built a GraphRAG Agent.\n", + "* Deployed it to Vertex AI.\n", + "* Visualized the intelligence data.\n", + "\n", + "You are ready to hunt threats! đŸ›Ąïž" + ], + "metadata": { + "id": "7I20siP5J1jS" + } + }, + { + "cell_type": "markdown", + "metadata": { + "id": "70309767" + }, + "source": [ + "**4. Cleanup**\n", + "Delete the agent to avoid incurring costs." + ] + }, + { + "cell_type": "code", + "metadata": { + "id": "3be25141" + }, + "source": [ + "# Uncomment to delete the agent when finished\n", + "remote_app.delete()\n", + "print(\"đŸ—‘ïž Agent Deleted\")" + ], + "execution_count": 20, + "outputs": [] + } + ], + "metadata": { + "colab": { + "toc_visible": true, + "provenance": [], + "collapsed_sections": [ + "Install_03", + "Seeding_06", + "ToolDef_08", + "AgentDef_10", + "LocalTest_12", + "Deploy_14", + "4aeb6e9d" + ] + }, + "kernelspec": { + "display_name": "Python 3", + "name": "python3" + }, + "language_info": { + "name": "python" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} diff --git a/gemini/use-cases/graphrag/graph_rag_spanner_sdk_adk.ipynb b/gemini/use-cases/graphrag/graph_rag_spanner_sdk_adk.ipynb new file mode 100644 index 0000000..03089b8 --- /dev/null +++ b/gemini/use-cases/graphrag/graph_rag_spanner_sdk_adk.ipynb @@ -0,0 +1,1466 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "TQ1nQmSbn9co" + }, + "outputs": [], + "source": [ + "# Copyright 2025 Google LLC\n", + "#\n", + "# Licensed under the Apache License, Version 2.0 (the \"License\");\n", + "# you may not use this file except in compliance with the License.\n", + "# You may obtain a copy of the License at\n", + "#\n", + "# https://www.apache.org/licenses/LICENSE-2.0\n", + "#\n", + "# Unless required by applicable law or agreed to in writing, software\n", + "# distributed under the License is distributed on an \"AS IS\" BASIS,\n", + "# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n", + "# See the License for the specific language governing permissions and\n", + "# limitations under the License." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "skeDTD9moBK5" + }, + "source": [ + "# GraphRAG on Google Cloud With Spanner and Vertex AI Agent Engine" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "LAoUEttbd9pg" + }, + "source": [ + "|Author(s) | [Tristan Li](https://github.com/codingphun), [Smitha Venkat](https://github.com/smitha-google)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "tC8nObmLw9P0" + }, + "source": [ + "## Overview\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "BpH_CYLqPzLw" + }, + "source": [ + "Spanner Graph now [integrates seamlessly with LangChain](https://cloud.google.com/python/docs/reference/langchain-google-spanner/latest#spanner-graph-store-usage), making it easier to build GraphRAG applications.\n", + "\n", + "Instead of simply retrieving relevant text snippets based on keyword similarity, GraphRAG takes a more sophisticated, structured approach to Retrieval Augmented Generation. It involves creating a knowledge graph from the text, organizing it hierarchically, summarizing key concepts, and then using this structured information to enhance the accuracy and depth of responses.\n", + "\n", + "\n", + "### Objectives\n", + "\n", + "In this tutorial, you will see a complete walkthrough of building a question-answering system using the GraphRAG method. You'll learn how to create a knowledge graph from scratch, store it efficiently in Spanner Graph, a functional FAQ system with Langchain agent." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "ZyXBLYuod9pj" + }, + "source": [ + "## Before you begin" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "C5wHkD06RzZN" + }, + "source": [ + "1. In the Google Cloud console, on the project selector page, select or [create a Google Cloud project](https://cloud.google.com/resource-manager/docs/creating-managing-projects).\n", + "1. [Make sure that billing is enabled for your Google Cloud project](https://cloud.google.com/billing/docs/how-to/verify-billing-enabled#console).\n", + "1. [Make sure Cloud Spanner API is enabled](https://console.cloud.google.com/flows/enableapi?apiid=spanner.googleapis.com)\n", + "\n", + "### Required roles\n", + "\n", + "To get the permissions that you need to complete the tutorial, ask your administrator to grant you the [Owner](https://cloud.google.com/iam/docs/understanding-roles#owner) (`roles/owner`) IAM role on your project. For more information about granting roles, see [Manage access](https://cloud.google.com/iam/docs/granting-changing-revoking-access).\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "arJM4CK4r6cj" + }, + "source": [ + "### Install Python Libraries" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "yhWJlYAmXSUq" + }, + "outputs": [], + "source": [ + "%pip install --quiet json-repair==0.30.2 networkx==3.3 langchain-core==0.3.59 langchain-google-vertexai==2.0.22 langchain-experimental==0.3.4 langchain-community==0.3.24 langchain-text-splitters==0.3.8\n", + "%pip install --quiet google-cloud-resource-manager==1.13.1 pydantic==2.9.2\n", + "%pip install --quiet google-cloud-spanner==3.48.0\n", + "%pip install --quiet langchain-google-spanner==0.8.2\n", + "%pip install --quiet google-adk==0.5.0\n", + "%pip install --quiet google-cloud-aiplatform==1.91.0" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "DoK1gW9dsRxB" + }, + "source": [ + "### Authenticating your notebook environment\n", + "* If you are using **Colab** to run this notebook, uncomment the cell below and continue.\n", + "* If you are using **Vertex AI Workbench**, check out the setup instructions [here](https://github.com/GoogleCloudPlatform/generative-ai/tree/main/setup-env)." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": { + "id": "os3H39sGXugN" + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "3.11.12 (main, Apr 9 2025, 08:55:54) [GCC 11.4.0]\n" + ] + } + ], + "source": [ + "import sys\n", + "\n", + "if \"google.colab\" in sys.modules:\n", + " from google.colab import auth as google_auth\n", + "\n", + " google_auth.authenticate_user()\n", + "print(sys.version)\n", + "# If using local jupyter instance, uncomment and run:\n", + "# !gcloud auth login" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "CzaWjgsqsuuu" + }, + "source": [ + "## Initialize and Import" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": { + "id": "zjz6i4vAXvZg" + }, + "outputs": [], + "source": [ + "# fmt: off\n", + "GCP_PROJECT_ID = \"\" # @param {type:\"string\"}\n", + "REGION = \"us-central1\" # @param {type:\"string\"}\n", + "MODEL_NAME = \"gemini-2.0-flash-001\" # @param {type:\"string\"}\n", + "EMBEDDING_MODEL_NAME = \"text-embedding-004\" # @param {type:\"string\"}\n", + "TASK_TYPE = \"SEMANTIC_SIMILARITY\" # @param {type:\"string\"}\n", + "ANSWER_TASK_TYPE = \"RETRIEVAL_DOCUMENT\" # @param {type:\"string\"}\n", + "SPANNER_INSTANCE_ID = \"graphrag-instance\" # @param {type:\"string\"}\n", + "SPANNER_DATABASE_ID = \"graphrag\" # @param {type:\"string\"}\n", + "SPANNER_GRAPH_NAME = \"wikigraph\" # @param {type:\"string\"}\n", + "# fmt: on" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "cXDp5uwyd9pk" + }, + "outputs": [], + "source": [ + "# Set the project id\n", + "!gcloud config set project {GCP_PROJECT_ID} --quiet\n", + "%env GOOGLE_CLOUD_PROJECT={GCP_PROJECT_ID}" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "GHyZn3Lns9YM" + }, + "source": [ + "### Import Packages" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": { + "id": "mCQuCvfrXxUM" + }, + "outputs": [], + "source": [ + "import os\n", + "\n", + "from langchain_core.documents import Document\n", + "from langchain_experimental.graph_transformers import LLMGraphTransformer\n", + "from langchain_google_vertexai import VertexAI\n", + "from langchain_text_splitters import RecursiveCharacterTextSplitter" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "Axb7c8Y1YmQ8" + }, + "source": [ + "## Create Spanner Instance and Database\n", + "\n", + "To prepare for future queries, we'll now store our newly created knowledge graph in a Google Cloud Spanner database. We'll also store the accompanying embeddings in Spanner's Vector Database to enable efficient semantic search.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": { + "id": "27gTtXr4m2n2" + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Updated property [core/project].\n" + ] + } + ], + "source": [ + "!gcloud config set project {GCP_PROJECT_ID}\n", + "!gcloud services enable spanner.googleapis.com\n", + "!gcloud spanner instances create {SPANNER_INSTANCE_ID} --config=regional-us-central1 --description=\"Graph RAG Instance\" --nodes=1 --edition=ENTERPRISE" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": { + "id": "jsoqBPlDthc7" + }, + "outputs": [], + "source": [ + "# prompt: create a spanner database and table to store the graph with nodes and edges created in graph\n", + "\n", + "\n", + "def create_database(project_id, instance_id, database_id):\n", + " \"\"\"Creates a database and tables for sample data.\"\"\"\n", + " from google.cloud import spanner\n", + " from google.cloud.spanner_admin_database_v1.types import spanner_database_admin\n", + "\n", + " spanner_client = spanner.Client(project_id)\n", + " database_admin_api = spanner_client.database_admin_api\n", + "\n", + " request = spanner_database_admin.CreateDatabaseRequest(\n", + " parent=database_admin_api.instance_path(spanner_client.project, instance_id),\n", + " create_statement=f\"CREATE DATABASE `{database_id}`\",\n", + " extra_statements=[\n", + " \"\"\"CREATE TABLE KgNode (\n", + " DocId INT64 NOT NULL,\n", + " Name STRING(1024),\n", + " DOC STRING(1024),\n", + " DocEmbedding ARRAY\n", + " ) PRIMARY KEY (DocId)\"\"\"\n", + " ],\n", + " )\n", + "\n", + " operation = database_admin_api.create_database(request=request)\n", + "\n", + " print(\"Waiting for operation to complete...\")\n", + " OPERATION_TIMEOUT_SECONDS = 60\n", + " database = operation.result(OPERATION_TIMEOUT_SECONDS)\n", + "\n", + " print(\n", + " f\"Created database {database.name} on instance {database_admin_api.instance_path(spanner_client.project, instance_id)}\"\n", + " )" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "7mVK78UWt2OJ" + }, + "outputs": [], + "source": [ + "from google.cloud import spanner\n", + "\n", + "create_database(GCP_PROJECT_ID, SPANNER_INSTANCE_ID, SPANNER_DATABASE_ID)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "xSuWYhb62UJS" + }, + "source": [ + "## Create a Knowledge Graph With LangChain and Gemini\n", + "\n", + "These texts extracted from Wikipedia are about [Larry Page](https://en.wikipedia.org/wiki/Larry_Page), co-founder of Google. These texts will be used to create a knowledge graph about Larry Page as well as embedding vectors for semantic search." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": { + "id": "NIokGkZii43z" + }, + "outputs": [], + "source": [ + "text = \"\"\"Lawrence Edward Page (born March 26, 1973) is an American businessman and computer scientist best known for co-founding Google with Sergey Brin.\n", + "Lawrence Edward Page was chief executive officer of Google from 1997 until August 2001 when he stepped down in favor of Eric Schmidt,\n", + "and then again from April 2011 until July 2015 when he became CEO of its newly formed parent organization Alphabet Inc.\n", + "He held that post until December 4, 2019, when he and Brin stepped down from all executive positions and day-to-day roles within the company.\n", + "He remains an Alphabet board member, employee, and controlling shareholder. Lawrence Edward Page has an estimated net worth of $156 billion as of June 2024,\n", + "according to the Bloomberg Billionaires Index, and $145.2 billion according to Forbes, making him the fifth-richest person in the world.\n", + "He has also invested in flying car startups Kitty Hawk and Opener. Like his Google co-founder, Sergey Brin, Page attended Montessori schools until he entered high school.\n", + "They both cite the educational method of Maria Montessori as the major influence in how they designed Google's work systems.\n", + "Maria Montessori believed that the liberty of the child was of utmost importance. In some sense, I feel like music training led to the high-speed legacy of Google for me\"\"\"" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "YDpwl48k27HU" + }, + "source": [ + "We will use Gemini and Langchain LLMGraphTransformer to parse the texts and generate a knowledge graph.\n", + "\n", + "Leveraging Gemini's capabilities, Langchain will use them to identify and extract key information from the text, such as people, countries, and their nationalities, to construct a comprehensive knowledge graph from the texts based on the nodes and relationships we define." + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": { + "id": "ZIcYPheujsmc" + }, + "outputs": [], + "source": [ + "documents = [Document(page_content=text)]\n", + "\n", + "llm = VertexAI(model_name=MODEL_NAME, project=GCP_PROJECT_ID, location=REGION)\n", + "\n", + "llm_transformer_filtered = LLMGraphTransformer(\n", + " llm=llm,\n", + " allowed_nodes=[\"Person\", \"Country\", \"Organization\", \"Asset\"],\n", + " allowed_relationships=[\n", + " \"NATIONALITY\",\n", + " \"LOCATED_IN\",\n", + " \"WORKED_AT\",\n", + " \"SPOUSE\",\n", + " \"NET_WORTH\",\n", + " \"INVESTMENT\",\n", + " \"INFLUENCED_BY\",\n", + " ],\n", + ")\n", + "graph_documents_filtered = llm_transformer_filtered.convert_to_graph_documents(\n", + " documents\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "QHXfcXadd9pl" + }, + "source": [ + "## Store the Knowledge Graph in Spanner\n", + "\n", + "Now that the Spanner database is created, we will store the Knowledge Graph in the Spanner Graph Store" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": { + "id": "JrGjHvf0j7vu" + }, + "outputs": [], + "source": [ + "from langchain_google_spanner import SpannerGraphStore\n", + "\n", + "graph_store = SpannerGraphStore(\n", + " instance_id=SPANNER_INSTANCE_ID,\n", + " database_id=SPANNER_DATABASE_ID,\n", + " graph_name=SPANNER_GRAPH_NAME,\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": { + "id": "t1fV5Y2Ad9pl" + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Waiting for DDL operations to complete...\n", + "Waiting for DDL operations to complete...\n", + "Insert nodes of type `Asset`...\n", + "Insert nodes of type `Organization`...\n", + "Insert nodes of type `Person`...\n", + "Insert nodes of type `Country`...\n", + "Insert edges of type `Person_WORKED_AT_Organization`...\n", + "Insert edges of type `Person_NATIONALITY_Country`...\n", + "Insert edges of type `Person_NET_WORTH_Asset`...\n", + "Insert edges of type `Person_INVESTMENT_Organization`...\n", + "Insert edges of type `Person_INFLUENCED_BY_Person`...\n" + ] + } + ], + "source": [ + "# Uncomment the line below, if you want to cleanup from previous iterations.\n", + "# BeWARE - THIS COULD REMOVE DATA FROM YOUR DATABASE !!!\n", + "graph_store.cleanup()\n", + "\n", + "for graph_document in graph_documents_filtered:\n", + " graph_store.add_graph_documents([graph_document])" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "aXIongn2d9pl" + }, + "source": [ + "## Build a QnA Agent\n", + "\n", + "Let's build a QnA agent and ask quick questions" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": { + "id": "cydr7r-gd9pl" + }, + "outputs": [], + "source": [ + "from langchain_google_spanner import SpannerGraphQAChain\n", + "from langchain_google_vertexai import ChatVertexAI\n", + "\n", + "# Initialize llm object\n", + "llm = ChatVertexAI(model=MODEL_NAME, temperature=0)\n", + "\n", + "# Initialize GraphQAChain\n", + "chain = SpannerGraphQAChain.from_llm(\n", + " llm,\n", + " graph=graph_store,\n", + " allow_dangerous_requests=True,\n", + " verbose=False,\n", + " return_intermediate_steps=True,\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": { + "id": "SVJEGkmZd9pm" + }, + "outputs": [ + { + "data": { + "application/vnd.google.colaboratory.intrinsic+json": { + "type": "string" + }, + "text/plain": [ + "'Lawrence Edward Page invests in Kitty Hawk and Opener.\\n'" + ] + }, + "execution_count": 13, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# fmt: off\n", + "question = \"What businesses does Lawrence Edward Page invest in?\" # @param {type:\"string\"}\n", + "# fmt: on\n", + "response = chain.invoke(\"query=\" + question)\n", + "response[\"result\"]" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "RrLJ3I-9aULZ" + }, + "source": [ + "**Important: Magic cell below only works on Colab**" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "AeLNbYT0erih" + }, + "outputs": [], + "source": [ + "%%spanner_graph --project {GCP_PROJECT_ID} --instance {SPANNER_INSTANCE_ID} --database {SPANNER_DATABASE_ID}\n", + "\n", + "GRAPH wikigraph\n", + "MATCH p = (a)-[e]->(b)\n", + "RETURN TO_JSON(p) AS path_json\n", + "LIMIT 50" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "e4lDWxd8Uzvr" + }, + "source": [ + "## Enhance Search Capability" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "38aY1a-Dd9pm" + }, + "source": [ + "Now if we rephrase the question with \"Larry Page\" instead of his legal name \" Lawrence Edward Page\", it would fail because it relies on exact keyword matching. Semantic search, using embeddings and vector search, overcomes this by understanding the meaning and relationships between words, thus recognizing both names refer to the same person! On the flip side, if we only use semantic search with vector similarity search, the results returned are not always accurate. See examples below on the challenges of either approach." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "52o_qCYpb1Ut" + }, + "source": [ + "#### Generate the embeddings" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": { + "id": "x7XGJrb2d9pm" + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[0.04362960532307625, -0.015628624707460403, -0.03651438280940056, 0.07635214179754257, 0.025854211300611496, 0.016094792634248734, 0.032845646142959595, -0.025009311735630035, 0.033768218010663986, 0.0557292141020298] Lawrence E Lawrence Edward Page, Sergey Brin, Eric Schmidt\n", + "[0.05250556766986847, 0.0059433588758111, -0.053659338504076004, 0.0366397500038147, 0.027847468852996826, 0.006960400380194187, 0.0055380454286932945, -0.03739484027028084, 0.05096553638577461, 0.06589022278785706] He held th Brin, Lawrence Edward Page\n", + "[0.00012151900591561571, -0.04247371852397919, -0.06008194014430046, 0.08208870142698288, 0.022667380049824715, 0.015254673548042774, 0.03223690763115883, -0.016212889924645424, 0.02410755306482315, 0.021432554349303246] He has als Sergey Brin, Page, Maria Montessori\n" + ] + } + ], + "source": [ + "import json\n", + "\n", + "import vertexai\n", + "from vertexai.generative_models import GenerationConfig, GenerativeModel\n", + "from vertexai.language_models import TextEmbeddingInput, TextEmbeddingModel\n", + "\n", + "# init the vertexai package\n", + "vertexai.init(project=GCP_PROJECT_ID, location=REGION)\n", + "\n", + "\n", + "def get_embedding(text, task_type, model):\n", + " try:\n", + " text_embedding_input = TextEmbeddingInput(task_type=task_type, text=text)\n", + " embeddings = model.get_embeddings([text_embedding_input])\n", + " return embeddings[0].values\n", + " except:\n", + " return []\n", + "\n", + "\n", + "embedding_model = TextEmbeddingModel.from_pretrained(EMBEDDING_MODEL_NAME)\n", + "text_model = GenerativeModel(MODEL_NAME)\n", + "documents = [Document(page_content=text)]\n", + "\n", + "spanner_embedding_values = []\n", + "\n", + "from langchain.text_splitter import RecursiveCharacterTextSplitter\n", + "\n", + "splitter = RecursiveCharacterTextSplitter(\n", + " chunk_size=500, chunk_overlap=100, separators=[\"\\n\\n\", \"\\n\", r\"(?<=\\. )\", \" \", \"\"]\n", + ")\n", + "splitted_text = splitter.split_documents(documents)\n", + "for chunk in splitted_text:\n", + " chunk_content = chunk.page_content\n", + " embedding = get_embedding(chunk_content, ANSWER_TASK_TYPE, embedding_model)\n", + " user_prompt_content = (\n", + " \"Find person's names but ignore any pronoun in the following sentence \\n\"\n", + " + chunk_content\n", + " )\n", + " response = text_model.generate_content(\n", + " user_prompt_content,\n", + " generation_config=GenerationConfig(\n", + " temperature=0,\n", + " response_mime_type=\"application/json\",\n", + " response_schema={\n", + " \"type\": \"OBJECT\",\n", + " \"properties\": {\"nodes\": {\"type\": \"STRING\"}},\n", + " },\n", + " ),\n", + " )\n", + " response_content = json.loads(response.candidates[0].content.parts[0].text)[\"nodes\"]\n", + " print(embedding[:10], chunk_content[:10], response_content)\n", + " spanner_embedding_values.append([embedding, chunk_content, response_content])" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "pADGrRrKnB4n" + }, + "source": [ + "#### Knowledge graph search only - no result found" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": { + "id": "Vr6aAs2Qd9pm" + }, + "outputs": [ + { + "data": { + "application/vnd.google.colaboratory.intrinsic+json": { + "type": "string" + }, + "text/plain": [ + "\"I don't know the answer.\"" + ] + }, + "execution_count": 15, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# fmt: off\n", + "question = \"What businesses does Larry Page invest in?\" # @param {type:\"string\"}\n", + "# fmt: on\n", + "response = chain.invoke(\"query=\" + question)\n", + "response[\"result\"]" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "NUZTMinum163" + }, + "source": [ + "#### Semantic search only - top similarity match is incorrect" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": { + "id": "4HWdbakKeJvi" + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "chunk: Lawrence Edward Page (born March 26, 1973) is an American businessman and computer scientist best known for co-founding Google with Sergey Brin.\n", + "Lawrence Edward Page was chief executive officer of Google from 1997 until August 2001 when he stepped down in favor of Eric Schmidt,\n", + "and then again from April 2011 until July 2015 when he became CEO of its newly formed parent organization Alphabet Inc. \n", + " similarity: [[0.78622893]] \n", + "\n", + "chunk: He held that post until December 4, 2019, when he and Brin stepped down from all executive positions and day-to-day roles within the company.\n", + "He remains an Alphabet board member, employee, and controlling shareholder. Lawrence Edward Page has an estimated net worth of $156 billion as of June 2024,\n", + "according to the Bloomberg Billionaires Index, and $145.2 billion according to Forbes, making him the fifth-richest person in the world. \n", + " similarity: [[0.81792139]] \n", + "\n", + "chunk: He has also invested in flying car startups Kitty Hawk and Opener. Like his Google co-founder, Sergey Brin, Page attended Montessori schools until he entered high school.\n", + "They both cite the educational method of Maria Montessori as the major influence in how they designed Google's work systems.\n", + "Maria Montessori believed that the liberty of the child was of utmost importance. In some sense, I feel like music training led to the high-speed legacy of Google for me \n", + " similarity: [[0.78612123]] \n", + "\n" + ] + } + ], + "source": [ + "from sklearn.metrics.pairwise import cosine_similarity\n", + "\n", + "# fmt: off\n", + "QUESTION = \"What businesses does Larry Page invest in?\" # @param {type:\"string\"}\n", + "# fmt: on\n", + "\n", + "q_emb = get_embedding(QUESTION, ANSWER_TASK_TYPE, embedding_model)\n", + "\n", + "for emb in spanner_embedding_values:\n", + " print(f\"chunk: {emb[1]} \\n similarity: {cosine_similarity([q_emb], [emb[0]])} \\n\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "4OVOZZ1Ld9pm" + }, + "source": [ + "#### Save the embeddings into Spanner" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": { + "id": "2K4tCvssd9pm" + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "1 record(s) inserted.\n", + "1 record(s) inserted.\n", + "1 record(s) inserted.\n" + ] + } + ], + "source": [ + "spanner_client = spanner.Client(GCP_PROJECT_ID)\n", + "instance = spanner_client.instance(SPANNER_INSTANCE_ID)\n", + "database = instance.database(SPANNER_DATABASE_ID)\n", + "\n", + "\n", + "def insert_values(transaction):\n", + " value1 = 0\n", + " for sub_list in spanner_embedding_values:\n", + " table_name = \"KgNode\"\n", + " col_name1 = \"docID\"\n", + " col_name2 = \"Name\"\n", + " col_name3 = \"Doc\"\n", + " col_name4 = \"DocEmbedding\"\n", + " value1 += 1\n", + " value2 = sub_list[2]\n", + " value3 = sub_list[1]\n", + " value4 = sub_list[0]\n", + " # print(col_name1, col_name2, col_name3, col_name4, value1, value2, value3, value4[:10])\n", + " row_ct1 = transaction.execute_update(\n", + " f\"INSERT INTO {table_name} ({col_name1}, {col_name2}, {col_name3}, {col_name4}) VALUES (@value1, @value2, @value3, @value4)\",\n", + " params={\n", + " \"value1\": value1,\n", + " \"value2\": value2,\n", + " \"value3\": value3,\n", + " \"value4\": value4,\n", + " },\n", + " param_types={\n", + " \"value1\": spanner.param_types.INT64,\n", + " \"value2\": spanner.param_types.STRING,\n", + " \"value3\": spanner.param_types.STRING,\n", + " \"value4\": spanner.param_types.Array(spanner.param_types.FLOAT64),\n", + " },\n", + " ) # Adjust types if needed\n", + "\n", + " print(f\"{row_ct1} record(s) inserted.\")\n", + "\n", + "\n", + "# print(insert_values) # This just prints the function object, remove this line\n", + "database.run_in_transaction(insert_values)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "ycefZwNfYTVK" + }, + "source": [ + "#### Combine both semantic search and graph search. Ask the question again - correct answer!\n" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "metadata": { + "id": "nD6gyU1bd9pm" + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "What businesses does Lawrence Edward Page invest in?\n" + ] + }, + { + "data": { + "application/vnd.google.colaboratory.intrinsic+json": { + "type": "string" + }, + "text/plain": [ + "'Lawrence Edward Page invests in Kitty Hawk and Opener.\\n'" + ] + }, + "execution_count": 19, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "import json\n", + "\n", + "import vertexai\n", + "from vertexai.generative_models import GenerationConfig, GenerativeModel\n", + "from vertexai.language_models import TextEmbeddingModel\n", + "\n", + "# fmt: off\n", + "QUESTION = \"What businesses does Larry Page invest in?\" # @param {type:\"string\"}\n", + "# fmt: on\n", + "q_emb = get_embedding(QUESTION, TASK_TYPE, embedding_model)\n", + "\n", + "spanner_client = spanner.Client(GCP_PROJECT_ID)\n", + "instance = spanner_client.instance(SPANNER_INSTANCE_ID)\n", + "database = instance.database(SPANNER_DATABASE_ID)\n", + "kgnodename = \"\"\n", + "with database.snapshot() as snapshot:\n", + " results = snapshot.execute_sql(\n", + " \"\"\"SELECT DocId, NAME, Doc FROM KgNode ORDER BY COSINE_DISTANCE(DocEmbedding, @q_emb) limit 1\"\"\",\n", + " params={\"q_emb\": q_emb},\n", + " param_types={\n", + " \"q_emb\": spanner.param_types.Array(spanner.param_types.FLOAT64)\n", + " }, # Adjust FLOAT64 if needed\n", + " )\n", + " for row in results:\n", + " kgnodename = str(row[1])\n", + "\n", + "text_model = GenerativeModel(MODEL_NAME)\n", + "user_prompt_content = (\n", + " \"Find and replace entities such as person's name, place, nationality in the following sentence \\n\"\n", + " + QUESTION\n", + " + \"with entities defined below \\n\"\n", + " + kgnodename\n", + " + \"\\n only replace matching person's name \\n output the best replacement in a string\"\n", + ")\n", + "# print (user_prompt_content)\n", + "response = text_model.generate_content(\n", + " user_prompt_content,\n", + " generation_config=GenerationConfig(\n", + " temperature=0,\n", + " response_mime_type=\"application/json\",\n", + " response_schema={\n", + " \"type\": \"OBJECT\",\n", + " \"properties\": {\"sentence\": {\"type\": \"STRING\"}},\n", + " },\n", + " ),\n", + ")\n", + "response_content = json.loads(response.candidates[0].content.parts[0].text)[\"sentence\"]\n", + "print(response_content)\n", + "response = chain.invoke(\"query=\" + response_content)\n", + "response[\"result\"]" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "FBI5WJcgIYUM" + }, + "source": [ + "## Agent Development Kit\n", + "\n", + "The [Agent Development Kit (ADK)](https://google.github.io/adk-docs/) is a flexible, modular framework for developing and deploying AI agents, designed to be model-agnostic, deployment-agnostic, and compatible with other frameworks, despite its optimization for Gemini and the Google ecosystem.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "metadata": { + "id": "PizppsQqJN52" + }, + "outputs": [], + "source": [ + "import json\n", + "\n", + "import vertexai\n", + "from google.adk.agents import Agent\n", + "from google.cloud import spanner\n", + "from langchain_google_spanner import SpannerGraphStore\n", + "from vertexai.generative_models import (\n", + " GenerationConfig,\n", + " GenerativeModel,\n", + ")\n", + "from vertexai.language_models import TextEmbeddingModel\n", + "\n", + "os.environ[\"GOOGLE_GENAI_USE_VERTEXAI\"] = \"1\"\n", + "os.environ[\"GOOGLE_CLOUD_PROJECT\"] = GCP_PROJECT_ID\n", + "os.environ[\"GOOGLE_CLOUD_LOCATION\"] = REGION" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "5I_sVhDrLfO8" + }, + "source": [ + "#### Create helper functions for function calling\n", + "\n", + "We will create a couple python functions for function calling for the agent. The first function is to generate embeddings of the user query. The second function is to query the Spanner database combining both semantic search and graph search." + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "metadata": { + "id": "jcbBn4teLqVz" + }, + "outputs": [], + "source": [ + "def ask_graph(query: str) -> dict:\n", + " from google.cloud import spanner\n", + " from langchain_google_spanner import SpannerGraphQAChain, SpannerGraphStore\n", + " from langchain_google_vertexai import ChatVertexAI\n", + "\n", + " GCP_PROJECT_ID = \"\"\n", + " REGION = \"us-central1\"\n", + " MODEL_NAME = \"gemini-2.0-flash-001\"\n", + " EMBEDDING_MODEL_NAME = \"text-embedding-004\"\n", + " TASK_TYPE = \"SEMANTIC_SIMILARITY\"\n", + " SPANNER_INSTANCE_ID = \"graphrag-instance\"\n", + " SPANNER_DATABASE_ID = \"graphrag\"\n", + " SPANNER_GRAPH_NAME = \"wikigraph\"\n", + "\n", + " graph_store = SpannerGraphStore(\n", + " instance_id=SPANNER_INSTANCE_ID,\n", + " database_id=SPANNER_DATABASE_ID,\n", + " graph_name=SPANNER_GRAPH_NAME,\n", + " )\n", + "\n", + " # Initialize llm object\n", + " llm = ChatVertexAI(model=MODEL_NAME, temperature=0)\n", + "\n", + " # Initialize GraphQAChain\n", + " chain = SpannerGraphQAChain.from_llm(\n", + " llm,\n", + " graph=graph_store,\n", + " allow_dangerous_requests=True,\n", + " verbose=False,\n", + " return_intermediate_steps=True,\n", + " )\n", + "\n", + " embedding_model = TextEmbeddingModel.from_pretrained(EMBEDDING_MODEL_NAME)\n", + " text_embedding_input = TextEmbeddingInput(task_type=TASK_TYPE, text=query)\n", + " q_emb = embedding_model.get_embeddings([text_embedding_input])[0].values\n", + " spanner_client = spanner.Client(GCP_PROJECT_ID)\n", + " instance = spanner_client.instance(SPANNER_INSTANCE_ID)\n", + " database = instance.database(SPANNER_DATABASE_ID)\n", + " kgnodename = \"\"\n", + " with database.snapshot() as snapshot:\n", + " results = snapshot.execute_sql(\n", + " \"\"\"SELECT DocId, NAME, Doc FROM KgNode ORDER BY COSINE_DISTANCE(DocEmbedding, @q_emb) limit 1\"\"\",\n", + " params={\"q_emb\": q_emb},\n", + " param_types={\n", + " \"q_emb\": spanner.param_types.Array(spanner.param_types.FLOAT64)\n", + " }, # Adjust FLOAT64 if needed\n", + " )\n", + " for row in results:\n", + " kgnodename = str(row[1])\n", + "\n", + " text_model = GenerativeModel(MODEL_NAME)\n", + " user_prompt_content = (\n", + " \"Find and replace entities such as person's name, place, nationality in the following sentence \\n\"\n", + " + query\n", + " + \"with entities defined below \\n\"\n", + " + kgnodename\n", + " + \"\\n only replace matching person's name \\n output the best replacement in a string\"\n", + " )\n", + " response = text_model.generate_content(\n", + " user_prompt_content,\n", + " generation_config=GenerationConfig(\n", + " temperature=0,\n", + " response_mime_type=\"application/json\",\n", + " response_schema={\n", + " \"type\": \"OBJECT\",\n", + " \"properties\": {\"sentence\": {\"type\": \"STRING\"}},\n", + " },\n", + " ),\n", + " )\n", + " response_content = json.loads(response.candidates[0].content.parts[0].text)[\n", + " \"sentence\"\n", + " ]\n", + " response = chain.invoke(\"query=\" + response_content)\n", + " response[\"result\"]\n", + " return response[\"result\"]" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "2O_mNmCLLvu1" + }, + "source": [ + "#### Create an ADK Agent\n", + "\n", + "Using the Google Agent Development Kit, we will create an agent and give it high level instructions on how to interact with users. ADK does have [built in tools](https://google.github.io/adk-docs/tools/built-in-tools/#how-to-use) for leveraging Google Search, Code Execution and Vertex AI Search. " + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "metadata": { + "id": "M6KOhbcNL18b" + }, + "outputs": [], + "source": [ + "from google.adk.tools import agent_tool, google_search\n", + "\n", + "search_agent = Agent(\n", + " model=\"gemini-2.0-flash-001\",\n", + " name=\"SearchAgent\",\n", + " instruction=\"\"\"\n", + " You're a specialist in Google Search\n", + " \"\"\",\n", + " tools=[google_search],\n", + ")\n", + "\n", + "root_agent = Agent(\n", + " name=\"graph_rag_agent\",\n", + " model=\"gemini-2.0-flash-001\",\n", + " description=(\n", + " \"Agent to answer questions from a graph database and google search if information not present in the database.\"\n", + " ),\n", + " instruction=(\n", + " \"\"\"You are a helpful information retrieval agent that can answer user's query from the\n", + " knowledge graph and do a broader search if you cant find answer in the graph database.\n", + " - After you get the user query, always check the graph database first.\n", + " - If the query can be answered from the graph, then call the ask_graph tool.\n", + " - If you are not able to find the answer in the graph, ask the user if they would like to do\n", + " a broader search.\n", + " - If the user says yes, then call the google_search tool.\n", + " - If the user says no, then ask them if there is anything else they would like to know.\n", + " - Always be courteous and dont assume anything.\n", + " - If you dont know an answer, please say I dont know the answer.\"\"\"\n", + " ),\n", + " tools=[agent_tool.AgentTool(agent=search_agent), ask_graph],\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "6r8KU3XXMCL2" + }, + "source": [ + "#### Test the ADK Agent locally\n", + "\n", + "We will create a session for the agent to run in" + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "metadata": { + "id": "0yq6gc5NMEUd" + }, + "outputs": [], + "source": [ + "from google.adk.agents import Agent\n", + "from google.adk.runners import Runner\n", + "from google.adk.sessions import InMemorySessionService\n", + "from google.adk.tools import google_search\n", + "from google.genai import types\n", + "\n", + "APP_NAME = \"google_search_agent\"\n", + "USER_ID = \"user1234\"\n", + "SESSION_ID = \"1234\"\n", + "\n", + "session_service = InMemorySessionService()\n", + "session = session_service.create_session(\n", + " app_name=APP_NAME, user_id=USER_ID, session_id=SESSION_ID\n", + ")\n", + "runner = Runner(agent=root_agent, app_name=APP_NAME, session_service=session_service)\n", + "\n", + "# Agent Interaction\n", + "\n", + "\n", + "def call_agent(query):\n", + " \"\"\"Helper function to call the agent with a query.\"\"\"\n", + " content = types.Content(role=\"user\", parts=[types.Part(text=query)])\n", + " events = runner.run(user_id=USER_ID, session_id=SESSION_ID, new_message=content)\n", + "\n", + " for event in events:\n", + " if event.is_final_response():\n", + " final_response = event.content.parts[0].text\n", + " print(\"Agent Response: \", final_response)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "498mkJMSMLM3" + }, + "source": [ + "Let's test out the agent" + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "metadata": { + "id": "ImdPXhJTMN8X" + }, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "WARNING:google_genai.types:Warning: there are non-text parts in the response: ['function_call'],returning concatenated text result from text parts,check out the non text parts for full response from model.\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Agent Response: Lawrence Edward Page invests in Kitty Hawk and Opener. Would you like me to do a broader search to see if he invests in any other businesses?\n", + "\n" + ] + } + ], + "source": [ + "# Test your Agent\n", + "call_agent(\"What businesses does Larry Page invest in?\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "1cwr_UavM8-k" + }, + "source": [ + "#### Google Search Grounding\n", + "\n", + "Currently, the agent retrieves answers from the Spanner-backed knowledge graph. For queries beyond the knowledge graph's scope, we can augement it with Google Search." + ] + }, + { + "cell_type": "code", + "execution_count": 26, + "metadata": { + "id": "To-V-eLXMod7" + }, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "WARNING:google_genai.types:Warning: there are non-text parts in the response: ['function_call'],returning concatenated text result from text parts,check out the non text parts for full response from model.\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Agent Response: Lawrence Edward Page did not invest in Deepseek. Would you like me to do a broader search to see if there is any information about this?\n", + "\n" + ] + } + ], + "source": [ + "# Calling the agent with another question that cannot be answered from knowledge graph\n", + "call_agent(\"Did Larry Page invest in Deepseek?\")" + ] + }, + { + "cell_type": "code", + "execution_count": 27, + "metadata": { + "id": "pEFgBw98Mw8M" + }, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "WARNING:google_genai.types:Warning: there are non-text parts in the response: ['function_call'],returning concatenated text result from text parts,check out the non text parts for full response from model.\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Agent Response: Although there's no direct evidence that Larry Page has invested in DeepSeek, DeepSeek's AI models have impacted the stock market and, as a result, have indirectly affected the wealth of tech figures like Larry Page. Would you like me to provide you with more information?\n", + "\n" + ] + } + ], + "source": [ + "# Answer yes to ask your agent to perform a Google Search\n", + "call_agent(\"Yes\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "dwHCEAJlNn3H" + }, + "source": [ + "## Vertex AI Agent Engine\n", + "\n", + "The [Vertex AI Agent Engine](https://cloud.google.com/vertex-ai/generative-ai/docs/agent-engine/overview) is a fully managed Google Cloud service that enables developers to deploy, manage, and scale AI agents efficiently in production. By abstracting away infrastructure complexities, it allows development teams to focus entirely on creating intelligent and impactful agentic applications." + ] + }, + { + "cell_type": "code", + "execution_count": 28, + "metadata": { + "id": "DaLJ8uqGOtWt" + }, + "outputs": [], + "source": [ + "from vertexai.preview import reasoning_engines\n", + "\n", + "app = reasoning_engines.AdkApp(\n", + " agent=root_agent,\n", + " enable_tracing=True,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "JfjX70JcTwUa" + }, + "source": [ + "Let's test it again locally" + ] + }, + { + "cell_type": "code", + "execution_count": 29, + "metadata": { + "id": "XzeW49hISLF1" + }, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "WARNING:google_genai.types:Warning: there are non-text parts in the response: ['function_call'],returning concatenated text result from text parts,check out the non text parts for full response from model.\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'content': {'parts': [{'function_call': {'id': 'adk-4a7274d7-65d9-4861-bccb-f4e816dfb792', 'args': {'query': 'What businesses does Larry Page invest in?'}, 'name': 'ask_graph'}}], 'role': 'model'}, 'invocation_id': 'e-0530bebf-d3d5-4b9a-ab4d-0b23a57593a3', 'author': 'graph_rag_agent', 'actions': {'state_delta': {}, 'artifact_delta': {}, 'requested_auth_configs': {}}, 'long_running_tool_ids': set(), 'id': 'Trb8qyRS', 'timestamp': 1747067830.149277}\n", + "{'content': {'parts': [{'function_response': {'id': 'adk-4a7274d7-65d9-4861-bccb-f4e816dfb792', 'name': 'ask_graph', 'response': {'result': 'Lawrence Edward Page invests in Kitty Hawk and Opener.\\n'}}}], 'role': 'user'}, 'invocation_id': 'e-0530bebf-d3d5-4b9a-ab4d-0b23a57593a3', 'author': 'graph_rag_agent', 'actions': {'state_delta': {}, 'artifact_delta': {}, 'requested_auth_configs': {}}, 'id': '1z7a3pyJ', 'timestamp': 1747067836.319501}\n", + "{'content': {'parts': [{'text': 'Lawrence Edward Page invests in Kitty Hawk and Opener. Would you like me to do a broader search to find other businesses he invests in?\\n'}], 'role': 'model'}, 'invocation_id': 'e-0530bebf-d3d5-4b9a-ab4d-0b23a57593a3', 'author': 'graph_rag_agent', 'actions': {'state_delta': {}, 'artifact_delta': {}, 'requested_auth_configs': {}}, 'id': 'XtJGNJ87', 'timestamp': 1747067836.321095}\n" + ] + } + ], + "source": [ + "for event in app.stream_query(\n", + " user_id=\"43\",\n", + " message=\"What businesses does Larry Page invest in?\",\n", + "):\n", + " print(event)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "DzcrUKHfYRql" + }, + "source": [ + "#### Deploy to Vertex AI Agent Engine\n", + "\n", + "Give service-PROJECT_NUMBER@gcp-sa-aiplatform-re.iam.gserviceaccount.com Cloud Spanner Database User role or Vertex AI Reasoning Engine will not have access to the Spanner database" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "_n71HAPNYaoi" + }, + "outputs": [], + "source": [ + "!gcloud projects add-iam-policy-binding {GCP_PROJECT_ID} \\\n", + " --member='serviceAccount:service-{GCP_PROJECT_NUMBER}@gcp-sa-aiplatform-re.iam.gserviceaccount.com' \\\n", + " --role='roles/spanner.databaseUser'" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "FtlUZjeYYcm7" + }, + "source": [ + "Deploy the package" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "6451BsttOvbo" + }, + "outputs": [], + "source": [ + "from vertexai import agent_engines\n", + "\n", + "STAGING_BUCKET = f\"gs://{GCP_PROJECT_ID}-vertexai-staging\"\n", + "\n", + "vertexai.init(\n", + " project=GCP_PROJECT_ID,\n", + " location=REGION,\n", + " staging_bucket=STAGING_BUCKET,\n", + ")\n", + "\n", + "remote_app = agent_engines.create(\n", + " app,\n", + " requirements=[\n", + " \"google-cloud-aiplatform==1.91.0\",\n", + " \"google-adk==0.5.0 \",\n", + " \"google-cloud-spanner==3.48.0\",\n", + " \"langchain-google-spanner==0.8.2\",\n", + " \"langchain-google-vertexai==2.0.22\",\n", + " \"langchain-experimental==0.3.4\",\n", + " ],\n", + ")\n", + "# \"cloudpickle==3.1.1\",\n", + "# \"pydantic==2.9.2\"" + ] + }, + { + "cell_type": "code", + "execution_count": 31, + "metadata": { + "id": "yqtHrs8P3HG9" + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'content': {'parts': [{'function_call': {'id': 'adk-527d88f2-6b8a-40f3-b9dd-50b477f92339', 'args': {'query': 'What businesses does Larry Page invest in?'}, 'name': 'ask_graph'}}], 'role': 'model'}, 'invocation_id': 'e-17225639-2f06-4fce-95ae-06603a4a4a05', 'author': 'graph_rag_agent', 'actions': {'state_delta': {}, 'artifact_delta': {}, 'requested_auth_configs': {}}, 'long_running_tool_ids': [], 'id': 'g76zfd1M', 'timestamp': 1747068089.968902}\n", + "{'content': {'parts': [{'function_response': {'id': 'adk-527d88f2-6b8a-40f3-b9dd-50b477f92339', 'name': 'ask_graph', 'response': {'result': 'Lawrence Edward Page invests in Kitty Hawk and Opener.\\n'}}}], 'role': 'user'}, 'invocation_id': 'e-17225639-2f06-4fce-95ae-06603a4a4a05', 'author': 'graph_rag_agent', 'actions': {'state_delta': {}, 'artifact_delta': {}, 'requested_auth_configs': {}}, 'id': 'QLwNY9jQ', 'timestamp': 1747068097.19637}\n", + "{'content': {'parts': [{'text': 'Lawrence Edward Page invests in Kitty Hawk and Opener. Would you like me to perform a broader search to look for other businesses he may have invested in?\\n'}], 'role': 'model'}, 'invocation_id': 'e-17225639-2f06-4fce-95ae-06603a4a4a05', 'author': 'graph_rag_agent', 'actions': {'state_delta': {}, 'artifact_delta': {}, 'requested_auth_configs': {}}, 'id': 'dACMoPCP', 'timestamp': 1747068097.268164}\n" + ] + } + ], + "source": [ + "# remote_app = vertexai.agent_engines.get('agent_engine_uri')\n", + "for event in remote_app.stream_query(\n", + " user_id=\"43\",\n", + " message=\"What businesses does Larry Page invest in?\",\n", + "):\n", + " print(event)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "QbjhQtdcPEAD" + }, + "source": [ + "Because we enabled [Tracing](https://cloud.google.com/trace/docs/overview) in Vertex AI Agent Engine, we can leverage it for troublshooting and monitoring." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "hbOxV-FdPe00" + }, + "source": [ + "![Screenshot 2025-05-12 at 12.02.39 PM.png](data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAABxcAAAJACAYAAABPBehFAAABWmlDQ1BJQ0MgUHJvZmlsZQAAKJFtkM0vA1EUxU9pM0KDhYWIRRfS+ChhhkV3qgkRFk0RH7vp62jJdPoyHUFsiIWNlUXXVv4CSSM2VsJa4vMvsLEh3dA895kyLW5y3/3l5OS+mwM0+HXOTT+AnOXYyamJ0NLySkh5RgBBNCGMLp0VeCyRmCULvmd9lW/hk/NmUO46V/eO8FZcn3ka2HnRLvf/+uuqOW0UGM0P6n7GbQfw9RInNh0ueYu4w6ajiA8lZ1w+lpxy+fTLM5+ME18Rt7Osnia+J46kavRMDefMDVa9QV4fNKyFOZqt1N2IQ4OKSXqjGKFs/veOVr15cGzDxhoyyMJBCDFSOEwYxNOwwDCECLGKYeoxmfHv7Dwtfw1EFaBx19NS9FdpHOg88LSeB6DtjPQw1239J1Ff2V9Y1VSXW0pAoCjE6yKg9AGVOyHeS0JUTmj/I3BR/gQDbmHnQ51WhQAAAFZlWElmTU0AKgAAAAgAAYdpAAQAAAABAAAAGgAAAAAAA5KGAAcAAAASAAAARKACAAQAAAABAAAHF6ADAAQAAAABAAACQAAAAABBU0NJSQAAAFNjcmVlbnNob3S1c01AAAAB12lUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iWE1QIENvcmUgNi4wLjAiPgogICA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPgogICAgICA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIgogICAgICAgICAgICB4bWxuczpleGlmPSJodHRwOi8vbnMuYWRvYmUuY29tL2V4aWYvMS4wLyI+CiAgICAgICAgIDxleGlmOlBpeGVsWURpbWVuc2lvbj41NzY8L2V4aWY6UGl4ZWxZRGltZW5zaW9uPgogICAgICAgICA8ZXhpZjpQaXhlbFhEaW1lbnNpb24+MTgxNTwvZXhpZjpQaXhlbFhEaW1lbnNpb24+CiAgICAgICAgIDxleGlmOlVzZXJDb21tZW50PlNjcmVlbnNob3Q8L2V4aWY6VXNlckNvbW1lbnQ+CiAgICAgIDwvcmRmOkRlc2NyaXB0aW9uPgogICA8L3JkZjpSREY+CjwveDp4bXBtZXRhPgrZZHDzAABAAElEQVR4AeydB5wNVxvGn60sq/dOSHSi9y56ix5EBB9C9N67IKJGdKK3RA+S6F2IGqIti+jd6mt3v/c911x31zaLZPGc3+/uzJw+/5l79s48532P0/gJEwJatmgBBhIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIIjYBzaIlMIwESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAGLgOuVqzdw9JiXdcwtCZAACZAACZAACZAACZAACZAACZAACZAACZAACZAACZAACZAACZAACQRLwDVRwnhImjRhsImMJAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAGLAN2iWiS4JQESIIFITODixauRuHfsGgm8GQK8798MV9ZKAiRAAiRAAiRAAiRAAiRAAiRAAiRAAiRAAq9CwPVVCr8LZX19n8LH5x4ePXryLpzOK59D1KjuiBHDE25u7/2t8cosWQEJkAAJkAAJkAAJkAAJkAAJkAAJkAAJkAAJkAAJkAAJkAAJvGsE3msFSYXFa9duIlasGIgbN/a7dm0jdD737z80TBIkiEuBMUIEWYgESOBNEzh+4qRp4oRsP/roQ6SXDwMJkAAJkAAJkAAJkAAJkAAJkAAJkAAJkAAJkAAJkMC/Q+C9FhfVYlGFxejRPf4d2m9BKxYLZUPB9S24YOziO0ng8KGD5ryyZsv+Tp5fRE9q5arVWLHyl2CLV6lcEZUrVQg2jZFvBwHe92/HdWIvSYAESIAESIAESIAESIAESIAESIAESIAESOC1iYtPnz41NF1dA1fp7+8PPz8/sYJzi3S01RUqBbQXL4sKjHfu+LyYwBgSIIF/hYDP3buv3M7Zs2fNuJs0adJXriusCm7evCmTNKIjSpQoYWWNULpaKq4UUVG3aqVYWYREDbqvgqMGFR2PHz9h0mjJaJC8dX9ex33/5MkTODk5RcrfHG/dBWGHSYAEgiXw+PHjcP+/0+cgze/hwYmMwcJkJAmQwDtFQMc7d3d381vsnToxngwJkMA7SSAgIAC+vr5m3AruBPlsGRwV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x7ymz2Klp+tVBg0qKPnLXarlgwt6rg8ePLC7EA0uT2hxWl7dbqrwG1wI67revn0r0PqIjnXcE1epfnJelhtZx7Tw7KvVqlrteYhr0aDhVbnoPRla34O2F/RYmauFYEjcguZ3PA6rbeXmKkKmo8WiY/mw9pWbugOOIubaQUNY3+Og+fU4tOsQXP43EafMLAvQkPbfRLuskwRI4EUCsRueQtvKyc3ElhdTGUMCJEACJEACJEACJEACJEACJEACJEACJEACkZ9A6oTO+KyAaFpiEmjJXirt+fvDCH3X7gZAtEez7mJIZ6N5tYwKg/Y6JMLnkU18jOXhZI/XOjT/9Xv+iBvdWQyzApfxFcFz4c4n8LoimSSoAdKYlf/g9qx05vitFRdN7/mHBEiABEiABEjgvSZQafBZuLlHR/Y0r28Sz3sNlCdPAiRAAiRAAiRAAiRAAiRAAiRAAiRAAiTwnxGIFsVJrBBVIgTcRCW8J8KgCoXPosy+ZaUYXCfVHM9T7KN8bSvaqSEiHj2x5VTh0s1F6he9UIXK+2KZKI4kTVDh8amftCv5XaXduw8DjFBpSwUOnrkn1pD3sapnKhMlxRlIgARIgARIgARI4O0k0K92PJQeeNl0Pltqz0Czr97OM2KvSYAESIAESIAESIAESIAESIAESIAESIAE3lcC6uLUCo+eyXuWsKjxjvtWPsetlvZ56BjzfP+JCI5Pntrqf+j7PF737oiY+Dw839f2Dnnfw8bDt7Gud2J7ltciLv62fpOpsEyp4vaKuUMCJEACJEACJEACb5pA7g89zQ+bfotuiGuG2/ID6/mPnzfdNusnARIgARIgARIgARIgARIgARIgARIgARIggXeZgC5NWDi9m3n/pu/hrPBaxEWrMm5JgARIgARIgARI4N8moD9sVvV8/uPm326f7ZEACZAACZAACZAACZAACZAACZAACZAACZDA+0RAlmhkIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIIGwCVBcDJsRc5AACZAACZAACZAACZAACZAACZAACZAACZAACZAACZAACZAACZAACQgB4xa130+PXgmG72VZBVLCjluvVs8rdYKFSYAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAE3igBIy42KxXllRrZtcO2dGP+gq9W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kAAJkAAJkAAJkAAJkAAJkAAJvP0EUid0xmcF3M16ipbspNKeyElG6Lt2NwCiPZp1F0M6W82rZRzdpKrxoc8jm/gYy8Mp0Ds0zX/9nj/iRneGi2iQ9naljLpEXbjzCbyuSCYJWs+YFedxdGw6c+x67do1s8M/gQnEjBU7cESQo1ix4gSJeX4YO07c5wdB9tR1alghrLbDKs90EiABEiABEnhfCORNFwWHvO8hexrP9+WUeZ4kQAIkQAIkQAIkQAIkQAIkQAIkQAIkQALvGAHvq/74ZvkjRJM1Ff2fuSN1E5XwngiDKvpZHkp137JSDA6BmsN5ioWj79NnqRLx6Ilt313ESTcXqV/0QhUq74tlop9NO4QKj0/9REGU/K7S7t2HAUaotNrQ92/6Hs4KThkyZQ5Yv+2wdcwtCZAACZAACZAACbw1BA5530fdkZdQIlscZEvtaZ9h9dacADtKAiRAAiRAAiRAAiRAAiRAAiRAAiRAAiRAApGUgIqaKixuPHQLCzokkfdv0U1PI5Vb1F07NptO5S9YLJJiZLdIgARIgARIgAQiGwEVGEeuuIk/Tj0ONKMqsvWT/SEBEiABEiABEiABEiABEiABEiABEiABEiCBt4mAWkKqxWKHKnHtwqL2XwwfGUiABEiABEiABEjg7SWgM6Z+bGObNfX2ngV7TgIkQAIkQAIkQAIkQAIkQAIkQAIkQAIkQAJvBwFZopGBBEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABMImYCwXJ69/HHbOfyGH72XbCpOHHkaO/vwLp8wmSIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESOCtIWDExX41o0aKDv+23ualtUypyNGfSAGFnSABEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABEiCBSEKAblEjyYVgN0iABEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABEggshOguBjZrxD7RwIkQAIkQAIkQAIkQAIkQAIkQAIkQAIkQAIkQAIkQAIkQAIkQAKRhIDND2kk6Qy7QQIkQAIkQAIkQAIvS8DX1xc+9+7h8eN/f83mI//4YeQaX+w4+RQBAQEv23XmJwESIAESIAESIAESIAESIAESIAESIAESIIFIS8DJyQmF07uh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lpq8AckTOOCRHDhKHft690QX3HfWH+9ldBJ9SCXCoKRt7j8ETl97hCwpHU3Z4yIjOnr7BiJRPAfE0jbWuNLITwTPmZsf4vhlmYAkFShHzj8Hn8lB8SBpuWhRtPtUIS0kYXHX7t3o0aMXvurdy4hR6h5UrRLz5PGyax10GFagy2cq22W4ubmZswm//mJERrsic+jkFPolU6HsZcd9epwJE34zrk1VQPzssxZQAU8tDLds2YIJEofSEh6fbmd/rr56w0oaPPRFUsuWLfHpp58808QKQPpMweOMkOahcSSrVK0mZsBfIKdYqM6aNQu79+xBnz69kS1bNhw5fDDE7qw5R4fvSJeu3bBk8VLEjftiHENcGDNJgARI4A0loA/6qCvzqEhFMjth3ylfeGVwjYrhOSYJkAAJkAAJkAAJkAAJkAAJkAAJkAAJkMBbQuDUlUcYPPc+4sVW8VAlQcBZFES1VlTR73GWTQBMIIKgn3+QyKi1LavGZXv9YZWZdlJ257HFowqI8VyknQiHevzQP9Ac61gJ4zqIR07pScZyknFv3QsSM7VMk94DK5bVOehE3mm5aEMRvgON0TdL4gXeEgtDFRV7dO+O1KlTha/xG1JL4yrqjVpnZ2eo+9BTp05j2PDh2CmxJlOlTo1aNWugadMmL7WaLVu2ok/ffli+bMlLtX+djTZs2IixY8fh5MlTJq5ky1YtULhQoVceIrK+I2ode+/ePTiKGJ5GrgsTCZAACcREAn5+frgmVvtRkQ6cC0C9UfdQOlci5PZ0tf24i4q5cEwSIAESIAESIAESIAESIAESIAESIAESIAESeN0EVNRUYXH1fh+s6JMSBTIHPWT/VoqLy1auMfwqlC31ujmyPxIgARIgARIggWhGQAXG276+UWLBqALjsMV+2HTUX54gC3qqLJrh4XRIgARIgARIgARIgARIgARIgARIgARIgARI4KUIqMdMtVjs3yCJTVjUjkL3sflSw7ARCZAACZAACZAACUQuAbW0TxxCnOTImEWqlEC5ApExEscgARIgARIgARIgARIgARIgARIgARIgARIggehBQLyqMpEACZAACZAACZAACZAACZAACZAACZAACZAACZAACZAACZAACZAACZDA8wlQXHw+I9YgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIQAo4H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UfDgwUN079ELZctVwPr1G3BYBMUPGjdF69Zt0L5DR1w4fwHTps8QMbAm7P3lWkMdPnoc72TMYJ0+86nuUBs3bmziQlqFBUWAnTZtmnWKGzduoGHDhkbQzJUrF1R0vHfvnikvU6YM2rdvDy8vL4kzWRZ58uTB0aNHbW31YOrUqcY6Ui0i9fhFks5d18BEAiRAAiRAAiRAAiRAAiRAAiRAAiRAAiRAAiRAAiRAAiQQfgJnvANx4vIjXLzxCH4SH1HT6rXrkSNHNmOspN5IbwXd6kfTYi5wFMVp67bteCACZPFiwb0h3rjhI1aKfpglLlb/njMf169fN/3dun0b7okSmWN98/BIjQOH/jPnZ8+eR88+/XHtmjd27NiNHl8NwL37fkjqprIkU1QScIrKwSNqbD+x1jt77jyWrVwT5hAVypYKVj5zxjQjLKqImC59RsycGSSQjRgxygiLhQsXCpfVosYA/ObbwRg1coSxGNRB+vcfgLESS3H8+LFGPBsyZDBqioWfpgRubpgxcxaqVatqziPirUH9+ujdu6fpul37DpgzZx4OHhS1X6wNvb2vI2++AhIzcR/ef/+9YMNfvnIVKZI/sT4MVignp06dgqen59PZRii0Mnv16iVPFbjKeAehlpRVqlTBzz//bERFZTV9+nScO3cOsWPHNhaUv/zyC4YOHWqaq+Cp1orq+jRr1qzo2rUrDh06hGzZslndh/mpc9c1MJEACZAACZAACZAACZAACZAACZAACZAACZAACZAACZAACYSfwAURFfWlUl7funGMh8EFCxejU/svTSeHLwbg3sNAZErpaKwMb/vewdTpM9G3dw+cOXsu+EAOscTo6AHixY2HU2fOoFvPPhj5w1BkyuCJSZOnStzFW0iYIAFWrV5na3fov/+QKaOnzapx7vxFuO17G1d8E9rq8CBqCLyV4uKroLQXGBs2fGLBqMKiloUn7d//r4hoASKwlbFV79K1C26KkKapQ4f2xmJRBcVrYha8du06CYr60FY3Ig7KlStr61b9ImtSYVFTkiSJjUmxui19OumTBE+7KbWvoxaIGmtR082bN9G8eXNbsVokFhaXryoMjh071lhxamGJEiWMG1Utv3z5snFzGjduXNNOrR5VhLTSwoUL5QmIxMifP7/JUmFSrRcHDRpkVQnzU+eua2AiARIgARIgARIgARIgARIg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eH0lkGxMAiRAAiRAAiRAAiRAAiRAAiRAAiTwRhJQK0UV/KwUlrdStXJMIJaJKjiqWKjN7ogw+byUUMRJf1UupY2TdHLrXqBpa7Xbe9IXD+77YkKLiNdJrDFj6qd6rUzs7m6WL5ee6U0g4CiSvoqIIb0iY/4N6tQ0Yp1aA6q70eiadG46RxUWdc5MJEACJEACbzcBN9eoE/Y6VXbG6v0+0B+x9j+k327iXB0JkAAJkAAJkAAJkAAJkAAJkAAJkAAJBBEIEONBFRStV1hctI7PnUBovEZfERXDIyxqfzdFTLwjksQdMYrUY0uO1Hsxek9G783oPRqmiCdgfx+OMRfD4B2d3KuGMc1IKXJzczVWgOoi9adfJsIrVw68kzEDUiRPGqLL10iZ1ONBVFC8fOUajp04aWIsFi9amK5QI/MCcCwSIAESiEICGus3aZIkuC1uyNX9dmSmHB6x8Ge7uBi22Bcj5/uIwGj9vI3MWXAsEiABEiABEiABEiABEiABEiABEiABEoh5BBzE/LFIZidzb0bv0TBFHAG1WFRhUe/DWYniokWCn+EioPEL38uXB1t37MTajZtF1LsKPz+/cLWNqEr6hU6RPBmyZs6ETm0/R6JEkRPTMaLWw35JgARIgARejID+d8ByyfBiLV+9dipxM1+uwKv3wx5IgARIgARIgARIgARIgARIgARIgARIgARI4E0hQHHxTblS0WieKt5VLFfGvKLRtDgVEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABEiCBCCbAmIsRDJjdkwAJkAAJkAAJkAAJkAAJkAAJkAAJkAAJkAAJkAAJkAAJkAAJkMDbQoDi4ttyJbkOEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABEohgAhQXIxgwuycBEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABEiCBt4UAxcW35UpyHSRAAiRAAiRAAiRAAiRAAiRAAiRAAiRAAiRAAiRAAiRAAiRAAiQQwQScIrh/dk8CJEACJEACJEACEUrAz88Pt3198eDBgwgdh51HLoED5wIwbLEfNh31R2BgYOQOztFIgARIgARIgARIgARIgARIgARIgASihICDgwOKZHZCp8rOyOERK0rmwEGDE4gdOzbcXF3h7OxsK6C4aEPBAxIgARIg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JdCmzedo3PiDsKqxjARIgARIwI6APhnH9PYRyNbNF+2re5iNyNu3Oq6IBEiABEiABEiABEiABEiABEiABKKeQPIEDvi4VGzEFf3wsaNAIyoGiFGjnt+8FwgXuQXvLC+nWA5yLz1IQNSZq4gYIC+nJ44Fgy/osUYY+Fh0tArltjou3niEFAkdJaZj8HHVJerMzQ9x/HKQVaWOMXL+ORwa6mo152c0IJAqZZBr25dThaLBAmLSFFxcXJ4RFnX9YQmLzyvP4+WFI4cPhohRhTYrOTs7i5/koKcTrDzrU122vmqaMOE37NixQ9zGTjddqWAYmrCoFVSsS5YsWbiH3b9/T4h1len48T9j7959aN06yAp04MCvUb5C+TCFRe0sbty4IfZpn/m8a2Nf9+jRo2jUqAmu37hun81jEiABEiCBcBBQC351p830dhEoktkJ+075wisDNxBv15XlakiABEiABEiABEiABEiABEiABKILgSu3AjF0nlgUipVhgKh+qgc6i4joKxaNKvypuGclPXaN4wA/VQclOcp9+rvhsDKML22kS2gzCceIW3cDZaygXhPGdYC/KpRS7iTmkrdEzAzqPahc7wvo/QGm6ENA78NZiVfGIhHDPtViz15EjIrljxo1wtwQDk28fB1zCmuNH330IYYPH4mO4j5V/1EUK1YU6ir1ZZNHmjQijCZ64eaenp6YM+cv085dLDKZSIAESIAEwk/ATVxSU1wMP683pWanys6oN8rHTDe3p6vtCco3Zf6cJwmQAAmQAAmQAAmQAAmQAAmQAAm8KQTu2ImED/yC5D1LALRfg7pRfZLsj5/kPn10J1ib4KVqGfkkPTlWIVOFxdX7ffBnu+cb+jzpg0cRTUDvw1mJblEtEiF8vi63qK+rnxCmyCwSIAESIAESiPEE1IX3bV9fioxv2TfhwLkADFvsh01H/eVpySebjLdsmVwOCZAACZAACZAACZAACZAACZAACZCAHQH1XqgWi/rgcQ6P54dGs2vKwwgioMZZKizaG4rRcjGCYLNbEiABEiABEiCByCGgP2wShxKrN3JmwFEigkAqceFfrkBE9Mw+SYAESIAESIAESIAESIAESIAESIAESIAEXoVAaOE2X6VPtiUBEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABEngLCVBcfAsvKpdEAiRA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+K9Vn9lZEiABEiCBF08gSiATBF58T3hEEiABEiABEiABEnh7CLQsEk5W7r4sO496I7JpgIbt2I9yKP8mmePl6P77Jp0fz4UESIAESIAESIAEAiLgPg73aOpYQCW5jQReEoH69eq+pCPzsCRAAiRAAq8TAYTdjqWhs5Dvhh6Mr9OVY19JgARIgARIgAReZwKpEoWRn5q+KwMXe2vI08uWJ9H/+cC7Dx6LKIfyNBIgARIgARIgARIggdeTADwWISy6p7+juPh6Xkv2mgRIgARIgARIwJcAHmxiPGXIb8IjARIgARIgARIgARJ4NgLxNd1O/ozP1gZrkwAJkAAJkAAJkAAJvJ4EGBb19bxu7DUJkAAJkAAJkAAJkAAJkAAJkAAJkAAJkAAJkAAJkAAJkAAJkAAJvHACFBdfOHIekARIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIgAReTwIUF1/P68ZekwAJkAAJkAAJkAAJkAAJkAAJkAAJkAAJkAAJkAAJkAAJkAAJkMALJ0Bx8YUj5wFJgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARI4PUkQHHx9bxu7DUJkAAJkAAJkAAJkAAJkAAJkAAJkAAJkAAJkAAJkAAJkAAJkAAJvHACYV/4EXlAEiABEiABEiABEniOBO7evSvXvL3l9u3bz7FVNkUCJEACJEACJEACJEACjwjsPXlfBi6+KxsO3ZOHDx8+2vGSl0KFCiXZPworLYuEk1SJwrzk3vDwJEACJEACJEACbyKBCBEiSJTIkSVcuHCu06O46ELBBRIgARIgARIggdeNAITF8xcuvG7dZn9JgARIgARIgARIgAReIwIQFssPvSl50kSTZiUii+p5r4xB59z1t7f277L81PRdCoyvzJVhR0iABEiABEjgzSGACf34xIoZ0yUwMizqm3N9eSYkQAIkQAIk8NYRgMcijQRIgARIgARIgARIgARCkgA8FiEspvN4tYRFnDOETvQL/UM/aSRAAiRAAiRAAiQQUgTcx+HouRhSlF/hdqEwhw8fXh9A/U61GzlylOTLl1eSJUv23Ht/7do1GTduvHzxRS2JFi2aq/0HDx7InTt35J133nFte5MWbt269Uqf240bNyRixIgvBPn9+/ftngsdmnMaXghwHoQE3hICDIX6llxoniYJ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hk9g40sYXzl/4YKVQZs0EiABEiABEiCBkCEQP148a5ieiyHD95VuNXr0J/dcw4kEJixiX3BtBSScoZ5jEHTixInjrAb464RIDWhnUH0LqPzTbMPDdEB5Ef234f+h2//+wNYhygZmQZ0z6jyN92Fgx3jS7XjBiOf7B8O9TlD9dy/3JMvIFfksFhSP4O6RyL5J6P0fP7Dzdi+Ha09h0Z0Il0ngxRPAgAjCPtNIgARIgARIgARIgARI4G0nkCB6GDlz6Y7Ej/5o8izWsT0gw3P0WZ1M7N/cBb0wGnHpvq/Y579cQOtI/xLYBMCLFy/aJGlMLn+oE4yRUtEREQNqK6htEDghKOLdHR96LQZFi/tIgARIgARI4PkQcHfu8Zus7fm0z1ZI4JkJICF3ggTxn7kdNkACJEACJPBmE4gSyASBN/useXYkQAIkQAIkQAIkQAIk8DiB+vnCyfq9F0xgxF4Ii1jH9qcxCHX4QPi75/v7NPWDKos20fYD35yKWP8vBi9J1IU3I4XF/0KQdUiABEiABEjg6Qm4j8PRc/Hp+bHGCyBQv17dF3AUHoIESIAESOB1J+CEXbqmIY7pwfi6X032nwRIgARIgARIgARI4FkIVMziIyKOWn5eTl+6bx6LEBad7c/SNuuSAAmQAAmQAAm8vQTgsQhh0T08OcXFt/d+4JmTAAmQAAmQwBtBAA82MZ4y5PcbceI8CRIgARIgARIgARIgARLwR6BZKRF8aCRAAiRAAiRAAiQQkgQYFjUk6bJtEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABEniDCFBcfIMuJk+FBEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABEKSAMXFkKTLtkmABEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABEjgDSJAcfENupg8FRIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIISQIUF0OSLtsmARIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIggTeIQNg36Fx4KiRAAiRAAiRAAm8hgbt378o1b2+5ffv2W3j2PGUSIAESIAESIAESIAESIAESIAESIIHXlcDBsw+k/Yzbsu/UPUmZMKx4VY4gyeLRJ+x1vZ5var8jRIggUSJHlnDhwrlOkXepCwUXSIAESIAESIAEXjcCEBbPX7hAYfF1u3DsLwmQAAmQAAmQAAmQAAmQAAmQAAmQgAmL8WNGkRalEgt+ITTSSOBVI4AJ/Rh/wzicYxQXHRL8JQESIAESIAESeO0IwGORRgIkQAIkQAIkQAIkQAIkQAIkQAIkQAKvIwF4LKb1iGxdxy/WaSTwqhJwH4djWNRX9SqFYL+gMocPH15ChQrl5ygjR46SfPnySrJkyfxsfx4r165dk3HjxssXX9SSaNGiuZp88OCB3LlzR9555x3Xtjdp4ebNm/Luu+8+t1N60bzu3bsnYcPyz8Rzu4BsiARI4LkTYCjU546UDZIACZAACZAACZAACZAACZAACZDAK0EgdOjQEu299ywUI8ayHz58aGPaTnqUqFGi2Nilsx2/12/cEG+3ichhwoSx8ehwbmOcKIcx6UuXL0vYMGF1/3uCcdzQWjaCjpvDQytq1KgS0Xdc12n/ro6VXrlyRTBmSiOBt5GA+zgcPRffojtg9eo1UrBQEfk4RSpJm85Tvvyqrly4cNFFYISKiwcPHnKtP8+Fq1evyqDBQ+XSpcvW7OnTZ+SruvUkefKUkiJlailSpJjMn7/geR7ypbWFc23evKWkSZveWOfOk0/GjB33xP1B/f37//RT/mXxypwlm8ydN89PX7hCAiRAAiRAAiRAAiRAAiRAAiRAAiRAAiRAAiRAAiFNIHasWIJcb8eOn5AfZ/0sk6ZMl9179prjTMwYMUx0/GP7Dvl+wmSZt2CR3Lp12/LCITecY7Fjx5bwmidu7779MnHyNG1ntvx77pw5u8SPF09ix45lAuWJE6fk8OEj1mZ0dY6JFDGiipTX5afZc2TCpKmya7ceV9uJFTOm0zR/SeCtJkBx8S25/GvWrJUvv6wrNapXkx3b/5AF8+famVeqXEXu37//winUVWERs0NWrFgqu3fvlIYNG0ibtu1k5cqVL7wvz/OAmMVSrXpNOX78uPw0a6bs27tHOnfqKMOGDpPvv//hiQ61du06wXVxt5fFa+wY9WbNm9e9K1wmARIgARIgARIgARIgARIgARIgARIgARIgARIggRAlAE9DeC4ePPSXePUfKHHixJb/fZhUho0YLdu277Rjr1y9VqZNnylp06SW8+cvSM8+XubdGClSJNsPr0XE7tu5a7d8p/WSffQ/9UaMKJ279hRE2pOHIkuWLpcmzdvIgIFDVID0cfhAlD14J3bq2kPua+S9FB8nlzHjfpCtf2w3z0mIjxA348aJIzGiR1eBMrYtQwyNjnXfXwijKAdBEr/YDk9M9ItGAq87AcY7fN2v4BP2v1+//lKtWhWpUaO61XhP/4h9+80ASe+ZQX7/fatkzZrlsZbwB3TJkqVy+MgR/cP9oRQoUEBncfj84du2bZv+AfaWXLk+s3oQ1WbM+FHy5s0jcePGtW2ot2zZcpstkj59Olf7l9XdfNfuPVp+qiROnNi2lyhRXCCqrVi5SvLkySPbd+zQmSa35D11P4cwGl3/+BYrWkQiu806QfsrV6y0P/DZs2WTNPqPCAxi6cyZs6Rw4UKyadNm/QfokKRLl1Zy58pl++EFuGnTJsmfP58sWrTY/uHB8sf6j4RjFy9eksWLF4v39evyaY7skipVKlfd9evXS5Ysma1ukSKF5YMPPnCqyfLlK2Tfvn2yYf1aFwe03ahxI/npp9lSu/YXrrKHDx+W5dp/uNfny5dP4sePZwymTZsuly9fkenTZ8hnn+UU/GMYHC80Glifse+GhgOYv2Ch/Pvvv/qP4cfKOLfrHzFcO1ynffv3y/vvv69iYh5z+0e9v/8+Zv84OtyDuid27Ngp15VXwoQJtb1lqC5ly5aVGDGi2zK/SIAESIAESIAESIAESIAESIAESIAESIAESIAESOBJCCA9FGztug1SsEBeyZvbZxz6zNmz8se27fKJZzoTBmtUryKeOvabOVMGadG6vY5xHpBUKT+2uk4bECNz6zh29mw+Y+C///GH/HngoGTKmEHHQaNI1y7tZeGi36yO8wVPRXgvVq5YzjZhDHXBwsWSMYOnhVO9ceOmCZlHjhyVD1X0/DCph3lHnjlzVlLq8RPpGOk7Ki6iDxA3z2sEQYijST5439Jo/aPjtE7/nGPylwReJwL0XHydrtZ/7CtEuj1792pI1IJ+WoDAOOeXn8XDI4mf7ViBGFWufEUZoh53FzXG9MBBg6VChYoWexr7EcJ04sRJWDTDH9d27Tuq6/hhW9+8eYsULVpcNm7YKDt37lJPvKq+JUVw3Lhx48ismT+ZgOjs6N/fS3r26G6rixf9KpUqVbU2T5w8KcOGfSely5RzHX/hwkVSrFgJOaIC47Fjx7SvFWSBimcwxNxGX4rqfvTz76N/S61atWXWrJ9s/4GDB6R5i1ZStWp12a5u8xALixcvqW0dtf04h0KFCsvqNWs0TOxB68ecuT6enqjbqnVbq7tu/QaNsX3V6jhfEEUhcjoCq7O9fr26KkY+CvuKvpYuXVb7dlS9NVdJ4SJF5ZCKoCf1XP/884BVwy8E3CfhFVSfr1+/IcX0/CC4Xrx4Ubp27SaNGjVxuibNmreQHj17qaB5WSbpNS1ZqoyGr71k+7t17yG79B8/WHD3BMTY+vUbSIMGDeXo33+bOFqmbDnGIDd6/CIBEiABEiABEiABEiABEiABEiABEiABEiABEnhaArVr1ZCypUu6qv177rx5CWID0kvBE9CxK7p+8tQpZ9W8DLGSOHEi+UvHfDGGDY/Ff/89L4kSJbRyWTJnMo9CrCCvo2NXtRxyMTqWKFEC2eubymq7ipXtO3WVpctWyD//npOvu/eWTl/3kI06Jn5axcX2Hbvq7xmrOmLUWBUlf7Vx5G8GDZUVq9ZYP+CVSSOB15kAPRdfwNVDotcTJ0/JkuWrnvloBfPlfuo2TukfVP27KfHixnusLjz6ArKx48aLt/4BhSAG920ktC2seRHHjf9emqgXXnDm5dVPypQuLV5efaxo9hzZpEmT5raMP9JjNNxmYxW4smbLIUXV+w8ebhl1pohj+EMP++Xn2eYtCeEr52e5ZZZ6/9VU70sIcf369ZVSJX3+YUHy3hk/zlSRsJjThGTKlFGGDhls66HDhJZff1uiAml51/4BA/pLihQf2x/znDlzaYjWFZI0aR3p2q2HFChYQPr07mVlP/H0lL59+0npUqVcdceOHWN1XRt8F07pdY6j7vBBGcS+Tp27SJ8+vQUem7DGTZrK8OEjZfDggeY12KFjJ+nevaurmeB4BdXnLb9vkRMal3zpkt+MZc0aNaRly1YmIMLFf+7ceTLzxxnmjQmvz9p1vpQ9Grs8Z85PXcfHwpPcE1euXpNNP81Sb8uIgvsuW/accuCAzhby9fz00yBXSIAESIAESIAESIAESIAESIAESIAESIAESIAESCAQAhgjRiS1cJrrELZn7z6NGrdfvqhZzdbhLbhp8+/ikeQDuebtbY4a7inAHAEvX55cslJFvYpVP7d61apUFORbhMGhApHj3IVFbP/QI4lM1FyLECwRXW/FyjXY7Mca1PvS1v9VgXHTlt+ld4+vbf2ff/7V8dV9kiB+fNmujjcd27Yy78YM6m35t47Twpzxb1vhFwm8hgQoj7+GF+1pu+z8ofL/BzKodtatW68ebCVNWES5dzV0Z8kSJWTdunVBVbN99+7dV1fvXVq/hKtsxgyPhENs9EyfXj32lkuvnj0Ef2wrq2djjZqfa5Jcb6tz6vRp+3XCsEbTONYI5blzp0887ebNm0n6dOlMUPzuuxGyevUaOeNbxyrqV+XKlZxFC/d54cJ51zoWICzCwAXhWS+ohyb+8UEYVnh7wtMRnytXrgjc2bHfMaeusx7Yb5evu8rHKVK5PnB1R9jUS5cuWwhR5xjhw4fX3JM+HoIBtRUUr+D6jDCo70Z8V701W6iAulLDliaQX36ZbfG/cV3TKcfeffrK7Nk/22yfiRN+eExYRJ+CuyeQQxMGYRGG8KihQ4eysLO2gV8kQAIkQAIkQAIkQAIkQAIkQAIkQAIkQAIkQAIk8AQEkOvw7D//uEQ4jBcPHzlGWjZvIhE1XCmsqoqEGzUtVqu2HaWP1zcaijSBhTl1msfYJ2zxb0slfPhwMmzQAOnVrYsKhavlmK/Id+fuXSvjjKHbin7B27FQwfzSsk0Had2ukyAcawJNawW7p2PIUaJEtmV8xYwZQwrmz+tajxQ5ko4v37b1KpUqSL9vB8vAwcPMqzH3Z5/aeLQjmLoqcYEEXjMCYV+z/r6W3UXy2aQ6e+K/eB0+jxOGyAOPbuTb8/BI4qdJ5NR7772oJjS570AoVAh67hY9ejQV2C66bwpw+fp1b51Rct9mdARYwHcjBDV4GuJz4sQJqVKlmoVf/bpLZ+ur/7pRdYYIzgE2afIUGYRQreXLSyL9Qx9Vz+G8usS7WwRtPzBzREv/+51woLt0Rsnt2z7/AKBMiRLFXOE9w4UL/D+bJHqdly71yTeIeq1btZSGDRqYKItwqrCz+g8RbK0/oTZz5sy2PbCvwHg1bFDfqgTW53g6C+e3XxfL5MmTpV//AeYlCe/TBr71ZkyfaiFMIXS279DJclv27dvbBGX3vgR3T6B/OT/N4V6FyyRAAiRAAiRAAiRAAiRAAiRAAiRAAiRAAiRAAiTwnwgg5CnGHC9oqiev/gOlQd0vLbeh01i8uHFlyMD+ghCmkdX7sGHTlpL8o49sd1gdk3ecbTZs2KQR8IpLvHhx7eOZPp1sVk/DD95P7BIvnTbxi7RbEP8qVSgrZUoVtzJr1q6XQ3/5pAQLGyaMpE2T2lUFZR0vSduIqHy+EVbz580tuXLmsHyMc+cvlAMHDwlCvSJaIBxcaCTwuhIIXCV5Xc+I/X6MAGZoJE+e3MJ+ZsnySMBCfOl8+Qtonr0JkiNHdj/1knh4WL5B940HNMmth0cS24Q/6s6sDmxwn9mBHIGYuXHkyFFXOMzbt3282lB2+fIV0qt3H1m+bInrjy48B3Pm1BCavvkGP0yaVLboH3h3O6rtJUvm84/DkMFD5euvu2joVZ9QpQiTevbsP+7Fg1x2/mHxXyhmzJjmedeiRXPN6VjU/25NCLz/sW3uGzKohybyVB7WXJA4B7DAJ3r06K5i77//vi336N5NZ7XEdG0PbCE4XsH1GfkYEYq1Xbu29gHX8hUqSUEN/QoBea+GE6hTp7Z9IN4WL1HKvBirV/cJL+D0K7h7wsq5xSV36vGXBEiABEiABEiABEiABEiABEiABEiABEiABEiABJ6GAMQ6jGtf17ClEBbhAZg27SNBz2kL47yIFjdyzHhJnTKFxI4dy2eX6nuO80kC9Wg8cPCgZMr4iUWuO6zjzAXy57FyAY0TIxRqLB23xb7QocNYONbZc+bJ1x3b+bSt2321Q99u6MEEHzfTVfS9cbNWMqBvL43Cl9bGaBf/usQKQcCkkcDrTIBhUV/nq/cUfYcH3Q8TJsqCBQtt5sW5c+cEef0gdGXJkuWxlmrUqGYC02ZNQgvbsHGjzJk7V/Md1rD1Dz/8UHZp6FN44SGU6ZAhQ22781W2bBn5bvgI2w9hC2E3HfPUHIYINdqjR08Lwwlhcqd6Ci5btszyJKIcBDnY9h077BfegOs3bHDlVIwSNYps3brVZnfs3btXJk6crOE3/XouWsWn/MI/GBUrVJBvvx1o3pTo2wY9brNmLfwIqE6ziPk9f/4Cl5cj8hR+qt57DRs2lv2a4Bf1kVh49Zo16q7/rv2DlDp1KkmdKqV06tTF/kHBPyQTJk6SUaNGW7M49xvKDOIvLDhewfV5167dUq16DRM80Z7zD2aoUKHllnpnIhztwoWLsMvEXp9/NB//0xDcPWEN8IsESIAESIAESIAESIAESIAESIAESIAESIAESIAEnpEAxlVh8xcslhMnT8nEKdOkXqNmPp+GTeXmzZsmKg4aOly69/KS2CoG1vfNgYi6XgMG6njyJmujcoVycujQYWneqp00ad5GEiVKKDmyZbV9DzTEKQxhWP/P3l3ASVV9ARw/dNfSCBIKKB2KhAjSpaACBmGgiBiElIDSjZQgrYKUIColipQSon+kBWlQQLpD+n/OXd4wLMvuEgvD8rv//+7MvLjvvu8bZv28M+dcC1Jas0frf9PmrfJ2o6Yy/fsfpHnTRpIuXVrfetveaxcuXNR9LgcX7dlF/V8CLd9qQdGPOnSW5q3ayrgJE+X5ms+53bzz8/rgEYG7TYDMxbvtit3geC1L7ePeWlNaMwbfa9TYpV0XKvSoTJo4QUIrEVqyRAlp26a1vFH/TRc4szTtdpopaMEza9U0Y3DKlKlSuEgxiRUzlgv6aVzO11o0bybvvdfYrbcsx5o1a/jWBQUlk/HjxkrHTp0lT978EjNGTIkRM4a8qHMkvqPlOr2WOXMmadeug6xfv94F6Zo3e18sM9Ba166dpZH2P2bMWEmVMqXU0D8QU6dOF2/eP7fRDf5q1aqFO+dSpcq6cSVKlMjNDekF5fy73f7339K0aTP57rvJvizNYUOHuGBq9Ro15YxmbJ6/cN7NMTley49aHzE0bX7o0MHSrHkLne8wv0TTb+Fkz55N+vbp7brOlz+fZMx4v+TNV0AG9O/nbMPzCmvMdq026DdzqlV71v1htD9cH33YRrLohMfWPh000F2L5i1aumBttapVr7hebiP9Fd57wtuORwQQQAABBBBAAAEEEEAAAQQQQAABBBBA4FYIvKDBOPvxmt3b9AJzdq/1nbfquxKm3npLILTgYMvmTSRu3Lhusd2P7tCujZzWeRBj6pRXXglT6+eC/lirW+sFX1KG13/mTPfL4E/6uvu5to23vGKFclKubGlb5JoFDP1ujUuD+vUkht7ztWZzMdrPMU3QsdKt3j1mL5DpNuIXAnehQLRBgwdfbNggeM62u3D8oQ551pz5bvnNznEYaP2EerI3sNDmFbT5Cy3IFZFm8yzapLShNevLJtC14GNozbLv7EP8WhPUWjDw8OHDkiJFCt+HuvXTtWt3rUO9VsaOGe2yHBPoB6/VyQ7ZbGz2x8H7UA65/mZe27yRR44cDrd0qW0XWoDW/thYNmWyZEGhrrexndTU+PNuAuBEVwzV9t2jExan1MCp/3W6lpe3c1hj9sYTFBR0RZ/evgcPHnIlYa91Lb3t7DGs94T/djxHAAEEIlvAJlSnIYAAAggggAACCCCAAAIIIIBA1BKw+702p2J4ze552rb2aAG7k5pxaEE8L3hn67xgon9fFzXT8OChg3JO782mTpXKVfuz9XYfe7fel02m00nZfVL//m2ORLvXbfdzLSgZW7e1dlar20XTGKUFLm176zO6hhstocbb322ov+y13eM9qPfVrT3U/Jg0qZrBPbdffaf8I3/1uvJesW8lTxAIAIG0adK4UVwdrQmAwTGEyBXwn/8vIke6VmDR9g2vL8v6C6tZVmMq/fAOq3klUkPbJqyxhbb99SyzgGFE5kQMLbBox7E/XBYcDKtZYDa05v54XvpH6r8+PK+wxhzeeCxIG9EWme4RHQPbIYAAAiZg/6F/Wks80xBAAAEEEEAAAQQQQAABBBBAIOoIWBDuRr9QbNN4hWwugUP7tDxFu09qAUKvhXYcL/jnbeN71Om+rqfZsSy4acFO79HOjYbA3Sjgn5h09cRqd+MZMeYoJxCkmZJeDesod3KcEAIIIIDALRNIlDDhLeuLjhBAAAEEEEAAAQQQQAABBBBAIGoKWDDRm1fRP7AY2WdrgUQ7nv+j/zFz3BdTVm0NDobao72mIRCoAv734XinBupVusfH1eDN+ve4AKePAAIIIBARAStXkkInbbe5C8hgjIgY2yCAAAIIIIAAAggggAACCCCAQKAIdH8hjrSacEzmrDrkAov2moZAoAlYxqIFFv2nv4t59BQpuIF2oRgPAggggAACCERcwP7DJihZxEs7R7xntkQAAQQQQAABBBBAAAEEEEAAAQQiTyCtTl/3a77I65+eEYgsAcqiRpYs/SKAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCAQxQQILkaxC8rpIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIBBZAgQXI0uWfhFAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBCIYgIEF6PYBeV0EEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEIgsAYKLkSVLvwgggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAghEMYGYUex8OB0EEEAAAQQQuMcEzp49K8eOH5fTp0/fY2fO6SKAAAIIIIAAAggggAACCCCAQFQT+HPHeekz86ws3nhOLl68eNXpRYsWTYpmjSlNK8aSnOljXLWeBQjcaoE4ceJIooQJJVasWL6uCS76KHiCAAIIIIAAAnebgAUW9x84cLcNm/EigAACCCCAAAIIIIAAAggggAACVwlYYLH6gFPyZO6k0uiphKJxxKuaxRtXbTuu2x2Wr9+LR4DxKiEW3GoB+0K//aRIntwXYKQs6q1Wpj8EEEAAAQQQuG0ClrFIQwABBBBAAAEEEEAAAQQQQAABBKKCgGUsWmAxb+bQA4t2jhZwtPW2nW1PQ+B2CfjfhyNz8XapB9BxLMIcO3Zs/RC68msPgwcPkdKlS0m2bNlu+WiPHTsmI0aMlFdffUWSJk3q6//ChQty5swZiRs3rm8ZTyIusHbdOlm0cLG88Ua9iO8Uzpbnzp2TmDH5aAjJhEtIEV4jEBgClEINjOvAKBBAAAEEEEAAAQQQQAABBBCIbIEYMWK4e8ux/O5dWtlQu7986PBhiRkjpq5PIqdOnZLoum0cvQcestpRvHjxXHnH6NGju5Kjdo/c+jhuX17W5wkTJLjivvn58+fdVCyJEyVyy21777667Wf3tw8fOeLGcCvO30qhWsZiRFqeTAml/7TDEdmUbRC4JQL+9+HIXLwlpHdHJz///IuUK19RHno4p+TJm19ef6O+HDhw0Df4TzW4uGHDRt/rW/nk6NGj0rffADl0KPjDbteuf+WN+m9K9uw55OEcuaRixcoybdr0W3nIO9LX99/PdL5mbOdVomQpadjwHfn7778jZTxr/1wrw4YPd30PHz5SqlR5+orjTPhqotyfMYtY2UCvzZs3z11/+6MbWiv0WBGZMnWqW2XXbd26v0Lb7KplW7Zs9Z27nb8d137suf306dP3qn0CdUHI816/foM89FBOsfctDQEEEEAAAQQQQAABBBBAAAEEEEAAAQRuv0DKlCklts759ufadTLqy3Hy1aTJsnffPpe4kjZNGkmZMoVLmvjnn52yefMWV77RgoheSxA/viRNksQFE3+aPVdGfD5KZsz8USypIHHixGIBRLtnOm36TBnx2Sj5ZcEisYBmsiRJ3eOS3/8n4yZMlLHjJ8qYcV+J9WGBRv9jeMe60UcLWGqXEWq2nW1PQ+BOCFz+l3Unjs4xb5vAL78skNdfry91ateSFcv/kOnTprhjP//Ci2Lfvrjdrb4GFu2Deu7cn2T16pUagHtLmrdoKRb4upub/SGyQN7sn36Un2b9IB/37iXRokeTp6s+IxawisyWP38+Wb1mjRzWb+l47RcNKFtb+scf3iL5Xf8IZn3wQZe96lvo92T4MM1gLVXKLVmwYKHYeyQi7f77M7jztnO3H2utW7fyLatX77WIdBMQ24Q87wceeEBGj/5c0qZNExDjYxAIIIAAAggggAACCCCAAAIIIIAAAgjcSwIW5LOY28pVq2Xgp0MlW9YHJX68+NK2XSexqnmiMbZZP82Rdxs3l159+msAMjhhwssyNKs4ceI4sr4DBslfmmTzSIH8mhTyj/TtP8gtt19de3wsu/fskXx588hPc+bKxK+/1YzG4NXz5y+QI3qPN0GC+BIvXlyJpZmR1qwqXyoNfKZOlUqCkiWTlClSSCp9nkIfkwcFuXnqEiZMqOON557b+uQ6d53NX5dIl9MQuBsFqH14N161Gxhzjx49pVatF6VOndpu7yT6DQ0LfOXLX1D+97+lUrjwY1f1aoGyWbN+ks1btsiDGlwpW7asfvMjhttu2bJl+qF9XEqUeMK9tm9ITJjwlZQq9aSkTp3aLbP9Zs+e4z4g8+XL6+vfgl+rVq/R7cdKhgwZ3PKnnqoiFtCZO2++PPnkk7J8xQr577//JIl+Y8QCo8n0Q7hypYpiH8Jes/7nzZ0n5zX1vGiRIpI7dy63yoKlEydOkgoVysuSJb/Jho0bJa/+MShZooRbb9lnS5YskTJlSotlGu7ff8A9f+ih7F7XcvDgIZk5c6YcP3FCHi9WVHLmzOnbd9GiRfLYY4XcvhUrVpCMGTP69rMnmhgv6dOnd8vs0Y6dI0duF9SzY1rbvHmzzNGx2x+U0qVLXxG0OqJp9DNmfC8HDh6UnDlyOFO306Vfdk5Ll/4hmTJldOfurbPj2B/IZcuWu33MYcGCBWJBRwsyFilc2G269I9lUkjHb82ur10DG4+l/r/wwvOybdt2/ZZPSlm+fIWMGzdeg5VHZPz4CfLEE8Xlvvvuc/uFNn4rpeqdt21kY8mg5++/LLTjnTt3Xn744Qf3Prv//vulfLmyEl+/RWTNts+cOZMcOXLUXcvUqVPJM89U85VttfedvcesPKztW1rff/YtI69d6z3irV+7dq0sWvSr+w+CEiVLyH3p0rn3YcjzTqDlELZv/1sKFizo+48Qy/qdPWeOHDxwQAoUKODeE16/4Y3b245HBBBAAAEEEEAAAQQQQAABBBBAAAEEEAhfwMqPWlu2fKXe531C7wcH38/+nyZV/KVVxx59pKDeF0wk7T5sJTO+D058sO39g4tn9X63ZRlaX2++/pre54st2bNlldfffEcTRs6JlXxcr0HHj9q0dPcf48aNI6PHjJfnazxrXbnSqy8+X93dr3QLLv36e/s/kixZUvlnx053T7eg3o+1sfxP78NaidZHCxZw9yztXqYl3Pz+vz9cwk+unDk0AJncBSf37d/v3yXPEQh4ATIXA/4S3fwALUi35s8/tSRquSs6swDjd99+c9WHoW108uRJea56Tek/4BMXPOnTt5/UqFHTfRjaeithOmrUaHvqmn0wtmzV2gWpbMFvv/0ulSpVkV8X/yorV67S7LeXgjfU33ZcCxJNmvi1CyB6K3r27C6dOnZwL2d+/4M8//xLrs9/duyQTz4ZKNWeec53fAu+Va78lGzRAOP27dt1rDVk+vQZbl/LHLSxVNL1Ns5tW7fJK6+8JpMmfe3Wr9+wXho3eV9eeqm2C6BZsLBKlae1r61uvQXOypevID//8ouWid3gxvHdlOBMT9v3/WYt3L4LFy12QS+3Uxi/zMZy2b16xDbOatWe1XFt1UzN+VKhYiXZqAFQa1t1rFZKda5mcFqQsWWrD+Sjj9q7dfZr6LDhUvflV9x5Wz/Nm7f0rYulJQEK5M8v/1u61C2z4GAMDfi9Wf8NF6C1hRbIW7lypRQq9KjbZuCgT9X1WenXr79s0vO21r5DR1ml3wDaoe5//bXeLbNHCyZbC2v8boMwfoU8npnYtRsyZKic1EDuSJ2Xs7q+72yc1mz7WrXrSncNju/6d5d06txFWrRs5TtCo8ZNpGOnzi5bc7S+Hy1D9NChQ259WO8R22DMmLFSs+aLsmnTJrFrWUFLBq9YsTLU8/73393uPeVqr+u+5mHvkRkzZsjOXbvkrYZvS/v2we9d6zu8cds2NAQQQAABBBBAAAEEEEAAAQQQQAABBBCImIAXJMyQIb27j2n3XC1jce/e/ZrcEJwQ8Zje87SsQGve9iF7twSJls2auMCirftr/UaXaBErVkyXgGBBws2bg+8Tb9i4STJqtTavHdT7jqvXrHUlUa1Eqtem6r3q95q2kHV6z9DuGzZr2Ubadeyq963/0fuNq6XNRx1dMNGSeVq1aaeJEn/pPcV/5YO27WWbJjREjx6c0OP1xyMCd4MAmYu34SrZNyLsWwuz5sy/6aOVK13yuvvYuXOn1l4WSZM6zVX7WrZbaG24BnmO64fz999Pd5la9g2LCjov4oiRn8m777wd2i5XLOvevYc8U62adO/e1S0vWqyIvPtuY/fcPtiHaenNd95+VwoXKSaVNPvv2WeflUf02yVecwE5ffHtN5NdtqRlOxZ/oqRM+nqy1NXsSwvG9ejRTao+HTzHoNXDtvkFq1Sp7HUhjz76iAzo38+9jh4juvzw4ywNkFb3re/Vq6c8/PBDri518eIltETrXMmSpZ60a99Rymr2XNcund22FrDr1q2HVKta1bfv8OHD3L6+BX5Pzmhwc/fu3W7J7t179FyHa5p7kDz+eDE5ceKktGn7oXTt2kUsW9PaO+++J4MGDdYAXx+xTLratWpJs2ZN3Trbp0GDhhpgbKv7nnDzFloQ1hvLk6XK6B+7LW5b+2UZlYt/XeJez5s/32VrWsbhe+811vk1D8jOnbtc2dZHH3nEt8+pU//JTz/9eNUf3BdffMF9o+aD1m2kQ4d2bvvwxu/rNIwn/sez4G0mzfy0c7JMR8sUzZMnnwY3V7psQAsUmuFvSxa78dm1aGdBvI/FBZqnTJkqE7+a4M7bMjVfq/e6rFnzpxQv/niY7xF7P3Xp2s29P8qWLeNGa8HBwTrv6NChg68675DzTnbQAGzJJ0tKb30PWXv1lZf1PVNB339V3Ps4rHG7HfiFAAIIIIAAAggggAACCCCAAAIIIIAAAhEW8OY1LP1kCZk3/xep+dLLbt9aL9YUm2/RmiXMWAWyawUWz+t9eruHaAFGm7Nxuia4WOvZrZN7tP3ee7uBtG0f/DqmlmIdMSy4ZKrdr7Z70FZtzkqd2v67dv4rzz7ztOzatVvvcd4vtV963vVT48W6WiY1hViWo+33Sr0G7h7nRa3dalXi6r1a143hwQeyyAkd84ULwYkWbmd+IXCXCBBcvA0X6sniRWXegsW34UihH8IL1F3rQzW0vRYuXKRZYE+7gI+tj6flO59+6ilZuHBhuMFFlx23apXOodjM1/UjWk7Sv+XPl0+z9ua4spfffvudluN8SYoULSyDPx3kSp9aNpg1rwxr0qRJtVxqSZd1pxNHSuPGjVzGogUU9+/bLz9r2U9LKfdvVuLTa1Yyc/Pmed5L92iBRWvmYqVBLfhmf1ysDGv16s/6Mh0tg9Ay12y917x9vdchH4s/8aRbZJl5iRIllBnTp7mMTStBe+jQYRco9DIpY2tt7pWaMWetcuVKLjBmmZL/6rdXLOP05MlTLovRyrvaOVapfDmAWlODpd26Bwe4bH8LLg7WLEDL3pyvWZFvvlnfeebX+uG/aNnZwxqsy/Hww77ysud0u/Lly17zD6716d8s+BnW+P23De15yONly5ZNPv64t16/n13m6Kn/TrmsRStdq5VG9Rr/LcWKFvGNz66jZVCag70n8+bN64KEL9et40rBjvric99hw3qPrF69xh2ndOnguSVtp2bNm8kRv/kqfR2FeGK2v/3+u0xoMs63xuZktNKo9u/DguRhjduuNw0BBBBAAAEEEEAAAQQQQAABBBBAAAEEIi5g9wKtzfzxJ4kdO5Z80reXqyo3aMgwya9TclmGoSV9JNBtvPvhbge/X7bcgnk2FdcLNatLpQrlZIFOmfTJoCHSq3tnved4Vj4dMsIFGHPoveNpM2bK5198Ke80fNPdn+zbu7vr2+4nP/hgFun3yacuuGjBwTy5c/qOdF+6tO6epi2wbePrHI1WXTBLlsxS+LFHdV7IZjqVVV55QhNLHsqezbcfTxC4mwQILt6Gq2Xzx1UuH5wddRsOd9UhbJ48/QzTFPG9V5VAtfn1kiRJrDWhk12xn80jZwE9/2Yp4TbPXHjtxInjLnBjH9JhNQuyWKah/fzzzz/y4ou1xMqvfvRhWzfWkPvaXHp2DtZGfzlG+lqp1urVJb2mwifWc7Ago3+LE0YQxwta+m9vz72Smqu0lKsFBr321FOV9ZzOuZeWIh9Wi63lSTduWOc2GTt2nPTs1VuCgoJ9vYzGBRqE8m+FCgXPgWhlOevUqSsVK1Z0mZFB+i0Yr1mQ095L9s0ar3l/VL3X+TWzzwKkVm71r/XrfXNiPqnzCdq8i2fOnvHNt2j7xNSx5s1zeT5Mr59rPYY3/mvt5y0PeTzLIHzuuRqSJm1aN1b71o9/s7k6i+qcl9dqE8aPdfNBWqC21Qdt3Lyc3bp1cYHHsN4jdtyECRO4Gute3wn1W032E147qpM2WwA9adIkV2zq/n3oN5eshTfuK3bkBQIIIIAAAggggAACCCCAAAIIIIAAAgiEKXDx0pyLixcv0Wp2VSRNmtTuxwKLv2mJUgsuhhZUtGV2H9rumSZMmFAfL7g5Gi2ol1KzC5+t9pRMmTZDp2TaLQcPHtL7r/Gk+OPB9yNrPFfNzcfYoH49OaXBQVvvlUm1ab+8KZTS6/33VClT+sZv93C9TEvfQn1igcY333hNjh0/ruVSV8nAT4dqtuMLLuBo60Ibv//+PEcgkAQuRykCaVSM5ZYKWAAqe/bsruynZbZ5zWpSly5TVkaP+kKKhQjgZMqc2c036G1rj+t1YtzMmTPpM3EfyPZNEK/5f/DZnIqWrbdly1bJmTP4GxunT1/OKpwzZ6507tJV5sye5fuQtczB4sWLy3qtSW3tgSxZ5He/utW2bKv2l00n2LXWv98ALRX6oZZeDS5VamVSrXxmRJt9WIfWkidP7mprN2nS2GURhtxm7brgoGHI5dd6/fzzz4uVmP3kk0HSps0HYpl31jp2aK+lUpNftduIkSN1rspKruSrrfx1yRKd2/JLt12G9Old1t5+ndw3xaXa4Wbs3+xa58mdWz777HP99kt+ly1p60tqcHHU6C9d8Mwrx+q/X0Sfhzf+iPbjbWfzIp7TP+xjvhzl/rja8g9at/VWu8doEvq1skzSP/9cK/XqveZ+LPBc5amqMnnyN1K7dq0w3yN2HhZItkChBa2t/f333y7j0EqqhtXsulkJhA0bNur78fI3i+x1kcKFfbtea9y+DXiCAAIIIIAAAggggAACCCCAAAIIIIAAAhESuHhpq3T3pZP1OtXSo48UcAHDzXp/tGyZ4Cpyod3zDdKkmhha3nTvvn3u0RJIOnXpIV07t3eBQptGypal0Ht+8eLGlT1798khTUxIpok36/V+X5BOd2XJHid1uquOXbpLn55dL1Wo+0PvYWe+9thDuaW5XAOKX02aLJ3af+gCmOs0OWTL1m0uuHjtjliDQGAKRA/MYTGqWy3Q7P2mmsI9SqZPn+FKZu7TD1ObS8+CLI899thVh6tTp5YL0vz22+9u3eJffxUr1Vm3Th332spArtLSp5bJZt/Q6N9/wBV9PPvsMzJw0Kduvc3TZ/Pbec2CXpaF17FjJxfcscDkSs0UnD17tpsn0bazAKW15StWuMeffpotixYv9s2pmChxIlm6dKlLJ/9TS4daAM6Cbjfb7A9QzRo1tFRnH5dNaWNbrMdt1KhJqN8csT8806ZNvyLL0X8MliHZvNn78oXa79ixQ3Llyim5cubQQOOHbv5FK7H5xajRMkRLmVpLlDCR/OlKj9pcg7ulZcsP3HI7To4cOTRInE26dOnm6ofv2bPH7RtD55P0b968i6VLBf9RtXUPaylUy8TcuGmzFHr0coDZf7/Qntt1sD+cFoi2Ft74Q+sjrGUJNQht70ULzNn7aMAnA93m584HZ4mGte9/ej516r4sFqC0Zt8GsuvnfSsorPdInjy5XWCwR89ersSqnV+z5i1k7rzg0rkhzzvkOGrXfknnyOyv9dT/df8RM2z4CDmw/4BU9ZuXM+Q+vEYAAQQQQAABBBBAAAEEEEAAAQQQQACBGxO4cClz8YUaz8nGjZul8fsttbxoc0mf/j4pViT4C/8XNInB2nnd1tvepleyjMOmzT7Qx4NuGrCGb9WXbj0/lvdbtJZO3XrK2w3e0EpocTWhI7m8WPM5+aBte2na/AM3r+J7DRu4Pi1T8blqT0vzVh+6dT/+NEdef+0Vt87mUrRjes3uKV+84IVDdam+ttKCVjrVApKNm7V0fWzevFUqlCvj7cYjAneVAJmLd9XluvHBlitXVj7u3ctlDL7XqLH7EC1U6FGZNHGCb15D/95Llighbdu0ljfqv+mCUnHixJF2minoZXVV04zBKVOmSuEixSRWzFgu6OefDNhC5697773Gbr2lndesWcPXvZUIHT9urHTs1Fny5M0vMWPElBgahHtR50h85523fdtlzpxJ2rXroBmT611gz4J0BS/N3di1a2dppP2PGTPWpZzX0D8qU6dOd4EiXwc3+KRVqxbunEuVKuvGlUiz1Dp36ujLrPPvdrtmuzVt2ky++26yL0vTf709t3kUhwwd5sqjDujfT4YOHewCWXn13KNpQMwChn379Ha7vffeu848X/6Cmh0aRzPwXpKRIz8XK1+bOnVq+XTQJzqP4luSM1cel8Zvc0Mu0LkU/ZsFFz8dPFTnILw8n6AF3TJnyuT+kHolWv33udbzfPnzSUadjDhvvgJiY7cStmGN/1r9XGt5xQoVxDJZK1aq7K5xpUoVJYP+B8F2Pd/w2n3p0qnHQPc+at6ipQs0V9PgnvdeC+s9Yu/JYUOHSKPGTdQyr35rKbrYWFq2aO4OG/K8LZju35o2beLKF5R8Mtg4nY5l1KjPNBv1yrKu/vvwHAEEEEAAAQQQQAABBBBAAAEEEEAAAQRuTMCChNbs3maHdm3k9H+ndcqnmL5EAwvoXbAgnra6tV64fC9X74sm0KmQ+vTu5hJa7D5psSKPSdHCheT4seNiyQ+2zGsVypeV8hrwO3nqlCTQ8qbWr9cqVSzv1tn8idan195v/K6GFy+3Tu3bSnTNlrR9re9BA/q4cdrrN+q94pIVLHHC+qchcLcKROvW59OLrZq8dbeOP9Rxz5oz3y0vV7pkqOsjuvBW9RPR492u7awcpJWCtHTwiDSbZ/FaQRPry2pIW/AxtGYZYXE1nTyWzu0XWrM/Cjb/nZX59DLObLuuXbu7DL6xY0a7LEf7sPafa9Dry8Zmf1D8/wB462720ebVO3LkcKjlS/37tu2uNYej/3Yhn5/UyYOt1rcFL0M2y+y81jnbtvZtG5vjLzLOO+RY7I+eZUmm1Lrh/u+ZsMYfso/wXp/SP9YX9Ns8CXRy4xtp5mH7hvY+DO89YhmTFmy0H/92rfP238au/fHjx66an9R/G54jgEDkCvyrWd40BBBAAAEEEEAAAQQQQAABBBCI+gI2Z2Ii/QnZLEvw4KGDbvql1KlSucp9to3dk7Z7mDaVlN1Htft99uN/H9rr67ze59ONXBKCt8x7PK33sGPqvXT/e6PeOnu0qnPWvPV2z9c7hj1aFqV3H9d7dDtc+nX8xAlf5biHWxyXRk+lt6GE2/RUpP+0HbKu59Um4e7MBgjcoEDaNGncnmQu3iDg3bxbMq0zfT3tWoFF6yO8vkILnPkf2wI6qfQDP6zmlUgNbZuwxhba9tezzAKGoc2LGLKPGwksWh8WlL1WC+ucbZ/ryT681jEiutz+4KW59IHhv09Y4/ffLiLP7Q/8zbSwPMJ7j9h/lITWrnXe/tvatU+q9ddpCCBw5wTsSwVW9pmGAAIIIIAAAggggAACCCCAAAJRW8CSBOzHBfEsUKina/fwLJjntdC+hHzk6FFvtXu0fSzoZ4FGe27BP3tuzV7bj3UeLfqV67zgoW0fQ/e3Uqjefm7nCPyyPmwf+7HnXmDS27Vo1piyattxyZs59HuW3nb2aNvZ9jQEbpeAf3LPlZO13a4RcBwEwhGwiXLTpUsbzlasRgABBBC41wVC+8bivW7C+SOAAAIIIIAAAggggAACCCAQlQUsmOjNq+gfWIzoOVtgz/azIKE9+gcI7bktv3Dx6nXuuJe2Pxdiv4ge2zuuHSdkYNH6eL9SLJm3+rCs3HpcxxV6r7bc1tt2tj0Ngdsl4H8fjrD27VLnONcl0ODN+te1PRsjgAACCNybAlbiJEXy5G4eVDIY7833AGeNAAIIIIAAAggggAACCCCAQFQRyHFfDJncKJ58/P1xLXl6+IrAp3eOlllpGYu2nW1PQyCyBSxj0QKL/tPfEVyMbHX6RwABBBBAAIFIFbD/sAm6zpLfkTogOkcAAQQQQAABBBBAAAEEEEAAAQRuUCCtTmlXuuAN7sxuCNwmAcqi3iZoDoMAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIDA3S5AcPFuv4KMHwEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAIHbJEBw8TZBcxgEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEE7nYBgot3+xVk/AgggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAgjcJgGCi7cJmsMggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAgggcLcLxLzbT4DxI4AAAggggMC9LXD27Fk5dvy4nD59+t6G4OwRQAABBBBAAAEEEEAAAQQQQACBKCRQuusJdzZzWieIQmd1951KnDhxJFHChBIrVizf4Aku+ih4ggACCCCAAAJ3m4AFFvcfOHC3DZvxIoAAAggggAACCCCAAAIIIIAAAgiEI7Dz0IVwtmD17RCwL/TbT4rkyX0BRoKLt0OeY9wVAl9/PVkuXrwoD2Z9UPLny3dXjDkyBrlp0yZZvnyF67pixQqSUL+RQEMAAQQCVcAyFmkIIIAAAggggAACCCCAAAIIIIAAAgggELkCdh8uKFkydxCCi5FrHXC9L1+xQlatWi0v160TcGO70wN6v1lzKVSokNSo/txNBRe3bdsuU6ZMkUaN3rvTp3TN47sxTp0qjd5796pt/tmxQ77/fqbMmTtPChYsSHDxKiEWIIBAIAlQCjWQrgZjQQABBBBAAAEEEEAAAQQQQACByBRInDixxIsbV6JFi+Y7zIULwdl90aNFl2jRLy+3DSyZxn5E/y+XVtm+tsyqQR08dMjXjz2JHj262DFs/YXz5yV27Nhy/MQJ9zppkiRuvbeD17f15/2EXHfmzBk5dPiwt5jHu1zA/z4cwcUAv5i7dv0r58+fkwwZMtySka7QjLQxY8YSXLyGpgXbHn+82DXWRmzx1m1bpV//AQEdXLQxDhjwSajBxSdLlpRs2bLJnCKPR+yE2QoBBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAgUgWSacZYXJ377tChw7Jg0WLZu3efPPBAFin5xOMuuDfzx5/kwMGDGkP0AowXJUaMGPJCzeouuLh0+XJZs2atC04Wf7yopEuX1pW59KabSZUypdveTmLnzl2ycdNmKVmiuMTT4KUFNC2Y+MuCRbJh4yZJnTqVlCtTSuLq8mUrVsq6dX/pXt5xrYeLUr5sGUmWLKm9oEVBgehR8Jyi1Cl9OniwdO7cNUqdEyeDAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACERewwOIJzSJs17GLnDl9RvLmya3V5+bL2PFfuU5Sp0op96VLJ+nT3yf3Z0gvljW4avWfbt3MH2fJpK+/lYceyiZx48WVD9t3dkHKmDGvzD/7c+06af1hB2nbvpPr23aOGSN4m4lffyNz5v2sVf/yyA4NPnbr+bHr2zIa06VNKxkuHTdFiuTyw6zZLvBoG1gAMolukyxpUokXL54LVNpzK6+ZMkUKSR4U5IKc9hhHz5F2dwjETBzPP5p8dwz6XhmlzQE4btx4OXfuvIwfP0Gqa7nOWLFi6etzMmvWT7J5yxZ58IEHpGzZshIzZgwfS3jrfRuG8cT6t2zJzZs3y3Gto/vCC8+7rRf/+qss+2O53KcfFBXKl3MfBl43K1eukkWLFrm06SeeKC7333+/W2WlWP/77z9JounUv/yyQJLph0TlShWvKLd54MBBmT1njhw8cEAKFCggjz1WyOvWnWvmzJnkyJGjsmTJb+5bEc88U03POfhDzb4xMXv2HFm7bp07ZulST7oxeB0cPHhIZs6c6dK3Hy9WVHLmzOmtCvPR3H/44QfnbOdSvlxZiR8/vm8fyyq19Wf1ehQq9Og1S6nu379ffvppthQv/rh+sKf37R/ak1+XLJE/li6TTJkyOofffv9dnqlWVbNXz8vEiZOkmI5//s+/SJbMmV2G5ZEjR2TGjO/dN1Jy5sghpfTcrdnYJ02aJOXKlZP58+fL7t27pUjRIlIgf/4rDnv06FFXAnX//gNSpkxp/eOS/Yr1vEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBC48wJ2j3ilBgtTaRCx+nPV3IASJIgvw0Z8IbVfekHy5893Re7gol+XSJXKFdx2FhR8/dWXXXDRFixfsUrW/bVeihZ5zK23X9a/3mqXeq/UceVSp0yb4dbFiBGco/Y/vW/9cp1akjtXDg1s5pHar7zugp1ZMmeSzHo/2yvVOm3GTCmh98ITJkwglhVp99Hz5M7l+rL70MeOn5CHNchpbfOWrRqD2KL3/FPrNjldwNHKqFo8gRbYAmQuBvD12bhxowsS2RD/0n/oVjv55MmT8lz1mtJfS1paIK5P335So0ZNOXXqlPAo1JAAAEAASURBVDuT8NZH9HQHDvpUqj3zrPTr1182aYDRWoeOnaRp02ayb98+GTF8hDz3XA3xauxa8PPFl2rJrn//lcWLf5UKFSvLsmXL3H4zv/9Bnn/+JWnZqrXYfH6ffDJQ+37ON2Y7t/LlK2iQbIbs3LVL3mr4trRv38Hta79sLLVq15XuPXpq/7ukU+cu0qJlK9/6Ro2bSMdOneWwfuiMHjVanq76jH7rIrhWtAVHre+ff/lFNmzY4Mbxnc6HGF6z83queg0ZMmSonNRvg4wcMVKDuzV918MCmaVKl5Flmkq+fft2eeml2vLFF6Ou6taCfzb2hRp0TaffGgmr2bHq1n1VtmjQePr0GfLEEyWli56rNat/bX7lylfU+Rynyr/qvHXrNilRspTMnTdPA69HdP0H8tFH7S9tf8ZtX6LEk7Jo8WJZvXqNu14WoPTa2bPn3LiXa6lcCwpXqfK0Hnurt5pHBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAgQARsOBd0cKFpE2r5r4R7d23XwNzKd3r85oEY4lH1v7ZsVP++WeHFNakGGu9uneW7NmzuucWZzigsQXLdPRv0XS+xVw5H3alVoNLnEbzX+2yITdu2uSWWeJTUFAyl4xj/XnHtQDl9zN/lMoVy7vtNmzYJN9OmebrZ7kmKM3SRCFr8+b/IgMGDtZg43GZ/O0U+XToCFd6NaaWcqUFvsCVOa+BP97bPsIt27bLrDnzb/q45UqXvO4+PviglZzQYOI+rZ3coUM7t78FFY8fO6bZZtNdirAFFS2QN2LkZ/LuO2/LcA2ChbX+egZx6tR/mnH3o/vGgQUKx3w5VrPm5rrUagu+lS5dVr79boq88HxNGT/hK3mz/hu+eQa7d+8hv//+P5d9Z5mF1r79ZrLLsLQgYHENnE3SzMy6dWrruXWUkk+WlN69errtXn3lZSlbroIGu6rII48UdIHC3bv3yG9LFruxWPZdOws+ata1nb8F2yZ+NcFlO9qH12v1Xtfa0X+6TMF27TtqX2Wla5fOrm/bt1u3HlKtalX3+lq/LGCYKWNG6dmzu3O2SWvz5Mknq1atdOc0Rc87X968MvCTAa6LIoULyw8//iiv6Ni9duLESan78quSNk0a6d+v7xWT3XrbeI+WQdhXA7l2PMtUtGaBRAu8+rdyei4D+vdziyxjsXatWtKsWVP32uaKbNCgoQYY2/p2qVXrRbH3kbW+Goju1au3Bhmf9a3v2auH5Hj4YfehXbx4CZk7d65kyVLPt54nCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAndewO6zWxAvugYBLdBoGX5fTZos7zd+1w3ulGb7WdlRa1M167BihXK+e9IWALT5F619891UVzrV5mv0b6d1/1O6XWKtQBicAhl8X9/b5tWXa8tr9d+W8V997RZ17vChL1vxtJZgtaqLvyxc5KrypU2b5tJuF33beP3odIyuWaCxtMYFnn6qklZJLCszNEnJzuu8joEW+AJkLoZxjSwgmEXTeQOpLdR/nE9XfdpXe9g+LJ5+6ilZuHChG2Z46yN6Luc0U668/oP2UpkX6gSx9gGxWB8nTfpapk6dJsm1HvLq1atdl0UKPyZjxo6TERrc3KYB2VatWmqg6023zrIRrXmlW5NqPeUn9UNj5cqVLiPPSn/WrFHDbWO/HtBSr1Ya1Tun7dv/lmJa0tMbi5UoPXbsuKsZbeefV4N8Xbp2k8mTvxEL0o364nMXWLRAo5VhtRRqG7P9WIbfv//udt/M8B0wlCfZsmWTjz/urWNYJMOHj5TPPvvcZS1aCre1xx57TP5YtlwsiGrZi1WqVJZBAz/x9XT+/AVp+PbbYlmBQ4Z86ivh6tsgxBMLIlrAtkrlSr41VgY3ZHv22Wd8iyrrtvXrvy6WiTl48BB3fidPnnLn6G1kJXO9Vq1aNdmjgWorkeo1CyxaM1srg2vfWKEhgAACCCCAAAIIIIAAAggggAACCCCAAAIIBJbAXq0oeFKTbSy4aPe8e/TqK888XUWnTgsOElrmomX92RRkfyxboYG7EmKBPLtnbvtYW7R4ift556367rVVzPOaZRBaslPI5u07dPhn8lTlijJkYD95p2F9GayZhlYdz+4tJ9DpxCz4OXX6TKlSKbgUa3A/lv1oP8HN+ooWPfi1bWdZjB06d9PKgwt9+9kcjbTAFyBzMZxrdCMZh+F0eVOrrRSqBef8W7JkSd0Hhi0Lb73/fmE9j6nfMsibJ69vEy8gNf/nn33LbGLYB7IEf3BZMDGP1lmeNm26y8DLmvVBl2FngcC9e/f69vGe2LcfbLl9sNn8gEmTJvFWuUd3TgcPuuf58uWVojrX4LXahPFj3ZyUFjxs9UEbN59jt25dXL1n22eVfgPCAndee+qpyr40bW9ZyEfLrrSyr2l0ItoSJZ5wk8r6b1NK5zacOHG8fDVhorz11tvuA7RH925uW287m4PSmj36zyHprfd/tKCnBUrt2x1eixfKh2iihAm91bJixUqpU6euVKxYUR5++CFNQw/yrfOe6Oe6r7lvnOiro0ePuWVesNe3AU8QQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEAlLAgm6JEyVycyN+3G+gFMifV8qVLe3GakHCBAkSuOcztCxpiSeK+bIYE+o9ZQsArtZqfxMmfi3t2n7g2/Y/vW8eM2ZwmMgCf2c0wShks0RDS0b63x/L5PPhg10pVJtTcZoGEjds2Cg5cjzk+v9DE21i6/3tnDmCE1q8fuy4Xlu5arWcOR18jGwaQ/ikX283VddcLZH6YfvO8nHPrq4Pb3seA1eA4GLgXptQR5Ypc2Y3d6D/yvXrN0jmzJncovDWu41u4JcFCe0fu392nteNBQcXLlrosgUtm84+yF5/o7706NnLbW8BSCuR6t+26tx+2bJlleTJk7sPRPsQsmxBr9lrKzXqtWh+327wltmjZdr9+edaqVfvNfdjAcsqT1V1WYy1ar2kH5LxpUmTxmLjup5mJUfPaebjmC9HuQ9G2/eD1pfLjf7xxx+SMmVKzVzs6rodoPNIvv9+M1m69HffYX6eP1eGDhsu777XSH6Y+b2rQe1bGeJJ+vTpNRh60gVcU6VK5dZu2bo1xFZXvhwxcqRUqlRJevTo5lb8umSJjBr15RUbHdfJcb229VJ/6dKlld17dvvOy1vPIwIIIIAAAggggAACCCCAAAIIIIAAAggggEBgCiRNksRlB1rGoM2zWLP65emvvKQVyzy0uQx7du/kTsKChbFjx5atWm1wiGYetm7xvqRIkdx3gpbMYok5iRMl1u1iyb9+Ve98G2lGovWfQisZ/qX37QtoMtChQ4dk3z6b7zGV7z7zlKkzfNmHVr7VC1paP6c1oBgnTmxZt269BkXzua4/bN9JKug0YMWKFpag5EHS4O3GoQY3fePgSUAJUBY1oC7H1YNJoh8Ye/bucR8atrZOnVoucPbbb8FBrMW//urKYtatU8ftHN76q48QsSVWenXHzp0yZOgw982IYzrvY+vWbcSObxlwH37YTsuI9nXrrMfo0aL7Uq3tHKwtX7HCPf7002xNvV7sSonagtq1X5J+Ot+glRy1UqbDho+QA/sPSNVw5kW0fe2bFXXqviwWDLTm0qr1Wxjeo5Vb/fjjPjp57T/OcLEet1GjJj5Pt1MovxImSqgfjvvcNy+Oazq4BQ+tnTt/zj1+9dUkady4qcu8tAV2vl7ZVnsdI0Z0sfN+v2kTscBsk6ZNwzzmQw9l1wl1s7n5IE/qHwALko4c+bl1dc2WKGEi+XPt2ktzUu6Wli0/cNt6k+fai06dOrt5Ke0cevf+2AWAvetxzY5ZgQACCCCAAAIIIIAAAggggAACCCCAAAIIIBBQAnbvfM2f6+TnBYs0mecPefPtRsE/DRuJZQRa+2n2XMmVK4ek0KQeaydOBCeffPbFl3L0yFHp2LWH26eB7tu1R293z7pHrz6yYNEi3719t6MGFPX/rtl8jdbebfimfDFqrDRt/oF88GEHefGFGi5QaeVQN2zcJHv27NWEoUJu26MaP/BvTVt8IE2atZKMGTO4e+e2rq4mB02a/J00b9VWWrVuJ3Veet4FQr3j+e/P88ATIHMx8K7JFSOqVLGCfP75F/Jg1uzy25LFUrJECWnbprW8Uf9N942COHHiSLuPPnRBI9sxvPVXdH4dLyzb7bORw13Z0T59goOI5cuVc6VQrZuRI4a5oF3OXHk0Rfqc5M6dS7p06ew7QubMmaRduw6yfv1694HVvNn7UrBgQbe+qQbgrJ5zySdLudfp0qXTDLzPNKvx6jKfbgO/X/fptp8OGigdNYjWvEVLV2u6mgYla9YMnsOxVasWzqlUqbISQ4OgiTRtvHOnjlcEAv268z2tWKGCzJkzVypWquzGW6lSRcmQ/j7Zrt/wsPbhh22kcZP3pdBjRdzroGRBGiDt6577/7JJcgd+0l8qVKyscy8O1RKqDfxX+55bYHLQwAHyZoOGYoaWql62bBkt6brSt03IJ++99657H+TLX1A/dOO4IK0FJG3Oyzx5crvNbf7KRx8trLW4T0p2zQwdPnxoyG54jQACCCCAAAIIIIAAAggggAACCCCAAAIIIBDgAhbEy50zh0waP/qKkfqCcRoMrPp0FZ1n8aK7/22JORcuRQg7tb9clc929vaxioGtWzbTe+eXQ0V2nIIF8kt+zVC0ZpUK7T53dq1EOKBvTzcvY3yd4svX9Liu6uGAj9121rf/NGW5c+WUls2auM3jaBalhi3d86xZH5C+vbu5eSTjapzDjmHNgqi0wBeINmjw4IsNG4Qe8Aj84Yc+wllz5rsVgTZfYuijDX+ppS7bHIBeuUxvD5uYNawAXFjrly1fLtWqPed1dcXjX+vWuLrJVyz0e2Epz/F1glYLbIZsx/WbEBf1w8OCeF7r2rW7y7AbO2a02NyCVvvZPyXa287Kqx4/fuyqOSW99eE9Hjx4yJVBDW1c1veRI4ddGdZr9ZMxUxYZO+ZLefzxYr5NTukEuRcuXHT9+hb6PbEsQTsnK+8aXgvP3MZtH6B2Hjbn5IABn7jM0K8mjA+z69BMbdzZH8op334zSfLly+cyLJMlSxZmP/4rd+7aJUWKPC7z582RLFky+6/iOQIIIBBQAqGW6wioETIYBBBAAAEEEEAAAQQQQAABBBBA4OYFUmpZ0tDuq1vPFhC0BBavqp1tt2//flfhL3lQ2Ek8/sG8vVrNL5VOB2Z9WZDQ7ldbbCK+3tO3wGDIZlOLWTDTtrMxWEXBo0ePugBk2jRp9P72bzJ77nz5qE1L367eWH0L/J7YWPbrVGhe8POh5sEZkH/1uhxv8Nucp3dAwK6rtcvh6DswCA4ZMQGriRwysGh7hhVYDG99vrx5ZcP6taEOwCaGDauFFaRKqB8yYbWwSnJaedWkSZOGtXuY64KCrh08s74jEgB8q+HbWnq2trRo3swdK57/NzBCObp9SEekX9s1PPNXXnnNZRxWrFhRluj8iSM/+1wswzO8Fpap7Wsf7GFds5D9T5jwlZsvM+RyXiOAAAKBKGBfzPD/NlwgjpExIYAAAggggAACCCCAAAIIIIAAAjcrYMHCG2nX+8VsCzCGbJYFaQFHCx5e1GQcTXh0wUQLFIbVMt6fQcqULuk2sam7bEqw85fKrNpCu79uAUXr2378A51uJ34FlIB/YhfBxYC6NLdvMPYhEF4Q8VaNxiZjtbKqgd4WLvjZDdFKkkZGC8+8fft20qdvX3n99TckrZZ7tQBn7dq1bmgo9kH84ANZbugaV6lSWYoVK+qOmzZt4F+3GwJiJwQQiDIC3sTjUeaEOBEEEEAAAQQQQAABBBBAAAEEEEAgAAUskOgL/oUdU3Sjt+1Tp07lfmyBlVf1DyzaMi/TMrwgpW1Lu/MCdh/OawQXPQkeI02gwZv1I63vW9lxhgwZbmV3191XpkwZZUD/fte9X2g7WOB47tzZoa0Kd5kFVyMrwBruwdkAAQQQuE6BWLFiuUnKbe5eMhivE4/NEUAAAQQQQAABBBBAAAEEEEAAgUgSOHbsmGYjaqaj/i+GJjud0eDi9bb7kkW/3l3YPhIELGPRAot2H85rBBc9CR4RQAABBBBA4K4UsP+wCbqOeWXvypNk0AgggAACCCCAAAIIIIAAAggggMBdLJAgnCnVQju1tQNCW8qyQBAg7BsIV4ExIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIHAXCBBcvAsuEkNEAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAIBAEoh84cCAQxsEYEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAgwAWif/75FwE+RIaHAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAKBIBC9UqWKgTAOxoAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAgEuEPPMmTMBPkSGF2gCe/ftl+UrV8n6jZtlz959cvbs2Ts6xFixYknqVCkle9YHJH/ePJIqZYo7Oh4OjgACCCBwewXs79Cx48fl9OnTt/fAHA0BBBBAAAEEEEAAAQQQQAABBBBAIGAE1u48Lx9/f1YWbzwnFy9eDJhx3c0DiRYtmjz+UGzpUDNICj6Y0HcqMbds3ep7wRMEwhOYMuMH+WPZCsmbO6eUKFZEg3opJHbs2OHtFqnrLUC+Z+9+2bRlqwwcMkIKFsgnVStXiNRj0jkCCCCAQGAIWGBxP/NHB8bFYBQIIIAAAggggAACCCCAAAIIIIDAHRKwwOJz/U/Jk7mTSqOnEorGxGi3QMBitKu2HZfSHXfLnI/S+AKM0Xr27n2x+fvv34JDBE4Xs+bMd4MpV7pk4AzqLh/JsWPH5Ysx4yV58iAp9USxOx5QvBanBRrn/rJIDhw4KK/UflESJbocSb/WPixHAAEEELh7BQ4eOkTG4t17+Rg5AggggAACCCCAAAIIIIAAAgggcEsE6g3/T+LETSh5MxMTuCWgITpZufW4VrE8KdNb3+/WxJz5w48S1YKLIc75rns5ePAQKV26lGTLlk2mTZsuCRIkkFKlngzzPPz3Wb5ihaxatVperlsnzH2uZ6UFFjNnvF+KFn70ena77dtaFmWFMk/K4iX/c8HQd99647aPgQMigAACCNw+AUqh3j5rjoQAAggggAACCCCAAAIIIIAAAoErEC9ePEmcKJFm7AWn7NmjVxr0woUL7nmMGDHco62zZUeOHvV9aTtZsmQSR++v2/Lo0aPLBU1Z27t3r++EEydOLPHixvX1byvOnz8vx0+ccMtt+jLvmPZo1aZsnY3J/7i236n//pMjR47Y01vWrBSqZSzSIkcgT6aE0n/aYV/nMU+dOul7wZPAEPhUg4sZMmQIDi5Ony4pU6YMN7jov8+K5StkzJixtyy4aKVQLWPxWoFF+wCZNm2arFy5UpIkSSKFCxd2P7dS87vvvtOA6Sr56KOPruj2559/lsOHD0vVqlWvWG5jParzb9nYKZF6BQ0vEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBKKYQHwNLlpQ8Lup0+XECS/uc1Fy5cwhefPklnPnzsuPP82Rv//5Rx7MkkVKliguQRpQtOlmLAgZN04cscqATZq1krYftJC0adP4hCzwaOsPHTosCxYt1qDjPnngAe3jicclqcYErG3dtl1++32p/KeBwwL580qe3Lkk6NKUar8v/UPWrFnrAo0ldJ+UKVNITA10Hjh40HeMm31i53AprnqzXbF/KAJm6wWrbXX0OrVrh7IZixAIFti7b7+bY9FKoYbWLGPkmWeekV69eklc/dbCvn37pLa+p/r27Rva5je8rGXLltK/f385qt+k8G+zZs2SiRMn+i/yPbcx2/yQdg40BBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQSiqkDMmDE1+iMyafJ3LpMwQfz47p69VfuzNnTEZ7Lmz7VSIF9eDRD+KmPHB99XT5I4iSQPCnLbTPz6W3c/fY9fxqKtsMDiCc1CbNexi5w5fcYFK+fMnS/jJgT3sWHjJunes4+kSpVSHtSg4yefDpVlmgRlbcbMH+Wbb6dK7lw53XRrH7bv7AKQNq4kmg2ZOlUqSZUylaRInvzS85RuPPbaxmVxh0Sa/ei9Tq7L7bkFU2l3TkDfbbTIEli7dq0s0n+kCRLElxIlS8h96dL5DrV5yxaZN3eenNcU46JFikhujeJHRjt37pxMmvS1lC1bRhYuWiTbt/8tjz5SUIoWLSp//bVe5mvmX2xNV65WrZoEBSW7agjLV66SvLmD/9FftVIXjBw5Unbt2iWWQWjlW609/fTT7qdOnTqSIkUKt8x+HdBvQNgHnGU3es2+xWDp0tZ27twpadKkuWo+R8uItFRsy4icPHmyvPrqq97uYT7ah5ON3c6hfJlSYW7LSgQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEE7lYBy1o8cfKkliCNLs8+8/QVp2H34RdqrOKzYZ9KvHhxXdbhe42bS83qz+r9+OByplu3bpMVei894/0ZrtjXXlj1wpWr/3TBw+rPVXPrLe4xbMTnUuvF52XR4l+lXNlSUqrkE27dv7t3yx8aXCyQP58mJO2X11972QUdbeUvCxfJps1bXEblmj/XSbasD7qg5zk9RpHHCslJrba5fMUqF9B8rNAjkixpUtfn/v0HZIVOB5cwYQK975/LxRkstmClXWm3XyD67T/kvXFEK0tas+aLsmnTJv1Hu1gqlK8oK1asdCc/Y8b3UrnyU7JFA4zbt2+X56rXkOnTZ0QKzGn9FkHLVq2ldJlysmDBQlmvAcUXX6otDRo0lEaNm8iunbtk3PgJGgys6qut7D+Q9Rs3a4p0Zv9FVzy3cqgvvfSSL7BoKx977DEZN26cb7tDhw7J888/7wKauXPnFgs6njp1yq0vVaqUNGrUSPLmzavzTJaWfPnyycaNG3372pOxY8e67EjLiLTn19Ns7HYONAQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEorKA3YuPrxmL02bMlAkTv3ZBPDvfYzqFmCUZWWDRmgXs/tOqhPv273PzJJ4/f0GGDP9M3qj3iiYIxXDb+P+yORSLFi4kbVo19y22ioGpU6dyr199uY48W+1yQNOt02xEa6/UreULLFoZ1MM612IqnQrOWp/+A6VD526yUxOY5mgyVvNWbaTfgEFigcSp07+XwUNHuO127NgpH7Rt55YvXbpMn7d3JVy9xCW3Eb9uq0CUzVzcovV9Z82Zf9OY5UqXvO4+bA7ALl27yYD+/VzGoHXQvn0HGaxzKQ4dOtgFz3r06CZVNcPPmk1oOuGriVKlSmX3OjJ+1axRQ9q0+cB1/V6jxvLdd1Nl7VqN8mu24YEDByV/gUd0zsRVUqjQo1ccfo/WTk6d6nL24RUr9cW2bdskU6ZMIRe7QKG3sHXr1vptgoR6vLVa1/mcVKpUSYYNG+aCimY1fvx42bFjh8TR1GrLoBw+fLj07NnT7W5lVy1b0UqfZs+eXZo3by7r1q2Thx9+2Os+zEcbu50DDQEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQACBqCpg8+HZvIrxtIyozZ2oL13grlXzpvLwQ9kldpzYsmr1GjcXomUOWrOgorWpmvyUNesDkj1bVvdaJNqlx+CH4L7PuTkdLdB4SO/rfzVpsrzf+F23gVVojKGZk9as9Oratevk1brBU/LZvmvX/SXtO3Vz6+u//qrLgLQXlhFZp9YLLsOxnFYfrN+wkTRv2kgyZEgvjxbML1169Hb7/Kn7Z9FEohdqPudeW+Dx6NFjEl+zJ2l3RiBKBhctIHgrAos3eklW6z9Q+0dcuvTlUpzNmjeTI/oPzlrjxo1cxqIFFPdrdP/nn39x/9hv9HgR2a9MmdK+zdKnT++eW2DRWvLkQS6V2MqWhmxnz569qkyp/zaWgWg1j60d0W8cvP76677VlpFYREu+WmBw8ODBLovTVj7xxBOujKqt37NnjytzGu9SfWTLerQgpNdmzJih5VqDpGDBgm6RBSYte7Fz587eJmE+WmlUOwcaAggggAACCCCAAAIIIIAAAggggAACCCCAAAJRUcACftYyZbxf+vbu7jtFy0KcPWee5MzxsDR44zXp/8lgFw+wOQtjxoghiRMnkn//3S3z5v8iPbp28u3nJm/UV2lTp5F/9+zWeRj3ueqFlihlJVZ79OorzzxdxZeRaNOaWXDRMhAHDR4mzTRAGD9+PA1waoRT20PZs8mQQf1k46bNMvKz0ZI7Zw6dIi21W5cmdfCjJShZ86Zas2nY7FjWrFyqVYhs1rKNPFKwgDxZorhul9z1b3EJ2u0XiJLBRWO8kYzDW8Vv2XhW99dqHHvNAnleMG/0l2Okb99+UqN6dUmvEfjESRK7IKO3bWQ8xtK6yV6z9Gebd9G/eR8+/svsuaUV27ccvElfQ67PkCGDfvj86xZbgPCNN95wzxs2bCi7ta6yzcdorX379r4PBXudJUsWe3DLrCSq12xORvsg8tpbb73lsh27dQv+VsPmzZtdJmO7du18czV624b2aGMnNTo0GZYhgAACCCCAAAIIIIAAAggggAACCCCAAAIIRAUBC+LZPf49e/ZqWdOYLoBo55VaA3frNwRPQ1awQH4ZNniAzml4So4eOSrb//7blUcdMHaIHD58RAN3rR2FlTTt0r23vKHzJJYp/aTrL77e+7dgn2UaftxvoGYa5tU5FoMTmux+fiw9ppU87d6zj7xV/3V5wKZa07iijemvvzZI5swZJbkmESUvFCS//LLIzZ1YQYOLNm9jyHYpTnrFYguCdmzXVvZqlcLf/rdUWn/UQbp37iApU1676uIVHfDilgtE2eDiLZe6jg7vv/9+sdrGR3Ui0cSJE7s9/9Z/qNu3/y3Fiz8u/fsNkI8++lCeqVbVrbM5Bnfv3nMdR7h9m6ZOlVLLiu6XDOnThXpQCwzOnj1b6tWr5wKQ5cqVc9tZiVNrqVKl0glkY7jSp/5BRLdSfyXXb0hcK/hnpVKtjOqzzz7rPrRsH8t6XLFihcyaNUvnrQy/jKyN3c6BhgACCCCAAAIIIIAAAggggAACCCCAAAIIIIBAVBXwyo/+vGCRfNi6hUt+WvrHMl92oZ23JUQdPnRYBg0ZJs89ExyfeOet+q6Eqpet2LL1R/LC89WlYP58LhHoouYCWWDR+rc5EFOnTik1qz/rY7Q+T5w86QKLLz5fQ/LkyRW87lJl1bETvpJiRQpLhfJlXSbiP3rfv0L5Mr79I/Jk3IRJGhQ9Ka+/+rJUqVRB5v+8QHZpxqVlYNLujADBxUhwz5Mnt2TLlk169Owl7TSIaPMGNmveQnLmzOmCi4k0yr506VKpWKG8WCbeqFFfSqJEwSm/kTCcm+oyu9ZZ3rRl6zWDizYHopU+HThwoNSvX98FGFevXi3Hjh1z5VItyGhBQCtjOmrUKJ0wNp4rk2rfWKhZs2aYY5swYYJYmVSbg9G/WTailUaNSHDRxm7nQEMAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAICoLFH+8qKzR+Q4bvtdUswljSdq0qeWpypVc3PCCRgnfbvS+JNGEqCqVKsrjxYo4Cv8KjLbASqlayVS7h79i5SoZ9eU46d+np86luE4scJksaVL539JlwYyanfhOw/rypx7znx07ZdSYce7HVlps0fZ7/bVXZMDAwfLDrNkuCFnyicfdvI/BHdiGl6KQvgWXntji4KqqOt7y0qf/IGnSrJX8p/EWK7OaK+fDIaeGDNkDryNRgOBiJODaP7phQ4dIo8ZNJGeuvJq5F10DiRWkZYvm7mhdu3aWRu81ljFjxkqqlCmlRo3nZOrU6WGWH42EYUaoy/x588jAISOkWOFHQy2NmjFjRvn++++lSZMmYqVLLQvRAor22gv+9e7dW+eZbCzZs2d36zNlyiRDhw4N8/j2LYjx48fLu+8GTwjrv/Hzzz8vZcqUcfM1+i8P+dyCkCtX/ynvNHg95CpeI4AAAggggAACCCCAAAIIIIAAAggggAACCCAQZQTsnrpVEbRMxHNnz4kFE70Kg7bO2id9e7ltLIZhzUqa7j9wwGUlJtL5FK38aZeO7Vxg0dZny5ZV2moWpLXcuXLIpPGj3XPv1/nzOteixj9y58opL9Ss7i12/dkxLXCZ8f4M0rtHFzmp2Y1x48Z1x/c2/Hz4YN+xLLbg37/FTr78IjjxyCpEtmvbysVQLKIYJ05s18V5vynWvD55vD0C0QYNHnyxYYMGt+do9+BRjh8/7oJyoc1ZeODAQQkKSub7xxMWz7Lly6VatedC3eSvdWt0ctSraxOHuvENLJwy4wc5ceKEVCjzZJh724eDlTH1SsGG3NgyOG0C1iRJkoRcFSmvf5g9z6VrV61cIVL6p1MEEEAAgTsv8K/O70tDAAEEEEAAAQQQQAABBBBAAAEE7nWBOLFjS7JkyRyDFzy0Fxbks2y/uJoU5L/8zNmzbno3CzBas0CkZSXavIq2nQUGbV+LS8bQ5y4V0W15c7+8QKcdw3tuY7DAqNcszmBzR4bVbN+DOj2dJRlZe7jFcWn0VHode1h7se5GBex90H/aDjk8+kHXRdhX50aPwn4+gYQJr13uNHnyIN924T3Jp3Mbbli/NtTNLNofmc2Cc58MHi6Ll/xPimoG47VaeAFO+3DyvilxrT5u1XIbqwVvX6oZekD2Vh2HfhBAAAEE7qyA/V2xL6/QEEAAAQQQQAABBBBAAAEEEEAAgXtZ4LQG2Xbv2eMCgRc0EmTBO4uzRTS7z+6v2P63uvkHKq1ve20BzOttFuC8VCU11PMqmjWmrNp2XPJmvnZM5nqPyfaXBcz28YeCM0ZtqYabaXeDgH1LwIKIof3cjvG/UvtF2br9b7FsQO+bALfjuNd7DBubjdHGamOmIYAAAghEbYFEYXyJJ2qfOWeHAAIIIIAAAggggAACCCCAAAIIXC1gwUTL6rNswIgGFq/u5dYtsbFYMNHG48Z0A4FFG42di68PfR6yvV8plsxbfVhWbj3usi1Druf1jQlYxqKZmm2HmpcT5iiLemOe9+xeViL1j2UrJG/unPJglsySOlWKUOdivJ1AFlDcs3e/bNqy1c2xWLBAPqEU6u28AhwLAQQQuLMCZ7WMxzEtQ04G4529DhwdAQQQQAABBBBAAAEEEEAAAQQQuJMCa3eel4+/PyuLN57zlVy9k+OJCse2TFPLWLTAYsEHL2eFElyMClf3Np/D3n37ZfnKVbJ+42YN6u0Tu6l7J5tN9Jo6VUrJnvUByZ83j6RKmeJODodjI4AAAggggAACCCCAAAIIIIAAAggggAACCCCAAAJRVoA5F6PspY28E7PgXfkypdxP5B2FnhFAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBAJNgDkXA+2KMB4EEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEAlSA4GKAXhiGhQACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggECgCRBcDLQrwngQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQCFABgosBemEYFgIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAKBJkBwMdCuCONBAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAIEAFCC4G6IVhWAgggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggEmgDBxUC7IowHAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAgQAVILgYoBeGYSGAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCAQaAIEFwPtijAeBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBAJUgOBigF4YhoUAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIBAoAkQXAy0K8J4EEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEAhQAYKLAXphGBYCCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACgSZAcDHQrgjjQQABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQCBABQguBuiFYVgIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIBJoAwcVAuyKMBwEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAIEAFSC4GKAXhmEhgAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAgggEGgCBBcD7YowHgQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQCVIDgYoBeGIaFAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAQKAJEFwMtCvCeBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBAIUAGCiwF6YRgWAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAoEmQHAx0K4I40EAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAgQAUILgbohWFYCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCASaAMHFQLsijAcBBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQACBABUguBigF4ZhIYAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIBBoAgQXA+2KMB4EEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEAlQgpo1r7V+bA3R4DAsBBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBAJFwAUXczz0QKCMh3EggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggECAClAWNUAvDMNCAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAINAECC4G2hVhPAgggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggEqIArixqgY2NYCCCAAAIIIIAAAggggAACt1Fg4jdTZOmyFbf0iI8UyCc1n616S/ukMwQQQAABBBBAAAEEEEAAgTsnQObinbPnyAgggAACCCCAAAIIIIBAQAnc6sCinVxk9BlQaAwGAQQQQAABBBBAAAEEELjHBAgu3mMXnNNFAAEEEEAAAQQQQAABBK4lYFmGt7pFRp+3eoz0hwACCCCAAAIIIIAAAgggEHEByqJG3IotEUAAAQQQQAABBBBAAIEoLWDlSylhGqUvMSeHAAIIIIAAAggggAACCNy0AJmLN01IBwgggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAgjcGwIEF++N68xZIoAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIHDTAgQXb5qQDhBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBC4NwQILt4b15mzRAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQOCmBQgu3jQhHSCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCBwbwgQXLw3rjNniQACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggMBNC0Q4uDhq9JcycOCnN33AQOlg4cJF0qFDRzl69GigDIlxIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIBDQAhEOLvbr11/69x8g//33X0CfUGiDs8Boj5695OTJk77V48aNl5GffSFr1qzxLeMJAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAghcWyDaoMGDLzZs0ODaW1xas3PnTjl9+oxkyZI53G0DbYMqT1WVVatWy/JlSyV58iA3vGPHjsmWLVslT57cEi1atEAbMuNBAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAIOAEYlSuUqX9o488Eu7Apk6dJhs2bJSCBQvIuXPnZPiIkbJn9x5JmjSJjBs3QabPmCGnTp6SrFkfdH1Nnz5D5s3/WTJlzCjx48d3y3bs2CHjJ3wlRw4f8QUp16/fIJO/+UZmzvxB9u/bL5kzZ5JYsWK57b1fFtj85tvvZOq0abJ//35JmzaNxIsXz1st+/btk/HjJ4iNcfPmLRIUFKTjSiqHDh2S0V+Oka+/nuy2ja4xRAsk3n///bJo0WJZtXq1pE9/nyRMmNCtP3v2rEybPl0mT/7WZTTGjRtPUqdO5TvOtzoGK6dqAclJ2ufkyZNl2/bt8vDDD0nMmDF92/EEAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAgagoEOGImJVFPXDwoLzxRj2XwdilSzdJkjiRxIkTR44dPy6nTv0nI0d+Lu0+aiv16r2mQb7N8nGffhI3blx5uW4dZzd58jduWZfOHd3rsWPHyUft2svZs+ckQYL4cuLESfn008EyevQXct9997ltLJjXoEFDOaqZht42mTJllPHjxrhtfv/9f1L35Ve0j7MaKEwv//zzj/Tp208+GzlCUqRMIYMHD/Fdt8FDhsn5Cxfk8ceLabDyWw1GTpcC+fNLmjRp5MCBg/Ky9rNq9RoNhsbT8zklPXv1lhbNm0nDhm+5PkaO/Mytt2CoBTHPnj0jFy5c1IDjQhkxfJjvODxBAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAICoKRHjOxdBO/sjRY9KjR3dZ/9daF+yzbb6aOMlt+vTTT7vH2bNnu0f7NXvOXM3wiyGVK1d2QcD27TtKmtSpZdHCn+XPNaukQ/uPZOOmzdKhYye3j2VItmr1gQYEz8uM6VNl3do18nHvnrJt23b59FLQ0AKAp0+flm+/mSw/z58ro0d9IceOHZdRo0bLA1myyIrlf7hMQ+vQyqK2bdPa9R3yVy/txwKL7zdt7Mby+2+/uv2t/7Vr116xeYH8+dxYbJsUKZLLrFmz5fDhw1dswwsEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEoprATQUXDaN06VLOpFixolqqNK7s2rnLvc6cOZPkyZ1Lfl28RI6fOOFKl65atcplDQYFJZMZ38+U02fOyOuv15MMGTJI9OjR5dVXXxHLSvzxx1kuc9C2//ufHVKubFnJrX1Zq179OZc9mUGzFK19Pekr2bplk+TMmUN2796tWYip3fLVa9a4x4j8unjxonw3ZYoLFL799tsSI0YMLYeaWt55522XmThlytQrumncuJELkqZKlUrPMbdbt2tX8HlfsSEvEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEIhCAhEuixqRc46tcyWe1WxDr1WtWlWzAbvIgl8WyJGjR0VjeFL1Ukbjtq3b3GZZs2b1Nve9tsxE+9n+9z9umc2R6DWbM/HDtm28ly6gaBmQs376SeeC/D97dwFmVbWGcfxj6O4GKZHu7lLpUEExSFEBFRCFSwoiKSICUgoiJS3SJYJ0SZd0dw09wMBd3xr28cwwcWYYYpz/us/MOWfvteu398x9nJdvLV/X8vu+/753LQzizblz5+SmmS8yV86cNjR0ur30kt+5HT5yxFlkX6NG/Zctegy/+SHdj+2vMx8QQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQ+I8IPFbl4ktZXwyWoWbN6qYiMYosMUOjLl261M7PWLlKZbtN/Pjx7evNmzf97ePWw88JzHyO8ePFs+tu377tr4/7h5YtP5FFixdLv759ZOeOrbJ/3x731R69jxcv8HPRwFGbc64e7YxOCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCPxHBR4rXAzJJFWqVFK0aBFZuHCRLF++QipVrCDx4sa1m+XOk8u+Ll++3LUbb29v2bxli+iwqWnSpJHs2bOJKVSUDRs3uvpo0Fi7zutmLsZOcvfuXds/relbr15dSZAggVy/fsPV13mj1Y7a7pu5GwNrcePGkcyZM8k/+/bJqVOnXV3+XLbMvs+T229IVtcK3iCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCAQCQW8tmzZ+kQvu44ZGlUDPx8fH6ldu5brWDWqVxcddnTir5Pkyy+7y/jxE+Sdd96zw5PqnIYaCKYz8yrWqlVT9Bw/bdVafjV9mzR5337OniO7RDfDsL744oty/MQJ6dOnr4wdN17qv/2OPcbt2z6uY+k8jtoGDR4iK8wQrYG1tp+1scOqNmjQ0O6nd+++MmLESBNyppb69d8KbBOWIYAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAKRSuDOvQfiez/wS77r+0DumS9tJy7es9PlBd4z/JbeN4fTc4qs7e/NW6TFx596fPnaV7d5nOY1b978x9k+xG2rVq3i6lPRVC46LWrUqDJ50kR5uVIlEzD+Kp27fCknT52SHl91k8aNGjrd5Jt+faX+W2/K4sVLpEPHzra6sFOnDq4+Q38YIi9mySLDR/wo3bt/JcWKFZX06dLKhYsX5OLFi3Y/TZo0loQJE8i4cRNk8pSprn27v9EQc+B338pVMzdk167dZNTo0VK8WDGZPm2KxIoVy70r7xFAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQACBSClQvOMxKdPl2CPXfurSPUnR5IB0nHhBVu29JbnaHJa+My890i+8F/y68qokN8fdf/pOeO86Quxv1OifbVjoScDoBIu6zeO0KKnTpn9w6sSjD8Hj7DS02+rwppcvX5YUKVIEuamvr68NC4Pqc/HiJdHhTYMKAnX706dPS7JkyYLs4xz8woULdp7FmDFjOot4RQABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQirECf3y76C/u8x2e115KwwX7XNXV4LYl0fD2p63Ngb/r8dsns56Ks7ZNBcqaL4eoycM5l6T71gvzZPb1kTBFdRiy+IjUKxZN8GZ9s1jJhxVX5+KezsumbDJI19b/n4zqxSPDGCQ0LFSwgw4cOCfSKPekT6IaBLHyicy4GcrxAF+nwpkGFhs4GWukYXJ+kSZMEGxrq9jrMalDho3McfdUAkmDRXYT3CCCAAAIIIIAAAggggAACCCCAAAIIIIAAAgggEJEFtIpQA0Xny7kW/ayhojZPKg3fLBnf9p225pp9db5NWnXVhnuFssSSe2bY1DWmevGc9z1ntfx98LY0HHJaMrU4JLX7npQ5m67bdev335bqvU7Imn9uufp2nHheapk+znCnt+48kBq9T8jopd6uPp6+uXzdV74Ye17yfX5ECrU7Kp1MZeX12/7HdR228IqU7HRMsrc6LN0mX5Afl1yx5+ocY/7mG1LKrE/T7KBU6HZcVu3591ydPs/yVQNFDRaDGiI1PINFvc7nIlx8luAcGwEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBCIrAJORaMTMIbkkCVVdCmYOZZMW3vNNafilsO35Z9Td6R+Kb/g8c7dB3Zo1DNXfO3ujp6/K3W/PSWHztyVFpUTSfRoUaTJD2dsn+xpY8i6fbdl/t83bN/bZttf/rwqf+26KWsfBo76utIEeumTRQvp9Pyt9zFzMb498LSMW+4t+TLEFD2WBoeNhpxxzQ2pnzXMjB0jilQtEFemm+vqPvWibDShpzYd7vWd709Jwrhe0q52EvEx59f4h9Ny08d/QOnvwM/gQ1ABY3gHi3ppobsLzwCDQyKAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCIS/gHuwqMOhelK5qGdRr0R8G8it339Lir8UWyatuiZRooi8VSpBoCfZe8YliRfLSxZ0SSfxY/vVvdXsfVK+m31Jfmuf1uwjlvy1+6bdViseNRRMmySaLNl2U8rliiMrzLo4MbykbM44ge4/qIVaXbl23y2Z2Ca1HaJV+2mFZfORZ0WrEWsViWevuVKeODK9XVrxMtegYWKpzv9OJ7j7xB0boraqlliqmPCxQfkE4mtyxTgxn7/6PQ0YnTBRX7VpNWNww6XaTqH89vxdeSgvgO4IIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAKhEwgYLOrWzjyMIe3pDRMuRjVJ3FQT3t31fWCr/Uplix1kZeGsjdfk2IW70mzYGXlrwCn7deLSXdl6xMce6tX8cWXnMR/RIUyXbL8hRV+MJXWKxrfvtcNfu2+ZkDG2xIpu0r9QtA2m+jBN4miuYFE31QBUA04djlUrKi9e85U3zTINFrWlMaFm6eyx/T6Y78WyxjLXFV0aDj4tTYaekY0HbkvKhFFd65+3N+4VjE8iWNTrJVx83u4654MAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIPEGBwILF0BxOw7WyOWPLrA3XbXWhBnT1Swdetaj71TkTtWVJFcP1VbVAPDOMagJbFVjZhIv3TZdVpmpRqxW1QrBKgTiy9+Qd2XXcR7aZEFKXhbZdMmFl0vj+g0ANERPFjWpDxRs+fucVN6b/0NK9KlGDyD+6pZPmZjjX7eY86n93ys4H+cBv09Ce0n+if7QuXTr9Jy6Ei0AAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEAheYJWZu1CHP9U5FnUoVPeWsMF+j6sX65WML8t23pTev1208xXWKRrPfVf+3lfIHccGjL3fTeZavs/M0ZghRXQ7nKrOhZgheXQZu/yq7D99R7SSMWvq6LbC8MvJF80wpA/sMtfGHr4pZOaGXGCGPz1/1VeSJ/ALGY+cuyvHTRVl4SyJJVuaGBLXDG/6hwk0axb2O38NQlc/nOvROYwOldqjfjL7NXHlVWn541lZbYLQ0jn+rXB0+j7rV2dYVB0KVZtWL+oyrWgMr8aci+ElyX4QQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAgedcQAMxT4c/De5Sapkwru2Yc7LjqI+8UTy+ay7FwLbRqsaPRpyRduPOy3tlE8juEz7S3rx/u0wC+aZBcruJVi/+uOSKHYI0Z7oYdpnOhfi7qY7MkyGmHd40sH07y8b8eVWSPQwQdVkZc506p+LAuZfljf4npfMbSe1ciV9NvSDJTDVjVVMJGdWM79mgXAIZsfiKRI8WRQpkjimTTHio4aMOp6rt56Xe8tkv56STCWJrmwD18Nm7dnmsGP6rHe3CZ/zNPVh0wkRnWXgGjISLz/hGc3gEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAIKIJ6HChlU1Ap0Oj1i8dP9jTr18qvh2GdMDsSzZA1LkTdVutBnSaEy5Wzh/HWWSHQtVwsYoJHkNqQxde9tflq7eSSZsaiWV6uzTysak0fNPM9agtZ7qY8vv/0krapH4RWZ/3kouXCRlnrL0uE1dclWqF4toAdIWZ51HbOyYM3WDmWdQKTf3SYVVbV08sWhX5PDUnRNSKRSdY1PPT98668AoYowwdPvxBy+bNn6fr51wQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQTCUcCZZzG4XQY2VGpw/UO7TucpPHDmjq1O1IDxabbTl++ZSsUoksLMF+nebvrclyPn70kOMzSrDr8aLWoUKdL+qBmWNYb8+llqV9drt+7LsQv3JHPK6HYYWNeK5+CNEx4GDBbdT82TPu79g3tPuBicDusQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQT+swK1+56UTQdvy3eNU9g5GKesuSrDFl6RQU1TSOMKCZ/76w5NaBiavsFdOMOiBqfDOgQQQAABBBBAAAEEEEAAAQQQQAABBBBcSYzNAABAAElEQVRAAAEEEEAAgf+swJBmKaTZsLPyoZkTUltsM5di+zpJIkSwqOfb7P2m5vvP/oZC1eWBNWeIVL9tAuvh2TIqFz1zohcCCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggMB/VOCct6+cvXJPsqeLIdHN0Ki0oAWoXAzahjUIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAKRQEDnYgw4H2MkuOwwXaJXmLaK4BvdunUrgl8Bp68Ct2/fBgIBBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQOApCoQ6XLx79674+vp6fIqPEwA9ePBAfHx8PD6WJx07duwss2bN9qSrq8/1Gzdc7wN7E9T6e/d8Rb/Cu+3d+4/07dtP+n3TP7x37dH+hg8fIfv27fOor3a6d++ex3097XjmzBmp92Z9uXjxoqeb0A8BBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQOAxBTwOFzXEea9BI8mVO6/9+uTTVnLz5s0gDz9gwEApXqKU5MiZW8qULS8LFi58pO/mzZvl8y/aySuvVpEVK1a61msg1/XLbpInT37JlSuv1HntdTl69KhrfVjfjBnzi5w+fVrefLOeaxcbNmyUdu3/Jx9+1Ny1zHkzceKvkjdfAcmbN78UKVLskWsIbv3UqdOkStVq9mvSpMnOLh/79ezZs1K37pty7PgJyZQx4yP7u3r1quzZs/eR5cEtOHDggAnpLgXXxd+6YTZc3O9vmfMh4L7Gj58gL79S2Vkdbq+pUqWSN15/TT5r+7loCE1DAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBB48gIeh4vt23ewZ7Np43pZ9udS+eefffJNEJVzP/88RqZMnSo/j/5J9v2zR5o1e19atPhYTp067bqiJUv+kHffayhp06SR3r16SrFiRV3rNATU9bNn/y7btm2RjBkymvCvhWt9WN5cuXJFvh3wnfTo0V28vLxEqw3Llqtgz2vz35vlxnX/1Yl6fRpwDv1hiBw8sE/at28nrVp9JufPn7eHD2l9//7fyozpU+X3mTNkgDmuBmBawdekyfty586dsFyC3UbD0OgxosuwoUP8haTODleuXCVv1X/b+ejRa/PmLWXOnDke9Q2pU8B91ahRXQZ9PzCkzcK0vkGD9+wztWDBo8F1mHbIRggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAsEKeBwubti4Ud6u/5YkSJBAUqc2VWNvvC6bNv0d6M6XLV8u777ztuTMmVOiR48ujRo2kPjx4smWLVtsf61M7NSps/Tq+bW0bfuZqQosLDFjxnTta+OmjfLyy5Ukc+ZMEjduHBPINbbVeO7Dj27btl2GDRsuEyZMlGPHjrm2DerN9OkzJH/+/PLCCy/YLlFNwPjVV91kw4a18uqrrzyy2azZs6VY0aJSpkxpiRIlitSrV1dSpkwhS5f+afsGt16v744ZPjZhwoQSz1y37/37osPJjhz5o+TMlVNixIjxyPHcF2j1nwa0v/wyVg4eOuRapd56zVplqNWQ+/f7rx7UYPHXXyfJlSvedv3Jkydd265dt85uO23adLl06bJdfvnyZdtv3/4DMtFs515dqscdNWq0jPzxJ9mxY6drP0G9CWpf586dlxMnTtjNTp46JTNn/m4DWr1vei3O9Wnl6uAhP8iMGb89MozqwYMH5cefRtl7ffr0GdcpRI0a1VRxvmGXuxbyBgEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBA4IkJeBwu5siR3VQRbnedyHbzPkfOHK7P7m/GjxsrrVu3ci3S4OmGGUI1Y8YMdtlff/0lUUy4lzVrVhswTTFDiHp7e7v658ieXXbt3OWa23Hrtm0mFEwv8eLGtX00WHv7nXfllBnidM2atWbo0eqiQ6wG1+bPXyAVK5R3dYkdO7ZUKF9eNKAKrJ02VZYZMvidr7M+gwkmT5zwC+yCWx8tWlQpVLCgfPvtd/L994Mkd+5conMEzpo9R1p9+omzu0Bff581S2rWqiO7du+W7dt3SI0atUwFp19VoQ6Juveff+x2Ou+iExI6O9IQT5dr09dr167b91qB+eknreS0OYffzXyTlatUtXMm+vj4uPprJeaRw0ds/3nz5kv16jXlkAkYdTjaN+rWk7lz59l1QX0Lal9r1qyR777zq1zca4Zrbd2mrbzf7EPZvWePDWqrmnvX7IMPZcSIkaLX93XPXtLms7auw+hx69R53ZzbYVm2bLkdZtY9VH25UkVZbY7h/vy4NuYNAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIBAuApE83Rv/b/pJ1Wr1ZARpvpOm4aNk36d6NHmX3/dS0qVKmnmT8xl++/bt8+EbWfNkKQtpVz5cqLB39Chw2SOGQZVq/2aN//IVBRulEyZs9r+0aNHk1m//+Y61qTJU+SjDz9wBZh9+/az/QuaQC+optWN6dOnD2r1I8svXb4kWbJk8bc8VqxYJrC7ZpeFtH7IkMEyywSF903V4rChP0jLjz+Rrl06+6vQ9Ldz8+H69evStWs36fl1D1sZquuLFi0iXbp+KRVNiFa9ejUTKF6S0aaqUasuA7a3365vK0s7mqpQZ706ajXj0j+WuMLd1q0/M+u/lokTx9t+q1evlvfee1caN25kd6nhXb9+faR2rVr2c4L48WXylKkm6Kwe8JCuzzoHoh4z4L5cHdze/PTjCFMFmtKGx3qPFy/+Q44d9avQLFWypHzaqrVdd/u2j3Tu0lV69+4lNWvWsHvQuT6HDh1uQtvv7GetRNUpF48fP26fHbfD8BYBBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQCCcBTwOF5f/tcIGV3Xq1BZfM+znn3/+KTt37pSyZcsEe0o//TRaVq5aZYNDp+O5c+fs27lzZ0uiRInskKEVK71iwq5fpWXLFmYI1D12yM8336wrMcywqitXrZa/zPFz585ttytRvJhMMH3jmkrGl19+WTp0+J+z60Bftaru/IULkix5skDXB7YwikQJbLFrWUjrdTjXd8zQsNp0CNUkiZPYCkatutRr1qFYA1ZNbt+xQ27dvGUq9Wq7jqPDfmpYuNMMTVrcXHdo2ypjX7RIEVewqNu/9VY9adCwsRl+1Fe0yjJga9Omta1Y1EDxwvkL1v5x5okMuH8NFrXp9adMkVxeypbN1UUrVO/evSdXr16VAwcOyuXLV+SGmR9Th3PVpkPKbtu6zdVfPydMmEBOmCFgnefDtZI3CCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAAC4SrgUbioQc9XX/Uwc9uNk5IlStgTmGyqBzt17iKrVv4V5AnpcJ6DhwyRKZN/Fa1sc9r9+w9MwJbQhmy6TOdlLFO6lB0qUz9/2e0rG8zpfIzajhw5KmXLVZA6r9WRtGnS2DAxb968MmfOXBlohh3NmvVFGTzoe9d8inYjt286Z2JoW/wE8eX2rVv+NrtphnZNkiSxXRbSemdDtRs8yBhMmWSu6T2pVaumrF+/wcw/udXMO9nB6WZfL5m5FOPFj+cvdIwWLZodDlYrFsPSLprtNMx0b4kSJ3YFeM71uK8fN36CDBz4vdSrW1fSpU8nCUx4pyFjeDStQnVvmbNkNnNh5nMtcr9XOpSsNg2n3VtRMxdmwBZS2BuwP58RQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQRCL+A/6Qlie6360yq3zJkyuXpkzpxZTppqsdu3b4sOFxqwrV69xoRnXeTnn0dJzpw5/a1+6aWsokNtPjDjWTphkrcJ4ZwQ7IyZSzGT2b/T0qVLZysYDx08ZCrdUsqq1aukTJnSdpjQu3fv2jn7+n3TX4b+MMTZxN+rVrelSJ5czp877295cB/SpU3rb45J7atDq2rAqS2k9baT+danTz/54INmdv7DqCYobNGiuZ0rsc5rrz8SLmbKlNGuu6BVlsn8qiw1YPO+ek0ymnVhaZkyZpT169b721TnV9Rqv8CCRe046PvB8uWXXeW1hxWUOkyqDmMbHs0risfTfLrC4h5fdZekSZMGeni9/97eVyVdurSBrmchAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIBA+Al4lPRkMUFfqlQpZdTon+0QprdMRd8vY8dJwQIFbLB47949W0Wow49q2717t5k3sYX07dtb8uTJLdpfv5z1Wr135Yq3/PjTKBswbjXDXC5Z8odUq1rVbl+qVCk7T6C3t7ede2/s2LESxctLdE5FHcZT5yUcMGCgXacbaGDlZdYH13RuPp2Xz9NW1ZzL+g0bbJWhbvPbbzPlwsWL8ooZhlVbSOu1z549e+XAwYNmGNI3zZCy8U0I5m3nYLx85bINV7WPe9M5KQsWzC/dun9lvbRSsnv3HlK4UEHJmSOHe9cg3+uclTdv3HTNDfmaCUNPm4DyZzNPo6+vr5w8dUoGDx4iDRq859qHbuNUCepCrcrctGmTDY537dolY8eOFw08PWkB9+XJNkH1yZ07l+TOlVM6d+5qhka9aZ89fe5GjBjp2uT48RMmoJZQzafp2pg3CCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACoRIIPpFz29WE8WNltZn7MFfuvJI3X0ETNp2XoUP9KgWPmoq+tm2/MHPkHbBb/DB0uK22a/nxp5Itey7XV+Uq1ez6+KZqcdz4X8wwqxPNupzy9jvvyueft5VSpUra9T179rCVdYWLFJccOXPL2HHjZaIZklXnMdQ2etSPssEEf3ouOXLkNkHlFenYwf8Qo7aj27fq1avJsmXL3ZYE/zZv3jzSqWMHadiosT2H7mZY2O8Hfueq9gtpve49R47s8vPon2x1plbe6TyLteu8Ls2afSitW7cK9ASGDxsmly9dlrx5C0g+43zt2jUZNuyHQPsGtjB/gfySIcMLki9/QZk7d56t+PtlzM/Ge4K9jgoVKlnnNm7H13kdh5vA7tXKfuFu7949ZfGixfbeNG7cVOrVe0Pu3Llrvu4Edkh/ywLuy9/KUH7QORlHjhwuGsbmy1dAcubKK9Onz5BKlSq69rRs2TIpXbq0nQ/UtZA3CCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACT0QgytDhwx+0bN7c451fv3FDopnQJ+BQqDpsqlYVhrZdvnzZBIkJA6081CEvtXpP1wfW9Fwe3L8vGlaG1LRqsFSpsjJv3mwTvmUIqbtrvVZl6jnqMKXOEK6uleZNSOvd++r769evWzudSzG4ptetLU4cv0A1uL4B1+lws2fPnpXkZihYDeicpiFsvHjxA71PGmLeN5bu1hfNHJA6dGpg1+3sM7DXwPYVWL/QLFMPrbx0v9d6ndWq1ZBWrT+VqlWqhGZ39EUAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEAiDQKjDxTAc47nZRKv3lpvqxdEPqwmfmxPjRMIkMHHir/LHH0vtvJ6hDUDDdEA2QgABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQiuYDHw6L+F5wamnkG06RNI9OmTf8vXE6kvgatzJw2fbqZe/PbUFdWRmo4Lh4BBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQeAyBSFW56Djdvn37kWFdnXW8RhwBHx8fiRkzZsQ5Yc4UAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEIjgApGqctG5VwHni3SW8xqxBAgWI9b94mwRQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAg4gtEynAx4t82rgABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQACBpy9AuPj0zTkiAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAhFSgHAxQt42ThoBBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQACBpy/gderUqad/VI6IAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIRTsBr7969Ee6kOWEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEHj6AgyL+vTNOSICCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACEVKAcDFC3jZOGgEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAIGnL0C4+PTNOSICCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACEVIgmp71gwcR8twj7UlHiRJpL50LRwABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQeIYCVC4+Q3wOjQACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggEBEEiBcjEh3i3NFAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBA4BkKeFWsWPEZHp5DI4AAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIBARBGgcjGi3CnOEwEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAIFnLEC4+IxvAIdHAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAIKIIEC5GlDvFeSKAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCDwjAUIF5/xDeDwCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCEQUAcLFiHKnOE8EEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEnrEA4eIzvgEcHgEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAIGIIkC4GFHuFOeJAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAwDMWIFx8xjeAwyOAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCAQUQSeSrh49959ued7/4mY3Pa5Jxeu3HLtW491/8ED12feeCZw7949efCcud26dVtOnjzl0QX4+vqKXkPAr+ftmjy6mOeo05Ur3nL8+Am5f//J/Py6X6reK71//8Wm1zZsxE+ybfsOf5enz+3Ro8fEx8fH33L9/MOwkXLw0GF/y5/3D8491OsKrIW0PrBtwmuZc2x9fdrt8uXLcvHiJY8OG9zv4jt37oj31ase7Uc76X14Gj+7Hp8QHRFAAAEEEEAAAQQQQAABBBBAAAEEEEDgPyHwxMPFrf+cl/TVxki++pNEg7+A7cipq3Lu0r/hoK6/euOOHDju7VEg+W6XxVK00VS5efuenDh73R5ryqL9AQ8Tqs96nnr8azfvhmq7iNr53Lnz8naDpvLHn8ufm0v4c/kKafJBC2nzRQf5tM0XcuHCxWDP7RPTR68h4NcRE9w8jXb5yhU5e+7c0zjUUznG1avXpGv3r+WDFp9K2/adpEmzFrJm7XrXsTW0OHXqtNy65f9n19UhDG/+Wrna3r/gHM+ePScaeEa0tmjJUln210p/Af7CRUuk0fsfyRcdupjX5jLq57GuICh69Ohy5sxZGfzDcNFAKaK05StW2nv4cevP/V2rc/6/jJto1/frP9BZFOLrzZs37bMWVGAZ4g4edtBgV38/HDh4yKNNwuO4GmSO+HG0fNiytTT/pI307vetDfyCOoGQfhf37jdAPmn1+SNhdFD7a/VZOxtqB7We5QgggAACCCCAAAIIIIAAAggggAACCCCAQFgEnni4OGXJfkmXIp54X/eRJesfDXpebzdfeo/Z5O/c5648LKXfny5nLtz0tzywD01r55S27xaQOLGiBbY6TMtOnrtujz9/9ZEwbc9Gjyegf9T/afQvUrF8WenXu4cUyJ9PTp85E+JOs76YRT5u8aG/r+TJkoW4XXh0mDBxsvTo2Tc8dvVc7GPg4B/k2rXr8sVnraTHl52lUsVystgEZBo6atOwt/Xn/5P1G/9+qufb7eve8uvkqU/1mI97sGvXr8ukydPs85w/X167Ow0Wx4ybILVrVre+TRs3kJWr18rwkaPsei8vL/mk5UeiYdNvv8953FN46ttfunRZtm7zX6V59+49WbFqtUSJEiVU57Nu/Ub7rF0y1X9Ps4XHcTXQXLrsL/nog6bSrUsHSZokSYj/UCK4a6xa+RWp+0YdiRkzZnDdWIcAAggggAACCCCAAAIIIIAAAggggAACCDxRgfBL5AI5zTt3fWXmskPS5f3CsmrraZmyeL9UK5XR9jx14YZ80u8vOXX+hulzUI6duSZt3s4nm/ack7Fz99o+9TstlLxZk8mwDuXlz40n5Iep26V9w4LSZfg6OXr6qmye+LYcOuEtOw76r2q7fuuOvN9jqazYclLyvphMmtXJJVVLZbD71ODy59l7ZOxXr0j8ONHtsrFz98iCNUdlcu8qsnjdMek5aqNdrq96zqO6VJQkCWPJFlOFOWzaDlm99ZTkNvttXDO763p0pL3vf90ik0z/K1d9pETeVNKzZXFJnzK+3VdE/7Zuw0ZZaiobteonW9as8uqrlaSgCf2cptVV403AtmHT3xI3Thx5rXZN2b1nryRPnkxer1PLVjH99vts0YrE69dvSM4c2aRpowZ2vbMP5zV27NiSJHEiOXX6jGTKmEEyZ8rorAr2NUWK5FK+bGl/fW7fvm1Dv3z58tggx1n5x9JlssqEOZ9/9qnEjx/fXtfsOfNk5+499piVX3lZihYpZLtv3bZdfp81Vxo1eEfGjv9VDh85KlmzmiCz+QeSOHFi+XbgYNmydbutMOveo7cULJBfatWsJidNZd+on3+xQ1smTJBAalSvKpVfqeScwnP9evnyFXvP8hs3DTJymPvltE2bt8jEX6fYj/q63NxTx1EDyHkLFsnFS5ckdapUUq/ua1K0sJ+jbqDh5JRpM6xXrFgxjVU+ebPu6xIjRgxn965X9RttqvnSp09nPYf8MMIOLblqzVobur3xem1Jny6dfD94qLGtYitvd+3eawPRfHlz2zA0uHPRIUdnmXu+a9ceSZs2jZQqWdzf/dFnPahnQivSPH2ely1bYaq278pb9d6w16bh+dgJk6Rq5Vel3huv2WXqGydObBk0ZLhUrFBOcmTPJqlSpTShbnmZPXee6VdHokaN6rJ53t/kzpnDhGrLzT8M8AtT9Xw3bNwk90zA+GKWTP5OX4ftnDFzlmiYpz+v+o8J3qlfz3jEkem/zZLFfyy1/Xv16W9/F7T6pIX9HNKzpvdV7+8/+w9I1hcz2/vrfmAfnzsyaco0WbNuvakEvGPPq3GDd+3z9jjHdT9GypQp7Eettn25YnnJnSun++pQv9d/ZHHo8BG7ne5Tn/3XX6sly01VrP4OSpo0iXzQtLFky5Y10H1PmjJd/tm3Xxq+97a13PT3Zpk8dYZoRXC6dGnlvXfeklzm3tEQQAABBBBAAAEEEEAAAQQQQAABBBBAAIHgBLyCW/m46xaawO76zTtSvXQmqVUukyzdcFzOX/YbRtHLVK/Ei+0X7kWP5mXfR4vqJbFiRLVfeuy4Zn3cWH59zpt5FddsOy0f9PxTYkb3kkI5Utjw4+DJq/K3CSTd27fjt8ieI5ekZplMJoS8Jh/2+lPW7vCrfDtz8abdzz0z9KnTtM/6h+v1XPS42mLHjGrPSyttNPx8t/MiOXTSW5q9lku030e9lsna7adt39/+PCD9xm6WknlTy4ev55INu85Kxx/W2nUR/Zv+AXrgoB/k/PkLUqxoEROanZL+AwbZUEavTYMWnR9Oh358MUtmyZI5k/xkQrWVq9fIiYdzJmqQp3/EzpUju9SoVln2/rNPRo0ZGyjNvXu+NoDcZYK+OfMWBNon4EI9h0MmLNI/vDtfp004GStWLIkWLZrMnbfQNeSkbjt3/kLxve9rg0WtDtPhCk+bYSirValsQxy9Xg1HtV3xviq7zPueffvbwCNTpoxm7rydNkTT9fYYD4OfWLFjSfQYfs9Pv/7fyTljVqdWTUmbJo2MHjPO4yEZdb/PstWoVsWGuzrcrNrduHHDdTrRjades7YY5lr1mk05mn0efjJhoAbCWpF39+5dGTBwiDhD02oA3deYLF+xSl4y4awGz3p/x4yd4Nq380bnqOvZ5xvrp0G1/r6wxzEd9H7qe63u09BO782In36W4ydOSr48uczykM9FQ5pe5n7u2fuPFCpUwATDPnZYUn1OtYX0TITmedbnKE3q1JIkSWK/fZtnQufV058V96Y/N9pOnfb7naLvNYzSir9z58/rxwjTihYtLH//vcXf/IBawVfMLA/YJvw6WWbOmiPZXsoqZUqXlNVr15nQbJj9vaLPV4zofsGzhtFOxZ4Gh8E9a8eOH5e+3w60/xBAj6lBnP7DANvM7wpts+bMlSVLl0nJ4sWkUoWycuTIMft7QOe7DOtx7Y7dvkWP5ve7QAP1/SbkfNymQxE7+3Ge/e+HDLPPTB7z7Ouz88v4iYEeRsNw/dLAV//Bhv4DgG/M7/G4ceOIBvX68zrQhJUB5/8MdGcsRAABBBBAAAEEEEAAAQQQQAABBBBAAIFILfBEKxcnL94vpQukkcQJYkqlIuklZoxoMn3pAWlRN4+kShpHxvV4RQq+O1nKFkwr339ext6IkvlSS6L4MaXtd6tkdNdKki5lPH83qEqJDNK/TSl/ywJ+SGuGYZ37fU07VOol79tS+ZNZ0n/cZvmtf7WAXR/5XKFwOsmUJoEUbzxNPm9QUN56Javt8+XIdRLPVDr+PqCGq+Kxbvv5MnjydlOlmFr2Hr1i/gjuJd0/LGrP/41KL0qaZHEf2X9EXDD6l/EmCMki3bt2FJ0LTquLunT7Wn4eO14GfNPbBohrTdXR22/VtVWKeo1aBdar77euyz12/IQNhRq+947EixdXypQqaatsXB3c3syY+bsNjeKYCkYdTlIraZzgxa2bv7caXF28eEn+1+lL1/LcuXKYoQg7SvnyZWTLoO2iYWWe3LnksAkgtSquZfNmtu/U6b9JbBOW9ejWWbRqUttXPfvYwCNnjuz2s34rV6aUqfh5x37u1qOXbN6yzVT61JdPzFCsQ+6PsIFph3Zt7XqthNK5A6tXrWJMatpgU//wny5tWrv+ef/2cqUKNkD81fiPnfCrTDFGTRu/JxXKlZV8ec3Pb8qUosFjPVN16FSL5jLe/fv2lIwZXrCXV7F8OWnZqq2tWNNlGsjtP3BQOrZva6s7tZOGi1HM/zQcdtrNGzelx/dDbNjRrXsHSZQooV2ltjpvXV5zD1uaqlFt5y9csK+JEiaUPj272+fTLjDfgjuX6TN+t916f91NdOhcPb4G4k6AGdIzEZrnWYfzTJY0qXNa4gzvqUNkujenz+XL/84pmSyZ33YadmolaERpeo+08vAvEyTXqlHNhrU7d+2Wzh3a+RvWVivm5pjwukmj90yw/6q9vLx5cks3UwF8wDwrum28uHFluJm38HMzRK8zzHFIz9pvM2ebZyGa2PtrQmydr1HD6p0mlHTaG6/VlhImWEyfzu9nMrMJd7Vy9MCBQ2E+rrNvfdVnSufMTGyqsLUS+Hvzvn+fr62Le7/HfZ/KVEd+3b2r/UcR+txq1aX31aui1dJO+9MEu1q1WNNUT2vwr+24+Z2s51irRnUpVDC/Gba3nP095QS4zra8IoAAAggggAACCCCAAAIIIIAAAggggAACAQWeWLh42gx7qkOZVi+d0b7qgaOaiiId8lTDxbC2t171C/uC277eyy+65mDU4Uxrls1kj3vfLcAIbvvA1s1dcVhu+fjKx32Xu1afNEO6Xr1+x36uZoZd/fG3nXauRj3ee9WymzA14gxj6LqoAG907jSdX6+uqWzRYFGbVq2VM8OPasWRDvF44OBBu7xcmX+HJNW55eLH/zcY1uqheQsWmrnT2ps/6BeVVypWcO3Pbvzwm/6xe/7Cxaa6saodCrJjl+7mD/7D5BvzR/ntO3fZP5hrhVPA5lQIfdSsiWuVDlmqrUihQjbQ1DntNFzUikq9hhLFitr1GoxqODnI/PHfaRpU3jAhl3srWaK466OGjvpHfA0tAhuuMmbMGOa4BW14dvjIESlujqV/vI9IrXSpEjYk1uEqJ06aauYDHG0rMF/K+mKQl6GB0GhTkXrB+Gloo0N96jOkbZ9Zl8wM26jDxjpNw46ATStEdfjU6lUr26FBA64P7LOG2c7z6awP7lw05NRhfZ2wSquTP3z/32cnpGfC0+dZz0UrwrQSzmkJzDC82jQAcm/epkJWm/vPjbPdrVu33bs+9++jRotqnx0NtTQg1OGQNSTOk9v/sKD7HlbzrVy1Rrbv2GmvS38HaDt4+IgZfjhsz9qBg4dFfwdpdaw2/RmtVLG8v3Dx/v0HdjhRnRdRq/V0+F1tF00YHFwL7rly306HT95jKrS7dGwvKcx5tDf/8OHH0b9Im09b2t9xxU0VuFPN6r5daN9rNbnzO0h/Lz148LscO3bc/q7Tfekwxn+tXG13+/Zb9Vy719+j+vwP+H6wHbq4tKkaLVywgGs9bxBAAAEEEEAAAQQQQAABBBBAAAEEEEAAgaAEntiwqNP/8BsCbt6qI/JO50X26+qNO3Lk1FXZauYuDGtLntivsiy47TVQdG+JTSXkLZ974nPH17XY+QO2LrjrNkSqq0OANxosasuUNoHr69XiL0hdE2Tq38ILZEsucwbWkIqmQnOOCSJfafm79B+/OcBeIt7Ha9ev25PWeQndW/x48UyVywM7fKnPbR+7KoYJ1NxbTLd59HQIyF5ffWnnU1u7boP5Q3tX0SqbgE0DFg1SMmfOaMOiz1p/Yiu99I/yOr/fwkV/BNzEfvYyQ+pqwKQVd86XVuNo0womrZRcv2GTnVtt9Zr1ZijEoq6hPTVY1KaVYc5XIfNH9rKmUtH9OXEPfXRozpBaaxMiNG74rgku78rPv4yTNl/8zw4tG9J2z8N6Dfc0ONbQTcPgr7p10pFPTRXi30Gens77NnLUGEmUMJGdczOdmcdQQxvH8Nq1aybk/TdwDmpHemxtGjg5z19QfZ3lCU3lonsL6VyuhnAuIT0Tnj7Pek4acl+4eNF1eskfViPqnKLuTYdq1eas1/ca0mrT6reI1vTnRyuE9+z5R5b9tUJKlyxhh7J1vw59JrTpULrOz54OIavD8mpFXlAtpPurz40T4jr7CPj5x1E/y8LFf5j5X7ObOT2rm6GuHwbAD8NNZzv315CO695Xh2XWpr/LUqdOJc2aNpTVa9bZOUfHmTk3N2/Z6t49zO/dfzc7v5ecn7mr167bfyShQwVr06FgnaZV2r2+/tJWjGoQ2q//QOnRq5/r59XpxysCCCCAAAIIIIAAAggggAACCCCAAAIIIBBQIOSEJOAWHn6esmS/7Tmhp99Qd/rhtgn4mn39p+i6/CaM06aBhc+de/a9802HSdR22y0MdNZ58rp662nXcKbaf7WZFzFr+oRmDsVokvrhUKX/HL1shzPV9VsChJ0aqGhzDyPLFUprA8qvPipm1+m3A8evyAup4ttruHPXV3TOyMHtypq5/B5Iu+9XyfDpO+TjenldVZSuDSPQG60+ixUzpqn42W2rW5xT188JEsSXFCmSS5aHc8dt27ZDtNpN24mTJ13BiH7WeeO8TPWQDiF6//59GWnmyJszd4Edos99GD6tbtIhLg8dOixa2ZM2TWozHGdDUzU3SncjbVp9bF9D+61C+bKyYNESW22pw1JWrPBvFWG+PLnNvfaRRg38hjzVfWsokiJ5cnNv/Z6FkI6n/e7cveuv25mz5+RlU6GpFXi6v3YdOpuQZaW8Wfc1f/2etw86VGXbdh2lSuVXpMG79e3paTCj1VE6V6BtD1m0Ks9pf2/eKjqMZ9s2n1g3DegWuIXBGshpHw0P9dnRdtLMyRnDhNBOhZkua/je23auwQ6du8n4iZOl5UfNdLFtetiAzg9X+Xvx5Fx0LkQNYZx7/M8/++WFF9LZoXFDeiY8fZ71pFKnSilr1q23z73OE6lBqFZ//rF0mR2mUsNvbfMWLLLz3+kwwE7TOfb0/FKZfUS0pvc7jQnVho38yVav6rDCAZvzu6N4sSKiw6Fq02pgDVpdQwg//Bm8a0J6p4V0f7NkymiHQXa/vzt27nY2t69/m3CvhDlu08YN7GcNM/U+uVoYjuva1rxxhgfW32U6lLBWdu/YsctWPGs/rf5+0k3nEtU2fsxPdohWnXOxlKnA1rBT2yUTXuvQzvqlwenQET/ZuWbdn0HbkW8IIIAAAggggAACCCCAAAIIIIAAAggggICbwBMJFzftPmeCN2/51syN+HLR9G6HE6lTPrPMXHZINKSLET2qFM6RUlZuPSWTF+2THJmTSL6syaRwTr+KlSFTtkkpMwfjmw/nPfS3o2A+TDXhpc7VWN4EglpFuHzTSflfo4J2C53fMVrUKPLdxK3SxoSAf5p1er6xY/47hGn6VPEkpZkTcsrifXZo04pmHsa6lbLIp9+skM5D14oOzbr3yGXpMmydPbeeLYtLq/4rZPH6YzLwszKSPVNiOX/5lh0GNnq0J1YcGoxA2FZt3bbdVCL6VSrqHlKmSCElSxSTcuVK24rBBPETSJ5cOWXz1m2iw+zVqVXDHihzpox2TsQfR4+xFVpaETN77nxXaKOdhg4faYOlFiYo0uEHtUJRgxZnOD+7o4ffihUrbIPHJGZOuqwvZnENq6mrdR7GoNqJEyftPInu6yuaUFHDHK2M0j/2a6WSBpbuQ6tqhdWQYSNNdeF4M6dgGTl24oR5P8H88b+UNG3kFzy47zOw9xoWqYled7p0ae0wiJ+37yTFihSW11+rZeec09DEGeYysH08L8tSmpDlxRczm2BwsZ37UkMfDR40UHNCBw1etZpumRnuUocjLZAvjx3i8crfV+zckxo661CqrjDSXFxRY/H7rLnS79uBJmB93VSo3pLxZmhdne+yX+8erssvWqSQffYqv1LJ3i+d09GZ+/KlrFlt0K2VcBleeMHfEKKuHZg3OtxkcOeiQc/goSNkmAlTKlUsL0eOHpOx438178tJsyaNbNVqcM9EaJ5nraBd/MefssMM66shkzYdKvTbgYPN1yAbQOvPngZmr9WuKe5h+5q168x8p5n8zZ9ndxBBvunP1uSpM8y9Si8ZHs7F6X7qmTNlssPnDjP/eKBZ00a22nDe/EW2qm/gt31tWJ3N/GxpmzlrjuQ2w6rqvQvp/pYqVVxG/Piz+b3zo61k1mFw9WfTvSUxFaWHDh+RM2fO2qFr9f67t7Ac1337tKZyV/9xhs4X+b75BxIaqN8w1cBOc0Jt57P7a1C/i937ePJe573U33Ua4Ddu+J5s275TfjJV4F926WCfSX3/Vr3XzbDNRUT/MYQ27UtDAAEEEEAAAQQQQAABBBBAAAEEEEAAAQSCE3gi4aJWJkY1AV61UhkfOXaNMhnl9+WHZMHqo1LbBI0fvJZL9h27Im0GrLRhpIaLWV9IJI1r5rAVjn9tPhnqcPGTt/LKhPl75bsJW2xo2LR2Tmnzjt9cUvHjRJd2DQvKTzN3yRvtF8iLpqKxYY3sMs2cs9O8TMVKe9Pnm3Gbpc23K2TV6LpS7+WscsnbRwZP3iajZ+2WWGY+xVfMsKhdmxWxm3V+v7AcP3tNPuq9zH6OHze6fNu6tBlqL+KEizrkpfuwlwXMnGUaLmrAdt/3vhlSb54d0k8DPq3Ge6e+3/xdGhB27vCF/SP6DDMPoQ6Pqut1CMBoZp22d99+S86dvyADBw+1n3UfH33QVJxh/OzCh9+c42nVmla/aQipAdA2E8BsMV9VXn3Zvbvr/VEzz5h+uTedd80ZMlOrF8eMnSAB5z7UAESHyZz5+xxb3RjDhGUaCDUw1TyethLGSe0mTppiK4NafdLCVuBNmjxN1m3YaHejnlrJGBFa2zafyvARo2TBwiX2S+eo1PtdpLBfSK/BiIYSU6b9ZgO6QQP6maElq9ng78uvetlLLGPmcNO55pymoW6H9m1tFWrPPt+Y+xrFhNKZpdXHzQMNmeu/+YboELo/jhoj3/brZZ+V6tW0CvSUOeYo+/zky+tX7eYcw3kN6Vz03HTozBkzZ8nyFatsoFKkUAFp9J5f9WpIz0Ronuc8uXPbMGzRkqWucFHnbNTnX5/xzVu22aF7dSjQt9+q61yCHDbB1/4Dh+QDE7pF1Fa2dCn7jKh3YE2rNrt2am/m/ftB+g/43g61rP+oofWnLWywqNtoSKdB8/IVK828jLtsuBjS/a1UobxcvHhZ5s5fYEP/uHHjyOt1arqqBnW/TRq9J/2/GySfftbO79kyv7Pcf3+E5bi6X/fWrWtHGWiuTYNkrdjWsK9alVdlxarVNtDW6s7AWlC/iwPrG9wy/T0bL25c20Writ8wc+dq6P/XylX2H1LonJf6M6xf+vNY2/wMB3VOwR2HdQgggAACCCCAAAIIIIAAAggggAACCCAQuQSiDB0+/EGLj5o/86u+cs1HEpm5Ed2bDqOqIUZME+SFtukwpYfN/I4ZUycIdHudZ1Hnf8ycLqGtMAxs/zq86c3b90QDSaeZURTl4AlvSW8qIwM7r3OXbon3dR87L6MOk/ok2sPR+p7EroPdp1beaXWLDvOogZ97u3Tpsjww/9MhTTVsvHzlijT/uLUJ2N6xQaPT98oVbzMH2A07zGNgVYtOP33V4+m8dFolF9MEljoXY+zYsdy7hOt7HUJR50nTITq1Gi8sTecp1CDO8dHKvdOmMipRwgSmys5vKNCw7PdZbaP36uKlS6aSMKW9BwHPQwMTnVdRq1WdpsOqamicNGkSZ9EjrxfNcIxaoRcvnl/w8UiHEBZcv37Do21DOhe951q5psFLYPc8pGfC0+f5T1PhqUP7tvu8tb/hhXX/fs+4/2dOn/1OXb+y1Z1awRfSz0oIXBFitT5rN001a/Jk/wbS7ieu/9BA///A/T6FdH8D/g5x35++d9YnMVW4cR+GcAH7hOW4Afehv7sumZ8jHd5W7+WT/l0W8PjBfdYK4vMXLgT5Mx7ctqxDAAEEEEAAAQQQQAABBBBAAAEEEEAAgcgp8NyEi5GTP2xX/azCxaDOVv9Q3rLVZ6LDDGqYGNWEqnPmLbBVRt+Y4S51GFQaApFdYISZZ7SUqXDNkztXiBQ+Pndk9C/j5NWXK1JJFqIWHRBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQSepgDh4tPUDqdjPW/hol7Wzl275Qczv5lWpGmLHz+etGz+gRQu6DccrV3INwQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAgQgt8ETmXIzQIpx8mARy58opw4cMlHPnztuhBlOnTmWHMAzTztgIAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEDguRQgXHwub0vEPCmdDy1lyhQR8+Q5awQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAgRAFvELsQQcEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEDACBAu8hgggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggIBHAoSLHjHRCQEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEvA4ePIQCAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAgggEKJAlLTpX3hw/OjREDvS4fkRiBLl+TkXzgQBBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQCDyCHgVLFQw8lwtV4oAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAmEWiJY4UWJJ1HB/mHfAhk9fwHt81qd/UI6IAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCAQ6QW8Ir0AAAgggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggg4JFANO21tjMZo0daz0mn3XsPPidnwmkggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAghEJgEbLubMniUyXTPXigACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACYRCgZDEMaGyCAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAQGQU8CpWrGhkvG6uGQEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEQilA5WIoweiOAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAQGQVIFyMrHee60YAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAglAKEi6EEozsCCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACkVWAcDGy3nmuGwEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAIFQChAuhhKM7ggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAghEVoFonlz47r0HPelGHwQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQ+A8K5MyexV6VR+Gi0/k/6MAlIYAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIICAhwIMi+ohFN0QQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQiOwChIuR/Qng+hFAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBDwUIBw0UMouiGAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCAQ2QUIFyP7E8D1I4AAAggggAACCCCAAAIIIIAAAggggAAC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lFwNn4QCFKAABShAAQpQgAIUoAAFKEABClCAAhSgAAUoQAEKUIACFLhnAtGn6IT2LBSgAAUoQAEKUIACFKAABShAAQpQgAIUoAAFKEABClCAAhSgAAX8CUQv92opf/twOwUoQAEKUIACFKAABShAAQpQgAIUoAAFKEABClCAAhSgAAUoQAGJTgMKUIACFKAABShAAQpQgAIUoAAFKEABClCAAhSgAAUoQAEKUIACgQgwuBiIEvehAAUoQAEKUIACFKAABShAAQpQgAIUoAAFKEABClCAAhSgAAU4cpHvAQpQgAIUoAAFKEABClCAAhSgAAUoQAEKUIACFKAABShAAQpQIDCBkMNHjvvdc8Om7X734Q4UoAAFKEABClCAAhSgAAUoQAEKUIACFKAABShAAQpQgAIUoMCjKZD12YzmwkKShyXxe4V2Z787cgcKUIACFKAABShAAQpQgAIUoAAFKEABClCAAhSgAAUoQAEKUOCRFeCci4/sS8sLowAFKEABClCAAhSgAAUoQAEKUIACFKAABShAAQpQgAIUoMDdFWBw8e56sjYKUIACFKAABShAAQpQgAIUoAAFKEABClCAAhSgAAUoQAEKPLICDC4+si8tL4wCFKAABShAAQpQgAIUoAAFKEABClCAAhSgAAUoQAEKUIACd1fg/wD8pU5v+LJW1QAAAABJRU5ErkJggg==)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "vaY6vhFE9egF" + }, + "source": [ + "## Clean Up\n", + "\n", + "* Delete the Spanner instance\n", + "* Delete the Vertex AI Agent Engine instance" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "Qb4FcKf9dS-d" + }, + "outputs": [], + "source": [ + "!gcloud spanner instances delete {SPANNER_INSTANCE_ID} --quiet" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "HCOr8HuJ1p7S" + }, + "outputs": [], + "source": [ + "remote_app.delete(force=True)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "IKFKrjkud9pn" + }, + "source": [ + "## What's next\n", + "\n", + "* Dive deeper into [LangChain with Spanner](https://github.com/googleapis/langchain-google-spanner-python/tree/main).\n", + "* Learn more about [Spanner](https://cloud.google.com/spanner/docs/getting-started/python).\n", + "* Explore other [Spanner Graph Notebooks](https://github.com/cloudspannerecosystem/spanner-graph-notebook/blob/main/README.md).\n", + "* Learn more about [Vertex AI Engine](https://cloud.google.com/vertex-ai/generative-ai/docs/agent-engine/overview).\n", + "* Learn more about [Agent Development Kit](https://google.github.io/adk-docs/)." + ] + } + ], + "metadata": { + "colab": { + "collapsed_sections": [ + "tC8nObmLw9P0", + "zTa3RFWesfsL", + "DoK1gW9dsRxB", + "Axb7c8Y1YmQ8", + "QHXfcXadd9pl", + "4OVOZZ1Ld9pm", + "IKFKrjkud9pn" + ], + "name": "graph_rag_spanner_sdk_adk.ipynb", + "toc_visible": true + }, + "kernelspec": { + "display_name": "Python 3", + "name": "python3" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} diff --git a/gemini/use-cases/graphrag/tourism_agentic_graph_rag_neo4j.ipynb b/gemini/use-cases/graphrag/tourism_agentic_graph_rag_neo4j.ipynb new file mode 100644 index 0000000..7073989 --- /dev/null +++ b/gemini/use-cases/graphrag/tourism_agentic_graph_rag_neo4j.ipynb @@ -0,0 +1,1122 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "Copyright_01" + }, + "outputs": [], + "source": [ + "# Copyright 2026 Google LLC\n", + "#\n", + "# Licensed under the Apache License, Version 2.0 (the \"License\");\n", + "# you may not use this file except in compliance with the License.\n", + "# You may obtain a copy of the License at\n", + "#\n", + "# https://www.apache.org/licenses/LICENSE-2.0\n", + "#\n", + "# Unless required by applicable law or agreed to in writing, software\n", + "# distributed under the License is distributed on an \"AS IS\" BASIS,\n", + "# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n", + "# See the License for the specific language governing permissions and\n", + "# limitations under the License." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "Header_01" + }, + "source": [ + "# 🌿 Eco-Nomad Swarm: 100% Real-Data Sustainable Travel Orchestration\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + "
    \n", + " \n", + " \"Google
    Open in Colab\n", + "
    \n", + "
    \n", + " \n", + " \"Google
    Open in Colab Enterprise\n", + "
    \n", + "
    \n", + " \n", + " \"Vertex
    Open in Vertex AI Workbench\n", + "
    \n", + "
    \n", + " \n", + " \"GitHub
    View on GitHub\n", + "
    \n", + "
    \n", + "\n", + "
    " + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "Author_Architecture" + }, + "source": [ + "| Author | Architecture | Rule |\n", + "| --- | --- | --- |\n", + "| [Aniket Agrawal](https://github.com/aniketagrawal2012) | **Vertex AI Agent Engine + ADK Swarm + Neo4j GraphRAG + Open-Meteo + Frankfurter + RESTCountries + Deep-Translator** | **ZERO MOCKED DATA.** |" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "taCKYJSpuF4f" + }, + "source": [ + "Share to:\n", + "\n", + "\n", + " \"LinkedIn\n", + "\n", + "\n", + "\n", + " \"Bluesky\n", + "\n", + "\n", + "\n", + " \"X\n", + "\n", + "\n", + "\n", + " \"Reddit\n", + "\n", + "\n", + "\n", + " \"Facebook\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "Overview_01" + }, + "source": [ + "### 💎 Executive Summary: Agentic Chaining with 100% Real APIs\n", + "\n", + "In an era of generic travel recommendation engines, true AI utility requires stepping out of the sandbox and interacting with the live, messy reality of global systems. Simulating travel data—like hardcoded flight prices or mocked weather states—fundamentally restricts an agent's reasoning capabilities.\n", + "\n", + "This notebook constructs the **Eco-Nomad Swarm**, a diamond-standard orchestrator designed around sustainable overland travel across Europe and Asia. By combining **Google's Agent Development Kit (ADK)** with six disparate, live REST APIs and a Neo4j Graph Database, we build an agent that reasons cross-culturally and mathematically without a single line of faked data.\n", + "\n", + "### 🧠 The Agentic Logic Chain\n", + "\n", + "To satisfy a single complex travel query, the EcoOrchestrator executes the following deterministic cascade:\n", + "\n", + "1. **GraphRAG Topology:** Navigates a **Neo4j** graph to locate low-carbon transit vectors (e.g., Mumbai to New Delhi) and extracts the destination country. \n", + "2. **Geo-Demographics:** Passes the extracted country to the **RESTCountries API** to dynamically discover the official national currency code (INR). \n", + "3. **Live Treasury/Forex:** Feeds the target currency code into the **Frankfurter API** to convert the user's USD budget against today's European Central Bank market rates. \n", + "4. **Environmental Telemetry:** Geocodes the destination and queries the **Open-Meteo API** for the exact real-time temperature and windspeed. \n", + "5. **Open Web Intel:** Utilizes the **DuckDuckGo Search API** to verify recent news, events, or trail closures. \n", + "6. **Cross-Lingual Bridging:** Leverages the **Deep-Translator API** to provide localized survival phrases natively translated into regional scripts.\n", + "\n", + "### 🏁 Objectives\n", + "\n", + "The primary goal of the **Eco-Nomad Swarm** is to bridge the gap between generative intelligence and the physical world through three core operational pillars:\n", + "\n", + "* **Zero-Mock Data Integrity:** Ensure every tool queries a live database or a real-world, open-source REST endpoint rather than simulated datasets. \n", + "* **Logical Tool Chaining:** Architect an orchestration flow where data extracted from the GraphRAG layer (e.g., country) is dynamically piped into subsequent functional tools (e.g., forex and weather APIs) to provide a unified, actionable travel brief. \n", + "* **End-to-End Orchestration:** Demonstrate enterprise-grade deployment of a reasoning swarm via **Vertex AI**, transforming localized agent logic into a managed, production-ready travel intelligence engine." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "s7xSyw2CudzD" + }, + "source": [ + "## Before you begin\n", + "\n", + "1. In the Google Cloud console, on the project selector page, select or [create a Google Cloud project](https://cloud.google.com/resource-manager/docs/creating-managing-projects).\n", + "2. [Make sure that billing is enabled for your Google Cloud project](https://cloud.google.com/billing/docs/how-to/verify-billing-enabled#console).\n", + "3. [Make sure the Vertex AI API is enabled](https://console.cloud.google.com/flows/enableapi?apiid=aiplatform.googleapis.com).\n", + "\n", + "### Required roles\n", + "To get the permissions that you need to complete the tutorial, ask your administrator to grant you the [Vertex AI User](https://cloud.google.com/iam/docs/understanding-roles#aiplatform.user) (`roles/aiplatform.user`) IAM role on your project. For more information about granting roles, see [Manage access](https://cloud.google.com/iam/docs/granting-changing-revoking-access)." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "Install_01" + }, + "source": [ + "### 1. Setup & Dependencies\n", + "We install the core libraries for agentic orchestration (`google-adk`), graph processing (`neo4j`), open web search (`duckduckgo-search`), real-time translation (`deep-translator`), and HTTP requests (`requests`)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "PipInstall_01", + "outputId": "d662f137-b8ac-412e-d11b-72c878c20b1b" + }, + "outputs": [], + "source": [ + "%pip install --quiet google-adk>=1.0.0\n", + "%pip install --quiet google-cloud-aiplatform>=1.97.0\n", + "%pip install --quiet langchain-google-vertexai\n", + "%pip install --quiet langchain-community neo4j pyvis duckduckgo-search requests deep-translator ddgs" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "vztzX3nkuzCf" + }, + "source": [ + "#### Authenticating your notebook environment\n", + "* If you are using **Colab** to run this notebook, uncomment the cell below and continue.\n", + "* If you are using **Vertex AI Workbench**, check out the setup instructions [here](https://github.com/GoogleCloudPlatform/generative-ai/tree/main/setup-env)." + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "Config_01", + "outputId": "6905010b-0332-407b-96cd-320b84aec732" + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "✅ Authenticated\n" + ] + } + ], + "source": [ + "import os\n", + "import vertexai\n", + "import sys\n", + "\n", + "if \"google.colab\" in sys.modules:\n", + " from google.colab import auth\n", + " auth.authenticate_user()\n", + " print(\"✅ Authenticated\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "BgPYTWtWu7qG" + }, + "outputs": [], + "source": [ + "# --- CONFIGURATION ---\n", + "PROJECT_ID = \"your-project-id\" # @param {type:\"string\"}\n", + "REGION = \"us-central1\" # @param {type:\"string\"}\n", + "NEO4J_URI = \"neo4j+s://your-instance.databases.neo4j.io\" # @param {type:\"string\"}\n", + "NEO4J_USER = \"neo4j\" # @param {type:\"string\"}\n", + "NEO4J_PASSWORD = \"your-password\" # @param {type:\"string\"}\n", + "NEO4J_DATABASE=\"neo4j-database\"\n", + "\n", + "os.environ[\"GOOGLE_CLOUD_PROJECT\"] = PROJECT_ID\n", + "os.environ[\"GOOGLE_CLOUD_LOCATION\"] = REGION\n", + "os.environ[\"NEO4J_URI\"] = NEO4J_URI\n", + "os.environ[\"NEO4J_USER\"] = NEO4J_USER\n", + "os.environ[\"NEO4J_PASSWORD\"] = NEO4J_PASSWORD\n", + "\n", + "vertexai.init(project=PROJECT_ID, location=REGION)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "Seeding_01" + }, + "source": [ + "### 2. Hydrating the Global Babel Graph\n", + "A robust agent requires a robust foundation. We map a European and Asian cross-border sustainable travel network directly into Neo4j using Cypher. This schema structurally links Points of Interest (POIs) to Cities, Cities to Countries, and calculates the exact `co2_saved_vs_flying_kg` on `TRAIN_ROUTE` vectors." + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "SeedCode_01", + "outputId": "402a9487-bf69-4574-d469-23e7c89c9a5c" + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "đŸŒ± Hydrating Eco-Nomad Global Rail Topology...\n", + "✅ Global Eco-Graph Hydrated.\n" + ] + } + ], + "source": [ + "from langchain_community.graphs import Neo4jGraph\n", + "\n", + "def seed_eco_graph():\n", + " print(\"đŸŒ± Hydrating Eco-Nomad Global Rail Topology...\")\n", + " graph = Neo4jGraph(url=NEO4J_URI, username=NEO4J_USER, password=NEO4J_PASSWORD, database=NEO4J_DATABASE)\n", + "\n", + " cypher = \"\"\"\n", + " // European Countries\n", + " MERGE (fr:Country {name: 'France'})\n", + " MERGE (ch:Country {name: 'Switzerland'})\n", + " MERGE (de:Country {name: 'Germany'})\n", + " // Asian Countries\n", + " MERGE (cn:Country {name: 'China'})\n", + " MERGE (kr:Country {name: 'South Korea'})\n", + " MERGE (in:Country {name: 'India'})\n", + "\n", + " // European Cities\n", + " MERGE (paris:City {name: 'Paris'})-[:IN]->(fr)\n", + " MERGE (geneva:City {name: 'Geneva'})-[:IN]->(ch)\n", + " MERGE (freiburg:City {name: 'Freiburg'})-[:IN]->(de)\n", + " // Asian Cities\n", + " MERGE (beijing:City {name: 'Beijing'})-[:IN]->(cn)\n", + " MERGE (seoul:City {name: 'Seoul'})-[:IN]->(kr)\n", + " MERGE (mumbai:City {name: 'Mumbai'})-[:IN]->(in)\n", + " MERGE (delhi:City {name: 'New Delhi'})-[:IN]->(in)\n", + "\n", + " // European POIs\n", + " MERGE (p1:POI {name: 'Louvre Museum', eco_rating: 'Silver'})-[:LOCATED_IN]->(paris)\n", + " MERGE (p2:POI {name: 'Lake Geneva Biosphere', eco_rating: 'Gold'})-[:LOCATED_IN]->(geneva)\n", + " MERGE (p3:POI {name: 'Black Forest Eco-Trail', eco_rating: 'Platinum'})-[:LOCATED_IN]->(freiburg)\n", + " // Asian POIs\n", + " MERGE (p4:POI {name: 'Great Wall Eco-Hike', eco_rating: 'Gold'})-[:LOCATED_IN]->(beijing)\n", + " MERGE (p5:POI {name: 'Namsan Mountain Trail', eco_rating: 'Silver'})-[:LOCATED_IN]->(seoul)\n", + " MERGE (p6:POI {name: 'Western Ghats Bio-Reserve', eco_rating: 'Platinum'})-[:LOCATED_IN]->(mumbai)\n", + " MERGE (p7:POI {name: 'Aravalli Biodiversity Park', eco_rating: 'Gold'})-[:LOCATED_IN]->(delhi)\n", + "\n", + " // European Routes\n", + " MERGE (paris)-[r1:TRAIN_ROUTE {train_type: 'TGV Lyria', duration_hrs: 3.2}]->(geneva)\n", + " SET r1.co2_saved_vs_flying_kg = 105, r1.scenic_rating = 'High'\n", + "\n", + " MERGE (paris)-[r2:TRAIN_ROUTE {train_type: 'TGV INOUI', duration_hrs: 1.8}]->(freiburg)\n", + " SET r2.co2_saved_vs_flying_kg = 88, r2.scenic_rating = 'Medium'\n", + "\n", + " // Asian Routes\n", + " MERGE (beijing)-[r3:TRAIN_ROUTE {train_type: 'CRH High-Speed + Ferry', duration_hrs: 14.5}]->(seoul)\n", + " SET r3.co2_saved_vs_flying_kg = 210, r3.scenic_rating = 'High'\n", + "\n", + " MERGE (mumbai)-[r4:TRAIN_ROUTE {train_type: 'Rajdhani Express', duration_hrs: 15.5}]->(delhi)\n", + " SET r4.co2_saved_vs_flying_kg = 145, r4.scenic_rating = 'Medium'\n", + " \"\"\"\n", + " graph.query(cypher)\n", + " print(\"✅ Global Eco-Graph Hydrated.\")\n", + "\n", + "if \"your-password\" not in NEO4J_PASSWORD:\n", + " seed_eco_graph()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "Tools_01" + }, + "source": [ + "### 3\\. Defining 100% Real Open-Source APIs (Zero Mocking)\n", + "\n", + "To achieve diamond-standard intelligence, we arm our orchestrator with six specialized functions. None of these functions contain simulated data; they act as live hooks into global datasets:\n", + "\n", + "* `query_eco_graph`: Uses GraphCypherQAChain to safely map NLP to our Neo4j instance. \n", + "* `get_country_intelligence`: Connects to restcountries.com to map city data to sovereign currency codes. \n", + "* `live_currency_conversion`: Fetches real-time ECB fiat rates from the Frankfurter API. \n", + "* `get_live_weather`: Hits the Open-Meteo API for current global telemetry. \n", + "* `search_live_web`: Queries DuckDuckGo for unstructured open-web intelligence. \n", + "* `translate_text`: Uses the deep-translator library to interface with free translation endpoints to bridge linguistic gaps." + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "metadata": { + "id": "ToolCode_01" + }, + "outputs": [], + "source": [ + "from langchain_google_vertexai import VertexAI\n", + "from langchain_community.chains.graph_qa.cypher import GraphCypherQAChain\n", + "from langchain_community.tools import DuckDuckGoSearchRun\n", + "from deep_translator import GoogleTranslator\n", + "import requests\n", + "import os\n", + "\n", + "# 1. GraphRAG Tool (Neo4j)\n", + "def query_eco_graph(question: str) -> str:\n", + " \"\"\"Queries the Neo4j database for overland train routes, CO2 savings, and sustainable destinations.\"\"\"\n", + " try:\n", + " from langchain_community.graphs import Neo4jGraph\n", + " graph = Neo4jGraph(url=os.environ[\"NEO4J_URI\"], username=os.environ[\"NEO4J_USER\"], password=os.environ[\"NEO4J_PASSWORD\"], database=NEO4J_DATABASE)\n", + " llm = VertexAI(model_name=\"gemini-2.0-flash\", temperature=0)\n", + " chain = GraphCypherQAChain.from_llm(llm=llm, graph=graph, verbose=True, allow_dangerous_requests=True)\n", + " return chain.invoke(question)['result']\n", + " except Exception as e:\n", + " return f\"Graph Database error: {e}\"\n", + "\n", + "# 2. Real Geo-Demographics (RESTCountries API - Free, No Auth)\n", + "def get_country_intelligence(country: str) -> str:\n", + " \"\"\"Fetches live demographic data including the official currency code (e.g. EUR, CHF, USD) for a country.\"\"\"\n", + " try:\n", + " url = f\"https://restcountries.com/v3.1/name/{country}?fullText=true\"\n", + " response = requests.get(url, timeout=5).json()[0]\n", + " currencies = list(response.get('currencies', {}).keys())\n", + " capital = response.get('capital', ['Unknown'])[0]\n", + " population = response.get('population', 'Unknown')\n", + " return f\"Country: {country}. Capital: {capital}. Population: {population}. Official Currency Codes: {currencies}.\"\n", + " except Exception as e:\n", + " return f\"Could not fetch country intel for {country}.\"\n", + "\n", + "# 3. Real Currency Conversion (Frankfurter API - Free, No Auth, ECB Rates)\n", + "def live_currency_conversion(amount: float, from_currency: str, to_currency: str) -> str:\n", + " \"\"\"Converts currency using LIVE European Central Bank rates. Must use exact 3-letter codes (e.g. USD, EUR, CHF).\"\"\"\n", + " try:\n", + " url = f\"https://api.frankfurter.app/latest?amount={amount}&from={from_currency}&to={to_currency}\"\n", + " data = requests.get(url, timeout=5).json()\n", + " converted = data.get(\"rates\", {}).get(to_currency)\n", + " if not converted: return f\"Frankfurter API does not support {to_currency} or {from_currency}.\"\n", + " return f\"Live Rate (Date: {data.get('date')}): {amount} {from_currency} = {converted} {to_currency}.\"\n", + " except Exception as e:\n", + " return f\"Live currency API error: {str(e)}\"\n", + "\n", + "# 4. Real Live Weather (Open-Meteo API - Free, No Auth)\n", + "def get_live_weather(city: str) -> str:\n", + " \"\"\"Fetches the actual real-time temperature and windspeed for a city right now.\"\"\"\n", + " try:\n", + " # Geocode first\n", + " geo_url = f\"https://geocoding-api.open-meteo.com/v1/search?name={city}&count=1&language=en&format=json\"\n", + " geo_data = requests.get(geo_url, timeout=5).json()\n", + " if not geo_data.get(\"results\"): return f\"Could not find coordinates for {city}\"\n", + " lat, lon = geo_data[\"results\"][0][\"latitude\"], geo_data[\"results\"][0][\"longitude\"]\n", + "\n", + " # Fetch weather\n", + " weather_url = f\"https://api.open-meteo.com/v1/forecast?latitude={lat}&longitude={lon}¤t_weather=true\"\n", + " weather_data = requests.get(weather_url, timeout=5).json()\n", + " cw = weather_data.get(\"current_weather\", {})\n", + " return f\"Live Weather in {city} (Lat: {lat}, Lon: {lon}): {cw.get('temperature')}°C, Windspeed {cw.get('windspeed')}km/h.\"\n", + " except Exception as e:\n", + " return f\"Weather API error: {str(e)}\"\n", + "\n", + "# 5. Open Web Search (DuckDuckGo)\n", + "def search_live_web(query: str) -> str:\n", + " \"\"\"Searches the open internet for live updates or news.\"\"\"\n", + " return DuckDuckGoSearchRun().invoke(query)\n", + "\n", + "# 6. Real Multilingual Translation (Deep-Translator - Free, No Auth)\n", + "def translate_text(text: str, target_language_code: str) -> str:\n", + " \"\"\"Translates text into the target language using free open-source endpoints. Use 2-letter ISO codes (e.g., 'fr', 'de', 'es', 'ja').\"\"\"\n", + " try:\n", + " # Uses GoogleTranslate free web tier under the hood\n", + " translated = GoogleTranslator(source='auto', target=target_language_code).translate(text)\n", + " return f\"Original: '{text}'\\nTranslated to '{target_language_code}': {translated}\"\n", + " except Exception as e:\n", + " return f\"Translation API error: {str(e)}\"\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "AgentDef_01" + }, + "source": [ + "### 4. Assemble the ADK Agent Swarm\n", + "Using the **Google Agent Development Kit (ADK)**, we establish the EcoOrchestrator. The orchestrator is bound to an exact process flow: it must map the network, identify the local currency, verify the financial footprint, and provide localized language bridges—compiling the final output into a single luxury travel brief." + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "AgentCode_01", + "outputId": "ac1eb54f-6b6b-491a-bce2-8a730d408eba" + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "✹ 7-Agent Real-Data Swarm Initialized (including Multilingual Support).\n" + ] + } + ], + "source": [ + "from google.adk.agents import Agent\n", + "\n", + "graph_agent = Agent(\n", + " name=\"EcoPathfinder\",\n", + " model=\"gemini-2.5-flash\",\n", + " instruction=\"Expert in Neo4j train routing, CO2 savings, and determining which country a city is located in. Always ensure Cypher queries return ALL properties of the route relationship (e.g., RETURN r) so train type and duration are included.\",\n", + " tools=[query_eco_graph]\n", + ")\n", + "\n", + "demographics_agent = Agent(\n", + " name=\"DemographicsDesk\",\n", + " model=\"gemini-2.5-flash\",\n", + " instruction=\"Expert at retrieving official currency codes (e.g., CHF, EUR) for a given country.\",\n", + " tools=[get_country_intelligence]\n", + ")\n", + "\n", + "treasury_agent = Agent(\n", + " name=\"TreasuryDesk\",\n", + " model=\"gemini-2.5-flash\",\n", + " instruction=\"Handles live market currency conversions using 3-letter codes.\",\n", + " tools=[live_currency_conversion]\n", + ")\n", + "\n", + "climate_agent = Agent(\n", + " name=\"ClimateDesk\",\n", + " model=\"gemini-2.5-flash\",\n", + " instruction=\"Retrieves actual live weather telemetry for cities.\",\n", + " tools=[get_live_weather]\n", + ")\n", + "\n", + "intelligence_agent = Agent(\n", + " name=\"WebIntelligence\",\n", + " model=\"gemini-2.5-flash\",\n", + " instruction=\"Searches the open internet for live tourism updates.\",\n", + " tools=[search_live_web]\n", + ")\n", + "\n", + "linguist_agent = Agent(\n", + " name=\"LinguistDesk\",\n", + " model=\"gemini-2.5-flash\",\n", + " instruction=\"Translates survival phrases into the target destination's local language utilizing real APIs.\",\n", + " tools=[translate_text]\n", + ")\n", + "\n", + "eco_orchestrator = Agent(\n", + " name=\"EcoOrchestrator\",\n", + " model=\"gemini-2.5-flash\",\n", + " description=\"The supreme manager for sustainable, real-data travel planning.\",\n", + " instruction=\"\"\"\n", + " You manage a swarm of agents to build 100% real travel itineraries.\n", + " When given a request:\n", + " 1. Use EcoPathfinder to find the train route in the graph, the CO2 saved, and the destination COUNTRY.\n", + " 2. Pass that COUNTRY to the DemographicsDesk to discover its native 3-letter currency code.\n", + " 3. Pass that currency code and the user's USD budget to the TreasuryDesk for a live conversion.\n", + " 4. Use ClimateDesk to get the live weather right now at the destination.\n", + " 5. Use WebIntelligence to find a recent news headline about the destination.\n", + " 6. Use LinguistDesk to translate any requested phrases into the local language.\n", + "\n", + " Output a highly detailed, professional brief compiling all this REAL data.\n", + " \"\"\",\n", + " tools=[\n", + " query_eco_graph,\n", + " get_country_intelligence,\n", + " live_currency_conversion,\n", + " get_live_weather,\n", + " search_live_web,\n", + " translate_text\n", + " ]\n", + ")\n", + "\n", + "print(\"✹ 7-Agent Real-Data Swarm Initialized (including Multilingual Support).\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "Test_01" + }, + "source": [ + "### 5. Execute the Swarm (Local Sandbox Trace)\n", + "We inject a complex, multi-lingual, multi-domain prompt into the system to observe the deterministic execution trace as the Orchestrator maps routes, fetches live forex data, retrieves real-time weather, and returns localized translations in multiple languages." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "9628e48f" + }, + "source": [ + "#### 5.1. Deploy the Agent Swarm to Vertex AI\n", + "\n", + "Since the local `AdkApp` sandbox is encountering authentication issues in this Colab environment (it expects GCE metadata service credentials), we will proceed by deploying the `eco_orchestrator` as a `ReasoningEngine` to Vertex AI. This is the intended production deployment method and correctly handles authentication. Once deployed, we can interact with the remote engine." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "TestCode_01", + "outputId": "9c33121f-4858-4a9c-9358-2c3ec29a804d" + }, + "outputs": [], + "source": [ + "from vertexai.preview import reasoning_engines\n", + "\n", + "app = reasoning_engines.AdkApp(agent=eco_orchestrator)\n", + "\n", + "complex_query = \"\"\"\n", + "I am in Mumbai. I want to take a train to the Aravalli Biodiversity Park.\n", + "Tell me the train route, how much CO2 I save vs flying, what the weather is there right now,\n", + "and convert my $800 USD budget into whatever currency they use in that country based on live rates.\n", + "Also, please translate the phrase 'Two train tickets to New Delhi, please' into Hindi, Korean, and Chinese.\n", + "\"\"\"\n", + "\n", + "print(f\"💬 Incoming Multi-Step Inquiry:\\n{complex_query}\")\n", + "print(\"=\" * 60)\n", + "\n", + "if \"your-password\" not in NEO4J_PASSWORD:\n", + " for event in app.stream_query(user_id=\"eco_traveler_1\", message=complex_query):\n", + " if 'content' in event and 'parts' in event['content']:\n", + " part = event['content']['parts'][0]\n", + " if 'text' in part:\n", + " print(part['text'], end=\"\")\n", + " elif 'function_call' in part:\n", + " print(f\"\\n[⚙ SWARM TRIGGER -> Executing Real API: {part['function_call']['name']}]\", end=\"\\n\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "Deploy_01" + }, + "source": [ + "### 6. Production Deployment to Vertex AI\n", + "We package the entire swarm, including the `requests`, `deep-translator`, and `duckduckgo-search` dependencies, and deploy it to Google Cloud." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "DeployCode_01", + "outputId": "abb1ebe7-d579-45d1-8f5c-1d09ec5e4542" + }, + "outputs": [], + "source": [ + "from vertexai.preview import reasoning_engines\n", + "\n", + "STAGING_BUCKET = f\"gs://{PROJECT_ID}-vertex-staging\"\n", + "vertexai.init(project=PROJECT_ID, location=REGION, staging_bucket=STAGING_BUCKET)\n", + "\n", + "class RealDataSwarmWrapper:\n", + " def __init__(self, app, project, location):\n", + " self.app = app\n", + " self.project = project\n", + " self.location = location\n", + "\n", + " def stream_query(self, **kwargs):\n", + " import vertexai\n", + " vertexai.init(project=self.project, location=self.location)\n", + " return self.app.stream_query(**kwargs)\n", + "\n", + "try:\n", + " remote_swarm = reasoning_engines.ReasoningEngine.create(\n", + " RealDataSwarmWrapper(reasoning_engines.AdkApp(agent=eco_orchestrator), PROJECT_ID, REGION),\n", + " requirements=[\n", + " \"google-adk>=1.0.0\",\n", + " \"langchain-community\",\n", + " \"langchain-google-vertexai\",\n", + " \"neo4j\",\n", + " \"google-cloud-aiplatform\",\n", + " \"duckduckgo-search\",\n", + " \"requests\", # Vital for the live REST APIs\n", + " \"deep-translator\" # Vital for Multilingual functionality\n", + " ],\n", + " )\n", + " print(f\"🎯 Live Swarm Deployed! Endpoint: {remote_swarm.resource_name}\")\n", + "except Exception as e:\n", + " print(f\"Deployment skipped: {e}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "Vis_01" + }, + "source": [ + "### 7. Interactive Topology Visualization\n", + "We render the Neo4j graph using `pyvis`, showing the train connections and Eco-Ratings." + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 651 + }, + "id": "VisCode_01", + "outputId": "870fd77f-6a78-42e6-e525-a88872db34eb" + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "eco_nomad_web.html\n" + ] + }, + { + "data": { + "text/html": [ + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
    \n", + "

    \n", + "
    \n", + "\n", + "\n", + " \n", + " \n", + "\n", + "\n", + "
    \n", + "

    \n", + "
    \n", + " \n", + " \n", + "\n", + "\n", + " \n", + "
    \n", + " \n", + " \n", + "
    \n", + "
    \n", + "\n", + " \n", + " \n", + "\n", + " \n", + " \n", + "" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "from pyvis.network import Network\n", + "from IPython.display import HTML, display\n", + "from neo4j import GraphDatabase\n", + "\n", + "def visualize_eco_web():\n", + " driver = GraphDatabase.driver(NEO4J_URI, auth=(NEO4J_USER, NEO4J_PASSWORD))\n", + " net = Network(notebook=True, cdn_resources='in_line', height=\"600px\", width=\"100%\", bgcolor=\"#1a2621\", font_color=\"white\")\n", + " cypher = \"MATCH (n)-[r]-(m) RETURN n, r, m\"\n", + "\n", + " with driver.session() as session:\n", + " results = session.run(cypher)\n", + " for record in results:\n", + " n, m, r = record['n'], record['m'], record['r']\n", + "\n", + " n_color = '#2ecc71' if n.get('eco_rating') else '#34495e'\n", + " m_color = '#2ecc71' if m.get('eco_rating') else '#34495e'\n", + "\n", + " net.add_node(n.element_id, label=n.get('name'), color=n_color)\n", + " net.add_node(m.element_id, label=m.get('name'), color=m_color)\n", + " net.add_edge(n.element_id, m.element_id, title=f\"{r.get('train_type')} ({r.get('co2_saved_vs_flying_kg')}kg CO2 Saved)\", label=r.type, color='#f1c40f')\n", + "\n", + " driver.close()\n", + " net.show('eco_nomad_web.html')\n", + " return HTML('eco_nomad_web.html')\n", + "\n", + "if \"your-password\" not in NEO4J_PASSWORD:\n", + " display(visualize_eco_web())" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "Conclusion_01" + }, + "source": [ + "### 🏁 The True Gold Standard: Zero-Mock Agentic Chaining\n", + "\n", + "We have achieved the ultimate level of agentic complexity:\n", + "* **No Simulated Data:** Every tool hits a live database or a real-world, open-source REST endpoint.\n", + "* **Logical Tool Chaining:** The Swarm successfully extracts data from Tool 1 (GraphRAG Country), feeds it to Tool 2 (RESTCountries Currency), and feeds *that* to Tool 3 (Frankfurter Forex).\n", + "* **Live Telemetry & Language:** Integrates real-time weather, open-web search, and multilingual free translation natively into the conversational response.\n", + "\n", + "Your **Eco-Nomad Swarm** is alive and ready to orchestrate real-world travel! 🌍🚄" + ] + } + ], + "metadata": { + "colab": { + "provenance": [] + }, + "kernelspec": { + "display_name": "Python 3", + "name": "python3" + }, + "language_info": { + "name": "python" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} diff --git a/gemini/use-cases/healthcare/react_gemini_healthcare_api.ipynb b/gemini/use-cases/healthcare/react_gemini_healthcare_api.ipynb new file mode 100644 index 0000000..b0e53d6 --- /dev/null +++ b/gemini/use-cases/healthcare/react_gemini_healthcare_api.ipynb @@ -0,0 +1,740 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "oofwDwjhdSAg" + }, + "outputs": [], + "source": [ + "# Copyright 2024 Google LLC\n", + "#\n", + "# Licensed under the Apache License, Version 2.0 (the \"License\");\n", + "# you may not use this file except in compliance with the License.\n", + "# You may obtain a copy of the License at\n", + "#\n", + "# https://www.apache.org/licenses/LICENSE-2.0\n", + "#\n", + "# Unless required by applicable law or agreed to in writing, software\n", + "# distributed under the License is distributed on an \"AS IS\" BASIS,\n", + "# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n", + "# See the License for the specific language governing permissions and\n", + "# limitations under the License." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "5D6p2h6hc-GM" + }, + "source": [ + "# ReAct (Reasoning + Acting) + Custom tool for Healthcare NL API + Gemini 2.0 + LangChain" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "hTjlKBVjdXSu" + }, + "source": [ + "\n", + " \n", + " \n", + " \n", + " \n", + "
    \n", + " \n", + " \"Google
    Run in Colab\n", + "
    \n", + "
    \n", + " \n", + " \"Google
    Run in Colab Enterprise\n", + "
    \n", + "
    \n", + " \n", + " \"Vertex
    Open in Vertex AI Workbench\n", + "
    \n", + "
    \n", + " \n", + " \"GitHub
    View on GitHub\n", + "
    \n", + "
    \n", + "\n", + "
    \n", + "\n", + "Share to:\n", + "\n", + "\n", + " \"LinkedIn\n", + "\n", + "\n", + "\n", + " \"Bluesky\n", + "\n", + "\n", + "\n", + " \"X\n", + "\n", + "\n", + "\n", + " \"Reddit\n", + "\n", + "\n", + "\n", + " \"Facebook\n", + " " + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "_t3GIIHmdajp" + }, + "source": [ + "| | |\n", + "|-|-|\n", + "|Author(s) | [Shade EL-Hadik](https://github.com/elhadik) |\n", + "|Last updated | 29 Jul 2024 |" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "uYjDcU6kaMCO" + }, + "source": [ + "## Overview\n", + "\n", + "Medical coding errors are common and can lead to denied claims, delayed payments, and even patient safety issues. Gen AI can help address these issues by suggesting accurate medical codes, automating tasks, and improving standardization. With a human-in-the-loop approach, this can lead to faster claims processing, better reimbursement, and reduced coder burnout.\n", + "\n", + "Google's large language models (LLMs) have the potential to significantly automate the medical coding process, improving both efficiency and accuracy. By first preprocessing medical records using Healthcare APIs to identify and extract relevant terms, patterns, and relationships, we can then leverage LLMs to suggest appropriate codes for diagnoses and procedures based on physician notes and reports. This will not only save coders time and effort but also reduce errors and inconsistencies, leading to improved reimbursement rates and better financial management for healthcare providers.\n", + "\n", + "**It is always recommended to have a human-in-the-loop when dealing with medical applications. This Colab is meant to accelerate the medical coding processing and not to fully replace a human coder.**" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "Ubh0oZviaMCO" + }, + "source": [ + "### Intro to Medical Codes\n", + "\n", + "Medical coding is the transformation of healthcare diagnoses, procedures, medical services, and equipment into standardized alphanumeric codes. These codes represent specific clinical information in a standardized format, facilitating communication between healthcare providers, payers, and researchers. Medical codes provide data points about diagnoses, procedures, and resource utilization.\n", + "\n", + "\n", + "\n", + "* **ICD-10 Codes**: International Classification of Diseases, 10th Revision. These are Diagnosis Codes for Conditions Such as Diabetes or Pneumonia. They Are Also the Codes for Symptoms Such as Headache or Chest Pain.\n", + "\n", + "* **CPT Codes**: Current Procedural Terminology. These are Procedures Codes that Describe What is Done by the Healthcare Provider, Hospital, etc., such as MRI, Gall Bladder Surgery or Complete Blood Count Lab Test. CPT Codes are Also Used for Doctor's Office Visits... These CPT Codes are Referred to as E/M Codes for Evaluation and Management Codes.\n", + "\n", + "* **DRG Codes**: Diagnosis Related Groups. DRG Codes are Used for Inpatient Medical Services. A Hospitalized Patient May Have Multiple ICD-10 Diagnosis Codes and Multiple CPT Codes and these will be 'Rolled Into' One DRG Diagnosis Related Group for that Hospital Stay.\n", + "\n", + "* **HCPCS Codes**: Healthcare Common Procedure Coding System. HCPCS Codes are Also Procedures Codes, but They Are for Procedures that Are Not a Part of the CPT Coding System. Many Special Medications that are Administered in the Hospital are Coded with a HCPCS Code Because a CPT Code for Them Does Not Exist." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "iKErD909aMCO" + }, + "source": [ + "### Architecture Diagram\n", + "\n", + "![screenshot-2024-01-15-at-12136-pm.png](https://storage.googleapis.com/github-repo/generative-ai/gemini/use-cases/healthcare/react_gemini_healthcare_api/screenshot-2024-01-15-at-12136-pm.png)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "56ZazrrbeFat" + }, + "source": [ + "## Getting Started" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "4LpzkDU5aMCR" + }, + "source": [ + "### Install libraries" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "Y39xzdr9a1iG" + }, + "outputs": [], + "source": [ + "# Tested with these package versions.\n", + "%pip install --upgrade google-cloud-aiplatform langchain==0.1.16 langchain-google-vertexai" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "WbdhJW4xaMCR" + }, + "source": [ + "### Restart current runtime\n", + "\n", + "To use the newly installed packages in this Jupyter runtime, you must restart the runtime. You can do this by running the cell below, which will restart the current kernel." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "8-JPNQEYaMCR" + }, + "outputs": [], + "source": [ + "# Restart kernel after installs so that your environment can access the new packages\n", + "import IPython\n", + "\n", + "app = IPython.Application.instance()\n", + "app.kernel.do_shutdown(True)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "C_x9Me45aMCR" + }, + "source": [ + "
    \n", + "⚠ The kernel is going to restart. Please wait until it is finished before continuing to the next step. ⚠\n", + "
    \n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "HJRzS-N7aMCS" + }, + "source": [ + "### Define Google Cloud project information" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "OPQc9VAXaOpc" + }, + "outputs": [], + "source": [ + "PROJECT_ID = \"YOUR_PROJECT_ID\" # @param {type:\"string\"}\n", + "LOCATION = \"us-central1\" # @param {type:\"string\"}" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "O_omzG97c-Ed" + }, + "source": [ + "### Enable APIs in Google Cloud project" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "lN2xA2OZc7ZC" + }, + "outputs": [], + "source": [ + "!gcloud services enable aiplatform.googleapis.com healthcare.googleapis.com --project $PROJECT_ID" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "lN2xA2OZc7ZC" + }, + "outputs": [], + "source": [ + "!gcloud config set healthcare/location $LOCATION" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "fzSnVXbEaMCS" + }, + "source": [ + "### Authenticate your notebook environment (Colab only)\n", + "\n", + "If you are running this notebook on Google Colab, run the following cell to authenticate your environment. This step is not required if you are using [Vertex AI Workbench](https://cloud.google.com/vertex-ai-workbench).\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "Ej9TaRzUZ8W8" + }, + "outputs": [], + "source": [ + "import sys\n", + "\n", + "# Additional authentication is required for Google Colab\n", + "if \"google.colab\" in sys.modules:\n", + " from google.colab import auth\n", + "\n", + " # Authenticate user to Google Cloud\n", + " auth.authenticate_user()\n", + "\n", + "import vertexai\n", + "\n", + "# Initialize Vertex AI\n", + "vertexai.init(project=PROJECT_ID, location=LOCATION)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "x5LAR1M0aMCS" + }, + "source": [ + "### Import libraries\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "F948zwebNJbR" + }, + "outputs": [], + "source": [ + "import json\n", + "\n", + "import google.auth\n", + "import google.auth.transport.requests\n", + "from langchain.agents import AgentType, initialize_agent\n", + "from langchain.tools import StructuredTool\n", + "from langchain_google_vertexai import VertexAI\n", + "import matplotlib.pyplot as plt\n", + "import networkx as nx\n", + "import pandas as pd\n", + "import requests\n", + "\n", + "# get access token\n", + "\n", + "creds, project = google.auth.default()\n", + "auth_req = google.auth.transport.requests.Request()\n", + "creds.refresh(auth_req)\n", + "creds.token" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "rFW_j3wbefsR" + }, + "source": [ + "## Configure and call Healthcare API\n", + "\n", + "* The [Cloud Healthcare API](https://cloud.google.com/healthcare-api?hl=en) provides industry-standard protocols and formats for ingesting, storing, analyzing, and integrating healthcare data with cloud-based applications. In the section below, we call the Google Healthcare API to preprocess medical records, in this case, a discharge report. Medical records can be unstructured text and are usually hard to read. The Healthcare API can help with this problem by extracting all medical terms from the unstructured text, identifying patterns, and establishing medical relationships." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "Oe3xzsvzc_ZD" + }, + "outputs": [], + "source": [ + "# @title Enter the Discharge Report Summary\n", + "\n", + "TEXT = \"Most Responsible Diagnosis: COPD Exacerbation Active Issues Managed in Hospital: Pulmonary edema Microcytic anemia Gout Purpuric rash NYD Course in Hospital: Mr. Johnson arrived in the ER from nursing home with a three-day history of worsening shortness of breath, yellow-green sputum, and increased sputum production. He was subsequently diagnosed with a COPD exacerbation and was satting at 84% on 4L O2 by nasal prongs. He was stepped up to BiPAP for 24 hours and prednisone, ciprofloxacin, and around the clock puffers were initiated. By day 2 of admission he was stepped down to oxygen by nasal prongs and QID puffers. In terms of respiratory complications, Mr. Johnson had a sudden hypoxic resp failure on day 3 of admission. CCOT was involved, but ICU was avoided. He was found to be in pulmonary edema that responded to diuresis. Last documented echo was completed 15 years ago and a repeat echo was ordered to be completed as an outpatient. Unfortunately on day 4 of admission Mr. Johnson also developed gout in the left MTP. This limited his mobility and contributed to deconditioning for which PT was involved. Also, by day 6 of admission a purpuric rash was noted on the upper and lower extremities, cause was unknown and punch biopsy was performed. The results are still pending. Lastly, upon admission Mr. Johnson was found to have a microcytic anemia. On history Mr. Johnson states he no longer eats much red meat or leafy greens, preferring tea and toast for most of his meals. There was no history of bleeding and previous FOBT testing was normal. Further testing revealed iron deficiency anemia and therapy with ferrous fumarate was initiated. On day of discharge, Ms. Johnson was on room air but continued to be on Lasix. Continued Home Medications: Albuterol 2 puffs q 4-6 hours daily Atrovent 2 puffs q 6h ASA 325 mg daily Metoprolol 25 mg BID Atorvastatin 40 mg daily Ramipril 10 mg daily Amlodipine 5 mg daily Metformin 1000 mg BID Terazosin 5 mg qhs Tylenol 325 mg qhs Lactulose 15cc qhs prn Citalopram 20 mg daily Multivitamin Medications Changes: Ramipril was STOPPED Lasix was STARTED at 20mg PO BID Amlodipine was INCREASED to 10mg daily Ferrous fumarate 325 mg QHS was STARTED Important Test Results: CXR completed April 20th 2019 revealed pulmonary edema and enlarged cardiac silhouette Sputum culture collected April 18th 2019 was positive for pseudomonas aeruginosa Pending Tests or Results: Echo ordered as outpatient Skin biopsy results pending Follow-up Plans: We asked the patient to make an appointment with their family MD next week. The patient will follow up in urgent resp clinic in 2-4 weeks time. Since moving to London the patient is not currently followed by a respirologist and since this is the third exacerbation this year a goals of care discussion may be warranted. The patient was also seen by our COPD Navigator Team and arrangements have been made to be seen as an outpatient.\" # @param{type:\"string\"}" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "INDqdyV_yM-g" + }, + "source": [ + "![screenshot-2024-01-11-at-14805-pm.png](https://storage.googleapis.com/github-repo/generative-ai/gemini/use-cases/healthcare/react_gemini_healthcare_api/screenshot-2024-01-11-at-14805-pm.png)\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "GxQapQpKzfPV" + }, + "source": [ + "In the below section, we use [`networkx`](https://networkx.org/), a Python Knowledge Graph libary, to visualize the relationships among medical terms that were captured by the Healthcare API. This is to illustrate the relationships that Healthcare API draws among the different medical terms, such as the relation between a medication and the recommend dose frequency, or the relation between a diagnosis and its severity" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "KatMEh0Mdq4n" + }, + "outputs": [], + "source": [ + "# @title Visualize Healthcare NLP\n", + "\n", + "\n", + "def healthcare_nl(text: str) -> str:\n", + " \"\"\"Sends a REST request to the Healthcare Natural Language API\"\"\"\n", + " # Set the API endpoint\n", + " url = f\"https://healthcare.googleapis.com/v1/projects/{PROJECT_ID}/locations/{LOCATION}/services/nlp:analyzeEntities\"\n", + "\n", + " # Set the request headers\n", + " headers = {\n", + " \"Authorization\": f\"Bearer {creds.token}\",\n", + " \"X-Goog-User-Project\": PROJECT_ID,\n", + " \"Content-Type\": \"application/json\",\n", + " \"Accept\": \"application/json\",\n", + " }\n", + "\n", + " # Set the request data\n", + " data = {\"documentContent\": text, \"licensedVocabularies\": [\"SNOMEDCT_US\", \"ICD10CM\"]}\n", + "\n", + " # Make the API request\n", + " response = requests.post(url, headers=headers, data=json.dumps(data))\n", + "\n", + " return response.json()\n", + "\n", + "\n", + "response_json = healthcare_nl(TEXT)\n", + "\n", + "# Create DataFrame for entities\n", + "df = pd.DataFrame.from_records(\n", + " [\n", + " {\n", + " \"ID\": entity[\"mentionId\"],\n", + " \"Desc\": f\"{entity['type']} : {entity['text']['content']}\",\n", + " \"Type\": entity[\"type\"],\n", + " \"Content\": entity[\"text\"][\"content\"],\n", + " }\n", + " for entity in response_json[\"entityMentions\"]\n", + " ]\n", + ")\n", + "\n", + "\n", + "# Create DataFrame for knowledge graph\n", + "kg_df = pd.DataFrame.from_records(\n", + " [\n", + " {\n", + " \"source\": df.loc[df[\"ID\"] == relation[\"subjectId\"], \"Desc\"].values[0],\n", + " \"target\": df.loc[df[\"ID\"] == relation[\"objectId\"], \"Content\"].values[0],\n", + " \"edge\": df.loc[df[\"ID\"] == relation[\"objectId\"], \"Type\"].values[0],\n", + " }\n", + " for relation in response_json[\"relationships\"]\n", + " ]\n", + ")\n", + "\n", + "\n", + "# Create a directed graph from the DataFrame\n", + "G = nx.from_pandas_edgelist(\n", + " kg_df, \"source\", \"target\", edge_attr=True, create_using=nx.MultiDiGraph()\n", + ")\n", + "\n", + "plt.figure(figsize=(15, 15))\n", + "\n", + "\n", + "# draw the graph\n", + "nx.draw(\n", + " G,\n", + " with_labels=True,\n", + " pos=nx.kamada_kawai_layout(G),\n", + " edge_color=\"silver\",\n", + " node_color=\"silver\",\n", + ")\n", + "nx.draw_networkx_edge_labels(G, pos=nx.kamada_kawai_layout(G))\n", + "\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "fq0EID9v93YI" + }, + "source": [ + "## Use ReAct agent and LangChain custom tools \n", + "\n", + "The section below uses LangChain [ReAct agent](https://python.langchain.com/docs/modules/agents/agent_types/react) (reasoning + actions), which combines chain of thought and tool usage together to reason through comlex tasks by interacting with external systems, in this case external system is the Healthcare API.\n", + "\n", + "ReAct-style prompting is currently (Fall 2023) the state-of-the-art for most prompt-driven LLM tasks. When you use plugins or extensions, where an LLM or LLM-based chatbot or system interacts with an external system, you are using a ReAct-style system. In general, any LLM system that reflects up-to-date knowledge is invisibly using ReAct-style functionality under-the-hood.\n", + "\n", + "ReAct chains typically have three interleaved parts:\n", + "\n", + "- **Thoughts**: Like in chain of thought, these are waypoints, plans, reasoning, etc. generated by the LLM as it makes progress towards the final output.\n", + "- **Actions**: LLM-generated commands, calls, or instructions to access an external system. The external system may be a tool that provides information, but can also be more general (i.e., the action observes or changes the state of an external system).\n", + "- **Observations**: A response, feedback, result, etc. from the external system, inserted into an LLM call to generate the next thought.\n", + "\n", + "These three steps are repeated until the LLM completes its task.\n", + "\n", + "**We are using LangChain ReAct as a quick means for prototyping, but you may also consider other prompting techniques like chain of thought or calling the LLM directly**" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "12s8PGCJ2Sad" + }, + "outputs": [], + "source": [ + "# @title Define the prompt\n", + "\n", + "prompt = \"\"\"\n", + "\n", + "pretend that you are a professional medical coder\n", + "First: Pass on the entire text to the list_of_medical_terms tool to find the correct medical terms in the discharge report\n", + "Second: Find the associated medical codes and their short explanation, for these medical terms that you just retrieved, and use only the below codes:\n", + " 1- ICD-10 : International Classification of Diseases, 10th Revision,\n", + " 2- CPT Codes: Current Procedural Terminology,\n", + " 3- DRG Codes: Diagnosis Related Groups.\n", + " 4- HCPCS Codes: Healthcare Common Procedure Coding System.\n", + "Third: Format the output as a JSON object with the following keys:\n", + " Code_Type\n", + " Code_Value\n", + " Code_Explanation\n", + "\n", + "\n", + "\n", + "\n", + " {discharge_report}\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "use the below as an example of the output:\n", + "\n", + " {example}\n", + "\n", + "\n", + "\"\"\"\n", + "\n", + "example_output = \"\"\"\n", + "\n", + "{'medical_terms': [{'Code_Type': 'ICD-10',\n", + " 'Code_Value': 'J44.9',\n", + " 'Code_Explanation': 'COPD with exacerbation'},\n", + " {'Code_Type': 'ICD-10',\n", + " 'Code_Value': 'J96.91',\n", + " 'Code_Explanation': 'Respiratory failure, unspecified'},\n", + " {'Code_Type': 'ICD-10',\n", + " 'Code_Value': 'J81.0',\n", + " 'Code_Explanation': 'Pulmonary edema'},\n", + " {'Code_Type': 'ICD-10',\n", + " 'Code_Value': 'D63.81',\n", + " 'Code_Explanation': 'Microcytic anemia'},\n", + " {'Code_Type': 'ICD-10',\n", + " 'Code_Value': 'M10.9',\n", + " 'Code_Explanation': 'Gout unspecified'},\n", + " {'Code_Type': 'ICD-10',\n", + " 'Code_Value': 'L98.9',\n", + " 'Code_Explanation': 'Purpura, unspecified'},\n", + " {'Code_Type': 'ICD-10',\n", + " 'Code_Value': 'B01.2',\n", + " 'Code_Explanation': 'Pseudomonas aeruginosa infection'},\n", + " {'Code_Type': 'ICD-10',\n", + " 'Code_Value': 'J44.1',\n", + " 'Code_Explanation': 'Chronic obstructive pulmonary disease'},\n", + " {'Code_Type': 'CPT',\n", + " 'Code_Value': '0005T',\n", + " 'Code_Explanation': 'Chest radiography, single view, frontal'},\n", + " {'Code_Type': 'CPT',\n", + " 'Code_Value': '95025',\n", + " 'Code_Explanation': 'Echocardiography, transthoracic'},\n", + " {'Code_Type': 'CPT',\n", + " 'Code_Value': '87650',\n", + " 'Code_Explanation': 'Skin biopsy, punch'},\n", + " {'Code_Type': 'CPT',\n", + " 'Code_Value': '87209',\n", + " 'Code_Explanation': 'Sputum culture, routine'},\n", + " {'Code_Type': 'CPT',\n", + " 'Code_Value': '99223-99238',\n", + " 'Code_Explanation': 'Office or other outpatient visit for the evaluation and management of an established patient...'},\n", + " {'Code_Type': 'DRG',\n", + " 'Code_Value': '896',\n", + " 'Code_Explanation': 'Cardiac dysrhythmias'},\n", + " {'Code_Type': 'DRG', 'Code_Value': '483', 'Code_Explanation': 'Pneumonia'},\n", + " {'Code_Type': 'DRG',\n", + " 'Code_Value': '963',\n", + " 'Code_Explanation': 'COPD and related conditions'},\n", + " {'Code_Type': 'HCPCS',\n", + " 'Code_Value': 'J7310',\n", + " 'Code_Explanation': 'Albuterol sulfate inhalation solution'},\n", + " {'Code_Type': 'HCPCS',\n", + " 'Code_Value': 'J3301',\n", + " 'Code_Explanation': 'Ipratropium bromide inhalation solution'},\n", + " {'Code_Type': 'HCPCS',\n", + " 'Code_Value': 'Q4016',\n", + " 'Code_Explanation': 'Metformin hydrochloride tablet'},\n", + " {'Code_Type': 'HCPCS',\n", + " 'Code_Value': 'J1800',\n", + " 'Code_Explanation': 'Metoprolol tartrate tablet'},\n", + " {'Code_Type': 'HCPCS',\n", + " 'Code_Value': 'J0630',\n", + " 'Code_Explanation': 'Amlodipine besylate tablet'},\n", + " {'Code_Type': 'HCPCS',\n", + " 'Code_Value': 'J1349',\n", + " 'Code_Explanation': 'Atorvastatin calcium tablet'},\n", + " {'Code_Type': 'HCPCS',\n", + " 'Code_Value': 'J7750',\n", + " 'Code_Explanation': 'Ramipril tablet'},\n", + " {'Code_Type': 'HCPCS',\n", + " 'Code_Value': 'J3105',\n", + " 'Code_Explanation': 'ASA enteric coated tablet'},\n", + " {'Code_Type': 'HCPCS',\n", + " 'Code_Value': 'J0305',\n", + " 'Code_Explanation': 'Citalopram hydrobromide tablet'},\n", + " {'Code_Type': 'HCPCS',\n", + " 'Code_Value': 'J9165',\n", + " 'Code_Explanation': 'Terazosin hydrochloride tablet'},\n", + " {'Code_Type': 'HCPCS',\n", + " 'Code_Value': 'Q6315',\n", + " 'Code_Explanation': 'Ferrous fumarate tablet'}]}\n", + " \"\"\"\n", + "\n", + "prompt = prompt.format(discharge_report=TEXT, example=example_output)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "_JBv1kCs-Syb" + }, + "outputs": [], + "source": [ + "# @title Call the LLM and pass on the Healthcare API as a custom tool and show reasoning steps\n", + "\n", + "\n", + "def list_of_medical_terms(text: str) -> list[str]:\n", + " \"\"\"Sends a REST request to the given Healthcare API to retrieve medical terms for diagnosis, procedures, medicines, and lab data\"\"\"\n", + " # Extract the entities from the response\n", + " response_json = healthcare_nl(text)\n", + "\n", + " type_categories = {\n", + " \"PROBLEM\",\n", + " \"MEDICINE\",\n", + " \"MEDICAL_DEVICE\",\n", + " \"PROCEDURE\",\n", + " \"LABORATORY_DATA\",\n", + " }\n", + "\n", + " # Remove duplicates\n", + " return list(\n", + " {\n", + " entity[\"text\"][\"content\"]\n", + " for entity in response_json[\"entityMentions\"]\n", + " if entity[\"type\"] in type_categories\n", + " }\n", + " )\n", + "\n", + "\n", + "# Create the custom tool that will call the external system to extract the medical terms from the patient records\n", + "tools = [StructuredTool.from_function(list_of_medical_terms)]\n", + "\n", + "# Create the LLM.\n", + "# This is the langchain connection to Vertex AI.\n", + "# Note this depends on vertexai.init\n", + "llm = VertexAI(model_name=\"gemini-2.0-flash\", max_output_tokens=8042, temperature=0)\n", + "\n", + "# Create the ReAct agent.\n", + "agent = initialize_agent(\n", + " tools,\n", + " llm,\n", + " verbose=True,\n", + " max_execution_time=1000,\n", + " max_iterations=3,\n", + " handle_parsing_errors=True,\n", + " agent=AgentType.STRUCTURED_CHAT_ZERO_SHOT_REACT_DESCRIPTION,\n", + ")\n", + "\n", + "# call the LLM\n", + "agent.run(prompt)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "phSMh0tf-8mH" + }, + "outputs": [], + "source": [ + "# @title Call the LLM and pass on the Healthcare API as a custom tool and DO NOT show reasoning steps\n", + "\n", + "agent = initialize_agent(\n", + " tools,\n", + " llm,\n", + " verbose=False, # Hides reasoning steps\n", + " max_execution_time=1000,\n", + " max_iterations=3,\n", + " handle_parsing_errors=True,\n", + " agent=AgentType.STRUCTURED_CHAT_ZERO_SHOT_REACT_DESCRIPTION,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "YOgVz321BeBn" + }, + "source": [ + "* Repeatedly querying a model with the same prompt and selecting the majority response as the final answer, a technique known as ***self-consistency***, demonstrably improves performance on arithmetic, common sense reasoning, and symbolic tasks." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "-wg2hJ1fgNHj" + }, + "outputs": [], + "source": [ + "outputs = []\n", + "vote = 0\n", + "final_output: str\n", + "num_of_attempts = 10\n", + "\n", + "# call the LLM\n", + "for _ in range(num_of_attempts):\n", + " tmp = agent.run(prompt)\n", + " outputs.append(tmp)\n", + " temp_vote = outputs.count(tmp)\n", + " if temp_vote > vote:\n", + " vote = temp_vote\n", + " final_output = tmp\n", + "\n", + "print(f\"vote count: {vote}\")\n", + "print(f\"final_output: {final_output}\")" + ] + } + ], + "metadata": { + "colab": { + "name": "react_gemini_healthcare_api.ipynb", + "toc_visible": true + }, + "kernelspec": { + "display_name": "Python 3", + "name": "python3" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} diff --git a/gemini/use-cases/intro_multimodal_use_cases.ipynb b/gemini/use-cases/intro_multimodal_use_cases.ipynb new file mode 100644 index 0000000..8f470e3 --- /dev/null +++ b/gemini/use-cases/intro_multimodal_use_cases.ipynb @@ -0,0 +1,1174 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "ijGzTHJJUCPY" + }, + "outputs": [], + "source": [ + "# Copyright 2023 Google LLC\n", + "#\n", + "# Licensed under the Apache License, Version 2.0 (the \"License\");\n", + "# you may not use this file except in compliance with the License.\n", + "# You may obtain a copy of the License at\n", + "#\n", + "# https://www.apache.org/licenses/LICENSE-2.0\n", + "#\n", + "# Unless required by applicable law or agreed to in writing, software\n", + "# distributed under the License is distributed on an \"AS IS\" BASIS,\n", + "# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n", + "# See the License for the specific language governing permissions and\n", + "# limitations under the License." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "VEqbX8OhE8y9" + }, + "source": [ + "# Gemini: An Overview of Multimodal Use Cases\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
    \n", + " \n", + " \"Google
    Run in Colab\n", + "
    \n", + "
    \n", + " \n", + " \"Google
    Run in Colab Enterprise\n", + "
    \n", + "
    \n", + " \n", + " \"Vertex
    Open in Vertex AI Workbench\n", + "
    \n", + "
    \n", + " \n", + " \"GitHub
    View on GitHub\n", + "
    \n", + "
    \n", + " \n", + " \"Google
    Open in Cloud Skills Boost\n", + "
    \n", + "
    \n", + "\n", + "
    \n", + "\n", + "Share to:\n", + "\n", + "\n", + " \"LinkedIn\n", + "\n", + "\n", + "\n", + " \"Bluesky\n", + "\n", + "\n", + "\n", + " \"X\n", + "\n", + "\n", + "\n", + " \"Reddit\n", + "\n", + "\n", + "\n", + " \"Facebook\n", + " \n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "8HKLOuOlJutv" + }, + "source": [ + "| Authors |\n", + "| --- |\n", + "| [Katie Nguyen](https://github.com/katiemn) |\n", + "| [Saeed Aghabozorgi](https://github.com/saeedaghabozorgi) |" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "VK1Q5ZYdVL4Y" + }, + "source": [ + "## Overview\n", + "\n", + "**YouTube Video: Multimodal AI in action**\n", + "\n", + "\n", + " \"Multimodal\n", + "\n", + "\n", + "In this notebook, you will explore a variety of different use cases enabled by multimodality with Gemini.\n", + "\n", + "Gemini is a family of generative AI models developed by [Google DeepMind](https://deepmind.google/) that is designed for multimodal use cases. [Gemini 3](https://docs.cloud.google.com/vertex-ai/generative-ai/docs/models/gemini/3-pro) is the latest model version.\n", + "\n", + "### Objectives\n", + "\n", + "This notebook demonstrates a variety of multimodal use cases with Gemini.\n", + "\n", + "In this tutorial, you will learn how to use Gemini with the Gen AI SDK for Python to:\n", + "\n", + " - Process and generate text\n", + " - Parse and summarize PDF documents\n", + " - Reason across multiple images\n", + " - Generating a video description\n", + " - Combining video data with external knowledge\n", + " - Understand Audio\n", + " - Analyze a code base\n", + " - Combine modalities\n", + " - Recommendation based on user preferences for e-commerce\n", + " - Understanding charts and diagrams\n", + " - Comparing images for similarities, anomalies, or differences" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "ZhsUe0fyc-ER" + }, + "source": [ + "### Costs\n", + "\n", + "This tutorial uses billable components of Google Cloud:\n", + "\n", + "- Vertex AI\n", + "\n", + "Learn about [Vertex AI pricing](https://cloud.google.com/vertex-ai/pricing) and use the [Pricing Calculator](https://cloud.google.com/products/calculator/) to generate a cost estimate based on your projected usage.\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "QDU0XJ1xRDlL" + }, + "source": [ + "## Getting Started\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "N5afkyDMSBW5" + }, + "source": [ + "### Install Google Gen AI SDK for Python" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "kc4WxYmLSBW5" + }, + "outputs": [], + "source": [ + "%pip install --upgrade --quiet google-genai gitingest" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "6Fom0ZkMSBW6" + }, + "source": [ + "### Authenticate your notebook environment (Colab only)\n", + "\n", + "If you are running this notebook on Google Colab, run the following cell to authenticate your environment. This step is not required if you are using [Vertex AI Workbench](https://cloud.google.com/vertex-ai-workbench).\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "LCaCx6PLSBW6" + }, + "outputs": [], + "source": [ + "import sys\n", + "\n", + "# Additional authentication is required for Google Colab\n", + "if \"google.colab\" in sys.modules:\n", + " # Authenticate user to Google Cloud\n", + " from google.colab import auth\n", + "\n", + " auth.authenticate_user()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "QGB8Txa_e4V0" + }, + "source": [ + "### Set Google Cloud project information and create client\n", + "\n", + "To get started using Vertex AI, you must have an existing Google Cloud project and [enable the Vertex AI API](https://console.cloud.google.com/flows/enableapi?apiid=aiplatform.googleapis.com).\n", + "\n", + "Learn more about [setting up a project and a development environment](https://cloud.google.com/vertex-ai/docs/start/cloud-environment)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "JGOJHtgDe5-r" + }, + "outputs": [], + "source": [ + "import os\n", + "\n", + "from google import genai\n", + "\n", + "# fmt: off\n", + "PROJECT_ID = \"[your-project-id]\" # @param {type: \"string\", placeholder: \"[your-project-id]\", isTemplate: true}\n", + "# fmt: on\n", + "if not PROJECT_ID or PROJECT_ID == \"[your-project-id]\":\n", + " PROJECT_ID = str(os.environ.get(\"GOOGLE_CLOUD_PROJECT\"))\n", + "\n", + "LOCATION = os.environ.get(\"GOOGLE_CLOUD_REGION\", \"global\")\n", + "\n", + "client = genai.Client(enterprise=True, project=PROJECT_ID, location=LOCATION)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "BuQwwRiniVFG" + }, + "source": [ + "### Import libraries\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "JTk488WDPBtQ" + }, + "outputs": [], + "source": [ + "import nest_asyncio\n", + "from IPython.display import Audio, Image, Markdown, Video, display\n", + "from gitingest import ingest\n", + "from google.genai.types import CreateCachedContentConfig, GenerateContentConfig, Part\n", + "\n", + "nest_asyncio.apply()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "eTNnM-lqfQRo" + }, + "source": [ + "### Load Gemini 2.5 Flash model\n", + "\n", + "Learn more about all [Gemini models on Vertex AI](https://docs.cloud.google.com/vertex-ai/generative-ai/docs/models)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "2998506fe6d1" + }, + "outputs": [], + "source": [ + "MODEL_ID = \"gemini-2.5-flash\" # @param {type: \"string\"}" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "22c7363baeb0" + }, + "source": [ + "## Individual Modalities" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "a052fcef47ea" + }, + "source": [ + "### Textual understanding\n", + "\n", + "Gemini can parse textual questions and retain that context across following prompts." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "577574234d7e" + }, + "outputs": [], + "source": [ + "question = \"What is the average weather in Mountain View, CA in the middle of May?\"\n", + "prompt = \"\"\"\n", + "Considering the weather, please provide some outfit suggestions.\n", + "\n", + "Give examples for the daytime and the evening.\n", + "\"\"\"\n", + "\n", + "contents = [question, prompt]\n", + "response = client.models.generate_content(model=MODEL_ID, contents=contents)\n", + "display(Markdown(response.text))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "7d5586dd92ed" + }, + "source": [ + "### Document Understanding (Question Answering & Summarization)\n", + "\n", + "You can use Gemini to process PDF documents, and analyze content, retain information, and provide answers to queries regarding the documents.\n", + "\n", + "The PDF document example used here is the [Gemini 2.5 paper](https://arxiv.org/pdf/2507.06261)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "d5af46d4da0c" + }, + "outputs": [], + "source": [ + "pdf_file = Part.from_uri(\n", + " file_uri=\"https://arxiv.org/pdf/2507.06261\", mime_type=\"application/pdf\"\n", + ")\n", + "\n", + "prompt = \"How many tokens can the model process?\"\n", + "\n", + "contents = [pdf_file, prompt]\n", + "\n", + "response = client.models.generate_content(model=MODEL_ID, contents=contents)\n", + "display(Markdown(response.text))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "25658ef8dcec" + }, + "outputs": [], + "source": [ + "prompt = \"\"\"\n", + " You are a professional document summarization specialist.\n", + " Please summarize the given document.\n", + "\"\"\"\n", + "\n", + "contents = [pdf_file, prompt]\n", + "\n", + "response = client.models.generate_content(model=MODEL_ID, contents=contents)\n", + "display(Markdown(response.text))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "5OWurhO4mu4J" + }, + "source": [ + "### Image understanding across multiple images\n", + "\n", + "One of the capabilities of Gemini is being able to reason across multiple images.\n", + "\n", + "This is an example of using Gemini to reason which glasses would be more suitable for an oval face shape." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "80048d6d0123" + }, + "outputs": [], + "source": [ + "image_glasses1_url = \"https://storage.googleapis.com/github-repo/img/gemini/multimodality_usecases_overview/glasses1.jpg\"\n", + "image_glasses2_url = \"https://storage.googleapis.com/github-repo/img/gemini/multimodality_usecases_overview/glasses2.jpg\"\n", + "\n", + "display(Image(image_glasses1_url, width=150))\n", + "display(Image(image_glasses2_url, width=150))\n", + "\n", + "prompt = \"\"\"\n", + "I have an oval face. Given my face shape, which glasses would be more suitable?\n", + "\n", + "Explain how you reached this decision.\n", + "Provide your recommendation based on my face shape, and please give an explanation for each.\n", + "\"\"\"\n", + "\n", + "contents = [\n", + " prompt,\n", + " Part.from_uri(file_uri=image_glasses1_url, mime_type=\"image/jpeg\"),\n", + " Part.from_uri(file_uri=image_glasses2_url, mime_type=\"image/jpeg\"),\n", + "]\n", + "response = client.models.generate_content(model=MODEL_ID, contents=contents)\n", + "display(Markdown(response.text))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "96b21923035e" + }, + "source": [ + "### Generating a video description\n", + "\n", + "Gemini can also extract tags throughout a video:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "3cef00d36cde" + }, + "outputs": [], + "source": [ + "video_url = \"https://storage.googleapis.com/github-repo/img/gemini/multimodality_usecases_overview/mediterraneansea.mp4\"\n", + "display(Video(video_url, width=350))\n", + "\n", + "prompt = \"\"\"\n", + "What is shown in this video?\n", + "Where should I go to see it?\n", + "What are the top 5 places in the world that look like this?\n", + "Provide the 10 best tags for this video?\n", + "\"\"\"\n", + "\n", + "video = Part.from_uri(\n", + " file_uri=video_url,\n", + " mime_type=\"video/mp4\",\n", + ")\n", + "contents = [prompt, video]\n", + "\n", + "response = client.models.generate_content(model=MODEL_ID, contents=contents)\n", + "display(Markdown(response.text))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "ca8100e88501" + }, + "source": [ + "> You can confirm that the location is indeed Antalya, Turkey by visiting the Wikipedia page: https://en.wikipedia.org/wiki/Antalya" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "2547a8887702" + }, + "source": [ + "You can also use Gemini to retrieve extra information beyond the video contents." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "c978cf9f8c71" + }, + "outputs": [], + "source": [ + "video_url = \"https://storage.googleapis.com/github-repo/img/gemini/multimodality_usecases_overview/ottawatrain3.mp4\"\n", + "display(Video(video_url, width=350))\n", + "\n", + "prompt = \"\"\"\n", + "Which train line is this?\n", + "Where does it go?\n", + "What are the stations/stops?\n", + "Which river is being crossed?\n", + "\"\"\"\n", + "\n", + "video = Part.from_uri(\n", + " file_uri=video_url,\n", + " mime_type=\"video/mp4\",\n", + ")\n", + "contents = [prompt, video]\n", + "\n", + "response = client.models.generate_content(\n", + " model=MODEL_ID, contents=contents, config=GenerateContentConfig(temperature=0)\n", + ")\n", + "display(Markdown(response.text))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "8a614b7f5284" + }, + "source": [ + "> You can confirm that this is indeed the Confederation Line on Wikipedia here: https://en.wikipedia.org/wiki/Confederation_Line" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "8fb9a30ff4c5" + }, + "source": [ + "### Audio understanding\n", + "\n", + "Gemini can directly process audio for long-context understanding." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "5424dbe4c7e1" + }, + "outputs": [], + "source": [ + "audio_url = (\n", + " \"https://storage.googleapis.com/cloud-samples-data/generative-ai/audio/pixel.mp3\"\n", + ")\n", + "display(Audio(audio_url))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "9ed4ea9d696f" + }, + "source": [ + "#### Summarization" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "c889e8db2aca" + }, + "outputs": [], + "source": [ + "prompt = \"\"\"\n", + " Please provide a short summary and title for the audio.\n", + " Provide chapter titles, be concise and short, no need to provide chapter summaries.\n", + " Provide each of the chapter titles in a numbered list.\n", + " Do not make up any information that is not part of the audio and do not be verbose.\n", + "\"\"\"\n", + "\n", + "audio_file = Part.from_uri(file_uri=audio_url, mime_type=\"audio/mpeg\")\n", + "contents = [audio_file, prompt]\n", + "\n", + "response = client.models.generate_content(model=MODEL_ID, contents=contents)\n", + "display(Markdown(response.text))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "e64eb3061613" + }, + "source": [ + "#### Transcription" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "4486a25573a0" + }, + "outputs": [], + "source": [ + "prompt = \"\"\"\n", + " Transcribe this interview, in the format of timecode, speaker, caption.\n", + " Use speaker A, speaker B, etc. to identify the speakers.\n", + " Provide each piece of information on a separate bullet point.\n", + "\"\"\"\n", + "\n", + "audio_file = Part.from_uri(file_uri=audio_url, mime_type=\"audio/mpeg\")\n", + "contents = [audio_file, prompt]\n", + "\n", + "response = client.models.generate_content(\n", + " model=MODEL_ID,\n", + " contents=contents,\n", + " config=GenerateContentConfig(max_output_tokens=8192, audio_timestamp=True),\n", + ")\n", + "display(Markdown(response.text))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "96c026a7f2ce" + }, + "source": [ + "### Reason across a codebase\n", + "\n", + "You will use the [Online Boutique repository](https://github.com/GoogleCloudPlatform/microservices-demo) as an example in this notebook. Online Boutique is a cloud-first microservices demo application. The application is a web-based e-commerce app where users can browse items, add them to the cart, and purchase them. This application consists of 11 microservices across multiple languages." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "ad22725252dc" + }, + "outputs": [], + "source": [ + "# The GitHub repository URL\n", + "# fmt: off\n", + "repo_url = \"https://github.com/GoogleCloudPlatform/microservices-demo\" # @param {type:\"string\"}\n", + "# fmt: on" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "1acc008e3d83" + }, + "source": [ + "#### Create an index and extract the contents of a codebase\n", + "\n", + "Clone the repo and create an index and extract content of code/text files." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "7f7e3cce09c7" + }, + "outputs": [], + "source": [ + "exclude_patterns = {\n", + " \"*.png\",\n", + " \"*.jpg\",\n", + " \"*.jpeg\",\n", + " \"*.gif\",\n", + " \"*.svg\",\n", + " \"*.ico\",\n", + " \"*.webp\",\n", + " \"*.jar\",\n", + " \".git/\",\n", + " \"*.gitkeep\",\n", + "}\n", + "_, code_index, code_text = ingest(repo_url, exclude_patterns=exclude_patterns)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "9572811ab5b2" + }, + "source": [ + "#### Create a content cache for the codebase\n", + "\n", + "The codebase prompt is going to be quite large with all of the included data.\n", + "Gemini supports [Context caching](https://cloud.google.com/vertex-ai/generative-ai/docs/context-cache/context-cache-overview), which lets you to store frequently used input tokens in a dedicated cache and reference them for subsequent requests, eliminating the need to repeatedly pass the same set of tokens to a model.\n", + "\n", + "**Note**: Context caching is available for the models stated in [Context caching](https://cloud.google.com/vertex-ai/generative-ai/docs/context-cache/context-cache-overview)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "daeff13cca4a" + }, + "outputs": [], + "source": [ + "prompt = f\"\"\"\n", + "Context:\n", + "- The entire codebase is provided below.\n", + "- Here is an index of all of the files in the codebase:\n", + " \\n\\n{code_index}\\n\\n.\n", + "- Then each of the files is concatenated together. You will find all of the code you need:\n", + " \\n\\n{code_text}\\n\\n\n", + "\"\"\"\n", + "\n", + "cached_content = client.caches.create(\n", + " model=MODEL_ID,\n", + " config=CreateCachedContentConfig(\n", + " contents=prompt,\n", + " ttl=\"3600s\",\n", + " ),\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "3d519f3b1763" + }, + "source": [ + "#### Create a developer getting started guide" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "fad43c9b32de" + }, + "outputs": [], + "source": [ + "question = \"\"\"\n", + " Provide a getting started guide to onboard new developers to the codebase.\n", + "\"\"\"\n", + "\n", + "response = client.models.generate_content(\n", + " model=MODEL_ID,\n", + " contents=question,\n", + " config=GenerateContentConfig(\n", + " cached_content=cached_content.name,\n", + " ),\n", + ")\n", + "display(Markdown(response.text))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "442e5eedc6dc" + }, + "source": [ + "#### Finding bugs in the code" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "95e1def33199" + }, + "outputs": [], + "source": [ + "question = \"\"\"\n", + " Find the top 3 most severe issues in the codebase.\n", + "\"\"\"\n", + "\n", + "response = client.models.generate_content(\n", + " model=MODEL_ID,\n", + " contents=question,\n", + " config=GenerateContentConfig(\n", + " cached_content=cached_content.name,\n", + " ),\n", + ")\n", + "display(Markdown(response.text))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "ae2123dfb85d" + }, + "source": [ + "#### Summarizing the codebase" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "807ab5b69ea8" + }, + "outputs": [], + "source": [ + "question = \"\"\"\n", + " Give me a summary of this codebase, and tell me the top 3 things that I can learn from it.\n", + "\"\"\"\n", + "\n", + "response = client.models.generate_content(\n", + " model=MODEL_ID,\n", + " contents=question,\n", + " config=GenerateContentConfig(\n", + " cached_content=cached_content.name,\n", + " ),\n", + ")\n", + "display(Markdown(response.text))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "06354c53e1a8" + }, + "source": [ + "## Combining multiple modalities" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "3c00e534189b" + }, + "source": [ + "### Video and audio understanding\n", + "\n", + "Try out Gemini's native multimodal and long-context capabilities on video interleaving with audio inputs." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "41b5bb1b04c2" + }, + "outputs": [], + "source": [ + "video_url = (\n", + " \"https://storage.googleapis.com/cloud-samples-data/generative-ai/video/pixel8.mp4\"\n", + ")\n", + "display(Video(video_url, width=350))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "a29e43974ca9" + }, + "outputs": [], + "source": [ + "prompt = \"\"\"\n", + " Provide a detailed description of the video.\n", + " The description should also contain any important dialogue from the video and key features of the phone.\n", + "\"\"\"\n", + "\n", + "video = Part.from_uri(\n", + " file_uri=video_url,\n", + " mime_type=\"video/mp4\",\n", + ")\n", + "contents = [prompt, video]\n", + "\n", + "response = client.models.generate_content(model=MODEL_ID, contents=contents)\n", + "display(Markdown(response.text))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "8b8eec259f62" + }, + "source": [ + "### All modalities (images, video, audio, text) at once\n", + "\n", + "Gemini is natively multimodal and supports interleaving of data from different modalities. It can support a mix of audio, visual, text, and code inputs in the same input sequence." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "ff9b20835ada" + }, + "outputs": [], + "source": [ + "video_url = \"gs://cloud-samples-data/generative-ai/video/behind_the_scenes_pixel.mp4\"\n", + "display(Video(video_url.replace(\"gs://\", \"https://storage.googleapis.com/\"), width=350))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "ecec6597c63e" + }, + "outputs": [], + "source": [ + "image_url = \"https://storage.googleapis.com/cloud-samples-data/generative-ai/image/a-man-and-a-dog.png\"\n", + "display(Image(image_url, width=350))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "a3f2ae2fb517" + }, + "outputs": [], + "source": [ + "prompt = \"\"\"\n", + " Look through each frame in the video carefully and answer the questions.\n", + " Only base your answers strictly on what information is available in the video attached.\n", + " Do not make up any information that is not part of the video and do not be too\n", + " verbose, be straightforward.\n", + "\n", + " Questions:\n", + " - When is the moment in the image happening in the video? Provide a timestamp.\n", + " - What is the context of the moment and what does the narrator say about it?\n", + "\"\"\"\n", + "\n", + "contents = [\n", + " prompt,\n", + " Part.from_uri(file_uri=video_url, mime_type=\"video/mp4\"),\n", + " Part.from_uri(file_uri=image_url, mime_type=\"image/png\"),\n", + "]\n", + "\n", + "response = client.models.generate_content(model=MODEL_ID, contents=contents)\n", + "display(Markdown(response.text))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "c4de7efa9637" + }, + "source": [ + "## Use Case: retail / e-commerce\n", + "\n", + "Suppose a customer shows you their living room and wants to find appropriate furniture and choose between four wall art options for the room.\n", + "\n", + "How can you use Gemini to help the customer choose the best option?" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "aaaac1880330" + }, + "source": [ + "### Generating open recommendations\n", + "\n", + "Using the same image, you can ask the model to recommend a piece of furniture that would make sense in the space.\n", + "\n", + "Note that the model can choose any furniture in this case, and can do so only from its built-in knowledge." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "60ca92c5776c" + }, + "outputs": [], + "source": [ + "room_image_url = \"https://storage.googleapis.com/cloud-samples-data/generative-ai/image/living-room.png\"\n", + "display(Image(room_image_url, width=350))\n", + "\n", + "room_image = Part.from_uri(file_uri=room_image_url, mime_type=\"image/png\")\n", + "\n", + "prompt = \"Describe this room\"\n", + "contents = [prompt, room_image]\n", + "\n", + "response = client.models.generate_content(model=MODEL_ID, contents=contents)\n", + "display(Markdown(response.text))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "e28323d5679a" + }, + "outputs": [], + "source": [ + "prompt1 = \"Recommend a new piece of furniture for this room\"\n", + "prompt2 = \"Explain the reason in detail\"\n", + "contents = [prompt1, room_image, prompt2]\n", + "\n", + "response = client.models.generate_content(model=MODEL_ID, contents=contents)\n", + "display(Markdown(response.text))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "0a4d4c38c8af" + }, + "source": [ + "### Generating recommendations based on provided images\n", + "\n", + "Instead of keeping the recommendation open, you can also provide a list of items for the model to choose from. Here, you will load a few art images that the Gemini model can recommend. This is particularly useful for retail companies who want to provide product recommendations to users based on their current setup." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "434725a7c58f" + }, + "outputs": [], + "source": [ + "art_image_urls = [\n", + " \"https://storage.googleapis.com/cloud-samples-data/generative-ai/image/room-art-1.png\",\n", + " \"https://storage.googleapis.com/cloud-samples-data/generative-ai/image/room-art-2.png\",\n", + " \"https://storage.googleapis.com/cloud-samples-data/generative-ai/image/room-art-3.png\",\n", + " \"https://storage.googleapis.com/cloud-samples-data/generative-ai/image/room-art-4.png\",\n", + "]\n", + "\n", + "md_content = f\"\"\"\n", + "|Customer photo |\n", + "|:-----:|\n", + "| |\n", + "\n", + "|Art 1| Art 2 | Art 3 | Art 4 |\n", + "|:-----:|:----:|:-----:|:----:|\n", + "| ||||\n", + "\"\"\"\n", + "\n", + "display(Markdown(md_content))\n", + "\n", + "# Load wall art images as Part objects\n", + "art_images = [\n", + " Part.from_uri(file_uri=url, mime_type=\"image/png\") for url in art_image_urls\n", + "]\n", + "\n", + "# To recommend an item from a selection, you will need to label the item number within the prompt.\n", + "# That way you are providing the model with a way to reference each image as you pose a question.\n", + "# Labeling images within your prompt also helps reduce hallucinations and produce better results.\n", + "prompt = \"\"\"\n", + " You are an interior designer.\n", + " For each piece of wall art, explain whether it would be appropriate for the style of the room.\n", + " Rank each piece according to how well it would be compatible in the room.\n", + "\"\"\"\n", + "\n", + "contents = [\n", + " \"Consider the following art pieces:\",\n", + " \"art 1:\",\n", + " art_images[0],\n", + " \"art 2:\",\n", + " art_images[1],\n", + " \"art 3:\",\n", + " art_images[2],\n", + " \"art 4:\",\n", + " art_images[3],\n", + " \"room:\",\n", + " room_image,\n", + " prompt,\n", + "]\n", + "\n", + "response = client.models.generate_content(model=MODEL_ID, contents=contents)\n", + "display(Markdown(response.text))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "4437b7608c8e" + }, + "source": [ + "## Use Case: Entity relationships in technical diagrams\n", + "\n", + "Gemini has multimodal capabilities that enable it to understand diagrams and take actionable steps, such as optimization or code generation. This example demonstrates how Gemini can decipher an entity relationship (ER) diagram, understand the relationships between tables, identify requirements for optimization in a specific environment like BigQuery, and even generate corresponding code." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "klY4yBEiKmET" + }, + "outputs": [], + "source": [ + "image_er_url = \"https://storage.googleapis.com/github-repo/img/gemini/multimodality_usecases_overview/er.png\"\n", + "display(Image(image_er_url, width=350))\n", + "\n", + "prompt = \"Document the entities and relationships in this ER diagram.\"\n", + "\n", + "contents = [prompt, Part.from_uri(file_uri=image_er_url, mime_type=\"image/png\")]\n", + "\n", + "response = client.models.generate_content(\n", + " model=MODEL_ID,\n", + " contents=contents,\n", + ")\n", + "display(Markdown(response.text))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "ZBrdsvIU7Zkf" + }, + "source": [ + "## Use Case: Similarity/Differences\n", + "\n", + "Gemini can compare images and identify similarities or differences between objects.\n", + "\n", + "The following example shows two scenes from [Marienplatz in Munich, Germany](https://en.wikipedia.org/wiki/Marienplatz) that are slightly different." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "JUSJduLh8457" + }, + "outputs": [], + "source": [ + "image_landmark1_url = \"https://storage.googleapis.com/github-repo/img/gemini/multimodality_usecases_overview/landmark1.jpg\"\n", + "image_landmark2_url = \"https://storage.googleapis.com/github-repo/img/gemini/multimodality_usecases_overview/landmark2.jpg\"\n", + "\n", + "md_content = f\"\"\"\n", + "| Image 1 | Image 2 |\n", + "|:-----:|:----:|\n", + "| | |\n", + "\"\"\"\n", + "\n", + "display(Markdown(md_content))\n", + "\n", + "prompt1 = \"\"\"\n", + "Consider the following two images:\n", + "Image 1:\n", + "\"\"\"\n", + "prompt2 = \"\"\"\n", + "Image 2:\n", + "\"\"\"\n", + "prompt3 = \"\"\"\n", + "1. What is shown in Image 1? Where is it?\n", + "2. What is similar between the two images?\n", + "3. What is difference between Image 1 and Image 2 in terms of the contents or people shown?\n", + "\"\"\"\n", + "\n", + "contents = [\n", + " prompt1,\n", + " Part.from_uri(file_uri=image_landmark1_url, mime_type=\"image/jpeg\"),\n", + " prompt2,\n", + " Part.from_uri(file_uri=image_landmark2_url, mime_type=\"image/jpeg\"),\n", + " prompt3,\n", + "]\n", + "\n", + "config = GenerateContentConfig(\n", + " temperature=0.0,\n", + " top_p=0.8,\n", + " top_k=40,\n", + " candidate_count=1,\n", + " max_output_tokens=2048,\n", + ")\n", + "\n", + "response = client.models.generate_content(\n", + " model=MODEL_ID,\n", + " contents=contents,\n", + " config=config,\n", + ")\n", + "display(Markdown(response.text))" + ] + } + ], + "metadata": { + "colab": { + "name": "intro_multimodal_use_cases.ipynb", + "toc_visible": true + }, + "kernelspec": { + "display_name": "Python 3", + "name": "python3" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} diff --git a/gemini/use-cases/knowledge-graph/knowledge_graph_generation.ipynb b/gemini/use-cases/knowledge-graph/knowledge_graph_generation.ipynb new file mode 100644 index 0000000..105b482 --- /dev/null +++ b/gemini/use-cases/knowledge-graph/knowledge_graph_generation.ipynb @@ -0,0 +1,3109 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "4KqMWyVlwu4L" + }, + "outputs": [], + "source": [ + "# Copyright 2026 Google LLC\n", + "#\n", + "# Licensed under the Apache License, Version 2.0 (the \"License\");\n", + "# you may not use this file except in compliance with the License.\n", + "# You may obtain a copy of the License at\n", + "#\n", + "# https://www.apache.org/licenses/LICENSE-2.0\n", + "#\n", + "# Unless required by applicable law or agreed to in writing, software\n", + "# distributed under the License is distributed on an \"AS IS\" BASIS,\n", + "# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n", + "# See the License for the specific language governing permissions and\n", + "# limitations under the License." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "8H5Di4ZXwu4M" + }, + "source": [ + "# Building Knowledge Graphs with Gemini\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
    \n", + " \n", + " \"Google
    Open in Colab\n", + "
    \n", + "
    \n", + " \n", + " \"Google
    Open in Colab Enterprise\n", + "
    \n", + "
    \n", + " \n", + " \"Workbench
    Open in Workbench\n", + "
    \n", + "
    \n", + " \n", + " \"GitHub
    View on GitHub\n", + "
    \n", + "
    \n", + "\n", + "
    \n", + "\n", + "

    \n", + "Share to:\n", + "\n", + "\n", + " \"LinkedIn\n", + "\n", + "\n", + "\n", + " \"Bluesky\n", + "\n", + "\n", + "\n", + " \"X\n", + "\n", + "\n", + "\n", + " \"Reddit\n", + "\n", + "\n", + "\n", + " \"Facebook\n", + "\n", + "

    \n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "m1Xixczzs9mQ" + }, + "source": [ + "| Author |\n", + "| ------------------------------------------------ |\n", + "| [Laurent Picard](https://github.com/PicardParis) |\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "Hsi0Sy73rYeH" + }, + "source": [ + "---\n", + "\n", + "## ✹ Overview\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "X5XQkkkrrYeH" + }, + "source": [ + "![intro image](https://storage.googleapis.com/github-repo/generative-ai/gemini/use-cases/knowledge-graph/kg_fr_dumas_comte_de_monte_cristo_3_flash.gif)\n", + "\n", + "In this exploration, we'll see how to turn raw, unstructured documents into structured knowledge graphs using Gemini. We'll start by prototyping to develop our intuition. Then, we'll optimize our prompts and outputs, and finally scale up to process entire books or dense legal contracts. By the end, we'll even visualize extracted book narratives and contractual network graphs!\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "om3Go4l7BQyf" + }, + "source": [ + "---\n", + "\n", + "## đŸ”„ Challenge\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "3zK85vQGBQyf" + }, + "source": [ + "Documents are everywhere. We use them for business, daily operations, legal matters, technical docs, education, and even just for fun. However, documents are not databases. They're generally unstructured, and fully understanding them requires multiple reading passes.\n", + "\n", + "So, can we extract structured knowledge from documents using only the following?\n", + "\n", + "- 1 document\n", + "- 1 prompt\n", + "- 1 request\n", + "\n", + "Let's try with Gemini
\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "_NlD5OhCwu4M" + }, + "source": [ + "---\n", + "\n", + "## 🏁 Setup\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "eHoOUP0JBQyg" + }, + "source": [ + "### 🐍 Python packages\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "xVaM54E5rYeJ" + }, + "source": [ + "We'll use the following packages:\n", + "\n", + "- `google-genai` for calling Gemini with the [Google Gen AI Python SDK](https://pypi.org/project/google-genai)\n", + "- `networkx` for graph management\n", + "\n", + "We'll also need:\n", + "\n", + "- `tenacity` for request management (a dependency of `google-genai`)\n", + "- `matplotlib` and `pillow` for data visualization (dependencies of `networkx`)\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "kZBN80r7qtgs" + }, + "outputs": [], + "source": [ + "%pip install --quiet \"google-genai>=2.6.0\" \"networkx[default]\"" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "51W6LrRJs9mT" + }, + "source": [ + "---\n", + "\n", + "### đŸ€ Gemini API\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "MDl51OblrYeJ" + }, + "source": [ + "To use the Gemini API, we have two main options:\n", + "\n", + "1. Via **Agent Platform** (formerly Vertex AI) with a Google Cloud project\n", + "2. Via **Google AI Studio** with a Gemini API key\n", + "\n", + "The Google Gen AI SDK provides a unified interface to these APIs and we can use environment variables for the configuration.\n", + "\n", + "**đŸ› ïž Option 1 - Gemini API via Agent Platform**\n", + "\n", + "Requirements:\n", + "\n", + "- A Google Cloud project\n", + "- The Agent Platform API must be enabled for this project: ▶ [Enable the Agent Platform API](https://console.cloud.google.com/flows/enableapi?apiid=aiplatform.googleapis.com,storage-component.googleapis.com)\n", + "\n", + "Gen AI SDK environment variables:\n", + "\n", + "- `GOOGLE_GENAI_USE_ENTERPRISE=\"True\"`\n", + "- `GOOGLE_CLOUD_PROJECT=\"\"`\n", + "- `GOOGLE_CLOUD_LOCATION=\"\"`\n", + "\n", + "> 💡 For preview models, the location must be set to `global`. For generally available models, we can choose the closest location among the [Google model endpoint locations](https://docs.cloud.google.com/vertex-ai/generative-ai/docs/learn/locations#google-models).\n", + "\n", + "> â„č Learn more about [setting up a project and a development environment](https://docs.cloud.google.com/vertex-ai/docs/start/cloud-environment).\n", + "\n", + "**đŸ› ïž Option 2 - Gemini API via Google AI Studio**\n", + "\n", + "Requirement:\n", + "\n", + "- A Gemini API key\n", + "\n", + "Gen AI SDK environment variables:\n", + "\n", + "- `GOOGLE_GENAI_USE_ENTERPRISE=\"False\"`\n", + "- `GOOGLE_API_KEY=\"\"`\n", + "\n", + "> â„č Learn more about [getting a Gemini API key from Google AI Studio](https://aistudio.google.com/app/apikey).\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "1bYRJlOXs9mT" + }, + "source": [ + "💡 You can store your environment configuration outside of the source code:\n", + "\n", + "| Environment | Method |\n", + "| ---------------- | ----------------------------------------------------------- |\n", + "| IDE | `.env` file (or equivalent) |\n", + "| Colab | Colab Secrets (đŸ—ïž icon in left panel, see code below) |\n", + "| Colab Enterprise | Google Cloud project and location are automatically defined |\n", + "| Workbench | Google Cloud project and location are automatically defined |\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "JcBky51us9mU" + }, + "source": [ + "Define the following environment detection functions. You can also define your configuration manually if needed.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "LAmlOAygs9mU" + }, + "outputs": [], + "source": [ + "# @title {display-mode: \"form\"}\n", + "import os\n", + "import sys\n", + "from collections.abc import Callable\n", + "\n", + "from google import genai\n", + "\n", + "# Manual setup (leave unchanged if setup is environment-defined)\n", + "\n", + "# @markdown **Which API: Agent Platform (formerly Vertex AI) or Google AI Studio?**\n", + "GOOGLE_GENAI_USE_ENTERPRISE = True # @param {type: \"boolean\"}\n", + "\n", + "# @markdown **Option A - Google Cloud project [+location]**\n", + "GOOGLE_CLOUD_PROJECT = \"\" # @param {type: \"string\"}\n", + "GOOGLE_CLOUD_LOCATION = \"global\" # @param {type: \"string\"}\n", + "\n", + "# @markdown **Option B - Google AI Studio API key**\n", + "GOOGLE_API_KEY = \"\" # @param {type: \"string\"}\n", + "\n", + "\n", + "def check_environment() -> bool:\n", + " check_colab_user_authentication()\n", + " return check_manual_setup() or check_enterprise() or check_colab() or check_local()\n", + "\n", + "\n", + "def check_manual_setup() -> bool:\n", + " return check_define_env_vars(\n", + " GOOGLE_GENAI_USE_ENTERPRISE,\n", + " GOOGLE_CLOUD_PROJECT.strip(), # Might have been pasted with a newline\n", + " GOOGLE_CLOUD_LOCATION,\n", + " GOOGLE_API_KEY,\n", + " )\n", + "\n", + "\n", + "def check_enterprise() -> bool:\n", + " # Workbench and Colab Enterprise\n", + " match os.getenv(\"VERTEX_PRODUCT\", \"\"):\n", + " case \"WORKBENCH_INSTANCE\":\n", + " pass\n", + " case \"COLAB_ENTERPRISE\":\n", + " if not running_in_colab_env():\n", + " return False\n", + " case _:\n", + " return False\n", + "\n", + " return check_define_env_vars(\n", + " True,\n", + " os.getenv(\"GOOGLE_CLOUD_PROJECT\", \"\"),\n", + " os.getenv(\"GOOGLE_CLOUD_REGION\", \"\"),\n", + " \"\",\n", + " )\n", + "\n", + "\n", + "def check_colab() -> bool:\n", + " if not running_in_colab_env():\n", + " return False\n", + "\n", + " # Colab Enterprise was checked before, so this is Colab only\n", + " from google.colab import auth as colab_auth # type: ignore\n", + "\n", + " colab_auth.authenticate_user()\n", + "\n", + " # Use Colab Secrets (đŸ—ïž icon in left panel) to store the environment variables\n", + " # Secrets are private, visible only to you and the notebooks that you select\n", + " # - Agent Platform: Store your settings as secrets\n", + " # - Google AI: Directly import your Gemini API key from the UI\n", + " enterprise, project, location, api_key = get_vars(get_colab_secret)\n", + "\n", + " return check_define_env_vars(enterprise, project, location, api_key)\n", + "\n", + "\n", + "def check_local() -> bool:\n", + " enterprise, project, location, api_key = get_vars(os.getenv)\n", + "\n", + " return check_define_env_vars(enterprise, project, location, api_key)\n", + "\n", + "\n", + "def running_in_colab_env() -> bool:\n", + " # Colab or Colab Enterprise\n", + " return \"google.colab\" in sys.modules\n", + "\n", + "\n", + "def check_colab_user_authentication() -> None:\n", + " if running_in_colab_env():\n", + " from google.colab import auth as colab_auth # type: ignore\n", + "\n", + " colab_auth.authenticate_user()\n", + "\n", + "\n", + "def get_colab_secret(secret_name: str, default: str) -> str:\n", + " from google.colab import errors, userdata # type: ignore\n", + "\n", + " try:\n", + " return userdata.get(secret_name)\n", + " except errors.SecretNotFoundError:\n", + " return default\n", + "\n", + "\n", + "def disable_colab_cell_scrollbar() -> None:\n", + " if running_in_colab_env():\n", + " from google.colab import output # type: ignore\n", + "\n", + " output.no_vertical_scroll()\n", + "\n", + "\n", + "def get_vars(getenv: Callable[[str, str], str]) -> tuple[bool, str, str, str]:\n", + " # Limit getenv calls to the minimum (may trigger UI confirmation for secret access)\n", + " enterprise_str = getenv(\"GOOGLE_GENAI_USE_ENTERPRISE\", \"\")\n", + " if not enterprise_str:\n", + " enterprise_str = getenv(\"GOOGLE_GENAI_USE_VERTEXAI\", \"\")\n", + " if enterprise_str:\n", + " enterprise = enterprise_str.lower() in [\"true\", \"1\"]\n", + " else:\n", + " enterprise = bool(getenv(\"GOOGLE_CLOUD_PROJECT\", \"\"))\n", + "\n", + " project = getenv(\"GOOGLE_CLOUD_PROJECT\", \"\") if enterprise else \"\"\n", + " location = getenv(\"GOOGLE_CLOUD_LOCATION\", \"\") if project else \"\"\n", + " api_key = getenv(\"GOOGLE_API_KEY\", \"\") if not project else \"\"\n", + "\n", + " return enterprise, project, location, api_key\n", + "\n", + "\n", + "def check_define_env_vars(\n", + " enterprise: bool,\n", + " project: str,\n", + " location: str,\n", + " api_key: str,\n", + ") -> bool:\n", + " match (enterprise, bool(project), bool(location), bool(api_key)):\n", + " case (True, True, _, _):\n", + " # Agent Platform - Google Cloud project [+location]\n", + " location = location or \"global\"\n", + " define_env_vars(enterprise, project, location, \"\")\n", + " case (True, False, _, True):\n", + " # Agent Platform - API key\n", + " define_env_vars(enterprise, \"\", \"\", api_key)\n", + " case (False, _, _, True):\n", + " # Google AI Studio - API key\n", + " define_env_vars(enterprise, \"\", \"\", api_key)\n", + " case _:\n", + " return False\n", + "\n", + " return True\n", + "\n", + "\n", + "def define_env_vars(\n", + " enterprise: bool,\n", + " project: str,\n", + " location: str,\n", + " api_key: str,\n", + ") -> None:\n", + " os.environ[\"GOOGLE_GENAI_USE_ENTERPRISE\"] = str(enterprise)\n", + " os.environ[\"GOOGLE_GENAI_USE_VERTEXAI\"] = str(enterprise)\n", + " os.environ[\"GOOGLE_CLOUD_PROJECT\"] = project\n", + " os.environ[\"GOOGLE_CLOUD_LOCATION\"] = location\n", + " os.environ[\"GOOGLE_API_KEY\"] = api_key\n", + "\n", + "\n", + "def check_configuration(client: genai.Client) -> None:\n", + " service = \"Agent Platform\" if client.vertexai else \"Google AI Studio\"\n", + " print(f\"✅ Using the {service} API\", end=\"\")\n", + "\n", + " if client._api_client.project:\n", + " print(f' with project \"{client._api_client.project[:7]}
\"', end=\"\")\n", + " print(f' in location \"{client._api_client.location}\"')\n", + " elif client._api_client.api_key:\n", + " api_key = client._api_client.api_key\n", + " print(f' with API key \"{api_key[:5]}
{api_key[-5:]}\"', end=\"\")\n", + " print(f\" (in case of error, make sure it was created for {service})\")\n", + "\n", + "\n", + "print(\"✅ Environment functions defined\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "Ee26klvTs9mU" + }, + "source": [ + "---\n", + "\n", + "### đŸ€– Gen AI SDK\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "dZu-UMI9rYeK" + }, + "source": [ + "To send Gemini requests, we'll use a `google.genai` client:\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "1vFn9oBTs9mU" + }, + "outputs": [], + "source": [ + "from google import genai\n", + "\n", + "check_environment()\n", + "\n", + "client = genai.Client()\n", + "\n", + "check_configuration(client)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "mU7VFMfVwu4N" + }, + "source": [ + "---\n", + "\n", + "### 🔣 Input data\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "hph-EFk3rYeK" + }, + "source": [ + "We need a suite of test data to develop our solution.\n", + "\n", + "**Multimodality**\n", + "\n", + "We'll test the following types:\n", + "\n", + "- Text (`text/plain`): Classic books are good text sources of varying lengths and languages.\n", + "- PDF (`application/pdf`): Legal agreements are also great examples of complex and dense documents.\n", + "\n", + "Gemini is natively multimodal, which means it can process different types of inputs. Once we've built knowledge graphs from text or PDF inputs, the solution will also naturally support the following formats:\n", + "\n", + "- Image (`image/*`)\n", + "- Audio (`audio/*`)\n", + "- Video (`video/*`)\n", + "\n", + "**General knowledge**\n", + "\n", + "⚠ LLMs are trained on general knowledge, which becomes part of their \"long-term memory\". To avoid generating memorized information, we'll explicitly instruct the model to use only the provided inputs.\n", + "\n", + "**Multilinguality**\n", + "\n", + "Gemini is also natively [multilingual](https://docs.cloud.google.com/vertex-ai/generative-ai/docs/models#expandable-1), which lets us process inputs and generate outputs in 100+ languages.\n", + "\n", + "To keep things general, we'll use English for prompts and knowledge graphs, but you can use any of the 100+ supported languages, as long as your prompts remain clear and explicit.\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "9I_dFyVkrYeK" + }, + "source": [ + "Let's define a few data sources and helpers:\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "4EVlIhc0wu4N" + }, + "outputs": [], + "source": [ + "# @title {display-mode: \"form\"}\n", + "import mimetypes\n", + "from collections.abc import Iterator\n", + "from enum import Enum\n", + "from pathlib import Path\n", + "\n", + "from google.genai.types import Part\n", + "\n", + "GOOGLE_CLOUD_STORAGE_PREFIX = \"gs://\"\n", + "HTTPS_PREFIX = \"https://\"\n", + "FILE_PREFIX = \"file://\"\n", + "LOCAL_FOLDER = \"./\"\n", + "\n", + "\n", + "class Source(Enum):\n", + " def yield_contents(self) -> Iterator[Part]:\n", + " file_uri = self.value\n", + " if not client.vertexai:\n", + " file_uri = convert_to_https_url_if_cloud_storage_uri(file_uri)\n", + " mime_type, _ = mimetypes.guess_type(file_uri)\n", + " assert mime_type is not None, f\"❌ Could not determine mime type: {file_uri=}\"\n", + "\n", + " if file_uri.startswith((GOOGLE_CLOUD_STORAGE_PREFIX, HTTPS_PREFIX)):\n", + " yield Part.from_uri(file_uri=file_uri, mime_type=mime_type)\n", + " return\n", + "\n", + " if file_uri.startswith(FILE_PREFIX):\n", + " file = Path(file_uri.removeprefix(FILE_PREFIX))\n", + " assert file.exists(), f\"❌ File does not exist: {file=}\"\n", + " if mime_type == \"text/plain\":\n", + " yield Part.from_text(text=file.read_text(encoding=\"utf-8\"))\n", + " else:\n", + " yield Part.from_bytes(data=file.read_bytes(), mime_type=mime_type)\n", + " return\n", + "\n", + " def yield_source_names(self) -> Iterator[str]:\n", + " yield self.name\n", + "\n", + " def yield_source_links(self) -> Iterator[str]:\n", + " file_uri = convert_to_https_url_if_cloud_storage_uri(self.value)\n", + " if file_uri.startswith(HTTPS_PREFIX):\n", + " yield file_uri\n", + " return\n", + " if file_uri.startswith(FILE_PREFIX):\n", + " yield file_uri.removeprefix(FILE_PREFIX)\n", + " return\n", + "\n", + "\n", + "def convert_to_https_url_if_cloud_storage_uri(uri: str) -> str:\n", + " return (\n", + " f\"{HTTPS_PREFIX}storage.googleapis.com/{uri.removeprefix(GOOGLE_CLOUD_STORAGE_PREFIX)}\"\n", + " if uri.startswith(GOOGLE_CLOUD_STORAGE_PREFIX)\n", + " else uri\n", + " )\n", + "\n", + "\n", + "def local_file(filename: str) -> str:\n", + " return f\"{FILE_PREFIX}{LOCAL_FOLDER}{filename}\"\n", + "\n", + "\n", + "# You can find public domain books on Project Gutenberg: https://gutenberg.org/ebooks\n", + "def project_gutenberg_txt_url(id: int) -> str:\n", + " return f\"{HTTPS_PREFIX}gutenberg.org/cache/epub/{id}/pg{id}.txt\"\n", + "\n", + "\n", + "class Classic(Source):\n", + " en_hugo_les_misĂ©rables = project_gutenberg_txt_url(135)\n", + " en_dumas_count_of_monte_cristo = project_gutenberg_txt_url(1184)\n", + " fr_zola_thĂ©rĂšse_raquin = project_gutenberg_txt_url(7461)\n", + " fr_dumas_trois_mousquetaires = project_gutenberg_txt_url(13951)\n", + " fr_dumas_vingt_ans_aprĂšs = project_gutenberg_txt_url(13952)\n", + " fr_dumas_comte_de_monte_cristo_1 = project_gutenberg_txt_url(17989)\n", + " fr_dumas_comte_de_monte_cristo_2 = project_gutenberg_txt_url(17990)\n", + " fr_dumas_comte_de_monte_cristo_3 = project_gutenberg_txt_url(17991)\n", + " fr_dumas_comte_de_monte_cristo_4 = project_gutenberg_txt_url(17992)\n", + "\n", + "\n", + "class Document(Source):\n", + " en_pharma_dev_agreement = \"gs://cloud-samples-data/documentai/ContractDocAI/CUAD_v1/Part_I/Development/PhasebioPharmaceuticalsInc_20200330_10-K_EX-10.21_12086810_EX-10.21_Development Agreement.pdf\"\n", + "\n", + "\n", + "class Collection(Source):\n", + " fr_dumas_comte_de_monte_cristo = [\n", + " Classic.fr_dumas_comte_de_monte_cristo_1,\n", + " Classic.fr_dumas_comte_de_monte_cristo_2,\n", + " Classic.fr_dumas_comte_de_monte_cristo_3,\n", + " Classic.fr_dumas_comte_de_monte_cristo_4,\n", + " ]\n", + " fr_dumas_trois_mousquetaires_vingt_ans_aprĂšs = [\n", + " Classic.fr_dumas_trois_mousquetaires,\n", + " Classic.fr_dumas_vingt_ans_aprĂšs,\n", + " ]\n", + "\n", + " def yield_contents(self) -> Iterator[Part]:\n", + " for source in self.value:\n", + " yield from source.yield_contents()\n", + "\n", + " def yield_source_names(self) -> Iterator[str]:\n", + " for source in self.value:\n", + " yield from source.yield_source_names()\n", + "\n", + " def yield_source_links(self) -> Iterator[str]:\n", + " for source in self.value:\n", + " yield from source.yield_source_links()\n", + "\n", + "\n", + "def display_input_data_caption(source: Source) -> None:\n", + " names = list(source.yield_source_names())\n", + " links = list(source.yield_source_links())\n", + " links = \", \".join(\n", + " f\"[{name}](<{link}>)\" for name, link in zip(names, links, strict=True)\n", + " )\n", + " md = f\"**Input data** ({links})\"\n", + " display_markdown(md)\n", + "\n", + "\n", + "print(\"✅ Data helpers defined\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "SB2kbdlBs9mV" + }, + "source": [ + "---\n", + "\n", + "### 🧠 Gemini model\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "40a0ukjJwu4O" + }, + "source": [ + "Gemini comes in different [versions](https://docs.cloud.google.com/vertex-ai/generative-ai/docs/models#gemini-models) and sizes (Flash-Lite, Flash, and Pro).\n", + "\n", + "Let's get started with Gemini 3.1 Flash-Lite, as it offers high performance, low latency, and very high output speed:\n", + "\n", + "- `GEMINI_3_1_FLASH_LITE = \"gemini-3.1-flash-lite\"`\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "a3-ICTRMs9mV" + }, + "source": [ + "---\n", + "\n", + "### ⚙ Gemini configuration\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "y-3U82ZGs9mV" + }, + "source": [ + "Gemini can be used in different ways, ranging from factual to creative modes. We're essentially dealing with a **data-extraction use case**. We want the results to be as factual and deterministic as possible. To achieve this, we can adjust the [content generation parameters](https://docs.cloud.google.com/vertex-ai/generative-ai/docs/multimodal/content-generation-parameters).\n", + "\n", + "We'll set the `temperature`, `top_p`, and `seed` parameters to minimize randomness:\n", + "\n", + "- `temperature=0.0`\n", + "- `top_p=0.0`\n", + "- `seed=42` (arbitrary fixed value)\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "SkkobNmYwu4O" + }, + "source": [ + "---\n", + "\n", + "### đŸ› ïž Helpers\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "fj1rJnozrYeL" + }, + "source": [ + "Now, let's add core helper classes and functions:\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "Bus2ODLIK02F" + }, + "outputs": [], + "source": [ + "# @title {display-mode: \"form\"}\n", + "from enum import StrEnum, auto\n", + "\n", + "import IPython.display\n", + "import tenacity\n", + "from google.genai.errors import ClientError\n", + "from google.genai.types import (\n", + " FinishReason,\n", + " GenerateContentConfig,\n", + " GenerateContentResponse,\n", + " ThinkingConfig,\n", + " ThinkingLevel,\n", + ")\n", + "\n", + "\n", + "class Model(Enum):\n", + " GEMINI_3_1_FLASH_LITE = \"gemini-3.1-flash-lite\"\n", + " GEMINI_3_5_FLASH = \"gemini-3.5-flash\"\n", + " GEMINI_2_5_FLASH = \"gemini-2.5-flash\"\n", + " GEMINI_2_5_PRO = \"gemini-2.5-pro\"\n", + " # Preview\n", + " GEMINI_3_1_PRO = \"gemini-3.1-pro-preview\"\n", + " # Default model\n", + " DEFAULT = GEMINI_3_1_FLASH_LITE\n", + "\n", + "\n", + "# Default configuration for more deterministic outputs\n", + "DEFAULT_CONFIG = GenerateContentConfig(\n", + " temperature=0.0,\n", + " top_p=0.0,\n", + " seed=42, # Arbitrary fixed value\n", + ")\n", + "\n", + "\n", + "class ShowAs(StrEnum):\n", + " DONT_SHOW = auto()\n", + " TEXT = auto()\n", + " MARKDOWN = auto()\n", + "\n", + "\n", + "def generate_content(\n", + " prompt: str,\n", + " source: Source | str | None = None,\n", + " *,\n", + " model: Model | None = None,\n", + " config: GenerateContentConfig | None = None,\n", + " system_instruction: str | None = None,\n", + " show_prompt: ShowAs = ShowAs.DONT_SHOW,\n", + " show_response: ShowAs = ShowAs.MARKDOWN,\n", + " only_show_prompt: bool = False,\n", + " return_response: bool = False,\n", + ") -> GenerateContentResponse | None:\n", + " disable_colab_cell_scrollbar()\n", + "\n", + " model = model or Model.DEFAULT\n", + " model_id = model.value\n", + " prompt_contents = get_prompt_contents(prompt, source, show_prompt, only_show_prompt)\n", + " if only_show_prompt:\n", + " return None\n", + " config = config or get_generate_content_config(model, system_instruction)\n", + " client = check_client_for_model(model)\n", + "\n", + " response = None\n", + " display_request_header(model_id, source)\n", + " for attempt in get_retrier():\n", + " with attempt:\n", + " response = client.models.generate_content(\n", + " model=model_id,\n", + " contents=prompt_contents, # type: ignore\n", + " config=config,\n", + " )\n", + " display_response_info(response)\n", + " display_response(response, show_response)\n", + "\n", + " return response if return_response else None\n", + "\n", + "\n", + "def get_prompt_contents(\n", + " prompt: str,\n", + " source: Source | str | None,\n", + " show_prompt: ShowAs,\n", + " only_show_prompt: bool,\n", + ") -> list[str | Part]:\n", + " def yield_prompt_contents() -> Iterator[str | Part]:\n", + " if not source:\n", + " yield prompt.strip()\n", + " return\n", + " yield \"==Start of input data==\\n\"\n", + " if isinstance(source, str):\n", + " yield f\"{source.strip()}\\n\"\n", + " else:\n", + " yield from source.yield_contents()\n", + " yield \"==End of input data==\\n\"\n", + " yield f\"==Start of user prompt==\\n{prompt.strip()}\\n==End of user prompt==\"\n", + "\n", + " prompt_contents = list(yield_prompt_contents())\n", + " display_prompt(prompt_contents, show_prompt, only_show_prompt)\n", + "\n", + " return prompt_contents\n", + "\n", + "\n", + "def get_generate_content_config(\n", + " model: Model,\n", + " system_instruction: str | None = None,\n", + ") -> GenerateContentConfig:\n", + " thinking_config = get_thinking_config_for_model(model)\n", + "\n", + " return GenerateContentConfig(\n", + " system_instruction=system_instruction,\n", + " temperature=DEFAULT_CONFIG.temperature,\n", + " top_p=DEFAULT_CONFIG.top_p,\n", + " seed=DEFAULT_CONFIG.seed,\n", + " thinking_config=thinking_config,\n", + " )\n", + "\n", + "\n", + "def get_thinking_config_for_model(model: Model) -> ThinkingConfig | None:\n", + " # Use minimal thinking configurations since our prompt will directly provide a chain of thought\n", + " match model:\n", + " case Model.GEMINI_2_5_FLASH:\n", + " return ThinkingConfig(thinking_budget=0)\n", + " case Model.GEMINI_2_5_PRO:\n", + " return ThinkingConfig(thinking_budget=128, include_thoughts=False)\n", + " case Model.GEMINI_3_1_FLASH_LITE | Model.GEMINI_3_5_FLASH:\n", + " return ThinkingConfig(thinking_level=ThinkingLevel.MINIMAL)\n", + " case Model.GEMINI_3_1_PRO:\n", + " return ThinkingConfig(thinking_level=ThinkingLevel.LOW)\n", + " case _:\n", + " return None # Default (dynamic thinking is generally enabled)\n", + "\n", + "\n", + "def check_client_for_model(model: Model) -> genai.Client:\n", + " if (\n", + " model.value.endswith(\"-preview\")\n", + " and client.vertexai\n", + " and client._api_client.location != \"global\"\n", + " ):\n", + " # Preview models are only available on the \"global\" location\n", + " return genai.Client(\n", + " enterprise=client.vertexai,\n", + " project=client._api_client.project,\n", + " location=\"global\",\n", + " )\n", + "\n", + " return client\n", + "\n", + "\n", + "def get_retrier() -> tenacity.Retrying:\n", + " return tenacity.Retrying(\n", + " stop=tenacity.stop_after_attempt(7),\n", + " wait=tenacity.wait_incrementing(start=10, increment=1),\n", + " retry=tenacity.retry_if_exception(should_retry_request),\n", + " reraise=True,\n", + " )\n", + "\n", + "\n", + "def should_retry_request(err: BaseException) -> bool:\n", + " if not isinstance(err, ClientError):\n", + " return False\n", + " print(f\"❌ ClientError {err.code}: {err.message}\")\n", + "\n", + " retry = False\n", + " match err.code:\n", + " case 400 if err.message is not None and \" try again \" in err.message:\n", + " # Workshop: project accessing Cloud Storage for the first time (service agent provisioning)\n", + " retry = True\n", + " case 429:\n", + " # Workshop: temporary project with 1 QPM quota\n", + " retry = True\n", + " print(f\"🔄 Retry: {retry}\")\n", + "\n", + " return retry\n", + "\n", + "\n", + "PRINT_COLUMNS = 80\n", + "PRINT_SEPARATOR_CHAR = \"-\"\n", + "PRINT_SEPARATOR = PRINT_COLUMNS * PRINT_SEPARATOR_CHAR\n", + "\n", + "\n", + "def print_caption(caption: str) -> None:\n", + " print(f\" {caption} \".center(PRINT_COLUMNS, PRINT_SEPARATOR_CHAR))\n", + "\n", + "\n", + "def print_separator() -> None:\n", + " print(PRINT_SEPARATOR)\n", + "\n", + "\n", + "def display_response_info(response: GenerateContentResponse) -> None:\n", + " if usage_metadata := response.usage_metadata:\n", + " if usage_metadata.prompt_token_count:\n", + " print(f\"Input tokens : {usage_metadata.prompt_token_count:9,d}\")\n", + " if usage_metadata.cached_content_token_count:\n", + " print(f\"Cached tokens : {usage_metadata.cached_content_token_count:9,d}\")\n", + " if usage_metadata.candidates_token_count:\n", + " print(f\"Output tokens : {usage_metadata.candidates_token_count:9,d}\")\n", + " if usage_metadata.thoughts_token_count:\n", + " print(f\"Thoughts tokens : {usage_metadata.thoughts_token_count:9,d}\")\n", + " if not response.candidates:\n", + " print(\"❌ No `response.candidates`\")\n", + " return\n", + " if (finish_reason := response.candidates[0].finish_reason) != FinishReason.STOP:\n", + " print(f\"❌ {finish_reason = }\")\n", + " if not response.text:\n", + " print(\"❌ No `response.text`\")\n", + " return\n", + "\n", + "\n", + "def display_prompt(\n", + " contents: list[str | Part],\n", + " show_as: ShowAs,\n", + " only_show_prompt: bool,\n", + ") -> None:\n", + " def yield_prompt_strings() -> Iterator[str]:\n", + " for content in contents:\n", + " if isinstance(content, Part):\n", + " yield f\"{content!r}\\n\"\n", + " continue\n", + " yield content\n", + "\n", + " if only_show_prompt and show_as == ShowAs.DONT_SHOW:\n", + " show_as = ShowAs.TEXT\n", + " if show_as == ShowAs.DONT_SHOW:\n", + " return\n", + "\n", + " separator = \"\\n\" if show_as == ShowAs.MARKDOWN else \"\"\n", + " prompt = separator.join(yield_prompt_strings())\n", + " print_caption(\"Prompt\")\n", + " match show_as:\n", + " case ShowAs.TEXT:\n", + " print(prompt)\n", + " case ShowAs.MARKDOWN:\n", + " display_markdown(prompt)\n", + " if only_show_prompt:\n", + " print_separator()\n", + "\n", + "\n", + "def display_request_header(model_id: str, source: Source | str | None = None) -> None:\n", + " print_caption(f\"Request / {model_id}\")\n", + "\n", + "\n", + "def display_response(response: GenerateContentResponse, show_as: ShowAs) -> None:\n", + " if show_as == ShowAs.DONT_SHOW or not (response_text := response.text):\n", + " return\n", + " print_caption(\"Start of Response\")\n", + " response_text = response_text.strip()\n", + " match show_as:\n", + " case ShowAs.TEXT:\n", + " print(response_text)\n", + " case ShowAs.MARKDOWN:\n", + " display_markdown(response_text)\n", + " print_caption(\"End of Response\")\n", + "\n", + "\n", + "def display_markdown(markdown: str) -> None:\n", + " IPython.display.display(IPython.display.Markdown(markdown))\n", + "\n", + "\n", + "print(\"✅ Helpers defined\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "xCstWp8Dwu4P" + }, + "source": [ + "---\n", + "\n", + "## đŸ—ïž Prototyping\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "0jisqsdjrYeM" + }, + "source": [ + "Before diving into a solution, it helps to start by prototyping to build some intuition about the natural behavior of the model.\n", + "\n", + "Let's define a short text of a few sentences:\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "QQdWNqzDBQyh" + }, + "outputs": [], + "source": [ + "text = \"\"\"\n", + "- Henry Jones is a famous archaeologist. He is actually a \"Junior\" because he is named after his father.\n", + "- Sophie is Henry's daughter, shares his last name, and works as a software engineer.\n", + "- William Smith is an aerospace engineer and Sophie's lifelong friend. Everybody calls him Bill and Beau is his dog.\n", + "- Short Round met Henry as a child. They first became close friends, and Henry officially adopted him a few years later.\n", + "- Sophie and Bill both work at Acme Aerospace.\n", + "\"\"\"" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "cdPd_Q7nrYeM" + }, + "source": [ + "---\n", + "\n", + "### 📋 Listing characters\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "gWZLdO61rYeM" + }, + "source": [ + "đŸ§Ș Let's see if Gemini can spot our characters
\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "Vlo-LQttBQyh" + }, + "outputs": [], + "source": [ + "prompt = \"\"\"\n", + "Using only the input data, list all people and animals mentioned.\n", + "\"\"\"\n", + "generate_content(prompt, text)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "9389bbe8f5a6" + }, + "source": [ + "💡 All people and animals are detected as expected.\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "Bj4vTDmWrYeM" + }, + "source": [ + "---\n", + "\n", + "### 📋 Listing characters & relationships\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "LBSoebt9rYeM" + }, + "source": [ + "đŸ§Ș Now, let's see if it can connect the dots and figure out who's who
\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "_ymgEE1ABQyh" + }, + "outputs": [], + "source": [ + "prompt = \"\"\"\n", + "Using only the input data, list all people and animals mentioned, and how they relate to each other.\n", + "\"\"\"\n", + "generate_content(prompt, text)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "425976f7f9f3" + }, + "source": [ + "💡 Notes\n", + "\n", + "- The relationships between the characters can also be neatly consolidated.\n", + "- Henry's father is not listed as a named character. Here, we can see the difference between explicit and implicit mentions. By default, only explicit entities are detailed (most likely because models are trained to summarize information).\n", + "- Implied characters (like _\"he is named after his father\"_) represent a level of indirection. We'll see in the next section how to detect them as well.\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "EoXFKrHUrYeR" + }, + "source": [ + "---\n", + "\n", + "### 📚 Domain terminology\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "dp8cpOWIrYeR" + }, + "source": [ + "We're not domain experts in the field we're exploring (yet!).\n", + "\n", + "An LLM processes instructions based on the given prompt and its training knowledge. This knowledge is part of its long-term memory, and we can learn a lot directly from the model itself.\n", + "\n", + "đŸ§Ș Let's ask Gemini:\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "7V7B6P1erYeR" + }, + "outputs": [], + "source": [ + "prompt = \"\"\"\n", + "What is the terminology used when building a knowledge graph?\n", + "Please provide a simple data example in JSON.\n", + "\"\"\"\n", + "generate_content(prompt)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "bfe9550d2a98" + }, + "source": [ + "💡 We learn that knowledge graphs are made of **entities** and **relationships**, also called **nodes** and **edges**, and we get a nice introduction to the field. Using domain terminology will help make our prompts explicit and precise.\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "H0PdbpOzrYeR" + }, + "source": [ + "---\n", + "\n", + "## ⛏ Tabular extraction\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "2d7f483b2a8d" + }, + "source": [ + "To extract knowledge graphs, we'll reason in terms of entities and relationships, adopting domain terminology.\n", + "\n", + "If we think of the final result as a database, our goal is to generate two linked tables, allowing us to reason in terms of data and fields.\n", + "\n", + "Here is a conceptual view of what we want to achieve:\n", + "\n", + "**Entities**\n", + "\n", + "| `id` | `name` | `label` |\n", + "| :--: | :-------------: | :------ |\n", + "| 0 | Henry Jones Jr. | person |\n", + "| 1 | Henry Jones Sr. | person |\n", + "\n", + "**Relationships**\n", + "\n", + "| `source_id` | `link` | `target_id` |\n", + "| :---------: | :------: | :---------: |\n", + "| 0 | child_of | 1 |\n", + "\n", + "Let's call this approach \"tabular extraction\" and split our instructions to output two successive tables, while still using a single request
\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "DvwrL4xlrYeR" + }, + "source": [ + "---\n", + "\n", + "### 💠 Entities\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "b4eb7ce49827" + }, + "source": [ + "In our prototype text, the entities we want to extract are characters (like people or animals). We can define an entity data schema with the fields `id` (0, 1, 2
), `name` (full name of the entity), and `label` (`person`|`animal`).\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "FCoqYCbrrYeR" + }, + "source": [ + "đŸ§Ș Let's extract the entities:\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "BYEHthFBrYeR" + }, + "outputs": [], + "source": [ + "prompt = \"\"\"\n", + "**Data Schema**\n", + "\n", + "Entity:\n", + "- `id`: Unique integer identifier (0, 1, 2
).\n", + "- `name`: Full name of the entity.\n", + "- `label`: `person`|`animal`.\n", + "\n", + "**Instructions**\n", + "\n", + "1. Entity Extraction:\n", + " - Extract every distinct entity from the input data that matches an allowed `label`.\n", + " - Include entities that are explicitly named as well as implied entities whose names can be determined from the context.\n", + "2. Output the results as a JSON array inside a fenced code block.\n", + "\"\"\"\n", + "\n", + "generate_content(prompt, text)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "e8ef88bfca1b" + }, + "source": [ + "💡 Remarks\n", + "\n", + "- Every entity is dynamically assigned a unique sequential identifier.\n", + "- The name \"Henry Jones Sr.\" is extracted even though he's not explicitly mentioned in the text. This stems from the instruction to _\"include entities that are explicitly named as well as implied entities whose names can be determined from the context\"_.\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "lVIEi9tlrYeS" + }, + "source": [ + "---\n", + "\n", + "### 🔗 Relationships\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "LtGde_DKrYeS" + }, + "source": [ + "đŸ§Ș Now, let's extract both the entities and their relationships:\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "WM0RFso_rYeS" + }, + "outputs": [], + "source": [ + "prompt = \"\"\"\n", + "**Data Schema**\n", + "\n", + "Entity:\n", + "- `id`: Unique integer identifier (0, 1, 2
).\n", + "- `name`: Full name of the entity.\n", + "- `label`: `person`|`animal`.\n", + "\n", + "Relationship:\n", + "- `source_id`: `id` of the subject entity.\n", + "- `link`: `snake_case` predicate describing the relationship.\n", + "- `target_id`: `id` of the object entity.\n", + "\n", + "**Instructions**\n", + "\n", + "1. Entity Extraction:\n", + " - Extract every distinct entity from the input data that matches an allowed `label`.\n", + " - Include entities that are explicitly named as well as implied entities whose names can be determined from the context.\n", + "2. Relationship Extraction:\n", + " - Extract every distinct relationship between them.\n", + " - If a relationship changes over time, make sure to include every distinct stage of the relationship.\n", + "3. Output a JSON object with keys `entities` and `relationships` inside a fenced code block.\n", + "\"\"\"\n", + "\n", + "response = generate_content(prompt, text, return_response=True)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "649eacf44e3d" + }, + "source": [ + "💡 Remarks\n", + "\n", + "- The output now includes the `relationships` array.\n", + "- We get the expected number of relationships, notably the two relationships between Henry and Short Round.\n", + "- The `link` predicates are completely dynamic (a level of flexibility we left in the prompt). While it's interesting to see this natural behavior, we'll want to make it more deterministic for production since our prompt has too many degrees of freedom.\n", + "- Our open prompt extracts relationships in only one direction. For example, \"[Animal] `pet_of` [Person]\" is an asymmetric relationship that could also be extracted as \"[Person] `owner_of` [Animal]\". This is another area where the prompt is too open-ended. In the finalization section, we'll see an example that asks the model to extract symmetric and asymmetric relationships in both directions.\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "r1PkyO-ZrYeS" + }, + "source": [ + "---\n", + "\n", + "## 💎 Structured output\n", + "\n", + "We've concluded our prototyping stage with promising results using a data schema.\n", + "\n", + "To move to production, the next step is to control the generation with a specific structured output.\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "sMLYiIlxrYeS" + }, + "source": [ + "---\n", + "\n", + "### đŸ—‚ïž JSON\n", + "\n", + "The JSON format has industry-wide support and serves as a core or intermediate format in many use cases.\n", + "\n", + "For the next step, we would typically define classes using the Pydantic library and request a pure JSON output with a response schema in the config parameters:\n", + "\n", + "- `response_mime_type=\"application/json\"`\n", + "- `response_schema=\"CLASS_DERIVED_FROM_PYDANTIC_BASE_MODEL\"` ([docs](https://cloud.google.com/vertex-ai/generative-ai/docs/multimodal/control-generated-output#fields))\n", + "\n", + "⚠ However, JSON is a pretty verbose format, designed for interoperability but not optimized for size. Even if we generate compact JSON (also called minified JSON), it still has inherent verbosity due to:\n", + "\n", + "- repeated keys (object fields) for each object instance\n", + "- opening and closing brackets\n", + "- enclosing quotes for each key and string value\n", + "\n", + "â„č When using LLMs, once the first token is generated, the remaining generation time is roughly proportional to the number of output tokens. Similarly, the cost of a request is based on token usage (input + output), with output tokens being significantly more expensive than input tokens.\n", + "\n", + "💡 A better output structure will positively impact both generation speed and request cost.\n", + "\n", + "Let's explore an alternative
\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "361f37b18efc" + }, + "source": [ + "---\n", + "\n", + "### 📅 TSV\n", + "\n", + "Our tabular-extraction problem clearly calls for table outputs. An interesting possibility is to ask for Tab-Separated Values (TSV) outputs. For example, we can define our output to be formatted as two consecutive TSV tables.\n", + "\n", + "**Example output format**\n", + "\n", + "````text\n", + "```tsv filename=\"entities.tsv\"\n", + "id{TAB}name{TAB}label\n", + "[rows]\n", + "```\n", + "\n", + "```tsv filename=\"relationships.tsv\"\n", + "source_id{TAB}link{TAB}target_id\n", + "[rows]\n", + "```\n", + "````\n", + "\n", + "**Will this work?**\n", + "\n", + "Generating structured outputs like TSV will work seamlessly, as Gemini excels at patterns. We just need to be explicit about what's expected.\n", + "\n", + "**Will this be efficient?**\n", + "\n", + "💡 For our use case, this structure looks optimal:\n", + "\n", + "- Field names are stated once in the header row.\n", + "- Field values are raw, without outer quotes.\n", + "- Tab and newline separators take at most a single token each and do not collide with field values (unless we're dealing with very special input data, but we could then prompt the model to escape these using different characters or strings).\n", + "\n", + "â„č CSV could be another alternative, but common separators like commas are everywhere in natural language and frequently appear in names and descriptions (e.g., if we decide to extend entity fields). If you're interested in this topic, check out the [TOON format](https://github.com/toon-format/toon), which proposes a JSON alternative using a YAML+CSV mix.\n", + "\n", + "To make an informed decision, we should actually compare the number of tokens needed to represent the same data
\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "DooNFicCrYeS" + }, + "source": [ + "---\n", + "\n", + "### đŸ—œïž Token comparison\n", + "\n", + "Let's define some helpers to compare the same data using pretty-printed JSON, compact JSON, and TSV:\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "ahwHZVdGrYeS" + }, + "outputs": [], + "source": [ + "# @title {display-mode: \"form\"}\n", + "import csv\n", + "import io\n", + "import json\n", + "import re\n", + "from typing import Literal\n", + "\n", + "\n", + "def get_data_from_response(response: GenerateContentResponse) -> dict:\n", + " response_text = response.text or \"\"\n", + " pattern = r\"```json\\s*(.*?)\\s*```\"\n", + " match = re.search(pattern, response_text, re.DOTALL)\n", + " json_str = match.group(1) if match else response_text\n", + " try:\n", + " data = json.loads(json_str)\n", + " if not isinstance(data, dict):\n", + " print(\"❌ Returning empty dict (could not parse response as dict)\")\n", + " data = {}\n", + " except (json.JSONDecodeError, TypeError):\n", + " print(\"❌ Returning empty dict (failed parsing the JSON string)\")\n", + " data = {}\n", + " return data\n", + "\n", + "\n", + "def get_tsv_string_from_data(data: dict) -> str:\n", + " output = \"\"\n", + " for key, items in data.items():\n", + " rows = \"\"\n", + " if items:\n", + " with io.StringIO() as out:\n", + " headers = list(items[0].keys())\n", + " writer = csv.DictWriter(\n", + " out,\n", + " fieldnames=headers,\n", + " delimiter=\"\\t\",\n", + " lineterminator=\"\\n\",\n", + " )\n", + " writer.writeheader()\n", + " writer.writerows(items)\n", + " rows = out.getvalue()\n", + " if output:\n", + " output += \"\\n\"\n", + " output += f'```tsv filename=\"{key}.tsv\"\\n{rows}```\\n'\n", + " return output\n", + "\n", + "\n", + "def print_text_excerpt(title: str, text: str, max_chars: int = 400) -> None:\n", + " assert max_chars > 0\n", + " chars = len(text)\n", + " if chars <= 0:\n", + " print_caption(\"❌ Empty text\")\n", + " return\n", + " if chars <= max_chars:\n", + " print_caption(f\"{title} ({chars} chars)\")\n", + " print(text)\n", + " return\n", + " print_caption(\n", + " f\"{title} - First {max_chars}/{chars} chars ({max_chars / chars:.0%})\"\n", + " )\n", + " print(f\"{text[:max_chars]}
\")\n", + "\n", + "\n", + "def compare_json_vs_tsv(\n", + " response: GenerateContentResponse | None,\n", + " only_show_excerpts: bool = False,\n", + ") -> None:\n", + " def get_gain(rows: list[tuple[int, int]], col: Literal[0, 1]) -> str:\n", + " val_0, val_1 = rows[0][col], rows[1][col]\n", + " return f\"**{1 - val_1 / val_0:.1%}**\" if val_0 > 0 else \"?\"\n", + "\n", + " def yield_table_string_rows(\n", + " source_title: str,\n", + " target_title: str,\n", + " rows: list[tuple[int, int]],\n", + " ) -> Iterator[list[str]]:\n", + " yield [\"\", \"Chars\", \"Tokens\"]\n", + " yield [\"-\", \"-:\", \"-:\"]\n", + " for caption, values in zip([source_title, target_title], rows):\n", + " yield [caption, *[str(value) for value in values]]\n", + " yield [\"**Gain**\", get_gain(rows, 0), get_gain(rows, 1)]\n", + "\n", + " def display_gain_table(\n", + " source_title: str,\n", + " target_title: str,\n", + " source_text: str,\n", + " target_text: str,\n", + " ) -> None:\n", + " print_caption(f\"{source_title} → {target_title}\")\n", + " model = Model.DEFAULT\n", + " model_id = model.value\n", + " client = check_client_for_model(model)\n", + "\n", + " rows: list[tuple[int, int]] = []\n", + " for s in [source_text, target_text]:\n", + " chars = len(s)\n", + " tokens = client.models.count_tokens(model=model_id, contents=s).total_tokens\n", + " rows.append((chars, tokens or 0))\n", + " markdown = \"\\n\".join(\n", + " \"| \" + \" | \".join(row) + \" |\"\n", + " for row in yield_table_string_rows(source_title, target_title, rows)\n", + " )\n", + " display_markdown(markdown)\n", + "\n", + " if response is None:\n", + " print(\"❌ response is None\")\n", + " return\n", + " data = get_data_from_response(response)\n", + " formatted_json = f\"```json\\n{json.dumps(data, indent=2)}\\n```\"\n", + " compact_json = f\"```json\\n{json.dumps(data, separators=(',', ':'))}\\n```\"\n", + " tsv = get_tsv_string_from_data(data)\n", + "\n", + " if only_show_excerpts:\n", + " max_chars = len(tsv)\n", + " print_text_excerpt(\"Formatted JSON\", formatted_json, max_chars)\n", + " print_text_excerpt(\"Compact JSON\", compact_json, max_chars)\n", + " print_text_excerpt(\"TSV\", tsv, max_chars)\n", + " return\n", + "\n", + " display_gain_table(\"Formatted JSON\", \"Compact JSON\", formatted_json, compact_json)\n", + " display_gain_table(\"Compact JSON\", \"TSV\", compact_json, tsv)\n", + " display_gain_table(\"Formatted JSON\", \"TSV\", formatted_json, tsv)\n", + " print_separator()\n", + "\n", + "\n", + "print(\"✅ JSON vs TSV helpers defined\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "b17ee6aac3c0" + }, + "source": [ + "đŸ§Ș First, let's compare how much data we can represent for the same number of characters based on our latest API response:\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "11db1bcf88da" + }, + "outputs": [], + "source": [ + "compare_json_vs_tsv(response, only_show_excerpts=True)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "630528d81396" + }, + "source": [ + "💡 Notice how much more information can be represented in the same number of text characters. This will apply similarly to token counts.\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "_GJKEnhhrYeS" + }, + "source": [ + "đŸ§Ș And now, let's compare the gains, especially for token counts:\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "otH73GYKrYeS" + }, + "outputs": [], + "source": [ + "compare_json_vs_tsv(response)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "syetSSLErYeS" + }, + "source": [ + "💡 **Savings in output tokens:**\n", + "\n", + "- **30 to 40%** compared to compact JSON\n", + "- **60 to 70%** compared to standard JSON\n", + "\n", + "With a double-digit percentage reduction in output tokens, building knowledge graphs with TSV outputs is significantly faster (and cheaper)!\n", + "\n", + "Now, let's finalize our code with optimized structures
\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "FvIgJs0xwu4P" + }, + "source": [ + "---\n", + "\n", + "## 🚀 Finalization\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "w7vY56A_BQyh" + }, + "source": [ + "### đŸ§© Structure\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "u0qCzi5prYeT" + }, + "source": [ + "First, it helps to define a structured prompt template, so we can focus on specific parts of our solution using a divide-and-conquer approach.\n", + "\n", + "Here's a possible prompt template:\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "pabif1zFrYeT" + }, + "outputs": [], + "source": [ + "KNOWLEDGE_GRAPH_PROMPT_TEMPLATE = \"\"\"\n", + "**Data Schema**\n", + "\n", + "{data_schema}\n", + "\n", + "**Instructions**\n", + "\n", + "{instructions}\n", + "\n", + "**Output Format**\n", + "\n", + "{output_format}\n", + "\"\"\"" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "Qzv2DQWvrYeT" + }, + "source": [ + "Then, here are some possible `Entity`, `Relationship`, and `KnowledgeGraph` data classes with the matching output format:\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "xiliL8EZrYeT" + }, + "outputs": [], + "source": [ + "from dataclasses import dataclass, field\n", + "\n", + "\n", + "@dataclass\n", + "class Entity:\n", + " id: int\n", + " name: str\n", + " label: str\n", + "\n", + "\n", + "@dataclass\n", + "class Relationship:\n", + " source_id: int\n", + " link: str\n", + " target_id: int\n", + "\n", + "\n", + "@dataclass\n", + "class KnowledgeGraph:\n", + " entities: list[Entity] = field(default_factory=list)\n", + " relationships: list[Relationship] = field(default_factory=list)\n", + "\n", + "\n", + "TAB = \"\\t\"\n", + "KNOWLEDGE_GRAPH_OUTPUT_FORMAT = f\"\"\"\n", + "Format the output strictly as two TSV code blocks (including the header row):\n", + "\n", + "```tsv filename=\"entities.tsv\"\n", + "id{TAB}name{TAB}label\n", + "[data_rows]\n", + "```\n", + "\n", + "```tsv filename=\"relationships.tsv\"\n", + "source_id{TAB}link{TAB}target_id\n", + "[data_rows]\n", + "```\n", + "\"\"\"" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "2rtBtsRRrYeT" + }, + "source": [ + "💡 While the Gen AI SDK natively supports Pydantic models for JSON structured outputs, we're using standard Python data classes here and TSV outputs to maximize our token efficiency.\n", + "\n", + "â„č If you use multiple entity or relationship data classes in your solution, you can dynamically generate the output format specification using features of the `dataclasses` package (like class docstrings and field descriptions).\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "Ny3Q6Df3rYeT" + }, + "source": [ + "Now, let's define a few helpers to generate a knowledge graph:\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "F3dbOXnKwu4P" + }, + "outputs": [], + "source": [ + "# @title {display-mode: \"form\"}\n", + "from dataclasses import fields, is_dataclass\n", + "from typing import get_args, get_origin, get_type_hints\n", + "\n", + "\n", + "def generate_knowledge_graph(\n", + " data_schema: str,\n", + " instructions: str,\n", + " source: Source | str,\n", + " *,\n", + " model: Model | None = None,\n", + " config: GenerateContentConfig | None = None,\n", + " system_instruction: str | None = None,\n", + " show_prompt: ShowAs = ShowAs.DONT_SHOW,\n", + " show_response: ShowAs = ShowAs.DONT_SHOW,\n", + ") -> KnowledgeGraph:\n", + " prompt = get_prompt_for_data_schema_and_instructions(data_schema, instructions)\n", + " response = generate_content(\n", + " prompt,\n", + " source,\n", + " model=model,\n", + " config=config,\n", + " system_instruction=system_instruction,\n", + " show_prompt=show_prompt,\n", + " show_response=show_response,\n", + " return_response=True,\n", + " )\n", + "\n", + " knowledge_graph = (\n", + " parse_list_dataclass(KnowledgeGraph, response)\n", + " if isinstance(response, GenerateContentResponse)\n", + " else KnowledgeGraph()\n", + " )\n", + " display_knowledge_graph_info(knowledge_graph)\n", + "\n", + " return knowledge_graph\n", + "\n", + "\n", + "def show_knowledge_graph_prompt(\n", + " data_schema: str,\n", + " instructions: str,\n", + " source: Source | str,\n", + " *,\n", + " model: Model | None = None,\n", + " config: GenerateContentConfig | None = None,\n", + " system_instruction: str | None = None,\n", + " show_as: ShowAs = ShowAs.TEXT,\n", + ") -> None:\n", + " prompt = get_prompt_for_data_schema_and_instructions(data_schema, instructions)\n", + " generate_content(\n", + " prompt,\n", + " source,\n", + " model=model,\n", + " config=config,\n", + " system_instruction=system_instruction,\n", + " show_prompt=show_as,\n", + " only_show_prompt=True,\n", + " )\n", + "\n", + "\n", + "def get_prompt_for_data_schema_and_instructions(\n", + " data_schema: str,\n", + " instructions: str,\n", + ") -> str:\n", + " return KNOWLEDGE_GRAPH_PROMPT_TEMPLATE.format(\n", + " data_schema=data_schema.strip(),\n", + " instructions=instructions.strip(),\n", + " output_format=KNOWLEDGE_GRAPH_OUTPUT_FORMAT.strip(),\n", + " ).strip()\n", + "\n", + "\n", + "def parse_list_dataclass[T](cls: type[T], response: GenerateContentResponse) -> T:\n", + " assert is_dataclass(cls)\n", + " if not (response_text := response.text):\n", + " return cls()\n", + "\n", + " data = {}\n", + " for f in fields(cls):\n", + " origin, list_types = get_origin(f.type), get_args(f.type)\n", + " assert origin is list, (\n", + " f\"❌ Field {f.name} must be a list[dataclass] parameterized list\"\n", + " )\n", + " assert len(list_types) == 1, f\"❌ Expected 1 single type: {len(list_types)=}\"\n", + " data[f.name] = parse_tsv_block(list_types[0], response_text, f.name)\n", + "\n", + " return cls(**data)\n", + "\n", + "\n", + "def parse_tsv_block[T](cls: type[T], data: str, tsv_filestem: str) -> list[T]:\n", + " rows = []\n", + " valid_fields = get_type_hints(cls)\n", + " tsv_string = extract_tsv_block(data, tsv_filestem)\n", + " for row in csv.DictReader(io.StringIO(tsv_string), delimiter=\"\\t\"):\n", + " casted_data = {}\n", + " for key, value in row.items():\n", + " if key not in valid_fields or value is None:\n", + " continue\n", + " field_type = valid_fields[key]\n", + " try: # Note: Works only for directly castable types such as int, float, str, enum (e.g., not bool)\n", + " casted_data[key] = field_type(value)\n", + " except (ValueError, TypeError):\n", + " print(f'❌ Could not cast \"{value}\" to {field_type} → Skipping {row=}')\n", + " break\n", + " else:\n", + " try:\n", + " rows.append(cls(**casted_data))\n", + " except TypeError as e:\n", + " print(f\"❌ Could not instantiate {cls.__name__}: {e} → Skipping {row=}\")\n", + "\n", + " return rows\n", + "\n", + "\n", + "def extract_tsv_block(data: str, filestem: str) -> str:\n", + " pattern = rf'```tsv filename=\"{re.escape(filestem)}.tsv\"\\s*\\n(.*?)```'\n", + " if not (match := re.search(pattern, data, flags=re.DOTALL)):\n", + " print(f'❌ Could not find a TSV block for \"{filestem=}\"')\n", + " return \"\"\n", + " return match.group(1)\n", + "\n", + "\n", + "def display_knowledge_graph_info(kg: KnowledgeGraph) -> None:\n", + " print_caption(\"Knowledge Graph Info\")\n", + " print(f\"Entities : {len(kg.entities):3,d}\")\n", + " print(f\"Relationships : {len(kg.relationships):3,d}\")\n", + " print_separator()\n", + "\n", + "\n", + "print(\"✅ Knowledge graph generation helpers defined\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "iuL4sTIkrYeT" + }, + "source": [ + "And here is a possible data schema with some instructions to generate a knowledge graph for our book analysis use case:\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "a34vdbdKrYeT" + }, + "outputs": [], + "source": [ + "from enum import StrEnum, auto\n", + "\n", + "\n", + "class BookAnalysisEntityLabel(StrEnum):\n", + " PERSON = auto()\n", + " ANIMAL = auto()\n", + " ORGANIZATION = auto()\n", + "\n", + "\n", + "def pipe_delimited_union(enum: type[StrEnum]) -> str:\n", + " return \"|\".join(f\"`{e.value}`\" for e in enum)\n", + "\n", + "\n", + "BOOK_ANALYSIS_DATA_SCHEMA = f\"\"\"\n", + "Entity:\n", + "- `id`: Unique integer identifier (0, 1, 2
).\n", + "- `name`: Most complete name as exclusively determined from the input data.\n", + "- `label`: {pipe_delimited_union(BookAnalysisEntityLabel)}.\n", + "\n", + "Relationship:\n", + "- `source_id`: `id` of the subject entity.\n", + "- `link`: `snake_case` predicate.\n", + "- `target_id`: `id` of the object entity.\n", + "\"\"\"\n", + "\n", + "BOOK_ANALYSIS_INSTRUCTIONS = \"\"\"\n", + "- Extract every distinct entity:\n", + " - Treat distinct pseudonyms/identities as separate entities.\n", + " - Include implied entities whose names can be exclusively determined from the input data.\n", + "- Extract every distinct relationship between them:\n", + " - Use specific `link` predicates in `snake_case` as needed (e.g., `alias_of`, `son_of`, `fiancĂ©e_of`, `friend_of`, `murderer_of`, `employer_of`, `in_love_with`, `rival_of`).\n", + " - If a relationship changes over time, make sure to include every distinct stage of the relationship.\n", + " - For every asymmetric relationship extracted, make sure to include the logical inverse relationship (e.g., `A husband_of B` AND `B wife_of A`, `A employer_of B` AND `B employee_of A`).\n", + " - For every symmetric relationship extracted, make sure to include both directions (e.g., `A friend_of B` AND `B friend_of A`).\n", + "\"\"\"" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "pF4ZMv1trYeT" + }, + "source": [ + "Verify the structured prompt:\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "KXJM7WafrYeU" + }, + "outputs": [], + "source": [ + "show_knowledge_graph_prompt(\n", + " BOOK_ANALYSIS_DATA_SCHEMA,\n", + " BOOK_ANALYSIS_INSTRUCTIONS,\n", + " text,\n", + " show_as=ShowAs.TEXT,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "scOh2ra7wu4P" + }, + "source": [ + "đŸ§Ș Let's generate a knowledge graph:\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "QN7RyhLrwu4P" + }, + "outputs": [], + "source": [ + "knowledge_graph = generate_knowledge_graph(\n", + " BOOK_ANALYSIS_DATA_SCHEMA,\n", + " BOOK_ANALYSIS_INSTRUCTIONS,\n", + " text,\n", + " show_response=ShowAs.TEXT,\n", + ")\n", + "\n", + "print(knowledge_graph.entities)\n", + "print(knowledge_graph.relationships)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "3puMj0dsrYeU" + }, + "source": [ + "💡 This is looking good!\n", + "\n", + "Now, let's go to the next stage and build a network graph from our data
\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "HXU002GOBQyi" + }, + "source": [ + "---\n", + "\n", + "### đŸȘą Network graph\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "ZR_BXXjrrYeU" + }, + "source": [ + "Now that we have our entities and relationships neatly packed into data classes, let's bring them to life. We'll use `networkx` to build a network graph. Using domain terminology, entities become nodes and relationships become directed edges. We'll also calculate node centralities to identify key nodes and use the Louvain method to detect communities (clusters of closely related nodes)
\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "gzjOfzJywu4Q" + }, + "outputs": [], + "source": [ + "# @title {display-mode: \"form\"}\n", + "import textwrap\n", + "from typing import cast\n", + "\n", + "import networkx as nx\n", + "import numpy as np\n", + "from networkx.algorithms.community.louvain import louvain_communities\n", + "\n", + "NODE_CENTRALITY = \"node_centrality\"\n", + "NODE_COMMUNITY_INDEX = \"node_community_index\"\n", + "NODE_COLOR = \"node_color\"\n", + "EDGE_COLOR = \"edge_color\"\n", + "# Wrap names over multiple lines (avoids long node labels)\n", + "MULTILINE_NAMES = True\n", + "MULTILINE_CHARS = 12\n", + "\n", + "\n", + "def build_graph(kg: KnowledgeGraph, remove_orphan_nodes: bool) -> nx.DiGraph:\n", + " # For simplicity, we build a DiGraph (adapt to MultiDiGraph if needed)\n", + " graph = nx.DiGraph()\n", + "\n", + " # Nodes ← Entities\n", + " node_name_from_id: dict[int, str] = {}\n", + " for entity in kg.entities:\n", + " node_name, display_name = get_node_and_display_names_for_entity(entity)\n", + " node_name_from_id[entity.id] = node_name\n", + " graph.add_node(node_name, name=display_name)\n", + "\n", + " # Edges ← Relationships\n", + " for relationship in kg.relationships:\n", + " source_node = node_name_from_id.get(relationship.source_id, \"\")\n", + " target_node = node_name_from_id.get(relationship.target_id, \"\")\n", + " if not source_node or not target_node:\n", + " print(f\"❌ Skipping relationship due to empty node:\\n{relationship}\")\n", + " continue\n", + " # For simplicity, each link has the same weight\n", + " # For better community detection, define your own weight mappings (e.g., family members have stronger bonds)\n", + " weight = 1\n", + " edge_label = relationship.link\n", + " if graph.has_edge(source_node, target_node):\n", + " existing_data = graph[source_node][target_node]\n", + " existing_data[\"link\"] += f\"\\n{edge_label}\"\n", + " existing_data[\"weight\"] += weight\n", + " else:\n", + " graph.add_edge(source_node, target_node, link=edge_label, weight=weight)\n", + "\n", + " if remove_orphan_nodes:\n", + " graph.remove_nodes_from(list(nx.isolates(graph)))\n", + "\n", + " return graph\n", + "\n", + "\n", + "def get_node_and_display_names_for_entity(entity: Entity) -> tuple[str, str]:\n", + " snake_case_name = \"_\".join(map(str.lower, entity.name.split()))\n", + " node_name = f\"{entity.id}_{snake_case_name}\"\n", + "\n", + " display_name = entity.name\n", + " if MULTILINE_NAMES:\n", + " display_name = \"\\n\".join(textwrap.wrap(display_name, width=MULTILINE_CHARS))\n", + "\n", + " return node_name, display_name\n", + "\n", + "\n", + "def color_gen(color_count: int) -> Iterator[str]:\n", + " B50, R50, Y50, G50 = (\"#4285F4\", \"#EA4335\", \"#FBBC04\", \"#34A853\")\n", + " B20, R20, Y20, G20 = (\"#AECBFA\", \"#F6AEA9\", \"#FDE293\", \"#A8DAB5\")\n", + " B05, R05, Y05, G05 = (\"#E8F0FE\", \"#FCE8E6\", \"#FEF7E0\", \"#E6F4EA\")\n", + " COLORS = [B50, R50, Y50, G50, B20, R20, Y20, G20, B05, R05, Y05, G05]\n", + " for i in range(color_count):\n", + " yield COLORS[i % len(COLORS)]\n", + "\n", + "\n", + "Node = str\n", + "Positions = dict[Node, np.ndarray]\n", + "Community = set[Node]\n", + "Communities = list[Community]\n", + "Nodes = list[Node]\n", + "INTER_COMMUNITY_EDGE_COLOR = \"#8888\"\n", + "\n", + "\n", + "def init_graph_data(graph: nx.Graph) -> Nodes:\n", + " def node_centrality(node: Node) -> float:\n", + " return graph.nodes[node][NODE_CENTRALITY]\n", + "\n", + " def community_max_centrality(community: Community) -> float:\n", + " return max(node_centrality(node) for node in community)\n", + "\n", + " def nodes_sorted_by_community(communities: Communities) -> Nodes:\n", + " entities = []\n", + " for community in communities:\n", + " sorted_entities = sorted(community, key=node_centrality, reverse=True)\n", + " entities.extend(sorted_entities)\n", + " return entities\n", + "\n", + " # Node centralities\n", + " centralities = nx.betweenness_centrality(graph, endpoints=True)\n", + " for node_key in graph.nodes:\n", + " graph.nodes[node_key][NODE_CENTRALITY] = centralities[node_key]\n", + "\n", + " # Communities\n", + " communities = cast(Communities, louvain_communities(graph, seed=42))\n", + " sorted_communities = sorted(communities, key=community_max_centrality, reverse=True)\n", + "\n", + " # Community colors\n", + " community_count = len(sorted_communities)\n", + " community_colors = list(color_gen(community_count))\n", + "\n", + " # Node community indices and colors\n", + " for community_index, community in enumerate(sorted_communities):\n", + " for node_key in community:\n", + " node = graph.nodes[node_key]\n", + " node[NODE_COMMUNITY_INDEX] = community_index\n", + " node[NODE_COLOR] = community_colors[community_index]\n", + "\n", + " # Edge colors\n", + " for node_key_i, node_key_j, edge_data in graph.edges(data=True):\n", + " node_i = graph.nodes[node_key_i]\n", + " node_j = graph.nodes[node_key_j]\n", + " same_community = node_i[NODE_COMMUNITY_INDEX] == node_j[NODE_COMMUNITY_INDEX]\n", + " edge_data[EDGE_COLOR] = (\n", + " node_i[NODE_COLOR] if same_community else INTER_COMMUNITY_EDGE_COLOR\n", + " )\n", + "\n", + " return nodes_sorted_by_community(sorted_communities)\n", + "\n", + "\n", + "def compute_node_positions(graph: nx.DiGraph, entities: Nodes) -> Positions:\n", + " # Derive an undirected graph (preserving the entity order)\n", + " undirected_graph = nx.Graph()\n", + " for entity in entities:\n", + " undirected_graph.add_node(entity)\n", + " undirected_graph.add_edges_from(graph.edges())\n", + "\n", + " if len(entities) < 10:\n", + " positions = nx.circular_layout(undirected_graph)\n", + " else:\n", + " positions = nx.kamada_kawai_layout(undirected_graph)\n", + " positions = nx.arf_layout(undirected_graph, positions, seed=42)\n", + "\n", + " return positions\n", + "\n", + "\n", + "@dataclass\n", + "class GraphData:\n", + " knowledge_graph: KnowledgeGraph\n", + " remove_orphan_nodes: bool = True\n", + " graph: nx.DiGraph = field(init=False)\n", + " nodes: Nodes = field(init=False)\n", + " positions: Positions = field(init=False)\n", + "\n", + " def __post_init__(self) -> None:\n", + " self.graph = build_graph(self.knowledge_graph, self.remove_orphan_nodes)\n", + " self.nodes = init_graph_data(self.graph)\n", + " self.positions = compute_node_positions(self.graph, self.nodes)\n", + "\n", + "\n", + "print(\"✅ Network graph helpers defined\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "eHZOgBWowu4Q" + }, + "source": [ + "Let's test this:\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "Ti6xC5Zswu4Q" + }, + "outputs": [], + "source": [ + "graph_data = GraphData(knowledge_graph)\n", + "\n", + "print(f\"{graph_data.graph = !s}\")\n", + "print(f\"{graph_data.nodes = }\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "Q-pgpVD3BQyi" + }, + "source": [ + "---\n", + "\n", + "### 🎹 Data visualization\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "aS_KWTKLrYeV" + }, + "source": [ + "The extracted data is much easier to understand when you can actually see it! We can use `matplotlib` to draw our network graphs. We'll size the nodes based on their centrality and color-code them by community. To make it even easier to digest, we'll generate an animated sequence highlighting each character's connections one by one
\n", + "\n", + "Let's define our data visualization helpers:\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "5HK-U0aqwu4Q" + }, + "outputs": [], + "source": [ + "# @title {display-mode: \"form\"}\n", + "import typing\n", + "from io import BytesIO\n", + "\n", + "import matplotlib.pyplot as plt\n", + "from IPython import display\n", + "from PIL import Image as PilImage\n", + "from matplotlib.axes import Axes\n", + "from matplotlib.backends.backend_agg import FigureCanvasAgg\n", + "from matplotlib.figure import Figure\n", + "\n", + "\n", + "class AnimationFormat(StrEnum):\n", + " # Matches PIL.Image.Image.format\n", + " WEBP = auto()\n", + " PNG = auto()\n", + " GIF = auto()\n", + "\n", + "\n", + "FIGURE_DPI = 200\n", + "FIGURE_FACTOR = 1.0\n", + "ANIMATION_INTRO_DURATION = 2500\n", + "ANIMATION_FRAME_DURATION = 250\n", + "EDGE_STYLE = \"arc3,rad=0.2\"\n", + "NodeSizes = dict[Node, int]\n", + "\n", + "if running_in_colab_env():\n", + " ANIMATION_FORMAT = AnimationFormat.WEBP\n", + "else:\n", + " ANIMATION_FORMAT = AnimationFormat.GIF\n", + "\n", + "\n", + "def init_figure(title: str, subtitle: str) -> tuple[Figure, Axes]:\n", + " figsize = (16 * FIGURE_FACTOR, 9 * FIGURE_FACTOR)\n", + " fig, ax = plt.subplots(figsize=figsize, dpi=FIGURE_DPI)\n", + " ax.set_title(title, loc=\"left\")\n", + " ax.set_title(subtitle, loc=\"right\")\n", + " ax.axis(\"off\")\n", + " fig.tight_layout(pad=2)\n", + "\n", + " return fig, ax\n", + "\n", + "\n", + "def draw_nodes(graph: nx.Graph, positions: Positions, ax: Axes) -> NodeSizes:\n", + " node_view = graph.nodes(data=True)\n", + " node_sizes = {\n", + " node: max(500, int(10000 * data[NODE_CENTRALITY])) for node, data in node_view\n", + " }\n", + " node_colors = [str(data[NODE_COLOR]) for _, data in node_view]\n", + " border_width = 3.0\n", + "\n", + " nx.draw_networkx_nodes(\n", + " graph,\n", + " pos=positions,\n", + " node_size=list(node_sizes.values()),\n", + " node_color=node_colors,\n", + " alpha=0.95,\n", + " ax=ax,\n", + " linewidths=border_width,\n", + " )\n", + " labels = {node: data.get(\"name\", str(node)) for node, data in node_view}\n", + " nx.draw_networkx_labels(graph, positions, labels=labels, ax=ax)\n", + "\n", + " if len(node_sizes) >= 10:\n", + " # Extend axis limits (default rounding changes them too much otherwise)\n", + " (x_min, x_max), (y_min, y_max) = ax.get_xlim(), ax.get_ylim()\n", + " x_min, x_max = int(x_min - 1.0), int(x_max + 1.0)\n", + " y_min, y_max = int(y_min - 1.0), int(y_max + 1.0)\n", + " ax.set_xlim(x_min, x_max)\n", + " ax.set_ylim(y_min, y_max)\n", + "\n", + " return node_sizes\n", + "\n", + "\n", + "def draw_edges(\n", + " graph: nx.DiGraph,\n", + " positions: Positions,\n", + " node_sizes: NodeSizes,\n", + " ax: Axes,\n", + " *,\n", + " focused_node: Node | None = None,\n", + ") -> None:\n", + " # Draw specific out-edges or all edges\n", + " if focused_node:\n", + " out_edges = graph.edges([focused_node], data=True)\n", + " else:\n", + " out_edges = graph.edges(data=True)\n", + " edge_colors = [data[EDGE_COLOR] for _, _, data in out_edges]\n", + "\n", + " edge_list = [(u, v) for u, v, _ in out_edges]\n", + " ordered_sizes = [node_sizes[n] for n in graph.nodes()]\n", + " nx.draw_networkx_edges(\n", + " graph,\n", + " positions,\n", + " edge_list,\n", + " edge_color=edge_colors,\n", + " style=\":\",\n", + " alpha=0.9,\n", + " arrowstyle=\"-|>\",\n", + " arrowsize=20,\n", + " ax=ax,\n", + " node_size=ordered_sizes,\n", + " connectionstyle=EDGE_STYLE,\n", + " )\n", + "\n", + " edge_labels = {(u, v): data[\"link\"] for u, v, data in out_edges}\n", + " nx.draw_networkx_edge_labels(\n", + " graph,\n", + " positions,\n", + " edge_labels,\n", + " font_size=8,\n", + " font_family=\"monospace\",\n", + " bbox=dict(ec=\"#FFF8\", fc=\"#FFF8\"),\n", + " ax=ax,\n", + " node_size=ordered_sizes, # type: ignore\n", + " connectionstyle=EDGE_STYLE, # type: ignore\n", + " )\n", + "\n", + "\n", + "def display_graph(title: str, subtitle: str, graph_data: GraphData) -> None:\n", + " _, ax = init_figure(title, subtitle)\n", + "\n", + " node_graph = graph_data.graph\n", + " edge_graph = graph_data.graph\n", + " positions = graph_data.positions\n", + " node_sizes = draw_nodes(node_graph, positions, ax)\n", + " draw_edges(edge_graph, positions, node_sizes, ax)\n", + "\n", + " plt.show()\n", + "\n", + "\n", + "def yield_images(\n", + " title: str,\n", + " subtitle: str,\n", + " graph_data: GraphData,\n", + ") -> Iterator[PilImage.Image]:\n", + " fig, ax = init_figure(title, subtitle)\n", + " canvas = FigureCanvasAgg(fig)\n", + "\n", + " positions = graph_data.positions\n", + " node_graph = graph_data.graph\n", + " node_sizes = draw_nodes(node_graph, positions, ax)\n", + " edge_graph = graph_data.graph\n", + "\n", + " for focused_node in [None, *graph_data.nodes]:\n", + " if focused_node is not None:\n", + " draw_edges(edge_graph, positions, node_sizes, ax, focused_node=focused_node)\n", + " canvas.draw()\n", + " image_size = canvas.get_width_height()\n", + " image_bytes = canvas.buffer_rgba()\n", + " yield PilImage.frombytes(\"RGBA\", image_size, image_bytes).convert(\"RGB\")\n", + " plt.close(fig)\n", + "\n", + "\n", + "def generate_animation(\n", + " title: str,\n", + " subtitle: str,\n", + " graph_data: GraphData,\n", + " format: AnimationFormat,\n", + ") -> BytesIO:\n", + " frames = list(yield_images(title, subtitle, graph_data))\n", + " assert len(frames) >= 1\n", + "\n", + " if format == AnimationFormat.GIF:\n", + " # Dither all frames with the same palette to optimize the animation\n", + " # The animation is cumulative, so most colors are in the last frame\n", + " method = PilImage.Quantize.MEDIANCUT\n", + " palettized = frames[-1].quantize(method=method)\n", + " frames = [frame.quantize(method=method, palette=palettized) for frame in frames]\n", + "\n", + " # The animation will be played in a loop: start cycling with the most complete frame\n", + " first_frame = frames[-1]\n", + " next_frames = frames[:-1]\n", + " durations = [ANIMATION_INTRO_DURATION]\n", + " durations += [ANIMATION_FRAME_DURATION] * len(next_frames)\n", + " params: dict[str, typing.Any] = dict(\n", + " save_all=True,\n", + " append_images=next_frames,\n", + " duration=durations,\n", + " loop=0,\n", + " )\n", + " match format:\n", + " case AnimationFormat.GIF:\n", + " params.update(optimize=False)\n", + " case AnimationFormat.PNG:\n", + " params.update(optimize=True)\n", + " case AnimationFormat.WEBP:\n", + " params.update(lossless=True)\n", + "\n", + " image_io = BytesIO()\n", + " first_frame.save(image_io, str(format).upper(), **params)\n", + "\n", + " return image_io\n", + "\n", + "\n", + "def display_graph_animation(title: str, subtitle: str, graph_data: GraphData) -> None:\n", + " image_io = generate_animation(title, subtitle, graph_data, ANIMATION_FORMAT)\n", + " ipython_image = display.Image(data=image_io.getvalue())\n", + " display.display(ipython_image)\n", + "\n", + "\n", + "def get_graph_title_and_subtitle(\n", + " source: Source | str,\n", + " model: Model | None = None,\n", + " domain: str = \"\",\n", + ") -> tuple[str, str]:\n", + " if isinstance(source, str):\n", + " source_name = f'\"{source.strip()[:20]}
\"'\n", + " else:\n", + " source_name = source.name\n", + " model_id = model.value if model else Model.DEFAULT.value\n", + " model_id = model_id.removesuffix(\"-preview\")\n", + " separator = \" ‱ \"\n", + " title_parts = [\"Knowledge Graph\"]\n", + " if domain:\n", + " title_parts.append(domain)\n", + " subtitle_parts = [source_name, model_id]\n", + " return separator.join(title_parts), separator.join(subtitle_parts)\n", + "\n", + "\n", + "def display_knowledge_graph(\n", + " knowledge_graph: KnowledgeGraph,\n", + " source: Source | str,\n", + " model: Model | None = None,\n", + " animated: bool = False,\n", + " domain: str = \"\",\n", + " remove_orphan_nodes: bool = True,\n", + ") -> None:\n", + " if not knowledge_graph.entities:\n", + " return\n", + " title, subtitle = get_graph_title_and_subtitle(source, model, domain)\n", + " graph_data = GraphData(knowledge_graph, remove_orphan_nodes)\n", + "\n", + " if animated:\n", + " display_graph_animation(title, subtitle, graph_data)\n", + " else:\n", + " display_graph(title, subtitle, graph_data)\n", + "\n", + "\n", + "def extract_knowledge_graph(\n", + " data_schema: str,\n", + " instructions: str,\n", + " source: Source | str,\n", + " model: Model | None = None,\n", + " *,\n", + " domain: str = \"\",\n", + " animated: bool = False,\n", + ") -> None:\n", + " if isinstance(source, Source):\n", + " display_input_data_caption(source)\n", + " knowledge_graph = generate_knowledge_graph(\n", + " data_schema,\n", + " instructions,\n", + " source,\n", + " model=model,\n", + " )\n", + " display_knowledge_graph(knowledge_graph, source, model, animated, domain)\n", + "\n", + "\n", + "print(\"✅ Data visualization helpers defined\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "cqFoT8eABQyl" + }, + "source": [ + "Let's test this:\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "UVerSJcQBQyl" + }, + "outputs": [], + "source": [ + "display_knowledge_graph(knowledge_graph, text)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "oSrGnlGirYeV" + }, + "source": [ + "💡 We can now quickly visualize and understand our knowledge graphs. This helps us iterate faster when refining prompts.\n", + "\n", + "â„č While this simple approach is great for a quick overview, you might want to swap it out for more specialized libraries if you want to explore the graph interactively.\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "3-mWOis7wu4Q" + }, + "source": [ + "---\n", + "\n", + "## ✅ Challenge completed\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "f0075432ee45" + }, + "source": [ + "Let's define a book analysis function:\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "c47KS08drYeV" + }, + "outputs": [], + "source": [ + "def analyze_book(\n", + " source: Source,\n", + " model: Model | None = None,\n", + " animated: bool = False,\n", + ") -> None:\n", + " extract_knowledge_graph(\n", + " BOOK_ANALYSIS_DATA_SCHEMA,\n", + " BOOK_ANALYSIS_INSTRUCTIONS,\n", + " source,\n", + " model,\n", + " domain=\"Character Connections\",\n", + " animated=animated,\n", + " )" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "24cbaef26a3b" + }, + "source": [ + "đŸ§Ș First, let's build a knowledge graph based on Zola's _ThĂ©rĂšse Raquin_:\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "zJ7KVZjrrYeV" + }, + "outputs": [], + "source": [ + "analyze_book(Classic.fr_zola_thĂ©rĂšse_raquin)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "be347a0796d1" + }, + "source": [ + "💡 We can extract and understand the book's narrative in seconds: The love triangle between ThĂ©rĂšse, Camille, and Laurent clearly stands out. Despite being over 200 pages long (and 100k+ tokens), this novel is incredibly minimalistic, revolving around a limited set of characters, which is reflected in the network graph. Adding locations to the extracted entities would also reveal the claustrophobic atmosphere of the book in the resulting knowledge graph.\n", + "\n", + "â„č We used the original French version with English instructions, which works seamlessly. If you translate the instructions, you can also generate dynamic relationship links in different languages (see the [100+ supported languages](https://docs.cloud.google.com/vertex-ai/generative-ai/docs/models#expandable-1)).\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "3dd1f59a87ff" + }, + "source": [ + "đŸ§Ș Let's see how the model handles the interconnected cast of Hugo's _Les MisĂ©rables_:\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "_iM5C6Lnwu4Q" + }, + "outputs": [], + "source": [ + "analyze_book(\n", + " Classic.en_hugo_les_misĂ©rables,\n", + " model=Model.GEMINI_3_5_FLASH,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "3513c2092b91" + }, + "source": [ + "💡 We quickly get the gist of the novel.\n", + "\n", + "â„č Donald Knuth famously used _Les MisĂ©rables_ as an example back in 1994 (see the [Stanford GraphBase](https://www-cs-faculty.stanford.edu/~knuth/sgb.html)). It's been consistently used in Natural Language Processing (NLP) because of its massive scale, linguistic complexity, and multiple high-quality translations. At 800k+ tokens, this book is still a true stress test. Note that we used Gemini 3.5 Flash (instead of 3.1 Flash-Lite). Larger models can infer more and perform deeper consolidation.\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "2ff0fc33592a" + }, + "source": [ + "đŸ§Ș Now, let's process just the first volume of _Le Comte de Monte-Cristo_ (in French):\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "ikWHgVgYwu4R" + }, + "outputs": [], + "source": [ + "analyze_book(Classic.fr_dumas_comte_de_monte_cristo_1)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "8ae553261079" + }, + "source": [ + "💡 The graph shows the initial setup of Edmond DantĂšs' world, his friends, and his betrayers. The love triangle between Edmond, MercĂ©dĂšs, and Fernand also comes to light.\n", + "\n", + "â„č Note that the antagonist is referred to only as _Fernand_, which is expected. We learn his full name, _Fernand Mondego_, only in volume three. In the prompt, we asked the model to \"extract\" entities and determine names \"from the input data\" to avoid generating memorized knowledge. To detect regressions when updating the prompt, we can use this as a unit test to ensure that our response actually provides extracted data and not memorized information.\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "f1f5c82e4b45" + }, + "source": [ + "đŸ§Ș And finally, let's analyze the entire saga of _Le Comte de Monte-Cristo_, all four volumes at once (800k+ tokens):\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "vUdGXxF1s9mc" + }, + "outputs": [], + "source": [ + "analyze_book(\n", + " Collection.fr_dumas_comte_de_monte_cristo,\n", + " model=Model.GEMINI_3_5_FLASH,\n", + " animated=True,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "3592b815c653" + }, + "source": [ + "💡 The multiple aliases of Edmond DantĂšs (and other characters) are nicely extracted. The complex plot of this book is built on characters juggling multiple identities in a psychological chess match.\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "8eb80869f13f" + }, + "source": [ + "🎉 This is an example of how we can complete the challenge in a single request, using the fewest tokens possible and zero thinking tokens, while providing multiple levels of consolidation.\n", + "\n", + "â„č Note that this is a proof of concept. For an exhaustive extraction in a fully professional solution, we would probably set up a multi-step workflow and consider the following:\n", + "\n", + "- As a preliminary step, cache the input data (cached tokens get a 90% discount; learn more about [context caching](https://docs.cloud.google.com/gemini-enterprise-agent-platform/models/context-cache/context-cache-overview)).\n", + "- In the first request, based on the cached content, focus on extracting the entities (possibly extending the entity labels with `location`, `date`
), and then save them to a database.\n", + "- In the second request, based on the cached content and the entity table, focus on extracting the stated relationships using additional fields (such as `evidence` or `excerpt` to serve as direct proof, or `chapter` or `page` for source attribution). In this step, we could also dig deeper and differentiate literal/figurative relationships (e.g., biological vs. figurative parents).\n", + "- In subsequent requests, focus on additional extractions (e.g., interactions between entities) or specific consolidations (e.g., adding inverse relationships, classifying the relationships or interactions into categories).\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "0cgofTAtrYeW" + }, + "source": [ + "---\n", + "\n", + "## 🔭 Generalization\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "M85CyOfErYeW" + }, + "source": [ + "With our current setup, generalizing to other types of content is as simple as adapting our data schema and instructions.\n", + "\n", + "For example, legal agreements are among the densest types of documents. They're usually made up of many articles and clauses, with every sentence carrying specific legal weight, outlining obligations, or providing definitions. But what kind of knowledge graph do we want to build from a legal agreement?\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "144WP1aXrYeW" + }, + "source": [ + "đŸ§Ș Let's test a minimalistic, open-ended prompt:\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "Hy5BBEHBrYeW" + }, + "outputs": [], + "source": [ + "source = Document.en_pharma_dev_agreement\n", + "model = Model.GEMINI_3_5_FLASH\n", + "\n", + "AGREEMENT_OPEN_DATA_SCHEMA = \"\"\"\n", + "Entity:\n", + "- `id`: Unique integer identifier (0, 1, 2
).\n", + "- `name`: Name of the entity.\n", + "- `label`: Entity type.\n", + "\n", + "Relationship:\n", + "- `source_id`: `id` of the subject entity.\n", + "- `link`: `snake_case` predicate.\n", + "- `target_id`: `id` of the object entity.\n", + "\"\"\"\n", + "\n", + "AGREEMENT_OPEN_INSTRUCTIONS = \"\"\"\n", + "Perform a comprehensive, highly-granular entity/relationship extraction.\n", + "\"\"\"\n", + "\n", + "extract_knowledge_graph(\n", + " AGREEMENT_OPEN_DATA_SCHEMA,\n", + " AGREEMENT_OPEN_INSTRUCTIONS,\n", + " source,\n", + " model,\n", + " domain=\"Agreement High-Level Extraction\",\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "IFOAPeKRrYeW" + }, + "source": [ + "💡 Remarks\n", + "\n", + "- Notice how we just passed a PDF directly! Gemini processes the document natively, automatically extracting text and images while performing OCR along the way.\n", + "- This minimalistic prompt offers the greatest degree of freedom, as the model can't guess exactly what data you want or how you plan to use it. As a result, only high-level entities and relationships are extracted, providing a nice summary of the agreement.\n", + "- LLMs are trained to synthesize information. With such highly open-ended instructions, the default behavior helps avoid generating unnecessary tokens.\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "dzkEGUmurYeW" + }, + "source": [ + "đŸ§Ș Then, let's try semi-open instructions focusing on legal obligations:\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "ILxuNknVrYeW" + }, + "outputs": [], + "source": [ + "class AgreementEntityLabel(StrEnum):\n", + " PARTY = auto()\n", + " PERSON = auto()\n", + " ROLE = auto()\n", + " LOCATION = auto()\n", + " JURISDICTION = auto()\n", + " FINANCIAL_AMOUNT = auto()\n", + " DATE = auto()\n", + " EVENT = auto()\n", + " ASSET = auto()\n", + " PRODUCT = auto()\n", + " INTELLECTUAL_PROPERTY = auto()\n", + " OBLIGATION_TYPE = auto()\n", + "\n", + "\n", + "AGREEMENT_SEMI_OPEN_DATA_SCHEMA = f\"\"\"\n", + "Entity:\n", + "- `id`: Unique integer identifier (0, 1, 2
).\n", + "- `name`: Name of the entity.\n", + "- `label`: {pipe_delimited_union(AgreementEntityLabel)}.\n", + "\n", + "Relationship: Obligation, right, or transfer from a source entity to a target entity.\n", + "- `source_id`: The `id` of the subject entity.\n", + "- `link`: Specific `snake_case` predicate.\n", + "- `target_id`: The `id` of the object entity.\n", + "\"\"\"\n", + "\n", + "AGREEMENT_SEMI_OPEN_INSTRUCTIONS = \"\"\"\n", + "- Analyze the input data for every covenant (e.g., \"shall\", \"will\", \"must\", \"is obligated to\") and perform an exhaustive extraction.\n", + "- Make sure to deconstruct complex obligations: For complex clauses (e.g., \"A shall pay B $X Million within Y days of the Effective Date\"), extract:\n", + " - The primary obligation: `[A] is_obligated_to_pay [B]`\n", + " - The value: `[A's obligation] subject_to [$X Million]`\n", + " - The trigger: `[A's obligation] triggered_by [Effective Date]`\n", + "\"\"\"\n", + "\n", + "extract_knowledge_graph(\n", + " AGREEMENT_SEMI_OPEN_DATA_SCHEMA,\n", + " AGREEMENT_SEMI_OPEN_INSTRUCTIONS,\n", + " source,\n", + " model,\n", + " domain=\"Agreement Obligations\",\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "QKWA6dnGrYeW" + }, + "source": [ + "💡 Remarks\n", + "\n", + "- This semi-open prompt lists the types of entities to extract, which naturally increases the number of extracted entities and their specificity.\n", + "- The resulting graph is denser and reflects the legal complexity of the document, rather than just giving us a high-level summary.\n", + "- The extracted relationships are still rather high-level due to how open-ended this part of our prompt is. For such specific extractions, it's possible to extend the entity and relationship data schemas with additional fields, or even to request specific tabular outputs.\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "947tZMu9rYeW" + }, + "source": [ + "What if we don't care about legal obligations, but rather the document's architecture? Let's shift our focus to the structure itself and extract how sections, clauses, and defined terms are hierarchically organized
\n", + "\n", + "đŸ§Ș And now, let's test closed instructions focusing on the document structure:\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "upnfalI_rYeW" + }, + "outputs": [], + "source": [ + "class AgreementStructureEntityLabel(StrEnum):\n", + " DEFINED_TERM = auto()\n", + " DOCUMENT_SECTION = auto()\n", + " DOCUMENT = auto()\n", + "\n", + "\n", + "class AgreementStructureRelationshipType(StrEnum):\n", + " DEFINED_IN = auto()\n", + " CONTAINS = auto()\n", + "\n", + "\n", + "AGREEMENT_STRUCTURAL_DATA_SCHEMA = f\"\"\"\n", + "Entity:\n", + "- `id`: Unique integer identifier (0, 1, 2
).\n", + "- `name`: Name of the entity.\n", + "- `label`: {pipe_delimited_union(AgreementStructureEntityLabel)}.\n", + "\n", + "Relationship: Connection from a source entity to a target entity.\n", + "- `source_id`: The `id` of the subject entity.\n", + "- `link`: {pipe_delimited_union(AgreementStructureRelationshipType)}.\n", + "- `target_id`: The `id` of the object entity.\n", + "\"\"\"\n", + "\n", + "AGREEMENT_STRUCTURAL_OPEN_INSTRUCTIONS = \"\"\"\n", + "- Extract every distinct entity that matches an allowed `label`.\n", + "- Extract every distinct relationship representing a structural connection (hierarchical organization) between these entities:\n", + " - You must be comprehensive and highly granular. If multiple distinct relationships exist between the same pair of entities, create a separate entry for each.\n", + "\"\"\"\n", + "\n", + "extract_knowledge_graph(\n", + " AGREEMENT_STRUCTURAL_DATA_SCHEMA,\n", + " AGREEMENT_STRUCTURAL_OPEN_INSTRUCTIONS,\n", + " source,\n", + " model,\n", + " domain=\"Agreement Structure\",\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "857e79d2b5ae" + }, + "source": [ + "💡 If you extract hundreds of entities from a massive document, your graph will quickly turn into an unreadable hairball. For larger datasets, you'll want to export your nodes and edges to a dedicated graph database, which typically comes with its own visualization and exploration tools.\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "Nx3dioUE6PnJ" + }, + "source": [ + "---\n", + "\n", + "## 💡 Analyze your documents\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "JDdluSOn6PnJ" + }, + "outputs": [], + "source": [ + "class MyDocs(Source):\n", + " # Classic books\n", + " en_lewis_carroll_alice = project_gutenberg_txt_url(11)\n", + " de_goethe_die_leiden_des_jungen_werther = project_gutenberg_txt_url(2407)\n", + " de_fontane_effi_briest = project_gutenberg_txt_url(5323)\n", + "\n", + " # Local files\n", + " # my_local_file = local_file(\"my_file.ext\")\n", + "\n", + " # Online files\n", + " # my_online_file = \"https://path/to/my_file.ext\"\n", + "\n", + "\n", + "MY_DATA_SCHEMA = \"\"\"\n", + "- Entity:\n", + " - `id`: Unique integer identifier (0, 1, 2
).\n", + " - `name`: Name of the entity.\n", + " - `label`\n", + "- Relationship: Connection from a source entity to a target entity.\n", + " - `source_id`: `id` of the subject entity.\n", + " - `link`: `snake_case` predicate describing the relationship.\n", + " - `target_id`: `id` of the object entity.\n", + "\"\"\"\n", + "\n", + "MY_INSTRUCTIONS = \"\"\"\n", + "Perform an entity and relationship extraction.\n", + "\"\"\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "X4wc6JXXrYeX" + }, + "outputs": [], + "source": [ + "# analyze_book(MyDocs.en_lewis_carroll_alice)\n", + "\n", + "# extract_knowledge_graph(MY_DATA_SCHEMA, MY_INSTRUCTIONS, MyDocs.my_local_file)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "yWMUeAYfBQym" + }, + "source": [ + "---\n", + "\n", + "## 🏁 Conclusion\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "Q1lyY001BQym" + }, + "source": [ + "We successfully extracted data and built knowledge graphs from documents by following these steps:\n", + "\n", + "- Prototyping with open prompts to develop an intuition for the model's natural strengths\n", + "- Crafting increasingly specific prompts using a tabular-extraction strategy\n", + "- Structuring our inputs to move towards production-ready and generalizable code\n", + "- Structuring and optimizing our outputs for faster and cheaper generation\n", + "- Adding data visualization for easier interpretation of responses and smoother iterations\n", + "- Conducting more tests, iterating, and enriching the extracted data\n", + "\n", + "These principles apply to many other data-extraction domains and will allow you to solve your own complex problems. Have fun and happy solving!\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "8GNUerw6BQym" + }, + "source": [ + "---\n", + "\n", + "## ➕ More!\n", + "\n", + "- Explore typical use cases in the [Agent Platform Prompt Gallery](https://console.cloud.google.com/agent-platform/studio/prompt-gallery)\n", + "- Stay updated with the [Agent Platform Release Notes](https://docs.cloud.google.com/gemini-enterprise-agent-platform/release-notes)\n" + ] + } + ], + "metadata": { + "colab": { + "name": "knowledge_graph_generation.ipynb", + "toc_visible": true + }, + "kernelspec": { + "display_name": "Python 3", + "name": "python3" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} diff --git a/gemini/use-cases/kyc/kyc-with-grounding.ipynb b/gemini/use-cases/kyc/kyc-with-grounding.ipynb new file mode 100644 index 0000000..ff46267 --- /dev/null +++ b/gemini/use-cases/kyc/kyc-with-grounding.ipynb @@ -0,0 +1,951 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "ur8xi4C7S06n" + }, + "outputs": [], + "source": [ + "# Copyright 2025 Google LLC\n", + "#\n", + "# Licensed under the Apache License, Version 2.0 (the \"License\");\n", + "# you may not use this file except in compliance with the License.\n", + "# You may obtain a copy of the License at\n", + "#\n", + "# https://www.apache.org/licenses/LICENSE-2.0\n", + "#\n", + "# Unless required by applicable law or agreed to in writing, software\n", + "# distributed under the License is distributed on an \"AS IS\" BASIS,\n", + "# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n", + "# See the License for the specific language governing permissions and\n", + "# limitations under the License." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "JAPoU8Sm5E6e" + }, + "source": [ + "# Know Your Customer Use Case - Gemini Grounding with Google Search \n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + "
    \n", + " \n", + " \"Google
    Open in Colab\n", + "
    \n", + "
    \n", + " \n", + " \"Google
    Open in Colab Enterprise\n", + "
    \n", + "
    \n", + " \n", + " \"Vertex
    Open in Vertex AI Workbench\n", + "
    \n", + "
    \n", + " \n", + " \"GitHub
    View on GitHub\n", + "
    \n", + "
    \n", + "\n", + "
    \n", + "\n", + "Share to:\n", + "\n", + "\n", + " \"LinkedIn\n", + "\n", + "\n", + "\n", + " \"Bluesky\n", + "\n", + "\n", + "\n", + " \"X\n", + "\n", + "\n", + "\n", + " \"Reddit\n", + "\n", + "\n", + "\n", + " \"Facebook\n", + "" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "84f0f73a0f76" + }, + "source": [ + "| Author |\n", + "| --- |\n", + "| [Lukas Geiger](https://github.com/ljogeiger) |" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "tvgnzT1CKxrO" + }, + "source": [ + "## Overview\n", + "\n", + "This notebook demonstrates how to use the Gemini API (specifically the `gemini-2.5-flash` model) to find and summarize negative news articles related to a specified entity. The entity can be a person, company, or ship. The notebook leverages Google Search as a tool for the Gemini API to ground its responses in real-world information.\n", + "\n", + "You will learn how to:\n", + "* Configure the Gemini API client.\n", + "* Define a detailed system instruction to guide the model's behavior.\n", + "* Craft a prompt that incorporates an input entity.\n", + "* Use Google Search as a grounding tool for the model.\n", + "* Process the model's response to extract the generated text and grounding metadata (sources).\n", + "* Evaluate the responses using GCP's Evaluation Framework and create custom metrics.\n", + "\n", + "## Use Case Definition\n", + "\"Know Your Customer\" (KYC) is a crucial due diligence process used by businesses, particularly in regulated industries, to verify the identity of their clients and assess potential risks associated with doing business with them. The primary goal of KYC is to prevent financial crimes like money laundering, terrorist financing, and fraud.\n", + "\n", + "For example, imagine you are conducting interviews for a board seat. As part of this process you might choose to do a background check to evaluate whether or not the candidate has been involved in any illegal activities. Then based on the report received you can make a more informed decision whether to proceed with the candidate or not. This can be applied to many other use cases and across many entity types (companies, people, vessels, governments, etc.)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "61RBz8LLbxCR" + }, + "source": [ + "## Get started" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "No17Cw5hgx12" + }, + "source": [ + "### Install Google Gen AI SDK and other required packages\n", + "The following command installs the Google Generative AI SDK, which is necessary to interact with the Gemini API." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "tFy3H3aPgx12" + }, + "outputs": [], + "source": [ + "%pip install --upgrade --quiet google-genai" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "dmWOrTJ3gx13" + }, + "source": [ + "### Authenticate your notebook environment (Colab only)\n", + "\n", + "If you're running this notebook on Google Colab, run the cell below to authenticate your environment. This allows the notebook to access Google Cloud services." + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": { + "id": "NyKGtVQjgx13" + }, + "outputs": [], + "source": [ + "import sys\n", + "\n", + "if \"google.colab\" in sys.modules:\n", + " from google.colab import auth\n", + "\n", + " auth.authenticate_user()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "c19f5974a642" + }, + "source": [ + "### Authenticate your notebook environment" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "1307a625f802" + }, + "outputs": [], + "source": [ + "!gcloud auth application-default login" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "DF4l8DTdWgPY" + }, + "source": [ + "### Set Google Cloud project information and Initialize API Client\n", + "\n", + "To get started using Vertex AI (which hosts the Gemini models used here), you must have an existing Google Cloud project and [enable the Vertex AI API](https://console.cloud.google.com/flows/enableapi?apiid=aiplatform.googleapis.com).\n", + "\n", + "Learn more about [setting up a project and a development environment](https://cloud.google.com/vertex-ai/docs/start/cloud-environment).\n", + "\n", + "This cell also initializes the `genai.Client` which will be used to interact with the Gemini API." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "Nqwi-5ufWp_B" + }, + "outputs": [], + "source": [ + "import os\n", + "\n", + "from google import genai\n", + "\n", + "PROJECT_ID = \"[your-project-id]\" # @param {type: \"string\", placeholder: \"[your-project-id]\", isTemplate: true}\n", + "if not PROJECT_ID or PROJECT_ID == \"[your-project-id]\":\n", + " # Attempt to get project ID from environment variable if not set by user\n", + " PROJECT_ID = str(os.environ.get(\"GOOGLE_CLOUD_PROJECT\"))\n", + " if not PROJECT_ID or PROJECT_ID == \"None\":\n", + " raise ValueError(\"Please set your Google Cloud Project ID.\")\n", + "print(f\"Using Project ID: {PROJECT_ID}\")\n", + "\n", + "LOCATION = os.environ.get(\n", + " \"GOOGLE_CLOUD_REGION\", \"us-central1\"\n", + ") # Default to us-central1 if not set\n", + "print(f\"Using Location: {LOCATION}\")\n", + "\n", + "# Initialize the Google Gen AI client\n", + "client = genai.Client(vertexai=True, project=PROJECT_ID, location=LOCATION)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "5303c05f7aa6" + }, + "source": [ + "### Import libraries\n", + "Import necessary libraries, including `google.genai.types` for defining specific configurations for the API call, and `IPython.display` for better rendering of markdown in the notebook." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": { + "id": "6fc324893334" + }, + "outputs": [], + "source": [ + "import json # Though not used in the final script, often useful for handling API responses\n", + "\n", + "from IPython.display import Markdown, display\n", + "from google.genai import types" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "EdvJRUWRNGHE" + }, + "source": [ + "## Generating Negative News Reports with Gemini" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "e43229f3ad4f" + }, + "source": [ + "### Load model\n", + "Specify the model ID to be used. We are using `gemini-2.5-flash`, a fast and versatile model with reasoning capabilities." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": { + "id": "cf93d5f0ce00" + }, + "outputs": [], + "source": [ + "MODEL_ID = \"gemini-2.5-flash\" # @param {type:\"string\"}" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "new_markdown_cell_prompt_system" + }, + "source": [ + "### Define Prompt Template and System Instructions\n", + "\n", + "**System Instructions:** These provide high-level guidance to the model on its role, the desired output format, and steps to follow. We instruct the model to act as a professional report generator for negative news, to search thoroughly, cite dates, and handle cases where no negative news is found. A list of specific activities is provided to focus the search.\n", + "\n", + "**Prompt Template:** This is the specific query sent to the model for each entity. It includes a placeholder `{entity}` which will be filled in with the actual entity name during execution." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": { + "id": "new_code_cell_prompt_system_def" + }, + "outputs": [], + "source": [ + "prompt_template = \"\"\"\n", + "Your task is to provide a comprehensive and professional report of negative news articles for a given input entity. The input entity can be a person, company, or ship.\n", + "\n", + "Input Entity:\n", + "{input_entity}\n", + "\n", + "Activities:\n", + "Money Laundering\n", + "Forgery\n", + "Bribery and Corruption\n", + "Human Trafficking\n", + "\n", + "Follow these steps:\n", + "\n", + "1. If the input entity is a company, map it to its legal business name.\n", + "2. Thoroughly search Google News for negative news articles related to the input entity and the specified activities across all time.\n", + "3. Summarize and interpret the Google Search results for the given input entity and each activity. If there are no results for a given activity, skip it.\n", + "4. For each activity with search results, create a headline that summarizes the event.\n", + "5. Group the search results under the corresponding headline, including the date of each news article.\n", + "6. For person names, strictly follow the entity names.\n", + "7. If there are no negative news articles associated with the input entity, respond with: \"There are no results found for {input_entity}.\"\n", + "\n", + "\n", + "Output Format:\n", + "\n", + "Headline: [Summary of the event]\n", + "Date: [Date of the news article]\n", + "Summary: [Brief summary of the news article]\n", + "\n", + "Example:\n", + "\n", + "Headline: John Doe Accused of Money Laundering\\n\n", + "Date: 2023-01-15\\n\n", + "Summary: John Doe is accused of laundering money through offshore accounts, according to a report by the International Consortium of Investigative Journalists.\n", + "\n", + "Ensure that the report is comprehensive, accurate, and professionally presented.\n", + "\"\"\"\n", + "\n", + "system_instructions_text = \"\"\"\n", + "You are a professional news analyst tasked with providing comprehensive reports on negative news articles related to a given entity. Your reports must be thorough, accurate, and professionally presented.\n", + "\"\"\"" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "new_markdown_cell_helpers" + }, + "source": [ + "### Helper Function to Get Sources\n", + "The `get_sources` function processes the `grounding_metadata` from the model's response. This metadata contains information about the web pages the model used to generate its answer (when using Google Search as a tool). The function extracts titles and URLs for these sources and formats them for display. This is crucial for verifying the information provided by the model." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": { + "id": "new_code_cell_get_sources" + }, + "outputs": [], + "source": [ + "def get_sources(response):\n", + " \"\"\"Return a formatted string of sources corresponding with response citations\n", + "\n", + " Args:\n", + " response: The response from the Gemini API containing grounding metadata\n", + "\n", + " Returns:\n", + " A formatted string containing the sources with their titles and URLs\n", + " \"\"\"\n", + " source_text = \"\\n\\n**Sources:**\\n\"\n", + " if not response.candidates or not response.candidates[0].grounding_metadata:\n", + " return source_text + \"No grounding metadata found.\\n\"\n", + "\n", + " metadata = response.candidates[0].grounding_metadata\n", + " sources = {}\n", + " source_titles = {}\n", + " max_chunk_index = -1\n", + "\n", + " if not metadata.grounding_supports:\n", + " return source_text + \"No grounding supports found in metadata.\\n\"\n", + "\n", + " for support in metadata.grounding_supports:\n", + " for chunk_index in support.grounding_chunk_indices:\n", + " display_chunk_index = chunk_index + 1 # offset 0 list index\n", + " if display_chunk_index > max_chunk_index:\n", + " max_chunk_index = display_chunk_index\n", + " if display_chunk_index not in source_titles and chunk_index < len(\n", + " metadata.grounding_chunks\n", + " ):\n", + " chunk = metadata.grounding_chunks[chunk_index]\n", + " source_titles[display_chunk_index] = chunk.web.title\n", + " sources[display_chunk_index] = chunk.web.uri # Corrected to use uri\n", + " elif chunk_index >= len(metadata.grounding_chunks):\n", + " print(\n", + " f\"Warning: chunk_index {chunk_index} out of bounds for grounding_chunks (len: {len(metadata.grounding_chunks)}).\"\n", + " )\n", + " sorted_source_titles = dict(sorted(source_titles.items()))\n", + "\n", + " if sources:\n", + " for i in sorted_source_titles:\n", + " source_text += f\"[[{i}] {sorted_source_titles[i]}]({sources[i]})\\n\"\n", + " else:\n", + " source_text += \"No sources extracted from grounding metadata.\\n\"\n", + "\n", + " # Debugging information (optional, can be commented out)\n", + " # print(f\"Max Chunk Index: {max_chunk_index}\")\n", + " # print(f\"Length of GroundingChunks: {len(metadata.grounding_chunks)}\")\n", + " return source_text" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "new_markdown_cell_generate_func" + }, + "source": [ + "### Define Generation Function\n", + "The `generate_negative_news_report` function encapsulates the logic for calling the Gemini API. It takes an entity string and the system instructions text as input.\n", + "Key configurations:\n", + "* **`model`**: Uses the `MODEL_ID` defined earlier.\n", + "* **`contents`**: The user prompt, formatted with the specific entity.\n", + "* **`tools`**: Configured to use `types.GoogleSearch()`, enabling the model to perform Google searches to find relevant information.\n", + "* **`generate_content_config`**: \n", + " * `temperature=1`, `top_p=0.95`: These parameters control the randomness and creativity of the output. Higher temperature and top_p values lead to more diverse responses.\n", + " * `max_output_tokens=8192`: Sets the maximum length of the generated response.\n", + " * `response_modalities=[\"TEXT\"]`: Specifies that we expect a text response.\n", + " * **`safety_settings`**: **Important Note:** All harm categories (`HATE_SPEECH`, `DANGEROUS_CONTENT`, `SEXUALLY_EXPLICIT`, `HARASSMENT`) are set to `\"OFF\"`. This is done to ensure the model can retrieve and report on potentially sensitive topics related to negative news. However, in a production environment or for other use cases, you should carefully consider and configure appropriate safety settings based on your application's requirements and responsible AI practices.\n", + " * `system_instruction`: The detailed instructions for the model's task.\n", + "\n", + "The function calls `client.models.generate_content` and returns the API response." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": { + "id": "new_code_cell_generate_def" + }, + "outputs": [], + "source": [ + "def generate_kyc_report_from_entity(\n", + " entity_name: str, system_instructions: str, prompt_template: str\n", + "):\n", + " \"\"\"Generate a KYC report for a given entity using the Gemini API.\n", + "\n", + " Args:\n", + " entity_name: The name of the entity to generate a report for\n", + " system_instructions: The system instructions to guide the model's behavior\n", + " prompt_template: The prompt template to use for the report\n", + " Returns:\n", + " response: The response from the Gemini API containing the generated report\n", + " \"\"\"\n", + " current_prompt = prompt_template.format(input_entity=entity_name)\n", + " contents = [current_prompt]\n", + "\n", + " tools = [\n", + " types.Tool(google_search=types.GoogleSearch()),\n", + " ]\n", + "\n", + " generate_content_config = types.GenerateContentConfig(\n", + " temperature=1,\n", + " top_p=0.95,\n", + " max_output_tokens=8192,\n", + " response_modalities=[\"TEXT\"], # Expect text modality output\n", + " safety_settings=[\n", + " types.SafetySetting(\n", + " category=\"HARM_CATEGORY_HATE_SPEECH\", threshold=\"BLOCK_NONE\"\n", + " ), # Using new enums if applicable, else use \"OFF\"\n", + " types.SafetySetting(\n", + " category=\"HARM_CATEGORY_DANGEROUS_CONTENT\", threshold=\"BLOCK_NONE\"\n", + " ),\n", + " types.SafetySetting(\n", + " category=\"HARM_CATEGORY_SEXUALLY_EXPLICIT\", threshold=\"BLOCK_NONE\"\n", + " ),\n", + " types.SafetySetting(\n", + " category=\"HARM_CATEGORY_HARASSMENT\", threshold=\"BLOCK_NONE\"\n", + " ),\n", + " ],\n", + " tools=tools,\n", + " system_instruction=types.Content(\n", + " parts=[types.Part(text=system_instructions)]\n", + " ), # System instructions should be Content object\n", + " thinking_config=types.ThinkingConfig(\n", + " include_thoughts=True,\n", + " ),\n", + " )\n", + "\n", + " print(f\"\\n--- Generating report for: {entity_name} ---\")\n", + " response = client.models.generate_content(\n", + " model=MODEL_ID, # Fully qualified model name\n", + " contents=contents,\n", + " config=generate_content_config, # Parameter name is generation_config\n", + " )\n", + "\n", + " return response" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "new_markdown_cell_run" + }, + "source": [ + "### Define Entities and Run Analysis\n", + "Define a list of entities for which to generate reports. The code then iterates through this list, calls the `generate_negative_news_report` function for each entity, and displays the model's text response along with the extracted sources. Using `display(Markdown(...))` helps in rendering the output in a more readable format." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "new_code_cell_run_analysis" + }, + "outputs": [], + "source": [ + "entities_to_check = [\n", + " \"Ricardo Martinelli\",\n", + " \"Robert Burke\",\n", + "] # Example entities, you can change or extend this list\n", + "\n", + "\n", + "def generate_kyc_report_from_entity_list(\n", + " entity_list: list[str], system_instructions_text: str, prompt_template: str\n", + "):\n", + " \"\"\"Generate a KYC report for a given entity using the Gemini API.\n", + "\n", + " Args:\n", + " entity_list: The list of entities to generate a report for\n", + " system_instructions: The system instructions to guide the model's behavior\n", + " prompt_template: The prompt template to use for the report\n", + " Returns:\n", + " response: The response from the Gemini API containing the generated report\n", + " \"\"\"\n", + " for entity in entity_list:\n", + " response = generate_kyc_report_from_entity(\n", + " entity, system_instructions_text, prompt_template\n", + " )\n", + "\n", + " # Display the model's text response\n", + " try:\n", + " if response.candidates and len(response.candidates) > 0:\n", + " parts = response.candidates[0].content.parts\n", + " # First display any thought parts\n", + " thought_parts = [\n", + " part for part in parts if hasattr(part, \"thought\") and part.thought\n", + " ]\n", + " if thought_parts:\n", + " display(Markdown(\"**Model Thoughts:**\"))\n", + " for part in thought_parts:\n", + " if hasattr(part, \"text\") and part.text:\n", + " display(Markdown(part.text))\n", + "\n", + " # Then display the final response\n", + " response_parts = [\n", + " part\n", + " for part in parts\n", + " if hasattr(part, \"thought\")\n", + " and not part.thought\n", + " and hasattr(part, \"text\")\n", + " and part.text\n", + " ]\n", + " if response_parts:\n", + " display(Markdown(\"**Model Response:**\"))\n", + " for part in response_parts:\n", + " display(Markdown(part.text))\n", + " else:\n", + " display(\n", + " Markdown(\n", + " \"**Model Response:**\\nNo text content found in response parts.\"\n", + " )\n", + " )\n", + " except Exception as e:\n", + " display(Markdown(f\"**Error displaying response:** {str(e)}\"))\n", + " print(\"Full response object:\", response)\n", + "\n", + " # Get and display grounding information (sources)\n", + " sources_text = get_sources(response)\n", + " display(Markdown(sources_text))\n", + "\n", + " print(\"------------------------------------------------------\")\n", + "\n", + "\n", + "generate_kyc_report_from_entity_list(\n", + " entities_to_check, system_instructions_text, prompt_template\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "c8d94bdf9283" + }, + "source": [ + "## Evaluation\n", + "Add section for evaluating of response using Evaluation Framework" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "37e677bd9264" + }, + "source": [ + "Import necessary libraries" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": { + "id": "f05ee5c78e48" + }, + "outputs": [], + "source": [ + "from IPython.display import Markdown, display\n", + "import pandas as pd\n", + "import plotly.graph_objects as go\n", + "from vertexai.evaluation import EvalTask, PointwiseMetric, PointwiseMetricPromptTemplate" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "64b671ead9cd" + }, + "source": [ + "Define helper functions" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": { + "id": "8567afdb0b28" + }, + "outputs": [], + "source": [ + "def display_eval_result(eval_result, metrics=None):\n", + " \"\"\"Display the evaluation results.\n", + "\n", + " Args:\n", + " eval_result: The evaluation result object containing metrics\n", + " metrics: Optional list of metric names to filter the display\n", + " Returns:\n", + " metrics_df: DataFrame containing summary metrics\n", + " metrics_table: DataFrame containing detailed metrics\n", + " \"\"\"\n", + " summary_metrics, metrics_table = (\n", + " eval_result.summary_metrics,\n", + " eval_result.metrics_table,\n", + " )\n", + "\n", + " metrics_df = pd.DataFrame.from_dict(summary_metrics, orient=\"index\").T\n", + " if metrics:\n", + " metrics_df = metrics_df.filter(\n", + " [\n", + " metric\n", + " for metric in metrics_df.columns\n", + " if any(selected_metric in metric for selected_metric in metrics)\n", + " ]\n", + " )\n", + " metrics_table = metrics_table.filter(\n", + " [\n", + " metric\n", + " for metric in metrics_table.columns\n", + " if any(selected_metric in metric for selected_metric in metrics)\n", + " ]\n", + " )\n", + "\n", + " # Display the summary metrics\n", + " display(Markdown(\"### Summary Metrics\"))\n", + " display(metrics_df)\n", + " # Display the metrics table\n", + " display(Markdown(\"### Row-based Metrics\"))\n", + " display(metrics_table)\n", + "\n", + "\n", + "def plot_bar_plot(eval_results, metrics=None):\n", + " \"\"\"Create a bar plot of evaluation results.\n", + "\n", + " Args:\n", + " eval_results: List of tuples containing (title, summary_metrics, metrics_table)\n", + " metrics: Optional list of metric names to filter the plot\n", + " Returns:\n", + " fig: The bar plot figure\n", + " \"\"\"\n", + " fig = go.Figure()\n", + " data = []\n", + "\n", + " for eval_result in eval_results:\n", + " title, summary_metrics, _ = eval_result\n", + " if metrics:\n", + " summary_metrics = {\n", + " k: summary_metrics[k]\n", + " for k, v in summary_metrics.items()\n", + " if any(selected_metric in k for selected_metric in metrics)\n", + " }\n", + "\n", + " data.append(\n", + " go.Bar(\n", + " x=list(summary_metrics.keys()),\n", + " y=list(summary_metrics.values()),\n", + " name=title,\n", + " )\n", + " )\n", + "\n", + " fig = go.Figure(data=data)\n", + " fig.update_layout(barmode=\"group\")\n", + " fig.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "3f2c996dcc6c" + }, + "source": [ + "### Test Evaluation 1: Response Completeness and Accuracy" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "11ccdd014218" + }, + "outputs": [], + "source": [ + "completeness_accuracy_template = PointwiseMetricPromptTemplate(\n", + " criteria={\n", + " \"category_coverage\": \"The response should cover all relevant negative news categories (Money Laundering, Forgery, Bribery, Human Trafficking) if they exist for the entity.\",\n", + " \"source_citation\": \"The response should properly cite sources for each claim.\",\n", + " },\n", + " rating_rubric={\n", + " \"5\": \"Response covers all relevant categories and properly cites sources.\",\n", + " \"3\": \"Response covers most categories but may miss some citations.\",\n", + " \"1\": \"Response is incomplete or contains inaccuracies.\",\n", + " },\n", + ")\n", + "\n", + "completeness_accuracy_metric = PointwiseMetric(\n", + " metric=\"completeness_accuracy\",\n", + " metric_prompt_template=completeness_accuracy_template,\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "cbb8467d904e" + }, + "outputs": [], + "source": [ + "def generate_ground_truth_data(\n", + " entities: list[str], system_instructions: str, prompt_template: str\n", + ") -> dict[str, list[str]]:\n", + " \"\"\"Generate ground truth data for evaluation using generate_kyc_report_from_entity.\n", + "\n", + " Args:\n", + " entities: List of entity names to generate reports for\n", + " system_instructions: System instructions for the model\n", + " prompt_template: The prompt template to use for the report\n", + "\n", + " Returns:\n", + " Dictionary containing prompts, references, and responses for each entity\n", + " \"\"\"\n", + " ground_truth_data = {\"prompt\": [], \"reference\": [], \"response\": []}\n", + "\n", + " # Process each entity's data\n", + " for entity in entities:\n", + " # Generate the prompt\n", + " prompt = prompt_template.format(input_entity=entity)\n", + " ground_truth_data[\"prompt\"].append(prompt)\n", + "\n", + " # Generate the report\n", + " response = generate_kyc_report_from_entity(\n", + " entity, system_instructions, prompt_template\n", + " )\n", + "\n", + " # Extract the response text\n", + " if response.candidates and len(response.candidates) > 0:\n", + " response_parts = [\n", + " part.text\n", + " for part in response.candidates[0].content.parts\n", + " if hasattr(part, \"text\") and part.text\n", + " ]\n", + " response_text = \"\\n\".join(response_parts)\n", + " else:\n", + " response_text = \"No response generated.\"\n", + "\n", + " ground_truth_data[\"response\"].append(response_text)\n", + "\n", + " # Note: we'll use second run response as reference\n", + " # In a real scenario, you might want to use human-verified references\n", + " ref_response = generate_kyc_report_from_entity(\n", + " entity, system_instructions, prompt_template\n", + " )\n", + "\n", + " # Extract the response text\n", + " if ref_response.candidates and len(ref_response.candidates) > 0:\n", + " ref_response_parts = [\n", + " part.text\n", + " for part in ref_response.candidates[0].content.parts\n", + " if hasattr(part, \"text\") and part.text\n", + " ]\n", + " ref_response_text = \"\\n\".join(ref_response_parts)\n", + " else:\n", + " ref_response_text = \"No response generated.\"\n", + "\n", + " ground_truth_data[\"reference\"].append(ref_response_text)\n", + "\n", + " return ground_truth_data\n", + "\n", + "\n", + "# Example usage:\n", + "entities_to_check = [\n", + " \"Ricardo Martinelli\",\n", + " \"Robert Burke\",\n", + "]\n", + "\n", + "ground_truth_data = generate_ground_truth_data(\n", + " entities_to_check, system_instructions_text, prompt_template\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "794b91041df4" + }, + "outputs": [], + "source": [ + "# Create evaluation dataset\n", + "eval_dataset = pd.DataFrame(ground_truth_data)\n", + "\n", + "# Run evaluation\n", + "eval_task = EvalTask(\n", + " dataset=eval_dataset,\n", + " metrics=[completeness_accuracy_metric],\n", + " experiment=\"kyc-completeness-accuracy\",\n", + ")\n", + "\n", + "eval_result = eval_task.evaluate()\n", + "\n", + "# Display results\n", + "display_eval_result(eval_result)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "2f1bc78ea45c" + }, + "source": [ + "#### Test Evaluation 2: Response Structure and Professionalism" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "cea9aad4cad0" + }, + "outputs": [], + "source": [ + "structure_professionalism_template = PointwiseMetricPromptTemplate(\n", + " criteria={\n", + " \"formatting\": \"The response should follow the specified format with clear headlines, dates, and summaries.\",\n", + " \"professional_tone\": \"The response should maintain a professional and objective tone throughout.\",\n", + " \"clarity\": \"The information should be presented clearly and be easy to understand.\",\n", + " },\n", + " rating_rubric={\n", + " \"5\": \"Response is well-structured, professional, and clear.\",\n", + " \"3\": \"Response has good structure but could be more professional or clearer.\",\n", + " \"1\": \"Response lacks proper structure or professionalism.\",\n", + " },\n", + ")\n", + "\n", + "structure_professionalism_metric = PointwiseMetric(\n", + " metric=\"structure_professionalism\",\n", + " metric_prompt_template=structure_professionalism_template,\n", + ")\n", + "\n", + "# Create evaluation dataset\n", + "eval_dataset = pd.DataFrame(ground_truth_data)\n", + "\n", + "# Run evaluation\n", + "eval_task = EvalTask(\n", + " dataset=eval_dataset,\n", + " metrics=[structure_professionalism_metric],\n", + " experiment=\"kyc-structure-professionalism\",\n", + ")\n", + "\n", + "eval_result = eval_task.evaluate()\n", + "\n", + "# Display results\n", + "display_eval_result(eval_result)\n", + "\n", + "# Create visualization of results\n", + "plot_bar_plot(\n", + " [\n", + " (\n", + " \"Structure and Professionalism\",\n", + " eval_result.summary_metrics,\n", + " eval_result.metrics_table,\n", + " )\n", + " ]\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "2a4e033321ad" + }, + "source": [ + "## Cleaning up\n", + "This notebook primarily makes API calls to Google Gemini models and does not create persistent resources in your Google Cloud project (like VMs, storage buckets, etc.) beyond the API usage itself. Therefore, specific cleanup steps for created resources are generally not required after running this notebook.\n", + "\n", + "If you want to disable the Vertex AI API used, you can do so from the Google Cloud Console, but this would affect any other services or notebooks relying on it." + ] + } + ], + "metadata": { + "colab": { + "name": "kyc-with-grounding.ipynb", + "toc_visible": true + }, + "kernelspec": { + "display_name": "Python 3", + "name": "python3" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} diff --git a/gemini/use-cases/marketing/creative_content_generation.ipynb b/gemini/use-cases/marketing/creative_content_generation.ipynb new file mode 100644 index 0000000..97e8737 --- /dev/null +++ b/gemini/use-cases/marketing/creative_content_generation.ipynb @@ -0,0 +1,851 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "ur8xi4C7S06n" + }, + "outputs": [], + "source": [ + "# Copyright 2024 Google LLC\n", + "#\n", + "# Licensed under the Apache License, Version 2.0 (the \"License\");\n", + "# you may not use this file except in compliance with the License.\n", + "# You may obtain a copy of the License at\n", + "#\n", + "# https://www.apache.org/licenses/LICENSE-2.0\n", + "#\n", + "# Unless required by applicable law or agreed to in writing, software\n", + "# distributed under the License is distributed on an \"AS IS\" BASIS,\n", + "# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n", + "# See the License for the specific language governing permissions and\n", + "# limitations under the License." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "JAPoU8Sm5E6e" + }, + "source": [ + "# Creative Content Generation with Gemini in Vertex AI and Imagen\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + "
    \n", + " \n", + " \"Google
    Run in Colab\n", + "
    \n", + "
    \n", + " \n", + " \"Google
    Run in Colab Enterprise\n", + "
    \n", + "
    \n", + " \n", + " \"GitHub
    View on GitHub\n", + "
    \n", + "
    \n", + " \n", + " \"Vertex
    Open in Vertex AI Workbench\n", + "
    \n", + "
    \n", + "\n", + "
    \n", + "\n", + "Share to:\n", + "\n", + "\n", + " \"LinkedIn\n", + "\n", + "\n", + "\n", + " \"Bluesky\n", + "\n", + "\n", + "\n", + " \"X\n", + "\n", + "\n", + "\n", + " \"Reddit\n", + "\n", + "\n", + "\n", + " \"Facebook\n", + " " + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "9P2TcUplDsBV" + }, + "source": [ + "| | |\n", + "|-|-|\n", + "|Author(s) | [Thu Ya Kyaw](https://github.com/iamthuya)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "24743cf4a1e1" + }, + "source": [ + "**_NOTE_**: This notebook has been tested in the following environment:\n", + "\n", + "* python - 3.11\n", + "* google-cloud-aiplatform - 1.54.0" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "tvgnzT1CKxrO" + }, + "source": [ + "## Overview\n", + "Generative AI is a game-changer for creative professionals in marketing and advertising. It can quickly create eye-catching visuals, write persuasive ad copy, and even come up with fresh ideas for campaigns.\n", + "\n", + "Generative AI helps create targeted campaigns with visuals and copy that can adapt to different audiences and platforms. It can also provide personalized customer experiences by analyzing data to tailor ads to specific people, making them more likely to engage and buy. It can also automate tasks like ideation and optimizing content, saving time and effort.\n", + "\n", + "In this notebook, you will be exploring how to harness the power of Generative AI do perform aforementioned tasks.\n", + "\n", + "### Allowlisting\n", + "\n", + "Imagen's features in this sample notebook requires users to be allowlisted. To request access to use this Imagen feature, fill out the [Imagen on Vertex AI access request form](https://docs.google.com/forms/d/1cqt9padvfMgqn23W5FMPTqh7bW1KLkEOsC5G6uC-uuM/viewform)." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "d975e698c9a4" + }, + "source": [ + "### Objective\n", + "\n", + "In this tutorial, you learn how to:\n", + "\n", + "- Generate creative content from keywords\n", + "- Personalize generated contents for various audience\n", + "- Identify the most suitable visual assets\n", + "- Create visual assets to supercharge the campaign messaging\n", + "\n", + "This tutorial uses the following Google Cloud AI services and resources:\n", + "\n", + "- Gemini API in Vertex AI\n", + "- Imagen on Vertex AI\n", + "\n", + "The step performed includes:\n", + "- Generating content using product name, product description, and product image\n", + "- Product social media post for various platforms such as Facebook, Instagram, LinkedIn, and Twitter\n", + "- Personalize generated content for various professions, different generations, and different languages\n", + "- Use outpainting feature from Imagen to generate images of different aspect ratios" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "BF1j6f9HApxa" + }, + "source": [ + "## Before you begin\n", + "\n", + "### Set up your Google Cloud project\n", + "\n", + "**The following steps are required, regardless of your notebook environment.**\n", + "\n", + "1. [Select or create a Google Cloud project](https://console.cloud.google.com/cloud-resource-manager). When you first create an account, you get a $300 free credit towards your compute/storage costs.\n", + "1. [Make sure that billing is enabled for your project](https://cloud.google.com/billing/docs/how-to/modify-project).\n", + "1. Enable the [Vertex AI API](https://console.cloud.google.com/flows/enableapi?apiid=aiplatform.googleapis.com).\n", + "1. If you are running this notebook locally, you need to install the [Cloud SDK](https://cloud.google.com/sdk)." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "i7EUnXsZhAGF" + }, + "source": [ + "### Installation\n", + "\n", + "Install the following packages required to execute this notebook." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "2b4ef9b72d43" + }, + "outputs": [], + "source": [ + "%pip install --upgrade --user --quiet google-cloud-aiplatform" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "sBCra4QMA2wR" + }, + "source": [ + "### Authenticate your Google Cloud account\n", + "\n", + "If you are running this notebook on Google Colab, you will need to authenticate your environment. To do this, run the new cell below. This step is not required if you are using Vertex AI Workbench." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": { + "id": "603adbbf0532" + }, + "outputs": [], + "source": [ + "import sys\n", + "\n", + "if \"google.colab\" in sys.modules:\n", + " # Authenticate user to Google Cloud\n", + " from google.colab import auth\n", + "\n", + " auth.authenticate_user()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "WReHDGG5g0XY" + }, + "source": [ + "### Set Google Cloud project information and initialize Vertex AI SDK\n", + "\n", + "To get started using Vertex AI, you must have an existing Google Cloud project and [enable the Vertex AI API](https://console.cloud.google.com/flows/enableapi?apiid=aiplatform.googleapis.com).\n", + "\n", + "Learn more about [setting up a project and a development environment](https://cloud.google.com/vertex-ai/docs/start/cloud-environment).\n", + "\n", + "**If you don't know your project ID**, try the following:\n", + "* Run `gcloud config list`.\n", + "* Run `gcloud projects list`.\n", + "* See the support page: [Locate the project ID](https://support.google.com/googleapi/answer/7014113)\n", + "\n", + "You can also change the `REGION` variable used by Vertex AI. Learn more about [Vertex AI regions](https://cloud.google.com/vertex-ai/docs/general/locations)." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": { + "id": "oM1iC_MfAts1" + }, + "outputs": [], + "source": [ + "# TODO (Developer): Replace with your project ID\n", + "\n", + "PROJECT_ID = \"YOUR_PROJECT_ID\" # @param {type:\"string\"}\n", + "REGION = \"us-central1\" # @param {type: \"string\"}" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "uA1PVor3E83R" + }, + "source": [ + "Initialize the Vertex AI Python SDK for your project:" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": { + "id": "init_aip:mbsdk,all" + }, + "outputs": [], + "source": [ + "import vertexai\n", + "\n", + "vertexai.init(project=PROJECT_ID, location=REGION)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "960505627ddf" + }, + "source": [ + "### Import libraries" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": { + "id": "PyQmSRbKA8r-" + }, + "outputs": [], + "source": [ + "from IPython.display import Markdown, display\n", + "from vertexai.generative_models import GenerativeModel, Part" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "9qI-gI-EDsBY" + }, + "source": [ + "Initialize the Gemini model from Vertex AI:" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": { + "id": "dqdpsB2lDsBY" + }, + "outputs": [], + "source": [ + "model = GenerativeModel(\"gemini-2.0-flash\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "0O7HQPWXcbzk" + }, + "source": [ + "## Content generation\n", + "\n", + "In this section, you will generate a few social media posts for a new product called **GShoe**." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "yF5vDI4egkGt" + }, + "source": [ + "### Generate with product name\n", + "\n", + "Try generating with just a product name." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": { + "id": "rF7wbZwCef2M" + }, + "outputs": [], + "source": [ + "product_name = \"GShoe\"\n", + "\n", + "prompt = f\"\"\"\n", + " Generate a few social media posts about a new product, {product_name}\n", + "\"\"\"\n", + "\n", + "print(f\"Prompt: {prompt}\")\n", + "\n", + "display(Markdown(model.generate_content(prompt).text))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "1P0R8bIDGVRR" + }, + "source": [ + "### Generate with product description\n", + "\n", + "Adding a product description can further improve the output quality." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": { + "id": "JvQqIItAfLj3" + }, + "outputs": [], + "source": [ + "product_description = \"\"\"\n", + " GShoe's design is focused on comfort, durability, and style. It includes the following features:\n", + " - Breathable and flexible upper: GShoe's upper is made from breathable and flexible materials to allow for maximum comfort and movement. It's designed to accommodate a range of foot shapes and sizes and provide adequate support and stability.\n", + " - Cushioned insole: GShoe's insole is cushioned to provide shock absorption and support. It helps reduce foot fatigue and prevent injuries caused by impact and stress.\n", + " - Durable outsole: GShoe's outsole is made from durable rubber to provide traction and protection. It's designed to withstand the wear and tear of daily use and provide grip on a variety of surfaces.\n", + " - Stylish design: GShoe's design is sleek and modern, with a range of color and style options to suit any taste. It's designed to look good both on and off the track and complement a variety of workout gear.\n", + "\"\"\"\n", + "\n", + "prompt = f\"\"\"\n", + " Generate a few social media posts about a new product, {product_name}.\\n\n", + " This is the product description: {product_description}\n", + "\"\"\"\n", + "\n", + "print(f\"Prompt: {prompt}\")\n", + "\n", + "display(Markdown(model.generate_content(prompt).text))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "sfMw20zjGBLy" + }, + "source": [ + "### Generate with product image\n", + "\n", + "Since Gemini is [multimodal](https://cloud.google.com/use-cases/multimodal-ai?hl=en), you can also provide a visual asset (e.g. a product image) to generate marketing messages based on that. Here you will generate marketing messages for based on this product image:\n", + "\n", + "" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": { + "id": "HyPfbvoGkNUv" + }, + "outputs": [], + "source": [ + "# prepare product image\n", + "\n", + "product_image_url = \"https://storage.googleapis.com/github-repo/use-cases/marketing/gshoe-images/gshoe-01.jpg\"\n", + "product_image_gcs_uri = \"gs://github-repo/use-cases/marketing/gshoe-images/gshoe-01.jpg\"\n", + "product_image_markdown = f\"\"\n", + "\n", + "product_image = Part.from_uri(\n", + " product_image_gcs_uri,\n", + " mime_type=\"image/jpeg\",\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": { + "id": "KRyZNel4N6tA" + }, + "outputs": [], + "source": [ + "prompt = f\"\"\"\n", + " Generate a few social media posts about a new product, {product_name}.\\n\n", + " This is the product description: {product_description}\n", + " This is the product image: \"\"\"\n", + "\n", + "content = [prompt, product_image]\n", + "\n", + "print(f\"Prompt: {prompt}\")\n", + "display(Markdown(product_image_markdown))\n", + "\n", + "display(Markdown(model.generate_content(content).text))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "Cceuou9XGON4" + }, + "source": [ + "### Generate for various platforms\n", + "\n", + "So far, the generated posts are too general. Fortunately, with Gemini, you can target for specific platforms. Here you will generate the messages targeted to various platforms." + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": { + "id": "hgcIL6DdfLmR" + }, + "outputs": [], + "source": [ + "platforms = \"LinkedIn, Facebook, Twitter, Instagram\"\n", + "\n", + "prompt = f\"\"\"\n", + " Generate a few social media posts about a new product, {product_name}\n", + " for platforms: '{platforms}'\\n\n", + " This is the product description: {product_description}\n", + " This is the product image: \"\"\"\n", + "\n", + "content = [prompt, product_image]\n", + "\n", + "print(f\"Prompt: {prompt}\")\n", + "display(Markdown(product_image_markdown))\n", + "\n", + "social_media_posts = model.generate_content(content).text\n", + "display(Markdown(social_media_posts))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "jGQoSAw0cbez" + }, + "source": [ + "## Content personalization\n", + "\n", + "In this section, you will use Gemini model to produce the marketing messages for a specific target segment or a customer profile" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "bQ3IaMF8JqLI" + }, + "source": [ + "### Generate for various professions\n", + "\n", + "Here you will generate for students and working professionals. Observe the responses to see how the messaging has changed to fit the audience profile." + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": { + "id": "oXXCZ9ehd0IJ" + }, + "outputs": [], + "source": [ + "# make it suitable for students\n", + "\n", + "prompt = f\"\"\"\n", + " Reference to these social media posts: \\n{social_media_posts}\n", + " Make the posts suitable for students\n", + " \"\"\"\n", + "\n", + "display(Markdown(model.generate_content(prompt).text))" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": { + "id": "fw9CXRNHgmOI" + }, + "outputs": [], + "source": [ + "# make it suitable for working adults\n", + "\n", + "prompt = f\"\"\"\n", + " Reference to these social media posts: \\n{social_media_posts}\n", + " Make the posts suitable for working adults\n", + " \"\"\"\n", + "\n", + "display(Markdown(model.generate_content(prompt).text))" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": { + "id": "qQsFC_H-l_PT" + }, + "outputs": [], + "source": [ + "# make it suitable for retiree\n", + "\n", + "prompt = f\"\"\"\n", + " Reference to these social media posts: \\n{social_media_posts}\n", + " Make the posts suitable for retiree\n", + " \"\"\"\n", + "\n", + "display(Markdown(model.generate_content(prompt).text))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "ea3jt7e_Jvbx" + }, + "source": [ + "### Generate for different generations\n", + "\n", + "Here you will generate the marketing messaging for millennial and baby boomer generations. Observe how Gemini is able to make the responses more relevant to the target audience." + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": { + "id": "e4pvTiE6gl5Z" + }, + "outputs": [], + "source": [ + "# make the posts appealing to millennial generation\n", + "\n", + "prompt = f\"\"\"\n", + " Reference to these social media posts: \\n{social_media_posts}\n", + " Make the posts appealing to millennial generation\n", + " \"\"\"\n", + "\n", + "display(Markdown(model.generate_content(prompt).text))" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": { + "id": "VqCotF0ujblz" + }, + "outputs": [], + "source": [ + "# make the posts appealing to baby boomer generation\n", + "\n", + "prompt = f\"\"\"\n", + " Reference to these social media posts: \\n{social_media_posts}\n", + " Make the posts appealing to baby boomer generation\n", + " \"\"\"\n", + "\n", + "display(Markdown(model.generate_content(prompt).text))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "zVD1XhmZw0Pi" + }, + "source": [ + "### Generate for different countries and languages" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": { + "id": "Negz0qW0xGI3" + }, + "outputs": [], + "source": [ + "# make it suitable for Indonesia\n", + "\n", + "prompt = f\"\"\"\n", + " Reference to these social media posts: \\n{social_media_posts}\n", + " Make the posts suitable for Indonesia\n", + " \"\"\"\n", + "\n", + "display(Markdown(model.generate_content(prompt).text))" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "metadata": { + "id": "jjDxLL1nnP9t" + }, + "outputs": [], + "source": [ + "# make it suitable for Japanese-speaking audience\n", + "\n", + "prompt = f\"\"\"\n", + " Reference to these social media posts: \\n{social_media_posts}\n", + " Make the posts suitable for Japanese speaking audience\n", + " \"\"\"\n", + "\n", + "display(Markdown(model.generate_content(prompt).text))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "AzOLznmmc5Oa" + }, + "source": [ + "## Creating visual asset for various platforms\n", + "\n", + "You can also create more visual assets for various platform using Imagen. For example, you might want to use 16:9 aspect ratio image instead of 1:1 aspect ratio image that you just used. Here you will use [outpainting feature](https://cloud.google.com/vertex-ai/generative-ai/docs/image/edit-outpainting#generativeaionvertexai_imagen_edit_image_outpainting_mask-python_vertex_ai_sdk) from Imagen to expend the 1:1 aspect ratio to 16:9.\n", + "\n", + "\n", + "Note: To complete the following part, you need to be allowlisted for Imagen's features. To request access to use the Imagen features, fill out the [Imagen on Vertex AI access request form](https://docs.google.com/forms/d/1cqt9padvfMgqn23W5FMPTqh7bW1KLkEOsC5G6uC-uuM/viewform)." + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "metadata": { + "id": "KDhoiWRkFwEm" + }, + "outputs": [], + "source": [ + "# initialize and select image generation model\n", + "\n", + "from vertexai.preview.vision_models import Image, ImageGenerationModel\n", + "\n", + "imagen_model = ImageGenerationModel.from_pretrained(\"imagegeneration@006\")" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "metadata": { + "id": "iO8E3UTlv2vz" + }, + "outputs": [], + "source": [ + "import shutil\n", + "\n", + "# helper function to download image from a URL\n", + "import requests\n", + "\n", + "def download_image(image_url, save_path):\n", + " \"\"\"Downloads an image from a URL and saves it to the specified path.\"\"\"\n", + " try:\n", + " response = requests.get(\n", + " image_url, stream=True\n", + " ) # Stream for efficient downloading\n", + " response.raise_for_status() # Check for HTTP errors\n", + "\n", + " with open(save_path, \"wb\") as file:\n", + " shutil.copyfileobj(response.raw, file)\n", + "\n", + " except requests.exceptions.RequestException as error:\n", + " print(f\"Error downloading image: {error}\")\n", + "\n", + "\n", + "from PIL import Image as PILImage\n", + "\n", + "# helper function to expend the image to 16:9 ratio\n", + "\n", + "\n", + "def make_16_9_image_and_mask(image_path):\n", + " \"\"\"Expands a 1:1 aspect ratio image into 16:9\"\"\"\n", + " with PILImage.open(image_path) as img:\n", + " width, height = img.size\n", + "\n", + " # ensure input image is 1:1 aspect ratio\n", + " if width != height:\n", + " raise ValueError(\"Image is not 1:1 aspect ratio\")\n", + "\n", + " # Calculate the new dimensions\n", + " new_width = width * 16 // 9\n", + " pad_width = (new_width - width) // 2 # Padding on each side\n", + "\n", + " # Create a new black image with the 16:9 dimensions\n", + " new_img = PILImage.new(\"RGB\", (new_width, height), (0, 0, 0))\n", + "\n", + " # Paste the original image in the center\n", + " new_img.paste(img, (pad_width, 0))\n", + "\n", + " # Save the expanded image\n", + " output_img_path = f\"{image_path.split('.')[0]}_16_9.jpg\"\n", + " new_img.save(output_img_path)\n", + "\n", + " # Create a new white mask with the 16:9 dimensions\n", + " white_mask = PILImage.new(\"RGB\", (new_width, height), (255, 255, 255))\n", + "\n", + " # Paste the black mask with original image's dimensions in the center\n", + " black_mask = PILImage.new(\"RGB\", (width, height), (0, 0, 0))\n", + " white_mask.paste(black_mask, (pad_width, 0))\n", + "\n", + " # Save the expanded image\n", + " output_mask_path = f\"{image_path.split('.')[0]}_16_9_mask.jpg\"\n", + " white_mask.save(output_mask_path)\n", + "\n", + " return output_img_path, output_mask_path" + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "metadata": { + "id": "wf58dw9Kvx9u" + }, + "outputs": [], + "source": [ + "# download and prepare the image and its mask\n", + "\n", + "\n", + "gshoe_image_url = \"https://storage.googleapis.com/github-repo/use-cases/marketing/gshoe-images/gshoe-01.jpg\"\n", + "gshoe_image_path = \"gshoe-01.jpg\" # Choose where to save the image\n", + "download_image(gshoe_image_url, gshoe_image_path)\n", + "\n", + "image_path, mask_path = make_16_9_image_and_mask(gshoe_image_path)\n", + "base_img = Image.load_from_file(location=image_path)\n", + "mask_img = Image.load_from_file(location=mask_path)" + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "metadata": { + "id": "sq4fQXuydwAD" + }, + "outputs": [], + "source": [ + "# show base image\n", + "\n", + "base_img.show()" + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "metadata": { + "id": "7AhL-ivvv0ie" + }, + "outputs": [], + "source": [ + "# expend to 16:9 using Imagen's outpainting feature\n", + "\n", + "images = imagen_model.edit_image(\n", + " base_image=base_img,\n", + " mask=mask_img,\n", + " edit_mode=\"outpainting\",\n", + " prompt=\"\",\n", + ")\n", + "\n", + "images[0].show()" + ] + }, + { + "cell_type": "code", + "execution_count": 58, + "metadata": { + "id": "kXQfMOV-3WAw" + }, + "outputs": [], + "source": [ + "# expend to 16:9 using Imagen with a modifier prompt\n", + "\n", + "images = imagen_model.edit_image(\n", + " base_image=base_img,\n", + " mask=mask_img,\n", + " edit_mode=\"outpainting\",\n", + " prompt=\"a shoe surround by flowers\",\n", + ")\n", + "\n", + "images[0].show()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "TpV-iwP9qw9c" + }, + "source": [ + "## Cleaning up\n", + "\n", + "To avoid incurring charges to your Google Cloud account for the resources used in this notebook, follow these steps:\n", + "\n", + "1. To avoid unnecessary Google Cloud charges, use the [Google Cloud console](https://console.cloud.google.com/) to delete your project if you do not need it. Learn more in the Google Cloud documentation for [managing and deleting your project](https://cloud.google.com/resource-manager/docs/creating-managing-projects).\n", + "1. If you used an existing Google Cloud project, delete the resources you created to avoid incurring charges to your account.\n", + "1. Disable the [Vertex AI API](https://console.cloud.google.com/apis/api/aiplatform.googleapis.com) in the Google Cloud Console." + ] + } + ], + "metadata": { + "colab": { + "name": "creative_content_generation.ipynb", + "toc_visible": true + }, + "kernelspec": { + "display_name": "Python 3", + "name": "python3" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} diff --git a/gemini/use-cases/media-generation/README.md b/gemini/use-cases/media-generation/README.md new file mode 100644 index 0000000..aca63ac --- /dev/null +++ b/gemini/use-cases/media-generation/README.md @@ -0,0 +1,44 @@ +# Media Generation + +## Featured Notebooks + + + + + + + + + + + + + + + +
    NotebookDescription
    + + animation showing the generation of consistent images +
    + ./consistent_imagery_generation.ipynb
    +
    + Generating Consistent Imagery with Nano Banana 🍌 +
      +
    • Start from an archive image
    • +
    • Extract a character to create a brand-new reference image
    • +
    • Generate a series of consistent images to illustrate the character's journey
    • +
    +
    + + ../../nano-banana/
    nano_banana_recipes.ipynb
    +
    +
    + Nano Banana 🍌: Gemini 2.5 Flash Image Recipes +
    + This notebook offers a collection of practical examples and best practices + for leveraging the Gemini 2.5 Flash model for a variety of image + generation and editing tasks. +
    + diff --git a/gemini/use-cases/media-generation/consistent_imagery_generation.ipynb b/gemini/use-cases/media-generation/consistent_imagery_generation.ipynb new file mode 100644 index 0000000..dad2d5c --- /dev/null +++ b/gemini/use-cases/media-generation/consistent_imagery_generation.ipynb @@ -0,0 +1,1848 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "WtxoQixAqoNu" + }, + "outputs": [], + "source": [ + "# Copyright 2025-2026 Google LLC\n", + "#\n", + "# Licensed under the Apache License, Version 2.0 (the \"License\");\n", + "# you may not use this file except in compliance with the License.\n", + "# You may obtain a copy of the License at\n", + "#\n", + "# https://www.apache.org/licenses/LICENSE-2.0\n", + "#\n", + "# Unless required by applicable law or agreed to in writing, software\n", + "# distributed under the License is distributed on an \"AS IS\" BASIS,\n", + "# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n", + "# See the License for the specific language governing permissions and\n", + "# limitations under the License." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "ke8cM4GQln_c" + }, + "source": [ + "# Generating Consistent Imagery with Gemini 🍌\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "BPRBl_orqoNv" + }, + "source": [ + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
    \n", + " \n", + " \"Google
    Open in Colab\n", + "
    \n", + "
    \n", + " \n", + " \"Google
    Open in Colab Enterprise\n", + "
    \n", + "
    \n", + " \n", + " \"Workbench
    Open in Workbench\n", + "
    \n", + "
    \n", + " \n", + " \"GitHub
    View on GitHub\n", + "
    \n", + "
    \n", + "\n", + "
    \n", + "\n", + "

    \n", + "Share to:\n", + "\n", + "\n", + " \"LinkedIn\n", + "\n", + "\n", + "\n", + " \"Bluesky\n", + "\n", + "\n", + "\n", + " \"X\n", + "\n", + "\n", + "\n", + " \"Reddit\n", + "\n", + "\n", + "\n", + " \"Facebook\n", + "\n", + "

    \n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "C0AcsmQ5hl9a" + }, + "source": [ + "| Author |\n", + "| ------------------------------------------------ |\n", + "| [Laurent Picard](https://github.com/PicardParis) |\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "DwKb-__qK02C" + }, + "source": [ + "---\n", + "\n", + "## đŸ”„ Challenge\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "![intro image](https://storage.googleapis.com/github-repo/generative-ai/gemini/use-cases/media-generation/consistent_imagery_generation/graph_animated.gif)\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "35E-CpC6qoNw" + }, + "source": [ + "We all have existing images worth reusing in different contexts. This typically involves modifying the images, a complex (if not impossible) task requiring very specific skills and tools. This explains why our archives are full of forgotten or unused treasures. State-of-the-art vision models have evolved so much that we can reconsider this problem.\n", + "\n", + "So, can we breathe new life into our visual archives?\n", + "\n", + "Let's try to complete this challenge with the following steps:\n", + "\n", + "- 1ïžâƒŁ Start with an archive image we'd like to reuse\n", + "- 2ïžâƒŁ Extract a character to create a brand-new reference image\n", + "- 3ïžâƒŁ Generate a series of images to illustrate the character's journey, using only prompts and the new assets\n", + "\n", + "For this, we'll explore the capabilities of \"Gemini 2.5 Flash Image\", also known as \"Nano Banana\" 🍌.\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "W0_VsUthqoNx" + }, + "source": [ + "---\n", + "\n", + "## 🏁 Setup\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "5wbID7ORqoNx" + }, + "source": [ + "### 🐍 Python packages\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "MZCjtEXjhj3z" + }, + "source": [ + "We'll use the following packages:\n", + "\n", + "- `google-genai`: The [Google Gen AI Python SDK](https://pypi.org/project/google-genai) lets us call Gemini with a few lines of code\n", + "- `networkx` for graph management\n", + "\n", + "We'll also need:\n", + "\n", + "- `tenacity` for request management (a dependency of `google-genai`)\n", + "- `matplotlib` and `pillow` for data visualization (dependencies of `networkx`)\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "kZBN80r7qtgs" + }, + "outputs": [], + "source": [ + "%pip install --quiet \"google-genai>=2.6.0\" \"networkx[default]\"" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "yGgmHVdQqoNz" + }, + "source": [ + "---\n", + "\n", + "### đŸ€ Gemini API\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "zOJv5DjZhj3z" + }, + "source": [ + "To use the Gemini API, we have two main options:\n", + "\n", + "1. Via **Agent Platform** (formerly Vertex AI) with a Google Cloud project\n", + "2. Via **Google AI Studio** with a Gemini API key\n", + "\n", + "The Google Gen AI SDK provides a unified interface to these APIs and we can use environment variables for the configuration.\n", + "\n", + "**đŸ› ïž Option 1 - Gemini API via Agent Platform**\n", + "\n", + "Requirements:\n", + "\n", + "- A Google Cloud project\n", + "- The Agent Platform API must be enabled for this project: ▶ [Enable the Agent Platform API](https://console.cloud.google.com/flows/enableapi?apiid=aiplatform.googleapis.com,storage-component.googleapis.com)\n", + "\n", + "Gen AI SDK environment variables:\n", + "\n", + "- `GOOGLE_GENAI_USE_ENTERPRISE=\"True\"`\n", + "- `GOOGLE_CLOUD_PROJECT=\"\"`\n", + "- `GOOGLE_CLOUD_LOCATION=\"\"`\n", + "\n", + "> 💡 For preview models, the location must be set to `global`. For generally available models, we can choose the closest location among the [Google model endpoint locations](https://docs.cloud.google.com/vertex-ai/generative-ai/docs/learn/locations#google-models).\n", + "\n", + "> â„č Learn more about [setting up a project and a development environment](https://docs.cloud.google.com/vertex-ai/docs/start/cloud-environment).\n", + "\n", + "**đŸ› ïž Option 2 - Gemini API via Google AI Studio**\n", + "\n", + "Requirement:\n", + "\n", + "- A Gemini API key\n", + "\n", + "Gen AI SDK environment variables:\n", + "\n", + "- `GOOGLE_GENAI_USE_ENTERPRISE=\"False\"`\n", + "- `GOOGLE_API_KEY=\"\"`\n", + "\n", + "> â„č Learn more about [getting a Gemini API key from Google AI Studio](https://aistudio.google.com/app/apikey).\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "wZcmLBKqhdvX" + }, + "source": [ + "💡 You can store your environment configuration outside of the source code:\n", + "\n", + "| Environment | Method |\n", + "| ---------------- | ----------------------------------------------------------- |\n", + "| IDE | `.env` file (or equivalent) |\n", + "| Colab | Colab Secrets (đŸ—ïž icon in left panel, see code below) |\n", + "| Colab Enterprise | Google Cloud project and location are automatically defined |\n", + "| Workbench | Google Cloud project and location are automatically defined |\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "ky2Escg3a1E2" + }, + "source": [ + "Define the following environment detection functions. You can also define your configuration manually if needed.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "VTov81qlqoNz" + }, + "outputs": [], + "source": [ + "# @title {display-mode: \"form\"}\n", + "import os\n", + "import sys\n", + "from collections.abc import Callable\n", + "\n", + "from google import genai\n", + "\n", + "# Manual setup (leave unchanged if setup is environment-defined)\n", + "\n", + "# @markdown **Which API: Agent Platform (formerly Vertex AI) or Google AI Studio?**\n", + "GOOGLE_GENAI_USE_ENTERPRISE = True # @param {type: \"boolean\"}\n", + "\n", + "# @markdown **Option A - Google Cloud project [+location]**\n", + "GOOGLE_CLOUD_PROJECT = \"\" # @param {type: \"string\"}\n", + "GOOGLE_CLOUD_LOCATION = \"global\" # @param {type: \"string\"}\n", + "\n", + "# @markdown **Option B - Google AI Studio API key**\n", + "GOOGLE_API_KEY = \"\" # @param {type: \"string\"}\n", + "\n", + "\n", + "def check_environment() -> bool:\n", + " check_colab_user_authentication()\n", + " return check_manual_setup() or check_enterprise() or check_colab() or check_local()\n", + "\n", + "\n", + "def check_manual_setup() -> bool:\n", + " return check_define_env_vars(\n", + " GOOGLE_GENAI_USE_ENTERPRISE,\n", + " GOOGLE_CLOUD_PROJECT.strip(), # Might have been pasted with a newline\n", + " GOOGLE_CLOUD_LOCATION,\n", + " GOOGLE_API_KEY,\n", + " )\n", + "\n", + "\n", + "def check_enterprise() -> bool:\n", + " # Workbench and Colab Enterprise\n", + " match os.getenv(\"VERTEX_PRODUCT\", \"\"):\n", + " case \"WORKBENCH_INSTANCE\":\n", + " pass\n", + " case \"COLAB_ENTERPRISE\":\n", + " if not running_in_colab_env():\n", + " return False\n", + " case _:\n", + " return False\n", + "\n", + " return check_define_env_vars(\n", + " True,\n", + " os.getenv(\"GOOGLE_CLOUD_PROJECT\", \"\"),\n", + " os.getenv(\"GOOGLE_CLOUD_REGION\", \"\"),\n", + " \"\",\n", + " )\n", + "\n", + "\n", + "def check_colab() -> bool:\n", + " if not running_in_colab_env():\n", + " return False\n", + "\n", + " # Colab Enterprise was checked before, so this is Colab only\n", + " from google.colab import auth as colab_auth # type: ignore\n", + "\n", + " colab_auth.authenticate_user()\n", + "\n", + " # Use Colab Secrets (đŸ—ïž icon in left panel) to store the environment variables\n", + " # Secrets are private, visible only to you and the notebooks that you select\n", + " # - Agent Platform: Store your settings as secrets\n", + " # - Google AI: Directly import your Gemini API key from the UI\n", + " enterprise, project, location, api_key = get_vars(get_colab_secret)\n", + "\n", + " return check_define_env_vars(enterprise, project, location, api_key)\n", + "\n", + "\n", + "def check_local() -> bool:\n", + " enterprise, project, location, api_key = get_vars(os.getenv)\n", + "\n", + " return check_define_env_vars(enterprise, project, location, api_key)\n", + "\n", + "\n", + "def running_in_colab_env() -> bool:\n", + " # Colab or Colab Enterprise\n", + " return \"google.colab\" in sys.modules\n", + "\n", + "\n", + "def check_colab_user_authentication() -> None:\n", + " if running_in_colab_env():\n", + " from google.colab import auth as colab_auth # type: ignore\n", + "\n", + " colab_auth.authenticate_user()\n", + "\n", + "\n", + "def get_colab_secret(secret_name: str, default: str) -> str:\n", + " from google.colab import errors, userdata # type: ignore\n", + "\n", + " try:\n", + " return userdata.get(secret_name)\n", + " except errors.SecretNotFoundError:\n", + " return default\n", + "\n", + "\n", + "def disable_colab_cell_scrollbar() -> None:\n", + " if running_in_colab_env():\n", + " from google.colab import output # type: ignore\n", + "\n", + " output.no_vertical_scroll()\n", + "\n", + "\n", + "def get_vars(getenv: Callable[[str, str], str]) -> tuple[bool, str, str, str]:\n", + " # Limit getenv calls to the minimum (may trigger UI confirmation for secret access)\n", + " enterprise_str = getenv(\"GOOGLE_GENAI_USE_ENTERPRISE\", \"\")\n", + " if not enterprise_str:\n", + " enterprise_str = getenv(\"GOOGLE_GENAI_USE_VERTEXAI\", \"\")\n", + " if enterprise_str:\n", + " enterprise = enterprise_str.lower() in [\"true\", \"1\"]\n", + " else:\n", + " enterprise = bool(getenv(\"GOOGLE_CLOUD_PROJECT\", \"\"))\n", + "\n", + " project = getenv(\"GOOGLE_CLOUD_PROJECT\", \"\") if enterprise else \"\"\n", + " location = getenv(\"GOOGLE_CLOUD_LOCATION\", \"\") if project else \"\"\n", + " api_key = getenv(\"GOOGLE_API_KEY\", \"\") if not project else \"\"\n", + "\n", + " return enterprise, project, location, api_key\n", + "\n", + "\n", + "def check_define_env_vars(\n", + " enterprise: bool,\n", + " project: str,\n", + " location: str,\n", + " api_key: str,\n", + ") -> bool:\n", + " match (enterprise, bool(project), bool(location), bool(api_key)):\n", + " case (True, True, _, _):\n", + " # Agent Platform - Google Cloud project [+location]\n", + " location = location or \"global\"\n", + " define_env_vars(enterprise, project, location, \"\")\n", + " case (True, False, _, True):\n", + " # Agent Platform - API key\n", + " define_env_vars(enterprise, \"\", \"\", api_key)\n", + " case (False, _, _, True):\n", + " # Google AI Studio - API key\n", + " define_env_vars(enterprise, \"\", \"\", api_key)\n", + " case _:\n", + " return False\n", + "\n", + " return True\n", + "\n", + "\n", + "def define_env_vars(\n", + " enterprise: bool,\n", + " project: str,\n", + " location: str,\n", + " api_key: str,\n", + ") -> None:\n", + " os.environ[\"GOOGLE_GENAI_USE_ENTERPRISE\"] = str(enterprise)\n", + " os.environ[\"GOOGLE_GENAI_USE_VERTEXAI\"] = str(enterprise)\n", + " os.environ[\"GOOGLE_CLOUD_PROJECT\"] = project\n", + " os.environ[\"GOOGLE_CLOUD_LOCATION\"] = location\n", + " os.environ[\"GOOGLE_API_KEY\"] = api_key\n", + "\n", + "\n", + "def check_configuration(client: genai.Client) -> None:\n", + " service = \"Agent Platform\" if client.vertexai else \"Google AI Studio\"\n", + " print(f\"✅ Using the {service} API\", end=\"\")\n", + "\n", + " if client._api_client.project:\n", + " print(f' with project \"{client._api_client.project[:7]}
\"', end=\"\")\n", + " print(f' in location \"{client._api_client.location}\"')\n", + " elif client._api_client.api_key:\n", + " api_key = client._api_client.api_key\n", + " print(f' with API key \"{api_key[:5]}
{api_key[-5:]}\"', end=\"\")\n", + " print(f\" (in case of error, make sure it was created for {service})\")\n", + "\n", + "\n", + "print(\"✅ Environment functions defined\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "GEYSAFqrqoNz" + }, + "source": [ + "---\n", + "\n", + "### đŸ€– Gen AI SDK\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "1Q_Irs4D1EAf" + }, + "source": [ + "To send Gemini requests, we'll use a `google.genai` client:\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "DR77aUhzqoNz" + }, + "outputs": [], + "source": [ + "from google import genai\n", + "\n", + "check_environment()\n", + "\n", + "client = genai.Client()\n", + "\n", + "check_configuration(client)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "4TDJ59_u2jhb" + }, + "source": [ + "---\n", + "\n", + "## 🧠 Gemini model\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "9Ls-wVEq2jhc" + }, + "source": [ + "For this challenge, we'll select the latest Gemini 2.5 Flash Image model:\n", + "\n", + "`GEMINI_2_5_FLASH_IMAGE = \"gemini-2.5-flash-image\"`\n", + "\n", + "> 💡 \"Gemini 2.5 Flash Image\" is also known as \"Nano Banana\" 🍌\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "eUBe2u8IqoNz" + }, + "source": [ + "---\n", + "\n", + "## đŸ› ïž Helpers\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "jsuIT4f1-CFe" + }, + "source": [ + "Now, let's define some helper functions to generate and display images:\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "Bus2ODLIK02F" + }, + "outputs": [], + "source": [ + "import IPython.display\n", + "import tenacity\n", + "from google.genai.errors import ClientError\n", + "from google.genai.types import GenerateContentConfig, ImageConfig, PIL_Image\n", + "\n", + "GEMINI_2_5_FLASH_IMAGE = \"gemini-2.5-flash-image\"\n", + "\n", + "# You can add the \"TEXT\" modality for potential textual feedback (or in iterative chat mode)\n", + "RESPONSE_MODALITIES = [\"IMAGE\"]\n", + "\n", + "# Supported aspect ratios: \"1:1\", \"2:3\", \"3:2\", \"3:4\", \"4:3\", \"4:5\", \"5:4\", \"9:16\", \"16:9\", and \"21:9\"\n", + "ASPECT_RATIO = \"16:9\"\n", + "\n", + "GENERATION_CONFIG = GenerateContentConfig(\n", + " response_modalities=RESPONSE_MODALITIES,\n", + " image_config=ImageConfig(aspect_ratio=ASPECT_RATIO),\n", + ")\n", + "\n", + "\n", + "def generate_content(sources: list[PIL_Image], prompt: str) -> PIL_Image | None:\n", + " prompt = prompt.strip()\n", + " contents = [*sources, prompt] if sources else prompt\n", + "\n", + " response = None\n", + " for attempt in get_retrier():\n", + " with attempt:\n", + " response = client.models.generate_content(\n", + " model=GEMINI_2_5_FLASH_IMAGE,\n", + " contents=contents,\n", + " config=GENERATION_CONFIG,\n", + " )\n", + "\n", + " if not response or not response.candidates:\n", + " return None\n", + " if not (content := response.candidates[0].content):\n", + " return None\n", + " if not (parts := content.parts):\n", + " return None\n", + "\n", + " image: PIL_Image | None = None\n", + " for part in parts:\n", + " if part.text:\n", + " display_markdown(part.text)\n", + " continue\n", + " assert (sdk_image := part.as_image())\n", + " assert (image := sdk_image._pil_image)\n", + " display_image(image)\n", + "\n", + " return image\n", + "\n", + "\n", + "def get_retrier() -> tenacity.Retrying:\n", + " return tenacity.Retrying(\n", + " stop=tenacity.stop_after_attempt(7),\n", + " wait=tenacity.wait_incrementing(start=10, increment=1),\n", + " retry=tenacity.retry_if_exception(should_retry_request),\n", + " reraise=True,\n", + " )\n", + "\n", + "\n", + "def should_retry_request(err: BaseException) -> bool:\n", + " if not isinstance(err, ClientError):\n", + " return False\n", + " print(f\"❌ ClientError {err.code}: {err.message}\")\n", + "\n", + " retry = False\n", + " match err.code:\n", + " case 400 if err.message is not None and \" try again \" in err.message:\n", + " # Workshop: project accessing Cloud Storage for the first time (service agent provisioning)\n", + " retry = True\n", + " case 429:\n", + " # Workshop: temporary project with 1 QPM quota\n", + " retry = True\n", + " print(f\"🔄 Retry: {retry}\")\n", + "\n", + " return retry\n", + "\n", + "\n", + "def display_markdown(markdown: str) -> None:\n", + " IPython.display.display(IPython.display.Markdown(markdown))\n", + "\n", + "\n", + "def display_image(image: PIL_Image) -> None:\n", + " IPython.display.display(image)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "3jONuiQS2jhc" + }, + "source": [ + "---\n", + "\n", + "## đŸ–Œïž Assets\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "0TK5tdo12jhc" + }, + "source": [ + "Let's define the assets for our character's journey and the functions to manage them:\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "uFnwc1fU2jhc" + }, + "outputs": [], + "source": [ + "import enum\n", + "from collections.abc import Sequence\n", + "from dataclasses import dataclass\n", + "\n", + "\n", + "class AssetId(enum.StrEnum):\n", + " ARCHIVE = \"0_archive\"\n", + " ROBOT = \"1_robot\"\n", + " MOUNTAINS = \"2_mountains\"\n", + " VALLEY = \"3_valley\"\n", + " FOREST = \"4_forest\"\n", + " CLEARING = \"5_clearing\"\n", + " ASCENSION = \"6_ascension\"\n", + " SUMMIT = \"7_summit\"\n", + " BRIDGE = \"8_bridge\"\n", + " HAMMOCK = \"9_hammock\"\n", + "\n", + "\n", + "@dataclass\n", + "class Asset:\n", + " id: str\n", + " source_ids: Sequence[str]\n", + " prompt: str\n", + " pil_image: PIL_Image\n", + "\n", + "\n", + "class Assets(dict[str, Asset]):\n", + " def set_asset(self, asset: Asset) -> None:\n", + " # Note: This replaces any existing asset (if needed, add guardrails to auto-save or keep all versions)\n", + " self[asset.id] = asset\n", + "\n", + "\n", + "def generate_image(source_ids: Sequence[str], prompt: str, new_id: str = \"\") -> None:\n", + " sources = [assets[source_id].pil_image for source_id in source_ids]\n", + " prompt = prompt.strip()\n", + " image = generate_content(sources, prompt)\n", + " if image and new_id:\n", + " assets.set_asset(Asset(new_id, source_ids, prompt, image))\n", + "\n", + "\n", + "assets = Assets()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "pgOW5Izd2jhc" + }, + "source": [ + "---\n", + "\n", + "### 📩 Reference archive\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "DaMbCBrM2jhc" + }, + "source": [ + "We can now fetch our reference archive and make it our first asset:\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "MwR0sjIj2jhc" + }, + "outputs": [], + "source": [ + "import urllib.request\n", + "from io import BytesIO\n", + "\n", + "import PIL.Image\n", + "\n", + "ARCHIVE_URL = \"https://storage.googleapis.com/github-repo/generative-ai/gemini/use-cases/media-generation/consistent_imagery_generation/0_archive.png\"\n", + "\n", + "\n", + "def load_archive() -> None:\n", + " image = get_image_from_url(ARCHIVE_URL)\n", + " assets.set_asset(Asset(AssetId.ARCHIVE, [], \"\", image))\n", + " display_image(image)\n", + "\n", + "\n", + "def get_image_from_url(image_url: str) -> PIL_Image:\n", + " with urllib.request.urlopen(image_url, timeout=10) as response:\n", + " return PIL.Image.open(BytesIO(response.read()))\n", + "\n", + "\n", + "load_archive()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "0_752MsD2jhd" + }, + "source": [ + "This archive image was generated in July 2024 with a beta version of Imagen 3, prompted with _\"On white background, a small hand-felted toy of a blue robot. The felt is soft and cuddly
\"_. The result looked really good, but there was absolutely no determinism or consistency at the time. This was a nice one-shot generation, and the cute little robot seemed lost forever
\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "rGdDA_wr2jhd" + }, + "source": [ + "---\n", + "\n", + "### ⛏ Asset extraction\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "6iBKtcN12jhd" + }, + "source": [ + "Let's try to extract our little robot:\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "source_ids = [AssetId.ARCHIVE]\n", + "prompt = \"Extract the robot in a sharp cutout over pure white.\"\n", + "\n", + "generate_image(source_ids, prompt)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "xw2c-L1s2jhd" + }, + "source": [ + "⚠ The robot is extracted perfectly, but this is essentially just a good background removal job (something many models can already do). This prompt uses terms from graphics software, whereas we can now think in terms of image composition. It's also not necessarily a good idea to use traditional binary masks, as object edges and shadows convey significant details about shapes, textures, positions, and lighting.\n", + "\n", + "Let's go back to our archive to perform an advanced extraction instead and directly generate a character sheet
\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "NBLVNM4c2jhd" + }, + "source": [ + "---\n", + "\n", + "### đŸȘ„ Character sheet\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "oW7RyrJy2jhd" + }, + "source": [ + "Gemini has spatial understanding, so it can provide different views while preserving visual features. Let's generate a front/back character sheet and add a backpack at the same time, since our little robot is going on a journey:\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "n9_HtlBy2jhd" + }, + "outputs": [], + "source": [ + "source_ids = [AssetId.ARCHIVE]\n", + "prompt = \"\"\"\n", + "- Scene: Robot character sheet.\n", + "- Left: Front view of the extracted robot.\n", + "- Right: Back view of the extracted robot (seamless back).\n", + "- In both views, the robot wears the same small, brown felt backpack, with a tiny polished-brass buckle and simple straps.\n", + "- Background: Pure white.\n", + "- Text: At the top, caption the image \"ROBOT CHARACTER SHEET\" and, at the bottom, caption the views \"FRONT VIEW\" and \"BACK VIEW\".\n", + "\"\"\"\n", + "new_id = AssetId.ROBOT\n", + "\n", + "generate_image(source_ids, prompt, new_id)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "TtcCfjNJ2jhh" + }, + "source": [ + "💡 A few notes:\n", + "\n", + "- Our prompt focuses on the composition of the scene, a common practice in media studios.\n", + "- Repeated generations will be consistent, preserving all robot features visible in the provided image. However, since we only described a few features of the backpack (e.g., a single buckle) and left the rest open to interpretation, we'll get slightly different backpacks.\n", + "- For simplicity, we directly included the backpack in the character sheet. In a real production pipeline, we would likely make it part of a separate accessory sheet.\n", + "- To control the backpack's exact shape and design, we could also use a reference photo of a real backpack and instruct Gemini to \"transform the backpack into a stylized felt version.\"\n", + "- Gemini can generate `1024 × 1024` images (`1:1` aspect ratio) or equivalent resolutions (token-wise) for other supported aspect ratios (`2:3`, `3:2`, `3:4`, `4:3`, `4:5`, `5:4`, `9:16`, `16:9`, and `21:9`).\n", + "- In the request configuration, we specified `aspect_ratio=\"16:9\"`, which generates images at `1344 × 768` pixels. If this parameter is omitted, Gemini uses the aspect ratio of the input image (the last one if multiple are provided) to select the closest supported aspect ratio.\n", + "\n", + "This new asset can now serve as a design reference for our future image-generation tasks.\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "zuNg8w0k2jhh" + }, + "source": [ + "---\n", + "\n", + "### ✹ First scene\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "p7etLsjD2jhh" + }, + "source": [ + "Let's get started with a mountain scene:\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "3KjM88602jhh" + }, + "outputs": [], + "source": [ + "source_ids = [AssetId.ROBOT]\n", + "prompt = \"\"\"\n", + "- Image 1: Robot character sheet.\n", + "- Scene: Macro photography of a beautifully crafted miniature diorama.\n", + "- Background: Soft-focus of a panoramic range of interspersed, dome-like felt mountains, in various shades of medium blue/green, with curvy white snowcaps, extending over the entire horizon.\n", + "- Foreground: In the bottom-left, the robot stands on the edge of a medium-gray felt cliff, viewed from a 3/4 back angle, looking out over a sea of clouds (made of white cotton).\n", + "- Lighting: Studio, clean and soft.\n", + "\"\"\"\n", + "new_id = AssetId.MOUNTAINS\n", + "\n", + "generate_image(source_ids, prompt, new_id)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "pyGWgPp-2jhh" + }, + "source": [ + "> 💡 The mountain shape is specified as \"dome-like\", so our character can stand on one of the summits later on.\n", + "\n", + "It's important to spend some time on this first scene, as it will have a cascading effect that defines the overall look of our story. Take some time to refine the prompt or run it a couple of times to get your preferred variation.\n", + "\n", + "From now on, our generation inputs will typically include the character sheet for consistency, along with a previous scene to provide context for the next step
\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "pmbGtc4j2jhh" + }, + "source": [ + "---\n", + "\n", + "### ✹ Successive scenes\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "7xIQxlfd2jhh" + }, + "source": [ + "Let's get the robot down into a valley:\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "dNg3LCq82jhh" + }, + "outputs": [], + "source": [ + "source_ids = [AssetId.ROBOT, AssetId.MOUNTAINS]\n", + "prompt = \"\"\"\n", + "- Image 1: Robot character sheet.\n", + "- Image 2: Previous scene.\n", + "- The robot has descended from the cliff into a gray felt valley. It stands in the center, seen directly from the back. It is holding/reading a felt map with outstretched arms.\n", + "- Large, smooth, round felt rocks in various beige/gray shades are visible on the sides.\n", + "- Background: The distant mountain range. A thin layer of clouds obscures its base and the end of the valley.\n", + "- Lighting: Golden hour light, soft and diffused.\n", + "\"\"\"\n", + "new_id = AssetId.VALLEY\n", + "\n", + "generate_image(source_ids, prompt, new_id)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "BM44h_jE2jhi" + }, + "source": [ + "> 💡 A few notes:\n", + ">\n", + "> - The specific references to our input images (`\"Image 1:
\"` and `\"Image 2:
\"`) are important. Without them, \"the robot\" could refer to any of the three robots in the input images (two in the character sheet, one in the previous scene). With them, we indicate that it is the same robot. In case of confusion, we can be more specific with `\"the [entity] from image [number]\"`.\n", + "> - On the other hand, since we didn't provide a precise description of the valley, repeated requests will produce different, interesting, and creative results (we can pick our favorite or make the prompt more precise for greater determinism).\n", + "> - Here, we also tested different lighting, which significantly changes the whole scene.\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "nyOOtD5k2jhi" + }, + "source": [ + "Then, we can move forward with the story:\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "TXKljQps2jhi" + }, + "outputs": [], + "source": [ + "source_ids = [AssetId.ROBOT, AssetId.VALLEY]\n", + "prompt = \"\"\"\n", + "- Image 1: Robot character sheet.\n", + "- Image 2: Previous scene.\n", + "- The robot continues its journey and faces a dense, infinite forest of simple, giant, thin trees that fill the entire background.\n", + "- The trees are made of various shades of light/medium/dark green felt.\n", + "- The robot is on the right, viewed from a 3/4 rear angle, no longer holding the map, with both hands clasped over its ears in despair.\n", + "- On the bottom-left and bottom-right sides, rocks (similar to image 2) are partially visible.\n", + "\"\"\"\n", + "new_id = AssetId.FOREST\n", + "\n", + "generate_image(source_ids, prompt, new_id)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "GPPt8c_W2jhi" + }, + "source": [ + "> 💡 A few things to note:\n", + ">\n", + "> - We can position the character, change its point of view, and even \"animate\" its arms for more expressiveness.\n", + "> - The \"no longer holding the map\" clarification prevents the model from carrying it over from the previous scene in an awkward or unwanted way (like having the robot drop the map on the floor).\n", + "> - Since we didn't provide lighting details, the light source, quality, and direction are preserved from the previous scene.\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "wOzXXqkI2jhi" + }, + "source": [ + "Let's go through the forest:\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "GU9oEeNj2jhi" + }, + "outputs": [], + "source": [ + "source_ids = [AssetId.ROBOT, AssetId.FOREST]\n", + "prompt = \"\"\"\n", + "- Image 1: Robot character sheet.\n", + "- Image 2: Previous scene.\n", + "- The robot makes its way through the dense forest and emerges into a clearing, pushing aside two tree trunks.\n", + "- The robot is in the center, now seen from the front view.\n", + "- The ground is made of green felt, with flat patches of white felt snow. Rocks are no longer visible.\n", + "\"\"\"\n", + "new_id = AssetId.CLEARING\n", + "\n", + "generate_image(source_ids, prompt, new_id)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "ElMVmWqk2jhi" + }, + "source": [ + "> 💡 We changed the ground but didn't provide additional details for the view or the forest: The model will generally preserve most of the trees.\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "oudFjUYT2jhi" + }, + "source": [ + "Now that the valley-forest sequence is over, we can journey up to the mountains, using the original mountain scene as our reference to return to that environment:\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "-1q1RG512jhi" + }, + "outputs": [], + "source": [ + "source_ids = [AssetId.ROBOT, AssetId.MOUNTAINS]\n", + "prompt = \"\"\"\n", + "- Image 1: Robot character sheet.\n", + "- Image 2: Previous scene.\n", + "- Close-up of the robot now climbing the peak of a medium-green mountain and reaching its summit.\n", + "- The mountain is right in the center, with the robot on its left slope, viewed from a 3/4 rear angle.\n", + "- The robot has both feet on the mountain and is using two felt ice axes (brown handles, gray heads), reaching the snowcap.\n", + "- Horizon: The distant mountain range.\n", + "\"\"\"\n", + "new_id = AssetId.ASCENSION\n", + "\n", + "generate_image(source_ids, prompt, new_id)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "NlgOaEfs2jhi" + }, + "source": [ + "> 💡 The mountain close-up, inferred from the blurred background, is pretty impressive.\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "YdFYQ0S32jhi" + }, + "source": [ + "Let's climb to the summit:\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "fL3TBiCu2jhi" + }, + "outputs": [], + "source": [ + "source_ids = [AssetId.ROBOT, AssetId.ASCENSION]\n", + "prompt = \"\"\"\n", + "- Image 1: Robot character sheet.\n", + "- Image 2: Previous scene.\n", + "- The robot reaches the top and stands on the summit, seen in the front view, in close-up.\n", + "- It is no longer holding the ice axes, which are planted upright in the snow on either side.\n", + "- It has both arms raised as a sign of victory.\n", + "\"\"\"\n", + "new_id = AssetId.SUMMIT\n", + "\n", + "generate_image(source_ids, prompt, new_id)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "ZqS10CmY2jhi" + }, + "source": [ + "> 💡 This is a logical follow-up but also a great alternative view.\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "z1RkWkqy2jhi" + }, + "source": [ + "Now, let's try something different to significantly recompose the scene:\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "jWSOc5rV2jhi" + }, + "outputs": [], + "source": [ + "source_ids = [AssetId.ROBOT, AssetId.SUMMIT]\n", + "prompt = \"\"\"\n", + "- Image 1: Robot character sheet.\n", + "- Image 2: Previous scene.\n", + "- Remove the ice axes.\n", + "- Move the center mountain to the left edge of the image and add a slightly taller medium-blue mountain to the right edge.\n", + "- Suspend a stylized felt bridge between the two mountains: Its deck is made of thick felt planks in various wood shades.\n", + "- Place the robot in the center of the bridge with one arm pointing toward the blue mountain.\n", + "- View: Close-up.\n", + "\"\"\"\n", + "new_id = AssetId.BRIDGE\n", + "\n", + "generate_image(source_ids, prompt, new_id)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "7KZF6bJ72jhj" + }, + "source": [ + "> 💡 Cool things to notice:\n", + ">\n", + "> - This imperative prompt composes the scene in terms of actions. It's sometimes easier than writing descriptions.\n", + "> - A new mountain is added as instructed, and it is both different and consistent.\n", + "> - The bridge attaches to the summits in very plausible ways and seems to obey the laws of physics.\n", + "> - The \"Remove the ice axes\" instruction is here for a reason. Without it, the model would need to decide what to do with them (leave them planted, have the robot carry them, or something else entirely), leading to random results.\n", + "> - It's also possible to get the robot to walk on the bridge, seen from the side (which we haven't generated before), but getting it to consistently walk from left to right is tricky. Adding left and right views in the character sheet should fix this.\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "sD_09k4d2jhj" + }, + "source": [ + "Let's generate a final scene and let the robot get some well-deserved rest:\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "TCfLo-PB2jhj" + }, + "outputs": [], + "source": [ + "source_ids = [AssetId.ROBOT, AssetId.BRIDGE]\n", + "prompt = \"\"\"\n", + "- Image 1: Robot character sheet.\n", + "- Image 2: Previous scene.\n", + "- The robot is sleeping peacefully in a comfortable brown-and-tan tartan hammock that has replaced the bridge.\n", + "- Both of the robot's eyes are closed, in a \"sleeping\" state.\n", + "\"\"\"\n", + "new_id = AssetId.HAMMOCK\n", + "\n", + "generate_image(source_ids, prompt, new_id)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "AL75Usr42jhj" + }, + "source": [ + "> 💡 What's interesting here:\n", + ">\n", + "> - This time, the prompt is descriptive, and it works just as well as the previous imperative one.\n", + "> - The bridge-hammock transformation is really nice and preserves the attachments on the mountain summits.\n", + "> - The robot's transformation is also impressive, as it hasn't been seen in this position before.\n", + "> - The closed eyes are the most difficult detail to get consistently (which might take a few tries), probably because we're accumulating many different transformations at once (and diluting the model's attention). For full control and more deterministic results, we can focus on significant changes over iterative steps, or create various character sheets upfront.\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "qtOBpebH2jhj" + }, + "source": [ + "We've illustrated our story with nine new, consistent images! Let's take a step back to understand what we've built
\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "2JZpWmZL2jhj" + }, + "source": [ + "---\n", + "\n", + "## đŸ—ș Graph visualization\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "l8jtvd542jhj" + }, + "source": [ + "We now have a collection of image assets, from the original archive to brand-new generated images.\n", + "\n", + "Let's add some data visualization to get a better sense of the steps completed
\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "R-U7ChRw2jhj" + }, + "source": [ + "---\n", + "\n", + "### 🔗 Directed graph\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "KJpes8dj2jhj" + }, + "source": [ + "Our new assets are all related, connected by one or more \"generated from\" links. In terms of data structures, this is a directed graph.\n", + "\n", + "We can build the corresponding directed graph using the `networkx` library:\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "5Lo20wLU2jhj" + }, + "outputs": [], + "source": [ + "import networkx as nx\n", + "\n", + "\n", + "def build_graph(assets: Assets) -> nx.DiGraph:\n", + " graph = nx.DiGraph(assets=assets)\n", + " # Nodes\n", + " for asset in assets.values():\n", + " graph.add_node(asset.id, asset=asset)\n", + " # Edges\n", + " for asset in assets.values():\n", + " for source_id in asset.source_ids:\n", + " graph.add_edge(source_id, asset.id)\n", + " return graph\n", + "\n", + "\n", + "asset_graph = build_graph(assets)\n", + "print(asset_graph)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "UcXNbeBk2jhj" + }, + "source": [ + "Let's place the most used asset in the center and display the other assets around it:\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "znWDmKoX2jhj" + }, + "outputs": [], + "source": [ + "import matplotlib.pyplot as plt\n", + "import numpy as np\n", + "\n", + "\n", + "def display_basic_graph(graph: nx.Graph) -> None:\n", + " pos = compute_node_positions(graph)\n", + " color = \"#4285F4\"\n", + " nx.draw(\n", + " graph,\n", + " pos,\n", + " with_labels=True,\n", + " node_color=color,\n", + " edge_color=color,\n", + " arrowstyle=\"wedge\",\n", + " font_size=\"small\",\n", + " bbox=dict(ec=\"black\", fc=\"white\", alpha=0.7),\n", + " )\n", + " plt.show()\n", + "\n", + "\n", + "def compute_node_positions(graph: nx.Graph) -> dict[str, np.ndarray]:\n", + " # Put the most connected node in the center\n", + " center_node = most_connected_node(graph)\n", + " edge_nodes = set(graph) - {center_node}\n", + " pos = nx.circular_layout(graph.subgraph(edge_nodes))\n", + " pos[center_node] = np.array([0.0, 0.0])\n", + " return pos\n", + "\n", + "\n", + "def most_connected_node(graph: nx.Graph) -> str:\n", + " if not graph.nodes():\n", + " return \"\"\n", + " centrality_by_id = nx.degree_centrality(graph)\n", + " return max(centrality_by_id, key=lambda s: centrality_by_id.get(s, 0.0))\n", + "\n", + "\n", + "display_basic_graph(asset_graph)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "sCdNWkpU2jhj" + }, + "source": [ + "That's an accurate summary of our different steps. It would be nice if we could also visualize our assets
\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "HlBKVFBK2jhj" + }, + "source": [ + "---\n", + "\n", + "### 🌟 Asset graph\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "ioowOWhb2jhj" + }, + "source": [ + "Let's add custom `matplotlib` functions to render the graph nodes with the assets in a more visually appealing way:\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "TdZBthQA2jhj" + }, + "outputs": [], + "source": [ + "# @title {display-mode: \"form\"}\n", + "import typing\n", + "from collections.abc import Iterator\n", + "from pathlib import Path\n", + "\n", + "import PIL.Image\n", + "import PIL.ImageDraw\n", + "from google.genai.types import PIL_Image\n", + "from matplotlib.axes import Axes\n", + "from matplotlib.backends.backend_agg import FigureCanvasAgg\n", + "from matplotlib.figure import Figure\n", + "from matplotlib.image import AxesImage\n", + "from matplotlib.patches import Patch\n", + "from matplotlib.text import Annotation\n", + "from matplotlib.transforms import Bbox, TransformedBbox\n", + "\n", + "\n", + "@enum.unique\n", + "class ImageFormat(enum.StrEnum):\n", + " # Matches PIL.Image.Image.format\n", + " WEBP = enum.auto()\n", + " PNG = enum.auto()\n", + " GIF = enum.auto()\n", + "\n", + "\n", + "def yield_generation_graph_frames(\n", + " graph: nx.DiGraph,\n", + " animated: bool,\n", + ") -> Iterator[PIL_Image]:\n", + " def get_fig_ax() -> tuple[Figure, Axes]:\n", + " factor = 1.0\n", + " figsize = (16 * factor, 9 * factor)\n", + " fig, ax = plt.subplots(figsize=figsize)\n", + " fig.tight_layout(pad=3)\n", + " handles = [\n", + " Patch(color=COL_OLD, label=\"Archive\"),\n", + " Patch(color=COL_NEW, label=\"Generated\"),\n", + " ]\n", + " ax.legend(handles=handles, loc=\"lower right\")\n", + " ax.set_axis_off()\n", + " return fig, ax\n", + "\n", + " def prepare_graph() -> None:\n", + " arrows = nx.draw_networkx_edges(graph, pos, ax=ax)\n", + " if isinstance(arrows, list):\n", + " for arrow in arrows:\n", + " arrow.set_visible(False)\n", + "\n", + " def get_box_size() -> tuple[float, float]:\n", + " xlim_l, xlim_r = ax.get_xlim()\n", + " ylim_t, ylim_b = ax.get_ylim()\n", + " factor = 0.08\n", + " box_w = (xlim_r - xlim_l) * factor\n", + " box_h = (ylim_b - ylim_t) * factor\n", + " return box_w, box_h\n", + "\n", + " def add_axes() -> Axes:\n", + " xf, yf = tr_figure(pos[node])\n", + " xa, ya = tr_axes([xf, yf])\n", + " x_y_w_h = (xa - box_w / 2.0, ya - box_h / 2.0, box_w, box_h)\n", + " a = plt.axes(x_y_w_h)\n", + " a.set_title(\n", + " asset.id,\n", + " loc=\"center\",\n", + " backgroundcolor=\"#FFF8\",\n", + " fontfamily=\"monospace\",\n", + " fontsize=\"small\",\n", + " )\n", + " a.set_axis_off()\n", + " return a\n", + "\n", + " def draw_box(color: str, image: bool) -> AxesImage:\n", + " if image:\n", + " result = pil_image.copy()\n", + " else:\n", + " result = PIL.Image.new(\"RGB\", image_size, color=\"white\")\n", + " xy = ((0, 0), image_size)\n", + " # Draw box outline\n", + " draw = PIL.ImageDraw.Draw(result)\n", + " draw.rounded_rectangle(xy, box_r, outline=color, width=outline_w)\n", + " # Make everything outside the box outline transparent\n", + " mask = PIL.Image.new(\"L\", image_size, 0)\n", + " draw = PIL.ImageDraw.Draw(mask)\n", + " draw.rounded_rectangle(xy, box_r, fill=0xFF)\n", + " result.putalpha(mask)\n", + " return a.imshow(result)\n", + "\n", + " def draw_prompt() -> Annotation:\n", + " text = f\"Prompt:\\n{asset.prompt}\"\n", + " margin = 2 * outline_w\n", + " image_w, image_h = image_size\n", + " bbox = Bbox([[0, margin], [image_w - margin, image_h - margin]])\n", + " clip_box = TransformedBbox(bbox, a.transData)\n", + " return a.annotate(\n", + " text,\n", + " xy=(0, 0),\n", + " xytext=(0.06, 0.5),\n", + " xycoords=\"axes fraction\",\n", + " textcoords=\"axes fraction\",\n", + " verticalalignment=\"center\",\n", + " fontfamily=\"monospace\",\n", + " fontsize=\"small\",\n", + " linespacing=1.3,\n", + " annotation_clip=True,\n", + " clip_box=clip_box,\n", + " )\n", + "\n", + " def draw_edges() -> None:\n", + " STYLE_STRAIGHT = \"arc3\"\n", + " STYLE_CURVED = \"arc3,rad=0.15\"\n", + " for parent in graph.predecessors(node):\n", + " edge = (parent, node)\n", + " color = COL_NEW if assets[parent].prompt else COL_OLD\n", + " style = STYLE_STRAIGHT if center_node in edge else STYLE_CURVED\n", + " nx.draw_networkx_edges(\n", + " graph,\n", + " pos,\n", + " [edge],\n", + " width=2,\n", + " edge_color=color,\n", + " style=\"dotted\",\n", + " ax=ax,\n", + " connectionstyle=style,\n", + " )\n", + "\n", + " def get_frame() -> PIL_Image:\n", + " canvas = typing.cast(FigureCanvasAgg, fig.canvas)\n", + " canvas.draw()\n", + " image_size = canvas.get_width_height(physical=True)\n", + " image_bytes = canvas.buffer_rgba()\n", + " return PIL.Image.frombytes(\"RGBA\", image_size, image_bytes).convert(\"RGB\")\n", + "\n", + " COL_OLD = \"#34A853\"\n", + " COL_NEW = \"#4285F4\"\n", + " assets = graph.graph[\"assets\"]\n", + " center_node = most_connected_node(graph)\n", + " pos = compute_node_positions(graph)\n", + " fig, ax = get_fig_ax()\n", + " prepare_graph()\n", + " box_w, box_h = get_box_size()\n", + " tr_figure = ax.transData.transform # Data → display coords\n", + " tr_axes = fig.transFigure.inverted().transform # Display → figure coords\n", + "\n", + " for node, data in graph.nodes(data=True):\n", + " if animated:\n", + " yield get_frame()\n", + " # Edges and sub-plot\n", + " asset = data[\"asset\"]\n", + " pil_image = asset.pil_image\n", + " image_size = pil_image.size\n", + " box_r = min(image_size) * 25 / 100 # Radius for rounded rect\n", + " outline_w = min(image_size) * 5 // 100\n", + " draw_edges()\n", + " a = add_axes() # a is used in sub-functions\n", + " # Prompt\n", + " if animated and asset.prompt:\n", + " box = draw_box(COL_NEW, image=False)\n", + " prompt = draw_prompt()\n", + " yield get_frame()\n", + " box.set_visible(False)\n", + " prompt.set_visible(False)\n", + " # Generated image\n", + " color = COL_NEW if asset.prompt else COL_OLD\n", + " draw_box(color, image=True)\n", + "\n", + " plt.close()\n", + " yield get_frame()\n", + "\n", + "\n", + "def draw_generation_graph(\n", + " graph: nx.DiGraph,\n", + " format: ImageFormat,\n", + ") -> BytesIO:\n", + " frames = list(yield_generation_graph_frames(graph, animated=False))\n", + " assert len(frames) == 1\n", + " frame = frames[0]\n", + "\n", + " params: dict[str, typing.Any] = dict()\n", + " match format:\n", + " case ImageFormat.WEBP:\n", + " params.update(lossless=True)\n", + "\n", + " image_io = BytesIO()\n", + " frame.save(image_io, format, **params)\n", + "\n", + " return image_io\n", + "\n", + "\n", + "def draw_generation_graph_animation(\n", + " graph: nx.DiGraph,\n", + " format: ImageFormat,\n", + ") -> BytesIO:\n", + " frames = list(yield_generation_graph_frames(graph, animated=True))\n", + " assert 1 <= len(frames)\n", + "\n", + " if format == ImageFormat.GIF:\n", + " # Dither all frames with the same palette to optimize the animation\n", + " # The animation is cumulative, so most colors are in the last frame\n", + " method = PIL.Image.Quantize.MEDIANCUT\n", + " palettized = frames[-1].quantize(method=method)\n", + " frames = [frame.quantize(method=method, palette=palettized) for frame in frames]\n", + "\n", + " # The animation will be played in a loop: start cycling with the most complete frame\n", + " first_frame = frames[-1]\n", + " next_frames = frames[:-1]\n", + " INTRO_DURATION = 3000\n", + " FRAME_DURATION = 1000\n", + " durations = [INTRO_DURATION] + [FRAME_DURATION] * len(next_frames)\n", + " params: dict[str, typing.Any] = dict(\n", + " save_all=True,\n", + " append_images=next_frames,\n", + " duration=durations,\n", + " loop=0,\n", + " )\n", + " match format:\n", + " case ImageFormat.GIF:\n", + " params.update(optimize=False)\n", + " case ImageFormat.WEBP:\n", + " params.update(lossless=True)\n", + "\n", + " image_io = BytesIO()\n", + " first_frame.save(image_io, format, **params)\n", + "\n", + " return image_io\n", + "\n", + "\n", + "def display_generation_graph(\n", + " graph: nx.DiGraph,\n", + " format: ImageFormat | None = None,\n", + " animated: bool = False,\n", + " save_image: bool = False,\n", + ") -> None:\n", + " if format is None:\n", + " format = ImageFormat.WEBP if running_in_colab_env() else ImageFormat.PNG\n", + " if animated:\n", + " image_io = draw_generation_graph_animation(graph, format)\n", + " else:\n", + " image_io = draw_generation_graph(graph, format)\n", + "\n", + " image_bytes = image_io.getvalue()\n", + " IPython.display.display(IPython.display.Image(image_bytes))\n", + "\n", + " if save_image:\n", + " stem = \"graph_animated\" if animated else \"graph\"\n", + " Path(f\"./{stem}.{format.value}\").write_bytes(image_bytes)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "pfX1N7-q2jhk" + }, + "source": [ + "We can now display our generation graph:\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "TnwQk6fM2jhk" + }, + "outputs": [], + "source": [ + "display_generation_graph(asset_graph)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "d1YcSgMvy_fl" + }, + "source": [ + "---\n", + "\n", + "### â„č Asset metadata\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "ED1Sgui6y_fm" + }, + "source": [ + "Have you ever generated something nice without saving the exact context?\n", + "\n", + "Our assets can be made self-sufficient by saving prompts and ancestors in the image metadata (e.g., in PNG chunks), allowing for full local storage and retrieval (without needing a database or worrying about lost prompts!).\n", + "\n", + "Let's add archive and restore functions, which might come in handy:\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "59o5AZgoy_fm" + }, + "outputs": [], + "source": [ + "# @title {display-mode: \"form\"}\n", + "from datetime import datetime\n", + "\n", + "from PIL.PngImagePlugin import PngInfo\n", + "\n", + "\n", + "def save_assets(assets: Assets) -> Path:\n", + " # Saves assets to a dated folder\n", + " now_here = datetime.now().astimezone()\n", + " folder = Path(\".\") / now_here.strftime(\"%Y-%m-%d_%H-%M-%S\")\n", + " folder.mkdir()\n", + " for asset in assets.values():\n", + " save_asset(asset, folder)\n", + " return folder\n", + "\n", + "\n", + "def save_asset(asset: Asset, folder: Path) -> None:\n", + " image_path = folder / f\"{asset.id}.png\"\n", + " assert not image_path.is_file()\n", + " metadata = PngInfo()\n", + " metadata.add_text(\"source_ids\", \"\\n\".join(asset.source_ids))\n", + " metadata.add_text(\"prompt\", asset.prompt)\n", + " asset.pil_image.save(image_path, pnginfo=metadata)\n", + "\n", + "\n", + "def load_assets(folder: Path) -> Assets:\n", + " assets = Assets()\n", + " for image_path in folder.glob(\"*.png\"):\n", + " assets.set_asset(load_asset(image_path))\n", + " return assets\n", + "\n", + "\n", + "def load_asset(image_path: Path) -> Asset:\n", + " asset_id = image_path.stem\n", + " image = PIL.Image.open(image_path)\n", + " image_info = image.info\n", + " source_ids = image_info.get(\"source_ids\", \"\")\n", + " source_ids = source_ids.split(\"\\n\") if source_ids else []\n", + " prompt = image_info.get(\"prompt\", \"\")\n", + " return Asset(asset_id, source_ids, prompt, image)\n", + "\n", + "\n", + "def demonstrate_save_and_restore(assets: Assets) -> None:\n", + " # Save\n", + " folder = save_assets(assets)\n", + " # Restore & use\n", + " restored_assets = load_assets(folder)\n", + " graph = build_graph(restored_assets)\n", + " display_generation_graph(graph)\n", + "\n", + "\n", + "# Uncomment to test\n", + "# demonstrate_save_and_restore(assets)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "RFO_u1-8hj3-" + }, + "source": [ + "---\n", + "\n", + "## 🚀 Challenge completed\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "nubJgw0Ahj3-" + }, + "source": [ + "We generated a full set of new, consistent images with Nano Banana, and we learned a few things along the way:\n", + "\n", + "- This proves once again that an image is worth a thousand words: It's now a lot easier to generate new images from existing ones using simple instructions.\n", + "- We can create or edit images purely through composition (letting us all become artistic directors).\n", + "- We can use descriptive or imperative instructions.\n", + "- The model's spatial understanding enables 3D manipulations.\n", + "- We can add text to our outputs (character sheet) and also refer to text in our inputs (front/back views).\n", + "- Consistency can be preserved across various levels: character, scene, texture, lighting, camera angle/type
\n", + "- The generation process can still be iterative, but it feels 10x to 100x faster to reach results that exceed expectations.\n", + "- It is now possible to breathe new life into our archives!\n", + "\n", + "Possible next steps:\n", + "\n", + "- The process we followed is essentially a generation pipeline. It can be adapted for automation (e.g., changing a node regenerates its descendants) or to generate multiple variations in parallel (e.g., the same set of images could be generated for different aesthetics, audiences, or simulations).\n", + "- For the sake of clarity and exploration, the prompts are intentionally simple. In a production environment, they could have a fixed structure with a standardized set of parameters.\n", + "- We described the scenes as if they were shot in a photo studio. Virtually any other imaginable artistic style is possible (photorealistic, abstract, 2D
).\n", + "\n", + "As a bonus, let's end with an animated version of our journey, with the generation graph also showing a glimpse of our instructions:\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "wiTx1Nk0y_fm" + }, + "outputs": [], + "source": [ + "display_generation_graph(asset_graph, animated=True)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "huldfEHkhj3-" + }, + "source": [ + "---\n", + "\n", + "## ➕ More!\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "H_-odeqEln_s" + }, + "source": [ + "- Check out the [Nano Banana recipes notebook](https://github.com/GoogleCloudPlatform/generative-ai/blob/main/gemini/nano-banana/nano_banana_recipes.ipynb) for more practical examples.\n", + "- Explore typical use cases in the [Agent Platform Prompt Gallery](https://console.cloud.google.com/agent-platform/studio/prompt-gallery)\n", + "- Stay updated with the [Agent Platform Release Notes](https://docs.cloud.google.com/gemini-enterprise-agent-platform/release-notes)\n" + ] + } + ], + "metadata": { + "colab": { + "provenance": [ + { + "file_id": "1pmb_xmjaw8F4reXMLba3RO3QzxkGpuJd", + "timestamp": 1736858287414 + } + ], + "toc_visible": true + }, + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.10" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} diff --git a/gemini/use-cases/multimodal-data-curation/README.md b/gemini/use-cases/multimodal-data-curation/README.md new file mode 100644 index 0000000..7e78e03 --- /dev/null +++ b/gemini/use-cases/multimodal-data-curation/README.md @@ -0,0 +1,76 @@ +# Building the Foundation for Breakthroughs: A Multimodal Data Curation Pipeline for Text-to-Video Models + +## Authors + +- Noa Ben-Efraim (`noabe`) +- John Semerdjian (`jsemer`) + + +In the rapidly evolving landscape of AI, Text-to-Video (T2V) models are capturing imaginations, promising to revolutionize content creation from film to marketing. But for all their dazzling potential, the real magic isn't just in the model architecture; it's in the data that feeds them. High-quality, diverse, and well-curated multimodal data is the secret sauce for building truly exceptional T2V models. + +That's where our new initiative comes in. We're thrilled to introduce a comprehensive solution designed specifically for organizations embarking on their journey into large-scale text-to-video model building – a fully built, serverless data curation pipeline on Google Cloud, accompanied by a blog series that dives deep into its operation and the strategic choices behind it. + +## Why Data is the New Frontier in Text-to-Video + +Just a short while ago, the focus in generative AI was heavily on novel model architectures. Today, with many powerful architectures being open-sourced and commoditized, the competitive edge has shifted. The true advantage now lies in the quality, quantity, and diversity of the underlying data used for training. + +Think of it this way: a brilliant artist can't create a masterpiece without high-quality paints and brushes. Similarly, even the most sophisticated T2V model will struggle to produce compelling results if it's trained on subpar or insufficient data. Data processing and filtering pipelines, once niche knowledge, have now become standardized best practices across leading open-source model builders. + +> Our mission is to aggregate these proven recipes and techniques into an easily deployable, robust pipeline and then, through this blog series, demystify the trade-offs and granular details. This isn't just about sharing code; it's about elevating the collective understanding of multimodal data curation, driving innovation, and demonstrating the immense value of Google Cloud's managed services. + +## What We're Offering: A Two-Pronged Approach + +1. **A Fully Built, Serverless Architecture + Working Pipeline**: We've engineered a complete, ready-to-deploy pipeline for curating pre-training data for text-to-video models. Built on a serverless architecture, it minimizes maintenance costs and leverages Google Cloud's pay-what-you-use pricing model. Our packaged pipelines implement the best video data curation techniques honed over the last few years, making advanced practices accessible. + +2. **A Comprehensive Blog Series**: This series will be your guide through the entire pipeline. We'll walk you through each step at a high level, explaining how to operate it effectively on top of Cloud resources. More importantly, we'll delve into the crucial trade-offs you'll encounter, providing insights backed by both cutting-edge research papers and practical links to public Cloud documentation. + +## Navigating the Data Curation Journey: Our Pipeline's Pillars + +Building a high-quality text-to-video dataset is a complex endeavor. Our pipeline addresses common challenges head-on, structured around key pillars that tackle specific pain points. + +### 1. Video Splitting + +* **Pain Point:** Raw video files are often too long and contain multiple distinct scenes. Manually segmenting these videos is time-consuming and prone to inconsistencies. +* **Our Solution:** We leverage advanced techniques like `PySceneDetect` with boundary detection and offer options for short clip creation using `MoviePy`, enabling efficient and accurate segmentation of long videos into manageable, meaningful clips. + +### 2. Quality Filtering + +* **Pain Point:** Not all video content is suitable for training. Low resolution, poor brightness, watermarks, or very short clips can degrade model performance. +* **Our Solution:** Our pipeline incorporates robust visual filtering based on resolution, brightness, aspect ratio, text boundary detection, and watermark presence. It also filters by minimum FPS to ensure smooth video. Furthermore, we include model-based filtering using techniques like aesthetic scoring (e.g., `LAION-Aesthetics`) and temporal consistency checks (e.g., using `CLIP`) to ensure only visually appealing and coherent content makes it through. + +### 3. Motion Filtering + +* **Pain Point:** T2V models benefit from understanding motion, but videos with too little or too much chaotic motion (e.g., slideshows, blurry camera shakes) can introduce noise. +* **Our Solution:** We implement sophisticated motion analysis using metrics like **VMAF** (Video Multimethod Assessment Fusion) for optical flow and `PySceneDetect` for static scene detection. This helps identify and filter out clips with no slow motion or excessive jitter. + +### 4. Captioning & Tagging + +* **Pain Point:** High-quality text descriptions are paramount for T2V models. Manual captioning is prohibitively expensive, and generic captions limit a model's learning capabilities. +* **Our Solution:** This pillar leverages Google Cloud's **Vertex AI** for automated Video Captioning and Tagging. We employ powerful models like **Gemini** for classifying camera motion (zoom, pan, etc.) and generating rich clip taxonomies. Additionally, we use Embedding APIs to create multimodal embeddings for semantic understanding. + +### 5. Deduplication + +* **Pain Point:** Large datasets often contain near-duplicate videos and captions, which can lead to overfitting and inefficient training. +* **Our Solution:** We address this with semantic deduplication via K-Means clustering on multimodal embeddings. This method effectively groups similar clips and captions, allowing for intelligent removal of redundant data. + +Each of these pillars is orchestrated using Google Cloud's powerful services like **Dataflow**, ensuring a scalable, serverless, and robust solution for handling even the largest video datasets. + +## Target Audience + +This solution is tailor-made for customers relatively new to building proprietary text-to-video models, such as: +* Film and TV studios +* Educational content creators +* Marketing agencies looking to leverage generative AI + +Our goal is to evangelize the power of Google Cloud's managed service offerings – including **Dataflow**, **BigQuery**, **Vector Search**, and **Gemini** – to demonstrate the tangible business value and productivity gains that migrating to Google Cloud can unlock. + +**Who it's not for:** Advanced research organizations that have already settled on alternative orchestration frameworks (like Ray or Spark) for their existing, highly specialized pipelines. + +## Unlocking Business Value and Driving Innovation + +By providing this pipeline and accompanying guidance, we aim to achieve several key business outcomes: + +* **Increased Thought Leadership:** Establish Google Cloud as a leading voice in the nascent but rapidly growing field of multimodal data curation for generative AI. +* **Increased Revenue Across Cloud SKUs:** Drive adoption across multiple Google Cloud services by showcasing the seamless integration and power of Dataflow, BigQuery, Vector Search, and Gemini. + +The quality of your models will be directly proportional to the quality of your data. Our multimodal data curation pipeline provides the robust foundation you need to build the next generation of creative AI applications. \ No newline at end of file diff --git a/gemini/use-cases/multimodal-data-curation/captioning.ipynb b/gemini/use-cases/multimodal-data-curation/captioning.ipynb new file mode 100644 index 0000000..626682e --- /dev/null +++ b/gemini/use-cases/multimodal-data-curation/captioning.ipynb @@ -0,0 +1,433 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "ur8xi4C7S06n" + }, + "outputs": [], + "source": [ + "# Copyright 2025 Google LLC\n", + "#\n", + "# Licensed under the Apache License, Version 2.0 (the \"License\");\n", + "# you may not use this file except in compliance with the License.\n", + "# You may obtain a copy of the License at\n", + "#\n", + "# https://www.apache.org/licenses/LICENSE-2.0\n", + "#\n", + "# Unless required by applicable law or agreed to in writing, software\n", + "# distributed under the License is distributed on an \"AS IS\" BASIS,\n", + "# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n", + "# See the License for the specific language governing permissions and\n", + "# limitations under the License." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "JAPoU8Sm5E6e" + }, + "source": [ + "# Video Captioning with Gemini\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + "
    \n", + " \n", + " \"Google
    Open in Colab\n", + "
    \n", + "
    \n", + " \n", + " \"Google
    Open in Colab Enterprise\n", + "
    \n", + "
    \n", + " \n", + " \"Vertex
    Open in Vertex AI Workbench\n", + "
    \n", + "
    \n", + " \n", + " \"GitHub
    View on GitHub\n", + "
    \n", + "
    \n", + "\n", + "
    \n", + "\n", + "Share to:\n", + "\n", + "\n", + " \"LinkedIn\n", + "\n", + "\n", + "\n", + " \"Bluesky\n", + "\n", + "\n", + "\n", + " \"X\n", + "\n", + "\n", + "\n", + " \"Reddit\n", + "\n", + "\n", + "\n", + " \"Facebook\n", + "" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "84f0f73a0f76" + }, + "source": [ + "| Author(s) |\n", + "| --- |\n", + "| [Noa Ben-Efraim](https://github.com/noabenefraim) |" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "tvgnzT1CKxrO" + }, + "source": [ + "## Overview\n", + "\n", + "This notebook details the application of the Gemini model for video captioning within our Video Language Model (VLM) training data curation pipeline. \n", + "\n", + "This process is a critical step because it establishes the necessary textual correspondence for effective multi-modal learning. By generating rich captions that describe the visual and auditory content of the video, we enable the VLM to learn joint embeddings and understand the complex interplay between different modalities. High-quality captions serve as a structured representation of the video's semantic content, directly contributing to the performance of the trained VLM on various downstream tasks. \n", + "\n", + "This notebook will cover:\n", + "+ How to set up a meaningful system prompt to cater your dataset \n", + "+ Detailed example that highlights Gemini's multi-modal capabilities that capture audio, visual, and text inputs from the video." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "f4cbc20d4056" + }, + "source": [ + "## Important Note\n", + "\n", + "This notebook explores methods for video captioning. However, it is crucial to understand that using the output or capabilities of the Gemini API to train other models that compete with Gemini API or Google AI Studio is a violation of the [Gemini API Additional Terms of Service](https://ai.google.dev/gemini-api/terms).\n", + "\n", + "Specifically, the terms state under \"Use Restrictions\": \"You may not use the Services to develop models that compete with the Services (e.g., Gemini API or Google AI Studio).\" Attempting to use data generated by Gemini, such as video captions produced by the API, as a dataset for training a separate video captioning model could fall under this restriction.\n", + "\n", + "Users are responsible for ensuring their use of the Gemini API complies with all applicable terms and policies. Proceeding with actions that violate the terms of service may result in suspension or termination of access to the service." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "61RBz8LLbxCR" + }, + "source": [ + "## Get started" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "No17Cw5hgx12" + }, + "source": [ + "### Install Google Gen AI SDK and other required packages\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "tFy3H3aPgx12" + }, + "outputs": [], + "source": [ + "%pip install --upgrade --quiet google-genai" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "dmWOrTJ3gx13" + }, + "source": [ + "### Authenticate your notebook environment (Colab only)\n", + "\n", + "If you're running this notebook on Google Colab, run the cell below to authenticate your environment." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "NyKGtVQjgx13" + }, + "outputs": [], + "source": [ + "import sys\n", + "\n", + "if \"google.colab\" in sys.modules:\n", + " from google.colab import auth\n", + "\n", + " auth.authenticate_user()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "DF4l8DTdWgPY" + }, + "source": [ + "### Set Google Cloud project information\n", + "\n", + "To get started using Vertex AI, you must have an existing Google Cloud project and [enable the Vertex AI API](https://console.cloud.google.com/flows/enableapi?apiid=aiplatform.googleapis.com).\n", + "\n", + "Learn more about [setting up a project and a development environment](https://cloud.google.com/vertex-ai/docs/start/cloud-environment)." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": { + "id": "Nqwi-5ufWp_B" + }, + "outputs": [], + "source": [ + "# Use the environment variable if the user doesn't provide Project ID.\n", + "import os\n", + "\n", + "PROJECT_ID = \"[your-project-id]\" # @param {type: \"string\", placeholder: \"[your-project-id]\", isTemplate: true}\n", + "if not PROJECT_ID or PROJECT_ID == \"[your-project-id]\":\n", + " PROJECT_ID = str(os.environ.get(\"GOOGLE_CLOUD_PROJECT\"))\n", + "\n", + "LOCATION = os.environ.get(\"GOOGLE_CLOUD_REGION\", \"us-central1\")\n", + "BUCKET = \"[your-gcs-bucket]\"\n", + "VIDEO_NAME = \"[your-video-name]\"\n", + "\n", + "from google import genai\n", + "\n", + "client = genai.Client(vertexai=True, project=PROJECT_ID, location=LOCATION)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "5303c05f7aa6" + }, + "source": [ + "### Import libraries" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "6fc324893334" + }, + "outputs": [], + "source": [ + "import os\n", + "from typing import Any\n", + "\n", + "from google import genai\n", + "from google.genai import types" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "01601b5549d5" + }, + "source": [ + "### Sample Video\n", + "\n", + "This notebook will use a sample video clip from the VidGen_1M dataset to illustrate the application of Gemini for comprehensive video captioning. Before proceeding, please review the video. You will observe a rich array of multi-modal information, including: textual elements present in tickets, on-screen graphics, and illustrations; visual elements depicting the weatherman, their attire, and the information conveyed through graphics and maps; and distinct audio elements comprising the weatherman's spoken content that is not explicitly represented visually or textually.\n", + "\n", + "Please refer to the sample video found [here](gs://github-repo/generative-ai/gemini/use-cases/multimodal-data-curation/_sNnSQ6cQYk-Scene-0006.mp4)." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "a71e75815bdf" + }, + "source": [ + "" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "917ae545862c" + }, + "source": [ + "#### System Prompt\n", + "\n", + "Below we have outlined a system prompt to use. This system prompt is designed to guide Gemini in generating detailed video captions that focus on capturing dynamic elements like camera movement and subject motion. Key instructions emphasize describing the scene from a specific perspective, using vivid language to illustrate actions and atmosphere, and including details about the setting, subject appearance, body language, and any visible text. The overarching goal is to create rich, nuanced captions that provide a comprehensive understanding of the video's content and context." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": { + "id": "af61d03f1280" + }, + "outputs": [], + "source": [ + "system_instructions = \"\"\"Generate a detailed video caption that prioritizes capturing the scene's motion, perspective, and nuanced descriptions. Focus on using strong verbs and adverbs to convey the action and atmosphere. Only return the caption. Do not send a preamble. \n", + "\n", + "# Key Elements to Emphasize\n", + "\n", + "* Perspective and Camera Movement: Specify the viewer's position relative to the scene (e.g., \\\"from the perspective of a toddler,\\\" \\\"from a low angle,\\\" \\\"from a high angle,\\\" \\\"a wide shot\\\").\n", + "* Describe any camera movement, including direction (e.g., \\\"strafing left,\\\" \\\"panning right,\\\" \\\"zooming in,\\\" \\\"tracking the subject\\\").\n", + "* Describe the relative size of the subject in the frame.\n", + "* Subject Motion and Actions: Use vivid verbs to illustrate the subject's actions (e.g., \\\"standing,\\\" \\\"sitting,\\\" \\\"walking,\\\" \\\"bowing,\\\" \\\"playing,\\\" \\\"illuminating\\\").\n", + "* Employ adverbs to enhance the description of the actions (e.g., \\\"slowly strafing,\\\" \\\"intensely focusing,\\\" \\\"dimly lit,\\\" \\\"dramatically illuminating\\\").\n", + "* Describe the direction of motion.\n", + "* Describe the speed of motion.\n", + "* Scene Composition and Atmosphere: Describe the setting in detail (e.g., \\\"grand piano on a stage,\\\" \\\"empty auditorium,\\\" \\\"dimly lit theater\\\").\n", + "* Capture the emotional tone or atmosphere (e.g., \\\"dramatic,\\\" \\\"solitude,\\\" \\\"introspection\\\").\n", + "* Describe the lighting.\n", + "* Describe the relative positions of the objects in the scene.\n", + "* Subject Details: Provide specific details about the subject's appearance (e.g., \\\"man in a gray suit\\\").\n", + "* Describe the subjects body language (e.g. \\\"takes a deep bow that wraps his arms around his back and abdomen\\\").\n", + "* Include references to text that is visible in the video.\"\"\"" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "e43229f3ad4f" + }, + "source": [ + "#### Load model" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "cf93d5f0ce00" + }, + "outputs": [], + "source": [ + "MODEL_ID = \"gemini-2.5-flash\" # @param {type:\"string\"}" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "1294dfb411f9" + }, + "source": [ + "#### Define helper functions" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "5c0804536c54" + }, + "outputs": [], + "source": [ + "def get_captions_from_video(\n", + " video_uri: str,\n", + " model: str = MODEL_ID,\n", + " system_instructions: str = system_instructions,\n", + " temperature: float = 0.7,\n", + " count_video_tokens: bool = True,\n", + ") -> dict[str, Any]:\n", + "\n", + " contents = [\n", + " types.Part.from_text(text=\"Caption this video\"),\n", + " types.Part.from_uri(file_uri=video_uri, mime_type=\"video/mp4\"),\n", + " ]\n", + "\n", + " video_token_count = None\n", + " if count_video_tokens:\n", + " video_token_count = client.models.count_tokens(\n", + " model=\"gemini-2.0-flash-001\",\n", + " contents=[types.Part.from_uri(file_uri=video_uri, mime_type=\"video/mp4\")],\n", + " ).total_tokens\n", + "\n", + " response = client.models.generate_content(\n", + " model=model,\n", + " contents=contents,\n", + " config=types.GenerateContentConfig(\n", + " system_instruction=system_instructions,\n", + " temperature=temperature,\n", + " ),\n", + " )\n", + "\n", + " return {\n", + " \"prompt_token_count\": response.usage_metadata.prompt_token_count,\n", + " \"candidates_token_count\": response.usage_metadata.candidates_token_count,\n", + " \"total_token_count\": response.usage_metadata.total_token_count,\n", + " \"model_version\": response.model_version,\n", + " \"text\": response.candidates[0].content.parts[0].text,\n", + " \"video_only_token_count\": video_token_count,\n", + " }" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "7008b586e58b" + }, + "outputs": [], + "source": [ + "# To test this yourself, run this cell\n", + "response = get_captions_from_video(\n", + " \"gs://github-repo/generative-ai/gemini/use-cases/multimodal-data-curation/_sNnSQ6cQYk-Scene-0006.mp4\"\n", + ")\n", + "response" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "de8be65f8c7f" + }, + "source": [ + "#### Sample output\n", + "\n", + "_From the perspective of a news viewer, the camera remains fixed on a weather report. A bald, middle-aged Caucasian man in a dark suit and magenta tie stands confidently on the right side of the screen. He gestures toward a graphic displaying the Omaha, Nebraska skyline at night, complete with falling snow effects . The graphic boldly states \"TONIGHT 27° Wintry Mix\" , with wind from the North-Northwest at 10-15 mph. The threat tracker is listed as \"High.\", The man explains that the wintry mix will end tonight, but that everything will be iced over as the temperature drops into the upper 20s. A smaller graphic on the lower left shows a map of the region, with varying shades of purple indicating \"Ice Storm Warning\", for several counties, including Washington, York, Madison, and Wayne. A banner at the bottom of the screen warns of snapped power lines and falling tree branches due to ice accumulation. As the man continues his report, the graphic switches to \"TOMORROW 36° Partly Sunny\", with the threat tracker listed as \"Low.\" He concludes that sunshine will reemerge tomorrow and that temperatures will climb back to about 36 degrees._\n", + "\n", + "Captured from: video, text, audio" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "a9db47478fd4" + }, + "source": [ + "The detailed and multi-modal captions generated by Gemini are highly valuable for downstream tasks such as embedding. By capturing a rich array of visual, textual, and implied auditory information, these captions provide a strong semantic representation of the video content. \n", + "\n", + "Next in the data curation pipeline, we will embed these comprehensive captions allows for effective semantic deduplication within the data curation pipeline, enabling the identification and removal of near-duplicate video content based on their meaning and context, rather than just pixel-level similarity. This ensures a cleaner and more diverse dataset for VLM training." + ] + } + ], + "metadata": { + "colab": { + "name": "captioning.ipynb", + "toc_visible": true + }, + "kernelspec": { + "display_name": "Python 3", + "name": "python3" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} diff --git a/gemini/use-cases/multimodal-data-curation/quality-filtering.ipynb b/gemini/use-cases/multimodal-data-curation/quality-filtering.ipynb new file mode 100644 index 0000000..a90e91b --- /dev/null +++ b/gemini/use-cases/multimodal-data-curation/quality-filtering.ipynb @@ -0,0 +1,940 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "id": "9f0d0f32-23b4-41a6-b364-579da297c326" + }, + "outputs": [], + "source": [ + "# @title Copyright & License (click to expand)\n", + "# Copyright 2025 Google LLC\n", + "#\n", + "# Licensed under the Apache License, Version 2.0 (the \"License\");\n", + "# you may not use this file except in compliance with the License.\n", + "# You may obtain a copy of the License at\n", + "#\n", + "# https://www.apache.org/licenses/LICENSE-2.0\n", + "#\n", + "# Unless required by applicable law or agreed to in writing, software\n", + "# distributed under the License is distributed on an \"AS IS\" BASIS,\n", + "# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n", + "# See the License for the specific language governing permissions and\n", + "# limitations under the License." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "dd53d60c-97eb-4c72-91ea-f274a753ab34" + }, + "source": [ + "# Video Data Curation - Video Quality Filtering\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + "
    \n", + " \n", + " \"Google
    Open in Colab\n", + "
    \n", + "
    \n", + " \n", + " \"Google
    Open in Colab Enterprise\n", + "
    \n", + "
    \n", + " \n", + " \"Vertex
    Open in Workbench\n", + "
    \n", + "
    \n", + " \n", + " \"GitHub
    View on GitHub\n", + "
    \n", + "
    \n", + "\n", + "
    \n", + "\n", + "Share to:\n", + "\n", + "\n", + " \"LinkedIn\n", + "\n", + "\n", + "\n", + " \"Bluesky\n", + "\n", + "\n", + "\n", + " \"X\n", + "\n", + "\n", + "\n", + " \"Reddit\n", + "\n", + "\n", + "\n", + " \"Facebook\n", + " " + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "MgVK7IeKpW27" + }, + "source": [ + "| | |\n", + "|-|-|\n", + "|Author(s) | [John Semerdjian]" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "9ef820fb-1203-4cab-965f-17093a4ba25e" + }, + "source": [ + "## Overview\n", + "\n", + "After we have split and transcoded our initial set of videos, the next step in a video curation pipeline is to filter videos based on their quality. Our goal is to discard as many \"low quality\" clips as possible in order to use our limited modeling compute as efficiently as possible. \"Quality\" in this context refers to a collection of subjective and practical based on the developer's own requirements. The practical requirements here may translate into filters that correspond to video metadata like resolution or the absence of visible text, while subjective quality filters may target the aesthetics of the video itself. Since there is not a single set of filters or thresholds to use for _all_ video data curation pipelines, we will focus on the process of extracting and inferring severals fields from our video collection. The approaches below have been aggregated from multiple research papers for training text-to-video foundation models.\n", + "\n", + "This notebook is organized as follows:\n", + "* Metadata Filters\n", + "* OCR & Watermark Detection\n", + "* Aesthetic Scoring\n", + "* Motion Scores " + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "0cbf01f0-5f6e-4bcd-903f-84ccaad5332c" + }, + "source": [ + "### Install required packages" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "MpDAgOsK6kZn" + }, + "outputs": [], + "source": [ + "%pip install --upgrade --user --quiet google-genai google-cloud-storage pyav opencv-python numpy torch transformers pillow" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "Moror1y0Qq2z" + }, + "source": [ + "### Restart runtime (Colab only)\n", + "\n", + "To use the newly installed packages, you must restart the runtime on Google Colab." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "4KLm_nKmQtC8" + }, + "outputs": [], + "source": [ + "# Automatically restart kernel after installs so that your environment can access the new packages\n", + "import sys\n", + "\n", + "if \"google.colab\" in sys.modules:\n", + " from google.colab import auth\n", + "\n", + " auth.authenticate_user()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "b37d4259-7e39-417b-8879-24f7575732c8" + }, + "source": [ + "## Before you begin\n", + "\n", + "### Set your project ID\n", + "\n", + "**If you don't know your project ID**, try the following:\n", + "* Run `gcloud config list`.\n", + "* Run `gcloud projects list`.\n", + "* See the support page: [Locate the project ID](https://support.google.com/googleapi/answer/7014113)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "caaf0d7e-c6cb-4e56-af5c-553db5180e00" + }, + "outputs": [], + "source": [ + "PROJECT_ID = \"[YOUR_PROJECT_ID]\" # @param {type:\"string\"}\n", + "# Set the project id\n", + "! gcloud config set project {PROJECT_ID}" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "054d794d-cd2e-4280-95ac-859b264ea2d6" + }, + "source": [ + "#### Region\n", + "\n", + "You can also change the `REGION` variable used by Vertex AI. Learn more about [Vertex AI regions](https://cloud.google.com/vertex-ai/docs/general/locations)." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": { + "id": "0121bf60-1acd-4272-afaf-aa54b4ded263" + }, + "outputs": [], + "source": [ + "REGION = \"us-central1\" # @param {type:\"string\"}" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "czjH2JfKaGfH" + }, + "source": [ + "#### Bucket\n" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": { + "id": "c_iZzYtraF3y" + }, + "outputs": [], + "source": [ + "BUCKET_NAME = \"[YOUR_BUCKET_NAME]\" # @param {type:\"string\"}\n", + "BUCKET_URI = f\"gs://{BUCKET_NAME}\"" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "eac9e842-d225-4876-836f-afdb1937d800" + }, + "source": [ + "### Authenticate your Google Cloud account\n", + "\n", + "Depending on your Jupyter environment, you may have to manually authenticate. Follow the relevant instructions below.\n", + "\n", + "**1. Vertex AI Workbench**\n", + "* Do nothing as you are already authenticated.\n", + "\n", + "**2. Local JupyterLab instance, uncomment and run:**" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": { + "id": "23082eec-b1bd-4594-b5b5-56fe2b74db6f" + }, + "outputs": [], + "source": [ + "# ! gcloud auth login" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "3c20f923-3c46-4d6d-80d2-d7cb22b1a8da" + }, + "source": [ + "**3. Authenticate your notebook environment**\n", + "\n", + "If you are running this notebook on Google Colab, run the cell below to authenticate your environment." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "60302a3f-fad9-452c-8998-a9c9822d2732" + }, + "outputs": [], + "source": [ + "from google.colab import auth\n", + "\n", + "auth.authenticate_user()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "ac33116d-b079-46cb-9614-86326c211e00" + }, + "source": [ + "**4. Service account or other**\n", + "* See how to grant Cloud Storage permissions to your service account at https://cloud.google.com/storage/docs/gsutil/commands/iam#ch-examples." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "e6a924d0-a034-4e53-b240-03d356c7b7a6" + }, + "source": [ + "### Import libraries" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": { + "id": "5cbbd54d0e08" + }, + "outputs": [], + "source": [ + "import io\n", + "import json\n", + "import math\n", + "\n", + "from PIL import Image\n", + "import av\n", + "import cv2 as cv\n", + "from google import genai\n", + "from google.cloud import storage\n", + "from google.genai import types\n", + "import numpy as np\n", + "import torch\n", + "from torch import nn\n", + "from transformers import CLIPModel, CLIPProcessor\n", + "\n", + "storage_client = storage.Client(project=PROJECT_ID)\n", + "gemini_client = genai.Client(vertexai=True, project=PROJECT_ID, location=REGION)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "727c349d4873" + }, + "source": [ + "### Load video paths from Cloud Storage\n", + "\n", + "We assume you have video data already stored in a Cloud Storage bucket. We will read a few videos and demonsrate their outputs in this notebook." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": { + "id": "ba417857ce4d" + }, + "outputs": [], + "source": [ + "def load_video_paths(\n", + " bucket: str,\n", + " num_videos: int,\n", + ") -> list[str]:\n", + " video_blobs = []\n", + " for i, blob in enumerate(storage_client.list_blobs(bucket)):\n", + " if i >= num_videos:\n", + " break\n", + " video_blobs.append(blob.name)\n", + " return video_blobs" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": { + "id": "080d79c0121a" + }, + "outputs": [], + "source": [ + "videos = load_video_paths(BUCKET_NAME, 3)\n", + "bucket = storage_client.get_bucket(BUCKET_NAME)\n", + "blobs = [bucket.get_blob(blob_name=blob_name) for blob_name in videos]" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "af0db3e8bc74" + }, + "source": [ + "## Metadata Filters\n", + "\n", + "To extract video metadata we will use [PyAV](https://github.com/PyAV-Org/PyAV), a Python wrapper over the popular video processing software FFmpeg. Identifying the video metadata fields below is important for adhering to downstream modeling requirements. For example, training over very dark or very bright videos may lead to poor modeling performance, and we should therefore exclude them during training. \n", + "\n", + "There are several metadata fields to consider but we will only cover the following:\n", + "* Frames per second (FPS)\n", + "* Duration (already covered in a previous notebook but we include it here as well)\n", + "* Resolution\n", + "* Brightness\n", + "* Aspect Ratio" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": { + "id": "31ef66832903" + }, + "outputs": [], + "source": [ + "def get_last_timestamp(container: av.container.Container) -> dict:\n", + " \"\"\"Gets the last frame timestamp and total frame count from video.\n", + "\n", + " Args:\n", + " container: PyAV container\n", + "\n", + " Returns:\n", + " dict: Last frame index and duration information\n", + "\n", + " \"\"\"\n", + " video_stream = container.streams.video[0]\n", + " if video_stream.duration:\n", + " duration = float(video_stream.duration * video_stream.time_base)\n", + " else:\n", + " duration = 0.0\n", + "\n", + " frame_count = 0\n", + " for _ in container.decode(video_stream):\n", + " frame_count += 1\n", + " return {\n", + " \"last_frame_index\": frame_count,\n", + " \"duration\": np.round(duration, 2).item(),\n", + " }\n", + "\n", + "\n", + "def get_average_brightness(\n", + " container: av.container.Container,\n", + " sample_interval: int = 10,\n", + ") -> float:\n", + " \"\"\"Calculate average brightness based on key frames from the video.\n", + "\n", + " See: https://en.wikipedia.org/wiki/Relative_luminance\n", + "\n", + " Args:\n", + " container: PyAV container\n", + " sample_interval: Interval at which to sample frames\n", + "\n", + " Returns:\n", + " float: Average brightness of the video\n", + "\n", + " \"\"\"\n", + " video_stream = container.streams.video[0]\n", + "\n", + " total_frames = 0\n", + " for _ in container.decode(video_stream):\n", + " total_frames += 1\n", + "\n", + " container.seek(0)\n", + " sample_interval = max(1, total_frames // sample_interval)\n", + " luminance_values = []\n", + "\n", + " frame_idx = 0\n", + " for frame in container.decode(video_stream):\n", + " if frame_idx % sample_interval == 0:\n", + " frame_array = frame.to_ndarray(format=\"rgb24\")\n", + " red = frame_array[..., 0]\n", + " green = frame_array[..., 1]\n", + " blue = frame_array[..., 2]\n", + " luminance = 0.2126 * red + 0.7152 * green + 0.0722 * blue\n", + " luminance_values.append(np.mean(luminance))\n", + " frame_idx += 1\n", + "\n", + " return np.round(np.mean(luminance_values), 2).item() if luminance_values else 0.0\n", + "\n", + "\n", + "def get_video_metadata(video_file_object: io.BytesIO) -> dict:\n", + " \"\"\"Extracts video metadata from a video file object using PyAV.\n", + "\n", + " Args:\n", + " video_file_object: A binary file-like object containing the video data\n", + "\n", + " Returns:\n", + " dict: Video metadata\n", + "\n", + " \"\"\"\n", + " aspect_ratio_map = {\n", + " \"16:9\": \"hdtv\",\n", + " \"4:3\": \"standard television\",\n", + " \"21:9\": \"ultrawide\",\n", + " \"3:2\": \"common photography\",\n", + " \"1:1\": \"square\",\n", + " \"5:4\": \"large format photography\",\n", + " \"16:10\": \"computer display\",\n", + " \"239:100\": \"anamorphic\",\n", + " \"47:20\": \"anamorphic\",\n", + " \"12:5\": \"anamorphic\",\n", + " \"37:20\": \"common widescreen theatrical\",\n", + " \"14:9\": \"cropped standard television\",\n", + " }\n", + "\n", + " video_file_object.seek(0)\n", + " container = av.open(video_file_object)\n", + " video_stream = container.streams.video[0]\n", + "\n", + " height = video_stream.height\n", + " width = video_stream.width\n", + " avg_fps = float(video_stream.average_rate)\n", + "\n", + " common_divisor = math.gcd(width, height)\n", + " aspect_ratio = f\"{width // common_divisor}:{height // common_divisor}\"\n", + " aspect_ratio_str = aspect_ratio_map.get(aspect_ratio, aspect_ratio)\n", + "\n", + " duration_info = get_last_timestamp(container)\n", + "\n", + " container.seek(0)\n", + " avg_brightness = get_average_brightness(container)\n", + " container.close()\n", + "\n", + " return {\n", + " \"height\": height,\n", + " \"width\": width,\n", + " \"avg_fps\": int(avg_fps),\n", + " \"aspect_ratio\": aspect_ratio,\n", + " \"aspect_ratio_type\": aspect_ratio_str,\n", + " \"avg_brightness\": avg_brightness,\n", + " **duration_info,\n", + " }" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "2977f2ee1ffd" + }, + "outputs": [], + "source": [ + "for b in blobs:\n", + " with b.open(\"rb\") as f:\n", + " video_file_object = io.BytesIO(f.read())\n", + " print(f\"{b.name}: {get_video_metadata(video_file_object)}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "d3534ba6501e" + }, + "source": [ + "## OCR and Watermark Detection\n", + "\n", + "Videos with text may or may not be desirable in our curated dataset, depending on the capabilities of the model we want to train. On the other hand, video watermarks may be more consistently undesirable. Either way, we will walk through examples of doing both using a single call to Gemini 2.5 Flash. We will also incorporate structured outputs to separate the text from the watermark content as JSON objects." + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": { + "id": "8c265d131385" + }, + "outputs": [], + "source": [ + "def get_text_and_watermarks(video_uri: str) -> dict:\n", + " \"\"\"Extracts text, watermarks, and usage metadata video.\n", + "\n", + " Args:\n", + " vr: decord.VideoReader\n", + "\n", + " Returns:\n", + " dict: Visitable text, watermark descriptions, and usage metadata\n", + "\n", + " \"\"\"\n", + " system_instruction = \"\"\"Your task is to carefully watch the provided video and do the following:\n", + "\n", + "* Extract all visible text that appears across all frames in the video (e.g. text overlays from a weather forecast, credits at the start or end of a video). For example, if there is text on a t-shirt, traffic signs, or something that is naturally part of the video content, do NOT include it in the response.\n", + "* Identify and describe any visual watermarks in the video. These are typically logos or other images that are overlaid on the video during the production process.\n", + "* If no text or watermarks are visible, return 'None' as the response.\n", + "* Be concise, especially if there is no text or watermarks visible.\n", + "* Return your response in JSON format with the following fields and nothing else:\n", + "{\n", + " \"text\": \"The text found in the video (if any)\",\n", + " \"watermarks\": \"Watermark descriptions found in the video (if any)\"\n", + "}\n", + "\"\"\"\n", + "\n", + " response = gemini_client.models.generate_content(\n", + " model=\"gemini-2.5-flash\",\n", + " contents=[\n", + " types.Part.from_text(text=\"Watch this video:\"),\n", + " types.Part.from_uri(file_uri=video_uri, mime_type=\"video/mp4\"),\n", + " ],\n", + " config=types.GenerateContentConfig(\n", + " system_instruction=system_instruction,\n", + " temperature=0.0,\n", + " response_mime_type=\"application/json\",\n", + " response_schema={\n", + " \"type\": \"ARRAY\",\n", + " \"items\": {\n", + " \"type\": \"OBJECT\",\n", + " \"properties\": {\n", + " \"text\": {\"type\": \"STRING\"},\n", + " \"watermarks\": {\"type\": \"STRING\"},\n", + " },\n", + " },\n", + " },\n", + " ),\n", + " )\n", + " response_json = json.loads(response.text)[0]\n", + " return {**response_json, **response.usage_metadata.to_json_dict()}" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "4b4429ed3df1" + }, + "outputs": [], + "source": [ + "for v in videos:\n", + " print(f\"{v}:\")\n", + " text_and_watermarks = get_text_and_watermarks(f\"{BUCKET_URI}/{v}\")\n", + " text = text_and_watermarks[\"text\"]\n", + " watermarks = text_and_watermarks[\"watermarks\"]\n", + " print(f\"\\ttext: {text}\")\n", + " print(f\"\\twatermarks: {watermarks}\\n\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "d250de6ca196" + }, + "source": [ + "## Aesthetic Scoring\n", + "\n", + "Aesthetic scoring involves using a model to evaluate the quality of sampled frames from our videos. Below we show how to use the [CLIP+MLP Aesthetic Score](https://github.com/christophschuhmann/improved-aesthetic-predictor) model that was train over an annotated dataset of images and aesthetic scores, i.e. a numeric value on a scale of 1 to 10, where the higher the score the better. For more details on the underlying training data and classifier see [LAION-Aesthetics blog](https://laion.ai/blog/laion-aesthetics/). We will sample frames from each video, run inference with the trained CLIP+MLP model above, and average the scores. We recommend figuring out the optimal score threshold to filter by manually inspecting a sample of low scoring videos.\n", + "\n", + "Alternatively, we can prompt a native multi-modal model like Gemini 2.x to make a similar classification, and even fine-tune Gemini 2.x using the labelled data from the LAION dataset. Either way, the threshold to use (or classification system) will involve manually inspecting a sample of low scoring predictions.\n", + "\n", + "Note: Be sure to download the [`sac+logos+ava1-l14-linearMSE.pth`](https://github.com/christophschuhmann/improved-aesthetic-predictor/raw/refs/heads/main/sac+logos+ava1-l14-linearMSE.pth) model weights before running the code below." + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": { + "id": "fbe0d62a2bd0" + }, + "outputs": [], + "source": [ + "class MLP(nn.Module):\n", + "\n", + " def __init__(self, input_size: int, xcol: str = \"emb\", ycol: str = \"avg_rating\"):\n", + " super().__init__()\n", + " self.input_size = input_size\n", + " self.xcol = xcol\n", + " self.ycol = ycol\n", + " self.layers = nn.Sequential(\n", + " nn.Linear(self.input_size, 1024),\n", + " nn.Dropout(0.2),\n", + " nn.Linear(1024, 128),\n", + " nn.Dropout(0.2),\n", + " nn.Linear(128, 64),\n", + " nn.Dropout(0.1),\n", + " nn.Linear(64, 16),\n", + " nn.Linear(16, 1),\n", + " )\n", + "\n", + " def forward(self, x: torch.Tensor) -> torch.Tensor:\n", + " return self.layers(x)\n", + "\n", + "\n", + "def normalized(a, axis: int = -1, order: int = 2) -> np.ndarray:\n", + " l2 = np.atleast_1d(np.linalg.norm(a, order, axis))\n", + " l2[l2 == 0] = 1\n", + " return a / np.expand_dims(l2, axis)\n", + "\n", + "\n", + "def sample_video_frames(\n", + " video_file_object: io.BytesIO,\n", + " num_frames: int,\n", + ") -> list[Image.Image]:\n", + " \"\"\"Sample frames from video at regular intervals.\n", + "\n", + " Args:\n", + " video_file_object: A binary file-like object containing the video data\n", + " num_frames: Number of frames to sample\n", + "\n", + " Returns:\n", + " List of PIL Images\n", + "\n", + " \"\"\"\n", + " video_file_object.seek(0)\n", + " container = av.open(video_file_object)\n", + " video_stream = container.streams.video[0]\n", + "\n", + " total_frames = 0\n", + " for _ in container.decode(video_stream):\n", + " total_frames += 1\n", + "\n", + " container.seek(0)\n", + " if num_frames == 1:\n", + " # Get the middle frame\n", + " target_frame = total_frames // 2\n", + " frame_idx = 0\n", + " for frame in container.decode(video_stream):\n", + " if frame_idx == target_frame:\n", + " frame_array = frame.to_ndarray(format=\"rgb24\")\n", + " container.close()\n", + " return [Image.fromarray(frame_array)]\n", + " frame_idx += 1\n", + " else:\n", + " # Sample frames at regular intervals, use 80% of frames\n", + " last_frame_index = int(total_frames * 0.8)\n", + " frame_indices = np.linspace(0, last_frame_index, num_frames).astype(int)\n", + "\n", + " sampled_frames = []\n", + " frame_idx = 0\n", + " for frame in container.decode(video_stream):\n", + " if frame_idx in frame_indices:\n", + " frame_array = frame.to_ndarray(format=\"rgb24\")\n", + " sampled_frames.append(Image.fromarray(frame_array))\n", + " frame_idx += 1\n", + " if len(sampled_frames) >= num_frames:\n", + " break\n", + "\n", + " container.close()\n", + " return sampled_frames\n", + "\n", + " container.close()\n", + " return []\n", + "\n", + "\n", + "def get_frames(blob: storage.blob.Blob) -> list[Image.Image]:\n", + " \"\"\"Extract key frames from video blob for aesthetic scoring.\n", + "\n", + " Args:\n", + " blob: Cloud Storage blob object\n", + "\n", + " Returns:\n", + " List of PIL Images\n", + "\n", + " \"\"\"\n", + " with blob.open(\"rb\") as f:\n", + " video_file_object = io.BytesIO(f.read())\n", + "\n", + " video_file_object.seek(0)\n", + " container = av.open(video_file_object)\n", + " video_stream = container.streams.video[0]\n", + "\n", + " # Sample every nth frame to get key frames (up to 10 frames)\n", + " total_frames = 0\n", + " for _ in container.decode(video_stream):\n", + " total_frames += 1\n", + "\n", + " container.seek(0)\n", + "\n", + " # Sample frames at regular intervals\n", + " sample_interval = max(1, total_frames // 10)\n", + " sampled_frames = []\n", + "\n", + " frame_idx = 0\n", + " for frame in container.decode(video_stream):\n", + " if frame_idx % sample_interval == 0:\n", + " frame_array = frame.to_ndarray(format=\"rgb24\")\n", + " sampled_frames.append(Image.fromarray(frame_array))\n", + " frame_idx += 1\n", + "\n", + " container.close()\n", + " return sampled_frames\n", + "\n", + "\n", + "def load_model(model_path: str) -> tuple[CLIPModel, CLIPProcessor, MLP, str]:\n", + " mlp_model = MLP(768)\n", + " device = \"cuda\" if torch.cuda.is_available() else \"cpu\"\n", + "\n", + " state_dict = torch.load(model_path, map_location=torch.device(device))\n", + " mlp_model.load_state_dict(state_dict)\n", + " mlp_model.to(device)\n", + " mlp_model.eval()\n", + "\n", + " clip_model_name = \"openai/clip-vit-large-patch14\"\n", + " processor = CLIPProcessor.from_pretrained(clip_model_name)\n", + " clip_model = CLIPModel.from_pretrained(clip_model_name).to(device)\n", + " return clip_model, processor, mlp_model, device\n", + "\n", + "\n", + "def get_aesthetic_score(\n", + " clip_model: CLIPModel,\n", + " processor: CLIPProcessor,\n", + " mlp_model: MLP,\n", + " device: str,\n", + " pil_images: Image.Image,\n", + ") -> float:\n", + " inputs = processor(\n", + " text=None,\n", + " images=pil_images,\n", + " return_tensors=\"pt\",\n", + " padding=True,\n", + " ).to(device)\n", + "\n", + " with torch.no_grad():\n", + " image_features = clip_model.get_image_features(\n", + " pixel_values=inputs[\"pixel_values\"],\n", + " )\n", + "\n", + " im_emb_arr = normalized(image_features.cpu().detach().numpy())\n", + "\n", + " input_tensor = torch.from_numpy(im_emb_arr).to(device).float()\n", + " return mlp_model(input_tensor).mean()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "32871eb5f38f" + }, + "outputs": [], + "source": [ + "clip_model, processor, mlp_model, device = load_model(\n", + " \"./sac+logos+ava1-l14-linearMSE.pth\"\n", + ")\n", + "\n", + "\n", + "for b in blobs:\n", + " pil_images = get_frames(b)\n", + " aesthetic_score = get_aesthetic_score(\n", + " clip_model, processor, mlp_model, device, pil_images\n", + " )\n", + " print(f\"{b.name}: {aesthetic_score}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "8176f9f4a1e6" + }, + "source": [ + "## Motion Scores\n", + "\n", + "In order to detect different types of motion we can measure the difference between consective frames in a video. Since we only care about measurement movement in the context of slow or very fast motion, we will focus on more coarse and computationally efficient approaches that fall under *Sparse Optical Flow Estimation*. These techniques look at displacement in important patches of an image, in contrast to *Dense Optical Flow Estimation*, which examines all pixel values ([RAFT (Recurrent All-pairs Field Transforms)](https://arxiv.org/pdf/2003.12039) is one example of a Dense Optical Flow model). The key method in OpenCV is `cv.calcOpticalFlowPyrLK`, which uses the Lucas-Kanade method to calculate optical flow. See the [OpenCV documentation](https://docs.opencv.org/4.x/db/d7f/tutorial_js_lucas_kanade.html) for more details." + ] + }, + { + "cell_type": "code", + "execution_count": 83, + "metadata": { + "id": "ef7263b4525f" + }, + "outputs": [], + "source": [ + "def estimate_video_motion(video_file_object: io.BytesIO) -> float:\n", + " \"\"\"Estimates motion in a video.\n", + "\n", + " Args:\n", + " video_file_object: A binary file-like object containing the video data\n", + "\n", + " Returns:\n", + " A single float representing the estimated average motion per frame.\n", + "\n", + " Raises:\n", + " ValueError: If the video data cannot be read or is empty.\n", + "\n", + " \"\"\"\n", + " feature_params = {\n", + " \"maxCorners\": 100,\n", + " \"qualityLevel\": 0.3,\n", + " \"minDistance\": 7,\n", + " \"blockSize\": 7,\n", + " }\n", + " lk_params = {\n", + " \"winSize\": (15, 15),\n", + " \"maxLevel\": 2,\n", + " \"criteria\": (cv.TERM_CRITERIA_EPS | cv.TERM_CRITERIA_COUNT, 10, 0.03),\n", + " }\n", + "\n", + " motion_per_frame = []\n", + "\n", + " try:\n", + " video_file_object.seek(0)\n", + " container = av.open(video_file_object)\n", + " stream = container.streams.video[0]\n", + " frames = container.decode(stream)\n", + " # Get the first frame\n", + " old_frame = next(frames)\n", + " old_frame_bgr = old_frame.to_ndarray(format=\"bgr24\")\n", + " except Exception as e:\n", + " raise ValueError(f\"Failed to read video data from memory. Error: {e}\")\n", + "\n", + " old_gray = cv.cvtColor(old_frame_bgr, cv.COLOR_BGR2GRAY)\n", + " p0 = cv.goodFeaturesToTrack(old_gray, mask=None, **feature_params)\n", + "\n", + " if p0 is None:\n", + " return 0.0\n", + "\n", + " for frame in frames:\n", + " frame_bgr = frame.to_ndarray(format=\"bgr24\")\n", + " frame_gray = cv.cvtColor(frame_bgr, cv.COLOR_BGR2GRAY)\n", + " p1, st, err = cv.calcOpticalFlowPyrLK(\n", + " old_gray, frame_gray, p0, None, **lk_params\n", + " )\n", + "\n", + " if p1 is not None and st is not None:\n", + " good_new = p1[st == 1]\n", + " good_old = p0[st == 1]\n", + "\n", + " if len(good_new) > 0:\n", + " distances = np.linalg.norm(good_new - good_old, axis=1)\n", + " motion_per_frame.append(np.mean(distances))\n", + "\n", + " p0 = good_new.reshape(-1, 1, 2)\n", + " else:\n", + " p0 = cv.goodFeaturesToTrack(frame_gray, mask=None, **feature_params)\n", + " if p0 is None:\n", + " break\n", + "\n", + " old_gray = frame_gray.copy()\n", + "\n", + " return np.mean(motion_per_frame) if motion_per_frame else 0.0" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "31597ed5a055" + }, + "outputs": [], + "source": [ + "for b in blobs:\n", + " with b.open(\"rb\") as f:\n", + " video_file_object = io.BytesIO(f.read())\n", + " motion_score = estimate_video_motion(video_file_object)\n", + " print(f\"{b.name}: {motion_score}\")" + ] + } + ], + "metadata": { + "colab": { + "name": "quality-filtering.ipynb", + "toc_visible": true + }, + "kernelspec": { + "display_name": "Python 3", + "name": "python3" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} diff --git a/gemini/use-cases/multimodal-data-curation/semantic-deduplication.ipynb b/gemini/use-cases/multimodal-data-curation/semantic-deduplication.ipynb new file mode 100644 index 0000000..75f4a37 --- /dev/null +++ b/gemini/use-cases/multimodal-data-curation/semantic-deduplication.ipynb @@ -0,0 +1,1046 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 50, + "metadata": { + "id": "ur8xi4C7S06n" + }, + "outputs": [], + "source": [ + "# Copyright 2025 Google LLC\n", + "#\n", + "# Licensed under the Apache License, Version 2.0 (the \"License\");\n", + "# you may not use this file except in compliance with the License.\n", + "# You may obtain a copy of the License at\n", + "#\n", + "# https://www.apache.org/licenses/LICENSE-2.0\n", + "#\n", + "# Unless required by applicable law or agreed to in writing, software\n", + "# distributed under the License is distributed on an \"AS IS\" BASIS,\n", + "# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n", + "# See the License for the specific language governing permissions and\n", + "# limitations under the License." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "JAPoU8Sm5E6e" + }, + "source": [ + "# Data Curation Pipeline: Splitting and Transcoding \n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + "
    \n", + " \n", + " \"Google
    Open in Colab\n", + "
    \n", + "
    \n", + " \n", + " \"Google
    Open in Colab Enterprise\n", + "
    \n", + "
    \n", + " \n", + " \"Vertex
    Open in Vertex AI Workbench\n", + "
    \n", + "
    \n", + " \n", + " \"GitHub
    View on GitHub\n", + "
    \n", + "
    \n", + "\n", + "
    \n", + "\n", + "Share to:\n", + "\n", + "\n", + " \"LinkedIn\n", + "\n", + "\n", + "\n", + " \"Bluesky\n", + "\n", + "\n", + "\n", + " \"X\n", + "\n", + "\n", + "\n", + " \"Reddit\n", + "\n", + "\n", + "\n", + " \"Facebook\n", + "" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "84f0f73a0f76" + }, + "source": [ + "| Author(s) |\n", + "| --- |\n", + "| [John Semerdjian](https://github.com/semerj) |" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "tvgnzT1CKxrO" + }, + "source": [ + "## Overview\n", + "\n", + "Data deduplication is a critical step in any data curation pipeline. Even after splitting, filtering, and captioning video clips, our dataset will still contain redundant information, especially since much of the clips are derived from the same source material. Imagine that you're building a specialized foundation model for sports videos — do you really need thousands of clips of athletes shooting free throws? While more high quality data usually leads to better downstream modeling performance, it's we're likely not spending our compute budget efficiently. Redundant examples in particular do little to enhance a model's ability to generalize to new tasks, and instead these examples waste precious FLOPs. Depending on the dataset, some researchers have show that eliminating [50% of training datasets using straightforward deduplication techniques can lead to a model of similar performance but at half the cost and twice as fast](https://arxiv.org/pdf/2303.09540).\n", + "\n", + "In this post we'll show one simple approach for detecting semantic duplicates using [multimodal embeddings](https://cloud.google.com/vertex-ai/generative-ai/docs/embeddings/get-multimodal-embeddings#video-modes) and [Approximate Nearest Neighbors using BigQuery Vector Search](https://en.wikipedia.org/wiki/Nearest_neighbor_search#Approximation_methods). There are m" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "61RBz8LLbxCR" + }, + "source": [ + "## Get started" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "No17Cw5hgx12" + }, + "source": [ + "### Install Google Gen AI SDK and other required packages\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "tFy3H3aPgx12" + }, + "outputs": [], + "source": [ + "%pip install --upgrade --quiet datasets google-cloud-bigquery sentencepiece pandas-gbq" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "dmWOrTJ3gx13" + }, + "source": [ + "### Authenticate your notebook environment (Colab only)\n", + "\n", + "If you're running this notebook on Google Colab, run the cell below to authenticate your environment." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": { + "id": "NyKGtVQjgx13" + }, + "outputs": [], + "source": [ + "import sys\n", + "\n", + "if \"google.colab\" in sys.modules:\n", + " from google.colab import auth\n", + "\n", + " auth.authenticate_user()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "DF4l8DTdWgPY" + }, + "source": [ + "### Set Google Cloud project information\n", + "\n", + "To get started using Vertex AI, you must have an existing Google Cloud project and [enable the Vertex AI API](https://console.cloud.google.com/flows/enableapi?apiid=aiplatform.googleapis.com).\n", + "\n", + "Learn more about [setting up a project and a development environment](https://cloud.google.com/vertex-ai/docs/start/cloud-environment)." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": { + "id": "Nqwi-5ufWp_B" + }, + "outputs": [], + "source": [ + "# Use the environment variable if the user doesn't provide Project ID.\n", + "import os\n", + "\n", + "PROJECT_ID = \"genai-scratchpad\" # @param {type: \"string\", placeholder: \"[your-project-id]\", isTemplate: true}\n", + "# if not PROJECT_ID or PROJECT_ID == \"[your-project-id]\":\n", + "# PROJECT_ID = str(os.environ.get(\"GOOGLE_CLOUD_PROJECT\"))\n", + "\n", + "LOCATION = os.environ.get(\"GOOGLE_CLOUD_REGION\", \"us-central1\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "5303c05f7aa6" + }, + "source": [ + "### Import libraries" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": { + "id": "6fc324893334" + }, + "outputs": [], + "source": [ + "from concurrent.futures import ThreadPoolExecutor, as_completed\n", + "import io\n", + "import threading\n", + "\n", + "from PIL import Image\n", + "import av\n", + "from google.cloud import aiplatform, bigquery, storage\n", + "import pandas as pd\n", + "import pandas_gbq\n", + "import torch\n", + "from torch import nn\n", + "from tqdm import tqdm\n", + "from transformers import AutoModel, AutoProcessor\n", + "import vertexai\n", + "from vertexai.vision_models import MultiModalEmbeddingModel, Video\n", + "\n", + "vertexai.init(project=PROJECT_ID, location=LOCATION)\n", + "aiplatform.init(project=PROJECT_ID, location=LOCATION)\n", + "\n", + "bq_client = bigquery.Client()\n", + "storage_client = storage.Client()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "f0b05899f92f" + }, + "source": [ + "## Video Embedding Generation\n", + "\n", + "### Load Video Data\n", + "\n", + "This notebook will use a subset of videos from the [VidGen-1M dataset](https://arxiv.org/abs/2408.02629). This authors of this dataset have already done some preliminary deduplication of the dataset, but let's see if we can identify some new duplicates with a small sample of records. \n", + "\n", + "In order to use the Multimodal Embeddings API the videos must be stored on Google Cloud Storage. You can download the compressed files and transfer them to a bucket here: https://huggingface.co/datasets/Fudan-FUXI/VIDGEN-1M/tree/main\n", + "\n", + "Alternatively, you can also load a subset of the dataset into memory using the `datasets` library:\n", + "\n", + "```\n", + "data = load_dataset(\"Fudan-FUXI/VIDGEN-1M\", data_files=\"VidGen_video_0.zip\")\n", + "```\n", + "\n", + "We'll read the clips directly from Cloud Storage below:" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": { + "id": "89779759ac65" + }, + "outputs": [ + { + "data": { + "text/plain": [ + "5000" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "def load_video_paths(\n", + " bucket: str,\n", + " num_videos: int,\n", + ") -> list[str]:\n", + " video_paths = []\n", + " for i, blob in enumerate(storage_client.list_blobs(bucket)):\n", + " if i >= num_videos:\n", + " break\n", + " video_paths.append(f\"gs://{bucket}/\" + blob.name)\n", + " return video_paths\n", + "\n", + "\n", + "video_paths = load_video_paths(\"vidgen-1m\", 5000)\n", + "len(video_paths)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "c6e0d65af056" + }, + "source": [ + "### Vertex AI Multimodal Embeddings\n", + "\n", + "We will generate embeddings using the Vertex AI multimodal embedding API. We will parallelize the calls to the embedding API (you may need to request a quota increase to get better throughput). In order to use this API we must store our videos on Cloud Storage. Video embeddings from this model have 1408 dimensions that encode a rich amount of semantic information about the content. It supports common video formats, e.g. mp4, webm, mov, and more. Our clips are already quite short so we don't need to worry about chunking the embeddings. Only 2 minutes of contents can be analyzed at a time, but there is no max video length. Audio data is not considered in the embeddings." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": { + "id": "976959ccd898" + }, + "outputs": [], + "source": [ + "def get_embedding_for_single_video(\n", + " model: vertexai.vision_models.MultiModalEmbeddingModel, video_file: str\n", + ") -> dict[str, str | vertexai.vision_models.MultiModalEmbeddingResponse]:\n", + " \"\"\"Get the initial set of embeddings of a video file.\n", + "\n", + " Args:\n", + " model: MultiModalEmbeddingModel instance.\n", + " video_file: URI for video on Cloud Storage.\n", + "\n", + " Returns:\n", + " Dictionary containing the URI, start and end offsets, and embeddings.\n", + "\n", + " \"\"\"\n", + " try:\n", + " video = Video.load_from_file(video_file)\n", + " embeddings = model.get_embeddings(video=video)\n", + " return {\n", + " \"uri\": video_file,\n", + " \"embedding\": embeddings.video_embeddings[0].embedding,\n", + " \"start_offset_sec\": embeddings.video_embeddings[0].start_offset_sec,\n", + " \"end_offset_sec\": embeddings.video_embeddings[0].end_offset_sec,\n", + " }\n", + " except Exception as e:\n", + " print(f\"Error processing {video_file}: {e}\")\n", + " return {\n", + " \"uri\": video_file,\n", + " \"embedding\": None,\n", + " \"error\": str(e),\n", + " }" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": { + "id": "52c10f375a5c" + }, + "outputs": [], + "source": [ + "def get_embeddings_with_semaphore(\n", + " video_files: list[str],\n", + " model_name: str = \"multimodalembedding@001\",\n", + " max_workers: int = 2,\n", + ") -> list[dict[str, str | vertexai.vision_models.MultiModalEmbeddingResponse]]:\n", + " \"\"\"Get embeddings for a list of video files.\n", + "\n", + " Args:\n", + " video_files: List of URIs for video files on Cloud Storage.\n", + " model_name: Name of the Vertex AI Multimodal EmbeddingModel to use.\n", + " max_workers: The maximum number of concurrent requests.\n", + "\n", + " Returns:\n", + " A list of dictionaries containing the URI and the embedding response or an error.\n", + "\n", + " \"\"\"\n", + " model = MultiModalEmbeddingModel.from_pretrained(model_name)\n", + " all_embeddings = []\n", + " semaphore = threading.Semaphore(max_workers)\n", + "\n", + " def rate_limited_embedding_task(video_file: str):\n", + " \"\"\"Acquires the semaphore before running the embedding task.\"\"\"\n", + " with semaphore:\n", + " return get_embedding_for_single_video(model, video_file)\n", + "\n", + " with ThreadPoolExecutor(max_workers=max_workers) as executor:\n", + " futures = [\n", + " executor.submit(rate_limited_embedding_task, video_file)\n", + " for video_file in video_files\n", + " ]\n", + "\n", + " for future in tqdm(\n", + " as_completed(futures), total=len(video_files), desc=\"Processing videos\"\n", + " ):\n", + " all_embeddings.append(future.result())\n", + "\n", + " return all_embeddings" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": { + "id": "1baa554e6f0f" + }, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Processing videos: 100%|██████████| 5000/5000 [13:25<00:00, 6.21it/s]\n" + ] + } + ], + "source": [ + "embeddings = get_embeddings_with_semaphore(\n", + " video_paths,\n", + " model_name=\"multimodalembedding@001\",\n", + " max_workers=30,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "16a97a42a6ae" + }, + "source": [ + "### Side Quest: Open Source Embeddings\n", + "\n", + "We can also try out open source embedding models designed for images. We can sample N frames from each video, pool their embeddings, and perform the same deduplication steps. We will use average pooling of frame embeddings as a robust and computationally efficient baseline, but other approaches can also be used. A drawback of average pooling ignore the temporal nature of the video frames. Depending on the approach, the number of frames may not be completely accurate. Since (1) each video has a different duration and (2) we're pooling the embeddings anyway, sampling exact same number per video is less important." + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": { + "id": "dc02f310e7c3" + }, + "outputs": [], + "source": [ + "def sample_frames(video_path: str, num_frames: int = 10) -> list[Image.Image]:\n", + " \"\"\"Samples n frames from a video, supporting both local paths and GCS URIs.\n", + "\n", + " Args:\n", + " video_path: Cloud storage URI.\n", + " num_frames: Number of frames to sample.\n", + "\n", + " Returns:\n", + " A list of PIL Image objects.\n", + "\n", + " \"\"\"\n", + " pil_images: list[Image.Image] = []\n", + " video_source_for_av = None\n", + " total_frames_reported_by_stream = 0\n", + "\n", + " try:\n", + " bucket_name, blob_name = video_path.replace(\"gs://\", \"\").split(\"/\", 1)\n", + " bucket = storage_client.bucket(bucket_name)\n", + " blob = bucket.blob(blob_name)\n", + "\n", + " video_bytes = blob.download_as_bytes()\n", + " video_source_for_av = io.BytesIO(video_bytes)\n", + " container = av.open(video_source_for_av)\n", + "\n", + " if not container.streams.video:\n", + " print(f\"No video streams found in {video_path}\")\n", + " return []\n", + "\n", + " video_stream = container.streams.video[0]\n", + " # total_frames_reported_by_stream can sometimes be 0 or inaccurate.\n", + " # PyAV's video_stream.frames gives an often reliable count.\n", + " total_frames_reported_by_stream = video_stream.frames\n", + "\n", + " if total_frames_reported_by_stream == 0 or num_frames == 0:\n", + " return []\n", + "\n", + " target_indices_to_sample = []\n", + " if num_frames == 1:\n", + " # Sample the middle frame index\n", + " target_indices_to_sample.append(\n", + " round((total_frames_reported_by_stream - 1) / 2)\n", + " )\n", + " elif num_frames >= total_frames_reported_by_stream:\n", + " # If more or equal frames are requested than available, sample all\n", + " target_indices_to_sample = list(range(total_frames_reported_by_stream))\n", + " else:\n", + " # Sample num_frames evenly distributed, aiming to include first and last.\n", + " # Using a set to ensure uniqueness if rounding collapses indices, then sort.\n", + " _indices = set()\n", + " for i in range(num_frames):\n", + " idx = round(\n", + " i * (total_frames_reported_by_stream - 1) / (num_frames - 1)\n", + " )\n", + " _indices.add(int(idx))\n", + " target_indices_to_sample = sorted(list(_indices))\n", + "\n", + " # Ensure the list is not empty if logic somehow failed or num_frames was valid\n", + " if (\n", + " not target_indices_to_sample\n", + " and num_frames > 0\n", + " and total_frames_reported_by_stream > 0\n", + " ):\n", + " # This case should ideally not be hit if calculations are correct\n", + " # Default to sampling just the first frame if something went wrong with index calculation\n", + " target_indices_to_sample.append(0)\n", + "\n", + " current_frame_idx = 0\n", + " # Iterate through frames and pick the ones at target_indices_to_sample\n", + " for frame in container.decode(video_stream):\n", + " if not target_indices_to_sample:\n", + " break\n", + "\n", + " if current_frame_idx == target_indices_to_sample[0]:\n", + " pil_images.append(frame.to_image())\n", + " target_indices_to_sample.pop(0)\n", + "\n", + " current_frame_idx += 1\n", + "\n", + " except Exception as e:\n", + " print(f\"General error processing video '{video_path}': {e}\")\n", + " raise\n", + " finally:\n", + " if container:\n", + " try:\n", + " container.close()\n", + " except Exception as ce:\n", + " print(f\"Error closing video container for '{video_path}': {ce}\")\n", + " if isinstance(video_source_for_av, io.BytesIO):\n", + " video_source_for_av.close()\n", + "\n", + " if not pil_images and num_frames > 0 and total_frames_reported_by_stream > 0:\n", + " print(\n", + " f\"Warning: No frames were sampled from '{video_path}'. \"\n", + " \"Check video integrity, stream content, or frame selection logic.\"\n", + " )\n", + "\n", + " return pil_images" + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "metadata": { + "id": "622983c20244" + }, + "outputs": [], + "source": [ + "sampled_frames = sample_frames(video_paths[0])" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "91b7075467ca" + }, + "source": [ + "Here is the first sample frame for the first video:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "89dd5fc60163" + }, + "outputs": [], + "source": [ + "sampled_frames[0]" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "48ec86e18016" + }, + "source": [ + "and the last sample frame:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "0eba85742de2" + }, + "outputs": [], + "source": [ + "sampled_frames[-1]" + ] + }, + { + "cell_type": "code", + "execution_count": 43, + "metadata": { + "id": "0da396bbb4d0" + }, + "outputs": [], + "source": [ + "class SiglipWithProjection(nn.Module):\n", + "\n", + " def __init__(self, model_name, target_dim):\n", + " super().__init__()\n", + " self.device = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n", + " self.model = AutoModel.from_pretrained(model_name).to(self.device)\n", + " for param in self.model.parameters():\n", + " param.requires_grad = False\n", + " original_dim = self.model.config.vision_config.hidden_size\n", + " self.projection = nn.Linear(original_dim, target_dim)\n", + "\n", + " def forward(self, **inputs):\n", + " \"\"\"Apply a projection layer to the image features.\"\"\"\n", + " image_features = self.model.get_image_features(**inputs)\n", + " low_dim_features = self.projection(image_features)\n", + " return low_dim_features\n", + "\n", + "\n", + "def get_video_embeddings(\n", + " video_paths: list[str],\n", + " model_name: str,\n", + ") -> list[dict[str, str | torch.Tensor]]:\n", + " \"\"\"Get the pooled image embeddings of videos from a list of video paths.\n", + "\n", + " Args:\n", + " video_paths: Cloud storage video URIs.\n", + " model_name: The name of the model to use.\n", + "\n", + " Returns:\n", + " A tensor of the average of the image embeddings of the sampled frames of a video.\n", + "\n", + " \"\"\"\n", + " device = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n", + "\n", + " model = SiglipWithProjection(model_name, target_dim=256)\n", + " processor = AutoProcessor.from_pretrained(model_name, use_fast=False)\n", + "\n", + " embeddings = []\n", + " for video_path in video_paths:\n", + " sampled_frames = sample_frames(video_path)\n", + " inputs = processor(images=sampled_frames, return_tensors=\"pt\").to(device)\n", + " outputs = model(**inputs)\n", + " embeddings.append(\n", + " {\n", + " \"uri\": video_path,\n", + " \"embedding\": outputs.mean(dim=0).tolist(),\n", + " },\n", + " )\n", + " return embeddings" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "bb283f61361d" + }, + "outputs": [], + "source": [ + "# \"laion/CLIP-ViT-B-16-laion2B-s34B-b88K\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "03a37437e81d" + }, + "outputs": [], + "source": [ + "embeddings = get_video_embeddings(video_paths, \"google/siglip2-base-patch16-512\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "1db7a5caacc7" + }, + "source": [ + "## Storing Embeddings in BigQuery\n", + "\n", + "We'll store the embeddings in a BigQuery table. We'll use the `bq_client` to create a table and insert the embeddings." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": { + "id": "7881ac7f12d7" + }, + "outputs": [ + { + "data": { + "text/plain": [ + "(5000, 4)" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "dataset_id = \"video_data_curation\"\n", + "table_id = \"clip_embeddings\"\n", + "\n", + "df_embedding = pd.DataFrame(embeddings)\n", + "df_embedding.shape" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": { + "id": "ac72d205625f" + }, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "100%|██████████| 1/1 [00:00<00:00, 5645.09it/s]\n" + ] + } + ], + "source": [ + "pandas_gbq.to_gbq(\n", + " df_embedding,\n", + " f\"{PROJECT_ID}.{dataset_id}.{table_id}\",\n", + " if_exists=\"replace\",\n", + " project_id=PROJECT_ID,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "a881ac879601" + }, + "source": [ + "## Deduplication in BigQuery\n", + "\n", + "Once the embeddings are in BigQuery we will use its Vector Search functionality to perform the deduplication step. While there are more sophisticated strategies that run K-means clustering on the embeddings, perform pairwise comparisons within each cluster, and prune based on similarity, this approach is can be done entirely within BigQuery and easy to understand and execute.\n", + "\n", + "First, let's create the test table that contains duplicate rows." + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": { + "id": "d0a9ac076c88" + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "genai-scratchpad.video_data_curation.clip_embeddings\n" + ] + } + ], + "source": [ + "full_table_id = f\"{PROJECT_ID}.{dataset_id}.{table_id}\"\n", + "print(full_table_id)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "b85526e5f962" + }, + "source": [ + "We will not create vector index to accelerate the vector search query. This steps is particularly important if the table is large, but we can still do brute force search since our table is small." + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": { + "id": "9cf81881f64f" + }, + "outputs": [], + "source": [ + "embedding_column = \"embedding\"\n", + "\n", + "index_job = bq_client.query(\n", + " f\"\"\"\n", + "CREATE VECTOR INDEX my_index ON \"{full_table_id}\"({embedding_column})\n", + "OPTIONS(index_type='TREE_AH', distance_type='COSINE');\n", + "\"\"\"\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "6f34ddaa5ac7" + }, + "source": [ + "The vector index creation takes a few minutes. You can monitor the vector index status using the following query. Once the coverage is 100, we can proceed to the next step." + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": { + "id": "6a17ab551a0a" + }, + "outputs": [], + "source": [ + "index_status_job = bq_client.query(\n", + " f\"\"\"\n", + "SELECT table_name, index_status, coverage_percentage\n", + "FROM '{PROJECT_ID}.{dataset_id}'.INFORMATION_SCHEMA.VECTOR_INDEXES\n", + "WHERE table_name = \"{table_id}\";\n", + "\"\"\"\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": { + "id": "f6296f192723" + }, + "outputs": [ + { + "data": { + "text/plain": [ + "pyarrow.Table\n", + "table_name: string\n", + "index_status: string\n", + "coverage_percentage: int64\n", + "----\n", + "table_name: [[\"clip_embeddings\"]]\n", + "index_status: [[\"ACTIVE\"]]\n", + "coverage_percentage: [[0]]" + ] + }, + "execution_count": 16, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "index_status_job.result().to_arrow()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "46b25bea8e9d" + }, + "source": [ + "We can now perform the deduplication step. For each embedding, we perform an approximate nearest neighbor (ANN) search and retrieve matching embeddings with a cosine distance. For records that are within the provided threshold - which should be tuned for each dataset and use case - these are the semantic duplicates and we will remove them. While this approach is simple it comes with some tradeoffs, specifically that we ignore any transitive links between the embeddings (e.g. if record A is near both record B and record C, then B and C may be nearby as well), and that we don't intelligently decide which embedding to include or exclude between matches. Nevertheless, this is a straightforward deduplication approach that leverages the foundational power of the [ScaNN algorithm](https://research.google/blog/announcing-scann-efficient-vector-similarity-search/) for semantic search." + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": { + "id": "62e27c32b895" + }, + "outputs": [], + "source": [ + "index_column = \"uri\"\n", + "top_k = 10\n", + "distance_threshold = 0.05\n", + "\n", + "dedupe_job = bq_client.query(\n", + " f\"\"\"\n", + "CREATE OR REPLACE TABLE '{full_table_id}_dedupe' AS\n", + "WITH dupes AS (\n", + " SELECT \n", + " DISTINCT query.{index_column}\n", + " FROM VECTOR_SEARCH(\n", + " Table '{full_table_id}', \"embedding\",\n", + " Table '{full_table_id}', top_k => {top_k})\n", + " WHERE \n", + " distance < {distance_threshold} AND query.{index_column} > base.{index_column}\n", + ")\n", + "SELECT * FROM '{full_table_id}'\n", + "WHERE {index_column} NOT IN (SELECT {index_column} FROM dupes);\n", + "\"\"\"\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "metadata": { + "id": "435e9d2fa246" + }, + "outputs": [ + { + "data": { + "text/plain": [ + "pyarrow.Table\n", + "f0_: int64\n", + "----\n", + "f0_: [[4989]]" + ] + }, + "execution_count": 18, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "bq_client.query(f\"SELECT COUNT(*) FROM '{full_table_id}_dedupe'\").result().to_arrow()" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "metadata": { + "id": "f1530a5c5ac5" + }, + "outputs": [ + { + "data": { + "text/plain": [ + "pyarrow.Table\n", + "f0_: int64\n", + "----\n", + "f0_: [[5000]]" + ] + }, + "execution_count": 19, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "bq_client.query(f\"SELECT COUNT(*) FROM '{full_table_id}'\").result().to_arrow()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "bd0ccdb6e46e" + }, + "source": [ + "Let's query the duplicate records" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "metadata": { + "id": "58d8dd121475" + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Downloading: 100%|\u001b[32m██████████\u001b[0m|\n" + ] + } + ], + "source": [ + "df_dupes = pandas_gbq.read_gbq(\n", + " f\"\"\"\n", + "SELECT \n", + " DISTINCT query.uri,\n", + " base.uri,\n", + " distance\n", + "FROM VECTOR_SEARCH(\n", + " Table '{full_table_id}', \"embedding\",\n", + " Table '{full_table_id}', top_k => 10)\n", + "WHERE \n", + " query.uri > base.uri\n", + " AND distance < .05\n", + "ORDER BY \n", + " distance DESC;\n", + "\"\"\"\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "metadata": { + "id": "09be1f6545d7" + }, + "outputs": [ + { + "data": { + "text/plain": [ + "['gs://vidgen-1m/VidGen_video_1002/1L2Ib7XbFl0-Scene-0002.mp4',\n", + " 'gs://vidgen-1m/VidGen_video_1001/7BdLCNVP3vc-Scene-0018.mp4',\n", + " 'gs://vidgen-1m/VidGen_video_1003/EumrLe0lv2o-Scene-0055.mp4',\n", + " 'gs://vidgen-1m/VidGen_video_1003/26ez1C6FHTU-Scene-0029.mp4',\n", + " 'gs://vidgen-1m/VidGen_video_1005/fGgj5Xwhca0-Scene-0220.mp4',\n", + " 'gs://vidgen-1m/VidGen_video_1004/BytDnUzySCc-Scene-0047.mp4',\n", + " 'gs://vidgen-1m/VidGen_video_1003/frcOHC7TLdA-Scene-0034.mp4',\n", + " 'gs://vidgen-1m/VidGen_video_1001/9yMovThU5Hg-Scene-1171.mp4',\n", + " 'gs://vidgen-1m/VidGen_video_1005/CVT7j05IHN4-Scene-0061.mp4',\n", + " 'gs://vidgen-1m/VidGen_video_1004/8gDxAwsHZzI-Scene-0151.mp4',\n", + " 'gs://vidgen-1m/VidGen_video_0/Copy of -Beq3x4K-xA-Scene-0001.mp4']" + ] + }, + "execution_count": 21, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "df_dupes[\"uri\"].tolist()" + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "metadata": { + "id": "a93d421d611d" + }, + "outputs": [ + { + "data": { + "text/plain": [ + "['gs://vidgen-1m/VidGen_video_1001/LVnVfhcFL3g-Scene-0005.mp4',\n", + " 'gs://vidgen-1m/VidGen_video_10/7BdLCNVP3vc-Scene-0030.mp4',\n", + " 'gs://vidgen-1m/VidGen_video_1001/8LAfQyTauYo-Scene-0053.mp4',\n", + " 'gs://vidgen-1m/VidGen_video_1002/OQWeq_TUMDE-Scene-0012.mp4',\n", + " 'gs://vidgen-1m/VidGen_video_1000/ekKb8hixxFM-Scene-0010.mp4',\n", + " 'gs://vidgen-1m/VidGen_video_1001/doPS18DtqLU-Scene-0390.mp4',\n", + " 'gs://vidgen-1m/VidGen_video_1/xzn_rmla6yU-Scene-0200.mp4',\n", + " 'gs://vidgen-1m/VidGen_video_1000/-_agFJmVJXk-Scene-0068.mp4',\n", + " 'gs://vidgen-1m/VidGen_video_1/JfkzqohuutQ-Scene-0178.mp4',\n", + " 'gs://vidgen-1m/VidGen_video_1003/06dEjljE80Q-Scene-0840.mp4',\n", + " 'gs://vidgen-1m/VidGen_video_0/-Beq3x4K-xA-Scene-0001.mp4']" + ] + }, + "execution_count": 23, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "df_dupes[\"uri_1\"].tolist()" + ] + } + ], + "metadata": { + "colab": { + "name": "semantic-deduplication.ipynb", + "toc_visible": true + }, + "kernelspec": { + "display_name": "Python 3", + "name": "python3" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} diff --git a/gemini/use-cases/multimodal-data-curation/splitting_and_transcoding.ipynb b/gemini/use-cases/multimodal-data-curation/splitting_and_transcoding.ipynb new file mode 100644 index 0000000..f7d3271 --- /dev/null +++ b/gemini/use-cases/multimodal-data-curation/splitting_and_transcoding.ipynb @@ -0,0 +1,1179 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "ur8xi4C7S06n" + }, + "outputs": [], + "source": [ + "# Copyright 2025 Google LLC\n", + "#\n", + "# Licensed under the Apache License, Version 2.0 (the \"License\");\n", + "# you may not use this file except in compliance with the License.\n", + "# You may obtain a copy of the License at\n", + "#\n", + "# https://www.apache.org/licenses/LICENSE-2.0\n", + "#\n", + "# Unless required by applicable law or agreed to in writing, software\n", + "# distributed under the License is distributed on an \"AS IS\" BASIS,\n", + "# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n", + "# See the License for the specific language governing permissions and\n", + "# limitations under the License." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "JAPoU8Sm5E6e" + }, + "source": [ + "# Data Curation Pipeline: Splitting and Transcoding \n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + "
    \n", + " \n", + " \"Google
    Open in Colab\n", + "
    \n", + "
    \n", + " \n", + " \"Google
    Open in Colab Enterprise\n", + "
    \n", + "
    \n", + " \n", + " \"Vertex
    Open in Vertex AI Workbench\n", + "
    \n", + "
    \n", + " \n", + " \"GitHub
    View on GitHub\n", + "
    \n", + "
    \n", + "\n", + "
    \n", + "\n", + "Share to:\n", + "\n", + "\n", + " \"LinkedIn\n", + "\n", + "\n", + "\n", + " \"Bluesky\n", + "\n", + "\n", + "\n", + " \"X\n", + "\n", + "\n", + "\n", + " \"Reddit\n", + "\n", + "\n", + "\n", + " \"Facebook\n", + "" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "84f0f73a0f76" + }, + "source": [ + "| Author(s) |\n", + "| --- |\n", + "| [Noa Ben-Efraim](https://github.com/noabenefraim) |" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "tvgnzT1CKxrO" + }, + "source": [ + "## Overview\n", + "\n", + "Video data presents a unique challenge and opportunity for multi-modal learning. Its sheer scale and temporal redundancy necessitate careful consideration when building datasets for Video Language Models (VLMs). Unlike static images, videos often contain lengthy sequences with minimal informational variance or semantic significance. Training foundation models on such raw data can lead to computational bottlenecks and hinder the model's ability to learn meaningful associations between visual and linguistic cues.\n", + "\n", + "This notebook explores the critical step of video splitting and transcoding involved in constructing effective data curation pipelines tailored for VLM pre-training. By focusing on extracting the most information-dense and semantically relevant segments, we can achieve significant gains in computational efficiency, optimize memory utilization during training, and empower models to capture more fine-grained temporal relationships. We will use a subset VIDGEN-1M to illustrate these steps in the data curation pipeline. The following sections will outline practical approaches to building a VLM data curation pipeline, covering aspects from initial video processing to advanced techniques for identifying and isolating key visual narratives:\n", + "\n", + "+ Data overview\n", + "+ Why video splitting?\n", + "+ Video splitting strategies \n", + "+ Transcoding overview" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "61RBz8LLbxCR" + }, + "source": [ + "## Get started" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "No17Cw5hgx12" + }, + "source": [ + "### Install Google Gen AI SDK and other required packages\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "tFy3H3aPgx12" + }, + "outputs": [], + "source": [ + "%pip install --upgrade --quiet google-genai" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "dmWOrTJ3gx13" + }, + "source": [ + "### Authenticate your notebook environment (Colab only)\n", + "\n", + "If you're running this notebook on Google Colab, run the cell below to authenticate your environment." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "NyKGtVQjgx13" + }, + "outputs": [], + "source": [ + "import sys\n", + "\n", + "if \"google.colab\" in sys.modules:\n", + " from google.colab import auth\n", + "\n", + " auth.authenticate_user()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "DF4l8DTdWgPY" + }, + "source": [ + "### Set Google Cloud project information\n", + "\n", + "To get started using Vertex AI, you must have an existing Google Cloud project and [enable the Vertex AI API](https://console.cloud.google.com/flows/enableapi?apiid=aiplatform.googleapis.com).\n", + "\n", + "Learn more about [setting up a project and a development environment](https://cloud.google.com/vertex-ai/docs/start/cloud-environment)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "Nqwi-5ufWp_B" + }, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/usr/local/google/home/noabe/projects/data_curation/video-data-curation/.venv_data_curation/lib/python3.12/site-packages/tqdm/auto.py:21: TqdmWarning: IProgress not found. Please update jupyter and ipywidgets. See https://ipywidgets.readthedocs.io/en/stable/user_install.html\n", + " from .autonotebook import tqdm as notebook_tqdm\n" + ] + } + ], + "source": [ + "# Use the environment variable if the user doesn't provide Project ID.\n", + "import os\n", + "\n", + "from google import genai\n", + "from google.cloud import storage\n", + "\n", + "# fmt: off\n", + "PROJECT_ID = \"\" # @param {type: \"string\", placeholder: \"[your-project-id]\", isTemplate: true}\n", + "# fmt: on\n", + "if not PROJECT_ID or PROJECT_ID == \"[your-project-id]\":\n", + " PROJECT_ID = str(os.environ.get(\"GOOGLE_CLOUD_PROJECT\"))\n", + "\n", + "LOCATION = os.environ.get(\"GOOGLE_CLOUD_REGION\", \"us-central1\")\n", + "\n", + "bucket = \"vidgen-1m\"\n", + "storage_client = storage.Client()\n", + "\n", + "client = genai.Client(vertexai=True, project=PROJECT_ID, location=LOCATION)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "5303c05f7aa6" + }, + "source": [ + "### Import libraries" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "6fc324893334" + }, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/usr/local/google/home/noabe/projects/data_curation/video-data-curation/.venv_data_curation/lib/python3.12/site-packages/tqdm/auto.py:21: TqdmWarning: IProgress not found. Please update jupyter and ipywidgets. See https://ipywidgets.readthedocs.io/en/stable/user_install.html\n", + " from .autonotebook import tqdm as notebook_tqdm\n" + ] + } + ], + "source": [ + "import glob\n", + "import io\n", + "import json\n", + "import os\n", + "import shutil\n", + "import subprocess\n", + "import tempfile\n", + "\n", + "from google.api_core.exceptions import NotFound\n", + "from google.cloud import storage\n", + "from google.cloud.transcoder_v1.services.transcoder_service import (\n", + " TranscoderServiceClient,\n", + ")\n", + "from scenedetect import SceneManager, open_video\n", + "from scenedetect.detectors import ContentDetector" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "c98ed44596e4" + }, + "source": [ + "## Metadata Extraction and Filtering\n", + "\n", + "Metadata extraction is an important step to extract essential metadata (duration, resolution, format) using ffmpeg or a similar library. Then we can filter out videos that do not meet the criteria for VLM training.\n", + "\n", + "* Example: Minimum duration of 10 seconds.\n", + "* Example: Minimum resolution of 240p.\n", + "\n", + "In this notebook, we are introducing the library __ffmpeg__. FFmpeg is a command-line tool and library suite for handling multimedia data. Its general uses include transcoding between various video/audio formats, resizing and scaling resolutions, extracting components like audio streams or individual frames, and performing basic editing operations such as cutting or concatenating video segments. In a video data curation pipeline, its role is critical for robust transcoding, ensuring raw video files are converted into consistent, compatible formats, and for precise splitting and manipulation, which are fundamental steps before quality filtering and captioning can begin." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "1bbf302acc2a" + }, + "source": [ + "### Load Video Data\n", + "\n", + "This notebook will use a subset of videos from the [VidGen-1M dataset](https://arxiv.org/abs/2408.02629).\n", + "\n", + "First we will load the videos from the GCS bucket." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": { + "id": "738f0e37d178" + }, + "outputs": [], + "source": [ + "def list_videos_in_bucket(bucket_name, prefix):\n", + " \"\"\"Lists video files in a Google Cloud Storage bucket/\n", + "\n", + " Args:\n", + " bucket_name (str): The name of the Google Cloud Storage bucket.\n", + " \"\"\"\n", + " blobs = storage_client.bucket(bucket_name).list_blobs(prefix=prefix)\n", + " video_files = [blob.name for blob in blobs if blob.name.lower().endswith(\".mp4\")]\n", + "\n", + " if not video_files:\n", + " print(\"No video files found in the specified bucket (or with the prefix).\")\n", + " return []\n", + " print(f\"Found {len(video_files)} video file(s).\")\n", + " return video_files" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "1c2d2a229fa3" + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Found 492 video file(s).\n" + ] + } + ], + "source": [ + "# TODO larger video files\n", + "video_files = list_videos_in_bucket(bucket_name=\"vidgen-1m\", prefix=\"VidGen_video_0\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "01e9a9580d29" + }, + "source": [ + "Now, that we have our video files. We perform the following steps:\n", + "\n", + "1. Metadata Extraction: The get_video_metadata function uses ffprobe to extract essential information from video files stored in Google Cloud Storage, such as duration, format, and stream details (codec, resolution). \n", + "\n", + "2. Video Filtering: The filter_videos function takes the extracted metadata and filters the video files based on user-defined criteria, specifically minimum duration and resolution." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "e47fdf88d121" + }, + "outputs": [], + "source": [ + "def get_video_metadata(bucket_name, blob_name):\n", + " \"\"\"Retrieves video metadata from a video file stored in Google Cloud Storage.\n", + "\n", + " Args:\n", + " bucket_name (str): The name of the Google Cloud Storage bucket.\n", + " blob_name (str): The name of the video file (blob) within the bucket.\n", + "\n", + " Returns:\n", + " dict: A dictionary containing video metadata, including:\n", + " - 'duration' (float): The length of the video in seconds.\n", + " - 'streams' (list): A list of stream dictionaries, containing information\n", + " about video, audio, and other streams. Useful for codec info,\n", + " resolution, etc.\n", + " - 'format' (dict): Contains overall format information about the\n", + " video file.\n", + " Returns None if any error occurs.\n", + " \"\"\"\n", + " storage_client = storage.Client()\n", + " bucket = storage_client.bucket(bucket_name)\n", + " blob = bucket.blob(blob_name)\n", + "\n", + " try:\n", + " video_data = blob.download_as_bytes()\n", + " video_stream = io.BytesIO(video_data)\n", + "\n", + " command = [\n", + " \"ffprobe\",\n", + " \"-i\",\n", + " \"pipe:0\",\n", + " \"-analyzeduration\",\n", + " \"2\",\n", + " \"-probesize\",\n", + " \"32\",\n", + " \"-show_format\",\n", + " \"-show_streams\",\n", + " \"-print_format\",\n", + " \"json\",\n", + " ]\n", + "\n", + " process = subprocess.Popen(\n", + " command,\n", + " stdin=subprocess.PIPE,\n", + " stdout=subprocess.PIPE,\n", + " stderr=subprocess.PIPE,\n", + " )\n", + " output, error = process.communicate(input=video_stream.getvalue())\n", + "\n", + " if error:\n", + " error_string = error.decode(\"utf-8\")\n", + " if \"partial file\" in error_string:\n", + " print(\n", + " f\"ffprobe error (partial file): {error_string}. Attempting to extract partial metadata.\"\n", + " )\n", + " else:\n", + " print(f\"ffprobe error: {error_string}\")\n", + " return None\n", + "\n", + " try:\n", + " metadata = json.loads(output.decode(\"utf-8\"))\n", + " duration = None\n", + " if \"format\" in metadata and \"duration\" in metadata[\"format\"]:\n", + " duration = float(metadata[\"format\"][\"duration\"])\n", + " metadata_dict = {\n", + " \"duration\": duration,\n", + " \"streams\": metadata.get(\"streams\", []),\n", + " \"format\": metadata.get(\"format\", {}),\n", + " \"filename\": blob_name,\n", + " }\n", + " return metadata_dict\n", + " except json.JSONDecodeError:\n", + " print(f\"Error decoding ffprobe output for {blob_name}. Skipping file.\")\n", + " return None\n", + "\n", + " except NotFound:\n", + " print(f\"File not found: {blob_name} in bucket {bucket_name}\")\n", + " return None\n", + " except Exception as e:\n", + " print(f\"Error processing {blob_name}: {e}\")\n", + " return None\n", + "\n", + "\n", + "def filter_videos(video_files, bucket_name, min_duration=10, min_resolution_height=240):\n", + " \"\"\"Filters video files based on duration and resolution, using metadata obtained\n", + " from get_video_metadata.\n", + "\n", + " Args:\n", + " video_files (list): A list of video file names (blobs) in the bucket.\n", + " bucket_name (str): The name of the Google Cloud Storage bucket.\n", + " min_duration (int, optional): The minimum duration in seconds.\n", + " min_resolution_height (int, optional): The minimum resolution height\n", + " in pixels.\n", + "\n", + " Returns:\n", + " list: A list of video file names that meet the criteria.\n", + " \"\"\"\n", + " filtered_videos = []\n", + " for video_file in video_files:\n", + " metadata = get_video_metadata(bucket_name, video_file)\n", + " if metadata:\n", + " duration = metadata.get(\"duration\")\n", + " streams = metadata.get(\"streams\", [])\n", + "\n", + " if duration is not None and duration >= min_duration:\n", + " meets_resolution = False\n", + " for stream in streams:\n", + " if stream.get(\"codec_type\") == \"video\":\n", + " height = stream.get(\"height\")\n", + " if height is not None and height >= min_resolution_height:\n", + " meets_resolution = True\n", + " break\n", + "\n", + " if meets_resolution:\n", + " filtered_videos.append(video_file)\n", + " else:\n", + " print(\n", + " f\"Video {video_file} does not meet minimum resolution criteria.\"\n", + " )\n", + " else:\n", + " print(f\"Video {video_file} does not meet minimum duration criteria.\")\n", + " else:\n", + " print(f\"Skipping {video_file} due to metadata extraction failure.\")\n", + " return filtered_videos" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "cfb9df161c92" + }, + "outputs": [], + "source": [ + "video_files_small = video_files[:10]\n", + "\n", + "filtered_video_files = filter_videos(\n", + " video_files_small, bucket, min_duration=10, min_resolution_height=240\n", + ")\n", + "\n", + "if filtered_video_files:\n", + " print(\"Filtered Videos:\")\n", + " for video_file in filtered_video_files:\n", + " print(video_file)\n", + "else:\n", + " print(\"No videos found that meet the criteria.\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "12919db7a864" + }, + "source": [ + "## Defining Split Points\n", + "\n", + "The process of defining split points is crucial for segmenting video data effectively for subsequent analysis, processing, or storage. The \"optimal\" split point strategy is highly dependent on the intended use case of the segmented data. Different approaches offer varying levels of granularity and rely on distinct characteristics of the video content. Choosing the right method can significantly impact the efficiency and relevance of downstream tasks.\n", + "\n", + "__Fixed-Length Segments__\n", + "This is perhaps the simplest method, involving splitting a video into clips of a predetermined, fixed duration (e.g., every 5 seconds). This approach is computationally inexpensive and easy to implement. It's suitable for scenarios where a consistent time-based segmentation is required, such as creating uniform chunks for distributed processing or generating short previews. However, it disregards the video's content, potentially cutting through crucial actions, dialogue, or scene transitions, which can lead to semantically incoherent segments. The fixed length must be chosen carefully based on the expected content and the goals of the data curation.\n", + "\n", + "__Scene Detection Algorithms__\n", + "Scene detection algorithms aim to identify transitions between distinct scenes within a video. These algorithms typically analyze visual differences between consecutive frames, such as changes in color histograms, pixel intensity, or edge detection. More advanced methods can also consider temporal information and machine learning to improve accuracy and differentiate between abrupt cuts, fades, dissolves, and other types of transitions. Splitting by scene creates semantically meaningful segments that represent coherent narrative units, which is beneficial for content-based indexing, retrieval, and analysis. However, the accuracy of these algorithms can vary depending on the video quality, editing style, and the complexity of the visual content.\n", + "\n", + "__Content-Aware Methods__\n", + "Content-aware methods go beyond simple visual differences to understand the underlying content and structure of the video. This category includes techniques like shot boundary detection and keyframe analysis. Shot boundary detection is a more refined form of scene detection, focusing on identifying the immediate transitions between individual camera shots. Keyframe analysis involves identifying the most representative frames within a shot or a longer segment, often based on visual saliency, information content, or diversity. Splitting or annotating based on these methods allows for a more granular and semantically rich representation of the video content, facilitating tasks such as video summarization, activity recognition, and object tracking. These methods often require more computational resources and can be more complex to implement than fixed-length splitting.\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "6f8f0e23447b" + }, + "source": [ + "#### Fixed Length Segments\n", + "\n", + "This method offers a straightforward, time-based strategy for segmenting video, commonly implemented using Python libraries like MoviePy which abstract low-level details. The process involves loading the video clip, mathematically determining segment start and end times based on a fixed duration, and programmatically extracting each subclip. The result is a series of video files of uniform length, providing simple partitioning at the cost of disregarding the video's inherent content or scene structure.\n", + "\n", + "Based on our experience, using direct FFmpeg via ```subprocess``` for fixed-length splitting proved more robust and reliable, particularly for handling the final, potentially short segment, leveraging FFmpeg's purpose-built segment mixer and efficient stream copying (```-c copy```). In contrast, while MoviePy offers a more pythonic and user-friendly API, it encountered a specific bug during the re-encoding process for short segments, making it less dependable for this particular task despite its ease of use. Therefore, for critical data curation pipelines prioritizing reliability and performance in splitting, the direct FFmpeg method holds an advantage even with its less abstract command-line interface.\n", + "\n", + "Therefore, to demonstrate fixed length segment splitting we will use FFmpeg." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "e3c2aba3231f" + }, + "outputs": [], + "source": [ + "bucket_name = \"vidgen-1m\" # Insert your GCS bucket name\n", + "source_blob_name = \"YOUR _BLOB_NAME\" # Example GCS blob name\n", + "\n", + "temp_dir = tempfile.mkdtemp()\n", + "downloaded_file_path = os.path.join(temp_dir, os.path.basename(source_blob_name))\n", + "\n", + "segment_duration_seconds = 5\n", + "output_directory = \"ffmpeg_segments_output\"\n", + "output_filename_pattern = \"segment_%04d.mp4\"\n", + "\n", + "\n", + "def download_blob(bucket_name, source_blob_name, destination_file_name):\n", + " \"\"\"Downloads a blob from the bucket to a local file using authenticated client.\"\"\"\n", + " try:\n", + " storage_client = storage.Client()\n", + " bucket = storage_client.bucket(bucket_name)\n", + " blob = bucket.blob(source_blob_name)\n", + "\n", + " print(\n", + " f\"Attempting authenticated download gs://{bucket_name}/{source_blob_name} to {destination_file_name}\"\n", + " )\n", + "\n", + " if not blob.exists():\n", + " print(\n", + " f\"Error: Blob gs://{bucket_name}/{source_blob_name} does not exist or is not accessible with current credentials.\"\n", + " )\n", + " return False\n", + "\n", + " blob.download_to_filename(destination_file_name)\n", + " print(f\"Download complete: {destination_file_name}\")\n", + " return True\n", + "\n", + " except Exception as e:\n", + " print(f\"An error occurred during authenticated download: {e}\")\n", + " return False\n", + "\n", + "\n", + "def get_video_duration(file_path):\n", + " \"\"\"Gets video duration in seconds using ffprobe.\"\"\"\n", + " ffprobe_command = [\n", + " \"ffprobe\",\n", + " \"-v\",\n", + " \"error\",\n", + " \"-show_entries\",\n", + " \"format=duration\",\n", + " \"-of\",\n", + " \"default=noprint_wrappers=1:nokey=1\",\n", + " file_path,\n", + " ]\n", + " try:\n", + " result = subprocess.run(\n", + " ffprobe_command, check=True, capture_output=True, text=True\n", + " )\n", + " duration_str = result.stdout.strip()\n", + " return float(duration_str)\n", + " except (\n", + " FileNotFoundError,\n", + " subprocess.CalledProcessError,\n", + " ValueError,\n", + " Exception,\n", + " ) as e:\n", + " print(f\"Warning: Could not get duration for {file_path} using ffprobe: {e}\")\n", + " return None\n", + "\n", + "\n", + "def ffmpeg_call():\n", + " \"\"\"Constructs and executes ffmpeg command for fixed length video segmenting.\"\"\"\n", + " ffmpeg_command = [\n", + " \"ffmpeg\",\n", + " \"-i\",\n", + " downloaded_file_path,\n", + " \"-map\",\n", + " \"0\",\n", + " \"-c\",\n", + " \"copy\",\n", + " \"-f\",\n", + " \"segment\",\n", + " \"-segment_time\",\n", + " str(segment_duration_seconds),\n", + " \"-reset_timestamps\",\n", + " \"1\",\n", + " \"-loglevel\",\n", + " \"error\",\n", + " os.path.join(output_directory, output_filename_pattern),\n", + " ]\n", + "\n", + " print(\"Executing FFmpeg command...\")\n", + " subprocess.run(ffmpeg_command, check=True, capture_output=True, text=True)\n", + "\n", + " print(\"FFmpeg command executed successfully.\")\n", + "\n", + " print(\"\\nVerifying output segment files:\")\n", + " try:\n", + " expected_prefix = output_filename_pattern.split(\"%\")[0]\n", + " expected_suffix = \".\" + output_filename_pattern.split(\".\")[-1]\n", + "\n", + " output_items = os.listdir(output_directory)\n", + " segment_files = [\n", + " item\n", + " for item in output_items\n", + " if item.startswith(expected_prefix)\n", + " and item.endswith(expected_suffix)\n", + " and os.path.isfile(os.path.join(output_directory, item))\n", + " ]\n", + "\n", + " if segment_files:\n", + " print(\n", + " f\"Detected {len(segment_files)} segment file(s) written to '{output_directory}':\"\n", + " )\n", + "\n", + " for seg_file in sorted(segment_files):\n", + " full_file_path = os.path.join(output_directory, seg_file)\n", + "\n", + " segment_duration = get_video_duration(full_file_path)\n", + " duration_display = (\n", + " f\"{segment_duration:.2f}s\"\n", + " if segment_duration is not None\n", + " else \"N/A (ffprobe failed)\"\n", + " )\n", + " print(f\"- {seg_file} (Duration: {duration_display})\")\n", + "\n", + " else:\n", + " print(\n", + " f\"No files matching the pattern '{expected_prefix}* {expected_suffix}' found in '{output_directory}'.\"\n", + " )\n", + " print(\n", + " \"This might indicate FFmpeg ran but produced no output, or the output pattern is incorrect.\"\n", + " )\n", + "\n", + " except Exception as e:\n", + " print(f\"Error listing or verifying output files: {e}\")\n", + "\n", + "\n", + "temp_dir_to_cleanup = temp_dir\n", + "input_video_duration = None\n", + "\n", + "try:\n", + " # 1. Download the video from GCS (This step requires authentication)\n", + " download_success = download_blob(\n", + " bucket_name, source_blob_name, downloaded_file_path\n", + " )\n", + "\n", + " if not download_success:\n", + " print(\"Authenticated video download failed. Exiting.\")\n", + " exit()\n", + "\n", + " input_video_duration = get_video_duration(downloaded_file_path)\n", + " if input_video_duration is None:\n", + " print(\n", + " f\"Error: Could not get duration of input video {downloaded_file_path}. Cannot proceed.\"\n", + " )\n", + " exit()\n", + " else:\n", + " print(f\"\\nInput video duration: {input_video_duration:.2f} seconds.\")\n", + "\n", + " # 2. Ensure the output directory for segments exists\n", + " os.makedirs(output_directory, exist_ok=True)\n", + " print(f\"\\nOutput directory '{output_directory}' ensured.\")\n", + "\n", + " # 3. Construct and Execute FFmpeg Command for Fixed-Length Splitting\n", + " print(\n", + " f\"\\nStarting FFmpeg fixed-length splitting ({segment_duration_seconds}s segments) for {downloaded_file_path}...\"\n", + " )\n", + " ffmpeg_call()\n", + "\n", + "\n", + "except FileNotFoundError:\n", + " print(\"Error: FFmpeg or ffprobe command not found.\")\n", + " print(\n", + " \"Please ensure FFmpeg (which includes ffprobe) is installed and in your system's PATH.\"\n", + " )\n", + "except subprocess.CalledProcessError as e:\n", + " print(\"Error during FFmpeg execution:\")\n", + " print(f\"Command: {' '.join(e.cmd)}\")\n", + " print(f\"Return Code: {e.returncode}\")\n", + " print(f\"stdout:\\n{e.stdout}\")\n", + " print(f\"stderr:\\n{e.stderr}\")\n", + "except Exception as e:\n", + " print(f\"\\nAn unexpected error occurred during processing: {e}\")\n", + "\n", + "\n", + "finally:\n", + " # Clean up the temporary local file and the temporary directory\n", + " print(\"\\n--- Cleaning up temporary files ---\")\n", + " if os.path.exists(downloaded_file_path):\n", + " try:\n", + " os.remove(downloaded_file_path)\n", + " print(f\"Removed temporary file: {downloaded_file_path}\")\n", + " except OSError as e:\n", + " print(f\"Error removing file {downloaded_file_path}: {e}\")\n", + "\n", + " # This removes the temporary directory the input file was in (e.g., /tmp/tmp...)\n", + " if os.path.exists(temp_dir_to_cleanup):\n", + " try:\n", + " shutil.rmtree(temp_dir_to_cleanup)\n", + " print(f\"Removed temporary directory: {temp_dir_to_cleanup}\")\n", + " except OSError as e:\n", + " print(f\"Error removing temporary directory {temp_dir_to_cleanup}: {e}.\")\n", + "\n", + "\n", + "print(\"\\nFixed-length splitting process finished.\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "4575f17c40e9" + }, + "source": [ + "#### Scene Detection\n", + "\n", + "For this approach, we are using PySceneDetect. This is a command-line tool and a Python library specifically designed for detecting scene changes in videos. The process utilizes the `open_video` function to correctly load the video source, preparing it for analysis by the scene detection algorithms. A `SceneManager` is then configured with specific detectors, such as the `ContentDetector`, which analyzes frame-to-frame visual changes to pinpoint potential scene transitions. For the `ContentDetector`, the `threshold` parameter is critical as it determines the sensitivity to visual changes, requiring careful tuning based on the video's characteristics. Finally, executing the detection process on the video object yields a structured list of detected scene boundaries, providing precise split points for data curation.\n", + "\n", + "In this example, we will show an example script for how to perform scene detection based splitting on one video of the dataset." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "1d30c9b63395" + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Attempting authenticated download gs://vidgen-1m/VidGen_video_0/-EGK4gGtz44-Scene-0398.mp4 to /tmp/tmpws4ba8nm/-EGK4gGtz44-Scene-0398.mp4\n", + "Download complete: /tmp/tmpws4ba8nm/-EGK4gGtz44-Scene-0398.mp4\n", + "\n", + "Processing local temporary file: /tmp/tmpws4ba8nm/-EGK4gGtz44-Scene-0398.mp4\n", + "Video source opened successfully with open_video.\n", + "Starting scene detection...\n", + "Scene detection finished.\n", + "\n", + "Detected 2 scenes for VidGen_video_0/-EGK4gGtz44-Scene-0398.mp4.\n", + " Scene 1: Start 00:00:00.000 - End 00:00:04.200\n", + " Scene 2: Start 00:00:04.200 - End 00:00:04.367\n", + "\n", + "--- Cleaning up temporary files ---\n", + "Removed temporary file: /tmp/tmpws4ba8nm/-EGK4gGtz44-Scene-0398.mp4\n", + "Removed temporary directory: /tmp/tmpws4ba8nm\n" + ] + } + ], + "source": [ + "bucket_name = \"vidgen-1m\"\n", + "source_blob_name = \"VidGen_video_0/-EGK4gGtz44-Scene-0398.mp4\"\n", + "\n", + "\n", + "# Create a temporary directory to store the downloaded file\n", + "temp_dir = tempfile.mkdtemp()\n", + "\n", + "destination_file_name = os.path.join(temp_dir, os.path.basename(source_blob_name))\n", + "\n", + "video_object = None\n", + "temp_dir_to_cleanup = temp_dir\n", + "\n", + "try:\n", + " # 1. Download the video from GCS (Requires Authentication)\n", + " download_success = download_blob(\n", + " bucket_name, source_blob_name, destination_file_name\n", + " )\n", + "\n", + " if not download_success:\n", + " print(\"Authenticated video download failed. Exiting.\")\n", + " exit()\n", + "\n", + " video_source_path = destination_file_name\n", + " print(f\"\\nProcessing local temporary file: {video_source_path}\")\n", + "\n", + " # 2. Open the video source using the recommended open_video function\n", + " video_object = open_video(video_source_path)\n", + " print(\"Video source opened successfully with open_video.\")\n", + "\n", + " # 3. Create SceneManager and add detectors\n", + " scene_manager = SceneManager(\n", + " stats_manager=None\n", + " ) # Pass stats_manager=None if not using\n", + " scene_manager.add_detector(\n", + " ContentDetector(threshold=20)\n", + " ) # Adjust threshold as needed\n", + "\n", + " # 4. Perform scene detection, passing the video object directly\n", + " print(\"Starting scene detection...\")\n", + " scene_manager.detect_scenes(video_object) # Pass the result of open_video here\n", + " print(\"Scene detection finished.\")\n", + "\n", + " # 5. Retrieve list of scenes\n", + " scene_list = scene_manager.get_scene_list()\n", + "\n", + " print(f\"\\nDetected {len(scene_list)} scenes for {source_blob_name}.\")\n", + "\n", + " # 6. Print scene timestamps\n", + " for i, scene in enumerate(scene_list):\n", + " print(\n", + " f\" Scene {i + 1}: Start {scene[0].get_timecode()} - End {scene[1].get_timecode()}\"\n", + " )\n", + "\n", + "\n", + "except Exception as e:\n", + " print(f\"\\nAn error occurred during scene processing: {e}\")\n", + "\n", + "finally:\n", + " # Clean up the temporary local file and the temporary directory\n", + " print(\"\\n--- Cleaning up temporary files ---\")\n", + " if \"destination_file_name\" in locals() and os.path.exists(destination_file_name):\n", + " try:\n", + " os.remove(destination_file_name)\n", + " print(f\"Removed temporary file: {destination_file_name}\")\n", + " except OSError as e:\n", + " print(f\"Error removing file {destination_file_name}: {e}\")\n", + "\n", + " if \"temp_dir_to_cleanup\" in locals() and os.path.exists(temp_dir_to_cleanup):\n", + " try:\n", + " shutil.rmtree(temp_dir_to_cleanup)\n", + " print(f\"Removed temporary directory: {temp_dir_to_cleanup}\")\n", + " except OSError as e:\n", + " print(\n", + " f\"Error removing temporary directory {temp_dir_to_cleanup}: {e}. It might not be empty.\"\n", + " )" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "153f9c9b4996" + }, + "source": [ + "## Transcoding \n", + "\n", + "Now that you have your video split into segments, the next step in a data curation pipeline is to transcode them. Transcoding is the process of converting a video file from one format (codec, container, resolution, bitrate, etc.) to another. This is often done to standardize the video format for analysis, reduce file size, or ensure compatibility with specific tools or platforms.   \n", + "\n", + "Similar to splitting, the most common and powerful tool for transcoding is FFmpeg. You can perform transcoding on each segment by calling FFmpeg via Python's ```subprocess``` module.   \n", + "\n", + "Determining the \"best\" target transcoding parameters for a video curation pipeline depends heavily on the specific goals and constraints of that pipeline, such as the required quality for analysis, storage limitations, compatibility with downstream tools, and processing speed requirements. However, here are some common and recommended parameters that strike a good balance for many curation purposes:\n", + "\n", + "1. Container Format (e.g., MP4): MP4 (.mp4) is the most widely compatible container format. It's well-supported across various operating systems, software libraries, and hardware, making it an excellent choice for ensuring your curated data is easily accessible and usable by different tools in the pipeline.   \n", + "\n", + "2. Video Codec (e.g., H.264 or H.265):\n", + "\n", + " H.264 (AVC) (libx264 in FFmpeg): This is the most compatible and still offers a very good balance between compression efficiency and quality. It's the safest default as almost all video software and hardware can decode it efficiently.   \n", + "\n", + " H.265 (HEVC) (libx265 in FFmpeg): Offers significantly better compression than H.264 for the same quality, resulting in smaller file sizes. Choose this if storage or bandwidth is a major concern, but be aware it requires more processing power to encode/decode and has less universal compatibility than H.264.   \n", + "\n", + "3. Video Quality/Bitrate (e.g., CRF 20-23):\n", + "\n", + " Constant Rate Factor (CRF) for H.264/H.265: Using CRF is often preferred over specifying a fixed bitrate. It tells the encoder to maintain a consistent perceptual quality throughout the video, adjusting the bitrate as needed for complex scenes. A CRF value between 18 and 23 typically provides a good balance, where 18 is often considered visually near-lossless and 23 offers significant file size reduction with minimal noticeable quality loss for many purposes.   \n", + "\n", + " Fixed Bitrate (-b:v): Use this if you have strict file size targets, but quality will vary depending on the complexity of the video content.\n", + "\n", + " Resolution: The ideal resolution depends on the detail needed for downstream analysis or display.\n", + "\n", + " Keep Original: If full detail is essential.\n", + "\n", + " Downscale: Common choices are standard HD resolutions like 1920x1080 (1080p) or 1280x720 (720p). Downscaling reduces file size and standardizes dimensions, which can be beneficial for consistent input to models or analysis tools. Use FFmpeg's scale video filter (-vf scale=1280:720 or -vf scale=-2:720 to maintain aspect ratio).   \n", + "\n", + "4. Frame Rate (FPS):\n", + "\n", + " Keep Original: Generally recommended unless standardization of temporal sampling is required.\n", + "\n", + " Standardize: Choosing a common frame rate like 25 or 30 fps can be useful if your analysis or models require consistent time steps. Use FFmpeg's -r option.\n", + "\n", + "5. Audio Codec and Bitrate (e.g., AAC 128k-192k):\n", + "\n", + " Audio Codec (e.g., AAC): AAC (aac in FFmpeg) is the standard audio codec for MP4 containers and offers good compression with decent quality.   \n", + " \n", + " Audio Bitrate (-b:a): A bitrate between 128k and 192k is typically sufficient for most curation purposes, providing clear audio without excessively increasing file size.\n", + " \n", + "\n", + "A solid starting point for a general-purpose curation pipeline, balancing compatibility, quality, and manageability, would be:\n", + "\n", + "+ Container: MP4 (.mp4)\n", + "+ Video Codec: H.264 (libx264)\n", + "+ Video Quality: CRF 21-23 (Adjust based on acceptable quality vs. size)\n", + "+ Resolution: 1920x1080 or 1280x720 (if downscaling is acceptable, choose based on need)\n", + "+ Frame Rate: Keep original (omit -r) or standardize to 25/30.\n", + "+ Audio Codec: AAC (aac)\n", + "+ Audio Bitrate: 128k or 192k (-b:a 128k)\n", + "\n", + "Always test your chosen parameters on a sample of your video data to ensure the output quality, file size, and processing time meet your pipeline's requirements.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "451df7a9d818" + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Transcoded output directory 'transcoded_segments' ensured.\n", + "\n", + "Starting transcoding for 2 files in ffmpeg_segments_output...\n", + "\n", + "Processing segment_0000.mp4 -> segment_0000_transcoded.mp4\n", + " Executing FFmpeg command for segment_0000.mp4...\n", + " Transcoding successful for segment_0000.mp4.\n", + "\n", + "Processing segment_0001.mp4 -> segment_0001_transcoded.mp4\n", + " Executing FFmpeg command for segment_0001.mp4...\n", + " Transcoding successful for segment_0001.mp4.\n", + "\n", + "Transcoding process finished for all files.\n" + ] + } + ], + "source": [ + "input_directory = \"ffmpeg_segments_output\"\n", + "transcoding_output_directory = \"transcoded_segments\"\n", + "\n", + "# Define target transcoding parameters (adjust as needed)\n", + "target_video_codec = \"libx264\" # Example: H.264\n", + "target_audio_codec = \"aac\" # Example: AAC\n", + "target_resolution = (\n", + " \"1280x720\" # Example resolution or None to keep original, or 'scale=-2:720' etc.\n", + ")\n", + "target_video_bitrate = \"1500k\" # Example bitrate or None\n", + "target_audio_bitrate = \"128k\" # Example bitrate or None\n", + "target_fps = \"25\" # Example frame rate or None to keep original\n", + "\n", + "if not os.path.isdir(input_directory):\n", + " print(f\"Error: Input directory not found at {input_directory}\")\n", + " exit()\n", + "\n", + "segment_files_to_transcode = glob.glob(os.path.join(input_directory, \"*.mp4\"))\n", + "\n", + "if not segment_files_to_transcode:\n", + " print(f\"No MP4 files found in the input directory: {input_directory}\")\n", + " exit()\n", + "\n", + "# Ensure the output directory exists\n", + "os.makedirs(transcoding_output_directory, exist_ok=True)\n", + "print(f\"Transcoded output directory '{transcoding_output_directory}' ensured.\")\n", + "\n", + "print(\n", + " f\"\\nStarting transcoding for {len(segment_files_to_transcode)} files in {input_directory}...\"\n", + ")\n", + "\n", + "\n", + "def ffmpeg_transcode_segment(input_segment_path):\n", + " \"\"\"Constructs the ffmpeg command for transcoding segments with transcoding parameters.\"\"\"\n", + " input_segment_filename = os.path.basename(input_segment_path)\n", + " base_filename, file_extension = os.path.splitext(input_segment_filename)\n", + " output_extension = \".mp4\"\n", + " output_transcoded_filename = f\"{base_filename}_transcoded{output_extension}\"\n", + " output_transcoded_path = os.path.join(\n", + " transcoding_output_directory, output_transcoded_filename\n", + " )\n", + "\n", + " print(f\"\\nProcessing {input_segment_filename} -> {output_transcoded_filename}\")\n", + "\n", + " ffmpeg_command = [\n", + " \"ffmpeg\",\n", + " \"-i\",\n", + " input_segment_path, # Input file\n", + " \"-c:v\",\n", + " target_video_codec, # Set video codec\n", + " \"-b:v\",\n", + " target_video_bitrate, # Set video bitrate\n", + " \"-c:a\",\n", + " target_audio_codec, # Set audio codec\n", + " \"-b:a\",\n", + " target_audio_bitrate, # Set audio bitrate\n", + " \"-vf\",\n", + " f\"scale={target_resolution}\", # Scale video filter\n", + " \"-r\",\n", + " target_fps, # Set frame rate\n", + " \"-y\", # Overwrite output file without asking\n", + " \"-loglevel\",\n", + " \"error\", # Suppress verbose FFmpeg output, show errors only\n", + " output_transcoded_path, # Output file\n", + " ]\n", + "\n", + " try:\n", + " print(f\" Executing FFmpeg command for {input_segment_filename}...\")\n", + " subprocess.run(ffmpeg_command, check=True, capture_output=True, text=True)\n", + " print(f\" Transcoding successful for {input_segment_filename}.\")\n", + "\n", + " except FileNotFoundError:\n", + " print(f\" Error: FFmpeg command not found. Skipping {input_segment_filename}.\")\n", + " except subprocess.CalledProcessError as e:\n", + " print(f\" Error during FFmpeg execution for {input_segment_filename}:\")\n", + " print(f\" Command: {' '.join(e.cmd)}\")\n", + " print(f\" Return Code: {e.returncode}\")\n", + " print(f\" stdout:\\n{e.stdout}\")\n", + " print(f\" stderr:\\n{e.stderr}\")\n", + " except Exception as e:\n", + " print(f\" An unexpected error occurred for {input_segment_filename}: {e}\")\n", + "\n", + "\n", + "# Sort the files for consistent processing order\n", + "for input_segment_path in sorted(segment_files_to_transcode):\n", + " ffmpeg_transcode_segment(input_segment_path)\n", + "\n", + "print(\"\\nTranscoding process finished for all files.\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "3144cdac492a" + }, + "source": [ + "### Transcoding with the Google Cloud Transcoder API\n", + "\n", + "The current implementation uses `ffmpeg`, which is the industry standard. However this requires constructing complex command line arguments. By contrast, Google Cloud Transcoder API is a fully managed service that simplifies complex video conversion, removing the need to operate and maintain your own encoding infrastructure. It specializes in producing adaptive bitrate formats like HLS and MPEG-DASH, ensuring a smooth, high-quality viewing experience for users on any device and network condition. Beyond simple transcoding, the API enables sophisticated content manipulation, including the ability to stitch multiple clips together, overlay watermarks, and embed captions. It's asynchronous, job-based workflow allows applications to submit a detailed processing request and receive notifications upon completion without being blocked. Ultimately, this service streamlines professional media pipelines by offloading heavy processing, which empowers developers to build advanced video features faster.\n", + "\n", + "Below is an example code block with how to use the Transcoder API." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "1adbf41bc5d1" + }, + "outputs": [], + "source": [ + "gcs_input_uri = \"gs://your-input-bucket/path/to/your-video.mp4\"\n", + "gcs_output_uri_prefix = \"gs://your-output-bucket/path/for-transcoded-segments/\"\n", + "\n", + "# Define the start and end times for the clip you want to extract.\n", + "# This example will create a 10-second clip starting at the 5-second mark.\n", + "start_time_seconds = 5.0\n", + "end_time_seconds = 15.0\n", + "\n", + "# Instantiate the Transcoder API client\n", + "transcoder_client = TranscoderServiceClient()\n", + "\n", + "# The parent resource for the API call\n", + "parent = f\"projects/{PROJECT_ID}/locations/{LOCATION}\"\n", + "\n", + "# Define a unique name for the output segment\n", + "output_segment_filename = (\n", + " f\"video_segment_{int(start_time_seconds)}s_to_{int(end_time_seconds)}s.mp4\"\n", + ")\n", + "gcs_output_uri = f\"{gcs_output_uri_prefix}{output_segment_filename}\"\n", + "\n", + "# Create the job configuration dictionary. This structure tells the Transcoder API\n", + "# exactly what to do.\n", + "job = {\n", + " \"input_uri\": gcs_input_uri,\n", + " \"output_uri\": gcs_output_uri,\n", + " \"config\": {\n", + " # 'edit_list' is used to define splits or stitch together clips.\n", + " # Here we define one \"atom\" that points to our input video and\n", + " # specifies the start and end times.\n", + " \"edit_list\": [\n", + " {\n", + " \"key\": \"atom0\",\n", + " \"inputs\": [{\"key\": \"input0\", \"uri\": gcs_input_uri}],\n", + " \"start_time_offset\": {\"seconds\": int(start_time_seconds)},\n", + " \"end_time_offset\": {\"seconds\": int(end_time_seconds)},\n", + " }\n", + " ],\n", + " # 'elementary_streams' define the video and audio encoding settings.\n", + " \"elementary_streams\": [\n", + " {\n", + " \"key\": \"video-stream0\",\n", + " \"video_stream\": {\n", + " \"h264\": {\n", + " \"height_pixels\": 720,\n", + " \"width_pixels\": 1280,\n", + " \"bitrate_bps\": 2500000,\n", + " \"frame_rate\": 30,\n", + " }\n", + " },\n", + " },\n", + " {\n", + " \"key\": \"audio-stream0\",\n", + " \"audio_stream\": {\"codec\": \"aac\", \"bitrate_bps\": 128000},\n", + " },\n", + " ],\n", + " # 'mux_streams' combine the elementary streams into an output file container.\n", + " \"mux_streams\": [\n", + " {\n", + " \"key\": \"output-mp4\",\n", + " \"container\": \"mp4\",\n", + " \"elementary_streams\": [\"video-stream0\", \"audio-stream0\"],\n", + " }\n", + " ],\n", + " },\n", + "}\n", + "\n", + "try:\n", + " print(f\"Submitting transcoding job to split '{gcs_input_uri}'...\")\n", + "\n", + " # Use the client to create the job\n", + " response = transcoder_client.create_job(parent=parent, job=job)\n", + "\n", + " print(\"\\nAPI Response:\")\n", + " print(f\"Job Name: {response.name}\")\n", + " print(f\"Creation Time: {response.create_time}\")\n", + " print(f\"Status: {response.state}\")\n", + " print(\"\\nThe job has been submitted and is processing asynchronously.\")\n", + " print(f\"Check the GCS output path for results: {gcs_output_uri}\")\n", + "\n", + "except Exception as e:\n", + " print(f\"\\nAn error occurred: {e}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "48696dbe3e42" + }, + "source": [ + "## Conclusion" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "412096908949" + }, + "source": [ + "This notebook demonstrates foundational steps in a video data curation pipeline, essential for transforming raw video sources into a format suitable for analysis or machine learning tasks. A key initial process is video splitting, which breaks down large video files into smaller, more manageable segments based on time or content boundaries. Following splitting, transcoding standardizes the format, resolution, and other technical parameters of these segments, ensuring compatibility and efficiency for consistent downstream processing. By completing these crucial splitting and transcoding stages, the video dataset becomes a standardized collection of clips, prepared for subsequent quality filtering and in-depth analysis.\n" + ] + } + ], + "metadata": { + "colab": { + "name": "splitting_and_transcoding.ipynb", + "toc_visible": true + }, + "kernelspec": { + "display_name": "Python 3", + "name": "python3" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} diff --git a/gemini/use-cases/multimodal-sentiment-analysis/intro_to_multimodal_sentiment_analysis.ipynb b/gemini/use-cases/multimodal-sentiment-analysis/intro_to_multimodal_sentiment_analysis.ipynb new file mode 100644 index 0000000..3ec6a45 --- /dev/null +++ b/gemini/use-cases/multimodal-sentiment-analysis/intro_to_multimodal_sentiment_analysis.ipynb @@ -0,0 +1,513 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "ur8xi4C7S06n" + }, + "outputs": [], + "source": [ + "# Copyright 2025-2026 Google LLC\n", + "#\n", + "# Licensed under the Apache License, Version 2.0 (the \"License\");\n", + "# you may not use this file except in compliance with the License.\n", + "# You may obtain a copy of the License at\n", + "#\n", + "# https://www.apache.org/licenses/LICENSE-2.0\n", + "#\n", + "# Unless required by applicable law or agreed to in writing, software\n", + "# distributed under the License is distributed on an \"AS IS\" BASIS,\n", + "# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n", + "# See the License for the specific language governing permissions and\n", + "# limitations under the License." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "JAPoU8Sm5E6e" + }, + "source": [ + "# Multimodal Sentiment Analysis with Gemini\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + "
    \n", + " \n", + " \"Google
    Open in Colab\n", + "
    \n", + "
    \n", + " \n", + " \"Google
    Open in Colab Enterprise\n", + "
    \n", + "
    \n", + " \n", + " \"Vertex
    Open in Vertex AI Workbench\n", + "
    \n", + "
    \n", + " \n", + " \"GitHub
    View on GitHub\n", + "
    \n", + "
    \n", + "\n", + "
    \n", + "\n", + "Share to:\n", + "\n", + "\n", + " \"LinkedIn\n", + "\n", + "\n", + "\n", + " \"Bluesky\n", + "\n", + "\n", + "\n", + " \"X\n", + "\n", + "\n", + "\n", + " \"Reddit\n", + "\n", + "\n", + "\n", + " \"Facebook\n", + "" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "84f0f73a0f76" + }, + "source": [ + "| | |\n", + "|-|-|\n", + "| Author(s) | [Mohammad Al-Ansari](https://github.com/mansari/) |" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "mzi17id6IXv-" + }, + "source": [ + "## Overview\n", + "\n", + "This notebook demonstrates multimodal sentiment analysis with Gemini by comparing sentiment analysis performed directly on audio with analysis performed on its text transcript, highlighting the benefits of multimodal analysis, including capturing nuances like tone, inflection, and other non-verbal cues, for a richer understanding of sentiment.\n", + "\n", + "Gemini is a family of generative AI models developed by [Google DeepMind](https://deepmind.google/) that is designed for multimodal use cases. [Gemini 2.0](https://cloud.google.com/vertex-ai/generative-ai/docs/gemini-v2) is the latest model version.\n", + "\n", + "### Objectives\n", + "\n", + "In this notebook, we will explore sentiment analysis using text and audio as two different modalities.\n", + "\n", + "In summary, you will learn how to use Gemini with the Gen AI SDK for Python to:\n", + "\n", + " - Load a sample conversation for between two individuals.\n", + " - Transcribe the audio content.\n", + " - Conduct sentiment analysis using two modalities: the audio content and the transcript.\n", + " - Compare the two analyses and provide advantages of each.\n", + "\n", + "For additional multimodal use cases with Gemini, check out [Gemini: An Overview of Multimodal Use Cases](https://github.com/GoogleCloudPlatform/generative-ai/blob/c00b23b5f4963ce3b452b9318dc4e8e3aff7232e/gemini/use-cases/intro_multimodal_use_cases.ipynb)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "dSxKV-CIH9oa" + }, + "source": [ + "### Costs\n", + "\n", + "This tutorial uses billable components of Google Cloud:\n", + "\n", + "- Vertex AI\n", + "\n", + "Learn about [Vertex AI pricing](https://cloud.google.com/vertex-ai/pricing) and use the [Pricing Calculator](https://cloud.google.com/products/calculator/) to generate a cost estimate based on your projected usage.\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "61RBz8LLbxCR" + }, + "source": [ + "## Getting Started" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "No17Cw5hgx12" + }, + "source": [ + "### Install Google Gen AI SDK for Python" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "tFy3H3aPgx12" + }, + "outputs": [], + "source": [ + "%pip install --upgrade --quiet google-genai" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "R5Xep4W9lq-Z" + }, + "source": [ + "### Restart current runtime\n", + "\n", + "To use the newly installed packages in this Jupyter runtime, you must restart the runtime. You can do this by running the cell below, which will restart the current kernel." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "XRvKdaPDTznN" + }, + "outputs": [], + "source": [ + "# Restart kernel after installs so that your environment can access the new packages\n", + "import IPython\n", + "\n", + "app = IPython.Application.instance()\n", + "app.kernel.do_shutdown(True)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "SbmM4z7FOBpM" + }, + "source": [ + "
    \n", + "⚠ The kernel is going to restart. Please wait until it is finished before continuing to the next step. ⚠\n", + "
    " + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "dmWOrTJ3gx13" + }, + "source": [ + "### Authenticate your notebook environment (Colab only)\n", + "\n", + "If you are running this notebook on Google Colab, run the following cell to authenticate your environment. This step is not required if you are using [Vertex AI Workbench](https://cloud.google.com/vertex-ai-workbench)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "NyKGtVQjgx13" + }, + "outputs": [], + "source": [ + "import sys\n", + "\n", + "# Additional authentication is required for Google Colab\n", + "if \"google.colab\" in sys.modules:\n", + " # Authenticate user to Google Cloud\n", + " from google.colab import auth\n", + "\n", + " auth.authenticate_user()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "DF4l8DTdWgPY" + }, + "source": [ + "### Set Google Cloud project information and initialize Vertex AI SDK\n", + "\n", + "To get started using Vertex AI, you must have an existing Google Cloud project and [enable the Vertex AI API](https://console.cloud.google.com/flows/enableapi?apiid=aiplatform.googleapis.com).\n", + "\n", + "Learn more about [setting up a project and a development environment](https://cloud.google.com/vertex-ai/docs/start/cloud-environment)." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "nQaM-2IPxsfV" + }, + "source": [ + "#### Create a client for Vertex AI (with all Google Cloud capabilities and services)\n", + "\n", + "Specify the project ID and location while creating the client." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "LxJ-OHg0xsPp" + }, + "outputs": [], + "source": [ + "import os\n", + "\n", + "from google import genai\n", + "\n", + "PROJECT_ID = \"[your-project-id]\" # @param {type: \"string\", placeholder: \"[your-project-id]\", isTemplate: true}\n", + "if not PROJECT_ID or PROJECT_ID == \"[your-project-id]\":\n", + " PROJECT_ID = str(os.environ.get(\"GOOGLE_CLOUD_PROJECT\"))\n", + "\n", + "LOCATION = os.environ.get(\"GOOGLE_CLOUD_REGION\", \"us-central1\")\n", + "\n", + "client = genai.Client(vertexai=True, project=PROJECT_ID, location=LOCATION)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "5303c05f7aa6" + }, + "source": [ + "### Import libraries" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "6fc324893334" + }, + "outputs": [], + "source": [ + "from IPython.display import Markdown, display\n", + "from google.genai.types import GenerateContentConfig, Part" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "e43229f3ad4f" + }, + "source": [ + "### Load Gemini 2.5 Flash model\n", + "\n", + "Learn more about all [Gemini models on Vertex AI](https://cloud.google.com/vertex-ai/generative-ai/docs/learn/models#gemini-models)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "cf93d5f0ce00" + }, + "outputs": [], + "source": [ + "MODEL_ID = \"gemini-2.5-flash\" # @param {type: \"string\"}" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "pyrOSJi2WPKp" + }, + "source": [ + "### Load sample audio content\n", + "\n", + "Run the code below to load a sample audio file that was previously generated with Gemini.\n", + "\n", + "The sample file includes text that can be interperted differently based on tone, and other audio elements that can impact the results of a sentiment analysis performed on the audio content.\n", + "\n", + "The file has been uploaded to a Google Cloud Storage bucket, so you can load it using the `Part.from_uri` method. You can listen to the content of the file [here](https://storage.googleapis.com/github-repo/generative-ai/gemini/use-cases/multimodal-sentiment-analysis/sample_conversation.wav)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "_CK7ikGLhQFq" + }, + "outputs": [], + "source": [ + "audio_part = Part.from_uri(\n", + " file_uri=\"gs://github-repo/generative-ai/gemini/use-cases/multimodal-sentiment-analysis/sample_conversation.wav\",\n", + " mime_type=\"audio/wav\",\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "06o-EHhcXy3j" + }, + "source": [ + "### Run sentiment analysis on audio content\n", + "\n", + "Now you can ask Gemini to run sentiment analysis directly on the audio file content." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "ggBSfVDCWW4U" + }, + "outputs": [], + "source": [ + "prompt = \"Provide a sentiment analysis of this conversation. Use speaker A, speaker B, etc to identify speakers.\"\n", + "response = client.models.generate_content(\n", + " model=MODEL_ID,\n", + " contents=[\n", + " audio_part,\n", + " prompt,\n", + " ],\n", + ")\n", + "audio_analysis = response.text\n", + "display(Markdown(audio_analysis))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "LAApDX8NXmHI" + }, + "source": [ + "### Generate a transcript from the audio content\n", + "\n", + "You can use Gemini to generate a transcript for the audio file that we can use in our text sentiment analysis." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "FyJoPvWlWWPI" + }, + "outputs": [], + "source": [ + "prompt = \"Generate a transcript of this conversation. Use speaker A, speaker B, etc to identify speakers.\"\n", + "\n", + "response = client.models.generate_content(\n", + " model=MODEL_ID,\n", + " contents=[\n", + " audio_part,\n", + " prompt,\n", + " ],\n", + ")\n", + "\n", + "transcript = response.text\n", + "display(Markdown(transcript))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "AWy4-5ZYXsM0" + }, + "source": [ + "### Run sentiment analysis on the transcript\n", + "\n", + "Now you can ask Gemini to run the sentiment analysis, but this time on the transcript we extracted from the audio." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "W1ab9WjuWWi9" + }, + "outputs": [], + "source": [ + "prompt = (\n", + " \"\"\"Provide a sentiment analysis of this conversation. Use speaker A, speaker B, etc to identify speakers.\n", + "Transcript:\"\"\"\n", + " + transcript\n", + ")\n", + "\n", + "response = client.models.generate_content(\n", + " model=MODEL_ID,\n", + " contents=prompt,\n", + " config=GenerateContentConfig(\n", + " response_modalities=[\"TEXT\"],\n", + " ),\n", + ")\n", + "\n", + "text_analysis = response.text\n", + "display(Markdown(text_analysis))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "6yvB8ysKX6hl" + }, + "source": [ + "### Compare the two analyses\n", + "\n", + "Now you can ask Gemini to compare the two different analyses and show if we gained any insight from either of them." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "SwnuX-bwWXGM" + }, + "outputs": [], + "source": [ + "prompt = (\n", + " \"\"\"Provide a short comparison of two analyses of an audio conversation.\n", + "One was made based on a text transcript of the call, and the other is based on an audio recording.\n", + "Transcript analysis:\"\"\"\n", + " + text_analysis\n", + " + \"\"\"Audio analysis:\"\"\"\n", + " + audio_analysis\n", + ")\n", + "\n", + "response = client.models.generate_content(\n", + " model=MODEL_ID,\n", + " contents=prompt,\n", + " config=GenerateContentConfig(\n", + " response_modalities=[\"TEXT\"],\n", + " ),\n", + ")\n", + "\n", + "comparison = response.text\n", + "display(Markdown(comparison))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "vZ6BAQHVNU9A" + }, + "source": [ + "### Conclusion\n", + "\n", + " Sentiment analysis of audio offers significant advantages, providing a deeper understanding of nuanced sentiment and delivery. Audio cues, like the enthusiastic tone of a speaker or the subtle inflections in their voice, add crucial context often missed by text alone.\n", + "\n", + " This richer information is invaluable for applications from understanding customer feedback to gauging emotional responses in communication. We encourage you to explore these benefits by analyzing your own audio data, such as interviews, podcasts, or presentations.\n" + ] + } + ], + "metadata": { + "colab": { + "name": "intro_to_multimodal_sentiment_analysis.ipynb", + "toc_visible": true + }, + "kernelspec": { + "display_name": "Python 3", + "name": "python3" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} diff --git a/gemini/use-cases/productivity/productivity_coaching_with_google_calendar.ipynb b/gemini/use-cases/productivity/productivity_coaching_with_google_calendar.ipynb new file mode 100644 index 0000000..dfd944f --- /dev/null +++ b/gemini/use-cases/productivity/productivity_coaching_with_google_calendar.ipynb @@ -0,0 +1,579 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "ur8xi4C7S06n" + }, + "outputs": [], + "source": [ + "# Copyright 2025-2026 Google LLC\n", + "#\n", + "# Licensed under the Apache License, Version 2.0 (the \"License\");\n", + "# you may not use this file except in compliance with the License.\n", + "# You may obtain a copy of the License at\n", + "#\n", + "# https://www.apache.org/licenses/LICENSE-2.0\n", + "#\n", + "# Unless required by applicable law or agreed to in writing, software\n", + "# distributed under the License is distributed on an \"AS IS\" BASIS,\n", + "# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n", + "# See the License for the specific language governing permissions and\n", + "# limitations under the License." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "84f0f73a0f76" + }, + "source": [ + "| | |\n", + "|-|-|\n", + "| Author(s) | [Mohammad Al-Ansari](https://github.com/mansari/) |" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "JAPoU8Sm5E6e" + }, + "source": [ + "# Productivity Coaching with Gemini and Google Calendar\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + "
    \n", + " \n", + " \"Google
    Open in Colab\n", + "
    \n", + "
    \n", + " \n", + " \"Google
    Open in Colab Enterprise\n", + "
    \n", + "
    \n", + " \n", + " \"Vertex
    Open in Vertex AI Workbench\n", + "
    \n", + "
    \n", + " \n", + " \"GitHub
    View on GitHub\n", + "
    \n", + "
    \n", + "\n", + "
    \n", + "\n", + "Share to:\n", + "\n", + "\n", + " \"LinkedIn\n", + "\n", + "\n", + "\n", + " \"Bluesky\n", + "\n", + "\n", + "\n", + " \"X\n", + "\n", + "\n", + "\n", + " \"Reddit\n", + "\n", + "\n", + "\n", + " \"Facebook\n", + "" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "mzi17id6IXv-" + }, + "source": [ + "## Overview\n", + "\n", + "This notebook demonstrates how to use Gemini as a personal productivity coach by leveraging external APIs, specifically the Google Calendar API.\n", + "\n", + "Gemini is a family of generative AI models developed by [Google DeepMind](https://deepmind.google/) designed for multimodal use cases. [Gemini 2.0](https://cloud.google.com/vertex-ai/generative-ai/docs/gemini-v2) is the latest model version.\n", + "\n", + "## Objectives\n", + "\n", + "In this notebook, you will learn how to use Gemini's tools feature, along with the Gen AI SDK and Google API SDK for Python, to:\n", + "\n", + "* Analyze a screenshot of a calendar for productivity gains\n", + "* Add Google Calendar API as a tool that Gemini can use\n", + "* Analyze your own calendar to identify productivity gains\n", + "* Learn more about your productivity style and get coaching tips" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "dSxKV-CIH9oa" + }, + "source": [ + "### Costs\n", + "\n", + "This tutorial uses billable components of Google Cloud:\n", + "\n", + "- Vertex AI\n", + "\n", + "Learn about [Vertex AI pricing](https://cloud.google.com/vertex-ai/pricing) and use the [Pricing Calculator](https://cloud.google.com/products/calculator/) to generate a cost estimate based on your projected usage.\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "61RBz8LLbxCR" + }, + "source": [ + "## Getting Started" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "No17Cw5hgx12" + }, + "source": [ + "### Install Google Gen AI SDK and Google API SDK for Python" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "tFy3H3aPgx12" + }, + "outputs": [], + "source": [ + "%pip install --upgrade --quiet google-genai google-api-python-client" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "5303c05f7aa6" + }, + "source": [ + "### Import libraries" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "6fc324893334" + }, + "outputs": [], + "source": [ + "from IPython.display import Markdown, display\n", + "from google import genai\n", + "from google.genai.types import GenerateContentConfig, Part" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "dmWOrTJ3gx13" + }, + "source": [ + "### Authenticate your notebook environment (Colab only)\n", + "\n", + "If you are running this notebook on Google Colab, run the following cell to authenticate your environment. This step is not required if you are using [Vertex AI Workbench](https://cloud.google.com/vertex-ai-workbench)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "NyKGtVQjgx13" + }, + "outputs": [], + "source": [ + "import sys\n", + "\n", + "# Additional authentication is required for Google Colab\n", + "if \"google.colab\" in sys.modules:\n", + " # Authenticate user to Google Cloud\n", + " from google.colab import auth\n", + "\n", + " auth.authenticate_user()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "v8YW7MCx_O6G" + }, + "source": [ + "### Set Google Cloud project information and create client\n", + "\n", + "To get started using Vertex AI, you must have an existing Google Cloud project and [enable the Vertex AI API](https://console.cloud.google.com/flows/enableapi?apiid=aiplatform.googleapis.com).\n", + "\n", + "Learn more about [setting up a project and a development environment](https://cloud.google.com/vertex-ai/docs/start/cloud-environment)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "YwFoe0T9_Ek0" + }, + "outputs": [], + "source": [ + "import os\n", + "\n", + "PROJECT_ID = \"[your-project-id]\" # @param {type: \"string\", placeholder: \"[your-project-id]\", isTemplate: true}\n", + "if not PROJECT_ID or PROJECT_ID == \"[your-project-id]\":\n", + " PROJECT_ID = str(os.environ.get(\"GOOGLE_CLOUD_PROJECT\"))\n", + "\n", + "LOCATION = os.environ.get(\"GOOGLE_CLOUD_REGION\", \"us-central1\")\n", + "\n", + "client = genai.Client(vertexai=True, project=PROJECT_ID, location=LOCATION)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "X_cgnwxgOHGY" + }, + "source": [ + "### Configure Google Calendar API access" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "oL6KigDV6NSG" + }, + "source": [ + "#### Enable Google Calendar API\n", + "\n", + "To start using the Google Calendar API, you must first [enable it](https://console.cloud.google.com/flows/enableapi?apiid=calendar-json.googleapis.com)." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "Lngat8co4a34" + }, + "source": [ + "#### Connect your Google Workspace account\n", + "\n", + "To access your calendar data, you will need to provide credential details. You will use [Application Default Credentials (ADC)](https://cloud.google.com/docs/authentication/provide-credentials-adc) to authorize access to your Google Calendar.\n", + "\n", + "Google Workspace APIs, including Calendar API, have usage limits known as [quotas](https://cloud.google.com/docs/quotas/overview). Specifying your project ensures that the correct quotas are applied when accessing your data.\n", + "\n", + "**Steps to Connect:**\n", + "\n", + "1. **Execute the code below.** This initiates the authentication process.\n", + "2. **Confirm if prompted:** Click on the blinking cursor on the output window, type `Y` and press Enter to proceed.\n", + "3. **Grant Permissions in your browser:** A link to a Google authorization page will be displayed. Click the link and follow the prompts to grant the required access to your calendar data.\n", + "4. **Copy the Authorization Code:** After granting permission, you will receive a code on the screen. Copy the provided code.\n", + "5. **Paste and Enter:** Click on the blinking cursor, paste the copied code into the notebook's output and press Enter to complete the authentication." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "SKenTDdj5NUu" + }, + "outputs": [], + "source": [ + "!gcloud auth application-default login --project \"$PROJECT_ID\" \\\n", + " --scopes https://www.googleapis.com/auth/cloud-platform,https://www.googleapis.com/auth/calendar.events.readonly" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "d5zLisT-ostz" + }, + "source": [ + "
    \n", + "⚠ Important: The credentials obtained during this process are stored in memory and are valid only for the current session. If the runtime is terminated, you will need to re-authorize. ⚠\n", + "
    " + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "e43229f3ad4f" + }, + "source": [ + "### Load Gemini 2.5 Flash model\n", + "\n", + "Learn more about all [Gemini models on Vertex AI](https://cloud.google.com/vertex-ai/generative-ai/docs/learn/models#gemini-models)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "cf93d5f0ce00" + }, + "outputs": [], + "source": [ + "MODEL_ID = \"gemini-2.5-flash\" # @param {type: \"string\"}" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "LVEy_dM5FEgR" + }, + "source": [ + "## Set up system instructions\n", + "\n", + "Before you dive into analyzing your data, let's configure Gemini with clear instructions. These [system instructions](https://cloud.google.com/vertex-ai/generative-ai/docs/learn/prompts/system-instruction-introduction) tell Gemini it's your productivity coach, allowing it to provide tailored analysis." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "0paF3u5uFEET" + }, + "outputs": [], + "source": [ + "system_instruction = \"\"\"\n", + "You are a productivity coach. Your role is to analyze a user's calendar events\n", + "to offer actionable recommendations for improvement and determine their\n", + "productivity style.\n", + "\n", + "In your analysis, consider:\n", + "\n", + "* **Time management:** Efficiency of time allocation and scheduling patterns.\n", + "* **Collaboration:** Patterns of meetings and collaborative work.\n", + "* **Task prioritization:** Methods of prioritizing tasks and potential\n", + "procrastination.\n", + "* **Work-life balance:** Indicators of healthy work-life integration.\n", + "\n", + "Provide specific, actionable recommendations to improve the user's productivity,\n", + " time management, communication, and overall well-being.\n", + "\"\"\"" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "pyrOSJi2WPKp" + }, + "source": [ + "## Analyze sample calendar images\n", + "\n", + "Before you connect to live data, let's test Gemini's ability to analyze calendar images.\n", + "\n", + "You have two sample calendars available: one for a _Developer Advocate_ and another for a _Machine Learning Team Lead_. Both are accessible from a Google Cloud Storage bucket and can be loaded using `Part.from_uri`.\n", + "\n", + "Take a look at the sample calendars:\n", + "\n", + "* [Developer Advocate Calendar](https://storage.googleapis.com/github-repo/generative-ai/gemini/use-cases/productivity/productivity_coaching_with_google_calendar_and_gmail/DevRel_Schedule.png)\n", + "* [Machine Learning Team Lead Calendar](https://storage.googleapis.com/github-repo/generative-ai/gemini/use-cases/productivity/productivity_coaching_with_google_calendar_and_gmail/ML_Lead_Schedule.png)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "0t3avJIcEEpi" + }, + "outputs": [], + "source": [ + "image_filename = \"DevRel_Schedule.png\" # or \"ML_Lead_Schedule.png\"\n", + "\n", + "base_path = \"gs://github-repo/generative-ai/gemini/use-cases/productivity/productivity_coaching_with_google_calendar_and_gmail/\"\n", + "image_file_uri = f\"{base_path}{image_filename}\"\n", + "\n", + "calendar_image_part = Part.from_uri(\n", + " file_uri=image_file_uri,\n", + " mime_type=\"image/png\",\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "FyJoPvWlWWPI" + }, + "outputs": [], + "source": [ + "prompt = \"\"\"\n", + "Analyze this screenshot of my weekly schedule and identify ways to improve\n", + "my productivity and work-life balance.\n", + "\"\"\"\n", + "\n", + "response = client.models.generate_content(\n", + " model=MODEL_ID,\n", + " contents=[\n", + " calendar_image_part,\n", + " prompt,\n", + " ],\n", + " config=GenerateContentConfig(\n", + " system_instruction=system_instruction,\n", + " ),\n", + ")\n", + "calendar_analysis = response.text\n", + "display(Markdown(calendar_analysis))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "AWy4-5ZYXsM0" + }, + "source": [ + "## Connect Google Calendar API\n", + "\n", + "You will now integrate Gemini with the Google Calendar API using [function calling](https://cloud.google.com/vertex-ai/generative-ai/docs/multimodal/function-calling). This allows Gemini to dynamically access your current calendar ensuring its responses are based on live data." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "grpNHeG8lfJ9" + }, + "outputs": [], + "source": [ + "import datetime\n", + "from typing import Any\n", + "\n", + "from googleapiclient.discovery import build\n", + "from googleapiclient.errors import HttpError\n", + "\n", + "\n", + "def get_next_n_calendar_events(num_of_events: int = 10) -> list[dict[str, Any]]:\n", + " \"\"\"\n", + " Retrieves upcoming events from a Google Calendar primary calendar.\n", + "\n", + " Requires valid Google Cloud credentials with Calendar API access.\n", + "\n", + " Args:\n", + " num_of_events: The maximum number of events to retrieve (defaults to 10, max 250).\n", + "\n", + " Returns:\n", + " A list of dictionaries, each representing an event with 'summary', 'start', 'end', and 'status'.\n", + " Date/time values are strings in RFC3339 format (e.g., '2024-07-19T10:00:00-04:00').\n", + " Returns an empty list if no events are found or if an error occurs. Errors are logged.\n", + "\n", + " Raises:\n", + " ValueError: If num_of_events is invalid.\n", + " \"\"\"\n", + " if not 1 <= num_of_events <= 250:\n", + " raise ValueError(\"num_of_events must be between 1 and 250.\")\n", + "\n", + " try:\n", + " service = build(\"calendar\", \"v3\")\n", + " now = datetime.datetime.utcnow().isoformat() + \"Z\"\n", + " events_result = (\n", + " service.events()\n", + " .list(\n", + " calendarId=\"primary\",\n", + " timeMin=now,\n", + " maxResults=num_of_events,\n", + " singleEvents=True,\n", + " orderBy=\"startTime\",\n", + " )\n", + " .execute()\n", + " )\n", + " events = events_result.get(\"items\", [])\n", + "\n", + " # Concise list comprehension for extracting event data\n", + " essential_events = [\n", + " {\n", + " \"summary\": event.get(\"summary\"),\n", + " \"start\": event.get(\"start\", {}).get(\"dateTime\"),\n", + " \"end\": event.get(\"end\", {}).get(\"dateTime\"),\n", + " \"status\": event.get(\"status\"),\n", + " }\n", + " for event in events\n", + " ]\n", + " return essential_events\n", + "\n", + " except HttpError as error:\n", + " print(f\"An error occurred: {error}\")\n", + " return []" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "9VKQhu-vJ34S" + }, + "source": [ + "## Get productivity insights based on your Google Calendar events\n", + "\n", + "You will now use the Google Calendar API tool to prompt Gemini to analyze your calendar events. This analysis can reveal insights into various aspects of your work style, including meeting habits and collaboration patterns.\n", + "\n", + "You will also ask Gemini to identify your unique productivity archetype and style, offering deeper insights into your individual work habits and communication patterns." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "o3rDNCuV8KQ1" + }, + "outputs": [], + "source": [ + "prompt = \"\"\"\n", + "Analyze my upcoming 25 calendar events to provide practical suggestions for\n", + "optimizing my schedule and increasing productivity.\n", + "\n", + "Additionally, identify and explain my productivity archetype and style,\n", + "including its key characteristics, strengths, and weaknesses. Based on this\n", + "analysis, offer actionable recommendations to improve my workflow, time\n", + "management, and communication.\n", + "\"\"\"\n", + "response = client.models.generate_content(\n", + " model=MODEL_ID,\n", + " contents=prompt,\n", + " config=GenerateContentConfig(\n", + " system_instruction=system_instruction,\n", + " temperature=0.9,\n", + " tools=[get_next_n_calendar_events],\n", + " ),\n", + ")\n", + "calendar_analysis = response.text\n", + "display(Markdown(calendar_analysis))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "vZ6BAQHVNU9A" + }, + "source": [ + "## Conclusion\n", + "\n", + "You've successfully extended Gemini to analyze your personal schedule and communication styles and gain valuable insights into your productivity. By integrating with Google Calendar you've enabled personalized recommendations based on your real-time data.\n", + "\n", + "This is just the beginning! Explore extending this analysis to other Google Workspace data, like Google Docs or Drive. With Gemini as your AI productivity coach, optimize your workflow, enhance communication, and achieve a better work-life balance.\n", + "\n", + "Ready to take it further? Expand this use case by building a [Google Workspace Add-on](https://developers.google.com/workspace/add-ons) or a [Google Chat app](https://developers.google.com/workspace/chat). Check out the [Google Workspace Developer documentation](https://developers.google.com/workspace) for more information." + ] + } + ], + "metadata": { + "colab": { + "name": "productivity_coaching_with_google_calendar.ipynb", + "toc_visible": true + }, + "kernelspec": { + "display_name": "Python 3", + "name": "python3" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} diff --git a/gemini/use-cases/productivity/slide_generation_with_marp.ipynb b/gemini/use-cases/productivity/slide_generation_with_marp.ipynb new file mode 100644 index 0000000..424b6a3 --- /dev/null +++ b/gemini/use-cases/productivity/slide_generation_with_marp.ipynb @@ -0,0 +1,410 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "sqi5B7V_Rjim" + }, + "outputs": [], + "source": [ + "# Copyright 2025 Google LLC\n", + "#\n", + "# Licensed under the Apache License, Version 2.0 (the \"License\");\n", + "# you may not use this file except in compliance with the License.\n", + "# You may obtain a copy of the License at\n", + "#\n", + "# https://www.apache.org/licenses/LICENSE-2.0\n", + "#\n", + "# Unless required by applicable law or agreed to in writing, software\n", + "# distributed under the License is distributed on an \"AS IS\" BASIS,\n", + "# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n", + "# See the License for the specific language governing permissions and\n", + "# limitations under the License." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "VyPmicX9RlZX" + }, + "source": [ + "# Slide Generation with Gemini and Marp\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + "
    \n", + " \n", + " \"Google
    Open in Colab\n", + "
    \n", + "
    \n", + " \n", + " \"Google
    Open in Colab Enterprise\n", + "
    \n", + "
    \n", + " \n", + " \"Vertex
    Open in Vertex AI Workbench\n", + "
    \n", + "
    \n", + " \n", + " \"GitHub
    View on GitHub\n", + "
    \n", + "
    \n", + "\n", + "
    \n", + "\n", + "Share to:\n", + "\n", + "\n", + " \"LinkedIn\n", + "\n", + "\n", + "\n", + " \"Bluesky\n", + "\n", + "\n", + "\n", + " \"X\n", + "\n", + "\n", + "\n", + " \"Reddit\n", + "\n", + "\n", + "\n", + " \"Facebook\n", + "" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "8MqT58L6Rm_q" + }, + "source": [ + "| | |\n", + "|-|-|\n", + "| Author(s) | [Paige Bailey](https://github.com/dynamicwebpaige), [Holt Skinner](https://github.com/holtskinner) |" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "nVxnv1D5RoZw" + }, + "source": [ + "## Overview\n", + "\n", + "[Marp](https://marp.app/) is a framework for creating slide decks using Markdown.\n", + "\n", + "This notebook shows how to use the Gemini API and the Google Gen AI SDK for Python to convert a blog post into a slide deck overview.\n", + "\n", + "You will complete the following tasks:\n", + "\n", + "- Use [Gitingest](https://gitingest.com/) to load the Marp repository as a large string.\n", + "- Send the Marp repository and a blog post URL to the Gemini API.\n", + "- Parse the markdown output from Gemini\n", + "- Send markdown to [Marp CLI](https://github.com/marp-team/marp-cli) to convert to PDF." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "gPiTOAHURvTM" + }, + "source": [ + "## Getting Started" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "CHRZUpfWSEpp" + }, + "source": [ + "### Install Google Gen AI SDK for Python\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "sG3_LKsWSD3A" + }, + "outputs": [], + "source": [ + "%pip install --upgrade --quiet google-genai gitingest" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "HlMVjiAWSMNX" + }, + "source": [ + "### Authenticate your notebook environment (Colab only)\n", + "\n", + "If you are running this notebook on Google Colab, run the cell below to authenticate your environment." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "12fnq4V0SNV3" + }, + "outputs": [], + "source": [ + "import sys\n", + "\n", + "if \"google.colab\" in sys.modules:\n", + " from google.colab import auth\n", + "\n", + " auth.authenticate_user()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "Ve4YBlDqzyj9" + }, + "source": [ + "### Connect to a generative AI API service\n", + "\n", + "Google Gen AI APIs and models including Gemini are available in the following two API services:\n", + "\n", + "- **[Google AI for Developers](https://ai.google.dev/gemini-api/docs)**: Experiment, prototype, and deploy small projects.\n", + "- **[Vertex AI](https://cloud.google.com/vertex-ai/generative-ai/docs)**: Build enterprise-ready projects on Google Cloud.\n", + "\n", + "The Google Gen AI SDK provides a unified interface to these two API services.\n", + "\n", + "This notebook shows how to use the Google Gen AI SDK with the Gemini API in Vertex AI." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "LymmEN6GSTn-" + }, + "source": [ + "### Set Google Cloud project information and create client\n", + "\n", + "To get started using Vertex AI, you must have an existing Google Cloud project and [enable the Vertex AI API](https://console.cloud.google.com/flows/enableapi?apiid=aiplatform.googleapis.com).\n", + "\n", + "Learn more about [setting up a project and a development environment](https://cloud.google.com/vertex-ai/docs/start/cloud-environment)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "UCgUOv4nSWhc" + }, + "outputs": [], + "source": [ + "import os\n", + "\n", + "# fmt: off\n", + "PROJECT_ID = \"[your-project-id]\" # @param {type: \"string\", placeholder: \"[your-project-id]\", isTemplate: true}\n", + "# fmt: on\n", + "if not PROJECT_ID or PROJECT_ID == \"[your-project-id]\":\n", + " PROJECT_ID = str(os.environ.get(\"GOOGLE_CLOUD_PROJECT\"))\n", + "\n", + "LOCATION = os.environ.get(\"GOOGLE_CLOUD_REGION\", \"us-central1\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "4ohJK8VpUoYz" + }, + "outputs": [], + "source": [ + "from google import genai\n", + "\n", + "client = genai.Client(vertexai=True, project=PROJECT_ID, location=LOCATION)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "EdvJRUWRNGHE" + }, + "source": [ + "### Import libraries\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "qgdSpVmDbdQ9" + }, + "outputs": [], + "source": [ + "import re\n", + "\n", + "import nest_asyncio\n", + "from IPython.display import Markdown, display\n", + "from gitingest import ingest\n", + "from google.genai.types import GenerateContentConfig, Part\n", + "\n", + "nest_asyncio.apply()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "eXHJi5B6P5vd" + }, + "source": [ + "### Load the Gemini 3 Flash Thinking model\n", + "\n", + "Learn more about all [Gemini models on Vertex AI](https://cloud.google.com/vertex-ai/generative-ai/docs/learn/models#gemini-models)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "-coEslfWPrxo" + }, + "outputs": [], + "source": [ + "MODEL_ID = \"gemini-3.5-flash\" # @param {type: \"string\"}" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "37CH91ddY9kG" + }, + "source": [ + "### Load the Marp repository" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "0M-0Nd8aUoY0" + }, + "outputs": [], + "source": [ + "repo_dir = \"https://github.com/marp-team/marp\"\n", + "\n", + "summary, tree, content = ingest(source=repo_dir, branch=\"main\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "f5a0e244a04c" + }, + "source": [ + "### Send the Marp codebase and blog post in prompt to Gemini" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "_HgBRR43UoY0" + }, + "outputs": [], + "source": [ + "PROMPT = \"\"\"\n", + "Please create a slide overview version of this website (about DeepMind's AI Co-scientist), using the same kind of style and formatting. Link to all of the images in the Marp deck (especially .mp4 files and .GIF). I've also included the Marp codebase, so you can refer back to it for design changes. Make sure that the text is appropriately sized for each slide (keep it quite small, or divide up extra long text into multiple slides), and make sure that the images are placed in well-designed locations.\n", + "\"\"\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "xRJuHj0KZ8xz" + }, + "outputs": [], + "source": [ + "response = client.models.generate_content(\n", + " model=MODEL_ID,\n", + " contents=[\n", + " content,\n", + " PROMPT,\n", + " Part.from_uri(\n", + " file_uri=\"https://research.google/blog/accelerating-scientific-breakthroughs-with-an-ai-co-scientist/\",\n", + " mime_type=\"text/html\",\n", + " ),\n", + " ],\n", + " config=GenerateContentConfig(\n", + " system_instruction=\"Only output markdown.\",\n", + " max_output_tokens=8192,\n", + " ),\n", + ")\n", + "\n", + "display(Markdown(response.text))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "c94b81f8b851" + }, + "source": [ + "### Parse response from Gemini" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "CKUWjwbkXkgD" + }, + "outputs": [], + "source": [ + "marp_text = re.findall(r\"```(?:\\w*\\n)?(.*?)```\", response.text + \"\\n```\", re.DOTALL)[0]\n", + "\n", + "with open(\"slide-deck.md\", \"w\") as f:\n", + " f.write(marp_text)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "ht5VnDnNdrbt" + }, + "source": [ + "### Convert Markdown to a PDF\n", + "\n", + "> **NOTE:** This will only work on a machine with a web browser installed. Colab is not supported." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "CWIU2ir_cBC9" + }, + "outputs": [], + "source": [ + "!npx @marp-team/marp-cli@latest slide-deck.md --pdf" + ] + } + ], + "metadata": { + "colab": { + "name": "slide_generation_with_marp.ipynb", + "toc_visible": true + }, + "kernelspec": { + "display_name": "Python 3", + "name": "python3" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} diff --git a/gemini/use-cases/retail/multimodal_retail_recommendations.ipynb b/gemini/use-cases/retail/multimodal_retail_recommendations.ipynb new file mode 100644 index 0000000..3f091ed --- /dev/null +++ b/gemini/use-cases/retail/multimodal_retail_recommendations.ipynb @@ -0,0 +1,540 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "ijGzTHJJUCPY" + }, + "outputs": [], + "source": [ + "# Copyright 2023 Google LLC\n", + "#\n", + "# Licensed under the Apache License, Version 2.0 (the \"License\");\n", + "# you may not use this file except in compliance with the License.\n", + "# You may obtain a copy of the License at\n", + "#\n", + "# https://www.apache.org/licenses/LICENSE-2.0\n", + "#\n", + "# Unless required by applicable law or agreed to in writing, software\n", + "# distributed under the License is distributed on an \"AS IS\" BASIS,\n", + "# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n", + "# See the License for the specific language governing permissions and\n", + "# limitations under the License." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "VEqbX8OhE8y9" + }, + "source": [ + "# Multimodal retail recommendation: using Gemini to recommend items based on images and image reasoning\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
    \n", + " \n", + " \"Google
    Run in Colab\n", + "
    \n", + "
    \n", + " \n", + " \"Google
    Run in Colab Enterprise\n", + "
    \n", + "
    \n", + " \n", + " \"Vertex
    Open in Vertex AI Workbench\n", + "
    \n", + "
    \n", + " \n", + " \"GitHub
    View on GitHub\n", + "
    \n", + "
    \n", + " \n", + " \"Google
    Open in Cloud Skills Boost\n", + "
    \n", + "
    \n", + "\n", + "
    \n", + "\n", + "Share to:\n", + "\n", + "\n", + " \"LinkedIn\n", + "\n", + "\n", + "\n", + " \"Bluesky\n", + "\n", + "\n", + "\n", + " \"X\n", + "\n", + "\n", + "\n", + " \"Reddit\n", + "\n", + "\n", + "\n", + " \"Facebook\n", + " \n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "048a84091064" + }, + "source": [ + "| | |\n", + "|-|-|\n", + "|Author(s) | [Thu Ya Kyaw](https://github.com/iamthuya) |" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "VK1Q5ZYdVL4Y" + }, + "source": [ + "## Overview\n", + "\n", + "For retail companies, recommendation systems improve customer experience and thus can increase sales.\n", + "\n", + "This notebook shows how you can use the multimodal capabilities of Gemini 3 model to rapidly create a multimodal recommendation system out-of-the-box." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "3zLxCZkKEAhC" + }, + "source": [ + "## Scenario\n", + "\n", + "The customer shows you their living room:\n", + "\n", + "|Customer photo |\n", + "|:-----:|\n", + "| |\n", + "\n", + "\n", + "\n", + "Below are four chair options that the customer is trying to decide between:\n", + "\n", + "|Chair 1| Chair 2 | Chair 3 | Chair 4 |\n", + "|:-----:|:----:|:-----:|:----:|\n", + "| ||||\n", + "\n", + "\n", + "How can you use Gemini, a multimodal model, to help the customer choose the best option, and also explain why?" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "RQT500QqVPIb" + }, + "source": [ + "### Objectives\n", + "\n", + "Your main objective is to learn how to create a recommendation system that can provide both recommendations and explanations using a multimodal model: Gemini 3.\n", + "\n", + "In this notebook, you will begin with a scene (e.g. a living room) and use the Gemini model to perform visual understanding. You will also investigate how the Gemini model can be used to recommend an item (e.g. a chair) from a list of furniture items as input.\n", + "\n", + "By going through this notebook, you will learn:\n", + "- how to use the Gemini model to perform visual understanding\n", + "- how to take multimodality into consideration in prompting for the Gemini model\n", + "- how the Gemini model can be used to create retail recommendation applications out-of-the-box" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "1y6_3dTwV2fI" + }, + "source": [ + "### Costs\n", + "This tutorial uses billable components of Google Cloud:\n", + "\n", + "- Vertex AI\n", + "\n", + "Learn about [Vertex AI pricing](https://cloud.google.com/vertex-ai/pricing) and use the [Pricing Calculator](https://cloud.google.com/products/calculator/) to generate a cost estimate based on your projected usage." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "QDU0XJ1xRDlL" + }, + "source": [ + "## Getting Started" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "N5afkyDMSBW5" + }, + "source": [ + "### Install Gen AI SDK for Python" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "V_-ThW_ZUYRV" + }, + "outputs": [], + "source": [ + "%pip install --upgrade google-genai" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "6Fom0ZkMSBW6" + }, + "source": [ + "### Authenticate your notebook environment (Colab only)\n", + "\n", + "If you are running this notebook on Google Colab, run the following cell to authenticate your environment. This step is not required if you are using [Vertex AI Workbench](https://cloud.google.com/vertex-ai-workbench)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "LCaCx6PLSBW6" + }, + "outputs": [], + "source": [ + "import sys\n", + "\n", + "# Additional authentication is required for Google Colab\n", + "if \"google.colab\" in sys.modules:\n", + " # Authenticate user to Google Cloud\n", + " from google.colab import auth\n", + "\n", + " auth.authenticate_user()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "BuQwwRiniVFG" + }, + "source": [ + "### Define Google Cloud project information and initialize Vertex AI\n", + "\n", + "Initialize the Vertex AI SDK for Python for your project:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "rtMowvm-yQ97" + }, + "outputs": [], + "source": [ + "# Use the environment variable if the user doesn't provide Project ID.\n", + "import os\n", + "\n", + "from google import genai\n", + "\n", + "# fmt: off\n", + "PROJECT_ID = \"[your-project-id]\" # @param {type: \"string\", placeholder: \"[your-project-id]\", isTemplate: true}\n", + "# fmt: on\n", + "if not PROJECT_ID or PROJECT_ID == \"[your-project-id]\":\n", + " PROJECT_ID = str(os.environ.get(\"GOOGLE_CLOUD_PROJECT\"))\n", + "\n", + "LOCATION = os.environ.get(\"GOOGLE_CLOUD_REGION\", \"us-central1\")\n", + "\n", + "client = genai.Client(vertexai=True, project=PROJECT_ID, location=LOCATION)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "4437b7608c8e" + }, + "source": [ + "### Import libraries" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "yZ75ioBU9EwM" + }, + "outputs": [], + "source": [ + "from google.genai.types import GenerateContentConfig, Part" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "hhcZv4HFUYRW" + }, + "source": [ + "## Using Gemini model\n", + "\n", + "Gemini is a multimodal model that supports adding image and video in text or chat prompts for a text response." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "ExdEEGUqUYRW" + }, + "source": [ + "### Load Gemini model" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "pxlvLmncUYRW" + }, + "outputs": [], + "source": [ + "MODEL_ID = \"gemini-3.5-flash\"" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "MONqa2K8IjSz" + }, + "source": [ + "### Visual understanding with Gemini\n", + "\n", + "Here you will ask the Gemini model to describe a room in details from its image. To do that you have to **combine text and image in a single prompt**." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "26wP-epVFBBK" + }, + "outputs": [], + "source": [ + "# urls for room images\n", + "room_image_url = \"https://storage.googleapis.com/github-repo/img/gemini/retail-recommendations/rooms/spacejoy-c0JoR_-2x3E-unsplash.jpg\"\n", + "room_image = Part.from_uri(file_uri=room_image_url, mime_type=\"image/jpeg\")\n", + "\n", + "prompt = \"Describe what's visible in this room and the overall atmosphere:\"\n", + "contents = [\n", + " room_image,\n", + " prompt,\n", + "]\n", + "\n", + "responses = client.models.generate_content_stream(model=MODEL_ID, contents=contents)\n", + "\n", + "print(\"\\n-------Response--------\")\n", + "for response in responses:\n", + " print(response.text, end=\"\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "64I8-GxSbkTv" + }, + "source": [ + "### Generating open recommendations based on built-in knowledge\n", + "\n", + "Using the same image, you can ask the model to recommend **a piece of furniture** that would fit in it alongside with the description of the room.\n", + "\n", + "Note that the model can choose **any furniture** to recommend in this case, and can do so from its only built-in knowledge." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "UaRDl1WvypT8" + }, + "outputs": [], + "source": [ + "prompt1 = \"Recommend a new piece of furniture for this room:\"\n", + "prompt2 = \"and explain the reason in detail\"\n", + "contents = [prompt1, room_image, prompt2]\n", + "\n", + "responses = client.models.generate_content_stream(model=MODEL_ID, contents=contents)\n", + "\n", + "print(\"\\n-------Response--------\")\n", + "for response in responses:\n", + " print(response.text, end=\"\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "lzdaG4iIdZM8" + }, + "source": [ + "In the next cell, you will ask the model to recommend **a type of chair** that would fit in it alongside with the description of the room.\n", + "\n", + "Note that the model can choose **any type of chair** to recommend in this case." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "h7xKDMFLyQuD" + }, + "outputs": [], + "source": [ + "prompt1 = \"Describe this room:\"\n", + "prompt2 = \"and recommend a type of chair that would fit in it\"\n", + "contents = [prompt1, room_image, prompt2]\n", + "\n", + "responses = client.models.generate_content_stream(model=MODEL_ID, contents=contents)\n", + "\n", + "print(\"\\n-------Response--------\")\n", + "for response in responses:\n", + " print(response.text, end=\"\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "idEB0JFcbznD" + }, + "source": [ + "### Generating recommendations based on provided images\n", + "\n", + "Instead of keeping the recommendation open, you can also provide a list of items for the model to choose from. Here you will download a few chair images and set them as options for the Gemini model to recommend from. This is particularly useful for retail companies who want to provide recommendations to users based on the kind of room they have, and the available items that the store offers." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "f_jMmwRiFcPF" + }, + "outputs": [], + "source": [ + "# Download and display sample chairs\n", + "furniture_image_urls = [\n", + " \"https://storage.googleapis.com/github-repo/img/gemini/retail-recommendations/furnitures/cesar-couto-OB2F6CsMva8-unsplash.jpg\",\n", + " \"https://storage.googleapis.com/github-repo/img/gemini/retail-recommendations/furnitures/daniil-silantev-1P6AnKDw6S8-unsplash.jpg\",\n", + " \"https://storage.googleapis.com/github-repo/img/gemini/retail-recommendations/furnitures/ruslan-bardash-4kTbAMRAHtQ-unsplash.jpg\",\n", + " \"https://storage.googleapis.com/github-repo/img/gemini/retail-recommendations/furnitures/scopic-ltd-NLlWwR4d3qU-unsplash.jpg\",\n", + "]\n", + "\n", + "# Load furniture images as Part Objects\n", + "furniture_images = [\n", + " Part.from_uri(file_uri=url, mime_type=\"image/jpeg\") for url in furniture_image_urls\n", + "]\n", + "\n", + "# To recommend an item from a selection, you will need to label the item number within the prompt.\n", + "# That way you are providing the model with a way to reference each image as you pose a question.\n", + "# Labelling images within your prompt also help to reduce hallucinations and overall produce better results.\n", + "contents = [\n", + " \"Consider the following chairs:\",\n", + " \"chair 1:\",\n", + " furniture_images[0],\n", + " \"chair 2:\",\n", + " furniture_images[1],\n", + " \"chair 3:\",\n", + " furniture_images[2],\n", + " \"chair 4:\",\n", + " furniture_images[3],\n", + " \"room:\",\n", + " room_image,\n", + " \"You are an interior designer. For each chair, explain whether it would be appropriate for the style of the room:\",\n", + "]\n", + "\n", + "responses = client.models.generate_content_stream(model=MODEL_ID, contents=contents)\n", + "\n", + "print(\"\\n-------Response--------\")\n", + "for response in responses:\n", + " print(response.text, end=\"\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "Mkw-DixOUYRc" + }, + "source": [ + "You can also return the responses in JSON format, to make it easier to plug recommendations into a recommendation system:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "MDBQufRYUYRc" + }, + "outputs": [], + "source": [ + "contents = [\n", + " \"Consider the following chairs:\",\n", + " \"chair 1:\",\n", + " furniture_images[0],\n", + " \"chair 2:\",\n", + " furniture_images[1],\n", + " \"chair 3:\",\n", + " furniture_images[2],\n", + " \"chair 4:\",\n", + " furniture_images[3],\n", + " \"room:\",\n", + " room_image,\n", + " \"You are an interior designer. Return in JSON, for each chair, whether it would fit in the room, with an explanation:\",\n", + "]\n", + "\n", + "responses = client.models.generate_content_stream(\n", + " model=MODEL_ID,\n", + " contents=contents,\n", + " config=GenerateContentConfig(response_mime_type=\"application/json\"),\n", + ")\n", + "\n", + "print(\"\\n-------Response--------\")\n", + "for response in responses:\n", + " print(response.text, end=\"\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "0_uhGdZERToX" + }, + "source": [ + "## Conclusion\n", + "\n", + "This notebook showed how you can easily build a multimodal recommendation system using Gemini for furniture, but you can also use the similar approach in:\n", + "\n", + "- recommending clothes based on an occasion or an image of the venue\n", + "- recommending wallpaper based on the room and settings\n", + "\n", + "You may also want to explore how you can build a RAG (retrieval-augmented generation) system where you retrieve relevant images from your store inventory to users who can they use Gemini to help identify the most ideal choice from the various options provided, and also explain the rationale to users." + ] + } + ], + "metadata": { + "colab": { + "name": "multimodal_retail_recommendations.ipynb", + "toc_visible": true + }, + "kernelspec": { + "display_name": "Python 3", + "name": "python3" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} diff --git a/gemini/use-cases/retail/product_attributes_extraction.ipynb b/gemini/use-cases/retail/product_attributes_extraction.ipynb new file mode 100644 index 0000000..afd3bfd --- /dev/null +++ b/gemini/use-cases/retail/product_attributes_extraction.ipynb @@ -0,0 +1,643 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "ur8xi4C7S06n" + }, + "outputs": [], + "source": [ + "# Copyright 2024 Google LLC\n", + "#\n", + "# Licensed under the Apache License, Version 2.0 (the \"License\");\n", + "# you may not use this file except in compliance with the License.\n", + "# You may obtain a copy of the License at\n", + "#\n", + "# https://www.apache.org/licenses/LICENSE-2.0\n", + "#\n", + "# Unless required by applicable law or agreed to in writing, software\n", + "# distributed under the License is distributed on an \"AS IS\" BASIS,\n", + "# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n", + "# See the License for the specific language governing permissions and\n", + "# limitations under the License." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "JAPoU8Sm5E6e" + }, + "source": [ + "# Product attributes extraction and detailed descriptions from images using Gemini 2.0\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + "
    \n", + " \n", + " \"Google
    Open in Colab\n", + "
    \n", + "
    \n", + " \n", + " \"Google
    Open in Colab Enterprise\n", + "
    \n", + "
    \n", + " \n", + " \"Vertex
    Open in Workbench\n", + "
    \n", + "
    \n", + " \n", + " \"GitHub
    View on GitHub\n", + "
    \n", + "
    \n", + "\n", + "
    \n", + "\n", + "Share to:\n", + "\n", + "\n", + " \"LinkedIn\n", + "\n", + "\n", + "\n", + " \"Bluesky\n", + "\n", + "\n", + "\n", + " \"X\n", + "\n", + "\n", + "\n", + " \"Reddit\n", + "\n", + "\n", + "\n", + " \"Facebook\n", + " " + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "84f0f73a0f76" + }, + "source": [ + "| | |\n", + "|-|-|\n", + "|Author(s) | [Tianli Yu](https://github.com/tianli) |" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "tvgnzT1CKxrO" + }, + "source": [ + "## Overview\n", + "\n", + "This Colab teaches how to build a general agent (on top of Gemini) to extract different product attributes or detailed product descriptions from an image input. It also introduces a prompting technique called \"self-correcting prompt\" where you can ask the model to check and verify the result by itself (all in one single prompt). Self-correcting prompt proves to improve the overall quality of the agent's output.\n", + "\n", + "In the following sections we will:\n", + "\n", + "* Write the necessary image loading and parsing library.\n", + "* Create a product image agent.\n", + "* Run the agent on a set of examples for tasks like product image description and attribute extraction." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "61RBz8LLbxCR" + }, + "source": [ + "## Get started" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "No17Cw5hgx12" + }, + "source": [ + "### Install Vertex AI SDK and other required packages" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "tFy3H3aPgx12" + }, + "outputs": [], + "source": [ + "%pip install --upgrade --user --quiet google-cloud-aiplatform" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "R5Xep4W9lq-Z" + }, + "source": [ + "### Restart runtime\n", + "\n", + "To use the newly installed packages in this Jupyter runtime, you must restart the runtime. You can do this by running the cell below, which restarts the current kernel.\n", + "\n", + "The restart might take a minute or longer. After it's restarted, continue to the next step." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": { + "id": "XRvKdaPDTznN" + }, + "outputs": [ + { + "data": { + "text/plain": [ + "{'status': 'ok', 'restart': True}" + ] + }, + "execution_count": 1, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "import IPython\n", + "\n", + "app = IPython.Application.instance()\n", + "app.kernel.do_shutdown(True)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "SbmM4z7FOBpM" + }, + "source": [ + "
    \n", + "⚠ The kernel is going to restart. Wait until it's finished before continuing to the next step. ⚠\n", + "
    " + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "dmWOrTJ3gx13" + }, + "source": [ + "### Authenticate your notebook environment (Colab only)\n", + "\n", + "If you're running this notebook on Google Colab, run the cell below to authenticate your environment." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": { + "id": "NyKGtVQjgx13" + }, + "outputs": [], + "source": [ + "import sys\n", + "\n", + "if \"google.colab\" in sys.modules:\n", + " from google.colab import auth\n", + "\n", + " auth.authenticate_user()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "DF4l8DTdWgPY" + }, + "source": [ + "### Set Google Cloud project information and initialize Vertex AI SDK\n", + "\n", + "To get started using Vertex AI, you must have an existing Google Cloud project and [enable the Vertex AI API](https://console.cloud.google.com/flows/enableapi?apiid=aiplatform.googleapis.com).\n", + "\n", + "Learn more about [setting up a project and a development environment](https://cloud.google.com/vertex-ai/docs/start/cloud-environment)." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": { + "id": "Nqwi-5ufWp_B" + }, + "outputs": [], + "source": [ + "PROJECT_ID = \"YOUR_PROJECT_ID\" # @param {type:\"string\"}\n", + "LOCATION = \"us-central1\" # @param {type:\"string\"}\n", + "\n", + "\n", + "import vertexai\n", + "\n", + "vertexai.init(project=PROJECT_ID, location=LOCATION)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "EdvJRUWRNGHE" + }, + "source": [ + "## Build a Product Image Agent" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": { + "cellView": "form", + "id": "xJQczGZX3FfW" + }, + "outputs": [], + "source": [ + "# @title Image loading and parsing library\n", + "import json\n", + "\n", + "import requests\n", + "from vertexai.preview.generative_models import Part\n", + "\n", + "\n", + "def get_mime_from_uri(image_uri: str) -> str:\n", + " \"\"\"Get the mime type from the image uri.\"\"\"\n", + " if image_uri.endswith(\".png\"):\n", + " return \"image/png\"\n", + " elif image_uri.endswith(\".gif\"):\n", + " return \"image/gif\"\n", + " else:\n", + " # Assume JPEG as the default mime\n", + " return \"image/jpeg\"\n", + "\n", + "\n", + "def load_image_part_from_uri(image_uri: str) -> None | Part:\n", + " \"\"\"Load image to a prompt Part from a remote or local file URI.\n", + "\n", + " Args:\n", + " image_uri (str): The uri or the local file path for the image.\n", + "\n", + " Returns:\n", + " The prompt Part object.\n", + " \"\"\"\n", + " if image_uri.startswith(\"gs://\"):\n", + " return Part.from_uri(image_uri, mime_type=get_mime_from_uri(image_uri))\n", + " elif image_uri.startswith(\"http://\") or image_uri.startswith(\"https://\"):\n", + " response = requests.get(image_uri)\n", + " if response.status_code == 200:\n", + " image_bytes = response.content\n", + " return Part.from_data(image_bytes, mime_type=get_mime_from_uri(image_uri))\n", + " else:\n", + " print(\n", + " f\"Fetch image failed for {image_uri}, status code: {response.status_code}\"\n", + " )\n", + " return None\n", + " else:\n", + " image_bytes = open(image_uri, \"rb\").read()\n", + " return Part.from_data(image_bytes, mime_type=get_mime_from_uri(image_uri))\n", + "\n", + "\n", + "def parse_json_from_markdown(answer: str) -> str:\n", + " \"\"\"Parse the json from the markdown answer.\n", + "\n", + " Args:\n", + " answer (str): The markdown answer from the model.\n", + "\n", + " Returns:\n", + " A parsed json dictionary}\n", + " \"\"\"\n", + " lines = answer.split(\"```\")\n", + " try:\n", + " # Tries to parse the last json in the answer.\n", + " answer = lines[-2]\n", + " if answer.startswith(\"json\"):\n", + " answer = answer[4:]\n", + " result = json.loads(answer)\n", + " except json.JSONDecodeError:\n", + " # Falls back to the first json in the answer.\n", + " answer = lines[1]\n", + " if answer.startswith(\"json\"):\n", + " answer = answer[4:]\n", + " result = json.loads(answer)\n", + " return json.dumps(result)" + ] + }, + { + "cell_type": "code", + "execution_count": 44, + "metadata": { + "id": "3nThlwYK_tN2" + }, + "outputs": [], + "source": [ + "# @title The ProductImageAgent Class.\n", + "\n", + "\n", + "from IPython.display import display\n", + "import ipywidgets as widgets\n", + "from vertexai.preview.generative_models import GenerationConfig, GenerativeModel, Part\n", + "\n", + "\n", + "class ProductImageAgent:\n", + " \"\"\"An agent that wraps around Gemini 2.0 to extract product attributes from\n", + " images.\n", + "\n", + " Args:\n", + " gemini_model_version (str): The version string of the Gemini 2.0 model.\n", + " gemini-2.0-flash or gemini-2.0-flash\n", + " temperature (float): The temperature of the model. Defaults to 1.0.\n", + " max_output_tokens (int): The maximum number of output tokens. Defaults to\n", + " 8192.\n", + " \"\"\"\n", + "\n", + " def __init__(\n", + " self,\n", + " gemini_model_version: str = \"gemini-2.0-flash\",\n", + " temperature: float = 0.0,\n", + " max_output_tokens: int = 8192,\n", + " ):\n", + " config = GenerationConfig(\n", + " temperature=temperature, max_output_tokens=max_output_tokens\n", + " )\n", + "\n", + " # System instructions, add any common instructions here.\n", + " sys_inst = \"\"\"\n", + " As an assistant for an online retailer, your task is to recognize\n", + " attributes from the provided product image.\n", + " If an attribute vocabulary is provided, please only select attribute values\n", + " in that vocabulary. You answer should be strictly consistent with what's in\n", + " the image. If any attributes do not exist in the image, please\n", + " return null for that attribute.\n", + " \"\"\"\n", + " self.gemini_model = GenerativeModel(\n", + " gemini_model_version, generation_config=config, system_instruction=sys_inst\n", + " )\n", + "\n", + " def get_detailed_description(self, image_uri: str, debug: bool = False) -> str:\n", + " \"\"\"Generates the detailed product description from an image.\n", + "\n", + " Args:\n", + " image_uri: The url to the image, can be a local file path, or a url\n", + " from the web or gcs.\n", + "\n", + " Returns:\n", + " The generated detailed description from the model response.\n", + " \"\"\"\n", + " image_part = load_image_part_from_uri(image_uri)\n", + " if image_part is None:\n", + " return \"\"\n", + " prompt = \"\"\"\n", + " Please write a complete and detailed product description for the\n", + " above product image. The length of the description should be at least\n", + " 200 words.\n", + " \"\"\"\n", + " if debug:\n", + " print(\"====== Begin Debug Info ======\")\n", + " preview = widgets.HTML(value=f'')\n", + " display(preview)\n", + " print(f\"Prompt:\\n{prompt}\")\n", + " print(\"====== End Debug Info ======\")\n", + "\n", + " model_response = self.gemini_model.generate_content([image_part, prompt])\n", + " return model_response.text\n", + "\n", + " def get_attributes(\n", + " self,\n", + " image_uri: str,\n", + " product_category: None | str = None,\n", + " vocabulary_json: None | str = None,\n", + " debug: bool = False,\n", + " ) -> str:\n", + " \"\"\"Generates the product attributes from an image.\n", + "\n", + " Args:\n", + " image_uri (str): The uri of the product image to generate attributes.\n", + " vocabulary_json (str): A json string list all the attribute names and\n", + " their possible vocabulary.\n", + "\n", + " Returns:\n", + " The product attribute json string from the model response.\n", + " \"\"\"\n", + " image_part = load_image_part_from_uri(image_uri)\n", + " if image_part is None:\n", + " return \"\"\n", + " if product_category:\n", + " prompt = f\"\"\"\n", + " The above image is a product image from the {product_category}\n", + " category.\n", + " Please list all the relevant attributes in the {product_category}\n", + " category for the main product in the above image and return a list of\n", + " key-value pairs in json format.\n", + " \"\"\"\n", + " else:\n", + " prompt = \"\"\"\n", + " Please recognize the main product's all relevant attributes in the\n", + " above image and return a list of key-value pairs in json format.\n", + " \"\"\"\n", + " if vocabulary_json:\n", + " prompt += f\"\"\"\n", + " Please using only the vocabulary defined in the following json:\n", + " {vocabulary_json}\n", + " For each key, you should select the most appropriate attribute value\n", + " from its corresponding vocabulary list and returns one value\n", + " for each attribute key.\n", + " You can return null for that key if no attributes match.\n", + " \"\"\"\n", + " if debug:\n", + " print(\"====== Begin Debug Info ======\")\n", + " preview = widgets.HTML(value=f'')\n", + " display(preview)\n", + " print(f\"Prompt:\\n{prompt}\")\n", + " print(\"====== End Debug Info ======\")\n", + "\n", + " model_response = self.gemini_model.generate_content([image_part, prompt])\n", + " return model_response.text\n", + "\n", + " def get_attributes_self_correcting_prompt(\n", + " self,\n", + " image_uri: str,\n", + " product_category: None | str = None,\n", + " vocabulary_json: None | str = None,\n", + " debug: bool = False,\n", + " ) -> str:\n", + " \"\"\"Generates the product attributes from an image using self-correcting prompt.\n", + "\n", + " Args:\n", + " image_uri (str): The uri of the product image to generate attributes.\n", + " vocabulary_json (str): A json string list all the attribute names and\n", + " their possible vocabulary.\n", + "\n", + " Returns:\n", + " The product attribute json string from the parsed model response.\n", + " \"\"\"\n", + " image_part = load_image_part_from_uri(image_uri)\n", + " if image_part is None:\n", + " return \"\"\n", + " if product_category:\n", + " prompt = f\"\"\"\n", + " The above image is a product image from the {product_category}\n", + " category.\n", + " First please list all the relevant attributes in the\n", + " {product_category} category for the main product in the above\n", + " image and return a list of key-value pairs in json format.\n", + " \"\"\"\n", + " else:\n", + " prompt = \"\"\"\n", + " First Please recognize the main product's all relevant attributes in\n", + " the above image and return a list of key-value pairs in json\n", + " format.\n", + " \"\"\"\n", + "\n", + " if vocabulary_json:\n", + " prompt += f\"\"\"\n", + " Please using only the vocabulary defined in the following json:\n", + " {vocabulary_json}\n", + " For each key, you should select the most appropriate attribute value\n", + " from its corresponding vocabulary list and return the attribute key-\n", + " value pair. You can return null for that key if no attributes match.\n", + " \"\"\"\n", + "\n", + " # Adding the self-correction instructions.\n", + " prompt += \"\"\"\n", + " Next, treat the returned json as the result generated by a different\n", + " model, rate each key-value pair as \"correct\" or \"wrong\" based on the\n", + " same image. You can output in a format like \"key - value: correct (or\n", + " wrong)\".\n", + " Then, based on this evaluation, please update all the attributes that\n", + " are corrected in the final json output.\n", + " Please use markdown to annotate different json in your output.\n", + " \"\"\"\n", + " model_response = self.gemini_model.generate_content([image_part, prompt])\n", + "\n", + " if debug:\n", + " print(\"====== Begin Debug Info ======\")\n", + " preview = widgets.HTML(value=f'')\n", + " display(preview)\n", + " print(f\"Prompt:\\n{prompt}\\n\")\n", + " print(f\"Response:\\n{model_response.candidates[0].content.parts[0].text}\\n\")\n", + " print(\"====== End Debug Info ======\")\n", + "\n", + " # Parse the model_response to get the final json.\n", + " return parse_json_from_markdown(\n", + " model_response.candidates[0].content.parts[0].text\n", + " )\n", + "\n", + "\n", + "# Creates the agent.\n", + "product_agent = ProductImageAgent(\n", + " gemini_model_version=\"gemini-2.0-flash-preview-0514\", temperature=0\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "OCF1YAGzASQw" + }, + "outputs": [], + "source": [ + "# @title Example 1: Generate detailed product description from an image.\n", + "image_uri = \"IMAGE_URI\" # @param {type:\"string\"}\n", + "\n", + "product_agent.get_detailed_description(image_uri, True)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "fLwaCYeHl4An" + }, + "outputs": [], + "source": [ + "# @title Example 2: Get product attribute json from an image -- open vocabulary.\n", + "image_uri = \"IMAGE_URI\" # @param {type:\"string\"}\n", + "\n", + "# Open vocabulary.\n", + "attribute_json = product_agent.get_attributes(image_uri, debug=True)\n", + "print(f\"Open vocabulary attributes:\\n{attribute_json}\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "Q-afCK3lY0U3" + }, + "outputs": [], + "source": [ + "# @title Example 3: Get product attribute json from an image -- closed vocabulary.\n", + "image_uri = \"IMAGE_URI\" # @param {type:\"string\"}\n", + "\n", + "# Closed vocabulary.\n", + "vocabulary = \"\"\"\n", + "{\"Pattern\": [\"Animal\", \"Letter\", \"Plaid\", \"Plain\", \"Polka Dot\", \"Quilted\", \"Striped\", \"Tie Dye\", \"Tropical\", \"Zebra\", \"Block\", \"Rainbow\", \"Floral\"], \"Toe\": [\"Almond Toe\", \"Cap Toe\", \"Closed Toe\", \"Peep Toe\", \"Point Toe\", \"Pointed Toe\", \"Round Toe\", \"Square Toe\", \"Toe Post\", \"Open Toe\"], \"Style\": [\"Ballet\", \"Bandage\", \"Basics\", \"Casual\", \"Classic\", \"Cute\", \"Elegant\", \"Formal\", \"Modern\", \"Motorcycle\", \"Retro\", \"Sexy\", \"Boho\", \"Modest\", \"Comfort\", \"Minimalist\"], \"Strap Type\": [\"Adjustable\", \"Ankle cuff\", \"Ankle straps\", \"Chain\", \"Convertible\", \"Criss Cross\", \"D'orsay\", \"Double Handle\", \"Flowers\", \"Gladiator\", \"Lace Up\", \"Mary Jane\", \"Ring\", \"Slingbacks\", \"Strappy\", \"T strap\", \"Zipper\", \"Elastic\", \"Velcro\", \"Ankle Strap\"], \"Heels\": [\"Chunky\", \"Cork\", \"Espadrilles\", \"Flat\", \"Flatform\", \"Platform\", \"Stiletto\", \"Cone Heel\", \"Kitten Heels\", \"Hidden Wedge\", \"Wedges\", \"Pyramid\"], \"Closure Type\": [\"Back Zipper\", \"Buckle\", \"Zipper\", \"Magnet\", \"Slip on\", \"Hook Loop\", \"Lace-up\", \"Flap\"]}\n", + "\"\"\"\n", + "\n", + "attribute_json = product_agent.get_attributes(\n", + " image_uri, vocabulary_json=vocabulary, debug=True\n", + ")\n", + "print(f\"Closed vocabulary attributes\\n{attribute_json}\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "aVCiddCOfYZ0" + }, + "outputs": [], + "source": [ + "# @title Example 4: Get product attribute json from an image using self-correcting prompt -- open vocabulary\n", + "image_uri = \"IMAGE_URI\" # @param {type:\"string\"}\n", + "\n", + "# Open vocabulary.\n", + "attribute_json = product_agent.get_attributes_self_correcting_prompt(\n", + " image_uri, product_category=\"Shoes\", debug=True\n", + ")\n", + "print(f\"Open vocabulary attributes:\\n{attribute_json}\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "rJk--yXekwcQ" + }, + "outputs": [], + "source": [ + "# @title Example 5: Get product attribute json from an image using self-correcting prompt -- closed vocabulary\n", + "image_uri = \"IMAGE_URI\" # @param {type:\"string\"}\n", + "\n", + "# Closed vocabulary.\n", + "vocabulary = \"\"\"\n", + "{\"Pattern\": [\"Animal\", \"Letter\", \"Plaid\", \"Plain\", \"Polka Dot\", \"Quilted\", \"Striped\", \"Tie Dye\", \"Tropical\", \"Zebra\", \"Block\", \"Rainbow\", \"Floral\"], \"Toe\": [\"Almond Toe\", \"Cap Toe\", \"Closed Toe\", \"Peep Toe\", \"Point Toe\", \"Pointed Toe\", \"Round Toe\", \"Square Toe\", \"Toe Post\", \"Open Toe\"], \"Style\": [\"Ballet\", \"Bandage\", \"Basics\", \"Casual\", \"Classic\", \"Cute\", \"Elegant\", \"Formal\", \"Modern\", \"Motorcycle\", \"Retro\", \"Sexy\", \"Boho\", \"Modest\", \"Comfort\", \"Minimalist\"], \"Strap Type\": [\"Adjustable\", \"Ankle cuff\", \"Ankle straps\", \"Chain\", \"Convertible\", \"Criss Cross\", \"D'orsay\", \"Double Handle\", \"Flowers\", \"Gladiator\", \"Lace Up\", \"Mary Jane\", \"Ring\", \"Slingbacks\", \"Strappy\", \"T strap\", \"Zipper\", \"Elastic\", \"Velcro\", \"Ankle Strap\"], \"Heels\": [\"Chunky\", \"Cork\", \"Espadrilles\", \"Flat\", \"Flatform\", \"Platform\", \"Stiletto\", \"Cone Heel\", \"Kitten Heels\", \"Hidden Wedge\", \"Wedges\", \"Pyramid\"], \"Closure Type\": [\"Back Zipper\", \"Buckle\", \"Zipper\", \"Magnet\", \"Slip on\", \"Hook Loop\", \"Lace-up\", \"Flap\"]}\n", + "\"\"\"\n", + "attribute_json = product_agent.get_attributes_self_correcting_prompt(\n", + " image_uri, vocabulary_json=vocabulary, debug=True\n", + ")\n", + "print(f\"Closed vocabulary attributes\\n{attribute_json}\")" + ] + } + ], + "metadata": { + "colab": { + "name": "product_attributes_extraction.ipynb", + "toc_visible": true + }, + "kernelspec": { + "display_name": "Python 3", + "name": "python3" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} diff --git a/gemini/use-cases/retail/retail_ai_location_strategy_gemini_3.ipynb b/gemini/use-cases/retail/retail_ai_location_strategy_gemini_3.ipynb new file mode 100644 index 0000000..44d3276 --- /dev/null +++ b/gemini/use-cases/retail/retail_ai_location_strategy_gemini_3.ipynb @@ -0,0 +1,2031 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "copyright" + }, + "outputs": [], + "source": [ + "# Copyright 2025 Google LLC\n", + "#\n", + "# Licensed under the Apache License, Version 2.0 (the \"License\");\n", + "# you may not use this file except in compliance with the License.\n", + "# You may obtain a copy of the License at\n", + "#\n", + "# https://www.apache.org/licenses/LICENSE-2.0\n", + "#\n", + "# Unless required by applicable law or agreed to in writing, software\n", + "# distributed under the License is distributed on an \"AS IS\" BASIS,\n", + "# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n", + "# See the License for the specific language governing permissions and\n", + "# limitations under the License." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "title" + }, + "source": [ + "# Retail AI Location Strategy: Autonomous Site Selection & Market Analysis\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + "
    \n", + " \n", + " \"Google
    Open in Colab\n", + "
    \n", + "
    \n", + " \n", + " \"Google
    Open in Colab Enterprise\n", + "
    \n", + "
    \n", + " \n", + " \"Workbench
    Open in Workbench\n", + "
    \n", + "
    \n", + " \n", + " \"GitHub
    View on GitHub\n", + "
    \n", + "
    \n", + "\n", + "
    \n", + "\n", + "

    \n", + "Share to:\n", + "\n", + "\n", + " \"LinkedIn\n", + "\n", + "\n", + "\n", + " \"Bluesky\n", + "\n", + "\n", + "\n", + " \"X\n", + "\n", + "\n", + "\n", + " \"Reddit\n", + "\n", + "\n", + "\n", + " \"Facebook\n", + "\n", + "

    " + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "authors" + }, + "source": [ + "| Authors |\n", + "| --- |\n", + "| [Deepak Moonat](https://github.com/dmoonat) |\n", + "| [Lavi Nigam](https://github.com/lavinigam-gcp) |" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "overview" + }, + "source": [ + "## Overview\n", + "\n", + "**Gemini 3** is Google's latest flagship model family, trained to be especially proficient in:\n", + "\n", + "* **Advanced reasoning and complex instruction following** \n", + "* **Agentic operations and autonomous code execution** \n", + "* **Multimodal understanding across long contexts** (text, image, audio, video)\n", + "\n", + "\n", + "In this notebook, we will explore **Gemini 3**, capable of performing complex market research.\n", + "\n", + "### The Business Problem: The \"Synthesis Bottleneck\"\n", + "Opening a physical retail location (e.g., a cafĂ©, gym, or boutique) is a high-stakes investment often plagued by a lack of unified data.\n", + "* **The Data Fragmentation:** Demographics are on Wikipedia, competitors are on Google Search/Maps, rent trends are in news articles, and math needs to be done in spreadsheets.\n", + "* **The Latency:** Validating a single location typically takes an analyst 1-2 weeks of manual research.\n", + "* **The Risk:** Decisions are often made on \"gut feeling\" rather than data, leading to a high failure rate for new brick-and-mortar businesses.\n", + "\n", + "**The Solution:** An AI pipeline that unifies these disparate data sources into a coherent strategy in minutes, not weeks.\n", + "\n", + "### Real-World Scenario\n", + "Imagine you are the **Chief Strategy Officer** for a retail chain. You have identified a city (e.g., \"Bangalore\"), but you need to know the *exact* micro-market strategy.\n", + "\n", + "You need an intelligent assistant that can:\n", + "1. **Validate** if the market is growing (requires live web data).\n", + "2. **Locate** every competitor on a map (requires Places APIs).\n", + "3. **Calculate** saturation density (requires Python code).\n", + "4. **Recommend** a specific strategy based on the data (requires reasoning).\n", + "5. **Present** the findings to the board (requires coding HTML).\n", + "\n", + "\n", + "\n", + "---\n", + "\n", + "\n", + "### The Architecture: Why Gemini 3?\n", + "This notebook demonstrates how to chain five distinct capabilities of Gemini 3 to solve this problem. We aren't just asking the model to \"write text\"; we are asking it to use tools to interact with the real world.\n", + "\n", + "| Stage | The Challenge | The Gemini 3 Solution |\n", + "| :--- | :--- | :--- |\n", + "| **1. Research** | LLMs have a knowledge cutoff. They don't know today's rent trends. | **Search Grounding:** Allows the model to fetch live, verifiable data from the web. |\n", + "| **2A. Mapping** | LLMs hallucinate addresses and physical locations. | **Function Calling:** The model autonomously queries the **Google Maps Places API** for ground-truth data. |\n", + "| **2B. Analysis** | LLMs are poor at complex arithmetic and density calculations. | **Code Execution:** The model writes and runs **Python code** to perform precise data science on the fly. |\n", + "| **3. Strategy** | Raw data doesn't equal insight. We need \"Executive Reasoning.\" | **Thinking Mode:** Uses extended reasoning to weigh pros/cons and produce structured JSON decisions. |\n", + "| **4. Reporting** | Text is boring. Stakeholders need visual slides. | **Code Generation:** The model designs a high-fidelity, multi-slide **HTML/CSS report** from scratch. |\n", + "\n", + "---\n", + "\n", + "### System Flow\n", + "\n", + "![](https://storage.googleapis.com/github-repo/generative-ai/gemini3/assets/location_finder_01.png)\n", + "\n", + "\n", + "---\n", + "\n", + "### Learning Objectives\n", + "\n", + "By the end of this notebook, you will understand how to architect sophisticated setup using Gemini 3. Specifically, you will learn to:\n", + "\n", + "* **Architect Multi-stage Workflows:** Move beyond single-turn prompts to build a multi-stage pipeline where the model maintains context across research, analysis, and decision-making phases.\n", + "* **Orchestrate Multi-Modal Tools:** Apply the integration of **Search Grounding** (for fresh data), **Function Calling** (for Maps interaction), and **Code Execution** (for computation) within a single session.\n", + "* **Implement Hybrid Grounding:** Solve the \"hallucination problem\" by forcing the model to base its reasoning on retrieved real-world data (Google Maps) and verified web sources.\n", + "* **Leverage Extended Reasoning:** Use Gemini 3's advanced thinking capabilities to produce structured, strategic outputs (JSON) suitable for enterprise downstream applications.\n", + "* **Autonomous Asset Generation:** Direct the model to act as a full-stack developer, autonomously generating and styling professional HTML reports to visualize your findings." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "setup-header" + }, + "source": [ + "## Setup" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "prerequisites" + }, + "source": [ + "### Prerequisites\n", + "\n", + "**Required:**\n", + "- Python 3.9 or higher\n", + "- Google Cloud project with Vertex AI API enabled\n", + "- Google Maps API key (Places APIs)\n", + "- Completed [Intro to Gemini 3 Pro](https://github.com/GoogleCloudPlatform/generative-ai/blob/main/gemini/getting-started/intro_gemini_3_pro.ipynb) (L100 tutorial)\n", + "\n", + "**Helpful:**\n", + "- Familiarity with Jupyter notebooks\n", + "- Basic understanding of AI concepts" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "install-header" + }, + "source": [ + "### Installation" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "install-packages" + }, + "outputs": [], + "source": [ + "# Install required packages\n", + "%pip install --upgrade --quiet \\\n", + " google-genai \\\n", + " googlemaps" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "auth-header" + }, + "source": [ + "### Authenticate (Colab Only)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "authenticate" + }, + "outputs": [], + "source": [ + "import sys\n", + "\n", + "# Authenticate if running on Colab\n", + "if \"google.colab\" in sys.modules:\n", + " from google.colab import auth\n", + "\n", + " auth.authenticate_user()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "project-header" + }, + "source": [ + "### Set Up Google Cloud Project" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "id": "project-setup" + }, + "outputs": [], + "source": [ + "import os\n", + "\n", + "# Configure your Google Cloud project\n", + "# fmt: off\n", + "PROJECT_ID = \"[your-project-id]\" # @param {type: \"string\", placeholder: \"[your-project-id]\", isTemplate: true}\n", + "# fmt: on\n", + "\n", + "if not PROJECT_ID or PROJECT_ID == \"[your-project-id]\":\n", + " PROJECT_ID = str(os.environ.get(\"GOOGLE_CLOUD_PROJECT\"))\n", + "\n", + "if not PROJECT_ID:\n", + " raise ValueError(\"Please set PROJECT_ID to your Google Cloud project ID\")\n", + "\n", + "LOCATION = \"global\" # Gemini 3 is available globally\n", + "\n", + "print(f\"✅ Using project: {PROJECT_ID}\")\n", + "print(f\"✅ Using location: {LOCATION}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "maps-api-header" + }, + "source": [ + "### Set Up Google Maps API Key\n", + "\n", + "**First, enable the Maps Places API in your Google Cloud Console:**\n", + "[Maps Places API](https://console.cloud.google.com/apis/api/places-backend.googleapis.com/overview)\n", + "\n", + "\n", + "Once you've enable the relevant Google Maps APIs, you can [generate a Maps API key](https://developers.google.com/maps/documentation/geocoding/get-api-key)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "maps-api-key" + }, + "outputs": [], + "source": [ + "# For Colab: Store API key in Colab Secrets\n", + "# [Left panel, look for a key icon] Secrets → Add MAPS_API_KEY\n", + "\n", + "if \"google.colab\" in sys.modules:\n", + " from google.colab import userdata\n", + "\n", + " MAPS_API_KEY = userdata.get(\"MAPS_API_KEY\")\n", + "else:\n", + " # For local/production: Use environment variable\n", + " MAPS_API_KEY = os.environ.get(\"MAPS_API_KEY\")\n", + "\n", + "if not MAPS_API_KEY:\n", + " raise ValueError(\n", + " \"Please set MAPS_API_KEY:\\n\"\n", + " \" Colab: Add to Secrets (Settings → Secrets)\\n\"\n", + " \" Local: export MAPS_API_KEY=your_api_key\"\n", + " )\n", + "\n", + "print(\"✅ Maps API key configured\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "imports-header" + }, + "source": [ + "### Import Libraries" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "imports" + }, + "outputs": [], + "source": [ + "# Standard libraries\n", + "import datetime\n", + "import json\n", + "import os\n", + "import sys\n", + "import warnings\n", + "\n", + "# Suppress all warnings from the google.genai module\n", + "warnings.filterwarnings(\"ignore\", module=\"google.genai\")\n", + "\n", + "\n", + "# Display libraries\n", + "from IPython.display import HTML, Image, Markdown, display\n", + "\n", + "# Google Gen AI SDK\n", + "from google import genai\n", + "from google.genai import types\n", + "\n", + "# For structured output\n", + "from pydantic import BaseModel\n", + "\n", + "print(\"✅ All libraries imported successfully\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "client-header" + }, + "source": [ + "### Initialize Gemini Client" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "client-init" + }, + "outputs": [], + "source": [ + "# Initialize the Gemini client\n", + "client = genai.Client(vertexai=True, project=PROJECT_ID, location=LOCATION)\n", + "\n", + "# Set the model\n", + "MODEL_ID = \"gemini-3.1-pro-preview\"\n", + "\n", + "print(\"✅ Gemini client initialized\")\n", + "print(f\"✅ Using model: {MODEL_ID}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "config-header" + }, + "source": [ + "## Configure Your Analysis" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "config-set-location" + }, + "source": [ + "### Set Your Location and Business Type" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "config-code" + }, + "outputs": [], + "source": [ + "print(\"=\" * 70)\n", + "print(\"SMART LOCATION FINDER - CONFIGURATION\")\n", + "print(\"=\" * 70)\n", + "\n", + "# Configure your analysis parameters\n", + "# You can change these to analyze any location worldwide!\n", + "\n", + "# Target location (city or area)\n", + "# Examples:\n", + "# - \"Whitefield, Bangalore, India\"\n", + "# - \"Austin, Texas, USA\"\n", + "# - \"Shibuya, Tokyo, Japan\"\n", + "# - \"Shoreditch, London, UK\"\n", + "TARGET_LOCATION = \"KR Puram, Bangalore, India\" # @param {type: \"string\"}\n", + "\n", + "# Business type you want to open\n", + "# Examples:\n", + "# - \"specialty coffee shop\"\n", + "# - \"fitness studio\"\n", + "# - \"fast-casual restaurant\"\n", + "# - \"bookstore cafe\"\n", + "BUSINESS_TYPE = \"fitness studio\" # @param {type: \"string\"}\n", + "\n", + "# Display configuration\n", + "print(f\"\\n📍 Target Location: {TARGET_LOCATION}\")\n", + "print(f\"đŸȘ Business Type: {BUSINESS_TYPE}\")\n", + "\n", + "print(\"\\n\" + \"=\" * 70)\n", + "print(\"Configuration complete! Starting analysis...\")\n", + "print(\"=\" * 70)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "config-note" + }, + "source": [ + "**Note:** You can change these parameters to analyze any location in the world. The notebook will adapt all prompts and analysis to your specific inputs." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "part1-header" + }, + "source": [ + "## PART 1: Market Research" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "G5GiiJFIh1hb" + }, + "source": [ + "![](https://storage.googleapis.com/github-repo/generative-ai/gemini3/assets/location_finder_02.png)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "part1-goal" + }, + "source": [ + "### The Goal: \"Macro\" Viability Assessment\n", + "Referring back to our System Flow, we are starting at the **Data Gathering** phase. Before we zoom into specific streets or buildings, we must validate the **Macro-Economic Environment**.\n", + "\n", + "Our first objective is to answer: *\"Is this location economically viable for this specific business type right now?\"*\n", + "\n", + "### The Technical Challenge: The Knowledge Cutoff\n", + "If we asked a standard LLM, *\"What are the commercial rental trends in KR Puram?\"*, it relies on training data that might be 6–12 months old **(Gemini 3 has a knowledge cutoff date of Jan'2025)**. In the fast-moving retail world, old data is dangerous. It might miss:\n", + "* A new metro station that opened last month (increasing foot traffic).\n", + "* A recent spike in commercial rents.\n", + "* New demographic shifts (e.g., a new tech park bringing in young professionals).\n", + "\n", + "### The Solution: Gemini 3 with Search Grounding\n", + "To solve this, we activate **Search Grounding**. This connects the model to Google Search, allowing it to:\n", + "1. **Query** live web sources for real-time demographics and news.\n", + "2. **Retrieve** current economic indicators.\n", + "3. **Synthesize** disparate information (census data + news articles + real estate listings) into a coherent analyst report.\n", + "\n", + "**What we will do:**\n", + "We will construct a prompt that acts as a \"Market Research Analyst,\" specifically requesting data on **Demographics**, **Market Growth**, and **Commercial Viability**. Notice how the system instruction enforces factual, data-driven responses." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "part1-implementation" + }, + "outputs": [], + "source": [ + "# get current date\n", + "CURRENT_DATE = datetime.datetime.now().strftime(\"%Y-%m-%d\")\n", + "\n", + "print(\"=\" * 70)\n", + "print(\"PART 1: MARKET RESEARCH\")\n", + "print(\"=\" * 70)\n", + "\n", + "# Define system instruction\n", + "system_instruction = \"\"\"\n", + "You are a market research analyst specializing in retail location intelligence.\n", + "\n", + "Your task is to research and validate target markets for new business locations.\n", + "\n", + "Use Google Search to find:\n", + "- Current demographics (age, income, population)\n", + "- Market trends and growth patterns\n", + "- Industry-specific insights\n", + "- Commercial real estate trends\n", + "\n", + "Provide specific data points with sources. Be factual and data-driven.\n", + "\"\"\"\n", + "\n", + "# Define the research prompt\n", + "market_research_prompt = f\"\"\"\n", + "Research {TARGET_LOCATION} as a market for opening a {BUSINESS_TYPE}.\n", + "\n", + "Focus on:\n", + "\n", + "1. DEMOGRAPHICS:\n", + " - Age distribution (identify key age groups)\n", + " - Income levels and purchasing power\n", + " - Lifestyle indicators (professionals, students, families)\n", + " - Population density\n", + "\n", + "2. MARKET GROWTH:\n", + " - Population trends (growing, stable, declining)\n", + " - New residential and commercial developments\n", + " - Infrastructure improvements\n", + " - Economic growth indicators\n", + "\n", + "3. INDUSTRY PRESENCE:\n", + " - Existing similar businesses in the area\n", + " - Consumer preferences and spending patterns\n", + " - Market saturation or opportunity\n", + "\n", + "4. COMMERCIAL VIABILITY:\n", + " - Foot traffic patterns\n", + " - Commercial real estate trends\n", + " - Typical rental costs (qualitative: low/medium/high)\n", + " - Business environment\n", + "\n", + "Provide specific data points (percentages, trends, indicators) with sources.\n", + "Conclude with: Is this a strong market for {BUSINESS_TYPE}? Why or why not?\n", + "\n", + "Current date: {CURRENT_DATE}\n", + "\"\"\"\n", + "\n", + "# Enable Google Search grounding\n", + "search_tool = types.Tool(google_search=types.GoogleSearch())\n", + "\n", + "# Generate market research\n", + "print(f\"\\n🔍 Researching {TARGET_LOCATION} market for {BUSINESS_TYPE}...\")\n", + "\n", + "response = client.models.generate_content(\n", + " model=MODEL_ID,\n", + " contents=market_research_prompt,\n", + " config=types.GenerateContentConfig(\n", + " system_instruction=system_instruction, tools=[search_tool]\n", + " ),\n", + ")\n", + "\n", + "# Display the research findings\n", + "display(Markdown(\"## Market Research Findings\"))\n", + "display(Markdown(response.text))\n", + "\n", + "# Display search sources\n", + "if response.candidates[0].grounding_metadata:\n", + " display(Markdown(\"\\n## Sources\"))\n", + " display(\n", + " HTML(\n", + " response.candidates[\n", + " 0\n", + " ].grounding_metadata.search_entry_point.rendered_content\n", + " )\n", + " )\n", + "\n", + "# Save for later parts\n", + "market_research_findings = response.text\n", + "\n", + "print(\"\\n✅ Market research complete and saved for next parts\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "part1-takeaway" + }, + "source": [ + "### 🔍 Analysis of the Output\n", + "\n", + "**What just happened?**\n", + "The model executed a real-time research task, adeptly connecting unprocessed web information with meaningful strategic understanding.\n", + "\n", + "**Key Observations for the Architect:**\n", + "1. **Freshness of Data:** Notice the references to current infrastructure (e.g., specific metro lines, new housing developments) or current rental price ranges (e.g., \"â‚č40 – â‚č65 per sq. ft\"). This is data the base model likely did not have in its frozen weights.\n", + "2. **Synthesis:** It combined *Demographic Data* (Age/Income) with *Infrastructure Data* (Traffic/Metro) to form a *Business Conclusion* (e.g., \"Avoid the junction, target the residential interior\").\n", + "3. **The \"Go/No-Go\" Signal:** The report concludes with a validated \"YES/NO\" on market strength.\n", + "\n", + "**Connecting to the Next Step:**\n", + "We now have the **\"Why\"** (The market is strong because of X, Y, Z).\n", + "But we still lack the **\"Where\"**.\n", + "The report might say \"competition is high,\" but it doesn't tell us *exactly* where those competitors are located on a map. To make a strategic decision, we need precise coordinates, not just general trends.\n", + "\n", + "This leads us directly to **Part 2A**, where we will switch tools from *Search* to *Maps* to plot the physical battlefield." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "part2-header" + }, + "source": [ + "## PART 2A: Competitor Mapping" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "VE2MJEgCj9Rl" + }, + "source": [ + "![](https://storage.googleapis.com/github-repo/generative-ai/gemini3/assets/location_finder_03.png)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "part2-goal" + }, + "source": [ + "### The Goal: \"Micro\" Competitive Intelligence\n", + "In Part 1, we validated that the *macro* market is strong. Now, we zoom in. To open a successful physical business, you need to know exactly who you are fighting against.\n", + "We need to answer: *\"Where exactly are my competitors located, and are they any good?\"*\n", + "\n", + "### The Technical Challenge: The Hallucination Trap\n", + "If we simply asked an LLM, *\"List all gyms in KR Puram with their addresses,\"* it would likely invent plausible-sounding names (\"KR Puram Fitness Center\") or give outdated addresses.\n", + "**An LLM is a reasoning engine, not a database.** It does not have a live map of the world in its memory.\n", + "\n", + "### The Solution: Function Calling + Google Maps API\n", + "To get ground-truth data, we use **Function Calling**. This capability allows Gemini 3 to \"realize\" it needs external data and autonomously execute a programming function to get it.\n", + "\n", + "**The Architecture:**\n", + "1. **We provide a tool:** We give Gemini a Python function (`search_places`) that wraps the Google Maps Places API.\n", + "2. **Gemini acts as the agent:** We don't run the function ourselves. We just tell Gemini, \"Find the competitors.\"\n", + "3. **Autonomous Execution:** Gemini analyzes the prompt, understands it needs the tool, generates the correct API query (e.g., \"fitness studio in KR Puram\"), executes the call, and interprets the JSON result.\n", + "\n", + "**What we will do:**\n", + "We will first define the `search_places` function. Then, we will prompt Gemini to use this function to build a comprehensive **Competitor Map**, analyzing clustering patterns, quality gaps (ratings), and saturation levels." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "part2-step1-header" + }, + "source": [ + "### Step 1: Define the Search Function" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "part2-step1-code" + }, + "outputs": [], + "source": [ + "print(\"=\" * 70)\n", + "print(\"PART 2: COMPETITOR MAPPING\")\n", + "print(\"=\" * 70)\n", + "\n", + "\n", + "def search_places(query: str):\n", + " \"\"\"Search for places using the Google Maps Places API.\"\"\"\n", + " import googlemaps\n", + "\n", + " gmaps = googlemaps.Client(key=MAPS_API_KEY)\n", + " return gmaps.places(query)\n", + "\n", + "\n", + "# Test the function\n", + "print(f\"\\nđŸ§Ș Testing search function for {BUSINESS_TYPE} near {TARGET_LOCATION}...\")\n", + "test_query = f\"{BUSINESS_TYPE} near {TARGET_LOCATION}\"\n", + "test_result = search_places(query=test_query)\n", + "print(f\"✅ Function works! Found {len(test_result.get('results', []))} results\")\n", + "if test_result.get(\"results\"):\n", + " print(f\" Sample: {test_result['results'][0]['name']}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "part2-step2-header" + }, + "source": [ + "### Step 2: Use Function Calling with Gemini" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "q_VPLsCJxupg" + }, + "source": [ + "![](https://storage.googleapis.com/github-repo/generative-ai/gemini3/assets/location_finder_06.png)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "O0syu5aDqSoe" + }, + "outputs": [], + "source": [ + "# Define system instruction for competitive analysis\n", + "system_instruction = \"\"\"\n", + "You are a market intelligence analyst specializing in competitive landscape analysis.\n", + "\n", + "You have access to the search_places() function which uses Google Maps to find competing businesses in a specific area.\n", + "\n", + "When analyzing competition:\n", + "1. Use the function to get REAL data about existing competitors\n", + "2. Analyze the distribution (clustered vs spread out)\n", + "3. Identify patterns in location types (malls, main roads, residential areas)\n", + "4. Note gaps or underserved areas\n", + "5. Assess market saturation\n", + "\n", + "Be specific with the data you receive. Reference actual business names, locations, and ratings.\n", + "\"\"\"\n", + "\n", + "# Prompt for competitor analysis\n", + "competitor_prompt = f\"\"\"\n", + "Find all competitors for a {BUSINESS_TYPE} in {TARGET_LOCATION}.\n", + "\n", + "Use the search_places function to get real data from Google Maps.\n", + "\n", + "After getting the results:\n", + "1. List the competitors found (name, location, rating)\n", + "2. Identify any patterns:\n", + " - Are they clustered in specific areas/zones?\n", + " - What types of locations (shopping areas, main roads, residential)?\n", + "3. Categorize them if applicable:\n", + " - Premium vs budget\n", + " - High-rated (4.5+) vs others\n", + " - Chain vs independent\n", + "4. Initial assessment:\n", + " - Which areas seem saturated?\n", + " - Which areas might be underserved?\n", + "\n", + "Be specific and reference the actual data you receive.\n", + "\n", + "Current date: {CURRENT_DATE}\n", + "\"\"\"\n", + "\n", + "\n", + "# Generate content with function calling enabled\n", + "print(f\"\\nđŸ—ș Mapping competitors in {TARGET_LOCATION} using Maps Places API...\")\n", + "\n", + "# Send the prompt - model will automatically call the function\n", + "response = client.models.generate_content(\n", + " model=MODEL_ID,\n", + " contents=competitor_prompt,\n", + " config=types.GenerateContentConfig(\n", + " system_instruction=system_instruction, tools=[search_places]\n", + " ),\n", + ")\n", + "\n", + "# Display the analysis\n", + "display(Markdown(\"## Competitor Analysis\"))\n", + "display(Markdown(response.text))\n", + "\n", + "# Show what function was called\n", + "print(\"\\n📋 Function Call Details:\")\n", + "for turn in response.automatic_function_calling_history:\n", + " for part in turn.parts:\n", + " if part.function_call:\n", + " print(f\" Called: {part.function_call.name}\")\n", + " print(f\" Arguments: {part.function_call.args}\")\n", + "\n", + "# Save for later parts\n", + "competitor_analysis = response.text\n", + "\n", + "print(\"\\n✅ Competitor mapping complete and saved for next parts\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "welzwsnwGals" + }, + "source": [ + "### Recommended\n", + "\n", + "- Use `Grounding with Google Maps`, to ground your model's responses, enabling your AI applications to provide local data and geospatial context.\n", + "- Wrap the Google Maps as a tool, and provide it as a model config\n", + "- Check out the [documentation](https://docs.cloud.google.com/vertex-ai/generative-ai/docs/grounding/grounding-with-google-maps) for more details" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "Ti5Fi8lPGsHW" + }, + "source": [ + "```\n", + "# Enable Google Maps grounding\n", + "maps_tool = types.Tool(google_maps=types.GoogleMaps())\n", + "```\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "USqJlmmaKUqN" + }, + "source": [ + "**Note:** Use built-in Grounding with Google Maps for simple, conversational applications where you want to automatically add location awareness with minimal code ( source). Use Custom Tools for complex, agentic workflows that require precise control and structured data to pass between different analysis steps. So depending on the use-case one can make a choice." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "part2-takeaway" + }, + "source": [ + "### đŸ—ș Analysis of the Output\n", + "\n", + "**What just happened?**\n", + "This was a demonstration of **Automatic Function Calling**.\n", + "\n", + "1. **Autonomous Tool Use:** Look at the Function Call Details at the bottom of the output. You didn't have to write the specific query for the tool. The model generated the necessary arguments (e.g., combining the business type and location) dynamically based on your high-level instruction.\n", + "2. **Real-World Data:** The list of competitors and their details (e.g., names, ratings) represents real-world businesses with verifiable data. This is not just generated text; it is structured data retrieved from a reliable external source.\n", + "3. **Strategic Clustering:** The model didn't just provide a simple list; it analyzed geographical patterns in the data. It can identify and label different zones, such as 'Primary Clusters' where competition is dense, and 'Emerging Clusters' where there might be an opportunity. This requires **spatial reasoning capability**.\n", + "\n", + "**Connecting to the Next Step:**\n", + "We now have two critical pieces of the puzzle:\n", + "1. **Market Context (Part 1):** The area is growing and needs premium services.\n", + "2. **Competitor Map (Part 2A):** The main roads are saturated, but quality gaps exist.\n", + "\n", + "However, \"saturation\" is currently just a *feeling* based on looking at a list. To make a multimillion-dollar investment decision, we need **Math**, not feelings. We need to calculate density scores and quantify the opportunity.\n", + "Gemini is great at words, but can it do data science?\n", + "In **Part 2B**, we will hand these datasets to a **Code Execution** engine to perform rigorous gap analysis." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "part3-header" + }, + "source": [ + "## PART 2B: Gap Analysis with Code Execution" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "kxlemPB7leos" + }, + "source": [ + "![](https://storage.googleapis.com/github-repo/generative-ai/gemini3/assets/location_finder_03.png)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "part3-goal" + }, + "source": [ + "### The Goal: Quantitative \"White Space\" Analysis\n", + "We have a validated market (Part 1) and a list of competitors (Part 2A). A human analyst would now open Excel to calculate density, saturation, and projected revenue.\n", + "We need to answer: *\"Which specific zone offers the highest reward for the lowest risk?\"*\n", + "\n", + "### The Technical Challenge: The \"Math\" Problem\n", + "Large Language Models (LLMs) are designed for language, not logic. If you ask an LLM to \"calculate the saturation index of 5 zones based on weighted averages,\" it might give you a plausible-looking number that is mathematically wrong.\n", + "**For business strategy, we cannot afford \"hallucinated math.\"**\n", + "\n", + "### The Solution: Code Execution (Python)\n", + "Gemini 3 overcomes this by writing and executing **real Python code**. It doesn't guess the answer; it *computes* it.\n", + "\n", + "**The Architecture:**\n", + "1. **Data Ingestion:** We feed the unstructured text from Part 1 and Part 2B into the prompt.\n", + "2. **Code Generation:** Gemini writes a Python script using `pandas` and `numpy`. It structures the data into a DataFrame.\n", + "3. **Algorithmic Logic:** It implements complex custom metrics:\n", + " * **Saturation Index:** `(Competitors × Quality Score) / Demand Signal`\n", + " * **Viability Score:** A weighted ranking of all zones.\n", + "4. **Execution:** The code runs in a secure sandbox, and the *actual* output (the calculated table) is returned.\n", + "\n", + "**What we will do:**\n", + "We will instruct Gemini to act as a Data Scientist. It will parse our previous findings, define zones, assign scores, and calculate a \"Viability Matrix\" to objectively rank the best locations." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "m25syGnsyk4j" + }, + "source": [ + "![](https://storage.googleapis.com/github-repo/generative-ai/gemini3/assets/location_finder_07.png)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "part3-implementation" + }, + "outputs": [], + "source": [ + "print(\"=\" * 70)\n", + "print(\"PART 2B: GAP ANALYSIS\")\n", + "print(\"=\" * 70)\n", + "\n", + "# Enhanced prompt with context from Parts 1 & 2\n", + "gap_analysis_prompt = f\"\"\"\n", + "You have conducted comprehensive research. Here is the COMPLETE CONTEXT:\n", + "\n", + "**MARKET RESEARCH FINDINGS (Part 1):**\n", + "{market_research_findings}\n", + "\n", + "**COMPETITOR ANALYSIS (Part 2):**\n", + "{competitor_analysis}\n", + "\n", + "Based on this REAL data for {BUSINESS_TYPE} in {TARGET_LOCATION}, perform an ADVANCED gap analysis.\n", + "\n", + "Write Python code to:\n", + "\n", + "1. PARSE REAL COMPETITOR DATA FROM PART 2:\n", + " Extract ALL competitors mentioned with their:\n", + " - Names\n", + " - Zones/locations\n", + " - Ratings\n", + " - Type (Chain/Boutique/Quick-Service based on description)\n", + "\n", + "2. EXTRACT MARKET FUNDAMENTALS FROM PART 1:\n", + " - Total population\n", + " - Population density\n", + " - Average income level\n", + " - Rental cost tier (High/Medium/Low)\n", + " - Key infrastructure (Metro stations, malls, tech parks mentioned)\n", + " - Foot traffic patterns\n", + "\n", + "3. CALCULATE ADVANCED METRICS PER ZONE:\n", + " a) Basic metrics:\n", + " - Competitor count\n", + " - Competitor density (per sq km)\n", + " - Average competitor rating\n", + "\n", + " b) Quality metrics:\n", + " - Competition Quality Score: Weight competitors by rating (4.5+ rated = higher threat)\n", + " - Chain Dominance Ratio: Percentage of big chains (Starbucks, Third Wave, etc.)\n", + " - High-Performer Count: Competitors rated 4.5+\n", + "\n", + " c) Market opportunity metrics:\n", + " - Demand Signal: Score based on population density, income, infrastructure from Part 1\n", + " - Market Saturation Index: (Competitors × Quality Score) / Demand Signal\n", + " - Cost Tier: Estimated rental cost tier (High/Medium/Low based on Part 1)\n", + "\n", + " d) Strategic scores:\n", + " - Infrastructure Score: Proximity to Metro/malls/tech parks mentioned in Part 1\n", + " - Customer Stickiness: Residential zones (repeat customers) vs Transit zones\n", + " - First-Mover Advantage: New infrastructure with low competition\n", + "\n", + "4. CATEGORIZE ZONES WITH STRATEGIC INSIGHT:\n", + " Not just SATURATED/MODERATE/OPPORTUNITY, but also:\n", + " - Opportunity TYPE: \"Underserved Premium\", \"Metro First-Mover\", \"Residential Sticky\", \"Weekend Destination\"\n", + " - Risk Level: Based on competition quality and chain dominance (Low/Medium/High)\n", + " - Investment Tier: Based on rental costs from Part 1 (High/Medium/Low)\n", + " - Best Customer Segment: Weekday corporate, weekend residential, WFH crowd, etc.\n", + "\n", + "5. RANK TOP 3 ZONES:\n", + " Use multi-factor scoring:\n", + " - Low competition density\n", + " - Low market saturation\n", + " - High demand signals (income, population, foot traffic)\n", + " - Strategic advantages (Metro proximity, low chain dominance)\n", + " - Manageable costs\n", + "\n", + "6. CREATE COMPREHENSIVE TABLE:\n", + " Show all zones with: competitors, density, quality score, saturation index,\n", + " opportunity type, risk level, investment tier, recommended customer segment\n", + "\n", + "Execute the code and provide ACTIONABLE, STRATEGIC recommendations.\n", + "Be specific about WHY each zone is an opportunity or risk.\n", + "\n", + "Current date: {CURRENT_DATE}\n", + "\"\"\"\n", + "\n", + "# Add code execution tool\n", + "code_execution_tool = types.Tool(code_execution=types.ToolCodeExecution())\n", + "\n", + "# Generate analysis with code execution\n", + "print(\n", + " f\"\\n📊 Analyzing competitor density and market opportunities in {TARGET_LOCATION}...\"\n", + ")\n", + "print(\" Using real data from Parts 1 & 2 for comprehensive analysis...\")\n", + "\n", + "response = client.models.generate_content(\n", + " model=MODEL_ID,\n", + " contents=gap_analysis_prompt,\n", + " config=types.GenerateContentConfig(tools=[code_execution_tool]),\n", + ")\n", + "\n", + "# Display the analysis\n", + "display(Markdown(\"## Gap Analysis\"))\n", + "display(Markdown(response.text))\n", + "\n", + "# Display generated code if available\n", + "if response.executable_code:\n", + " display(Markdown(\"\\n### Generated Python Code\"))\n", + " display(Markdown(f\"```python\\n{response.executable_code}\\n```\"))\n", + "\n", + " display(Markdown(\"\\n### Code Output\"))\n", + " display(Markdown(f\"```\\n{response.code_execution_result}\\n```\"))\n", + "\n", + "# Save for Part 4\n", + "gap_analysis = response.text\n", + "\n", + "print(\"\\n✅ Gap analysis complete and saved for final recommendation\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "part3-takeaway" + }, + "source": [ + "### 📊 Analysis of the Output\n", + "\n", + "**What just happened?**\n", + "This is the most powerful demonstration of **Neuro-Symbolic AI** (combining neural networks with symbolic logic/code).\n", + "\n", + "**Key Observations:**\n", + "1. **Structured Thinking:** Look at the **Strategic Zone Ranking Table** in the output. Gemini autonomously created columns for *Risk Level*, *Investment Tier*, and *Viability Score*. It didn't just list zones; it created a quantitative model to evaluate and compare them.\n", + "2. The \"Blue Ocean\" Finding: This analysis can uncover \"Blue Ocean\" opportunities—areas with high demand but low competition. The generated Python code can identify a top-ranked zone by finding a high 'Demand' score coupled with a low number of 'Competitors'. In contrast, it can also identify high-risk \"Red Ocean\" zones where, despite high demand, the market is oversaturated.\n", + "3. **Algorithmic Reasoning:** The model demonstrates algorithmic reasoning by writing Python logic to handle specific edge cases (e.g., correctly calculating saturation if there are zero competitors). This level of conditional logic and mathematical precision is difficult to achieve with pure text-based prompting but is handled reliably by the Code Execution tool.\n", + "\n", + "**Connecting to the Next Step:**\n", + "We now have the Numbers: The analysis identifies the best location mathematically. But business decisions aren't made on numbers alone; they require Nuance.\n", + "\n", + "The next logical step is to explore the qualitative aspects of the top-ranked location, such as:\n", + "\n", + "\"What is the specific customer persona for this area?\"\n", + "\"What are the specific risks of opening a business here?\"\n", + "\"What is a viable mitigation strategy for those risks?\"\n", + "\n", + "We need to synthesize these cold, hard numbers into a persuasive **Strategic Narrative**.\n", + "In **Part 3**, we will switch from \"Calculator Mode\" (Code Execution) to \"Deep Thinking Mode\" (Extended Reasoning) to build the final business case." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "part4-header" + }, + "source": [ + "## PART 3: Final Recommendation with Extended Reasoning" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "nkPK7wnnmZMy" + }, + "source": [ + "![](https://storage.googleapis.com/github-repo/generative-ai/gemini3/assets/location_finder_04.png)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "part4-goal" + }, + "source": [ + "### The Goal: The \"CEO Moment\"\n", + "We have gathered intelligence (Part 1), mapped the battlefield (Part 2A), and crunched the numbers (Part 2B).\n", + "Now, we must answer the ultimate question: **\"So, what is the single best move?\"**\n", + "\n", + "This is not about listing facts anymore. It is about **Strategic Synthesis**. We need an AI that can weigh conflicting evidence (e.g., \"High demand but high rent\" vs. \"Low rent but low visibility\") and make a definitive, justified recommendation.\n", + "\n", + "### The Technical Challenge: Complexity & consistency\n", + "1. **Hallucination Risk:** In complex reasoning tasks, models often \"forget\" the specific numbers calculated earlier (e.g., recommending a zone that Part 2 proved was saturated).\n", + "2. **Downstream Automation:** We need the output to be computer-readable (JSON) so we can generate a website in Part 4. Generating code directly from large blocks of unorganized text is not feasible.\n", + "\n", + "### The Solution: Extended Reasoning & Structured Output\n", + "We leverage two flagship capabilities of Gemini 3:\n", + "1. **Extended Reasoning (Thinking Mode):** We enable `thinking_level=\"HIGH\"`. This forces the model to pause and \"think\" (generate an internal chain-of-thought) before answering. It critiques its own logic, checks against the Part 3 math, and formulates a robust argument.\n", + "2. **Structured Output (Pydantic):** We define a strict Python schema (`LocationIntelligenceReport`). The model *must* return data that perfectly matches this structure, ensuring reliable JSON for the next steps.\n", + "\n", + "**What we will do:**\n", + "1. **Define the Schema:** Create complex Pydantic classes to model our ideal report.\n", + "2. **Synthesize:** Feed *all* previous data (Parts 1-2) into the model.\n", + "3. **Reason & Decide:** Watch the model \"think\" through the options and output a final, structured strategic plan." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "B1xy4npcTJ5q" + }, + "source": [ + "![](https://storage.googleapis.com/github-repo/generative-ai/gemini3/assets/location_finder_08.png)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "part4-step1-header" + }, + "source": [ + "### Step 1: Define the Output Structure (Schema)\n", + "To ensure our AI consultant produces a report we can *use* programmatically, we define a \"Contract\" using **Pydantic**.\n", + "* Instead of saying \"Give me a report,\" we say \"Fill out this exact form.\"\n", + "* This ensures we get specific fields like `overall_score` (integer), `chain_dominance_pct` (float), and `next_steps` (list of strings)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "part4-step1-code" + }, + "outputs": [], + "source": [ + "print(\"=\" * 70)\n", + "print(\"PART 3: FINAL RECOMMENDATION\")\n", + "print(\"=\" * 70)\n", + "\n", + "# Define structured output schemas with nested complexity\n", + "\n", + "\n", + "class StrengthAnalysis(BaseModel):\n", + " \"\"\"Detailed strength with evidence.\"\"\"\n", + "\n", + " factor: str\n", + " description: str\n", + " evidence_from_analysis: str\n", + "\n", + "\n", + "class ConcernAnalysis(BaseModel):\n", + " \"\"\"Detailed concern with mitigation.\"\"\"\n", + "\n", + " risk: str\n", + " description: str\n", + " mitigation_strategy: str\n", + "\n", + "\n", + "class CompetitionProfile(BaseModel):\n", + " \"\"\"Competition characteristics in the zone.\"\"\"\n", + "\n", + " total_competitors: int\n", + " density_per_km2: float\n", + " chain_dominance_pct: float\n", + " avg_competitor_rating: float\n", + " high_performers_count: int\n", + "\n", + "\n", + "class MarketCharacteristics(BaseModel):\n", + " \"\"\"Market fundamentals for the zone.\"\"\"\n", + "\n", + " population_density: str\n", + " income_level: str\n", + " infrastructure_access: str\n", + " foot_traffic_pattern: str\n", + " rental_cost_tier: str\n", + "\n", + "\n", + "class LocationRecommendation(BaseModel):\n", + " \"\"\"Complete recommendation for a specific location.\"\"\"\n", + "\n", + " location_name: str\n", + " area: str\n", + " overall_score: int\n", + " opportunity_type: str\n", + " strengths: list[StrengthAnalysis]\n", + " concerns: list[ConcernAnalysis]\n", + " competition: CompetitionProfile\n", + " market: MarketCharacteristics\n", + " best_customer_segment: str\n", + " estimated_foot_traffic: str\n", + " next_steps: list[str]\n", + "\n", + "\n", + "class AlternativeLocation(BaseModel):\n", + " \"\"\"Brief summary of alternative location.\"\"\"\n", + "\n", + " location_name: str\n", + " area: str\n", + " overall_score: int\n", + " opportunity_type: str\n", + " key_strength: str\n", + " key_concern: str\n", + " why_not_top: str\n", + "\n", + "\n", + "class LocationIntelligenceReport(BaseModel):\n", + " \"\"\"Complete location intelligence analysis report.\"\"\"\n", + "\n", + " target_location: str\n", + " business_type: str\n", + " analysis_date: str\n", + " market_validation: str\n", + " total_competitors_found: int\n", + " zones_analyzed: int\n", + " top_recommendation: LocationRecommendation\n", + " alternative_locations: list[AlternativeLocation]\n", + " key_insights: list[str]\n", + " methodology_summary: str\n", + "\n", + "\n", + "print(\"✅ Complex nested Pydantic schemas defined\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "part4-step2-header" + }, + "source": [ + "### Step 2: Generate with Deep Reasoning\n", + "Now we invoke Gemini 3. Notice the configuration:\n", + "* `thinking_level=types.ThinkingLevel.HIGH`: Activates the extended reasoning engine.\n", + "* `response_schema=LocationIntelligenceReport`: Enforces the Pydantic structure defined above.\n", + "\n", + "**Watch the \"Internal Reasoning\":**\n", + "When the output appears, look for the **🧠 Internal Reasoning** section. You will see the thinking logic." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "part4-step2-code" + }, + "outputs": [], + "source": [ + "# Final synthesis prompt with all context\n", + "final_recommendation_prompt = f\"\"\"\n", + "Create a comprehensive location intelligence report for opening a {BUSINESS_TYPE} in {TARGET_LOCATION}.\n", + "\n", + "SYNTHESIZE ALL DATA FROM PREVIOUS ANALYSIS:\n", + "\n", + "**PART 1 - MARKET RESEARCH:**\n", + "{market_research_findings}\n", + "\n", + "**PART 2 - COMPETITOR MAPPING:**\n", + "{competitor_analysis}\n", + "\n", + "**PART 3 - GAP ANALYSIS:**\n", + "{gap_analysis}\n", + "\n", + "TASK:\n", + "\n", + "Analyze ALL the data above and provide a complete recommendation with:\n", + "\n", + "1. TOP RECOMMENDATION:\n", + " - Identify the SINGLE BEST location/zone\n", + " - Score it 0-100 based on comprehensive analysis\n", + " - Classify opportunity type (e.g., \"Metro First-Mover\", \"Residential Sticky\", \"Underserved Premium\")\n", + " - Provide 3-4 detailed STRENGTHS with specific evidence from the analysis\n", + " - Provide 2-3 CONCERNS with mitigation strategies\n", + " - Detailed competition profile (density, chain dominance, ratings)\n", + " - Market characteristics (population, income, infrastructure, traffic, rent)\n", + " - Best customer segment to target\n", + " - Foot traffic estimate\n", + " - 3-5 actionable next steps\n", + "\n", + "2. TWO ALTERNATIVE LOCATIONS:\n", + " - Second and third best options\n", + " - Score each 0-100\n", + " - Opportunity type for each\n", + " - Key strength and concern for each\n", + " - Explain WHY each is NOT the top choice\n", + "\n", + "3. KEY INSIGHTS:\n", + " - 3-5 high-level strategic insights from the entire analysis\n", + "\n", + "4. METHODOLOGY:\n", + " - Brief summary of how this analysis was performed\n", + "\n", + "Focus on:\n", + "- Optimal balance of low competition and strong market fundamentals\n", + "- Clear competitive advantage potential\n", + "- Sustainable business viability\n", + "- Risk-adjusted opportunity assessment\n", + "\n", + "Be SPECIFIC. Reference actual data from the analysis. Show your reasoning.\n", + "Return the complete report in structured JSON format matching the schema.\n", + "\n", + "Current date: {CURRENT_DATE}\n", + "\"\"\"\n", + "\n", + "# Enable extended reasoning with HIGH thinking level\n", + "print(\n", + " f\"\\n🧠 Generating final recommendation for {BUSINESS_TYPE} in {TARGET_LOCATION}...\"\n", + ")\n", + "print(\" Using DEEP REASONING mode for comprehensive analysis...\")\n", + "\n", + "response = client.models.generate_content(\n", + " model=MODEL_ID,\n", + " contents=final_recommendation_prompt,\n", + " config=types.GenerateContentConfig(\n", + " thinking_config=types.ThinkingConfig(\n", + " thinking_level=types.ThinkingLevel.HIGH, # Deep reasoning\n", + " include_thoughts=True, # Show thought process\n", + " ),\n", + " response_mime_type=\"application/json\",\n", + " response_schema=LocationIntelligenceReport,\n", + " ),\n", + ")\n", + "\n", + "# Display the reasoning process\n", + "print(\"\\n\" + \"=\" * 70)\n", + "print(\"MODEL'S REASONING PROCESS\")\n", + "print(\"=\" * 70)\n", + "\n", + "for part in response.candidates[0].content.parts:\n", + " if part.thought:\n", + " display(Markdown(\"### 🧠 Internal Reasoning\"))\n", + " display(Markdown(part.text))\n", + " print(\"\\n\" + \"=\" * 70)\n", + "\n", + "# Parse the structured output\n", + "report = response.parsed\n", + "\n", + "print(\"\\n\" + \"=\" * 70)\n", + "print(\"FINAL RECOMMENDATION\")\n", + "print(\"=\" * 70)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "part4-step3-header" + }, + "source": [ + "### Step 3: The Strategic Decision\n", + "Below is the parsed result. Notice how the raw JSON has been rendered into a clean, human-readable format.\n", + "\n", + "**Key Output Features:**\n", + "* **Top Recommendation:** A single, definitive choice with a calculated score.\n", + "* **Evidence-Based:** Every strength cites specific data from previous parts.\n", + "* **Actionable:** It doesn't just analyze; it prescribes." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "GZIupAwBCQc9" + }, + "outputs": [], + "source": [ + "comp = report.top_recommendation.competition\n", + "market = report.top_recommendation.market\n", + "\n", + "strengths_md = \"\"\n", + "for i, strength in enumerate(report.top_recommendation.strengths, 1):\n", + " strengths_md += f\"\"\"**{i}. {strength.factor}**\n", + " - {strength.description}\n", + " - *Evidence:* {strength.evidence_from_analysis}\\n\n", + "\"\"\"\n", + "\n", + "concerns_md = \"\"\n", + "for i, concern in enumerate(report.top_recommendation.concerns, 1):\n", + " concerns_md += f\"\"\"**{i}. {concern.risk}**\n", + " - {concern.description}\n", + " - *Mitigation:* {concern.mitigation_strategy}\\n\n", + "\"\"\"\n", + "\n", + "alternatives_md = \"\"\n", + "for alt in report.alternative_locations:\n", + " alternatives_md += f\"\"\"\n", + "### {alt.location_name} ({alt.area})\n", + "- **Score:** {alt.overall_score}/100\n", + "- **Type:** {alt.opportunity_type}\n", + "- **Strength:** {alt.key_strength}\n", + "- **Concern:** {alt.key_concern}\n", + "- **Why Not Top:** {alt.why_not_top}\n", + "\"\"\"\n", + "\n", + "next_steps_md = \"\"\n", + "for i, step in enumerate(report.top_recommendation.next_steps, 1):\n", + " next_steps_md += f\"{i}. {step}\\n\"\n", + "\n", + "key_insights_md = \"\"\n", + "for insight in report.key_insights:\n", + " key_insights_md += f\"- {insight}\\n\"\n", + "\n", + "markdown_content = f\"\"\"\n", + "## Executive Summary\n", + "**Location:** {report.target_location}\n", + "**Business:** {report.business_type}\n", + "**Analysis Date:** {report.analysis_date}\n", + "\n", + "**Market Validation:** {report.market_validation}\n", + "**Analysis Scope:** {report.total_competitors_found} competitors analyzed across {report.zones_analyzed} zones\n", + "\n", + "## 🏆 Top Recommendation\n", + "### {report.top_recommendation.location_name}\n", + "**Area:** {report.top_recommendation.area}\n", + "**Overall Score:** {report.top_recommendation.overall_score}/100\n", + "**Opportunity Type:** {report.top_recommendation.opportunity_type}\n", + "**Target Segment:** {report.top_recommendation.best_customer_segment}\n", + "\n", + "#### Strengths:\n", + "{strengths_md}\n", + "#### Concerns:\n", + "{concerns_md}\n", + "#### Competition Profile:\n", + "- **Total Competitors:** {comp.total_competitors}\n", + "- **Density:** {comp.density_per_km2:.2f} competitors/kmÂČ\n", + "- **Chain Dominance:** {comp.chain_dominance_pct:.1f}%\n", + "- **Avg Rating:** {comp.avg_competitor_rating:.2f} ⭐\n", + "- **High Performers (4.5+):** {comp.high_performers_count}\n", + "\n", + "#### Market Characteristics:\n", + "- **Population Density:** {market.population_density}\n", + "- **Income Level:** {market.income_level}\n", + "- **Infrastructure:** {market.infrastructure_access}\n", + "- **Foot Traffic:** {market.foot_traffic_pattern}\n", + "- **Rental Costs:** {market.rental_cost_tier}\n", + "\n", + "**Estimated Foot Traffic:** {report.top_recommendation.estimated_foot_traffic}\n", + "\n", + "## Alternative Locations\n", + "{alternatives_md}\n", + "\n", + "## 🎯 Next Steps\n", + "{next_steps_md}\n", + "\n", + "## 💡 Key Insights\n", + "{key_insights_md}\n", + "\n", + "## Methodology\n", + "{report.methodology_summary}\n", + "\"\"\"\n", + "\n", + "# Display the entire report in one go\n", + "display(Markdown(markdown_content))\n", + "\n", + "# Save as JSON\n", + "with open(\"location_intelligence_report.json\", \"w\") as f:\n", + " json.dump(report.model_dump(), f, indent=2)\n", + "\n", + "print(\"\\n✅ Report saved to location_intelligence_report.json\")\n", + "print(\"✅ Final recommendation complete\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "part4-takeaway" + }, + "source": [ + "### 🧠 Analysis of the Reasoning\n", + "\n", + "**What just happened?**\n", + "The model successfully synthesized qualitative *Market Trends* (from Part 1), geospatial *Competitor Locations* (from Part 2A), and quantitative *Saturation Metrics* (from Part 2B) into a cohesive business strategy.\n", + "\n", + "**The \"Aha!\" Moment: The Power of Synthesis**\n", + "\n", + "This is where the model connects all the dots from the previous steps. For example, it might connect:\n", + "\n", + "- A demographic trend from **Part 1** (e.g., a growing population of young professionals in a specific area).\n", + "- A geographical gap from **Part 2A** (e.g., a lack of a certain type of business in that same neighborhood).\n", + "- A quantitative opportunity from **Part 2B** (e.g., a low market saturation score calculated for that area).\n", + "- **Part 3 (Here)** then synthesizes these individual data points into a single, actionable recommendation (e.g., \"Open a premium version of the business in that neighborhood to cater to the underserved young professional demographic\").\n", + "\n", + "This is **Strategic Intelligence** in action. The model doesn't just summarize the data; it identifies a market gap that a human analyst might have missed while looking at disconnected spreadsheets and reports.\n", + "\n", + "**Connecting to the Final Step:**\n", + "We now have the *Strategy* in a structured JSON format. But a JSON file isn't something you present to a Board of Directors. They want a **Presentation/Slide Deck**.\n", + "In the final section (**Part 4**), we will ask Gemini to act as a \"Frontend Developer\" and autonomously convert this JSON data into a stunning, interactive HTML presentation." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "25a2ee9b" + }, + "source": [ + "## PART 4: Executive Report Generation" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "RJSffc9nuwZz" + }, + "source": [ + "![](https://storage.googleapis.com/github-repo/generative-ai/gemini3/assets/location_finder_05.png)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "f4ab911f" + }, + "source": [ + "### The Goal: From \"Raw Data\" to \"Client Deliverable\"\n", + "We have successfully identified the perfect location (*Seegehalli*) and backed it up with math and strategy. But a JSON file is not a deliverable. You cannot present a JSON object to a Board of Directors or an Investor.\n", + "We need a **Professional Slide Deck**.\n", + "\n", + "### The Technical Challenge: The \"Last Mile\" Problem\n", + "Typically, turning data into a presentation is a manual, tedious process:\n", + "1. Copy-paste data from Excel to PowerPoint.\n", + "2. Manually adjust fonts, colors, and layout.\n", + "3. Repeat every time the data changes.\n", + "\n", + "### The Solution: Code Generation (HTML/CSS)\n", + "We treat the \"Presentation\" as a \"Software Problem.\" We ask Gemini 3 to act as a **Frontend Developer + UI Designer**.\n", + "Instead of using a drag-and-drop tool, we instruct the model to **write the code** for a fully interactive, self-contained HTML website that acts as a slide deck.\n", + "\n", + "**The Architecture:**\n", + "1. **Data Aggregation:** We collect the \"Top Recommendation,\" \"Competitor Stats,\" and \"Market Metrics\" from our previous steps.\n", + "2. **Design Prompting:** We give Gemini a \"Design System\" prompt (e.g., \"Use a McKinsey/BCG aesthetic,\" \"Navy Blue color palette,\" \"Card-based layout\").\n", + "3. **Autonomous Coding:** Gemini writes the full HTML structure and embedded CSS styles from scratch.\n", + "\n", + "**What we will do:**\n", + "We will execute a single prompt that transforms our raw analysis into a polished, 7-slide executive presentation—instantly." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "b8f1c4a2" + }, + "source": [ + "### Step 1: Format the Context\n", + "\n", + "We gather the specific insights generated in Parts 1 through 3—the \"Top Recommendation,\" the \"Viability Score,\" the \"Competitor Counts\"—and bundle them into a clean context object. This ensures the report is populated with our actual findings, not placeholder text.\n", + "\n", + "We structure this data into a clear, readable format for the model. Think of this as writing a creative brief for a designer: *\"Here are the facts. Now make them look good.\"*" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "zErXoZDRHLoY" + }, + "outputs": [], + "source": [ + "# @title\n", + "print(\"=\" * 70)\n", + "print(\"PART 4: EXECUTIVE REPORT GENERATION\")\n", + "print(\"=\" * 70)\n", + "\n", + "# Create comprehensive structured data for HTML generation\n", + "print(\"\\n📋 Formatting comprehensive data context for HTML generation...\")\n", + "\n", + "# Generate the dynamic parts of the report using list comprehensions, accessing the report object directly.\n", + "strengths_section = \"\\n\".join(\n", + " [\n", + " f\"{i}. {s.factor}\\n Description: {s.description}\\n Evidence: {s.evidence_from_analysis}\"\n", + " for i, s in enumerate(report.top_recommendation.strengths, 1)\n", + " ]\n", + ")\n", + "\n", + "concerns_section = \"\\n\".join(\n", + " [\n", + " f\"{i}. Risk: {c.risk}\\n Description: {c.description}\\n Mitigation: {c.mitigation_strategy}\"\n", + " for i, c in enumerate(report.top_recommendation.concerns, 1)\n", + " ]\n", + ")\n", + "\n", + "alternatives_section = \"\\n\\n\".join(\n", + " [\n", + " f\"ALTERNATIVE {i}: {alt.location_name}\\nArea: {alt.area}\\nScore: {alt.overall_score}/100\\nOpportunity Type: {alt.opportunity_type}\\nKey Strength: {alt.key_strength}\\nKey Concern: {alt.key_concern}\\nWhy Not Top Choice: {alt.why_not_top}\"\n", + " for i, alt in enumerate(report.alternative_locations, 1)\n", + " ]\n", + ")\n", + "\n", + "next_steps_section = \"\\n\".join(\n", + " [f\"{i}. {step}\" for i, step in enumerate(report.top_recommendation.next_steps, 1)]\n", + ")\n", + "\n", + "key_insights_section = \"\\n\".join(\n", + " [f\"{i}. {insight}\" for i, insight in enumerate(report.key_insights, 1)]\n", + ")\n", + "\n", + "# For readability in the f-string, create a few aliases for nested objects\n", + "top_rec = report.top_recommendation\n", + "comp = top_rec.competition\n", + "market = top_rec.market\n", + "\n", + "# Build the final data_context string using a single f-string, accessing the report object directly\n", + "data_context = f\"\"\"\n", + "LOCATION INTELLIGENCE REPORT - COMPREHENSIVE DATA\n", + "{\"=\" * 80}\n", + "\n", + "ANALYSIS OVERVIEW:\n", + "- Target Location: {report.target_location}\n", + "- Business Type: {report.business_type}\n", + "- Analysis Date: {report.analysis_date}\n", + "- Market Validation: {report.market_validation}\n", + "- Total Competitors Found: {report.total_competitors_found}\n", + "- Zones Analyzed: {report.zones_analyzed}\n", + "\n", + "{\"=\" * 80}\n", + "TOP RECOMMENDATION (PRIMARY - SHOW PROMINENTLY ON SLIDE 1)\n", + "{\"=\" * 80}\n", + "\n", + "Location: {top_rec.location_name}\n", + "Area: {top_rec.area}\n", + "Overall Score: {top_rec.overall_score}/100\n", + "Opportunity Type: {top_rec.opportunity_type}\n", + "Target Customer Segment: {top_rec.best_customer_segment}\n", + "Estimated Foot Traffic: {top_rec.estimated_foot_traffic}\n", + "\n", + "KEY STRENGTHS:\n", + "{strengths_section}\n", + "\n", + "CONCERNS & MITIGATION:\n", + "{concerns_section}\n", + "\n", + "{\"=\" * 80}\n", + "COMPETITION ANALYSIS\n", + "{\"=\" * 80}\n", + "\n", + "Total Competitors in Zone: {comp.total_competitors}\n", + "Competitor Density: {comp.density_per_km2:.2f} competitors/kmÂČ\n", + "Chain Dominance: {comp.chain_dominance_pct:.1f}%\n", + "Average Competitor Rating: {comp.avg_competitor_rating:.2f} ⭐\n", + "High-Performing Competitors (4.5+ rating): {comp.high_performers_count}\n", + "\n", + "{\"=\" * 80}\n", + "MARKET CHARACTERISTICS\n", + "{\"=\" * 80}\n", + "\n", + "Population Density: {market.population_density}\n", + "Income Level: {market.income_level}\n", + "Infrastructure Access: {market.infrastructure_access}\n", + "Foot Traffic Pattern: {market.foot_traffic_pattern}\n", + "Rental Cost Tier: {market.rental_cost_tier}\n", + "\n", + "{\"=\" * 80}\n", + "ALTERNATIVE LOCATIONS\n", + "{\"=\" * 80}\n", + "\n", + "{alternatives_section}\n", + "\n", + "{\"=\" * 80}\n", + "NEXT STEPS (ACTIONABLE)\n", + "{\"=\" * 80}\n", + "\n", + "{next_steps_section}\n", + "\n", + "{\"=\" * 80}\n", + "KEY STRATEGIC INSIGHTS\n", + "{\"=\" * 80}\n", + "\n", + "{key_insights_section}\n", + "\n", + "{\"=\" * 80}\n", + "METHODOLOGY\n", + "{\"=\" * 80}\n", + "\n", + "{report.methodology_summary}\n", + "\n", + "{\"=\" * 80}\n", + "\"\"\"\n", + "\n", + "print(\"✅ Comprehensive data context formatted\")\n", + "print(f\" ‱ Total context length: {len(data_context):,} characters\")\n", + "print(\n", + " \" ‱ Includes: Executive summary, top recommendation, competition, market, alternatives, insights, next steps\"\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "3f8a1d5c" + }, + "source": [ + "### Step 2: The \"Designer\" Prompt\n", + "Now comes the magic. We prompt Gemini 3 with a dual role:\n", + "1. **The Developer:** \"Write valid, bug-free HTML5 and CSS3.\"\n", + "2. **The Designer:** \"Use a professional 'Consulting' aesthetic. Use shadows for depth. Use a grid for layout.\"\n", + "\n", + "Notice that we don't write a single line of HTML ourselves. The model is responsible for the entire DOM structure and Styling." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "7c3e8b9f" + }, + "outputs": [], + "source": [ + "# @title\n", + "# Comprehensive prompt for multi-slide HTML generation\n", + "html_generation_prompt = f\"\"\"\n", + "Generate a comprehensive, professional HTML report for a location intelligence analysis.\n", + "\n", + "This report should be in the style of McKinsey/BCG consulting presentations:\n", + "- Multi-slide format using full-screen scrollable sections\n", + "- Modern, clean, executive-ready design\n", + "- Data-driven visualizations\n", + "- Professional color scheme and typography\n", + "\n", + "CRITICAL REQUIREMENTS:\n", + "\n", + "1. STRUCTURE - Create 7 distinct slides (full-screen sections):\n", + "\n", + " SLIDE 1 - EXECUTIVE SUMMARY & TOP RECOMMENDATION\n", + " - Large, prominent display of recommended location and score\n", + " - Business type and target location\n", + " - High-level market validation\n", + " - Eye-catching hero section\n", + "\n", + " SLIDE 2 - TOP RECOMMENDATION DETAILS\n", + " - All strengths with evidence (cards/boxes)\n", + " - All concerns with mitigation strategies\n", + " - Opportunity type and target customer segment\n", + "\n", + " SLIDE 3 - COMPETITION ANALYSIS\n", + " - Competition metrics (total competitors, density, chain dominance)\n", + " - Visual representation of key numbers (large stat boxes)\n", + " - Average ratings, high performers count\n", + "\n", + " SLIDE 4 - MARKET CHARACTERISTICS\n", + " - Population density, income level, infrastructure\n", + " - Foot traffic patterns, rental costs\n", + " - Grid/card layout for each characteristic\n", + "\n", + " SLIDE 5 - ALTERNATIVE LOCATIONS\n", + " - Each alternative in a comparison card\n", + " - Scores, opportunity types, strengths/concerns\n", + " - Why each is not the top choice\n", + "\n", + " SLIDE 6 - KEY INSIGHTS & NEXT STEPS\n", + " - Strategic insights (bullet points or cards)\n", + " - Actionable next steps (numbered list)\n", + "\n", + " SLIDE 7 - METHODOLOGY\n", + " - How the analysis was performed\n", + " - Data sources and approach\n", + "\n", + "2. DESIGN:\n", + " - Use professional consulting color palette:\n", + " * Primary: Navy blue (#1e3a8a, #3b82f6) for headers/trust\n", + " * Success: Green (#059669, #10b981) for positive metrics\n", + " * Warning: Amber (#d97706, #f59e0b) for concerns\n", + " * Neutral: Grays (#6b7280, #e5e7eb) for backgrounds\n", + " - Modern sans-serif fonts (system: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto)\n", + " - Cards with subtle shadows and rounded corners\n", + " - Generous white space and padding\n", + " - Responsive grid layouts\n", + "\n", + "3. TECHNICAL:\n", + " - Self-contained: ALL CSS embedded in \n", + "\n", + " \n", + " \n", + " \n", + " \n", + "
    \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
    " + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "tvgnzT1CKxrO" + }, + "source": [ + "> [**Qwen 3**](https://qwenlm.github.io/blog/qwen3/) is the latest generation of large language models in Qwen series, offering a comprehensive suite of dense and mixture-of-experts (MoE) models.\n", + "It supports thinking and function calling.\n", + "\n", + "> **[Cloud Run](https://cloud.google.com/run)**:\n", + "It's a serverless platform by Google Cloud for running containerized applications. It automatically scales and manages infrastructure, supporting various programming languages. Cloud Run now offers GPU acceleration for AI/ML workloads. With 30 seconds to the first token, Cloud Run is a perfect platform for serving lightweight models.\n", + "\n", + "> **Note:** [GPU support in Cloud Run](https://cloud.google.com/run/docs/configuring/services/gpu) is Generally Available.\n", + "To use the GPU feature, your project must have `Total Nvidia L4 GPU allocation without zonal redundancy, per project per region`.\n", + "\n", + "> **[Ollama](ollama.com)**: is an open-source tool for easily running and deploying large language models locally. It offers simple management and usage of LLMs on personal computers or servers.\n", + "\n", + "This notebook showcase how to deploy [Qwen 3](https://developers.googleblog.com/en/introducing-gemma3) in Cloud Run,\n", + "with the objective to an API for running AI Agents built with Google [Agent Development Kit](https://google.github.io/adk-docs/).\n", + "\n", + "By the end of this notebook, you will learn how to:\n", + "\n", + "1. Deploy Qwen 3 as an OpenAI-compatible API on Cloud Run using Ollama.\n", + "2. Build a custom container with Ollama to deploy any Large Language Model (LLM) of your choice.\n", + "3. Make requests to an API hosted on Cloud Run.\n", + "4. Create and run an Agent that uses Qwen 3.\n", + "\n", + "We will build an agent using [Agent Development Kit](https://google.github.io/adk-docs/) - a flexible and modular model-agnostic framework for developing and deploying AI agents." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "61RBz8LLbxCR" + }, + "source": [ + "## Get started" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "aOiPjM5DEPhK" + }, + "source": [ + "### Install Google Cloud CLI\n", + "\n", + "Make sure you Google Cloud CLI is installed (try running `gcloud version`) or [install it](https://cloud.google.com/sdk/docs/install) before executing this notebook.\n", + "\n", + "> If you are running in Colab or Vertex AI workbench, you already have Google Cloud CLI installed." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "b0b84cb331d5" + }, + "source": [ + "### Install Agent Development Kit and other required packages" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "330f4fbf7da9" + }, + "outputs": [], + "source": [ + "%pip install --upgrade --quiet google-genai google-adk litellm" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "HfAVa08RDDJB" + }, + "source": [ + "### Choose a model, a project, and a region to host the model\n", + "\n", + "[Choose a Qwen 3 model](https://ollama.com/library/qwen3) to use, a Google Cloud project to host your Cloud Run service, and a region to host it in.\n", + "Ollama offers multiple sizes with different quantization.\n", + "In this notebook, we use [Qwen3:8b](https://ollama.com/library/qwen3:8b) with `Q4_K_M` quantization.\n", + "\n", + "For Google Cloud project, if you don't have a project yet:\n", + "\n", + "1. [Create a project](https://console.cloud.google.com/projectcreate) in the Google Cloud Console.\n", + "2. Copy your `Project ID` from the project's [Settings page](https://console.cloud.google.com/iam-admin/settings).\n", + "\n", + "The project must have `Total Nvidia L4 GPU allocation without zonal redundancy, per project per region` quota allocated in the selected region.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "id": "TV0pbqJHDDJB" + }, + "outputs": [], + "source": [ + "# { display-mode: \"form\", run: \"auto\" }\n", + "\n", + "MODEL = \"qwen3:8b\" # @param {type:\"string\", isTemplate: true}\n", + "\n", + "PROJECT_ID = \"[your-project-id]\" # @param {type:\"string\", isTemplate: true}\n", + "REGION = \"us-central1\" # @param {type:\"string\", isTemplate: true}\n", + "\n", + "if PROJECT_ID == \"[your-project-id]\" or not PROJECT_ID:\n", + " print(\"Please specify your project id in PROJECT_ID variable.\")\n", + " raise KeyboardInterrupt\n", + "\n", + "MODEL_NAME_ESCAPED = MODEL.translate(str.maketrans(\".:/\", \"---\"))\n", + "SERVICE_NAME = f\"ollama--{MODEL_NAME_ESCAPED}\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "6c36c31ee2de" + }, + "outputs": [], + "source": [ + "### Python dependency imports\n", + "import os\n", + "import subprocess\n", + "from datetime import datetime" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "dmWOrTJ3gx13" + }, + "source": [ + "### Authenticate your Google Cloud account\n", + "\n", + "Depending on your Jupyter environment, you may have to manually authenticate. Run the cell below." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "Xc8Jm1P3Y7fs" + }, + "outputs": [], + "source": [ + "!gcloud auth print-identity-token -q &> /dev/null || gcloud auth login --project=\"{PROJECT_ID}\" --update-adc --quiet" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "l728UOEPDDJB" + }, + "source": [ + "## Prepare serving container\n", + "\n", + "First, let's create a Docker file for a container with the model embedded into it." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "f56f9803255a" + }, + "outputs": [], + "source": [ + "!rm -f Dockerfile\n", + "!echo \"ARG _MODEL=\\\"{MODEL}\\\"\" > Dockerfile" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "glBn9gPKDDJB" + }, + "outputs": [], + "source": [ + "%%writefile -a Dockerfile\n", + "\n", + "FROM ollama/ollama:latest\n", + "ARG _MODEL\n", + "\n", + "# Set the host and port to listen on\n", + "ENV OLLAMA_HOST 0.0.0.0:8080\n", + "\n", + "# Set the directory to store model weight files\n", + "ENV OLLAMA_MODELS /models\n", + "\n", + "# Reduce the verbosity of the logs\n", + "ENV OLLAMA_DEBUG false\n", + "\n", + "# Do not unload model weights from the GPU\n", + "ENV OLLAMA_KEEP_ALIVE -1\n", + "\n", + "# Start the ollama server and download the model weights\n", + "RUN ollama serve & sleep 5 && ollama pull $_MODEL\n", + "\n", + "# At startup time we start the server and run a dummy request\n", + "# to request the model to be loaded in the GPU memory\n", + "ENTRYPOINT [\"/bin/sh\"]\n", + "CMD [\"-c\", \"ollama serve & (ollama run $_MODEL 'Say one word' &) && wait\"]" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "vbDiABJcDDJC" + }, + "source": [ + "## Build Container Image and Deploy Cloud Run Service\n", + "\n", + "We are ready to build our container image and deploy Cloud Run service.\n", + "\n", + "The script below performs the following actions:\n", + "\n", + "* Enables necessary APIs.\n", + "* Creates an Artifact Repository for the image.\n", + "* Creates a Service Account for the service.\n", + "* Submits a Cloud Build job to create and push the container image.\n", + "* Deploys the Cloud Run service.\n", + "\n", + "> The script may take 10-45 minutes to finish.\n", + "\n", + "Note the following important flags in Cloud Build deployment command:\n", + "\n", + "* `--concurrency 4` is set to match the value of the environment variable `OLLAMA_NUM_PARALLEL`.\n", + "* `--gpu 1` with `--gpu-type nvidia-l4` assigns 1 NVIDIA L4 GPU to every Cloud Run instance in the service.\n", + "* `--no-gpu-zonal-redundancy` allows using the default GPU quota.\n", + "* `--no-allow-authenticated` restricts unauthenticated access to the service.\n", + "By keeping the service private, you can rely on Cloud Run's built-in [Identity and Access Management (IAM)](https://cloud.google.com/iam) authentication for service-to-service communication.\n", + "* `--no-cpu-throttling` is required for enabling GPU.\n", + "* `--service-account` the service identity of the service.\n", + "* `--max-instances` sets maximum number of instances of the service.\n", + "It has to be equal to or lower than your project's NVIDIA L4 GPU quota.\n", + "\n", + "For optimal GPU utilization, increase `--concurrency`, keeping it within twice the value of `OLLAMA_NUM_PARALLEL`.\n", + "While this leads to request queuing in Ollama, it can help improve utilization:\n", + "Ollama instances can immediately process requests from their queue, and the queues help absorb traffic spikes." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "TXg7IYU1DDJC" + }, + "outputs": [], + "source": [ + "%%writefile deploy.sh\n", + "\n", + "PROJECT_ID=$1\n", + "REGION=$2\n", + "MODEL_ID=\"${3}\"\n", + "SERVICE_NAME=\"${4}\"\n", + "SERVICE_ACCOUNT=\"ollama-cloud-run-sa\"\n", + "SERVICE_ACCOUNT_ADDRESS=\"${SERVICE_ACCOUNT}@$PROJECT_ID.iam.gserviceaccount.com\"\n", + "MAX_INSTANCES=1 # Adjust this value to match your Cloud Run L4 GPU quota\n", + "\n", + "echo \"Enabling APIs in project ${PROJECT_ID}.\"\n", + "gcloud services enable run.googleapis.com \\\n", + " cloudbuild.googleapis.com \\\n", + " artifactregistry.googleapis.com \\\n", + " --project ${PROJECT_ID} \\\n", + " --quiet\n", + "\n", + "set -e\n", + "\n", + "# Creating the service account if doesn't exist.\n", + "sa_list=$(gcloud iam service-accounts list --quiet --format 'value(email)' --project $PROJECT_ID --filter=email:$SERVICE_ACCOUNT@$PROJECT_ID.iam.gserviceaccount.com 2>/dev/null)\n", + "if [ -z \"${sa_list}\" ]; then\n", + " echo \"Creating Service Account ${SERVICE_ACCOUNT}.\"\n", + " gcloud iam service-accounts create $SERVICE_ACCOUNT \\\n", + " --project ${PROJECT_ID} \\\n", + " --display-name=\"${SERVICE_ACCOUNT} - Cloud Run Service Account\"\n", + "fi\n", + "\n", + "echo \"Deploying Service ${SERVICE_NAME}. It will take a few minutes...\"\n", + "gcloud beta run deploy $SERVICE_NAME \\\n", + " --source . \\\n", + " --project=${PROJECT_ID} \\\n", + " --service-account $SERVICE_ACCOUNT_ADDRESS \\\n", + " --cpu=8 \\\n", + " --memory=32Gi \\\n", + " --gpu=1 \\\n", + " --gpu-type=nvidia-l4 \\\n", + " --concurrency 4 \\\n", + " --set-env-vars OLLAMA_NUM_PARALLEL=4 \\\n", + " --region ${REGION} \\\n", + " --no-allow-unauthenticated \\\n", + " --max-instances ${MAX_INSTANCES} \\\n", + " --no-cpu-throttling \\\n", + " --timeout 1h \\\n", + " --no-gpu-zonal-redundancy \\\n", + " --quiet \\\n", + " --no-user-output-enabled\n", + "\n", + "rm -f ./Dockerfile # Cleanup\n", + "\n", + "SERVICE_URL=$(gcloud run services describe ${SERVICE_NAME} --project=${PROJECT_ID} --region $REGION --format 'value(status.url)' --quiet)\n", + "echo \"✅ Success!\"\n", + "echo \"🚀 Service URL: ${SERVICE_URL}\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "e6L2dVGyOAxB" + }, + "outputs": [], + "source": [ + "!/bin/bash ./deploy.sh \"{PROJECT_ID}\" \"{REGION}\" \"{MODEL}\" \"{SERVICE_NAME}\" && rm -f ./deploy.sh" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "dgaJ62rmDDJC" + }, + "source": [ + "### Test the deployed service\n", + "\n", + "Now, let's test the service you deployed.\n", + "\n", + "First, simply by using `cURL`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "iX7LmWwGDDJC" + }, + "outputs": [], + "source": [ + "%%bash -s $MODEL $SERVICE_NAME $PROJECT_ID $REGION\n", + "\n", + "PROMPT=\"Hello!\"\n", + "SERVICE_URL=$(gcloud run services describe ${2} --project ${3} --region ${4} --format 'value(status.url)' --quiet)\n", + "AUTH_TOKEN=$(gcloud auth print-identity-token -q)\n", + "\n", + "curl -s -X POST \"${SERVICE_URL}/api/generate\" \\\n", + "-H \"Authorization: Bearer ${AUTH_TOKEN}\" \\\n", + "-H \"Content-Type: application/json\" \\\n", + "-d '{ \"model\": \"'${1}'\", \"prompt\": \"'${PROMPT}'\", \"max_tokens\": 100, \"stream\": false}'" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "2b60e54c901d" + }, + "source": [ + "## Create an AI Agent with Qwen 3" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "1e1f3cb538ed" + }, + "source": [ + "#### Retrieve an Identity Token\n", + "\n", + "Cloud Run with authentication by\n", + "[Google Cloud IAM](https://cloud.google.com/iam/docs/) requires an [identity token](https://cloud.google.com/docs/authentication/get-id-token) in every request's authentication header." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "X-2TbV6tDDJC" + }, + "outputs": [], + "source": [ + "auth_id_token = (\n", + " subprocess.check_output(\"gcloud auth print-identity-token -q\", shell=True)\n", + " .decode()\n", + " .strip()\n", + ")\n", + "service_url = (\n", + " subprocess.check_output(\n", + " (\n", + " \"gcloud run services describe \"\n", + " f\"{SERVICE_NAME} --project={PROJECT_ID} \"\n", + " f\"--region={REGION} \"\n", + " \"--format='value(status.url)' -q\"\n", + " ),\n", + " shell=True,\n", + " )\n", + " .decode()\n", + " .strip()\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "2f5ae9755e13" + }, + "source": [ + "#### Create and run an Agent\n", + "\n", + "We create a simple agent that can answer questions about current time.\n", + "\n", + "First, we make a tool that returns current user's time with time zone offset." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "d66169bd7a12" + }, + "outputs": [], + "source": [ + "def get_current_time() -> str:\n", + " \"\"\"Returns user's local time and timezone offset.\n", + "\n", + " Returns:\n", + " str: Time in ISO format with user's timezone offset.\n", + " \"\"\"\n", + " return datetime.now().astimezone().isoformat()\n", + "\n", + "\n", + "print(get_current_time())" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "b1a1ae76f3bb" + }, + "source": [ + "Now, we create an agent that can use our tool to answer user's question." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "d7c2b8951050" + }, + "outputs": [], + "source": [ + "from google.adk import Runner\n", + "from google.adk.agents import Agent\n", + "from google.adk.artifacts import InMemoryArtifactService\n", + "from google.adk.models.lite_llm import LiteLlm\n", + "from google.adk.sessions import InMemorySessionService, Session\n", + "from google.genai import types\n", + "\n", + "os.environ[\"OLLAMA_API_BASE\"] = service_url # still required for LiteLlm to work\n", + "\n", + "root_agent = Agent(\n", + " name=\"time_agent\",\n", + " model=LiteLlm(\n", + " model=f\"openai/{MODEL}\",\n", + " api_base=service_url + \"/v1\",\n", + " api_key=auth_id_token,\n", + " temperature=0.1, # for stable function calling\n", + " ),\n", + " description=(\"Agent to answer questions about current time.\"),\n", + " # Agent instructions with `/no_think` for Qwen 3 to run faster.\n", + " instruction=\"\"\"\n", + " You are a helpful agent who can answer user questions\n", + " about the current local time.\n", + " Always output date and time is human-readable form.\n", + " /no_think\n", + " \"\"\",\n", + " tools=[get_current_time],\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "0763595c0eff" + }, + "source": [ + "Initialize the runtime." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "2d8631d9da3f" + }, + "outputs": [], + "source": [ + "app_name = \"my_app\"\n", + "user_id_1 = \"user1\"\n", + "session_service = InMemorySessionService()\n", + "artifact_service = InMemoryArtifactService()\n", + "runner = Runner(\n", + " app_name=app_name,\n", + " agent=root_agent,\n", + " artifact_service=artifact_service,\n", + " session_service=session_service,\n", + ")\n", + "new_session = await session_service.create_session(app_name=app_name, user_id=user_id_1)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "64b9387644c6" + }, + "source": [ + "And run our agent!" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "f09d7a5e0ca8" + }, + "outputs": [], + "source": [ + "async def run_prompt(session: Session, new_message: str):\n", + " content = types.Content(role=\"user\", parts=[types.Part.from_text(text=new_message)])\n", + " print(\"** User says:\", new_message)\n", + " async for event in runner.run_async(\n", + " user_id=user_id_1,\n", + " session_id=session.id,\n", + " new_message=content,\n", + " ):\n", + " if not event.content or not event.content.parts:\n", + " continue\n", + " print(f\"** {event.author}:\")\n", + " for part in event.content.parts:\n", + " if part.function_call and part.function_call.name:\n", + " print(\n", + " f\"\\t#### Calling `{part.function_call.name}` \"\n", + " f\"with args: {part.function_call.args}\"\n", + " )\n", + " elif part.function_response and part.function_response.response:\n", + " print(f\"\\t### Function call result: {part.function_response.response}\")\n", + " elif part.text and part.text.strip():\n", + " print(f\"\\t{part.text.strip()}\")\n", + "\n", + "\n", + "QUESTION = \"What time is it now?\"\n", + "await run_prompt(new_session, QUESTION) # if not Jupyter, wrap in asyncio.run" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "FFL2V_ClDDJD" + }, + "source": [ + "## Conclusion\n", + "Congratulations! You can now use Qwen 3 for running your AI Agents built with Agent Development Kit in Cloud Run!" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "f6f17f9aff65" + }, + "source": [ + "## Cleaning up" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "s1blF2ziDDJD" + }, + "source": [ + "To delete the Cloud Run service you created, you can uncomment and run the following cell." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "VbhAz7-9DDJD" + }, + "outputs": [], + "source": [ + "# !gcloud run services delete $SERVICE_NAME --project $PROJECT_ID --region $LOCATION --quiet" + ] + } + ], + "metadata": { + "colab": { + "name": "cloud_run_ollama_qwen3_inference.ipynb", + "toc_visible": true + }, + "kernelspec": { + "display_name": "Python 3", + "name": "python3" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} diff --git a/open-models/serving/cloud_run_vllm_gemma3_inference.ipynb b/open-models/serving/cloud_run_vllm_gemma3_inference.ipynb new file mode 100644 index 0000000..384a8b3 --- /dev/null +++ b/open-models/serving/cloud_run_vllm_gemma3_inference.ipynb @@ -0,0 +1,685 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "ur8xi4C7S06n" + }, + "outputs": [], + "source": [ + "# Copyright 2025 Google LLC\n", + "#\n", + "# Licensed under the Apache License, Version 2.0 (the \"License\");\n", + "# you may not use this file except in compliance with the License.\n", + "# You may obtain a copy of the License at\n", + "#\n", + "# https://www.apache.org/licenses/LICENSE-2.0\n", + "#\n", + "# Unless required by applicable law or agreed to in writing, software\n", + "# distributed under the License is distributed on an \"AS IS\" BASIS,\n", + "# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n", + "# See the License for the specific language governing permissions and\n", + "# limitations under the License." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "JAPoU8Sm5E6e" + }, + "source": [ + "# Serving Gemma 3 with vLLM on Cloud Run\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + "
    \n", + " \n", + " \"Google
    Open in Colab\n", + "
    \n", + "
    \n", + " \n", + " \"Google
    Open in Colab Enterprise\n", + "
    \n", + "
    \n", + " \n", + " \"Vertex
    Open in Vertex AI Workbench\n", + "
    \n", + "
    \n", + " \n", + " \"GitHub
    View on GitHub\n", + "
    \n", + "
    \n", + "\n", + "
    \n", + "Share to:\n", + "\n", + "\n", + " \"LinkedIn\n", + "\n", + "\n", + "\n", + " \"Bluesky\n", + "\n", + "\n", + "\n", + " \"X\n", + "\n", + "\n", + "\n", + " \"Reddit\n", + "\n", + "\n", + "\n", + " \"Facebook\n", + " " + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "83b98b0ba19c" + }, + "source": [ + "\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "84f0f73a0f76" + }, + "source": [ + "| | |\n", + "|-|-|\n", + "| Author(s) | [Vlad Kolesnikov](https://github.com/vladkol) |" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "ccd500ae19b5" + }, + "source": [ + "## Overview" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "tvgnzT1CKxrO" + }, + "source": [ + "> [**Gemma 3**](https://ai.google.dev/gemma) is a new generation of open models developed by Google. It is a collection of lightweight, state-of-the-art open models built from the same research and technology that powers our Gemini 2.0 models. Gemma 3 comes in a range of sizes (1B, 4B, 12B and 27B), allowing you to choose the best model for your specific hardware and performance needs. Gemma 3 models are available through platforms like Google AI Studio, Kaggle, and Hugging Face.\n", + "\n", + "> **[Cloud Run](https://cloud.google.com/run)**:\n", + "It's a serverless platform by Google Cloud for running containerized applications. It automatically scales and manages infrastructure, supporting various programming languages. Cloud Run now offers GPU acceleration for AI/ML workloads. With 30 seconds to the first token, Cloud Run is a perfect platform for serving lightweight models like Gemma.\n", + "\n", + "> **Note:** GPU support in Cloud Run is in preview. To use the GPU feature, you must request `Total Nvidia L4 GPU allocation, per project per region` quota under Cloud Run in the [Quotas and system limits page](https://cloud.google.com/run/quotas#increase).\n", + "\n", + "\n", + "> **[vLLM](https://docs.vllm.ai/)**: is a fast and easy-to-use library for LLM inference and serving. It provides high-throughput serving, optimized model execution, and tensor parallelism.\n", + "\n", + "This notebook showcase how to deploy [Google Gemma 3](https://developers.googleblog.com/en/introducing-gemma3) in Cloud Run, with the objective to build a simple API for chat or RAG applications.\n", + "\n", + "By the end of this notebook, you will learn how to:\n", + "\n", + "1. Deploy Google Gemma 3 as an OpenAI-compatible API on Cloud Run using vLLM.\n", + "2. Build a custom container with vLLM to deploy any Large Language Model (LLM) of your choice.\n", + "3. Make requests to an API hosted on Cloud Run." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "61RBz8LLbxCR" + }, + "source": [ + "## Get started" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "No17Cw5hgx12" + }, + "source": [ + "### Install Google Cloud SDK and HuggingFace Hub dependencies\n", + "\n", + "Make sure you Google Cloud SDK is installed (try running `gcloud version`) or [install it](https://cloud.google.com/sdk/docs/install) before executing this notebook.\n", + "\n", + "> If you are running in Colab or Vertex AI workbench, you have Google Cloud SDK installed.\n", + "\n", + "You also need to install `huggingface_hub` Python package." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "Tm31a7_cDbeu" + }, + "outputs": [], + "source": [ + "%pip install --upgrade --quiet huggingface_hub" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "MajzyxmFDbeu" + }, + "source": [ + "### Choose a model, a project, and a region to host the model\n", + "\n", + "Choose a Gemma 3 model to use, a Google Cloud project to host your Cloud Run service, and a region to host it in.\n", + "\n", + "If you don't have a project yet:\n", + "\n", + "1. [Create a project](https://console.cloud.google.com/projectcreate) in the Google Cloud Console.\n", + "2. Copy your `Project ID` from the project's [Settings page](https://console.cloud.google.com/iam-admin/settings).\n", + "\n", + "The project must have `Total Nvidia L4 GPU allocation, per project per region` quota allocated in the selected region.\n", + "To make sure it's available, check Cloud Run in the [Quotas and system limits page](https://console.cloud.google.com/iam-admin/quotas)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "id": "Do_Z5bVNDbeu" + }, + "outputs": [], + "source": [ + "# { display-mode: \"form\", run: \"auto\" }\n", + "\n", + "MODEL = \"google/gemma-3-4b-it\" # @param {type:\"string\"}\n", + "\n", + "PROJECT_ID = \"[your-project-id]\" # @param {type:\"string\", isTemplate: true}\n", + "REGION = \"us-central1\" # @param {type:\"string\", isTemplate: true}\n", + "\n", + "if PROJECT_ID == \"[your-project-id]\" or not PROJECT_ID:\n", + " print(\"Please specify your project id in PROJECT_ID variable.\")\n", + " raise KeyboardInterrupt\n", + "\n", + "SERVICE_NAME = f\"vllm--{MODEL.replace('.', '-').replace('/','--')}\"" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "dmWOrTJ3gx13" + }, + "source": [ + "### Authenticate your Google Cloud account\n", + "\n", + "Depending on your Jupyter environment, you may have to manually authenticate. Run the cell below." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "c93d96bc9c43" + }, + "outputs": [], + "source": [ + "!gcloud auth print-identity-token -q &> /dev/null || gcloud auth login --project=\"{PROJECT_ID}\" --update-adc --quiet" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "f9af3e57f89a" + }, + "source": [ + "### Authenticate your Hugging Face account\n", + "\n", + "As Google Gemma (https://huggingface.co/google/gemma-3-4b-it) are gated models, you need to have a Hugging Face Hub account, and accept the Google's usage license for Gemma. Once that's done, you need to generate a new user access token with read-only access so that the weights can be downloaded from the Hub.\n", + "\n", + "> Note that the user access token can only be generated via [the Hugging Face Hub UI](https://huggingface.co/settings/tokens/new), where you can either select read-only access to your account, or follow the recommendations and generate a fine-grained token with read-only access to [`google/gemma-3-4b-it`](https://huggingface.co/google/gemma-3-4b-it).\n", + ">\n", + "> Even you had an access token before accepting the Gemma license, you need to generate a new one after accepting it.\n", + "\n", + "Then you can use `huggingface_hub` for the authentication with the token generated in advance. So that then the token can be safely retrieved via `huggingface_hub.get_token`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "8d836e0210fe" + }, + "outputs": [], + "source": [ + "from huggingface_hub import get_token, interpreter_login\n", + "\n", + "interpreter_login(new_session=False)\n", + "HF_TOKEN = get_token()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "c71a4314c250" + }, + "source": [ + "Read more about [Hugging Face Security](https://huggingface.co/docs/hub/en/security), specifically about [Hugging Face User Access Tokens](https://huggingface.co/docs/hub/en/security-tokens)." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "gwSj4-j9Dbev" + }, + "source": [ + "## Prepare container image\n", + "\n", + "First, let's create a Docker file for a container with the model embedded into it.\n", + "\n", + "It will use your Hugging Face Hub secret at build time." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "8Pt7jloHDbev" + }, + "outputs": [], + "source": [ + "%%writefile Dockerfile\n", + "\n", + "FROM us-docker.pkg.dev/vertex-ai/vertex-vision-model-garden-dockers/pytorch-vllm-serve:20250312_0916_RC01\n", + "\n", + "ARG MODEL\n", + "ENV HF_MODEL=$MODEL\n", + "ENV MODEL_ID=$MODEL\n", + "\n", + "ENV HF_HOME=/model-cache\n", + "# Download the model to the cache\n", + "RUN --mount=type=secret,id=HF_TOKEN HF_TOKEN=$(cat /run/secrets/HF_TOKEN) \\\n", + " huggingface-cli download ${HF_MODEL}\n", + "# Switch to offline mode to make sure vLLM always uses the cached model\n", + "ENV HF_HUB_OFFLINE=1\n", + "\n", + "ENTRYPOINT python3 -m vllm.entrypoints.openai.api_server \\\n", + " --port ${PORT:-8080} \\\n", + " --model ${HF_MODEL} \\\n", + " --max-num-seqs=4 \\\n", + " --max-model-len 32768" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "vZG5JfZRDbev" + }, + "source": [ + "Second, we create a Cloud Build file to use for building and pushing our container image." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "cRiA0VArDbev" + }, + "outputs": [], + "source": [ + "%%writefile cloudbuild.yaml\n", + "\n", + "steps:\n", + "- name: 'gcr.io/cloud-builders/docker'\n", + " id: build\n", + " entrypoint: 'bash'\n", + " secretEnv: ['HF_TOKEN']\n", + " args:\n", + " - -c\n", + " - |\n", + " SECRET_TOKEN=\"$$HF_TOKEN\" docker buildx build --tag=${_IMAGE} --build-arg MODEL=${_MODEL} --secret id=HF_TOKEN .\n", + "\n", + "availableSecrets:\n", + " secretManager:\n", + " - versionName: 'projects/${PROJECT_ID}/secrets/${_HF_TOKEN_SECRET_NAME}/versions/latest'\n", + " env: 'HF_TOKEN'\n", + "\n", + "images: [\"${_IMAGE}\"]\n", + "\n", + "substitutions:\n", + " _IMAGE: '${_REGION}-docker.pkg.dev/${PROJECT_ID}/${_AR_REPO_NAME}/${_SERVICE_NAME}'\n", + "\n", + "options:\n", + " dynamicSubstitutions: true\n", + " machineType: \"E2_HIGHCPU_32\"\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "9Vppnp8kDbev" + }, + "source": [ + "## Build Container Image and Deploy Cloud Run Service\n", + "\n", + "We are ready to build our container image and deploy Cloud Run service.\n", + "\n", + "The script below performs the following actions:\n", + "\n", + "* Enables necessary APIs.\n", + "* Creates an Artifact Repository for the image.\n", + "* Creates a Service Account for the service.\n", + "* Create a secret in Secret Manager that Cloud Build will use for building the container image.\n", + "* Assigns Service Account permissions to access the secret.\n", + "* Submits a Cloud Build job to create and push the container image.\n", + "* Deploys the Cloud Run service.\n", + "\n", + "> The script may take 15-45 minutes to finish.\n", + "\n", + "Note the following important flags in Cloud Build deployment command:\n", + "\n", + "* `--gpu 1` with `--gpu-type nvidia-l4` assigns 1 NVIDIA L4 GPU to every Cloud Run instance in the service.\n", + "`--no-allow-authenticated` restricts unauthenticated access to the service.\n", + "By keeping the service private, you can rely on Cloud Run's built-in [Identity and Access Management (IAM)](https://cloud.google.com/iam) authentication for service-to-service communication.\n", + "* `--no-cpu-throttling` is required for enabling GPU.\n", + "* `--service-account` the service identity of the service.\n", + "* `--max-instances` sets maximum number of instances of the service.\n", + "It has to be equal to or lower than your project's NVIDIA L4 GPU (`Total Nvidia L4 GPU allocation, per project per region`) quota." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "xsbvKy-_Dbev" + }, + "outputs": [], + "source": [ + "%%writefile deploy.sh\n", + "\n", + "PROJECT_ID=$1\n", + "REGION=$2\n", + "MODEL_ID=\"${3}\"\n", + "SERVICE_NAME=\"${5}\"\n", + "AR_REPO_NAME=\"vllm-repo\"\n", + "SERVICE_ACCOUNT=\"vllm-cloud-run-sa\"\n", + "SERVICE_ACCOUNT_ADDRESS=\"${SERVICE_ACCOUNT}@$PROJECT_ID.iam.gserviceaccount.com\"\n", + "HF_TOKEN_SECRET_NAME=\"HF_TOKEN_SECRET\"\n", + "HF_TOKEN=$4\n", + "MAX_INSTANCES=1 # Adjust this value to match your Cloud Run L4 GPU quota (\"Total Nvidia L4 GPU allocation, per project per region\", NvidiaL4GpuAllocPerProjectRegion, run.googleapis.com/nvidia_l4_gpu_allocation)\n", + "\n", + "echo \"Enabling APIs in project ${PROJECT_ID}.\"\n", + "gcloud services enable run.googleapis.com \\\n", + " cloudbuild.googleapis.com \\\n", + " secretmanager.googleapis.com \\\n", + " artifactregistry.googleapis.com \\\n", + " --project ${PROJECT_ID} \\\n", + " --quiet\n", + "\n", + "set -e\n", + "\n", + "# Creating the service account if doesn't exist.\n", + "sa_list=$(gcloud iam service-accounts list --quiet --format 'value(email)' --project $PROJECT_ID --filter=email:$SERVICE_ACCOUNT@$PROJECT_ID.iam.gserviceaccount.com 2>/dev/null)\n", + "if [ -z \"${sa_list}\" ]; then\n", + " echo \"Creating Service Account ${SERVICE_ACCOUNT}.\"\n", + " gcloud iam service-accounts create $SERVICE_ACCOUNT \\\n", + " --project ${PROJECT_ID} \\\n", + " --display-name=\"${SERVICE_ACCOUNT} - Cloud Run Service Account\"\n", + "fi\n", + "\n", + "# Creating or updating the secret.\n", + "secrets_list=$(gcloud secrets list --project=\"${PROJECT_ID}\" --quiet --format 'value(name)' --filter=name:\"${HF_TOKEN_SECRET_NAME}\" 2>/dev/null)\n", + "if [ -z \"${secrets_list}\" ]; then\n", + " echo \"Creating Secret ${HF_TOKEN_SECRET_NAME}.\"\n", + " echo \"${HF_TOKEN}\" | gcloud secrets create \"${HF_TOKEN_SECRET_NAME}\" --project=${PROJECT_ID} --data-file=-\n", + "else\n", + " echo \"Updating Secret ${HF_TOKEN_SECRET_NAME}.\"\n", + " echo \"${HF_TOKEN}\" | gcloud secrets versions add \"${HF_TOKEN_SECRET_NAME}\" --project=${PROJECT_ID} --data-file=-\n", + "fi\n", + "\n", + "# Getting Cloud Build service account and giving it access to the secret\n", + "echo \"Applying Cloud Build account permissions.\"\n", + "cloud_build_sa=$(gcloud builds get-default-service-account --format 'value(serviceAccountEmail)' --project ${PROJECT_ID} --quiet)\n", + "cloud_build_sa_email=\"${cloud_build_sa##*/}\"\n", + "gcloud secrets add-iam-policy-binding \"${HF_TOKEN_SECRET_NAME}\" \\\n", + " --member serviceAccount:\"${cloud_build_sa_email}\" \\\n", + " --role='roles/secretmanager.secretAccessor' \\\n", + " --project=${PROJECT_ID}\n", + "\n", + "# Creating the Artifacts Repository if doesn't exist\n", + "repo_list=$(gcloud artifacts repositories list --format 'value(name)' --filter=name=\"projects/${PROJECT_ID}/locations/${REGION}/repositories/${AR_REPO_NAME}\" --project ${PROJECT_ID} --quiet --location ${REGION} 2>/dev/null)\n", + "if [ -z \"${repo_list}\" ]; then\n", + " echo \"Creating Artifact Registry ${AR_REPO_NAME}.\"\n", + " gcloud artifacts repositories create $AR_REPO_NAME \\\n", + " --repository-format docker \\\n", + " --location ${REGION} \\\n", + " --project=${PROJECT_ID}\n", + "fi\n", + "\n", + "echo \"Building container image.\"\n", + "gcloud builds submit --config=cloudbuild.yaml --project=${PROJECT_ID} . \\\n", + " --suppress-logs \\\n", + " --substitutions \\\n", + " _AR_REPO_NAME=$AR_REPO_NAME,_REGION=$REGION,_SERVICE_NAME=$SERVICE_NAME,_MODEL=$MODEL_ID,_HF_TOKEN_SECRET_NAME=$HF_TOKEN_SECRET_NAME\n", + "rm -f cloudbuild.yaml\n", + "rm -f Dockerfile\n", + "\n", + "echo \"Deploying Service ${SERVICE_NAME}.\"\n", + "gcloud beta run deploy $SERVICE_NAME \\\n", + " --project=${PROJECT_ID} \\\n", + " --image=${REGION}-docker.pkg.dev/$PROJECT_ID/$AR_REPO_NAME/$SERVICE_NAME \\\n", + " --service-account $SERVICE_ACCOUNT_ADDRESS \\\n", + " --cpu=8 \\\n", + " --memory=32Gi \\\n", + " --gpu=1 --gpu-type=nvidia-l4 \\\n", + " --region ${REGION} \\\n", + " --no-allow-unauthenticated \\\n", + " --max-instances ${MAX_INSTANCES} \\\n", + " --no-cpu-throttling \\\n", + " --timeout 1h\n", + "\n", + "SERVICE_URL=$(gcloud run services describe ${SERVICE_NAME} --project=${PROJECT_ID} --region $REGION --format 'value(status.url)' --quiet)\n", + "echo \"✅ Success!\"\n", + "echo \"🚀 Service URL: ${SERVICE_URL}\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "eff7e1585597" + }, + "outputs": [], + "source": [ + "!/bin/bash ./deploy.sh \"{PROJECT_ID}\" \"{REGION}\" \"{MODEL}\" \"{HF_TOKEN}\" \"{SERVICE_NAME}\" && rm -f ./deploy.sh" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "WzSFgfuyDbew" + }, + "source": [ + "## Test the deployed service\n", + "\n", + "Now, let's test the service you deployed.\n", + "\n", + "First, simply by using `cURL`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "1LDyxRmADbew" + }, + "outputs": [], + "source": [ + "%%bash -s {MODEL} {SERVICE_NAME} {PROJECT_ID} {REGION}\n", + "\n", + "PROMPT=\"Why is the sky blue?\"\n", + "SERVICE_URL=$(gcloud run services describe ${2} --project ${3} --region ${4} --format 'value(status.url)' --quiet)\n", + "AUTH_TOKEN=$(gcloud auth print-identity-token -q)\n", + "\n", + "curl \"${SERVICE_URL}/v1/chat/completions\" \\\n", + " -s \\\n", + " -X POST \\\n", + " -H \"Content-Type: application/json\" \\\n", + " -H \"Authorization: Bearer ${AUTH_TOKEN}\" \\\n", + " -d @- <\n", + " \n", + " \n", + " \"Google
    Open in Colab\n", + "
    \n", + " \n", + " \n", + " \n", + " \"Google
    Open in Colab Enterprise\n", + "
    \n", + " \n", + " \n", + " \n", + " \"Vertex
    Open in Vertex AI Workbench\n", + "
    \n", + " \n", + " \n", + " \n", + " \"GitHub
    View on GitHub\n", + "
    \n", + " \n", + "\n", + "\n", + "
    \n", + "\n", + "

    \n", + "Share to:\n", + "\n", + "\n", + " \"LinkedIn\n", + "\n", + "\n", + "\n", + " \"Bluesky\n", + "\n", + "\n", + "\n", + " \"X\n", + "\n", + "\n", + "\n", + " \"Reddit\n", + "\n", + "\n", + "\n", + " \"Facebook\n", + "\n", + "

    " + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "84f0f73a0f76" + }, + "source": [ + "| Author(s) |\n", + "| --- |\n", + "| [Ivan Nardini](https://github.com/inardini) |" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "tvgnzT1CKxrO" + }, + "source": [ + "## Overview\n", + "\n", + "This tutorial provides a comprehensive guide to deploying multiple LoRA (Low-Rank Adaptation) adapters on Google Cloud's Vertex AI using vLLM. By the end of this tutorial, you'll be able to serve a single base model with multiple specialized adapters, allowing you to handle different types of tasks (like SQL generation and code generation) using the same infrastructure.\n", + "\n", + "### What you'll cover\n", + "\n", + "You'll deploy a Vertex AI endpoint that serves:\n", + "- **Base Model**: `google/gemma-2-2b-it` (Gemma 2 2B Instruct)\n", + "- **SQL Adapter**: `google-cloud-partnership/gemma-2-2b-it-lora-sql` (specialized for SQL query generation)\n", + "- **Magicoder Adapter**: `google-cloud-partnership/gemma-2-2b-it-lora-magicoder` (specialized for code generation)\n", + "\n", + "All three models will be available simultaneously, and clients can switch between them on a per-request basis with minimal overhead.\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "61RBz8LLbxCR" + }, + "source": [ + "## Get started\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "No17Cw5hgx12" + }, + "source": [ + "### Install required packages" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "tFy3H3aPgx12" + }, + "outputs": [], + "source": [ + "%pip install --upgrade --quiet google-cloud-aiplatform huggingface_hub[hf_transfer]" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "dmWOrTJ3gx13" + }, + "source": [ + "### Authenticate your notebook environment (Colab only)\n", + "\n", + "If you're running this notebook on Google Colab, run the cell below to authenticate your environment." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "NyKGtVQjgx13" + }, + "outputs": [], + "source": [ + "import sys\n", + "\n", + "if \"google.colab\" in sys.modules:\n", + " from google.colab import auth\n", + "\n", + " auth.authenticate_user()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "DF4l8DTdWgPY" + }, + "source": [ + "### Set Google Cloud project information\n", + "\n", + "To get started using Vertex AI, you must have an existing Google Cloud project and [enable the Vertex AI API](https://console.cloud.google.com/flows/enableapi?apiid=aiplatform.googleapis.com).\n", + "\n", + "Also you need to be sure that the following IAM permissions are assigned:\n", + "\n", + " - `roles/aiplatform.user` (Vertex AI User)\n", + " - `roles/artifactregistry.admin` (Artifact Registry Admin)\n", + " - `roles/cloudbuild.builds.editor` (Cloud Build Editor)\n", + " - `roles/storage.admin` (Storage Admin)\n", + "\n", + "Learn more about [setting up a project and a development environment](https://cloud.google.com/vertex-ai/docs/start/cloud-environment)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "Nqwi-5ufWp_B" + }, + "outputs": [], + "source": [ + "# Use the environment variable if the user doesn't provide Project ID.\n", + "import os\n", + "import vertexai\n", + "\n", + "# fmt: off\n", + "PROJECT_ID = \"[your-project-id]\" # @param {type: \"string\", placeholder: \"[your-project-id]\", isTemplate: true}\n", + "# fmt: on\n", + "if not PROJECT_ID or PROJECT_ID == \"[your-project-id]\":\n", + " PROJECT_ID = str(os.environ.get(\"GOOGLE_CLOUD_PROJECT\"))\n", + "\n", + "LOCATION = os.environ.get(\"GOOGLE_CLOUD_REGION\", \"us-central1\")\n", + "\n", + "# Create GCS bucket\n", + "BUCKET_NAME = f\"{PROJECT_ID}-vllm-peft-serving\"\n", + "BUCKET_URI = f\"gs://{BUCKET_NAME}\"\n", + "\n", + "! gcloud storage buckets create {BUCKET_URI} --location={LOCATION} --project={PROJECT_ID}\n", + "\n", + "# Set fast download from HF\n", + "os.environ[\"HF_HUB_ENABLE_HF_TRANSFER\"] = \"1\"\n", + "\n", + "# Initialize Vertex AI SDK\n", + "vertexai.init(project=PROJECT_ID, location=LOCATION)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "wr4_ajtJKC8c" + }, + "source": [ + "### Import libraries\n", + "\n", + "Import required libraries." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "-5WG5dRqKJcw" + }, + "outputs": [], + "source": [ + "from pathlib import Path as p\n", + "from huggingface_hub import interpreter_login\n", + "from huggingface_hub import get_token, snapshot_download\n", + "from google.cloud import aiplatform\n", + "import json" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "-Ef1BGnlJR27" + }, + "source": [ + "## Create Artifact Registry Repository\n", + "\n", + "Artifact Registry is Google Cloud's service for storing and managing container images, packages, and other artifacts. Think of it as a private Docker Hub for your organization.\n", + "\n", + "In the Artifact Registry, you will store your custom Docker container image.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "uiNgcL05JRQI" + }, + "outputs": [], + "source": [ + "DOCKER_REPOSITORY = \"vllm-lora-repo\"\n", + "\n", + "# Create the repository\n", + "!gcloud artifacts repositories create {DOCKER_REPOSITORY} \\\n", + " --repository-format=docker \\\n", + " --project={PROJECT_ID} \\\n", + " --location={LOCATION} \\\n", + " --description=\"Repository for vLLM containers with LoRA support\"" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "5n6KDpFxJqrg" + }, + "source": [ + "Verify the repository." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "EmAsoJxgJt-4" + }, + "outputs": [], + "source": [ + "!gcloud artifacts repositories list --location={LOCATION} --project={PROJECT_ID}" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "EmkgjGjbU3s9" + }, + "source": [ + "## Download Models and Adapters\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "f9af3e57f89a" + }, + "source": [ + "### Authenticate your Hugging Face account\n", + "\n", + "Many models on HuggingFace, including Gemma, are \"gated\" - meaning you need to request access and accept terms of use before downloading. The HuggingFace token authenticates your downloads. This token is only needed during the download phase, not when serving the model.\n", + "\n", + "To generate a new user access token with read-only access, you need:\n", + "\n", + "1. Create a [HuggingFace account](https://huggingface.co/) if you don't have one\n", + "2. Go to **Settings → Access Tokens**\n", + "3. Click **New Token**\n", + "4. Set name (e.g., \"vertex-ai-deployment\") and role (Read)\n", + "5. Click **Generate**\n", + "6. Copy the token" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "8d836e0210fe" + }, + "outputs": [], + "source": [ + "interpreter_login()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "2pezdYCcWafD" + }, + "source": [ + "### Create Build directory and download models\n", + "\n", + "Prepare the directory you will use to build the serving image." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "GRtgKHakWdyd" + }, + "outputs": [], + "source": [ + "base_model_id = \"google/gemma-2-2b-it\"\n", + "sql_adapter_id = \"google-cloud-partnership/gemma-2-2b-it-lora-sql\"\n", + "magicoder_adapter_id = \"google-cloud-partnership/gemma-2-2b-it-lora-magicoder\"\n", + "\n", + "models_dir = \"./models\"\n", + "adapters_dir = \"./adapters\"\n", + "\n", + "p(models_dir).mkdir(exist_ok=True, parents=True)\n", + "p(adapters_dir).mkdir(exist_ok=True, parents=True)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "cjuvuOKg5yOQ" + }, + "source": [ + "Download base model and adapters." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "qXkm4JfFXEY4" + }, + "outputs": [], + "source": [ + "# Download base model\n", + "base_model_path = snapshot_download(\n", + " repo_id=base_model_id,\n", + " token=get_token(),\n", + " local_dir=f\"{models_dir}/gemma-2-2b-it\",\n", + " local_dir_use_symlinks=False\n", + ")\n", + "\n", + "# Download SQL LoRA adapter\n", + "sql_adapter_path = snapshot_download(\n", + " repo_id=sql_adapter_id,\n", + " token=get_token(),\n", + " local_dir=f\"{adapters_dir}/sql-lora\",\n", + " local_dir_use_symlinks=False\n", + ")\n", + "\n", + "# Download Magicoder LoRA adapter\n", + "magicoder_adapter_path = snapshot_download(\n", + " repo_id=magicoder_adapter_id,\n", + " token=get_token(),\n", + " local_dir=f\"{adapters_dir}/magicoder-lora\",\n", + " local_dir_use_symlinks=False\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "hiQDJsu3cqzz" + }, + "source": [ + "#### Upload Models to Google Cloud Storage\n", + "\n", + "Upload models to GCS for fast downloads during container startup.\n", + "\n", + "> **Note**: Instead of baking models into the Docker image (which would create a ~13GB image), we store models in GCS and download them when the container starts. This keeps the Docker image lightweight, makes model updates easier (no rebuild needed), and leverages GCS's fast regional download speeds.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "7Jn_qYQndbG0" + }, + "outputs": [], + "source": [ + "! gcloud config set storage/parallel_composite_upload_enabled True\n", + "\n", + "# Upload base model\n", + "!gcloud storage cp -r {models_dir}/gemma-2-2b-it {BUCKET_URI}/models/\n", + "\n", + "# Upload adapters\n", + "!gcloud storage cp -r {adapters_dir}/sql-lora {BUCKET_URI}/adapters/\n", + "!gcloud storage cp -r {adapters_dir}/magicoder-lora {BUCKET_URI}/adapters/" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "8L8-S4OHJzP_" + }, + "source": [ + "## Build Custom vLLM Container\n", + "\n", + "Our custom Docker container will:\n", + "\n", + "1. Start from the official vLLM GPU image\n", + "2. **Add a script to download the base model and LoRA adapters** from GCS at container startup\n", + "3. Configure vLLM to use the local models (no downloads at runtime)\n", + "4. Set up Vertex AI compatible health checks\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "y2muZr-xKcNL" + }, + "source": [ + "### Create the build files" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "9NQi9aFUKYlA" + }, + "source": [ + "The build uses three key files:\n", + "\n", + "- **`Dockerfile`**: Defines the container image\n", + "- **`entrypoint.sh`**: Downloads models from GCS at startup\n", + "- **`cloudbuild.yaml`**: Orchestrates the Docker build process" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "CLUhK5tRKkV0" + }, + "source": [ + "#### Create Dockerfile\n", + "\n", + "This Dockerfile:\n", + "\n", + "- Starts from the official vLLM GPU image\n", + "- Installs Google Cloud SDK (for downloading from GCS)\n", + "- Adds entrypoint script\n", + "\n", + "The Dockerfile uses a multi-layer approach. The base vLLM image already contains Python, CUDA drivers, and vLLM itself. We add only the `gcloud` CLI tool to enable GCS downloads. The `ENTRYPOINT` directive ensures our custom script runs before vLLM starts, downloading models first." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "efA_W6c3hmx-" + }, + "outputs": [], + "source": [ + "build_dir = \"build\"\n", + "p(build_dir).mkdir(exist_ok=True, parents=True)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "0ND1QfwKKnpO" + }, + "outputs": [], + "source": [ + "dockerfile = \"\"\"\n", + "ARG BASE_IMAGE\n", + "FROM ${BASE_IMAGE}\n", + "\n", + "ENV DEBIAN_FRONTEND=noninteractive\n", + "\n", + "# Install gcloud SDK for downloading models from GCS\n", + "RUN apt-get update && \\\\\n", + " apt-get install -y apt-utils git apt-transport-https gnupg ca-certificates curl && \\\\\n", + " echo \"deb [signed-by=/usr/share/keyrings/cloud.google.gpg] https://packages.cloud.google.com/apt cloud-sdk main\" | tee -a /etc/apt/sources.list.d/google-cloud-sdk.list && \\\\\n", + " curl https://packages.cloud.google.com/apt/doc/apt-key.gpg | gpg --dearmor -o /usr/share/keyrings/cloud.google.gpg && \\\\\n", + " apt-get update -y && apt-get install google-cloud-cli -y && \\\\\n", + " rm -rf /var/lib/apt/lists/*\n", + "\n", + "WORKDIR /workspace/vllm\n", + "\n", + "# Copy entrypoint script\n", + "COPY ./entrypoint.sh /workspace/vllm/vertexai/entrypoint.sh\n", + "RUN chmod +x /workspace/vllm/vertexai/entrypoint.sh\n", + "\n", + "ENTRYPOINT [\"/workspace/vllm/vertexai/entrypoint.sh\"]\n", + "\"\"\"\n", + "\n", + "# Write Dockerfile\n", + "with open(f\"{build_dir}/Dockerfile\", \"w\") as f:\n", + " f.write(dockerfile)\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "IKCLLHOAK_Kf" + }, + "source": [ + "#### Create entrypoint.sh\n", + "\n", + "This script downloads models/adapters from GCS and starts vLLM.\n", + "\n", + "In particular, it intercepts vLLM arguments, detects GCS paths (gs://...), downloads those resources to local directories, rewrites the paths to point locally, then launches vLLM with the updated arguments. This happens transparently - vLLM never knows it's loading from GCS. The `set -euo pipefail` ensures the script fails fast if any download fails, preventing vLLM from starting with missing models." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "9sA-RI07LFE3" + }, + "outputs": [], + "source": [ + "entrypoint = \"\"\"#!/bin/bash\n", + "\n", + "set -euo pipefail\n", + "\n", + "readonly LOCAL_MODEL_DIR=\"/workspace/models\"\n", + "readonly LOCAL_ADAPTER_DIR=\"/workspace/adapters\"\n", + "\n", + "gcloud config set storage/parallel_composite_upload_enabled True\n", + "\n", + "download_from_gcs() {\n", + " gcs_uri=$1\n", + " local_dir=$2\n", + "\n", + " echo \"Downloading from $gcs_uri to $local_dir...\"\n", + " parent_dir=$(dirname \"$local_dir\")\n", + " mkdir -p \"$parent_dir\"\n", + "\n", + " # Download contents to parent, which creates the final directory\n", + " if gcloud storage cp -r \"$gcs_uri\" \"$parent_dir/\"; then\n", + " echo \"Downloaded successfully to ${local_dir}\"\n", + " else\n", + " echo \"Failed to download from: $gcs_uri\" >&2\n", + " exit 1\n", + " fi\n", + "}\n", + "\n", + "updated_args=()\n", + "for arg in \"$@\"; do\n", + " # Check if argument starts with --model= and points to gs://\n", + " if [[ $arg == --model=gs://* ]]; then\n", + " model_path=\"${arg#--model=}\"\n", + " base_model_name=$(basename \"$model_path\")\n", + " local_path=\"${LOCAL_MODEL_DIR}/${base_model_name}\"\n", + "\n", + " download_from_gcs \"$model_path\" \"$local_path\"\n", + " updated_args+=(\"--model=${local_path}\")\n", + "\n", + " # Check if argument is a LoRA module path with gs://\n", + " elif [[ $arg == *=gs://* && $arg != --model=* ]]; then\n", + " # Format: name=gs://path\n", + " adapter_name=\"${arg%%=*}\"\n", + " gcs_path=\"${arg#*=}\"\n", + " adapter_basename=$(basename \"$gcs_path\")\n", + " local_adapter_path=\"${LOCAL_ADAPTER_DIR}/${adapter_basename}\"\n", + "\n", + " download_from_gcs \"$gcs_path\" \"$local_adapter_path\"\n", + " updated_args+=(\"${adapter_name}=${local_adapter_path}\")\n", + " else\n", + " updated_args+=(\"$arg\")\n", + " fi\n", + "done\n", + "\n", + "echo \"Starting vLLM with arguments: ${updated_args[@]}\"\n", + "exec \"${updated_args[@]}\"\n", + "\"\"\"\n", + "\n", + "with open(f\"{build_dir}/entrypoint.sh\", \"w\") as f:\n", + " f.write(entrypoint)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "xNT1hGlILua9" + }, + "source": [ + "#### Create cloudbuild.yaml\n", + "\n", + "This file tells Cloud Build how to build your container:\n", + "- Uses the official vLLM GPU image as a base\n", + "- Adds custom entrypoint for Vertex AI compatibility\n", + "- Pushes the image to Artifact Registry" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "LSoRnzRALiyv" + }, + "outputs": [], + "source": [ + "cloudbuild = \"\"\"\n", + "steps:\n", + " - name: 'gcr.io/cloud-builders/docker'\n", + " automapSubstitutions: true\n", + " script: |\n", + " #!/usr/bin/env bash\n", + " set -euo pipefail\n", + "\n", + " base_image=${_BASE_IMAGE}\n", + " image_name=\"vllm-lora-gcs\"\n", + "\n", + " echo \"Building container image...\"\n", + " docker build -t $LOCATION-docker.pkg.dev/$PROJECT_ID/${_REPOSITORY}/$image_name --build-arg BASE_IMAGE=$base_image .\n", + "\n", + " echo \"Pushing image to Artifact Registry...\"\n", + " docker push $LOCATION-docker.pkg.dev/$PROJECT_ID/${_REPOSITORY}/$image_name\n", + "\n", + "substitutions:\n", + " _BASE_IMAGE: vllm/vllm-openai:v0.11.0\n", + " _REPOSITORY: {DOCKER_REPOSITORY}\n", + "\n", + "timeout: 1800s\n", + "\"\"\".replace(\n", + " \"{DOCKER_REPOSITORY}\", DOCKER_REPOSITORY\n", + ")\n", + "\n", + "with open(f\"{build_dir}/cloudbuild.yaml\", \"w\") as f:\n", + " f.write(cloudbuild)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "LIg0ZfuYMMr_" + }, + "source": [ + "### Build the Container\n", + "\n", + "Now let's build the container using Cloud Build. This process takes less than 10 minutes.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "bol28Mc2MRFn" + }, + "outputs": [], + "source": [ + "# Build the container using Cloud Build\n", + "!cd {build_dir} && \\\n", + "gcloud builds submit \\\n", + " --config=cloudbuild.yaml \\\n", + " --project={PROJECT_ID} \\\n", + " --region={LOCATION} \\\n", + " --timeout=\"1h\" \\\n", + " --machine-type=e2-highcpu-8 \\\n", + " --substitutions=_REPOSITORY={DOCKER_REPOSITORY},_BASE_IMAGE=vllm/vllm-openai:latest" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "nTxSX7ITMyjS" + }, + "source": [ + "Verify image exists in Artifact Registry\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "vvi9J4sfMz2P" + }, + "outputs": [], + "source": [ + "! gcloud artifacts docker images list {LOCATION}-docker.pkg.dev/{PROJECT_ID}/{DOCKER_REPOSITORY} --include-tags" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "4AKpi6eZjiKz" + }, + "source": [ + "## Configure and Upload Model to Vertex AI\n", + "\n", + "When you upload a model to Vertex AI, you're creating a **Model Resource** that contains:\n", + "- Container image location\n", + "- Server startup arguments\n", + "- Environment variables\n", + "- Health check configuration\n", + "- Resource requirements" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "RDpy0w_UkTE1" + }, + "source": [ + "#### Define model configuration and environment variables\n", + "\n", + "Below you have a description of some of the main parameters to deploy the model.\n", + "\n", + "| Argument | Purpose | Impact |\n", + "|----------|---------|--------|\n", + "| `--model=gs://...` | GCS path to base model | Downloaded at startup by entrypoint.sh |\n", + "| `--enable-lora` | Enables LoRA adapter support | **Required** for serving adapters |\n", + "| `--lora-modules name=gs://...` | Pre-load LoRA adapters from GCS | Downloaded and loaded at startup |\n", + "| `--max-loras=4` | Max adapters in memory simultaneously | Higher = more adapters, more memory usage |\n", + "| `--max-lora-rank=64` | Max rank of LoRA matrices | Must match or exceed your adapter ranks |\n", + "| `--max-model-len=2048` | Max sequence length (tokens) | Lower = more memory for batch processing |\n", + "| `--gpu-memory-utilization=0.9` | GPU memory to use | 0.9 leaves 10% buffer for safety |\n", + "| `--enable-prefix-caching` | Cache common prefixes | Improves latency for similar requests |\n", + "\n", + "\n", + "> **Note**: The L4 GPU has 24GB VRAM. With our settings: ~5GB for the base model, ~8GB for KV cache, ~50MB per LoRA adapter, ~2GB CUDA overhead = ~15GB used, leaving ~9GB buffer. If you increase `--max-model-len` or `--max-loras`, you may need to reduce `--gpu-memory-utilization` to avoid OOM errors." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "sj8IaNpTjuGL" + }, + "outputs": [], + "source": [ + "# Model configuration\n", + "MODEL_NAME = \"gemma-2-2b-multi-lora\"\n", + "MACHINE_TYPE = \"g2-standard-8\"\n", + "ACCELERATOR_TYPE = \"NVIDIA_L4\"\n", + "ACCELERATOR_COUNT = 1\n", + "DOCKER_URI = f\"{LOCATION}-docker.pkg.dev/{PROJECT_ID}/{DOCKER_REPOSITORY}/vllm-lora-gcs\"\n", + "\n", + "# vLLM server arguments - using GCS paths (downloaded by entrypoint.sh at startup)\n", + "vllm_args = [\n", + " \"python3\",\n", + " \"-m\",\n", + " \"vllm.entrypoints.openai.api_server\",\n", + " \"--host=0.0.0.0\",\n", + " \"--port=8080\",\n", + " f\"--model={BUCKET_URI}/models/gemma-2-2b-it\", # GCS path\n", + " \"--max-model-len=2048\",\n", + " \"--gpu-memory-utilization=0.9\",\n", + " \"--enable-lora\", # CRITICAL: Enable LoRA support\n", + " \"--max-loras=4\", # Allow up to 4 LoRA adapters in memory\n", + " \"--max-lora-rank=64\",\n", + " \"--enable-prefix-caching\",\n", + " f\"--tensor-parallel-size={ACCELERATOR_COUNT}\",\n", + " # Load LoRA adapters at startup from GCS\n", + " \"--lora-modules\",\n", + " f\"sql-lora={BUCKET_URI}/adapters/sql-lora\",\n", + " f\"magicoder-lora={BUCKET_URI}/adapters/magicoder-lora\",\n", + "]\n", + "\n", + "# Environment variables for the container\n", + "env_vars = {\n", + " \"LD_LIBRARY_PATH\": \"$LD_LIBRARY_PATH:/usr/local/nvidia/lib64\", # NVIDIA libraries\n", + "}" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "21vlLwHPk5Fl" + }, + "source": [ + "#### Upload Model to Model Registry\n", + "\n", + "We are now ready to register the model in Vertex AI Model Registry, a managed model repository to version your models.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "207GTbAuk5YA" + }, + "outputs": [], + "source": [ + "vertexai_model = aiplatform.Model.upload(\n", + " display_name=MODEL_NAME,\n", + " serving_container_image_uri=DOCKER_URI,\n", + " serving_container_args=vllm_args,\n", + " serving_container_ports=[8080],\n", + " serving_container_predict_route=\"/v1/completions\",\n", + " serving_container_health_route=\"/health\",\n", + " serving_container_environment_variables=env_vars,\n", + " serving_container_shared_memory_size_mb=(16 * 1024), # 16 GB shared memory\n", + " serving_container_deployment_timeout=1800, # 30 minutes timeout\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "U7fGOjeslSd4" + }, + "source": [ + "## Create Vertex AI Endpoint\n", + "\n", + "An **Endpoint** in Vertex AI is a stable URL for making predictions. You can host one or more deployed models, it handles load balancing and traffic splitting and provides monitoring and logging.\n", + "\n", + "Let's create a new endpoint to deploy our models.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "zydF5Kr7lZjS" + }, + "outputs": [], + "source": [ + "vertexai_endpoint = aiplatform.Endpoint.create(\n", + " display_name=f\"{MODEL_NAME}-endpoint\"\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "8CN7dXmBlld6" + }, + "source": [ + "## Deploy Model to Endpoint\n", + "\n", + "Finally, let's deploy our models. Vertex AI provisions a VM with the specified GPU, pulls your Docker image from Artifact Registry, starts the container, and monitors health checks. Your entrypoint.sh script downloads ~5.1 GB of model files from GCS (fast because it's in the same region), then vLLM loads them into GPU memory and starts the OpenAI-compatible API server on port 8080. The deployment completes when the health check endpoint (`/health`) returns 200 OK.\n", + "\n", + "This would take 15-25 minutes.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "1oR9w-uplzNR" + }, + "outputs": [], + "source": [ + "vertexai_model.deploy(\n", + " endpoint=vertexai_endpoint,\n", + " deployed_model_display_name=MODEL_NAME,\n", + " machine_type=MACHINE_TYPE,\n", + " accelerator_type=ACCELERATOR_TYPE,\n", + " accelerator_count=ACCELERATOR_COUNT,\n", + " min_replica_count=1, # Minimum number of instances\n", + " max_replica_count=4, # Maximum for autoscaling\n", + " autoscaling_target_accelerator_duty_cycle=60, # Scale up at 60% GPU utilization\n", + " traffic_percentage=100, # Route 100% of traffic to this model\n", + " deploy_request_timeout=1800, # 30 minute timeout\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "lVIhUgoQmP5Q" + }, + "source": [ + "## Testing Your Deployment\n", + "\n", + "Now for the fun part - testing your multi-LoRA deployment." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "a7bUjVZ4mFtO" + }, + "source": [ + "### Test 1: Base Model (No Adapter)\n", + "\n", + "Let's first test the base Gemma model without any adapter.\n", + "\n", + "Without a LoRA adapter, you're using the pure Gemma-2-2b-it model. It will provide general conversational responses. This serves as a baseline - when you specify a LoRA adapter in subsequent tests, you'll see how the model's behavior changes for specialized tasks." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "QpK20fOpmWkm" + }, + "outputs": [], + "source": [ + "# Test base model without adapter\n", + "prompt = \"What is machine learning?\"\n", + "\n", + "request_body = json.dumps({\n", + " \"prompt\": prompt,\n", + " \"max_tokens\": 100,\n", + " \"temperature\": 0.7,\n", + "})\n", + "\n", + "response = vertexai_endpoint.raw_predict(\n", + " body=request_body,\n", + " headers={\"Content-Type\": \"application/json\"}\n", + ")\n", + "\n", + "if response.status_code == 200:\n", + " result = json.loads(response.text)\n", + " generated_text = result[\"choices\"][0][\"text\"]\n", + " print(f\"Response:\\n{generated_text}\\n\")\n", + "else:\n", + " print(f\"Error: {response.status_code}\")\n", + " print(response.text)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "Ymb9YgWRngj7" + }, + "source": [ + "### Test 2: SQL Adapter\n", + "\n", + "Now let's test the SQL adapter for database query generation.\n", + "\n", + "Notice we're adding `\"model\": \"sql-lora\"` to the request body. This tells vLLM to apply the SQL LoRA adapter on top of the base model. The adapter was trained specifically for text-to-SQL tasks, so it should generate syntactically correct SQL that accurately answers the question. We also set `temperature=0.0` for deterministic output - SQL queries shouldn't be creative." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "kn6UZ9tQniNt" + }, + "outputs": [], + "source": [ + "# Test SQL LoRA adapter\n", + "prompt = \"\"\"Write a SQL query to answer the question based on the table schema.\n", + "\n", + "context: CREATE TABLE employees (\n", + " id INT PRIMARY KEY,\n", + " name VARCHAR(100),\n", + " department VARCHAR(50),\n", + " salary DECIMAL(10,2),\n", + " hire_date DATE\n", + ")\n", + "\n", + "question: What is the average salary of employees in the Engineering department?\n", + "\n", + "SQL query:\"\"\"\n", + "\n", + "request_body = json.dumps({\n", + " \"model\": \"sql-lora\", # Specify the adapter\n", + " \"prompt\": prompt,\n", + " \"max_tokens\": 150,\n", + " \"temperature\": 0.0, # Use 0 for deterministic SQL generation\n", + " # \"stop\": [\";\", \"\\n\\n\"] # Stop at query end\n", + "})\n", + "\n", + "response = vertexai_endpoint.raw_predict(\n", + " body=request_body,\n", + " headers={\"Content-Type\": \"application/json\"}\n", + ")\n", + "\n", + "if response.status_code == 200:\n", + " result = json.loads(response.text)\n", + " generated_sql = result[\"choices\"][0][\"text\"]\n", + " print(f\"Generated SQL:\\n{generated_sql}\\n\")\n", + "else:\n", + " print(f\"Error: {response.status_code}\")\n", + " print(response.text)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "OwiFd1KcntJ5" + }, + "source": [ + "### Test 3: Magicoder Adapter\n", + "\n", + "Test the code generation adapter." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "lTFaz4O4nuTD" + }, + "outputs": [], + "source": [ + "# Test Magicoder LoRA adapter for code generation\n", + "prompt = \"\"\"Write a Python function to count to 10\"\"\"\n", + "\n", + "request_body = json.dumps({\n", + " \"model\": \"magicoder-lora\", # Specify the adapter\n", + " \"prompt\": prompt,\n", + " \"max_tokens\": 200,\n", + " \"temperature\": 0.2,\n", + " # \"stop\": [\"\\n\\n\", \"# Example\", \"# Test\"]\n", + "})\n", + "\n", + "response = vertexai_endpoint.raw_predict(\n", + " body=request_body,\n", + " headers={\"Content-Type\": \"application/json\"}\n", + ")\n", + "\n", + "if response.status_code == 200:\n", + " result = json.loads(response.text)\n", + " generated_code = result[\"choices\"][0][\"text\"]\n", + " print(f\"Generated Code:\\n{generated_code}\\n\")\n", + "else:\n", + " print(f\"Error: {response.status_code}\")\n", + " print(response.text)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "0RhJi_SLglqq" + }, + "source": [ + "## Advanced Usage Patterns" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "n9Oy75AQg3gV" + }, + "source": [ + "### Pattern 1: Batch Processing with Different Adapters\n", + "\n", + "Process multiple requests with different adapters in parallel using the Vertex AI SDK." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "u3fettbig22j" + }, + "outputs": [], + "source": [ + "import json\n", + "from concurrent.futures import ThreadPoolExecutor, as_completed\n", + "\n", + "def batch_predict_multi_adapter(endpoint: aiplatform.Endpoint, requests: list):\n", + " \"\"\"\n", + " Send multiple requests to different adapters in parallel.\n", + "\n", + " Args:\n", + " endpoint: The Vertex AI endpoint object\n", + " requests: List of (model_name, prompt) tuples\n", + "\n", + " Returns:\n", + " List of (model_name, response_text) tuples\n", + " \"\"\"\n", + " def single_predict(model_name, prompt):\n", + " request_body = {\n", + " \"prompt\": prompt,\n", + " \"max_tokens\": 300,\n", + " \"temperature\": 0.7,\n", + " }\n", + "\n", + " if model_name:\n", + " request_body[\"model\"] = model_name\n", + "\n", + " response = endpoint.raw_predict(\n", + " body=json.dumps(request_body),\n", + " headers={\"Content-Type\": \"application/json\"}\n", + " )\n", + "\n", + " if response.status_code == 200:\n", + " result = json.loads(response.text)\n", + " return (model_name or \"base\", result[\"choices\"][0][\"text\"])\n", + " else:\n", + " return (model_name or \"base\", f\"Error: {response.status_code}\")\n", + "\n", + " # Execute requests in parallel\n", + " results = []\n", + " with ThreadPoolExecutor(max_workers=4) as executor:\n", + " futures = [executor.submit(single_predict, model_name, prompt)\n", + " for model_name, prompt in requests]\n", + "\n", + " for future in as_completed(futures):\n", + " results.append(future.result())\n", + "\n", + " return results\n", + "\n", + "# Example usage\n", + "requests = [\n", + " (None, \"What is Python?\"),\n", + " (\"sql-lora\", \"Generate SQL: SELECT all users\"),\n", + " (\"magicoder-lora\", \"Write a function to reverse a string\"),\n", + " (\"sql-lora\", \"Generate SQL: JOIN users and orders\"),\n", + "]\n", + "\n", + "results = batch_predict_multi_adapter(vertexai_endpoint, requests)\n", + "\n", + "for model_name, response in results:\n", + " print(f\"\\n{model_name}:\")\n", + " print(f\" {response}...\")\n", + " print(\"=\"*50)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "-p5AVRTZj8s4" + }, + "source": [ + "### Pattern 2: Longer Generation with Streaming\n", + "\n", + "Although Vertex AI supports `stream_raw_predict` method, Streaming responses require additional server-side configuration in the vLLM container. For production streaming needs, consider using the vLLM server's native OpenAI-compatible streaming endpoint directly with appropriate authentication." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "HAALLPfyq9m5" + }, + "source": [ + "### Pattern 3: Dynamic Adapter Loading\n", + "\n", + "Dynamic adapter loading at runtime is an advanced feature that requires:\n", + "1. Setting `VLLM_ALLOW_RUNTIME_LORA_UPDATING=True` in environment variables\n", + "2. Redeploying the model with this configuration\n", + "3. Direct access to vLLM's API endpoints (not available through Vertex AI raw_predict)\n", + "\n", + "For this tutorial's setup, **adapters are pre-loaded at startup** via the `--lora-modules` flag. To add new adapters, you would:\n", + "\n", + "```python\n", + "# Steps to add a new adapter (requires redeployment):\n", + "\n", + "# 1. Upload new adapter to GCS\n", + "# !gcloud storage cp -r ./adapters/new-adapter gs://{BUCKET_NAME}/adapters/new-adapter\n", + "\n", + "# 2. Update vLLM args to include the new adapter\n", + "new_vllm_args = [\n", + " \"python3\", \"-m\", \"vllm.entrypoints.openai.api_server\",\n", + " \"--host=0.0.0.0\", \"--port=8080\",\n", + " f\"--model={BUCKET_URI}/models/gemma-2-2b-it\",\n", + " \"--max-model-len=2048\",\n", + " \"--gpu-memory-utilization=0.9\",\n", + " \"--enable-lora\",\n", + " \"--max-loras=4\",\n", + " \"--max-lora-rank=64\",\n", + " \"--enable-prefix-caching\",\n", + " \"--tensor-parallel-size=1\",\n", + " \"--lora-modules\",\n", + " f\"sql-lora={BUCKET_URI}/adapters/sql-lora\",\n", + " f\"magicoder-lora={BUCKET_URI}/adapters/magicoder-lora\",\n", + " f\"new-adapter={BUCKET_URI}/adapters/new-adapter\", # New adapter\n", + "]\n", + "\n", + "# 3. Upload new model version with updated args\n", + "# 4. Redeploy to endpoint\n", + "```\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "2a4e033321ad" + }, + "source": [ + "## Cleaning up\n", + "\n", + "To avoid extra charges, don't forget to delete your resources.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "V9B5RP04sMyX" + }, + "outputs": [], + "source": [ + "delete_endpoint = True\n", + "delete_model = True\n", + "delete_docker_repo = True\n", + "delete_bucket = True\n", + "\n", + "\n", + "if delete_endpoint and \"vertexai_endpoint\" in globals():\n", + " vertexai_endpoint.undeploy_all()\n", + " vertexai_endpoint.delete()\n", + " print(\"Endpoint deleted.\")\n", + "\n", + "\n", + "if delete_model and \"vertexai_model\" in globals():\n", + " vertexai_model.delete()\n", + " print(\"Model deleted.\")\n", + "\n", + "if delete_docker_repo:\n", + " !gcloud artifacts repositories delete {DOCKER_REPOSITORY} \\\n", + " --location={LOCATION} \\\n", + " --quiet\n", + "\n", + "if delete_bucket:\n", + " !gcloud storage rm --recursive {BUCKET_URI} && gcloud storage buckets delete {BUCKET_URI} --quiet" + ] + } + ], + "metadata": { + "colab": { + "name": "get_started_with_vllm_lora_serving_on_vertex_ai.ipynb", + "toc_visible": true + }, + "kernelspec": { + "display_name": "Python 3", + "name": "python3" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} diff --git a/open-models/serving/vertex_ai_ollama_gemma2_rag_agent.ipynb b/open-models/serving/vertex_ai_ollama_gemma2_rag_agent.ipynb new file mode 100644 index 0000000..f9c43ee --- /dev/null +++ b/open-models/serving/vertex_ai_ollama_gemma2_rag_agent.ipynb @@ -0,0 +1,1645 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "ur8xi4C7S06n" + }, + "outputs": [], + "source": [ + "# Copyright 2024 Google LLC\n", + "#\n", + "# Licensed under the Apache License, Version 2.0 (the \"License\");\n", + "# you may not use this file except in compliance with the License.\n", + "# You may obtain a copy of the License at\n", + "#\n", + "# https://www.apache.org/licenses/LICENSE-2.0\n", + "#\n", + "# Unless required by applicable law or agreed to in writing, software\n", + "# distributed under the License is distributed on an \"AS IS\" BASIS,\n", + "# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n", + "# See the License for the specific language governing permissions and\n", + "# limitations under the License." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "JAPoU8Sm5E6e" + }, + "source": [ + "# Running a Gemma 2-based agentic RAG with Ollama on Vertex AI and LangGraph\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + "
    \n", + " \n", + " \"Google
    Open in Colab\n", + "
    \n", + "
    \n", + " \n", + " \"Google
    Open in Colab Enterprise\n", + "
    \n", + "
    \n", + " \n", + " \"Vertex
    Open in Vertex AI Workbench\n", + "
    \n", + "
    \n", + " \n", + " \"GitHub
    View on GitHub\n", + "
    \n", + "
    \n", + "\n", + "
    \n", + "\n", + "Share to:\n", + "\n", + "\n", + " \"LinkedIn\n", + "\n", + "\n", + "\n", + " \"Bluesky\n", + "\n", + "\n", + "\n", + " \"X\n", + "\n", + "\n", + "\n", + " \"Reddit\n", + "\n", + "\n", + "\n", + " \"Facebook\n", + "" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "84f0f73a0f76" + }, + "source": [ + "| | |\n", + "|-|-|\n", + "| Author(s) | [Ivan Nardini](https://github.com/inardini) |" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "tvgnzT1CKxrO" + }, + "source": [ + "## Overview\n", + "\n", + "> [**Gemma 2**](https://ai.google.dev/gemma) is a new generation of open models developed by Google. It offers pre-trained and instruction-tuned variants in two sizes (2B, 7B and 9b parameters), designed for high performance and efficiency on various hardware. Gemma 2 models are available through platforms like Google AI Studio, Kaggle, and Hugging Face.\n", + "\n", + "> [**Ollama**](https://github.com/ollama/ollama) is a tool for running open-source large language models (LLMs) locally. It simplifies LLM usage by bundling model weights, configurations, and datasets into a single package managed by a [`Modelfile`](https://github.com/ollama/ollama/blob/main/docs/modelfile.md). Ollama supports various models like LLaMA-2, Mistral, and CodeLLaMA, and is compatible with macOS and Linux.\n", + "\n", + "> [**LangGraph**](https://python.langchain.com/en/latest/modules/graphs/langgraph.html) is a framework developed by LangChain for building applications with complex workflows, including agents and multi-agent systems. It offers precise control over application flow and state, supporting cyclical graphs and advanced state management. LangGraph enhances LangChain's capabilities, providing more flexibility for agentic applications.\n", + "\n", + "> [**Google Vertex AI**](https://cloud.google.com/vertex-ai) is Google Cloud's unified machine learning (ML) platform. It provides a comprehensive suite of tools for building, training, deploying, and managing ML models and AI applications, including large language models (LLMs). Vertex AI streamlines the entire ML workflow, from data management to prediction, and supports customization for specific business needs.\n", + "\n", + "This notebook showcases how to run a Gemma 2-based Agent with Ollama on Vertex AI and LangGraph.\n", + "\n", + "By the end of this notebook, you will learn how to:\n", + "\n", + "- Deploy Google Gemma 2 on Vertex AI using Ollama\n", + "- Learn how to test the container using Vertex AI LocalModel class\n", + "- Implement a simple RAG agent application with Gemma 2 and Ollama using LangGraph" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "61RBz8LLbxCR" + }, + "source": [ + "## Get started" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "No17Cw5hgx12" + }, + "source": [ + "### Install Vertex AI SDK and other required packages\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "tFy3H3aPgx12" + }, + "outputs": [], + "source": [ + "%pip install --upgrade --user --quiet \"huggingface_hub\" \\\n", + " \"google-cloud-aiplatform[prediction]\" \\\n", + " \"torch\" \\\n", + " \"etils\" \\\n", + " \"crcmod\"" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "R5Xep4W9lq-Z" + }, + "source": [ + "### Restart runtime\n", + "\n", + "To use the newly installed packages in this Jupyter runtime, you must restart the runtime. You can do this by running the cell below, which restarts the current kernel.\n", + "\n", + "The restart might take a minute or longer. After it's restarted, continue to the next step." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "XRvKdaPDTznN" + }, + "outputs": [], + "source": [ + "import IPython\n", + "\n", + "app = IPython.Application.instance()\n", + "app.kernel.do_shutdown(True)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "SbmM4z7FOBpM" + }, + "source": [ + "
    \n", + "⚠ The kernel is going to restart. In Colab or Colab Enterprise, you might see an error message that says \"Your session crashed for an unknown reason.\" This is expected. Wait until it's finished before continuing to the next step. ⚠\n", + "
    \n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "dmWOrTJ3gx13" + }, + "source": [ + "### Authenticate your notebook environment (Colab only)\n", + "\n", + "If you're running this notebook on Google Colab, run the cell below to authenticate your environment." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "NyKGtVQjgx13" + }, + "outputs": [], + "source": [ + "# import sys\n", + "\n", + "# if \"google.colab\" in sys.modules:\n", + "# from google.colab import auth\n", + "\n", + "# auth.authenticate_user()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "ee37e1544281" + }, + "source": [ + "### Requirements" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "877cd3fb2dce" + }, + "source": [ + "You will need to have the following IAM roles set:\n", + "\n", + "- Artifact Registry Administrator (`roles/artifactregistry.admin`)\n", + "- Cloud Build Editor (`roles/cloudbuild.builds.editor`)\n", + "- Vertex AI User (`roles/aiplatform.user`)\n", + "- Service Account User (`roles/iam.serviceAccountUser`)\n", + "- Service Usage Consumer (`roles/serviceusage.serviceUsageConsumer`)\n", + "- Storage Admin (`roles/storage.admin`)\n", + "\n", + "For more information about granting roles, see [Manage access](https://cloud.google.com/iam/docs/granting-changing-revoking-access).\n", + "\n", + "---\n", + "\n", + "You will also need to enable the following APIs (if not enabled already):\n", + "\n", + "- Artifact Registry API (artifactregistry.googleapis.com)\n", + "- Vertex AI API (aiplatform.googleapis.com)\n", + "- Compute Engine API (compute.googleapis.com)\n", + "\n", + "For more information about API enablement, see [Enabling APIs](https://cloud.google.com/apis/docs/getting-started#enabling_apis)." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "f9af3e57f89a" + }, + "source": [ + "### Authenticate your Hugging Face account\n", + "\n", + "As [`google/gemma-2b-it`](https://huggingface.co/google/gemma-2-2b-it) is a gated model, you need to have a Hugging Face Hub account, and accept the Google's usage license for Gemma. Once that's done, you need to generate a new user access token with read-only access so that the weights can be downloaded from the Hub.\n", + "\n", + "> Note that the user access token can only be generated via [the Hugging Face Hub UI](https://huggingface.co/settings/tokens/new), where you can either select read-only access to your account, or follow the recommendations and generate a fine-grained token with read-only access to [`google/gemma-9b-it`](https://huggingface.co/google/gemma-2-2b-it)." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "4c31c7272804" + }, + "source": [ + "Then you can install the `huggingface_hub` that comes with a CLI that will be used for the authentication with the token generated in advance. So that then the token can be safely retrieved via `huggingface_hub.get_token`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "8d836e0210fe" + }, + "outputs": [], + "source": [ + "from huggingface_hub import interpreter_login\n", + "\n", + "interpreter_login()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "c71a4314c250" + }, + "source": [ + "Read more about [Hugging Face Security](https://huggingface.co/docs/hub/en/security), specifically about [Hugging Face User Access Tokens](https://huggingface.co/docs/hub/en/security-tokens)." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "DF4l8DTdWgPY" + }, + "source": [ + "### Set Google Cloud project information and initialize Vertex AI SDK\n", + "\n", + "To get started using Vertex AI, you must have an existing Google Cloud project and [enable the Vertex AI API](https://console.cloud.google.com/flows/enableapi?apiid=aiplatform.googleapis.com).\n", + "\n", + "Learn more about [setting up a project and a development environment](https://cloud.google.com/vertex-ai/docs/start/cloud-environment)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "Nqwi-5ufWp_B" + }, + "outputs": [], + "source": [ + "# Use the environment variable if the user doesn't provide Project ID.\n", + "import os\n", + "\n", + "import vertexai\n", + "\n", + "PROJECT_ID = \"[your-project-id]\" # @param {type: \"string\", placeholder: \"[your-project-id]\", isTemplate: true}\n", + "\n", + "if not PROJECT_ID or PROJECT_ID == \"[your-project-id]\":\n", + " PROJECT_ID = str(os.environ.get(\"GOOGLE_CLOUD_PROJECT\"))\n", + "\n", + "LOCATION = os.environ.get(\"GOOGLE_CLOUD_REGION\", \"us-central1\")\n", + "\n", + "BUCKET_NAME = \"[your-bucket-name]\" # @param {type: \"string\", placeholder: \"[your-bucket-name]\", isTemplate: true}\n", + "\n", + "if not BUCKET_NAME or BUCKET_NAME == \"[your-bucket-name]\":\n", + " BUCKET_NAME = f\"{PROJECT_ID}-bucket\"\n", + "\n", + "BUCKET_URI = f\"gs://{BUCKET_NAME}\"\n", + "\n", + "! gsutil mb -p $PROJECT_ID -l $LOCATION $BUCKET_URI\n", + "\n", + "vertexai.init(project=PROJECT_ID, location=LOCATION, staging_bucket=BUCKET_URI)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "BZGpwahJBkeo" + }, + "source": [ + "### Set tutorial folder\n", + "\n", + "Define a folder for the tutorial." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "XhFT7nQUBm23" + }, + "outputs": [], + "source": [ + "from etils import epath\n", + "\n", + "TUTORIAL_DIR = epath.Path(\"ollama_on_vertex_ai_tutorial\")\n", + "BUILD_DIR = TUTORIAL_DIR / \"build\"\n", + "MODELS_DIR = BUILD_DIR / \"ollama_models\"\n", + "\n", + "MODELS_DIR.mkdir(exist_ok=True, parents=True)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "5303c05f7aa6" + }, + "source": [ + "### Import libraries\n", + "\n", + "Import main libraries." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "6fc324893334" + }, + "outputs": [], + "source": [ + "import gc\n", + "import json\n", + "\n", + "from google.cloud import aiplatform\n", + "from google.cloud.aiplatform import Endpoint, Model\n", + "from google.cloud.aiplatform.prediction import LocalModel\n", + "from huggingface_hub import get_token, snapshot_download\n", + "import torch" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "4-HLop9sq-BA" + }, + "source": [ + "### Libraries settings" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "cNF1FIlDrCtJ" + }, + "outputs": [], + "source": [ + "os.environ[\"HF_HUB_ENABLE_HF_TRANSFER\"] = \"1\"" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "E4rjOknk5yvq" + }, + "source": [ + "### Helpers\n", + "\n", + "Define some helpers." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "MOoaB5xf51BD" + }, + "outputs": [], + "source": [ + "def get_cuda_device_names():\n", + " \"\"\"A function to get the list of NVIDIA GPUs\"\"\"\n", + " if not torch.cuda.is_available():\n", + " return None\n", + "\n", + " return [str(i) for i in range(torch.cuda.device_count())]\n", + "\n", + "\n", + "def empty_gpu_ram():\n", + " gc.collect()\n", + " torch.cuda.empty_cache()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "njDb6J4FQihU" + }, + "source": [ + "## Deploy Gemma 2 using Ollama on Vertex AI Prediction\n", + "\n", + "To deploy Gemma 2 as an Ollama model on Vertex AI Prediction, a custom container with the Ollama server and the Gemma 2 model is required. You can use Cloud Build, a serverless CI/CD platform, to build the serving container image." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "-7QAx-XzRUmW" + }, + "source": [ + "### Download Gemma 2 from Hugging Face Hub\n", + "\n", + "Download `google-cloud-partnership/gemma-2-2b-it-lora-sql` , a Gemma 2 adapter which allows you to handle both SQL user requests using Gemma 2." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "hws_NxS-RZQR" + }, + "outputs": [], + "source": [ + "base_model_id = \"google-cloud-partnership/gemma-2-2b-it-lora-sql\"\n", + "model_dir = MODELS_DIR / \"gemma-2-2b-it-lora-sql\"\n", + "\n", + "ignore_patterns = [\".gitattributes\", \".gitkeep\", \"*.md\"]\n", + "\n", + "snapshot_download(\n", + " repo_id=base_model_id,\n", + " token=get_token(),\n", + " local_dir=model_dir,\n", + " local_dir_use_symlinks=False,\n", + " ignore_patterns=ignore_patterns,\n", + ")\n", + "\n", + "! rm -rf $model_dir/.cache" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "IprOEAAN1sBQ" + }, + "source": [ + "### Create Artifact Registry repository\n", + "\n", + "To build a container, create a repository in Google Cloud Artifact Registry." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "p5hXDtoYsCEB" + }, + "outputs": [], + "source": [ + "REPOSITORY_NAME = \"ollama-gemma-on-vertex\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "z1ZBM9PDrbdM" + }, + "outputs": [], + "source": [ + "!gcloud artifacts repositories create $REPOSITORY_NAME \\\n", + " --repository-format=docker \\\n", + " --location=$LOCATION \\\n", + " --project=$PROJECT_ID" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "IDMpuXEu2thu" + }, + "source": [ + "### Create a Dockerfile\n", + "\n", + "Use the following Dockerfile to define the container's build steps. The Dockerfile installs Python and Flask, sets environment variables, copies Ollama model files, exposes ports, and runs the Ollama model and a proxy server.\n", + "\n", + "> In this scenario, both Ollama and Fast API in the same container for simplicity." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "Vi9T53CScWdn" + }, + "outputs": [], + "source": [ + "dockerfile = \"\"\"\n", + "# Use multi-stage build for a smaller final image\n", + "FROM ollama/ollama:0.5.5\n", + "\n", + "# Install Python and FastAPI\n", + "RUN apt-get update && \\\n", + " apt-get install -y python3 python3-pip curl && \\\n", + " pip3 install fastapi uvicorn httpx asyncio\n", + "\n", + "# Set build-time arguments for better flexibility\n", + "ARG OLLAMA_PORT=8079\n", + "ARG SERVING_PORT=8080\n", + "\n", + "# Set environment variables\n", + "ENV OLLAMA_HOST=0.0.0.0:${OLLAMA_PORT} \\\n", + " OLLAMA_MODELS=/ollama_models \\\n", + " OLLAMA_KEEP_ALIVE=-1 \\\n", + " OLLAMA_DEBUG=false\n", + "\n", + "# Copy model files\n", + "COPY ./ollama_models /ollama_models\n", + "COPY gemma-2-2b-it-lora-sql.modelfile .\n", + "\n", + "# Expose ollama port\n", + "EXPOSE ${OLLAMA_PORT}\n", + "\n", + "# Create model in a proper way with health check\n", + "RUN ollama serve & \\\n", + " sleep 5 && ollama create gemma-2-2b-it-lora-sql-2b -f gemma-2-2b-it-lora-sql.modelfile\n", + "\n", + "# Expose port\n", + "EXPOSE ${SERVING_PORT}\n", + "\n", + "# Copy the proxy server code and entrypoint script\n", + "COPY main.py .\n", + "COPY entrypoint.sh .\n", + "\n", + "# Run the proxy server\n", + "RUN chmod +x ./entrypoint.sh\n", + "ENTRYPOINT [\"./entrypoint.sh\"]\n", + "\"\"\"\n", + "\n", + "with BUILD_DIR.joinpath(\"Dockerfile\").open(\"w\") as f:\n", + " f.write(dockerfile)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "IPPxybcDXNvn" + }, + "source": [ + "### Create Modelfile\n", + "\n", + "Define an Ollama Modelfile which is the configuration file Ollama needs to define and use the Gemma 2 adapter model." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "YbHAP5gxXQjM" + }, + "outputs": [], + "source": [ + "modelfile = \"\"\"FROM gemma2:2b\n", + "ADAPTER ollama_models/gemma-2-2b-it-lora-sql\n", + "\"\"\"\n", + "\n", + "with BUILD_DIR.joinpath(\"gemma-2-2b-it-lora-sql.modelfile\").open(\"w\") as f:\n", + " f.write(modelfile)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "ZLUVwtidieQq" + }, + "source": [ + "### Serve engine proxy\n", + "\n", + "This FastAPI application serves as a proxy between Vertex AI Endpoint and a local Ollama model server.\n", + "\n", + "It receives prediction requests from Vertex AI, forwards them to Ollama, and returns the responses to Vertex AI in a standardized format. The application also includes health checks, request validation, error handling, and asynchronous API calls.\n", + "\n", + "> In this scenario, the FastAPI application only maps the `generate` API." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "OyfS_j3fiiFv" + }, + "outputs": [], + "source": [ + "app_module = \"\"\"\n", + "'''\n", + "FastAPI proxy for Vertex AI Endpoint running Ollama.\n", + "'''\n", + "\n", + "import os\n", + "from typing import Any, Dict, List, Optional\n", + "from pydantic import BaseModel, Field\n", + "import httpx\n", + "from fastapi import FastAPI, HTTPException, status\n", + "from fastapi.responses import JSONResponse\n", + "import asyncio\n", + "\n", + "\n", + "# Configuration\n", + "class PredictionRequest(BaseModel):\n", + " '''Request model for predictions'''\n", + " instances: List[Dict] = Field(..., description=\"List of prediction instances\")\n", + "\n", + "class PredictionResponse(BaseModel):\n", + " '''Response model for predictions'''\n", + " predictions: List[str] = Field(..., description=\"List of model responses\")\n", + "\n", + "class Config:\n", + " '''Application configuration.'''\n", + " HEALTH_ROUTE: str = os.environ.get('AIP_HEALTH_ROUTE', '/health')\n", + " PREDICT_ROUTE: str = os.environ.get('AIP_PREDICT_ROUTE', '/predict')\n", + " PORT: int = int(os.environ.get('AIP_HTTP_PORT', '8080'))\n", + " OLLAMA_URL: str = os.environ.get('OLLAMA_URL', 'http://localhost:8079')\n", + " MODEL_NAME: str = os.environ.get('MODEL_NAME', 'gemma-2-2b-it-lora-sql-2b')\n", + " TIMEOUT: int = int(os.environ.get('TIMEOUT_SECONDS', '30'))\n", + "\n", + "# Helper function\n", + "async def ollama_generate(prompt: str, parameters: Dict['str', Any]) -> str:\n", + " '''\n", + " Make a prediction using the Ollama model.\n", + " '''\n", + " async with httpx.AsyncClient(timeout=Config.TIMEOUT) as client:\n", + " try:\n", + " response = await client.post(\n", + " f\"{Config.OLLAMA_URL}/api/generate\",\n", + " json={\n", + " \"prompt\": prompt,\n", + " \"stream\": False,\n", + " \"options\": parameters,\n", + " \"model\": Config.MODEL_NAME\n", + " }\n", + " )\n", + " response.raise_for_status()\n", + " return response.json()[\"response\"]\n", + "\n", + " except httpx.HTTPError as e:\n", + " raise HTTPException(\n", + " status_code=status.HTTP_502_BAD_GATEWAY,\n", + " detail=f\"Error calling Ollama: {str(e)}\"\n", + " )\n", + "# Application\n", + "app = FastAPI(\n", + " title=\"Ollama Vertex AI Proxy\",\n", + " description=\"A proxy service to run Ollama models on Vertex AI\"\n", + ")\n", + "\n", + "@app.get(\n", + " Config.HEALTH_ROUTE,\n", + " response_model=Dict[str, str],\n", + " description=\"Health check endpoint\",\n", + ")\n", + "async def health() -> Dict[str, str]:\n", + " '''Check if the service is healthy.'''\n", + " return {'status': 'healthy'}\n", + "\n", + "@app.post(\n", + " Config.PREDICT_ROUTE,\n", + " response_model=PredictionResponse,\n", + " description=\"Make predictions using the Ollama model\",\n", + ")\n", + "async def predict(request: PredictionRequest) -> PredictionResponse:\n", + " '''Process predictions using the Ollama model concurrently.'''\n", + "\n", + " if not request.instances:\n", + " raise HTTPException(\n", + " status_code=status.HTTP_400_BAD_REQUEST,\n", + " detail=\"No instances provided in request\"\n", + " )\n", + "\n", + " try:\n", + " # Process all prompts concurrently\n", + " tasks = []\n", + " for instance in request.instances:\n", + " prompt = instance.get('inputs', '')\n", + " parameters = instance.get('parameters', {})\n", + " tasks.append(ollama_generate(prompt, parameters))\n", + " \n", + " # Wait for all requests to complete\n", + " predictions = await asyncio.gather(*tasks)\n", + " return PredictionResponse(predictions=predictions)\n", + "\n", + " except Exception as e:\n", + " raise HTTPException(\n", + " status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,\n", + " detail=f\"Error processing prediction: {str(e)}\"\n", + " )\n", + "\n", + "@app.exception_handler(HTTPException)\n", + "async def http_exception_handler(request, exc):\n", + " '''Handle HTTP exceptions with a consistent format.'''\n", + " return JSONResponse(\n", + " status_code=exc.status_code,\n", + " content={\n", + " \"error\": {\n", + " \"code\": exc.status_code,\n", + " \"message\": exc.detail\n", + " }\n", + " }\n", + " )\n", + "\n", + "if __name__ == '__main__':\n", + " import uvicorn\n", + " uvicorn.run(\n", + " \"main:app\",\n", + " host=\"0.0.0.0\",\n", + " port=Config.PORT\n", + " )\n", + "\"\"\"\n", + "\n", + "with BUILD_DIR.joinpath(\"main.py\").open(\"w\") as f:\n", + " f.write(app_module)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "aIBHXaHMWlpN" + }, + "source": [ + "### Entrypoint script\n", + "\n", + "Create an entrypoint script to startup FastAPI application and its Ollama service. The script launches Ollama (a local AI model server) in the background, verifies its readiness through health checks, and then initializes a FastAPI application to serve as the main interface." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "PR8esJG0WpGe" + }, + "outputs": [], + "source": [ + "entrypoint_script = \"\"\"#!/bin/bash\n", + "\n", + "# Enable error handling\n", + "set -e\n", + "\n", + "# Function to log messages with timestamps\n", + "log() {\n", + " echo \"[$(date +'%Y-%m-%d %H:%M:%S')] $1\"\n", + "}\n", + "\n", + "# Function to check if Ollama is ready\n", + "check_ollama() {\n", + " for i in {1..30}; do\n", + " if curl -s http://localhost:8079 >/dev/null; then\n", + " log \"✅ Ollama is ready!\"\n", + " return 0\n", + " fi\n", + " log \"⏳ Waiting for Ollama to start... ($i/30)\"\n", + " sleep 1\n", + " done\n", + " log \"❌ Ollama failed to start within 30 seconds\"\n", + " return 1\n", + "}\n", + "\n", + "# Start Ollama in the background\n", + "log \"🚀 Starting Ollama...\"\n", + "ollama serve & sleep 5\n", + "\n", + "# Wait for Ollama to be ready\n", + "check_ollama\n", + "\n", + "# Start the FastAPI serving application\n", + "log \"🚀 Starting FastAPI serving application...\"\n", + "exec python3 /main.py\n", + "\"\"\"\n", + "\n", + "with BUILD_DIR.joinpath(\"entrypoint.sh\").open(\"w\") as f:\n", + " f.write(entrypoint_script)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "5RnaYx2p235W" + }, + "source": [ + "### Build the container image with Cloud Build\n", + "\n", + "Use Cloud Build to build the container image.\n", + "\n", + "> The operation will take less than 5 minutes.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "27nddRobW-_Y" + }, + "outputs": [], + "source": [ + "SERVING_CONTAINER_IMAGE_URI = (\n", + " f\"{LOCATION}-docker.pkg.dev/{PROJECT_ID}/{REPOSITORY_NAME}/ollama-gemma-2-serve\"\n", + ")\n", + "\n", + "! gcloud auth configure-docker $LOCATION-docker.pkg.dev --quiet\n", + "! gcloud builds submit --tag $SERVING_CONTAINER_IMAGE_URI --project $PROJECT_ID --machine-type e2-highcpu-32 $BUILD_DIR" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "xgcPjmMfXwAg" + }, + "source": [ + "### (Optional) Testing Ollama container locally using serving container with Vertex AI LocalModel\n", + "\n", + "For debugging purpose, Vertex AI provides `LocalModel` class, accessible through the Vertex AI SDK for Python. This class allows you to build and deploy your model locally, simulating the Vertex AI environment. Using LocalModel involves creating a Docker image that encapsulates your custom predictor code and the associated handler.\n", + "\n", + "> **Important**: Running the LocalModel class requires a local Docker installation. This allows the model to be encapsulated within a container for consistent execution across different environments.\n", + "\n", + "> If you haven't already installed Docker Engine, please refer to the official installation guide: [Install Docker Engine](https://docs.docker.com/engine/install/). This documentation provides detailed instructions for various operating systems and will guide you through the installation process. Ensure Docker is running correctly before proceeding with the LocalModel examples.\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "Im5wedPQuMoY" + }, + "source": [ + "#### Create a LocalModel instance\n", + "\n", + "Set up a local model by specifying the container image to use and the port it will listen on (8080).\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "VS9Z75d9uW3x" + }, + "outputs": [], + "source": [ + "local_ollama_gemma_model = LocalModel(\n", + " serving_container_image_uri=SERVING_CONTAINER_IMAGE_URI,\n", + " serving_container_ports=[8080],\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "3bxcGRl8uas-" + }, + "source": [ + "#### Create a LocalEndpoint instance\n", + "\n", + "Deploy the model to a local endpoint for serving. The `gpu_device_ids` sets available GPUs if present.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "hfbRbRHcuee3" + }, + "outputs": [], + "source": [ + "local_ollama_gemma_endpoint = local_ollama_gemma_model.deploy_to_local_endpoint(\n", + " gpu_device_ids=get_cuda_device_names()\n", + ")\n", + "\n", + "local_ollama_gemma_endpoint.serve()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "XmnfGmT8sQ-k" + }, + "source": [ + "#### Monitoring Your Containerized Deployment\n", + "\n", + "To keep track of your container's deployment progress and identify any potential issues, you can use the following Docker commands within your terminal:\n", + "\n", + "1. **List all containers:** `docker container ls -a` displays a list of all running and stopped containers. Locate the container associated with your local endpoint and copy its ID. This ID is essential for the next step.\n", + "\n", + "2. **Stream container logs:** `docker logs --follow ` provides a real-time stream of your container's logs. Replace `` with the ID you copied in the previous step. Monitoring these logs allows you to observe the deployment process, identify any errors or warnings, and understand the container's overall health." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "7EcVi9Y_ujFj" + }, + "source": [ + "#### Generate predictions\n", + "\n", + "Send a prediction request to a local Vertex AI endpoint.\n", + "\n", + "You convert the request data into a JSON string, send it to the endpoint, and then print the predictions from the JSON response.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "CvkpSHzymXEz" + }, + "outputs": [], + "source": [ + "prediction_request = {\n", + " \"instances\": [\n", + " {\n", + " \"inputs\": \"How to run a select all query\",\n", + " \"parameters\": {\n", + " \"temperature\": 1.0,\n", + " },\n", + " },\n", + " ]\n", + "}" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "Iso7dA7BzQiy" + }, + "outputs": [], + "source": [ + "vertex_prediction_request = json.dumps(prediction_request)\n", + "vertex_prediction_response = local_ollama_gemma_endpoint.predict(\n", + " request=vertex_prediction_request, headers={\"Content-Type\": \"application/json\"}\n", + ")\n", + "print(vertex_prediction_response.json()[\"predictions\"])" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "3aa8a5163e2e" + }, + "outputs": [], + "source": [ + "vertex_prediction_response" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "ecK96GglLTEb" + }, + "source": [ + "### Register Ollama model on Vertex AI\n", + "\n", + "To serve Gemma 2 with Ollama on Vertex AI, import the model on Vertex AI Model Registry, a central repository where you can manage the lifecycle of your ML models on Vertex AI, using the `aiplatform.Model.upload` method.\n", + "\n", + "Some of the main arguments of the `aiplatform.Model.upload` are:\n", + "\n", + "- `display_name`: The name shown in the Vertex AI Model Registry.\n", + "- `serving_container_image_uri`: The location of the Ollama container.\n", + "- (Optional) `serving_container_ports`: The port where the Vertex AI endpoint will be exposed (default 8080).\n", + "\n", + "For more information on the supported `aiplatform.Model.upload` arguments, check [its Python reference](https://cloud.google.com/python/docs/reference/aiplatform/latest/google.cloud.aiplatform.Model#google_cloud_aiplatform_Model_upload)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "ef487ce082f3" + }, + "outputs": [], + "source": [ + "model = Model.upload(\n", + " display_name=\"google--gemma-2-2b-it-lora-sql-ollama\",\n", + " serving_container_image_uri=SERVING_CONTAINER_IMAGE_URI,\n", + " serving_container_ports=[8080],\n", + ")\n", + "model.wait()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "c427c0a87016" + }, + "source": [ + "### Deploy Ollama model on Vertex AI\n", + "\n", + "After the model is registered on Vertex AI, deploy the model to a Vertex AI endpoint using the `aiplatform.Model.deploy` method.\n", + "\n", + "Some of the main arguments of the `aiplatform.Model.upload` are:\n", + "\n", + "* (optional) **`endpoint`** : Set an endpoint for model deployment.\n", + "* **`machine_type, accelerator_type, accelerator_count`** : Define the deployment instance and accelerator configuration.\n", + "\n", + "For more information on the supported `aiplatform.Model.deploy` arguments, you can check [its Python reference](https://cloud.google.com/python/docs/reference/aiplatform/latest/google.cloud.aiplatform.Model#google_cloud_aiplatform_Model_deploy)." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "JrcXG5KcozFm" + }, + "source": [ + "> Note that the model deployment on Vertex AI can take around 15 to 25 minutes; most of the time being the allocation / reservation of the resources, setting up the network and security, and such." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "e777427015db" + }, + "outputs": [], + "source": [ + "endpoint = Endpoint.create(\n", + " display_name=\"google--gemma-2-2b-it-lora-sql-ollama-endpoint\"\n", + ")\n", + "\n", + "deployed_model = model.deploy(\n", + " endpoint=endpoint,\n", + " machine_type=\"g2-standard-4\",\n", + " accelerator_type=\"NVIDIA_L4\",\n", + " accelerator_count=1,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "9b3fd1898241" + }, + "source": [ + "### Online predictions on Vertex AI" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "2aa9cee03bd0" + }, + "source": [ + "Once the model is deployed on Vertex AI, run the online predictions using the `aiplatform.Endpoint.predict` method, which will send the requests to the running endpoint in the `/predict` route specified within the container following Vertex AI I/O payload formatting." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "gOrBkKoy-_2d" + }, + "outputs": [], + "source": [ + "output = deployed_model.predict(\n", + " instances=[\n", + " {\n", + " \"inputs\": \"How to run a select all query\",\n", + " \"parameters\": {\n", + " \"temperature\": 1.0,\n", + " },\n", + " },\n", + " ]\n", + ")\n", + "predictions = output.predictions\n", + "print(predictions[0])" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "3u7fALoY_S50" + }, + "source": [ + "## Build an agentic RAG application using Ollama model on Vertex AI with LangGraph\n", + "\n", + "After deployed the Ollama model on Vertex AI, consume the model to build an agentic RAG application using Ollama model on Vertex AI with LangGraph." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "dxG6zfZ9TYa-" + }, + "source": [ + "### Install additional libraries\n", + "\n", + "Install langgraph libraries." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "zQd0iPF7Tb8R" + }, + "outputs": [], + "source": [ + "%pip install --upgrade --user --quiet \"langchain-community\" \\\n", + " \"langchainhub\" \\\n", + " \"langchain_google_vertexai\" \\\n", + " \"langgraph\" \\\n", + " \"faiss-gpu\"" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "y_0aWPbuOHyy" + }, + "source": [ + "### Import additional libraries\n", + "\n", + "Import additional libraries to build the agent." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "NiFX3teSNzZ-" + }, + "outputs": [], + "source": [ + "from typing import Any, TypedDict\n", + "\n", + "from IPython.display import Image, display\n", + "from langchain_community.document_loaders import WebBaseLoader\n", + "from langchain_community.vectorstores import FAISS\n", + "from langchain_core.messages import AIMessage, BaseMessage, HumanMessage\n", + "from langchain_core.runnables.graph import MermaidDrawMethod\n", + "from langchain_google_vertexai.embeddings import VertexAIEmbeddings\n", + "from langchain_text_splitters import RecursiveCharacterTextSplitter\n", + "from langgraph.graph import END, StateGraph" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "mx4LDoATRtXN" + }, + "source": [ + "### Define some helpers\n", + "\n", + "Define a `CustomVertexAIModel` to handle all the endpoint formatting and parameter management in a way to make the model compatible with a LangGraph agentic workflow." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "V9-1wIkXR0bA" + }, + "outputs": [], + "source": [ + "class CustomVertexAIModel:\n", + " \"\"\"\n", + " A simple wrapper for Vertex AI Endpoints.\n", + " \"\"\"\n", + "\n", + " def __init__(\n", + " self,\n", + " project: str,\n", + " location: str,\n", + " endpoint_id: str,\n", + " **model_params: Any,\n", + " ):\n", + " \"\"\"\n", + " Initialize the Model Garden client.\n", + "\n", + " Args:\n", + " project: Google Cloud project ID\n", + " location: Model location (e.g., \"us-central1\")\n", + " endpoint_id: Vertex AI endpoint ID\n", + " **model_params: Model parameters (temperature, max_tokens, etc.)\n", + " \"\"\"\n", + " self.endpoint = aiplatform.Endpoint(\n", + " endpoint_name=f\"projects/{project}/locations/{location}/endpoints/{endpoint_id}\"\n", + " )\n", + " self.model_params = model_params\n", + "\n", + " def invoke(\n", + " self,\n", + " prompt: str,\n", + " **kwargs: Any,\n", + " ) -> str:\n", + " \"\"\"\n", + " Invoke the model with a prompt and optional parameter overrides.\n", + "\n", + " Args:\n", + " prompt: The input text prompt\n", + " **kwargs: Optional parameter overrides for this specific call\n", + "\n", + " Returns:\n", + " The model's response as a string\n", + " \"\"\"\n", + " # Merge default parameters with any call-specific overrides\n", + " parameters = {**self.model_params, **kwargs}\n", + "\n", + " instance = {\"inputs\": prompt, \"parameters\": parameters}\n", + "\n", + " response = self.endpoint.predict([instance])\n", + " return response.predictions[0]" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "mdNXjMN6y7zg" + }, + "source": [ + "### Build the LangGraph agent" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "pyQ7xAFFTTqO" + }, + "source": [ + "#### Initialize Vertex AI components\n", + "\n", + "Initialize the Google's embedding model and the LLM model hosted on the Vertex AI Endpoint.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "LloVRJuNTbLP" + }, + "outputs": [], + "source": [ + "embeddings = VertexAIEmbeddings(model_name=\"text-embedding-005\", project=PROJECT_ID)\n", + "\n", + "llm = CustomVertexAIModel(\n", + " endpoint_id=endpoint.name,\n", + " temperature=1.0,\n", + " project=PROJECT_ID,\n", + " location=LOCATION,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "Y9HSN0CETeTA" + }, + "source": [ + "#### Define agent components\n", + "\n", + "According to LangGraph documentation, define an agent state and the following functions to build the agent:\n", + "\n", + "1. `create_vectorstore_from_urls`: Loads web pages, splits them into chunks, and creates a searchable vector database using FAISS embeddings.\n", + "2. `retrieve`: Finds the 3 most similar document chunks from the vector store based on the user's query and adds them to the state context.\n", + "3. `generate_response`: Takes the retrieved context and query, sends them to the LLM for processing, and updates the state with the response and conversation history.\n", + "4. `should_rewrite`: Checks if the generated response is in proper SQL format by looking for SQL keywords.\n", + "5. `rewrite_response`: Asks the LLM to reformat the response into a proper SQL query with comments and proper syntax" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "qDnsnmAiR5GP" + }, + "outputs": [], + "source": [ + "class AgentState(TypedDict):\n", + " query: str\n", + " messages: list[BaseMessage]\n", + " context: str\n", + " response: str\n", + " chat_history: list[BaseMessage]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "tLDKJ9J-Tifw" + }, + "outputs": [], + "source": [ + "def create_vectorstore_from_urls(urls: list[str]) -> FAISS:\n", + " \"\"\"Create a FAISS vectorstore from webpage contents\"\"\"\n", + " loader = WebBaseLoader(urls)\n", + " documents = loader.load()\n", + " text_splitter = RecursiveCharacterTextSplitter(chunk_size=1000, chunk_overlap=200)\n", + " splits = text_splitter.split_documents(documents)\n", + " return FAISS.from_documents(splits, embeddings)\n", + "\n", + "\n", + "def retrieve(state: AgentState, vectorstore: FAISS) -> AgentState:\n", + " \"\"\"Retrieve relevant documents\"\"\"\n", + " docs = vectorstore.similarity_search(state[\"query\"], k=3)\n", + " state[\"context\"] = \"\\n\".join(doc.page_content for doc in docs)\n", + " return state\n", + "\n", + "\n", + "def generate_response(state: AgentState) -> AgentState:\n", + " \"\"\"Generate response using the Model Garden LLM\"\"\"\n", + " prompt = f\"\"\"Context: {state[\"context\"]}\n", + " Question: {state[\"query\"]}\n", + " Please provide a helpful response based on the context above.\"\"\"\n", + "\n", + " response = llm.invoke(prompt)\n", + "\n", + " state[\"response\"] = response\n", + " state[\"messages\"].append(AIMessage(content=response))\n", + " state[\"chat_history\"].extend(\n", + " [HumanMessage(content=state[\"query\"]), AIMessage(content=response)]\n", + " )\n", + " return state\n", + "\n", + "\n", + "def should_rewrite(state: AgentState) -> AgentState:\n", + " \"\"\"Decide if the response needs rewriting\"\"\"\n", + " # First check if query is SQL-related\n", + " sql_keywords = [\n", + " \"sql\",\n", + " \"query\",\n", + " \"select\",\n", + " \"table\",\n", + " \"database\",\n", + " \"bigquery\",\n", + " \"join\",\n", + " \"where\",\n", + " ]\n", + " query_is_sql = any(keyword in state[\"query\"].lower() for keyword in sql_keywords)\n", + "\n", + " # Only check SQL formatting if the query was SQL-related\n", + " if query_is_sql:\n", + " response = state[\"response\"].lower()\n", + " needs_rewrite = (\n", + " not response.strip().startswith(\"select\")\n", + " and not response.strip().startswith(\"with\")\n", + " and not response.strip().startswith(\"create\")\n", + " and not response.strip().startswith(\"/*\")\n", + " and \"select\" not in response[:100]\n", + " )\n", + " state[\"next\"] = \"rewrite\" if needs_rewrite else \"end\"\n", + " else:\n", + " state[\"next\"] = \"end\"\n", + "\n", + " return state\n", + "\n", + "\n", + "def rewrite_response(state: AgentState) -> AgentState:\n", + " \"\"\"\n", + " Rewrite the response to ensure it's in proper SQL format\n", + " \"\"\"\n", + " prompt = f\"\"\"\n", + " Original question: {state[\"query\"]}\n", + " Previous response: {state[\"response\"]}\n", + "\n", + " Rewrite the above as a proper SQL query following these rules:\n", + " - Start with SQL keywords (SELECT, WITH, CREATE, etc.)\n", + " - Include comments explaining the logic\n", + " - Format the query properly\n", + " - Use BigQuery SQL syntax\n", + " \"\"\"\n", + "\n", + " new_response = llm.invoke(prompt)\n", + " state[\"response\"] = new_response\n", + " state[\"messages\"][-1] = AIMessage(content=new_response) # Replace last message\n", + " state[\"chat_history\"][-1] = AIMessage(\n", + " content=new_response\n", + " ) # Replace last history item\n", + "\n", + " return state" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "FEzPObd4mV-4" + }, + "source": [ + "#### Assemble the agent\n", + "\n", + "Create a simple agentic RAG that first builds a searchable database from URLs, then sets up a sequence of steps (retrieve → generate → check for rewrite → either end or rewrite and loop back) to process queries and generate SQL responses." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "EVysTj9BmVRA" + }, + "outputs": [], + "source": [ + "def create_rag_agent(urls: list[str]) -> Any:\n", + " \"\"\"Create the RAG agent workflow\"\"\"\n", + " vectorstore = create_vectorstore_from_urls(urls)\n", + "\n", + " workflow = StateGraph(AgentState)\n", + "\n", + " # Add nodes\n", + " workflow.add_node(\"retrieve\", lambda s: retrieve(s, vectorstore))\n", + " workflow.add_node(\"generate\", generate_response)\n", + " workflow.add_node(\"should_rewrite\", should_rewrite)\n", + " workflow.add_node(\"rewrite\", rewrite_response)\n", + "\n", + " # Add edges\n", + " workflow.add_edge(\"retrieve\", \"generate\")\n", + " workflow.add_edge(\"generate\", \"should_rewrite\")\n", + "\n", + " # Add conditional edges\n", + " workflow.add_conditional_edges(\n", + " \"should_rewrite\", lambda x: x[\"next\"], {\"rewrite\": \"rewrite\", \"end\": END}\n", + " )\n", + "\n", + " workflow.add_edge(\"rewrite\", \"retrieve\")\n", + "\n", + " workflow.set_entry_point(\"retrieve\")\n", + "\n", + " return workflow.compile()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "PvFPaW4H0X5o" + }, + "source": [ + "#### Initialize the agent\n", + "\n", + "Initialize the agent by passing the BigQuery documentation to ground the agent." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "cptGsg_mmeCP" + }, + "outputs": [], + "source": [ + "urls = [\n", + " \"https://cloud.google.com/bigquery/docs/reference/standard-sql/query-syntax\",\n", + " \"https://cloud.google.com/bigquery/docs/reference/standard-sql/dml-syntax\",\n", + " \"https://cloud.google.com/bigquery/docs/reference/standard-sql/arrays\",\n", + " \"https://cloud.google.com/bigquery/docs/reference/standard-sql/aggregate_functions\",\n", + " \"https://cloud.google.com/bigquery/docs/reference/standard-sql/window-function-calls\",\n", + " \"https://cloud.google.com/bigquery/docs/reference/standard-sql/subqueries\",\n", + " \"https://cloud.google.com/bigquery/docs/reference/standard-sql/joins\",\n", + " \"https://cloud.google.com/bigquery/docs/reference/standard-sql/operators\",\n", + " \"https://cloud.google.com/bigquery/docs/reference/standard-sql/functions-reference\",\n", + "]\n", + "\n", + "\n", + "agent = create_rag_agent(urls)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "xSY4cq1N0c6b" + }, + "source": [ + "#### Visualize the agent\n", + "\n", + "Plot the agentic workflow.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "FklWq97Rmfdl" + }, + "outputs": [], + "source": [ + "display(\n", + " Image(\n", + " agent.get_graph().draw_mermaid_png(\n", + " draw_method=MermaidDrawMethod.API,\n", + " )\n", + " )\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "RCH92w2cnB9B" + }, + "source": [ + "### Query the agent\n", + "\n", + "Use the agent to answer SQL code generation user requests." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "Tk4jB1qxUv-l" + }, + "outputs": [], + "source": [ + "def query_rag_agent(agent, query: str) -> dict[str, Any]:\n", + " \"\"\"Query the RAG agent\"\"\"\n", + " state = {\n", + " \"query\": query,\n", + " \"messages\": [],\n", + " \"context\": \"\",\n", + " \"response\": \"\",\n", + " \"chat_history\": [],\n", + " \"next\": None, # Add next step field\n", + " }\n", + " return agent.invoke(state)\n", + "\n", + "\n", + "questions = [\n", + " \"Write a SQL query to calculate the total sales per month and include a 3-month moving average\",\n", + " \"Create a query that finds the top 5 customers by revenue in each region, including their total spend and number of orders\",\n", + " \"Write a SQL query to analyze user engagement: calculate daily active users (DAU), weekly active users (WAU), and the DAU/WAU ratio for the last 30 days\",\n", + "]\n", + "\n", + "# Test each question\n", + "for question in questions:\n", + " result = query_rag_agent(agent, question)\n", + " print(f\"\\nQuestion: {question}\")\n", + " print(\"\\nResponse:\")\n", + " print(result[\"response\"])\n", + " print(\"\\n\" + \"=\" * 80)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "f6f17f9aff65" + }, + "source": [ + "## Cleaning up" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "KFOXWLLTvMvJ" + }, + "outputs": [], + "source": [ + "delete_endpoint = False\n", + "delete_model = False\n", + "delete_artifact_registry = False\n", + "delete_tutorial_folder = False\n", + "\n", + "if delete_endpoint:\n", + " deployed_model.undeploy_all()\n", + " deployed_model.delete()\n", + "\n", + "if delete_model:\n", + " delete_model.delete()\n", + "\n", + "if delete_artifact_registry:\n", + " ! gcloud artifacts repositories delete $REPOSITORY_NAME \\\n", + " --repository-format=docker \\\n", + " --location=$LOCATION \\\n", + " --project=$PROJECT_ID\n", + "\n", + "if delete_tutorial_folder:\n", + " import shutil\n", + "\n", + " shutil.rmtree(tutorial_folder)" + ] + } + ], + "metadata": { + "colab": { + "name": "vertex_ai_ollama_gemma2_rag_agent.ipynb", + "toc_visible": true + }, + "kernelspec": { + "display_name": "Python 3", + "name": "python3" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} diff --git a/open-models/serving/vertex_ai_pytorch_inference_paligemma_with_custom_handler.ipynb b/open-models/serving/vertex_ai_pytorch_inference_paligemma_with_custom_handler.ipynb new file mode 100644 index 0000000..fb0dab5 --- /dev/null +++ b/open-models/serving/vertex_ai_pytorch_inference_paligemma_with_custom_handler.ipynb @@ -0,0 +1,1384 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "ur8xi4C7S06n" + }, + "outputs": [], + "source": [ + "# Copyright 2024 Google LLC\n", + "#\n", + "# Licensed under the Apache License, Version 2.0 (the \"License\");\n", + "# you may not use this file except in compliance with the License.\n", + "# You may obtain a copy of the License at\n", + "#\n", + "# https://www.apache.org/licenses/LICENSE-2.0\n", + "#\n", + "# Unless required by applicable law or agreed to in writing, software\n", + "# distributed under the License is distributed on an \"AS IS\" BASIS,\n", + "# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n", + "# See the License for the specific language governing permissions and\n", + "# limitations under the License." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "JAPoU8Sm5E6e" + }, + "source": [ + "# Hugging Face DLCs: Serving PaliGemma using Pytorch Inference on Vertex AI with Custom Handler\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + "
    \n", + " \n", + " \"Google
    Open in Colab\n", + "
    \n", + "
    \n", + " \n", + " \"Google
    Open in Colab Enterprise\n", + "
    \n", + "
    \n", + " \n", + " \"Vertex
    Open in Vertex AI Workbench\n", + "
    \n", + "
    \n", + " \n", + " \"GitHub
    View on GitHub\n", + "
    \n", + "
    \n", + "\n", + "
    \n", + "\n", + "Share to:\n", + "\n", + "\n", + " \"LinkedIn\n", + "\n", + "\n", + "\n", + " \"Bluesky\n", + "\n", + "\n", + "\n", + " \"X\n", + "\n", + "\n", + "\n", + " \"Reddit\n", + "\n", + "\n", + "\n", + " \"Facebook\n", + "" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "84f0f73a0f76" + }, + "source": [ + "| | |\n", + "|-|-|\n", + "| Author(s) | [Ivan Nardini](https://github.com/inardini), [Alvaro Bartolome](https://github.com/alvarobartt) |" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "tvgnzT1CKxrO" + }, + "source": [ + "## Overview\n", + "\n", + "> [**PaliGemma**](https://ai.google.dev/gemma) is a lightweight open vision-language model (VLM) inspired by PaLI-3, and based on open components like the SigLIP vision model and the Gemma language model.\n", + "\n", + "> [**Hugging Face DLCs**](https://github.com/huggingface/Google-Cloud-Containers) are pre-built and optimized Deep Learning Containers (DLCs) maintained by Hugging Face and Google Cloud teams to simplify environment configuration for your ML workloads.\n", + "\n", + "> [**Google Vertex AI**](https://cloud.google.com/vertex-ai) is a Machine Learning (ML) platform that lets you train and deploy ML models and AI applications, and customize large language models (LLMs) for use in your AI-powered applications.\n", + "\n", + "This notebook showcases how to deploy Google PaliGemma from the Hugging Face Hub on Vertex AI using the Hugging Face Deep Learning Container (DLC) for Pytorch Inference in combination with a [custom handler](https://huggingface.co/docs/inference-endpoints/en/guides/custom_handler#create-custom-inference-handler).\n", + "\n", + "By the end of this notebook, you will learn how to:\n", + "\n", + "- Create a custom handler and test it\n", + "- Register any LLM from the Hugging Face Hub on Vertex AI\n", + "- Deploy an LLM on Vertex AI\n", + "- Send online predictions on Vertex AI" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "61RBz8LLbxCR" + }, + "source": [ + "## Get started" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "No17Cw5hgx12" + }, + "source": [ + "### Install Vertex AI SDK and other required packages\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "tFy3H3aPgx12" + }, + "outputs": [], + "source": [ + "%pip install --upgrade --user --quiet 'torch' 'torchvision' 'torchaudio'\n", + "%pip install --upgrade --user --quiet 'transformers' 'accelerate>=0.26.0'\n", + "%pip install --upgrade --user --quiet 'google-cloud-aiplatform[prediction]' 'crcmod' 'etils'" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "R5Xep4W9lq-Z" + }, + "source": [ + "### Restart runtime\n", + "\n", + "To use the newly installed packages in this Jupyter runtime, you must restart the runtime. You can do this by running the cell below, which restarts the current kernel.\n", + "\n", + "The restart might take a minute or longer. After it's restarted, continue to the next step." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "XRvKdaPDTznN" + }, + "outputs": [], + "source": [ + "import IPython\n", + "\n", + "app = IPython.Application.instance()\n", + "app.kernel.do_shutdown(True)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "SbmM4z7FOBpM" + }, + "source": [ + "
    \n", + "⚠ The kernel is going to restart. In Colab or Colab Enterprise, you might see an error message that says \"Your session crashed for an unknown reason.\" This is expected. Wait until it's finished before continuing to the next step. ⚠\n", + "
    \n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "dmWOrTJ3gx13" + }, + "source": [ + "### Authenticate your notebook environment (Colab only)\n", + "\n", + "If you're running this notebook on Google Colab, run the cell below to authenticate your environment." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "NyKGtVQjgx13" + }, + "outputs": [], + "source": [ + "import sys\n", + "\n", + "if \"google.colab\" in sys.modules:\n", + " from google.colab import auth\n", + "\n", + " auth.authenticate_user()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "e0vxVy5J3g41" + }, + "source": [ + "### Set Hugging Face `HF_HOME` variable" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "P5UCCYrV3pEW" + }, + "outputs": [], + "source": [ + "import os\n", + "\n", + "from etils.epath import Path\n", + "\n", + "ROOT_PATH = Path(\".\")\n", + "TUTORIAL_PATH = ROOT_PATH / \"deploy_paligemma_with_custom_handler_tutorial\"\n", + "\n", + "os.environ[\"HF_HOME\"] = str(TUTORIAL_PATH)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "f9af3e57f89a" + }, + "source": [ + "### Authenticate your Hugging Face account\n", + "\n", + "As [`google/paligemma-3b-mix-448`](https://huggingface.co/google/paligemma-3b-mix-448) is a gated model, you are required to review and agree to Google usage license on the Hugging Face Hub for any of the models from the [PaliGemma release collection](https://huggingface.co/collections/google/paligemma-release-6643a9ffbf57de2ae0448dda), and the access request will be processed inmediately.\n", + "\n", + "Once that's done, you need to generate a new user access token with read-only access so that the weights can be downloaded from the Hub in the Hugging Face DLC for TGI.\n", + "\n", + "> Note that the user access token can only be generated via [the Hugging Face Hub UI](https://huggingface.co/settings/tokens/new), where you can either select read-only access to your account, or follow the recommendations and generate a fine-grained token with read-only access to [`google/paligemma-3b-mix-448`](https://huggingface.co/google/paligemma-3b-mix-448)." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "4c31c7272804" + }, + "source": [ + "Then you can install the `huggingface_hub` that comes with a CLI that will be used for the authentication with the token generated in advance. So that then the token can be safely retrieved via `huggingface_hub.get_token`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "8d836e0210fe" + }, + "outputs": [], + "source": [ + "from huggingface_hub import interpreter_login\n", + "\n", + "interpreter_login()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "c71a4314c250" + }, + "source": [ + "Read more about [Hugging Face Security](https://huggingface.co/docs/hub/en/security), specifically about [Hugging Face User Access Tokens](https://huggingface.co/docs/hub/en/security-tokens)." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "Dy5VO78rzX8c" + }, + "source": [ + "### Requirements" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "R8Zm9y0hxU5O" + }, + "source": [ + "#### Set Project ID and Location\n", + "\n", + "To get started using Vertex AI, you must have an existing Google Cloud project and [enable these APIs](https://console.cloud.google.com/flows/enableapi?apiid=aiplatform.googleapis.com,artifactregistry.googleapis.com).\n", + "\n", + "Learn more about [setting up a project and a development environment](https://cloud.google.com/vertex-ai/docs/start/cloud-environment)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "Nqwi-5ufWp_B" + }, + "outputs": [], + "source": [ + "# Use the environment variable if the user doesn't provide Project ID.\n", + "import os\n", + "\n", + "PROJECT_ID = \"[your-project-id]\" # @param {type: \"string\", placeholder: \"[your-project-id]\", isTemplate: true}\n", + "\n", + "if not PROJECT_ID or PROJECT_ID == \"[your-project-id]\":\n", + " PROJECT_ID = str(os.environ.get(\"GOOGLE_CLOUD_PROJECT\"))\n", + "\n", + "PROJECT_NUMBER = !gcloud projects describe {PROJECT_ID} --format=\"get(projectNumber)\"[0]\n", + "PROJECT_NUMBER = PROJECT_NUMBER[0]\n", + "\n", + "LOCATION = os.environ.get(\"GOOGLE_CLOUD_REGION\", \"us-central1\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "zgPO1eR3CYjk" + }, + "source": [ + "#### Set and create a Cloud Storage bucket\n", + "\n", + "Create a storage bucket to store intermediate artifacts such as models." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "MzGDU7TWdts_" + }, + "outputs": [], + "source": [ + "BUCKET_NAME = \"[your-bucket-name]\" # @param {type: \"string\", placeholder: \"[your-bucket-name]\", isTemplate: true}\n", + "\n", + "BUCKET_URI = f\"gs://{BUCKET_NAME}\" # @param {type:\"string\"}" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "a-Yz-SSagqUW" + }, + "outputs": [], + "source": [ + "! gsutil mb -l {LOCATION} -p {PROJECT_ID} {BUCKET_URI}" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "set_service_account" + }, + "source": [ + "#### Set Service Account and permissions\n", + "\n", + "You will need to have the following IAM roles set:\n", + "\n", + "- Vertex AI User (roles/aiplatform.user)\n", + "- Artifact Registry Reader (roles/artifactregistry.reader)\n", + "- Storage Object Admin (roles/storage.objectAdmin)\n", + "\n", + "For more information about granting roles, see [Manage access](https://cloud.google.com/iam/docs/granting-changing-revoking-access).\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "VX9tpdtuQI5L" + }, + "source": [ + "> If you run following commands using Vertex AI Workbench, run directly in the terminal.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "ssUJJqXJJHgC" + }, + "outputs": [], + "source": [ + "SERVICE_ACCOUNT = f\"{PROJECT_NUMBER}-compute@developer.gserviceaccount.com\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "wqOHg5aid6HP" + }, + "outputs": [], + "source": [ + "for role in ['aiplatform.user', 'storage.objectAdmin', 'artifactregistry.reader']:\n", + "\n", + " ! gcloud projects add-iam-policy-binding {PROJECT_ID} \\\n", + " --member=serviceAccount:{SERVICE_ACCOUNT} \\\n", + " --role=roles/{role} --condition=None" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "EjebjdiNxe_D" + }, + "source": [ + "### Initiate Vertex AI SDK\n", + "\n", + "Initiate Vertex AI client session." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "GReulMr0xjZs" + }, + "outputs": [], + "source": [ + "import vertexai\n", + "\n", + "vertexai.init(project=PROJECT_ID, location=LOCATION, staging_bucket=BUCKET_URI)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "8XvMcYzm7xmm" + }, + "source": [ + "### Import libraries\n", + "\n", + "Import relevant libraries." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "3fXW5p7o7zlo" + }, + "outputs": [], + "source": [ + "import base64\n", + "import gc\n", + "from io import BytesIO\n", + "import json\n", + "\n", + "from PIL import Image\n", + "from etils import epath\n", + "from google.cloud import aiplatform\n", + "from google.cloud.aiplatform import Endpoint, Model\n", + "from google.cloud.aiplatform.prediction import LocalModel\n", + "import requests\n", + "import torch\n", + "from transformers import PaliGemmaForConditionalGeneration, PaliGemmaProcessor" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "E4rjOknk5yvq" + }, + "source": [ + "### Helpers\n", + "\n", + "Define some helpers." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "MOoaB5xf51BD" + }, + "outputs": [], + "source": [ + "def hf_model_path(hf_home_path: str) -> str:\n", + " \"\"\"\n", + " Copy model files from the Hugging Face cache.\n", + " \"\"\"\n", + "\n", + " # Convert and expand the input path\n", + " base_path = Path(hf_home_path).expanduser()\n", + "\n", + " if not base_path.exists():\n", + " raise FileNotFoundError(f\"Cache path does not exist: {base_path}\")\n", + "\n", + " # Find the deepest snapshot directory\n", + " snapshot_path = None\n", + " for model_dir in base_path.iterdir():\n", + " if not model_dir.is_dir():\n", + " continue\n", + "\n", + " # Look for 'snapshots' directory\n", + " snapshots_dir = model_dir / \"snapshots\"\n", + " if not snapshots_dir.exists():\n", + " continue\n", + "\n", + " # Get the first snapshot folder (usually there's only one)\n", + " for snapshot in snapshots_dir.iterdir():\n", + " if snapshot.is_dir():\n", + " snapshot_path = snapshot\n", + " break\n", + "\n", + " if snapshot_path:\n", + " break\n", + " if not snapshot_path:\n", + " raise FileNotFoundError(\"No snapshot directory found in the cache\")\n", + "\n", + " return snapshot_path\n", + "\n", + "\n", + "def get_cuda_device_names():\n", + " \"\"\"A function to get the list of NVIDIA GPUs\"\"\"\n", + " if not torch.cuda.is_available():\n", + " return None\n", + "\n", + " return [str(i) for i in range(torch.cuda.device_count())]\n", + "\n", + "\n", + "def empty_gpu_ram():\n", + " gc.collect()\n", + " torch.cuda.empty_cache()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "EdvJRUWRNGHE" + }, + "source": [ + "## Prepare a custom handler to serve PaliGemma on Vertex AI\n", + "\n", + "Custom Handlers are custom classes in Python that define the pre-processing, inference, and post-processing steps required to run the inference using HuggingFace Pytorch Prediction container on Vertex AI.\n", + "\n", + "Think of Custom Handlers as personalized instructions for Hugging Face models. They define how to prepare the input data, run the model, and handle the results. In this sense, Custom Handlers add flexibility. They let you customize how data is prepared and processed, add extra steps, and even build in custom measurements or logging. This means you can tailor the process to your exact needs when the standard setup isn't sufficient.\n", + "\n", + "These instructions are stored in a file named `handler.py`. If you need additional dependecies, you can list it in a `requirements.txt` file. The PyTorch container automatically finds and uses these files if they're present.\n", + "\n", + "Have a look at [đŸ€— Serve Anything with Inference Endpoints + Custom Handlers](https://huggingface.co/blog/alvarobartt/serve-anything-inference-endpoints-custom-code) to learn more." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "CgPwfG07iMCS" + }, + "source": [ + "### Test the `handler` locally\n", + "\n", + "Before to build the handler module, you can test its coding locally." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "OVLBAtraJkQ_" + }, + "source": [ + "#### Load Paligemma from HuggingFace Hub\n", + "\n", + "Load a pre-trained model called \"paligemma\" for text generation. You first sets up the text preprocessor, which handles tasks like tokenization (breaking text into words or sub-words). Then you load the actual model itself, optimized for lower memory usage and automatic device placement (likely GPU if available)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "QmHijFltikJZ" + }, + "outputs": [], + "source": [ + "paligemma_processor = PaliGemmaProcessor.from_pretrained(\"google/paligemma-3b-mix-448\")\n", + "\n", + "paligemma_model = PaliGemmaForConditionalGeneration.from_pretrained(\n", + " \"google/paligemma-3b-mix-448\",\n", + " low_cpu_mem_usage=True,\n", + " device_map=\"auto\",\n", + ").eval()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "zOWCRhkpJqlR" + }, + "source": [ + "#### Collect image for generating predictions\n", + "\n", + "Download an image from a URL and opens it using the Pillow library. The code first fetches the image data from the given URL, then checks if the download was successful (raising an error if not). Finally, it opens the downloaded image data directly from memory using BytesIO, making it ready for further processing with Pillow." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "65FfEb0YjITa" + }, + "outputs": [], + "source": [ + "image_url = \"https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/transformers/tasks/car.jpg?download=true\"\n", + "image_response = requests.get(image_url)\n", + "image_response.raise_for_status()\n", + "image = Image.open(BytesIO(image_response.content))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "j5Qir_dtJ0-X" + }, + "source": [ + "#### Prepare the prediction request\n", + "\n", + "Prepare the image captioning request by creating a dictionary with image data and captioning instructions.\n", + "\n", + "The image, downloaded from a previous response (`image_response`), is encoded into a base64 string. `max_new_tokens`limits the caption to 100 words and `do_sample` tells the model to generate the most likely caption rather than a more creative, potentially less accurate one.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "6g3Mt_FVyaME" + }, + "outputs": [], + "source": [ + "prediction_request = {\n", + " \"instances\": [\n", + " {\n", + " \"prompt\": \"caption it\",\n", + " \"image_base64\": base64.b64encode(image_response.content).decode(\"utf-8\"),\n", + " \"generation_kwargs\": {\"max_new_tokens\": 100, \"do_sample\": False},\n", + " }\n", + " ]\n", + "}" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "DH-WU2IAJ4yY" + }, + "source": [ + "#### Generate prediction\n", + "\n", + "Process a list of prediction requests, each containing an image and a text prompt.\n", + "\n", + "For each request, you decode the base64-encoded image, prepare the image and prompt for a model (paligemma_model) using a processor (paligemma_processor), and then generate a text response using the model. Finally, you decode the generated response and adds it to a list of predictions.\n", + "\n", + "Error handling ensures that each request includes the necessary keys and that the image can be loaded correctly. Custom generation parameters can be provided via generation_kwargs for each instance." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "UyKf2v3vlOtY" + }, + "outputs": [], + "source": [ + "predictions = []\n", + "\n", + "for instance in prediction_request[\"instances\"]:\n", + " if any(key not in instance for key in {\"prompt\", \"image_base64\"}):\n", + " raise ValueError(\n", + " \"The request body for each instance should contain both the `prompt` and the `image_base64` key with a valid image.\"\n", + " )\n", + "\n", + " try:\n", + " image_bytes = base64.b64decode(instance[\"image_base64\"])\n", + " image_file = BytesIO(image_bytes)\n", + " image = Image.open(image_file)\n", + " except Exception as e:\n", + " raise ValueError(f\"The provided image cannot be loaded (with exception {e}).\")\n", + " inputs = paligemma_processor(\n", + " text=instance[\"prompt\"], images=image, return_tensors=\"pt\"\n", + " ).to(paligemma_model.device)\n", + "\n", + " input_len = inputs[\"input_ids\"].shape[-1]\n", + " with torch.inference_mode():\n", + " generation_kwargs = instance.get(\n", + " \"generation_kwargs\", {\"max_new_tokens\": 100, \"do_sample\": False}\n", + " )\n", + " generation = paligemma_model.generate(**inputs, **generation_kwargs)\n", + " generation = generation[0][input_len:]\n", + " response = paligemma_processor.decode(generation, skip_special_tokens=True)\n", + " predictions.append(response)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "zzmvzmxgaiFc" + }, + "source": [ + "Get the prediction." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "XlML8-o8lV9X" + }, + "outputs": [], + "source": [ + "print(predictions)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "Yvh-vsUblb-W" + }, + "source": [ + "### Define the `handler.py` module\n", + "\n", + "After testing the handler code, you assemble the code in a Python module which defines the custom inference handler for PaliGemma.\n", + "\n", + "You write this handler code to a file named handler.py within the model directory." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "Qa5_u-Sn6OZ4" + }, + "outputs": [], + "source": [ + "model_path = hf_model_path(TUTORIAL_PATH / \"hub\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "WpBfVVbtoA_m" + }, + "outputs": [], + "source": [ + "handler_module = \"\"\"\n", + "from typing import Any, Dict, List\n", + "import torch\n", + "from transformers import PaliGemmaProcessor, PaliGemmaForConditionalGeneration\n", + "import base64\n", + "from io import BytesIO\n", + "from PIL import Image\n", + "import logging\n", + "import sys\n", + "\n", + "# Configure logging to output to stdout\n", + "logging.basicConfig(\n", + " level=logging.INFO,\n", + " format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',\n", + " handlers=[logging.StreamHandler(sys.stdout)]\n", + ")\n", + "logger = logging.getLogger('huggingface_inference_toolkit')\n", + "\n", + "class EndpointHandler:\n", + " def __init__(\n", + " self,\n", + " model_dir: str = '/opt/huggingface/model',\n", + " **kwargs: Any,\n", + " ) -> None:\n", + " self.processor = PaliGemmaProcessor.from_pretrained(model_dir)\n", + " self.model = PaliGemmaForConditionalGeneration.from_pretrained(\n", + " model_dir,\n", + " low_cpu_mem_usage=True,\n", + " device_map=\"auto\",\n", + " revision=\"bfloat16\",\n", + " torch_dtype=torch.bfloat16,\n", + " ).eval()\n", + "\n", + " def __call__(self, data: Dict[str, Any]) -> Dict[str, List[Any]]:\n", + " logger.info(\"Processing new request\")\n", + " predictions = []\n", + "\n", + " for instance in data['instances']:\n", + " logger.info(f\"Processing instance: {instance.get('prompt', '')[:100]}...\")\n", + "\n", + " if any(key not in instance for key in {\"prompt\", \"image_base64\"}):\n", + " error_msg = \"Missing prompt or image_base64 in request body\"\n", + " logger.info(error_msg)\n", + " raise ValueError(error_msg)\n", + "\n", + " try:\n", + " image_bytes = BytesIO(base64.b64decode(instance['image_base64']))\n", + " image = Image.open(image_bytes)\n", + " logger.info(\"Image loaded successfully\")\n", + " except Exception as e:\n", + " error_msg = f\"Failed to load image: {str(e)}\"\n", + " logger.info(error_msg)\n", + " raise ValueError(error_msg)\n", + "\n", + " inputs = self.processor(\n", + " text=instance[\"prompt\"], images=image, return_tensors=\"pt\"\n", + " ).to(self.model.device)\n", + " input_len = inputs[\"input_ids\"].shape[-1]\n", + " logger.info(f\"Input processed, length: {input_len}\")\n", + "\n", + " with torch.inference_mode():\n", + " generation_kwargs = data.get(\n", + " \"generation_kwargs\", {\"max_new_tokens\": 100, \"do_sample\": False}\n", + " )\n", + " logger.info(f\"Generation kwargs: {generation_kwargs}\")\n", + "\n", + " generation = self.model.generate(**inputs, **generation_kwargs)\n", + " generation = generation[0][input_len:]\n", + " response = self.processor.decode(generation, skip_special_tokens=True)\n", + " logger.info(f\"Generated response: {response[:100]}...\")\n", + " predictions.append(response)\n", + "\n", + " logger.info(f\"Successfully processed {len(predictions)} instances\")\n", + " return {\"predictions\": predictions}\n", + "\"\"\"\n", + "\n", + "with open(model_path / \"handler.py\", \"w\") as hf:\n", + " hf.write(handler_module)\n", + "hf.close()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "3zOgAT4DpO8Z" + }, + "source": [ + "### Copy model with custom handler on Cloud Bucket\n", + "\n", + "Efficiently upload the model directory to Google Cloud Storage using `gsutil`.\n", + "\n", + "Note that `-m` enables multi-threaded uploads for faster transfer, especially for large directories. `-o GSUtil:parallel_composite_upload_threshold=150M` optimizes large file uploads by splitting them into smaller parts for parallel transfer, significantly speeding up the process for files larger than 150MB." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "daQnz0Kv6Khn" + }, + "outputs": [], + "source": [ + "model_uri = epath.Path(BUCKET_URI) / \"model\"\n", + "model_uri.mkdir(parents=True, exist_ok=True)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "ukQWCwz1pXzn" + }, + "outputs": [], + "source": [ + "! gsutil -o GSUtil:parallel_composite_upload_threshold=150M -m cp -r {str(model_path)}/* {str(model_uri)}" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "xgcPjmMfXwAg" + }, + "source": [ + "### (Optional) Testing the handler offline using serving container with Vertex AI LocalModel\n", + "\n", + "For debugging purpose, Vertex AI provides `LocalModel` class, accessible through the Vertex AI SDK for Python. This class allows you to build and deploy your model locally, simulating the Vertex AI environment. Using LocalModel involves creating a Docker image that encapsulates your custom predictor code and the associated handler.\n", + "\n", + "> **Important**: Running the LocalModel class requires a local Docker installation. This allows the model to be encapsulated within a container for consistent execution across different environments.\n", + "\n", + "> If you haven't already installed Docker Engine, please refer to the official installation guide: [Install Docker Engine](https://docs.docker.com/engine/install/). This documentation provides detailed instructions for various operating systems and will guide you through the installation process. Ensure Docker is running correctly before proceeding with the LocalModel examples.\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "Im5wedPQuMoY" + }, + "source": [ + "#### Create a LocalModel instance\n", + "\n", + "Set up a local model by specifying the container image to use (a Hugging Face Transformers model optimized for PyTorch and GPUs) and the port it will listen on (5000).\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "VS9Z75d9uW3x" + }, + "outputs": [], + "source": [ + "local_paligemma_model = LocalModel(\n", + " serving_container_image_uri=\"us-docker.pkg.dev/deeplearning-platform-release/gcr.io/huggingface-pytorch-inference-cu121.2-2.transformers.4-44.ubuntu2204.py311\",\n", + " serving_container_ports=[5000],\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "3bxcGRl8uas-" + }, + "source": [ + "#### Create a LocalEndpoint instance\n", + "\n", + "Deploy the model to a local endpoint for serving. You load the model from the specified location (model_uri) and start serving the model locally.\n", + "\n", + "The `gpu_device_ids` sets leverages available GPUs if present.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "hfbRbRHcuee3" + }, + "outputs": [], + "source": [ + "local_paligemma_endpoint = local_paligemma_model.deploy_to_local_endpoint(\n", + " artifact_uri=str(model_uri), gpu_device_ids=get_cuda_device_names()\n", + ")\n", + "\n", + "local_paligemma_endpoint.serve()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "XmnfGmT8sQ-k" + }, + "source": [ + "#### Monitoring Your Containerized Deployment\n", + "\n", + "To keep track of your container's deployment progress and identify any potential issues, you can use the following Docker commands within your terminal:\n", + "\n", + "1. **List all containers:** `docker container ls -a` displays a list of all running and stopped containers. Locate the container associated with your local endpoint and copy its ID. This ID is essential for the next step.\n", + "\n", + "2. **Stream container logs:** `docker logs --follow ` provides a real-time stream of your container's logs. Replace `` with the ID you copied in the previous step. Monitoring these logs allows you to observe the deployment process, identify any errors or warnings, and understand the container's overall health." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "7EcVi9Y_ujFj" + }, + "source": [ + "#### Generate predictions\n", + "\n", + "Send a prediction request to a local Vertex AI endpoint.\n", + "\n", + "You convert the request data (prediction_request) into a JSON string, send it to the endpoint, and then print the predictions from the JSON response.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "Iso7dA7BzQiy" + }, + "outputs": [], + "source": [ + "vertex_prediction_request = json.dumps(prediction_request)\n", + "vertex_prediction_response = local_paligemma_endpoint.predict(\n", + " request=vertex_prediction_request, headers={\"Content-Type\": \"application/json\"}\n", + ")\n", + "print(vertex_prediction_response.json()[\"predictions\"])" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "ecK96GglLTEb" + }, + "source": [ + "## Register Google Paligemma on Vertex AI" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "28d24c673821" + }, + "source": [ + "To serve PaliGemma with Pytorch Inference on Vertex AI, you start importing the model on Vertex AI Model Registry, a central repository where you can manage the lifecycle of your ML models on Vertex AI.\n", + "\n", + "Before going into the code to upload or import a model on Vertex AI, let's quickly review the arguments provided to the `aiplatform.Model.upload` method:\n", + "\n", + "* **`display_name`** is the name that will be shown in the Vertex AI Model Registry.\n", + "\n", + "* **`serving_container_image_uri`** is the location of the Hugging Face DLC for Pytorch Inference that will be used for serving the model.\n", + "\n", + "* (optional) **`serving_container_ports`** is the port where the Vertex AI endpoint |will be exposed, by default 8080.\n", + "\n", + "For more information on the supported `aiplatform.Model.upload` arguments, check [its Python reference](https://cloud.google.com/python/docs/reference/aiplatform/latest/google.cloud.aiplatform.Model#google_cloud_aiplatform_Model_upload)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "ef487ce082f3" + }, + "outputs": [], + "source": [ + "model = Model.upload(\n", + " display_name=\"google--paligemma-3b-mix-448\",\n", + " artifact_uri=str(model_uri),\n", + " serving_container_image_uri=\"us-docker.pkg.dev/deeplearning-platform-release/gcr.io/huggingface-pytorch-inference-cu121.2-2.transformers.4-44.ubuntu2204.py311\",\n", + " serving_container_ports=[8080],\n", + ")\n", + "model.wait()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "c427c0a87016" + }, + "source": [ + "## Deploy Google PaliGemma on Vertex AI" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "ef4cd160dc09" + }, + "source": [ + "After the model is registered on Vertex AI, you can deploy the model to an endpoint.\n", + "\n", + "You need to first deploy a model to an endpoint before that model can be used to serve online predictions. Deploying a model associates physical resources with the model so it can serve online predictions with low latency.\n", + "\n", + "Before going into the code to deploy a model to an endpoint, let's quickly review the arguments provided to the `aiplatform.Model.deploy` method:\n", + "\n", + "- **`endpoint`** is the endpoint to deploy the model to, which is optional, and by default will be set to the model display name with the `_endpoint` suffix.\n", + "- **`machine_type`**, **`accelerator_type`** and **`accelerator_count`** are arguments that define which instance to use, and additionally, the accelerator to use and the number of accelerators, respectively. The `machine_type` and the `accelerator_type` are tied together, so you will need to select an instance that supports the accelerator that you are using and vice-versa. More information about the different instances at [Compute Engine Documentation - GPU machine types](https://cloud.google.com/compute/docs/gpus), and about the `accelerator_type` naming at [Vertex AI Documentation - MachineSpec](https://cloud.google.com/vertex-ai/docs/reference/rest/v1/MachineSpec).\n", + "\n", + "For more information on the supported `aiplatform.Model.deploy` arguments, you can check [its Python reference](https://cloud.google.com/python/docs/reference/aiplatform/latest/google.cloud.aiplatform.Model#google_cloud_aiplatform_Model_deploy)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "e777427015db" + }, + "outputs": [], + "source": [ + "deployed_model = model.deploy(\n", + " endpoint=Endpoint.create(display_name=\"google--paligemma-7b-it-endpoint\"),\n", + " machine_type=\"g2-standard-4\",\n", + " accelerator_type=\"NVIDIA_L4\",\n", + " accelerator_count=1,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "5f2380d3ea60" + }, + "source": [ + "> Note that the model deployment on Vertex AI can take around 15 to 25 minutes; most of the time being the allocation / reservation of the resources, setting up the network and security, and such." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "9b3fd1898241" + }, + "source": [ + "## Online predictions on Vertex AI" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "2aa9cee03bd0" + }, + "source": [ + "Once the model is deployed on Vertex AI, you can run the online predictions using the `aiplatform.Endpoint.predict` method, which will send the requests to the running endpoint in the `/predict` route specified within the container following Vertex AI I/O payload formatting." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "79e393e97ddb" + }, + "source": [ + "### Via Python" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "1a404fd19a08" + }, + "source": [ + "#### Within the same session\n", + "\n", + "To run the online prediction via the Vertex AI SDK, you can simply use the `predict` method." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "4ab63079a69a" + }, + "outputs": [], + "source": [ + "output = deployed_model.predict(instances=prediction_request[\"instances\"])\n", + "output" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "b28efb473c7e" + }, + "source": [ + "#### From a different session\n", + "\n", + "To run the online prediction from a different session, you can run the following snippet." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "e003ab03f30f" + }, + "outputs": [], + "source": [ + "import base64\n", + "from io import BytesIO\n", + "import os\n", + "\n", + "from PIL import Image\n", + "from google.cloud import aiplatform\n", + "import requests\n", + "\n", + "PROJECT_ID = \"[your-project-id]\" # @param {type:\"string\", isTemplate: true}\n", + "if PROJECT_ID == \"[your-project-id]\":\n", + " PROJECT_ID = str(os.environ.get(\"GOOGLE_CLOUD_PROJECT\"))\n", + "\n", + "LOCATION = os.environ.get(\"GOOGLE_CLOUD_REGION\", \"us-central1\")\n", + "\n", + "aiplatform.init(project=PROJECT_ID, location=LOCATION)\n", + "\n", + "ENDPOINT_DISPLAY_NAME = \"google--paligemma-7b-it-endpoint\"\n", + "\n", + "# Iterates over all the Vertex AI Endpoints within the current project and keeps the first match (if any), otherwise set to None\n", + "ENDPOINT_ID = next(\n", + " (\n", + " endpoint.name\n", + " for endpoint in aiplatform.Endpoint.list()\n", + " if endpoint.display_name == ENDPOINT_DISPLAY_NAME\n", + " ),\n", + " None,\n", + ")\n", + "assert ENDPOINT_ID, (\n", + " \"`ENDPOINT_ID` is not set, please make sure that the `endpoint_display_name` is correct at \"\n", + " f\"https://console.cloud.google.com/vertex-ai/online-prediction/endpoints?project={os.getenv('PROJECT_ID')}\"\n", + ")\n", + "\n", + "# Initiate the endpoint\n", + "endpoint = aiplatform.Endpoint(\n", + " f\"projects/{PROJECT_ID}/locations/{LOCATION}/endpoints/{ENDPOINT_ID}\"\n", + ")\n", + "\n", + "# Get the image\n", + "IMAGE_URL = \"https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/transformers/tasks/car.jpg?download=true\" # @param {type:\"string\"}\n", + "image_response = requests.get(IMAGE_URL)\n", + "image_response.raise_for_status()\n", + "image = Image.open(BytesIO(image_response.content))\n", + "\n", + "# Generate the prediction\n", + "output = endpoint.predict(\n", + " instances=[\n", + " {\n", + " \"prompt\": \"caption it\",\n", + " \"image_base64\": base64.b64encode(image_response.content).decode(\"utf-8\"),\n", + " \"generation_kwargs\": {\"max_new_tokens\": 100, \"do_sample\": False},\n", + " }\n", + " ],\n", + ")\n", + "output" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "7cc0b19ab641" + }, + "source": [ + "### Via gcloud" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "8fed7007c31c" + }, + "source": [ + "You can also send the requests using the `gcloud` CLI via the `gcloud ai endpoints` command.\n", + "\n", + "> Note that, before proceeding, you should either replace the values or set the following environment variables in advance from the Python variables set in the example, as follows:\n", + ">\n", + "> ```python\n", + "> import os\n", + "> os.environ[\"PROJECT_ID\"] = PROJECT_ID\n", + "> os.environ[\"LOCATION\"] = LOCATION\n", + "> os.environ[\"ENDPOINT_NAME\"] = \"google--paligemma-7b-it-endpoint\"\n", + "> ```" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "7fb1547d409f" + }, + "outputs": [], + "source": [ + "%%bash\n", + "# Get Endpoint ID\n", + "ENDPOINT_ID=$(gcloud ai endpoints list \\\n", + " --project=$PROJECT_ID \\\n", + " --region=$LOCATION \\\n", + " --filter=\"display_name=$ENDPOINT_NAME\" \\\n", + " --format=\"value(name)\" \\\n", + " | cut -d'/' -f6)\n", + "\n", + "# Get the image in base64\n", + "IMAGE_URL=\"https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/transformers/tasks/car.jpg?download=true\" # @param {type:\"string\"}\n", + "BASE64_IMAGE=$(curl -s \"$IMAGE_URL\" | base64 -w 0)\n", + "\n", + "# Generate the prediction\n", + "echo '{\n", + " \"instances\": [\n", + " {\n", + " \"prompt\": \"caption it\",\n", + " \"image_base64\": \"${BASE64_IMAGE}\",\n", + " \"generation_kwargs\": {\"max_new_tokens\": 100, \"do_sample\": false}\n", + " }\n", + " ]\n", + "}' | gcloud ai endpoints predict $ENDPOINT_ID \\\n", + " --project=$PROJECT_ID \\\n", + " --region=$LOCATION \\\n", + " --json-request=\"-\"" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "7e588a65759e" + }, + "source": [ + "### Via cURL" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "cf3de731bcf4" + }, + "source": [ + "Alternatively, you can also send the requests via `cURL`.\n", + "\n", + "> Note that, before proceeding, you should either replace the values or set the following environment variables in advance from the Python variables set in the example, as follows:\n", + ">\n", + "> ```python\n", + "> import os\n", + "> os.environ[\"PROJECT_ID\"] = PROJECT_ID\n", + "> os.environ[\"LOCATION\"] = LOCATION\n", + "> os.environ[\"ENDPOINT_NAME\"] = \"google--paligemma-7b-it-endpoint\"\n", + "> ```" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "a71da4e1152b" + }, + "outputs": [], + "source": [ + "%%bash\n", + "# Get Endpoint ID\n", + "ENDPOINT_ID=$(gcloud ai endpoints list \\\n", + " --project=$PROJECT_ID \\\n", + " --region=$LOCATION \\\n", + " --filter=\"display_name=$ENDPOINT_NAME\" \\\n", + " --format=\"value(name)\" \\\n", + " | cut -d'/' -f6)\n", + "\n", + "# Get the image in base64\n", + "IMAGE_URL=\"https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/transformers/tasks/car.jpg?download=true\" # @param {type:\"string\"}\n", + "BASE64_IMAGE=$(curl -s \"$IMAGE_URL\" | base64 -w 0)\n", + "\n", + "# Generate the prediction\n", + "curl -X POST \\\n", + " -H \"Authorization: Bearer $(gcloud auth print-access-token)\" \\\n", + " -H \"Content-Type: application/json\" \\\n", + " \"https://${LOCATION}-aiplatform.googleapis.com/v1/projects/${PROJECT_ID}/locations/${LOCATION}/endpoints/${ENDPOINT_ID}:predict\" \\\n", + " -d @- <\n", + " \n", + " \n", + " \"Google
    Open in Colab\n", + "
    \n", + " \n", + " \n", + " \n", + " \"Google
    Open in Colab Enterprise\n", + "
    \n", + " \n", + " \n", + " \n", + " \"Vertex
    Open in Vertex AI Workbench\n", + "
    \n", + " \n", + " \n", + " \n", + " \"GitHub
    View on GitHub\n", + "
    \n", + " \n", + "\n", + "\n", + "
    \n", + "\n", + "Share to:\n", + "\n", + "\n", + " \"LinkedIn\n", + "\n", + "\n", + "\n", + " \"Bluesky\n", + "\n", + "\n", + "\n", + " \"X\n", + "\n", + "\n", + "\n", + " \"Reddit\n", + "\n", + "\n", + "\n", + " \"Facebook\n", + "" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "84f0f73a0f76" + }, + "source": [ + "| | |\n", + "|-|-|\n", + "| Author(s) | [Remigiusz Samborski](https://github.com/rsamborski) |" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "tvgnzT1CKxrO" + }, + "source": [ + "## Overview\n", + "\n", + "> [PLLuM from Ministry of Digital Affairs of Poland\n", + "](https://huggingface.co/CYFRAGOVPL) is a family of large language models (LLMs) specialized in Polish and other Slavic/Baltic languages, with additional English data incorporated for broader generalization.\n", + "\n", + "> [Vertex AI](https://cloud.google.com/vertex-ai/docs) provides a comprehensive platform for the entire machine learning lifecycle. It empowers you to build, train, and deploy ML models and AI applications, including customizing powerful large language models (LLMs).\n", + "\n", + "This notebook shows how to deploy Polish Large Language Model (PLLuM) from the Hugging Face Hub on Vertex AI using the Hugging Face Deep Learning Container (DLC) for Pytorch Inference in combination with a [custom handler](https://huggingface.co/docs/inference-endpoints/en/guides/custom_handler#create-custom-inference-handler).\n", + "\n", + "By the end of this notebook, you will learn how to:\n", + "\n", + "- Create a custom handler and test it\n", + "- Register any LLM from the Hugging Face Hub on Vertex AI\n", + "- Deploy an LLM on Vertex AI\n", + "- Send online predictions on Vertex AI" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "61RBz8LLbxCR" + }, + "source": [ + "## Get started" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "No17Cw5hgx12" + }, + "source": [ + "### Install Vertex AI SDK and other required packages\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "tFy3H3aPgx12" + }, + "outputs": [], + "source": [ + "%pip install --upgrade --user --quiet 'torch' 'torchvision' 'torchaudio'\n", + "%pip install --upgrade --user --quiet 'transformers' 'accelerate>=0.26.0'\n", + "%pip install --upgrade --user --quiet 'google-cloud-aiplatform[prediction]' 'crcmod' 'etils'" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "R5Xep4W9lq-Z" + }, + "source": [ + "### Restart runtime\n", + "\n", + "To use the newly installed packages in this Jupyter runtime, you must restart the runtime. You can do this by running the cell below, which restarts the current kernel.\n", + "\n", + "The restart might take a minute or longer. After it's restarted, continue to the next step." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "XRvKdaPDTznN" + }, + "outputs": [], + "source": [ + "import IPython\n", + "\n", + "app = IPython.Application.instance()\n", + "app.kernel.do_shutdown(True)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "SbmM4z7FOBpM" + }, + "source": [ + "
    \n", + "⚠ The kernel is going to restart. In Colab or Colab Enterprise, you might see an error message that says \"Your session crashed for an unknown reason.\" This is expected. Wait until it's finished before continuing to the next step. ⚠\n", + "
    \n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "dmWOrTJ3gx13" + }, + "source": [ + "### Authenticate your notebook environment (Colab only)\n", + "\n", + "If you're running this notebook on Google Colab, run the cell below to authenticate your environment." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "NyKGtVQjgx13" + }, + "outputs": [], + "source": [ + "import sys\n", + "\n", + "if \"google.colab\" in sys.modules:\n", + " from google.colab import auth\n", + "\n", + " auth.authenticate_user()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "e0vxVy5J3g41" + }, + "source": [ + "### Set Hugging Face `HF_HOME` variable" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "P5UCCYrV3pEW" + }, + "outputs": [], + "source": [ + "import os\n", + "\n", + "from etils.epath import Path\n", + "\n", + "ROOT_PATH = Path(\".\")\n", + "TUTORIAL_PATH = ROOT_PATH / \"deploy_pllum_with_custom_handler_tutorial\"\n", + "\n", + "os.environ[\"HF_HOME\"] = str(TUTORIAL_PATH)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "Dy5VO78rzX8c" + }, + "source": [ + "### Requirements" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "R8Zm9y0hxU5O" + }, + "source": [ + "#### Set Project ID and Location\n", + "\n", + "To get started using Vertex AI, you must have an existing Google Cloud project and [enable these APIs](https://console.cloud.google.com/flows/enableapi?apiid=aiplatform.googleapis.com,artifactregistry.googleapis.com).\n", + "\n", + "Learn more about [setting up a project and a development environment](https://cloud.google.com/vertex-ai/docs/start/cloud-environment)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "Nqwi-5ufWp_B" + }, + "outputs": [], + "source": [ + "# Use the environment variable if the user doesn't provide Project ID.\n", + "import os\n", + "\n", + "PROJECT_ID = \"[your-project-id]\" # @param {type: \"string\", placeholder: \"[your-project-id]\", isTemplate: true}\n", + "\n", + "if not PROJECT_ID or PROJECT_ID == \"[your-project-id]\":\n", + " PROJECT_ID = str(os.environ.get(\"GOOGLE_CLOUD_PROJECT\"))\n", + "\n", + "PROJECT_NUMBER = !gcloud projects describe {PROJECT_ID} --format=\"get(projectNumber)\"[0]\n", + "PROJECT_NUMBER = PROJECT_NUMBER[0]\n", + "\n", + "LOCATION = os.environ.get(\"GOOGLE_CLOUD_REGION\", \"us-central1\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "zgPO1eR3CYjk" + }, + "source": [ + "#### Set and create a Cloud Storage bucket\n", + "\n", + "Create a storage bucket to store intermediate artifacts such as models." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "MzGDU7TWdts_" + }, + "outputs": [], + "source": [ + "BUCKET_NAME = \"[your-bucket-name]\" # @param {type: \"string\", placeholder: \"[your-bucket-name]\", isTemplate: true}\n", + "BUCKET_URI = f\"gs://{BUCKET_NAME}\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "a-Yz-SSagqUW" + }, + "outputs": [], + "source": [ + "! gsutil mb -l {LOCATION} -p {PROJECT_ID} {BUCKET_URI}" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "set_service_account" + }, + "source": [ + "#### Set Service Account and permissions\n", + "\n", + "You will need to have the following IAM roles set:\n", + "\n", + "- Vertex AI User (roles/aiplatform.user)\n", + "- Artifact Registry Reader (roles/artifactregistry.reader)\n", + "- Storage Object Admin (roles/storage.objectAdmin)\n", + "\n", + "For more information about granting roles, see [Manage access](https://cloud.google.com/iam/docs/granting-changing-revoking-access).\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "VX9tpdtuQI5L" + }, + "source": [ + "> If you run following commands using Vertex AI Workbench, run directly in the terminal.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "ssUJJqXJJHgC" + }, + "outputs": [], + "source": [ + "SERVICE_ACCOUNT = f\"{PROJECT_NUMBER}-compute@developer.gserviceaccount.com\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "wqOHg5aid6HP" + }, + "outputs": [], + "source": [ + "for role in ['aiplatform.user', 'storage.objectAdmin', 'artifactregistry.reader']:\n", + "\n", + " ! gcloud projects add-iam-policy-binding {PROJECT_ID} \\\n", + " --member=serviceAccount:{SERVICE_ACCOUNT} \\\n", + " --role=roles/{role} --condition=None" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "EjebjdiNxe_D" + }, + "source": [ + "### Initiate Vertex AI SDK\n", + "\n", + "Initiate Vertex AI client session." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "GReulMr0xjZs" + }, + "outputs": [], + "source": [ + "import vertexai\n", + "\n", + "vertexai.init(project=PROJECT_ID, location=LOCATION, staging_bucket=BUCKET_URI)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "8XvMcYzm7xmm" + }, + "source": [ + "### Import libraries\n", + "\n", + "Import relevant libraries." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "3fXW5p7o7zlo" + }, + "outputs": [], + "source": [ + "import json\n", + "\n", + "from etils import epath\n", + "from google.cloud import aiplatform\n", + "from google.cloud.aiplatform import Endpoint, Model\n", + "from google.cloud.aiplatform.prediction import LocalModel\n", + "import torch\n", + "from transformers import AutoModelForCausalLM, AutoTokenizer" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "E4rjOknk5yvq" + }, + "source": [ + "### Helpers\n", + "\n", + "Define some helpers." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "MOoaB5xf51BD" + }, + "outputs": [], + "source": [ + "def hf_model_path(hf_home_path: str) -> str:\n", + " \"\"\"\n", + " Copy model files from the Hugging Face cache.\n", + " \"\"\"\n", + "\n", + " # Convert and expand the input path\n", + " base_path = Path(hf_home_path).expanduser()\n", + "\n", + " if not base_path.exists():\n", + " raise FileNotFoundError(f\"Cache path does not exist: {base_path}\")\n", + "\n", + " # Find the deepest snapshot directory\n", + " snapshot_path = None\n", + " for model_dir in base_path.iterdir():\n", + " if not model_dir.is_dir():\n", + " continue\n", + "\n", + " # Look for 'snapshots' directory\n", + " snapshots_dir = model_dir / \"snapshots\"\n", + " if not snapshots_dir.exists():\n", + " continue\n", + "\n", + " # Get the first snapshot folder (usually there's only one)\n", + " for snapshot in snapshots_dir.iterdir():\n", + " if snapshot.is_dir():\n", + " snapshot_path = snapshot\n", + " break\n", + "\n", + " if snapshot_path:\n", + " break\n", + " if not snapshot_path:\n", + " raise FileNotFoundError(\"No snapshot directory found in the cache\")\n", + "\n", + " return snapshot_path\n", + "\n", + "\n", + "def get_cuda_device_names():\n", + " \"\"\"A function to get the list of NVIDIA GPUs\"\"\"\n", + " if not torch.cuda.is_available():\n", + " return None\n", + "\n", + " return [str(i) for i in range(torch.cuda.device_count())]" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "EdvJRUWRNGHE" + }, + "source": [ + "## Prepare a custom handler to serve PLLuM on Vertex AI\n", + "\n", + "Custom Handlers are custom classes in Python that define the pre-processing, inference, and post-processing steps required to run the inference using HuggingFace Pytorch Prediction container on Vertex AI.\n", + "\n", + "Think of Custom Handlers as personalized instructions for Hugging Face models. They define how to prepare the input data, run the model, and handle the results. In this sense, Custom Handlers add flexibility. They let you customize how data is prepared and processed, add extra steps, and even build in custom measurements or logging. This means you can tailor the process to your exact needs when the standard setup isn't sufficient.\n", + "\n", + "These instructions are stored in a file named `handler.py`. If you need additional dependecies, you can list it in a `requirements.txt` file. The PyTorch container automatically finds and uses these files if they're present.\n", + "\n", + "Have a look at [đŸ€— Serve Anything with Inference Endpoints + Custom Handlers](https://huggingface.co/blog/alvarobartt/serve-anything-inference-endpoints-custom-code) to learn more." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "CgPwfG07iMCS" + }, + "source": [ + "### Test the `handler` locally\n", + "\n", + "Before to build the handler module, you can test its coding locally.\n", + "\n", + "*Note: this step is resource exhausting, so make sure you're running it on capable hardware (i.e. g2-standard-8).*" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "OVLBAtraJkQ_" + }, + "source": [ + "#### Load PLLuM from HuggingFace Hub\n", + "\n", + "Load a pre-trained model called PLLuM for text generation. You first sets up the text preprocessor, which handles tasks like tokenization (breaking text into words or sub-words). Then you load the actual model itself, optimized for lower memory usage and automatic device placement (likely GPU if available).\n", + "\n", + "If your hardware (e.g., newer GPUs) supports bfloat16, you can reduce memory usage and potentially speed up inference by setting `gpu_with_bfloat16` flag to True." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "QmHijFltikJZ" + }, + "outputs": [], + "source": [ + "gpu_with_bfloat16 = True # @param {type: \"boolean\"}\n", + "model_name = \"CYFRAGOVPL/PLLuM-12B-chat\" # @param {type: \"string\"}\n", + "tokenizer = AutoTokenizer.from_pretrained(model_name)\n", + "\n", + "if gpu_with_bfloat16:\n", + " model = AutoModelForCausalLM.from_pretrained(\n", + " model_name,\n", + " torch_dtype=torch.bfloat16,\n", + " device_map=\"auto\", # automatically places model layers on available devices\n", + " )\n", + "else:\n", + " model = AutoModelForCausalLM.from_pretrained(model_name)\n", + "\n", + "model.eval()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "j5Qir_dtJ0-X" + }, + "source": [ + "#### Prepare the prediction request\n", + "\n", + "Prepare the prompt using the tokenizer.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "c5MJVNnvbU0Y" + }, + "outputs": [], + "source": [ + "# EN:\"Write a short poem about spring.\"\n", + "prompt = \"Napisz krĂłtki wiersz o wioƛnie.\" # @param {type: \"string\"}\n", + "\n", + "prediction_request = {\n", + " \"instances\": [\n", + " {\n", + " \"prompt\": prompt,\n", + " \"generation_kwargs\": {\"max_new_tokens\": 50, \"do_sample\": True},\n", + " }\n", + " ]\n", + "}" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "DH-WU2IAJ4yY" + }, + "source": [ + "#### Generate prediction\n", + "\n", + "Process a prediction request based on the prompt above." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "UyKf2v3vlOtY" + }, + "outputs": [], + "source": [ + "predictions = []\n", + "\n", + "for instance in prediction_request[\"instances\"]:\n", + " if \"prompt\" not in instance:\n", + " raise ValueError(\n", + " \"The request body for each instance must contain the `prompt` key.\"\n", + " )\n", + "\n", + " inputs = tokenizer(\n", + " instance[\"prompt\"], return_tensors=\"pt\", return_token_type_ids=False\n", + " ).to(model.device)\n", + "\n", + " input_len = inputs[\"input_ids\"].shape[-1]\n", + " with torch.inference_mode():\n", + " generation_kwargs = instance.get(\n", + " \"generation_kwargs\",\n", + " {\n", + " \"max_new_tokens\": 100,\n", + " \"do_sample\": False,\n", + " \"top_k\": 50,\n", + " \"top_p\": 0.9,\n", + " \"temperature\": 0.7,\n", + " },\n", + " )\n", + " generation = model.generate(**inputs, **generation_kwargs)\n", + " generation = generation[0][input_len:]\n", + " response = tokenizer.decode(generation, skip_special_tokens=True)\n", + " predictions.append(response)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "zzmvzmxgaiFc" + }, + "source": [ + "Print the prediction." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "XlML8-o8lV9X" + }, + "outputs": [], + "source": [ + "print(predictions)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "Yvh-vsUblb-W" + }, + "source": [ + "### Define the `handler.py` module\n", + "\n", + "After testing the handler code, you assemble the code in a Python module which defines the custom inference handler for PLLuM.\n", + "\n", + "You write this handler code to a file named handler.py within the model directory." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "Qa5_u-Sn6OZ4" + }, + "outputs": [], + "source": [ + "model_path = hf_model_path(TUTORIAL_PATH / \"hub\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "WpBfVVbtoA_m" + }, + "outputs": [], + "source": [ + "handler_module = \"\"\"\n", + "from typing import Any, Dict, List\n", + "import torch\n", + "from transformers import AutoTokenizer, AutoModelForCausalLM\n", + "import base64\n", + "from io import BytesIO\n", + "import logging\n", + "import sys\n", + "\n", + "# Configure logging to output to stdout\n", + "logging.basicConfig(\n", + " level=logging.INFO,\n", + " format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',\n", + " handlers=[logging.StreamHandler(sys.stdout)]\n", + ")\n", + "logger = logging.getLogger('huggingface_inference_toolkit')\n", + "\n", + "class EndpointHandler:\n", + " def __init__(\n", + " self,\n", + " model_dir: str = '/opt/huggingface/model',\n", + " **kwargs: Any,\n", + " ) -> None:\n", + " self.processor = AutoTokenizer.from_pretrained(model_dir)\n", + "\n", + " self.model = AutoModelForCausalLM.from_pretrained(\n", + " model_dir,\n", + " torch_dtype=torch.bfloat16,\n", + " device_map=\"auto\" # automatically places model layers on available devices\n", + " ).eval()\n", + "\n", + " def __call__(self, data: Dict[str, Any]) -> Dict[str, List[Any]]:\n", + " logger.info(\"Processing new request\")\n", + " predictions = []\n", + "\n", + " for instance in data['instances']:\n", + " logger.info(f\"Processing instance: {instance.get('prompt', '')[:100]}...\")\n", + "\n", + " if \"prompt\" not in instance:\n", + " error_msg = \"Missing prompt in request body\"\n", + " logger.info(error_msg)\n", + " raise ValueError(error_msg)\n", + "\n", + " inputs = self.processor(\n", + " instance[\"prompt\"], return_tensors=\"pt\", return_token_type_ids=False\n", + " ).to(self.model.device)\n", + " input_len = inputs[\"input_ids\"].shape[-1]\n", + " logger.info(f\"Input processed, length: {input_len}\")\n", + "\n", + " with torch.inference_mode():\n", + " generation_kwargs = data.get(\n", + " \"generation_kwargs\", {\n", + " \"max_new_tokens\": 100,\n", + " \"do_sample\": False,\n", + " \"top_k\": 50,\n", + " \"top_p\": 0.9,\n", + " \"temperature\": 0.7\n", + " }\n", + " )\n", + " logger.info(f\"Generation kwargs: {generation_kwargs}\")\n", + "\n", + " generation = self.model.generate(**inputs, **generation_kwargs)\n", + " generation = generation[0][input_len:]\n", + " response = self.processor.decode(generation, skip_special_tokens=True)\n", + " logger.info(f\"Generated response: {response[:100]}...\")\n", + " predictions.append(response)\n", + "\n", + " logger.info(f\"Successfully processed {len(predictions)} instances\")\n", + " return {\"predictions\": predictions}\n", + "\"\"\"\n", + "\n", + "with open(model_path / \"handler.py\", \"w\") as hf:\n", + " hf.write(handler_module)\n", + "hf.close()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "3zOgAT4DpO8Z" + }, + "source": [ + "### Copy model with custom handler on Cloud Bucket\n", + "\n", + "Efficiently upload the model directory to Google Cloud Storage using `gsutil`.\n", + "\n", + "Note that `-m` enables multi-threaded uploads for faster transfer, especially for large directories. `-o GSUtil:parallel_composite_upload_threshold=150M` optimizes large file uploads by splitting them into smaller parts for parallel transfer, significantly speeding up the process for files larger than 150MB." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "daQnz0Kv6Khn" + }, + "outputs": [], + "source": [ + "model_uri = epath.Path(BUCKET_URI) / \"model\"\n", + "model_uri.mkdir(parents=True, exist_ok=True)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "ukQWCwz1pXzn" + }, + "outputs": [], + "source": [ + "! gsutil -o GSUtil:parallel_composite_upload_threshold=150M -m cp -r {str(model_path)}/* {str(model_uri)}" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "xgcPjmMfXwAg" + }, + "source": [ + "### (Optional) Testing the handler offline using serving container with Vertex AI LocalModel\n", + "\n", + "For debugging purpose, Vertex AI provides `LocalModel` class, accessible through the Vertex AI SDK for Python. This class allows you to build and deploy your model locally, simulating the Vertex AI environment. Using LocalModel involves creating a Docker image that encapsulates your custom predictor code and the associated handler.\n", + "\n", + "> **Important**: Running the LocalModel class requires a local Docker installation. This allows the model to be encapsulated within a container for consistent execution across different environments.\n", + "\n", + "> If you haven't already installed Docker Engine, please refer to the official installation guide: [Install Docker Engine](https://docs.docker.com/engine/install/). This documentation provides detailed instructions for various operating systems and will guide you through the installation process. Ensure Docker is running correctly before proceeding with the LocalModel examples.\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "Im5wedPQuMoY" + }, + "source": [ + "#### Create a LocalModel instance\n", + "\n", + "Set up a local model by specifying the container image to use (a Hugging Face Transformers model optimized for PyTorch and GPUs) and the port it will listen on (5000).\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "VS9Z75d9uW3x" + }, + "outputs": [], + "source": [ + "local_pllum_model = LocalModel(\n", + " serving_container_image_uri=\"us-docker.pkg.dev/deeplearning-platform-release/gcr.io/huggingface-pytorch-inference-cu121.2-3.transformers.4-48.ubuntu2204.py311\",\n", + " serving_container_ports=[5000],\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "3bxcGRl8uas-" + }, + "source": [ + "#### Create a LocalEndpoint instance\n", + "\n", + "Deploy the model to a local endpoint for serving. You load the model from the specified location (model_uri) and start serving the model locally.\n", + "\n", + "The `gpu_device_ids` sets leverages available GPUs if present.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "hfbRbRHcuee3" + }, + "outputs": [], + "source": [ + "local_pllum_endpoint = local_pllum_model.deploy_to_local_endpoint(\n", + " artifact_uri=str(model_uri), gpu_device_ids=get_cuda_device_names()\n", + ")\n", + "\n", + "local_pllum_endpoint.serve()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "XmnfGmT8sQ-k" + }, + "source": [ + "#### Monitoring Your Containerized Deployment\n", + "\n", + "To keep track of your container's deployment progress and identify any potential issues, you can use the following Docker commands within your terminal:\n", + "\n", + "1. **List all containers:** `docker container ls -a` displays a list of all running and stopped containers. Locate the container associated with your local endpoint and copy its ID. This ID is essential for the next step.\n", + "\n", + "2. **Stream container logs:** `docker logs --follow ` provides a real-time stream of your container's logs. Replace `` with the ID you copied in the previous step. Monitoring these logs allows you to observe the deployment process, identify any errors or warnings, and understand the container's overall health." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "7EcVi9Y_ujFj" + }, + "source": [ + "#### Generate predictions\n", + "\n", + "Send a prediction request to a local Vertex AI endpoint.\n", + "\n", + "You convert the request data (prediction_request) into a JSON string, send it to the endpoint, and then print the predictions from the JSON response.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "Iso7dA7BzQiy" + }, + "outputs": [], + "source": [ + "vertex_prediction_request = json.dumps(prediction_request)\n", + "vertex_prediction_response = local_pllum_endpoint.predict(\n", + " request=vertex_prediction_request, headers={\"Content-Type\": \"application/json\"}\n", + ")\n", + "print(vertex_prediction_response.json()[\"predictions\"])" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "ecK96GglLTEb" + }, + "source": [ + "## Register PLLuM on Vertex AI" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "28d24c673821" + }, + "source": [ + "To serve PLLuM with Pytorch Inference on Vertex AI, you start importing the model on Vertex AI Model Registry, a central repository where you can manage the lifecycle of your ML models on Vertex AI.\n", + "\n", + "Before going into the code to upload or import a model on Vertex AI, let's quickly review the arguments provided to the `aiplatform.Model.upload` method:\n", + "\n", + "* **`display_name`** is the name that will be shown in the Vertex AI Model Registry.\n", + "\n", + "* **`serving_container_image_uri`** is the location of the Hugging Face DLC for Pytorch Inference that will be used for serving the model.\n", + "\n", + "* (optional) **`serving_container_ports`** is the port where the Vertex AI endpoint |will be exposed, by default 8080.\n", + "\n", + "For more information on the supported `aiplatform.Model.upload` arguments, check [its Python reference](https://cloud.google.com/python/docs/reference/aiplatform/latest/google.cloud.aiplatform.Model#google_cloud_aiplatform_Model_upload)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "ef487ce082f3" + }, + "outputs": [], + "source": [ + "model = Model.upload(\n", + " display_name=\"cyfragovpl--pllum-12b-it\",\n", + " artifact_uri=str(model_uri),\n", + " serving_container_image_uri=\"us-docker.pkg.dev/deeplearning-platform-release/gcr.io/huggingface-pytorch-inference-cu121.2-3.transformers.4-48.ubuntu2204.py311\",\n", + " serving_container_ports=[8080],\n", + ")\n", + "model.wait()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "c427c0a87016" + }, + "source": [ + "## Deploy PLLuM on Vertex AI" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "ef4cd160dc09" + }, + "source": [ + "After the model is registered on Vertex AI, you can deploy the model to an endpoint.\n", + "\n", + "You need to first deploy a model to an endpoint before that model can be used to serve online predictions. Deploying a model associates physical resources with the model so it can serve online predictions with low latency.\n", + "\n", + "Before going into the code to deploy a model to an endpoint, let's quickly review the arguments provided to the `aiplatform.Model.deploy` method:\n", + "\n", + "- **`endpoint`** is the endpoint to deploy the model to, which is optional, and by default will be set to the model display name with the `_endpoint` suffix.\n", + "- **`machine_type`**, **`accelerator_type`** and **`accelerator_count`** are arguments that define which instance to use, and additionally, the accelerator to use and the number of accelerators, respectively. The `machine_type` and the `accelerator_type` are tied together, so you will need to select an instance that supports the accelerator that you are using and vice-versa. More information about the different instances at [Compute Engine Documentation - GPU machine types](https://cloud.google.com/compute/docs/gpus), and about the `accelerator_type` naming at [Vertex AI Documentation - MachineSpec](https://cloud.google.com/vertex-ai/docs/reference/rest/v1/MachineSpec).\n", + "\n", + "For more information on the supported `aiplatform.Model.deploy` arguments, you can check [its Python reference](https://cloud.google.com/python/docs/reference/aiplatform/latest/google.cloud.aiplatform.Model#google_cloud_aiplatform_Model_deploy)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "e777427015db" + }, + "outputs": [], + "source": [ + "deployed_model = model.deploy(\n", + " endpoint=Endpoint.create(display_name=\"cyfragovpl--pllum-12b-it-endpoint\"),\n", + " machine_type=\"g2-standard-8\",\n", + " accelerator_type=\"NVIDIA_L4\",\n", + " accelerator_count=1,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "5f2380d3ea60" + }, + "source": [ + "> Note that the model deployment on Vertex AI can take around 15 to 25 minutes; most of the time being the allocation / reservation of the resources, setting up the network and security, and such." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "9b3fd1898241" + }, + "source": [ + "## Online predictions on Vertex AI" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "2aa9cee03bd0" + }, + "source": [ + "Once the model is deployed on Vertex AI, you can run the online predictions using the `aiplatform.Endpoint.predict` method, which will send the requests to the running endpoint in the `/predict` route specified within the container following Vertex AI I/O payload formatting." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "79e393e97ddb" + }, + "source": [ + "### Via Python" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "1a404fd19a08" + }, + "source": [ + "#### Within the same session\n", + "\n", + "To run the online prediction via the Vertex AI SDK, you can simply use the `predict` method." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "4ab63079a69a" + }, + "outputs": [], + "source": [ + "output = deployed_model.predict(instances=prediction_request[\"instances\"])\n", + "output" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "b28efb473c7e" + }, + "source": [ + "#### From a different session\n", + "\n", + "To run the online prediction from a different session, you can run the following snippet." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "e003ab03f30f" + }, + "outputs": [], + "source": [ + "import os\n", + "\n", + "from google.cloud import aiplatform\n", + "\n", + "PROJECT_ID = \"\" # @param {type:\"string\", isTemplate: true}\n", + "if PROJECT_ID == \"[your-project-id]\":\n", + " PROJECT_ID = str(os.environ.get(\"GOOGLE_CLOUD_PROJECT\"))\n", + "\n", + "LOCATION = os.environ.get(\"GOOGLE_CLOUD_REGION\", \"us-central1\")\n", + "\n", + "aiplatform.init(project=PROJECT_ID, location=LOCATION)\n", + "\n", + "ENDPOINT_DISPLAY_NAME = \"cyfragovpl--pllum-12b-it-endpoint\"\n", + "\n", + "# Iterates over all the Vertex AI Endpoints within the current project and keeps the first match (if any), otherwise set to None\n", + "ENDPOINT_ID = next(\n", + " (\n", + " endpoint.name\n", + " for endpoint in aiplatform.Endpoint.list()\n", + " if endpoint.display_name == ENDPOINT_DISPLAY_NAME\n", + " ),\n", + " None,\n", + ")\n", + "assert ENDPOINT_ID, (\n", + " \"`ENDPOINT_ID` is not set, please make sure that the `endpoint_display_name` is correct at \"\n", + " f\"https://console.cloud.google.com/vertex-ai/online-prediction/endpoints?project={os.getenv('PROJECT_ID')}\"\n", + ")\n", + "\n", + "# Initiate the endpoint\n", + "endpoint = aiplatform.Endpoint(\n", + " f\"projects/{PROJECT_ID}/locations/{LOCATION}/endpoints/{ENDPOINT_ID}\"\n", + ")\n", + "\n", + "# Generate the prediction\n", + "output = endpoint.predict(\n", + " instances=[\n", + " {\n", + " \"prompt\": \"Napisz krĂłtki wiersz o zimie.\",\n", + " \"generation_kwargs\": {\"max_new_tokens\": 100, \"do_sample\": False},\n", + " }\n", + " ],\n", + ")\n", + "output" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "7cc0b19ab641" + }, + "source": [ + "### Via gcloud" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "8fed7007c31c" + }, + "source": [ + "You can also send the requests using the `gcloud` CLI via the `gcloud ai endpoints` command.\n", + "\n", + "> Note that, before proceeding, you should either replace the values or set the following environment variables in advance from the Python variables set in the example, as follows:\n", + ">\n", + "> ```python\n", + "> import os\n", + "> os.environ[\"PROJECT_ID\"] = PROJECT_ID\n", + "> os.environ[\"LOCATION\"] = LOCATION\n", + "> os.environ[\"ENDPOINT_NAME\"] = \"cyfragovpl--pllum-12b-it-endpoint\"\n", + "> ```" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "7fb1547d409f" + }, + "outputs": [], + "source": [ + "%%bash\n", + "# Get Endpoint ID\n", + "ENDPOINT_ID=$(gcloud ai endpoints list \\\n", + " --project=$PROJECT_ID \\\n", + " --region=$LOCATION \\\n", + " --filter=\"display_name=$ENDPOINT_NAME\" \\\n", + " --format=\"value(name)\" \\\n", + " | cut -d'/' -f6)\n", + "\n", + "# Generate the prediction\n", + "echo '{\n", + " \"instances\": [\n", + " {\n", + " \"prompt\": \"Napisz krĂłtki wiersz o jesieni.\",\n", + " \"generation_kwargs\": {\"max_new_tokens\": 100, \"do_sample\": false}\n", + " }\n", + " ]\n", + "}' | gcloud ai endpoints predict $ENDPOINT_ID \\\n", + " --project=$PROJECT_ID \\\n", + " --region=$LOCATION \\\n", + " --json-request=\"-\"" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "7e588a65759e" + }, + "source": [ + "### Via cURL" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "cf3de731bcf4" + }, + "source": [ + "Alternatively, you can also send the requests via `cURL`.\n", + "\n", + "> Note that, before proceeding, you should either replace the values or set the following environment variables in advance from the Python variables set in the example, as follows:\n", + ">\n", + "> ```python\n", + "> import os\n", + "> os.environ[\"PROJECT_ID\"] = PROJECT_ID\n", + "> os.environ[\"LOCATION\"] = LOCATION\n", + "> os.environ[\"ENDPOINT_NAME\"] = \"cyfragovpl--pllum-12b-it-endpoint\"\n", + "> ```" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "a71da4e1152b" + }, + "outputs": [], + "source": [ + "%%bash\n", + "# Get Endpoint ID\n", + "ENDPOINT_ID=$(gcloud ai endpoints list \\\n", + " --project=$PROJECT_ID \\\n", + " --region=$LOCATION \\\n", + " --filter=\"display_name=$ENDPOINT_NAME\" \\\n", + " --format=\"value(name)\" \\\n", + " | cut -d'/' -f6)\n", + "\n", + "# Generate the prediction\n", + "curl -X POST \\\n", + " -H \"Authorization: Bearer $(gcloud auth print-access-token)\" \\\n", + " -H \"Content-Type: application/json\" \\\n", + " \"https://${LOCATION}-aiplatform.googleapis.com/v1/projects/${PROJECT_ID}/locations/${LOCATION}/endpoints/${ENDPOINT_ID}:predict\" \\\n", + " -d @- <\n", + " \n", + " \n", + " \"Google
    Open in Colab\n", + "
    \n", + " \n", + " \n", + " \n", + " \"Google
    Open in Colab Enterprise\n", + "
    \n", + " \n", + " \n", + " \n", + " \"Vertex
    Open in Vertex AI Workbench\n", + "
    \n", + " \n", + " \n", + " \n", + " \"GitHub
    View on GitHub\n", + "
    \n", + " \n", + "\n", + "\n", + "
    \n", + "\n", + "Share to:\n", + "\n", + "\n", + " \"LinkedIn\n", + "\n", + "\n", + "\n", + " \"Bluesky\n", + "\n", + "\n", + "\n", + " \"X\n", + "\n", + "\n", + "\n", + " \"Reddit\n", + "\n", + "\n", + "\n", + " \"Facebook\n", + " " + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "83b98b0ba19c" + }, + "source": [ + "![Hugging Face x Google Cloud](https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/google-cloud/thumbnail.png)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "84f0f73a0f76" + }, + "source": [ + "| | |\n", + "|-|-|\n", + "| Author(s) | [Alvaro Bartolome](https://github.com/alvarobartt), [Philipp Schmid](https://github.com/philschmid), [Simon Pagezy](https://github.com/pagezyhf), and [Jeff Boudier](https://github.com/jeffboudier) |" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "ccd500ae19b5" + }, + "source": [ + "## Overview" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "tvgnzT1CKxrO" + }, + "source": [ + "> [**Gemma**](https://ai.google.dev/gemma) is a family of lightweight, state-of-the-art open models built from the same research and technology used to create the Gemini models, developed by Google DeepMind and other teams across Google.\n", + "\n", + "> [**Text Generation Inference (TGI)**](https://github.com/huggingface/text-generation-inference) is a toolkit developed by Hugging Face for deploying and serving LLMs, with high performance text generation.\n", + "\n", + "> [**Hugging Face DLCs**](https://github.com/huggingface/Google-Cloud-Containers) are pre-built and optimized Deep Learning Containers (DLCs) maintained by Hugging Face and Google Cloud teams to simplify environment configuration for your ML workloads.\n", + "\n", + "> [**Google Vertex AI**](https://cloud.google.com/vertex-ai) is a Machine Learning (ML) platform that lets you train and deploy ML models and AI applications, and customize large language models (LLMs) for use in your AI-powered applications.\n", + "\n", + "This notebook showcases how to deploy Google Gemma from the Hugging Face Hub on Vertex AI using the Hugging Face Deep Learning Container (DLC) for Text Generation Inference (TGI).\n", + "\n", + "By the end of this notebook, you will learn how to:\n", + "\n", + "- Register any LLM from the Hugging Face Hub on Vertex AI\n", + "- Deploy an LLM on Vertex AI\n", + "- Send online predictions on Vertex AI" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "61RBz8LLbxCR" + }, + "source": [ + "## Get started" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "No17Cw5hgx12" + }, + "source": [ + "### Install Vertex AI SDK and other required packages\n", + "\n", + "To run this example, you will only need the [`google-cloud-aiplatform`](https://github.com/googleapis/python-aiplatform) Python SDK and the [`huggingface_hub`](https://github.com/huggingface/huggingface_hub) Python package." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "tFy3H3aPgx12" + }, + "outputs": [], + "source": [ + "%pip install --upgrade --user --quiet google-cloud-aiplatform huggingface_hub" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "R5Xep4W9lq-Z" + }, + "source": [ + "### Restart runtime (Colab only)\n", + "\n", + "To use the newly installed packages in this Jupyter environment, if you are on Colab you must restart the runtime. You can do this by running the cell below, which restarts the current kernel. The restart might take a minute or longer. After it's restarted, continue to the next step." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "XRvKdaPDTznN" + }, + "outputs": [], + "source": [ + "# Automatically restart kernel after installs so that your environment can access the new packages\n", + "# import IPython\n", + "\n", + "# app = IPython.Application.instance()\n", + "# app.kernel.do_shutdown(True)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "SbmM4z7FOBpM" + }, + "source": [ + "
    \n", + "⚠ The kernel is going to restart. Wait until it's finished before continuing to the next step. ⚠\n", + "
    \n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "dmWOrTJ3gx13" + }, + "source": [ + "### Authenticate your Google Cloud account\n", + "\n", + "Depending on your Jupyter environment, you may have to manually authenticate. Follow the relevant instructions below." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "9cc332aee5fd" + }, + "source": [ + "**1. Vertex AI Workbench**\n", + "\n", + "* Do nothing as you are already authenticated." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "54c81e1a63e6" + }, + "source": [ + "**2. Local JupyterLab instance, uncomment and run:**" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "c93d96bc9c43" + }, + "outputs": [], + "source": [ + "# !gcloud auth login" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "d3e571ce6c56" + }, + "source": [ + "**3. Colab, uncomment and run:**" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "984a0526fb68" + }, + "outputs": [], + "source": [ + "# from google.colab import auth\n", + "# auth.authenticate_user()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "f9af3e57f89a" + }, + "source": [ + "### Authenticate your Hugging Face account\n", + "\n", + "As [`google/gemma-7b-it`](https://huggingface.co/google/gemma-7b-it) is a gated model, you need to have a Hugging Face Hub account, and accept the Google's usage license for Gemma. Once that's done, you need to generate a new user access token with read-only access so that the weights can be downloaded from the Hub in the Hugging Face DLC for TGI.\n", + "\n", + "> Note that the user access token can only be generated via [the Hugging Face Hub UI](https://huggingface.co/settings/tokens/new), where you can either select read-only access to your account, or follow the recommendations and generate a fine-grained token with read-only access to [`google/gemma-7b-it`](https://huggingface.co/google/gemma-7b-it)." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "4c31c7272804" + }, + "source": [ + "Then you can install the `huggingface_hub` that comes with a CLI that will be used for the authentication with the token generated in advance. So that then the token can be safely retrieved via `huggingface_hub.get_token`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "8d836e0210fe" + }, + "outputs": [], + "source": [ + "from huggingface_hub import interpreter_login\n", + "\n", + "interpreter_login()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "c71a4314c250" + }, + "source": [ + "Read more about [Hugging Face Security](https://huggingface.co/docs/hub/en/security), specifically about [Hugging Face User Access Tokens](https://huggingface.co/docs/hub/en/security-tokens)." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "DF4l8DTdWgPY" + }, + "source": [ + "### Set Google Cloud project information and initialize Vertex AI SDK\n", + "\n", + "To get started using Vertex AI, you must have an existing Google Cloud project and [enable the Vertex AI API](https://console.cloud.google.com/flows/enableapi?apiid=aiplatform.googleapis.com), if not enabled already.\n", + "\n", + "Learn more about [setting up a project and a development environment](https://cloud.google.com/vertex-ai/docs/start/cloud-environment)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "Nqwi-5ufWp_B" + }, + "outputs": [], + "source": [ + "import os\n", + "\n", + "from google.cloud import aiplatform\n", + "\n", + "PROJECT_ID = \"[your-project-id]\" # @param {type:\"string\", isTemplate: true}\n", + "if PROJECT_ID == \"[your-project-id]\":\n", + " PROJECT_ID = str(os.environ.get(\"GOOGLE_CLOUD_PROJECT\"))\n", + "\n", + "LOCATION = os.environ.get(\"GOOGLE_CLOUD_REGION\", \"us-central1\")\n", + "\n", + "aiplatform.init(project=PROJECT_ID, location=LOCATION)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "ee37e1544281" + }, + "source": [ + "## Requirements" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "877cd3fb2dce" + }, + "source": [ + "You will need to have the following IAM roles set:\n", + "\n", + "- Artifact Registry Reader (roles/artifactregistry.reader)\n", + "- Vertex AI User (roles/aiplatform.user)\n", + "\n", + "For more information about granting roles, see [Manage access](https://cloud.google.com/iam/docs/granting-changing-revoking-access).\n", + "\n", + "---\n", + "\n", + "You will also need to enable the following APIs (if not enabled already):\n", + "\n", + "- Vertex AI API (aiplatform.googleapis.com)\n", + "- Artifact Registry API (artifactregistry.googleapis.com)\n", + "\n", + "For more information about API enablement, see [Enabling APIs](https://cloud.google.com/apis/docs/getting-started#enabling_apis).\n", + "\n", + "---\n", + "\n", + "To access Gemma on Hugging Face, you’re required to review and agree to Google’s usage license on the Hugging Face Hub for any of the models from the [Gemma release collection](https://huggingface.co/collections/google/gemma-release-65d5efbccdbb8c4202ec078b), and the access request will be processed inmediately." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "EdvJRUWRNGHE" + }, + "source": [ + "## Register Google Gemma on Vertex AI" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "28d24c673821" + }, + "source": [ + "To serve Gemma with Text Generation Inference (TGI) on Vertex AI, you start importing the model on Vertex AI Model Registry.\n", + "\n", + "The Vertex AI Model Registry is a central repository where you can manage the lifecycle of your ML models. From the Model Registry, you have an overview of your models so you can better organize, track, and train new versions. When you have a model version you would like to deploy, you can assign it to an endpoint directly from the registry, or using aliases, deploy models to an endpoint. The models on the Vertex AI Model Registry just contain the model configuration and not the weights per se, as it's not a storage.\n", + "\n", + "Before going into the code to upload or import a model on Vertex AI, let's quickly review the arguments provided to the `aiplatform.Model.upload` method:\n", + "\n", + "* **`display_name`** is the name that will be shown in the Vertex AI Model Registry.\n", + "\n", + "* **`serving_container_image_uri`** is the location of the Hugging Face DLC for TGI that will be used for serving the model.\n", + "\n", + "* **`serving_container_environment_variables`** are the environment variables that will be used during the container runtime, so these are aligned with the environment variables defined by `text-generation-inference`, which are analog to the [`text-generation-launcher` arguments](https://huggingface.co/docs/text-generation-inference/en/basic_tutorials/launcher). Additionally, the Hugging Face DLCs for TGI also capture the `AIP_` environment variables from Vertex AI as in [Vertex AI Documentation - Custom container requirements for prediction](https://cloud.google.com/vertex-ai/docs/predictions/custom-container-requirements).\n", + "\n", + " * `MODEL_ID` is the identifier of the model in the Hugging Face Hub. To explore all the supported models you can check [the Hugging Face Hub](https://huggingface.co/models?sort=trending&other=text-generation-inference).\n", + " * `NUM_SHARD` is the number of shards to use if you don't want to use all GPUs on a given machine e.g. if you have two GPUs but you just want to use one for TGI then `NUM_SHARD=1`, otherwise it matches the `CUDA_VISIBLE_DEVICES`.\n", + " * `MAX_INPUT_TOKENS` is the maximum allowed input length (expressed in number of tokens), the larger it is, the larger the prompt can be, but also more memory will be consumed.\n", + " * `MAX_TOTAL_TOKENS` is the most important value to set as it defines the \"memory budget\" of running clients requests, the larger this value, the larger amount each request will be in your RAM and the less effective batching can be.\n", + " * `MAX_BATCH_PREFILL_TOKENS` limits the number of tokens for the prefill operation, as it takes the most memory and is compute bound, it is interesting to limit the number of requests that can be sent.\n", + " * `HUGGING_FACE_HUB_TOKEN` is the Hugging Face Hub token, required as [`google/gemma-7b-it`](https://huggingface.co/google/gemma-7b-it) is a gated model.\n", + "\n", + "* (optional) **`serving_container_ports`** is the port where the Vertex AI endpoint |will be exposed, by default 8080.\n", + "\n", + "For more information on the supported `aiplatform.Model.upload` arguments, check [its Python reference](https://cloud.google.com/python/docs/reference/aiplatform/latest/google.cloud.aiplatform.Model#google_cloud_aiplatform_Model_upload)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "ef487ce082f3" + }, + "outputs": [], + "source": [ + "from huggingface_hub import get_token\n", + "\n", + "model = aiplatform.Model.upload(\n", + " display_name=\"google--gemma-7b-it\",\n", + " serving_container_image_uri=\"us-docker.pkg.dev/deeplearning-platform-release/gcr.io/huggingface-text-generation-inference-cu121.2-2.ubuntu2204.py310\",\n", + " serving_container_environment_variables={\n", + " \"MODEL_ID\": \"google/gemma-7b-it\",\n", + " \"NUM_SHARD\": \"1\",\n", + " \"MAX_INPUT_TOKENS\": \"512\",\n", + " \"MAX_TOTAL_TOKENS\": \"1024\",\n", + " \"MAX_BATCH_PREFILL_TOKENS\": \"1512\",\n", + " \"HUGGING_FACE_HUB_TOKEN\": get_token(),\n", + " },\n", + " serving_container_ports=[8080],\n", + ")\n", + "model.wait()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "c427c0a87016" + }, + "source": [ + "## Deploy Google Gemma on Vertex AI" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "ef4cd160dc09" + }, + "source": [ + "After the model is registered on Vertex AI, you can deploy the model to an endpoint.\n", + "\n", + "You need to first deploy a model to an endpoint before that model can be used to serve online predictions. Deploying a model associates physical resources with the model so it can serve online predictions with low latency.\n", + "\n", + "Before going into the code to deploy a model to an endpoint, let's quickly review the arguments provided to the `aiplatform.Model.deploy` method:\n", + "\n", + "- **`endpoint`** is the endpoint to deploy the model to, which is optional, and by default will be set to the model display name with the `_endpoint` suffix.\n", + "- **`machine_type`**, **`accelerator_type`** and **`accelerator_count`** are arguments that define which instance to use, and additionally, the accelerator to use and the number of accelerators, respectively. The `machine_type` and the `accelerator_type` are tied together, so you will need to select an instance that supports the accelerator that you are using and vice-versa. More information about the different instances at [Compute Engine Documentation - GPU machine types](https://cloud.google.com/compute/docs/gpus), and about the `accelerator_type` naming at [Vertex AI Documentation - MachineSpec](https://cloud.google.com/vertex-ai/docs/reference/rest/v1/MachineSpec).\n", + "\n", + "For more information on the supported `aiplatform.Model.deploy` arguments, you can check [its Python reference](https://cloud.google.com/python/docs/reference/aiplatform/latest/google.cloud.aiplatform.Model#google_cloud_aiplatform_Model_deploy)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "e777427015db" + }, + "outputs": [], + "source": [ + "deployed_model = model.deploy(\n", + " endpoint=aiplatform.Endpoint.create(display_name=\"google--gemma-7b-it-endpoint\"),\n", + " machine_type=\"g2-standard-4\",\n", + " accelerator_type=\"NVIDIA_L4\",\n", + " accelerator_count=1,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "5f2380d3ea60" + }, + "source": [ + "> Note that the model deployment on Vertex AI can take around 15 to 25 minutes; most of the time being the allocation / reservation of the resources, setting up the network and security, and such." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "9b3fd1898241" + }, + "source": [ + "## Online predictions on Vertex AI" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "2aa9cee03bd0" + }, + "source": [ + "Once the model is deployed on Vertex AI, you can run the online predictions using the `aiplatform.Endpoint.predict` method, which will send the requests to the running endpoint in the `/predict` route specified within the container following Vertex AI I/O payload formatting.\n", + "\n", + "As you are serving a `text-generation` model fine-tuned for instruction-following, you will need to make sure that the chat template, if any, is applied correctly to the input conversation; meaning that `transformers` need to be installed so as to instantiate the `tokenizer` for [`google/gemma-7b-it`](https://huggingface.co/google/gemma-7b-it) and run the `apply_chat_template` method over the input conversation before sending the input within the payload to the Vertex AI endpoint.\n", + "\n", + "> Note that the Messages API will be supported on Vertex AI on upcoming TGI releases, starting on 2.3, meaning that at the time of writing this post, the prompts need to be formatted before sending the request if you want to achieve nice results (assuming you are using an intruction-following model and not a base model)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "d46c0a866ffc" + }, + "outputs": [], + "source": [ + "%pip install --upgrade --user --quiet transformers jinja2" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "7e1ab8de0b5c" + }, + "source": [ + "After the installation is complete, the following snippet will apply the chat template to the conversation:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "82fd1842f0cc" + }, + "outputs": [], + "source": [ + "from huggingface_hub import get_token\n", + "from transformers import AutoTokenizer\n", + "\n", + "tokenizer = AutoTokenizer.from_pretrained(\"google/gemma-7b-it\", token=get_token())\n", + "\n", + "messages = [\n", + " {\"role\": \"user\", \"content\": \"What's Deep Learning?\"},\n", + "]\n", + "\n", + "inputs = tokenizer.apply_chat_template(\n", + " messages,\n", + " tokenize=False,\n", + " add_generation_prompt=True,\n", + ")\n", + "# user\\nWhat's Deep Learning?\\nmodel\\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "d0522c9b9bc2" + }, + "source": [ + "Which is what you will be sending within the payload to the deployed Vertex AI Endpoint, as well as [the generation parameters](https://huggingface.co/docs/huggingface_hub/main/en/package_reference/inference_client#huggingface_hub.InferenceClient.text_generation)." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "79e393e97ddb" + }, + "source": [ + "### Via Python" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "1a404fd19a08" + }, + "source": [ + "#### Within the same session\n", + "\n", + "To run the online prediction via the Vertex AI SDK, you can simply use the `predict` method." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "4ab63079a69a" + }, + "outputs": [], + "source": [ + "output = deployed_model.predict(\n", + " instances=[\n", + " {\n", + " \"inputs\": \"user\\nWhat's Deep Learning?\\nmodel\\n\",\n", + " \"parameters\": {\n", + " \"max_new_tokens\": 20,\n", + " \"do_sample\": True,\n", + " \"top_p\": 0.95,\n", + " \"temperature\": 1.0,\n", + " },\n", + " },\n", + " ]\n", + ")\n", + "output" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "b28efb473c7e" + }, + "source": [ + "#### From a different session\n", + "\n", + "To run the online prediction from a different session, you can run the following snippet." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "e003ab03f30f" + }, + "outputs": [], + "source": [ + "import os\n", + "\n", + "from google.cloud import aiplatform\n", + "\n", + "PROJECT_ID = \"[your-project-id]\" # @param {type:\"string\", isTemplate: true}\n", + "if PROJECT_ID == \"[your-project-id]\":\n", + " PROJECT_ID = str(os.environ.get(\"GOOGLE_CLOUD_PROJECT\"))\n", + "\n", + "LOCATION = os.environ.get(\"GOOGLE_CLOUD_REGION\", \"us-central1\")\n", + "\n", + "aiplatform.init(project=PROJECT_ID, location=LOCATION)\n", + "\n", + "endpoint_display_name = (\n", + " \"google--gemma-7b-it-endpoint\" # TODO: change to your endpoint display name\n", + ")\n", + "\n", + "# Iterates over all the Vertex AI Endpoints within the current project and keeps the first match (if any), otherwise set to None\n", + "ENDPOINT_ID = next(\n", + " (\n", + " endpoint.name\n", + " for endpoint in aiplatform.Endpoint.list()\n", + " if endpoint.display_name == endpoint_display_name\n", + " ),\n", + " None,\n", + ")\n", + "assert ENDPOINT_ID, (\n", + " \"`ENDPOINT_ID` is not set, please make sure that the `endpoint_display_name` is correct at \"\n", + " f\"https://console.cloud.google.com/vertex-ai/online-prediction/endpoints?project={os.getenv('PROJECT_ID')}\"\n", + ")\n", + "\n", + "endpoint = aiplatform.Endpoint(\n", + " f\"projects/{PROJECT_ID}/locations/{LOCATION}/endpoints/{ENDPOINT_ID}\"\n", + ")\n", + "output = endpoint.predict(\n", + " instances=[\n", + " {\n", + " \"inputs\": \"user\\nWhat's Deep Learning?\\nmodel\\n\",\n", + " \"parameters\": {\n", + " \"max_new_tokens\": 20,\n", + " \"do_sample\": True,\n", + " \"top_p\": 0.95,\n", + " \"temperature\": 0.7,\n", + " },\n", + " },\n", + " ],\n", + ")\n", + "output" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "7cc0b19ab641" + }, + "source": [ + "### Via gcloud" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "8fed7007c31c" + }, + "source": [ + "You can also send the requests using the `gcloud` CLI via the `gcloud ai endpoints` command.\n", + "\n", + "> Note that, before proceeding, you should either replace the values or set the following environment variables in advance from the Python variables set in the example, as follows:\n", + ">\n", + "> ```python\n", + "> import os\n", + "> os.environ[\"PROJECT_ID\"] = PROJECT_ID\n", + "> os.environ[\"LOCATION\"] = LOCATION\n", + "> os.environ[\"ENDPOINT_NAME\"] = \"google--gemma-7b-it-endpoint\"\n", + "> ```" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "7fb1547d409f" + }, + "outputs": [], + "source": [ + "%%bash\n", + "ENDPOINT_ID=$(gcloud ai endpoints list \\\n", + " --project=$PROJECT_ID \\\n", + " --region=$LOCATION \\\n", + " --filter=\"display_name=$ENDPOINT_NAME\" \\\n", + " --format=\"value(name)\" \\\n", + " | cut -d'/' -f6)\n", + "\n", + "echo '{\n", + " \"instances\": [\n", + " {\n", + " \"inputs\": \"user\\nWhat'\\''s Deep Learning?\\nmodel\\n\",\n", + " \"parameters\": {\n", + " \"max_new_tokens\": 20,\n", + " \"do_sample\": true,\n", + " \"top_p\": 0.95,\n", + " \"temperature\": 1.0\n", + " }\n", + " }\n", + " ]\n", + "}' | gcloud ai endpoints predict $ENDPOINT_ID \\\n", + " --project=$PROJECT_ID \\\n", + " --region=$LOCATION \\\n", + " --json-request=\"-\"" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "7e588a65759e" + }, + "source": [ + "### Via cURL" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "cf3de731bcf4" + }, + "source": [ + "Alternatively, you can also send the requests via `cURL`.\n", + "\n", + "> Note that, before proceeding, you should either replace the values or set the following environment variables in advance from the Python variables set in the example, as follows:\n", + ">\n", + "> ```python\n", + "> import os\n", + "> os.environ[\"PROJECT_ID\"] = PROJECT_ID\n", + "> os.environ[\"LOCATION\"] = LOCATION\n", + "> os.environ[\"ENDPOINT_NAME\"] = \"google--gemma-7b-it-endpoint\"\n", + "> ```" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "a71da4e1152b" + }, + "outputs": [], + "source": [ + "%%bash\n", + "ENDPOINT_ID=$(gcloud ai endpoints list \\\n", + " --project=$PROJECT_ID \\\n", + " --region=$LOCATION \\\n", + " --filter=\"display_name=$ENDPOINT_NAME\" \\\n", + " --format=\"value(name)\" \\\n", + " | cut -d'/' -f6)\n", + "\n", + "curl -X POST \\\n", + " -H \"Authorization: Bearer $(gcloud auth print-access-token)\" \\\n", + " -H \"Content-Type: application/json\" \\\n", + " https://$LOCATION-aiplatform.googleapis.com/v1/projects/$PROJECT_ID/locations/$LOCATION/endpoints/$ENDPOINT_ID:predict \\\n", + " -d '{\n", + " \"instances\": [\n", + " {\n", + " \"inputs\": \"user\\nWhat'\\''s Deep Learning?\\nmodel\\n\",\n", + " \"parameters\": {\n", + " \"max_new_tokens\": 20,\n", + " \"do_sample\": true,\n", + " \"top_p\": 0.95,\n", + " \"temperature\": 1.0\n", + " }\n", + " }\n", + " ]\n", + " }'" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "f6f17f9aff65" + }, + "source": [ + "## Cleaning up" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "2a4e033321ad" + }, + "source": [ + "Finally, you can already release the resources that you've created as follows, to avoid unnecessary costs:\n", + "\n", + "- `deployed_model.undeploy_all` to undeploy the model from all the endpoints.\n", + "- `deployed_model.delete` to delete the endpoint/s where the model was deployed gracefully, after the `undeploy_all` method.\n", + "- `model.delete` to delete the model from the registry." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "b074ffdbd0c4" + }, + "outputs": [], + "source": [ + "deployed_model.undeploy_all()\n", + "deployed_model.delete()\n", + "model.delete()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "6bc4da2fcc35" + }, + "source": [ + "Alternatively, you can also remove those from the Google Cloud Console following the steps:\n", + "\n", + "- Go to Vertex AI in Google Cloud\n", + "- Go to Deploy and use -> Online prediction\n", + "- Click on the endpoint and then on the deployed model/s to \"Undeploy model from endpoint\"\n", + "- Then go back to the endpoint list and remove the endpoint\n", + "- Finally, go to Deploy and use -> Model Registry, and remove the model" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "137d82da56f2" + }, + "source": [ + "## References\n", + "\n", + "- [GitHub Repository - Hugging Face DLCs for Google Cloud](https://github.com/huggingface/Google-Cloud-Containers): contains all the containers developed by the collaboration of both Hugging Face and Google Cloud teams; as well as a lot of examples on both training and inference, covering both CPU and GPU, as well support for most of the models within the Hugging Face Hub.\n", + "- [Google Cloud Documentation - Hugging Face DLCs](https://cloud.google.com/deep-learning-containers/docs/choosing-container#hugging-face): contains a table with the latest released Hugging Face DLCs on Google Cloud.\n", + "- [Google Artifact Registry - Hugging Face DLCs](https://console.cloud.google.com/artifacts/docker/deeplearning-platform-release/us/gcr.io): contains all the DLCs released by Google Cloud that can be used.\n", + "- [Hugging Face Documentation - Google Cloud](https://huggingface.co/docs/google-cloud): contains the official Hugging Face documentation for the Google Cloud DLCs." + ] + } + ], + "metadata": { + "colab": { + "name": "vertex_ai_text_generation_inference_gemma.ipynb", + "toc_visible": true + }, + "kernelspec": { + "display_name": "Python 3", + "name": "python3" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} diff --git a/open-models/serving/vertex_ai_tgi_gemma_multi_lora_adapters_deployment.ipynb b/open-models/serving/vertex_ai_tgi_gemma_multi_lora_adapters_deployment.ipynb new file mode 100644 index 0000000..aa1c9c2 --- /dev/null +++ b/open-models/serving/vertex_ai_tgi_gemma_multi_lora_adapters_deployment.ipynb @@ -0,0 +1,1537 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "ur8xi4C7S06n" + }, + "outputs": [], + "source": [ + "# Copyright 2024 Google LLC\n", + "#\n", + "# Licensed under the Apache License, Version 2.0 (the \"License\");\n", + "# you may not use this file except in compliance with the License.\n", + "# You may obtain a copy of the License at\n", + "#\n", + "# https://www.apache.org/licenses/LICENSE-2.0\n", + "#\n", + "# Unless required by applicable law or agreed to in writing, software\n", + "# distributed under the License is distributed on an \"AS IS\" BASIS,\n", + "# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n", + "# See the License for the specific language governing permissions and\n", + "# limitations under the License." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "JAPoU8Sm5E6e" + }, + "source": [ + "# Hugging Face DLCs: Serving Gemma 2 with multiple LoRA adapters with Text Generation Inference (TGI) on Vertex AI\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + "
    \n", + " \n", + " \"Google
    Open in Colab\n", + "
    \n", + "
    \n", + " \n", + " \"Google
    Open in Colab Enterprise\n", + "
    \n", + "
    \n", + " \n", + " \"Vertex
    Open in Vertex AI Workbench\n", + "
    \n", + "
    \n", + " \n", + " \"GitHub
    View on GitHub\n", + "
    \n", + "
    \n", + "\n", + "
    \n", + "\n", + "Share to:\n", + "\n", + "\n", + " \"LinkedIn\n", + "\n", + "\n", + "\n", + " \"Bluesky\n", + "\n", + "\n", + "\n", + " \"X\n", + "\n", + "\n", + "\n", + " \"Reddit\n", + "\n", + "\n", + "\n", + " \"Facebook\n", + "" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "84f0f73a0f76" + }, + "source": [ + "| | |\n", + "|-|-|\n", + "| Author(s) | [Ivan Nardini](https://github.com/inardini)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "tvgnzT1CKxrO" + }, + "source": [ + "## Overview\n", + "\n", + "> [**Gemma**](https://ai.google.dev/gemma) is a family of lightweight, state-of-the-art open models built from the same research and technology used to create the Gemini models, developed by Google DeepMind and other teams across Google.\n", + "\n", + "> [**Hugging Face DLCs**](https://github.com/huggingface/Google-Cloud-Containers) are pre-built and optimized Deep Learning Containers (DLCs) maintained by Hugging Face and Google Cloud teams to simplify environment configuration for your ML workloads.\n", + "\n", + "> [**Google Cloud Vertex AI**](https://cloud.google.com/vertex-ai) is a Machine Learning (ML) platform that lets you train and deploy ML models and AI applications, and customize large language models (LLMs) for use in your AI-powered applications.\n", + "\n", + "This notebook showcases how to deploy Gemma 2 2B from Hugging Face Hub with multiple LoRA adapters fine-tuned for different purposes such as coding, or SQL using Hugging Face's Text Generation Inference (TGI) Deep Learning Container (DLC) in combination with a [custom handler](https://huggingface.co/docs/inference-endpoints/en/guides/custom_handler#create-custom-inference-handler) on Vertex AI.\n", + "\n", + "By the end of this notebook, you will learn how to:\n", + "\n", + "- Create a custom handler and test it\n", + "- Register any LLM from the Hugging Face Hub on Vertex AI\n", + "- Deploy an LLM on Vertex AI\n", + "- Send online predictions on Vertex AI" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "61RBz8LLbxCR" + }, + "source": [ + "## Get started" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "80q9vEu5OH59" + }, + "source": [ + "### (Optional) Set the runtime\n", + "\n", + "Depending on your notebook enviroment, consider to set a GPU runtime.\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "No17Cw5hgx12" + }, + "source": [ + "### Install Vertex AI SDK and other required packages\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "tFy3H3aPgx12" + }, + "outputs": [], + "source": [ + "%pip install --upgrade --user --quiet 'torch' 'torchvision' 'torchaudio'\n", + "%pip install --upgrade --user --quiet 'huggingface_hub[hf_transfer]' 'transformers' 'accelerate>=0.26.0'\n", + "%pip install --upgrade --user --quiet 'google-cloud-aiplatform' 'crcmod' 'etils'" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "R5Xep4W9lq-Z" + }, + "source": [ + "### Restart runtime\n", + "\n", + "To use the newly installed packages in this Jupyter runtime, you must restart the runtime. You can do this by running the cell below, which restarts the current kernel.\n", + "\n", + "The restart might take a minute or longer. After it's restarted, continue to the next step." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "XRvKdaPDTznN" + }, + "outputs": [], + "source": [ + "import IPython\n", + "\n", + "app = IPython.Application.instance()\n", + "app.kernel.do_shutdown(True)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "SbmM4z7FOBpM" + }, + "source": [ + "
    \n", + "⚠ The kernel is going to restart. In Colab or Colab Enterprise, you might see an error message that says \"Your session crashed for an unknown reason.\" This is expected. Wait until it's finished before continuing to the next step. ⚠\n", + "
    \n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "dmWOrTJ3gx13" + }, + "source": [ + "### Authenticate your notebook environment (Colab only)\n", + "\n", + "If you're running this notebook on Google Colab, run the cell below to authenticate your environment." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "NyKGtVQjgx13" + }, + "outputs": [], + "source": [ + "import sys\n", + "\n", + "if \"google.colab\" in sys.modules:\n", + " from google.colab import auth\n", + "\n", + " auth.authenticate_user()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "e0vxVy5J3g41" + }, + "source": [ + "### Set Hugging Face variables" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "P5UCCYrV3pEW" + }, + "outputs": [], + "source": [ + "import os\n", + "\n", + "from etils.epath import Path\n", + "\n", + "ROOT_PATH = Path(\".\")\n", + "TUTORIAL_PATH = ROOT_PATH / \"deploy_gemma_with_multi_lora_adapters_tutorial\"\n", + "\n", + "os.environ[\"HF_HOME\"] = str(TUTORIAL_PATH)\n", + "os.environ[\"HF_HUB_ENABLE_HF_TRANSFER\"] = \"1\"" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "f9af3e57f89a" + }, + "source": [ + "### Authenticate your Hugging Face account\n", + "\n", + "As [`google/gemma-2b`](https://huggingface.co/google/gemma-2-2b) is a gated model, you need to have a Hugging Face Hub account, and accept the Google's usage license for Gemma. Once that's done, you need to generate a new user access token with read-only access so that the weights can be downloaded from the Hub in the Hugging Face DLC for TGI.\n", + "\n", + "> Note that the user access token can only be generated via [the Hugging Face Hub UI](https://huggingface.co/settings/tokens/new), where you can either select read-only access to your account, or follow the recommendations and generate a fine-grained token with read-only access to [`google/gemma-2b`](https://huggingface.co/google/gemma-2-2b)." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "4c31c7272804" + }, + "source": [ + "Then you can install the `huggingface_hub` that comes with a CLI that will be used for the authentication with the token generated in advance. So that then the token can be safely retrieved via `huggingface_hub.get_token`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "8d836e0210fe" + }, + "outputs": [], + "source": [ + "from huggingface_hub import interpreter_login\n", + "\n", + "interpreter_login()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "c71a4314c250" + }, + "source": [ + "Read more about [Hugging Face Security](https://huggingface.co/docs/hub/en/security), specifically about [Hugging Face User Access Tokens](https://huggingface.co/docs/hub/en/security-tokens)." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "Dy5VO78rzX8c" + }, + "source": [ + "### Requirements" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "R8Zm9y0hxU5O" + }, + "source": [ + "#### Set Project ID and Location\n", + "\n", + "To get started using Vertex AI, you must have an existing Google Cloud project and [enable these APIs](https://console.cloud.google.com/flows/enableapi?apiid=aiplatform.googleapis.com,artifactregistry.googleapis.com).\n", + "\n", + "Learn more about [setting up a project and a development environment](https://cloud.google.com/vertex-ai/docs/start/cloud-environment)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "Nqwi-5ufWp_B" + }, + "outputs": [], + "source": [ + "# Use the environment variable if the user doesn't provide Project ID.\n", + "import os\n", + "\n", + "PROJECT_ID = \"[your-project-id]\" # @param {type: \"string\", placeholder: \"[your-project-id]\", isTemplate: true}\n", + "\n", + "if not PROJECT_ID or PROJECT_ID == \"[your-project-id]\":\n", + " PROJECT_ID = str(os.environ.get(\"GOOGLE_CLOUD_PROJECT\"))\n", + "\n", + "PROJECT_NUMBER = !gcloud projects describe {PROJECT_ID} --format=\"get(projectNumber)\"[0]\n", + "PROJECT_NUMBER = PROJECT_NUMBER[0]\n", + "\n", + "LOCATION = os.environ.get(\"GOOGLE_CLOUD_REGION\", \"us-central1\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "d03b04d07dc0" + }, + "source": [ + "#### Create a bucket in Google Cloud Storage (GCS)\n", + "\n", + "Create a storage bucket to store intermediate artifacts such as models." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "0ca9b8a1bf63" + }, + "outputs": [], + "source": [ + "BUCKET_NAME = \"[your-bucket-name]\" # @param {type:\"string\", isTemplate: true}\n", + "\n", + "if BUCKET_NAME == \"[your-bucket-name]\":\n", + " raise ValueError(\"A valid BUCKET_NAME needs to be specified\")\n", + "\n", + "BUCKET_URI = f\"gs://{BUCKET_NAME}\"\n", + "os.environ[\"BUCKET_URI\"] = BUCKET_URI" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "c1a68956ee21" + }, + "source": [ + "Uncomment the `gcloud storage buckets create` command below if you need to create a bucket on GCS." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "2ff73fc11e33" + }, + "outputs": [], + "source": [ + "!gcloud storage buckets create $BUCKET_URI --project $PROJECT_ID --location=$LOCATION --default-storage-class=STANDARD --uniform-bucket-level-access" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "set_service_account" + }, + "source": [ + "#### Set Service Account and permissions\n", + "\n", + "You will need to have the following IAM roles set:\n", + "\n", + "- Vertex AI User (roles/aiplatform.user)\n", + "- Artifact Registry Reader (roles/artifactregistry.reader)\n", + "- Storage Object Admin (roles/storage.objectAdmin)\n", + "\n", + "For more information about granting roles, see [Manage access](https://cloud.google.com/iam/docs/granting-changing-revoking-access).\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "VX9tpdtuQI5L" + }, + "source": [ + "> If you run following commands using Vertex AI Workbench, run directly in the terminal.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "ssUJJqXJJHgC" + }, + "outputs": [], + "source": [ + "SERVICE_ACCOUNT = f\"{PROJECT_NUMBER}-compute@developer.gserviceaccount.com\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "wqOHg5aid6HP" + }, + "outputs": [], + "source": [ + "for role in ['aiplatform.user', 'storage.objectAdmin', 'artifactregistry.reader']:\n", + "\n", + " ! gcloud projects add-iam-policy-binding {PROJECT_ID} \\\n", + " --member=serviceAccount:{SERVICE_ACCOUNT} \\\n", + " --role=roles/{role} --condition=None" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "EjebjdiNxe_D" + }, + "source": [ + "### Initiate Vertex AI SDK\n", + "\n", + "Initiate Vertex AI client session." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "GReulMr0xjZs" + }, + "outputs": [], + "source": [ + "import vertexai\n", + "\n", + "vertexai.init(project=PROJECT_ID, location=LOCATION, staging_bucket=BUCKET_URI)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "8XvMcYzm7xmm" + }, + "source": [ + "### Import libraries\n", + "\n", + "Import relevant libraries." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "3fXW5p7o7zlo" + }, + "outputs": [], + "source": [ + "import gc\n", + "import json\n", + "from pprint import pprint as pp\n", + "\n", + "from etils import epath\n", + "from google.cloud import aiplatform\n", + "from google.cloud.aiplatform import Endpoint, Model\n", + "from huggingface_hub import get_token, snapshot_download\n", + "import requests\n", + "import torch\n", + "from transformers import AutoModelForCausalLM, AutoTokenizer\n", + "from transformers.generation import GenerationConfig as TGenerationConfig\n", + "from vertexai.generative_models import GenerationConfig, GenerativeModel" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "E4rjOknk5yvq" + }, + "source": [ + "### Helpers\n", + "\n", + "Define some helpers." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "MOoaB5xf51BD" + }, + "outputs": [], + "source": [ + "def hf_model_path(hf_home_path: str, model_name: str | None = None) -> str:\n", + " \"\"\"Copy model files from the Hugging Face cache.\"\"\"\n", + " # Convert and expand the input path\n", + " base_path = Path(hf_home_path).expanduser()\n", + "\n", + " if not base_path.exists():\n", + " raise FileNotFoundError(f\"Cache path does not exist: {base_path}\")\n", + "\n", + " if model_name:\n", + " # Search in specific model directory\n", + " model_dir_name = \"models--\" + model_name.replace(\"/\", \"--\")\n", + " model_dirs = [base_path / model_dir_name]\n", + " if not model_dirs[0].exists():\n", + " raise FileNotFoundError(f\"Model directory not found: {model_dirs[0]}\")\n", + " else:\n", + " # Search all directories (original behavior)\n", + " model_dirs = [d for d in base_path.iterdir() if d.is_dir()]\n", + "\n", + " # Find the deepest snapshot directory\n", + " snapshot_path = None\n", + " for model_dir in model_dirs:\n", + " # Look for 'snapshots' directory\n", + " snapshots_dir = model_dir / \"snapshots\"\n", + " if not snapshots_dir.exists():\n", + " continue\n", + "\n", + " # Get the first snapshot folder (usually there's only one)\n", + " for snapshot in snapshots_dir.iterdir():\n", + " if snapshot.is_dir():\n", + " snapshot_path = snapshot\n", + " break\n", + "\n", + " if snapshot_path:\n", + " break\n", + "\n", + " if not snapshot_path:\n", + " error_msg = (\n", + " f\"No snapshot directory found for model: {model_name}\"\n", + " if model_name\n", + " else \"No snapshot directory found in the cache\"\n", + " )\n", + " raise FileNotFoundError(error_msg)\n", + "\n", + " return str(snapshot_path)\n", + "\n", + "\n", + "def get_cuda_device_names():\n", + " \"\"\"A function to get the list of NVIDIA GPUs\"\"\"\n", + " if not torch.cuda.is_available():\n", + " return None\n", + "\n", + " return [str(i) for i in range(torch.cuda.device_count())]\n", + "\n", + "\n", + "def empty_gpu_ram():\n", + " \"\"\"A function to empty the GPU RAM\"\"\"\n", + " gc.collect()\n", + " torch.cuda.empty_cache()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "EdvJRUWRNGHE" + }, + "source": [ + "## Prepare a custom handler to serve Lora Adapters on Vertex AI\n", + "\n", + "Custom Handlers are custom classes in Python that define the pre-processing, inference, and post-processing steps required to run the inference using Hugging Face Pytorch Prediction container on Vertex AI.\n", + "\n", + "Think of Custom Handlers as personalized instructions for Hugging Face models. They define how to prepare the input data, run the model, and handle the results. In this sense, Custom Handlers add flexibility. They let you customize how data is prepared and processed, add extra steps, and even build in custom measurements or logging. This means you can tailor the process to your exact needs when the standard setup isn't sufficient.\n", + "\n", + "These instructions are stored in a file named `handler.py`. If you need additional dependencies, you can list it in a `requirements.txt` file. The PyTorch container automatically finds and uses these files if they're present.\n", + "\n", + "Have a look at [đŸ€— Serve Anything with Inference Endpoints + Custom Handlers](https://huggingface.co/blog/alvarobartt/serve-anything-inference-endpoints-custom-code) to learn more." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "CgPwfG07iMCS" + }, + "source": [ + "### Test the `handler` locally\n", + "\n", + "Before to build the handler module, you can test its coding locally." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "npEogfIziA1O" + }, + "source": [ + "#### Download Gemma 2 and adapters locally from Hugging Face Hub" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "RZ-IsZ_niNdw" + }, + "outputs": [], + "source": [ + "# Download Gemma 2 and adapters\n", + "base_model_id = \"google/gemma-2-2b-it\"\n", + "sql_adapter_id = \"google-cloud-partnership/gemma-2-2b-it-lora-sql\"\n", + "magicoder_adapter_id = \"google-cloud-partnership/gemma-2-2b-it-lora-magicoder\"\n", + "\n", + "snapshot_download(repo_id=base_model_id, token=get_token())\n", + "snapshot_download(repo_id=sql_adapter_id, token=get_token())\n", + "snapshot_download(repo_id=magicoder_adapter_id, token=get_token())" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "OVLBAtraJkQ_" + }, + "source": [ + "#### Load Gemma model\n", + "\n", + "Load a pre-trained model called \"gemma\" for text generation. You first sets up the text preprocessor, which handles tasks like tokenization (breaking text into words or sub-words). Then you load the actual model itself, optimized for lower memory usage and automatic device placement (likely GPU if available)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "QmHijFltikJZ" + }, + "outputs": [], + "source": [ + "gemma_path = hf_model_path(TUTORIAL_PATH / \"hub\", base_model_id)\n", + "\n", + "tokenizer = AutoTokenizer.from_pretrained(gemma_path)\n", + "\n", + "model = AutoModelForCausalLM.from_pretrained(\n", + " gemma_path, low_cpu_mem_usage=True, torch_dtype=torch.float16, device_map=\"auto\"\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "swrlVm7j99kl" + }, + "source": [ + "#### Load Gemma's Lora adapters\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "zikOj8jHFr0K" + }, + "outputs": [], + "source": [ + "sql_adapter_path = hf_model_path(TUTORIAL_PATH / \"hub\", sql_adapter_id)\n", + "magicoder_adapter_path = hf_model_path(TUTORIAL_PATH / \"hub\", magicoder_adapter_id)\n", + "\n", + "model.load_adapter(\n", + " sql_adapter_path,\n", + " adapter_name=\"sql_adapter\",\n", + " is_trainable=False,\n", + " device_map=\"auto\",\n", + " offload_folder=\"/tmp/offload\",\n", + ")\n", + "model.load_adapter(\n", + " magicoder_adapter_path,\n", + " adapter_name=\"magicoder_adapter\",\n", + " is_trainable=False,\n", + " device_map=\"auto\",\n", + " offload_folder=\"/tmp/offload\",\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "j5Qir_dtJ0-X" + }, + "source": [ + "#### Prepare the prediction request\n", + "\n", + "Prepare the text generation request by creating a dictionary with user prompt and generation parameters.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "6g3Mt_FVyaME" + }, + "outputs": [], + "source": [ + "requests = []\n", + "user_prompts = [\n", + " \"I have a table called orders with columns order_id (INT), customer_id (INT), order_date (DATE), and total_amount (DECIMAL). I need to find the total revenue generated in the month of October 2023. How can I write a SQL query to achieve this?\",\n", + " \"# Context: You have a list of numbers called `my_numbers`.\\n# Question: How do I calculate the sum of all the numbers in `my_numbers` using a built-in function?\\n# Example `my_numbers` list:\\nmy_numbers = [1, 2, 3, 4, 5]\",\n", + "]\n", + "\n", + "for prompt in user_prompts:\n", + " requests.append(\n", + " {\n", + " \"inputs\": prompt,\n", + " \"parameters\": {\"max_new_tokens\": 10, \"temperature\": 0.7, \"do_sample\": True},\n", + " }\n", + " )\n", + "\n", + "prediction_request = {\n", + " \"instances\": requests,\n", + "}" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "nsyWS86Qr3CL" + }, + "outputs": [], + "source": [ + "pp(prediction_request, indent=3)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "7JUYwkwlEf9I" + }, + "source": [ + "#### Define an LLM-based router\n", + "\n", + "To send the right user request to the right adapted model, define a LLM-based router which takes the user input and determines if it's a SQL query generation task (SQL) or a code generation task (CODE)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "72kz_w3-7_R7" + }, + "outputs": [], + "source": [ + "router_model_id = \"gemini-2.0-flash\"\n", + "router_model = GenerativeModel(router_model_id)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "0QmsqI73Ejln" + }, + "outputs": [], + "source": [ + "def route_prompt(prompt, router_model):\n", + " router_prompt = \"\"\"Analyze the following prompt and determine if it's a SQL query generation task (SQL) or a code generation task (CODE).\n", + "\n", + " Guidelines:\n", + " - SQL: Database queries, table operations, data retrieval, SQL explanations, database schema questions\n", + " - CODE: Programming problems, algorithm implementations, software development tasks, code optimization\n", + "\n", + " Consider these examples:\n", + " 1. \"Given a users table with columns (id, name, age), show me all users above 25\" -> SQL\n", + " 2. \"Write a function to reverse a linked list in Python\" -> CODE\n", + " 3. \"How can I join these two tables and filter by date?\" -> SQL\n", + " 4. \"Implement a binary search tree in Rust\" -> CODE\n", + "\n", + " User prompt: \"{prompt}\"\n", + " Response:\n", + " \"\"\"\n", + "\n", + " response_schema = {\n", + " \"type\": \"OBJECT\",\n", + " \"properties\": {\n", + " \"classification\": {\n", + " \"type\": \"string\",\n", + " \"enum\": [\"SQL\", \"CODE\"],\n", + " },\n", + " },\n", + " }\n", + "\n", + " try:\n", + " response = router_model.generate_content(\n", + " router_prompt.format(prompt=prompt),\n", + " generation_config=GenerationConfig(\n", + " response_mime_type=\"application/json\", response_schema=response_schema\n", + " ),\n", + " ).text\n", + " result = json.loads(response)\n", + " return result\n", + " except:\n", + " return {\"classification\": \"CODE\"}" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "vxSTwBiLFNaw" + }, + "outputs": [], + "source": [ + "for instance in prediction_request[\"instances\"]:\n", + " prompt = instance[\"inputs\"]\n", + " print(prompt[:55], \"-->\", route_prompt(prompt, router_model))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "DH-WU2IAJ4yY" + }, + "source": [ + "#### Generate prediction\n", + "\n", + "Process a list of prediction requests, each containing a text prompt.\n", + "\n", + "> Note that depending on your runtime, generating predictions may require ~ 10 mins." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "UyKf2v3vlOtY" + }, + "outputs": [], + "source": [ + "predictions = []\n", + "\n", + "for instance in prediction_request[\"instances\"]:\n", + " # Check the prompt\n", + " if \"inputs\" not in instance:\n", + " raise ValueError(\"The request body must contain the `inputs` key.\")\n", + "\n", + " # Get the adapter label\n", + " prompt = instance[\"inputs\"]\n", + " prompt_classification = route_prompt(prompt, router_model)[\"classification\"]\n", + "\n", + " # Set the adapter model\n", + " if prompt_classification == \"SQL\":\n", + " model.set_adapter(\"sql_adapter\")\n", + " else:\n", + " model.set_adapter(\"magicoder_adapter\")\n", + "\n", + " # Prepare input\n", + " messages = [{\"role\": \"user\", \"content\": prompt}]\n", + " input_ids = tokenizer.apply_chat_template(messages, return_tensors=\"pt\").to(\n", + " model.device\n", + " )\n", + "\n", + " # Generate prediction\n", + " input_len = input_ids.shape[-1]\n", + " with torch.inference_mode():\n", + " generation_config = instance.get(\n", + " \"parameters\", {\"max_new_tokens\": 10, \"temperature\": 0.7, \"do_sample\": True}\n", + " )\n", + " generation = model.generate(\n", + " input_ids=input_ids,\n", + " generation_config=TGenerationConfig(**generation_config),\n", + " )\n", + " generation = generation[0][input_len:]\n", + " response = tokenizer.decode(generation, skip_special_tokens=True).removeprefix(\n", + " \"model\\n\"\n", + " )\n", + " predictions.append(response)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "zzmvzmxgaiFc" + }, + "source": [ + "Get the prediction." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "XlML8-o8lV9X" + }, + "outputs": [], + "source": [ + "for prediction in predictions:\n", + " print(\"------- Prediction -------\")\n", + " print(prediction)\n", + " print(\"--------------------------\\n\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "Yvh-vsUblb-W" + }, + "source": [ + "### Define the `handler.py` module\n", + "\n", + "After testing the handler code, you assemble the code in a Python module which defines the custom inference handler for Gemma.\n", + "\n", + "You write this handler code to a file named handler.py within the model directory together with the requirements file which contains dependencies for executing handler code." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "J0uEcbJSEeKy" + }, + "outputs": [], + "source": [ + "serve_uri = epath.Path(BUCKET_URI) / \"serve\"\n", + "serve_uri.mkdir(parents=True, exist_ok=True)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "WpBfVVbtoA_m" + }, + "outputs": [], + "source": [ + "handler_module = '''\n", + "from typing import Any, Dict, List\n", + "import torch\n", + "from transformers import AutoTokenizer, AutoModelForCausalLM\n", + "from transformers.generation import GenerationConfig as TGenerationConfig\n", + "from vertexai.generative_models import GenerationConfig, GenerativeModel\n", + "import json\n", + "import logging\n", + "import sys\n", + "from huggingface_inference_toolkit.logging import logger\n", + "\n", + "def route_prompt(prompt, router_model):\n", + " router_prompt = \"\"\"Analyze the following prompt and determine if it's a SQL query generation task (SQL) or a code generation task (CODE).\n", + "\n", + " Guidelines:\n", + " - SQL: Database queries, table operations, data retrieval, SQL explanations, database schema questions\n", + " - CODE: Programming problems, algorithm implementations, software development tasks, code optimization\n", + "\n", + " Consider these examples:\n", + " 1. \"Given a users table with columns (id, name, age), show me all users above 25\" -> SQL\n", + " 2. \"Write a function to reverse a linked list in Python\" -> CODE\n", + " 3. \"How can I join these two tables and filter by date?\" -> SQL\n", + " 4. \"Implement a binary search tree in Rust\" -> CODE\n", + "\n", + " User prompt: \"{prompt}\"\n", + " Response:\n", + " \"\"\"\n", + "\n", + " response_schema = {\n", + " \"type\": \"OBJECT\",\n", + " \"properties\": {\n", + " \"classification\": {\n", + " \"type\": \"string\",\n", + " \"enum\": [\"SQL\", \"CODE\"],\n", + " },\n", + " }}\n", + "\n", + " try:\n", + " response = router_model.generate_content(\n", + " router_prompt.format(prompt=prompt),\n", + " generation_config=GenerationConfig(\n", + " response_mime_type=\"application/json\", response_schema=response_schema\n", + " ),\n", + " ).text\n", + " result = json.loads(response)\n", + " return result\n", + " except:\n", + " return {\"classification\": \"CODE\"}\n", + "\n", + "class EndpointHandler:\n", + " def __init__(\n", + " self,\n", + " model_dir: str = \"google/gemma-2-2b-it\",\n", + " sql_adapter_id: str = \"/tmp/model/google-cloud-partnership/gemma-2-2b-it-lora-sql\"\n", + " magicoder_adapter_id: str = \"/tmp/model/google-cloud-partnership/gemma-2-2b-it-lora-magicoder\"\n", + " router_model_id: str = \"gemini-2.0-flash\",\n", + " **kwargs: Any,\n", + " ) -> None:\n", + "\n", + " self.processor = AutoTokenizer.from_pretrained(model_dir, token=os.getenv(\"HF_TOKEN\"))\n", + " self.model = AutoModelForCausalLM.from_pretrained(\n", + " model_dir,\n", + " low_cpu_mem_usage=True,\n", + " torch_dtype=torch.float16,\n", + " device_map=\"auto\",\n", + " token=os.getenv(\"HF_TOKEN\")\n", + " )\n", + "\n", + " self.model.load_adapter(sql_adapter_id, adapter_name=\"sql_adapter\", is_trainable=False, device_map=\"auto\", offload_folder=\"/tmp/offload\")\n", + " self.model.load_adapter(magicoder_adapter_id, adapter_name=\"magicoder_adapter\", is_trainable=False, device_map=\"auto\", offload_folder=\"/tmp/offload\")\n", + " self.router_model = GenerativeModel(router_model_id)\n", + "\n", + " def __call__(self, data: Dict[str, Any]) -> Dict[str, List[Any]]:\n", + " logger.info(\"Processing new request\")\n", + " predictions = []\n", + "\n", + " for instance in data[\"instances\"]:\n", + " # Check the prompt\n", + " if \"inputs\" not in instance:\n", + " raise ValueError(\"The request body must contain the `inputs` key.\")\n", + "\n", + " # Get the adapter label\n", + " logger.info(f'Getting the adapter label for the prompt: {instance[\"inputs\"]}')\n", + " prompt = instance[\"inputs\"]\n", + " prompt_classification = route_prompt(prompt, self.router_model)[\"classification\"]\n", + "\n", + " # Set the adapter model\n", + " logger.info(f'Setting the model to {prompt_classification} adapter')\n", + " if prompt_classification == \"SQL\":\n", + " self.model.set_adapter(\"sql_adapter\")\n", + " else:\n", + " self.model.set_adapter(\"magicoder_adapter\")\n", + "\n", + " # Prepare input\n", + " logger.info('Preparing the input for the prompt')\n", + " messages = [{\"role\": \"user\", \"content\": prompt}]\n", + " input_ids = self.tokenizer.apply_chat_template(\n", + " messages,\n", + " return_tensors=\"pt\"\n", + " ).to(self.model.device)\n", + "\n", + " # Generate prediction\n", + " logger.info('Generating the prediction')\n", + " input_len = input_ids.shape[-1]\n", + " with torch.inference_mode():\n", + " generation_config = instance.get(\n", + " \"parameters\", {\"temperature\": 0.7, \"do_sample\": True}\n", + " )\n", + " generation = self.model.generate(\n", + " input_ids=input_ids,\n", + " generation_config=TGenerationConfig(**generation_config),\n", + " )\n", + " generation = generation[0][input_len:]\n", + " response = self.tokenizer.decode(generation, skip_special_tokens=True)\n", + " logger.info(f'Generated response: {response[:50]}...')\n", + " predictions.append(response)\n", + "\n", + " logger.info(f\"Successfully processed {len(predictions)} instances\")\n", + " return {\"predictions\": predictions}\n", + "'''\n", + "\n", + "with serve_uri.joinpath(\"handler.py\").open(\"w\") as f:\n", + " f.write(handler_module)\n", + "f.close()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "UAzfd7q9REBw" + }, + "source": [ + "### Provide requirements file\n", + "\n", + "Save a `requirements.txt` file with handler's dependencies." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "AvOHlMKyRJaW" + }, + "outputs": [], + "source": [ + "requirements_file = \"\"\"\n", + "google-cloud-aiplatform\n", + "\"\"\"\n", + "\n", + "with serve_uri.joinpath(\"requirements.txt\").open(\"w\") as f:\n", + " f.write(requirements_file)\n", + "f.close()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "3zOgAT4DpO8Z" + }, + "source": [ + "### Copy model with custom handler on Cloud Bucket\n", + "\n", + "Efficiently upload the model directory to Google Cloud Storage using `gsutil`.\n", + "\n", + "Note that `-m` enables multi-threaded uploads for faster transfer, especially for large directories. `-o GSUtil:parallel_composite_upload_threshold=150M` optimizes large file uploads by splitting them into smaller parts for parallel transfer, significantly speeding up the process for files larger than 150MB." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "jZ1_2h3nTC-F" + }, + "outputs": [], + "source": [ + "! gsutil -o GSUtil:parallel_composite_upload_threshold=150M -m cp -r {str(gemma_path)}/* {str(serve_uri)}\n", + "! gsutil -o GSUtil:parallel_composite_upload_threshold=150M -m cp -r {str(sql_adapter_path)}/* {str(serve_uri)}/{sql_adapter_id}\n", + "! gsutil -o GSUtil:parallel_composite_upload_threshold=150M -m cp -r {str(magicoder_adapter_path)}/* {str(serve_uri)}/{magicoder_adapter_id}" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "ecK96GglLTEb" + }, + "source": [ + "## Register Google Gemma on Vertex AI" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "28d24c673821" + }, + "source": [ + "To serve Gemma with Pytorch Inference on Vertex AI, you start importing the model on Vertex AI Model Registry, a central repository where you can manage the lifecycle of your ML models on Vertex AI.\n", + "\n", + "Before going into the code to upload or import a model on Vertex AI, let's quickly review the arguments provided to the `aiplatform.Model.upload` method:\n", + "\n", + "* **`display_name`** is the name that will be shown in the Vertex AI Model Registry.\n", + "\n", + "* **`serving_container_image_uri`** is the location of the Hugging Face DLC for Pytorch Inference that will be used for serving the model.\n", + "\n", + "* (optional) **`serving_container_ports`** is the port where the Vertex AI endpoint |will be exposed, by default 8080.\n", + "\n", + "For more information on the supported `aiplatform.Model.upload` arguments, check [its Python reference](https://cloud.google.com/python/docs/reference/aiplatform/latest/google.cloud.aiplatform.Model#google_cloud_aiplatform_Model_upload)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "ef487ce082f3" + }, + "outputs": [], + "source": [ + "model = Model.upload(\n", + " display_name=\"google--gemma2-tgi-multi-lora-model\",\n", + " artifact_uri=str(serve_uri),\n", + " serving_container_image_uri=\"us-docker.pkg.dev/deeplearning-platform-release/gcr.io/huggingface-pytorch-inference-cu121.2-3.transformers.4-46.ubuntu2204.py311\",\n", + " serving_container_ports=[8080],\n", + " serving_container_environment_variables={\n", + " \"HUGGING_FACE_HUB_TOKEN\": get_token(),\n", + " },\n", + ")\n", + "model.wait()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "c427c0a87016" + }, + "source": [ + "## Deploy Google Gemma on Vertex AI" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "ef4cd160dc09" + }, + "source": [ + "After the model is registered on Vertex AI, you can deploy the model to an endpoint.\n", + "\n", + "You need to first deploy a model to an endpoint before that model can be used to serve online predictions. Deploying a model associates physical resources with the model so it can serve online predictions with low latency.\n", + "\n", + "Before going into the code to deploy a model to an endpoint, let's quickly review the arguments provided to the `aiplatform.Model.deploy` method:\n", + "\n", + "- **`endpoint`** is the endpoint to deploy the model to, which is optional, and by default will be set to the model display name with the `_endpoint` suffix.\n", + "- **`machine_type`**, **`accelerator_type`** and **`accelerator_count`** are arguments that define which instance to use, and additionally, the accelerator to use and the number of accelerators, respectively. The `machine_type` and the `accelerator_type` are tied together, so you will need to select an instance that supports the accelerator that you are using and vice-versa. More information about the different instances at [Compute Engine Documentation - GPU machine types](https://cloud.google.com/compute/docs/gpus), and about the `accelerator_type` naming at [Vertex AI Documentation - MachineSpec](https://cloud.google.com/vertex-ai/docs/reference/rest/v1/MachineSpec).\n", + "\n", + "For more information on the supported `aiplatform.Model.deploy` arguments, you can check [its Python reference](https://cloud.google.com/python/docs/reference/aiplatform/latest/google.cloud.aiplatform.Model#google_cloud_aiplatform_Model_deploy)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "e777427015db" + }, + "outputs": [], + "source": [ + "deployed_model = model.deploy(\n", + " endpoint=Endpoint.create(display_name=\"google--gemma2-tgi-multi-lora-endpoint\"),\n", + " machine_type=\"g2-standard-4\",\n", + " accelerator_type=\"NVIDIA_L4\",\n", + " accelerator_count=1,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "5f2380d3ea60" + }, + "source": [ + "> Note that the model deployment on Vertex AI can take around 15 to 25 minutes; most of the time being the allocation / reservation of the resources, setting up the network and security, and such." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "9b3fd1898241" + }, + "source": [ + "## Online predictions on Vertex AI" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "2aa9cee03bd0" + }, + "source": [ + "Once the model is deployed on Vertex AI, you can run the online predictions using the `aiplatform.Endpoint.predict` method, which will send the requests to the running endpoint in the `/predict` route specified within the container following Vertex AI I/O payload formatting." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "79e393e97ddb" + }, + "source": [ + "### Via Python" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "1a404fd19a08" + }, + "source": [ + "#### Within the same session\n", + "\n", + "To run the online prediction via the Vertex AI SDK, you can simply use the `predict` method." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "28Hy9Ok6tz7J" + }, + "outputs": [], + "source": [ + "output = deployed_model.predict(instances=prediction_request[\"instances\"])\n", + "for prediction in output.predictions:\n", + " print(\"------- Prediction -------\")\n", + " print(prediction)\n", + " print(\"--------------------------\\n\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "b28efb473c7e" + }, + "source": [ + "#### From a different session\n", + "\n", + "To run the online prediction from a different session, you can run the following snippet." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "e003ab03f30f" + }, + "outputs": [], + "source": [ + "import os\n", + "\n", + "from google.cloud import aiplatform\n", + "import requests\n", + "\n", + "PROJECT_ID = \"[your-project-id]\" # @param {type:\"string\", isTemplate: true}\n", + "if PROJECT_ID == \"[your-project-id]\":\n", + " PROJECT_ID = str(os.environ.get(\"GOOGLE_CLOUD_PROJECT\"))\n", + "\n", + "LOCATION = os.environ.get(\"GOOGLE_CLOUD_REGION\", \"us-central1\")\n", + "\n", + "aiplatform.init(project=PROJECT_ID, location=LOCATION)\n", + "\n", + "ENDPOINT_DISPLAY_NAME = \"google--gemma2-tgi-multi-lora-endpoint\"\n", + "\n", + "# Iterates over all the Vertex AI Endpoints within the current project and keeps the first match (if any), otherwise set to None\n", + "ENDPOINT_ID = next(\n", + " (\n", + " endpoint.name\n", + " for endpoint in aiplatform.Endpoint.list()\n", + " if endpoint.display_name == ENDPOINT_DISPLAY_NAME\n", + " ),\n", + " None,\n", + ")\n", + "assert ENDPOINT_ID, (\n", + " \"`ENDPOINT_ID` is not set, please make sure that the `endpoint_display_name` is correct at \"\n", + " f\"https://console.cloud.google.com/vertex-ai/online-prediction/endpoints?project={os.getenv('PROJECT_ID')}\"\n", + ")\n", + "\n", + "# Initiate the endpoint\n", + "endpoint = aiplatform.Endpoint(\n", + " f\"projects/{PROJECT_ID}/locations/{LOCATION}/endpoints/{ENDPOINT_ID}\"\n", + ")\n", + "\n", + "# Set instances\n", + "instances = []\n", + "user_prompts = [\n", + " \"I have a table called orders with columns order_id (INT), customer_id (INT), order_date (DATE), and total_amount (DECIMAL). I need to find the total revenue generated in the month of October 2023. How can I write a SQL query to achieve this?\",\n", + " \"# Context: You have a list of numbers called `my_numbers`.\\n# Question: How do I calculate the sum of all the numbers in `my_numbers` using a built-in function?\\n# Example `my_numbers` list:\\nmy_numbers = [1, 2, 3, 4, 5]\",\n", + "]\n", + "\n", + "for prompt in user_prompts:\n", + " instances.append(\n", + " {\n", + " \"inputs\": prompt,\n", + " \"parameters\": {\"temperature\": 0.7, \"do_sample\": True},\n", + " }\n", + " )\n", + "\n", + "# Generate the prediction\n", + "output = endpoint.predict(instances=instances)\n", + "for prediction in output.predictions:\n", + " print(\"------- Prediction -------\")\n", + " print(prediction)\n", + " print(\"--------------------------\\n\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "7cc0b19ab641" + }, + "source": [ + "### Via gcloud" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "8fed7007c31c" + }, + "source": [ + "You can also send the requests using the `gcloud` CLI via the `gcloud ai endpoints` command.\n", + "\n", + "> Note that, before proceeding, you should either replace the values or set the following environment variables in advance from the Python variables set in the example, as follows:\n", + ">\n", + "> ```python\n", + "> import os\n", + "> os.environ[\"PROJECT_ID\"] = PROJECT_ID\n", + "> os.environ[\"LOCATION\"] = LOCATION\n", + "> os.environ[\"ENDPOINT_NAME\"] = \"google--gemma2-tgi-multi-lora-endpoint\"\n", + "> ```" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "7fb1547d409f" + }, + "outputs": [], + "source": [ + "%%bash\n", + "# Get Endpoint ID\n", + "ENDPOINT_ID=$(gcloud ai endpoints list \\\n", + " --project=$PROJECT_ID \\\n", + " --region=$LOCATION \\\n", + " --filter=\"display_name=$ENDPOINT_NAME\" \\\n", + " --format=\"value(name)\" \\\n", + " | cut -d'/' -f6)\n", + "\n", + "# Generate the prediction\n", + "echo '{\n", + " \"instances\": [\n", + " {\n", + " \"inputs\":\"I have a table called orders with columns order_id (INT), customer_id (INT), order_date (DATE), and total_amount (DECIMAL). I need to find the total revenue generated in the month of October 2023. How can I write a SQL query to achieve this?\",\n", + " \"parameters\":{\n", + " \"temperature\":0.7,\n", + " \"do_sample\":true\n", + " }\n", + " },\n", + " {\n", + " \"inputs\":\"# Context: You have a list of numbers called `my_numbers`.\\n# Question: How do I calculate the sum of all the numbers in `my_numbers` using a built-in function?\\n# Example `my_numbers` list:\\nmy_numbers = [1, 2, 3, 4, 5]\",\n", + " \"parameters\":{\n", + " \"temperature\":0.7,\n", + " \"do_sample\":true\n", + " }\n", + " }]\n", + "}' | gcloud ai endpoints predict $ENDPOINT_ID \\\n", + " --project=$PROJECT_ID \\\n", + " --region=$LOCATION \\\n", + " --json-request=\"-\"" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "7e588a65759e" + }, + "source": [ + "### Via cURL" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "cf3de731bcf4" + }, + "source": [ + "Alternatively, you can also send the requests via `cURL`.\n", + "\n", + "> Note that, before proceeding, you should either replace the values or set the following environment variables in advance from the Python variables set in the example, as follows:\n", + ">\n", + "> ```python\n", + "> import os\n", + "> os.environ[\"PROJECT_ID\"] = PROJECT_ID\n", + "> os.environ[\"LOCATION\"] = LOCATION\n", + "> os.environ[\"ENDPOINT_NAME\"] = \"google--gemma2-tgi-multi-lora-endpoint\"\n", + "> ```" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "a71da4e1152b" + }, + "outputs": [], + "source": [ + "%%bash\n", + "# Get Endpoint ID\n", + "ENDPOINT_ID=$(gcloud ai endpoints list \\\n", + " --project=$PROJECT_ID \\\n", + " --region=$LOCATION \\\n", + " --filter=\"display_name=$ENDPOINT_NAME\" \\\n", + " --format=\"value(name)\" \\\n", + " | cut -d'/' -f6)\n", + "\n", + "# Generate the prediction\n", + "curl -X POST \\\n", + " -H \"Authorization: Bearer $(gcloud auth print-access-token)\" \\\n", + " -H \"Content-Type: application/json\" \\\n", + " \"https://${LOCATION}-aiplatform.googleapis.com/v1/projects/${PROJECT_ID}/locations/${LOCATION}/endpoints/${ENDPOINT_ID}:predict\" \\\n", + " -d @- <\n", + " \n", + " \n", + " \"Google
    Open in Colab\n", + "
    \n", + " \n", + " \n", + " \n", + " \"Google
    Open in Colab Enterprise\n", + "
    \n", + " \n", + " \n", + " \n", + " \"Vertex
    Open in Vertex AI Workbench\n", + "
    \n", + " \n", + " \n", + " \n", + " \"BigQuery
    Open in BigQuery Studio\n", + "
    \n", + " \n", + " \n", + " \n", + " \"GitHub
    View on GitHub\n", + "
    \n", + " \n", + "\n", + "\n", + "
    \n", + "\n", + "Share to:\n", + "\n", + "\n", + " \"LinkedIn\n", + "\n", + "\n", + "\n", + " \"Bluesky\n", + "\n", + "\n", + "\n", + " \"X\n", + "\n", + "\n", + "\n", + " \"Reddit\n", + "\n", + "\n", + "\n", + " \"Facebook\n", + "" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "84f0f73a0f76" + }, + "source": [ + "| | |\n", + "|-|-|\n", + "| Author(s) | [Jasper Xu](https://github.com/ZehaoXU), [Luis Lopez](https://github.com/luseloso) |" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "tvgnzT1CKxrO" + }, + "source": [ + "## Overview\n", + "\n", + "This notebook showcases a simple end-to-end process for extracting entities and performing data analytics using BigQuery in conjunction with an open-source text-generation Large Language Model (LLM). We use Meta's Llama 3.3 70B model as an example, and the process involves:\n", + "\n", + "* Deploy the Llama 3.3 70B model on Vertex AI.\n", + "* Configure the necessary setup, including downloading sample data and enabling BigQuery access to Vertex AI.\n", + "* Create a [remote model](https://cloud.google.com/bigquery/docs/reference/standard-sql/bigqueryml-syntax-create-remote-model-open) in BigQuery against the Llama 3.3 70B\n", + "* Employ the [ML.GENERATE_TEXT](https://cloud.google.com/bigquery/docs/reference/standard-sql/bigqueryml-syntax-generate-text) function to extract structured information from medical transcripts.\n", + "* Analyzing the resulting structured data to gain insights.\n", + "\n", + "### Costs\n", + "\n", + "This tutorial uses billable components of Google Cloud:\n", + "\n", + "* Vertex AI\n", + "* Cloud Storage\n", + "* BigQuery\n", + "\n", + "Learn about [Vertex AI pricing](https://cloud.google.com/vertex-ai/pricing), [Cloud Storage pricing](https://cloud.google.com/storage/pricing), [BigQuery pricing](https://cloud.google.com/bigquery/pricing), and use the [Pricing Calculator](https://cloud.google.com/products/calculator/) to generate a cost estimate based on your projected usage." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "dmWOrTJ3gx13" + }, + "source": [ + "## Authenticate your notebook environment (Colab only)\n", + "\n", + "If you're running this notebook on Google Colab, run the cell below to authenticate your environment." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "NyKGtVQjgx13" + }, + "outputs": [], + "source": [ + "import sys\n", + "\n", + "if \"google.colab\" in sys.modules:\n", + " from google.colab import auth\n", + "\n", + " auth.authenticate_user()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "61RBz8LLbxCR" + }, + "source": [ + "## Deploy the Llama 3.3 70B Model on Vertex AI" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "No17Cw5hgx12" + }, + "source": [ + "#### Request H100 quota\n", + "\n", + "By default, the quota for H100 deployment `Custom model serving per region` is 0. If you prefer to use H100 for higher throughput (recommended), you need to request for H100 quota following the instructions at [\"Request a higher quota\"](https://cloud.google.com/docs/quota/view-manage#requesting_higher_quota). We recommend starting with a small quota request, for example setting it to 8.\n", + "\n", + "If you want to run predictions with A100 80GB or H100 GPUs, we recommend using the regions listed below. \n", + "\n", + "| Machine Type | Accelerator Type | Recommended Regions |\n", + "| --- | --- | --- |\n", + "| a2-ultragpu-1g | 1 NVIDIA_A100_80GB | us-central1, us-east4, europe-west4, asia-southeast1, us-east4 |\n", + "| a3-highgpu-2g | 2 NVIDIA_H100_80GB | us-west1, asia-southeast1, europe-west4 |\n", + "| a3-highgpu-4g | 4 NVIDIA_H100_80GB | us-west1, asia-southeast1, europe-west4 |\n", + "| a3-highgpu-8g | 8 NVIDIA_H100_80GB | us-central1, us-east5, europe-west4, us-west1, asia-southeast1 |\n", + "\n", + "**NOTE:** Make sure you have associated quota in selected regions. Click the links to see your current quota for each GPU type: [Nvidia A100 80GB](https://console.cloud.google.com/iam-admin/quotas?metric=aiplatform.googleapis.com%2Fcustom_model_serving_nvidia_a100_80gb_gpus), [Nvidia H100 80GB](https://console.cloud.google.com/iam-admin/quotas?metric=aiplatform.googleapis.com%2Fcustom_model_serving_nvidia_h100_gpus).\n", + "\n", + "Alternatively, you can choose to use Nvidia L4 GPUs for this deployment.\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "ABhBcw0bloIu" + }, + "source": [ + "#### Setup Google Cloud project\n", + "\n", + "1. [Make sure that billing is enabled for your project](https://cloud.google.com/billing/docs/how-to/modify-project).\n", + "\n", + "\n", + "2. Point the code to your Project ID. \n", + "\n", + "\n", + "3. [Create a Cloud Storage bucket](https://cloud.google.com/storage/docs/creating-buckets) for storing experiment outputs. The specified Cloud Storage bucket (`BUCKET_URI`) should be located in the same region as where the notebook was launched. Note that a multi-region bucket (eg. \"us\") is not considered a match for a single region covered by the multi-region range (eg. \"us-central1\").\n", + "\n", + "\n", + "4. Set your preferred region.\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "1f81f6289064" + }, + "source": [ + "**Note:** You do not need to fill out the `PROJECT_ID`, `BUCKET_URI`, or `REGION` as we will pick these values from `os.environ[\"GOOGLE_CLOUD_PROJECT\"]`, `os.environ[\"GOOGLE_CLOUD_REGION\"]` and the code will create a temporary bucket. \n", + "\n", + "If you would like to specify specific values, replace the empty strings below." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "fda9a1639038" + }, + "outputs": [], + "source": [ + "# fmt: off\n", + "PROJECT_ID = \"\" # @param {type:\"string\"}\n", + "BUCKET_URI = \"gs://\" # @param {type:\"string\"}\n", + "REGION = \"\" # @param {type:\"string\"}\n", + "# fmt: on" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "SPdyTYbDjeHw" + }, + "outputs": [], + "source": [ + "# Import the necessary packages\n", + "\n", + "# Upgrade Vertex AI SDK.\n", + "% pip install --upgrade --quiet 'google-cloud-aiplatform>=1.64.0'\n", + "! git clone https://github.com/GoogleCloudPlatform/vertex-ai-samples.git\n", + "\n", + "import datetime\n", + "import importlib\n", + "import os\n", + "import uuid\n", + "\n", + "from google.cloud import aiplatform\n", + "\n", + "common_util = importlib.import_module(\n", + " \"vertex-ai-samples.community-content.vertex_model_garden.model_oss.notebook_util.common_util\"\n", + ")\n", + "\n", + "models, endpoints = {}, {}\n", + "\n", + "# Get the default cloud project id.\n", + "if not PROJECT_ID:\n", + " PROJECT_ID = os.environ[\"GOOGLE_CLOUD_PROJECT\"]\n", + "\n", + "# Get the default region for launching jobs.\n", + "if not REGION:\n", + " REGION = os.environ[\"GOOGLE_CLOUD_REGION\"]\n", + "\n", + "# Enable the Vertex AI API and Compute Engine API, if not already.\n", + "print(\"Enabling Vertex AI API and Compute Engine API.\")\n", + "! gcloud services enable aiplatform.googleapis.com compute.googleapis.com --project {PROJECT_ID}\n", + "\n", + "# Cloud Storage bucket for storing the experiment artifacts.\n", + "# A unique GCS bucket will be created for the purpose of this notebook. If you\n", + "# prefer using your own GCS bucket, change the value yourself below.\n", + "now = datetime.datetime.now().strftime(\"%Y%m%d%H%M%S\")\n", + "BUCKET_NAME = \"/\".join(BUCKET_URI.split(\"/\")[:3])\n", + "\n", + "if BUCKET_URI is None or BUCKET_URI.strip() == \"\" or BUCKET_URI == \"gs://\":\n", + " BUCKET_URI = f\"gs://{PROJECT_ID}-tmp-{now}-{str(uuid.uuid4())[:4]}\"\n", + " BUCKET_NAME = \"/\".join(BUCKET_URI.split(\"/\")[:3])\n", + " ! gsutil mb -l {REGION} {BUCKET_URI}\n", + "else:\n", + " assert BUCKET_URI.startswith(\"gs://\"), \"BUCKET_URI must start with `gs://`.\"\n", + " shell_output = ! gsutil ls -Lb {BUCKET_NAME} | grep \"Location constraint:\" | sed \"s/Location constraint://\"\n", + " bucket_region = shell_output[0].strip().lower()\n", + " if bucket_region != REGION:\n", + " raise ValueError(\n", + " \"Bucket region %s is different from notebook region %s\"\n", + " % (bucket_region, REGION)\n", + " )\n", + "print(f\"Using this GCS Bucket: {BUCKET_URI}\")\n", + "\n", + "STAGING_BUCKET = os.path.join(BUCKET_URI, \"temporal\")\n", + "MODEL_BUCKET = os.path.join(BUCKET_URI, \"llama3-3\")\n", + "\n", + "\n", + "# Initialize Vertex AI API.\n", + "print(\"Initializing Vertex AI API.\")\n", + "aiplatform.init(project=PROJECT_ID, location=REGION, staging_bucket=STAGING_BUCKET)\n", + "\n", + "# Gets the default SERVICE_ACCOUNT.\n", + "shell_output = ! gcloud projects describe $PROJECT_ID\n", + "project_number = shell_output[-1].split(\":\")[1].strip().replace(\"'\", \"\")\n", + "SERVICE_ACCOUNT = f\"{project_number}-compute@developer.gserviceaccount.com\"\n", + "print(\"Using this default Service Account:\", SERVICE_ACCOUNT)\n", + "\n", + "\n", + "# Provision permissions to the SERVICE_ACCOUNT with the GCS bucket\n", + "! gsutil iam ch serviceAccount:{SERVICE_ACCOUNT}:roles/storage.admin $BUCKET_NAME\n", + "\n", + "! gcloud config set project $PROJECT_ID\n", + "! gcloud projects add-iam-policy-binding --no-user-output-enabled {PROJECT_ID} --member=serviceAccount:{SERVICE_ACCOUNT} --role=\"roles/storage.admin\"\n", + "! gcloud projects add-iam-policy-binding --no-user-output-enabled {PROJECT_ID} --member=serviceAccount:{SERVICE_ACCOUNT} --role=\"roles/aiplatform.user\"" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "c55789278f88" + }, + "source": [ + "# Access Llama 3.3 models on Vertex AI for serving\n", + "The original models from Meta are converted into the Hugging Face format for serving in Vertex AI.\n", + "Accept the model agreement to access the models:\n", + "1. Open the [Llama 3.3 model card](https://console.cloud.google.com/vertex-ai/publishers/meta/model-garden/llama3-3) from [Vertex AI Model Garden](https://cloud.google.com/model-garden).\n", + "2. Review and accept the agreement in the pop-up window on the model card page. If you have previously accepted the model agreement, there will not be a pop-up window on the model card page and this step is not needed.\n", + "3. After accepting the agreement of Llama 3.3, a `gs://` URI containing Llama 3.3 models will be shared.\n", + "4. Paste the URI in the `VERTEX_AI_MODEL_GARDEN_LLAMA_3_3` field below." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "7b8ae65ff28c" + }, + "outputs": [], + "source": [ + "# fmt: off\n", + "VERTEX_AI_MODEL_GARDEN_LLAMA_3_3 = \"gs://vertex-model-garden-public-us/llama3.3\" # @param {type:\"string\", isTemplate:true}\n", + "# fmt: on\n", + "assert VERTEX_AI_MODEL_GARDEN_LLAMA_3_3, (\n", + " \"Click the agreement of Llama 3.3 in Vertex AI Model Garden, and get the GCS path of Llama 3.3 model artifacts.\"\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "KCsVQ2qolxSD" + }, + "source": [ + "### Deploy Llama 3.3 70B Instruct with vLLM\n", + "\n", + "The following step will register the model in Vertex AI and deploy it to an endpoint for serving. \n", + "\n", + "\n", + "**NOTE:** It usually takes 15~30 minutes to complete. \n", + "\n", + "To monitor deployment progress, navigate to [Vertex AI](https://console.cloud.google.com/vertex-ai) -> Online Prediction -> View logs for the Llama 3.3 endpoint." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "bVxM9XiEjt1I" + }, + "outputs": [], + "source": [ + "# @markdown This section uploads Llama 3.3 to Model Registry and deploys it to a Vertex AI Endpoint. It takes ~30 minutes.\n", + "\n", + "# @markdown The serving efficiency of L4 GPUs is inferior to that of H100 GPUs, but L4 GPUs are nevertheless good serving solutions if you do not have H100 quota.\n", + "\n", + "# @markdown H100 is hard to get for now. It's recommended to use the deployment button in the model card. You can still try to deploy H100 endpoint through the notebook, but there is a chance that resource is not available.\n", + "\n", + "# @markdown Set the model to deploy.\n", + "\n", + "# fmt: off\n", + "base_model_name = \"Llama-3.3-70B-Instruct\" # @param [\"Llama-3.3-70B-Instruct\"] {isTemplate:true}\n", + "# fmt: on\n", + "model_id = os.path.join(VERTEX_AI_MODEL_GARDEN_LLAMA_3_3, base_model_name)\n", + "ENABLE_DYNAMIC_LORA = False\n", + "hf_model_id = \"meta-llama/\" + base_model_name\n", + "\n", + "accelerator_type = \"NVIDIA_L4\" # @param [\"NVIDIA_H100_80GB\", \"NVIDIA_L4\"]\n", + "\n", + "# The pre-built serving docker images.\n", + "VLLM_DOCKER_URI = \"us-docker.pkg.dev/vertex-ai/vertex-vision-model-garden-dockers/pytorch-vllm-serve:20241001_0916_RC00\"\n", + "\n", + "use_dedicated_endpoint = False\n", + "# @markdown Find Vertex AI prediction supported accelerators and regions at https://cloud.google.com/vertex-ai/docs/predictions/configure-compute.\n", + "if accelerator_type == \"NVIDIA_L4\":\n", + " machine_type = \"g2-standard-96\"\n", + " accelerator_count = 8\n", + " max_loras = 1\n", + "elif accelerator_type == \"NVIDIA_H100_80GB\":\n", + " machine_type = \"a3-highgpu-4g\"\n", + " accelerator_count = 4\n", + " max_loras = 1\n", + "else:\n", + " raise ValueError(\n", + " f\"Recommended GPU setting not found for: {accelerator_type} and {base_model_name}.\"\n", + " )\n", + "\n", + "common_util.check_quota(\n", + " project_id=PROJECT_ID,\n", + " region=REGION,\n", + " accelerator_type=accelerator_type,\n", + " accelerator_count=accelerator_count,\n", + " is_for_training=False,\n", + ")\n", + "\n", + "gpu_memory_utilization = 0.95\n", + "max_model_len = 8192 # Maximum context length.\n", + "\n", + "\n", + "def deploy_model_vllm(\n", + " model_name: str,\n", + " model_id: str,\n", + " publisher: str,\n", + " publisher_model_id: str,\n", + " service_account: str,\n", + " base_model_id: str = None,\n", + " machine_type: str = \"g2-standard-8\",\n", + " accelerator_type: str = \"NVIDIA_L4\",\n", + " accelerator_count: int = 1,\n", + " gpu_memory_utilization: float = 0.9,\n", + " max_model_len: int = 4096,\n", + " dtype: str = \"auto\",\n", + " enable_trust_remote_code: bool = False,\n", + " enforce_eager: bool = False,\n", + " enable_lora: bool = False,\n", + " enable_chunked_prefill: bool = False,\n", + " enable_prefix_cache: bool = False,\n", + " host_prefix_kv_cache_utilization_target: float = 0.0,\n", + " max_loras: int = 1,\n", + " max_cpu_loras: int = 8,\n", + " use_dedicated_endpoint: bool = False,\n", + " max_num_seqs: int = 256,\n", + " model_type: str = None,\n", + ") -> tuple[aiplatform.Model, aiplatform.Endpoint]:\n", + " \"\"\"Deploys trained models with vLLM into Vertex AI.\"\"\"\n", + " endpoint = aiplatform.Endpoint.create(\n", + " display_name=f\"{model_name}-endpoint\",\n", + " dedicated_endpoint_enabled=use_dedicated_endpoint,\n", + " )\n", + "\n", + " if not base_model_id:\n", + " base_model_id = model_id\n", + "\n", + " # See https://docs.vllm.ai/en/latest/models/engine_args.html for a list of possible arguments with descriptions.\n", + " vllm_args = [\n", + " \"python\",\n", + " \"-m\",\n", + " \"vllm.entrypoints.api_server\",\n", + " \"--host=0.0.0.0\",\n", + " \"--port=8080\",\n", + " f\"--model={model_id}\",\n", + " f\"--tensor-parallel-size={accelerator_count}\",\n", + " \"--swap-space=16\",\n", + " f\"--gpu-memory-utilization={gpu_memory_utilization}\",\n", + " f\"--max-model-len={max_model_len}\",\n", + " f\"--dtype={dtype}\",\n", + " f\"--max-loras={max_loras}\",\n", + " f\"--max-cpu-loras={max_cpu_loras}\",\n", + " f\"--max-num-seqs={max_num_seqs}\",\n", + " \"--disable-log-stats\",\n", + " ]\n", + "\n", + " if enable_trust_remote_code:\n", + " vllm_args.append(\"--trust-remote-code\")\n", + "\n", + " if enforce_eager:\n", + " vllm_args.append(\"--enforce-eager\")\n", + "\n", + " if enable_lora:\n", + " vllm_args.append(\"--enable-lora\")\n", + "\n", + " if enable_chunked_prefill:\n", + " vllm_args.append(\"--enable-chunked-prefill\")\n", + "\n", + " if enable_prefix_cache:\n", + " vllm_args.append(\"--enable-prefix-caching\")\n", + "\n", + " if 0 < host_prefix_kv_cache_utilization_target < 1:\n", + " vllm_args.append(\n", + " f\"--host-prefix-kv-cache-utilization-target={host_prefix_kv_cache_utilization_target}\"\n", + " )\n", + "\n", + " if model_type:\n", + " vllm_args.append(f\"--model-type={model_type}\")\n", + "\n", + " env_vars = {\n", + " \"MODEL_ID\": base_model_id,\n", + " \"DEPLOY_SOURCE\": \"notebook\",\n", + " }\n", + "\n", + " # HF_TOKEN is not a compulsory field and may not be defined.\n", + " try:\n", + " if HF_TOKEN:\n", + " env_vars[\"HF_TOKEN\"] = HF_TOKEN\n", + " except NameError:\n", + " pass\n", + "\n", + " model = aiplatform.Model.upload(\n", + " display_name=model_name,\n", + " serving_container_image_uri=VLLM_DOCKER_URI,\n", + " serving_container_args=vllm_args,\n", + " serving_container_ports=[8080],\n", + " serving_container_predict_route=\"/generate\",\n", + " serving_container_health_route=\"/ping\",\n", + " serving_container_environment_variables=env_vars,\n", + " serving_container_shared_memory_size_mb=(16 * 1024), # 16 GB\n", + " serving_container_deployment_timeout=7200,\n", + " model_garden_source_model_name=(\n", + " f\"publishers/{publisher}/models/{publisher_model_id}\"\n", + " ),\n", + " )\n", + " print(\n", + " f\"Deploying {model_name} on {machine_type} with {accelerator_count} {accelerator_type} GPU(s).\"\n", + " )\n", + " model.deploy(\n", + " endpoint=endpoint,\n", + " machine_type=machine_type,\n", + " accelerator_type=accelerator_type,\n", + " accelerator_count=accelerator_count,\n", + " deploy_request_timeout=1800,\n", + " service_account=service_account,\n", + " system_labels={\n", + " \"NOTEBOOK_NAME\": \"model_garden_pytorch_llama3_3_deployment.ipynb\",\n", + " },\n", + " )\n", + " print(\"endpoint_name:\", endpoint.name)\n", + "\n", + " return model, endpoint\n", + "\n", + "\n", + "models[\"vllm_gpu\"], endpoints[\"vllm_gpu\"] = deploy_model_vllm(\n", + " model_name=common_util.get_job_name_with_datetime(prefix=\"llama3-3-serve\"),\n", + " model_id=model_id,\n", + " publisher=\"meta\",\n", + " publisher_model_id=\"llama3-3\",\n", + " base_model_id=hf_model_id,\n", + " service_account=SERVICE_ACCOUNT,\n", + " machine_type=machine_type,\n", + " accelerator_type=accelerator_type,\n", + " accelerator_count=accelerator_count,\n", + " gpu_memory_utilization=gpu_memory_utilization,\n", + " max_model_len=max_model_len,\n", + " max_loras=max_loras,\n", + " enforce_eager=True,\n", + " enable_lora=ENABLE_DYNAMIC_LORA,\n", + " enable_chunked_prefill=not ENABLE_DYNAMIC_LORA,\n", + " use_dedicated_endpoint=use_dedicated_endpoint,\n", + " model_type=\"llama3.1\",\n", + ")\n", + "# @markdown Click \"Show Code\" to see more details." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "s8bAhMq8wglq" + }, + "source": [ + "#### Get the endpoint ID\n", + "\n", + "Once the deployment is successful, we need to grab the endpoint. We can reconstruct the String from our previous step" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "xIWHZNzkwEIt" + }, + "outputs": [], + "source": [ + "ENDPOINT_ID = f\"https://{REGION}-aiplatform.googleapis.com/v1/projects/{PROJECT_ID}/locations/{REGION}/endpoints/{endpoints['vllm_gpu'].name}\"\n", + "print(ENDPOINT_ID)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "sFqr3PYnoxUf" + }, + "source": [ + "## Data Analytics in BigQuery" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "SNeGovGko2Aw" + }, + "source": [ + "### Setup" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "RXDi9glXtUBN" + }, + "source": [ + "#### Create a new dataset\n", + "\n", + "This will house any tables created throughout this notebook." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "VxcDQUwxta8v" + }, + "outputs": [], + "source": [ + "!bq mk --location=us --dataset --project_id={PROJECT_ID} demo_dataset" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "M26CPteyth0R" + }, + "source": [ + "#### Create a Cloud resource connection\n", + "\n", + "[Cloud resource connections](https://cloud.google.com/bigquery/docs/connections-api-intro#cloud-resource-connections) enable BigQuery to access other Cloud services, like Cloud Storage and Vertex AI." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "rZB9N43itty-" + }, + "outputs": [], + "source": [ + "!bq mk --connection --connection_type=CLOUD_RESOURCE --location=us --project_id={PROJECT_ID} \"demo_conn\"\n", + "!bq show --location=us --connection --project_id={PROJECT_ID} \"demo_conn\"" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "jd-6mfdot8x7" + }, + "source": [ + "#### Add permissions to Cloud resource connection service account\n", + "\n", + "The Cloud resource connection is associated with a service account. \n", + "\n", + "The following cell enables the service account to access the Vertex AI service.\n", + "\n", + "**Note:** Copy the service account ID from the prior cell and input it below. It will look like `your-copied-service-account@gcp-sa-bigquery-condel.iam.gserviceaccount.com`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "NiJUgWYYuC6n" + }, + "outputs": [], + "source": [ + "# fmt: off\n", + "connection_service_account = \"your-copied-service-account@gcp-sa-bigquery-condel.iam.gserviceaccount.com\" # @param {type: \"string\"}\n", + "# fmt: on\n", + "connection_member = f\"serviceAccount:{connection_service_account}\"\n", + "\n", + "\n", + "!gcloud projects add-iam-policy-binding {PROJECT_ID} --member={connection_member} --role='roles/aiplatform.user' --condition=None --quiet" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "tEhojixwvGrt" + }, + "source": [ + "### Load medical transcripts into BigQuery\n", + "\n", + "For this demo, we will use a small medical transcripts dataset which has unstructured, varied raw transcripts capturing history, diagnosis and treatment provided of patients visiting a medical facility. \n", + "\n", + "The dataset contains 164 rows, each with an average of 814 input tokens.\n", + "\n", + "Let's load this dataset into a BigQuery table and peek one example row:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "x2D6NnFovOdb" + }, + "outputs": [], + "source": [ + "%%bigquery --project $PROJECT_ID\n", + "\n", + "LOAD DATA OVERWRITE\n", + " demo_dataset.medical_transcript\n", + "FROM FILES( format='NEWLINE_DELIMITED_JSON',\n", + " uris = ['gs://cloud-samples-data/vertex-ai/model-evaluation/peft_eval_sample.jsonl'] );\n", + "\n", + "SELECT * FROM demo_dataset.medical_transcript LIMIT 10;" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "7UbctAXm0XaA" + }, + "source": [ + "### Create a remote model\n", + "\n", + "In this next step we will register a Vertex AI endpoint as a remote model in BigQuery. Then, you use the [ML.GENERATE_TEXT function](https://cloud.google.com/bigquery/docs/reference/standard-sql/bigqueryml-syntax-predict) to make predictions using the remote model.\n", + "\n", + "You can use remote models when a model is too large to import into BigQuery. They are also useful when you want to have a single point of inference for online, batch, and micro-batch use cases.\n", + "\n", + "**Note**: If you get an error in this step is because the service account permissions have yet finished propagating, please wait a minute and retry\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "BsMbwJc10okx" + }, + "outputs": [], + "source": [ + "from google.cloud import bigquery\n", + "\n", + "client = bigquery.Client(project=PROJECT_ID)\n", + "\n", + "query = f\"\"\"\n", + "CREATE OR REPLACE MODEL demo_dataset.llama_3_3\n", + "REMOTE WITH CONNECTION\n", + " `{PROJECT_ID}.us.demo_conn`\n", + "OPTIONS(\n", + " endpoint='{ENDPOINT_ID}'\n", + ");\n", + "\"\"\"\n", + "\n", + "query_job = client.query(query) # API request\n", + "query_job.result() # Waits for the query to complete\n", + "\n", + "print(\"Remote model `demo_dataset.llama_3_3` created or replaced successfully.\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "jJ9KjfU52gf3" + }, + "source": [ + "### Extract structured entity from medical transcripts\n", + "\n", + "We will now use our Llama model to extract structured data from the unstructured transcripts with [ML.GENERATE_TEXT](https://cloud.google.com/bigquery/docs/reference/standard-sql/bigqueryml-syntax-generate-text). Say we want to extract the patient's age, gender and list of diseases for each entry. We can do so with a SQL statement like the following and save the derived insights to a table. We include the information we want to extract and its schema in the model prompt." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "-fd4S6Wp2ari" + }, + "outputs": [], + "source": [ + "%%bigquery --project $PROJECT_ID\n", + "\n", + "CREATE TEMP FUNCTION ExtractOutput(s STRING)\n", + "RETURNS STRING\n", + "AS (\n", + " SUBSTR(s, INSTR(s, \"Output:\")+8)\n", + ");\n", + "\n", + "\n", + "CREATE OR REPLACE TABLE demo_dataset.medical_transcript_analysis AS (\n", + "SELECT\n", + " ExtractOutput(ml_generate_text_llm_result) AS generated_text, * EXCEPT(ml_generate_text_llm_result)\n", + "FROM\n", + " ML.GENERATE_TEXT( MODEL `demo_dataset.llama_3_3`,\n", + " (\n", + " SELECT\n", + " CONCAT('Extract the Gender, Age (in years), and Disease information from the following medical transcript. Return **only** a JSON in the following schema: \\n{ \"Age\": Int, \"Gender\": \"String\", \"Disease\": [\"String\"]}. \\nIf Age, Gender, or Disease information is not found, return `null` for that field. Summarize the disease(s) in 1 to 5 words. If the patient has multiple diseases, include them in a comma-separated list within the \"Disease\" field. \\n**Do not include any other text or labels in your response.**. \\n', input_text) AS prompt\n", + " FROM\n", + " demo_dataset.medical_transcript\n", + " ),\n", + " STRUCT(\n", + " 0 AS temperature,\n", + " 0.001 AS top_p,\n", + " 1 AS top_k,\n", + " 128 AS max_output_tokens,\n", + " TRUE AS flatten_json_output))\n", + ");\n", + "\n", + "\n", + "SELECT * FROM demo_dataset.medical_transcript_analysis;" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "voR8CEbx4IHv" + }, + "source": [ + "### Analyze structured outputs for insights\n", + "\n", + "We can now perform all sorts of analytics on this data. For example, let us answer `What are the most common diseases in females ages 30+ in our sample?`. Using a simple SQL query like below we see that 'Hypertension', 'Arthritis' and 'Hyperlipidemia' are most common." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "8UU1N1z85i_q" + }, + "outputs": [], + "source": [ + "%%bigquery --project $PROJECT_ID\n", + "\n", + "WITH\n", + " parsed_data AS (\n", + " SELECT\n", + " JSON_EXTRACT_SCALAR(generated_text, '$.Gender') AS gender,\n", + " CAST(JSON_EXTRACT_SCALAR(generated_text, '$.Age') AS INT64) AS age,\n", + " JSON_EXTRACT_ARRAY(generated_text, '$.Disease') AS diseases,\n", + " FROM\n", + " demo_dataset.medical_transcript_analysis)\n", + "\n", + "SELECT\n", + " disease,\n", + " count(*) AS occurrence\n", + "FROM\n", + " parsed_data, UNNEST(diseases) AS disease\n", + "WHERE\n", + " LOWER(gender) = 'female'\n", + " AND age >= 30\n", + "GROUP BY\n", + " disease\n", + "ORDER BY\n", + " occurrence DESC\n", + "LIMIT 3;" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "2a4e033321ad" + }, + "source": [ + "## Cleaning up\n", + "\n", + "To clean up all Google Cloud resources used in this project, you can [delete the Google Cloud project](https://cloud.google.com/resource-manager/docs/creating-managing-projects#shutting_down_projects) you used for the tutorial.\n", + "\n", + "Otherwise, you can delete the individual resources you created in this tutorial on both Vertex AI and BigQuery side:" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "eQ3LpU9763Lv" + }, + "source": [ + "### Delete the Vertex AI model and endpoint\n", + "Delete the experiment models and endpoints to recycle the resources and avoid unnecessary continuous charges that may incur." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "_5iltjKv68NJ" + }, + "outputs": [], + "source": [ + "# Undeploy model and delete endpoint.\n", + "for endpoint in endpoints.values():\n", + " endpoint.delete(force=True)\n", + "\n", + "# Delete models.\n", + "for model in models.values():\n", + " model.delete()\n", + "\n", + "# fmt: off\n", + "delete_bucket = True # @param {type:\"boolean\"}\n", + "# fmt: on\n", + "if delete_bucket:\n", + " ! gsutil -m rm -r $BUCKET_NAME" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "gwlpFUem7S4M" + }, + "source": [ + "### Delete BigQuery dataset and connnection" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "d7beBMcw7U_J" + }, + "outputs": [], + "source": [ + "!gcloud projects remove-iam-policy-binding $PROJECT_ID --member=serviceAccount:$SERVICE_ACCOUNT --role=\"roles/aiplatform.user\n", + "!gcloud projects remove-iam-policy-binding $PROJECT_ID --member=serviceAccount:$SERVICE_ACCOUNT --role=\"roles/storage.admin\n", + "!bq rm -r -f $PROJECT_ID:demo_dataset\n", + "!bq rm --connection --project_id=$PROJECT_ID --location=us demo_conn" + ] + } + ], + "metadata": { + "colab": { + "name": "bigquery_ml_llama_inference.ipynb", + "toc_visible": true + }, + "kernelspec": { + "display_name": "Python 3", + "name": "python3" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} diff --git a/open-models/use-cases/cloud_run_ollama_gemma2_rag_qa.ipynb b/open-models/use-cases/cloud_run_ollama_gemma2_rag_qa.ipynb new file mode 100644 index 0000000..062ac6f --- /dev/null +++ b/open-models/use-cases/cloud_run_ollama_gemma2_rag_qa.ipynb @@ -0,0 +1,1166 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "ur8xi4C7S06n" + }, + "outputs": [], + "source": [ + "# Copyright 2024 Google LLC\n", + "#\n", + "# Licensed under the Apache License, Version 2.0 (the \"License\");\n", + "# you may not use this file except in compliance with the License.\n", + "# You may obtain a copy of the License at\n", + "#\n", + "# https://www.apache.org/licenses/LICENSE-2.0\n", + "#\n", + "# Unless required by applicable law or agreed to in writing, software\n", + "# distributed under the License is distributed on an \"AS IS\" BASIS,\n", + "# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n", + "# See the License for the specific language governing permissions and\n", + "# limitations under the License." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "JAPoU8Sm5E6e" + }, + "source": [ + "# Cloud Run GPU Inference: Gemma 2 RAG Q&A with Ollama and LangChain\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + "
    \n", + " \n", + " \"Google
    Open in Colab\n", + "
    \n", + "
    \n", + " \n", + " \"Google
    Open in Colab Enterprise\n", + "
    \n", + "
    \n", + " \n", + " \"Vertex
    Open in Workbench\n", + "
    \n", + "
    \n", + " \n", + " \"GitHub
    View on GitHub\n", + "
    \n", + "
    \n", + "\n", + "
    \n", + "\n", + "Share to:\n", + "\n", + "\n", + " \"LinkedIn\n", + "\n", + "\n", + "\n", + " \"Bluesky\n", + "\n", + "\n", + "\n", + " \"X\n", + "\n", + "\n", + "\n", + " \"Reddit\n", + "\n", + "\n", + "\n", + " \"Facebook\n", + " " + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "84f0f73a0f76" + }, + "source": [ + "| | |\n", + "|-|-|\n", + "| Author(s) | [Elia Secchi](https://github.com/eliasecchig/) |" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "tvgnzT1CKxrO" + }, + "source": [ + "## Overview\n", + "\n", + "\n", + "\n", + "> **[Cloud Run](https://cloud.google.com/run)**:\n", + "It's a serverless platform by Google Cloud for running containerized applications. It automatically scales and manages infrastructure, supporting various programming languages. Cloud Run now offers GPU acceleration for AI/ML workloads.\n", + "\n", + "> **Note:** GPU support in Cloud Run is a guarded feature. Before running this notebook, make sure your Google Cloud project is enabled. You can do that by visiting this page [g.co/cloudrun/gpu](https://g.co/cloudrun/gpu).\n", + "\n", + "\n", + "> **[Ollama](ollama.com)**: is an open-source tool for easily running and deploying large language models locally. It offers simple management and usage of LLMs on personal computers or servers.\n", + "\n", + "This notebook showcase how to deploy [Google Gemma 2](https://blog.google/technology/developers/google-gemma-2/) in Cloud Run, with the objective to build a simple RAG Q&A application.\n", + "\n", + "By the end of this notebook, you will learn how to:\n", + "\n", + "1. Deploy Google Gemma 2 on Cloud Run using Ollama\n", + "2. Implement a Retrieval-Augmented Generation (RAG) application with Gemma 2 and Ollama\n", + "3. Build a custom container with Ollama to deploy any Large Language Model (LLM) of your choice\n", + "\n", + "\n", + "\n", + "### Required roles\n", + "\n", + "To get the permissions that you need to complete the tutorial, ask your administrator to grant you the following IAM roles on your project:\n", + "\n", + "1. Artifact Registry Administrator (`roles/artifactregistry.admin`)\n", + "2. Cloud Build Editor (`roles/cloudbuild.builds.editor`)\n", + "3. Cloud Run Admin (`roles/run.developer`)\n", + "4. Service Account User (`roles/iam.serviceAccountUser`)\n", + "5. Service Usage Consumer (`roles/serviceusage.serviceUsageConsumer`)\n", + "6. Storage Admin (`roles/storage.admin`)\n", + "\n", + "\n", + "\n", + "For more information about granting roles, see [Manage access](https://cloud.google.com/iam/docs/granting-changing-revoking-access)." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "FYbo7iEPluZQ" + }, + "source": [ + "![cloud_run_gemma_ollama.png](https://storage.googleapis.com/github-repo/generative-ai/open-models/serving/cloud_run_gemma_ollama.png)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "61RBz8LLbxCR" + }, + "source": [ + "## Get started" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "No17Cw5hgx12" + }, + "source": [ + "### Install Vertex AI SDK and other required packages" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "tFy3H3aPgx12" + }, + "outputs": [], + "source": [ + "%pip install --upgrade --user --quiet google-cloud-aiplatform langchain-community langchainhub langchain_google_vertexai" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "R5Xep4W9lq-Z" + }, + "source": [ + "### Restart runtime\n", + "\n", + "To use the newly installed packages in this Jupyter runtime, you must restart the runtime. You can do this by running the cell below, which restarts the current kernel.\n", + "\n", + "The restart might take a minute or longer. After it's restarted, continue to the next step." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "XRvKdaPDTznN" + }, + "outputs": [], + "source": [ + "import IPython\n", + "\n", + "app = IPython.Application.instance()\n", + "app.kernel.do_shutdown(True)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "SbmM4z7FOBpM" + }, + "source": [ + "
    \n", + "⚠ The kernel is going to restart. Wait until it's finished before continuing to the next step. ⚠\n", + "
    " + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "dmWOrTJ3gx13" + }, + "source": [ + "### Authenticate your notebook environment (Colab only)\n", + "\n", + "If you're running this notebook on Google Colab, run the cell below to authenticate your environment." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "NyKGtVQjgx13" + }, + "outputs": [], + "source": [ + "!gcloud auth login --update-adc --quiet" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "DF4l8DTdWgPY" + }, + "source": [ + "### Set Google Cloud project information and initialize Vertex AI SDK\n", + "\n", + "To get started using Vertex AI, you must have an existing Google Cloud project and [enable the Vertex AI API](https://console.cloud.google.com/flows/enableapi?apiid=aiplatform.googleapis.com).\n", + "\n", + "Learn more about [setting up a project and a development environment](https://cloud.google.com/vertex-ai/docs/start/cloud-environment)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "Nqwi-5ufWp_B" + }, + "outputs": [], + "source": [ + "# Use the environment variable if the user doesn't provide Project ID.\n", + "import os\n", + "\n", + "import vertexai\n", + "\n", + "PROJECT_ID = \"[your-project-id]\" # @param {type:\"string\", isTemplate: true}\n", + "if PROJECT_ID == \"[your-project-id]\":\n", + " PROJECT_ID = str(os.environ.get(\"GOOGLE_CLOUD_PROJECT\"))\n", + "\n", + "LOCATION = os.environ.get(\"GOOGLE_CLOUD_REGION\", \"us-central1\")\n", + "\n", + "vertexai.init(project=PROJECT_ID, location=LOCATION)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "8pB4NiQAMzgt" + }, + "source": [ + "### Fetch your Google Cloud project number" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "Y54slycDMjHK" + }, + "outputs": [], + "source": [ + "PROJECT_NUMBER = get_ipython().getoutput('gcloud projects describe $PROJECT_ID --format=\"value(projectNumber)\"')[0]" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "EdvJRUWRNGHE" + }, + "source": [ + "## Deploy Ollama with Cloud Run" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "5J5rY6YhxTRl" + }, + "source": [ + "## Build your container\n", + "\n", + "For deploying Gemma 2 in Cloud Run, create a container that packages the Ollama server and the Gemma 2 model.\n", + "\n", + "To build the container, you can use [Cloud Build](https://cloud.google.com/build), a serverless CI/CD platform which allows developers to easily build software.\n", + "\n", + "> For optimal startup time and improved scalability, it's recommended to store model weights for Gemma 2 (9B) and similarly sized models directly in the container image.\n", + "However, consider the storage requirements of larger models as they might be impractical to store in the container image. Refer to [Best practices: AI inference on Cloud Run with GPUs](https://cloud.google.com/run/docs/configuring/services/gpu-best-practices#loading-storing-models-tradeoff) for an overview of the trade-offs." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "IprOEAAN1sBQ" + }, + "source": [ + "### Create Artifact Registry repository\n", + "\n", + "To build a container you will need to first create a repository in Google Cloud Artifact Registry:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "p5hXDtoYsCEB" + }, + "outputs": [], + "source": [ + "AR_REPOSITORY_NAME = \"cr-gpu-repo\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "z1ZBM9PDrbdM" + }, + "outputs": [], + "source": [ + "!gcloud artifacts repositories create $AR_REPOSITORY_NAME \\\n", + " --repository-format=docker \\\n", + " --location=$LOCATION \\\n", + " --project=$PROJECT_ID" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "IDMpuXEu2thu" + }, + "source": [ + "### Create a Dockerfile\n", + "\n", + "You will then need to create a Dockerfile which defines the build steps of the container.\n", + "\n", + "You can customize the model used by modifying the `MODEL_NAME` variable. \n", + "Explore the [Ollama library](https://ollama.com/library) for a comprehensive list of available models." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "IcPKeFPNQZzI" + }, + "outputs": [], + "source": [ + "MODEL_NAME = \"gemma2:9b\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "Vi9T53CScWdn" + }, + "outputs": [], + "source": [ + "dockerfile_content = f\"\"\"\n", + "FROM ollama/ollama\n", + "\n", + "# Set the host and port to listen on\n", + "ENV OLLAMA_HOST 0.0.0.0:8080\n", + "\n", + "# Set the directory to store model weight files\n", + "ENV OLLAMA_MODELS /models\n", + "\n", + "# Reduce the verbosity of the logs\n", + "ENV OLLAMA_DEBUG false\n", + "\n", + "# Do not unload model weights from the GPU\n", + "ENV OLLAMA_KEEP_ALIVE -1\n", + "\n", + "# Choose the model to load. Ollama defaults to 4-bit quantized weights\n", + "ENV MODEL {MODEL_NAME}\n", + "\n", + "# Start the ollama server and download the model weights\n", + "RUN ollama serve & sleep 5 && ollama pull $MODEL\n", + "\n", + "# At startup time we start the server and run a dummy request\n", + "# to request the model to be loaded in the GPU memory\n", + "ENTRYPOINT [\"/bin/sh\"]\n", + "CMD [\"-c\", \"ollama serve & (ollama run $MODEL 'Say one word' &) && wait\"]\n", + "\"\"\"\n", + "\n", + "# Write the Dockerfile\n", + "with open(\"Dockerfile\", \"w\") as f:\n", + " f.write(dockerfile_content)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "5RnaYx2p235W" + }, + "source": [ + "### Trigger Cloud Build\n", + "\n", + "You are now ready to trigger the container build process!\n", + "We will use the `gcloud builds submit` command, using a `e2-highcpu-32` machine to optimize build time. We use e2-highcpu-32 machines because multiple cores allow for parallel downloads, significantly speeding up the build process.\n", + "\n", + "Cloud Build pricing is based on build minutes consumed. See [the pricing page](https://cloud.google.com/build/pricing) for details\n", + "\n", + "The operation will take ~10 minutes for completion." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "k2aooaREsT-F" + }, + "outputs": [], + "source": [ + "CONTAINER_URI = (\n", + " f\"{LOCATION}-docker.pkg.dev/{PROJECT_ID}/{AR_REPOSITORY_NAME}/ollama-gemma-2\"\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "CU8n7kk5OeP8" + }, + "outputs": [], + "source": [ + "!gcloud builds submit --tag $CONTAINER_URI --project $PROJECT_ID --machine-type e2-highcpu-32" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "xd_Zfz9c3cZy" + }, + "source": [ + "You can now use the container you just built to deploy a new Cloud Run service!" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "3YGGFLB-JElj" + }, + "source": [ + "### Deploy container in Cloud Run\n", + "\n", + "You are now ready for deployment! Cloud Run offers multiple deployment methods, including Console, gcloud CLI, Cloud Code, Terraform, YAML, and Client Libraries. Explore all the options in the [official documentation](https://cloud.google.com/run/docs/deploying#service).\n", + "\n", + "For quick prototyping, you can start with the gcloud CLI `gcloud run deploy` command. This convenient command-line tool provides a straightforward way to get your container running on Cloud Run. Learn more about its features and usage in the [gcloud CLI reference](https://cloud.google.com/sdk/gcloud/reference/run/deploy)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "8e6kybbhp3Na" + }, + "outputs": [], + "source": [ + "SERVICE_NAME = \"ollama-gemma-2\" # @param {type:\"string\"}" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "kDkLl8AFKKD0" + }, + "outputs": [], + "source": [ + "!gcloud beta run deploy $SERVICE_NAME \\\n", + " --project $PROJECT_ID \\\n", + " --region $LOCATION \\\n", + " --image $CONTAINER_URI \\\n", + " --concurrency 4 \\\n", + " --cpu 8 \\\n", + " --gpu 1 \\\n", + " --gpu-type nvidia-l4 \\\n", + " --max-instances 7 \\\n", + " --memory 32Gi \\\n", + " --no-allow-unauthenticated \\\n", + " --no-cpu-throttling \\\n", + " --timeout=600" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "e1afbaee64a4" + }, + "source": [ + "*Expect a slower initial deployment as the container image is being pulled for the first time.*" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "8IRTamcobASG" + }, + "source": [ + "### Setting concurrency for optimal performance\n", + "\n", + "In Cloud Run, [concurrency](https://cloud.google.com/run/docs/about-concurrency) defines the maximum number of requests that can be processed simultaneously by a given instance.\n", + "\n", + "For this sample we set a `concurrency` value equal to 4.\n", + "\n", + "As part of your use case you might need to experiment with different concurrency settings to find the best latency vs throughput tradeoff.\n", + "\n", + "Refer to the following documentation pages to know more about performance optimizations:\n", + "- [Setting concurrency for optimal performance in Cloud Run](https://cloud.google.com/run/docs/tutorials/gpu-gemma2-with-ollama#set-concurrency-for-performance)\n", + "- [GPU performance best practices](https://cloud.google.com/run/docs/configuring/services/gpu-best-practices)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "XSrXJkabGdjw" + }, + "source": [ + "## Invoking Gemma 2 in Cloud Run\n", + "\n", + "We are now ready to send some requests to Gemma!" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "Vrx30A8jKwrY" + }, + "source": [ + "### Fetch identity token\n", + "\n", + "Once deployed to Cloud Run, to invoke Gemma 2, we will need to fetch an Identity token to perform authentication. See the relative documentation to discover more about [authentication in Cloud Run](https://cloud.google.com/run/docs/authenticating/overview).\n", + "\n", + "In the appendix of this sample, you'll find a helper function that supports the automatic refresh of the [Identity Token](https://cloud.google.com/docs/authentication/token-types#id), which expires every hour by default." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "qSa5aZCPuLlU" + }, + "outputs": [], + "source": [ + "ID_TOKEN = get_ipython().getoutput('gcloud auth print-identity-token -q')[0]" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "UmA-haVjOA6U" + }, + "source": [ + "### Setup the Service URL" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "LOVfy893tvcl" + }, + "outputs": [], + "source": [ + "SERVICE_URL = f\"https://{SERVICE_NAME}-{PROJECT_NUMBER}.{LOCATION}.run.app\" # type: ignore" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "XbOtGicVLNgD" + }, + "source": [ + "## Invoking Gemma\n", + "\n", + "You are ready to test the model you just deployed! The [Ollama API docs](https://github.com/ollama/ollama/blob/main/docs/api.md) are a great resource to learn more about the different endpoints and how to interact with your model." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "iMI0nlXVT20t" + }, + "source": [ + "#### Invoke through CURL request\n", + "You can invoke Gemma and Cloud Run in many ways. For example, you can send an HTTP CURL request to Cloud Run:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "4b1c47642f7e" + }, + "outputs": [], + "source": [ + "ENDPOINT_URL = f\"{SERVICE_URL}/api/generate\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "NsixJcaBP2q4" + }, + "outputs": [], + "source": [ + "%%bash -s \"$ENDPOINT_URL\" \"$ID_TOKEN\" \"$MODEL_NAME\" \n", + "ENDPOINT_URL=$1\n", + "ID_TOKEN=$2\n", + "MODEL_NAME=$3\n", + "\n", + "curl -s -X POST \"${ENDPOINT_URL}\" \\\n", + "-H \"Authorization: Bearer ${ID_TOKEN}\" \\\n", + "-H \"Content-Type: application/json\" \\\n", + "-d '{ \"model\": \"'${MODEL_NAME}'\", \"prompt\": \"Hi\", \"max_tokens\": 100, \"stream\": false}'" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "41657205b738" + }, + "source": [ + "#### Invoke with a Python POST Request\n", + "\n", + "You can also invoke the model using a POST request with Python's popular `requests` library. [Learn more about the `requests` library here.](https://requests.readthedocs.io/en/latest/) " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "2e8c87dfd38b" + }, + "outputs": [], + "source": [ + "import requests\n", + "\n", + "headers = {\"Authorization\": f\"Bearer {ID_TOKEN}\", \"Content-Type\": \"application/json\"} # type: ignore\n", + "\n", + "data = {\n", + " \"model\": MODEL_NAME,\n", + " \"prompt\": \"Hi, I am using python!\",\n", + " \"max_tokens\": 100,\n", + " \"stream\": False,\n", + "}\n", + "\n", + "response = requests.post(ENDPOINT_URL, headers=headers, json=data)\n", + "\n", + "print(response.text)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "bFoe0NVOT6DD" + }, + "source": [ + "#### Invoke Ollama with Python integrations\n", + "\n", + "Popular Generative AI orchestration frameworks like [LangChain](https://www.langchain.com) and [LlamaIndex](https://www.llamaindex.ai/) offer direct integration with Ollama:\n", + "- [LangChain integration](https://python.langchain.com/v0.2/docs/integrations/llms/ollama/)\n", + "- [LlamaIndex integration](https://docs.llamaindex.ai/en/stable/api_reference/llms/ollama/)\n", + "\n", + "As part of this sample, we will be using the LangChain integration to perform different calls and build a sample RAG chain." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "vZyZqnnNaeWw" + }, + "source": [ + "### Import libraries" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "gQDWB66Vadlx" + }, + "outputs": [], + "source": [ + "import google.auth\n", + "from langchain.schema import BaseMessage, Document\n", + "from langchain_community.chat_models import ChatOllama\n", + "from langchain_community.document_loaders import WebBaseLoader\n", + "from langchain_community.vectorstores import SKLearnVectorStore\n", + "from langchain_core.output_parsers import StrOutputParser\n", + "from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder\n", + "from langchain_core.runnables import RunnablePassthrough\n", + "from langchain_google_vertexai import VertexAIEmbeddings\n", + "from langchain_text_splitters import CharacterTextSplitter" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "_hnaKrZftjbT" + }, + "outputs": [], + "source": [ + "llm = ChatOllama(\n", + " model=MODEL_NAME,\n", + " base_url=SERVICE_URL,\n", + " num_predict=300,\n", + " headers={\"Authorization\": f\"Bearer {ID_TOKEN}\"}, # type: ignore\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "9GYGr76T7aYF" + }, + "outputs": [], + "source": [ + "# You can perform a synchronous invocation through the `.invoke` method\n", + "\n", + "llm.invoke(\"Hi!\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "yVqxaDylWjck" + }, + "source": [ + "Or invoke through the generation of a stream through the `.stream` **method**" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "2twtoa6a4_ui" + }, + "outputs": [], + "source": [ + "# You can also generate a stream through the `.stream` method\n", + "\n", + "for m in llm.stream(\"Hi!\"):\n", + " print(m)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "bqr_QPso7shY" + }, + "source": [ + "## RAG Q&A Chain with Gemma 2 and Cloud Run\n", + "\n", + "We can leverage the LangChain integration to create a sample RAG application with Gemma, Cloud Run, [Vertex AI Embedding](https://cloud.google.com/vertex-ai/generative-ai/docs/embeddings/get-text-embeddings) for generating embeddings and [FAISS vector store](https://python.langchain.com/v0.2/docs/integrations/vectorstores/faiss/) for document retrieval.\n", + "\n", + "Through RAG, we will ask Gemma 2 to answer questions about the [Cloud Run documentation page](https://cloud.google.com/run/docs/overview/what-is-cloud-run)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "wHq8zpG5a4u9" + }, + "source": [ + "### Setup embedding model and retriever\n", + "\n", + "We are ready to setup our embedding model and retriever." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "tI66kos7a8B4" + }, + "outputs": [], + "source": [ + "credentials, _ = google.auth.default(quota_project_id=PROJECT_ID)\n", + "embeddings = VertexAIEmbeddings(\n", + " project=PROJECT_ID, model_name=\"text-embedding-005\", credentials=credentials\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "xxCu8MST6oWK" + }, + "outputs": [], + "source": [ + "loader = WebBaseLoader(\"https://cloud.google.com/run/docs/overview/what-is-cloud-run\")\n", + "docs = loader.load()\n", + "documents = CharacterTextSplitter(chunk_size=800, chunk_overlap=100).split_documents(\n", + " docs\n", + ")\n", + "\n", + "vector = SKLearnVectorStore.from_documents(documents, embeddings)\n", + "retriever = vector.as_retriever()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "rCH9n2UEzwMv" + }, + "source": [ + "### RAG Chain Definition\n", + "\n", + "We will define now our RAG Chain.\n", + "\n", + "The RAG chain works as follows:\n", + "\n", + "1. The user's query and conversation history are passed to the `query_rewrite_chain` to generate a rewritten query optimized for semantic search.\n", + "2. The rewritten query is used by the `retriever` to fetch relevant documents.\n", + "3. The retrieved documents are formatted into a single string.\n", + "4. The formatted documents, along with the original user messages, are passed to the LLM with instructions to generate an answer based on the provided context.\n", + "5. The LLM's response is parsed and returned as the final answer." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "x1nPO00s4r4o" + }, + "outputs": [], + "source": [ + "answer_generation_template = ChatPromptTemplate.from_messages(\n", + " [\n", + " (\n", + " \"system\",\n", + " \"You are an assistant for question answering-tasks. \"\n", + " \"Use the following pieces of retrieved context to answer the question. If you don't know the answer, just say that you don't know. \"\n", + " \"{context}\",\n", + " ),\n", + " MessagesPlaceholder(variable_name=\"messages\"),\n", + " ]\n", + ")\n", + "query_rewrite_template = ChatPromptTemplate.from_messages(\n", + " [\n", + " (\n", + " \"system\",\n", + " \"Rewrite a query to a semantic search engine using the current conversation. \"\n", + " \"Provide only the rewritten query as output.\",\n", + " ),\n", + " MessagesPlaceholder(variable_name=\"messages\"),\n", + " ]\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "jm309i0r0Lwv" + }, + "outputs": [], + "source": [ + "query_rewrite_chain = query_rewrite_template | llm\n", + "\n", + "\n", + "def extract_query(messages: list[BaseMessage]) -> str:\n", + " return query_rewrite_chain.invoke(messages).content\n", + "\n", + "\n", + "def format_docs(docs: list[Document]) -> str:\n", + " return \"\\n\\n\".join(doc.page_content for doc in docs)\n", + "\n", + "\n", + "rag_chain = (\n", + " {\n", + " \"context\": extract_query | retriever | format_docs,\n", + " \"messages\": RunnablePassthrough(),\n", + " }\n", + " | answer_generation_template\n", + " | llm\n", + " | StrOutputParser()\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "rRFtc7eo0Tn7" + }, + "source": [ + "### Testing the RAG Chain" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "MIfd_Wwa7RUe" + }, + "outputs": [], + "source": [ + "rag_chain.invoke([(\"human\", \"What features does Cloud Run offer?\")])" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "f0994074042d" + }, + "source": [ + "Now, let's use a specific question from the documentation to explore how RAG addresses potential gaps in the model's knowledge." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "ec5895d100f5" + }, + "outputs": [], + "source": [ + "QUESTION = \"List all the different Cloud Run integrations\"" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "e0abfae19b32" + }, + "source": [ + "First, we'll ask the LLM directly:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "4fb7c3e2426e" + }, + "outputs": [], + "source": [ + "print(llm.invoke(QUESTION).content)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "5575573f8174" + }, + "source": [ + "Then, we'll ask the same question using the RAG chain:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "AszY1ke5bWC9" + }, + "outputs": [], + "source": [ + "print(rag_chain.invoke([(\"human\", QUESTION)]))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "c98450b91f72" + }, + "source": [ + "We can notice how RAG chain provides a more accurate and comprehensive answer than the LLM by leveraging the [source documentation](https://cloud.google.com/run/docs/overview/what-is-cloud-run). " + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "88c5f72f1981" + }, + "source": [ + "## Conclusion\n", + "Congratulations. Now you know how to deploy an open model to Cloud Run powered by a GPU! Specifically, you deployed a Gemma 2 model to Cloud Run with a GPU, as part of a RAG application powered by LangChain. You were able to ask answers from Gemma 2 about a documentation page.\n", + "\n", + "For more information about your identity tokens expiring and how to refresh your tokens, see the next section below \"Appendix: Handling Identity Token Expiration\".\n", + "\n", + "To clean up the resources you created in this section, see the section at the bottom \"Cleaning up\"." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "A5VPH4gP3igf" + }, + "source": [ + "## Appendix: Handling Identity Token Expiration\n", + "\n", + "When deploying a Generative AI application Google Cloud Run, you'll often need to authenticate your requests using Identity Tokens.\n", + "\n", + "These tokens will expire hourly, requiring a mechanism for automatic refresh to ensure uninterrupted operation.\n", + "\n", + "The following helper classes provide an example of how to deal with token refresh. \n", + "It leverages:\n", + "1. The `google.auth` library to handle the authentication process and automatically refresh the token when necessary\n", + "2. ChatOllama's [auth parameter](https://api.python.langchain.com/en/latest/chat_models/langchain_community.chat_models.ollama.ChatOllama.html#langchain_community.chat_models.ollama.ChatOllama.auth) for passing an authentication callable\n", + "\n", + "\n", + "See the following resources for more information on authentication:\n", + "* [Identity Token Overview](https://cloud.google.com/docs/authentication/token-types#id)\n", + "* [Google Cloud Run Authentication Documentation](https://cloud.google.com/run/docs/authenticating/overview)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "e76fe84f0d63" + }, + "outputs": [], + "source": [ + "import time\n", + "\n", + "import google.auth\n", + "from google.auth.credentials import Credentials\n", + "from google.auth.exceptions import DefaultCredentialsError\n", + "import google.auth.transport.requests\n", + "import google.oauth2.id_token\n", + "from requests.auth import AuthBase\n", + "from requests.models import PreparedRequest\n", + "\n", + "\n", + "class GoogleCloudAuth(AuthBase):\n", + " def __init__(self, url: str, token_lifetime: int = 3600):\n", + " self.url: str = url\n", + " self.token: str | None = None\n", + " self.expiry_time: float = 0\n", + " self.token_lifetime: int = token_lifetime\n", + " self.creds: Credentials\n", + " self.creds, _ = google.auth.default()\n", + "\n", + " def __call__(self, r: PreparedRequest) -> PreparedRequest:\n", + " r.headers[\"Authorization\"] = f\"Bearer {self.get_token()}\"\n", + " return r\n", + "\n", + " def get_token(self) -> str | None:\n", + " if time.time() >= self.expiry_time:\n", + " self.refresh_token()\n", + " return self.token\n", + "\n", + " def refresh_token(self) -> None:\n", + " \"\"\"\n", + " Retrieves an ID token, attempting to use default credentials first,\n", + " and falling back to fetching a service-to-service new token if necessary.\n", + " See more on Cloud Run authentication at this link:\n", + " https://cloud.google.com/run/docs/authenticating/service-to-service\n", + " Args:\n", + " url: The URL to use for the token request.\n", + " \"\"\"\n", + " auth_req = google.auth.transport.requests.Request()\n", + " try:\n", + " self.token = google.oauth2.id_token.fetch_id_token(auth_req, self.url)\n", + " except DefaultCredentialsError:\n", + " self.creds.refresh(auth_req)\n", + " self.token = self.creds.id_token\n", + " self.expiry_time = time.time() + self.token_lifetime" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "d96d74e2bf40" + }, + "outputs": [], + "source": [ + "llm = ChatOllama(\n", + " auth=GoogleCloudAuth(url=SERVICE_URL),\n", + " model=MODEL_NAME,\n", + " base_url=SERVICE_URL,\n", + " num_predict=300,\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "d0aff0566961" + }, + "outputs": [], + "source": [ + "llm.invoke(\"Hi, testing a request\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "LjgBf0M4Mokn" + }, + "source": [ + "You can now use the `invoke` function as usual, with the token being refreshed automatically every hour." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "270LFAC0P3i4" + }, + "source": [ + "## Cleaning up\n", + "To clean up all Google Cloud resources, you can run the following cell to delete the Cloud Run service you created." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "XJRe7I7KP2HM" + }, + "outputs": [], + "source": [ + "# Delete the Cloud Run service deployed above\n", + "\n", + "!gcloud run services delete $SERVICE_NAME --project $PROJECT_ID --region $LOCATION --quiet" + ] + } + ], + "metadata": { + "colab": { + "name": "cloud_run_ollama_gemma2_rag_qa.ipynb", + "toc_visible": true + }, + "kernelspec": { + "display_name": "Python 3", + "name": "python3" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} diff --git a/open-models/use-cases/deploy_llama3_vllm_gke_cloud_function.ipynb b/open-models/use-cases/deploy_llama3_vllm_gke_cloud_function.ipynb new file mode 100644 index 0000000..d2d73a7 --- /dev/null +++ b/open-models/use-cases/deploy_llama3_vllm_gke_cloud_function.ipynb @@ -0,0 +1,1116 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "id": "7d9bbf86da5e" + }, + "outputs": [], + "source": [ + "# Copyright 2025 Google LLC\n", + "#\n", + "# Licensed under the Apache License, Version 2.0 (the \"License\");\n", + "# you may not use this file except in compliance with the License.\n", + "# You may obtain a copy of the License at\n", + "#\n", + "# https://www.apache.org/licenses/LICENSE-2.0\n", + "#\n", + "# Unless required by applicable law or agreed to in writing, software\n", + "# distributed under the License is distributed on an \"AS IS\" BASIS,\n", + "# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n", + "# See the License for the specific language governing permissions and\n", + "# limitations under the License." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "99c1c3fc2ca5" + }, + "source": [ + "# Deploying Llama 3 on Google Kubernetes Engine with Cloud Functions and vLLM" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "uXqCSRBUX_6Q" + }, + "source": [ + "\n", + " \n", + " \n", + " \n", + "
    \n", + " \n", + " \"Google
    Open in Colab\n", + "
    \n", + "
    \n", + " \n", + " \"Google
    Open in Colab Enterprise\n", + "
    \n", + "
    \n", + " \n", + " \"GitHub
    View on GitHub\n", + "
    \n", + "
    \n", + "\n", + "
    \n", + "\n", + "Share to:\n", + "\n", + "\n", + " \"LinkedIn\n", + "\n", + "\n", + "\n", + " \"Bluesky\n", + "\n", + "\n", + "\n", + " \"X\n", + "\n", + "\n", + "\n", + " \"Reddit\n", + "\n", + "\n", + "\n", + " \"Facebook\n", + "" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "QPhuLTRXZAy_" + }, + "source": [ + "| Author(s) |\n", + "| --- |\n", + "| [KC Ayyagari](https://github.com/krishchyt) |" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "3de7470326a2" + }, + "source": [ + "This notebook demonstrates how to deploy and serve Llama 3 models on Google Kubernetes Engine (GKE) using GPUs, and how to integrate this deployment with a Cloud Function to create an accessible API endpoint. This uses Virtual Large Language Model [vLLM](https://developers.googleblog.com/en/inference-with-gemma-using-dataflow-and-vllm/#:~:text=model%20frameworks%20simple.-,What%20is%20vLLM%3F,-vLLM%20is%20an) inference server.\n", + "\n", + "## Objective\n", + "\n", + "* Deploy and run inference for serving Llama 3 on GPUs.\n", + "* Create a Cloud Function.\n", + "* Call Llama from the Cloud Function.\n", + "\n", + "![Workflow diagram](https://github.com/krishchyt/kc-demoassets/blob/main/gke-cf-workflow.png?raw=true)\n", + "\n", + "## Key Steps\n", + "\n", + "### Setup\n", + "\n", + "* Authenticates with Google Cloud.\n", + "* Sets up the Google Cloud project, region, network, and subnet configurations.\n", + "* Enables the necessary Google Cloud services (Container Registry, VPC Access).\n", + "* Installs `kubectl`.\n", + "\n", + "### GKE Cluster Creation\n", + "\n", + "* Creates an Autopilot GKE cluster with specified configurations (region, network, private nodes, etc.).\n", + "\n", + "### Llama 3 Deployment on GKE\n", + "\n", + "* Defines Kubernetes deployment and service configurations for deploying a selected Llama 3 model on GKE using vLLM.\n", + "* Allows the user to select from a range of Llama 3 models (Llama-3-2-1B-Instruct, Llama-3-2-11B-Vision, etc.).\n", + "* Creates a Kubernetes YAML file based on the selected model and applies it to the cluster.\n", + "* Waits for the container to be created and the server to be up and running.\n", + "\n", + "### Endpoint Testing\n", + "\n", + "* Retrieves the Internal Load Balancer IP address for the Llama service.\n", + "* Sends a test request to the Llama 3 endpoint and prints the response.\n", + "\n", + "### Cloud Function Integration\n", + "\n", + "* Creates a service account with the necessary permissions.\n", + "* Prepares the code for a Cloud Function that will act as an API endpoint for the Llama 3 model.\n", + "* Creates a VPC connector to allow the Cloud Function to access the GKE cluster.\n", + "* Deploys the Cloud Function, configuring environment variables (including the Llama endpoint IP) and other settings.\n", + "\n", + "### Cleanup\n", + "\n", + "* Provides options to delete the deployment, cluster, Cloud Function, and VPC connector to avoid unnecessary charges.\n", + "\n", + "## Use Cases\n", + "\n", + "This setup enables various use cases, including:\n", + "\n", + "* **Chatbots/Conversational AI:** Building a chatbot that can answer user questions, provide information, or engage in conversations. The Cloud Function acts as the API endpoint, receiving user input and passing it to the Llama 3 model running on GKE for generating responses.\n", + "* **Content Generation:** Generating different kinds of creative text formats, like poems, code, scripts, musical pieces, email, letters, etc. The Cloud Function receives a prompt and parameters (e.g., length, style) and uses Llama 3 to generate the content.\n", + "* **Question Answering:** Providing answers to specific questions based on a knowledge base. The Cloud Function receives the question, and Llama 3 extracts the relevant information and formulates an answer.\n", + "* **Code Generation/Completion:** Assisting developers by generating code snippets or completing code based on context. The Cloud Function receives the code context and uses Llama 3 to suggest code completions or generate entire functions.\n", + "* **Image Captioning and Visual Question Answering (if using a vision-enabled model):** If you're using a Llama 3 model with vision capabilities, you can use this setup for image captioning (generating descriptions of images) or visual question answering (answering questions about the content of an image).\n", + "* **Data Analysis and Insights:** Use Llama 3 to analyze text data and extract insights. The Cloud Function can receive text data, pass it to Llama 3 for analysis, and then return the insights.\n", + "* **Workflow Automation:** Integrate Llama 3 into automated workflows to perform tasks such as summarizing documents, extracting key information, or translating text.\n", + "\n", + "\n", + "## Benefits\n", + "\n", + "* **Scalability:** GKE allows you to scale the Llama 3 deployment based on demand.\n", + "* **Cost-Effectiveness:** Cloud Functions are serverless and only charged when used.\n", + "* **Flexibility:** You can easily update the Llama 3 model or the Cloud Function code.\n", + "\n", + "## GPUs\n", + "\n", + "GPUs let you accelerate specific workloads running on your nodes such as machine learning and data processing. GKE provides a range of machine type options for node configuration, including machine types with NVIDIA H100, L4, and A100 GPUs.\n", + "\n", + "Before you use GPUs in GKE, we recommend that you complete the following learning path:\n", + "\n", + "- Learn about [current GPU version availability](https://cloud.google.com/compute/docs/gpus)\n", + "\n", + "- Learn about [GPUs in GKE](https://cloud.google.com/kubernetes-engine/docs/concepts/gpus)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "Vyf-fGQ3Znts" + }, + "outputs": [], + "source": [ + "%pip install requests>=2.26.0 functions-framework>=3.0.0 google-cloud-logging>=3.0.0" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "PZAiVWhA-efF" + }, + "outputs": [], + "source": [ + "! gcloud auth application-default login" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "qJMLYKTIUG-c" + }, + "source": [ + "## Setup Google Cloud project\n" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": { + "id": "855d6b96f291" + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Updated property [core/project].\n", + "\n", + "All components are up to date.\n" + ] + } + ], + "source": [ + "# @markdown 1. [Make sure that billing is enabled for your project](https://cloud.google.com/billing/docs/how-to/modify-project).\n", + "\n", + "# @markdown 3. **[Optional]** Set `CLUSTER_NAME` if you want to use your own GKE cluster. If not set, this example will create an auto-pilot cluster in the specified project.\n", + "import datetime\n", + "\n", + "# Get the default region and project for launching jobs.\n", + "\n", + "REGION = \"us-central1\" # @param [\"us-central1\", \"us-west1\", \"us-east4\"]\n", + "PROJECT_ID = \"\" # @param {type:\"string\"}\n", + "assert PROJECT_ID\n", + "\n", + "# Enter sure network is in same region\n", + "NETWORK_NAME = \"\" # @param {type:\"string\"}\n", + "\n", + "# Make sure the subnet is in same region\n", + "SUBNET_NAME = \"\" # @param {type:\"string\"}\n", + "NETWORK_URI = f\"projects/{PROJECT_ID}/global/networks/{NETWORK_NAME}\"\n", + "SUBNET_URI = f\"projects/{PROJECT_ID}/regions/{REGION}/subnetworks/{SUBNET_NAME}\"\n", + "\n", + "# Set up gcloud.\n", + "! gcloud config set project \"$PROJECT_ID\"\n", + "! gcloud services enable container.googleapis.com\n", + "\n", + "# Add kubectl to the set of available tools.\n", + "! mkdir -p /tools/google-cloud-sdk/.install\n", + "! gcloud components install kubectl --quiet\n", + "\n", + "# The cluster name to create\n", + "CLUSTER_NAME = \"\" # @param {type:\"string\"}\n", + "\n", + "now = datetime.datetime.now().strftime(\"%Y%m%d%H%M%S\")\n", + "\n", + "CLUSTER_NAME = f\"{CLUSTER_NAME}-{now}\"" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "oOtW4baPURcZ" + }, + "source": [ + "## Set up GKE" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "u4vjRPrOTf2t" + }, + "source": [ + "### Create auto pilot cluster" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "70W0Fa-qTei0" + }, + "outputs": [], + "source": [ + "# create auto-pilot cluster\n", + "!gcloud beta container --project {PROJECT_ID} clusters create-auto {CLUSTER_NAME} --region {REGION} --release-channel \"regular\" --tier \"standard\" --enable-private-nodes --enable-ip-access --no-enable-google-cloud-access --network {NETWORK_URI} --subnetwork {SUBNET_URI} --cluster-ipv4-cidr \"/17\" --binauthz-evaluation-mode=DISABLED" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "3ixcamqUTqu8" + }, + "source": [ + "### Deploy Llama on GKE" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "6psJZY_zUDgj" + }, + "outputs": [], + "source": [ + "# @markdown This section deploys llama 3.2 on GKE.\n", + "\n", + "# @markdown The model deployment takes about 5 to 15 minutes to complete. Larger models may take longer.\n", + "\n", + "# @markdown Select the model to deploy:\n", + "# fmt: off\n", + "MODEL_NAME = \"Llama-3-2-1B-Instruct\" # @param [ 'Llama-3-2-1B-Instruct', 'Llama-3-2-11B-Vision', 'Llama-3-2-11B-Vision-Instruct', 'Llama-3-2-3B', 'Llama-3-2-3B-Instruct', 'Llama-3-2-1B']\n", + "# fmt: on\n", + "ARGS_TEMPLATE = \"\"\"args:\n", + " - python\n", + " - -m\n", + " - vllm.entrypoints.api_server\n", + " - --host 0.0.0.0\n", + " - --port 7080\n", + " - --model=gs://vertex-model-garden-public-us/llama3.2/{}\n", + " - --tensor-parallel-size {}\n", + " - --swap-space 16\n", + " - --gpu-memory-utilization 0.95\n", + " {}\n", + " - --max-num-seqs {}\n", + " {}\n", + " - --enable-auto-tool-choice\n", + " {}\n", + " - --disable-log-stats\n", + " {}\"\"\"\n", + "# Model_name, tensor_size, - --model-type=llama3.1, max_seqs, - --enforce-eager, - --limit_mm_per_prompt='image=1', - --max-model-len 8192\n", + "\n", + "\n", + "def generate_args(missing_args):\n", + " args = ARGS_TEMPLATE.format(\n", + " missing_args[0],\n", + " missing_args[1],\n", + " missing_args[2],\n", + " missing_args[3],\n", + " missing_args[4],\n", + " missing_args[5],\n", + " missing_args[6],\n", + " )\n", + " lines = args.splitlines()\n", + " non_empty_lines = [line for line in lines if line.strip()]\n", + " return \"\\n\".join(non_empty_lines)\n", + "\n", + "\n", + "attr = {\n", + " \"Llama-3-2-11B-Vision\": [\n", + " [\n", + " \"Llama-3.2-11B-Vision\",\n", + " \"2\",\n", + " \"- --tool-call-parser=vertex-llama-3\",\n", + " \"12\",\n", + " \"- --enforce-eager\",\n", + " \"- --limit_mm_per_prompt='image=1'\",\n", + " \"- --max-model-len 8192\",\n", + " ],\n", + " 15,\n", + " \"58Gi\",\n", + " \"120Gi\",\n", + " 2,\n", + " ],\n", + " \"Llama-3-2-11B-Vision-Instruct\": [\n", + " [\n", + " \"Llama-3.2-11B-Vision-Instruct\",\n", + " \"2\",\n", + " \"- --tool-call-parser=vertex-llama-3\",\n", + " \"12\",\n", + " \"- --enforce-eager\",\n", + " \"- --limit_mm_per_prompt='image=1'\",\n", + " \"- --max-model-len 8192\",\n", + " ],\n", + " 15,\n", + " \"58Gi\",\n", + " \"120Gi\",\n", + " 2,\n", + " ],\n", + " \"Llama-3-2-3B\": [\n", + " [\n", + " \"Llama-3.2-3B\",\n", + " \"1\",\n", + " \"\",\n", + " \"64\",\n", + " \"\",\n", + " \"\",\n", + " \"- --tool-call-parser=vertex-llama-3\",\n", + " ],\n", + " 10,\n", + " \"39Gi\",\n", + " \"100Gi\",\n", + " 1,\n", + " ],\n", + " \"Llama-3-2-3B-Instruct\": [\n", + " [\n", + " \"Llama-3.2-3B-Instruct\",\n", + " \"1\",\n", + " \"\",\n", + " \"64\",\n", + " \"\",\n", + " \"\",\n", + " \"- --tool-call-parser=vertex-llama-3\",\n", + " ],\n", + " 10,\n", + " \"39Gi\",\n", + " \"100Gi\",\n", + " 1,\n", + " ],\n", + " \"Llama-3-2-1B\": [\n", + " [\n", + " \"Llama-3.2-1B\",\n", + " \"1\",\n", + " \"\",\n", + " \"64\",\n", + " \"\",\n", + " \"\",\n", + " \"- --tool-call-parser=vertex-llama-3\",\n", + " ],\n", + " 8,\n", + " \"29Gi\",\n", + " \"80Gi\",\n", + " 1,\n", + " ],\n", + " \"Llama-3-2-1B-Instruct\": [\n", + " [\n", + " \"Llama-3.2-1B-Instruct\",\n", + " \"1\",\n", + " \"\",\n", + " \"64\",\n", + " \"\",\n", + " \"\",\n", + " \"- --tool-call-parser=vertex-llama-3\",\n", + " ],\n", + " 8,\n", + " \"29Gi\",\n", + " \"80Gi\",\n", + " 1,\n", + " ],\n", + "}\n", + "\n", + "model_attr = attr[MODEL_NAME]\n", + "ARGS = generate_args(model_attr[0])\n", + "CPU_LIMITS = model_attr[1]\n", + "MEMORY_SIZE = model_attr[2]\n", + "EPHEMERAL_STORAGE_SIZE = model_attr[3]\n", + "GPU_COUNT = model_attr[4]\n", + "\n", + "\n", + "K8S_YAML = f\"\"\"\n", + "apiVersion: apps/v1\n", + "kind: Deployment\n", + "metadata:\n", + " name: llama-deployment\n", + "spec:\n", + " replicas: 1\n", + " selector:\n", + " matchLabels:\n", + " app: llama-server\n", + " template:\n", + " metadata:\n", + " labels:\n", + " app: llama-server\n", + " ai.gke.io/model: {MODEL_NAME}\n", + " ai.gke.io/inference-server: vllm\n", + " examples.ai.gke.io/source: model-garden\n", + " spec:\n", + " containers:\n", + " - name: inference-server\n", + " image: us-docker.pkg.dev/vertex-ai/vertex-vision-model-garden-dockers/pytorch-vllm-serve:20241007_2233_RC00\n", + " resources:\n", + " requests:\n", + " cpu: {CPU_LIMITS}\n", + " memory: {MEMORY_SIZE}\n", + " ephemeral-storage: {EPHEMERAL_STORAGE_SIZE}\n", + " nvidia.com/gpu: {GPU_COUNT}\n", + " limits:\n", + " cpu: {CPU_LIMITS}\n", + " memory: {MEMORY_SIZE}\n", + " ephemeral-storage: {EPHEMERAL_STORAGE_SIZE}\n", + " nvidia.com/gpu: {GPU_COUNT}\n", + " {ARGS}\n", + " env:\n", + " - name: MODEL_ID\n", + " value: 'meta-llama/{MODEL_NAME}'\n", + " - name: DEPLOY_SOURCE\n", + " value: 'UI_NATIVE_MODEL'\n", + " volumeMounts:\n", + " - mountPath: /dev/shm\n", + " name: dshm\n", + " volumes:\n", + " - name: dshm\n", + " emptyDir:\n", + " medium: Memory\n", + " nodeSelector:\n", + " cloud.google.com/gke-accelerator: nvidia-l4\n", + "---\n", + "apiVersion: v1\n", + "kind: Service\n", + "metadata:\n", + " name: llama-service\n", + " annotations:\n", + " cloud.google.com/load-balancer-type: \"Internal\"\n", + "spec:\n", + " selector:\n", + " app: llama-server # Should match the labels on your Pods\n", + " type: LoadBalancer\n", + " ports:\n", + " - protocol: TCP\n", + " port: 8000\n", + " targetPort: 7080\n", + "\"\"\"\n", + "\n", + "with open(\"llama_32.yaml\", \"w\") as f:\n", + " f.write(K8S_YAML)\n", + "\n", + "! kubectl apply -f llama_32.yaml" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "JgMxqkSdT45A" + }, + "source": [ + "### Wait for container to be created.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "bvEZBNGKD2Hp" + }, + "outputs": [], + "source": [ + "import time\n", + "\n", + "MAX_WAIT_TIME = 600 # 10 minutes in seconds\n", + "start_time = time.time()\n", + "end_time = time.time() + MAX_WAIT_TIME\n", + "\n", + "print(\"Waiting for container to be created...\\n\")\n", + "while start_time < end_time:\n", + " shell_output = ! kubectl get pod -l app=llama-server\n", + " container_status = \"\\n\".join(shell_output)\n", + " if \"1/1\" in container_status:\n", + " break\n", + " time.sleep(15)\n", + " start_time += 15\n", + "\n", + "if start_time > end_time:\n", + " print(\"Deployment took longer than expected\")\n", + "\n", + "print(container_status)\n", + "\n", + "# Wait for downloading artifacts.\n", + "start_time = time.time()\n", + "end_time = time.time() + MAX_WAIT_TIME\n", + "print(\"\\nDownloading artifacts...\")\n", + "while start_time < end_time:\n", + " shell_output = ! kubectl logs -l app=llama-server\n", + " logs = \"\\n\".join(shell_output)\n", + " if \"Connected\" in logs or \"Uvicorn running\" in logs:\n", + " break\n", + " time.sleep(15)\n", + " start_time += 15\n", + "\n", + "if start_time > end_time:\n", + " print(\"Deployment took longer than expected\")\n", + "\n", + "\n", + "print(\"\\nServer is up and running!\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "Qhkc2pVpcoLq" + }, + "source": [ + "### Testing endpoint" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "JxWMiOWgNfM1" + }, + "outputs": [], + "source": [ + "import json\n", + "import subprocess\n", + "import sys\n", + "import time\n", + "\n", + "import requests # Required: pip install requests\n", + "\n", + "# --- Configuration ---\n", + "SERVICE_NAME = \"llama-service\"\n", + "NAMESPACE = \"default\" # Adjust if your service is in a different namespace\n", + "SERVICE_PORT = 8000 # The 'port' specified in your Service YAML\n", + "API_PATH = \"/generate\" # The specific API path for vLLM generation\n", + "\n", + "# --- Request Payload ---\n", + "user_message = \"What is AI?\" # @param {type: \"string\"}\n", + "max_tokens = 50 # @param {type:\"integer\"}\n", + "temperature = 0.9 # @param {type:\"number\"}\n", + "\n", + "request_payload = {\n", + " \"prompt\": user_message,\n", + " \"max_tokens\": max_tokens,\n", + " \"temperature\": temperature,\n", + "}\n", + "\n", + "\n", + "# --- Function to Get Internal Load Balancer IP ---\n", + "def get_internal_lb_ip(service_name, namespace, retries=5, delay=10):\n", + " \"\"\"Fetches the Internal Load Balancer IP for a Kubernetes Service.\"\"\"\n", + " command = [\n", + " \"kubectl\",\n", + " \"get\",\n", + " \"service\",\n", + " service_name,\n", + " \"-n\",\n", + " namespace,\n", + " \"-o\",\n", + " \"jsonpath={.status.loadBalancer.ingress[0].ip}\",\n", + " ]\n", + " print(\n", + " f\"Attempting to get IP for service '{service_name}' in namespace '{namespace}'...\"\n", + " )\n", + " for attempt in range(retries):\n", + " try:\n", + " result = subprocess.run(\n", + " command, capture_output=True, text=True, check=True, timeout=30\n", + " )\n", + " ip_address = result.stdout.strip()\n", + " if ip_address:\n", + " print(f\"Successfully found Internal LB IP: {ip_address}\")\n", + " return ip_address\n", + " print(\n", + " f\"Attempt {attempt + 1}/{retries}: IP address not assigned yet. Waiting {delay} seconds...\"\n", + " )\n", + " except subprocess.CalledProcessError as e:\n", + " # Handle cases where the service might not exist or JSON path fails temporarily\n", + " print(\n", + " f\"Attempt {attempt + 1}/{retries}: Error getting service IP: {e}. stderr: {e.stderr}. Waiting {delay} seconds...\"\n", + " )\n", + " except subprocess.TimeoutExpired:\n", + " print(\n", + " f\"Attempt {attempt + 1}/{retries}: 'kubectl' command timed out. Waiting {delay} seconds...\"\n", + " )\n", + " except FileNotFoundError:\n", + " print(\n", + " \"Error: 'kubectl' command not found. Please ensure kubectl is installed and in your PATH.\"\n", + " )\n", + " return None\n", + "\n", + " if attempt < retries - 1:\n", + " time.sleep(delay)\n", + "\n", + " print(\n", + " f\"Error: Could not retrieve Internal LB IP for service '{service_name}' after {retries} attempts.\"\n", + " )\n", + " return None\n", + "\n", + "\n", + "# --- Main Execution ---\n", + "internal_ip = get_internal_lb_ip(SERVICE_NAME, NAMESPACE)\n", + "\n", + "if internal_ip:\n", + " # Construct the full endpoint URL\n", + " endpoint_url = f\"http://{internal_ip}:{SERVICE_PORT}{API_PATH}\"\n", + " print(f\"Calling endpoint: {endpoint_url}\")\n", + "\n", + " headers = {\"Content-Type\": \"application/json\"}\n", + "\n", + " try:\n", + " # Make the POST request\n", + " response = requests.post(\n", + " endpoint_url,\n", + " headers=headers,\n", + " json=request_payload, # requests library handles json serialization\n", + " timeout=120, # Set a reasonable timeout (in seconds)\n", + " )\n", + "\n", + " # Raise an exception for bad status codes (4xx or 5xx)\n", + " response.raise_for_status()\n", + "\n", + " # Parse the JSON response\n", + " response_data = response.json()\n", + "\n", + " # Extract and print the prediction (adjust path if vLLM format differs)\n", + " # Assuming the structure is similar to the output you got via kubectl exec\n", + " if (\n", + " \"predictions\" in response_data\n", + " and isinstance(response_data[\"predictions\"], list)\n", + " and len(response_data[\"predictions\"]) > 0\n", + " ):\n", + " print(\"\\n--- Response ---\")\n", + " print(response_data[\"predictions\"][0])\n", + " elif (\n", + " \"text\" in response_data\n", + " and isinstance(response_data[\"text\"], list)\n", + " and len(response_data[\"text\"]) > 0\n", + " ):\n", + " # Handle potential alternative vLLM output format like {'text': ['...']}\n", + " print(\"\\n--- Response ---\")\n", + " print(response_data[\"text\"][0])\n", + " else:\n", + " print(\n", + " \"\\n--- Full Response (Could not find 'predictions' or 'text' array) ---\"\n", + " )\n", + " print(json.dumps(response_data, indent=2))\n", + "\n", + " except requests.exceptions.ConnectionError as e:\n", + " print(\n", + " f\"\\nError: Could not connect to the endpoint {endpoint_url}.\",\n", + " file=sys.stderr,\n", + " )\n", + " print(\n", + " \"Check network connectivity and if the service/pods are running.\",\n", + " file=sys.stderr,\n", + " )\n", + " print(f\" Details: {e}\", file=sys.stderr)\n", + " except requests.exceptions.Timeout:\n", + " print(\n", + " f\"\\nError: Request timed out while calling {endpoint_url}.\", file=sys.stderr\n", + " )\n", + " except requests.exceptions.RequestException as e:\n", + " print(\n", + " f\"\\nError: An error occurred during the request to {endpoint_url}: {e}\",\n", + " file=sys.stderr,\n", + " )\n", + " # Print response body if available, might contain useful error info from the server\n", + " if e.response is not None:\n", + " print(\"--- Server Response ---\", file=sys.stderr)\n", + " try:\n", + " print(json.dumps(e.response.json(), indent=2), file=sys.stderr)\n", + " except json.JSONDecodeError:\n", + " print(e.response.text, file=sys.stderr)\n", + " except json.JSONDecodeError:\n", + " print(\n", + " \"\\nError: Could not decode the JSON response from the server.\",\n", + " file=sys.stderr,\n", + " )\n", + " print(\"--- Raw Response ---\", file=sys.stderr)\n", + " print(response.text, file=sys.stderr)\n", + "else:\n", + " print(\n", + " \"\\nExecution failed: Could not determine the endpoint IP address.\",\n", + " file=sys.stderr,\n", + " )" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "lc1FaXLKcrmW" + }, + "source": [ + "## Cloud Function" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "ICtkVx7ShVC7" + }, + "source": [ + "### Create service account and assign right permissions" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "4JDnEAqZcqsx" + }, + "outputs": [], + "source": [ + "SERVICE_ACCOUNT_NAME = \"\" # @param {type: \"string\"}\n", + "\n", + "!gcloud iam service-accounts create {SERVICE_ACCOUNT_NAME} \\\n", + " --project={PROJECT_ID} \\\n", + " --description=\"llama service account\" \\\n", + " --display-name=\"llama testing service account\"\n", + "\n", + "SERVICE_ACCOUNT = f\"{SERVICE_ACCOUNT_NAME}@{PROJECT_ID}.iam.gserviceaccount.com\"\n", + "\n", + "for role in ['aiplatform.user', 'storage.objectAdmin', 'artifactregistry.reader', 'run.developer', 'run.invoker']:\n", + "\n", + " ! gcloud projects add-iam-policy-binding {PROJECT_ID} \\\n", + " --member=serviceAccount:{SERVICE_ACCOUNT} \\\n", + " --role=roles/{role} --condition=None" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "kBCL4vkCyDtV" + }, + "source": [ + "### Prepare code for Cloud function" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "f7CjHee3dFqT" + }, + "outputs": [], + "source": [ + "from pathlib import Path as path\n", + "\n", + "ROOT_PATH = path.cwd()\n", + "TUTORIAL_PATH = ROOT_PATH / \"tutorial\"\n", + "BUILD_PATH = TUTORIAL_PATH / \"build\"\n", + "\n", + "TUTORIAL_PATH.mkdir(parents=True, exist_ok=True)\n", + "BUILD_PATH.mkdir(parents=True, exist_ok=True)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "Q0Ud8hWedfMe" + }, + "outputs": [], + "source": [ + "requirements = \"\"\"\n", + "requests>=2.26.0\n", + "functions-framework>=3.0.0\n", + "google-cloud-logging>=3.0.0\n", + "\"\"\"\n", + "\n", + "with open(BUILD_PATH / \"requirements.txt\", \"w\") as f:\n", + " f.write(requirements)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "0I079nDYdqMo" + }, + "outputs": [], + "source": [ + "cloud_function_code = '''\n", + "import os\n", + "import json\n", + "import requests\n", + "import logging\n", + "import functions_framework # Required by GCF\n", + "\n", + "# Configure logging\n", + "logging.basicConfig(level=logging.INFO)\n", + "\n", + "# --- Configuration (Partially from Environment) ---\n", + "# Fetch required info from environment variables set during deployment\n", + "LLAMA_ILB_IP = os.environ.get(\"LLAMA_ENDPOINT_IP\") # Expecting the IP here\n", + "SERVICE_PORT = os.environ.get(\"LLAMA_ENDPOINT_PORT\", \"8000\") # Default to 8000 if not set\n", + "API_PATH = \"/generate\" # The specific API path for vLLM generation\n", + "REQUEST_TIMEOUT = 120 # Timeout for the request to the Llama service\n", + "\n", + "@functions_framework.http # Defines this as an HTTP-triggered function\n", + "def call_llama_service(request):\n", + " \"\"\"\n", + " Google Cloud Function entry point.\n", + " Expects a POST request with JSON body containing:\n", + " {\n", + " \"prompt\": \"Your question here\",\n", + " \"max_tokens\": 50, // optional\n", + " \"temperature\": 0.9 // optional\n", + " }\n", + " \"\"\"\n", + " if not LLAMA_ILB_IP:\n", + " logging.error(\"Environment variable LLAMA_ENDPOINT_IP is not set.\")\n", + " return (\"Internal Server Error: Service endpoint IP not configured.\", 500)\n", + "\n", + " if request.method != 'POST':\n", + " logging.warning(f\"Received non-POST request method: {request.method}\")\n", + " return ('Method Not Allowed', 405)\n", + "\n", + " try:\n", + " request_json = request.get_json(silent=True)\n", + " if not request_json or 'prompt' not in request_json:\n", + " logging.error(\"Invalid request: Missing JSON payload or 'prompt' key.\")\n", + " return (\"Invalid request: Missing JSON payload or 'prompt' key.\", 400)\n", + "\n", + " # --- Get Parameters from Request ---\n", + " prompt = request_json['prompt']\n", + " max_tokens = request_json.get('max_tokens', 50) # Default if not provided\n", + " temperature = request_json.get('temperature', 0.9) # Default if not provided\n", + "\n", + " request_payload = {\n", + " \"prompt\": prompt,\n", + " \"max_tokens\": max_tokens,\n", + " \"temperature\": temperature,\n", + " }\n", + "\n", + " # Construct the full endpoint URL\n", + " endpoint_url = f\"http://{LLAMA_ILB_IP}:{SERVICE_PORT}{API_PATH}\"\n", + " logging.info(f\"Calling Llama endpoint: {endpoint_url}\")\n", + " # Log payload without potentially sensitive prompt details in production if needed\n", + " logging.info(f\"Payload (excluding prompt): max_tokens={max_tokens}, temperature={temperature}\")\n", + "\n", + " headers = {\"Content-Type\": \"application/json\"}\n", + "\n", + " # --- Make the POST request ---\n", + " response = requests.post(\n", + " endpoint_url,\n", + " headers=headers,\n", + " json=request_payload,\n", + " timeout=REQUEST_TIMEOUT\n", + " )\n", + "\n", + " # Raise an exception for bad status codes (4xx or 5xx)\n", + " response.raise_for_status()\n", + "\n", + " # Parse the JSON response from Llama service\n", + " response_data = response.json()\n", + " logging.info(\"Successfully received response from Llama service.\")\n", + "\n", + " # --- Process and Return Response ---\n", + " # Extract prediction (adjust keys if needed based on actual vLLM response)\n", + " prediction = None\n", + " if \"predictions\" in response_data and isinstance(response_data[\"predictions\"], list) and len(response_data[\"predictions\"]) > 0:\n", + " prediction = response_data[\"predictions\"][0]\n", + " elif \"text\" in response_data and isinstance(response_data[\"text\"], list) and len(response_data[\"text\"]) > 0:\n", + " prediction = response_data[\"text\"][0]\n", + " else:\n", + " logging.warning(\"Could not find 'predictions' or 'text' in response. Returning full response.\")\n", + " # Return the full response if the expected key isn't found\n", + " return (response_data, 200) # Return raw JSON with 200 OK\n", + "\n", + " if prediction is not None:\n", + " # Return only the prediction text/data\n", + " return ({ \"prediction\": prediction }, 200) # Return prediction in a structured way\n", + " else:\n", + " logging.error(\"Prediction key found but content was empty/invalid.\")\n", + " return (\"Error processing model response\", 500)\n", + "\n", + "\n", + " except requests.exceptions.ConnectionError as e:\n", + " logging.error(f\"Connection Error calling {endpoint_url}: {e}\")\n", + " return (f\"Could not connect to the backend service: {e}\", 503) # 503 Service Unavailable\n", + " except requests.exceptions.Timeout:\n", + " logging.error(f\"Request timed out calling {endpoint_url}\")\n", + " return (\"Backend service timed out.\", 504) # 504 Gateway Timeout\n", + " except requests.exceptions.RequestException as e:\n", + " logging.error(f\"RequestException calling {endpoint_url}: {e}\")\n", + " # Log response body if available\n", + " error_details = str(e)\n", + " if e.response is not None:\n", + " logging.error(f\"Backend service response status: {e.response.status_code}\")\n", + " try:\n", + " error_details = json.dumps(e.response.json())\n", + " logging.error(f\"Backend service response body: {error_details}\")\n", + " except json.JSONDecodeError:\n", + " error_details = e.response.text\n", + " logging.error(f\"Backend service response body (non-JSON): {error_details}\")\n", + " return (f\"Error calling backend service: {error_details}\", 502) # 502 Bad Gateway\n", + " except json.JSONDecodeError:\n", + " logging.error(f\"Could not decode the JSON response from the Llama service. Raw response: {response.text}\")\n", + " return (\"Invalid response format from backend service.\", 502)\n", + " except Exception as e:\n", + " logging.exception(f\"An unexpected error occurred: {e}\") # Log full traceback\n", + " return (\"An internal server error occurred.\", 500)\n", + "'''\n", + "\n", + "\n", + "with open(BUILD_PATH / \"main.py\", \"w\") as f:\n", + " f.write(cloud_function_code)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "1o01XKBmx5gs" + }, + "source": [ + "### Create VPC connector" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "wZdxnxIvj6_w" + }, + "outputs": [], + "source": [ + "! gcloud config set project {PROJECT_ID}\n", + "! gcloud services enable vpcaccess.googleapis.com" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "Jme6o84CorG0" + }, + "outputs": [], + "source": [ + "VPC_CONNECTOR_NAME = \"\" # @param {type: \"string\"}\n", + "RANGE = \"10.6.0.0/28\" # @param {type: \"string\"}\n", + "\n", + "!gcloud compute networks vpc-access connectors create {VPC_CONNECTOR_NAME} \\\n", + " --region {REGION} \\\n", + " --network {NETWORK_NAME} \\\n", + " --range {RANGE} \\\n", + " --min-instances 2 \\\n", + " --max-instances 5 \\\n", + " --machine-type \"e2-micro\"" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "LXV5eikfx8no" + }, + "source": [ + "### Create function" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "jtI5uQQpfsh4" + }, + "outputs": [], + "source": [ + "# --- Set these variables ---\n", + "FUNCTION_NAME=\"test-llama-gke-1\" # @param {type: \"string\"}\n", + "ILB_IP= internal_ip # The IP address you found above\n", + "\n", + "# --- Deployment Command ---\n", + "!gcloud functions deploy {FUNCTION_NAME} \\\n", + " --gen2 \\\n", + " --runtime=\"python311\" \\\n", + " --source={str(BUILD_PATH)} \\\n", + " --region={REGION} \\\n", + " --entry-point=call_llama_service \\\n", + " --trigger-http \\\n", + " --no-allow-unauthenticated \\\n", + " --set-env-vars=LLAMA_ENDPOINT_IP={ILB_IP} \\\n", + " --quiet \\\n", + " --service-account={SERVICE_ACCOUNT} \\\n", + " --timeout=600 \\\n", + " --memory=2Gb \\\n", + " --concurrency=2 \\\n", + " --min-instances=2 \\\n", + " --project {PROJECT_ID} \\\n", + " --vpc-connector {VPC_CONNECTOR_NAME}\n", + "\n", + "\n", + "print(f\"Function: {FUNCTION_NAME} deployed successfully.\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "wbRmgoOZF6es" + }, + "source": [ + "## Clean up resources" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "911406c1561e" + }, + "outputs": [], + "source": [ + "# @markdown Delete the experiment models and endpoints to recycle the resources\n", + "# @markdown and avoid unnecessary continuous charges that may incur.\n", + "DELETE_DEPLOYMENT = False # @param {type: \"boolean\"}\n", + "DELETE_CLUSTER = False # @param {type: \"boolean\"}\n", + "\n", + "if DELETE_DEPLOYMENT:\n", + " ! kubectl delete deployments llama-deployment\n", + " ! kubectl delete services llama-service\n", + "\n", + "if DELETE_CLUSTER:\n", + " ! gcloud container clusters delete {CLUSTER_NAME} --region={REGION} --quiet\n", + "\n", + "! gcloud functions delete {FUNCTION_NAME} --region={REGION} --quiet\n", + "\n", + "! gcloud compute networks vpc-access connectors delete {VPC_CONNECTOR_NAME} --region {REGION} --quiet" + ] + } + ], + "metadata": { + "colab": { + "name": "deploy_llama3_vllm_gke_cloud_function.ipynb", + "toc_visible": true + }, + "kernelspec": { + "display_name": "Python 3", + "name": "python3" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} diff --git a/open-models/use-cases/guess_app.ipynb b/open-models/use-cases/guess_app.ipynb new file mode 100644 index 0000000..2933073 --- /dev/null +++ b/open-models/use-cases/guess_app.ipynb @@ -0,0 +1,784 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "ur8xi4C7S06n" + }, + "outputs": [], + "source": [ + "# Copyright 2024 Google LLC\n", + "#\n", + "# Licensed under the Apache License, Version 2.0 (the \"License\");\n", + "# you may not use this file except in compliance with the License.\n", + "# You may obtain a copy of the License at\n", + "#\n", + "# https://www.apache.org/licenses/LICENSE-2.0\n", + "#\n", + "# Unless required by applicable law or agreed to in writing, software\n", + "# distributed under the License is distributed on an \"AS IS\" BASIS,\n", + "# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n", + "# See the License for the specific language governing permissions and\n", + "# limitations under the License." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "JAPoU8Sm5E6e" + }, + "source": [ + "# Guess who or what app using Hugging Face Deep Learning container model on Vertex AI\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + "
    \n", + " \n", + " \"Google
    Open in Colab\n", + "
    \n", + "
    \n", + " \n", + " \"Google
    Open in Colab Enterprise\n", + "
    \n", + "
    \n", + " \n", + " \"Vertex
    Open in Vertex AI Workbench\n", + "
    \n", + "
    \n", + " \n", + " \"GitHub
    View on GitHub\n", + "
    \n", + "
    \n", + "\n", + "
    \n", + "\n", + "Share to:\n", + "\n", + "\n", + " \"LinkedIn\n", + "\n", + "\n", + "\n", + " \"Bluesky\n", + "\n", + "\n", + "\n", + " \"X\n", + "\n", + "\n", + "\n", + " \"Reddit\n", + "\n", + "\n", + "\n", + " \"Facebook\n", + "" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "84f0f73a0f76" + }, + "source": [ + "| | |\n", + "|-|-|\n", + "| Author(s) | [Ivan Nardini](https://github.com/inardini) |" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "tvgnzT1CKxrO" + }, + "source": [ + "## Overview\n", + "\n", + "This notebook shows how you can use Vertex AI and Hugging Face Deep Learning container to create a simple \"Guess who or what\" application which combines an image generation open model with Gemini to answer and represent subjects of Guess who or what riddles." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "61RBz8LLbxCR" + }, + "source": [ + "## Get started" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "No17Cw5hgx12" + }, + "source": [ + "### Install Vertex AI SDK and other required packages\n", + "\n", + "To run this example, you will only need the [`google-cloud-aiplatform`](https://github.com/googleapis/python-aiplatform) Python SDK and the [`huggingface_hub`](https://github.com/huggingface/huggingface_hub) Python package." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "tFy3H3aPgx12" + }, + "outputs": [], + "source": [ + "%pip install --upgrade --user --quiet google-cloud-aiplatform huggingface_hub gradio" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "R5Xep4W9lq-Z" + }, + "source": [ + "### Restart runtime (Colab only)\n", + "\n", + "To use the newly installed packages in this Jupyter environment, if you are on Colab you must restart the runtime. You can do this by running the cell below, which restarts the current kernel. The restart might take a minute or longer. After it's restarted, continue to the next step." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "XRvKdaPDTznN" + }, + "outputs": [], + "source": [ + "# Automatically restart kernel after installs so that your environment can access the new packages\n", + "# import IPython\n", + "\n", + "# app = IPython.Application.instance()\n", + "# app.kernel.do_shutdown(True)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "SbmM4z7FOBpM" + }, + "source": [ + "
    \n", + "⚠ The kernel is going to restart. Wait until it's finished before continuing to the next step. ⚠\n", + "
    \n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "dmWOrTJ3gx13" + }, + "source": [ + "### Authenticate your Google Cloud account\n", + "\n", + "Depending on your Jupyter environment, you may have to manually authenticate. Follow the relevant instructions below." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "9cc332aee5fd" + }, + "source": [ + "**1. Vertex AI Workbench**\n", + "\n", + "* Do nothing as you are already authenticated." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "54c81e1a63e6" + }, + "source": [ + "**2. Local JupyterLab instance, uncomment and run:**" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "c93d96bc9c43" + }, + "outputs": [], + "source": [ + "# !gcloud auth login" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "d3e571ce6c56" + }, + "source": [ + "**3. Colab, uncomment and run:**" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "984a0526fb68" + }, + "outputs": [], + "source": [ + "from google.colab import auth\n", + "\n", + "auth.authenticate_user()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "f9af3e57f89a" + }, + "source": [ + "### Authenticate your Hugging Face account" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "4c31c7272804" + }, + "source": [ + "Then you can install the `huggingface_hub` that comes with a CLI that will be used for the authentication with the token generated in advance. So that then the token can be safely retrieved via `huggingface_hub.get_token`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "8d836e0210fe" + }, + "outputs": [], + "source": [ + "from huggingface_hub import interpreter_login\n", + "\n", + "interpreter_login()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "c71a4314c250" + }, + "source": [ + "Read more about [Hugging Face Security](https://huggingface.co/docs/hub/en/security), specifically about [Hugging Face User Access Tokens](https://huggingface.co/docs/hub/en/security-tokens)." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "DF4l8DTdWgPY" + }, + "source": [ + "### Set Google Cloud project information and initialize Vertex AI SDK\n", + "\n", + "To get started using Vertex AI, you must have an existing Google Cloud project and [enable the Vertex AI API](https://console.cloud.google.com/flows/enableapi?apiid=aiplatform.googleapis.com), if not enabled already.\n", + "\n", + "Learn more about [setting up a project and a development environment](https://cloud.google.com/vertex-ai/docs/start/cloud-environment)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "Nqwi-5ufWp_B" + }, + "outputs": [], + "source": [ + "import os\n", + "\n", + "from google.cloud import aiplatform\n", + "\n", + "PROJECT_ID = \"[your-project-id]\" # @param {type:\"string\", isTemplate: true}\n", + "if PROJECT_ID == \"[your-project-id]\":\n", + " PROJECT_ID = str(os.environ.get(\"GOOGLE_CLOUD_PROJECT\"))\n", + "\n", + "LOCATION = os.environ.get(\"GOOGLE_CLOUD_REGION\", \"us-central1\")\n", + "\n", + "aiplatform.init(project=PROJECT_ID, location=LOCATION)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "ee37e1544281" + }, + "source": [ + "### Requirements" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "877cd3fb2dce" + }, + "source": [ + "To successfully deploy your Hugging Face model on Vertex AI, you need to have the following IAM roles set:\n", + "\n", + "- Artifact Registry Reader (`roles/artifactregistry.reader`)\n", + "- Vertex AI User (`roles/aiplatform.user`)\n", + "\n", + "For more information about granting roles, see [Manage access](https://cloud.google.com/iam/docs/granting-changing-revoking-access).\n", + "\n", + "---\n", + "\n", + "You also need to enable the following APIs (if not enabled already):\n", + "\n", + "- Vertex AI API (`aiplatform.googleapis.com`)\n", + "- Artifact Registry API (`artifactregistry.googleapis.com`)\n", + "\n", + "For more information about API enablement, see [Enabling APIs](https://cloud.google.com/apis/docs/getting-started#enabling_apis).\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "2pNoqLHVWG6X" + }, + "source": [ + "### Import libraries" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "V41joDcRWIzY" + }, + "outputs": [], + "source": [ + "import base64\n", + "import io\n", + "import os\n", + "\n", + "from IPython.display import display\n", + "from PIL import Image\n", + "from google.cloud.aiplatform import Endpoint\n", + "import gradio as gr\n", + "from huggingface_hub import get_token\n", + "import vertexai\n", + "from vertexai.generative_models import GenerationConfig, GenerativeModel, SafetySetting" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "2qxTWPNu1p-d" + }, + "source": [ + "# Build a \"Guess what or who\" app\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "dJsGNCv4T6R-" + }, + "source": [ + "## Register FLUX on Vertex AI\n", + "\n", + "To deploy a \"Guess what or who\" application using a Hugging Face model like [FLUX](https://console.cloud.google.com/vertex-ai/publishers/black-forest-labs/model-garden/flux1-schnell) for image generation on Vertex AI, you first register the chosen model within the [Vertex AI Model Registry](https://cloud.google.com/vertex-ai/docs/model-registry/introduction). This registry serves as a central repository for managing your models on Vertex AI.\n", + "\n", + "Registering a model involves specifying a serving container image and corresponding environment variables, which vary depending on the chosen model. For instance, for FLUX, you'll use a regular PyTorch Inference Deep Learning Container. You can find the appropriate container URI and further details in the [Google Cloud Deep Learning Containers documentation](https://huggingface.co/docs/google-cloud/en/containers/introduction). " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "Pc-Owc_7WiZb" + }, + "outputs": [], + "source": [ + "flux_model = aiplatform.Model.upload(\n", + " display_name=\"flux--generate\",\n", + " serving_container_image_uri=\"us-docker.pkg.dev/deeplearning-platform-release/gcr.io/huggingface-text-generation-inference-cu121.2-2.ubuntu2204.py310\",\n", + " serving_container_environment_variables={\n", + " \"HF_MODEL_ID\": \"black-forest-labs/FLUX.1-dev\",\n", + " \"HF_TASK\": \"text-to-image\",\n", + " \"HF_TOKEN\": get_token(),\n", + " },\n", + ")\n", + "flux_model.wait()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "c427c0a87016" + }, + "source": [ + "## Deploy Flux on Vertex AI\n", + "\n", + "After successfully registering the model, you can then deploy it to a Vertex AI Endpoint according to your preferred deployment configuration, making it ready for image generation.\n", + "\n", + "This deployment creates a new instance of the FLUX model in Vertex AI Prediction, the managed inference service on Vertex AI." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "sxI3SsHxVYn4" + }, + "outputs": [], + "source": [ + "endpoint = aiplatform.Endpoint.create(display_name=\"flux--generate-endpoint\")\n", + "\n", + "deployed_flux_model = flux_model.deploy(\n", + " endpoint=endpoint,\n", + " machine_type=\"g2-standard-48\",\n", + " accelerator_type=\"NVIDIA_L4\",\n", + " accelerator_count=4,\n", + " sync=False,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "8lIr8uko04T_" + }, + "source": [ + "## Generate predictions\n", + "\n", + "After the model gets successfully deployed, you can test it by submitting a prediction request." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "ERBKKCPylaTt" + }, + "source": [ + "### Generate an image using FLUX" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "Hxw6otB3SBZE" + }, + "outputs": [], + "source": [ + "response = deployed_flux_model.predict(\n", + " instances=[\"a image of a cat riding a horse in illustration style\"],\n", + " parameters={\n", + " \"width\": 512,\n", + " \"height\": 512,\n", + " \"num_inference_steps\": 8,\n", + " \"guidance_scale\": 3.5,\n", + " },\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "wAGzijoxOqvr" + }, + "source": [ + "### Get the generated image" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "-9vR2U6b12sK" + }, + "outputs": [], + "source": [ + "image = Image.open(io.BytesIO(base64.b64decode(response.predictions[0])))\n", + "display(image)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "9b3fd1898241" + }, + "source": [ + "## Play \"Guess who or what\"\n", + "\n", + "With the image generation open model deployed in a Vertex AI Endpoint, you are now able to build your \"Guess who or what\" Gen AI application.\n", + "\n", + "In this scenario, you use Gradio to quickly build a web application for your Gen AI models." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "wkBl-jwH0_AJ" + }, + "outputs": [], + "source": [ + "# Get enviroment variables\n", + "PROJECT_ID = os.environ.get(\"PROJECT_ID\", PROJECT_ID)\n", + "LOCATION = os.environ.get(\"LOCATION\", LOCATION)\n", + "MODEL_ID = os.environ.get(\"MODEL_ID\", \"gemini-2.0-flash\")\n", + "ENDPOINT_NAME = os.environ.get(\"ENDPOINT\", endpoint.resource_name)\n", + "\n", + "# Initialize Vertex AI SDK\n", + "aiplatform.init(project=PROJECT_ID, location=LOCATION)\n", + "vertexai.init(project=PROJECT_ID, location=LOCATION)\n", + "\n", + "# Initialize Gemini model\n", + "MODEL = GenerativeModel(\"gemini-2.0-flash\")\n", + "ENDPOINT = Endpoint(ENDPOINT_NAME)\n", + "\n", + "# Common generation config and safety settings\n", + "GENERATION_CONFIG = GenerationConfig(\n", + " candidate_count=1, max_output_tokens=8192, temperature=0\n", + ")\n", + "\n", + "SAFETY_SETTINGS = [\n", + " SafetySetting(\n", + " category=SafetySetting.HarmCategory.HARM_CATEGORY_HATE_SPEECH,\n", + " threshold=SafetySetting.HarmBlockThreshold.OFF,\n", + " ),\n", + " SafetySetting(\n", + " category=SafetySetting.HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT,\n", + " threshold=SafetySetting.HarmBlockThreshold.OFF,\n", + " ),\n", + " SafetySetting(\n", + " category=SafetySetting.HarmCategory.HARM_CATEGORY_SEXUALLY_EXPLICIT,\n", + " threshold=SafetySetting.HarmBlockThreshold.OFF,\n", + " ),\n", + " SafetySetting(\n", + " category=SafetySetting.HarmCategory.HARM_CATEGORY_HARASSMENT,\n", + " threshold=SafetySetting.HarmBlockThreshold.OFF,\n", + " ),\n", + "]\n", + "\n", + "\n", + "def generate_gemini_content(prompt_template: str, **kwargs) -> str:\n", + " \"\"\"Generate content using Gemini model with given prompt template.\"\"\"\n", + " prompt = prompt_template.format(**kwargs)\n", + " response = MODEL.generate_content(\n", + " [prompt],\n", + " generation_config=GENERATION_CONFIG,\n", + " safety_settings=SAFETY_SETTINGS,\n", + " stream=False,\n", + " )\n", + " return response.text\n", + "\n", + "\n", + "def generate_subject(riddle: str) -> str:\n", + " \"\"\"Extract the subject/answer from a given riddle using Gemini model.\"\"\"\n", + " riddle_solver_prompt_template = \"\"\"\n", + " You are the best riddle solver. Given a riddle, your goal is solve it and only indicate the subject of the riddle.\n", + " RIDDLE: {riddle}\n", + " SUBJECT:\n", + " \"\"\"\n", + " subject = generate_gemini_content(riddle_solver_prompt_template, riddle=riddle)\n", + " return subject.replace(\"SUBJECT:\", \"\").strip()\n", + "\n", + "\n", + "def generate_prompt(subject: str) -> str:\n", + " \"\"\"Generate an image generation prompt for a given subject.\"\"\"\n", + " image_gen_prompt_template = \"\"\"\n", + " You are a professional prompt engineer. Given a subject, prototype the most appropriate prompt to best visualize the subject.\n", + " Only return the preferred prompt.\n", + " SUBJECT: {subject}\n", + " PROMPT:\n", + " \"\"\"\n", + " return generate_gemini_content(image_gen_prompt_template, subject=subject)\n", + "\n", + "\n", + "def generate_image(image_gen_prompt: str) -> Image.Image:\n", + " \"\"\"Generate an image based on the provided prompt.\"\"\"\n", + " response = ENDPOINT.predict(\n", + " instances=[image_gen_prompt],\n", + " parameters={\n", + " \"width\": 512,\n", + " \"height\": 512,\n", + " \"num_inference_steps\": 8,\n", + " \"guidance_scale\": 3.5,\n", + " },\n", + " )\n", + " return Image.open(io.BytesIO(base64.b64decode(response.predictions[0])))\n", + "\n", + "\n", + "def guess_game(riddle: str) -> tuple[Image.Image, str, str]:\n", + " \"\"\"Run the complete riddle-to-image game flow.\"\"\"\n", + " answer = generate_subject(riddle)\n", + " prompt = generate_prompt(answer)\n", + " image = generate_image(prompt)\n", + " return image, answer, prompt\n", + "\n", + "\n", + "def increment_counter(counter: int) -> tuple[int, Image.Image | None, str, str, str]:\n", + " \"\"\"Increment the game counter\"\"\"\n", + " return counter + 1, None, \"\", \"\", \"\"\n", + "\n", + "\n", + "def reset_game(counter: int) -> tuple[int, Image.Image | None, str, str, str]:\n", + " \"\"\"Reset the game state.\"\"\"\n", + " return counter, None, \"\", \"\", \"\"" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "5vzru9pIXxIT" + }, + "source": [ + "## Build the app\n", + "\n", + "Put together functions you define to build your Gradio app." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "OpZ0mbmzXirY" + }, + "outputs": [], + "source": [ + "# Create main application block with Ocean theme\n", + "with gr.Blocks(theme=gr.themes.Ocean()) as app:\n", + "\n", + " # Header row containing title and counter state\n", + " with gr.Row():\n", + " gr.Markdown(\"# Guess What Game ❓\")\n", + " counter_state = gr.State(value=0)\n", + "\n", + " # Input row for user's riddle/description\n", + " with gr.Row():\n", + " prompt_input = gr.Textbox(label=\"Describe someone or something 💬 \")\n", + "\n", + " # Submit button to generate image and answer\n", + " submit_btn = gr.Button(\"Submit\")\n", + "\n", + " # Output row displaying generated content\n", + " with gr.Row():\n", + " image_prompt = gr.Textbox(\n", + " label=\"Generated Image Prompt with Gemini 🎹 \", visible=True\n", + " )\n", + " image_output = gr.Image(label=\"Generated Image đŸ–Œïž \")\n", + " answer_output = gr.Textbox(\n", + " label=\"Generated Answer with Gemini 🌌 \", interactive=False\n", + " )\n", + "\n", + " # Game control buttons row\n", + " with gr.Row():\n", + " correct_btn = gr.Button(\"+1 Correct\")\n", + " reset_btn = gr.Button(\"Reset\")\n", + "\n", + " # Display for tracking correct guesses\n", + " counter_display = gr.Number(value=0, label=\"Correct Guesses 👍\", interactive=False)\n", + "\n", + " # Event handler for submit button\n", + " submit_btn.click(\n", + " guess_game,\n", + " inputs=[prompt_input],\n", + " outputs=[image_output, answer_output, image_prompt],\n", + " )\n", + "\n", + " # Event handler for correct button\n", + " correct_btn.click(\n", + " increment_counter,\n", + " inputs=[counter_state],\n", + " outputs=[\n", + " counter_state,\n", + " image_output,\n", + " answer_output,\n", + " image_prompt,\n", + " prompt_input,\n", + " ],\n", + " ).then(lambda x: x, inputs=[counter_state], outputs=[counter_display])\n", + "\n", + " # Event handler for reset button\n", + " reset_btn.click(\n", + " reset_game,\n", + " inputs=[counter_state],\n", + " outputs=[\n", + " counter_state,\n", + " image_output,\n", + " answer_output,\n", + " image_prompt,\n", + " prompt_input,\n", + " ],\n", + " ).then(lambda x: x, inputs=[counter_state], outputs=[counter_display])" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "sbSdpVjoOSgD" + }, + "source": [ + "### Launch the app\n", + "\n", + "You are now ready to launch your \"Guess who or what\" Gradio app." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "cwUjdcP23Qmw" + }, + "outputs": [], + "source": [ + "app.launch()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "2a4e033321ad" + }, + "source": [ + "## Cleaning up" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "iRfRH5fQc1IL" + }, + "outputs": [], + "source": [ + "delete_app = True\n", + "delete_endpoint = True\n", + "delete_model = True\n", + "\n", + "if delete_app:\n", + " app.close()\n", + "\n", + "if delete_endpoint:\n", + " deployed_flux_model.delete()\n", + "\n", + "if delete_model:\n", + " flux_model.delete()" + ] + } + ], + "metadata": { + "colab": { + "name": "guess_app.ipynb", + "toc_visible": true + }, + "kernelspec": { + "display_name": "Python 3", + "name": "python3" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} diff --git a/open-models/use-cases/model_garden_litellm_inference.ipynb b/open-models/use-cases/model_garden_litellm_inference.ipynb new file mode 100644 index 0000000..93eacf3 --- /dev/null +++ b/open-models/use-cases/model_garden_litellm_inference.ipynb @@ -0,0 +1,667 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "ur8xi4C7S06n" + }, + "outputs": [], + "source": [ + "# Copyright 2025 Google LLC\n", + "#\n", + "# Licensed under the Apache License, Version 2.0 (the \"License\");\n", + "# you may not use this file except in compliance with the License.\n", + "# You may obtain a copy of the License at\n", + "#\n", + "# https://www.apache.org/licenses/LICENSE-2.0\n", + "#\n", + "# Unless required by applicable law or agreed to in writing, software\n", + "# distributed under the License is distributed on an \"AS IS\" BASIS,\n", + "# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n", + "# See the License for the specific language governing permissions and\n", + "# limitations under the License." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "JAPoU8Sm5E6e" + }, + "source": [ + "# Serving Open-Source LLMs on Vertex AI with LiteLLM and OpenAI-Compatible APIs\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + "
    \n", + " \n", + " \"Google
    Open in Colab\n", + "
    \n", + "
    \n", + " \n", + " \"Google
    Open in Colab Enterprise\n", + "
    \n", + "
    \n", + " \n", + " \"Vertex
    Open in Vertex AI Workbench\n", + "
    \n", + "
    \n", + " \n", + " \"GitHub
    View on GitHub\n", + "
    \n", + "
    \n", + "\n", + "
    \n", + "\n", + "Share to:\n", + "\n", + "\n", + " \"LinkedIn\n", + "\n", + "\n", + "\n", + " \"Bluesky\n", + "\n", + "\n", + "\n", + " \"X\n", + "\n", + "\n", + "\n", + " \"Reddit\n", + "\n", + "\n", + "\n", + " \"Facebook\n", + "" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "84f0f73a0f76" + }, + "source": [ + "| Author |\n", + "| --- |\n", + "| [Eliza Huang](https://github.com/lizzij) |" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "tvgnzT1CKxrO" + }, + "source": [ + "## Overview\n", + "\n", + "This notebook demonstrates how to:\n", + "- Deploy an open-source LLM (e.g. DeepSeek, Qwen, LLaMA, Gemma) via [Vertex AI Model Garden](https://cloud.google.com/vertex-ai/docs/generative-ai/model-garden).\n", + "- Serve the model with a public Vertex AI endpoint.\n", + "- Connect it to [LiteLLM](https://docs.litellm.ai/) using an OpenAI-compatible schema for chat completion and function calling.\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "61RBz8LLbxCR" + }, + "source": [ + "## Get started" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "No17Cw5hgx12" + }, + "source": [ + "### Install Vertex AI SDK and other required packages\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "tFy3H3aPgx12" + }, + "outputs": [], + "source": [ + "%pip install --upgrade --force-reinstall --quiet 'google-cloud-aiplatform>=1.93.1' 'litellm' 'openai' 'google-auth' 'requests'" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "dmWOrTJ3gx13" + }, + "source": [ + "### Authenticate your notebook environment (Colab only)\n", + "\n", + "If you're running this notebook on Google Colab, run the cell below to authenticate your environment." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "NyKGtVQjgx13" + }, + "outputs": [], + "source": [ + "import sys\n", + "\n", + "if \"google.colab\" in sys.modules:\n", + " from google.colab import auth\n", + "\n", + " auth.authenticate_user()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "DF4l8DTdWgPY" + }, + "source": [ + "### Set Google Cloud project information\n", + "\n", + "To get started using Vertex AI, you must have an existing Google Cloud project and [enable the Vertex AI API](https://console.cloud.google.com/flows/enableapi?apiid=aiplatform.googleapis.com).\n", + "\n", + "Learn more about [setting up a project and a development environment](https://cloud.google.com/vertex-ai/docs/start/cloud-environment)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "Nqwi-5ufWp_B" + }, + "outputs": [], + "source": [ + "# Use the environment variable if the user doesn't provide Project ID.\n", + "import os\n", + "\n", + "import vertexai\n", + "\n", + "PROJECT_ID = \"[your-project-id]\" # @param {type: \"string\", placeholder: \"[your-project-id]\", isTemplate: true}\n", + "\n", + "if not PROJECT_ID or PROJECT_ID == \"[your-project-id]\":\n", + " PROJECT_ID = os.environ.get(\"GOOGLE_CLOUD_PROJECT\")\n", + " if PROJECT_ID:\n", + " PROJECT_ID = str(PROJECT_ID)\n", + "\n", + "LOCATION = os.environ.get(\"GOOGLE_CLOUD_REGION\", \"us-central1\")\n", + "\n", + "vertexai.init(project=PROJECT_ID, location=LOCATION)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "haaUJ5VaWajl" + }, + "source": [ + "### Import libraries" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "kCp1oCOyWfZe" + }, + "outputs": [], + "source": [ + "import os\n", + "import re\n", + "\n", + "from vertexai import model_garden" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "fvVKEpROoXW5" + }, + "source": [ + "### Define helpers" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "9AoCHkCNoaLG" + }, + "outputs": [], + "source": [ + "def print_models(data_list: list[str], items_per_line: int = 2) -> None:\n", + " \"\"\"Prints the list with a specified number of items per line with index and emojis,\n", + " and includes the total count.\"\"\"\n", + " print(\"🌟--- Models available ---🌟\")\n", + " print(\"\\n\")\n", + " print(f\"🔱 Total models: {len(data_list)} 🔱\\n\") # Print the count here\n", + "\n", + " for i, item in enumerate(data_list):\n", + " print(f\"✹ {item} \", end=\"\")\n", + " if (i + 1) % items_per_line == 0:\n", + " print()\n", + " else:\n", + " print(\" --- \", end=\"\")\n", + "\n", + " if len(data_list) % items_per_line != 0:\n", + " print()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "dJmOAajdW7qQ" + }, + "source": [ + "## Find the models that you can deploy\n", + "\n", + "In Vertex AI Model Garden, you can discover and deploy a wide range of open-source models.\n", + "\n", + "Many of these models are directly supported in Vertex AI Model Garden with some pre-configured for optimized deployment on Vertex AI. When the open model is not available in Vertex AI Model Garden or you want to deploy your model from HF hub, you can leverage the Hugging Face gallery which gives you access to more that 1M models.\n", + "\n", + "With the Vertex AI Model Garden SDK, you can browse and deploy supported models, including those from the Hugging Face gallery. You can also filter by model name or type to find the best fit for your use case.\n", + "\n", + "Let's explore which Llama models are available in Vertex AI Model Garden." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "2l5LsbqTevUr" + }, + "outputs": [], + "source": [ + "model_garden_models = model_garden.list_deployable_models(\n", + " model_filter=\"Llama\", list_hf_models=False\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "z5Fa1spZr2YZ" + }, + "outputs": [], + "source": [ + "print_models(model_garden_models, items_per_line=3)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "EWRuFtX4iXW4" + }, + "source": [ + "To include Llama models that are available via Hugging Face Gallery, you can enable `list_hf_models` flag." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "e9zjeZ-rrPPk" + }, + "outputs": [], + "source": [ + "deployable_models = model_garden.list_deployable_models(\n", + " model_filter=\"llama\", list_hf_models=True\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "On3fzp-erTse" + }, + "outputs": [], + "source": [ + "print_models(deployable_models)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "3BOPydegEkP2" + }, + "source": [ + "## Deploy your 1st Model Garden model\n", + "\n", + "In this section, you'll deploy an open-source model from Vertex AI Model Garden to a public endpoint. You can use the Cloud Console or Vertex AI SDK for deployment.\n", + "\n", + "For more advanced workflows (e.g., custom hardware, model tuning, or shared endpoints), refer to the [official SDK notebook](https://github.com/GoogleCloudPlatform/generative-ai/blob/main/open-models/get_started_with_model_garden_sdk.ipynb)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "4qGzyKIPrkFE" + }, + "outputs": [], + "source": [ + "model_id = \"meta/llama3_1@llama-3.1-8b-instruct\"\n", + "\n", + "model = model_garden.OpenModel(model_id)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "vSFfpZ7Nxlkt" + }, + "source": [ + "### Check the deployment configuration" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "cA_yIRK-t_2T" + }, + "source": [ + "After you initiate the model, use `list_deploy_options()` method to discover the verified deployment configurations supported by a specific model.\n", + "\n", + "This is important to verify if you have enough resources to deploy the model." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "PaOvRoFZt9T1" + }, + "outputs": [], + "source": [ + "deploy_options = model.list_deploy_options(concise=True)\n", + "print(deploy_options)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "C3OKT16hugDY" + }, + "source": [ + "### Deploy the model\n", + "\n", + "Now that you know how the model will be deployed, let's use the `deploy()` method to serve the selected open model to a Vertex AI Endpoint. Depending on the model, the deployment would require some minutes.\n", + "\n", + "> **Note**: If the model has an End User License Agreement (EULA), you can accept it using `accept_eula` flag.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "0YqJ9pKi0cjz" + }, + "outputs": [], + "source": [ + "endpoint = model.deploy(\n", + " accept_eula=True,\n", + " serving_container_image_uri=\"us-docker.pkg.dev/vertex-ai/vertex-vision-model-garden-dockers/pytorch-vllm-serve:20241016_0916_RC00_maas\",\n", + " machine_type=\"g2-standard-12\",\n", + " accelerator_type=\"NVIDIA_L4\",\n", + " accelerator_count=1,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "sai-h7DryWKi" + }, + "source": [ + "## LiteLLM Inference" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Set up LiteLLM\n", + "\n", + "Configure [LiteLLM](https://docs.litellm.ai/) to route OpenAI-compatible API calls (e.g. chat, function calling) to your Vertex AI endpoint. Use the `vertex_ai/openai/{endpoint_id}` format to enable OpenAI-style behavior. Other formats like `vertex_ai/gemini/{endpoint}` are also supported — see the full list [here](https://litellm-api.up.railway.app/)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def _setup_model_garden_endpoint(endpoint_resource_name: str) -> str:\n", + " \"\"\"Sets up the model garden endpoint by extracting project, location, and endpoint ID.\n", + "\n", + " Args:\n", + " endpoint_resource_name: The full resource name of the endpoint,\n", + " e.g., \"projects/{project}/locations/{location}/endpoints/{endpoint}\".\n", + "\n", + " Returns:\n", + " A formatted string for the endpoint, e.g., \"vertex_ai/openai/{endpoint_id}\".\n", + "\n", + " Raises:\n", + " ValueError: If the endpoint_resource_name does not match the expected format.\n", + " \"\"\"\n", + " _MODEL_GARDEN_ENDPOINT_REGEX = r\"^projects/([^/]+)/locations/([^/]+)/endpoints/([^/]+)$\"\n", + "\n", + " match = re.fullmatch(_MODEL_GARDEN_ENDPOINT_REGEX, endpoint_resource_name)\n", + " if not match:\n", + " raise ValueError(\n", + " f\"Invalid model garden endpoint: '{endpoint_resource_name}'. \"\n", + " \"Please use the format 'projects/{{project}}/locations/{{location}}/endpoints/{{endpoint}}'.\"\n", + " )\n", + "\n", + " project_id, location_id, endpoint_id = match.groups()\n", + "\n", + " os.environ.setdefault(\"GOOGLE_GENAI_USE_VERTEXAI\", \"True\")\n", + " os.environ[\"VERTEXAI_PROJECT\"] = project_id\n", + " os.environ[\"VERTEXAI_LOCATION\"] = location_id\n", + " os.environ[\"LITELLM_LOG\"] = \"DEBUG\" # Consider if this is always desired in production\n", + "\n", + " return f\"vertex_ai/openai/{endpoint_id}\"\n", + "\n", + "deployed_model = _setup_model_garden_endpoint(endpoint.resource_name)\n", + "print(f\"The current model is: {deployed_model}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Chat Completion via LiteLLM\n", + "\n", + "This section demonstrates how to send role-based chat messages to your Vertex AI–hosted model using LiteLLM’s `completion` API. For examples and formatting guidance, refer to the [LiteLLM input documentation](https://docs.litellm.ai/completion/input)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from litellm import completion\n", + "\n", + "response = completion(\n", + " model=deployed_model,\n", + " messages=[\n", + " {\"role\": \"system\", \"content\": \"You are a helpful assistant.\"},\n", + " {\"role\": \"user\", \"content\": \"What's the capital of Japan?\"}\n", + " ]\n", + ")\n", + "\n", + "print(response[\"choices\"][0][\"message\"][\"content\"])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Function Calling via LiteLLM\n", + "\n", + "Here we use OpenAI-style function calling with LiteLLM and a Vertex AI–hosted model. You’ll define a function schema, and the model will return structured arguments suitable for downstream execution. For reference, see [`test_parallel_function_call`](https://docs.litellm.ai/docs/completion/function_call#full-code---parallel-function-calling-with-gpt-35-turbo-1106) in the LiteLLM documentation." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import google.auth\n", + "\n", + "creds, project = google.auth.default()\n", + "auth_req = google.auth.transport.requests.Request()\n", + "creds.refresh(auth_req)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import litellm\n", + "import json\n", + "\n", + "# set openai api key\n", + "import os\n", + "os.environ['OPENAI_API_KEY'] = creds.token\n", + "\n", + "# Example dummy function hard coded to return the same weather\n", + "# In production, this could be your backend API or an external API\n", + "def get_current_weather(location, unit=\"fahrenheit\"):\n", + " \"\"\"Get the current weather in a given location\"\"\"\n", + " if \"tokyo\" in location.lower():\n", + " return json.dumps({\"location\": \"Tokyo\", \"temperature\": \"10\", \"unit\": \"celsius\"})\n", + " elif \"san francisco\" in location.lower():\n", + " return json.dumps({\"location\": \"San Francisco\", \"temperature\": \"72\", \"unit\": \"fahrenheit\"})\n", + " elif \"paris\" in location.lower():\n", + " return json.dumps({\"location\": \"Paris\", \"temperature\": \"22\", \"unit\": \"celsius\"})\n", + " else:\n", + " return json.dumps({\"location\": location, \"temperature\": \"unknown\"})\n", + "\n", + "\n", + "def test_parallel_function_call():\n", + " try:\n", + " # Step 1: send the conversation and available functions to the model\n", + " messages = [{\"role\": \"user\", \"content\": \"What's the weather like in San Francisco, Tokyo, and Paris?\"}]\n", + " tools = [\n", + " {\n", + " \"type\": \"function\",\n", + " \"function\": {\n", + " \"name\": \"get_current_weather\",\n", + " \"description\": \"Get the current weather in a given location\",\n", + " \"parameters\": {\n", + " \"type\": \"object\",\n", + " \"properties\": {\n", + " \"location\": {\n", + " \"type\": \"string\",\n", + " \"description\": \"The city and state, e.g. San Francisco, CA\",\n", + " },\n", + " \"unit\": {\"type\": \"string\", \"enum\": [\"celsius\", \"fahrenheit\"]},\n", + " },\n", + " \"required\": [\"location\"],\n", + " },\n", + " },\n", + " }\n", + " ]\n", + " response = litellm.completion(\n", + " model=deployed_model,\n", + " messages=messages,\n", + " tools=tools,\n", + " tool_choice=\"auto\", # auto is default, but we'll be explicit\n", + " )\n", + " print(\"\\nFirst LLM Response:\\n\", response)\n", + " response_message = response.choices[0].message\n", + " tool_calls = response_message.tool_calls\n", + "\n", + " print(\"\\nLength of tool calls\", len(tool_calls))\n", + "\n", + " # Step 2: check if the model wanted to call a function\n", + " if tool_calls:\n", + " # Step 3: call the function\n", + " # Note: the JSON response may not always be valid; be sure to handle errors\n", + " available_functions = {\n", + " \"get_current_weather\": get_current_weather,\n", + " } # only one function in this example, but you can have multiple\n", + " messages.append(response_message) # extend conversation with assistant's reply\n", + "\n", + " # Step 4: send the info for each function call and function response to the model\n", + " for tool_call in tool_calls:\n", + " function_name = tool_call.function.name\n", + " function_to_call = available_functions[function_name]\n", + " function_args = json.loads(tool_call.function.arguments)\n", + " function_response = function_to_call(\n", + " location=function_args.get(\"location\"),\n", + " unit=function_args.get(\"unit\"),\n", + " )\n", + " messages.append(\n", + " {\n", + " \"tool_call_id\": tool_call.id,\n", + " \"role\": \"tool\",\n", + " \"name\": function_name,\n", + " \"content\": function_response,\n", + " }\n", + " ) # extend conversation with function response\n", + " second_response = litellm.completion(\n", + " model=deployed_model,\n", + " messages=messages,\n", + " ) # get a new response from the model where it can see the function response\n", + " print(\"\\nSecond LLM response:\\n\", second_response)\n", + " return second_response\n", + " except Exception as e:\n", + " print(f\"Error occurred: {e}\")\n", + "\n", + "test_parallel_function_call()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "2a4e033321ad" + }, + "source": [ + "## Cleaning up" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "N5UrKKMifeGi" + }, + "outputs": [], + "source": [ + "delete_endpoints = True\n", + "\n", + "if delete_endpoints:\n", + " endpoint.delete(force=True)" + ] + } + ], + "metadata": { + "colab": { + "name": "get_started_with_model_garden_sdk.ipynb", + "toc_visible": true + }, + "kernelspec": { + "display_name": "Python 3", + "name": "python3" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} diff --git a/open-models/use-cases/vertex_ai_deepseek_smolagents.ipynb b/open-models/use-cases/vertex_ai_deepseek_smolagents.ipynb new file mode 100644 index 0000000..66b1def --- /dev/null +++ b/open-models/use-cases/vertex_ai_deepseek_smolagents.ipynb @@ -0,0 +1,1498 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "ur8xi4C7S06n" + }, + "outputs": [], + "source": [ + "# Copyright 2025 Google LLC\n", + "#\n", + "# Licensed under the Apache License, Version 2.0 (the \"License\");\n", + "# you may not use this file except in compliance with the License.\n", + "# You may obtain a copy of the License at\n", + "#\n", + "# https://www.apache.org/licenses/LICENSE-2.0\n", + "#\n", + "# Unless required by applicable law or agreed to in writing, software\n", + "# distributed under the License is distributed on an \"AS IS\" BASIS,\n", + "# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n", + "# See the License for the specific language governing permissions and\n", + "# limitations under the License." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "JAPoU8Sm5E6e" + }, + "source": [ + "# Build and deploy a Hugging Face smolagent using DeepSeek-r1 on Vertex AI\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + "
    \n", + " \n", + " \"Google
    Open in Colab\n", + "
    \n", + "
    \n", + " \n", + " \"Google
    Open in Colab Enterprise\n", + "
    \n", + "
    \n", + " \n", + " \"Vertex
    Open in Vertex AI Workbench\n", + "
    \n", + "
    \n", + " \n", + " \"GitHub
    View on GitHub\n", + "
    \n", + "
    \n", + "\n", + "
    \n", + "\n", + "\n", + "\n", + "Share to:\n", + "\n", + "\n", + " \"LinkedIn\n", + "\n", + "\n", + "\n", + " \"Bluesky\n", + "\n", + "\n", + "\n", + " \"X\n", + "\n", + "\n", + "\n", + " \"Reddit\n", + "\n", + "\n", + "\n", + " \"Facebook\n", + "" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "84f0f73a0f76" + }, + "source": [ + "| | |\n", + "|-|-|\n", + "| Author(s) | [Ivan Nardini](https://github.com/inardini) |" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "tvgnzT1CKxrO" + }, + "source": [ + "## Overview\n", + "\n", + "> [DeepSeek-R1 from DeepSeek](https://huggingface.co/deepseek-ai/DeepSeek-R1) is a powerful language model developed with a focus on enhancing reasoning capabilities. DeepSeek-R1-Zero, DeepSeek-R1, and a collection of six distilled, dense models derived from DeepSeek-R1. These distilled models, based on the popular Llama and Qwen architectures, offer a range of sizes and capabilities to suit diverse research needs.\n", + "\n", + "> [HuggingFace's smol-agents](https://huggingface.co/docs/smolagents/en/index) library provides a lightweight and flexible framework for building and experimenting with language agents.\n", + "\n", + "> [Vertex AI](https://cloud.google.com/vertex-ai/docs) provides a comprehensive platform for the entire machine learning lifecycle. It empowers you to build, train, and deploy ML models and AI applications, including customizing powerful large language models (LLMs).\n", + "\n", + "This notebook showcases how to deploy DeepSeek R1 Distill Qwen 7B from the Hugging Face Hub on Vertex AI using Vertex AI Model Garden. It also shows how to prototype and deploy a simple agent using HuggingFace's smol-agents library on Vertex AI Reasoning Engine.\n", + "\n", + "\n", + "By the end of this notebook, you will learn how to:\n", + "\n", + "- Register and deploy Deepseek-r1 from the Hugging Face Hub on Vertex AI\n", + "- Prototype and evaluate an Deepseek-r1 agent on Vertex AI Reasoning Engine\n", + "- Prototype and deploy an Deepseek-r1 agent on Vertex AI Reasoning Engine\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "61RBz8LLbxCR" + }, + "source": [ + "## Get started" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "No17Cw5hgx12" + }, + "source": [ + "### Install Vertex AI SDK and other required packages\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "tFy3H3aPgx12" + }, + "outputs": [], + "source": [ + "%pip install --upgrade --user --quiet \"google-cloud-aiplatform[reasoningengine, evaluation]\" \"openai\" \"smolagents\" \\\n", + " \"cloudpickle==3.0.0\" \\\n", + " \"pydantic>=2.10\" \\\n", + " \"requests\"" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "R5Xep4W9lq-Z" + }, + "source": [ + "### Restart runtime\n", + "\n", + "To use the newly installed packages in this Jupyter runtime, you must restart the runtime. You can do this by running the cell below, which restarts the current kernel.\n", + "\n", + "The restart might take a minute or longer. After it's restarted, continue to the next step." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "XRvKdaPDTznN" + }, + "outputs": [], + "source": [ + "import IPython\n", + "\n", + "app = IPython.Application.instance()\n", + "app.kernel.do_shutdown(True)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "SbmM4z7FOBpM" + }, + "source": [ + "
    \n", + "⚠ The kernel is going to restart. In Colab or Colab Enterprise, you might see an error message that says \"Your session crashed for an unknown reason.\" This is expected. Wait until it's finished before continuing to the next step. ⚠\n", + "
    \n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "dmWOrTJ3gx13" + }, + "source": [ + "### Authenticate your notebook environment (Colab only)\n", + "\n", + "If you're running this notebook on Google Colab, run the cell below to authenticate your environment." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "NyKGtVQjgx13" + }, + "outputs": [], + "source": [ + "import sys\n", + "\n", + "if \"google.colab\" in sys.modules:\n", + " from google.colab import auth\n", + "\n", + " auth.authenticate_user()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "WZeNZkeq3wS6" + }, + "source": [ + "### Authenticate your Hugging Face account\n", + "\n", + "Then you can install the `huggingface_hub` that comes with a CLI that will be used for the authentication with the token generated in advance. So that then the token can be safely retrieved via `huggingface_hub.get_token`.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "zB-DuCZj30hb" + }, + "outputs": [], + "source": [ + "from huggingface_hub import interpreter_login\n", + "\n", + "interpreter_login()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "jTYJgjRN34RH" + }, + "source": [ + "Read more about [Hugging Face Security](https://huggingface.co/docs/hub/en/security), specifically about [Hugging Face User Access Tokens](https://huggingface.co/docs/hub/en/security-tokens).\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "DF4l8DTdWgPY" + }, + "source": [ + "### Set Google Cloud project information and initialize Vertex AI SDK\n", + "\n", + "To get started using Vertex AI, you must have an existing Google Cloud project and [enable the Vertex AI API](https://console.cloud.google.com/flows/enableapi?apiid=aiplatform.googleapis.com).\n", + "\n", + "Learn more about [setting up a project and a development environment](https://cloud.google.com/vertex-ai/docs/start/cloud-environment)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "Nqwi-5ufWp_B" + }, + "outputs": [], + "source": [ + "# Use the environment variable if the user doesn't provide Project ID.\n", + "import os\n", + "\n", + "import vertexai\n", + "\n", + "PROJECT_ID = \"[your-project-id]\" # @param {type: \"string\", placeholder: \"[your-project-id]\", isTemplate: true}\n", + "\n", + "if not PROJECT_ID or PROJECT_ID == \"[your-project-id]\":\n", + " PROJECT_ID = str(os.environ.get(\"GOOGLE_CLOUD_PROJECT\"))\n", + "\n", + "LOCATION = os.environ.get(\"GOOGLE_CLOUD_REGION\", \"us-central1\")\n", + "\n", + "BUCKET_NAME = \"[your-bucket-name]\" # @param {type: \"string\", placeholder: \"[your-bucket-name]\", isTemplate: true}\n", + "\n", + "if not BUCKET_NAME or BUCKET_NAME == \"[your-bucket-name]\":\n", + " BUCKET_NAME = f\"{PROJECT_ID}-bucket\"\n", + "\n", + "BUCKET_URI = f\"gs://{BUCKET_NAME}\"\n", + "\n", + "! gsutil mb -p $PROJECT_ID -l $LOCATION $BUCKET_URI\n", + "\n", + "vertexai.init(project=PROJECT_ID, location=LOCATION, staging_bucket=BUCKET_URI)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "5303c05f7aa6" + }, + "source": [ + "## Import libraries" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "6fc324893334" + }, + "outputs": [], + "source": [ + "import random\n", + "import string\n", + "import threading\n", + "import time\n", + "\n", + "from IPython.display import HTML, Markdown, display\n", + "import google.auth\n", + "from google.auth import default\n", + "import google.auth.transport.requests\n", + "from google.cloud import aiplatform\n", + "from huggingface_hub import get_token\n", + "import openai\n", + "import pandas as pd\n", + "import plotly.graph_objects as go\n", + "from smolagents import ChatMessage, CodeAgent, Model\n", + "from smolagents.agents import ActionStep\n", + "from smolagents.tools import Tool\n", + "from vertexai.preview import reasoning_engines\n", + "from vertexai.preview.evaluation import EvalTask" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "H_PZhSWSqrA6" + }, + "source": [ + "## Helpers" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "R0uBN4Kpqtmv" + }, + "outputs": [], + "source": [ + "def get_id(length: int = 8) -> str:\n", + " \"\"\"Generate a uuid of a specified length (default=8).\"\"\"\n", + " return \"\".join(random.choices(string.ascii_lowercase + string.digits, k=length))\n", + "\n", + "\n", + "def parse_smolagents_output_to_dictionary(agent, agent_outcome):\n", + " \"\"\"\n", + " Parse SmolAgent output into a structured dictionary format.\n", + " \"\"\"\n", + "\n", + " final_output = {\"response\": str(agent_outcome), \"predicted_trajectory\": []}\n", + "\n", + " try:\n", + " # Access the agent's action logs\n", + " for log in agent.logs:\n", + " # First check if the log is an ActionStep\n", + " if isinstance(log, ActionStep):\n", + " # Then check if it has tool_calls\n", + " if hasattr(log, \"tool_calls\"):\n", + " for tool_call in log.tool_calls:\n", + " # Parse tool arguments - split by newline and create key-value pairs\n", + " args_list = [\n", + " arg.strip()\n", + " for arg in tool_call.arguments.split(\"\\n\")\n", + " if arg.strip()\n", + " ]\n", + " tool_args = {\n", + " f\"arg_{idx}\": arg\n", + " for idx, arg in enumerate(\n", + " args_list\n", + " ) # Using enumerate ensures sequential numbering\n", + " }\n", + "\n", + " # Create tool info dictionary\n", + " tool_info = {\n", + " \"tool_name\": tool_call.name,\n", + " \"tool_input\": tool_args,\n", + " }\n", + " final_output[\"predicted_trajectory\"].append(tool_info)\n", + "\n", + " except Exception as e:\n", + " final_output[\"error\"] = f\"Error parsing tools results: {str(e)}\"\n", + "\n", + " return final_output\n", + "\n", + "\n", + "def format_output_as_markdown(output: dict) -> str:\n", + " \"\"\"\n", + " Convert the output dictionary to a detailed execution report.\n", + "\n", + " Args:\n", + " output: Dictionary containing response and predicted trajectory\n", + "\n", + " Returns:\n", + " str: Formatted string with detailed execution information\n", + " \"\"\"\n", + " report = \"📊 Execution Report\\n\"\n", + " report += \"=\" * 50 + \"\\n\\n\"\n", + "\n", + " report += \"🎯 Final Result:\\n\"\n", + " report += f\"{output['response']}\\n\\n\"\n", + "\n", + " if output[\"predicted_trajectory\"]:\n", + " report += \"🔍 Execution Details:\\n\"\n", + " report += \"-\" * 50\n", + " for idx, call in enumerate(output[\"predicted_trajectory\"], 1):\n", + " report += f\"\\n📌 Operation {idx}:\\n\"\n", + " report += f\"Tool: {call['tool_name']}\\n\"\n", + " report += \"Args:\\n\"\n", + " for arg_name, command in call[\"tool_input\"].items():\n", + " report += f\" ▶ {command}\\n\"\n", + " report += \"-\" * 50 + \"\\n\"\n", + "\n", + " return report\n", + "\n", + "\n", + "def display_dataframe_rows(\n", + " df: pd.DataFrame,\n", + " columns: list[str] | None = None,\n", + " num_rows: int = 3,\n", + " display_drilldown: bool = False,\n", + ") -> None:\n", + " \"\"\"Displays a subset of rows from a DataFrame, optionally including a drill-down view.\"\"\"\n", + "\n", + " if columns:\n", + " df = df[columns]\n", + "\n", + " base_style = \"font-family: monospace; font-size: 14px; white-space: pre-wrap; width: auto; overflow-x: auto;\"\n", + " header_style = base_style + \"font-weight: bold;\"\n", + "\n", + " for _, row in df.head(num_rows).iterrows():\n", + " for column in df.columns:\n", + " display(\n", + " HTML(\n", + " f\"{column.replace('_', ' ').title()}: \"\n", + " )\n", + " )\n", + " display(HTML(f\"{row[column]}
    \"))\n", + "\n", + " display(HTML(\"
    \"))\n", + "\n", + " if (\n", + " display_drilldown\n", + " and \"predicted_trajectory\" in df.columns\n", + " and \"reference_trajectory\" in df.columns\n", + " ):\n", + " display_drilldown(row)\n", + "\n", + "\n", + "def display_eval_report(eval_result: pd.DataFrame) -> None:\n", + " \"\"\"Display the evaluation results.\"\"\"\n", + " metrics_df = pd.DataFrame.from_dict(eval_result.summary_metrics, orient=\"index\").T\n", + " display(Markdown(\"### Summary Metrics\"))\n", + " display(metrics_df)\n", + "\n", + " display(Markdown(\"### Row-wise Metrics\"))\n", + " display(eval_result.metrics_table)\n", + "\n", + "\n", + "def plot_bar_plot(\n", + " eval_result: pd.DataFrame, title: str, metrics: list[str] = None\n", + ") -> None:\n", + " fig = go.Figure()\n", + " data = []\n", + "\n", + " summary_metrics = eval_result.summary_metrics\n", + " if metrics:\n", + " summary_metrics = {\n", + " k: summary_metrics[k]\n", + " for k, v in summary_metrics.items()\n", + " if any(selected_metric in k for selected_metric in metrics)\n", + " }\n", + "\n", + " data.append(\n", + " go.Bar(\n", + " x=list(summary_metrics.keys()),\n", + " y=list(summary_metrics.values()),\n", + " name=title,\n", + " )\n", + " )\n", + "\n", + " fig = go.Figure(data=data)\n", + "\n", + " # Change the bar mode\n", + " fig.update_layout(barmode=\"group\")\n", + " fig.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "e43229f3ad4f" + }, + "source": [ + "## Set model\n", + "\n", + "Set the model ID from Hugging Face Hub. In this case, you use DeepSeek-R1-Distill-Qwen-7B, a dense model distilled from DeepSeek-R1 good at math." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "cf93d5f0ce00" + }, + "outputs": [], + "source": [ + "MODEL_ID = \"deepseek-ai/DeepSeek-R1-Distill-Qwen-7B\" # @param {type:\"string\", isTemplate: true}" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "H0Ze75ouQrdZ" + }, + "source": [ + "## Register and Deploy DeepSeek model on Vertex AI\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "dJsGNCv4T6R-" + }, + "source": [ + "### Register a DeepSeek model on Vertex AI Model Registry\n", + "\n", + "Deploying a DeepSeek model on Vertex AI begins with importing the model into the [Vertex AI Model Registry](https://www.google.com/search?q=model+registry+vertex+ai&oq=model+registry+vertex+ai&gs_lcrp=EgZjaHJvbWUqBwgAEAAYgAQyBwgAEAAYgAQyCggBEAAYgAQYogQyBggCEEUYPDIGCAMQRRg8MgYIBBBFGDwyBggFEEUYQDIGCAYQRRhAMgYIBxBFGEDSAQg2MzMxajBqN6gCALACAA&sourceid=chrome&ie=UTF-8), a central hub for managing your ML model lifecycle. This registry stores model configurations, enabling streamlined organization, tracking, and versioning. \n", + "\n", + "The `aiplatform.Model.upload` method specifies the display name, the serving container image URI (pointing to the vLLM inference container on Vertex AI Model Garden), and arguments for the vLLM API server. Key arguments include the model name, tensor parallelism size, maximum model length, and enforcement of eager execution.\n", + "\n", + "It also defines the serving container port, predict route, health route, and crucial environment variables, notably the Hugging Face token for downloading the model from the Hugging Face Hub.\n", + "\n", + "See the [vLLM documentation](https://docs.vllm.ai/en/latest/getting_started/quickstart.html#quickstart-online) and [aiplatform.Model.upload](https://cloud.google.com/python/docs/reference/aiplatform/latest/google.cloud.aiplatform.Model#google_cloud_aiplatform_Model_upload) Python reference for a complete list of arguments." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "Pc-Owc_7WiZb" + }, + "outputs": [], + "source": [ + "deepseek_model = aiplatform.Model.upload(\n", + " display_name=MODEL_ID.replace(\"/\", \"--\").lower(),\n", + " serving_container_image_uri=\"us-docker.pkg.dev/deeplearning-platform-release/vertex-model-garden/vllm-inference.cu121.0-6.ubuntu2204.py310\",\n", + " serving_container_args=[\n", + " \"python\",\n", + " \"-m\",\n", + " \"vllm.entrypoints.api_server\",\n", + " \"--host=0.0.0.0\",\n", + " \"--port=8080\",\n", + " f\"--model={MODEL_ID}\",\n", + " # Hugging Face configuration\n", + " \"--tensor-parallel-size=1\",\n", + " \"--max-model-len=16384\",\n", + " \"--enforce-eager\",\n", + " ],\n", + " serving_container_ports=[8080],\n", + " serving_container_predict_route=\"/generate\",\n", + " serving_container_health_route=\"/ping\",\n", + " serving_container_environment_variables={\n", + " \"HF_TOKEN\": get_token(),\n", + " \"DEPLOY_SOURCE\": \"notebook\",\n", + " },\n", + ")\n", + "deepseek_model.wait()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "c427c0a87016" + }, + "source": [ + "### Deploy DeepSeek model on Vertex AI Prediction\n", + "\n", + "After the model is registered on Vertex AI, you can deploy the model to an endpoint.\n", + "\n", + "First create the endpoint with aiplatform.Endpoint.create method. Then you deploys the model to this endpoint, specifying the machine type (`g2-standard-24`), accelerator type (`NVIDIA_L4`), and the number of accelerators (`2`).\n", + "\n", + "> This deployment configuration is based on [Vertex AI Model Garden](https://console.cloud.google.com/vertex-ai/model-garden/featured-partners/hugging-face). Be sure you have enough GPU quota for deploying the model.\n", + "\n", + "For more information on the supported `aiplatform.Model.deploy` arguments, you can check its [Python reference](https://cloud.google.com/python/docs/reference/aiplatform/latest/google.cloud.aiplatform.Model#google_cloud_aiplatform_Model_deploy)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "sxI3SsHxVYn4" + }, + "outputs": [], + "source": [ + "deepseek_endpoint = aiplatform.Endpoint.create(\n", + " display_name=MODEL_ID.replace(\"/\", \"--\").lower() + \"-endpoint\"\n", + ")\n", + "\n", + "deployed_deepseek_model = deepseek_model.deploy(\n", + " endpoint=deepseek_endpoint,\n", + " machine_type=\"g2-standard-12\",\n", + " accelerator_type=\"NVIDIA_L4\",\n", + " accelerator_count=1,\n", + " sync=False,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "UEY206Oj0U2G" + }, + "source": [ + "> Note that the model deployment on Vertex AI can take around 20 minutes to get deployed.\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "8lIr8uko04T_" + }, + "source": [ + "### Generate predictions with Vertex AI API\n", + "\n", + "After deploying the model, you can use the `aiplatform.Endpoint.predict` method to generate online predictions. This sends requests to the deployed endpoint, utilizing the `/predict` route defined within the container and adhering to Vertex AI's input/output payload formatting requirements.\n", + "\n", + "> Note the instance request format is aligned the [vLLM OpenAI Completions API interface](https://docs.vllm.ai/en/latest/getting_started/quickstart.html#openai-completions-api-with-vllm)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "6lu1C-nXSvH-" + }, + "outputs": [], + "source": [ + "prediction_request = {\n", + " \"instances\": [\n", + " {\n", + " \"@requestFormat\": \"chatCompletions\",\n", + " \"messages\": [\n", + " {\n", + " \"role\": \"user\",\n", + " \"content\": \"Count the number of 'r' in the word Strawberry\",\n", + " }\n", + " ],\n", + " \"max_tokens\": 2048,\n", + " \"temperature\": 0.7,\n", + " }\n", + " ]\n", + "}\n", + "\n", + "output = deployed_deepseek_model.predict(instances=prediction_request[\"instances\"])\n", + "for prediction in output.predictions[0]:\n", + " print(\"------- DeepSeek prediction -------\")\n", + " print(prediction[\"message\"][\"content\"])\n", + " print(\"---------------------------------\\n\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "iljyx7y5Nbl1" + }, + "source": [ + "## Build a simple math agent with Hugging Face's smolagents\n", + "\n", + "With your DeepSeek model now deployed on Vertex AI, let's leverage its mathematical capabilities. The `deepseek-ai/DeepSeek-R1-Distill-Qwen-7B` excels at mathematical reasoning, making it an ideal tool for an agent designed to verify math results. \n", + "\n", + "Let's create a simple agent that combines the strengths of Gemini's function calling for orchestration and answer generation with DeepSeek's verification abilities on Vertex AI. This agent will use Hugging Face's smol-agents library." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "jZ5l0fTr3wAu" + }, + "source": [ + "### Create a VertexAIServerModel class\n", + "\n", + "To integrate Gemini with Vertex AI for agent development, a custom [Model](https://huggingface.co/docs/smolagents/v1.5.0/en/reference/agents#models) class is required. This class will represent the Gemini text generation model, serving as the engine for your agent." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "T_yzMnYZ4tz8" + }, + "source": [ + "> Note the code is based on the official [Model](https://github.com/huggingface/smolagents/blob/main/src/smolagents/models.py) implementation." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "kvClfddqLxcR" + }, + "outputs": [], + "source": [ + "class VertexAIServerModel(Model):\n", + " \"\"\"This model connects to a Vertex AI-compatible API server.\"\"\"\n", + "\n", + " def __init__(\n", + " self, model_id: str, project_id: str, location: str, endpoint_id: str, **kwargs\n", + " ):\n", + " # Try to import dependencies\n", + " try:\n", + " from google.auth import default\n", + " except ModuleNotFoundError:\n", + " raise ModuleNotFoundError(\n", + " \"Please install 'openai, google-auth and requests' extra to use VertexAIGeminiModel as described in the official documentation: https://cloud.google.com/vertex-ai/generative-ai/docs/multimodal/call-vertex-using-openai-library\"\n", + " ) from None\n", + "\n", + " # Initialize parent class with any additional keyword arguments\n", + " super().__init__(**kwargs)\n", + " self.model_id = model_id\n", + " self.project_id = project_id\n", + " self.location = location\n", + " self.endpoint_id = endpoint_id\n", + " self.kwargs = kwargs\n", + " self._refresh_task = None\n", + "\n", + " # Initialize credentials and set up Google Cloud authentication with required permissions\n", + " self.credentials, _ = default(\n", + " scopes=[\"https://www.googleapis.com/auth/cloud-platform\"]\n", + " )\n", + " self._refresh_token()\n", + " self._setup_client()\n", + " self._start_refresh_loop()\n", + "\n", + " def __call__(\n", + " self,\n", + " messages: list[dict[str, str]],\n", + " **kwargs,\n", + " ) -> ChatMessage:\n", + "\n", + " # Prepare the API call parameters\n", + " completion_kwargs = self._prepare_completion_kwargs(\n", + " messages=messages,\n", + " model=self.model_id,\n", + " **self.kwargs,\n", + " )\n", + "\n", + " # Make the API call to Vertex AI\n", + " response = self.client.chat.completions.create(**completion_kwargs)\n", + " self.last_input_token_count = response.usage.prompt_tokens\n", + " self.last_output_token_count = response.usage.completion_tokens\n", + "\n", + " # Convert API response to ChatMessage format\n", + " message = ChatMessage.from_dict(\n", + " response.choices[0].message.model_dump(\n", + " include={\"role\", \"content\", \"tool_calls\"}\n", + " )\n", + " )\n", + " return message\n", + "\n", + " def _refresh_token(self):\n", + " \"\"\"Refresh the Google Cloud token\"\"\"\n", + " try:\n", + " self.credentials.refresh(google.auth.transport.requests.Request())\n", + " self._setup_client()\n", + " except Exception as e:\n", + " print(f\"Token refresh failed: {e}\")\n", + "\n", + " def _setup_client(self):\n", + " \"\"\"Setup OpenAI client with current credentials\"\"\"\n", + " self.client = openai.OpenAI(\n", + " base_url=f\"https://{self.location}-aiplatform.googleapis.com/v1beta1/projects/{self.project_id}/locations/{self.location}/endpoints/{self.endpoint_id}\",\n", + " api_key=self.credentials.token,\n", + " )\n", + "\n", + " def _start_refresh_loop(self):\n", + " \"\"\"Start the token refresh loop\"\"\"\n", + "\n", + " def refresh_loop():\n", + " while True:\n", + " time.sleep(3600)\n", + " self._refresh_token()\n", + "\n", + " self._refresh_thread = threading.Thread(target=refresh_loop, daemon=True)\n", + " self._refresh_thread.start()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "9HX1BrGz30_i" + }, + "source": [ + "### Create a math tool using a DeepSeek model\n", + "\n", + "In the context of language agents, a tool is a self-contained function the agent can utilize. For a language model to effectively use a tool, the tool must have a well-defined API, including a name, a concise description, specifications for input types and their descriptions, and a defined output type. \n", + "\n", + "To integrate our deployed DeepSeek model on Vertex AI as a tool within a smol-agents framework, a custom [Tool](https://huggingface.co/docs/smolagents/en/guided_tour#tools) class is required. This class will represent the DeepSeek model, serving as the mean to take action for your agent. In this case, the tool would verify math results.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "T6wciAad4MAB" + }, + "outputs": [], + "source": [ + "class DeepSeekMathVerifierTool(Tool):\n", + " \"\"\"A tool that verifies math responses\"\"\"\n", + "\n", + " name = \"math_verifier\"\n", + " description = \"\"\"This is a tool that verifies math responses\"\"\"\n", + " inputs = {\n", + " \"content\": {\n", + " \"type\": \"string\",\n", + " \"description\": \"a text containing math\",\n", + " }\n", + " }\n", + " output_type = \"string\"\n", + "\n", + " def __init__(self, project_id: str, location: str, endpoint_id: str, **kwargs):\n", + " try:\n", + " from google.cloud import aiplatform\n", + " import vertexai\n", + " except ModuleNotFoundError:\n", + " raise ModuleNotFoundError(\n", + " \"Please install 'vertexai' and 'google-cloud-aiplatform' extra to use DeepSeekMathVerifierTool\"\n", + " ) from None\n", + "\n", + " super().__init__()\n", + " self.endpoint_id = endpoint_id\n", + " self.project_id = project_id\n", + " self.location = location\n", + " self.kwargs = kwargs\n", + " self._refresh_task = None\n", + "\n", + " # Initialize credentials and set up Google Cloud authentication with required permissions\n", + " self.credentials, _ = default(\n", + " scopes=[\"https://www.googleapis.com/auth/cloud-platform\"]\n", + " )\n", + " self._refresh_token()\n", + " self._start_refresh_loop()\n", + "\n", + " # Initialize Vertex ai session and the endpoint\n", + " vertexai.init(\n", + " project=self.project_id,\n", + " location=self.location,\n", + " credentials=self.credentials,\n", + " **self.kwargs,\n", + " )\n", + " self.endpoint = aiplatform.Endpoint(\n", + " endpoint_name=f\"projects/{self.project_id}/locations/{self.location}/endpoints/{self.endpoint_id}\"\n", + " )\n", + "\n", + " def forward(self, content: str):\n", + " \"\"\"Submit the prediction request\"\"\"\n", + " content = str(content)\n", + " prediction_request = {\n", + " \"instances\": [\n", + " {\n", + " \"@requestFormat\": \"chatCompletions\",\n", + " \"messages\": [{\"role\": \"user\", \"content\": content}],\n", + " }\n", + " ]\n", + " }\n", + "\n", + " try:\n", + " output = self.endpoint.predict(instances=prediction_request[\"instances\"])\n", + " except Exception as e:\n", + " print(f\"Prediction failed: {e}\")\n", + " return None\n", + " prediction = output.predictions[0][0][\"message\"][\"content\"]\n", + " return prediction\n", + "\n", + " def _refresh_token(self):\n", + " \"\"\"Refresh the Google Cloud token\"\"\"\n", + " try:\n", + " self.credentials.refresh(google.auth.transport.requests.Request())\n", + " except Exception as e:\n", + " print(f\"Token refresh failed: {e}\")\n", + "\n", + " def _start_refresh_loop(self):\n", + " \"\"\"Start the token refresh loop\"\"\"\n", + "\n", + " def refresh_loop():\n", + " while True:\n", + " time.sleep(3600)\n", + " self._refresh_token()\n", + "\n", + " self._refresh_thread = threading.Thread(target=refresh_loop, daemon=True)\n", + " self._refresh_thread.start()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "gDoVJbEo920U" + }, + "source": [ + "### Assemble the agent\n", + "\n", + "Having defined both the model and the tool, we can now assemble a basic agent. \n", + "\n", + "`smolagents` provides a default implementation called `CodeAgent`, which is designed to write and execute Python code at each step of its process. \n", + "\n", + "For more detailed information on agent construction and capabilities, refer to the `smolagents` [Agent](https://huggingface.co/docs/smolagents/en/guided_tour#codeagent-and-toolcallingagent) documentation.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "mqZRJTL96OIa" + }, + "outputs": [], + "source": [ + "endpoint_id = next(\n", + " (\n", + " endpoint.name\n", + " for endpoint in aiplatform.Endpoint.list()\n", + " if endpoint.display_name == MODEL_ID.replace(\"/\", \"--\").lower() + \"-endpoint\"\n", + " ),\n", + " None,\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "OO4-Smd3C63D" + }, + "outputs": [], + "source": [ + "model = VertexAIServerModel(\n", + " model_id=\"google/gemini-2.0-flash\",\n", + " endpoint_id=\"openapi\",\n", + " project_id=PROJECT_ID,\n", + " location=LOCATION,\n", + ")\n", + "\n", + "tools = [\n", + " DeepSeekMathVerifierTool(\n", + " endpoint_id=endpoint_id, project_id=PROJECT_ID, location=LOCATION\n", + " )\n", + "]\n", + "\n", + "agent = CodeAgent(model=model, tools=tools, add_base_tools=False)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "CKBraQd5GefX" + }, + "source": [ + "### Test the agent\n", + "\n", + "After you assemble the agent, you are now able to test it." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "cITl7egiTWl-" + }, + "outputs": [], + "source": [ + "response = agent.run(\"Hello! How are you?\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "x7MlhZZUyxgy" + }, + "outputs": [], + "source": [ + "print(format_output_as_markdown(parse_smolagents_output_to_dictionary(agent, response)))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "WtNKWuWwDE4Q" + }, + "outputs": [], + "source": [ + "response = agent.run(\n", + " \"Count the number of 'r' in the word Strawberry. Verify the answer\"\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "aWly89543ZR8" + }, + "outputs": [], + "source": [ + "print(format_output_as_markdown(parse_smolagents_output_to_dictionary(agent, response)))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "Ui_tK-3d8D4E" + }, + "source": [ + "## Evaluate the smolagent with Vertex AI Gen AI Evaluation\n", + "\n", + "Building effective AI agents requires careful performance evaluation. This involves two key practices: monitoring and observability. Monitoring focuses on task-specific performance: how well an agent executes individual actions. Observability provides a broader view, assessing the agent's overall health and efficiency. \n", + "\n", + "The [Vertex AI Gen AI Evaluation service](https://cloud.google.com/blog/products/ai-machine-learning/introducing-agent-evaluation-in-vertex-ai-gen-ai-evaluation-service?e=48754805) streamlines both monitoring and observability, offering pre-built criteria and metrics applicable from prototyping to production. This allows you to gain deep insights into agent performance, pinpoint areas for improvement, and optimize your AI solutions. Explore the documentation for details on available evaluation tools." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "A34b-M15-Ow7" + }, + "source": [ + "### Prepare Agent Evaluation dataset\n", + "\n", + "To evaluate your AI agent using the Vertex AI Gen AI Evaluation service, you need a specific dataset depending on what aspects you want to evaluate of your agent as shown below." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "XwrvCom18FXY" + }, + "outputs": [], + "source": [ + "eval_data = {\n", + " \"prompt\": [\n", + " \"Count the number of 'r' in the word Strawberry. Verify the answer\",\n", + " \"How many times does the digit '2' appear in the number 2,222,222? Verify your answer.\",\n", + " \"Count the number of words with more than five letters in this sentence: `The quick brown fox jumps over the lazy dog.` Verify your answer.\",\n", + " ],\n", + " \"reference_trajectory\": [\n", + " [\n", + " {\n", + " \"tool_name\": \"python_interpreter\",\n", + " \"tool_input\": {\n", + " \"arg_0\": \"count = 0\",\n", + " \"arg_1\": 'for letter in \"Strawberry\":',\n", + " \"arg_2\": \"if letter == 'r':\",\n", + " \"arg_3\": \"count += 1\",\n", + " \"arg_4\": \"print(f\\\"There are {count} \\\\'r\\\\'s in the word Strawberry.\\\")\",\n", + " \"arg_5\": \"verification = math_verifier(content={\\\\'type\\\\': \\\\'string\\\\', \\\\'description\\\\': f\\\"There are {count} \\\\'r\\\\'s in the word Strawberry.\\\"})\",\n", + " \"arg_6\": \"final_answer(verification)\",\n", + " \"arg_7\": \"final_answer(verification)\",\n", + " },\n", + " }\n", + " ],\n", + " [\n", + " {\n", + " \"tool_name\": \"python_interpreter\",\n", + " \"tool_input\": {\n", + " \"arg_0\": \"count = 0\",\n", + " \"arg_1\": \"num_str = str(2222222)\",\n", + " \"arg_2\": \"for digit in num_str:\",\n", + " \"arg_3\": \"if digit == '2':\",\n", + " \"arg_4\": \"count += 1\",\n", + " \"arg_5\": 'print(f\"The digit 2 appears {count} times in the number 2,222,222.\")',\n", + " \"arg_6\": \"verification = math_verifier(content={'type': 'string', 'description': f\\\"The digit 2 appears {count} times in the number 2,222,222.\\\"})\",\n", + " \"arg_7\": \"final_answer(verification)\",\n", + " },\n", + " },\n", + " ],\n", + " [\n", + " {\n", + " \"tool_name\": \"python_interpreter\",\n", + " \"tool_input\": {\n", + " \"arg_0\": \"count = 0\",\n", + " \"arg_1\": 'sentence = \"The quick brown fox jumps over the lazy dog.\"',\n", + " \"arg_2\": \"words = sentence.split()\",\n", + " \"arg_3\": \"for word in words:\",\n", + " \"arg_4\": \"if len(word) > 5:\",\n", + " \"arg_5\": \"count += 1\",\n", + " \"arg_6\": 'print(f\"There are {count} words with more than five letters.\")',\n", + " \"arg_7\": \"verification = math_verifier(content={'type': 'string', 'description': f\\\"There are {count} words with more than five letters in the sentence.\\\"})\",\n", + " \"arg_8\": \"final_answer(verification)\",\n", + " },\n", + " }\n", + " ],\n", + " ],\n", + "}\n", + "\n", + "eval_sample_dataset = pd.DataFrame(eval_data)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "PQEI1EcfvFHb" + }, + "source": [ + "Print some samples from the dataset." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "EjsonqWWvIvE" + }, + "outputs": [], + "source": [ + "display_dataframe_rows(eval_sample_dataset, num_rows=3)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "htCrOS9fRVi8" + }, + "source": [ + "### Prepare an Agent function\n", + "\n", + "In this scenario with a custom agent, you need an agent function to parse the agent output and pass it to Vertex AI Gen AI Evaluation." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "GdO56MIDRZri" + }, + "outputs": [], + "source": [ + "def agent_parsed_response(input: str) -> dict:\n", + " \"\"\"Parse the agent output and pass it to Vertex AI Gen AI Evaluation.\"\"\"\n", + "\n", + " result = agent.run(input)\n", + "\n", + " # Parse function calls separately\n", + " agent_output = parse_smolagents_output_to_dictionary(agent, result)\n", + "\n", + " return agent_output" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "oEYmU2eJ7q-1" + }, + "source": [ + "### Run an evaluation task\n", + "\n", + "Once you've assembled your evaluation dataset, the next step is to select the appropriate metrics for assessing your agent's performance. A comprehensive overview of available metrics and their interpretations can be found in the [Evaluate Gen AI agents documentation](https://cloud.google.com/blog/products/ai-machine-learning/introducing-agent-evaluation-in-vertex-ai-gen-ai-evaluation-service?e=48754805).\n", + "\n", + "With your dataset and chosen metrics in hand, you're ready to launch your first agent evaluation job on Vertex AI. This is accomplished by initiating an EvalTask with your defined dataset and metrics, followed by executing the evaluate method. Vertex AI Gen AI evaluation seamlessly integrates with [Vertex AI Experiments](https://cloud.google.com/vertex-ai/docs/experiments/intro-vertex-ai-experiments), the platform's managed experiment tracking service, automatically logging your evaluation run as an experiment.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "wBD-4wpB7q-3" + }, + "outputs": [], + "source": [ + "EXPERIMENT_NAME = f\"evaluate-smolagent-deepseek-{get_id()}\"\n", + "EXPERIMENT_RUN_NAME = f\"response-and-tools-{get_id()}\"\n", + "\n", + "response_tool_metrics = [\n", + " \"trajectory_exact_match\",\n", + " \"trajectory_in_order_match\",\n", + " \"coherence\",\n", + "]\n", + "\n", + "response_eval_tool_task = EvalTask(\n", + " dataset=eval_data,\n", + " metrics=response_tool_metrics,\n", + " experiment=EXPERIMENT_NAME,\n", + ")\n", + "\n", + "response_eval_tool_result = response_eval_tool_task.evaluate(\n", + " experiment_run_name=EXPERIMENT_RUN_NAME,\n", + " runnable=agent_parsed_response,\n", + ")\n", + "\n", + "display_eval_report(response_eval_tool_result)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "9eU3LG6r7q-3" + }, + "source": [ + "### Visualize evaluation results\n", + "\n", + "Visualize evaluation result sample." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "pQFzmd2I7q-3" + }, + "outputs": [], + "source": [ + "display_dataframe_rows(response_eval_tool_result.metrics_table, num_rows=3)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "DJr8GqQKTpUa" + }, + "outputs": [], + "source": [ + "plot_bar_plot(\n", + " response_eval_tool_result,\n", + " title=\"Agent eval metrics\",\n", + " metrics=[f\"{metric}/mean\" for metric in response_tool_metrics],\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "4RK-hjsL97hH" + }, + "source": [ + "## Deploy the agent on Vertex AI Reasoning Engine\n", + "\n", + "Your agent prototype is running smoothly in Colab, but it's time to scale it for wider accessibility.\n", + "\n", + "[Reasoning Engine on Vertex AI](https://cloud.google.com/vertex-ai/generative-ai/docs/model-reference/reasoning-engine) provides a managed platform for creating and deploying advanced agent reasoning frameworks. \n", + "\n", + "This notebook's approach utilizes a custom application template within Reasoning Engine, which can be further extended with frameworks like smolagents.\n", + "\n", + "Let's explore how to deploy our smol-agents agent using Reasoning Engine on Vertex AI.\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "9c1IqyNOGbfz" + }, + "source": [ + "### Assemble the agent\n", + "\n", + "Define a SmolAgent class designed to interact with a DeepSeek model deployed on Vertex AI.\n", + "\n", + "The two main components for building a custom agent are `set_up` and query methods:\n", + "\n", + "- The `set_up` method instantiates the agent's core components: a VertexAIServerModel to connect to the deployed DeepSeek model, a DeepSeekMathVerifierTool for mathematical verification tasks, and a CodeAgent to orchestrate the model and tools.\n", + "\n", + "- The `query` method provides a simple interface for sending input to the agent and receiving its response, effectively triggering the agent's execution.\n", + "\n", + "To know more about custom agent, check out how to [customize an application template ](https://cloud.google.com/vertex-ai/generative-ai/docs/reasoning-engine/customize).\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "QaTZPdSB1Nbl" + }, + "outputs": [], + "source": [ + "class SmolAgent:\n", + "\n", + " def __init__(\n", + " self,\n", + " model_id: str,\n", + " endpoint_id: str,\n", + " tool_endpoint_id: str,\n", + " project_id: str,\n", + " location: str,\n", + " **kwargs,\n", + " ):\n", + " self.model_id = model_id\n", + " self.endpoint_id = endpoint_id\n", + " self.tool_endpoint_id = tool_endpoint_id\n", + " self.project_id = project_id\n", + " self.location = location\n", + " self.add_base_tools = False\n", + " self.kwargs = kwargs\n", + "\n", + " def set_up(self) -> None:\n", + " \"\"\"Set up the agent.\"\"\"\n", + "\n", + " self.model = VertexAIServerModel(\n", + " model_id=self.model_id,\n", + " endpoint_id=self.endpoint_id,\n", + " project_id=self.project_id,\n", + " location=self.location,\n", + " **self.kwargs,\n", + " )\n", + " self.tools = [\n", + " DeepSeekMathVerifierTool(\n", + " project_id=self.project_id,\n", + " location=self.location,\n", + " endpoint_id=self.tool_endpoint_id,\n", + " **self.kwargs,\n", + " )\n", + " ]\n", + " self.app = CodeAgent(\n", + " model=self.model,\n", + " tools=self.tools,\n", + " add_base_tools=self.add_base_tools,\n", + " **self.kwargs,\n", + " )\n", + "\n", + " def query(self, input: str):\n", + " \"\"\"Query the application.\"\"\"\n", + " return self.app.run(input)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "qSQunNwr95Du" + }, + "source": [ + "### Test the agent\n", + "\n", + "After you get the agent assembled, you can now test it locally to confirm its expected behavior." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "iatBy35eBmBc" + }, + "outputs": [], + "source": [ + "local_custom_agent = SmolAgent(\n", + " model_id=\"google/gemini-2.0-flash\",\n", + " endpoint_id=\"openapi\",\n", + " tool_endpoint_id=endpoint_id,\n", + " project_id=PROJECT_ID,\n", + " location=LOCATION,\n", + ")\n", + "local_custom_agent.set_up()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "VHoBatTRUA_4" + }, + "outputs": [], + "source": [ + "output = local_custom_agent.query(input=\"Hello! How are you?\")\n", + "print(output)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "5zLz5qWqfpjE" + }, + "outputs": [], + "source": [ + "output = local_custom_agent.query(\n", + " input=\"Count the number of 'r' in the word Strawberry. Verify the answer\"\n", + ")\n", + "print(output)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "wvHDdFB6UKBS" + }, + "source": [ + "### Deploy the SmolAgent\n", + "\n", + "Your `smol-agent` application is running smoothly locally—excellent! \n", + "\n", + "Let's now deploy it to Reasoning Engine on Vertex AI. This deployment will make your application accessible remotely, opening up possibilities for integration with broader systems and use as a standalone service." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "CJclYoSDUPo3" + }, + "outputs": [], + "source": [ + "local_custom_agent = SmolAgent(\n", + " model_id=\"google/gemini-2.0-flash\",\n", + " endpoint_id=\"openapi\",\n", + " tool_endpoint_id=endpoint_id,\n", + " project_id=PROJECT_ID,\n", + " location=LOCATION,\n", + ")\n", + "\n", + "remote_custom_agent = reasoning_engines.ReasoningEngine.create(\n", + " local_custom_agent,\n", + " requirements=[\n", + " \"google-cloud-aiplatform[reasoningengine]\",\n", + " \"openai\",\n", + " \"smolagents\",\n", + " \"cloudpickle==3.0.0\",\n", + " \"pydantic>=2.10\",\n", + " \"requests\",\n", + " ],\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "2NL5UL6jVT66" + }, + "source": [ + "### Call the agent\n", + "\n", + "Now that the agent is deployed, let's call the agent to answer our math questions." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "_HbvguziVVtw" + }, + "outputs": [], + "source": [ + "output = remote_custom_agent.query(\n", + " input=\"Count the number of 'r' in the word Strawberry. Verify the answer\"\n", + ")\n", + "print(\"Agent response:\", output)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "2a4e033321ad" + }, + "source": [ + "## Cleaning up" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "mYDB5a_MDWiO" + }, + "outputs": [], + "source": [ + "delete_bucket = False\n", + "delete_endpoint = False\n", + "delete_model = False\n", + "delete_remote_agent = False\n", + "\n", + "if delete_bucket:\n", + " ! gsutil rm -r $BUCKET_URI\n", + "if delete_endpoint:\n", + " deepseek_endpoint.delete(force=True)\n", + "if delete_model:\n", + " deepseek_model.delete()\n", + "if delete_remote_agent:\n", + " remote_custom_agent.delete()" + ] + } + ], + "metadata": { + "colab": { + "name": "vertex_ai_deepseek_smolagents.ipynb", + "toc_visible": true + }, + "kernelspec": { + "display_name": "Python 3", + "name": "python3" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} diff --git a/partner-models/README.md b/partner-models/README.md new file mode 100644 index 0000000..298f498 --- /dev/null +++ b/partner-models/README.md @@ -0,0 +1,5 @@ +# Partner Models + +This directory contains models and resources provided by Google Cloud partners. + +These models can be integrated with Vertex AI and other Google Cloud services to enhance your machine learning projects. Each subdirectory typically corresponds to a specific partner or model. diff --git a/partner-models/claude/claude_multimodal_function_calling.ipynb b/partner-models/claude/claude_multimodal_function_calling.ipynb new file mode 100644 index 0000000..39c2ad2 --- /dev/null +++ b/partner-models/claude/claude_multimodal_function_calling.ipynb @@ -0,0 +1,1030 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 61, + "metadata": { + "id": "FWwAUaf5Fi2_" + }, + "outputs": [], + "source": [ + "# Copyright 2024 Google LLC\n", + "#\n", + "# Licensed under the Apache License, Version 2.0 (the \"License\");\n", + "# you may not use this file except in compliance with the License.\n", + "# You may obtain a copy of the License at\n", + "#\n", + "# https://www.apache.org/licenses/LICENSE-2.0\n", + "#\n", + "# Unless required by applicable law or agreed to in writing, software\n", + "# distributed under the License is distributed on an \"AS IS\" BASIS,\n", + "# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n", + "# See the License for the specific language governing permissions and\n", + "# limitations under the License." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "5tmkeOihFiE2" + }, + "source": [ + "# Multimodal Function Calling with Claude Models\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + "
    \n", + " \n", + " \"Google
    Open in Colab\n", + "
    \n", + "
    \n", + " \n", + " \"Google
    Open in Colab Enterprise\n", + "
    \n", + "
    \n", + " \n", + " \"Vertex
    Open in Vertex AI Workbench\n", + "
    \n", + "
    \n", + " \n", + " \"GitHub
    View on GitHub\n", + "
    \n", + "
    \n", + "\n", + "
    \n", + "\n", + "Share to:\n", + "\n", + "\n", + " \"LinkedIn\n", + "\n", + "\n", + "\n", + " \"Bluesky\n", + "\n", + "\n", + "\n", + " \"X\n", + "\n", + "\n", + "\n", + " \"Reddit\n", + "\n", + "\n", + "\n", + " \"Facebook\n", + " " + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "hL_l-HtkF6la" + }, + "source": [ + "## Overview\n", + "\n", + "### Claude on Vertex AI\n", + "\n", + "Anthropic's Claude models on Vertex AI offer fully managed and serverless models as APIs. To use a Claude model on Vertex AI, send a request directly to the Vertex AI API endpoint.\n", + "\n", + "In this notebook you'll use multimodal prompting and function calling (tools use) with Anthropic Claude models. To learn more about function calling with Claude, [check out the Anthropic docs.](https://docs.anthropic.com/en/docs/build-with-claude/tool-use)\n", + "\n", + "### Available Anthropic Claude models\n", + "\n", + "#### Claude 3.5 Sonnet v2\n", + "The upgraded Claude 3.5 Sonnet is now state-of-the-art for a variety of tasks including real-world software engineering, enhanced agentic capabilities, and computer use.\n", + "\n", + "#### Claude 3.5 Sonnet\n", + "Anthropic's most powerful AI model. Claude 3.5 Sonnet outperforms competitor models and Claude 3 Opus at higher speeds and lower cost.\n", + "\n", + "#### Claude 3 Opus\n", + "Claude 3 Opus is Anthropic's second-most intelligent AI model, with top-level performance on highly complex tasks.\n", + "\n", + "#### Claude 3 Haiku\n", + "Anthropic Claude 3 Haiku is Anthropic's fastest, most compact vision and text model for near-instant responses to simple queries, meant for seamless AI experiences mimicking human interactions.\n", + "\n", + "#### Claude 3 Sonnet\n", + "Anthropic Claude 3 Sonnet is engineered to be dependable for scaled AI deployments across a variety of use cases.\n", + "\n", + "All Claude 3 models can process images and return text outputs, call tools, and feature a 200K context window" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "tjnAqz4BGOyk" + }, + "source": [ + "## Get started" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "VHxAl1WAGQq4" + }, + "source": [ + "Install the required packages" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "62b7f4adee9c" + }, + "outputs": [], + "source": [ + "%pip install --upgrade --user --quiet 'anthropic[vertex]' wikipedia" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "i0c93wc9Go-y" + }, + "source": [ + "### Restart runtime\n", + "\n", + "To use the newly installed packages in this Jupyter runtime, you must restart the runtime. You can do this by running the cell below, which restarts the current kernel.\n", + "\n", + "The restart might take a minute or longer. After it's restarted, continue to the next step." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "f3d98395d9a4" + }, + "outputs": [], + "source": [ + "import IPython\n", + "\n", + "app = IPython.Application.instance()\n", + "app.kernel.do_shutdown(True)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "TTbdL4vtGwSY" + }, + "source": [ + "
    \n", + "⚠ The kernel is going to restart. Wait until it's finished before continuing to the next step. ⚠\n", + "
    \n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "mv14NC0_Gyqk" + }, + "source": [ + "### Authenticate your notebook environment (Colab only)\n", + "\n", + "If you're running this notebook on Google Colab, run the cell below to authenticate your environment." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "o4rK7PYLXYhZ" + }, + "outputs": [], + "source": [ + "import sys\n", + "\n", + "if \"google.colab\" in sys.modules:\n", + " from google.colab import auth\n", + "\n", + " auth.authenticate_user()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "YtsDY4JCG1cf" + }, + "source": [ + "### Set Google Cloud project information\n", + "\n", + "To get started using Vertex AI, you must have an existing Google Cloud project and [enable the Vertex AI API](https://console.cloud.google.com/flows/enableapi?apiid=aiplatform.googleapis.com).\n", + "\n", + "Learn more about [setting up a project and a development environment](https://cloud.google.com/vertex-ai/docs/start/cloud-environment)." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": { + "id": "bJnsUWOrG3XN" + }, + "outputs": [], + "source": [ + "# Use the environment variable if the user doesn't provide Project ID.\n", + "import os\n", + "\n", + "# fmt: off\n", + "PROJECT_ID = \"[your-project-id]\" # @param {type: \"string\", placeholder: \"[your-project-id]\", isTemplate: true}\n", + "# fmt: on\n", + "if not PROJECT_ID or PROJECT_ID == \"[your-project-id]\":\n", + " PROJECT_ID = str(os.environ.get(\"GOOGLE_CLOUD_PROJECT\"))\n", + "\n", + "LOCATION = os.environ.get(\"GOOGLE_CLOUD_REGION\", \"us-east5\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "8VEAR8IEv-3Q" + }, + "source": [ + "### Set up Anthropic client" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "671a07af230b" + }, + "source": [ + "#### Select Claude model" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "909e2d6f176e" + }, + "source": [ + "Models have different location availability, so you'll need to select the model before you create the Anthropic client. You can see the [available regions for each model here.](https://cloud.google.com/vertex-ai/generative-ai/docs/partner-models/use-claude#regions)" + ] + }, + { + "cell_type": "code", + "execution_count": 41, + "metadata": { + "id": "28d272a965f8" + }, + "outputs": [], + "source": [ + "# fmt: off\n", + "MODEL = \"claude-3-5-sonnet-v2@20241022\" # @param [\"claude-3-5-sonnet-v2@20241022\", \"claude-3-5-sonnet@20240620\", \"claude-3-opus@20240229\", \"claude-3-haiku@20240307\", \"claude-3-sonnet@20240229\" ]\n", + "# fmt: on\n", + "if MODEL == \"claude-3-5-sonnet-v2@20241022\":\n", + " available_regions = [\"us-east5\"]\n", + "elif MODEL == \"claude-3-5-sonnet@20240620\":\n", + " available_regions = [\"us-east5\", \"europe-west1\", \"asia-southeast1\"]\n", + "elif MODEL == \"claude-3-opus@20240229\":\n", + " available_regions = [\"us-east5\"]\n", + "elif MODEL == \"claude-3-haiku@20240307\":\n", + " available_regions = [\"us-east5\", \"europe-west1\", \"asia-southeast1\"]\n", + "elif MODEL == \"claude-3-sonnet@20240229\":\n", + " available_regions = [\"us-east5\"]" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "94fb805e888b" + }, + "source": [ + "Next, choose from the available regions for the selected Claude model." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "58658cdacc51" + }, + "outputs": [], + "source": [ + "import ipywidgets as widgets\n", + "from IPython.display import display\n", + "\n", + "dropdown = widgets.Dropdown(\n", + " options=available_regions,\n", + " description=\"Select a location:\",\n", + " font_weight=\"bold\",\n", + " style={\"description_width\": \"initial\"},\n", + ")\n", + "\n", + "\n", + "def dropdown_eventhandler(change):\n", + " global LOCATION\n", + " if change[\"type\"] == \"change\" and change[\"name\"] == \"value\":\n", + " LOCATION = change.new\n", + " print(\"Selected:\", change.new)\n", + "\n", + "\n", + "LOCATION = dropdown.value\n", + "dropdown.observe(dropdown_eventhandler, names=\"value\")\n", + "display(dropdown)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "c755e985a4f1" + }, + "source": [ + "#### Enable Claude model and accept terms of service" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "b51cf10828e2" + }, + "source": [ + "Before you can send messages to Claude, you'll need to enable the model on Vertex AI. You can do this from the model card in [Vertex AI Model Garden.](https://console.cloud.google.com/vertex-ai/model-garden) You will search for the Claude model you want to use and select the blue ENABLE button.\n", + "\n", + "For example, here's the [link to enable Claude 3.2 Sonnet v2](https://console.cloud.google.com/vertex-ai/model-garden/questionnaire?model=publishers%2Fanthropic%2Fmodels%2Fclaude-3-5-sonnet-v2&mp=anthropic%2Fclaude-3-5-sonnet-v2.endpoints.mp-anthropic.cloud.goog). Note that if you do not enable the specific model you want to use and accept the terms of service, you will receive an error message when sending a prompt." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "3674328de61b" + }, + "source": [ + "#### Create Anthropic client" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "8SSrfXmiIfIq" + }, + "source": [ + "Now you can import the AnthropicVertex SDK and initialize the client." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": { + "id": "Vt4z00dGXbwo" + }, + "outputs": [], + "source": [ + "from anthropic import AnthropicVertex\n", + "\n", + "client = AnthropicVertex(region=LOCATION, project_id=PROJECT_ID)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "LkqV_DD0wEZ6" + }, + "source": [ + "### Multimodal prompting" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "JVz3xwl8YCte" + }, + "source": [ + "To start, create a multimodal prompt that contains an image.\n", + "\n", + "If you want to send an image in your prompt, you'll need to base64 encode it first. You can follow the steps in the cell below to do so." + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": { + "id": "K2CiOQtKY7lP" + }, + "outputs": [], + "source": [ + "import base64\n", + "\n", + "import httpx\n", + "import requests\n", + "from IPython.display import Image" + ] + }, + { + "cell_type": "code", + "execution_count": 43, + "metadata": { + "id": "Oy_v3g21Y3dk" + }, + "outputs": [], + "source": [ + "image_url = (\n", + " \"https://upload.wikimedia.org/wikipedia/commons/b/b7/Dendroica_chrysoparia1.jpg\"\n", + ")\n", + "image_media_type = \"image/jpeg\"\n", + "image_b64 = base64.b64encode(httpx.get(image_url).content).decode(\"utf-8\")\n", + "\n", + "headers = {\n", + " \"User-Agent\": \"Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Mobile Safari/537.36\",\n", + "}\n", + "\n", + "response = requests.get(image_url, headers=headers)\n", + "image = Image(response.content, width=300, height=200)\n", + "\n", + "image" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "kU91q-QkYcWj" + }, + "source": [ + "Next, define the user message to send to Claude. Because this is a multimodal message, it contains both `\"type\": \"image\"` and `\"type\": \"text\"`.\n", + "\n", + "The image content contains the `\"media_type\"`, in this case `\"image/jpeg\"`, as well as the base64 encoded image." + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "metadata": { + "id": "oDN8AGC3YSPC" + }, + "outputs": [], + "source": [ + "messages = [\n", + " {\n", + " \"role\": \"user\",\n", + " \"content\": [\n", + " {\n", + " \"type\": \"image\", # pass in the image\n", + " \"source\": {\n", + " \"type\": \"base64\",\n", + " \"media_type\": image_media_type,\n", + " \"data\": image_b64,\n", + " },\n", + " },\n", + " {\n", + " \"type\": \"text\", # pass in the text\n", + " \"text\": \"What is the region where this animal lives?\",\n", + " },\n", + " ],\n", + " }\n", + "]" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "suhbpNIzwlCP" + }, + "source": [ + "Once the message is defined, you can call `messages.create` to get a response from the model endpoint." + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": { + "id": "KJk_z8qGYelQ" + }, + "outputs": [], + "source": [ + "response = client.messages.create(\n", + " max_tokens=1024,\n", + " messages=messages,\n", + " model=MODEL,\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": { + "id": "wiLqdiH9aB60" + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{\n", + " \"id\": \"msg_vrtx_01PbLXjy7XzfNMiH76f3s5Ep\",\n", + " \"content\": [\n", + " {\n", + " \"text\": \"This appears to be a Golden-cheeked Warbler (Setophaga chrysoparia), which has a very limited breeding range. It breeds exclusively in central Texas in the Edwards Plateau region, making it the only bird species whose breeding range is entirely within Texas. During winter, these warblers migrate to southern Mexico and Central America (primarily in southern Mexico, Guatemala, Honduras, and Nicaragua).\\n\\nThe bird is highly specialized in its habitat requirements, preferring mature juniper-oak woodlands, particularly areas with Ashe juniper (also known as cedar) trees, which is clearly visible in this image where the bird is perched on what appears to be juniper foliage. This specific habitat requirement is one reason why the species is considered endangered, as urban development and habitat fragmentation in central Texas have significantly reduced its breeding grounds.\",\n", + " \"type\": \"text\"\n", + " }\n", + " ],\n", + " \"model\": \"claude-3-5-sonnet-v2-20241022\",\n", + " \"role\": \"assistant\",\n", + " \"stop_reason\": \"end_turn\",\n", + " \"stop_sequence\": null,\n", + " \"type\": \"message\",\n", + " \"usage\": {\n", + " \"input_tokens\": 1587,\n", + " \"output_tokens\": 179\n", + " }\n", + "}\n" + ] + } + ], + "source": [ + "print(response.model_dump_json(indent=2))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "_I0CZL5NxBee" + }, + "source": [ + "### Multimodal function calling" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "_lZmF9xqaM34" + }, + "source": [ + "What if you wanted Claude to retrieve specific information about the animal from a database instead of drawing on information from its training data?\n", + "\n", + "Combining multimodal prompting with function calling can unlock some really neat use cases (If you're new to the concept of function calling or connecting LLMs to tools, check out this [introductory concepts video](https://www.youtube.com/watch?v=P3buv6P_u7c)).\n", + "\n", + "As an example, let's say you want Claude to use a tool every time a user requests information about an animal's habitat. Instead of the answer coming from the model's training data, you want the model to pull the information from an external source of information accessible via an API. To do this, or use Claude with any other tools, there are four high level steps you'll follow." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "1f42f06465ea" + }, + "source": [ + "* **STEP 1:** Define tool and send a request to Claude\n", + "* **STEP 2:** Claude decides to use a tool\n", + "* **STEP 3:** Developer makes an API call to the tool\n", + "* **STEP 4:** Claude uses tool result to formulate a conversational response\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "d08dacffbed6" + }, + "source": [ + "**STEP 1: Define tool and send a request to Claude**" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "KJVqiPn2aVm-" + }, + "source": [ + "The first thing you need to do is write a tool definition. This is a dictionary with information like a name and description of the tool, as well as the required input schema. Anthropic's [docs have some great tips](https://docs.anthropic.com/en/docs/build-with-claude/tool-use#best-practices-for-tool-definitions) for crafting successful tool descriptions.\n", + "\n", + "Here, you define a tool called `get_wildlife_region`, with a simple description `Look up the region where an animal can be found.`" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "metadata": { + "id": "Vq_MUvHDZD6W" + }, + "outputs": [], + "source": [ + "TOOL = {\n", + " \"name\": \"get_wildlife_region\",\n", + " \"description\": \"Look up the region where an animal can be found\",\n", + " \"input_schema\": {\n", + " \"type\": \"object\",\n", + " \"properties\": {\n", + " \"animal\": {\"type\": \"string\", \"description\": \"species of animal\"},\n", + " },\n", + " },\n", + "}" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "kdqMkUNAaaaU" + }, + "source": [ + "Next, send the same message as earlier, which contained the image of the warbler and the text _\"What is the region where this animal lives?\"_, but this time add in the tool parameter and pass in the `TOOL` dictionary defined above.\n", + "\n", + "Note: for more information on sending messages to Claude, check out the [Messages API reference.](https://docs.anthropic.com/en/api/messages)" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "metadata": { + "id": "JN0j8YsNaYyO" + }, + "outputs": [], + "source": [ + "response = client.messages.create(\n", + " max_tokens=1024,\n", + " messages=messages,\n", + " tools=[TOOL], # pass in the tools!\n", + " model=MODEL,\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "metadata": { + "id": "lvRWjow9acTq" + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{\n", + " \"id\": \"msg_vrtx_015F4GZi1En2FBwsTdrtrc12\",\n", + " \"content\": [\n", + " {\n", + " \"text\": \"This appears to be a Golden-cheeked Warbler (Setophaga chrysoparia), a rare songbird. Let me look up its region for you.\",\n", + " \"type\": \"text\"\n", + " },\n", + " {\n", + " \"id\": \"toolu_vrtx_01AoZske3Zi12oYewSYnng2z\",\n", + " \"input\": {\n", + " \"animal\": \"Golden-cheeked Warbler\"\n", + " },\n", + " \"name\": \"get_wildlife_region\",\n", + " \"type\": \"tool_use\"\n", + " }\n", + " ],\n", + " \"model\": \"claude-3-5-sonnet-v2-20241022\",\n", + " \"role\": \"assistant\",\n", + " \"stop_reason\": \"tool_use\",\n", + " \"stop_sequence\": null,\n", + " \"type\": \"message\",\n", + " \"usage\": {\n", + " \"input_tokens\": 1963,\n", + " \"output_tokens\": 99\n", + " }\n", + "}\n" + ] + } + ], + "source": [ + "print(response.model_dump_json(indent=2))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "S4EKTDkyah8K" + }, + "source": [ + "**STEP 2: Claude decides to use a tool**\n", + "\n", + "Even though the message sent to Claude was the same, the response will look different now because you provided Claude with a list of tool(s)." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "A5YwIL7K0kqU" + }, + "source": [ + "The content returned from the model contains a `TextBlock` and a `ToolUseBlock`." + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "metadata": { + "id": "FswChxN-0jro" + }, + "outputs": [ + { + "data": { + "text/plain": [ + "[TextBlock(text='This appears to be a Golden-cheeked Warbler (Setophaga chrysoparia), a rare songbird. Let me look up its region for you.', type='text'),\n", + " ToolUseBlock(id='toolu_vrtx_01AoZske3Zi12oYewSYnng2z', input={'animal': 'Golden-cheeked Warbler'}, name='get_wildlife_region', type='tool_use')]" + ] + }, + "execution_count": 24, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "response.content" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "0ftyFbldak_R" + }, + "source": [ + "In the `TextBlock`, there's an explanation from Claude indicating that the model has identified a tool is necessary. Depending on the model you use, you'll see something like _\"Let me look up its region\"_ or _\"To answer your question about the typical habitat I'll need to use an available tool to look up its region\"_, etc.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 27, + "metadata": { + "id": "J8n7Qw6K0p4w" + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{\n", + " \"text\": \"This appears to be a Golden-cheeked Warbler (Setophaga chrysoparia), a rare songbird. Let me look up its region for you.\",\n", + " \"type\": \"text\"\n", + "}\n" + ] + } + ], + "source": [ + "textblock = response.content[0]\n", + "print(textblock.model_dump_json(indent=2))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "HPmXG9fh1LTr" + }, + "source": [ + "In the `ToolUseBlock` there's a tool `id`, and then the API parameters specified in the tool definition, in this case the `\"animal\" : \"Golden-cheeked Warbler\"`.\n", + "\n", + "At this point Claude has identified that calling the `get_wildlife_region` tool is necessary to respond to the user's query, but we as developers have to go do that ourselves. Claude can do a lot, but it isn't going to start actually making API calls on our behalf!" + ] + }, + { + "cell_type": "code", + "execution_count": 29, + "metadata": { + "id": "CPFb4b3g1EZk" + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{\n", + " \"id\": \"toolu_vrtx_01AoZske3Zi12oYewSYnng2z\",\n", + " \"input\": {\n", + " \"animal\": \"Golden-cheeked Warbler\"\n", + " },\n", + " \"name\": \"get_wildlife_region\",\n", + " \"type\": \"tool_use\"\n", + "}\n" + ] + } + ], + "source": [ + "tooluseblock = response.content[1]\n", + "print(tooluseblock.model_dump_json(indent=2))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "5a57261f0f91" + }, + "source": [ + "Note that in this example, you have only provided Claude with a single tool. However, you can define multiple tools when sending a message. Additionally, Claude supports [parallel tool use](https://docs.anthropic.com/en/docs/build-with-claude/tool-use#disabling-parallel-tool-use), meaning that the model may use multiple tools to answer a user query. For more details on configuring tools with Claude, [refer to the docs.](https://docs.anthropic.com/en/docs/build-with-claude/tool-use#controlling-claudes-output)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "Rl4icSkranVS" + }, + "source": [ + "**STEP 3: Developer makes an API call to the tool**\n", + "\n", + "Instead of creating an actual database of habitat information, you'll use the Wikipedia API to retrieve information about where the beloved warbler lives. \n" + ] + }, + { + "cell_type": "code", + "execution_count": 30, + "metadata": { + "id": "6zI-BXYvae-H" + }, + "outputs": [], + "source": [ + "import wikipedia\n", + "\n", + "animal_name = response.content[1].input[\"animal\"] # Golden-cheeked Warbler\n", + "api_response = wikipedia.page(animal_name).content" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "Nqf06f_la5mu" + }, + "source": [ + "The `api_response` is a huge blob of text from the Golden-cheeked Warbler Wikipedia page. The users of your wildlife chatbot would be pretty upset if you just dropped all of that text into the response box. So as a final step, you'll use Claude to format all of this information nicely and craft an eloquent response for the user" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "DgMXA9Wsa7Qs" + }, + "source": [ + "**STEP 4: Claude uses tool result to formulate a conversational response**\n", + "\n", + "The messages list defined in Step 1 currently contains a single message from `\"role\" : \"user\"`.\n", + "\n", + "Next, you'll add Claude's response from Step 2 to this list. To do this you'll append a new dictionary. This new dictionary has the `\"role\" : \"assistant\"` (instead of user). The value corresponding to the `content` key is the blob of text from Claude that contained the `TextBlock` and `ToolUseBlock`.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 31, + "metadata": { + "id": "An7kojsla9gt" + }, + "outputs": [], + "source": [ + "messages.append({\"role\": \"assistant\", \"content\": response.content})" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "z80yBfPea_gM" + }, + "source": [ + "Finally, you'll append one more dictionary to this messages list. This dictionary again will have the `\"role\" : \"user\"`, and the content will contain the tool id and api response (aka that big blob of Wikipedia text).\n" + ] + }, + { + "cell_type": "code", + "execution_count": 35, + "metadata": { + "id": "h2RUPDOVaqCW" + }, + "outputs": [], + "source": [ + "tool_use = response.content[-1]\n", + "\n", + "# create tool response message\n", + "tool_response = {\n", + " \"role\": \"user\",\n", + " \"content\": [\n", + " {\"type\": \"tool_result\", \"tool_use_id\": tool_use.id, \"content\": api_response}\n", + " ],\n", + "}" + ] + }, + { + "cell_type": "code", + "execution_count": 36, + "metadata": { + "id": "41eh4A0h3ktI" + }, + "outputs": [], + "source": [ + "# append tool_response dictionary to messages\n", + "messages.append(tool_response)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "e5kXNZeQbCw6" + }, + "source": [ + "If you're tracking this closely, you'll notice that the messages list now contains three dictionaries." + ] + }, + { + "cell_type": "code", + "execution_count": 37, + "metadata": { + "id": "OnK8_1sw3o29" + }, + "outputs": [ + { + "data": { + "text/plain": [ + "3" + ] + }, + "execution_count": 37, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "len(messages)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "7s5ye7LX3qMv" + }, + "source": [ + "These messages are:\n", + "\n", + "1. **[user]** Original user message, which included an image of the warbler and the text _\"What is the region where this animal lives?\"_\n", + "2. **[assistant]** The response from the model, which included a `TextBlock`, as well as the `ToolUseBlock` that indicated what tool to call and the value `Golden-Cheeked Warbler` for the animal parameter\n", + "3. **[user]** Then the tool response you just formatted, which is a dictionary with the tool id from the model as well as the big blog of text returned from the API call made to Wikipedia.\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "lGbvvoPrbEVy" + }, + "source": [ + "Take all three of these dictionaries, and send all of them as messages to Claude." + ] + }, + { + "cell_type": "code", + "execution_count": 38, + "metadata": { + "id": "mGkZP1hgbA32" + }, + "outputs": [], + "source": [ + "follow_up_response = client.messages.create(\n", + " model=MODEL,\n", + " messages=messages, # long list with three dictionaries\n", + " max_tokens=1000,\n", + " tools=[TOOL], # don't forget the tool!\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "XLA10cKx4QOb" + }, + "source": [ + "After all the hard work, Claude will responds with something fluent and conversational, based on info extracted from the tool." + ] + }, + { + "cell_type": "code", + "execution_count": 39, + "metadata": { + "id": "hOLydfiubJ3w" + }, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Based on the results, the Golden-cheeked Warbler is endemic to (meaning it exclusively breeds in) Central Texas, specifically in an area from Palo Pinto County southwestward along the eastern and southern edge of the Edwards Plateau to Kinney County. It's the only bird species with a breeding range that is completely endemic to Texas.\\n\\nDuring the breeding season (March to June), these warblers can be found in various Texas state parks including Colorado Bend, Dinosaur Valley, Garner, Guadalupe River, and several others. However, during winter months (November-February), they migrate to parts of Mexico and Central America, specifically Guatemala, Nicaragua, and Honduras.\\n\\nThe bird requires a very specific habitat of juniper-oak woodlands, particularly areas with mature Ashe juniper trees, which they use for nesting material. Their total breeding range is quite limited, covering only about 1,768 square kilometers (437,000 acres), which is one reason why this species is currently endangered.\"" + ] + }, + "execution_count": 39, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "follow_up_response.content[0].text" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "MTWqODRFbFg1" + }, + "source": [ + "đŸ„ł Congratulations! You've successfully used Claude with tools" + ] + } + ], + "metadata": { + "colab": { + "name": "claude_multimodal_function_calling.ipynb", + "toc_visible": true + }, + "kernelspec": { + "display_name": "Python 3", + "name": "python3" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} diff --git a/partner-models/claude/computer-use-demo/README.md b/partner-models/claude/computer-use-demo/README.md new file mode 100644 index 0000000..03e1ca4 --- /dev/null +++ b/partner-models/claude/computer-use-demo/README.md @@ -0,0 +1,136 @@ +# Anthropic Computer Use Demo on Google Cloud + +[![Deploy in Cloud Shell](https://gstatic.com/cloudssh/images/open-btn.svg)](https://shell.cloud.google.com/cloudshell/editor?cloudshell_git_repo=https%3A%2F%2Fgithub.com%2FGoogleCloudPlatform%2Fgenerative-ai&cloudshell_git_branch=main&cloudshell_workspace=partner-models%2Fclaude%2Fcomputer-use-demo&cloudshell_tutorial=partner-models%2Fclaude%2Fcomputer-use-demo%2FREADME.md) + +> [!CAUTION] +> Computer use is a beta feature. Please be aware that computer use poses unique risks that are distinct from standard API features or chat interfaces. These risks are heightened when using computer use to interact with the internet. To minimize risks, consider taking precautions such as: +> +> 1. Use a dedicated virtual machine or container with minimal privileges to prevent direct system attacks or accidents. +> 2. Avoid giving the model access to sensitive data, such as account login information, to prevent information theft. +> 3. Limit internet access to an allowlist of domains to reduce exposure to malicious content. +> 4. Ask a human to confirm decisions that may result in meaningful real-world consequences as well as any tasks requiring affirmative consent, such as accepting cookies, executing financial transactions, or agreeing to terms of service. +> In some circumstances, Claude will follow commands found in content even if it conflicts with the user's instructions. For example, instructions on webpages or contained in images may override user instructions or cause Claude to make mistakes. We suggest taking precautions to isolate Claude from sensitive data and actions to avoid risks related to prompt injection. +> Finally, please inform end users of relevant risks and obtain their consent prior to enabling computer use in your own products. + +This repository helps you get started with computer use on Claude, with reference implementations of: + +- Build files to create a Docker container with all necessary dependencies +- A computer use agent loop using the Vertex AI and GKE to access the updated Claude 3.5 Sonnet model +- Anthropic-defined computer use tools +- A streamlit app for interacting with the agent loop + +Please use [this form](https://forms.gle/BT1hpBrqDPDUrCqo7) to provide feedback on the quality of the model responses, the API itself, or the quality of the documentation - we cannot wait to hear from you! + +> [!IMPORTANT] +> The Beta API used in this reference implementation is subject to change. Please refer to the [API release notes](https://docs.anthropic.com/en/release-notes/api) for the most up-to-date information. +> The components are weakly separated: the agent loop runs in the container being controlled by Claude, can only be used by one session at a time, and must be restarted or reset between sessions if necessary. + +## Quickstart: Deploying the app on Google Cloud using GKE and Cloud Build + +We provide a simplified way to deploy this app on Google Cloud using Google Kubernetes Engine (GKE). To set this up, follow the steps below. + +1. Set up the environment variables + + ```bash + export PROJECT_ID=%your_project_id% + ``` + +2. Authenticate with Google Cloud + + ```bash + gcloud auth application-default login + ``` + +3. Create a Cloud Build job to deploy the app + + ```bash + gcloud builds submit --config cloudbuild.yaml + ``` + +4. [Optional] Delete all created resources + + ```bash + gcloud builds submit --config cloudbuild-destroy.yaml + ``` + +## Local development + +### Running the app locally + +You'll need to pass in Google Cloud credentials with appropriate permissions to use Claude on Vertex AI. + +```bash +docker build . -t computer-use-demo +gcloud auth application-default login +export VERTEX_REGION=%your_vertex_region% +export VERTEX_PROJECT_ID=%your_vertex_project_id% +docker run \ + -e API_PROVIDER=vertex \ + -e CLOUD_ML_REGION=$VERTEX_REGION \ + -e ANTHROPIC_VERTEX_PROJECT_ID=$VERTEX_PROJECT_ID \ + -v $HOME/.config/gcloud/application_default_credentials.json:/home/computeruse/.config/gcloud/application_default_credentials.json \ + -p 5900:5900 \ + -p 8501:8501 \ + -p 6080:6080 \ + -p 8080:8080 \ + -it computer-use-demo +``` + +Once the container is running, see the [Accessing the demo app](#accessing-the-demo-app) section below for instructions on how to connect to the interface. + +This example shows how to use the Google Cloud Application Default Credentials to authenticate with Vertex AI. + +You can also set `GOOGLE_APPLICATION_CREDENTIALS` to use an arbitrary credential file, see the [Google Cloud Authentication documentation](https://cloud.google.com/docs/authentication/application-default-credentials#GAC) for more details. + +### Accessing the demo app + +Once the container is running, open your browser to [http://localhost:8080](http://localhost:8080) to access the combined interface that includes both the agent chat and desktop view. + +The container stores settings like the API key and custom system prompt in `~/.anthropic/`. Mount this directory to persist these settings between container runs. + +Alternative access points: + +- Streamlit interface only: [http://localhost:8501](http://localhost:8501) +- Desktop view only: [http://localhost:6080/vnc.html](http://localhost:6080/vnc.html) +- Direct VNC connection: `vnc://localhost:5900` (for VNC clients) + +## Screen size + +Environment variables `WIDTH` and `HEIGHT` can be used to set the screen size. For example: + +```bash +docker run \ + -e API_PROVIDER=vertex \ + -e CLOUD_ML_REGION=$VERTEX_REGION \ + -e ANTHROPIC_VERTEX_PROJECT_ID=$VERTEX_PROJECT_ID \ + -v $HOME/.config/gcloud/application_default_credentials.json:/home/computeruse/.config/gcloud/application_default_credentials.json \ + -p 5900:5900 \ + -p 8501:8501 \ + -p 6080:6080 \ + -p 8080:8080 \ + -e WIDTH=1920 \ + -e HEIGHT=1080 \ + -it computer-use-demo +``` + +We do not recommend sending screenshots in resolutions above [XGA/WXGA](https://en.wikipedia.org/wiki/Display_resolution_standards#XGA) to avoid issues related to [image resizing](https://docs.anthropic.com/en/docs/build-with-claude/vision#evaluate-image-size). +Relying on the image resizing behavior in the API will result in lower model accuracy and slower performance than implementing scaling in your tools directly. The `computer` tool implementation in this project demonstrates how to scale both images and coordinates from higher resolutions to the suggested resolutions. + +## Development + +```bash +./setup.sh # configure venv, install development dependencies, and install pre-commit hooks +docker build . -t computer-use-demo:local # manually build the docker image (optional) +export ANTHROPIC_API_KEY=%your_api_key% +docker run \ + -e ANTHROPIC_API_KEY=$ANTHROPIC_API_KEY \ + -v $(pwd)/computer_use_demo:/home/computeruse/computer_use_demo/ `# mount local python module for development` \ + -v $HOME/.anthropic:/home/computeruse/.anthropic \ + -p 5900:5900 \ + -p 8501:8501 \ + -p 6080:6080 \ + -p 8080:8080 \ + -it computer-use-demo:local # can also use ghcr.io/anthropics/anthropic-quickstarts:computer-use-demo-latest +``` + +The docker run command above mounts the repository inside the docker image, such that you can edit files from the host. Streamlit is already configured with auto reloading. diff --git a/partner-models/claude/computer-use-demo/cloudbuild-destroy.yaml b/partner-models/claude/computer-use-demo/cloudbuild-destroy.yaml new file mode 100644 index 0000000..127d018 --- /dev/null +++ b/partner-models/claude/computer-use-demo/cloudbuild-destroy.yaml @@ -0,0 +1,89 @@ +# Copyright 2024 Google LLC +# +# 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 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +steps: + - id: "delete-k8s-resources" + name: "gcr.io/google.com/cloudsdktool/cloud-sdk" + entrypoint: "bash" + args: + - "-c" + - | + if gcloud container clusters describe ${_CLUSTER_NAME} \ + --project=${PROJECT_ID} \ + --region=${_REGION} --format="none" 2>/dev/null; then + gcloud container clusters get-credentials ${_CLUSTER_NAME} \ + --project=${PROJECT_ID} \ + --region=${_REGION} + + # Delete namespace (this will delete all resources in it) + kubectl delete namespace ${_NAMESPACE} --ignore-not-found + fi + + - id: "cleanup-iam" + name: "gcr.io/google.com/cloudsdktool/cloud-sdk" + waitFor: ["delete-k8s-resources"] + entrypoint: "bash" + args: + - "-c" + - | + # Remove IAM bindings + gcloud projects remove-iam-policy-binding ${PROJECT_ID} \ + --member="serviceAccount:${_IAM_SA_NAME}@${PROJECT_ID}.iam.gserviceaccount.com" \ + --role="roles/aiplatform.admin" \ + --quiet || true + + # Delete service account + gcloud iam service-accounts delete ${_IAM_SA_NAME}@${PROJECT_ID}.iam.gserviceaccount.com \ + --project=${PROJECT_ID} \ + --quiet || true + + - id: "delete-cluster" + name: "gcr.io/google.com/cloudsdktool/cloud-sdk" + waitFor: ["cleanup-iam"] + entrypoint: "bash" + args: + - "-c" + - | + # Delete GKE cluster + echo "Deleting GKE cluster..." + gcloud container clusters delete ${_CLUSTER_NAME} \ + --project=${PROJECT_ID} \ + --region=${_REGION} \ + --quiet || true + + - id: "delete-registry" + name: "gcr.io/google.com/cloudsdktool/cloud-sdk" + waitFor: ["delete-cluster"] + entrypoint: "bash" + args: + - "-c" + - | + # Delete Artifact Registry repository + echo "Deleting Artifact Registry repository..." + gcloud artifacts repositories delete ${_ARTIFACT_REGISTRY_ID} \ + --project=${PROJECT_ID} \ + --location=${_REGION} \ + --quiet || true + +timeout: "1800s" +options: + logging: CLOUD_LOGGING_ONLY + dynamicSubstitutions: true +substitutions: + _REGION: us-east5 + _CLUSTER_NAME: computer-use-demo-cluster + _NAMESPACE: computer-use-demo + _IAM_SA_NAME: computer-use-sa + _ARTIFACT_REGISTRY_ID: computer-use-ar-repo # Added this +tags: ["gke-cleanup", "${_CLUSTER_NAME}"] diff --git a/partner-models/claude/computer-use-demo/cloudbuild.yaml b/partner-models/claude/computer-use-demo/cloudbuild.yaml new file mode 100644 index 0000000..1b62506 --- /dev/null +++ b/partner-models/claude/computer-use-demo/cloudbuild.yaml @@ -0,0 +1,202 @@ +steps: + - id: "check-cluster" + name: "gcr.io/google.com/cloudsdktool/cloud-sdk" + entrypoint: "bash" + args: + - "-c" + - | + if gcloud container clusters describe ${_CLUSTER_NAME} \ + --project=${PROJECT_ID} \ + --region=${_REGION} --format="none" 2>/dev/null; then + echo "Cluster exists, skipping creation" + exit 0 + else + echo "Cluster not found, will create" + gcloud container clusters create-auto ${_CLUSTER_NAME} \ + --project=${PROJECT_ID} \ + --region=${_REGION} \ + --quiet + fi + + - id: "setup-artifact-registry" + name: "gcr.io/google.com/cloudsdktool/cloud-sdk" + waitFor: ["check-cluster"] + entrypoint: "bash" + args: + - "-c" + - | + if gcloud artifacts repositories describe ${_ARTIFACT_REGISTRY_ID} \ + --project=${PROJECT_ID} \ + --location=${_REGION} --format="none" 2>/dev/null; then + echo "Repository exists, skipping creation" + else + echo "Creating Artifact Registry repository" + gcloud artifacts repositories create ${_ARTIFACT_REGISTRY_ID} \ + --repository-format=docker \ + --location=${_REGION} \ + --quiet + fi + + - id: "pull-tag-push-image" + name: "gcr.io/cloud-builders/docker" + waitFor: ["setup-artifact-registry"] + entrypoint: "bash" + args: + - "-c" + - | + # Pull the prebuilt image + docker pull ghcr.io/anthropics/anthropic-quickstarts:computer-use-demo-latest + + # Tag for Artifact Registry + docker tag ghcr.io/anthropics/anthropic-quickstarts:computer-use-demo-latest \ + ${_REGION}-docker.pkg.dev/${PROJECT_ID}/${_ARTIFACT_REGISTRY_ID}/${_IMAGE_NAME}:${_IMAGE_TAG} + + # Push to Artifact Registry + docker push ${_REGION}-docker.pkg.dev/${PROJECT_ID}/${_ARTIFACT_REGISTRY_ID}/${_IMAGE_NAME}:${_IMAGE_TAG} + + - id: "setup-k8s-iam" + name: "gcr.io/google.com/cloudsdktool/cloud-sdk" + waitFor: ["pull-tag-push-image"] + entrypoint: "bash" + args: + - "-c" + - | + # Get cluster credentials + gcloud container clusters get-credentials ${_CLUSTER_NAME} \ + --project=${PROJECT_ID} \ + --region=${_REGION} + + # Check and create namespace + if ! kubectl get namespace ${_NAMESPACE} 2>/dev/null; then + kubectl create namespace ${_NAMESPACE} + fi + + # Check and create KSA + if ! kubectl get serviceaccount ${_KSA_NAME} -n ${_NAMESPACE} 2>/dev/null; then + kubectl create serviceaccount ${_KSA_NAME} -n ${_NAMESPACE} + fi + + # Check and create IAM SA + if ! gcloud iam service-accounts describe ${_IAM_SA_NAME}@${PROJECT_ID}.iam.gserviceaccount.com \ + --project=${PROJECT_ID} 2>/dev/null; then + gcloud iam service-accounts create ${_IAM_SA_NAME} \ + --project=${PROJECT_ID} \ + --display-name="${_IAM_SA_DISPLAY_NAME}" + fi + + # Add IAM roles + gcloud projects add-iam-policy-binding ${PROJECT_ID} \ + --member="serviceAccount:${_IAM_SA_NAME}@${PROJECT_ID}.iam.gserviceaccount.com" \ + --role="roles/aiplatform.admin" \ + --quiet + + # Setup workload identity (idempotent) + gcloud iam service-accounts add-iam-policy-binding \ + ${_IAM_SA_NAME}@${PROJECT_ID}.iam.gserviceaccount.com \ + --role roles/iam.workloadIdentityUser \ + --member "serviceAccount:${PROJECT_ID}.svc.id.goog[${_NAMESPACE}/${_KSA_NAME}]" \ + --quiet + + kubectl annotate serviceaccount ${_KSA_NAME} \ + --namespace ${_NAMESPACE} \ + iam.gke.io/gcp-service-account=${_IAM_SA_NAME}@${PROJECT_ID}.iam.gserviceaccount.com \ + --overwrite + + - id: "deploy-app" + name: "gcr.io/google.com/cloudsdktool/cloud-sdk" + waitFor: ["setup-k8s-iam"] + entrypoint: "bash" + args: + - "-c" + - | + # Install gettext package which contains envsubst + apt-get update && apt-get install -y gettext-base + + gcloud container clusters get-credentials ${_CLUSTER_NAME} \ + --project=${PROJECT_ID} \ + --region=${_REGION} + + # Export all variables that will be used in envsubst + export _REGION=${_REGION} + export PROJECT_ID=${PROJECT_ID} + export _ARTIFACT_REGISTRY_ID=${_ARTIFACT_REGISTRY_ID} + export _IMAGE_NAME=${_IMAGE_NAME} + export _IMAGE_TAG=${_IMAGE_TAG} + export _NAMESPACE=${_NAMESPACE} + export _KSA_NAME=${_KSA_NAME} + export _IAM_SA_NAME=${_IAM_SA_NAME} + + envsubst < deployment.yaml | kubectl apply -f - + + - id: "get-service-ip" + name: "gcr.io/google.com/cloudsdktool/cloud-sdk" + waitFor: ["deploy-app"] + entrypoint: "bash" + args: + - "-c" + - | + echo "Waiting for LoadBalancer IP assignment..." + + # Debug current state + echo "Checking service in namespace ${_NAMESPACE}..." + kubectl get services -n ${_NAMESPACE} + + get_external_ip() { + kubectl get service ${_IMAGE_NAME} -n ${_NAMESPACE} \ + -o jsonpath='{.status.loadBalancer.ingress[0].ip}' + } + + # Wait for IP with timeout + TIMEOUT=300 # 5 minutes timeout + INTERVAL=10 # Check every 10 seconds + ELAPSED=0 + + until [ -n "$$(get_external_ip)" ] || [ $$ELAPSED -ge $$TIMEOUT ]; do + echo "Waiting for external IP... ($$ELAPSED seconds elapsed)" + # Debug service state + kubectl get service ${_IMAGE_NAME} -n ${_NAMESPACE} + sleep $$INTERVAL + ELAPSED=$$((ELAPSED + INTERVAL)) + done + + EXTERNAL_IP=$$(get_external_ip) + + if [ -n "$$EXTERNAL_IP" ]; then + echo "✅ Service is ready!" + echo "###################################################" + echo "# Deployment complete! 🚀 #" + echo "# External IP: $$EXTERNAL_IP #" + echo "# Services available at: #" + echo "# Streamlit UI: http://$$EXTERNAL_IP:8501 #" + echo "# VNC: $$EXTERNAL_IP:5900 #" + echo "# noVNC Web UI: http://$$EXTERNAL_IP:6080 #" + echo "# Web: http://$$EXTERNAL_IP:8080 #" + echo "###################################################" + else + echo "❌ Timeout waiting for external IP" + echo "Debug information:" + echo "Current services in namespace:" + kubectl get services -n ${_NAMESPACE} + echo "Service details:" + kubectl describe service ${_IMAGE_NAME} -n ${_NAMESPACE} + echo "Pod status:" + kubectl get pods -n ${_NAMESPACE} + exit 1 + fi + +timeout: "3600s" +options: + logging: CLOUD_LOGGING_ONLY + machineType: "E2_HIGHCPU_8" + dynamicSubstitutions: true +substitutions: + _REGION: us-east5 + _CLUSTER_NAME: computer-use-demo-cluster + _ARTIFACT_REGISTRY_ID: computer-use-ar-repo + _NAMESPACE: computer-use-demo + _KSA_NAME: computer-use-ksa + _IAM_SA_NAME: computer-use-sa + _IAM_SA_DISPLAY_NAME: Computer Use Demo SA + _IMAGE_NAME: computer-use-demo + _IMAGE_TAG: latest +tags: ["gke-deployment", "${_CLUSTER_NAME}"] diff --git a/partner-models/claude/computer-use-demo/deployment.yaml b/partner-models/claude/computer-use-demo/deployment.yaml new file mode 100644 index 0000000..14b7a79 --- /dev/null +++ b/partner-models/claude/computer-use-demo/deployment.yaml @@ -0,0 +1,111 @@ +# Copyright 2024 Google LLC +# +# 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 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +apiVersion: v1 +kind: ServiceAccount +metadata: + name: ${_KSA_NAME} + namespace: ${_NAMESPACE} + annotations: + iam.gke.io/gcp-service-account: ${_IAM_SA_NAME}@${PROJECT_ID}.iam.gserviceaccount.com +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: ${_IMAGE_NAME} + namespace: ${_NAMESPACE} +spec: + replicas: 1 + selector: + matchLabels: + app: ${_IMAGE_NAME} + template: + metadata: + labels: + app: ${_IMAGE_NAME} + spec: + serviceAccountName: ${_KSA_NAME} + containers: + - name: ${_IMAGE_NAME} + image: ${_REGION}-docker.pkg.dev/${PROJECT_ID}/${_ARTIFACT_REGISTRY_ID}/${_IMAGE_NAME}:${_IMAGE_TAG} + resources: + requests: + memory: "1Gi" + cpu: "1000m" + limits: + memory: "2Gi" + cpu: "1000m" + env: + - name: API_PROVIDER + value: "vertex" + - name: CLOUD_ML_REGION + value: "${_REGION}" + - name: ANTHROPIC_VERTEX_PROJECT_ID + value: "${PROJECT_ID}" + - name: WIDTH + value: "1024" + - name: HEIGHT + value: "768" + - name: VNC_PORT + value: "5900" + - name: NOVNC_PORT + value: "6080" + - name: WEBSOCKIFY_OPTIONS + value: "--web /opt/noVNC --cert=/opt/noVNC/self.pem" + ports: + - containerPort: 5900 + name: vnc + - containerPort: 8501 + name: streamlit + - containerPort: 6080 + name: novnc + - containerPort: 8080 + name: web + readinessProbe: + tcpSocket: + port: streamlit + initialDelaySeconds: 90 + periodSeconds: 10 + timeoutSeconds: 5 + failureThreshold: 6 + livenessProbe: + tcpSocket: + port: web + initialDelaySeconds: 120 + periodSeconds: 20 + timeoutSeconds: 5 + failureThreshold: 6 +--- +apiVersion: v1 +kind: Service +metadata: + name: ${_IMAGE_NAME} + namespace: ${_NAMESPACE} +spec: + type: LoadBalancer + ports: + - port: 5900 + targetPort: vnc + name: vnc + - port: 8501 + targetPort: streamlit + name: streamlit + - port: 6080 + targetPort: novnc + name: novnc + - port: 8080 + targetPort: web + name: web + selector: + app: ${_IMAGE_NAME} diff --git a/rag-grounding/README.md b/rag-grounding/README.md new file mode 100644 index 0000000..29f560f --- /dev/null +++ b/rag-grounding/README.md @@ -0,0 +1,246 @@ +# RAG and Grounding + + + +**YouTube Video: Introduction to grounding with Gemini on Vertex AI** + + + + + + Introduction to grounding with Gemini on Vertex AI + + + +This directory provides a curated list of examples that explore Retrieval +Augmented Generation (RAG), grounding techniques, knowledge bases, grounded +generation, and related topics like vector search and semantic search. + +All of these links are examples in this repository, but are indexed here for +your convenience. + +## What is RAG and Grounding? + +![Animated GIF showing "what is grounding"](./img/what-is-grounding.gif) + +- Ungrounded generation relies on the LLM training data alone and is prone to + hallucinations when it doesn't have all the right facts +- **Grounding** a LLM with relevant facts provides fresh and potentially + private data to the model as part of it's input or prompt +- **RAG** is a technique which retrieves relevant facts, often via search, and + provides them to the LLM + +Using RAG and Grounding to improve generations and reduce hallucinations is +becoming commonplace. Doing so well and generating extremely high quality +results which are entirely grounded on the most relevant facts, potentially from +a very large corpus of information and at high scale - is an art. Vertex AI +provides a platform of tools and APIs which help you build and maintain a great +search engine and RAG application, and the evaluations needed to hill climb +"quality". + +## Building a Grounded Generation Application + +Grounded generation is crucial for enhancing the accuracy, factuality, and relevance of large language models (LLMs). By connecting LLMs to real-time data sources, including private enterprise data, grounding minimizes hallucinations and enables access to the latest information. + +The [Vertex AI Search Grounded Generation Playground](../search/grounded-generation-playground/README.md) showcases this by offering a Next.js-based interface for experimenting with the Vertex AI Search Grounded Generation API. It differentiates itself through features like a chat interface with multi-turn grounding, side-by-side comparison of grounded and ungrounded responses, dynamic retrieval with an adjustable threshold for cost optimization, and support for various grounding sources, including Google Search, Vertex AI Search, and custom integrations. Explore the playground to experience the power of grounded generation and learn how to integrate it into your own applications. For detailed instructions and setup, refer to the [grounded-generation-playground setup instructions](../search/grounded-generation-playground/README.md). + +## Measuring RAG/Grounding Quality + +See +[this blog post: How to evaluate generated answers from RAG at scale on Vertex AI](https://medium.com/google-cloud/vqa-3-how-to-evaluate-generated-answers-from-rag-at-scale-on-vertex-ai-70bc397cb33d) +for a more in-depth walkthrough. + +- **[evaluate_rag_gen_ai_evaluation_service_sdk.ipynb](../gemini/evaluation/evaluate_rag_gen_ai_evaluation_service_sdk.ipynb)**: + Evaluates RAG systems using the Gen AI Evaluation Service SDK, offering both + reference-free and reference-based evaluation methods with visualization. +- **[ragas_with_gemini.ipynb](../gemini/use-cases/retrieval-augmented-generation/rag-evaluation/ragas_with_gemini.ipynb)**: + Evaluates RAG pipelines using the RAGAS framework and the Gemini model + for Q&A tasks. +- **[deepeval_with_gemini.ipynb](../gemini/use-cases/retrieval-augmented-generation/rag-evaluation/deepeval_with_gemini.ipynb)**: + Evaluates Gemini's performance on a question-answering task using + DeepEval and the Gemini API in Vertex AI, including Pytest integration. + +## Out of the Box RAG/Grounding + +With +**[Vertex AI Search](https://cloud.google.com/generative-ai-app-builder/docs/)**, +you can build a RAG/Grounding system in a few clicks or a few lines of code and +be ready for scale with high quality results. Vertex AI Search is an end-to-end +Search engine builder, giving you Google quality search for your own data. + +- **[Vertex AI Search - sample Web App](../search/web-app/)**: Take a look at + this sample web app using Vertex AI Search, which is a flexible and easy to + use "out of the box" solution for search & RAG/Grounding. +- **[bulk_question_answering.ipynb](../search/bulk-question-answering/bulk_question_answering.ipynb)**: + Processes questions from a CSV and outputs the results (top documents and + extractive answers) to a TSV file using Vertex AI Search. +- **[contract_analysis.ipynb](../search/retrieval-augmented-generation/examples/contract_analysis.ipynb)**: + Demonstrates RAG for contract analysis using Palm2, LangChain, and a vector + store, with a Gradio interface for querying contracts and retrieving answers + with source references. +- **[question_answering.ipynb](../search/retrieval-augmented-generation/examples/question_answering.ipynb)**: + Builds a question-answering system using Vertex AI Search and LangChain to + retrieve information from unstructured documents and leverage LLMs for + answering with citations. +- **[rag_google_documentation.ipynb](../search/retrieval-augmented-generation/examples/rag_google_documentation.ipynb)**: + Builds a question-answering system from Google Cloud documentation using RAG + and evaluates the impact of different parameter settings on model + performance. +- **[rag_google_documentation.ipynb](../search/retrieval-augmented-generation/examples/rag_google_documentation.ipynb)**: + Showcase specific RAG use cases +- **[search_data_blending_with_gemini_summarization.ipynb](../search/search_data_blending_with_gemini_summarization.ipynb)**: + Demonstrates calling a search app that blends information from multiple + stores (GCS, BQ, site) and summarizes search snippets and responses using + the Gemini model. +- **[vertexai_search_options.ipynb](../search/vertexai-search-options/vertexai_search_options.ipynb)**: + Demonstrates three approaches for using Vertex AI Search: direct API usage, + grounding with Gemini, and integration with LangChain. + +Vertex AI Search can be configured to adapt to many different use cases and +data. + + + + +## Orchestration with RAG Engine + +[RAG Engine on Vertex AI](https://cloud.google.com/vertex-ai/generative-ai/docs/rag-overview) is a data framework for developing context-augmented large language model (LLM) applications. Context augmentation occurs when you apply an LLM to your data. This implements retrieval-augmented generation (RAG). + +- **[intro_rag_engine.ipynb](../gemini/rag-engine/intro_rag_engine.ipynb)**: Introduction to RAG Engine +- Vector Database Choices + - **[rag_engine_feature_store.ipynb](../gemini/rag-engine/rag_engine_feature_store.ipynb)**: How to use RAG Engine with [Vertex AI Feature Store](https://cloud.google.com/vertex-ai/docs/featurestore/latest/overview) + - **[rag_engine_pinecone.ipynb](../gemini/rag-engine/rag_engine_pinecone.ipynb)**: How to use RAG Engine with [Pinecone](https://www.pinecone.io/) + - **[rag_engine_vector_search.ipynb](../gemini/rag-engine/rag_engine_vector_search.ipynb)**: How to use RAG Engine with [Vertex AI Vector Search](https://cloud.google.com/vertex-ai/docs/vector-search/overview) + - **[rag_engine_weaviate.ipynb](../gemini/rag-engine/rag_engine_weaviate.ipynb)**: How to use RAG Engine with [Weaviate](https://weaviate.io/) +- **[rag_engine_vertex_ai_search.ipynb](../gemini/rag-engine/rag_engine_vertex_ai_search.ipynb)**: How to use RAG Engine with [Vertex AI Search](https://cloud.google.com/generative-ai-app-builder/docs/enterprise-search-introduction) +- **[rag_engine_evaluation.ipynb](../gemini/rag-engine/rag_engine_evaluation.ipynb)**: Advanced RAG Techniques and Evaluation with RAG Engine + +## Bring your own Search for RAG/Grounding + +The Vertex AI Search - Grounded Generation API allows you to use a custom search +engine for RAG/Grounding. You can wrap any search engine with a cloud function +exposing a REST API and then plug it into the Grounded Generation API as a +`grounding_source`. + +See the +[Grounded Generation API documentation](https://cloud.google.com/generative-ai-app-builder/docs/grounded-gen) +for more information. + +Demo coming soon. + +## Build your own Search for RAG/Grounding + +You may want to build your own search engine for RAG/Grounding, perhaps because +you have a unique use case or perhaps because you want to use a specific search +engine, or perhaps there are constraints on what you can use. + +We have many component APIs which can be used to build a RAG/Grounding pipeline +of your own. + +- [Vertex AI APIs for building search and RAG](https://cloud.google.com/generative-ai-app-builder/docs/builder-apis) + has a list of several APIs you can use in isolation or in combination + +We have a managed service to assemble component using a LlamaIndex style SDK. + +- [RAG Engine](https://cloud.google.com/vertex-ai/generative-ai/docs/rag-overview) + allows you to assemble a RAG search using popular OSS framework and + components from Google or Open Source + +We have a few reference architectures you can use to build your own +RAG/Grounding pipeline from the ground up. + +- [This end-to-end DIY RAG example in a notebook](https://github.com/GoogleCloudPlatform/applied-ai-engineering-samples/blob/main/genai-on-vertex-ai/retrieval_augmented_generation/diy_rag_with_vertexai_apis/build_grounded_rag_app_with_vertex.ipynb) + written in LangChain and using some of these APIs +- The Google Cloud Architecture Center has reference architectures on + [building a RAG infrastructure with GKE](https://cloud.google.com/architecture/rag-capable-gen-ai-app-using-gke) + or + [using alloydb and a few Vertex services](https://cloud.google.com/architecture/rag-capable-gen-ai-app-using-vertex-ai) + +More coming soon. + +## Build with a Vector Database + +Vertex AI Vector Search (Formerly known as Matching Engine) is a highly scalable +and performant vector database which powers Vertex AI Search. + +AlloyDB, BigQuery and Redis also have vector search capabilities, each with +different performance characteristics - though each of them is a general purpose +database and not purpose built for embeddings like Vector Search is. + +Note that you can use a Vector Database for RAG/Grounding and for many other use +cases, like recommendation systems, clustering, and anomaly detection. + +**[Document_QnA_using_gemini_and_vector_search.ipynb](../gemini/use-cases/retrieval-augmented-generation/Document_QnA_using_gemini_and_vector_search.ipynb)** +Demonstrates building a multimodal question-answering system using Gemini and +Vertex AI Vector Search for PDFs containing text and images, employing retrieval +augmented generation (RAG). + +### Embeddings + +The best explanation of embeddings I've seen + +- **[hybrid-search.ipynb](../embeddings/hybrid-search.ipynb)**: Demonstrates + hybrid search (combining semantic and keyword search) using Vertex AI Vector + Search. +- **[intro-textemb-vectorsearch.ipynb](../embeddings/intro-textemb-vectorsearch.ipynb)**: + Demonstrates building semantic search capabilities using Vertex AI's text + embeddings and vector search, grounding LLM outputs with real-world data. +- **[vector-search-quickstart.ipynb](../embeddings/vector-search-quickstart.ipynb)**: + Provides a quickstart tutorial for Vertex AI Vector Search, guiding users + through setting up, building, deploying, and querying a vector search index + using sample product data. +- **[bq-vector-search-log-outlier-detection.ipynb](../embeddings/use-cases/outlier-detection/bq-vector-search-log-outlier-detection.ipynb)**: + Demonstrates log anomaly detection and investigation using Vertex AI, + BigQuery, and text embeddings to identify semantically similar past actions + for outlier analysis. + +### Gemini + +- **[intro-grounding-gemini.ipynb](../gemini/grounding/intro-grounding-gemini.ipynb)**: + Demonstrates grounding LLM responses in Google Search and Vertex AI Search + using Gemini, improving response accuracy and reducing hallucinations. +- **[summarization_large_documents_langchain.ipynb](../gemini/use-cases/document-processing/summarization_large_documents_langchain.ipynb)**: + Demonstrates three LangChain methods (Stuffing, MapReduce, Refine) for + summarizing large documents using Vertex AI models, comparing their + effectiveness and limitations. +- **[llamaindex_workflows.ipynb](../gemini/orchestration/llamaindex_workflows.ipynb)** Using LlamaIndex Workflows to build an event driven RAG flow. + +### Open Models + +- **[cloud_run_ollama_gemma2_rag_qa.ipynb](../open-models/use-cases/cloud_run_ollama_gemma2_rag_qa.ipynb)**: + Demonstrates deploying Gemma 2 on Google Cloud Run with GPU acceleration + using Ollama and LangChain, building a RAG question-answering application. + +## Agents on top of RAG + +- **[tutorial_vertex_ai_search_rag_agent.ipynb](../gemini/agent-engine/tutorial_vertex_ai_search_rag_agent.ipynb)**: + Demonstrates building and deploying a conversational search agent on Vertex AI using LangChain, a reasoning engine, and RAG with Vertex AI Search to + query a movie dataset. +- **[tutorial_alloydb_rag_agent.ipynb](../gemini/agent-engine/tutorial_alloydb_rag_agent.ipynb)**: + Demonstrates deploying a RAG application using LangChain, AlloyDB for + PostgreSQL, and Vertex AI, covering setup, deployment, and cleanup. +- **[tutorial_cloud_sql_pg_rag_agent.ipynb](../gemini/agent-engine/tutorial_cloud_sql_pg_rag_agent.ipynb)**: + Demonstrates deploying a RAG application using LangChain, Vertex AI, and + Cloud SQL for PostgreSQL, enabling semantic search and LLM-based responses. + +## Use Cases + +These notebooks offer a valuable resource to understand and implement RAG and +grounding techniques in various applications. Feel free to dive into the +notebooks that pique your interest and start building your own RAG-powered +solutions. + +- Examples of RAG in different domains + + - **[NLP2SQL_using_dynamic_RAG.ipynb](../gemini/use-cases/retrieval-augmented-generation/NLP2SQL_using_dynamic_RAG.ipynb)** + - **[RAG_Based_on_Sensitive_Data_Protection_using_Faker.ipynb](../gemini/use-cases/retrieval-augmented-generation/RAG_Based_on_Sensitive_Data_Protection_using_Faker.ipynb)** + - **[code_rag.ipynb](../gemini/use-cases/retrieval-augmented-generation/code_rag.ipynb)** + - **[intra_knowledge_qna.ipynb](../gemini/use-cases/retrieval-augmented-generation/intra_knowledge_qna.ipynb)** + - **[intro_multimodal_rag.ipynb](../gemini/use-cases/retrieval-augmented-generation/intro_multimodal_rag.ipynb)** + - **[llamaindex_rag.ipynb](../gemini/use-cases/retrieval-augmented-generation/llamaindex_rag.ipynb)** + - **[multimodal_rag_langchain.ipynb](../gemini/use-cases/retrieval-augmented-generation/multimodal_rag_langchain.ipynb)** + - **[small_to_big_rag.ipynb](../gemini/use-cases/retrieval-augmented-generation/small_to_big_rag/small_to_big_rag.ipynb)** + +- Build RAG systems using BigQuery + - **[rag_qna_with_bq_and_featurestore.ipynb](../gemini/use-cases/retrieval-augmented-generation/rag_qna_with_bq_and_featurestore.ipynb)** + - **[rag_vector_embedding_in_bigquery.ipynb](../gemini/use-cases/retrieval-augmented-generation/rag_vector_embedding_in_bigquery.ipynb)** diff --git a/rag-grounding/img/what-is-grounding.gif b/rag-grounding/img/what-is-grounding.gif new file mode 100644 index 0000000..f3035d2 Binary files /dev/null and b/rag-grounding/img/what-is-grounding.gif differ diff --git a/renovate.json b/renovate.json new file mode 100644 index 0000000..da343aa --- /dev/null +++ b/renovate.json @@ -0,0 +1,29 @@ +{ + "extends": ["group:allNonMajor"], + "prConcurrentLimit": 0, + "lockFileMaintenance": { + "enabled": false + }, + "packageRules": [ + { + "matchUpdateTypes": ["major", "minor", "patch", "lockFileMaintenance", "bump", "pin", "digest"], + "extends": ["schedule:quarterly"], + "automerge": true + }, + { + "matchUpdateTypes": ["major", "minor", "patch", "lockFileMaintenance", "bump", "pin", "digest"], + "excludePaths": ["gemini/sample-apps/genwealth/**"] + } + ], + "vulnerabilityAlerts": { + "schedule": ["at any time"] + }, + "pip_requirements": { + "fileMatch": [ + "requirements.txt", + "requirements-test.txt", + "constraints.txt", + "constraints-test.txt" + ] + } +} diff --git a/scripts/format.sh b/scripts/format.sh new file mode 100755 index 0000000..39016e2 --- /dev/null +++ b/scripts/format.sh @@ -0,0 +1,126 @@ +#!/bin/bash +# Copyright 2024 Google LLC +# +# 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 +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +set -e + +# Check if uv is installed +if ! command -v uv &> /dev/null; then + echo "uv could not be found. Please install it to run the formatting script." + echo "See https://github.com/astral-sh/uv for installation instructions." + exit 1 +fi + +# Define the uv run command with all necessary dependencies +UV_RUN="uv run --with autoflake --with ruff --with nbqa --with nbformat>=5.10.4 --with git+https://github.com/tensorflow/docs" + +# Sorting and de-duplicating spelling allow file +SPELLING_ALLOW_FILE=".github/actions/spelling/allow.txt" +SPELLING_EXPECT_FILE=".github/actions/spelling/expect.txt" +if [ -f "$SPELLING_EXPECT_FILE" ]; then + echo "Combining $SPELLING_EXPECT_FILE into $SPELLING_ALLOW_FILE" + cat "$SPELLING_EXPECT_FILE" >> "$SPELLING_ALLOW_FILE" + rm "$SPELLING_EXPECT_FILE" +fi + +if [ -f "$SPELLING_ALLOW_FILE" ]; then + echo "Sorting and de-duplicating $SPELLING_ALLOW_FILE" + sort -u "$SPELLING_ALLOW_FILE" -o "$SPELLING_ALLOW_FILE" +fi + +# Determine files to lint +FORMAT_ALL=false +UNSAFE_FIXES=false +PASSED_FILES="" +for arg in "$@"; do + if [ "$arg" == "--all" ]; then + FORMAT_ALL=true + elif [ "$arg" == "--unsafe-fixes" ]; then + UNSAFE_FIXES=true + elif [[ "$arg" == *.ipynb ]] || [[ "$arg" == *.py ]]; then + PASSED_FILES="$PASSED_FILES $arg" + fi +done + +LINT_PATHS_NB="" +LINT_PATHS_PY="" + +if [ -n "$PASSED_FILES" ]; then + for f in $PASSED_FILES; do + if [[ "$f" == *.ipynb ]]; then + LINT_PATHS_NB="$LINT_PATHS_NB $f" + elif [[ "$f" == *.py ]]; then + LINT_PATHS_PY="$LINT_PATHS_PY $f" + fi + done +elif [ "$FORMAT_ALL" = true ]; then + LINT_PATHS_NB=$(find . -name "*.ipynb" -not -path "*/.*") + LINT_PATHS_PY=$(find . -name "*.py" -not -path "*/.*" -not -name "noxfile.py") +else + TARGET_BRANCH="main" + # Get changed files + CHANGED_FILES=$(git diff --name-only --diff-filter=ACMRTUXB "$TARGET_BRANCH" | sort -u) + STAGED_FILES=$(git diff --cached --name-only --diff-filter=ACMRTUXB "$TARGET_BRANCH" | sort -u) + COMMITTED_FILES=$(git diff HEAD "$TARGET_BRANCH" --name-only --diff-filter=ACMRTUXB | sort -u) + + ALL_CHANGED=$(echo "$CHANGED_FILES $STAGED_FILES $COMMITTED_FILES" | tr ' ' '\n' | sort -u) + + for f in $ALL_CHANGED; do + if [ -f "$f" ]; then + if [[ "$f" == *.ipynb ]]; then + LINT_PATHS_NB="$LINT_PATHS_NB $f" + elif [[ "$f" == *.py ]] && [ "$f" != "noxfile.py" ]; then + LINT_PATHS_PY="$LINT_PATHS_PY $f" + fi + fi + done +fi + +# --- Format Notebooks --- +if [ -n "$LINT_PATHS_NB" ]; then + echo "Formatting notebooks..." + + # Run custom notebook processor + $UV_RUN python3 scripts/notebook_processor.py $LINT_PATHS_NB + + # Run nbqa tools + $UV_RUN nbqa autoflake $LINT_PATHS_NB -i -r --remove-all-unused-imports + + UNSAFE_FLAG="" + if [ "$UNSAFE_FIXES" = true ]; then + UNSAFE_FLAG=" --unsafe-fixes" + fi + $UV_RUN nbqa "ruff check --fix-only$UNSAFE_FLAG" $LINT_PATHS_NB + + $UV_RUN nbqa "ruff format" $LINT_PATHS_NB + + $UV_RUN python3 -m tensorflow_docs.tools.nbfmt $LINT_PATHS_NB +else + echo "No notebooks to format." +fi + +# --- Format Python Files --- +if [ -n "$LINT_PATHS_PY" ]; then + echo "Formatting Python files..." + $UV_RUN autoflake -i -r --remove-all-unused-imports $LINT_PATHS_PY + + UNSAFE_FLAG="" + if [ "$UNSAFE_FIXES" = true ]; then + UNSAFE_FLAG=" --unsafe-fixes" + fi + $UV_RUN ruff check --fix-only$UNSAFE_FLAG $LINT_PATHS_PY + $UV_RUN ruff format $LINT_PATHS_PY +else + echo "No Python files to format." +fi diff --git a/scripts/notebook_processor.py b/scripts/notebook_processor.py new file mode 100644 index 0000000..d7d0f3d --- /dev/null +++ b/scripts/notebook_processor.py @@ -0,0 +1,207 @@ +# pylint: skip-file +# type: ignore +# -*- coding: utf-8 -*- +# +# Copyright 2024 Google LLC +# +# 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 +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os +import sys +import urllib.parse + +import nbformat + +DEFAULT_RUFF_LINE_LENGTH = 88 + +# --- Notebook Link Constants --- +LINK_PREFIXES = { + "colab_link": "https://colab.research.google.com/github/GoogleCloudPlatform/generative-ai/blob/main/", + "colab_enterprise_link": "https://console.cloud.google.com/agent-platform/colab/import/", + "github_link": "https://github.com/GoogleCloudPlatform/generative-ai/blob/main/", + "workbench_link": "https://console.cloud.google.com/agent-platform/workbench/instances?download_url=", + "bigquery_studio_link": "https://console.cloud.google.com/bigquery/import?url=", + "linkedin_link": "https://www.linkedin.com/sharing/share-offsite/?url=", + "bluesky_link": "https://bsky.app/intent/compose?text=", + "twitter_link": "https://twitter.com/intent/tweet?url=", + "reddit_link": "https://reddit.com/submit?url=", + "facebook_link": "https://www.facebook.com/sharer/sharer.php?u=", +} + +GITHUB_URL_PREFIX = LINK_PREFIXES["github_link"] +RAW_URL_PREFIX = ( + "https://raw.githubusercontent.com/GoogleCloudPlatform/generative-ai/main/" +) + + +def fix_markdown_links( + cell_source: str, relative_notebook_path: str +) -> tuple[str, bool]: + """Fixes links in a markdown cell and returns the updated source.""" + new_lines = [] + changes_made = False + + encoded_url = urllib.parse.quote(f"{GITHUB_URL_PREFIX}{relative_notebook_path}") + + for line in cell_source.splitlines(): + for key, prefix in LINK_PREFIXES.items(): + if prefix not in line or "**NOTE:**" in line: + continue + + start_index = line.find(prefix) + len(prefix) + end_index = line.find(".ipynb", start_index) + len(".ipynb") + correct_link = "" + + if key in {"colab_link", "github_link"}: + correct_link = relative_notebook_path + elif key == "colab_enterprise_link": + correct_link = urllib.parse.quote( + f"{RAW_URL_PREFIX}{relative_notebook_path}", + safe=":", + ) + elif key == "workbench_link": + correct_link = f"{RAW_URL_PREFIX}{relative_notebook_path}" + elif key == "bigquery_studio_link": + correct_link = f"{GITHUB_URL_PREFIX}{relative_notebook_path}" + elif key in { + "linkedin_link", + "bluesky_link", + "twitter_link", + "reddit_link", + "facebook_link", + }: + correct_link = encoded_url + + if correct_link.lower() not in line.lower(): + line = line.replace(line[start_index:end_index], correct_link) + changes_made = True + + new_lines.append(line) + + return "\n".join(new_lines), changes_made + + +def update_notebook_links(notebook_paths: list[str]) -> None: + """Checks and fixes specific types of links in the provided list of notebooks.""" + print("Checking notebook links...") + links_updated_count = 0 + + for notebook_path in notebook_paths: + # False positive + if "vector-search-2-intro" in notebook_path: + continue + try: + with open(notebook_path, encoding="utf-8") as f: + notebook = nbformat.read(f, as_version=4) + + relative_notebook_path = os.path.relpath( + notebook_path, start=os.getcwd() + ).lower() + notebook_modified = False + + for cell in notebook.cells: + if ( + cell.cell_type == "markdown" + and " Fixed links in {notebook_path}") + + except Exception as e: + print(f"Could not check links in {notebook_path}. Error: {e}") + + if links_updated_count > 0: + print(f"Fixed links in {links_updated_count} notebooks.") + + +def preprocess_notebook( + notebook_paths: list[str], + max_line_length: int = DEFAULT_RUFF_LINE_LENGTH, +) -> None: + """Parses notebooks and wraps '@param' blocks with '# fmt: off/on'.""" + print("Checking for '@param' blocks to wrap with '# fmt: off/on'...") + for path in notebook_paths: + try: + with open(path, encoding="utf-8") as f: + notebook = nbformat.read(f, as_version=4) + + notebook_modified = False + for cell in notebook.cells: + if cell.cell_type != "code" or "@param" not in cell.source: + continue + + source_lines = cell.source.split("\n") + + clean_source_lines = [ + line + for line in source_lines + if line.strip() not in ["# fmt: off", "# fmt: on"] + ] + + param_indices = [ + i + for i, line in enumerate(clean_source_lines) + if "@param" in line and len(line) >= max_line_length + ] + + if not param_indices: + continue + + first_param_index = param_indices[0] + last_param_index = param_indices[-1] + + # Reconstruct the cell source with new directives + new_source_lines = [] + for i, line in enumerate(clean_source_lines): + if i == first_param_index: + new_source_lines.append("# fmt: off") + new_source_lines.append(line) + if i == last_param_index: + new_source_lines.append("# fmt: on") + + new_source = "\n".join(new_source_lines) + + if new_source != cell.source: + cell.source = new_source + notebook_modified = True + + if notebook_modified: + print(f" -> Wrapped '@param' blocks with '# fmt: off/on' in {path}") + with open(path, "w", encoding="utf-8") as f: + nbformat.write(notebook, f) + + except Exception as e: + print(f"Could not process notebook {path}. Error: {e}") + + +if __name__ == "__main__": + if len(sys.argv) < 2: + print( + "Usage: python notebook_processor.py ..." + ) + sys.exit(1) + + notebook_paths = sys.argv[1:] + preprocess_notebook(notebook_paths) + update_notebook_links(notebook_paths) diff --git a/sdk/intro_genai_sdk.ipynb b/sdk/intro_genai_sdk.ipynb new file mode 100644 index 0000000..bcc0c4e --- /dev/null +++ b/sdk/intro_genai_sdk.ipynb @@ -0,0 +1,1342 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "ur8xi4C7S06n" + }, + "outputs": [], + "source": [ + "# Copyright 2024 Google LLC\n", + "#\n", + "# Licensed under the Apache License, Version 2.0 (the \"License\");\n", + "# you may not use this file except in compliance with the License.\n", + "# You may obtain a copy of the License at\n", + "#\n", + "# https://www.apache.org/licenses/LICENSE-2.0\n", + "#\n", + "# Unless required by applicable law or agreed to in writing, software\n", + "# distributed under the License is distributed on an \"AS IS\" BASIS,\n", + "# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n", + "# See the License for the specific language governing permissions and\n", + "# limitations under the License." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "JAPoU8Sm5E6e" + }, + "source": [ + "# Getting started with Google Generative AI using the Gen AI SDK\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + "
    \n", + " \n", + " \"Google
    Open in Colab\n", + "
    \n", + "
    \n", + " \n", + " \"Google
    Open in Colab Enterprise\n", + "
    \n", + "
    \n", + " \n", + " \"Workbench
    Open in Workbench\n", + "
    \n", + "
    \n", + " \n", + " \"GitHub
    View on GitHub\n", + "
    \n", + "
    \n", + "\n", + "
    \n", + "\n", + "

    \n", + "Share to:\n", + "\n", + "\n", + " \"LinkedIn\n", + "\n", + "\n", + "\n", + " \"Bluesky\n", + "\n", + "\n", + "\n", + " \"X\n", + "\n", + "\n", + "\n", + " \"Reddit\n", + "\n", + "\n", + "\n", + " \"Facebook\n", + "\n", + "

    " + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "84f0f73a0f76" + }, + "source": [ + "| Author |\n", + "| --- |\n", + "| [Eric Dong](https://github.com/gericdong) |" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "tvgnzT1CKxrO" + }, + "source": [ + "## Overview\n", + "\n", + "The [Google Gen AI SDK](https://googleapis.github.io/python-genai/) provides a unified interface to Google's generative AI API services. This SDK simplifies the process of integrating generative AI capabilities into applications and services, enabling developers to leverage Google's advanced AI models for various tasks.\n", + "\n", + "In this tutorial, you learn about the key features of the Google Gen AI SDK for Python to help you get started with Google generative AI services and models including Gemini. You will complete the following tasks:\n", + "\n", + "- Install the Gen AI SDK\n", + "- Connect to an API service\n", + "- Send text prompts\n", + "- Send multimodal prompts\n", + "- Set system instruction\n", + "- Configure model parameters\n", + "- Configure safety filters\n", + "- Start a multi-turn chat\n", + "- Control generated output\n", + "- Generate content stream\n", + "- Send asynchronous requests\n", + "- Count tokens and compute tokens\n", + "- Use context caching\n", + "- Function calling\n", + "- Batch prediction\n", + "- Get text embeddings\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "61RBz8LLbxCR" + }, + "source": [ + "## Getting started" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "No17Cw5hgx12" + }, + "source": [ + "### Install Google Gen AI SDK\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "tFy3H3aPgx12" + }, + "outputs": [], + "source": [ + "%pip install --upgrade --quiet google-genai pandas" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "dmWOrTJ3gx13" + }, + "source": [ + "### Authenticate your notebook environment (Colab only)\n", + "\n", + "If you're running this notebook on Google Colab, run the cell below to authenticate your environment." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "NyKGtVQjgx13" + }, + "outputs": [], + "source": [ + "import sys\n", + "\n", + "if \"google.colab\" in sys.modules:\n", + " from google.colab import auth\n", + "\n", + " auth.authenticate_user()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "EdvJRUWRNGHE" + }, + "source": [ + "## Using Google Gen AI SDK\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "qgdSpVmDbdQ9" + }, + "outputs": [], + "source": [ + "import datetime" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "Ve4YBlDqzyj9" + }, + "source": [ + "## Connect to a Generative AI API service\n", + "\n", + "Google Gen AI APIs and models including Gemini are available in the following two API services:\n", + "\n", + "- **[Google AI for Developers](https://ai.google.dev/gemini-api/docs)**: Experiment, prototype, and deploy small projects.\n", + "- **[Vertex AI](https://cloud.google.com/vertex-ai/generative-ai/docs)**: Build enterprise-ready projects on Google Cloud.\n", + "\n", + "The Gen AI SDK provided an unified interface to these two API services. This notebook shows how to use the Gen AI SDK in Vertex AI." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "eN9kmPKJGAJQ" + }, + "source": [ + "### Vertex AI\n", + "\n", + "To start using Vertex AI, you must have a Google Cloud project and [enable the Vertex AI API](https://console.cloud.google.com/flows/enableapi?apiid=aiplatform.googleapis.com). Learn more about [setting up a project and a development environment](https://cloud.google.com/vertex-ai/docs/start/cloud-environment)." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "DF4l8DTdWgPY" + }, + "source": [ + "#### Set Google Cloud project information\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "Nqwi-5ufWp_B" + }, + "outputs": [], + "source": [ + "import os\n", + "\n", + "from google import genai\n", + "from google.genai.types import (\n", + " CreateBatchJobConfig,\n", + " CreateCachedContentConfig,\n", + " EmbedContentConfig,\n", + " FunctionDeclaration,\n", + " GenerateContentConfig,\n", + " HarmBlockThreshold,\n", + " HarmCategory,\n", + " Part,\n", + " SafetySetting,\n", + " Tool,\n", + ")\n", + "\n", + "# fmt: off\n", + "PROJECT_ID = \"[your-project-id]\" # @param {type: \"string\", placeholder: \"[your-project-id]\", isTemplate: true}\n", + "# fmt: on\n", + "if not PROJECT_ID or PROJECT_ID == \"[your-project-id]\":\n", + " PROJECT_ID = str(os.environ.get(\"GOOGLE_CLOUD_PROJECT\"))\n", + "\n", + "LOCATION = os.environ.get(\"GOOGLE_CLOUD_REGION\", \"global\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "T-tiytzQE0uM" + }, + "outputs": [], + "source": [ + "client = genai.Client(enterprise=True, project=PROJECT_ID, location=LOCATION)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "eXHJi5B6P5vd" + }, + "source": [ + "## Choose a model\n", + "\n", + "For more information about all AI models and APIs on Vertex AI, see [Google Models](https://cloud.google.com/vertex-ai/generative-ai/docs/learn/models#gemini-models) and [Model Garden](https://cloud.google.com/vertex-ai/generative-ai/docs/model-garden/explore-models)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "-coEslfWPrxo" + }, + "outputs": [], + "source": [ + "MODEL_ID = \"gemini-3.5-flash\" # @param {type: \"string\"}" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "37CH91ddY9kG" + }, + "source": [ + "## Send text prompts\n", + "\n", + "Use the `generate_content` method to generate responses to your prompts. You can pass text to `generate_content`, and use the `.text` property to get the text content of the response.\n", + "\n", + "For more examples of prompt engineering, refer to [this notebook](https://github.com/GoogleCloudPlatform/generative-ai/blob/main/gemini/prompts/intro_prompt_design.ipynb)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "6fc324893334" + }, + "outputs": [], + "source": [ + "response = client.models.generate_content(\n", + " model=MODEL_ID, contents=\"What's the largest planet in our solar system?\"\n", + ")\n", + "\n", + "print(response.text)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "zurBcEcWhFc6" + }, + "source": [ + "Optionally, you can display the response in markdown." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "3PoF18EwhI7e" + }, + "outputs": [], + "source": [ + "from IPython.display import Markdown, display\n", + "\n", + "display(Markdown(response.text))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "rZV2TY5Pa3Dd" + }, + "source": [ + "## Send multimodal prompts\n", + "\n", + "You can include text, PDF documents, images, audio and video in your prompt requests and get text or code responses.\n", + "\n", + "For more examples of multimodal use cases, refer to [this notebook](https://github.com/GoogleCloudPlatform/generative-ai/blob/main/gemini/use-cases/intro_multimodal_use_cases.ipynb)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "D3SI1X-JVMBj" + }, + "outputs": [], + "source": [ + "import requests\n", + "from PIL import Image\n", + "\n", + "image = Image.open(\n", + " requests.get(\n", + " \"https://storage.googleapis.com/cloud-samples-data/generative-ai/image/meal.png\",\n", + " stream=True,\n", + " ).raw\n", + ")\n", + "\n", + "response = client.models.generate_content(\n", + " model=MODEL_ID,\n", + " contents=[\n", + " image,\n", + " \"Write a short and engaging blog post based on this picture.\",\n", + " ],\n", + ")\n", + "\n", + "print(response.text)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "eN6wMdY1RSk3" + }, + "source": [ + "You can also pass the file URL in `Part.from_uri` in the request to the model directly." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "pG6l1Fuka6ZJ" + }, + "outputs": [], + "source": [ + "response = client.models.generate_content(\n", + " model=MODEL_ID,\n", + " contents=[\n", + " Part.from_uri(\n", + " file_uri=\"https://storage.googleapis.com/cloud-samples-data/generative-ai/image/meal.png\",\n", + " mime_type=\"image/png\",\n", + " ),\n", + " \"Write a short and engaging blog post based on this picture.\",\n", + " ],\n", + ")\n", + "\n", + "print(response.text)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "El1lx8P9ElDq" + }, + "source": [ + "## Set system instruction\n", + "\n", + "[System instructions](https://cloud.google.com/vertex-ai/generative-ai/docs/learn/prompts/system-instruction-introduction) allow you to steer the behavior of the model. By setting the system instruction, you are giving the model additional context to understand the task, provide more customized responses, and adhere to guidelines over the user interaction." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "7A-yANiyCLaO" + }, + "outputs": [], + "source": [ + "system_instruction = \"\"\"\n", + " You are a helpful language translator.\n", + " Your mission is to translate text in English to French.\n", + "\"\"\"\n", + "\n", + "prompt = \"\"\"\n", + " User input: I like bagels.\n", + " Answer:\n", + "\"\"\"\n", + "\n", + "response = client.models.generate_content(\n", + " model=MODEL_ID,\n", + " contents=prompt,\n", + " config=GenerateContentConfig(\n", + " system_instruction=system_instruction,\n", + " ),\n", + ")\n", + "\n", + "print(response.text)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "hIJVEr0RQY8S" + }, + "source": [ + "## Configure model parameters\n", + "\n", + "You can include parameter values in each call that you send to a model to control how the model generates a response. Learn more about [experimenting with parameter values](https://cloud.google.com/vertex-ai/generative-ai/docs/learn/prompts/adjust-parameter-values)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "d9NXP5N2Pmfo" + }, + "outputs": [], + "source": [ + "response = client.models.generate_content(\n", + " model=MODEL_ID,\n", + " contents=\"Tell me how the internet works, but pretend I'm a puppy who only understands squeaky toys.\",\n", + " config=GenerateContentConfig(\n", + " temperature=0.4,\n", + " top_p=0.95,\n", + " top_k=20,\n", + " candidate_count=1,\n", + " seed=5,\n", + " max_output_tokens=100,\n", + " stop_sequences=[\"STOP!\"],\n", + " presence_penalty=0.0,\n", + " frequency_penalty=0.0,\n", + " ),\n", + ")\n", + "\n", + "print(response.text)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "H9daipRiUzAY" + }, + "source": [ + "## Configure safety filters\n", + "\n", + "The Gemini API provides safety filters that you can adjust across multiple filter categories to restrict or allow certain types of content. You can use these filters to adjust what's appropriate for your use case. See the [Configure safety filters](https://cloud.google.com/vertex-ai/generative-ai/docs/multimodal/configure-safety-filters) page for details.\n", + "\n", + "For more examples of safety filters, refer to [this notebook](https://github.com/GoogleCloudPlatform/generative-ai/blob/main/gemini/responsible-ai/gemini_safety_ratings.ipynb)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "yPlDRaloU59b" + }, + "outputs": [], + "source": [ + "prompt = \"\"\"\n", + " Write a list of 2 disrespectful things that I might say to the universe after stubbing my toe in the dark.\n", + "\"\"\"\n", + "\n", + "safety_settings = [\n", + " SafetySetting(\n", + " category=HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT,\n", + " threshold=HarmBlockThreshold.BLOCK_LOW_AND_ABOVE,\n", + " ),\n", + " SafetySetting(\n", + " category=HarmCategory.HARM_CATEGORY_HARASSMENT,\n", + " threshold=HarmBlockThreshold.BLOCK_LOW_AND_ABOVE,\n", + " ),\n", + " SafetySetting(\n", + " category=HarmCategory.HARM_CATEGORY_HATE_SPEECH,\n", + " threshold=HarmBlockThreshold.BLOCK_LOW_AND_ABOVE,\n", + " ),\n", + " SafetySetting(\n", + " category=HarmCategory.HARM_CATEGORY_SEXUALLY_EXPLICIT,\n", + " threshold=HarmBlockThreshold.BLOCK_LOW_AND_ABOVE,\n", + " ),\n", + "]\n", + "\n", + "response = client.models.generate_content(\n", + " model=MODEL_ID,\n", + " contents=prompt,\n", + " config=GenerateContentConfig(\n", + " safety_settings=safety_settings,\n", + " ),\n", + ")\n", + "\n", + "print(response.text)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "DpKKhHbx3CaJ" + }, + "source": [ + "When you make a request to the model, the content is analyzed and assigned a safety rating. You can inspect the safety ratings of the generated content by printing out the model responses, as in this example:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "7R7eyEBetsns" + }, + "outputs": [], + "source": [ + "print(response.candidates[0].safety_ratings)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "29jFnHZZWXd7" + }, + "source": [ + "## Start a multi-turn chat\n", + "\n", + "The Gemini API enables you to have freeform conversations across multiple turns." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "DbM12JaLWjiF" + }, + "outputs": [], + "source": [ + "system_instruction = \"\"\"\n", + " You are an expert software developer and a helpful coding assistant.\n", + " You are able to generate high-quality code in any programming language.\n", + "\"\"\"\n", + "\n", + "chat = client.chats.create(\n", + " model=MODEL_ID,\n", + " config=GenerateContentConfig(\n", + " system_instruction=system_instruction,\n", + " temperature=0.5,\n", + " ),\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "JQem1halYDBW" + }, + "outputs": [], + "source": [ + "response = chat.send_message(\"Write a function that checks if a year is a leap year.\")\n", + "\n", + "print(response.text)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "6Fn69TurZ9DB" + }, + "outputs": [], + "source": [ + "response = chat.send_message(\"Okay, write a unit test of the generated function.\")\n", + "\n", + "print(response.text)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "rVlo0mWuZGkQ" + }, + "source": [ + "## Control generated output\n", + "\n", + "The [controlled generation](https://cloud.google.com/vertex-ai/generative-ai/docs/multimodal/control-generated-output) capability in Gemini API allows you to constraint the model output to a structured format. You can provide the schemas as Pydantic Models or a JSON string.\n", + "\n", + "For more examples of controlled generation, refer to [this notebook](https://github.com/GoogleCloudPlatform/generative-ai/blob/main/gemini/controlled-generation/intro_controlled_generation.ipynb)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "OjSgf2cDN_bG" + }, + "outputs": [], + "source": [ + "from pydantic import BaseModel\n", + "\n", + "\n", + "class Recipe(BaseModel):\n", + " name: str\n", + " description: str\n", + " ingredients: list[str]\n", + "\n", + "\n", + "response = client.models.generate_content(\n", + " model=MODEL_ID,\n", + " contents=\"List a few popular cookie recipes and their ingredients.\",\n", + " config=GenerateContentConfig(\n", + " response_mime_type=\"application/json\",\n", + " response_schema=Recipe,\n", + " ),\n", + ")\n", + "\n", + "print(response.text)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "nKai5CP_PGQF" + }, + "source": [ + "Optionally, you can parse the response string to JSON." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "ZeyDWbnxO-on" + }, + "outputs": [], + "source": [ + "import json\n", + "\n", + "json_response = json.loads(response.text)\n", + "print(json.dumps(json_response, indent=2))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "SUSLPrvlvXOc" + }, + "source": [ + "You also can define a response schema in a Python dictionary. You can only use the supported fields as listed below. All other fields are ignored.\n", + "\n", + "- `enum`\n", + "- `items`\n", + "- `maxItems`\n", + "- `nullable`\n", + "- `properties`\n", + "- `required`\n", + "\n", + "In this example, you instruct the model to analyze product review data, extract key entities, perform sentiment classification (multiple choices), provide additional explanation, and output the results in JSON format.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "F7duWOq3vMmS" + }, + "outputs": [], + "source": [ + "response_schema = {\n", + " \"type\": \"ARRAY\",\n", + " \"items\": {\n", + " \"type\": \"ARRAY\",\n", + " \"items\": {\n", + " \"type\": \"OBJECT\",\n", + " \"properties\": {\n", + " \"rating\": {\"type\": \"INTEGER\"},\n", + " \"flavor\": {\"type\": \"STRING\"},\n", + " \"sentiment\": {\n", + " \"type\": \"STRING\",\n", + " \"enum\": [\"POSITIVE\", \"NEGATIVE\", \"NEUTRAL\"],\n", + " },\n", + " \"explanation\": {\"type\": \"STRING\"},\n", + " },\n", + " \"required\": [\"rating\", \"flavor\", \"sentiment\", \"explanation\"],\n", + " },\n", + " },\n", + "}\n", + "\n", + "prompt = \"\"\"\n", + " Analyze the following product reviews, output the sentiment classification and give an explanation.\n", + "\n", + " - \"Absolutely loved it! Best ice cream I've ever had.\" Rating: 4, Flavor: Strawberry Cheesecake\n", + " - \"Quite good, but a bit too sweet for my taste.\" Rating: 1, Flavor: Mango Tango\n", + "\"\"\"\n", + "\n", + "response = client.models.generate_content(\n", + " model=MODEL_ID,\n", + " contents=prompt,\n", + " config=GenerateContentConfig(\n", + " response_mime_type=\"application/json\",\n", + " response_schema=response_schema,\n", + " ),\n", + ")\n", + "\n", + "print(response.text)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "w9DRn59MZOoa" + }, + "source": [ + "## Generate content stream\n", + "\n", + "By default, the model returns a response after completing the entire generation process. You can also use `generate_content_stream` method to stream the response as it is being generated, and the model will return chunks of the response as soon as they are generated." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "ztOhpfznZSzo" + }, + "outputs": [], + "source": [ + "for chunk in client.models.generate_content_stream(\n", + " model=MODEL_ID,\n", + " contents=\"Tell me a story about a lonely robot who finds friendship in a most unexpected place.\",\n", + "):\n", + " print(chunk.text)\n", + " print(\"*****************\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "arLJE4wOuhh6" + }, + "source": [ + "## Send asynchronous requests\n", + "\n", + "You can send asynchronous requests using the `client.aio` module. This module exposes all the analogous async methods that are available on `client`.\n", + "\n", + "For example, `client.aio.models.generate_content` is the async version of `client.models.generate_content`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "gSReaLazs-dP" + }, + "outputs": [], + "source": [ + "response = await client.aio.models.generate_content(\n", + " model=MODEL_ID,\n", + " contents=\"Compose a song about the adventures of a time-traveling squirrel.\",\n", + ")\n", + "\n", + "print(response.text)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "gV1dR-QlTKRs" + }, + "source": [ + "## Count tokens and compute tokens\n", + "\n", + "You can use `count_tokens` method to calculates the number of input tokens before sending a request to the Gemini API. See the [List and count tokens](https://cloud.google.com/vertex-ai/generative-ai/docs/multimodal/list-token) page for more details.\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "Syx-fwLkV1j-" + }, + "source": [ + "#### Count tokens" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "UhNElguLRRNK" + }, + "outputs": [], + "source": [ + "response = client.models.count_tokens(\n", + " model=MODEL_ID,\n", + " contents=\"What's the highest mountain in Africa?\",\n", + ")\n", + "\n", + "print(response)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "VS-AP7AHUQmV" + }, + "source": [ + "#### Compute tokens\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "Cdhi5AX1TuH0" + }, + "outputs": [], + "source": [ + "response = client.models.compute_tokens(\n", + " model=MODEL_ID,\n", + " contents=\"What's the longest word in the English language?\",\n", + ")\n", + "\n", + "print(response)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "T0pb-Kh1xEHU" + }, + "source": [ + "## Function calling\n", + "\n", + "[Function calling](https://cloud.google.com/vertex-ai/generative-ai/docs/multimodal/function-calling) lets you provide a set of tools that it can use to respond to the user's prompt. You create a description of a function in your code, then pass that description to a language model in a request. The response from the model includes the name of a function that matches the description and the arguments to call it with.\n", + "\n", + "For more examples of Function Calling, refer to [this notebook](https://github.com/GoogleCloudPlatform/generative-ai/blob/main/gemini/function-calling/intro_function_calling.ipynb)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "2BDQPwgcxRN3" + }, + "outputs": [], + "source": [ + "get_destination = FunctionDeclaration(\n", + " name=\"get_destination\",\n", + " description=\"Get the destination that the user wants to go to\",\n", + " parameters={\n", + " \"type\": \"OBJECT\",\n", + " \"properties\": {\n", + " \"destination\": {\n", + " \"type\": \"STRING\",\n", + " \"description\": \"Destination that the user wants to go to\",\n", + " },\n", + " },\n", + " },\n", + ")\n", + "\n", + "destination_tool = Tool(\n", + " function_declarations=[get_destination],\n", + ")\n", + "\n", + "response = client.models.generate_content(\n", + " model=MODEL_ID,\n", + " contents=\"I'd like to travel to Paris.\",\n", + " config=GenerateContentConfig(\n", + " tools=[destination_tool],\n", + " temperature=0,\n", + " ),\n", + ")\n", + "\n", + "response.candidates[0].content.parts[0].function_call" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "EA1Sn-VQE6_J" + }, + "source": [ + "## Use context caching\n", + "\n", + "[Context caching](https://cloud.google.com/vertex-ai/generative-ai/docs/context-cache/context-cache-overview) lets you to store frequently used input tokens in a dedicated cache and reference them for subsequent requests, eliminating the need to repeatedly pass the same set of tokens to a model.\n", + "\n", + "**Note**: Context caching is only available for stable models with fixed versions (for example, `gemini-3.5-flash`). You must include the version postfix (for example, the `-001`)." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "nqxTesUPIkNC" + }, + "source": [ + "#### Create a cache" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "adsuvFDA6xP5" + }, + "outputs": [], + "source": [ + "system_instruction = \"\"\"\n", + " You are an expert researcher who has years of experience in conducting systematic literature surveys and meta-analyses of different topics.\n", + " You pride yourself on incredible accuracy and attention to detail. You always stick to the facts in the sources provided, and never make up new facts.\n", + " Now look at the research paper below, and answer the following questions in 1-2 sentences.\n", + "\"\"\"\n", + "\n", + "pdf_parts = [\n", + " Part.from_uri(\n", + " file_uri=\"gs://cloud-samples-data/generative-ai/pdf/2312.11805v3.pdf\",\n", + " mime_type=\"application/pdf\",\n", + " ),\n", + " Part.from_uri(\n", + " file_uri=\"gs://cloud-samples-data/generative-ai/pdf/2403.05530.pdf\",\n", + " mime_type=\"application/pdf\",\n", + " ),\n", + "]\n", + "\n", + "cached_content = client.caches.create(\n", + " model=\"gemini-3.5-flash\",\n", + " config=CreateCachedContentConfig(\n", + " system_instruction=system_instruction,\n", + " contents=pdf_parts,\n", + " ttl=\"3600s\",\n", + " ),\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "JBdQNHEoJmC5" + }, + "source": [ + "#### Use a cache" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "N8EhgCzlIoFI" + }, + "outputs": [], + "source": [ + "response = client.models.generate_content(\n", + " model=\"gemini-3.5-flash\",\n", + " contents=\"What is the research goal shared by these research papers?\",\n", + " config=GenerateContentConfig(\n", + " cached_content=cached_content.name,\n", + " ),\n", + ")\n", + "\n", + "print(response.text)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "azhqrdiCer19" + }, + "source": [ + "#### Delete a cache" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "rAUYcfOUdeoi" + }, + "outputs": [], + "source": [ + "client.caches.delete(name=cached_content.name)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "43be33d2672b" + }, + "source": [ + "## Batch prediction\n", + "\n", + "Different from getting online (synchronous) responses, where you are limited to one input request at a time, [batch predictions for the Gemini API in Vertex AI](https://cloud.google.com/vertex-ai/generative-ai/docs/multimodal/batch-prediction-gemini) allow you to send a large number of requests to Gemini in a single batch request. Then, the model responses asynchronously populate to your storage output location in [Cloud Storage](https://cloud.google.com/storage/docs/introduction) or [BigQuery](https://cloud.google.com/bigquery/docs/storage_overview).\n", + "\n", + "Batch predictions are generally more efficient and cost-effective than online predictions when processing a large number of inputs that are not latency sensitive." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "adf948ae326b" + }, + "source": [ + "### Prepare batch inputs\n", + "\n", + "The input for batch requests specifies the items to send to your model for prediction.\n", + "\n", + "Batch requests for Gemini accept BigQuery storage sources and Cloud Storage sources. You can learn more about the batch input formats in the [Batch text generation](https://cloud.google.com/vertex-ai/generative-ai/docs/multimodal/batch-prediction-gemini#prepare_your_inputs) page.\n", + "\n", + "This tutorial uses Cloud Storage as an example. The requirements for Cloud Storage input are:\n", + "\n", + "- File format: [JSON Lines (JSONL)](https://jsonlines.org/)\n", + "- Located in `us-central1`\n", + "- Appropriate read permissions for the service account\n", + "\n", + "Each request that you send to a model can include parameters that control how the model generates a response. Learn more about Gemini parameters in the [Experiment with parameter values](https://cloud.google.com/vertex-ai/generative-ai/docs/learn/prompts/adjust-parameter-values) page.\n", + "\n", + "This is one of the example requests in the input JSONL file `batch_requests_for_multimodal_input_2.jsonl`:\n", + "\n", + "```json\n", + "{\"request\":{\"contents\": [{\"role\": \"user\", \"parts\": [{\"text\": \"List objects in this image.\"}, {\"file_data\": {\"file_uri\": \"gs://cloud-samples-data/generative-ai/image/office-desk.jpeg\", \"mime_type\": \"image/jpeg\"}}]}],\"generationConfig\":{\"temperature\": 0.4}}}\n", + "```" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "81b25154a51a" + }, + "outputs": [], + "source": [ + "# fmt: off\n", + "INPUT_DATA = \"gs://cloud-samples-data/generative-ai/batch/batch_requests_for_multimodal_input_2.jsonl\" # @param {type:\"string\"}\n", + "# fmt: on" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "2031bb3f44c2" + }, + "source": [ + "### Prepare batch output location\n", + "\n", + "When a batch prediction task completes, the output is stored in the location that you specified in your request.\n", + "\n", + "- The location is in the form of a Cloud Storage or BigQuery URI prefix, for example:\n", + "`gs://path/to/output/data` or `bq://projectId.bqDatasetId`.\n", + "\n", + "- If not specified, `gs://STAGING_BUCKET/gen-ai-batch-prediction` will be used for Cloud Storage source and `bq://PROJECT_ID.gen_ai_batch_prediction.predictions_TIMESTAMP` will be used for BigQuery source.\n", + "\n", + "This tutorial uses a Cloud Storage bucket as an example for the output location.\n", + "\n", + "- You can specify the URI of your Cloud Storage bucket in `BUCKET_URI`, or\n", + "- if it is not specified, a new Cloud Storage bucket in the form of `gs://PROJECT_ID-TIMESTAMP` will be created for you." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "fddd98cd84cd" + }, + "outputs": [], + "source": [ + "BUCKET_URI = \"[your-cloud-storage-bucket]\" # @param {type:\"string\"}\n", + "\n", + "if BUCKET_URI == \"[your-cloud-storage-bucket]\":\n", + " TIMESTAMP = datetime.datetime.now().strftime(\"%Y%m%d%H%M%S\")\n", + " BUCKET_URI = f\"gs://{PROJECT_ID}-{TIMESTAMP}\"\n", + "\n", + " ! gsutil mb -l {LOCATION} -p {PROJECT_ID} {BUCKET_URI}" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "d7da62c98880" + }, + "source": [ + "### Send a batch prediction request\n", + "\n", + "To make a batch prediction request, you specify a source model ID, an input source and an output location where Vertex AI stores the batch prediction results.\n", + "\n", + "To learn more, see the [Batch prediction API](https://cloud.google.com/vertex-ai/generative-ai/docs/model-reference/batch-prediction-api) page.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "7ed3c2925663" + }, + "outputs": [], + "source": [ + "batch_job = client.batches.create(\n", + " model=MODEL_ID,\n", + " src=INPUT_DATA,\n", + " config=CreateBatchJobConfig(dest=BUCKET_URI),\n", + ")\n", + "batch_job.name" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "f1bd49ff2c9e" + }, + "source": [ + "Print out the job status and other properties. You can also check the status in the Cloud Console at https://console.cloud.google.com/vertex-ai/batch-predictions" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "ee2ec586e4f1" + }, + "outputs": [], + "source": [ + "batch_job = client.batches.get(name=batch_job.name)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "64eaf082ecb0" + }, + "source": [ + "Optionally, you can list all the batch prediction jobs in the project." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "da8e9d43a89b" + }, + "outputs": [], + "source": [ + "for job in client.batches.list():\n", + " print(job.name, job.create_time, job.state)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "de178468ba15" + }, + "source": [ + "### Wait for the batch prediction job to complete\n", + "\n", + "Depending on the number of input items that you submitted, a batch generation task can take some time to complete. You can use the following code to check the job status and wait for the job to complete." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "c2187c091738" + }, + "outputs": [], + "source": [ + "import time\n", + "\n", + "# Refresh the job until complete\n", + "while batch_job.state == \"JOB_STATE_RUNNING\":\n", + " time.sleep(5)\n", + " batch_job = client.batches.get(name=batch_job.name)\n", + "\n", + "# Check if the job succeeds\n", + "if batch_job.state == \"JOB_STATE_SUCCEEDED\":\n", + " print(\"Job succeeded!\")\n", + "else:\n", + " print(f\"Job failed: {batch_job.error}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "0156eaf66675" + }, + "source": [ + "### Retrieve batch prediction results\n", + "\n", + "When a batch prediction task is complete, the output of the prediction is stored in the location that you specified in your request. It is also available in `batch_job.dest.bigquery_uri` or `batch_job.dest.gcs_uri`.\n", + "\n", + "Example output:\n", + "\n", + "```json\n", + "{\"status\": \"\", \"processed_time\": \"2024-11-13T14:04:28.376+00:00\", \"request\": {\"contents\": [{\"parts\": [{\"file_data\": null, \"text\": \"List objects in this image.\"}, {\"file_data\": {\"file_uri\": \"gs://cloud-samples-data/generative-ai/image/gardening-tools.jpeg\", \"mime_type\": \"image/jpeg\"}, \"text\": null}], \"role\": \"user\"}], \"generationConfig\": {\"temperature\": 0.4}}, \"response\": {\"candidates\": [{\"avgLogprobs\": -0.10394711927934126, \"content\": {\"parts\": [{\"text\": \"Here's a list of the objects in the image:\\n\\n* **Watering can:** A green plastic watering can with a white rose head.\\n* **Plant:** A small plant (possibly oregano) in a terracotta pot.\\n* **Terracotta pots:** Two terracotta pots, one containing the plant and another empty, stacked on top of each other.\\n* **Gardening gloves:** A pair of striped gardening gloves.\\n* **Gardening tools:** A small trowel and a hand cultivator (hoe). Both are green with black handles.\"}], \"role\": \"model\"}, \"finishReason\": \"STOP\"}], \"modelVersion\": \"gemini-3.5-flash@default\", \"usageMetadata\": {\"candidatesTokenCount\": 110, \"promptTokenCount\": 264, \"totalTokenCount\": 374}}}\n", + "```" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "c2ce0968112c" + }, + "outputs": [], + "source": [ + "import fsspec\n", + "import pandas as pd\n", + "\n", + "fs = fsspec.filesystem(\"gcs\")\n", + "\n", + "file_paths = fs.glob(f\"{batch_job.dest.gcs_uri}/*/predictions.jsonl\")\n", + "\n", + "if batch_job.state == \"JOB_STATE_SUCCEEDED\":\n", + " # Load the JSONL file into a DataFrame\n", + " df = pd.read_json(f\"gs://{file_paths[0]}\", lines=True)\n", + "\n", + " display(df)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "f81ccNPjiVzH" + }, + "source": [ + "## Get text embeddings\n", + "\n", + "You can get text embeddings for a snippet of text by using `embed_content` method. All models produce an output with 768 dimensions by default. However, some models give users the option to choose an output dimensionality between `1` and `768`. See [Vertex AI text embeddings API](https://cloud.google.com/vertex-ai/generative-ai/docs/embeddings/get-text-embeddings) for more details." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "zGOCzT7y31rk" + }, + "outputs": [], + "source": [ + "TEXT_EMBEDDING_MODEL_ID = \"text-embedding-005\" # @param {type: \"string\"}" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "s94DkG5JewHJ" + }, + "outputs": [], + "source": [ + "response = client.models.embed_content(\n", + " model=TEXT_EMBEDDING_MODEL_ID,\n", + " contents=[\n", + " \"How do I get a driver's license/learner's permit?\",\n", + " \"How do I renew my driver's license?\",\n", + " \"How do I change my address on my driver's license?\",\n", + " ],\n", + " config=EmbedContentConfig(output_dimensionality=128),\n", + ")\n", + "\n", + "print(response.embeddings)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "eQwiONFdVHw5" + }, + "source": [ + "# What's next\n", + "\n", + "- Explore other notebooks in the [Google Cloud Generative AI GitHub repository](https://github.com/GoogleCloudPlatform/generative-ai).\n", + "- Explore AI models in [Model Garden](https://cloud.google.com/vertex-ai/generative-ai/docs/model-garden/explore-models)." + ] + } + ], + "metadata": { + "colab": { + "name": "intro_genai_sdk.ipynb", + "toc_visible": true + }, + "kernelspec": { + "display_name": "Python 3", + "name": "python3" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} diff --git a/sdk/retries/configure_retries.ipynb b/sdk/retries/configure_retries.ipynb new file mode 100644 index 0000000..2806045 --- /dev/null +++ b/sdk/retries/configure_retries.ipynb @@ -0,0 +1,428 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "id": "2RexaI2BoZZy" + }, + "outputs": [], + "source": [ + "# Copyright 2026 Google LLC\n", + "#\n", + "# Licensed under the Apache License, Version 2.0 (the \"License\");\n", + "# you may not use this file except in compliance with the License.\n", + "# You may obtain a copy of the License at\n", + "#\n", + "# https://www.apache.org/licenses/LICENSE-2.0\n", + "#\n", + "# Unless required by applicable law or agreed to in writing, software\n", + "# distributed under the License is distributed on an \"AS IS\" BASIS,\n", + "# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n", + "# See the License for the specific language governing permissions and\n", + "# limitations under the License." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "rJ43O7oc6xGP" + }, + "source": [ + "# Configuring Retries in the Gen AI SDK\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + "
    \n", + " \n", + " \"Google
    Open in Colab\n", + "
    \n", + "
    \n", + " \n", + " \"Google
    Open in Colab Enterprise\n", + "
    \n", + "
    \n", + " \n", + " \"Workbench
    Open in Workbench\n", + "
    \n", + "
    \n", + " \n", + " \"GitHub
    View on GitHub\n", + "
    \n", + "
    \n", + "\n", + "
    \n", + "\n", + "

    \n", + "Share to:\n", + "\n", + "\n", + " \"LinkedIn\n", + "\n", + "\n", + "\n", + " \"Bluesky\n", + "\n", + "\n", + "\n", + " \"X\n", + "\n", + "\n", + "\n", + " \"Reddit\n", + "\n", + "\n", + "\n", + " \"Facebook\n", + "\n", + "

    " + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "uYusEvgifp9U" + }, + "source": [ + "| Author |\n", + "| --- |\n", + "| [Eric Dong](https://github.com/gericdong) |" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "8sg1R6UQ7Q3t" + }, + "source": [ + "## Overview\n", + "\n", + "When interacting with Gemini through Gemini API, transient network errors or service disruptions can occur. Implementing a retry strategy is crucial for building robust and reliable applications. This guide explains how to configure HTTP retry options for the [Google Gen AI SDK](https://docs.cloud.google.com/vertex-ai/generative-ai/docs/sdks/overview).\n", + "\n", + "\n", + "### **Why Configure Retries?**\n", + "\n", + "By default, the Gen AI SDK has some level of retry for certain types of errors. However, for fine-grained control over the retry behavior, you can configure your own retry strategy. This is particularly important for:\n", + "\n", + "- **Handling transient errors**: Automatically retry requests that fail due to temporary issues, such as network timeouts or temporary server unavailability (e.g., 5xx errors).\n", + "- **Managing rate limits**: If you anticipate hitting rate limits (e.g., 429 errors), a retry strategy with exponential backoff can help manage the request rate and avoid overwhelming the service.\n", + "- **Improving application resilience**: Make your application more resilient to intermittent issues, leading to a better user experience.\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "gPiTOAHURvTM" + }, + "source": [ + "## Getting Started" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "-tn3uw268iw4" + }, + "source": [ + "### Install Google Gen AI SDK for Python\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "3CaXL22k8iw4" + }, + "outputs": [], + "source": [ + "%pip install --upgrade --quiet google-genai" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "xW5WwfAOfp9V" + }, + "source": [ + "### Import libraries\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "o0JUCkvVfp9V" + }, + "outputs": [], + "source": [ + "import os\n", + "import sys\n", + "\n", + "from IPython.display import Markdown, display\n", + "from google import genai\n", + "from google.genai import types" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "SY-GRP3m8iw5" + }, + "source": [ + "### Authenticate your notebook environment\n", + "\n", + "If you are running this notebook on Google Colab, run the cell below to authenticate your environment." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "06RAe75C8iw5" + }, + "outputs": [], + "source": [ + "if \"google.colab\" in sys.modules:\n", + " from google.colab import auth\n", + "\n", + " auth.authenticate_user()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "wkInCpT9fp9V" + }, + "source": [ + "### Authenticate your Google Cloud Project for Vertex AI\n", + "\n", + "You can use a Google Cloud Project or an API Key for authentication. This tutorial uses a Google Cloud Project.\n", + "\n", + "- [Enable the Vertex AI API](https://console.cloud.google.com/flows/enableapi?apiid=aiplatform.googleapis.com)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "z-jVOPQVfp9V" + }, + "outputs": [], + "source": [ + "# fmt: off\n", + "PROJECT_ID = \"[your-project-id]\" # @param {type: \"string\", placeholder: \"[your-project-id]\", isTemplate: true}\n", + "# fmt: on\n", + "if not PROJECT_ID or PROJECT_ID == \"[your-project-id]\":\n", + " PROJECT_ID = str(os.environ.get(\"GOOGLE_CLOUD_PROJECT\"))\n", + "\n", + "LOCATION = \"global\"" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "LBGTO9MUFWP0" + }, + "source": [ + "## **Option 1**: Configuring Retries at the Client Level\n", + "\n", + "The Gen AI SDK provides the `genai.types.HttpRetryOptions` class to configure a custom retry policy. You can then apply this policy to the `genai.Client` using `genai.types.HttpOptions`." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "RR5RO9KxFvRR" + }, + "source": [ + "### **Step 1**: Define `HttpRetryOptions`\n", + "\n", + "First, create an instance of `HttpRetryOptions` and specify the desired retry behavior. Here are some of the key parameters:\n", + "\n", + "- `initial_delay`: The initial delay in seconds before the first retry.\n", + "- `attempts`: The maximum number of retry attempts.\n", + "- `exp_base`: The base for the exponential backoff calculation.\n", + "- `max_delay`: The maximum delay in seconds between retries.\n", + "- `jitter`: A factor to add a random delay to the backoff, which can help avoid a \"thundering herd\" problem.\n", + "- `http_status_codes`: A list of HTTP status codes that should trigger a retry." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "pzHKE306dSJ2" + }, + "outputs": [], + "source": [ + "# Configure the retry policy.\n", + "retry_options = types.HttpRetryOptions(\n", + " initial_delay=1.0, # Initial delay of 1 second.\n", + " attempts=5, # Retry up to 5 times.\n", + " exp_base=2, # Double the delay for each subsequent retry.\n", + " http_status_codes=[429, 500, 502, 503, 504], # Retry on these server errors.\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "sP0bwwcoGJKE" + }, + "source": [ + "### **Step 2**: Apply to the Client with `HttpOptions`\n", + "\n", + "Next, create an instance of `HttpOptions` and pass your `retry_options` to it.\n", + "\n", + "You can also configure other HTTP options, such as `timeout` - Timeout for the request in milliseconds." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "D90UFQOpGaEh" + }, + "outputs": [], + "source": [ + "# Configure HTTP options with the retry settings.\n", + "http_options = types.HttpOptions(\n", + " retry_options=retry_options,\n", + " timeout=120 * 1000, # Set a 120-second timeout for requests.\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "u8Vh9ePQGp_A" + }, + "source": [ + "### **Step 3**: Initialize the `genai.Client`\n", + "\n", + "Finally, initialize the `genai.Client` with the `http_options`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "lSvDL3LoGzDx" + }, + "outputs": [], + "source": [ + "# Initialize the client with the configured HTTP options.\n", + "client = genai.Client(\n", + " vertexai=True,\n", + " project=PROJECT_ID,\n", + " location=LOCATION,\n", + " http_options=http_options,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "k5aRhD50HBSM" + }, + "source": [ + "Now, any API calls made with this client will use the configured retry policy." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "5kFIrV5NfD0d" + }, + "source": [ + "### Complete Code Example\n", + "\n", + "Here is the complete code sample." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "pdoPLP6lfMFH" + }, + "outputs": [], + "source": [ + "client = genai.Client(\n", + " vertexai=True,\n", + " project=PROJECT_ID,\n", + " location=LOCATION,\n", + " http_options=types.HttpOptions(\n", + " retry_options=types.HttpRetryOptions(\n", + " initial_delay=1.0,\n", + " attempts=10,\n", + " http_status_codes=[429, 500, 502, 503, 504],\n", + " ),\n", + " timeout=120 * 1000,\n", + " ),\n", + ")\n", + "\n", + "# You can now use this client to make API calls with the custom retry policy.\n", + "# For example:\n", + "response = client.models.generate_content(\n", + " model=\"gemini-3.5-flash\", contents=\"Tell me a story\"\n", + ")\n", + "\n", + "display(Markdown(response.text))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "b5386769b966" + }, + "source": [ + "## **Option 2**: Configuring Retries at the Request Level\n", + "\n", + "You can also set `HttpRetryOptions` per-request using the `config` parameter." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "620c8177fc44" + }, + "outputs": [], + "source": [ + "response = client.models.generate_content(\n", + " model=\"gemini-3.5-flash\",\n", + " contents=\"Tell me a joke about a rabbit.\",\n", + " config=types.GenerateContentConfig(\n", + " http_options=types.HttpOptions(\n", + " retry_options=types.HttpRetryOptions(\n", + " initial_delay=1.0,\n", + " attempts=10,\n", + " http_status_codes=[429, 500, 502, 503, 504],\n", + " ),\n", + " timeout=120 * 1000,\n", + " )\n", + " ),\n", + ")\n", + "\n", + "display(Markdown(response.text))" + ] + } + ], + "metadata": { + "colab": { + "name": "configure_retries.ipynb", + "toc_visible": true + }, + "kernelspec": { + "display_name": "Python 3", + "name": "python3" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} diff --git a/search/README.md b/search/README.md new file mode 100644 index 0000000..7d2d4d4 --- /dev/null +++ b/search/README.md @@ -0,0 +1,23 @@ +# Vertex AI Search + +This directory contains notebooks and resources for Vertex AI Search. + +## Notebooks + +- [Create a Vertex AI Datastore and Search Engine](create_datastore_and_search.ipynb): Learn how to create and populate a Vertex AI Search Datastore, create a search app connected to that datastore, and submit queries through the search engine. +- [Querying Blended Data Apps and Summarization with Gemini](search_data_blending_with_gemini_summarization.ipynb): Learn how to call a search app with mixed datastore, get search snippets and summarize the response using Gemini. +- [Vertex AI Search with Filters & Metadata](search_filters_metadata.ipynb): Learn how to use filters and metadata in search requests to Vertex AI Search. + +## Samples + +- [Gemini Enterprise](gemini-enterprise/): Learn how to use Gemini Enterprise with Vertex AI Search. +- [Bulk Question Answering](bulk-question-answering/): Learn how to perform bulk question answering with Vertex AI Search. +- [Cloud Function](cloud-function/): Learn how to access Vertex AI Search via Google Cloud Functions. +- [Custom Embeddings](custom-embeddings/): Learn how to use custom embeddings with Vertex AI Search. +- [Custom Ranking](custom-ranking/): Learn how to use custom ranking with Vertex AI Search. +- [Ranking API](ranking-api/): Learn how to use the Ranking API with Vertex AI Search. +- [Retrieval-Augmented Generation](retrieval-augmented-generation/): Learn how to use Google Cloud Vertex AI Search, PaLM and LangChain for Retrieval Augmented Generation. +- [Tuning](tuning/): Learn how to tune Vertex AI Search. +- [VAIS Building Blocks](vais-building-blocks/): A collection of notebooks that demonstrate how different functionalities within Vertex AI Search can be used as building blocks to achieve higher-level goals. +- [Vertex AI Search Options](vertexai-search-options/): Learn about the different options available for Vertex AI Search. +- [Web App](web-app/): A demo that illustrates how to search through a corpus of documents using Vertex AI Search. diff --git a/search/auto-rag-eval/README.md b/search/auto-rag-eval/README.md new file mode 100644 index 0000000..4dc4172 --- /dev/null +++ b/search/auto-rag-eval/README.md @@ -0,0 +1,458 @@ +# Auto RAG Eval: Automated Benchmark Generation for RAG Systems + +| Authors | +| --- | +| [Pouya Omran](https://github.com/PouyaOmran) | +| [Tanya Dixit](https://github.com/coderkhaleesi) | +| [Jingyi Wang](https://github.com/Jingyi-Wang-jessie) | + +## TL;DR - Quick Start + +```bash +# 1. Install requirements +pip install -r requirements.txt + +# 2. Download required files from Google Cloud Storage +gcloud storage cp gs://github-repo/search/auto-rag-eval/qa_profiles.json . + +# 3. Set up environment +# Edit .env with your values: +# - PROJECT_ID=your-gcp-project-id +# - LOCATION=us-central1 +# - DATA_STORE_ID=your-vertex-ai-search-datastore-id + +# 4. Authenticate with Google Cloud +gcloud auth application-default login + +# 5. Generate benchmark +python main.py --docs 2 --chunks 2 --clues 2 --profiles 2 + +# 6. (Optional) Transform benchmark for evaluation frameworks +python transform_benchmark.py benchmark.json converted_benchmark.json +``` + +That's it! Your benchmark Q&A pairs will be saved to `benchmark.json`. + +## What is it? + +Auto RAG Eval is an automated benchmark generation tool for evaluating Retrieval-Augmented Generation (RAG) systems. +It creates high-quality question-answer pairs from your document corpus using Google Cloud's Vertex AI Search and Gemini models. + +The primary component of this solution is the **Benchmark Generator** (`main.py`), which creates the Q&A pairs from your documents in Vertex AI Search. + +A secondary utility, the **Benchmark Transformer** (`transform_benchmark.py`), is also provided. This simple script can be used to convert the generated benchmark into a format compatible with evaluation frameworks like Google's Agent Development Kit (ADK). + +## Why do we need this solution? + +### The Challenge +- **Manual Benchmark Creation is Time-Consuming**: Creating quality benchmarks for RAG systems manually can take weeks or months +- **Coverage Gaps**: Human-created benchmarks often miss edge cases and don't comprehensively cover the document corpus +- **Scalability Issues**: As document stores grow, maintaining relevant benchmarks becomes increasingly difficult +- **Evaluation Consistency**: Without standardized benchmarks, it's hard to consistently evaluate RAG system performance + +### The Solution +Auto RAG Eval addresses these challenges by: +- **Automating Benchmark Generation**: Creates hundreds of Q&A pairs in hours instead of weeks +- **Ensuring Comprehensive Coverage**: Systematically samples documents and chunks to cover the entire corpus +- **Multi-Stage Quality Control**: Uses multiple AI agents to review and validate each Q&A pair +- **Scalable Architecture**: Works with any size Vertex AI Search data store +- **Format Flexibility**: Transforms benchmarks into various formats for different evaluation frameworks + +## How to use this solution + +### Prerequisites + +1. **Google Cloud Project** with the following APIs enabled: + - Vertex AI API + - Discovery Engine API (for Vertex AI Search) + - Cloud Storage API + +2. **Authentication**: + ```bash + # Set up Application Default Credentials + gcloud auth application-default login + ``` + +3. **Vertex AI Search Data Store**: + - Create a data store in Vertex AI Search + - Ingest your documents into the data store + - Note the data store ID + - Detailed instructions on creating a data store in Google Cloud Console: + 1. **Navigate to AI Applications** in the Google Cloud Console + 2. **Create a new data store** for your application + 3. **Configure the parser settings**: + - Set the parser to either **Digital Parser** or **Layout Parser** + 4. **Enable Advanced Chunking Configuration**: + - ✓ Tick **"Include ancestor headings in chunks"** + - Keep all other settings at their default values + 5. **Ingest your free text documents** into the data store + 6. **Copy the DATA_STORE_ID** from the console + 7. **Add the DATA_STORE_ID to your `.env` file**: + DATA_STORE_ID=your-data-store-id + +Upload the documents in exemplary_docs to the datastore from gs://github-repo/search/auto-rag-eval/ + +4. **Python Environment**: + ```bash + # Install all required dependencies + pip install -r requirements.txt + ``` + +5. **Required Files**: + - `qa_profiles.json`: Q&A generation profiles. Download it from Google Cloud Storage: + ```bash + gcloud storage cp gs://github-repo/search/auto-rag-eval/qa_profiles.json . + ``` + - `.env`: Environment configuration (create from `env_example`) + +### Step 1: Configure Environment + +Create a `.env` file based on `env_example`: + +```bash +# Copy the example file +cp env_example .env + +# Edit with your values +PROJECT_ID=your-gcp-project-id +LOCATION=us-central1 +DATA_STORE_ID=your-data-store-id +``` + +### Step 2: Generate Benchmark + +Run the benchmark generator with default settings: + +```bash +python main.py +``` + +Or customize the generation parameters: + +```bash +python main.py \ + --docs 5 \ + --chunks 3 \ + --clues 2 \ + --profiles 2 \ + --chunks-to-merge 3 \ + --output-file my_benchmark.json \ + --qa-profiles-file custom_profiles.json +``` + +**Parameters**: +- `--project-id`: Override PROJECT_ID from .env file +- `--location`: Override LOCATION from .env file +- `--data-store-id`: Override DATA_STORE_ID from .env file +- `--docs`: Number of documents to process (default: 2) +- `--chunks`: Number of chunks per document (default: 2) +- `--clues`: Number of clues per chunk (default: 2) +- `--profiles`: Number of Q&A profiles per clue (default: 2) +- `--chunks-to-merge`: Number of chunks to merge into bigger chunks (default: 3) +- `--output-file`: Output JSON filename (default: benchmark.json) +- `--qa-profiles-file`: Path to custom QA profiles JSON file (default: qa_profiles.json in script directory) +- `--llm-model`: LLM model to use (default: gemini-2.0-flash) +- `--top-k-chunks`: Number of top chunks to retrieve during context search (default: 3) +- `--neighbour-chunks`: Number of neighboring chunks to include for context (default: 0) +- `--max-retries`: Maximum retry attempts for API calls (default: 3) + +### Step 3: Transform Benchmark (Optional) + +After generating your benchmark, you can optionally convert it for use with evaluation frameworks like the Agent Development Kit (ADK). You can use the standalone Python script. + +```bash +python transform_benchmark.py benchmark.json converted_bench.json +``` + +The script accepts command-line arguments: +- First argument: Input benchmark file path +- Second argument: Output file path +- Optional `--indent`: JSON indentation level (default: 2) + +The transformer converts from Auto RAG Eval format: +```json +{ + "context": "...", + "Q&A Gen Profile": {...}, + "Question": "...", + "Answer": "..." +} +``` + +To ADK evaluation format: +```json +{ + "query": "...", + "expected_tool_use": [], + "reference": "..." +} +``` + +## Architecture Overview + +### System Architecture + +Auto RAG Eval follows a sophisticated multi-stage pipeline architecture that orchestrates various AI models and services to generate high-quality Q&A pairs: + +``` +┌─────────────────────────┐ +│ Vertex AI Search │ +│ (Document Store) │ +└───────────┬─────────────┘ + │ + â–Œ +┌─────────────────────────┐ ┌─────────────────────┐ +│ Document Selection │────▶│ Chunk Processing │ +│ - List all documents │ │ - Retrieve chunks │ +│ - Random sampling │ │ - Merge chunks │ +└─────────────────────────┘ └──────────┬──────────┘ + │ + â–Œ + ┌─────────────────────┐ + │ Clue Generation │ + │ - Identify topics │ + │ - Generate question│ + └──────────┬──────────┘ + │ + â–Œ +┌─────────────────────────┐ ┌─────────────────────┐ +│ Context Retrieval │────▶│ Context Distillation│ +│ - Search with clues │ │ - Relevance filter │ +│ - Find related chunks │ │ - Extract focused │ +└─────────────────────────┘ │ content │ + └──────────┬──────────┘ + │ + â–Œ +┌─────────────────────────┐ ┌─────────────────────┐ +│ Q&A Profile Generation │────▶│ Q&A Generation │ +│ - Analyze context │ │ - Create Q&A pairs │ +│ - Suggest profiles │ │ - Self-contained │ +└─────────────────────────┘ └──────────┬──────────┘ + │ + â–Œ +┌─────────────────────────┐ ┌─────────────────────┐ +│ Multi-Agent Review │────▶│ Incremental Saving │ +│ - 3 AI critics │ │ - Immediate save │ +│ - Consensus decision │ │ - JSON output │ +└─────────────────────────┘ └─────────────────────┘ +``` + +### Data Flow + +The data flows through the pipeline as follows: + +1. **Input**: Vertex AI Search data store containing your documents +2. **Processing**: Documents → Chunks → Clues → Retrieved Contexts → Distilled Context → Profiles → Q&As +3. **Output**: JSON file with validated Q&A pairs + +### Detailed Pipeline Stages + +1. **Document Selection** + - Lists all documents from Vertex AI Search data store + - Randomly selects specified number of documents + - Ensures diverse coverage of the corpus + +2. **Chunk Processing** + - Retrieves chunks for each selected document + - Merges consecutive chunks into bigger chunks for better context + - Default: merges 3 chunks together + - Randomly selects chunks for processing + +3. **Clue Generation** + - For each chunk, generates potential questions (clues) + - Uses Gemini model to identify key topics and concepts + - Ensures questions are answerable from the text + +4. **Context Retrieval** + - For each clue, searches for relevant contexts using Vertex AI Search + - Enhances the clue with hypothetical examples and descriptions + - Retrieves top-k most relevant chunks from the entire corpus + - Can include neighboring chunks for additional context + +5. **Context Distillation** + - Extracts only the most relevant portions from retrieved contexts + - Uses both individual block-level and overall document-level relevance assessment + - Filters out irrelevant information to create focused context + - Aggregates relevant text from multiple sources + +6. **Q&A Profile Generation** + - Analyzes the distilled context to suggest Q&A generation profiles + - Profiles vary by customizable dimensions from `qa_profiles.json` + - Default dimensions: Type, Persona, Scope, Difficulty + - Generates diverse profiles for comprehensive coverage + +7. **Q&A Generation** + - Creates question-answer pairs based on profiles and distilled context + - Ensures Q&As are self-contained and context-based + - Questions synthesize information across the entire context + - Answers are grounded only in the provided text + +### Key Design Decisions + +1. **Incremental Processing**: Each Q&A is saved immediately upon approval, preventing data loss +2. **Multi-Stage Relevance**: Both individual and aggregate relevance assessment ensures comprehensive context +3. **Consensus-Based Review**: Multiple AI critics ensure high-quality output +4. **Flexible Profiles**: Customizable Q&A dimensions via external JSON configuration +5. **Retry Mechanisms**: Automatic retry with exponential backoff for API resilience +6. **Progress Tracking**: Detailed logging with [LOGGING] prefix for monitoring + +### API Integration Points + +- **Vertex AI Search**: Document listing, chunk retrieval, semantic search +- **Gemini Models**: Clue generation, profile suggestion, Q&A generation, review +- **Google Cloud Storage**: Optional for document storage +- **Discovery Engine API**: Core search and retrieval functionality + +### Key Features + +- **Fault Tolerance**: Automatic retry with exponential backoff for API failures +- **Progress Tracking**: Detailed logging with [LOGGING] prefix +- **Incremental Saving**: Each Q&A saved immediately upon approval +- **Quality Control**: Multi-agent review system ensures high-quality outputs +- **Diversity**: Generates varied Q&A types for comprehensive evaluation + +## Example Data and Output + +### Exemplary Documents + +This repository includes a folder `exemplary_docs/` containing three PDF documents about Google AI agents: +- `input_2_ai-responsibility-update-published-february-2025.pdf` - Google's AI responsibility update +- `input_2_exec_guide_gen_ai.pdf` - Executive guide to generative AI +- `input_2_google-about-generative-ai.pdf` - General information about Google's generative AI + +These documents were ingested into a Vertex AI Search data store with the following settings: +- **LLM feature enabled** for table and image annotation +- **Layout parser option** enabled during ingestion time +- The data store ID is configured in the `env example` file + +### Generated Benchmark Files + +From these exemplary documents, we have generated: +- `benchmark.json` - The raw benchmark output from Auto RAG Eval containing 15 Q&A pairs +- `converted_benchmark.json` - The transformed benchmark ready for evaluation frameworks + +## Output Format + +### Auto RAG Eval Benchmark Format (benchmark.json) +```json +[ + { + "context": "The distilled context used for Q&A generation", + "Q&A Gen Profile": { + "type": "How-to", + "persona": "The Expert", + "scope": "Whole", + "difficulty": "Hard" + }, + "Question": "The generated question", + "Answer": "The generated answer" + } +] +``` + +### ADK Format (after transformation) +```json +[ + { + "query": "The generated question", + "expected_tool_use": [], + "reference": "The generated answer" + } +] +``` + +## Customizing Q&A Profiles + +The `qa_profiles.json` file contains the configuration for Q&A generation. The updated script now supports flexible dimension handling: + +### What You Can Customize: + +1. **Dimension Names**: You can rename dimensions (e.g., "Type" → "QuestionType", "Persona" → "AudienceLevel") +2. **Number of Dimensions**: Add or remove dimensions as needed (minimum 1 dimension required) +3. **Dimension Values**: Add, remove, or modify values within each dimension +4. **Value Descriptions**: Customize descriptions for each value + +### Structure Requirements: + +The only requirement is maintaining this JSON structure: +```json +{ + "parameters": { + "YourDimensionName": { + "description": "Description of this dimension", + "values": { + "ValueName1": {"description": "Description of this value"}, + "ValueName2": {"description": "Description of this value"} + } + } + } +} +``` + +### Example: Custom Profile with Different Dimensions + +```json +{ + "parameters": { + "Domain": { + "description": "Subject area", + "values": { + "Technical": {"description": "Technical documentation"}, + "Business": {"description": "Business processes"} + } + } + } +} +``` + +### Note + +While RAG-Crusher employs an intelligent, LLM-infused methodology to automatically generate high-quality benchmarks, the +generated Q&A pairs should be treated with caution. Despite the multi-stage quality control and multi-agent review process, +the ultimate validity and accuracy of the generated benchmarks should be verified by human domain experts who are familiar with +the subject matter. +We recommend: + +Having subject matter experts review the generated Q&A pairs before using them in production evaluations +Treating the generated benchmark as a starting point that requires human validation +Being particularly careful with answers related to critical, safety-sensitive, or highly specialized domains +Manually reviewing a representative sample of the generated pairs to ensure they meet your quality standards + +The tool is designed to accelerate benchmark creation, not to replace human expertise and judgment. + + +### To customize profiles: +1. Edit `qa_profiles.json` with your desired dimensions and values +2. Run the benchmark generator - it will automatically adapt to your new structure +3. The script will validate the structure and use whatever dimensions you provide + +## Monitoring and Troubleshooting + +### Logging +- Look for `[LOGGING]` prefix in console output for detailed execution tracking +- Each function entry/exit is logged +- API retry attempts are logged with error details + +### Common Issues + +1. **Authentication Errors**: + ```bash + gcloud auth application-default login + ``` + +2. **API Rate Limits**: + - Adjust delays in the code + - Reduce concurrent processing + +3. **Empty Benchmark**: + - Check data store ID is correct + - Verify documents are properly ingested + - Check API permissions + +4. **Memory Issues**: + - Process fewer documents at once + - Reduce chunk merge size + +5. **Missing qa_profiles.json**: + - The script will use default profiles if the file is missing + - Check that the file is in the same directory as the script diff --git a/search/auto-rag-eval/llm_utils.py b/search/auto-rag-eval/llm_utils.py new file mode 100644 index 0000000..f461db4 --- /dev/null +++ b/search/auto-rag-eval/llm_utils.py @@ -0,0 +1,163 @@ +from google import genai +from google.genai import types +from pydantic import BaseModel, Field + +# Pydantic models for Structured Output + + +class QuestionClue(BaseModel): + chain_of_thought: str = Field( + ..., + description="Reasoning for why this question is relevant and answerable based on the text.", + ) + question: str = Field(..., description="The question itself.") + + +class ClueResponse(BaseModel): + questions: list[QuestionClue] + + +class TargetedInfo(BaseModel): + description: str = Field( + ..., + description="Concise description of the type of text that would be most relevant.", + ) + original_question: str = Field( + ..., description="Rephrased query as a clear and concise question." + ) + hypothetical_example: str = Field( + ..., + description="Hypothetical excerpt of text that could be part of a relevant document.", + ) + + +class QAPair(BaseModel): + question: str + answer: str + + +class ReviewResult(BaseModel): + decision: str = Field(..., description="APPROVED or REJECTED") + reasoning: str = Field(..., description="Reasoning for the decision") + + +def get_client(project_id: str, location: str): + return genai.Client(vertexai=True, project=project_id, location=location) + + +def clue_generator( + text: str, client: genai.Client, model_name: str = "gemini-2.0-flash" +) -> ClueResponse: + """Generate clues from text using Structured Output""" + prompt = f""" + Reference Text: + {text} + + Task: + You are a reference question creator. Imagine the provided text is a section from a comprehensive reference document. Based **solely** on the given Reference Text, formulate a set of insightful questions with corresponding reasoning. Each question must be answerable **exclusively** using the information found within the provided text. Do not use any external knowledge or information. + + Each question you generate should be: + 1. **Directly Relevant**: The question must pertain specifically to the content of the Reference Text. + 2. **Comprehensive**: The questions, as a whole, should reflect the major themes and key details present in the Reference Text. + 3. **Sound and Logical**: The questions should be well-formed, clear, and appropriate for a reference context. + 4. **Standalone**: The question should be self-contained and understandable without directly referencing the provided text. + """ + + response = client.models.generate_content( + model=model_name, + contents=prompt, + config=types.GenerateContentConfig( + response_mime_type="application/json", + response_schema=ClueResponse, + ), + ) + return response.parsed + + +def targeted_information_seeking( + query: str, client: genai.Client, model_name: str = "gemini-2.0-flash" +) -> TargetedInfo: + """Generate targeted information for a query using Structured Output""" + prompt = f""" + You are a helpful information retrieval assistant. + I will give you a query, and you need to perform the following three tasks: + 1. **Describe Text**: Provide a concise description of the type of text that would be most relevant for answering the query. + 2. **Original Question**: Rephrase the query as a clear and concise question. + 3. **Hypothetical Example**: Create a hypothetical excerpt (around 50-100 words) of text that could be part of a relevant document. + + Here is the query: "{query}" + """ + + response = client.models.generate_content( + model=model_name, + contents=prompt, + config=types.GenerateContentConfig( + response_mime_type="application/json", + response_schema=TargetedInfo, + ), + ) + return response.parsed + + +def generate_qa_pair( + context: str, + profile: dict, + client: genai.Client, + model_name: str = "gemini-2.0-flash", +) -> QAPair: + """Generate a Q&A pair based on context and profile""" + prompt = f""" + Context: + {context} + + Profile: + {profile} + + Task: + Generate a question and answer pair based on the provided context and profile. + The question should match the profile's type, persona, and difficulty. + The answer must be grounded only in the provided context. + """ + + response = client.models.generate_content( + model=model_name, + contents=prompt, + config=types.GenerateContentConfig( + response_mime_type="application/json", + response_schema=QAPair, + ), + ) + return response.parsed + + +def review_qa_pair( + qa_pair: QAPair, + context: str, + critic_type: str, + client: genai.Client, + model_name: str = "gemini-2.0-flash", +) -> ReviewResult: + """Review a Q&A pair using a specific critic persona""" + prompt = f""" + Context: + {context} + + Question: {qa_pair.question} + Answer: {qa_pair.answer} + + Critic Type: {critic_type} + + Task: + As a {critic_type} critic, review the Q&A pair for accuracy, clarity, and relevance to the context. + Provide a decision (APPROVED or REJECTED) and reasoning. + """ + + response = client.models.generate_content( + model=model_name, + contents=prompt, + config=types.GenerateContentConfig( + response_mime_type="application/json", + response_schema=ReviewResult, + ), + ) + return response.parsed diff --git a/search/auto-rag-eval/main.py b/search/auto-rag-eval/main.py new file mode 100644 index 0000000..583662e --- /dev/null +++ b/search/auto-rag-eval/main.py @@ -0,0 +1,201 @@ +import argparse +import json +import os +import random + +import llm_utils + +# Import local modules +import utils +import vertex_search_utils +from dotenv import load_dotenv + + +def main(): + parser = argparse.ArgumentParser( + description="Auto RAG Eval: Automated Benchmark Generation" + ) + parser.add_argument("--project-id", help="Google Cloud Project ID") + parser.add_argument("--location", help="GCP Region") + parser.add_argument("--data-store-id", help="Vertex AI Search Data Store ID") + parser.add_argument( + "--docs", type=int, default=2, help="Number of documents to process" + ) + parser.add_argument( + "--chunks", type=int, default=2, help="Number of chunks per document" + ) + parser.add_argument( + "--clues", type=int, default=2, help="Number of clues per chunk" + ) + parser.add_argument( + "--profiles", type=int, default=2, help="Number of Q&A profiles per clue" + ) + parser.add_argument( + "--chunks-to-merge", type=int, default=3, help="Number of chunks to merge" + ) + parser.add_argument( + "--output-file", default="benchmark.json", help="Output JSON filename" + ) + parser.add_argument( + "--qa-profiles-file", + default="qa_profiles.json", + help="QA profiles JSON file path", + ) + parser.add_argument( + "--llm-model", default="gemini-2.0-flash", help="LLM model to use" + ) + parser.add_argument( + "--top-k-chunks", type=int, default=3, help="Top K chunks for retrieval" + ) + parser.add_argument( + "--neighbour-chunks", type=int, default=0, help="Number of neighboring chunks" + ) + parser.add_argument( + "--max-retries", type=int, default=3, help="Maximum retry attempts" + ) + + args = parser.parse_args() + + load_dotenv() + project_id = args.project_id or os.getenv("PROJECT_ID") + location = args.location or os.getenv("LOCATION", "us-central1") + data_store_id = args.data_store_id or os.getenv("DATA_STORE_ID") + + if not project_id or not data_store_id: + print( + "Error: Project ID and Data Store ID must be provided via arguments or .env file." + ) + return + + # Download qa_profiles.json if missing + if not os.path.exists(args.qa_profiles_file): + print(f"{args.qa_profiles_file} not found. Attempting to download from GCS...") + # In a real scenario, we would have the bucket name here. + # For now, we'll assume it's provided or skip if not available. + bucket_name = os.getenv("GCS_BUCKET_NAME", "github-repo") + source_blob_name = f"search/auto-rag-eval/{args.qa_profiles_file}" + if not utils.download_from_gcs( + bucket_name, source_blob_name, args.qa_profiles_file + ): + print("Failed to download qa_profiles.json. Using default profiles.") + # Fallback to default profiles if needed, or exit + return + + with open(args.qa_profiles_file) as f: + qa_profiles_data = json.load(f) + + client = llm_utils.get_client(project_id, location) + + print(f"[LOGGING] Starting Auto RAG Eval with {args.docs} documents...") + + try: + documents = vertex_search_utils.list_documents_in_datastore( + project_id, location, data_store_id + ) + if not documents: + print("No documents found in data store.") + return + + selected_docs = random.sample(documents, min(len(documents), args.docs)) + + for doc in selected_docs: + print(f"[LOGGING] Processing document: {doc['id']}") + chunks = vertex_search_utils.list_chunks_for_document( + doc["id"], project_id, location, data_store_id + ) + if not chunks: + continue + + bigger_chunks = vertex_search_utils.merge_chunks_into_bigger_chunks( + chunks, args.chunks_to_merge + ) + selected_chunks = random.sample( + bigger_chunks, min(len(bigger_chunks), args.chunks) + ) + + for chunk in selected_chunks: + try: + clues_response = llm_utils.clue_generator( + chunk["content"], client, args.llm_model + ) + selected_clues = random.sample( + clues_response.questions, + min(len(clues_response.questions), args.clues), + ) + + for clue in selected_clues: + # Context enhancement and search + target_info = llm_utils.targeted_information_seeking( + clue.question, client, args.llm_model + ) + search_results = ( + vertex_search_utils.search_with_chunk_augmentation( + target_info.original_question, + project_id, + location, + data_store_id, + args.top_k_chunks, + args.neighbour_chunks, + ) + ) + + if not search_results: + continue + + # Use first result's augmented content as context for simplicity in this refactor + context = search_results[0]["augmented_content"] + + # Generate Q&A pairs based on profiles + try: + # For simplicity, we'll randomly select profiles from the loaded data + # In a real scenario, we might use LLM to suggest profiles first + for _ in range(args.profiles): + # Randomly construct a profile from available dimensions + profile = {} + for dimension, details in qa_profiles_data[ + "parameters" + ].items(): + value_name = random.choice( + list(details["values"].keys()) + ) + profile[dimension] = details["values"][value_name] + profile[dimension]["name"] = value_name + + qa_pair = llm_utils.generate_qa_pair( + context, profile, client, args.llm_model + ) + + # Review + # Simplified review: just use one critic for now + review = llm_utils.review_qa_pair( + qa_pair, context, "Analyst", client, args.llm_model + ) + + if review.decision == "APPROVED": + benchmark_entry = { + "distilled context:": context, + "qa gen profile:": profile, + "qa:": { + "question": {"question": qa_pair.question}, + "answer": {"answer": qa_pair.answer}, + }, + } + utils.save_qa_incrementally( + benchmark_entry, args.output_file + ) + except KeyError as ke: + print(f"[LOGGING] KeyError during profile generation: {ke}") + print( + f"[LOGGING] qa_profiles_data keys: {qa_profiles_data.keys()}" + ) + continue + except Exception as e: + print(f"[LOGGING] Error processing chunk: {e}") + continue + + except Exception as e: + print(f"An error occurred: {e}") + + +if __name__ == "__main__": + main() diff --git a/search/auto-rag-eval/requirements.txt b/search/auto-rag-eval/requirements.txt new file mode 100644 index 0000000..9766a49 --- /dev/null +++ b/search/auto-rag-eval/requirements.txt @@ -0,0 +1,25 @@ +# Google Cloud AI/ML packages +google-cloud-aiplatform>=1.38.0 +google-cloud-storage>=2.10.0 +google-cloud-discoveryengine>=0.11.0 +google-generativeai>=0.3.0 +vertexai>=1.38.0 + +# Data processing +pandas>=2.0.0 + +# Environment and utilities +python-dotenv>=1.0.0 + +# Note: The following are part of Python standard library and don't need to be installed: +# - json +# - re +# - io +# - time +# - datetime +# - random +# - types +# - os +# - argparse +# - sys +# - pathlib \ No newline at end of file diff --git a/search/auto-rag-eval/transform_benchmark.py b/search/auto-rag-eval/transform_benchmark.py new file mode 100644 index 0000000..72526a6 --- /dev/null +++ b/search/auto-rag-eval/transform_benchmark.py @@ -0,0 +1,125 @@ +#!/usr/bin/env python3 +"""Copyright 2025 Google LLC + +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 + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + + +Benchmark Data Transformation Script + +This script transforms a source benchmark file into the desired format for evaluation. + +The transformation follows this mapping: +- query (target) <-- Question (source) +- expected_tool_use (target) <-- [] (empty list) +- reference (target) <-- Answer (source) +""" + +import argparse +import json +import sys +from pathlib import Path + + +def transform_benchmark_data(source_records): + """Transforms a list of source benchmark records to the target format. + + Args: + source_records: A list of dictionaries in the source format. + + Returns: + A list of dictionaries in the target format. + """ + transformed_data = [] + skipped_count = 0 + + for i, record in enumerate(source_records): + if "Question" in record and "Answer" in record: + transformed_record = { + "query": record["Question"], + "expected_tool_use": [], + "reference": record["Answer"], + } + transformed_data.append(transformed_record) + else: + print( + f"Warning: Skipping record {i + 1} due to missing 'Question' or 'Answer' fields" + ) + skipped_count += 1 + + if skipped_count > 0: + print(f"\nTotal records skipped: {skipped_count}") + + return transformed_data + + +def main(): + parser = argparse.ArgumentParser( + description="Transform benchmark data from source format to target format" + ) + parser.add_argument( + "input_file", type=str, help="Path to the source benchmark JSON file" + ) + parser.add_argument( + "output_file", type=str, help="Path to save the transformed benchmark JSON file" + ) + parser.add_argument( + "--indent", type=int, default=2, help="JSON indentation (default: 2)" + ) + + args = parser.parse_args() + + # Convert to Path objects + input_path = Path(args.input_file) + output_path = Path(args.output_file) + + # Check if input file exists + if not input_path.exists(): + print(f"Error: Input file '{input_path}' does not exist") + sys.exit(1) + + # Load source data + try: + with open(input_path, encoding="utf-8") as f: + source_data = json.load(f) + print(f"Successfully loaded {len(source_data)} records from {input_path}") + except json.JSONDecodeError as e: + print(f"Error: Failed to parse JSON from {input_path}: {e}") + sys.exit(1) + except Exception as e: + print(f"Error: Failed to read file {input_path}: {e}") + sys.exit(1) + + # Ensure source_data is a list + if not isinstance(source_data, list): + print("Error: Source data must be a JSON array") + sys.exit(1) + + # Transform the data + transformed_data = transform_benchmark_data(source_data) + + if not transformed_data: + print("Warning: No records were successfully transformed") + + # Save the transformed data + try: + with open(output_path, "w", encoding="utf-8") as f: + json.dump(transformed_data, f, indent=args.indent, ensure_ascii=False) + print(f"\nSuccessfully transformed {len(transformed_data)} records") + print(f"Saved transformed data to '{output_path}'") + except Exception as e: + print(f"Error: Failed to write output file {output_path}: {e}") + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/search/auto-rag-eval/utils.py b/search/auto-rag-eval/utils.py new file mode 100644 index 0000000..9cf5413 --- /dev/null +++ b/search/auto-rag-eval/utils.py @@ -0,0 +1,91 @@ +import json +import os +import types + +from google.cloud import storage + + +def convert_to_serializable(obj): + """Recursively converts an object to a JSON-serializable representation""" + if isinstance(obj, dict): + return {k: convert_to_serializable(v) for k, v in obj.items()} + if isinstance(obj, list): + return [convert_to_serializable(item) for item in obj] + if isinstance(obj, types.MappingProxyType): + return convert_to_serializable(dict(obj)) + if hasattr(obj, "__dict__"): + return convert_to_serializable(obj.__dict__) + return obj + + +def save_qa_incrementally(benchmark_entry, output_file): + """Save a single Q&A entry incrementally to the output file""" + try: + # Convert the benchmark entry to the format used by convert_list_to_json + formatted_entry = {} + if "distilled context:" in benchmark_entry: + formatted_entry["context"] = convert_to_serializable( + benchmark_entry["distilled context:"] + ) + if "qa gen profile:" in benchmark_entry: + formatted_entry["Q&A Gen Profile"] = convert_to_serializable( + benchmark_entry["qa gen profile:"] + ) + + if "qa:" in benchmark_entry and isinstance(benchmark_entry["qa:"], dict): + qa_data = benchmark_entry["qa:"] + if ( + "question" in qa_data + and isinstance(qa_data["question"], dict) + and "question" in qa_data["question"] + ): + formatted_entry["Question"] = convert_to_serializable( + qa_data["question"]["question"] + ) + if ( + "answer" in qa_data + and isinstance(qa_data["answer"], dict) + and "answer" in qa_data["answer"] + ): + formatted_entry["Answer"] = convert_to_serializable( + qa_data["answer"]["answer"] + ) + + # Read existing data or initialize new list + existing_data = [] + if os.path.exists(output_file): + try: + with open(output_file) as f: + existing_data = json.load(f) + except (json.JSONDecodeError, FileNotFoundError): + existing_data = [] + + # Append new entry + existing_data.append(formatted_entry) + + # Write back + with open(output_file, "w") as f: + json.dump(existing_data, f, indent=4) + + print( + f"[LOGGING] Successfully saved Q&A #{len(existing_data)} to {output_file}" + ) + return True + + except Exception as e: + print(f"[LOGGING] Error saving Q&A incrementally: {e}") + return False + + +def download_from_gcs(bucket_name, source_blob_name, destination_file_name): + """Downloads a blob from the bucket.""" + try: + storage_client = storage.Client() + bucket = storage_client.bucket(bucket_name) + blob = bucket.blob(source_blob_name) + blob.download_to_filename(destination_file_name) + print(f"Blob {source_blob_name} downloaded to {destination_file_name}.") + return True + except Exception as e: + print(f"Error downloading from GCS: {e}") + return False diff --git a/search/auto-rag-eval/vertex_search_utils.py b/search/auto-rag-eval/vertex_search_utils.py new file mode 100644 index 0000000..a67380c --- /dev/null +++ b/search/auto-rag-eval/vertex_search_utils.py @@ -0,0 +1,206 @@ +from typing import Any + +from google.api_core.client_options import ClientOptions +from google.cloud import discoveryengine_v1 as discoveryengine + + +def list_documents_in_datastore( + project_id: str, location: str, data_store_id: str +) -> list[dict[str, str]]: + """List all documents in a Vertex AI Search data store""" + client_options = ClientOptions(api_endpoint="discoveryengine.googleapis.com") + client = discoveryengine.DocumentServiceClient(client_options=client_options) + + parent = client.branch_path( + project=project_id, + location=location, + data_store=data_store_id, + branch="default_branch", + ) + + documents = [] + try: + response = client.list_documents(parent=parent) + for document in response: + doc_info = {"id": document.id, "name": document.name, "metadata": {}} + if hasattr(document, "struct_data") and document.struct_data: + doc_info["metadata"] = document.struct_data + documents.append(doc_info) + return documents + except Exception as e: + raise Exception(f"Failed to list documents: {e}") + + +def search_with_chunk_augmentation( + query: str, + project_id: str, + location: str, + data_store_id: str, + top_n: int = 5, + num_chunks: int = 1, +) -> list[dict[str, Any]]: + """Search the Vertex AI data store and return results with augmented chunks""" + if num_chunks > 5: + num_chunks = 5 + elif num_chunks < 0: + num_chunks = 0 + + client_options = ClientOptions(api_endpoint="discoveryengine.googleapis.com") + client = discoveryengine.SearchServiceClient(client_options=client_options) + + serving_config = client.serving_config_path( + project=project_id, + location=location, + data_store=data_store_id, + serving_config="default_search", + ) + + content_search_spec = discoveryengine.SearchRequest.ContentSearchSpec( + search_result_mode=discoveryengine.SearchRequest.ContentSearchSpec.SearchResultMode.CHUNKS, + chunk_spec=discoveryengine.SearchRequest.ContentSearchSpec.ChunkSpec( + num_previous_chunks=num_chunks, num_next_chunks=num_chunks + ), + ) + + request = discoveryengine.SearchRequest( + serving_config=serving_config, + query=query, + page_size=top_n, + content_search_spec=content_search_spec, + ) + + try: + response = client.search(request) + results = [] + for i, result in enumerate(response.results): + result_data = { + "rank": i + 1, + "document_metadata": {}, + "page_span": {}, + "chunks": [], + "augmented_content": "", + } + + if hasattr(result, "chunk") and result.chunk: + chunk = result.chunk + if hasattr(chunk, "document_metadata"): + result_data["document_metadata"] = { + "uri": getattr(chunk.document_metadata, "uri", None), + "title": getattr(chunk.document_metadata, "title", None), + } + if hasattr(chunk, "page_span"): + result_data["page_span"] = { + "start": getattr(chunk.page_span, "page_start", None), + "end": getattr(chunk.page_span, "page_end", None), + } + + all_chunk_content = [] + if hasattr(chunk.chunk_metadata, "previous_chunks"): + for prev_chunk in chunk.chunk_metadata.previous_chunks: + result_data["chunks"].append( + { + "type": "previous", + "id": prev_chunk.id, + "content": prev_chunk.content, + } + ) + all_chunk_content.append(prev_chunk.content) + + result_data["chunks"].append( + {"type": "relevant", "id": chunk.id, "content": chunk.content} + ) + all_chunk_content.append(chunk.content) + + if hasattr(chunk.chunk_metadata, "next_chunks"): + for next_chunk in chunk.chunk_metadata.next_chunks: + result_data["chunks"].append( + { + "type": "next", + "id": next_chunk.id, + "content": next_chunk.content, + } + ) + all_chunk_content.append(next_chunk.content) + + result_data["augmented_content"] = " ".join(all_chunk_content) + results.append(result_data) + return results + except Exception as e: + raise Exception(f"Search failed: {e}") + + +def list_chunks_for_document( + document_id: str, project_id: str, location: str, data_store_id: str +) -> list[dict[str, Any]]: + """List all chunks for a specific document in Vertex AI Search""" + client_options = ClientOptions(api_endpoint="discoveryengine.googleapis.com") + try: + # Using v1alpha for chunk support if available, otherwise fallback + from google.cloud import discoveryengine_v1alpha + + client = discoveryengine_v1alpha.ChunkServiceClient( + client_options=client_options + ) + + parent = client.document_path( + project=project_id, + location=location, + data_store=data_store_id, + branch="default_branch", + document=document_id, + ) + + chunks = [] + page_result = client.list_chunks(parent=parent) + for chunk in page_result: + chunk_data = { + "id": chunk.id, + "name": chunk.name, + "content": chunk.content, + "page_span": { + "start": getattr(chunk.page_span, "page_start", None) + if hasattr(chunk, "page_span") + else None, + "end": getattr(chunk.page_span, "page_end", None) + if hasattr(chunk, "page_span") + else None, + }, + } + chunks.append(chunk_data) + return chunks + except Exception as e: + print(f"Error listing chunks for document {document_id}: {e}") + # Fallback to search-based approach could be implemented here if needed + return [] + + +def merge_chunks_into_bigger_chunks( + chunks: list[dict[str, Any]], merge_count: int = 3 +) -> list[dict[str, Any]]: + """Merge consecutive chunks into bigger chunks""" + if not chunks: + return [] + + bigger_chunks = [] + for i in range(0, len(chunks), merge_count): + chunk_slice = chunks[i : i + merge_count] + merged_content = " ".join([chunk["content"] for chunk in chunk_slice]) + + bigger_chunk = { + "content": merged_content, + "chunk_ids": [chunk["id"] for chunk in chunk_slice], + "chunk_count": len(chunk_slice), + "start_index": i, + "end_index": min(i + merge_count - 1, len(chunks) - 1), + } + + if ( + "page_span" in chunk_slice[0] + and chunk_slice[0]["page_span"]["start"] is not None + ): + bigger_chunk["page_span"] = { + "start": chunk_slice[0]["page_span"]["start"], + "end": chunk_slice[-1]["page_span"]["end"], + } + bigger_chunks.append(bigger_chunk) + return bigger_chunks diff --git a/search/cloud-function/README.md b/search/cloud-function/README.md new file mode 100644 index 0000000..4833b1b --- /dev/null +++ b/search/cloud-function/README.md @@ -0,0 +1,128 @@ +# Vertex AI Search accessed via Google Cloud Functions + +This directory contains several versions of approximately the same +implementation. + +The functions can be deployed to +[Cloud functions](https://cloud.google.com/functions/) and can be modified to +supports many different triggers and use cases. Each can also +[be deployed locally](https://cloud.google.com/functions/docs/running/overview) +which allows easy experimentation and iteration. + +This example is powered by +[Vertex AI Search](https://cloud.google.com/generative-ai-app-builder/docs/enterprise-search-introduction) +which does many different things, including **Document & Intranet Search**, +**Recommendations** and **Grounding and RAG** out-of-the-box (For more +information, see the blog post +[Your RAG powered by Google Search](https://cloud.google.com/blog/products/ai-machine-learning/rags-powered-by-google-search-technology-part-1)). + +If you want even more control see +[Vertex AI Search Component APIs](https://cloud.google.com/generative-ai-app-builder/docs/builder-apis), +but first explore the out-of-the-box offering because it's easy to setup. + +This example is for the out-of-the-box Vertex AI Search supporting many +configurations and data types. + +## Pre-requisites + +Before you can use these functions to query Vertex AI Search, you need to create +and populate a search "data store"; read through instructions in +[get started with generic search](https://cloud.google.com/generative-ai-app-builder/docs/try-enterprise-search). +These functions could easily be adapted to other types of Vertex AI Search like +[generic recommendations](https://cloud.google.com/generative-ai-app-builder/docs/try-generic-recommendations), +[media search](https://cloud.google.com/generative-ai-app-builder/docs/try-media-search), +[media recommendations](https://cloud.google.com/generative-ai-app-builder/docs/try-media-recommendations), +[healthcare search](https://cloud.google.com/generative-ai-app-builder/docs/create-data-store-hc), +or even +[retail product discovery](https://cloud.google.com/solutions/retail-product-discovery#documentation). + +You'll need to collect the following details from your search app data store: + +```python +PROJECT_ID = "YOUR_PROJECT_ID" # alphanumeric +LOCATION = "global" # or an alternate location +DATA_STORE_ID = "YOUR_DATA_STORE_ID" # not the app id, alphanumeric +``` + +Additionally you'll need to keep track of some of the choices you make when you +configure Vertex AI Search. + +### Type of data source + + + +- UNSTRUCTURED +- STRUCTURED +- WEBSITE +- BLENDED + + + +```python +ENGINE_DATA_TYPE = UNSTRUCTURED +``` + +### Type of chunks to return + +- DOCUMENT_WITH_SNIPPETS +- DOCUMENT_WITH_EXTRACTIVE_SEGMENTS +- CHUNK +- NONE + +```python +ENGINE_CHUNK_TYPE = DOCUMENT_WITH_EXTRACTIVE_SEGMENTS +``` + +### Type of summarization + +- NONE results only +- VERTEX_AI_SEARCH LLM add on provided by +[Vertex AI Search](https://cloud.google.com/generative-ai-app-builder/docs/enterprise-search-introduction) + + +```python +SUMMARY_TYPE = VERTEX_AI_SEARCH +``` + +## Architecture + +1. Vertex AI Search is an API hosted on Google Cloud +2. You will call that API via a Google Cloud Function, which exposes its own API +3. Your users will the Google Cloud Function API, via your custom app or UI + +```mermaid +flowchart LR + A[fa:fa-search Vertex AI Search] --> B(Google Cloud Function) + B --> C[My App Server] + C -->|One| D[fa:fa-laptop web] + C -->|Two| E[fa:fa-mobile mobile] +``` + +## Use case: RAG / Grounding + +Any time you have more source data than can fit into a LLM context window, you +could benefit from RAG (Retrieval Augmented Generation). The more data you have, +the more important search is - to get the relevant chunks into the prompt of the +LLM. + +- **Retrieve** relevant search results, with text chunks (snippets or segments) +- **Augmented Generation** uses Gemini to generate an answer or summary grounded + on the relevant search results + +## Use case: Agent Tool (Knowledge Base) + +A natural extension of RAG / Grounding is agentic behavior. + +Whether creating a basic chatbot or a sophisticated tool using multi-agent +system, you're always going to need search based RAG. The better the search +quality the better the agent response based on your source data. + +For more on agents, check out +[Vertex AI Search Use Cases](https://cloud.google.com/products/agent-builder?hl=en#common-uses) +and +[https://github.com/GoogleCloudPlatform/generative-ai](https://github.com/GoogleCloudPlatform/generative-ai). diff --git a/search/cloud-function/python/.env.example b/search/cloud-function/python/.env.example new file mode 100644 index 0000000..949af7e --- /dev/null +++ b/search/cloud-function/python/.env.example @@ -0,0 +1,6 @@ +DATA_STORE_ID=your-data-store-id-here +ENGINE_CHUNK_TYPE=DOCUMENT_WITH_EXTRACTIVE_SEGMENTS +ENGINE_DATA_TYPE=UNSTRUCTURED +LOCATION=global +PROJECT_ID=your-project-id-here +SUMMARY_TYPE=VERTEX_AI_SEARCH diff --git a/search/cloud-function/python/README.md b/search/cloud-function/python/README.md new file mode 100644 index 0000000..45e8ab4 --- /dev/null +++ b/search/cloud-function/python/README.md @@ -0,0 +1,127 @@ +# Vertex AI Search accessed via Google Cloud Functions + +This example is based on the +[Python client for the Vertex AI Search API](https://cloud.google.com/generative-ai-app-builder/docs/libraries#client-libraries-usage-python), +which will get search results, snippets, metadata, and the LLM summary grounded +on search results. This is implemented in the `vertex_ai_search_client.py` file. + +That functionality is exposed on a REST API which is implemented in `main.py` +intended to be deployed to a Google Cloud Function using an HTTPS trigger on a +Python 3 runtime; +[read more here](https://cloud.google.com/functions/docs/samples/functions-http-content#functions_http_content-python). + +**[Read more about Vertex AI Search accessed via Google Cloud Functions](../)** + +## Environment Variables + +The following environment variables are required for both local development and +deployment: + +- `PROJECT_ID`: Your Google Cloud project ID +- `LOCATION`: The location of your Vertex AI Search data store +- `DATA_STORE_ID`: The ID of your Vertex AI Search data store +- `ENGINE_DATA_TYPE`: Type of data in the engine (0-3) +- `ENGINE_CHUNK_TYPE`: Type of chunking used (0-3) +- `SUMMARY_TYPE`: Type of summary used (0-3) + +## Local Development + +### Setup + +1. Ensure you have the Google Cloud SDK installed and configured. +2. Clone this repository and navigate to the project directory. +3. Set up your environment variables: + +```bash +gcloud auth login +bash setup_env.sh +``` + +Alternatively, you can manually create and edit a `.env` file with the required +variables. + +### Run locally + +Run this code locally via **Functions Framework** or **Functions Emulator**; +[read more about running cloud functions locally](https://cloud.google.com/functions/docs/running/overview). + +```bash +pip install -r requirements.txt +pip install functions-framework +functions-framework --target=vertex_ai_search +``` + +In a different terminal, execute a `POST` search query based on your data: + +```bash +export SEARCH_TERM="What is the ... for ...?" +curl -m 310 -X POST localhost:8080 \ +-H "Content-Type: application/json" \ +-d "{\"search_term\": \"${SEARCH_TERM}\"}" +``` + +### Run tests + +#### Unit tests + +These tests mock the API interactions and should run quickly: + +```bash +pip install pytest +pytest test_vertex_ai_search_client.py +``` + +#### Integration tests + +These tests actually call the Vertex AI Search API and depend on your data +stores being configured in Vertex AI Search: + +```bash +pip install pytest +pytest test_integration_vertex_ai_search_client.py +``` + +## Deployment + +To deploy this function to Google Cloud: + +1. Ensure you have set up the required environment variables (see Environment + Variables section). +2. Run the following command: + +```bash +gcloud functions deploy vertex_ai_search --runtime python39 --trigger-http --allow-unauthenticated +``` + +You will get back a URL for triggering the function. + +## Usage + +After deployment, you can use the function as follows: + +```bash +curl -X POST https://YOUR_FUNCTION_URL \ +-H "Content-Type: application/json" \ +-d '{"search_term": "your search query"}' +``` + +Replace `YOUR_FUNCTION_URL` with the URL of your deployed function, and fill in +the search query. + +If you run into problems, go to +[Google Cloud Functions](https://console.cloud.google.com/functions), find the +function you just deployed, and review the logs for informative errors. Perhaps +you need to setup +[Google Cloud IAM](https://cloud.google.com/functions/docs/reference/iam) roles +or permissions. + +## Customization + +This implementation provides a basic way to access and control your queries to +the Vertex AI Search API. It simplifies CORS and bearer token authentication, +and allows for some minor customization of inputs and outputs. + +If you require more extensive customization, consider using an orchestration +framework like [LangChain](https://www.langchain.com/) or +[LlamaIndex](https://www.llamaindex.ai/) which have Vertex AI Search +integrations. diff --git a/search/cloud-function/python/main.py b/search/cloud-function/python/main.py new file mode 100644 index 0000000..a0180d8 --- /dev/null +++ b/search/cloud-function/python/main.py @@ -0,0 +1,135 @@ +# Copyright 2024 Google LLC +# +# 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 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" +Google Cloud Function for Vertex AI Search + +This module provides an HTTP endpoint for performing searches using +the Vertex AI Search API. It uses the VertexAISearchClient to handle +the core search functionality. + +For deployment instructions, environment variable setup, and usage examples, +please refer to the README.md file. +""" + +import os +from typing import Any + +from flask import Flask, Request, jsonify, request +import functions_framework +from google.api_core.exceptions import GoogleAPICallError +from vertex_ai_search_client import VertexAISearchClient, VertexAISearchConfig + +# Load environment variables +project_id = os.getenv("PROJECT_ID", "your-project") +location = os.getenv("LOCATION", "global") +data_store_id = os.getenv("DATA_STORE_ID", "your-data-store") +engine_data_type = os.getenv("ENGINE_DATA_TYPE", "UNSTRUCTURED") +engine_chunk_type = os.getenv("ENGINE_CHUNK_TYPE", "CHUNK") +summary_type = os.getenv("SUMMARY_TYPE", "VERTEX_AI_SEARCH") + +# Create VertexAISearchConfig +config = VertexAISearchConfig( + project_id=project_id, + location=location, + data_store_id=data_store_id, + engine_data_type=engine_data_type, + engine_chunk_type=engine_chunk_type, + summary_type=summary_type, +) + +# Initialize VertexAISearchClient +vertex_ai_search_client = VertexAISearchClient(config) + + +@functions_framework.http +def vertex_ai_search(http_request: Request) -> tuple[Any, int, dict[str, str]]: + """ + Handle HTTP requests for Vertex AI Search. + + This function processes incoming HTTP requests, performs the search using + the VertexAISearchClient, and returns the results. It handles CORS, validates + the request, and manages potential errors. + + Args: + http_request (flask.Request): The request object. + + + Returns: + Tuple[Any, int, Dict[str, str]]: A tuple containing the response body, + status code, and headers. This output will be turned into a Response + object using `make_response` + . + """ + # Set CORS headers for the preflight request + if http_request.method == "OPTIONS": + # Allows GET requests from any origin with the Content-Type + # header and caches preflight response for an 3600s + headers = { + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Methods": "GET", + "Access-Control-Allow-Headers": "Content-Type", + "Access-Control-Max-Age": "3600", + } + return ("", 204, headers) + + # Set CORS headers for all responses + headers = {"Access-Control-Allow-Origin": "*"} + + def create_error_response( + message: str, status_code: int + ) -> tuple[Any, int, dict[str, str]]: + """Standardize the error responses with common headers.""" + return (jsonify({"error": message}), status_code, headers) + + # Handle the request and get the search_term + request_json = http_request.get_json(silent=True) + request_args = http_request.args + + if request_json and "search_term" in request_json: + search_term = request_json["search_term"] + elif request_args and "search_term" in request_args: + search_term = request_args["search_term"] + else: + return create_error_response("No search term provided", 400) + + # Handle the Vertex AI Search and return JSON + try: + results = vertex_ai_search_client.search(search_term) + return (jsonify(results), 200, headers) + except GoogleAPICallError as e: + return create_error_response( + f"Error calling Vertex AI Search API: {str(e)}", 500 + ) + except ValueError as e: + return create_error_response(f"Invalid input: {str(e)}", 400) + + +if __name__ == "__main__": + app = Flask(__name__) + + @app.route("/", methods=["POST"]) + def index() -> tuple[Any, int, dict[str, str]]: + """ + Flask route for handling POST requests when running locally. + + This function is used when the script is run directly (not as a Google Cloud Function). + It mimics the behavior of the vertex_ai_search function for local testing. + + Returns: + Tuple[Any, int, Dict[str, str]]: The vertex search result. + """ + + return vertex_ai_search(request) + + app.run("localhost", 8080, debug=True) diff --git a/search/cloud-function/python/requirements.txt b/search/cloud-function/python/requirements.txt new file mode 100644 index 0000000..1072b91 --- /dev/null +++ b/search/cloud-function/python/requirements.txt @@ -0,0 +1,7 @@ +Flask==3.1.1 +functions_framework==3.8.2 +google-cloud-discoveryengine>=0.11 +mypy==1.15.0 +protobuf==5.29.5 +pytest==8.3.5 +python-dotenv==1.1.0 \ No newline at end of file diff --git a/search/cloud-function/python/setup_env.sh b/search/cloud-function/python/setup_env.sh new file mode 100755 index 0000000..76c151d --- /dev/null +++ b/search/cloud-function/python/setup_env.sh @@ -0,0 +1,6 @@ +#!/bin/bash +cp .env.example .env +PROJECT_ID=$(gcloud config get-value project) +sed -i.old "s/PROJECT_ID=.*/PROJECT_ID=$PROJECT_ID/" .env && rm .env.old +echo "Project ID set to $PROJECT_ID in .env file" +echo "Please open .env and set the LOCATION, DATA_STORE_ID, and enum values" diff --git a/search/cloud-function/python/test_integration_vertex_search_client.py b/search/cloud-function/python/test_integration_vertex_search_client.py new file mode 100644 index 0000000..b6587d4 --- /dev/null +++ b/search/cloud-function/python/test_integration_vertex_search_client.py @@ -0,0 +1,121 @@ +# Copyright 2024 Google LLC +# +# 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 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" +Integration tests for the VertexAISearchClient. + +This module contains integration tests that interact with the actual +Vertex AI Search API. These tests require proper configuration of +environment variables and access to the Vertex AI Search service. +""" + +from collections.abc import Generator +import os + +import pytest +from vertex_ai_search_client import VertexAISearchClient, VertexAISearchConfig + +# Load environment variables +PROJECT_ID = os.getenv("PROJECT_ID", "your-project") +LOCATION = os.getenv("LOCATION", "global") +DATA_STORE_ID = os.getenv("DATA_STORE_ID", "your-data-store") +ENGINE_DATA_TYPE = os.getenv("ENGINE_DATA_TYPE", "UNSTRUCTURED") +ENGINE_CHUNK_TYPE = os.getenv("ENGINE_CHUNK_TYPE", "CHUNK") +SUMMARY_TYPE = os.getenv("SUMMARY_TYPE", "VERTEX_AI_SEARCH") + + +@pytest.fixture(scope="module") +def vertex_ai_search_client() -> Generator[VertexAISearchClient, None, None]: + """ + Fixture to create and yield a VertexAISearchClient instance for testing. + + This fixture creates a VertexAISearchClient instance using the + environment variables and yields it for use in tests. The client + is shared across all tests in the module for efficiency. + + Yields: + VertexAISearchClient: An instance of the VertexAISearchClient for testing. + """ + config = VertexAISearchConfig( + project_id=PROJECT_ID, + location=LOCATION, + data_store_id=DATA_STORE_ID, + engine_data_type="UNSTRUCTURED", + engine_chunk_type="DOCUMENT_WITH_EXTRACTIVE_SEGMENTS", + summary_type="VERTEX_AI_SEARCH", + ) + client = VertexAISearchClient(config) + yield client + + +def test_search_integration(client: VertexAISearchClient) -> None: + """ + Test the search functionality of VertexAISearchClient with the actual API. + + This test performs a search using the VertexAISearchClient and verifies + that the results have the expected structure and content types. + + Args: + vertex_ai_search_client (VertexAISearchClient): The client instance to test. + """ + # Perform a search + query = "test query" + results = client.search(query) + + # Check the structure of the results + assert "simplified_results" in results + assert isinstance(results["simplified_results"], list) + + if results["simplified_results"]: + first_result = results["simplified_results"][0] + assert "metadata" in first_result + assert "page_content" in first_result + + # Check for other expected fields + assert "total_size" in results + assert isinstance(results["total_size"], int) + + if "summary" in results: + assert "summary_text" in results["summary"] + + +def test_unstructured_summary() -> None: + """ + Test VertexAISearchClient with unstructured data and summary generation. + + This test creates a new VertexAISearchClient instance with specific + settings for unstructured data and summary generation, then performs + a search to verify the results. + """ + config = VertexAISearchConfig( + project_id=PROJECT_ID, + location=LOCATION, + data_store_id=DATA_STORE_ID, + engine_data_type="UNSTRUCTURED", + engine_chunk_type="DOCUMENT_WITH_EXTRACTIVE_SEGMENTS", + summary_type="VERTEX_AI_SEARCH", + ) + client = VertexAISearchClient(config) + results = client.search("What is the name of the company?") + # Check the structure of the results + assert "simplified_results" in results + assert isinstance(results["simplified_results"], list) + + if results["simplified_results"]: + first_result = results["simplified_results"][0] + assert "metadata" in first_result + assert "page_content" in first_result + + +if __name__ == "__main__": + pytest.main() diff --git a/search/cloud-function/python/test_vertex_ai_search_client.py b/search/cloud-function/python/test_vertex_ai_search_client.py new file mode 100644 index 0000000..a78e806 --- /dev/null +++ b/search/cloud-function/python/test_vertex_ai_search_client.py @@ -0,0 +1,338 @@ +# Copyright 2024 Google LLC +# +# 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 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# pylint: disable=redefined-outer-name,protected-access + +""" +Unit tests for the VertexAISearchClient class. + +This module contains unit tests for the VertexAISearchClient class, using +mocks to simulate the behavior of the Google Cloud Search API. These tests +ensure that the client correctly handles various scenarios and data structures. +""" + +import json +from unittest.mock import MagicMock, patch + +from google.cloud import discoveryengine_v1alpha as discoveryengine +from google.cloud.discoveryengine_v1alpha.services.search_service.pagers import ( + SearchPager, +) +from google.cloud.discoveryengine_v1alpha.types import Document, SearchResponse +import pytest +from vertex_ai_search_client import VertexAISearchClient, VertexAISearchConfig + + +# Test helper functions +def create_mock_search_pager_result() -> MagicMock: + """Create a mock SearchPager result for testing.""" + mock_pager = MagicMock(spec=SearchPager) + mock_pager.__iter__.return_value = [create_mock_search_pager_return_value()] + mock_pager.total_size = 1 + mock_pager.attribution_token = "test-token" + mock_pager.next_page_token = "next-page" + mock_pager.corrected_query = "corrected query" + mock_pager.summary = SearchResponse.Summary(summary_text="Test summary") + mock_pager.applied_controls = None + mock_pager.facets = [] + mock_pager.guided_search_result = [] + mock_pager.query_expansion_info = [] + return mock_pager + + +def create_mock_search_pager_return_value() -> SearchResponse.SearchResult: + """Create a mock SearchResponse.SearchResult for testing.""" + search_result = SearchResponse.SearchResult() + document = Document() + + derived_struct_data = { + "title": "Employee Benefits Summary", + "link": "gs://company-docs/Employee_Benefits_Summary.pdf", + "extractive_answers": [{"content": "Test content", "pageNumber": "1"}], + "snippets": [{"snippet_status": "SUCCESS", "snippet": "Test snippet"}], + } + + document.derived_struct_data = derived_struct_data + search_result.document = document + return search_result + + +# Fixtures +@pytest.fixture +def mock_search_service_client() -> MagicMock: + """Fixture to create a mock SearchServiceClient.""" + with patch( + "vertex_ai_search_client.discoveryengine.SearchServiceClient" + ) as mock_client: + mock_client.return_value.serving_config_path.return_value = ( + "projects/test-project/locations/us-central1/dataStores/test-data-store/" + "servingConfigs/default_config" + ) + yield mock_client + + +@pytest.fixture +def search_config() -> VertexAISearchConfig: + """Fixture to create a VertexAISearchConfig instance for testing.""" + return VertexAISearchConfig( + project_id="test-project", + location="us-central1", + data_store_id="test-data-store", + engine_data_type="UNSTRUCTURED", + engine_chunk_type="DOCUMENT_WITH_EXTRACTIVE_SEGMENTS", + summary_type="VERTEX_AI_SEARCH", + ) + + +@pytest.fixture +def search_client(search_config: VertexAISearchConfig) -> VertexAISearchClient: + """Fixture to create a VertexAISearchClient instance for testing.""" + with patch( + "vertex_ai_search_client.discoveryengine.SearchServiceClient" + ) as mock_client: + mock_client.return_value.serving_config_path.return_value = ( + "projects/test-project/locations/us-central1/dataStores/test-data-store/" + "servingConfigs/default_config" + ) + return VertexAISearchClient(search_config) + + +# Tests +def test_init(search_config: VertexAISearchConfig) -> None: + """Test the initialization of VertexAISearchClient.""" + with patch("vertex_ai_search_client.discoveryengine.SearchServiceClient"): + client = VertexAISearchClient(search_config) + + assert client.config.project_id == "test-project" + assert client.config.location == "us-central1" + assert client.config.data_store_id == "test-data-store" + assert client.config.engine_data_type == "UNSTRUCTURED" + assert client.config.engine_chunk_type == "DOCUMENT_WITH_EXTRACTIVE_SEGMENTS" + assert client.config.summary_type == "VERTEX_AI_SEARCH" + + +def test_get_serving_config(search_client: VertexAISearchClient) -> None: + """Test the get_serving_config method of VertexAISearchClient.""" + expected_serving_config = ( + "projects/test-project/locations/us-central1/dataStores/test-data-store/" + "servingConfigs/default_config" + ) + assert search_client.serving_config == expected_serving_config + + +def test_build_search_request(search_client: VertexAISearchClient) -> None: + """Test the build_search_request method of VertexAISearchClient.""" + query = "test query" + page_size = 5 + # Access to protected member is necessary for testing + request = search_client.build_search_request( + query, page_size + ) # pylint: disable=protected-access + + assert isinstance(request, discoveryengine.SearchRequest) + assert request.serving_config == search_client.serving_config + assert request.query == query + assert request.page_size == page_size + + assert ( + request.query_expansion_spec.condition + == discoveryengine.SearchRequest.QueryExpansionSpec.Condition.AUTO + ) + assert ( + request.spell_correction_spec.mode + == discoveryengine.SearchRequest.SpellCorrectionSpec.Mode.AUTO + ) + + assert request.content_search_spec.snippet_spec.return_snippet is True + assert request.content_search_spec.summary_spec.summary_result_count == 5 + assert request.content_search_spec.summary_spec.include_citations is True + assert request.content_search_spec.summary_spec.ignore_adversarial_query is True + assert ( + request.content_search_spec.summary_spec.ignore_non_summary_seeking_query + is True + ) + assert ( + request.content_search_spec.extractive_content_spec.max_extractive_answer_count + == 1 + ) + assert ( + request.content_search_spec.extractive_content_spec.return_extractive_segment_score + is True + ) + + +def test_map_search_pager_to_dict_basic(search_client: VertexAISearchClient) -> None: + """Test the map_search_pager_to_dict method with basic data.""" + mock_pager = create_mock_search_pager_result() + + # Access to protected member is necessary for testing + result = search_client.map_search_pager_to_dict( + mock_pager + ) # pylint: disable=protected-access + + assert "results" in result + assert len(result["results"]) == 1 + assert result["total_size"] == 1 + assert result["attribution_token"] == "test-token" + assert result["next_page_token"] == "next-page" + assert result["corrected_query"] == "corrected query" + assert result["summary"]["summary_text"] == "Test summary" + + +def test_map_search_pager_to_dict_document_content( + search_client: VertexAISearchClient, +) -> None: + """Test the map_search_pager_to_dict method with document content.""" + mock_pager = create_mock_search_pager_result() + + # Access to protected member is necessary for testing + result = search_client.map_search_pager_to_dict( + mock_pager + ) # pylint: disable=protected-access + + document = result["results"][0]["document"] + assert document["derived_struct_data"]["title"] == "Employee Benefits Summary" + assert ( + document["derived_struct_data"]["link"] + == "gs://company-docs/Employee_Benefits_Summary.pdf" + ) + assert len(document["derived_struct_data"]["extractive_answers"]) == 1 + assert ( + document["derived_struct_data"]["extractive_answers"][0]["content"] + == "Test content" + ) + assert len(document["derived_struct_data"]["snippets"]) == 1 + assert document["derived_struct_data"]["snippets"][0]["snippet"] == "Test snippet" + + +def test_parse_chunk_result(search_client: VertexAISearchClient) -> None: + """Test the _parse_chunk_result method of VertexAISearchClient.""" + chunk = { + "id": "chunk1", + "relevance_score": 0.95, + "content": "Test content", + "document_metadata": {"title": "Test Title", "uri": "https://example.com"}, + "page_span": {"page_start": 1, "page_end": 2}, + } + + # Access to protected member is necessary for testing + result = search_client._parse_chunk_result( + chunk + ) # pylint: disable=protected-access + + assert result["page_content"] == "Test content" + assert result["metadata"]["chunk_id"] == "chunk1" + assert result["metadata"]["score"] == 0.95 + assert result["metadata"]["title"] == "Test Title" + assert result["metadata"]["uri"] == "https://example.com" + assert result["metadata"]["page"] == 1 + assert result["metadata"]["page_span_end"] == 2 + + +def test_strip_content() -> None: + """Test the _strip_content static method of VertexAISearchClient.""" + input_text = "

    Test content with "quotes"

    " + expected_output = 'Test content with "quotes"' + # Access to protected member is necessary for testing + assert ( + VertexAISearchClient._strip_content(input_text) == expected_output + ) # pylint: disable=protected-access + + +def test_simplify_search_results_mixed_chunk_and_segments( + search_client: VertexAISearchClient, +) -> None: + """Test the simplify_search_results method with mixed chunk and segment data.""" + input_dict = { + "results": [ + {"document": {"id": "doc1", "derived_struct_data": {"title": "Test"}}}, + {"chunk": {"id": "chunk1", "content": "Test content"}}, + ] + } + + # Access to protected member is necessary for testing + result = search_client.simplify_search_results( + input_dict + ) # pylint: disable=protected-access + + assert "simplified_results" in result + assert len(result["simplified_results"]) == 2 + assert "metadata" in result["simplified_results"][0] + assert "page_content" in result["simplified_results"][1] + + +def test_parse_document_result(search_client: VertexAISearchClient) -> None: + """Test the _parse_document_result method of VertexAISearchClient.""" + document = { + "id": "doc1", + "derived_struct_data": { + "title": "Employee Benefits Summary", + "extractive_answers": [ + { + "content": "Test content", + "page_number": "3", + } + ], + "snippets": [ + { + "snippet_status": "SUCCESS", + "snippet": "Test snippet", + } + ], + "link": "gs://company-docs/Employee_Benefits_Summary.pdf", + }, + } + + # Access to protected member is necessary for testing + result = search_client._parse_document_result( + document + ) # pylint: disable=protected-access + + assert result["metadata"]["title"] == "Employee Benefits Summary" + assert ( + result["metadata"]["link"] == "gs://company-docs/Employee_Benefits_Summary.pdf" + ) + assert "page_content" in result + assert "Test content" in result["page_content"] + assert "On page 3" in result["page_content"] + + +@patch("vertex_ai_search_client.VertexAISearchClient.map_search_pager_to_dict") +@patch("vertex_ai_search_client.VertexAISearchClient.simplify_search_results") +def test_search( + mock_simplify: MagicMock, + mock_map_pager: MagicMock, + search_client: VertexAISearchClient, +) -> None: + """Test the search method of VertexAISearchClient.""" + mock_pager = create_mock_search_pager_result() + + search_client.client.search.return_value = mock_pager + + mock_map_pager.return_value = {"results": [{"document": {"id": "doc1"}}]} + mock_simplify.return_value = {"simplified_results": [{"id": "doc1"}]} + + results = search_client.search("test query") + + search_client.client.search.assert_called_once() + mock_map_pager.assert_called_once_with(mock_pager) + mock_simplify.assert_called_once_with({"results": [{"document": {"id": "doc1"}}]}) + assert results == {"simplified_results": [{"id": "doc1"}]} + + results_json = json.dumps(results) + assert results_json == '{"simplified_results": [{"id": "doc1"}]}' + + +if __name__ == "__main__": + pytest.main() diff --git a/search/cloud-function/python/vertex_ai_search_client.py b/search/cloud-function/python/vertex_ai_search_client.py new file mode 100644 index 0000000..720cef6 --- /dev/null +++ b/search/cloud-function/python/vertex_ai_search_client.py @@ -0,0 +1,420 @@ +# Copyright 2024 Google LLC +# +# 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 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" +VertexAISearchClient for interacting with Google Cloud Vertex AI Search. + +This module provides a client class for simplifying interactions with the +Vertex AI Search API. It handles configuration, query construction, and +result parsing. + +Example usage: + config = VertexAISearchConfig( + project_id="your-project", + location="global", + data_store_id="your-data-store", + engine_data_type="UNSTRUCTURED", + engine_chunk_type="CHUNK", + summary_type="VERTEX_AI_SEARCH", + ) + client = VertexAISearchClient(config) + results = client.search("your search query") + print(results) +""" +from dataclasses import dataclass +import html +import json +import re +from typing import Any, Literal + +from google.api_core.client_options import ClientOptions +from google.cloud import discoveryengine_v1alpha as discoveryengine +from google.cloud.discoveryengine_v1alpha.services.search_service.pagers import ( + SearchPager, +) +from google.cloud.discoveryengine_v1alpha.types import SearchResponse + +# Define types using string literals, similar to enums. +EngineDataTypeStr = Literal["UNSTRUCTURED", "STRUCTURED", "WEBSITE", "BLENDED"] +EngineChunkTypeStr = Literal[ + "DOCUMENT_WITH_SNIPPETS", "DOCUMENT_WITH_EXTRACTIVE_SEGMENTS", "CHUNK" +] +SummaryTypeStr = Literal[ + "NONE", "VERTEX_AI_SEARCH", "GENERATE_GROUNDED_ANSWERS", "GEMINI" +] + + +@dataclass +class VertexAISearchConfig: + """Config for the Vertex AI Search data store.""" + + project_id: str + location: str + data_store_id: str + engine_data_type: EngineDataTypeStr | str + engine_chunk_type: EngineChunkTypeStr | str + summary_type: SummaryTypeStr | str + + def __post_init__(self) -> None: + """Validate and convert string inputs to appropriate types.""" + self.engine_data_type = self._validate_enum( + self.engine_data_type, EngineDataTypeStr, "UNSTRUCTURED" + ) + self.engine_chunk_type = self._validate_enum( + self.engine_chunk_type, EngineChunkTypeStr, "CHUNK" + ) + self.summary_type = self._validate_enum( + self.summary_type, SummaryTypeStr, "VERTEX_AI_SEARCH" + ) + + @staticmethod + def _validate_enum(value: str, enum_type: Any, default: str) -> str: + """Validate and convert string to enum type.""" + if value in enum_type.__args__: + return value + print(f"Warning: Invalid value '{value}'. Using default: '{default}'") + return default + + def to_dict(self) -> dict[str, str]: + """Convert the config to a dictionary.""" + return { + "project_id": self.project_id, + "location": self.location, + "data_store_id": self.data_store_id, + "engine_data_type": self.engine_data_type, + "engine_chunk_type": self.engine_chunk_type, + "summary_type": self.summary_type, + } + + +class VertexAISearchClient: + """ + A client for interacting with Google Cloud Vertex AI Search. + + This class provides methods to configure the search engine, perform searches, + and parse the results. It supports different types of data stores and search + configurations. + """ + + def __init__(self, config: VertexAISearchConfig): + """ + Initialize the VertexAISearchClient. + + Args: + config (VertexAISearchConfig): The configuration for the Vertex AI Search client. + """ + self.config = config + self.client = self._create_client() + self.serving_config = self._get_serving_config() + + def _create_client(self) -> discoveryengine.SearchServiceClient: + """ + Create and configure the SearchServiceClient. + + Returns: + discoveryengine.SearchServiceClient: The configured client. + """ + client_options = None + if self.config.location != "global": + api_endpoint = f"{self.config.location}-discoveryengine.googleapis.com" + client_options = ClientOptions(api_endpoint=api_endpoint) + return discoveryengine.SearchServiceClient(client_options=client_options) + + def _get_serving_config(self) -> str: + """ + Get the serving configuration path for the Vertex AI Search data store. + + Returns: + str: The serving configuration path. + """ + return self.client.serving_config_path( + project=self.config.project_id, + location=self.config.location, + data_store=self.config.data_store_id, + serving_config="default_config", + ) + + def search(self, query: str, page_size: int = 10) -> dict[str, Any]: + """ + Perform a search query using Vertex AI Search. + + Args: + query (str): The search query. + page_size (int): Number of results to return per page. + + Returns: + dict: Parsed and simplified search results. + """ + request = self.build_search_request(query, page_size) + print(f" {request} ") + search_pager = self.client.search(request) + response = self.map_search_pager_to_dict(search_pager) + print(f" {response} ") + return self.simplify_search_results(response) + + def build_search_request( + self, query: str, page_size: int + ) -> discoveryengine.SearchRequest: + """ + Build a SearchRequest object based on the client configuration and query. + + Args: + query (str): The search query. + page_size (int): Number of results to return per page. + + Returns: + discoveryengine.SearchRequest: The configured search request object. + """ + snippet_spec = None + extractive_content_spec = None + if self.config.engine_chunk_type == "DOCUMENT_WITH_SNIPPETS": + snippet_spec = discoveryengine.SearchRequest.ContentSearchSpec.SnippetSpec( + return_snippet=True, + ) + if self.config.engine_chunk_type == "DOCUMENT_WITH_EXTRACTIVE_SEGMENTS": + snippet_spec = discoveryengine.SearchRequest.ContentSearchSpec.SnippetSpec( + return_snippet=True, + ) + extractive_content_spec = ( + discoveryengine.SearchRequest.ContentSearchSpec.ExtractiveContentSpec( + max_extractive_answer_count=1, + return_extractive_segment_score=True, + ) + ) + + summary_spec = None + if self.config.summary_type == "VERTEX_AI_SEARCH": + summary_spec = discoveryengine.SearchRequest.ContentSearchSpec.SummarySpec( + summary_result_count=5, + include_citations=True, + ignore_adversarial_query=True, + ignore_non_summary_seeking_query=True, + ) + + return discoveryengine.SearchRequest( + serving_config=self.serving_config, + query=query, + page_size=page_size, + content_search_spec=discoveryengine.SearchRequest.ContentSearchSpec( + snippet_spec=snippet_spec, + extractive_content_spec=extractive_content_spec, + summary_spec=summary_spec, + ), + query_expansion_spec=discoveryengine.SearchRequest.QueryExpansionSpec( + condition=discoveryengine.SearchRequest.QueryExpansionSpec.Condition.AUTO, + ), + spell_correction_spec=discoveryengine.SearchRequest.SpellCorrectionSpec( + mode=discoveryengine.SearchRequest.SpellCorrectionSpec.Mode.AUTO + ), + ) + + def map_search_pager_to_dict(self, pager: SearchPager) -> dict[str, Any]: + """ + Maps a SearchPager to a dictionary structure, iterativly requesting results. + + https://cloud.google.com/python/docs/reference/discoveryengine/latest/google.cloud.discoveryengine_v1alpha.services.search_service.pagers.SearchPager + + Args: + pager (SearchPager): The pager returned by the search method. + + Returns: + Dict[str, Any]: A dictionary containing the search results and metadata. + """ + output: dict[str, Any] = { + "results": [ + SearchResponse.SearchResult.to_dict(result) for result in pager + ], + "total_size": pager.total_size, + "attribution_token": pager.attribution_token, + "next_page_token": pager.next_page_token, + "corrected_query": pager.corrected_query, + "facets": [], + "applied_controls": [], + } + + if pager.summary: + output["summary"] = SearchResponse.Summary.to_dict(pager.summary) + + if pager.facets: + output["facets"] = [ + SearchResponse.Facet.to_dict(facet) for facet in pager.facets + ] + + if pager.guided_search_result: + output["guided_search_result"] = SearchResponse.GuidedSearchResult.to_dict( + pager.guided_search_result + ) + + if pager.query_expansion_info: + output["query_expansion_info"] = SearchResponse.QueryExpansionInfo.to_dict( + pager.query_expansion_info + ) + + if pager.applied_controls: + output["applied_controls"] = [ + control.strip() for control in pager.applied_controls + ] + + return output + + def simplify_search_results(self, response: dict[str, Any]) -> dict[str, Any]: + """ + Simplify the search results by parsing documents and chunks. + + Args: + response (Dict[str, Any]): The raw search response. + + Returns: + Dict[str, Any]: The simplified search results. + """ + if "results" not in response: + return response + simplified_results = [] + for result in response["results"]: + if "document" in result: + simplified_results.append( + self._parse_document_result(result["document"]) + ) + elif "chunk" in result: + simplified_results.append(self._parse_chunk_result(result["chunk"])) + response["simplified_results"] = simplified_results + return response + + def _parse_document_result(self, document: dict[str, Any]) -> dict[str, Any]: + """ + Parse a single document result from the search response. + + This supports both structured and unstructured data, and also supports + extractive segments and answers and snippets. + + Args: + document (Dict[str, Any]): The document data from the search result. + + Returns: + Dict[str, Any]: The parsed document page_content and metadata. + """ + metadata = { + **document.get("derived_struct_data", {}), + **document.get("struct_data", {}), + } + + json_data = document.get("json_data", {}) + if isinstance(json_data, str): + try: + json_data = json.loads(json_data) + except json.JSONDecodeError: + print(f"Warning: Failed to parse json_data: {json_data}") + json_data = {} + + metadata.update(json_data) + result: dict[str, Any] = {"metadata": metadata} + + if self.config.engine_data_type == "STRUCTURED": + structured_data = ( + json_data if json_data else document.get("struct_data", {}) + ) + result["page_content"] = json.dumps(structured_data, indent=2) + for key in structured_data.keys(): + result["metadata"].pop(key, None) + elif "extractive_answers" in metadata: + result["page_content"] = self._parse_segments( + metadata.get("extractive_answers", []) + ) + elif "snippets" in metadata: + result["page_content"] = self._parse_snippets(metadata.get("snippets", [])) + else: + result["page_content"] = metadata.get("content", "") + + return result + + def _parse_segments(self, segments: list[dict[str, Any]]) -> str: + """ + Parse extractive segments from a single document of search results. + + Args: + segments (List[Dict[str, Any]]): The list of extractive segments. + + Returns: + str: A formatted string containing page number, score and the text of each segment. + """ + parsed_segments = [ + { + "content": self._strip_content(segment.get("content", "")), + "page_number": segment.get("page_number") or segment.get("pageNumber"), + "score": segment.get("score"), + } + for segment in segments + ] + return "\n\n".join( + f"On page {segment['page_number']} with a relevance score of {segment['score']}:\n" + f"```\n{segment['content']}\n```" + for segment in parsed_segments + ) + + def _parse_snippets(self, snippets: list[dict[str, Any]]) -> str: + """ + Parse snippets from a single document of search results. + + Args: + snippets (List[Dict[str, Any]]): The list of snippets. + + Returns: + str: A formatted string containing the successfully parsed snippets. + """ + return "\n\n".join( + self._strip_content(snippet.get("snippet", "")) + for snippet in snippets + if snippet.get("snippetStatus") == "SUCCESS" + ) + + def _parse_chunk_result(self, chunk: dict[str, Any]) -> dict[str, Any]: + """ + Parse a single chunk result from the search response. + + Args: + chunk (Dict[str, Any]): The chunk data from the search result. + + Returns: + Dict[str, Any]: The parsed chunk page_content and metadata. + """ + metadata = { + "chunk_id": chunk.get("id"), + "score": chunk.get("relevance_score"), + } + + page_span = chunk.get("page_span", {}) + if page_span: + metadata["page"] = page_span.get("page_start") + metadata["page_span_end"] = page_span.get("page_end") + + metadata.update(chunk.get("document_metadata", {})) + metadata.update(chunk.get("derived_struct_data", {})) + + return { + "metadata": metadata, + "page_content": self._strip_content(chunk.get("content", "")), + } + + @staticmethod + def _strip_content(text: str) -> str: + """ + Strip HTML tags and unescape HTML entities from the given text. + + Args: + text (str): The input text to clean. + + Returns: + str: The cleaned text. + """ + text = re.sub("<.*?>", "", text) + return html.unescape(text).strip() diff --git a/search/create_datastore_and_search.ipynb b/search/create_datastore_and_search.ipynb new file mode 100644 index 0000000..3bb572b --- /dev/null +++ b/search/create_datastore_and_search.ipynb @@ -0,0 +1,533 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "id": "829cc6c6-f8c3-4fc6-942b-f698be5fa1a2" + }, + "source": [ + "# Create a Vertex AI Datastore and Search Engine\n", + "\n", + "\n", + "\n", + " \n", + " \n", + " \n", + "
    \n", + " \n", + " \"Colab Run in Colab\n", + " \n", + " \n", + " \n", + " \"GitHub\n", + " View on GitHub\n", + " \n", + " \n", + " \n", + " \"Vertex\n", + " Open in Vertex AI Workbench\n", + " \n", + "
    \n", + "\n", + "
    \n", + "\n", + "Share to:\n", + "\n", + "\n", + " \"LinkedIn\n", + "\n", + "\n", + "\n", + " \"Bluesky\n", + "\n", + "\n", + "\n", + " \"X\n", + "\n", + "\n", + "\n", + " \"Reddit\n", + "\n", + "\n", + "\n", + " \"Facebook\n", + " " + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "0460bdbe-05db-42f8-822c-82195de8329a" + }, + "source": [ + "---\n", + "\n", + "* Author(s): [Kara Greenfield](https://github.com/kgreenfield2)\n", + "* Created: 22 Nov 2023\n", + "* Updated: 31 Oct 2024\n", + "\n", + "---\n", + "\n", + "## Objective\n", + "\n", + "This notebook shows how to create and populate a Vertex AI Search Datastore, how to create a search app connected to that datastore, and how to submit queries through the search engine.\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "0b4b1050-7113-487e-aeaf-55690c831a1d" + }, + "source": [ + "Services used in the notebook:\n", + "\n", + "- ✅ Vertex AI Search for document search and retrieval" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "43828625-a130-449c-ba5f-6a948220f559" + }, + "source": [ + "## Install pre-requisites\n", + "\n", + "If running in Colab install the pre-requisites into the runtime. Otherwise it is assumed that the notebook is running in Vertex AI Workbench." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": { + "id": "8ea5db8a-dccc-4442-b5d7-7088d5ffb5ac" + }, + "outputs": [], + "source": [ + "%pip install --upgrade --user -q google-cloud-discoveryengine" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "10f9e321" + }, + "source": [ + "### Restart current runtime\n", + "\n", + "To use the newly installed packages in this Jupyter runtime, you must restart the runtime. You can do this by running the cell below, which will restart the current kernel." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "da755736" + }, + "outputs": [], + "source": [ + "# Restart kernel after installs so that your environment can access the new packages\n", + "\n", + "import IPython\n", + "\n", + "app = IPython.Application.instance()\n", + "app.kernel.do_shutdown(True)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "9c31dbe0" + }, + "source": [ + "
    \n", + "⚠ The kernel is going to restart. Please wait until it is finished before continuing to the next step. ⚠\n", + "
    \n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "5f6a5de5-156e-4f14-99e6-3ab33e076c81" + }, + "source": [ + "## Authenticate\n", + "\n", + "If running in Colab authenticate with `google.colab.google.auth` otherwise assume that running on Vertex AI Workbench." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": { + "id": "df3b7309-c51c-44a0-a466-b0b2733e0c28" + }, + "outputs": [], + "source": [ + "import sys\n", + "\n", + "if \"google.colab\" in sys.modules:\n", + " from google.colab import auth\n", + "\n", + " auth.authenticate_user()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "ae7a4925-145a-40f3-9fa1-3b69a42d488d" + }, + "source": [ + "## Configure notebook environment" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": { + "id": "389b9e51-1ce4-4bdf-80b5-fcdc1882f853" + }, + "outputs": [], + "source": [ + "from google.api_core.client_options import ClientOptions\n", + "from google.cloud import discoveryengine\n", + "\n", + "PROJECT_ID = \"YOUR_PROJECT_ID\" # @param {type:\"string\"}\n", + "LOCATION = \"global\"" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "HjoerqoksBdx" + }, + "source": [ + "Set [Application Default Credentials](https://cloud.google.com/docs/authentication/application-default-credentials)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "s-jH2yG1rtxn" + }, + "outputs": [], + "source": [ + "!gcloud auth application-default login --project {PROJECT_ID}" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "07f1aecd-4633-4451-b5a7-1f26e4cb2631" + }, + "source": [ + "## Create and Populate a Datastore" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": { + "id": "9a1d0021-e299-49d4-a657-2e101ae49eb6" + }, + "outputs": [], + "source": [ + "def create_data_store(\n", + " project_id: str, location: str, data_store_name: str, data_store_id: str\n", + "):\n", + " # Create a client\n", + " client_options = (\n", + " ClientOptions(api_endpoint=f\"{location}-discoveryengine.googleapis.com\")\n", + " if location != \"global\"\n", + " else None\n", + " )\n", + " client = discoveryengine.DataStoreServiceClient(client_options=client_options)\n", + "\n", + " # Initialize request argument(s)\n", + " data_store = discoveryengine.DataStore(\n", + " display_name=data_store_name,\n", + " industry_vertical=discoveryengine.IndustryVertical.GENERIC,\n", + " content_config=discoveryengine.DataStore.ContentConfig.CONTENT_REQUIRED,\n", + " )\n", + "\n", + " operation = client.create_data_store(\n", + " request=discoveryengine.CreateDataStoreRequest(\n", + " parent=client.collection_path(project_id, location, \"default_collection\"),\n", + " data_store=data_store,\n", + " data_store_id=data_store_id,\n", + " )\n", + " )\n", + "\n", + " # Make the request\n", + " # The try block is necessary to prevent execution from halting due to an error being thrown when the datastore takes a while to instantiate\n", + " try:\n", + " response = operation.result(timeout=90)\n", + " except:\n", + " print(\"long-running operation error.\")" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": { + "id": "8a4db726-8da1-4c76-8934-944aaf5f9b53" + }, + "outputs": [], + "source": [ + "# The datastore name can only contain lowercase letters, numbers, and hyphens\n", + "DATASTORE_NAME = \"alphabet-contracts\"\n", + "DATASTORE_ID = f\"{DATASTORE_NAME}-id\"\n", + "\n", + "create_data_store(PROJECT_ID, LOCATION, DATASTORE_NAME, DATASTORE_ID)" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": { + "id": "03121270-5d2f-403b-81ea-c1d241357bd1" + }, + "outputs": [], + "source": [ + "def import_documents(\n", + " project_id: str,\n", + " location: str,\n", + " data_store_id: str,\n", + " gcs_uri: str,\n", + "):\n", + " # Create a client\n", + " client_options = (\n", + " ClientOptions(api_endpoint=f\"{location}-discoveryengine.googleapis.com\")\n", + " if location != \"global\"\n", + " else None\n", + " )\n", + " client = discoveryengine.DocumentServiceClient(client_options=client_options)\n", + "\n", + " # The full resource name of the search engine branch.\n", + " # e.g. projects/{project}/locations/{location}/dataStores/{data_store_id}/branches/{branch}\n", + " parent = client.branch_path(\n", + " project=project_id,\n", + " location=location,\n", + " data_store=data_store_id,\n", + " branch=\"default_branch\",\n", + " )\n", + "\n", + " source_documents = [f\"{gcs_uri}/*\"]\n", + "\n", + " request = discoveryengine.ImportDocumentsRequest(\n", + " parent=parent,\n", + " gcs_source=discoveryengine.GcsSource(\n", + " input_uris=source_documents, data_schema=\"content\"\n", + " ),\n", + " # Options: `FULL`, `INCREMENTAL`\n", + " reconciliation_mode=discoveryengine.ImportDocumentsRequest.ReconciliationMode.INCREMENTAL,\n", + " )\n", + "\n", + " # Make the request\n", + " operation = client.import_documents(request=request)\n", + "\n", + " response = operation.result()\n", + "\n", + " # Once the operation is complete,\n", + " # get information from operation metadata\n", + " metadata = discoveryengine.ImportDocumentsMetadata(operation.metadata)\n", + "\n", + " # Handle the response\n", + " return operation.operation.name" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": { + "id": "1ddfe66a-acb4-4fdb-b9ed-76a332bb0f0c" + }, + "outputs": [], + "source": [ + "source_documents_gs_uri = (\n", + " \"gs://cloud-samples-data/gen-app-builder/search/alphabet-investor-pdfs\"\n", + ")\n", + "\n", + "import_documents(PROJECT_ID, LOCATION, DATASTORE_ID, source_documents_gs_uri)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "7a957202-b67e-47ca-84c3-b8a62cfbe405" + }, + "source": [ + "## Create a Search Engine\n", + "\n", + "This is used to set the `search_tier` to enterprise and to enable advanced LLM features.\n", + "\n", + "Enterprise tier is required to get extractive answers from a search query and advanced LLM features are required to summarize search results." + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": { + "id": "0e39e9bb-f381-44f6-a45e-3322669a171f" + }, + "outputs": [], + "source": [ + "def create_engine(\n", + " project_id: str, location: str, engine_name: str, engine_id: str, data_store_id: str\n", + "):\n", + " # Create a client\n", + " client_options = (\n", + " ClientOptions(api_endpoint=f\"{location}-discoveryengine.googleapis.com\")\n", + " if location != \"global\"\n", + " else None\n", + " )\n", + " client = discoveryengine.EngineServiceClient(client_options=client_options)\n", + "\n", + " # Initialize request argument(s)\n", + " engine = discoveryengine.Engine(\n", + " display_name=engine_name,\n", + " solution_type=discoveryengine.SolutionType.SOLUTION_TYPE_SEARCH,\n", + " industry_vertical=discoveryengine.IndustryVertical.GENERIC,\n", + " data_store_ids=[data_store_id],\n", + " search_engine_config=discoveryengine.Engine.SearchEngineConfig(\n", + " search_tier=discoveryengine.SearchTier.SEARCH_TIER_ENTERPRISE,\n", + " search_add_ons=[discoveryengine.SearchAddOn.SEARCH_ADD_ON_LLM],\n", + " ),\n", + " )\n", + "\n", + " request = discoveryengine.CreateEngineRequest(\n", + " parent=client.collection_path(project_id, location, \"default_collection\"),\n", + " engine=engine,\n", + " engine_id=engine.display_name,\n", + " )\n", + "\n", + " # Make the request\n", + " operation = client.create_engine(request=request)\n", + " response = operation.result(timeout=90)" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": { + "id": "4a853982-8c2e-402a-a808-5364bf932619" + }, + "outputs": [], + "source": [ + "ENGINE_NAME = DATASTORE_NAME\n", + "ENGINE_ID = DATASTORE_ID\n", + "create_engine(PROJECT_ID, LOCATION, ENGINE_NAME, ENGINE_ID, DATASTORE_ID)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "e9f4d978-9164-4de3-b01a-179051706313" + }, + "source": [ + "## Query your Search Engine\n", + "\n", + "Note: The Engine will take some time to be ready to query.\n", + "\n", + "If you recently created an engine and you receive an error similar to:\n", + "\n", + "`404 Engine {ENGINE_NAME} is not found`\n", + "\n", + "Then wait a few minutes and try your query again." + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": { + "id": "1c4dfb62-7846-43d0-9cba-fd8886ce5546" + }, + "outputs": [], + "source": [ + "def search_sample(\n", + " project_id: str,\n", + " location: str,\n", + " engine_id: str,\n", + " search_query: str,\n", + ") -> list[discoveryengine.SearchResponse]:\n", + " # For more information, refer to:\n", + " # https://cloud.google.com/generative-ai-app-builder/docs/locations#specify_a_multi-region_for_your_data_store\n", + " client_options = (\n", + " ClientOptions(api_endpoint=f\"{location}-discoveryengine.googleapis.com\")\n", + " if LOCATION != \"global\"\n", + " else None\n", + " )\n", + "\n", + " # Create a client\n", + " client = discoveryengine.SearchServiceClient(client_options=client_options)\n", + "\n", + " # The full resource name of the search engine serving config\n", + " # e.g. projects/{project_id}/locations/{location}/dataStores/{data_store_id}/servingConfigs/{serving_config_id}\n", + " serving_config = f\"projects/{project_id}/locations/{location}/collections/default_collection/engines/{engine_id}/servingConfigs/default_search\"\n", + "\n", + " # Optional: Configuration options for search\n", + " # Refer to the `ContentSearchSpec` reference for all supported fields:\n", + " # https://cloud.google.com/python/docs/reference/discoveryengine/latest/google.cloud.discoveryengine_v1.types.SearchRequest.ContentSearchSpec\n", + " content_search_spec = discoveryengine.SearchRequest.ContentSearchSpec(\n", + " # For information about snippets, refer to:\n", + " # https://cloud.google.com/generative-ai-app-builder/docs/snippets\n", + " snippet_spec=discoveryengine.SearchRequest.ContentSearchSpec.SnippetSpec(\n", + " return_snippet=True\n", + " ),\n", + " # For information about search summaries, refer to:\n", + " # https://cloud.google.com/generative-ai-app-builder/docs/get-search-summaries\n", + " summary_spec=discoveryengine.SearchRequest.ContentSearchSpec.SummarySpec(\n", + " summary_result_count=5,\n", + " include_citations=True,\n", + " ignore_adversarial_query=True,\n", + " ignore_non_summary_seeking_query=True,\n", + " ),\n", + " )\n", + "\n", + " # Refer to the `SearchRequest` reference for all supported fields:\n", + " # https://cloud.google.com/python/docs/reference/discoveryengine/latest/google.cloud.discoveryengine_v1.types.SearchRequest\n", + " request = discoveryengine.SearchRequest(\n", + " serving_config=serving_config,\n", + " query=search_query,\n", + " page_size=10,\n", + " content_search_spec=content_search_spec,\n", + " query_expansion_spec=discoveryengine.SearchRequest.QueryExpansionSpec(\n", + " condition=discoveryengine.SearchRequest.QueryExpansionSpec.Condition.AUTO,\n", + " ),\n", + " spell_correction_spec=discoveryengine.SearchRequest.SpellCorrectionSpec(\n", + " mode=discoveryengine.SearchRequest.SpellCorrectionSpec.Mode.AUTO\n", + " ),\n", + " )\n", + "\n", + " response = client.search(request)\n", + " return response" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": { + "id": "ad41e18b-38d2-4f4c-98ae-df14eda900ae" + }, + "outputs": [], + "source": [ + "query = \"Who is the CEO of Google?\"\n", + "\n", + "response = search_sample(PROJECT_ID, LOCATION, ENGINE_ID, query)\n", + "print(response.summary.summary_text)" + ] + } + ], + "metadata": { + "colab": { + "name": "create_datastore_and_search.ipynb", + "toc_visible": true + }, + "kernelspec": { + "display_name": "Python 3", + "name": "python3" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} diff --git a/search/custom-embeddings/custom_embeddings.ipynb b/search/custom-embeddings/custom_embeddings.ipynb new file mode 100644 index 0000000..69fa068 --- /dev/null +++ b/search/custom-embeddings/custom_embeddings.ipynb @@ -0,0 +1,871 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "z3Gi-zdCeEbE" + }, + "outputs": [], + "source": [ + "# Copyright 2024 Google LLC\n", + "#\n", + "# Licensed under the Apache License, Version 2.0 (the \"License\");\n", + "# you may not use this file except in compliance with the License.\n", + "# You may obtain a copy of the License at\n", + "#\n", + "# https://www.apache.org/licenses/LICENSE-2.0\n", + "#\n", + "# Unless required by applicable law or agreed to in writing, software\n", + "# distributed under the License is distributed on an \"AS IS\" BASIS,\n", + "# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n", + "# See the License for the specific language governing permissions and\n", + "# limitations under the License." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "WcfbGhs1eJF6" + }, + "source": [ + "# Custom Embeddings with Vertex AI Search\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
    \n", + " \n", + " \"Google
    Open in Colab\n", + "
    \n", + "
    \n", + " \n", + " \"Google
    Open in Colab Enterprise\n", + "
    \n", + "
    \n", + " \n", + " \"Vertex
    Open in Vertex AI Workbench\n", + "
    \n", + "
    \n", + " \n", + " \"BigQuery
    Open in BigQuery Studio\n", + "
    \n", + "
    \n", + " \n", + " \"GitHub
    View on GitHub\n", + "
    \n", + "
    \n", + "\n", + "
    \n", + "\n", + "Share to:\n", + "\n", + "\n", + " \"LinkedIn\n", + "\n", + "\n", + "\n", + " \"Bluesky\n", + "\n", + "\n", + "\n", + " \"X\n", + "\n", + "\n", + "\n", + " \"Reddit\n", + "\n", + "\n", + "\n", + " \"Facebook\n", + " " + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "ghQ2aBsbnMyn" + }, + "source": [ + "---\n", + "\n", + "* Author: Holt Skinner\n", + "\n", + "---\n", + "\n", + "This notebook demonstrates how to:\n", + "\n", + " - Get text embeddings using [`text-embedding-005` in Vertex AI](https://cloud.google.com/vertex-ai/docs/generative-ai/embeddings/get-text-embeddings)\n", + " - Convert embeddings into the [format expected by Vertex AI Search](https://cloud.google.com/generative-ai-app-builder/docs/prepare-data#unstructured)\n", + " - [Create a search app with custom embeddings](https://cloud.google.com/generative-ai-app-builder/docs/bring-embeddings)\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "DsW5tPDRkT4m" + }, + "source": [ + "## Getting started" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "jx1FQVAokWVb" + }, + "source": [ + "### Install libraries" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "nJFw23w1kYVj" + }, + "outputs": [], + "source": [ + "%pip install -q --upgrade --user google-cloud-aiplatform google-cloud-discoveryengine google-cloud-storage google-cloud-bigquery[pandas] google-cloud-bigquery-storage pandas ipywidgets" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "a778a925e288" + }, + "outputs": [], + "source": [ + "%load_ext google.cloud.bigquery" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "42f78e5e31a4" + }, + "source": [ + "---\n", + "#### ⚠ Do not forget to click the \"RESTART RUNTIME\" button above.\n", + "---" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "-jwsaMQYkZm8" + }, + "source": [ + "### Authenticate your notebook environment (Colab only)\n", + "\n", + "If you are running this notebook on Google Colab, you will need to authenticate your environment. To do this, run the new cell below. This step is not required if you are using [Vertex AI Workbench](https://cloud.google.com/vertex-ai-workbench)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "ikOmH4doxOFs" + }, + "outputs": [], + "source": [ + "import sys\n", + "\n", + "if \"google.colab\" in sys.modules:\n", + " # Authenticate user to Google Cloud\n", + " from google.colab import auth\n", + "\n", + " auth.authenticate_user()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "-h0ba4rmkpKW" + }, + "source": [ + "### Import libraries" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "YLUml_s7iqBc" + }, + "outputs": [], + "source": [ + "import subprocess\n", + "import time\n", + "\n", + "from google.api_core.client_options import ClientOptions\n", + "from google.api_core.exceptions import GoogleAPICallError\n", + "from google.cloud import bigquery\n", + "from google.cloud import discoveryengine_v1alpha as discoveryengine\n", + "from google.cloud import storage\n", + "import requests\n", + "from tqdm import tqdm # to show a progress bar\n", + "import vertexai\n", + "from vertexai.language_models import TextEmbeddingInput, TextEmbeddingModel\n", + "\n", + "tqdm.pandas()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "7933d0d989be" + }, + "source": [ + "## Configure notebook environment\n", + "\n", + "### Set the following constants to reflect your environment" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "277ff46137d2" + }, + "outputs": [], + "source": [ + "# Define project information for Vertex AI\n", + "PROJECT_ID = \"[your-project-id]\" # @param {type:\"string\"}\n", + "LOCATION = \"us-central1\" # @param {type:\"string\"}\n", + "\n", + "# Initialize Vertex AI SDK\n", + "vertexai.init(project=PROJECT_ID, location=LOCATION)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "cd2d5363e770" + }, + "source": [ + "## Creating embeddings with Vertex AI\n", + "\n", + "### Data Preparation\n", + "\n", + "We will be using [the Stack Overflow public dataset](https://console.cloud.google.com/marketplace/product/stack-exchange/stack-overflow) hosted on BigQuery table `bigquery-public-data.stackoverflow.posts_questions`.\n", + "\n", + "This is a very big dataset with 23 million rows that doesn't fit into memory. We are going to limit it to 500 rows for this tutorial.\n", + "\n", + "- Fetch the data from BigQuery\n", + "- Concat the Title and Body, and create embeddings from the text." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "caeb5dc9d824" + }, + "outputs": [], + "source": [ + "bq_client = bigquery.Client(project=PROJECT_ID)\n", + "query = f\"\"\"\n", + "SELECT\n", + " DISTINCT \n", + " q.id,\n", + " q.title,\n", + " q.body,\n", + " q.answer_count,\n", + " q.comment_count,\n", + " q.creation_date,\n", + " q.last_activity_date,\n", + " q.score,\n", + " q.tags,\n", + " q.view_count\n", + "FROM\n", + " `bigquery-public-data.stackoverflow.posts_questions` AS q\n", + "WHERE\n", + " q.score > 0\n", + "ORDER BY\n", + " q.view_count DESC\n", + "LIMIT\n", + " 500;\n", + "\"\"\"\n", + "\n", + "# Load the BQ Table into a Pandas DataFrame\n", + "df = bq_client.query(query).result().to_dataframe()\n", + "\n", + "# Convert ID to String\n", + "df[\"id\"] = df[\"id\"].apply(str)\n", + "\n", + "# examine the data\n", + "df.head()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "1e8c38f4cb9a" + }, + "source": [ + "### Call the API to generate embeddings\n", + "\n", + "With the Stack Overflow dataset, we will use the `title` and `body` columns (the question title and description) and generate embedding for it with Embeddings for Text API. The API is available under the [`vertexai`](https://cloud.google.com/python/docs/reference/aiplatform/latest/vertexai) package of the SDK.\n", + "\n", + "From the package, import [`TextEmbeddingModel`](https://cloud.google.com/python/docs/reference/aiplatform/latest/vertexai.language_models.TextEmbeddingModel) and get a model.\n", + "\n", + "For more information, refer to:\n", + "\n", + "- [Vertex AI: Get Text Embeddings](https://cloud.google.com/vertex-ai/docs/generative-ai/embeddings/get-text-embeddings)\n", + "- [Vertex AI: Model versions and lifecycle](https://cloud.google.com/vertex-ai/docs/generative-ai/learn/model-versioning)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "20498d0ff2bb" + }, + "outputs": [], + "source": [ + "# Load the text embeddings model\n", + "model = TextEmbeddingModel.from_pretrained(\"text-embedding-005\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "b5f970232b4e" + }, + "outputs": [], + "source": [ + "# Get embeddings for a list of texts\n", + "\n", + "\n", + "def get_embeddings_wrapper(texts, batch_size: int = 5) -> list:\n", + " embs = []\n", + " for i in tqdm(range(0, len(texts), batch_size)):\n", + " # Create embeddings optimized for document retrieval\n", + " result = model.get_embeddings(\n", + " [\n", + " TextEmbeddingInput(text=text, task_type=\"RETRIEVAL_DOCUMENT\")\n", + " for text in texts[i : i + batch_size]\n", + " ]\n", + " )\n", + " embs.extend([e.values for e in result])\n", + " return embs" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "9ad26e79ea33" + }, + "source": [ + "Get embeddings for the question titles/body and add them as the `\"embedding\"` column." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "84025146f55e" + }, + "outputs": [], + "source": [ + "df[\"title_body\"] = df[\"title\"] + \"\\n\" + df[\"body\"]\n", + "\n", + "df = df.assign(embedding=get_embeddings_wrapper(df.title_body))\n", + "df.head()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "TKBmi2BMk_OU" + }, + "source": [ + "## Scrape HTML from Question Pages\n", + "\n", + "- Get the HTML from the StackOverflow Question page\n", + " - Upload it to GCS as the Document Store/for displayed search results" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "tXHmC10IitET" + }, + "outputs": [], + "source": [ + "JSONL_MIME_TYPE = \"application/jsonl\"\n", + "HTML_MIME_TYPE = \"text/html\"\n", + "\n", + "BUCKET_NAME = \"ucs-demo\"\n", + "DIRECTORY = \"embeddings-stackoverflow\"\n", + "BLOB_PREFIX = f\"{DIRECTORY}/html/\"\n", + "\n", + "GCS_URI_PREFIX = f\"gs://{BUCKET_NAME}/{BLOB_PREFIX}\"\n", + "\n", + "storage_client = storage.Client()\n", + "bucket = storage_client.bucket(BUCKET_NAME)\n", + "\n", + "\n", + "def scrape_question(question_url: str) -> str:\n", + " response = requests.get(question_url)\n", + "\n", + " if response.status_code != 200 or not response.content:\n", + " print(f\"URL: {question_url} Code: {response.status_code}\")\n", + " return None\n", + "\n", + " print(f\"Scraping {question_url}\")\n", + "\n", + " link_title = response.url.split(\"/\")[-1] + \".html\"\n", + " gcs_uri = f\"{GCS_URI_PREFIX}{link_title}\"\n", + "\n", + " # Upload HTML to Google Cloud Storage\n", + " blob = bucket.blob(f\"{BLOB_PREFIX}{link_title}\")\n", + " blob.upload_from_string(response.content, content_type=HTML_MIME_TYPE)\n", + " time.sleep(1)\n", + " return gcs_uri" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "7b2109f01aef" + }, + "outputs": [], + "source": [ + "# Get the published URL from the ID\n", + "QUESTION_BASE_URL = \"https://stackoverflow.com/questions/\"\n", + "df[\"question_url\"] = df[\"id\"].apply(lambda x: f\"{QUESTION_BASE_URL}{x}\")\n", + "\n", + "# Scrape HTML from stackoverflow.com and upload to GCS\n", + "df[\"gcs_uri\"] = df[\"question_url\"].apply(scrape_question)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "31fd6d9cf358" + }, + "source": [ + "Restructure the embeddings data to JSONL to follow the [Vertex AI Search format (Unstructured with Metadata)](https://cloud.google.com/generative-ai-app-builder/docs/prepare-data). This format is required to use custom embeddings." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "61e4989dfde3" + }, + "outputs": [], + "source": [ + "EMBEDDINGS_FIELD_NAME = \"embedding_vector\"\n", + "\n", + "\n", + "def format_row(row):\n", + " return {\n", + " \"id\": row[\"id\"],\n", + " \"content\": {\"mimeType\": HTML_MIME_TYPE, \"uri\": row[\"gcs_uri\"]},\n", + " \"structData\": {\n", + " EMBEDDINGS_FIELD_NAME: row[\"embedding\"],\n", + " \"title\": row[\"title\"],\n", + " \"body\": row[\"body\"],\n", + " \"question_url\": row[\"question_url\"],\n", + " \"answer_count\": row[\"answer_count\"],\n", + " \"creation_date\": row[\"creation_date\"],\n", + " \"score\": row[\"score\"],\n", + " },\n", + " }\n", + "\n", + "\n", + "vais_embeddings = (\n", + " df.apply(format_row, axis=1)\n", + " .to_json(orient=\"records\", lines=True, force_ascii=False)\n", + " .replace(r\"\\/\", \"/\") # To prevent escaping the / characters\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "ca66ea170841" + }, + "source": [ + "Upload the JSONL file to Google Cloud Storage" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "09d9ef95f44f" + }, + "outputs": [], + "source": [ + "jsonl_filename = f\"{DIRECTORY}/vais_embeddings.jsonl\"\n", + "embeddings_file = f\"gs://{BUCKET_NAME}/{jsonl_filename}\"\n", + "\n", + "blob = bucket.blob(jsonl_filename)\n", + "blob.upload_from_string(vais_embeddings, content_type=JSONL_MIME_TYPE)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "2a7db96893f7" + }, + "source": [ + "## Set up Vertex AI Search & Conversation" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "1046f3aaa57a" + }, + "outputs": [], + "source": [ + "DATA_STORE_LOCATION = \"global\"\n", + "\n", + "client_options = (\n", + " ClientOptions(api_endpoint=f\"{DATA_STORE_LOCATION}-discoveryengine.googleapis.com\")\n", + " if DATA_STORE_LOCATION != \"global\"\n", + " else None\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "a11ceb9aded5" + }, + "outputs": [], + "source": [ + "def create_data_store(\n", + " project_id: str, location: str, data_store_name: str, data_store_id: str\n", + "):\n", + " # Create a client\n", + " client = discoveryengine.DataStoreServiceClient(client_options=client_options)\n", + "\n", + " # Initialize request argument(s)\n", + " data_store = discoveryengine.DataStore(\n", + " display_name=data_store_name,\n", + " industry_vertical=\"GENERIC\",\n", + " content_config=\"CONTENT_REQUIRED\",\n", + " solution_types=[\"SOLUTION_TYPE_SEARCH\"],\n", + " )\n", + "\n", + " request = discoveryengine.CreateDataStoreRequest(\n", + " parent=discoveryengine.DataStoreServiceClient.collection_path(\n", + " project_id, location, \"default_collection\"\n", + " ),\n", + " data_store=data_store,\n", + " data_store_id=data_store_id,\n", + " )\n", + " operation = client.create_data_store(request=request)\n", + "\n", + " try:\n", + " operation.result()\n", + " except GoogleAPICallError:\n", + " pass\n", + "\n", + "\n", + "def update_schema(\n", + " project_id: str,\n", + " location: str,\n", + " data_store_id: str,\n", + "):\n", + " client = discoveryengine.SchemaServiceClient(client_options=client_options)\n", + "\n", + " schema = discoveryengine.Schema(\n", + " name=client.schema_path(project_id, location, data_store_id, \"default_schema\"),\n", + " struct_schema={\n", + " \"$schema\": \"https://json-schema.org/draft/2020-12/schema\",\n", + " \"type\": \"object\",\n", + " \"properties\": {\n", + " EMBEDDINGS_FIELD_NAME: {\n", + " \"type\": \"array\",\n", + " \"keyPropertyMapping\": \"embedding_vector\",\n", + " \"dimension\": 768,\n", + " \"items\": {\"type\": \"number\"},\n", + " }\n", + " },\n", + " },\n", + " )\n", + "\n", + " operation = client.update_schema(\n", + " request=discoveryengine.UpdateSchemaRequest(schema=schema)\n", + " )\n", + "\n", + " print(\"Waiting for operation to complete...\")\n", + "\n", + " response = operation.result()\n", + "\n", + " # Handle the response\n", + " print(response)\n", + "\n", + "\n", + "def import_documents(\n", + " project_id: str,\n", + " location: str,\n", + " data_store_id: str,\n", + " gcs_uri: str,\n", + "):\n", + " client = discoveryengine.DocumentServiceClient(client_options=client_options)\n", + "\n", + " # The full resource name of the search engine branch.\n", + " # e.g. projects/{project}/locations/{location}/dataStores/{data_store_id}/branches/{branch}\n", + " parent = client.branch_path(\n", + " project=project_id,\n", + " location=location,\n", + " data_store=data_store_id,\n", + " branch=\"default_branch\",\n", + " )\n", + "\n", + " request = discoveryengine.ImportDocumentsRequest(\n", + " parent=parent,\n", + " gcs_source=discoveryengine.GcsSource(input_uris=[gcs_uri]),\n", + " # Options: `FULL`, `INCREMENTAL`\n", + " reconciliation_mode=discoveryengine.ImportDocumentsRequest.ReconciliationMode.FULL,\n", + " )\n", + "\n", + " # Make the request\n", + " operation = client.import_documents(request=request)\n", + "\n", + "\n", + "def create_engine(\n", + " project_id: str, location: str, data_store_name: str, data_store_id: str\n", + "):\n", + " client = discoveryengine.EngineServiceClient(client_options=client_options)\n", + "\n", + " # Initialize request argument(s)\n", + " config = discoveryengine.Engine.SearchEngineConfig(\n", + " search_tier=\"SEARCH_TIER_ENTERPRISE\", search_add_ons=[\"SEARCH_ADD_ON_LLM\"]\n", + " )\n", + "\n", + " engine = discoveryengine.Engine(\n", + " display_name=data_store_name,\n", + " solution_type=\"SOLUTION_TYPE_SEARCH\",\n", + " industry_vertical=\"GENERIC\",\n", + " data_store_ids=[data_store_id],\n", + " search_engine_config=config,\n", + " )\n", + "\n", + " request = discoveryengine.CreateEngineRequest(\n", + " parent=discoveryengine.DataStoreServiceClient.collection_path(\n", + " project_id, location, \"default_collection\"\n", + " ),\n", + " engine=engine,\n", + " engine_id=engine.display_name,\n", + " )\n", + "\n", + " # Make the request\n", + " operation = client.create_engine(request=request)\n", + " response = operation.result(timeout=90)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "07821e2bf681" + }, + "outputs": [], + "source": [ + "DATA_STORE_NAME = \"stackoverflow-embeddings\"\n", + "DATA_STORE_ID = f\"{DATA_STORE_NAME}-id\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "abacceea9f35" + }, + "outputs": [], + "source": [ + "# Create a Data Store\n", + "create_data_store(PROJECT_ID, DATA_STORE_LOCATION, DATA_STORE_NAME, DATA_STORE_ID)\n", + "\n", + "# Update the Data Store Schema for embeddings\n", + "update_schema(PROJECT_ID, DATA_STORE_LOCATION, DATA_STORE_ID)\n", + "\n", + "# Import the embeddings JSONL file\n", + "import_documents(PROJECT_ID, DATA_STORE_LOCATION, DATA_STORE_ID, embeddings_file)\n", + "\n", + "# Create a Search App and attach the Data Store\n", + "create_engine(PROJECT_ID, DATA_STORE_LOCATION, DATA_STORE_NAME, DATA_STORE_ID)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "b6066cae8e5b" + }, + "source": [ + "Next, we need to set the embedding specification for the data store. We will set the same spec for all search requests.\n", + "\n", + "`0.5 * relevance_score`\n", + "\n", + "- This is not supported in client libraries, so we will use the `requests` module to make a REST request\n", + "- Documentation: https://cloud.google.com/generative-ai-app-builder/docs/bring-embeddings#global" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "1ea9cda23c98" + }, + "outputs": [], + "source": [ + "access_token = (\n", + " subprocess.check_output([\"gcloud\", \"auth\", \"print-access-token\"])\n", + " .decode(\"utf-8\")\n", + " .strip()\n", + ")\n", + "\n", + "response = requests.patch(\n", + " url=f\"https://discoveryengine.googleapis.com/v1alpha/projects/{PROJECT_ID}/locations/{DATA_STORE_LOCATION}/collections/default_collection/dataStores/{DATA_STORE_ID}/servingConfigs/default_search?updateMask=embeddingConfig,rankingExpression\",\n", + " headers={\n", + " \"Authorization\": f\"Bearer {access_token}\",\n", + " \"Content-Type\": \"application/json; charset=utf-8\",\n", + " \"X-Goog-User-Project\": PROJECT_ID,\n", + " },\n", + " json={\n", + " \"name\": f\"projects/{PROJECT_ID}/locations/{DATA_STORE_LOCATION}/collections/default_collection/dataStores/{DATA_STORE_ID}/servingConfigs/default_search\",\n", + " \"embeddingConfig\": {\"fieldPath\": EMBEDDINGS_FIELD_NAME},\n", + " \"ranking_expression\": \"0.5 * relevance_score\",\n", + " },\n", + ")\n", + "\n", + "print(response.text)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "b4dbe6e41fb9" + }, + "source": [ + "## Test Search App\n", + "\n", + "Make a sample query to check how the results look." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "f0892ff0bf0e" + }, + "outputs": [], + "source": [ + "def search_data_store(\n", + " project_id: str,\n", + " location: str,\n", + " data_store_id: str,\n", + " search_query: str,\n", + ") -> list[discoveryengine.SearchResponse]:\n", + " # Create a client\n", + " client = discoveryengine.SearchServiceClient(client_options=client_options)\n", + "\n", + " # The full resource name of the search engine serving config\n", + " # e.g. projects/{project_id}/locations/{location}/dataStores/{data_store_id}/servingConfigs/{serving_config_id}\n", + " serving_config = client.serving_config_path(\n", + " project=project_id,\n", + " location=location,\n", + " data_store=data_store_id,\n", + " serving_config=\"default_config\",\n", + " )\n", + "\n", + " # Optional: Configuration options for search\n", + " # Refer to the `ContentSearchSpec` reference for all supported fields:\n", + " # https://cloud.google.com/python/docs/reference/discoveryengine/latest/google.cloud.discoveryengine_v1.types.SearchRequest.ContentSearchSpec\n", + " content_search_spec = discoveryengine.SearchRequest.ContentSearchSpec(\n", + " # For information about snippets, refer to:\n", + " # https://cloud.google.com/generative-ai-app-builder/docs/snippets\n", + " snippet_spec=discoveryengine.SearchRequest.ContentSearchSpec.SnippetSpec(\n", + " return_snippet=True\n", + " ),\n", + " # For information about search summaries, refer to:\n", + " # https://cloud.google.com/generative-ai-app-builder/docs/get-search-summaries\n", + " summary_spec=discoveryengine.SearchRequest.ContentSearchSpec.SummarySpec(\n", + " summary_result_count=5,\n", + " include_citations=True,\n", + " ignore_adversarial_query=True,\n", + " ignore_non_summary_seeking_query=True,\n", + " ),\n", + " )\n", + "\n", + " # Refer to the `SearchRequest` reference for all supported fields:\n", + " # https://cloud.google.com/python/docs/reference/discoveryengine/latest/google.cloud.discoveryengine_v1.types.SearchRequest\n", + " request = discoveryengine.SearchRequest(\n", + " serving_config=serving_config,\n", + " query=search_query,\n", + " page_size=10,\n", + " content_search_spec=content_search_spec,\n", + " query_expansion_spec=discoveryengine.SearchRequest.QueryExpansionSpec(\n", + " condition=discoveryengine.SearchRequest.QueryExpansionSpec.Condition.AUTO,\n", + " ),\n", + " spell_correction_spec=discoveryengine.SearchRequest.SpellCorrectionSpec(\n", + " mode=discoveryengine.SearchRequest.SpellCorrectionSpec.Mode.AUTO\n", + " ),\n", + " )\n", + "\n", + " response = client.search(request)\n", + " return response" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "64ba2ea463f5" + }, + "outputs": [], + "source": [ + "search_query = \"How do I create an array in Java?\"\n", + "\n", + "response = search_data_store(\n", + " PROJECT_ID, DATA_STORE_LOCATION, DATA_STORE_ID, search_query\n", + ")\n", + "\n", + "print(f\"Summary: {response.summary.summary_text}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "d11679ae854d" + }, + "source": [ + "## Deploy search engine\n", + "\n", + "This search engine can now be deployed to a web page using the prebuilt [search widget](https://cloud.google.com/generative-ai-app-builder/docs/add-widget)\n", + "\n", + "For a deployed example of the Stack Overflow custom embeddings search engine, go to [vertex-ai-search.web.app](https://vertex-ai-search.web.app/)." + ] + } + ], + "metadata": { + "colab": { + "name": "custom_embeddings.ipynb", + "toc_visible": true + }, + "kernelspec": { + "display_name": "Python 3", + "name": "python3" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} diff --git a/search/custom-ranking/clearbox.ipynb b/search/custom-ranking/clearbox.ipynb new file mode 100644 index 0000000..abf0cc4 --- /dev/null +++ b/search/custom-ranking/clearbox.ipynb @@ -0,0 +1,659 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "2zyrrTp0QwkE" + }, + "outputs": [], + "source": [ + "# Copyright 2025 Google LLC\n", + "#\n", + "# Licensed under the Apache License, Version 2.0 (the \"License\");\n", + "# you may not use this file except in compliance with the License.\n", + "# You may obtain a copy of the License at\n", + "#\n", + "# https://www.apache.org/licenses/LICENSE-2.0\n", + "#\n", + "# Unless required by applicable law or agreed to in writing, software\n", + "# distributed under the License is distributed on an \"AS IS\" BASIS,\n", + "# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n", + "# See the License for the specific language governing permissions and\n", + "# limitations under the License." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "4yz5pBb8RHi2" + }, + "source": [ + "# Clearbox for Ranking Tuning" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "iGLQTYerXKa0" + }, + "source": [ + "\n", + " \n", + " \n", + " \n", + " \n", + "
    \n", + " \n", + " \"Google
    Open in Colab\n", + "
    \n", + "
    \n", + " \n", + " \"Google
    Open in Colab Enterprise\n", + "
    \n", + "
    \n", + " \n", + " \"Vertex
    Open in Vertex AI Workbench\n", + "
    \n", + "
    \n", + " \n", + " \"GitHub
    View on GitHub\n", + "
    \n", + "
    \n", + "\n", + "
    \n", + "\n", + "Share to:\n", + "\n", + "\n", + " \"LinkedIn\n", + "\n", + "\n", + "\n", + " \"Bluesky\n", + "\n", + "\n", + "\n", + " \"X\n", + "\n", + "\n", + "\n", + " \"Reddit\n", + "\n", + "\n", + "\n", + " \"Facebook\n", + "" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "jZy3Q-RagNKm" + }, + "source": [ + "| Author |\n", + "| --- |\n", + "| [Andrei Papou](https://github.com/andrei-papou) |" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "8tGlF6DhStHp" + }, + "source": [ + "## Overview" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "eMZE2a9vSwsR" + }, + "source": [ + "In this notebook we will use the [ClearBox](https://github.com/GoogleCloudPlatform/clearbox) library to improve the default [Vertex AI Search](https://cloud.google.com/generative-ai-app-builder/docs/enterprise-search-introduction) ranking for a given query set." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "i-iwIpJqRcke" + }, + "source": [ + "## Get Started" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "nxI7hhthRf6L" + }, + "source": [ + "### Install ClearBox from GitHub" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "kzPb2Ju8RmhD" + }, + "outputs": [], + "source": [ + "%pip install \"git+https://github.com/GoogleCloudPlatform/clearbox\"" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "OLreVAILTBAL" + }, + "source": [ + "## Ranking Tuning" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "dFy2MsD-TFNl" + }, + "source": [ + "### Imports" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "ODC6BF12n6VA" + }, + "outputs": [], + "source": [ + "import math\n", + "import random\n", + "import warnings\n", + "\n", + "from clearbox import features as F\n", + "from clearbox.features import signals as S\n", + "from clearbox.metrics.recall import RecallAtK\n", + "from clearbox.models.probabilistic import (\n", + " BayesianRidgeModel,\n", + " BayesOptLinearModel,\n", + ")\n", + "from clearbox.models.probabilistic import ExpectedImprovement as EI\n", + "from clearbox.models.regression import LinRegModel\n", + "from clearbox.training import Trainer\n", + "from clearbox.visualization import Visualizer\n", + "import matplotlib.pyplot as plt\n", + "import numpy as np\n", + "import pandas as pd" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "7fGvXIH4DbQj" + }, + "outputs": [], + "source": [ + "warnings.filterwarnings(\"ignore\", category=DeprecationWarning)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "_F0-noZcTHmz" + }, + "source": [ + "### Pin the random seed" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "0Zv36-pQsobR" + }, + "outputs": [], + "source": [ + "def seed_everything(seed: int):\n", + " random.seed(seed)\n", + " np.random.seed(seed)\n", + "\n", + "\n", + "seed_everything(42)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "vJMQd5XwTMfx" + }, + "source": [ + "### Load and explore the dataset" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "M2eLSvNPmjYv" + }, + "outputs": [], + "source": [ + "qs_df = pd.read_csv(\n", + " \"https://storage.googleapis.com/github-repo/search/clearbox/beir_fiqa_50.csv\"\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "YGJumCMEmqfi" + }, + "outputs": [], + "source": [ + "qs_df.head(3)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "a9MZiVShTQpu" + }, + "source": [ + "### Preprocess the dataset and generate features" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "AKVClzm49g9v" + }, + "outputs": [], + "source": [ + "COL_QUERY_CODE = \"query_code\"\n", + "COL_TARGET = \"is_match\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "mDfVntkE9qc6" + }, + "outputs": [], + "source": [ + "qs_df[COL_QUERY_CODE] = F.encode(qs_df[\"query\"])" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "G9y3f7oT_uuy" + }, + "outputs": [], + "source": [ + "def _generate_is_match_col(df: pd.DataFrame) -> pd.DataFrame:\n", + " query_to_matched_titles = {}\n", + " for _, row in df.iterrows():\n", + " query_code = row[COL_QUERY_CODE]\n", + " if query_code in query_to_matched_titles:\n", + " continue\n", + " query_to_matched_titles[query_code] = {\n", + " targets_str.removeprefix(\"gs://beir_fiqa/\").removesuffix(\".txt\")\n", + " for targets_str in row[\"targets\"].split(\" \")\n", + " }\n", + " df[COL_TARGET] = np.zeros((len(df),), dtype=np.uint8)\n", + " for query_code, matched_titles in query_to_matched_titles.items():\n", + " df.loc[\n", + " (df[COL_QUERY_CODE] == query_code)\n", + " & (df[\"title\"].astype(str).isin(matched_titles)),\n", + " COL_TARGET,\n", + " ] = 1\n", + "\n", + "\n", + "_generate_is_match_col(qs_df)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "PnH-R_JjB0XL" + }, + "outputs": [], + "source": [ + "qs_df.head(3)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "GNnDCy_NCqA7" + }, + "outputs": [], + "source": [ + "len(qs_df[qs_df[\"is_match\"] == 1])" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "2tsCIFS0CDaH" + }, + "source": [ + " ### Explore the dataset" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "wORGCOf4CHej" + }, + "outputs": [], + "source": [ + "def _plot_signal_distribution(\n", + " df: pd.DataFrame, signal_list: list[str], n_per_row: int = 3\n", + "):\n", + " n = len(signal_list)\n", + " n_rows = math.ceil(n / n_per_row)\n", + " f, axes = plt.subplots(n_rows, n_per_row, squeeze=False)\n", + " f.set_figheight(5 * n_rows)\n", + " f.set_figwidth(7 * n_per_row)\n", + " for i, signal in enumerate(signal_list):\n", + " ax = axes[i // n_per_row][i % n_per_row]\n", + " ax.set_title(signal)\n", + " ax.hist(df[signal], bins=50)\n", + "\n", + "\n", + "_plot_signal_distribution(\n", + " qs_df,\n", + " [\n", + " \"gecko_score\",\n", + " \"bm25_score\",\n", + " \"jetstream_score\",\n", + " \"freshness_rank\",\n", + " \"base_rank\",\n", + " ],\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "F1xu_uVtDBiM" + }, + "outputs": [], + "source": [ + "for signal_name in [\n", + " \"gecko_score\",\n", + " \"bm25_score\",\n", + " \"jetstream_score\",\n", + " \"freshness_rank\",\n", + " \"base_rank\",\n", + "]:\n", + " if qs_df[signal_name].isna().any():\n", + " print(\n", + " f\"{signal_name}: {len(qs_df[qs_df[signal_name].isna()]) / len(qs_df) * 100:.2f}% are NaNs\"\n", + " )" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "BtpiU9gkbERh" + }, + "source": [ + "Looks like we have some NaN values in `bm25_score` column. We'll handle that later." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "7U1o1AMDC_wt" + }, + "outputs": [], + "source": [ + "plt.hist(qs_df[\"is_match\"], bins=50)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "d-s2nczJTetv" + }, + "source": [ + "### Train the model" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "jJ8XcHqxToSi" + }, + "source": [ + "First we will create instances of `Trainer` and `Visualizer` classes. We will use `trainer` to train and validate the model and `visualizer` will help us explore the results.\n", + "\n", + "The `Trainer` has a number of parameters to customize the training procedure, let's look at some of them.\n", + "\n", + "First of all, under the hood `Trainer` does a number\n", + "of random cross-validation splits and trains and validates a new model on each of them. `seeds` argument specifies how many CV splits we should do and which random seed we should use for each. `n_folds` specifies the number of folds we generate for each split.\n", + "\n", + "Once a new model is trained for a given seed and fold combination, the `Trainer` will calculate metric value on both train and validation parts of the fold for each metric object from `metrics` list. All those metric values are then aggregated and added to the return value of `train` method, you can access them using `.metrics` attribute.\n", + "\n", + "`target_col` specifies the column of the `qs_df` data frame to use as a target. `query_col` is used to identify the query column, please note that we use the integer encoded version of the query we've generated above." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "GJScuHenA7w9" + }, + "outputs": [], + "source": [ + "trainer = Trainer(\n", + " df=qs_df,\n", + " seeds=[7, 15, 21, 42, 81],\n", + " n_folds=3,\n", + " metrics=[RecallAtK(k) for k in [1, 3, 5]],\n", + " target_col=COL_TARGET,\n", + " query_col=COL_QUERY_CODE,\n", + ")\n", + "visualizer = Visualizer(metrics=trainer.metrics)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "LlrDrP7nUDJo" + }, + "source": [ + "It's a good practice in ranking to compare the model against the individual signal, just to make sure our model is an improvement compared to the features we train on. Also, as the ranking signals Vertex AI exposes take different aspects of query-document relevance into account (e.g. topicality, relevance, semantic similarity, etc.), looking at the baselines is a good way to understand which if the signals are more important for your query set." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "L2V8grQ0BV6K" + }, + "outputs": [], + "source": [ + "baseline_list = [\n", + " (\"base_rank\", trainer.get_feature_baseline(feature=-S.base_rank)),\n", + " (\"gecko_score\", trainer.get_feature_baseline(feature=S.gecko_score)),\n", + " (\n", + " \"bm25_score\",\n", + " trainer.get_feature_baseline(feature=F.FillNaN(S.bm25_score, F.Constant(0.0))),\n", + " ),\n", + " (\"jetstream_score\", trainer.get_feature_baseline(feature=S.jetstream_score)),\n", + " (\"freshness_rank\", trainer.get_feature_baseline(feature=-S.freshness_rank)),\n", + "]" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "12qHHlKLVIWD" + }, + "source": [ + "Now we will train a linear regression model on reciprocal ranks of input signals we've explored above.\n", + "\n", + "`features` argument of the `train` method accepts a list of input features to train the model on. Notice how the features are created:\n", + "\n", + "- We use `clearbox.features` module (conveniently imported as `F` alias) to do all the feature engineering we need. Doing it this way guarantees we'll be able to seamlessly deploy the formula to production.\n", + "\n", + "- We use `S` utility object to create a signal node. `S.gecko_score` tells the trainer that we should read the `gecko_score` column of the input data frame. Under the hood, it's just a syntactic sugar over `F.Signal(\"gecko_score\")`.\n", + "\n", + "- We use `F.RR` class to generate reciprocal ranks. Although for some of the signals (like `gecko_score` or `bm25_score`) we have raw values, not just ranks, it's always recommended to train the model on reciprocal ranks. Doing it this way guarantees better stability of the formula across signal updates (e.g. a new version of Jetstream model is released and the distribution of the score changes slightly).\n", + "\n", + "- When computing reciprocal ranks, we make sure the input signal is monotonically increasing (e.g. better match corresponds to higher signal value). As the ranks are usually monotonically decreasing, we preprocess them using negation operation: `-S.base_rank`.\n", + "\n", + "- We use `F.FillNaN` class to replace `NaN` values with `0`. It's important to do that here using `F` module utilities as this way we can guarantee same logic will be used for any NaNs we encounter while serving the formula in production." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "OkcfwMF4FO5d" + }, + "outputs": [], + "source": [ + "reg_training_results = trainer.train(\n", + " LinRegModel(),\n", + " features=[\n", + " F.RR(-S.base_rank, 40.0, group_by=S.query_code),\n", + " F.RR(S.gecko_score, 40.0, group_by=S.query_code),\n", + " F.RR(F.FillNaN(S.bm25_score, F.Constant(0.0)), 40.0, group_by=S.query_code),\n", + " F.RR(S.jetstream_score, 40.0, group_by=S.query_code),\n", + " F.RR(-S.freshness_rank, 40.0, group_by=S.query_code),\n", + " ],\n", + " num_parallel_workers=4,\n", + " print_progress=True,\n", + ")\n", + "visualizer.visualize_training_results(reg_training_results, baseline_list);" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "-cV8yG98cBSM" + }, + "source": [ + "Looks like we've improved over the baselines quite a bit!\n", + "\n", + "`training_results` object which is returned by the `train` method provides a number of useful properties:\n", + "\n", + "- `metrics` data frame contains all the metric values computed on train and validation parts for each seed & fold pair.\n", + "- `ranking_formula` represents the final ranking model we've trained.\n", + "\n", + "The ranking formula object has a number of useful methods, but the most important one is `serialize_to_ranking_expression`. It serializes the formula into the deployment format. To deploy the formula, put the string returned by the method into the `ranking_expression` field of the request and make sure `ranking_expression_backend` field is set to `CLEARBOX`. And that's it, the formula is now used for reranking the results of this particular request.\n", + "\n", + "Now let's look at the formula we've just tuned." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "n4o3RKTefINo" + }, + "outputs": [], + "source": [ + "print(reg_training_results.ranking_formula.serialize_to_ranking_expression())" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "-xSEAMJXfghj" + }, + "source": [ + "Now let's try a different kind of model and see if we can beat the current result. Intuitively it does seem like optimizing against a 0/1 target using a regression model isn't a great idea.\n", + "\n", + "What we'll do instead is optimize the recall directly. Of course, the metric is non-differentiable, but we still can try different combinations of weights and see which of those give us better results. But even if we are going to try just 10 values from 0.0 to 1.0 for each of the 5 signal, we'll have to wait a lot as we'll need to compute recall 5 seed x 3 folds x 10 ** 5 models = 1500000 times. Instead, we gonna use Bayesian Optimization technique to pick the most promising combinations of weights. `BayesOptLinearModel` does just that." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "u-WehEiOk2ub" + }, + "outputs": [], + "source": [ + "bayes_training_results = trainer.train(\n", + " BayesOptLinearModel(\n", + " surrogate_model=BayesianRidgeModel(),\n", + " acquisition_function=EI(xi=1e-6),\n", + " metric=RecallAtK(1),\n", + " grid_size=21,\n", + " seed_batch_size=512,\n", + " batch_size=32,\n", + " n_opt_steps=8,\n", + " ),\n", + " features=[\n", + " F.RR(-S.base_rank, 40.0, group_by=S.query_code),\n", + " F.RR(S.gecko_score, 40.0, group_by=S.query_code),\n", + " F.RR(F.FillNaN(S.bm25_score, F.Constant(0.0)), 40.0, group_by=S.query_code),\n", + " F.RR(S.jetstream_score, 40.0, group_by=S.query_code),\n", + " F.RR(-S.freshness_rank, 40.0, group_by=S.query_code),\n", + " ],\n", + " num_parallel_workers=4,\n", + " print_progress=True,\n", + ")\n", + "visualizer.visualize_training_results(\n", + " bayes_training_results,\n", + " [\n", + " *baseline_list,\n", + " (\"regression_model\", reg_training_results.metrics),\n", + " ],\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "03zXmeIOtkwO" + }, + "outputs": [], + "source": [ + "print(bayes_training_results.ranking_formula.serialize_to_ranking_expression())" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "F3gyRLipxiv_" + }, + "source": [ + "The intuition worked and we were able to improve all 3 recall metrics, `~0.04` improvement in `recall@1` looks decent. Notice how the formula is very close to the previous one, the only substantial difference is that it uses `base_rank` instead of `freshness_rank`." + ] + } + ], + "metadata": { + "colab": { + "name": "clearbox.ipynb", + "toc_visible": true + }, + "kernelspec": { + "display_name": "Python 3", + "name": "python3" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} diff --git a/search/gemini-enterprise/ge-migration-agent/.gitignore b/search/gemini-enterprise/ge-migration-agent/.gitignore new file mode 100644 index 0000000..0a131f4 --- /dev/null +++ b/search/gemini-enterprise/ge-migration-agent/.gitignore @@ -0,0 +1,47 @@ +# Environments & Virtualenvs +.env +.env.* +!.env.sample +.venv/ +venv/ +ENV/ + +# Python Cache & Runtime +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg + +# ADK Runtime & SQLite DBs +.adk/ +*.db +*.sqlite3 + +# Agent Data & Staging Backups +*.json +!skills/*.json +skills/migration_skill.json +qbr_export.json +*_export.json + +# OS & Editor Files +.DS_Store +Thumbs.db +.vscode/ +.idea/ diff --git a/search/gemini-enterprise/ge-migration-agent/README.md b/search/gemini-enterprise/ge-migration-agent/README.md new file mode 100644 index 0000000..accfb09 --- /dev/null +++ b/search/gemini-enterprise/ge-migration-agent/README.md @@ -0,0 +1,132 @@ +# 🚀 Gemini Enterprise Migration Agent + +> Seamlessly migrate your low-code agents and Notebooklms to Gemini Enterprise. + +Built with the **Agent Development Kit (ADK)**, this specialized AI agent facilitates the migration of low code agents and notebooklm from one Gemini Enterprise App to another. It can be further extended to support migration of 3rd Party agents to Gemini Enterprise App via adapters(mappings between 3rd party agent and import format required by Gemini Enterprise App) + +## Architecture + +The following diagram illustrates the high-level architecture of the migration agent and its interactions with various services. + +![Architecture Diagram](./ge-migration-agent-architecture.png) + +## đŸ› ïž Capabilities & Skills + +### đŸ€– Agent Migration +- **Discovery**: List and explore source low-code agents available for migration. +- **Direct Migration**: Seamlessly transfer agent definitions to target Gemini Enterprise engines. +- **GCS Staging**: Export definitions to Google Cloud Storage for isolated or staged migrations. + +### 📚 Notebooklm migration +- **Instant Notebooks**: Create NotebookLM-style knowledge bases in the target project preserving source titles exactly. +- **Batch Ingestion**: Rapidly populate notebooks with sources from web URLs. +- **GCS Staging**: Export notebook definitions (metadata and source references) to Google Cloud Storage for backup or staged imports. + +### đŸ›Ąïž Validation & Safety +- **Enterprise Separation of Concerns (`SKILL.md`)**: Fully decouple operational administrative configuration (Source/Target Project IDs, Engine IDs, DataStore Grounding Mappings) from agent execution logic. +- **Smart Introspection**: Automatically present clear capabilities lists and verify active DataStore mappings on greeting. +- **Safe Guardrails**: Enforce explicit Project and Engine IDs to prevent accidental data overwrites. + +## 🚀 Getting Started + +### 📋 Prerequisites +- **Python**: Version 3.10 or higher. +- **Google Cloud**: A project with the **Discovery Engine API** enabled. +- **Authentication**: Application Default Credentials (ADC) configured. + +### đŸ› ïž Setup & Run + +1. **Configure Environment**: Ensure your Google Cloud project is set up and Application Default Credentials (ADC) are configured. Make sure your `.env` file matches the format in `ge_migration_agent/.env.sample`. Additionally, configure your canonical migration parameters and DataStore mappings in `skills/SKILL.md`. + +--- + +#### Option A: Run the Deterministic CLI via uv (Highly Recommended) +Before running the commands, ensure your Application Default Credentials (ADC) are authenticated: +```bash +gcloud auth application-default login +``` + +For 100% reliable, fast, and deterministic execution of migrations without any AI dependencies, use the newly added root entrypoint script: + +```bash +# Show help and all available commands +uv run ./migrate.py --help + +# List recently viewed NotebookLM notebooks in the source project +uv run ./migrate.py list-notebooks --project + +# Migrate an entire notebook and all of its sources atomically preserving the exact title +uv run ./migrate.py migrate-notebook "my-notebook-title" --source-project --target-project + +# Export a notebook definition to a Google Cloud Storage backup +uv run ./migrate.py export-notebook-gcs "my-notebook-title" "exports/my_notebook.json" --project --bucket + +# Import a notebook definition from GCS into your target project +uv run ./migrate.py import-notebook-gcs "exports/my_notebook.json" --target-project --bucket + +# List all employee-made low-code agents in the source engine +uv run ./migrate.py list-agents --project --engine-id + +# Introspect and map connectors (Dry Run) +uv run ./migrate.py lookup-map-connectors "Quarterly Business Review Generator" --source-project --source-engine --target-project --target-engine + +# Migrate a low-code agent directly from source to target +uv run ./migrate.py migrate-agent "Quarterly Business Review Generator" --source-project --source-engine --target-project --target-engine --force + +# Export an agent definition to a Google Cloud Storage staging backup +uv run ./migrate.py export-agent-gcs "Quarterly Business Review Generator" "exports/quarterly_business_review_generator.json" --project --engine-id --bucket + +# Import an agent definition from GCS into your target environment +uv run ./migrate.py import-agent-gcs "exports/quarterly_business_review_generator.json" --bucket --target-project --target-engine +``` + +--- + +#### Option B: Run the Conversational AI Agent +For natural language guidance and conversational interactions, launch the ADK playground: + +```bash +./ge_migration_agent/run_web_playground.sh +``` + +Open `http://localhost:8001` in your browser to chat with the agent. The agent is now powered by the exact same underlying deterministic core. + +## 💡 Example Usage + +Here is a sample session showing autonomous administrative greeting and low-code agent migration: + +### Sample Session Traces + +**User**: +> Hi / Help + +**Agent**: +> I can assist you with migrating your Gemini Enterprise app low-code agents and NotebookLM notebooks. +> +> **Active Migration Configuration**: +> - Source Environment: Project (``), Engine `` +> - Target Environment: Project (``), Engine `` +> - Verified DataStore Mappings: `ge-drive-all_<1776953145638_google_drive` -> `ge-drive-all_1780835769760_google_drive` + +**User**: +> List all the human made agents in source + +**Agent**: +> I found the following employee-made agents in your source environment: ... (Lists agents) + +**User**: +> Migrate Quarterly Business Review Generator from source to target + +**Agent**: +> The agent 'Quarterly Business Review Generator' has been successfully migrated to your target environment. + +## Project Structure +- `ge_migration_agent/`: Contains the core agent definition, tools, and execution scripts. + - `agent.py`: Core ADK agent definition and tool implementations. + - `run_web_playground.sh`: Script to start the persistent ADK web server. +- `skills/`: Holds enterprise migration configuration skills. + - `SKILL.md`: Single source of truth for cross-environment migration parameters and administrative setup guide. + +## Author +- **Navneet Tuteja** - [LinkedIn Profile](https://www.linkedin.com/in/navneettuteja/) + diff --git a/search/gemini-enterprise/ge-migration-agent/core.py b/search/gemini-enterprise/ge-migration-agent/core.py new file mode 100644 index 0000000..1b14f11 --- /dev/null +++ b/search/gemini-enterprise/ge-migration-agent/core.py @@ -0,0 +1,991 @@ +import json +import os +import logging +import time +import re +import urllib.parse +try: + from dotenv import load_dotenv + env_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), ".env") + load_dotenv(env_path, override=True) +except ImportError: + pass +import google.auth +from google.auth.transport.requests import AuthorizedSession + +def get_endpoint_host(location: str) -> str: + """Returns the host domain based on the geographic location.""" + if location == "global": + return "discoveryengine.googleapis.com" + return f"{location}-discoveryengine.googleapis.com" + +def get_session(): + """Returns an authorized Google API requests session.""" + credentials, project = google.auth.default( + scopes=["https://www.googleapis.com/auth/cloud-platform"] + ) + return AuthorizedSession(credentials) + +def list_notebooks(project_number: str, location: str = "global") -> list: + """Lists recently viewed notebooks in the specified project.""" + session = get_session() + host = get_endpoint_host(location) + url = f"https://{host}/v1alpha/projects/{project_number}/locations/{location}/notebooks:listRecentlyViewed" + + resp = session.get(url) + resp.raise_for_status() + data = resp.json() + return data.get("notebooks", []) + +def list_sources_and_types(notebook_id: str, project_number: str, location: str = "global") -> list: + """Lists sources and maps their types for a given notebook.""" + session = get_session() + host = get_endpoint_host(location) + base_url = f"https://{host}/v1alpha/projects/{project_number}/locations/{location}/notebooks/{notebook_id}" + + get_resp = session.get(base_url) + get_resp.raise_for_status() + nb_data = get_resp.json() + sources = nb_data.get("sources", []) + + results = [] + for src in sources: + src_id = src.get("sourceId", {}).get("id") + src_title = src.get("title") + + src_url = f"{base_url}/sources/{src_id}" + src_resp = session.get(src_url) + src_resp.raise_for_status() + src_data = src_resp.json() + + metadata = src_data.get("metadata", {}) + + source_type = "copied text" + source_location = "N/A" + + if "webpageMetadata" in metadata: + source_type = "website" + source_location = metadata["webpageMetadata"].get("webpageUrl") + elif "googleDocsMetadata" in metadata: + source_type = "google docs" + doc_id = metadata["googleDocsMetadata"].get("documentId") + source_location = f"https://docs.google.com/document/d/{doc_id}/edit" + + results.append({ + "title": src_title, + "id": src_id, + "type": source_type, + "location": source_location, + "raw_data": src_data + }) + + return results + +def create_notebook(target_project_number: str, target_location: str, title: str) -> dict: + """Creates a new empty notebook in the target project.""" + logging.info(f"DEBUG: create_notebook called with title='{title}', project='{target_project_number}', location='{target_location}'") + session = get_session() + host = get_endpoint_host(target_location) + url = f"https://{host}/v1alpha/projects/{target_project_number}/locations/{target_location}/notebooks" + + resp = session.post(url, json={"title": title}) + resp.raise_for_status() + return resp.json() + +def add_source_to_notebook(target_project_number: str, target_location: str, notebook_id: str, source_content: dict) -> dict: + """Adds a single source (userContent payload) to the specified notebook.""" + logging.info(f"DEBUG: add_source_to_notebook called with notebook_id='{notebook_id}', project='{target_project_number}'") + session = get_session() + host = get_endpoint_host(target_location) + url = f"https://{host}/v1alpha/projects/{target_project_number}/locations/{target_location}/notebooks/{notebook_id}/sources:batchCreate" + + # Make a copy to avoid mutating inputs + content_obj = json.loads(json.dumps(source_content)) + if "sourceName" in content_obj: + logging.info(f"DEBUG: Removing invalid 'sourceName' from payload: {content_obj['sourceName']}") + content_obj.pop("sourceName") + + logging.info(f"DEBUG: Sending request to {url} with payload: {json.dumps(content_obj)}") + resp = session.post(url, json={"userContents": [content_obj]}, timeout=60) + logging.info(f"DEBUG: Response status: {resp.status_code}") + resp.raise_for_status() + return resp.json() + +def extract_notebook_source_payload(src_raw_data: dict) -> dict: + """Intelligently extracts the userContent payload from a raw source data dict, reconstructs it if needed, and removes read-only fields.""" + user_content = src_raw_data.get("userContent") + if user_content: + payload = json.loads(json.dumps(user_content)) + else: + payload = {} + metadata = src_raw_data.get("metadata", {}) + if "webpageMetadata" in metadata: + payload["webContent"] = { + "url": metadata["webpageMetadata"].get("webpageUrl") + } + elif "googleDocsMetadata" in metadata: + payload["googleDriveContent"] = { + "documentId": metadata["googleDocsMetadata"].get("documentId"), + "mimeType": "application/vnd.google-apps.document" + } + elif "textContent" in src_raw_data: + payload["textContent"] = src_raw_data["textContent"] + elif "textContent" in metadata: + payload["textContent"] = metadata["textContent"] + + if "sourceName" in payload: + payload.pop("sourceName") + return payload + +def migrate_notebook_pipeline( + notebook_id_or_title: str, + target_project_number: str, + target_location: str, + source_project_number: str, + source_location: str = "global", + backup_bucket: str = "" +) -> dict: + """Migrates an entire notebook and all its sources deterministically in a python loop.""" + logging.info(f"Starting deterministic notebook migration for '{notebook_id_or_title}'") + + # 1. List source notebooks + notebooks = list_notebooks(source_project_number, source_location) + source_notebook = None + for nb in notebooks: + nb_id = nb.get("name", "").split("/")[-1] + nb_title = nb.get("title", "") + if nb_id == notebook_id_or_title or nb_title == notebook_id_or_title: + source_notebook = nb + break + + if not source_notebook: + raise ValueError(f"Source notebook '{notebook_id_or_title}' not found.") + + source_nb_id = source_notebook.get("name", "").split("/")[-1] + source_nb_title = source_notebook.get("title", "Untitled Notebook") + + # 2. Get all sources + sources = list_sources_and_types(source_nb_id, source_project_number, source_location) + logging.info(f"Found {len(sources)} sources to migrate for notebook '{source_nb_title}'") + + # 2.5 Upload to GCS (Backup) implicitly + bucket_name = backup_bucket if backup_bucket else os.environ.get("GCS_BUCKET_NAME") + if bucket_name: + try: + timestamp = int(time.time()) + object_name = f"exports/{source_nb_title}_{timestamp}.json" + exported_sources = [] + for src in sources: + payload = extract_notebook_source_payload(src.get("raw_data", {})) + exported_sources.append({ + "title": src["title"], + "userContent": payload + }) + notebook_def = { + "title": source_nb_title, + "sources": exported_sources + } + session = get_session() + encoded_obj = urllib.parse.quote(object_name, safe="") + upload_url = f"https://storage.googleapis.com/upload/storage/v1/b/{bucket_name}/o?uploadType=media&name={encoded_obj}" + up_resp = session.post( + upload_url, + data=json.dumps(notebook_def, indent=2), + headers={"Content-Type": "application/json"} + ) + up_resp.raise_for_status() + logging.info(f"Implicitly backed up notebook '{source_nb_title}' to gs://{bucket_name}/{object_name}") + except Exception as e: + logging.error(f"Failed to implicitly backup notebook to GCS: {e}") + + # 3. Create target notebook + target_nb = create_notebook(target_project_number, target_location, source_nb_title) + target_nb_id = target_nb.get("name", "").split("/")[-1] + logging.info(f"Created target notebook '{source_nb_title}' with ID: {target_nb_id}") + + # 4. For each source, map and add + migrated_sources = [] + failed_sources = [] + + for src in sources: + title = src.get("title", "Untitled Source") + raw_data = src.get("raw_data", {}) + payload = extract_notebook_source_payload(raw_data) + + logging.info(f"Adding source '{title}' to target notebook '{target_nb_id}'") + try: + add_resp = add_source_to_notebook(target_project_number, target_location, target_nb_id, payload) + migrated_sources.append({ + "title": title, + "status": "success", + "response": add_resp + }) + except Exception as e: + logging.error(f"Failed to add source '{title}': {e}") + failed_sources.append({ + "title": title, + "status": "failed", + "error": str(e) + }) + + return { + "success": len(failed_sources) == 0, + "source_notebook_id": source_nb_id, + "source_notebook_title": source_nb_title, + "target_notebook_id": target_nb_id, + "migrated_sources_count": len(migrated_sources), + "failed_sources_count": len(failed_sources), + "migrated": migrated_sources, + "failed": failed_sources + } + +def list_employee_agents( + project_id: str, + location: str = "global", + engine_id: str = "enterprise-search-17416389_1741638989378", + basic: bool = False +) -> list: + """Lists employee-made low-code agents.""" + session = get_session() + host = get_endpoint_host(location) + base_url = f"https://{host}/v1alpha" + parent = f"projects/{project_id}/locations/{location}/collections/default_collection/engines/{engine_id}/assistants/default_assistant" + url = f"{base_url}/{parent}/agents" + + logging.info(f"DEBUG: list_employee_agents called for {parent}") + resp = session.get(url) + resp.raise_for_status() + data = resp.json() + agents = data.get("agents", []) + + employee_agents = [] + for agent in agents: + displayName = (agent.get("displayName") or "").replace("\r\n", " ").replace("\n", " ") + name = agent.get("name") + description = (agent.get("description") or "").replace("\r\n", " ").replace("\n", " ") + + root_instructions = "No instructions found." + root_tools = [] + root_knowledge = [] + sub_agents = [] + + if "lowCodeAgentDefinition" in agent: + definition = agent.get("lowCodeAgentDefinition", {}) + nodes = definition.get("nodes", []) + root_id = definition.get("rootAgentId") + + for node in nodes: + node_id = node.get("id") + llm_node = node.get("llmAgentNode", {}) + node_instruction = llm_node.get("instruction", "No instructions found.") + + # Extract tools for this node + node_tools = [] + for t in llm_node.get("selectedTools", {}).get("tool", []): + node_tools.append(t.get("name", "Unknown Tool")) + for spec in llm_node.get("dataStoreSpecs", {}).get("specs", []): + ds = spec.get("dataStore", "") + if ds: + ds_name = ds.split("/")[-1] + node_tools.append(f"DataStore: {ds_name}") + node_knowledge = [] + for k_field in ["groundingSources", "userContents", "files", "knowledge"]: + for item in llm_node.get(k_field, []): + title = item.get("displayName") or item.get("googleDriveContent", {}).get("documentId") + if title and title not in node_knowledge: + node_knowledge.append(title) + for match in re.findall(r'\[([^\]]+)\]\(https://drive\.google\.com/[^\)]+\)', node_instruction): + if match and match not in node_knowledge: + node_knowledge.append(match) + + if node_id == root_id: + root_instructions = node_instruction + for nt in node_tools: + if nt not in root_tools: + root_tools.append(nt) + for nk in node_knowledge: + if nk not in root_knowledge: + root_knowledge.append(nk) + else: + sub_agents.append({ + "displayName": node.get("displayName", "Sub-Agent"), + "description": llm_node.get("description", ""), + "model": llm_node.get("model", "Unknown Model"), + "instructions": node_instruction, + "tools": node_tools, + "knowledge": node_knowledge + }) + elif "skillAgentDefinition" in agent: + definition = agent.get("skillAgentDefinition", {}) + root_instructions = definition.get("instruction", "No instructions found.") + + if basic: + employee_agents.append({ + "displayName": displayName, + "name": name, + "description": description, + "connectors_and_tools": root_tools, + }) + else: + employee_agents.append({ + "displayName": displayName, + "name": name, + "description": description, + "instructions": root_instructions, + "connectors_and_tools": root_tools, + "knowledge": root_knowledge, + "sub_agents": sub_agents, + "raw_agent": agent + }) + return employee_agents + +def extract_connectors(agent_def: dict) -> list: + """Extracts unique connector and datastore names from an agent definition.""" + connectors = set() + nodes = agent_def.get("nodes", []) + for node in nodes: + llm_node = node.get("llmAgentNode", {}) + + selected_tools = llm_node.get("selectedTools", {}) + tools_list = selected_tools.get("tool", []) + for t in tools_list: + name = t.get("name") + if name: + connectors.add(name) + + for spec in llm_node.get("dataStoreSpecs", {}).get("specs", []): + ds = spec.get("dataStore", "") + if ds: + ds_name = ds.split("/")[-1] + connectors.add(ds_name) + + return list(connectors) + +def list_target_connectors(session, base_url, target_parent) -> list: + """Lists connectors used by agents in the target environment.""" + target_url = f"{base_url}/{target_parent}/agents" + try: + logging.info(f"Fetching target agents from {target_url}") + resp = session.get(target_url) + resp.raise_for_status() + agents = resp.json().get("agents", []) + + target_connectors = set() + for agent in agents: + if "lowCodeAgentDefinition" in agent: + definition = agent.get("lowCodeAgentDefinition", {}) + connectors = extract_connectors(definition) + for c in connectors: + target_connectors.add(c) + return list(target_connectors) + except Exception as e: + logging.error(f"Failed to list target connectors: {e}") + return [] + +def extract_agent_datastores( + source_agent_name: str, + source_project_id: str, + source_location: str = "global", + source_engine_id: str = "enterprise-search-17416389_1741638989378" +) -> dict: + """Extracts and lists the names of datastores used by an agent and its subagents.""" + session = get_session() + host = get_endpoint_host(source_location) + base_url = f"https://{host}/v1alpha" + source_parent = f"projects/{source_project_id}/locations/{source_location}/collections/default_collection/engines/{source_engine_id}/assistants/default_assistant" + source_url = f"{base_url}/{source_parent}/agents" + + resp = session.get(source_url) + resp.raise_for_status() + agents = resp.json().get("agents", []) + + agent_to_check = None + for agent in agents: + if agent.get("displayName") == source_agent_name and "lowCodeAgentDefinition" in agent: + agent_to_check = agent + break + + if not agent_to_check: + raise ValueError(f"Source agent '{source_agent_name}' not found or is not a low-code agent.") + + definition = agent_to_check.get("lowCodeAgentDefinition", {}) + nodes = definition.get("nodes", []) + + datastore_report = {} + for node in nodes: + node_name = node.get("displayName", "Unknown Node") + llm_node = node.get("llmAgentNode", {}) + datastores = [] + for spec in llm_node.get("dataStoreSpecs", {}).get("specs", []): + ds = spec.get("dataStore", "") + if ds: + ds_name = ds.split("/")[-1] + datastores.append(ds_name) + if datastores: + datastore_report[node_name] = datastores + + return { + "agent_name": source_agent_name, + "datastores": datastore_report + } + +def clean_connector_name(name: str) -> str: + """Strips common suffixes like May29, dates, punctuation for robust fuzzy matching.""" + clean = re.sub(r'(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\d*', '', name, flags=re.IGNORECASE) + return re.sub(r'[^a-zA-Z0-9]', '', clean).lower() + +def lookup_and_map_connectors( + source_agent_name: str, + target_project_id: str, + target_location: str, + target_engine_id: str, + source_project_id: str, + source_location: str = "global", + source_engine_id: str = "enterprise-search-17416389_1741638989378" +) -> dict: + """Intelligently matches source agent connectors against target environment connectors.""" + session = get_session() + source_host = get_endpoint_host(source_location) + source_base_url = f"https://{source_host}/v1alpha" + source_parent = f"projects/{source_project_id}/locations/{source_location}/collections/default_collection/engines/{source_engine_id}/assistants/default_assistant" + source_url = f"{source_base_url}/{source_parent}/agents" + + resp = session.get(source_url) + if resp.status_code == 200: + agents = resp.json().get("agents", []) + else: + agents = [] + + src_connectors = set() + for ag in agents: + if source_agent_name.upper() == "ALL" or ag.get("displayName", "").lower() == source_agent_name.lower(): + for c in extract_connectors(ag.get("lowCodeAgentDefinition", {})): + src_connectors.add(c) + src_connectors = list(src_connectors) + + target_host = get_endpoint_host(target_location) + target_base_url = f"https://{target_host}/v1alpha" + target_parent = f"projects/{target_project_id}/locations/{target_location}/collections/default_collection/engines/{target_engine_id}/assistants/default_assistant" + target_connectors = list_target_connectors(session, target_base_url, target_parent) + try: + source_ds_objs = list_datastores(source_project_id, source_location) + except Exception: + source_ds_objs = [] + try: + target_ds_objs = list_datastores(target_project_id, target_location) + except Exception: + target_ds_objs = [] + + canonical_tools = ["googleSearch", "urlContext", "geGmail", "snowflakeMcp"] + verified_target_ids = [ds["id"] for ds in target_ds_objs] + all_targets = canonical_tools + verified_target_ids + + mapping = {} + missing = [] + for sc in src_connectors: + sc_clean = clean_connector_name(sc) + matched = None + for s_ds in source_ds_objs: + if s_ds["id"] == sc or s_ds["displayName"].lower() == sc.lower(): + for t_ds in target_ds_objs: + if t_ds["displayName"].lower() == s_ds["displayName"].lower(): + if s_ds["displayName"].lower() == "mcp_data": + s_pref = s_ds["id"].split("_")[0].split("-")[0].lower() + t_pref = t_ds["id"].split("_")[0].split("-")[0].lower() + if s_pref != t_pref: + continue + matched = t_ds["id"] + break + break + + if not matched: + for tc in all_targets: + if clean_connector_name(tc) == sc_clean: + matched = tc + break + + if matched: + mapping[sc] = matched + else: + mapping[sc] = "❌ Missing (Not Ingested in Target)" + missing.append(sc) + + return { + "source_agent": source_agent_name, + "source_connectors": src_connectors, + "target_available": all_targets, + "proposed_mapping": mapping, + "missing_connectors": missing + } + +def migrate_employee_agent( + source_agent_name: str, + target_project_id: str, + target_location: str, + target_engine_id: str, + source_project_id: str, + source_location: str = "global", + source_engine_id: str = "enterprise-search-17416389_1741638989378", + force: bool = False, + backup_bucket: str = "", + connector_mapping: str = "" +) -> dict: + """Migrates an employee-made low-code agent to a target environment.""" + session = get_session() + + target_host = get_endpoint_host(target_location) + target_base_url = f"https://{target_host}/v1alpha" + target_parent = f"projects/{target_project_id}/locations/{target_location}/collections/default_collection/engines/{target_engine_id}/assistants/default_assistant" + target_url = f"{target_base_url}/{target_parent}/agents" + + # 1. Fetch the source agent definition + source_host = get_endpoint_host(source_location) + source_base_url = f"https://{source_host}/v1alpha" + source_parent = f"projects/{source_project_id}/locations/{source_location}/collections/default_collection/engines/{source_engine_id}/assistants/default_assistant" + source_url = f"{source_base_url}/{source_parent}/agents" + + logging.info(f"Fetching source agent from {source_url}") + resp = session.get(source_url) + resp.raise_for_status() + agents = resp.json().get("agents", []) + + agent_to_migrate = None + for agent in agents: + if agent.get("displayName") == source_agent_name and "lowCodeAgentDefinition" in agent: + agent_to_migrate = agent + break + + if not agent_to_migrate: + raise ValueError(f"Source agent '{source_agent_name}' not found or is not a low-code agent.") + + # 1.4 Validation and Reporting + src_connectors = extract_connectors(agent_to_migrate.get("lowCodeAgentDefinition", {})) + target_connectors = list_target_connectors(session, target_base_url, target_parent) + + missing_connectors = [c for c in src_connectors if c not in target_connectors] + + report = { + "source_connectors": src_connectors, + "target_connectors_used": target_connectors, + "missing_connectors": missing_connectors + } + + logging.info(f"Connector Validation Report: {json.dumps(report)}") + + if missing_connectors and not force: + return { + "success": False, + "warning": "Missing dependencies in target environment.", + "report": report, + "message": f"The following dependencies are missing in the target environment: {missing_connectors}." + } + + # 1.5 Upload to GCS (Backup) implicitly + bucket_name = backup_bucket if backup_bucket else os.environ.get("GCS_BUCKET_NAME") + if bucket_name: + try: + timestamp = int(time.time()) + object_name = f"exports/{source_agent_name}_{timestamp}.json" + encoded_obj = urllib.parse.quote(object_name, safe="") + upload_url = f"https://storage.googleapis.com/upload/storage/v1/b/{bucket_name}/o?uploadType=media&name={encoded_obj}" + up_resp = session.post( + upload_url, + data=json.dumps(agent_to_migrate, indent=2), + headers={"Content-Type": "application/json"} + ) + up_resp.raise_for_status() + logging.info(f"Implicitly backed up agent '{source_agent_name}' to gs://{bucket_name}/{object_name}") + except Exception as e: + logging.error(f"Failed to implicitly backup agent to GCS: {e}") + + # 2. Create the payload for the target environment + definition = agent_to_migrate.get("lowCodeAgentDefinition", {}) + if "session" in definition: + del definition["session"] + + mapping_dict = {} + if connector_mapping: + try: + mapping_dict = json.loads(connector_mapping) + except Exception: + for pair in connector_mapping.split(","): + if ":" in pair: + k, v = pair.split(":", 1) + mapping_dict[k.strip()] = v.strip() + + # Apply tool mapping and ensure googleSearch + all_nodes = definition.get("nodes", []) + definition.get("deployedNodes", []) + for node in all_nodes: + llm_node = node.get("llmAgentNode", {}) + if "selectedTools" not in llm_node: + llm_node["selectedTools"] = {"tool": []} + selected_tools = llm_node["selectedTools"] + tools_list = selected_tools.get("tool", []) + + new_tools = [] + has_google_search = False + for t in tools_list: + t_name = t.get("name", "") + if t_name in mapping_dict: + t_name = mapping_dict[t_name] + elif clean_connector_name(t_name) == clean_connector_name("snowflakeMcp"): + t_name = mapping_dict.get("Snowflake Mcp May29", "snowflakeMcp") + + if t_name == "googleSearch": + has_google_search = True + if t_name: + new_tools.append({"name": t_name}) + + if not has_google_search: + new_tools.append({"name": "googleSearch"}) + selected_tools["tool"] = new_tools + + new_specs = [] + for spec in llm_node.get("dataStoreSpecs", {}).get("specs", []): + ds = spec.get("dataStore", "") + if ds: + ds_id = ds.split("/")[-1] + if ds_id in mapping_dict: + ds_id = mapping_dict[ds_id] + new_ds = f"projects/{target_project_id}/locations/{target_location}/collections/default_collection/dataStores/{ds_id}" + new_specs.append({"dataStore": new_ds}) + if new_specs: + llm_node["dataStoreSpecs"] = {"specs": new_specs} + if any("snowflake" in s.get("dataStore", "").lower() for s in new_specs): + sf_conn = mapping_dict.get("Snowflake Mcp May29", "custom_mcp") + if not any(t.get("name") == sf_conn for t in new_tools): + new_tools.append({"name": sf_conn}) + selected_tools["tool"] = new_tools + if any("drive" in s.get("dataStore", "").lower() for s in new_specs): + dr_conn = mapping_dict.get("ge-drive-all", "Drive") + if not any(t.get("name") == dr_conn for t in new_tools): + new_tools.append({"name": dr_conn}) + selected_tools["tool"] = new_tools + + payload_str = json.dumps({ + "displayName": agent_to_migrate.get("displayName"), + "description": agent_to_migrate.get("description", ""), + "lowCodeAgentDefinition": definition + }) + payload_str = payload_str.replace(f"projects/{source_project_id}", f"projects/{target_project_id}") + payload_str = payload_str.replace(source_engine_id, target_engine_id) + payload = json.loads(payload_str) + + logging.info(f"Creating new agent at target {target_url}") + create_resp = session.post(target_url, json=payload) + create_resp.raise_for_status() + + created_agent = create_resp.json() + tgt_extracted = extract_connectors(created_agent.get("lowCodeAgentDefinition", {})) + message = f"Successfully migrated agent '{source_agent_name}' to target environment.\nConnectors in Source Agent: {src_connectors}\nConnectors in Target Agent: {tgt_extracted}" + truly_missing = [c for c in missing_connectors if c not in mapping_dict] + if truly_missing: + message += f"\nWARNING: Missing connectors were ignored: {truly_missing}" + + return { + "success": True, + "message": message, + "target_agent": created_agent + } + + + +def export_agent_to_gcs( + source_agent_name: str, + object_name: str, + bucket_name: str = "", + source_project_id: str = "", + source_location: str = "global", + source_engine_id: str = "enterprise-search-17416389_1741638989378" +) -> dict: + """Exports an employee-made low-code agent definition to a GCS bucket.""" + if not bucket_name: + bucket_name = os.environ.get("GCS_BUCKET_NAME") + if not bucket_name: + raise ValueError("bucket_name not provided and GCS_BUCKET_NAME not found in environment.") + + session = get_session() + host = get_endpoint_host(source_location) + base_url = f"https://{host}/v1alpha" + source_parent = f"projects/{source_project_id}/locations/{source_location}/collections/default_collection/engines/{source_engine_id}/assistants/default_assistant" + source_url = f"{base_url}/{source_parent}/agents" + + resp = session.get(source_url) + resp.raise_for_status() + agents = resp.json().get("agents", []) + + agent_to_export = None + for agent in agents: + if agent.get("displayName") == source_agent_name and "lowCodeAgentDefinition" in agent: + agent_to_export = agent + break + + if not agent_to_export: + raise ValueError(f"Source agent '{source_agent_name}' not found or is not a low-code agent.") + + encoded_obj = urllib.parse.quote(object_name, safe="") + upload_url = f"https://storage.googleapis.com/upload/storage/v1/b/{bucket_name}/o?uploadType=media&name={encoded_obj}" + up_resp = session.post( + upload_url, + data=json.dumps(agent_to_export, indent=2), + headers={"Content-Type": "application/json"} + ) + up_resp.raise_for_status() + + return { + "success": True, + "message": f"Successfully exported agent '{source_agent_name}' to gs://{bucket_name}/{object_name}" + } + +def import_agent_from_gcs( + object_name: str, + target_project_id: str, + target_location: str, + target_engine_id: str, + bucket_name: str = "", + connector_mapping: str = "" +) -> dict: + """Imports an agent definition from GCS and creates it in a target environment.""" + if not bucket_name: + bucket_name = os.environ.get("GCS_BUCKET_NAME") + if not bucket_name: + raise ValueError("bucket_name not provided and GCS_BUCKET_NAME not found in environment.") + + session = get_session() + host = get_endpoint_host(target_location) + base_url = f"https://{host}/v1alpha" + + encoded_object = urllib.parse.quote(object_name, safe="") + gcs_url = f"https://storage.googleapis.com/storage/v1/b/{bucket_name}/o/{encoded_object}?alt=media" + dl_resp = session.get(gcs_url) + dl_resp.raise_for_status() + agent_data = dl_resp.json() + + mapping_dict = {} + if connector_mapping: + try: + mapping_dict = json.loads(connector_mapping) + except Exception: + for pair in connector_mapping.split(","): + if ":" in pair: + k, v = pair.split(":", 1) + mapping_dict[k.strip()] = v.strip() + + definition = agent_data.get("lowCodeAgentDefinition", {}) + if "session" in definition: + del definition["session"] + + nodes = definition.get("nodes", []) + for node in nodes: + llm_node = node.get("llmAgentNode", {}) + if "selectedTools" not in llm_node: + llm_node["selectedTools"] = {"tool": []} + selected_tools = llm_node["selectedTools"] + if "tool" not in selected_tools: + selected_tools["tool"] = [] + + tools_list = selected_tools["tool"] + has_google_search = False + for t in tools_list: + if t.get("name") == "googleSearch": + has_google_search = True + break + if not has_google_search: + tools_list.append({"name": "googleSearch"}) + + new_specs = llm_node.get("dataStoreSpecs", {}).get("specs", []) + if any("snowflake" in s.get("dataStore", "").lower() for s in new_specs): + sf_conn = mapping_dict.get("Snowflake Mcp May29", "custom_mcp") + if not any(t.get("name") == sf_conn for t in tools_list): + tools_list.append({"name": sf_conn}) + if any("drive" in s.get("dataStore", "").lower() for s in new_specs): + dr_conn = mapping_dict.get("ge-drive-all", "Drive") + if not any(t.get("name") == dr_conn for t in tools_list): + tools_list.append({"name": dr_conn}) + + if "selectedTools" in llm_node: + for t in llm_node["selectedTools"].get("tool", []): + tool_name = t.get("name") + if tool_name in mapping_dict: + t["name"] = mapping_dict[tool_name] + + if "dataStoreSpecs" in llm_node: + for s in llm_node["dataStoreSpecs"].get("specs", []): + ds = s.get("dataStore", "") + for src_id, tgt_id in mapping_dict.items(): + if src_id in ds: + s["dataStore"] = ds.replace(src_id, tgt_id) + ds = s["dataStore"] + + source_name = agent_data.get("name", "") + match = re.search(r"projects/([^/]+)/locations/([^/]+)/collections/default_collection/engines/([^/]+)", source_name) + + payload_str = json.dumps({ + "displayName": agent_data.get("displayName"), + "description": agent_data.get("description", ""), + "lowCodeAgentDefinition": definition + }) + + if match: + source_project_id = match.group(1) + source_engine_id = match.group(3) + payload_str = payload_str.replace(f"projects/{source_project_id}", f"projects/{target_project_id}") + payload_str = payload_str.replace(source_engine_id, target_engine_id) + + payload = json.loads(payload_str) + target_parent = f"projects/{target_project_id}/locations/{target_location}/collections/default_collection/engines/{target_engine_id}/assistants/default_assistant" + target_url = f"{base_url}/{target_parent}/agents" + + logging.info(f"Creating new agent from GCS at target {target_url}") + create_resp = session.post(target_url, json=payload) + create_resp.raise_for_status() + + return { + "success": True, + "message": f"Successfully imported agent from gs://{bucket_name}/{object_name} to target environment.", + "target_agent": create_resp.json() + } + +def list_datastores(project_id: str, location: str = "global", collection: str = "default_collection") -> list: + """Lists all available datastores and their IDs in a target project/location.""" + session = get_session() + host = get_endpoint_host(location) + base_url = f"https://{host}/v1alpha" + url = f"{base_url}/projects/{project_id}/locations/{location}/collections/{collection}/dataStores" + + logging.info(f"Fetching datastores from {url}") + resp = session.get(url) + resp.raise_for_status() + + datastores = resp.json().get("dataStores", []) + results = [] + for ds in datastores: + ds_name = ds.get("name", "") + ds_id = ds_name.split("/")[-1] if ds_name else "Unknown ID" + results.append({ + "id": ds_id, + "displayName": ds.get("displayName", ""), + "name": ds_name + }) + return results + +def export_notebook_to_gcs( + notebook_id_or_title: str, + object_name: str, + bucket_name: str = "", + source_project_number: str = "", + source_location: str = "global" +) -> dict: + """Exports a notebook definition (including metadata and sources) to GCS.""" + if not bucket_name: + bucket_name = os.environ.get("GCS_BUCKET_NAME") + if not bucket_name: + raise ValueError("bucket_name not provided and GCS_BUCKET_NAME not found in environment.") + + notebooks = list_notebooks(source_project_number, source_location) + source_notebook = None + for nb in notebooks: + nb_id = nb.get("name", "").split("/")[-1] + nb_title = nb.get("title", "") + if nb_id == notebook_id_or_title or nb_title == notebook_id_or_title: + source_notebook = nb + break + + if not source_notebook: + raise ValueError(f"Source notebook '{notebook_id_or_title}' not found.") + + source_nb_id = source_notebook.get("name", "").split("/")[-1] + source_nb_title = source_notebook.get("title", "Untitled Notebook") + + sources = list_sources_and_types(source_nb_id, source_project_number, source_location) + + exported_sources = [] + for src in sources: + title = src.get("title", "Untitled Source") + raw_data = src.get("raw_data", {}) + payload = extract_notebook_source_payload(raw_data) + + exported_sources.append({ + "title": title, + "userContent": payload + }) + + notebook_def = { + "title": source_nb_title, + "sources": exported_sources + } + + session = get_session() + encoded_obj = urllib.parse.quote(object_name, safe="") + upload_url = f"https://storage.googleapis.com/upload/storage/v1/b/{bucket_name}/o?uploadType=media&name={encoded_obj}" + up_resp = session.post( + upload_url, + data=json.dumps(notebook_def, indent=2), + headers={"Content-Type": "application/json"} + ) + up_resp.raise_for_status() + + return { + "success": True, + "message": f"Successfully exported notebook '{source_nb_title}' to gs://{bucket_name}/{object_name}", + "notebook_def": notebook_def + } + +def import_notebook_from_gcs( + object_name: str, + target_project_number: str, + target_location: str, + bucket_name: str = "" +) -> dict: + """Imports a notebook definition from GCS and creates it in the target project.""" + if not bucket_name: + bucket_name = os.environ.get("GCS_BUCKET_NAME") + if not bucket_name: + raise ValueError("bucket_name not provided and GCS_BUCKET_NAME not found in environment.") + + session = get_session() + encoded_object = urllib.parse.quote(object_name, safe="") + gcs_url = f"https://storage.googleapis.com/storage/v1/b/{bucket_name}/o/{encoded_object}?alt=media" + dl_resp = session.get(gcs_url) + dl_resp.raise_for_status() + nb_data = dl_resp.json() + + title = nb_data.get("title", "Untitled Notebook") + sources = nb_data.get("sources", []) + + target_nb = create_notebook(target_project_number, target_location, title) + target_nb_id = target_nb.get("name", "").split("/")[-1] + logging.info(f"Created target notebook '{title}' from GCS with ID: {target_nb_id}") + + migrated_sources = [] + failed_sources = [] + + for src in sources: + src_title = src.get("title", "Untitled Source") + payload = src.get("userContent", {}) + + logging.info(f"Adding source '{src_title}' to target notebook '{target_nb_id}'") + try: + add_resp = add_source_to_notebook(target_project_number, target_location, target_nb_id, payload) + migrated_sources.append({ + "title": src_title, + "status": "success", + "response": add_resp + }) + except Exception as e: + logging.error(f"Failed to add source '{src_title}': {e}") + failed_sources.append({ + "title": src_title, + "status": "failed", + "error": str(e) + }) + + return { + "success": len(failed_sources) == 0, + "target_notebook_id": target_nb_id, + "target_notebook_title": title, + "migrated_sources_count": len(migrated_sources), + "failed_sources_count": len(failed_sources), + "migrated": migrated_sources, + "failed": failed_sources + } diff --git a/search/gemini-enterprise/ge-migration-agent/ge-migration-agent-architecture.png b/search/gemini-enterprise/ge-migration-agent/ge-migration-agent-architecture.png new file mode 100644 index 0000000..7c1aa7e Binary files /dev/null and b/search/gemini-enterprise/ge-migration-agent/ge-migration-agent-architecture.png differ diff --git a/search/gemini-enterprise/ge-migration-agent/ge_migration_agent/.env.sample b/search/gemini-enterprise/ge-migration-agent/ge_migration_agent/.env.sample new file mode 100644 index 0000000..230e326 --- /dev/null +++ b/search/gemini-enterprise/ge-migration-agent/ge_migration_agent/.env.sample @@ -0,0 +1,5 @@ +GEMINI_API_MODEL="gemini-2.5-flash" +GOOGLE_GENAI_USE_VERTEXAI="true" +GOOGLE_CLOUD_PROJECT="YOUR_PROJECT_NUMBER" +GOOGLE_CLOUD_LOCATION="YOUR_LOCATION" +GCS_BUCKET_NAME="YOUR_BUCKET_NAME" diff --git a/search/gemini-enterprise/ge-migration-agent/ge_migration_agent/__init__.py b/search/gemini-enterprise/ge-migration-agent/ge_migration_agent/__init__.py new file mode 100644 index 0000000..47b77f8 --- /dev/null +++ b/search/gemini-enterprise/ge-migration-agent/ge_migration_agent/__init__.py @@ -0,0 +1 @@ +# Notebook Agent package diff --git a/search/gemini-enterprise/ge-migration-agent/ge_migration_agent/a2ui_schema.py b/search/gemini-enterprise/ge-migration-agent/ge_migration_agent/a2ui_schema.py new file mode 100644 index 0000000..69161c9 --- /dev/null +++ b/search/gemini-enterprise/ge-migration-agent/ge_migration_agent/a2ui_schema.py @@ -0,0 +1,14 @@ +A2UI_SCHEMA = """ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "contents": { + "type": "array", + "items": { + "type": "object" + } + } + } +} +""" diff --git a/search/gemini-enterprise/ge-migration-agent/ge_migration_agent/agent.py b/search/gemini-enterprise/ge-migration-agent/ge_migration_agent/agent.py new file mode 100644 index 0000000..ab8a708 --- /dev/null +++ b/search/gemini-enterprise/ge-migration-agent/ge_migration_agent/agent.py @@ -0,0 +1,354 @@ +import json +import os +import logging +import subprocess +import shutil +import sys +from dotenv import load_dotenv +env_path = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), ".env") +load_dotenv(env_path, override=True) +from google.adk.agents.llm_agent import Agent +from google.adk.tools.tool_context import ToolContext +from google.adk.tools.skill_toolset import SkillToolset +sys.path.append(os.path.join(os.path.dirname(__file__), "..")) +from skills import migration_skill + +PROJECT_NUMBER = os.environ.get("GOOGLE_CLOUD_PROJECT") or os.environ.get("GEMINI_API_PROJECT") +LOCATION = os.environ.get("GOOGLE_CLOUD_LOCATION") or os.environ.get("GEMINI_API_LOCATION", "global") +MODEL_NAME = os.environ.get("GEMINI_API_MODEL") or os.environ.get("GEMINI_MODEL", "gemini-2.5-flash") +if "/" not in MODEL_NAME: + MODEL_PATH = f"projects/{PROJECT_NUMBER}/locations/{LOCATION}/publishers/google/models/{MODEL_NAME}" +else: + MODEL_PATH = MODEL_NAME + +def run_cli(args: list[str]) -> str: + script_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + cli_path = os.path.join(script_dir, "migrate.py") + + if shutil.which("uv"): + cmd = ["uv", "run", cli_path, "--json"] + args + else: + cmd = [sys.executable, cli_path, "--json"] + args + + try: + logging.info(f"Running CLI: {' '.join(cmd)}") + res = subprocess.run(cmd, capture_output=True, text=True, check=True) + return res.stdout.strip() + except subprocess.CalledProcessError as e: + logging.error(f"CLI failed with exit code {e.returncode}: {e.stderr}") + return json.dumps({"error": f"CLI command failed: {e.stderr.strip() or e.stdout.strip()}"}) + except Exception as e: + logging.error(f"Execution error: {e}") + return json.dumps({"error": str(e)}) + + +def list_notebooks(tool_context: ToolContext) -> str: + """Call this tool to get a list of recently viewed notebooks.""" + return run_cli(["list-notebooks"]) + + +def list_sources_and_types(tool_context: ToolContext, notebook_id: str) -> str: + """Call this tool to list sources and their types for a specific notebook. + + Args: + notebook_id: The ID of the notebook. + """ + return run_cli(["list-sources", notebook_id]) + + +def create_notebook(tool_context: ToolContext, title: str) -> str: + """Call this tool to create a new notebook. + + Args: + title: The title of the notebook to create. + """ + return run_cli(["create-notebook", title]) + + +def add_source_to_notebook(tool_context: ToolContext, notebook_id: str, source_content: str) -> str: + """Call this tool to add a source to a notebook. + + Args: + notebook_id: The ID of the notebook to add the source to. + source_content: The JSON string of the userContent to add. + """ + return run_cli(["add-source-to-notebook", notebook_id, source_content]) + + +def list_employee_agents(tool_context: ToolContext) -> str: + """Call this tool to list all employee-made low-code agents.""" + return run_cli(["list-agents"]) + + +def get_agent_details(tool_context: ToolContext, agent_name: str) -> str: + """Call this tool to retrieve the configuration details (instructions, model, connectors, and sub-agents) of a specific employee agent by name. + + Args: + agent_name: The exact display name of the agent to retrieve details for. + """ + return run_cli(["get-agent-details", agent_name]) + + +def extract_agent_datastores(tool_context: ToolContext, source_agent_name: str) -> str: + """Call this tool to extract and list the names of datastores used by an agent and its subagents. + + Args: + source_agent_name: The exact display name of the source agent. + """ + return run_cli(["extract-datastores", source_agent_name]) + + +def migrate_agent( + tool_context: ToolContext, + source_agent_name: str, + force: bool = False, + connector_mapping: str = "" +) -> str: + """Call this tool to migrate (create) an employee-made low-code agent to a target environment. + + Args: + source_agent_name: The exact display name of the source agent to migrate. + force: Set to True to proceed with migration even if some dependencies are missing. + connector_mapping: Optional JSON dict or mapping string. + """ + args = ["migrate-agent", source_agent_name] + if force: + args.append("--force") + if connector_mapping: + args.extend(["--connector-mapping", connector_mapping]) + return run_cli(args) + + +def migrate_notebook( + tool_context: ToolContext, + notebook_name_or_id: str, + backup_bucket: str = "" +) -> str: + """Call this tool to migrate an entire NotebookLM notebook and all its sources to the target environment. + This will also implicitly upload a backup of the notebook definition to a GCS bucket first. + + Args: + notebook_name_or_id: The exact title or ID of the source notebook to migrate. + backup_bucket: Optional GCS bucket name for the backup. + """ + args = ["migrate-notebook", notebook_name_or_id] + if backup_bucket: + args.extend(["--backup-bucket", backup_bucket]) + return run_cli(args) + + +def lookup_and_map_connectors(tool_context: ToolContext, source_agent_name: str) -> str: + """Call this tool before migrating an agent to look up source connectors, map them intelligently to target connectors, and show the proposed mapping to the user for approval. + + Args: + source_agent_name: The exact display name of the source agent. + """ + return run_cli(["lookup-map-connectors", source_agent_name]) + + +def export_agent_to_gcs( + tool_context: ToolContext, + source_agent_name: str, + object_name: str, + bucket_name: str = "", + source_project_id: str = PROJECT_NUMBER, + source_location: str = "global", + source_engine_id: str = "enterprise-search-17416389_1741638989378" +) -> str: + """Call this tool to export an employee-made low-code agent definition to a GCS bucket. + + Args: + source_agent_name: The exact display name of the source agent to export. + object_name: The name of the object (file path) in the bucket. + bucket_name: The name of the GCS bucket to save the definition to. If not provided, reads from GCS_BUCKET_NAME env var. + source_project_id: The Google Cloud project number of the source environment. + source_location: The geographic location of the source environment. + source_engine_id: The Discovery Engine engine ID of the source environment. + """ + args = ["export-agent-gcs", source_agent_name, object_name, "--engine-id", source_engine_id] + if bucket_name: + args.extend(["--bucket", bucket_name]) + if source_project_id: + args.extend(["--project", source_project_id]) + if source_location: + args.extend(["--location", source_location]) + return run_cli(args) + + +def import_agent_from_gcs( + tool_context: ToolContext, + object_name: str, + target_project_id: str, + target_location: str, + target_engine_id: str, + bucket_name: str = "", +) -> str: + """Call this tool to import an agent definition from GCS and create it in a target environment. + + Args: + object_name: The name of the object (file path) in the bucket. + target_project_id: The Google Cloud project number of the target environment. + target_location: The geographic location of the target environment. + target_engine_id: The Discovery Engine engine ID of the target environment. + bucket_name: The name of the GCS bucket containing the definition. If not provided, reads from GCS_BUCKET_NAME env var. + """ + args = [ + "import-agent-gcs", object_name, + "--target-project", target_project_id, + "--target-location", target_location, + "--target-engine", target_engine_id + ] + if bucket_name: + args.extend(["--bucket", bucket_name]) + return run_cli(args) + + +def export_notebook_to_gcs( + tool_context: ToolContext, + notebook_name: str, + object_name: str, + bucket_name: str = "", + source_project_id: str = PROJECT_NUMBER, + source_location: str = "global", +) -> str: + """Call this tool to export a NotebookLM notebook definition (including metadata and sources) to a GCS bucket. + + Args: + notebook_name: The exact title or ID of the source notebook to export. + object_name: The name of the object (file path) in the bucket. + bucket_name: The name of the GCS bucket to save the definition to. If not provided, reads from GCS_BUCKET_NAME env var. + source_project_id: The Google Cloud project number of the source environment. + source_location: The geographic location of the source environment. + """ + args = ["export-notebook-gcs", notebook_name, object_name] + if bucket_name: + args.extend(["--bucket", bucket_name]) + if source_project_id: + args.extend(["--project", source_project_id]) + if source_location: + args.extend(["--location", source_location]) + return run_cli(args) + + +def import_notebook_from_gcs( + tool_context: ToolContext, + object_name: str, + target_project_id: str, + target_location: str, + bucket_name: str = "", +) -> str: + """Call this tool to import a notebook definition from GCS and create it in a target environment. + + Args: + object_name: The name of the object (file path) in the bucket containing the notebook definition JSON. + target_project_id: The Google Cloud project number of the target environment. + target_location: The geographic location of the target environment. + bucket_name: The name of the GCS bucket containing the definition. If not provided, reads from GCS_BUCKET_NAME env var. + """ + args = [ + "import-notebook-gcs", object_name, + "--target-project", target_project_id, + "--target-location", target_location, + ] + if bucket_name: + args.extend(["--bucket", bucket_name]) + return run_cli(args) + + +def list_datastores( + tool_context: ToolContext, + project_id: str, + location: str = "global" +) -> str: + """Call this tool to connect to a target location and get a list of all available datastores and their datastore IDs. + + Args: + project_id: The target Google Cloud project number. + location: The target location (e.g. global). + """ + return run_cli(["list-datastores", "--project", project_id, "--location", location]) + + +def get_migration_config(tool_context: ToolContext) -> str: + """Call this tool FIRST upon greeting to retrieve the verified canonical Source and Target environment details and DataStore/Connector mappings directly from SKILL.md / migration_skill.json.""" + import sys + sys.path.append(os.path.join(os.path.dirname(__file__), "..")) + from skills import migration_skill + skill = migration_skill.MigrationConfigSkill() + return json.dumps({ + "environments": { + "source": skill.get_source_env(), + "target": skill.get_target_env() + }, + "datastores_mapping": skill.get_datastores_mapping(), + "connectors_mapping": skill.get_connectors_mapping() + }, indent=2) + + +AGENT_INSTRUCTION = """ +You are the Gemini Enterprise App Migration Agent. Your job is to migrate notebooks from one Gemini app to another, and to help users list employee-made agents. + +CRITICAL RULES: +1. **Initial Greeting & Configuration Verification**: When a user asks for help or begins the session, FIRST show a clearly formatted bulleted list of tasks/activities you can perform (e.g., Migrate Notebooks, List Employee Agents, Migrate Employee Low-Code Agents). Then call `get_migration_config` to read the real verified migration configuration from `SKILL.md` and show both the active pre-loaded Source and Target environment details (Project Number, Project ID, Region, Engine ID) AND verified DataStore Mappings (`datastores_mapping`) directly on screen. Do NOT display Connector Mappings on screen! Never ask the user to input Project Numbers or Engine IDs! +2. **Mapping Verification**: Before migrating, verify that your active DataStore mappings match `SKILL.md`. +3. **Environment Memory**: Rely entirely on `SKILL.md` configuration defaults for migrations without prompting again. +4. **Name Verification**: When requested to perform an action on a specific agent or notebook, you MUST first verify its exact display name or title. If there is any shorthand, abbreviation, or potential name mismatch, call `list_employee_agents` (for agents) or `list_notebooks` (for notebooks) to find the exact match from the source environment. Use ONLY the exact retrieved display name or title in all subsequent tool calls. Do NOT call any other search or question tools (such as 'search_and_answer'). + +Workflow: +1. List all notebook apps from the source app (using `list_notebooks`). +2. Ask the user which notebook they want to migrate, or if they want to migrate all of them. +3. Ask the user for the target Gemini Enterprise app details (Project Number and Location). +4. Migrate each selected notebook using `migrate_notebook`. This will automatically save a backup to the GCS bucket first before performing target creation. +5. List employee-made agents using `list_employee_agents`. If you need configuration details (such as instructions, model, connectors, and sub-agents) for a specific agent, you MUST call `get_agent_details` with the exact display name of the agent. Do NOT call any other tool (such as 'get_agent'). +6. Migrate (create) employee-made low-code agents to a target Gemini Enterprise environment using `migrate_agent`. This will also implicitly save a backup to a GCS bucket. +7. Export employee-made low-code agents to a GCS bucket using `export_agent_to_gcs` when requested. +8. Import employee-made low-code agents from a GCS bucket to a target environment using `import_agent_from_gcs` when requested. +9. Before migrating an employee-made low-code agent using `migrate_agent`, FIRST call `lookup_and_map_connectors` to look up available connectors in the target environment and intelligently map source connectors (e.g., Snowflake Mcp May29 -> snowflakeMcp). Present the proposed mapping table to the user and GET THEIR APPROVAL on the mapping before calling `migrate_agent` with the approved `connector_mapping`! +10. Export NotebookLM notebooks to a GCS bucket using `export_notebook_to_gcs` when requested. +11. Import NotebookLM notebooks from a GCS bucket to a target environment using `import_notebook_from_gcs` when requested. + + +Mapping sources: +- Type "google docs": use "googleDriveContent" with documentId extracted from location URL and mimeType "application/vnd.google-apps.document". Do NOT include "sourceName" at the root of the object. +- Type "website": use "webContent" with url. Do NOT include "sourceName" at the root unless confirmed valid. +- Type "copied text": use "textContent" with content. +- Type "youtube": use "videoContent" with youtubeUrl. + +Critical Rule: Do NOT delete the source notebook or its sources after migration. This is a read-and-copy operation only. + +Output Format: +When presenting lists of notebooks, sources, employee-made agents, or proposed/missing connector and datastore mappings, you MUST format them as complete, pristine Markdown tables: +1. Always include a proper Markdown header and separator row (e.g. `| Display Name | Description |\n|---|---|` or `| Source Connector | Target Mapping | Status |\n|---|---|---|`). +2. Separate each row cleanly with a line break (`\n`). +3. Replace any internal newlines within descriptions/items with spaces or `
    ` so table formatting does not break. +Do NOT output raw JSON or A2UI JSON, as the client cannot render it. + +When a user asks for the details of an employee agent (e.g. "give details on Quarterly Business Review Generator"), output the details in exactly the following format without markdown code blocks: + +[Agent Description] + +Instructions: [Agent Instructions] + +Model: [Model Name or "Gemini 3 Flash"] + +Connectors: [List of connectors/tools/datastores] + +Knowledge: [List of attached knowledge/files/datastores or "None"] + +SubAgent : [SubAgent Display Name] +Description: [SubAgent Description] +Instructions: [SubAgent Instructions] +Model: [SubAgent Model] +Connectors: [SubAgent Connectors] +Output Format: [SubAgent Output Format] +Label: [SubAgent Label] + +""" + +root_agent = Agent( + model=MODEL_PATH, + name="GE_Migration_Agent", + description="Gemini Enterprise App Migration Agent that migrates notebooks between apps and lists custom agents.", + instruction=AGENT_INSTRUCTION, + tools=[list_notebooks, list_sources_and_types, create_notebook, add_source_to_notebook, migrate_notebook, list_employee_agents, get_agent_details, migrate_agent, export_agent_to_gcs, import_agent_from_gcs, export_notebook_to_gcs, import_notebook_from_gcs, lookup_and_map_connectors, list_datastores, get_migration_config, SkillToolset(skills=[migration_skill.migration_config_skill])], +) diff --git a/search/gemini-enterprise/ge-migration-agent/ge_migration_agent/run_playground.sh b/search/gemini-enterprise/ge-migration-agent/ge_migration_agent/run_playground.sh new file mode 100644 index 0000000..3415a34 --- /dev/null +++ b/search/gemini-enterprise/ge-migration-agent/ge_migration_agent/run_playground.sh @@ -0,0 +1,6 @@ +#!/bin/bash +SCRIPT_DIR=$(dirname "$0") +PROJECT_ROOT=$(cd "$SCRIPT_DIR/.." && pwd) +AGENT_PATH="$PROJECT_ROOT/notebook_agent" + +uv run adk run "$AGENT_PATH" diff --git a/search/gemini-enterprise/ge-migration-agent/ge_migration_agent/run_web_playground.sh b/search/gemini-enterprise/ge-migration-agent/ge_migration_agent/run_web_playground.sh new file mode 100755 index 0000000..bc56b54 --- /dev/null +++ b/search/gemini-enterprise/ge-migration-agent/ge_migration_agent/run_web_playground.sh @@ -0,0 +1,5 @@ +#!/bin/bash +SCRIPT_DIR=$(dirname "$0") +PROJECT_ROOT=$(cd "$SCRIPT_DIR/.." && pwd) + +uv run adk web --port 8001 "$PROJECT_ROOT" diff --git a/search/gemini-enterprise/ge-migration-agent/migrate.py b/search/gemini-enterprise/ge-migration-agent/migrate.py new file mode 100644 index 0000000..8e1a79b --- /dev/null +++ b/search/gemini-enterprise/ge-migration-agent/migrate.py @@ -0,0 +1,739 @@ +#!/usr/bin/env python3 +""" +🚀 Gemini Enterprise Migration CLI + +This script exposes all capabilities of the Gemini Enterprise Migration tool +directly via a deterministic, rapid Command-Line Interface. +""" +import os +import sys +import json +import argparse +import logging + +# Ensure package directory is in sys.path +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) + +import core + +# ANSI color codes +COLOR_GREEN = "\033[92m" +COLOR_YELLOW = "\033[93m" +COLOR_RED = "\033[91m" +COLOR_BLUE = "\033[94m" +COLOR_BOLD = "\033[1m" +COLOR_RESET = "\033[0m" + +def print_color(text, color): + """Prints text in the specified ANSI color to stdout, or to stderr if outputting JSON.""" + file = sys.stderr if "--json" in sys.argv else sys.stdout + if file.isatty(): + print(f"{color}{text}{COLOR_RESET}", file=file) + else: + print(text, file=file) + +def format_table(headers, rows): + """Formats and prints rows in a text-based grid table.""" + if not rows: + print("No results found.") + return + + # Calculate column widths + col_widths = [len(h) for headers_list in [headers] for h in headers_list] + for row in rows: + for i, val in enumerate(row): + str_val = str(val or "") + if len(str_val) > col_widths[i]: + col_widths[i] = len(str_val) + + # Build separator + separator = "+" + "+".join("-" * (w + 2) for w in col_widths) + "+" + + # Print headers + print_color(separator, COLOR_BOLD) + header_row = "|" + "|".join(f" {h:<{col_widths[i]}} " for i, h in enumerate(headers)) + "|" + print_color(header_row, COLOR_BOLD) + print_color(separator, COLOR_BOLD) + + # Print rows + for row in rows: + row_str = "|" + "|".join(f" {str(row[i] or ''):<{col_widths[i]}} " for i in range(len(headers))) + "|" + print(row_str) + + print_color(separator, COLOR_BOLD) + +def get_env_default(var_names, default=None): + """Returns the first non-empty environment variable value or default.""" + for name in var_names: + val = os.environ.get(name) + if val: + return val + return default + +try: + from skills import MigrationConfigSkill + SKILL_CFG = MigrationConfigSkill() +except Exception: + SKILL_CFG = None + +def get_skill_env(env_type: str, key: str, default=None): + if SKILL_CFG: + cfg = SKILL_CFG.get_source_env() if env_type == "source" else SKILL_CFG.get_target_env() + return cfg.get(key, default) + return default + +def handle_list_notebooks(args): + project = args.project or get_skill_env("source", "project_number") or get_env_default(["GOOGLE_CLOUD_PROJECT", "GEMINI_API_PROJECT"]) + location = args.location or get_skill_env("source", "region") or get_env_default(["GOOGLE_CLOUD_LOCATION", "GEMINI_API_LOCATION"], "global") + + if not project: + print_color("Error: Google Cloud project number must be provided via --project or environment variables.", COLOR_RED) + sys.exit(1) + + print_color(f"Listing recently viewed notebooks in project {project} ({location})...", COLOR_BLUE) + try: + notebooks = core.list_notebooks(project, location) + if args.json: + print(json.dumps(notebooks, indent=2)) + else: + headers = ["ID", "Title", "Create Time", "Update Time"] + rows = [] + for nb in notebooks: + nb_id = nb.get("name", "").split("/")[-1] + rows.append([ + nb_id, + nb.get("title", ""), + nb.get("createTime", "N/A"), + nb.get("updateTime", "N/A") + ]) + format_table(headers, rows) + except Exception as e: + print_color(f"Error: {e}", COLOR_RED) + sys.exit(1) + +def handle_list_sources(args): + project = args.project or get_skill_env("source", "project_number") or get_env_default(["GOOGLE_CLOUD_PROJECT", "GEMINI_API_PROJECT"]) + location = args.location or get_skill_env("source", "region") or get_env_default(["GOOGLE_CLOUD_LOCATION", "GEMINI_API_LOCATION"], "global") + + if not project: + print_color("Error: Google Cloud project number must be provided via --project or environment variables.", COLOR_RED) + sys.exit(1) + + print_color(f"Listing sources for notebook '{args.notebook_id}' in project {project}...", COLOR_BLUE) + try: + sources = core.list_sources_and_types(args.notebook_id, project, location) + if args.json: + # Strip raw_data from print to keep it clean, unless requested explicitly + for s in sources: + if not args.include_raw and "raw_data" in s: + s.pop("raw_data") + print(json.dumps(sources, indent=2)) + else: + headers = ["Title", "ID", "Type", "Location"] + rows = [] + for src in sources: + rows.append([ + src.get("title", ""), + src.get("id", ""), + src.get("type", ""), + src.get("location", "") + ]) + format_table(headers, rows) + except Exception as e: + print_color(f"Error: {e}", COLOR_RED) + sys.exit(1) + +def handle_migrate_notebook(args): + source_project = args.source_project or get_skill_env("source", "project_number") or get_env_default(["GOOGLE_CLOUD_PROJECT", "GEMINI_API_PROJECT"]) + target_project = args.target_project or get_skill_env("target", "project_number") or get_env_default(["GOOGLE_CLOUD_PROJECT", "GEMINI_API_PROJECT"]) + source_location = args.source_location or get_skill_env("source", "region") or get_env_default(["GOOGLE_CLOUD_LOCATION", "GEMINI_API_LOCATION"], "global") + target_location = args.target_location or get_skill_env("target", "region") or get_env_default(["GOOGLE_CLOUD_LOCATION", "GEMINI_API_LOCATION"], "global") + + bucket = get_env_default(["GCS_BUCKET_NAME"]) + + if not source_project: + print_color("Error: Source project must be specified via --source-project or environment.", COLOR_RED) + sys.exit(1) + if not target_project: + print_color("Error: Target project must be specified via --target-project or environment.", COLOR_RED) + sys.exit(1) + + print_color(f"Migrating notebook '{args.notebook}' from {source_project} ({source_location}) to {target_project} ({target_location})...", COLOR_BLUE) + try: + res = core.migrate_notebook_pipeline( + args.notebook, + target_project, + target_location, + source_project, + source_location, + bucket + ) + if args.json: + print(json.dumps(res, indent=2)) + else: + if res.get("success"): + print_color(f"\n🎉 Successfully migrated notebook '{res['source_notebook_title']}'!", COLOR_GREEN) + print(f"Target Notebook ID: {res['target_notebook_id']}") + print(f"Migrated Sources: {res['migrated_sources_count']}") + else: + print_color("\n⚠ Notebook migrated with partial success.", COLOR_YELLOW) + print(f"Target Notebook ID: {res['target_notebook_id']}") + print(f"Migrated Sources: {res['migrated_sources_count']}") + print_color(f"Failed Sources: {res['failed_sources_count']}", COLOR_RED) + for f in res.get("failed", []): + print_color(f" - {f['title']}: {f['error']}", COLOR_RED) + except Exception as e: + print_color(f"Error during migration: {e}", COLOR_RED) + sys.exit(1) + +def handle_list_agents(args): + project = args.project or get_skill_env("source", "project_number") or get_env_default(["GOOGLE_CLOUD_PROJECT", "GEMINI_API_PROJECT"]) + location = args.location or get_skill_env("source", "region") or get_env_default(["GOOGLE_CLOUD_LOCATION", "GEMINI_API_LOCATION"], "global") + engine_id = args.engine_id or get_skill_env("source", "engine_id") + + if not project: + print_color("Error: Google Cloud project number must be specified.", COLOR_RED) + sys.exit(1) + if not engine_id: + print_color("Error: Discovery Engine engine ID must be specified.", COLOR_RED) + sys.exit(1) + + print_color(f"Listing employee-made agents in project {project} (Engine: {engine_id})...", COLOR_BLUE) + try: + agents = core.list_employee_agents(project, location, engine_id, basic=True) + if args.json: + for a in agents: + if "raw_agent" in a: + a.pop("raw_agent") + print(json.dumps(agents, indent=2)) + else: + headers = ["Display Name", "Description", "Primary Tools"] + rows = [] + for agent in agents: + tools_str = ", ".join(agent.get("connectors_and_tools", [])) + if len(tools_str) > 40: + tools_str = tools_str[:37] + "..." + rows.append([ + agent.get("displayName", ""), + agent.get("description", "No description"), + tools_str + ]) + format_table(headers, rows) + except Exception as e: + print_color(f"Error: {e}", COLOR_RED) + sys.exit(1) + + +def handle_get_agent_details(args): + project = args.project or get_skill_env("source", "project_number") or get_env_default(["GOOGLE_CLOUD_PROJECT", "GEMINI_API_PROJECT"]) + location = args.location or get_skill_env("source", "region") or get_env_default(["GOOGLE_CLOUD_LOCATION", "GEMINI_API_LOCATION"], "global") + engine_id = args.engine_id or get_skill_env("source", "engine_id") + + if not project: + print_color("Error: Google Cloud project number must be specified.", COLOR_RED) + sys.exit(1) + if not engine_id: + print_color("Error: Discovery Engine engine ID must be specified.", COLOR_RED) + sys.exit(1) + + print_color(f"Retrieving details for employee agent '{args.agent_name}'...", COLOR_BLUE) + try: + agents = core.list_employee_agents(project, location, engine_id, basic=False) + for agent in agents: + if agent.get("displayName", "").lower() == args.agent_name.lower(): + if args.json: + if "raw_agent" in agent: + agent.pop("raw_agent") + print(json.dumps(agent, indent=2)) + else: + print_color(f"\nAgent: {agent['displayName']}", COLOR_BOLD) + print(f"Description: {agent.get('description', 'No description')}") + print(f"Instructions:\n{agent.get('instructions', 'No instructions')}") + print(f"Connectors & Tools: {', '.join(agent.get('connectors_and_tools', []))}") + print(f"Knowledge: {', '.join(agent.get('knowledge', [])) or 'None'}") + sub_agents_list = [] + for sub in agent.get("sub_agents", []): + sub_agents_list.append(f"{sub.get('displayName')} ({sub.get('model')})") + print(f"Sub-Agents: {', '.join(sub_agents_list) or 'None'}") + return + print_color(f"Error: Agent '{args.agent_name}' not found.", COLOR_RED) + sys.exit(1) + except Exception as e: + print_color(f"Error: {e}", COLOR_RED) + sys.exit(1) + + +def handle_extract_datastores(args): + project = args.project or get_skill_env("source", "project_number") or get_env_default(["GOOGLE_CLOUD_PROJECT", "GEMINI_API_PROJECT"]) + location = args.location or get_skill_env("source", "region") or get_env_default(["GOOGLE_CLOUD_LOCATION", "GEMINI_API_LOCATION"], "global") + engine_id = args.engine_id or get_skill_env("source", "engine_id") + + if not project: + print_color("Error: Source project must be specified.", COLOR_RED) + sys.exit(1) + if not engine_id: + print_color("Error: Discovery Engine engine ID must be specified.", COLOR_RED) + sys.exit(1) + + print_color(f"Extracting datastores for agent '{args.agent_name}'...", COLOR_BLUE) + try: + report = core.extract_agent_datastores( + args.agent_name, + project, + location, + engine_id + ) + if args.json: + print(json.dumps(report, indent=2)) + else: + print_color(f"\nDatastore dependencies for agent: {report['agent_name']}", COLOR_BOLD) + datastores = report.get("datastores", {}) + if not datastores: + print("No datastores found.") + for node, ds_list in datastores.items(): + print(f"Node '{node}':") + for ds in ds_list: + print(f" - {ds}") + except Exception as e: + print_color(f"Error: {e}", COLOR_RED) + sys.exit(1) + +def handle_migrate_agent(args): + source_project = args.source_project or get_skill_env("source", "project_number") or get_env_default(["GOOGLE_CLOUD_PROJECT", "GEMINI_API_PROJECT"]) + target_project = args.target_project or get_skill_env("target", "project_number") or get_env_default(["GOOGLE_CLOUD_PROJECT", "GEMINI_API_PROJECT"]) + source_location = args.source_location or get_skill_env("source", "region") or get_env_default(["GOOGLE_CLOUD_LOCATION", "GEMINI_API_LOCATION"], "global") + target_location = args.target_location or get_skill_env("target", "region") or get_env_default(["GOOGLE_CLOUD_LOCATION", "GEMINI_API_LOCATION"], "global") + source_engine = args.source_engine or get_skill_env("source", "engine_id") + target_engine = args.target_engine or get_skill_env("target", "engine_id") + bucket = args.backup_bucket or get_env_default(["GCS_BUCKET_NAME"]) + + mapping_str = args.connector_mapping + if not mapping_str and SKILL_CFG: + mapping = SKILL_CFG.get_connectors_mapping() + mapping.update(SKILL_CFG.get_datastores_mapping()) + if mapping: + mapping_str = json.dumps(mapping) + + if not source_project or not target_project: + print_color("Error: Source and Target projects must be specified.", COLOR_RED) + sys.exit(1) + + print_color(f"Migrating employee agent '{args.agent_name}' from project {source_project} to target project {target_project}...", COLOR_BLUE) + try: + res = core.migrate_employee_agent( + args.agent_name, + target_project, + target_location, + target_engine, + source_project, + source_location, + source_engine, + args.force, + bucket, + mapping_str + ) + if args.json: + print(json.dumps(res, indent=2)) + else: + if res.get("success"): + print_color(f"\n🎉 Successfully migrated agent '{args.agent_name}'!", COLOR_GREEN) + print(res.get("message")) + else: + # Handle pre-flight validation failure + if "warning" in res: + print_color(f"\n⚠ Pre-flight Validation Failed: {res['warning']}", COLOR_YELLOW) + print(res.get("message")) + report = res.get("report", {}) + print(f"Missing Connectors/Datastores: {report.get('missing_connectors', [])}") + print("\nTo bypass this check and proceed anyway, run the command again with --force.") + else: + print_color(f"Migration failed: {res.get('message')}", COLOR_RED) + except Exception as e: + print_color(f"Error: {e}", COLOR_RED) + sys.exit(1) + +def handle_lookup_and_map_connectors(args): + source_project = args.source_project or get_skill_env("source", "project_number") or get_env_default(["GOOGLE_CLOUD_PROJECT", "GEMINI_API_PROJECT"]) + target_project = args.target_project or get_skill_env("target", "project_number") or get_env_default(["GOOGLE_CLOUD_PROJECT", "GEMINI_API_PROJECT"]) + source_location = args.source_location or get_skill_env("source", "region") or get_env_default(["GOOGLE_CLOUD_LOCATION", "GEMINI_API_LOCATION"], "global") + target_location = args.target_location or get_skill_env("target", "region") or get_env_default(["GOOGLE_CLOUD_LOCATION", "GEMINI_API_LOCATION"], "global") + source_engine = args.source_engine or get_skill_env("source", "engine_id") + target_engine = args.target_engine or get_skill_env("target", "engine_id") + + try: + res = core.lookup_and_map_connectors( + args.agent_name, + target_project, + target_location, + target_engine, + source_project, + source_location, + source_engine + ) + if args.json: + print(json.dumps(res, indent=2)) + else: + headers = ["Source Connector / DataStore", "Target Mapping", "Status"] + mapping = res.get("proposed_mapping", {}) + missing = res.get("missing_connectors", []) + rows = [] + for k, v in mapping.items(): + status = "❌ Missing" if k in missing else "✅ Mapped" + rows.append([k, v, status]) + format_table(headers, rows) + if missing: + print_color(f"\n⚠ Missing Connectors/DataStores in Target: {missing}", COLOR_YELLOW) + except Exception as e: + print_color(f"Error: {e}", COLOR_RED) + sys.exit(1) + + + +def handle_list_datastores(args): + project = args.project or get_env_default(["GOOGLE_CLOUD_PROJECT", "GEMINI_API_PROJECT"]) + location = args.location or get_env_default(["GOOGLE_CLOUD_LOCATION", "GEMINI_API_LOCATION"], "global") + + if not project: + print_color("Error: Google Cloud project number must be specified.", COLOR_RED) + sys.exit(1) + + print_color(f"Listing DataStores in project {project} ({location})...", COLOR_BLUE) + try: + datastores = core.list_datastores(project, location) + if args.json: + print(json.dumps(datastores, indent=2)) + else: + headers = ["DataStore ID", "Display Name", "Full Resource Name"] + rows = [[ds["id"], ds["displayName"], ds["name"]] for ds in datastores] + format_table(headers, rows) + except Exception as e: + print_color(f"Error: {e}", COLOR_RED) + sys.exit(1) + +def handle_export_agent_gcs(args): + project = args.project or get_env_default(["GOOGLE_CLOUD_PROJECT", "GEMINI_API_PROJECT"]) + location = args.location or get_env_default(["GOOGLE_CLOUD_LOCATION", "GEMINI_API_LOCATION"], "global") + bucket = args.bucket or get_env_default(["GCS_BUCKET_NAME"]) + + if not project: + print_color("Error: Source project must be specified.", COLOR_RED) + sys.exit(1) + if not bucket: + print_color("Error: GCS bucket name must be specified via --bucket or environment.", COLOR_RED) + sys.exit(1) + + print_color(f"Exporting agent '{args.agent_name}' definition to gs://{bucket}/{args.object_name}...", COLOR_BLUE) + try: + res = core.export_agent_to_gcs( + args.agent_name, + args.object_name, + bucket, + project, + location, + args.engine_id + ) + if args.json: + print(json.dumps(res, indent=2)) + else: + print_color(f"\n🎉 {res.get('message')}", COLOR_GREEN) + except Exception as e: + print_color(f"Error during export: {e}", COLOR_RED) + sys.exit(1) + +def handle_import_agent_gcs(args): + project = args.target_project or get_env_default(["GOOGLE_CLOUD_PROJECT", "GEMINI_API_PROJECT"]) + location = args.target_location or get_env_default(["GOOGLE_CLOUD_LOCATION", "GEMINI_API_LOCATION"], "global") + bucket = args.bucket or get_env_default(["GCS_BUCKET_NAME"]) + + mapping_str = "" + if SKILL_CFG: + mapping = SKILL_CFG.get_connectors_mapping() + mapping.update(SKILL_CFG.get_datastores_mapping()) + if mapping: + mapping_str = json.dumps(mapping) + + if not project: + print_color("Error: Target project must be specified.", COLOR_RED) + sys.exit(1) + if not bucket: + print_color("Error: GCS bucket name must be specified via --bucket or environment.", COLOR_RED) + sys.exit(1) + + print_color(f"Importing agent definition from gs://{bucket}/{args.object_name} to project {project}...", COLOR_BLUE) + try: + res = core.import_agent_from_gcs( + args.object_name, + project, + location, + args.target_engine, + bucket, + mapping_str + ) + if args.json: + print(json.dumps(res, indent=2)) + else: + print_color(f"\n🎉 {res.get('message')}", COLOR_GREEN) + except Exception as e: + print_color(f"Error during import: {e}", COLOR_RED) + sys.exit(1) + +def handle_export_notebook_gcs(args): + project = args.project or get_skill_env("source", "project_number") or get_env_default(["GOOGLE_CLOUD_PROJECT", "GEMINI_API_PROJECT"]) + location = args.location or get_skill_env("source", "region") or get_env_default(["GOOGLE_CLOUD_LOCATION", "GEMINI_API_LOCATION"], "global") + bucket = args.bucket or get_env_default(["GCS_BUCKET_NAME"]) + + if not project: + print_color("Error: Source project must be specified.", COLOR_RED) + sys.exit(1) + if not bucket: + print_color("Error: GCS bucket name must be specified via --bucket or environment.", COLOR_RED) + sys.exit(1) + + print_color(f"Exporting notebook '{args.notebook_name}' definition to gs://{bucket}/{args.object_name}...", COLOR_BLUE) + try: + res = core.export_notebook_to_gcs( + args.notebook_name, + args.object_name, + bucket, + project, + location + ) + if args.json: + print(json.dumps(res, indent=2)) + else: + print_color(f"\n🎉 {res.get('message')}", COLOR_GREEN) + except Exception as e: + print_color(f"Error during export: {e}", COLOR_RED) + sys.exit(1) + +def handle_import_notebook_gcs(args): + project = args.target_project or get_skill_env("target", "project_number") or get_env_default(["GOOGLE_CLOUD_PROJECT", "GEMINI_API_PROJECT"]) + location = args.target_location or get_skill_env("target", "region") or get_env_default(["GOOGLE_CLOUD_LOCATION", "GEMINI_API_LOCATION"], "global") + bucket = args.bucket or get_env_default(["GCS_BUCKET_NAME"]) + + if not project: + print_color("Error: Target project must be specified.", COLOR_RED) + sys.exit(1) + if not bucket: + print_color("Error: GCS bucket name must be specified via --bucket or environment.", COLOR_RED) + sys.exit(1) + + print_color(f"Importing notebook definition from gs://{bucket}/{args.object_name} to project {project}...", COLOR_BLUE) + try: + res = core.import_notebook_from_gcs( + args.object_name, + project, + location, + bucket + ) + if args.json: + print(json.dumps(res, indent=2)) + else: + if res.get("success"): + print_color(f"\n🎉 Successfully imported notebook '{res['target_notebook_title']}'!", COLOR_GREEN) + print(f"Target Notebook ID: {res['target_notebook_id']}") + print(f"Migrated Sources: {res['migrated_sources_count']}") + else: + print_color("\n⚠ Notebook imported with partial success.", COLOR_YELLOW) + print(f"Target Notebook ID: {res['target_notebook_id']}") + print(f"Migrated Sources: {res['migrated_sources_count']}") + print_color(f"Failed Sources: {res['failed_sources_count']}", COLOR_RED) + for f in res.get("failed", []): + print_color(f" - {f['title']}: {f['error']}", COLOR_RED) + except Exception as e: + print_color(f"Error during import: {e}", COLOR_RED) + sys.exit(1) + +def handle_create_notebook(args): + target_project = args.target_project or get_env_default(["GOOGLE_CLOUD_PROJECT", "GEMINI_API_PROJECT"]) + target_location = args.target_location or get_env_default(["GOOGLE_CLOUD_LOCATION", "GEMINI_API_LOCATION"], "global") + + if not target_project: + print_color("Error: Target project must be specified.", COLOR_RED) + sys.exit(1) + + print_color(f"Creating notebook '{args.title}' in project {target_project}...", COLOR_BLUE) + try: + res = core.create_notebook(target_project, target_location, args.title) + if args.json: + print(json.dumps(res, indent=2)) + else: + print_color(f"🎉 Successfully created notebook '{args.title}'!", COLOR_GREEN) + print(f"ID: {res.get('name', '').split('/')[-1]}") + except Exception as e: + print_color(f"Error: {e}", COLOR_RED) + sys.exit(1) + +def handle_add_source_to_notebook(args): + target_project = args.target_project or get_env_default(["GOOGLE_CLOUD_PROJECT", "GEMINI_API_PROJECT"]) + target_location = args.target_location or get_env_default(["GOOGLE_CLOUD_LOCATION", "GEMINI_API_LOCATION"], "global") + + if not target_project: + print_color("Error: Target project must be specified.", COLOR_RED) + sys.exit(1) + + print_color(f"Adding source to notebook '{args.notebook_id}' in project {target_project}...", COLOR_BLUE) + try: + content_obj = json.loads(args.source_content) + res = core.add_source_to_notebook(target_project, target_location, args.notebook_id, content_obj) + if args.json: + print(json.dumps(res, indent=2)) + else: + print_color(f"🎉 Successfully added source to notebook '{args.notebook_id}'!", COLOR_GREEN) + except Exception as e: + print_color(f"Error: {e}", COLOR_RED) + sys.exit(1) + + + +def main(): + parser = argparse.ArgumentParser( + description="🚀 Gemini Enterprise Migration CLI - Migrate low-code agents and notebooks deterministically." + ) + parser.add_argument("--json", action="store_true", help="Output raw JSON results instead of user-friendly text tables") + + subparsers = parser.add_subparsers(dest="command", help="Migration command to execute") + + # List notebooks + p_list_nb = subparsers.add_parser("list-notebooks", help="List recently viewed notebooks in source project") + p_list_nb.add_argument("--project", help="Google Cloud project number (defaults to GOOGLE_CLOUD_PROJECT env)") + p_list_nb.add_argument("--location", help="Geographic location (defaults to GOOGLE_CLOUD_LOCATION env or 'global')") + p_list_nb.set_defaults(func=handle_list_notebooks) + + # List sources + p_list_src = subparsers.add_parser("list-sources", help="List sources and their types in a specific notebook") + p_list_src.add_argument("notebook_id", help="The source notebook ID") + p_list_src.add_argument("--project", help="Google Cloud project number (defaults to GOOGLE_CLOUD_PROJECT env)") + p_list_src.add_argument("--location", help="Geographic location (defaults to GOOGLE_CLOUD_LOCATION env or 'global')") + p_list_src.add_argument("--include-raw", action="store_true", help="Include raw source metadata in JSON output") + p_list_src.set_defaults(func=handle_list_sources) + + # Migrate notebook + p_mig_nb = subparsers.add_parser("migrate-notebook", help="Migrate a notebook and all its sources in a fast python loop") + p_mig_nb.add_argument("notebook", help="The source notebook ID or title to migrate") + p_mig_nb.add_argument("--source-project", help="Source project number (defaults to GOOGLE_CLOUD_PROJECT env)") + p_mig_nb.add_argument("--target-project", help="Target project number (defaults to GOOGLE_CLOUD_PROJECT env)") + p_mig_nb.add_argument("--source-location", help="Source geographic location (defaults to 'global')") + p_mig_nb.add_argument("--target-location", help="Target geographic location (defaults to 'global')") + p_mig_nb.set_defaults(func=handle_migrate_notebook) + + # List agents + p_list_ag = subparsers.add_parser("list-agents", help="List all employee-made low-code agents") + p_list_ag.add_argument("--project", help="Source project number (defaults to GOOGLE_CLOUD_PROJECT env or SKILL.md)") + p_list_ag.add_argument("--engine-id", help="Discovery Engine engine ID containing the agents (defaults to SKILL.md)") + p_list_ag.add_argument("--location", help="Geographic location (defaults to 'global')") + p_list_ag.set_defaults(func=handle_list_agents) + + # Get agent details + p_get_det = subparsers.add_parser("get-agent-details", help="Get details of a specific agent") + p_get_det.add_argument("agent_name", help="Exact display name of low-code agent") + p_get_det.add_argument("--project", help="Source project number (defaults to GOOGLE_CLOUD_PROJECT env or SKILL.md)") + p_get_det.add_argument("--engine-id", help="Discovery Engine engine ID containing the agent (defaults to SKILL.md)") + p_get_det.add_argument("--location", help="Geographic location (defaults to 'global')") + p_get_det.set_defaults(func=handle_get_agent_details) + + # Extract datastores + p_ext_ds = subparsers.add_parser("extract-datastores", help="Extract datastores used by an agent and its subagents") + p_ext_ds.add_argument("agent_name", help="Exact display name of source low-code agent") + p_ext_ds.add_argument("--project", help="Source project number (defaults to GOOGLE_CLOUD_PROJECT env or SKILL.md)") + p_ext_ds.add_argument("--engine-id", help="Discovery Engine engine ID containing the agent (defaults to SKILL.md)") + p_ext_ds.add_argument("--location", help="Geographic location (defaults to 'global')") + p_ext_ds.set_defaults(func=handle_extract_datastores) + + # Migrate agent + p_mig_ag = subparsers.add_parser("migrate-agent", help="Migrate a low-code agent from source to target") + p_mig_ag.add_argument("agent_name", help="Exact display name of source low-code agent to migrate") + p_mig_ag.add_argument("--source-project", help="Source project number (defaults to GOOGLE_CLOUD_PROJECT env)") + p_mig_ag.add_argument("--target-project", help="Target project number (defaults to GOOGLE_CLOUD_PROJECT env)") + p_mig_ag.add_argument("--source-engine", help="Source Discovery Engine engine ID") + p_mig_ag.add_argument("--target-engine", help="Target Discovery Engine engine ID") + p_mig_ag.add_argument("--source-location", help="Source geographic location (defaults to 'global')") + p_mig_ag.add_argument("--target-location", help="Target geographic location (defaults to 'global')") + p_mig_ag.add_argument("--force", action="store_true", help="Bypass connector pre-flight validation check") + p_mig_ag.add_argument("--backup-bucket", help="GCS bucket name for implicit backup (defaults to GCS_BUCKET_NAME env)") + p_mig_ag.add_argument("--connector-mapping", help="Optional JSON dict or comma-separated k:v mapping string", default="") + p_mig_ag.set_defaults(func=handle_migrate_agent) + + # Lookup and Map Connectors + p_lookup_map = subparsers.add_parser("lookup-map-connectors", help="Lookup and intelligently map source connectors to target connectors") + p_lookup_map.add_argument("agent_name", help="Exact display name of source agent") + p_lookup_map.add_argument("--source-project", help="Source project number") + p_lookup_map.add_argument("--target-project", help="Target project number") + p_lookup_map.add_argument("--source-engine", help="Source engine ID") + p_lookup_map.add_argument("--target-engine", help="Target engine ID") + p_lookup_map.add_argument("--source-location", help="Source location", default="global") + p_lookup_map.add_argument("--target-location", help="Target location", default="global") + p_lookup_map.set_defaults(func=handle_lookup_and_map_connectors) + + + # List DataStores + p_list_ds = subparsers.add_parser("list-datastores", help="List available DataStores in a project") + p_list_ds.add_argument("--project", help="Google Cloud project number") + p_list_ds.add_argument("--location", help="Location (defaults to 'global')", default="global") + p_list_ds.set_defaults(func=handle_list_datastores) + # Export Agent GCS + p_exp_gcs = subparsers.add_parser("export-agent-gcs", help="Export an employee-made low-code agent to GCS") + p_exp_gcs.add_argument("agent_name", help="Exact display name of source low-code agent") + p_exp_gcs.add_argument("object_name", help="GCS object name (e.g. exports/my_agent.json)") + p_exp_gcs.add_argument("--bucket", help="GCS bucket name (defaults to GCS_BUCKET_NAME env)") + p_exp_gcs.add_argument("--project", help="Source project number (defaults to GOOGLE_CLOUD_PROJECT env)") + p_exp_gcs.add_argument("--engine-id", required=True, help="Source Discovery Engine engine ID") + p_exp_gcs.add_argument("--location", help="Source geographic location (defaults to 'global')") + p_exp_gcs.set_defaults(func=handle_export_agent_gcs) + + # Import Agent GCS + p_imp_gcs = subparsers.add_parser("import-agent-gcs", help="Import an employee-made low-code agent definition from GCS") + p_imp_gcs.add_argument("object_name", help="GCS object name of stored definition JSON") + p_imp_gcs.add_argument("--bucket", help="GCS bucket name (defaults to GCS_BUCKET_NAME env)") + p_imp_gcs.add_argument("--target-project", help="Target project number (defaults to GOOGLE_CLOUD_PROJECT env)") + p_imp_gcs.add_argument("--target-engine", required=True, help="Target Discovery Engine engine ID") + p_imp_gcs.add_argument("--target-location", help="Target geographic location (defaults to 'global')") + p_imp_gcs.set_defaults(func=handle_import_agent_gcs) + + # Export Notebook GCS + p_exp_nb_gcs = subparsers.add_parser("export-notebook-gcs", help="Export a NotebookLM notebook definition to GCS") + p_exp_nb_gcs.add_argument("notebook_name", help="Exact title or ID of source notebook") + p_exp_nb_gcs.add_argument("object_name", help="GCS object name (e.g. exports/my_notebook.json)") + p_exp_nb_gcs.add_argument("--bucket", help="GCS bucket name (defaults to GCS_BUCKET_NAME env)") + p_exp_nb_gcs.add_argument("--project", help="Source project number (defaults to GOOGLE_CLOUD_PROJECT env)") + p_exp_nb_gcs.add_argument("--location", help="Source geographic location (defaults to 'global')") + p_exp_nb_gcs.set_defaults(func=handle_export_notebook_gcs) + + # Import Notebook GCS + p_imp_nb_gcs = subparsers.add_parser("import-notebook-gcs", help="Import a NotebookLM notebook definition from GCS") + p_imp_nb_gcs.add_argument("object_name", help="GCS object name of stored definition JSON") + p_imp_nb_gcs.add_argument("--bucket", help="GCS bucket name (defaults to GCS_BUCKET_NAME env)") + p_imp_nb_gcs.add_argument("--target-project", help="Target project number (defaults to GOOGLE_CLOUD_PROJECT env)") + p_imp_nb_gcs.add_argument("--target-location", help="Target geographic location (defaults to 'global')") + p_imp_nb_gcs.set_defaults(func=handle_import_notebook_gcs) + + # Create notebook + p_create_nb = subparsers.add_parser("create-notebook", help="Create a new empty notebook in the target project") + p_create_nb.add_argument("title", help="The title of the notebook to create") + p_create_nb.add_argument("--target-project", help="Target project number (defaults to GOOGLE_CLOUD_PROJECT env)") + p_create_nb.add_argument("--target-location", help="Target geographic location (defaults to 'global')") + p_create_nb.set_defaults(func=handle_create_notebook) + + # Add source to notebook + p_add_src = subparsers.add_parser("add-source-to-notebook", help="Add a single source to a notebook in the target project") + p_add_src.add_argument("notebook_id", help="The ID of the notebook to add the source to") + p_add_src.add_argument("source_content", help="The JSON string of the userContent to add") + p_add_src.add_argument("--target-project", help="Target project number (defaults to GOOGLE_CLOUD_PROJECT env)") + p_add_src.add_argument("--target-location", help="Target geographic location (defaults to 'global')") + p_add_src.set_defaults(func=handle_add_source_to_notebook) + + + + args = parser.parse_args() + + # Set up basic logging format + logging.basicConfig(level=logging.WARNING, format="%(asctime)s - %(levelname)s - %(message)s") + + if not args.command: + parser.print_help() + sys.exit(0) + + args.func(args) + +if __name__ == "__main__": + main() diff --git a/search/gemini-enterprise/ge-migration-agent/pyproject.toml b/search/gemini-enterprise/ge-migration-agent/pyproject.toml new file mode 100644 index 0000000..44d0f28 --- /dev/null +++ b/search/gemini-enterprise/ge-migration-agent/pyproject.toml @@ -0,0 +1,16 @@ +[project] +name = "ge-migration-agent" +version = "0.1.0" +description = "Seamlessly migrate your low-code agents and NotebookLM notebooks to Gemini Enterprise." +readme = "README.md" +requires-python = ">=3.10" +dependencies = [ + "google-adk", + "google-auth>=2.20.0", + "google-cloud-storage>=2.10.0", + "python-dotenv>=1.0.0", + "requests>=2.31.0", +] + +[tool.uv] +package = false diff --git a/search/gemini-enterprise/ge-migration-agent/requirements.txt b/search/gemini-enterprise/ge-migration-agent/requirements.txt new file mode 100644 index 0000000..eca7ab3 --- /dev/null +++ b/search/gemini-enterprise/ge-migration-agent/requirements.txt @@ -0,0 +1,5 @@ +google-adk +google-auth>=2.20.0 +google-cloud-storage>=2.10.0 +python-dotenv>=1.0.0 +requests>=2.31.0 diff --git a/search/gemini-enterprise/ge-migration-agent/skills/SKILL.md b/search/gemini-enterprise/ge-migration-agent/skills/SKILL.md new file mode 100644 index 0000000..8abf821 --- /dev/null +++ b/search/gemini-enterprise/ge-migration-agent/skills/SKILL.md @@ -0,0 +1,70 @@ +--- +name: migration-config-skill +description: Holds canonical source and target environment parameters and verified DataStore mappings. +metadata: + environments: + source: + project_number: "" + project_id: "" + region: global + engine_id: "" + target: + project_number: "" + project_id: "" + region: global + engine_id: "" + datastores_mapping: + # Format: source-datastore-id-suffix: target-datastore-id-suffix + "": "" + connectors_mapping: + ge-drive-all: Drive + GeGmail: geGmail + googleSearch: googleSearch + urlContext: urlContext +--- + +# ADK Administration Setup & Migration Instructions + +This file serves as the single source of truth for Gemini Enterprise migrations. Administrators can fully configure source-to-target resource pairings by updating the YAML metadata frontmatter above. + +--- + +## Administrative Setup Guide + +### 1. Environments Configuration +Set your official canonical source and target project definitions under `environments`: +- **`project_number`**: The numeric Google Cloud Project Number (e.g., `404109417257`). +- **`engine_id`**: The fully qualified Discovery Engine app/engine ID. + +### 2. DataStore Grounding Mappings (`datastores_mapping`) +Map underlying knowledge search collections and grounding data stores (assigned under `dataStoreSpecs`) using their exact resource ID suffix: +```yaml +datastores_mapping: + # Source ID -> Target ID + example-mcp-source_12345_mcp_data: example-mcp-target_67890_mcp_data + example-drive-source_12345_google_drive: example-drive-target_67890_google_drive +``` +*Note: Always use the fully qualified ID suffix (e.g., `_google_drive`, `_mcp_data`) to prevent substring truncation.* + +### 3. Visual Connector & Tool Mappings (`connectors_mapping`) +Map display aliases and frontend extension tool chips (assigned under `selectedTools`) to ensure visual rendering parity in the Agent Designer UI canvas: +```yaml +connectors_mapping: + # Source UI Badge -> Canonical Target UI Token + "Example Source Connector": custom_mcp + "example-drive": Drive +``` + +--- + +## Operational Execution + +Once configured, run live migrations or offline GCS imports directly: + +```bash +# 1. Execute live cross-environment migration +uv run ./migrate.py migrate-agent "Quarterly Business Review Generator" --force + +# 2. Import agent definition from GCS offline backup +uv run ./migrate.py import-agent-gcs quarterly_business_review_export.json --target-engine +``` diff --git a/search/gemini-enterprise/ge-migration-agent/skills/__init__.py b/search/gemini-enterprise/ge-migration-agent/skills/__init__.py new file mode 100644 index 0000000..608956b --- /dev/null +++ b/search/gemini-enterprise/ge-migration-agent/skills/__init__.py @@ -0,0 +1,3 @@ +from .migration_skill import MigrationConfigSkill + +__all__ = ["MigrationConfigSkill"] diff --git a/search/gemini-enterprise/ge-migration-agent/skills/migration_skill.py b/search/gemini-enterprise/ge-migration-agent/skills/migration_skill.py new file mode 100644 index 0000000..9f0da7c --- /dev/null +++ b/search/gemini-enterprise/ge-migration-agent/skills/migration_skill.py @@ -0,0 +1,80 @@ +""" +🚀 ADK Migration Skill + +Provides reusable context containing pre-configured Source and Target environments +as well as hardcoded/verified DataStore and Connector mappings using ADK models.Skill. +""" +import os +import json +import logging + +try: + from google.adk.skills import models +except ImportError: + models = None + +def load_metadata_from_skill_md() -> dict: + """Dynamically parses and extracts metadata from the SKILL.md YAML frontmatter.""" + skill_md_path = os.path.join(os.path.dirname(__file__), "SKILL.md") + if os.path.exists(skill_md_path): + try: + import yaml + with open(skill_md_path, "r", encoding="utf-8") as f: + content = f.read() + parts = content.split("---") + if len(parts) >= 3: + frontmatter_str = parts[1] + data = yaml.safe_load(frontmatter_str) + if data and "metadata" in data: + return data["metadata"] + except Exception as e: + logging.warning(f"Failed to parse SKILL.md frontmatter at import: {e}") + return {} + +# Single source of truth dynamically loaded from SKILL.md +SKILL_METADATA = load_metadata_from_skill_md() + +if models: + migration_config_skill = models.Skill( + frontmatter=models.Frontmatter( + name="migration-config-skill", + description="Holds canonical source and target environment parameters and verified DataStore mappings.", + metadata=SKILL_METADATA + ), + instructions="Reference SKILL.md for complete administrative setup guides covering environments, grounding datastores_mapping, and visual connectors_mapping.", + resources=models.Resources(references={}) + ) +else: + migration_config_skill = None + +class MigrationConfigSkill: + """Skill holding pre-verified source/target configurations and datastore mappings.""" + + def __init__(self, config_path: str = "migration_skill.json"): + self.config_path = os.path.join(os.path.dirname(__file__), config_path) + self.config = SKILL_METADATA or self._load_fallback_config() + + def _load_fallback_config(self) -> dict: + if os.path.exists(self.config_path): + try: + with open(self.config_path, "r") as f: + return json.load(f) + except Exception: + pass + return {} + + def get_source_env(self) -> dict: + """Returns source environment configuration.""" + return self.config.get("environments", {}).get("source", {}) + + def get_target_env(self) -> dict: + """Returns target environment configuration.""" + return self.config.get("environments", {}).get("target", {}) + + def get_datastores_mapping(self) -> dict: + """Returns verified mappings between source and target DataStores.""" + return self.config.get("datastores_mapping", {}) + + def get_connectors_mapping(self) -> dict: + """Returns verified mappings between source and target Connectors.""" + return self.config.get("connectors_mapping", {}) diff --git a/search/gemini-enterprise/ge-migration-agent/uv.lock b/search/gemini-enterprise/ge-migration-agent/uv.lock new file mode 100644 index 0000000..110fcc6 --- /dev/null +++ b/search/gemini-enterprise/ge-migration-agent/uv.lock @@ -0,0 +1,1519 @@ +version = 1 +revision = 3 +requires-python = ">=3.10" +resolution-markers = [ + "python_full_version >= '3.13'", + "python_full_version >= '3.11' and python_full_version < '3.13'", + "python_full_version < '3.11'", +] + +[[package]] +name = "aiosqlite" +version = "0.22.1" +source = { registry = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/simple/" } +sdist = { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/aiosqlite/aiosqlite-0.22.1.tar.gz", hash = "sha256:043e0bd78d32888c0a9ca90fc788b38796843360c855a7262a532813133a0650" } +wheels = [ + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/aiosqlite/aiosqlite-0.22.1-py3-none-any.whl", hash = "sha256:21c002eb13823fad740196c5a2e9d8e62f6243bd9e7e4a1f87fb5e44ecb4fceb" }, +] + +[[package]] +name = "annotated-doc" +version = "0.0.4" +source = { registry = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/simple/" } +sdist = { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/annotated-doc/annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4" } +wheels = [ + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/annotated-doc/annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320" }, +] + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/simple/" } +sdist = { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/annotated-types/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89" } +wheels = [ + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/annotated-types/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53" }, +] + +[[package]] +name = "anyio" +version = "4.13.0" +source = { registry = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/simple/" } +dependencies = [ + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "idna" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/anyio/anyio-4.13.0.tar.gz", hash = "sha256:334b70e641fd2221c1505b3890c69882fe4a2df910cba14d97019b90b24439dc" } +wheels = [ + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/anyio/anyio-4.13.0-py3-none-any.whl", hash = "sha256:08b310f9e24a9594186fd75b4f73f4a4152069e3853f1ed8bfbf58369f4ad708" }, +] + +[[package]] +name = "attrs" +version = "26.1.0" +source = { registry = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/simple/" } +sdist = { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/attrs/attrs-26.1.0.tar.gz", hash = "sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32" } +wheels = [ + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/attrs/attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309" }, +] + +[[package]] +name = "authlib" +version = "1.7.2" +source = { registry = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/simple/" } +dependencies = [ + { name = "cryptography" }, + { name = "joserfc" }, +] +sdist = { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/authlib/authlib-1.7.2.tar.gz", hash = "sha256:2cea25fefcd4e7173bdf1372c0afc265c8034b23a8cd5dcb6a9164b826c64231" } +wheels = [ + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/authlib/authlib-1.7.2-py2.py3-none-any.whl", hash = "sha256:3e1faedc9d87e7d56a164eca3ccb6ace0d61b94abe83e92242f8dc8bba9b4a9f" }, +] + +[[package]] +name = "certifi" +version = "2026.5.20" +source = { registry = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/simple/" } +sdist = { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/certifi/certifi-2026.5.20.tar.gz", hash = "sha256:69dea482ab64caa7b9f6aba1c6bf48bb6a5448d1c0f1b17ab42ad8c763a5344d" } +wheels = [ + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/certifi/certifi-2026.5.20-py3-none-any.whl", hash = "sha256:3c52e209ba0a4ad7aebe60436a4ab349c39e1e602e8c134221e546902ad25897" }, +] + +[[package]] +name = "cffi" +version = "2.0.0" +source = { registry = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/simple/" } +dependencies = [ + { name = "pycparser", marker = "implementation_name != 'PyPy'" }, +] +sdist = { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529" } +wheels = [ + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.0.0-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.0.0-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:53f77cbe57044e88bbd5ed26ac1d0514d2acf0591dd6bb02a3ae37f76811b80c" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.0.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3e837e369566884707ddaf85fc1744b47575005c0a229de3327f8f9a20f4efeb" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.0.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5eda85d6d1879e692d546a078b44251cdd08dd1cfb98dfb77b670c97cee49ea0" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.0.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9332088d75dc3241c702d852d4671613136d90fa6881da7d770a483fd05248b4" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.0.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cf364028c016c03078a23b503f02058f1814320a56ad535686f90565636a9495" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.0.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e11e82b744887154b182fd3e7e8512418446501191994dbf9c9fc1f32cc8efd5" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8ea985900c5c95ce9db1745f7933eeef5d314f0565b27625d9a10ec9881e1bfb" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.0.0-cp310-cp310-win32.whl", hash = "sha256:1f72fb8906754ac8a2cc3f9f5aaa298070652a0ffae577e0ea9bd480dc3c931a" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:b18a3ed7d5b3bd8d9ef7a8cb226502c6bf8308df1525e1cc676c3680e7176739" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.7" +source = { registry = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/simple/" } +sdist = { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.4.7.tar.gz", hash = "sha256:ae89db9e5f98a11a4bf50407d4363e7b09b31e55bc117b4f7d80aab97ba009e5" } +wheels = [ + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.4.7-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cdd68a1fb318e290a2077696b7eb7a21a49163c455979c639bf5a5dcdc46617d" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.4.7-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e17b8d5d6a8c47c85e68ca8379def1303fd360c3e22093a807cd34a71cd082b8" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.4.7-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:511ef87c8aec0783e08ac18565a16d435372bc1ac25a91e6ac7f5ef2b0bff790" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.4.7-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:007d05ec7321d12a40227aae9e2bc6dca73f3cb21058999a1df9e193555a9dcc" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.4.7-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cf29836da5119f3c8a8a70667b0ef5fdca3bb12f80fd06487cfa575b3909b393" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.4.7-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:12d8baf840cc7889b37c7c770f478adea7adce3dcb3944d02ec87508e2dcf153" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.4.7-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d560742f3c0d62afaccf9f41fe485ed69bd7661a241f86a3ef0f0fb8b1a397af" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b14b2d9dac08e28bb8046a1a0434b1750eb221c8f5b87a68f4fa11a6f97b5e34" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:bc17a677b21b3502a21f66a8cc64f5bfad4df8a0b8434d661666f8ce90ac3af1" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:750e02e074872a3fad7f233b47734166440af3cdea0add3e95163110816d6752" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:4e5163c14bffd570ef2affbfdd77bba66383890797df43dc8b4cc7d6f500bf53" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6ed74185b2db44f41ef35fd1617c5888e59792da9bbc9190d6c7300617182616" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:94e1885b270625a9a828c9793b4d52a64445299baa1fea5a173bf1d3dd9a1a5a" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.4.7-cp310-cp310-win32.whl", hash = "sha256:6785f414ae0f3c733c437e0f3929197934f526d19dfaa75e18fdb4f94c6fb374" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.4.7-cp310-cp310-win_amd64.whl", hash = "sha256:6696b7688f54f5af4462118f0bfa7c1621eeb87154f77fa04b9295ce7a8f2943" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.4.7-cp310-cp310-win_arm64.whl", hash = "sha256:66671f93accb62ed07da56613636f3641f1a12c13046ce91ffc923721f23c008" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.4.7-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7641bb8895e77f921102f72833904dcd9901df5d6d72a2ab8f31d04b7e51e4e7" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:202389074300232baeb53ae2569a60901f7efadd4245cf3a3bf0617d60b439d7" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:30b8d1d8c52a48c2c5690e152c169b673487a2a58de1ec7393196753063fcd5e" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:532bc9bf33a68613fd7d65e4b1c71a6a38d7d42604ecf239c77392e9b4e8998c" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2fe249cb4651fd12605b7288b24751d8bfd46d35f12a20b1ba33dea122e690df" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.4.7-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:65bcd23054beab4d166035cabbc868a09c1a49d1efe458fe8e4361215df40265" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.4.7-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:08e721811161356f97b4059a9ba7bafb23ea5ee2255402c42881c214e173c6b4" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e060d01aec0a910bdccb8be71faf34e7799ce36950f8294c8bf612cba65a2c9e" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:38c0109396c4cfc574d502df99742a45c72c08eff0a36158b6f04000043dbf38" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:1c2a768fdd44ee4a9339a9b0b130049139b8ce3c01d2ce09f67f5a68048d477c" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:1a87ca9d5df6fe460483d9a5bbf2b18f620cbed41b432e2bddb686228282d10b" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:d635aab80466bc95771bb78d5370e74d36d1fe31467b6b29b8b57b2a3cd7d22c" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ae196f021b5e7c78e918242d217db021ed2a6ace2bc6ae94c0fc596221c7f58d" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.4.7-cp311-cp311-win32.whl", hash = "sha256:adb2597b428735679446b46c8badf467b4ca5f5056aae4d51a19f9570301b1ad" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.4.7-cp311-cp311-win_amd64.whl", hash = "sha256:8e385e4267ab76874ae30db04c627faaaf0b509e1ccc11a95b3fc3e83f855c00" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.4.7-cp311-cp311-win_arm64.whl", hash = "sha256:d4a48e5b3c2a489fae013b7589308a40146ee081f6f509e047e0e096084ceca1" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.4.7-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:eca9705049ad3c7345d574e3510665cb2cf844c2f2dcfe675332677f081cbd46" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6178f72c5508bfc5fd446a5905e698c6212932f25bcdd4b47a757a50605a90e2" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e1421b502d83040e6d7fb2fb18dff63957f720da3d77b2fbd3187ceb63755d7b" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:edac0f1ab77644605be2cbba52e6b7f630731fc42b34cb0f634be1a6eface56a" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5649fd1c7bade02f320a462fdefd0b4bd3ce036065836d4f42e0de958038e116" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:203104ed3e428044fd943bc4bf45fa73c0730391f9621e37fe39ecf477b128cb" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:298930cec56029e05497a76988377cbd7457ba864beeea92ad7e844fe74cd1f1" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:708838739abf24b2ceb208d0e22403dd018faeef86ddac04319a62ae884c4f15" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:0f7eb884681e3938906ed0434f20c63046eacd0111c4ba96f27b76084cd679f5" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4dc1e73c36828f982bfe79fadf5919923f8a6f4df2860804db9a98c48824ce8d" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:aed52fea0513bac0ccde438c188c8a471c4e0f457c2dd20cdbf6ea7a450046c7" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:fea24543955a6a729c45a73fe90e08c743f0b3334bbf3201e6c4bc1b0c7fa464" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:bb6d88045545b26da47aa879dd4a89a71d1dce0f0e549b1abcb31dfe4a8eac49" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.4.7-cp312-cp312-win32.whl", hash = "sha256:2257141f39fe65a3fdf38aeccae4b953e5f3b3324f4ff0daf9f15b8518666a2c" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.4.7-cp312-cp312-win_amd64.whl", hash = "sha256:5ed6ab538499c8644b8a3e18debabcd7ce684f3fa91cf867521a7a0279cab2d6" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.4.7-cp312-cp312-win_arm64.whl", hash = "sha256:56be790f86bfb2c98fb742ce566dfb4816e5a83384616ab59c49e0604d49c51d" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.4.7-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f496c9c3cc02230093d8330875c4c3cdfc3b73612a5fd921c65d39cbcef08063" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ea948db76d31190bf08bd371623927ee1339d5f2a0b4b1b4a4439a65298703c" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a277ab8928b9f299723bc1a2dabb1265911b1a76341f90a510368ca44ad9ab66" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3bec022aec2c514d9cf199522a802bd007cd588ab17ab2525f20f9c34d067c18" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e044c39e41b92c845bc815e5ae4230804e8e7bc29e399b0437d64222d92809dd" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:f495a1652cf3fbab2eb0639776dad966c2fb874d79d87ca07f9d5f059b8bd215" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e712b419df8ba5e42b226c510472b37bd57b38e897d3eca5e8cfd410a29fa859" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7804338df6fcc08105c7745f1502ba68d900f45fd770d5bdd5288ddccb8a42d8" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:481551899c856c704d58119b5025793fa6730adda3571971af568f66d2424bb5" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f59099f9b66f0d7145115e6f80dd8b1d847176df89b234a5a6b3f00437aa0832" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:f59ad4c0e8f6bba240a9bb85504faa1ab438237199d4cce5f622761507b8f6a6" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:3dedcc22d73ec993f42055eff4fcfed9318d1eeb9a6606c55892a26964964e48" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:64f02c6841d7d83f832cd97ccf8eb8a906d06eb95d5276069175c696b024b60a" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.4.7-cp313-cp313-win32.whl", hash = "sha256:4042d5c8f957e15221d423ba781e85d553722fc4113f523f2feb7b188cc34c5e" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.4.7-cp313-cp313-win_amd64.whl", hash = "sha256:3946fa46a0cf3e4c8cb1cc52f56bb536310d34f25f01ca9b6c16afa767dab110" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.4.7-cp313-cp313-win_arm64.whl", hash = "sha256:80d04837f55fc81da168b98de4f4b797ef007fc8a79ab71c6ec9bc4dd662b15b" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.4.7-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:c36c333c39be2dbca264d7803333c896ab8fa7d4d6f0ab7edb7dfd7aea6e98c0" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1c2aed2e5e41f24ea8ef1590b8e848a79b56f3a5564a65ceec43c9d692dc7d8a" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:54523e136b8948060c0fa0bc7b1b50c32c186f2fceee897a495406bb6e311d2b" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:715479b9a2802ecac752a3b0efa2b0b60285cf962ee38414211abdfccc233b41" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bd6c2a1c7573c64738d716488d2cdd3c00e340e4835707d8fdb8dc1a66ef164e" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:c45e9440fb78f8ddabcf714b68f936737a121355bf59f3907f4e17721b9d1aae" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3534e7dcbdcf757da6b85a0bbf5b6868786d5982dd959b065e65481644817a18" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e8ac484bf18ce6975760921bb6148041faa8fef0547200386ea0b52b5d27bf7b" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a5fe03b42827c13cdccd08e6c0247b6a6d4b5e3cdc53fd1749f5896adcdc2356" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:2d6eb928e13016cea4f1f21d1e10c1cebd5a421bc57ddf5b1142ae3f86824fab" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e74327fb75de8986940def6e8dee4f127cc9752bee7355bb323cc5b2659b6d46" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:d6038d37043bced98a66e68d3aa2b6a35505dc01328cd65217cefe82f25def44" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7579e913a5339fb8fa133f6bbcfd8e6749696206cf05acdbdca71a1b436d8e72" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.4.7-cp314-cp314-win32.whl", hash = "sha256:5b77459df20e08151cd6f8b9ef8ef1f961ef73d85c21a555c7eed5b79410ec10" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.4.7-cp314-cp314-win_amd64.whl", hash = "sha256:92a0a01ead5e668468e952e4238cccd7c537364eb7d851ab144ab6627dbbe12f" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.4.7-cp314-cp314-win_arm64.whl", hash = "sha256:67f6279d125ca0046a7fd386d01b311c6363844deac3e5b069b514ba3e63c246" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.4.7-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:effc3f449787117233702311a1b7d8f59cba9ced946ba727bdc329ec69028e24" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fbccdc05410c9ee21bbf16a35f4c1d16123dcdeb8a1d38f33654fa21d0234f79" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:733784b6d6def852c814bce5f318d25da2ee65dd4839a0718641c696e09a2960" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a89c23ef8d2c6b27fd200a42aa4ac72786e7c60d40efdc76e6011260b6e949c4" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6c114670c45346afedc0d947faf3c7f701051d2518b943679c8ff88befe14f8e" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:a180c5e59792af262bf263b21a3c49353f25945d8d9f70628e73de370d55e1e1" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3c9a494bc5ec77d43cea229c4f6db1e4d8fe7e1bbffa8b6f0f0032430ff8ab44" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8d828b6667a32a728a1ad1d93957cdf37489c57b97ae6c4de2860fa749b8fc1e" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:cf1493cd8607bec4d8a7b9b004e699fcf8f9103a9284cc94962cb73d20f9d4a3" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0c96c3b819b5c3e9e165495db84d41914d6894d55181d2d108cc1a69bfc9cce0" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:752a45dc4a6934060b3b0dab47e04edc3326575f82be64bc4fc293914566503e" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:8778f0c7a52e56f75d12dae53ae320fae900a8b9b4164b981b9c5ce059cd1fcb" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ce3412fbe1e31eb81ea42f4169ed94861c56e643189e1e75f0041f3fe7020abe" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.4.7-cp314-cp314t-win32.whl", hash = "sha256:c03a41a8784091e67a39648f70c5f97b5b6a37f216896d44d2cdcb82615339a0" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.4.7-cp314-cp314t-win_amd64.whl", hash = "sha256:03853ed82eeebbce3c2abfdbc98c96dc205f32a79627688ac9a27370ea61a49c" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.4.7-cp314-cp314t-win_arm64.whl", hash = "sha256:c35abb8bfff0185efac5878da64c45dafd2b37fb0383add1be155a763c1f083d" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.4.7-py3-none-any.whl", hash = "sha256:3dce51d0f5e7951f8bb4900c257dad282f49190fdbebecd4ba99bcc41fef404d" }, +] + +[[package]] +name = "click" +version = "8.4.1" +source = { registry = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/simple/" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/click/click-8.4.1.tar.gz", hash = "sha256:918b5633eddf6b41c32d4f454bf0de810065c74e3f7dbf8ee5452f8be88d3e96" } +wheels = [ + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/click/click-8.4.1-py3-none-any.whl", hash = "sha256:482be17c6991b8c19c5429a1e995d9b0efdbb63172824c41f99965dc0ade8ec2" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/simple/" } +sdist = { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/colorama/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44" } +wheels = [ + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/colorama/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6" }, +] + +[[package]] +name = "cryptography" +version = "48.0.0" +source = { registry = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/simple/" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/cryptography/cryptography-48.0.0.tar.gz", hash = "sha256:5c3932f4436d1cccb036cb0eaef46e6e2db91035166f1ad6505c3c9d5a635920" } +wheels = [ + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/cryptography/cryptography-48.0.0-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:0c558d2cdffd8f4bbb30fc7134c74d2ca9a476f830bb053074498fbc86f41ed6" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/cryptography/cryptography-48.0.0-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f5333311663ea94f75dd408665686aaf426563556bb5283554a3539177e03b8c" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/cryptography/cryptography-48.0.0-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7995ef305d7165c3f11ae07f2517e5a4f1d5c18da1376a0a9ed496336b69e5f3" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/cryptography/cryptography-48.0.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:40ba1f85eaa6959837b1d51c9767e230e14612eea4ef110ee8854ada22da1bf5" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/cryptography/cryptography-48.0.0-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:369a6348999f94bbd53435c894377b20ab95f25a9065c283570e70150d8abc3c" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/cryptography/cryptography-48.0.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:a0e692c683f4df67815a2d258b324e66f4738bd7a96a218c826dce4f4bd05d8f" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/cryptography/cryptography-48.0.0-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:18349bbc56f4743c8b12dc32e2bccb2cf83ee8b69a3bba74ef8ae857e26b3d25" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/cryptography/cryptography-48.0.0-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:7e8eac43dfca5c4cccc6dad9a80504436fca53bb9bc3100a2386d730fbe6b602" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/cryptography/cryptography-48.0.0-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:9ccdac7d40688ecb5a3b4a604b8a88c8002e3442d6c60aead1db2a89a041560c" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/cryptography/cryptography-48.0.0-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:bd72e68b06bb1e96913f97dd4901119bc17f39d4586a5adf2d3e47bc2b9d58b5" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/cryptography/cryptography-48.0.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:59baa2cb386c4f0b9905bd6eb4c2a79a69a128408fd31d32ca4d7102d4156321" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/cryptography/cryptography-48.0.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:9249e3cd978541d665967ac2cb2787fd6a62bddf1e75b3e347a594d7dacf4f74" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/cryptography/cryptography-48.0.0-cp311-abi3-win32.whl", hash = "sha256:9c459db21422be75e2809370b829a87eb37f74cd785fc4aa9ea1e5f43b47cda4" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/cryptography/cryptography-48.0.0-cp311-abi3-win_amd64.whl", hash = "sha256:5b012212e08b8dd5edc78ef54da83dd9892fd9105323b3993eff6bea65dc21d7" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/cryptography/cryptography-48.0.0-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:3cb07a3ed6431663cd321ea8a000a1314c74211f823e4177fefa2255e057d1ec" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/cryptography/cryptography-48.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8c7378637d7d88016fa6791c159f698b3d3eed28ebf844ac36b9dc04a14dae18" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/cryptography/cryptography-48.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cc90c0b39b2e3c65ef52c804b72e3c58f8a04ab2a1871272798e5f9572c17d20" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/cryptography/cryptography-48.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:76341972e1eff8b4bea859f09c0d3e64b96ce931b084f9b9b7db8ef364c30eff" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/cryptography/cryptography-48.0.0-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:55b7718303bf06a5753dcdccf2f3945cf18ad7bffde41b61226e4db31ab89a9c" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/cryptography/cryptography-48.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:a64697c641c7b1b2178e573cbc31c7c6684cd56883a478d75143dbb7118036db" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/cryptography/cryptography-48.0.0-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:561215ea3879cb1cbbf272867e2efda62476f240fb58c64de6b393ae19246741" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/cryptography/cryptography-48.0.0-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:ad64688338ed4bc1a6618076ba75fd7194a5f1797ac60b47afe926285adb3166" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/cryptography/cryptography-48.0.0-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:906cbf0670286c6e0044156bc7d4af9cbb0ef6db9f73e52c3ec56ba6bdde5336" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/cryptography/cryptography-48.0.0-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:ea8990436d914540a40ab24b6a77c0969695ed52f4a4874c5137ccf7045a7057" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/cryptography/cryptography-48.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c18684a7f0cc9a3cb60328f496b8e3372def7c5d2df39ac267878b05565aaaae" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/cryptography/cryptography-48.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9be5aafa5736574f8f15f262adc81b2a9869e2cfe9014d52a44633905b40d52c" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/cryptography/cryptography-48.0.0-cp314-cp314t-win32.whl", hash = "sha256:c17dfe85494deaeddc5ce251aebd1d60bbe6afc8b62071bb0b469431a000124f" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/cryptography/cryptography-48.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27241b1dc9962e056062a8eef1991d02c3a24569c95975bd2322a8a52c6e5e12" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/cryptography/cryptography-48.0.0-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:58d00498e8933e4a194f3076aee1b4a97dfec1a6da444535755822fe5d8b0b86" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/cryptography/cryptography-48.0.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:614d0949f4790582d2cc25553abd09dd723025f0c0e7c67376a1d77196743d6e" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/cryptography/cryptography-48.0.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7ce4bfae76319a532a2dc68f82cc32f5676ee792a983187dac07183690e5c66f" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/cryptography/cryptography-48.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:2eb992bbd4661238c5a397594c83f5b4dc2bc5b848c365c8f991b6780efcc5c7" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/cryptography/cryptography-48.0.0-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:22a5cb272895dce158b2cacdfdc3debd299019659f42947dbdac6f32d68fe832" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/cryptography/cryptography-48.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:2b4d59804e8408e2fea7d1fbaf218e5ec984325221db76e6a241a9abd6cdd95c" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/cryptography/cryptography-48.0.0-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:984a20b0f62a26f48a3396c72e4bc34c66e356d356bf370053066b3b6d54634a" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/cryptography/cryptography-48.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:5a5ed8fde7a1d09376ca0b40e68cd59c69fe23b1f9768bd5824f54681626032a" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/cryptography/cryptography-48.0.0-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:8cd666227ef7af430aa5914a9910e0ddd703e75f039cef0825cd0da71b6b711a" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/cryptography/cryptography-48.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:9071196d81abc88b3516ac8cdfad32e2b66dd4a5393a8e68a961e9161ddc6239" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/cryptography/cryptography-48.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1e2d54c8be6152856a36f0882ab231e70f8ec7f14e93cf87db8a2ed056bf160c" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/cryptography/cryptography-48.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a5da777e32ffed6f85a7b2b3f7c5cbc88c146bfcd0a1d7baf5fcc6c52ee35dd4" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/cryptography/cryptography-48.0.0-cp39-abi3-win32.whl", hash = "sha256:77a2ccbbe917f6710e05ba9adaa25fb5075620bf3ea6fb751997875aff4ae4bd" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/cryptography/cryptography-48.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:16cd65b9330583e4619939b3a3843eec1e6e789744bb01e7c7e2e62e33c239c8" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/cryptography/cryptography-48.0.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:84cf79f0dc8b36ac5da873481716e87aef31fcfa0444f9e1d8b4b2cece142855" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/cryptography/cryptography-48.0.0-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:fdfef35d751d510fcef5252703621574364fec16418c4a1e5e1055248401054b" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/cryptography/cryptography-48.0.0-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:0890f502ddf7d9c6426129c3f49f5c0a39278ed7cd6322c8755ffca6ee675a13" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/cryptography/cryptography-48.0.0-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:ecde28a596bead48b0cfd2a1b4416c3d43074c2d785e3a398d7ec1fc4d0f7fbb" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/cryptography/cryptography-48.0.0-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:4defde8685ae324a9eb9d818717e93b4638ef67070ac9bc15b8ca85f63048355" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/cryptography/cryptography-48.0.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:db63bf618e5dea46c07de12e900fe1cdd2541e6dc9dbae772a70b7d4d4765f6a" }, +] + +[[package]] +name = "distro" +version = "1.9.0" +source = { registry = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/simple/" } +sdist = { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/distro/distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed" } +wheels = [ + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/distro/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2" }, +] + +[[package]] +name = "exceptiongroup" +version = "1.3.1" +source = { registry = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/simple/" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/exceptiongroup/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219" } +wheels = [ + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/exceptiongroup/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598" }, +] + +[[package]] +name = "fastapi" +version = "0.136.3" +source = { registry = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/simple/" } +dependencies = [ + { name = "annotated-doc" }, + { name = "pydantic" }, + { name = "starlette" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/fastapi/fastapi-0.136.3.tar.gz", hash = "sha256:e487fae93ad408e6f47641ee4dfe389864fd7bec92e547ea8498fc13f43e83ab" } +wheels = [ + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/fastapi/fastapi-0.136.3-py3-none-any.whl", hash = "sha256:3d2a69bdf04b7e9f3afa292c3bc7a98816bbfafa10bc9b45f3f3700d2f761620" }, +] + +[[package]] +name = "ge-migration-agent" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "google-adk" }, + { name = "google-auth" }, + { name = "google-cloud-storage" }, + { name = "python-dotenv" }, + { name = "requests" }, +] + +[package.metadata] +requires-dist = [ + { name = "google-adk" }, + { name = "google-auth", specifier = ">=2.20.0" }, + { name = "google-cloud-storage", specifier = ">=2.10.0" }, + { name = "python-dotenv", specifier = ">=1.0.0" }, + { name = "requests", specifier = ">=2.31.0" }, +] + +[[package]] +name = "google-adk" +version = "2.2.0" +source = { registry = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/simple/" } +dependencies = [ + { name = "aiosqlite" }, + { name = "authlib" }, + { name = "click" }, + { name = "fastapi" }, + { name = "google-auth", extra = ["pyopenssl"] }, + { name = "google-genai" }, + { name = "graphviz" }, + { name = "httpx" }, + { name = "jsonschema" }, + { name = "opentelemetry-api" }, + { name = "opentelemetry-sdk" }, + { name = "packaging" }, + { name = "pydantic" }, + { name = "python-dotenv" }, + { name = "python-multipart" }, + { name = "pyyaml" }, + { name = "requests" }, + { name = "starlette" }, + { name = "tenacity" }, + { name = "typing-extensions" }, + { name = "tzlocal" }, + { name = "uvicorn" }, + { name = "watchdog" }, + { name = "websockets" }, +] +sdist = { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/google-adk/google_adk-2.2.0.tar.gz", hash = "sha256:04cb6318aba8829fe7c941ee1b456ccb4745253898c13595708c9eb07b4582ff" } +wheels = [ + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/google-adk/google_adk-2.2.0-py3-none-any.whl", hash = "sha256:ebdf3d931dc2b9c5b30d995358fc2ae99d59594c48a4aaf7496869ccd2c5f245" }, +] + +[[package]] +name = "google-api-core" +version = "2.31.0" +source = { registry = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/simple/" } +dependencies = [ + { name = "google-auth" }, + { name = "googleapis-common-protos" }, + { name = "proto-plus" }, + { name = "protobuf" }, + { name = "requests" }, +] +sdist = { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/google-api-core/google_api_core-2.31.0.tar.gz", hash = "sha256:2be84ee0f584c48e6bde1b36766e23348b361fb7e55e56135fc76ce1c397f9c2" } +wheels = [ + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/google-api-core/google_api_core-2.31.0-py3-none-any.whl", hash = "sha256:ef79fb3784c71cbac89cbd03301ba0c8fb8ad2aa95d7f9204dd9628f7adf59ab" }, +] + +[[package]] +name = "google-auth" +version = "2.53.0" +source = { registry = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/simple/" } +dependencies = [ + { name = "cryptography" }, + { name = "pyasn1-modules" }, +] +sdist = { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/google-auth/google_auth-2.53.0.tar.gz", hash = "sha256:e7e6aa16f6bee7b2b264830fd04f08087a1d5a836df516251a5d15327b246c9c" } +wheels = [ + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/google-auth/google_auth-2.53.0-py3-none-any.whl", hash = "sha256:6e7449917c599b35126a99ec268ec6880301f2fea41dce198fe8fd83ff642b68" }, +] + +[package.optional-dependencies] +pyopenssl = [ + { name = "pyopenssl" }, +] +requests = [ + { name = "requests" }, +] + +[[package]] +name = "google-cloud-core" +version = "2.6.0" +source = { registry = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/simple/" } +dependencies = [ + { name = "google-api-core" }, + { name = "google-auth" }, +] +sdist = { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/google-cloud-core/google_cloud_core-2.6.0.tar.gz", hash = "sha256:e76149739f90fac1fc6757c09f47eaccb3145b54adbd7759b0f7c4b235f46c83" } +wheels = [ + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/google-cloud-core/google_cloud_core-2.6.0-py3-none-any.whl", hash = "sha256:6d63ac8e5eca6d9e4319d0a1e2265fadcd7f1049904378caecfa01cf52dd869e" }, +] + +[[package]] +name = "google-cloud-storage" +version = "3.11.0" +source = { registry = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/simple/" } +dependencies = [ + { name = "google-api-core" }, + { name = "google-auth" }, + { name = "google-cloud-core" }, + { name = "google-crc32c" }, + { name = "google-resumable-media" }, + { name = "requests" }, +] +sdist = { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/google-cloud-storage/google_cloud_storage-3.11.0.tar.gz", hash = "sha256:498bf37c999028f69a245f586b5e50d89f59df1fafc0e3a93783ac56be2a456b" } +wheels = [ + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/google-cloud-storage/google_cloud_storage-3.11.0-py3-none-any.whl", hash = "sha256:cfcc33aa6b899ec9dd1771286f8e79fbed5c35c1c174718071b079aa827f37c2" }, +] + +[[package]] +name = "google-crc32c" +version = "1.8.0" +source = { registry = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/simple/" } +sdist = { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/google-crc32c/google_crc32c-1.8.0.tar.gz", hash = "sha256:a428e25fb7691024de47fecfbff7ff957214da51eddded0da0ae0e0f03a2cf79" } +wheels = [ + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/google-crc32c/google_crc32c-1.8.0-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:0470b8c3d73b5f4e3300165498e4cf25221c7eb37f1159e221d1825b6df8a7ff" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/google-crc32c/google_crc32c-1.8.0-cp310-cp310-macosx_12_0_x86_64.whl", hash = "sha256:119fcd90c57c89f30040b47c211acee231b25a45d225e3225294386f5d258288" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/google-crc32c/google_crc32c-1.8.0-cp310-cp310-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6f35aaffc8ccd81ba3162443fabb920e65b1f20ab1952a31b13173a67811467d" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/google-crc32c/google_crc32c-1.8.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:864abafe7d6e2c4c66395c1eb0fe12dc891879769b52a3d56499612ca93b6092" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/google-crc32c/google_crc32c-1.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:db3fe8eaf0612fc8b20fa21a5f25bd785bc3cd5be69f8f3412b0ac2ffd49e733" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/google-crc32c/google_crc32c-1.8.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:014a7e68d623e9a4222d663931febc3033c5c7c9730785727de2a81f87d5bab8" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/google-crc32c/google_crc32c-1.8.0-cp311-cp311-macosx_12_0_x86_64.whl", hash = "sha256:86cfc00fe45a0ac7359e5214a1704e51a99e757d0272554874f419f79838c5f7" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/google-crc32c/google_crc32c-1.8.0-cp311-cp311-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:19b40d637a54cb71e0829179f6cb41835f0fbd9e8eb60552152a8b52c36cbe15" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/google-crc32c/google_crc32c-1.8.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:17446feb05abddc187e5441a45971b8394ea4c1b6efd88ab0af393fd9e0a156a" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/google-crc32c/google_crc32c-1.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:71734788a88f551fbd6a97be9668a0020698e07b2bf5b3aa26a36c10cdfb27b2" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/google-crc32c/google_crc32c-1.8.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:4b8286b659c1335172e39563ab0a768b8015e88e08329fa5321f774275fc3113" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/google-crc32c/google_crc32c-1.8.0-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:2a3dc3318507de089c5384cc74d54318401410f82aa65b2d9cdde9d297aca7cb" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/google-crc32c/google_crc32c-1.8.0-cp312-cp312-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:14f87e04d613dfa218d6135e81b78272c3b904e2a7053b841481b38a7d901411" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/google-crc32c/google_crc32c-1.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cb5c869c2923d56cb0c8e6bcdd73c009c36ae39b652dbe46a05eb4ef0ad01454" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/google-crc32c/google_crc32c-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:3cc0c8912038065eafa603b238abf252e204accab2a704c63b9e14837a854962" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/google-crc32c/google_crc32c-1.8.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:3ebb04528e83b2634857f43f9bb8ef5b2bbe7f10f140daeb01b58f972d04736b" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/google-crc32c/google_crc32c-1.8.0-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:450dc98429d3e33ed2926fc99ee81001928d63460f8538f21a5d6060912a8e27" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/google-crc32c/google_crc32c-1.8.0-cp313-cp313-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:3b9776774b24ba76831609ffbabce8cdf6fa2bd5e9df37b594221c7e333a81fa" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/google-crc32c/google_crc32c-1.8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:89c17d53d75562edfff86679244830599ee0a48efc216200691de8b02ab6b2b8" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/google-crc32c/google_crc32c-1.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:57a50a9035b75643996fbf224d6661e386c7162d1dfdab9bc4ca790947d1007f" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/google-crc32c/google_crc32c-1.8.0-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:e6584b12cb06796d285d09e33f63309a09368b9d806a551d8036a4207ea43697" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/google-crc32c/google_crc32c-1.8.0-cp314-cp314-macosx_12_0_x86_64.whl", hash = "sha256:f4b51844ef67d6cf2e9425983274da75f18b1597bb2c998e1c0a0e8d46f8f651" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/google-crc32c/google_crc32c-1.8.0-cp314-cp314-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b0d1a7afc6e8e4635564ba8aa5c0548e3173e41b6384d7711a9123165f582de2" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/google-crc32c/google_crc32c-1.8.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8b3f68782f3cbd1bce027e48768293072813469af6a61a86f6bb4977a4380f21" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/google-crc32c/google_crc32c-1.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:d511b3153e7011a27ab6ee6bb3a5404a55b994dc1a7322c0b87b29606d9790e2" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/google-crc32c/google_crc32c-1.8.0-pp311-pypy311_pp73-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:87fa445064e7db928226b2e6f0d5304ab4cd0339e664a4e9a25029f384d9bb93" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/google-crc32c/google_crc32c-1.8.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f639065ea2042d5c034bf258a9f085eaa7af0cd250667c0635a3118e8f92c69c" }, +] + +[[package]] +name = "google-genai" +version = "2.8.0" +source = { registry = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/simple/" } +dependencies = [ + { name = "anyio" }, + { name = "distro" }, + { name = "google-auth", extra = ["requests"] }, + { name = "httpx" }, + { name = "pydantic" }, + { name = "requests" }, + { name = "sniffio" }, + { name = "tenacity" }, + { name = "typing-extensions" }, + { name = "websockets" }, +] +sdist = { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/google-genai/google_genai-2.8.0.tar.gz", hash = "sha256:37a9b3cb127d763e7f4ca47452ae3562c87728773bd1b149f7b559c239da2bc1" } +wheels = [ + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/google-genai/google_genai-2.8.0-py3-none-any.whl", hash = "sha256:4da0a223a100f4b37f609a68b835e3326ab0fa313314dc0fd9d34e76ee293844" }, +] + +[[package]] +name = "google-resumable-media" +version = "2.10.0" +source = { registry = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/simple/" } +dependencies = [ + { name = "google-crc32c" }, +] +sdist = { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/google-resumable-media/google_resumable_media-2.10.0.tar.gz", hash = "sha256:e324bc9d0fdae4c52a08ae90456edc4e71ece858399e1217ac0eb3a51d6bc6ee" } +wheels = [ + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/google-resumable-media/google_resumable_media-2.10.0-py3-none-any.whl", hash = "sha256:88152884bee37b2bf36a0ab81ad8c7fd12212c9803dd981d77c1b35b02d34e7c" }, +] + +[[package]] +name = "googleapis-common-protos" +version = "1.75.0" +source = { registry = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/simple/" } +dependencies = [ + { name = "protobuf" }, +] +sdist = { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/googleapis-common-protos/googleapis_common_protos-1.75.0.tar.gz", hash = "sha256:53a062ff3c32552fbd62c11fe23768b78e4ddf0494d5e5fd97d3f4689c75fbbd" } +wheels = [ + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/googleapis-common-protos/googleapis_common_protos-1.75.0-py3-none-any.whl", hash = "sha256:961ed60399c457ceb0ee8f285a84c870aabc9c6a832b9d37bb281b5bebde43ed" }, +] + +[[package]] +name = "graphviz" +version = "0.21" +source = { registry = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/simple/" } +sdist = { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/graphviz/graphviz-0.21.tar.gz", hash = "sha256:20743e7183be82aaaa8ad6c93f8893c923bd6658a04c32ee115edb3c8a835f78" } +wheels = [ + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/graphviz/graphviz-0.21-py3-none-any.whl", hash = "sha256:54f33de9f4f911d7e84e4191749cac8cc5653f815b06738c54db9a15ab8b1e42" }, +] + +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/simple/" } +sdist = { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/h11/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1" } +wheels = [ + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/h11/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86" }, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/simple/" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/httpcore/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8" } +wheels = [ + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/httpcore/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55" }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/simple/" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/httpx/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc" } +wheels = [ + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/httpx/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad" }, +] + +[[package]] +name = "idna" +version = "3.18" +source = { registry = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/simple/" } +sdist = { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/idna/idna-3.18.tar.gz", hash = "sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848" } +wheels = [ + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/idna/idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2" }, +] + +[[package]] +name = "importlib-metadata" +version = "8.7.1" +source = { registry = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/simple/" } +dependencies = [ + { name = "zipp" }, +] +sdist = { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/importlib-metadata/importlib_metadata-8.7.1.tar.gz", hash = "sha256:49fef1ae6440c182052f407c8d34a68f72efc36db9ca90dc0113398f2fdde8bb" } +wheels = [ + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/importlib-metadata/importlib_metadata-8.7.1-py3-none-any.whl", hash = "sha256:5a1f80bf1daa489495071efbb095d75a634cf28a8bc299581244063b53176151" }, +] + +[[package]] +name = "joserfc" +version = "1.7.0" +source = { registry = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/simple/" } +dependencies = [ + { name = "cryptography" }, +] +sdist = { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/joserfc/joserfc-1.7.0.tar.gz", hash = "sha256:4aced6ab0c47846f0a531402aec2419a874b91e918df9c4c9da8a82fb559d6c4" } +wheels = [ + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/joserfc/joserfc-1.7.0-py3-none-any.whl", hash = "sha256:17e5d7a5a35e65442b05efc435a3d5d46696ffa2c8a2ed0eea6f63fc268e3224" }, +] + +[[package]] +name = "jsonschema" +version = "4.26.0" +source = { registry = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/simple/" } +dependencies = [ + { name = "attrs" }, + { name = "jsonschema-specifications" }, + { name = "referencing" }, + { name = "rpds-py", version = "0.30.0", source = { registry = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/simple/" }, marker = "python_full_version < '3.11'" }, + { name = "rpds-py", version = "2026.5.1", source = { registry = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/simple/" }, marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/jsonschema/jsonschema-4.26.0.tar.gz", hash = "sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326" } +wheels = [ + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/jsonschema/jsonschema-4.26.0-py3-none-any.whl", hash = "sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce" }, +] + +[[package]] +name = "jsonschema-specifications" +version = "2025.9.1" +source = { registry = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/simple/" } +dependencies = [ + { name = "referencing" }, +] +sdist = { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/jsonschema-specifications/jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d" } +wheels = [ + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/jsonschema-specifications/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe" }, +] + +[[package]] +name = "opentelemetry-api" +version = "1.41.1" +source = { registry = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/simple/" } +dependencies = [ + { name = "importlib-metadata" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/opentelemetry-api/opentelemetry_api-1.41.1.tar.gz", hash = "sha256:0ad1814d73b875f84494387dae86ce0b12c68556331ce6ce8fe789197c949621" } +wheels = [ + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/opentelemetry-api/opentelemetry_api-1.41.1-py3-none-any.whl", hash = "sha256:a22df900e75c76dc08440710e51f52f1aa6b451b429298896023e60db5b3139f" }, +] + +[[package]] +name = "opentelemetry-sdk" +version = "1.41.1" +source = { registry = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/simple/" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-semantic-conventions" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/opentelemetry-sdk/opentelemetry_sdk-1.41.1.tar.gz", hash = "sha256:724b615e1215b5aeacda0abb8a6a8922c9a1853068948bd0bd225a56d0c792e6" } +wheels = [ + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/opentelemetry-sdk/opentelemetry_sdk-1.41.1-py3-none-any.whl", hash = "sha256:edee379c126c1bce952b0c812b48fe8ff35b30df0eecf17e98afa4d598b7d85d" }, +] + +[[package]] +name = "opentelemetry-semantic-conventions" +version = "0.62b1" +source = { registry = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/simple/" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/opentelemetry-semantic-conventions/opentelemetry_semantic_conventions-0.62b1.tar.gz", hash = "sha256:c5cc6e04a7f8c7cdd30be2ed81499fa4e75bfbd52c9cb70d40af1f9cd3619802" } +wheels = [ + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/opentelemetry-semantic-conventions/opentelemetry_semantic_conventions-0.62b1-py3-none-any.whl", hash = "sha256:cf506938103d331fbb78eded0d9788095f7fd59016f2bda813c3324e5a74a93c" }, +] + +[[package]] +name = "packaging" +version = "26.2" +source = { registry = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/simple/" } +sdist = { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/packaging/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661" } +wheels = [ + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/packaging/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e" }, +] + +[[package]] +name = "proto-plus" +version = "1.28.0" +source = { registry = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/simple/" } +dependencies = [ + { name = "protobuf" }, +] +sdist = { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/proto-plus/proto_plus-1.28.0.tar.gz", hash = "sha256:38e5696342835b08fc116f30a25665b29531cda9d5d5643e9b81fc312385abd9" } +wheels = [ + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/proto-plus/proto_plus-1.28.0-py3-none-any.whl", hash = "sha256:a630604310899e73c59ec302e5765c058d412b2f090b9c79c8822589f14955b8" }, +] + +[[package]] +name = "protobuf" +version = "7.35.0" +source = { registry = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/simple/" } +sdist = { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/protobuf/protobuf-7.35.0.tar.gz", hash = "sha256:a2efd84605f41e559f1881b0912b44099d0a2ac9bf46b3474823f10fb393b0e6" } +wheels = [ + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/protobuf/protobuf-7.35.0-cp310-abi3-macosx_10_9_universal2.whl", hash = "sha256:66be6c513931c794fa92c080ffee41671390da3d79da219cf9c0c0907f035dda" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/protobuf/protobuf-7.35.0-cp310-abi3-manylinux2014_aarch64.whl", hash = "sha256:fcbe42a4ac09d3ec9c987ddfcd956afd0b15f1ff613bd8371bde9405ffd5c8e5" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/protobuf/protobuf-7.35.0-cp310-abi3-manylinux2014_s390x.whl", hash = "sha256:4cbf5cc286130e06a6c9bbefac442431173906dfcc979712183d4adcc01b37ee" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/protobuf/protobuf-7.35.0-cp310-abi3-manylinux2014_x86_64.whl", hash = "sha256:6c0f98f10c8a05ea30f8993dfef2de093d27b490fdae78bb60c8343795d55011" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/protobuf/protobuf-7.35.0-cp310-abi3-win32.whl", hash = "sha256:4c4617b83ade0e279d1d2bfe04025a1adb87f9ed657de038620dc0ff959357f6" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/protobuf/protobuf-7.35.0-cp310-abi3-win_amd64.whl", hash = "sha256:f05bcadf9a2a6b8dda047007075135fb7d08c73d9177aabc067e1be46881a201" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/protobuf/protobuf-7.35.0-py3-none-any.whl", hash = "sha256:c13f325cf242bad135c350629eeb5d54b24228eb472fb3e2e9ebbd4c5dc20ca0" }, +] + +[[package]] +name = "pyasn1" +version = "0.6.3" +source = { registry = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/simple/" } +sdist = { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pyasn1/pyasn1-0.6.3.tar.gz", hash = "sha256:697a8ecd6d98891189184ca1fa05d1bb00e2f84b5977c481452050549c8a72cf" } +wheels = [ + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pyasn1/pyasn1-0.6.3-py3-none-any.whl", hash = "sha256:a80184d120f0864a52a073acc6fc642847d0be408e7c7252f31390c0f4eadcde" }, +] + +[[package]] +name = "pyasn1-modules" +version = "0.4.2" +source = { registry = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/simple/" } +dependencies = [ + { name = "pyasn1" }, +] +sdist = { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pyasn1-modules/pyasn1_modules-0.4.2.tar.gz", hash = "sha256:677091de870a80aae844b1ca6134f54652fa2c8c5a52aa396440ac3106e941e6" } +wheels = [ + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pyasn1-modules/pyasn1_modules-0.4.2-py3-none-any.whl", hash = "sha256:29253a9207ce32b64c3ac6600edc75368f98473906e8fd1043bd6b5b1de2c14a" }, +] + +[[package]] +name = "pycparser" +version = "3.0" +source = { registry = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/simple/" } +sdist = { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pycparser/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29" } +wheels = [ + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pycparser/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992" }, +] + +[[package]] +name = "pydantic" +version = "2.13.4" +source = { registry = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/simple/" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pydantic/pydantic-2.13.4.tar.gz", hash = "sha256:c40756b57adaa8b1efeeced5c196f3f3b7c435f90e84ea7f443901bec8099ef6" } +wheels = [ + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pydantic/pydantic-2.13.4-py3-none-any.whl", hash = "sha256:45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba" }, +] + +[[package]] +name = "pydantic-core" +version = "2.46.4" +source = { registry = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/simple/" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4.tar.gz", hash = "sha256:62f875393d7f270851f20523dd2e29f082bcc82292d66db2b64ea71f64b6e1c1" } +wheels = [ + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:a396dcc17e5a0b164dbe026896245a4fa9ff402edca1dff0be3d53a517f74de4" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:da4b951fe36dc7c3a1ccb4e3cd1747c3542b8c9ceede8fc86cae054e764485f5" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb63e0198ca18aad131c089b9204c23079c3afa95487e561f4c522d519e55aba" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f47286a97f0bc9b8859519809077b91b2cefe4ae47fcbf5e466a009c1c5d742b" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:905a0ed8ea6f2d61c1738835f99b699348d7857379083e5fc497fa0c967a407c" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ea793e075b70290d89d8142074262885d3f7da19634845135751bd6344f73b50" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:395aebd9183f9d112f569aeb5b2214d1a10a33bec8456447f7fbdfa51d38d4cd" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:b078afbc25f3a1436c7a1d2cd3e322497ee99615ba97c563566fdf46aff1ee01" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f747929cf940cddb5b3668a390056ddd5ba2e5010615ea2dcf4f9c4f3ab8791d" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:daa27d92c36f24388fe3ad306b174781c747627f134452e4f128ea00ce1fe8c4" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:19e51f073cd3df251856a8a4189fbdf1de4012c3ebacfb1884f94f1eb406079f" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c1747f85cee84c26985853c6f3d9bd3e75da5212912443fa111c113b9c246f39" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-cp310-cp310-win32.whl", hash = "sha256:2f84c03c8607173d16b5a854ec68a2f9079ae03237a54fb506d13af47e1d018d" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-cp310-cp310-win_amd64.whl", hash = "sha256:8358a950c8909158e3df31538a7e4edc2d7265a7c54b47f0864d9e5bae9dcebf" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:0e96592440881c74a213e5ad528e2b24d3d4f940de2766bed9010ab1d9e51594" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e0d65b8c354be7fb5f720c3caa8bc940bc2d20ce749c8e06135f07f8ed95dd7c" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bfb192b3f4b9e8a89b6277b6ce787564f62cfd272055f6e685726b111dc7826" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9037063db01f09b09e237c282b6792bd4da634b5402c4e7f0c61effed7701a04" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fc010ab034c8c7452522748bf937df58020d256ccae0874463d1f4d01758af8e" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c5dac79fa1614d1e06ca695109c6105923bd9c7d1d6c918d4e637b7e6b32fd3" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f9fa868638bf362d3d138ea55829cefb3d5f4b0d7f142234382a15e2485dbec4" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:17299feefe090f2caa5b8e37222bb5f663e4935a8bfa6931d4102e5df1a9f398" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4c63ebc82684aa89d9a3bcbd13d515b3be44250dc68dd3bd81526c1cb31286c3" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:aaa2a54443eff1950ba5ddc6b6ccda0d9c84a364276a62f969bdf2a390650848" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:18e5ceec2ab67e6d5f1a9085e5a24c9c4e2ac4545730bfe668680bca05e555f3" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a0f62d0a58f4e7da165457e995725421e0064f2255d8eccebc49f41bbc23b109" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-cp311-cp311-win32.whl", hash = "sha256:041bde0a48fd37cf71cab1c9d56d3e8625a3793fef1f7dd232b3ff37e978ecda" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-cp311-cp311-win_amd64.whl", hash = "sha256:6f2eeda33a839975441c86a4119e1383c50b47faf0cbb5176985565c6bb02c33" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-cp311-cp311-win_arm64.whl", hash = "sha256:14f4c5d6db102bd796a627bbb3a17b4cf4574b9ae861d8b7c9a9661c6dd3362d" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:3245406455a5d98187ec35530fd772b1d799b26667980872c8d4614991e2c4a2" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:962ccbab7b642487b1d8b7df90ef677e03134cf1fd8880bf698649b22a69371f" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8233f2947cf85404441fd7e0085f53b10c93e0ee78611099b5c7237e36aacbf7" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3a233125ac121aa3ffba9a2b59edfc4a985a76092dc8279586ab4b71390875e7" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b712b53160b79a5850310b912a5ef8e57e56947c8ad690c227f5c9d7e561712" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9401557acd873c3a7f3eb9383edef8ac4968f9510e340f4808d427e75667e7b4" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:926c9541b14b12b1681dca8a0b75feb510b06c6341b70a8e500c2fdcff837cce" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:56cb4851bcaf3d117eddcef4fe66afd750a50274b0da8e22be256d10e5611987" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c68fcd102d71ea85c5b2dfac3f4f8476eff42a9e078fd5faefff6d145063536b" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b2f69dec1725e79a012d920df1707de5caf7ed5e08f3be4435e25803efc47458" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:8d0820e8192167f80d88d64038e609c31452eeca865b4e1d9950a27a4609b00b" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fbdb89b3e1c94a30cc5edfce477c6e6a5dc4d8f84665b455c27582f211a1c72c" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-cp312-cp312-win32.whl", hash = "sha256:9aa768456404a8bf48a4406685ac2bec8e72b62c69313734fa3b73cf33b3a894" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-cp312-cp312-win_amd64.whl", hash = "sha256:e9c26f834c65f5752f3f06cb08cb86a913ceb7274d0db6e267808a708b46bc89" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-cp312-cp312-win_arm64.whl", hash = "sha256:4fc73cb559bdb54b1134a706a2802a4cddd27a0633f5abb7e53056268751ac6a" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:5d5902252db0d3cedf8d4a1bc68f70eeb430f7e4c7104c8c476753519b423008" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c94f0688e7b8d0a67abf40e57a7eaaecd17cc9586706a31b76c031f63df052b4" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f027324c56cd5406ca49c124b0db10e56c69064fec039acc571c29020cc87c76" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e739fee756ba1010f8bcccb534252e85a35fe45ae92c295a06059ce58b74ccd3" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d56801be94b86a9da183e5f3766e6310752b99ff647e38b09a9500d88e46e76" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2412e734dcb48da14d4e4006b82b46b74f2518b8a26ee7e58c6844a6cd6d03c4" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9551187363ffc0de2a00b2e47c25aeaeb1020b69b668762966df15fc5659dd5a" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:0186750b482eefa11d7f435892b09c5c606193ef3375bcf94aa00ae6bfb66262" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5855698a4856556d86e8e6cd8434bc3ac0314ee8e12089ae0e143f64c6256e4e" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:cbaf13819775b7f769bf4a1f066cb6df7a28d4480081a589828ef190226881cd" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:633147d34cf4550417f12e2b1a0383973bdf5cdfde212cb09e9a581cf10820be" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:82cf5301172168103724d49a1444d3378cb20cdee30b116a1bd6031236298a5d" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-cp313-cp313-win32.whl", hash = "sha256:9fa8ae11da9e2b3126c6426f147e0fba88d96d65921799bb30c6abd1cb2c97fb" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-cp313-cp313-win_amd64.whl", hash = "sha256:6b3ace8194b0e5204818c92802dcdca7fc6d88aabbb799d7c795540d9cd6d292" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-cp313-cp313-win_arm64.whl", hash = "sha256:184c081504d17f1c1066e430e117142b2c77d9448a97f7b65c6ac9fd9aee238d" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:428e04521a40150c85216fc8b85e8d39fece235a9cf5e383761238c7fa9b96fb" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:23ace664830ee0bfe014a0c7bc248b1f7f25ed7ad103852c317624a1083af462" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce5c1d2a8b27468f433ca974829c44060b8097eedc39933e3c206a90ee49c4a9" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7283d57845ecf5a163403eb0702dfc220cc4fbdd18919cb5ccea4f95ee1cdab4" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8daafc69c93ee8a0204506a3b6b30f586ef54028f52aeeeb5c4cfc5184fd5914" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd2213145bcc2ba85884d0ac63d222fece9209678f77b9b4d76f054c561adb28" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a5f930472650a82629163023e630d160863fce524c616f4e5186e5de9d9a49b" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:c1b3f518abeca3aa13c712fd202306e145abf59a18b094a6bafb2d2bbf59192c" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1a7dd0b3ee80d90150e3495a3a13ac34dbcbfd4f012996a6a1d8900e91b5c0fb" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:3fb702cd90b0446a3a1c5e470bfa0dd23c0233b676a9099ddcc964fa6ca13898" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b8458003118a712e66286df6a707db01c52c0f52f7db8e4a38f0da1d3b94fc4e" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:372429a130e469c9cd698925ce5fc50940b7a1336b0d82038e63d5bbc4edc519" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-cp314-cp314-win32.whl", hash = "sha256:85bb3611ff1802f3ee7fdd7dbff26b56f343fb432d57a4728fdd49b6ef35e2f4" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-cp314-cp314-win_amd64.whl", hash = "sha256:811ff8e9c313ab425368bcbb36e5c4ebd7108c2bbf4e4089cfbb0b01eff63fac" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-cp314-cp314-win_arm64.whl", hash = "sha256:bfec22eab3c8cc2ceec0248aec886624116dc079afa027ecc8ad4a7e62010f8a" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:af8244b2bef6aaad6d92cda81372de7f8c8d36c9f0c3ea36e827c60e7d9467a0" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a4330cdbc57162e4b3aa303f588ba752257694c9c9be3e7ebb11b4aca659b5d" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29c61fc04a3d840155ff08e475a04809278972fe6aef51e2720554e96367e34b" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c50f2528cf200c5eed56faf3f4e22fcd5f38c157a8b78576e6ba3168ec35f000" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0cbe8b01f948de4286c74cdd6c667aceb38f5c1e26f0693b3983d9d74887c65e" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:617d7e2ca7dcb8c5cf6bcb8c59b8832c94b36196bbf1cbd1bfb56ed341905edd" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7027560ee92211647d0d34e3f7cd6f50da56399d26a9c8ad0da286d3869a53f3" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:f99626688942fb746e545232e7726926f3be91b5975f8b55327665fafda991c7" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fc3e9034a63de20e15e8ade85358bc6efc614008cab72898b4b4952bea0509ff" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:97e7cf2be5c77b7d1a9713a05605d49460d02c6078d38d8bef3cbe323c548424" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:3bf92c5d0e00fefaab325a4d27828fe6b6e2a21848686b5b60d2d9eeb09d76c6" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:3ecbc122d18468d06ca279dc26a8c2e2d5acb10943bb35e36ae92096dc3b5565" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-cp314-cp314t-win32.whl", hash = "sha256:e846ae7835bf0703ae43f534ab79a867146dadd59dc9ca5c8b53d5c8f7c9ef02" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-cp314-cp314t-win_amd64.whl", hash = "sha256:2108ba5c1c1eca18030634489dc544844144ee36357f2f9f780b93e7ddbb44b5" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-cp314-cp314t-win_arm64.whl", hash = "sha256:4fcbe087dbc2068af7eda3aa87634eba216dbda64d1ae73c8684b621d33f6596" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:14d4edf427bdcf950a8a02d7cb44a08614388dd6e1bdcbf4f67504fa7887da9c" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:0ce40cd7b21210e99342afafbd4d0f76d784eb5b1d60f3bdc566be4983c6c73b" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:90884113d8b48f760e9587002789ddd741e76ab9f89518cd1e43b1f1a52ec44b" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:66ce7632c22d837c95301830e111ad0128a32b8207533b60896a96c4915192ea" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:1d8ba486450b14f3b1d63bc521d410ec7565e52f887b9fb671791886436a42f7" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:3009f12e4e90b7f88b4f9adb1b0c4a3d58fe7820f3238c190047209d148026df" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad785e92e6dc634c21555edc8bd6b64957ab844541bcb96a1366c202951ae526" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00c603d540afdd6b80eb39f078f33ebd46211f02f33e34a32d9f053bba711de0" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:0c563b08bca408dc7f65f700633d8442fffb2421fc47b8101377e9fd65051ff0" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:db06ffe51636ffe9ca531fe9023dd64bdd794be8754cb5df57c5498ae5b518a7" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:133878133d271ade3d41d1bfb2a45ec38dbdbda40bc065921c6b04e4630127e2" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9bc519fbf2b7578398853d815009ae5e4d4603d12f4e3f91da8c06852d3da3e9" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:c7a7bd4e39e8e4c12c39cd480356842b6a8a06e41b23a55a5e3e191718838ddf" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:d396ec2b979760aaf3218e76c24e65bd0aca24983298653b3a9d7a45f9e47b30" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:86e1a4418c6cd97d60c95c71164158eaf7324fae7b0923264016baa993eba6fc" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:d51026d73fcfd93610abc7b27789c26b313920fcfb20e27462d74a7f8b06e983" }, +] + +[[package]] +name = "pyopenssl" +version = "26.2.0" +source = { registry = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/simple/" } +dependencies = [ + { name = "cryptography" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pyopenssl/pyopenssl-26.2.0.tar.gz", hash = "sha256:8c6fcecd1183a7fc897548dfe388b0cdb7f37e018200d8409cf33959dbe35387" } +wheels = [ + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pyopenssl/pyopenssl-26.2.0-py3-none-any.whl", hash = "sha256:4f9d971bc5298b8bc1fab282803da04bf000c755d4ad9d99b52de2569ca19a70" }, +] + +[[package]] +name = "python-dotenv" +version = "1.2.2" +source = { registry = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/simple/" } +sdist = { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/python-dotenv/python_dotenv-1.2.2.tar.gz", hash = "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3" } +wheels = [ + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/python-dotenv/python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a" }, +] + +[[package]] +name = "python-multipart" +version = "0.0.32" +source = { registry = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/simple/" } +sdist = { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/python-multipart/python_multipart-0.0.32.tar.gz", hash = "sha256:be54b7f3fa167bb83e4fcd936b887b708f4e57fe75911c02aebf53efaf8d938e" } +wheels = [ + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/python-multipart/python_multipart-0.0.32-py3-none-any.whl", hash = "sha256:ff6d3f776f16878c894e52e107296ffc890e913c611b1a4ec6c44e2821fe2e23" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.3" +source = { registry = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/simple/" } +sdist = { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pyyaml/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f" } +wheels = [ + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pyyaml/pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pyyaml/pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pyyaml/pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pyyaml/pyyaml-6.0.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pyyaml/pyyaml-6.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pyyaml/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pyyaml/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pyyaml/pyyaml-6.0.3-cp310-cp310-win32.whl", hash = "sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pyyaml/pyyaml-6.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pyyaml/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pyyaml/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pyyaml/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pyyaml/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pyyaml/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pyyaml/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pyyaml/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pyyaml/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pyyaml/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pyyaml/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pyyaml/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pyyaml/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pyyaml/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pyyaml/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pyyaml/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pyyaml/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pyyaml/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pyyaml/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pyyaml/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pyyaml/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pyyaml/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pyyaml/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pyyaml/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pyyaml/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pyyaml/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pyyaml/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pyyaml/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pyyaml/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pyyaml/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pyyaml/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pyyaml/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pyyaml/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pyyaml/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pyyaml/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pyyaml/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pyyaml/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pyyaml/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pyyaml/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pyyaml/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pyyaml/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pyyaml/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pyyaml/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pyyaml/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pyyaml/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pyyaml/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pyyaml/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pyyaml/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b" }, +] + +[[package]] +name = "referencing" +version = "0.37.0" +source = { registry = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/simple/" } +dependencies = [ + { name = "attrs" }, + { name = "rpds-py", version = "0.30.0", source = { registry = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/simple/" }, marker = "python_full_version < '3.11'" }, + { name = "rpds-py", version = "2026.5.1", source = { registry = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/simple/" }, marker = "python_full_version >= '3.11'" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/referencing/referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8" } +wheels = [ + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/referencing/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231" }, +] + +[[package]] +name = "requests" +version = "2.34.2" +source = { registry = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/simple/" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/requests/requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed" } +wheels = [ + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/requests/requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0" }, +] + +[[package]] +name = "rpds-py" +version = "0.30.0" +source = { registry = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/simple/" } +resolution-markers = [ + "python_full_version < '3.11'", +] +sdist = { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0.tar.gz", hash = "sha256:dd8ff7cf90014af0c0f787eea34794ebf6415242ee1d6fa91eaba725cc441e84" } +wheels = [ + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:679ae98e00c0e8d68a7fda324e16b90fd5260945b45d3b824c892cec9eea3288" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4cc2206b76b4f576934f0ed374b10d7ca5f457858b157ca52064bdfc26b9fc00" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:389a2d49eded1896c3d48b0136ead37c48e221b391c052fba3f4055c367f60a6" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:32c8528634e1bf7121f3de08fa85b138f4e0dc47657866630611b03967f041d7" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f207f69853edd6f6700b86efb84999651baf3789e78a466431df1331608e5324" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:67b02ec25ba7a9e8fa74c63b6ca44cf5707f2fbfadae3ee8e7494297d56aa9df" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c0e95f6819a19965ff420f65578bacb0b00f251fefe2c8b23347c37174271f3" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:a452763cc5198f2f98898eb98f7569649fe5da666c2dc6b5ddb10fde5a574221" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e0b65193a413ccc930671c55153a03ee57cecb49e6227204b04fae512eb657a7" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:858738e9c32147f78b3ac24dc0edb6610000e56dc0f700fd5f651d0a0f0eb9ff" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:da279aa314f00acbb803da1e76fa18666778e8a8f83484fba94526da5de2cba7" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7c64d38fb49b6cdeda16ab49e35fe0da2e1e9b34bc38bd78386530f218b37139" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp310-cp310-win32.whl", hash = "sha256:6de2a32a1665b93233cde140ff8b3467bdb9e2af2b91079f0333a0974d12d464" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp310-cp310-win_amd64.whl", hash = "sha256:1726859cd0de969f88dc8673bdd954185b9104e05806be64bcd87badbe313169" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a2bffea6a4ca9f01b3f8e548302470306689684e61602aa3d141e34da06cf425" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dc4f992dfe1e2bc3ebc7444f6c7051b4bc13cd8e33e43511e8ffd13bf407010d" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:422c3cb9856d80b09d30d2eb255d0754b23e090034e1deb4083f8004bd0761e4" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:07ae8a593e1c3c6b82ca3292efbe73c30b61332fd612e05abee07c79359f292f" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:12f90dd7557b6bd57f40abe7747e81e0c0b119bef015ea7726e69fe550e394a4" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:99b47d6ad9a6da00bec6aabe5a6279ecd3c06a329d4aa4771034a21e335c3a97" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:33f559f3104504506a44bb666b93a33f5d33133765b0c216a5bf2f1e1503af89" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:946fe926af6e44f3697abbc305ea168c2c31d3e3ef1058cf68f379bf0335a78d" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:495aeca4b93d465efde585977365187149e75383ad2684f81519f504f5c13038" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9a0ca5da0386dee0655b4ccdf46119df60e0f10da268d04fe7cc87886872ba7" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8d6d1cc13664ec13c1b84241204ff3b12f9bb82464b8ad6e7a5d3486975c2eed" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3896fa1be39912cf0757753826bc8bdc8ca331a28a7c4ae46b7a21280b06bb85" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp311-cp311-win32.whl", hash = "sha256:55f66022632205940f1827effeff17c4fa7ae1953d2b74a8581baaefb7d16f8c" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp311-cp311-win_amd64.whl", hash = "sha256:a51033ff701fca756439d641c0ad09a41d9242fa69121c7d8769604a0a629825" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp311-cp311-win_arm64.whl", hash = "sha256:47b0ef6231c58f506ef0b74d44e330405caa8428e770fec25329ed2cb971a229" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a161f20d9a43006833cd7068375a94d035714d73a172b681d8881820600abfad" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6abc8880d9d036ecaafe709079969f56e876fcf107f7a8e9920ba6d5a3878d05" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca28829ae5f5d569bb62a79512c842a03a12576375d5ece7d2cadf8abe96ec28" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a1010ed9524c73b94d15919ca4d41d8780980e1765babf85f9a2f90d247153dd" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8d1736cfb49381ba528cd5baa46f82fdc65c06e843dab24dd70b63d09121b3f" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d948b135c4693daff7bc2dcfc4ec57237a29bd37e60c2fabf5aff2bbacf3e2f1" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47f236970bccb2233267d89173d3ad2703cd36a0e2a6e92d0560d333871a3d23" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:2e6ecb5a5bcacf59c3f912155044479af1d0b6681280048b338b28e364aca1f6" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a8fa71a2e078c527c3e9dc9fc5a98c9db40bcc8a92b4e8858e36d329f8684b51" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:73c67f2db7bc334e518d097c6d1e6fed021bbc9b7d678d6cc433478365d1d5f5" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5ba103fb455be00f3b1c2076c9d4264bfcb037c976167a6047ed82f23153f02e" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7cee9c752c0364588353e627da8a7e808a66873672bcb5f52890c33fd965b394" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp312-cp312-win32.whl", hash = "sha256:1ab5b83dbcf55acc8b08fc62b796ef672c457b17dbd7820a11d6c52c06839bdf" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp312-cp312-win_amd64.whl", hash = "sha256:a090322ca841abd453d43456ac34db46e8b05fd9b3b4ac0c78bcde8b089f959b" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp312-cp312-win_arm64.whl", hash = "sha256:669b1805bd639dd2989b281be2cfd951c6121b65e729d9b843e9639ef1fd555e" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:f83424d738204d9770830d35290ff3273fbb02b41f919870479fab14b9d303b2" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e7536cd91353c5273434b4e003cbda89034d67e7710eab8761fd918ec6c69cf8" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2771c6c15973347f50fece41fc447c054b7ac2ae0502388ce3b6738cd366e3d4" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0a59119fc6e3f460315fe9d08149f8102aa322299deaa5cab5b40092345c2136" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:76fec018282b4ead0364022e3c54b60bf368b9d926877957a8624b58419169b7" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:692bef75a5525db97318e8cd061542b5a79812d711ea03dbc1f6f8dbb0c5f0d2" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9027da1ce107104c50c81383cae773ef5c24d296dd11c99e2629dbd7967a20c6" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:9cf69cdda1f5968a30a359aba2f7f9aa648a9ce4b580d6826437f2b291cfc86e" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a4796a717bf12b9da9d3ad002519a86063dcac8988b030e405704ef7d74d2d9d" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5d4c2aa7c50ad4728a094ebd5eb46c452e9cb7edbfdb18f9e1221f597a73e1e7" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ba81a9203d07805435eb06f536d95a266c21e5b2dfbf6517748ca40c98d19e31" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:945dccface01af02675628334f7cf49c2af4c1c904748efc5cf7bbdf0b579f95" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp313-cp313-win32.whl", hash = "sha256:b40fb160a2db369a194cb27943582b38f79fc4887291417685f3ad693c5a1d5d" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp313-cp313-win_amd64.whl", hash = "sha256:806f36b1b605e2d6a72716f321f20036b9489d29c51c91f4dd29a3e3afb73b15" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp313-cp313-win_arm64.whl", hash = "sha256:d96c2086587c7c30d44f31f42eae4eac89b60dabbac18c7669be3700f13c3ce1" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:eb0b93f2e5c2189ee831ee43f156ed34e2a89a78a66b98cadad955972548be5a" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:922e10f31f303c7c920da8981051ff6d8c1a56207dbdf330d9047f6d30b70e5e" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdc62c8286ba9bf7f47befdcea13ea0e26bf294bda99758fd90535cbaf408000" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:47f9a91efc418b54fb8190a6b4aa7813a23fb79c51f4bb84e418f5476c38b8db" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1f3587eb9b17f3789ad50824084fa6f81921bbf9a795826570bda82cb3ed91f2" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:39c02563fc592411c2c61d26b6c5fe1e51eaa44a75aa2c8735ca88b0d9599daa" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51a1234d8febafdfd33a42d97da7a43f5dcb120c1060e352a3fbc0c6d36e2083" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:eb2c4071ab598733724c08221091e8d80e89064cd472819285a9ab0f24bcedb9" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6bdfdb946967d816e6adf9a3d8201bfad269c67efe6cefd7093ef959683c8de0" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c77afbd5f5250bf27bf516c7c4a016813eb2d3e116139aed0096940c5982da94" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:61046904275472a76c8c90c9ccee9013d70a6d0f73eecefd38c1ae7c39045a08" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4c5f36a861bc4b7da6516dbdf302c55313afa09b81931e8280361a4f6c9a2d27" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp313-cp313t-win32.whl", hash = "sha256:3d4a69de7a3e50ffc214ae16d79d8fbb0922972da0356dcf4d0fdca2878559c6" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp313-cp313t-win_amd64.whl", hash = "sha256:f14fc5df50a716f7ece6a80b6c78bb35ea2ca47c499e422aa4463455dd96d56d" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:68f19c879420aa08f61203801423f6cd5ac5f0ac4ac82a2368a9fcd6a9a075e0" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ec7c4490c672c1a0389d319b3a9cfcd098dcdc4783991553c332a15acf7249be" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f251c812357a3fed308d684a5079ddfb9d933860fc6de89f2b7ab00da481e65f" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac98b175585ecf4c0348fd7b29c3864bda53b805c773cbf7bfdaffc8070c976f" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3e62880792319dbeb7eb866547f2e35973289e7d5696c6e295476448f5b63c87" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4e7fc54e0900ab35d041b0601431b0a0eb495f0851a0639b6ef90f7741b39a18" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47e77dc9822d3ad616c3d5759ea5631a75e5809d5a28707744ef79d7a1bcfcad" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:b4dc1a6ff022ff85ecafef7979a2c6eb423430e05f1165d6688234e62ba99a07" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4559c972db3a360808309e06a74628b95eaccbf961c335c8fe0d590cf587456f" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0ed177ed9bded28f8deb6ab40c183cd1192aa0de40c12f38be4d59cd33cb5c65" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ad1fa8db769b76ea911cb4e10f049d80bf518c104f15b3edb2371cc65375c46f" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:46e83c697b1f1c72b50e5ee5adb4353eef7406fb3f2043d64c33f20ad1c2fc53" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp314-cp314-win32.whl", hash = "sha256:ee454b2a007d57363c2dfd5b6ca4a5d7e2c518938f8ed3b706e37e5d470801ed" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp314-cp314-win_amd64.whl", hash = "sha256:95f0802447ac2d10bcc69f6dc28fe95fdf17940367b21d34e34c737870758950" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp314-cp314-win_arm64.whl", hash = "sha256:613aa4771c99f03346e54c3f038e4cc574ac09a3ddfb0e8878487335e96dead6" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:7e6ecfcb62edfd632e56983964e6884851786443739dbfe3582947e87274f7cb" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a1d0bc22a7cdc173fedebb73ef81e07faef93692b8c1ad3733b67e31e1b6e1b8" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d08f00679177226c4cb8c5265012eea897c8ca3b93f429e546600c971bcbae7" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5965af57d5848192c13534f90f9dd16464f3c37aaf166cc1da1cae1fd5a34898" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9a4e86e34e9ab6b667c27f3211ca48f73dba7cd3d90f8d5b11be56e5dbc3fb4e" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5d3e6b26f2c785d65cc25ef1e5267ccbe1b069c5c21b8cc724efee290554419" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:626a7433c34566535b6e56a1b39a7b17ba961e97ce3b80ec62e6f1312c025551" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:acd7eb3f4471577b9b5a41baf02a978e8bdeb08b4b355273994f8b87032000a8" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fe5fa731a1fa8a0a56b0977413f8cacac1768dad38d16b3a296712709476fbd5" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:74a3243a411126362712ee1524dfc90c650a503502f135d54d1b352bd01f2404" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:3e8eeb0544f2eb0d2581774be4c3410356eba189529a6b3e36bbbf9696175856" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:dbd936cde57abfee19ab3213cf9c26be06d60750e60a8e4dd85d1ab12c8b1f40" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp314-cp314t-win32.whl", hash = "sha256:dc824125c72246d924f7f796b4f63c1e9dc810c7d9e2355864b3c3a73d59ade0" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27f4b0e92de5bfbc6f86e43959e6edd1425c33b5e69aab0984a72047f2bcf1e3" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c2262bdba0ad4fc6fb5545660673925c2d2a5d9e2e0fb603aad545427be0fc58" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:ee6af14263f25eedc3bb918a3c04245106a42dfd4f5c2285ea6f997b1fc3f89a" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3adbb8179ce342d235c31ab8ec511e66c73faa27a47e076ccc92421add53e2bb" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:250fa00e9543ac9b97ac258bd37367ff5256666122c2d0f2bc97577c60a1818c" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9854cf4f488b3d57b9aaeb105f06d78e5529d3145b1e4a41750167e8c213c6d3" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:993914b8e560023bc0a8bf742c5f303551992dcb85e247b1e5c7f4a7d145bda5" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58edca431fb9b29950807e301826586e5bbf24163677732429770a697ffe6738" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:dea5b552272a944763b34394d04577cf0f9bd013207bc32323b5a89a53cf9c2f" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ba3af48635eb83d03f6c9735dfb21785303e73d22ad03d489e88adae6eab8877" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:dff13836529b921e22f15cb099751209a60009731a68519630a24d61f0b1b30a" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:1b151685b23929ab7beec71080a8889d4d6d9fa9a983d213f07121205d48e2c4" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:ac37f9f516c51e5753f27dfdef11a88330f04de2d564be3991384b2f3535d02e" }, +] + +[[package]] +name = "rpds-py" +version = "2026.5.1" +source = { registry = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/simple/" } +resolution-markers = [ + "python_full_version >= '3.13'", + "python_full_version >= '3.11' and python_full_version < '3.13'", +] +sdist = { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.5.1.tar.gz", hash = "sha256:07b24fea40541e28570e5b795a4a38fbdcd12550c06bd0748005ecc8116ca256" } +wheels = [ + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.5.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:3397a5ed7174dc2786bb214030232fc36fe8e5584fec43a9952cc542b1a12036" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.5.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:99ab6ba7bfa2cb0f96a04e3652355bf04e3f51aceb1e943b8541dab7ba4828cc" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.5.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d0efbe45632665e53e3db8fe1e5692db58fc5cb9bab4459d570b83efefe11164" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.5.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:01d17b29c0c23d82b1f4751147ec49cf451f1fc2554eb9ef5f957e55d2656ead" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.5.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7559f72b94ae52659086c595dfa017cde03155f7832071d30959049052cb3ece" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.5.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9e25b7088f9ccbfc0dfcaa52bf969300ca229e10ecf758974ebcbb080a4b37bb" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.5.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:613fc4ee9eaef26dc5840666214dd6fbcebcf32f46e76f4abc473059f4e13dda" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.5.1-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:85264a90ff4c05c1568dd65f5921c837614b67c60358fb4c17df3b7f2e90690a" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.5.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fe71bca7d547acb17027c7fd1624ff8aae623499c498d3e7011182c4de5c25e0" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.5.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05fa4f41f37ec97c9c260441a940450a192f78d774d2b097eee1379f1e1246a" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.5.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:df1d2a1996755b24b9ecee92cb4d36c28f86f464a6a173349c26bab41e94b8c2" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.5.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:8895840ac4809e5f60c88fd07617cd71326e73d6e5a8aa783c5c0f7c24985de2" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.5.1-cp311-cp311-win32.whl", hash = "sha256:3684a59b158a7683aaeb8e25352e9a9dd2122cec78f2d8530266e4f91b4c7b3f" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.5.1-cp311-cp311-win_amd64.whl", hash = "sha256:7bd530e6a530bb3ea892f194fafa455f3516ac25ecf7143fd33c09be62b0470a" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.5.1-cp311-cp311-win_arm64.whl", hash = "sha256:0a5ae4dbe43c1076983b72616496919872ae7bbe7a1e21cc48336bc3154d130b" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.5.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:3abe24a66e57adcfa645d718063a5fa5103ecc71ddbf26d78af8f9368018ff1d" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.5.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:58b1d94308ddf0b1982f61f2eb54bf92997c9ece8a8093ef014250f4a517906c" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.5.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fa92420128dadce7f54bd73ba1825a273e9268fe9e35dbf7e6362890efa4e08" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.5.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ca653c6546386227cd9800d1bef6a348099acf8db4250341da6d90f663d6dfcb" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.5.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:66c93681c4729e4e3ecba31b8179fae083ff3118841672835140338b4b9867c1" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.5.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:40ff257542e04796880e011e15cd4dc21c2599975df2aaa8f2c8495ca574e1a5" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.5.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b6825cc329b290e93c5f6a9be2393118a763f6ccf6abd83704e0c102ca583644" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.5.1-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:de42116e69cb53b911cc34aee5ab98f36c597b822545045d49e938818b99e5e4" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.5.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c0f920015df2a504bebaba6d4c31ccf3fcf942f92655c086da30b671aad19aa6" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.5.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0408a24e44feb919423dc6d9da677cb5cddb894d2ca9e763967d156d9c60fab4" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.5.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:cea68bcd53467561ae2f96a6bdad1544299ba97b5b0ddcd5ac3d376e5c781c24" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.5.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:4be8b1d2a705cc37d08256004e1d07de143fa0075c8e85a3df020b776f62b732" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.5.1-cp312-cp312-win32.whl", hash = "sha256:6736718bd4fc49cbcb538ba30516fdbef161522acefb739657d48b97bd864fed" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.5.1-cp312-cp312-win_amd64.whl", hash = "sha256:0a7d1eec967df0e9b22614a5e177622e0c89611d03727fa0cb48e45028907870" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.5.1-cp312-cp312-win_arm64.whl", hash = "sha256:1841d067089e117142d79b98aa0df2f08b52f2ecc1819dd2700636c0db74a473" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.5.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:efef4ac29c6ff495531eb17ee705b62841ecaa291b7c7077e848ea03e237164d" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.5.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c39f5b67a8a2e67179ada2a954227d670fe65fa9098457f698f56ddf248709b3" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.5.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b5c30f3f04eef4fbd362226a6f31d7c8895ca4fbb6e0b790f6890a98d8da8559" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.5.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:277f6c82f0580848796c7ecc8a7173aa3bfb928e4ff831261c2f60a81dc270db" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.5.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:63c2c4c213f1a4e3f3de28ecab029dbdee976324e729c0d7a55211be72576b02" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.5.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3350ec808fb538fe71a1f94dfaa0e29c598dfad805ce49f0caec5ae3183c652b" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.5.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b1b964e3ab599e718dc46c018d104b1ebc007cbc6567d827c94a687fca56d77e" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.5.1-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:19cb09fab7b7fc96b2a6e28f2e34b72a3705ff27b37edb77455316e5d3f3dc9b" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.5.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:abe76bcdba31e576cb83eeb8797aa0d882b738fef6dc65d0601fc753806a5b46" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.5.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:8bff7073db3899158fff55ebf57b113a67030af26f80a18978f9f0aa60250ddf" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.5.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:8ba264fa49be666cd9cc56bf34ec7002fb3d27a4aee5bcb4d43d0d18feb1bb6f" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.5.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4860b603ddda0475a8885499b3729e90229d480105b42651962a5397d995fa89" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.5.1-cp313-cp313-win32.whl", hash = "sha256:7944270ae71383f6e2657dd7d5ce4eeb4ac2d0059a6738f0510583d462ab4842" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.5.1-cp313-cp313-win_amd64.whl", hash = "sha256:88647f43a73c4e01be19b04ceef0c8d3a1958153604d13c773becd8016f2a0cf" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.5.1-cp313-cp313-win_arm64.whl", hash = "sha256:453895624ecf7db7063b1004e44037522bbaef9ff6a945e59bc71662d7a03abd" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.5.1-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:b4e4bc98639ec915f512fde3aa7a95e0041d95d9c3cc86eea841fa63cb1e8600" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.5.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:cacedb7a6e167680acba45ad5716e89067d225dc80da0d7040cae8c81d4572fa" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.5.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:68700371c5d7ae1412862ddfa719090925c93ecf351c566d66f09d04b136ea00" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.5.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:296c799becfa849c779c8725494fe9ed94959ed886787df4364b058465bad7f0" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.5.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d3858b908218ee108d0bbfb2095ccc237648053c9bf98affad7cb079acaf1d97" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.5.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4fb8d2e7cb2f850b169806d61d1b991738acec96500a75c30f49caf064ce7cef" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.5.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:27b74c10ed6a8f190f4287f53bcfea348b92a84a9c9f70d30183d1e6172d580d" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.5.1-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:b9a6528956191c48c52294a592dbd4a8386d7048bdb25c0efcb6b966466c6d83" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.5.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:af03e34e860047bc7a352b842856fcf78798fbb81132cc98bd2f907ab4eb9cd2" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.5.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:fea6e836d10abbe191d557d33bd58bd5987725fe63aa1eefe557d230209855bd" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.5.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:fc0c0f878ea770a0a8a462456c5ad36fc9fe6358e6b76fdadc7f17575e0b8bf1" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.5.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:e0b360f316d966b048b085857630b3cc51f3db2f07b06f440eac8f695374d1e3" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.5.1-cp313-cp313t-win32.whl", hash = "sha256:a2999883eedf72fdfb7520b92c7d4ec2572a71ff40239377aa604cc529eecafc" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.5.1-cp313-cp313t-win_amd64.whl", hash = "sha256:e07be2a9d7122bd6e82dea89814ef8dc893feb1aae97fec1630f3263bbb30e55" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.5.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:1f2c391c3059798093b65df23aca2cac150460ae9c630d99dec83d703d9485b9" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.5.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:413b424f7c4ee65ab5e5be91f5731be0f8b41a1ee2b12dfe810d716312e95a78" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.5.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2c595a1d9255dce0599e13130d1440ab2506654f2b50294226ee06402f8fef63" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.5.1-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1c27c5f6102eac8c03e7595a00827a53b271ba40a53b59ff8709170e0855ea4a" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.5.1-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6c7fcf61d44cacecaf3aea542b0e053db77972a4573e7ceda16fb2b399161195" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.5.1-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2c817a189d4ee14290420e5ff051e4dd6baa13f3edf84685071dee07a6d538ee" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.5.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21846aac0ed2e0589f38c12dc44e77bb64e494b771eadbcf169cba00566ba7ba" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.5.1-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:b317c87a13f769a4e787819bd508aaa5d69aa09b0880de9af6d3a8a54571cdec" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.5.1-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ce87129d9f2c14fa6c4a8601fb80eb4488c80d38a20cd13758ef11123e14995d" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.5.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9cdddb6c1207d284d94fd1530adf57fbd797fe7c4b8704ba85f49414f2557e7d" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.5.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:4e237e139f94d3c036fd28eb9f564c99055476ff4ff05cd42be55ce349b5aa02" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.5.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ed0954b524873214369184a9c82b0eaa45a3fbb9a798cd95b17e0d98499e7ea0" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.5.1-cp314-cp314-win32.whl", hash = "sha256:2d88621d6a7d4dfa633d21abe90f280bb205274e16b1d1e61c6ad4640b2453b7" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.5.1-cp314-cp314-win_amd64.whl", hash = "sha256:cef8ac28d26f4dda3533060c20fbf80a325458fa9fd23ea72a73cdfa8e978838" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.5.1-cp314-cp314-win_arm64.whl", hash = "sha256:eaaea962c68cdc68d4a533ba985ab8e9484277910bbfaa2ab3ef7732667bfed8" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.5.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:21942f52dbbd5f8758bf021213d28bd45c39e873e65e2407faf5f1846f5761ad" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.5.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f414556f6e3958300ff941e40c9f97e3dc9774ddd1b3434c475d73dd354bbed3" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.5.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ef1013a8625c74043210190b246f5b1551e09757c1f356c6e4160ef96c5bc081" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.5.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cc68e231a77a5f0d774ae278a1f8e55c0456501820847c1e4efb3829f3441df6" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.5.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9baffb505aff33acc69b422a19f77806680f3c8632227d79f48de8a810d1c2c5" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.5.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b8d2f912928d426e8cfa396f7f3f8d29a59e6689c86dcca3c420730c1096322b" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.5.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90f628283be835db980c941767d41c9a27b5239e54ba0a9c1335247e82406964" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.5.1-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:1ebb2f0ab7e16132995a72de805170e0203df0c3dd22e1ef1cd1fdd90bd7a131" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.5.1-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f3df3d16ded76f1f8c9cdebd0e1ea55fdf4c23b812de189814da7cf229c22a81" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.5.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:9af8905b8f854990e40d5206aa5ac58d9b0fe0b7f351ff2bb086c20f6c8c6a47" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.5.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:036a36a87fb1cd3b214d11c4b3c4f7d2ddad933625dca1c900b56a057c07740a" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.5.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:62ae3853454fe9ef283a03c96c2d835d39e84b14643a9d62c82ef0fb87d702ca" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.5.1-cp314-cp314t-win32.whl", hash = "sha256:6c3d771a46ec18b12af06ce36243a9a80b07a5d0515236332d90863ca8bb326a" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.5.1-cp314-cp314t-win_amd64.whl", hash = "sha256:c93c629be4636cf54337bd5f06c104d55e42ced54d681f6fe21ae510a65116f6" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.5.1-cp315-cp315-macosx_10_12_x86_64.whl", hash = "sha256:3574b55c604b8f75dacb007136508bbc0db406e626301778096a133327e7f2fb" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.5.1-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:94068eb3ae6d43f5a786b7db96a406a34e6d5c24489feef32fd6e8946ea7b291" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.5.1-cp315-cp315-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f3a5b10e8ce894825f380a8f1b6444cf73c294dfea62afbb2d13e3a9e630cec1" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.5.1-cp315-cp315-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fc09f82e63d4bcd58149572f857a431bae851dc747e313c3b5bdf7abb907fda8" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.5.1-cp315-cp315-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e10464d17df3b582745c25cec695cb9558bca2cb6ddb631aee1787fc72c767b2" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.5.1-cp315-cp315-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ba05adbf15d994c38ec0b7ab32e858e5110c21e9009a00a86545fd220f84e038" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.5.1-cp315-cp315-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:77c004fdc7b891967106f78ddfd7b076bfe6813c6139c6fff6aed3bcaa960b26" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.5.1-cp315-cp315-manylinux_2_31_riscv64.whl", hash = "sha256:83bcf894486c9d78dd290d3c0124ff6dd8875d3025e2090a8ec49fcc37c55fdd" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.5.1-cp315-cp315-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c3df104083952a0e0c6f10de33e440eabe98fb6317d23e1a58c68f6df08d01b9" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.5.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:980450826cf22e133c57e0835070bdd0dd3f73b9b708c3ce223def2cb9469e14" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.5.1-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:205dde846f24332ab0c1188699a043b8d165b79bb84529ce272c45048ff6be01" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.5.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:3966b82dd563176396df030f3dd52a6e54cb69b718e95e78bd555ed3d1e0185d" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.5.1-cp315-cp315-win32.whl", hash = "sha256:7818f8d0a415be74d2be3590b0a1c1f463a642f4d0217e7d10602dceef5b79aa" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.5.1-cp315-cp315-win_amd64.whl", hash = "sha256:b3cc20c0d800af78fd0fac68086e28c1856cec51ea528bb81ea851aa40d39325" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.5.1-cp315-cp315-win_arm64.whl", hash = "sha256:3609e9939a8a76cd904cf98a3f1f13b5dc7e150adeaee89e0ea09652ea213e16" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.5.1-cp315-cp315t-macosx_10_12_x86_64.whl", hash = "sha256:5d333a7127d4b307601ac37792bee01bb95c867cbfacf21b6375b804d6bbd723" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.5.1-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:b5f077b44a4f7808520f66dae234988d867deb9aed9be5da057ce9ba831b2a41" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.5.1-cp315-cp315t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:55d8f9b7b78c9538fc9e04e82ec0e888ff0c3cffcfad152c77e57cd09351a98a" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.5.1-cp315-cp315t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e3a8ae58895ac107ed934a6bf51e5846f95c53b9b940c2c6d310838fd5846358" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.5.1-cp315-cp315t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0957cf3c2b8632ec7aaebffebea8005b353cc2a237b6e2ae3c2cac0820704cfb" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.5.1-cp315-cp315t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c396c1304de421050b3681ea70f371874b54d41b0151e96109758144c231e30b" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.5.1-cp315-cp315t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aad1bff7f666b9598e573815affd666aac6a13a585dde336f843e33350c7fadc" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.5.1-cp315-cp315t-manylinux_2_31_riscv64.whl", hash = "sha256:656a042550878f12d45752452d47094b7cfe5ad1e9d7b87b5a22ad3ae5ff8015" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.5.1-cp315-cp315t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:73c4bd4f70294737b5206a3e8e30ccadbf8a60301831c8ea23eec5dbeea1ecfa" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.5.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:43bca78665423cabae77146f2fe7ce55272b6c8d55d82cca83effd42c7e13972" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.5.1-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:42d0f20e85e549c870749d0e247f0c10d318a45b7e9676d575d2dcb04a1b2e66" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.5.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:b1be5c35683684d5331b93600c210e8367c254683d8a6df6bd21bd2da3a334fb" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.5.1-cp315-cp315t-win32.whl", hash = "sha256:75808f6c38ce7749bb68cc2770161aae5045e6c6f6781a9782e74b93304399df" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.5.1-cp315-cp315t-win_amd64.whl", hash = "sha256:90bd6630002a1c7f09e7843dd79f0d24f3d2897cc25a753480917865d14f15b3" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.5.1-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:edf2765d84e42447f112ad877af8fe1db0089aaec5b28e88d6eab45e7fe99cea" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.5.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:ad3773236e95f7f33991eb125224b7da66f206504d032a253a02da7e134519fb" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.5.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a04df86b3f0fade39ec8fd0e0aab089b1da9fbd2b48df778a57ef96f5e7d38df" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.5.1-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6142dbd80c4df62a5d899f0d616d417f84e0bc8d32526c8e5589019d75d028a7" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.5.1-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0b35217adefe87f2fe4db7e9766cabe84744bfe9616d9667be18988928c7f2dc" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.5.1-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b95d5e11fc712b752081183a55a244c03cd00570489edd7014d8899f8ceb8162" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.5.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:141c9498daf2ace9eda35d2b0e376f9ea8b058d84f2aef4f96fccfd449a2f251" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.5.1-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:6f249f8b860a200ad35193af961183ebe9132710484e6f6ce0cf89fd83c63a9a" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.5.1-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e4abbf391a70be864920858bf360f4fb380577c9a0f732438a1996726e2c195b" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.5.1-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:c74005a7bb87752acf351c93897ec63ad77a07a0da7ecad9c050e32e7286ba34" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.5.1-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:8213afbe8a3a906fb9acb2014423fe3359ee783d0bf90995f70623a3217bfa6c" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.5.1-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:8c43a8a973270fd173bf48cdf80bbe66312421cba68d40845034f174f2389049" }, +] + +[[package]] +name = "sniffio" +version = "1.3.1" +source = { registry = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/simple/" } +sdist = { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/sniffio/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc" } +wheels = [ + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/sniffio/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2" }, +] + +[[package]] +name = "starlette" +version = "1.2.1" +source = { registry = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/simple/" } +dependencies = [ + { name = "anyio" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/starlette/starlette-1.2.1.tar.gz", hash = "sha256:9b9b5ebb992e67d6093741e63c2f59e4f6fff986f81163c087867bd7b924b3f6" } +wheels = [ + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/starlette/starlette-1.2.1-py3-none-any.whl", hash = "sha256:4de0082d08c8f6764a85a54cf1120d6939507a19905c7768acad2a9f875d2b89" }, +] + +[[package]] +name = "tenacity" +version = "9.1.4" +source = { registry = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/simple/" } +sdist = { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/tenacity/tenacity-9.1.4.tar.gz", hash = "sha256:adb31d4c263f2bd041081ab33b498309a57c77f9acf2db65aadf0898179cf93a" } +wheels = [ + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/tenacity/tenacity-9.1.4-py3-none-any.whl", hash = "sha256:6095a360c919085f28c6527de529e76a06ad89b23659fa881ae0649b867a9d55" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/simple/" } +sdist = { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/typing-extensions/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466" } +wheels = [ + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/typing-extensions/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.2" +source = { registry = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/simple/" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/typing-inspection/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464" } +wheels = [ + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/typing-inspection/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7" }, +] + +[[package]] +name = "tzdata" +version = "2026.2" +source = { registry = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/simple/" } +sdist = { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/tzdata/tzdata-2026.2.tar.gz", hash = "sha256:9173fde7d80d9018e02a662e168e5a2d04f87c41ea174b139fbef642eda62d10" } +wheels = [ + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/tzdata/tzdata-2026.2-py2.py3-none-any.whl", hash = "sha256:bbe9af844f658da81a5f95019480da3a89415801f6cc966806612cc7169bffe7" }, +] + +[[package]] +name = "tzlocal" +version = "5.3.1" +source = { registry = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/simple/" } +dependencies = [ + { name = "tzdata", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/tzlocal/tzlocal-5.3.1.tar.gz", hash = "sha256:cceffc7edecefea1f595541dbd6e990cb1ea3d19bf01b2809f362a03dd7921fd" } +wheels = [ + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/tzlocal/tzlocal-5.3.1-py3-none-any.whl", hash = "sha256:eb1a66c3ef5847adf7a834f1be0800581b683b5608e74f86ecbcef8ab91bb85d" }, +] + +[[package]] +name = "urllib3" +version = "2.7.0" +source = { registry = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/simple/" } +sdist = { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/urllib3/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c" } +wheels = [ + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/urllib3/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897" }, +] + +[[package]] +name = "uvicorn" +version = "0.49.0" +source = { registry = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/simple/" } +dependencies = [ + { name = "click" }, + { name = "h11" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/uvicorn/uvicorn-0.49.0.tar.gz", hash = "sha256:ebf4271aa580d9de97f93192d4595176df6e91f9aae919ca73e4fc07df1e66a3" } +wheels = [ + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/uvicorn/uvicorn-0.49.0-py3-none-any.whl", hash = "sha256:ba3d14c3ee7e41c6c654c46c9eb489d33213cdd30aa1696eab1374337c13f68f" }, +] + +[[package]] +name = "watchdog" +version = "6.0.0" +source = { registry = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/simple/" } +sdist = { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/watchdog/watchdog-6.0.0.tar.gz", hash = "sha256:9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282" } +wheels = [ + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/watchdog/watchdog-6.0.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d1cdb490583ebd691c012b3d6dae011000fe42edb7a82ece80965b42abd61f26" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/watchdog/watchdog-6.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bc64ab3bdb6a04d69d4023b29422170b74681784ffb9463ed4870cf2f3e66112" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/watchdog/watchdog-6.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c897ac1b55c5a1461e16dae288d22bb2e412ba9807df8397a635d88f671d36c3" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/watchdog/watchdog-6.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6eb11feb5a0d452ee41f824e271ca311a09e250441c262ca2fd7ebcf2461a06c" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/watchdog/watchdog-6.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ef810fbf7b781a5a593894e4f439773830bdecb885e6880d957d5b9382a960d2" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/watchdog/watchdog-6.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:afd0fe1b2270917c5e23c2a65ce50c2a4abb63daafb0d419fde368e272a76b7c" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/watchdog/watchdog-6.0.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bdd4e6f14b8b18c334febb9c4425a878a2ac20efd1e0b231978e7b150f92a948" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/watchdog/watchdog-6.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c7c15dda13c4eb00d6fb6fc508b3c0ed88b9d5d374056b239c4ad1611125c860" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/watchdog/watchdog-6.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6f10cb2d5902447c7d0da897e2c6768bca89174d0c6e1e30abec5421af97a5b0" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/watchdog/watchdog-6.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:490ab2ef84f11129844c23fb14ecf30ef3d8a6abafd3754a6f75ca1e6654136c" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/watchdog/watchdog-6.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:76aae96b00ae814b181bb25b1b98076d5fc84e8a53cd8885a318b42b6d3a5134" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/watchdog/watchdog-6.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a175f755fc2279e0b7312c0035d52e27211a5bc39719dd529625b1930917345b" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/watchdog/watchdog-6.0.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:c7ac31a19f4545dd92fc25d200694098f42c9a8e391bc00bdd362c5736dbf881" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/watchdog/watchdog-6.0.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:9513f27a1a582d9808cf21a07dae516f0fab1cf2d7683a742c498b93eedabb11" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/watchdog/watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/watchdog/watchdog-6.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:9041567ee8953024c83343288ccc458fd0a2d811d6a0fd68c4c22609e3490379" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/watchdog/watchdog-6.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:82dc3e3143c7e38ec49d61af98d6558288c415eac98486a5c581726e0737c00e" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/watchdog/watchdog-6.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:212ac9b8bf1161dc91bd09c048048a95ca3a4c4f5e5d4a7d1b1a7d5752a7f96f" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/watchdog/watchdog-6.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:e3df4cbb9a450c6d49318f6d14f4bbc80d763fa587ba46ec86f99f9e6876bb26" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/watchdog/watchdog-6.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:2cce7cfc2008eb51feb6aab51251fd79b85d9894e98ba847408f662b3395ca3c" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/watchdog/watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:20ffe5b202af80ab4266dcd3e91aae72bf2da48c0d33bdb15c66658e685e94e2" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/watchdog/watchdog-6.0.0-py3-none-win32.whl", hash = "sha256:07df1fdd701c5d4c8e55ef6cf55b8f0120fe1aef7ef39a1c6fc6bc2e606d517a" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/watchdog/watchdog-6.0.0-py3-none-win_amd64.whl", hash = "sha256:cbafb470cf848d93b5d013e2ecb245d4aa1c8fd0504e863ccefa32445359d680" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/watchdog/watchdog-6.0.0-py3-none-win_ia64.whl", hash = "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f" }, +] + +[[package]] +name = "websockets" +version = "15.0.1" +source = { registry = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/simple/" } +sdist = { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/websockets/websockets-15.0.1.tar.gz", hash = "sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee" } +wheels = [ + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/websockets/websockets-15.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d63efaa0cd96cf0c5fe4d581521d9fa87744540d4bc999ae6e08595a1014b45b" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/websockets/websockets-15.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ac60e3b188ec7574cb761b08d50fcedf9d77f1530352db4eef1707fe9dee7205" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/websockets/websockets-15.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5756779642579d902eed757b21b0164cd6fe338506a8083eb58af5c372e39d9a" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/websockets/websockets-15.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fdfe3e2a29e4db3659dbd5bbf04560cea53dd9610273917799f1cde46aa725e" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/websockets/websockets-15.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c2529b320eb9e35af0fa3016c187dffb84a3ecc572bcee7c3ce302bfeba52bf" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/websockets/websockets-15.0.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac1e5c9054fe23226fb11e05a6e630837f074174c4c2f0fe442996112a6de4fb" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/websockets/websockets-15.0.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:5df592cd503496351d6dc14f7cdad49f268d8e618f80dce0cd5a36b93c3fc08d" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/websockets/websockets-15.0.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0a34631031a8f05657e8e90903e656959234f3a04552259458aac0b0f9ae6fd9" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/websockets/websockets-15.0.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3d00075aa65772e7ce9e990cab3ff1de702aa09be3940d1dc88d5abf1ab8a09c" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/websockets/websockets-15.0.1-cp310-cp310-win32.whl", hash = "sha256:1234d4ef35db82f5446dca8e35a7da7964d02c127b095e172e54397fb6a6c256" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/websockets/websockets-15.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:39c1fec2c11dc8d89bba6b2bf1556af381611a173ac2b511cf7231622058af41" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/websockets/websockets-15.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:823c248b690b2fd9303ba00c4f66cd5e2d8c3ba4aa968b2779be9532a4dad431" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/websockets/websockets-15.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678999709e68425ae2593acf2e3ebcbcf2e69885a5ee78f9eb80e6e371f1bf57" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/websockets/websockets-15.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d50fd1ee42388dcfb2b3676132c78116490976f1300da28eb629272d5d93e905" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/websockets/websockets-15.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d99e5546bf73dbad5bf3547174cd6cb8ba7273062a23808ffea025ecb1cf8562" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/websockets/websockets-15.0.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:66dd88c918e3287efc22409d426c8f729688d89a0c587c88971a0faa2c2f3792" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/websockets/websockets-15.0.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8dd8327c795b3e3f219760fa603dcae1dcc148172290a8ab15158cf85a953413" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/websockets/websockets-15.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8fdc51055e6ff4adeb88d58a11042ec9a5eae317a0a53d12c062c8a8865909e8" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/websockets/websockets-15.0.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:693f0192126df6c2327cce3baa7c06f2a117575e32ab2308f7f8216c29d9e2e3" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/websockets/websockets-15.0.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:54479983bd5fb469c38f2f5c7e3a24f9a4e70594cd68cd1fa6b9340dadaff7cf" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/websockets/websockets-15.0.1-cp311-cp311-win32.whl", hash = "sha256:16b6c1b3e57799b9d38427dda63edcbe4926352c47cf88588c0be4ace18dac85" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/websockets/websockets-15.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:27ccee0071a0e75d22cb35849b1db43f2ecd3e161041ac1ee9d2352ddf72f065" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/websockets/websockets-15.0.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:3e90baa811a5d73f3ca0bcbf32064d663ed81318ab225ee4f427ad4e26e5aff3" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/websockets/websockets-15.0.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:592f1a9fe869c778694f0aa806ba0374e97648ab57936f092fd9d87f8bc03665" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/websockets/websockets-15.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0701bc3cfcb9164d04a14b149fd74be7347a530ad3bbf15ab2c678a2cd3dd9a2" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/websockets/websockets-15.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8b56bdcdb4505c8078cb6c7157d9811a85790f2f2b3632c7d1462ab5783d215" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/websockets/websockets-15.0.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0af68c55afbd5f07986df82831c7bff04846928ea8d1fd7f30052638788bc9b5" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/websockets/websockets-15.0.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64dee438fed052b52e4f98f76c5790513235efaa1ef7f3f2192c392cd7c91b65" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/websockets/websockets-15.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d5f6b181bb38171a8ad1d6aa58a67a6aa9d4b38d0f8c5f496b9e42561dfc62fe" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/websockets/websockets-15.0.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5d54b09eba2bada6011aea5375542a157637b91029687eb4fdb2dab11059c1b4" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/websockets/websockets-15.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3be571a8b5afed347da347bfcf27ba12b069d9d7f42cb8c7028b5e98bbb12597" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/websockets/websockets-15.0.1-cp312-cp312-win32.whl", hash = "sha256:c338ffa0520bdb12fbc527265235639fb76e7bc7faafbb93f6ba80d9c06578a9" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/websockets/websockets-15.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcd5cf9e305d7b8338754470cf69cf81f420459dbae8a3b40cee57417f4614a7" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/websockets/websockets-15.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ee443ef070bb3b6ed74514f5efaa37a252af57c90eb33b956d35c8e9c10a1931" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/websockets/websockets-15.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5a939de6b7b4e18ca683218320fc67ea886038265fd1ed30173f5ce3f8e85675" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/websockets/websockets-15.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:746ee8dba912cd6fc889a8147168991d50ed70447bf18bcda7039f7d2e3d9151" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/websockets/websockets-15.0.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:595b6c3969023ecf9041b2936ac3827e4623bfa3ccf007575f04c5a6aa318c22" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/websockets/websockets-15.0.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c714d2fc58b5ca3e285461a4cc0c9a66bd0e24c5da9911e30158286c9b5be7f" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/websockets/websockets-15.0.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f3c1e2ab208db911594ae5b4f79addeb3501604a165019dd221c0bdcabe4db8" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/websockets/websockets-15.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:229cf1d3ca6c1804400b0a9790dc66528e08a6a1feec0d5040e8b9eb14422375" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/websockets/websockets-15.0.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:756c56e867a90fb00177d530dca4b097dd753cde348448a1012ed6c5131f8b7d" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/websockets/websockets-15.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:558d023b3df0bffe50a04e710bc87742de35060580a293c2a984299ed83bc4e4" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/websockets/websockets-15.0.1-cp313-cp313-win32.whl", hash = "sha256:ba9e56e8ceeeedb2e080147ba85ffcd5cd0711b89576b83784d8605a7df455fa" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/websockets/websockets-15.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:e09473f095a819042ecb2ab9465aee615bd9c2028e4ef7d933600a8401c79561" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/websockets/websockets-15.0.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0c9e74d766f2818bb95f84c25be4dea09841ac0f734d1966f415e4edfc4ef1c3" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/websockets/websockets-15.0.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:1009ee0c7739c08a0cd59de430d6de452a55e42d6b522de7aa15e6f67db0b8e1" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/websockets/websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76d1f20b1c7a2fa82367e04982e708723ba0e7b8d43aa643d3dcd404d74f1475" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/websockets/websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f29d80eb9a9263b8d109135351caf568cc3f80b9928bccde535c235de55c22d9" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/websockets/websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b359ed09954d7c18bbc1680f380c7301f92c60bf924171629c5db97febb12f04" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/websockets/websockets-15.0.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:cad21560da69f4ce7658ca2cb83138fb4cf695a2ba3e475e0559e05991aa8122" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/websockets/websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f" }, +] + +[[package]] +name = "zipp" +version = "4.1.0" +source = { registry = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/simple/" } +sdist = { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/zipp/zipp-4.1.0.tar.gz", hash = "sha256:4cb57381f544315db7688e976e922a2b18cdb513d21cc194eb42232ba2a3e602" } +wheels = [ + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/zipp/zipp-4.1.0-py3-none-any.whl", hash = "sha256:25ad4e16390cd314347dd8f1de67a2ac538ae658ed4ab9db16029c07c188e97f" }, +] diff --git a/search/gemini-enterprise/gemini_enterprise_eval.ipynb b/search/gemini-enterprise/gemini_enterprise_eval.ipynb new file mode 100644 index 0000000..3ada37b --- /dev/null +++ b/search/gemini-enterprise/gemini_enterprise_eval.ipynb @@ -0,0 +1,1082 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "IxEDQsSwYLyX" + }, + "outputs": [], + "source": [ + "# Copyright 2025 Google LLC\n", + "#\n", + "# Licensed under the Apache License, Version 2.0 (the \"License\");\n", + "# you may not use this file except in compliance with the License.\n", + "# You may obtain a copy of the License at\n", + "#\n", + "# https://www.apache.org/licenses/LICENSE-2.0\n", + "#\n", + "# Unless required by applicable law or agreed to in writing, software\n", + "# distributed under the License is distributed on an \"AS IS\" BASIS,\n", + "# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n", + "# See the License for the specific language governing permissions and\n", + "# limitations under the License." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "EN9HLxPkbbKh" + }, + "source": [ + "# Gemini Enterprise answer eval using BLEU, ROUGE, BERT, Similarity Score" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "eec94beefdbb" + }, + "source": [ + "\n", + " \n", + " \n", + " \n", + " \n", + "
    \n", + " \n", + " \"Google
    Open in Colab\n", + "
    \n", + "
    \n", + " \n", + " \"Google
    Open in Colab Enterprise\n", + "
    \n", + "
    \n", + " \n", + " \"Vertex
    Open in Vertex AI Workbench\n", + "
    \n", + "
    \n", + " \n", + " \"GitHub
    View on GitHub\n", + "
    \n", + "
    \n", + "\n", + "
    \n", + "\n", + "Share to:\n", + "\n", + "\n", + " \"LinkedIn\n", + "\n", + "\n", + "\n", + " \"Bluesky\n", + "\n", + "\n", + "\n", + " \"X\n", + "\n", + "\n", + "\n", + " \"Reddit\n", + "\n", + "\n", + "\n", + " \"Facebook\n", + "" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "31447366377f" + }, + "source": [ + "| Authors |\n", + "| --- |\n", + "| [Nikhil Kulkarni](https://github.com/nikhilkul) |\n", + "| [Koushik Ghosh](https://github.com/Koushik25feb) |\n", + "| [Koyel Guha](https://github.com/koyelguha) |" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "8BnpNxbCMHy7" + }, + "source": [ + "## Overview: Gemini Enterprise Answer Evaluation\n", + "\n", + "This notebook provides a comprehensive framework for evaluating the quality of answers generated by an Gemini Enterprise application. It compares the Gemini Enterprise output against a \"golden dataset\" of expected answers using several widely recognized natural language processing (NLP) metrics.\n", + "\n", + "**Target Audience:** Developers and researchers working with Gemini Enterprise applications who need to quantitatively assess the performance of their answer generation models.\n", + "\n", + "**Key Features:**\n", + "\n", + "* **Metric-Based Evaluation:** Utilizes established NLP metrics such as BLEU, ROUGE, BERTScore, and Semantic Similarity to provide a robust evaluation.\n", + "* **Google Sheets Integration:** Seamlessly loads the golden dataset from a specified Google Sheet and can output the evaluation results back to a sheet.\n", + "* **BigQuery Integration:** Provides an option to save the evaluation results to a BigQuery table for further analysis and historical tracking.\n", + "* **Gemini Enterprise API Interaction:** Includes helper functions to interact with Gemini Enterprise Search and Assist APIs to fetch answers for evaluation.\n", + "* **Qualitative Rating:** Maps the numerical scores from the metrics to a qualitative rating (e.g., Excellent, Good) for easier interpretation.\n", + "* **Configurable:** Allows users to configure project details, engine ID, application type, and input/output sheet URLs.\n", + "\n", + "**Evaluation Framework:**\n", + "\n", + "The notebook employs a multi-faceted evaluation approach using the following NLP metrics:\n", + "\n", + "* **BLEU (Bilingual Evaluation Understudy):** Measures the n-gram overlap between the generated answer and the expected answer. It is a precision-focused metric, indicating how much of the generated text is present in the reference.\n", + "* **ROUGE (Recall-Oriented Understudy for Gisting Evaluation):** A set of metrics that measure the overlap of n-grams, word sequences, and word pairs between the generated answer and the expected answer. It is a recall-focused metric, indicating how much of the reference text is covered by the generated text. The notebook specifically uses ROUGE-L, which measures the longest common subsequence.\n", + "* **BERTScore:** Leverages pre-trained BERT embeddings to compute a similarity score between the generated answer and the expected answer. It considers semantic similarity beyond simple word overlap, making it more robust to paraphrasing.\n", + "* **Semantic Similarity:** Calculates the semantic similarity between the generated answer and the expected answer using a Sentence Transformer model (`all-MiniLM-L6-v2`). This provides a measure of how similar the meaning of the two texts is, regardless of the exact wording.\n", + "\n", + "For each question in the golden dataset, the notebook calculates these four scores. A qualitative rating (Excellent, Good, Moderate, Low, Poor) is then assigned based on the average of the BLEU, ROUGE, and BERT scores, and also individually for the Semantic Similarity score. This provides both numerical and easily interpretable qualitative feedback on the performance of the Gemini Enterprise application.\n", + "\n", + "**Input and Output Options:**\n", + "\n", + "* **Input (Golden Dataset):** The golden dataset, containing the test queries and their corresponding expected answers, is loaded from a Google Sheet. You need to provide the Google Drive URL of your sheet and the name of the worksheet containing the data. The notebook expects at least two columns: one for the query/question and one for the expected answer. Column names MUST be `search_query` and `expected answers`\n", + "\n", + "Sample:\n", + "\n", + "| `search_query` | `expected_answers` |\n", + "|----------------|--------------------|\n", + "| | |\n", + "| | |\n", + "\n", + "* **Output (Evaluation Results):** The evaluation results, including the calculated scores (BLEU, ROUGE, BERTScore, Semantic Similarity) and their corresponding qualitative ratings for each question, can be saved in the following formats:\n", + " * **CSV File:** The results are saved to a local CSV file within the Colab environment.\n", + " * **Google Sheet:** The results can be written to a specified worksheet within your Google Sheet. The notebook handles the case where the worksheet already exists.\n", + " * **BigQuery Table:** The results can be appended to a BigQuery table, including a timestamp for each run, allowing for historical tracking and further analysis using BigQuery's capabilities. You need to provide the dataset ID and table name.\n", + "\n", + "**How to Use:**\n", + "\n", + "1. **Setup:** Provide your Google Cloud project details, Gemini Enterprise engine ID, and the URLs for your golden dataset Google Sheet, as well as the desired BigQuery dataset and table names if using that option.\n", + "2. **Authentication:** Authenticate your Google Cloud account and enable the necessary APIs (Discovery Engine, Sheets, Drive, BigQuery).\n", + "3. **Data Loading:** The notebook will fetch your golden dataset from the specified Google Sheet.\n", + "4. **Answer Retrieval:** The notebook will query your Gemini Enterprise application with the questions from the golden dataset to get the generated answers.\n", + "5. **Evaluation:** The notebook will compute the specified NLP metrics by comparing the generated answers to the expected answers in your golden dataset.\n", + "6. **Results:** The results, including the scores and qualitative ratings, will be saved to the configured output options (CSV, Google Sheet, and/or BigQuery).\n", + "\n", + "**Prerequisites:**\n", + "\n", + "* Access to a Google Cloud project.\n", + "* An Gemini Enterprise application (Search or Assist).\n", + "* A Google Sheet containing your golden dataset with at least two columns: one for the query/question and one for the expected answer.\n", + "* Necessary APIs enabled in your Google Cloud project (Discovery Engine, Sheets, Drive, BigQuery).\n", + "\n", + "This notebook can be easily adapted to evaluate other answer generation systems by modifying the helper functions to interact with the relevant APIs." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "65-2go3u2WKL" + }, + "source": [ + "## Step 1: Initialization" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "uQrsAEV90W3R" + }, + "outputs": [], + "source": [ + "# @title Step 1.1 Install necessary libraries\n", + "\n", + "%pip install --upgrade --quiet pandas openpyxl nltk rouge-score bert-score transformers colabtools google-cloud-discoveryengine pandas==2.2.2" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "xtiEtYsi2TeX" + }, + "outputs": [], + "source": [ + "import datetime\n", + "\n", + "# @title Step 1.2 Import necessary libraries\n", + "import json\n", + "import logging\n", + "import os\n", + "import time\n", + "\n", + "import google.auth.transport.requests\n", + "import pandas as pd\n", + "import requests\n", + "import vertexai\n", + "from google.auth import default\n", + "from google.colab import auth\n", + "\n", + "creds, _ = google.auth.default()\n", + "auth_req = google.auth.transport.requests.Request()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "3af2185658f8" + }, + "outputs": [], + "source": [ + "# @title Add logger\n", + "\n", + "logger = logging.getLogger(\"gemini_enterprise_eval\")\n", + "logger.setLevel(logging.DEBUG)\n", + "log_file_path = \"./gemini_enterprise_eval_notebook_logs.log\"\n", + "\n", + "# Ensuring that handlers are not added multiple times if the cell is run multiple times\n", + "# This prevents duplicate log entries in the file and console\n", + "if logger.hasHandlers():\n", + " logger.handlers.clear() # Clear existing handlers\n", + "\n", + "\n", + "file_handler = logging.FileHandler(log_file_path, mode=\"a\")\n", + "file_handler.setLevel(logging.DEBUG)\n", + "formatter = logging.Formatter(\"%(asctime)s - %(levelname)s - %(name)s - %(message)s\")\n", + "file_handler.setFormatter(formatter)\n", + "logger.addHandler(file_handler)\n", + "\n", + "# Optionally, adding a StreamHandler to also print logs to the Colab console output\n", + "console_handler = logging.StreamHandler()\n", + "console_handler.setLevel(logging.INFO)\n", + "console_handler.setFormatter(formatter)\n", + "logger.addHandler(console_handler)\n", + "\n", + "\n", + "logger.info(f\"Logging initialized. Logs will be saved to: {log_file_path}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "ozdeRSNN2bX6" + }, + "source": [ + "## Step 2: Setup" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "k03CjFR72aup" + }, + "outputs": [], + "source": [ + "# @title Step 2.1 Set the project related configuration.\n", + "\n", + "# Use project number and not Project ID\n", + "project_num = \"00000000\" # @param{ type : 'string' }\n", + "\n", + "# Engine ID is the ID of the Gemini Enterprise APP\n", + "engine_id = \"gemini_enterprise_app_engine_id\" # @param{ type : 'string' }\n", + "\n", + "# Assist or Search - Note: \"Assist\" means \"Search + Assist\"\n", + "# Whereas Search means \"Search + Answer\"\n", + "# option to put are [search, assist]\n", + "app_type = \"search\" # @param{ type : 'string' }\n", + "\n", + "# Location global, us and eu\n", + "location = \"global\" # @param [\"us\", \"eu\", \"global\"]\n", + "\n", + "# Use Project ID\n", + "auth_project_id = \"[your-project-id]\" # @param{ type : 'string' }\n", + "\n", + "# Input Queries\n", + "# Note: Every user needs to have their own copy of this doc. Please make a copy of the golden data google sheet below and add that link.\n", + "eval_data_google_drive_url = \"[spreadsheet-url]\" # @param{ type : 'string' }\n", + "\n", + "# Input Queries\n", + "worksheet_name = \"input_queries\" # @param{ type : 'string' }\n", + "\n", + "# (Optional) Output file name used for debugging. This file will be saved in the colab env.\n", + "output_file_name = \"test_output\" # @param{ type : 'string' }\n", + "\n", + "# Eval data worksheet\n", + "sheet_name_suffix = datetime.datetime.fromtimestamp(time.time()).strftime(\n", + " \"%Y-%m-%d %H:%M:%S\"\n", + ")\n", + "eval_data_worksheet_name = \"sample_outputs\" # @param{ type : 'string' }\n", + "\n", + "# Eval data with metrics worksheet\n", + "eval_data_results_worksheet_name = \"eval_data_results\" # @param{ type : 'string' }\n", + "\n", + "# Top 'K' for search Results\n", + "K = 10 # @param{ type : 'string' }" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "U7ao9DKG2v9F" + }, + "outputs": [], + "source": [ + "# @title Step 2.2 Enable Google Sheets Integration\n", + "\n", + "# Enable Google Sheets Integration by visiting (only if you are using the golden dataset from the spreadsheet\n", + "\n", + "print(\n", + " f\"https://console.developers.google.com/apis/api/sheets.googleapis.com/overview?project={auth_project_id}\"\n", + ")\n", + "print(\n", + " f\"https://console.developers.google.com/apis/api/drive.googleapis.com/overview?project={auth_project_id}\"\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "nNF1D9zwqtI5" + }, + "source": [ + "## Step 3: Authenticate your Google Cloud Account and enable APIs\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "Am9pMNg5_rJq" + }, + "outputs": [], + "source": [ + "# @title Step 3.1 Authenticate your Google Cloud Account and enable APIs\n", + "\n", + "# Authenticate gcloud.\n", + "auth.authenticate_user(project_id=auth_project_id)\n", + "\n", + "region = \"us-central1\" # region = global is not supported yet\n", + "\n", + "# Configure gcloud.\n", + "!gcloud config set project {auth_project_id}\n", + "!gcloud config get-value project\n", + "\n", + "# Initialize Vertex AI\n", + "vertexai.init(project=auth_project_id, location=region)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "9pSfnRBC-ZWm" + }, + "outputs": [], + "source": [ + "# @title Step 3.2 Authenticate refresh\n", + "\n", + "creds.refresh(auth_req)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "pGEKqBzb3TJc" + }, + "source": [ + "## Step 4: Helper Functions\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "d2d2ae9ebc19" + }, + "outputs": [], + "source": [ + "# @title Step 4.0: Build the Discovery engine API based on the location\n", + "\n", + "if location == \"us\":\n", + " base_discovery_engine_domain = \"us-discoveryengine.googleapis.com\"\n", + "elif location == \"eu\":\n", + " base_discovery_engine_domain = \"eu-discoveryengine.googleapis.com\"\n", + "else: # Default to global if the location is not explicitly 'us' or 'eu'\n", + " base_discovery_engine_domain = \"discoveryengine.googleapis.com\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "BfXosgvSRYnz" + }, + "outputs": [], + "source": [ + "# @title Step 4.1: Search\n", + "\n", + "from functools import cache\n", + "\n", + "serving_config_id = \"default_search\" # @param{ type : 'string' }\n", + "discovery_engine_url = f\"https://{base_discovery_engine_domain}/v1alpha/projects/{auth_project_id}/locations/{location}/collections/default_collection/engines/{engine_id}/servingConfigs/{serving_config_id}\"\n", + "\n", + "# Create serving config with Control\n", + "create_serving_config_response = requests.post(\n", + " discovery_engine_url,\n", + " headers={\n", + " \"Content-Type\": \"application/json\",\n", + " \"Authorization\": \"Bearer \" + creds.token,\n", + " \"X-Goog-User-Project\": auth_project_id,\n", + " },\n", + " json={\n", + " \"displayName\": f\"{serving_config_id}\",\n", + " \"solutionType\": \"SOLUTION_TYPE_SEARCH\",\n", + " },\n", + ")\n", + "\n", + "\n", + "@cache\n", + "def get_search_results(query, num=K):\n", + " search_response = requests.post(\n", + " f\"{discovery_engine_url}:search\",\n", + " headers={\n", + " \"Content-Type\": \"application/json\",\n", + " \"Authorization\": \"Bearer \" + creds.token,\n", + " \"X-Goog-User-Project\": auth_project_id,\n", + " },\n", + " json={\n", + " \"query\": query,\n", + " \"pageSize\": num,\n", + " },\n", + " )\n", + " results = search_response.json()\n", + " logger.info(f\"query : {query}\")\n", + " logger.info(f\"Response code: {search_response.status_code}\")\n", + " return results" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "75fJyI1Li7W9" + }, + "outputs": [], + "source": [ + "# @title Step 4.2: Assistant\n", + "\n", + "\n", + "ENDPOINT = f\"https://{base_discovery_engine_domain}\"\n", + "ASSISTANT_NAME = f\"projects/{auth_project_id}/locations/{location}/collections/default_collection/engines/{engine_id}/assistants/default_assistant\"\n", + "\n", + "\n", + "@cache\n", + "def get_assist_results(query: str):\n", + " response = requests.post(\n", + " f\"{ENDPOINT}/v1alpha/{ASSISTANT_NAME}:assist\",\n", + " headers={\n", + " \"Content-Type\": \"application/json; charset=utf-8\",\n", + " \"Authorization\": f\"Bearer {creds.token}\",\n", + " \"X-Goog-User-Project\": f\"{auth_project_id}\",\n", + " },\n", + " data=json.dumps({\"query\": {\"text\": query}}),\n", + " )\n", + " if response.status_code != 200:\n", + " logger.error(f\"Assistant failed for query: {query} | {response.content}\")\n", + " answer = \"FAILED\"\n", + " assist_token = \"None\"\n", + " else:\n", + " assist_response = response.json()\n", + " assist_token = assist_response.get(\"assistToken\")\n", + " answer_data = assist_response.get(\"answer\")\n", + " state = answer_data.get(\"state\")\n", + " if state == \"SKIPPED\":\n", + " answer = answer_data[\"assistSkippedReasons\"][0]\n", + " else:\n", + " answer = (\n", + " answer_data.get(\"replies\")[0]\n", + " .get(\"groundedContent\")\n", + " .get(\"content\")\n", + " .get(\"text\")\n", + " )\n", + " return answer, assist_token" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "HaBPXNospMFZ" + }, + "outputs": [], + "source": [ + "# @title Step 4.3: Answer\n", + "\n", + "\n", + "session = requests.post(\n", + " f\"https://{base_discovery_engine_domain}/v1alpha/projects/{auth_project_id}/locations/{location}/collections/default_collection/engines/{engine_id}/sessions\", # auto session model\n", + " headers={\n", + " \"Content-Type\": \"application/json\",\n", + " \"Authorization\": \"Bearer \" + creds.token,\n", + " },\n", + " json={\n", + " \"userPseudoId\": \"12345\", # customer id\n", + " },\n", + ")\n", + "\n", + "\n", + "@cache\n", + "def get_answer_results(query):\n", + " response = requests.post(\n", + " f\"https://{base_discovery_engine_domain}/v1alpha/projects/{auth_project_id}/locations/{location}/collections/default_collection/engines/{engine_id}/servingConfigs/default_search:answer\",\n", + " headers={\n", + " \"Content-Type\": \"application/json\",\n", + " \"Authorization\": \"Bearer \" + creds.token,\n", + " },\n", + " json={\n", + " \"query\": {\"text\": query},\n", + " \"searchSpec\": {\n", + " \"searchParams\": {\n", + " \"maxReturnResults\": K,\n", + " },\n", + " },\n", + " \"session\": session.json()[\"name\"],\n", + " },\n", + " )\n", + "\n", + " if response.status_code != 200:\n", + " logger.error(f\"Answer API failed for query: {query} | {response.content}\")\n", + " answer_data = \"FAILED\"\n", + " answer_token = \"None\"\n", + " else:\n", + " answer_response = response.json()\n", + " answer_token = answer_response.get(\"answerQueryToken\")\n", + " answer_data = answer_response.get(\"answer\").get(\"answerText\")\n", + " return answer_data, answer_token" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "zt2WtcbFTDkw" + }, + "source": [ + "## Step 5: Get Golden Dataset" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "c60b3b38e62f" + }, + "outputs": [], + "source": [ + "# Define the path for the golden dataset file\n", + "GOLDEN_DATA_FILE_PATH = \"/content/golden_dataset.csv\"\n", + "\n", + "# Check if the file already exists.\n", + "# This prevents overwriting a user's uploaded file if they run the cell multiple times.\n", + "if not os.path.exists(GOLDEN_DATA_FILE_PATH):\n", + " # Create an empty DataFrame with the required columns\n", + " sample_df = pd.DataFrame(columns=[\"search_query\", \"expected_answers\"])\n", + "\n", + " # Save the DataFrame to a CSV file without writing the index\n", + " sample_df.to_csv(GOLDEN_DATA_FILE_PATH, index=False)\n", + " logger.info(f\"✅ A sample '{GOLDEN_DATA_FILE_PATH}' has been created!\")\n", + " logger.info(\"It contains the columns: 'search_query' and 'expected_answers'.\")\n", + " logger.info(\"\\n**Instructions for your Golden Dataset:**\")\n", + " logger.info(\"1. **Option A: Download, Populate, and Re-upload:**\")\n", + " logger.info(\" - Click the folder icon on the left sidebar (Files).\")\n", + " logger.info(\n", + " \" - Hover over 'golden_dataset.csv', click the three dots (`⋼`), and select 'Download'.\"\n", + " )\n", + " logger.info(\n", + " \" - Open the downloaded file in a spreadsheet editor (like Excel or Google Sheets).\"\n", + " )\n", + " logger.info(\n", + " \" - Populate the `search_query` and `expected_answers` columns with your data.\"\n", + " )\n", + " logger.info(\" - Save the file (ensure it's still named `golden_dataset.csv`).\")\n", + " logger.info(\n", + " \" - Re-upload the populated file to the `/content/` directory in Colab.\"\n", + " )\n", + " logger.info(\n", + " \" (Click the folder icon -> Click the 'Upload' icon (paper with up arrow) -> Select your file).\"\n", + " )\n", + " logger.info(\"2. **Option B: Upload Your Own CSV:**\")\n", + " logger.info(\n", + " \" - If you already have a CSV file with `search_query` and `expected_answers` columns,\"\n", + " )\n", + " logger.info(\" you can directly upload it to the `/content/` directory.\")\n", + " logger.info(\n", + " \" (Click the folder icon -> Click the 'Upload' icon -> Select your file).\"\n", + " )\n", + " logger.info(\" - Ensure your file is named `golden_dataset.csv`.\")\n", + "\n", + "else:\n", + " logger.info(\n", + " f\"✹ '{GOLDEN_DATA_FILE_PATH}' already exists. Skipping sample file creation.\"\n", + " )\n", + " logger.info(\"You can proceed to the next cell to load your dataset.\")\n", + " logger.info(\"\\n**Remember:** Your loaded file should be named 'golden_dataset.csv'\")\n", + " logger.info(\n", + " \"and located in the `content` directory with 'search_query' and 'expected_answers' columns.\"\n", + " )" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "NSmQbXaf5fW_" + }, + "outputs": [], + "source": [ + "# @title Step 5.1: Get Golden Dataset from CSV file locally(notebook)\n", + "import pandas as pd\n", + "\n", + "# Define the path to your golden dataset file\n", + "GOLDEN_DATA_FILE_PATH = \"/content/golden_dataset.csv\"\n", + "\n", + "try:\n", + " df = pd.read_csv(GOLDEN_DATA_FILE_PATH)\n", + " print(f\"Dataset loaded successfully from '{GOLDEN_DATA_FILE_PATH}'!\")\n", + " print(\"\\nFirst 5 rows of your dataset:\")\n", + " print(df.head())\n", + " print(f\"\\nDataset has {len(df)} rows and {len(df.columns)} columns.\")\n", + "\n", + " # Optional: Basic validation of expected columns\n", + " required_columns = [\"search_query\", \"expected_answers\"]\n", + " if not all(col in df.columns for col in required_columns):\n", + " print(\"\\n⚠ Warning: The loaded CSV does not contain all expected columns.\")\n", + " print(f\"Expected columns: {required_columns}\")\n", + " print(f\"Found columns: {df.columns.tolist()}\")\n", + "\n", + "except FileNotFoundError:\n", + " print(f\"❌ Error: '{GOLDEN_DATA_FILE_PATH}' not found.\")\n", + " print(\n", + " \"Please ensure you have created or uploaded the file correctly as per the instructions in the previous cell.\"\n", + " )\n", + "except pd.errors.EmptyDataError:\n", + " print(f\"⚠ Warning: '{GOLDEN_DATA_FILE_PATH}' is empty or only contains headers.\")\n", + " print(\"Please populate the file with your data.\")\n", + "except Exception as e:\n", + " print(f\"An unexpected error occurred while reading the CSV: {e}\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "tqcxHBclTocn" + }, + "outputs": [], + "source": [ + "import gspread\n", + "\n", + "# @title Step 5.2: Get Golden Dataset from Google Drive/Sheet\n", + "import pandas as pd\n", + "from google.colab import auth, drive\n", + "\n", + "drive.mount(\"/content/drive\")\n", + "auth.authenticate_user()\n", + "creds, _ = default()\n", + "gc = gspread.authorize(creds)\n", + "\n", + "# Replace with your actual spreadsheet details\n", + "spreadsheet = gc.open_by_url(f\"{eval_data_google_drive_url}\")\n", + "worksheet = spreadsheet.worksheet(f\"{worksheet_name}\")\n", + "# spreadsheet.add_worksheet(f\"{eval_data_worksheet_name}\", rows=\"100\", cols=\"20\")\n", + "\n", + "df = pd.DataFrame(worksheet.get_all_values()[1:], columns=worksheet.get_all_values()[0])" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "z7T3p6NGTpDF" + }, + "outputs": [], + "source": [ + "# @title Step 5.3 Populate Gemini Enterprise answer based on the Golden dataset\n", + "\n", + "df[[\"answer_result\", \"answer_token\"]] = df[\"search_query\"].apply(\n", + " lambda q: pd.Series(get_answer_results(q))\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "nogEpwig7-KB" + }, + "outputs": [], + "source": [ + "# @title Step 5.4: Visualise the Golden dataset to verify\n", + "df" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "W25FZRfQ2EUG" + }, + "source": [ + "## Step 6: Evaluation Functionality\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "EyGQKZ4j0WKd" + }, + "outputs": [], + "source": [ + "# @title Step 6.1: Evaluation imports\n", + "\n", + "import nltk\n", + "import pandas as pd\n", + "from bert_score import score as bert_score\n", + "from nltk.translate.bleu_score import sentence_bleu\n", + "from rouge_score import rouge_scorer\n", + "from sentence_transformers import SentenceTransformer, util\n", + "\n", + "model = SentenceTransformer(\n", + " \"all-MiniLM-L6-v2\" # similarity score model (open source - Hugging Face based)\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "YuzV1_pY8hlF" + }, + "outputs": [], + "source": [ + "# @title Step 6.2: Download the punkt for BERT eval process\n", + "\n", + "nltk.download(\"punkt\")\n", + "nltk.download(\"punkt_tab\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "i1TjiHn40MV1" + }, + "outputs": [], + "source": [ + "# @title Step 6.3: Evaluation utility functions\n", + "\n", + "\n", + "def get_semantic_score(expected, actual):\n", + " emb1 = model.encode(expected, convert_to_tensor=True)\n", + " emb2 = model.encode(actual, convert_to_tensor=True)\n", + " similarity = util.cos_sim(emb1, emb2).item()\n", + " return similarity # Score between 0 and 1\n", + "\n", + "\n", + "def compute_scores(expected, actual):\n", + " \"\"\"Compute BLEU, ROUGE, and BERTScore\"\"\"\n", + " # BLEU\n", + " reference = [nltk.word_tokenize(expected.lower())]\n", + " candidate = nltk.word_tokenize(actual.lower())\n", + " bleu = sentence_bleu(reference, candidate)\n", + "\n", + " # ROUGE\n", + " scorer = rouge_scorer.RougeScorer([\"rougeL\"], use_stemmer=True)\n", + " rouge = scorer.score(expected, actual)[\"rougeL\"].fmeasure\n", + "\n", + " # BERT Score\n", + " P, R, F1 = bert_score([actual], [expected], lang=\"en\", verbose=False)\n", + " bert = F1[0].item()\n", + "\n", + " return bleu, rouge, bert\n", + "\n", + "\n", + "def score_to_rating(score):\n", + " \"\"\"Map metric values to a qualitative rating\"\"\"\n", + " if score > 0.8:\n", + " return 5, \"Excellent match – high semantic and lexical similarity.\"\n", + " if score > 0.6:\n", + " return 4, \"Good match – minor differences, but meaning mostly intact.\"\n", + " if score > 0.4:\n", + " return 3, \"Moderate match – some loss in meaning.\"\n", + " if score > 0.2:\n", + " return 2, \"Low match – significant differences from expected.\"\n", + " return 1, \"Poor match – largely incorrect or off-topic.\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "n6MaJX4rBRCC" + }, + "outputs": [], + "source": [ + "# @title Step 6.4: Evaluate the Golden dataset with answers with respect to BLEU, ROUGE, BERT and Similarity score\n", + "results = []\n", + "\n", + "for _, row in df.iterrows():\n", + " question = row[\"search_query\"]\n", + " expected = row[\"expected_answers\"]\n", + " actual = row[\"answer_result\"]\n", + "\n", + " bleu, rouge, bert = compute_scores(expected, actual)\n", + "\n", + " avg = (bleu + rouge + bert) / 3\n", + " score, reasoning = score_to_rating(avg)\n", + " similarity = get_semantic_score(expected, actual)\n", + " score_to_rating\n", + "\n", + " results.append(\n", + " {\n", + " \"question\": question,\n", + " \"expected_answer\": expected,\n", + " \"actual_answer\": actual,\n", + " \"BLEU_score\": bleu,\n", + " \"BLEU_rating\": score_to_rating(bleu),\n", + " \"ROUGE_score\": rouge,\n", + " \"ROUGE_rating\": score_to_rating(rouge),\n", + " \"BERTScore\": bert,\n", + " \"BERT_rating\": score_to_rating(bert),\n", + " \"similarity\": similarity,\n", + " \"similarity_rating\": score_to_rating(similarity),\n", + " }\n", + " )" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "TstyJSJ1oMzQ" + }, + "source": [ + "## Step 7: Saving the Eval result" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "69Et8wmKKEoj" + }, + "outputs": [], + "source": [ + "# @title Step 7.1: Converting the result in the dataframe and adding timestamp\n", + "import datetime\n", + "\n", + "output_df = pd.DataFrame(results)\n", + "# Add a timestamp column to the DataFrame\n", + "output_df[\"timestamp\"] = datetime.datetime.now()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "Ox5dYhEYBSx2" + }, + "outputs": [], + "source": [ + "# @title Step 7.2: Save the output in the CSV\n", + "\n", + "output_csv_file_name = \"eval_output.csv\" # @param{ type : 'string' }\n", + "output_df.to_csv(output_csv_file_name, index=False)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "veY1b9m_BeCc" + }, + "outputs": [], + "source": [ + "# @title Step 7.3: Save the output in Google Sheet\n", + "\n", + "spreadsheet = gc.open_by_url(f\"{eval_data_google_drive_url}\")\n", + "\n", + "try:\n", + " worksheet_eval_data = spreadsheet.add_worksheet(\n", + " f\"{eval_data_worksheet_name}\", rows=\"100\", cols=\"20\"\n", + " )\n", + "except gspread.exceptions.APIError as e:\n", + " logger.info(\n", + " f\"Worksheet '{eval_data_worksheet_name}' already exists or another API error occurred: {e}\"\n", + " )\n", + " # If the worksheet already exists, get it\n", + " worksheet_eval_data = spreadsheet.worksheet(eval_data_worksheet_name)\n", + "else:\n", + " pass # No need to get the worksheet again if it was just created\n", + "\n", + "# Convert tuple columns to strings\n", + "for col in [\"BLEU_rating\", \"ROUGE_rating\", \"BERT_rating\", \"similarity_rating\"]:\n", + " if col in output_df.columns:\n", + " output_df[col] = output_df[col].apply(\n", + " lambda x: f\"{x[0]}: {x[1]}\" if isinstance(x, tuple) else x\n", + " )\n", + "\n", + "# Convert timestamp column to string\n", + "if \"timestamp\" in output_df.columns:\n", + " output_df[\"timestamp\"] = output_df[\"timestamp\"].astype(str)\n", + "\n", + "worksheet_eval_data.clear()\n", + "worksheet_eval_data.update(\n", + " [output_df.columns.values.tolist()] + output_df.values.tolist()\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "f8931369" + }, + "outputs": [], + "source": [ + "# @title Step 7.4: Save the output in BigQuery table\n", + "\n", + "# provide the BigQuery details here\n", + "dataset_id = \"gemini_enterprise_eval_dataset\" # @param{type: 'string'}\n", + "table_name = \"gemini_enterprise_eval_result\" # @param{type: 'string'}\n", + "project_id = auth_project_id # Use the authenticated project ID\n", + "\n", + "import datetime\n", + "\n", + "from google.cloud import bigquery\n", + "from google.cloud.exceptions import NotFound\n", + "\n", + "client = bigquery.Client(project=project_id)\n", + "\n", + "# Add a timestamp column to the DataFrame (if not already added in a previous step)\n", + "if \"timestamp\" not in output_df.columns:\n", + " output_df[\"timestamp\"] = datetime.datetime.now()\n", + "\n", + "# Convert tuple columns to strings before saving to BigQuery\n", + "for col in [\"BLEU_rating\", \"ROUGE_rating\", \"BERT_rating\", \"similarity_rating\"]:\n", + " if col in output_df.columns:\n", + " output_df[col] = output_df[col].apply(\n", + " lambda x: f\"{x[0]}: {x[1]}\" if isinstance(x, tuple) else x\n", + " )\n", + "\n", + "# Convert timestamp column to string before saving to BigQuery\n", + "if \"timestamp\" in output_df.columns:\n", + " output_df[\"timestamp\"] = output_df[\"timestamp\"].astype(str)\n", + "\n", + "# Construct a full table ID\n", + "table_id = f\"{project_id}.{dataset_id}.{table_name}\"\n", + "\n", + "# Check if the dataset exists, create if not\n", + "try:\n", + " client.get_dataset(dataset_id)\n", + " logger.info(f\"Dataset {dataset_id} already exists.\")\n", + "except NotFound:\n", + " logger.info(f\"Dataset {dataset_id} not found. Creating dataset.\")\n", + " dataset = bigquery.Dataset(f\"{project_id}.{dataset_id}\")\n", + " dataset.location = location # Use the location defined earlier\n", + " client.create_dataset(dataset, timeout=30)\n", + " logger.info(f\"Dataset {dataset_id} created.\")\n", + "\n", + "\n", + "# Check if the table exists, create if not\n", + "try:\n", + " client.get_table(table_id)\n", + " logger.info(f\"Table {table_name} already exists.\")\n", + "except NotFound:\n", + " logger.info(f\"Table {table_name} not found. Creating table.\")\n", + " # Create table with inferred schema (schema will be inferred during the load job)\n", + " table = bigquery.Table(table_id)\n", + " client.create_table(table, exists_ok=True)\n", + " logger.info(f\"Table {table_name} created.\")\n", + "\n", + "\n", + "# Load data from DataFrame to BigQuery\n", + "job_config = bigquery.LoadJobConfig(\n", + " write_disposition=\"WRITE_APPEND\", # Append data if table exists\n", + ")\n", + "\n", + "job = client.load_table_from_dataframe(output_df, table_id, job_config=job_config)\n", + "\n", + "job.result() # Wait for the job to complete.\n", + "\n", + "logger.info(f\"Loaded {job.output_rows} rows into {table_id}.\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "186a3b18e798" + }, + "source": [ + "## Learnings and Conclusion\n", + "\n", + "### What We Learned\n", + "\n", + "This notebook provided a comprehensive framework for evaluating the quality of answers generated by an Gemini Enterprise application. Our process involved several key stages:\n", + "\n", + "1. **Environment Setup and Configuration:**\n", + " * We successfully installed and imported all necessary libraries for data manipulation (pandas), NLP evaluation (NLTK, rouge-score, bert-score, sentence-transformers), and Google Cloud integration (google-cloud-discoveryengine, gspread, google-auth).\n", + " * We configured essential parameters, including Google Cloud project details, Gemini Enterprise engine ID, application type (search/assist), and locations for input (Google Sheets) and output (CSV, Google Sheets, BigQuery).\n", + "\n", + "2. **Data Ingestion and Answer Retrieval:**\n", + " * The notebook demonstrated how to load a \"golden dataset\" containing queries and their expected answers from a Google Sheet.\n", + " * We utilized helper functions to interact with the Gemini Enterprise Discovery Engine APIs (specifically `get_answer_results` in the main flow) to retrieve answers generated by the Gemini Enterprise application for each query in our golden dataset.\n", + "\n", + "3. **Metric-Based Evaluation:**\n", + " * We implemented a robust evaluation methodology using four distinct NLP metrics:\n", + " * **BLEU Score:** To measure n-gram precision against reference answers.\n", + " * **ROUGE-L Score:** To measure recall based on the longest common subsequence.\n", + " * **BERTScore:** To assess semantic similarity using contextual embeddings, going beyond lexical overlap.\n", + " * **Semantic Similarity:** Calculated using the `all-MiniLM-L6-v2` Sentence Transformer model to capture the similarity in meaning between generated and expected answers.\n", + " * For each query, we computed these four scores by comparing the Gemini Enterprise-generated answer with the corresponding expected answer.\n", + " * A qualitative rating system (Excellent, Good, Moderate, Low, Poor) was applied to the numerical scores, providing an easily interpretable assessment of answer quality for each metric.\n", + "\n", + "4. **Results Storage and Reporting:**\n", + " * The evaluation results, including the raw scores, qualitative ratings, original queries, expected answers, and actual answers, were compiled into a structured Pandas DataFrame.\n", + " * A timestamp was added to each evaluation run for tracking purposes.\n", + " * The notebook demonstrated multiple ways to persist these results:\n", + " * Saving to a local CSV file.\n", + " * Writing to a new or existing worksheet in a Google Sheet.\n", + " * Appending to a BigQuery table, enabling historical analysis and more complex querying. The notebook also handled the creation of the BigQuery dataset and table if they didn't already exist.\n", + "\n", + "### Conclusion\n", + "\n", + "This notebook successfully establishes a repeatable and quantitative process for evaluating the performance of an Gemini Enterprise application's answer generation capabilities. By leveraging a golden dataset and a suite of established NLP metrics, we can:\n", + "\n", + "* **Objectively measure answer quality:** Moving beyond subjective assessments to data-driven insights.\n", + "* **Identify areas for improvement:** Pinpoint queries or types of queries where the Gemini Enterprise application may be underperforming.\n", + "* **Track performance over time:** By storing results (especially in BigQuery), we can monitor how changes to the Gemini Enterprise configuration, underlying data, or models impact answer quality.\n", + "* **Benchmark different configurations:** The framework can be used to compare the performance of different Gemini Enterprise engine settings or versions.\n", + "\n", + "The integration with Google Sheets and BigQuery makes the golden dataset management and results analysis highly accessible and scalable. The qualitative ratings alongside numerical scores offer a balanced view, catering to both technical and non-technical stakeholders.\n", + "\n", + "**Potential Next Steps:**\n", + "\n", + "* **Automate the evaluation pipeline:** Integrate this notebook into a CI/CD pipeline for regular, automated performance checks.\n", + "* **Expand the golden dataset:** Continuously add more diverse and challenging queries to the golden dataset for more comprehensive testing.\n", + "* **Error Analysis:** Perform a deeper dive into queries with low scores to understand the root causes of poor performance (e.g., issues with grounding, summarization, or relevance).\n", + "* **Experiment with different models/settings:** Use this evaluation framework to systematically test the impact of different Gemini Enterprise configurations or underlying LLMs.\n", + "* **Visualize results:** Create dashboards (e.g., in Looker Studio using BigQuery data) to visualize evaluation trends and key metrics.\n", + "\n", + "This evaluation framework is a valuable asset for anyone looking to rigorously assess and improve their Gemini Enterprise applications." + ] + } + ], + "metadata": { + "colab": { + "name": "gemini_enterprise_eval.ipynb", + "toc_visible": true + }, + "kernelspec": { + "display_name": "Python 3", + "name": "python3" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} diff --git a/search/gemini-enterprise/gemini_enterprise_prompt_management.ipynb b/search/gemini-enterprise/gemini_enterprise_prompt_management.ipynb new file mode 100644 index 0000000..4572bbd --- /dev/null +++ b/search/gemini-enterprise/gemini_enterprise_prompt_management.ipynb @@ -0,0 +1,576 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "id": "7954bec8-6dba-453d-948e-74eada8c87b0", + "metadata": { + "id": "1Vs2C5ZBkrjmrHTfAyz77hpV" + }, + "outputs": [], + "source": [ + "# Copyright 2025 Google LLC\n", + "#\n", + "# Licensed under the Apache License, Version 2.0 (the \"License\");\n", + "# you may not use this file except in compliance with the License.\n", + "# You may obtain a copy of the License at\n", + "#\n", + "# https://www.apache.org/licenses/LICENSE-2.0\n", + "#\n", + "# Unless required by applicable law or agreed to in writing, software\n", + "# distributed under the License is distributed on an \"AS IS\" BASIS,\n", + "# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n", + "# See the License for the specific language governing permissions and\n", + "# limitations under the License." + ] + }, + { + "cell_type": "markdown", + "id": "588192ab-3488-4ae2-bf0e-62c837deb91c", + "metadata": { + "id": "VoJZgH6X77se" + }, + "source": [ + "# **Gemini Enterprise custom agent with prompt management**" + ] + }, + { + "cell_type": "markdown", + "id": "aecb9f66-0a4c-423b-a56a-cd7464e6206d", + "metadata": { + "id": "qfDq3m0R8Fe1" + }, + "source": [ + "\n", + " \n", + " \n", + " \n", + " \n", + "
    \n", + " \n", + " \"Google
    Open in Colab\n", + "
    \n", + "
    \n", + " \n", + " \"Google
    Open in Colab Enterprise\n", + "
    \n", + "
    \n", + " \n", + " \"Vertex
    Open in Vertex AI Workbench\n", + "
    \n", + "
    \n", + " \n", + " \"GitHub
    View on GitHub\n", + "
    \n", + "
    \n", + "\n", + "\n", + "\n", + "
    \n", + "
    \n", + "
    \n", + "\n", + "
    \n", + "\n", + "Share to:\n", + "\n", + "\n", + " \"LinkedIn\n", + "\n", + "\n", + "\n", + " \"Bluesky\n", + "\n", + "\n", + "\n", + " \"X\n", + "\n", + "\n", + "\n", + " \"Reddit\n", + "\n", + "\n", + "\n", + " \"Facebook\n", + "" + ] + }, + { + "cell_type": "markdown", + "id": "3f9aed85-8ea3-4f4b-ba16-0849a7f28519", + "metadata": { + "id": "UD1Cy9O88Zn9" + }, + "source": [ + "| Authors |\n", + "| --- |\n", + "| Rupjit Chakraborty|\n", + "| Sharmila Devi |\n", + "| Parag Mhatre |" + ] + }, + { + "cell_type": "markdown", + "id": "b06fa0ed-4dc1-4bc3-a9b1-e5a91fa9c508", + "metadata": { + "id": "28b524cdd168" + }, + "source": [ + "## Overview : Custom agent with file context and prompt management in Gemini Enterprise\n", + "\n", + "This notebook demonstrates an end-to-end example of a custom agent integrated with Gemini Enterprise that can accept a file as context and generate a response using that. In this case, a simple example of a SQL query generator based on a given schema DDL has been demonstrated. We also demonstrate custom agent [prompt management using Vertex AI](https://cloud.google.com/vertex-ai/generative-ai/docs/model-reference/prompt-classes)\n", + "\n", + "### Background\n", + "\n", + "Custom agents integrated with Gemini Enterprise by developers may require instruction modification based on the user choice. Also, the instructions given to agents via prompts, need to be maintainable via versioning. The prompt management sdk by Vertex AI can be used to manage prompts for agents at scale.\n", + "\n", + "### Business Application Scenario\n", + "\n", + "We demonstrate a simple scenario where a prompt is acquired by a custom agent deployed on Agent Engine via Vertex AI Prompt Management sdk. The end-user or business users can update the prompt whenever needed and the agent can just be redeployed to read the new prompt, without any changes in the code.\n", + "\n", + "### Notebook Overview\n", + "This notebook guides you through the following key steps:\n", + "\n", + "**1. Creation of prompt using Vertex AI Prompt Management** \n", + "    a. Create the prompt and get the prompt id\n", + "\n", + "**2. Creation of custom agent that uses the prompt in Vertex AI Prompt Management** \n", + "    a. Create the custom agent using the prompt id \n", + "    b. Test and Deploy the agent to Agent Engine\n", + "\n", + "**3. Integration of custom agent to Gemini Enterprise** \n", + "    a. Register the agent with Gemini Enterprise and integrate it\n", + "\n", + "**4. Demonstration of prompt change for custom agent without re-deploying custom agent** \n", + "    a. Make a change in prompt being used by the agent and demonstrate the change in output" + ] + }, + { + "cell_type": "markdown", + "id": "3707f148-1816-4fc5-b60b-a1080685a70e", + "metadata": { + "id": "37f300fa5e86" + }, + "source": [ + "## Imports" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "dd9e3aa7-6a19-48fb-8d6c-ca5e218dfd4e", + "metadata": { + "id": "352c647bd47d" + }, + "outputs": [], + "source": [ + "import vertexai\n", + "from google.adk.agents import Agent\n", + "from google.adk.agents.callback_context import CallbackContext\n", + "from google.genai import types\n", + "from vertexai import agent_engines\n", + "from vertexai.preview import prompts, reasoning_engines\n", + "from vertexai.preview.prompts import Prompt" + ] + }, + { + "cell_type": "markdown", + "id": "81440e98-7541-42e6-9fd4-96e6546acc5b", + "metadata": { + "id": "9a02fc0443de" + }, + "source": [ + "## Constants and Initializations" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "674934a9-d2f0-4644-881b-e6b2b2d9a8ea", + "metadata": { + "id": "4806e8208f1b" + }, + "outputs": [], + "source": [ + "# TODO for Developer: Update project name, location, bucket and schema path (for testing)\n", + "PROJECT_ID = \"[your-project-id]\"\n", + "PROJECT_NUMBER = \"[your-project-number]\"\n", + "LOCATION = \"[your-location]\"\n", + "STAGING_BUCKET = \"[your-bucket]\"\n", + "SCHEMA = \"[your-schema-txt-containing-DDLs]\"\n", + "\n", + "vertexai.init(\n", + " project=PROJECT_ID,\n", + " location=LOCATION,\n", + " staging_bucket=STAGING_BUCKET,\n", + ")\n", + "\n", + "try:\n", + " with open(SCHEMA) as f_handle:\n", + " schema = f_handle.read()\n", + "except FileNotFoundError:\n", + " print(f\"Error: The schema file was not found at '{SCHEMA}'.\")\n", + " print(\"Please update the 'SCHEMA' variable with the correct file path.\")\n", + " schema = \"\" # Assign a default value to prevent further errors" + ] + }, + { + "cell_type": "markdown", + "id": "4aa5342c-3b71-4212-931b-a4ac79d59295", + "metadata": { + "id": "bf35bbdf7278" + }, + "source": [ + "## Prompt creation using Vertex AI Prompt Management sdk" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ef0ffa5f-9357-4939-b30c-ff6c1bf75f03", + "metadata": { + "id": "7937127bcd34" + }, + "outputs": [], + "source": [ + "# Add a new prompt to the Prompt Management store via sdk\n", + "# This can also be done via the UI\n", + "# This task needs to be done only once\n", + "\n", + "sql_query_gen_prompt = \"\"\"\n", + "Look carefully at the schema DDL statements and provide the SQL query for the user's question.\n", + "Also provide a detailed explanation of the query generated.\n", + "If you cannot find a proper schema return \"Please upload a schema or a valid schema\".\n", + "Do not assume anything.\n", + "\"\"\"\n", + "\n", + "sql_query_gen_prompt_obj = Prompt(\n", + " prompt_name=\"SQL Generator\",\n", + " prompt_data=sql_query_gen_prompt,\n", + " model_name=\"gemini-2.0-flash-001\",\n", + ")\n", + "agent_prompt = prompts.create_version(prompt=sql_query_gen_prompt_obj)\n", + "print(\"Prompt id = \", agent_prompt.prompt_id)" + ] + }, + { + "cell_type": "markdown", + "id": "2aa80a71-7470-4c1d-b995-d7715ac69f31", + "metadata": { + "id": "37485861d327" + }, + "source": [ + "![SQL generation prompt in Prompt Management\"](https://services.google.com/fh/files/blogs/prompt_mgmt.png)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "975a9af7-dc54-44f2-bc55-eecbec15ebc2", + "metadata": { + "id": "a00bbd0b0a53" + }, + "outputs": [], + "source": [ + "# List all the prompts and copy the prompt_id of the recently created prompt\n", + "# Alternatively we can get is as: pid = agent_prompt.prompt_id\n", + "\n", + "prompts.list()" + ] + }, + { + "cell_type": "markdown", + "id": "812fddb2-5f2d-42dc-b9dd-526d8cbfff41", + "metadata": { + "id": "831950cda4d6" + }, + "source": [ + "## Create Agent" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "321bbedf-2d72-4bff-b325-cc58d6393341", + "metadata": { + "id": "2aade5b3db6f" + }, + "outputs": [], + "source": [ + "\"\"\"SQL query generator agent\"\"\"\n", + "\n", + "# prompt id obtained after listing prompts\n", + "PID = \"[your-prompt-id]\"\n", + "\n", + "\n", + "def update_instructions_add_schema(callback_context: CallbackContext):\n", + " \"\"\"Add the contents of the file to the agent context.\n", + "\n", + " Args:\n", + " callback_context: The agent context.\n", + " \"\"\"\n", + " from vertexai.preview import prompts\n", + "\n", + " flag = 0\n", + " # acquire the prompt added by an end user and add based instructions to it\n", + " sql_query_gen_prompt = prompts.get(prompt_id=PID).prompt_data\n", + " # add the prompt instruction to the agent context\n", + " callback_context._invocation_context.agent.instruction = sql_query_gen_prompt\n", + "\n", + " # iterate over the callback_context object and check if file data is present\n", + " for part in callback_context.user_content.parts:\n", + " if hasattr(part, \"inline_data\") and getattr(part.inline_data, \"mime_type\", \"\"):\n", + " blob = part.inline_data\n", + " raw_bytes = blob.data\n", + " flag = 1\n", + "\n", + " # update the context if there is any file data found\n", + " if flag and raw_bytes and isinstance(raw_bytes, (bytes, bytearray)):\n", + " uploaded_file_content = raw_bytes.decode(\"utf-8\")\n", + " callback_context._invocation_context.agent.instruction += uploaded_file_content\n", + "\n", + "\n", + "# root agent that does the SQL generation\n", + "root_agent = Agent(\n", + " name=\"SQL_Generation_Agent\",\n", + " model=\"gemini-2.0-flash\",\n", + " description=(\n", + " \"Agent to convert natural language query to SQL based on a given schema DDL.\"\n", + " ),\n", + " before_agent_callback=update_instructions_add_schema,\n", + " generate_content_config=types.GenerateContentConfig(temperature=0.01),\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "ac1dafbb-f501-4ca8-8f49-955424571c22", + "metadata": { + "id": "bddb8ddae816" + }, + "source": [ + "## Test agent locally" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4481ebf6-f990-45ea-b601-9656a53ede46", + "metadata": { + "id": "c5d52171d161" + }, + "outputs": [], + "source": [ + "# create an AdkApp object for testing and deployment of agent\n", + "\n", + "app = reasoning_engines.AdkApp(\n", + " agent=root_agent,\n", + " enable_tracing=True,\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b46b9673-d026-4003-9000-6e929647a93a", + "metadata": { + "id": "4672a9e0575f" + }, + "outputs": [], + "source": [ + "# create a session for interacting with agent\n", + "\n", + "session = app.create_session(user_id=\"u_123\")\n", + "session" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5ec4b2e9-501e-4d28-a118-f9d1b52a4005", + "metadata": { + "id": "b2fa3fcc4244" + }, + "outputs": [], + "source": [ + "# build the user query object for getting response from agent\n", + "\n", + "query = \"list all benefits of every employee mapped to the employee name\"\n", + "\n", + "file_artifact = types.Part.from_bytes(\n", + " mime_type=\"text/plain\", data=schema.encode(\"utf-8\")\n", + ")\n", + "contents = types.Content(\n", + " role=\"user\", parts=[types.Part.from_text(text=query), file_artifact]\n", + ")\n", + "contents.model_dump()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "071c49a4-0d8f-45bc-ab1e-0935514a9ccd", + "metadata": { + "id": "3707c84923f3" + }, + "outputs": [], + "source": [ + "# get output for the user query\n", + "\n", + "for event in app.stream_query(\n", + " user_id=\"u_123\",\n", + " session_id=session.id,\n", + " message=contents.model_dump(),\n", + "):\n", + " print(event[\"content\"][\"parts\"][0][\"text\"])" + ] + }, + { + "cell_type": "markdown", + "id": "ed2d5839-31cd-4acf-ac43-53b398af4c7c", + "metadata": { + "id": "f2165b1edbed" + }, + "source": [ + "## Deploy agent to Agent Engine" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "24fe67c2-0239-4ac7-93b7-a85524c17ddc", + "metadata": { + "id": "0d4c781f4ad9" + }, + "outputs": [], + "source": [ + "# add the necessary dependencies and install agent to agent engine\n", + "\n", + "remote_app = agent_engines.create(\n", + " display_name=\"SQL generator\",\n", + " agent_engine=app,\n", + " requirements=[\n", + " \"google-adk (==1.5.0)\",\n", + " \"google-genai (==1.24.0)\",\n", + " \"pydantic (==2.11.7)\",\n", + " \"google-cloud-aiplatform (==1.101.0)\",\n", + " ],\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a9e7131e-a7ba-4b91-a1bb-2096865a7581", + "metadata": { + "id": "fc1ab19e7f7e" + }, + "outputs": [], + "source": [ + "# get the reasoning engine id\n", + "\n", + "remote_app.resource_name" + ] + }, + { + "cell_type": "markdown", + "id": "7e9ba57c-8e49-4a63-9b6c-9f67e79338d4", + "metadata": { + "id": "82981776ff62" + }, + "source": [ + "## Register agent in Gemini Enterprise" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d5593a82-ffdd-42d8-bd1b-7dd7dcf62c70", + "metadata": { + "id": "3635a286d182" + }, + "outputs": [], + "source": [ + "%%bash\n", + "\n", + "curl -X POST \\\n", + "-H \"Authorization: Bearer $(gcloud auth print-access-token)\" \\\n", + "-H \"Content-Type: application/json\" \\\n", + "-H \"X-Goog-User-Project: [your-project-id]\" \\\n", + "\"https://discoveryengine.googleapis.com/v1alpha/projects/[your-project-id]/locations/global/authorizations?authorizationId=sqlgen2\" \\\n", + "-d '{\n", + "\"name\": \"projects/[your-project-id]/locations/global/authorizations/sqlgen2\",\n", + "\"serverSideOauth2\": {\n", + "\"clientId\": \"[UPDATE-CLIENT-ID]\",\n", + "\"clientSecret\": \"[UPDATE-CLIENT-SECRET]\",\n", + "\"authorizationUri\": \"https://accounts.google.com/o/oauth2/v2/auth?client_id=[UPDATE-CLIENT-ID]&scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fcloud-platform&include_granted_scopes=true&response_type=code&access_type=offline&prompt=consent\",\n", + "\"tokenUri\": \"https://oauth2.googleapis.com/token\"\n", + "}\n", + "}'" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "70b2af97-dc7d-4a58-a5a5-ceb3a82c4cb9", + "metadata": { + "id": "b33096f67166" + }, + "outputs": [], + "source": [ + "%%bash\n", + "\n", + "curl -X POST \\\n", + "-H \"Authorization: Bearer $(gcloud auth print-access-token)\" \\\n", + "-H \"Content-Type: application/json\" \\\n", + "-H \"X-Goog-User-Project: [your-project-id]\" \\\n", + "\"https://discoveryengine.googleapis.com/v1alpha/projects/[your-project-id]/locations/global/collections/default_collection/engines/[your-gemini-enterprise-engine-id]/assistants/default_assistant/agents\" \\\n", + "-d '{\n", + "\"displayName\": \"DDL SQL Generator\",\n", + "\"description\": \"SQL generation agent to help user with SQL queries for a give schema.\",\n", + "\"adk_agent_definition\": {\n", + "\"tool_settings\": {\n", + "\"tool_description\": \"You are an expert SQL developer capable of understanding user queries and converting them to SQL\"\n", + "},\n", + "\"provisioned_reasoning_engine\": {\n", + "\"reasoning_engine\": \"projects/[your-project-number]/locations/us-central1/reasoningEngines/[reasoning-engine-id]\"\n", + "},\n", + "\"authorizations\": [\"projects/[your-project-number]/locations/global/authorizations/sqlgen2\"]\n", + "}\n", + "}'" + ] + }, + { + "cell_type": "markdown", + "id": "79faa8d1-3ee2-4268-8c59-ad1d237b3bcd", + "metadata": { + "id": "3e61ecea5040" + }, + "source": [ + "## SQL Generator agent in Gemini Enterprise" + ] + }, + { + "cell_type": "markdown", + "id": "0dce3cf5-189c-43c1-bc75-8cd769df4200", + "metadata": { + "id": "c18598400989" + }, + "source": [ + "![DDL based SQL query generator agent](https://services.google.com/fh/files/blogs/sql_gen.png)" + ] + } + ], + "metadata": { + "colab": { + "name": "gemini_enterprise_prompt_management.ipynb", + "toc_visible": true + }, + "kernelspec": { + "display_name": "Python 3", + "name": "python3" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} diff --git a/search/gemini-enterprise/gemini_enterprise_session.ipynb b/search/gemini-enterprise/gemini_enterprise_session.ipynb new file mode 100644 index 0000000..3102ed5 --- /dev/null +++ b/search/gemini-enterprise/gemini_enterprise_session.ipynb @@ -0,0 +1,631 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "id": "cf1e9798-cbff-4a30-90fc-cdd3d0814a7c", + "metadata": { + "id": "1Vs2C5ZBkrjmrHTfAyz77hpV" + }, + "outputs": [], + "source": [ + "# Copyright 2025 Google LLC\n", + "#\n", + "# Licensed under the Apache License, Version 2.0 (the \"License\");\n", + "# you may not use this file except in compliance with the License.\n", + "# You may obtain a copy of the License at\n", + "#\n", + "# https://www.apache.org/licenses/LICENSE-2.0\n", + "#\n", + "# Unless required by applicable law or agreed to in writing, software\n", + "# distributed under the License is distributed on an \"AS IS\" BASIS,\n", + "# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n", + "# See the License for the specific language governing permissions and\n", + "# limitations under the License." + ] + }, + { + "cell_type": "markdown", + "id": "08dd40a3-3ecd-4d2b-bbad-cd4486cc1f21", + "metadata": { + "id": "VoJZgH6X77se" + }, + "source": [ + "# Gemini Enterprise custom agent with Vertex AI session" + ] + }, + { + "cell_type": "markdown", + "id": "efddf600-bb65-408f-ac49-72d4c2782b54", + "metadata": { + "id": "qfDq3m0R8Fe1" + }, + "source": [ + "\n", + " \n", + " \n", + " \n", + " \n", + "
    \n", + " \n", + " \"Google
    Open in Colab\n", + "
    \n", + "
    \n", + " \n", + " \"Google
    Open in Colab Enterprise\n", + "
    \n", + "
    \n", + " \n", + " \"Vertex
    Open in Vertex AI Workbench\n", + "
    \n", + "
    \n", + " \n", + " \"GitHub
    View on GitHub\n", + "
    \n", + "
    \n", + "\n", + "
    \n", + "\n", + "Share to:\n", + "\n", + "\n", + " \"LinkedIn\n", + "\n", + "\n", + "\n", + " \"Bluesky\n", + "\n", + "\n", + "\n", + " \"X\n", + "\n", + "\n", + "\n", + " \"Reddit\n", + "\n", + "\n", + "\n", + " \"Facebook\n", + "" + ] + }, + { + "cell_type": "markdown", + "id": "11811f0a-c6ee-4a35-a3ae-f387abb4a810", + "metadata": { + "id": "UD1Cy9O88Zn9" + }, + "source": [ + "| Authors |\n", + "| --- |\n", + "| Rupjit Chakraborty|\n", + "| Sharmila Devi |\n", + "| Parag Mhatre |" + ] + }, + { + "cell_type": "markdown", + "id": "fd037092-f37a-4137-8180-978f6558a767", + "metadata": { + "id": "28b524cdd168" + }, + "source": [ + "## Overview : Custom agent with sessions in Gemini Enterprise\n", + "\n", + "This notebook demonstrates an end-to-end example of a custom agent integrated with Gemini Enterprise that uses [VertexAISessionService](https://google.github.io/adk-docs/sessions/session/#managing-sessions-with-a-sessionservice). In this case, a simple example of a travel planner has been demonstrated, which takes the help of two sub-agents, viz. query completeness checker and itinerary \n", + "generator. This agent helps a user plan their travel based on their travel destination and time of travel. Due to the use of Vertex AI Session service, this agent can also modify generated itineraries.\n", + "\n", + "### Background\n", + "\n", + "Custom agents integrated with Gemini Enterprise may need to leverage a user's previous conversations and preferences to answer a query satisfactorily. By default, [`AdkApp` template is already connected to Vertex AI Agent Engine Sessions through `session_service`](https://cloud.google.com/vertex-ai/generative-ai/docs/agent-engine/sessions/manage-sessions-adk#python). However, this session service is not persistent. There are two persistent session service - `VertexAiSessionService` and `DatabaseSessionService`. The use of `VertexAiSessionService` has been demonstrated below.\n", + "\n", + "### Business Application Scenario\n", + "\n", + "We demonstrate a simple scenario where a user is planning a travel to a certain location. The user provides destination information first but to generate a suitable itinerary the agent also needs a time information. The agent requests the user for time information and after the user provides it, the agent generates the itinerary. After the itinerary generation the user requests modification based on a certain criteria and agent updates the itinerary based on that.\n", + "\n", + "### Notebook Overview\n", + "\n", + "This notebook guides you through the following key steps:\n", + "\n", + "1. Creation of custom travel agent\n", + " a. Create the sub-agents - query completeness checker and itinerary generator \n", + " b. Create the root agent to call the sub-agents to respond to a user query\n", + "2. Local demo of the custom travel agent using VertexAISessionService\n", + " a. Create an Agent Engine instance and VertexAISessionService \n", + " b. Demo user query response using the VertexAISessionService\n", + "3. Update/Deploy the custom travel agent to agent engine and demo user queries \n", + " a. Update the agent engine instance created in 2. \n", + " b. Demo user query response using the VertexAISessionService via Agent Engine remote agent.\n", + "4. Integrate the agent with Gemini Enterprise\n", + " a. Register the agent in Gemini Enterprise" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "73051a45bd86" + }, + "outputs": [], + "source": [ + "%pip install --upgrade google-adk google-genai google-cloud-aiplatform" + ] + }, + { + "cell_type": "markdown", + "id": "cbbd420a-328e-4c29-9b75-329a8b6128da", + "metadata": { + "id": "37f300fa5e86" + }, + "source": [ + "## Imports" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e5e6755d-f1f2-41c9-8ae9-176a7d56cb56", + "metadata": { + "id": "79697b7e4ea7" + }, + "outputs": [], + "source": [ + "import vertexai\n", + "from dotenv import load_dotenv\n", + "from google import adk\n", + "from google.adk.agents import Agent\n", + "from google.adk.sessions import VertexAiSessionService\n", + "from google.adk.tools.agent_tool import AgentTool\n", + "from google.genai import types\n", + "from vertexai import agent_engines\n", + "from vertexai.preview import reasoning_engines" + ] + }, + { + "cell_type": "markdown", + "id": "94c74b28-3dc3-4f45-aa55-2f8b470777b5", + "metadata": { + "id": "9a02fc0443de" + }, + "source": [ + "## Constants and Initializations" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "541b8f47-ce1a-42a1-9e7f-f85e5f7530be", + "metadata": { + "id": "09ba8e3747ff" + }, + "outputs": [], + "source": [ + "# TODO for Developer: Update project, location, bucket details\n", + "\n", + "PROJECT_ID = \"[your-project-name]\"\n", + "PROJECT_NUMBER = \"[your-project-number]\"\n", + "LOCATION = \"[your-location]\"\n", + "STAGING_BUCKET = \"[your-bucket]\"\n", + "\n", + "vertexai.init(\n", + " project=PROJECT_ID,\n", + " location=LOCATION,\n", + " staging_bucket=STAGING_BUCKET,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "75881789-6436-4567-ab62-f6d2e310da66", + "metadata": { + "id": "7aeeb9e88bc4" + }, + "source": [ + "## Set and load environment variables" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "900f5dc1-909d-4e47-9f0b-30c598ed756f", + "metadata": { + "id": "f3d99cee0b8d" + }, + "outputs": [], + "source": [ + "# environment variables\n", + "\n", + "ENV_SETTINGS = \"\"\"\n", + "# Choose Model Backend: 0 -> API Key, 1 -> Vertex AI\n", + "GOOGLE_GENAI_USE_VERTEXAI=1\n", + "\n", + "# --- API Key Configuration (if GOOGLE_GENAI_USE_VERTEXAI=0) ---\n", + "# Obtain your API key from Google AI Studio or Google Cloud console\n", + "GOOGLE_API_KEY=YOUR_GOOGLE_API_KEY\n", + "\n", + "# --- Vertex AI Backend Configuration (if GOOGLE_GENAI_USE_VERTEXAI=1) ---\n", + "# Your Google Cloud Project ID\n", + "GOOGLE_CLOUD_PROJECT=[your-project-name]\n", + "# The location (region) for Vertex AI services\n", + "GOOGLE_CLOUD_LOCATION=[your-location]\n", + "\"\"\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1eabbccc-d10f-4c5a-a640-d9c2fc2d0897", + "metadata": { + "id": "e28f8b752601" + }, + "outputs": [], + "source": [ + "# Save and load environment variables\n", + "\n", + "with open(\".env\", \"w\") as f_handle:\n", + " f_handle.write(ENV_SETTINGS)\n", + "\n", + "load_dotenv()" + ] + }, + { + "cell_type": "markdown", + "id": "a1d151cf-e30b-4561-b2f6-ecab61560b82", + "metadata": { + "id": "8cf87ececdd4" + }, + "source": [ + "## Create agent and sub-agents" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "bf77bce0-608c-4ec4-8cfa-ede5e90e9993", + "metadata": { + "id": "059794ab594e" + }, + "outputs": [], + "source": [ + "# prompt to check if the user query is complete in\n", + "\n", + "QUERY_CHECK_PROMPT = \"\"\"\n", + "You are an expert travel planner and can check a user query for completeness and correctness\n", + "\n", + "INSTRUCTIONS\n", + "Given the user query check if \n", + "1. the query is related to travel itinerary planning\n", + "2. the query mandatorily contains both place and time of travel\n", + "\n", + "EXAMPLE\n", + "travel is Aug --> incomplete query as no place of travel information\n", + "travel to Karnataka --> incomplete query as no time of travel information\n", + "travel to Karnataka in Aug --> correct query\n", + "\n", + "OUTPUT\n", + "If the user query is complete: just say \"query is complete\"\n", + "If the user query is not complete: question user to complete the query like \"Can you provide the month information ?\"\n", + "\"\"\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9ea216cd-ae30-453a-81e8-c817cd35add0", + "metadata": { + "id": "476202ab69dd" + }, + "outputs": [], + "source": [ + "travel_query_validator = Agent(\n", + " model=\"gemini-2.5-flash\",\n", + " name=\"travel_query_validator\",\n", + " description=\"This agent checks if the user query appropriate\",\n", + " instruction=QUERY_CHECK_PROMPT,\n", + " disallow_transfer_to_parent=False,\n", + " disallow_transfer_to_peers=True,\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d005f456-3fe3-42f0-a881-91c6e4c8f2a2", + "metadata": { + "id": "6525470f6823" + }, + "outputs": [], + "source": [ + "ITINERARY_PROMPT = \"\"\"\n", + "You are an expert travel planner and can provide a user itinerary\n", + "\n", + "INSTRUCTION\n", + "Given the user query, provide a suitable itinerary\n", + "\"\"\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c3833926-426b-4f16-840b-76fc2045efc0", + "metadata": { + "id": "d0dd2872f4f6" + }, + "outputs": [], + "source": [ + "planner_agent = Agent(\n", + " model=\"gemini-2.5-flash\",\n", + " name=\"planner_agent\",\n", + " description=\"This agent provides a travel plan to user based on the query\",\n", + " instruction=ITINERARY_PROMPT,\n", + " disallow_transfer_to_parent=True,\n", + " disallow_transfer_to_peers=True,\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "46c491c1-b2b3-4b49-b1c8-b7e8bd343edf", + "metadata": { + "id": "ffe1b6d2d110" + }, + "outputs": [], + "source": [ + "root_agent = Agent(\n", + " name=\"root\",\n", + " model=\"gemini-2.5-flash\",\n", + " description=(\n", + " \"A travel agent that takes a user query and prepares a detailed itinerary with the help of sub-agents\"\n", + " ),\n", + " generate_content_config=types.GenerateContentConfig(temperature=0.01),\n", + " tools=[AgentTool(agent=travel_query_validator), AgentTool(agent=planner_agent)],\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "63769b50-f502-478d-8fe1-d2c606441991", + "metadata": { + "id": "d6775ab8dc85" + }, + "source": [ + "## Test the agent locally" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8d800dfd-1cf3-419b-a84a-5c3a655550cb", + "metadata": { + "id": "90be473ddf07" + }, + "outputs": [], + "source": [ + "# TODO for Developer: Update user_id\n", + "\n", + "import nest_asyncio\n", + "\n", + "nest_asyncio.apply()\n", + "\n", + "import asyncio\n", + "\n", + "# Create an agent engine instance\n", + "agent_engine = agent_engines.create()\n", + "app_name = agent_engine.name.split(\"/\")[-1]\n", + "user_id = \"[session-user-id]\"\n", + "\n", + "# Create the ADK runner with VertexAiSessionService\n", + "session_service = VertexAiSessionService(\n", + " project=PROJECT_ID, location=LOCATION, agent_engine_id=app_name\n", + ")\n", + "runner = adk.Runner(\n", + " agent=root_agent, app_name=app_name, session_service=session_service\n", + ")\n", + "\n", + "\n", + "# Helper method to send query to the runner\n", + "async def call_agent_async(query, session_id, user_id):\n", + " content = types.Content(role=\"user\", parts=[types.Part(text=query)])\n", + " events = runner.run(user_id=user_id, session_id=session_id, new_message=content)\n", + " for event in events:\n", + " if event.is_final_response():\n", + " final_response = event.content.parts[0].text\n", + " print(\"Agent: \", final_response)\n", + "\n", + "\n", + "async def main():\n", + " session = await session_service.create_session(app_name=app_name, user_id=user_id)\n", + " print(f\"Session created with ID: {session.id}\")\n", + " user_queries = [\n", + " \"trip to London\",\n", + " \"in Jan or Feb\",\n", + " \"suitable for aged people\",\n", + " ]\n", + " for user_query in user_queries:\n", + " print(f\"User Query: {user_query}\")\n", + " await call_agent_async(user_query, session.id, user_id)\n", + "\n", + "\n", + "if __name__ == \"__main__\":\n", + " asyncio.run(main())" + ] + }, + { + "cell_type": "markdown", + "id": "76ec3c5f-1c2e-401c-b389-5dfc3c8098df", + "metadata": { + "id": "8ad6030a1761" + }, + "source": [ + "## Update the agent in agent engine" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d6cb40dd-935f-4d42-8fca-99f82698b0e6", + "metadata": { + "id": "6cc73eaa5137" + }, + "outputs": [], + "source": [ + "# TODO for Developer: Update display_name (of agent)\n", + "\n", + "# create a callable session service builder that can use with AdkApp\n", + "\n", + "\n", + "def session_service_builder():\n", + " return VertexAiSessionService(\n", + " project=PROJECT_ID, location=LOCATION, agent_engine_id=app_name\n", + " )\n", + "\n", + "\n", + "# create the AdkApp with the session service\n", + "adk_app = reasoning_engines.AdkApp(\n", + " agent=root_agent,\n", + " enable_tracing=True,\n", + " session_service_builder=session_service_builder,\n", + ")\n", + "\n", + "# update the agent engine instance (created during local testing)\n", + "remote_app = agent_engines.update(\n", + " display_name=\"[agent-display-name]\",\n", + " resource_name=agent_engine.name,\n", + " agent_engine=adk_app,\n", + " requirements=[\n", + " \"google-adk (==1.5.0)\",\n", + " ],\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "54f034af-c1fd-4365-8219-fbed0e16fba9", + "metadata": { + "id": "0108a0d59750" + }, + "source": [ + "## Test the agent in agent engine" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c2ab846f-baba-4657-b834-10cc09567475", + "metadata": { + "id": "f024562b934f" + }, + "outputs": [], + "source": [ + "# TODO for Developer: Update user_id\n", + "\n", + "user_queries = [\"trip to London\", \"in Jan or Feb\", \"suitable for aged people\"]\n", + "user_id = \"[session-user-id]\"\n", + "\n", + "remote_session = remote_app.create_session(user_id=user_id)\n", + "\n", + "for user_query in user_queries:\n", + " print(f\"User Query: {user_query}\")\n", + " contents = types.Content(role=\"user\", parts=[types.Part.from_text(text=user_query)])\n", + " for event in remote_app.stream_query(\n", + " user_id=user_id, session_id=remote_session[\"id\"], message=contents.model_dump()\n", + " ):\n", + " if \"text\" in event[\"content\"][\"parts\"][0]:\n", + " print(f\"Agent: {event['content']['parts'][0]['text']}\")" + ] + }, + { + "cell_type": "markdown", + "id": "f6be3a75-5cdd-4d1b-956a-211e1d8e4507", + "metadata": { + "id": "8adac8e3459c" + }, + "source": [ + "## Register agent in Gemini Enterprise" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "245a96d4-cb28-4bde-b8a5-c2d8f5542092", + "metadata": { + "id": "3635a286d182" + }, + "outputs": [], + "source": [ + "%%bash\n", + "\n", + "curl -X POST \\\n", + "-H \"Authorization: Bearer $(gcloud auth print-access-token)\" \\\n", + "-H \"Content-Type: application/json\" \\\n", + "-H \"X-Goog-User-Project: [your-project-id]\" \\\n", + "\"https://discoveryengine.googleapis.com/v1alpha/projects/[your-project-id]/locations/global/authorizations?authorizationId=travel\" \\\n", + "-d '{\n", + "\"name\": \"projects/[your-project-id]/locations/global/authorizations/travel\",\n", + "\"serverSideOauth2\": {\n", + "\"clientId\": \"[UPDATE-CLIENT-ID]\",\n", + "\"clientSecret\": \"[UPDATE-CLIENT-SECRET]\",\n", + "\"authorizationUri\": \"https://accounts.google.com/o/oauth2/v2/auth?client_id=[UPDATE-CLIENT-ID]&scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fcloud-platform&include_granted_scopes=true&response_type=code&access_type=offline&prompt=consent\",\n", + "\"tokenUri\": \"https://oauth2.googleapis.com/token\"\n", + "}\n", + "}'" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "884de4d3-6f31-492c-9bc9-d4a65071126f", + "metadata": { + "id": "b33096f67166" + }, + "outputs": [], + "source": [ + "%%bash\n", + "\n", + "curl -X POST \\\n", + "-H \"Authorization: Bearer $(gcloud auth print-access-token)\" \\\n", + "-H \"Content-Type: application/json\" \\\n", + "-H \"X-Goog-User-Project: [your-project-id]\" \\\n", + "\"https://discoveryengine.googleapis.com/v1alpha/projects/[your-project-id]/locations/global/collections/default_collection/engines/[your-app-id]/assistants/default_assistant/agents\" \\\n", + "-d '{\n", + "\"displayName\": \"DDL SQL Generator\",\n", + "\"description\": \"SQL generation agent to help user with SQL queries for a give schema.\",\n", + "\"adk_agent_definition\": {\n", + "\"tool_settings\": {\n", + "\"tool_description\": \"You are an expert SQL developer capable of understanding user queries and converting them to SQL\"\n", + "},\n", + "\"provisioned_reasoning_engine\": {\n", + "\"reasoning_engine\": \"projects/[your-project-number]/locations/us-central1/reasoningEngines/[reasoning-engine-id]\"\n", + "},\n", + "\"authorizations\": [\"projects/[your-project-number]/locations/global/authorizations/travel\"]\n", + "}\n", + "}'" + ] + }, + { + "cell_type": "markdown", + "id": "41f8a8fb-d1e5-426b-b824-959643a97b62", + "metadata": { + "id": "0a5c58d0b550" + }, + "source": [ + "## Travel Agent in Gemini Enterprise\n", + "\n", + "![DDL based SQL query generator agent](https://services.google.com/fh/files/blogs/ss_travel_agent.png)" + ] + } + ], + "metadata": { + "colab": { + "name": "gemini_enterprise_session.ipynb", + "toc_visible": true + }, + "kernelspec": { + "display_name": "Python 3", + "name": "python3" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} diff --git a/search/gemini-enterprise/group-licensing/.gitignore b/search/gemini-enterprise/group-licensing/.gitignore new file mode 100644 index 0000000..b6deaca --- /dev/null +++ b/search/gemini-enterprise/group-licensing/.gitignore @@ -0,0 +1,64 @@ +# Copyright 2026 Google LLC +# +# 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 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Binaries +/bin/ +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Go build cache and test artifacts +*.test +*.out +coverage.html +coverage.txt + +# Go workspace (local only) +go.work +go.work.sum + +# Secrets and credentials — NEVER commit these +*.json.key +service-account*.json +*-credentials.json +*-keyfile.json +.env +.env.* +/run/ +secrets/ + +# GCP CLI artifacts +.gcloudignore +gcloud-service-key.json + +# IDE +.idea/ +.vscode/ +*.swp +*.swo + +# macOS +.DS_Store +.AppleDouble + +# Test and coverage output +*.coverprofile + +# Local deployment scripts with sensitive values +deploy-local.sh + +# AI stuff +.claude \ No newline at end of file diff --git a/search/gemini-enterprise/group-licensing/Dockerfile b/search/gemini-enterprise/group-licensing/Dockerfile new file mode 100644 index 0000000..55da300 --- /dev/null +++ b/search/gemini-enterprise/group-licensing/Dockerfile @@ -0,0 +1,38 @@ +# Copyright 2026 Google LLC +# +# 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 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# ---- Builder ---- +FROM golang:1.25-alpine AS builder + +WORKDIR /app + +COPY go.mod go.sum ./ +RUN go mod download + +COPY . . + +RUN CGO_ENABLED=0 GOOS=linux go build \ + -trimpath \ + -ldflags="-s -w" \ + -o /job \ + ./cmd/job + +# ---- Runtime ---- +# distroless/static includes CA certificates and timezone data but no shell, +# package manager, or other tooling. The :nonroot variant runs as UID 65532. +FROM gcr.io/distroless/static-debian12:nonroot + +COPY --from=builder /job /job + +ENTRYPOINT ["/job"] diff --git a/search/gemini-enterprise/group-licensing/LICENSE b/search/gemini-enterprise/group-licensing/LICENSE new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/search/gemini-enterprise/group-licensing/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/search/gemini-enterprise/group-licensing/README.md b/search/gemini-enterprise/group-licensing/README.md new file mode 100644 index 0000000..74ff947 --- /dev/null +++ b/search/gemini-enterprise/group-licensing/README.md @@ -0,0 +1,176 @@ +# Gemini Box Office + +A Cloud Run Job that automates Gemini Enterprise license lifecycle management through scheduled batch reconciliation. It bridges the gap between Google Cloud Identity group membership and Discovery Engine license assignment, providing group-based SKU mapping, automatic provisioning, and stale-license cleanup. + +**Authors: [Matt Williams](https://github.com/williamsmt), [Charles He](https://github.com/googlecharles)** + +## How it works + +Two Cloud Run Job definitions are deployed from a single container image, each triggered by Cloud Scheduler on its own schedule: + +| Job | `JOB_TYPE` | Schedule | Description | +|---|---|---|---| +| `gemini-box-office-joiner` | `joiner` | Daily (24hr) | Pages through all configured group members and grants missing licenses. If a user belongs to multiple SKU-mapped groups, the highest-precedence SKU wins. | +| `gemini-box-office-gc` | `garbage_collection` | Every 6 hours | Pages through all licensed users and revokes licenses from users who are stale (no login within the configured threshold) or no longer a member of any entitled group. | + +## Environment variables + +| Variable | Description | Default | +|---|---|---| +| `JOB_TYPE` | Selects the workflow to run (`joiner` or `garbage_collection`). Required. | — | +| `DRY_RUN` | When `true`, the full evaluation runs but no write API calls are made. Can be overridden per execution via the Cloud Scheduler request body. | `false` | +| `CLOUD_RUN_TASK_INDEX` | Injected by Cloud Run. 0-based index of this task instance. | `0` | +| `CLOUD_RUN_TASK_COUNT` | Injected by Cloud Run. Total number of concurrent task instances. | `1` | + +## SKU names and precedence + +The current list of Gemini Enterprise subscription tier SKUs is [here](https://docs.cloud.google.com/gemini/enterprise/docs/reference/rest/v1beta/SubscriptionTier). + +When a user qualifies for multiple SKUs, the highest-ranked one is assigned: + +1. `SUBSCRIPTION_TIER_SEARCH_AND_ASSISTANT` +2. `SUBSCRIPTION_TIER_ENTERPRISE` +3. `SUBSCRIPTION_TIER_SEARCH` +4. `SUBSCRIPTION_TIER_NOTEBOOK_LM` +5. `SUBSCRIPTION_TIER_AGENTSPACE_BUSINESS` +6. `SUBSCRIPTION_TIER_AGENTSPACE_STARTER` +7. `SUBSCRIPTION_TIER_FRONTLINE_WORKER` +8. `SUBSCRIPTION_TIER_FRONTLINE_STARTER` +9. `SUBSCRIPTION_TIER_ENTERPRISE_EMERGING` +10. `SUBSCRIPTION_TIER_EDU_PRO` +11. `SUBSCRIPTION_TIER_EDU` +12. `SUBSCRIPTION_TIER_EDU_PRO_EMERGING` +13. `SUBSCRIPTION_TIER_EDU_EMERGING` + +## Configuration + +Configuration is stored in **GCP Secret Manager** and mounted as a file volume into the job at `/run/secrets/entitlements.json`. + +```json +{ + "billing_account_id": "ABCDE-12345-FGHIJ", + "projects": { + "customer-project-alpha": [ + { + "subscription_tier": "SUBSCRIPTION_TIER_ENTERPRISE", + "location": "global", + "groups": [ + "group-data-scientists@example.com", + "group-senior-devs@example.com" + ] + }, + { + "subscription_tier": "SUBSCRIPTION_TIER_AGENTSPACE_BUSINESS", + "location": "global", + "groups": [ + "group-marketing@example.com", + "group-general-staff@example.com" + ] + } + ] + }, + "settings": { + "staleness_threshold_days": 30 + } +} +``` + +| Field | Description | +|---|---| +| `billing_account_id` | The GCP billing account ID associated with the managed projects. Required. | +| `projects` | Map of GCP project ID → list of entitlement entries (one per SKU/location combination). | +| `projects[].subscription_tier` | The Gemini SKU for this entry. See **SKU precedence** below for all valid values. | +| `projects[].location` | Geographic region for license management. Must be one of: `global`, `us`, `eu`. | +| `projects[].groups` | List of Google Group email addresses whose members are entitled to this SKU. | +| `settings.staleness_threshold_days` | Users inactive beyond this many days are revoked. Set to `0` (or omit) to disable staleness checking — only entitlement is evaluated. | + +Multiple projects can be managed from a single job by adding additional entries under `projects`. For very large project portfolios, set `--tasks N` on the job execution to shard the project list across N parallel task instances. + +## IAM, OAuth, API requirements + +The job's service account requires the following: + +**IAM roles:** +- `roles/discoveryengine.admin` — list and update user licenses +- `roles/cloudidentity.groups.viewer` — list group members and verify membership +- `roles/secretmanager.secretAccessor` — read the mounted configuration secret +- `roles/billing.viewer` — read the purchased Gemini Enterprise subscription configurations + +**OAuth scopes:** +- `https://www.googleapis.com/auth/cloud-platform` +- `https://www.googleapis.com/auth/admin.directory.group.member.readonly` + +**API requirements:** +- Discovery Engine API (GCP) +- Resource Manager API (GCP) +- Admin SDK API (Cloud Identity / Workspace) + +The job uses **Application Default Credentials**. No service account key files are required. + +**Trigger authorization:** The Cloud Scheduler service account must be granted `roles/run.invoker` on each Cloud Run Job resource. + +## Project structure + +``` +cmd/job/ # Job entry point +internal/ + adapters/ + cloudidentity/ # Cloud Identity Admin API adapter + discoveryengine/ # Discovery Engine API adapter + config/ # Config loading and validation (entitlement config + job settings) + middleware/ # Structured logging middleware + models/ # Domain types, DTOs, errors, constants, enums + ports/ # Interface definitions (IdpClient, GeminiClient) + services/ # Business logic (JoinerService, GCService) +docs/ + PRD.md # Product requirements + TDD.md # Technical design document +Dockerfile # Multi-stage build → distroless runtime image +``` + +## Building + +```bash +docker build -t gemini-box-office:latest . +``` + +## Running tests + +```bash +go test ./... +``` + +All tests are unit tests and require no external services or credentials. + +## Deploying to Cloud Run as Job + +An example command to deploy as a job on Cloud Run is below. For more information on creating Cloud Run jobs, reference documentation [here](https://docs.cloud.google.com/run/docs/create-jobs). +```bash +gcloud run jobs deploy [name_of_job] \ + --source . \ + --region [desired_cloud_run_region] \ + --update-secrets=/run/secrets/entitlements.json=[name_of_secret_in_secret_manager]:latest \ + --set-env-vars JOB_TYPE=[joiner_or_garbage_collection],DRY_RUN=false +``` + +## Job output + +On completion the job exits `0` (success) or `1` (failure). Results are emitted as structured JSON log entries to stdout and ingested automatically by Cloud Logging: + +```json +{"time":"...","level":"INFO","workflow":"joiner","task_index":0,"msg":"joiner workflow complete","duration_ms":4821,"licenses_granted":42,"licenses_soft_failed":0,"groups_processed":5,"dry_run":false} +``` + +If the license pool for a SKU is exhausted mid-run, a warning is emitted and the job continues: + +```json +{"time":"...","level":"WARN","workflow":"joiner","task_index":0,"msg":"license pool exhausted, soft-failing remaining users","project_id":"customer-project-alpha","license_config_path":"projects/123/locations/global/licenseConfigs/ent-config","available":3,"soft_failed":17} +``` + +```json +{"time":"...","level":"INFO","workflow":"garbage_collection","task_index":0,"msg":"garbage collection workflow complete","duration_ms":9134,"licenses_revoked":12,"users_evaluated":500,"dry_run":false} +``` + +## Logging + +All log output is structured JSON emitted to stdout for automatic ingestion by Cloud Logging. Every entry includes `workflow` and `task_index` fields. PII (user email addresses) is never logged. diff --git a/search/gemini-enterprise/group-licensing/cmd/job/main.go b/search/gemini-enterprise/group-licensing/cmd/job/main.go new file mode 100644 index 0000000..906fe2c --- /dev/null +++ b/search/gemini-enterprise/group-licensing/cmd/job/main.go @@ -0,0 +1,133 @@ +/* +Copyright © 2026 Google LLC + +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 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "cmp" + "context" + "log/slog" + "maps" + "os" + "slices" + + discoveryenginesdk "cloud.google.com/go/discoveryengine/apiv1" + admin "google.golang.org/api/admin/directory/v1" + cloudresourcemanager "google.golang.org/api/cloudresourcemanager/v3" + "google.golang.org/api/option" + + "github.com/cloud-gtm/gemini-box-office/internal/adapters/cloudidentity" + "github.com/cloud-gtm/gemini-box-office/internal/adapters/discoveryengine" + "github.com/cloud-gtm/gemini-box-office/internal/adapters/resourcemanager" + "github.com/cloud-gtm/gemini-box-office/internal/config" + "github.com/cloud-gtm/gemini-box-office/internal/models" + "github.com/cloud-gtm/gemini-box-office/internal/models/dto" + "github.com/cloud-gtm/gemini-box-office/internal/services" +) + +func main() { + // Step 1: plain JSON logger for startup errors, before settings are loaded. + slog.SetDefault(slog.New(slog.NewJSONHandler(os.Stdout, nil))) + + // Step 2: load entitlement config. + cfg, err := config.Load(models.ConfigFilePath) + if err != nil { + slog.Error("failed to load entitlement config", slog.Any("error", err)) + os.Exit(1) + } + + // Step 3: load job settings from Cloud Run Job environment variables. + settings, err := config.LoadJobSettings() + if err != nil { + slog.Error("failed to load job settings", slog.Any("error", err)) + os.Exit(1) + } + + // Step 4: re-set the default logger with workflow and task_index attributes + // so that every subsequent log line — including those emitted by the service + // layer via middleware.LoggerFromContext — carries these fields automatically. + slog.SetDefault(slog.New(slog.NewJSONHandler(os.Stdout, nil)).With( + slog.String("workflow", string(settings.JobType)), + slog.Int("task_index", settings.TaskIndex), + )) + + // Step 5: partition cfg.Projects to only the entries assigned to this task. + // Iterate project keys in sorted order for determinism across all tasks. + sortedKeys := slices.SortedFunc(maps.Keys(cfg.Projects), cmp.Compare[string]) + + partitioned := make(map[string]config.ProjectConfig, len(sortedKeys)) + for i, key := range sortedKeys { + if i%settings.TaskCount == settings.TaskIndex { + partitioned[key] = cfg.Projects[key] + } + } + + if len(partitioned) == 0 { + slog.Info("no projects assigned to this task; nothing to do", + slog.Int("task_index", settings.TaskIndex), + slog.Int("task_count", settings.TaskCount), + slog.Int("total_projects", len(cfg.Projects)), + ) + os.Exit(0) + } + + cfg.Projects = partitioned + + ctx := context.Background() + + // Step 6: wire adapters — identical to cmd/server/main.go. + adminSvc, err := admin.NewService(ctx, option.WithScopes(admin.AdminDirectoryGroupMemberReadonlyScope)) + if err != nil { + slog.Error("failed to create admin SDK client", slog.Any("error", err)) + os.Exit(1) + } + + deClient, err := discoveryenginesdk.NewUserLicenseClient(ctx) + if err != nil { + slog.Error("failed to create discovery engine client", slog.Any("error", err)) + os.Exit(1) + } + + crmSvc, err := cloudresourcemanager.NewService(ctx, option.WithScopes(cloudresourcemanager.CloudPlatformReadOnlyScope)) + if err != nil { + slog.Error("failed to create resource manager client", slog.Any("error", err)) + os.Exit(1) + } + + idpAdapter := cloudidentity.New(adminSvc) + geminiAdapter := discoveryengine.New(deClient) + rmAdapter := resourcemanager.New(crmSvc) + + // Step 7: run the workflow determined by JobType. + switch settings.JobType { + case models.WorkflowJoiner: + req := dto.SyncAddRequest{DryRun: &settings.DryRun} + if _, err := services.NewJoinerService(idpAdapter, geminiAdapter, rmAdapter).Run(ctx, cfg, req); err != nil { + slog.Error("joiner workflow failed", slog.Any("error", err)) + os.Exit(1) + } + + case models.WorkflowGarbageCollection: + req := dto.SyncRemoveRequest{DryRun: &settings.DryRun} + if _, err := services.NewGCService(idpAdapter, geminiAdapter).Run(ctx, cfg, req); err != nil { + slog.Error("garbage collection workflow failed", slog.Any("error", err)) + os.Exit(1) + } + } + + // Step 8: clean exit. + os.Exit(0) +} diff --git a/search/gemini-enterprise/group-licensing/docs/PRD.md b/search/gemini-enterprise/group-licensing/docs/PRD.md new file mode 100644 index 0000000..1704f81 --- /dev/null +++ b/search/gemini-enterprise/group-licensing/docs/PRD.md @@ -0,0 +1,90 @@ +## **1. Executive Summary** {#1.-executive-summary} + +Gemini Enterprise licensing operates on a per-user model that currently lacks the granularity required for large-scale enterprise deployment. The native "Auto-assign license" feature in the Google Cloud Console for Gemini Enterprise is binary (on/off) and cannot distinguish between license SKUs (e.g., Standard, Enterprise, Enterprise Plus, vs. Frontline) nor does it respect user group membership. + +This document outlines the requirements for an automated solution to manage license lifecycles. This solution will bridge the gap between Billing Account subscriptions and Project-level enforcement, utilizing a **Scheduled Batch Reconciliation** approach for robust state management, eventual consistency, and cleanup. + +## **2. Problem Statement** {#2.-problem-statement} + +Enterprise customers managing Gemini Enterprise licenses face significant friction due to these primary limitations in the current Cloud Console experience: + +1. **No SKU Differentiation:** The "Auto-assign" toggle grants a license to any user accessing the project, regardless of whether they require a "Standard" or "Frontline" seat. This leads to license waste and compliance issues. + +2. **Lack of Group Inheritance:** Admins cannot map Google Cloud Identity or external IdP groups (e.g., "Sales_NorthAM") to specific license types. + +3. **Missing Automated Removal & Cleanup Tools:** There is no automation to assist Admins with automatic removal nor automatic re-assignment of Gemini Enterprise licenses. This is causing maintainability friction for admins & contention during renewals. + +4. **No Bulk Management:** The Google Cloud Console manage user license assignment page currently does not afford Admins the ability to assign licenses in bulk using a CSV or other structured file. The only option available to Admins is to manually enter multiple email addresses on the UI text box directly. + +5. **Manual Toil:** The only viable workaround currently is manual assignment per user, which is operationally unscalable for organizations with thousands of seats. + +## **3. Goals & Non-Goals** {#3.-goals-&-non-goals} + +### **3.1 Goals** {#3.1-goals} + +* **Group-Based Assignment:** Enable administrators to map specific Identity Groups (Google Groups) to specific Gemini Enterprise license SKUs. + +* **Scheduled Reconciliation (Batch Job):** Implement a periodic "state-of-the-world" sweep that serves two critical functions: + + * **Garbage Collection (Leaver Support):** Identify and revoke licenses for users who are inactive or no longer present in valid groups. + + * **Self-Healing (Joiner Support):** Identify valid group members missing a license (e.g., due to accidental manual deletion or API failures) and re-provision them, ensuring eventual consistency. + +### **3.2 Non-Goals** {#3.2-non-goals} + +* **Real-Time Lifecycle Management:** Immediate "Joiner/Leaver" support via push notifications is strictly **out of scope** due to API limitations and security concerns. + +* **3rd Party Identity / Workforce Identity Federation (WIF):** Support for ephemeral identities authenticated via WIF is strictly **out of scope** due to technical feasibility concerns. + +* **Billing Account Level Enforcement:** This solution focuses on Project-level assignment automation, not changing the underlying billing infrastructure. + +## **4. User Stories** {#4.-user-stories} + +* **Use Case 1 (SKU Segmentation):** As a System Administrator, I want to ensure that my "Field Sales" group automatically receives "Frontline" licenses and my "Software Engineering" group receives "Enterprise" licenses, so that I optimize my spend. + +* **Use Case 2 (Onboarding):** As a Hiring Manager, I want new employees added to the "Gemini-Users" group to automatically have a license assigned without filing a ticket. + +* **Use Case 3 (Scheduled Offboarding):** As a Security Admin, I want a user's Gemini license to be revoked during the next scheduled reconciliation job when they are removed from the "Active Employees" group to maintain compliance. + +* **Use Case 4 (Hygiene & Reliability):** As a FinOps Lead, I want a nightly job that sweeps my user base to remove unused licenses (saving money) while simultaneously fixing any missing assignments for active users (reducing support tickets). + +## **5. Functional Requirements** {#5.-functional-requirements} + +### **5.1 Configuration & Mapping** {#5.1-configuration-&-mapping} + +* **FR-01:** The system MUST allow admins to define a configuration file (or map) linking one or more Google Groups identified by group email address(es) to a specific Gemini License SKU ID. + +* **FR-02:** The system MUST support multiple distinct mappings within the same GCP Project. This also needs to be scalable across multiple GCP Projects. + +### **5.2 Batch Reconciliation Engine** {#5.2-batch-reconciliation-engine} + +* **FR-03 (The Sweeper):** The system MUST support a configurable schedule (e.g., daily/weekly) to scan the full membership of all configured Google Groups against the list of currently assigned licenses. + +* **FR-04 (Garbage Collection Logic):** + + * If a user HAS a license but HAS NOT logged in for `X` days (Staleness): **Revoke License.** Staleness checking is optional and disabled when `staleness_threshold_days` is `0` or omitted from configuration. + * If a user has **never** logged in, the staleness reference date falls back to the license assignment date. This prevents a recently provisioned account from being immediately revoked before the user has had a chance to sign in. If the user still has not logged in after `X` days from assignment, the license is revoked. + +* **FR-05 (Self-Healing Logic):** + + * If a user IS in a valid group but DOES NOT HAVE a license: **Grant License.** + +* **FR-06 (Conflict Resolution):** If a user is in multiple groups mapped to different SKUs, the batch job MUST deterministically apply a precedence rule (e.g., "Enterprise" trumps "Frontline"). + +## **6. Technical Constraints** {#6.-technical-constraints} + +* **API Rate Limits:** The solution must respect the quotas for the `Cloud Billing` and `Enterprise License Manager` APIs, implementing exponential backoff where necessary. + +* **Latency:** License assignment and revocation are eventually consistent and occur based on the execution frequency of the scheduled batch jobs. + +* **License Type:** Customer should be on Gemini Enterprise subscription licensing SKUs (not on the legacy Agentspace licensing mechanism). + +## **7. Metrics & Success Criteria** {#7.-metrics-&-success-criteria} + +* **Reduction in Unassigned Seats:** % utilization of purchased licenses increases. + +* **Operational Savings:** Reduction in manual "Grant License" tickets filed. + +* **Security Compliance:** Time-to-revocation for terminated employees \< 24 hours (or the configured batch frequency). + +* **Accuracy:** \< 1% of active users reporting "License Not Found" errors (indicating aggressive Garbage Collection). diff --git a/search/gemini-enterprise/group-licensing/docs/TDD.md b/search/gemini-enterprise/group-licensing/docs/TDD.md new file mode 100644 index 0000000..64ba725 --- /dev/null +++ b/search/gemini-enterprise/group-licensing/docs/TDD.md @@ -0,0 +1,195 @@ +# **Technical Design Document: Gemini Enterprise License Management Utility** + +## **1. Overview** + +This document outlines the technical design for a standalone utility that provides robust, policy-driven license management for Gemini Enterprise. The solution addresses the need for granular, group-based license assignment, which is a critical requirement for large enterprise customers. It is designed to be deployed and managed by the customer within the customer's own Google Cloud project. + +The core of the utility is a **Cloud Run Job** that executes scheduled reconciliation and garbage-collection tasks based on a provided JSON configuration. + +## **2. Core Application Design & Hosting** + +### **2.1. Artifact** + +The utility will be delivered as a **Cloud Run Job** container image. This encapsulates all application logic, ensuring consistency and simplifying deployment. + +### **2.2. Hosting** + +The utility is designed for deployment on **Cloud Run Jobs**. This provides a fully managed, serverless environment that runs to completion and exits — there is no persistent HTTP server. The job scales to zero between executions, minimizing operational overhead and cost. + +### **2.3. Performance & Execution** + +The utility operates on an eventually consistent model — there is no requirement for "always-on" operation or sub-second response times. Jobs are triggered by Cloud Scheduler on a periodic basis and run to completion. + +Cloud Run Jobs support a task timeout of up to **168 hours**, which accommodates organizations with very large user populations. Native task-level parallelism is available via `CLOUD_RUN_TASK_INDEX` and `CLOUD_RUN_TASK_COUNT` environment variables (injected automatically by Cloud Run), allowing the configured project list to be sharded across multiple concurrent task instances. + +## **3. Data Storage & Configuration** + +The utility uses a JSON-based configuration model which is persisted to GCP Secret Manager. + +### **3.1. Entitlement Configuration (Secret Manager)** + +Configuration is supplied by administrators as a JSON object stored in **Google Cloud Secret Manager**. This secret is mapped as a **volume mount** to the Cloud Run Job, allowing the utility to read the configuration from a local file path (e.g., `/run/secrets/entitlements.json`) at runtime. This approach ensures that group mappings are managed securely and can be updated independently of the function code. + +**Example Configuration Structure:** + +```json +{ + "billing_account_id": "ABCDE-12345-FGHIJ", + "projects": { + "customer-project-alpha": [ + { + "subscription_tier": "SUBSCRIPTION_TIER_ENTERPRISE", + "location": "global", + "groups": [ + "group-data-scientists@example.com", + "group-senior-devs@example.com" + ] + }, + { + "subscription_tier": "SUBSCRIPTION_TIER_AGENTSPACE_BUSINESS", + "location": "global", + "groups": [ + "group-marketing@example.com", + "group-sales@example.com", + "group-general-staff@example.com" + ] + } + ] + }, + "settings": { + "staleness_threshold_days": 30 + } +} +``` + +**Top-level fields:** + +| Field | Type | Required | Description | +| ----- | ----- | ----- | ----- | +| `billing_account_id` | string | Yes | The GCP billing account ID associated with the managed projects. | +| `projects` | object | Yes | Map of GCP project ID → list of entitlement entries. | +| `settings.staleness_threshold_days` | integer | No | Users inactive beyond this many days are revoked. `0` (default) disables the staleness check. | + +**Per-project entry fields:** + +Each project maps to an ordered array of entitlement entries. A project may have multiple entries — one per SKU/location combination. + +| Field | Type | Required | Description | +| ----- | ----- | ----- | ----- | +| `subscription_tier` | string | Yes | The Gemini SKU to assign. Must be a valid `SubscriptionTier` enum value (see section 5.3). | +| `location` | string | Yes | Geographic region for license management. Must be one of: `global`, `us`, `eu`. | +| `groups` | array of strings | Yes | List of Google Group email addresses whose members are entitled to this SKU. | +``` + +## **4. Job Definitions & Triggers** + +The utility is packaged as a single container image and deployed as **two Cloud Run Job definitions**, each configured with a different `JOB_TYPE` environment variable. Both jobs are triggered by Cloud Scheduler via the Cloud Run Jobs API on independent schedules. + +| Job Definition | `JOB_TYPE` | Schedule | Description | +| ----- | ----- | ----- | ----- | +| `gemini-box-office-joiner` | `joiner` | Daily (24hr) | Finds and grants licenses to members of entitled groups. | +| `gemini-box-office-gc` | `gc` | Every 6 hours | Finds and revokes licenses from stale or unentitled users. | + +### **4.1. Environment Variables** + +| Variable | Source | Description | Default | +| ----- | ----- | ----- | ----- | +| `JOB_TYPE` | Job definition | Selects the workflow to execute (`joiner` or `gc`). Required. | — | +| `DRY_RUN` | Job definition / per-execution override | When `true`, evaluation runs in full but no write API calls are made. Can be overridden per execution via the Cloud Scheduler request body. | `false` | +| `CLOUD_RUN_TASK_INDEX` | Injected by Cloud Run | 0-based index of this task instance. Used to shard the project list. | `0` | +| `CLOUD_RUN_TASK_COUNT` | Injected by Cloud Run | Total number of concurrent task instances. | `1` | + +### **4.2. Project-Level Sharding** + +When `CLOUD_RUN_TASK_COUNT` is greater than `1`, each task instance processes only the projects assigned to it: projects where `projectIndex % CLOUD_RUN_TASK_COUNT == CLOUD_RUN_TASK_INDEX`. This allows all configured projects to be processed in parallel with no shared state between tasks. + +## **5. Detailed Workflows & External API Integrations** + +The utility's logic is divided into two primary reconciliation workflows. + +### **5.1. Workflow 1: Bulk "Joiner" Job (Scheduled: 24hr)** + +* **Trigger:** Cloud Scheduler via the Cloud Run Jobs API (`JOB_TYPE=joiner`). +* **External APIs:** + 1. **Cloud Identity Admin API:** [`members.list`](https://developers.google.com/workspace/admin/directory/reference/rest/v1/members/list) — Go client: [`google.golang.org/api/admin/directory/v1`](https://pkg.go.dev/google.golang.org/api/admin/directory/v1) + 2. **Discovery Engine API:** [`batchUpdateUserLicenses`](https://cloud.google.com/go/discoveryengine/apiv1), [`licenseConfigsUsageStats.list`](https://cloud.google.com/go/discoveryengine/apiv1) +* **Flow:** + 1. For each group in the configuration, it calls the **Cloud Identity Admin API's** `members.list` method (with `includeDerivedMembership=true`). + 2. The job handles cursor-based pagination to traverse all members, including those in nested groups. + 3. For each member that is a `User`, it calls the **Discovery Engine API** (`batchUpdateUserLicenses`) to assign the license. This operation is idempotent. + +* **License Pool Exhaustion Handling:** + If a `batchUpdateUserLicenses` call fails because the license pool for a SKU is exhausted, the job does **not** treat this as a fatal error. Instead it: + 1. Calls `licenseConfigsUsageStats.list` for the affected project to retrieve the current `usedLicenseCount` for the relevant `licenseConfig`. + 2. Computes `available = allocatedCount - usedLicenseCount` (the allocated count is derived from the billing account `licenseConfigDistributions` map fetched at startup). + 3. Retries the batch trimmed to `available` items, granting as many licenses as possible. + 4. Soft-fails the remaining users: logs a `WARN` entry with the count of users who could not be assigned a license, then continues to the next batch or project. The job exits `0`. + The `licenses_soft_failed` count is included in the final summary log and in the `SyncAddResponse`. + +### **5.2. Workflow 2: Bulk "Garbage Collection" Job (Scheduled: 6hr)** + +* **Trigger:** Cloud Scheduler via the Cloud Run Jobs API (`JOB_TYPE=gc`). +* **External APIs:** + 1. **Discovery Engine API:** [`listUserLicenses`](https://cloud.google.com/go/discoveryengine/apiv1) and [`batchUpdateUserLicenses`](https://cloud.google.com/go/discoveryengine/apiv1). + 2. **Cloud Identity Admin API:** [`members.hasMember`](https://developers.google.com/workspace/admin/directory/reference/rest/v1/members/hasMember) — Go client: [`google.golang.org/api/admin/directory/v1`](https://pkg.go.dev/google.golang.org/api/admin/directory/v1) +* **Flow:** + 1. Iterates through all licensed users via the **Discovery Engine API** (`listUserLicenses`). + 2. For each user, the job evaluates two deprovisioning conditions: + - **Staleness**: If `staleness_threshold_days` is greater than `0`, checks whether the user's staleness reference date is more than `X` days ago. The reference date is chosen as follows: if `lastLoginTime` is set, it is used; if `lastLoginTime` is absent (the user has never logged in), `createTime` (the license assignment timestamp) is used instead, so that a recently provisioned account is not immediately revoked before the user has had a chance to sign in. If both timestamps are absent, the user is treated as immediately stale. Both timestamps are retrieved from the `UserLicense` object provided by the **Discovery Engine API**. Setting `staleness_threshold_days` to `0` (or omitting it) disables this check entirely — only entitlement is evaluated. + - **Entitlement**: Calls the **Cloud Identity Admin API's** `members.hasMember` method to confirm if the user is still a member of any entitled group. + 3. If **either** condition is met (the user is stale OR no longer entitled), the utility calls the **Discovery Engine API** (`batchUpdateUserLicenses`) to revoke the license. + +### **5.3. License Precedence Logic** + +In scenarios where a user belongs to multiple groups mapped to different SKUs, the utility applies a hardcoded precedence ranking to ensure the highest entitlement is granted. + +**Precedence Ranking (Highest to Lowest):** +1. `SUBSCRIPTION_TIER_SEARCH_AND_ASSISTANT` +2. `SUBSCRIPTION_TIER_ENTERPRISE` +3. `SUBSCRIPTION_TIER_SEARCH` +4. `SUBSCRIPTION_TIER_NOTEBOOK_LM` +5. `SUBSCRIPTION_TIER_AGENTSPACE_BUSINESS` +6. `SUBSCRIPTION_TIER_AGENTSPACE_STARTER` +7. `SUBSCRIPTION_TIER_FRONTLINE_WORKER` +8. `SUBSCRIPTION_TIER_FRONTLINE_STARTER` +9. `SUBSCRIPTION_TIER_ENTERPRISE_EMERGING` +10. `SUBSCRIPTION_TIER_EDU_PRO` +11. `SUBSCRIPTION_TIER_EDU` +12. `SUBSCRIPTION_TIER_EDU_PRO_EMERGING` +13. `SUBSCRIPTION_TIER_EDU_EMERGING` + +If a user is eligible for multiple SKUs, the "Joiner" job will only attempt to assign the highest-ranked SKU. The "Garbage Collection" job will not revoke a higher-tier license if the user is still entitled to it, even if they are removed from a lower-tier group. + +## **6. Authentication & Authorization** {#6.-authentication-&-authorization} + +Two distinct layers of security must be implemented: + +1. **Service-Level Authorization:** The utility's Cloud Run Job must run with a dedicated **Google Cloud Service Account**. This account requires the following IAM roles and OAuth scopes: + + **IAM Roles:** + * `roles/discoveryengine.admin`: Required to list and update user licenses. + * `roles/cloudidentity.groups.viewer`: Required to list group members and verify membership. + * `roles/secretmanager.secretAccessor`: Required to read the entitlement configuration mounted from Secret Manager. + + **OAuth Scopes:** + * `https://www.googleapis.com/auth/cloud-platform` + * `https://www.googleapis.com/auth/cloud-identity.groups.readonly` + + *Note: Domain-Wide Delegation is not required as the Cloud Identity API honors IAM-based permissions at the Organization/Folder level.* + +2. **Trigger Authorization:** Access to the Cloud Run Jobs must be restricted to authorized callers. The Cloud Scheduler service account must be granted `roles/run.invoker` on each job resource. No HTTP endpoint is exposed. + +## **7. Non-Functional Requirements** {#7.-non-functional-requirements} + +* **API Rate Limiting & Concurrency:** + * **Request Batching:** The utility MUST utilize the `batchUpdateUserLicenses` endpoint to consolidate up to 100 license modifications (assignments or revocations) per API request, minimizing round-trip latency and quota consumption. + * **Backoff:** Exponential backoff will be applied to all 429 and 5xx errors using a standard retry library. + * **License Pool Exhaustion:** License pool exhaustion (`ErrLicensesExhausted`) is treated as a soft failure distinct from rate limiting (`ErrAPIRateLimited`). Exhaustion triggers a `licenseConfigsUsageStats.list` lookup, a trimmed retry for remaining available seats, and a structured warning log — it does not abort the job. +* **Structured Logging:** + * **Format:** All logs MUST be emitted in JSON format to stdout for automatic ingestion by Cloud Logging. + * **Library:** Use Go's standard `log/slog` library. + * **Fields:** Every log entry MUST include `severity`, `workflow` (`joiner` | `garbage_collection`), and `message`. + * **Contextual Fields:** Include `group_id`, `sku_id`, and `duration_ms` where applicable. + * **PII Protection:** `user_email` and other personally identifiable information MUST NOT be logged. `group_id` is the primary identifier for reconciliation context. +* **Multi-Project Deployment:** A single job definition handles all configured projects. For customers with very large project portfolios, `CLOUD_RUN_TASK_COUNT` can be increased to shard the project list across parallel task instances (see section 4.2). +* **Automation:** All components should be deployable with example gCloud commands provided in this repository. diff --git a/search/gemini-enterprise/group-licensing/go.mod b/search/gemini-enterprise/group-licensing/go.mod new file mode 100644 index 0000000..0983b0d --- /dev/null +++ b/search/gemini-enterprise/group-licensing/go.mod @@ -0,0 +1,47 @@ +module github.com/cloud-gtm/gemini-box-office + +go 1.25.4 + +require ( + cloud.google.com/go/discoveryengine v1.23.0 + github.com/stretchr/testify v1.11.1 + google.golang.org/api v0.269.0 + google.golang.org/grpc v1.79.1 + google.golang.org/protobuf v1.36.11 +) + +require ( + cloud.google.com/go v0.123.0 // indirect + cloud.google.com/go/auth v0.18.2 // indirect + cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect + cloud.google.com/go/compute/metadata v0.9.0 // indirect + cloud.google.com/go/longrunning v0.8.0 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/google/s2a-go v0.1.9 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/googleapis/enterprise-certificate-proxy v0.3.12 // indirect + github.com/googleapis/gax-go/v2 v2.17.0 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/stretchr/objx v0.5.2 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect + go.opentelemetry.io/otel v1.39.0 // indirect + go.opentelemetry.io/otel/metric v1.39.0 // indirect + go.opentelemetry.io/otel/trace v1.39.0 // indirect + golang.org/x/crypto v0.48.0 // indirect + golang.org/x/net v0.50.0 // indirect + golang.org/x/oauth2 v0.35.0 // indirect + golang.org/x/sync v0.19.0 // indirect + golang.org/x/sys v0.41.0 // indirect + golang.org/x/text v0.34.0 // indirect + golang.org/x/time v0.14.0 // indirect + google.golang.org/genproto v0.0.0-20260128011058-8636f8732409 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20260203192932-546029d2fa20 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260217215200-42d3e9bedb6d // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/search/gemini-enterprise/group-licensing/go.sum b/search/gemini-enterprise/group-licensing/go.sum new file mode 100644 index 0000000..bbffa74 --- /dev/null +++ b/search/gemini-enterprise/group-licensing/go.sum @@ -0,0 +1,105 @@ +cloud.google.com/go v0.123.0 h1:2NAUJwPR47q+E35uaJeYoNhuNEM9kM8SjgRgdeOJUSE= +cloud.google.com/go v0.123.0/go.mod h1:xBoMV08QcqUGuPW65Qfm1o9Y4zKZBpGS+7bImXLTAZU= +cloud.google.com/go/auth v0.18.2 h1:+Nbt5Ev0xEqxlNjd6c+yYUeosQ5TtEUaNcN/3FozlaM= +cloud.google.com/go/auth v0.18.2/go.mod h1:xD+oY7gcahcu7G2SG2DsBerfFxgPAJz17zz2joOFF3M= +cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc= +cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c= +cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs= +cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10= +cloud.google.com/go/discoveryengine v1.23.0 h1:TOWCOr3V2xT43Q2jrGoBnr9iqOAau5IC4E+8Jv8lfwM= +cloud.google.com/go/discoveryengine v1.23.0/go.mod h1:gBnZDfONsXi/dd4fXQBauI+j0YcSUeI4B1r5g6yAsyw= +cloud.google.com/go/longrunning v0.8.0 h1:LiKK77J3bx5gDLi4SMViHixjD2ohlkwBi+mKA7EhfW8= +cloud.google.com/go/longrunning v0.8.0/go.mod h1:UmErU2Onzi+fKDg2gR7dusz11Pe26aknR4kHmJJqIfk= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5 h1:6xNmx7iTtyBRev0+D/Tv1FZd4SCg8axKApyNyRsAt/w= +github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5/go.mod h1:KdCmV+x/BuvyMxRnYBlmVaq4OLiKW6iRQfvC62cvdkI= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/envoyproxy/go-control-plane v0.14.0 h1:hbG2kr4RuFj222B6+7T83thSPqLjwBIfQawTkC++2HA= +github.com/envoyproxy/go-control-plane/envoy v1.36.0 h1:yg/JjO5E7ubRyKX3m07GF3reDNEnfOboJ0QySbH736g= +github.com/envoyproxy/go-control-plane/envoy v1.36.0/go.mod h1:ty89S1YCCVruQAm9OtKeEkQLTb+Lkz0k8v9W0Oxsv98= +github.com/envoyproxy/protoc-gen-validate v1.3.0 h1:TvGH1wof4H33rezVKWSpqKz5NXWg5VPuZ0uONDT6eb4= +github.com/envoyproxy/protoc-gen-validate v1.3.0/go.mod h1:HvYl7zwPa5mffgyeTUHA9zHIH36nmrm7oCbo4YKoSWA= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0= +github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/enterprise-certificate-proxy v0.3.12 h1:Fg+zsqzYEs1ZnvmcztTYxhgCBsx3eEhEwQ1W/lHq/sQ= +github.com/googleapis/enterprise-certificate-proxy v0.3.12/go.mod h1:vqVt9yG9480NtzREnTlmGSBmFrA+bzb0yl0TxoBQXOg= +github.com/googleapis/gax-go/v2 v2.17.0 h1:RksgfBpxqff0EZkDWYuz9q/uWsTVz+kf43LsZ1J6SMc= +github.com/googleapis/gax-go/v2 v2.17.0/go.mod h1:mzaqghpQp4JDh3HvADwrat+6M3MOIDp5YKHhb9PAgDY= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo= +github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 h1:q4XOmH/0opmeuJtPsbFNivyl7bCt7yRBbeEm2sC/XtQ= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0/go.mod h1:snMWehoOh2wsEwnvvwtDyFCxVeDAODenXHtn5vzrKjo= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q= +go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48= +go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8= +go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0= +go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs= +go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18= +go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE= +go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8= +go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew= +go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI= +go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA= +golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= +golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= +golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60= +golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM= +golang.org/x/oauth2 v0.35.0 h1:Mv2mzuHuZuY2+bkyWXIHMfhNdJAdwW3FuWeCPYN5GVQ= +golang.org/x/oauth2 v0.35.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= +golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= +golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= +golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= +golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= +gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= +gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= +google.golang.org/api v0.269.0 h1:qDrTOxKUQ/P0MveH6a7vZ+DNHxJQjtGm/uvdbdGXCQg= +google.golang.org/api v0.269.0/go.mod h1:N8Wpcu23Tlccl0zSHEkcAZQKDLdquxK+l9r2LkwAauE= +google.golang.org/genproto v0.0.0-20260128011058-8636f8732409 h1:VQZ/yAbAtjkHgH80teYd2em3xtIkkHd7ZhqfH2N9CsM= +google.golang.org/genproto v0.0.0-20260128011058-8636f8732409/go.mod h1:rxKD3IEILWEu3P44seeNOAwZN4SaoKaQ/2eTg4mM6EM= +google.golang.org/genproto/googleapis/api v0.0.0-20260203192932-546029d2fa20 h1:7ei4lp52gK1uSejlA8AZl5AJjeLUOHBQscRQZUgAcu0= +google.golang.org/genproto/googleapis/api v0.0.0-20260203192932-546029d2fa20/go.mod h1:ZdbssH/1SOVnjnDlXzxDHK2MCidiqXtbYccJNzNYPEE= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260217215200-42d3e9bedb6d h1:t/LOSXPJ9R0B6fnZNyALBRfZBH0Uy0gT+uR+SJ6syqQ= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260217215200-42d3e9bedb6d/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/grpc v1.79.1 h1:zGhSi45ODB9/p3VAawt9a+O/MULLl9dpizzNNpq7flY= +google.golang.org/grpc v1.79.1/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/search/gemini-enterprise/group-licensing/internal/adapters/cloudidentity/adapter.go b/search/gemini-enterprise/group-licensing/internal/adapters/cloudidentity/adapter.go new file mode 100644 index 0000000..fcab55b --- /dev/null +++ b/search/gemini-enterprise/group-licensing/internal/adapters/cloudidentity/adapter.go @@ -0,0 +1,178 @@ +/* +Copyright © 2026 Google LLC + +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 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package cloudidentity + +import ( + "context" + "errors" + "fmt" + + admin "google.golang.org/api/admin/directory/v1" + "google.golang.org/api/googleapi" + + "github.com/cloud-gtm/gemini-box-office/internal/models" + "github.com/cloud-gtm/gemini-box-office/internal/ports" +) + +var _ ports.IdpClient = (*Adapter)(nil) + +// MembersListCall is the interface wrapping the fluent builder and execution of +// a members.list API call. The exported method names allow external test +// packages to implement this interface with fakes. +type MembersListCall interface { + IncludeDerivedMembership(v bool) MembersListCall + MaxResults(n int64) MembersListCall + PageToken(tok string) MembersListCall + Do() (*admin.Members, error) +} + +// MemberHasMemberCall is the interface wrapping the execution of a +// members.hasMember API call. +type MemberHasMemberCall interface { + Do() (*admin.MembersHasMember, error) +} + +// MembersService is the interface wrapping only the Admin SDK methods called +// by Adapter. External test packages implement this with fakes. +type MembersService interface { + List(groupEmail string) MembersListCall + HasMember(groupEmail, userEmail string) MemberHasMemberCall +} + +// adminMembersService wraps *admin.MembersService to satisfy MembersService. +type adminMembersService struct { + svc *admin.MembersService +} + +func (a *adminMembersService) List(groupEmail string) MembersListCall { + return &adminMembersListCall{call: a.svc.List(groupEmail)} +} + +func (a *adminMembersService) HasMember(groupEmail, userEmail string) MemberHasMemberCall { + return &adminMemberHasMemberCall{call: a.svc.HasMember(groupEmail, userEmail)} +} + +type adminMembersListCall struct { + call *admin.MembersListCall +} + +func (c *adminMembersListCall) IncludeDerivedMembership(v bool) MembersListCall { + c.call = c.call.IncludeDerivedMembership(v) + return c +} + +func (c *adminMembersListCall) MaxResults(n int64) MembersListCall { + c.call = c.call.MaxResults(n) + return c +} + +func (c *adminMembersListCall) PageToken(tok string) MembersListCall { + c.call = c.call.PageToken(tok) + return c +} + +func (c *adminMembersListCall) Do() (*admin.Members, error) { + return c.call.Do() +} + +type adminMemberHasMemberCall struct { + call *admin.MembersHasMemberCall +} + +func (c *adminMemberHasMemberCall) Do() (*admin.MembersHasMember, error) { + return c.call.Do() +} + +// Adapter implements ports.IdpClient using the Google Cloud Identity Admin API. +type Adapter struct { + members MembersService +} + +// New constructs an Adapter from an already-authenticated *admin.Service. The +// caller is responsible for configuring Application Default Credentials before +// calling New. +func New(service *admin.Service) *Adapter { + return &Adapter{members: &adminMembersService{svc: service.Members}} +} + +// newWithMembers is used by tests to inject a mock MembersService. +func newWithMembers(svc MembersService) *Adapter { + return &Adapter{members: svc} +} + +// ListMembers returns one page of members for groupEmail. It includes derived +// (nested) membership so that users inside sub-groups are enumerated. Pass an +// empty pageToken to start from the beginning. +func (a *Adapter) ListMembers(ctx context.Context, groupEmail, pageToken string) ([]models.Member, string, error) { + call := a.members.List(groupEmail). + IncludeDerivedMembership(true). + MaxResults(models.MembersListPageSize) + + if pageToken != "" { + call = call.PageToken(pageToken) + } + + resp, err := call.Do() + if err != nil { + transport := mapHTTPError(err) + return nil, "", fmt.Errorf("%w: %w", models.ErrMemberListFailed, transport) + } + + members := make([]models.Member, 0, len(resp.Members)) + for _, m := range resp.Members { + members = append(members, models.Member{ + Email: m.Email, + Type: models.MemberType(m.Type), + }) + } + + return members, resp.NextPageToken, nil +} + +// HasMember reports whether userEmail is a direct or indirect member of +// groupEmail. +func (a *Adapter) HasMember(ctx context.Context, groupEmail, userEmail string) (bool, error) { + resp, err := a.members.HasMember(groupEmail, userEmail).Do() + if err != nil { + transport := mapHTTPError(err) + return false, fmt.Errorf("%w: %w", models.ErrMembershipCheckFailed, transport) + } + + return resp.IsMember, nil +} + +// mapHTTPError converts a *googleapi.Error into a domain transport sentinel +// error. Non-googleapi errors are returned unchanged. +func mapHTTPError(err error) error { + var apiErr *googleapi.Error + if !errors.As(err, &apiErr) { + return err + } + + switch { + case apiErr.Code == 429: + return fmt.Errorf("%w: %w", models.ErrAPIRateLimited, apiErr) + case apiErr.Code == 404: + return fmt.Errorf("%w: %w", models.ErrAPINotFound, apiErr) + case apiErr.Code == 401 || apiErr.Code == 403: + return fmt.Errorf("%w: %w", models.ErrAPIUnauthorized, apiErr) + case apiErr.Code >= 500: + return fmt.Errorf("%w: %w", models.ErrAPIUnavailable, apiErr) + default: + return apiErr + } +} diff --git a/search/gemini-enterprise/group-licensing/internal/adapters/cloudidentity/adapter_test.go b/search/gemini-enterprise/group-licensing/internal/adapters/cloudidentity/adapter_test.go new file mode 100644 index 0000000..5669360 --- /dev/null +++ b/search/gemini-enterprise/group-licensing/internal/adapters/cloudidentity/adapter_test.go @@ -0,0 +1,359 @@ +/* +Copyright © 2026 Google LLC + +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 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package cloudidentity + +import ( + "context" + "errors" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + admin "google.golang.org/api/admin/directory/v1" + "google.golang.org/api/googleapi" + + "github.com/cloud-gtm/gemini-box-office/internal/models" +) + +// --- mocks --- + +// MockMembersListCall implements MembersListCall using testify/mock. +type MockMembersListCall struct { + mock.Mock +} + +func (m *MockMembersListCall) IncludeDerivedMembership(v bool) MembersListCall { + args := m.Called(v) + return args.Get(0).(MembersListCall) +} + +func (m *MockMembersListCall) MaxResults(n int64) MembersListCall { + args := m.Called(n) + return args.Get(0).(MembersListCall) +} + +func (m *MockMembersListCall) PageToken(tok string) MembersListCall { + args := m.Called(tok) + return args.Get(0).(MembersListCall) +} + +func (m *MockMembersListCall) Do() (*admin.Members, error) { + args := m.Called() + if args.Get(0) == nil { + return nil, args.Error(1) + } + return args.Get(0).(*admin.Members), args.Error(1) +} + +// MockMemberHasMemberCall implements MemberHasMemberCall using testify/mock. +type MockMemberHasMemberCall struct { + mock.Mock +} + +func (m *MockMemberHasMemberCall) Do() (*admin.MembersHasMember, error) { + args := m.Called() + if args.Get(0) == nil { + return nil, args.Error(1) + } + return args.Get(0).(*admin.MembersHasMember), args.Error(1) +} + +// MockMembersService implements MembersService using testify/mock. +type MockMembersService struct { + mock.Mock +} + +func (m *MockMembersService) List(groupEmail string) MembersListCall { + args := m.Called(groupEmail) + return args.Get(0).(MembersListCall) +} + +func (m *MockMembersService) HasMember(groupEmail, userEmail string) MemberHasMemberCall { + args := m.Called(groupEmail, userEmail) + return args.Get(0).(MemberHasMemberCall) +} + +// --- helpers --- + +func makeAPIError(code int) *googleapi.Error { + return &googleapi.Error{Code: code, Message: "test error"} +} + +// --- tests --- + +func TestListMembers_HappyPath(t *testing.T) { + listCall := new(MockMembersListCall) + listCall.On("IncludeDerivedMembership", true).Return(listCall) + listCall.On("MaxResults", int64(models.MembersListPageSize)).Return(listCall) + listCall.On("Do").Return(&admin.Members{ + Members: []*admin.Member{ + {Email: "alice@example.com", Type: "USER"}, + {Email: "nested-group@example.com", Type: "GROUP"}, + }, + NextPageToken: "", + }, nil) + + mockSvc := new(MockMembersService) + mockSvc.On("List", "group@example.com").Return(listCall) + + adapter := newWithMembers(mockSvc) + got, tok, err := adapter.ListMembers(context.Background(), "group@example.com", "") + + require.NoError(t, err) + assert.Equal(t, "", tok) + require.Len(t, got, 2) + assert.Equal(t, "alice@example.com", got[0].Email) + assert.Equal(t, models.MemberTypeUser, got[0].Type) + assert.Equal(t, "nested-group@example.com", got[1].Email) + assert.Equal(t, models.MemberTypeGroup, got[1].Type) + + mockSvc.AssertExpectations(t) + listCall.AssertExpectations(t) +} + +func TestListMembers_Pagination(t *testing.T) { + const wantToken = "next-page-token-abc" + + listCall := new(MockMembersListCall) + listCall.On("IncludeDerivedMembership", true).Return(listCall) + listCall.On("MaxResults", int64(models.MembersListPageSize)).Return(listCall) + listCall.On("Do").Return(&admin.Members{ + Members: []*admin.Member{ + {Email: "bob@example.com", Type: "USER"}, + }, + NextPageToken: wantToken, + }, nil) + + mockSvc := new(MockMembersService) + mockSvc.On("List", "group@example.com").Return(listCall) + + adapter := newWithMembers(mockSvc) + _, tok, err := adapter.ListMembers(context.Background(), "group@example.com", "") + + require.NoError(t, err) + assert.Equal(t, wantToken, tok) + + mockSvc.AssertExpectations(t) + listCall.AssertExpectations(t) +} + +func TestListMembers_PageTokenPropagated(t *testing.T) { + // When ListMembers is called with a non-empty pageToken, the adapter must + // pass it to call.PageToken — exercising the conditional branch in adapter.go. + const incomingToken = "resume-token-xyz" + + listCall := new(MockMembersListCall) + listCall.On("IncludeDerivedMembership", true).Return(listCall) + listCall.On("MaxResults", int64(models.MembersListPageSize)).Return(listCall) + listCall.On("PageToken", incomingToken).Return(listCall) + listCall.On("Do").Return(&admin.Members{ + Members: []*admin.Member{{Email: "carol@example.com", Type: "USER"}}, + NextPageToken: "", + }, nil) + + mockSvc := new(MockMembersService) + mockSvc.On("List", "group@example.com").Return(listCall) + + adapter := newWithMembers(mockSvc) + _, _, err := adapter.ListMembers(context.Background(), "group@example.com", incomingToken) + + require.NoError(t, err) + + mockSvc.AssertExpectations(t) + listCall.AssertExpectations(t) // verifies PageToken(incomingToken) was called +} + +func TestListMembers_RateLimited(t *testing.T) { + listCall := new(MockMembersListCall) + listCall.On("IncludeDerivedMembership", true).Return(listCall) + listCall.On("MaxResults", int64(models.MembersListPageSize)).Return(listCall) + listCall.On("Do").Return((*admin.Members)(nil), makeAPIError(429)) + + mockSvc := new(MockMembersService) + mockSvc.On("List", "group@example.com").Return(listCall) + + adapter := newWithMembers(mockSvc) + _, _, err := adapter.ListMembers(context.Background(), "group@example.com", "") + + require.Error(t, err) + assert.True(t, errors.Is(err, models.ErrAPIRateLimited), + "error chain missing ErrAPIRateLimited; err = %v", err) + assert.True(t, errors.Is(err, models.ErrMemberListFailed), + "error chain missing ErrMemberListFailed; err = %v", err) + + mockSvc.AssertExpectations(t) + listCall.AssertExpectations(t) +} + +func TestHasMember_True(t *testing.T) { + hasMemberCall := new(MockMemberHasMemberCall) + hasMemberCall.On("Do").Return(&admin.MembersHasMember{IsMember: true}, nil) + + mockSvc := new(MockMembersService) + mockSvc.On("HasMember", "group@example.com", "alice@example.com").Return(hasMemberCall) + + adapter := newWithMembers(mockSvc) + got, err := adapter.HasMember(context.Background(), "group@example.com", "alice@example.com") + + require.NoError(t, err) + assert.True(t, got) + + mockSvc.AssertExpectations(t) + hasMemberCall.AssertExpectations(t) +} + +func TestHasMember_False(t *testing.T) { + hasMemberCall := new(MockMemberHasMemberCall) + hasMemberCall.On("Do").Return(&admin.MembersHasMember{IsMember: false}, nil) + + mockSvc := new(MockMembersService) + mockSvc.On("HasMember", "group@example.com", "unknown@example.com").Return(hasMemberCall) + + adapter := newWithMembers(mockSvc) + got, err := adapter.HasMember(context.Background(), "group@example.com", "unknown@example.com") + + require.NoError(t, err) + assert.False(t, got) + + mockSvc.AssertExpectations(t) + hasMemberCall.AssertExpectations(t) +} + +func TestHasMember_Forbidden(t *testing.T) { + hasMemberCall := new(MockMemberHasMemberCall) + hasMemberCall.On("Do").Return((*admin.MembersHasMember)(nil), makeAPIError(403)) + + mockSvc := new(MockMembersService) + mockSvc.On("HasMember", "group@example.com", "alice@example.com").Return(hasMemberCall) + + adapter := newWithMembers(mockSvc) + _, err := adapter.HasMember(context.Background(), "group@example.com", "alice@example.com") + + require.Error(t, err) + assert.True(t, errors.Is(err, models.ErrAPIUnauthorized), + "error chain missing ErrAPIUnauthorized; err = %v", err) + assert.True(t, errors.Is(err, models.ErrMembershipCheckFailed), + "error chain missing ErrMembershipCheckFailed; err = %v", err) + + mockSvc.AssertExpectations(t) + hasMemberCall.AssertExpectations(t) +} + +func TestListMembers_ServerError(t *testing.T) { + listCall := new(MockMembersListCall) + listCall.On("IncludeDerivedMembership", true).Return(listCall) + listCall.On("MaxResults", int64(models.MembersListPageSize)).Return(listCall) + listCall.On("Do").Return((*admin.Members)(nil), makeAPIError(500)) + + mockSvc := new(MockMembersService) + mockSvc.On("List", "group@example.com").Return(listCall) + + adapter := newWithMembers(mockSvc) + _, _, err := adapter.ListMembers(context.Background(), "group@example.com", "") + + require.Error(t, err) + assert.True(t, errors.Is(err, models.ErrMemberListFailed), + "error chain missing ErrMemberListFailed; err = %v", err) + assert.True(t, errors.Is(err, models.ErrAPIUnavailable), + "error chain missing ErrAPIUnavailable; err = %v", err) + + mockSvc.AssertExpectations(t) + listCall.AssertExpectations(t) +} + +func TestListMembers_Unauthorized(t *testing.T) { + listCall := new(MockMembersListCall) + listCall.On("IncludeDerivedMembership", true).Return(listCall) + listCall.On("MaxResults", int64(models.MembersListPageSize)).Return(listCall) + listCall.On("Do").Return((*admin.Members)(nil), makeAPIError(401)) + + mockSvc := new(MockMembersService) + mockSvc.On("List", "group@example.com").Return(listCall) + + adapter := newWithMembers(mockSvc) + _, _, err := adapter.ListMembers(context.Background(), "group@example.com", "") + + require.Error(t, err) + assert.True(t, errors.Is(err, models.ErrMemberListFailed), + "error chain missing ErrMemberListFailed; err = %v", err) + assert.True(t, errors.Is(err, models.ErrAPIUnauthorized), + "error chain missing ErrAPIUnauthorized; err = %v", err) + + mockSvc.AssertExpectations(t) + listCall.AssertExpectations(t) +} + +func TestListMembers_EmptyGroup(t *testing.T) { + listCall := new(MockMembersListCall) + listCall.On("IncludeDerivedMembership", true).Return(listCall) + listCall.On("MaxResults", int64(models.MembersListPageSize)).Return(listCall) + listCall.On("Do").Return(&admin.Members{Members: nil, NextPageToken: ""}, nil) + + mockSvc := new(MockMembersService) + mockSvc.On("List", "group@example.com").Return(listCall) + + adapter := newWithMembers(mockSvc) + got, tok, err := adapter.ListMembers(context.Background(), "group@example.com", "") + + require.NoError(t, err) + assert.Empty(t, got) + assert.Equal(t, "", tok) + + mockSvc.AssertExpectations(t) + listCall.AssertExpectations(t) +} + +func TestHasMember_RateLimited(t *testing.T) { + hasMemberCall := new(MockMemberHasMemberCall) + hasMemberCall.On("Do").Return((*admin.MembersHasMember)(nil), makeAPIError(429)) + + mockSvc := new(MockMembersService) + mockSvc.On("HasMember", "group@example.com", "alice@example.com").Return(hasMemberCall) + + adapter := newWithMembers(mockSvc) + _, err := adapter.HasMember(context.Background(), "group@example.com", "alice@example.com") + + require.Error(t, err) + assert.True(t, errors.Is(err, models.ErrMembershipCheckFailed), + "error chain missing ErrMembershipCheckFailed; err = %v", err) + assert.True(t, errors.Is(err, models.ErrAPIRateLimited), + "error chain missing ErrAPIRateLimited; err = %v", err) + + mockSvc.AssertExpectations(t) + hasMemberCall.AssertExpectations(t) +} + +func TestHasMember_ServerError(t *testing.T) { + hasMemberCall := new(MockMemberHasMemberCall) + hasMemberCall.On("Do").Return((*admin.MembersHasMember)(nil), makeAPIError(500)) + + mockSvc := new(MockMembersService) + mockSvc.On("HasMember", "group@example.com", "alice@example.com").Return(hasMemberCall) + + adapter := newWithMembers(mockSvc) + _, err := adapter.HasMember(context.Background(), "group@example.com", "alice@example.com") + + require.Error(t, err) + assert.True(t, errors.Is(err, models.ErrMembershipCheckFailed), + "error chain missing ErrMembershipCheckFailed; err = %v", err) + assert.True(t, errors.Is(err, models.ErrAPIUnavailable), + "error chain missing ErrAPIUnavailable; err = %v", err) + + mockSvc.AssertExpectations(t) + hasMemberCall.AssertExpectations(t) +} diff --git a/search/gemini-enterprise/group-licensing/internal/adapters/discoveryengine/adapter.go b/search/gemini-enterprise/group-licensing/internal/adapters/discoveryengine/adapter.go new file mode 100644 index 0000000..fe3d4ef --- /dev/null +++ b/search/gemini-enterprise/group-licensing/internal/adapters/discoveryengine/adapter.go @@ -0,0 +1,348 @@ +/* +Copyright © 2026 Google LLC + +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 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package discoveryengine + +import ( + "context" + "errors" + "fmt" + "strconv" + "strings" + "time" + + discoveryenginesdk "cloud.google.com/go/discoveryengine/apiv1" + "cloud.google.com/go/discoveryengine/apiv1/discoveryenginepb" + discoveryengineapi "google.golang.org/api/discoveryengine/v1alpha" + "google.golang.org/api/iterator" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/types/known/fieldmaskpb" + + "github.com/cloud-gtm/gemini-box-office/internal/models" + "github.com/cloud-gtm/gemini-box-office/internal/ports" +) + +var _ ports.GeminiClient = (*Adapter)(nil) + +// licenseListPageSize is the number of user licenses requested per page from +// the Discovery Engine API. iterator.NewPager requires a strictly positive +// value; the server may return fewer items on the final page. +const licenseListPageSize = 1000 + +// userLicenseIterator is the internal interface that the adapter requires from +// a Discovery Engine list iterator. *discoveryenginesdk.UserLicenseIterator +// satisfies this interface naturally. Tests provide a fakeIterator. +type userLicenseIterator interface { + PageInfo() *iterator.PageInfo +} + +// userLicenseClient is the internal interface that wraps only the Discovery +// Engine client methods called by Adapter. Tests provide a fakeUserLicenseClient. +// The BatchUpdateUserLicenses method blocks until the long-running operation +// completes; the real implementation calls op.Wait internally. +type userLicenseClient interface { + ListUserLicenses(ctx context.Context, req *discoveryenginepb.ListUserLicensesRequest) userLicenseIterator + BatchUpdateUserLicenses(ctx context.Context, req *discoveryenginepb.BatchUpdateUserLicensesRequest) error +} + +// realClient wraps *discoveryenginesdk.UserLicenseClient to satisfy userLicenseClient. +type realClient struct { + c *discoveryenginesdk.UserLicenseClient +} + +func (r *realClient) ListUserLicenses(ctx context.Context, req *discoveryenginepb.ListUserLicensesRequest) userLicenseIterator { + return r.c.ListUserLicenses(ctx, req) +} + +func (r *realClient) BatchUpdateUserLicenses(ctx context.Context, req *discoveryenginepb.BatchUpdateUserLicensesRequest) error { + op, err := r.c.BatchUpdateUserLicenses(ctx, req) + if err != nil { + return err + } + _, err = op.Wait(ctx) + return err +} + +// Adapter implements ports.GeminiClient using the Discovery Engine API. +type Adapter struct { + client userLicenseClient +} + +// New constructs an Adapter from an already-authenticated +// *discoveryenginesdk.UserLicenseClient. The caller is responsible for +// configuring Application Default Credentials before calling New. +func New(client *discoveryenginesdk.UserLicenseClient) *Adapter { + return &Adapter{client: &realClient{c: client}} +} + +// newWithClient is used by tests to inject a fake userLicenseClient. +func newWithClient(c userLicenseClient) *Adapter { + return &Adapter{client: c} +} + +// FetchLicenseConfigIndex retrieves all billing account license configurations +// and returns an index mapping (SKU, ProjectID, Location) to the full +// per-project licenseConfig resource path. The service layer calls this once +// at the start of each run and resolves paths before issuing grant operations. +func (a *Adapter) FetchLicenseConfigIndex(ctx context.Context, billingAccountID string) (models.LicenseConfigIndex, error) { + svc, err := discoveryengineapi.NewService(ctx) + if err != nil { + return nil, fmt.Errorf("creating discoveryengine REST client: %w", err) + } + + parent := "billingAccounts/" + billingAccountID + index := make(models.LicenseConfigIndex) + + err = svc.BillingAccounts.BillingAccountLicenseConfigs.List(parent).Context(ctx).Pages(ctx, + func(resp *discoveryengineapi.GoogleCloudDiscoveryengineV1alphaListBillingAccountLicenseConfigsResponse) error { + for _, c := range resp.BillingAccountLicenseConfigs { + if c.State != "ACTIVE" { + continue + } + if c.SubscriptionTier == "" || c.SubscriptionTier == "SUBSCRIPTION_TIER_UNSPECIFIED" { + continue + } + for licenseConfigPath, allocatedStr := range c.LicenseConfigDistributions { + // Path format: projects/{project}/locations/{location}/licenseConfigs/{id} + parts := strings.Split(licenseConfigPath, "/") + if len(parts) != 6 { + continue + } + allocated, err := strconv.ParseInt(allocatedStr, 10, 64) + if err != nil { + continue + } + key := models.LicenseConfigKey{ + SKU: models.SKU(c.SubscriptionTier), + ProjectNumber: parts[1], + Location: models.Location(parts[3]), + } + index[key] = models.LicenseConfigEntry{ + Path: licenseConfigPath, + AllocatedCount: allocated, + } + } + } + return nil + }, + ) + if err != nil { + return nil, fmt.Errorf("fetching billing account license configs: %w", err) + } + + return index, nil +} + +// ListUserLicenses returns one page of user licenses for the given projectID. +// Pass an empty pageToken to start from the beginning. It uses iterator.NewPager +// to correctly honour the caller-supplied pageToken, fixing the pagination bug +// that existed when InternalFetch was called directly. +func (a *Adapter) ListUserLicenses(ctx context.Context, projectID, pageToken string) ([]models.UserLicense, string, error) { + parent := fmt.Sprintf("projects/%s/locations/global/userStores/default_user_store", projectID) + + req := &discoveryenginepb.ListUserLicensesRequest{ + Parent: parent, + PageToken: pageToken, + } + + it := a.client.ListUserLicenses(ctx, req) + + pager := iterator.NewPager(it, licenseListPageSize, pageToken) + + var protos []*discoveryenginepb.UserLicense + nextPageToken, err := pager.NextPage(&protos) + if err != nil { + if errors.Is(err, iterator.Done) { + return nil, "", nil + } + return nil, "", fmt.Errorf("%w: %w", models.ErrLicenseListFailed, mapGRPCError(err)) + } + + licenses := make([]models.UserLicense, 0, len(protos)) + for _, p := range protos { + license, err := protoToUserLicense(p) + if err != nil { + return nil, "", fmt.Errorf("%w: %w", models.ErrLicenseListFailed, err) + } + licenses = append(licenses, license) + } + + return licenses, nextPageToken, nil +} + +// BatchUpdateUserLicenses applies up to models.MaxBatchSize grant or revoke +// operations in a single API call. Batches must be homogeneous (all grants or +// all revokes). For grants, LicenseConfigPath must be set on every update; for +// revokes it is not sent to the API — only license_assignment_state is masked — +// to avoid the "subscription reaches the limit" error that occurs when the API +// processes a license_config field alongside a NO_LICENSE state. +func (a *Adapter) BatchUpdateUserLicenses(ctx context.Context, projectID string, updates []models.LicenseUpdate) error { + if len(updates) > models.MaxBatchSize { + return fmt.Errorf("%w: batch size %d exceeds maximum %d", models.ErrBatchUpdateFailed, len(updates), models.MaxBatchSize) + } + + parent := fmt.Sprintf("projects/%s/locations/global/userStores/default_user_store", projectID) + + isRevoke := len(updates) > 0 && updates[0].Action == models.LicenseActionRevoke + + protoLicenses := make([]*discoveryenginepb.UserLicense, 0, len(updates)) + for _, u := range updates { + protoLicenses = append(protoLicenses, licenseUpdateToProto(u)) + } + + maskPaths := []string{"license_assignment_state", "license_config"} + if isRevoke { + maskPaths = []string{"license_assignment_state"} + } + + req := &discoveryenginepb.BatchUpdateUserLicensesRequest{ + Parent: parent, + Source: &discoveryenginepb.BatchUpdateUserLicensesRequest_InlineSource_{ + InlineSource: &discoveryenginepb.BatchUpdateUserLicensesRequest_InlineSource{ + UserLicenses: protoLicenses, + UpdateMask: &fieldmaskpb.FieldMask{ + Paths: maskPaths, + }, + }, + }, + } + + if err := a.client.BatchUpdateUserLicenses(ctx, req); err != nil { + return fmt.Errorf("%w: %w", models.ErrBatchUpdateFailed, mapGRPCError(err)) + } + + return nil +} + +// protoToUserLicense converts a discoveryenginepb.UserLicense to the domain model. +// The LicenseConfig field from the API is a resource path +// (projects/{p}/locations/{l}/licenseConfigs/{id}), stored as-is in LicenseConfigPath. +func protoToUserLicense(p *discoveryenginepb.UserLicense) (models.UserLicense, error) { + state, err := assignmentStateToLicenseState(p.LicenseAssignmentState) + if err != nil { + return models.UserLicense{}, err + } + + var lastLogin time.Time + if p.LastLoginTime != nil { + lastLogin = p.LastLoginTime.AsTime() + } + + var assignmentTime time.Time + if p.CreateTime != nil { + assignmentTime = p.CreateTime.AsTime() + } + + return models.UserLicense{ + UserEmail: p.UserPrincipal, + LicenseConfigPath: p.LicenseConfig, + State: state, + LastLoginTime: lastLogin, + AssignmentTime: assignmentTime, + }, nil +} + +// assignmentStateToLicenseState converts a proto LicenseAssignmentState to +// the domain LicenseState. +func assignmentStateToLicenseState(s discoveryenginepb.UserLicense_LicenseAssignmentState) (models.LicenseState, error) { + switch s { + case discoveryenginepb.UserLicense_ASSIGNED: + return models.LicenseStateAssigned, nil + case discoveryenginepb.UserLicense_NO_LICENSE, + discoveryenginepb.UserLicense_UNASSIGNED, + discoveryenginepb.UserLicense_NO_LICENSE_ATTEMPTED_LOGIN, + discoveryenginepb.UserLicense_LICENSE_ASSIGNMENT_STATE_UNSPECIFIED: + return models.LicenseStateRevoked, nil + default: + return "", fmt.Errorf("%w: %d", models.ErrUnknownLicenseState, s) + } +} + +// licenseUpdateToProto converts a domain LicenseUpdate to a proto UserLicense. +// For grants, LicenseConfig is set to the resolved resource path. For revokes, +// LicenseConfig is left empty — the API must not receive it alongside NO_LICENSE +// or it triggers a subscription-limit error. +func licenseUpdateToProto(u models.LicenseUpdate) *discoveryenginepb.UserLicense { + if u.Action == models.LicenseActionGrant { + return &discoveryenginepb.UserLicense{ + UserPrincipal: u.UserEmail, + LicenseConfig: u.LicenseConfigPath, + LicenseAssignmentState: discoveryenginepb.UserLicense_ASSIGNED, + } + } + return &discoveryenginepb.UserLicense{ + UserPrincipal: u.UserEmail, + LicenseAssignmentState: discoveryenginepb.UserLicense_NO_LICENSE, + } +} + +// FetchLicenseUsageStats returns a map of licenseConfig resource path to the +// number of licenses currently assigned (usedLicenseCount) for all licenseConfigs +// under the given project's default user store. The projectID must be the numeric +// project number that appears in licenseConfig resource paths. +func (a *Adapter) FetchLicenseUsageStats(ctx context.Context, projectID string) (map[string]int64, error) { + svc, err := discoveryengineapi.NewService(ctx) + if err != nil { + return nil, fmt.Errorf("creating discoveryengine REST client: %w", err) + } + + parent := fmt.Sprintf("projects/%s/locations/global/userStores/default_user_store", projectID) + + resp, err := svc.Projects.Locations.UserStores.LicenseConfigsUsageStats.List(parent).Context(ctx).Do() + if err != nil { + return nil, fmt.Errorf("fetching license usage stats: %w", err) + } + + stats := make(map[string]int64, len(resp.LicenseConfigUsageStats)) + for _, s := range resp.LicenseConfigUsageStats { + stats[s.LicenseConfig] = s.UsedLicenseCount + } + return stats, nil +} + +// mapGRPCError converts a gRPC status error into a domain transport sentinel error. +// License pool exhaustion (codes.InvalidArgument with a message containing +// "subscription" and "limit") is mapped to ErrLicensesExhausted so callers can +// distinguish it from a transient rate limit (codes.ResourceExhausted). +// The Discovery Engine API returns the message: +// +// "Subscription reaches the limit of N licenses for license config {path}" +func mapGRPCError(err error) error { + st, ok := status.FromError(err) + if !ok { + return err + } + + switch st.Code() { + case codes.InvalidArgument: + msg := strings.ToLower(st.Message()) + if strings.Contains(msg, "subscription") && strings.Contains(msg, "limit") { + return fmt.Errorf("%w: %w", models.ErrLicensesExhausted, err) + } + return err + case codes.ResourceExhausted: + return fmt.Errorf("%w: %w", models.ErrAPIRateLimited, err) + case codes.NotFound: + return fmt.Errorf("%w: %w", models.ErrAPINotFound, err) + case codes.Unauthenticated, codes.PermissionDenied: + return fmt.Errorf("%w: %w", models.ErrAPIUnauthorized, err) + case codes.Unavailable, codes.Internal: + return fmt.Errorf("%w: %w", models.ErrAPIUnavailable, err) + default: + return err + } +} diff --git a/search/gemini-enterprise/group-licensing/internal/adapters/discoveryengine/adapter_test.go b/search/gemini-enterprise/group-licensing/internal/adapters/discoveryengine/adapter_test.go new file mode 100644 index 0000000..a82e865 --- /dev/null +++ b/search/gemini-enterprise/group-licensing/internal/adapters/discoveryengine/adapter_test.go @@ -0,0 +1,559 @@ +/* +Copyright © 2026 Google LLC + +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 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package discoveryengine + +import ( + "context" + "errors" + "testing" + "time" + + "cloud.google.com/go/discoveryengine/apiv1/discoveryenginepb" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + "google.golang.org/api/iterator" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/types/known/timestamppb" + + "github.com/cloud-gtm/gemini-box-office/internal/models" +) + +// --- fakeIterator --- + +// fakeIterator implements userLicenseIterator (and therefore iterator.Pageable) +// by constructing a *iterator.PageInfo whose fetch function serves controlled +// pages of UserLicense protos. This lets tests exercise the real +// iterator.NewPager code path without hitting the Discovery Engine API. +type fakeIterator struct { + pageInfo *iterator.PageInfo +} + +func (f *fakeIterator) PageInfo() *iterator.PageInfo { + return f.pageInfo +} + +// newFakeIterator builds a fakeIterator that delivers pages in order. +// Each element of pages is one page of results. After all pages are exhausted, +// subsequent fetches return an empty slice and an empty token, signalling the +// end of iteration. If fetchErr is non-nil it is returned on the first fetch. +func newFakeIterator(pages [][]*discoveryenginepb.UserLicense, fetchErr error) *fakeIterator { + // buf holds the items appended by the most recent fetch call. The PageInfo + // machinery reads from buf via bufLen and takes from it via takeBuf. + var buf []*discoveryenginepb.UserLicense + pageIdx := 0 + + fetch := func(_ int, _ string) (string, error) { + if fetchErr != nil { + return "", fetchErr + } + if pageIdx >= len(pages) { + // No more pages; signal end of iteration with empty token. + buf = nil + return "", nil + } + buf = pages[pageIdx] + pageIdx++ + // Return a non-empty token when more pages follow so that + // iterator.Pager continues iterating. The token value itself is + // opaque to our tests because fakeIterator ignores it. + nextToken := "" + if pageIdx < len(pages) { + nextToken = "page-token" + } + return nextToken, nil + } + + bufLen := func() int { return len(buf) } + + takeBuf := func() interface{} { + b := buf + buf = nil + return b + } + + pi, _ := iterator.NewPageInfo(fetch, bufLen, takeBuf) + return &fakeIterator{pageInfo: pi} +} + +// --- fakeUserLicenseClient --- + +// fakeUserLicenseClient implements userLicenseClient using testify/mock. The +// ListUserLicenses method returns a userLicenseIterator so that tests can +// supply a fakeIterator directly. +type fakeUserLicenseClient struct { + mock.Mock +} + +func (f *fakeUserLicenseClient) ListUserLicenses(ctx context.Context, req *discoveryenginepb.ListUserLicensesRequest) userLicenseIterator { + args := f.Called(ctx, req) + return args.Get(0).(userLicenseIterator) +} + +func (f *fakeUserLicenseClient) BatchUpdateUserLicenses(ctx context.Context, req *discoveryenginepb.BatchUpdateUserLicensesRequest) error { + args := f.Called(ctx, req) + return args.Error(0) +} + +// --- helpers --- + +func makeGRPCError(code codes.Code) error { + return status.Error(code, "test error") +} + +// --- ListUserLicenses tests --- + +func TestListUserLicenses_HappyPath(t *testing.T) { + loginTime := time.Date(2025, 1, 15, 10, 0, 0, 0, time.UTC) + assignTime := time.Date(2025, 1, 1, 8, 0, 0, 0, time.UTC) + + page := []*discoveryenginepb.UserLicense{ + { + UserPrincipal: "alice@example.com", + LicenseConfig: "SUBSCRIPTION_TIER_ENTERPRISE", + LicenseAssignmentState: discoveryenginepb.UserLicense_ASSIGNED, + LastLoginTime: timestamppb.New(loginTime), + CreateTime: timestamppb.New(assignTime), + }, + } + fakeIt := newFakeIterator([][]*discoveryenginepb.UserLicense{page}, nil) + + mockClient := new(fakeUserLicenseClient) + mockClient.On("ListUserLicenses", mock.Anything, mock.AnythingOfType("*discoveryenginepb.ListUserLicensesRequest")). + Return(fakeIt) + + adapter := newWithClient(mockClient) + got, tok, err := adapter.ListUserLicenses(context.Background(), "my-project", "") + + require.NoError(t, err) + assert.Equal(t, "", tok) + require.Len(t, got, 1) + + lic := got[0] + assert.Equal(t, "alice@example.com", lic.UserEmail) + assert.Equal(t, "SUBSCRIPTION_TIER_ENTERPRISE", lic.LicenseConfigPath) + assert.Equal(t, models.LicenseStateAssigned, lic.State) + assert.True(t, lic.LastLoginTime.Equal(loginTime)) + assert.True(t, lic.AssignmentTime.Equal(assignTime)) + + mockClient.AssertExpectations(t) +} + +func TestListUserLicenses_NilLastLoginTime(t *testing.T) { + // When both LastLoginTime and CreateTime are nil, both domain fields must + // be zero so that the GC service's staleness fallback can handle them. + page := []*discoveryenginepb.UserLicense{ + { + UserPrincipal: "bob@example.com", + LicenseConfig: "SUBSCRIPTION_TIER_AGENTSPACE_BUSINESS", + LicenseAssignmentState: discoveryenginepb.UserLicense_ASSIGNED, + LastLoginTime: nil, + CreateTime: nil, + }, + } + fakeIt := newFakeIterator([][]*discoveryenginepb.UserLicense{page}, nil) + + mockClient := new(fakeUserLicenseClient) + mockClient.On("ListUserLicenses", mock.Anything, mock.AnythingOfType("*discoveryenginepb.ListUserLicensesRequest")). + Return(fakeIt) + + adapter := newWithClient(mockClient) + got, _, err := adapter.ListUserLicenses(context.Background(), "my-project", "") + + require.NoError(t, err) + require.Len(t, got, 1) + assert.True(t, got[0].LastLoginTime.IsZero()) + assert.True(t, got[0].AssignmentTime.IsZero()) + + mockClient.AssertExpectations(t) +} + +func TestListUserLicenses_CreateTimeMappedToAssignmentTime(t *testing.T) { + // When LastLoginTime is nil but CreateTime is set, AssignmentTime must + // be populated from CreateTime so the GC service can apply the + // assignment-date staleness fallback. + assignTime := time.Date(2026, 3, 1, 9, 0, 0, 0, time.UTC) + + page := []*discoveryenginepb.UserLicense{ + { + UserPrincipal: "new-user@example.com", + LicenseConfig: "SUBSCRIPTION_TIER_ENTERPRISE", + LicenseAssignmentState: discoveryenginepb.UserLicense_ASSIGNED, + LastLoginTime: nil, + CreateTime: timestamppb.New(assignTime), + }, + } + fakeIt := newFakeIterator([][]*discoveryenginepb.UserLicense{page}, nil) + + mockClient := new(fakeUserLicenseClient) + mockClient.On("ListUserLicenses", mock.Anything, mock.AnythingOfType("*discoveryenginepb.ListUserLicensesRequest")). + Return(fakeIt) + + adapter := newWithClient(mockClient) + got, _, err := adapter.ListUserLicenses(context.Background(), "my-project", "") + + require.NoError(t, err) + require.Len(t, got, 1) + assert.True(t, got[0].LastLoginTime.IsZero(), "LastLoginTime must be zero when proto field is nil") + assert.True(t, got[0].AssignmentTime.Equal(assignTime), "AssignmentTime must be populated from CreateTime") + + mockClient.AssertExpectations(t) +} + +func TestListUserLicenses_EmptyPage(t *testing.T) { + // An empty page set signals end of iteration immediately. + fakeIt := newFakeIterator([][]*discoveryenginepb.UserLicense{}, nil) + + mockClient := new(fakeUserLicenseClient) + mockClient.On("ListUserLicenses", mock.Anything, mock.AnythingOfType("*discoveryenginepb.ListUserLicensesRequest")). + Return(fakeIt) + + adapter := newWithClient(mockClient) + got, tok, err := adapter.ListUserLicenses(context.Background(), "my-project", "") + + require.NoError(t, err) + assert.Equal(t, "", tok) + assert.Empty(t, got) + + mockClient.AssertExpectations(t) +} + +// TestListUserLicenses_NextPageTokenPropagation verifies the contract that the +// service layer depends on: when the underlying iterator signals that more data +// is available, ListUserLicenses returns a non-empty nextPageToken; when it is +// the last page, nextPageToken is empty. +// +// iterator.NewPager accumulates items until it has filled licenseListPageSize +// items or the server signals end-of-results with an empty token. A non-empty +// nextPageToken is returned to the adapter caller only when a full page of +// licenseListPageSize items was fetched AND the server returned a cursor for +// more data. The fake simulates exactly that scenario. +func TestListUserLicenses_NextPageTokenPropagation(t *testing.T) { + // Build a page of exactly licenseListPageSize items so that NewPager stops + // after one fetch (buffer is full) and returns the server token as nextPageToken. + fullPage := make([]*discoveryenginepb.UserLicense, licenseListPageSize) + for i := range fullPage { + fullPage[i] = &discoveryenginepb.UserLicense{ + UserPrincipal: "user@example.com", + LicenseConfig: "SUBSCRIPTION_TIER_ENTERPRISE", + LicenseAssignmentState: discoveryenginepb.UserLicense_ASSIGNED, + } + } + + // First adapter call: one full page followed by a second page (simulated by + // having two pages in the fake). NewPager will fetch the first page, fill the + // buffer to licenseListPageSize, and — because the token is non-empty — return + // it as nextPageToken without fetching the second page. + lastPage := []*discoveryenginepb.UserLicense{ + {UserPrincipal: "alice@example.com", LicenseConfig: "SUBSCRIPTION_TIER_ENTERPRISE", LicenseAssignmentState: discoveryenginepb.UserLicense_ASSIGNED}, + } + fakeIt1 := newFakeIterator([][]*discoveryenginepb.UserLicense{fullPage, lastPage}, nil) + + mockClient := new(fakeUserLicenseClient) + mockClient.On("ListUserLicenses", mock.Anything, mock.AnythingOfType("*discoveryenginepb.ListUserLicensesRequest")). + Return(fakeIt1).Once() + + adapter := newWithClient(mockClient) + got1, nextTok, err := adapter.ListUserLicenses(context.Background(), "my-project", "") + + require.NoError(t, err) + require.Len(t, got1, licenseListPageSize) + assert.NotEmpty(t, nextTok, "expected a non-empty next page token when server has more data") + + // Second adapter call: only the last (partial) page remains. The server + // returns an empty token, so nextPageToken must be empty. + fakeIt2 := newFakeIterator([][]*discoveryenginepb.UserLicense{lastPage}, nil) + mockClient.On("ListUserLicenses", mock.Anything, mock.AnythingOfType("*discoveryenginepb.ListUserLicensesRequest")). + Return(fakeIt2).Once() + + got2, finalTok, err := adapter.ListUserLicenses(context.Background(), "my-project", nextTok) + + require.NoError(t, err) + require.Len(t, got2, 1) + assert.Equal(t, "alice@example.com", got2[0].UserEmail) + assert.Empty(t, finalTok, "expected an empty next page token on the last page") + + mockClient.AssertExpectations(t) +} + +func TestListUserLicenses_ResourceExhausted(t *testing.T) { + fakeIt := newFakeIterator(nil, makeGRPCError(codes.ResourceExhausted)) + + mockClient := new(fakeUserLicenseClient) + mockClient.On("ListUserLicenses", mock.Anything, mock.AnythingOfType("*discoveryenginepb.ListUserLicensesRequest")). + Return(fakeIt) + + adapter := newWithClient(mockClient) + _, _, err := adapter.ListUserLicenses(context.Background(), "my-project", "") + + require.Error(t, err) + assert.True(t, errors.Is(err, models.ErrAPIRateLimited), + "error chain missing ErrAPIRateLimited; err = %v", err) + assert.True(t, errors.Is(err, models.ErrLicenseListFailed), + "error chain missing ErrLicenseListFailed; err = %v", err) + + mockClient.AssertExpectations(t) +} + +func TestListUserLicenses_Unavailable(t *testing.T) { + fakeIt := newFakeIterator(nil, makeGRPCError(codes.Unavailable)) + + mockClient := new(fakeUserLicenseClient) + mockClient.On("ListUserLicenses", mock.Anything, mock.AnythingOfType("*discoveryenginepb.ListUserLicensesRequest")). + Return(fakeIt) + + adapter := newWithClient(mockClient) + _, _, err := adapter.ListUserLicenses(context.Background(), "my-project", "") + + require.Error(t, err) + assert.True(t, errors.Is(err, models.ErrAPIUnavailable), + "error chain missing ErrAPIUnavailable; err = %v", err) + assert.True(t, errors.Is(err, models.ErrLicenseListFailed), + "error chain missing ErrLicenseListFailed; err = %v", err) + + mockClient.AssertExpectations(t) +} + +func TestListUserLicenses_Unauthorized(t *testing.T) { + fakeIt := newFakeIterator(nil, makeGRPCError(codes.Unauthenticated)) + + mockClient := new(fakeUserLicenseClient) + mockClient.On("ListUserLicenses", mock.Anything, mock.AnythingOfType("*discoveryenginepb.ListUserLicensesRequest")). + Return(fakeIt) + + adapter := newWithClient(mockClient) + _, _, err := adapter.ListUserLicenses(context.Background(), "my-project", "") + + require.Error(t, err) + assert.True(t, errors.Is(err, models.ErrLicenseListFailed), + "error chain missing ErrLicenseListFailed; err = %v", err) + assert.True(t, errors.Is(err, models.ErrAPIUnauthorized), + "error chain missing ErrAPIUnauthorized; err = %v", err) + + mockClient.AssertExpectations(t) +} + +func TestListUserLicenses_RevokedState(t *testing.T) { + page := []*discoveryenginepb.UserLicense{ + { + UserPrincipal: "carol@example.com", + LicenseConfig: "SUBSCRIPTION_TIER_ENTERPRISE", + LicenseAssignmentState: discoveryenginepb.UserLicense_NO_LICENSE, + }, + } + fakeIt := newFakeIterator([][]*discoveryenginepb.UserLicense{page}, nil) + + mockClient := new(fakeUserLicenseClient) + mockClient.On("ListUserLicenses", mock.Anything, mock.AnythingOfType("*discoveryenginepb.ListUserLicensesRequest")). + Return(fakeIt) + + adapter := newWithClient(mockClient) + got, _, err := adapter.ListUserLicenses(context.Background(), "my-project", "") + + require.NoError(t, err) + require.Len(t, got, 1) + assert.Equal(t, models.LicenseStateRevoked, got[0].State) + + mockClient.AssertExpectations(t) +} + +func TestListUserLicenses_UnknownState(t *testing.T) { + // Use a raw integer value that does not correspond to any known + // LicenseAssignmentState constant to simulate a future SDK addition. + const unknownStateValue = discoveryenginepb.UserLicense_LicenseAssignmentState(99) + + page := []*discoveryenginepb.UserLicense{ + { + UserPrincipal: "dave@example.com", + LicenseConfig: "SUBSCRIPTION_TIER_ENTERPRISE", + LicenseAssignmentState: unknownStateValue, + }, + } + fakeIt := newFakeIterator([][]*discoveryenginepb.UserLicense{page}, nil) + + mockClient := new(fakeUserLicenseClient) + mockClient.On("ListUserLicenses", mock.Anything, mock.AnythingOfType("*discoveryenginepb.ListUserLicensesRequest")). + Return(fakeIt) + + adapter := newWithClient(mockClient) + _, _, err := adapter.ListUserLicenses(context.Background(), "my-project", "") + + require.Error(t, err) + assert.True(t, errors.Is(err, models.ErrLicenseListFailed), + "error chain missing ErrLicenseListFailed; err = %v", err) + assert.True(t, errors.Is(err, models.ErrUnknownLicenseState), + "error chain missing ErrUnknownLicenseState; err = %v", err) + + mockClient.AssertExpectations(t) +} + +// --- BatchUpdateUserLicenses tests --- + +func TestBatchUpdateUserLicenses_ExceedsMaxBatchSize(t *testing.T) { + updates := make([]models.LicenseUpdate, models.MaxBatchSize+1) + for i := range updates { + updates[i] = models.LicenseUpdate{ + UserEmail: "user@example.com", + LicenseConfigPath: "projects/my-project/locations/global/licenseConfigs/ent-uuid", + Action: models.LicenseActionGrant, + } + } + + // No expectations set — the API must not be called. + mockClient := new(fakeUserLicenseClient) + + adapter := newWithClient(mockClient) + err := adapter.BatchUpdateUserLicenses(context.Background(), "my-project", updates) + + require.Error(t, err) + assert.True(t, errors.Is(err, models.ErrBatchUpdateFailed), + "error chain missing ErrBatchUpdateFailed; err = %v", err) + + // AssertExpectations verifies no unexpected calls were made. + mockClient.AssertExpectations(t) +} + +func TestBatchUpdateUserLicenses_Grant_SetsLicenseConfigAndMask(t *testing.T) { + configPath := "projects/123/locations/global/licenseConfigs/ent-uuid" + updates := []models.LicenseUpdate{ + {UserEmail: "alice@example.com", LicenseConfigPath: configPath, Action: models.LicenseActionGrant}, + } + + var capturedReq *discoveryenginepb.BatchUpdateUserLicensesRequest + mockClient := new(fakeUserLicenseClient) + mockClient.On("BatchUpdateUserLicenses", mock.Anything, mock.AnythingOfType("*discoveryenginepb.BatchUpdateUserLicensesRequest")). + Run(func(args mock.Arguments) { + capturedReq = args.Get(1).(*discoveryenginepb.BatchUpdateUserLicensesRequest) + }). + Return(nil) + + adapter := newWithClient(mockClient) + err := adapter.BatchUpdateUserLicenses(context.Background(), "my-project", updates) + + require.NoError(t, err) + + src := capturedReq.GetSource().(*discoveryenginepb.BatchUpdateUserLicensesRequest_InlineSource_).InlineSource + assert.Equal(t, []string{"license_assignment_state", "license_config"}, src.UpdateMask.Paths) + require.Len(t, src.UserLicenses, 1) + assert.Equal(t, discoveryenginepb.UserLicense_ASSIGNED, src.UserLicenses[0].LicenseAssignmentState) + assert.Equal(t, configPath, src.UserLicenses[0].LicenseConfig) + + mockClient.AssertExpectations(t) +} + +func TestBatchUpdateUserLicenses_Revoke_OmitsLicenseConfigAndMask(t *testing.T) { + updates := []models.LicenseUpdate{ + {UserEmail: "bob@example.com", LicenseConfigPath: "projects/123/locations/global/licenseConfigs/ent-uuid", Action: models.LicenseActionRevoke}, + } + + var capturedReq *discoveryenginepb.BatchUpdateUserLicensesRequest + mockClient := new(fakeUserLicenseClient) + mockClient.On("BatchUpdateUserLicenses", mock.Anything, mock.AnythingOfType("*discoveryenginepb.BatchUpdateUserLicensesRequest")). + Run(func(args mock.Arguments) { + capturedReq = args.Get(1).(*discoveryenginepb.BatchUpdateUserLicensesRequest) + }). + Return(nil) + + adapter := newWithClient(mockClient) + err := adapter.BatchUpdateUserLicenses(context.Background(), "my-project", updates) + + require.NoError(t, err) + + src := capturedReq.GetSource().(*discoveryenginepb.BatchUpdateUserLicensesRequest_InlineSource_).InlineSource + assert.Equal(t, []string{"license_assignment_state"}, src.UpdateMask.Paths) + require.Len(t, src.UserLicenses, 1) + assert.Equal(t, discoveryenginepb.UserLicense_NO_LICENSE, src.UserLicenses[0].LicenseAssignmentState) + assert.Empty(t, src.UserLicenses[0].LicenseConfig) + + mockClient.AssertExpectations(t) +} + +func TestBatchUpdateUserLicenses_APIError(t *testing.T) { + updates := []models.LicenseUpdate{ + {UserEmail: "alice@example.com", LicenseConfigPath: "projects/my-project/locations/global/licenseConfigs/ent-uuid", Action: models.LicenseActionGrant}, + } + + mockClient := new(fakeUserLicenseClient) + mockClient.On("BatchUpdateUserLicenses", mock.Anything, mock.AnythingOfType("*discoveryenginepb.BatchUpdateUserLicensesRequest")). + Return(makeGRPCError(codes.Unavailable)) + + adapter := newWithClient(mockClient) + err := adapter.BatchUpdateUserLicenses(context.Background(), "my-project", updates) + + require.Error(t, err) + assert.True(t, errors.Is(err, models.ErrBatchUpdateFailed), + "error chain missing ErrBatchUpdateFailed; err = %v", err) + assert.True(t, errors.Is(err, models.ErrAPIUnavailable), + "error chain missing ErrAPIUnavailable; err = %v", err) + + mockClient.AssertExpectations(t) +} + +func TestBatchUpdateUserLicenses_LicensesExhausted(t *testing.T) { + // An InvalidArgument error whose message contains "subscription" and "limit" + // must produce ErrLicensesExhausted in the error chain, not ErrAPIRateLimited + // or ErrAPIUnavailable. This matches the Discovery Engine API response: + // "Subscription reaches the limit of N licenses for license config {path}" + updates := []models.LicenseUpdate{ + {UserEmail: "alice@example.com", LicenseConfigPath: "projects/my-project/locations/global/licenseConfigs/ent-uuid", Action: models.LicenseActionGrant}, + } + + exhaustionErr := status.Error(codes.InvalidArgument, "Subscription reaches the limit of 3 licenses for license config projects/123/locations/global/licenseConfigs/ent-uuid") + + mockClient := new(fakeUserLicenseClient) + mockClient.On("BatchUpdateUserLicenses", mock.Anything, mock.AnythingOfType("*discoveryenginepb.BatchUpdateUserLicensesRequest")). + Return(exhaustionErr) + + adapter := newWithClient(mockClient) + err := adapter.BatchUpdateUserLicenses(context.Background(), "my-project", updates) + + require.Error(t, err) + assert.True(t, errors.Is(err, models.ErrBatchUpdateFailed), + "error chain missing ErrBatchUpdateFailed; err = %v", err) + assert.True(t, errors.Is(err, models.ErrLicensesExhausted), + "error chain missing ErrLicensesExhausted; err = %v", err) + assert.False(t, errors.Is(err, models.ErrAPIRateLimited), + "ErrAPIRateLimited must not be in chain for exhaustion errors; err = %v", err) + + mockClient.AssertExpectations(t) +} + +func TestBatchUpdateUserLicenses_InvalidArgument_NonExhaustion_NotMapped(t *testing.T) { + // An InvalidArgument error that does NOT contain both "subscription" and + // "limit" must not be mapped to ErrLicensesExhausted — it passes through as-is. + updates := []models.LicenseUpdate{ + {UserEmail: "alice@example.com", LicenseConfigPath: "projects/my-project/locations/global/licenseConfigs/ent-uuid", Action: models.LicenseActionGrant}, + } + + otherPrecondErr := status.Error(codes.InvalidArgument, "invalid field: user_principal") + + mockClient := new(fakeUserLicenseClient) + mockClient.On("BatchUpdateUserLicenses", mock.Anything, mock.AnythingOfType("*discoveryenginepb.BatchUpdateUserLicensesRequest")). + Return(otherPrecondErr) + + adapter := newWithClient(mockClient) + err := adapter.BatchUpdateUserLicenses(context.Background(), "my-project", updates) + + require.Error(t, err) + assert.True(t, errors.Is(err, models.ErrBatchUpdateFailed), + "error chain missing ErrBatchUpdateFailed; err = %v", err) + assert.False(t, errors.Is(err, models.ErrLicensesExhausted), + "ErrLicensesExhausted must not appear for non-exhaustion FailedPrecondition; err = %v", err) + + mockClient.AssertExpectations(t) +} diff --git a/search/gemini-enterprise/group-licensing/internal/adapters/resourcemanager/adapter.go b/search/gemini-enterprise/group-licensing/internal/adapters/resourcemanager/adapter.go new file mode 100644 index 0000000..476726d --- /dev/null +++ b/search/gemini-enterprise/group-licensing/internal/adapters/resourcemanager/adapter.go @@ -0,0 +1,56 @@ +/* +Copyright © 2026 Google LLC + +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 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package resourcemanager + +import ( + "context" + "fmt" + "strings" + + cloudresourcemanager "google.golang.org/api/cloudresourcemanager/v3" + + "github.com/cloud-gtm/gemini-box-office/internal/ports" +) + +var _ ports.ResourceManagerClient = (*Adapter)(nil) + +// Adapter implements ports.ResourceManagerClient using the Cloud Resource +// Manager API v3. +type Adapter struct { + svc *cloudresourcemanager.Service +} + +// New constructs an Adapter from an already-authenticated +// *cloudresourcemanager.Service. The caller is responsible for configuring +// Application Default Credentials before calling New. +func New(svc *cloudresourcemanager.Service) *Adapter { + return &Adapter{svc: svc} +} + +// ResolveProjectNumber returns the numeric project number for the given project +// ID by calling projects.get on the Resource Manager API. The returned string +// contains only the number (e.g. "415104041262"), with the "projects/" prefix +// stripped. +func (a *Adapter) ResolveProjectNumber(ctx context.Context, projectID string) (string, error) { + project, err := a.svc.Projects.Get("projects/" + projectID).Context(ctx).Do() + if err != nil { + return "", fmt.Errorf("resolving project number for %q: %w", projectID, err) + } + // project.Name has the form "projects/415104041262". + number := strings.TrimPrefix(project.Name, "projects/") + return number, nil +} diff --git a/search/gemini-enterprise/group-licensing/internal/config/config.go b/search/gemini-enterprise/group-licensing/internal/config/config.go new file mode 100644 index 0000000..d7c4ce4 --- /dev/null +++ b/search/gemini-enterprise/group-licensing/internal/config/config.go @@ -0,0 +1,135 @@ +/* +Copyright © 2026 Google LLC + +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 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package config + +import ( + "encoding/json" + "errors" + "fmt" + "os" + "regexp" + + "github.com/cloud-gtm/gemini-box-office/internal/models" +) + +// validProjectID matches GCP project IDs: 6–30 chars, starts with a lowercase +// letter, contains only [a-z0-9-], and ends with a letter or digit. +var validProjectID = regexp.MustCompile(`^[a-z][a-z0-9\-]{4,28}[a-z0-9]$`) + +// validEmail matches group email addresses that contain exactly one '@' with +// non-empty local and domain parts and at least one dot in the domain. +var validEmail = regexp.MustCompile(`^[^@\s]+@[^@\s]+\.[^@\s]+$`) + +// ProjectEntry maps a SKU and location to the list of Google Group email +// addresses whose members are entitled to that SKU at that location within +// a given GCP project. +type ProjectEntry struct { + SubscriptionTier models.SKU `json:"subscription_tier"` + Location models.Location `json:"location"` + Groups []string `json:"groups"` +} + +// ProjectConfig is the ordered list of SKU+location+group entries for a +// single GCP project. Each entry describes one tier of entitlement. +type ProjectConfig []ProjectEntry + +// Settings holds operator-controlled knobs for the batch job behaviour. +type Settings struct { + StalenessThresholdDays int `json:"staleness_threshold_days"` +} + +// EntitlementConfig is the top-level structure parsed from the mounted JSON +// file. Projects keys are GCP project IDs. +type EntitlementConfig struct { + BillingAccountID string `json:"billing_account_id"` + Projects map[string]ProjectConfig `json:"projects"` + Settings Settings `json:"settings"` +} + +// Load reads, parses, and validates the entitlement config file at path. It +// returns a sentinel-wrapped error on every failure so callers can use errors.Is. +// +// A StalenessThresholdDays of 0 means the staleness check is disabled entirely; +// only the entitlement check will run during garbage collection. +func Load(path string) (*EntitlementConfig, error) { + data, err := os.ReadFile(path) + if err != nil { + if errors.Is(err, os.ErrNotExist) { + return nil, fmt.Errorf("reading config: %w", models.ErrConfigNotFound) + } + return nil, fmt.Errorf("reading config: %w", models.ErrConfigUnreadable) + } + + var cfg EntitlementConfig + if err := json.Unmarshal(data, &cfg); err != nil { + return nil, fmt.Errorf("parsing config: %w", models.ErrConfigInvalid) + } + + if err := validate(&cfg); err != nil { + return nil, err + } + + return &cfg, nil +} + +// validate enforces all structural and semantic constraints on the config. +// It is read-only and does not mutate cfg. +func validate(cfg *EntitlementConfig) error { + if cfg.BillingAccountID == "" { + return fmt.Errorf("config validation: billing_account_id is required: %w", models.ErrConfigInvalid) + } + + if len(cfg.Projects) == 0 { + return fmt.Errorf("config validation: %w", models.ErrConfigNoProjects) + } + + for projectID, projectCfg := range cfg.Projects { + if !validProjectID.MatchString(projectID) { + return fmt.Errorf("project %q is not a valid GCP project ID: %w", projectID, models.ErrConfigInvalid) + } + + if len(projectCfg) == 0 { + return fmt.Errorf("project %q has no entries: %w", projectID, models.ErrConfigNoGroups) + } + + for i, entry := range projectCfg { + if !entry.SubscriptionTier.IsValid() { + return fmt.Errorf("project %q entry %d contains unrecognized SKU %q: %w", projectID, i, entry.SubscriptionTier, models.ErrInvalidSKU) + } + + if !entry.Location.IsValid() { + return fmt.Errorf("project %q entry %d contains invalid location %q: %w", projectID, i, entry.Location, models.ErrInvalidLocation) + } + + if len(entry.Groups) == 0 { + return fmt.Errorf("project %q entry %d has no group emails: %w", projectID, i, models.ErrConfigNoGroups) + } + + for _, email := range entry.Groups { + if !validEmail.MatchString(email) { + return fmt.Errorf("project %q entry %d contains invalid group email %q: %w", projectID, i, email, models.ErrConfigInvalid) + } + } + } + } + + if cfg.Settings.StalenessThresholdDays < 0 { + return fmt.Errorf("settings.staleness_threshold_days must be >= 0: %w", models.ErrConfigInvalid) + } + + return nil +} diff --git a/search/gemini-enterprise/group-licensing/internal/config/config_test.go b/search/gemini-enterprise/group-licensing/internal/config/config_test.go new file mode 100644 index 0000000..570ab9e --- /dev/null +++ b/search/gemini-enterprise/group-licensing/internal/config/config_test.go @@ -0,0 +1,283 @@ +/* +Copyright © 2026 Google LLC + +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 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package config + +import ( + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/cloud-gtm/gemini-box-office/internal/models" +) + +func writeFixture(t *testing.T, content string) string { + t.Helper() + path := filepath.Join(t.TempDir(), "entitlements.json") + if err := os.WriteFile(path, []byte(content), 0600); err != nil { + t.Fatalf("writeFixture: %v", err) + } + return path +} + +func TestLoad_ValidConfig(t *testing.T) { + path := writeFixture(t, `{ + "billing_account_id": "ABCDE-12345-FGHIJ", + "projects": { + "acme-prod": [ + { + "subscription_tier": "SUBSCRIPTION_TIER_ENTERPRISE", + "location": "global", + "groups": ["eng@acme.com"] + }, + { + "subscription_tier": "SUBSCRIPTION_TIER_AGENTSPACE_BUSINESS", + "location": "global", + "groups": ["mkt@acme.com"] + } + ] + }, + "settings": { + "staleness_threshold_days": 45 + } + }`) + + cfg, err := Load(path) + require.NoError(t, err) + + assert.Equal(t, "ABCDE-12345-FGHIJ", cfg.BillingAccountID) + + proj, ok := cfg.Projects["acme-prod"] + require.True(t, ok, "expected project acme-prod to be present") + require.Len(t, proj, 2) + + // Find the SUBSCRIPTION_TIER_ENTERPRISE entry and verify its groups. + var entEntry ProjectEntry + for _, e := range proj { + if e.SubscriptionTier == models.SKUEnterprise { + entEntry = e + break + } + } + require.Equal(t, models.SKUEnterprise, entEntry.SubscriptionTier) + require.Len(t, entEntry.Groups, 1) + assert.Equal(t, "eng@acme.com", entEntry.Groups[0]) + + assert.Equal(t, 45, cfg.Settings.StalenessThresholdDays) +} + +func TestLoad_MultiProjectConfig(t *testing.T) { + path := writeFixture(t, `{ + "billing_account_id": "ABCDE-12345-FGHIJ", + "projects": { + "proj-alpha": [ + { + "subscription_tier": "SUBSCRIPTION_TIER_SEARCH_AND_ASSISTANT", + "location": "global", + "groups": ["researchers@alpha.com"] + } + ], + "proj-beta": [ + { + "subscription_tier": "SUBSCRIPTION_TIER_FRONTLINE_WORKER", + "location": "us", + "groups": ["sales@beta.com"] + }, + { + "subscription_tier": "SUBSCRIPTION_TIER_AGENTSPACE_BUSINESS", + "location": "eu", + "groups": ["ops@beta.com"] + } + ], + "proj-gamma": [ + { + "subscription_tier": "SUBSCRIPTION_TIER_ENTERPRISE", + "location": "global", + "groups": ["devs@gamma.com"] + } + ] + }, + "settings": { + "staleness_threshold_days": 14 + } + }`) + + cfg, err := Load(path) + require.NoError(t, err) + + assert.Len(t, cfg.Projects, 3) + assert.Contains(t, cfg.Projects, "proj-alpha") + assert.Contains(t, cfg.Projects, "proj-beta") + assert.Contains(t, cfg.Projects, "proj-gamma") + + assert.Len(t, cfg.Projects["proj-beta"], 2) +} + +func TestLoad_StalenessThresholdZeroMeansDisabled(t *testing.T) { + // A StalenessThresholdDays of 0 is valid and signals that the staleness + // check is disabled; the value must be preserved as-is (not normalised to + // any default). + path := writeFixture(t, `{ + "billing_account_id": "ABCDE-12345-FGHIJ", + "projects": { + "proj-a": [ + { + "subscription_tier": "SUBSCRIPTION_TIER_ENTERPRISE", + "location": "global", + "groups": ["team@corp.com"] + } + ] + }, + "settings": { + "staleness_threshold_days": 0 + } + }`) + + cfg, err := Load(path) + require.NoError(t, err) + + assert.Equal(t, 0, cfg.Settings.StalenessThresholdDays) +} + +func TestLoad_FileNotFound(t *testing.T) { + _, err := Load(filepath.Join(t.TempDir(), "does-not-exist.json")) + assert.ErrorIs(t, err, models.ErrConfigNotFound) +} + +func TestLoad_FileUnreadable(t *testing.T) { + path := writeFixture(t, `{}`) + if err := os.Chmod(path, 0000); err != nil { + t.Skip("cannot change file permissions on this platform") + } + t.Cleanup(func() { _ = os.Chmod(path, 0600) }) + + _, err := Load(path) + assert.ErrorIs(t, err, models.ErrConfigUnreadable) +} + +func TestLoad_MalformedJSON(t *testing.T) { + path := writeFixture(t, `{ this is not valid json }`) + + _, err := Load(path) + assert.ErrorIs(t, err, models.ErrConfigInvalid) +} + +func TestLoad_ValidationFailures(t *testing.T) { + tests := []struct { + name string + json string + wantErr error + }{ + { + name: "missing billing_account_id", + json: `{"projects": {"proj-x": [{"subscription_tier": "SUBSCRIPTION_TIER_ENTERPRISE", "location": "global", "groups": ["team@corp.com"]}]}, "settings": {"staleness_threshold_days": 30}}`, + wantErr: models.ErrConfigInvalid, + }, + { + name: "no projects", + json: `{"billing_account_id": "ABCDE-12345-FGHIJ", "projects": {}, "settings": {"staleness_threshold_days": 30}}`, + wantErr: models.ErrConfigNoProjects, + }, + { + name: "project with no entries", + json: `{ + "billing_account_id": "ABCDE-12345-FGHIJ", + "projects": { + "empty-proj": [] + }, + "settings": {"staleness_threshold_days": 30} + }`, + wantErr: models.ErrConfigNoGroups, + }, + { + name: "invalid SKU", + json: `{ + "billing_account_id": "ABCDE-12345-FGHIJ", + "projects": { + "proj-x": [ + { + "subscription_tier": "SUBSCRIPTION_TIER_INVALID_UNKNOWN", + "location": "global", + "groups": ["team@corp.com"] + } + ] + }, + "settings": {"staleness_threshold_days": 30} + }`, + wantErr: models.ErrInvalidSKU, + }, + { + name: "invalid location", + json: `{ + "billing_account_id": "ABCDE-12345-FGHIJ", + "projects": { + "proj-x": [ + { + "subscription_tier": "SUBSCRIPTION_TIER_ENTERPRISE", + "location": "apac", + "groups": ["team@corp.com"] + } + ] + }, + "settings": {"staleness_threshold_days": 30} + }`, + wantErr: models.ErrInvalidLocation, + }, + { + name: "entry with empty groups slice", + json: `{ + "billing_account_id": "ABCDE-12345-FGHIJ", + "projects": { + "proj-x": [ + { + "subscription_tier": "SUBSCRIPTION_TIER_ENTERPRISE", + "location": "global", + "groups": [] + } + ] + }, + "settings": {"staleness_threshold_days": 30} + }`, + wantErr: models.ErrConfigNoGroups, + }, + { + name: "negative staleness threshold", + json: `{"billing_account_id": "ABCDE-12345-FGHIJ", "projects": {"proj-x": [{"subscription_tier": "SUBSCRIPTION_TIER_ENTERPRISE", "location": "global", "groups": ["team@corp.com"]}]}, "settings": {"staleness_threshold_days": -1}}`, + wantErr: models.ErrConfigInvalid, + }, + { + name: "invalid project ID", + json: `{"billing_account_id": "ABCDE-12345-FGHIJ", "projects": {"INVALID_PROJECT!": [{"subscription_tier": "SUBSCRIPTION_TIER_ENTERPRISE", "location": "global", "groups": ["team@corp.com"]}]}, "settings": {"staleness_threshold_days": 30}}`, + wantErr: models.ErrConfigInvalid, + }, + { + name: "invalid group email - missing at sign", + json: `{"billing_account_id": "ABCDE-12345-FGHIJ", "projects": {"valid-proj-id": [{"subscription_tier": "SUBSCRIPTION_TIER_ENTERPRISE", "location": "global", "groups": ["notanemail"]}]}, "settings": {"staleness_threshold_days": 30}}`, + wantErr: models.ErrConfigInvalid, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + path := writeFixture(t, tc.json) + _, err := Load(path) + assert.ErrorIs(t, err, tc.wantErr) + }) + } +} diff --git a/search/gemini-enterprise/group-licensing/internal/config/job_settings.go b/search/gemini-enterprise/group-licensing/internal/config/job_settings.go new file mode 100644 index 0000000..a5a7a18 --- /dev/null +++ b/search/gemini-enterprise/group-licensing/internal/config/job_settings.go @@ -0,0 +1,78 @@ +package config + +import ( + "fmt" + "os" + "strconv" + + "github.com/cloud-gtm/gemini-box-office/internal/models" +) + +// JobSettings holds the Cloud Run Job execution parameters read from +// well-known environment variables injected by the Cloud Run Jobs runtime. +type JobSettings struct { + JobType models.WorkflowType + DryRun bool + TaskIndex int + TaskCount int +} + +// LoadJobSettings reads Cloud Run Job configuration from environment variables +// and returns a validated JobSettings. It returns a sentinel-wrapped error on +// every validation failure so callers can use errors.Is. +// +// - JOB_TYPE (required): must be "joiner" or "garbage_collection". +// - DRY_RUN (optional, default false): parsed via strconv.ParseBool. +// - CLOUD_RUN_TASK_INDEX (optional, default 0): non-negative integer. +// - CLOUD_RUN_TASK_COUNT (optional, default 1): integer >= 1. +// - TaskIndex must be < TaskCount. +func LoadJobSettings() (*JobSettings, error) { + // JOB_TYPE — required. + rawJobType := os.Getenv("JOB_TYPE") + jobType := models.WorkflowType(rawJobType) + if jobType != models.WorkflowJoiner && jobType != models.WorkflowGarbageCollection { + return nil, fmt.Errorf("JOB_TYPE %q is not a recognised workflow type: %w", rawJobType, models.ErrConfigInvalid) + } + + // DRY_RUN — optional, default false. + dryRun := false + if raw := os.Getenv("DRY_RUN"); raw != "" { + var err error + dryRun, err = strconv.ParseBool(raw) + if err != nil { + return nil, fmt.Errorf("DRY_RUN %q is not a valid boolean: %w", raw, models.ErrConfigInvalid) + } + } + + // CLOUD_RUN_TASK_INDEX — optional, default 0. + taskIndex := 0 + if raw := os.Getenv("CLOUD_RUN_TASK_INDEX"); raw != "" { + var err error + taskIndex, err = strconv.Atoi(raw) + if err != nil || taskIndex < 0 { + return nil, fmt.Errorf("CLOUD_RUN_TASK_INDEX %q must be a non-negative integer: %w", raw, models.ErrConfigInvalid) + } + } + + // CLOUD_RUN_TASK_COUNT — optional, default 1. + taskCount := 1 + if raw := os.Getenv("CLOUD_RUN_TASK_COUNT"); raw != "" { + var err error + taskCount, err = strconv.Atoi(raw) + if err != nil || taskCount < 1 { + return nil, fmt.Errorf("CLOUD_RUN_TASK_COUNT %q must be an integer >= 1: %w", raw, models.ErrConfigInvalid) + } + } + + // Cross-field: index must be a valid slot within the task pool. + if taskIndex >= taskCount { + return nil, fmt.Errorf("CLOUD_RUN_TASK_INDEX (%d) must be < CLOUD_RUN_TASK_COUNT (%d): %w", taskIndex, taskCount, models.ErrConfigInvalid) + } + + return &JobSettings{ + JobType: jobType, + DryRun: dryRun, + TaskIndex: taskIndex, + TaskCount: taskCount, + }, nil +} diff --git a/search/gemini-enterprise/group-licensing/internal/config/job_settings_test.go b/search/gemini-enterprise/group-licensing/internal/config/job_settings_test.go new file mode 100644 index 0000000..5dcbcd8 --- /dev/null +++ b/search/gemini-enterprise/group-licensing/internal/config/job_settings_test.go @@ -0,0 +1,116 @@ +package config + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/cloud-gtm/gemini-box-office/internal/models" +) + +func TestLoadJobSettings(t *testing.T) { + tests := []struct { + name string + env map[string]string + want *JobSettings + wantErr error + }{ + { + name: "happy path: only JOB_TYPE set, all others default", + env: map[string]string{ + "JOB_TYPE": "joiner", + }, + want: &JobSettings{ + JobType: models.WorkflowJoiner, + DryRun: false, + TaskIndex: 0, + TaskCount: 1, + }, + }, + { + name: "happy path: all fields set to valid non-default values", + env: map[string]string{ + "JOB_TYPE": "garbage_collection", + "DRY_RUN": "true", + "CLOUD_RUN_TASK_INDEX": "2", + "CLOUD_RUN_TASK_COUNT": "5", + }, + want: &JobSettings{ + JobType: models.WorkflowGarbageCollection, + DryRun: true, + TaskIndex: 2, + TaskCount: 5, + }, + }, + { + name: "missing JOB_TYPE", + env: map[string]string{}, + wantErr: models.ErrConfigInvalid, + }, + { + name: "unrecognised JOB_TYPE", + env: map[string]string{ + "JOB_TYPE": "gc", + }, + wantErr: models.ErrConfigInvalid, + }, + { + name: "invalid DRY_RUN value", + env: map[string]string{ + "JOB_TYPE": "joiner", + "DRY_RUN": "yes", + }, + wantErr: models.ErrConfigInvalid, + }, + { + name: "invalid CLOUD_RUN_TASK_INDEX", + env: map[string]string{ + "JOB_TYPE": "joiner", + "CLOUD_RUN_TASK_INDEX": "abc", + }, + wantErr: models.ErrConfigInvalid, + }, + { + name: "CLOUD_RUN_TASK_COUNT=0", + env: map[string]string{ + "JOB_TYPE": "joiner", + "CLOUD_RUN_TASK_COUNT": "0", + }, + wantErr: models.ErrConfigInvalid, + }, + { + name: "TaskIndex >= TaskCount", + env: map[string]string{ + "JOB_TYPE": "joiner", + "CLOUD_RUN_TASK_INDEX": "3", + "CLOUD_RUN_TASK_COUNT": "3", + }, + wantErr: models.ErrConfigInvalid, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + // Unset all relevant vars so each sub-test starts from a clean slate, + // then apply only the vars declared for this case. + for _, key := range []string{"JOB_TYPE", "DRY_RUN", "CLOUD_RUN_TASK_INDEX", "CLOUD_RUN_TASK_COUNT"} { + t.Setenv(key, "") + } + for k, v := range tc.env { + t.Setenv(k, v) + } + + got, err := LoadJobSettings() + + if tc.wantErr != nil { + assert.ErrorIs(t, err, tc.wantErr) + assert.Nil(t, got) + return + } + + require.NoError(t, err) + assert.Equal(t, tc.want, got) + }) + } +} diff --git a/search/gemini-enterprise/group-licensing/internal/middleware/logging.go b/search/gemini-enterprise/group-licensing/internal/middleware/logging.go new file mode 100644 index 0000000..f188510 --- /dev/null +++ b/search/gemini-enterprise/group-licensing/internal/middleware/logging.go @@ -0,0 +1,35 @@ +/* +Copyright © 2026 Google LLC + +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 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package middleware + +import ( + "context" + "log/slog" +) + +// loggerKey is the unexported context key used to store the request-scoped logger. +type loggerKey struct{} + +// LoggerFromContext retrieves the request-scoped logger stored in ctx. +// When no logger is present (e.g. in unit tests) it returns slog.Default() +// as a safe fallback so callers never need to guard against a nil logger. +func LoggerFromContext(ctx context.Context) *slog.Logger { + if logger, ok := ctx.Value(loggerKey{}).(*slog.Logger); ok && logger != nil { + return logger + } + return slog.Default() +} diff --git a/search/gemini-enterprise/group-licensing/internal/middleware/logging_test.go b/search/gemini-enterprise/group-licensing/internal/middleware/logging_test.go new file mode 100644 index 0000000..7771943 --- /dev/null +++ b/search/gemini-enterprise/group-licensing/internal/middleware/logging_test.go @@ -0,0 +1,31 @@ +/* +Copyright © 2026 Google LLC + +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 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package middleware + +import ( + "context" + "log/slog" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestLoggerFromContext_ReturnsSlogDefaultWhenNotSet(t *testing.T) { + ctx := context.Background() + logger := LoggerFromContext(ctx) + assert.Equal(t, slog.Default(), logger, "LoggerFromContext must return slog.Default() when no logger is stored in context") +} diff --git a/search/gemini-enterprise/group-licensing/internal/models/constants.go b/search/gemini-enterprise/group-licensing/internal/models/constants.go new file mode 100644 index 0000000..416d325 --- /dev/null +++ b/search/gemini-enterprise/group-licensing/internal/models/constants.go @@ -0,0 +1,38 @@ +/* +Copyright © 2026 Google LLC + +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 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package models + +const ( + // ConfigFilePath is the filesystem path where the entitlement configuration + // is mounted from GCP Secret Manager. + ConfigFilePath = "/run/secrets/entitlements.json" + + // MaxBatchSize is the maximum number of license modifications that may be + // included in a single batchUpdateUserLicenses API call. + MaxBatchSize = 100 + + // MembersListPageSize is the number of members fetched per page when + // calling the Cloud Identity Admin API members.list method. + MembersListPageSize = 200 + + // MaxPagesPerGroup is the maximum number of pages fetched per group or + // license listing in a single workflow run. If exceeded, the loop logs a + // warning and stops processing further pages rather than failing the run — + // partial results are preferable to a full job failure for a scheduled + // reconciliation job. + MaxPagesPerGroup = 500 +) diff --git a/search/gemini-enterprise/group-licensing/internal/models/dto/sync.go b/search/gemini-enterprise/group-licensing/internal/models/dto/sync.go new file mode 100644 index 0000000..c310ee7 --- /dev/null +++ b/search/gemini-enterprise/group-licensing/internal/models/dto/sync.go @@ -0,0 +1,59 @@ +/* +Copyright © 2026 Google LLC + +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 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package dto + +// SyncAddRequest is the HTTP request body for POST /sync/add. +// Cloud Scheduler may POST an empty body; all fields are optional overrides. +type SyncAddRequest struct { + // DryRun overrides the config-level dry_run setting when present. + // A nil pointer means "use the config value". + DryRun *bool `json:"dry_run,omitempty"` +} + +// Validate checks that SyncAddRequest contains only valid field values. +func (r *SyncAddRequest) Validate() error { + // No required fields; nothing to validate beyond type safety. + return nil +} + +// SyncAddResponse is the HTTP response body for POST /sync/add. +type SyncAddResponse struct { + RequestID string `json:"request_id"` + LicensesGranted int `json:"licenses_granted"` + LicensesSoftFailed int `json:"licenses_soft_failed"` + GroupsProcessed int `json:"groups_processed"` + DryRun bool `json:"dry_run"` +} + +// SyncRemoveRequest is the HTTP request body for POST /sync/remove. +type SyncRemoveRequest struct { + // DryRun overrides the config-level dry_run setting when present. + DryRun *bool `json:"dry_run,omitempty"` +} + +// Validate checks that SyncRemoveRequest contains only valid field values. +func (r *SyncRemoveRequest) Validate() error { + return nil +} + +// SyncRemoveResponse is the HTTP response body for POST /sync/remove. +type SyncRemoveResponse struct { + RequestID string `json:"request_id"` + LicensesRevoked int `json:"licenses_revoked"` + UsersEvaluated int `json:"users_evaluated"` + DryRun bool `json:"dry_run"` +} diff --git a/search/gemini-enterprise/group-licensing/internal/models/dto/sync_test.go b/search/gemini-enterprise/group-licensing/internal/models/dto/sync_test.go new file mode 100644 index 0000000..bad6a88 --- /dev/null +++ b/search/gemini-enterprise/group-licensing/internal/models/dto/sync_test.go @@ -0,0 +1,124 @@ +/* +Copyright © 2026 Google LLC + +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 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package dto + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func boolPtr(v bool) *bool { return &v } + +func TestSyncAddRequest_Validate(t *testing.T) { + tests := []struct { + name string + req SyncAddRequest + wantErr bool + }{ + { + name: "zero value is valid", + req: SyncAddRequest{}, + wantErr: false, + }, + { + name: "dry_run true is valid", + req: SyncAddRequest{DryRun: boolPtr(true)}, + wantErr: false, + }, + { + name: "dry_run false is valid", + req: SyncAddRequest{DryRun: boolPtr(false)}, + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := tt.req.Validate() + if tt.wantErr { + require.Error(t, err) + } else { + require.NoError(t, err) + } + }) + } +} + +func TestSyncRemoveRequest_Validate(t *testing.T) { + tests := []struct { + name string + req SyncRemoveRequest + wantErr bool + }{ + { + name: "zero value is valid", + req: SyncRemoveRequest{}, + wantErr: false, + }, + { + name: "dry_run true is valid", + req: SyncRemoveRequest{DryRun: boolPtr(true)}, + wantErr: false, + }, + { + name: "dry_run false is valid", + req: SyncRemoveRequest{DryRun: boolPtr(false)}, + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := tt.req.Validate() + if tt.wantErr { + require.Error(t, err) + } else { + require.NoError(t, err) + } + }) + } +} + +func TestSyncAddResponse_HasRequestID(t *testing.T) { + resp := SyncAddResponse{RequestID: "req-123"} + assert.Equal(t, "req-123", resp.RequestID) +} + +func TestSyncRemoveResponse_HasRequestID(t *testing.T) { + resp := SyncRemoveResponse{RequestID: "req-456"} + assert.Equal(t, "req-456", resp.RequestID) +} + +// TestSyncAddResponse_ZeroValue ensures the zero value of SyncAddResponse is +// a safe, usable default (dry_run defaults to false, counts default to 0). +func TestSyncAddResponse_ZeroValue(t *testing.T) { + var resp SyncAddResponse + assert.False(t, resp.DryRun) + assert.Equal(t, 0, resp.LicensesGranted) + assert.Equal(t, 0, resp.GroupsProcessed) +} + +// TestSyncRemoveResponse_ZeroValue ensures the zero value of SyncRemoveResponse +// is a safe, usable default. +func TestSyncRemoveResponse_ZeroValue(t *testing.T) { + var resp SyncRemoveResponse + assert.False(t, resp.DryRun) + assert.Equal(t, 0, resp.LicensesRevoked) + assert.Equal(t, 0, resp.UsersEvaluated) +} diff --git a/search/gemini-enterprise/group-licensing/internal/models/enums.go b/search/gemini-enterprise/group-licensing/internal/models/enums.go new file mode 100644 index 0000000..cec24a4 --- /dev/null +++ b/search/gemini-enterprise/group-licensing/internal/models/enums.go @@ -0,0 +1,128 @@ +/* +Copyright © 2026 Google LLC + +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 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package models + +// SKU represents a Gemini Enterprise subscription tier identifier as returned +// by the Discovery Engine API. +type SKU string + +const ( + SKUSearchAndAssistant SKU = "SUBSCRIPTION_TIER_SEARCH_AND_ASSISTANT" // Gemini Enterprise Plus tier. + SKUEnterprise SKU = "SUBSCRIPTION_TIER_ENTERPRISE" // Gemini Enterprise Standard tier. + SKUSearch SKU = "SUBSCRIPTION_TIER_SEARCH" // Search + NotebookLM tier. + SKUNotebookLM SKU = "SUBSCRIPTION_TIER_NOTEBOOK_LM" // NotebookLM-only tier. + SKUAgentspaceBusiness SKU = "SUBSCRIPTION_TIER_AGENTSPACE_BUSINESS" // Gemini Business tier. + SKUAgentspaceStarter SKU = "SUBSCRIPTION_TIER_AGENTSPACE_STARTER" // Gemini Business Starter tier. + SKUFrontlineWorker SKU = "SUBSCRIPTION_TIER_FRONTLINE_WORKER" // Gemini Frontline Worker tier. + SKUFrontlineStarter SKU = "SUBSCRIPTION_TIER_FRONTLINE_STARTER" // Gemini Frontline Starter tier. + SKUEnterpriseEmerging SKU = "SUBSCRIPTION_TIER_ENTERPRISE_EMERGING" // Gemini Enterprise Standard — emerging markets. + SKUEduPro SKU = "SUBSCRIPTION_TIER_EDU_PRO" // Gemini Enterprise EDU Pro tier. + SKUEdu SKU = "SUBSCRIPTION_TIER_EDU" // Gemini Enterprise EDU tier. + SKUEduProEmerging SKU = "SUBSCRIPTION_TIER_EDU_PRO_EMERGING" // Gemini Enterprise EDU Pro — emerging markets. + SKUEduEmerging SKU = "SUBSCRIPTION_TIER_EDU_EMERGING" // Gemini Enterprise EDU — emerging markets. + SKUUnspecified SKU = "SUBSCRIPTION_TIER_UNSPECIFIED" // Default/unset value. +) + +// skuPrecedence maps each SKU to its precedence rank. Higher value = higher +// precedence. SUBSCRIPTION_TIER_UNSPECIFIED is not included and returns 0 by +// default, the same as any unrecognised value. +var skuPrecedence = map[SKU]int{ + SKUSearchAndAssistant: 13, + SKUEnterprise: 12, + SKUSearch: 11, + SKUNotebookLM: 10, + SKUAgentspaceBusiness: 9, + SKUAgentspaceStarter: 8, + SKUFrontlineWorker: 7, + SKUFrontlineStarter: 6, + SKUEnterpriseEmerging: 5, + SKUEduPro: 4, + SKUEdu: 3, + SKUEduProEmerging: 2, + SKUEduEmerging: 1, +} + +// IsValid reports whether s is one of the four recognized SKU values. +func (s SKU) IsValid() bool { + _, ok := skuPrecedence[s] + return ok +} + +// Precedence returns the numeric precedence of the SKU. +// A higher value indicates a higher-tier entitlement. +// Returns 0 for unrecognized SKUs. +func (s SKU) Precedence() int { + return skuPrecedence[s] +} + +// HasHigherPrecedenceThan reports whether s outranks other. +func (s SKU) HasHigherPrecedenceThan(other SKU) bool { + return s.Precedence() > other.Precedence() +} + +// WorkflowType identifies which reconciliation workflow is executing. +type WorkflowType string + +const ( + WorkflowJoiner WorkflowType = "joiner" + WorkflowGarbageCollection WorkflowType = "garbage_collection" +) + +// LicenseState represents the current state of a user license as returned +// by the Discovery Engine API. +type LicenseState string + +const ( + LicenseStateAssigned LicenseState = "ASSIGNED" + LicenseStateRevoked LicenseState = "REVOKED" +) + +// LicenseAction represents the operation to apply during batchUpdateUserLicenses. +type LicenseAction string + +const ( + LicenseActionGrant LicenseAction = "GRANT" + LicenseActionRevoke LicenseAction = "REVOKE" +) + +// Location represents the geographic region where Gemini licenses are managed. +type Location string + +const ( + LocationGlobal Location = "global" + LocationUS Location = "us" + LocationEU Location = "eu" +) + +var validLocations = map[Location]bool{ + LocationGlobal: true, + LocationUS: true, + LocationEU: true, +} + +// IsValid reports whether l is one of the three recognized location values. +func (l Location) IsValid() bool { + return validLocations[l] +} + +// MemberType classifies a Cloud Identity group member as returned by members.list. +type MemberType string + +const ( + MemberTypeUser MemberType = "USER" + MemberTypeGroup MemberType = "GROUP" +) diff --git a/search/gemini-enterprise/group-licensing/internal/models/enums_test.go b/search/gemini-enterprise/group-licensing/internal/models/enums_test.go new file mode 100644 index 0000000..5c99735 --- /dev/null +++ b/search/gemini-enterprise/group-licensing/internal/models/enums_test.go @@ -0,0 +1,168 @@ +/* +Copyright © 2026 Google LLC + +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 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package models + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestSKU_IsValid(t *testing.T) { + tests := []struct { + name string + sku SKU + want bool + }{ + {"search and assistant is valid", SKUSearchAndAssistant, true}, + {"enterprise is valid", SKUEnterprise, true}, + {"search is valid", SKUSearch, true}, + {"notebook LM is valid", SKUNotebookLM, true}, + {"agentspace business is valid", SKUAgentspaceBusiness, true}, + {"agentspace starter is valid", SKUAgentspaceStarter, true}, + {"frontline worker is valid", SKUFrontlineWorker, true}, + {"frontline starter is valid", SKUFrontlineStarter, true}, + {"enterprise emerging is valid", SKUEnterpriseEmerging, true}, + {"edu pro is valid", SKUEduPro, true}, + {"edu is valid", SKUEdu, true}, + {"edu pro emerging is valid", SKUEduProEmerging, true}, + {"edu emerging is valid", SKUEduEmerging, true}, + {"unspecified is invalid", SKUUnspecified, false}, + {"empty string is invalid", SKU(""), false}, + {"arbitrary string is invalid", SKU("GEMINI_UNKNOWN"), false}, + {"old SKU name is invalid", SKU("GEMINI_ENTERPRISE"), false}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.want, tt.sku.IsValid()) + }) + } +} + +func TestSKU_Precedence(t *testing.T) { + tests := []struct { + name string + sku SKU + want int + }{ + {"search and assistant has precedence 13", SKUSearchAndAssistant, 13}, + {"enterprise has precedence 12", SKUEnterprise, 12}, + {"search has precedence 11", SKUSearch, 11}, + {"notebook LM has precedence 10", SKUNotebookLM, 10}, + {"agentspace business has precedence 9", SKUAgentspaceBusiness, 9}, + {"agentspace starter has precedence 8", SKUAgentspaceStarter, 8}, + {"frontline worker has precedence 7", SKUFrontlineWorker, 7}, + {"frontline starter has precedence 6", SKUFrontlineStarter, 6}, + {"enterprise emerging has precedence 5", SKUEnterpriseEmerging, 5}, + {"edu pro has precedence 4", SKUEduPro, 4}, + {"edu has precedence 3", SKUEdu, 3}, + {"edu pro emerging has precedence 2", SKUEduProEmerging, 2}, + {"edu emerging has precedence 1", SKUEduEmerging, 1}, + {"unspecified has precedence 0", SKUUnspecified, 0}, + {"unknown has precedence 0", SKU("UNKNOWN"), 0}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.want, tt.sku.Precedence()) + }) + } +} + +func TestSKU_PrecedenceOrdering(t *testing.T) { + // Verify strict total ordering from highest to lowest precedence. + ordered := []SKU{ + SKUSearchAndAssistant, + SKUEnterprise, + SKUSearch, + SKUNotebookLM, + SKUAgentspaceBusiness, + SKUAgentspaceStarter, + SKUFrontlineWorker, + SKUFrontlineStarter, + SKUEnterpriseEmerging, + SKUEduPro, + SKUEdu, + SKUEduProEmerging, + SKUEduEmerging, + } + + for i := 0; i < len(ordered)-1; i++ { + higher := ordered[i] + lower := ordered[i+1] + assert.Greater(t, higher.Precedence(), lower.Precedence(), + "expected %q (%d) to have higher precedence than %q (%d)", + higher, higher.Precedence(), lower, lower.Precedence()) + } +} + +func TestSKU_HasHigherPrecedenceThan(t *testing.T) { + tests := []struct { + name string + s SKU + other SKU + want bool + }{ + {"search_and_assistant > enterprise", SKUSearchAndAssistant, SKUEnterprise, true}, + {"search_and_assistant > agentspace_business", SKUSearchAndAssistant, SKUAgentspaceBusiness, true}, + {"search_and_assistant > frontline_worker", SKUSearchAndAssistant, SKUFrontlineWorker, true}, + {"enterprise > agentspace_business", SKUEnterprise, SKUAgentspaceBusiness, true}, + {"enterprise > frontline_worker", SKUEnterprise, SKUFrontlineWorker, true}, + {"search > agentspace_business", SKUSearch, SKUAgentspaceBusiness, true}, + {"notebook_lm > agentspace_business", SKUNotebookLM, SKUAgentspaceBusiness, true}, + {"agentspace_business > frontline_worker", SKUAgentspaceBusiness, SKUFrontlineWorker, true}, + {"agentspace_starter above enterprise_emerging", SKUAgentspaceStarter, SKUEnterpriseEmerging, true}, + {"frontline_starter above enterprise_emerging", SKUFrontlineStarter, SKUEnterpriseEmerging, true}, + {"enterprise_emerging > edu_pro", SKUEnterpriseEmerging, SKUEduPro, true}, + {"edu_pro > edu", SKUEduPro, SKUEdu, true}, + {"edu > edu_pro_emerging", SKUEdu, SKUEduProEmerging, true}, + {"edu_pro_emerging > edu_emerging", SKUEduProEmerging, SKUEduEmerging, true}, + {"frontline_worker not > enterprise", SKUFrontlineWorker, SKUEnterprise, false}, + {"agentspace_business not > search", SKUAgentspaceBusiness, SKUSearch, false}, + {"edu_emerging not > enterprise_emerging", SKUEduEmerging, SKUEnterpriseEmerging, false}, + {"same SKU not > itself", SKUEnterprise, SKUEnterprise, false}, + {"unknown not > frontline_worker", SKU("UNKNOWN"), SKUFrontlineWorker, false}, + {"frontline_worker > unknown", SKUFrontlineWorker, SKU("UNKNOWN"), true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.want, tt.s.HasHigherPrecedenceThan(tt.other)) + }) + } +} + +func TestWorkflowType_Values(t *testing.T) { + assert.Equal(t, WorkflowType("joiner"), WorkflowJoiner) + assert.Equal(t, WorkflowType("garbage_collection"), WorkflowGarbageCollection) +} + +func TestLicenseState_Values(t *testing.T) { + assert.Equal(t, LicenseState("ASSIGNED"), LicenseStateAssigned) + assert.Equal(t, LicenseState("REVOKED"), LicenseStateRevoked) +} + +func TestLicenseAction_Values(t *testing.T) { + assert.Equal(t, LicenseAction("GRANT"), LicenseActionGrant) + assert.Equal(t, LicenseAction("REVOKE"), LicenseActionRevoke) +} + +func TestMemberType_Values(t *testing.T) { + assert.Equal(t, MemberType("USER"), MemberTypeUser) + assert.Equal(t, MemberType("GROUP"), MemberTypeGroup) +} diff --git a/search/gemini-enterprise/group-licensing/internal/models/errors.go b/search/gemini-enterprise/group-licensing/internal/models/errors.go new file mode 100644 index 0000000..84b691b --- /dev/null +++ b/search/gemini-enterprise/group-licensing/internal/models/errors.go @@ -0,0 +1,60 @@ +/* +Copyright © 2026 Google LLC + +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 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package models + +import "errors" + +// Config errors. +var ( + ErrConfigNotFound = errors.New("entitlement config file not found") + ErrConfigUnreadable = errors.New("entitlement config file could not be read") + ErrConfigInvalid = errors.New("entitlement config is invalid or malformed") + ErrConfigNoProjects = errors.New("entitlement config contains no projects") + ErrConfigNoGroups = errors.New("entitlement config contains no groups for project") +) + +// SKU errors. +var ( + ErrInvalidSKU = errors.New("unrecognized SKU value") +) + +// Location errors. +var ( + ErrInvalidLocation = errors.New("invalid location value") +) + +// License operation errors. +var ( + ErrLicenseListFailed = errors.New("failed to list user licenses") + ErrBatchUpdateFailed = errors.New("batch update of user licenses failed") + ErrLicensesExhausted = errors.New("license pool exhausted for SKU") + ErrUnknownLicenseState = errors.New("unknown license assignment state received from API") +) + +// Membership check errors. +var ( + ErrMemberListFailed = errors.New("failed to list group members") + ErrMembershipCheckFailed = errors.New("failed to check group membership") +) + +// API transport errors. +var ( + ErrAPIRateLimited = errors.New("api request was rate limited (429)") + ErrAPINotFound = errors.New("api resource not found (404)") + ErrAPIUnavailable = errors.New("api service temporarily unavailable (5xx)") + ErrAPIUnauthorized = errors.New("api request unauthorized (401/403)") +) diff --git a/search/gemini-enterprise/group-licensing/internal/models/types.go b/search/gemini-enterprise/group-licensing/internal/models/types.go new file mode 100644 index 0000000..ad893ba --- /dev/null +++ b/search/gemini-enterprise/group-licensing/internal/models/types.go @@ -0,0 +1,85 @@ +/* +Copyright © 2026 Google LLC + +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 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package models + +import "time" + +// Member represents a single entry returned by the Cloud Identity +// members.list API. Only members whose Type is MemberTypeUser are +// eligible to receive a license; GROUP-typed entries are containers +// that the adapter resolves by passing includeDerivedMembership=true. +type Member struct { + Email string + Type MemberType +} + +// UserLicense represents a single record returned by the Discovery +// Engine listUserLicenses API. LastLoginTime is zero when the API +// returns no last-login value (the user has never logged in or the +// field was omitted). AssignmentTime reflects when the license record +// was first created (the proto create_time field); it is used as a +// fallback reference for staleness checks when LastLoginTime is zero, +// so that a recently provisioned user who has not yet signed in is not +// immediately revoked. +type UserLicense struct { + UserEmail string + LicenseConfigPath string // full resource path: projects/{p}/locations/{l}/licenseConfigs/{id} + State LicenseState + LastLoginTime time.Time + AssignmentTime time.Time // zero when the API omits create_time +} + +// LicenseUpdate is the unit of work passed to BatchUpdateUserLicenses. +// Each value represents one grant or revoke operation for a single user. +// - For grants: set SKU and Location; the adapter resolves the licenseConfig path. +// - For revokes: set LicenseConfigPath from the UserLicense returned by ListUserLicenses. +// +// Callers must not construct slices longer than MaxBatchSize; the adapter +// enforces this limit but the service layer is expected to chunk proactively. +type LicenseUpdate struct { + UserEmail string + SKU SKU + Location Location + LicenseConfigPath string // revokes only: resource path from UserLicense + Action LicenseAction +} + +// LicenseConfigKey identifies a license configuration by its SKU, project +// number, and location. It is used as the key in a LicenseConfigIndex. +// ProjectNumber is the numeric GCP project number (e.g. "415104041262"), not +// the human-readable project ID — the Discovery Engine API uses numbers in +// licenseConfig resource paths. +type LicenseConfigKey struct { + SKU SKU + ProjectNumber string + Location Location +} + +// LicenseConfigEntry holds the full licenseConfig resource path and the number +// of licenses allocated to it from the billing account subscription. +// AllocatedCount is the value from BillingAccountLicenseConfig.LicenseConfigDistributions +// and represents the seat cap for this project/SKU/location combination. +type LicenseConfigEntry struct { + Path string + AllocatedCount int64 +} + +// LicenseConfigIndex maps (SKU, ProjectNumber, Location) to the resolved +// LicenseConfigEntry. It is built once at startup from the +// billingAccountLicenseConfigs API and used by the service layer to resolve +// grant operations and look up available seat capacity. +type LicenseConfigIndex map[LicenseConfigKey]LicenseConfigEntry diff --git a/search/gemini-enterprise/group-licensing/internal/ports/gemini.go b/search/gemini-enterprise/group-licensing/internal/ports/gemini.go new file mode 100644 index 0000000..4c9e1c6 --- /dev/null +++ b/search/gemini-enterprise/group-licensing/internal/ports/gemini.go @@ -0,0 +1,56 @@ +/* +Copyright © 2026 Google LLC + +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 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package ports + +import ( + "context" + + "github.com/cloud-gtm/gemini-box-office/internal/models" +) + +// GeminiClient is the port through which the service layer interacts +// with the Google Gemini Enterprise API. Concrete adapter +// implementations live in internal/adapters/discoveryengine and must satisfy +// this interface. +// +// Adapter satisfaction tests belong in internal/adapters/discoveryengine, not here. +type GeminiClient interface { + // FetchLicenseConfigIndex retrieves all license configurations for the given + // billing account and returns an index mapping (SKU, ProjectID, Location) to + // the full licenseConfig resource path. Call this once at startup and pass the + // result to SetLicenseConfigIndex before issuing any grant operations. + FetchLicenseConfigIndex(ctx context.Context, billingAccountID string) (models.LicenseConfigIndex, error) + + // ListUserLicenses returns one page of licensed users for projectID. Pass + // an empty pageToken to start from the beginning. A non-empty nextPageToken + // in the response means more pages are available. + ListUserLicenses(ctx context.Context, projectID, pageToken string) (licenses []models.UserLicense, nextPageToken string, err error) + + // BatchUpdateUserLicenses applies up to models.MaxBatchSize grant or revoke + // operations in a single API call. The adapter returns models.ErrBatchUpdateFailed + // if the underlying API reports any per-item or request-level failure. + // When the license pool for a SKU is exhausted, the error chain contains + // models.ErrLicensesExhausted. Callers are responsible for splitting slices + // longer than models.MaxBatchSize into multiple calls. + BatchUpdateUserLicenses(ctx context.Context, projectID string, updates []models.LicenseUpdate) error + + // FetchLicenseUsageStats returns a map of licenseConfig resource path to + // the number of licenses currently assigned (usedLicenseCount) for all + // licenseConfigs under the given project's default user store. Callers use + // this to compute available seats after a license pool exhaustion error. + FetchLicenseUsageStats(ctx context.Context, projectID string) (map[string]int64, error) +} diff --git a/search/gemini-enterprise/group-licensing/internal/ports/idp.go b/search/gemini-enterprise/group-licensing/internal/ports/idp.go new file mode 100644 index 0000000..d6faa47 --- /dev/null +++ b/search/gemini-enterprise/group-licensing/internal/ports/idp.go @@ -0,0 +1,40 @@ +/* +Copyright © 2026 Google LLC + +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 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package ports + +import ( + "context" + + "github.com/cloud-gtm/gemini-box-office/internal/models" +) + +// IdpClient is the port through which the service layer interacts +// with the Google Cloud Identity Admin API. Concrete adapter implementations +// live in internal/adapters/cloudidentity and must satisfy this interface. +// +// Adapter satisfaction tests belong in internal/adapters/cloudidentity, not here. +type IdpClient interface { + // ListMembers returns one page of members for groupEmail. Pass an empty + // pageToken to start from the beginning. The adapter calls members.list + // with includeDerivedMembership=true so nested groups are flattened. + // A non-empty nextPageToken in the response means more pages are available. + ListMembers(ctx context.Context, groupEmail, pageToken string) (members []models.Member, nextPageToken string, err error) + + // HasMember reports whether userEmail is a direct or indirect member of + // groupEmail. It wraps the members.hasMember API call. + HasMember(ctx context.Context, groupEmail, userEmail string) (bool, error) +} diff --git a/search/gemini-enterprise/group-licensing/internal/ports/resourcemanager.go b/search/gemini-enterprise/group-licensing/internal/ports/resourcemanager.go new file mode 100644 index 0000000..5c9bfdc --- /dev/null +++ b/search/gemini-enterprise/group-licensing/internal/ports/resourcemanager.go @@ -0,0 +1,30 @@ +/* +Copyright © 2026 Google LLC + +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 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package ports + +import "context" + +// ResourceManagerClient is the port through which the service layer resolves +// GCP project metadata. Concrete adapter implementations live in +// internal/adapters/resourcemanager and must satisfy this interface. +type ResourceManagerClient interface { + // ResolveProjectNumber returns the numeric project number for the given + // project ID (e.g. "my-project" → "415104041262"). The number is required + // to match resource paths returned by the Discovery Engine API, which uses + // project numbers rather than project IDs in licenseConfig resource names. + ResolveProjectNumber(ctx context.Context, projectID string) (string, error) +} diff --git a/search/gemini-enterprise/group-licensing/internal/services/gc.go b/search/gemini-enterprise/group-licensing/internal/services/gc.go new file mode 100644 index 0000000..832adaf --- /dev/null +++ b/search/gemini-enterprise/group-licensing/internal/services/gc.go @@ -0,0 +1,238 @@ +/* +Copyright © 2026 Google LLC + +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 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package services + +import ( + "context" + "fmt" + "log/slog" + "time" + + "github.com/cloud-gtm/gemini-box-office/internal/config" + "github.com/cloud-gtm/gemini-box-office/internal/middleware" + "github.com/cloud-gtm/gemini-box-office/internal/models" + "github.com/cloud-gtm/gemini-box-office/internal/models/dto" + "github.com/cloud-gtm/gemini-box-office/internal/ports" +) + +// GCService implements the "garbage_collection" workflow: for every configured +// project it pages through all licensed users, determines which are stale or +// no longer entitled to any configured group, and revokes their licenses in +// batches. +type GCService struct { + idp ports.IdpClient + gemini ports.GeminiClient +} + +// NewGCService constructs a GCService wired to the supplied port +// implementations. Logging is provided per-request via middleware.LoggerFromContext. +func NewGCService(idp ports.IdpClient, gemini ports.GeminiClient) *GCService { + return &GCService{ + idp: idp, + gemini: gemini, + } +} + +// Run executes the garbage-collection workflow against the provided configuration. +// +// It evaluates each licensed user for staleness (last login older than +// StalenessThresholdDays) and entitlement (membership in at least one configured +// group), revoking licences for users who fail either check. When +// StalenessThresholdDays is 0 the staleness check is skipped entirely and only +// the entitlement check runs. When req.DryRun is true the evaluation runs in +// full but no write API calls are made. +func (s *GCService) Run(ctx context.Context, cfg *config.EntitlementConfig, req dto.SyncRemoveRequest) (dto.SyncRemoveResponse, error) { + logger := middleware.LoggerFromContext(ctx) + dryRun := false + if req.DryRun != nil { + dryRun = *req.DryRun + } + + start := time.Now() + logger.InfoContext(ctx, "garbage collection workflow starting", + slog.Int("project_count", len(cfg.Projects)), + slog.Int("staleness_threshold_days", cfg.Settings.StalenessThresholdDays), + slog.Bool("dry_run", dryRun), + ) + + var totalRevoked, totalEvaluated int + + for projectID, projectCfg := range cfg.Projects { + revoked, evaluated, err := s.processProject(ctx, projectID, projectCfg, cfg.Settings.StalenessThresholdDays, dryRun) + if err != nil { + logger.ErrorContext(ctx, "garbage collection workflow failed", + slog.String("project_id", projectID), + slog.Any("error", err), + slog.Int("licenses_revoked_before_failure", totalRevoked), + slog.Int("users_evaluated_before_failure", totalEvaluated), + ) + return dto.SyncRemoveResponse{}, err + } + totalRevoked += revoked + totalEvaluated += evaluated + } + + elapsed := time.Since(start).Milliseconds() + logger.InfoContext(ctx, "garbage collection workflow complete", + slog.Int64("duration_ms", elapsed), + slog.Int("licenses_revoked", totalRevoked), + slog.Int("users_evaluated", totalEvaluated), + slog.Bool("dry_run", dryRun), + ) + + return dto.SyncRemoveResponse{ + LicensesRevoked: totalRevoked, + UsersEvaluated: totalEvaluated, + DryRun: dryRun, + }, nil +} + +// processProject pages through all licensed users for a single GCP project and +// revokes licences from users who are stale or no longer entitled. It returns +// the number of licenses revoked and users evaluated. +// +// Revocation candidates are chunked and flushed per page so that memory usage +// is bounded to one page of candidates at any point rather than accumulating +// the full result set before issuing any writes. +func (s *GCService) processProject(ctx context.Context, projectID string, projectCfg config.ProjectConfig, thresholdDays int, dryRun bool) (licensesRevoked, usersEvaluated int, err error) { + var pageToken string + var pageCount int + + for { + if err := ctx.Err(); err != nil { + return 0, 0, fmt.Errorf("context cancelled: %w", err) + } + if pageCount >= models.MaxPagesPerGroup { + middleware.LoggerFromContext(ctx).WarnContext(ctx, + "license listing exceeded page limit, truncating", + slog.String("project_id", projectID), + slog.Int("max_pages", models.MaxPagesPerGroup), + ) + break + } + pageCount++ + licenses, next, err := s.gemini.ListUserLicenses(ctx, projectID, pageToken) + if err != nil { + return 0, 0, fmt.Errorf("project %q listing licenses: %w", projectID, err) + } + + var pageRevocations []models.LicenseUpdate + + for _, license := range licenses { + if err := ctx.Err(); err != nil { + return 0, 0, fmt.Errorf("context cancelled: %w", err) + } + if license.State == models.LicenseStateRevoked { + continue + } + usersEvaluated++ + + shouldRevoke, err := s.shouldRevoke(ctx, license, projectCfg, thresholdDays) + if err != nil { + return 0, 0, fmt.Errorf("project %q evaluating license: %w", projectID, err) + } + + if shouldRevoke { + pageRevocations = append(pageRevocations, models.LicenseUpdate{ + UserEmail: license.UserEmail, + LicenseConfigPath: license.LicenseConfigPath, + Action: models.LicenseActionRevoke, + }) + } + } + + if len(pageRevocations) > 0 { + if !dryRun { + for _, chunk := range chunkLicenseUpdates(pageRevocations, models.MaxBatchSize) { + if err := s.gemini.BatchUpdateUserLicenses(ctx, projectID, chunk); err != nil { + return 0, 0, fmt.Errorf("project %q batch revoke: %w", projectID, err) + } + } + } + licensesRevoked += len(pageRevocations) + } + + if next == "" { + break + } + pageToken = next + } + + return licensesRevoked, usersEvaluated, nil +} + +// shouldRevoke returns true when the user holding license should have it +// revoked. A user is revocable when they are stale OR when they are not a +// member of any configured group. When thresholdDays is 0 the staleness check +// is disabled and only the entitlement check runs. When thresholdDays > 0 the +// staleness check short-circuits the more expensive membership check. +// +// Staleness reference time: +// - When LastLoginTime is set, it is used as the reference. +// - When LastLoginTime is zero (the user has never logged in), AssignmentTime +// is used instead, so that a recently provisioned account is not revoked +// before the user has had a chance to sign in. +// - When both are zero (pathological; should not occur in practice), the user +// is treated as immediately stale and the license is revoked. +func (s *GCService) shouldRevoke(ctx context.Context, license models.UserLicense, projectCfg config.ProjectConfig, thresholdDays int) (bool, error) { + // Staleness check: only performed when thresholdDays > 0. + if thresholdDays > 0 { + ref := license.LastLoginTime + if ref.IsZero() { + // Never logged in: fall back to the assignment date so a recently + // provisioned user is not immediately revoked. + ref = license.AssignmentTime + } + if ref.IsZero() || ref.Before(time.Now().AddDate(0, 0, -thresholdDays)) { + return true, nil + } + } + + // Entitlement check: the user must be a member of at least one group + // across all entries in the project config. + for _, entry := range projectCfg { + for _, groupEmail := range entry.Groups { + isMember, err := s.idp.HasMember(ctx, groupEmail, license.UserEmail) + if err != nil { + return false, fmt.Errorf("checking membership: %w", err) + } + if isMember { + // Still entitled; no revocation needed. + return false, nil + } + } + } + + // Not a member of any configured group. + return true, nil +} + +// chunkLicenseUpdates splits updates into slices of at most size elements. +func chunkLicenseUpdates(updates []models.LicenseUpdate, size int) [][]models.LicenseUpdate { + if size <= 0 { + size = models.MaxBatchSize + } + var chunks [][]models.LicenseUpdate + for i := 0; i < len(updates); i += size { + end := i + size + if end > len(updates) { + end = len(updates) + } + chunks = append(chunks, updates[i:end]) + } + return chunks +} diff --git a/search/gemini-enterprise/group-licensing/internal/services/gc_test.go b/search/gemini-enterprise/group-licensing/internal/services/gc_test.go new file mode 100644 index 0000000..e4fcb37 --- /dev/null +++ b/search/gemini-enterprise/group-licensing/internal/services/gc_test.go @@ -0,0 +1,643 @@ +/* +Copyright © 2026 Google LLC + +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 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package services + +import ( + "context" + "errors" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + + "github.com/cloud-gtm/gemini-box-office/internal/config" + "github.com/cloud-gtm/gemini-box-office/internal/models" + "github.com/cloud-gtm/gemini-box-office/internal/models/dto" +) + +// newGCConfig is a helper that returns an EntitlementConfig ready for GC tests. +// thresholdDays is used directly: 0 means the staleness check is disabled. +func newGCConfig(thresholdDays int, projects map[string]config.ProjectConfig) *config.EntitlementConfig { + return &config.EntitlementConfig{ + BillingAccountID: "ABCDE-12345-FGHIJ", + Projects: projects, + Settings: config.Settings{ + StalenessThresholdDays: thresholdDays, + }, + } +} + +func TestGCService_Run_StaleUser_LicenseRevoked(t *testing.T) { + // A user whose last login is older than the staleness threshold must be revoked. + ctx := context.Background() + + idp := new(MockIdpClient) + gemini := new(MockGeminiClient) + + const ( + projectID = "proj-stale" + group = "grp@example.com" + userEmail = "stale-user@example.com" + ) + + staleLogin := time.Now().AddDate(0, 0, -60) // 60 days ago — beyond 30-day threshold. + + gemini.On("ListUserLicenses", mock.Anything, projectID, ""). + Return([]models.UserLicense{ + { + UserEmail: userEmail, + State: models.LicenseStateAssigned, + LastLoginTime: staleLogin, + }, + }, "", nil) + + gemini.On("BatchUpdateUserLicenses", mock.Anything, projectID, mock.MatchedBy(func(updates []models.LicenseUpdate) bool { + return len(updates) == 1 && + updates[0].UserEmail == userEmail && + updates[0].Action == models.LicenseActionRevoke + })).Return(nil) + + cfg := newGCConfig(30, map[string]config.ProjectConfig{ + projectID: { + {SubscriptionTier: models.SKUAgentspaceBusiness, Location: models.LocationGlobal, Groups: []string{group}}, + }, + }) + + svc := NewGCService(idp, gemini) + resp, err := svc.Run(ctx, cfg, dto.SyncRemoveRequest{}) + + require.NoError(t, err) + assert.Equal(t, 1, resp.LicensesRevoked) + assert.Equal(t, 1, resp.UsersEvaluated) + + gemini.AssertExpectations(t) + // Stale check short-circuits; HasMember must NOT be called. + idp.AssertNotCalled(t, "HasMember") +} + +func TestGCService_Run_NeverLoggedIn_RecentAssignment_LicenseKept(t *testing.T) { + // A user who has never logged in but was assigned a license recently (within + // the staleness threshold) must NOT be revoked. AssignmentTime is used as the + // staleness reference when LastLoginTime is zero. + ctx := context.Background() + + idp := new(MockIdpClient) + gemini := new(MockGeminiClient) + + const ( + projectID = "proj-never-recent" + group = "grp@example.com" + userEmail = "new-user@example.com" + ) + + recentAssignment := time.Now().AddDate(0, 0, -5) // assigned 5 days ago — within 30-day threshold + + gemini.On("ListUserLicenses", mock.Anything, projectID, ""). + Return([]models.UserLicense{ + { + UserEmail: userEmail, + State: models.LicenseStateAssigned, + LastLoginTime: time.Time{}, // never logged in + AssignmentTime: recentAssignment, // but recently provisioned + }, + }, "", nil) + + // Still entitled — must be checked because staleness does not short-circuit. + idp.On("HasMember", mock.Anything, group, userEmail).Return(true, nil) + + cfg := newGCConfig(30, map[string]config.ProjectConfig{ + projectID: { + {SubscriptionTier: models.SKUAgentspaceBusiness, Location: models.LocationGlobal, Groups: []string{group}}, + }, + }) + + svc := NewGCService(idp, gemini) + resp, err := svc.Run(ctx, cfg, dto.SyncRemoveRequest{}) + + require.NoError(t, err) + assert.Equal(t, 0, resp.LicensesRevoked) + assert.Equal(t, 1, resp.UsersEvaluated) + + idp.AssertExpectations(t) + gemini.AssertNotCalled(t, "BatchUpdateUserLicenses") +} + +func TestGCService_Run_NeverLoggedIn_StaleAssignment_LicenseRevoked(t *testing.T) { + // A user who has never logged in and whose assignment date is beyond the + // staleness threshold must have their license revoked. AssignmentTime is + // used as the staleness reference when LastLoginTime is zero. + ctx := context.Background() + + idp := new(MockIdpClient) + gemini := new(MockGeminiClient) + + const ( + projectID = "proj-never-stale" + group = "grp@example.com" + userEmail = "ghost@example.com" + ) + + staleAssignment := time.Now().AddDate(0, 0, -60) // assigned 60 days ago — beyond 30-day threshold + + gemini.On("ListUserLicenses", mock.Anything, projectID, ""). + Return([]models.UserLicense{ + { + UserEmail: userEmail, + State: models.LicenseStateAssigned, + LastLoginTime: time.Time{}, // never logged in + AssignmentTime: staleAssignment, // and assigned long ago + }, + }, "", nil) + + gemini.On("BatchUpdateUserLicenses", mock.Anything, projectID, mock.MatchedBy(func(updates []models.LicenseUpdate) bool { + return len(updates) == 1 && + updates[0].UserEmail == userEmail && + updates[0].Action == models.LicenseActionRevoke + })).Return(nil) + + cfg := newGCConfig(30, map[string]config.ProjectConfig{ + projectID: { + {SubscriptionTier: models.SKUAgentspaceBusiness, Location: models.LocationGlobal, Groups: []string{group}}, + }, + }) + + svc := NewGCService(idp, gemini) + resp, err := svc.Run(ctx, cfg, dto.SyncRemoveRequest{}) + + require.NoError(t, err) + assert.Equal(t, 1, resp.LicensesRevoked) + assert.Equal(t, 1, resp.UsersEvaluated) + + gemini.AssertExpectations(t) + // Staleness short-circuits via AssignmentTime; HasMember must NOT be called. + idp.AssertNotCalled(t, "HasMember") +} + +func TestGCService_Run_NeverLoggedIn_NoAssignmentTime_LicenseRevoked(t *testing.T) { + // When both LastLoginTime and AssignmentTime are zero (pathological case), + // the user is treated as immediately stale and the license is revoked. + ctx := context.Background() + + idp := new(MockIdpClient) + gemini := new(MockGeminiClient) + + const ( + projectID = "proj-never-no-assign" + group = "grp@example.com" + userEmail = "no-timestamps@example.com" + ) + + gemini.On("ListUserLicenses", mock.Anything, projectID, ""). + Return([]models.UserLicense{ + { + UserEmail: userEmail, + State: models.LicenseStateAssigned, + LastLoginTime: time.Time{}, // never logged in + AssignmentTime: time.Time{}, // no assignment time available + }, + }, "", nil) + + gemini.On("BatchUpdateUserLicenses", mock.Anything, projectID, mock.MatchedBy(func(updates []models.LicenseUpdate) bool { + return len(updates) == 1 && + updates[0].UserEmail == userEmail && + updates[0].Action == models.LicenseActionRevoke + })).Return(nil) + + cfg := newGCConfig(30, map[string]config.ProjectConfig{ + projectID: { + {SubscriptionTier: models.SKUAgentspaceBusiness, Location: models.LocationGlobal, Groups: []string{group}}, + }, + }) + + svc := NewGCService(idp, gemini) + resp, err := svc.Run(ctx, cfg, dto.SyncRemoveRequest{}) + + require.NoError(t, err) + assert.Equal(t, 1, resp.LicensesRevoked) + assert.Equal(t, 1, resp.UsersEvaluated) + + gemini.AssertExpectations(t) + idp.AssertNotCalled(t, "HasMember") +} + +func TestGCService_Run_EntitledActiveUser_NotRevoked(t *testing.T) { + // A user with a recent login who is still a member of a configured group + // must NOT be revoked. + ctx := context.Background() + + idp := new(MockIdpClient) + gemini := new(MockGeminiClient) + + const ( + projectID = "proj-ok" + group = "grp@example.com" + userEmail = "active@example.com" + ) + + recentLogin := time.Now().AddDate(0, 0, -5) // 5 days ago — within threshold. + + gemini.On("ListUserLicenses", mock.Anything, projectID, ""). + Return([]models.UserLicense{ + { + UserEmail: userEmail, + State: models.LicenseStateAssigned, + LastLoginTime: recentLogin, + }, + }, "", nil) + + // The user is still a member of the configured group. + idp.On("HasMember", mock.Anything, group, userEmail).Return(true, nil) + + cfg := newGCConfig(30, map[string]config.ProjectConfig{ + projectID: { + {SubscriptionTier: models.SKUAgentspaceBusiness, Location: models.LocationGlobal, Groups: []string{group}}, + }, + }) + + svc := NewGCService(idp, gemini) + resp, err := svc.Run(ctx, cfg, dto.SyncRemoveRequest{}) + + require.NoError(t, err) + assert.Equal(t, 0, resp.LicensesRevoked) + assert.Equal(t, 1, resp.UsersEvaluated) + + idp.AssertExpectations(t) + gemini.AssertNotCalled(t, "BatchUpdateUserLicenses") +} + +func TestGCService_Run_UnentitledUser_LicenseRevoked(t *testing.T) { + // A user with a recent login who is NOT a member of any configured group + // must have their license revoked. + ctx := context.Background() + + idp := new(MockIdpClient) + gemini := new(MockGeminiClient) + + const ( + projectID = "proj-unentitled" + group = "grp@example.com" + userEmail = "removed@example.com" + ) + + recentLogin := time.Now().AddDate(0, 0, -5) + + gemini.On("ListUserLicenses", mock.Anything, projectID, ""). + Return([]models.UserLicense{ + { + UserEmail: userEmail, + State: models.LicenseStateAssigned, + LastLoginTime: recentLogin, + }, + }, "", nil) + + // The user is no longer in the group. + idp.On("HasMember", mock.Anything, group, userEmail).Return(false, nil) + + gemini.On("BatchUpdateUserLicenses", mock.Anything, projectID, mock.MatchedBy(func(updates []models.LicenseUpdate) bool { + return len(updates) == 1 && + updates[0].UserEmail == userEmail && + updates[0].Action == models.LicenseActionRevoke + })).Return(nil) + + cfg := newGCConfig(30, map[string]config.ProjectConfig{ + projectID: { + {SubscriptionTier: models.SKUAgentspaceBusiness, Location: models.LocationGlobal, Groups: []string{group}}, + }, + }) + + svc := NewGCService(idp, gemini) + resp, err := svc.Run(ctx, cfg, dto.SyncRemoveRequest{}) + + require.NoError(t, err) + assert.Equal(t, 1, resp.LicensesRevoked) + assert.Equal(t, 1, resp.UsersEvaluated) + + idp.AssertExpectations(t) + gemini.AssertExpectations(t) +} + +func TestGCService_Run_DryRun_NoAPIWrite(t *testing.T) { + // In dry-run mode, evaluation runs in full but BatchUpdateUserLicenses is + // never called, even when users qualify for revocation. + ctx := context.Background() + + idp := new(MockIdpClient) + gemini := new(MockGeminiClient) + + const ( + projectID = "proj-dry-gc" + group = "grp@example.com" + userEmail = "stale@example.com" + ) + + staleLogin := time.Now().AddDate(0, 0, -90) + + gemini.On("ListUserLicenses", mock.Anything, projectID, ""). + Return([]models.UserLicense{ + { + UserEmail: userEmail, + State: models.LicenseStateAssigned, + LastLoginTime: staleLogin, + }, + }, "", nil) + + cfg := newGCConfig(30, map[string]config.ProjectConfig{ + projectID: { + {SubscriptionTier: models.SKUAgentspaceBusiness, Location: models.LocationGlobal, Groups: []string{group}}, + }, + }) + + svc := NewGCService(idp, gemini) + resp, err := svc.Run(ctx, cfg, dto.SyncRemoveRequest{DryRun: boolPtr(true)}) + + require.NoError(t, err) + assert.True(t, resp.DryRun) + assert.Equal(t, 1, resp.LicensesRevoked) + assert.Equal(t, 1, resp.UsersEvaluated) + + gemini.AssertNotCalled(t, "BatchUpdateUserLicenses") +} + +func TestGCService_Run_ListUserLicensesError_ReturnsError(t *testing.T) { + ctx := context.Background() + + idp := new(MockIdpClient) + gemini := new(MockGeminiClient) + + const projectID = "proj-list-err" + + gemini.On("ListUserLicenses", mock.Anything, projectID, ""). + Return(nil, "", models.ErrLicenseListFailed) + + cfg := newGCConfig(30, map[string]config.ProjectConfig{ + projectID: { + {SubscriptionTier: models.SKUAgentspaceBusiness, Location: models.LocationGlobal, Groups: []string{"grp@example.com"}}, + }, + }) + + svc := NewGCService(idp, gemini) + _, err := svc.Run(ctx, cfg, dto.SyncRemoveRequest{}) + + require.Error(t, err) + assert.True(t, errors.Is(err, models.ErrLicenseListFailed)) + + gemini.AssertNotCalled(t, "BatchUpdateUserLicenses") +} + +func TestGCService_Run_MultiPagePagination_AllUsersEvaluated(t *testing.T) { + // Licensed users are spread across two pages; both must be evaluated. + ctx := context.Background() + + idp := new(MockIdpClient) + gemini := new(MockGeminiClient) + + const ( + projectID = "proj-gc-pages" + group = "grp@example.com" + tokenP1 = "gc-page-token-1" + ) + + recentLogin := time.Now().AddDate(0, 0, -1) + + // Page 1. + gemini.On("ListUserLicenses", mock.Anything, projectID, ""). + Return([]models.UserLicense{ + { + UserEmail: "user-a@example.com", + State: models.LicenseStateAssigned, + LastLoginTime: recentLogin, + }, + }, tokenP1, nil) + + // Page 2. + gemini.On("ListUserLicenses", mock.Anything, projectID, tokenP1). + Return([]models.UserLicense{ + { + UserEmail: "user-b@example.com", + State: models.LicenseStateAssigned, + LastLoginTime: recentLogin, + }, + }, "", nil) + + // Both users are still entitled. + idp.On("HasMember", mock.Anything, group, "user-a@example.com").Return(true, nil) + idp.On("HasMember", mock.Anything, group, "user-b@example.com").Return(true, nil) + + cfg := newGCConfig(30, map[string]config.ProjectConfig{ + projectID: { + {SubscriptionTier: models.SKUAgentspaceBusiness, Location: models.LocationGlobal, Groups: []string{group}}, + }, + }) + + svc := NewGCService(idp, gemini) + resp, err := svc.Run(ctx, cfg, dto.SyncRemoveRequest{}) + + require.NoError(t, err) + assert.Equal(t, 2, resp.UsersEvaluated) + assert.Equal(t, 0, resp.LicensesRevoked) + + idp.AssertExpectations(t) + gemini.AssertExpectations(t) + gemini.AssertNotCalled(t, "BatchUpdateUserLicenses") +} + +func TestGCService_Run_HasMemberError_ReturnsError(t *testing.T) { + // If HasMember returns an error, Run must propagate it. + ctx := context.Background() + + idp := new(MockIdpClient) + gemini := new(MockGeminiClient) + + const ( + projectID = "proj-hm-err" + group = "grp@example.com" + userEmail = "user@example.com" + ) + + recentLogin := time.Now().AddDate(0, 0, -1) + + gemini.On("ListUserLicenses", mock.Anything, projectID, ""). + Return([]models.UserLicense{ + { + UserEmail: userEmail, + State: models.LicenseStateAssigned, + LastLoginTime: recentLogin, + }, + }, "", nil) + + idp.On("HasMember", mock.Anything, group, userEmail). + Return(false, models.ErrMembershipCheckFailed) + + cfg := newGCConfig(30, map[string]config.ProjectConfig{ + projectID: { + {SubscriptionTier: models.SKUAgentspaceBusiness, Location: models.LocationGlobal, Groups: []string{group}}, + }, + }) + + svc := NewGCService(idp, gemini) + _, err := svc.Run(ctx, cfg, dto.SyncRemoveRequest{}) + + require.Error(t, err) + assert.True(t, errors.Is(err, models.ErrMembershipCheckFailed)) + + gemini.AssertNotCalled(t, "BatchUpdateUserLicenses") +} + +func TestGCService_Run_AlreadyRevokedLicense_Skipped(t *testing.T) { + // A license whose State is already LicenseStateRevoked must be skipped + // entirely — no membership check, no revocation, not counted in UsersEvaluated. + ctx := context.Background() + + idp := new(MockIdpClient) + gemini := new(MockGeminiClient) + + const projectID = "proj-already-revoked" + + gemini.On("ListUserLicenses", mock.Anything, projectID, ""). + Return([]models.UserLicense{ + { + UserEmail: "revoked@example.com", + State: models.LicenseStateRevoked, + }, + }, "", nil) + + cfg := newGCConfig(30, map[string]config.ProjectConfig{ + projectID: { + {SubscriptionTier: models.SKUAgentspaceBusiness, Location: models.LocationGlobal, Groups: []string{"grp@example.com"}}, + }, + }) + + svc := NewGCService(idp, gemini) + resp, err := svc.Run(ctx, cfg, dto.SyncRemoveRequest{}) + + require.NoError(t, err) + assert.Equal(t, 0, resp.UsersEvaluated) + assert.Equal(t, 0, resp.LicensesRevoked) + + idp.AssertNotCalled(t, "HasMember") + gemini.AssertNotCalled(t, "BatchUpdateUserLicenses") +} + +func TestGCService_Run_ContextCancelled_ReturnsError(t *testing.T) { + // A cancelled context must cause Run to return an error immediately without + // ever calling ListUserLicenses. + ctx, cancel := context.WithCancel(context.Background()) + cancel() // cancel before Run is invoked + + idp := new(MockIdpClient) + gemini := new(MockGeminiClient) + + const projectID = "proj-ctx-gc" + + cfg := newGCConfig(30, map[string]config.ProjectConfig{ + projectID: { + {SubscriptionTier: models.SKUAgentspaceBusiness, Location: models.LocationGlobal, Groups: []string{"grp@example.com"}}, + }, + }) + + svc := NewGCService(idp, gemini) + _, err := svc.Run(ctx, cfg, dto.SyncRemoveRequest{}) + + require.Error(t, err) + gemini.AssertNotCalled(t, "ListUserLicenses") +} + +func TestGCService_Run_StalenessDisabled_NeverLoggedInUserNotRevoked(t *testing.T) { + // When StalenessThresholdDays is 0 the staleness check is disabled. A user + // who has never logged in (zero LastLoginTime) but IS a member of the + // configured group must NOT be revoked — only the entitlement check runs. + ctx := context.Background() + + idp := new(MockIdpClient) + gemini := new(MockGeminiClient) + + const ( + projectID = "proj-staleness-disabled" + group = "grp@example.com" + userEmail = "new-user@example.com" + ) + + gemini.On("ListUserLicenses", mock.Anything, projectID, ""). + Return([]models.UserLicense{ + { + UserEmail: userEmail, + State: models.LicenseStateAssigned, + LastLoginTime: time.Time{}, // never logged in + }, + }, "", nil) + + // The user is still a member of the configured group. + idp.On("HasMember", mock.Anything, group, userEmail).Return(true, nil) + + cfg := newGCConfig(0, map[string]config.ProjectConfig{ + projectID: { + {SubscriptionTier: models.SKUAgentspaceBusiness, Location: models.LocationGlobal, Groups: []string{group}}, + }, + }) + + svc := NewGCService(idp, gemini) + resp, err := svc.Run(ctx, cfg, dto.SyncRemoveRequest{}) + + require.NoError(t, err) + assert.Equal(t, 0, resp.LicensesRevoked) + assert.Equal(t, 1, resp.UsersEvaluated) + + // Entitlement check must still run even when staleness is disabled. + idp.AssertExpectations(t) + gemini.AssertNotCalled(t, "BatchUpdateUserLicenses") +} + +func TestGCService_processProject_PageLimitReached(t *testing.T) { + // ListUserLicenses always returns one stale licensed user and a non-empty + // next-page token, simulating an endless paginator. processProject must + // stop after models.MaxPagesPerGroup calls and return nil error. + ctx := context.Background() + + idp := new(MockIdpClient) + gemini := new(MockGeminiClient) + + const projectID = "proj-gc-page-limit" + + staleLogin := time.Now().AddDate(0, 0, -60) // beyond 30-day threshold + + // The mock returns a stale user and a non-empty next-page token on every + // call regardless of the token supplied. + gemini.On("ListUserLicenses", mock.Anything, projectID, mock.Anything). + Return([]models.UserLicense{ + { + UserEmail: "stale@example.com", + State: models.LicenseStateAssigned, + LastLoginTime: staleLogin, + }, + }, "next-token", nil) + + // Each page produces one stale user who triggers a revocation batch. + gemini.On("BatchUpdateUserLicenses", mock.Anything, projectID, mock.Anything). + Return(nil) + + projectCfg := config.ProjectConfig{ + {SubscriptionTier: models.SKUAgentspaceBusiness, Location: models.LocationGlobal, Groups: []string{"grp@example.com"}}, + } + + svc := NewGCService(idp, gemini) + _, _, err := svc.processProject(ctx, projectID, projectCfg, 30, false) + + require.NoError(t, err) + gemini.AssertNumberOfCalls(t, "ListUserLicenses", models.MaxPagesPerGroup) +} diff --git a/search/gemini-enterprise/group-licensing/internal/services/joiner.go b/search/gemini-enterprise/group-licensing/internal/services/joiner.go new file mode 100644 index 0000000..392db7d --- /dev/null +++ b/search/gemini-enterprise/group-licensing/internal/services/joiner.go @@ -0,0 +1,286 @@ +/* +Copyright © 2026 Google LLC + +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 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package services + +import ( + "context" + "errors" + "fmt" + "log/slog" + "time" + + "github.com/cloud-gtm/gemini-box-office/internal/config" + "github.com/cloud-gtm/gemini-box-office/internal/middleware" + "github.com/cloud-gtm/gemini-box-office/internal/models" + "github.com/cloud-gtm/gemini-box-office/internal/models/dto" + "github.com/cloud-gtm/gemini-box-office/internal/ports" +) + +// JoinerService implements the "joiner" workflow: for every configured project +// it enumerates group memberships, resolves the highest-precedence SKU each +// user is entitled to, and grants the missing licenses in batches. +type JoinerService struct { + idp ports.IdpClient + gemini ports.GeminiClient + rm ports.ResourceManagerClient +} + +// NewJoinerService constructs a JoinerService wired to the supplied port +// implementations. Logging is provided per-request via middleware.LoggerFromContext. +func NewJoinerService(idp ports.IdpClient, gemini ports.GeminiClient, rm ports.ResourceManagerClient) *JoinerService { + return &JoinerService{ + idp: idp, + gemini: gemini, + rm: rm, + } +} + +// Run executes the joiner workflow against the provided configuration. +// +// It first fetches the billing account license config index to resolve +// licenseConfig resource paths for grant operations. For each project it then +// builds a map of userEmail → highest-entitled (SKU, location) by iterating +// every entry in the project config and paging through all group members. It +// issues BatchUpdateUserLicenses calls (chunked to models.MaxBatchSize) for +// the resolved grants. When req.DryRun is true the enumeration runs in full +// but no write API calls are made. +func (s *JoinerService) Run(ctx context.Context, cfg *config.EntitlementConfig, req dto.SyncAddRequest) (dto.SyncAddResponse, error) { + if err := ctx.Err(); err != nil { + return dto.SyncAddResponse{}, fmt.Errorf("context cancelled: %w", err) + } + + logger := middleware.LoggerFromContext(ctx) + dryRun := false + if req.DryRun != nil { + dryRun = *req.DryRun + } + + licenseIndex, err := s.gemini.FetchLicenseConfigIndex(ctx, cfg.BillingAccountID) + if err != nil { + return dto.SyncAddResponse{}, fmt.Errorf("fetching license config index: %w", err) + } + + // Resolve all configured project IDs to their numeric project numbers once. + // The Discovery Engine API uses project numbers in licenseConfig resource + // paths, while the entitlement config uses human-readable project IDs. + projectNumbers := make(map[string]string, len(cfg.Projects)) + for projectID := range cfg.Projects { + number, err := s.rm.ResolveProjectNumber(ctx, projectID) + if err != nil { + return dto.SyncAddResponse{}, fmt.Errorf("resolving project number for %q: %w", projectID, err) + } + projectNumbers[projectID] = number + } + + start := time.Now() + logger.InfoContext(ctx, "joiner workflow starting", + slog.Int("project_count", len(cfg.Projects)), + slog.Bool("dry_run", dryRun), + ) + + var totalGranted, totalSoftFailed, totalGroups int + + for projectID, projectCfg := range cfg.Projects { + granted, softFailed, groups, err := s.processProject(ctx, projectID, projectNumbers[projectID], projectCfg, licenseIndex, dryRun) + if err != nil { + logger.ErrorContext(ctx, "joiner workflow failed", + slog.String("project_id", projectID), + slog.Any("error", err), + slog.Int("licenses_granted_before_failure", totalGranted), + slog.Int("groups_processed_before_failure", totalGroups), + ) + return dto.SyncAddResponse{}, err + } + totalGranted += granted + totalSoftFailed += softFailed + totalGroups += groups + } + + elapsed := time.Since(start).Milliseconds() + logger.InfoContext(ctx, "joiner workflow complete", + slog.Int64("duration_ms", elapsed), + slog.Int("licenses_granted", totalGranted), + slog.Int("licenses_soft_failed", totalSoftFailed), + slog.Int("groups_processed", totalGroups), + slog.Bool("dry_run", dryRun), + ) + + return dto.SyncAddResponse{ + LicensesGranted: totalGranted, + LicensesSoftFailed: totalSoftFailed, + GroupsProcessed: totalGroups, + DryRun: dryRun, + }, nil +} + +// userEntitlement pairs the highest-precedence SKU a user is entitled to with +// the location of the group entry that granted it. +type userEntitlement struct { + SKU models.SKU + Location models.Location +} + +// processProject enumerates all configured groups for a single GCP project, +// resolves the highest-precedence (SKU, location) per user, looks up the +// licenseConfig resource path from index, and issues the grant batches. +// Updates are grouped by licenseConfigPath so each batch is homogeneous, +// which allows per-SKU exhaustion handling without ambiguity. +// It returns the number of licenses granted, the number of users soft-failed +// due to license pool exhaustion, and the number of groups processed. +func (s *JoinerService) processProject(ctx context.Context, projectID, projectNumber string, projectCfg config.ProjectConfig, index models.LicenseConfigIndex, dryRun bool) (licensesGranted, licensesSoftFailed, groupsProcessed int, err error) { + // userBestEntitlement maps each user email to the highest-ranked entitlement + // (SKU + location) across all groups in this project. + userBestEntitlement := make(map[string]userEntitlement) + + for _, cfgEntry := range projectCfg { + for _, groupEmail := range cfgEntry.Groups { + if err := s.collectGroupMembers(ctx, groupEmail, cfgEntry.SubscriptionTier, cfgEntry.Location, userBestEntitlement); err != nil { + return 0, 0, 0, fmt.Errorf("project %q group %q: %w", projectID, groupEmail, err) + } + groupsProcessed++ + } + } + + // Group updates by licenseConfigPath so each batch is homogeneous. + // This allows exhaustion handling to target the correct SKU pool. + pendingByConfig := make(map[string][]models.LicenseUpdate) + entryByConfigPath := make(map[string]models.LicenseConfigEntry) + + for email, ent := range userBestEntitlement { + key := models.LicenseConfigKey{SKU: ent.SKU, ProjectNumber: projectNumber, Location: ent.Location} + idxEntry, ok := index[key] + if !ok { + return 0, 0, 0, fmt.Errorf("project %q: no licenseConfig found for SKU %q location %q", projectID, ent.SKU, ent.Location) + } + pendingByConfig[idxEntry.Path] = append(pendingByConfig[idxEntry.Path], models.LicenseUpdate{ + UserEmail: email, + SKU: ent.SKU, + Location: ent.Location, + LicenseConfigPath: idxEntry.Path, + Action: models.LicenseActionGrant, + }) + entryByConfigPath[idxEntry.Path] = idxEntry + } + + for configPath, updates := range pendingByConfig { + idxEntry := entryByConfigPath[configPath] + for _, chunk := range chunkLicenseUpdates(updates, models.MaxBatchSize) { + if dryRun { + licensesGranted += len(chunk) + continue + } + granted, softFailed, err := s.grantBatch(ctx, projectID, projectNumber, idxEntry, chunk) + if err != nil { + return 0, 0, 0, fmt.Errorf("project %q batch grant: %w", projectID, err) + } + licensesGranted += granted + licensesSoftFailed += softFailed + } + } + + return licensesGranted, licensesSoftFailed, groupsProcessed, nil +} + +// grantBatch issues a BatchUpdateUserLicenses call for a homogeneous batch +// (all updates share the same licenseConfigPath). On license pool exhaustion +// it fetches the current usage stats, retries with however many seats remain, +// and soft-fails the rest. Non-exhaustion errors are returned as hard failures. +// It returns the number of licenses granted and the number soft-failed. +func (s *JoinerService) grantBatch(ctx context.Context, projectID, projectNumber string, entry models.LicenseConfigEntry, batch []models.LicenseUpdate) (granted, softFailed int, err error) { + logger := middleware.LoggerFromContext(ctx) + + if batchErr := s.gemini.BatchUpdateUserLicenses(ctx, projectID, batch); batchErr == nil { + return len(batch), 0, nil + } else if !errors.Is(batchErr, models.ErrLicensesExhausted) { + return 0, 0, batchErr + } + + // License pool exhausted. Look up how many seats are still available. + usageStats, statsErr := s.gemini.FetchLicenseUsageStats(ctx, projectNumber) + if statsErr != nil { + return 0, 0, fmt.Errorf("fetching license usage stats after exhaustion: %w", statsErr) + } + + used := usageStats[entry.Path] + available := entry.AllocatedCount - used + if available < 0 { + available = 0 + } + + logger.WarnContext(ctx, "license pool exhausted, soft-failing remaining users", + slog.String("project_id", projectID), + slog.String("license_config_path", entry.Path), + slog.Int64("available", available), + slog.Int("soft_failed", len(batch)-int(available)), + ) + + if available == 0 { + return 0, len(batch), nil + } + + // Retry with only the available seats. Any error here is a hard failure. + trimmed := batch[:available] + if retryErr := s.gemini.BatchUpdateUserLicenses(ctx, projectID, trimmed); retryErr != nil { + return 0, 0, retryErr + } + return int(available), len(batch) - int(available), nil +} + +// collectGroupMembers pages through all members of groupEmail and updates +// userBestEntitlement with the supplied (sku, location) when sku is higher +// than any previously recorded SKU for that user. +func (s *JoinerService) collectGroupMembers(ctx context.Context, groupEmail string, sku models.SKU, location models.Location, userBestEntitlement map[string]userEntitlement) error { + var pageToken string + var pageCount int + + for { + if err := ctx.Err(); err != nil { + return fmt.Errorf("context cancelled: %w", err) + } + if pageCount >= models.MaxPagesPerGroup { + middleware.LoggerFromContext(ctx).WarnContext(ctx, + "group member enumeration exceeded page limit, truncating", + slog.String("group_email", groupEmail), + slog.Int("max_pages", models.MaxPagesPerGroup), + ) + break + } + pageCount++ + members, next, err := s.idp.ListMembers(ctx, groupEmail, pageToken) + if err != nil { + return fmt.Errorf("group %q: %w", groupEmail, err) + } + + for _, m := range members { + if m.Type != models.MemberTypeUser { + continue + } + + existing, seen := userBestEntitlement[m.Email] + if !seen || sku.HasHigherPrecedenceThan(existing.SKU) { + userBestEntitlement[m.Email] = userEntitlement{SKU: sku, Location: location} + } + } + + if next == "" { + break + } + pageToken = next + } + + return nil +} diff --git a/search/gemini-enterprise/group-licensing/internal/services/joiner_test.go b/search/gemini-enterprise/group-licensing/internal/services/joiner_test.go new file mode 100644 index 0000000..359bbfa --- /dev/null +++ b/search/gemini-enterprise/group-licensing/internal/services/joiner_test.go @@ -0,0 +1,691 @@ +/* +Copyright © 2026 Google LLC + +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 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package services + +import ( + "context" + "errors" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + + "github.com/cloud-gtm/gemini-box-office/internal/config" + "github.com/cloud-gtm/gemini-box-office/internal/models" + "github.com/cloud-gtm/gemini-box-office/internal/models/dto" +) + +// boolPtr is a helper that returns a pointer to the given bool value. +func boolPtr(b bool) *bool { return &b } + +// newJoinerConfig builds a minimal EntitlementConfig for joiner tests. +func newJoinerConfig(projects map[string]config.ProjectConfig) *config.EntitlementConfig { + return &config.EntitlementConfig{ + BillingAccountID: "ABCDE-12345-FGHIJ", + Projects: projects, + Settings: config.Settings{ + StalenessThresholdDays: 30, + }, + } +} + +// projectNumber is the fake numeric project number used in joiner tests. +// licenseIndexForProject keys use this number (as the API would return), +// while the config uses human-readable project IDs. +const projectNumber = "123456789" + +// licenseIndexForProject returns a LicenseConfigIndex keyed by project number +// (as the Discovery Engine API returns) for the SKU/location combinations used +// across joiner tests. +func licenseIndexForProject(number string) models.LicenseConfigIndex { + return models.LicenseConfigIndex{ + {SKU: models.SKUAgentspaceBusiness, ProjectNumber: number, Location: models.LocationGlobal}: {Path: "projects/" + number + "/locations/global/licenseConfigs/biz-config", AllocatedCount: 100}, + {SKU: models.SKUEnterprise, ProjectNumber: number, Location: models.LocationGlobal}: {Path: "projects/" + number + "/locations/global/licenseConfigs/ent-config", AllocatedCount: 50}, + } +} + +func TestJoinerService_Run_HappyPath_SKUPrecedence(t *testing.T) { + // A user who is a member of both a GEMINI_BUSINESS group and a + // GEMINI_ENTERPRISE group should receive only GEMINI_ENTERPRISE (higher + // precedence). A user in only GEMINI_BUSINESS gets that SKU. + ctx := context.Background() + + idp := new(MockIdpClient) + gemini := new(MockGeminiClient) + rm := new(MockResourceManagerClient) + + const ( + projectID = "proj-1" + groupBiz = "biz@example.com" + groupEnt = "ent@example.com" + userBizOnly = "biz-only@example.com" + userBoth = "both@example.com" + ) + + gemini.On("FetchLicenseConfigIndex", mock.Anything, "ABCDE-12345-FGHIJ"). + Return(licenseIndexForProject(projectNumber), nil) + rm.On("ResolveProjectNumber", mock.Anything, projectID).Return(projectNumber, nil) + + // GEMINI_BUSINESS group: biz-only and both users. + idp.On("ListMembers", mock.Anything, groupBiz, ""). + Return([]models.Member{ + {Email: userBizOnly, Type: models.MemberTypeUser}, + {Email: userBoth, Type: models.MemberTypeUser}, + }, "", nil) + + // GEMINI_ENTERPRISE group: only the "both" user. + idp.On("ListMembers", mock.Anything, groupEnt, ""). + Return([]models.Member{ + {Email: userBoth, Type: models.MemberTypeUser}, + }, "", nil) + + // Updates are now grouped by licenseConfigPath, so we expect two separate + // BatchUpdateUserLicenses calls: one for ent-config (bothUser) and one for + // biz-config (bizOnlyUser). The calls may arrive in either order. + gemini.On("BatchUpdateUserLicenses", mock.Anything, projectID, mock.MatchedBy(func(updates []models.LicenseUpdate) bool { + return len(updates) == 1 && + updates[0].UserEmail == userBoth && + updates[0].LicenseConfigPath == "projects/"+projectNumber+"/locations/global/licenseConfigs/ent-config" && + updates[0].Action == models.LicenseActionGrant + })).Return(nil) + gemini.On("BatchUpdateUserLicenses", mock.Anything, projectID, mock.MatchedBy(func(updates []models.LicenseUpdate) bool { + return len(updates) == 1 && + updates[0].UserEmail == userBizOnly && + updates[0].LicenseConfigPath == "projects/"+projectNumber+"/locations/global/licenseConfigs/biz-config" && + updates[0].Action == models.LicenseActionGrant + })).Return(nil) + + cfg := newJoinerConfig(map[string]config.ProjectConfig{ + projectID: { + {SubscriptionTier: models.SKUAgentspaceBusiness, Location: models.LocationGlobal, Groups: []string{groupBiz}}, + {SubscriptionTier: models.SKUEnterprise, Location: models.LocationGlobal, Groups: []string{groupEnt}}, + }, + }) + + svc := NewJoinerService(idp, gemini, rm) + resp, err := svc.Run(ctx, cfg, dto.SyncAddRequest{}) + + require.NoError(t, err) + assert.Equal(t, 2, resp.LicensesGranted) + assert.Equal(t, 2, resp.GroupsProcessed) + assert.False(t, resp.DryRun) + + idp.AssertExpectations(t) + gemini.AssertExpectations(t) + rm.AssertExpectations(t) +} + +func TestJoinerService_Run_DryRun_NoAPIWrite(t *testing.T) { + // In dry-run mode, ListMembers is still called but BatchUpdateUserLicenses + // must never be invoked. + ctx := context.Background() + + idp := new(MockIdpClient) + gemini := new(MockGeminiClient) + rm := new(MockResourceManagerClient) + + const ( + projectID = "proj-dry" + group = "grp@example.com" + ) + + gemini.On("FetchLicenseConfigIndex", mock.Anything, "ABCDE-12345-FGHIJ"). + Return(licenseIndexForProject(projectNumber), nil) + rm.On("ResolveProjectNumber", mock.Anything, projectID).Return(projectNumber, nil) + + idp.On("ListMembers", mock.Anything, group, ""). + Return([]models.Member{ + {Email: "user@example.com", Type: models.MemberTypeUser}, + }, "", nil) + + cfg := newJoinerConfig(map[string]config.ProjectConfig{ + projectID: { + {SubscriptionTier: models.SKUAgentspaceBusiness, Location: models.LocationGlobal, Groups: []string{group}}, + }, + }) + + svc := NewJoinerService(idp, gemini, rm) + resp, err := svc.Run(ctx, cfg, dto.SyncAddRequest{DryRun: boolPtr(true)}) + + require.NoError(t, err) + assert.True(t, resp.DryRun) + assert.Equal(t, 1, resp.LicensesGranted) + assert.Equal(t, 1, resp.GroupsProcessed) + + idp.AssertExpectations(t) + rm.AssertExpectations(t) + gemini.AssertNotCalled(t, "BatchUpdateUserLicenses") +} + +func TestJoinerService_Run_FetchLicenseConfigIndexError_ReturnsError(t *testing.T) { + ctx := context.Background() + + idp := new(MockIdpClient) + gemini := new(MockGeminiClient) + rm := new(MockResourceManagerClient) + + gemini.On("FetchLicenseConfigIndex", mock.Anything, "ABCDE-12345-FGHIJ"). + Return(models.LicenseConfigIndex(nil), errors.New("billing api unavailable")) + + cfg := newJoinerConfig(map[string]config.ProjectConfig{ + "proj-x": { + {SubscriptionTier: models.SKUAgentspaceBusiness, Location: models.LocationGlobal, Groups: []string{"grp@example.com"}}, + }, + }) + + svc := NewJoinerService(idp, gemini, rm) + _, err := svc.Run(ctx, cfg, dto.SyncAddRequest{}) + + require.Error(t, err) + rm.AssertNotCalled(t, "ResolveProjectNumber") + idp.AssertNotCalled(t, "ListMembers") + gemini.AssertNotCalled(t, "BatchUpdateUserLicenses") +} + +func TestJoinerService_Run_ResolveProjectNumberError_ReturnsError(t *testing.T) { + ctx := context.Background() + + idp := new(MockIdpClient) + gemini := new(MockGeminiClient) + rm := new(MockResourceManagerClient) + + gemini.On("FetchLicenseConfigIndex", mock.Anything, "ABCDE-12345-FGHIJ"). + Return(licenseIndexForProject(projectNumber), nil) + rm.On("ResolveProjectNumber", mock.Anything, "proj-x"). + Return("", errors.New("project not found")) + + cfg := newJoinerConfig(map[string]config.ProjectConfig{ + "proj-x": { + {SubscriptionTier: models.SKUAgentspaceBusiness, Location: models.LocationGlobal, Groups: []string{"grp@example.com"}}, + }, + }) + + svc := NewJoinerService(idp, gemini, rm) + _, err := svc.Run(ctx, cfg, dto.SyncAddRequest{}) + + require.Error(t, err) + idp.AssertNotCalled(t, "ListMembers") + gemini.AssertNotCalled(t, "BatchUpdateUserLicenses") +} + +func TestJoinerService_Run_ListMembersError_ReturnsWrappedError(t *testing.T) { + ctx := context.Background() + + idp := new(MockIdpClient) + gemini := new(MockGeminiClient) + rm := new(MockResourceManagerClient) + + const ( + projectID = "proj-err" + group = "grp@example.com" + ) + + gemini.On("FetchLicenseConfigIndex", mock.Anything, "ABCDE-12345-FGHIJ"). + Return(licenseIndexForProject(projectNumber), nil) + rm.On("ResolveProjectNumber", mock.Anything, projectID).Return(projectNumber, nil) + + idp.On("ListMembers", mock.Anything, group, ""). + Return(nil, "", models.ErrMemberListFailed) + + cfg := newJoinerConfig(map[string]config.ProjectConfig{ + projectID: { + {SubscriptionTier: models.SKUAgentspaceBusiness, Location: models.LocationGlobal, Groups: []string{group}}, + }, + }) + + svc := NewJoinerService(idp, gemini, rm) + _, err := svc.Run(ctx, cfg, dto.SyncAddRequest{}) + + require.Error(t, err) + assert.True(t, errors.Is(err, models.ErrMemberListFailed)) + + gemini.AssertNotCalled(t, "BatchUpdateUserLicenses") +} + +func TestJoinerService_Run_BatchUpdateError_ReturnsError(t *testing.T) { + ctx := context.Background() + + idp := new(MockIdpClient) + gemini := new(MockGeminiClient) + rm := new(MockResourceManagerClient) + + const ( + projectID = "proj-batch-err" + group = "grp@example.com" + ) + + gemini.On("FetchLicenseConfigIndex", mock.Anything, "ABCDE-12345-FGHIJ"). + Return(licenseIndexForProject(projectNumber), nil) + rm.On("ResolveProjectNumber", mock.Anything, projectID).Return(projectNumber, nil) + + idp.On("ListMembers", mock.Anything, group, ""). + Return([]models.Member{ + {Email: "user@example.com", Type: models.MemberTypeUser}, + }, "", nil) + + gemini.On("BatchUpdateUserLicenses", mock.Anything, projectID, mock.Anything). + Return(models.ErrBatchUpdateFailed) + + cfg := newJoinerConfig(map[string]config.ProjectConfig{ + projectID: { + {SubscriptionTier: models.SKUAgentspaceBusiness, Location: models.LocationGlobal, Groups: []string{group}}, + }, + }) + + svc := NewJoinerService(idp, gemini, rm) + _, err := svc.Run(ctx, cfg, dto.SyncAddRequest{}) + + require.Error(t, err) + assert.True(t, errors.Is(err, models.ErrBatchUpdateFailed)) + + idp.AssertExpectations(t) + gemini.AssertExpectations(t) + rm.AssertExpectations(t) +} + +func TestJoinerService_Run_MultiPagePagination_AllMembersCollected(t *testing.T) { + // Members are spread across two pages. All must be collected and granted. + ctx := context.Background() + + idp := new(MockIdpClient) + gemini := new(MockGeminiClient) + rm := new(MockResourceManagerClient) + + const ( + projectID = "proj-pages" + group = "grp@example.com" + tokenP1 = "page-token-1" + ) + + gemini.On("FetchLicenseConfigIndex", mock.Anything, "ABCDE-12345-FGHIJ"). + Return(licenseIndexForProject(projectNumber), nil) + rm.On("ResolveProjectNumber", mock.Anything, projectID).Return(projectNumber, nil) + + idp.On("ListMembers", mock.Anything, group, ""). + Return([]models.Member{ + {Email: "user-a@example.com", Type: models.MemberTypeUser}, + }, tokenP1, nil) + + idp.On("ListMembers", mock.Anything, group, tokenP1). + Return([]models.Member{ + {Email: "user-b@example.com", Type: models.MemberTypeUser}, + }, "", nil) + + gemini.On("BatchUpdateUserLicenses", mock.Anything, projectID, mock.MatchedBy(func(updates []models.LicenseUpdate) bool { + return len(updates) == 2 + })).Return(nil) + + cfg := newJoinerConfig(map[string]config.ProjectConfig{ + projectID: { + {SubscriptionTier: models.SKUAgentspaceBusiness, Location: models.LocationGlobal, Groups: []string{group}}, + }, + }) + + svc := NewJoinerService(idp, gemini, rm) + resp, err := svc.Run(ctx, cfg, dto.SyncAddRequest{}) + + require.NoError(t, err) + assert.Equal(t, 2, resp.LicensesGranted) + + idp.AssertExpectations(t) + gemini.AssertExpectations(t) + rm.AssertExpectations(t) +} + +func TestJoinerService_Run_EmptyGroup_NoBatchCall(t *testing.T) { + // A group that returns zero members must not trigger any batch call. + ctx := context.Background() + + idp := new(MockIdpClient) + gemini := new(MockGeminiClient) + rm := new(MockResourceManagerClient) + + const ( + projectID = "proj-empty" + group = "empty@example.com" + ) + + gemini.On("FetchLicenseConfigIndex", mock.Anything, "ABCDE-12345-FGHIJ"). + Return(models.LicenseConfigIndex{}, nil) + rm.On("ResolveProjectNumber", mock.Anything, projectID).Return(projectNumber, nil) + + idp.On("ListMembers", mock.Anything, group, ""). + Return([]models.Member{}, "", nil) + + cfg := newJoinerConfig(map[string]config.ProjectConfig{ + projectID: { + {SubscriptionTier: models.SKUAgentspaceBusiness, Location: models.LocationGlobal, Groups: []string{group}}, + }, + }) + + svc := NewJoinerService(idp, gemini, rm) + resp, err := svc.Run(ctx, cfg, dto.SyncAddRequest{}) + + require.NoError(t, err) + assert.Equal(t, 0, resp.LicensesGranted) + + idp.AssertExpectations(t) + rm.AssertExpectations(t) + gemini.AssertNotCalled(t, "BatchUpdateUserLicenses") +} + +func TestJoinerService_Run_ContextCancelled_ReturnsError(t *testing.T) { + // A cancelled context must cause Run to return an error immediately without + // calling FetchLicenseConfigIndex, ResolveProjectNumber, or ListMembers. + ctx, cancel := context.WithCancel(context.Background()) + cancel() + + idp := new(MockIdpClient) + gemini := new(MockGeminiClient) + rm := new(MockResourceManagerClient) + + cfg := newJoinerConfig(map[string]config.ProjectConfig{ + "proj-ctx-joiner": { + {SubscriptionTier: models.SKUAgentspaceBusiness, Location: models.LocationGlobal, Groups: []string{"grp@example.com"}}, + }, + }) + + svc := NewJoinerService(idp, gemini, rm) + _, err := svc.Run(ctx, cfg, dto.SyncAddRequest{}) + + require.Error(t, err) + gemini.AssertNotCalled(t, "FetchLicenseConfigIndex") + rm.AssertNotCalled(t, "ResolveProjectNumber") + idp.AssertNotCalled(t, "ListMembers") +} + +func TestJoinerService_Run_GroupTypeMembersIgnored(t *testing.T) { + // Members whose Type is GROUP (not USER) must not receive a license. + ctx := context.Background() + + idp := new(MockIdpClient) + gemini := new(MockGeminiClient) + rm := new(MockResourceManagerClient) + + const ( + projectID = "proj-group-type" + group = "parent@example.com" + ) + + gemini.On("FetchLicenseConfigIndex", mock.Anything, "ABCDE-12345-FGHIJ"). + Return(licenseIndexForProject(projectNumber), nil) + rm.On("ResolveProjectNumber", mock.Anything, projectID).Return(projectNumber, nil) + + idp.On("ListMembers", mock.Anything, group, ""). + Return([]models.Member{ + {Email: "nested-group@example.com", Type: models.MemberTypeGroup}, + {Email: "user@example.com", Type: models.MemberTypeUser}, + }, "", nil) + + gemini.On("BatchUpdateUserLicenses", mock.Anything, projectID, mock.MatchedBy(func(updates []models.LicenseUpdate) bool { + return len(updates) == 1 && updates[0].UserEmail == "user@example.com" + })).Return(nil) + + cfg := newJoinerConfig(map[string]config.ProjectConfig{ + projectID: { + {SubscriptionTier: models.SKUAgentspaceBusiness, Location: models.LocationGlobal, Groups: []string{group}}, + }, + }) + + svc := NewJoinerService(idp, gemini, rm) + resp, err := svc.Run(ctx, cfg, dto.SyncAddRequest{}) + + require.NoError(t, err) + assert.Equal(t, 1, resp.LicensesGranted) + + idp.AssertExpectations(t) + gemini.AssertExpectations(t) + rm.AssertExpectations(t) +} + +func TestJoinerService_Run_LicensePoolExhausted_TrimsAndSoftFails(t *testing.T) { + // BatchUpdateUserLicenses returns ErrLicensesExhausted on the first call. + // FetchLicenseUsageStats reports 48 used out of 50 allocated → 2 available. + // The service must retry with 2 users, soft-fail the remaining 3, and exit 0. + ctx := context.Background() + + idp := new(MockIdpClient) + gemini := new(MockGeminiClient) + rm := new(MockResourceManagerClient) + + const ( + projectID = "proj-exhaust" + group = "grp@example.com" + configPath = "projects/" + projectNumber + "/locations/global/licenseConfigs/ent-config" + allocatedStr = "50" + ) + + gemini.On("FetchLicenseConfigIndex", mock.Anything, "ABCDE-12345-FGHIJ"). + Return(licenseIndexForProject(projectNumber), nil) + rm.On("ResolveProjectNumber", mock.Anything, projectID).Return(projectNumber, nil) + + // 5 users in the group — all entitled to ENTERPRISE. + idp.On("ListMembers", mock.Anything, group, ""). + Return([]models.Member{ + {Email: "user-1@example.com", Type: models.MemberTypeUser}, + {Email: "user-2@example.com", Type: models.MemberTypeUser}, + {Email: "user-3@example.com", Type: models.MemberTypeUser}, + {Email: "user-4@example.com", Type: models.MemberTypeUser}, + {Email: "user-5@example.com", Type: models.MemberTypeUser}, + }, "", nil) + + // First call (all 5) → exhaustion. + gemini.On("BatchUpdateUserLicenses", mock.Anything, projectID, mock.MatchedBy(func(updates []models.LicenseUpdate) bool { + return len(updates) == 5 + })).Return(models.ErrLicensesExhausted).Once() + + // Usage stats: 48 used, allocated 50 → 2 available. + gemini.On("FetchLicenseUsageStats", mock.Anything, projectNumber). + Return(map[string]int64{configPath: 48}, nil) + + // Trimmed retry with exactly 2 users. + gemini.On("BatchUpdateUserLicenses", mock.Anything, projectID, mock.MatchedBy(func(updates []models.LicenseUpdate) bool { + return len(updates) == 2 + })).Return(nil).Once() + + cfg := newJoinerConfig(map[string]config.ProjectConfig{ + projectID: { + {SubscriptionTier: models.SKUEnterprise, Location: models.LocationGlobal, Groups: []string{group}}, + }, + }) + + svc := NewJoinerService(idp, gemini, rm) + resp, err := svc.Run(ctx, cfg, dto.SyncAddRequest{}) + + require.NoError(t, err) + assert.Equal(t, 2, resp.LicensesGranted) + assert.Equal(t, 3, resp.LicensesSoftFailed) + assert.False(t, resp.DryRun) + + idp.AssertExpectations(t) + gemini.AssertExpectations(t) + rm.AssertExpectations(t) +} + +func TestJoinerService_Run_LicensePoolFullyExhausted_AllSoftFailed(t *testing.T) { + // Pool is completely full (allocated == used). All users must be soft-failed; + // no trimmed retry should be issued and the job must still exit 0. + ctx := context.Background() + + idp := new(MockIdpClient) + gemini := new(MockGeminiClient) + rm := new(MockResourceManagerClient) + + const ( + projectID = "proj-full" + group = "grp@example.com" + configPath = "projects/" + projectNumber + "/locations/global/licenseConfigs/ent-config" + ) + + gemini.On("FetchLicenseConfigIndex", mock.Anything, "ABCDE-12345-FGHIJ"). + Return(licenseIndexForProject(projectNumber), nil) + rm.On("ResolveProjectNumber", mock.Anything, projectID).Return(projectNumber, nil) + + idp.On("ListMembers", mock.Anything, group, ""). + Return([]models.Member{ + {Email: "user-1@example.com", Type: models.MemberTypeUser}, + {Email: "user-2@example.com", Type: models.MemberTypeUser}, + }, "", nil) + + // First call → exhaustion. + gemini.On("BatchUpdateUserLicenses", mock.Anything, projectID, mock.MatchedBy(func(updates []models.LicenseUpdate) bool { + return len(updates) == 2 + })).Return(models.ErrLicensesExhausted).Once() + + // AllocatedCount for ent-config is 50; report 50 used → 0 available. + gemini.On("FetchLicenseUsageStats", mock.Anything, projectNumber). + Return(map[string]int64{configPath: 50}, nil) + + cfg := newJoinerConfig(map[string]config.ProjectConfig{ + projectID: { + {SubscriptionTier: models.SKUEnterprise, Location: models.LocationGlobal, Groups: []string{group}}, + }, + }) + + svc := NewJoinerService(idp, gemini, rm) + resp, err := svc.Run(ctx, cfg, dto.SyncAddRequest{}) + + require.NoError(t, err) + assert.Equal(t, 0, resp.LicensesGranted) + assert.Equal(t, 2, resp.LicensesSoftFailed) + + // No trimmed retry should have been issued. + gemini.AssertNumberOfCalls(t, "BatchUpdateUserLicenses", 1) + gemini.AssertExpectations(t) + rm.AssertExpectations(t) +} + +func TestJoinerService_Run_LicensePoolExhausted_UsageStatsFails_ReturnsError(t *testing.T) { + // When BatchUpdateUserLicenses returns ErrLicensesExhausted and the + // FetchLicenseUsageStats call also fails, the error must propagate as a + // hard failure. + ctx := context.Background() + + idp := new(MockIdpClient) + gemini := new(MockGeminiClient) + rm := new(MockResourceManagerClient) + + const ( + projectID = "proj-stats-err" + group = "grp@example.com" + ) + + gemini.On("FetchLicenseConfigIndex", mock.Anything, "ABCDE-12345-FGHIJ"). + Return(licenseIndexForProject(projectNumber), nil) + rm.On("ResolveProjectNumber", mock.Anything, projectID).Return(projectNumber, nil) + + idp.On("ListMembers", mock.Anything, group, ""). + Return([]models.Member{ + {Email: "user@example.com", Type: models.MemberTypeUser}, + }, "", nil) + + gemini.On("BatchUpdateUserLicenses", mock.Anything, projectID, mock.Anything). + Return(models.ErrLicensesExhausted) + + gemini.On("FetchLicenseUsageStats", mock.Anything, projectNumber). + Return(nil, errors.New("usage stats api unavailable")) + + cfg := newJoinerConfig(map[string]config.ProjectConfig{ + projectID: { + {SubscriptionTier: models.SKUEnterprise, Location: models.LocationGlobal, Groups: []string{group}}, + }, + }) + + svc := NewJoinerService(idp, gemini, rm) + _, err := svc.Run(ctx, cfg, dto.SyncAddRequest{}) + + require.Error(t, err) + + idp.AssertExpectations(t) + gemini.AssertExpectations(t) + rm.AssertExpectations(t) +} + +func TestJoinerService_Run_LicensePoolExhausted_DryRun_NoFetchOrRetry(t *testing.T) { + // In dry-run mode, exhaustion can never be triggered because no API writes + // are made. All users should be counted as granted without any + // BatchUpdateUserLicenses or FetchLicenseUsageStats calls. + ctx := context.Background() + + idp := new(MockIdpClient) + gemini := new(MockGeminiClient) + rm := new(MockResourceManagerClient) + + const ( + projectID = "proj-dry-exhaust" + group = "grp@example.com" + ) + + gemini.On("FetchLicenseConfigIndex", mock.Anything, "ABCDE-12345-FGHIJ"). + Return(licenseIndexForProject(projectNumber), nil) + rm.On("ResolveProjectNumber", mock.Anything, projectID).Return(projectNumber, nil) + + idp.On("ListMembers", mock.Anything, group, ""). + Return([]models.Member{ + {Email: "user-1@example.com", Type: models.MemberTypeUser}, + {Email: "user-2@example.com", Type: models.MemberTypeUser}, + }, "", nil) + + cfg := newJoinerConfig(map[string]config.ProjectConfig{ + projectID: { + {SubscriptionTier: models.SKUEnterprise, Location: models.LocationGlobal, Groups: []string{group}}, + }, + }) + + svc := NewJoinerService(idp, gemini, rm) + resp, err := svc.Run(ctx, cfg, dto.SyncAddRequest{DryRun: boolPtr(true)}) + + require.NoError(t, err) + assert.Equal(t, 2, resp.LicensesGranted) + assert.Equal(t, 0, resp.LicensesSoftFailed) + assert.True(t, resp.DryRun) + + gemini.AssertNotCalled(t, "BatchUpdateUserLicenses") + gemini.AssertNotCalled(t, "FetchLicenseUsageStats") +} + +func TestJoinerService_collectGroupMembers_PageLimitReached(t *testing.T) { + // ListMembers always returns one member and a non-empty next-page token, + // simulating an endless paginator. collectGroupMembers must stop after + // models.MaxPagesPerGroup calls and return nil (no error). + ctx := context.Background() + + idp := new(MockIdpClient) + gemini := new(MockGeminiClient) + + const ( + groupEmail = "endless@example.com" + userEmail = "user@example.com" + ) + + // The mock returns a non-empty next-page token on every call regardless + // of the token supplied, so we use mock.Anything for the token argument. + idp.On("ListMembers", mock.Anything, groupEmail, mock.Anything). + Return([]models.Member{ + {Email: userEmail, Type: models.MemberTypeUser}, + }, "next-token", nil) + + svc := NewJoinerService(idp, gemini, new(MockResourceManagerClient)) + userBestEntitlement := make(map[string]userEntitlement) + + err := svc.collectGroupMembers(ctx, groupEmail, models.SKUAgentspaceBusiness, models.LocationGlobal, userBestEntitlement) + + require.NoError(t, err) + idp.AssertNumberOfCalls(t, "ListMembers", models.MaxPagesPerGroup) + gemini.AssertNotCalled(t, "BatchUpdateUserLicenses") +} diff --git a/search/gemini-enterprise/group-licensing/internal/services/mocks_test.go b/search/gemini-enterprise/group-licensing/internal/services/mocks_test.go new file mode 100644 index 0000000..e9871ce --- /dev/null +++ b/search/gemini-enterprise/group-licensing/internal/services/mocks_test.go @@ -0,0 +1,88 @@ +/* +Copyright © 2026 Google LLC + +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 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package services + +import ( + "context" + + "github.com/stretchr/testify/mock" + + "github.com/cloud-gtm/gemini-box-office/internal/models" +) + +// MockResourceManagerClient is a testify/mock implementation of ports.ResourceManagerClient. +type MockResourceManagerClient struct { + mock.Mock +} + +// ResolveProjectNumber satisfies ports.ResourceManagerClient. +func (m *MockResourceManagerClient) ResolveProjectNumber(ctx context.Context, projectID string) (string, error) { + args := m.Called(ctx, projectID) + return args.String(0), args.Error(1) +} + +// MockIdpClient is a testify/mock implementation of ports.IdpClient. +// It is shared across all service test files in this package. +type MockIdpClient struct { + mock.Mock +} + +// ListMembers satisfies ports.IdpClient. +func (m *MockIdpClient) ListMembers(ctx context.Context, groupEmail, pageToken string) ([]models.Member, string, error) { + args := m.Called(ctx, groupEmail, pageToken) + members, _ := args.Get(0).([]models.Member) + return members, args.String(1), args.Error(2) +} + +// HasMember satisfies ports.IdpClient. +func (m *MockIdpClient) HasMember(ctx context.Context, groupEmail, userEmail string) (bool, error) { + args := m.Called(ctx, groupEmail, userEmail) + return args.Bool(0), args.Error(1) +} + +// MockGeminiClient is a testify/mock implementation of ports.GeminiClient. +// It is shared across all service test files in this package. +type MockGeminiClient struct { + mock.Mock +} + +// FetchLicenseConfigIndex satisfies ports.GeminiClient. +func (m *MockGeminiClient) FetchLicenseConfigIndex(ctx context.Context, billingAccountID string) (models.LicenseConfigIndex, error) { + args := m.Called(ctx, billingAccountID) + index, _ := args.Get(0).(models.LicenseConfigIndex) + return index, args.Error(1) +} + +// ListUserLicenses satisfies ports.GeminiClient. +func (m *MockGeminiClient) ListUserLicenses(ctx context.Context, projectID, pageToken string) ([]models.UserLicense, string, error) { + args := m.Called(ctx, projectID, pageToken) + licenses, _ := args.Get(0).([]models.UserLicense) + return licenses, args.String(1), args.Error(2) +} + +// BatchUpdateUserLicenses satisfies ports.GeminiClient. +func (m *MockGeminiClient) BatchUpdateUserLicenses(ctx context.Context, projectID string, updates []models.LicenseUpdate) error { + args := m.Called(ctx, projectID, updates) + return args.Error(0) +} + +// FetchLicenseUsageStats satisfies ports.GeminiClient. +func (m *MockGeminiClient) FetchLicenseUsageStats(ctx context.Context, projectID string) (map[string]int64, error) { + args := m.Called(ctx, projectID) + stats, _ := args.Get(0).(map[string]int64) + return stats, args.Error(1) +} diff --git a/search/gemini-enterprise/intro_gemini_enterprise.ipynb b/search/gemini-enterprise/intro_gemini_enterprise.ipynb new file mode 100644 index 0000000..31e1f45 --- /dev/null +++ b/search/gemini-enterprise/intro_gemini_enterprise.ipynb @@ -0,0 +1,621 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "ur8xi4C7S06n" + }, + "outputs": [], + "source": [ + "# Copyright 2025 Google LLC\n", + "#\n", + "# Licensed under the Apache License, Version 2.0 (the \"License\");\n", + "# you may not use this file except in compliance with the License.\n", + "# You may obtain a copy of the License at\n", + "#\n", + "# https://www.apache.org/licenses/LICENSE-2.0\n", + "#\n", + "# Unless required by applicable law or agreed to in writing, software\n", + "# distributed under the License is distributed on an \"AS IS\" BASIS,\n", + "# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n", + "# See the License for the specific language governing permissions and\n", + "# limitations under the License." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "TykEnWYpmYgd" + }, + "source": [ + "# Intro to Gemini Enterprise\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + "
    \n", + " \n", + " \"Google
    Open in Colab\n", + "
    \n", + "
    \n", + " \n", + " \"Google
    Open in Colab Enterprise\n", + "
    \n", + "
    \n", + " \n", + " \"Workbench
    Open in Workbench\n", + "
    \n", + "
    \n", + " \n", + " \"GitHub
    View on GitHub\n", + "
    \n", + "
    \n", + "\n", + "
    \n", + "\n", + "

    \n", + "Share to:\n", + "\n", + "\n", + " \"LinkedIn\n", + "\n", + "\n", + "\n", + " \"Bluesky\n", + "\n", + "\n", + "\n", + " \"X\n", + "\n", + "\n", + "\n", + " \"Reddit\n", + "\n", + "\n", + "\n", + " \"Facebook\n", + "\n", + "

    " + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "q9SSXN56moZJ" + }, + "source": [ + "| | |\n", + "|-|-|\n", + "| Author(s) | [Dave Wang](https://github.com/wadave/), [Holt Skinner](https://github.com/holtskinner/) |" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "JAPoU8Sm5E6e" + }, + "source": [ + "## Overview\n", + "\n", + "[Google Gemini Enterprise](https://cloud.google.com/products/gemini-enterprise) is a powerful AI-powered search and productivity tool designed for businesses. It acts as a central hub for finding information and getting things done within an organization. It unlocks enterprise expertise for employees with agents that bring together Gemini advanced reasoning, Google-quality search, and enterprise data, regardless of where it is hosted.\n", + "\n", + "This notebook demonstrates how to use the Gemini Enterprise Client libraries to access the search engine and make `search` and `answer` queries.\n", + "\n", + "**Reference:**\n", + "\n", + "- https://cloud.google.com/gemini/enterprise/docs/libraries#client-libraries-usage-python\n", + "- https://cloud.google.com/gemini/enterprise/docs/reference/rest" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "i7EUnXsZhAGF" + }, + "source": [ + "### Install client library\n", + "\n", + "Install the following packages required to execute this notebook." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "2b4ef9b72d43" + }, + "outputs": [], + "source": [ + "%pip install --upgrade --quiet google-cloud-discoveryengine" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "f42d12d15616" + }, + "source": [ + "### Restart runtime\n", + "\n", + "To use the newly installed packages in this Jupyter runtime, you must restart the runtime. You can do this by running the cell below, which restarts the current kernel.\n", + "\n", + "The restart might take a minute or longer. After it's restarted, continue to the next step." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "f3d98395d9a4" + }, + "outputs": [], + "source": [ + "import IPython\n", + "\n", + "app = IPython.Application.instance()\n", + "app.kernel.do_shutdown(True)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "89cf56cd5a4d" + }, + "source": [ + "
    \n", + "⚠ The kernel is going to restart. Wait until it's finished before continuing to the next step. ⚠\n", + "
    " + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "2FyOKZ4aIW5h" + }, + "source": [ + "### Authenticate your notebook environment (Colab only)\n", + "\n", + "If you're running this notebook on Google Colab, run the cell below to authenticate your environment." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": { + "id": "XXpf9rYGIb60" + }, + "outputs": [], + "source": [ + "import sys\n", + "\n", + "if \"google.colab\" in sys.modules:\n", + " from google.colab import auth\n", + "\n", + " auth.authenticate_user()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "WReHDGG5g0XY" + }, + "source": [ + "### Set Google Cloud project information\n", + "\n", + "Learn more about [setting up a project and a development environment](https://cloud.google.com/vertex-ai/docs/start/cloud-environment).\n", + "\n", + "**If you don't know your project ID**, try the following:\n", + "\n", + "* Run `gcloud config list`.\n", + "* Run `gcloud projects list`.\n", + "* See the support page: [Locate the project ID](https://support.google.com/googleapi/answer/7014113)" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": { + "id": "oM1iC_MfAts1" + }, + "outputs": [], + "source": [ + "# Use the environment variable if the user doesn't provide Project ID.\n", + "import os\n", + "\n", + "LOCATION = \"global\" # @param {type: \"string\"}\n", + "ENGINE_ID = \"[your-Gemini Enterprise-app-id]\" # This is from your pre-created Gemini Enterprise app.\n", + "\n", + "# fmt: off\n", + "PROJECT_ID = \"[your-project-id]\" # @param {type: \"string\", placeholder: \"[your-project-id]\", isTemplate: true}\n", + "# fmt: on\n", + "if not PROJECT_ID or PROJECT_ID == \"[your-project-id]\":\n", + " PROJECT_ID = str(os.environ.get(\"GOOGLE_CLOUD_PROJECT\"))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "3ec91fa477b9" + }, + "source": [ + "### Prerequisites\n", + "\n", + "- You need to follow [this quickstart guide](https://cloud.google.com/gemini/enterprise/docs/quickstart-gemini-enterprise) to set up datastore and engine first.\n", + "- Make a note of your Gemini Enterprise engine id (app id)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "ygUXErJxaSVj" + }, + "source": [ + "### Import libraries" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": { + "id": "63yT2Wg7aWiP" + }, + "outputs": [], + "source": [ + "from google.api_core.client_options import ClientOptions\n", + "from google.cloud import discoveryengine_v1 as discoveryengine\n", + "from google.protobuf.json_format import MessageToDict" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "YjgeLM3KIBEv" + }, + "source": [ + "## Search method\n", + "\n", + "Use the [`search()`](https://cloud.google.com/gemini/enterprise/docs/preview-search-results) method to retrieve all search results for a given query." + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": { + "id": "T_EQbaViIBEv" + }, + "outputs": [], + "source": [ + "def get_search_sdk_response(\n", + " project_id: str,\n", + " location: str,\n", + " engine_id: str,\n", + " search_query: str,\n", + " prompt_preamble: str | None = None,\n", + ") -> list[discoveryengine.SearchResponse]:\n", + " \"\"\"Retrieves search results from using the Discovery Engine Python SDK.\n", + "\n", + " Args:\n", + " project_id: The Google Cloud project ID.\n", + " location: The location of the Discovery Engine (e.g., \"global\", \"us-central1\").\n", + " engine_id: The ID of the Discovery Engine.\n", + " search_query: The search query string.\n", + " prompt_preamble: The preamble to use for search summaries.\n", + "\n", + " Returns:\n", + " A list of `discoveryengine.SearchResponse` objects containing the search results.\n", + " This list may be empty if no results are found.\n", + "\n", + " \"\"\"\n", + " # For more information, refer to:\n", + " # https://cloud.google.com/generative-ai-app-builder/docs/locations#specify_a_multi-region_for_your_data_store\n", + " client_options = (\n", + " ClientOptions(api_endpoint=f\"{location}-discoveryengine.googleapis.com\")\n", + " if location != \"global\"\n", + " else None\n", + " )\n", + "\n", + " # Create a client\n", + " client = discoveryengine.SearchServiceClient(client_options=client_options)\n", + "\n", + " # The full resource name of the search app serving config\n", + " serving_config = f\"projects/{project_id}/locations/{location}/collections/default_collection/engines/{engine_id}/servingConfigs/default_config\"\n", + "\n", + " # Optional - only supported for unstructured data: Configuration options for search.\n", + " # Refer to the `ContentSearchSpec` reference for all supported fields:\n", + " # https://cloud.google.com/python/docs/reference/discoveryengine/latest/google.cloud.discoveryengine_v1.types.SearchRequest.ContentSearchSpec\n", + " content_search_spec = discoveryengine.SearchRequest.ContentSearchSpec(\n", + " # For information about snippets, refer to:\n", + " # https://cloud.google.com/generative-ai-app-builder/docs/snippets\n", + " snippet_spec=discoveryengine.SearchRequest.ContentSearchSpec.SnippetSpec(\n", + " return_snippet=True\n", + " ),\n", + " # For information about search summaries, refer to:\n", + " # https://cloud.google.com/generative-ai-app-builder/docs/get-search-summaries\n", + " summary_spec=discoveryengine.SearchRequest.ContentSearchSpec.SummarySpec(\n", + " summary_result_count=5,\n", + " include_citations=True,\n", + " ignore_adversarial_query=True,\n", + " ignore_non_summary_seeking_query=True,\n", + " model_prompt_spec=discoveryengine.SearchRequest.ContentSearchSpec.SummarySpec.ModelPromptSpec(\n", + " preamble=prompt_preamble\n", + " ),\n", + " model_spec=discoveryengine.SearchRequest.ContentSearchSpec.SummarySpec.ModelSpec(\n", + " version=\"preview\",\n", + " ),\n", + " ),\n", + " # -------extractive content-----\n", + " extractive_content_spec=discoveryengine.SearchRequest.ContentSearchSpec.ExtractiveContentSpec(\n", + " max_extractive_segment_count=2,\n", + " max_extractive_answer_count=2,\n", + " return_extractive_segment_score=True,\n", + " ),\n", + " )\n", + "\n", + " # Refer to the `SearchRequest` reference for all supported fields:\n", + " # https://cloud.google.com/python/docs/reference/discoveryengine/latest/google.cloud.discoveryengine_v1.types.SearchRequest\n", + " request = discoveryengine.SearchRequest(\n", + " serving_config=serving_config,\n", + " query=search_query,\n", + " page_size=10,\n", + " content_search_spec=content_search_spec,\n", + " query_expansion_spec=discoveryengine.SearchRequest.QueryExpansionSpec(\n", + " condition=discoveryengine.SearchRequest.QueryExpansionSpec.Condition.AUTO,\n", + " ),\n", + " spell_correction_spec=discoveryengine.SearchRequest.SpellCorrectionSpec(\n", + " mode=discoveryengine.SearchRequest.SpellCorrectionSpec.Mode.AUTO\n", + " ),\n", + " )\n", + "\n", + " response = client.search(request)\n", + " return response" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": { + "id": "9bxnz1ZnIBEv" + }, + "outputs": [], + "source": [ + "search_query = \"Who founded Cymbal Bank?\"\n", + "\n", + "response = get_search_sdk_response(\n", + " PROJECT_ID,\n", + " LOCATION,\n", + " ENGINE_ID,\n", + " search_query,\n", + ")\n", + "\n", + "print(response.summary)" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": { + "id": "0osSLrWOIBEv" + }, + "outputs": [], + "source": [ + "document_dict = MessageToDict(\n", + " response.results[0].document._pb, preserving_proto_field_name=True\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": { + "id": "M5Ad5MORIBEv" + }, + "outputs": [], + "source": [ + "document_dict[\"derived_struct_data\"][\"extractive_answers\"][0][\"content\"]" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "Fzxr_0yhIBEv" + }, + "source": [ + "## Answer method\n", + "\n", + "Use the [`answer()`](https://cloud.google.com/gemini/enterprise/docs/answer) method to provide a conversational experience for search results.\n", + "\n", + "Key features of the answer method are as follows:\n", + "\n", + " - The ability to generate answers to complex queries. For example, the answer method can break down compound queries, such as the following, into multiple, smaller queries to return better results that are used to power better answers:\n", + " - \"What is Google Cloud and Google Ads respective revenue in 2024?\"\n", + " - \"After how many years since its founding, did Google reach 1 billion USD revenue?\"\n", + " - The ability to combine search and answer generation in a multi-turn conversation by calling the answer method in each turn.\n", + " - The ability to pair with the search method to reduce search latency. You can call the search method and the answer method separately and render the search results and answers in different iframes at different times. This means that you can show your users search results (the 10 blue links) within milliseconds. You don't need to wait for answers to be generated before you can show search results." + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "metadata": { + "id": "qzRQ8MYRIBEz" + }, + "outputs": [], + "source": [ + "def get_answer_sdk_response(\n", + " project_id: str,\n", + " location: str,\n", + " engine_id: str,\n", + " query: str,\n", + ") -> discoveryengine.AnswerQueryResponse:\n", + " \"\"\"Retrieves an answer from the Discovery Engine using the AnswerQuery API.\n", + "\n", + " This function queries the specified Discovery Engine with the provided query\n", + " and configuration options using the `AnswerQuery` API. It supports\n", + " specifying a location-specific endpoint for the API.\n", + "\n", + " Args:\n", + " project_id: The Google Cloud project ID.\n", + " location: The location of the Discovery Engine (e.g., \"global\", \"us-central1\").\n", + " engine_id: The ID of the Discovery Engine.\n", + " query: The query string.\n", + "\n", + " Returns:\n", + " A `discoveryengine.AnswerQueryResponse` object containing the answer,\n", + " citations, and other related information.\n", + "\n", + " # For more information, refer to:\n", + " # https://cloud.google.com/generative-ai-app-builder/docs/locations#specify_a_multi-region_for_your_data_store\n", + " \"\"\"\n", + " client_options = (\n", + " ClientOptions(api_endpoint=f\"{location}-discoveryengine.googleapis.com\")\n", + " if location != \"global\"\n", + " else None\n", + " )\n", + "\n", + " # Create a client\n", + " client = discoveryengine.ConversationalSearchServiceClient(\n", + " client_options=client_options\n", + " )\n", + "\n", + " # The full resource name of the Search serving config\n", + " serving_config = f\"projects/{project_id}/locations/{location}/collections/default_collection/engines/{engine_id}/servingConfigs/default_serving_config\"\n", + "\n", + " # Optional: Options for query phase\n", + " # The `query_understanding_spec` below includes all available query phase options.\n", + " # For more details, refer to https://cloud.google.com/generative-ai-app-builder/docs/reference/rest/v1/QueryUnderstandingSpec\n", + " query_understanding_spec = discoveryengine.AnswerQueryRequest.QueryUnderstandingSpec(\n", + " query_rephraser_spec=discoveryengine.AnswerQueryRequest.QueryUnderstandingSpec.QueryRephraserSpec(\n", + " disable=False, # Optional: Disable query rephraser\n", + " max_rephrase_steps=1, # Optional: Number of rephrase steps\n", + " ),\n", + " # Optional: Classify query types\n", + " query_classification_spec=discoveryengine.AnswerQueryRequest.QueryUnderstandingSpec.QueryClassificationSpec(\n", + " types=[\n", + " discoveryengine.AnswerQueryRequest.QueryUnderstandingSpec.QueryClassificationSpec.Type.ADVERSARIAL_QUERY,\n", + " discoveryengine.AnswerQueryRequest.QueryUnderstandingSpec.QueryClassificationSpec.Type.NON_ANSWER_SEEKING_QUERY,\n", + " ] # Options: ADVERSARIAL_QUERY, NON_ANSWER_SEEKING_QUERY or both\n", + " ),\n", + " )\n", + "\n", + " # Optional: Options for answer phase\n", + " # The `answer_generation_spec` below includes all available query phase options.\n", + " # For more details, refer to https://cloud.google.com/generative-ai-app-builder/docs/reference/rest/v1/AnswerGenerationSpec\n", + " answer_generation_spec = discoveryengine.AnswerQueryRequest.AnswerGenerationSpec(\n", + " ignore_adversarial_query=False, # Optional: Ignore adversarial query\n", + " ignore_non_answer_seeking_query=False, # Optional: Ignore non-answer seeking query\n", + " ignore_low_relevant_content=False, # Optional: Return fallback answer when content is not relevant\n", + " model_spec=discoveryengine.AnswerQueryRequest.AnswerGenerationSpec.ModelSpec(\n", + " model_version=\"gemini-2.0-flash/answer_gen/v2\", # Optional: Model to use for answer generation\n", + " ),\n", + " prompt_spec=discoveryengine.AnswerQueryRequest.AnswerGenerationSpec.PromptSpec(\n", + " preamble=\"Give a detailed answer.\", # Optional: Natural language instructions for customizing the answer.\n", + " ),\n", + " include_citations=True, # Optional: Include citations in the response\n", + " answer_language_code=\"en\", # Optional: Language code of the answer\n", + " )\n", + "\n", + " # Initialize request argument(s)\n", + " request = discoveryengine.AnswerQueryRequest(\n", + " serving_config=serving_config,\n", + " query=discoveryengine.Query(text=query),\n", + " session=None, # Optional: include previous session ID to continue a conversation\n", + " query_understanding_spec=query_understanding_spec,\n", + " answer_generation_spec=answer_generation_spec,\n", + " )\n", + "\n", + " # Make the request\n", + " response = client.answer_query(request)\n", + "\n", + " return response" + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "metadata": { + "id": "_zwOQEqJIBE0" + }, + "outputs": [], + "source": [ + "query = \"Who founded Cymbal Bank?\"\n", + "response = get_answer_sdk_response(\n", + " PROJECT_ID,\n", + " LOCATION,\n", + " ENGINE_ID,\n", + " query,\n", + ")\n", + "\n", + "print(response.answer.answer_text)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "TpV-iwP9qw9c" + }, + "source": [ + "## Cleaning up\n" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": { + "id": "7f14b3b54cb0" + }, + "outputs": [], + "source": [ + "# Delete search engine and datastore\n", + "\n", + "\n", + "def delete_search_engine(\n", + " project_id: str, location: str, data_store_id: str, search_engine_id: str\n", + "):\n", + " \"\"\"Deletes a Discovery Engine Search Engine.\n", + "\n", + " Args:\n", + " project_id: The Google Cloud project ID.\n", + " location: The location of the Data Store (e.g., \"global\").\n", + " data_store_id: The ID of the Data Store containing the Search Engine.\n", + " search_engine_id: The ID of the Search Engine to delete.\n", + " \"\"\"\n", + " client = discoveryengine.SearchEngineServiceClient()\n", + "\n", + " # Construct the Search Engine name.\n", + " search_engine_name = client.search_engine_path(\n", + " project=project_id,\n", + " location=location,\n", + " data_store=data_store_id,\n", + " search_engine=search_engine_id,\n", + " )\n", + "\n", + " try:\n", + " # Delete the Search Engine.\n", + " client.delete_search_engine(name=search_engine_name)\n", + " print(f\"Search Engine '{search_engine_id}' deleted successfully.\")\n", + "\n", + " except Exception as e:\n", + " print(f\"Error deleting Search Engine: {e}\")\n", + "\n", + "\n", + "data_store_id = \"[your-data-store-id]\" # Replace with your Data Store ID\n", + "delete_search_engine(PROJECT_ID, LOCATION, data_store_id, ENGINE_ID)" + ] + } + ], + "metadata": { + "colab": { + "name": "intro_gemini_enterprise.ipynb", + "toc_visible": true + }, + "kernelspec": { + "display_name": "Python 3", + "name": "python3" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} diff --git a/search/gemini-enterprise/mcp_with_gemini_enterprise.ipynb b/search/gemini-enterprise/mcp_with_gemini_enterprise.ipynb new file mode 100644 index 0000000..d75586c --- /dev/null +++ b/search/gemini-enterprise/mcp_with_gemini_enterprise.ipynb @@ -0,0 +1,918 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "1Vs2C5ZBkrjmrHTfAyz77hpV" + }, + "outputs": [], + "source": [ + "# Copyright 2025 Google LLC\n", + "#\n", + "# Licensed under the Apache License, Version 2.0 (the \"License\");\n", + "# you may not use this file except in compliance with the License.\n", + "# You may obtain a copy of the License at\n", + "#\n", + "# https://www.apache.org/licenses/LICENSE-2.0\n", + "#\n", + "# Unless required by applicable law or agreed to in writing, software\n", + "# distributed under the License is distributed on an \"AS IS\" BASIS,\n", + "# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n", + "# See the License for the specific language governing permissions and\n", + "# limitations under the License." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "VoJZgH6X77se" + }, + "source": [ + "# MCP Server with Gemini Enterprise" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "qfDq3m0R8Fe1" + }, + "source": [ + "\n", + " \n", + " \n", + " \n", + " \n", + "
    \n", + " \n", + " \"Google
    Open in Colab\n", + "
    \n", + "
    \n", + " \n", + " \"Google
    Open in Colab Enterprise\n", + "
    \n", + "
    \n", + " \n", + " \"Vertex
    Open in Vertex AI Workbench\n", + "
    \n", + "
    \n", + " \n", + " \"GitHub
    View on GitHub\n", + "
    \n", + "
    \n", + "\n", + "\n", + "\n", + "
    \n", + "
    \n", + "
    \n", + "\n", + "
    \n", + "\n", + "Share to:\n", + "\n", + "\n", + " \"LinkedIn\n", + "\n", + "\n", + "\n", + " \"Bluesky\n", + "\n", + "\n", + "\n", + " \"X\n", + "\n", + "\n", + "\n", + " \"Reddit\n", + "\n", + "\n", + "\n", + " \"Facebook\n", + "" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "UD1Cy9O88Zn9" + }, + "source": [ + "| Authors |\n", + "| --- |\n", + "| Parag Mhatre |\n", + "| Sharmila Devi |\n", + "| Rupjit Chakraborty|" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "28b524cdd168" + }, + "source": [ + "## Overview : MCP Server with Gemini Enterprise\n", + "\n", + "This notebook demonstrates a complete end-to-end integration of a business application with Gemini Enterprise, leveraging a Model Context Protocol (MCP) server built with FastMCP and an agent developed using the Agent Development Kit (ADK). This guide is designed to assist developers in understanding and implementing similar solutions for their upcoming projects.\n", + "\n", + "### Background\n", + "The increasing demand for integrating business applications with intelligent agents highlights the need for robust and scalable solutions. This notebook addresses this by providing a practical demonstration of how an MCP server can be seamlessly integrated with Gemini Enterprise using the ADK.\n", + "\n", + "### Business Application Scenario\n", + "For this demonstration, we use a common business application: employee leave application. The application requires employees to provide their employee ID, start date, and end date for leave requests.\n", + "\n", + "### Notebook Overview\n", + "This notebook guides you through the following key steps:\n", + "\n", + "#### 1. BigQuery Setup\n", + "\n", + "> The process begins with setting up the necessary data infrastructure. You'll learn how to:
    \n", + "Create a BigQuery dataset.
    \n", + "Create a BigQuery table to store employee leave application data.\n", + "\n", + "#### 2. MCP Server Deployment to Cloud Run\n", + "> This section focuses on deploying the MCP server, which acts as the bridge between your business application and the agents.
    \n", + "The current MCP server is built using the FastMCP framework, providing a robust RESTful API for communication.
    \n", + "You'll be guided through the process of building a container image for the MCP server.
    \n", + "Instructions for deploying this container to Cloud Run will be provided, ensuring a scalable and serverless deployment.\n", + "\n", + "#### 3. ADK Configuration and MCP Server Communication\n", + "> Once the MCP server is deployed, the notebook demonstrates how to configure the ADK to interact with it.
    \n", + "Learn how to set up your ADK environment.
    \n", + "Understand how to use MCP Tools to facilitate communication between your ADK-based agent and the deployed MCP server.
    \n", + "This section includes steps for testing the integration to ensure smooth data exchange.\n", + "\n", + "#### 4. Agent Deployment to Agent Engine\n", + "> After successful testing, the notebook walks you through deploying your ADK-built agent.
    \n", + "Detailed steps are provided for deploying the agent to the Agent Engine.\n", + "\n", + "#### 5. Agent Engine Registration with Gemini Enterprise\n", + "> The final step involves integrating the deployed agent with the broader Gemini Enterprise ecosystem.\n", + "\n", + "Learn how to register the Agent Engine with Gemini Enterprise using REST APIs, making your agent discoverable and accessible within the Gemini Enterprise environment.\n", + "\n", + "\n", + "**Open Issue:**\n", + "> 1. ADK currently utilizes the MCP Tool to interface with the MCP server, which is deployed on Cloud Run. Authorization headers for Cloud Run expire after one hour. Efforts are underway to integrate the Agent Engine service account for interaction with Cloud Run." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "k0nKem_m_tOt" + }, + "source": [ + "### MCP Server for Human Resources Management Application" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "5bb4021007d1" + }, + "outputs": [], + "source": [ + "# TODO for Developer: Update project id.\n", + "PROJECT_NUMBER = \"[your-project-number]\"\n", + "\n", + "# TODO for Developer: Update project name.\n", + "PROJECT_ID = \"[your-project-id]\"\n", + "\n", + "DATASET_ID = \"bqds_hr_data\"\n", + "TABLE_NAME = \"employee_leave\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "bc0a45394998" + }, + "outputs": [], + "source": [ + "# Setup Google Cloud Product project.\n", + "!gcloud config set project {PROJECT_ID}\n", + "!gcloud config get-value project\n", + "\n", + "# Enable required services.\n", + "!gcloud services enable iam.googleapis.com" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "K3QuYpHs_wR0" + }, + "source": [ + "#### Step 1: BigQuery Setup" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "a587b1e6b850" + }, + "outputs": [], + "source": [ + "# Create required BigQuery dataset.\n", + "!bq mk --dataset --PROJECT_NUMBER {PROJECT_NUMBER} {DATASET_ID}" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "a4f38854a72f" + }, + "outputs": [], + "source": [ + "# Create required BigQuery table.\n", + "!bq mk \\\n", + " --table \\\n", + " {PROJECT_NUMBER}:{DATASET_ID}.{TABLE_NAME} \\\n", + " employee_id:INTEGER,leave_start_date:DATE,leave_end_date:DATE" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "af2224878401" + }, + "outputs": [], + "source": [ + "# Setting up environment variable.\n", + "%env PROJECT_ID={PROJECT_ID}\n", + "%env DATASET_ID={DATASET_ID}\n", + "%env TABLE_NAME={TABLE_NAME}" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "0d5680e059dd" + }, + "source": [ + "#### Step 2: MCP Server Deployment to Cloud Run" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "79ae8974bb0b" + }, + "source": [ + "##### Step 2.1 : Build MCP Server container image" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "9b93783e1936" + }, + "outputs": [], + "source": [ + "%%writefile mcp_server.py\n", + "import os\n", + "import json\n", + "import uvicorn\n", + "\n", + "from requests.exceptions import RequestException\n", + "from starlette.applications import Starlette\n", + "from starlette.requests import Request\n", + "from starlette.routing import Route, Mount\n", + "\n", + "from mcp.server.fastmcp import FastMCP\n", + "from mcp.shared.exceptions import McpError\n", + "from mcp.types import ErrorData, INTERNAL_ERROR, INVALID_PARAMS\n", + "from mcp.server.sse import SseServerTransport\n", + "from google.cloud import bigquery\n", + "\n", + "# Create an MCP server instance with an identifier (\"HR Tool\")\n", + "mcp = FastMCP(\"HR Tool\")\n", + "APP_HOST = os.environ.get(\"APP_HOST\", \"0.0.0.0\")\n", + "APP_PORT = os.environ.get(\"APP_PORT\", 8080)\n", + "\n", + "# Retrieve environment variables.\n", + "PROJECT_NUMBER = os.environ.get(\"PROJECT_ID\")\n", + "dataset_id = os.environ.get(\"DATASET_ID\")\n", + "table_name = os.environ.get(\"TABLE_NAME\")\n", + "table_id = f\"{PROJECT_NUMBER}.{dataset_id}.{table_name}\"\n", + "\n", + "# Setup BigQuery client.\n", + "client = bigquery.Client(project=PROJECT_NUMBER)\n", + "\n", + "\n", + "@mcp.tool()\n", + "def apply_leave(\n", + " employee_id: int, start_date: str, end_date: str\n", + ") -> bigquery.job.LoadJob:\n", + " \"\"\"\n", + " Inserts data into a BigQuery table.\n", + "\n", + " Args:\n", + " employee_id: Employee identification int. should be in string format.\n", + " start_date: Start date of a leave. Dates should be in 'YYYY-MM-DD' string format.\n", + " end_date: End date of a leave. Dates should be in 'YYYY-MM-DD' string format.\n", + "\n", + " Returns:\n", + " A BigQuery LoadJob object if the insert operation is successful.\n", + " \"\"\"\n", + "\n", + " # Prepare row to be inserted.\n", + " data_to_insert = [\n", + " {\n", + " \"employee_id\": employee_id,\n", + " \"leave_start_date\": start_date,\n", + " \"leave_end_date\": end_date,\n", + " },\n", + " ]\n", + "\n", + " # insert row to BigQuery.\n", + " errors = client.insert_rows_json(table_id, data_to_insert)\n", + "\n", + " if errors:\n", + " print(f\"Encountered errors while inserting rows: {errors}\")\n", + " raise Exception(f\"Failed to insert rows: {errors}\")\n", + " else:\n", + " print(f\"Successfully inserted {len(data_to_insert)} rows into {table_id}\")\n", + " return \"Leave applied successfuly. for \" + json.dumps(data_to_insert)\n", + "\n", + "\n", + "sse = SseServerTransport(\"/messages/\")\n", + "\n", + "\n", + "async def handle_sse(request: Request) -> None:\n", + " _server = mcp._mcp_server\n", + " async with sse.connect_sse(\n", + " request.scope,\n", + " request.receive,\n", + " request._send,\n", + " ) as (reader, writer):\n", + " await _server.run(reader, writer, _server.create_initialization_options())\n", + "\n", + "\n", + "app = Starlette(\n", + " debug=True,\n", + " routes=[\n", + " Route(\"/sse\", endpoint=handle_sse),\n", + " Mount(\"/messages/\", app=sse.handle_post_message),\n", + " ],\n", + ")\n", + "\n", + "if __name__ == \"__main__\":\n", + " uvicorn.run(app, host=APP_HOST, port=APP_PORT)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "68ed7244d84a" + }, + "outputs": [], + "source": [ + "%%writefile Dockerfile\n", + "# Use an official Python runtime as a parent image\n", + "FROM python:3.10-slim\n", + "\n", + "# Set the working directory in the container\n", + "WORKDIR /app\n", + "\n", + "# --- Dependency Installation ---\n", + "COPY requirements.txt /app/requirements.txt\n", + "RUN pip install --no-cache-dir -r requirements.txt\n", + "\n", + "# --- Application Code ---\n", + "COPY . /app\n", + "\n", + "# --- Environment ---\n", + "ENV PYTHONPATH = /app\n", + "\n", + "# Make port 8080 available to the world outside this container\n", + "# Cloud Run uses the PORT env var, but EXPOSE is good practice.\n", + "EXPOSE 8080\n", + "\n", + "# --- Run the application ---\n", + "CMD [\"python\", \"mcp_server.py\"]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "2c3d35247c0b" + }, + "outputs": [], + "source": [ + "%%writefile requirements.txt\n", + "google-adk == 0.3.0\n", + "mcp[cli] == 1.5.0\n", + "requests == 2.32.3\n", + "google-cloud-bigquery ==3.35.0" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "0a0ec35d06e4" + }, + "source": [ + "##### Step 2.1: Deploy MCP Server to Cloud Run" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "d370f0eb341e" + }, + "outputs": [], + "source": [ + "# Setup Image repository URL\n", + "IMAGE_TAG = \"latest\"\n", + "MCP_IMAGE_NAME = \"hr-tool-server\"\n", + "REPO_NAME = \"mcp-server\"\n", + "REGION = \"us-central1\"\n", + "IMAGE_PATH = (\n", + " f\"{REGION}-docker.pkg.dev/{PROJECT_ID}/{REPO_NAME}/{MCP_IMAGE_NAME}:{IMAGE_TAG}\"\n", + ")\n", + "SERVICE_NAME = \"hr-tool-server\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "bbc9198085e4" + }, + "outputs": [], + "source": [ + "# Create repository\n", + "!gcloud artifacts repositories create {REPO_NAME} \\\n", + " --repository-format=docker \\\n", + " --location={REGION} \\\n", + " --description=\"Repository for my application's Docker images\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "153e3ac7f8d0" + }, + "outputs": [], + "source": [ + "# Build image and push to artifact registry.\n", + "!gcloud builds submit . \\\n", + " --tag={IMAGE_PATH} \\\n", + " --project={PROJECT_ID}" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "cba3beca8369" + }, + "outputs": [], + "source": [ + "# Deploy MCP Server to cloud run.\n", + "cmd = f\"\"\"gcloud run deploy {SERVICE_NAME} \\\n", + " --image={IMAGE_PATH} \\\n", + " --platform=managed \\\n", + " --region={REGION} \\\n", + " --no-allow-unauthenticated \\\n", + " --set-env-vars=\"APP_HOST=0.0.0.0\" \\\n", + " --set-env-vars=\"APP_PORT=8080\" \\\n", + " --set-env-vars=\"GOOGLE_GENAI_USE_VERTEXAI=TRUE\" \\\n", + " --set-env-vars=\"GOOGLE_CLOUD_LOCATION={REGION}\" \\\n", + " --set-env-vars=\"GOOGLE_CLOUD_PROJECT={PROJECT_NUMBER}\" \\\n", + " --set-env-vars=\"PROJECT_ID={PROJECT_ID}\" \\\n", + " --set-env-vars=\"DATASET_ID={DATASET_ID}\" \\\n", + " --set-env-vars=\"TABLE_NAME={TABLE_NAME}\" \\\n", + " --project={PROJECT_ID} \\\n", + " --min-instances=1\"\"\"\n", + "!{cmd}" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "2e37d2d52406" + }, + "source": [ + "#### Step 3: ADK Configuration and MCP Server Communication" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "dea5bf7484e1" + }, + "outputs": [], + "source": [ + "pip install --quiet google-adk==1.4.2" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": { + "id": "97e86592081a" + }, + "outputs": [], + "source": [ + "import logging\n", + "\n", + "import google.auth.transport.requests\n", + "import google.oauth2.id_token\n", + "from dotenv import load_dotenv\n", + "from google.adk.agents import Agent\n", + "from google.adk.tools.mcp_tool.mcp_session_manager import SseServerParams\n", + "from google.adk.tools.mcp_tool.mcp_toolset import MCPToolset\n", + "from google.genai import types\n", + "from vertexai.preview import reasoning_engines" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": { + "id": "1dbb9681ee2b" + }, + "outputs": [], + "source": [ + "# The URL of the target Cloud Run service\n", + "MCP_SERVER_URL = \"https://hr-tool-server-[your-project-number].us-central1.run.app/sse\" # TODO for Developer : Copy Paste the url you will receive after cloud run deployment.\n", + "\n", + "# Create a Google Auth request object\n", + "auth_req = google.auth.transport.requests.Request()\n", + "\n", + "# Generate the ID token\n", + "id_token = google.oauth2.id_token.fetch_id_token(auth_req, MCP_SERVER_URL)\n", + "\n", + "# Make the request with the ID token\n", + "headers = {\"Authorization\": f\"Bearer {id_token}\"}" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "b69ed8e2715b" + }, + "source": [ + "##### Step 3.1: Register MCP Tool to ADK Agent" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": { + "id": "bc754ccfda73" + }, + "outputs": [], + "source": [ + "agent_prompt = \"\"\"\n", + "You are having tools to apply leave. If your user is looking for applying leave then ask for employee id, start date and end date. once you have all the information then apply leave.\n", + "\"\"\"" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": { + "id": "c5b7fcd20d13" + }, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "INFO:__main__:Initializing agent...\n", + "INFO:__main__:MCP Toolset created successfully.\n", + "INFO:__main__:Agent initialized successfully.\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Attempting to connect to MCP Filesystem server...\n", + "LlmAgent created.\n" + ] + } + ], + "source": [ + "# Load environment variables from .env file in the parent directory\n", + "# Place this near the top, before using env vars like API keys\n", + "load_dotenv()\n", + "\n", + "logging.basicConfig(level=logging.INFO)\n", + "log = logging.getLogger(__name__)\n", + "\n", + "# --- Global variables ---\n", + "# Define them first, initialize as None\n", + "root_agent: Agent | None = None\n", + "\n", + "\n", + "def get_tools_async():\n", + " print(\"Attempting to connect to MCP Filesystem server...\")\n", + " tools = MCPToolset(\n", + " connection_params=SseServerParams(url=MCP_SERVER_URL, headers=headers),\n", + " errlog=None,\n", + " )\n", + " log.info(\"MCP Toolset created successfully.\")\n", + "\n", + " return tools\n", + "\n", + "\n", + "def get_agent_async():\n", + " \"\"\"Asynchronously creates the MCP Toolset and the LlmAgent.\n", + "\n", + " Returns:\n", + " tuple: (Agent instance)\n", + " \"\"\"\n", + " tools = get_tools_async()\n", + "\n", + " root_agent = Agent(\n", + " model=\"gemini-2.5-pro\", # Adjust model name if needed based on availability\n", + " name=\"hr_agent\",\n", + " instruction=agent_prompt,\n", + " tools=[tools],\n", + " )\n", + " print(\"LlmAgent created.\")\n", + "\n", + " # Return both the agent and the exit_stack needed for cleanup\n", + " return root_agent\n", + "\n", + "\n", + "global root_agent\n", + "if root_agent is None:\n", + " log.info(\"Initializing agent...\")\n", + " root_agent = get_agent_async()\n", + " if root_agent:\n", + " log.info(\"Agent initialized successfully.\")\n", + " else:\n", + " log.error(\"Agent initialization failed.\")\n", + "else:\n", + " log.info(\"Agent already initialized.\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "be2c8b2153d3" + }, + "source": [ + "##### 3.2 Evaluate ADK Agent Locally." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "abb9563867ee" + }, + "outputs": [], + "source": [ + "app = reasoning_engines.AdkApp(\n", + " agent=root_agent,\n", + " enable_tracing=True,\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "600350db1ae3" + }, + "outputs": [], + "source": [ + "session = app.create_session(user_id=\"u_1231\")\n", + "session" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "5468bfe24dc3" + }, + "outputs": [], + "source": [ + "query = \"My employee id is 12 and want to apply for a leave from 21 July 2025 to 22 July 2025.\"\n", + "contents = types.Content(role=\"user\", parts=[types.Part.from_text(text=query)])" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "d431f9f81184" + }, + "outputs": [], + "source": [ + "for event in app.stream_query(\n", + " user_id=\"u_1231\",\n", + " session_id=session.id,\n", + " message=contents.model_dump(),\n", + "):\n", + " print(event)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "31bd98e3819f" + }, + "source": [ + "#### Step 4: Agent Deployment to Agent Engine" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "f4b19198f858" + }, + "outputs": [], + "source": [ + "from vertexai.preview.reasoning_engines import AdkApp\n", + "\n", + "app = AdkApp(agent=root_agent)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "308582742532" + }, + "outputs": [], + "source": [ + "pip install --upgrade --quiet google-cloud-aiplatform" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "c066be5562e3" + }, + "outputs": [], + "source": [ + "import vertexai\n", + "\n", + "PROJECT_NUMBER = PROJECT_NUMBER\n", + "LOCATION = \"us-central1\" # TODO for Developer : Update region here.\n", + "STAGING_BUCKET = (\n", + " \"gs://[bucket-name]\" # TODO for Developer : Update GCS bucket name here.\n", + ")\n", + "vertexai.init(project=PROJECT_NUMBER, location=LOCATION, staging_bucket=STAGING_BUCKET)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "7fded9eca51a" + }, + "outputs": [], + "source": [ + "from vertexai import agent_engines\n", + "\n", + "remote_app = agent_engines.create(\n", + " display_name=\"HR Agent V3\",\n", + " # agent_engine=root_agent,\n", + " agent_engine=app,\n", + " requirements=[\n", + " \"google-adk (==1.5.0)\",\n", + " \"google-genai (==1.24.0)\",\n", + " \"pydantic (==2.11.7)\",\n", + " ],\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "7bea20750e56" + }, + "outputs": [], + "source": [ + "remote_app.resource_name" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "9355b5b8f624" + }, + "source": [ + "#### Step 5: Agent Engine Registration with Gemini Enterprise" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "2b5a9e686af1" + }, + "source": [ + "**Create oauth consent and mention the \"clientId\" and \"clientSecret\".**" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "3635a286d182" + }, + "outputs": [], + "source": [ + "%%bash\n", + "\n", + "curl -X POST \\\n", + "-H \"Authorization: Bearer $(gcloud auth print-access-token)\" \\\n", + "-H \"Content-Type: application/json\" \\\n", + "-H \"X-Goog-User-Project: [your-project-id]\" \\\n", + "\"https://discoveryengine.googleapis.com/v1alpha/projects/[your-project-id]/locations/global/authorizations?authorizationId=customhr9893\" \\\n", + "-d '{\n", + "\"name\": \"projects/[your-project-id]/locations/global/authorizations/customhr9893\",\n", + "\"serverSideOauth2\": {\n", + "\"clientId\": \"[UPDATE-CLIENT-ID]\",\n", + "\"clientSecret\": \"[UPDATE-CLIENT-SECRET]\",\n", + "\"authorizationUri\": \"https://accounts.google.com/o/oauth2/v2/auth?client_id=[UPDATE-CLIENT-ID]&scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fcloud-platform&include_granted_scopes=true&response_type=code&access_type=offline&prompt=consent\",\n", + "\"tokenUri\": \"https://oauth2.googleapis.com/token\"\n", + "}\n", + "}'" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "b33096f67166" + }, + "outputs": [], + "source": [ + "%%bash\n", + "\n", + "curl -X POST \\\n", + "-H \"Authorization: Bearer $(gcloud auth print-access-token)\" \\\n", + "-H \"Content-Type: application/json\" \\\n", + "-H \"X-Goog-User-Project: [your-project-id]\" \\\n", + "\"https://discoveryengine.googleapis.com/v1alpha/projects/[your-project-id]/locations/global/collections/default_collection/engines/[your-gemini-enterprise-engine-id]/assistants/default_assistant/agents\" \\\n", + "-d '{\n", + "\"displayName\": \"HR Agent V3\",\n", + "\"description\": \"HR Agent can help employees to apply leave.\",\n", + "\"adk_agent_definition\": {\n", + "\"tool_settings\": {\n", + "\"tool_description\": \"HR Agent can help employees to apply leave.\"\n", + "},\n", + "\"provisioned_reasoning_engine\": {\n", + "\"reasoning_engine\": \"projects/[your-project-number]/locations/us-central1/reasoningEngines/[reasoning-engine-id]\"\n", + "},\n", + "\"authorizations\": [\"projects/[your-project-number]/locations/global/authorizations/customhr9893\"]\n", + "}\n", + "}'" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "69085722b50a" + }, + "source": [ + "#### After agent with MCP server is enabled, you can interact with agent on Gemini Enterprise. " + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "931d26753aca" + }, + "source": [ + "![MCP_server_with_gemini_enterprise](https://services.google.com/fh/files/misc/mcp_with_agentspace_v2.png)" + ] + } + ], + "metadata": { + "colab": { + "name": "mcp_with_gemini_enterprise.ipynb", + "toc_visible": true + }, + "kernelspec": { + "display_name": "Python 3", + "name": "python3" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} diff --git a/search/gemini-enterprise/oss_model_with_gemini_enterprise.ipynb b/search/gemini-enterprise/oss_model_with_gemini_enterprise.ipynb new file mode 100644 index 0000000..cd2a43b --- /dev/null +++ b/search/gemini-enterprise/oss_model_with_gemini_enterprise.ipynb @@ -0,0 +1,598 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "1Vs2C5ZBkrjmrHTfAyz77hpV" + }, + "outputs": [], + "source": [ + "# Copyright 2025 Google LLC\n", + "#\n", + "# Licensed under the Apache License, Version 2.0 (the \"License\");\n", + "# you may not use this file except in compliance with the License.\n", + "# You may obtain a copy of the License at\n", + "#\n", + "# https://www.apache.org/licenses/LICENSE-2.0\n", + "#\n", + "# Unless required by applicable law or agreed to in writing, software\n", + "# distributed under the License is distributed on an \"AS IS\" BASIS,\n", + "# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n", + "# See the License for the specific language governing permissions and\n", + "# limitations under the License." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "VoJZgH6X77se" + }, + "source": [ + "# Open Source Models (Gemma) as a agent with Gemini Enterprise" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "qfDq3m0R8Fe1" + }, + "source": [ + "\n", + " \n", + " \n", + " \n", + " \n", + "
    \n", + " \n", + " \"Google
    Open in Colab\n", + "
    \n", + "
    \n", + " \n", + " \"Google
    Open in Colab Enterprise\n", + "
    \n", + "
    \n", + " \n", + " \"Vertex
    Open in Vertex AI Workbench\n", + "
    \n", + "
    \n", + " \n", + " \"GitHub
    View on GitHub\n", + "
    \n", + "
    \n", + "\n", + "\n", + "\n", + "
    \n", + "
    \n", + "
    \n", + "\n", + "
    \n", + "\n", + "Share to:\n", + "\n", + "\n", + " \"LinkedIn\n", + "\n", + "\n", + "\n", + " \"Bluesky\n", + "\n", + "\n", + "\n", + " \"X\n", + "\n", + "\n", + "\n", + " \"Reddit\n", + "\n", + "\n", + "\n", + " \"Facebook\n", + "\n", + "\n", + "TODO: Update links" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "UD1Cy9O88Zn9" + }, + "source": [ + "| Author |\n", + "| --- |\n", + "| Parag Mhatre |" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "28b524cdd168" + }, + "source": [ + "## Building Agents with Open-Source Models and Gemini Enterprise\n", + "\n", + "This repository contains a comprehensive notebook that provides an end-to-end guide for deploying open-source Large Language Models (LLMs), like Gemma, and building a conversational agent that interacts with the deployed model.\n", + "\n", + "### Overview 📝\n", + "\n", + "This notebook walks you through the entire lifecycle of creating and deploying a custom agent powered by an open-source LLM. The key stages covered are:\n", + "\n", + "1. Model Deployment: Deploying an open-source model (Gemma) to a serving endpoint.\n", + "\n", + "2. Agent Construction: Building an agent that uses the deployed model.\n", + "\n", + "3. Root Agent Configuration: Providing two distinct options for the agent's core logic:\n", + "\n", + " 3.1. Option 1: Using Google Gemini model as the primary \"root agent\" to orchestrate tasks and interact with the specialized Gemma model.\n", + "\n", + " 3.2. Option 2: Using LiteLLM to enable the deployed Gemma model itself to function as the root agent, offering a fully open-source-based solution.\n", + "\n", + "4. Testing & Deployment: Testing the agent locally with the Agent Development Kit (ADK), deploying it to the Agent Engine, and finally registering it in Gemini Enterprise for user access.\n", + "\n", + "### Background 💡\n", + "The demand for specialized, fine-tuned LLMs is rapidly growing. Organizations are increasingly looking to leverage open-source models, training them on proprietary data to create experts in specific domains like customer support, internal documentation, or financial analysis.\n", + "\n", + "However, a key challenge remains: how to make these powerful, fine-tuned models easily accessible to business users? Simply deploying a model isn't enough. You need a robust, interactive, and discoverable interface. This is where agents come in. An agent acts as a smart layer on top of the model, enabling seamless conversation and integration with other tools.\n", + "\n", + "This notebook provides a practical blueprint for bridging that gap—from deploying a custom open-source model to making it available as a fully functional agent in Gemini Enterprise.\n", + "\n", + "### Business Scenarios 🏱\n", + "This solution is ideal for organizations that want to:\n", + "\n", + "1. Create Expert Chatbots: Fine-tune a model like Gemma on an internal knowledge base (e.g., HR policies, technical documentation, product specs). The resulting agent can then provide instant, accurate answers to employee or customer queries.\n", + "\n", + "2. Develop Specialized Assistants: Build an agent trained on financial reports or market data to assist analysts with data retrieval and summarization.\n", + "\n", + "3. Ensure Data Privacy and Control: Use open-source models hosted within their own cloud environment to maintain full control over sensitive data, instead of relying on external model APIs.\n", + "\n", + "4. Reduce Costs: Leverage powerful, cost-effective open-source models as an alternative to proprietary, closed-source options.\n", + "\n", + "### Notebook Steps 🚀\n", + "The notebook is structured into a clear, step-by-step process. Each section contains the necessary code and explanations to guide you through the implementation.\n", + "\n", + "#### Step 1: Setup and Prerequisites\n", + "> This initial step involves importing the required libraries and configuring your project environment variables. It ensures all dependencies are in place before you begin.\n", + "\n", + "#### Step 2: Deploying the Open-Source Model (Gemma)\n", + "> Here, you will take the open-source Gemma model and deploy it to a serving endpoint (e.g., on Vertex AI). This makes the model available to receive requests via an API, a crucial first step for agent integration.\n", + "\n", + "#### Step 3: Configuring the Root Agent\n", + "> This is a critical decision point where you choose the \"brain\" of your agent. The notebook provides two paths:\n", + "\n", + "> **Option A: Using Gemini as the Root Agent**\n", + "\n", + "> In this configuration, the powerful, multi-modal Gemini model acts as the primary orchestrator. It understands the user's intent and can decide when to call the specialized, deployed Gemma model for specific tasks. This is a great hybrid approach that combines the broad capabilities of Gemini with the specialized knowledge of your fine-tuned model.\n", + "\n", + "> **Option B: Using Gemma as the Root Agent via LiteLLM**\n", + "\n", + "> This approach uses LiteLLM, a clever library that creates a standardized interface for interacting with various LLMs. We use it to wrap our deployed Gemma model, allowing it to serve as the root agent itself. This is the perfect choice for creating a solution built entirely on open-source components.\n", + "\n", + "#### Step 4: Building and Testing the Agent with ADK\n", + "> With the model deployed and the root agent configured, you will now formally build the agent. The notebook then guides you through using the Agent Development Kit (ADK) to run the agent locally. This allows for rapid testing and debugging to ensure the agent behaves as expected before a full deployment.\n", + "\n", + "#### Step 5: Deploying the Agent to Agent Engine\n", + "> Once the agent is validated locally, this step shows you how to deploy it to the scalable, managed Agent Engine. This moves your agent from a local test environment to a production-ready platform.\n", + "\n", + "#### Step 6: Registering the Agent in Gemini Enterprise\n", + "> In the final step, you will register your deployed agent with Gemini Enterprise. This makes the agent discoverable and accessible to authorized end-users within your organization, allowing them to easily find and interact with your new, custom-built assistant." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "k0nKem_m_tOt" + }, + "source": [ + "#### Step 1: Setup and Prerequisites" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "5bb4021007d1" + }, + "outputs": [], + "source": [ + "# TODO for Developer: Update project id.\n", + "PROJECT_NUMBER = \"[your-project-number]\"\n", + "\n", + "# TODO for Developer: Update project name.\n", + "PROJECT_ID = \"[your-project-id]\"\n", + "\n", + "GEMMA_PARAMETER = \"gemma3-1b\"\n", + "REGION = \"us-central1\"\n", + "SERVICE_NAME = \"gemma-3-1b\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "bc0a45394998" + }, + "outputs": [], + "source": [ + "# Setup Google Cloud Product project.\n", + "!gcloud config set project {PROJECT_ID}\n", + "!gcloud config get-value project\n", + "\n", + "# Enable required services.\n", + "!gcloud services enable iam.googleapis.com" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "6bbb6c1939e6" + }, + "source": [ + "#### Step 2: Deploying the Open-Source Model (Gemma)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "346fb246646c" + }, + "outputs": [], + "source": [ + "!gcloud run deploy {SERVICE_NAME} \\\n", + " --image us-docker.pkg.dev/cloudrun/container/gemma/{GEMMA_PARAMETER} \\\n", + " --concurrency 4 \\\n", + " --cpu 8 \\\n", + " --set-env-vars OLLAMA_NUM_PARALLEL=4 \\\n", + " --gpu 1 \\\n", + " --gpu-type nvidia-l4 \\\n", + " --max-instances 1 \\\n", + " --memory 32Gi \\\n", + " --no-allow-unauthenticated \\\n", + " --no-cpu-throttling \\\n", + " --timeout=600 \\\n", + " --region {REGION} \\\n", + " --no-gpu-zonal-redundancy" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "43e765d31f23" + }, + "source": [ + "#### Step 3: Configuring the Root Agent" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "6802b8bbe4af" + }, + "outputs": [], + "source": [ + "pip install --quiet google-adk==1.7.0 litellm==1.74.7 ollama==0.5.1" + ] + }, + { + "cell_type": "code", + "execution_count": 207, + "metadata": { + "id": "cd76cb439634" + }, + "outputs": [], + "source": [ + "import google.auth.transport.requests\n", + "import google.oauth2.id_token\n", + "from google.adk.agents import LlmAgent\n", + "from google.genai import types\n", + "from ollama import Client\n", + "\n", + "api_base_url = \"https://gemma-3-1b-[your-project-number].us-central1.run.app\"\n", + "model_name_at_endpoint = \"ollama/gemma3:1b\"\n", + "\n", + "# Create a Google Auth request object\n", + "auth_req = google.auth.transport.requests.Request()\n", + "\n", + "# Generate the ID token\n", + "id_token = google.oauth2.id_token.fetch_id_token(auth_req, api_base_url)\n", + "\n", + "# Make the request with the ID token\n", + "auth_headers = {\"Authorization\": f\"Bearer {id_token}\"}\n", + "\n", + "\n", + "def get_answer(question: str):\n", + " \"\"\"/\n", + " Answer the question using gemma model.\n", + "\n", + " Args:\n", + " question: user's question.\n", + "\n", + " Returns:\n", + " returns detailed answer based on user's question.\n", + " \"\"\"\n", + " try:\n", + " client = Client(host=api_base_url, headers=auth_headers)\n", + " response = client.chat(\n", + " model=\"gemma3:1b\",\n", + " messages=[\n", + " {\n", + " \"role\": \"user\",\n", + " \"content\": question,\n", + " },\n", + " ],\n", + " )\n", + "\n", + " return response.message.content\n", + " except Exception as e:\n", + " print(e)\n", + " return \"Not able to provide answer.\"\n", + "\n", + "\n", + "# Option 1: Use Gemma as root agent.\n", + "# from google.adk.models.lite_llm import LiteLlm\n", + "# model=LiteLlm(\n", + "# model=model_name_at_endpoint,\n", + "# api_base=api_base_url,\n", + "# extra_headers=auth_headers\n", + "# )\n", + "\n", + "# Option 2: Use Gemini as root agent and use gemma agent as supportive agent for better results.\n", + "\n", + "gemma_agent = LlmAgent(\n", + " model=\"gemini-2.5-pro\",\n", + " name=\"gemma_agent\",\n", + " instruction=\"\"\"1. You are a helpful assistant, your task is the answer the question.\n", + " 2. Only answer the question asked by user, do not ask follow up questions .\n", + " 3. if no answer produce from function call tool then, you can mention that 'Sorry, answer can't be generated.'.\n", + " 4. Only answer the question based on provided context from tool. Do not add additinoal text. Preferably return the answer as is.\n", + " \"\"\",\n", + " tools=[get_answer],\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "1f38a350f0ee" + }, + "source": [ + "#### Step 4: Building and Testing the Agent with ADK" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "00d4b030125f" + }, + "outputs": [], + "source": [ + "from vertexai.preview import reasoning_engines\n", + "\n", + "app = reasoning_engines.AdkApp(\n", + " agent=gemma_agent,\n", + " enable_tracing=True,\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "130cbe7d0d11" + }, + "outputs": [], + "source": [ + "session = app.create_session(user_id=\"u_1232\")\n", + "session" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "801692e7994a" + }, + "outputs": [], + "source": [ + "query = \"where is india located?\"\n", + "contents = types.Content(role=\"user\", parts=[types.Part.from_text(text=query)])" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "d590093630ce" + }, + "outputs": [], + "source": [ + "contents" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "6c9559e54427" + }, + "outputs": [], + "source": [ + "for event in app.stream_query(\n", + " user_id=\"u_1232\",\n", + " session_id=session.id,\n", + " message=contents.model_dump(),\n", + "):\n", + " print(event)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "31bd98e3819f" + }, + "source": [ + "#### Step 5: Deploying the Agent to Agent Engine" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "c066be5562e3" + }, + "outputs": [], + "source": [ + "import vertexai\n", + "\n", + "PROJECT_NUMBER = PROJECT_NUMBER\n", + "LOCATION = \"us-central1\" # TODO for Developer : Update region here.\n", + "STAGING_BUCKET = (\n", + " \"gs://[bucket-name]\" # TODO for Developer : Update GCS bucket name here.\n", + ")\n", + "vertexai.init(project=PROJECT_NUMBER, location=LOCATION, staging_bucket=STAGING_BUCKET)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "7fded9eca51a" + }, + "outputs": [], + "source": [ + "from vertexai import agent_engines\n", + "\n", + "remote_app = agent_engines.create(\n", + " display_name=\"Gemma Agent v7\",\n", + " agent_engine=app,\n", + " requirements=[\n", + " \"litellm (==1.74.7)\",\n", + " \"google-adk (==1.7.0)\",\n", + " \"google-genai (==1.24.0)\",\n", + " \"pydantic (==2.11.7)\",\n", + " \"ollama (==0.5.1)\",\n", + " ],\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "7bea20750e56" + }, + "outputs": [], + "source": [ + "remote_app.resource_name" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "9355b5b8f624" + }, + "source": [ + "#### Step 6: Registering the Agent in Gemini Enterprise" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "2b5a9e686af1" + }, + "source": [ + "**Create oauth consent and mention the \"clientId\" and \"clientSecret\".**" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "3635a286d182" + }, + "outputs": [], + "source": [ + "%%bash\n", + "\n", + "curl -X POST \\\n", + "-H \"Authorization: Bearer $(gcloud auth print-access-token)\" \\\n", + "-H \"Content-Type: application/json\" \\\n", + "-H \"X-Goog-User-Project: [your-project-id]\" \\\n", + "\"https://discoveryengine.googleapis.com/v1alpha/projects/[your-project-id]/locations/global/authorizations?authorizationId=customhr9893\" \\\n", + "-d '{\n", + "\"name\": \"projects/[your-project-id]/locations/global/authorizations/customhr9893\",\n", + "\"serverSideOauth2\": {\n", + "\"clientId\": \"[UPDATE-CLIENT-ID]\",\n", + "\"clientSecret\": \"[UPDATE-CLIENT-SECRET]\",\n", + "\"authorizationUri\": \"https://accounts.google.com/o/oauth2/v2/auth?client_id=[UPDATE-CLIENT-ID]&scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fcloud-platform&include_granted_scopes=true&response_type=code&access_type=offline&prompt=consent\",\n", + "\"tokenUri\": \"https://oauth2.googleapis.com/token\"\n", + "}\n", + "}'" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "b33096f67166" + }, + "outputs": [], + "source": [ + "%%bash\n", + "\n", + "curl -X POST \\\n", + "-H \"Authorization: Bearer $(gcloud auth print-access-token)\" \\\n", + "-H \"Content-Type: application/json\" \\\n", + "-H \"X-Goog-User-Project: [your-project-number]\" \\\n", + "\"https://discoveryengine.googleapis.com/v1alpha/projects/[your-project-number]/locations/global/collections/default_collection/engines/[your-gemini-enterprise-engine-id]/assistants/default_assistant/agents\" \\\n", + "-d '{\n", + "\"displayName\": \"Gemma Agent v7\",\n", + "\"description\": \"Gemma Agent to answer questions.\",\n", + "\"adk_agent_definition\": {\n", + "\"tool_settings\": {\n", + "\"tool_description\": \"Gemma Agent to answer questions.\"\n", + "},\n", + "\"provisioned_reasoning_engine\": {\n", + "\"reasoning_engine\": \"projects/[your-project-number]/locations/us-central1/reasoningEngines/[reasoning-engine-id]\"\n", + "},\n", + "\"authorizations\": [\"projects/[your-project-number]/locations/global/authorizations/customhr9885\"]\n", + "}\n", + "}'" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "69085722b50a" + }, + "source": [ + "#### After agent with MCP server is enabled, you can interact with agent on Gemini Enterprise. " + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "2d990f30c874" + }, + "source": [ + "![oss_model_with_gemini_enterprise](https://services.google.com/fh/files/misc/oss_model_with_agentspace_v2.png)" + ] + } + ], + "metadata": { + "colab": { + "name": "oss_model_with_gemini_enterprise.ipynb", + "toc_visible": true + }, + "kernelspec": { + "display_name": "Python 3", + "name": "python3" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} diff --git a/search/gemini-enterprise/sample_schema.sql b/search/gemini-enterprise/sample_schema.sql new file mode 100644 index 0000000..13e88df --- /dev/null +++ b/search/gemini-enterprise/sample_schema.sql @@ -0,0 +1,253 @@ +-- HR Database Schema + +-- Table: Departments +-- Stores information about different departments within the organization. +CREATE TABLE Departments ( + department_id INT PRIMARY KEY AUTO_INCREMENT, -- Unique identifier for the department (AUTO_INCREMENT for MySQL/SQL Server, SERIAL for PostgreSQL) + department_name VARCHAR(100) NOT NULL UNIQUE, -- Name of the department, must be unique + location VARCHAR(255), -- Physical location of the department + phone_number VARCHAR(20), -- Contact number for the department + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, -- Timestamp when the record was created + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP -- Timestamp for last update (for MySQL, similar triggers needed for others) +); + +-- Table: JobTitles +-- Stores information about different job titles/roles. +CREATE TABLE JobTitles ( + job_id INT PRIMARY KEY AUTO_INCREMENT, -- Unique identifier for the job title + job_title VARCHAR(100) NOT NULL UNIQUE, -- Name of the job title, must be unique + min_salary DECIMAL(10, 2), -- Minimum salary for this job title + max_salary DECIMAL(10, 2), -- Maximum salary for this job title + job_description TEXT, -- Detailed description of the job + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP +); + +-- Table: Employees +-- Core table storing employee personal and professional information. +CREATE TABLE Employees ( + employee_id INT PRIMARY KEY AUTO_INCREMENT, -- Unique identifier for the employee + first_name VARCHAR(50) NOT NULL, -- Employee's first name + last_name VARCHAR(50) NOT NULL, -- Employee's last name + email VARCHAR(100) NOT NULL UNIQUE, -- Employee's email address, must be unique + phone_number VARCHAR(20), -- Employee's phone number + hire_date DATE NOT NULL, -- Date when the employee was hired + job_id INT NOT NULL, -- Foreign Key to JobTitles + department_id INT NOT NULL, -- Foreign Key to Departments + manager_id INT, -- Foreign Key to Employees (self-referencing for managers) + salary DECIMAL(10, 2) NOT NULL, -- Employee's current salary + date_of_birth DATE, -- Employee's date of birth + gender ENUM('Male', 'Female', 'Other', 'Prefer not to say'), -- Employee's gender (ENUM is MySQL specific, use CHECK constraint for others) + address VARCHAR(255), -- Employee's home address + city VARCHAR(100), -- Employee's city + state VARCHAR(100), -- Employee's state/province + zip_code VARCHAR(20), -- Employee's zip/postal code + country VARCHAR(100), -- Employee's country + is_active BOOLEAN DEFAULT TRUE, -- Is the employee currently active? + termination_date DATE, -- Date of termination if applicable + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + + FOREIGN KEY (job_id) REFERENCES JobTitles(job_id), + FOREIGN KEY (department_id) REFERENCES Departments(department_id), + FOREIGN KEY (manager_id) REFERENCES Employees(employee_id) +); + +-- Table: Dependents +-- Stores information about employee's dependents (for benefits, emergency contacts, etc.). +CREATE TABLE Dependents ( + dependent_id INT PRIMARY KEY AUTO_INCREMENT, -- Unique identifier for the dependent + employee_id INT NOT NULL, -- Foreign Key to Employees + first_name VARCHAR(50) NOT NULL, + last_name VARCHAR(50) NOT NULL, + relationship VARCHAR(50), -- e.g., 'Spouse', 'Child', 'Parent' + date_of_birth DATE, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + + FOREIGN KEY (employee_id) REFERENCES Employees(employee_id) ON DELETE CASCADE -- If employee deleted, dependents also deleted +); + +-- Table: Benefits +-- Defines different types of benefits offered by the company. +CREATE TABLE Benefits ( + benefit_id INT PRIMARY KEY AUTO_INCREMENT, -- Unique identifier for the benefit + benefit_name VARCHAR(100) NOT NULL UNIQUE, -- Name of the benefit (e.g., 'Health Insurance', 'Dental', '401k') + description TEXT, -- Description of the benefit + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP +); + +-- Table: EmployeeBenefits +-- Links employees to the benefits they are enrolled in. +CREATE TABLE EmployeeBenefits ( + employee_id INT NOT NULL, + benefit_id INT NOT NULL, + enrollment_date DATE, -- Date when the employee enrolled + coverage_start_date DATE, -- Date when coverage begins + coverage_end_date DATE, -- Date when coverage ends + PRIMARY KEY (employee_id, benefit_id), -- Composite primary key + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + + FOREIGN KEY (employee_id) REFERENCES Employees(employee_id) ON DELETE CASCADE, + FOREIGN KEY (benefit_id) REFERENCES Benefits(benefit_id) +); + +-- Table: PerformanceReviews +-- Stores information about employee performance reviews. +CREATE TABLE PerformanceReviews ( + review_id INT PRIMARY KEY AUTO_INCREMENT, -- Unique identifier for the review + employee_id INT NOT NULL, -- Foreign Key to Employees (employee being reviewed) + reviewer_id INT NOT NULL, -- Foreign Key to Employees (employee conducting the review) + review_date DATE NOT NULL, -- Date of the review + rating DECIMAL(3, 1), -- Numerical rating (e.g., 1.0-5.0) + comments TEXT, -- General comments + goals_achieved TEXT, -- Text describing goals achieved + development_areas TEXT, -- Text describing areas for development + next_review_date DATE, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + + FOREIGN KEY (employee_id) REFERENCES Employees(employee_id), + FOREIGN KEY (reviewer_id) REFERENCES Employees(employee_id) +); + +-- Table: TrainingPrograms +-- Defines available training programs. +CREATE TABLE TrainingPrograms ( + program_id INT PRIMARY KEY AUTO_INCREMENT, -- Unique identifier for the training program + program_name VARCHAR(150) NOT NULL UNIQUE, -- Name of the program + description TEXT, + duration_hours INT, -- Duration in hours + cost DECIMAL(10, 2), + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP +); + +-- Table: EmployeeTraining +-- Records employee's participation in training programs. +CREATE TABLE EmployeeTraining ( + employee_id INT NOT NULL, + program_id INT NOT NULL, + completion_date DATE, -- Date training was completed + grade VARCHAR(10), -- Grade or status (e.g., 'Pass', 'Fail', 'In Progress') + PRIMARY KEY (employee_id, program_id), -- Composite primary key + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + + FOREIGN KEY (employee_id) REFERENCES Employees(employee_id) ON DELETE CASCADE, + FOREIGN KEY (program_id) REFERENCES TrainingPrograms(program_id) +); + +-- Table: Absences +-- Tracks employee absences (sick leave, vacation, etc.). +CREATE TABLE Absences ( + absence_id INT PRIMARY KEY AUTO_INCREMENT, -- Unique identifier for the absence record + employee_id INT NOT NULL, -- Foreign Key to Employees + absence_type VARCHAR(50) NOT NULL, -- e.g., 'Sick Leave', 'Vacation', 'Unpaid Leave' + start_date DATE NOT NULL, + end_date DATE NOT NULL, + reason TEXT, + approved_by INT, -- Foreign Key to Employees (approving manager) + status ENUM('Pending', 'Approved', 'Rejected') DEFAULT 'Pending', -- (ENUM is MySQL specific, use CHECK constraint for others) + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + + FOREIGN KEY (employee_id) REFERENCES Employees(employee_id), + FOREIGN KEY (approved_by) REFERENCES Employees(employee_id) +); + +-- Table: EmergencyContacts +-- Stores emergency contact information for employees. +CREATE TABLE EmergencyContacts ( + contact_id INT PRIMARY KEY AUTO_INCREMENT, -- Unique identifier for the contact + employee_id INT NOT NULL, -- Foreign Key to Employees + name VARCHAR(100) NOT NULL, + relationship VARCHAR(50), + phone_number VARCHAR(20) NOT NULL, + email VARCHAR(100), + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + + FOREIGN KEY (employee_id) REFERENCES Employees(employee_id) ON DELETE CASCADE +); + +-- Table: Credentials +-- Stores information about employee's professional licenses, certifications etc. +CREATE TABLE Credentials ( + credential_id INT PRIMARY KEY AUTO_INCREMENT, + employee_id INT NOT NULL, + credential_name VARCHAR(100) NOT NULL, + issuing_organization VARCHAR(100), + issue_date DATE, + expiration_date DATE, + credential_number VARCHAR(100), + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + + FOREIGN KEY (employee_id) REFERENCES Employees(employee_id) ON DELETE CASCADE +); + +-- Table: Skills +-- Defines various skills that employees might possess. +CREATE TABLE Skills ( + skill_id INT PRIMARY KEY AUTO_INCREMENT, + skill_name VARCHAR(100) NOT NULL UNIQUE, + description TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP +); + +-- Table: EmployeeSkills +-- Associates employees with their skills and proficiency levels. +CREATE TABLE EmployeeSkills ( + employee_id INT NOT NULL, + skill_id INT NOT NULL, + proficiency_level ENUM('Beginner', 'Intermediate', 'Advanced', 'Expert'), -- (ENUM is MySQL specific, use CHECK constraint for others) + years_experience DECIMAL(4, 1), + PRIMARY KEY (employee_id, skill_id), + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + + FOREIGN KEY (employee_id) REFERENCES Employees(employee_id) ON DELETE CASCADE, + FOREIGN KEY (skill_id) REFERENCES Skills(skill_id) +); + + +-- DDL for PostgreSQL Specifics (if using PostgreSQL, replace AUTO_INCREMENT with SERIAL, and ENUM with CHECK) +/* +-- Example for PostgreSQL: +CREATE TABLE Departments ( + department_id SERIAL PRIMARY KEY, + department_name VARCHAR(100) NOT NULL UNIQUE, + location VARCHAR(255), + phone_number VARCHAR(20), + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- For gender/status ENUMs in PostgreSQL, use CHECK constraints: +gender VARCHAR(20), +CONSTRAINT check_gender CHECK (gender IN ('Male', 'Female', 'Other', 'Prefer not to say')), + +status VARCHAR(20) DEFAULT 'Pending', +CONSTRAINT check_status CHECK (status IN ('Pending', 'Approved', 'Rejected')), + +proficiency_level VARCHAR(20), +CONSTRAINT check_proficiency_level CHECK (proficiency_level IN ('Beginner', 'Intermediate', 'Advanced', 'Expert')), + +-- To automatically update 'updated_at' in PostgreSQL, you typically use a trigger: +CREATE OR REPLACE FUNCTION update_timestamp() +RETURNS TRIGGER AS $$ +BEGIN + NEW.updated_at = NOW(); + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +CREATE TRIGGER update_departments_timestamp +BEFORE UPDATE ON Departments +FOR EACH ROW EXECUTE FUNCTION update_timestamp(); +-- (Repeat for other tables) +*/ diff --git a/search/ranking-api/ranking_api_beir_evaluation.ipynb b/search/ranking-api/ranking_api_beir_evaluation.ipynb new file mode 100644 index 0000000..b75e416 --- /dev/null +++ b/search/ranking-api/ranking_api_beir_evaluation.ipynb @@ -0,0 +1,761 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "metadata": { + "id": "V0QBQDRJ4kOG" + }, + "outputs": [], + "source": [ + "# Copyright 2025 Google LLC\n", + "#\n", + "# Licensed under the Apache License, Version 2.0 (the \"License\");\n", + "# you may not use this file except in compliance with the License.\n", + "# You may obtain a copy of the License at\n", + "#\n", + "# https://www.apache.org/licenses/LICENSE-2.0\n", + "#\n", + "# Unless required by applicable law or agreed to in writing, software\n", + "# distributed under the License is distributed on an \"AS IS\" BASIS,\n", + "# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n", + "# See the License for the specific language governing permissions and\n", + "# limitations under the License." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "XRUrOsMa42ar" + }, + "source": [ + "\n", + " \n", + " \n", + " \n", + " \n", + "
    \n", + " \n", + " \"Google
    Open in Colab\n", + "
    \n", + "
    \n", + " \n", + " \"Google
    Open in Colab Enterprise\n", + "
    \n", + "
    \n", + " \n", + " \"Vertex
    Open in Vertex AI Workbench\n", + "
    \n", + "
    \n", + " \n", + " \"GitHub
    View on GitHub\n", + "
    \n", + "
    \n", + "\n", + "
    \n", + "\n", + "Share to:\n", + "\n", + "\n", + " \"LinkedIn\n", + "\n", + "\n", + "\n", + " \"Bluesky\n", + "\n", + "\n", + "\n", + " \"X\n", + "\n", + "\n", + "\n", + " \"Reddit\n", + "\n", + "\n", + "\n", + " \"Facebook\n", + "" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "VPVofWuB5BFe" + }, + "source": [ + "| Author |\n", + "| --- |\n", + "| [Jannis Grönberg](https://github.com/puebster) |" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "alpsCA9D5e5m" + }, + "source": [ + "## Overview\n", + "### Ranking API BEIR Evaluation\n", + "\n", + "This notebook is designed to evaluate the performance of the ranking model `googles/semantic-ranker-default-004` against established information retrieval benchmarks. It uses the BEIR (Benchmarking IR) dataset, which provides diverse collections and tasks for evaluating ranking models. The ground truth for evaluation consist of human-annotated relevance judgments.\n", + "\n", + "**Context:**\n", + "\n", + "This notebook performs the evaluation in several key stages:\n", + "\n", + "1. **Data Loading:** It begins by accessing a public Google Cloud Storage bucket (`ranking_datasets`). From this bucket, it loads humanly annotated data corresponding to the selected BEIR subdatasets: `nfcorpus`, `scidocs`, `trec-covid`, `webis-touche2020`, `dbpedia-entity`, and `msmarco`. This data includes the queries, documents, and their associated relevance judgments.\n", + "\n", + "2. **Score Computation:** Using the loaded data, the notebook proceeds to compute ranking scores. It performs the scoring operation (using the target model, `googles/semantic-ranker-default-004`) necessary for ranking. These computed scores are then stored back into the *same* designated Google Cloud Storage bucket, making them available for later use or inspection.\n", + "\n", + "3. **Recomputation Option:** For users who wish to re-run the scoring process or store the results in a different location, the notebook allows for configuration of the target Google Cloud Storage bucket to one within their own Google Cloud Platform project.\n", + "\n", + "4. **Score Loading for Evaluation:** The notebook loads these computed scores from the specified bucket.\n", + "\n", + "5. **Metric Calculation:** Finally, utilizing the `pytrec_eval` library, the notebook calculates standard ranking performance metrics. Specifically, it computes the Normalized Discounted Cumulative Gain (NDCG) at various cutoffs: NDCG@1, NDCG@3, NDCG@5, and NDCG@10. These values provide a quantitative measure of how well the model ranks relevant documents for the given queries, based on the loaded ground truth relevance judgments and the computed scores.\n", + "\n", + "**Important Notes on Evaluation:**\n", + "\n", + "* **Labeled Data Only:** We have restricted our evaluation to only the labeled datapoints within each BEIR subdataset. This decision stems from the observation that many datasets contain examples that are clearly relevant but lack explicit labels. Including these unlabeled but relevant examples would lead to an underestimation of the model's performance, making a fair evaluation challenging.\n", + "* **Datasets with Multiple Labels:** We have further filtered the BEIR datasets to include only those that have more than one unique label assigned. This is because computing meaningful Normalized Discounted Cumulative Gain (NDCG) scores on datasets where each query has only a single relevant document is problematic and can lead to misleading results.\n", + "* **Enable the API** User need to first enable the discoveryengine api by visiting [Enable Discoveryengine API](https://console.developers.google.com/apis/api/discoveryengine.googleapis.com/)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "yd2T58Vf5cOM" + }, + "source": [ + "### Install Google Cloud SDKs and other required packages\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "X99z1Ml65c1V" + }, + "outputs": [], + "source": [ + "%pip install --upgrade --quiet google-cloud-discoveryengine pytrec_eval numpy pandas scikit-learn tqdm gcsfs" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "VA_Ko1_66t6q" + }, + "source": [ + "# Setup" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "mQVTqw4H5wH2" + }, + "source": [ + "### Authenticate your notebook environment (Colab only)\n", + "\n", + "If you're running this notebook on Google Colab, run the cell below to authenticate your environment." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": { + "cellView": "form", + "id": "zHlYMqp-5v4Q" + }, + "outputs": [], + "source": [ + "# @title Colab authentification\n", + "import sys\n", + "\n", + "if \"google.colab\" in sys.modules:\n", + " from google.colab import auth\n", + "\n", + " auth.authenticate_user()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "id": "sZiEMnBLy84D" + }, + "outputs": [], + "source": [ + "# @title Import Libraries\n", + "\n", + "# Standard library imports\n", + "import json\n", + "import os\n", + "import time\n", + "\n", + "# Third-party library imports\n", + "import numpy as np\n", + "import pandas as pd\n", + "import pytrec_eval\n", + "from google.cloud import discoveryengine_v1 as discoveryengine\n", + "from google.cloud import storage\n", + "from sklearn.metrics import roc_auc_score\n", + "from tqdm.auto import tqdm" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": { + "cellView": "form", + "id": "A61SFkLs5ojJ" + }, + "outputs": [], + "source": [ + "# @title Helper functions\n", + "\n", + "\n", + "def call_model_in_batches(\n", + " df: pd.DataFrame, model_name: str, project_id: str, location: str\n", + "):\n", + " \"\"\"Generates ranking scores for document IDs based on provided queries using\n", + "\n", + " the Google Cloud Discovery Engine Rank API.\n", + "\n", + " The function processes a DataFrame of queries and associated documents,\n", + " sends batches of documents to the Discovery Engine Rank API for scoring\n", + " against each query, and returns a dictionary of query-to-document-score\n", + " mappings.\n", + "\n", + " Args:\n", + " df: A Pandas DataFrame containing query and document information. It must\n", + " have columns: \"query_id\", \"query\", \"document_id\", \"title\", and\n", + " \"content\". Each row represents a document associated with a specific\n", + " query.\n", + " project_id: The ID of the Google Cloud Project.\n", + " location: The location (region) of the Discovery Engine instance.\n", + " model_name: The name of the ranking model to use in the Discovery Engine.\n", + "\n", + " Returns:\n", + " A dictionary where the outer keys are query IDs (str) and the inner values\n", + " are dictionaries mapping document IDs (str) to their ranking scores\n", + " (float),\n", + " rounded to four decimal places.\n", + " \"\"\"\n", + " results = {}\n", + "\n", + " client = discoveryengine.RankServiceClient()\n", + " ranking_config = client.ranking_config_path(\n", + " project=project_id,\n", + " location=location,\n", + " ranking_config=\"default_ranking_config\",\n", + " )\n", + " query_groups = df.groupby(\"query_id\")\n", + " dataset_name = df.iloc[0][\"name\"]\n", + " overall_passed_seconds = 0\n", + "\n", + " # Iterate through datasets\n", + " for query_id, group in tqdm(query_groups, desc=f\"Scoring {dataset_name}\"):\n", + " # Add the query id to the results\n", + " query = group.iloc[0][\"query\"]\n", + " results[query_id] = {}\n", + "\n", + " records = []\n", + " # iterate through rows of each subgroup.\n", + " for _, row in group.iterrows():\n", + " records.append(\n", + " discoveryengine.RankingRecord(\n", + " id=row[\"document_id\"],\n", + " title=row[\"title\"],\n", + " content=row[\"content\"],\n", + " )\n", + " )\n", + " # We process in batches of 200\n", + " if len(records) >= 200:\n", + " now = time.time()\n", + " request = discoveryengine.RankRequest(\n", + " ranking_config=ranking_config,\n", + " model=model_name,\n", + " query=query,\n", + " records=records,\n", + " ignore_record_details_in_response=True,\n", + " )\n", + " resp = client.rank(request=request)\n", + " overall_passed_seconds += time.time() - now\n", + " for i in resp.records:\n", + " results[query_id][i.id] = i.score\n", + " records = []\n", + "\n", + " # If any records are left process them\n", + " if len(records) > 0:\n", + " now = time.time()\n", + " request = discoveryengine.RankRequest(\n", + " ranking_config=ranking_config,\n", + " model=model_name,\n", + " query=query,\n", + " records=records,\n", + " ignore_record_details_in_response=True,\n", + " )\n", + " resp = client.rank(request=request)\n", + " overall_passed_seconds += time.time() - now\n", + "\n", + " # Round the scores and add them to the overall results.\n", + " for i in resp.records:\n", + " results[query_id][i.id] = round(i.score, 4)\n", + "\n", + " print(\n", + " f\"The time to process {len(query_groups)} queries with at total number of\"\n", + " f\" {len(df)} documents took: {round(overall_passed_seconds, 2)}s.\\n That\"\n", + " f\" is {round(1000 * overall_passed_seconds / len(query_groups), 2)}ms per\"\n", + " f\" query or {round(1000 * overall_passed_seconds / len(df), 2)}ms per\"\n", + " \" document.\"\n", + " )\n", + " return results\n", + "\n", + "\n", + "def evaluate(\n", + " query_document_ground_truth: dict[str, dict[str, int]],\n", + " query_document_prediction: dict[str, dict[str, float]],\n", + " k_values: list[int],\n", + " ignore_identical_ids: bool = True,\n", + " verbose: bool = True,\n", + ") -> tuple[dict[str, float], dict[str, float], dict[str, float], dict[str, float]]:\n", + " \"\"\"Evaluates the retrieval results against the ground truth relevance judgments (query_document_ground_truth)\n", + "\n", + " using Normalized Discounted Cumulative Gain (NDCG).\n", + " This code is taken from the official BEIR evaluation code.\n", + "\n", + " Args:\n", + " query_document_ground_truth: A dictionary representing the ground truth relevance judgments. The\n", + " outer keys are query IDs (str), and the inner dictionary maps document\n", + " IDs (str) to their relevance scores (int).\n", + " query_document_prediction: A dictionary representing the retrieval results. The outer keys\n", + " are query IDs (str), and the inner dictionary maps retrieved document\n", + " IDs (str) to their retrieval scores (float).\n", + " k_values: A list of integers representing the cutoff ranks for NDCG\n", + " calculation (e.g., [1, 3, 5, 10]). NDCG will be calculated for each k in\n", + " this list.\n", + " ignore_identical_ids: If True, documents with the same ID as the query ID\n", + " will be ignored during evaluation to prevent self-ranking. Defaults to\n", + " True.\n", + " verbose: If True, detailed evaluation metrics for each k-value will be\n", + " printed. Defaults to True.\n", + "\n", + " Returns:\n", + " A tuple containing four dictionaries:\n", + " - ndcg_scores: A dictionary with the k values as keys and ndcg@k\n", + " scores as values.\n", + " \"\"\"\n", + " if ignore_identical_ids:\n", + " popped = []\n", + " for query_id, document_scores in query_document_prediction.items():\n", + " for document_id in list(document_scores):\n", + " if query_id == document_id:\n", + " document_scores.pop(document_id)\n", + " popped.append(document_id)\n", + "\n", + " ndcg = {}\n", + "\n", + " for k in k_values:\n", + " ndcg[f\"NDCG@{k}\"] = 0.0\n", + "\n", + " ndcg_string = \"ndcg_cut.\" + \",\".join([str(k) for k in k_values])\n", + " evaluator = pytrec_eval.RelevanceEvaluator(\n", + " query_document_ground_truth,\n", + " {\n", + " ndcg_string,\n", + " },\n", + " )\n", + "\n", + " scores = evaluator.evaluate(query_document_prediction)\n", + "\n", + " for query_id in scores.keys():\n", + " for k in k_values:\n", + " ndcg[f\"NDCG@{k}\"] += scores[query_id][\"ndcg_cut_\" + str(k)]\n", + "\n", + " for k in k_values:\n", + " ndcg[f\"NDCG@{k}\"] = round(ndcg[f\"NDCG@{k}\"] / len(scores), 5)\n", + "\n", + " if verbose:\n", + " for eval in [ndcg]:\n", + " for k in eval.keys():\n", + " print(f\"{k}: {eval[k]:.4f}\")\n", + "\n", + " return ndcg\n", + "\n", + "\n", + "def load_bytes_from_gcs(bucket_name: str, file_path: str):\n", + " \"\"\"Accesses a Google Cloud Storage (GCS) bucket, opens a file,\n", + "\n", + " loads its contents and returns its bytes\n", + "\n", + " Args:\n", + " bucket_name: The name of the GCS bucket.\n", + " file_path: The path to the file within the bucket (e.g.,\n", + " 'data/my_data.json').\n", + "\n", + " Returns:\n", + " Bytes like object\n", + " \"\"\"\n", + " try:\n", + " # Initialize the GCS client\n", + " client = storage.Client()\n", + "\n", + " # Get the bucket object\n", + " bucket = client.bucket(bucket_name)\n", + "\n", + " # Get the blob (file) object\n", + " blob = bucket.blob(file_path)\n", + "\n", + " # Download the blob's content as bytes\n", + " _bytes = blob.download_as_bytes()\n", + "\n", + " return _bytes\n", + "\n", + " except Exception as e:\n", + " print(f\"An error occurred: {e}\")\n", + " return None\n", + "\n", + "\n", + "def save_dict_to_gcs_json(\n", + " data: dict, bucket_name: str, file_path: str, encoding: str = \"utf-8\"\n", + ") -> None:\n", + " \"\"\"Saves a Python dictionary as a JSON file to a Google Cloud Storage (GCS) bucket.\n", + "\n", + " Args:\n", + " data: The dictionary to be saved.\n", + " bucket_name: The name of the GCS bucket.\n", + " file_path: The desired path for the JSON file within the bucket (e.g.,\n", + " 'data/my_data.json').\n", + " encoding: The encoding to use when writing the JSON file. Defaults to\n", + " 'utf-8'.\n", + " \"\"\"\n", + " try:\n", + " # Initialize the GCS client\n", + " client = storage.Client()\n", + "\n", + " # Get the bucket object\n", + " bucket = client.bucket(bucket_name)\n", + "\n", + " # Get the blob (file) object\n", + " blob = bucket.blob(file_path)\n", + "\n", + " # Serialize the dictionary to a JSON string\n", + " json_string = json.dumps(\n", + " data, indent=2, ensure_ascii=False\n", + " ) # indent for readability\n", + "\n", + " # Upload the JSON string to GCS as bytes with specified encoding\n", + " blob.upload_from_string(\n", + " json_string.encode(encoding), content_type=\"application/json\"\n", + " )\n", + "\n", + " print(f\"Dictionary successfully saved to gs://{bucket_name}/{file_path}\")\n", + "\n", + " except Exception as e:\n", + " print(f\"An error occurred while saving to GCS: {e}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "mR1elDv27JaZ" + }, + "source": [ + "# If you want to run the scoring for yourself run the following section. Otherwise you can skip this section" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": { + "id": "T5cRJpRI7VD6" + }, + "outputs": [], + "source": [ + "# @title Google Cloud Platform parameters\n", + "\n", + "# @markdown Change this to your bucket, project id and location\n", + "\n", + "\n", + "# fmt: off\n", + "PROJECT_ID = \"[your-project-id]\" # @param {type: \"string\", placeholder: \"[your-project-id]\", isTemplate: true}\n", + "if not PROJECT_ID or PROJECT_ID == \"[your-project-id]\":\n", + " PROJECT_ID = str(os.environ.get(\"GOOGLE_CLOUD_PROJECT\"))\n", + "\n", + "LOCATION = os.environ.get(\"GOOGLE_CLOUD_REGION\", \"us-central1\")\n", + "\n", + "# @markdown If you want to run the scoring for yourself you need to change this to your bucket otherwise you can leave these variables as is.\n", + "OUTPUT_SCORES_BUCKET_NAME = \"ranking_datasets\" # @param {type: \"string\", placeholder: \"[your-project-id]\", isTemplate: true}\n", + "MODEL_NAME = \"semantic-ranker-default-004\" # @param {type: \"string\", placeholder: \"[your-project-id]\", isTemplate: true}\n", + "SCORE_PATH = \"scores\" # @param {type: \"string\", placeholder: \"[your-project-id]\", isTemplate: true}\n", + "# fmt: on\n", + "\n", + "DATA_INPUT_BUCKET_NAME = \"ranking_datasets\" # leave this as is\n", + "DATASET_PATH = \"input_data/labeled_beir_with_more_than_1_label.pkl\" # leave this as is" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "id": "YzpKt2ul-8cR" + }, + "outputs": [], + "source": [ + "# @title Read Dataset\n", + "df = pd.read_pickle(f\"gs://{DATA_INPUT_BUCKET_NAME}/{DATASET_PATH}\")\n", + "display(df.groupby(\"name\").agg({\"query_id\": \"nunique\", \"document_id\": \"nunique\"}))" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": { + "id": "lKV9uEfOhpFJ" + }, + "outputs": [], + "source": [ + "# @title Test connection\n", + "docs = [\n", + " {\n", + " \"query_id\": \"1\",\n", + " \"query\": \"Why is the sky blue?\",\n", + " \"name\": \"test\",\n", + " \"document_id\": \"1\",\n", + " \"title\": \"Rayleigh Scattering and Blue Sky\",\n", + " \"content\": (\n", + " \"The sky appears blue because of a phenomenon called Rayleigh scattering. \"\n", + " \"When sunlight travels through the atmosphere, it collides with gas molecules \"\n", + " \"and scatters in all directions. Blue light is scattered more than other colors \"\n", + " \"because it travels in shorter waves.\"\n", + " ),\n", + " },\n", + " {\n", + " \"query_id\": \"1\",\n", + " \"query\": \"Why is the sky blue?\",\n", + " \"name\": \"test\",\n", + " \"document_id\": \"2\",\n", + " \"title\": \"Cooking Pasta\",\n", + " \"content\": (\n", + " \"To cook perfect pasta, bring a large pot of salted water to a rolling boil. \"\n", + " \"Add the pasta and cook until al dente, stirring occasionally. Drain and serve \"\n", + " \"with your favorite sauce.\"\n", + " ),\n", + " },\n", + "]\n", + "\n", + "res = call_model_in_batches(\n", + " df=pd.DataFrame(docs),\n", + " model_name=MODEL_NAME,\n", + " project_id=PROJECT_ID,\n", + " location=LOCATION,\n", + ")\n", + "\n", + "for d in docs:\n", + " for r, v in res[\"1\"].items():\n", + " if r == d[\"document_id\"]:\n", + " print(d[\"title\"], v)\n", + " break" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "id": "qjf6MgGe0Dta" + }, + "outputs": [], + "source": [ + "# @title Score Query-Document pairs\n", + "\n", + "dataset_groups = df.groupby(\"name\")\n", + "for subdataset_name, dataset in dataset_groups:\n", + " score_output_path = f\"{SCORE_PATH}/{subdataset_name.replace('-', '_')}/{MODEL_NAME.replace('-', '_')}.json\"\n", + "\n", + " print(f\"Evaluating : {subdataset_name.upper()}\")\n", + " print(f\"Shape: {dataset.shape}\")\n", + "\n", + " scores = call_model_in_batches(\n", + " df=dataset,\n", + " model_name=MODEL_NAME,\n", + " project_id=PROJECT_ID,\n", + " location=LOCATION,\n", + " )\n", + "\n", + " save_dict_to_gcs_json(\n", + " data=scores, bucket_name=OUTPUT_SCORES_BUCKET_NAME, file_path=score_output_path\n", + " )" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "53evNKYvtmRC" + }, + "source": [ + "# Calculate NDCG and ROC AUC Scores" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "id": "qGbk22265OrG" + }, + "outputs": [], + "source": [ + "# @title Calculate...\n", + "\n", + "k_values = [1, 3, 5, 10]\n", + "\n", + "# Get datasets\n", + "client = storage.Client()\n", + "bucket = client.bucket(OUTPUT_SCORES_BUCKET_NAME)\n", + "blobs = [\n", + " \"nfcorpus\",\n", + " \"scidocs\",\n", + " \"trec-covid\",\n", + " \"webis-touche2020\",\n", + " \"dbpedia-entity\",\n", + " \"msmarco\",\n", + "]\n", + "_res = None\n", + "\n", + "for sub_name in blobs:\n", + " print(sub_name)\n", + " query_document_ground_truth_path = (\n", + " f\"{SCORE_PATH}/{sub_name.replace('-', '_')}/qrels.json\"\n", + " )\n", + " query_document_prediction_path = (\n", + " f\"{SCORE_PATH}/{sub_name.replace('-', '_')}/{MODEL_NAME.replace('-', '_')}.json\"\n", + " )\n", + "\n", + " query_document_ground_truth = json.loads(\n", + " load_bytes_from_gcs(\n", + " OUTPUT_SCORES_BUCKET_NAME, query_document_ground_truth_path\n", + " ).decode()\n", + " )\n", + " query_document_prediction = json.loads(\n", + " load_bytes_from_gcs(\n", + " OUTPUT_SCORES_BUCKET_NAME, query_document_prediction_path\n", + " ).decode()\n", + " )\n", + "\n", + " # Check if all query document pairs are present in Query Document Ground Truth and Query Document Prediction\n", + " for first, second, n1, n2 in [\n", + " (\n", + " query_document_ground_truth,\n", + " query_document_prediction,\n", + " \"Query Document Ground Truth\",\n", + " \"Query Document Prediction\",\n", + " ),\n", + " (\n", + " query_document_prediction,\n", + " query_document_ground_truth,\n", + " \"Query Document Prediction\",\n", + " \"Query Document Ground Truth\",\n", + " ),\n", + " ]:\n", + " deleted_queries = 0\n", + " deleted_docs = 0\n", + " first_query_id_keys = list(first.keys())\n", + " for query_id in first_query_id_keys:\n", + " if query_id not in second:\n", + " del first[query_id]\n", + " deleted_queries += 1\n", + " first_document_id_keys = list(first[query_id].keys())\n", + " for document_id in first_document_id_keys:\n", + " if document_id not in second[query_id]:\n", + " del first[query_id][document_id]\n", + " deleted_docs += 1\n", + " if deleted_queries > 0 or deleted_docs > 0:\n", + " print(\n", + " f\"Deleted from {n1} (because not present in {n2}):\"\n", + " f\" {deleted_queries} Queries, {deleted_docs} Documents\"\n", + " )\n", + "\n", + " # The following conversion is needed as pytrec eval only excepts positive integers as ground truth labels\n", + " y_true, y_pred = [], []\n", + " max_score = 0\n", + " for query_id, document_id_scores in query_document_ground_truth.items():\n", + " for document_id, y_true_score in document_id_scores.items():\n", + " max_score = max(max_score, y_true_score)\n", + "\n", + " for query_id, document_id_scores in query_document_ground_truth.items():\n", + " for document_id, y_pred_score in document_id_scores.items():\n", + " y_true_label = 0 if y_pred_score / max_score <= 0.5 else 1\n", + " y_true.append(y_true_label)\n", + " y_pred.append(query_document_prediction[query_id][document_id])\n", + "\n", + " ndcg = evaluate(\n", + " query_document_ground_truth, query_document_prediction, k_values, verbose=False\n", + " )\n", + " results = {k: [v] for k, v in ndcg.items()}\n", + "\n", + " results[\"ROC AUC\"] = [float(roc_auc_score(np.array(y_true), np.array(y_pred)))]\n", + "\n", + " ndcg_res = pd.DataFrame.from_dict(results, orient=\"index\", columns=[sub_name])\n", + " if _res is None:\n", + " _res = ndcg_res\n", + " else:\n", + " _res = _res.merge(ndcg_res, left_index=True, right_index=True)\n", + "\n", + "_res[\"Macro Avg.\"] = _res.mean(axis=1)\n", + "_res.index.name = MODEL_NAME" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "3vQoBzg3oklS" + }, + "outputs": [], + "source": [ + "_res" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "71137aacc1d4" + }, + "source": [ + "## Conclusion\n", + "\n", + "In this notebook, we successfully demonstrated how to evaluate the performance of the `googles/semantic-ranker-default-004` model on several key BEIR subdatasets.\n", + "\n", + "We walked through the process of:\n", + "* Loading human-annotated ground truth data from a public Google Cloud Storage bucket.\n", + "* Utilizing the Google Ranking API (implicitly through the scoring process) to compute relevance scores for query-document pairs.\n", + "* Storing the computed scores back into GCS for persistence and accessibility.\n", + "* Loading these scores and the ground truth to calculate standard information retrieval metrics, specifically NDCG@(1, 3, 5, 10), using the `pytrec_eval` library.\n", + "\n", + "A significant takeaway from this evaluation is the strong performance of the `googles/semantic-ranker-default-004` model. At the point in time of publishing this notebook, the results indicate that this new model achieves state-of-the-art results on these specific BEIR benchmarks, showcasing its effectiveness for ranking tasks.\n", + "\n", + "### Where to Learn More\n", + "\n", + "* **Google Ranking API:** Explore the official documentation for the Ranking API to understand its capabilities, pricing, and other available models.\n", + "* **BEIR Dataset:** Visit the BEIR project website to learn more about the various datasets available, their characteristics, and how they are used for benchmarking.\n", + "* **pytrec_eval:** Refer to the documentation for the `pytrec_eval` library to understand the NDCG metric and other potential evaluation metrics in more detail." + ] + } + ], + "metadata": { + "colab": { + "name": "ranking_api_beir_evaluation.ipynb", + "toc_visible": true + }, + "kernelspec": { + "display_name": "Python 3", + "name": "python3" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} diff --git a/search/tuning/vertexai-search-tuning.ipynb b/search/tuning/vertexai-search-tuning.ipynb new file mode 100644 index 0000000..8e068d1 --- /dev/null +++ b/search/tuning/vertexai-search-tuning.ipynb @@ -0,0 +1,1551 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "id": "068fcf807336" + }, + "source": [ + "# Search tuning in Vertex AI Search\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + "
    \n", + " \n", + " \"Google
    Open in Colab\n", + "
    \n", + "
    \n", + " \n", + " \"Google
    Open in Colab Enterprise\n", + "
    \n", + "
    \n", + " \n", + " \"Vertex
    Open in Vertex AI Workbench\n", + "
    \n", + "
    \n", + " \n", + " \"GitHub
    View on GitHub\n", + "
    \n", + "
    \n", + "\n", + "
    \n", + " \n", + "Share to:\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
    \n", + " \n", + " \"LinkedIn\n", + " \n", + " \n", + " \n", + " \"Bluesky\n", + " \n", + " \n", + " \n", + " \"X\n", + " \n", + " \n", + " \n", + " \"Reddit\n", + " \n", + " \n", + " \n", + " \"Facebook\n", + " \n", + "
    " + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "ca3c8a638e33" + }, + "source": [ + "| Author |\n", + "| --- |\n", + "| [Jincheol Kim](https://github.com/JincheolKim) |" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "80c810fbe603" + }, + "source": [ + "When users try to provide a search service over their archived documents and data, search performance may not meet the performance expectation all the time. The performance of Vertex AI Search can be measured in two aspects: the accuracy and the relevance of the search results, and the correctness of the summarized responses from the search results with correct annotations and references to the source document. Among the two aspects of the search performances, the accuracy and the relevance of the search results should be enhanced by generating embedding vectors which are more relevant semantically with document chunking and other document processing methods. The correctness of the summarized responses generated from the backend LLM (Gemini) behind the Vertex AI Search endpoint can be enhanced by tuning the backend LLM with some additional relevant data. The process of tuning the backend LLM with some domain-specific data is what Vertex AI Search Tuning is for.\n", + " \n", + "Before we tune the backend LLM behind Vertex AI Search, we should the prepare the raw text data in a specific JSONL format with a question-answer mapping file in the tab-separated table format. We will use some FAQ documents from an open source project (Kubernetes) to tune the backend LLM to enhance answers on the questions on Kubernetes. After we learn how we prepare the tuning data in JSONL and TSV format, we will learn how we can configure a search tuning job and submit it to Vertex AI.\n", + " \n", + "To learn more about the search tuning process, please refer to the following documents in the Google Cloud Documentation.\n", + " \n", + "- [Improve search results with search tuning](https://cloud.google.com/generative-ai-app-builder/docs/tune-search)\n", + "- [Create a search data store](https://cloud.google.com/generative-ai-app-builder/docs/create-data-store-es)\n", + "- [Create a search app](https://cloud.google.com/generative-ai-app-builder/docs/create-engine-es)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "11198a79d42c" + }, + "source": [ + "## Overview\n", + "\n", + "![Key user journey of the search tuning in Vertex AI Search](https://storage.googleapis.com/github-repo/generative-ai/search/tuning/images/key_user_journey_search_tuning.png)\n", + "\n", + "* Prepare your data for tuning\n", + " - The datasets should be prepared in JSONL format with identifier-text pairs.\n", + " - The mapping between query and answer texts should be described in tab-separated values (TSV) formats.\n", + "* Update the datastore with the additional documents\n", + " - Before we update the datastore attached to the search app, the additional documents and data for tuning should be uploaded to the bucket in Cloud Storage.\n", + " - After uploading the new documents and data onto the bucket in Cloud Storage, the datastore is refreshed just by creating the datastore with the same configuration used in the previous creation. In the refresh process, we can see that only the files just added are used to generate new search indexes at the console interface.\n", + "* Rebuild the search app with the updated datastore\n", + " - After the refresh of the datastore is completed, the search app must be rebuilt to be connected to the updated datastore.\n", + "\n", + "In order to obtain the correct results with the additional documents and data, users must rebuild the search app after they rebuilt the datastore." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "4d998a5140b2" + }, + "source": [ + "## Get started" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "7de8816128de" + }, + "source": [ + "### Install Vertex AI SDK and other required packages\n", + "\n", + "We will install some dependencies to run the cells in this notebook. \n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "35b342466ed4" + }, + "outputs": [], + "source": [ + "%pip install --upgrade --user --quiet google-cloud-aiplatform google-cloud-discoveryengine langchain_google_community langchain langchain-google-vertexai langchain-google-community[vertexaisearch] shortuuid" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "f10b11bebfd1" + }, + "source": [ + "## Restart runtime\n", + "\n", + "To use the newly installed packages in this Jupyter runtime, you must restart the runtime. You can do this by running the cell below, which restarts the current kernel.\n", + "\n", + "The restart might take a minute or longer. After it has restarted, continue to the next step." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "d76628df8180" + }, + "outputs": [], + "source": [ + "import IPython\n", + "\n", + "app = IPython.Application.instance()\n", + "app.kernel.do_shutdown(True)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "7c12448f2e71" + }, + "source": [ + "## Authenticate your notebook environment (Colab only)\n", + "\n", + "If you're running this notebook on Google Colab, run the cell below to authenticate your environment." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "d8d593172549" + }, + "outputs": [], + "source": [ + "import os\n", + "import sys\n", + "\n", + "if \"google.colab\" in sys.modules:\n", + "\n", + " from google.colab import auth\n", + "\n", + " auth.authenticate_user()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "8b3a2e9b94d5" + }, + "source": [ + "## Set Google Cloud project information and initialize Vertex AI SDK for Python\n", + "\n", + "To get started using Vertex AI, you must have an existing Google Cloud project and [enable the Vertex AI API](https://console.cloud.google.com/flows/enableapi?apiid=aiplatform.googleapis.com). Learn more about [setting up a project and a development environment](https://cloud.google.com/vertex-ai/docs/start/cloud-environment)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "1800f6c7238e" + }, + "outputs": [], + "source": [ + "import json\n", + "import logging\n", + "\n", + "# Imports common packages\n", + "import os\n", + "import platform\n", + "import re\n", + "import sys\n", + "\n", + "from google.api_core.client_options import ClientOptions\n", + "from google.api_core.operation import Operation\n", + "from google.cloud import discoveryengine\n", + "import shortuuid\n", + "import vertexai" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "efdd454efb6a" + }, + "outputs": [], + "source": [ + "!gcloud auth login" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "1875997de41d" + }, + "outputs": [], + "source": [ + "# Use the environment variable if the user doesn't provide Project ID.\n", + "PROJECT_ID = \"[your-project-id]\" # @param {type: \"string\", placeholder: \"[your-project-id]\", isTemplate: true}\n", + "if not PROJECT_ID or PROJECT_ID == \"[your-project-id]\":\n", + " PROJECT_ID = str(os.environ.get(\"GOOGLE_CLOUD_PROJECT\"))\n", + "\n", + "PROJECT_ID = \"genai-customersupport\"\n", + "LOCATION = \"global\"\n", + "STORAGE_LOCATION = \"us\"\n", + "\n", + "vertexai.init(project=PROJECT_ID, location=LOCATION)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "643f6d02011b" + }, + "source": [ + "## Create a Cloud Storage bucket\n", + "\n", + "Create a storage bucket to store intermediate artifacts such as datasets." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "f5c2d63395c1" + }, + "outputs": [], + "source": [ + "PUBLIC_DATA_SOURCE_URI = f\"gs://github-repo/generative-ai/search/tuning\"\n", + "BASE_DATA_SOURCE_URI = f\"gs://github-repo/generative-ai/search/tuning/awesome_rlhf\"\n", + "BUCKET_URI = f\"gs://sample-search-tuning-{PROJECT_ID}\" # @param {type:\"string\"}\n", + "TUNING_DATA_PATH_SOURCE = \"gs://github-repo/generative-ai/search/tuning/tuning_data\"\n", + "TUNING_DATA_PATH_LOCAL = f\"./tuning_data\"\n", + "TUNING_DATA_PATH_REMOTE = f\"{BUCKET_URI}/tuning_data\"\n", + "SEARCH_DATASTORE_PATH_REMOTE = f\"{BUCKET_URI}/rlhf-datastore\"\n", + "SEARCH_DATASTORE_ID = f\"search-datastore-{PROJECT_ID}-{shortuuid.uuid().lower()}\"\n", + "SEARCH_DATASTORE_NAME = \"RLHF-ARTICLE-DATASTORE\"" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "85597b5e35a8" + }, + "source": [ + "\"**If your bucket doesn't already exist**: Run the following cell to create your Cloud Storage bucket." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "abaea548b38b" + }, + "outputs": [], + "source": [ + "! gcloud storage buckets create --location={STORAGE_LOCATION} --project={PROJECT_ID} --enable-hierarchical-namespace --uniform-bucket-level-access -b {BUCKET_URI}\n", + "! mkdir $TUNING_DATA_PATH_LOCAL\n", + "! gcloud storage cp $TUNING_DATA_PATH_SOURCE/* $TUNING_DATA_PATH_LOCAL" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "c5b4068c0c26" + }, + "source": [ + "## Prepare the data\n", + "\n", + "We will use the following datasets for this notebook. \n", + "\n", + "(1) FAQ data from the open source projects Kubernetes and Kubernetes Client. This data is a short list of questions and answers which can be useful to test the working of this notebook in a short period of time.\n", + "\n", + "(2) BEIR ([Benchmarking IR datasets](https://github.com/beir-cellar/beir)): BEIR is a heterogeneous benchmark containing diverse IR tasks. It also provides a common and easy framework for evaluation of your NLP-based retrieval models within the benchmark. This public dataset is hosted in Google BigQuery and is included in BigQuery's 1TB/mo of free tier processing. This means that each user receives 1TB of free BigQuery processing every month, which can be used to run queries on this public dataset.\n", + " - For an overview, checkout our new wiki page: https://github.com/beir-cellar/beir/wiki.\n", + " - For models and datasets, checkout out Hugging Face (HF) page: https://huggingface.co/BeIR.\n", + " \n", + "(3) SciFact ([SciFact](https://huggingface.co/datasets/allenai/scifact)): SciFact, a dataset of 1.4K expert-written scientific claims paired with evidence-containing abstracts, and annotated with labels and rationales.\n", + " \n", + "For BEIR and SciFact, the datasets are already prepared in JSONL and TSV formats. You can use them for testing the search tuning feature without any data preprocessing chore. However, the amount of the data of the BEIR and SciFact is large which make the tuning job run too long. Given that, we will try to generate a small amount of the data first to check if the search tuning feature is working correctly with the FAQ data from the Kubernetes project." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "d7d8d04e32e5" + }, + "source": [ + "This ```generate_source_dataset``` function is a function to read the raw FAQ data from the FAQ and the README documents of the Kubernetes project and to generate the ```corpus_file.jsonl``` and ```query_file.jsonl``` for the tuning job." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "6958fb25e38f" + }, + "outputs": [], + "source": [ + "# Helper function to generate JSONL-format datasets for search tuning.\n", + "#\n", + "# Args:\n", + "# source_file (string): Path to the source files from which the JSONL\n", + "# files will be generated\n", + "# corpus_filepath (string): Path to the corpus file to which the JSONL\n", + "# formated corpus dataset file will be stored.\n", + "# query_filepath (string): Path to the query file to which the JSONL\n", + "# formated query dataset file will be stored.\n", + "# cleanup_at_start (bool, default=True): Clears the corpus files and\n", + "# the query files generated in the previous time\n", + "# before generating new datasets when the value\n", + "# is True\n", + "#\n", + "# Raises:\n", + "#\n", + "# Returns:\n", + "# No return values. Two JSONL files. (Corpus File, Query File)\n", + "\n", + "\n", + "def generate_source_dataset(\n", + " source_file: str,\n", + " corpus_filepath: str,\n", + " query_filepath: str,\n", + " cleanup_at_start: bool = True,\n", + "):\n", + " questions = []\n", + " answers = []\n", + "\n", + " # If cleanup_at_start is True, this section deletes the corpus files and\n", + " # the query files before generating new corpus and query files in JSONL\n", + " if cleanup_at_start:\n", + " if os.path.isfile(corpus_filepath):\n", + " print(f\"Removing previous file: %s\")\n", + " os.remove(corpus_filepath)\n", + " if os.path.isfile(query_filepath):\n", + " print(f\"Removing previous file: %s\")\n", + " os.remove(query_filepath)\n", + "\n", + " # This section generates a corpus file dataset in JSONL format\n", + " # from the source files.\n", + " logging.info(f\"{generate_source_dataset.__name__}: {1}\")\n", + " with open(source_file) as f:\n", + " line_str = f.readline()\n", + " answer = \"\"\n", + " answer_flag = False\n", + " while line_str:\n", + " if re.match(r\"^(#{3})\\s+(.+)$\", line_str):\n", + " question = re.split(r\"^(#{3})\\s+(.+)$\", line_str)\n", + " question_str = \"\"\n", + " len_question = len(question) - 1\n", + " reidx = 0\n", + " while not (question[len_question - reidx] == \"###\"):\n", + " question_str += question[len_question - reidx]\n", + " reidx += 1\n", + " questions.append(question_str)\n", + " # print(\"Question: %s\" % question_str)\n", + " answer_flag = True\n", + " answers.append(str.strip(answer, \"\"))\n", + " # print(\"Answer: %s\" % answer)\n", + " answer = \"\"\n", + " elif answer_flag == True:\n", + " answer += line_str\n", + " line_str = f.readline()\n", + "\n", + " logging.info(f\"{generate_source_dataset.__name__}: {2}\")\n", + " corpus_idx_start = 0\n", + " try:\n", + " with open(corpus_filepath) as cf:\n", + " corpus_idx_start = len(list(enumerate(cf)))\n", + " except:\n", + " corpus_idx_start = 0\n", + "\n", + " with open(corpus_filepath, \"a\") as cf:\n", + " jsonfile = \"\"\n", + " idx = corpus_idx_start\n", + " print(f\"start idx:%d\" % idx)\n", + " for answer in answers:\n", + " idx += 1\n", + " answer = answer.replace(\"\\\\[\", \"\\\\\\\\[\")\n", + " answer = answer.replace(\"\\\\]\", \"\\\\\\\\]\")\n", + " answer = answer.replace('\"', '\\\\\"')\n", + " json_line = '{{\"_id\": \"ans{:04d}\", \"text\": \"{}\" }}\\n'.format(\n", + " idx, str.strip(answer).replace(\"\\n\", \" \")\n", + " )\n", + " jsonfile += json_line\n", + " cf.writelines(jsonfile)\n", + "\n", + " # This section generates a query file dataset in JSONL format\n", + " # from the source files.\n", + " logging.info(f\"{generate_source_dataset.__name__}: {3}\")\n", + " query_idx_start = 0\n", + " try:\n", + " with open(query_filepath) as qf:\n", + " query_idx_start = len(list(enumerate(qf)))\n", + " except:\n", + " query_idx_start = 0\n", + "\n", + " with open(query_filepath, \"a\") as qf:\n", + " jsonfile = \"\"\n", + " idx = query_idx_start\n", + " print(f\"start idx:%d\" % idx)\n", + " for question in questions:\n", + " idx += 1\n", + " question = question.replace(\"\\\\[\", \"\\\\\\\\[\")\n", + " question = question.replace(\"\\\\]\", \"\\\\\\\\]\")\n", + " question = question.replace('\"', '\\\\\"')\n", + " json_line = '{{ \"_id\": \"que{:04d}\", \"text\": \"{}\" }}\\n'.format(\n", + " idx, str.strip(question).replace(\"\\n\", \" \")\n", + " )\n", + " jsonfile += json_line\n", + " qf.writelines(jsonfile)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "180fc88a543e" + }, + "source": [ + "This ```generate_training_test_dataset``` generates the query-answer mapping in a tab-separated value format to help the tuning job to map the queries and the texts for the answers to the queries from the FAQ." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "c241fd547498" + }, + "outputs": [], + "source": [ + "# Helper function to generate TSV(tab separated values)-format datasets\n", + "# to map the corpus file and the query file for search tuning.\n", + "#\n", + "# Args:\n", + "# corpus_filepath (string): Path to the corpus file to which the JSONL\n", + "# formated corpus dataset file will be stored.\n", + "# query_filepath (string): Path to the query file to which the JSONL\n", + "# formated query dataset file will be stored.\n", + "# training_filepath (string): Path to the mapping file between the entries\n", + "# in the corpus file and the query file to define\n", + "# a training dataset in TSV-format\n", + "# test_filepath (string): Path to the mapping file between the entries\n", + "# in the corpus file and the query file to define\n", + "# a test dataset in TSV-format\n", + "# cleanup_at_start (bool, default=True): Clears the training dataset files and\n", + "# the test dataset files generated in the previous time\n", + "# before generating new datasets when the value is True\n", + "#\n", + "# Raises:\n", + "#\n", + "# Returns:\n", + "# No return values. Two TSV files. (Training Dataset File, Test Dataset File)\n", + "\n", + "\n", + "def generate_training_test_dataset(\n", + " corpus_filepath: str,\n", + " query_filepath: str,\n", + " training_filepath: str,\n", + " test_filepath: str,\n", + " cleanup_at_start: bool = True,\n", + "):\n", + " questions = []\n", + " answers = []\n", + "\n", + " # If cleanup_at_start is True, this section deletes the training dataset files\n", + " # and the test dataset files before generating new TSV dataset files\n", + "\n", + " if cleanup_at_start:\n", + " if os.path.isfile(training_filepath):\n", + " print(f\"Removing previous file: %s\")\n", + " os.remove(training_filepath)\n", + " if os.path.isfile(test_filepath):\n", + " print(f\"Removing previous file: %s\")\n", + " os.remove(test_filepath)\n", + "\n", + " # Opens the corpus dataset file to generate the mapping between the corpus entries\n", + " # and the query entries\n", + "\n", + " with open(corpus_filepath) as corpus_file:\n", + " line_str = corpus_file.readline()\n", + " while line_str:\n", + " jsonl = json.loads(line_str, strict=False)\n", + " questions.append(jsonl[\"text\"])\n", + " line_str = corpus_file.readline()\n", + "\n", + " logging.info(f\"{generate_training_test_dataset.__name__}: {1}\")\n", + "\n", + " # Opens the query dataset file to generate the mapping between the corpus entries\n", + " # and the query entries\n", + "\n", + " with open(query_filepath) as query_file:\n", + " line_str = query_file.readline()\n", + " while line_str:\n", + " jsonl = json.loads(line_str, strict=False)\n", + " answers.append(jsonl[\"text\"])\n", + " line_str = query_file.readline()\n", + "\n", + " logging.info(f\"{generate_training_test_dataset.__name__}: {2}\")\n", + "\n", + " # Opens the training dataset file to generate the mapping between the corpus entries\n", + " # and the query entries\n", + "\n", + " with open(training_filepath, \"a\") as trf:\n", + " jsonfile = \"\"\n", + " json_line = \"query-id\\tcorpus-id\\tscore\\n\"\n", + " idx = 1\n", + " jsonfile += json_line\n", + " len_questions = len(questions)\n", + " for question in questions:\n", + " json_line = f\"que{idx:04d}\\tans{idx:04d}\\t1\\n\"\n", + " jsonfile += json_line\n", + " idx = idx + 1\n", + " if idx > 0.85 * len_questions:\n", + " break\n", + " trf.write(jsonfile)\n", + "\n", + " logging.info(f\"{generate_training_test_dataset.__name__}: {3}\")\n", + "\n", + " # Opens the test dataset file to generate the mapping between the corpus entries\n", + " # and the query entries\n", + "\n", + " with open(test_filepath, \"a\") as tef:\n", + " jsonfile = \"\"\n", + " json_line = \"query-id\\tcorpus-id\\tscore\\n\"\n", + " idx = 1\n", + " len_questions = len(questions)\n", + " jsonfile += json_line\n", + " for question in questions:\n", + " if idx <= 0.85 * len_questions:\n", + " idx = idx + 1\n", + " elif idx > 0.85 * len_questions and idx <= len_questions:\n", + " json_line = f\"que{idx:04d}\\tans{idx:04d}\\t1\\n\"\n", + " jsonfile += json_line\n", + " idx = idx + 1\n", + " tef.write(jsonfile)\n", + "\n", + " logging.info(f\"{generate_training_test_dataset.__name__}: {4}\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "038afd6614f5" + }, + "outputs": [], + "source": [ + "# Collects the generated JSONL and TSV files with PDF documents\n", + "# to update the search index after the search tuning\n", + "\n", + "\n", + "if __name__ == \"__main__\":\n", + " datasets = [\n", + " \"./tuning_data/FAQ.md\",\n", + " \"./tuning_data/FAQ-Kubernetes-Client.md\",\n", + " \"./tuning_data/README.md\",\n", + " ]\n", + " if os.path.isfile(\"./tuning_data/corpus_file.jsonl\"):\n", + " os.remove(\"./tuning_data/corpus_file.jsonl\")\n", + " if os.path.isfile(\"./tuning_data/query_file.jsonl\"):\n", + " os.remove(\"./tuning_data/query_file.jsonl\")\n", + "\n", + " for file in datasets:\n", + " print(file)\n", + " generate_source_dataset(\n", + " file,\n", + " \"./tuning_data/corpus_file.jsonl\",\n", + " \"./tuning_data/query_file.jsonl\",\n", + " cleanup_at_start=False,\n", + " )\n", + " generate_training_test_dataset(\n", + " \"./tuning_data/corpus_file.jsonl\",\n", + " \"./tuning_data/query_file.jsonl\",\n", + " \"./tuning_data/training_data.tsv\",\n", + " \"./tuning_data/test_data.tsv\",\n", + " )" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "2be63d1eccb3" + }, + "source": [ + "We create pdf files for the FAQ documents which are importable to the datastore of Vertex AI Search." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "8609d45b559b" + }, + "outputs": [], + "source": [ + "# Check if the system is macOS\n", + "if platform.system() == \"Darwin\":\n", + " # Install using Homebrew\n", + " !brew install xelatex # xelatex is used for pdf document creation in macOS.\n", + " !pandoc --pdf-engine=xelatex ./tuning_data/FAQ-Kubernetes-Client.md -o ./tuning_data/FAQ-Kubernetes-Client.pdf\n", + " !pandoc --pdf-engine=xelatex ./tuning_data/FAQ.md -o ./tuning_data/FAQ.pdf\n", + " !pandoc --pdf-engine=xelatex ./tuning_data/README.md -o ./tuning_data/README.pdf\n", + "elif platform.system() == \"Linux\":\n", + " # Install using apt-get for Ubuntu Linux\n", + " !sudo apt-get install pdflatex # pdflatex is used for pdf document creation in macOS.\n", + " !pandoc --pdf-engine=pdflatex ./tuning_data/FAQ-Kubernetes-Client.md -o ./tuning_data/FAQ-Kubernetes-Client.pdf\n", + " !pandoc --pdf-engine=pdflatex ./tuning_data/FAQ.md -o ./tuning_data/FAQ.pdf\n", + " !pandoc --pdf-engine=pdflatex ./tuning_data/README.md -o ./tuning_data/README.pdf" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "ecaab399efba" + }, + "source": [ + "After generating the test tuning datasets, we will upload the datasets to the bucket in Cloud Storage which will be used as a data store for the search tuning." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "fc0b7bf1960f" + }, + "outputs": [], + "source": [ + "# Uploading the preprocessed data with the PDF files for reindexing to the search app data store\n", + "!echo \"Preprocessed tuning data: {TUNING_DATA_PATH_LOCAL}\"\n", + "!echo \"Destination path: {TUNING_DATA_PATH_REMOTE}\"\n", + "!gcloud storage folders create \"{TUNING_DATA_PATH_REMOTE}\"\n", + "!gcloud storage cp $TUNING_DATA_PATH_LOCAL/* $TUNING_DATA_PATH_REMOTE" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "30c11d2bc24c" + }, + "source": [ + "## Uploading data for a search app datastore (papers on RLHF)\n", + "\n", + "To create a Vertex AI search app, we will upload some pdf files on Reinforcement Learning on Human Feedback from [Awesome RLHF](https://github.com/opendilab/awesome-RLHF.git) github repository to a bucket in Cloud Storage which will be used as a search datastore. The pdf files are available at [Awesome RLHF - PDF Files](https://gitlab.com/jincheolkim/awesome-rlhf)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "e98fc0c02b96" + }, + "outputs": [], + "source": [ + "!echo {SEARCH_DATASTORE_PATH_REMOTE}\n", + "!gcloud storage folders create \"{SEARCH_DATASTORE_PATH_REMOTE}\"\n", + "!gcloud storage cp --recursive $BASE_DATA_SOURCE_URI/* $SEARCH_DATASTORE_PATH_REMOTE" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "772abea3560e" + }, + "source": [ + "## Helper functions to facilitate the following steps\n", + "\n", + "The following functions are helper functions to help you clear the steps to perform the search tuning without distraction on other details.\n", + "\n", + "* ```create_data_store```: function creates a datastore for an agent app with the identifier of a datastore with the ```data_store_id``` and the ```data_store_name```\n", + "* ```import_documents```: function imports documents from Cloud Storage to generate indices\n", + "* ```create_search_engine```: function creates a search agent app\n", + "* ```search```: function to perform a query with the query given through the argument\n", + "* ```train_custom_model```: function to tune the backend LLM for the search agent app\n", + "* ```delete_engine```: function to delete the search agent app\n", + "* ```purge_documents```: function to delete the index and the documents indexed for the search agent app\n", + "* ```delete_data_store```: function to delete the data store\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "9cfd1a1f9da2" + }, + "outputs": [], + "source": [ + "# For more information, refer to:\n", + "# https://cloud.google.com/generative-ai-app-builder/docs/locations#specify_a_multi-region_for_your_data_store\n", + "search_client_options = (\n", + " ClientOptions(api_endpoint=f\"{LOCATION}-discoveryengine.googleapis.com\")\n", + " if LOCATION != \"global\"\n", + " else None\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "0b3005f1e60e" + }, + "outputs": [], + "source": [ + "def create_data_store(\n", + " project_id: str,\n", + " location: str,\n", + " data_store_id: str,\n", + " data_store_name: str,\n", + " client_options: ClientOptions = search_client_options,\n", + ") -> str:\n", + " client = discoveryengine.DataStoreServiceClient(client_options=client_options)\n", + "\n", + " # The full resource name of the collection\n", + " # e.g. projects/{project}/locations/{location}/collections/default_collection\n", + " parent = client.collection_path(\n", + " project=project_id,\n", + " location=location,\n", + " collection=\"default_collection\",\n", + " )\n", + "\n", + " data_store = discoveryengine.DataStore(\n", + " display_name=data_store_name,\n", + " # Options: GENERIC, MEDIA, HEALTHCARE_FHIR\n", + " industry_vertical=discoveryengine.IndustryVertical.GENERIC,\n", + " # Options: SOLUTION_TYPE_RECOMMENDATION, SOLUTION_TYPE_SEARCH, SOLUTION_TYPE_CHAT, SOLUTION_TYPE_GENERATIVE_CHAT\n", + " solution_types=[discoveryengine.SolutionType.SOLUTION_TYPE_SEARCH],\n", + " # Options: NO_CONTENT, CONTENT_REQUIRED, PUBLIC_WEBSITE\n", + " content_config=discoveryengine.DataStore.ContentConfig.CONTENT_REQUIRED,\n", + " )\n", + "\n", + " request = discoveryengine.CreateDataStoreRequest(\n", + " parent=parent,\n", + " data_store_id=data_store_id,\n", + " data_store=data_store,\n", + " # Optional: For Advanced Site Search Only\n", + " # create_advanced_site_search=True,\n", + " )\n", + "\n", + " # Make the request\n", + " operation = client.create_data_store(request=request)\n", + "\n", + " print(f\"Waiting for operation to complete: {operation.operation.name}\")\n", + " response = operation.result()\n", + "\n", + " # After the operation is complete,\n", + " # get information from operation metadata\n", + " metadata = discoveryengine.CreateDataStoreMetadata(operation.metadata)\n", + "\n", + " # Handle the response\n", + " print(response)\n", + " print(metadata)\n", + "\n", + " return operation.operation.name" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "e93b7aa62243" + }, + "outputs": [], + "source": [ + "def import_documents(\n", + " project_id: str,\n", + " location: str,\n", + " data_store_id: str,\n", + " client_options: ClientOptions = search_client_options,\n", + ") -> discoveryengine.PurgeDocumentsMetadata:\n", + " client = discoveryengine.DocumentServiceClient(client_options=client_options)\n", + "\n", + " # The full resource name of the search engine branch.\n", + " # e.g. projects/{project}/locations/{location}/dataStores/{data_store_id}/branches/{branch}\n", + " parent = client.branch_path(\n", + " project=project_id,\n", + " location=location,\n", + " data_store=data_store_id,\n", + " branch=\"default_branch\",\n", + " )\n", + "\n", + " # With the new datastore, we will import and make an index over the documents in the datastore.\n", + " # The ```ImportDocumentsRequests``` generates a REST API request message in the JSON format\n", + " # and the ```import_documents``` method of the DocumentServiceClient class lets you import\n", + " # the documents and make an index over the document set with the information\n", + " # in the ```ImportDocumentRequest``` request.\n", + " document_import_request = discoveryengine.ImportDocumentsRequest(\n", + " parent=parent,\n", + " gcs_source=discoveryengine.GcsSource(\n", + " # Multiple URIs are supported\n", + " input_uris=[f\"{SEARCH_DATASTORE_PATH_REMOTE}/*\"],\n", + " # Options:\n", + " # - `content` - Unstructured documents (PDF, HTML, DOC, TXT, PPTX)\n", + " # - `custom` - Unstructured documents with custom JSONL metadata\n", + " # - `document` - Structured documents in the discoveryengine.Document format.\n", + " # - `csv` - Unstructured documents with CSV metadata\n", + " data_schema=\"content\",\n", + " ),\n", + " # Options: `FULL`, `INCREMENTAL`\n", + " reconciliation_mode=discoveryengine.ImportDocumentsRequest.ReconciliationMode.INCREMENTAL,\n", + " )\n", + "\n", + " # Make the request\n", + " operation = client.import_documents(request=document_import_request)\n", + "\n", + " print(f\"Waiting for operation to complete: {operation.operation.name}\")\n", + "\n", + " # After the operation is complete,\n", + " # get information from operation metadata\n", + " response = operation.result()\n", + " metadata = discoveryengine.ImportDocumentsMetadata(operation.metadata)\n", + "\n", + " # Handle the response\n", + " print(response)\n", + " print(metadata)\n", + "\n", + " return metadata" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "31066e205da3" + }, + "outputs": [], + "source": [ + "def create_search_engine(\n", + " project_id: str,\n", + " location: str,\n", + " engine_name: str,\n", + " engine_id: str,\n", + " data_store_ids: list[str],\n", + " client_options: ClientOptions = search_client_options,\n", + ") -> str:\n", + " client = discoveryengine.EngineServiceClient(client_options=client_options)\n", + "\n", + " # The full resource name of the collection\n", + " # e.g. projects/{project}/locations/{location}/collections/default_collection\n", + " parent = client.collection_path(\n", + " project=project_id,\n", + " location=location,\n", + " collection=\"default_collection\",\n", + " )\n", + "\n", + " engine = discoveryengine.Engine(\n", + " display_name=engine_name,\n", + " # Options: GENERIC, MEDIA, HEALTHCARE_FHIR\n", + " industry_vertical=discoveryengine.IndustryVertical.GENERIC,\n", + " # Options: SOLUTION_TYPE_RECOMMENDATION, SOLUTION_TYPE_SEARCH, SOLUTION_TYPE_CHAT, SOLUTION_TYPE_GENERATIVE_CHAT\n", + " solution_type=discoveryengine.SolutionType.SOLUTION_TYPE_SEARCH,\n", + " # For search apps only\n", + " search_engine_config=discoveryengine.Engine.SearchEngineConfig(\n", + " # Options: SEARCH_TIER_STANDARD, SEARCH_TIER_ENTERPRISE\n", + " search_tier=discoveryengine.SearchTier.SEARCH_TIER_ENTERPRISE,\n", + " # Options: SEARCH_ADD_ON_LLM, SEARCH_ADD_ON_UNSPECIFIED\n", + " search_add_ons=[discoveryengine.SearchAddOn.SEARCH_ADD_ON_LLM],\n", + " ),\n", + " # For generic recommendation apps only\n", + " # similar_documents_config=discoveryengine.Engine.SimilarDocumentsEngineConfig,\n", + " data_store_ids=data_store_ids,\n", + " )\n", + "\n", + " request = discoveryengine.CreateEngineRequest(\n", + " parent=parent,\n", + " engine=engine,\n", + " engine_id=engine_id,\n", + " )\n", + "\n", + " # Make the request\n", + " operation = client.create_engine(request=request)\n", + "\n", + " print(f\"Waiting for operation to complete: {operation.operation.name}\")\n", + " response = operation.result()\n", + "\n", + " # After the operation is complete,\n", + " # get information from operation metadata\n", + " metadata = discoveryengine.CreateEngineMetadata(operation.metadata)\n", + "\n", + " # Handle the response\n", + " print(response)\n", + " print(metadata)\n", + "\n", + " return operation.operation.name" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "726bf37f458a" + }, + "outputs": [], + "source": [ + "def search(\n", + " project_id: str,\n", + " location: str,\n", + " engine_id: str,\n", + " search_query: str,\n", + " client_options: ClientOptions = search_client_options,\n", + ") -> list[discoveryengine.SearchResponse]:\n", + " client = discoveryengine.SearchServiceClient(client_options=client_options)\n", + "\n", + " # The full resource name of the search app serving config\n", + " serving_config = f\"projects/{project_id}/locations/{location}/collections/default_collection/engines/{engine_id}/servingConfigs/default_config\"\n", + "\n", + " # Optional - only supported for unstructured data: Configuration options for search.\n", + " # Refer to the `ContentSearchSpec` reference for all supported fields:\n", + " # https://cloud.google.com/python/docs/reference/discoveryengine/latest/google.cloud.discoveryengine_v1.types.SearchRequest.ContentSearchSpec\n", + " content_search_spec = discoveryengine.SearchRequest.ContentSearchSpec(\n", + " # For information about snippets, refer to:\n", + " # https://cloud.google.com/generative-ai-app-builder/docs/snippets\n", + " snippet_spec=discoveryengine.SearchRequest.ContentSearchSpec.SnippetSpec(\n", + " return_snippet=True\n", + " ),\n", + " # For information about search summaries, refer to:\n", + " # https://cloud.google.com/generative-ai-app-builder/docs/get-search-summaries\n", + " summary_spec=discoveryengine.SearchRequest.ContentSearchSpec.SummarySpec(\n", + " summary_result_count=5,\n", + " include_citations=True,\n", + " ignore_adversarial_query=True,\n", + " ignore_non_summary_seeking_query=True,\n", + " model_prompt_spec=discoveryengine.SearchRequest.ContentSearchSpec.SummarySpec.ModelPromptSpec(\n", + " preamble=\"YOUR_CUSTOM_PROMPT\"\n", + " ),\n", + " model_spec=discoveryengine.SearchRequest.ContentSearchSpec.SummarySpec.ModelSpec(\n", + " version=\"stable\",\n", + " ),\n", + " ),\n", + " )\n", + "\n", + " # Refer to the `SearchRequest` reference for all supported fields:\n", + " # https://cloud.google.com/python/docs/reference/discoveryengine/latest/google.cloud.discoveryengine_v1.types.SearchRequest\n", + " request = discoveryengine.SearchRequest(\n", + " serving_config=serving_config,\n", + " query=search_query,\n", + " page_size=10,\n", + " content_search_spec=content_search_spec,\n", + " query_expansion_spec=discoveryengine.SearchRequest.QueryExpansionSpec(\n", + " condition=discoveryengine.SearchRequest.QueryExpansionSpec.Condition.AUTO,\n", + " ),\n", + " spell_correction_spec=discoveryengine.SearchRequest.SpellCorrectionSpec(\n", + " mode=discoveryengine.SearchRequest.SpellCorrectionSpec.Mode.AUTO\n", + " ),\n", + " )\n", + "\n", + " response = client.search(request)\n", + " print(response)\n", + "\n", + " return response" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "23b351587b01" + }, + "outputs": [], + "source": [ + "def train_custom_model(\n", + " project_id: str,\n", + " location: str,\n", + " data_store_id: str,\n", + " corpus_data_path: str,\n", + " query_data_path: str,\n", + " train_data_path: str,\n", + " test_data_path: str,\n", + " client_options: ClientOptions = search_client_options,\n", + ") -> Operation:\n", + " client = discoveryengine.SearchTuningServiceClient(client_options=client_options)\n", + "\n", + " # The full resource name of the data store\n", + " data_store = f\"projects/{project_id}/locations/{location}/collections/default_collection/dataStores/{data_store_id}\"\n", + "\n", + " # Make the request\n", + " operation = client.train_custom_model(\n", + " request=discoveryengine.TrainCustomModelRequest(\n", + " gcs_training_input=discoveryengine.TrainCustomModelRequest.GcsTrainingInput(\n", + " corpus_data_path=corpus_data_path,\n", + " query_data_path=query_data_path,\n", + " train_data_path=train_data_path,\n", + " test_data_path=test_data_path,\n", + " ),\n", + " data_store=data_store,\n", + " model_type=\"search-tuning\",\n", + " )\n", + " )\n", + "\n", + " # Optional: Wait for training to complete\n", + " print(f\"Waiting for operation to complete: {operation.operation.name}\")\n", + " response = operation.result()\n", + "\n", + " # After the operation is complete,\n", + " # get information from operation metadata\n", + " metadata = discoveryengine.TrainCustomModelMetadata(operation.metadata)\n", + "\n", + " # Handle the response\n", + " print(response)\n", + " print(metadata)\n", + " print(operation)\n", + "\n", + " return operation" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "4431a0cac4f5" + }, + "outputs": [], + "source": [ + "def delete_engine(\n", + " project_id: str,\n", + " location: str,\n", + " engine_id: str,\n", + " client_options: ClientOptions = search_client_options,\n", + ") -> str:\n", + " client = discoveryengine.EngineServiceClient(client_options=client_options)\n", + "\n", + " # The full resource name of the engine\n", + " # e.g. projects/{project}/locations/{location}/collections/default_collection/engines/{engine_id}\n", + " name = client.engine_path(\n", + " project=project_id,\n", + " location=location,\n", + " collection=\"default_collection\",\n", + " engine=engine_id,\n", + " )\n", + "\n", + " # Make the request\n", + " operation = client.delete_engine(name=name)\n", + "\n", + " print(f\"Operation: {operation.operation.name}\")\n", + "\n", + " return operation.operation.name" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "d695940875b4" + }, + "outputs": [], + "source": [ + "def purge_documents(\n", + " project_id: str,\n", + " location: str,\n", + " data_store_id: str,\n", + " client_options: ClientOptions = search_client_options,\n", + ") -> discoveryengine.PurgeDocumentsMetadata:\n", + " client = discoveryengine.DocumentServiceClient(client_options=client_options)\n", + "\n", + " operation = client.purge_documents(\n", + " request=discoveryengine.PurgeDocumentsRequest(\n", + " # The full resource name of the search engine branch.\n", + " # e.g. projects/{project}/locations/{location}/dataStores/{data_store_id}/branches/{branch}\n", + " parent=client.branch_path(\n", + " project=project_id,\n", + " location=location,\n", + " data_store=data_store_id,\n", + " branch=\"default_branch\",\n", + " ),\n", + " filter=\"*\",\n", + " # If force is set to `False`, return the expected purge count without deleting any documents.\n", + " force=True,\n", + " )\n", + " )\n", + "\n", + " print(f\"Waiting for operation to complete: {operation.operation.name}\")\n", + " response = operation.result()\n", + "\n", + " # After the operation is complete,\n", + " # get information from operation metadata\n", + " metadata = discoveryengine.PurgeDocumentsMetadata(operation.metadata)\n", + "\n", + " # Handle the response\n", + " print(response)\n", + " print(metadata)\n", + "\n", + " return metadata" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "dea04a3478cb" + }, + "outputs": [], + "source": [ + "def delete_data_store(\n", + " project_id: str,\n", + " location: str,\n", + " data_store_id: str,\n", + " client_options: ClientOptions = search_client_options,\n", + ") -> str:\n", + " client = discoveryengine.DataStoreServiceClient(client_options=client_options)\n", + "\n", + " request = discoveryengine.DeleteDataStoreRequest(\n", + " # The full resource name of the data store\n", + " name=client.data_store_path(project_id, location, data_store_id)\n", + " )\n", + "\n", + " # Make the request\n", + " operation = client.delete_data_store(request=request)\n", + "\n", + " print(f\"Operation: {operation.operation.name}\")\n", + "\n", + " return operation.operation.name" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "df2d7e8fc6e6" + }, + "source": [ + "## Creating a data store for a search app with the cloud storage bucket with PDF documents\n", + "\n", + "We create a datastore with the datastore bucket in Cloud Storage with the PDF files on RLHF and import them to generate indices for search." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "588af607bc6d" + }, + "outputs": [], + "source": [ + "!echo \"Datastore ID: {SEARCH_DATASTORE_ID}\"\n", + "!echo \"Datastore Name: {SEARCH_DATASTORE_NAME}\"\n", + "create_datastore_op_name = create_data_store(\n", + " PROJECT_ID, LOCATION, SEARCH_DATASTORE_ID, SEARCH_DATASTORE_NAME\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "765dcf64879d" + }, + "outputs": [], + "source": [ + "metadata = import_documents(PROJECT_ID, LOCATION, SEARCH_DATASTORE_ID)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "acdb037fce28" + }, + "source": [ + "## Creating a search app using the Vertex AI Search SDK\n", + "\n", + "As we just created a datastore and made an index over the documents in it in the above, we will create a search app with the datastore. \n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "f549f8d5091f" + }, + "outputs": [], + "source": [ + "SEARCH_DATASTORE_REF_ID = f\"projects/{PROJECT_ID}/locations/{LOCATION}/collections/default_collection/dataStores/{SEARCH_DATASTORE_ID}\"\n", + "SEARCH_APP_ID = f\"search-app-{PROJECT_ID}-{shortuuid.uuid().lower()}\"\n", + "SEARCH_APP_NAME = \"RLHF_SEARCH_APP\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "10a707979daf" + }, + "outputs": [], + "source": [ + "create_search_app_op_name = create_search_engine(\n", + " PROJECT_ID, LOCATION, SEARCH_APP_NAME, SEARCH_APP_ID, [SEARCH_DATASTORE_ID]\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "3862d318d1ad" + }, + "source": [ + "### Test the search app with a test prompt\n", + "\n", + "We will test the search app we just created with information about a paper regarding a world model for autonomous driving which is described in a paper among the documents in the datastore." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "68365934e4b2" + }, + "outputs": [], + "source": [ + "QUERY_PROMPT = \"\"\"\n", + " What is the name of the world model for autonomous driving developed recently?\n", + "\"\"\"" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "bad64c16adca" + }, + "source": [ + "We can see that the search app returns a list of relevant documents with references to the related documents in the datastore. Please keep the \n", + "result in your mind to compare it with the results after the search tuning is performed." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "fc7cd4f71bbc" + }, + "outputs": [], + "source": [ + "search_response = search(PROJECT_ID, LOCATION, SEARCH_APP_ID, QUERY_PROMPT)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "3c307dc82f9f" + }, + "source": [ + "## Configuring and submitting a search tuning job\n", + "\n", + "With the search app ready, we will perform a search tuning with a test tuning data on Kubernetes.\n", + "\n", + "First, we will upload the documents of FAQs about Kubernetes and Kubernetes Client API. The original documents were in the Markdown format but we transform them to PDF format files as the Vertex AI Search cannot accept Markdown files but only HTML, PDF and PDF with embedded text, TXT, JSON, XHTML, and XML format. PPTX, DOCX and XLSX formats are available in Preview. The PDF files are uploaded to the buckets for the datastore of the search app." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "5bd5eb9e64ee" + }, + "source": [ + "#### Uploading the additional PDF files for tuning to the bucket of the datastore" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "0741318fcd32" + }, + "outputs": [], + "source": [ + "!gcloud storage cp {TUNING_DATA_PATH_LOCAL}/*.jsonl \"{TUNING_DATA_PATH_REMOTE}\"\n", + "!gcloud storage cp {TUNING_DATA_PATH_LOCAL}/*.tsv \"{TUNING_DATA_PATH_REMOTE}\"\n", + "!gcloud storage ls \"{TUNING_DATA_PATH_REMOTE}\"" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "53d377765df5" + }, + "source": [ + "#### Uploading the datasets for the search tuning and perform the tuning\n", + "\n", + "These are the information on the tuning dataset files to be used to tune the backend LLM behind the search app. Please refer to the [Prepare data for ingesting](https://cloud.google.com/generative-ai-app-builder/docs/prepare-data#website) in the Google Cloud Documentation." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "b91ea4e8b7a5" + }, + "outputs": [], + "source": [ + "data_store_id = f\"{SEARCH_DATASTORE_ID}\"\n", + "corpus_data_path = f\"{TUNING_DATA_PATH_REMOTE}/corpus_file.jsonl\"\n", + "query_data_path = f\"{TUNING_DATA_PATH_REMOTE}/query_file.jsonl\"\n", + "train_data_path = f\"{TUNING_DATA_PATH_REMOTE}/training_data.tsv\"\n", + "test_data_path = f\"{TUNING_DATA_PATH_REMOTE}/test_data.tsv\"" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "1a44073dbafc" + }, + "source": [ + "This ```train_custom_model``` function is to submit a search tuning job with the datasets we just prepared." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "47206888e8e1" + }, + "outputs": [], + "source": [ + "tuning_op = train_custom_model(\n", + " PROJECT_ID,\n", + " LOCATION,\n", + " data_store_id,\n", + " corpus_data_path,\n", + " query_data_path,\n", + " train_data_path,\n", + " test_data_path,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "a0e334074066" + }, + "source": [ + "We can see that three additional documents related to the tuning task was uploaded to the datastore bucket, ```FAQ-Kubernetes-Client.pdf, FAQ.pdf, README.pdf.``` With these new documents, we should perform the indexing again by calling the ```import_documents``` method of the client again." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "3c3d193b5c3e" + }, + "outputs": [], + "source": [ + "!gcloud storage cp \"{TUNING_DATA_PATH_LOCAL}/*.pdf\" \"{SEARCH_DATASTORE_PATH_REMOTE}\"\n", + "!gcloud storage ls \"{SEARCH_DATASTORE_PATH_REMOTE}\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "17191f107297" + }, + "outputs": [], + "source": [ + "metadata = import_documents(PROJECT_ID, LOCATION, SEARCH_DATASTORE_ID)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "32c8827f86e6" + }, + "source": [ + "#### Testing the tuned search app endpoint with a question on Kubernetes\n", + "\n", + "The tuning job will take about 30 to 60 minutes. After the tuning job completed, we test the search app with a query prompt regarding Kubernetes which is the information in the documents indexed additionally with the tuning." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "214b65078ce4" + }, + "outputs": [], + "source": [ + "QUERY_PROMPT = \"\"\"\n", + " How do I determine the status of a deployment of Kubernetes?\n", + "\"\"\"" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "aa567f3c38eb" + }, + "source": [ + "We can see that the information on the deployment of Kubernetes which was described in the FAQ documents are correctly returned with the new documents indexed in the tuning." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "cdbb758533ef" + }, + "outputs": [], + "source": [ + "search_response = search(PROJECT_ID, LOCATION, SEARCH_APP_ID, QUERY_PROMPT)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "f717e9cb30e7" + }, + "source": [ + "## Clean up\n", + "\n", + "We should clean up the deployed resources and data not to create unnecessary costs." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "2b2bf33f37c7" + }, + "source": [ + "#### Deleting the search app" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "82138114c3bb" + }, + "outputs": [], + "source": [ + "delete_search_app_op_name = delete_engine(PROJECT_ID, LOCATION, SEARCH_APP_ID)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "ba60668eb8bc" + }, + "source": [ + "#### Deleting the documents in the datastore" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "a76b43ef3767" + }, + "outputs": [], + "source": [ + "purge_document_metadata = purge_documents(PROJECT_ID, LOCATION, SEARCH_DATASTORE_ID)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "80f199d6f782" + }, + "source": [ + "#### Deleting the datastore" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "ac2f897cf840" + }, + "outputs": [], + "source": [ + "delete_datastore_op_name = delete_data_store(PROJECT_ID, LOCATION, SEARCH_DATASTORE_ID)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "2d0d394cb942" + }, + "outputs": [], + "source": [ + "!gcloud storage rm -r \"{BUCKET_URI}\"" + ] + } + ], + "metadata": { + "colab": { + "name": "vertexai-search-tuning.ipynb", + "toc_visible": true + }, + "kernelspec": { + "display_name": "Python 3", + "name": "python3" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} diff --git a/search/vais-building-blocks/README.md b/search/vais-building-blocks/README.md new file mode 100644 index 0000000..5a8d470 --- /dev/null +++ b/search/vais-building-blocks/README.md @@ -0,0 +1,48 @@ +# What is Vertex AI Search? + +Vertex AI Search (VAIS) is a fully-managed platform, powered by large +language models, that lets you build AI-enabled search and recommendation +experiences for your public or private sites or mobile applications +VAIS can handle a diverse set of data sources including structured, +unstructured, and site data, as well as data from third-party applications +such as Jira, Salesforce, and Confluence. +VAIS also has built-in integration with LLMs which enables you to provide +answers to complex questions, grounded in your data + +## Sample Notebooks + +This folder contains a series of notebooks to demonstrate how different +functionalities within Vertex AI Search can be used +We aim to keep these notebooks broader than a single API call and smaller +than a fully fledged application. +The notebooks are expected to serve as building blocks which can be +combined to achieve higher levels goals (e.g. ingest unstructured documents +with metadata and generate accurate answers based on that) +We will try to use REST APIs which will hopefully make the codes easier to +understand without a need to read through documentations of different object +types. For production use, many customer prefer Client libraries. Please consult + +The [official documentation](https://cloud.google.com/generative-ai-app-builder/docs/apis) for alternative ways of achieving the same goals. + +## List of Notebooks + +1. [Ingestion of Unstructured Documents with Metadata in Vertex AI Search] + (./ingesting_unstructured_documents_with_metadata.ipynb) + +2. [Parsing and Chunking in Vertex AI Search: Featuring BYO Capabilities] + (./parsing_and_chunking_with_BYO.ipynb) + +3. [Defining custom attributes based on URL patterns in Vertex AI Search + Site Datastores](./custom_attributes_by_url_pattern.ipynb) + +4. [Query-Level Boosting, Filtering, and Facets for Vertex AI Search Site + Datastores](./query_level_boosting_filtering_and_facets.ipynb) + +5. [Inline Ingestion of Documents into Vertex AI Search] + (./inline_ingestion_of_documents.ipynb) + +6. [Event-based Triggering of Manual Recrawl for Vertex AI Search Advanced + Site Datastores](./manual_recrawl_urls_with_trigger.ipynb) + +7. [Recording Real-Time User Events in Vertex AI Search Datastores] + (./record_user_events.ipynb) diff --git a/search/vais-building-blocks/custom_attributes_by_url_pattern.ipynb b/search/vais-building-blocks/custom_attributes_by_url_pattern.ipynb new file mode 100644 index 0000000..756a37b --- /dev/null +++ b/search/vais-building-blocks/custom_attributes_by_url_pattern.ipynb @@ -0,0 +1,918 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "5XNYlDkDLpqU" + }, + "outputs": [], + "source": [ + "# Copyright 2024 Google LLC\n", + "#\n", + "# Licensed under the Apache License, Version 2.0 (the \"License\");\n", + "# you may not use this file except in compliance with the License.\n", + "# You may obtain a copy of the License at\n", + "#\n", + "# https://www.apache.org/licenses/LICENSE-2.0\n", + "#\n", + "# Unless required by applicable law or agreed to in writing, software\n", + "# distributed under the License is distributed on an \"AS IS\" BASIS,\n", + "# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n", + "# See the License for the specific language governing permissions and\n", + "# limitations under the License." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "5tR528hOD4Dx" + }, + "source": [ + "# Defining custom attributes based on URL patterns in Vertex AI Search Website Datastores\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + "
    \n", + " \n", + " \"Google
    Open in Colab\n", + "
    \n", + "
    \n", + " \n", + " \"Google
    Open in Colab Enterprise\n", + "
    \n", + "
    \n", + " \n", + " \"Vertex
    Open in Vertex AI Workbench\n", + "
    \n", + "
    \n", + " \n", + " \"GitHub
    View on GitHub\n", + "
    \n", + "
    \n", + "\n", + "
    \n", + "\n", + "Share to:\n", + "\n", + "\n", + " \"LinkedIn\n", + "\n", + "\n", + "\n", + " \"Bluesky\n", + "\n", + "\n", + "\n", + " \"X\n", + "\n", + "\n", + "\n", + " \"Reddit\n", + "\n", + "\n", + "\n", + " \"Facebook\n", + "" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "pkd93iDpEBWx" + }, + "source": [ + "| | |\n", + "|----------|-------------|\n", + "| Author(s) | Hossein Mansour|\n", + "| Reviewers(s) | Ismail Najim, Rajesh Thallam|\n", + "| Last updated | 2024-08-09: The first draft |" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "yAnTektvEQjb" + }, + "source": [ + "# Overview\n", + "\n", + "In this notebook, we demonstrate how to create [custom attributes](https://cloud.google.com/generative-ai-app-builder/docs/reference/rest/v1alpha/projects.locations.collections.dataStores.siteSearchEngine/getUriPatternDocumentData) based on URL patterns in [Vertex AI Search](https://cloud.google.com/generative-ai-app-builder/docs/introduction) Website datastores.\n", + "\n", + "These custom attributes will act similarly to metadata from page source and can be used for different purposes such as improving recall and precision, influencing results via boosting and filtering, and including additional context to be retrieved together with the documents.\n", + "\n", + "You can find more information about different types of metadata [here](https://cloud.google.com/generative-ai-app-builder/docs/provide-schema#about_providing_your_own_schema_as_a_json_object).\n", + "\n", + "Custom attributes based on URL patterns are particularly helpful in cases where adjusting page source to include relevant information is not feasible due to a need to keep that information private or when organizational complexities make it difficult to influence the page source content (e.g., content being managed by a third party).\n", + "\n", + "Custom attributes can be used, in lieu of page source metadata, in conjunction with page source metadata, or to override poor quality page content via post-processing (e.g., a Title_Override custom attribute to override the actual page title for certain URLs).\n", + "\n", + "Note that basic URL-based [boosting](https://cloud.google.com/generative-ai-app-builder/docs/boost-search-results) and [filtering](https://cloud.google.com/generative-ai-app-builder/docs/filter-website-search#examples-advanced-indexing) can be done directly. Custom Attributes are intended for more advanced use cases.\n", + "\n", + "If the custom attribute is made searchable, it can be used to implicitly influence retrieval and ranking of the page by providing additional information such as tags and related topics.\n", + "\n", + "We will perform the following steps:\n", + "\n", + "- [Prerequisite] Creating a Vertex AI Search Website Datastore and Search App\n", + "- Setting Schema and URL mapping for Customer Attributes\n", + "- Getting Schema and URL mapping to confirm this is what we want\n", + "- Searching the Datastore and demonstrating how custom attributes can be used for filtering\n", + "- Clean up\n", + "\n", + "\n", + "Please refer to the [official documentation](https://cloud.google.com/generative-ai-app-builder/docs/create-datastore-ingest) for the definition of Datastores and Apps and their relationships to one another\n", + "\n", + "REST API is used throughout this notebook. Please consult the [official documentation](https://cloud.google.com/generative-ai-app-builder/docs/apis) for alternative ways to achieve the same goal, namely Client libraries and RPC.\n", + "\n", + "\n", + "# Vertex AI Search\n", + "Vertex AI Search (VAIS) is a fully-managed platform, powered by large language models, that lets you build AI-enabled search and recommendation experiences for your public or private websites or mobile applications\n", + "\n", + "VAIS can handle a diverse set of data sources including structured, unstructured, and website data, as well as data from third-party applications such as Jira, Salesforce, and Confluence.\n", + "\n", + "VAIS also has built-in integration with LLMs which enables you to provide answers to complex questions, grounded in your data\n", + "\n", + "# Using this Notebook\n", + "If you're running outside of Colab, depending on your environment you may need to install pip packages that are included in the Colab environment by default but are not part of the Python Standard Library. Outside of Colab you'll also notice comments in code cells that look like #@something, these trigger special Colab functionality but don't change the behavior of the notebook.\n", + "\n", + "This tutorial uses the following Google Cloud services and resources:\n", + "\n", + "- Service Usage API\n", + "- Discovery Engine API\n", + "\n", + "This notebook has been tested in the following environment:\n", + "\n", + "- Python version = 3.10.12\n", + "- google.cloud.storage = 2.8.0\n", + "- google.auth = 2.27.0\n", + "\n", + "# Getting Started\n", + "\n", + "The following steps are necessary to run this notebook, no matter what notebook environment you're using.\n", + "\n", + "If you're entirely new to Google Cloud, [get started here](https://cloud.google.com/docs/get-started)\n", + "\n", + "## Google Cloud Project Setup\n", + "\n", + "1. [Select or create a Google Cloud project](https://console.cloud.google.com/cloud-resource-manager). When you first create an account, you get a $300 free credit towards your compute/storage costs\n", + "2. [Make sure that billing is enabled for your project](https://cloud.google.com/billing/docs/how-to/modify-project)\n", + "3. [Enable the Service Usage API](https://console.cloud.google.com/apis/library/serviceusage.googleapis.com)\n", + "4. [Enable the Cloud Storage API](https://console.cloud.google.com/flows/enableapi?apiid=storage.googleapis.com)\n", + "5. [Enable the Discovery Engine API for your project](https://console.cloud.google.com/marketplace/product/google/discoveryengine.googleapis.com)\n", + "\n", + "## Google Cloud Permissions\n", + "\n", + "Ideally you should have [Owner role](https://cloud.google.com/iam/docs/understanding-roles) for your project to run this notebook. If that is not an option, you need at least the following [roles](https://cloud.google.com/iam/docs/granting-changing-revoking-access)\n", + "- **`roles/serviceusage.serviceUsageAdmin`** to enable APIs\n", + "- **`roles/iam.serviceAccountAdmin`** to modify service agent permissions\n", + "- **`roles/discoveryengine.admin`** to modify discoveryengine assets" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "49x_J4vWOuNg" + }, + "source": [ + "#Setup Environment" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "kMYYfGpyOl5G" + }, + "source": [ + "## Authentication\n", + "\n", + " If you're using Colab, run the code in the next cell. Follow the pop-ups and authenticate with an account that has access to your Google Cloud [project](https://cloud.google.com/resource-manager/docs/creating-managing-projects#identifying_projects).\n", + "\n", + "If you're running this notebook somewhere besides Colab, make sure your environment has the right Google Cloud access. If that's a new concept to you, consider looking into [Application Default Credentials for your local environment](https://cloud.google.com/docs/authentication/provide-credentials-adc#local-dev) and [initializing the Google Cloud CLI](https://cloud.google.com/docs/authentication/gcloud). In many cases, running `gcloud auth application-default login` in a shell on the machine running the notebook kernel is sufficient.\n", + "\n", + "More authentication options are discussed [here](https://cloud.google.com/docs/authentication)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "DZjtfEDG7Sr3" + }, + "outputs": [], + "source": [ + "# Colab authentication.\n", + "import sys\n", + "\n", + "if \"google.colab\" in sys.modules:\n", + " from google.colab import auth\n", + "\n", + " auth.authenticate_user()\n", + " print(\"Authenticated\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "kT3Eda7_mlTP" + }, + "outputs": [], + "source": [ + "from google.auth import default\n", + "from google.auth.transport.requests import AuthorizedSession\n", + "\n", + "creds, _ = default()\n", + "authed_session = AuthorizedSession(creds)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "otijhCIjOzk-" + }, + "source": [ + "## Import Libraries" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "DlIp4zv3cdA7" + }, + "outputs": [], + "source": [ + "import json\n", + "import time" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "N51y_mPgPHsj" + }, + "source": [ + "## Configure environment\n", + "\n", + "The Location of a Datastore is set at the time of creation and it should be called appropriately to query the Datastore. `global` is typically recommended unless you have a particular reason to use a regional Datastore.\n", + "\n", + "You can find more information regarding the `Location` of datastores and associated limitations [here](https://cloud.google.com/generative-ai-app-builder/docs/locations#specify_a_multi-region_for_your_data_store).\n", + "\n", + "`VAIS_BRANCH` is the branch of VAIS to use. At the time of writing this notebook, URL mapping for Custom Attributes is only available in [v1alpha](https://cloud.google.com/generative-ai-app-builder/docs/reference/rest/v1alpha/projects.locations.collections.dataStores.siteSearchEngine/getUriPatternDocumentData) of Discovery Engine API.\n", + "\n", + "\n", + "`INCLUDE_URL_PATTERN` is the pattern of a website to be included in the datastore, e.g. `www.example.com/*`, `www.example.com/abc/*`.\n", + "\n", + "Note that you need to [verify the ownership of a domain](https://cloud.google.com/generative-ai-app-builder/docs/domain-verification) to be able to index it." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "hKLBf1GqROW7" + }, + "outputs": [], + "source": [ + "PROJECT_ID = \"\" # @param {type: 'string'}\n", + "DATASTORE_ID = \"\" # @param {type: 'string'}\n", + "APP_ID = \"\" # @param {type: 'string'}\n", + "LOCATION = \"global\" # @param [\"global\", \"us\", \"eu\"]\n", + "VAIS_BRANCH = \"v1alpha\" # @param {type: 'string'}\n", + "INCLUDE_URL_PATTERN = \"\" # @param {type: 'string'}" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "Akk3C5vK8oG6" + }, + "source": [ + "# Step 1. [Prerequisite] Create a Website Search Datastore and APP\n", + "In this section we will programmatically create a VAIS [Advanced Website Datastore and APP](https://cloud.google.com/generative-ai-app-builder/docs/about-advanced-features#advanced-website-indexing). You can achieve the same goal with a [few clicks](https://cloud.google.com/generative-ai-app-builder/docs/website-search-checklist?indexing=advanced) in the UI.\n", + "\n", + "If you already have an Advanced Website Datastore available, you can skip this section.\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "C2hXlewDINDg" + }, + "source": [ + "## Helper functions to issue basic search on a Datastore or an App" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "v-XHQIOooshe" + }, + "outputs": [], + "source": [ + "def search_by_datastore(project_id: str, location: str, datastore_id: str, query: str):\n", + " \"\"\"Searches a datastore using the provided query.\"\"\"\n", + " response = authed_session.post(\n", + " f\"https://discoveryengine.googleapis.com/{VAIS_BRANCH}/projects/{project_id}/locations/{location}/collections/default_collection/dataStores/{datastore_id}/servingConfigs/default_search:search\",\n", + " headers={\n", + " \"Content-Type\": \"application/json\",\n", + " },\n", + " json={\"query\": query, \"pageSize\": 1},\n", + " )\n", + " return response\n", + "\n", + "\n", + "def search_by_app(project_id: str, location: str, app_id: str, query: str):\n", + " \"\"\"Searches an app using the provided query.\"\"\"\n", + " response = authed_session.post(\n", + " f\"https://discoveryengine.googleapis.com/v1/projects/{project_id}/locations/{location}/collections/default_collection/engines/{app_id}/servingConfigs/default_config:search\",\n", + " headers={\n", + " \"Content-Type\": \"application/json\",\n", + " },\n", + " json={\"query\": query, \"pageSize\": 1},\n", + " )\n", + " return response" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "eAigF6KHkMZ2" + }, + "source": [ + "## Helper functions to check whether or not a Datastore or an App already exist" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "IO1AxLZckXYK" + }, + "outputs": [], + "source": [ + "def datastore_exists(project_id: str, location: str, datastore_id: str) -> bool:\n", + " \"\"\"Check if a datastore exists.\"\"\"\n", + " response = search_by_datastore(project_id, location, datastore_id, \"test\")\n", + " status_code = response.status_code\n", + " # A 400 response is expected as the URL pattern needs to be set first\n", + " if status_code == 200 or status_code == 400:\n", + " return True\n", + " if status_code == 404:\n", + " return False\n", + " raise Exception(f\"Error: {status_code}\")\n", + "\n", + "\n", + "def app_exists(project_id: str, location: str, app_id: str) -> bool:\n", + " \"\"\"Check if an App exists.\"\"\"\n", + " response = search_by_app(project_id, location, app_id, \"test\")\n", + " status_code = response.status_code\n", + " if status_code == 200:\n", + " return True\n", + " if status_code == 404:\n", + " return False\n", + " raise Exception(f\"Error: {status_code}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "DYArgsiAiVfs" + }, + "source": [ + "## Helper functions to create a Datastore or an App" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "_0uAQTKD78_k" + }, + "outputs": [], + "source": [ + "def create_website_datastore(\n", + " vais_branch: str, project_id: str, location: str, datastore_id: str\n", + ") -> int:\n", + " \"\"\"Create a website datastore\"\"\"\n", + " payload = {\n", + " \"displayName\": datastore_id,\n", + " \"industryVertical\": \"GENERIC\",\n", + " \"solutionTypes\": [\"SOLUTION_TYPE_SEARCH\"],\n", + " \"contentConfig\": \"PUBLIC_WEBSITE\",\n", + " }\n", + " header = {\"X-Goog-User-Project\": project_id, \"Content-Type\": \"application/json\"}\n", + " es_endpoint = f\"https://discoveryengine.googleapis.com/{vais_branch}/projects/{project_id}/locations/{location}/collections/default_collection/dataStores?dataStoreId={datastore_id}\"\n", + " response = authed_session.post(\n", + " es_endpoint, data=json.dumps(payload), headers=header\n", + " )\n", + " if response.status_code == 200:\n", + " print(f\"The creation of Datastore {datastore_id} is initiated.\")\n", + " print(\"It may take a few minutes for the Datastore to become available\")\n", + " else:\n", + " print(f\"Failed to create Datastore {datastore_id}\")\n", + " print(response.text())\n", + " return response.status_code\n", + "\n", + "\n", + "def create_app(\n", + " vais_branch: str, project_id: str, location: str, datastore_id: str, app_id: str\n", + ") -> int:\n", + " \"\"\"Create a search app.\"\"\"\n", + " payload = {\n", + " \"displayName\": app_id,\n", + " \"dataStoreIds\": [datastore_id],\n", + " \"solutionType\": \"SOLUTION_TYPE_SEARCH\",\n", + " \"searchEngineConfig\": {\n", + " \"searchTier\": \"SEARCH_TIER_ENTERPRISE\",\n", + " \"searchAddOns\": [\"SEARCH_ADD_ON_LLM\"],\n", + " },\n", + " }\n", + " header = {\"X-Goog-User-Project\": project_id, \"Content-Type\": \"application/json\"}\n", + " es_endpoint = f\"https://discoveryengine.googleapis.com/{vais_branch}/projects/{project_id}/locations/{location}/collections/default_collection/engines?engineId={app_id}\"\n", + " response = authed_session.post(\n", + " es_endpoint, data=json.dumps(payload), headers=header\n", + " )\n", + " if response.status_code == 200:\n", + " print(f\"The creation of App {app_id} is initiated.\")\n", + " print(\"It may take a few minutes for the App to become available\")\n", + " else:\n", + " print(f\"Failed to create App {app_id}\")\n", + " print(response.json())\n", + " return response.status_code" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "1hAp5cBnIYxJ" + }, + "source": [ + "## Create a Datastores with the provided ID if it doesn't exist\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "hBUwJxxeAazj" + }, + "outputs": [], + "source": [ + "if datastore_exists(PROJECT_ID, LOCATION, DATASTORE_ID):\n", + " print(f\"Datastore {DATASTORE_ID} already exists.\")\n", + "else:\n", + " create_website_datastore(VAIS_BRANCH, PROJECT_ID, LOCATION, DATASTORE_ID)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "C1d-pd2WLJZI" + }, + "source": [ + "## [Optional] Check if the Datastore is created successfully\n", + "\n", + "\n", + "The Datastore is polled to track when it becomes available.\n", + "\n", + "This may take a few minutes" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "EZGzOCnTLOwf" + }, + "outputs": [], + "source": [ + "while not datastore_exists(PROJECT_ID, LOCATION, DATASTORE_ID):\n", + " print(f\"Datastore {DATASTORE_ID} is still being created.\")\n", + " time.sleep(30)\n", + "print(f\"Datastore {DATASTORE_ID} is created successfully.\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "vSzz2AzmI5kx" + }, + "source": [ + "## Create an App with the provided ID if it doesn't exist\n", + "The App will be connected to a Datastore with the ID provided earlier in this notebook" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "4lp4kPXNm9sE" + }, + "outputs": [], + "source": [ + "if app_exists(PROJECT_ID, LOCATION, APP_ID):\n", + " print(f\"App {APP_ID} already exists.\")\n", + "else:\n", + " create_app(VAIS_BRANCH, PROJECT_ID, LOCATION, DATASTORE_ID, APP_ID)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "fxlTn7dVK-Q2" + }, + "source": [ + "## [Optional] Check if the App is created successfully\n", + "\n", + "\n", + "The App is polled to track when it becomes available.\n", + "\n", + "This may take a few minutes" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "ZuQQ2HCGK4BA" + }, + "outputs": [], + "source": [ + "while not app_exists(PROJECT_ID, LOCATION, APP_ID):\n", + " print(f\"App {APP_ID} is still being created.\")\n", + " time.sleep(30)\n", + "print(f\"App {APP_ID} is created successfully.\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "A38IfFRD83UG" + }, + "source": [ + "## Upgrade an existing Website Datastore to [Advanced Website](https://cloud.google.com/generative-ai-app-builder/docs/about-advanced-features#advanced-website-indexing) DataStore" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "BYXR-yQ38vdd" + }, + "outputs": [], + "source": [ + "def upgrade_to_advanced(\n", + " vais_branch: str, project_id: str, location: str, datastore_id: str\n", + ") -> int:\n", + " \"\"\"Upgrade the website search datastore to advanced\"\"\"\n", + " header = {\"X-Goog-User-Project\": project_id}\n", + " es_endpoint = f\"https://discoveryengine.googleapis.com/{vais_branch}/projects/{project_id}/locations/{location}/collections/default_collection/dataStores/{datastore_id}/siteSearchEngine:enableAdvancedSiteSearch\"\n", + " response = authed_session.post(es_endpoint, headers=header)\n", + " if response.status_code == 200:\n", + " print(f\"Datastore {datastore_id} upgraded to Advanced Website Search\")\n", + " else:\n", + " print(f\"Failed to upgrade Datastore {datastore_id}\")\n", + " print(response.text())\n", + " return response.status_code\n", + "\n", + "\n", + "upgrade_to_advanced(VAIS_BRANCH, PROJECT_ID, LOCATION, DATASTORE_ID)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "NlUq4ADT8975" + }, + "source": [ + "## Set the URLs to Include/Exclude in the Index\n", + "\n", + "You can set up to 500 Include and Exclude URL patterns for Advanced website search Datastores.\n", + "\n", + "This function sets a single URL pattern to be included every time it gets executed.\n", + "\n", + "The field `type` in the payload is used to indicate if the provided Uri pattern should be included or excluded. Here we only use `INCLUDE`.\n", + "\n", + "The `INCLUDE` and `EXCLUDE` URL patters specified with this function are incremental. You also have options to [Delete](https://cloud.google.com/generative-ai-app-builder/docs/reference/rest/v1alpha/projects.locations.collections.dataStores.siteSearchEngine.targetSites/delete), [List](https://cloud.google.com/generative-ai-app-builder/docs/reference/rest/v1alpha/projects.locations.collections.dataStores.siteSearchEngine.targetSites/list), [Batch Create](https://cloud.google.com/generative-ai-app-builder/docs/reference/rest/v1alpha/projects.locations.collections.dataStores.siteSearchEngine.targetSites/batchCreate), etc \n", + "\n", + "For this example, we index http://cloud.google.com/generative-ai-app-builder/*\n", + "\n", + "Note that you need to [verify the ownership of a domain](https://cloud.google.com/generative-ai-app-builder/docs/domain-verification) to be able to index it." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "yc2OWvFd9Tvu" + }, + "outputs": [], + "source": [ + "def include_url_patterns(\n", + " vais_branch: str,\n", + " project_id: str,\n", + " location: str,\n", + " datastore_id: str,\n", + " include_url_patterns,\n", + ") -> int:\n", + " \"\"\"Set include and exclude URL patterns for the Datastore\"\"\"\n", + " payload = {\n", + " \"providedUriPattern\": include_url_patterns,\n", + " \"type\": \"INCLUDE\",\n", + " }\n", + " header = {\"X-Goog-User-Project\": project_id, \"Content-Type\": \"application/json\"}\n", + " es_endpoint = f\"https://discoveryengine.googleapis.com/{vais_branch}/projects/{project_id}/locations/{location}/dataStores/{datastore_id}/siteSearchEngine/targetSites\"\n", + " response = authed_session.post(\n", + " es_endpoint, data=json.dumps(payload), headers=header\n", + " )\n", + " if response.status_code == 200:\n", + " print(\"URL patterns successfully set\")\n", + " print(\n", + " \"Depending on the size of your domain, the initial indexing may take from minutes to hours\"\n", + " )\n", + " else:\n", + " print(f\"Failed to set URL patterns for the Datastore {datastore_id}\")\n", + " print(response.text())\n", + " return response.status_code\n", + "\n", + "\n", + "include_url_patterns(\n", + " VAIS_BRANCH, PROJECT_ID, LOCATION, DATASTORE_ID, INCLUDE_URL_PATTERN\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "rSAbsrg8Pkc2" + }, + "source": [ + "# Step 2. Schema and URL mapping for Custom Attributes" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "Dc-gaZ6rP6mC" + }, + "source": [ + "## Set the Schema and URL mapping\n", + "\n", + "In this example we use [VAIS REST API documentation](https://cloud.google.com/generative-ai-app-builder/docs/reference/rest) as the source for the datastore. For the mapping we add \"REST\" tags to all branches of REST documentation. We also add an additional tag to identify each branch (i.e. V1, V1alpha, V1beta). The schema and URL mapping should follow [this](https://cloud.google.com/generative-ai-app-builder/docs/reference/rest/v1alpha/projects.locations.collections.dataStores.siteSearchEngine/setUriPatternDocumentData#request-body) formatting.\n", + "\n", + "Separately, we identify pages under Samples with a corresponding tag.\n", + "\n", + "As mentioned above, you can only index a website you own, as a result your mapping will be different from the ones used in this example.\n", + "\n", + "Note that each successful mapping request overrides the previous ones (i.e. mappings are not incremental)\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "TDpAyVAUbxXM" + }, + "outputs": [], + "source": [ + "header = {\"X-Goog-User-Project\": PROJECT_ID}\n", + "es_endpoint = f\"https://discoveryengine.googleapis.com/{VAIS_BRANCH}/projects/{PROJECT_ID}/locations/{LOCATION}/collections/default_collection/dataStores/{DATASTORE_ID}/siteSearchEngine:setUriPatternDocumentData\"\n", + "json_data = {\n", + " \"documentDataMap\": {\n", + " \"https://cloud.google.com/generative-ai-app-builder/docs/reference/rest/v1/*\": {\n", + " \"Topic\": [\"Rest\", \"V1\"]\n", + " },\n", + " \"https://cloud.google.com/generative-ai-app-builder/docs/reference/rest/v1alpha/*\": {\n", + " \"Topic\": [\"Rest\", \"V1alpha\"]\n", + " },\n", + " \"https://cloud.google.com/generative-ai-app-builder/docs/reference/rest/v1beta/*\": {\n", + " \"Topic\": [\"Rest\", \"V1beta\"]\n", + " },\n", + " \"https://cloud.google.com/generative-ai-app-builder/docs/samples*\": {\n", + " \"Topic\": [\"Samples\"]\n", + " },\n", + " },\n", + " \"schema\": {\n", + " \"$schema\": \"https://json-schema.org/draft/2020-12/schema\",\n", + " \"properties\": {\n", + " \"Topic\": {\n", + " \"items\": {\n", + " \"indexable\": True,\n", + " \"retrievable\": True,\n", + " \"searchable\": True,\n", + " \"type\": \"string\",\n", + " },\n", + " \"type\": \"array\",\n", + " }\n", + " },\n", + " \"type\": \"object\",\n", + " },\n", + "}\n", + "\n", + "set_schema_response = authed_session.post(es_endpoint, headers=header, json=json_data)\n", + "\n", + "print(json.dumps(set_schema_response.json(), indent=1))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "xRCbxXEG2XmF" + }, + "source": [ + "## Get the Schema and URL mapping\n", + "\n", + "Get the Schema and URL mapping to ensure it is updated according to your expectations." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "7TXWY_6nTH3u" + }, + "outputs": [], + "source": [ + "header = {\"X-Goog-User-Project\": PROJECT_ID}\n", + "es_endpoint = f\"https://discoveryengine.googleapis.com/{VAIS_BRANCH}/projects/{PROJECT_ID}/locations/{LOCATION}/collections/default_collection/dataStores/{DATASTORE_ID}/siteSearchEngine:getUriPatternDocumentData\"\n", + "get_schema_response = authed_session.get(es_endpoint, headers=header)\n", + "\n", + "print(json.dumps(get_schema_response.json(), indent=1))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "vESfZZ_QDLc8" + }, + "source": [ + "# Step 3. Run queries w/wo Metadata filter" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "dce2yLIoZz0f" + }, + "source": [ + "## Search Parameters\n", + "`QUERY`: Used to query VAIS.\n", + "\n", + "`PAGE_SIZE`: The maximum number of results retrieved from VAIS.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "ZObBBqVdZZUV" + }, + "outputs": [], + "source": [ + "QUERY = \"\" # @param {type: 'string'}\n", + "PAGE_SIZE = 5 # @param {type: 'integer'}" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "nnnxTO8CC9Mz" + }, + "source": [ + "## Search Without Filter\n", + "Given that the `Topic` custom attribute is made `retrievable` in the Schema, You will get it back in the response, when applicable.\n", + "\n", + "Custom attributes are included in the `structData` field of the `result`)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "Usr8OMTu5EUk" + }, + "outputs": [], + "source": [ + "search_response = authed_session.post(\n", + " f\"https://discoveryengine.googleapis.com/{VAIS_BRANCH}/projects/{PROJECT_ID}/locations/{LOCATION}/collections/default_collection/dataStores/{DATASTORE_ID}/servingConfigs/default_search:search\",\n", + " headers={\"Content-Type\": \"application/json\"},\n", + " json={\"query\": QUERY, \"pageSize\": PAGE_SIZE},\n", + ")\n", + "\n", + "print(json.dumps(search_response.json(), indent=1))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "uCRfkzGyC53w" + }, + "source": [ + "## Search with Filter\n", + "Now we apply a filter so that a search only returns results from the V1alpha branch of the REST documentation. The filter and expected results will be different based on the domain included in your website datastore. \n", + "\n", + "We could also use this indexable field for other purposes such as Boosting, if desired." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "qatUukazC4oH" + }, + "outputs": [], + "source": [ + "search_response = authed_session.post(\n", + " f\"https://discoveryengine.googleapis.com/{VAIS_BRANCH}/projects/{PROJECT_ID}/locations/{LOCATION}/collections/default_collection/dataStores/{DATASTORE_ID}/servingConfigs/default_search:search\",\n", + " headers={\"Content-Type\": \"application/json\"},\n", + " json={\"query\": QUERY, \"filter\": 'Topic: ANY(\"V1alpha\")', \"pageSize\": PAGE_SIZE},\n", + ")\n", + "\n", + "print(json.dumps(search_response.json(), indent=1))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "e1kgs_XdDlHL" + }, + "source": [ + "# Clean up" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "tGuk4ZnJk0S7" + }, + "source": [ + "## Delete the Search App\n", + "\n", + "Delete the App if you no longer need it\n", + "\n", + "Alternatively you can follow [these instructions](https://console.cloud.google.com/gen-app-builder/data-stores) to delete an App from the UI\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "QEfxXtzfk0rx" + }, + "outputs": [], + "source": [ + "response = authed_session.delete(\n", + " f\"https://discoveryengine.googleapis.com/{VAIS_BRANCH}/projects/{PROJECT_ID}/locations/{LOCATION}/collections/default_collection/engines/{APP_ID}\",\n", + " headers={\"X-Goog-User-Project\": PROJECT_ID},\n", + ")\n", + "\n", + "print(response.text)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "Tgm5idL4DjjU" + }, + "source": [ + "## Delete the Datastores\n", + "Delete the Datastore if you no longer need it\n", + "\n", + "Alternatively you can follow [these instructions](https://console.cloud.google.com/gen-app-builder/data-stores) to delete a Datastore from the UI" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "vj8BpuS62tgt" + }, + "outputs": [], + "source": [ + "response = authed_session.delete(\n", + " f\"https://discoveryengine.googleapis.com/{VAIS_BRANCH}/projects/{PROJECT_ID}/locations/{LOCATION}/collections/default_collection/dataStores/{DATASTORE_ID}\",\n", + " headers={\"X-Goog-User-Project\": PROJECT_ID},\n", + ")\n", + "\n", + "print(response.text)" + ] + } + ], + "metadata": { + "colab": { + "name": "custom_attributes_by_url_pattern.ipynb", + "toc_visible": true + }, + "kernelspec": { + "display_name": "Python 3", + "name": "python3" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} diff --git a/search/vais-building-blocks/ingesting_unstructured_documents_with_metadata.ipynb b/search/vais-building-blocks/ingesting_unstructured_documents_with_metadata.ipynb new file mode 100644 index 0000000..656755a --- /dev/null +++ b/search/vais-building-blocks/ingesting_unstructured_documents_with_metadata.ipynb @@ -0,0 +1,1349 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "CBk3jQ3fVWUp" + }, + "outputs": [], + "source": [ + "# Copyright 2024 Google LLC\n", + "#\n", + "# Licensed under the Apache License, Version 2.0 (the \"License\");\n", + "# you may not use this file except in compliance with the License.\n", + "# You may obtain a copy of the License at\n", + "#\n", + "# https://www.apache.org/licenses/LICENSE-2.0\n", + "#\n", + "# Unless required by applicable law or agreed to in writing, software\n", + "# distributed under the License is distributed on an \"AS IS\" BASIS,\n", + "# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n", + "# See the License for the specific language governing permissions and\n", + "# limitations under the License." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "Z6z9Ibm0VXRd" + }, + "source": [ + "# Ingestion of Unstructured Documents with Metadata in Vertex AI Search\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + "
    \n", + " \n", + " \"Google
    Open in Colab\n", + "
    \n", + "
    \n", + " \n", + " \"Google
    Open in Colab Enterprise\n", + "
    \n", + "
    \n", + " \n", + " \"Vertex
    Open in Vertex AI Workbench\n", + "
    \n", + "
    \n", + " \n", + " \"GitHub
    View on GitHub\n", + "
    \n", + "
    \n", + "\n", + "
    \n", + "\n", + "Share to:\n", + "\n", + "\n", + " \"LinkedIn\n", + "\n", + "\n", + "\n", + " \"Bluesky\n", + "\n", + "\n", + "\n", + " \"X\n", + "\n", + "\n", + "\n", + " \"Reddit\n", + "\n", + "\n", + "\n", + " \"Facebook\n", + "" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "rhWHRiVePfQV" + }, + "source": [ + "| | |\n", + "|----------|-------------|\n", + "| Author(s) | Hossein Mansour|\n", + "| Reviewers(s) | Meltem Subasioglu, Rajesh Thallam|\n", + "| Last updated | 2024-07-23: The first draft |" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "GVFVUibCCiAD" + }, + "source": [ + "# Overview\n", + "\n", + "In this notebook, we will show you how to prepare and ingest unstructured documents with metadata into [Vertex AI Search](https://cloud.google.com/generative-ai-app-builder/docs/introduction). Metadata can be used for different purposes such as improving recall and precision, influencing results via boosting and filtering, and including additional context to be retrieved together with the documents. You can find more information about different types of metadata [here](https://cloud.google.com/generative-ai-app-builder/docs/provide-schema#about_providing_your_own_schema_as_a_json_object).\n", + "\n", + "We will perform the following steps:\n", + "\n", + "- Creating a Vertex AI Search Datastore\n", + "- Creating a Vertex AI Search App\n", + "- [Optional] Updating the Schema for the Datastore\n", + "- Reading Documents and their Metadata from a GCS bucket and combining them together as JSONL file\n", + "- Uploading the documents with their metadata to the Datastore\n", + "- Searching the Datastore\n", + "\n", + "\n", + "Please refer to the [official documentation](https://cloud.google.com/generative-ai-app-builder/docs/create-datastore-ingest) of Vertex AI Search for the definition of Datastores and Apps and their relationships to one another.\n", + "\n", + "REST API is used throughout this notebook. Please consult the [official documentation](https://cloud.google.com/generative-ai-app-builder/docs/apis) for alternative ways to achieve the same goal, namely Client libraries and RPC.\n", + "\n", + "\n", + "## Vertex AI Search\n", + "Vertex AI Search (VAIS) is a fully-managed platform, powered by large language models, that lets you build AI-enabled search and recommendation experiences for your public or private websites or mobile applications\n", + "\n", + "VAIS can handle a diverse set of data sources including structured, unstructured, and website data, as well as data from third-party applications such as Jira, Salesforce, and Confluence.\n", + "\n", + "VAIS also has built-in integration with LLMs which enables you to provide answers to complex questions, grounded in your data\n", + "\n", + "## Using this Notebook\n", + "If you're running outside of Colab, depending on your environment you may need to install pip packages that are included in the Colab environment by default but are not part of the Python Standard Library. Outside of Colab you'll also notice comments in code cells that look like #@something, these trigger special Colab functionality but don't change the behavior of the notebook.\n", + "\n", + "This tutorial uses the following Google Cloud services and resources:\n", + "\n", + "- Service Usage API\n", + "- Discovery Engine\n", + "- Google Cloud Storage Client\n", + "\n", + "This notebook has been tested in the following environment:\n", + "\n", + "- Python version = 3.10.12\n", + "- google.cloud.storage = 2.8.0\n", + "- google.auth = 2.27.0\n", + "\n", + "# Getting Started\n", + "\n", + "The following steps are necessary to run this notebook, no matter what notebook environment you're using.\n", + "\n", + "If you're entirely new to Google Cloud, [get started here](https://cloud.google.com/docs/get-started)\n", + "\n", + "## Google Cloud Project Setup\n", + "\n", + "1. [Select or create a Google Cloud project](https://console.cloud.google.com/cloud-resource-manager). When you first create an account, you get a $300 free credit towards your compute/storage costs\n", + "2. [Make sure that billing is enabled for your project](https://cloud.google.com/billing/docs/how-to/modify-project)\n", + "3. [Enable the Service Usage API](https://console.cloud.google.com/apis/library/serviceusage.googleapis.com)\n", + "4. [Enable the Cloud Storage API](https://console.cloud.google.com/flows/enableapi?apiid=storage.googleapis.com)\n", + "5. [Enable the Discovery Engine API for your project](https://console.cloud.google.com/marketplace/product/google/discoveryengine.googleapis.com)\n", + "\n", + "## Google Cloud Permissions\n", + "\n", + "Ideally you should have [Owner role](https://cloud.google.com/iam/docs/understanding-roles) for your project to run this notebook. If that is not an option, you need at least the following [roles](https://cloud.google.com/iam/docs/granting-changing-revoking-access)\n", + "- **`roles/serviceusage.serviceUsageAdmin`** to enable APIs\n", + "- **`roles/iam.serviceAccountAdmin`** to modify service agent permissions\n", + "- **`roles/discoveryengine.admin`** to modify discoveryengine assets\n", + "- **`roles/storage.objectAdmin`** to modify and delete GCS buckets\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "NuQphNnDp3xA" + }, + "source": [ + "# Setup Environment" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "YqcV8aj8GvZA" + }, + "source": [ + "## Authentication\n", + "\n", + " If you're using Colab, run the code in the next cell. Follow the pop-ups and authenticate with an account that has access to your Google Cloud [project](https://cloud.google.com/resource-manager/docs/creating-managing-projects#identifying_projects).\n", + "\n", + "If you're running this notebook somewhere besides Colab, make sure your environment has the right Google Cloud access. If that's a new concept to you, consider looking into [Application Default Credentials for your local environment](https://cloud.google.com/docs/authentication/provide-credentials-adc#local-dev) and [initializing the Google Cloud CLI](https://cloud.google.com/docs/authentication/gcloud). In many cases, running `gcloud auth application-default login` in a shell on the machine running the notebook kernel is sufficient.\n", + "\n", + "More authentication options are discussed [here](https://cloud.google.com/docs/authentication)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "DZjtfEDG7Sr3" + }, + "outputs": [], + "source": [ + "# Colab authentication.\n", + "import sys\n", + "\n", + "if \"google.colab\" in sys.modules:\n", + " from google.colab import auth\n", + "\n", + " auth.authenticate_user()\n", + " print(\"Authenticated\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "kT3Eda7_mlTP" + }, + "outputs": [], + "source": [ + "from google.auth import default\n", + "from google.auth.transport.requests import AuthorizedSession\n", + "\n", + "creds, _ = default()\n", + "authed_session = AuthorizedSession(creds)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "_OyCUmMVGeo-" + }, + "source": [ + "## Import Libraries" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "gCIgR1NCatrP" + }, + "outputs": [], + "source": [ + "import glob\n", + "import json\n", + "import os\n", + "import re\n", + "import shutil\n", + "import time\n", + "from typing import Any\n", + "from urllib.parse import urlparse\n", + "\n", + "from google.cloud import storage\n", + "import pandas as pd\n", + "import requests" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "KTSL1m_CHFBI" + }, + "source": [ + "## Configure environment\n", + "\n", + "You can enter the ID for an existing App and Datastore to be used in this notebook. Alternatively, you can enter the desired IDs for non-existings App and Datastore and they will be created later in this notebook.\n", + "\n", + "Same applies to the GCS Directory of Documents and Metadata. The Documents and Metadata can be in separate buckets, but it is advised to keep them (together with the JSONL created later in this notebook) in the same temporary bucket for the ease of cleanup.\n", + "\n", + "You can find more information regarding the \"Location\" of datastores and associated limitations [here](https://cloud.google.com/generative-ai-app-builder/docs/locations#specify_a_multi-region_for_your_data_store). The Location of a Datastore is set at the time of creation and it should be called appropriately to query the Datastore." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "089_6PdMa64e" + }, + "outputs": [], + "source": [ + "PROJECT_ID = \"\" # @param {type:\"string\"}\n", + "\n", + "# Vertex AI Search Parameters\n", + "DATASTORE_ID = \"\" # @param {type:\"string\"}\n", + "APP_ID = \"\" # @param {type:\"string\"}\n", + "LOCATION = \"global\" # @param [\"global\", \"us\", \"eu\"] Global is preferred\n", + "\n", + "# GCS Parameters, e.g. 'gs://my_bucket/folder1/docs/'\n", + "GCS_DIRECTORY_DOCS = \"\" # @param {type:\"string\"}\n", + "GCS_DIRECTORY_METADATA = \"\" # @param {type:\"string\"}" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "sWkMcJej-2Gy" + }, + "source": [ + "# Create VAIS App and Datastore" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "2sSInI07MCll" + }, + "source": [ + "## [Prerequisite] Create a GCS bucket with sample documents\n", + "\n", + "This step is only needed for the purpose of this demo. For the real use case you will need to upload your actual documents to a GCS bucket.\n", + "\n", + "Here, we download Alphabet's 2022 Q1-Q4 Earning transcripts as sample documents." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "Yg0Y_T_iLx_K" + }, + "outputs": [], + "source": [ + "def create_gcs_bucket_and_download_files(project_id, new_bucket_path, file_urls):\n", + " \"\"\"\n", + " Creates a new GCS bucket (if it doesn't exist) and downloads files from specified URLs.\n", + "\n", + " Handles paths with subdirectories correctly using `urlparse`.\n", + " \"\"\"\n", + "\n", + " if not new_bucket_path.startswith(\"gs://\") or not new_bucket_path.endswith(\"/\"):\n", + " raise ValueError(\n", + " \"Invalid GCS path format. Must start with 'gs://' and end with '/'. \"\n", + " f\"Received: '{new_bucket_path}'\"\n", + " )\n", + "\n", + " storage_client = storage.Client(project=project_id)\n", + "\n", + " # Extract bucket name and prefix from path\n", + " parsed_path = urlparse(new_bucket_path)\n", + " new_bucket_name = parsed_path.netloc\n", + " blob_prefix = parsed_path.path.strip(\"/\") # Remove leading and trailing slashes\n", + "\n", + " new_bucket = storage_client.bucket(new_bucket_name)\n", + "\n", + " if not new_bucket.exists():\n", + " new_bucket = storage_client.create_bucket(new_bucket_name)\n", + " print(f\"Bucket {new_bucket_name} created.\")\n", + "\n", + " for url in file_urls:\n", + " file_name = url.split(\"/\")[-1]\n", + " print(f\"Downloading: {file_name}\")\n", + "\n", + " try:\n", + " response = requests.get(url)\n", + " response.raise_for_status()\n", + "\n", + " # Construct the full blob path (including prefix)\n", + " blob_name = f\"{blob_prefix}/{file_name}\" if blob_prefix else file_name\n", + " blob = new_bucket.blob(blob_name)\n", + "\n", + " blob.upload_from_string(response.content)\n", + " print(f\"Uploaded: {blob_name}\") # Print the uploaded blob path\n", + " except requests.exceptions.RequestException as e:\n", + " print(f\"Error downloading {file_name}: {e}\")\n", + "\n", + "\n", + "file_urls = [\n", + " \"https://abc.xyz/assets/investor/static/pdf/2022_Q1_Earnings_Transcript.pdf\",\n", + " \"https://abc.xyz/assets/investor/static/pdf/2022_Q2_Earnings_Transcript.pdf\",\n", + " \"https://abc.xyz/assets/investor/static/pdf/2022_Q3_Earnings_Transcript.pdf\",\n", + " \"https://abc.xyz/assets/investor/static/pdf/2022_Q4_Earnings_Transcript.pdf\",\n", + "]\n", + "\n", + "create_gcs_bucket_and_download_files(PROJECT_ID, GCS_DIRECTORY_DOCS, file_urls)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "8qFNMjxJMuEr" + }, + "source": [ + "## [Prerequisite] Create a GCS bucket with sample Metadata\n", + "\n", + "Similar to the code block above, this step is only needed for the purpose of this demo.\n", + "\n", + "Here we extract some trivial metadata from the file name. Each Metadata will have a content similar to the one below:\n", + "\n", + "```json\n", + " {\n", + " \"doc_name\": \"2022_Q1_Earnings_Transcript\",\n", + " \"year\": \"2022\",\n", + " \"quarter\": \"Q1\",\n", + " \"doc_type\": \"earnings transcript\",\n", + " \"stock_tickers\": [\"GOOG\", \"GOOGL\"],\n", + " \"company_name\": \"alphabet\",\n", + " }\n", + " ```" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "xh_kZyI1MkpG" + }, + "outputs": [], + "source": [ + "def create_metadata_files(source_folder_path, metadata_folder_path):\n", + " \"\"\"Creates metadata JSON files for documents in a GCS folder.\"\"\"\n", + "\n", + " if not metadata_folder_path.startswith(\n", + " \"gs://\"\n", + " ) or not metadata_folder_path.endswith(\"/\"):\n", + " raise ValueError(\n", + " \"Invalid GCS path format. Must start with 'gs://' and end with '/'. \"\n", + " f\"Received: '{metadata_folder_path}'\"\n", + " )\n", + "\n", + " bucket_name = source_folder_path.split(\"/\")[2]\n", + " storage_client = storage.Client()\n", + " bucket = storage_client.bucket(bucket_name)\n", + "\n", + " source_folder = source_folder_path.replace(f\"gs://{bucket_name}/\", \"\")\n", + " metadata_folder = metadata_folder_path.replace(f\"gs://{bucket_name}/\", \"\")\n", + "\n", + " blobs = bucket.list_blobs(prefix=source_folder)\n", + "\n", + " for blob in blobs:\n", + " # Explicitly check if the blob is a folder/directory\n", + " if blob.name.endswith(\"/\"):\n", + " print(f\"Skipping folder: {blob.name}\")\n", + " continue\n", + "\n", + " # Get the filename by splitting on the last \"/\"\n", + " filename = blob.name.split(\"/\")[-1]\n", + "\n", + " # Improved regex to match a wider variety of file names\n", + " doc_name_match = re.match(r\"(\\d{4})_Q(\\d)_\\w+_Transcript\\.pdf\", filename)\n", + " if not doc_name_match:\n", + " print(f\"Skipping file with unexpected name: {filename}\")\n", + " continue\n", + "\n", + " year, quarter = doc_name_match.groups()\n", + "\n", + " # Construct doc_type from the filename (without path)\n", + " doc_type = \"_\".join(filename.split(\"_\")[2:-1]).replace(\"_\", \" \")\n", + "\n", + " metadata = {\n", + " \"doc_name\": filename.replace(\".pdf\", \"\"),\n", + " \"year\": year,\n", + " \"quarter\": f\"Q{quarter}\",\n", + " \"doc_type\": doc_type,\n", + " \"stock_tickers\": [\"GOOG\", \"GOOGL\"],\n", + " \"company_name\": \"alphabet\",\n", + " }\n", + "\n", + " metadata_file_name = f\"{metadata['doc_name']}.txt\"\n", + " metadata_blob = bucket.blob(metadata_folder + metadata_file_name)\n", + "\n", + " metadata_blob.upload_from_string(json.dumps(metadata, indent=4))\n", + "\n", + " print(f\"Created metadata file: {metadata_blob.name}\")\n", + "\n", + "\n", + "create_metadata_files(GCS_DIRECTORY_DOCS, GCS_DIRECTORY_METADATA)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "C2hXlewDINDg" + }, + "source": [ + "## Helper functions to issue basic search on a Datastore or an App" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "v-XHQIOooshe" + }, + "outputs": [], + "source": [ + "def search_by_datastore(\n", + " project_id: str, location: str, datastore_id: str, query: str\n", + ") -> dict[str, Any]:\n", + " \"\"\"Searches a datastore using the provided query.\"\"\"\n", + " response = authed_session.post(\n", + " f\"https://discoveryengine.googleapis.com/v1/projects/{project_id}/locations/{location}/collections/default_collection/dataStores/{datastore_id}/servingConfigs/default_search:search\",\n", + " headers={\n", + " \"Content-Type\": \"application/json\",\n", + " },\n", + " json={\"query\": query, \"pageSize\": 1},\n", + " )\n", + " return response\n", + "\n", + "\n", + "def search_by_app(\n", + " project_id: str, location: str, app_id: str, query: str\n", + ") -> dict[str, Any]:\n", + " \"\"\"Searches an app using the provided query.\"\"\"\n", + " response = authed_session.post(\n", + " f\"https://discoveryengine.googleapis.com/v1/projects/{project_id}/locations/{location}/collections/default_collection/engines/{app_id}/servingConfigs/default_config:search\",\n", + " headers={\n", + " \"Content-Type\": \"application/json\",\n", + " },\n", + " json={\"query\": query, \"pageSize\": 1},\n", + " )\n", + " return response" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "eAigF6KHkMZ2" + }, + "source": [ + "## Helper functions to check whether or not a Datastore or an App already exist" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "IO1AxLZckXYK" + }, + "outputs": [], + "source": [ + "def datastore_exists(project_id: str, location: str, datastore_id: str) -> bool:\n", + " \"\"\"Check if a datastore exists.\"\"\"\n", + " response = search_by_datastore(project_id, location, datastore_id, \"test\")\n", + " status_code = response.status_code\n", + " if status_code == 200:\n", + " return True\n", + " if status_code == 404:\n", + " return False\n", + " raise Exception(f\"Error: {status_code}\")\n", + "\n", + "\n", + "def app_exists(project_id: str, location: str, app_id: str) -> bool:\n", + " \"\"\"Check if an App exists.\"\"\"\n", + " response = search_by_app(project_id, location, app_id, \"test\")\n", + " status_code = response.status_code\n", + " if status_code == 200:\n", + " return True\n", + " if status_code == 404:\n", + " return False\n", + " raise Exception(f\"Error: {status_code}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "dsUql1_wkeaO" + }, + "source": [ + "## Helper functions to create a Datastore or an App\n", + "\n", + "The datastore is created with [Chunk Mode](https://cloud.google.com/generative-ai-app-builder/docs/parse-chunk-documents) and Chunk size of 500 tokens.\n", + "\n", + "The documents will be processed with Layout parser (higher quality for complex documents containing elements like tables and lists) and Ancestor information (i.e. headings) is included with each Chunk. Please see [official documentation](https://cloud.google.com/generative-ai-app-builder/docs/parse-chunk-documents) for more details.\n", + "\n", + "These settings are chosen to optimize accuracy, they can be adjusted in the create_datastore function below." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "SxR3hw5Tke-q" + }, + "outputs": [], + "source": [ + "def create_datastore(project_id: str, location: str, datastore_id: str) -> int:\n", + " \"\"\"Create a datastore.\"\"\"\n", + " payload = {\n", + " \"displayName\": datastore_id,\n", + " \"industryVertical\": \"GENERIC\",\n", + " \"solutionTypes\": [\"SOLUTION_TYPE_SEARCH\"],\n", + " \"contentConfig\": \"CONTENT_REQUIRED\",\n", + " \"documentProcessingConfig\": {\n", + " \"chunkingConfig\": {\n", + " \"layoutBasedChunkingConfig\": {\n", + " \"chunkSize\": 500,\n", + " \"includeAncestorHeadings\": True,\n", + " }\n", + " },\n", + " \"defaultParsingConfig\": {\"layoutParsingConfig\": {}},\n", + " },\n", + " }\n", + " header = {\"X-Goog-User-Project\": project_id, \"Content-Type\": \"application/json\"}\n", + " es_endpoint = f\"https://discoveryengine.googleapis.com/v1/projects/{project_id}/locations/{location}/collections/default_collection/dataStores?dataStoreId={datastore_id}\"\n", + " response = authed_session.post(\n", + " es_endpoint, data=json.dumps(payload), headers=header\n", + " )\n", + " if response.status_code == 200:\n", + " print(f\"The creation of Datastore {datastore_id} is initiated.\")\n", + " print(\"It may take a few minutes for the Datastore to become available\")\n", + " else:\n", + " print(f\"Failed to create Datastore {datastore_id}\")\n", + " print(response.json())\n", + " return response.status_code\n", + "\n", + "\n", + "def create_app(project_id: str, location: str, datastore_id: str, app_id: str) -> int:\n", + " \"\"\"Create a search app.\"\"\"\n", + " payload = {\n", + " \"displayName\": app_id,\n", + " \"dataStoreIds\": [datastore_id],\n", + " \"solutionType\": \"SOLUTION_TYPE_SEARCH\",\n", + " \"searchEngineConfig\": {\n", + " \"searchTier\": \"SEARCH_TIER_ENTERPRISE\",\n", + " \"searchAddOns\": [\"SEARCH_ADD_ON_LLM\"],\n", + " },\n", + " }\n", + " header = {\"X-Goog-User-Project\": project_id, \"Content-Type\": \"application/json\"}\n", + " es_endpoint = f\"https://discoveryengine.googleapis.com/v1/projects/{project_id}/locations/{location}/collections/default_collection/engines?engineId={app_id}\"\n", + " response = authed_session.post(\n", + " es_endpoint, data=json.dumps(payload), headers=header\n", + " )\n", + " if response.status_code == 200:\n", + " print(f\"The creation of App {app_id} is initiated.\")\n", + " print(\"It may take a few minutes for the App to become available\")\n", + " else:\n", + " print(f\"Failed to create App {app_id}\")\n", + " print(response.json())\n", + " return response.status_code" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "1hAp5cBnIYxJ" + }, + "source": [ + "## Create a Datastore with the provided ID if it doesn't exist" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "hBUwJxxeAazj" + }, + "outputs": [], + "source": [ + "if datastore_exists(PROJECT_ID, LOCATION, DATASTORE_ID):\n", + " print(f\"Datastore {DATASTORE_ID} already exists.\")\n", + "else:\n", + " create_datastore(PROJECT_ID, LOCATION, DATASTORE_ID)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "C1d-pd2WLJZI" + }, + "source": [ + "## [Optional] Check if the Datastore is created successfully\n", + "\n", + "\n", + "The Datastore is polled to track when it becomes available.\n", + "\n", + "This may take a few minutes" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "EZGzOCnTLOwf" + }, + "outputs": [], + "source": [ + "while not datastore_exists(PROJECT_ID, LOCATION, DATASTORE_ID):\n", + " print(f\"Datastore {DATASTORE_ID} is still being created.\")\n", + " time.sleep(30)\n", + "print(f\"Datastore {DATASTORE_ID} is created successfully.\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "vSzz2AzmI5kx" + }, + "source": [ + "## Create an App with the provided ID if it doesn't exist\n", + "The App will be connected to a Datastore with the provided ID earlier in this notebook" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "4lp4kPXNm9sE" + }, + "outputs": [], + "source": [ + "if app_exists(PROJECT_ID, LOCATION, APP_ID):\n", + " print(f\"App {APP_ID} already exists.\")\n", + "else:\n", + " create_app(PROJECT_ID, LOCATION, DATASTORE_ID, APP_ID)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "fxlTn7dVK-Q2" + }, + "source": [ + "## [Optional] Check if the App is created successfully\n", + "\n", + "\n", + "The App is polled to track when it becomes available.\n", + "\n", + "This may take a few minutes" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "ZuQQ2HCGK4BA" + }, + "outputs": [], + "source": [ + "while not app_exists(PROJECT_ID, LOCATION, APP_ID):\n", + " print(f\"App {APP_ID} is still being created.\")\n", + " time.sleep(30)\n", + "print(f\"App {APP_ID} is created successfully.\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "MQJ8In3r-2G0" + }, + "source": [ + "# Providing your own schema for the Metadata" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "-7LR113gJg7B" + }, + "source": [ + "## [Optional] Provide your own Schema\n", + "\n", + " The schema is detected automatically but it can be optionally adjusted to decide which fields should be:\n", + "\n", + " - Retrievable (returned in the response),\n", + " - Searchable (searched through term-based and semantically),\n", + " - Indexable (filtered, boosted etc)\n", + "\n", + "We can also specify keyProperties which gives special retrieval treatment to certain fields.\n", + "\n", + "Note that the Schema is only relevant to the Metadata and not the actual documents and it's hierarchical structure.\n", + "\n", + "See this documentation on [auto-detecting versus providing your own Schema](https://cloud.google.com/generative-ai-app-builder/docs/provide-schema)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "aO13rwu1Q6jr" + }, + "outputs": [], + "source": [ + "schema: dict[str, Any] = {\n", + " \"structSchema\": {\n", + " \"type\": \"object\",\n", + " \"properties\": {\n", + " \"doc_name\": {\n", + " \"keyPropertyMapping\": \"title\",\n", + " \"retrievable\": True,\n", + " \"dynamicFacetable\": False,\n", + " \"type\": \"string\",\n", + " },\n", + " \"year\": {\n", + " \"retrievable\": True,\n", + " \"indexable\": True,\n", + " \"dynamicFacetable\": False,\n", + " \"searchable\": False,\n", + " \"type\": \"string\",\n", + " },\n", + " \"quarter\": {\n", + " \"retrievable\": True,\n", + " \"indexable\": True,\n", + " \"dynamicFacetable\": False,\n", + " \"searchable\": False,\n", + " \"type\": \"string\",\n", + " },\n", + " \"doc_type\": {\n", + " \"retrievable\": True,\n", + " \"indexable\": True,\n", + " \"dynamicFacetable\": False,\n", + " \"searchable\": False,\n", + " \"type\": \"string\",\n", + " },\n", + " \"stock_tickers\": {\n", + " \"type\": \"array\",\n", + " \"items\": {\"type\": \"string\", \"keyPropertyMapping\": \"category\"},\n", + " },\n", + " \"company_name\": {\n", + " \"retrievable\": True,\n", + " \"indexable\": True,\n", + " \"dynamicFacetable\": False,\n", + " \"searchable\": False,\n", + " \"type\": \"string\",\n", + " },\n", + " },\n", + " \"$schema\": \"https://json-schema.org/draft/2020-12/schema\",\n", + " }\n", + "}\n", + "\n", + "response = authed_session.patch(\n", + " f\"https://discoveryengine.googleapis.com/v1/projects/{PROJECT_ID}/locations/{LOCATION}/collections/default_collection/dataStores/{DATASTORE_ID}/schemas/default_schema\",\n", + " headers={\n", + " \"Content-Type\": \"application/json\",\n", + " },\n", + " json=schema,\n", + ")\n", + "print(response.json())\n", + "schema_update_lro = response.json()[\"name\"]" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "EeuAlWHJKZ2t" + }, + "source": [ + "## Check the status of Schema update\n", + "\n", + "For an empty Datastore the Schema update should be almost instantaneous.\n", + "\n", + "A request to update the schema creates a [Long-Running Operation](https://cloud.google.com/generative-ai-app-builder/docs/long-running-operations) which can be polled." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "om_a4O4NV-dU" + }, + "outputs": [], + "source": [ + "while True:\n", + " response = authed_session.get(\n", + " f\"https://discoveryengine.googleapis.com/v1/{schema_update_lro}\",\n", + " )\n", + " try:\n", + " status = response.json()[\"done\"]\n", + " if status:\n", + " print(\"Import completed!\")\n", + " break\n", + " except:\n", + " print(\"Import in progress.\")\n", + " time.sleep(10)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "Mr3n2jbaLoo5" + }, + "source": [ + "## [Optional] Get the current Schema\n", + "This block can be used to check whether or not the schema is in the desired state (particularly useful for an auto-detected schema)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "Ueel8I0NS66p" + }, + "outputs": [], + "source": [ + "resp = authed_session.get(\n", + " f\"https://discoveryengine.googleapis.com/v1/projects/{PROJECT_ID}/locations/{LOCATION}/collections/default_collection/dataStores/{DATASTORE_ID}/schemas/default_schema\",\n", + ")\n", + "resp.json()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "lh3TvqnNqQoP" + }, + "source": [ + "# Prepare documents with metadata for ingestion" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "xoq4nUxyPuF5" + }, + "source": [ + "## Define the path to documents and Metadata (both in GCS and Local)\n", + "The JSONL GCS Directory will be used to store the JSONL file to-be-cereated. If such a directory does not exist, it will be created.\n", + "\n", + "For the purpose of this demo, the documents and their correponding metadata are joined based on the FIELD_FOR_FILE_NAME within the metadata (doc_name in this example)\n", + "\n", + "Based on that convention, the metadata for \"2022_Q1_Earnings_Transcript.pdf\" will have the following content:\n", + "\n", + "```json\n", + " {\n", + " \"doc_name\": \"2022_Q1_Earnings_Transcript\",\n", + " \"year\": \"2022\",\n", + " \"quarter\": \"Q1\",\n", + " \"doc_type\": \"earnings transcript\",\n", + " \"stock_tickers\": [\"GOOG\", \"GOOGL\"],\n", + " \"company_name\": \"alphabet\",\n", + " }\n", + " ```\n", + "\n", + "The logic is applied for illustration purposes and you can apply any other joining logic that fits your data (e.g. common name between metadata and document files)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "s8YpjxkrprRg" + }, + "outputs": [], + "source": [ + "DOCUMENT_FORMAT = \"pdf\" # @param [\"docx\", \"pdf\"]\n", + "GCS_DIRECTORY_JSONL = \"\" # @param {type:\"string\"}\n", + "FIELD_FOR_FILE_NAME = \"doc_name\" # @param {type:\"string\"}\n", + "\n", + "JSONL_FILENAME = \"alphabet_earnings.json\"\n", + "LOCAL_DOCS_PATH = \"data\"\n", + "LOCAL_METADATA_PATH = \"metadata\"\n", + "LOCAL_JSONL_PATH = \"jsonl\"" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "_KGV_xyI1fxh" + }, + "source": [ + "## Helper function to prepare JSONL content\n", + "A JSONL file needs to be created which contains a joined list of documents to be ingested and their metadata. You can find more details on the expected formatting [here](https://cloud.google.com/generative-ai-app-builder/docs/prepare-data#storage-unstructured)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "qwpDRUWK1gEY" + }, + "outputs": [], + "source": [ + "def prepare_jsonl(row: pd.Series) -> dict[str, Any]:\n", + " \"\"\"Prepares metadata for a given row in the DataFrame.\"\"\"\n", + " mimetype = (\n", + " \"application/vnd.openxmlformats-officedocument.wordprocessingml.document\"\n", + " if DOCUMENT_FORMAT == \"docx\"\n", + " else \"application/pdf\"\n", + " )\n", + " struct_data = row.to_dict()\n", + " return {\n", + " \"id\": row[FIELD_FOR_FILE_NAME],\n", + " \"structData\": struct_data,\n", + " \"content\": {\n", + " \"mimeType\": mimetype,\n", + " \"uri\": f\"{GCS_DIRECTORY_DOCS}{row[FIELD_FOR_FILE_NAME]}.{DOCUMENT_FORMAT}\",\n", + " },\n", + " }" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "ZrJHYvSO11EG" + }, + "source": [ + "## Prepare JSONL file and save to GCS\n", + "Documents and their metadata are copied to the local path, loaded in a DataFrame, and processed to prepare a JSONL file with the expected format\n", + "The JSONL file is then uploaded the provided GCS path" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "t8i9eB7G10ul" + }, + "outputs": [], + "source": [ + "# Copy files from GCS to local\n", + "os.makedirs(LOCAL_DOCS_PATH, exist_ok=True)\n", + "os.makedirs(LOCAL_METADATA_PATH, exist_ok=True)\n", + "os.makedirs(LOCAL_JSONL_PATH, exist_ok=True)\n", + "!gsutil -m cp -r {GCS_DIRECTORY_DOCS}* {LOCAL_DOCS_PATH}\n", + "!gsutil -m cp -r {GCS_DIRECTORY_METADATA}* {LOCAL_METADATA_PATH}\n", + "\n", + "# Load and process metadata\n", + "metadata_files = glob.glob(f\"{os.getcwd()}/{LOCAL_METADATA_PATH}/*.txt\")\n", + "df_json = pd.concat(\n", + " [pd.read_json(file, typ=\"series\") for file in metadata_files], axis=1\n", + ").T # Load all JSON into one DataFrame\n", + "\n", + "# Apply metadata preparation and save as JSONL\n", + "df_json[\"metadata\"] = df_json.apply(prepare_jsonl, axis=1)\n", + "df_json[\"metadata\"].to_json(\n", + " f\"{LOCAL_JSONL_PATH}/{JSONL_FILENAME}\", orient=\"records\", lines=True\n", + ")\n", + "\n", + "# Upload the local JSONL file to GCS\n", + "!gsutil -m cp {LOCAL_JSONL_PATH}/* {GCS_DIRECTORY_JSONL}\n", + "\n", + "# Optional print of the jsonL content\n", + "print(\"\\nJSONL Content:\")\n", + "for metadata_entry in df_json[\"metadata\"]:\n", + " print(json.dumps(metadata_entry, indent=2))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "J4PuuT-jqdqZ" + }, + "source": [ + "# Ingest documents to Datastore" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "x7cW0g5t2DSk" + }, + "source": [ + "## Import documents with metadata from JSONL on GCS\n", + "This is where the actual import to the Datastore happens.\n", + "The process is done Async, and the request returns an instance of a \"Long running Operation\"\n", + "\n", + "This may take xx minutes. Feel free to grab a coffee." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "-FVcu7wGJcom" + }, + "outputs": [], + "source": [ + "def import_documents_from_gcs_jsonl(\n", + " project_id: str, location: str, datastore_id: str, gcs_uri: str\n", + ") -> str:\n", + " \"\"\"Imports documents from a JSONL file in GCS.\"\"\"\n", + " payload = {\n", + " \"reconciliationMode\": \"INCREMENTAL\",\n", + " \"gcsSource\": {\"inputUris\": [gcs_uri]},\n", + " }\n", + " header = {\"Content-Type\": \"application/json\"}\n", + " es_endpoint = f\"https://discoveryengine.googleapis.com/v1/projects/{project_id}/locations/{location}/collections/default_collection/dataStores/{datastore_id}/branches/default_branch/documents:import\"\n", + " response = authed_session.post(\n", + " es_endpoint, data=json.dumps(payload), headers=header\n", + " )\n", + " print(f\"--{response.json()}\")\n", + " return response.json()[\"name\"]\n", + "\n", + "\n", + "import_lro = import_documents_from_gcs_jsonl(\n", + " project_id=PROJECT_ID,\n", + " location=LOCATION,\n", + " datastore_id=DATASTORE_ID,\n", + " gcs_uri=f\"{GCS_DIRECTORY_JSONL}{JSONL_FILENAME}\",\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "vttgvfVB2M-H" + }, + "source": [ + "## [Optional] Check the status of document import via polling\n", + "Optionally check the status of the long running operation for the import job. You can check this in the UI as well by looking at the \"activity\" tab of the corresponding Datastore" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "rYq_P_hDnSrh" + }, + "outputs": [], + "source": [ + "while True:\n", + " response = authed_session.get(\n", + " f\"https://discoveryengine.googleapis.com/v1/{import_lro}\",\n", + " )\n", + " try:\n", + " status = response.json()[\"done\"]\n", + " if status:\n", + " print(\"Import completed!\")\n", + " break\n", + " except KeyError:\n", + " print(\"Import in progress.\")\n", + " time.sleep(60)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "e-qjlyc_qk3U" + }, + "source": [ + "# Run queries with and without Metadata filter" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "cXo1n5dmQT4N" + }, + "source": [ + "## Sample search without filter\n", + "A basic search request issued to the Datastore\n", + "\n", + "We get relevant results from all four documents in the datastore" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "Pj3QBnCjQUVT" + }, + "outputs": [], + "source": [ + "test_query = \"Google revenue\"\n", + "\n", + "response = authed_session.post(\n", + " f\"https://discoveryengine.googleapis.com/v1alpha/projects/{PROJECT_ID}/locations/{LOCATION}/collections/default_collection/dataStores/{DATASTORE_ID}/servingConfigs/default_search:search\",\n", + " headers={\n", + " \"Content-Type\": \"application/json\",\n", + " },\n", + " json={\n", + " \"query\": test_query,\n", + " },\n", + ")\n", + "response.json()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "xn7DSXTD2XCT" + }, + "source": [ + "## Sample search with filter\n", + "\n", + "Now let's apply a filter to only show results relevant to Q2.\n", + "\n", + "You can see that now we only get results from a single document in the corpus which matches the filter.\n", + "\n", + "Note that this block shows a very basic way of querying a Datastore. You can find more information [here](https://cloud.google.com/generative-ai-app-builder/docs/preview-search-results)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "S9vz8canXd1r" + }, + "outputs": [], + "source": [ + "test_query = \"Google revenue\"\n", + "\n", + "response = authed_session.post(\n", + " f\"https://discoveryengine.googleapis.com/v1alpha/projects/{PROJECT_ID}/locations/{LOCATION}/collections/default_collection/dataStores/{DATASTORE_ID}/servingConfigs/default_search:search\",\n", + " headers={\n", + " \"Content-Type\": \"application/json\",\n", + " },\n", + " json={\n", + " \"query\": test_query,\n", + " \"filter\": 'quarter: ANY(\"Q2\")',\n", + " },\n", + ")\n", + "response.json()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "F_t_Afjbq1Ow" + }, + "source": [ + "# Cleanup\n", + "Clean up resources created in this notebook.\n", + "\n", + "## Clean up GCS bucket\n", + "\n", + "❗❗❗ Only run the below cells if you created a new bucket just for this notebook ❗❗❗\n", + "\n", + "Technically you could have used different buckets for documents, their Metadata and JSONL. If you happened to use the same **TEST** bucket for all of them, the following cells help you do the cleanup.\n", + "\n", + "To confirm the assumption above, you're asked to explicitly enter the Bucket name." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "n-SHsaMcu3YV" + }, + "outputs": [], + "source": [ + "def empty_bucket(bucket_name):\n", + " \"\"\"Deletes all objects in the specified GCS bucket.\"\"\"\n", + " client = storage.Client()\n", + " bucket = client.get_bucket(bucket_name)\n", + "\n", + " blobs = bucket.list_blobs() # List all blobs (objects)\n", + " for blob in blobs:\n", + " blob.delete() # Delete each blob\n", + "\n", + " print(f\"Bucket {bucket_name} emptied.\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "Uif1acXlwozv" + }, + "outputs": [], + "source": [ + "# Name of the bucket to be deleted. e.g. \"my_bucket\"\n", + "BUCKET_TO_DELETE = \"\" # @param {type:\"string\"}\n", + "\n", + "# Empty the bucket by deleting all files in it\n", + "empty_bucket(BUCKET_TO_DELETE)\n", + "\n", + "# Create a client object\n", + "client = storage.Client(project=PROJECT_ID)\n", + "\n", + "# Get the bucket object\n", + "bucket = client.get_bucket(BUCKET_TO_DELETE)\n", + "\n", + "# Delete the bucket\n", + "bucket.delete()\n", + "\n", + "print(f\"Bucket {BUCKET_TO_DELETE} deleted successfully.\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "T7xp_ujTxqLu" + }, + "source": [ + "## Delete local files\n", + "This will delete local folders for Documents, Metadata, and JSONL according to paths specified earlier in this notebook." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "rr70hhfExoPW" + }, + "outputs": [], + "source": [ + "shutil.rmtree(LOCAL_DOCS_PATH)\n", + "shutil.rmtree(LOCAL_METADATA_PATH)\n", + "shutil.rmtree(LOCAL_JSONL_PATH)\n", + "\n", + "print(\"Local files deleted successfully.\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "tGuk4ZnJk0S7" + }, + "source": [ + "## Delete the Search App\n", + "\n", + "Delete the App if you no longer need it\n", + "\n", + "Alternatively you can follow [these instructions](https://console.cloud.google.com/gen-app-builder/data-stores) to delete an App from the UI\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "QEfxXtzfk0rx" + }, + "outputs": [], + "source": [ + "response = authed_session.delete(\n", + " f\"https://discoveryengine.googleapis.com/v1alpha/projects/{PROJECT_ID}/locations/{LOCATION}/collections/default_collection/engines/{APP_ID}\",\n", + " headers={\"X-Goog-User-Project\": PROJECT_ID},\n", + ")\n", + "\n", + "print(response.text)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "Tgm5idL4DjjU" + }, + "source": [ + "## Delete the Datastores\n", + "Delete the Datastore if you no longer need it\n", + "\n", + "Alternatively you can follow [these instructions](https://console.cloud.google.com/gen-app-builder/data-stores) to delete a Datastore from the UI" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "vj8BpuS62tgt" + }, + "outputs": [], + "source": [ + "response = authed_session.delete(\n", + " f\"https://discoveryengine.googleapis.com/v1alpha/projects/{PROJECT_ID}/locations/{LOCATION}/collections/default_collection/dataStores/{DATASTORE_ID}\",\n", + " headers={\"X-Goog-User-Project\": PROJECT_ID},\n", + ")\n", + "\n", + "print(response.text)" + ] + } + ], + "metadata": { + "colab": { + "name": "ingesting_unstructured_documents_with_metadata.ipynb", + "toc_visible": true + }, + "kernelspec": { + "display_name": "Python 3", + "name": "python3" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} diff --git a/search/vais-building-blocks/inline_ingestion_of_documents.ipynb b/search/vais-building-blocks/inline_ingestion_of_documents.ipynb new file mode 100644 index 0000000..d4485d2 --- /dev/null +++ b/search/vais-building-blocks/inline_ingestion_of_documents.ipynb @@ -0,0 +1,1124 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "KsbFABffnCMA" + }, + "outputs": [], + "source": [ + "# Copyright 2024 Google LLC\n", + "#\n", + "# Licensed under the Apache License, Version 2.0 (the \"License\");\n", + "# you may not use this file except in compliance with the License.\n", + "# You may obtain a copy of the License at\n", + "#\n", + "# https://www.apache.org/licenses/LICENSE-2.0\n", + "#\n", + "# Unless required by applicable law or agreed to in writing, software\n", + "# distributed under the License is distributed on an \"AS IS\" BASIS,\n", + "# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n", + "# See the License for the specific language governing permissions and\n", + "# limitations under the License." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "q3pW20ECIDpX" + }, + "source": [ + "# Inline Ingestion of Documents into Vertex AI Search\n", + "\n", + " \n", + " \n", + " \n", + " \n", + "
    \n", + " \n", + " \"Google
    Open in Colab\n", + "
    \n", + "
    \n", + " \n", + " \"Google
    Open in Colab Enterprise\n", + "
    \n", + "
    \n", + " \n", + " \"Vertex
    Open in Vertex AI Workbench\n", + "
    \n", + "
    \n", + " \n", + " \"GitHub
    View on GitHub\n", + "
    \n", + "
    \n", + "\n", + "
    \n", + "\n", + "Share to:\n", + "\n", + "\n", + " \"LinkedIn\n", + "\n", + "\n", + "\n", + " \"Bluesky\n", + "\n", + "\n", + "\n", + " \"X\n", + "\n", + "\n", + "\n", + " \"Reddit\n", + "\n", + "\n", + "\n", + " \"Facebook\n", + "" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "YQpT9IS3K-Fn" + }, + "source": [ + "| | |\n", + "|----------|-------------|\n", + "| Author(s) | Jaival Desai, Hossein Mansour|\n", + "| Reviewers(s) | Lei Chen, Abhishek Bhagwat|\n", + "| Last updated | 2024-09-11: The first draft |" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "zNu-9XmEDF52" + }, + "source": [ + "# Overview\n", + "\n", + "In this notebook, we will demonstrate how to make an inline ingestion of documents into [Vertex AI Search](https://cloud.google.com/generative-ai-app-builder/docs/introduction) (VAIS) datastores.\n", + "\n", + "VAIS supports a variety of sources and data types. For [structured documents or unstructured documents, with or without metadata](https://cloud.google.com/generative-ai-app-builder/docs/prepare-data), it is advised to initially stage them on a GCS bucket or a BQ table and perform a subsequent [import](https://cloud.google.com/generative-ai-app-builder/docs/reference/rest/v1/projects.locations.collections.dataStores.branches.documents/import) by referring to those documents by their URI. This approach creates a source-of-truth which can be investigated in details and allows for the possibility of `Incremental` import or `Full` import depending on the choice of [ReconciliationMode](https://cloud.google.com/generative-ai-app-builder/docs/reference/rest/v1/ReconciliationMode). The `Full` option is particularly useful to resolve possible conflicts and duplicates.\n", + "\n", + "However in some cases customers may prefer an inline ingestion of documents for its simplicity or to help them stay compliant with some restrictions defined on Org level. Note that inline ingestion comes with some limitations including more strict [limits](https://cloud.google.com/generative-ai-app-builder/docs/reference/rest/v1/projects.locations.collections.dataStores.branches.documents#content) on the file size, and lower visibility on the UI given the fact that the content needs to be encoded into rawBytes.\n", + "\n", + "We will perform the following steps:\n", + "\n", + "- Create a VAIS Datastore\n", + "- Prepare sample documents\n", + "- Import sample documents (and other operations)\n", + "- Query the datastore\n", + "- Cleanup\n", + "\n", + "REST API is used throughout this notebook. Please consult the [official documentation](https://cloud.google.com/generative-ai-app-builder/docs/apis) for alternative ways to achieve the same goal, namely Client libraries and RPC.\n", + "\n", + "\n", + "# Vertex AI Search\n", + "Vertex AI Search (VAIS) is a fully-managed platform, powered by large language models, that lets you build AI-enabled search and recommendation experiences for your public or private websites or mobile applications\n", + "\n", + "VAIS can handle a diverse set of data sources including structured, unstructured, and website data, as well as data from third-party applications such as Jira, Salesforce, and Confluence.\n", + "\n", + "VAIS also has built-in integration with LLMs which enables you to provide answers to complex questions, grounded in your data\n", + "\n", + "#Using this Notebook\n", + "If you're running outside of Colab, depending on your environment you may need to install pip packages that are included in the Colab environment by default but are not part of the Python Standard Library. Outside of Colab you'll also notice comments in code cells that look like #@something, these trigger special Colab functionality but don't change the behavior of the notebook.\n", + "\n", + "This tutorial uses the following Google Cloud services and resources:\n", + "\n", + "- Service Usage API\n", + "- Discovery Engine\n", + "- Google Cloud Storage Client\n", + "\n", + "This notebook has been tested in the following environment:\n", + "\n", + "- Python version = 3.10.12\n", + "- google.cloud.storage = 2.8.0\n", + "- google.auth = 2.27.0\n", + "\n", + "# Getting Started\n", + "\n", + "The following steps are necessary to run this notebook, no matter what notebook environment you're using.\n", + "\n", + "If you're entirely new to Google Cloud, [get started here](https://cloud.google.com/docs/get-started)\n", + "\n", + "## Google Cloud Project Setup\n", + "\n", + "1. [Select or create a Google Cloud project](https://console.cloud.google.com/cloud-resource-manager). When you first create an account, you get a $300 free credit towards your compute/storage costs\n", + "2. [Make sure that billing is enabled for your project](https://cloud.google.com/billing/docs/how-to/modify-project)\n", + "3. [Enable the Service Usage API](https://console.cloud.google.com/apis/library/serviceusage.googleapis.com)\n", + "4. [Enable the Cloud Storage API](https://console.cloud.google.com/flows/enableapi?apiid=storage.googleapis.com)\n", + "5. [Enable the Discovery Engine API for your project](https://console.cloud.google.com/marketplace/product/google/discoveryengine.googleapis.com)\n", + "\n", + "## Google Cloud Permissions\n", + "\n", + "Ideally you should have [Owner role](https://cloud.google.com/iam/docs/understanding-roles) for your project to run this notebook. If that is not an option, you need at least the following [roles](https://cloud.google.com/iam/docs/granting-changing-revoking-access)\n", + "- **`roles/serviceusage.serviceUsageAdmin`** to enable APIs\n", + "- **`roles/iam.serviceAccountAdmin`** to modify service agent permissions\n", + "- **`roles/discoveryengine.admin`** to modify discoveryengine assets\n", + "- **`roles/storage.objectAdmin`** to modify and delete GCS buckets" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "slhopo_NhUrA" + }, + "source": [ + "#Setup Environment" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "lJFp9LUmrSOf" + }, + "source": [ + "## Authentication\n", + "\n", + " If you're using Colab, run the code in the next cell. Follow the pop-ups and authenticate with an account that has access to your Google Cloud [project](https://cloud.google.com/resource-manager/docs/creating-managing-projects#identifying_projects).\n", + "\n", + "If you're running this notebook somewhere besides Colab, make sure your environment has the right Google Cloud access. If that's a new concept to you, consider looking into [Application Default Credentials for your local environment](https://cloud.google.com/docs/authentication/provide-credentials-adc#local-dev) and [initializing the Google Cloud CLI](https://cloud.google.com/docs/authentication/gcloud). In many cases, running `gcloud auth application-default login` in a shell on the machine running the notebook kernel is sufficient.\n", + "\n", + "More authentication options are discussed [here](https://cloud.google.com/docs/authentication)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "x_miQy2C3DmT" + }, + "outputs": [], + "source": [ + "# Colab authentication.\n", + "import sys\n", + "\n", + "if \"google.colab\" in sys.modules:\n", + " from google.colab import auth\n", + "\n", + " auth.authenticate_user()\n", + " print(\"Authenticated\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "Fa9DrhVx3HQ0" + }, + "outputs": [], + "source": [ + "from google.auth import default\n", + "from google.auth.transport.requests import AuthorizedSession\n", + "\n", + "creds, _ = default()\n", + "authed_session = AuthorizedSession(creds)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "kdQPp72R11pd" + }, + "source": [ + "## Import Libraries" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "a2fjJjzn3LdX" + }, + "outputs": [], + "source": [ + "import base64\n", + "import json\n", + "import os\n", + "import shutil\n", + "import time\n", + "\n", + "import requests" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "MGXEinm3q1ks" + }, + "source": [ + "## Configure environment\n", + "\n", + "You can enter the ID for an existing Vertex AI Search Datastore to be used in this notebook.\n", + "\n", + "You can find more information regarding the `location` of datastores and associated limitations [here](https://cloud.google.com/generative-ai-app-builder/docs/locations#specify_a_multi-region_for_your_data_store). `global` is preferred unless there is a certain data residency requirement you have to comply with.\n", + "\n", + "The location of a Datastore is set at the time of creation and it should be called appropriately to query the Datastore.\n", + "\n", + "`LOCAL_DIRECTORY_DOCS` is used to store the sample files locally." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "RVGPkT132xno" + }, + "outputs": [], + "source": [ + "PROJECT_ID = \"\" # @param {type:\"string\"}\n", + "\n", + "# Vertex AI Search Parameters\n", + "DATASTORE_ID = \"\" # @param {type:\"string\"}\n", + "LOCATION = \"global\" # @param [\"global\", \"us\", \"eu\"]\n", + "LOCAL_DIRECTORY_DOCS = \"./sample_docs\" # @param {type:\"string\"}" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "hVJt8gfLhmwX" + }, + "source": [ + "# STEP 1. Create VAIS Datastore\n", + "\n", + "You can skip this section if you already have a datastore set up." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "hEOMvaNomc4q" + }, + "source": [ + "## Helper functions to [create a Datastore](https://cloud.google.com/generative-ai-app-builder/docs/reference/rest/v1/projects.locations.collections.dataStores/create)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "lIBkqEZYNNMA" + }, + "outputs": [], + "source": [ + "def create_datastore(project_id: str, location: str, datastore_id: str) -> int:\n", + " \"\"\"Create a datastore with doc mode and the basic digital parser\"\"\"\n", + " payload = {\n", + " \"displayName\": datastore_id,\n", + " \"industryVertical\": \"GENERIC\",\n", + " \"solutionTypes\": [\"SOLUTION_TYPE_SEARCH\"],\n", + " \"contentConfig\": \"CONTENT_REQUIRED\",\n", + " \"documentProcessingConfig\": {\n", + " \"defaultParsingConfig\": {\"digitalParsingConfig\": {}}\n", + " },\n", + " }\n", + " header = {\"X-Goog-User-Project\": project_id, \"Content-Type\": \"application/json\"}\n", + " es_endpoint = f\"https://discoveryengine.googleapis.com/v1/projects/{project_id}/locations/{location}/collections/default_collection/dataStores?dataStoreId={datastore_id}\"\n", + " response = authed_session.post(\n", + " es_endpoint, data=json.dumps(payload), headers=header\n", + " )\n", + " if response.status_code == 200:\n", + " print(f\"The creation of Datastore {datastore_id} is initiated.\")\n", + " print(\"It may take a few minutes for the Datastore to become available\")\n", + " else:\n", + " print(f\"Failed to create Datastore {datastore_id}\")\n", + " print(response.json())\n", + " return response.status_code" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "w2q8UZxwRd1m" + }, + "source": [ + "## Helper functions to issue [basic search on a Datastore](https://cloud.google.com/generative-ai-app-builder/docs/reference/rest/v1/projects.locations.collections.dataStores.servingConfigs/search)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "98bc0zgCWhqP" + }, + "outputs": [], + "source": [ + "def search_by_datastore(\n", + " project_id: str, location: str, datastore_id: str, query: str\n", + ") -> requests.Response:\n", + " \"\"\"Searches a datastore using the provided query.\"\"\"\n", + " response = authed_session.post(\n", + " f\"https://discoveryengine.googleapis.com/v1/projects/{project_id}/locations/{location}/collections/default_collection/dataStores/{datastore_id}/servingConfigs/default_search:search\",\n", + " headers={\n", + " \"Content-Type\": \"application/json\",\n", + " },\n", + " json={\"query\": query, \"pageSize\": 1},\n", + " )\n", + " return response" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "fJ3ULZOeR-E2" + }, + "source": [ + "## Helper functions to check whether or not a Datastore already exists" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "ShppuvcxWBut" + }, + "outputs": [], + "source": [ + "def datastore_exists(project_id: str, location: str, datastore_id: str) -> bool:\n", + " \"\"\"Check if a datastore exists.\"\"\"\n", + " response = search_by_datastore(project_id, location, datastore_id, \"test\")\n", + " status_code = response.status_code\n", + " if status_code == 200:\n", + " return True\n", + " if status_code == 404:\n", + " return False\n", + " raise Exception(f\"Error: {status_code}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "NnCzxjV9SVO_" + }, + "source": [ + "## Create a Datastore with the provided ID if it doesn't exist" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "nBnAc1NIV59z" + }, + "outputs": [], + "source": [ + "# Create Chunk mode Datastore if it doesn't exist\n", + "if datastore_exists(PROJECT_ID, LOCATION, DATASTORE_ID):\n", + " print(f\"Datastore {DATASTORE_ID} already exists.\")\n", + "else:\n", + " create_datastore(PROJECT_ID, LOCATION, DATASTORE_ID)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "MF07QdwxW_1n" + }, + "source": [ + "## [Optional] Check if the Datastore is created successfully\n", + "\n", + "\n", + "The Datastore is polled to track when it becomes available.\n", + "\n", + "This may take a few minutes after the datastore creation is initiated" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "S7xR3uX8XEnH" + }, + "outputs": [], + "source": [ + "while not datastore_exists(PROJECT_ID, LOCATION, DATASTORE_ID):\n", + " print(f\"Datastore {DATASTORE_ID} is still being created.\")\n", + " time.sleep(30)\n", + "print(f\"Datastore {DATASTORE_ID} is created successfully.\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "FSgKPm-nnB81" + }, + "source": [ + "# STEP 2. Prepare sample documents" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "9o-wr_8_7g0D" + }, + "source": [ + "## Create a folder to store the files locally" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "RgcYQVxu7mQO" + }, + "outputs": [], + "source": [ + "# Check if the folder already exists\n", + "if not os.path.exists(LOCAL_DIRECTORY_DOCS):\n", + " # Create the folder\n", + " os.makedirs(LOCAL_DIRECTORY_DOCS)\n", + " print(f\"Folder '{LOCAL_DIRECTORY_DOCS}' created successfully!\")\n", + "else:\n", + " print(f\"Folder '{LOCAL_DIRECTORY_DOCS}' already exists.\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "6ekzngh0nbdY" + }, + "source": [ + "## Helper function to download pdf files and store them locally" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "SpDDcZ9Bmtsl" + }, + "outputs": [], + "source": [ + "def download_pdfs(\n", + " url_list: list[str], save_directory: str = LOCAL_DIRECTORY_DOCS\n", + ") -> list[str]:\n", + " \"\"\"Downloads PDFs from a list of URLs and saves them to a specified directory.\n", + "\n", + " Args:\n", + " url_list: A list of URLs pointing to PDF files.\n", + " save_directory: The directory where the PDFs will be saved. Defaults to LOCAL_DIRECTORY_DOCS.\n", + "\n", + " Returns:\n", + " A list of file paths where the PDFs were saved.\n", + " \"\"\"\n", + "\n", + " pdf_file_paths = []\n", + "\n", + " # Create the save directory if it doesn't exist\n", + " if not os.path.exists(save_directory):\n", + " os.makedirs(save_directory)\n", + "\n", + " for i, url in enumerate(url_list):\n", + " try:\n", + " response = requests.get(url)\n", + " response.raise_for_status()\n", + "\n", + " # Construct the full file path within the save directory\n", + " file_name = f\"downloaded_pdf_{i+1}.pdf\"\n", + " file_path = os.path.join(save_directory, file_name)\n", + "\n", + " with open(file_path, \"wb\") as f:\n", + " f.write(response.content)\n", + "\n", + " pdf_file_paths.append(file_path)\n", + " print(f\"Downloaded PDF from {url} and saved to {file_path}\")\n", + " except requests.exceptions.RequestException as e:\n", + " print(f\"Error downloading PDF from {url}: {e}\")\n", + "\n", + " return pdf_file_paths" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "qO-MlhvrnkiZ" + }, + "source": [ + "## Download sample PDF files" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "wsw0wSOnm0LG" + }, + "outputs": [], + "source": [ + "file_urls = [\n", + " \"https://abc.xyz/assets/91/b3/3f9213d14ce3ae27e1038e01a0e0/2024q1-alphabet-earnings-release-pdf.pdf\",\n", + " \"https://abc.xyz/assets/19/e4/3dc1d4d6439c81206370167db1bd/2024q2-alphabet-earnings-release.pdf\",\n", + "]\n", + "\n", + "pdf_variables = download_pdfs(file_urls)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "aoVBLH8rnrnx" + }, + "source": [ + "## Create a sample text file and store locally" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "5J1h_sAym3kJ" + }, + "outputs": [], + "source": [ + "sample_text = \"\"\"\n", + "MOUNTAIN VIEW, Calif. - January 30, 2024 - Alphabet Inc. (NASDAQ: GOOG, GOOGL) today announced\n", + "financial results for the quarter and fiscal year ended December 31, 2023.\n", + "Sundar Pichai, CEO, said: \"We are pleased with the ongoing strength in Search and the growing contribution from\n", + "YouTube and Cloud. Each of these is already benefiting from our AI investments and innovation. As we enter the\n", + "Gemini era, the best is yet to come.\"\n", + "Ruth Porat, President and Chief Investment Officer; CFO said: \"We ended 2023 with very strong fourth quarter\n", + "financial results, with Q4 consolidated revenues of $86 billion, up 13% year over year. We remain committed to our\n", + "work to durably re-engineer our cost base as we invest to support our growth opportunities.\"\n", + "\"\"\"\n", + "\n", + "\n", + "def save_string_to_file(\n", + " string_to_save, filename=\"doc_3.txt\", save_directory=LOCAL_DIRECTORY_DOCS\n", + "):\n", + " \"\"\"Saves a string to a text file within a specified directory.\n", + "\n", + " Args:\n", + " string_to_save: The string content to be saved.\n", + " filename: The desired name for the output file (default: \"doc_3.txt\").\n", + " save_directory: The directory where the file will be saved (default: LOCAL_DIRECTORY_DOCS).\n", + "\n", + " Returns:\n", + " None\n", + " \"\"\"\n", + "\n", + " # Create the save directory if it doesn't exist\n", + " if not os.path.exists(save_directory):\n", + " os.makedirs(save_directory)\n", + "\n", + " # Construct the full file path within the save directory\n", + " file_path = os.path.join(save_directory, filename)\n", + "\n", + " try:\n", + " with open(file_path, \"w\", encoding=\"utf-8\") as file:\n", + " file.write(string_to_save)\n", + " print(f\"String successfully saved to {file_path}\")\n", + " except OSError as e:\n", + " print(f\"An error occurred while saving the file: {e}\")\n", + "\n", + "\n", + "save_string_to_file(sample_text)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "UlpkwFdSoCxz" + }, + "source": [ + "## Helper function to convert the content of a file to Base64 encoding" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "R06xrOx3Mdvf" + }, + "outputs": [], + "source": [ + "def file_to_base64(file_path):\n", + " \"\"\"Converts the content of a file to Base64 encoding.\n", + "\n", + " Args:\n", + " file_path: The path to the file.\n", + "\n", + " Returns:\n", + " The Base64 encoded string representing the file's content.\n", + " \"\"\"\n", + "\n", + " with open(file_path, \"rb\") as file:\n", + " file_data = file.read()\n", + " base64_encoded_data = base64.b64encode(file_data).decode(\"utf-8\")\n", + " return base64_encoded_data" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "Dxko2yr7oOWN" + }, + "source": [ + "## Convert sample files to Base64 encoding" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "wLWaNs7wnDlu" + }, + "outputs": [], + "source": [ + "content_doc_1 = file_to_base64(LOCAL_DIRECTORY_DOCS + \"/downloaded_pdf_1.pdf\")\n", + "content_doc_2 = file_to_base64(LOCAL_DIRECTORY_DOCS + \"/downloaded_pdf_2.pdf\")\n", + "content_doc_3 = file_to_base64(LOCAL_DIRECTORY_DOCS + \"/doc_3.txt\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "9AAYFQAjogMP" + }, + "source": [ + "## Create JSON documents from sample contents\n", + "\n", + "Here we create [`Documents`](https://cloud.google.com/generative-ai-app-builder/docs/reference/rest/v1/projects.locations.collections.dataStores.branches.documents#Document) in VAIS terminology based on contents from sample files created earlier.\n", + "\n", + "Note that the field [`content`](https://cloud.google.com/generative-ai-app-builder/docs/reference/rest/v1/projects.locations.collections.dataStores.branches.documents#Document.Content) in the document references rawBytes as opposed to `uri` that is used when the file is staged elsewhere.\n", + "\n", + "mimeType should be consistent with the format of the files to be ingested (e.g. application/pdf). See a list of supported mimeTypes [here](https://cloud.google.com/generative-ai-app-builder/docs/reference/rest/v1/projects.locations.collections.dataStores.branches.documents#Document.Content)\n", + "\n", + "We add some metadata to each document as well to demonstrate this more advanced functionality. This is optional and you can ingest the content with no metadata as well." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "cRU3hZPw_MUn" + }, + "outputs": [], + "source": [ + "my_document_1 = {\n", + " \"id\": \"doc-1\",\n", + " \"structData\": {\"title\": \"test_doc_1\", \"color_theme\": \"blue\"},\n", + " \"content\": {\"mimeType\": \"application/pdf\", \"rawBytes\": content_doc_1},\n", + "}\n", + "my_document_2 = {\n", + " \"id\": \"doc-2\",\n", + " \"structData\": {\"title\": \"test_doc_2\", \"color_theme\": \"red\"},\n", + " \"content\": {\"mimeType\": \"application/pdf\", \"rawBytes\": content_doc_2},\n", + "}\n", + "my_document_3 = {\n", + " \"id\": \"doc-3\",\n", + " \"structData\": {\"title\": \"test_doc_3\", \"color_theme\": \"green\"},\n", + " \"content\": {\"mimeType\": \"text/plain\", \"rawBytes\": content_doc_3},\n", + "}" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "cKrqND3Trt1Q" + }, + "source": [ + "# STEP 3. Import, List, Get, and Delete documents\n", + "\n", + "In this section we demonstrate some common operations on documents. You can find a more complete list [here](https://cloud.google.com/generative-ai-app-builder/docs/reference/rest/v1/projects.locations.collections.dataStores.branches.documents)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "xw3DcHRFqJyG" + }, + "source": [ + "## Inline [import](https://cloud.google.com/generative-ai-app-builder/docs/reference/rest/v1/projects.locations.collections.dataStores.branches.documents/import) of documents\n", + "\n", + "This block contains the main logic to be demonstrated in this notebook that is an inline ingestion of documents." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "NHsyjWqb3065" + }, + "outputs": [], + "source": [ + "def import_documents_rawbytes(project_id: str, location: str, datastore_id: str) -> str:\n", + " \"\"\"Imports unstructured documents Inline.\"\"\"\n", + " payload = {\n", + " \"reconciliationMode\": \"INCREMENTAL\",\n", + " \"inlineSource\": {\"documents\": [my_document_1, my_document_2, my_document_3]},\n", + " }\n", + " header = {\"Content-Type\": \"application/json\"}\n", + " es_endpoint = f\"https://discoveryengine.googleapis.com/v1/projects/{project_id}/locations/{location}/collections/default_collection/dataStores/{datastore_id}/branches/default_branch/documents:import\"\n", + " response = authed_session.post(\n", + " es_endpoint, data=json.dumps(payload), headers=header\n", + " )\n", + " print(f\"--{response.json()}\")\n", + " return response.json()\n", + "\n", + "\n", + "import_documents_rawbytes(PROJECT_ID, LOCATION, DATASTORE_ID)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "7jXQ3hD1a5Eo" + }, + "source": [ + "## List all documents\n", + "\n", + "[List](https://cloud.google.com/generative-ai-app-builder/docs/reference/rest/v1/projects.locations.collections.dataStores.branches.documents/list) all documents and their contents for a datastore. A maximum of 1000 documents are retrieved together with a page token to retrieve the next batch of documents (i.e. pagination)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "TMMnEwg7bJQy" + }, + "outputs": [], + "source": [ + "def list_documents_datastore(\n", + " project_id: str, location: str, data_store_id: str\n", + ") -> list[dict[str, str]] | None:\n", + " \"\"\"Lists documents in a specified data store using the REST API.\n", + "\n", + " Args:\n", + " project_id: The ID of your Google Cloud project.\n", + " location: The location of your data store.\n", + " Values: \"global\", \"us\", \"eu\"\n", + " data_store_id: The ID of the datastore.\n", + "\n", + " Returns:\n", + " The JSON response containing the list of documents, or None if an error occurs.\n", + " \"\"\"\n", + "\n", + " base_url = (\n", + " f\"{location}-discoveryengine.googleapis.com\"\n", + " if location != \"global\"\n", + " else \"discoveryengine.googleapis.com\"\n", + " )\n", + " url = f\"https://{base_url}/v1alpha/projects/{project_id}/locations/{location}/collections/default_collection/dataStores/{data_store_id}/branches/default_branch/documents\"\n", + "\n", + " try:\n", + " # Assuming 'authed_session' is available and properly configured for authentication\n", + " response = authed_session.get(url)\n", + " response.raise_for_status() # Raise an exception for bad status codes\n", + " documents = response.json()\n", + " print(\n", + " f\"Successfully retrieved {len(documents.get('documents', []))} document(s).\\n\"\n", + " )\n", + " return [document for document in documents.get(\"documents\", [])]\n", + "\n", + " except requests.exceptions.RequestException as e:\n", + " print(f\"Error listing documents: {e}\")\n", + " return None\n", + "\n", + "\n", + "list_documents_datastore(PROJECT_ID, LOCATION, DATASTORE_ID)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "apGqssFpayOd" + }, + "source": [ + "## Get a specific document\n", + "\n", + "[Get](https://cloud.google.com/generative-ai-app-builder/docs/reference/rest/v1/projects.locations.collections.dataStores.branches.documents/get) a document and some of its details (regarding indexing status) by referencing the document ID." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "06wLHBmMcHIy" + }, + "outputs": [], + "source": [ + "DOCUMENT_ID = \"doc-1\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "1Y5HopVnb_xq" + }, + "outputs": [], + "source": [ + "def get_document_datastore(\n", + " project_id: str, location: str, data_store_id: str, document_id: str\n", + ") -> dict[str, str] | None:\n", + " \"\"\"Gets a specific document from a data store using the REST API.\n", + "\n", + " Args:\n", + " project_id: The ID of your Google Cloud project.\n", + " location: The location of your data store.\n", + " Values: \"global\", \"us\", \"eu\"\n", + " data_store_id: The ID of the datastore.\n", + " document_id: The ID of the document to retrieve.\n", + "\n", + " Returns:\n", + " The JSON response containing the document data, or None if an error occurs.\n", + " \"\"\"\n", + "\n", + " base_url = (\n", + " f\"{location}-discoveryengine.googleapis.com\"\n", + " if location != \"global\"\n", + " else \"discoveryengine.googleapis.com\"\n", + " )\n", + " url = f\"https://{base_url}/v1alpha/projects/{project_id}/locations/{location}/collections/default_collection/dataStores/{data_store_id}/branches/default_branch/documents/{document_id}\"\n", + "\n", + " try:\n", + " # Assuming 'authed_session' is available and properly configured for authentication\n", + " response = authed_session.get(url)\n", + " response.raise_for_status() # Raise an exception for bad status codes\n", + " document = response.json()\n", + " print(f\"Successfully retrieved document with ID: {document_id}\\n\")\n", + " return document\n", + "\n", + " except requests.exceptions.RequestException as e:\n", + " print(f\"Error getting document: {e}\")\n", + " return None\n", + "\n", + "\n", + "get_document_datastore(PROJECT_ID, LOCATION, DATASTORE_ID, DOCUMENT_ID)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "bUqTVMaMa7X9" + }, + "source": [ + "## Delete a document\n", + "\n", + "[Delete](https://cloud.google.com/generative-ai-app-builder/docs/reference/rest/v1/projects.locations.collections.dataStores.branches.documents/delete) a particular document from a datastore by referencing its ID.\n", + "\n", + "The line that actually deletes the document is commented out here as we need all documents in a subsequent section.\n", + "\n", + "Note that if you are leveraging GCS/BQ staging approach for importing, a Full import from the source will make the document reappear in the datastore. Same goes with a page within an advanced website datastore which may reappear by subsequent re-crawls." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "9-fC8pNjdH5C" + }, + "outputs": [], + "source": [ + "def delete_document_datastore(\n", + " project_id: str, location: str, data_store_id: str, document_id: str\n", + ") -> bool:\n", + " \"\"\"Deletes a specific document from a data store using the REST API.\n", + "\n", + " Args:\n", + " project_id: The ID of your Google Cloud project.\n", + " location: The location of your data store.\n", + " Values: \"global\", \"us\", \"eu\"\n", + " data_store_id: The ID of the datastore.\n", + " document_id: The ID of the document to delete.\n", + "\n", + " Returns:\n", + " True if the document was deleted successfully, False otherwise.\n", + " \"\"\"\n", + "\n", + " base_url = (\n", + " f\"{location}-discoveryengine.googleapis.com\"\n", + " if location != \"global\"\n", + " else \"discoveryengine.googleapis.com\"\n", + " )\n", + " url = f\"https://{base_url}/v1alpha/projects/{project_id}/locations/{location}/collections/default_collection/dataStores/{data_store_id}/branches/default_branch/documents/{document_id}\"\n", + "\n", + " try:\n", + " # Assuming 'authed_session' is available and properly configured for authentication\n", + " response = authed_session.delete(url)\n", + " response.raise_for_status() # Raise an exception for bad status codes\n", + " print(f\"Successfully deleted document with ID: {document_id}\\n\")\n", + " return True\n", + "\n", + " except requests.exceptions.RequestException as e:\n", + " print(f\"Error deleting document: {e}\")\n", + " return False\n", + "\n", + "\n", + "# delete_document_datastore(PROJECT_ID, LOCATION, DATASTORE_ID, DOCUMENT_ID)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "C2JPuO53q42c" + }, + "source": [ + "# STEP 4. Run queries with and without Metadata filter" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "0wAa9WFOq8jd" + }, + "source": [ + "## Sample search without filter\n", + "A basic search request issued to the Datastore\n", + "\n", + "We get relevant results from all three documents in the datastore" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "m9GZazgQRTzL" + }, + "outputs": [], + "source": [ + "test_query = \"Google revenue\"\n", + "\n", + "response = authed_session.post(\n", + " f\"https://discoveryengine.googleapis.com/v1alpha/projects/{PROJECT_ID}/locations/{LOCATION}/collections/default_collection/dataStores/{DATASTORE_ID}/servingConfigs/default_search:search\",\n", + " headers={\n", + " \"Content-Type\": \"application/json\",\n", + " },\n", + " json={\n", + " \"query\": test_query,\n", + " },\n", + ")\n", + "response.json()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "JZwPBsOHrGt2" + }, + "source": [ + "## Sample search with filter\n", + "\n", + "Now let's apply a filter to showcase how metadata can be used to influence the results.\n", + "\n", + "We issue the same query as above, but limit the results to color_theme \"red\". A expected we only get one result back\n", + "\n", + "Note that this block shows a very basic way of querying a Datastore. You can find more information [here](https://cloud.google.com/generative-ai-app-builder/docs/preview-search-results)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "y9-N7M--RYuJ" + }, + "outputs": [], + "source": [ + "test_query = \"Google revenue\"\n", + "\n", + "response = authed_session.post(\n", + " f\"https://discoveryengine.googleapis.com/v1alpha/projects/{PROJECT_ID}/locations/{LOCATION}/collections/default_collection/dataStores/{DATASTORE_ID}/servingConfigs/default_search:search\",\n", + " headers={\n", + " \"Content-Type\": \"application/json\",\n", + " },\n", + " json={\n", + " \"query\": test_query,\n", + " \"filter\": 'color_theme: ANY(\"red\")',\n", + " },\n", + ")\n", + "response.json()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "b_9s1JT6AS7o" + }, + "source": [ + "#Cleanup\n", + "Clean up resources created in this notebook.\n", + "\n", + "Set `DELETE_RESOURCES` flag to `True` to delete resources." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "9yeinaBzeok9" + }, + "outputs": [], + "source": [ + "DELETE_RESOURCES = False" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "4cZ8lvPQ3OnY" + }, + "source": [ + "## Delete local files" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "OqoAKBai3A4i" + }, + "outputs": [], + "source": [ + "if DELETE_RESOURCES:\n", + " shutil.rmtree(LOCAL_DIRECTORY_DOCS)\n", + "\n", + " print(\"Local files deleted successfully.\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "L0aU2DdTckUo" + }, + "source": [ + "## Delete the Datastore\n", + "Delete the Datastore if you no longer need it\n", + "\n", + "Alternatively you can follow [these instructions](https://console.cloud.google.com/gen-app-builder/data-stores) to delete a Datastore from the UI" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "pBKcL_oicjxL" + }, + "outputs": [], + "source": [ + "if DELETE_RESOURCES:\n", + " response = authed_session.delete(\n", + " f\"https://discoveryengine.googleapis.com/v1alpha/projects/{PROJECT_ID}/locations/{LOCATION}/collections/default_collection/dataStores/{DATASTORE_ID}\",\n", + " headers={\"X-Goog-User-Project\": PROJECT_ID},\n", + " )\n", + "\n", + " print(response.json())" + ] + } + ], + "metadata": { + "colab": { + "name": "inline_ingestion_of_documents.ipynb", + "toc_visible": true + }, + "kernelspec": { + "display_name": "Python 3", + "name": "python3" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} diff --git a/search/vais-building-blocks/manual_recrawl_urls_with_trigger.ipynb b/search/vais-building-blocks/manual_recrawl_urls_with_trigger.ipynb new file mode 100644 index 0000000..083e006 --- /dev/null +++ b/search/vais-building-blocks/manual_recrawl_urls_with_trigger.ipynb @@ -0,0 +1,435 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "5XNYlDkDLpqU" + }, + "outputs": [], + "source": [ + "# Copyright 2025 Google LLC\n", + "#\n", + "# Licensed under the Apache License, Version 2.0 (the \"License\");\n", + "# you may not use this file except in compliance with the License.\n", + "# You may obtain a copy of the License at\n", + "#\n", + "# https://www.apache.org/licenses/LICENSE-2.0\n", + "#\n", + "# Unless required by applicable law or agreed to in writing, software\n", + "# distributed under the License is distributed on an \"AS IS\" BASIS,\n", + "# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n", + "# See the License for the specific language governing permissions and\n", + "# limitations under the License." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "5tR528hOD4Dx" + }, + "source": [ + "# Event-based Triggering of Manual Recrawl for Vertex AI Search Advanced Website Datastores\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + "
    \n", + " \n", + " \"Google
    Open in Colab\n", + "
    \n", + "
    \n", + " \n", + " \"Google
    Open in Colab Enterprise\n", + "
    \n", + "
    \n", + " \n", + " \"Vertex
    Open in Vertex AI Workbench\n", + "
    \n", + "
    \n", + " \n", + " \"GitHub
    View on GitHub\n", + "
    \n", + "
    \n", + "\n", + "
    \n", + "\n", + "Share to:\n", + "\n", + "\n", + " \"LinkedIn\n", + "\n", + "\n", + "\n", + " \"Bluesky\n", + "\n", + "\n", + "\n", + " \"X\n", + "\n", + "\n", + "\n", + " \"Reddit\n", + "\n", + "\n", + "\n", + " \"Facebook\n", + "" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "pkd93iDpEBWx" + }, + "source": [ + "| | |\n", + "|----------|-------------|\n", + "| Author(s) | Hossein Mansour|\n", + "| Reviewers(s) | Abhishek Bhagwat|\n", + "| Last updated | 2025-01-03: Initial commit |" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "yAnTektvEQjb" + }, + "source": [ + "# Overview\n", + "\n", + "In this notebook, we demonstrate how to automate [manual recrawl](https://cloud.google.com/generative-ai-app-builder/docs/recrawl-websites#manual_refresh) to keep an [Advanced Website Search](https://cloud.google.com/generative-ai-app-builder/docs/about-advanced-features#advanced-website-indexing) datastores within [Vertex AI Search](https://cloud.google.com/generative-ai-app-builder/docs/introduction).\n", + "\n", + "We focus on a particular workflow where the list of URLs to-be-crawled land as a JSON file in a GCS bucket. This then triggers parsing of the URLs and passing them to the manual Recrawl API call. One can automate different workflows where, for instance, the list of new URLs is added to a BQ table, etc.\n", + "\n", + "The workflow demonstrated here is useful for automating other workflows. An example of that would be to trigger a document import as a new PDF file lands on a GCS bucket.\n", + "\n", + "You can find more information about advanced website search datastore and its differences with the basic website search datastore [here](https://cloud.google.com/generative-ai-app-builder/docs/about-advanced-features#advanced-website-indexing).\n", + "\n", + "The web pages in an advanced website search datastore are refreshed in the following ways:\n", + "\n", + "* **Automatic refresh**: Discovers added, deleted, and updated pages and reindexes those on a best-effort basis. The expected indexing latency is in the order of 2 weeks at the time of preparing this notebook.\n", + "* [**Manual refresh**](https://cloud.google.com/generative-ai-app-builder/docs/recrawl-websites): Customers can initiate manual recrawl of an explicit list of URLs (not URL patterns) within certain [limits](https://cloud.google.com/generative-ai-app-builder/docs/recrawl-websites#limits_on_recrawling). While there is no SLO around manual re-crawls, they typically happen within minutes to hours depending on the size.\n", + "* [**Sitemap-based refresh**](https://cloud.google.com/generative-ai-app-builder/docs/index-refresh-sitemap): Customers can submit and use sitemaps to index and refresh the web pages in your data store. This feature supports only XML sitemaps and sitemap indexes. The indexing latency with this approach is in the order of hours.\n", + "\n", + "The automation demonstrated in this notebook is relevant for manual recrawl as stated above.\n", + "\n", + "As opposed to other notebooks in the repository, this notebook is not self contained and requires an existing datastore. We demonstrate a step-by-step guide on how to add the trigger and subsequent automation from within the UI. We also provide a gcloud function to automate part of the process.\n", + "\n", + "We will perform the following steps:\n", + "\n", + "- Creating a GCS bucket for staging\n", + "- Creating cloud function and trigger via UI\n", + "- Deploying the function\n", + "- Testing with a sample JSON\n", + "\n", + "\n", + "# Vertex AI Search\n", + "Vertex AI Search (VAIS) is a fully-managed platform, powered by large language models, that lets you build AI-enabled search and recommendation experiences for your public or private websites or mobile applications\n", + "\n", + "VAIS can handle a diverse set of data sources including structured, unstructured, and website data, as well as data from third-party applications such as Jira, Salesforce, and Confluence.\n", + "\n", + "VAIS also has built-in integration with LLMs which enables you to provide answers to complex questions, grounded in your data\n", + "\n", + "# Using this Notebook\n", + "This notebook cannot be run as is. It's more of a step-by-step guide on how to achieve the goal via UI.\n", + "\n", + "## Google Cloud Project Setup\n", + "\n", + "1. [Select or create a Google Cloud project](https://console.cloud.google.com/cloud-resource-manager). When you first create an account, you get a $300 free credit towards your compute/storage costs\n", + "2. [Make sure that billing is enabled for your project](https://cloud.google.com/billing/docs/how-to/modify-project)\n", + "3. [Enable the Service Usage API](https://console.cloud.google.com/apis/library/serviceusage.googleapis.com)\n", + "4. [Enable the Cloud Storage API](https://console.cloud.google.com/flows/enableapi?apiid=storage.googleapis.com)\n", + "5. [Enable the Discovery Engine API for your project](https://console.cloud.google.com/marketplace/product/google/discoveryengine.googleapis.com)\n", + "\n", + "## Google Cloud Permissions\n", + "\n", + "Ideally you should have [Owner role](https://cloud.google.com/iam/docs/understanding-roles) for your project to run this notebook. If that is not an option, you need at least the following [roles](https://cloud.google.com/iam/docs/granting-changing-revoking-access)\n", + "- **`roles/serviceusage.serviceUsageAdmin`** to enable APIs\n", + "- **`roles/iam.serviceAccountAdmin`** to modify service agent permissions\n", + "- **`roles/discoveryengine.admin`** to modify discoveryengine assets" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "49x_J4vWOuNg" + }, + "source": [ + "# Create GCS bucket\n", + "\n", + "As a prerequisite, we need a GCS bucket to use it as a staging area. JSON files containing URLs-to-be-crawled will land there.\n", + "\n", + "We call this bucket `recrawl_test` and use `us-central1` for the location.\n", + "\n", + "![create_bucket.png](https://storage.googleapis.com/github-repo/generative-ai/search/vais-building-blocks/manual_recrawl_urls_with_trigger/create_bucket.png)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "TZ4lJ9bDRzDi" + }, + "source": [ + "# Create cloud run function\n", + "\n", + "As the next step, we create a cloud run function from within the bucket\n", + "![create_function.png](https://storage.googleapis.com/github-repo/generative-ai/search/vais-building-blocks/manual_recrawl_urls_with_trigger/create_function.png)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "vVpjKl0SSxCR" + }, + "source": [ + "# Basic settings of the cloud run function\n", + "\n", + "Here we apply the basic settings to our cloud function. Specifically, we use the same region as our bucket (`us-central1` in this example), set the event type to finalized, and give our function a meaningful name.\n", + "\n", + "![function_basics.png](https://storage.googleapis.com/github-repo/generative-ai/search/vais-building-blocks/manual_recrawl_urls_with_trigger/function_basics.png)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "cWUrF50JTE4b" + }, + "source": [ + "# Add the function source\n", + "\n", + "In this step we add the function source. Python 3.12 is used for this example. We need an entry point that is the function used in main.py which runs upon triggering of the associated cloud function.\n", + "\n", + "While a more manual application of the trigger was possible, we use [python function framework](https://github.com/GoogleCloudPlatform/functions-framework-python) which is the recommended way at the time of preparing this notebook.\n", + "\n", + "You can see the full source, including authentication, in the code block below.\n", + "\n", + "Note that you need to update the source with your own project_id and datastore_id. It is also recommended to use an environment variable for that.\n", + "\n", + "![function_source.png](https://storage.googleapis.com/github-repo/generative-ai/search/vais-building-blocks/manual_recrawl_urls_with_trigger/function_source.png)\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "toPqo9SDWNBl" + }, + "outputs": [], + "source": [ + "import json\n", + "\n", + "import functions_framework\n", + "from google.auth import default\n", + "from google.auth.transport.requests import Request as GoogleAuthRequest\n", + "from google.cloud import storage\n", + "import requests\n", + "\n", + "# Replace with your actual values\n", + "PROJECT_ID = \"your_project_id\"\n", + "DATA_STORE_ID = \"your_datastore_id\"\n", + "\n", + "\n", + "@functions_framework.cloud_event\n", + "def recrawl_uris(cloud_event):\n", + " \"\"\"\n", + " Cloud Function triggered by Cloud Storage events (file creation).\n", + "\n", + " Args:\n", + " cloud_event (functions_framework.cloud_event.CloudEvent): The CloudEvent that triggered this function.\n", + " \"\"\"\n", + "\n", + " data = cloud_event.data\n", + " bucket_name = data[\"bucket\"]\n", + " file_name = data[\"name\"]\n", + " print(f\"File {file_name} created in bucket {bucket_name}.\")\n", + "\n", + " # Process only for finalized objects. An object is in finalized state if it is written to the bucket or an existing object is overwritten\n", + "\n", + " if (\n", + " file_name.endswith(\".json\")\n", + " and cloud_event[\"type\"] == \"google.cloud.storage.object.v1.finalized\"\n", + " ):\n", + " try:\n", + " # Read the URIs from the JSON file\n", + " uris = read_uris_from_gcs(bucket_name, file_name)\n", + "\n", + " if not uris:\n", + " print(\"No URIs found in the JSON file.\")\n", + " return\n", + "\n", + " print(\"URIs to recrawl:\", uris)\n", + "\n", + " # Recrawl the URIs using the Discovery Engine API\n", + " recrawl_uris_with_api(uris)\n", + "\n", + " except Exception as e:\n", + " print(f\"Error processing file: {e}\")\n", + "\n", + "\n", + "def read_uris_from_gcs(bucket_name, file_name):\n", + " \"\"\"\n", + " Reads the URIs from a JSON file in a GCS bucket.\n", + "\n", + " Args:\n", + " bucket_name (str): Name of the GCS bucket.\n", + " file_name (str): Name of the JSON file.\n", + "\n", + " Returns:\n", + " list: List of URIs, or None if an error occurs.\n", + " \"\"\"\n", + "\n", + " storage_client = storage.Client()\n", + " bucket = storage_client.bucket(bucket_name)\n", + " blob = bucket.blob(file_name)\n", + "\n", + " try:\n", + " file_content = blob.download_as_text()\n", + " data = json.loads(file_content)\n", + " return data.get(\"uris\", [])\n", + " except Exception as e:\n", + " print(f\"Error reading URIs from {bucket_name}/{file_name}: {e}\")\n", + " return None\n", + "\n", + "\n", + "def recrawl_uris_with_api(uris):\n", + " \"\"\"\n", + " Re-crawls the specified URIs using the Discovery Engine API.\n", + "\n", + " Args:\n", + " uris (list): List of URIs to recrawl.\n", + " \"\"\"\n", + "\n", + " # fetch bearer token to make REST API call\n", + " creds, _ = default()\n", + " auth_req = GoogleAuthRequest()\n", + " creds.refresh(auth_req)\n", + " access_token = creds.token\n", + "\n", + " # recrawl API endpoint to be invoked\n", + " url = f\"https://discoveryengine.googleapis.com/v1alpha/projects/{PROJECT_ID}/locations/global/collections/default_collection/dataStores/{DATA_STORE_ID}/siteSearchEngine:recrawlUris\"\n", + "\n", + " headers = {\n", + " \"Authorization\": f\"Bearer {access_token}\",\n", + " \"Content-Type\": \"application/json\",\n", + " \"X-Goog-User-Project\": PROJECT_ID,\n", + " }\n", + " data = {\"uris\": uris}\n", + "\n", + " for attempt in range(3): # Retry up to 3 times\n", + " try:\n", + " response = requests.post(\n", + " url, headers=headers, json=data, timeout=10\n", + " ) # Added timeout\n", + " response.raise_for_status() # Raise an exception for bad status codes\n", + " print(f\"Recrawl request successful. Response: {response.json()}\")\n", + " return\n", + " except requests.exceptions.RequestException as e:\n", + " print(f\"Error during recrawl request (attempt {attempt + 1}): {e}\")\n", + " if attempt < 2 and isinstance(\n", + " e, (requests.exceptions.ConnectionError, requests.exceptions.Timeout)\n", + " ): # retry only for connection and timeout errors\n", + " print(\"Retrying in 5 seconds...\")\n", + " time.sleep(5)\n", + "\n", + " print(\"Failed to recrawl URIs after multiple attempts.\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "VOefNBmqXNvI" + }, + "source": [ + "# Include dependencies to run the function\n", + "\n", + "Finally we need to add the dependencies in the `requirements.txt` file to run the function above.\n", + "\n", + "You can copy the requirements from the following code block.\n", + "\n", + "![function_recs.png](https://storage.googleapis.com/github-repo/generative-ai/search/vais-building-blocks/manual_recrawl_urls_with_trigger/function_recs.png)\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "bqu-z4AGXjJY" + }, + "outputs": [], + "source": [ + "functions-framework==3.*\n", + "google-cloud-storage\n", + "google-auth\n", + "requests" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "A4Sf7AHpYBeP" + }, + "source": [ + "# Deploy the function\n", + "\n", + "Once the source and requirements are added the function can be deployed with a single click.\n", + "\n", + "You need to ensure that the service account used by your Cloud Function has the necessary permissions:\n", + "* Storage Object Viewer on your trigger bucket to read the files.\n", + "* Discovery Engine Admin to interact with the Discovery Engine API.\n", + "* Cloud Scheduler Invoker to recrawl uris\n", + "* Cloud Functions Invoker to invoke the cloud function\n", + "* Service Account User to make secure requests\n", + "* Logs Writer to post logs to GCP\n", + "\n", + "You will typically get notified to provide necessary permissions as you try to deploy the function.\n", + "\n", + "Once the function is deployed, you can find it (called `recrawl_function` in this example) in the `Cloud Run Functions` section of the cloud console. You will have the ability to edit the function, adjust the trigger, and read logs among other things." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "n7c74l2ZZTr4" + }, + "source": [ + "# Test the function\n", + "\n", + "Finally, you can test the function you just deployed by adding a new JSON file to your target bucket. Once the file is uploaded to the bucket, you can check the status of the function in the logs.\n", + "\n", + "An acceptable JSON file to be used with the source file provided in this example needs to follow the following format:\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "QxOdMGF2aOve" + }, + "outputs": [], + "source": [ + "{\n", + " \"uris\": [\n", + " \"https://example.com/page-1\",\n", + " \"https://example.com/page-2\",\n", + " \"https://example.com/page-3\",\n", + " ]\n", + "}" + ] + } + ], + "metadata": { + "colab": { + "name": "manual_recrawl_urls_with_trigger.ipynb", + "toc_visible": true + }, + "kernelspec": { + "display_name": "Python 3", + "name": "python3" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} diff --git a/search/vais-building-blocks/parsing_and_chunking_with_BYO.ipynb b/search/vais-building-blocks/parsing_and_chunking_with_BYO.ipynb new file mode 100644 index 0000000..3464162 --- /dev/null +++ b/search/vais-building-blocks/parsing_and_chunking_with_BYO.ipynb @@ -0,0 +1,1478 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "DuN2KIZUlzbS" + }, + "outputs": [], + "source": [ + "# Copyright 2024 Google LLC\n", + "#\n", + "# Licensed under the Apache License, Version 2.0 (the \"License\");\n", + "# you may not use this file except in compliance with the License.\n", + "# You may obtain a copy of the License at\n", + "#\n", + "# https://www.apache.org/licenses/LICENSE-2.0\n", + "#\n", + "# Unless required by applicable law or agreed to in writing, software\n", + "# distributed under the License is distributed on an \"AS IS\" BASIS,\n", + "# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n", + "# See the License for the specific language governing permissions and\n", + "# limitations under the License." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "q3pW20ECIDpX" + }, + "source": [ + "# Parsing and Chunking in Vertex AI Search: Featuring BYO Capabilities\n", + "\n", + " \n", + " \n", + " \n", + " \n", + "
    \n", + " \n", + " \"Google
    Open in Colab\n", + "
    \n", + "
    \n", + " \n", + " \"Google
    Open in Colab Enterprise\n", + "
    \n", + "
    \n", + " \n", + " \"Vertex
    Open in Vertex AI Workbench\n", + "
    \n", + "
    \n", + " \n", + " \"GitHub
    View on GitHub\n", + "
    \n", + "
    \n", + "\n", + "
    \n", + "\n", + "Share to:\n", + "\n", + "\n", + " \"LinkedIn\n", + "\n", + "\n", + "\n", + " \"Bluesky\n", + "\n", + "\n", + "\n", + " \"X\n", + "\n", + "\n", + "\n", + " \"Reddit\n", + "\n", + "\n", + "\n", + " \"Facebook\n", + "" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "YQpT9IS3K-Fn" + }, + "source": [ + "| | |\n", + "|----------|-------------|\n", + "| Author(s) | Jaival Desai, Hossein Mansour|\n", + "| Reviewers(s) | Allie Chen, Rajesh Thallam|\n", + "| Last updated | 2024-08-08: The first draft |" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "zNu-9XmEDF52" + }, + "source": [ + "# Overview\n", + "\n", + "In this notebook, we will demonstrate how to retrieve Parsed and Chunked documents from a [Vertex AI Search](https://cloud.google.com/generative-ai-app-builder/docs/introduction) (VAIS) datastore. Additionally, we will show how to Bring Your Own Chunks (BYOC) and ingest them into the datastore as needed. You can find more information [here](https://cloud.google.com/generative-ai-app-builder/docs/parse-chunk-documents#bring-parsed-document).\n", + "\n", + "We will perform the following steps:\n", + "\n", + "- [Prerequisite] Create a VAIS Datastore and import sample documents\n", + "- Get [Processed Document](https://cloud.google.com/generative-ai-app-builder/docs/parse-chunk-documents#get-parsed-documents) from datastore\n", + "- Get [Chunks](https://cloud.google.com/generative-ai-app-builder/docs/parse-chunk-documents#get-processed-chunks) from datastore\n", + "- Reconstruct the document from Chunks for visual inspection\n", + "- Store Chunks for offline review and/or edit\n", + "-[Bring your Own Chunks](https://cloud.google.com/generative-ai-app-builder/docs/parse-chunk-documents#bring-chunks). At the time of publishing this notebook, the BYOC feature is available under private preview. To be allowlisted for this feature, please contact your Google account team.\n", + "\n", + "![byoc.png](https://storage.googleapis.com/github-repo/generative-ai/search/vais-building-blocks/parsing_and_chunking_with_BYO/byoc.png)\n", + "\n", + "REST API is used throughout this notebook. Please consult the [official documentation](https://cloud.google.com/generative-ai-app-builder/docs/apis) for alternative ways to achieve the same goal, namely Client libraries and RPC.\n", + "\n", + "\n", + "# Vertex AI Search\n", + "Vertex AI Search (VAIS) is a fully-managed platform, powered by large language models, that lets you build AI-enabled search and recommendation experiences for your public or private websites or mobile applications\n", + "\n", + "VAIS can handle a diverse set of data sources including structured, unstructured, and website data, as well as data from third-party applications such as Jira, Salesforce, and Confluence.\n", + "\n", + "VAIS also has built-in integration with LLMs which enables you to provide answers to complex questions, grounded in your data\n", + "\n", + "#Using this Notebook\n", + "If you're running outside of Colab, depending on your environment you may need to install pip packages that are included in the Colab environment by default but are not part of the Python Standard Library. Outside of Colab you'll also notice comments in code cells that look like #@something, these trigger special Colab functionality but don't change the behavior of the notebook.\n", + "\n", + "This tutorial uses the following Google Cloud services and resources:\n", + "\n", + "- Service Usage API\n", + "- Discovery Engine\n", + "- Google Cloud Storage Client\n", + "\n", + "This notebook has been tested in the following environment:\n", + "\n", + "- Python version = 3.10.12\n", + "- google.cloud.storage = 2.8.0\n", + "- google.auth = 2.27.0\n", + "\n", + "# Getting Started\n", + "\n", + "The following steps are necessary to run this notebook, no matter what notebook environment you're using.\n", + "\n", + "If you're entirely new to Google Cloud, [get started here](https://cloud.google.com/docs/get-started)\n", + "\n", + "## Google Cloud Project Setup\n", + "\n", + "1. [Select or create a Google Cloud project](https://console.cloud.google.com/cloud-resource-manager). When you first create an account, you get a $300 free credit towards your compute/storage costs\n", + "2. [Make sure that billing is enabled for your project](https://cloud.google.com/billing/docs/how-to/modify-project)\n", + "3. [Enable the Service Usage API](https://console.cloud.google.com/apis/library/serviceusage.googleapis.com)\n", + "4. [Enable the Cloud Storage API](https://console.cloud.google.com/flows/enableapi?apiid=storage.googleapis.com)\n", + "5. [Enable the Discovery Engine API for your project](https://console.cloud.google.com/marketplace/product/google/discoveryengine.googleapis.com)\n", + "\n", + "## Google Cloud Permissions\n", + "\n", + "Ideally you should have [Owner role](https://cloud.google.com/iam/docs/understanding-roles) for your project to run this notebook. If that is not an option, you need at least the following [roles](https://cloud.google.com/iam/docs/granting-changing-revoking-access)\n", + "- **`roles/serviceusage.serviceUsageAdmin`** to enable APIs\n", + "- **`roles/iam.serviceAccountAdmin`** to modify service agent permissions\n", + "- **`roles/discoveryengine.admin`** to modify discoveryengine assets\n", + "- **`roles/storage.objectAdmin`** to modify and delete GCS buckets" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "JwTJMRNlrOEf" + }, + "source": [ + "#Setup Environment" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "lJFp9LUmrSOf" + }, + "source": [ + "## Authentication\n", + "\n", + " If you're using Colab, run the code in the next cell. Follow the pop-ups and authenticate with an account that has access to your Google Cloud [project](https://cloud.google.com/resource-manager/docs/creating-managing-projects#identifying_projects).\n", + "\n", + "If you're running this notebook somewhere besides Colab, make sure your environment has the right Google Cloud access. If that's a new concept to you, consider looking into [Application Default Credentials for your local environment](https://cloud.google.com/docs/authentication/provide-credentials-adc#local-dev) and [initializing the Google Cloud CLI](https://cloud.google.com/docs/authentication/gcloud). In many cases, running `gcloud auth application-default login` in a shell on the machine running the notebook kernel is sufficient.\n", + "\n", + "More authentication options are discussed [here](https://cloud.google.com/docs/authentication)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "-OKbRWQGrc-R" + }, + "outputs": [], + "source": [ + "# Colab authentication.\n", + "import sys\n", + "\n", + "if \"google.colab\" in sys.modules:\n", + " from google.colab import auth\n", + "\n", + " auth.authenticate_user()\n", + " print(\"Authenticated\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "pqF-4eiwP_j8" + }, + "outputs": [], + "source": [ + "from google.auth import default\n", + "from google.auth.transport.requests import AuthorizedSession\n", + "\n", + "creds, _ = default()\n", + "authed_session = AuthorizedSession(creds)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "kdQPp72R11pd" + }, + "source": [ + "## Import Libraries" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "JeAqU1WBrXDy" + }, + "outputs": [], + "source": [ + "import json\n", + "import re\n", + "import sys\n", + "import textwrap\n", + "import time\n", + "from typing import Any\n", + "from urllib.parse import urlparse\n", + "\n", + "from google.auth import default\n", + "from google.auth.transport.requests import AuthorizedSession\n", + "from google.cloud import storage\n", + "import requests" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "MGXEinm3q1ks" + }, + "source": [ + "## Configure environment\n", + "\n", + "You can enter the ID for an existing Vertex AI Search App and Datastore to be used in this notebook.\n", + "\n", + "Alternatively, you can enter the desired IDs for non-existings App and Datastore and they will be created later in this notebook.\n", + "\n", + "Same applies to the Cloud Storage buckets to store Documents and Metadata. The Documents and Metadata can be in separate buckets, but it is advised to keep them (together with the JSONL created later in this notebook) in the same temporary bucket for the ease of cleanup.\n", + "\n", + "You can find more information regarding the `location` of datastores and associated limitations [here](https://cloud.google.com/generative-ai-app-builder/docs/locations#specify_a_multi-region_for_your_data_store).\n", + "\n", + "The location of a Datastore is set at the time of creation and it should be called appropriately to query the Datastore.\n", + "\n", + "`FILE_NAME_VAIS_OUTPUT` is used to upload the Chunked Document to the bucket specified." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "_wsHOE5dl_3i" + }, + "outputs": [], + "source": [ + "PROJECT_ID = \"\" # @param {type:\"string\"}\n", + "\n", + "# Vertex AI Search Parameters\n", + "DATASTORE_ID = \"goog_earnings_test\" # @param {type:\"string\"}\n", + "LOCATION = \"global\" # @param [\"global\", \"us\", \"eu\"] Global is preferred\n", + "GCS_BUCKET = \"sample_earnings\" # @param {type:\"string\"}\n", + "FILE_NAME_VAIS_OUTPUT = \"chunked_doc_from_VAIS.json\" # @param {type:\"string\"}" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "nwHJzjwbTlF_" + }, + "source": [ + "# STEP 1. Create VAIS Datastore\n", + "\n", + "You can skip this section if you already have a datastore with your target unstructured documents ingested with [Chunk mode](https://cloud.google.com/generative-ai-app-builder/docs/parse-chunk-documents), which indexes your data as chunks to improve relevance and decrease computational load for LLMs." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "v-soe_CgVnFb" + }, + "source": [ + "## Helper functions to create a Datastore\n", + "\n", + "The datastore is created with [Chunk Mode](https://cloud.google.com/generative-ai-app-builder/docs/parse-chunk-documents) and Chunk size of 500 tokens.\n", + "\n", + "The documents will be processed with Layout parser (higher quality for complex documents containing elements like tables and lists) and Ancestor information (i.e. headings) is included with each Chunk. Please see [official documentation](https://cloud.google.com/generative-ai-app-builder/docs/parse-chunk-documents) for more details." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "BG1sHc6hWH86" + }, + "outputs": [], + "source": [ + "def create_chunk_mode_datastore(\n", + " project_id: str, location: str, datastore_id: str\n", + ") -> int:\n", + " \"\"\"Create a datastore with chunk mode and the more advanced layout parser\"\"\"\n", + " payload = {\n", + " \"displayName\": datastore_id,\n", + " \"industryVertical\": \"GENERIC\",\n", + " \"solutionTypes\": [\"SOLUTION_TYPE_SEARCH\"],\n", + " \"contentConfig\": \"CONTENT_REQUIRED\",\n", + " \"documentProcessingConfig\": {\n", + " \"chunkingConfig\": {\n", + " \"layoutBasedChunkingConfig\": {\n", + " \"chunkSize\": 500,\n", + " \"includeAncestorHeadings\": True,\n", + " }\n", + " },\n", + " \"defaultParsingConfig\": {\"layoutParsingConfig\": {}},\n", + " },\n", + " }\n", + " header = {\"X-Goog-User-Project\": project_id, \"Content-Type\": \"application/json\"}\n", + " es_endpoint = f\"https://discoveryengine.googleapis.com/v1/projects/{project_id}/locations/{location}/collections/default_collection/dataStores?dataStoreId={datastore_id}\"\n", + " response = authed_session.post(\n", + " es_endpoint, data=json.dumps(payload), headers=header\n", + " )\n", + " if response.status_code == 200:\n", + " print(f\"The creation of Datastore {datastore_id} is initiated.\")\n", + " print(\"It may take a few minutes for the Datastore to become available\")\n", + " else:\n", + " print(f\"Failed to create Datastore {datastore_id}\")\n", + " print(response.json())\n", + " return response.status_code" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "w2q8UZxwRd1m" + }, + "source": [ + "## Helper functions to issue basic search on a Datastore" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "98bc0zgCWhqP" + }, + "outputs": [], + "source": [ + "def search_by_datastore(\n", + " project_id: str, location: str, datastore_id: str, query: str\n", + ") -> requests.Response:\n", + " \"\"\"Searches a datastore using the provided query.\"\"\"\n", + " response = authed_session.post(\n", + " f\"https://discoveryengine.googleapis.com/v1/projects/{project_id}/locations/{location}/collections/default_collection/dataStores/{datastore_id}/servingConfigs/default_search:search\",\n", + " headers={\n", + " \"Content-Type\": \"application/json\",\n", + " },\n", + " json={\"query\": query, \"pageSize\": 1},\n", + " )\n", + " return response" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "fJ3ULZOeR-E2" + }, + "source": [ + "## Helper functions to check whether or not a Datastore already exists" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "ShppuvcxWBut" + }, + "outputs": [], + "source": [ + "def datastore_exists(project_id: str, location: str, datastore_id: str) -> bool:\n", + " \"\"\"Check if a datastore exists.\"\"\"\n", + " response = search_by_datastore(project_id, location, datastore_id, \"test\")\n", + " status_code = response.status_code\n", + " if status_code == 200:\n", + " return True\n", + " if status_code == 404:\n", + " return False\n", + " raise Exception(f\"Error: {status_code}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "NnCzxjV9SVO_" + }, + "source": [ + "## Create a Datastore with the provided ID if it doesn't exist" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "nBnAc1NIV59z" + }, + "outputs": [], + "source": [ + "# Create Chunk mode Datastore if it doesn't exist\n", + "if datastore_exists(PROJECT_ID, LOCATION, DATASTORE_ID):\n", + " print(f\"Datastore {DATASTORE_ID} already exists.\")\n", + "else:\n", + " create_chunk_mode_datastore(PROJECT_ID, LOCATION, DATASTORE_ID)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "MF07QdwxW_1n" + }, + "source": [ + "## [Optional] Check if the Datastore is created successfully\n", + "\n", + "\n", + "The Datastore is polled to track when it becomes available.\n", + "\n", + "This may take a few minutes after the datastore creation is initiated" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "S7xR3uX8XEnH" + }, + "outputs": [], + "source": [ + "while not datastore_exists(PROJECT_ID, LOCATION, DATASTORE_ID):\n", + " print(f\"Datastore {DATASTORE_ID} is still being created.\")\n", + " time.sleep(30)\n", + "print(f\"Datastore {DATASTORE_ID} is created successfully.\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "Gxyl1Fs2XXBp" + }, + "source": [ + "# STEP 2. Import sample document into VAIS Datastore" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "IRaL_VoXTnIw" + }, + "source": [ + "## Create a GCS bucket with sample document(s)\n", + "\n", + "This step is only needed for the purpose of this demo. For the real use case you will need to upload your actual documents to a GCS bucket\n", + "\n", + "Here, we download [Alphabet's 2024 Q2 Earnings Release](https://abc.xyz/assets/19/e4/3dc1d4d6439c81206370167db1bd/2024q2-alphabet-earnings-release.pdf) as a sample document." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "HHVR3KkzT2H4" + }, + "outputs": [], + "source": [ + "def create_gcs_bucket_and_download_files(\n", + " project_id: str, bucket_name: str, file_urls: list[str]\n", + ") -> None:\n", + " \"\"\"\n", + " Creates a GCS bucket (if it doesn't exist) and downloads files from specified URLs.\n", + "\n", + " Args:\n", + " project_id (str): Your Google Cloud Project ID.\n", + " bucket_name (str): The name of the GCS bucket (e.g., \"my-documents-bucket\").\n", + " file_urls (list): A list of URLs to files you want to download.\n", + " \"\"\"\n", + "\n", + " storage_client = storage.Client(project=project_id)\n", + " bucket = storage_client.bucket(bucket_name)\n", + "\n", + " if not bucket.exists():\n", + " bucket = storage_client.create_bucket(bucket_name)\n", + "\n", + " print(f\"Bucket {bucket_name} created.\")\n", + "\n", + " for url in file_urls:\n", + " file_name = url.split(\"/\")[-1]\n", + " print(f\"Downloading: {file_name}\")\n", + "\n", + " try:\n", + " response = requests.get(url)\n", + " response.raise_for_status() # Raise an exception for HTTP errors\n", + "\n", + " blob = bucket.blob(file_name)\n", + " blob.upload_from_string(\n", + " response.content,\n", + " content_type=\"application/pdf\", # Explicitly set the content type\n", + " )\n", + " print(f\"Uploaded: {file_name}\")\n", + " except requests.exceptions.RequestException as e:\n", + " print(f\"Error downloading {file_name}: {e}\")\n", + "\n", + "\n", + "file_urls = [\n", + " \"https://abc.xyz/assets/19/e4/3dc1d4d6439c81206370167db1bd/2024q2-alphabet-earnings-release.pdf\"\n", + "]\n", + "\n", + "create_gcs_bucket_and_download_files(PROJECT_ID, GCS_BUCKET, file_urls)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "js_EG6U2XaKL" + }, + "source": [ + "## Helper function to import documents into a VAIS datastore\n", + "\n", + "This helper function is used to import the documents in a GCS folder into VAIS\n", + "\n", + "NOTE: The [\"dataSchema\"](https://cloud.google.com/generative-ai-app-builder/docs/reference/rest/v1/GcsSource?hl=en) should be specified as \"content\". This allows us to ingest PDF files directly. The default \"dataSchema\" is \"document\" which expects JSONL files(s) in `gcs_uri`. This option is most useful when we want to include Metadata. See [documentation](https://cloud.google.com/generative-ai-app-builder/docs/prepare-data?hl=en#storage-unstructured) for more details.\n", + "\n", + "The process is done asynchronously, and the request returns an instance of a \"Long running Operation\".\n", + "\n", + "For a small corpus like the one we are experimenting with in this notebook, the process takes in the order of xx minutes." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "a2EnwXpOXlj3" + }, + "outputs": [], + "source": [ + "def import_documents_from_gcs(\n", + " project_id: str, location: str, datastore_id: str, gcs_uri: str\n", + ") -> str:\n", + " \"\"\"Imports unstructured documents from a GCS bucket.\"\"\"\n", + " payload = {\n", + " \"reconciliationMode\": \"INCREMENTAL\",\n", + " \"gcsSource\": {\"inputUris\": [gcs_uri], \"dataSchema\": \"content\"},\n", + " }\n", + " header = {\"Content-Type\": \"application/json\"}\n", + " es_endpoint = f\"https://discoveryengine.googleapis.com/v1/projects/{project_id}/locations/{location}/collections/default_collection/dataStores/{datastore_id}/branches/default_branch/documents:import\"\n", + " response = authed_session.post(\n", + " es_endpoint, data=json.dumps(payload), headers=header\n", + " )\n", + " print(f\"--{response.json()}\")\n", + " return response.json()[\"name\"]" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "NrypDv7kXtLf" + }, + "source": [ + "## Importing sample documents into the Chunk Mode Datastore" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "qzKBkp9bXxZT" + }, + "outputs": [], + "source": [ + "chunk_mode_import_lro = import_documents_from_gcs(\n", + " project_id=PROJECT_ID,\n", + " location=LOCATION,\n", + " datastore_id=DATASTORE_ID,\n", + " gcs_uri=f\"gs://{GCS_BUCKET}/*\",\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "0SmHf6ZfX3GI" + }, + "source": [ + "## [Optional] Check the status of document import for the Chunk Mode Datastore\n", + "Optionally check the status of the long running operation for the import job. You can check this in the UI as well by looking at the \"activity\" tab of the corresponding Datastore" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "L_ID_6ozX5-u" + }, + "outputs": [], + "source": [ + "while True:\n", + " response = authed_session.get(\n", + " f\"https://discoveryengine.googleapis.com/v1/{chunk_mode_import_lro}\",\n", + " )\n", + " try:\n", + " status = response.json()[\"done\"]\n", + " if status:\n", + " print(\"Import completed!\")\n", + " break\n", + " except KeyError:\n", + " print(\"Import in progress.\")\n", + " time.sleep(60)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "ndoKhd9lguyc" + }, + "source": [ + "#Helper Functions for formatting and ease of visual inspection\n", + "The following helper functions are used to reconstruct a document from its chunks and to show them in a human-friendly manner.\n", + "\n", + "These functions are not particularly related to VAIS and you do not need to worry about their details to understand the flow of this notebook" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "XLT8IvSMXG58" + }, + "source": [ + "##Helper function to beautify JSON outputs" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "kkZiVM8nR53S" + }, + "outputs": [], + "source": [ + "def parse_and_print_json(data: dict[str, Any]) -> dict[str, Any] | None:\n", + " \"\"\"\n", + " Recursively parses and structures JSON data into a more readable dictionary format,\n", + " handling nested dictionaries.\n", + "\n", + " Args:\n", + " data (dict): The dictionary potentially containing JSON strings at any level.\n", + "\n", + " Returns:\n", + " dict or None: The original dictionary with JSON strings parsed into dictionaries,\n", + " or None if there's an error during JSON decoding.\n", + " \"\"\"\n", + "\n", + " for key, value in data.items():\n", + " if isinstance(value, str) and value.startswith(\"{\"): # Check for JSON string\n", + " try:\n", + " data[key] = json.loads(\n", + " value\n", + " ) # Parse and replace with the parsed dictionary\n", + " except json.JSONDecodeError as e:\n", + " print(f\"Error decoding JSON in key '{key}': {e}\")\n", + " return None\n", + " elif isinstance(value, dict): # Recurse into nested dictionaries\n", + " result = parse_and_print_json(value)\n", + " if result is None: # If an error occurred during recursion, propagate it\n", + " return None\n", + "\n", + " return data\n", + "\n", + "\n", + "def print_json(data: dict[str, Any]) -> dict[str, Any]:\n", + " \"\"\"\n", + " Structures the JSON data into a more readable dictionary format\n", + "\n", + " Args:\n", + " data (dict): The parsed JSON data as a dictionary.\n", + "\n", + " Returns:\n", + " dict: The structured JSON data\n", + " \"\"\"\n", + " output = {}\n", + "\n", + " for key, value in data.items():\n", + " if isinstance(value, dict):\n", + " output[key] = print_json(value)\n", + " elif isinstance(value, list):\n", + " output[key] = [\n", + " print_json(item) if isinstance(item, dict) else item for item in value\n", + " ]\n", + " else:\n", + " output[key] = value\n", + "\n", + " return output" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "ZPZl6Qiag2ML" + }, + "source": [ + "##Helper function to reconstruct a document from chunks\n", + "\n", + "Stitch chunks together to reconstruct the document while including pointers for chunk start and end." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "yMa-PpoxhVEa" + }, + "outputs": [], + "source": [ + "def reconstruct_document(chunked_document: dict[str, Any]) -> str:\n", + " \"\"\"Reconstructs a document from its chunks.\"\"\"\n", + " reconstructed_document = \"\"\n", + " for chunk in chunked_document[\"jsonData\"][\"chunks\"]:\n", + " reconstructed_document += \"Start of chunk: \" + chunk[\"id\"] + \"\\n\\n\"\n", + " reconstructed_document += chunk[\"content\"]\n", + " reconstructed_document += \"\\n\\nEnd of chunk: \" + chunk[\"id\"] + \"\\n\\n\"\n", + "\n", + " return reconstructed_document" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "d1WK0DUEjd39" + }, + "source": [ + "## Helper function to beautify a Markdown Table\n", + "\n", + "Takes the markdown table from chunks and makes it more human readable using appropriate column widths using pipes and horizontal separators." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "J5VOpJyKhDdP" + }, + "outputs": [], + "source": [ + "def format_markdown_table(table: str, max_cell_width: int = 15) -> str:\n", + " \"\"\"Formats a poorly formatted Markdown table with aligned pipes,\n", + " horizontal separators, and cell value wrapping.\n", + "\n", + " Args:\n", + " table: A string containing the poorly formatted Markdown table.\n", + " max_cell_width: The maximum allowed width for each cell.\n", + "\n", + " Returns:\n", + " A string containing the nicely formatted Markdown table with\n", + " wrapped cell values.\n", + " \"\"\"\n", + "\n", + " # Split the table into rows.\n", + " rows = table.strip().split(\"\\n\")\n", + "\n", + " # Find the actual header row (skipping rows with only hyphens)\n", + " header_row_index = next(\n", + " (\n", + " i\n", + " for i, row in enumerate(rows)\n", + " if not all(cell.strip() == \"-\" for cell in row.strip().split(\"|\")[1:-1])\n", + " ),\n", + " 0, # Default to the first row if no suitable header is found\n", + " )\n", + "\n", + " # Split each row into cells, ensuring empty cells are accounted for\n", + " cells_per_row = [\n", + " [\n", + " cell.strip() for cell in row.strip().split(\"|\")[1:-1]\n", + " ] # Remove leading and trailing pipes before splitting\n", + " for row in rows\n", + " ]\n", + "\n", + " # Determine the number of columns, considering both header and data rows\n", + " num_columns = max(len(row) for row in cells_per_row)\n", + "\n", + " # Determine the maximum width of each column, considering the max_cell_width limit.\n", + " column_widths = [\n", + " min(\n", + " max(\n", + " len(cells_per_row[row_index][col_index])\n", + " for row_index in range(len(cells_per_row))\n", + " if col_index < len(cells_per_row[row_index])\n", + " ), # Handle rows with fewer columns\n", + " max_cell_width,\n", + " )\n", + " for col_index in range(num_columns)\n", + " ]\n", + "\n", + " # Set a minimum column width to prevent 0 width\n", + " min_column_width = 3 # Or any other reasonable minimum\n", + " column_widths = [max(width, min_column_width) for width in column_widths]\n", + "\n", + " # Function to wrap cell values if they exceed the column width\n", + " def wrap_cell_value(cell_value, width):\n", + " wrapped_lines = textwrap.wrap(cell_value, width=width)\n", + " return wrapped_lines\n", + "\n", + " # Format the header row, potentially adding empty cells if needed\n", + " formatted_header_cells = [\n", + " wrap_cell_value(cell, column_widths[i])\n", + " for i, cell in enumerate(cells_per_row[header_row_index])\n", + " ]\n", + " formatted_header_cells += [[\"\"]] * (\n", + " num_columns - len(formatted_header_cells)\n", + " ) # Add empty cells if needed\n", + " max_lines_in_header = max(len(lines) for lines in formatted_header_cells)\n", + " formatted_header_rows = []\n", + " for line_index in range(max_lines_in_header):\n", + " formatted_header_rows.append(\n", + " \"| \"\n", + " + \" | \".join(\n", + " (cell[line_index] if line_index < len(cell) else \"\")\n", + " + \" \"\n", + " * (\n", + " column_widths[i]\n", + " - len(cell[line_index] if line_index < len(cell) else \"\")\n", + " )\n", + " for i, cell in enumerate(formatted_header_cells)\n", + " )\n", + " + \" |\"\n", + " )\n", + "\n", + " formatted_rows = formatted_header_rows\n", + "\n", + " # Format the separator row beneath the header.\n", + " formatted_rows.append(\n", + " \"|\" + \"|\".join(\"-\" * (width + 2) for width in column_widths) + \"|\"\n", + " )\n", + "\n", + " # Format the remaining rows (excluding the hyphen-only row if present), adding separators after each row\n", + " for row_index, row in enumerate(cells_per_row):\n", + " if row_index != header_row_index and not all(\n", + " cell.strip() == \"-\" for cell in row\n", + " ): # Skip header and hyphen-only rows\n", + " # Pad row with empty cells if needed\n", + " padded_row = row + [\"\"] * (num_columns - len(row))\n", + " wrapped_cells = [\n", + " wrap_cell_value(cell, column_widths[i])\n", + " for i, cell in enumerate(padded_row)\n", + " ]\n", + " max_lines_in_row = max(len(lines) for lines in wrapped_cells)\n", + " for line_index in range(max_lines_in_row):\n", + " formatted_row = (\n", + " \"| \"\n", + " + \" | \".join(\n", + " (cell[line_index] if line_index < len(cell) else \"\")\n", + " + \" \"\n", + " * (\n", + " column_widths[i]\n", + " - len(cell[line_index] if line_index < len(cell) else \"\")\n", + " )\n", + " for i, cell in enumerate(wrapped_cells)\n", + " )\n", + " + \" |\"\n", + " )\n", + " formatted_rows.append(formatted_row)\n", + "\n", + " # Add separator row after each data row (except the last one)\n", + " if row_index < len(cells_per_row) - 1:\n", + " formatted_rows.append(\n", + " \"|\" + \"|\".join(\"-\" * (width + 2) for width in column_widths) + \"|\"\n", + " )\n", + "\n", + " # Join the formatted rows into a single string.\n", + " return \"\\n\".join(formatted_rows)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "I-50mUGuiC_2" + }, + "source": [ + "## Helper function to beautify all Markdown Tables\n", + "\n", + "This function goes over the whole reconstructed document and replaces all markdown tables with their beautified versions" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "GKxnoDn9hHA1" + }, + "outputs": [], + "source": [ + "def format_chunked_document(text: str) -> str:\n", + " \"\"\"Identifies markdown tables within a string, formats them, and replaces the original instances.\n", + "\n", + " Args:\n", + " text: The input string potentially containing multiple markdown tables.\n", + "\n", + " Returns:\n", + " The modified string with formatted markdown tables replacing the original ones.\n", + " \"\"\"\n", + "\n", + " # Define the pattern to match markdown table instances\n", + " table_pattern = r\"_START_OF_TABLE_\\nTABLE_IN_MARKDOWN:\\n(.*?)\\n_END_OF_TABLE_\"\n", + "\n", + " # Find all matches of the pattern within the text\n", + " matches = re.findall(\n", + " table_pattern, text, re.DOTALL\n", + " ) # re.DOTALL allows '.' to match newlines\n", + "\n", + " # Process each matched table and replace it in the original text\n", + " for table_content in matches:\n", + " formatted_table = format_markdown_table(table_content)\n", + " # Remove the extra newline before inserting the formatted table\n", + " text = text.replace(\n", + " f\"_START_OF_TABLE_\\nTABLE_IN_MARKDOWN:\\n{table_content}\\n_END_OF_TABLE_\",\n", + " \"\\n\" + formatted_table + \"\\n\\n\",\n", + " 1,\n", + " )\n", + "\n", + " return text" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "nkWefjvj-NGn" + }, + "source": [ + "# STEP 3. Get Parsed and Chunked Document\n", + "\n", + "In this section we visually review Parsed and Chunked versions of a document" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "jspb2xq2ab_F" + }, + "source": [ + "##List all documents in a Datastore\n", + "Get a list of all documents in the datastore. You can then select the ID for the document of interest." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "281LTsCrabCX" + }, + "outputs": [], + "source": [ + "def list_documents_datastore(\n", + " project_id: str, location: str, data_store_id: str\n", + ") -> list[dict[str, str]] | None:\n", + " \"\"\"Lists documents in a specified data store using the REST API.\n", + "\n", + " Args:\n", + " project_id: The ID of your Google Cloud project.\n", + " location: The location of your data store.\n", + " Values: \"global\", \"us\", \"eu\"\n", + " data_store_id: The ID of the datastore.\n", + "\n", + " Returns:\n", + " The JSON response containing the list of documents, or None if an error occurs.\n", + " \"\"\"\n", + "\n", + " base_url = (\n", + " f\"{location}-discoveryengine.googleapis.com\"\n", + " if location != \"global\"\n", + " else \"discoveryengine.googleapis.com\"\n", + " )\n", + " url = f\"https://{base_url}/v1alpha/projects/{project_id}/locations/{location}/collections/default_collection/dataStores/{data_store_id}/branches/default_branch/documents\"\n", + "\n", + " try:\n", + " # Assuming 'authed_session' is available and properly configured for authentication\n", + " response = authed_session.get(url)\n", + " response.raise_for_status() # Raise an exception for bad status codes\n", + " documents = response.json()\n", + " print(\n", + " f\"Successfully retrieved {len(documents.get('documents', []))} document(s).\\n\"\n", + " )\n", + " return [\n", + " {\"id\": document[\"id\"], \"uri\": document[\"content\"][\"uri\"]}\n", + " for document in documents.get(\"documents\", [])\n", + " ]\n", + "\n", + " except requests.exceptions.RequestException as e:\n", + " print(f\"Error listing documents: {e}\")\n", + " return None\n", + "\n", + "\n", + "list_documents_datastore(PROJECT_ID, LOCATION, DATASTORE_ID)\n", + "DOCUMENT_ID = list_documents_datastore(PROJECT_ID, LOCATION, DATASTORE_ID)[0][\n", + " \"id\"\n", + "] # provisionally take the first document in the datastore as the document we want to analyze" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "OMFcf4zPiZ6N" + }, + "source": [ + "##Select the Document of interest\n", + "By running the previous block, the Document ID of interest will be pre-set to the first document in the Datastore.\n", + "\n", + "You can update as needed.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "h6xUzPqQcR3o" + }, + "outputs": [], + "source": [ + "DOCUMENT_ID = \"\" # @param {type:\"string\"}" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "c7h6PhYr-R-F" + }, + "source": [ + "## Get the Parsed Document\n", + "\n", + "Get the parsed version of the document of interest.\n", + "\n", + "The parsed document is not really human readable. However it might be useful to troubleshoot downstream issues such as text element identification or cell block detection.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "HJ_mtgCxmNv7" + }, + "outputs": [], + "source": [ + "def get_parsed_document(\n", + " project_id: str, data_store_id: str, document_id: str\n", + ") -> dict[str, Any] | None:\n", + " \"\"\"Retrieves a parsed document in JSON from Vertex AI Agent Builder.\"\"\"\n", + " \"\"\"Only applicable for data stores with Chunking config set.\"\"\"\n", + "\n", + " # Get authentication token (replace with your method)\n", + "\n", + " base_url = \"https://discoveryengine.googleapis.com/v1alpha\"\n", + " url = f\"{base_url}/projects/{project_id}/locations/global/collections/default_collection/dataStores/{data_store_id}/branches/0/documents/{document_id}:getProcessedDocument?processed_document_type=PARSED_DOCUMENT\"\n", + " response = authed_session.get(url)\n", + "\n", + " if response.status_code == 200:\n", + " parsed_document = parse_and_print_json(response.json())\n", + " return parsed_document\n", + " else:\n", + " print(f\"Error: {response.status_code}, {response.text}\")\n", + " return None\n", + "\n", + "\n", + "parsed_document = get_parsed_document(PROJECT_ID, DATASTORE_ID, DOCUMENT_ID)\n", + "parsed_document" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "heR9Ebfix3hD" + }, + "source": [ + "##Get the Chunked Document\n", + "\n", + "Get Chunks from the document in JSON format" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "aHGruIcax7ij" + }, + "outputs": [], + "source": [ + "def get_chunked_document(\n", + " project_id: str, data_store_id: str, document_id: str\n", + ") -> dict[str, Any] | None:\n", + " \"\"\"Retrieves a chunked document in JSON from Vertex AI Agent Builder.\"\"\"\n", + " \"\"\"Only applicable for data stores with Chunking config set.\"\"\"\n", + "\n", + " # Get authentication token (replace with your method)\n", + "\n", + " base_url = \"https://discoveryengine.googleapis.com/v1alpha\"\n", + " url = f\"{base_url}/projects/{project_id}/locations/global/collections/default_collection/dataStores/{data_store_id}/branches/0/documents/{document_id}:getProcessedDocument?processed_document_type=CHUNKED_DOCUMENT\"\n", + " response = authed_session.get(url)\n", + "\n", + " if response.status_code == 200:\n", + " chunked_document = parse_and_print_json(response.json())\n", + " return chunked_document\n", + " else:\n", + " print(f\"Error: {response.status_code}, {response.text}\")\n", + " return None\n", + "\n", + "\n", + "chunked_document = get_chunked_document(PROJECT_ID, DATASTORE_ID, DOCUMENT_ID)\n", + "chunked_document" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "t1gLR_5cN-3-" + }, + "source": [ + "## Visually review the Chunked document\n", + "\n", + "Visually review to spot issues with the chunked document.\n", + "\n", + "The chunks from JSON object are stacked together first, and beautified later for ease of human reviewing.\n", + "\n", + "Helper functions defined earlier in this notebook are used here.\n", + "\n", + "For offline reviewing, you can export the string `chunked_document` to your desired format (e.g. PDF)\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "i6Oy79QnFqHm" + }, + "outputs": [], + "source": [ + "reconstructed_document = reconstruct_document(chunked_document)\n", + "processed_string = format_chunked_document(reconstructed_document)\n", + "print(processed_string)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "68QbshVtqTXj" + }, + "source": [ + "The beautified chunked version of the sample document use in this notebook will begin like the screenshot below:\n", + "\n", + "![chunked_document.png](https://storage.googleapis.com/github-repo/generative-ai/search/vais-building-blocks/parsing_and_chunking_with_BYO/chunked_document.png)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "65AKofzIuU2t" + }, + "source": [ + "## [Optional] Upload Chunks to GCS Bucket\n", + "\n", + "Upload chunked document for offline review and edit.\n", + "\n", + "You can always transform JSON to your preferred formats (e.g. CSV, XLSX) before exporting." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "e95_7EcOs1Vt" + }, + "outputs": [], + "source": [ + "def upload_json_to_gcs(\n", + " bucket_name: str, file_name: str, json_data: dict[str, Any] | list[Any]\n", + ") -> None:\n", + " \"\"\"Uploads a JSON variable to a GCS bucket as a file.\n", + "\n", + " Args:\n", + " bucket_name: The name of the GCS bucket (must start with 'gs://' and end with '/').\n", + " file_name: The desired name of the JSON file within the bucket.\n", + " json_data: The JSON data to be uploaded (Python dictionary or list).\n", + "\n", + " Raises:\n", + " ValueError: If the bucket_name format is invalid.\n", + " \"\"\"\n", + "\n", + " if not bucket_name.startswith(\"gs://\") or not bucket_name.endswith(\"/\"):\n", + " raise ValueError(\n", + " \"Invalid GCS path format. Must start with 'gs://' and end with '/'. \"\n", + " f\"Received: '{bucket_name}'\"\n", + " )\n", + "\n", + " storage_client = storage.Client(\n", + " project=PROJECT_ID\n", + " ) # Assuming PROJECT_ID is defined\n", + "\n", + " parsed_path = urlparse(bucket_name)\n", + " bucket_name = parsed_path.netloc\n", + "\n", + " bucket = storage_client.bucket(bucket_name)\n", + " blob = bucket.blob(file_name)\n", + "\n", + " # Convert the JSON data to a string\n", + " json_string = json.dumps(json_data, indent=2)\n", + "\n", + " # Upload the JSON string as the file contents\n", + " blob.upload_from_string(json_string, content_type=\"application/json\")\n", + "\n", + " print(\n", + " f\"JSON data uploaded to https://storage.mtls.cloud.google.com/{bucket_name}/{file_name}\\n\"\n", + " )\n", + "\n", + "\n", + "upload_json_to_gcs(\n", + " \"gs://\" + GCS_BUCKET + \"/\", FILE_NAME_VAIS_OUTPUT, chunked_document[\"jsonData\"]\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "9yM5bnVvvOxi" + }, + "source": [ + "# STEP 4. [Needs Allowlisting] Bring Your Own Chunks (BYOC)\n", + "\n", + "This section describes how to [bring your own chunks](https://cloud.google.com/generative-ai-app-builder/docs/parse-chunk-documents#bring-chunks) into VAIS.\n", + "\n", + "The chunks can be completely generated by you, or you can take chunks generated by VAIS and modify them. Examples of the latter could be augmenting with additional context, or even batch processing to fix systematic issues with a certain document template like heading detection.\n", + "\n", + "Note that chunks should comply with the token limit specified at the time of creating the Datastore.\n", + "\n", + "At the time of publishing this notebook, the BYOC feature is available under private preview. To be allowlisted for this feature, please contact your Google account team.\n", + "\n", + "Additional Notes:\n", + "\n", + "1. This notebook showcases a particular use case of BYOC where VAIS is used for the initial parsing and chunking as well. In most cases for BYOC parsing and chunking is done outside VAIS and the chunks are brought into VAIS using BYOC.\n", + "\n", + "2. A document ingested using this feature is of type JSON and is treated separately from the original document used to generate the chunks (assuming that part is done in VAIS as well). To avoid duplicates, the original file needs to be removed after the BYOC document is ingested. You can use [this](https://github.com/GoogleCloudPlatform/applied-ai-engineering-samples/blob/main/genai-on-vertex-ai/vertex_ai_search/inline_ingestion_of_documents.ipynb) notebook to see how to delete a specific document via API.\n", + "\n", + "3. If you use VAIS to do the initial chunking, the `document metadata` will reference the original source document and its title. `document metadata` field in the chunked document is **only** used for retrieval purposes. You can modify that field as desired if you want to leverage it for other purposes.\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "fb8JrGgnxP73" + }, + "source": [ + "## Function to import Chunks" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "A39akvzx89cA" + }, + "outputs": [], + "source": [ + "def upload_document_chunks(project_id: str, data_store_id: str, uri_path: str) -> None:\n", + " \"\"\"Uploads chunks of a document to a Vertex AI data store.\"\"\"\n", + "\n", + " # Get authentication token using gcloud\n", + "\n", + " base_url = \"https://discoveryengine.googleapis.com/v1alpha\"\n", + " url = f\"{base_url}/projects/{project_id}/locations/global/collections/default_collection/dataStores/{data_store_id}/branches/default_branch/documents:import\"\n", + "\n", + " # Prepare the request payload\n", + " # header = {\"Content-Type\": \"application/json\"}\n", + " payload = {\n", + " \"reconciliationMode\": \"INCREMENTAL\",\n", + " \"gcsSource\": {\"inputUris\": uri_path, \"dataSchema\": \"content\"},\n", + " }\n", + "\n", + " response = authed_session.post(url=url, json=payload)\n", + "\n", + " if response.status_code == 200:\n", + " print(\"Chunked file uploaded successfully!\")\n", + " else:\n", + " print(f\"Error uploading chunked file: {response.status_code}, {response.text}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "EjMOrPkty7PR" + }, + "source": [ + "## Import Chunks\n", + "\n", + "Define the file name with chunks to be imported and run the function to actually import it.\n", + "\n", + "The formatting of the file should be same as `jsonData` field in the Chunked document.\n", + "\n", + "For the sake of quick testing you use the exported chunked document here and reimport it into VAIS." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "Klnz7UCsyWzF" + }, + "outputs": [], + "source": [ + "FILE_NAME_TO_IMPORT = \"chunked_doc_to_import.json\" # @param {type:\"string\"}\n", + "upload_document_chunks(\n", + " PROJECT_ID, DATASTORE_ID, \"gs://\" + GCS_BUCKET + \"/\" + FILE_NAME_TO_IMPORT\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "QVJ5iIf4cuOC" + }, + "source": [ + "## Visually review BYO Chunked document\n", + "\n", + "Follow the instructions under step 3 to\n", + "\n", + "- List documents in the datastore\n", + "- Identify the BYO chunked document\n", + "- Get the chunked document, and use helper functions to stack the chunks together and visually review it.\n", + "\n", + "The screenshot below shows what you can get by slightly modifying the chunked document by VAIS and ingesting it back into VAIS (Note that the first line is manually added to the first chunk).\n", + "\n", + "![byoc.png](https://storage.googleapis.com/github-repo/generative-ai/search/vais-building-blocks/parsing_and_chunking_with_BYO/byoc.png)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "b_9s1JT6AS7o" + }, + "source": [ + "#Cleanup\n", + "Clean up resources created in this notebook.\n", + "\n", + "Set `DELETE_RESOURCES` flag to `True` to delete resources." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "9yeinaBzeok9" + }, + "outputs": [], + "source": [ + "DELETE_RESOURCES = False" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "sS7tVxEgAXGA" + }, + "source": [ + "## Clean up GCS bucket\n", + "\n", + "❗❗❗ Only run the below cells if you created a new bucket just for this notebook ❗❗❗\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "dAOJ46asAuoa" + }, + "outputs": [], + "source": [ + "def empty_bucket(bucket_name: str) -> None:\n", + " \"\"\"Deletes all objects in the specified GCS bucket.\"\"\"\n", + " client = storage.Client()\n", + " bucket = client.get_bucket(bucket_name)\n", + "\n", + " blobs = bucket.list_blobs() # List all blobs (objects)\n", + " for blob in blobs:\n", + " blob.delete() # Delete each blob\n", + "\n", + " print(f\"Bucket {bucket_name} emptied.\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "DoIjtak6Ab7O" + }, + "outputs": [], + "source": [ + "if DELETE_RESOURCES:\n", + " # Empty the bucket by deleting all files in it\n", + " empty_bucket(GCS_BUCKET)\n", + "\n", + " # Create a client object\n", + " client = storage.Client(project=PROJECT_ID)\n", + "\n", + " # Get the bucket object\n", + " bucket = client.get_bucket(GCS_BUCKET)\n", + "\n", + " # Delete the bucket\n", + " bucket.delete()\n", + "\n", + " print(f\"Bucket {GCS_BUCKET} deleted successfully.\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "L0aU2DdTckUo" + }, + "source": [ + "## Delete the Datastore\n", + "Delete the Datastore if you no longer need it\n", + "\n", + "Alternatively you can follow [these instructions](https://console.cloud.google.com/gen-app-builder/data-stores) to delete a Datastore from the UI" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "pBKcL_oicjxL" + }, + "outputs": [], + "source": [ + "if DELETE_RESOURCES:\n", + " response = authed_session.delete(\n", + " f\"https://discoveryengine.googleapis.com/v1alpha/projects/{PROJECT_ID}/locations/{LOCATION}/collections/default_collection/dataStores/{DATASTORE_ID}\",\n", + " headers={\"X-Goog-User-Project\": PROJECT_ID},\n", + " )\n", + "\n", + " print(response.json())" + ] + } + ], + "metadata": { + "colab": { + "name": "parsing_and_chunking_with_BYO.ipynb", + "toc_visible": true + }, + "kernelspec": { + "display_name": "Python 3", + "name": "python3" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} diff --git a/search/vais-building-blocks/query_level_boosting_filtering_and_facets.ipynb b/search/vais-building-blocks/query_level_boosting_filtering_and_facets.ipynb new file mode 100644 index 0000000..3cd7d3b --- /dev/null +++ b/search/vais-building-blocks/query_level_boosting_filtering_and_facets.ipynb @@ -0,0 +1,1296 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "5XNYlDkDLpqU" + }, + "outputs": [], + "source": [ + "# Copyright 2024 Google LLC\n", + "#\n", + "# Licensed under the Apache License, Version 2.0 (the \"License\");\n", + "# you may not use this file except in compliance with the License.\n", + "# You may obtain a copy of the License at\n", + "#\n", + "# https://www.apache.org/licenses/LICENSE-2.0\n", + "#\n", + "# Unless required by applicable law or agreed to in writing, software\n", + "# distributed under the License is distributed on an \"AS IS\" BASIS,\n", + "# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n", + "# See the License for the specific language governing permissions and\n", + "# limitations under the License." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "5tR528hOD4Dx" + }, + "source": [ + "# Query-Level Boosting, Filtering, and Facets for Vertex AI Search Website Datastores\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + "
    \n", + " \n", + " \"Google
    Open in Colab\n", + "
    \n", + "
    \n", + " \n", + " \"Google
    Open in Colab Enterprise\n", + "
    \n", + "
    \n", + " \n", + " \"Vertex
    Open in Vertex AI Workbench\n", + "
    \n", + "
    \n", + " \n", + " \"GitHub
    View on GitHub\n", + "
    \n", + "
    \n", + "\n", + "
    \n", + "\n", + "Share to:\n", + "\n", + "\n", + " \"LinkedIn\n", + "\n", + "\n", + "\n", + " \"Bluesky\n", + "\n", + "\n", + "\n", + " \"X\n", + "\n", + "\n", + "\n", + " \"Reddit\n", + "\n", + "\n", + "\n", + " \"Facebook\n", + "" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "pkd93iDpEBWx" + }, + "source": [ + "| | |\n", + "|----------|-------------|\n", + "| Author(s) | Hossein Mansour|\n", + "| Reviewers(s) | Ismail Najim, Rajesh Thallam|\n", + "| Last updated | 2024-09-06: The first draft |" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "yAnTektvEQjb" + }, + "source": [ + "# Overview\n", + "\n", + "In this notebook, we demonstrate how to influence search results and their respective ranking by specifying [filters](https://cloud.google.com/generative-ai-app-builder/docs/filter-website-search#examples-advanced-indexing) and [boost rules](https://cloud.google.com/generative-ai-app-builder/docs/boost-search-results) within the request. Boosting and Filtering are typically used to improve precision and recall as well as removing certain pages from consideration to satisfy a user-specified preference (e.g. limit the results to movies and exclude TV series), or customer-specified preference (e.g. do not show this movie in search results before it's officially released, do not show specific results to users from a specific country, quickly exclude an noncompliant page from search results until it is properly removed from the index). User specified preferences are typically applied via facets in the UI, for that reason, we also cover facets in this notebook.\n", + "\n", + "Boosting and Filtering are applied at the data store/index level and as part of the retrieval process. For that reason, customers cannot achieve the same goal by post processing the search results.\n", + "\n", + "VAIS website search leverages a sophisticated algorithm and many signals to surface relevant results in the right order (similar to what you get in google.com), as a result it is generally advised to evaluate the results without additional rules and incrementally add custom rules only as needed.\n", + "\n", + "Also note that Boosting is one of many ways by which you can influence ranking and retrieval in VAIS. A few examples of alternative routes are:\n", + "\n", + "- [User Events](https://cloud.google.com/generative-ai-app-builder/docs/user-events) to implicitly and gradually tune the ranking based on end-user behavior\n", + "- [Search Tuning](https://cloud.google.com/generative-ai-app-builder/docs/tune-search) to fine-tune the definition of \"relevant\" to your corpus, organization, domain, or preferences\n", + "- [Custom Embeddings](https://cloud.google.com/generative-ai-app-builder/docs/bring-embeddings) to augment the ranking identified by VAIS\n", + "- [Synonyms](https://cloud.google.com/generative-ai-app-builder/docs/configure-serving-controls#synonyms) to expand abbreviations and domain-specific terms based on their broadly-understood meaning \n", + "\n", + "While these functionalities are available irrespective of the datastore type (e.g. structured, unstructured, or website), we limit our focus in this notebook to [Advanced Website datastores](https://cloud.google.com/generative-ai-app-builder/docs/about-advanced-features#advanced-website-indexing). Other than a few exceptions (e.g. filtering and boosting based on URL) most of the syntaxes presented here are applicable to other Datastore types as well.\n", + "\n", + "Note that there is an alternative way to apply [serving controls](https://cloud.google.com/generative-ai-app-builder/docs/configure-serving-controls) (i.e. Boosting, Filtering, Synonyms, and Redirects) at the Global level, which means they do not need to be provided together with each query. That alternative path is out of the scope for this notebook and will be covered in a separate one.\n", + "\n", + "In order to specify boosting and filtering, we need particular attributes for each document which act as the hooks to identify the right target documents and to apply the corresponding modifiers to them. While each page (i.e. document) within the index has some predefined fields (e.g. URL, datePublished, dateModified), it is common to [leverage metadata within the page-source](https://cloud.google.com/generative-ai-app-builder/docs/add-website-metadata) as additional hooks. In order to identify those metadata, we need to update the Datastore Schema which is also covered in this notebook.\n", + "\n", + "We will perform the following steps:\n", + "\n", + "- [Prerequisite] Creating a Vertex AI Search Website Datastore and Search App via API\n", + "- Updating the Schema to identify page Metadata\n", + "- Filtering based on predefined fields\n", + "- Filtering based on page Metadata\n", + "- Defining Facets based on page Metadata\n", + "- Basic boosting\n", + "- A sample advanced boosting based on user ratings\n", + "- Clean up\n", + "\n", + "\n", + "Please refer to the [official documentation](https://cloud.google.com/generative-ai-app-builder/docs/create-datastore-ingest) for the definition of Datastores and Apps and their relationships to one another\n", + "\n", + "REST API is used throughout this notebook. Please consult the [official documentation](https://cloud.google.com/generative-ai-app-builder/docs/apis) for alternative ways to achieve the same goal, namely Client libraries and RPC.\n", + "\n", + "\n", + "# Vertex AI Search\n", + "Vertex AI Search (VAIS) is a fully-managed platform, powered by large language models, that lets you build AI-enabled search and recommendation experiences for your public or private websites or mobile applications\n", + "\n", + "VAIS can handle a diverse set of data sources including structured, unstructured, and website data, as well as data from third-party applications such as Jira, Salesforce, and Confluence.\n", + "\n", + "VAIS also has built-in integration with LLMs which enables you to provide answers to complex questions, grounded in your data" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "nszpkYyzAqpA" + }, + "source": [ + "# Using this Notebook\n", + "If you're running outside of Colab, depending on your environment you may need to install pip packages that are included in the Colab environment by default but are not part of the Python Standard Library. Outside of Colab you'll also notice comments in code cells that look like #@something, these trigger special Colab functionality but don't change the behavior of the notebook.\n", + "\n", + "This tutorial uses the following Google Cloud services and resources:\n", + "\n", + "- Service Usage API\n", + "- Discovery Engine API\n", + "\n", + "This notebook has been tested in the following environment:\n", + "\n", + "- Python version = 3.10.12\n", + "- google.cloud.storage = 2.8.0\n", + "- google.auth = 2.27.0" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "NZKU9BnbA0xz" + }, + "source": [ + "## Getting Started\n", + "\n", + "The following steps are necessary to run this notebook, no matter what notebook environment you're using.\n", + "\n", + "If you're entirely new to Google Cloud, [get started here](https://cloud.google.com/docs/get-started)\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "HNxgWBHqA5CF" + }, + "source": [ + "## Google Cloud Project Setup\n", + "\n", + "1. [Select or create a Google Cloud project](https://console.cloud.google.com/cloud-resource-manager). When you first create an account, you get a $300 free credit towards your compute/storage costs\n", + "2. [Make sure that billing is enabled for your project](https://cloud.google.com/billing/docs/how-to/modify-project)\n", + "3. [Enable the Service Usage API](https://console.cloud.google.com/apis/library/serviceusage.googleapis.com)\n", + "4. [Enable the Cloud Storage API](https://console.cloud.google.com/flows/enableapi?apiid=storage.googleapis.com)\n", + "5. [Enable the Discovery Engine API for your project](https://console.cloud.google.com/marketplace/product/google/discoveryengine.googleapis.com)\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "F2vGcA6QA6xG" + }, + "source": [ + "## Google Cloud Permissions\n", + "\n", + "Ideally you should have [Owner role](https://cloud.google.com/iam/docs/understanding-roles) for your project to run this notebook. If that is not an option, you need at least the following [roles](https://cloud.google.com/iam/docs/granting-changing-revoking-access)\n", + "- **`roles/serviceusage.serviceUsageAdmin`** to enable APIs\n", + "- **`roles/iam.serviceAccountAdmin`** to modify service agent permissions\n", + "- **`roles/discoveryengine.admin`** to modify discoveryengine assets" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "49x_J4vWOuNg" + }, + "source": [ + "#Setup Environment" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "kMYYfGpyOl5G" + }, + "source": [ + "## Authentication\n", + "\n", + " If you're using Colab, run the code in the next cell. Follow the pop-ups and authenticate with an account that has access to your Google Cloud [project](https://cloud.google.com/resource-manager/docs/creating-managing-projects#identifying_projects).\n", + "\n", + "If you're running this notebook somewhere besides Colab, make sure your environment has the right Google Cloud access. If that's a new concept to you, consider looking into [Application Default Credentials for your local environment](https://cloud.google.com/docs/authentication/provide-credentials-adc#local-dev) and [initializing the Google Cloud CLI](https://cloud.google.com/docs/authentication/gcloud). In many cases, running `gcloud auth application-default login` in a shell on the machine running the notebook kernel is sufficient.\n", + "\n", + "More authentication options are discussed [here](https://cloud.google.com/docs/authentication)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "DZjtfEDG7Sr3" + }, + "outputs": [], + "source": [ + "# Colab authentication.\n", + "import sys\n", + "\n", + "if \"google.colab\" in sys.modules:\n", + " from google.colab import auth\n", + "\n", + " auth.authenticate_user()\n", + " print(\"Authenticated\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "kT3Eda7_mlTP" + }, + "outputs": [], + "source": [ + "from google.auth import default\n", + "from google.auth.transport.requests import AuthorizedSession\n", + "\n", + "creds, _ = default()\n", + "authed_session = AuthorizedSession(creds)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "otijhCIjOzk-" + }, + "source": [ + "## Import Libraries" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "DlIp4zv3cdA7" + }, + "outputs": [], + "source": [ + "import json\n", + "import time" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "N51y_mPgPHsj" + }, + "source": [ + "## Configure environment\n", + "\n", + "`DATASTORE_ID` and `APP_ID` must match the pattern: [a-z0-9][a-z0-9-_]*\n", + "\n", + "The Location of a Datastore is set at the time of creation and it should be called appropriately to query the Datastore. `global` is typically recommended unless you have a particular reason to use a regional Datastore.\n", + "\n", + "You can find more information regarding the `Location` of datastores and associated limitations [here](https://cloud.google.com/generative-ai-app-builder/docs/locations#specify_a_multi-region_for_your_data_store).\n", + "\n", + "`VAIS_BRANCH` is the branch of VAIS to use.\n", + "\n", + "\n", + "`INCLUDE_URL_PATTERN` is the pattern of a website to be included in the datastore, e.g. \"www.example.com/*\", \"www.example.com/abc/*\".\n", + "\n", + "For this particular example we Index books on Google Play Store. To keep the size of the index manageable, we only include books with \"The\" in their title. The corresponding URL pattern to include looks like: \"play.google.com/store/books/details/\\*_The_\\*\"\n", + "\n", + "Note that you need to [verify the ownership of a domain](https://cloud.google.com/generative-ai-app-builder/docs/domain-verification) to be able to index it." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "hKLBf1GqROW7" + }, + "outputs": [], + "source": [ + "PROJECT_ID = \"\" # @param {type: 'string'}\n", + "DATASTORE_ID = \"\" # @param {type: 'string'}\n", + "APP_ID = \"\" # @param {type: 'string'}\n", + "LOCATION = \"global\" # @param [\"global\", \"us\", \"eu\"]\n", + "VAIS_BRANCH = \"v1alpha\" # @param [\"v1\", \"v1beta\", \"v1alpha\"]\n", + "INCLUDE_URL_PATTERN = \"\" # @param {type: 'string'}" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "Akk3C5vK8oG6" + }, + "source": [ + "# Step 1. [Prerequisite] Create a Website Search Datastore and APP\n", + "In this section we will programmatically create a VAIS [Advanced Website Datastore and APP](https://cloud.google.com/generative-ai-app-builder/docs/about-advanced-features#advanced-website-indexing). You can achieve the same goal with a [few clicks](https://cloud.google.com/generative-ai-app-builder/docs/website-search-checklist?indexing=advanced) in the UI.\n", + "\n", + "If you already have an Advanced Website Datastore available, you can skip this section.\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "C2hXlewDINDg" + }, + "source": [ + "## Helper functions to issue basic search on a Datastore or an App" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "v-XHQIOooshe" + }, + "outputs": [], + "source": [ + "def search_by_datastore(project_id: str, location: str, datastore_id: str, query: str):\n", + " \"\"\"Searches a datastore using the provided query.\"\"\"\n", + " response = authed_session.post(\n", + " f\"https://discoveryengine.googleapis.com/{VAIS_BRANCH}/projects/{project_id}/locations/{location}/collections/default_collection/dataStores/{datastore_id}/servingConfigs/default_search:search\",\n", + " headers={\n", + " \"Content-Type\": \"application/json\",\n", + " },\n", + " json={\"query\": query, \"pageSize\": 1},\n", + " )\n", + " return response\n", + "\n", + "\n", + "def search_by_app(project_id: str, location: str, app_id: str, query: str):\n", + " \"\"\"Searches an app using the provided query.\"\"\"\n", + " response = authed_session.post(\n", + " f\"https://discoveryengine.googleapis.com/v1/projects/{project_id}/locations/{location}/collections/default_collection/engines/{app_id}/servingConfigs/default_config:search\",\n", + " headers={\n", + " \"Content-Type\": \"application/json\",\n", + " },\n", + " json={\"query\": query, \"pageSize\": 1},\n", + " )\n", + " return response" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "eAigF6KHkMZ2" + }, + "source": [ + "## Helper functions to check whether or not a Datastore or an App already exist" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "IO1AxLZckXYK" + }, + "outputs": [], + "source": [ + "def datastore_exists(project_id: str, location: str, datastore_id: str) -> bool:\n", + " \"\"\"Check if a datastore exists.\"\"\"\n", + " response = search_by_datastore(project_id, location, datastore_id, \"test\")\n", + " status_code = response.status_code\n", + " # A 400 response is expected as the URL pattern needs to be set first\n", + " if status_code == 200 or status_code == 400:\n", + " return True\n", + " if status_code == 404:\n", + " return False\n", + " raise Exception(f\"Error: {status_code}\")\n", + "\n", + "\n", + "def app_exists(project_id: str, location: str, app_id: str) -> bool:\n", + " \"\"\"Check if an App exists.\"\"\"\n", + " response = search_by_app(project_id, location, app_id, \"test\")\n", + " status_code = response.status_code\n", + " if status_code == 200:\n", + " return True\n", + " if status_code == 404:\n", + " return False\n", + " raise Exception(f\"Error: {status_code}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "DYArgsiAiVfs" + }, + "source": [ + "## Helper functions to create a Datastore or an App" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "_0uAQTKD78_k" + }, + "outputs": [], + "source": [ + "def create_website_datastore(\n", + " vais_branch: str, project_id: str, location: str, datastore_id: str\n", + ") -> int:\n", + " \"\"\"Create a website datastore\"\"\"\n", + " payload = {\n", + " \"displayName\": datastore_id,\n", + " \"industryVertical\": \"GENERIC\",\n", + " \"solutionTypes\": [\"SOLUTION_TYPE_SEARCH\"],\n", + " \"contentConfig\": \"PUBLIC_WEBSITE\",\n", + " }\n", + " header = {\"X-Goog-User-Project\": project_id, \"Content-Type\": \"application/json\"}\n", + " es_endpoint = f\"https://discoveryengine.googleapis.com/{vais_branch}/projects/{project_id}/locations/{location}/collections/default_collection/dataStores?dataStoreId={datastore_id}\"\n", + " response = authed_session.post(\n", + " es_endpoint, data=json.dumps(payload), headers=header\n", + " )\n", + " if response.status_code == 200:\n", + " print(f\"The creation of Datastore {datastore_id} is initiated.\")\n", + " print(\"It may take a few minutes for the Datastore to become available\")\n", + " else:\n", + " print(f\"Failed to create Datastore {datastore_id}\")\n", + " print(response.json())\n", + " return response.status_code\n", + "\n", + "\n", + "def create_app(\n", + " vais_branch: str, project_id: str, location: str, datastore_id: str, app_id: str\n", + ") -> int:\n", + " \"\"\"Create a search app.\"\"\"\n", + " payload = {\n", + " \"displayName\": app_id,\n", + " \"dataStoreIds\": [datastore_id],\n", + " \"solutionType\": \"SOLUTION_TYPE_SEARCH\",\n", + " \"searchEngineConfig\": {\n", + " \"searchTier\": \"SEARCH_TIER_ENTERPRISE\",\n", + " \"searchAddOns\": [\"SEARCH_ADD_ON_LLM\"],\n", + " },\n", + " }\n", + " header = {\"X-Goog-User-Project\": project_id, \"Content-Type\": \"application/json\"}\n", + " es_endpoint = f\"https://discoveryengine.googleapis.com/{vais_branch}/projects/{project_id}/locations/{location}/collections/default_collection/engines?engineId={app_id}\"\n", + " response = authed_session.post(\n", + " es_endpoint, data=json.dumps(payload), headers=header\n", + " )\n", + " if response.status_code == 200:\n", + " print(f\"The creation of App {app_id} is initiated.\")\n", + " print(\"It may take a few minutes for the App to become available\")\n", + " else:\n", + " print(f\"Failed to create App {app_id}\")\n", + " print(response.json())\n", + " return response.status_code" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "1hAp5cBnIYxJ" + }, + "source": [ + "## Create a Datastores with the provided ID if it doesn't exist\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "hBUwJxxeAazj" + }, + "outputs": [], + "source": [ + "if datastore_exists(PROJECT_ID, LOCATION, DATASTORE_ID):\n", + " print(f\"Datastore {DATASTORE_ID} already exists.\")\n", + "else:\n", + " create_website_datastore(VAIS_BRANCH, PROJECT_ID, LOCATION, DATASTORE_ID)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "C1d-pd2WLJZI" + }, + "source": [ + "## [Optional] Check if the Datastore is created successfully\n", + "\n", + "\n", + "The Datastore is polled to track when it becomes available.\n", + "\n", + "This may take a few minutes" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "EZGzOCnTLOwf" + }, + "outputs": [], + "source": [ + "while not datastore_exists(PROJECT_ID, LOCATION, DATASTORE_ID):\n", + " print(f\"Datastore {DATASTORE_ID} is still being created.\")\n", + " time.sleep(30)\n", + "print(f\"Datastore {DATASTORE_ID} is created successfully.\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "vSzz2AzmI5kx" + }, + "source": [ + "## Create an App with the provided ID if it doesn't exist\n", + "The App will be connected to a Datastore with the ID provided earlier in this notebook" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "4lp4kPXNm9sE" + }, + "outputs": [], + "source": [ + "if app_exists(PROJECT_ID, LOCATION, APP_ID):\n", + " print(f\"App {APP_ID} already exists.\")\n", + "else:\n", + " create_app(VAIS_BRANCH, PROJECT_ID, LOCATION, DATASTORE_ID, APP_ID)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "fxlTn7dVK-Q2" + }, + "source": [ + "## [Optional] Check if the App is created successfully\n", + "\n", + "\n", + "The App is polled to track when it becomes available.\n", + "\n", + "This may take a few minutes" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "ZuQQ2HCGK4BA" + }, + "outputs": [], + "source": [ + "while not app_exists(PROJECT_ID, LOCATION, APP_ID):\n", + " print(f\"App {APP_ID} is still being created.\")\n", + " time.sleep(30)\n", + "print(f\"App {APP_ID} is created successfully.\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "A38IfFRD83UG" + }, + "source": [ + "## Upgrade an existing Website Datastore to [Advanced Website](https://cloud.google.com/generative-ai-app-builder/docs/about-advanced-features#advanced-website-indexing) DataStore" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "BYXR-yQ38vdd" + }, + "outputs": [], + "source": [ + "def upgrade_to_advanced(\n", + " vais_branch: str, project_id: str, location: str, datastore_id: str\n", + ") -> int:\n", + " \"\"\"Upgrade the website search datastore to advanced\"\"\"\n", + " header = {\"X-Goog-User-Project\": project_id}\n", + " es_endpoint = f\"https://discoveryengine.googleapis.com/{vais_branch}/projects/{project_id}/locations/{location}/collections/default_collection/dataStores/{datastore_id}/siteSearchEngine:enableAdvancedSiteSearch\"\n", + " response = authed_session.post(es_endpoint, headers=header)\n", + " if response.status_code == 200:\n", + " print(f\"Datastore {datastore_id} upgraded to Advanced Website Search\")\n", + " else:\n", + " print(f\"Failed to upgrade Datastore {datastore_id}\")\n", + " print(response.text())\n", + " return response.status_code\n", + "\n", + "\n", + "upgrade_to_advanced(VAIS_BRANCH, PROJECT_ID, LOCATION, DATASTORE_ID)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "NlUq4ADT8975" + }, + "source": [ + "## Set the URLs to Include/Exclude in the Index\n", + "\n", + "You can set up to 500 Include and Exclude URL patterns for Advanced website search Datastores.\n", + "\n", + "This function sets a single URL pattern to be included every time it gets executed.\n", + "\n", + "The field `type` in the payload is used to indicate if the provided Uri pattern should be included or excluded. Here we only use `INCLUDE`.\n", + "\n", + "The `INCLUDE` and `EXCLUDE` URL patterns specified with this function are incremental. You also have options to [Delete](https://cloud.google.com/generative-ai-app-builder/docs/reference/rest/v1alpha/projects.locations.collections.dataStores.siteSearchEngine.targetSites/delete), [List](https://cloud.google.com/generative-ai-app-builder/docs/reference/rest/v1alpha/projects.locations.collections.dataStores.siteSearchEngine.targetSites/list), [Batch Create](https://cloud.google.com/generative-ai-app-builder/docs/reference/rest/v1alpha/projects.locations.collections.dataStores.siteSearchEngine.targetSites/batchCreate), etc \n", + "\n", + "For this example, we index \"play.google.com/store/books/details/\\*_The_\\*\"\n", + "\n", + "Note that you need to [verify the ownership of a domain](https://cloud.google.com/generative-ai-app-builder/docs/domain-verification) to be able to index it." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "yc2OWvFd9Tvu" + }, + "outputs": [], + "source": [ + "def include_url_patterns(\n", + " vais_branch: str,\n", + " project_id: str,\n", + " location: str,\n", + " datastore_id: str,\n", + " include_url_patterns,\n", + ") -> int:\n", + " \"\"\"Set include and exclude URL patterns for the Datastore\"\"\"\n", + " payload = {\n", + " \"providedUriPattern\": include_url_patterns,\n", + " \"type\": \"INCLUDE\",\n", + " }\n", + " header = {\"X-Goog-User-Project\": project_id, \"Content-Type\": \"application/json\"}\n", + " es_endpoint = f\"https://discoveryengine.googleapis.com/{vais_branch}/projects/{project_id}/locations/{location}/dataStores/{datastore_id}/siteSearchEngine/targetSites\"\n", + " response = authed_session.post(\n", + " es_endpoint, data=json.dumps(payload), headers=header\n", + " )\n", + " if response.status_code == 200:\n", + " print(\"URL patterns successfully set\")\n", + " print(\n", + " \"Depending on the size of your domain, the initial indexing may take from minutes to hours\"\n", + " )\n", + " else:\n", + " print(f\"Failed to set URL patterns for the Datastore {datastore_id}\")\n", + " print(response.text())\n", + " return response.status_code\n", + "\n", + "\n", + "include_url_patterns(\n", + " VAIS_BRANCH, PROJECT_ID, LOCATION, DATASTORE_ID, INCLUDE_URL_PATTERN\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "rSAbsrg8Pkc2" + }, + "source": [ + "# Step 2. Update the Schema to include page Metadata" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "Dc-gaZ6rP6mC" + }, + "source": [ + "## Set the Schema\n", + "\n", + "In this example, we use [Books on Google Play Store](https://play.google.com/store/books) as the source for the datastore.\n", + "\n", + "In addition to the properties of the pages which are available by default (e.g. Date Published, Date Modified, URL, and Title), we are also interest in a few other properties such as Rating Count, Average Rating, Price, and Date Published (i.e. the actual publication date of the book, not the page of Play Store). VAIS extracts this additional information for each URL within the datastore upon appropriately [updating the Schema](https://cloud.google.com/generative-ai-app-builder/docs/provide-schema).\n", + "\n", + "At the time of creating this notebook, VAIS supports three types of Metadata within the page source: [Meta Tags](https://cloud.google.com/generative-ai-app-builder/docs/add-website-metadata#example-meta-tags), [PageMap](https://cloud.google.com/generative-ai-app-builder/docs/add-website-metadata#example-pagemaps), and [Schema.org](https://cloud.google.com/generative-ai-app-builder/docs/add-website-metadata#example-schema-org)\n", + "\n", + "For this example we extract the Description field from Meta tags and the other fields from Schema.org. You are encouraged to check the page source for a [sample app](https://play.google.com/store/books/details/Margaret_Atwood_The_Testaments?id=P6F7DwAAQBAJ) to see how these fields are defined in our target domain.\n", + "\n", + "As mentioned above, you can only index a website you own, as a result the metadata defined on your Datastore will be different with the ones defined in this example.\n", + "\n", + "Updating the Schema will trigger an update to the index which may take a few hours to complete.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "TDpAyVAUbxXM" + }, + "outputs": [], + "source": [ + "header = {\"X-Goog-User-Project\": PROJECT_ID}\n", + "es_endpoint = f\"https://discoveryengine.googleapis.com/{VAIS_BRANCH}/projects/{PROJECT_ID}/locations/{LOCATION}/collections/default_collection/dataStores/{DATASTORE_ID}/schemas/default_schema\"\n", + "json_data = {\n", + " \"structSchema\": {\n", + " \"type\": \"object\",\n", + " \"properties\": {\n", + " \"aggregate_rating\": {\n", + " \"type\": \"array\",\n", + " \"items\": {\n", + " \"type\": \"number\",\n", + " \"retrievable\": True,\n", + " \"indexable\": True,\n", + " \"dynamicFacetable\": True,\n", + " \"siteSearchSchemaOrgPaths\": [\"_root.aggregateRating.ratingValue\"],\n", + " },\n", + " },\n", + " \"rating_count\": {\n", + " \"type\": \"array\",\n", + " \"items\": {\n", + " \"type\": \"number\",\n", + " \"retrievable\": True,\n", + " \"indexable\": True,\n", + " \"dynamicFacetable\": True,\n", + " \"siteSearchSchemaOrgPaths\": [\"_root.aggregateRating.ratingCount\"],\n", + " },\n", + " },\n", + " \"price\": {\n", + " \"type\": \"array\",\n", + " \"items\": {\n", + " \"type\": \"number\",\n", + " \"retrievable\": True,\n", + " \"indexable\": True,\n", + " \"dynamicFacetable\": True,\n", + " \"siteSearchSchemaOrgPaths\": [\n", + " \"_root.workExample.potentialAction.expectsAcceptanceOf.price\"\n", + " ],\n", + " },\n", + " },\n", + " \"author\": {\n", + " \"type\": \"array\",\n", + " \"items\": {\n", + " \"type\": \"string\",\n", + " \"retrievable\": True,\n", + " \"indexable\": True,\n", + " \"dynamicFacetable\": True,\n", + " \"siteSearchSchemaOrgPaths\": [\"_root.author.name\"],\n", + " },\n", + " },\n", + " \"date_published\": {\n", + " \"type\": \"array\",\n", + " \"items\": {\n", + " \"type\": \"datetime\",\n", + " \"retrievable\": True,\n", + " \"indexable\": True,\n", + " \"siteSearchSchemaOrgPaths\": [\"_root.workExample.datePublished\"],\n", + " },\n", + " },\n", + " },\n", + " \"$schema\": \"https://json-schema.org/draft/2020-12/schema\",\n", + " }\n", + "}\n", + "\n", + "set_schema_response = authed_session.patch(es_endpoint, headers=header, json=json_data)\n", + "\n", + "print(json.dumps(set_schema_response.json(), indent=1))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "xRCbxXEG2XmF" + }, + "source": [ + "## [optional] Get the Schema\n", + "\n", + "Get the Schema and URL mapping to ensure it is updated according to your expectations." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "7TXWY_6nTH3u" + }, + "outputs": [], + "source": [ + "header = {\"X-Goog-User-Project\": PROJECT_ID}\n", + "es_endpoint = f\"https://discoveryengine.googleapis.com/{VAIS_BRANCH}/projects/{PROJECT_ID}/locations/{LOCATION}/collections/default_collection/dataStores/{DATASTORE_ID}/schemas/default_schema\"\n", + "get_schema_response = authed_session.get(es_endpoint, headers=header)\n", + "\n", + "print(json.dumps(get_schema_response.json(), indent=1))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "vESfZZ_QDLc8" + }, + "source": [ + "# Step 3. Results w/wo Filtering" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "nnnxTO8CC9Mz" + }, + "source": [ + "## Search Without Filter\n", + "Let's start by making a simple search on the datastore\n", + "\n", + "Note that the `Retreivable` Metadata fields defined in the schema are included in the `structData` field of the `result`).\n", + "\n", + "In our example, we issue the query \"house\" with a page size of 1, and get the following result:\n", + "![basic_search.png](https://storage.googleapis.com/github-repo/generative-ai/search/vais-building-blocks/query_level_boosting_filtering_and_facets/basic_search.png)\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "Usr8OMTu5EUk" + }, + "outputs": [], + "source": [ + "QUERY = \"\" # @param {type: 'string'}\n", + "PAGE_SIZE = None # @param {type: 'integer'}\n", + "\n", + "search_response = authed_session.post(\n", + " f\"https://discoveryengine.googleapis.com/{VAIS_BRANCH}/projects/{PROJECT_ID}/locations/{LOCATION}/collections/default_collection/dataStores/{DATASTORE_ID}/servingConfigs/default_search:search\",\n", + " headers={\"Content-Type\": \"application/json\"},\n", + " json={\"query\": QUERY, \"pageSize\": PAGE_SIZE},\n", + ")\n", + "\n", + "print(json.dumps(search_response.json(), indent=1))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "uCRfkzGyC53w" + }, + "source": [ + "## Search with Filter on a predefined field\n", + "Now let's apply a simple filter based on URL pattern. This is typically useful when your domain of interest has an interesting categorization of subdomains. Since the Google App Store used for our example doesn't have subdomains, we specify a certain pattern to only retrieve books with \"the roots\" in their title. The corresponding pattern will look like: \"play.google.com/store/books/details/\\*_The_roots_\\*\". We search for a generic Query \"Books\" but only get results with \"_The_root_\" in their URLs.\n", + "\n", + "See more examples of filtering based on URLs [here](https://cloud.google.com/generative-ai-app-builder/docs/filter-website-search#examples-advanced-indexing)\n", + "\n", + "You can also access [Google-inferred page date](https://cloud.google.com/generative-ai-app-builder/docs/boost-search-results#predefined-date-example) (i.e. datePublished and dateModified) which you can similarly use for filtering and boosting without a need for a Schema update.\n", + "\n", + "Note that you ger a different (and a more comprehensive) set of predefined fields for [basic website search](https://cloud.google.com/generative-ai-app-builder/docs/filter-website-search#filter-expressions-basic-indexing)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "qatUukazC4oH" + }, + "outputs": [], + "source": [ + "QUERY = \"\" # @param {type: 'string'}\n", + "PAGE_SIZE = None # @param {type: 'integer'}\n", + "\n", + "search_response = authed_session.post(\n", + " f\"https://discoveryengine.googleapis.com/{VAIS_BRANCH}/projects/{PROJECT_ID}/locations/{LOCATION}/collections/default_collection/dataStores/{DATASTORE_ID}/servingConfigs/default_search:search\",\n", + " headers={\"Content-Type\": \"application/json\"},\n", + " json={\n", + " \"query\": QUERY,\n", + " # Update this filter based on the structure of your domain/subdomains\n", + " \"filter\": 'siteSearch:\"https://play.google.com/store/books/details/*_The_roots_*\"',\n", + " \"pageSize\": PAGE_SIZE,\n", + " },\n", + ")\n", + "\n", + "print(json.dumps(search_response.json(), indent=1))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "gxblV5Bgx1fE" + }, + "source": [ + "## Search with Filter on a user-defined metadata\n", + "Next, let's apply sample filters based on user-defined metadata.\n", + "\n", + "In this example we limit our search to highly-rated free books. Let's also set a threshold for the number of ratings to make sure \"high rating\" is meaningful and reliable. (i.e. price < 10 AND aggregate_rating > 4.5 AND rating_count > 10).\n", + "\n", + "You can find more details on [Filter expression syntax](https://cloud.google.com/generative-ai-app-builder/docs/filter-search-metadata#filter-expression-syntax)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "q8k4Z07kx1fF" + }, + "outputs": [], + "source": [ + "QUERY = \"\" # @param {type: 'string'}\n", + "PAGE_SIZE = None # @param {type: 'integer'}\n", + "\n", + "search_response = authed_session.post(\n", + " f\"https://discoveryengine.googleapis.com/{VAIS_BRANCH}/projects/{PROJECT_ID}/locations/{LOCATION}/collections/default_collection/dataStores/{DATASTORE_ID}/servingConfigs/default_search:search\",\n", + " headers={\"Content-Type\": \"application/json\"},\n", + " json={\n", + " \"query\": QUERY,\n", + " # Update this filter definition based on your use case and metadata\n", + " \"filter\": \"rating_count>10 AND aggregate_rating>4.5 AND price=0\",\n", + " \"pageSize\": PAGE_SIZE,\n", + " },\n", + ")\n", + "\n", + "print(json.dumps(search_response.json(), indent=1))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "WeomJ_G-gcA9" + }, + "source": [ + "# Step 4. Get Facets from Document Metadata\n", + "\n", + "Facets are used to enhance user experience by providing UI elements that allow users to narrow down their search universe. They are most commonly found on retail website where you can choose your brand, size, color, etc after making an initial search (or even without putting in any queries as you \"Browse\" the landing page).\n", + "\n", + "Typically upon selection of a particular facet by the end user, you will issue a subsequent search with a corresponding filter added to update the results accordingly. Each facet response contains the syntax of its corresponding filter as well.\n", + "\n", + "![facets.png](https://storage.googleapis.com/github-repo/generative-ai/search/vais-building-blocks/query_level_boosting_filtering_and_facets/facets.png)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "rUtpm0E_mK-f" + }, + "source": [ + "## Fixed and Dynamic facets\n", + "\n", + "In order to get Facets in the response, you need to specify `facetSpecs` in the request. You can find more details in the [documentation](https://cloud.google.com/generative-ai-app-builder/docs/reference/rest/v1/FacetSpec).\n", + "\n", + "Each facet within the list of `facetSpecs` can have a fixed, or a dynamic positioning.\n", + "\n", + "In the response, facets with fixed positioning (i.e. enableDynamicPosition = False) will always show up at top with the same ordering as in the request. The Dynamics facets are ordered lower, with their relative ordering decided based on the query and likelihood of users being interested in them.\n", + "\n", + "For the fields to be eligible for Dynamic faceting, they should be specified as both indexable and dynamicFacetable in the [Schema](https://cloud.google.com/generative-ai-app-builder/docs/provide-schema). You also need to send [user events](https://cloud.google.com/generative-ai-app-builder/docs/user-events?hl=en) to make effective use of the facets. \n", + "\n", + "Below is a screenshot of the facet part of the response for a sample query used here:\n", + "\n", + "![facet_response.png](https://storage.googleapis.com/github-repo/generative-ai/search/vais-building-blocks/query_level_boosting_filtering_and_facets/facet_response.png)\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "GHDWUXW9xiAb" + }, + "outputs": [], + "source": [ + "QUERY = \"\" # @param {type: 'string'}\n", + "PAGE_SIZE = None # @param {type: 'integer'}\n", + "\n", + "search_response = authed_session.post(\n", + " f\"https://discoveryengine.googleapis.com/{VAIS_BRANCH}/projects/{PROJECT_ID}/locations/{LOCATION}/collections/default_collection/dataStores/{DATASTORE_ID}/servingConfigs/default_search:search\",\n", + " headers={\"Content-Type\": \"application/json\"},\n", + " json={\n", + " \"query\": QUERY,\n", + " # Update this facet definition based on your use case and metadata\n", + " \"facetSpecs\": [\n", + " {\"facetKey\": {\"key\": \"author\"}, \"limit\": 2, \"enableDynamicPosition\": False},\n", + " {\n", + " \"facetKey\": {\n", + " \"key\": \"aggregate_rating\",\n", + " \"intervals\": [\n", + " {\"minimum\": 0, \"maximum\": 3},\n", + " {\"minimum\": 3, \"maximum\": 4.5},\n", + " {\"minimum\": 4.5, \"maximum\": 5},\n", + " ],\n", + " },\n", + " \"limit\": 3,\n", + " \"enableDynamicPosition\": True,\n", + " },\n", + " {\n", + " \"facetKey\": {\n", + " \"key\": \"rating_count\",\n", + " \"intervals\": [\n", + " {\"minimum\": 0, \"maximum\": 10},\n", + " {\"minimum\": 10, \"maximum\": 100},\n", + " {\"minimum\": 100},\n", + " ],\n", + " },\n", + " \"limit\": 3,\n", + " \"enableDynamicPosition\": True,\n", + " },\n", + " ],\n", + " \"pageSize\": PAGE_SIZE,\n", + " },\n", + ")\n", + "\n", + "print(json.dumps(search_response.json(), indent=1))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "cpzZqjnc4M3g" + }, + "source": [ + "# Step 5. Influence the Ranking via Boosting" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "POtXQbRBc-SZ" + }, + "source": [ + "## Basic Boosting\n", + "As a basic demonstration of Boosting, let's look at a constant boost value on all documents meeting a certain criteria. A basic Boost has a condition (with the same syntax as [filters](https://cloud.google.com/generative-ai-app-builder/docs/filter-search-metadata#filter-expression-syntax)) and a Boost value.\n", + "\n", + "Boost value should be a number between -1.0 and +1.0 where negative numbers demote the matched documents (a.k.a. Bury). The boost function behaves roughly exponentially.\n", + "\n", + "It is generally advised to start with smaller boost values and adjust it as needed. If a document gets hit by several Boost conditions the boost amounts are additive.\n", + "\n", + "In this example we boost all the books written by \"Margaret Atwood\". The boost value in this example is set to 0.9. With this boost we get books by \"Margaret Atwood\" for a generic query like \"Book\", but if you search for a particular title not written by Margaret Atwood (e.g. \"house of cards\") you'd still get that title as the top result. to put it in physics terms, you can think of Boosting as a forcing function whereas Filters are constraints.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "jQuTzkiuc-Sb" + }, + "outputs": [], + "source": [ + "QUERY = \"\" # @param {type: 'string'}\n", + "PAGE_SIZE = None # @param {type: 'integer'}\n", + "\n", + "search_response = authed_session.post(\n", + " f\"https://discoveryengine.googleapis.com/{VAIS_BRANCH}/projects/{PROJECT_ID}/locations/{LOCATION}/collections/default_collection/dataStores/{DATASTORE_ID}/servingConfigs/default_search:search\",\n", + " headers={\"Content-Type\": \"application/json\"},\n", + " json={\n", + " \"boostSpec\": {\n", + " \"conditionBoostSpecs\": {\n", + " \"condition\": 'author: ANY(\"Margaret Atwood\")',\n", + " \"boost\": 0.9,\n", + " }\n", + " },\n", + " \"query\": QUERY,\n", + " \"pageSize\": PAGE_SIZE,\n", + " },\n", + ")\n", + "\n", + "print(json.dumps(search_response.json(), indent=1))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "tTlQ0-r7iFvU" + }, + "source": [ + "## Advanced Boosting\n", + "Now let's look at a more sophisticated example of a boost rule. As mentioned earlier in this notebook,it is generally advised to try VAIS results out of the box and/or leverage user events to fine tune the rankings based on user behavior. However, in some cases, customers are interested to apply a certain business logic to the results which makes custom rules inevitable.\n", + "\n", + "For this particular example we want to primarily leverage user provided ratings to influence search results, specifically rating count and rating average. We also leverage VAIS's ability to apply [piecewise linear boost functions](https://cloud.google.com/generative-ai-app-builder/docs/boost-search-results#custom-numerical-attr-boost) as opposed to fixed boost amounts. We apply different Boost values as the function of the average rating for different buckets of rating counts (see more details in comments of the code block below). We also apply a separate boost rule to boost books with a larger number of ratings irrespective of the average rating (i.e. generally popular books). To make sure that rule does not demote newer content unfairly, we supplement the boost rules by a freshness boost. Lastly to ensure we're not suggesting popular and highly rated, yet irrelevant books to all queries, we're adding a [relevancy threshold filter](https://cloud.google.com/generative-ai-app-builder/docs/filter-by-relevance).\n", + "\n", + "Note that the value and logic used here are for demonstration purposes. Please adjust them based on your business logic and metadata schema.\n", + "\n", + "You can find more examples of Boosting in [public documentation](https://cloud.google.com/generative-ai-app-builder/docs/boost-search-results)\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "hdIhVo8XiFvU" + }, + "outputs": [], + "source": [ + "QUERY = \"\" # @param {type: 'string'}\n", + "PAGE_SIZE = None # @param {type: 'integer'}\n", + "\n", + "search_response = authed_session.post(\n", + " f\"https://discoveryengine.googleapis.com/{VAIS_BRANCH}/projects/{PROJECT_ID}/locations/{LOCATION}/collections/default_collection/dataStores/{DATASTORE_ID}/servingConfigs/default_search:search\",\n", + " headers={\"Content-Type\": \"application/json\"},\n", + " json={\n", + " \"boostSpec\": {\n", + " \"conditionBoostSpecs\": [ # The absolute level of boost values can be offset to adjust the balance between recipes and other template types\n", + " {\n", + " \"condition\": \"rating_count>=10\", # only apply to books with more than 10 ratings\n", + " \"boostControlSpec\": {\n", + " \"attributeType\": \"NUMERICAL\",\n", + " \"interpolationType\": \"LINEAR\",\n", + " \"fieldName\": \"aggregate_rating\",\n", + " \"controlPoints\": [\n", + " {\n", + " \"attributeValue\": \"0.0\",\n", + " \"boostAmount\": -0.8,\n", + " }, # kill results with high rating count and low rating. They've had their chance!\n", + " {\n", + " \"attributeValue\": \"3.0\",\n", + " \"boostAmount\": -0.6,\n", + " }, # be aggressive for anything less than 3 stars\n", + " {\n", + " \"attributeValue\": \"4.5\",\n", + " \"boostAmount\": 0.0,\n", + " }, # People are typically generous. Let's assume 4.5 means ok\n", + " {\n", + " \"attributeValue\": \"5.0\",\n", + " \"boostAmount\": 0.3,\n", + " }, # go more aggressivly up as we get closer to 5. more than 35 votes very close to 5 means awesome.\n", + " ],\n", + " },\n", + " },\n", + " {\n", + " \"condition\": \"rating_count<10\", # Now let's consider books with fewer ratings\n", + " \"boostControlSpec\": {\n", + " \"attributeType\": \"NUMERICAL\",\n", + " \"interpolationType\": \"LINEAR\",\n", + " \"fieldName\": \"aggregate_rating\",\n", + " \"controlPoints\": [\n", + " {\n", + " \"attributeValue\": \"0.0\",\n", + " \"boostAmount\": -1.0,\n", + " }, # I really don't want to see low rating AND low rating count\n", + " {\n", + " \"attributeValue\": \"4.5\",\n", + " \"boostAmount\": 0,\n", + " }, # with average rating of 4.5, let's give it a chance\n", + " {\n", + " \"attributeValue\": \"5.0\",\n", + " \"boostAmount\": 0.1,\n", + " }, # a small boost, but with fewer reviews, high rating may not mean much.\n", + " ],\n", + " },\n", + " },\n", + " {\n", + " \"condition\": \"rating_count>=0\", # no particular meaning, it's just to make the condition True\n", + " \"boostControlSpec\": {\n", + " \"attributeType\": \"NUMERICAL\",\n", + " \"interpolationType\": \"LINEAR\",\n", + " \"fieldName\": \"rating_count\",\n", + " \"controlPoints\": [\n", + " {\n", + " \"attributeValue\": \"0\",\n", + " \"boostAmount\": -0.3,\n", + " }, # burry low rating count\n", + " {\n", + " \"attributeValue\": \"20\",\n", + " \"boostAmount\": 0.05,\n", + " }, # a steep boost curve from 0 to 20\n", + " {\n", + " \"attributeValue\": \"300\",\n", + " \"boostAmount\": 0.2,\n", + " }, # more gentle boost from 20 to 300\n", + " {\n", + " \"attributeValue\": \"1000\",\n", + " \"boostAmount\": 0.35,\n", + " }, # even mor gentle as we get passed 300, and saturate at 1000\n", + " ],\n", + " },\n", + " },\n", + " {\n", + " \"condition\": \"rating_count>=0\", # no particular meaning, it's just to make the condition True\n", + " \"boostControlSpec\": {\n", + " \"attributeType\": \"FRESHNESS\",\n", + " \"interpolationType\": \"LINEAR\",\n", + " \"fieldName\": \"date_published\",\n", + " \"controlPoints\": [\n", + " {\"attributeValue\": \"0d\", \"boostAmount\": 0.2},\n", + " {\"attributeValue\": \"30d\", \"boostAmount\": 0.15},\n", + " {\"attributeValue\": \"60d\", \"boostAmount\": 0.1},\n", + " {\"attributeValue\": \"180d\", \"boostAmount\": 0.0},\n", + " ],\n", + " },\n", + " },\n", + " ]\n", + " },\n", + " \"query\": QUERY,\n", + " \"relevanceThreshold\": \"MEDIUM\",\n", + " \"pageSize\": PAGE_SIZE,\n", + " },\n", + ")\n", + "\n", + "print(json.dumps(search_response.json(), indent=1))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "e1kgs_XdDlHL" + }, + "source": [ + "# Clean up" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "tGuk4ZnJk0S7" + }, + "source": [ + "## Delete the Search App\n", + "\n", + "Delete the App if you no longer need it\n", + "\n", + "Alternatively you can follow [these instructions](https://console.cloud.google.com/gen-app-builder/data-stores) to delete an App from the UI\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "QEfxXtzfk0rx" + }, + "outputs": [], + "source": [ + "response = authed_session.delete(\n", + " f\"https://discoveryengine.googleapis.com/{VAIS_BRANCH}/projects/{PROJECT_ID}/locations/{LOCATION}/collections/default_collection/engines/{APP_ID}\",\n", + " headers={\"X-Goog-User-Project\": PROJECT_ID},\n", + ")\n", + "\n", + "print(response.text)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "Tgm5idL4DjjU" + }, + "source": [ + "##Delete the Datastores\n", + "Delete the Datastore if you no longer need it\n", + "\n", + "Alternatively you can follow [these instructions](https://console.cloud.google.com/gen-app-builder/data-stores) to delete a Datastore from the UI" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "vj8BpuS62tgt" + }, + "outputs": [], + "source": [ + "response = authed_session.delete(\n", + " f\"https://discoveryengine.googleapis.com/{VAIS_BRANCH}/projects/{PROJECT_ID}/locations/{LOCATION}/collections/default_collection/dataStores/{DATASTORE_ID}\",\n", + " headers={\"X-Goog-User-Project\": PROJECT_ID},\n", + ")\n", + "\n", + "print(response.text)" + ] + } + ], + "metadata": { + "colab": { + "name": "query_level_boosting_filtering_and_facets.ipynb", + "toc_visible": true + }, + "kernelspec": { + "display_name": "Python 3", + "name": "python3" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} diff --git a/search/vais-building-blocks/record_user_events.ipynb b/search/vais-building-blocks/record_user_events.ipynb new file mode 100644 index 0000000..ba22277 --- /dev/null +++ b/search/vais-building-blocks/record_user_events.ipynb @@ -0,0 +1,607 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "5XNYlDkDLpqU" + }, + "outputs": [], + "source": [ + "# Copyright 2025 Google LLC\n", + "#\n", + "# Licensed under the Apache License, Version 2.0 (the \"License\");\n", + "# you may not use this file except in compliance with the License.\n", + "# You may obtain a copy of the License at\n", + "#\n", + "# https://www.apache.org/licenses/LICENSE-2.0\n", + "#\n", + "# Unless required by applicable law or agreed to in writing, software\n", + "# distributed under the License is distributed on an \"AS IS\" BASIS,\n", + "# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n", + "# See the License for the specific language governing permissions and\n", + "# limitations under the License." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "5tR528hOD4Dx" + }, + "source": [ + "# Recording Real-Time User Events in Vertex AI Search Datastores\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + "
    \n", + " \n", + " \"Google
    Open in Colab\n", + "
    \n", + "
    \n", + " \n", + " \"Google
    Open in Colab Enterprise\n", + "
    \n", + "
    \n", + " \n", + " \"Vertex
    Open in Vertex AI Workbench\n", + "
    \n", + "
    \n", + " \n", + " \"GitHub
    View on GitHub\n", + "
    \n", + "
    \n", + "\n", + "
    \n", + "\n", + "Share to:\n", + "\n", + "\n", + " \"LinkedIn\n", + "\n", + "\n", + "\n", + " \"Bluesky\n", + "\n", + "\n", + "\n", + " \"X\n", + "\n", + "\n", + "\n", + " \"Reddit\n", + "\n", + "\n", + "\n", + " \"Facebook\n", + "" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "pkd93iDpEBWx" + }, + "source": [ + "| | |\n", + "|----------|-------------|\n", + "| Author(s) | Hossein Mansour|\n", + "| Reviewers(s) | \tAbhishek Bhagwat|\n", + "| Last updated | 2025-01-07: The first draft |" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "yAnTektvEQjb" + }, + "source": [ + "# Overview\n", + "\n", + "\n", + "In this notebook, we demonstrate how to [record real-time](https://cloud.google.com/generative-ai-app-builder/docs/record-user-events) user events for Vertex AI Search (VAIS) Website Datastores. While there are subtle differences, a very similar approach can be applied to recording real-time user events for other types of VAIS datastores such as unstructured datastores containing PDFs.\n", + "\n", + "\n", + "User events are reported by customers as their users interact with their search and recommendation apps in different ways. [Event types](https://cloud.google.com/generative-ai-app-builder/docs/user-events#types) supported by the broader family of VAIS apps include search, view homepage, view category, view item, add to card, purchased, media play, and media complete. User events are the primary way VAIS recommendations apps learn from user behavior. They can also lead to significant improvements in search apps hence are highly encouraged.\n", + "\n", + "\n", + "In the context of Website search, VAIS considers both the relevancy of the page to the query as well as the likelihood of a click learned from user behavior on the public web to come up with the final ranking of the results. Reranking based on user behavior is most helpful for ambiguous head queries where many results are relevant to the query and the tie breaker is the user preference captured via click events.\n", + "\n", + "\n", + "While VAIS is designed to provide a great experience out-of-the-box, there is a possibility to further enhance the user experience by reporting historical and real time user events by the customer as additional signals. This enables the search app to learn from user interaction with the search results which leads to increasingly better ranking over time. This is particularly helpful when user behavior on a customer's domain significantly deviates from user behavior on the web. It is also important for authenticated search where the target pages are not publically available.\n", + "\n", + "![user_events.png](https://storage.googleapis.com/github-repo/generative-ai/search/vais-building-blocks/record_user_events/user_events.png)\n", + "\n", + "\n", + "User events can influence the ranking of the results in three tires:\n", + "- **Popularity** where pages with more clicks get prioritized\n", + "- **Relevancy** where the relevance of pages to a certain query is judged by the likelihood of a click learned from the past interaction of ALL users with the app\n", + "- **Personalization** where the preference of a certain user is captured via their own past interactions with the app\n", + "Depending on the quality and quantity of user events one or more of these tiers can get unlocked. Enabling these tiers does not require any explicit opt-in or config change by the customer.\n", + "\n", + "\n", + "In addition to better ranking, user events enable VAIS to report on more advanced search metrics such as CTR in the [analytics tab](https://cloud.google.com/generative-ai-app-builder/docs/view-analytics).\n", + "\n", + "\n", + "The focus of this notebook is on recording real-time user events, however VAIS allows for [importing historical user events](https://cloud.google.com/generative-ai-app-builder/docs/import-user-events) as well.\n", + "\n", + "\n", + "Additionally in this notebook we'll focus on explicit reporting of user events via `userEvents.write` method but there is an alternative way of leveraging [JavaPixel](https://cloud.google.com/generative-ai-app-builder/docs/record-user-events#record-with-javascript-pixel) to achieve the same goal which is the recommended approach if the customer can control the page source.\n", + "\n", + "\n", + "To run this notebook you need an existing advanced website search datastore.\n", + "\n", + "\n", + "We will perform the following steps:\n", + "\n", + "\n", + "- Define helper functions to issue search, and report search and view-item events\n", + "- Define a function to generate and report synthetic user events\n", + "- Issue a sample search and report corresponding events\n", + "- Cleanup\n", + "\n", + "\n", + "REST API is used throughout this notebook. Please consult the [official documentation](https://cloud.google.com/generative-ai-app-builder/docs/apis) for alternative ways to achieve the same goal, namely Client libraries and RPC.\n", + "\n", + "\n", + "\n", + "\n", + "# Vertex AI Search\n", + "Vertex AI Search (VAIS) is a fully-managed platform, powered by large language models, that lets you build AI-enabled search and recommendation experiences for your public or private websites or mobile applications\n", + "\n", + "\n", + "VAIS can handle a diverse set of data sources including structured, unstructured, and website data, as well as data from third-party applications such as Jira, Salesforce, and Confluence.\n", + "\n", + "\n", + "VAIS also has built-in integration with LLMs which enables you to provide answers to complex questions, grounded in your data\n", + "\n", + "\n", + "# Using this Notebook\n", + "If you're running outside of Colab, depending on your environment you may need to install pip packages that are included in the Colab environment by default but are not part of the Python Standard Library. Outside of Colab you'll also notice comments in code cells that look like #@something, these trigger special Colab functionality but don't change the behavior of the notebook.\n", + "\n", + "\n", + "This tutorial uses the following Google Cloud services and resources:\n", + "\n", + "\n", + "- Service Usage API\n", + "- Discovery Engine API\n", + "\n", + "\n", + "This notebook has been tested in the following environment:\n", + "\n", + "\n", + "- Python version = 3.10.12\n", + "- google.cloud.storage = 2.8.0\n", + "- google.auth = 2.27.0\n", + "\n", + "\n", + "# Getting Started\n", + "\n", + "\n", + "The following steps are necessary to run this notebook, no matter what notebook environment you're using.\n", + "\n", + "\n", + "If you're entirely new to Google Cloud, [get started here](https://cloud.google.com/docs/get-started)\n", + "\n", + "\n", + "## Google Cloud Project Setup\n", + "\n", + "\n", + "1. [Select or create a Google Cloud project](https://console.cloud.google.com/cloud-resource-manager). When you first create an account, you get a $300 free credit towards your compute/storage costs\n", + "2. [Make sure that billing is enabled for your project](https://cloud.google.com/billing/docs/how-to/modify-project)\n", + "3. [Enable the Service Usage API](https://console.cloud.google.com/apis/library/serviceusage.googleapis.com)\n", + "4. [Enable the Discovery Engine API for your project](https://console.cloud.google.com/marketplace/product/google/discoveryengine.googleapis.com)\n", + "\n", + "\n", + "## Google Cloud Permissions\n", + "\n", + "\n", + "Ideally you should have [Owner role](https://cloud.google.com/iam/docs/understanding-roles) for your project to run this notebook. If that is not an option, you need at least the following [roles](https://cloud.google.com/iam/docs/granting-changing-revoking-access)\n", + "- **`roles/serviceusage.serviceUsageAdmin`** to enable APIs\n", + "- **`roles/iam.serviceAccountAdmin`** to modify service agent permissions\n", + "- **`roles/discoveryengine.admin`** to modify discoveryengine assets\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "49x_J4vWOuNg" + }, + "source": [ + "#Setup Environment" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "kMYYfGpyOl5G" + }, + "source": [ + "## Authentication\n", + "\n", + " If you're using Colab, run the code in the next cell. Follow the pop-ups and authenticate with an account that has access to your Google Cloud [project](https://cloud.google.com/resource-manager/docs/creating-managing-projects#identifying_projects).\n", + "\n", + "If you're running this notebook somewhere besides Colab, make sure your environment has the right Google Cloud access. If that's a new concept to you, consider looking into [Application Default Credentials for your local environment](https://cloud.google.com/docs/authentication/provide-credentials-adc#local-dev) and [initializing the Google Cloud CLI](https://cloud.google.com/docs/authentication/gcloud). In many cases, running `gcloud auth application-default login` in a shell on the machine running the notebook kernel is sufficient.\n", + "\n", + "More authentication options are discussed [here](https://cloud.google.com/docs/authentication)." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": { + "id": "DZjtfEDG7Sr3" + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Authenticated\n" + ] + } + ], + "source": [ + "# Colab authentication.\n", + "import sys\n", + "\n", + "if \"google.colab\" in sys.modules:\n", + " from google.colab import auth\n", + "\n", + " auth.authenticate_user()\n", + " print(\"Authenticated\")" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": { + "id": "kT3Eda7_mlTP" + }, + "outputs": [], + "source": [ + "from google.auth import default\n", + "from google.auth.transport.requests import AuthorizedSession\n", + "\n", + "creds, _ = default()\n", + "authed_session = AuthorizedSession(creds)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "otijhCIjOzk-" + }, + "source": [ + "## Import Libraries" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": { + "id": "DlIp4zv3cdA7" + }, + "outputs": [], + "source": [ + "import time" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "N51y_mPgPHsj" + }, + "source": [ + "## Configure environment\n", + "\n", + "The Location of a Datastore is set at the time of creation and it should be called appropriately to query the Datastore. Given that our sample website datastore is `global`, we use that within this notebook.\n", + "\n", + "You can find more information regarding the `Location` of datastores and associated limitations [here](https://cloud.google.com/generative-ai-app-builder/docs/locations#specify_a_multi-region_for_your_data_store).\n", + "\n", + "`VAIS_BRANCH` is the branch of VAIS to use. Given that this feature is available in GA, we use V1 here." + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": { + "id": "hKLBf1GqROW7" + }, + "outputs": [], + "source": [ + "PROJECT_ID = \"\" # @param {type: 'string'}\n", + "DATASTORE_ID = \"\" # @param {type: 'string'}\n", + "LOCATION = \"global\" # @param [\"global\", \"us\", \"eu\"]\n", + "VAIS_BRANCH = \"v1\" # @param {type: 'string'}" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "Akk3C5vK8oG6" + }, + "source": [ + "# Step 1. Define Helper functions\n", + "\n", + "In this section we define helper functions to issue a sample search request to a datastore, report a search event, report a view-item event, and pull all of these together to simulate the flow of a user doing search and clicking on a result.\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "C2hXlewDINDg" + }, + "source": [ + "## Helper functions to issue basic search on a Datastore" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "v-XHQIOooshe" + }, + "outputs": [], + "source": [ + "def search(\n", + " project_id: str, location: str, datastore_id: str, searchQuery: str, pageSize: int\n", + "):\n", + " \"\"\"Searches a datastore using the provided query.\"\"\"\n", + " response = authed_session.post(\n", + " f\"https://discoveryengine.googleapis.com/{VAIS_BRANCH}/projects/{project_id}/locations/{location}/collections/default_collection/dataStores/{datastore_id}/servingConfigs/default_search:search\",\n", + " headers={\n", + " \"Content-Type\": \"application/json\",\n", + " },\n", + " json={\"query\": searchQuery, \"pageSize\": pageSize},\n", + " )\n", + " return response" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "eAigF6KHkMZ2" + }, + "source": [ + "## Helper functions to report search and view-item events" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "IO1AxLZckXYK" + }, + "outputs": [], + "source": [ + "def search_event(\n", + " project_id: str,\n", + " location: str,\n", + " datastore_id: str,\n", + " user_pseudo_id: str,\n", + " search_query: str,\n", + " impressions: list,\n", + " attribution_token: str,\n", + "):\n", + " \"\"\"Report search events.\"\"\"\n", + " response = authed_session.post(\n", + " f\"https://discoveryengine.googleapis.com/{VAIS_BRANCH}/projects/{project_id}/locations/{location}/collections/default_collection/dataStores/{datastore_id}/userEvents:write\",\n", + " headers={\n", + " \"Content-Type\": \"application/json\",\n", + " },\n", + " json={\n", + " \"eventType\": \"search\",\n", + " \"userPseudoId\": user_pseudo_id,\n", + " \"searchInfo\": {\n", + " \"searchQuery\": search_query,\n", + " },\n", + " \"documents\": impressions,\n", + " \"attributionToken\": attribution_token,\n", + " },\n", + " )\n", + " return response\n", + "\n", + "\n", + "def view_event(\n", + " project_id: str,\n", + " location: str,\n", + " datastore_id: str,\n", + " user_pseudo_id: str,\n", + " viewed_uri: str,\n", + " attribution_token: str,\n", + "):\n", + " \"\"\"Report view-item events.\"\"\"\n", + " response = authed_session.post(\n", + " f\"https://discoveryengine.googleapis.com/{VAIS_BRANCH}/projects/{project_id}/locations/{location}/collections/default_collection/dataStores/{datastore_id}/userEvents:write\",\n", + " headers={\n", + " \"Content-Type\": \"application/json\",\n", + " },\n", + " json={\n", + " \"attributionToken\": attribution_token,\n", + " \"eventType\": \"view-item\",\n", + " \"userPseudoId\": user_pseudo_id,\n", + " \"documents\": [{\"uri\": viewed_uri}],\n", + " },\n", + " )\n", + " return response" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "DYArgsiAiVfs" + }, + "source": [ + "# Step 2. Define a function to generate and report synthetic user events\n", + "\n", + "In this function we issue a search, report a search event which includes the corresponding attribution token and impressions (i.e. list of results shown to the end user), and finally report a view-item event which shows which link did the user click on.\n", + "\n", + "For the purpose of this notebook, we extract the list of impressions from the search response, however some customers may apply their own post-processiong logic in which case the final ranking of the results may differ from the one in VAIS response. For those cases the explicit list of impressions should be provided in the following format:\n", + "```\n", + "impressions = [\n", + " {\n", + " 'uri': 'https://example.com/page-1'\n", + " },\n", + " {\n", + " 'uri': 'https://example.com/page-2'\n", + " },\n", + " {\n", + " 'uri': 'https://example.com/page-3[link text](https://)'\n", + " },\n", + "]\n", + "```\n", + "\n", + "Note that the only change needed to apply the same approach to reporting user events for other types of datastores is to [identify documents](https://cloud.google.com/generative-ai-app-builder/docs/reference/rest/v1/projects.locations.dataStores.userEvents#documentinfo) with other fields such as document id.\n", + "\n", + "\n", + "On a broader note, a similar approach can be applied to reporting user events for recommendation apps. See the full anatomy of a user event in [reference documentation](https://cloud.google.com/generative-ai-app-builder/docs/reference/rest/v1/projects.locations.dataStores.userEvents). Noteworthy is the attributes field which can be used to improve recommendation by providing additional user info such as their age, gender, location, etc." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "_0uAQTKD78_k" + }, + "outputs": [], + "source": [ + "def generate_synthetic_events(\n", + " project_id: str,\n", + " location: str,\n", + " datastore_id: str,\n", + " user_pseudo_id: str,\n", + " search_query: str,\n", + " page_size: int,\n", + " viewed_uri: str,\n", + "):\n", + " \"\"\"Issue a search hand report the corresponding events.\"\"\"\n", + "\n", + " search_resp = search(project_id, location, datastore_id, search_query, page_size)\n", + " print(\"Search response:\")\n", + " print(search_resp.json())\n", + " attribution_token = search_resp.json()[\"attributionToken\"]\n", + "\n", + " impressions = [\n", + " {\"uri\": result[\"document\"][\"derivedStructData\"][\"link\"]}\n", + " for result in search_resp.json()[\"results\"]\n", + " ]\n", + " print(\"Impression list:\")\n", + " print(impressions)\n", + " time.sleep(1)\n", + "\n", + " search_event_resp = search_event(\n", + " project_id,\n", + " location,\n", + " datastore_id,\n", + " user_pseudo_id,\n", + " search_query,\n", + " impressions,\n", + " attribution_token,\n", + " )\n", + " print(\"Sent search event:\")\n", + " print(search_event_resp.json())\n", + " time.sleep(2)\n", + "\n", + " view_event_resp = view_event(\n", + " project_id,\n", + " location,\n", + " datastore_id,\n", + " user_pseudo_id,\n", + " viewed_uri,\n", + " attribution_token,\n", + " )\n", + " print(\"Sent view event:\")\n", + " print(view_event_resp.json())" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "HtpTWTZIVsFD" + }, + "source": [ + "# Step 3. Issue a sample search and report corresponding events\n", + "\n", + "In this example we have a VAIS website datastore for VAIS documentation. An end user with user_pseudo_id of Alice searches for the term `filter results`. The top five results are presented and Alice clicks on one of the results, a page describing how to filter generic search results in this instance.\n", + "\n", + "Please modify these parameters as it relates to you target datastore." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "p0fO9uNgVzKl" + }, + "outputs": [], + "source": [ + "generate_synthetic_events(\n", + " project_id=PROJECT_ID,\n", + " location=LOCATION,\n", + " datastore_id=DATASTORE_ID,\n", + " user_pseudo_id=\"Alice\",\n", + " search_query=\"filter results\",\n", + " page_size=5,\n", + " viewed_uri=\"https://cloud.google.com/generative-ai-app-builder/docs/filter-search-metadata\",\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "N4A-_l_jLY5W" + }, + "source": [ + "# Cleanup\n", + "\n", + "Cleanup resources created in this notebook, specifically purging the events recorded in the notebook.\n", + "\n", + "Purge events can optionally have filters to only purge a subset of events. We purge events reported for \"Alice\" userPseudoId.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "WgMKwi7hLZjA" + }, + "outputs": [], + "source": [ + "def purge_event(project_id: str, location: str, datastore_id: str, purge_filter: str):\n", + " \"\"\"Purge events matching a filter.\"\"\"\n", + " response = authed_session.post(\n", + " f\"https://discoveryengine.googleapis.com/{VAIS_BRANCH}/projects/{project_id}/locations/{location}/collections/default_collection/dataStores/{datastore_id}/userEvents:purge\",\n", + " headers={\n", + " \"Content-Type\": \"application/json\",\n", + " },\n", + " json={\"filter\": purge_filter},\n", + " )\n", + " return response\n", + "\n", + "\n", + "resp = purge_event(\n", + " project_id=PROJECT_ID,\n", + " location=LOCATION,\n", + " datastore_id=DATASTORE_ID,\n", + " purge_filter='eventTime>\"2024-12-23T18:25:43.511Z\" userPseudoId=\"Alice\"',\n", + ")\n", + "print(resp.json())" + ] + } + ], + "metadata": { + "colab": { + "name": "record_user_events.ipynb", + "toc_visible": true + }, + "kernelspec": { + "display_name": "Python 3", + "name": "python3" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} diff --git a/search/vertexai-search-options/search_options.png b/search/vertexai-search-options/search_options.png new file mode 100644 index 0000000..1a7be45 Binary files /dev/null and b/search/vertexai-search-options/search_options.png differ diff --git a/search/vertexai-search-options/vertex_ai_search_website_summary.ipynb b/search/vertexai-search-options/vertex_ai_search_website_summary.ipynb new file mode 100644 index 0000000..49bc9a9 --- /dev/null +++ b/search/vertexai-search-options/vertex_ai_search_website_summary.ipynb @@ -0,0 +1,668 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "id": "552cacc9-3791-47a7-bacb-0768a1799d5a", + "metadata": { + "id": "2eec5cc39a59" + }, + "outputs": [], + "source": [ + "# Copyright 2024 Google LLC\n", + "#\n", + "# Licensed under the Apache License, Version 2.0 (the \"License\");\n", + "# you may not use this file except in compliance with the License.\n", + "# You may obtain a copy of the License at\n", + "#\n", + "# https://www.apache.org/licenses/LICENSE-2.0\n", + "#\n", + "# Unless required by applicable law or agreed to in writing, software\n", + "# distributed under the License is distributed on an \"AS IS\" BASIS,\n", + "# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n", + "# See the License for the specific language governing permissions and\n", + "# limitations under the License." + ] + }, + { + "cell_type": "markdown", + "id": "6d5d6512-5032-4709-9cc9-ec94095189ef", + "metadata": { + "id": "42004fb8ef27" + }, + "source": [ + "# Q&A Chatbot with Vertex AI Search for summarized website results without advanced indexing\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + "
    \n", + " \n", + " \"Google
    Open in Colab\n", + "
    \n", + "
    \n", + " \n", + " \"Google
    Open in Colab Enterprise\n", + "
    \n", + "
    \n", + " \n", + " \"Vertex
    Open in Vertex AI Workbench\n", + "
    \n", + "
    \n", + " \n", + " \"GitHub
    View on GitHub\n", + "
    \n", + "
    \n", + "\n", + "
    \n", + "\n", + "Share to:\n", + "\n", + "\n", + " \"LinkedIn\n", + "\n", + "\n", + "\n", + " \"Bluesky\n", + "\n", + "\n", + "\n", + " \"X\n", + "\n", + "\n", + "\n", + " \"Reddit\n", + "\n", + "\n", + "\n", + " \"Facebook\n", + " " + ] + }, + { + "cell_type": "markdown", + "id": "51148404-3cff-4d11-af8d-29200e604b26", + "metadata": { + "id": "c0f3f355347e" + }, + "source": [ + "| | |\n", + "|-|-|\n", + "|Author | [Neeraj Shivhare](https://github.com/nshivhar) |" + ] + }, + { + "cell_type": "markdown", + "id": "ab0e6437-898f-41f4-9006-c3d3e341704a", + "metadata": { + "id": "2e673e771345" + }, + "source": [ + "## Objective\n", + "\n", + "The main goal of this code is to provide a way to query a website data store in Vertex AI Search, retrieve the most relevant webpage, and extract and summarize its content. This can be used to build a question-answering system or to simply retrieve and present information from a website in a concise manner.\n", + "\n", + "### Key Features\n", + "- Vertex AI Search Integration: Utilizes the Discovery Engine API to query a website data store in Vertex AI Search.\n", + "- Top Result Retrieval: Selects the first (presumably most relevant) URL from the search results.\n", + "- Webpage Content Extraction: Fetches the webpage content using requests and extracts relevant information (title, description, page content) using BeautifulSoup.\n", + "- Gemini 2.0 Summarization: Using Gemini 2.0 to summarize the extracted page content. This would involve sending the page_content to the Gemini API for summarization.\n", + "\n", + "\n", + "## How to use the notebook\n", + "- Initialization: Initialize the notebook by providing your `project_id` and `data_store_id`.\n", + "- Search: Call the `get_page_contents` method with your `search_query`. This method will:\n", + " 1. Perform the search using Vertex AI Search.\n", + " 2. Extract the first link from the results.\n", + " 3. Fetch and format the content from the link.\n", + "- Print the formatted document details (title, source, description, page content).\n", + "- Summarize the content using Gemini 2.0 (not shown in the code)." + ] + }, + { + "cell_type": "markdown", + "id": "a9caaa86-fa5f-4b38-bbad-8d3209eac12a", + "metadata": { + "id": "628e815b6b1f" + }, + "source": [ + "## Getting Started\n", + "\n", + "### Install Vertex AI SDK for Python" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "75a04e1e-03dc-421d-a8eb-ad53a41baa64", + "metadata": { + "id": "3f035428fdba" + }, + "outputs": [], + "source": [ + "%pip install google-cloud-discoveryengine==0.12.1 langchain_google_vertexai" + ] + }, + { + "cell_type": "markdown", + "id": "793c2ef4-8010-4a69-86f1-4ad851b33c07", + "metadata": { + "id": "2d4000d88ad8" + }, + "source": [ + "### Restart kernel" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "e3d8bf53-93a1-4706-8edc-c14a7486c3f5", + "metadata": { + "id": "0c5492fd0156" + }, + "outputs": [], + "source": [ + "# Restart kernel after installs so that your environment can access the new packages\n", + "import IPython\n", + "\n", + "app = IPython.Application.instance()\n", + "app.kernel.do_shutdown(True)" + ] + }, + { + "cell_type": "markdown", + "id": "d61c331b-1f44-4a62-9529-d2755d9c4fae", + "metadata": { + "id": "6582b5d47c28" + }, + "source": [ + "### Authenticate your notebook environment (Colab only)\n", + "\n", + "If you are running this notebook on Google Colab, run the following cell to authenticate your environment. This step is not required if you are using [Vertex AI Workbench](https://cloud.google.com/vertex-ai-notebooks?hl=en)." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "c7aa2fc1-33d4-4b48-b02e-4736d13fa577", + "metadata": { + "id": "4788c6f28f01" + }, + "outputs": [], + "source": [ + "import sys\n", + "\n", + "# Additional authentication is required for Google Colab\n", + "if \"google.colab\" in sys.modules:\n", + " # Authenticate user to Google Cloud\n", + " from google.colab import auth\n", + "\n", + " auth.authenticate_user()" + ] + }, + { + "cell_type": "markdown", + "id": "9e33e9ef-0617-4ff9-b03f-44157a9e3384", + "metadata": { + "id": "9ccc6635848a" + }, + "source": [ + "### imports" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "f606c3df-f9b6-429c-a05c-aac5212f1605", + "metadata": { + "id": "9b3d8b3cfab5" + }, + "outputs": [], + "source": [ + "import logging\n", + "from typing import TypeVar\n", + "\n", + "from bs4 import BeautifulSoup\n", + "from google.api_core.client_options import ClientOptions\n", + "from google.cloud import discoveryengine_v1beta as discoveryengine\n", + "from google.protobuf import json_format\n", + "from langchain_core.prompts import PromptTemplate\n", + "from langchain_google_vertexai import VertexAI\n", + "import requests\n", + "\n", + "Output_co = TypeVar(\"Output_co\", covariant=True)" + ] + }, + { + "cell_type": "markdown", + "id": "93617401-cac3-47c5-9e4a-b57081986a78", + "metadata": { + "id": "51d2b9ba060f" + }, + "source": [ + "### Initialization" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6480c516-7196-4da3-8e7d-3d06d33e9a98", + "metadata": { + "id": "00f9e929ba8c" + }, + "outputs": [], + "source": [ + "# Use the environment variable if the user doesn't provide Project ID.\n", + "import os\n", + "\n", + "import vertexai\n", + "\n", + "PROJECT_ID = \"[your-project-id]\" # @param {type: \"string\", placeholder: \"[your-project-id]\" isTemplate: true}\n", + "if not PROJECT_ID or PROJECT_ID == \"[your-project-id]\":\n", + " PROJECT_ID = str(os.environ.get(\"GOOGLE_CLOUD_PROJECT\"))\n", + "\n", + "LOCATION = os.environ.get(\"GOOGLE_CLOUD_REGION\", \"us-central1\")\n", + "\n", + "vertexai.init(project=PROJECT_ID, location=LOCATION)\n", + "!gcloud config set project {project_id}\n", + "\n", + "DATA_STORE_LOCATION = \"global\" # @param {type: \"string\"}\n", + "DATA_STORE_ID = \"your_web_datastore_id\" # @param {type: \"string\"}\n", + "\n", + "logging.basicConfig(level=logging.INFO)\n", + "logger = logging.getLogger(__name__)" + ] + }, + { + "cell_type": "markdown", + "id": "f11da7ed-37a2-466b-9607-af11a2511dd5", + "metadata": { + "id": "a40e6c6f0faf" + }, + "source": [ + "### Initialize the Discovery Engine client" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "5e3baab5-9ba6-4608-9297-4d04aa683618", + "metadata": { + "id": "2e404af887cf" + }, + "outputs": [], + "source": [ + "logger.info(\"Initializing Discovery Engine client...\")\n", + "client_options = (\n", + " ClientOptions(api_endpoint=f\"{LOCATION}-discoveryengine.googleapis.com\")\n", + " if LOCATION != \"global\"\n", + " else None\n", + ")\n", + "client = discoveryengine.SearchServiceClient(client_options=client_options)" + ] + }, + { + "cell_type": "markdown", + "id": "030c31a9-6003-449b-bb8c-9dd735a70093", + "metadata": { + "id": "2ca07ed2fdea" + }, + "source": [ + "### Search the data store using the Google Cloud Discovery Engine API" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "15597eb5-9c72-4dee-b9bb-ea0c6b28773b", + "metadata": { + "id": "23dec5882d8e" + }, + "outputs": [], + "source": [ + "def get_relevant_snippets(search_query: str) -> None | (discoveryengine.SearchResponse):\n", + " \"\"\"\n", + " Search the data store using the Google Cloud Discovery Engine API.\n", + "\n", + " Args:\n", + " search_query (str): The search query string.\n", + "\n", + " Returns:\n", + " Optional[discoveryengine.SearchResponse]:\n", + " The search response from the Discovery Engine API.\n", + " \"\"\"\n", + " logger.info(\"Searching data store with query: %s\", search_query)\n", + " try:\n", + " serving_config = client.serving_config_path(\n", + " project=PROJECT_ID,\n", + " location=LOCATION,\n", + " data_store=DATA_STORE_ID,\n", + " serving_config=\"default_config\",\n", + " )\n", + "\n", + " content_search_spec = discoveryengine.SearchRequest.ContentSearchSpec(\n", + " snippet_spec=discoveryengine.SearchRequest.ContentSearchSpec.SnippetSpec(\n", + " return_snippet=True\n", + " )\n", + " )\n", + "\n", + " request = discoveryengine.SearchRequest(\n", + " serving_config=serving_config,\n", + " query=search_query,\n", + " page_size=5,\n", + " content_search_spec=content_search_spec,\n", + " query_expansion_spec=discoveryengine.SearchRequest.QueryExpansionSpec(\n", + " condition=discoveryengine.SearchRequest.QueryExpansionSpec.Condition.AUTO,\n", + " ),\n", + " spell_correction_spec=discoveryengine.SearchRequest.SpellCorrectionSpec(\n", + " mode=discoveryengine.SearchRequest.SpellCorrectionSpec.Mode.AUTO\n", + " ),\n", + " )\n", + "\n", + " response = client.search(request)\n", + " logger.info(\"Search successful.\")\n", + " return response\n", + "\n", + " except Exception as e: # pylint: disable=broad-exception-caught\n", + " logger.error(\"Error during data store search: %s\", e)\n", + " return None" + ] + }, + { + "cell_type": "markdown", + "id": "aec3a92f-916b-4b29-b45a-780b31b7a655", + "metadata": { + "id": "ac0a77f6fb55" + }, + "source": [ + "### Extracts the first link from the top search response." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "7bd1a8cb-9e05-4204-805a-61cd7dabaa21", + "metadata": { + "id": "1908e84e6489" + }, + "outputs": [], + "source": [ + "def get_first_link(response: discoveryengine.SearchResponse | None) -> str | None:\n", + " \"\"\"\n", + " Extracts the first link from the search response.\n", + "\n", + " Args:\n", + " response (Optional[discoveryengine.SearchResponse]):\n", + " The search response object from the Discovery Engine API.\n", + "\n", + " Returns:\n", + " Optional[str]: The first link extracted from the search results.\n", + " \"\"\"\n", + " logger.info(\"Extracting first link from search response...\")\n", + " if response is None or not response.results:\n", + " logger.error(\"No results found or empty response.\")\n", + " return None\n", + "\n", + " try:\n", + " first_result = response.results[0]\n", + " result_json = json_format.MessageToDict(\n", + " first_result.document._pb # pylint: disable=protected-access\n", + " )\n", + " derived_struct_data = result_json.get(\"derivedStructData\", {})\n", + " link = derived_struct_data.get(\"link\", None)\n", + " logger.info(\"First link extracted successfully: %s\", link)\n", + " return link\n", + " except Exception as e: # pylint: disable=broad-exception-caught\n", + " logger.error(\"Error extracting link from results: %s\", e)\n", + " return None" + ] + }, + { + "cell_type": "markdown", + "id": "74d7b43e-4a14-4a1b-9de6-b132f6313d8f", + "metadata": { + "id": "0905b7c8fb99" + }, + "source": [ + "### Loads and formats the full text from the given link using requests and BeautifulSoup." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "39b5e9d5-5f0e-4b60-88e6-b3f66f83e26a", + "metadata": { + "id": "1885380243bb" + }, + "outputs": [], + "source": [ + "def load_and_format_page_content(link: str) -> dict[str, str] | None:\n", + " \"\"\"\n", + " Loads and formats the full text from the given link using requests and\n", + " BeautifulSoup.\n", + "\n", + " Args:\n", + " link (str): The URL to fetch and extract the content from.\n", + "\n", + " Returns:\n", + " Optional[Dict[str, str]]:\n", + " A dictionary with formatted source,\n", + " title, description,\n", + " and page content.\n", + " \"\"\"\n", + " logger.info(\"Loading and formatting page content from: %s\", link)\n", + " try:\n", + " response = requests.get(link)\n", + " response.raise_for_status() # Ensure we notice bad responses\n", + "\n", + " soup = BeautifulSoup(response.text, \"html.parser\")\n", + "\n", + " # Extract title, source, description, and page content\n", + " title = soup.title.string.strip() if soup.title else \"No title available\"\n", + " source = link\n", + " description_meta = soup.find(\"meta\", {\"name\": \"description\"})\n", + " description = (\n", + " description_meta[\"content\"]\n", + " if description_meta\n", + " else \"No description available\"\n", + " )\n", + " page_content = \" \".join(p.get_text() for p in soup.find_all(\"p\"))\n", + "\n", + " logger.info(\"Page content loaded and formatted successfully.\")\n", + " return {\n", + " \"source\": source,\n", + " \"title\": title,\n", + " \"description\": description,\n", + " \"page_content\": page_content,\n", + " }\n", + " except Exception as e: # pylint: disable=broad-exception-caught\n", + " logger.error(\"Error loading content from link: %s\", e)\n", + " return None" + ] + }, + { + "cell_type": "markdown", + "id": "15582e7a-251c-4fad-91c6-6f4b6e780532", + "metadata": { + "id": "551c725708e5" + }, + "source": [ + "### Performs a search, extracts the first link, and retrieves and formats the full text from the link." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "733df3e7-74fc-4bcd-81d9-5a8cc191469c", + "metadata": { + "id": "c963366b9a48" + }, + "outputs": [], + "source": [ + "def get_page_contents(search_query: str) -> str | None:\n", + " \"\"\"\n", + " Performs a search, extracts the first link,\n", + " and retrieves and formats the full text from the link.\n", + "\n", + " Args:\n", + " search_query (str): The search query string.\n", + "\n", + " Returns:\n", + " Optional[str]:\n", + " The full text extracted from the first link of the search results.\n", + " \"\"\"\n", + " logger.info(\"Getting page contents for query: %s\", search_query)\n", + " response = get_relevant_snippets(search_query)\n", + " link = get_first_link(response)\n", + " if link:\n", + " details = load_and_format_page_content(link)\n", + " logger.info(\"Page contents retrieved successfully.\")\n", + " return details if details else None\n", + " logger.warning(\"No link found for the query.\")\n", + " return None" + ] + }, + { + "cell_type": "markdown", + "id": "bf4769fc-5e7c-4f68-8202-7d50b0a3f2a5", + "metadata": { + "id": "01ad085708b7" + }, + "source": [ + "#### Prompt for response summarization" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "32efb3dc-6108-4c33-8604-02fda882e461", + "metadata": { + "id": "4ba87432ee5c" + }, + "outputs": [], + "source": [ + "WEBPAGE_EXTRACTION_TEMPLATE = \"\"\" You are a helpful and informative Q&A bot. A user will provide you with text content from a web page and ask questions related to it. \n", + "Your task is to analyze the content and answer the user's questions accurately and concisely. \n", + "\n", + "Here's how you should approach each request:\n", + "\n", + "1. Thoroughly read the provided web page content.\n", + "2. Understand the user's question.\n", + "3. Identify the relevant information within the content.\n", + "4. Formulate a clear and concise answer based on the content.Whenever possible, use bullet points to summarize the answer.\n", + "5. If the answer cannot be found in the content**, say \"I'm sorry, but I cannot find the answer to that question in the provided text.\"\n", + "\n", + "Example:\n", + "\n", + "User:\n", + "Here's the content from a web page: {context} \n", + "My question is: What are the use cases of Vertex AI?\n", + "\n", + "Bot:\n", + "[Provide a concise answer based on the web page content, Use Markdown and bullet Points where ever applicable. If the answer is not found, say you cannot find it.] \n", + "\"\"\"\n", + "WEBPAGE_EXTRACTION_PROMPT = PromptTemplate(\n", + " input_variables=[\"context\"], template=WEBPAGE_EXTRACTION_TEMPLATE\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "688dd8ff-da62-4e5c-9de5-40b72d992d79", + "metadata": { + "id": "451ba8781bd8" + }, + "source": [ + "### Create Q&A chain" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "dfafed12-9ddd-46e4-8093-d6aa048751f5", + "metadata": { + "id": "3dd1545bfbde" + }, + "outputs": [], + "source": [ + "def get_chain():\n", + " \"\"\"Return a RunnableSerializable Chain\"\"\"\n", + " logger.info(\"Building VertexAI chain...\")\n", + " search_llm_kwargs = {\"prompt\": WEBPAGE_EXTRACTION_PROMPT}\n", + "\n", + " return VertexAI(\n", + " model_name=\"gemini-2.0-flash\",\n", + " verbose=False,\n", + " search_llm_kwargs=search_llm_kwargs,\n", + " return_direct=False,\n", + " generation_config={\n", + " \"temperature\": 0.2,\n", + " \"top_p\": 0.95,\n", + " \"top_k\": 10,\n", + " \"max_output_tokens\": 4000,\n", + " },\n", + " )" + ] + }, + { + "cell_type": "markdown", + "id": "87155e1b-c9d1-4b47-be93-d25aa3d6804d", + "metadata": { + "id": "df21ab2864be" + }, + "source": [ + "### Invoking chain with query" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "id": "83373eb9-5505-4ff6-8489-2e0d39989e8d", + "metadata": { + "id": "adfab87fc413" + }, + "outputs": [], + "source": [ + "def invoke(query: str) -> Output_co:\n", + " \"\"\"Invoke chain and return the answer\"\"\"\n", + " logger.info(\"Invoking chain with query: %s\", query)\n", + " page_content = get_page_contents(query)\n", + " if page_content:\n", + " logger.info(\"Page content retrieved successfully.\")\n", + " chain = get_chain()\n", + " formatted_prompt = WEBPAGE_EXTRACTION_PROMPT.format(context=page_content)\n", + " response = chain(formatted_prompt)\n", + " return response[\"result\"] if \"result\" in response else response\n", + " logger.warning(\"No relevant context found to summarize.\")\n", + " return \"No relevant context found to summarize.\"" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "id": "766b76c6-2a35-41f6-9b01-8ec9f40dccc9", + "metadata": { + "id": "d28bcaca4870" + }, + "outputs": [], + "source": [ + "search_query = \"What are the benefits of Vertex AI?\"\n", + "invoke(search_query)" + ] + } + ], + "metadata": { + "colab": { + "name": "vertex_ai_search_website_summary.ipynb", + "toc_visible": true + }, + "kernelspec": { + "display_name": "Python 3", + "name": "python3" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} diff --git a/search/vertexai-search-options/vertexai_search_options.ipynb b/search/vertexai-search-options/vertexai_search_options.ipynb new file mode 100644 index 0000000..a1307c6 --- /dev/null +++ b/search/vertexai-search-options/vertexai_search_options.ipynb @@ -0,0 +1,632 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "ur8xi4C7S06n" + }, + "outputs": [], + "source": [ + "# Copyright 2024 Google LLC\n", + "#\n", + "# Licensed under the Apache License, Version 2.0 (the \"License\");\n", + "# you may not use this file except in compliance with the License.\n", + "# You may obtain a copy of the License at\n", + "#\n", + "# https://www.apache.org/licenses/LICENSE-2.0\n", + "#\n", + "# Unless required by applicable law or agreed to in writing, software\n", + "# distributed under the License is distributed on an \"AS IS\" BASIS,\n", + "# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n", + "# See the License for the specific language governing permissions and\n", + "# limitations under the License." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "JAPoU8Sm5E6e" + }, + "source": [ + "# Building Search Applications with Vertex AI Search\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + "
    \n", + " \n", + " \"Google
    Open in Colab\n", + "
    \n", + "
    \n", + " \n", + " \"Google
    Open in Colab Enterprise\n", + "
    \n", + "
    \n", + " \n", + " \"Vertex
    Open in Vertex AI Workbench\n", + "
    \n", + "
    \n", + " \n", + " \"GitHub
    View on GitHub\n", + "
    \n", + "
    \n", + "\n", + "
    \n", + "\n", + "Share to:\n", + "\n", + "\n", + " \"LinkedIn\n", + "\n", + "\n", + "\n", + " \"Bluesky\n", + "\n", + "\n", + "\n", + " \"X\n", + "\n", + "\n", + "\n", + " \"Reddit\n", + "\n", + "\n", + "\n", + " \"Facebook\n", + " " + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "84f0f73a0f76" + }, + "source": [ + "| | |\n", + "|-|-|\n", + "| Author(s) | [Megha Agarwal](https://github.com/agarwal22megha/) |" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "tvgnzT1CKxrO" + }, + "source": [ + "## Overview\n", + "\n", + "Vertex AI Search leverages decades of expertise Google has in information retrieval and brings together the power of deep information retrieval, state-of-the-art natural language processing, and the latest in large language model (LLM) processing to understand user intent and return the most relevant results for the user.\n", + "\n", + "Based on where Developers are in their journey, their orchestration framework of choice, they can select Vertex AI Search out-of-the-box capabilities or customize their search solutions with Vertex Retrievers or use the Vertex AI DIY APIs to build the end to end RAG application.\n", + "\n", + "This notebook explores how you can leverage Vertex AI Search out-of-the-box capabilities/ customize your search application with Vertex AI Search Retriever via LangChain or Grounding Service.\n", + "\n", + "\n", + "We create a Vertex AI Agent Builder Search App with unstructured data store on Google Cloud Console.\n", + "\n", + "Once the Search App is created, this notebook walks you through how you can leverage:\n", + "\n", + "1. Vertex AI Search out-of-the-box capabilities by leveraging the [Vertex AI Agent Builder Python API reference documentation](https://cloud.google.com/python/docs/reference/discoveryengine/latest)\n", + "\n", + "2. Leverage Vertex AI Search Datastore as a [Grounding Source](https://cloud.google.com/vertex-ai/generative-ai/docs/model-reference/grounding) with Gemini Models directly to ground responses in your data\n", + "\n", + "3. Leverage Vertex DataStore as [LangChain Retriever](https://python.langchain.com/v0.2/docs/integrations/retrievers/google_vertex_ai_search/)\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "61RBz8LLbxCR" + }, + "source": [ + "## Get started" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "No17Cw5hgx12" + }, + "source": [ + "### Install Vertex AI SDK and other required packages\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "tFy3H3aPgx12" + }, + "outputs": [], + "source": [ + "%pip install --upgrade --user --quiet google-cloud-aiplatform\n", + "%pip install google-cloud-discoveryengine\n", + "%pip install langchain_google_community\n", + "%pip install langchain langchain-google-vertexai\n", + "%pip install langchain-google-community[vertexaisearch]" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "R5Xep4W9lq-Z" + }, + "source": [ + "### Restart runtime\n", + "\n", + "To use the newly installed packages in this Jupyter runtime, you must restart the runtime. You can do this by running the cell below, which restarts the current kernel.\n", + "\n", + "The restart might take a minute or longer. After it has restarted, continue to the next step." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "XRvKdaPDTznN" + }, + "outputs": [], + "source": [ + "import IPython\n", + "\n", + "app = IPython.Application.instance()\n", + "app.kernel.do_shutdown(True)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "SbmM4z7FOBpM" + }, + "source": [ + "
    \n", + "⚠ The kernel is going to restart. Wait until it is finished before continuing to the next step. ⚠\n", + "
    \n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "dmWOrTJ3gx13" + }, + "source": [ + "### Authenticate your notebook environment (Colab only)\n", + "\n", + "If you're running this notebook on Google Colab, run the cell below to authenticate your environment." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "NyKGtVQjgx13" + }, + "outputs": [], + "source": [ + "import sys\n", + "\n", + "if \"google.colab\" in sys.modules:\n", + " from google.colab import auth\n", + "\n", + " auth.authenticate_user()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "DF4l8DTdWgPY" + }, + "source": [ + "### Set Google Cloud project information and initialize Vertex AI SDK\n", + "\n", + "To get started using Vertex AI, you must have an existing Google Cloud project and [enable the Vertex AI API](https://console.cloud.google.com/flows/enableapi?apiid=aiplatform.googleapis.com).\n", + "\n", + "Learn more about [setting up a project and a development environment](https://cloud.google.com/vertex-ai/docs/start/cloud-environment)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "Nqwi-5ufWp_B" + }, + "outputs": [], + "source": [ + "PROJECT_ID = \"your_project_id\" # @param {type:\"string\"}\n", + "LOCATION = \"us-central1\" # @param {type:\"string\"}\n", + "\n", + "\n", + "import vertexai\n", + "\n", + "vertexai.init(project=PROJECT_ID, location=LOCATION)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "EdvJRUWRNGHE" + }, + "source": [ + "### Create a Vertex AI Search App On Google Cloud\n", + "[Follow These Steps](https://cloud.google.com/generative-ai-app-builder/docs/try-enterprise-search#unstructured-data_1) to leverage Vertex AI Agent Builder to create a Search App with unstructured data store on Google Cloud Console.\n", + "\n", + "Once the search app is created successfully, make a Note of the Search App Id, Datastore ID, location from Vertex AI Agent Builder console." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "CaXr_Fz9OXkQ" + }, + "outputs": [], + "source": [ + "# Set to your data store location\n", + "VERTEX_AI_SEARCH_LOCATION = \"global\" # @param {type:\"string\"}\n", + "# Set to your search app ID\n", + "VERTEX_AI_SEARCH_APP_ID = \"your_search_app_id\" # @param {type:\"string\"}\n", + "# Set to your data store ID\n", + "VERTEX_AI_SEARCH_DATASTORE_ID = \"your_datastore_id\" # @param {type:\"string\"}\n", + "\n", + "MODEL = \"gemini-2.0-flash\" # @param {type:\"string\"}\n", + "\n", + "SEARCH_QUERY = \"When does Alphabet plan to get to net zero?\" # @param {type:\"string\"}" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "5303c05f7aa6" + }, + "source": [ + "### Import libraries" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "6fc324893334" + }, + "outputs": [], + "source": [ + "from google.api_core.client_options import ClientOptions\n", + "from google.cloud import discoveryengine_v1 as discoveryengine\n", + "from langchain.chains import RetrievalQAWithSourcesChain\n", + "from langchain_google_community import VertexAISearchRetriever\n", + "from langchain_google_vertexai import VertexAI\n", + "from vertexai.generative_models import (\n", + " GenerationConfig,\n", + " GenerativeModel,\n", + " SafetySetting,\n", + " Tool,\n", + ")\n", + "import vertexai.preview.generative_models as generative_models" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "QhOI5q1LIzmU" + }, + "source": [ + "## Part 1: Vertex AI Search Out-of-the-box capabilities\n", + "Leverage the [Vertex AI Agent Builder Python API reference documentation](https://cloud.google.com/python/docs/reference/discoveryengine/latest) to get the search results" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "Chkv32AgF9Sx" + }, + "outputs": [], + "source": [ + "CUSTOM_PROMPT = \"\"\"\n", + " \n", + " You are a helpful assistant knowledgeable about Alphabet quarterly earning reports.\n", + " Be mindful of the time frame user inputs\n", + " Help user with their queries related with Alphabet\n", + " Only respond with relevant information available in Grounding Knowledge snippet.\n", + " If no relevant snippet is available, respond with you dont know\n", + " Do not make up information\n", + " \n", + "\"\"\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "0eOlSO08IK2Z" + }, + "outputs": [], + "source": [ + "def search_spec():\n", + " content_search_spec = discoveryengine.SearchRequest.ContentSearchSpec(\n", + " snippet_spec=discoveryengine.SearchRequest.ContentSearchSpec.SnippetSpec(\n", + " return_snippet=True\n", + " ),\n", + " summary_spec=discoveryengine.SearchRequest.ContentSearchSpec.SummarySpec(\n", + " summary_result_count=10,\n", + " include_citations=True,\n", + " ignore_adversarial_query=True,\n", + " ignore_non_summary_seeking_query=True,\n", + " model_prompt_spec=discoveryengine.SearchRequest.ContentSearchSpec.SummarySpec.ModelPromptSpec(\n", + " preamble=CUSTOM_PROMPT\n", + " ),\n", + " model_spec=discoveryengine.SearchRequest.ContentSearchSpec.SummarySpec.ModelSpec(\n", + " version=\"stable\",\n", + " ),\n", + " ),\n", + " )\n", + " return content_search_spec\n", + "\n", + "\n", + "def search_sample(project_id: str, location: str, engine_id: str, search_query: str):\n", + " client_options = (\n", + " ClientOptions(api_endpoint=f\"{location}-discoveryengine.googleapis.com\")\n", + " if location != \"global\"\n", + " else None\n", + " )\n", + "\n", + " client = discoveryengine.SearchServiceClient(client_options=client_options)\n", + "\n", + " serving_config = f\"projects/{project_id}/locations/{location}/collections/default_collection/engines/{engine_id}/servingConfigs/default_config\"\n", + "\n", + " content_search_spec = search_spec()\n", + "\n", + " request = discoveryengine.SearchRequest(\n", + " serving_config=serving_config,\n", + " query=search_query,\n", + " page_size=10,\n", + " content_search_spec=content_search_spec,\n", + " query_expansion_spec=discoveryengine.SearchRequest.QueryExpansionSpec(\n", + " condition=discoveryengine.SearchRequest.QueryExpansionSpec.Condition.AUTO,\n", + " ),\n", + " spell_correction_spec=discoveryengine.SearchRequest.SpellCorrectionSpec(\n", + " mode=discoveryengine.SearchRequest.SpellCorrectionSpec.Mode.AUTO\n", + " ),\n", + " )\n", + " response = client.search(request)\n", + "\n", + " return response" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "IV52oaTJIK-i" + }, + "outputs": [], + "source": [ + "search_response = search_sample(\n", + " PROJECT_ID, VERTEX_AI_SEARCH_LOCATION, VERTEX_AI_SEARCH_APP_ID, SEARCH_QUERY\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "JsbqEIgyILBJ" + }, + "outputs": [], + "source": [ + "search_response.summary.summary_with_metadata" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "S_OnZDEfJCzi" + }, + "source": [ + "## Part 2: Grounding with Gemini on Vertex AI Search Datastore\n", + "\n", + "Leverage Vertex AI Search Datastore as a [Grounding Source](https://cloud.google.com/vertex-ai/generative-ai/docs/model-reference/grounding) with Gemini Models directly to ground responses in your data." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "uGV_iw5YJMNK" + }, + "outputs": [], + "source": [ + "system_instructions = \"\"\"\n", + "\n", + " You are a helpful assistant knowledgeable about Alphabet quarterly earning reports.\n", + " Help user with their queries related with Alphabet by following given and \n", + " only respond with information available in Grounding Knowledge store.\n", + "\n", + "\n", + "\n", + "- Always refer to the tool and Ground your answers in it\n", + "- Understand the retrieved snippet by the tool and only use that information to help users\n", + "- For supporting references, you can provide the Grounding tool snippets verbatim, and any other info like page number\n", + "- For Information not available in the tool, mention you dont have access to the information.\n", + "- Output \"answer\" should be I dont know when the user question is irrelevant or outside the \n", + "- Leave \"reference_snippet\" as null if you are not sure about the page and text snippet\n", + "\n", + "\n", + "\n", + " Grounding tool finds most relevant snippets from the Alphabet earning reports data store.\n", + " Use the information provided by the tool as your knowledge base.\n", + "\n", + "\n", + "\n", + "- ONLY use information available from the Grounding tool\n", + "\n", + "\n", + "\n", + "- Response should ALWAYS be in following JSON Output with answer and reference_snippet as keys, e.g. {\"answer\": , \"reference_snippet\": }\n", + "\n", + "\"\"\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "wSo25gr9MBSd" + }, + "outputs": [], + "source": [ + "tools = [\n", + " Tool.from_retrieval(\n", + " retrieval=generative_models.grounding.Retrieval(\n", + " source=generative_models.grounding.VertexAISearch(\n", + " datastore=VERTEX_AI_SEARCH_DATASTORE_ID,\n", + " project=PROJECT_ID,\n", + " location=VERTEX_AI_SEARCH_LOCATION,\n", + " ),\n", + " disable_attribution=False,\n", + " )\n", + " ),\n", + "]\n", + "\n", + "generation_config = GenerationConfig(max_output_tokens=8192, temperature=1, top_p=0.95)\n", + "\n", + "safety_settings = [\n", + " SafetySetting(\n", + " category=SafetySetting.HarmCategory.HARM_CATEGORY_HATE_SPEECH,\n", + " threshold=SafetySetting.HarmBlockThreshold.BLOCK_ONLY_HIGH,\n", + " ),\n", + " SafetySetting(\n", + " category=SafetySetting.HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT,\n", + " threshold=SafetySetting.HarmBlockThreshold.BLOCK_ONLY_HIGH,\n", + " ),\n", + " SafetySetting(\n", + " category=SafetySetting.HarmCategory.HARM_CATEGORY_SEXUALLY_EXPLICIT,\n", + " threshold=SafetySetting.HarmBlockThreshold.BLOCK_ONLY_HIGH,\n", + " ),\n", + " SafetySetting(\n", + " category=SafetySetting.HarmCategory.HARM_CATEGORY_HARASSMENT,\n", + " threshold=SafetySetting.HarmBlockThreshold.BLOCK_ONLY_HIGH,\n", + " ),\n", + "]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "jCJAOvHdLrGI" + }, + "outputs": [], + "source": [ + "model = GenerativeModel(MODEL, tools=tools, system_instruction=[system_instructions])\n", + "\n", + "response = model.generate_content(\n", + " [SEARCH_QUERY],\n", + " generation_config=generation_config,\n", + " safety_settings=safety_settings,\n", + " stream=True,\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "BNrZ-w3gRoLx" + }, + "outputs": [], + "source": [ + "for candidate in response:\n", + " print(candidate)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "lQv61qA5Q71g" + }, + "source": [ + "##### [Here](https://github.com/GoogleCloudPlatform/generative-ai/blob/main/gemini/grounding/intro-grounding-gemini.ipynb) is another Grounding Gemini with Vertex AI Search Example which you might find useful" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "V2X81IQUOImp" + }, + "source": [ + "## Part 3: Vertex AI Search with LangChain\n", + "\n", + "Developers have the flexibility to incorporate Vertex AI Search as a [LangChain Retriever](https://python.langchain.com/v0.2/docs/integrations/retrievers/google_vertex_ai_search/) in their existing LangChain applications.\n", + "\n", + "This means you can continue leveraging your preferred orchestrator while seamlessly integrating Vertex AI Search data stores into existing RAG pipelines. Vertex AI Search enables Google-quality search capabilities applied directly to your custom data, elevating the search result quality and relevance of your retrieval-augmented generation workflows.\n", + "\n", + "Find more notebook examples of leveraging VertexAISearchRetriever with LangChain [here](https://github.com/GoogleCloudPlatform/generative-ai/blob/main/search/retrieval-augmented-generation/examples/question_answering.ipynb).\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "Kt09YTDmJMYW" + }, + "outputs": [], + "source": [ + "llm = VertexAI(model_name=MODEL)\n", + "\n", + "retriever = VertexAISearchRetriever(\n", + " project_id=PROJECT_ID,\n", + " location_id=VERTEX_AI_SEARCH_LOCATION,\n", + " data_store_id=VERTEX_AI_SEARCH_DATASTORE_ID,\n", + " get_extractive_answers=True,\n", + " max_documents=10,\n", + " max_extractive_segment_count=1,\n", + " max_extractive_answer_count=5,\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "Dkt_l9tmPeYH" + }, + "outputs": [], + "source": [ + "retrieval_qa_with_sources = RetrievalQAWithSourcesChain.from_chain_type(\n", + " llm=llm, chain_type=\"stuff\", retriever=retriever\n", + ")\n", + "\n", + "retrieval_qa_with_sources.invoke(SEARCH_QUERY, return_only_outputs=True)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "2a4e033321ad" + }, + "source": [ + "## Cleaning up\n", + "\n", + "[Delete the Vertex AI Search App](https://cloud.google.com/generative-ai-app-builder/docs/delete-engine) and\n", + "\n", + "[Delete the datastore](https://cloud.google.com/generative-ai-app-builder/docs/delete-a-data-store) your created" + ] + } + ], + "metadata": { + "colab": { + "name": "vertexai_search_options.ipynb", + "toc_visible": true + }, + "kernelspec": { + "display_name": "Python 3", + "name": "python3" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} diff --git a/search/web-app/.dockerignore b/search/web-app/.dockerignore new file mode 100644 index 0000000..8267703 --- /dev/null +++ b/search/web-app/.dockerignore @@ -0,0 +1,10 @@ +Dockerfile +LICENSE +README.md +*.pyc +*.pyo +*.pyd +__pycache__ +.pytest_cache +img/ +.prettierrc diff --git a/search/web-app/.prettierrc b/search/web-app/.prettierrc new file mode 100644 index 0000000..519bf56 --- /dev/null +++ b/search/web-app/.prettierrc @@ -0,0 +1,8 @@ +{ + "tabWidth": 2, + "useTabs": false, + "printWidth": 120, + "bracketSameLine": true, + "pluginSearchDirs": ["."], + "overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }] +} diff --git a/search/web-app/Dockerfile b/search/web-app/Dockerfile new file mode 100644 index 0000000..7bafa2d --- /dev/null +++ b/search/web-app/Dockerfile @@ -0,0 +1,22 @@ +# Use the official lightweight Python image. +# https://hub.docker.com/_/python +FROM python:3.13-slim + +# Allow statements and log messages to immediately appear in the Knative logs +ENV PYTHONUNBUFFERED True + +# Copy local code to the container image. +ENV APP_HOME /app +WORKDIR $APP_HOME +COPY . ./ + +# Install production dependencies. +RUN pip install --no-cache-dir -r requirements.txt + +# Run the web service on container startup. Here we use the gunicorn +# webserver, with one worker process and 8 threads. +# For environments with multiple CPU cores, increase the number of workers +# to be equal to the cores available. +# Timeout is set to 0 to disable the timeouts of the workers to allow Cloud Run to handle instance scaling. +# hadolint ignore=DL3025 +CMD exec gunicorn --bind :$PORT --workers 1 --threads 8 --timeout 0 main:app diff --git a/search/web-app/README.md b/search/web-app/README.md new file mode 100644 index 0000000..339195c --- /dev/null +++ b/search/web-app/README.md @@ -0,0 +1,117 @@ +# Vertex AI Search Web App Demo + +> NOTE: Some of the features in this demo require allowlist access. If you would like early access, apply to become a [Trusted Tester for Google Cloud Generative AI][trustedtester]. + +This demo illustrates how to search through a corpus of documents using [Vertex AI Search][enterprisesearch] (formerly known as Enterprise Search). + +Additional features include how to search the public Cloud Knowledge Graph using the [Enterprise Knowledge Graph][enterpriseknowledgegraph] API. + +## Video Walkthrough + +[![VAIS Web App Walkthrough](https://storage.googleapis.com/github-repo/search/web-app/vais_web_app_walkthrough.png)](https://storage.googleapis.com/github-repo/search/web-app/vais_web_app_walkthrough.mp4) + +## Architecture + +### Google Cloud Products Used + +- [Vertex AI Search][enterprisesearch] +- [Vertex AI Search: Recommendations][try_recommendations] +- [Cloud Run][cloudrun] +- [Enterprise Knowledge Graph][enterpriseknowledgegraph] + +## Setup + +- Follow steps in [Get started with Vertex AI Search][try_search] for Unstructured Data. + + - Sample Data Sources used in the deployed demo: + - [Contract Understanding Atticus Dataset (CUAD)](https://www.atticusprojectai.org/cuad) + - `gs://cloud-samples-data/gen-app-builder/search/CUAD_v1` + - [Alphabet Earnings Reports](https://abc.xyz/investor/) + - `gs://cloud-samples-data/gen-app-builder/search/alphabet-investor-pdfs` + +- Follow steps in [Get started with Vertex AI Search][try_search] for Websites + + - [Google Cloud site](https://cloud.google.com) + - `https://cloud.google.com` + +- Follow steps in [Get started with Recommendations][try_recommendations] for Unstructured Data. + + - Sample Data Sources used in the deployed demo: + - [Natural language papers from arXiv](https://arxiv.org) + - `gs://cloud-samples-data/gen-app-builder/search/arxiv` + +### Dependencies + +1. [Install Python](https://www.python.org/downloads/) +2. Install the [Google Cloud SDK](https://cloud.google.com/sdk/docs/install) +3. Install the prerequisites: + - `pip install -r requirements.txt` +4. Run `gcloud init`, create a new project, and + [enable billing](https://cloud.google.com/billing/docs/how-to/modify-project#enable_billing_for_a_project) +5. Enable the Vertex AI Search API: + - `gcloud services enable discoveryengine.googleapis.com` +6. Enable the Enterprise Knowledge Graph API: + - `gcloud services enable enterpriseknowledgegraph.googleapis.com` +7. Enable Cloud Run: + - `gcloud services enable run.googleapis.com` +8. Setup application default authentication, run: + - `gcloud auth application-default login` +9. Give the Cloud Run service account required permissions: + + ```sh + gcloud projects add-iam-policy-binding [PROJECT_ID or PROJECT_NUMBER] \ + --member='serviceAccount:[PROJECT_NUMBER]-compute@developer.gserviceaccount.com' \ + --role='roles/discoveryengine.viewer' + ``` + +10. (Optional) If your Google Cloud organization has polices to [restrict sharing by domain](https://cloud.google.com/resource-manager/docs/organization-policy/restricting-domains), then you'll need to change this to allow all domains for the Cloud Run application to be open to the public Internet. + +### Demo Deployment + +1. Update the `consts.py` file with your own `PROJECT_ID` and `LOCATION`. + +2. Configure Vertex AI Search + + - To use the [prebuilt widget](https://cloud.google.com/generative-ai-app-builder/docs/add-widget), copy the `configId` from the `` in the `Integration > Widget` tab in the [Cloud Console](https://console.cloud.google.com/gen-app-builder). + - ![configId](img/configId.png) + - Be sure to set authorization type as `Public Access` and add your web application url to the `Allowed Domains` once it's deployed. + - Add the `configId` for your Search Engines to `WIDGET_CONFIGS` in `consts.py` + - To use the Custom UI, add the engine id for your search engine to `CUSTOM_UI_ENGINE_IDS` in `consts.py` + - This is the string after `/engines/` in the Cloud Console URL. + - `https://console.cloud.google.com/gen-app-builder/engines/website-search-engine_1681248733152/...` + - Engine ID is `website-search-engine_1681248733152` + +3. Configure Recommendations + + - Add the datastore id and engine id for your recommendations engine to `RECOMMENDATIONS_DATASTORE_IDs` in `consts.py`. + - The datastore id is visible on the `Data > Details` page. + - The engine id is the string after `/engines/` in the Cloud Console URL. + - `https://console.cloud.google.com/gen-app-builder/engines/contracts-personalize_1687884886933/data/records` + - Engine ID is `contracts-personalize_1687884886933` + +4. Configure Image Search + + - Follow the instructions in the documentation to [enable image search](https://cloud.google.com/generative-ai-app-builder/docs/image-search#enable-advanced) for a website search engine. + - NOTE: You must enable [Advanced Website Indexing](https://cloud.google.com/generative-ai-app-builder/docs/about-advanced-features#advanced-website-indexing) which requires [domain verification](https://cloud.google.com/generative-ai-app-builder/docs/domain-verification). + - Add the engine id for your search engine to `IMAGE_SEARCH_DATASTORE_IDs` in `consts.py`. + +5. Deploy the Cloud Run app in your project. + + - `gcloud run deploy vertex-ai-search-demo --source .` + + - To test locally: `flask --app main run` + +6. Visit the deployed web page + - Example: [`https://vertex-ai-search-demo-lnppzg3rxa-uc.a.run.app`](https://vertex-ai-search.web.app/) + +--- + +> Copyright 2023 Google LLC +> Author: Holt Skinner @holtskinner + +[cloudrun]: https://cloud.google.com/run +[enterpriseknowledgegraph]: https://cloud.google.com/enterprise-knowledge-graph/docs/overview +[enterprisesearch]: https://cloud.google.com/enterprise-search +[try_recommendations]: https://cloud.google.com/generative-ai-app-builder/docs/try-personalize +[try_search]: https://cloud.google.com/generative-ai-app-builder/docs/try-enterprise-search +[trustedtester]: https://cloud.google.com/ai/earlyaccess/join diff --git a/search/web-app/consts.py b/search/web-app/consts.py new file mode 100644 index 0000000..65618f4 --- /dev/null +++ b/search/web-app/consts.py @@ -0,0 +1,126 @@ +# Copyright 2022 Google LLC +# +# 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 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Vertex AI Search Demo Constant Definitions""" + +PROJECT_ID = "YOUR_PROJECT_ID" +LOCATION = "global" + +WIDGET_CONFIGS = [ + { + "name": "Google Cloud Website", + "config_id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", + "placeholder": "Vertex AI", + }, + { + "name": "Contracts", + "config_id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", + "placeholder": "What is the SLA?", + }, + { + "name": "Alphabet Earnings Reports", + "config_id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", + "placeholder": "What was Google's revenue in 2021?", + }, + { + "name": "Kaggle Movies", + "config_id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", + "placeholder": "Harry Potter", + }, +] + +CUSTOM_UI_ENGINE_IDS = [ + { + "name": "Google Cloud Website", + "engine_id": "google-cloud-site-search", + }, + { + "name": "Google Merchandise Store (Advanced Indexing)", + "engine_id": "google-merch-store", + }, + { + "name": "Cymbal Bank", + "engine_id": "cymbal-bank-onboarding", + }, +] + + +IMAGE_SEARCH_ENGINE_IDs = [ + { + "name": "Google Merchandise Store", + "engine_id": "google-merch-store", + } +] + +RECOMMENDATIONS_DATASTORE_IDs = [ + { + "name": "arXiv Natural Language Papers", + "datastore_id": "arxiv", + "engine_id": "arxiv-personalize", + } +] + +# iso639-1 code +# First Index will be default selection +VALID_LANGUAGES = [ + {"code": "en", "name": "English"}, + {"code": "sq", "name": "Albanian"}, + {"code": "ar", "name": "Arabic"}, + {"code": "hy", "name": "Armenian"}, + {"code": "eu", "name": "Basque"}, + {"code": "bn", "name": "Bengali"}, + {"code": "bg", "name": "Bulgarian"}, + {"code": "ca", "name": "Catalan"}, + {"code": "km", "name": "Central Khmer"}, + {"code": "zh", "name": "Chinese"}, + {"code": "hr", "name": "Croatian"}, + {"code": "cs", "name": "Czech"}, + {"code": "da", "name": "Danish"}, + {"code": "nl", "name": "Dutch"}, + {"code": "fil", "name": "Filipino"}, + {"code": "fi", "name": "Finnish"}, + {"code": "fr", "name": "French"}, + {"code": "gl", "name": "Galician"}, + {"code": "de", "name": "German"}, + {"code": "iw", "name": "Hebrew"}, + {"code": "hi", "name": "Hindi"}, + {"code": "hu", "name": "Hungarian"}, + {"code": "is", "name": "Icelandic"}, + {"code": "id", "name": "Indonesian"}, + {"code": "ga", "name": "Irish"}, + {"code": "it", "name": "Italian"}, + {"code": "ja", "name": "Japanese"}, + {"code": "ko", "name": "Korean"}, + {"code": "lo", "name": "Lao"}, + {"code": "lv", "name": "Latvian"}, + {"code": "lt", "name": "Lithuanian"}, + {"code": "el", "name": "Modern Greek"}, + {"code": "no", "name": "Norwegian"}, + {"code": "fa", "name": "Persian"}, + {"code": "pl", "name": "Polish"}, + {"code": "pt", "name": "Portuguese"}, + {"code": "ro", "name": "Romanian"}, + {"code": "ru", "name": "Russian"}, + {"code": "sr", "name": "Serbian"}, + {"code": "sk", "name": "Slovak"}, + {"code": "sl", "name": "Slovenian"}, + {"code": "es", "name": "Spanish"}, + {"code": "sv", "name": "Swedish"}, + {"code": "th", "name": "Thai"}, + {"code": "tr", "name": "Turkish"}, + {"code": "uk", "name": "Ukrainian"}, + {"code": "vi", "name": "Vietnamese"}, +] + +SUMMARY_MODELS = ["stable", "preview"] diff --git a/search/web-app/ekg_utils.py b/search/web-app/ekg_utils.py new file mode 100644 index 0000000..374500c --- /dev/null +++ b/search/web-app/ekg_utils.py @@ -0,0 +1,75 @@ +# Copyright 2022 Google LLC +# +# 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 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Enterprise Knowledge Graph Utilities""" +from collections.abc import Sequence +import json + +from google.cloud import enterpriseknowledgegraph as ekg + +JSON_INDENT = 2 + + +# pylint: disable=too-many-arguments +def search_public_kg( + project_id: str, + location: str, + search_query: str, + languages: Sequence[str] | None = None, + types: Sequence[str] | None = None, + limit: int | None = None, +) -> tuple: + """ + Make API Request to Public Knowledge Graph. + """ + client = ekg.EnterpriseKnowledgeGraphServiceClient() + + # Fully qualified location string, e.g. projects/{project_id}/locations/{location} + parent = client.common_location_path(project=project_id, location=location) + + request = ekg.SearchPublicKgRequest( + parent=parent, query=search_query, languages=languages, types=types, limit=limit + ) + + response = client.search_public_kg(request=request) + + request_url = f"https://enterpriseknowledgegraph.googleapis.com/v1/{parent}/publicKnowledgeGraphEntities:Search?query={search_query}" # noqa: E501 + + request_json = ekg.SearchPublicKgRequest.to_json( + request, including_default_value_fields=False, indent=JSON_INDENT + ) + + response_json = ekg.SearchPublicKgResponse.to_json( + response, including_default_value_fields=False, indent=JSON_INDENT + ) + + entities = get_entities(response) + return entities, request_url, request_json, response_json + + +def get_entities(response: ekg.SearchPublicKgResponse) -> list: + """ + Extract Entities from Knowledge Graph Response + """ + item_list_element = ekg.SearchPublicKgResponse.to_dict(response)[ + "item_list_element" + ] + + entities = [] + for element in item_list_element: + result = element["result"] + result["resultJson"] = json.dumps(result, sort_keys=True, indent=JSON_INDENT) + entities.append(result) + + return entities diff --git a/search/web-app/img/configId.png b/search/web-app/img/configId.png new file mode 100644 index 0000000..2257f73 Binary files /dev/null and b/search/web-app/img/configId.png differ diff --git a/search/web-app/main.py b/search/web-app/main.py new file mode 100644 index 0000000..fc88ac7 --- /dev/null +++ b/search/web-app/main.py @@ -0,0 +1,401 @@ +# Copyright 2022 Google LLC +# +# 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 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Flask Web Server""" + +import base64 +import os +import re +from urllib.parse import urlparse + +from consts import ( + CUSTOM_UI_ENGINE_IDS, + LOCATION, + PROJECT_ID, + SUMMARY_MODELS, + VALID_LANGUAGES, + WIDGET_CONFIGS, + IMAGE_SEARCH_ENGINE_IDs, + RECOMMENDATIONS_DATASTORE_IDs, +) +from ekg_utils import search_public_kg +from flask import Flask, render_template, request +from google.api_core.exceptions import ResourceExhausted +import requests +from vais_utils import list_documents, recommend_personalize, search_enterprise_search +from werkzeug.exceptions import HTTPException + +app = Flask(__name__) + +app.config["MAX_CONTENT_LENGTH"] = 16 * 1024 * 1024 # Set maximum upload size to 16MB + +FORM_OPTIONS = { + "language_list": VALID_LANGUAGES, + "default_language": VALID_LANGUAGES[0], +} + +CUSTOM_UI_SEARCH_ENGINES = [d["name"] for d in CUSTOM_UI_ENGINE_IDS] + +NAV_LINKS = [ + {"link": "/", "name": "Widgets", "icon": "widgets"}, + { + "link": "/search", + "name": "Custom UI", + "icon": "build", + }, + { + "link": "/image-search", + "name": "Image Search", + "icon": "image", + }, + { + "link": "/recommend", + "name": "Recommendations", + "icon": "recommend", + }, + {"link": "/ekg", "name": "Enterprise Knowledge Graph", "icon": "scatter_plot"}, + { + "link": "https://github.com/GoogleCloudPlatform/generative-ai/blob/main/search/retrieval-augmented-generation/examples/question_answering.ipynb", # noqa: E501 + "name": "đŸŠœïžđŸ”— Retrieval Augmented Generation (RAG)", + }, + { + "link": "https://github.com/GoogleCloudPlatform/generative-ai/tree/main/search/web-app", + "name": "Source Code", + "icon": "code", + }, +] + +RECOMMENDATIONS_DOCUMENTS = list_documents( + project_id=PROJECT_ID, + location=LOCATION, + datastore_id=RECOMMENDATIONS_DATASTORE_IDs[0]["datastore_id"], +) + +VALID_IMAGE_MIMETYPES = {"image/jpeg", "image/png", "image/bmp"} + + +@app.route("/", methods=["GET"]) +@app.route("/finance", methods=["GET"]) +def index() -> str: + """ + Web Server, Homepage for Widgets + """ + + return render_template( + "index.html", + title=NAV_LINKS[0]["name"], + nav_links=NAV_LINKS, + search_engine_options=WIDGET_CONFIGS, + ) + + +@app.route("/search", methods=["GET"]) +def search() -> str: + """ + Web Server, Homepage for Search - Custom UI + """ + + return render_template( + "search.html", + title=NAV_LINKS[1]["name"], + nav_links=NAV_LINKS, + search_engines=CUSTOM_UI_SEARCH_ENGINES, + summary_models=SUMMARY_MODELS, + ) + + +@app.route("/search_vais", methods=["POST"]) +def search_vais() -> str: + """ + Handle Search Vertex AI Search Request + """ + search_query = request.form.get("search_query", "") + + # Check if POST Request includes search query + if not search_query: + return render_template( + "search.html", + title=NAV_LINKS[1]["name"], + nav_links=NAV_LINKS, + search_engines=CUSTOM_UI_SEARCH_ENGINES, + summary_models=SUMMARY_MODELS, + message_error="No query provided", + ) + + search_engine = request.form.get("search_engine", "") + + if not search_engine: + return render_template( + "search.html", + title=NAV_LINKS[1]["name"], + nav_links=NAV_LINKS, + search_engines=CUSTOM_UI_SEARCH_ENGINES, + summary_models=SUMMARY_MODELS, + message_error="No search engine selected", + ) + + summary_model = request.form.get("summary_model") + summary_preamble = request.form.get("summary_preamble") + + results, summary, request_url, raw_request, raw_response = search_enterprise_search( + project_id=PROJECT_ID, + location=LOCATION, + engine_id=CUSTOM_UI_ENGINE_IDS[int(search_engine)]["engine_id"], + search_query=search_query, + summary_model=summary_model, + summary_preamble=summary_preamble, + ) + + return render_template( + "search.html", + title=NAV_LINKS[1]["name"], + nav_links=NAV_LINKS, + search_engines=CUSTOM_UI_SEARCH_ENGINES, + summary_models=SUMMARY_MODELS, + message_success=search_query, + results=results, + summary=summary, + request_url=request_url, + raw_request=raw_request, + raw_response=raw_response, + ) + + +@app.route("/image-search", methods=["GET"]) +def image_search() -> str: + """ + Web Server, Homepage for Image Search - Custom UI + """ + return render_template( + "image-search.html", + title=NAV_LINKS[2]["name"], + nav_links=NAV_LINKS, + ) + + +@app.route("/imagesearch_vais", methods=["POST"]) +def imagesearch_vais() -> str: + """ + Handle Image Search Vertex AI Search Request + """ + search_query = request.form.get("search_query", "") + image_file = request.files["image"] + image_content = None + image_bytes = None + + # Check if POST Request includes search query + if not search_query and not image_file: + return render_template( + "image-search.html", + nav_links=NAV_LINKS, + message_error="No query provided", + ) + + if image_file: + image_content = image_file.read() + elif search_query: + # Check if text is a url + image_url = urlparse(search_query) + if all([image_url.scheme, image_url.netloc, image_url.path]): + image_response = requests.get( + image_url.geturl(), allow_redirects=True, timeout=5 + ) + mime_type = image_response.headers["Content-Type"] + if mime_type not in VALID_IMAGE_MIMETYPES: + return render_template( + "image-search.html", + nav_links=NAV_LINKS, + message_error=f"Invalid image format - {mime_type}. Valid types {VALID_IMAGE_MIMETYPES}", + ) + image_content = image_response.content + + if image_content: + search_query = None + image_bytes = base64.b64encode(image_content) + + try: + results, _, request_url, raw_request, raw_response = search_enterprise_search( + project_id=PROJECT_ID, + location=LOCATION, + engine_id=IMAGE_SEARCH_ENGINE_IDs[0]["engine_id"], + search_query=search_query, + image_bytes=image_bytes, + params={"search_type": 1}, + ) + except Exception as e: + return render_template( + "image-search.html", + nav_links=NAV_LINKS, + message_error=e.args[0], + ) + + return render_template( + "image-search.html", + title=NAV_LINKS[2]["name"], + nav_links=NAV_LINKS, + message_success="Success", + results=results, + request_url=request_url, + raw_request=raw_request, + raw_response=raw_response, + ) + + +@app.route("/recommend", methods=["GET"]) +def recommend() -> str: + """ + Web Server, Homepage for Recommendations - Custom UI + """ + return render_template( + "recommend.html", + nav_links=NAV_LINKS, + title=NAV_LINKS[3]["name"], + documents=RECOMMENDATIONS_DOCUMENTS, + attribution_token="", + ) + + +@app.route("/recommend_vais", methods=["POST"]) +def recommend_vais() -> str: + """ + Handle Recommend Vertex AI Search Request + """ + document_id = request.form.get("document_id", "") + attribution_token = request.form.get("attribution_token", "") + + # Check if POST Request includes document id + if not document_id: + return render_template( + "recommend.html", + title=NAV_LINKS[3]["name"], + nav_links=NAV_LINKS, + documents=RECOMMENDATIONS_DOCUMENTS, + attribution_token=attribution_token, + message_error="No document provided", + ) + + ( + results, + attribution_token, + request_url, + raw_request, + raw_response, + ) = recommend_personalize( + project_id=PROJECT_ID, + location=LOCATION, + datastore_id=RECOMMENDATIONS_DATASTORE_IDs[0]["datastore_id"], + serving_config_id=RECOMMENDATIONS_DATASTORE_IDs[0]["engine_id"], + document_id=document_id, + attribution_token=attribution_token, + ) + + return render_template( + "recommend.html", + title=NAV_LINKS[3]["name"], + nav_links=NAV_LINKS, + documents=RECOMMENDATIONS_DOCUMENTS, + message_success=document_id, + results=results, + attribution_token=attribution_token, + request_url=request_url, + raw_request=raw_request, + raw_response=raw_response, + ) + + +@app.route("/ekg", methods=["GET"]) +def ekg() -> str: + """ + Web Server, Homepage for EKG + """ + + return render_template( + "ekg.html", + title=NAV_LINKS[4]["name"], + nav_links=NAV_LINKS, + form_options=FORM_OPTIONS, + ) + + +@app.route("/search_ekg", methods=["POST"]) +def search_ekg() -> str: + """ + Handle Search EKG Request + """ + search_query = request.form.get("search_query", "") + + # Check if POST Request includes search query + if not search_query: + return render_template( + "ekg.html", + title=NAV_LINKS[4]["name"], + nav_links=NAV_LINKS, + form_options=FORM_OPTIONS, + message_error="No query provided", + ) + + languages = request.form.getlist("languages") + form_types = request.form.get("types", "") + + types = re.split(r"[\s,]", form_types) if form_types else [] + + entities, request_url, raw_request, raw_response = search_public_kg( + project_id=PROJECT_ID, + location=LOCATION, + search_query=search_query, + languages=languages, + types=types, + ) + + return render_template( + "ekg.html", + title=NAV_LINKS[4]["name"], + nav_links=NAV_LINKS, + form_options=FORM_OPTIONS, + message_success=search_query, + entities=entities, + request_url=request_url, + raw_request=raw_request, + raw_response=raw_response, + ) + + +@app.errorhandler(Exception) +def handle_exception(ex: Exception): + """ + Handle Application Exceptions + """ + message_error = "An Unknown Error Occurred" + + # Pass through HTTP errors + if isinstance(ex, HTTPException): + message_error = ex.get_description() + elif isinstance(ex, ResourceExhausted): + message_error = ex.message + else: + message_error = str(ex) + + return render_template( + "search.html", + title=NAV_LINKS[1]["name"], + form_options=FORM_OPTIONS, + nav_links=NAV_LINKS, + search_engines=CUSTOM_UI_SEARCH_ENGINES, + summary_models=SUMMARY_MODELS, + message_error=message_error, + ) + + +if __name__ == "__main__": + app.run(debug=True, host="0.0.0.0", port=int(os.environ.get("PORT", 8080))) diff --git a/search/web-app/requirements.txt b/search/web-app/requirements.txt new file mode 100644 index 0000000..cdaed8a --- /dev/null +++ b/search/web-app/requirements.txt @@ -0,0 +1,9 @@ +typing +requests + +google-api-core +google-cloud-enterpriseknowledgegraph +google-cloud-discoveryengine>=0.11.10 + +Flask +gunicorn diff --git a/search/web-app/static/blue_pack.jpeg b/search/web-app/static/blue_pack.jpeg new file mode 100644 index 0000000..bacc627 Binary files /dev/null and b/search/web-app/static/blue_pack.jpeg differ diff --git a/search/web-app/static/ekg-logo.svg b/search/web-app/static/ekg-logo.svg new file mode 100644 index 0000000..e12f690 --- /dev/null +++ b/search/web-app/static/ekg-logo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/search/web-app/static/ekg.js b/search/web-app/static/ekg.js new file mode 100644 index 0000000..4fd7d9b --- /dev/null +++ b/search/web-app/static/ekg.js @@ -0,0 +1,59 @@ +/** + * Copyright 2023 Google LLC + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +hljs.highlightAll(); +hljs.addPlugin(new CopyButtonPlugin()); + +mdc.autoInit(); + +const MDCRipple = mdc.ripple.MDCRipple; +const MDCTextField = mdc.textField.MDCTextField; +const MDCSelect = mdc.select.MDCSelect; +const MDCTabBar = mdc.tabBar.MDCTabBar; +const MDCChipSet = mdc.chips.MDCChipSet; +const MDCChip = mdc.chips.MDCChip; + +MDCRipple.attachTo(document.querySelector(".search-button")); + +const queryTextField = new MDCTextField(document.querySelector(".query-field")); +const typesTextField = new MDCTextField(document.querySelector(".mdc-text-field.types-field")); + +const select = new MDCSelect(document.querySelector(".mdc-select")); +const tabBar = new MDCTabBar(document.querySelector(".mdc-tab-bar")); + +const chipSet = new MDCChipSet(document.querySelector(".mdc-chip-set")); +const chip = new MDCChipSet(document.querySelector(".mdc-chip")); + +const jsonTabSelector = document.querySelector("#json-tab-selector"); +const entitiesTabSelector = document.querySelector("#entities-tab-selector"); + +const jsonTab = document.querySelector("#json-tab"); +const entitiesTab = document.querySelector("#entities-tab"); + +if (jsonTabSelector) { + jsonTabSelector.onclick = () => { + jsonTab.classList.replace("tab-hidden", "tab-visible"); + entitiesTab.classList.replace("tab-visible", "tab-hidden"); + entitiesTab.replaceWith(jsonTab); + }; +} + +if (entitiesTabSelector) { + entitiesTabSelector.onclick = () => { + entitiesTab.classList.replace("tab-hidden", "tab-visible"); + jsonTab.classList.replace("tab-visible", "tab-hidden"); + jsonTab.replaceWith(entitiesTab); + }; +} diff --git a/search/web-app/static/favicon.ico b/search/web-app/static/favicon.ico new file mode 100644 index 0000000..7f2a57c Binary files /dev/null and b/search/web-app/static/favicon.ico differ diff --git a/search/web-app/static/index.css b/search/web-app/static/index.css new file mode 100644 index 0000000..0aedd91 --- /dev/null +++ b/search/web-app/static/index.css @@ -0,0 +1,238 @@ +/** + * Copyright 2023 Google LLC + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +html, +body { + height: 100%; +} + +body { + font-family: "Google Sans", sans-serif; + margin: 0; + padding-top: 0.1px; +} + +.header, +.footer { + text-align: center; +} + +.product-logo, +.ekg-logo { + height: 3rem; + padding-top: 1rem; + fill: currentcolor; +} + +.query-field, +.summary-preamble-field, +.schema-info, +.types-field, +.search-engine-name, +.file-upload { + display: block; + width: 350px; +} + +.schema-info, +.types-field, +.file-upload { + margin: 20px auto; +} + +.query-field, +.summary-preamble-field { + margin: 1rem auto 2rem; +} + +.search-engine-name { + margin: 1.5rem auto 1rem; +} + +.button-container { + display: flex; + justify-content: flex-end; + width: 300px; + margin: auto; +} + +.button-container button { + margin: 3px; +} + +.entity-checkboxes { + display: flex; + width: 300px; + margin: auto; +} + +.language-selection, +.document-selection { + display: flex; + width: 350px; + margin: auto; +} + +footer { + margin: auto; +} + +.tab-content { + margin: auto; +} + +.tab-hidden { + visibility: hidden; +} + +.tab-visible { + visibility: visible; +} + +.error-message { + width: 500px; + margin: auto; +} + +pre code { + max-height: 30rem; + overflow-y: scroll; + white-space: pre-wrap; +} + +.entity-primary-action { + display: flex; + flex-direction: row; +} + +.entity-image { + width: 25%; + height: auto; + background-size: contain; +} + +.entity-primary-action .entity-card-primary { + padding: 1rem; + width: 60%; +} + +.entity-detailed-description { + padding: 1rem; +} + +/* stylelint-disable-next-line selector-id-pattern */ +#searchWidgetTrigger { + margin: auto; + display: block; +} + +#main-nav-bar { + width: 100%; + text-align: center; + margin: auto; + display: block; +} + +a:link { + text-decoration: none; +} + +a:visited { + text-decoration: none; +} + +a:hover { + text-decoration: none; +} + +a:active { + text-decoration: none; +} + +.document-selection { + width: 70rem; +} + +.document-list-item { + padding-top: 5px; + padding-bottom: 5px; +} + +.empty-results-message { + text-align: center; +} + +.footer { + padding-bottom: 1rem; +} + +.image-result .entity-image { + width: 100%; +} + +.image-result .entity-card-primary { + width: 30%; +} + +.image-result .entity-detailed-description { + width: 80%; +} + +#image-input { + display: none; +} + +.file-upload-label { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 8px 16px; + border: 1px solid #ccc; + border-radius: 4px; + cursor: pointer; + font-size: 16px; +} + +.file-upload-icon { + margin-right: 8px; +} + +.file-upload-input { + display: none; +} + +.sample-image-text { + align-items: center; + justify-content: center; + text-align: center; +} + +.sample-images { + display: inline-flex; + align-items: center; + justify-content: center; +} + +.search-form .search-engine-radio, +.search-form .summary-model-radio { + text-align: center; + margin: 1rem auto; + height: 8rem; +} + +.search-form .mdc-radio { + margin-right: 4rem; + vertical-align: top; +} diff --git a/search/web-app/static/index.js b/search/web-app/static/index.js new file mode 100644 index 0000000..961418b --- /dev/null +++ b/search/web-app/static/index.js @@ -0,0 +1,22 @@ +/** + * Copyright 2023 Google LLC + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +mdc.autoInit(); + +const MDCTabBar = mdc.tabBar.MDCTabBar; +const MDCTextField = mdc.textField.MDCTextField; + +const tabBar = new MDCTabBar(document.querySelector(".mdc-tab-bar")); +const queryTextField = new MDCTextField(document.querySelector(".mdc-text-field.query-field")); diff --git a/search/web-app/static/lego_cloud.png b/search/web-app/static/lego_cloud.png new file mode 100644 index 0000000..acc98bb Binary files /dev/null and b/search/web-app/static/lego_cloud.png differ diff --git a/search/web-app/static/recommend.js b/search/web-app/static/recommend.js new file mode 100644 index 0000000..a72318a --- /dev/null +++ b/search/web-app/static/recommend.js @@ -0,0 +1,51 @@ +/** + * Copyright 2023 Google LLC + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +hljs.highlightAll(); +hljs.addPlugin(new CopyButtonPlugin()); + +mdc.autoInit(); + +const MDCRipple = mdc.ripple.MDCRipple; +const MDCTextField = mdc.textField.MDCTextField; +const MDCSelect = mdc.select.MDCSelect; +const MDCTabBar = mdc.tabBar.MDCTabBar; + +MDCRipple.attachTo(document.querySelector(".search-button")); + +const select = new MDCSelect(document.querySelector(".mdc-select")); + +const jsonTabSelector = document.querySelector("#json-tab-selector"); +const entitiesTabSelector = document.querySelector("#entities-tab-selector"); + +const tabContent = document.querySelector(".tab-content"); +const jsonTab = document.querySelector("#json-tab"); +const entitiesTab = document.querySelector("#entities-tab"); + +if (jsonTabSelector) { + jsonTabSelector.onclick = () => { + jsonTab.classList.replace("tab-hidden", "tab-visible"); + entitiesTab.classList.replace("tab-visible", "tab-hidden"); + entitiesTab.replaceWith(jsonTab); + }; +} + +if (entitiesTabSelector) { + entitiesTabSelector.onclick = () => { + entitiesTab.classList.replace("tab-hidden", "tab-visible"); + jsonTab.classList.replace("tab-visible", "tab-hidden"); + jsonTab.replaceWith(entitiesTab); + }; +} diff --git a/search/web-app/static/search.js b/search/web-app/static/search.js new file mode 100644 index 0000000..5c3f337 --- /dev/null +++ b/search/web-app/static/search.js @@ -0,0 +1,102 @@ +/** + * Copyright 2023 Google LLC + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +hljs.highlightAll(); +hljs.addPlugin(new CopyButtonPlugin()); + +mdc.autoInit(); + +const MDCRipple = mdc.ripple.MDCRipple; +const MDCTextField = mdc.textField.MDCTextField; +const MDCTabBar = mdc.tabBar.MDCTabBar; + +MDCRipple.attachTo(document.querySelector(".search-button")); + +const queryTextField = new MDCTextField(document.querySelector(".query-field")); + +const summaryPreambleFieldSelector = document.querySelector(".summary-preamble-field"); +if (summaryPreambleFieldSelector) { + const summaryPreambleTextField = new MDCTextField(summaryPreambleFieldSelector); +} +// const documentTypesField = document.querySelector('.mdc-text-field.types-field'); + +// const select = new MDCSelect(document.querySelector('.mdc-select')); + +const jsonTabSelector = document.querySelector("#json-tab-selector"); +const entitiesTabSelector = document.querySelector("#entities-tab-selector"); + +const tabContent = document.querySelector(".tab-content"); +const jsonTab = document.querySelector("#json-tab"); +const entitiesTab = document.querySelector("#entities-tab"); + +if (jsonTabSelector) { + jsonTabSelector.onclick = () => { + jsonTab.classList.replace("tab-hidden", "tab-visible"); + entitiesTab.classList.replace("tab-visible", "tab-hidden"); + entitiesTab.replaceWith(jsonTab); + }; +} + +if (entitiesTabSelector) { + entitiesTabSelector.onclick = () => { + entitiesTab.classList.replace("tab-hidden", "tab-visible"); + jsonTab.classList.replace("tab-visible", "tab-hidden"); + jsonTab.replaceWith(entitiesTab); + }; +} + +const imageInput = document.getElementById("image-input"); + +if (imageInput) { + imageInput.addEventListener("change", function (e) { + const fileInput = e.target; + const fileUploadLabel = document.getElementById("file-upload-label"); + if (fileInput.files.length > 0) { + fileUploadLabel.textContent = fileInput.files[0].name; + } else { + fileUploadLabel.textContent = "No file selected"; + } + }); +} + +const searchEngine0Radio = document.getElementById("search-engine-0"); +// Default - Uses Advanced Indexing +const searchEngine1Radio = document.getElementById("search-engine-1"); + +const summaryModelRadioSelector = document.querySelector(".summary-model-radio"); +const summaryPreambleSelector = document.querySelector(".summary-preamble-field"); + +function toggleElement(element, show) { + if (show) { + element.style.display = "block"; + } else { + element.style.display = "none"; + } +} + +// Add event listeners to radio buttons +searchEngine0Radio.addEventListener("change", function () { + if (searchEngine0Radio.checked) { + toggleElement(summaryModelRadioSelector, false); + toggleElement(summaryPreambleSelector, false); + } +}); + +searchEngine1Radio.addEventListener("change", function () { + if (searchEngine1Radio.checked) { + toggleElement(summaryModelRadioSelector, true); + toggleElement(summaryPreambleSelector, true); + } +}); diff --git a/search/web-app/static/socks.jpg b/search/web-app/static/socks.jpg new file mode 100644 index 0000000..7bf8e36 Binary files /dev/null and b/search/web-app/static/socks.jpg differ diff --git a/search/web-app/static/vertexai_32dp.svg b/search/web-app/static/vertexai_32dp.svg new file mode 100644 index 0000000..2c67393 --- /dev/null +++ b/search/web-app/static/vertexai_32dp.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/search/web-app/templates/.prettierrc b/search/web-app/templates/.prettierrc new file mode 100644 index 0000000..c1dd6c0 --- /dev/null +++ b/search/web-app/templates/.prettierrc @@ -0,0 +1,8 @@ +{ + "tabWidth": 2, + "useTabs": false, + "printWidth": 100, + "bracketSameLine": true, + "pluginSearchDirs": ["."], + "overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }] +} diff --git a/search/web-app/templates/base.html b/search/web-app/templates/base.html new file mode 100644 index 0000000..4f9f759 --- /dev/null +++ b/search/web-app/templates/base.html @@ -0,0 +1,69 @@ + + + + + + + + + + {% block css_imports %}{% endblock %} + + + {{title}} + + + + +
    + {% block product_logo %} + + {% endblock %} +

    {% block title %}{% endblock %}

    +

    {% block description %}{% endblock %}

    +
    +
    + {% block content %}{% endblock %} +
    + + + + + {% block js_imports %}{% endblock %} + diff --git a/search/web-app/templates/ekg.html b/search/web-app/templates/ekg.html new file mode 100644 index 0000000..6c276d4 --- /dev/null +++ b/search/web-app/templates/ekg.html @@ -0,0 +1,260 @@ +{% extends 'base.html' %} + +{% block css_imports %} + + +{% endblock %} + +{% block title %}{{title}}: Search Demo{% endblock %} + +{% block product_logo %} + +{% endblock %} + +{% block content %} +
    + +
    + +
    +
    +
    + +
    +
    +
    +
    + +
    + +
    + + Language + + + {% if form_options["default_language"] %} {{ form_options["default_language"].name + }} {% endif %} + + + + + + + + + +
    +
    +
      +
    • + +
    • + {% if form_options["default_language"] %} +
    • + + {{ form_options["default_language"].name }} +
    • + {% endif %} {% for l in form_options["language_list"] %} +
    • + + {{ l.name }} +
    • + {% endfor %} +
    +
    +
    + +
    + Refer to the + Schema.org full hierarchy + for supported types. +
    +
    + +
    +
    +
    + +
    +
    +
    +
    + +
    + +
    +
    +{% if message_error %} +
    +
    +
    +
    +
    {{message_error}}
    +
    +
    +
    +
    +{% elif message_success %} +
    +
    +
    +
    + + +
    +
    +
    +
    +
    +
    + {% for entity in entities %} +
    +
    +
    +
    +
    + {% if entity.get("image") %} {% set imageUrl = entity["image"]["contentUrl"] %} +
    + {% endif %} +
    +

    {{entity["name"]}}

    + {% if entity.get("@type") %} + + + {% for type in entity["@type"] %} + + + {{type}} + + + {% endfor %} + + + {% endif %} +

    + MID: {{entity["@id"]}} +
    + GOOGLE KG MID: + {{entity["identifier"][0]["value"]}} +
    + {% if entity.get("detailedDescription") %} {% set entityUrl = + entity["detailedDescription"]["url"] %} URL: + {{entityUrl}} + {% endif %} +

    +
    + {% if entity.get("detailedDescription") %} +
    +

    Detailed Description

    + {{entity["detailedDescription"]["articleBody"]}} +
    + {% endif %} +
    +
    +
    +
    +
    +
    {{entity["resultJson"]}}
    +
    +
    +
    +
    + {% endfor %} +
    +
    + {% if request_url %} +
    +
    +
    +
    + Request URL: +
    {{request_url}}
    +
    +
    +
    +
    + {% endif %} +
    +
    + {% if raw_request %} +
    + Request Body: +
    {{raw_request}}
    +
    + {% endif %} {% if raw_response %} +
    + Response Body: +
    {{raw_response}}
    +
    + {% endif %} +
    +
    +
    +
    +{% endif %} +{% endblock %} + +{% block footer %} +Made by holtskinner@ - Powered by +Enterprise Knowledge Graph +on Google Cloud +{% endblock %} + +{% block js_imports %} + + + +{% endblock %} diff --git a/search/web-app/templates/image-search.html b/search/web-app/templates/image-search.html new file mode 100644 index 0000000..a84ef5c --- /dev/null +++ b/search/web-app/templates/image-search.html @@ -0,0 +1,211 @@ +{% extends 'base.html' %} + +{% block css_imports %} + + +{% endblock %} + +{% block title %}Vertex AI Search Demo - {{title}}{% endblock %} +{% block description %} +Search the +Google Merchandise Store by providing a text query, +an image URL, or image file to find similar matches. +{% endblock %} + +{% block content %} +
    + +
    + +
    +
    +
    + +
    +
    +
    +
    + +
    + + +
    + +
    + +
    +
    +{% if not message_success %} +
    +
    +
    +
    +

    Here are some sample images to try:

    +
    +
    +
    +
    +
    +
    + Blue Pack +
    +
    + Lego Cloud +
    +
    + Purple Socks +
    +
    +
    +
    +{% endif %} +{% if message_error %} +
    +
    +
    +
    +
    {{message_error}}
    +
    +
    +
    +
    +{% elif message_success %} +
    +
    +
    +
    + + +
    +
    +
    +
    +
    +
    + {% if results|length == 0 %} +
    No results
    + {% endif %} {% for result in results %} +
    +
    +
    +
    +
    + {% set imageUrl = result["thumbnailImage"] %} {% set imageFullSize = + result["link"] %} + +
    +

    {{result["title"]}}

    +

    + {% set resultUrl = result["htmlFormattedUrl"] %} URL: + {{result["displayLink"]|safe}} +

    +
    + {% for snippet in result["snippets"] %} +
    + {{snippet|safe}} +
    + {% endfor %} +
    +
    +
    +
    +
    +
    {{result["resultJson"]}}
    +
    +
    +
    +
    + {% endfor %} +
    +
    + {% if request_url %} +
    +
    +
    +
    + Request URL: +
    {{request_url}}
    +
    +
    +
    +
    + {% endif %} +
    +
    + {% if raw_request %} +
    + Request Body: +
    {{raw_request}}
    +
    + {% endif %} {% if raw_response %} +
    + Response Body: +
    {{raw_response}}
    +
    + {% endif %} +
    +
    +
    +
    +{% endif %} +{% endblock %} + +{% block js_imports %} + + + +{% endblock %} diff --git a/search/web-app/templates/index.html b/search/web-app/templates/index.html new file mode 100644 index 0000000..1d77b6c --- /dev/null +++ b/search/web-app/templates/index.html @@ -0,0 +1,31 @@ +{% extends 'base.html' %} + +{% block title %}Vertex AI Search Demo - {{title}}{% endblock %} +{% block description %}Click on one of the text boxes to open up a full-page widget for the search engine.{% endblock %} + +{% block content %} + + + +{% for s in search_engine_options %} + + + +
    {{s.name}}
    + + +
    + +
    +
    +
    + +
    +
    +
    +
    +{% endfor %} +{% endblock %} +{% block js_imports %} + +{% endblock %} diff --git a/search/web-app/templates/recommend.html b/search/web-app/templates/recommend.html new file mode 100644 index 0000000..025cbbe --- /dev/null +++ b/search/web-app/templates/recommend.html @@ -0,0 +1,210 @@ +{% extends 'base.html' %} + +{% block css_imports %} + + +{% endblock %} + +{% block title %}Vertex AI Search Demo - {{title}}{% endblock %} +{% block description %}Select a document from the list to view recommendations of similar documents in the dataset.{% endblock %} + +{% block content %} +
    + + + +
    + + +
    +
      +
    • + +
    • + {% for d in documents %} +
    • + + {{ d.title }} +
    • + {% endfor %} +
    +
    +
    + +
    + +
    +
    +{% if message_error %} +
    +
    +
    +
    +
    {{message_error}}
    +
    +
    +
    +
    +{% elif message_success %} +
    +
    +
    +
    + + +
    +
    +
    +
    +
    +
    + {% if results|length == 0 %} +

    No recommendations were found for this document.

    + {% endif %} {% for result in results %} +
    +
    +
    +
    +
    +
    + {% set resultUrl = result["link"] %} + +

    + {{result["title"]|safe}} +

    +
    +

    + Document ID: {{result["id"]|safe}} +

    +

    + MIME Type: {{result["mimeType"]|safe}} +

    +
    +
    +
    +
    +
    +
    +
    {{result["resultJson"]}}
    +
    +
    +
    +
    + {% endfor %} +
    +
    + {% if request_url %} +
    +
    +
    +
    + Request URL: +
    {{request_url}}
    +
    +
    +
    +
    + {% endif %} +
    +
    + {% if raw_request %} +
    + Request Body: +
    {{raw_request}}
    +
    + {% endif %} {% if raw_response %} +
    + Response Body: +
    {{raw_response}}
    +
    + {% endif %} +
    +
    +
    +
    +{% endif %} +{% endblock %} + +{% block footer %} +Made by holtskinner@ - Powered by +Personalize +with Vertex AI Search +on Google Cloud +{% endblock %} + +{% block js_imports %} + + + +{% endblock %} diff --git a/search/web-app/templates/search.html b/search/web-app/templates/search.html new file mode 100644 index 0000000..b914687 --- /dev/null +++ b/search/web-app/templates/search.html @@ -0,0 +1,223 @@ +{% extends 'base.html' %} + +{% block css_imports %} + + +{% endblock %} + +{% block title %}Vertex AI Search Demo - {{title}}{% endblock %} +{% block content %} +
    + +
    + +
    +
    +
    + +
    +
    +
    +
    + +
    +
    Search engine
    + {% for engine_name in search_engines %} + + {% endfor %} +
    +
    + +
    + + {% for summary_model in summary_models %} + + {% endfor %} +
    + +
    + +
    +
    +
    + +
    +
    +
    +
    + +
    + +
    +
    +{% if message_error %} +
    +
    +
    +
    +
    {{message_error}}
    +
    +
    +
    +
    +{% elif message_success %} +
    +
    +
    +
    + + +
    +
    +
    +
    +
    +
    + {% if summary %} +
    +
    +
    +
    + Generative AI Summary: {{summary}} +
    +
    +
    +
    + {% endif %} + {% for result in results %} +
    +
    +
    +
    +
    + {% set imageUrl = result["thumbnailImage"] %} +
    +
    +

    [{{loop.index}}] {{result["title"]}}

    +

    + {% set resultUrl = result["link"] %} URL: + {{result["htmlFormattedUrl"]|safe}} +

    +
    +
    + {% if result["snippets"] %} +

    Snippet:

    + {{result["snippets"][0]|safe}} + {% endif %} +
    + {% if result["extractiveAnswers"] %} +

    Extractive Answer:

    + {{result["extractiveAnswers"][0]|safe}} + {% endif %} + {% if result["extractiveSegments"] %} +

    Extractive Segments:

    + {{result["extractiveSegments"][0]|safe}} + {% endif %} +
    +
    +
    +
    +
    +
    +
    {{result["resultJson"]}}
    +
    +
    +
    +
    + {% endfor %} +
    +
    + {% if request_url %} +
    +
    +
    +
    + Request URL: +
    {{request_url}}
    +
    +
    +
    +
    + {% endif %} +
    +
    + {% if raw_request %} +
    + Request Body: +
    {{raw_request}}
    +
    + {% endif %} {% if raw_response %} +
    + Response Body: +
    {{raw_response}}
    +
    + {% endif %} +
    +
    +
    +
    +{% endif %} +{% endblock %} + +{% block js_imports %} + + + +{% endblock %} diff --git a/search/web-app/vais_utils.py b/search/web-app/vais_utils.py new file mode 100644 index 0000000..91ce7d4 --- /dev/null +++ b/search/web-app/vais_utils.py @@ -0,0 +1,278 @@ +# Copyright 2022 Google LLC +# +# 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 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Vertex AI Search Utilities""" +from os.path import basename + +from google.cloud import discoveryengine_v1alpha as discoveryengine + +JSON_INDENT = 2 + + +def list_documents( + project_id: str, + location: str, + datastore_id: str, +) -> list[dict[str, str]]: + client = discoveryengine.DocumentServiceClient() + + parent = client.branch_path( + project=project_id, + location=location, + data_store=datastore_id, + branch="default_branch", + ) + + request = discoveryengine.ListDocumentsRequest(parent=parent, page_size=10) + + page_result = client.list_documents(request=request) + + return [ + {"id": document.id, "title": basename(document.content.uri)} + for document in page_result + ] + + +def search_enterprise_search( + project_id: str, + location: str, + engine_id: str, + page_size: int = 50, + search_query: str | None = None, + image_bytes: bytes | None = None, + params: dict | None = None, + summary_model: str | None = None, + summary_preamble: str | None = None, +) -> tuple[list[dict[str, str | list]], str, str, str, str]: + if bool(search_query) == bool(image_bytes): + raise ValueError("Cannot provide both search_query and image_bytes") + + # Create a client + client = discoveryengine.SearchServiceClient() + + serving_config = f"projects/{project_id}/locations/{location}/collections/default_collection/engines/{engine_id}/servingConfigs/default_config" + + # Configuration options for search + content_search_spec = discoveryengine.SearchRequest.ContentSearchSpec( + snippet_spec=discoveryengine.SearchRequest.ContentSearchSpec.SnippetSpec( + return_snippet=True + ), + summary_spec=discoveryengine.SearchRequest.ContentSearchSpec.SummarySpec( + summary_result_count=5, + include_citations=True, + ignore_adversarial_query=True, + ignore_non_summary_seeking_query=True, + model_spec=discoveryengine.SearchRequest.ContentSearchSpec.SummarySpec.ModelSpec( + version=summary_model + ), + model_prompt_spec=discoveryengine.SearchRequest.ContentSearchSpec.SummarySpec.ModelPromptSpec( + preamble=summary_preamble + ), + ), + extractive_content_spec=discoveryengine.SearchRequest.ContentSearchSpec.ExtractiveContentSpec( + max_extractive_answer_count=1, max_extractive_segment_count=1 + ), + ) + + request = discoveryengine.SearchRequest( + serving_config=serving_config, + page_size=page_size, + content_search_spec=content_search_spec, + query_expansion_spec=discoveryengine.SearchRequest.QueryExpansionSpec( + condition=discoveryengine.SearchRequest.QueryExpansionSpec.Condition.AUTO, + ), + spell_correction_spec=discoveryengine.SearchRequest.SpellCorrectionSpec( + mode=discoveryengine.SearchRequest.SpellCorrectionSpec.Mode.AUTO + ), + params=params, + ) + + if search_query: + request.query = search_query + elif image_bytes: + request.image_query = discoveryengine.SearchRequest.ImageQuery( + image_bytes=image_bytes + ) + + try: + response_pager = client.search(request) + except Exception as exc: + raise exc + + response = discoveryengine.SearchResponse( + results=response_pager.results, + facets=response_pager.facets, + guided_search_result=response_pager.guided_search_result, + total_size=response_pager.total_size, + attribution_token=response_pager.attribution_token, + next_page_token=response_pager.next_page_token, + corrected_query=response_pager.corrected_query, + summary=response_pager.summary, + ) + + request_url = ( + f"https://discoveryengine.googleapis.com/v1alpha/{serving_config}:search" + ) + + request_json = discoveryengine.SearchRequest.to_json( + request, + including_default_value_fields=False, + use_integers_for_enums=False, + indent=JSON_INDENT, + ) + response_json = discoveryengine.SearchResponse.to_json( + response, + including_default_value_fields=True, + use_integers_for_enums=False, + indent=JSON_INDENT, + ) + + results = get_enterprise_search_results(response) + summary = getattr(response.summary, "summary_text", "") + return results, summary, request_url, request_json, response_json + + +def get_enterprise_search_results( + response: discoveryengine.SearchResponse, +) -> list[dict[str, str | list]]: + """ + Extract Results from Enterprise Search Response + """ + + ROBOT = "https://www.google.com/images/errors/robot.png" + + def get_thumbnail_image(data: dict) -> str: + cse_thumbnail = data.get("pagemap", {}).get("cse_thumbnail") + image_link = data.get("image", {}).get("thumbnailLink") + + if cse_thumbnail: + return cse_thumbnail[0]["src"] + if image_link: + return image_link + return ROBOT + + def get_formatted_link(data: dict) -> str: + html_formatted_url = data.get("htmlFormattedUrl") + image_context_link = data.get("image", {}).get("contextLink") + link = data.get("link") + return html_formatted_url or image_context_link or link or ROBOT + + return [ + { + "title": result.document.derived_struct_data["title"], + "htmlTitle": result.document.derived_struct_data.get( + "htmlTitle", result.document.derived_struct_data["title"] + ), + "link": result.document.derived_struct_data["link"], + "htmlFormattedUrl": get_formatted_link(result.document.derived_struct_data), + "displayLink": result.document.derived_struct_data["displayLink"], + "snippets": [ + s.get("htmlSnippet", s.get("snippet", "")) + for s in result.document.derived_struct_data.get("snippets", []) + ], + "extractiveAnswers": [ + e["content"] + for e in result.document.derived_struct_data.get( + "extractive_answers", [] + ) + ], + "extractiveSegments": [ + e["content"] + for e in result.document.derived_struct_data.get( + "extractive_segments", [] + ) + ], + "thumbnailImage": get_thumbnail_image(result.document.derived_struct_data), + "resultJson": discoveryengine.SearchResponse.SearchResult.to_json( + result, including_default_value_fields=True, indent=JSON_INDENT + ), + } + for result in response.results + ] + + +def recommend_personalize( + project_id: str, + location: str, + datastore_id: str, + serving_config_id: str, + document_id: str, + user_pseudo_id: str | None = "xxxxxxxxxxx", + attribution_token: str | None = None, +) -> tuple: + # Create a client + client = discoveryengine.RecommendationServiceClient() + + # The full resource name of the search engine serving config + # e.g. projects/{project_id}/locations/{location} + serving_config = client.serving_config_path( + project=project_id, + location=location, + data_store=datastore_id, + serving_config=serving_config_id, + ) + + user_event = discoveryengine.UserEvent( + event_type="view-item", + user_pseudo_id=user_pseudo_id, + attribution_token=attribution_token, + documents=[discoveryengine.DocumentInfo(id=document_id)], + ) + + request = discoveryengine.RecommendRequest( + serving_config=serving_config, + user_event=user_event, + params={"returnDocument": True, "returnScore": True}, + ) + + response = client.recommend(request) + + request_url = ( + f"https://discoveryengine.googleapis.com/v1beta/{serving_config}:recommend" + ) + + request_json = discoveryengine.RecommendRequest.to_json( + request, including_default_value_fields=False, indent=JSON_INDENT + ) + response_json = discoveryengine.RecommendResponse.to_json( + response, including_default_value_fields=True, indent=JSON_INDENT + ) + + results = get_personalize_results(response) + return results, response.attribution_token, request_url, request_json, response_json + + +def get_storage_link(uri: str) -> str: + return uri.replace("gs://", "https://storage.googleapis.com/") + + +def get_personalize_results( + response: discoveryengine.RecommendResponse, +) -> list[dict]: + """ + Extract Results from Personalize Response + """ + return [ + { + "id": result.id, + "title": basename(result.document.content.uri), + "htmlFormattedUrl": result.document.content.uri, + "link": get_storage_link(result.document.content.uri), + "mimeType": result.document.content.mime_type, + "resultJson": discoveryengine.RecommendResponse.RecommendationResult.to_json( + result, including_default_value_fields=True, indent=JSON_INDENT + ), + } + for result in response.results + ] diff --git a/setup-env/README.md b/setup-env/README.md new file mode 100644 index 0000000..3052295 --- /dev/null +++ b/setup-env/README.md @@ -0,0 +1,87 @@ +# Setup instructions to use Generative AI on Google Cloud + +This folder contains instructions on: + +- Setting up your Google Cloud project +- Notebook environments + - Setting up Colab + - Setting up Vertex AI Workbench +- Python SDK for Vertex AI + +## Setting up your Google Cloud project + +1. [Select or create a Google Cloud project](https://console.cloud.google.com/cloud-resource-manager). + When you first create an account, you get a $300 free credit towards your compute/storage costs. + +2. [Make sure that billing is enabled for your project](https://cloud.google.com/billing/docs/how-to/modify-project). + +3. [Enable the Vertex AI API and Google Cloud Storage API](https://console.cloud.google.com/flows/enableapi?apiid=aiplatform.googleapis.com,storage.googleapis.com). + +## Notebook environments + +### Colab + +[Google Colab](https://colab.research.google.com/) allows you to write and execute Python in your browser with minimal setup. + +To use Colab with this repo, please click on the "Open in Colab" link at the top of any notebook file in this repo to launch it in Colab. Then follow the instructions within. + +For Colab you will need to authenticate so that you can use Google Cloud from Colab: + +```py +from google.colab import auth +auth.authenticate_user() +``` + +When using the vertexai Python SDK, you will also need to initialize it with your Google Cloud `project_id` and `location`: + +```py +PROJECT_ID = "your-project-id" +LOCATION = "" #e.g. us-central1 + +import vertexai +vertexai.init(project=PROJECT_ID, location=LOCATION) +``` + +### Vertex AI Workbench + +[Vertex AI Workbench](https://cloud.google.com/vertex-ai-workbench) is the JupyterLab notebook environment on Google Cloud, which enables you to create and customize notebook instances. You do not need extra authentication steps. + +#### Creating your notebook instance on Vertex AI Workbench + +To create a new JupyterLab instance on Vertex AI Workbench, follow the [instructions here to create a user-managed notebooks instance](https://cloud.google.com/vertex-ai/docs/workbench/user-managed/create-new). + +#### Using this repository on Vertex AI Workbench + +After launching the notebook instance, you can clone this repository in your JupyterLab environment. To do so, open a Terminal in JupyterLab. Then run the command below to clone the repository into your instance: + +```sh +git clone https://github.com/GoogleCloudPlatform/generative-ai.git +``` + +#### Local development + +- Install the [Google Cloud SDK](https://cloud.google.com/sdk). + +- Obtain authentication credentials. Create local credentials by running the following command and following the oauth2 flow (read more about the command [here](https://cloud.google.com/sdk/gcloud/reference/beta/auth/application-default/login)): + + ```bash + gcloud auth application-default login + ``` + +## Python library + +Install the latest Python SDK: + +```sh +%pip install google-cloud-aiplatform --upgrade +``` + +You will need to initialize `vertexai` with your `project_id` and `location`: + +```py +PROJECT_ID = "your-project-id" +LOCATION = "" #e.g. us-central1 + +import vertexai +vertexai.init(project=PROJECT_ID, location=LOCATION) +``` diff --git a/tools/llmevalkit/CONTRIBUTING.md b/tools/llmevalkit/CONTRIBUTING.md new file mode 100644 index 0000000..2e17204 --- /dev/null +++ b/tools/llmevalkit/CONTRIBUTING.md @@ -0,0 +1,29 @@ +## Before you begin + +### Sign our Contributor License Agreement + +Contributions to this project must be accompanied by a +[Contributor License Agreement](https://cla.developers.google.com/about) (CLA). +You (or your employer) retain the copyright to your contribution; this simply +gives us permission to use and redistribute your contributions as part of the +project. + +If you or your current employer have already signed the Google CLA (even if it +was for a different project), you probably don't need to do it again. + +Visit to see your current agreements or to +sign a new one. + +### Review our community guidelines + +This project follows +[Google's Open Source Community Guidelines](https://opensource.google/conduct/). + +## Contribution process + +### Code reviews + +All submissions, including submissions by project members, require review. We +use GitHub pull requests for this purpose. Consult +[GitHub Help](https://help.github.com/articles/about-pull-requests/) for more +information on using pull requests. diff --git a/tools/llmevalkit/LICENSE b/tools/llmevalkit/LICENSE new file mode 100644 index 0000000..7a4a3ea --- /dev/null +++ b/tools/llmevalkit/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. \ No newline at end of file diff --git a/tools/llmevalkit/README.md b/tools/llmevalkit/README.md new file mode 100644 index 0000000..d5642e8 --- /dev/null +++ b/tools/llmevalkit/README.md @@ -0,0 +1,265 @@ +# LLM EvalKit + +## Summary + +LLMEvalKit is a tool designed to help developers evaluate and improve the performance of Large Language Models (LLMs) on specific tasks. It provides a comprehensive workflow to create, test, and optimize prompts, manage datasets, and analyze evaluation results. With LLMEvalKit, developers can conduct both human and model-based evaluations, compare results, and use automated processes to refine prompts for better accuracy and relevance. This toolkit streamlines the iterative process of prompt engineering and evaluation, enabling developers to build more effective and reliable LLM-powered applications. + +![Image](assets/image.gif) + +**Authors: [Mike Santoro](https://github.com/Michael-Santoro), [Katherine Larson](https://github.com/kat-litinsky)** + +## 🚀 Getting Started + +There are two ways to work through a tutorial of this application one method is more stable one is less stable. + +1. Scroll down to the Tutorial Section here. + +2. Open this [notebook](https://github.com/GoogleCloudPlatform/generative-ai/blob/main/tools/llmevalkit/prompt-management-tutorial.ipynb) in colab running the application on a colab server. + +## Overview + +This tutorial provides a comprehensive guide to prompt engineering, covering the entire lifecycle from creation to evaluation and optimization. It's broken down into the following sections: + +1. **Prompt Management:** This section focuses on the core tasks of creating, editing, and managing prompts. You can: + - **Create new prompts:** Define the prompt's name, text, the model it's designed for, and any system instructions. + - **Load and edit existing prompts:** Browse a library of saved prompts, load a specific version, and make modifications. + - **Test prompts:** Before saving, you can provide sample input and generate a response to see how the prompt performs. + - **Versioning:** Each time you save a change to a prompt, a new version is created, allowing you to track its evolution and compare different iterations. + +2. **Dataset Creation:** A crucial part of prompt engineering is having good data to test and evaluate your prompts. This section allows you to: + + - **Create new datasets:** A dataset is essentially a folder in Google Cloud Storage where you can group related files. + - **Upload data:** You can upload files in CSV, JSON, or JSONL format to your datasets. This data will be used for evaluating your prompts. + +3. **Evaluation:** Once you have a prompt and a dataset, you need to see how well the prompt performs. The evaluation section helps you with this by: + + - **Running evaluations:** You can select a prompt and a dataset and run an evaluation. This will generate responses from the model for each item in your dataset. + - **Human-in-the-loop rating:** For a more nuanced evaluation, you can manually review the model's responses and rate them. + - **Automated metrics:** The tutorial also supports automated evaluation metrics to get a quantitative measure of your prompt's performance. + +4. **One-Click Refiner:** Instantly upgrade a draft prompt into a structured, production-ready instruction without managing any datasets. This is a quick way to apply prompt engineering best practices to your initial drafts. + +5. **Performance Tuner:** Optimize your prompt's System Instructions using data-driven iteration to maximize metric performance. This tool uses hill-climbing algorithms to automatically refine prompts based on your evaluation metrics. + +6. **Prompt Optimization:** This section helps you automatically improve your prompts using Agent Platform's prompt optimization capabilities. It provides a structured way to: + + - **Configure and launch optimization jobs:** You can set up and run a job that will take your prompt and a dataset and try to find a better-performing version of the prompt. + +7. **Prompt Optimization Results:** After an optimization job has run, this section allows you to: + + - **View the results:** You can see the different prompt versions that the optimizer came up with and how they performed. + - **Compare versions:** The results are presented in a way that makes it easy to compare the different optimized prompts and choose the best one. + +8. **Prompt Records:** This is a leaderboard that shows you the evaluation results of all your different prompt versions. It helps you to: + + - **Track performance over time:** See how your prompts have improved with each new version. + - **Compare different prompts:** You can compare the performance of different prompts for the same task. + +In summary, this tutorial provides a complete and integrated environment for all your prompt engineering needs, from initial creation to sophisticated optimization and evaluation. + +## Tutorial: Step-by-Step + +This section walks you through using the app. + +### 0. Startup + +First, clone the repository and set up the environment: + +# Clone the repository +git clone https://github.com/GoogleCloudPlatform/generative-ai.git + +# Navigate to the project directory +cd generative-ai/tools/llmevalkit + + + +Next, `cp src/.env.example src/.env` open the file and set `BUCKET_NAME` and `PROJECT_ID` + +# Authorize gcloud +`gcloud auth application-default login` + +# Run the Streamlit application +`uv run streamlit run index.py` + +### 1. Prompt Management + +In the Prompt Name field enter: + +``` +math_prompt_test +``` + +In the Prompt Text field enter: + +``` +Problem: {{query}} +Image: {{image}} @@@image/jpeg +Answer: {{target}} +``` + +In the Model Name field enter: +``` +gemini-2.5-flash +``` + +In the System Instructions field enter: +``` +Solve the problem given the image. +``` + +Click `Save Prompt` + +Copy this text for testing: + +``` +{"query": "Hint: Please answer the question and provide the correct option letter, e.g., A, B, C, D, at the end.\nQuestion: As shown in the figure, CD is the diameter of \u2299O, chord DE \u2225 OA, if the degree of \u2220D is 50.0, then the degree of \u2220C is ()", "Choices":"\n(A) 25\u00b0\n(B) 30\u00b0\n(C) 40\u00b0\n(D) 50\u00b0", "image": "gs://github-repo/prompts/prompt_optimizer/mathvista_dataset/images/643.jpg", "target": "25\u00b0"} +``` + +đŸ–±ïž Click `Generate`. + +### 2. Dataset Creation + +Download a copy of the dataset. Then upload this file in the application. + +**Dataset Name:** `mathvista` + +You can preview the dataset at the bottom of the page. + +To download the dataset, run this command: +```bash +gsutil cp gs://github-repo/prompts/prompt_optimizer/mathvista_dataset/mathvista_input.jsonl . +``` + +### 3. Evaluation + +*Note: Ensure you have completed Step 2 to create a dataset before proceeding here.* + +We will now run an evaluation, prior to doing any tweaking to get a baseline. + +- **Existing Dataset:** 'mathvista' +- **Dataset File:** 'mathvista_input.jsonl' +- **Number of Samples:** '100' +- **Ground Truth Column Name:** 'target' +- **Existing Prompt:** 'math_prompt_test' +- **Version:** '1' + +**Note:** if the prompt is not in the list refresh the page. + +Click Load Prompt, and Upload and Get Response... ⏰ Wait!! + +Review the responses. + +- **Model-Based:** 'question-answering-quality' +- **Model:** 'gemini-2.5-pro' + +Launch the Eval... ⏰ Wait!! + +View the Evaluation Results, and save to prompt records. This will save this initial version to the prompt records for the baseline. + +### 4. One-Click Refiner + +Use this for a quick, zero-data upgrade to your prompt. + +- **Select Existing Prompt:** 'math_prompt_test' +- **Select Version:** '1' + +đŸ–±ïž Click **Load Prompt**. + +- **Target Model:** 'gemini-2.0-flash-001' (or your preferred model) +- **Tone:** 'Professional' + +đŸ–±ïž Click **Auto-Suggest Directives**. +đŸ–±ïž Click **Optimize Now**. + +Review the **Optimized Result** and the **Insights** (why it changed). If satisfied, click **Save as New Version**. + +### (Optional) Run new Evaluation + +Navigate back to **Evaluation** and run an evaluation similar to step 3, but load **Version 2** of the prompt. + +### 5. Performance Tuner + +Use this for data-driven optimization using a hill-climbing algorithm. + +- **Select Dataset:** 'mathvista' +- **Select File:** 'mathvista_input.jsonl' + +đŸ–±ïž Click **Load Dataset**. + +- **Select Prompt:** 'math_prompt_test' +- **Select Version:** '1' + +đŸ–±ïž Click **Load Prompt**. + +- **Evaluation Metrics:** 'question_answering_correctness' (default) +- **Target Model:** 'gemini-2.5-flash' + +đŸ–±ïž Click **Start Optimization Job**. + +⏰ Wait!! This may take some time as it runs multiple iterations. + +Once complete, click **Load Results** to see the **Score Jump** and the **Winner System Instruction**. You can test the winner on a blind case before clicking **Export Final Prompt** to save it as a new version. + +### (Optional) Run new Evaluation + +Navigate back to **Evaluation** and run an evaluation similar to step 3, but load **Version 2** of the prompt. + +### 6. Prompt Optimization + +🔧 Set-Up Prompt Optimization using Agent Platform's batch optimization service. + +- **Target Model:** 'gemini-2.5-flash' +- **Existing Prompt:** 'math_prompt_test' +- **Version:** '1' + +đŸ–±ïž Click Load Prompt. + +- **Select Existing Dataset:** 'mathvista' +- **Select the File:** 'mathvista_input.jsonl' + +đŸ–±ïž Click Load Dataset. + +Preview the dataset. + +đŸ–±ïž Click Start Optimization. + +**Note:** If Interested in viewing the progress, Navigate to https://console.cloud.google.com/vertex-ai/training/custom-jobs + +⏰ Wait!! This step will take about 20-min to run. + +### 7. Prompt Optimization Results + +View the Optimization Results. + +The last run will be shown at the top of the screen. Pick this from the dropdown menu: + +![image.png](assets/prompt_optimization_result.png) + +Review the results and select the highest scoring version and copy the instruction. + +### 8. Navigate Back to Prompt for New Version + +Load your existing prompt from before. + +📋 Paste your new instructions from the prompt optimizer, and save new version. + +### 9. Run new Evaluation + +Repeat step 3 with your new version. + +### 10. View the Records + +Navigate to the leaderboard and load the results. + +## License +``` +Copyright 2025 Google LLC +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 + https://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language +``` diff --git a/tools/llmevalkit/assets/favicon.ico b/tools/llmevalkit/assets/favicon.ico new file mode 100644 index 0000000..7217989 Binary files /dev/null and b/tools/llmevalkit/assets/favicon.ico differ diff --git a/tools/llmevalkit/assets/image.gif b/tools/llmevalkit/assets/image.gif new file mode 100644 index 0000000..9f9117e Binary files /dev/null and b/tools/llmevalkit/assets/image.gif differ diff --git a/tools/llmevalkit/assets/prompt_optimization_result.png b/tools/llmevalkit/assets/prompt_optimization_result.png new file mode 100644 index 0000000..a9e6a20 Binary files /dev/null and b/tools/llmevalkit/assets/prompt_optimization_result.png differ diff --git a/tools/llmevalkit/assets/welcome_page.png b/tools/llmevalkit/assets/welcome_page.png new file mode 100644 index 0000000..62696f5 Binary files /dev/null and b/tools/llmevalkit/assets/welcome_page.png differ diff --git a/tools/llmevalkit/index.py b/tools/llmevalkit/index.py new file mode 100644 index 0000000..c7c0572 --- /dev/null +++ b/tools/llmevalkit/index.py @@ -0,0 +1,54 @@ +## Copyright 2025 Google LLC +## 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 +## https://www.apache.org/licenses/LICENSE-2.0 +## Unless required by applicable law or agreed to in writing, software +## distributed under the License is distributed on an "AS IS" BASIS, +## WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +## See the License for the specific language + + +"""The main landing page for the LLM EvalKit Streamlit application.""" + +import streamlit as st +from dotenv import load_dotenv + +load_dotenv("src/.env") + + +def main() -> None: + """Renders the main landing page of the application.""" + st.set_page_config( + page_title="LLM EvalKit", + layout="wide", + initial_sidebar_state="expanded", + page_icon="assets/favicon.ico", + ) + + st.title("Welcome to the LLM EvalKit") + st.markdown( + "A suite of tools for managing, evaluating, and optimizing LLM prompts and datasets." + ) + + st.subheader("Getting Started") + st.markdown( + """ + This application helps you streamline your prompt engineering workflow. + Select a tool from the sidebar on the left to begin. + + **Available Tools:** + * **Prompt Management:** Create, test, and manage your prompts. + * **Dataset Creation:** Create evaluation datasets from CSV files. + * **Simple Evaluation:** Run simple evaluations on your prompts. + * **Evaluation Human Judge:** Manually rate model responses for evaluation. + * **Prompt Optimization:** Optimize your prompts for better performance. + * **Prompt Optimization Results:** View the results of prompt optimization runs. + * **Prompt Records:** View and manage your prompt records. + """ + ) + st.caption("LLM EvalKit | Home") + + +if __name__ == "__main__": + main() diff --git a/tools/llmevalkit/pages/1_Prompt_Management.py b/tools/llmevalkit/pages/1_Prompt_Management.py new file mode 100644 index 0000000..2aa269c --- /dev/null +++ b/tools/llmevalkit/pages/1_Prompt_Management.py @@ -0,0 +1,590 @@ +# Copyright 2025 Google LLC +# 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 +# https://www.apache.org/licenses/LICENSE-2.0 +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language + +"""Streamlit user interface for managing prompts in the LLM EvalKit. + +This page provides a comprehensive interface for prompt engineering, allowing users +to create, load, edit, and test prompts that are stored and versioned in a +backend service (e.g., Google Cloud's Vertex AI Prompt Management). + +The page is divided into two main sections: +1. **Create New Prompt**: A form to define a new prompt from scratch, including + its name, text, model, system instructions, and other metadata. Users can + test the prompt with sample input before saving it. +2. **Load & Edit Prompt**: A section to load existing prompts and their specific + versions. Users can modify the loaded prompt's details and save the changes + as a new version, facilitating iterative development and A/B testing. + +Helper functions handle JSON parsing, data type conversions, and interactions +with the `gcp_prompt` object, which abstracts the backend communication. +""" + +import json +import logging +from typing import Any + +import streamlit as st +from dotenv import load_dotenv +from src.gcp_prompt import GcpPrompt as gcp_prompt +from vertexai.preview import prompts + +# --- Initial Configuration --- +load_dotenv("src/.env") +logging.basicConfig( + level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s" +) +logger = logging.getLogger(__name__) + +# --- Constants --- +AVAILABLE_PROMPT_TASKS = [ + "Classification", + "Summarization", + "Translation", + "Creative Writing", + "Q&A", +] + + +# --- Helper Functions --- +def _parse_json_input(json_string: str, field_name: str) -> dict[str, Any] | None: + """Safely parses a JSON string from a text area. + + Cleans the input string to handle common copy-paste errors and displays + an error in the Streamlit UI if parsing fails. + + Args: + json_string: The raw string from a Streamlit text_area. + field_name: The user-facing name of the field for error messages. + + Returns: + A dictionary if parsing is successful, otherwise None. + """ + if not json_string: + return None + try: + # Clean up common copy-paste issues like smart quotes and newlines + json_string_cleaned = ( + json_string.replace("’", "'") + .replace("\n", " ") + .replace("\t", " ") + .replace("\r", "") + ) + return json.loads(json_string_cleaned) + except json.JSONDecodeError as e: + st.error(f"Invalid JSON format for {field_name}: {e}") + return None + + +def _apply_generation_config_typing(config: dict[str, Any]) -> dict[str, Any]: + """Applies correct data types to generation config parameters. + + Streamlit text inputs return strings, but the underlying API requires + specific types (e.g., float for temperature). This function converts + common configuration values to their expected types. + + Args: + config: The generation configuration dictionary with string values. + + Returns: + The configuration dictionary with values cast to the correct types. + """ + if "temperature" in config: + config["temperature"] = float(config["temperature"]) + if "top_p" in config: + config["top_p"] = float(config["top_p"]) + if "max_output_tokens" in config: + config["max_output_tokens"] = int(config["max_output_tokens"]) + return config + + +# --- Handlers for "Create New Prompt" Tab --- +def _handle_save_new_prompt() -> None: + """Validates inputs and saves a new prompt. + + Retrieves all necessary data from the Streamlit session state for the + "Create New Prompt" tab, validates that required fields are filled, + constructs the prompt object, and calls the backend service to save it. + Displays success or error messages in the UI. + """ + required_fields = { + "new_prompt_name": "Prompt Name", + "new_prompt_data": "Prompt Text", + "new_model_name": "Model Name", + "new_system_instructions": "System Instructions", + } + for key, name in required_fields.items(): + if not st.session_state.get(key): + st.warning(f"Please enter a value for {name}.") + return + + prompt_obj = st.session_state.local_prompt + prompt_obj.prompt_to_run.prompt_name = st.session_state.new_prompt_name + prompt_obj.prompt_to_run.prompt_data = st.session_state.new_prompt_data + prompt_obj.prompt_to_run.model_name = st.session_state.new_model_name.strip() + prompt_obj.prompt_to_run.system_instruction = ( + st.session_state.new_system_instructions + ) + + response_schema = _parse_json_input( + st.session_state.new_response_schema, "Response Schema" + ) + generation_config = _parse_json_input( + st.session_state.new_generation_config, "Generation Config" + ) + + if generation_config: + generation_config = _apply_generation_config_typing(generation_config) + if response_schema: + generation_config["response_schema"] = response_schema + prompt_obj.prompt_meta["generation_config"] = generation_config + + if response_schema: + prompt_obj.prompt_meta["response_schema"] = response_schema + prompt_obj.prompt_meta["meta_tags"] = st.session_state.new_meta_tags + + try: + logger.info("Saving new prompt...") + prompt_meta_info = prompt_obj.save_prompt(check_existing=True) + logger.info("Prompt saved successfully: %s", prompt_meta_info) + st.success("Prompt saved successfully!") + except Exception as e: + logger.error("Failed to save prompt: %s", e, exc_info=True) + st.error(f"Failed to save prompt: {e}") + + +def _handle_generate_test_for_new() -> None: + """Generates a test response for the new prompt form. + + Takes the user-provided sample input and the current prompt configuration + from the "Create" tab, sends it to the model for a response, and displays + the output in the UI. This allows for quick testing before saving. + """ + user_input_str = st.session_state.new_sample_user_input + if not user_input_str: + st.warning("Please provide sample user input to generate a response.") + return + + sample_user_input = _parse_json_input(user_input_str, "User Input") + if sample_user_input is None: + return + + try: + prompt_obj = st.session_state.local_prompt + prompt_obj.prompt_to_run.prompt_data = st.session_state.new_prompt_data + prompt_obj.prompt_to_run.model_name = st.session_state.new_model_name.strip() + prompt_obj.prompt_to_run.system_instruction = ( + st.session_state.new_system_instructions + ) + prompt_obj.prompt_meta["sample_user_input"] = sample_user_input + + with st.spinner("Generating response..."): + response = prompt_obj.generate_response(sample_user_input) + st.session_state.new_sample_output = response + st.success("Prompt response generated!") + except Exception as e: + logger.error("Error during test generation: %s", e, exc_info=True) + st.error(f"An error occurred during generation: {e}") + + +# --- Handlers for "Load & Edit Prompt" Tab --- +def _populate_ui_from_prompt() -> None: + """Populates session state for UI widgets from the loaded prompt object. + + After a prompt is loaded from the backend, this function takes the data + from the `gcp_prompt` object and sets the corresponding values in the + Streamlit session state. This updates the "Load & Edit" tab's input + widgets to display the loaded prompt's information. + """ + prompt_obj = st.session_state.local_prompt + st.session_state.edit_prompt_name = prompt_obj.prompt_to_run.prompt_name + st.session_state.edit_prompt_data = prompt_obj.prompt_to_run.prompt_data + st.session_state.edit_model_name = prompt_obj.prompt_to_run.model_name.split("/")[ + -1 + ] + st.session_state.edit_system_instructions = ( + prompt_obj.prompt_to_run.system_instruction + ) + st.session_state.edit_response_schema = json.dumps( + prompt_obj.prompt_meta.get("response_schema", {}), indent=2 + ) + st.session_state.edit_generation_config = json.dumps( + prompt_obj.prompt_meta.get("generation_config", {}), indent=2 + ) + st.session_state.edit_meta_tags = prompt_obj.prompt_meta.get("meta_tags", []) + st.session_state.edit_sample_user_input = json.dumps( + prompt_obj.prompt_meta.get("sample_user_input", {}), indent=2 + ) + st.session_state.edit_sample_output = "" # Clear previous output + + +def _handle_load_prompt() -> None: + """Loads the selected prompt and version and populates the UI. + + Triggered by the 'Load Prompt' button. It retrieves the selected prompt + name and version from the UI, calls the backend to fetch the data, + and then uses `_populate_ui_from_prompt` to display it. + """ + if not st.session_state.get("selected_prompt") or not st.session_state.get( + "selected_version" + ): + st.warning("Please select both a prompt and a version to load.") + return + + prompt_name = st.session_state.selected_prompt + prompt_id = st.session_state.local_prompt.existing_prompts[prompt_name] + version_id = st.session_state.selected_version + + try: + with st.spinner(f"Loading version '{version_id}' of prompt '{prompt_name}'..."): + st.session_state.local_prompt.load_prompt( + prompt_id, prompt_name, version_id + ) + logger.info( + "Successfully loaded prompt '%s' version '%s'.", prompt_name, version_id + ) + _populate_ui_from_prompt() + st.success(f"Loaded prompt '{prompt_name}' (Version: {version_id}).") + except Exception as e: + logger.error("Failed to load prompt: %s", e, exc_info=True) + st.error(f"Failed to load prompt: {e}") + + +def _handle_save_edited_prompt() -> None: + """Validates inputs and saves the current prompt config as a new version. + + Similar to saving a new prompt, but it takes the data from the "Edit" tab's + widgets. It saves the current configuration as a new version of the + already existing prompt. + """ + if not st.session_state.get("edit_prompt_name"): + st.warning("Cannot save. Please load a prompt first.") + return + + required_fields = { + "edit_prompt_data": "Prompt Text", + "edit_model_name": "Model Name", + "edit_system_instructions": "System Instructions", + } + for key, name in required_fields.items(): + if not st.session_state.get(key): + st.warning(f"Please ensure '{name}' is not empty.") + return + + prompt_obj = st.session_state.local_prompt + prompt_obj.prompt_to_run.prompt_name = st.session_state.edit_prompt_name + prompt_obj.prompt_to_run.prompt_data = st.session_state.edit_prompt_data + prompt_obj.prompt_to_run.model_name = st.session_state.edit_model_name.strip() + prompt_obj.prompt_to_run.system_instruction = ( + st.session_state.edit_system_instructions + ) + + response_schema = _parse_json_input( + st.session_state.edit_response_schema, "Response Schema" + ) + generation_config = _parse_json_input( + st.session_state.edit_generation_config, "Generation Config" + ) + + if generation_config: + generation_config = _apply_generation_config_typing(generation_config) + if response_schema: + generation_config["response_schema"] = response_schema + prompt_obj.prompt_meta["generation_config"] = generation_config + + if response_schema: + prompt_obj.prompt_meta["response_schema"] = response_schema + prompt_obj.prompt_meta["meta_tags"] = st.session_state.edit_meta_tags + + try: + with st.spinner("Saving as new version..."): + prompt_meta_info = prompt_obj.save_prompt(check_existing=False) + logger.info("Prompt saved successfully: %s", prompt_meta_info) + st.success("Saved as a new version successfully!") + st.session_state.local_prompt.refresh_prompt_cache() + except Exception as e: + logger.error("Failed to save prompt: %s", e, exc_info=True) + st.error(f"Failed to save prompt: {e}") + + +def _handle_generate_test_for_edit() -> None: + """Generates a test response for the edited prompt. + + Allows users to test changes made in the "Edit" tab before saving them + as a new version. It uses the current values in the UI fields to generate + a response from the model. + """ + if not st.session_state.get("edit_prompt_name"): + st.warning("Please load a prompt before generating a response.") + return + + user_input_str = st.session_state.get("edit_sample_user_input", "") + if not user_input_str: + st.warning("Please provide sample user input to generate a response.") + return + + sample_user_input = _parse_json_input(user_input_str, "Sample User Input") + if sample_user_input is None: + return + + try: + prompt_obj = st.session_state.local_prompt + prompt_obj.prompt_to_run.prompt_data = st.session_state.edit_prompt_data + prompt_obj.prompt_to_run.system_instruction = ( + st.session_state.edit_system_instructions + ) + prompt_obj.prompt_meta["sample_user_input"] = sample_user_input + + with st.spinner("Generating response..."): + response = prompt_obj.generate_response(sample_user_input) + st.session_state.edit_sample_output = response + st.success("Prompt response generated!") + except Exception as e: + logger.error("Error during test generation: %s", e, exc_info=True) + st.error(f"An error occurred during generation: {e}") + + +# --- UI Rendering Functions --- +def render_create_tab() -> None: + """Renders the UI components for the 'Create New Prompt' tab. + + This function defines and lays out all the Streamlit widgets (text inputs, + buttons, etc.) for the prompt creation workflow. + """ + st.subheader("1. Define Prompt Details") + st.text_input( + "**Prompt Name**", + key="new_prompt_name", + placeholder="e.g., customer_sentiment_classifier_v1", + help="A unique name to identify your prompt.", + ) + st.text_area( + "**Prompt Text**", + key="new_prompt_data", + height=150, + placeholder="e.g., Classify the sentiment of the following text: {customer_review}", + help="The core text of your prompt. Use curly braces `{}` for variables.", + ) + st.text_input( + "**Model Name**", + key="new_model_name", + placeholder="gemini-2.5-pro-001", + help="The specific model version to use (e.g., gemini-2.5-pro).", + ) + st.text_area( + "**System Instructions**", + key="new_system_instructions", + height=300, + placeholder="e.g., You are an expert in sentiment analysis...", + help="Optional instructions to guide the model's behavior.", + ) + st.multiselect( + "**Prompt Task**", + options=AVAILABLE_PROMPT_TASKS, + key="new_meta_tags", + help="Select the most appropriate task type for this prompt.", + ) + st.text_area( + "**Response Schema (JSON)**", + key="new_response_schema", + height=150, + placeholder='{\n "type": "object", ... \n}', + help="Define the desired JSON structure for the model's output.", + ) + st.text_area( + "**Generation Config (JSON)**", + key="new_generation_config", + height=150, + placeholder='{\n "temperature": 0.2, ... \n}', + help="A dictionary of generation parameters.", + ) + + if st.button( + "Save Prompt", type="primary", use_container_width=True, key="save_new" + ): + _handle_save_new_prompt() + + st.divider() + + st.subheader("2. Test Your Prompt") + st.markdown("You can test your prompt here before saving.") + st.text_area( + "**Sample User Input (JSON)**", + key="new_sample_user_input", + height=150, + placeholder='{\n "customer_review": "The product was amazing!"\n}', + help="A JSON object where keys match the variables in your prompt text.", + ) + + if st.button("Generate Test Response", use_container_width=True, key="test_new"): + _handle_generate_test_for_new() + + st.text_area( + "**Test Output**", + key="new_sample_output", + height=150, + placeholder="The model's response will be displayed here.", + disabled=True, + ) + + +def render_edit_tab() -> None: + """Renders the UI components for the 'Load & Edit Prompt' tab. + + This function defines and lays out all the Streamlit widgets for loading, + editing, and versioning existing prompts. + """ + st.subheader("1. Load Prompt") + if st.button("Refresh List"): + with st.spinner("Refreshing..."): + st.session_state.local_prompt.refresh_prompt_cache() + st.toast("Prompt list refreshed.") + + col1, col2 = st.columns(2) + with col1: + selected_prompt_name = st.selectbox( + "Select Existing Prompt", + options=st.session_state.local_prompt.existing_prompts.keys(), + placeholder="Select Prompt...", + key="selected_prompt", + help="Choose the prompt you want to load.", + ) + + with col2: + versions = [] + if selected_prompt_name: + try: + prompt_id = st.session_state.local_prompt.existing_prompts[ + selected_prompt_name + ] + versions = [v.version_id for v in prompts.list_versions(prompt_id)] + except Exception as e: + st.error(f"Could not fetch versions: {e}") + st.selectbox( + "Select Version", + options=versions, + placeholder="Select Version...", + key="selected_version", + help="Choose the specific version to load.", + ) + + st.button( + "Load Prompt", + on_click=_handle_load_prompt, + use_container_width=True, + type="primary", + ) + + st.divider() + + st.subheader("2. Edit Prompt Details") + st.text_input("Prompt Name", key="edit_prompt_name", disabled=True) + st.text_area("Prompt Text", key="edit_prompt_data", height=150) + st.text_input("Model Name", key="edit_model_name") + st.text_area("System Instructions", key="edit_system_instructions", height=300) + st.multiselect("Prompt Task", options=AVAILABLE_PROMPT_TASKS, key="edit_meta_tags") + + col_schema, col_config = st.columns(2) + with col_schema: + st.text_area("Response Schema (JSON)", key="edit_response_schema", height=200) + with col_config: + st.text_area( + "Generation Config (JSON)", key="edit_generation_config", height=200 + ) + + if st.button( + "Save as New Version", type="primary", use_container_width=True, key="save_edit" + ): + _handle_save_edited_prompt() + + st.divider() + + st.subheader("3. Test Your Prompt") + st.text_area("Sample User Input (JSON)", key="edit_sample_user_input", height=150) + + if st.button("Generate Test Response", use_container_width=True, key="test_edit"): + _handle_generate_test_for_edit() + + st.text_area( + "Test Output", + key="edit_sample_output", + height=150, + placeholder="The model's response will be displayed here.", + disabled=True, + ) + + +# --- Main Application --- +def main() -> None: + """Renders the main Prompt Management page. + + Sets the page configuration, initializes the session state (including the + `gcp_prompt` object and UI field defaults), and renders the main title + and tabbed layout for creating and editing prompts. + """ + st.set_page_config( + layout="wide", + page_title="Prompt Management", + page_icon="assets/favicon.ico", + ) + + # Initialize session state object and UI fields + if "local_prompt" not in st.session_state: + st.session_state.local_prompt = gcp_prompt() + + ui_fields = { + "new_prompt_name": "", + "new_prompt_data": "", + "new_model_name": "", + "new_system_instructions": "", + "new_response_schema": "", + "new_generation_config": "", + "new_meta_tags": [], + "new_sample_user_input": "", + "new_sample_output": "", + "edit_prompt_name": "", + "edit_prompt_data": "", + "edit_model_name": "", + "edit_system_instructions": "", + "edit_response_schema": "", + "edit_generation_config": "", + "edit_meta_tags": [], + "edit_sample_user_input": "", + "edit_sample_output": "", + } + for field, default_val in ui_fields.items(): + if field not in st.session_state: + st.session_state[field] = default_val + + st.title("Prompt Management") + st.markdown( + "Create new prompts or load, edit, and test existing ones from the Prompt Management service." + ) + st.divider() + + # Use st.radio to create stateful tabs that persist across reruns. + # This prevents the UI from resetting to the first tab on every interaction. + selected_tab = st.radio( + "Select Action", + ["Create New Prompt", "Load & Edit Prompt"], + key="prompt_management_tab", + horizontal=True, + label_visibility="collapsed", + ) + + if selected_tab == "Create New Prompt": + render_create_tab() + elif selected_tab == "Load & Edit Prompt": + render_edit_tab() + + st.caption("LLM EvalKit | Prompt Management") + + +if __name__ == "__main__": + main() diff --git a/tools/llmevalkit/pages/2_Dataset_Creation.py b/tools/llmevalkit/pages/2_Dataset_Creation.py new file mode 100644 index 0000000..f84df2a --- /dev/null +++ b/tools/llmevalkit/pages/2_Dataset_Creation.py @@ -0,0 +1,250 @@ +# Copyright 2025 Google LLC +# 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 +# https://www.apache.org/licenses/LICENSE-2.0 +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language + +"""Streamlit page for creating and managing datasets in Google Cloud Storage.""" + +import logging +import os + +import streamlit as st +from dotenv import load_dotenv +from google.cloud import storage +from streamlit.runtime.uploaded_file_manager import UploadedFile + +# Load environment variables from .env file +load_dotenv("src/.env") + +# Configure logging to the console +logging.basicConfig( + level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s" +) +logger = logging.getLogger(__name__) + + +@st.cache_data(ttl=300) +def get_existing_datasets( + _storage_client: storage.Client, bucket_name: str +) -> list[str]: + """Lists 'directories' in GCS under the 'datasets/' prefix. + These directories represent the existing datasets. + """ + if not bucket_name or not _storage_client: + return [] + + bucket = _storage_client.bucket(bucket_name) + prefix = "datasets/" + retrieved_prefixes = set() + + try: + # Explicitly iterate through pages for robustness. + iterator = bucket.list_blobs(prefix=prefix, delimiter="/") + for page in iterator.pages: + retrieved_prefixes.update(page.prefixes) + + # The retrieved prefixes are the "subdirectories". + # e.g., {'datasets/my_dataset_1/', 'datasets/my_dataset_2/'} + dir_names = [] + for p in retrieved_prefixes: + # Extract 'my_dataset_1' from 'datasets/my_dataset_1/' + name = p[len(prefix) :].strip("/") + if name: + dir_names.append(name) + logger.info(f"Found datasets: {dir_names}") + return sorted(dir_names) + except Exception as e: + st.error(f"Error listing datasets from GCS: {e}") + logger.error(f"Error in get_existing_datasets: {e}", exc_info=True) + return [] + + +def _handle_upload( + storage_client: storage.Client, + bucket_name: str, + dataset_name: str, + uploaded_file: UploadedFile, +) -> None: + """Handles the logic of uploading a file to GCS.""" + if not all([storage_client, bucket_name, dataset_name, uploaded_file]): + st.warning("Missing required information for upload.") + return + + try: + file_name = uploaded_file.name + content_type = "text/plain" # Default + if file_name.endswith(".csv"): + content_type = "text/csv" + elif file_name.endswith(".json"): + content_type = "application/json" + elif file_name.endswith(".jsonl"): + content_type = "application/x-jsonlines" + + blob_path = f"datasets/{dataset_name}/{uploaded_file.name}" + bucket = storage_client.bucket(bucket_name) + blob = bucket.blob(blob_path) + + with st.spinner(f"Uploading '{uploaded_file.name}' to '{dataset_name}'..."): + blob.upload_from_string(uploaded_file.getvalue(), content_type=content_type) + + st.success( + f"Successfully uploaded '{uploaded_file.name}' to dataset '{dataset_name}'!" + ) + logger.info(f"Uploaded file to gs://{bucket_name}/{blob_path}") + # Clear the cache for get_existing_datasets to reflect the new dataset if created + get_existing_datasets.clear() + st.rerun() + except Exception as e: + st.error(f"Failed to upload file: {e}") + logger.error(f"Error during GCS upload: {e}", exc_info=True) + + +def _ensure_datasets_folder_exists( + storage_client: storage.Client, bucket_name: str +) -> None: + """Ensures the 'datasets/' folder exists by creating a placeholder object if needed. + + This helps it appear in the GCS UI even when empty. + """ + if not storage_client or not bucket_name: + return + try: + bucket = storage_client.bucket(bucket_name) + blob = bucket.blob("datasets/") + if not blob.exists(): + blob.upload_from_string("", content_type="application/x-directory") + logger.info( + f"Created placeholder for 'datasets/' folder in bucket '{bucket_name}'." + ) + except Exception as e: + # This is not a critical failure, so just log a warning. + logger.warning(f"Could not ensure 'datasets/' folder exists: {e}") + + +def main() -> None: + """Renders the Dataset Creation page.""" + st.set_page_config( + layout="wide", page_title="Dataset Management", page_icon="assets/favicon.ico" + ) + + # --- Initialize Session State & GCS Client --- + if "storage_client" not in st.session_state: + try: + st.session_state.storage_client = storage.Client() + except Exception as e: + st.error(f"Could not connect to Google Cloud Storage: {e}") + st.stop() + + BUCKET_NAME = os.getenv("BUCKET") + if not BUCKET_NAME: + st.error("BUCKET environment variable is not set. Please configure it in .env.") + st.stop() + + # Ensure the base 'datasets/' folder exists for UI consistency + _ensure_datasets_folder_exists(st.session_state.storage_client, BUCKET_NAME) + + st.title("Dataset Management") + st.markdown( + "Create new datasets or upload files (CSV, JSON, or JSONL) to existing ones. " + "A 'Dataset' is a folder in your GCS bucket used to group related evaluation files." + ) + st.divider() + + # --- Section 1: Upload File --- + st.subheader("1. Upload a File") + + existing_datasets = get_existing_datasets( + st.session_state.storage_client, BUCKET_NAME + ) + + # Let user choose whether to create a new dataset or add to an existing one + upload_mode = st.radio( + "Choose an action:", + ("Create a new dataset", "Add to an existing dataset"), + key="upload_mode", + horizontal=True, + ) + + dataset_name = "" + if upload_mode == "Create a new dataset": + dataset_name = st.text_input( + "Enter a name for the new dataset:", + key="new_dataset_name", + help="Use a descriptive name, e.g., 'sentiment_analysis_v1'.", + ) + else: + dataset_name = st.selectbox( + "Select an existing dataset:", + options=existing_datasets, + key="selected_dataset_for_upload", + help="Choose the dataset folder to upload your file into.", + index=None, + placeholder="Select a dataset...", + ) + + uploaded_file = st.file_uploader( + "Select a file to upload", + type=["csv", "json", "jsonl"], + key="file_uploader", + ) + + if st.button("Upload to Cloud Storage", type="primary", use_container_width=True): + if not dataset_name: + st.warning("Please provide or select a dataset name.") + elif not uploaded_file: + st.warning("Please select a file to upload.") + else: + _handle_upload( + st.session_state.storage_client, + BUCKET_NAME, + dataset_name, + uploaded_file, + ) + + st.divider() + + # --- Section 2: View Existing Datasets --- + st.subheader("2. View Existing Datasets") + + with st.expander("Browse datasets and their contents", expanded=True): + selected_dataset_to_view = st.selectbox( + "Select a dataset to view its contents:", + options=existing_datasets, + key="selected_dataset_for_view", + index=None, + placeholder="Select a dataset...", + ) + + if selected_dataset_to_view: + prefix = f"datasets/{selected_dataset_to_view}/" + blobs = st.session_state.storage_client.list_blobs( + BUCKET_NAME, prefix=prefix + ) + filenames = [ + os.path.basename(b.name) + for b in blobs + if b.name.endswith((".csv", ".json", ".jsonl")) + ] + + if filenames: + st.write(f"**Files in '{selected_dataset_to_view}':**") + st.text_area( + "Files", + value="\n".join(filenames), + height=150, + disabled=True, + label_visibility="collapsed", + ) + else: + st.info(f"No files found in the '{selected_dataset_to_view}' dataset.") + + st.caption("LLM EvalKit | Dataset Management") + + +if __name__ == "__main__": + main() diff --git a/tools/llmevalkit/pages/3_Evaluation.py b/tools/llmevalkit/pages/3_Evaluation.py new file mode 100644 index 0000000..6b66c45 --- /dev/null +++ b/tools/llmevalkit/pages/3_Evaluation.py @@ -0,0 +1,1043 @@ +# Copyright 2025 Google LLC +# +# 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 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import datetime +import json +import logging +import os +import re + +import pandas as pd +import streamlit as st +import vertexai +from dotenv import load_dotenv +from google import genai +from google.cloud import storage +from pydantic import BaseModel +from src.gcp_prompt import GcpPrompt as gcp_prompt +from vertexai.evaluation import ( + EvalTask, + MetricPromptTemplateExamples, + PairwiseMetricPromptTemplate, + PointwiseMetricPromptTemplate, +) +from vertexai.preview import prompts + +load_dotenv("src/.env") + + +logging.basicConfig( + level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s" +) +logger = logging.getLogger(__name__) + + +def get_metric_object_by_name(metric_name: str): + """Retrieves a metric template object from its string name.""" + try: + return MetricPromptTemplateExamples._PROMPT_TEMPLATE_MAP[metric_name] + except Exception as e: + logger.exception(f"Failed to get metric object for {metric_name}: {e}") + raise + + +def refresh_bucket() -> list[str]: + """Refreshes the list of available dataset URIs from the GCS bucket. + + This function lists all blobs in the configured GCS bucket, filters for + CSV and JSONL files located within the 'datasets/' prefix, and constructs a list + of their full gs:// URI paths. + + Returns: + A list of strings, where each string is a GCS URI to a dataset file. + """ + logger.info("Bucket: %s", os.getenv("BUCKET")) + bucket = st.session_state.storage_client.bucket(os.getenv("BUCKET")) + blobs = bucket.list_blobs() + data_uris = [] + for i in blobs: + if i.name.split("/")[0] == "datasets" and ( + i.name.endswith(".csv") or i.name.endswith(".jsonl") + ): + data_uris.append(f"gs://{i.bucket.name}/{i.name}") + logger.info("Data URIs: %s", data_uris) + return data_uris + + +def get_autorater_pairwise_response(metric_prompt: str, model: str) -> dict: + """Gets a response from the autorater model for pairwise evaluation. + + Args: + metric_prompt: The prompt to send to the autorater model. + model: The name of the evaluation model to use. + + Returns: + A dictionary containing the autorater's response. + """ + + class AutoraterResponse(BaseModel): + pairwise_choice: str + explanation: str + + client = genai.Client( + vertexai=True, + project=os.getenv("PROJECT_ID"), + location=os.getenv("LOCATION"), + ) + + response = client.models.generate_content( + model=model, + contents=metric_prompt, + config={ + "response_mime_type": "application/json", + "response_schema": AutoraterResponse, + "safety_settings": [ + {"category": "HARM_CATEGORY_HATE_SPEECH", "threshold": "BLOCK_NONE"}, + { + "category": "HARM_CATEGORY_DANGEROUS_CONTENT", + "threshold": "BLOCK_NONE", + }, + { + "category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", + "threshold": "BLOCK_NONE", + }, + {"category": "HARM_CATEGORY_HARASSMENT", "threshold": "BLOCK_NONE"}, + ], + }, + ) + + if response.parsed: + return response.parsed.model_dump() + + try: + return json.loads(response.text) + except Exception as e: + logger.warning(f"Failed to parse response: {e}") + return {} + + +def format_display_text(text): + """Formats text for display in st.text_area, handling dicts/lists as JSON.""" + if isinstance(text, (dict, list)): + try: + return json.dumps(text, indent=2) + except (TypeError, ValueError): + return str(text) + return str(text) + + +def main() -> None: + """Initializes and runs the Streamlit evaluation application. + + This function sets up the Streamlit page configuration, initializes session state + variables, and orchestrates the user interface for the evaluation workflow. + It handles dataset and prompt selection, response generation or loading, + human-in-the-loop rating, and the launching of automated evaluations. + """ + st.set_page_config( + layout="wide", + page_title="Prompt Engineering App", + page_icon="assets/favicon.ico", + ) + + st.header("Evaluation Set-Up") + + if "storage_client" not in st.session_state: + st.session_state["storage_client"] = storage.Client() + + if "data_uris" not in st.session_state: + st.session_state["data_uris"] = refresh_bucket() + + if "current_index" not in st.session_state: + st.session_state.current_index = 0 + + if "eval_result" not in st.session_state: + st.session_state.eval_result = None + + if "custom_eval_result" not in st.session_state: + st.session_state.custom_eval_result = None + + if "df_data" not in st.session_state: + st.session_state.df_data = None + + if "df_dataset_eval" not in st.session_state: + st.session_state.df_dataset_eval = None + + if "all_metrics_eval" not in st.session_state: + st.session_state.all_metrics_eval = None + + if "metrics" not in st.session_state: + st.session_state.metrics = None + + if "local_prompt" not in st.session_state: + st.session_state.local_prompt = gcp_prompt() + + if "cached_data_files" not in st.session_state: + st.session_state.cached_data_files = {} + if "last_selected_dataset_for_cache" not in st.session_state: + st.session_state.last_selected_dataset_for_cache = None + + if "cached_prompt_versions" not in st.session_state: + st.session_state.cached_prompt_versions = {} + + if "last_selected_prompt_for_versions_cache" not in st.session_state: + st.session_state.last_selected_prompt_for_versions_cache = None + + if "human_rated_dict" not in st.session_state: + st.session_state.human_rated_dict = {} + + if "metric_preview_index" not in st.session_state: + st.session_state.metric_preview_index = 0 + + if "vertex_session_init" not in st.session_state: + vertexai.init( + project=os.getenv("PROJECT_ID"), + location=os.getenv("LOCATION"), + staging_bucket=os.getenv("BUCKET"), + experiment=os.getenv("EXPERIMENT_NAME"), + ) + st.session_state.vertex_session_init = True + + data_sets = list({i.split("/")[4] for i in st.session_state.data_uris}) + logger.info(f"Data Sets: {data_sets}") + st.selectbox( + "Select an Existing Dataset", options=[None, *data_sets], key="selected_dataset" + ) + + files_to_display_in_selectbox = [] + if st.session_state.selected_dataset: + if ( + st.session_state.selected_dataset + != st.session_state.last_selected_dataset_for_cache + or st.session_state.selected_dataset + not in st.session_state.cached_data_files + ): + logger.info( + "Cache miss or dataset changed for files. Fetching for: %s", + st.session_state.selected_dataset, + ) + bucket = st.session_state.storage_client.bucket(os.getenv("BUCKET")) + prefix = f"datasets/{st.session_state.selected_dataset}/" + blobs_iterator = bucket.list_blobs(prefix=prefix) + + current_dataset_files = [] + for blob in blobs_iterator: + if ( + blob.name.endswith(".csv") or blob.name.endswith(".jsonl") + ) and not blob.name.endswith("/"): + filename = blob.name[len(prefix) :] + if filename: + current_dataset_files.append(filename) + + st.session_state.cached_data_files[st.session_state.selected_dataset] = ( + sorted(set(current_dataset_files)) + ) + st.session_state.last_selected_dataset_for_cache = ( + st.session_state.selected_dataset + ) + logger.info( + "Cached files for %s: %s", + st.session_state.selected_dataset, + st.session_state.cached_data_files[st.session_state.selected_dataset], + ) + + if "selected_file_from_dataset" in st.session_state: + st.session_state.selected_file_from_dataset = None + logger.info("Reset selected_file_from_dataset due to dataset change.") + + files_to_display_in_selectbox = st.session_state.cached_data_files.get( + st.session_state.selected_dataset, [] + ) + + st.selectbox( + "Select a file from this dataset:", + options=[None, *files_to_display_in_selectbox], + key="selected_file_from_dataset", + ) + + st.text_input("Number of Samples", key="n_samples") + + st.text_input( + "Ground Truth Column Name", + key="ground_truth_column_name", + value="target", + help="The name of the column in your dataset that contains the ground truth or target response.", + ) + + st.selectbox( + "Select Existing Prompt", + options=[None, *list(st.session_state.local_prompt.existing_prompts.keys())], + placeholder="Select Prompt...", + key="selected_prompt", + ) + + versions_to_display_in_selectbox = [] + if st.session_state.selected_prompt: + st.session_state.local_prompt.prompt_meta["name"] = ( + st.session_state.selected_prompt + ) + + selected_prompt_obj = st.session_state.local_prompt.existing_prompts[ + st.session_state.selected_prompt + ] + prompt_resource_name_for_cache = str(selected_prompt_obj) + + if ( + st.session_state.selected_prompt + != st.session_state.last_selected_prompt_for_versions_cache + or prompt_resource_name_for_cache + not in st.session_state.cached_prompt_versions + ): + logger.info( + "Cache miss or prompt changed for versions. Fetching for: %s", + st.session_state.selected_prompt, + ) + fetched_versions = [ + v.version_id for v in prompts.list_versions(selected_prompt_obj) + ] + + st.session_state.cached_prompt_versions[prompt_resource_name_for_cache] = ( + fetched_versions + ) + st.session_state.last_selected_prompt_for_versions_cache = ( + st.session_state.selected_prompt + ) + logger.info( + "Cached versions for %s: %s", + st.session_state.selected_prompt, + fetched_versions, + ) + + if "selected_version" in st.session_state: + st.session_state.selected_version = None + logger.info("Reset selected_version due to prompt change.") + versions_to_display_in_selectbox = st.session_state.cached_prompt_versions.get( + prompt_resource_name_for_cache, [] + ) + + st.selectbox( + "Select Version", + options=versions_to_display_in_selectbox, + placeholder="Select Version...", + key="selected_version", + ) + + st.button("Load Prompt", key="load_prompt_button") + if st.session_state.load_prompt_button: + if not st.session_state.get("selected_prompt"): + st.warning("Please select a prompt before loading.") + else: + logger.info( + f"Selected Prompt ID: {st.session_state.local_prompt.existing_prompts[st.session_state.selected_prompt]}" + ) + logger.info(f"Version: {st.session_state.selected_version}") + st.session_state.local_prompt.load_prompt( + st.session_state.local_prompt.existing_prompts[ + st.session_state.selected_prompt + ], + st.session_state.selected_prompt, + st.session_state.selected_version, + ) + logger.info( + f"Local Prompt Meta: {st.session_state.local_prompt.prompt_meta}" + ) + logger.info( + f"Local Prompt Meta Dict Keys: {st.session_state.local_prompt.prompt_meta.keys()}" + ) + + st.session_state.prompt_name = ( + st.session_state.local_prompt.prompt_to_run.prompt_name + ) + st.session_state.prompt_data = ( + st.session_state.local_prompt.prompt_to_run.prompt_data + ) + st.session_state.model_name = ( + st.session_state.local_prompt.prompt_to_run.model_name.split("/")[-1] + ) + st.session_state.system_instructions = ( + st.session_state.local_prompt.prompt_to_run.system_instruction + ) + st.session_state.response_schema = json.dumps( + st.session_state.local_prompt.prompt_meta.get("response_schema", {}) + ) + st.session_state.generation_config = json.dumps( + st.session_state.local_prompt.prompt_meta.get("generation_config", {}) + ) + st.session_state.meta_tags = st.session_state.local_prompt.prompt_meta[ + "meta_tags" + ] + + st.button("Upload Data and Get Responses", key="upload_data_get_responses_button") + + if ( + st.session_state.upload_data_get_responses_button + and st.session_state.n_samples + and st.session_state.selected_dataset + ): + if not st.session_state.n_samples: + st.warning("Please enter the Number of Samples.") + return + if not st.session_state.selected_dataset: + st.warning("Please select an Existing Dataset.") + return + if not st.session_state.selected_file_from_dataset: + st.warning("Please select a file from the dataset.") + return + + try: + num_samples = int(st.session_state.n_samples) + if num_samples <= 0: + st.warning("Number of Samples must be a positive integer.") + return + except ValueError: + st.warning("Number of Samples must be a valid integer.") + return + + gcs_path = f"gs://{os.getenv('BUCKET')}/datasets/{st.session_state.selected_dataset}/{st.session_state.selected_file_from_dataset}" + st.session_state["input_data_uri"] = gcs_path + try: + if gcs_path.endswith(".csv"): + df_full = pd.read_csv(gcs_path) + elif gcs_path.endswith(".jsonl"): + df_full = pd.read_json(gcs_path, lines=True) + else: + st.error( + f"Unsupported file type: {gcs_path.rsplit('.', maxsplit=1)[-1]}" + ) + return + except Exception as e: + st.error(f"Error reading data from {gcs_path}: {e}") + return + + df = df_full.iloc[:num_samples] + if df.empty: + st.warning( + "No data found for the first %s samples in %s, or the file is smaller than requested.", + num_samples, + st.session_state.selected_file_from_dataset, + ) + st.session_state.human_rated_dict = {} + st.session_state.ratings = [] + st.session_state.include_in_evaluations = [] + st.session_state.current_index = 0 + return + + user_input_list = [] + expected_result_list = [] + assistant_response_list = [] + baseline_model_response_list = [] + + generate = False + ground_truth_col = st.session_state.ground_truth_column_name + required_cols_for_loading_existing = [ + "user_input", + ground_truth_col, + "assistant_response", + ] + + if "assistant_response" in df.columns: + missing_loading_cols = [ + col + for col in required_cols_for_loading_existing + if col not in df.columns + ] + if not missing_loading_cols: + logger.info("Sufficient columns found to load existing responses.") + generate = False + else: + st.error( + f"The file has 'assistant_response' column, but is missing other essential columns for loading: {missing_loading_cols}. Required for loading: {required_cols_for_loading_existing}. Found columns: {df.columns.tolist()}", + ) + st.session_state.human_rated_dict = {} + st.session_state.ratings = [] + st.session_state.include_in_evaluations = [] + st.session_state.current_index = 0 + return + else: + if not st.session_state.get("prompt_data"): + st.error( + "To generate new responses, please load a prompt first using the 'Load Prompt' button." + ) + st.session_state.human_rated_dict = {} + st.session_state.ratings = [] + st.session_state.include_in_evaluations = [] + st.session_state.current_index = 0 + return + + template_vars = re.findall(r"{(\w+)}", st.session_state.prompt_data) + required_cols_for_generating_new = list(set(template_vars)) + + all_required_cols = [*required_cols_for_generating_new, ground_truth_col] + missing_generating_cols = [ + col for col in all_required_cols if col not in df.columns + ] + + if not missing_generating_cols: + logger.info( + "'assistant_response' column not found. Required columns for generating new responses are present. Will generate." + ) + generate = True + else: + st.error( + f"The file does not have 'assistant_response' column, and is also missing columns required for generating new responses based on the loaded prompt: {missing_generating_cols}. Required for generation: {all_required_cols}. Found columns: {df.columns.tolist()}", + ) + st.session_state.human_rated_dict = {} + st.session_state.ratings = [] + st.session_state.include_in_evaluations = [] + st.session_state.current_index = 0 + return + + logger.info("Generate flag set to: %s for %s samples.", generate, len(df)) + + if generate: + logger.info("Proceeding with generating new assistant responses.") + if ( + not st.session_state.selected_prompt + or not st.session_state.selected_version + ): + st.error( + "A prompt and version must be loaded to generate new responses. Please use the 'Load Prompt' button." + ) + st.session_state.human_rated_dict = {} + st.session_state.ratings = [] + st.session_state.include_in_evaluations = [] + st.session_state.current_index = 0 + return + if not st.session_state.local_prompt.prompt_to_run.prompt_data: + st.error( + "Prompt data is missing from the loaded prompt. Cannot generate. Please re-load the prompt using 'Load Prompt'." + ) + st.session_state.human_rated_dict = {} + st.session_state.ratings = [] + st.session_state.include_in_evaluations = [] + st.session_state.current_index = 0 + return + + import concurrent.futures + + template_vars = re.findall(r"{(\w+)}", st.session_state.prompt_data) + required_cols_for_generating_new = list(set(template_vars)) + + tasks = [] + for idx, r in df.iterrows(): + current_user_input_item = { + col: r[col] for col in required_cols_for_generating_new + } + expected_res = r[st.session_state.ground_truth_column_name] + tasks.append((idx, current_user_input_item, expected_res)) + + # Bind the function reference to avoid accessing st.session_state directly in threads + generate_function = st.session_state.local_prompt.generate_response + + def process_task(task_args): + t_idx, item, expected = task_args + try: + res = generate_function(item) + return t_idx, item, expected, res, None + except Exception as e: + return t_idx, item, expected, None, e + + completed = 0 + total = len(tasks) + results = [] + + if total > 0: + progress_bar = st.progress( + 0, text="Generating responses in parallel..." + ) + + with concurrent.futures.ThreadPoolExecutor(max_workers=10) as executor: + future_to_idx = { + executor.submit(process_task, t): t[0] for t in tasks + } + for future in concurrent.futures.as_completed(future_to_idx): + t_idx, item, expected, res, error = future.result() + completed += 1 + + progress_text = f"Generating response {completed} of {total}..." + progress_bar.progress(completed / total, text=progress_text) + + if error: + logger.error( + "Error generating response for row index %s: %s", + t_idx, + error, + ) + else: + results.append((t_idx, item, expected, res)) + + progress_bar.empty() + if len(results) < total: + st.warning( + f"Generated {len(results)} responses. {total - len(results)} failed." + ) + + results.sort(key=lambda x: x[0]) + for t_idx, item, expected, res in results: + user_input_list.append(item) + expected_result_list.append(expected) + assistant_response_list.append(res) + + if len(user_input_list) < len(df): + st.info( + f"Successfully generated responses for {len(user_input_list)} out of {len(df)} requested samples due to errors during generation." + ) + else: + logger.info( + "Proceeding with loading existing assistant responses from file." + ) + parsed_user_inputs_temp = [] + for item_str in df["user_input"].astype(str).tolist(): + try: + parsed_user_inputs_temp.append(json.loads(item_str)) + except json.JSONDecodeError: + logger.debug( + "User input item is not valid JSON, using as raw string: %s", + item_str[:100], + ) + parsed_user_inputs_temp.append(item_str) + user_input_list = parsed_user_inputs_temp + expected_result_list = df[ + st.session_state.ground_truth_column_name + ].tolist() + assistant_response_list = df.assistant_response.tolist() + baseline_model_response_list = [] + if "baseline_model_response" in df.columns: + baseline_model_response_list = df.baseline_model_response.tolist() + + st.session_state.human_rated_dict = { + "user_input": user_input_list, + "ground_truth": expected_result_list, + "assistant_response": assistant_response_list, + } + if baseline_model_response_list: + st.session_state.human_rated_dict["baseline_model_response"] = ( + baseline_model_response_list + ) + num_processed_items = len(user_input_list) + + if num_processed_items > 0: + st.session_state.include_in_evaluations = [True] * num_processed_items + st.session_state.current_index = 0 + st.success(f"Successfully processed {num_processed_items} samples.") + else: + st.warning( + "No data items were processed successfully. Check logs for errors or review file structure." + ) + st.session_state.human_rated_dict = {} + st.session_state.include_in_evaluations = [] + st.session_state.current_index = 0 + + st.divider() + + if st.session_state.human_rated_dict: + st.title("Review Responses") + + col1, col2, col3 = st.columns(3) + + with col1: + st.subheader("User Input") + st.text_area( + label="User's original query/text", + value=format_display_text( + st.session_state.human_rated_dict["user_input"][ + st.session_state.current_index + ] + ), + height=200, + disabled=True, + ) + + with col2: + st.subheader("Ground Truth") + st.text_area( + label="The ideal/target response", + value=format_display_text( + st.session_state.human_rated_dict["ground_truth"][ + st.session_state.current_index + ] + ), + height=200, + disabled=True, + ) + + with col3: + st.subheader("Assistant Response") + st.text_area( + label="The assistant's generated response", + value=format_display_text( + st.session_state.human_rated_dict["assistant_response"][ + st.session_state.current_index + ] + ), + height=200, + disabled=True, + ) + + eval_include = st.checkbox( + "Include in Evaluation", + value=st.session_state.include_in_evaluations[ + st.session_state.current_index + ], + ) + + if ( + st.session_state.include_in_evaluations + and eval_include + != st.session_state.include_in_evaluations[st.session_state.current_index] + ): + st.session_state.include_in_evaluations[st.session_state.current_index] = ( + eval_include + ) + + st.markdown("---") + col_prev, col_spacer, col_next = st.columns([1, 3, 1]) + + with col_prev: + if st.button("Previous", disabled=(st.session_state.current_index == 0)): + st.session_state.current_index -= 1 + st.rerun() + + with col_next: + if st.button( + "Next", + disabled=( + st.session_state.current_index + == len(st.session_state.human_rated_dict["user_input"]) - 1 + ), + ): + st.session_state.current_index += 1 + st.rerun() + + st.markdown( + f"

    Case {st.session_state.current_index + 1} of {len(st.session_state.human_rated_dict['user_input'])}

    ", + unsafe_allow_html=True, + ) + + st.markdown("---") + st.subheader("Launch Eval") + + st.subheader("Metrics Selection") + + col1, col2 = st.columns(2) + + with col1: + st.write("**Model-Based**") + metric_names = MetricPromptTemplateExamples.list_example_metric_names() + selected_model_based_metrics = st.multiselect( + "Select from model-based metrics", + metric_names, + key="selected_model_based_metrics", + label_visibility="collapsed", + ) + if selected_model_based_metrics: + num_metrics = len(selected_model_based_metrics) + if st.session_state.metric_preview_index >= num_metrics: + st.session_state.metric_preview_index = 0 + + current_metric_name = selected_model_based_metrics[ + st.session_state.metric_preview_index + ] + + st.markdown( + f"**Previewing Template: {current_metric_name} ({st.session_state.metric_preview_index + 1}/{num_metrics})**" + ) + + try: + metric_object = get_metric_object_by_name(current_metric_name) + if isinstance( + metric_object, + PointwiseMetricPromptTemplate | PairwiseMetricPromptTemplate, + ): + st.text_area( + "Template Preview", + metric_object.metric_prompt_template, + height=200, + ) + except Exception as e: + st.error( + f"Could not retrieve template for {current_metric_name}: {e}" + ) + + if num_metrics > 1: + prev_col, next_col = st.columns(2) + with prev_col: + if st.button( + "Previous Template", + disabled=st.session_state.metric_preview_index <= 0, + ): + st.session_state.metric_preview_index -= 1 + st.rerun() + with next_col: + if st.button( + "Next Template", + disabled=st.session_state.metric_preview_index + >= num_metrics - 1, + ): + st.session_state.metric_preview_index += 1 + st.rerun() + + with col2: + st.write("**Computation-Based Pointwise**") + computation_based_pointwise = [ + "bleu", + "rouge_1", + "rouge_2", + "rouge_l", + "rouge_l_sum", + "exact_match", + ] + st.multiselect( + "Select from computation-based pointwise metrics", + computation_based_pointwise, + key="selected_cbp", + label_visibility="collapsed", + ) + + st.selectbox( + "Select Evaluation Model", + options=[ + "gemini-2.0-flash-lite", + "gemini-2.5-flash", + "gemini-2.5-pro", + ], + key="selected_evaluation_model", + ) + + st.button("Launch Eval", key="launch_eval_button") + + if st.session_state.launch_eval_button: + selected_mbp_names = st.session_state.get( + "selected_model_based_metrics", [] + ) + selected_cbp_metrics = st.session_state.get("selected_cbp", []) + + all_metrics = selected_mbp_names + selected_cbp_metrics + + if not all_metrics: + st.warning("Please select at least one evaluation metric.") + return + + evaluation_data_list = [] + for idx, include_item in enumerate(st.session_state.include_in_evaluations): + if include_item: + user_input_values = st.session_state.human_rated_dict["user_input"][ + idx + ] + prompt_template = ( + st.session_state.local_prompt.prompt_to_run.prompt_data + ) + logger.info(f"Prompt template: {prompt_template}") + system_instruction = ( + st.session_state.local_prompt.prompt_to_run.system_instruction + ) + prediction = str( + st.session_state.human_rated_dict["assistant_response"][idx] + ) + + # Process reference value like in the old code + reference_val = st.session_state.human_rated_dict["ground_truth"][ + idx + ] + final_reference_str = "" + if isinstance(reference_val, int | float | bool): + final_reference_str = json.dumps({"value": reference_val}) + elif isinstance(reference_val, str): + try: + parsed_json = json.loads(reference_val) + if isinstance(parsed_json, int | float | bool): + final_reference_str = json.dumps({"value": parsed_json}) + else: + final_reference_str = reference_val + except json.JSONDecodeError: + final_reference_str = reference_val + elif isinstance(reference_val, dict | list): + final_reference_str = json.dumps(reference_val) + else: + final_reference_str = str(reference_val) + + # Extract all variables from the template + template_vars = re.findall(r"\{(\w+)\}", prompt_template) + + if template_vars: + # Template expects variables + if isinstance(user_input_values, dict): + # Ensure all expected vars are present, default to empty string if missing + formatted_input = { + v: user_input_values.get(v, "") for v in template_vars + } + instruction = prompt_template.format(**formatted_input) + # Input is not a dict but template expects vars + # Try to use the input as the first variable if there's only one, otherwise empty + elif len(template_vars) == 1: + instruction = prompt_template.format( + **{template_vars[0]: user_input_values} + ) + else: + instruction = prompt_template # Fallback + else: + # No variables in template + instruction = prompt_template + context = system_instruction if system_instruction else "" + prompt_str = ( + json.dumps(user_input_values) + if isinstance(user_input_values, dict) + else str(user_input_values) + ) + + eval_item = { + "context": context, + "instruction": instruction, + "prompt": prompt_str, + "prediction": prediction, + "reference": final_reference_str, + } + evaluation_data_list.append(eval_item) + + if not evaluation_data_list: + st.warning( + "No items were selected for evaluation. Please check the 'Include in Evaluation' checkboxes." + ) + return + + df_dataset = pd.DataFrame(evaluation_data_list) + st.session_state.df_dataset_eval = df_dataset + st.session_state.all_metrics_eval = all_metrics + logger.info(f"Evaluation DataFrame columns: {df_dataset.columns.tolist()}") + logger.info(f"Evaluation DataFrame head:\n{df_dataset.head()}") + + task = EvalTask( + dataset=df_dataset, + metrics=all_metrics, + experiment=os.getenv("EXPERIMENT_NAME"), + ) + st.session_state.eval_result = task.evaluate( + response_column_name="prediction", + baseline_model_response_column_name="reference", + ) + + st.markdown("---") + st.subheader("View Eval") + + st.button("View Evaluation Results", key="eval_results_button") + + if st.session_state.eval_result and st.session_state.eval_results_button: + print(st.session_state.eval_result.metrics_table) + st.dataframe(st.session_state.eval_result.metrics_table) + if st.session_state.eval_result: + st.markdown("---") + st.subheader("Summary Scores") + + mean_scores = {} + if ( + st.session_state.eval_result + and hasattr(st.session_state.eval_result, "metrics_table") + and not st.session_state.eval_result.metrics_table.empty + ): + for col in st.session_state.eval_result.metrics_table.columns: + if col.endswith("/score"): + scores = pd.to_numeric( + st.session_state.eval_result.metrics_table[col], + errors="coerce", + ) + if not scores.dropna().empty: + mean_scores[col] = scores.dropna().mean() + if mean_scores: + for metric, score in mean_scores.items(): + st.metric(label=f"Mean {metric}", value=f"{score:.2f}") + else: + st.metric( + label="Mean Automated Score", + value="N/A", + ) + + st.markdown("---") + st.subheader("Save to Prompt Records") + save_to_records = st.checkbox( + "I want to save the results of this evaluation to the prompt records.", + key="save_to_records_checkbox", + ) + if st.button("Save to Prompt Records", key="save_to_records_button"): + if save_to_records: + prompt_name = st.session_state.selected_prompt + prompt_version = st.session_state.selected_version + data_file = st.session_state.input_data_uri + + if "df_dataset_eval" in st.session_state: + data = st.session_state.df_dataset_eval.to_dict( + orient="records" + ) + else: + st.error( + "Evaluation data not found in session state. Please re-run evaluation." + ) + return + + if "all_metrics_eval" in st.session_state: + metrics = st.session_state.all_metrics_eval + else: + st.error( + "Metrics not found in session state. Please re-run evaluation." + ) + return + + scores_df = st.session_state.eval_result.metrics_table + scores = scores_df.to_dict(orient="records") + + mean_scores_to_save = {} + for col in scores_df.columns: + if col.endswith("/score"): + s = pd.to_numeric(scores_df[col], errors="coerce") + if not s.dropna().empty: + mean_scores_to_save[col] = s.dropna().mean() + + record_data = { + "prompt_name": prompt_name, + "prompt_version": prompt_version, + "data_file": data_file, + "metrics": metrics, + "mean_scores": mean_scores_to_save, + "scores": scores, + "evaluation_data": data, + "timestamp": datetime.datetime.now().isoformat(), + } + + try: + filename = f"record_{prompt_name}_v{prompt_version}_{datetime.datetime.now().strftime('%Y%m%d%H%M%S')}.json" + bucket = st.session_state.storage_client.bucket( + os.getenv("BUCKET") + ) + blob = bucket.blob(f"records/{filename}") + + json_data = json.dumps(record_data, indent=4) + blob.upload_from_string( + json_data, content_type="application/json" + ) + + gcs_path = f"gs://{os.getenv('BUCKET')}/records/{filename}" + + st.success( + f"Successfully saved to prompt records at: {gcs_path}" + ) + st.json(json_data) + except Exception as e: + st.error(f"Failed to save to GCS: {e}") + else: + st.warning( + "Please check the box to confirm you want to save to prompt records." + ) + + +if __name__ == "__main__": + main() diff --git a/tools/llmevalkit/pages/4_One_Click_Refiner.py b/tools/llmevalkit/pages/4_One_Click_Refiner.py new file mode 100644 index 0000000..1d5c6de --- /dev/null +++ b/tools/llmevalkit/pages/4_One_Click_Refiner.py @@ -0,0 +1,321 @@ +# Copyright 2025 Google LLC +# 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 +# https://www.apache.org/licenses/LICENSE-2.0 +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language + +"""Streamlit user interface for the One-Click Refiner. + +This page provides an interface to instantly upgrade a draft prompt into a +structured, production-ready instruction without managing any datasets. +""" + +import json +import logging + +import streamlit as st +from dotenv import load_dotenv +from src.gcp_prompt import GcpPrompt as gcp_prompt +from vertexai.generative_models import GenerationConfig, GenerativeModel +from vertexai.preview import prompts + +load_dotenv("src/.env") +logging.basicConfig( + level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s" +) +logger = logging.getLogger(__name__) + +# --- Prompt Templates --- +META_PROMPT_TEMPLATE = """You are an expert prompt engineer. Your goal is to improve the user's draft prompt and system instructions into highly structured, production-ready iterations. + +Ensure you include and follow these directives: +{custom_directives} + +Ensure tone relates to the optional requested Tone: {tone}. + +CRITICAL REQUIREMENTS: +- You MUST preserve all variable placeholders exactly as they appear (e.g., `{{{{query}}}}`, `{{{{target}}}}`). Note: the draft prompt might use curly brackets like `{{variable}}`. Do NOT strip them. +- You MUST preserve any multimodal tags exactly as they appear (e.g., `@@@image/jpeg`). Do not alter or remove image attachments. + +Draft System Instructions: +{draft_system_instructions} + +Draft Prompt: +{draft_prompt} + +You must respond in pure JSON format with exactly three keys: +1. "optimized_system_instruction": A single string containing the rewritten system instructions. +2. "optimized_prompt": A single string containing the fully rewritten structured prompt template. +3. "insights": A list of strings explaining exactly what you changed and why. +""" + +SUGGEST_DIRECTIVES_PROMPT = """Analyze the following draft prompt and system instructions. Suggest 3-5 specific prompt engineering best practices that would improve it. Focus on structure, constraints, format, clarity, and safety. +Return ONLY a markdown list of suggestions suitable to be used as instructions for another LLM prompt engineer. Do not include introductory text. + +Draft System Instructions: +{draft_system_instructions} + +Draft Prompt: +{draft_prompt} +""" + + +def initialize_session_state() -> None: + """Initializes needed session state variables.""" + if "local_prompt" not in st.session_state: + st.session_state.local_prompt = gcp_prompt() + if "ocr_directives" not in st.session_state: + st.session_state.ocr_directives = "1. Add a clear Role definition.\n2. Add specific Context to constrain the generator.\n3. Clarify output format expectations." + if "opt_sys" not in st.session_state: + st.session_state.opt_sys = "" + if "opt_prompt" not in st.session_state: + st.session_state.opt_prompt = "" + if "ocr_insights" not in st.session_state: + st.session_state.ocr_insights = None + + +def _handle_load_prompt(): + """Loads the selected prompt and version into the gcp_prompt object.""" + if not st.session_state.get("selected_prompt") or not st.session_state.get( + "selected_version" + ): + st.warning("Please select both a prompt and a version to load.") + return + prompt_name = st.session_state.selected_prompt + prompt_id = st.session_state.local_prompt.existing_prompts[prompt_name] + version_id = st.session_state.selected_version + try: + with st.spinner(f"Loading version '{version_id}' of prompt '{prompt_name}'..."): + st.session_state.local_prompt.load_prompt( + prompt_id, prompt_name, version_id + ) + st.success(f"Loaded prompt '{prompt_name}' (Version: {version_id}).") + + # Clear previous optimizations + st.session_state.opt_sys = "" + st.session_state.opt_prompt = "" + st.session_state.ocr_insights = None + except Exception as e: + logger.error("Failed to load prompt: %s", e, exc_info=True) + st.error(f"Failed to load prompt: {e}") + + +def _handle_auto_suggest(): + """Calls Agent Platform to automatically suggest prompt engineering directives.""" + sys_inst = st.session_state.local_prompt.prompt_to_run.system_instruction or "None" + prompt_data = st.session_state.local_prompt.prompt_to_run.prompt_data or "None" + + model_name = st.session_state.get("ocr_target_model", "gemini-2.5-pro") + if not model_name: + model_name = "gemini-2.5-pro" + + try: + model = GenerativeModel(model_name) + prompt_text = SUGGEST_DIRECTIVES_PROMPT.format( + draft_system_instructions=sys_inst, draft_prompt=prompt_data + ) + with st.spinner("Analyzing prompt and generating suggestions..."): + response = model.generate_content(prompt_text) + st.session_state.ocr_directives = response.text + except Exception as e: + logger.error("Error auto-suggesting directives: %s", e, exc_info=True) + st.error(f"Failed to generate suggestions: {e}") + + +def _handle_optimize(): + """Optimizes the loaded prompt using the meta-prompt and custom directives.""" + sys_inst = st.session_state.local_prompt.prompt_to_run.system_instruction or "None" + prompt_data = st.session_state.local_prompt.prompt_to_run.prompt_data or "None" + directives = st.session_state.get("ocr_directives", "") + tone = st.session_state.get("ocr_tone", "Professional") + + model_name = st.session_state.get("ocr_target_model", "gemini-2.5-pro") + if not model_name: + model_name = "gemini-2.5-pro" + + try: + model = GenerativeModel(model_name) + prompt_text = META_PROMPT_TEMPLATE.format( + custom_directives=directives, + tone=tone, + draft_system_instructions=sys_inst, + draft_prompt=prompt_data, + ) + with st.spinner("Optimizing..."): + response = model.generate_content( + prompt_text, + generation_config=GenerationConfig( + temperature=0.4, response_mime_type="application/json" + ), + ) + # Parse response + try: + res_obj = json.loads(response.text) + st.session_state.opt_sys = res_obj.get( + "optimized_system_instruction", "" + ) + st.session_state.opt_prompt = res_obj.get("optimized_prompt", "") + st.session_state.ocr_insights = res_obj.get("insights", []) + st.success("Optimization Complete!") + except json.JSONDecodeError as e: + st.error(f"Failed to parse optimization output as JSON: {e}") + logger.error("Raw response: %s", response.text) + except Exception as e: + logger.error("Error optimizing prompt: %s", e, exc_info=True) + st.error(f"Failed to optimize prompt: {e}") + + +def _handle_save_new_version(): + """Saves the optimized prompt to the backend registry as a new version.""" + prompt_obj = st.session_state.local_prompt + if not prompt_obj.prompt_to_run.prompt_name: + st.warning("No prompt is currently loaded to save.") + return + + prompt_obj.prompt_to_run.prompt_data = st.session_state.opt_prompt + prompt_obj.prompt_to_run.system_instruction = st.session_state.opt_sys + + try: + with st.spinner("Saving as new version..."): + prompt_obj.save_prompt(check_existing=False) + st.success("Successfully saved new optimized version to registry!") + prompt_obj.refresh_prompt_cache() + except Exception as e: + logger.error("Failed to save new version: %s", e, exc_info=True) + st.error(f"Failed to save prompt: {e}") + + +def main(): + """Renders the One-Click Refiner page layout.""" + st.set_page_config( + layout="wide", page_title="One-Click Refiner", page_icon="assets/favicon.ico" + ) + initialize_session_state() + + st.title("One-Click Refiner") + st.markdown( + "Instantly upgrade a draft prompt into a structured, production-ready instruction without managing any datasets." + ) + st.divider() + + # SECTION 1: Load Existing Prompt + st.subheader("1. Load Prompt") + + if st.button("Refresh List"): + with st.spinner("Refreshing..."): + st.session_state.local_prompt.refresh_prompt_cache() + st.toast("Prompt list refreshed.") + + col1, col2 = st.columns(2) + with col1: + selected_prompt_name = st.selectbox( + "Select Existing Prompt", + options=st.session_state.local_prompt.existing_prompts.keys(), + placeholder="Select Prompt...", + key="selected_prompt", + ) + with col2: + versions = [] + if selected_prompt_name: + try: + prompt_id = st.session_state.local_prompt.existing_prompts[ + selected_prompt_name + ] + versions = [v.version_id for v in prompts.list_versions(prompt_id)] + except Exception as e: + st.error(f"Could not fetch versions: {e}") + st.selectbox( + "Select Version", + options=versions, + placeholder="Select Version...", + key="selected_version", + ) + + st.button("Load Prompt", on_click=_handle_load_prompt, type="primary") + st.divider() + + p_data = st.session_state.local_prompt.prompt_to_run.prompt_data + if p_data: + # SECTION 2: Configuration + st.subheader("2. Configuration") + c1, c2 = st.columns(2) + with c1: + current_model = st.session_state.local_prompt.prompt_to_run.model_name + if current_model and "/" in current_model: + current_model = current_model.split("/")[-1] + st.text_input( + "Target Model", + value=current_model if current_model else "gemini-2.0-flash-001", + key="ocr_target_model", + ) + with c2: + st.selectbox( + "Tone", + options=[ + "Professional", + "Creative", + "Concise", + "Assertive", + "Friendly", + "None", + ], + key="ocr_tone", + ) + + st.markdown("**Optimization Directives**") + st.text_area( + "Modify the guidelines the optimizer should follow:", + key="ocr_directives", + height=120, + ) + st.button("✹ Auto-Suggest Directives", on_click=_handle_auto_suggest) + + st.button("🚀 Optimize Now", on_click=_handle_optimize, type="primary") + + st.divider() + + # SECTION 3: Review + st.subheader("3. Review") + + rev_c1, rev_c2 = st.columns(2) + with rev_c1: + st.markdown("### Original Draft") + st.text_area( + "System Instructions", + value=st.session_state.local_prompt.prompt_to_run.system_instruction + or "", + disabled=True, + height=200, + key="org_sys", + ) + st.text_area( + "Prompt Data", + value=p_data or "", + disabled=True, + height=200, + key="org_prompt", + ) + + with rev_c2: + st.markdown("### Optimized Result") + st.text_area("System Instructions", key="opt_sys", height=200) + st.text_area("Prompt Data", key="opt_prompt", height=200) + + if st.session_state.ocr_insights: + with st.expander("💡 Why this changed (Insights)", expanded=True): + for insight in st.session_state.ocr_insights: + st.markdown(f"- {insight}") + + st.divider() + st.subheader("4. Action") + st.button( + "Save as New Version", on_click=_handle_save_new_version, type="primary" + ) + + +if __name__ == "__main__": + main() diff --git a/tools/llmevalkit/pages/5_Performance_Tuner.py b/tools/llmevalkit/pages/5_Performance_Tuner.py new file mode 100644 index 0000000..8b33e83 --- /dev/null +++ b/tools/llmevalkit/pages/5_Performance_Tuner.py @@ -0,0 +1,604 @@ +# Copyright 2025 Google LLC +# +# 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 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Streamlit page for Performance Tuner (Prompt Optimization).""" + +import json +import logging +import os +from argparse import Namespace +from datetime import datetime + +import pandas as pd +import streamlit as st +from dotenv import load_dotenv +from etils import epath +from google.cloud import aiplatform, storage +from src import vapo_lib +from src.gcp_prompt import GcpPrompt as gcp_prompt +from vertexai.evaluation import MetricPromptTemplateExamples +from vertexai.preview import prompts + +load_dotenv("src/.env") + +logging.basicConfig( + level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s" +) +logger = logging.getLogger(__name__) + +TARGET_MODELS = [ + "gemini-2.5-pro", + "gemini-2.5-flash", + "gemini-2.5-flash-lite", + "gemini-2.0-flash", + "gemini-2.0-flash-001", + "gemini-2.0-flash-lite", + "gemini-2.0-flash-lite-001", +] + + +def initialize_session_state() -> None: + if "op_id" not in st.session_state: + st.session_state.op_id = vapo_lib.get_id() + + if "local_prompt" not in st.session_state: + st.session_state.local_prompt = gcp_prompt() + + if "storage_client" not in st.session_state: + st.session_state["storage_client"] = storage.Client() + + if "data_uris" not in st.session_state: + st.session_state["data_uris"] = refresh_bucket() + + if "dataset" not in st.session_state: + st.session_state["dataset"] = None + + if "cached_data_files" not in st.session_state: + st.session_state.cached_data_files = {} + if "last_selected_dataset_for_cache" not in st.session_state: + st.session_state.last_selected_dataset_for_cache = None + + if "tuner_launched_job" not in st.session_state: + st.session_state.tuner_launched_job = None + + if "tuner_run_uri" not in st.session_state: + st.session_state.tuner_run_uri = None + + if "tuner_winning_template" not in st.session_state: + st.session_state.tuner_winning_template = None + + +def refresh_bucket() -> list[str]: + logger.info("Bucket: %s", os.getenv("BUCKET")) + bucket = st.session_state.storage_client.bucket(os.getenv("BUCKET")) + blobs = bucket.list_blobs() + data_uris = [] + for i in blobs: + if i.name.split("/")[0] == "datasets" and ( + i.name.endswith(".csv") or i.name.endswith(".jsonl") + ): + data_uris.append(f"gs://{i.bucket.name}/{i.name}") + return data_uris + + +def get_optimization_args( + input_optimization_data_file_uri, + output_optimization_run_uri, + target_model, + selected_metrics, + target_qps=1.0, + optimizer_qps=1.0, + eval_qps=1.0, + data_limit=10, +): + response_schema_str = st.session_state.local_prompt.prompt_meta.get( + "response_schema", "{}" + ) + try: + response_schema = ( + json.loads(response_schema_str) + if isinstance(response_schema_str, str) + else response_schema_str + ) + except json.JSONDecodeError: + response_schema = {} + + response_mime_type = "application/json" if response_schema else "text/plain" + response_schema_arg = response_schema if response_schema else "" + + has_multimodal = False + if ( + st.session_state.dataset is not None + and "image" in st.session_state.dataset.columns + ): + has_multimodal = True + + metrics = ( + selected_metrics if selected_metrics else ["question_answering_correctness"] + ) + weights = [1.0 for _ in metrics] + + return Namespace( + system_instruction=st.session_state.local_prompt.prompt_to_run.system_instruction, + prompt_template=( + f"{st.session_state.local_prompt.prompt_to_run.prompt_data}" + "\n\tAnswer: {target}" + ), + target_model=target_model, + optimization_mode="instruction", + eval_metrics_types=metrics, + eval_metrics_weights=weights, + aggregation_type="weighted_sum", + input_data_path=input_optimization_data_file_uri, + output_path=f"gs://{output_optimization_run_uri}", + project=os.getenv("PROJECT_ID"), + num_steps=5, + num_demo_set_candidates=10, + demo_set_size=3, + target_model_location="us-central1", + source_model="", + source_model_location="", + target_model_qps=target_qps, + optimizer_model_qps=optimizer_qps, + eval_qps=eval_qps, + source_model_qps="", + response_mime_type=response_mime_type, + response_schema=response_schema_arg, + language="English", + placeholder_to_content=json.loads("{}"), + data_limit=data_limit, + translation_source_field_name="", + has_multimodal_inputs=has_multimodal, + ) + + +def check_job_status(job_name: str, project_id: str, location: str) -> str: + client_options = {"api_endpoint": f"{location}-aiplatform.googleapis.com"} + client = aiplatform.gapic.JobServiceClient(client_options=client_options) + parent = f"projects/{project_id}/locations/{location}" + response = client.list_custom_jobs(parent=parent) + for job in response: + if job.display_name == job_name: + return job.state.name + return "NOT_FOUND" + + +def write_record(metrics, prompt_name, version, system_instruction): + bucket_name = os.getenv("BUCKET") + if not bucket_name: + return + + record = { + "timestamp": datetime.now().isoformat(), + "prompt_name": prompt_name, + "prompt_version": version, + "system_instruction": system_instruction, + "scores": metrics, + } + + blob_name = f"records/{prompt_name}_{version}_{datetime.now().strftime('%Y%m%d%H%M%S')}.json" + bucket = st.session_state.storage_client.bucket(bucket_name) + blob = bucket.blob(blob_name) + blob.upload_from_string(json.dumps([record], indent=2)) + logger.info(f"Saved optimized record to gs://{bucket_name}/{blob_name}") + + +def main() -> None: + st.set_page_config( + layout="wide", page_title="Performance Tuner", page_icon="assets/favicon.ico" + ) + initialize_session_state() + + st.title("Performance Tuner") + st.markdown( + "Optimize your prompt's System Instructions using data-driven iteration to maximize metric performance." + ) + + # 1. & 2. Data Setup & Template Definition + st.header("1. Data & Prompt Setup") + col1, col2 = st.columns(2) + + with col1: + st.subheader("Data Setup") + data_sets = list({i.split("/")[4] for i in st.session_state.data_uris}) + st.selectbox("Select Dataset", options=[None, *data_sets], key="tuner_dataset") + + if st.session_state.tuner_dataset: + if ( + st.session_state.tuner_dataset + != st.session_state.last_selected_dataset_for_cache + or st.session_state.tuner_dataset + not in st.session_state.cached_data_files + ): + bucket = st.session_state.storage_client.bucket(os.getenv("BUCKET")) + prefix = f"datasets/{st.session_state.tuner_dataset}/" + blobs_iterator = bucket.list_blobs(prefix=prefix) + current_dataset_files = [ + blob.name[len(prefix) :] + for blob in blobs_iterator + if (blob.name.endswith(".csv") or blob.name.endswith(".jsonl")) + and not blob.name.endswith("/") + ] + st.session_state.cached_data_files[st.session_state.tuner_dataset] = ( + sorted(set(current_dataset_files)) + ) + st.session_state.last_selected_dataset_for_cache = ( + st.session_state.tuner_dataset + ) + + files = st.session_state.cached_data_files.get( + st.session_state.tuner_dataset, [] + ) + st.selectbox( + "Select File (.csv or .jsonl)", options=[None, *files], key="tuner_file" + ) + + if st.button("Load Dataset", key="tuner_load_data"): + if st.session_state.tuner_file: + gcs_uri = f"gs://{os.getenv('BUCKET')}/datasets/{st.session_state.tuner_dataset}/{st.session_state.tuner_file}" + if st.session_state.tuner_file.endswith(".jsonl"): + st.session_state.dataset = pd.read_json(gcs_uri, lines=True) + else: + st.session_state.dataset = pd.read_csv(gcs_uri) + st.success(f"Loaded {len(st.session_state.dataset)} rows.") + + with col2: + st.subheader("Template Definition") + st.selectbox( + "Select Prompt", + options=st.session_state.local_prompt.existing_prompts.keys(), + placeholder="Select Prompt...", + key="tuner_prompt", + ) + + if st.session_state.tuner_prompt: + st.session_state.local_prompt.prompt_meta["name"] = ( + st.session_state.tuner_prompt + ) + versions = [ + i.version_id + for i in prompts.list_versions( + st.session_state.local_prompt.existing_prompts[ + st.session_state.tuner_prompt + ] + ) + ] + st.selectbox( + "Select Version", + options=versions, + placeholder="Select Version...", + key="tuner_version", + ) + + if st.button("Load Prompt", key="tuner_load_prompt"): + if st.session_state.tuner_prompt and st.session_state.tuner_version: + st.session_state.local_prompt.load_prompt( + st.session_state.local_prompt.existing_prompts[ + st.session_state.tuner_prompt + ], + st.session_state.tuner_prompt, + st.session_state.tuner_version, + ) + st.success("Prompt loaded successfully.") + + if st.session_state.local_prompt.prompt_to_run.system_instruction: + with st.expander("View Loaded Prompt Details", expanded=False): + st.text_area( + "System Instruction", + st.session_state.local_prompt.prompt_to_run.system_instruction, + disabled=True, + height=100, + ) + st.text_area( + "Prompt Template", + st.session_state.local_prompt.prompt_to_run.prompt_data, + disabled=True, + height=100, + ) + + st.divider() + + # 3. Metric Selection + st.header("2. Metric Selection") + metric_names = MetricPromptTemplateExamples.list_example_metric_names() + computation_metrics = [ + "bleu", + "rouge_1", + "rouge_2", + "rouge_l", + "rouge_l_sum", + "exact_match", + "question_answering_correctness", + ] + all_metrics = list(set(metric_names + computation_metrics)) + + selected_metrics = st.multiselect( + "Select Evaluation Metrics for Optimization", + options=all_metrics, + default=["question_answering_correctness"], + key="tuner_metrics", + ) + target_model = st.selectbox( + "Select Target Model", options=TARGET_MODELS, key="tuner_target_model" + ) + + with st.expander("Advanced Settings"): + st.session_state.tuner_target_qps = st.number_input( + "Target Model QPS", + min_value=0.1, + max_value=10.0, + value=1.0, + step=0.1, + key="tuner_target_qps_input", + ) + st.session_state.tuner_optimizer_qps = st.number_input( + "Optimizer Model QPS", + min_value=0.1, + max_value=10.0, + value=1.0, + step=0.1, + key="tuner_optimizer_qps_input", + ) + st.session_state.tuner_eval_qps = st.number_input( + "Evaluation QPS", + min_value=0.1, + max_value=10.0, + value=1.0, + step=0.1, + key="tuner_eval_qps_input", + ) + st.session_state.tuner_data_limit = st.number_input( + "Data Limit (Sample Size)", + min_value=1, + max_value=1000, + value=10, + step=1, + key="tuner_data_limit_input", + ) + + st.divider() + + # 4. Execution + st.header("3. Execution") + if st.button("Start Optimization Job", type="primary"): + if not st.session_state.dataset is not None: + st.error("Please load a dataset first.") + return + if not st.session_state.local_prompt.prompt_to_run.system_instruction: + st.error("Please load a prompt first.") + return + if not selected_metrics: + st.error("Please select at least one metric.") + return + + with st.spinner("Initializing Job..."): + workspace_uri = ( + epath.Path(os.getenv("BUCKET")) + / "optimization" + / st.session_state.op_id + ) + input_data_uri = workspace_uri / "data" + workspace_uri.mkdir(parents=True, exist_ok=True) + input_data_uri.mkdir(parents=True, exist_ok=True) + output_optimization_data_uri = workspace_uri / "optimization_jobs" + + job_name = f"{st.session_state.tuner_prompt}-{st.session_state.tuner_version}-{st.session_state.tuner_dataset}-{st.session_state.op_id}" + output_optimization_run_uri = str(output_optimization_data_uri / job_name) + input_optimization_data_file_uri = f"gs://{input_data_uri}/{job_name}.jsonl" + + st.session_state.dataset.to_json( + str(input_optimization_data_file_uri), orient="records", lines=True + ) + + args = get_optimization_args( + input_optimization_data_file_uri, + output_optimization_run_uri, + target_model, + selected_metrics, + st.session_state.tuner_target_qps, + st.session_state.tuner_optimizer_qps, + st.session_state.tuner_eval_qps, + st.session_state.tuner_data_limit, + ) + args_dict = vars(args) + + config_file_uri = "gs://" + str(workspace_uri / "config" / "config.json") + with epath.Path(config_file_uri).open("w") as config_file: + json.dump(args_dict, config_file) + + worker_pool_specs = [ + { + "machine_spec": {"machine_type": "n1-standard-4"}, + "replica_count": 1, + "container_spec": { + "image_uri": os.getenv("APD_CONTAINER_URI"), + "args": ["--config=" + config_file_uri], + }, + } + ] + + custom_job = aiplatform.CustomJob( + display_name=job_name, + worker_pool_specs=worker_pool_specs, + staging_bucket=str(workspace_uri), + ) + custom_job.run(service_account=os.getenv("APD_SERVICE_ACCOUNT"), sync=False) + + st.session_state.tuner_launched_job = job_name + st.session_state.tuner_run_uri = f"gs://{os.getenv('BUCKET')}/optimization/{st.session_state.op_id}/optimization_jobs/{job_name}" + st.success(f"Started Optimization Job: {job_name}") + + if st.session_state.tuner_launched_job: + st.info(f"Active Job Tracked: {st.session_state.tuner_launched_job}") + + st.divider() + + # 5. Results Report + st.header("4. Results Report") + if st.button("Load Results"): + if not st.session_state.tuner_launched_job: + st.warning("No optimization job has been launched in this session.") + else: + with st.spinner("Checking job status..."): + status = check_job_status( + st.session_state.tuner_launched_job, + os.getenv("PROJECT_ID"), + os.getenv("LOCATION"), + ) + if status in ["JOB_STATE_PENDING", "JOB_STATE_RUNNING"]: + st.info( + f"Job is still {status.replace('JOB_STATE_', '')}. Please check back later. (Hill-climbing algorithms may take a while)." + ) + elif status == "JOB_STATE_FAILED": + st.error( + "The optimization job failed. Check Agent Platform console logs." + ) + elif status == "JOB_STATE_SUCCEEDED": + st.success("Job Complete! Processing results...") + + try: + results_ui = vapo_lib.ResultsUI(st.session_state.tuner_run_uri) + if getattr(results_ui, "templates", None) and getattr( + results_ui, "eval_results", None + ): + baseline = results_ui.templates[0] + winner = results_ui.templates[-1] + + st.subheader("Score Jump") + mean_cols = [ + c + for c in baseline.columns + if c.startswith("metrics.") and "/mean" in c + ] + + col_metrics = st.columns(min(len(mean_cols), 4) or 1) + final_scores = {} + for idx, m_col in enumerate(mean_cols): + b_val = ( + float(baseline[m_col].iloc[0]) + if m_col in baseline + else 0.0 + ) + w_val = ( + float(winner[m_col].iloc[0]) + if m_col in winner + else 0.0 + ) + diff = w_val - b_val + label = ( + m_col.replace("metrics.", "") + .replace("/mean", "") + .title() + ) + final_scores[label] = w_val + + with col_metrics[idx % len(col_metrics)]: + st.metric( + label, f"{w_val:.3f}", delta=f"{diff:.3f}" + ) + + st.subheader("Winner System Instruction") + winning_system_text = ( + winner["prompt"].iloc[0] + if "prompt" in winner + else "Unable to parse winning prompt." + ) + # Usually the optimizer alters the instruction which is the "prompt" field in vapo_lib output. + st.text_area( + "Optimized Instruction", winning_system_text, height=150 + ) + + st.session_state.tuner_winning_template = ( + winning_system_text + ) + st.session_state.tuner_final_scores = final_scores + else: + st.warning("Results found but could not be parsed.") + except Exception as e: + st.error(f"Error loading results: {e}") + else: + st.warning(f"Job state is currently: {status}") + + st.divider() + + # 6. Validation + st.header("5. Validation") + st.markdown("Test the best performing prompt on a blind test case.") + blind_test_json = st.text_area( + "Blind Test Case Input (JSON)", + placeholder='{"ticket_text": "I lost my password..."}', + height=100, + ) + + if st.button("Test Best Prompt"): + if not st.session_state.tuner_winning_template: + st.warning("Please load successful results first.") + elif not blind_test_json: + st.warning("Please provide a test case in JSON format.") + else: + try: + test_input = json.loads(blind_test_json) + + # Setup temporary prompt object to run test + prompt_obj = st.session_state.local_prompt + prompt_obj.prompt_to_run.system_instruction = ( + st.session_state.tuner_winning_template + ) + # Keep original generation config/schema + + with st.spinner("Generating Response..."): + res = prompt_obj.generate_response(test_input) + + st.success("Evaluation complete.") + st.text_area("Validation Response", res, height=150, disabled=True) + except Exception as e: + st.error(f"Failed to generate test response: {e}") + + st.divider() + + # 7. Outcome + st.header("6. Outcome") + if st.button("Export Final Prompt & Save Report", type="primary"): + if not st.session_state.tuner_winning_template: + st.error("No winning template found. Please load results first.") + else: + try: + prompt_obj = st.session_state.local_prompt + prompt_obj.prompt_to_run.system_instruction = ( + st.session_state.tuner_winning_template + ) + + # Save as new version + with st.spinner("Saving optimized prompt to registry..."): + prompt_obj.save_prompt(check_existing=False) + + new_version = prompt_obj.prompt_to_run._version_id or "latest" + st.success(f"Successfully exported as version: {new_version}") + + # Save evaluation records + if "tuner_final_scores" in st.session_state: + with st.spinner("Saving performance report..."): + write_record( + st.session_state.tuner_final_scores, + st.session_state.tuner_prompt, + new_version, + st.session_state.tuner_winning_template, + ) + st.success("Performance report saved to GCS.") + except Exception as e: + st.error(f"Failed to save outcome: {e}") + + +if __name__ == "__main__": + main() diff --git a/tools/llmevalkit/pages/6_Prompt_Optimization.py b/tools/llmevalkit/pages/6_Prompt_Optimization.py new file mode 100644 index 0000000..ffc1b98 --- /dev/null +++ b/tools/llmevalkit/pages/6_Prompt_Optimization.py @@ -0,0 +1,456 @@ +# Copyright 2025 Google LLC +# +# 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 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +"""Streamlit page for running Vertex AI Prompt Optimization. + +This script provides a user interface for: +- Loading existing prompts from Vertex AI Prompt Registry. +- Loading datasets from a Google Cloud Storage bucket. +- Generating baseline responses and evaluating them against a ground truth. +- Configuring and launching a Vertex AI CustomJob for prompt optimization. +- Displaying baseline evaluation results. + +File Source: +https://github.com/GoogleCloudPlatform/generative-ai/blob/main/gemini/prompts/prompt_optimizer/vapo_lib.py +""" + +import json +import logging +import os +from argparse import Namespace + +import pandas as pd +import streamlit as st +from dotenv import load_dotenv +from etils import epath +from google.cloud import aiplatform, storage +from src import vapo_lib +from src.gcp_prompt import GcpPrompt as gcp_prompt +from vertexai.preview import prompts + +load_dotenv("src/.env") + +logging.basicConfig( + level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s" +) +logger = logging.getLogger(__name__) + +TARGET_MODELS = [ + "gemini-2.5-pro", + "gemini-2.5-flash", + "gemini-2.0-flash-001", + "gemini-2.0-flash-lite-001", + "gemini-1.5-pro-002", + "gemini-1.5-flash-002", +] + + +def initialize_session_state() -> None: + """Initializes the session state variables.""" + if "op_id" not in st.session_state: + st.session_state.op_id = vapo_lib.get_id() + + if "local_prompt" not in st.session_state: + st.session_state.local_prompt = gcp_prompt() + + if "storage_client" not in st.session_state: + st.session_state["storage_client"] = storage.Client() + + if "data_uris" not in st.session_state: + st.session_state["data_uris"] = refresh_bucket() + + if "dataset" not in st.session_state: + st.session_state["dataset"] = None + + if "cached_data_files" not in st.session_state: + st.session_state.cached_data_files = {} + if "last_selected_dataset_for_cache" not in st.session_state: + st.session_state.last_selected_dataset_for_cache = None + + +def refresh_bucket() -> list[str]: + """Refreshes the list of available dataset URIs from the GCS bucket. + + This function lists all blobs in the configured GCS bucket, filters for + CSV and JSONL files located within the 'datasets/' prefix, and constructs a list + of their full gs:// URI paths. + + Returns: + A list of strings, where each string is a GCS URI to a dataset file. + """ + logger.info("Bucket: %s", os.getenv("BUCKET")) + bucket = st.session_state.storage_client.bucket(os.getenv("BUCKET")) + blobs = bucket.list_blobs() + data_uris = [] + for i in blobs: + if i.name.split("/")[0] == "datasets" and ( + i.name.endswith(".csv") or i.name.endswith(".jsonl") + ): + data_uris.append(f"gs://{i.bucket.name}/{i.name}") + logger.info("Data URIs: %s", data_uris) + return data_uris + + +def prompt_selection() -> None: + """Handles the prompt selection and loading.""" + st.selectbox( + "Select Existing Prompt", + options=st.session_state.local_prompt.existing_prompts.keys(), + placeholder="Select Prompt...", + key="selected_prompt", + ) + if st.session_state.selected_prompt: + logger.info("Prompt Meta: %s", st.session_state.local_prompt.prompt_meta) + st.session_state.local_prompt.prompt_meta["name"] = ( + st.session_state.selected_prompt + ) + versions = [ + i.version_id + for i in prompts.list_versions( + st.session_state.local_prompt.existing_prompts[ + st.session_state.selected_prompt + ] + ) + ] + + st.selectbox( + "Select Version", + options=versions, + placeholder="Select Version...", + key="selected_version", + ) + + st.button("Load Prompt", key="load_existing_prompt_button") + if st.session_state.load_existing_prompt_button: + logger.info( + "Selected Prompt ID: %s", + st.session_state.local_prompt.existing_prompts[ + st.session_state.selected_prompt + ], + ) + logger.info("Version: %s", st.session_state.selected_version) + st.session_state.local_prompt.load_prompt( + st.session_state.local_prompt.existing_prompts[ + st.session_state.selected_prompt + ], + st.session_state.selected_prompt, + st.session_state.selected_version, + ) + logger.info("Local Prompt Meta: %s", st.session_state.local_prompt.prompt_meta) + logger.info( + "Local Prompt Meta Dict Keys: %s", + st.session_state.local_prompt.prompt_meta.keys(), + ) + + st.session_state.prompt_name = ( + st.session_state.local_prompt.prompt_to_run.prompt_name + ) + st.session_state.prompt_data = ( + st.session_state.local_prompt.prompt_to_run.prompt_data + ) + st.session_state.model_name = ( + st.session_state.local_prompt.prompt_to_run.model_name.split("/")[-1] + ) + st.session_state.system_instructions = ( + st.session_state.local_prompt.prompt_to_run.system_instruction + ) + st.session_state.response_schema = json.dumps( + st.session_state.local_prompt.prompt_meta.get("response_schema", {}) + ) + st.session_state.generation_config = json.dumps( + st.session_state.local_prompt.prompt_meta.get("generation_config", {}) + ) + st.session_state.meta_tags = st.session_state.local_prompt.prompt_meta[ + "meta_tags" + ] + + +def dataset_selection() -> None: + """Handles the dataset selection and loading.""" + data_sets = list({i.split("/")[4] for i in st.session_state.data_uris}) + logger.info("Data Sets: %s", data_sets) + st.selectbox( + "Select an Existing Dataset", options=[None, *data_sets], key="selected_dataset" + ) + + files_to_display_in_selectbox = [] + if st.session_state.selected_dataset: + if ( + st.session_state.selected_dataset + != st.session_state.last_selected_dataset_for_cache + or st.session_state.selected_dataset + not in st.session_state.cached_data_files + ): + logger.info( + "Cache miss or dataset changed for files. Fetching for: %s", + st.session_state.selected_dataset, + ) + bucket = st.session_state.storage_client.bucket(os.getenv("BUCKET")) + prefix = f"datasets/{st.session_state.selected_dataset}/" + blobs_iterator = bucket.list_blobs(prefix=prefix) + + current_dataset_files = [] + for blob in blobs_iterator: + if ( + blob.name.endswith(".csv") or blob.name.endswith(".jsonl") + ) and not blob.name.endswith("/"): + filename = blob.name[len(prefix) :] + if filename: + current_dataset_files.append(filename) + + st.session_state.cached_data_files[st.session_state.selected_dataset] = ( + sorted(set(current_dataset_files)) + ) + st.session_state.last_selected_dataset_for_cache = ( + st.session_state.selected_dataset + ) + logger.info( + "Cached files for %s: %s", + st.session_state.selected_dataset, + st.session_state.cached_data_files[st.session_state.selected_dataset], + ) + + if "selected_file_from_dataset" in st.session_state: + st.session_state.selected_file_from_dataset = None + logger.info("Reset selected_file_from_dataset due to dataset change.") + + files_to_display_in_selectbox = st.session_state.cached_data_files.get( + st.session_state.selected_dataset, [] + ) + + st.selectbox( + "Select a file from this dataset:", + options=[None, *files_to_display_in_selectbox], + key="selected_file_from_dataset", + ) + + st.button("Load Dataset", key="load_existing_dataset_button") + if st.session_state.load_existing_dataset_button: + if not st.session_state.get("selected_dataset") or not st.session_state.get( + "selected_file_from_dataset" + ): + st.warning("Please select a dataset and a file first.") + else: + gcs_uri = f"gs://{os.getenv('BUCKET')}/datasets/{st.session_state.selected_dataset}/{st.session_state.selected_file_from_dataset}" + logger.info("Loading file: %s", gcs_uri) + if st.session_state.selected_file_from_dataset.endswith(".jsonl"): + st.session_state.dataset = pd.read_json(gcs_uri, lines=True) + else: + st.session_state.dataset = pd.read_csv(gcs_uri) + + if st.session_state.dataset is not None: + st.dataframe(st.session_state.dataset) + + +def get_optimization_args( + input_optimization_data_file_uri, + output_optimization_run_uri, + target_model, + target_qps=1.0, + optimizer_qps=1.0, + eval_qps=1.0, + data_limit=10, +): + """Gets the arguments for the optimization job.""" + response_schema_str = st.session_state.local_prompt.prompt_meta.get( + "response_schema", "{}" + ) + try: + response_schema = ( + json.loads(response_schema_str) + if isinstance(response_schema_str, str) + else response_schema_str + ) + except json.JSONDecodeError: + response_schema = {} + + if response_schema and response_schema != {}: + response_mime_type = "application/json" + response_schema_arg = response_schema + else: + response_mime_type = "text/plain" + response_schema_arg = "" + + has_multimodal = False + if ( + st.session_state.dataset is not None + and "image" in st.session_state.dataset.columns + ): + has_multimodal = True + + return Namespace( + system_instruction=st.session_state.local_prompt.prompt_to_run.system_instruction, + prompt_template=( + f"{st.session_state.local_prompt.prompt_to_run.prompt_data}" + "\n\tAnswer: {target}" + ), + target_model=target_model, + optimization_mode="instruction", + eval_metrics_types=[ + "question_answering_correctness", + ], + eval_metrics_weights=[ + 1.0, + ], + aggregation_type="weighted_sum", + input_data_path=input_optimization_data_file_uri, + output_path=f"gs://{output_optimization_run_uri}", + project=os.getenv("PROJECT_ID"), + num_steps=10, + num_demo_set_candidates=10, + demo_set_size=3, + target_model_location="us-central1", + source_model="", + source_model_location="", + target_model_qps=target_qps, + optimizer_model_qps=optimizer_qps, + eval_qps=eval_qps, + source_model_qps="", + response_mime_type=response_mime_type, + response_schema=response_schema_arg, + language="English", + placeholder_to_content=json.loads("{}"), + data_limit=data_limit, + translation_source_field_name="", + has_multimodal_inputs=has_multimodal, + ) + + +def start_optimization() -> None: + """Starts the optimization job.""" + st.divider() + + st.subheader("Run Optimization") + st.button("Start Optimization", key="start_optimization_button") + + if st.session_state.start_optimization_button: + workspace_uri = ( + epath.Path(os.getenv("BUCKET")) / "optimization" / st.session_state.op_id + ) + logger.info("Workspace URI: %s", workspace_uri) + + input_data_uri = epath.Path(workspace_uri) / "data" + logger.info("Input Data URI: %s", input_data_uri) + + workspace_uri.mkdir(parents=True, exist_ok=True) + input_data_uri.mkdir(parents=True, exist_ok=True) + + output_optimization_data_uri = epath.Path(workspace_uri) / "optimization_jobs" + logger.info("Output Data URI: %s", output_optimization_data_uri) + + prompt_optimization_job = ( + f"{st.session_state.selected_prompt}-" + f"{st.session_state.selected_version}-" + f"{st.session_state.selected_dataset}-" + f"{st.session_state.op_id}" + ) + output_optimization_run_uri = str( + output_optimization_data_uri / prompt_optimization_job + ) + input_optimization_data_file_uri = ( + f"gs://{input_data_uri}/{prompt_optimization_job}.jsonl" + ) + logger.info("Input Optimization Data URI: %s", input_optimization_data_file_uri) + if st.session_state.dataset is not None: + st.session_state.dataset.to_json( + str(input_optimization_data_file_uri), orient="records", lines=True + ) + else: + st.error("Please load a dataset first.") + return + + args = get_optimization_args( + input_optimization_data_file_uri, + output_optimization_run_uri, + st.session_state.target_model_optimization, + st.session_state.target_qps, + st.session_state.optimizer_qps, + st.session_state.eval_qps, + st.session_state.data_limit, + ) + + with st.expander("Prompt Optimization Config"): + st.json(vars(args)) + + args = vars(args) + + config_file_uri = "gs://" + str(workspace_uri / "config" / "config.json") + + with epath.Path(config_file_uri).open("w") as config_file: + json.dump(args, config_file) + config_file.close() + st.success(f"Successfully wrote config file to {config_file_uri}") + + worker_pool_specs = [ + { + "machine_spec": { + "machine_type": "n1-standard-4", + }, + "replica_count": 1, + "container_spec": { + "image_uri": os.getenv("APD_CONTAINER_URI"), + "args": ["--config=" + config_file_uri], + }, + } + ] + + custom_job = aiplatform.CustomJob( + display_name=prompt_optimization_job, + worker_pool_specs=worker_pool_specs, + staging_bucket=str(workspace_uri), + ) + + custom_job.run(service_account=os.getenv("APD_SERVICE_ACCOUNT"), sync=False) + + st.success("Successfully Started Job!!") + + +def main() -> None: + """Streamlit page for Prompt Optimization.""" + st.set_page_config( + layout="wide", page_title="Prompt Optimization", page_icon="assets/favicon.ico" + ) + + initialize_session_state() + + st.header("Prompt Optimization") + + st.selectbox( + "Select Target Model for Optimization:", + options=TARGET_MODELS, + key="target_model_optimization", + ) + + with st.expander("Advanced Settings"): + st.session_state.target_qps = st.number_input( + "Target Model QPS", min_value=0.1, max_value=10.0, value=1.0, step=0.1 + ) + st.session_state.optimizer_qps = st.number_input( + "Optimizer Model QPS", min_value=0.1, max_value=10.0, value=1.0, step=0.1 + ) + st.session_state.eval_qps = st.number_input( + "Evaluation QPS", min_value=0.1, max_value=10.0, value=1.0, step=0.1 + ) + st.session_state.data_limit = st.number_input( + "Data Limit (Sample Size)", min_value=1, max_value=1000, value=10, step=1 + ) + + prompt_selection() + dataset_selection() + start_optimization() + + +if __name__ == "__main__": + main() diff --git a/tools/llmevalkit/pages/7_Prompt_Optimization_Results.py b/tools/llmevalkit/pages/7_Prompt_Optimization_Results.py new file mode 100644 index 0000000..a8d2cbb --- /dev/null +++ b/tools/llmevalkit/pages/7_Prompt_Optimization_Results.py @@ -0,0 +1,481 @@ +## Copyright 2025 Google LLC +## 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 +## https://www.apache.org/licenses/LICENSE-2.0 +## Unless required by applicable law or agreed to in writing, software +## distributed under the License is distributed on an "AS IS" BASIS, +## WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +## See the License for the specific language + +import json +import logging +import os + +import pandas as pd +import streamlit as st +from dotenv import load_dotenv +from google.cloud import storage +from src import vapo_lib + +# Load environment variables +load_dotenv("src/.env") + +# Configure logging to the console +logging.basicConfig( + level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s" +) +logger = logging.getLogger(__name__) + +# --- Constants --- +BASE_OPTIMIZATION_PREFIX = "optimization/" +OPTIMIZATION_JOBS_SUBDIR = "optimization_jobs/" + +from google.cloud import aiplatform + + +def list_custom_training_jobs(project_id: str, location: str): + """Lists all custom training jobs and their statuses in a given project and location. + + Args: + project_id: The Google Cloud project ID. + location: The region for the Agent Platform jobs, e.g., "us-central1". + + Returns: + A list of dictionaries, where each dictionary contains details of a custom job. + """ + # Initialize the Agent Platform client + # The API endpoint is determined by the location + client_options = {"api_endpoint": f"{location}-aiplatform.googleapis.com"} + client = aiplatform.gapic.JobServiceClient(client_options=client_options) + + # The parent resource path format + parent = f"projects/{project_id}/locations/{location}" + + # Make the API request to list custom jobs + response = client.list_custom_jobs(parent=parent) + + # Process the response and format the output + jobs_list = [] + print(f"Fetching jobs from project '{project_id}' in '{location}'...") + for job in response: + job_info = { + "display_name": job.display_name, + "name": job.name, + "status": job.state.name, # .name gets the string representation of the enum + } + jobs_list.append(job_info) + + print(f"Found {len(jobs_list)} jobs.") + return jobs_list + + +# --- Example Usage --- +if __name__ == "__main__": + # Replace with your project ID and desired location + PROJECT_ID = os.getenv("PROJECT_ID") + LOCATION = os.getenv("LOCATION") + + # Ensure you have authenticated with Google Cloud CLI: + # gcloud auth application-default login + + # And have the necessary permissions (e.g., "Agent Platform User" role) + + try: + all_jobs = list_custom_training_jobs(project_id=PROJECT_ID, location=LOCATION) + + # Print the results + if all_jobs: + print("\n--- Job Statuses ---") + for job in all_jobs: + print(f" - Name: {job['display_name']:<40} Status: {job['status']}") + print("--------------------\n") + else: + print("No custom jobs found.") + + except Exception as e: + print( + "\nAn error occurred. Please ensure your project ID and location are correct," + ) + print(f"and that you have authenticated correctly. Error: {e}") + + +def safe_json_loads(s): + """Safely loads a JSON string, returning the original value on failure.""" + if not isinstance(s, str): + return s + try: + return json.loads(s) + except (json.JSONDecodeError, TypeError): + return s + + +@st.cache_data(ttl=300) +def list_gcs_directories( + bucket_name: str, prefix: str, _storage_client: storage.Client +) -> list[str]: + """Lists 'directories' in GCS under a given prefix. + A 'directory' is inferred from the common prefixes of objects. + Caches the result for 5 minutes to improve performance. + """ + if not bucket_name: + st.warning("BUCKET environment variable is not set.") + return [] + if not _storage_client: + st.warning("Storage client is not initialized.") + return [] + + bucket = _storage_client.bucket(bucket_name) + retrieved_prefixes = set() + try: + for page in bucket.list_blobs(prefix=prefix, delimiter="/").pages: + retrieved_prefixes.update(page.prefixes) + + # The retrieved prefixes are the "subdirectories". + # e.g., for prefix 'optimization/', a retrieved prefix might be 'optimization/op_id/'. + # We want to extract just 'op_id'. + dir_names = [] + for p in retrieved_prefixes: + name = p.replace(prefix, "").strip("/") + if name: + dir_names.append(name) + return sorted(set(dir_names)) + except Exception as e: + st.error( + f"Error listing GCS directories under gs://{bucket_name}/{prefix}: {e}" + ) + logger.error( + f"Error listing GCS directories under gs://{bucket_name}/{prefix}: {e}", + exc_info=True, + ) + return [] + + +def _display_interactive_results(results_ui: vapo_lib.ResultsUI) -> None: + """Processes results from a VAPO run and displays them in an interactive + Streamlit UI with tabs for each prompt version. + """ + try: + if ( + not hasattr(results_ui, "templates") + or not results_ui.templates + or not hasattr(results_ui, "eval_results") + ): + logger.info( + "ResultsUI object does not have 'templates' or 'eval_results', or templates list is empty. Falling back." + ) + st.info( + "No completed runs found yet in this directory. The evaluation might still be running or failed to produce results." + ) + else: + processed_results_for_tabs = [] + for i, template_summary_df in enumerate(results_ui.templates): + if ( + not isinstance(template_summary_df, pd.DataFrame) + or template_summary_df.empty + ): + logger.warning( + f"Template summary data at index {i} is not a non-empty DataFrame. Skipping." + ) + continue + + # Get the detailed results to perform the custom calculation + detailed_eval_df = pd.DataFrame() + if i < len(results_ui.eval_results) and isinstance( + results_ui.eval_results[i], pd.DataFrame + ): + detailed_eval_df = results_ui.eval_results[i] + + # Add a custom exact_match calculation. This is more robust than simple + # string comparison as it handles differences in JSON key order and whitespace. + if ( + not detailed_eval_df.empty + and "ground_truth" in detailed_eval_df.columns + and "reference" in detailed_eval_df.columns + ): + # Parse the JSON strings into Python objects before comparing. + parsed_ground_truths = detailed_eval_df["ground_truth"].apply( + safe_json_loads + ) + parsed_references = detailed_eval_df["reference"].apply( + safe_json_loads + ) + + # Create a boolean series for the comparison + is_match = parsed_ground_truths.eq(parsed_references) + + # Map boolean to 'yes'/'no' for display in the detailed table + detailed_eval_df["calculated_exact_match"] = is_match.map( + {True: "yes", False: "no"} + ) + + # Calculate the mean from the boolean series for the summary metric + new_exact_match_mean = is_match.mean() + template_summary_df["metrics.calculated_exact_match/mean"] = ( + new_exact_match_mean + ) + + prompt_text = "Prompt text not found in template data." + if "prompt" in template_summary_df.columns: + prompt_text = template_summary_df["prompt"].iloc[0] + else: + logger.warning( + f"Column 'prompt' not found in template_summary_df at index {i}." + ) + + # Determine the primary score and build the tab name. + primary_score_label = "Score" + primary_score_value = "N/A" + if "metrics.calculated_exact_match/mean" in template_summary_df.columns: + primary_score_label = "Calculated Exact Match" + primary_score_value = template_summary_df[ + "metrics.calculated_exact_match/mean" + ].iloc[0] + else: + # Fallback to the first available metric + mean_metric_columns = [ + col + for col in template_summary_df.columns + if col.startswith("metrics.") and "/mean" in col + ] + if mean_metric_columns: + first_metric_col = mean_metric_columns[0] + primary_score_label = ( + first_metric_col.replace("metrics.", "") + .replace("/mean", "") + .replace("_", " ") + .title() + ) + primary_score_value = template_summary_df[ + first_metric_col + ].iloc[0] + + # Build the tab name with all available metrics for a quick overview. + tab_name_metrics_parts = [] + mean_metric_columns = [ + col + for col in template_summary_df.columns + if col.startswith("metrics.") and "/mean" in col + ] + for metric_col in mean_metric_columns: + metric_name_short = metric_col.replace("metrics.", "").replace( + "/mean", "" + ) + metric_val = template_summary_df[metric_col].iloc[0] + if metric_name_short == "calculated_exact_match" and isinstance( + metric_val, float + ): + tab_name_metrics_parts.append( + f"{metric_name_short}: {metric_val:.1%}" + ) + else: + tab_name_metrics_parts.append( + f"{metric_name_short}: {metric_val:.3f}" + if isinstance(metric_val, float) + else f"{metric_name_short}: {metric_val}" + ) + + tab_name = f"Template {i}" + if tab_name_metrics_parts: + tab_name += f" ({', '.join(tab_name_metrics_parts)})" + + current_summary_df_display = template_summary_df.copy() + if "prompt" in current_summary_df_display.columns: + current_summary_df_display = current_summary_df_display.drop( + columns=["prompt"] + ) + + processed_results_for_tabs.append( + { + "name": tab_name, + "template_text": prompt_text, + "primary_score_label": primary_score_label, + "primary_score_value": primary_score_value, + "summary_metrics_df": current_summary_df_display, + "detailed_eval_df": detailed_eval_df, + } + ) + + if ( + processed_results_for_tabs + ): # If we successfully processed data, show the new UI + st.write("### Interactive Prompt Versions") + tab_titles = [res["name"] for res in processed_results_for_tabs] + tabs = st.tabs(tab_titles) + + for i, tab_content in enumerate(tabs): + with tab_content: + result_data = processed_results_for_tabs[i] + + st.subheader("Prompt Template") + # Sanitize tab name for key + clean_key_name = "".join( + filter(str.isalnum, result_data["name"]) + ) + st.text_area( + "Template", + value=result_data["template_text"], + height=200, + disabled=True, + key=f"template_view_{clean_key_name}_{i}", + ) + + st.subheader("Primary Score") + score_val = result_data["primary_score_value"] + score_label = result_data["primary_score_label"] + if score_label == "Calculated Exact Match" and isinstance( + score_val, float + ): + st.metric(label=score_label, value=f"{score_val:.2%}") + else: + st.metric( + label=score_label, + value=f"{score_val:.4f}" + if isinstance(score_val, float) + else str(score_val), + ) + + if not result_data["summary_metrics_df"].empty: + st.subheader("Summary Metrics (from templates.json)") + st.dataframe(result_data["summary_metrics_df"]) + + if not result_data["detailed_eval_df"].empty: + st.subheader( + "Detailed Evaluation Results (from eval_results.json)" + ) + st.dataframe(result_data["detailed_eval_df"]) + else: + st.caption( + "No detailed evaluation results available for this template." + ) + else: + st.warning("No valid results could be processed for display.") + + except Exception as e: + st.error(f"An error occurred while trying to display results: {e}") + logger.error(f"Error in results display section: {e}", exc_info=True) + st.markdown( + "For now, you can access the results directly at the GCS path shown above." + ) + + +def main() -> None: + """Renders the Streamlit page for viewing Prompt Optimization Results.""" + st.set_page_config( + layout="wide", + page_title="Prompt Optimization Results", + page_icon="assets/favicon.ico", + ) + st.header("Prompt Optimization Results Browser") + + if "storage_client" not in st.session_state: + try: + st.session_state["storage_client"] = storage.Client() + logger.info("Storage client initialized.") + except Exception as e: + st.error(f"Failed to initialize Google Cloud Storage client: {e}") + logger.error( + f"Failed to initialize Google Cloud Storage client: {e}", exc_info=True + ) + st.session_state["storage_client"] = None + return + + bucket_name = os.getenv("BUCKET") + if not bucket_name: + st.error("BUCKET environment variable is not set. Please configure it in .env.") + return + + # --- Step 1: Select Operation ID --- + op_ids = list_gcs_directories( + bucket_name, BASE_OPTIMIZATION_PREFIX, st.session_state.storage_client + ) + if not op_ids: + st.info( + f"No optimization operation IDs found under gs://{bucket_name}/{BASE_OPTIMIZATION_PREFIX}" + ) + return + + if "op_id" in st.session_state and st.session_state.op_id: + st.caption( + f"Hint: The last optimization run you initiated had the ID: `{st.session_state.op_id}`." + ) + + selected_op_id = st.selectbox( + "Select an Operation ID:", options=[None, *op_ids], key="selected_op_id_results" + ) + if not selected_op_id: + st.write("Please select an Operation ID to see its optimization job runs.") + return + + st.divider() + + # --- Step 2: Select Experiment Run --- + st.subheader(f"Optimization Job Runs for Operation ID: {selected_op_id}") + optimization_jobs_prefix = ( + f"{BASE_OPTIMIZATION_PREFIX}{selected_op_id}/{OPTIMIZATION_JOBS_SUBDIR}" + ) + experiment_runs = list_gcs_directories( + bucket_name, optimization_jobs_prefix, st.session_state.storage_client + ) + + if not experiment_runs: + st.info( + f"No completed optimization job runs found under gs://{bucket_name}/{optimization_jobs_prefix}" + ) + return + + selected_run = st.selectbox( + "Select an Optimization Job Run:", + options=[None, *experiment_runs], + key="selected_experiment_run", + ) + if not selected_run: + st.write("Please select an optimization job run to view its results.") + return + + st.divider() + + # --- Step 3: Check Job Status and Display Results --- + st.subheader(f"Results for: {selected_run}") + + project_id = os.getenv("PROJECT_ID") + location = os.getenv("LOCATION") + + if not project_id or not location: + st.error("PROJECT_ID or REGION environment variables are not set.") + return + + try: + jobs = list_custom_training_jobs(project_id=project_id, location=location) + job_status = "Not Found" + for job in jobs: + if job["display_name"] == selected_run: + job_status = job["status"] + break + + st.info(f"Status for job '{selected_run}': **{job_status}**") + + if job_status == "JOB_STATE_FAILED": + st.error( + "This optimization job has failed. Please check the logs in the Agent Platform console for more details." + ) + return + if job_status not in ["JOB_STATE_SUCCEEDED", "JOB_STATE_CANCELLED"]: + st.warning( + f"Job is currently in status: {job_status}. Results may be incomplete." + ) + + except Exception as e: + st.error(f"Could not retrieve job status. Error: {e}") + logger.error( + f"Failed to retrieve job status for {selected_run}: {e}", exc_info=True + ) + + run_uri = f"gs://{bucket_name}/{optimization_jobs_prefix}{selected_run}" + st.info(f"Loading results from: {run_uri}") + results_ui = vapo_lib.ResultsUI(run_uri) + _display_interactive_results(results_ui) + + +if __name__ == "__main__": + main() diff --git a/tools/llmevalkit/pages/8_Prompt_Records.py b/tools/llmevalkit/pages/8_Prompt_Records.py new file mode 100644 index 0000000..c8932c1 --- /dev/null +++ b/tools/llmevalkit/pages/8_Prompt_Records.py @@ -0,0 +1,129 @@ +# Copyright 2025 Google LLC +# +# 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 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import json +import logging +import os + +import pandas as pd +import streamlit as st +from dotenv import load_dotenv +from google.cloud import storage + +load_dotenv("src/.env") + +logging.basicConfig( + level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s" +) +logger = logging.getLogger(__name__) + + +def load_records_from_gcs(bucket_name: str, prefix: str) -> pd.DataFrame: + """Loads all JSON record files from a GCS prefix and returns a DataFrame.""" + try: + storage_client = storage.Client() + bucket = storage_client.bucket(bucket_name) + blobs = bucket.list_blobs(prefix=prefix) + + all_records = [] + for blob in blobs: + if blob.name.endswith(".json"): + logger.info(f"Loading record from {blob.name}") + try: + record_data = json.loads(blob.download_as_string()) + if isinstance(record_data, list): + all_records.extend(record_data) + else: + all_records.append(record_data) + except json.JSONDecodeError: + logger.warning(f"Could not decode JSON from {blob.name}") + except Exception as e: + logger.exception(f"Failed to process blob {blob.name}: {e}") + + if not all_records: + st.warning(f"No JSON records found at gs://{bucket_name}/{prefix}") + return pd.DataFrame() + + return pd.json_normalize(all_records) + + except Exception as e: + st.error(f"Failed to load or parse records from GCS: {e}") + logger.error("Error loading records: %s", e, exc_info=True) + return pd.DataFrame() + + +def main() -> None: + """Renders the Prompt Records Leaderboard page.""" + st.set_page_config( + layout="wide", + page_title="Prompt Records Leaderboard", + page_icon="assets/favicon.ico", + ) + st.header("Prompt Records Leaderboard") + st.markdown( + "This page allows you to view and compare the evaluation results of different prompt versions." + ) + + records_prefix = "records/" + + if "leaderboard_df" not in st.session_state: + st.session_state.leaderboard_df = pd.DataFrame() + + if st.button("Load/Refresh Leaderboard"): + with st.spinner("Loading records from GCS..."): + st.session_state.leaderboard_df = load_records_from_gcs( + os.getenv("BUCKET"), records_prefix + ) + if not st.session_state.leaderboard_df.empty: + st.success("Leaderboard loaded successfully.") + else: + st.info("Leaderboard is empty or could not be loaded.") + + if st.session_state.leaderboard_df.empty: + st.info("Click the button above to load the leaderboard data.") + return + + st.divider() + + prompt_names = st.session_state.leaderboard_df["prompt_name"].unique().tolist() + selected_prompt = st.selectbox( + "Select a Prompt to Compare Versions", options=[None, *prompt_names] + ) + + if selected_prompt: + st.subheader(f"Comparison for: {selected_prompt}") + + prompt_df = st.session_state.leaderboard_df[ + st.session_state.leaderboard_df["prompt_name"] == selected_prompt + ].copy() + + if prompt_df.empty: + st.info("No records found for the selected prompt.") + return + + # Use prompt_df directly as it already contains flattened mean_scores columns + comparison_df = prompt_df.reset_index(drop=True) + + # Clean up the view + display_columns = [ + col + for col in comparison_df.columns + if col not in ["scores", "evaluation_data"] + and not (col.startswith("score.") and col[6:].isdigit()) + ] + st.dataframe(comparison_df[display_columns]) + + +if __name__ == "__main__": + main() diff --git a/tools/llmevalkit/prompt-management-tutorial.ipynb b/tools/llmevalkit/prompt-management-tutorial.ipynb new file mode 100644 index 0000000..3dfa4cd --- /dev/null +++ b/tools/llmevalkit/prompt-management-tutorial.ipynb @@ -0,0 +1,580 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "81450b47de75" + }, + "outputs": [], + "source": [ + "# Copyright 2025 Google LLC\n", + "#\n", + "# Licensed under the Apache License, Version 2.0 (the \"License\");\n", + "# you may not use this file except in compliance with the License.\n", + "# You may obtain a copy of the License at\n", + "#\n", + "# http://www.apache.org/licenses/LICENSE-2.0\n", + "#\n", + "# Unless required by applicable law or agreed to in writing, software\n", + "# distributed under the License is distributed on an \"AS IS\" BASIS,\n", + "# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n", + "# See the License for the specific language governing permissions and\n", + "# limitations under the License." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "a204d0ab284d" + }, + "source": [ + "# Tutorial for Running Prompt Management and Evaluation\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + "
    \n", + " \n", + " \"Google
    Open in Colab\n", + "
    \n", + "
    \n", + " \n", + " \"Google
    Open in Colab Enterprise\n", + "
    \n", + "
    \n", + " \n", + " \"Vertex
    Open in Vertex AI Workbench\n", + "
    \n", + "
    \n", + " \n", + " \"GitHub
    View on GitHub\n", + "
    \n", + "
    \n", + "\n", + "
    \n", + "\n", + "Share to:\n", + "\n", + "\n", + " \"LinkedIn\n", + "\n", + "\n", + "\n", + " \"Bluesky\n", + "\n", + "\n", + "\n", + " \"X\n", + "\n", + "\n", + "\n", + " \"Reddit\n", + "\n", + "\n", + "\n", + " \"Facebook\n", + "" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "8aee03ecd776" + }, + "source": [ + "| Author(s) |\n", + "| --- |\n", + "| [Mike Santoro](https://github.com/Michael-Santoro) |" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "7cb0282fcd41" + }, + "source": [ + "## 1. Overview\n", + "\n", + "This tutorial provides a comprehensive guide to prompt engineering, covering the entire lifecycle from creation to evaluation and optimization. It's broken down into the following sections:\n", + "\n", + "1. **Prompt Management:** This section focuses on the core tasks of creating, editing, and managing prompts. You can: \n", + " - **Create new prompts:** Define the prompt's name, text, the model it's designed for, and any system instructions. \n", + " - **Load and edit existing prompts:** Browse a library of saved prompts, load a specific version, and make modifications.\n", + " - **Test prompts:** Before saving, you can provide sample input and generate a response to see how the prompt performs.\n", + " - **Versioning:** Each time you save a change to a prompt, a new version is created, allowing you to track its evolution and compare different iterations.\n", + "\n", + "2. **Dataset Creation:** A crucial part of prompt engineering is having good data to test and evaluate your prompts. This section allows you to:\n", + "\n", + " - **Create new datasets:** A dataset is essentially a folder in Google Cloud Storage where you can group related files.\n", + " - **Upload data:** You can upload files in CSV, JSON, or JSONL format to your datasets. This data will be used for evaluating your prompts.\n", + "\n", + "3. **Evaluation:** Once you have a prompt and a dataset, you need to see how well the prompt performs. The evaluation section helps you with this by:\n", + "\n", + " - **Running evaluations:** You can select a prompt and a dataset and run an evaluation. This will generate responses from the model for each item in your dataset.\n", + " - **Human-in-the-loop rating:** For a more nuanced evaluation, you can manually review the model's responses and rate them.\n", + " - **Automated metrics:** The tutorial also supports automated evaluation metrics to get a quantitative measure of your prompt's performance.\n", + "\n", + "4. **Prompt Optimization:** This section helps you automatically improve your prompts. It uses Vertex AI's prompt optimization capabilities to:\n", + "\n", + " - **Configure and launch optimization jobs:** You can set up and run a job that will take your prompt and a dataset and try to find a better-performing version of the prompt.\n", + "\n", + "5. **Prompt Optimization Results:** After an optimization job has run, this section allows you to:\n", + "\n", + " - **View the results:** You can see the different prompt versions that the optimizer came up with and how they performed.\n", + " - **Compare versions:** The results are presented in a way that makes it easy to compare the different optimized prompts and choose the best one.\n", + "\n", + "6. **Prompt Records:** This is a leaderboard that shows you the evaluation results of all your different prompt versions. It helps you to:\n", + "\n", + " - **Track performance over time:** See how your prompts have improved with each new version.\n", + " - **Compare different prompts:** You can compare the performance of different prompts for the same task.\n", + "\n", + "In summary, this tutorial provides a complete and integrated environment for all your prompt engineering needs, from initial creation to sophisticated optimization and evaluation.\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "bbd5f8c2144a" + }, + "source": [ + "## 2. Before you start" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "6b2e004206a7" + }, + "source": [ + "### Clone the GitHub Repo" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "bcafdf59c8b9" + }, + "outputs": [], + "source": [ + "! git clone https://github.com/GoogleCloudPlatform/generative-ai.git" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "36e497494e38" + }, + "outputs": [], + "source": [ + "! gcloud storage cp gs://github-repo/prompts/prompt_optimizer/mathvista_dataset/mathvista_input.jsonl mathvista_input.jsonl" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "7445a03507f7" + }, + "source": [ + "### Install Python Dependencies" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "44fee8ab2679" + }, + "outputs": [], + "source": [ + "% pip install -r requirements.txt" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "870814a62e87" + }, + "source": [ + "### Authenticate your notebook environment (Colab only)\n", + "\n", + "Authenticate your environment on Google Colab." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "9b6bfee6ba31" + }, + "outputs": [], + "source": [ + "import sys\n", + "\n", + "if \"google.colab\" in sys.modules:\n", + " from google.colab import auth\n", + "\n", + " auth.authenticate_user()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "f5cee1b8b3ab" + }, + "source": [ + "### Alternative Authenticate" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "4632dbaa3b73" + }, + "outputs": [], + "source": [ + "# fmt: off\n", + "PROJECT_ID = \"[your-project-id]\" # @param {type: \"string\", placeholder: \"[your-project-id]\", isTemplate: true}\n", + "LOCATION = \"[your-project-region]\" # @param{type: \"string\", placeholder: \"[your-project-region]\", isTemplate: true}\n", + "# fmt: on\n", + "\n", + "! gcloud auth application-default login\n", + "! gcloud config set project {PROJECT_ID}" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "390a62d0e8de" + }, + "source": [ + "### Set Google Cloud project information\n", + "\n", + "**TO-DO: Check these APIs**\n", + "To get started using Vertex AI, you must have an existing Google Cloud project and [enable the following APIs](https://console.cloud.google.com/flows/enableapi?apiid=cloudresourcemanager.googleapis.com,aiplatform.googleapis.com,cloudfunctions.googleapis.com,run.googleapis.com).\n", + "\n", + "Learn more about [setting up a project and a development environment](https://cloud.google.com/vertex-ai/docs/start/cloud-environment)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "ad1be5b6c02d" + }, + "outputs": [], + "source": [ + "! cp src/.env.example src/.env" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "94dd1d71983f" + }, + "source": [ + "### Copy sample.env and Modify\n", + "\n", + "- BUCKET_NAME - Pick an existing bucket or make a new one below\n", + "- PROJECT_ID\n", + "- SERVICE_ACCOUNT - Created Below\n", + "\n", + "\n", + "#### Create a New Bucket (Not Required if using existing)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "e27ab4bb8a87" + }, + "outputs": [], + "source": [ + "# fmt: off\n", + "BUCKET_NAME = \"[your-bucket-name]\" # @param {type: \"string\", placeholder: \"[your-bucket-name]\", isTemplate: true}\n", + "# fmt: on\n", + "\n", + "BUCKET_URI = f\"gs://{BUCKET_NAME}\"\n", + "\n", + "\n", + "! gcloud storage buckets create {BUCKET_URI} --location {LOCATION}" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "33cc66a9351d" + }, + "source": [ + "#### Create a Service Account" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "492567ee96db" + }, + "outputs": [], + "source": [ + "PROJECT_NUMBER = !gcloud projects describe {PROJECT_ID} --format=\"get(projectNumber)\"[0]\n", + "PROJECT_NUMBER = PROJECT_NUMBER[0]\n", + "SERVICE_ACCOUNT = f\"{PROJECT_NUMBER}-compute@developer.gserviceaccount.com\"\n", + "\n", + "for role in ['aiplatform.user', 'storage.objectAdmin']:\n", + "\n", + " ! gcloud projects add-iam-policy-binding {PROJECT_ID} \\\n", + " --member=serviceAccount:{SERVICE_ACCOUNT} \\\n", + " --role=roles/{role} --condition=None" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "4bc0ae8e0c5f" + }, + "source": [ + "## 3. Run the App" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "21fed89471c8" + }, + "outputs": [], + "source": [ + "! cd generative-ai/llmevalkit && streamlit run index.py & npx localtunnel --port 8501" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "90a521d73796" + }, + "source": [ + "Click the link and use just the external ip as the password.\n", + "\n", + "📝 **Note:** You can run `wget -q -O - https://loca.lt/mytunnelpassword` to get the external ip (i.e 35.194.128.20)\n", + "\n", + "📝 **Note:** If you are having issues displaying the app, clear your cache." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "1e31b6cc7645" + }, + "source": [ + "![image.png](assets/welcome_page.png)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "9ca8d05072cd" + }, + "source": [ + "## 4. Work with the App" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "c7c11886dc17" + }, + "source": [ + "### 1. Prompt Management\n", + "\n", + "In the Prompt Name field enter:\n", + "\n", + "```\n", + "math_prompt_test\n", + "```\n", + "\n", + "In the Prompt Data field enter:\n", + "\n", + "```\n", + "Problem: {{query}}\n", + "Image: {{image}} @@@image/jpeg\n", + "Answer: {{target}}\n", + "```\n", + "\n", + "In the Model Name field enter:\n", + "```\n", + "gemini-2.0-flash-001\n", + "```\n", + "\n", + "In the System Instructions field enter:\n", + "```\n", + "Solve the problem given the image.\n", + "```\n", + "\n", + "Click `Save`\n", + "\n", + "Copy this text for testing:\n", + "\n", + "```\n", + "{\"query\": \"Hint: Please answer the question and provide the correct option letter, e.g., A, B, C, D, at the end.\\nQuestion: As shown in the figure, CD is the diameter of \\u2299O, chord DE \\u2225 OA, if the degree of \\u2220D is 50.0, then the degree of \\u2220C is ()\\nChoices:\\n(A) 25\\u00b0\\n(B) 30\\u00b0\\n(C) 40\\u00b0\\n(D) 50\\u00b0\", \"image\": \"gs://github-repo/prompts/prompt_optimizer/mathvista_dataset/images/643.jpg\", \"target\": \"25\\u00b0\"}\n", + "```\n", + "\n", + "đŸ–±ïž Click `Generate`.\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "306b19d374b6" + }, + "source": [ + "### 2. Dataset Creation\n", + "\n", + "Download a copy of the dataset. Then upload this file in the application.\n", + "\n", + "**Dataset Name:** `mathvista`\n", + "\n", + "You can preview the dataset at the bottom of the page." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "814fb0866bd5" + }, + "outputs": [], + "source": [ + "! gcloud storage cp gs://github-repo/prompts/prompt_optimizer/mathvista_dataset/mathvista_input.jsonl ." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "0b06fa624a7a" + }, + "source": [ + "### 3. Evaluation\n", + "\n", + "We will now run an evaluation, prior to doing any tweaking to get a baseline.\n", + "\n", + "- **Existing Dataset:** 'mathvista'\n", + "- **Dataset File:** 'mathvista_input.jsonl'\n", + "- **Number of Samples:** '100'\n", + "- **Ground Truth Column Name:** 'target'\n", + "- **Existing Prompt:** 'math_prompt_test'\n", + "- **Version:** '1'\n", + "\n", + "Click Load Prompt, and Upload and Get Response... ⏰ Wait!!\n", + "\n", + "Review the responses.\n", + "\n", + "- **Model-Based:** 'question-answering-quality'\n", + "\n", + "Launch the Eval... ⏰ Wait!!\n", + "\n", + "View the Evaluation Results, and save to prompt records. This will save this initial version to the prompt records for the baseline.\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "c76e0e5065fa" + }, + "source": [ + "### 4. Prompt Optimization\n", + "\n", + "🔧 Set-Up Prompt Optimization.\n", + "\n", + "- **Target Model:** 'gemini-2.0-flash-001'\n", + "- **Existing Prompt:** 'math_prompt_test'\n", + "- **Version:** '1'\n", + "\n", + "đŸ–±ïž Click Load Prompt.\n", + "\n", + "- **Select Existing Dataset:** 'mathvista'\n", + "- **Select the File:** 'mathvista_input.jsonl'\n", + "\n", + "đŸ–±ïž Click Load Dataset.\n", + "\n", + "Preview the dataset.\n", + "\n", + "đŸ–±ïž Click Start Optimization.\n", + "\n", + "**Note:** If Interested in viewing the progress, Navigate to https://console.cloud.google.com/vertex-ai/training/custom-jobs\n", + "\n", + "⏰ Wait!! This step will take about 20-min to run." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "9987b016703e" + }, + "source": [ + "### 5. Prompt Optimization Results\n", + "\n", + "View the Optimization Results.\n", + "\n", + "The last run will be shown at the top of the screen. Pick this from the dropdown menu: \n", + "\n", + "![image.png](assets/prompt_optimization_result.png)\n", + "\n", + "Review the results and select the highest scoring version and copy the instruction." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "f15bce27fc94" + }, + "source": [ + "### 6. Navigate Back to Prompt for New Version\n", + "\n", + "Load your existing prompt from before.\n", + "\n", + "📋 Paste your new instructions from the prompt optimizer, and save new version." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "68957d00c89e" + }, + "source": [ + "### 7. Run new Evaluation\n", + "\n", + "Repeat step 3 with your new version." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "c2e6c524d69a" + }, + "source": [ + "### 8. View the Records\n", + "\n", + "Navigate to the leaderboard and load the results." + ] + } + ], + "metadata": { + "colab": { + "name": "prompt-management-tutorial.ipynb", + "toc_visible": true + }, + "kernelspec": { + "display_name": "Python 3", + "name": "python3" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} diff --git a/tools/llmevalkit/pyproject.toml b/tools/llmevalkit/pyproject.toml new file mode 100644 index 0000000..08ecc22 --- /dev/null +++ b/tools/llmevalkit/pyproject.toml @@ -0,0 +1,41 @@ +[project] +name = "llmevalkit" +version = "0.1.0" +description = "LLM Evaluation Kit" +readme = "README.md" +requires-python = ">=3.10" +dependencies = [ + "google-genai", + "vertexai", + "streamlit", + "pandas", + "google-cloud-storage", + "python-dotenv", + "pydantic", + "plotly", + "matplotlib", + "db-dtypes", + "nbformat>=4.2.0", + "streamlit-tags", + "google-cloud-pipeline-components", + "gcsfs", + "ipython", + "tqdm", + "tenacity", + "etils", + "importlib-resources", + "fsspec", + "ipywidgets", + "google-cloud-aiplatform", + "tensorflow", + "pylint", + "ipykernel" +] + +[tool.uv] +index = [{ url = "https://pypi.org/simple", default = true }] + +[dependency-groups] +dev = [ + "pytest>=9.0.2", +] diff --git a/tools/llmevalkit/requirements.txt b/tools/llmevalkit/requirements.txt new file mode 100644 index 0000000..0d7619f --- /dev/null +++ b/tools/llmevalkit/requirements.txt @@ -0,0 +1,25 @@ +ipykernel +google-cloud-storage +streamlit +pandas +plotly +matplotlib +db-dtypes +nbformat>=4.2.0 +streamlit-tags +dotenv +google-cloud-pipeline-components +gcsfs +pipfile +ipython +asyncio +tqdm +tenacity +etils +importlib-resources +fsspec +ipywidgets +google-cloud-aiplatform +tensorflow +pylint +google-genai diff --git a/tools/llmevalkit/src/.env.example b/tools/llmevalkit/src/.env.example new file mode 100644 index 0000000..94a92a4 --- /dev/null +++ b/tools/llmevalkit/src/.env.example @@ -0,0 +1,25 @@ +# Project Configuration +BUCKET = "" +PROMPT_PREFIX = "prompts" +PROJECT_ID = "" +LOCATION = "" + +START_PROMPT_PATH = "src/initial_local_prompt_empty.json" + +## Constants for Eval +JITTER_AMOUNT = 0.1 +PLOTLY_RENDERER = "colab" +DEFAULT_HUMAN_RATING_COL = "human_rating" +DEFAULT_SCORE_COL = "score" +EXPERIMENT_NAME = "eval-open-judge-test" + +## Constants for Optimize +OPTIMIZATION_MODE = "instruction" +EVAL_METRIC = "exact_match" +NUM_INST_OPTIMIZATION_STEPS = 10 +TARGET_MODEL_QPS = 3.0 +EVAL_QPS = 3.0 +RESPONSE_MIME_TYPE = "text/plain" +TARGET_LANGUAGE = "English" +SERVICE_ACCOUNT = "" +APD_CONTAINER_URI = "us-docker.pkg.dev/vertex-ai-restricted/builtin-algorithm/apd:preview_v1_0" diff --git a/tools/llmevalkit/src/__init__.py b/tools/llmevalkit/src/__init__.py new file mode 100644 index 0000000..0a2669d --- /dev/null +++ b/tools/llmevalkit/src/__init__.py @@ -0,0 +1,13 @@ +# Copyright 2025 Google LLC +# +# 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 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/tools/llmevalkit/src/gcp_dataset.py b/tools/llmevalkit/src/gcp_dataset.py new file mode 100644 index 0000000..8131353 --- /dev/null +++ b/tools/llmevalkit/src/gcp_dataset.py @@ -0,0 +1,95 @@ +# Copyright 2025 Google LLC +# +# 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 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +"""This module provides utility functions for managing and processing datasets +stored on Google Cloud Storage (GCS). + +It includes functionalities to: +- List available datasets from a specified GCS bucket. +- Fetch and read CSV files from GCS into pandas DataFrames. +- Process raw data from CSVs to generate structured user prompts and expected + outcomes for model evaluation. +- A helper function to escape special characters in text to be used with the + Gemini API. + +The primary purpose is to abstract the GCS interactions and data preprocessing +steps required for the prompt management and evaluation application. +""" + +import io +import logging +import os + +import pandas as pd +from dotenv import load_dotenv +from google.cloud import storage + +logging.basicConfig( + level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s" +) +logger = logging.getLogger(__name__) + +load_dotenv("src/.env") + + +PREFIX = "datasets_meta" + + +def get_existing_datasets() -> list[str]: + """Gets the list of existing dataset names from the GCS bucket. + + Returns: + list[str]: A list of dataset names. + """ + storage_client = storage.Client() + blobs = storage_client.list_blobs(os.getenv("BUCKET_NAME"), prefix=PREFIX) + return [i.name.split("/")[-1] for i in blobs] + + +def escape_special_characters(text: str) -> str: + """Escapes special characters for Gemini Flash API.""" + if not isinstance(text, str): + return text + text = text.replace("\\", "\\") + text = text.replace("\n", "\n") + text = text.replace("\r", "\r") + text = text.replace("\t", "\t") + return text.replace('"', '"') + + +def process_csv_from_gcs(bucket_name: str, file_path: str) -> pd.DataFrame: + """Reads a CSV file from GCS, processes each row to create user prompts + and expected results, and returns a pandas DataFrame. + + Args: + bucket_name: Name of the GCS bucket. + file_path: Path to the CSV file within the bucket. + + Returns: + pandas.DataFrame: DataFrame with original data and added user_prompt + and expected_result columns. + """ + try: + client = storage.Client() + bucket = client.get_bucket(bucket_name) + blob = bucket.blob(file_path) + + content = blob.download_as_bytes() + + return pd.read_csv(io.BytesIO(content)) + + except (OSError, ValueError) as e: + print(f"An error occurred: {e}") + return pd.DataFrame() diff --git a/tools/llmevalkit/src/gcp_evaluation.py b/tools/llmevalkit/src/gcp_evaluation.py new file mode 100644 index 0000000..45f7c67 --- /dev/null +++ b/tools/llmevalkit/src/gcp_evaluation.py @@ -0,0 +1,401 @@ +# Copyright 2025 Google LLC +# +# 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 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Provides tools for evaluating and visualizing model performance against human ratings. + +This module contains functions to process evaluation data, which includes both +human-provided scores and model-generated scores. It prepares the data into a +pandas DataFrame, extracts key metrics like the confusion matrix, and generates +a series of Plotly visualizations to compare the two sets of scores. + +The primary functions are: +- prepare_dataframe: Cleans and formats the raw evaluation data. +- extract_completeness_metrics: Pulls confusion matrix data from metrics logs. +- plot_distribution_comparison: Compares the distribution of human vs. model scores. +- plot_confusion_matrix: Creates a heatmap to show agreement and disagreement. +- plot_jitter_scatter: Visualizes the alignment of individual data points. +- run_visual_analysis: An orchestrator function that runs the full analysis and + returns a summary report along with the generated figures. +""" + +import ast +import logging +import os +from typing import Any + +import numpy as np +import pandas as pd +import plotly.graph_objects as go +from dotenv import load_dotenv +from plotly.subplots import make_subplots + +load_dotenv("src/.env") + + +logging.basicConfig( + level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s" +) +logger = logging.getLogger(__name__) + + +def format_user_content(user_content: str, tokenizer: Any, **kwargs: Any) -> str | None: + """Applies tokenizer.apply_chat_template to user content string. + Assumes user_content is the text for the 'user' role. + """ + message = [ + {"role": "user", "content": user_content}, + ] + kwargs.setdefault("tokenize", False) + kwargs.setdefault("add_generation_prompt", True) + + try: + return tokenizer.apply_chat_template(message, **kwargs) + except Exception as e: + print( + f"Error applying chat template to content: '{user_content[:50]}...'. Error: {e}" + ) + return None + + +def prepare_dataframe( + raw_data: Any, + human_col_name: str, + score_col_name: str, +) -> pd.DataFrame: + """Prepares the DataFrame from raw data, renames columns, and converts types.""" + try: + if isinstance(raw_data, pd.DataFrame): + df = raw_data.copy() + if len(df.columns) >= 2: + # If it's a DataFrame, check if desired columns exist, else use first two + if human_col_name not in df.columns or score_col_name not in df.columns: + original_cols = df.columns + df = df[ + [original_cols[0], original_cols[1]] + ].copy() # Use first two + df.columns = [human_col_name, score_col_name] + else: + # Ensure we only keep the needed columns if more exist + pass + if human_col_name not in df.columns or score_col_name not in df.columns: + original_cols = df.columns + df = df[[original_cols[0], original_cols[1]]].copy() + df.columns = [human_col_name, score_col_name] + else: + df = df[[human_col_name, score_col_name]].copy() + else: + print( + f"Warning: Input DataFrame has < 2 columns. Expected at least '{human_col_name}' and '{score_col_name}'." + ) + return pd.DataFrame(columns=[human_col_name, score_col_name]) + else: + df = pd.DataFrame(raw_data) + if df.empty: + print("Warning: Created empty DataFrame from raw_data.") + return pd.DataFrame(columns=[human_col_name, score_col_name]) + + if len(df.columns) >= 2: + df = df.iloc[:, :2] + df.columns = [human_col_name, score_col_name] + else: + print( + f"Warning: DataFrame from raw_data has < 2 columns. Cannot set '{human_col_name}' and '{score_col_name}'." + ) + return pd.DataFrame(columns=[human_col_name, score_col_name]) + + df[human_col_name] = pd.to_numeric(df[human_col_name], errors="coerce") + df[score_col_name] = pd.to_numeric(df[score_col_name], errors="coerce") + df.dropna(subset=[human_col_name, score_col_name], inplace=True) + df[human_col_name] = df[human_col_name].astype(float) + df[score_col_name] = df[score_col_name].astype(float) + return df + + except Exception as e: + print(f"Error preparing DataFrame: {e}") + return pd.DataFrame(columns=[human_col_name, score_col_name]) + + +def extract_completeness_metrics( + metrics_data: list[dict[str, Any]] | None, +) -> tuple[list[list[Any]] | None, list[str] | None, list[float] | None]: + """Extracts confusion matrix info from metrics data (expected at index 0).""" + if not metrics_data or not isinstance(metrics_data, list) or len(metrics_data) == 0: + print("Warning: Metrics data is empty or not a list.") + return None, None, None + + try: + completeness_metrics = metrics_data[0] + if ( + "confusion_matrix" not in completeness_metrics + or "confusion_matrix_labels" not in completeness_metrics + ): + print( + "Warning: 'confusion_matrix' or 'confusion_matrix_labels' not in first metrics item." + ) + return None, None, None + + cm = completeness_metrics["confusion_matrix"] + cm_labels = completeness_metrics["confusion_matrix_labels"] + cm_labels_numeric = [float(cl) for cl in cm_labels] + return cm, cm_labels, cm_labels_numeric + except (KeyError, IndexError, ValueError, TypeError) as e: + print(f"Warning: Could not extract confusion matrix info from metrics: {e}") + return None, None, None + + +def plot_distribution_comparison( + df: pd.DataFrame, + human_col: str, + score_col: str, +) -> go.Figure: + """Generates bar charts comparing distributions of human ratings and model scores.""" + human_counts = df[human_col].value_counts().sort_index() + score_counts = df[score_col].value_counts().sort_index() + + fig = make_subplots( + rows=1, + cols=2, + subplot_titles=("Human Rating Distribution", "Model Score Distribution"), + ) + + fig.add_trace( + go.Bar( + x=human_counts.index, + y=human_counts.values, + name="Human Rating", + marker_color="indianred", + ), + row=1, + col=1, + ) + + fig.add_trace( + go.Bar( + x=score_counts.index, + y=score_counts.values, + name="Model Score", + marker_color="lightsalmon", + ), + row=1, + col=2, + ) + + fig.update_layout( + title_text="Distribution of Human Ratings vs. Model Scores", + bargap=0.2, + xaxis1_title="Rating Value", + yaxis1_title="Count", + xaxis2_title="Score Value", + yaxis2_title="Count", + xaxis1_type="category", + xaxis2_type="category", + xaxis1={ + "categoryorder": "array", + "categoryarray": sorted(human_counts.index.unique()), + }, + xaxis2={ + "categoryorder": "array", + "categoryarray": sorted(score_counts.index.unique()), + }, + height=400, + ) + return fig + + +def plot_confusion_matrix( + cm: list[list[Any]] | None, cm_labels: list[str] | None +) -> go.Figure | None: + """Generates a heatmap for the confusion matrix.""" + if cm is None or cm_labels is None: + print("Skipping confusion matrix plot: missing data.") + return None + + fig = go.Figure( + data=go.Heatmap( + z=cm, + x=cm_labels, + y=cm_labels, + hoverongaps=False, + colorscale="Blues", + text=cm, + texttemplate="%{text}", + zmin=0, + ) + ) + + fig.update_layout( + title="Confusion Matrix: Human Rating vs. Model Score (Completeness)", + xaxis_title="Predicted (Model Score)", + yaxis_title="True (Human Rating)", + yaxis={ + "type": "category", + "categoryorder": "array", + "categoryarray": cm_labels, + }, + xaxis={ + "type": "category", + "categoryorder": "array", + "categoryarray": cm_labels, + }, + height=600, + width=600, + ) + return fig + + +def plot_jitter_scatter( + df: pd.DataFrame, + cm_labels: list[str] | None, + cm_labels_numeric: list[float] | None, + human_col: str, + score_col: str, +) -> go.Figure: + """Generates a jitter scatter plot comparing individual scores and ratings.""" + df_jitter = df[[human_col, score_col]].copy() + + if cm_labels is None or cm_labels_numeric is None: + print( + "Using data range for jitter plot axes due to missing confusion matrix labels." + ) + min_val: float = ( + min(df_jitter[human_col].min(), df_jitter[score_col].min()) - 0.5 + ) + max_val: float = ( + max(df_jitter[human_col].max(), df_jitter[score_col].max()) + 0.5 + ) + plot_range = [min_val, max_val] + tick_vals = sorted( + df_jitter[human_col].unique() + ) # Use unique human ratings for ticks if available + tick_text = [str(int(v)) if v == int(v) else str(v) for v in tick_vals] + else: + plot_range = [min(cm_labels_numeric) - 0.5, max(cm_labels_numeric) + 0.5] + tick_vals = cm_labels_numeric + tick_text = cm_labels + + df_jitter[f"{human_col}_jitter"] = df_jitter[human_col] + np.random.uniform( + -ast.literal_eval(os.getenv("JITTER_AMOUNT")), + ast.literal_eval(os.getenv("JITTER_AMOUNT")), + size=len(df_jitter), + ) + df_jitter[f"{score_col}_jitter"] = df_jitter[score_col] + np.random.uniform( + -ast.literal_eval(os.getenv("JITTER_AMOUNT")), + ast.literal_eval(os.getenv("JITTER_AMOUNT")), + size=len(df_jitter), + ) + + fig = go.Figure() + + fig.add_trace( + go.Scatter( + x=df_jitter[f"{score_col}_jitter"], + y=df_jitter[f"{human_col}_jitter"], + mode="markers", + marker={ + "color": "rgba(0, 100, 200, 0.7)", + "size": 10, + "line": {"width": 1, "color": "DarkSlateGrey"}, + }, + text=[ + f"HR: {hr:.1f}, Score: {s:.1f}" + for hr, s in zip( + df_jitter[human_col], df_jitter[score_col], strict=False + ) + ], + hoverinfo="text", + name="Ratings", + ) + ) + + fig.add_trace( + go.Scatter( + x=plot_range, + y=plot_range, + mode="lines", + name="Ideal Alignment (Score = Human Rating)", + line={"color": "red", "dash": "dash"}, + ) + ) + + fig.update_layout( + title="Model Score vs. Human Rating (with Jitter)", + xaxis_title="Model Score (Jittered)", + yaxis_title="Human Rating (Jittered)", + xaxis={"range": plot_range, "tickvals": list(tick_vals), "ticktext": tick_text}, + yaxis={"range": plot_range, "tickvals": list(tick_vals), "ticktext": tick_text}, + width=600, + height=600, + showlegend=True, + hovermode="closest", + ) + return fig + + +def run_visual_analysis( + df_data: Any, + metrics: list[dict[str, Any]] | None, + human_col_name: str, + score_col_name: str, +) -> tuple[str, go.Figure | None, go.Figure | None, go.Figure | None]: + """Runs the visual analysis comparing model scores and human ratings. + Returns a markdown string and the three figure objects. + """ + result_str = "" + + df = prepare_dataframe( + df_data, human_col_name=human_col_name, score_col_name=score_col_name + ) + cm, cm_labels, cm_labels_numeric = extract_completeness_metrics(metrics) + + if df.empty: + result_str = f"Could not create valid DataFrame with columns '{human_col_name}' and '{score_col_name}' from input data. Stopping analysis." + return result_str, None, None, None + + if human_col_name not in df.columns or score_col_name not in df.columns: + result_str = f"Expected columns '{human_col_name}' and '{score_col_name}' not found in DataFrame. Stopping analysis." + return result_str, None, None, None + + result_str += "# Visual Analysis: Model Score vs. Human Rating Alignment \n" + + result_str += "## 1. Distributions Comparison\n" + fig_dist: go.Figure | None = plot_distribution_comparison( + df, human_col=human_col_name, score_col=score_col_name + ) + if fig_dist: + result_str += "*Shows if the model score distribution mirrors human ratings.*\n" + else: + result_str += "*Could not generate distribution comparison plot.*\n" + + result_str += "## 2. Confusion Matrix (Human vs. Model)\n" + fig_cm: go.Figure | None = plot_confusion_matrix(cm, cm_labels) + if fig_cm: + result_str += "*Visualizes agreement and disagreement between discrete human ratings and model scores. Ideal alignment is along the diagonal.*\n" + + else: + result_str += "*Confusion Matrix could not be generated (check metrics data or ensure it's at index 0).**\n" + + result_str += "## 3. Score vs. Rating Alignment (Jitter Plot)\n" + fig_scatter: go.Figure | None = plot_jitter_scatter( + df, + cm_labels, + cm_labels_numeric, + human_col=human_col_name, + score_col=score_col_name, + ) + if fig_scatter: + result_str += "*Shows individual item alignment. Points close to the red dashed line indicate good agreement.*\n" + else: + result_str += "*Could not generate jitter scatter plot.*" + + return result_str, fig_dist, fig_cm, fig_scatter diff --git a/tools/llmevalkit/src/gcp_prompt.py b/tools/llmevalkit/src/gcp_prompt.py new file mode 100644 index 0000000..130b4a6 --- /dev/null +++ b/tools/llmevalkit/src/gcp_prompt.py @@ -0,0 +1,280 @@ +# Copyright 2025 Google LLC +# +# 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 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Manages the lifecycle of prompts using Google Cloud Platform services. + +This module provides a `gcp_prompt` class that facilitates the creation, +storage, retrieval, and execution of prompts with Vertex AI and Cloud Storage. +It handles the interaction with the Vertex AI SDK to manage prompt versions +and uses Cloud Storage to persist prompt metadata. + +Key functionalities include: +- Initializing a connection to Google Cloud Platform services (Vertex AI, Cloud Storage). +- Caching and refreshing a list of existing prompts. +- Saving new or updated prompts, including their metadata, to both the + Vertex AI prompt registry and a Cloud Storage bucket. +- Loading existing prompts from the registry and their associated metadata + from the bucket. +- Generating responses from a specified model using a loaded prompt and + a given set of variables. +- Helper functions for escaping special characters in prompt text. + +The `model_response` Pydantic model defines the expected structure of the +response from the generative model. +""" + +import json +import logging +import os +import warnings +from typing import Any + +from dotenv import load_dotenv +from google import genai +from google.cloud import aiplatform, storage +from vertexai.generative_models import GenerationConfig +from vertexai.preview import prompts +from vertexai.preview.prompts import Prompt + +# Suppress the UserWarning from vertexai.generative_models as we need to use it +# for Prompt Management until google-genai SDK supports it. +warnings.filterwarnings( + "ignore", category=UserWarning, module="vertexai.generative_models" +) + +load_dotenv("src/.env") + +# Configure logging to the console +logging.basicConfig( + level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s" +) +logger = logging.getLogger(__name__) + +aiplatform.init( + project=os.getenv("PROJECT_ID"), + location=os.getenv("LOCATION"), + staging_bucket=os.getenv("BUCKET"), +) + +# --- Constants --- +PROMPT_PREFIX = os.getenv("PROMPT_PREFIX", "prompts_meta") + + +class GcpPrompt: + """A wrapper class for the Vertex AI Prompt Management service.""" + + def __init__(self) -> None: + """Initializes the GcpPrompt client.""" + self.storage_client = storage.Client() + + self.refresh_prompt_cache() + logger.info("Found %d existing prompts.", len(self.existing_prompts)) + + # Load a default/initial state for the prompt metadata. + self.prompt_meta: dict[str, Any] = {} + + self.prompt_to_run: Prompt = Prompt() + self.refresh_bucket_cache() + + def refresh_prompt_cache(self) -> None: + """Refreshes the local cache of existing prompts from the service.""" + self.existing_prompts = {p.display_name: p.prompt_id for p in prompts.list()} + + def refresh_bucket_cache(self) -> None: + """Refreshes the list of metadata files from GCS.""" + blobs = self.storage_client.list_blobs( + os.getenv("BUCKET"), prefix=PROMPT_PREFIX + ) + self.bucket_cache_index = [i.name for i in blobs] + logger.debug("Found %d metadata files in GCS.", len(self.bucket_cache_index)) + + def _get_metadata_blob_name(self) -> str: + """Constructs the GCS blob name for the prompt metadata file.""" + return ( + f"{PROMPT_PREFIX}/{self.prompt_to_run.prompt_name}_" + f"{self.prompt_to_run._prompt_name}_{self.prompt_to_run._version_id}_" + f"{self.prompt_to_run._version_name}.json" + ) + + def save_prompt(self, check_existing: bool = False) -> str: + """Saves the current prompt. + + If check_existing is True, it ensures a prompt with the same name does not + already exist before creating version "1". + If check_existing is False, it creates a new version of an existing prompt. + + Args: + check_existing: If True, raises an error if a prompt with the same + display name already exists. + + Returns: + A string with the details of the saved prompt version. + """ + logger.info("Attempting to save prompt: %s", self.prompt_to_run.prompt_name) + + if check_existing and self.prompt_to_run.prompt_name in self.existing_prompts: + raise ValueError( + f"Prompt with name '{self.prompt_to_run.prompt_name}' already exists. " + "To create a new version, load the prompt and save from the " + "'Existing Prompt' page." + ) + + if "generation_config" in self.prompt_meta and isinstance( + self.prompt_meta["generation_config"], dict + ): + self.prompt_to_run.generation_config = GenerationConfig( + **self.prompt_meta["generation_config"] + ) + elif not isinstance(self.prompt_to_run.generation_config, GenerationConfig): + logger.warning("No valid generation_config found. Using default.") + self.prompt_to_run.generation_config = GenerationConfig() + + logger.debug("Prompt object being sent to SDK: %s", self.prompt_to_run) + + self.prompt_to_run = prompts.create_version(prompt=self.prompt_to_run) + + sdk_details = ( + f"SDK Prompt Version Details received:\n" + f"- Resource Name: {self.prompt_to_run._prompt_name}\n" + f"- Version ID: {self.prompt_to_run._version_id}\n" + f"- Version Name: {self.prompt_to_run._version_name}\n" + f"- Display Name: {self.prompt_to_run.prompt_name}" + ) + logger.info(sdk_details) + + self.prompt_meta["name"] = self.prompt_to_run.prompt_name + self.write_to_bucket() + + return sdk_details + + def load_prompt(self, prompt_id: str, prompt_name: str, version_id: str) -> None: + """Loads a specific version of a prompt and its associated metadata.""" + self.prompt_to_run = prompts.get(prompt_id, version_id) + self.prompt_to_run.prompt_name = prompt_name + + blob_name = self._get_metadata_blob_name() + + none_blob_name = ( + f"{PROMPT_PREFIX}/{None}_{None}_" + f"{self.prompt_to_run._version_id}_{self.prompt_to_run._version_name}.json" + ) + + self.refresh_bucket_cache() + bucket = self.storage_client.bucket(os.getenv("BUCKET")) + + if blob_name in self.bucket_cache_index: + blob = bucket.blob(blob_name) + self.prompt_meta = json.loads(blob.download_as_string()) + logger.info("Loaded metadata from %s", blob_name) + elif none_blob_name in self.bucket_cache_index: + logger.warning( + "Metadata not found at %s, using fallback %s", blob_name, none_blob_name + ) + blob = bucket.blob(none_blob_name) + self.prompt_meta = json.loads(blob.download_as_string()) + + self.write_to_bucket() + else: + raise FileNotFoundError( + f"Could not find metadata file in GCS for prompt '{prompt_name}' " + f"version '{version_id}'. Looked for '{blob_name}' and '{none_blob_name}'." + ) + + def write_to_bucket(self) -> None: + """Writes the current prompt_meta to a GCS blob.""" + blob_name = self._get_metadata_blob_name() + bucket = self.storage_client.bucket(os.getenv("BUCKET")) + blob = bucket.blob(blob_name) + blob.upload_from_string( + json.dumps(self.prompt_meta, indent=2), content_type="application/json" + ) + logger.info("Wrote metadata to gs://%s/%s", os.getenv("BUCKET"), blob_name) + + def generate_response(self, variables: dict[str, Any]) -> str | None: + """Generates a response from the currently loaded prompt and variables, + handling image uploads. + """ + if not self.prompt_to_run.prompt_data: + raise ValueError("Prompt data is not loaded. Cannot generate response.") + + client = genai.Client( + vertexai=True, + project=os.getenv("PROJECT_ID"), + location=os.getenv("LOCATION"), + ) + updated_contents: list[Any] = [] + for key, value in variables.items(): + logger.info("Iterating through variables") + if key == "image": + if "jpg" in value or "jpeg" in value: + mime_type = "image/jpeg" + elif "png" in value: + mime_type = "image/png" + else: + logger.warning( + f"Unsupported image type for {value}, attempting to process as is." + ) + updated_contents.append(value) + continue + + try: + # Create Image Part from URI + updated_contents.append( + genai.types.Part.from_uri(mime_type=mime_type, file_uri=value) + ) + except Exception as e: + logger.error( + "Error creating Part from URI for variable '%s': %s", + key, + e, + exc_info=True, + ) + updated_contents.append(value) + else: + updated_contents.append(value) + + # Append the prompt text if it's not already included in variables + prompt_text = self.prompt_to_run.prompt_data + if not any( + isinstance(item, str) and item == prompt_text for item in updated_contents + ): + updated_contents.append(prompt_text) + + logger.info("Generating response with contents: %s", updated_contents) + + model = self.prompt_to_run.model_name + response = client.models.generate_content( + model=model, contents=updated_contents + ) + + return response.text if response and response.text else None + + def get_generation_config_dict(self): + """Returns the generation configuration as a dictionary.""" + return self.prompt_to_run.generation_config.to_dict() + + +def escape_special_characters(text: str) -> str: + """Escapes special characters for embedding in a string. + + Note: This function is also present in gcp_dataset.py and could be moved + to a shared utility module in the future. + """ + if not isinstance(text, str): + return text + text = text.replace("\\", "\\\\") + text = text.replace("\n", "\\n") + text = text.replace("\r", "\\r") + text = text.replace("\t", "\\t") + return text.replace('"', '\\"') diff --git a/tools/llmevalkit/src/vapo_lib.py b/tools/llmevalkit/src/vapo_lib.py new file mode 100644 index 0000000..1924d5a --- /dev/null +++ b/tools/llmevalkit/src/vapo_lib.py @@ -0,0 +1,27 @@ +# Copyright 2024 Google LLC +# +# 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 +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Utility functions and classes for the VAPO notebook. + +This file imports all the functions and classes from the original vapo_lib.py file. +""" + +import os +import sys + +repo_root = os.path.abspath(os.path.join(os.path.dirname(__file__), "../../..")) +if repo_root not in sys.path: + sys.path.append(repo_root) + +from gemini.prompts.prompt_optimizer.vapo_lib import * # noqa: F403 diff --git a/tools/llmevalkit/tests/test_evaluation_page.py b/tools/llmevalkit/tests/test_evaluation_page.py new file mode 100644 index 0000000..633f0c0 --- /dev/null +++ b/tools/llmevalkit/tests/test_evaluation_page.py @@ -0,0 +1,30 @@ +import os +import sys +from unittest.mock import patch + +from streamlit.testing.v1 import AppTest + +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))) + +os.environ["PROJECT_ID"] = "test-project" +os.environ["LOCATION"] = "us-central1" +os.environ["BUCKET"] = "test-bucket" + + +@patch("vertexai.preview.prompts.list_versions") +@patch("src.gcp_prompt.GcpPrompt") +@patch("google.cloud.storage.Client") +@patch("vertexai.init") +def test_evaluation_load_prompt(mock_init, mock_storage, mock_gcp_prompt, mock_prompts): + # Setup mock behavior + mock_instance = mock_gcp_prompt.return_value + mock_instance.existing_prompts = {"test_prompt": "123"} + + at = AppTest.from_file("pages/3_Evaluation.py") + at.run(timeout=30) + + # Verify the warning is triggered + at.button(key="load_prompt_button").click().run(timeout=30) + + warnings = [w.value for w in getattr(at, "warning", [])] + assert any("Please select a prompt before loading." in w for w in warnings) diff --git a/translation/README.md b/translation/README.md new file mode 100644 index 0000000..cc1171c --- /dev/null +++ b/translation/README.md @@ -0,0 +1,8 @@ +# Translation + +This directory contains notebooks and resources for using the Google Cloud Translation API. + +## Notebooks + +- [Getting Started with Translation](intro_translation.ipynb): Learn how to use the Google Cloud Translation API to translate text in 130+ languages. +- [Generate training dataset for Cloud Translation API NMT (Neural Machine Translation) model training](translation_training_data_tsv_generator.ipynb): Learn how to generate a TSV file out of documents (docx) for training a custom NMT (neural machine translation) model. diff --git a/translation/intro_translation.ipynb b/translation/intro_translation.ipynb new file mode 100644 index 0000000..929dad1 --- /dev/null +++ b/translation/intro_translation.ipynb @@ -0,0 +1,515 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "ur8xi4C7S06n" + }, + "outputs": [], + "source": [ + "# Copyright 2024 Google LLC\n", + "#\n", + "# Licensed under the Apache License, Version 2.0 (the \"License\");\n", + "# you may not use this file except in compliance with the License.\n", + "# You may obtain a copy of the License at\n", + "#\n", + "# https://www.apache.org/licenses/LICENSE-2.0\n", + "#\n", + "# Unless required by applicable law or agreed to in writing, software\n", + "# distributed under the License is distributed on an \"AS IS\" BASIS,\n", + "# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n", + "# See the License for the specific language governing permissions and\n", + "# limitations under the License." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "JAPoU8Sm5E6e" + }, + "source": [ + "# Getting Started with Translation\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + "
    \n", + " \n", + " \"Google
    Open in Colab\n", + "
    \n", + "
    \n", + " \n", + " \"Google
    Open in Colab Enterprise\n", + "
    \n", + "
    \n", + " \n", + " \"Vertex
    Open in Vertex AI Workbench\n", + "
    \n", + "
    \n", + " \n", + " \"GitHub
    View on GitHub\n", + "
    \n", + "
    \n", + "\n", + "
    \n", + "\n", + "Share to:\n", + "\n", + "\n", + " \"LinkedIn\n", + "\n", + "\n", + "\n", + " \"Bluesky\n", + "\n", + "\n", + "\n", + " \"X\n", + "\n", + "\n", + "\n", + " \"Reddit\n", + "\n", + "\n", + "\n", + " \"Facebook\n", + "" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "adb516335e46" + }, + "source": [ + "| Author |\n", + "| --- |\n", + "| [Holt Skinner](https://github.com/holtskinner) |" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "tvgnzT1CKxrO" + }, + "source": [ + "## Overview\n", + "\n", + "This notebook demonstrates how to use the [Google Cloud Translation API](https://cloud.google.com/translate) to translate text in [130+ languages](https://cloud.google.com/translate/docs/languages)." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "d975e698c9a4" + }, + "source": [ + "### Objective\n", + "\n", + "This tutorial uses the following Google Cloud AI services and resources:\n", + "\n", + "- [Cloud Translation API](https://cloud.google.com/translate/docs/overview)\n", + "- Cloud Storage\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "aed92deeb4a0" + }, + "source": [ + "### Costs\n", + "\n", + "This tutorial uses billable components of Google Cloud:\n", + "\n", + "* Cloud Translation\n", + "* Cloud Storage\n", + "\n", + "Learn about [Translate pricing](https://cloud.google.com/translate/pricing),\n", + "and [Cloud Storage pricing](https://cloud.google.com/storage/pricing),\n", + "and use the [Pricing Calculator](https://cloud.google.com/products/calculator/)\n", + "to generate a cost estimate based on your projected usage." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "i7EUnXsZhAGF" + }, + "source": [ + "## Getting Started\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "NNSWiCNPjh_p" + }, + "source": [ + "### Install Vertex AI SDK, other packages and their dependencies\n", + "\n", + "Install the following packages required to execute this notebook." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "2b4ef9b72d43" + }, + "outputs": [], + "source": [ + "# Install the packages\n", + "%pip install --user --upgrade -q google-cloud-translate" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "YbMFqPZ3tnwz" + }, + "source": [ + "Set the project and region.\n", + "\n", + "* Please note the **available regions** for Translation, see [documentation](https://cloud.google.com/translate/docs/advanced/endpoints)" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": { + "id": "GjSsu6cmUdEx" + }, + "outputs": [], + "source": [ + "# Use the environment variable if the user doesn't provide Project ID.\n", + "import os\n", + "\n", + "PROJECT_ID = \"[your-project-id]\" # @param {type: \"string\", placeholder: \"[your-project-id]\", isTemplate: true}\n", + "if not PROJECT_ID or PROJECT_ID == \"[your-project-id]\":\n", + " PROJECT_ID = str(os.environ.get(\"GOOGLE_CLOUD_PROJECT\"))\n", + "\n", + "LOCATION = os.environ.get(\"GOOGLE_CLOUD_REGION\", \"us-central1\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "opUxT_k5TdgP" + }, + "source": [ + "### Authenticating your notebook environment\n", + "\n", + "* If you are using **Colab** to run this notebook, run the cell below and continue.\n", + "* If you are using **Vertex AI Workbench**, check out the setup instructions [here](https://github.com/GoogleCloudPlatform/generative-ai/tree/main/setup-env)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "vbNgv4q1T2Mi" + }, + "outputs": [], + "source": [ + "import sys\n", + "\n", + "# Additional authentication is required for Google Colab\n", + "if \"google.colab\" in sys.modules:\n", + " # Authenticate user to Google Cloud\n", + " from google.colab import auth\n", + "\n", + " auth.authenticate_user()\n", + "\n", + " ! gcloud config set project {PROJECT_ID}\n", + " ! gcloud auth application-default login -q" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "960505627ddf" + }, + "source": [ + "### Import libraries" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": { + "id": "PyQmSRbKA8r-" + }, + "outputs": [], + "source": [ + "from google.cloud import translate" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "v4gUI8WqciKS" + }, + "source": [ + "### Create client" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": { + "id": "6Pl3un_YciKS" + }, + "outputs": [], + "source": [ + "client = translate.TranslationServiceClient(\n", + " # Optional: https://cloud.google.com/translate/docs/advanced/endpoints\n", + " # client_options=ClientOptions(\n", + " # api_endpoint=f\"translate-{TRANSLATE_LOCATION}.googleapis.com\"\n", + " # )\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "v5CEc4-Wrjk2" + }, + "source": [ + "### Create helper functions" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": { + "id": "kYx2wwhjrmD6" + }, + "outputs": [], + "source": [ + "def translate_text(\n", + " text: str,\n", + " project_id: str = PROJECT_ID,\n", + " location: str = LOCATION,\n", + " glossary: str | None = None,\n", + ") -> translate.TranslateTextResponse:\n", + " \"\"\"Translating Text.\"\"\"\n", + " # Translate text from English to Spanish\n", + " # Detail on supported types can be found here:\n", + " # https://cloud.google.com/translate/docs/supported-formats\n", + " response = client.translate_text(\n", + " request=translate.TranslateTextRequest(\n", + " parent=client.common_location_path(project_id, location),\n", + " contents=[text],\n", + " # Supported language codes: https://cloud.google.com/translate/docs/languages\n", + " source_language_code=\"en\",\n", + " target_language_code=\"es\",\n", + " glossary_config=(\n", + " translate.TranslateTextGlossaryConfig(glossary=glossary)\n", + " if glossary\n", + " else None\n", + " ),\n", + " )\n", + " )\n", + "\n", + " return response\n", + "\n", + "\n", + "def create_glossary(\n", + " input_uri: str,\n", + " glossary_id: str,\n", + " project_id: str = PROJECT_ID,\n", + " location: str = LOCATION,\n", + " timeout: int = 180,\n", + ") -> translate.Glossary:\n", + " \"\"\"\n", + " Create a unidirectional glossary. Glossary can be words or\n", + " short phrases (usually fewer than five words).\n", + " https://cloud.google.com/translate/docs/advanced/glossary#format-glossary\n", + " \"\"\"\n", + " glossary = translate.Glossary(\n", + " name=client.glossary_path(project_id, location, glossary_id),\n", + " # Supported language codes: https://cloud.google.com/translate/docs/languages\n", + " language_pair=translate.Glossary.LanguageCodePair(\n", + " source_language_code=\"en\", target_language_code=\"es\"\n", + " ),\n", + " input_config=translate.GlossaryInputConfig(\n", + " gcs_source=translate.GcsSource(input_uri=input_uri)\n", + " ),\n", + " )\n", + "\n", + " # glossary is a custom dictionary Translation API uses\n", + " # to translate the domain-specific terminology.\n", + " operation = client.create_glossary(\n", + " parent=client.common_location_path(project_id, location), glossary=glossary\n", + " )\n", + "\n", + " result = operation.result(timeout)\n", + " return result" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "a93a71e75b78" + }, + "source": [ + "Now let's try to translate a simple phrase from English to Spanish." + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": { + "id": "2e8e84e860db" + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Translated text: ÂĄHola!\n" + ] + } + ], + "source": [ + "response = translate_text(\"Hi there!\")\n", + "\n", + "# Display the translation for each input text provided\n", + "for translation in response.translations:\n", + " print(f\"Translated text: {translation.translated_text}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "6a84167808aa" + }, + "source": [ + "## Glossaries" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "bd76360f8068" + }, + "source": [ + "That looks great! However, let's look at what happens if we try to translate a technical word, such as the Google Cloud product [Compute Engine](https://cloud.google.com/compute?hl=en)." + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "metadata": { + "id": "894401e53921" + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Translated text: Motor de ComputaciĂłn\n" + ] + } + ], + "source": [ + "response = translate_text(\"Compute Engine\")\n", + "\n", + "# Display the translation for each input text provided\n", + "for translation in response.translations:\n", + " print(f\"Translated text: {translation.translated_text}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "6760e9c66d88" + }, + "source": [ + "### Create a Glossary" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "120088b42a11" + }, + "source": [ + "Notice that the Translation API translated the name literally.\n", + "\n", + "Suppose we want this name to be the same in all languages, we can create a [Glossary](https://cloud.google.com/translate/docs/advanced/glossary) to consistently translate domain-specific words and phrases.\n", + "\n", + "Next, we'll create a glossary for lots of Google Cloud product names to indicate how they should be translated into Spanish.\n", + "\n", + "We've already created an input TSV file and uploaded it to a publicly-accessible Cloud Storage bucket." + ] + }, + { + "cell_type": "code", + "execution_count": 32, + "metadata": { + "id": "25714948fd80" + }, + "outputs": [], + "source": [ + "glossary = create_glossary(\n", + " input_uri=\"gs://github-repo/translation/GoogleCloudGlossary.tsv\",\n", + " glossary_id=\"google_cloud_english_to_spanish\",\n", + ")\n", + "print(glossary)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "059abb41d254" + }, + "source": [ + "Now, let's try translating the text again using the glossary." + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": { + "id": "0c9e51afa18f" + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Default Translated text: Motor de ComputaciĂłn\n", + "Glossary Translated text: Compute Engine\n" + ] + } + ], + "source": [ + "response = translate_text(\"Compute Engine\", glossary=glossary.name)\n", + "\n", + "# Display the translation for each input text provided\n", + "for translation in response.translations:\n", + " print(f\"Default Translated text: {translation.translated_text}\")\n", + "\n", + "for translation in response.glossary_translations:\n", + " print(f\"Glossary Translated text: {translation.translated_text}\")" + ] + } + ], + "metadata": { + "colab": { + "name": "intro_translation.ipynb", + "toc_visible": true + }, + "kernelspec": { + "display_name": "Python 3", + "name": "python3" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} diff --git a/translation/translation_training_data_tsv_generator.ipynb b/translation/translation_training_data_tsv_generator.ipynb new file mode 100644 index 0000000..72c7b1d --- /dev/null +++ b/translation/translation_training_data_tsv_generator.ipynb @@ -0,0 +1,790 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "2eec5cc39a59" + }, + "outputs": [], + "source": [ + "# Copyright 2024 Google LLC\n", + "#\n", + "# Licensed under the Apache License, Version 2.0 (the \"License\");\n", + "# you may not use this file except in compliance with the License.\n", + "# You may obtain a copy of the License at\n", + "#\n", + "# https://www.apache.org/licenses/LICENSE-2.0\n", + "#\n", + "# Unless required by applicable law or agreed to in writing, software\n", + "# distributed under the License is distributed on an \"AS IS\" BASIS,\n", + "# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n", + "# See the License for the specific language governing permissions and\n", + "# limitations under the License." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "594c6f39b5d1" + }, + "source": [ + "# Generate training dataset for Cloud Translation API NMT (Neural Machine Translation) model training\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + "
    \n", + " \n", + " \"Google
    Open in Colab\n", + "
    \n", + "
    \n", + " \n", + " \"Google
    Open in Colab Enterprise\n", + "
    \n", + "
    \n", + " \n", + " \"Vertex
    Open in Vertex AI Workbench\n", + "
    \n", + "
    \n", + " \n", + " \"GitHub
    View on GitHub\n", + "
    \n", + "
    \n", + "\n", + "
    \n", + "\n", + "Share to:\n", + "\n", + "\n", + " \"LinkedIn\n", + "\n", + "\n", + "\n", + " \"Bluesky\n", + "\n", + "\n", + "\n", + " \"X\n", + "\n", + "\n", + "\n", + " \"Reddit\n", + "\n", + "\n", + "\n", + " \"Facebook\n", + "" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "86c1d4a789c2" + }, + "source": [ + "| | |\n", + "|-|-|\n", + "|Author | [Abhijat Gupta](https://github.com/abhijat-gupta)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "25e10371ed0e" + }, + "source": [ + "## **Overview**\n", + "\n", + "[Cloud Translation API](https://cloud.google.com/translate/docs) uses Google's neural machine translation technology to let you dynamically translate text through the API using a Google pre-trained, custom model, or a translation specialized large language model (LLMs). \n", + "\n", + "It comes in [Basic and Advanced](https://cloud.google.com/translate/docs/editions) editions. Both provide fast and dynamic translation, but Advanced offers customization features, such as domain-specific translation, formatted document translation, and batch translation.\n", + "\n", + "[AutoML Translation](https://cloud.google.com/translate/docs/advanced/automl-beginner) lets you build custom models (without writing code) that are tailored for your domain-specific content compared to the default Google Neural Machine Translation (NMT) model\n", + "\n", + "The first 500,000 characters sent to the API to process (Basic and Advanced combined) per month are free (not applicable to LLMs).\n", + "\n", + "## Objective\n", + "\n", + "### Key Features\n", + "1. Paragraphs are converted into line-pairs of less than 200 words.\n", + "2. Tables in documents are converted into a line-pair with each row as a separate line-pair.\n", + "3. Limit of 200 words per line is handled.\n", + "4. Empty or blank lines are not added to the TSV.\n", + "\n", + "This notebook enables you to generate a TSV file out of documents (docx) for training NMT (neural machine translation) model. The generated TSV file will contain the source and target line pairs for 2 languages in 2 columns respectively. Limit of 200 words for a line is handled within the code. Example: If a line is exceeding 200 words, it won't be added to the training dataset, but will be captured and returned in a dictionary so that you can decide on how to convert it to line-pair of less than 200 words.\n", + "The code also removes any blank or empty lines in a document from both source and reference before making line-pairs. This makes sure that both the documents do not mismatch with line-pairs due to empty lines.\n", + "\n", + "\n", + "## How to use the notebook\n", + "\n", + "##### input: a dictionary containing source and reference GCS paths.\n", + "\n", + "##### output: a single TSV file, 2 dictionaries\n", + "\n", + "##### Steps to follow:\n", + "- Provide as many source and reference files in the input dictionary: `source_ref_dictionary`, *key* being the source file path and reference file path as its *value*\n", + "- Trigger all the cells after providing the input.\n", + "- The TSV gets created in your local path.\n", + "\n", + "\n", + "\n", + "## Costs\n", + "\n", + "Learn about [Translation pricing](https://cloud.google.com/translate/pricing) and use the [Pricing Calculator](https://cloud.google.com/products/calculator/) to generate a cost estimate based on your projected usage." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "628e815b6b1f" + }, + "source": [ + "## **Getting Started**\n", + "### Install docx SDK for Python" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": { + "id": "a6a9e6e0448d" + }, + "outputs": [], + "source": [ + "%pip install --proxy \"\" docx --quiet\n", + "%pip install --proxy \"\" python-docx --quiet" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "2d4000d88ad8" + }, + "source": [ + "### Restart kernel" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "0c5492fd0156" + }, + "outputs": [], + "source": [ + "# Restart kernel after installs so that your environment can access the new packages\n", + "import IPython\n", + "\n", + "app = IPython.Application.instance()\n", + "app.kernel.do_shutdown(True)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "6582b5d47c28" + }, + "source": [ + "### Authenticate your notebook environment (Colab only)\n", + "\n", + "If you are running this notebook on Google Colab, run the following cell to authenticate your environment. This step is not required if you are using [Vertex AI Workbench](https://cloud.google.com/vertex-ai-notebooks?hl=en)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "4788c6f28f01" + }, + "outputs": [], + "source": [ + "import sys\n", + "\n", + "# Additional authentication is required for Google Colab\n", + "if \"google.colab\" in sys.modules:\n", + " # Authenticate user to Google Cloud\n", + " from google.colab import auth\n", + "\n", + " auth.authenticate_user()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "9ccc6635848a" + }, + "source": [ + "### imports" + ] + }, + { + "cell_type": "code", + "execution_count": 62, + "metadata": { + "id": "9eb336b1b801" + }, + "outputs": [], + "source": [ + "import json\n", + "import os\n", + "\n", + "import docx\n", + "from docx.document import Document as _Document\n", + "from docx.oxml.table import CT_Tbl\n", + "from docx.oxml.text.paragraph import CT_P\n", + "from docx.table import Table, _Cell\n", + "from docx.text.paragraph import Paragraph\n", + "import google.auth\n", + "from google.auth.credentials import Credentials\n", + "from google.cloud import storage\n", + "import requests" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "bf8a23062635" + }, + "source": [ + "### output TSV file name" + ] + }, + { + "cell_type": "code", + "execution_count": 77, + "metadata": { + "id": "df55cc8143be" + }, + "outputs": [], + "source": [ + "# file name for the output tabular TSV.\n", + "tsv_file_name = \"your_tsv_file_name.tsv\" # @param {type:\"string\"}\n", + "PROJECT_ID = \"your project id\" # @param {type:\"string\"}\n", + "LOCATION = \"us-central1\" # @param {type:\"string\"}\n", + "DEFAULT_SOURCE_LANG_CODE = \"\" # @param {type:\"string\"}\n", + "DEFAULT_DATASET_PREFIX = \"\" # @param {type:\"string\"}\n", + "DEFAULT_DATASET_SUFFIX = \"\" # @param {type:\"string\"}\n", + "\n", + "url = (\n", + " f\"https://translation.googleapis.com/v3/projects/{PROJECT_ID}/locations/{LOCATION}\"\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "a072ff983a3c" + }, + "source": [ + "### source and reference paths" + ] + }, + { + "cell_type": "code", + "execution_count": 58, + "metadata": { + "id": "51d246d095b8" + }, + "outputs": [], + "source": [ + "source_ref_dictionary = {\n", + " \"source_path1.docx\": \"reference_path1.docx\",\n", + " \"source_path2.docx\": \"reference_path2.docx\",\n", + "}" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "7a8d61c373c5" + }, + "source": [ + "### Generate TSV" + ] + }, + { + "cell_type": "code", + "execution_count": 71, + "metadata": { + "id": "a56ca18f530a" + }, + "outputs": [], + "source": [ + "def get_document_objects(\n", + " src_path: str, ref_path: str, source_bucket_name: str\n", + ") -> tuple[_Document, _Document]:\n", + " \"\"\"Fetches a source document and its translated/reference version from GCS bucket.\"\"\"\n", + "\n", + " client = storage.Client()\n", + " ref_file_name = ref_path.split(source_bucket_name + \"/\")[1]\n", + " file_name = src_path.split(source_bucket_name + \"/\")[1]\n", + "\n", + " try:\n", + " bucket = client.get_bucket(source_bucket_name)\n", + " src_blob = bucket.get_blob(file_name)\n", + " ref_blob = bucket.get_blob(ref_file_name)\n", + " except TypeError as te:\n", + " return te\n", + "\n", + " src_file_downloaded_name = file_name.split(\"source/\")[1]\n", + " ref_file_downloaded_name = ref_file_name.split(\"reference/\")[1]\n", + "\n", + " src_filepath = os.path.join(os.getcwd(), src_file_downloaded_name + \"_local.docx\")\n", + " ref_filepath = os.path.join(os.getcwd(), ref_file_downloaded_name + \"_local.docx\")\n", + "\n", + " with open(src_filepath, \"wb\") as src_f:\n", + " src_blob.download_to_file(src_f)\n", + " src_f.close()\n", + "\n", + " with open(ref_filepath, \"wb\") as ref_f:\n", + " ref_blob.download_to_file(ref_f)\n", + " ref_f.close()\n", + "\n", + " source = docx.Document(src_filepath)\n", + " reference = docx.Document(ref_filepath)\n", + "\n", + " return source, reference\n", + "\n", + "\n", + "def iter_block_items(parent: _Document) -> Paragraph or Table:\n", + " \"\"\"\n", + " Generate a reference to each paragraph and table child within *parent*,\n", + " in document order. Each returned value is an instance of either Table or\n", + " Paragraph. *parent* would most commonly be a reference to a main\n", + " Document object, but also works for a _Cell object, which itself can\n", + " contain paragraphs and tables.\n", + " \"\"\"\n", + " if isinstance(parent, _Document):\n", + " parent_elm = parent.element.body\n", + " elif isinstance(parent, _Cell):\n", + " parent_elm = parent._tc\n", + " elif isinstance(parent, _Row):\n", + " parent_elm = parent._tr\n", + " else:\n", + " raise ValueError(\"something's not right\")\n", + " for child in parent_elm.iterchildren():\n", + " if isinstance(child, CT_P):\n", + " yield Paragraph(child, parent)\n", + " elif isinstance(child, CT_Tbl):\n", + " yield Table(child, parent)\n", + "\n", + "\n", + "def make_tsv(source_ref_dictionary: dict, tsv_file_name: str) -> tuple[dict, dict]:\n", + " \"\"\"\n", + " - This function reads the source and reference/translated documents from local paths iteratively, block-by-block.\n", + " - A page blocks can be: Paragraphs and Tables.\n", + " - In order to generate correct pairs, the type of blocks should be same for both source and reference.\n", + " - If a block don't match, it get captured in mismatched_block dictionary and will not be added to the TSV. The Iteration stops and a TSV is created uptill the matching blocks.\n", + " - ONLY docx format is supported.\n", + " - Creates and saves the TSV in local path(Can be configured to save in GCS bucket).\n", + " - Returns the mismatched blocks from the documents as a dictionary.\n", + " \"\"\"\n", + "\n", + " for src_path, ref_path in source_ref_dictionary.items():\n", + " if src_path is None or src_path == \"\":\n", + " return \"source file path is invalid.\"\n", + " if ref_path is None or ref_path == \"\":\n", + " return \"translated/reference file path is invalid.\"\n", + " if src_path.split(\".\", -1)[::-1][0] != ref_path.split(\".\", -1)[::-1][0]:\n", + " return \"source and translated versions are in different format.\"\n", + "\n", + " tsv_file = os.path.join(os.getcwd(), tsv_file_name)\n", + " if \".pdf\" in src_path.split(src_path.split(\"gs://\")[1].split(\"/\")[0] + \"/\")[1]:\n", + " return \"PDFs are not supported. Process exited.\"\n", + "\n", + " try:\n", + " mismatched_block = {}\n", + " more_than_200_words = {}\n", + " for source_path, reference_path in source_ref_dictionary.items():\n", + " source_bucket_name = source_path.split(\"gs://\")[1].split(\"/\")[0]\n", + " source, reference = get_document_objects(\n", + " source_path, reference_path, source_bucket_name\n", + " )\n", + "\n", + " with open(tsv_file, \"a\") as tsv_f:\n", + " for para in source.paragraphs:\n", + " if len(para.text.strip()) == 0:\n", + " p = para._element\n", + " p.getparent().remove(p)\n", + " p._p = p._element = None\n", + " for para in reference.paragraphs:\n", + " if len(para.text.strip()) == 0:\n", + " p = para._element\n", + " p.getparent().remove(p)\n", + " p._p = p._element = None\n", + "\n", + " for src_block, ref_block in zip(\n", + " iter_block_items(source), iter_block_items(reference)\n", + " ):\n", + " if (\n", + " isinstance(src_block, Paragraph)\n", + " and isinstance(ref_block, Paragraph)\n", + " and src_block.text is not None\n", + " and ref_block.text is not None\n", + " ):\n", + " try:\n", + " tsv_f.write(src_block.text + \"\\t\" + ref_block.text)\n", + " tsv_f.write(\"\\n\")\n", + " except Exception as e:\n", + " print(e)\n", + " elif isinstance(src_block, Table) and isinstance(ref_block, Table):\n", + " try:\n", + " for src_row, ref_row in zip(src_block.rows, ref_block.rows):\n", + " src_row_data = []\n", + " ref_row_data = []\n", + " for cell in src_row.cells:\n", + " for paragraph in cell.paragraphs:\n", + " src_row_data.append(paragraph.text)\n", + " for cell in ref_row.cells:\n", + " for paragraph in cell.paragraphs:\n", + " ref_row_data.append(paragraph.text)\n", + " if len(src_row_data) >= 200 or len(ref_row_data) >= 200:\n", + " print(\n", + " \"Length of a pair detected to be greater than 200 words.\"\n", + " )\n", + " print(\"this pair will be skipped\")\n", + " more_than_200_words[\" \".join(src_row_data)] = (\n", + " \" \".join(ref_row_data)\n", + " )\n", + " else:\n", + " tsv_f.write(\n", + " \" \".join(src_row_data)\n", + " + \"\\t\"\n", + " + \" \".join(ref_row_data)\n", + " )\n", + " tsv_f.write(\"\\n\")\n", + " except Exceptio as e:\n", + " print(e)\n", + " else:\n", + " try:\n", + " mismatched_block[src_block.text] = ref_block\n", + " except:\n", + " mismatched_block[src_block] = ref_block.text\n", + " break\n", + "\n", + " tsv_f.close()\n", + " print(f\"Generated TSV stored at {tsv_file}\")\n", + " return mismatched_block, more_than_200_words\n", + " except Exception as e:\n", + " print(e)" + ] + }, + { + "cell_type": "code", + "execution_count": 72, + "metadata": { + "id": "c242ace80a3b" + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Generated TSV stored at /home/jupyter/src/your_tsv_file_name.tsv\n" + ] + } + ], + "source": [ + "mismatched_block, more_than_200_words = make_tsv(source_ref_dictionary, tsv_file_name)" + ] + }, + { + "cell_type": "code", + "execution_count": 73, + "metadata": { + "id": "79de10f3c921" + }, + "outputs": [ + { + "data": { + "text/plain": [ + "{}" + ] + }, + "execution_count": 73, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "mismatched_block" + ] + }, + { + "cell_type": "code", + "execution_count": 74, + "metadata": { + "id": "75fecf1a251a" + }, + "outputs": [ + { + "data": { + "text/plain": [ + "{}" + ] + }, + "execution_count": 74, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "more_than_200_words" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "8ecb64ddb0cd" + }, + "source": [ + "## Custom model training" + ] + }, + { + "cell_type": "code", + "execution_count": 85, + "metadata": { + "id": "8297f0a3814f" + }, + "outputs": [], + "source": [ + "def generate_access_token() -> Credentials:\n", + " \"\"\"Generates access token to call translate APIs.\"\"\"\n", + " creds, project = google.auth.default()\n", + "\n", + " auth_req = google.auth.transport.requests.Request()\n", + " creds.refresh(auth_req)\n", + " return creds.token\n", + "\n", + "\n", + "def create_dataset(\n", + " target_lang_code: str,\n", + " url: str,\n", + " source_lang_code: str | None = DEFAULT_SOURCE_LANG_CODE,\n", + ") -> dict or None:\n", + " \"\"\"Creates a dataset.\"\"\"\n", + " ACCESS_TOKEN = generate_access_token()\n", + " headers = {\n", + " \"Authorization\": f\"Bearer {ACCESS_TOKEN}\",\n", + " \"Content-Type\": \"application/json; charset=UTF-8\",\n", + " }\n", + "\n", + " if DEFAULT_DATASET_SUFFIX != \"\" and DEFAULT_DATASET_SUFFIX is not None:\n", + " dataset_display_name = f\"{DEFAULT_DATASET_PREFIX}_{source_lang_code}_to_{target_lang_code}_{DEFAULT_DATASET_SUFFIX}\"\n", + " else:\n", + " dataset_display_name = (\n", + " f\"{DEFAULT_DATASET_PREFIX}_{source_lang_code}_to_{target_lang_code}\"\n", + " )\n", + "\n", + " data = {\n", + " \"display_name\": dataset_display_name,\n", + " \"source_language_code\": source_lang_code,\n", + " \"target_language_code\": target_lang_code,\n", + " }\n", + " dataset_url = f\"{url}/datasets\"\n", + " try:\n", + " response = requests.post(dataset_url, data=json.dumps(data), headers=headers)\n", + " data_create_response = json.loads(response.text)\n", + " return data_create_response\n", + " except Exception as e:\n", + " return e\n", + "\n", + "\n", + "def fetch_dataset_id(name: str, url: str) -> str or None:\n", + " \"\"\"Fetches dataset id for the given dataset name.\"\"\"\n", + " ACCESS_TOKEN = generate_access_token()\n", + " headers = {\n", + " \"Authorization\": f\"Bearer {ACCESS_TOKEN}\",\n", + " \"Content-Type\": \"application/json; charset=UTF-8\",\n", + " }\n", + " print(f\"dataset name provided: {name}\")\n", + "\n", + " fetch_dataset_url = f\"{url}/datasets\"\n", + " datasets = requests.get(fetch_dataset_url, headers=headers)\n", + " dataset_list = json.loads(datasets.text)\n", + " all_datasets = dataset_list[\"datasets\"]\n", + "\n", + " for dataset_details in all_datasets:\n", + " if name.lower() == dataset_details[\"displayName\"].lower():\n", + " print(dataset_details[\"name\"].split(\"/\", -1)[::-1][0])\n", + " return dataset_details[\"name\"].split(\"/\", -1)[::-1][0]\n", + " return\n", + "\n", + "\n", + "def import_data(url: str, dataset_id: str, tsv_uri: str) -> dict or None:\n", + " \"\"\"Imports TSV into a translation dataset.\"\"\"\n", + " if dataset_id is None:\n", + " return \"valid Dataset not found. Exiting.\"\n", + "\n", + " ACCESS_TOKEN = generate_access_token()\n", + " headers = {\n", + " \"Authorization\": f\"Bearer {ACCESS_TOKEN}\",\n", + " \"Content-Type\": \"application/json; charset=UTF-8\",\n", + " }\n", + "\n", + " print(f\"Dataset used: {dataset_id}\")\n", + "\n", + " data = {\n", + " \"input_config\": {\n", + " \"input_files\": [\n", + " {\n", + " \"display_name\": \"training_data.tsv\",\n", + " \"usage\": \"UNASSIGNED\",\n", + " \"gcs_source\": {\"input_uri\": tsv_uri},\n", + " }\n", + " ]\n", + " }\n", + " }\n", + "\n", + " importDataset_url = f\"{url}/datasets/{dataset_id}:importData\"\n", + " response = requests.post(importDataset_url, data=json.dumps(data), headers=headers)\n", + " try:\n", + " data_import_response = json.loads(response.text)\n", + " return data_import_response\n", + " except Exception as e:\n", + " print(\"Service unavailable!\", 500)\n", + " return e\n", + "\n", + "\n", + "def train_model(\n", + " model_name: str, project_id: str, location: str, dataset_id: str, url: str\n", + ") -> dict:\n", + " \"\"\"Creates a custom model on top of NMT model\"\"\"\n", + " if dataset_id is None:\n", + " return \"valid dataset not found. Exiting.\"\n", + "\n", + " ACCESS_TOKEN = generate_access_token()\n", + " headers = {\n", + " \"Authorization\": f\"Bearer {ACCESS_TOKEN}\",\n", + " \"Content-Type\": \"application/json; charset=UTF-8\",\n", + " }\n", + "\n", + " data = {\n", + " \"display_name\": model_name,\n", + " \"dataset\": f\"projects/{project_id}/locations/{location}/datasets/{dataset_id}\",\n", + " }\n", + " models_url = f\"{url}/models\"\n", + " print(\n", + " f\"\"\"Model training details:\n", + " \n", + " 'model display name': {model_name},\n", + " 'dataset': {dataset_id}\n", + " \n", + " \"\"\"\n", + " )\n", + " response = requests.post(models_url, data=json.dumps(data), headers=headers)\n", + " try:\n", + " model_training_response = json.loads(response.text)\n", + " return model_training_response\n", + " except Exception as e:\n", + " print(\"Service unavailable!\", 500)\n", + " return e" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "b2bd00d9c381" + }, + "source": [ + "### Create a dataset\n", + "\n", + "Creates a Translation dataset. View in [console](https://console.cloud.google.com/translation/datasets)" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": { + "id": "f23b1f449fe6" + }, + "outputs": [], + "source": [ + "create_dataset(\"de\", url, \"en\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "967544145ee2" + }, + "source": [ + "### Import data\n", + "Imports data into a Translation dataset. View in [console](https://console.cloud.google.com/translation/datasets)" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": { + "id": "759dc4a0b2bd" + }, + "outputs": [], + "source": [ + "import_data(\n", + " url,\n", + " fetch_dataset_id(\n", + " name=(\n", + " f\"{DEFAULT_DATASET_PREFIX}_en_to_de{DEFAULT_DATASET_SUFFIX}\"\n", + " if DEFAULT_DATASET_SUFFIX is not None\n", + " else f\"{DEFAULT_DATASET_PREFIX}_en_to_de\"\n", + " ),\n", + " url=url,\n", + " ),\n", + " f\"/{tsv_file_name}\",\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "f82ee49970f2" + }, + "source": [ + "### Train a model\n", + "\n", + "Triggers training for the given dataset name. View in [console](https://console.cloud.google.com/translation/locations/us-central1/datasets/1372e4ac8f9fa3a9/train)" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": { + "id": "e9597d13b3be" + }, + "outputs": [], + "source": [ + "train_model(\n", + " \"test_model\",\n", + " PROJECT_ID,\n", + " LOCATION,\n", + " fetch_dataset_id(\n", + " name=(\n", + " f\"{DEFAULT_DATASET_PREFIX}_en_to_de{DEFAULT_DATASET_SUFFIX}\"\n", + " if DEFAULT_DATASET_SUFFIX is not None\n", + " else f\"{DEFAULT_DATASET_PREFIX}_en_to_de\"\n", + " ),\n", + " url=url,\n", + " ),\n", + " url,\n", + ")" + ] + } + ], + "metadata": { + "colab": { + "name": "translation_training_data_tsv_generator.ipynb", + "toc_visible": true + }, + "kernelspec": { + "display_name": "Python 3", + "name": "python3" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} diff --git a/vision/README.md b/vision/README.md new file mode 100644 index 0000000..58ac533 --- /dev/null +++ b/vision/README.md @@ -0,0 +1,43 @@ +# Vision + +This directory contains examples and guides for using Google Cloud's models for image and video generation, including Imagen for image generation and [Veo](https://docs.cloud.google.com/gemini-enterprise-agent-platform/models/video/overview) for video generation. + +If you're interested in using the Nano Banana models for image generation, check out these notebooks: +- [Nano Banana 2](../gemini/getting-started/intro_gemini_3_1_flash_image_gen.ipynb): Learn how to generate and edit images with Gemini 3.1 Flash Image. +- [Nano Banana Pro](../gemini/getting-started/intro_gemini_3_image_gen.ipynb): Generate and edit images with Gemini 3 Pro Image. + +## Getting Started + +- [Image Segmentation](getting-started/image_segmentation.ipynb): Segment images using various modes like foreground/background, semantic, and prompt-based segmentation. +- [Imagen 3 Editing](getting-started/imagen3_editing.ipynb): Edit images with Imagen 3 using mask-based editing. +- [Imagen 4 Upscaling](getting-started/imagen4_upscale.ipynb): Upscale images with Imagen 4 by a factor of 2x, 3x, or 4x. +- [Veo 3 Advanced Controls](getting-started/veo3_advanced_controls.ipynb): Explore advanced controls for Veo 3, like frame interpolation and video extension. +- [Veo 3 Reference to Video](getting-started/veo3_reference_to_video.ipynb): Generate videos from asset reference images with Veo 3. +- [Veo 3 Video Generation](getting-started/veo3_video_generation.ipynb): Generate videos with audio from text and image inputs with Veo 3. +- [Virtual Try-On](getting-started/virtual_try_on.ipynb): Perform virtual try-on by providing an image of a model and clothing items. + +## Use Cases + +- [Batch Virtual Try-On](use-cases/batch_virtual_try_on.ipynb): Learn how to perform batch virtual try-on. + +## Sample Apps + +- [V-Start](sample-apps/V-Start/README.md): A sample application that helps users easily and quickly create effective prompts for Veo and evaluate how well generated videos align with their intended prompts. + +## Video Overviews + +**YouTube Video: Introduction to Image Editing** + + + + Introduction to Image Editing + + + +**YouTube Video: Introduction to Video Editing** + + + + Introduction to Video Editing + + diff --git a/vision/getting-started/gemini_omni_flash_video_gen.ipynb b/vision/getting-started/gemini_omni_flash_video_gen.ipynb new file mode 100644 index 0000000..609ffde --- /dev/null +++ b/vision/getting-started/gemini_omni_flash_video_gen.ipynb @@ -0,0 +1,785 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "Jv_d8Y2XCyRm" + }, + "outputs": [], + "source": [ + "# Copyright 2026 Google LLC\n", + "#\n", + "# Licensed under the Apache License, Version 2.0 (the \"License\");\n", + "# you may not use this file except in compliance with the License.\n", + "# You may obtain a copy of the License at\n", + "#\n", + "# https://www.apache.org/licenses/LICENSE-2.0\n", + "#\n", + "# Unless required by applicable law or agreed to in writing, software\n", + "# distributed under the License is distributed on an \"AS IS\" BASIS,\n", + "# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n", + "# See the License for the specific language governing permissions and\n", + "# limitations under the License." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "WIgrkCFIC3Fq" + }, + "source": [ + "# Gemini Omni Flash Video Generation\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + "
    \n", + " \n", + " \"Google
    Open in Colab\n", + "
    \n", + "
    \n", + " \n", + " \"Google
    Open in Colab Enterprise\n", + "
    \n", + "
    \n", + " \n", + " \"Workbench
    Open in Workbench\n", + "
    \n", + "
    \n", + " \n", + " \"GitHub
    View on GitHub\n", + "
    \n", + "
    \n", + "\n", + "
    \n", + "\n", + "

    \n", + "Share to:\n", + "\n", + "\n", + " \"LinkedIn\n", + "\n", + "\n", + "\n", + " \"Bluesky\n", + "\n", + "\n", + "\n", + " \"X\n", + "\n", + "\n", + "\n", + " \"Reddit\n", + "\n", + "\n", + "\n", + " \"Facebook\n", + "\n", + "

    " + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "NbZi7r8HDLd_" + }, + "source": [ + "| Author |\n", + "| --- |\n", + "| [Katie Nguyen](https://github.com/katiemn) |" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "COhyHClwDPFM" + }, + "source": [ + "## Overview\n", + "\n", + "Gemini Omni Flash is a model that understands the world around you, allowing you to animate photos or create videos from any input. Built on Gemini's world understanding and native multimodality, Gemini Omni Flash creates outputs that reflect the logic of the real world and lets you shape them step-by-step through natural conversation.\n", + "\n", + "In this tutorial, you'll learn how to use Gemini Omni Flash in Agent Platform with the Google Gen AI SDK to try out the following scenarios:\n", + "\n", + "- Video generation:\n", + " - Text-to-video generation\n", + " - Providing a reference image as a starting frame\n", + " - Supplying reference images to guide video generation\n", + " - Async video generation\n", + "- Video editing:\n", + " - Prompt-based video editing\n", + " - Multi-turn video editing (chat)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "gIpYz_L8D9eK" + }, + "source": [ + "## Get started" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "axSoEhOzERDm" + }, + "source": [ + "### Install Google Gen AI SDK for Python" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "mfJgJBMeLAWi" + }, + "outputs": [], + "source": [ + "%pip install --upgrade --quiet google-genai" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "cfk06hgxEiYS" + }, + "source": [ + "### Authenticate your notebook environment (Colab only)\n", + "\n", + "If you are running this notebook on Google Colab, run the following cell to authenticate your environment." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "NW-We3ATemq6" + }, + "outputs": [], + "source": [ + "import sys\n", + "\n", + "if \"google.colab\" in sys.modules:\n", + " from google.colab import auth\n", + "\n", + " auth.authenticate_user()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "YF5OKjXxEpp4" + }, + "source": [ + "### Import libraries" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "f1HU9IT_erEb" + }, + "outputs": [], + "source": [ + "import os\n", + "import base64\n", + "\n", + "import matplotlib.image as img\n", + "import matplotlib.pyplot as plt\n", + "from IPython.display import Video, display\n", + "from google import genai\n", + "from google.genai import interactions" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "-yg0mBBaEtXn" + }, + "source": [ + "### Set Google Cloud project information\n", + "\n", + "To get started using Agent Platform, you must have an existing Google Cloud project and [enable the Agent Platform API](https://console.cloud.google.com/flows/enableapi?apiid=aiplatform.googleapis.com).\n", + "\n", + "Learn more about [setting up a project](https://docs.cloud.google.com/resource-manager/docs/creating-managing-projects) and a [development environment](https://cloud.google.com/docs/authentication/set-up-adc-local-dev-environment)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "80On1BQxetma" + }, + "outputs": [], + "source": [ + "# fmt: off\n", + "PROJECT_ID = \"[your-project-id]\" # @param {type: \"string\", placeholder: \"[your-project-id]\", isTemplate: true}\n", + "# fmt: on\n", + "if not PROJECT_ID or PROJECT_ID == \"[your-project-id]\":\n", + " PROJECT_ID = str(os.environ.get(\"GOOGLE_CLOUD_PROJECT\"))\n", + "\n", + "LOCATION = os.environ.get(\"GOOGLE_CLOUD_REGION\", \"global\")\n", + "\n", + "client = genai.Client(enterprise=True, project=PROJECT_ID, location=LOCATION)\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "P2OgDTYvEzZs" + }, + "source": [ + "### Define a helper function to display media" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "ZDN9qlAGe44s" + }, + "outputs": [], + "source": [ + "def show_video(video_bytes):\n", + " data = base64.b64decode(video_bytes)\n", + " with open(\"sample.mp4\", \"wb\") as out_file:\n", + " out_file.write(data)\n", + " display(Video(\"sample.mp4\", embed=True, width=600))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "Cj-ZmZbME24B" + }, + "source": [ + "### Load the video generation model" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "vg2SdM1ue7IM" + }, + "outputs": [], + "source": [ + "omni_model = \"gemini-omni-flash-preview\"" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "hLHGlUYNLIjB" + }, + "source": [ + "## Video generation" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "WxaeXTZlWgrI" + }, + "source": [ + "### Generate videos from a text prompt\n", + "\n", + "With Gemini Omni Flash, you can generate videos directly from text prompts via the [Interactions API](https://docs.cloud.google.com/gemini-enterprise-agent-platform/reference/models/interactions-api). To generate a video in the sample below, specify the following information:\n", + "- **Prompt:** A detailed description of the video you would like to see. For best results, consider attributes such as shot framing, motion, style, lighting, location, and action.\n", + "- **Generation Config:** Within this configuration, you can specify a `VideoConfig` that contains a `task` parameter.\n", + " - **Task:** Valid options are `text_to_video`, `image_to_video`, `reference_to_video`, or `edit`. Make sure to set each task accordingly based on your inputs and desired behavior.\n", + "- **Video Response Format:** Configure `aspect_ratio`, `duration`, and `delivery` parameters. Alternatively, you can specify these configurations through the text prompt.\n", + " - **Aspect ratio:** 16:9 or 9:16\n", + " - **Duration:** 3s - 10s\n", + " - **Delivery:** If you'd like to save your generated videos to GCS, set `delivery=\"uri\"`. Additionally, set `gcs_uri=\"gs://\"`, making sure to replace `` with your desired bucket path. To find your video's location, look inside the content list and copy the value next to `uri=` in the `VideoContent` object.\n", + "\n", + "\n", + "Notes on output videos:\n", + "- **Audio generation:** Audio will be generated alongside the output video.\n", + "- **Resolution:** 720p\n", + "\n", + "All videos from `gemini-omni-flash-preview` include both [C2PA metadata](https://docs.cloud.google.com/gemini-enterprise-agent-platform/models/content-credentials) and a [SynthID watermark](https://deepmind.google/technologies/synthid/)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "mk2tOv2Rf7e8" + }, + "outputs": [], + "source": [ + "prompt = \"The words Gemini Omni Flash displayed boldly and centered. The words rapidly and instantly transform, cycling through different styles, fonts, and materials. The words remains perfectly positioned and constant while the colors, textures, and environments instantaneously change around it.\"\n", + "\n", + "interaction = client.interactions.create(\n", + " model=omni_model,\n", + " input=prompt,\n", + " generation_config=interactions.GenerationConfig(\n", + " video_config=interactions.VideoConfig(\n", + " task=\"text_to_video\"\n", + " )\n", + " ),\n", + " response_format=interactions.VideoResponseFormat(\n", + " aspect_ratio=\"16:9\",\n", + " duration=\"9s\",\n", + " # delivery=\"uri\",\n", + " # gcs_uri=\"gs://\"\n", + " )\n", + ")\n", + "\n", + "contents = []\n", + "for step in interaction.steps:\n", + " if step.type == \"model_output\":\n", + " contents.extend(step.content)\n", + "\n", + "show_video(contents[0].data)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "-aV_ahDKWjPU" + }, + "source": [ + "### Video generation from a starting image" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "v3xtidCsGiCH" + }, + "source": [ + "#### Download the starting image\n", + "\n", + "You can also generate a video by starting with an input image. In this example, you'll locally download an image that's stored in Google Cloud Storage. If you'd like, you can provide the URL of an image to display it below. If you have a local image you'd like to use, you can specify that in the following steps." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "p6TPPMW66XCl" + }, + "outputs": [], + "source": [ + "!wget -q https://storage.googleapis.com/cloud-samples-data/generative-ai/image/suitcase.png" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "oi7eAyOhGmoU" + }, + "source": [ + "If you'd like to use a different local image, modify the file name in `start`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "qsneJ_lP6cKk" + }, + "outputs": [], + "source": [ + "start = \"suitcase.png\"\n", + "\n", + "fig, ax1 = plt.subplots(1, 1, figsize=(12, 6))\n", + "ax1.imshow(img.imread(start))\n", + "ax1.axis(\"off\")\n", + "plt.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "2_g6-aP661EV" + }, + "outputs": [], + "source": [ + "prompt = \"A single continuous shot of a hard-shell suitcase rolling. It stops, stands vertically, unzips alongside and then opens in half. The wheels and shell stay solid. White bubbly retro text reading 'Omnicase' pops out of the center. Colorful travel stickers pop up around the text: a plane, palm tree, boat, city skyline, and a train.\"\n", + "\n", + "with open(start, \"rb\") as f:\n", + " img_b64 = base64.b64encode(f.read()).decode(\"utf-8\")\n", + "\n", + "interaction = client.interactions.create(\n", + " model=omni_model,\n", + " input=[\n", + " {\"type\": \"text\", \"text\": prompt},\n", + " {\"type\": \"image\", \"mime_type\": \"image/png\", \"data\": img_b64},\n", + " ],\n", + " generation_config=interactions.GenerationConfig(\n", + " video_config=interactions.VideoConfig(\n", + " task=\"image_to_video\"\n", + " )\n", + " ),\n", + ")\n", + "\n", + "contents = []\n", + "for step in interaction.steps:\n", + " if step.type == \"model_output\":\n", + " contents.extend(step.content)\n", + "\n", + "show_video(contents[0].data)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "YdBtdsaLWo0F" + }, + "source": [ + "### Reference inputs in video generation\n", + "\n", + "With Gemini Omni Flash, you can use existing media to generate and edit new videos. The model processes these inputs in one of two ways:\n", + "\n", + " - **Source Media:** This content acts as the direct foundation for the final output.\n", + " - *Examples:* In image-to-video, the provided image becomes the literal first frame. In video editing, the original video is modified via natural language prompts while keeping its core structure intact.\n", + " - **Reference Media:** This content acts as a guide or inspiration rather than the literal base. You are asking the model to extract a specific style, subject, or mood to creatively reimagine it.\n", + " - *Examples:* Storyboard frames, product images, style guides, audio jingles, or reference scenery. You can also use reference media for video editing—such as providing an image of a new character or art style to apply to a base video.\n", + "\n", + "\n", + "**NOTE:** Video and audio reference inputs are not currently supported." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "jVjssXdqMrMo" + }, + "source": [ + "#### Download and display reference images\n", + "\n", + "In this example, you'll locally download two images that are stored in Google Cloud Storage." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "5FvshnXiyNKb" + }, + "outputs": [], + "source": [ + "!wget -q https://storage.googleapis.com/cloud-samples-data/generative-ai/image/woman.jpeg\n", + "\n", + "!wget -q https://storage.googleapis.com/cloud-samples-data/generative-ai/image/arcade-game.png" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "j1jN_J7onPyY" + }, + "outputs": [], + "source": [ + "character = \"woman.jpeg\"\n", + "product = \"arcade-game.png\"\n", + "\n", + "# Display the images\n", + "fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 6))\n", + "ax1.imshow(img.imread(character))\n", + "ax2.imshow(img.imread(product))\n", + "ax1.axis(\"off\")\n", + "ax2.axis(\"off\")\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "1ZLbIRXDJa7T" + }, + "source": [ + "**Tip:** You can supply storyboard reference images to better direct the video without supplying all details in a text prompt." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "LKofc-c5mH_H" + }, + "outputs": [], + "source": [ + "prompt = \"A woman walks up to the Omni-Sphere game on the side wall of a bowling alley. Bass music begins to play as she starts playing the game by pressing a button. 9:16 aspect ratio. 7 second video.\"\n", + "\n", + "image_files = [character, product]\n", + "\n", + "images_input = []\n", + "for img_path in image_files:\n", + " with open(img_path, \"rb\") as f:\n", + " img_b64 = base64.b64encode(f.read()).decode(\"utf-8\")\n", + " images_input.append({\"type\": \"image\", \"mime_type\": \"image/jpeg\",\n", + " \"data\": img_b64})\n", + "\n", + "interaction = client.interactions.create(\n", + " model=omni_model,\n", + " input=[\n", + " {\"type\": \"text\", \"text\": prompt},\n", + " *images_input,\n", + " ],\n", + " generation_config=interactions.GenerationConfig(\n", + " video_config=interactions.VideoConfig(\n", + " task=\"reference_to_video\"\n", + " )\n", + " ),\n", + ")\n", + "\n", + "contents = []\n", + "for step in interaction.steps:\n", + " if step.type == \"model_output\":\n", + " contents.extend(step.content)\n", + "\n", + "show_video(contents[0].data)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "qWVoa9xpi6l-" + }, + "source": [ + "### Async video generation\n", + "\n", + "To generate a video that can be checked on later, set the `background` parameter to True. You can then use `interactions.get` to check the status of the initial interaction ID." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "nVOn-pwrgcil" + }, + "outputs": [], + "source": [ + "import time\n", + "\n", + "prompt = \"Raw cookie ingredients beautifully laid out on a kitchen counter. The view glides smoothly from one ingredient to the next. As each individual ingredient fills the view, a sleek 3D text graphic pops up to label it. The sequence finishes by resting intimately on the rich textures and details of the final ingredient after the label disappears.\"\n", + "\n", + "initial_interaction = client.interactions.create(\n", + " model=omni_model,\n", + " input=prompt,\n", + " background=True,\n", + ")\n", + "interaction = initial_interaction\n", + "\n", + "while interaction.status not in [\"completed\", \"failed\"]:\n", + " print(f\"Current Status: {interaction.status}\")\n", + " time.sleep(10)\n", + " interaction = client.interactions.get(id=initial_interaction.id)\n", + "\n", + "if interaction.status == \"completed\":\n", + " contents = []\n", + " steps = getattr(interaction, \"steps\", []) or []\n", + " for step in steps:\n", + " if step.type == \"model_output\":\n", + " contents.extend(step.content)\n", + " show_video(contents[0].data)\n", + "else:\n", + " print(f\"Status: {interaction.status}\")\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "VlXnMxwyWsfM" + }, + "source": [ + "## Video editing" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "SJs81D2SNf4e" + }, + "source": [ + "#### Download and display the video\n", + "\n", + "In this example, you'll locally download a video and an image that are stored in Google Cloud Storage.\n", + "\n", + "**Note:** You can supply reference images when editing source videos. The input source video must be under 10 seconds." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "DOJhxrMKGfiK" + }, + "outputs": [], + "source": [ + "from PIL import Image as PIL_IMAGE\n", + "\n", + "video_uri = \"gs://cloud-samples-data/generative-ai/video/dog_day1.mp4\"\n", + "image_uri = \"gs://cloud-samples-data/generative-ai/image/chair-cat.png\"\n", + "\n", + "!gcloud storage cp {video_uri} {\"dog_day1.mp4\"}\n", + "!gcloud storage cp {image_uri} {\"chair-cat.png\"}\n", + "\n", + "display(Video(\"dog_day1.mp4\", embed=True, width=600))\n", + "\n", + "fig, ax1 = plt.subplots(1, 1, figsize=(6, 6))\n", + "ax1.imshow(PIL_IMAGE.open(\"chair-cat.png\"))\n", + "ax1.axis(\"off\")\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "5lHP-aNbJwLS" + }, + "source": [ + "With video editing, you can add, remove, or alter existing objects in the source video. You can also change the content in the video to a different style or reference provided images." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "lWvtlz6AIRwe" + }, + "outputs": [], + "source": [ + "prompt = \"Change the dog to the cat. Remove the backpack and add a propeller hat. Change the tennis balls to balls of yarn.\"\n", + "interaction = client.interactions.create(\n", + " model=omni_model,\n", + " input=[\n", + " {\"type\": \"text\", \"text\": prompt},\n", + " {\"type\": \"image\", \"mime_type\": \"image/png\", \"uri\": image_uri},\n", + " {\"type\": \"video\", \"mime_type\": \"video/mp4\", \"uri\": video_uri},\n", + " ],\n", + " generation_config=interactions.GenerationConfig(\n", + " video_config=interactions.VideoConfig(\n", + " task=\"edit\"\n", + " )\n", + " ),\n", + ")\n", + "\n", + "contents = []\n", + "for step in interaction.steps:\n", + " if step.type == \"model_output\":\n", + " contents.extend(step.content)\n", + "\n", + "show_video(contents[0].data)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "k1yLTAvyWx0x" + }, + "source": [ + "### Multi-turn editing (chat)\n", + "\n", + "You can iteratively chat with the model to continuously change aspects of the video using the example below.\n", + "\n", + "To successfully accomplish this using the Interactions API, make sure to provide the previous chat history in the `input` parameter." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "Z9qWfdgEKg_U" + }, + "outputs": [], + "source": [ + "prompt1 = \"A claymation explainer of Newton's First Law of Motion, everything is made out of clay, no hands, stop motion, sync character's words to mouth movements, show a cute ball character moving and then being stopped by a wall\"\n", + "\n", + "interaction1 = client.interactions.create(\n", + " model=omni_model,\n", + " input=prompt1,\n", + ")\n", + "\n", + "contents = []\n", + "for step in interaction1.steps:\n", + " if step.type == \"model_output\":\n", + " contents.extend(step.content)\n", + "\n", + "show_video(contents[0].data)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "XcEvGMlQKqfg" + }, + "outputs": [], + "source": [ + "interaction1.steps" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "4OudxC1_KtoK" + }, + "outputs": [], + "source": [ + "prompt2 = \"Now make the same video in a doodle style.\"\n", + "\n", + "turn2_input = interaction1.steps + [\n", + " {\n", + " \"type\": \"user_input\",\n", + " \"content\": [\n", + " {\n", + " \"type\": \"text\",\n", + " \"text\": prompt2\n", + " }\n", + " ]\n", + " }\n", + "]\n", + "\n", + "interaction2 = client.interactions.create(\n", + " model=omni_model,\n", + " input=turn2_input,\n", + ")\n", + "\n", + "contents = []\n", + "for step in interaction2.steps:\n", + " if step.type == \"model_output\":\n", + " contents.extend(step.content)\n", + "\n", + "show_video(contents[0].data)\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "2ycGCBPq8pRI" + }, + "source": [ + "\n", + "**Note:** Support for audio references, video references, last frame, scene extension, and higher resolutions for the Gemini Omni Flash via Gemini Enterprise Agent Platform API will be available soon." + ] + } + ], + "metadata": { + "colab": { + "provenance": [] + }, + "kernelspec": { + "display_name": "Python 3", + "name": "python3" + }, + "language_info": { + "name": "python" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} diff --git a/vision/getting-started/imagen3_editing.ipynb b/vision/getting-started/imagen3_editing.ipynb new file mode 100644 index 0000000..945d2a0 --- /dev/null +++ b/vision/getting-started/imagen3_editing.ipynb @@ -0,0 +1,838 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "ijGzTHJJUCPY" + }, + "outputs": [], + "source": [ + "# Copyright 2024 Google LLC\n", + "#\n", + "# Licensed under the Apache License, Version 2.0 (the \"License\");\n", + "# you may not use this file except in compliance with the License.\n", + "# You may obtain a copy of the License at\n", + "#\n", + "# https://www.apache.org/licenses/LICENSE-2.0\n", + "#\n", + "# Unless required by applicable law or agreed to in writing, software\n", + "# distributed under the License is distributed on an \"AS IS\" BASIS,\n", + "# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n", + "# See the License for the specific language governing permissions and\n", + "# limitations under the License." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "9003470a8d3b" + }, + "source": [ + "# Imagen 3 Image Editing\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + "
    \n", + " \n", + " \"Google
    Open in Colab\n", + "
    \n", + "
    \n", + " \n", + " \"Google
    Open in Colab Enterprise\n", + "
    \n", + "
    \n", + " \n", + " \"Workbench
    Open in Workbench\n", + "
    \n", + "
    \n", + " \n", + " \"GitHub
    View on GitHub\n", + "
    \n", + "
    \n", + "\n", + "
    \n", + "\n", + "

    \n", + "Share to:\n", + "\n", + "\n", + " \"LinkedIn\n", + "\n", + "\n", + "\n", + " \"Bluesky\n", + "\n", + "\n", + "\n", + " \"X\n", + "\n", + "\n", + "\n", + " \"Reddit\n", + "\n", + "\n", + "\n", + " \"Facebook\n", + "\n", + "

    " + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "G1KDmM_PBAXz" + }, + "source": [ + "| Author |\n", + "| --- |\n", + "| [Katie Nguyen](https://github.com/katiemn) |" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "CkHPv2myT2cx" + }, + "source": [ + "## Overview\n", + "\n", + "### Imagen 3\n", + "\n", + "Imagen 3 on Agent Platform brings Google's state of the art generative AI capabilities to application developers. Imagen 3 is Google's highest quality text-to-image model to date. It's capable of creating images with astonishing detail. Thus, developers have more control when building next-generation AI products that transform their imagination into high quality visual assets.\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "DrkcqHrrwMAo" + }, + "source": [ + "In this tutorial, you will learn how to use the Google Gen AI SDK for Python to interact with Imagen 3 and modify existing images with mask-based editing and mask-free editing in the following modes:\n", + "\n", + "- Inpainting\n", + "- Product background editing\n", + "- Outpainting\n", + "- Mask-free" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "r11Gu7qNgx1p" + }, + "source": [ + "## Get started\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "No17Cw5hgx12" + }, + "source": [ + "### Install Google Gen AI SDK for Python\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "tFy3H3aPgx12" + }, + "outputs": [], + "source": [ + "%pip install --upgrade --quiet google-genai" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "dmWOrTJ3gx13" + }, + "source": [ + "### Authenticate your notebook environment (Colab only)\n", + "\n", + "If you are running this notebook on Google Colab, run the following cell to authenticate your environment.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "NyKGtVQjgx13" + }, + "outputs": [], + "source": [ + "import sys\n", + "\n", + "if \"google.colab\" in sys.modules:\n", + " from google.colab import auth\n", + "\n", + " auth.authenticate_user()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "Ua6PDqB1iBSb" + }, + "source": [ + "### Import libraries" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "DF4l8DTdWgPY" + }, + "source": [ + "### Set Google Cloud project information\n", + "\n", + "To get started using Agent Platform, you must have an existing Google Cloud project and [enable the Agent Platform API](https://console.cloud.google.com/flows/enableapi?apiid=aiplatform.googleapis.com).\n", + "\n", + "Learn more about [setting up a project](https://docs.cloud.google.com/resource-manager/docs/creating-managing-projects) and a [development environment](https://cloud.google.com/docs/authentication/set-up-adc-local-dev-environment)." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": { + "id": "Nqwi-5ufWp_B" + }, + "outputs": [], + "source": [ + "import os\n", + "\n", + "from google import genai\n", + "from google.genai.types import (\n", + " EditImageConfig,\n", + " GenerateImagesConfig,\n", + " Image,\n", + " MaskReferenceConfig,\n", + " MaskReferenceImage,\n", + " RawReferenceImage,\n", + ")\n", + "\n", + "# fmt: off\n", + "PROJECT_ID = \"[your-project-id]\" # @param {type: \"string\", placeholder: \"[your-project-id]\", isTemplate: true}\n", + "# fmt: on\n", + "if not PROJECT_ID or PROJECT_ID == \"[your-project-id]\":\n", + " PROJECT_ID = str(os.environ.get(\"GOOGLE_CLOUD_PROJECT\"))\n", + "\n", + "LOCATION = os.environ.get(\"GOOGLE_CLOUD_REGION\", \"us-central1\")\n", + "\n", + "client = genai.Client(enterprise=True, project=PROJECT_ID, location=LOCATION)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "Sr2Y3lFwKW1M" + }, + "source": [ + "### Define helper functions" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "r_38e5rRKB6s" + }, + "outputs": [], + "source": [ + "import io\n", + "import urllib\n", + "\n", + "import matplotlib.pyplot as plt\n", + "from PIL import Image as PIL_Image\n", + "\n", + "# Gets the image bytes from a PIL Image object.\n", + "\n", + "\n", + "def get_bytes_from_pil(image: PIL_Image) -> bytes:\n", + " byte_io_png = io.BytesIO()\n", + " image.save(byte_io_png, \"PNG\")\n", + " return byte_io_png.getvalue()\n", + "\n", + "\n", + "# Pads an image for outpainting.\n", + "def pad_to_target_size(\n", + " source_image,\n", + " target_size=(1536, 1536),\n", + " mode=\"RGB\",\n", + " vertical_offset_ratio=0,\n", + " horizontal_offset_ratio=0,\n", + " fill_val=255,\n", + "):\n", + " orig_image_size_w, orig_image_size_h = source_image.size\n", + " target_size_w, target_size_h = target_size\n", + "\n", + " insert_pt_x = (target_size_w - orig_image_size_w) // 2 + int(\n", + " horizontal_offset_ratio * target_size_w\n", + " )\n", + " insert_pt_y = (target_size_h - orig_image_size_h) // 2 + int(\n", + " vertical_offset_ratio * target_size_h\n", + " )\n", + " insert_pt_x = min(insert_pt_x, target_size_w - orig_image_size_w)\n", + " insert_pt_y = min(insert_pt_y, target_size_h - orig_image_size_h)\n", + "\n", + " if mode == \"RGB\":\n", + " source_image_padded = PIL_Image.new(\n", + " mode, target_size, color=(fill_val, fill_val, fill_val)\n", + " )\n", + " elif mode == \"L\":\n", + " source_image_padded = PIL_Image.new(mode, target_size, color=(fill_val))\n", + " else:\n", + " raise ValueError(\"source image mode must be RGB or L.\")\n", + "\n", + " source_image_padded.paste(source_image, (insert_pt_x, insert_pt_y))\n", + " return source_image_padded\n", + "\n", + "\n", + "# Pads and resizes image and mask to the same target size.\n", + "def pad_image_and_mask(\n", + " image: PIL_Image,\n", + " mask: PIL_Image,\n", + " target_size,\n", + " vertical_offset_ratio,\n", + " horizontal_offset_ratio,\n", + "):\n", + " image.thumbnail(target_size)\n", + " mask.thumbnail(target_size)\n", + "\n", + " image = pad_to_target_size(\n", + " image,\n", + " target_size=target_size,\n", + " mode=\"RGB\",\n", + " vertical_offset_ratio=vertical_offset_ratio,\n", + " horizontal_offset_ratio=horizontal_offset_ratio,\n", + " fill_val=0,\n", + " )\n", + " mask = pad_to_target_size(\n", + " mask,\n", + " target_size=target_size,\n", + " mode=\"L\",\n", + " vertical_offset_ratio=vertical_offset_ratio,\n", + " horizontal_offset_ratio=horizontal_offset_ratio,\n", + " fill_val=255,\n", + " )\n", + " return image, mask\n", + "\n", + "\n", + "def display_images(original_image, modified_image) -> None:\n", + " fig, axis = plt.subplots(1, 2, figsize=(12, 6))\n", + " axis[0].imshow(original_image)\n", + " axis[0].set_title(\"Original Image\")\n", + " axis[1].imshow(modified_image)\n", + " axis[1].set_title(\"Edited Image\")\n", + " for ax in axis:\n", + " ax.axis(\"off\")\n", + " plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "VLmwIj2RD0Fx" + }, + "source": [ + "### Load the image models\n", + "\n", + "Imagen 3 Generation: `imagen-3.0-generate-002`\n", + "\n", + "Imagen 3 Editing: `imagen-3.0-capability-001`" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "F-gd2ypQhh7K" + }, + "outputs": [], + "source": [ + "generation_model = \"imagen-3.0-generate-002\"\n", + "\n", + "edit_model = \"imagen-3.0-capability-001\"" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "f64d92aef6cb" + }, + "source": [ + "### Inpainting insert\n", + "\n", + "In these examples you will specify a targeted area to apply edits to. In the case of inpainting insert, you'll use a mask area to add image content to an existing image. Start by generating an image using Imagen 3. Then create two ```ReferenceImage``` objects, one for your reference image and one for your mask. For the ```MaskReferenceImage``` set ```reference_image=None```, this will allow for automatic mask detection based on the specified ```mask_mode```.\n", + "\n", + "When generating images you can also set the `safety_filter_level` and `person_generation` parameters accordingly:\n", + "* `person_generation`: DONT_ALLOW, ALLOW_ADULT, ALLOW_ALL\n", + "* `safety_filter_level`: BLOCK_LOW_AND_ABOVE, BLOCK_MEDIUM_AND_ABOVE, BLOCK_ONLY_HIGH, BLOCK_NONE" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "wwZBW0UW-PiW" + }, + "outputs": [], + "source": [ + "image_prompt = \"\"\"\n", + "a small wooden bowl with grapes and apples on a marble kitchen counter, light brown cabinets blurred in the background\n", + "\"\"\"\n", + "generated_image = client.models.generate_images(\n", + " model=generation_model,\n", + " prompt=image_prompt,\n", + " config=GenerateImagesConfig(\n", + " number_of_images=1,\n", + " aspect_ratio=\"1:1\",\n", + " safety_filter_level=\"BLOCK_MEDIUM_AND_ABOVE\",\n", + " person_generation=\"DONT_ALLOW\",\n", + " ),\n", + ")\n", + "\n", + "edit_prompt = \"a small white ceramic bowl with lemons and limes\"\n", + "raw_ref_image = RawReferenceImage(\n", + " reference_image=generated_image.generated_images[0].image, reference_id=0\n", + ")\n", + "mask_ref_image = MaskReferenceImage(\n", + " reference_id=1,\n", + " reference_image=None,\n", + " config=MaskReferenceConfig(\n", + " mask_mode=\"MASK_MODE_FOREGROUND\",\n", + " mask_dilation=0.1,\n", + " ),\n", + ")\n", + "edited_image = client.models.edit_image(\n", + " model=edit_model,\n", + " prompt=edit_prompt,\n", + " reference_images=[raw_ref_image, mask_ref_image],\n", + " config=EditImageConfig(\n", + " edit_mode=\"EDIT_MODE_INPAINT_INSERTION\",\n", + " number_of_images=1,\n", + " safety_filter_level=\"BLOCK_MEDIUM_AND_ABOVE\",\n", + " person_generation=\"ALLOW_ADULT\",\n", + " ),\n", + ")\n", + "\n", + "display_images(\n", + " generated_image.generated_images[0].image._pil_image,\n", + " edited_image.generated_images[0].image._pil_image,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "ec7135f4de3d" + }, + "source": [ + "This next example demonstrates another instance of inpainting insert. However, you'll use the semantic mask mode. When using this mask mode, you'll need to specify the class ID of the object in the image that you wish to mask and replace. A list of possible instance types is shown at the end of this notebook. Once you've found the correct segmentation class ID, list it in ```segmentation_classes```.\n", + "\n", + "Within the ```MaskReferenceImage``` object you can also configure the dilation value. This float between 0 and 1 represents the percentage of the provided mask." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "8pyAJlvQsocc" + }, + "outputs": [], + "source": [ + "image_prompt = \"\"\"\n", + "a french bulldog sitting in a living room on a couch with green throw pillows and a throw blanket,\n", + "a circular mirror is on the wall above the couch\n", + "\"\"\"\n", + "generated_image = client.models.generate_images(\n", + " model=generation_model,\n", + " prompt=image_prompt,\n", + " config=GenerateImagesConfig(\n", + " number_of_images=1,\n", + " aspect_ratio=\"1:1\",\n", + " safety_filter_level=\"BLOCK_MEDIUM_AND_ABOVE\",\n", + " person_generation=\"DONT_ALLOW\",\n", + " ),\n", + ")\n", + "\n", + "edit_prompt = \"a corgi sitting on a couch\"\n", + "raw_ref_image = RawReferenceImage(\n", + " reference_image=generated_image.generated_images[0].image, reference_id=0\n", + ")\n", + "mask_ref_image = MaskReferenceImage(\n", + " reference_id=1,\n", + " reference_image=None,\n", + " config=MaskReferenceConfig(\n", + " mask_mode=\"MASK_MODE_SEMANTIC\",\n", + " segmentation_classes=[8],\n", + " mask_dilation=0.1,\n", + " ),\n", + ")\n", + "edited_image = client.models.edit_image(\n", + " model=edit_model,\n", + " prompt=edit_prompt,\n", + " reference_images=[raw_ref_image, mask_ref_image],\n", + " config=EditImageConfig(\n", + " edit_mode=\"EDIT_MODE_INPAINT_INSERTION\",\n", + " number_of_images=1,\n", + " safety_filter_level=\"BLOCK_MEDIUM_AND_ABOVE\",\n", + " person_generation=\"ALLOW_ADULT\",\n", + " ),\n", + ")\n", + "\n", + "display_images(\n", + " generated_image.generated_images[0].image._pil_image,\n", + " edited_image.generated_images[0].image._pil_image,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "AnneP_O4vdL1" + }, + "source": [ + "Below you'll see another instance of inpainting insert. This time you'll use a local image and mask that have been downloaded from Google Cloud Storage. When using your own mask, you'll specify \"MASK_MODE_USER_PROVIDED\" as the ```mask_mode```." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "87yClMcjsM_O" + }, + "outputs": [], + "source": [ + "! gcloud storage cp \"gs://cloud-samples-data/generative-ai/image/image-dog.png\" .\n", + "! gcloud storage cp \"gs://cloud-samples-data/generative-ai/image/image-dog-mask.png\" .\n", + "initial_image = Image.from_file(location=\"image-dog.png\")\n", + "initial_image_mask = Image.from_file(location=\"image-dog-mask.png\")\n", + "\n", + "edit_prompt = \"a Persian cat sitting in a white cat bed\"\n", + "raw_ref_image = RawReferenceImage(reference_image=initial_image, reference_id=0)\n", + "mask_ref_image = MaskReferenceImage(\n", + " reference_id=1,\n", + " reference_image=initial_image_mask,\n", + " config=MaskReferenceConfig(\n", + " mask_mode=\"MASK_MODE_USER_PROVIDED\",\n", + " mask_dilation=0.1,\n", + " ),\n", + ")\n", + "\n", + "edited_image = client.models.edit_image(\n", + " model=edit_model,\n", + " prompt=edit_prompt,\n", + " reference_images=[raw_ref_image, mask_ref_image],\n", + " config=EditImageConfig(\n", + " edit_mode=\"EDIT_MODE_INPAINT_INSERTION\",\n", + " number_of_images=1,\n", + " safety_filter_level=\"BLOCK_MEDIUM_AND_ABOVE\",\n", + " person_generation=\"ALLOW_ADULT\",\n", + " ),\n", + ")\n", + "\n", + "display_images(\n", + " initial_image._pil_image, edited_image.generated_images[0].image._pil_image\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "6ad62258e803" + }, + "source": [ + "### Inpainting remove\n", + "\n", + "Inpainting remove allows you to use a mask area to remove image content.\n", + "\n", + "In this next example, you'll take an image in Google Cloud Storage of a wall with a mirror and some photos and create a mask over detected mirror instances. You'll then remove this object by setting the edit mode to \"EDIT_MODE_INPAINT_REMOVAL.\" For these types of requests the prompt can be an empty string." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "mOdKM3ZCB4g6" + }, + "outputs": [], + "source": [ + "starting_image = Image(gcs_uri=\"gs://cloud-samples-data/generative-ai/image/mirror.png\")\n", + "raw_ref_image = RawReferenceImage(reference_image=starting_image, reference_id=0)\n", + "mask_ref_image = MaskReferenceImage(\n", + " reference_id=1,\n", + " reference_image=None,\n", + " config=MaskReferenceConfig(\n", + " mask_mode=\"MASK_MODE_SEMANTIC\", segmentation_classes=[85]\n", + " ),\n", + ")\n", + "\n", + "remove_image = client.models.edit_image(\n", + " model=edit_model,\n", + " prompt=\"\",\n", + " reference_images=[raw_ref_image, mask_ref_image],\n", + " config=EditImageConfig(\n", + " edit_mode=\"EDIT_MODE_INPAINT_REMOVAL\",\n", + " number_of_images=1,\n", + " safety_filter_level=\"BLOCK_MEDIUM_AND_ABOVE\",\n", + " person_generation=\"ALLOW_ADULT\",\n", + " ),\n", + ")\n", + "\n", + "starting_image_show = PIL_Image.open(\n", + " urllib.request.urlopen(\n", + " \"https://storage.googleapis.com/cloud-samples-data/generative-ai/image/mirror.png\"\n", + " )\n", + ")\n", + "\n", + "display_images(\n", + " starting_image_show,\n", + " remove_image.generated_images[0].image._pil_image,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "68909c926952" + }, + "source": [ + "### Product background editing via background swap mode\n", + "\n", + "\n", + "You can also use Imagen 3 for product image editing. By setting `edit_mode` to \"EDIT_MODE_BGSWAP\", you can maintain the product content while modifying the image background.\n", + "\n", + "For this example, start with an image stored in a Google Cloud Storage bucket, and provide a prompt describing the new background scene. " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "S5zv8PqYweHW" + }, + "outputs": [], + "source": [ + "product_image = Image(\n", + " gcs_uri=\"gs://cloud-samples-data/generative-ai/image/suitcase.png\"\n", + ")\n", + "raw_ref_image = RawReferenceImage(reference_image=product_image, reference_id=0)\n", + "mask_ref_image = MaskReferenceImage(\n", + " reference_id=1,\n", + " reference_image=None,\n", + " config=MaskReferenceConfig(mask_mode=\"MASK_MODE_BACKGROUND\"),\n", + ")\n", + "\n", + "prompt = \"a light blue suitcase in front of a window in an airport, lots of bright, natural lighting coming in from the windows, planes taking off in the distance\"\n", + "edited_image = client.models.edit_image(\n", + " model=edit_model,\n", + " prompt=prompt,\n", + " reference_images=[raw_ref_image, mask_ref_image],\n", + " config=EditImageConfig(\n", + " edit_mode=\"EDIT_MODE_BGSWAP\",\n", + " number_of_images=1,\n", + " safety_filter_level=\"BLOCK_MEDIUM_AND_ABOVE\",\n", + " person_generation=\"ALLOW_ADULT\",\n", + " ),\n", + ")\n", + "\n", + "product_image_show = PIL_Image.open(\n", + " urllib.request.urlopen(\n", + " \"https://storage.googleapis.com/cloud-samples-data/generative-ai/image/suitcase.png\"\n", + " )\n", + ")\n", + "display_images(product_image_show, edited_image.generated_images[0].image._pil_image)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "76df73e7bbd2" + }, + "source": [ + "### Outpainting\n", + "\n", + "Imagen 3 editing can be used for image outpainting. Outpainting is used to expand the content of an image to a larger area or area with different dimensions. To use the outpainting feature, you must create an image mask and prepare the original image by padding some empty space around it. Once you've padded the image, you can use the ```outpainting``` editing mode to fill in the empty space." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "yUevoHxxIsIN" + }, + "outputs": [], + "source": [ + "! gcloud storage cp \"gs://cloud-samples-data/generative-ai/image/living-room.png\" .\n", + "initial_image = Image.from_file(location=\"living-room.png\")\n", + "mask = PIL_Image.new(\"L\", initial_image._pil_image.size, 0)\n", + "\n", + "target_size_w = int(2500 * eval(\"3/4\"))\n", + "target_size = (target_size_w, 2500)\n", + "image_pil_outpaint, mask_pil_outpaint = pad_image_and_mask(\n", + " initial_image._pil_image,\n", + " mask,\n", + " target_size,\n", + " 0,\n", + " 0,\n", + ")\n", + "image_pil_outpaint_image = Image(image_bytes=get_bytes_from_pil(image_pil_outpaint))\n", + "mask_pil_outpaint_image = Image(image_bytes=get_bytes_from_pil(mask_pil_outpaint))\n", + "\n", + "raw_ref_image = RawReferenceImage(\n", + " reference_image=image_pil_outpaint_image, reference_id=0\n", + ")\n", + "mask_ref_image = MaskReferenceImage(\n", + " reference_id=1,\n", + " reference_image=mask_pil_outpaint_image,\n", + " config=MaskReferenceConfig(\n", + " mask_mode=\"MASK_MODE_USER_PROVIDED\",\n", + " mask_dilation=0.03,\n", + " ),\n", + ")\n", + "\n", + "prompt = \"a chandelier hanging from the ceiling\"\n", + "edited_image = client.models.edit_image(\n", + " model=edit_model,\n", + " prompt=prompt,\n", + " reference_images=[raw_ref_image, mask_ref_image],\n", + " config=EditImageConfig(\n", + " edit_mode=\"EDIT_MODE_OUTPAINT\",\n", + " number_of_images=1,\n", + " safety_filter_level=\"BLOCK_MEDIUM_AND_ABOVE\",\n", + " person_generation=\"ALLOW_ADULT\",\n", + " ),\n", + ")\n", + "\n", + "display_images(\n", + " initial_image._pil_image, edited_image.generated_images[0].image._pil_image\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "D3A7GIQBjSQX" + }, + "source": [ + "### Mask-free editing\n", + "\n", + "Imagen 3 editing also lets you edit images without a mask. Simply write the changes you wish to make to the image in the prompt and provide the original image as the sole reference image." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "E8df7clFJNRa" + }, + "outputs": [], + "source": [ + "original_image = Image(gcs_uri=\"gs://cloud-samples-data/generative-ai/image/latte.jpg\")\n", + "raw_ref_image = RawReferenceImage(reference_image=original_image, reference_id=0)\n", + "\n", + "\n", + "prompt = \"swan latte art in the coffee cup and an assortment of red velvet cupcakes in gold wrappers on the white plate\"\n", + "edited_image = client.models.edit_image(\n", + " model=edit_model,\n", + " prompt=prompt,\n", + " reference_images=[raw_ref_image],\n", + " config=EditImageConfig(\n", + " edit_mode=\"EDIT_MODE_DEFAULT\",\n", + " number_of_images=1,\n", + " safety_filter_level=\"BLOCK_MEDIUM_AND_ABOVE\",\n", + " person_generation=\"ALLOW_ADULT\",\n", + " ),\n", + ")\n", + "\n", + "original_image_show = PIL_Image.open(\n", + " urllib.request.urlopen(\n", + " \"https://storage.googleapis.com/cloud-samples-data/generative-ai/image/latte.jpg\"\n", + " )\n", + ")\n", + "display_images(original_image_show, edited_image.generated_images[0].image._pil_image)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "5c37c0c51d8f" + }, + "source": [ + "### Semantic segmentation classes\n", + "\n", + "| Class ID | Instance Type | Class ID | Instance Type | Class ID | Instance Type | Class ID | Instance Type |\n", + "| --- | --- | --- | --- | --- | --- | --- | --- |\n", + "| 0 | backpack | 50 | carrot | 100 | sidewalk_pavement | 150 | skis |\n", + "| 1 | umbrella | 51 | hot_dog | 101 | runway | 151 | snowboard |\n", + "| 2 | bag | 52 | pizza | 102 | terrain | 152 | sports_ball |\n", + "| 3 | tie | 53 | donut | 103 | book | 153 | kite |\n", + "| 4 | suitcase | 54 | cake | 104 | box | 154 | baseball_bat |\n", + "| 5 | case | 55 | fruit_other | 105 | clock | 155 | baseball_glove |\n", + "| 6 | bird | 56 | food_other | 106 | vase | 156 | skateboard |\n", + "| 7 | cat | 57 | chair_other | 107 | scissors | 157 | surfboard |\n", + "| 8 | dog | 58 | armchair | 108 | plaything_other | 158 | tennis_racket |\n", + "| 9 | horse | 59 | swivel_chair | 109 | teddy_bear | 159 | net |\n", + "| 10 | sheep | 60 | stool | 110 | hair_dryer | 160 | base |\n", + "| 11 | cow | 61 | seat | 111 | toothbrush | 161 | sculpture |\n", + "| 12 | elephant | 62 | couch | 112 | painting | 162 | column |\n", + "| 13 | bear | 63 | trash_can | 113 | poster | 163 | fountain |\n", + "| 14 | zebra | 64 | potted_plant | 114 | bulletin_board | 164 | awning |\n", + "| 15 | giraffe | 65 | nightstand | 115 | bottle | 165 | apparel |\n", + "| 16 | animal_other | 66 | bed | 116 | cup | 166 | banner |\n", + "| 17 | microwave | 67 | table | 117 | wine_glass | 167 | flag |\n", + "| 18 | radiator | 68 | pool_table | 118 | knife | 168 | blanket |\n", + "| 19 | oven | 69 | barrel | 119 | fork | 169 | curtain_other |\n", + "| 20 | toaster | 70 | desk | 120 | spoon | 170 | shower_curtain |\n", + "| 21 | storage_tank | 71 | ottoman | 121 | bowl | 171 | pillow |\n", + "| 22 | conveyor_belt | 72 | wardrobe | 122 | tray | 172 | towel |\n", + "| 23 | sink | 73 | crib | 123 | range_hood | 173 | rug_floormat |\n", + "| 24 | refrigerator | 74 | basket | 124 | plate | 174 | vegetation |\n", + "| 25 | washer_dryer | 75 | chest_of_drawers | 125 | person | 175 | bicycle |\n", + "| 26 | fan | 76 | bookshelf | 126 | rider_other | 176 | car |\n", + "| 27 | dishwasher | 77 | counter_other | 127 | bicyclist | 177 | autorickshaw |\n", + "| 28 | toilet | 78 | bathroom_counter | 128 | motorcyclist | 178 | motorcycle |\n", + "| 29 | bathtub | 79 | kitchen_island | 129 | paper | 179 | airplane |\n", + "| 30 | shower | 80 | door | 130 | streetlight | 180 | bus |\n", + "| 31 | tunnel | 81 | light_other | 131 | road_barrier | 181 | train |\n", + "| 32 | bridge | 82 | lamp | 132 | mailbox | 182 | truck |\n", + "| 33 | pier_wharf | 83 | sconce | 133 | cctv_camera | 183 | trailer |\n", + "| 34 | tent | 84 | chandelier | 134 | junction_box | 184 | boat_ship |\n", + "| 35 | building | 85 | mirror | 135 | traffic_sign | 185 | slow_wheeled_object |\n", + "| 36 | ceiling | 86 | whiteboard | 136 | traffic_light | 186 | river_lake |\n", + "| 37 | laptop | 87 | shelf | 137 | fire_hydrant | 187 | sea |\n", + "| 38 | keyboard | 88 | stairs | 138 | parking_meter | 188 | water_other |\n", + "| 39 | mouse | 89 | escalator | 139 | bench | 189 | swimming_pool |\n", + "| 40 | remote | 90 | cabinet | 140 | bike_rack | 190 | waterfall |\n", + "| 41 | cell phone | 91 | fireplace | 141 | billboard | 191 | wall |\n", + "| 42 | television | 92 | stove | 142 | sky | 192 | window |\n", + "| 43 | floor | 93 | arcade_machine | 143 | pole | 193 | window_blind |\n", + "| 44 | stage | 94 | gravel | 144 | fence | | |\n", + "| 45 | banana | 95 | platform | 145 | railing_banister | | |\n", + "| 46 | apple | 96 | playingfield | 146 | guard_rail | | |\n", + "| 47 | sandwich | 97 | railroad | 147 | mountain_hill | | |\n", + "| 48 | orange | 98 | road | 148 | rock | | |\n", + "| 49 | broccoli | 99 | snow | 149 | frisbee | | |\n" + ] + } + ], + "metadata": { + "colab": { + "name": "imagen3_editing.ipynb", + "toc_visible": true + }, + "kernelspec": { + "display_name": "Python 3", + "name": "python3" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} diff --git a/vision/getting-started/imagen4_upscale.ipynb b/vision/getting-started/imagen4_upscale.ipynb new file mode 100644 index 0000000..428ae59 --- /dev/null +++ b/vision/getting-started/imagen4_upscale.ipynb @@ -0,0 +1,372 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "ijGzTHJJUCPY" + }, + "outputs": [], + "source": [ + "# Copyright 2025 Google LLC\n", + "#\n", + "# Licensed under the Apache License, Version 2.0 (the \"License\");\n", + "# you may not use this file except in compliance with the License.\n", + "# You may obtain a copy of the License at\n", + "#\n", + "# https://www.apache.org/licenses/LICENSE-2.0\n", + "#\n", + "# Unless required by applicable law or agreed to in writing, software\n", + "# distributed under the License is distributed on an \"AS IS\" BASIS,\n", + "# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n", + "# See the License for the specific language governing permissions and\n", + "# limitations under the License." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "VEqbX8OhE8y9" + }, + "source": [ + "# Imagen 4 Image Upscale\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + "
    \n", + " \n", + " \"Google
    Open in Colab\n", + "
    \n", + "
    \n", + " \n", + " \"Google
    Open in Colab Enterprise\n", + "
    \n", + "
    \n", + " \n", + " \"Workbench
    Open in Workbench\n", + "
    \n", + "
    \n", + " \n", + " \"GitHub
    View on GitHub\n", + "
    \n", + "
    \n", + "\n", + "
    \n", + "\n", + "

    \n", + "Share to:\n", + "\n", + "\n", + " \"LinkedIn\n", + "\n", + "\n", + "\n", + " \"Bluesky\n", + "\n", + "\n", + "\n", + " \"X\n", + "\n", + "\n", + "\n", + " \"Reddit\n", + "\n", + "\n", + "\n", + " \"Facebook\n", + "\n", + "

    " + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "G1KDmM_PBAXz" + }, + "source": [ + "| Author |\n", + "| --- |\n", + "| [Katie Nguyen](https://github.com/katiemn) |" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "CkHPv2myT2cx" + }, + "source": [ + "## Overview\n", + "\n", + "### Imagen 4\n", + "\n", + "Imagen 4 on Agent Platform brings Google's state-of-the-art generative AI capabilities to application developers. It's capable of creating images with astonishing detail and rendered text across a wide variety of styles. With Imagen 4, you can also upscale a generated, edited, or existing image to increase the size of an image without losing quality.\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "DrkcqHrrwMAo" + }, + "source": [ + "In this tutorial, you will learn how to use the Google Gen AI SDK for Python to interact with Imagen 4's upscaling model to increase images from:\n", + "\n", + "- Imagen outputs\n", + "- Local files\n", + "- Cloud Storage\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "r11Gu7qNgx1p" + }, + "source": [ + "## Get started\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "No17Cw5hgx12" + }, + "source": [ + "### Install Google Gen AI SDK for Python\n" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": { + "id": "tFy3H3aPgx12" + }, + "outputs": [], + "source": [ + "%pip install --upgrade --quiet google-genai" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "dmWOrTJ3gx13" + }, + "source": [ + "### Authenticate your notebook environment (Colab only)\n", + "\n", + "If you are running this notebook on Google Colab, run the following cell to authenticate your environment.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": { + "id": "NyKGtVQjgx13" + }, + "outputs": [], + "source": [ + "import sys\n", + "\n", + "if \"google.colab\" in sys.modules:\n", + " from google.colab import auth\n", + "\n", + " auth.authenticate_user()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "Ua6PDqB1iBSb" + }, + "source": [ + "### Import libraries" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "h_SlrGfc-d74" + }, + "source": [ + "### Set Google Cloud project information\n", + "\n", + "To get started using Agent Platform, you must have an existing Google Cloud project and [enable the Agent Platform API](https://console.cloud.google.com/flows/enableapi?apiid=aiplatform.googleapis.com).\n", + "\n", + "Learn more about [setting up a project](https://docs.cloud.google.com/resource-manager/docs/creating-managing-projects) and a [development environment](https://cloud.google.com/docs/authentication/set-up-adc-local-dev-environment)." + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "metadata": { + "id": "6-0EZL74_GCA" + }, + "outputs": [], + "source": [ + "import os\n", + "\n", + "from google import genai\n", + "from google.genai import types\n", + "\n", + "# fmt: off\n", + "PROJECT_ID = \"[your-project-id]\" # @param {type: \"string\", placeholder: \"[your-project-id]\", isTemplate: true}\n", + "# fmt: on\n", + "if not PROJECT_ID or PROJECT_ID == \"[your-project-id]\":\n", + " PROJECT_ID = str(os.environ.get(\"GOOGLE_CLOUD_PROJECT\"))\n", + "\n", + "LOCATION = os.environ.get(\"GOOGLE_CLOUD_REGION\", \"us-central1\")\n", + "\n", + "client = genai.Client(enterprise=True, project=PROJECT_ID, location=LOCATION)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "VLmwIj2RD0Fx" + }, + "source": [ + "### Load the Imagen 4 models" + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "metadata": { + "id": "Fx2O_9h5sNAI" + }, + "outputs": [], + "source": [ + "generation_model = \"imagen-4.0-generate-001\"\n", + "upscale_model = \"imagen-4.0-upscale-preview\"" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "mvH2OTrK8IRS" + }, + "source": [ + "## Upscale images generated with Imagen\n", + "\n", + "With Imagen 4, you can generate high quality images with the following parameters:\n", + "- `aspect ratio`: `1:1`, `9:16`, `16:9`, `3:4`, `4:3`\n", + "- `number_of_images`: any value between 1 and 4\n", + "- `image_size`: `1K` or `2K`\n", + "\n", + "Once generated, you can then upscale it by a factor of `x2`, `x3`, or `x4`.\n", + "\n", + "By default, a digital watermark, or [SynthID](https://deepmind.google/technologies/synthid/), is added to Imagen 4 images." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "KJqHuGqi31rE" + }, + "outputs": [], + "source": [ + "prompt = \"A cartoon logo of a cat in a chef hat\"\n", + "\n", + "image = client.models.generate_images(\n", + " model=generation_model,\n", + " prompt=prompt,\n", + " config=types.GenerateImagesConfig(\n", + " aspect_ratio=\"1:1\",\n", + " number_of_images=1,\n", + " image_size=\"2K\",\n", + " ),\n", + ")\n", + "\n", + "upscale = client.models.upscale_image(\n", + " model=upscale_model,\n", + " image=image.generated_images[0].image,\n", + " upscale_factor=\"x2\",\n", + ")\n", + "upscale.generated_images[0].image.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "nDCthO_aoX0x" + }, + "source": [ + "## Upscale local images\n", + "\n", + "You're also able to upscale local images with Imagen 4. In the next cell you'll download an image to use in the request. If you'd like to use a different image, modify the `image` variable below." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "pwdaN5x1Z3mc" + }, + "outputs": [], + "source": [ + "!wget https://storage.googleapis.com/cloud-samples-data/generative-ai/image/boats.jpeg\n", + "\n", + "image = \"boats.jpeg\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "Mi08aLXpaEX0" + }, + "outputs": [], + "source": [ + "upscale = client.models.upscale_image(\n", + " model=upscale_model,\n", + " image=types.Image.from_file(location=image),\n", + " upscale_factor=\"x3\",\n", + ")\n", + "upscale.generated_images[0].image.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "UND0wrdf3mGS" + }, + "source": [ + "## Upscale images from Cloud Storage\n", + "\n", + "Additionally, you can upscale images stored in Cloud Storage. If you'd like to change the initial image in this example modify the `image` variable below." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "e5VzcTI0bvon" + }, + "outputs": [], + "source": [ + "image = \"gs://cloud-samples-data/generative-ai/image/daisy.jpg\"\n", + "\n", + "upscale = client.models.upscale_image(\n", + " model=upscale_model,\n", + " image=types.Image(gcs_uri=image),\n", + " upscale_factor=\"x4\",\n", + ")\n", + "upscale.generated_images[0].image.show()" + ] + } + ], + "metadata": { + "colab": { + "name": "imagen4_upscale.ipynb", + "toc_visible": true + }, + "kernelspec": { + "display_name": "Python 3", + "name": "python3" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} diff --git a/vision/getting-started/veo3_advanced_controls.ipynb b/vision/getting-started/veo3_advanced_controls.ipynb new file mode 100644 index 0000000..a10987c --- /dev/null +++ b/vision/getting-started/veo3_advanced_controls.ipynb @@ -0,0 +1,540 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "KjTHAV8FgEza" + }, + "outputs": [], + "source": [ + "# Copyright 2025 Google LLC\n", + "#\n", + "# Licensed under the Apache License, Version 2.0 (the \"License\");\n", + "# you may not use this file except in compliance with the License.\n", + "# You may obtain a copy of the License at\n", + "#\n", + "# https://www.apache.org/licenses/LICENSE-2.0\n", + "#\n", + "# Unless required by applicable law or agreed to in writing, software\n", + "# distributed under the License is distributed on an \"AS IS\" BASIS,\n", + "# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n", + "# See the License for the specific language governing permissions and\n", + "# limitations under the License." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "UdL4uvQQs76x" + }, + "source": [ + "# Veo 3.1 Advanced Controls\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + "
    \n", + " \n", + " \"Google
    Open in Colab\n", + "
    \n", + "
    \n", + " \n", + " \"Google
    Open in Colab Enterprise\n", + "
    \n", + "
    \n", + " \n", + " \"Workbench
    Open in Workbench\n", + "
    \n", + "
    \n", + " \n", + " \"GitHub
    View on GitHub\n", + "
    \n", + "
    \n", + "\n", + "
    \n", + "\n", + "

    \n", + "Share to:\n", + "\n", + "\n", + " \"LinkedIn\n", + "\n", + "\n", + "\n", + " \"Bluesky\n", + "\n", + "\n", + "\n", + " \"X\n", + "\n", + "\n", + "\n", + " \"Reddit\n", + "\n", + "\n", + "\n", + " \"Facebook\n", + "\n", + "

    " + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "lUCUMoTmN_lJ" + }, + "source": [ + "| Author |\n", + "| --- |\n", + "| [Katie Nguyen](https://github.com/katiemn) |" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "rDjAqcgigwdX" + }, + "source": [ + "## Overview\n", + "\n", + "### Veo 3.1\n", + "\n", + "Veo 3.1 on Agent Platform gives application developers access to Google's cutting-edge video generation. This model creates videos with stunning detail and realistic physics across a wide array of visual styles. Veo 3.1 enhances video quality from text and image prompts, and now includes dialogue and audio generation.\n", + "\n", + "In this tutorial, you will learn how to use the Google Gen AI SDK for Python to interact with Veo 3.1 advanced control features, including:\n", + " - Frame interpolation\n", + " - Video extend\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "v2_iOv5uhXVg" + }, + "source": [ + "## Get started" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "K4uerc9Xhf1f" + }, + "source": [ + "### Install Google Gen AI SDK for Python" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "rJyFNKoQhiwF" + }, + "outputs": [], + "source": [ + "%pip install --upgrade --quiet google-genai" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "GWYnCW0-h6HI" + }, + "source": [ + "### Authenticate your notebook environment (Colab only)\n", + "\n", + "If you are running this notebook on Google Colab, run the following cell to authenticate your environment." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "bqz5LUG6h8fA" + }, + "outputs": [], + "source": [ + "import sys\n", + "\n", + "if \"google.colab\" in sys.modules:\n", + " from google.colab import auth\n", + "\n", + " auth.authenticate_user()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "LVrasKoriKZn" + }, + "source": [ + "### Import libraries" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "c5XgNQ2HD_HH" + }, + "outputs": [], + "source": [ + "import os\n", + "import time\n", + "import urllib.request\n", + "\n", + "import matplotlib.image as img\n", + "import matplotlib.pyplot as plt\n", + "from IPython.display import Video, display\n", + "from PIL import Image as PIL_Image\n", + "from google import genai\n", + "from google.genai import types" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "yxBkUEqdiB1g" + }, + "source": [ + "### Set Google Cloud project information\n", + "\n", + "To get started using Agent Platform, you must have an existing Google Cloud project and [enable the Agent Platform API](https://console.cloud.google.com/flows/enableapi?apiid=aiplatform.googleapis.com).\n", + "\n", + "Learn more about [setting up a project](https://docs.cloud.google.com/resource-manager/docs/creating-managing-projects) and a [development environment](https://cloud.google.com/docs/authentication/set-up-adc-local-dev-environment)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "GtjPBmYHiEfx" + }, + "outputs": [], + "source": [ + "# fmt: off\n", + "PROJECT_ID = \"[your-project-id]\" # @param {type: \"string\", placeholder: \"[your-project-id]\", isTemplate: true}\n", + "# fmt: on\n", + "if not PROJECT_ID or PROJECT_ID == \"[your-project-id]\":\n", + " PROJECT_ID = str(os.environ.get(\"GOOGLE_CLOUD_PROJECT\"))\n", + "\n", + "LOCATION = os.environ.get(\"GOOGLE_CLOUD_REGION\", \"us-central1\")\n", + "\n", + "client = genai.Client(enterprise=True, project=PROJECT_ID, location=LOCATION)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "qD_bwA9hiMzL" + }, + "source": [ + "### Define a helper function to display media" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "GUrEwbvFiPhJ" + }, + "outputs": [], + "source": [ + "def show_video(video: str | bytes) -> None:\n", + " if isinstance(video, str):\n", + " file_name = video.split(\"/\")[-1]\n", + " !gsutil cp {video} {file_name}\n", + " display(Video(file_name, embed=True, width=600))\n", + " else:\n", + " with open(\"sample.mp4\", \"wb\") as out_file:\n", + " out_file.write(video)\n", + " display(Video(\"sample.mp4\", embed=True, width=600))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "2jaSOOadiUj6" + }, + "source": [ + "### Load the video generation model" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "APRfTklCiYR2" + }, + "outputs": [], + "source": [ + "video_model = \"veo-3.1-generate-001\"\n", + "video_extension_model = \"veo-3.1-generate-preview\"" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "_1-VPOexGWe8" + }, + "source": [ + "## Video generation with Veo 3.1 advanced controls" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "P604bVwQeDPN" + }, + "source": [ + "### Frame interpolation\n", + "\n", + "With frame interpolation, you can specify the first and last frame of the video clip, and Veo will generate the rest.\n", + "\n", + "#### Load the images\n", + "\n", + "Download the first and last frames that you will use in the request. If you would like to use local images, simply comment out the `wget` command and edit the `first_frame` and `last_frame` variables.\n", + "\n", + "**Note:** This feature tends to work best when the starting and ending frames are similar in nature." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "5-fpFTzwefcH" + }, + "outputs": [], + "source": [ + "# Download first frame\n", + "!wget https://storage.googleapis.com/cloud-samples-data/generative-ai/image/cookies.png\n", + "# fmt: off\n", + "first_frame = \"cookies.png\" # @param {type: 'string'}\n", + "\n", + "# Download last frame\n", + "!wget https://storage.googleapis.com/cloud-samples-data/generative-ai/image/cookies-milk.png\n", + "last_frame = \"cookies-milk.png\" # @param {type: 'string'}\n", + "# fmt: on\n", + "\n", + "# Display the images\n", + "fig, axis = plt.subplots(1, 2, figsize=(12, 6))\n", + "axis[0].imshow(img.imread(first_frame))\n", + "axis[0].set_title(\"First Frame\")\n", + "axis[1].imshow(img.imread(last_frame))\n", + "axis[1].set_title(\"Last Frame\")\n", + "for ax in axis:\n", + " ax.axis(\"off\")\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "yKR0riV_K5nT" + }, + "source": [ + "With Veo 3.1, you have the option to generate videos with audio from a text prompt and/or input image(s). In order to generate a video in the following sample, specify the following info:\n", + "- **Prompt:** A description of the video you would like to see. Since you're starting with images you can also leave the prompt field empty.\n", + "- **Aspect ratio:** Select either 16:9 or 9:16.\n", + "- **Resolution:** 720p, 1080p or 4k.\n", + "- **Number of videos:** Set this value to 1, 2, 3, or 4.\n", + "- **Video duration:** Can be 4, 6, or 8 seconds.\n", + "- **Person generation:** Set to `allow_adult` or `dont_allow`.\n", + "- **Audio generation:** Set `generate_audio` to True if you'd like audio included in your generated video.\n", + "\n", + "\n", + "**Safety:** All Veo videos include [SynthID](https://deepmind.google/technologies/synthid/), which embeds a digital watermark directly into the AI-generated video.\n", + "\n", + "**NOTE:** Generating a video in 4k will introduce increased latency up to several minutes." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "3YzkQI5JekEh" + }, + "outputs": [], + "source": [ + "# fmt: off\n", + "prompt = \"a hand reaches in and places a glass of milk next to the plate of cookies\" # @param {type: 'string'}\n", + "# fmt: on\n", + "\n", + "operation = client.models.generate_videos(\n", + " model=video_model,\n", + " prompt=prompt,\n", + " image=types.Image.from_file(location=first_frame),\n", + " config=types.GenerateVideosConfig(\n", + " last_frame=types.Image.from_file(location=last_frame),\n", + " aspect_ratio=\"9:16\",\n", + " resolution=\"1080p\",\n", + " number_of_videos=1,\n", + " duration_seconds=8,\n", + " person_generation=\"allow_adult\",\n", + " generate_audio=True,\n", + " ),\n", + ")\n", + "\n", + "while not operation.done:\n", + " time.sleep(10)\n", + " operation = client.operations.get(operation)\n", + " print(operation)\n", + "\n", + "if operation.response:\n", + " show_video(operation.result.generated_videos[0].video.video_bytes)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "Rln2_Oi3Piu-" + }, + "source": [ + "In this next example, you'll continue to use first and last frames stored in Cloud Storage. However, in this request, you'll reference the images in Cloud Storage directly, rather than downloading them locally first. If you'd like to use a different Cloud Storage image, replace the URL and `gcs_uri` variables below." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "aCzame3wGEpi" + }, + "outputs": [], + "source": [ + "first_frame = PIL_Image.open(\n", + " urllib.request.urlopen(\n", + " \"https://storage.googleapis.com/cloud-samples-data/generative-ai/image/forest-1.png\"\n", + " )\n", + ")\n", + "first_frame_gcs = \"gs://cloud-samples-data/generative-ai/image/forest-1.png\"\n", + "\n", + "last_frame = PIL_Image.open(\n", + " urllib.request.urlopen(\n", + " \"https://storage.googleapis.com/cloud-samples-data/generative-ai/image/forest-2.png\"\n", + " )\n", + ")\n", + "last_frame_gcs = \"gs://cloud-samples-data/generative-ai/image/forest-2.png\"\n", + "\n", + "# Display the images\n", + "fig, axis = plt.subplots(1, 2, figsize=(18, 6))\n", + "axis[0].imshow(first_frame)\n", + "axis[1].imshow(last_frame)\n", + "for ax in axis:\n", + " ax.axis(\"off\")\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "XPu4QrPAGanp" + }, + "source": [ + "Rather than output `video_bytes` in this section, you'll save your video to Cloud Storage. In order to accomplish this, set your Cloud Storage bucket location in `output_gcs`.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "ylUHNer1GleN" + }, + "outputs": [], + "source": [ + "# fmt: off\n", + "prompt = \"the chipmunk climbs down the tree to sit next to the rabbit\" # @param {type: 'string'}\n", + "# fmt: on\n", + "output_gcs = \"gs://[your-bucket-path]\" # @param {type: 'string'}\n", + "\n", + "operation = client.models.generate_videos(\n", + " model=video_model,\n", + " prompt=prompt,\n", + " image=types.Image(gcs_uri=first_frame_gcs, mime_type=\"image/png\"),\n", + " config=types.GenerateVideosConfig(\n", + " last_frame=types.Image(gcs_uri=last_frame_gcs, mime_type=\"image/png\"),\n", + " output_gcs_uri=output_gcs,\n", + " aspect_ratio=\"16:9\",\n", + " number_of_videos=1,\n", + " duration_seconds=6,\n", + " resolution=\"1080p\",\n", + " person_generation=\"allow_adult\",\n", + " generate_audio=True,\n", + " ),\n", + ")\n", + "\n", + "while not operation.done:\n", + " time.sleep(10)\n", + " operation = client.operations.get(operation)\n", + " print(operation)\n", + "\n", + "if operation.response:\n", + " show_video(operation.result.generated_videos[0].video.uri)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "gg6mf3iWbjRP" + }, + "source": [ + "### Video extend\n", + "\n", + "In this next example, you'll use Veo to extend an existing video. You'll use the same parameters as before. However, the following are configured differently in this instance:\n", + "- **Input video:** Since you're starting with a video, specify a Cloud Storage video location in the `video_gcs` variable.\n", + "- **Output video:** As these videos will be bigger, you'll save your output video in Cloud Storage. Include the Cloud Storage bucket in the `output_gcs` field.\n", + "- **Video duration:** You can extend your video by 7 seconds." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "-dXqvIuAbzVQ" + }, + "outputs": [], + "source": [ + "prompt = \"a butterfly flies in and lands on the flower\" # @param {type: 'string'}\n", + "# fmt: off\n", + "video_gcs = \"gs://cloud-samples-data/generative-ai/video/red-flower.mp4\" # @param {type: 'string'}\n", + "# fmt: on\n", + "output_gcs = \"gs://[your-bucket-path]\" # @param {type: 'string'}\n", + "\n", + "operation = client.models.generate_videos(\n", + " model=video_extension_model,\n", + " prompt=prompt,\n", + " video=types.Video(uri=video_gcs, mime_type=\"video/mp4\"),\n", + " config=types.GenerateVideosConfig(\n", + " output_gcs_uri=output_gcs,\n", + " number_of_videos=1,\n", + " duration_seconds=7,\n", + " person_generation=\"allow_adult\",\n", + " generate_audio=True,\n", + " ),\n", + ")\n", + "\n", + "while not operation.done:\n", + " time.sleep(5)\n", + " operation = client.operations.get(operation)\n", + " print(operation)\n", + "\n", + "if operation.response:\n", + " show_video(operation.result.generated_videos[0].video.uri)" + ] + } + ], + "metadata": { + "colab": { + "name": "veo3_advanced_controls.ipynb", + "toc_visible": true + }, + "kernelspec": { + "display_name": "Python 3", + "name": "python3" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} diff --git a/vision/getting-started/veo3_reference_to_video.ipynb b/vision/getting-started/veo3_reference_to_video.ipynb new file mode 100644 index 0000000..9ec9a4f --- /dev/null +++ b/vision/getting-started/veo3_reference_to_video.ipynb @@ -0,0 +1,732 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "bZKaz0oSwAx-" + }, + "outputs": [], + "source": [ + "# Copyright 2025 Google LLC\n", + "#\n", + "# Licensed under the Apache License, Version 2.0 (the \"License\");\n", + "# you may not use this file except in compliance with the License.\n", + "# You may obtain a copy of the License at\n", + "#\n", + "# https://www.apache.org/licenses/LICENSE-2.0\n", + "#\n", + "# Unless required by applicable law or agreed to in writing, software\n", + "# distributed under the License is distributed on an \"AS IS\" BASIS,\n", + "# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n", + "# See the License for the specific language governing permissions and\n", + "# limitations under the License." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "UdL4uvQQs76x" + }, + "source": [ + "# Veo 3.1 Reference to Video\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + "
    \n", + " \n", + " \"Google
    Open in Colab\n", + "
    \n", + "
    \n", + " \n", + " \"Google
    Open in Colab Enterprise\n", + "
    \n", + "
    \n", + " \n", + " \"Workbench
    Open in Workbench\n", + "
    \n", + "
    \n", + " \n", + " \"GitHub
    View on GitHub\n", + "
    \n", + "
    \n", + "\n", + "
    \n", + "\n", + "

    \n", + "Share to:\n", + "\n", + "\n", + " \"LinkedIn\n", + "\n", + "\n", + "\n", + " \"Bluesky\n", + "\n", + "\n", + "\n", + " \"X\n", + "\n", + "\n", + "\n", + " \"Reddit\n", + "\n", + "\n", + "\n", + " \"Facebook\n", + "\n", + "

    " + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "NnMaDH8jwReT" + }, + "source": [ + "| | |\n", + "|-|-|\n", + "|Author(s) | [Katie Nguyen](https://github.com/katiemn) |" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "vIDE4FhjwW67" + }, + "source": [ + "## Overview\n", + "\n", + "### Veo 3.1\n", + "\n", + "Veo 3.1 on Agent Platform gives application developers access to Google's cutting-edge video generation. Veo 3.1 enhances video quality from text and image prompts, and now includes dialogue and audio generation.\n", + "\n", + "Reference-to-Video functionality on Veo 3.1 is optimized for high-energy, short-form narratives. It excels at maintaining consistency across rapid intercutting and dynamic camera transitions for better visual storytelling.\n", + "\n", + "In this tutorial, you will learn how to use the Google Gen AI SDK for Python to interact with Veo 3.1 to:\n", + "- Generate a video from asset images, including subjects, objects and scenes\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "dEPqvne0w4qx" + }, + "source": [ + "## Get started" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "s8p3AOlALGpj" + }, + "source": [ + "### Install Google Gen AI SDK for Python" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "LWGj2AmpLJ2D" + }, + "outputs": [], + "source": [ + "%pip install --upgrade --quiet google-genai" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "aDvFfD83w7iL" + }, + "source": [ + "### Authenticate your notebook environment (Colab only)\n", + "\n", + "If you are running this notebook on Google Colab, run the following cell to authenticate your environment." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": { + "id": "iTfXlEVQw9xV" + }, + "outputs": [], + "source": [ + "import sys\n", + "\n", + "if \"google.colab\" in sys.modules:\n", + " from google.colab import auth\n", + "\n", + " auth.authenticate_user()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "PYCYpliKxFES" + }, + "source": [ + "### Import libraries" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "ffYy0e81xAV6" + }, + "source": [ + "### Set Google Cloud project information\n", + "\n", + "To get started using Agent Platform, you must have an existing Google Cloud project and [enable the Agent Platform API](https://console.cloud.google.com/flows/enableapi?apiid=aiplatform.googleapis.com).\n", + "\n", + "Learn more about [setting up a project](https://docs.cloud.google.com/resource-manager/docs/creating-managing-projects) and a [development environment](https://cloud.google.com/docs/authentication/set-up-adc-local-dev-environment)." + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": { + "id": "PMz0sZASxCTU" + }, + "outputs": [], + "source": [ + "import os\n", + "import time\n", + "import urllib.request\n", + "\n", + "import matplotlib.image as img\n", + "import matplotlib.pyplot as plt\n", + "import numpy as np\n", + "from IPython.display import Video, display\n", + "from PIL import Image as PIL_Image\n", + "from google import genai\n", + "from google.genai import types\n", + "\n", + "# fmt: off\n", + "PROJECT_ID = \"[your-project-id]\" # @param {type: \"string\", placeholder: \"[your-project-id]\", isTemplate: true}\n", + "# fmt: on\n", + "if not PROJECT_ID or PROJECT_ID == \"[your-project-id]\":\n", + " PROJECT_ID = str(os.environ.get(\"GOOGLE_CLOUD_PROJECT\"))\n", + "\n", + "LOCATION = os.environ.get(\"GOOGLE_CLOUD_REGION\", \"us-central1\")\n", + "\n", + "client = genai.Client(enterprise=True, project=PROJECT_ID, location=LOCATION)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "br8QTmuyxL5R" + }, + "source": [ + "### Define helper functions" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": { + "id": "TgkK6Vr4xN5j" + }, + "outputs": [], + "source": [ + "def show_video(video):\n", + " if isinstance(video, str):\n", + " file_name = video.split(\"/\")[-1]\n", + " !gcloud storage cp {video} {file_name}\n", + " display(Video(file_name, embed=True, width=600))\n", + " else:\n", + " with open(\"sample.mp4\", \"wb\") as out_file:\n", + " out_file.write(video)\n", + " display(Video(\"sample.mp4\", embed=True, width=600))\n", + "\n", + "\n", + "def show_images(\n", + " images: list[str],\n", + "):\n", + " fig, axes = plt.subplots(1, len(images), figsize=(12, 6))\n", + " if len(images) == 1:\n", + " axes = np.array([axes])\n", + " for i, ax in enumerate(axes):\n", + " image = img.imread(images[i])\n", + " ax.imshow(image)\n", + " ax.axis(\"off\")\n", + " plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "5a6UxKZfxQoH" + }, + "source": [ + "### Load the video model" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": { + "id": "H6K66dOfxSmr" + }, + "outputs": [], + "source": [ + "video_model = \"veo-3.1-generate-preview\"\n", + "video_model_fast = \"veo-3.1-fast-generate-preview\"" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "UMANSB1YN11I" + }, + "source": [ + "## Reference images to videos\n", + "\n", + "With Reference-to-Video in Veo 3.1, you can use reference images to generate videos. The reference images are `asset` images of subjects, objects, or scenes that will be included in the final video output.\n", + "\n", + "**NOTE:** You can include up to 3 `asset` images in a request." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "UBPpj5BTZaY9" + }, + "source": [ + "### Asset references\n", + "\n", + "Download and display the asset images that you'll use in the following requests. To use your own local images, modify the URLs in the `wget` command and update the `first_image`, `second_image`, and/or `third_image` variables accordingly." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "X81WV5lIpSEe" + }, + "source": [ + "#### Subject reference images\n", + "\n", + "In this example, you'll use two subject reference images of different people. You'll generate a new scene for them based on a text prompt." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "E2vrXKXdu39e" + }, + "outputs": [], + "source": [ + "# Download subject images from Cloud Storage\n", + "!wget https://storage.googleapis.com/cloud-samples-data/generative-ai/image/man-in-field.png\n", + "\n", + "!wget https://storage.googleapis.com/cloud-samples-data/generative-ai/image/woman.jpeg" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "twURgtG16oUu" + }, + "source": [ + "Set the `first_image` and `second_image` variables." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "e_aPJ5dzvCnS" + }, + "outputs": [], + "source": [ + "first_image = \"man-in-field.png\" # @param {type: 'string'}\n", + "second_image = \"woman.jpeg\" # @param {type: 'string'}\n", + "\n", + "show_images([first_image, second_image])" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "LcrdbeRE9oW5" + }, + "source": [ + "Now, you'll send a request to generate a video. With Veo 3.1, you can generate videos with audio from a text prompt, input image(s), or both. In order to generate a video in the following sample, specify the following info:\n", + "\n", + " - **Prompt:** A description of the video you would like to see with the reference images.\n", + " - **Reference images:** Up to three `asset` images.\n", + " - **Aspect ratio:** 16:9 (Landscape), 9:16 (Portrait)\n", + " - **Number of videos:** Set this value to 1, 2, 3, or 4\n", + " - **Video duration:** 8 seconds\n", + " - **Resolution:** 720p, 1080p, 4k\n", + " - **Person generation:** Set to `allow_adult` or `dont_allow`.\n", + " - **Generate audio:** Set to `True` if you'd like audio in your generated video.\n", + "\n", + " **NOTE:** Generating a video in 4k will introduce increased latency up to several minutes." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "Ym4PP2AvvPW2" + }, + "outputs": [], + "source": [ + "prompt = \"\"\"\n", + "a woman and a man drinking a cup of coffee in a cafe, chatting about the new restaurant around the block\n", + "\"\"\"\n", + "\n", + "operation = client.models.generate_videos(\n", + " model=video_model_fast,\n", + " prompt=prompt,\n", + " config=types.GenerateVideosConfig(\n", + " reference_images=[\n", + " types.VideoGenerationReferenceImage(\n", + " image=types.Image.from_file(location=first_image),\n", + " reference_type=\"asset\",\n", + " ),\n", + " types.VideoGenerationReferenceImage(\n", + " image=types.Image.from_file(location=second_image),\n", + " reference_type=\"asset\",\n", + " ),\n", + " ],\n", + " aspect_ratio=\"16:9\",\n", + " number_of_videos=1,\n", + " duration_seconds=8,\n", + " resolution=\"1080p\",\n", + " person_generation=\"allow_adult\",\n", + " generate_audio=True,\n", + " ),\n", + ")\n", + "\n", + "while not operation.done:\n", + " time.sleep(15)\n", + " operation = client.operations.get(operation)\n", + " print(operation)\n", + "\n", + "if operation.response:\n", + " show_video(operation.result.generated_videos[0].video.video_bytes)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "ZSPqF01yySYl" + }, + "source": [ + "#### Setting reference image\n", + "\n", + "Now, you'll use a single scenery reference image and a text prompt to generate a video with different subjects and actions." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "h5Jz6bYKyby5" + }, + "outputs": [], + "source": [ + "# Download the image from Cloud Storage\n", + "!wget https://storage.googleapis.com/cloud-samples-data/generative-ai/image/room.png" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "IotQjwiP7Og2" + }, + "source": [ + "Set the `first_image` variable." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "ULTrObxdylip" + }, + "outputs": [], + "source": [ + "first_image = \"room.png\" # @param {type: 'string'}\n", + "\n", + "show_images([first_image])" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "tqEjvGNJ7TD4" + }, + "source": [ + "Run the request. Update the `prompt` if you'd like to see different content within the scene." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "w7KnIJMYyv66" + }, + "outputs": [], + "source": [ + "prompt = \"\"\"\n", + "a Corgi walks around in a living room, then jumps on the couch and starts reading a book on the coffee table\n", + "\"\"\"\n", + "\n", + "operation = client.models.generate_videos(\n", + " model=video_model,\n", + " prompt=prompt,\n", + " config=types.GenerateVideosConfig(\n", + " reference_images=[\n", + " types.VideoGenerationReferenceImage(\n", + " image=types.Image.from_file(location=first_image),\n", + " reference_type=\"asset\",\n", + " ),\n", + " ],\n", + " aspect_ratio=\"9:16\",\n", + " number_of_videos=1,\n", + " duration_seconds=8,\n", + " resolution=\"720p\",\n", + " person_generation=\"allow_adult\",\n", + " generate_audio=True,\n", + " ),\n", + ")\n", + "\n", + "while not operation.done:\n", + " time.sleep(15)\n", + " operation = client.operations.get(operation)\n", + " print(operation)\n", + "\n", + "if operation.response:\n", + " show_video(operation.result.generated_videos[0].video.video_bytes)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "0ybB1oG05W4X" + }, + "source": [ + "#### Product reference image\n", + "\n", + "Next, you'll use a product reference image and a text prompt to generate a video. This will demonstrate how Veo maintains product consistency while in motion." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "Nnfezu0-6aAz" + }, + "outputs": [], + "source": [ + "# Download the image from Cloud Storage\n", + "!wget https://storage.googleapis.com/cloud-samples-data/generative-ai/image/vase.png" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "zEqhYMQG8C5w" + }, + "source": [ + "Set the `first_image` variable." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "fl9hn5oT78Wx" + }, + "outputs": [], + "source": [ + "first_image = \"vase.png\" # @param {type: 'string'}\n", + "\n", + "show_images([first_image])" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "4ivJgSH68RE3" + }, + "source": [ + "Run the request. Update the `prompt` if you'd like to visualize the product in a different manner." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "hvFkg-vzfcY-" + }, + "outputs": [], + "source": [ + "prompt = \"\"\"\n", + "a person walks in carrying a vase full of flowers and places the vase on a kitchen table\n", + "\"\"\"\n", + "\n", + "operation = client.models.generate_videos(\n", + " model=video_model,\n", + " prompt=prompt,\n", + " config=types.GenerateVideosConfig(\n", + " reference_images=[\n", + " types.VideoGenerationReferenceImage(\n", + " image=types.Image.from_file(location=first_image),\n", + " reference_type=\"asset\",\n", + " ),\n", + " ],\n", + " aspect_ratio=\"9:16\",\n", + " number_of_videos=1,\n", + " duration_seconds=8,\n", + " resolution=\"720p\",\n", + " person_generation=\"allow_adult\",\n", + " generate_audio=True,\n", + " ),\n", + ")\n", + "\n", + "while not operation.done:\n", + " time.sleep(15)\n", + " operation = client.operations.get(operation)\n", + " print(operation)\n", + "\n", + "if operation.response:\n", + " show_video(operation.result.generated_videos[0].video.video_bytes)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "FOqUuJRGoQtO" + }, + "source": [ + "#### Three distinct reference images\n", + "\n", + "In this example, you'll use three different reference images (a product, a subject, and a scene) from Google Cloud Storage. Instead of downloading them, you'll reference their Cloud Storage URIs directly. To use your own images, replace the gcs_uri variables below." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "5m1xhMENZrYx" + }, + "outputs": [], + "source": [ + "first_image = PIL_Image.open(\n", + " urllib.request.urlopen(\n", + " \"https://storage.googleapis.com/cloud-samples-data/generative-ai/image/flowers.png\"\n", + " )\n", + ")\n", + "first_image_gcs = \"gs://cloud-samples-data/generative-ai/image/flowers.png\"\n", + "\n", + "second_image = PIL_Image.open(\n", + " urllib.request.urlopen(\n", + " \"https://storage.googleapis.com/cloud-samples-data/generative-ai/image/suitcase.png\"\n", + " )\n", + ")\n", + "second_image_gcs = \"gs://cloud-samples-data/generative-ai/image/suitcase.png\"\n", + "\n", + "third_image = PIL_Image.open(\n", + " urllib.request.urlopen(\n", + " \"https://storage.googleapis.com/cloud-samples-data/generative-ai/image/woman.jpg\"\n", + " )\n", + ")\n", + "third_image_gcs = \"gs://cloud-samples-data/generative-ai/image/woman.jpg\"\n", + "\n", + "# Display the images\n", + "fig, axis = plt.subplots(1, 3, figsize=(18, 6))\n", + "axis[0].imshow(first_image)\n", + "axis[1].imshow(second_image)\n", + "axis[2].imshow(third_image)\n", + "for ax in axis:\n", + " ax.axis(\"off\")\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "5PAnNvt79xNU" + }, + "source": [ + "Rather than output video_bytes in this section, you'll save your video to Cloud Storage. In order to accomplish this, set your Cloud Storage bucket location in `output_gcs`.\n", + "\n", + "**Safety:** All Veo videos include [SynthID](https://deepmind.google/science/synthid/), which embeds a digital watermark directly into the AI-generated video." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "0Jq-yKddAFJw" + }, + "outputs": [], + "source": [ + "# fmt: off\n", + "prompt = \"a wide shot of a woman wheeling a blue suitcase through a flower field\" # @param {type: 'string'}\n", + "# fmt: on\n", + "output_gcs = \"gs://[your-bucket-path]\" # @param {type: 'string'}\n", + "\n", + "operation = client.models.generate_videos(\n", + " model=video_model,\n", + " prompt=prompt,\n", + " config=types.GenerateVideosConfig(\n", + " reference_images=[\n", + " types.VideoGenerationReferenceImage(\n", + " image=types.Image(gcs_uri=first_image_gcs, mime_type=\"image/png\"),\n", + " reference_type=\"asset\",\n", + " ),\n", + " types.VideoGenerationReferenceImage(\n", + " image=types.Image(gcs_uri=second_image_gcs, mime_type=\"image/png\"),\n", + " reference_type=\"asset\",\n", + " ),\n", + " types.VideoGenerationReferenceImage(\n", + " image=types.Image(gcs_uri=third_image_gcs, mime_type=\"image/jpeg\"),\n", + " reference_type=\"asset\",\n", + " ),\n", + " ],\n", + " output_gcs_uri=output_gcs,\n", + " aspect_ratio=\"16:9\",\n", + " number_of_videos=1,\n", + " duration_seconds=8,\n", + " resolution=\"1080p\",\n", + " person_generation=\"allow_adult\",\n", + " generate_audio=True,\n", + " ),\n", + ")\n", + "\n", + "while not operation.done:\n", + " time.sleep(15)\n", + " operation = client.operations.get(operation)\n", + " print(operation)\n", + "\n", + "if operation.response:\n", + " show_video(operation.result.generated_videos[0].video.uri)" + ] + } + ], + "metadata": { + "colab": { + "name": "veo3_reference_to_video.ipynb", + "toc_visible": true + }, + "kernelspec": { + "display_name": "Python 3", + "name": "python3" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} diff --git a/vision/getting-started/veo3_video_generation.ipynb b/vision/getting-started/veo3_video_generation.ipynb new file mode 100644 index 0000000..6ae776d --- /dev/null +++ b/vision/getting-started/veo3_video_generation.ipynb @@ -0,0 +1,739 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "KjTHAV8FgEza" + }, + "outputs": [], + "source": [ + "# Copyright 2025 Google LLC\n", + "#\n", + "# Licensed under the Apache License, Version 2.0 (the \"License\");\n", + "# you may not use this file except in compliance with the License.\n", + "# You may obtain a copy of the License at\n", + "#\n", + "# https://www.apache.org/licenses/LICENSE-2.0\n", + "#\n", + "# Unless required by applicable law or agreed to in writing, software\n", + "# distributed under the License is distributed on an \"AS IS\" BASIS,\n", + "# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n", + "# See the License for the specific language governing permissions and\n", + "# limitations under the License." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "UdL4uvQQs76x" + }, + "source": [ + "# Veo 3.1 Video Generation\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + "
    \n", + " \n", + " \"Google
    Open in Colab\n", + "
    \n", + "
    \n", + " \n", + " \"Google
    Open in Colab Enterprise\n", + "
    \n", + "
    \n", + " \n", + " \"Workbench
    Open in Workbench\n", + "
    \n", + "
    \n", + " \n", + " \"GitHub
    View on GitHub\n", + "
    \n", + "
    \n", + "\n", + "
    \n", + "\n", + "

    \n", + "Share to:\n", + "\n", + "\n", + " \"LinkedIn\n", + "\n", + "\n", + "\n", + " \"Bluesky\n", + "\n", + "\n", + "\n", + " \"X\n", + "\n", + "\n", + "\n", + " \"Reddit\n", + "\n", + "\n", + "\n", + " \"Facebook\n", + "\n", + "

    " + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "lUCUMoTmN_lJ" + }, + "source": [ + "| | |\n", + "|-|-|\n", + "|Author(s) | [Katie Nguyen](https://github.com/katiemn) |" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "rDjAqcgigwdX" + }, + "source": [ + "## Overview\n", + "\n", + "### Veo 3.1\n", + "\n", + "Veo 3.1 on Agent Platform gives application developers access to Google's cutting-edge video generation. This model creates videos with stunning detail and realistic physics across a wide array of visual styles. Veo 3.1 enhances video quality from text and image prompts, and now includes dialogue and audio generation.\n", + "\n", + "In this tutorial, you will learn how to use the Google Gen AI SDK for Python to interact with Veo 3.1 and generate new videos with audio from:\n", + "- Enhanced text prompts\n", + "- Starting input images" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "v2_iOv5uhXVg" + }, + "source": [ + "## Get started" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "K4uerc9Xhf1f" + }, + "source": [ + "### Install Google Gen AI SDK for Python" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "rJyFNKoQhiwF" + }, + "outputs": [], + "source": [ + "%pip install --upgrade --quiet google-genai" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "GWYnCW0-h6HI" + }, + "source": [ + "### Authenticate your notebook environment (Colab only)\n", + "\n", + "If you are running this notebook on Google Colab, run the following cell to authenticate your environment." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": { + "id": "bqz5LUG6h8fA" + }, + "outputs": [], + "source": [ + "import sys\n", + "\n", + "if \"google.colab\" in sys.modules:\n", + " from google.colab import auth\n", + "\n", + " auth.authenticate_user()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "LVrasKoriKZn" + }, + "source": [ + "### Import libraries" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "yxBkUEqdiB1g" + }, + "source": [ + "### Set Google Cloud project information\n", + "\n", + "To get started using Agent Platform, you must have an existing Google Cloud project and [enable the Agent Platform API](https://console.cloud.google.com/flows/enableapi?apiid=aiplatform.googleapis.com).\n", + "\n", + "Learn more about [setting up a project](https://docs.cloud.google.com/resource-manager/docs/creating-managing-projects) and a [development environment](https://cloud.google.com/docs/authentication/set-up-adc-local-dev-environment)." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": { + "id": "GtjPBmYHiEfx" + }, + "outputs": [], + "source": [ + "import os\n", + "import time\n", + "\n", + "import matplotlib.image as img\n", + "import matplotlib.pyplot as plt\n", + "from IPython.display import Markdown, Video, display\n", + "from google import genai\n", + "from google.genai import types\n", + "\n", + "# fmt: off\n", + "PROJECT_ID = \"[your-project-id]\" # @param {type: \"string\", placeholder: \"[your-project-id]\", isTemplate: true}\n", + "# fmt: on\n", + "if not PROJECT_ID or PROJECT_ID == \"[your-project-id]\":\n", + " PROJECT_ID = str(os.environ.get(\"GOOGLE_CLOUD_PROJECT\"))\n", + "\n", + "LOCATION = os.environ.get(\"GOOGLE_CLOUD_REGION\", \"us-central1\")\n", + "\n", + "client = genai.Client(enterprise=True, project=PROJECT_ID, location=LOCATION)\n", + "\n", + "gemini_client = genai.Client(enterprise=True, project=PROJECT_ID, location=\"global\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "qD_bwA9hiMzL" + }, + "source": [ + "### Define a helper function to display media" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": { + "id": "GUrEwbvFiPhJ" + }, + "outputs": [], + "source": [ + "def show_video(video):\n", + " if isinstance(video, str):\n", + " file_name = video.split(\"/\")[-1]\n", + " !gcloud storage cp {video} {file_name}\n", + " display(Video(file_name, embed=True, width=600))\n", + " else:\n", + " with open(\"sample.mp4\", \"wb\") as out_file:\n", + " out_file.write(video)\n", + " display(Video(\"sample.mp4\", embed=True, width=600))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "2jaSOOadiUj6" + }, + "source": [ + "### Load the video generation model" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": { + "id": "APRfTklCiYR2" + }, + "outputs": [], + "source": [ + "video_model = \"veo-3.1-generate-001\"\n", + "video_model_fast = \"veo-3.1-fast-generate-001\"\n", + "video_model_lite = \"veo-3.1-lite-generate-001\"\n", + "gemini_model = \"gemini-3.5-flash\"" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "H_R1_Y76i4QB" + }, + "source": [ + "## Generate videos\n", + "Now, you'll generate videos from text and/or image prompts. You can get started with your own prompts or complete the next section to optimize your prompts with some established best practices.\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "v9ERu5b3EEdp" + }, + "source": [ + "### [Optional] Optimize your prompt: Text-to-video\n", + "\n", + "By considering the following options in your prompt, you can use Veo to create higher quality videos that more closely resemble your desired outcome. Learn more about advanced [prompting techniques for Veo 3.1](https://cloud.google.com/blog/products/ai-machine-learning/ultimate-prompting-guide-for-veo-3-1). To get started specify the following, or leave them as `None` if they don't align with your specific goals.\n", + "- **Subject:** The \"who\" or \"what\" of your video\n", + "- **Action:** Describe movements, interactions, etc.\n", + "- **Scene:** The \"where\" and \"when\" of your video\n", + "- **Camera angles:** The shot's viewpoint\n", + "- **Camera movements:** For a more cinematic/dynamic experience\n", + "- **Lens effects:** How the camera \"sees\" the world\n", + "- **Style:** The video's artistic filter\n", + "- **Temporal elements:** To imply changes in time\n", + "- **Audio:** Various sound effects or dialogue that guides the visuals through sound" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": { + "id": "yx7DKfqri3Vf" + }, + "outputs": [], + "source": [ + "subject = \"a detective\" # @param {type: 'string'}\n", + "action = \"interrogating a rubber duck\" # @param {type: 'string'}\n", + "scene = \"in a dark interview room\" # @param {type: 'string'}\n", + "\n", + "# fmt: off\n", + "camera_angle = \"Over-the-Shoulder Shot\" # @param [\"None\", \"Eye-Level Shot\", \"Low-Angle Shot\", \"High-Angle Shot\", \"Bird's-Eye View\", \"Top-Down Shot\", \"Worm's-Eye View\", \"Dutch Angle\", \"Canted Angle\", \"Close-Up\", \"Extreme Close-Up\", \"Medium Shot\", \"Full Shot\", \"Long Shot\", \"Wide Shot\", \"Establishing Shot\", \"Over-the-Shoulder Shot\", \"Point-of-View (POV) Shot\"]\n", + "camera_movement = \"Zoom (In)\" # @param [\"None\", \"Static Shot (or fixed)\", \"Pan (left)\", \"Pan (right)\", \"Tilt (up)\", \"Tilt (down)\", \"Dolly (In)\", \"Dolly (Out)\", \"Zoom (In)\", \"Zoom (Out)\", \"Truck (Left)\", \"Truck (Right)\", \"Pedestal (Up)\", \"Pedestal (Down)\", \"Crane Shot\", \"Aerial Shot\", \"Drone Shot\", \"Handheld\", \"Shaky Cam\", \"Whip Pan\", \"Arc Shot\"]\n", + "lens_effects = \"None\" # @param [\"None\", \"Wide-Angle Lens (e.g., 24mm)\", \"Telephoto Lens (e.g., 85mm)\", \"Shallow Depth of Field\", \"Bokeh\", \"Deep Depth of Field\", \"Lens Flare\", \"Rack Focus\", \"Fisheye Lens Effect\", \"Vertigo Effect (Dolly Zoom)\"]\n", + "style = \"Cinematic\" # @param [\"None\", \"Photorealistic\", \"Cinematic\", \"Vintage\", \"Japanese anime style\", \"Claymation style\", \"Stop-motion animation\", \"In the style of Van Gogh\", \"Surrealist painting\", \"Monochromatic black and white\", \"Vibrant and saturated\", \"Film noir style\", \"High-key lighting\", \"Low-key lighting\", \"Golden hour glow\", \"Volumetric lighting\", \"Backlighting to create a silhouette\"]\n", + "temporal_elements = \"None\" # @param [\"None\", \"Slow-motion\", \"Fast-paced action\", \"Time-lapse\", \"Hyperlapse\", \"Pulsating light\", \"Rhythmic movement\"]\n", + "\n", + "sound_effects = \"Ticking clock\" # @param [\"None\", \"Sound of a phone ringing\", \"Water splashing\", \"Soft house sounds\", \"Ticking clock\", \"City traffic and sirens\", \"Waves crashing\", \"Quiet office hum\"]\n", + "# fmt: on\n", + "dialogue = \"Where were you last night?\" # @param {type: 'string'}" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "PMFj078dLG0D" + }, + "source": [ + "Now, you'll use Gemini to take all of these keywords and combine them into a detailed Veo prompt." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "f3jvksummytE" + }, + "outputs": [], + "source": [ + "prompt = \"\"\n", + "\n", + "keywords = [subject, action, scene]\n", + "optional_keywords = [\n", + " camera_angle,\n", + " camera_movement,\n", + " lens_effects,\n", + " style,\n", + " temporal_elements,\n", + " sound_effects,\n", + "]\n", + "for keyword in optional_keywords:\n", + " if keyword != \"None\":\n", + " keywords.append(keyword)\n", + "if dialogue != \"\":\n", + " keywords.append(dialogue)\n", + "\n", + "gemini_prompt = f\"\"\"\n", + "You are an expert video prompt engineer for Google's Veo model. Your task is to construct the most effective and optimal prompt string using the following keywords. Every single keyword MUST be included. Synthesize them into a single, cohesive, and cinematic instruction. Do not add any new core concepts. Output ONLY the final prompt string, without any introduction or explanation. Mandatory Keywords: {\",\".join(keywords)}\n", + "\"\"\"\n", + "response = gemini_client.models.generate_content(\n", + " model=gemini_model,\n", + " contents=gemini_prompt,\n", + ")\n", + "\n", + "# Set Gemini's response in a prompt variable\n", + "prompt = response.text\n", + "display(Markdown(response.text))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "aDaTx8WCidRG" + }, + "source": [ + "### Generate videos from a text prompt\n", + "\n", + "With Veo 3.1, you have the option to generate videos from a text prompt. In order to generate a video in the following sample, specify the following info:\n", + "- **Prompt:** A detailed description of the video you would like to see. Only edit the prompt if you didn't generate a detailed prompt with Gemini in the previous section.\n", + "- **Audio generation:** Set `generate_audio` to True if you'd like audio to be included in the output video.\n", + "- **Aspect ratio:** 16:9 or 9:16.\n", + "- **Number of videos:** Set this value to 1 or 2.\n", + "- **Video duration:** 4, 6, or 8 seconds.\n", + "- **Resolution:** Can be 1080p, 720p or 4k.\n", + "\n", + "**NOTE:** Generating a video in 4k will introduce increased latency up to several minutes." + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": { + "id": "W_iqdJNGl2Sa" + }, + "outputs": [], + "source": [ + "if prompt == \"\":\n", + " # fmt: off\n", + " prompt = \"a garden gnome singing a pop song in a whimsical outdoor garden\" # @param {type: 'string'}\n", + "# fmt: on" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "tygfLLlWyTo_" + }, + "outputs": [], + "source": [ + "print(prompt)\n", + "\n", + "operation = client.models.generate_videos(\n", + " model=video_model,\n", + " prompt=prompt,\n", + " config=types.GenerateVideosConfig(\n", + " aspect_ratio=\"16:9\",\n", + " number_of_videos=1,\n", + " duration_seconds=6,\n", + " resolution=\"1080p\",\n", + " person_generation=\"allow_adult\",\n", + " generate_audio=True,\n", + " ),\n", + ")\n", + "\n", + "while not operation.done:\n", + " time.sleep(15)\n", + " operation = client.operations.get(operation)\n", + " print(operation)\n", + "\n", + "if operation.response:\n", + " show_video(operation.result.generated_videos[0].video.video_bytes)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "688nb6GEwqR4" + }, + "source": [ + "- **Veo 3.1 Fast:** For this next example, you'll use Veo 3.1 Fast. This model is a great option for use cases where latency is a priority.\n", + "- **File location:** In this request, the video will be stored in Cloud Storage once video generation is complete. Specify the bucket path where you would like this video to be stored in the `output_gcs` field.\n", + "- **Person generation:** When generating videos of people you can also set the `person_generation` parameter accordingly: `allow_adult`, `dont_allow`" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "sp0K0WYUwxLJ" + }, + "outputs": [], + "source": [ + "# fmt: off\n", + "prompt = \"sculpting a bowl on a pottery wheel while the person sculpting remarks: wow, I love this one\" # @param {type: 'string'}\n", + "# fmt: on\n", + "output_gcs = \"gs://[your-bucket-path]\" # @param {type: 'string'}\n", + "\n", + "\n", + "operation = client.models.generate_videos(\n", + " model=video_model_fast,\n", + " prompt=prompt,\n", + " config=types.GenerateVideosConfig(\n", + " aspect_ratio=\"9:16\",\n", + " output_gcs_uri=output_gcs,\n", + " number_of_videos=1,\n", + " duration_seconds=8,\n", + " resolution=\"720p\",\n", + " person_generation=\"allow_adult\",\n", + " generate_audio=True,\n", + " ),\n", + ")\n", + "\n", + "while not operation.done:\n", + " time.sleep(15)\n", + " operation = client.operations.get(operation)\n", + " print(operation)\n", + "\n", + "if operation.response:\n", + " show_video(operation.result.generated_videos[0].video.uri)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "8494d440c7d4" + }, + "source": [ + "- **Veo 3.1 Lite:** In the next example, you'll use Veo 3.1 Lite, which is ideal for rapid prototyping and iteration." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "dc95738712eb" + }, + "outputs": [], + "source": [ + "prompt = \"\"\"\n", + "A sleek, modern white swivel chair begins to rotate smoothly in a full 360-degree circle. As it spins, a vibrant purple glow gradually washes over the fabric, transitioning the chair from pure white to a deep royal purple by the time it completes its rotation.\n", + "\"\"\"\n", + "\n", + "operation = client.models.generate_videos(\n", + " model=video_model_lite,\n", + " prompt=prompt,\n", + " config=types.GenerateVideosConfig(\n", + " aspect_ratio=\"16:9\",\n", + " number_of_videos=1,\n", + " duration_seconds=8,\n", + " resolution=\"720p\",\n", + " person_generation=\"allow_adult\",\n", + " generate_audio=True,\n", + " ),\n", + ")\n", + "\n", + "while not operation.done:\n", + " time.sleep(10)\n", + " operation = client.operations.get(operation)\n", + " print(operation)\n", + "\n", + "if operation.response:\n", + " show_video(operation.result.generated_videos[0].video.video_bytes)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "YysYLyiVj8Zd" + }, + "source": [ + "### Generate videos from an image\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "X-NdoBONKpJD" + }, + "source": [ + "#### Download the starting image\n", + "\n", + "You can also generate a video by starting with an input image. In this example, you'll locally download an image that's stored in Google Cloud Storage. If you'd like, you can add the URL of the image you'd like to use to display it below. If you have a local image you'd like to use, you can specify that in the following steps." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "h_X5dS1Rye2Q" + }, + "outputs": [], + "source": [ + "!wget https://storage.googleapis.com/cloud-samples-data/generative-ai/image/flowers.png" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "WGtJsg-MPSXi" + }, + "source": [ + "If you'd like to use a different local image, modify the file name in `starting_image`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "cl_lTdvEIt8k" + }, + "outputs": [], + "source": [ + "starting_image = \"flowers.png\" # @param {type: 'string'}\n", + "\n", + "# Display the image\n", + "fig, axis = plt.subplots(1, 1, figsize=(12, 6))\n", + "axis.imshow(img.imread(starting_image))\n", + "axis.set_title(\"Starting Frame\")\n", + "axis.axis(\"off\")\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "FsHmRk-_NYya" + }, + "source": [ + "#### [Optional] Optimize your prompt: Image-to-video\n", + "\n", + "By considering the following options in your prompt, you can use Veo to create higher quality videos that more closely resemble your desired outcome. Learn more about advanced [prompting techniques for Veo 3.1](https://cloud.google.com/blog/products/ai-machine-learning/ultimate-prompting-guide-for-veo-3-1). To get started specify the following, or leave them as `None` if they don't align with your specific goals.\n", + "- **Camera motion:** The camera movement that occurs while the rest of the scene remains static\n", + "- **Subject animation:** Main character or object movement \n", + "- **Environmental animation:** Background or atmosphere movement\n", + "- **Audio:** Various sound effects or dialogue that guides the visuals through sound" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "6UE7AfftqSM5" + }, + "outputs": [], + "source": [ + "# fmt: off\n", + "camera_motion = \"Zoom (In)\" # @param [\"None\", \"Eye-Level Shot\", \"Low-Angle Shot\", \"High-Angle Shot\", \"Bird's-Eye View\", \"Top-Down Shot\", \"Worm's-Eye View\", \"Dutch Angle\", \"Canted Angle\", \"Close-Up\", \"Extreme Close-Up\", \"Medium Shot\", \"Full Shot\", \"Long Shot\", \"Wide Shot\", \"Establishing Shot\", \"Over-the-Shoulder Shot\", \"Point-of-View (POV) Shot\", \"Static Shot (or fixed)\", \"Pan (left)\", \"Pan (right)\", \"Tilt (up)\", \"Tilt (down)\", \"Dolly (In)\", \"Dolly (Out)\", \"Zoom (In)\", \"Zoom (Out)\", \"Truck (Left)\", \"Truck (Right)\", \"Pedestal (Up)\", \"Pedestal (Down)\", \"Crane Shot\", \"Aerial Shot\", \"Drone Shot\", \"Handheld\", \"Shaky Cam\", \"Whip Pan\", \"Arc Shot\", \"Wide-Angle Lens (e.g., 24mm)\", \"Telephoto Lens (e.g., 85mm)\", \"Shallow Depth of Field\", \"Bokeh\", \"Deep Depth of Field\", \"Lens Flare\", \"Rack Focus\", \"Fisheye Lens Effect\", \"Vertigo Effect (Dolly Zoom)\"]\n", + "\n", + "subject_animation = \"None\" # @param [\"None\", \"The subject's head turns slowly\", \"The subject blinks slowly\", \"The subject's hair and clothes flutter gently in the wind\", \"A subtle smile appears on the subject's face\"]\n", + "environmental_animation = \"Light changes subtly\" # @param [\"None\", \"Fog rolls in slowly\", \"Rain starts to fall gently\", \"Leaves rustle in the wind\", \"Light changes subtly\", \"Reflections move on water\"]\n", + "\n", + "sound_effects = \"None\" # @param [\"None\", \"Sound of a phone ringing\", \"Water splashing\", \"Soft house sounds\", \"Ticking clock\", \"City traffic and sirens\", \"Waves crashing\", \"Quiet office hum\"]\n", + "# fmt: on\n", + "dialogue = \"\" # @param {type: 'string'}" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "nG4tknxOR70c" + }, + "source": [ + "Now, you'll use Gemini to take all of these keywords and combine them into a detailed Veo prompt." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "a1NvfHEHrx2m" + }, + "outputs": [], + "source": [ + "prompt = \"\"\n", + "\n", + "keywords = []\n", + "optional_keywords = [\n", + " camera_motion,\n", + " subject_animation,\n", + " environmental_animation,\n", + " sound_effects,\n", + "]\n", + "for keyword in optional_keywords:\n", + " if keyword != \"None\":\n", + " keywords.append(keyword)\n", + "if dialogue != \"\":\n", + " keywords.append(dialogue)\n", + "\n", + "gemini_prompt = f\"\"\"\n", + "You are an expert prompt engineer for Google's Veo model. Analyze the provided image and combine its content with the following motion and audio keywords to generate a single, cohesive, and cinematic prompt. Integrate the image's subject and scene with the requested motion and audio effects. The final output must be ONLY the prompt itself, with no preamble. Mandatory Keywords: {\",\".join(keywords)}\n", + "\"\"\"\n", + "with open(starting_image, \"rb\") as f:\n", + " image = f.read()\n", + "\n", + "response = gemini_client.models.generate_content(\n", + " model=gemini_model,\n", + " contents=[gemini_prompt, types.Part.from_bytes(data=image, mime_type=\"image/png\")],\n", + ")\n", + "\n", + "# Set Gemini's response in a prompt variable\n", + "prompt = response.text\n", + "display(Markdown(response.text))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "mCc77j59KjGN" + }, + "source": [ + "#### Send the video request\n", + "\n", + "If you're generating a video from an image you don't need to provide a prompt. The model will simply add motion to your image; however, a detailed prompt will help with video quality.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "thPYb0eNtwxN" + }, + "outputs": [], + "source": [ + "if prompt == \"\":\n", + " # fmt: off\n", + " prompt = \"zoom out of the flower field, play whimsical music\" # @param {type: 'string'}\n", + "# fmt: on" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "tFgQYeXHnidx" + }, + "outputs": [], + "source": [ + "print(prompt)\n", + "\n", + "operation = client.models.generate_videos(\n", + " model=video_model,\n", + " prompt=prompt,\n", + " image=types.Image.from_file(location=starting_image),\n", + " config=types.GenerateVideosConfig(\n", + " aspect_ratio=\"16:9\",\n", + " number_of_videos=1,\n", + " duration_seconds=6,\n", + " resolution=\"1080p\",\n", + " person_generation=\"allow_adult\",\n", + " generate_audio=True,\n", + " ),\n", + ")\n", + "\n", + "while not operation.done:\n", + " time.sleep(15)\n", + " operation = client.operations.get(operation)\n", + " print(operation)\n", + "\n", + "if operation.response:\n", + " show_video(operation.result.generated_videos[0].video.video_bytes)" + ] + } + ], + "metadata": { + "colab": { + "name": "veo3_video_generation.ipynb", + "toc_visible": true + }, + "kernelspec": { + "display_name": "Python 3", + "name": "python3" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} diff --git a/vision/getting-started/virtual_try_on.ipynb b/vision/getting-started/virtual_try_on.ipynb new file mode 100644 index 0000000..584251c --- /dev/null +++ b/vision/getting-started/virtual_try_on.ipynb @@ -0,0 +1,682 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "uxCkB_DXTHzf" + }, + "outputs": [], + "source": [ + "# Copyright 2025 Google LLC\n", + "#\n", + "# Licensed under the Apache License, Version 2.0 (the \"License\");\n", + "# you may not use this file except in compliance with the License.\n", + "# You may obtain a copy of the License at\n", + "#\n", + "# https://www.apache.org/licenses/LICENSE-2.0\n", + "#\n", + "# Unless required by applicable law or agreed to in writing, software\n", + "# distributed under the License is distributed on an \"AS IS\" BASIS,\n", + "# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n", + "# See the License for the specific language governing permissions and\n", + "# limitations under the License." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "Hny4I-ODTIS6" + }, + "source": [ + "# Virtual Try-On: Image Generation\n", + "\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + "
    \n", + " \n", + " \"Google
    Open in Colab\n", + "
    \n", + "
    \n", + " \n", + " \"Google
    Open in Colab Enterprise\n", + "
    \n", + "
    \n", + " \n", + " \"Workbench
    Open in Workbench\n", + "
    \n", + "
    \n", + " \n", + " \"GitHub
    View on GitHub\n", + "
    \n", + "
    \n", + "\n", + "
    \n", + "\n", + "

    \n", + "Share to:\n", + "\n", + "\n", + " \"LinkedIn\n", + "\n", + "\n", + "\n", + " \"Bluesky\n", + "\n", + "\n", + "\n", + " \"X\n", + "\n", + "\n", + "\n", + " \"Reddit\n", + "\n", + "\n", + "\n", + " \"Facebook\n", + "\n", + "

    " + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "EXljS1-Ib0ue" + }, + "source": [ + "| Authors |\n", + "| --- |\n", + "| [Jorj Ismailyan](https://github.com/jismailyan-google) |\n", + "| [Katie Nguyen](https://github.com/katiemn) |" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "-nLS57E2TO5y" + }, + "source": [ + "## Overview\n", + "\n", + "### Virtual Try-On\n", + "\n", + "[Virtual Try-On](https://cloud.google.com/vertex-ai/generative-ai/docs/image/generate-virtual-try-on-images) uses Google's cutting-edge image generation models to create high-quality images of people virtually trying on clothes. By providing an image of a model and a clothing item, you can generate a new image of the model wearing that product.\n", + "\n", + "In this tutorial, you will learn how to use the Google Gen AI SDK for Python to interact with the Virtual Try-On model to:\n", + "- Try-on multiple clothing items from locally stored images\n", + "- Try-on a clothing item in Cloud Storage with an Imagen generated person\n", + "\n", + "Learn more about [quotas](https://cloud.google.com/vertex-ai/generative-ai/docs/models/imagen/virtual-try-on-preview-08-04) and [pricing](https://cloud.google.com/vertex-ai/generative-ai/pricing#imagen-models) for Virtual Try-On in the product documentation." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "mvKl-BtQTRiQ" + }, + "source": [ + "## Get started" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "B-UOCMvJdmlq" + }, + "source": [ + "### Install Google Gen AI SDK for Python" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "u5lOntr-doIT" + }, + "outputs": [], + "source": [ + "%pip install --upgrade --quiet google-genai" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "opUxT_k5TdgP" + }, + "source": [ + "### Authenticate your notebook environment (Colab only)\n", + "\n", + "If you are running this notebook on Google Colab, run the following cell to authenticate your environment." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "vbNgv4q1T2Mi" + }, + "outputs": [], + "source": [ + "import sys\n", + "\n", + "if \"google.colab\" in sys.modules:\n", + " from google.colab import auth\n", + "\n", + " auth.authenticate_user()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "Sh5eUOaLsJ46" + }, + "source": [ + "### Import libraries" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "ybBXSukZkgjg" + }, + "source": [ + "### Set Google Cloud project information\n", + "\n", + "To get started using Agent Platform, you must have an existing Google Cloud project and [enable the Agent Platform API](https://console.cloud.google.com/flows/enableapi?apiid=aiplatform.googleapis.com).\n", + "\n", + "Learn more about [setting up a project](https://docs.cloud.google.com/resource-manager/docs/creating-managing-projects) and a [development environment](https://cloud.google.com/docs/authentication/set-up-adc-local-dev-environment)." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": { + "id": "q7YvbXXdtzDT" + }, + "outputs": [], + "source": [ + "import os\n", + "import typing\n", + "import urllib.request\n", + "\n", + "import IPython.display\n", + "import matplotlib.image as img\n", + "import matplotlib.pyplot as plt\n", + "import numpy as np\n", + "from PIL import Image as PIL_Image\n", + "from PIL import ImageOps as PIL_ImageOps\n", + "from google import genai\n", + "from google.genai.types import (\n", + " GenerateImagesConfig,\n", + " Image,\n", + " ProductImage,\n", + " RecontextImageConfig,\n", + " RecontextImageSource,\n", + ")\n", + "\n", + "# fmt: off\n", + "PROJECT_ID = \"[your-project-id]\" # @param {type: \"string\", placeholder: \"[your-project-id]\", isTemplate: true}\n", + "# fmt: on\n", + "if not PROJECT_ID or PROJECT_ID == \"[your-project-id]\":\n", + " PROJECT_ID = str(os.environ.get(\"GOOGLE_CLOUD_PROJECT\"))\n", + "\n", + "LOCATION = os.environ.get(\"GOOGLE_CLOUD_REGION\", \"us-central1\")\n", + "\n", + "client = genai.Client(enterprise=True, project=PROJECT_ID, location=LOCATION)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "ic4fVqs8skdA" + }, + "source": [ + "### Define helper functions to display media" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": { + "id": "hk8bfm7Xsvg8" + }, + "outputs": [], + "source": [ + "def display_image(\n", + " image,\n", + " max_width: int = 700,\n", + " max_height: int = 400,\n", + ") -> None:\n", + " pil_image = typing.cast(PIL_Image.Image, image._pil_image)\n", + " if pil_image.mode != \"RGB\":\n", + " # RGB is supported by all Jupyter environments (e.g. RGBA is not yet)\n", + " pil_image = pil_image.convert(\"RGB\")\n", + " image_width, image_height = pil_image.size\n", + " if max_width < image_width or max_height < image_height:\n", + " # Resize to display a smaller notebook image\n", + " pil_image = PIL_ImageOps.contain(pil_image, (max_width, max_height))\n", + " IPython.display.display(pil_image)\n", + "\n", + "\n", + "def display_local_image(\n", + " images: list[str],\n", + ") -> None:\n", + " fig, axes = plt.subplots(1, len(images), figsize=(12, 6))\n", + " if len(images) == 1:\n", + " axes = np.array([axes])\n", + " for i, ax in enumerate(axes):\n", + " image = img.imread(images[i])\n", + " ax.imshow(image)\n", + " ax.axis(\"off\")\n", + " plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "qnEcP0UUsy-_" + }, + "source": [ + "### Load the image models" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": { + "id": "w_TfEzggs2fp" + }, + "outputs": [], + "source": [ + "virtual_try_on = \"virtual-try-on-001\"\n", + "image_generation = \"imagen-4.0-generate-001\"" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "fU32286ooc8Q" + }, + "source": [ + "## Virtual Try-On with local files\n", + "\n", + "In this section, you'll download images of a person and clothing items to try them on from local files.\n", + "\n", + "Supported Clothing:\n", + " - **Tops:** shirts, hoodies, sweaters, tank tops, blouses\n", + " - **Bottoms:** pants, leggings, shorts, skirts\n", + " - **Other:** shoes, full body items\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "faTZezkUs7KP" + }, + "source": [ + "### Download an image of a person\n", + "\n", + "First, download an image of a person. The following example uses an image from Cloud Storage. If you prefer to use a different image, you can either change the URL in the `wget` command or, if you have a local file, update the `person_image` variable in the subsequent step." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "19kR3OR7Fwgv" + }, + "outputs": [], + "source": [ + "!wget https://storage.googleapis.com/cloud-samples-data/generative-ai/image/man-in-field.png" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "p04U4s73tRe2" + }, + "source": [ + "If you'd like to use a different local image, modify the file name in `person_image`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "Spzul-ObF00N" + }, + "outputs": [], + "source": [ + "person_image = \"man-in-field.png\" # @param {type: 'string'}\n", + "\n", + "display_local_image([person_image])" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "SRgtZr0YteN2" + }, + "source": [ + "### Download clothing images\n", + "\n", + "Next, download the clothing images. The examples below are stored in Cloud Storage, but you can use your own by modifying the URLs or by specifying the paths to local images in the next step." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "b5HxMgDpMQta" + }, + "outputs": [], + "source": [ + "!wget https://storage.googleapis.com/cloud-samples-data/generative-ai/image/sweater.jpg\n", + "\n", + "!wget https://storage.googleapis.com/cloud-samples-data/generative-ai/image/trousers.jpg" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "mm-jYPq5t7yN" + }, + "source": [ + "If you'd like to use a different top, modify the file name in `top_image`. The same goes for the file name in `bottom_image`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "IY0LHYHfMjkK" + }, + "outputs": [], + "source": [ + "top_image = \"sweater.jpg\" # @param {type: 'string'}\n", + "bottom_image = \"trousers.jpg\" # @param {type: 'string'}\n", + "\n", + "display_local_image([top_image, bottom_image])" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "mTgQo5Hauk92" + }, + "source": [ + "### Send the request\n", + "\n", + "With the Virtual Try-On model, you can only specify one clothing item to try on at a time. Since this example has two clothing items, you'll need to make two separate requests. In each call, you can specify the following parameters in addition to the `person_image` and `product_images`:\n", + " - **Number of images:** 1 - 4\n", + "\n", + "You'll save the output image locally so that it can be referenced in the next step." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "RHnSHJl-t8ud" + }, + "outputs": [], + "source": [ + "response = client.models.recontext_image(\n", + " model=virtual_try_on,\n", + " source=RecontextImageSource(\n", + " person_image=Image.from_file(location=person_image),\n", + " product_images=[\n", + " ProductImage(product_image=Image.from_file(location=top_image))\n", + " ],\n", + " ),\n", + " config=RecontextImageConfig(\n", + " output_mime_type=\"image/jpeg\",\n", + " number_of_images=1,\n", + " safety_filter_level=\"BLOCK_LOW_AND_ABOVE\",\n", + " ),\n", + ")\n", + "\n", + "response.generated_images[0].image.save(\"try-on.jpeg\")\n", + "display_image(response.generated_images[0].image)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "cP_gkDUVx-3Z" + }, + "source": [ + "When generating images you can also set the `safety_filter_level` parameter accordingly:\n", + "- `safety_filter_level`\n", + " - `BLOCK_LOW_AND_ABOVE`\n", + " - `BLOCK_MEDIUM_AND_ABOVE`\n", + " - `BLOCK_ONLY_HIGH`\n", + " - `BLOCK_NONE`" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "JKtLpyrZ3wA5" + }, + "outputs": [], + "source": [ + "response = client.models.recontext_image(\n", + " model=virtual_try_on,\n", + " source=RecontextImageSource(\n", + " person_image=Image.from_file(location=\"try-on.jpeg\"),\n", + " product_images=[\n", + " ProductImage(product_image=Image.from_file(location=bottom_image))\n", + " ],\n", + " ),\n", + " config=RecontextImageConfig(\n", + " output_mime_type=\"image/jpeg\",\n", + " number_of_images=1,\n", + " safety_filter_level=\"BLOCK_LOW_AND_ABOVE\",\n", + " ),\n", + ")\n", + "display_image(response.generated_images[0].image)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "TmLGH9LAd371" + }, + "source": [ + "### Try-on multiple items at once\n", + "\n", + "By combining multiple clothing items into one photo, you can have the model try-on multiple clothing items in one request. You'll start by downloading and displaying a new model image and outfit image for this example. Like before, you can use your own images by modifying the URLs." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "na3_ELDnefNI" + }, + "outputs": [], + "source": [ + "!wget https://storage.googleapis.com/cloud-samples-data/generative-ai/image/woman.jpeg\n", + "\n", + "!wget https://storage.googleapis.com/cloud-samples-data/generative-ai/image/outfit.png\n", + "\n", + "person_image = \"woman.jpeg\" # @param {type: 'string'}\n", + "outfit_image = \"outfit.png\" # @param {type: 'string'}\n", + "\n", + "display_local_image([person_image, outfit_image])" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "xMKIaRsWgxSw" + }, + "source": [ + "Now, you'll send the request." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "bhJhmUraWngz" + }, + "outputs": [], + "source": [ + "response = client.models.recontext_image(\n", + " model=virtual_try_on,\n", + " source=RecontextImageSource(\n", + " person_image=Image.from_file(location=person_image),\n", + " product_images=[\n", + " ProductImage(product_image=Image.from_file(location=outfit_image))\n", + " ],\n", + " ),\n", + " config=RecontextImageConfig(\n", + " output_mime_type=\"image/png\",\n", + " number_of_images=1,\n", + " safety_filter_level=\"BLOCK_LOW_AND_ABOVE\",\n", + " ),\n", + ")\n", + "display_image(response.generated_images[0].image)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "mBLJtICO8iMQ" + }, + "source": [ + "## Virtual Try-On with files in Cloud Storage\n", + "\n", + "In this section, you'll use images of a clothing item stored in Cloud Storage and a person generated with Imagen for virtual try-on." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "08ZEbEDgyyCB" + }, + "source": [ + "### Generate an image of a person\n", + "\n", + "In this example, you'll generate a person to try on the clothing item with Imagen. Run the step below and change the prompt if you see fit." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "I9caIrZ7Dek1" + }, + "outputs": [], + "source": [ + "prompt = \"\"\"\n", + "A high-resolution, full-body, head-on photograph of a woman standing in a photography studio wearing jeans and a white t-shirt. The backdrop is clean and neutral.\n", + "\"\"\"\n", + "\n", + "image = client.models.generate_images(\n", + " model=image_generation,\n", + " prompt=prompt,\n", + " config=GenerateImagesConfig(\n", + " output_mime_type=\"image/jpeg\",\n", + " number_of_images=1,\n", + " image_size=\"2K\",\n", + " safety_filter_level=\"BLOCK_MEDIUM_AND_ABOVE\",\n", + " ),\n", + ")\n", + "display_image(image.generated_images[0].image)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "l2p7TJtXzhpM" + }, + "source": [ + "### View clothing image\n", + "\n", + "Here, you'll view the clothing image that is stored in Cloud Storage. If you'd like, you can modify the URL below to use a different image in Cloud Storage." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "qj70GAQjfCPC" + }, + "outputs": [], + "source": [ + "clothing_image = PIL_Image.open(\n", + " urllib.request.urlopen(\n", + " \"https://storage.googleapis.com/cloud-samples-data/generative-ai/image/dress.jpg\"\n", + " )\n", + ")\n", + "\n", + "# Display the image\n", + "fig, axis = plt.subplots(1, 2, figsize=(12, 6))\n", + "axis[0].imshow(clothing_image)\n", + "for ax in axis:\n", + " ax.axis(\"off\")\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "P-yAQ2rt_pll" + }, + "source": [ + "### Send the request\n", + "\n", + "By default, a digital watermark, or [SynthID](https://deepmind.google/technologies/synthid/), is added to images. If you would like to explicitly set the watermark to True, you can do so with the `add_watermark` parameter." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "c9N8l0oo_cWs" + }, + "outputs": [], + "source": [ + "response = client.models.recontext_image(\n", + " model=virtual_try_on,\n", + " source=RecontextImageSource(\n", + " person_image=image.generated_images[0].image,\n", + " product_images=[\n", + " ProductImage(\n", + " product_image=Image(\n", + " gcs_uri=\"gs://cloud-samples-data/generative-ai/image/dress.jpg\"\n", + " )\n", + " )\n", + " ],\n", + " ),\n", + " config=RecontextImageConfig(\n", + " output_mime_type=\"image/jpeg\",\n", + " number_of_images=1,\n", + " safety_filter_level=\"BLOCK_LOW_AND_ABOVE\",\n", + " ),\n", + ")\n", + "display_image(response.generated_images[0].image)" + ] + } + ], + "metadata": { + "colab": { + "name": "virtual_try_on.ipynb", + "toc_visible": true + }, + "kernelspec": { + "display_name": "Python 3", + "name": "python3" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} diff --git a/vision/sample-apps/V-Start/.env.example b/vision/sample-apps/V-Start/.env.example new file mode 100644 index 0000000..ba11a90 --- /dev/null +++ b/vision/sample-apps/V-Start/.env.example @@ -0,0 +1,3 @@ +# .env.example +API_KEY="your_gemini_api_key_here" +PORT=8080 diff --git a/vision/sample-apps/V-Start/.gitignore b/vision/sample-apps/V-Start/.gitignore new file mode 100644 index 0000000..350a9f1 --- /dev/null +++ b/vision/sample-apps/V-Start/.gitignore @@ -0,0 +1,3 @@ +/node_modules + +.env \ No newline at end of file diff --git a/vision/sample-apps/V-Start/CONTRIBUTING.md b/vision/sample-apps/V-Start/CONTRIBUTING.md new file mode 100644 index 0000000..26f0793 --- /dev/null +++ b/vision/sample-apps/V-Start/CONTRIBUTING.md @@ -0,0 +1,36 @@ +# Contributing to V-Start + +First off, thank you for considering contributing to V-Start! We welcome any help, whether it's reporting a bug, suggesting a new feature, or writing code. + +## How Can I Contribute? + +### 🐛 Reporting Bugs + +If you find a bug, please open an issue on our GitHub repository. When you report a bug, please include: + +* **A clear and descriptive title.** +* **Steps to reproduce the bug.** Be as specific as possible. +* **What you expected to happen** versus what actually happened. +* **Screenshots or screen recordings** if they help illustrate the issue. + +### ✹ Suggesting Enhancements + +If you have an idea for a new feature or an improvement to an existing one, please open an issue to start a discussion. This allows us to coordinate our efforts and prevent duplication of work. + +Please provide: + +* **A clear and descriptive title.** +* **A detailed description of the proposed enhancement** and why you think it would be valuable. +* **Any mockups or examples** that might help explain your idea. + +### 🔧 Pull Requests + +We are happy to review pull requests! If you'd like to contribute code, please follow these steps: + +1. **Fork the repository** and create your branch from `main`. +2. **Make your changes.** Please ensure your code follows the existing style of the project. +3. **Add comments** to any complex or non-obvious parts of your code. +4. **Ensure your code lints** without any errors. +5. **Open a pull request** with a clear title and a detailed description of the changes you've made. + +Thank you for helping make V-Start better! \ No newline at end of file diff --git a/vision/sample-apps/V-Start/Dockerfile b/vision/sample-apps/V-Start/Dockerfile new file mode 100644 index 0000000..fa007a2 --- /dev/null +++ b/vision/sample-apps/V-Start/Dockerfile @@ -0,0 +1,21 @@ +# Use an official Node.js image. The 'lts' (Long Term Support) version is stable. +FROM node:18-alpine + +# Set the working directory inside the container to /app +WORKDIR /app + +# Copy package.json and package-lock.json first to leverage Docker's caching. +# This makes subsequent builds faster if you only change your source code. +COPY package*.json ./ + +# Install all project dependencies +RUN npm install + +# Copy the rest of your application's source code into the container +COPY . . + +# Expose port 8080 to the outside world +EXPOSE 8080 + +# The command to run when the container starts +CMD [ "node", "server.js" ] diff --git a/vision/sample-apps/V-Start/LICENSE b/vision/sample-apps/V-Start/LICENSE new file mode 100644 index 0000000..59711bc --- /dev/null +++ b/vision/sample-apps/V-Start/LICENSE @@ -0,0 +1,51 @@ +Apache License +Version 2.0, January 2004 +http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + +"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. + +"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. + +"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + +"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. + +"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. + +"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. + +"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). + +"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. + +"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." + +"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: + +You must give any other recipients of the Work or Derivative Works a copy of this License; and +You must cause any modified files to carry prominent notices stating that You changed the files; and +You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and +If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. +You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS \ No newline at end of file diff --git a/vision/sample-apps/V-Start/README.md b/vision/sample-apps/V-Start/README.md new file mode 100644 index 0000000..0000c92 --- /dev/null +++ b/vision/sample-apps/V-Start/README.md @@ -0,0 +1,231 @@ +# V-Start: A Toolkit for Veo Prompting and Evaluation + +[![GitHub license](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](https://github.com/GoogleCloudPlatform/generative-ai/blob/main/vision/sample-apps/V-Start/LICENSE) + +**Author: [Wafae Bakkali](https://github.com/WafaeBakkali)** + +V-Start is an experimental toolkit that helps users easily and quickly create effective prompts for Veo and evaluate how well generated videos align with their intended prompts. The main goal is to simplify the process of creating high-quality videos with Veo. + +V-Start Application Screenshot + +## ✹ Features + +V-Start is divided into two main categories: Prompting and Evaluation. + +### Prompting Tools +* **Prompt Generator**: Generate expert-level prompts based on [the ideal structure for Veo](https://medium.com/google-cloud/veo-3-a-detailed-prompting-guide-867985b46018), with support for both Text-to-Video and Image-to-Video and options for short or long outputs. For Image-to-Video, a base description is automatically generated from the uploaded image, which can then be customized. +* **Prompt Enhancer**: Improve an existing prompt by leveraging Gemini to enhance its cinematic detail and effectiveness. +* **Prompt Converter**: Convert prompts between different formats, such as Plain Text, JSON, YAML or XML. +* **Timeline Prompting**: Create multi-shot scenes by sequencing multiple prompts, defining the start and end times for each action to build a detailed narrative. +* **Gallery**: Explore a curated library of high-quality video examples and copy their prompts for inspiration. + +### Evaluation Tools +* **Alignment Eval**: An autorater that provides an objective score (0-100%) of how well a video matches its prompt. You can evaluate a single prompt-video pair or process multiple pairs in bulk by pasting data directly into the tool or uploading a CSV file from your local machine. The tool works by breaking the prompt into sub-questions, and Gemini uses its Visual Question Answering (VQA) capabilities to score the video's alignment. All results can be stored for further analysis. +* **Side-by-Side Comparison**: Compare videos side-by-side to gather human feedback. Participate in existing studies (like prompt format evaluation) or create your own for qualitative evaluation. Results can be stored for further analysis. + +## đŸ› ïž Tech Stack + +* **Backend**: Node.js with Express.js +* **Frontend**: HTML, CSS, and modern vanilla JavaScript (ES modules) +* **Styling**: Tailwind CSS (via CDN) with a custom dark mode theme. +* **Core AI**: Google Gemini API (specifically gemini-2.5-pro) +* **Deployment**: The application also includes a Dockerfile for containerization. + +## 📂 Project Structure + +The repository is organized as follows: + +``` +/ +├── .env.example # Example environment file for new contributors +├── .gitignore # Specifies files to be ignored by Git +├── CONTRIBUTING.md # Guidelines for contributing to the project +├── Dockerfile # Defines the Docker container for the application +├── index.html # The main HTML file for the single-page application +├── LICENSE # The Apache 2.0 open-source license for the project +├── package.json # Lists project dependencies and scripts +├── package-lock.json # Records exact versions of dependencies +├── README.md # The project's readme file +├── server.js # The Node.js/Express backend server +├── style.css # Main stylesheet for the application +│ +├── data/ # Contains static data and assets +│ ├── V-Start.png # Screenshot of the application UI +│ └── veo-youtube-study.json # Data for the A/B evaluation study +│ +└── src/ # Contains all frontend JavaScript source code + ├── api.js # Handles the fetch call to the backend Gemini API + ├── main.js # The main entry point for the frontend application logic + ├── ui.js # Contains UI helper functions (e.g., notifications, toast: + │ + ├── features/ # Each file represents a major feature/tab in the UI + │ ├── alignment-eval.js + │ ├── converter.js + │ ├── enhancer.js + │ ├── eval.js + │ ├── gallery.js + │ ├── generator.js + │ └── timeline.js + │ + └── templates/ # Contains the HTML templates for each feature + ├── alignment-eval.html + ├── converter.html + ├── enhancer.html + ├── eval.html + ├── gallery.html + ├── generator.html + └── timeline.html +``` + +## Authentication Setup + +V-Start supports two authentication methods for flexibility: + +### Method 1: Google Cloud Access Token + +This method uses your own Google Cloud Project. + +1. **Set up a Google Cloud Project:** + - Go to the [Google Cloud Console](https://console.cloud.google.com/) + - Create a new project or select an existing one + - Enable billing for your project + +2. **Enable required APIs:** + ```bash + # Set your project ID + export PROJECT_ID="your-gcp-project-id" + gcloud config set project $PROJECT_ID + + # Enable Vertex AI API + gcloud services enable aiplatform.googleapis.com + ``` + +3. **Install and authenticate gcloud CLI:** + - Install the [Google Cloud SDK](https://cloud.google.com/sdk/docs/install) + - Authenticate with your Google account: + ```bash + gcloud auth login + ``` + +4. **Get your access token:** + ```bash + gcloud auth print-access-token + ``` + **Note:** Access tokens expire after 1 hour. You'll need to run this command again to get a new token when it expires. + +5. **Use in V-Start:** + - In the V-Start UI, select "gcloud Access Token" as your authentication method + - Enter your Project ID and the access token from step 4 + +### Method 2: Google AI Studio API Key + +1. **Get your API Key:** + - Go to [Google AI Studio](https://aistudio.google.com/app/apikey) + - Sign in with your Google account + - Click "Create API Key" + - Choose "Create API key in new project" or select an existing project + - Copy the generated API key + +2. **Configure the application:** + - Add the API key to your .env file (see Installation section below) + - In the V-Start UI, select "API Key" as your authentication method + +## 🚀 Getting Started (Local Development) + +Follow these instructions to get a copy of the project up and running on your local machine. + +### Prerequisites + +* [Node.js](https://nodejs.org/) (v18 or later recommended) +* npm (usually comes with Node.js) +* Authentication setup (see Authentication Setup section above) + +### Installation + +1. **Clone the repository:** + ```bash + git clone https://github.com/GoogleCloudPlatform/generative-ai.git + cd vision/sample-apps/V-Start + ``` + +2. **Install NPM packages:** + ```bash + npm install + ``` + +3. **Set up your environment variables:** + Create a file named `.env` in the root of the project by copying the example file. + ```bash + cp .env.example .env + ``` + Open the `.env` file and add your Gemini API Key (if using Method 2): + ``` + API_KEY=your_gemini_api_key_here + ``` + **Note:** If you're only using the Access Token method, you can leave the API_KEY empty. + +4. **Run the server:** + ```bash + npm start + ``` + +5. Open your browser and navigate to `http://localhost:8080`. + +## ☁ Deployment to Cloud Run + +The recommended way to deploy this application is directly from source to Google Cloud Run, secured with Identity-Aware Proxy (IAP). When you deploy from source, Cloud Build automatically uses the Dockerfile in your repository to build and deploy your container. + +### Prerequisites + +* A Google Cloud Project with billing enabled. +* The [Google Cloud SDK](https://cloud.google.com/sdk/docs/install) (gcloud CLI) installed and authenticated. +* Project ID already configured (see Authentication Setup section above) + +### Step 1: Enable Required Services + +```bash +# Enable required services (using the PROJECT_ID from Authentication Setup) +gcloud services enable run.googleapis.com cloudbuild.googleapis.com artifactregistry.googleapis.com secretmanager.googleapis.com iap.googleapis.com +``` + +### Step 2: Secure Your API Key + +Store your Gemini API key in Secret Manager. + +```bash +# Create the secret +gcloud secrets create gemini-api-key --replication-policy="automatic" + +# Add your API key value to the secret +printf "your_gemini_api_key_here" | gcloud secrets versions add gemini-api-key --data-file=- +``` + +### Step 3: Configure OAuth Consent Screen + +This is required for IAP. In the Google Cloud Console, navigate to **APIs & Services → OAuth consent screen** and complete the setup wizard. + +### Step 4: Deploy the Service + +Deploy the application as a private service. + +```bash +gcloud run deploy veo-start-app \ + --source . \ + --region us-central1 \ + --no-allow-unauthenticated \ + --set-env-vars="API_KEY=sm://${PROJECT_ID}/gemini-api-key/latest" +``` + +### Step 5: Grant Access Permissions + +After deploying, make sure to enforce IAP by granting access permissions to authorized users or groups. For detailed instructions, please follow the official documentation. + +**Official Guide**: [Securing Cloud Run services with IAP](https://cloud.google.com/iap/docs/enabling-cloud-run) + +## License + +This project is licensed under the Apache License, Version 2.0. See the [LICENSE](https://github.com/GoogleCloudPlatform/generative-ai/blob/main/vision/sample-apps/V-Start/LICENSE) file for the full license text. + +## đŸ€ Contributing + +Contributions are welcome! Please see the [CONTRIBUTING.md](CONTRIBUTING.md) file for guidelines on how to report bugs, suggest enhancements, or submit pull requests. \ No newline at end of file diff --git a/vision/sample-apps/V-Start/data/V-Start.png b/vision/sample-apps/V-Start/data/V-Start.png new file mode 100644 index 0000000..686ea0f Binary files /dev/null and b/vision/sample-apps/V-Start/data/V-Start.png differ diff --git a/vision/sample-apps/V-Start/data/veo-youtube-study.json b/vision/sample-apps/V-Start/data/veo-youtube-study.json new file mode 100644 index 0000000..1d2c755 --- /dev/null +++ b/vision/sample-apps/V-Start/data/veo-youtube-study.json @@ -0,0 +1,82 @@ +[ + { + "videoA": "https://www.youtube.com/embed/8LZRFk1Oq9c", + "videoB": "https://www.youtube.com/embed/5_i8c0DUvPs" + }, + { + "videoA": "https://www.youtube.com/embed/0uvpkAWilUg", + "videoB": "https://www.youtube.com/embed/a9bGjdPy-II" + }, + { + "videoA": "https://www.youtube.com/embed/n1-gxRdnMds", + "videoB": "https://www.youtube.com/embed/Mhl6DXFFmns" + }, + { + "videoA": "https://www.youtube.com/embed/e9Ocpv1kaGI", + "videoB": "https://www.youtube.com/embed/xs-cgvK0lXs" + }, + { + "videoA": "https://www.youtube.com/embed/c_nWZzdmfBw", + "videoB": "https://www.youtube.com/embed/57fwHRa1ysw" + }, + { + "videoA": "https://www.youtube.com/embed/5RD_XTVc78U", + "videoB": "https://www.youtube.com/embed/NClZ6dHOOcM" + }, + { + "videoA": "https://www.youtube.com/embed/nas9uhTao6g", + "videoB": "https://www.youtube.com/embed/T9JfAyFsFME" + }, + { + "videoA": "https://www.youtube.com/embed/HIZjgwmGHtc", + "videoB": "https://www.youtube.com/embed/rrv22qr_4Tk" + }, + { + "videoA": "https://www.youtube.com/embed/W4nIwf6hWNg", + "videoB": "https://www.youtube.com/embed/D_Jw_wfLpbg" + }, + { + "videoA": "https://www.youtube.com/embed/-olV0cJrFhQ", + "videoB": "https://www.youtube.com/embed/WvAiE4PWdxc" + }, + { + "videoA": "https://www.youtube.com/embed/2C19bcbf4pY", + "videoB": "https://www.youtube.com/embed/bk_snr9dTw8" + }, + { + "videoA": "https://www.youtube.com/embed/TKfIPKk1izQ", + "videoB": "https://www.youtube.com/embed/89OyyuUxc_s" + }, + { + "videoA": "https://www.youtube.com/embed/r4Jqg4SY-h4", + "videoB": "https://www.youtube.com/embed/8Pbubzz1iws" + }, + { + "videoA": "https://www.youtube.com/embed/xCk5CTSD7iM", + "videoB": "https://www.youtube.com/embed/pZR_yZdMj1I" + }, + { + "videoA": "https://www.youtube.com/embed/TkDGHXgbanM", + "videoB": "https://www.youtube.com/embed/pklcqQ94gsI" + }, + { + "videoA": "https://www.youtube.com/embed/OYcZSvHmSLI", + "videoB": "https://www.youtube.com/embed/xVDOpr-Y8pQ" + }, + { + "videoA": "https://www.youtube.com/embed/LHj3UTlfqHE", + "videoB": "https://www.youtube.com/embed/use3hkP2XTA" + }, + { + "videoA": "https://www.youtube.com/embed/bA-VGhl7wDc", + "videoB": "https://www.youtube.com/embed/wUC48oUOwf4" + }, + { + "videoA": "https://www.youtube.com/embed/UaJtLA-6Q8I", + "videoB": "https://www.youtube.com/embed/HMpjuNDOvjk" + }, + { + "videoA": "https://www.youtube.com/embed/dBMa25frZd8", + "videoB": "https://www.youtube.com/embed/zZZKJ0UnPLU" + } +] \ No newline at end of file diff --git a/vision/sample-apps/V-Start/index.html b/vision/sample-apps/V-Start/index.html new file mode 100644 index 0000000..75a3277 --- /dev/null +++ b/vision/sample-apps/V-Start/index.html @@ -0,0 +1,172 @@ + + + + + + V-Start + + + + + + + +
    +
    +
    +
    +

    V-Start

    +

    A Toolkit for Veo Prompting and Evaluation

    +
    +
    + +
    +
    +
    +
    + +
    +
    + + +
    +
    +
    +

    Authentication

    +
    +

    (Click to hide)

    + + + +
    +
    +
    +
    +
    + + +
    + +
    +
    + + +
    +
    + + +

    + Select the region where your Vertex AI API is enabled +

    +
    +
    + +
    + + +
    +
    +

    + Run gcloud auth print-access-token in your terminal to get a new token. +

    +
    +
    +
    +
    +
    + +
    +
    +
    +
    + + + +
    + + + + \ No newline at end of file diff --git a/vision/sample-apps/V-Start/package-lock.json b/vision/sample-apps/V-Start/package-lock.json new file mode 100644 index 0000000..27a6d01 --- /dev/null +++ b/vision/sample-apps/V-Start/package-lock.json @@ -0,0 +1,1242 @@ +{ + "name": "v-start", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "v-start", + "version": "1.0.0", + "license": "Apache-2.0", + "dependencies": { + "@google/genai": "^1.17.0", + "dotenv": "^16.4.5", + "express": "^4.19.2", + "node-fetch": "^3.3.2" + } + }, + "node_modules/@google/genai": { + "version": "1.17.0", + "resolved": "https://registry.npmjs.org/@google/genai/-/genai-1.17.0.tgz", + "integrity": "sha512-r/OZWN9D8WvYrte3bcKPoLODrZ+2TjfxHm5OOyVHUbdFYIp1C4yJaXX4+sCS8I/+CbN9PxLjU5zm1cgmS7qz+A==", + "license": "Apache-2.0", + "dependencies": { + "google-auth-library": "^9.14.2", + "ws": "^8.18.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@modelcontextprotocol/sdk": "^1.11.4" + }, + "peerDependenciesMeta": { + "@modelcontextprotocol/sdk": { + "optional": true + } + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "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/bignumber.js": { + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz", + "integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/body-parser": { + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.13.0", + "raw-body": "2.5.2", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "license": "MIT" + }, + "node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", + "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.3", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.7.1", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.3.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.12", + "proxy-addr": "~2.0.7", + "qs": "6.13.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.19.0", + "serve-static": "1.16.2", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "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/fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, + "node_modules/finalhandler": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "license": "MIT", + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "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/gaxios": { + "version": "6.7.1", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-6.7.1.tgz", + "integrity": "sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ==", + "license": "Apache-2.0", + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^7.0.1", + "is-stream": "^2.0.0", + "node-fetch": "^2.6.9", + "uuid": "^9.0.1" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/gaxios/node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/gcp-metadata": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-6.1.1.tgz", + "integrity": "sha512-a4tiq7E0/5fTjxPAaH4jpjkSv/uCaU2p5KC6HVGrvl0cDjA8iBZv4vv1gyzlmK0ZUKqwpOyQMKzZQe3lTit77A==", + "license": "Apache-2.0", + "dependencies": { + "gaxios": "^6.1.1", + "google-logging-utils": "^0.0.2", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/google-auth-library": { + "version": "9.15.1", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-9.15.1.tgz", + "integrity": "sha512-Jb6Z0+nvECVz+2lzSMt9u98UsoakXxA2HGHMCxh+so3n90XgYWkq5dur19JAJV7ONiJY22yBTyJB1TSkvPq9Ng==", + "license": "Apache-2.0", + "dependencies": { + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "gaxios": "^6.1.1", + "gcp-metadata": "^6.1.0", + "gtoken": "^7.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/google-logging-utils": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-0.0.2.tgz", + "integrity": "sha512-NEgUnEcBiP5HrPzufUkBzJOD/Sxsco3rLNo1F1TNf7ieU8ryUzBhqba8r756CjLX7rn3fHl6iLEwPYuqpoKgQQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gtoken": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-7.1.0.tgz", + "integrity": "sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw==", + "license": "MIT", + "dependencies": { + "gaxios": "^6.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent/node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/https-proxy-agent/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/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/json-bigint": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", + "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", + "license": "MIT", + "dependencies": { + "bignumber.js": "^9.0.0" + } + }, + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", + "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", + "license": "MIT", + "dependencies": { + "jwa": "^2.0.0", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "deprecated": "Use your platform's native DOMException instead", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "engines": { + "node": ">=10.5.0" + } + }, + "node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "license": "MIT", + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "license": "MIT" + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/qs": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/send": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/send/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/serve-static": { + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.19.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/web-streams-polyfill": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + } + } +} diff --git a/vision/sample-apps/V-Start/package.json b/vision/sample-apps/V-Start/package.json new file mode 100644 index 0000000..346a2f0 --- /dev/null +++ b/vision/sample-apps/V-Start/package.json @@ -0,0 +1,23 @@ +{ + "name": "v-start", + "version": "1.0.0", + "description": "A toolkit for Veo prompting and evaluation.", + "main": "server.js", + "scripts": { + "start": "node server.js" + }, + "dependencies": { + "@google/genai": "^1.17.0", + "dotenv": "^16.4.5", + "express": "^4.19.2", + "node-fetch": "^3.3.2" + }, + "keywords": [ + "veo", + "gemini", + "generative-ai" + ], + "author": "Wafae Bakkali", + "license": "Apache-2.0", + "type": "commonjs" +} \ No newline at end of file diff --git a/vision/sample-apps/V-Start/server.js b/vision/sample-apps/V-Start/server.js new file mode 100644 index 0000000..e7966fd --- /dev/null +++ b/vision/sample-apps/V-Start/server.js @@ -0,0 +1,398 @@ +/* + * Copyright 2025 Google LLC + * + * 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 + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +const express = require('express'); +const path = require('path'); +const fs = require('fs'); +const fetch = (...args) => import('node-fetch').then(({default: fetch}) => fetch(...args)); + +// Import the new Google Gen AI SDK +const { GoogleGenAI } = require('@google/genai'); + +require('dotenv').config(); + +const app = express(); +const port = process.env.PORT || 8080; + +// Middleware & Static File Serving +app.use(express.json({ limit: '50mb' })); +app.use(express.static(path.join(__dirname))); + +// --- API ENDPOINTS --- + +// 1. Endpoint to securely load the YouTube study data from a private file +app.get('/api/study/veo-youtube-study', (req, res) => { + const filePath = path.join(__dirname, 'data', 'veo-youtube-study.json'); + fs.readFile(filePath, 'utf8', (err, data) => { + if (err) { + console.error("Error reading study file:", err); + return res.status(500).json({ error: "Could not load the study data." }); + } + try { + const jsonData = JSON.parse(data); + res.json(jsonData); + } catch (parseErr) { + console.error("Error parsing study JSON:", parseErr); + return res.status(500).json({ error: "Study data is corrupted." }); + } + }); +}); + +// 2. Endpoint to proxy video URLs to avoid CORS issues +app.get('/api/proxy-video', async (req, res) => { + const videoUrl = req.query.url; + if (!videoUrl) { + return res.status(400).json({ error: 'URL query parameter is required.' }); + } + + try { + console.log(`Proxying video from: ${videoUrl}`); + const videoResponse = await fetch(videoUrl); + + if (!videoResponse.ok) { + throw new Error(`Failed to fetch video with status: ${videoResponse.statusText}`); + } + + const contentType = videoResponse.headers.get('content-type'); + if (contentType) res.setHeader('Content-Type', contentType); + + videoResponse.body.pipe(res); + } catch (error) { + console.error('Error proxying video:', error.message); + res.status(500).json({ error: `Failed to proxy video. Reason: ${error.message}` }); + } +}); + +// 3. Fast validation endpoint using Gemini Flash model +app.post('/api/validate-token', async (req, res) => { + const { projectId, accessToken, location } = req.body; + const validationLocation = location || 'us-central1'; + console.log(`Fast validation using Flash for project: ${projectId} in location: ${validationLocation}`); + + if (projectId && !accessToken) { + return res.status(400).json({ + valid: false, + message: 'Access Token is required for validation.' + }); + } + + // Store original env variable to restore later + const originalAuthToken = process.env.GOOGLE_AUTH_TOKEN; + + try { + let ai; + + if (accessToken) { + // Access token validation + process.env.GOOGLE_AUTH_TOKEN = accessToken; + + ai = new GoogleGenAI({ + vertexai: true, + project: projectId, + location: validationLocation + }); + } else { + // API key validation + const apiKey = process.env.API_KEY || process.env.GEMINI_API_KEY; + if (!apiKey) { + return res.json({ + valid: false, + message: 'No API key configured on server.' + }); + } + + ai = new GoogleGenAI({ + vertexai: false, + apiKey: apiKey + }); + } + + console.log('Running fast validation with gemini-2.0-flash...'); + + // Use Flash model for much faster validation + const response = await ai.models.generateContent({ + model: 'gemini-2.5-flash', + contents: [{ + role: "user", + parts: [{ text: "1+1" }] // Minimal math question + }], + generationConfig: { + maxOutputTokens: 1, + temperature: 0 + } + }); + + // If we get here, credentials are valid + console.log('Flash validation successful'); + res.json({ + valid: true, + message: 'Credentials validated successfully!' + }); + + } catch (error) { + console.error('Validation error:', error.message); + + // Use a generic error message for all credential-related failures + res.json({ + valid: false, + message: 'Invalid credentials. Please check your configuration.' + }); + } finally { + // Always restore the original environment variable + if (originalAuthToken !== undefined) { + process.env.GOOGLE_AUTH_TOKEN = originalAuthToken; + } else { + delete process.env.GOOGLE_AUTH_TOKEN; + } + } +}); + +// 4. Main Gemini API proxy endpoint using SDK +app.post('/api/generate', async (req, res) => { + console.log('========== Generate endpoint called =========='); + const { authMethod, accessToken, projectId, location, systemPrompt, contentParts } = req.body; + const model = 'gemini-2.5-pro'; + + // Debug logging + console.log('Request received with:', { + authMethod, + hasToken: !!accessToken, + hasProjectId: !!projectId, + location: location || 'us-central1', + systemPromptLength: systemPrompt?.length || 0, + contentPartsCount: contentParts?.length || 0 + }); + + try { + let ai; + let response; + + if (authMethod === 'access-token') { + // === ACCESS TOKEN PATH === + if (!projectId || !accessToken) { + return res.status(400).json({ + error: "Project ID and Access Token are required for gcloud auth.", + validationError: true + }); + } + + const vertexLocation = location || 'us-central1'; + console.log(`Using Gen AI SDK with access token for project: ${projectId} in location: ${vertexLocation}`); + + // The SDK can use access tokens through environment variables + // Store the original value to restore later + const originalAuthToken = process.env.GOOGLE_AUTH_TOKEN; + + try { + // Set the access token as environment variable for the SDK + process.env.GOOGLE_AUTH_TOKEN = accessToken; + + // Initialize SDK with Vertex AI configuration + // The SDK will automatically use the GOOGLE_AUTH_TOKEN we just set + ai = new GoogleGenAI({ + vertexai: true, + project: projectId, + location: vertexLocation + }); + + // Build parts array + const parts = []; + + // Add text prompt first + if (systemPrompt) { + parts.push({ text: systemPrompt }); + console.log('Added system prompt to parts'); + } + + // Add any images/videos from contentParts + if (contentParts && contentParts.length > 0) { + contentParts.forEach((part, index) => { + if (part.inlineData && part.inlineData.data && part.inlineData.mimeType) { + parts.push({ + inlineData: { + mimeType: part.inlineData.mimeType, + data: part.inlineData.data + } + }); + console.log(`Added media to parts: ${part.inlineData.mimeType}`); + } + }); + } + + console.log(`Sending ${parts.length} parts via SDK to Vertex AI in ${vertexLocation}`); + + // Generate content using the SDK + response = await ai.models.generateContent({ + model: model, + contents: [{ + role: "user", + parts: parts + }] + }); + + const text = response.text; + console.log('Response received, text length:', text.length); + res.json({ text: text.trim() }); + + } finally { + // Always restore the original environment variable + if (originalAuthToken !== undefined) { + process.env.GOOGLE_AUTH_TOKEN = originalAuthToken; + } else { + delete process.env.GOOGLE_AUTH_TOKEN; + } + } + + } else { + // === API KEY PATH === + const apiKey = process.env.API_KEY || process.env.GEMINI_API_KEY; + if (!apiKey) { + return res.status(500).json({ + error: 'API Key is not configured on the server. Please check your .env file.', + validationError: true + }); + } + + console.log('Using Gen AI SDK with API key'); + + // Initialize SDK with API key + ai = new GoogleGenAI({ + vertexai: false, + apiKey: apiKey + }); + + // Build parts array + const parts = []; + + // Add text prompt first + if (systemPrompt) { + parts.push({ text: systemPrompt }); + console.log('Added system prompt to parts'); + } + + // Add any images/videos from contentParts + if (contentParts && contentParts.length > 0) { + contentParts.forEach((part, index) => { + if (part.inlineData && part.inlineData.data && part.inlineData.mimeType) { + parts.push({ + inlineData: { + mimeType: part.inlineData.mimeType, + data: part.inlineData.data + } + }); + console.log(`Added media to parts: ${part.inlineData.mimeType}`); + } + }); + } + + console.log(`Sending ${parts.length} parts via SDK to Gemini API`); + + // Generate content using the SDK + response = await ai.models.generateContent({ + model: model, + contents: [{ + role: "user", + parts: parts + }] + }); + + const text = response.text; + console.log('Response received, text length:', text.length); + res.json({ text: text.trim() }); + } + + } catch (error) { + console.error('Generation error:', error); + + // Use generic error message for validation errors + if (error.message?.includes('401') || error.message?.includes('Unauthorized') || error.message?.includes('invalid')) { + return res.status(401).json({ + error: 'Invalid credentials. Please check your configuration.', + validationError: true + }); + } + + if (error.message?.includes('API_KEY_INVALID')) { + return res.status(401).json({ + error: 'Invalid credentials. Please check your configuration.', + validationError: true + }); + } + + if (error.message?.includes('403') || error.message?.includes('Permission denied')) { + return res.status(403).json({ + error: 'Invalid credentials. Please check your configuration.', + validationError: true + }); + } + + if (error.message?.includes('404') || error.message?.includes('not found')) { + return res.status(404).json({ + error: 'Invalid credentials. Please check your configuration.', + validationError: true + }); + } + + if (error.message?.includes('quota')) { + return res.status(429).json({ + error: 'API quota exceeded. Please try again later.' + }); + } + + if (error.code === 'ENOTFOUND' || error.code === 'ECONNREFUSED') { + return res.status(500).json({ + error: 'Cannot connect to Google services. Check your internet connection.', + validationError: true + }); + } + + res.status(500).json({ + error: 'An internal server error occurred: ' + error.message + }); + } +}); + +// Test endpoint to verify server is running +app.get('/api/health', (req, res) => { + res.json({ + status: 'healthy', + timestamp: new Date().toISOString(), + endpoints: [ + '/api/health', + '/api/validate-token', + '/api/generate', + '/api/proxy-video', + '/api/study/veo-youtube-study' + ], + sdkVersion: { + '@google/genai': require('@google/genai/package.json').version + } + }); +}); + +// Root route to serve index.html +app.get('/', (req, res) => { + res.sendFile(path.join(__dirname, 'index.html')); +}); + +// --- Server Start --- +app.listen(port, '0.0.0.0', () => { + console.log(`Server listening at http://localhost:${port}`); + console.log('Environment variables configured:'); + console.log(' API_KEY/GEMINI_API_KEY:', !!(process.env.API_KEY || process.env.GEMINI_API_KEY)); + console.log(' GOOGLE_CLOUD_PROJECT:', process.env.GOOGLE_CLOUD_PROJECT || 'Not set'); + console.log(' GOOGLE_CLOUD_LOCATION:', process.env.GOOGLE_CLOUD_LOCATION || 'Not set (will use UI selection)'); +}); \ No newline at end of file diff --git a/vision/sample-apps/V-Start/src/api.js b/vision/sample-apps/V-Start/src/api.js new file mode 100644 index 0000000..fa154a0 --- /dev/null +++ b/vision/sample-apps/V-Start/src/api.js @@ -0,0 +1,50 @@ +/* + * Copyright 2025 Google LLC + * + * 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 + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export async function callGeminiApi(systemPrompt, contentParts = []) { + // Read the selected auth method and credentials from the global UI elements + const authMethod = document.getElementById('auth-method-select').value; + const accessToken = document.getElementById('access-token-input').value; + const projectId = document.getElementById('project-id-input').value; + + const payload = { + authMethod, + accessToken, + projectId, + systemPrompt, + contentParts + }; + + try { + const response = await fetch('/api/generate', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(payload) + }); + + if (!response.ok) { + const errorBody = await response.json(); + throw new Error(`API Error: ${errorBody.error || 'Unknown server error'}`); + } + + const result = await response.json(); + return result.text; + + } catch (error) { + console.error("Error calling backend service:", error); + throw error; + } +} diff --git a/vision/sample-apps/V-Start/src/features/alignment-eval.js b/vision/sample-apps/V-Start/src/features/alignment-eval.js new file mode 100644 index 0000000..bbe0576 --- /dev/null +++ b/vision/sample-apps/V-Start/src/features/alignment-eval.js @@ -0,0 +1,666 @@ +/* + * Copyright 2025 Google LLC + * + * 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 + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { callGeminiApi } from '../api.js'; +import { showToast } from '../ui.js'; + +let alignmentResults = []; +let loadedPairs = []; +let loadedPrompts = []; +let loadedVideos = []; + +// --- HELPER FUNCTIONS --- + +function extractJSON(text) { + try { + const match = text.match(/\{[\s\S]*\}/); + if (match) return JSON.parse(match[0]); + } catch (e) { /* Fallback */ } + throw new Error('Failed to parse valid JSON from the API response.'); +} + +function getScoreColor(score) { + if (score >= 80) return { + bg: 'bg-green-100 dark:bg-green-900', + text: 'text-green-800 dark:text-green-200', + border: 'border-green-500 dark:border-green-400', + label: 'Excellent' + }; + if (score >= 60) return { + bg: 'bg-blue-100 dark:bg-blue-900', + text: 'text-blue-800 dark:text-blue-200', + border: 'border-blue-500 dark:border-blue-400', + label: 'Good' + }; + if (score >= 40) return { + bg: 'bg-yellow-100 dark:bg-yellow-900', + text: 'text-yellow-800 dark:text-yellow-200', + border: 'border-yellow-500 dark:border-yellow-400', + label: 'Fair' + }; + return { + bg: 'bg-red-100 dark:bg-red-900', + text: 'text-red-800 dark:text-red-200', + border: 'border-red-500 dark:border-red-400', + label: 'Poor' + }; +} + +function parsePromptsFromCSV(textContent) { + return textContent + .split(/\r?\n/) + .map(line => line.trim()) + .filter(line => line.length > 0); +} + +async function getVideoAsBase64(videoSource) { + let blob; + if (videoSource instanceof File) { + blob = videoSource; + } else if (typeof videoSource === 'string') { + const response = await fetch('/api/proxy-video?url=' + encodeURIComponent(videoSource)); + if (!response.ok) { + const errorData = await response.json(); + throw new Error(errorData.error || 'Failed to fetch video via proxy: ' + response.statusText); + } + blob = await response.blob(); + } else { + throw new Error('Invalid video source provided.'); + } + + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onload = () => resolve({ + base64Data: reader.result.split(',')[1], + mimeType: blob.type + }); + reader.onerror = reject; + reader.readAsDataURL(blob); + }); +} + +// --- DRAG AND DROP FUNCTIONS --- + +function initDragAndDrop(listId, itemsArray, updateCallback) { + const list = document.getElementById(listId); + if (!list) return; + + let draggedElement = null; + + list.addEventListener('dragstart', (e) => { + if (e.target.draggable) { + draggedElement = e.target; + e.target.style.opacity = '0.5'; + e.dataTransfer.effectAllowed = 'move'; + } + }); + + list.addEventListener('dragend', (e) => { + if (e.target.draggable) { + e.target.style.opacity = ''; + } + }); + + list.addEventListener('dragover', (e) => { + e.preventDefault(); + e.dataTransfer.dropEffect = 'move'; + + const afterElement = getDragAfterElement(list, e.clientY); + if (afterElement == null) { + list.appendChild(draggedElement); + } else { + list.insertBefore(draggedElement, afterElement); + } + }); + + list.addEventListener('drop', (e) => { + e.preventDefault(); + + // Get the new order + const items = [...list.querySelectorAll('[draggable="true"]')]; + const newOrder = items.map(item => parseInt(item.dataset.index)); + + // Reorder the array + const reorderedItems = newOrder.map(oldIndex => itemsArray[oldIndex]); + + // Update the original array + itemsArray.length = 0; + itemsArray.push(...reorderedItems); + + // Re-render + updateCallback(); + + showToast('Items reordered successfully!', 'success'); + }); +} + +function getDragAfterElement(container, y) { + const draggableElements = [...container.querySelectorAll('[draggable="true"]:not([style*="opacity"])')]; + + return draggableElements.reduce((closest, child) => { + const box = child.getBoundingClientRect(); + const offset = y - box.top - box.height / 2; + + if (offset < 0 && offset > closest.offset) { + return { offset: offset, element: child }; + } else { + return closest; + } + }, { offset: Number.NEGATIVE_INFINITY }).element; +} + +// --- HTML TEMPLATE LOADER --- + +export async function getAlignmentEvalContent() { + try { + const response = await fetch('/src/features/templates/alignment-eval.html'); + if (!response.ok) { + throw new Error('Network response was not ok: ' + response.statusText); + } + return await response.text(); + } catch (error) { + console.error('Failed to fetch alignment-eval template:', error); + return '
    Error loading content. Please check the console for details.
    '; + } +} + +// --- INITIALIZATION AND LOGIC --- + +function renderPromptsList() { + const promptsList = document.getElementById('prompts-list'); + promptsList.innerHTML = ''; + + loadedPrompts.forEach((prompt, index) => { + const promptDiv = document.createElement('div'); + promptDiv.className = 'flex items-center gap-2 p-2 bg-gray-100 dark:bg-gray-700 rounded cursor-move hover:shadow-md transition-all duration-200 border border-transparent hover:border-gray-300 dark:hover:border-gray-600'; + promptDiv.draggable = true; + promptDiv.dataset.index = index; + + const dragIcon = ''; + + promptDiv.innerHTML = '
    ' + dragIcon + '
    ' + + '
    ' + (index + 1) + '.
    ' + + '
    ' + prompt + '
    '; + + promptsList.appendChild(promptDiv); + }); + + initDragAndDrop('prompts-list', loadedPrompts, renderPromptsList); +} + +function renderVideosList() { + const videosList = document.getElementById('videos-list'); + videosList.innerHTML = ''; + + loadedVideos.forEach((video, index) => { + const videoName = (video.source instanceof File) ? video.source.name : video.source.split('/').pop(); + const videoDiv = document.createElement('div'); + videoDiv.className = 'flex items-center gap-2 p-2 bg-gray-100 dark:bg-gray-700 rounded cursor-move hover:shadow-md transition-all duration-200 border border-transparent hover:border-gray-300 dark:hover:border-gray-600'; + videoDiv.draggable = true; + videoDiv.dataset.index = index; + + const dragIcon = ''; + + videoDiv.innerHTML = '
    ' + dragIcon + '
    ' + + '
    ' + (index + 1) + '.
    ' + + '
    ' + videoName + '
    '; + + videosList.appendChild(videoDiv); + }); + + initDragAndDrop('videos-list', loadedVideos, renderVideosList); +} + +function renderFinalPairs() { + const pairsContainer = document.getElementById('alignment-pairs-container'); + pairsContainer.innerHTML = ''; +} + +export function initAlignmentEval() { + alignmentResults = []; + loadedPairs = []; + loadedPrompts = []; + loadedVideos = []; + + const loadAndPairBtn = document.getElementById('load-and-pair-btn'); + const clearAllBtn = document.getElementById('clear-all-btn'); + const startEvalBtn = document.getElementById('start-alignment-eval-btn'); + const downloadBtn = document.getElementById('download-alignment-results-btn'); + const reorderSection = document.getElementById('reorder-section'); + const evalActionsContainer = document.getElementById('eval-actions-container'); + + // STEP 1: Load inputs and automatically create pairs + const loadAndCreatePairs = async () => { + const csvInput = document.getElementById('prompts-csv-input'); + const textInput = document.getElementById('prompts-text-input'); + const videosFileInput = document.getElementById('batch-videos-input'); + const videosUrlInput = document.getElementById('videos-url-input'); + + // Load prompts + loadedPrompts = []; + if (csvInput.files.length > 0) { + loadedPrompts = parsePromptsFromCSV(await csvInput.files[0].text()); + } else if (textInput.value.trim()) { + loadedPrompts = textInput.value.split(';').map(p => p.trim()).filter(p => p); + } + + // Load videos + loadedVideos = []; + if (videosFileInput.files.length > 0) { + const files = Array.from(videosFileInput.files); + loadedVideos = files.map(file => ({ source: file })); + } else if (videosUrlInput.value.trim()) { + const urls = videosUrlInput.value.split(/\r?\n/).map(url => url.trim()).filter(url => url); + loadedVideos = urls.map(url => ({ source: url })); + } + + // Validation + if (loadedPrompts.length === 0) { + showToast('Please provide prompts either via CSV file or text input.', 'error'); + return; + } + + if (loadedVideos.length === 0) { + showToast('Please provide videos either via file upload or URL input.', 'error'); + return; + } + + if (loadedPrompts.length !== loadedVideos.length) { + showToast('Mismatch: ' + loadedPrompts.length + ' prompts and ' + loadedVideos.length + ' videos. Please ensure they match.', 'error'); + return; + } + + // Automatically create pairs + loadedPairs = loadedPrompts.map((prompt, i) => { + const video = loadedVideos[i]; + const name = (video.source instanceof File) ? video.source.name : video.source.split('/').pop(); + return { + id: i + 1, + prompt: prompt, + videoSource: video.source, + videoName: name + }; + }); + + // Show reordering only if more than 1 pair + if (loadedPairs.length > 1) { + renderPromptsList(); + renderVideosList(); + reorderSection.classList.remove('hidden'); + showToast('Created ' + loadedPairs.length + ' pairs. Drag to reorder if needed, then click "Start Evaluation".', 'success'); + } else { + reorderSection.classList.add('hidden'); + showToast('Created ' + loadedPairs.length + ' pair. Ready for evaluation!', 'success'); + } + + renderFinalPairs(); + evalActionsContainer.classList.remove('hidden'); + + // Update pairs when reordering (only if more than 1 pair) + if (loadedPairs.length > 1) { + const updatePairsFromReorder = () => { + loadedPairs = loadedPrompts.map((prompt, i) => { + const video = loadedVideos[i]; + const name = (video.source instanceof File) ? video.source.name : video.source.split('/').pop(); + return { + id: i + 1, + prompt: prompt, + videoSource: video.source, + videoName: name + }; + }); + renderFinalPairs(); + }; + + // Re-initialize drag and drop with the update callback + initDragAndDrop('prompts-list', loadedPrompts, () => { + renderPromptsList(); + updatePairsFromReorder(); + }); + initDragAndDrop('videos-list', loadedVideos, () => { + renderVideosList(); + updatePairsFromReorder(); + }); + } + }; + + const clearAll = () => { + loadedPairs = []; + loadedPrompts = []; + loadedVideos = []; + alignmentResults = []; + + document.getElementById('alignment-pairs-container').innerHTML = ''; + document.getElementById('prompts-list').innerHTML = ''; + document.getElementById('videos-list').innerHTML = ''; + + reorderSection.classList.add('hidden'); + evalActionsContainer.classList.add('hidden'); + document.getElementById('alignment-results-container').classList.add('hidden'); + document.getElementById('download-alignment-results-container').classList.add('hidden'); + + document.getElementById('prompts-csv-input').value = ''; + document.getElementById('prompts-text-input').value = ''; + document.getElementById('batch-videos-input').value = ''; + document.getElementById('videos-url-input').value = ''; + + showToast('All data cleared.', 'info'); + }; + + // STEP 2: Start evaluation + const startAlignmentEvaluation = async () => { + const btn = document.getElementById('start-alignment-eval-btn'); + const resultsContainer = document.getElementById('alignment-results-container'); + + btn.disabled = true; + btn.innerHTML = '' + + '' + + '' + + 'Evaluating...'; + + resultsContainer.classList.remove('hidden'); + resultsContainer.innerHTML = ''; + document.getElementById('download-alignment-results-container').classList.add('hidden'); + alignmentResults = []; + + for (const pair of loadedPairs) { + const resultCard = document.createElement('div'); + resultCard.className = 'bg-white dark:bg-gray-800 rounded-lg shadow-lg overflow-hidden border border-gray-200 dark:border-gray-700'; + + const loadingHTML = '
    ' + + '

    Pair #' + pair.id + ': Evaluating...

    ' + + '

    ' + pair.videoName + '

    ' + + '
    ' + + '
    ' + + '
    ' + + '' + + '' + + '' + + '' + + '
    ' + + '

    Step 1: Preparing video data...

    ' + + '
    '; + + resultCard.innerHTML = loadingHTML; + resultsContainer.appendChild(resultCard); + const progressText = document.getElementById('progress-text-' + pair.id); + + try { + const videoData = await getVideoAsBase64(pair.videoSource); + const base64Data = videoData.base64Data; + const mimeType = videoData.mimeType; + + // Use camelCase to match server.js expectations + const videoParts = [{ + inlineData: { mimeType: mimeType, data: base64Data } + }]; + + progressText.textContent = 'Step 2: Analyzing video and generating score...'; + + // Clean the prompt to avoid syntax issues + const cleanPrompt = pair.prompt + .replace(/[\\`"'\n\r\t]/g, ' ') + .replace(/\s+/g, ' ') + .trim(); + + // Build prompt using array join + const promptParts = []; + promptParts.push('You are an expert video-to-prompt alignment auditor. Your job is to perform STRICT, CRITICAL evaluation.'); + promptParts.push(''); + promptParts.push('PROMPT TO EVALUATE: ' + cleanPrompt); + promptParts.push(''); + promptParts.push('CRITICAL INSTRUCTION: You MUST actually watch and analyze the video content. Do not make assumptions.'); + promptParts.push(''); + promptParts.push('EVALUATION PROTOCOL:'); + promptParts.push(''); + promptParts.push('STEP 1 - FIRST DESCRIBE WHAT YOU SEE:'); + promptParts.push('Before evaluating, describe in one sentence what is actually visible in this video.'); + promptParts.push(''); + promptParts.push('STEP 2 - DECOMPOSE THE PROMPT:'); + promptParts.push('Extract EVERY verifiable element from the prompt:'); + promptParts.push('- SUBJECTS: All entities mentioned (people, animals, objects)'); + promptParts.push('- ACTIONS: All verbs and movements described'); + promptParts.push('- ATTRIBUTES: All descriptive details (colors, sizes, quantities, qualities)'); + promptParts.push('- SETTINGS: Locations, environments, time of day, weather'); + promptParts.push('- RELATIONSHIPS: Spatial arrangements, interactions between elements'); + promptParts.push(''); + promptParts.push('STEP 3 - DETERMINE QUESTION COUNT:'); + promptParts.push('Count the total distinct elements extracted above. Generate questions based on complexity:'); + promptParts.push('- 1-3 elements: Generate 3-5 questions'); + promptParts.push('- 4-6 elements: Generate 5-8 questions'); + promptParts.push('- 7-10 elements: Generate 8-12 questions'); + promptParts.push('- 11-15 elements: Generate 12-18 questions'); + promptParts.push('- 16+ elements: Generate 18-25 questions'); + promptParts.push(''); + promptParts.push('STEP 4 - IDENTIFY CORE REQUIREMENT:'); + promptParts.push('What is the SINGLE most important element that defines this video?'); + promptParts.push(''); + promptParts.push('STEP 5 - CREATE VERIFICATION QUESTIONS:'); + promptParts.push('Generate specific questions that test EXACT requirements:'); + promptParts.push('- Each question must test ONE specific claim from the prompt'); + promptParts.push('- If prompt says "red car", ask "Is the car red?" not just "Is there a car?"'); + promptParts.push('- If prompt says "jumping", ask "Is [subject] jumping?" not "Is there movement?"'); + promptParts.push('- Questions MUST have a mix of Yes and No answers based on what is actually in the video'); + promptParts.push(''); + promptParts.push('STEP 6 - EVALUATE THE VIDEO:'); + promptParts.push('For each question, look at what is ACTUALLY in the video:'); + promptParts.push('- YES: Element is clearly visible AND matches the exact requirement'); + promptParts.push('- NO: Element is absent, wrong, or does not match the requirement'); + promptParts.push('- UNCERTAIN: Only if genuinely impossible to determine'); + promptParts.push(''); + promptParts.push('CRITICAL EVALUATION RULES:'); + promptParts.push('- You MUST look at the actual video content, not guess based on the prompt'); + promptParts.push('- DEFAULT TO NO when in doubt'); + promptParts.push('- Partial matches are NO (blue car when prompt says red = NO)'); + promptParts.push('- Generic matches are NO (animal when prompt says cat = NO)'); + promptParts.push('- If the video shows something completely different from the prompt, most answers should be NO'); + promptParts.push(''); + promptParts.push('STEP 7 - CALCULATE SCORE:'); + promptParts.push('- Core element missing: Maximum 20 points'); + promptParts.push('- Core element present: Start with 100 points'); + promptParts.push('- For each NO: Deduct (80 / total_questions) points'); + promptParts.push('- For each UNCERTAIN: Deduct (40 / total_questions) points'); + promptParts.push(''); + promptParts.push('OUTPUT FORMAT - Return ONLY this JSON (no markdown):'); + promptParts.push('{'); + promptParts.push(' "core_subject": "the main subject from the prompt",'); + promptParts.push(' "core_subject_present": true or false based on actual video content,'); + promptParts.push(' "holistic_alignment_score": 0 to 100,'); + promptParts.push(' "answers": ['); + promptParts.push(' {"question": "Specific question?", "answer": "Yes/No/Uncertain"}'); + promptParts.push(' ]'); + promptParts.push('}'); + + const systemPrompt = promptParts.join('\n'); + + console.log('Sending evaluation request for pair #' + pair.id); + console.log('Video data length:', base64Data ? base64Data.length : 0); + const resultRaw = await callGeminiApi(systemPrompt, videoParts); + console.log('Raw response for pair #' + pair.id + ':', resultRaw); + + const resultData = extractJSON(resultRaw); + console.log('Parsed data for pair #' + pair.id + ':', JSON.stringify(resultData, null, 2)); + + const answers = resultData.answers || []; + const core_subject = resultData.core_subject || 'Unknown'; + const core_subject_present = resultData.core_subject_present || false; + const holistic_alignment_score = resultData.holistic_alignment_score || 0; + const score = holistic_alignment_score; + + // Validation check for evaluation quality + if (answers && answers.length > 2) { + const yesCount = answers.filter(a => String(a.answer).toLowerCase() === 'yes').length; + const noCount = answers.filter(a => String(a.answer).toLowerCase() === 'no').length; + const yesPercentage = (yesCount / answers.length) * 100; + + console.log('Pair #' + pair.id + ' evaluation distribution: ' + yesCount + ' Yes, ' + noCount + ' No, ' + (answers.length - yesCount - noCount) + ' Uncertain'); + + if (yesPercentage === 100 && answers.length > 3) { + console.warn('Warning: All answers are YES for pair #' + pair.id + ' - evaluation might be too lenient'); + } + } + + alignmentResults.push({ + pairId: pair.id, + prompt: pair.prompt, + videoName: pair.videoName, + score: score, + core_subject: core_subject, + core_subject_present: core_subject_present, + answers: answers + }); + + const colorScheme = getScoreColor(score); + + // Build result HTML + let answersHTML = ''; + answers.forEach((a, idx) => { + const answer = String(a.answer).toLowerCase(); + const answerColor = answer === 'yes' ? 'text-green-700 dark:text-green-400' : answer === 'no' ? 'text-red-700 dark:text-red-400' : 'text-yellow-700 dark:text-yellow-400'; + const answerIcon = answer === 'yes' ? '[YES]' : answer === 'no' ? '[NO]' : '[?]'; + answersHTML += '
    ' + + '' + answerIcon + '' + + '
    ' + + '

    ' + (idx + 1) + '. ' + a.question + '

    ' + + '

    ' + a.answer + '

    ' + + '
    ' + + '
    '; + }); + + const yesCountFinal = answers.filter(a => String(a.answer).toLowerCase() === 'yes').length; + + resultCard.innerHTML = '
    ' + + '
    ' + + '
    ' + + '

    Pair #' + pair.id + '

    ' + + '

    ' + pair.videoName + '

    ' + + '
    ' + + '
    ' + + '

    ' + score + '%

    ' + + '

    ' + colorScheme.label + '

    ' + + '
    ' + + '
    ' + + '
    ' + + '
    ' + + '
    ' + + '

    Prompt: "' + pair.prompt + '"

    ' + + '

    Evaluation: ' + answers.length + ' criteria checked

    ' + + '
    ' + + '
    ' + + 'View Detailed Evaluation' + + '
    ' + + '
    ' + + '' + (core_subject_present ? '[YES]' : '[NO]') + '' + + '
    ' + + '

    Core Subject: "' + core_subject + '"

    ' + + '

    ' + (core_subject_present ? 'Present in video' : 'Missing from video') + '

    ' + + '
    ' + + '
    ' + + answersHTML + + '
    ' + + '

    ' + + 'Summary: ' + yesCountFinal + '/' + answers.length + ' criteria met' + + (!core_subject_present ? ' (Core subject missing - score capped at 20%)' : '') + + '

    ' + + '
    ' + + '
    ' + + '
    ' + + '
    '; + + } catch (error) { + console.error('Error evaluating Pair #' + pair.id + ':', error); + resultCard.innerHTML = '
    ' + + '

    Error evaluating Pair #' + pair.id + '

    ' + + '

    ' + error.message + '

    ' + + '
    '; + showToast('Error on pair #' + pair.id + ': ' + error.message, 'error'); + } + } + + if (alignmentResults.length > 0) { + document.getElementById('download-alignment-results-container').classList.remove('hidden'); + } + + btn.disabled = false; + btn.innerHTML = 'Start Evaluation'; + }; + + const downloadAlignmentResults = () => { + if (alignmentResults.length === 0) { + showToast("No results to download.", "error"); + return; + } + + const headers = ["Pair ID", "Prompt", "Video Name", "Overall Score", "Core Subject", "Core Subject Present", "Total Questions", "Questions Met", "Question", "Answer"]; + let csvRows = [headers.join(',')]; + + alignmentResults.forEach(r => { + const escapeCsv = (str) => '"' + String(str).replace(/"/g, '""') + '"'; + const totalQuestions = r.answers ? r.answers.length : 0; + const questionsMet = r.answers ? r.answers.filter(a => String(a.answer).toLowerCase() === 'yes').length : 0; + + if (r.answers && r.answers.length > 0) { + r.answers.forEach((answer, idx) => { + const row = [ + r.pairId, + escapeCsv(r.prompt), + escapeCsv(r.videoName), + r.score, + escapeCsv(r.core_subject), + r.core_subject_present, + totalQuestions, + questionsMet, + escapeCsv((idx + 1) + '. ' + answer.question), + escapeCsv(answer.answer) + ]; + csvRows.push(row.join(',')); + }); + } else { + const row = [ + r.pairId, + escapeCsv(r.prompt), + escapeCsv(r.videoName), + r.score, + escapeCsv(r.core_subject), + r.core_subject_present, + 0, + 0, + "N/A", + "N/A" + ]; + csvRows.push(row.join(',')); + } + }); + + const csvContent = csvRows.join('\n'); + const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' }); + const link = document.createElement('a'); + link.href = URL.createObjectURL(blob); + const dateStr = new Date().toISOString().slice(0,10); + link.download = 'alignment_evaluation_results_' + dateStr + '.csv'; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + + showToast("Results downloaded successfully!", "success"); + }; + + // Attach event listeners to the buttons + loadAndPairBtn.addEventListener('click', loadAndCreatePairs); + clearAllBtn.addEventListener('click', clearAll); + startEvalBtn.addEventListener('click', startAlignmentEvaluation); + downloadBtn.addEventListener('click', downloadAlignmentResults); +} \ No newline at end of file diff --git a/vision/sample-apps/V-Start/src/features/converter.js b/vision/sample-apps/V-Start/src/features/converter.js new file mode 100644 index 0000000..e20a293 --- /dev/null +++ b/vision/sample-apps/V-Start/src/features/converter.js @@ -0,0 +1,90 @@ +/* + * Copyright 2025 Google LLC + * + * 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 + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { callGeminiApi } from '../api.js'; +import { showToast, copyToClipboard } from '../ui.js'; + +export async function getConverterContent() { + try { + const response = await fetch('/src/features/templates/converter.html'); + if (!response.ok) { + throw new Error(`Network response was not ok: ${response.statusText}`); + } + return await response.text(); + } catch (error) { + console.error('Failed to fetch converter template:', error); + return '
    Error loading content. Please check the console for details.
    '; + } +} + +export function initConverter() { + window.copyToClipboard = copyToClipboard; + + const convertBtn = document.getElementById('convert-format-btn'); + const clearBtn = document.getElementById('clear-converter-btn'); + const converterInput = document.getElementById('converter-input'); + const formatSelect = document.getElementById('format-select'); + const outputContainer = document.getElementById('converter-output-container'); + const outputElement = document.getElementById('converter-output'); + + const clearAll = () => { + converterInput.value = ''; + outputElement.textContent = ''; + outputContainer.style.display = 'none'; + formatSelect.selectedIndex = 0; + showToast('Converter form cleared!', 'info'); + }; + + convertBtn.addEventListener('click', async () => { + const inputText = converterInput.value; + const targetFormat = formatSelect.value; + + if (!inputText.trim()) { + showToast('Please enter a prompt to convert.', 'error'); + return; + } + + const systemPrompt = `You are a data format converter. Convert the following text into the ${targetFormat} format. Provide only the converted text as a raw string, without any additional explanation or markdown code fences (e.g., \`\`\`json). Text to convert:\n\n${inputText}`; + + const originalButtonHtml = convertBtn.innerHTML; + convertBtn.disabled = true; + convertBtn.innerHTML = ` + + + + + Converting... + `; + outputContainer.style.display = 'block'; + outputElement.textContent = `Converting to ${targetFormat}...`; + + try { + const convertedText = await callGeminiApi(systemPrompt); + outputElement.textContent = convertedText; + showToast('Format converted successfully!', 'success'); + } catch (error) { + console.error('Conversion error:', error); + outputElement.textContent = `Error: ${error.message}`; + showToast('Failed to convert the format.', 'error'); + } finally { + convertBtn.disabled = false; + convertBtn.innerHTML = originalButtonHtml; + } + }); + + clearBtn.addEventListener('click', clearAll); +} + diff --git a/vision/sample-apps/V-Start/src/features/enhancer.js b/vision/sample-apps/V-Start/src/features/enhancer.js new file mode 100644 index 0000000..9a58ba2 --- /dev/null +++ b/vision/sample-apps/V-Start/src/features/enhancer.js @@ -0,0 +1,88 @@ +/* + * Copyright 2025 Google LLC + * + * 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 + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { callGeminiApi } from '../api.js'; +import { showToast, copyToClipboard } from '../ui.js'; + +export async function getEnhancerContent() { + try { + const response = await fetch('/src/features/templates/enhancer.html'); + if (!response.ok) { + throw new Error(`Network response was not ok: ${response.statusText}`); + } + return await response.text(); + } catch (error) { + console.error('Failed to fetch enhancer template:', error); + return '
    Error loading content. Please check the console for details.
    '; + } +} + +// --- INITIALIZATION AND LOGIC --- + +export function initEnhancer() { + window.copyToClipboard = copyToClipboard; + + const enhanceBtn = document.getElementById('enhance-prompt-btn'); + const clearBtn = document.getElementById('clear-enhancer-btn'); + const enhancerInput = document.getElementById('enhancer-input'); + const outputContainer = document.getElementById('enhancer-output-container'); + const outputElement = document.getElementById('enhancer-output'); + + clearBtn.addEventListener('click', () => { + enhancerInput.value = ''; + outputContainer.classList.add('hidden'); + outputElement.textContent = ''; + showToast('Enhancer form cleared!', 'info'); + }); + + enhanceBtn.addEventListener('click', async () => { + const originalPrompt = enhancerInput.value; + + if (!originalPrompt.trim()) { + showToast('Please enter a prompt to enhance.', 'error'); + return; + } + + const systemPrompt = `You are a world-class video director and an expert prompt engineer for Google's Veo model. Your task is to significantly enhance and improve the following prompt. Make it more cinematic, detailed, descriptive, and effective for an AI video generation model. Add specific details about camera work, lighting, mood, and visual style. Here is the prompt to improve: "${originalPrompt}" + +Output ONLY the final, enhanced prompt string, without any introduction, explanation, or quotation marks.`; + + const originalButtonHtml = enhanceBtn.innerHTML; + enhanceBtn.disabled = true; + enhanceBtn.innerHTML = ` + + + + + Enhancing... + `; + outputContainer.classList.remove('hidden'); + outputElement.textContent = 'Generating a more cinematic version...'; + + try { + const enhancedPrompt = await callGeminiApi(systemPrompt); + outputElement.textContent = enhancedPrompt; + showToast('Prompt enhanced successfully!', 'success'); + } catch (error) { + console.error('Enhancement error:', error); + outputElement.textContent = `Error: ${error.message}`; + showToast('Failed to enhance the prompt.', 'error'); + } finally { + enhanceBtn.disabled = false; + enhanceBtn.innerHTML = originalButtonHtml; + } + }); +} diff --git a/vision/sample-apps/V-Start/src/features/eval.js b/vision/sample-apps/V-Start/src/features/eval.js new file mode 100644 index 0000000..8b83fff --- /dev/null +++ b/vision/sample-apps/V-Start/src/features/eval.js @@ -0,0 +1,384 @@ +/* + * Copyright 2025 Google LLC + * + * 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 + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { showToast } from '../ui.js'; + +// --- MODULE-LEVEL STATE --- +const questions = [ + { + id: 'q_quality_comparison', + text: 'How would you rate the overall quality of Video A compared to Video B?', + options: [ + 'Video A is higher quality', + 'Video B is higher quality', + 'They are about the same quality' + ] + } +]; + +let evaluationPairs = []; +let currentPairIndex = 0; +let evaluationResults = []; +let isPreloadedStudy = false; + +// --- DATA FETCHING --- +async function fetchVeoYouTubeStudy() { + try { + const response = await fetch('/data/veo-youtube-study.json'); + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + return await response.json(); + } catch (error) { + console.error("Error fetching YouTube study data:", error); + showToast("Could not load the YouTube study. Please try again later.", "error"); + return []; + } +} + +// --- HTML TEMPLATE --- + +export async function getEvalContent() { + try { + const response = await fetch('/src/features/templates/eval.html'); + if (!response.ok) { + throw new Error(`Network response was not ok: ${response.statusText}`); + } + return await response.text(); + } catch (error) { + console.error('Failed to fetch eval template:', error); + return '
    Error loading content. Please check the console for details.
    '; + } +} + +// --- MAIN INITIALIZATION AND LOGIC --- + +export function initEval() { + const choiceSection = document.getElementById('choice-section'); + const setupSection = document.getElementById('setup-section'); + const mainEvalContent = document.getElementById('main-eval-content'); + const thankYouSection = document.getElementById('thank-you-section'); + + const participateButton = document.getElementById('participate-button'); + const createButton = document.getElementById('create-button'); + const backButtonCreator = document.getElementById('back-button-creator'); + const startEvalButtonCreator = document.getElementById('start-eval-button-creator'); + const submitButton = document.getElementById('submit-vote'); + const previousButton = document.getElementById('previous-pair'); + const backToChoiceButton = document.getElementById('back-to-choice-button'); + const backToMenuFinal = document.getElementById('back-to-menu-final'); + const infoIcon = document.getElementById('info-icon'); + const veoStudyDetails = document.getElementById('veo-study-details'); + + const showSection = (sectionId) => { + [choiceSection, setupSection, mainEvalContent, thankYouSection].forEach(section => { + section.style.display = section.id === sectionId ? 'block' : 'none'; + }); + }; + + const resetState = () => { + currentPairIndex = 0; + evaluationResults = []; + evaluationPairs = []; + isPreloadedStudy = false; + showSection('choice-section'); + }; + + const startEvaluation = () => { + if (isPreloadedStudy) { + document.getElementById('study-title').textContent = 'Veo Prompt Format Evaluation'; + document.getElementById('study-subtitle').textContent = 'Comparing JSON vs Plain Text prompts'; + } else { + document.getElementById('study-title').textContent = 'A/B Video Evaluation'; + document.getElementById('study-subtitle').textContent = 'Comparing Group A vs Group B'; + } + + showSection('main-eval-content'); + renderQuestions(); + loadVideoPair(currentPairIndex); + }; + + const renderQuestions = () => { + const questionsContainer = document.getElementById('questions-container'); + questionsContainer.innerHTML = ''; + questions.forEach(q => { + const optionsHtml = q.options.map(option => ` + + `).join(''); + questionsContainer.innerHTML += `

    ${q.text}

    ${optionsHtml}
    `; + }); + }; + + const loadVideoPair = (index) => { + if (index >= evaluationPairs.length) { + showResultsAnalysis(); + return; + } + + previousButton.disabled = (index === 0); + document.getElementById('progress-counter').textContent = `Pair ${index + 1} of ${evaluationPairs.length}`; + submitButton.disabled = true; + + const pair = evaluationPairs[index]; + const isBFirst = document.getElementById('randomize-pairs-checkbox')?.checked ? Math.random() > 0.5 : false; + + const iframeA = document.getElementById('video-a'); + const iframeB = document.getElementById('video-b'); + const videoTagA = document.getElementById('video-a-tag'); + const videoTagB = document.getElementById('video-b-tag'); + + let videoSrcA = isBFirst ? pair.videoB : pair.videoA; + let videoSrcB = isBFirst ? pair.videoA : pair.videoB; + + let readyCount = 0; + const onReady = () => { + readyCount++; + if (readyCount === 2) { + submitButton.disabled = false; + } + }; + + if (isPreloadedStudy) { // YouTube Study + iframeA.style.display = 'block'; + iframeB.style.display = 'block'; + videoTagA.style.display = 'none'; + videoTagB.style.display = 'none'; + + iframeA.src = `${videoSrcA}?autoplay=1&mute=1&loop=1&playlist=${videoSrcA.split('/').pop()}`; + iframeB.src = `${videoSrcB}?autoplay=1&mute=1&loop=1&playlist=${videoSrcB.split('/').pop()}`; + iframeA.onload = onReady; + iframeB.onload = onReady; + + iframeA.dataset.group = isBFirst ? 'json' : 'plain'; + iframeB.dataset.group = isBFirst ? 'plain' : 'json'; + } else { // User-created study + iframeA.style.display = 'none'; + iframeB.style.display = 'none'; + videoTagA.style.display = 'block'; + videoTagB.style.display = 'block'; + + if (typeof videoSrcA === 'string' && videoSrcA.startsWith('http')) { + videoSrcA = `/api/proxy-video?url=${encodeURIComponent(videoSrcA)}`; + } + if (typeof videoSrcB === 'string' && videoSrcB.startsWith('http')) { + videoSrcB = `/api/proxy-video?url=${encodeURIComponent(videoSrcB)}`; + } + + videoTagA.src = videoSrcA; + videoTagB.src = videoSrcB; + videoTagA.oncanplay = onReady; + videoTagB.oncanplay = onReady; + videoTagA.load(); + videoTagB.load(); + + videoTagA.dataset.group = isBFirst ? 'B' : 'A'; + videoTagB.dataset.group = isBFirst ? 'A' : 'B'; + } + + document.querySelectorAll('input[type="radio"]').forEach(radio => { radio.checked = false; }); + }; + + const handleSubmit = () => { + const selectedOption = document.querySelector(`input[name="${questions[0].id}"]:checked`); + if (!selectedOption) { + showToast('Please select an answer.', 'error'); + return; + } + + const videoTagA = document.getElementById('video-a-tag'); + const iframeA = document.getElementById('video-a'); + + const newResult = { + pairId: currentPairIndex + 1, + assignment: { + videoA_group: isPreloadedStudy ? iframeA.dataset.group : videoTagA.dataset.group, + videoB_group: isPreloadedStudy ? document.getElementById('video-b').dataset.group : document.getElementById('video-b-tag').dataset.group + }, + response: selectedOption.value + }; + + const resultIndex = evaluationResults.findIndex(r => r.pairId === newResult.pairId); + if (resultIndex >= 0) { + evaluationResults[resultIndex] = newResult; + } else { + evaluationResults.push(newResult); + } + + currentPairIndex++; + loadVideoPair(currentPairIndex); + }; + + const handlePrevious = () => { + if (currentPairIndex > 0) { + currentPairIndex--; + loadVideoPair(currentPairIndex); + + const previousResult = evaluationResults.find(r => r.pairId === currentPairIndex + 1); + if (previousResult) { + const radioButton = document.querySelector(`input[name="${questions[0].id}"][value="${previousResult.response}"]`); + if (radioButton) radioButton.checked = true; + } + } + }; + + const calculateResults = () => { + const stats = { totalPairs: evaluationResults.length, groupA_wins: 0, groupB_wins: 0, ties: 0 }; + + evaluationResults.forEach(result => { + if (result.response === 'Video A is higher quality') { + if (result.assignment.videoA_group === 'plain' || result.assignment.videoA_group === 'A') stats.groupA_wins++; + else stats.groupB_wins++; + } else if (result.response === 'Video B is higher quality') { + if (result.assignment.videoB_group === 'plain' || result.assignment.videoB_group === 'A') stats.groupA_wins++; + else stats.groupB_wins++; + } else { + stats.ties++; + } + }); + + if (isPreloadedStudy) { + stats.plain_wins = stats.groupA_wins; + stats.json_wins = stats.groupB_wins; + } + + return stats; + }; + + const generateInterpretation = (stats) => { + if (stats.totalPairs === 0) return '

    No results to analyze.

    '; + const tiePercent = (stats.ties / stats.totalPairs) * 100; + + if (isPreloadedStudy) { + const jsonPercent = (stats.json_wins / stats.totalPairs) * 100; + const plainPercent = (stats.plain_wins / stats.totalPairs) * 100; + if (tiePercent > 50) return `

    Conclusion: The model appears robust to prompt format. Both formats produced comparable results.

    `; + if (stats.json_wins > stats.plain_wins) return `

    Conclusion: JSON-structured prompts performed better. Videos from JSON prompts were preferred in ${jsonPercent.toFixed(1)}% of evaluations.

    `; + if (stats.plain_wins > stats.json_wins) return `

    Conclusion: Plain text prompts performed better. Videos from plain text prompts were preferred in ${plainPercent.toFixed(1)}% of evaluations.

    `; + return `

    Conclusion: The results were tied. Both formats performed equally well.

    `; + } else { + const groupAPercent = (stats.groupA_wins / stats.totalPairs) * 100; + const groupBPercent = (stats.groupB_wins / stats.totalPairs) * 100; + return `

    Results: Group A was preferred ${groupAPercent.toFixed(1)}% of the time, and Group B was preferred ${groupBPercent.toFixed(1)}% of the time.

    `; + } + }; + + const showResultsAnalysis = () => { + showSection('thank-you-section'); + const stats = calculateResults(); + + const chartContainer = document.getElementById('results-chart'); + chartContainer.innerHTML = ''; + + const data = isPreloadedStudy + ? [['Format', 'Wins'], ['JSON', stats.json_wins], ['Plain Text', stats.plain_wins], ['Tie', stats.ties]] + : [['Group', 'Wins'], ['Group A', stats.groupA_wins], ['Group B', stats.groupB_wins], ['Tie', stats.ties]]; + + data.slice(1).forEach(row => { + const percent = stats.totalPairs > 0 ? ((row[1] / stats.totalPairs) * 100).toFixed(1) : 0; + const bar = document.createElement('div'); + bar.innerHTML = ` +
    + ${row[0]} + ${row[1]}/${stats.totalPairs} (${percent}%) +
    +
    ${percent}%
    + `; + bar.className = 'mb-4'; + chartContainer.appendChild(bar); + }); + + document.getElementById('interpretation-text').innerHTML = generateInterpretation(stats); + + const downloadButton = document.getElementById('download-results'); + const csvContent = "pair_id,video_a_group,video_b_group,response\n" + evaluationResults.map(r => + [r.pairId, r.assignment.videoA_group, r.assignment.videoB_group, `"${r.response}"`].join(',') + ).join('\n'); + + const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' }); + const url = URL.createObjectURL(blob); + + downloadButton.onclick = () => { + const a = document.createElement('a'); + a.href = url; + a.download = isPreloadedStudy ? 'Veo_YouTube_Study_Results.csv' : 'AB_Eval_Results.csv'; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + showToast('Results downloaded successfully!', 'success'); + }; + }; + + const validateAndLoadCustomPairs = () => { + const groupAFiles = Array.from(document.getElementById('group-a-files').files); + const groupAUrls = document.getElementById('group-a-urls').value.split('\n').filter(url => url.trim() !== ''); + const groupBFiles = Array.from(document.getElementById('group-b-files').files); + const groupBUrls = document.getElementById('group-b-urls').value.split('\n').filter(url => url.trim() !== ''); + + const groupAVideos = [...groupAUrls, ...groupAFiles.map(file => URL.createObjectURL(file))]; + const groupBVideos = [...groupBUrls, ...groupBFiles.map(file => URL.createObjectURL(file))]; + + if (groupAVideos.length > 0 && groupAVideos.length === groupBVideos.length) { + startEvalButtonCreator.disabled = false; + evaluationPairs = groupAVideos.map((videoA, index) => ({ videoA, videoB: groupBVideos[index] })); + } else { + startEvalButtonCreator.disabled = true; + } + }; + + // --- Event Listeners --- + infoIcon.addEventListener('click', (e) => { + e.stopPropagation(); + veoStudyDetails.classList.toggle('hidden'); + }); + + participateButton.addEventListener('click', async (e) => { + if (e.target !== infoIcon && !infoIcon.contains(e.target)) { + veoStudyDetails.classList.add('hidden'); + const data = await fetchVeoYouTubeStudy(); + if (data && data.length > 0) { + evaluationPairs = data; + isPreloadedStudy = true; + startEvaluation(); + } + } + }); + + createButton.addEventListener('click', () => { + veoStudyDetails.classList.add('hidden'); + showSection('setup-section'); + }); + + backButtonCreator.addEventListener('click', resetState); + backToChoiceButton.addEventListener('click', resetState); + backToMenuFinal.addEventListener('click', resetState); + + document.getElementById('group-a-files').addEventListener('change', validateAndLoadCustomPairs); + document.getElementById('group-a-urls').addEventListener('input', validateAndLoadCustomPairs); + document.getElementById('group-b-files').addEventListener('change', validateAndLoadCustomPairs); + document.getElementById('group-b-urls').addEventListener('input', validateAndLoadCustomPairs); + + startEvalButtonCreator.addEventListener('click', () => { + isPreloadedStudy = false; // Make sure to set this flag + startEvaluation(); + }); + submitButton.addEventListener('click', handleSubmit); + previousButton.addEventListener('click', handlePrevious); + + resetState(); +} diff --git a/vision/sample-apps/V-Start/src/features/gallery.js b/vision/sample-apps/V-Start/src/features/gallery.js new file mode 100644 index 0000000..9c1e1c2 --- /dev/null +++ b/vision/sample-apps/V-Start/src/features/gallery.js @@ -0,0 +1,227 @@ +/* + * Copyright 2025 Google LLC + * + * 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 + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { copyToClipboard, showToast } from '../ui.js'; + +// --- GALLERY DATA --- +const galleryData = { + subject: { + title: "Subject, Scene & Action", + examples: [ + { + title: "Complex Subject", + prompt: "A hyper-realistic, cinematic portrait of a wise, androgynous shaman of indeterminate age. Their weathered skin is etched with intricate, bioluminescent circuit-like tattoos that pulse with a soft, cyan light. They are draped in ceremonial robes woven from dark moss and shimmering, metallic fiber-optic threads. In one hand, they hold a gnarled wooden staff entwined with glowing energy conduits and topped with a floating, crystalline artifact. Perched on their shoulder is a small, mechanical owl with holographic wings and camera-lens eyes that blink with a soft, red light. Their expression is serene and ancient, eyes holding a deep, knowing look.", + youtubeId: "GKOpOcs8IF8" + }, + { + title: "Portrait", + prompt: "A cinematic close-up portrait of a woman sitting in a café at night, with a very shallow depth of field. Her face is in sharp focus, while the city lights outside the window behind her are transformed into soft, beautiful bokeh circles.", + youtubeId: "Ol66pK2N7L0" + }, + { + title: "Sequencing of Actions", + prompt: "A gloved hand carefully slices open the spine of an ancient, leather-bound book with a scalpel. The hand then delicately extracts a tiny, metallic data chip hidden within the binding. The character's eyes, previously focused and calm, widen in a flash of alarm as a floorboard creaks off-screen. They quickly palm the chip, their head snapping up to scan the dimly lit room, their body tense and listening for any other sound.", + youtubeId: "EbCAqMF2DBo" + }, + { + title: "Emotional Expression", + prompt: "A close-up shot of a man's face, approximately 30-40 years old, with short brown hair and a beard, wearing a gray t-shirt. The man is seated at a table, looking at a document, and expressing a range of emotions, including surprise, sadness and frustration.", + youtubeId: "lYMjzZHykCo" + } + ] + }, + camera: { + title: "Camera Work", + examples: [ + { + title: "Low Angle Shot", + prompt: "Dynamic low-angle shot of a basketball player soaring for a slam dunk, stadium lights flaring.", + youtubeId: "zCZ91E7tPeE" + }, + { + title: "Drone Shot", + prompt: "Sweeping aerial drone shot flying over a tropical island chain.", + youtubeId: "gvPtt5f-kKc" + }, + { + title: "Zoom In Shot", + prompt: "A slow, dramatic zoom in on a mysterious, ancient compass lying on a dusty map. The camera starts wide, showing the map and a flickering candle, then smoothly zooms in until the intricate, glowing symbols on the compass face fill the entire frame.", + youtubeId: "izn8VHHFy3c" + }, + { + title: "Over-the-Shoulder", + prompt: "An over-the-shoulder shot from behind a seasoned detective, looking at a nervous informant sitting across a table in a dimly lit interrogation room. The focus is on the informant's expressive, anxious face.", + youtubeId: "z73bvXtUC_0" + }, + { + title: "Rack Focus", + prompt: "A medium shot of a detective's hand in the foreground, holding a single, spent bullet casing. The camera then performs a slow rack focus, shifting from the casing to reveal the anxious face of a witness in the background, now in sharp focus.", + youtubeId: "-p6W4mCYuvc" + }, + { + title: "Handheld Camera", + prompt: "An intense handheld camera shot during a chaotic marketplace chase. The camera struggles to keep up, with jerky movements and quick, unstable pans as it follows the character weaving through dense crowds and knocking over stalls.", + youtubeId: "csnE4FNogJQ" + } + ] + }, + style: { + title: "Visual & Temporal Styles", + examples: [ + { + title: "Anime Style", + prompt: "A dynamic scene in a vibrant Japanese anime style. A magical girl with silver hair and glowing blue eyes walks in a forest The style features sharp lines, bright, saturated colors, and expressive.", + youtubeId: "vu2ZFw-9ZMI" + }, + { + title: "Lens Flare", + prompt: "A cinematic shot of a couple embracing on a beach at sunset. As the sun dips below the horizon behind them, a warm, anamorphic lens flare streaks horizontally across the frame, adding a romantic and nostalgic feeling to the scene.", + youtubeId: "jY3gQS73614" + }, + { + title: "Jump Cut", + prompt: "A person sitting in the same position but wearing different outfits, with sharp jump cuts between each outfit change. The background should stay static and the person should reappear instantly in the new outfit, creating a fast-paced, rhythmic jump cut effect. The lighting and framing should remain consistent to emphasize the sudden changes.", + youtubeId: "d-cGj3kAnsQ" + }, + { + title: "Time-Lapse", + prompt: "A time-lapse of a bustling city skyline as day transitions to night. The camera is static. Watch as the sun sets, casting long shadows, and the city lights begin to twinkle on, with streaks of car headlights moving along the streets below.", + youtubeId: "CT0PIze9w0Y" + }, + { + title: "Cyberpunk Lighting", + prompt: "A hyper-realistic, cinematic shot of a rain-slicked cyberpunk alleyway at midnight. Pulsating pink and teal neon signs reflect off puddles on the ground, illuminating the steam rising from a street vendor's cart.", + youtubeId: "bKIZ-pdCJnA" + }, + { + title: "Vintage Style", + prompt: "A vintage 1920s street scene, sepia toned, film grain, with characters in period attire.", + youtubeId: "WJwj6y7p8SI" + } + ] + }, + audio: { + title: "Audio", + examples: [ + { + title: "Ambient Noise", + prompt: "A static, wide shot of a vast, ancient library at night. The only sounds are the soft, rhythmic ticking of a grandfather clock, the gentle rustle of turning pages, and the faint sound of wind howling outside the tall, arched windows.", + youtubeId: "WsXXBDhO7l0" + }, + { + title: "Dialogue", + prompt: "A medium shot in a dimly lit interrogation room. The seasoned detective says: Your story has holes. The nervous informant, sweating under a single bare bulb, replies: I'm telling you everything I know. The only other sounds are the slow, rhythmic ticking of a wall clock and the faint sound of rain against the window.", + youtubeId: "yGcMvkFK9Zo" + } + ] + } +}; + +export async function getGalleryContent() { + try { + const response = await fetch('/src/features/templates/gallery.html'); + if (!response.ok) { + throw new Error(`Network response was not ok: ${response.statusText}`); + } + return await response.text(); + } catch (error) { + console.error('Failed to fetch gallery template:', error); + return '
    Error loading content. Please check the console for details.
    '; + } +} + +export function initGallery() { + window.copyToClipboard = copyToClipboard; + + const catContainer = document.getElementById('gallery-categories'); + const slidersContainer = document.getElementById('gallery-sliders-container'); + catContainer.innerHTML = ''; + slidersContainer.innerHTML = ''; + + Object.keys(galleryData).forEach(key => { + const button = document.createElement('button'); + button.className = 'gallery-category-button py-2 px-5 rounded-full font-semibold shadow-sm bg-gray-200 hover:bg-gray-300 dark:bg-gray-700 dark:hover:bg-gray-600 text-gray-700 dark:text-gray-300 transition-colors duration-200'; + button.textContent = galleryData[key].title; + button.onclick = () => showGalleryCategory(key); + catContainer.appendChild(button); + slidersContainer.appendChild(createSlider(key, galleryData[key])); + }); + + showGalleryCategory('subject'); // Show the first category by default +} + +function createSlider(categoryId, categoryData) { + const sliderWrapper = document.createElement('div'); + sliderWrapper.id = `gallery-${categoryId}`; + sliderWrapper.className = 'gallery-examples relative'; + sliderWrapper.innerHTML = ` +
    + ${categoryData.examples.map((ex, index) => { + const promptId = `gallery-prompt-${categoryId}-${index}`; + // Direct YouTube embed URL, autoplay=0 (no auto-play), mute=1 (no sound), controls=1 + const youtubeEmbedUrl = ex.youtubeId ? + `https://www.youtube.com/embed/${ex.youtubeId}?autoplay=0&mute=1&controls=1&modestbranding=1&rel=0` : + ''; // Empty if no YouTube ID + + return ` +
    +
    + ${youtubeEmbedUrl ? ` + + ` : ` +
    + No video available +
    + `} +
    +
    +

    ${ex.title}

    +

    ${ex.prompt}

    +
    + +
    +
    +
    `; + }).join('')} +
    `; + return sliderWrapper; +} + +function showGalleryCategory(category) { + document.querySelectorAll('.gallery-examples').forEach(el => el.style.display = 'none'); + document.querySelectorAll('.gallery-category-button').forEach(el => { + el.classList.remove('bg-indigo-600', 'hover:bg-indigo-700', 'text-white'); + el.classList.add('bg-gray-200', 'hover:bg-gray-300', 'dark:bg-gray-700', 'dark:hover:bg-gray-600', 'text-gray-700', 'dark:text-gray-300'); + }); + + const activeBtn = Array.from(document.querySelectorAll('.gallery-category-button')).find(btn => btn.textContent === galleryData[category].title); + if (activeBtn) { + document.getElementById(`gallery-${category}`).style.display = 'block'; + activeBtn.classList.remove('bg-gray-200', 'hover:bg-gray-300', 'dark:bg-gray-700', 'dark:hover:bg-gray-600', 'text-gray-700', 'dark:text-gray-300'); + activeBtn.classList.add('bg-indigo-600', 'hover:bg-indigo-700', 'text-white'); + } +} diff --git a/vision/sample-apps/V-Start/src/features/generator.js b/vision/sample-apps/V-Start/src/features/generator.js new file mode 100644 index 0000000..a3401b8 --- /dev/null +++ b/vision/sample-apps/V-Start/src/features/generator.js @@ -0,0 +1,420 @@ +/* + * Copyright 2025 Google LLC + * + * 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 + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { callGeminiApi } from '../api.js'; +import { showToast, copyToClipboard } from '../ui.js'; + +// --- DATA FOR THE FORM --- +const textVideoSelectData = { + camera_angle: { label: "Camera Angles", options: ["Eye-Level Shot", "Low-Angle Shot", "High-Angle Shot", "Bird's-Eye View", "Top-Down Shot", "Worm's-Eye View", "Dutch Angle", "Canted Angle", "Close-Up", "Extreme Close-Up", "Medium Shot", "Full Shot", "Long Shot", "Wide Shot", "Establishing Shot", "Over-the-Shoulder Shot", "Point-of-View (POV) Shot"] }, + camera_movement: { label: "Camera Movements", options: ["Static Shot (or fixed)", "Pan (left)", "Pan (right)", "Tilt (up)", "Tilt (down)", "Dolly (In)", "Dolly (Out)", "Zoom (In)", "Zoom (Out)", "Truck (Left)", "Truck (Right)", "Pedestal (Up)", "Pedestal (Down)", "Crane Shot", "Aerial Shot", "Drone Shot", "Handheld", "Shaky Cam", "Whip Pan", "Arc Shot"] }, + lens_effect: { label: "Lens & Optical Effects", options: ["Wide-Angle Lens (e.g., 24mm)", "Telephoto Lens (e.g., 85mm)", "Shallow Depth of Field", "Bokeh", "Deep Depth of Field", "Lens Flare", "Rack Focus", "Fisheye Lens Effect", "Vertigo Effect (Dolly Zoom)"] }, + visual_style: { label: "Visual Style & Aesthetics", options: ["Photorealistic", "Cinematic", "Vintage", "Japanese anime style", "Claymation style", "Stop-motion animation", "In the style of Van Gogh", "Surrealist painting", "Monochromatic black and white", "Vibrant and saturated", "Film noir style", "High-key lighting", "Low-key lighting", "Golden hour glow", "Volumetric lighting", "Backlighting to create a silhouette"] }, + temporal_element: { label: "Temporal Elements", options: ["Slow-motion", "Fast-paced action", "Time-lapse", "Hyperlapse", "Pulsating light", "Rhythmic movement"] }, + sound_effects: { label: "Sound Effects & Ambience", options: ["Sound of a phone ringing", "Water splashing", "Soft house sounds", "Ticking clock", "City traffic and sirens", "Waves crashing", "Quiet office hum"] } +}; + +// --- HELPER FUNCTIONS TO BUILD THE FORM --- +function createInputComponent(id, labelText, placeholder) { + const container = document.createElement('div'); + const label = document.createElement('label'); + label.htmlFor = id; + label.className = 'block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1'; + label.textContent = labelText; + const input = document.createElement('input'); + input.type = 'text'; + input.id = id; + input.autocomplete = 'off'; + input.className = 'w-full p-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100'; + input.placeholder = placeholder; + container.appendChild(label); + container.appendChild(input); + return container; +} + +function createSelectComponent(id, data) { + const container = document.createElement('div'); + const label = document.createElement('label'); + label.htmlFor = id; + label.className = 'block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1'; + label.textContent = data.label; + const select = document.createElement('select'); + select.id = id; + select.className = 'w-full p-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100'; + + select.innerHTML = `` + data.options.map(o => ``).join(''); + + const customInput = document.createElement('input'); + customInput.type = 'text'; + customInput.id = `${id}-custom`; + customInput.className = 'custom-input w-full p-2 mt-2 border border-gray-300 dark:border-gray-600 rounded-md hidden bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100'; + customInput.placeholder = 'Enter custom value...'; + select.onchange = () => { + customInput.style.display = select.value === 'custom' ? 'block' : 'none'; + }; + container.appendChild(label); + container.appendChild(select); + container.appendChild(customInput); + return container; +} + +function populateTextToVideoForm() { + const container = document.querySelector('#text-to-video-content .grid'); + if (!container) return; + container.innerHTML = ''; + const elements = [ + { type: 'input', id: 'subject-input', label: 'Subject', placeholder: 'e.g., A dog' }, + { type: 'input', id: 'action-input', label: 'Action', placeholder: 'e.g., running' }, + { type: 'input', id: 'scene-input', label: 'Scene / Context', placeholder: 'e.g., on a sunny beach' }, + ...Object.keys(textVideoSelectData).map(key => ({ type: 'select', id: key, data: textVideoSelectData[key] })), + { type: 'input', id: 'dialogue-input', label: 'Dialogue', placeholder: `e.g., Let's go!` } + ]; + elements.forEach(el => { + if (el.type === 'input') container.appendChild(createInputComponent(el.id, el.label, el.placeholder)); + else if (el.type === 'select') container.appendChild(createSelectComponent(el.id, el.data)); + }); +} + +function populateImageToVideoForm() { + const container = document.getElementById('image-to-video-fields'); + if (!container) return; + container.innerHTML = ''; + const elements = [ + { type: 'input', id: 'image-action-input', label: 'Action', placeholder: 'e.g., snow falling gently' }, + { type: 'input', id: 'image-scene-input', label: 'Scene / Context', placeholder: 'e.g., steam rising from a coffee cup' }, + ...Object.keys(textVideoSelectData).map(key => ({ type: 'select', id: `image-${key}`, data: textVideoSelectData[key] })), + { type: 'input', id: 'image-dialogue-input', label: 'Dialogue', placeholder: `e.g., a character sighs` } + ]; + elements.forEach(el => { + if (el.type === 'input') container.appendChild(createInputComponent(el.id, el.label, el.placeholder)); + else if (el.type === 'select') container.appendChild(createSelectComponent(el.id, el.data)); + }); +} + +export async function getGeneratorContent() { + try { + const response = await fetch('/src/features/templates/generator.html'); + if (!response.ok) { + throw new Error(`Network response was not ok: ${response.statusText}`); + } + return await response.text(); + } catch (error) { + console.error('Failed to fetch generator template:', error); + return '
    Error loading content. Please check the console for details.
    '; + } +} + +async function generateLongerPrompt(type) { + const isText = type === 'text'; + const shortPromptElement = document.getElementById(isText ? 'text-prompt-output' : 'image-prompt-output'); + const longPromptSection = document.getElementById(isText ? 'text-long-prompt-section' : 'image-long-prompt-section'); + const longPromptOutputElement = document.getElementById(isText ? 'text-long-prompt-output' : 'image-long-prompt-output'); + const generateBtn = document.getElementById(isText ? 'generate-longer-text-prompt-btn' : 'generate-longer-image-prompt-btn'); + + const initialPrompt = shortPromptElement.textContent; + if (!initialPrompt) { + showToast("Initial prompt is missing.", 'error'); + return; + } + + const originalButtonHtml = generateBtn.innerHTML; + generateBtn.disabled = true; + generateBtn.innerHTML = ` + + + + + Enhancing... + `; + + longPromptSection.classList.remove('hidden'); + longPromptOutputElement.textContent = "Enhancing prompt with more cinematic detail..."; + + const systemPrompt = `You are a Google Veo3 prompt engineer. Enhance this video prompt to be more cinematic and detailed: "${initialPrompt}". Output ONLY the enhanced video prompt text - no analysis, no markdown, no asterisks, no brackets, no explanations. Just the pure enhanced prompt text that will be sent to Veo3.`; + + try { + const result = await callGeminiApi(systemPrompt); + longPromptOutputElement.textContent = result; + generateBtn.classList.add('hidden'); + } catch (error) { + showToast(error.message, 'error'); + longPromptOutputElement.textContent = `Error: ${error.message}`; + } finally { + if (!generateBtn.classList.contains('hidden')) { + generateBtn.disabled = false; + generateBtn.innerHTML = originalButtonHtml; + } + } +} + +function clearForm(type) { + const prefix = type === 'image' ? 'image-' : ''; + + const inputs = document.querySelectorAll(`#${type}-to-video-content input[type="text"], #${type}-to-video-content textarea`); + inputs.forEach(input => input.value = ''); + + const selects = document.querySelectorAll(`#${type}-to-video-content select`); + selects.forEach(select => { + select.selectedIndex = 0; + const customInput = document.getElementById(`${select.id}-custom`); + if (customInput) { + customInput.style.display = 'none'; + } + }); + + if (type === 'image') { + const imageUpload = document.getElementById('image-upload'); + const imagePreviewContainer = document.getElementById('image-preview-container'); + if(imageUpload) imageUpload.value = ''; + if(imagePreviewContainer) imagePreviewContainer.classList.add('hidden'); + document.getElementById('image-prompt-output-container').classList.add('hidden'); + document.getElementById('image-long-prompt-section').classList.add('hidden'); + } else { + document.getElementById('text-prompt-output-container').classList.add('hidden'); + document.getElementById('text-long-prompt-section').classList.add('hidden'); + } + + showToast(`${type.charAt(0).toUpperCase() + type.slice(1)} form cleared!`, 'info'); +} + +export function initGenerator() { + window.copyToClipboard = copyToClipboard; + + populateTextToVideoForm(); + populateImageToVideoForm(); + + const textTab = document.getElementById('text-to-video-tab'); + const imageTab = document.getElementById('image-to-video-tab'); + const textContent = document.getElementById('text-to-video-content'); + const imageContent = document.getElementById('image-to-video-content'); + const generateTextBtn = document.getElementById('generate-text-prompt-btn'); + const generateLongerTextBtn = document.getElementById('generate-longer-text-prompt-btn'); + const generateImageBtn = document.getElementById('generate-image-prompt-btn'); + const generateLongerImageBtn = document.getElementById('generate-longer-image-prompt-btn'); + const imageUpload = document.getElementById('image-upload'); + const imagePreviewContainer = document.getElementById('image-preview-container'); + const imagePreview = document.getElementById('image-preview'); + + const clearTextFormBtn = document.getElementById('clear-text-form-btn'); + const clearImageFormBtn = document.getElementById('clear-image-form-btn'); + + textTab.addEventListener('click', () => { + textContent.classList.remove('hidden'); + imageContent.classList.add('hidden'); + textTab.classList.add('text-indigo-600', 'dark:text-indigo-400', 'border-indigo-500'); + textTab.classList.remove('text-gray-500', 'dark:text-gray-400', 'border-transparent'); + imageTab.classList.add('text-gray-500', 'dark:text-gray-400', 'border-transparent'); + imageTab.classList.remove('text-indigo-600', 'dark:text-indigo-400', 'border-indigo-500'); + }); + + imageTab.addEventListener('click', () => { + imageContent.classList.remove('hidden'); + textContent.classList.add('hidden'); + imageTab.classList.add('text-indigo-600', 'dark:text-indigo-400', 'border-indigo-500'); + imageTab.classList.remove('text-gray-500', 'dark:text-gray-400', 'border-transparent'); + textTab.classList.add('text-gray-500', 'dark:text-gray-400', 'border-transparent'); + textTab.classList.remove('text-indigo-600', 'dark:text-indigo-400', 'border-indigo-500'); + }); + + imageUpload.addEventListener('change', () => { + const file = imageUpload.files[0]; + if (file) { + const reader = new FileReader(); + reader.onload = (e) => { + imagePreview.src = e.target.result; + imagePreviewContainer.classList.remove('hidden'); + }; + reader.readAsDataURL(file); + } else { + imagePreviewContainer.classList.add('hidden'); + } + }); + + // TEXT TO VIDEO GENERATION + generateTextBtn.addEventListener('click', async () => { + const keywords = []; + ['subject-input', 'action-input', 'scene-input', 'dialogue-input'].forEach(id => { + const element = document.getElementById(id); + if (element && element.value) { + keywords.push(id === 'dialogue-input' ? `A character says: '${element.value}'` : element.value); + } + }); + Object.keys(textVideoSelectData).forEach(key => { + const select = document.getElementById(key); + if (!select) return; + let value = select.value; + if (value === 'custom') { + const customInput = document.getElementById(`${key}-custom`); + value = customInput ? customInput.value : ''; + } + if (value) keywords.push(value); + }); + if (keywords.length === 0) { + showToast("Please provide at least one keyword.", 'error'); + return; + } + + // PROMPT for Veo3 + const systemPrompt = `You are a Google Veo3 prompt engineer. Create a cinematic video prompt using these keywords: [${keywords.join(', ')}]. Synthesize them into a single cohesive video instruction. Output ONLY the video prompt text - no analysis, no markdown, no asterisks, no explanations. Just the pure prompt text that will be sent to Veo3.`; + + const outputContainer = document.getElementById('text-prompt-output-container'); + const outputElement = document.getElementById('text-prompt-output'); + const originalButtonHtml = generateTextBtn.innerHTML; + generateTextBtn.disabled = true; + generateTextBtn.innerHTML = ` + + + + + Generating... + `; + outputContainer.classList.remove('hidden'); + document.getElementById('text-long-prompt-section').classList.add('hidden'); + generateLongerTextBtn.classList.add('hidden'); + outputElement.textContent = 'Processing...'; + try { + const result = await callGeminiApi(systemPrompt); + outputElement.textContent = result; + if (result && !result.toLowerCase().startsWith('error')) { + generateLongerTextBtn.innerHTML = 'Generate Longer Prompt'; + generateLongerTextBtn.classList.remove('hidden'); + } + } catch (error) { + showToast(error.message, 'error'); + outputElement.textContent = `Error: ${error.message}`; + } finally { + generateTextBtn.disabled = false; + generateTextBtn.innerHTML = originalButtonHtml; + } + }); + + // IMAGE TO VIDEO GENERATION + generateImageBtn.addEventListener('click', async () => { + const imageFile = imageUpload.files[0]; + if (!imageFile) { + showToast("Please upload an image first.", 'error'); + return; + } + + // Read the file and ensure we get proper base64 data + const reader = new FileReader(); + reader.onload = async (e) => { + try { + // Get the data URL and extract base64 + const dataUrl = e.target.result; + const base64Data = dataUrl.split(',')[1]; + + // Verify we have valid base64 data + if (!base64Data || typeof base64Data !== 'string') { + showToast("Failed to read image data", 'error'); + console.error('Invalid base64 data:', typeof base64Data); + return; + } + + console.log('Image loaded successfully:', { + fileType: imageFile.type, + fileSize: imageFile.size, + base64Length: base64Data.length + }); + + // Create the image part with proper structure + const imagePart = { + inlineData: { + mimeType: imageFile.type, + data: base64Data + } + }; + + // Collect keywords + const keywords = []; + ['image-action-input', 'image-scene-input', 'image-dialogue-input'].forEach(id => { + const element = document.getElementById(id); + if (element && element.value) { + keywords.push(id === 'image-dialogue-input' ? `A character says: '${element.value}'` : element.value); + } + }); + Object.keys(textVideoSelectData).forEach(key => { + const select = document.getElementById(`image-${key}`); + if (!select) return; + let value = select.value; + if (value === 'custom') { + const customInput = document.getElementById(`image-${key}-custom`); + value = customInput ? customInput.value : ''; + } + if (value) keywords.push(value); + }); + + // PROMPT for Veo3 + const systemPrompt = keywords.length > 0 + ? `You are a Google Veo3 prompt engineer. Based on the uploaded image, create a cinematic video prompt that brings this exact scene to life with motion, incorporating these keywords: [${keywords.join(', ')}]. Output ONLY the video prompt text - no analysis, no markdown, no asterisks, no explanations. Just the pure prompt text that will be sent to Veo3.` + : `You are a Google Veo3 prompt engineer. Based on the uploaded image, create a cinematic video prompt that brings this exact scene to life with motion and animation. Output ONLY the video prompt text - no analysis, no markdown, no asterisks, no explanations. Just the pure prompt text that will be sent to Veo3.`; + + const outputContainer = document.getElementById('image-prompt-output-container'); + const outputElement = document.getElementById('image-prompt-output'); + const originalButtonHtml = generateImageBtn.innerHTML; + + generateImageBtn.disabled = true; + generateImageBtn.innerHTML = ` + + + + + Generating... + `; + + outputContainer.classList.remove('hidden'); + document.getElementById('image-long-prompt-section').classList.add('hidden'); + generateLongerImageBtn.classList.add('hidden'); + outputElement.textContent = 'Processing image...'; + + try { + // Call the API with the image + const result = await callGeminiApi(systemPrompt, [imagePart]); + outputElement.textContent = result; + if (result && !result.toLowerCase().startsWith('error')) { + generateLongerImageBtn.innerHTML = 'Generate Longer Prompt'; + generateLongerImageBtn.disabled = false; + generateLongerImageBtn.classList.remove('hidden'); + } + } catch (error) { + showToast(error.message, 'error'); + outputElement.textContent = `Error: ${error.message}`; + } finally { + generateImageBtn.disabled = false; + generateImageBtn.innerHTML = originalButtonHtml; + } + } catch (error) { + console.error('Error processing image:', error); + showToast('Failed to process image', 'error'); + generateImageBtn.disabled = false; + generateImageBtn.innerHTML = originalButtonHtml; + } + }; + + reader.onerror = () => { + showToast('Failed to read image file', 'error'); + console.error('FileReader error'); + }; + + // Start reading the file + reader.readAsDataURL(imageFile); + }); + + generateLongerTextBtn.addEventListener('click', () => generateLongerPrompt('text')); + generateLongerImageBtn.addEventListener('click', () => generateLongerPrompt('image')); + clearTextFormBtn.addEventListener('click', () => clearForm('text')); + clearImageFormBtn.addEventListener('click', () => clearForm('image')); +} \ No newline at end of file diff --git a/vision/sample-apps/V-Start/src/features/templates/alignment-eval.html b/vision/sample-apps/V-Start/src/features/templates/alignment-eval.html new file mode 100644 index 0000000..3505cef --- /dev/null +++ b/vision/sample-apps/V-Start/src/features/templates/alignment-eval.html @@ -0,0 +1,101 @@ +
    +
    +
    +

    Prompt-Video Alignment Evaluation

    +

    Streamlined 2-step process: Load inputs to automatically create pairs, then start evaluation.

    + +
    +

    Step 1: Load Prompts and Videos (Auto-Pairing)

    +
    +
    + + + +
    + +
    + + +

    OR

    + +
    + +
    + + +
    +
    +
    + + + + + +
    + + + + + + +
    +
    +
    \ No newline at end of file diff --git a/vision/sample-apps/V-Start/src/features/templates/converter.html b/vision/sample-apps/V-Start/src/features/templates/converter.html new file mode 100644 index 0000000..6441b89 --- /dev/null +++ b/vision/sample-apps/V-Start/src/features/templates/converter.html @@ -0,0 +1,42 @@ +
    +
    +
    +

    Prompt Format Converter

    +

    Convert a prompt between plain text, JSON, YAML, or XML.

    +
    +
    + + +
    +
    + + +
    +
    +
    + + +
    + +
    +
    +
    + diff --git a/vision/sample-apps/V-Start/src/features/templates/enhancer.html b/vision/sample-apps/V-Start/src/features/templates/enhancer.html new file mode 100644 index 0000000..68d07df --- /dev/null +++ b/vision/sample-apps/V-Start/src/features/templates/enhancer.html @@ -0,0 +1,30 @@ +
    +
    +
    +

    Prompt Enhancer

    +

    Paste your existing prompt below and let Gemini make it more cinematic and descriptive.

    +
    + + +
    +
    + + +
    + +
    +
    +
    diff --git a/vision/sample-apps/V-Start/src/features/templates/eval.html b/vision/sample-apps/V-Start/src/features/templates/eval.html new file mode 100644 index 0000000..d3ea9f9 --- /dev/null +++ b/vision/sample-apps/V-Start/src/features/templates/eval.html @@ -0,0 +1,98 @@ +
    +
    +

    A/B Video Evaluation

    +

    Participate in existing studies or create your own.

    +
    +
    + + +
    + +
    +
    + + + + + + +
    diff --git a/vision/sample-apps/V-Start/src/features/templates/gallery.html b/vision/sample-apps/V-Start/src/features/templates/gallery.html new file mode 100644 index 0000000..4e50378 --- /dev/null +++ b/vision/sample-apps/V-Start/src/features/templates/gallery.html @@ -0,0 +1,47 @@ + + diff --git a/vision/sample-apps/V-Start/src/features/templates/generator.html b/vision/sample-apps/V-Start/src/features/templates/generator.html new file mode 100644 index 0000000..1100cab --- /dev/null +++ b/vision/sample-apps/V-Start/src/features/templates/generator.html @@ -0,0 +1,85 @@ +
    +
    +
    +

    Prompt Generator

    +

    Build a detailed prompt from scratch by selecting various cinematic elements, or generate one for an uploaded image.

    +
    + +
    + +
    +
    +
    + + +
    + +
    + + +
    +
    +
    + diff --git a/vision/sample-apps/V-Start/src/features/templates/timeline.html b/vision/sample-apps/V-Start/src/features/templates/timeline.html new file mode 100644 index 0000000..7d40cbc --- /dev/null +++ b/vision/sample-apps/V-Start/src/features/templates/timeline.html @@ -0,0 +1,42 @@ +
    +
    +
    +

    Timeline Prompting

    +

    Define what happens at specific moments in your 8-second video. The AI will enhance your descriptions into a cinematic timeline.

    + +
    + +
    +
    + +
    + +
    + +
    + +
    + + +
    + + +
    +
    +
    + diff --git a/vision/sample-apps/V-Start/src/features/timeline.js b/vision/sample-apps/V-Start/src/features/timeline.js new file mode 100644 index 0000000..77c7656 --- /dev/null +++ b/vision/sample-apps/V-Start/src/features/timeline.js @@ -0,0 +1,215 @@ +/* + * Copyright 2025 Google LLC + * + * 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 + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { callGeminiApi } from '../api.js'; +import { showToast, copyToClipboard } from '../ui.js'; + +export async function getTimelineContent() { + try { + const response = await fetch('/src/features/templates/timeline.html'); + if (!response.ok) { + throw new Error(`Network response was not ok: ${response.statusText}`); + } + return await response.text(); + } catch (error) { + console.error('Failed to fetch timeline template:', error); + return '
    Error loading content. Please check the console for details.
    '; + } +} + +// --- INITIALIZATION AND LOGIC --- + +export function initTimeline() { + window.copyToClipboard = copyToClipboard; + let segmentIdCounter = 0; + + const addSegmentBtn = document.getElementById('add-timeline-segment-btn'); + const segmentsContainer = document.getElementById('timeline-segments-container'); + const generateBtn = document.getElementById('generate-timeline-prompt-btn'); + const clearBtn = document.getElementById('clear-timeline-btn'); + + const updateTimelineVisualizer = () => { + const visualizer = document.getElementById('timeline-visualizer'); + if (!visualizer) return; + visualizer.innerHTML = ''; + const segments = Array.from(document.querySelectorAll('.timeline-segment')); + let totalDuration = 0; + + segments.forEach(segment => { + const start = parseFloat(segment.querySelector('.segment-start').value); + const end = parseFloat(segment.querySelector('.segment-end').value); + const duration = isNaN(end) || isNaN(start) || end <= start ? 0 : end - start; + totalDuration += duration; + + const color = segment.dataset.color; + const width = (duration / 8) * 100; + + if (width > 0) { + const bar = document.createElement('div'); + bar.className = `h-full ${color}`; + bar.style.width = `${width}%`; + bar.title = `${start.toFixed(1)}s - ${end.toFixed(1)}s`; + visualizer.appendChild(bar); + } + }); + + if (totalDuration < 8) { + const remainingWidth = ((8 - totalDuration) / 8) * 100; + const emptyBar = document.createElement('div'); + emptyBar.className = 'h-full bg-gray-200 dark:bg-gray-700'; + emptyBar.style.width = `${remainingWidth}%`; + visualizer.appendChild(emptyBar); + } + }; + + const addTimelineSegment = () => { + segmentIdCounter++; + const segmentDiv = document.createElement('div'); + segmentDiv.className = 'timeline-segment bg-gray-50 dark:bg-gray-700 p-4 rounded-lg border border-gray-200 dark:border-gray-600'; + + const colors = ['bg-red-400', 'bg-blue-400', 'bg-green-400', 'bg-yellow-400', 'bg-purple-400', 'bg-pink-400', 'bg-teal-400', 'bg-orange-400']; + segmentDiv.dataset.color = colors[segmentIdCounter % colors.length]; + + segmentDiv.innerHTML = ` +
    +
    Segment ${segmentIdCounter}
    + +
    +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + `; + segmentsContainer.appendChild(segmentDiv); + + segmentDiv.querySelector('.remove-segment-btn').addEventListener('click', () => { + segmentDiv.remove(); + updateTimelineVisualizer(); + }); + + segmentDiv.querySelectorAll('input, textarea').forEach(el => { + el.addEventListener('change', updateTimelineVisualizer); + el.addEventListener('input', updateTimelineVisualizer); + }); + + updateTimelineVisualizer(); + }; + + const clearAll = () => { + segmentsContainer.innerHTML = ''; + segmentIdCounter = 0; + addTimelineSegment(); // Add back one default segment + document.getElementById('timeline-output-container').classList.add('hidden'); + showToast('Timeline cleared!', 'info'); + }; + + const generateTimelinePrompt = async () => { + const segments = Array.from(document.querySelectorAll('.timeline-segment')); + const segmentData = []; + let totalDuration = 0; + + for (const segment of segments) { + const start = parseFloat(segment.querySelector('.segment-start').value); + const end = parseFloat(segment.querySelector('.segment-end').value); + const promptText = segment.querySelector('.segment-prompt').value; + + if (isNaN(start) || isNaN(end) || end <= start || !promptText.trim()) continue; + + totalDuration += (end - start); + segmentData.push({ + timestamp: `${start.toFixed(2)}s-${end.toFixed(2)}s`, + prompt: promptText + }); + } + + if (segmentData.length === 0) { + showToast('Please add at least one valid timeline segment.', 'error'); + return; + } + if (totalDuration > 8) { + showToast(`Total duration exceeds 8 seconds (${totalDuration.toFixed(1)}s). Please adjust segments.`, 'error'); + return; + } + + const outputContainer = document.getElementById('timeline-output-container'); + const outputElement = document.getElementById('timeline-output'); + const originalButtonHtml = generateBtn.innerHTML; + + generateBtn.disabled = true; + generateBtn.innerHTML = ` + + + + + Generating... + `; + outputContainer.classList.remove('hidden'); + outputElement.textContent = 'Creating a cinematic timeline...'; + + try { + const systemPrompt = `You are an expert video prompt engineer. Your task is to take a series of timeline segments with simple descriptions and enhance them into a cinematic, ready-to-use prompt. + +For each segment, you will output the timestamp on one line, followed by a single, detailed paragraph on the next line. This paragraph should cinematically describe the scene. If the user's input mentions sound, incorporate it naturally into the description. If not, describe only the visuals. + +Example Input: +[ + {"timestamp":"0.00s-2.00s","prompt":"a cat appears, meow"}, + {"timestamp":"2.00s-5.00s","prompt":"it runs away"} +] + +Example Output: +timestamp: "00:00-00:02" +A sleek black cat suddenly appears in a soft pool of light, looking directly at the camera. A gentle meow is heard. + +timestamp: "00:02-00:05" +The cat darts quickly out of frame, its tail disappearing into the shadows. + +Now, process the following user input and generate the final timeline prompt. Output ONLY the timeline. +User Input: +${JSON.stringify(segmentData, null, 2)}`; + + const result = await callGeminiApi(systemPrompt); + outputElement.textContent = result; + showToast('Timeline prompt generated successfully!', 'success'); + } catch (error) { + console.error('Timeline generation error:', error); + showToast(`Generation failed: ${error.message}`, 'error'); + outputElement.textContent = `Error: ${error.message}`; + } finally { + generateBtn.disabled = false; + generateBtn.innerHTML = originalButtonHtml; + } + }; + + // --- Attach Event Listeners --- + addSegmentBtn.addEventListener('click', addTimelineSegment); + generateBtn.addEventListener('click', generateTimelinePrompt); + clearBtn.addEventListener('click', clearAll); + + // Add the first segment by default + addTimelineSegment(); +} + diff --git a/vision/sample-apps/V-Start/src/main.js b/vision/sample-apps/V-Start/src/main.js new file mode 100644 index 0000000..6fde485 --- /dev/null +++ b/vision/sample-apps/V-Start/src/main.js @@ -0,0 +1,401 @@ +/* + * Copyright 2025 Google LLC + * + * 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 + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { initGenerator, getGeneratorContent } from './features/generator.js'; +import { initEnhancer, getEnhancerContent } from './features/enhancer.js'; +import { initConverter, getConverterContent } from './features/converter.js'; +import { initEval, getEvalContent } from './features/eval.js'; +import { initAlignmentEval, getAlignmentEvalContent } from './features/alignment-eval.js'; +import { initGallery, getGalleryContent } from './features/gallery.js'; +import { initTimeline, getTimelineContent } from './features/timeline.js'; + +const mainContent = document.getElementById('main-content'); +const authSectionContainer = document.getElementById('auth-section-container'); + +const tabs = { + generator: { getContent: getGeneratorContent, init: initGenerator, needsAuth: true }, + enhancer: { getContent: getEnhancerContent, init: initEnhancer, needsAuth: true }, + converter: { getContent: getConverterContent, init: initConverter, needsAuth: true }, + 'alignment-eval': { getContent: getAlignmentEvalContent, init: initAlignmentEval, needsAuth: true }, + eval: { getContent: getEvalContent, init: initEval, needsAuth: false }, + gallery: { getContent: getGalleryContent, init: initGallery, needsAuth: false }, + timeline: { getContent: getTimelineContent, init: initTimeline, needsAuth: true } +}; + +// Dark Mode Functions +function initDarkMode() { + const themeToggleBtn = document.getElementById('theme-toggle'); + const lightIcon = document.getElementById('theme-toggle-light-icon'); + const darkIcon = document.getElementById('theme-toggle-dark-icon'); + + if (!themeToggleBtn || !lightIcon || !darkIcon) { + console.warn('Dark mode elements not found'); + return; + } + + // Check for saved theme preference or default to light mode + const savedTheme = localStorage.getItem('theme'); + + // Set initial theme - defaults to light mode + let currentTheme = savedTheme || 'light'; + applyTheme(currentTheme); + + // Toggle theme function + function toggleTheme() { + currentTheme = currentTheme === 'light' ? 'dark' : 'light'; + applyTheme(currentTheme); + localStorage.setItem('theme', currentTheme); + + // Show notification about theme change + showNotification(`Switched to ${currentTheme} mode`, 'info', 2000); + } + + // Apply theme function + function applyTheme(theme) { + const html = document.documentElement; + const toggleBtn = document.getElementById('theme-toggle'); + + if (theme === 'dark') { + html.classList.add('dark'); + // In dark mode, show sun icon (click to go to light) + lightIcon.style.display = 'block'; + darkIcon.style.display = 'none'; + if (toggleBtn) toggleBtn.title = 'Switch to light mode'; + } else { + html.classList.remove('dark'); + // In light mode, show moon icon (click to go to dark) + lightIcon.style.display = 'none'; + darkIcon.style.display = 'block'; + if (toggleBtn) toggleBtn.title = 'Switch to dark mode'; + } + + // Update CSS custom properties for smoother transitions + updateThemeProperties(theme); + } + + // Update CSS custom properties + function updateThemeProperties(theme) { + const root = document.documentElement; + + if (theme === 'dark') { + root.style.setProperty('--theme-bg', '#0f172a'); + root.style.setProperty('--theme-text', '#f1f5f9'); + root.style.setProperty('--theme-border', '#334155'); + } else { + root.style.setProperty('--theme-bg', '#ffffff'); + root.style.setProperty('--theme-text', '#1e293b'); + root.style.setProperty('--theme-border', '#e2e8f0'); + } + } + + + + // Add event listener to toggle button + themeToggleBtn.addEventListener('click', toggleTheme); + + // Add smooth transition class after initial theme is set + setTimeout(() => { + document.body.classList.add('transition-colors', 'duration-300'); + + // Add transitions to other elements + const elementsToTransition = document.querySelectorAll('nav, .main-tab, input, textarea, select, button'); + elementsToTransition.forEach(element => { + element.classList.add('transition-colors', 'duration-300'); + }); + }, 100); + + console.log(`Theme system initialized. Current theme: ${currentTheme} (defaults to light mode)`); +} + +// This function is async to handle fetching HTML templates. +async function showMainTab(tabName) { + const feature = tabs[tabName]; + if (!feature) { + console.warn(`Tab ${tabName} not found`); + return; + } + + // Show/hide auth section based on feature needs + authSectionContainer.style.display = feature.needsAuth ? 'block' : 'none'; + + // Load tab content asynchronously. + mainContent.innerHTML = await feature.getContent(); + feature.init(); + + // Update active tab styling + Object.keys(tabs).forEach(tabKey => { + const tabEl = document.getElementById(`${tabKey}-main-tab`); + if (tabEl) { + tabEl.classList.toggle('main-tab-active', tabKey === tabName); + } + }); + + // Add fade-in animation to content + mainContent.classList.add('fade-in'); + setTimeout(() => { + mainContent.classList.remove('fade-in'); + }, 500); + + console.log(`Switched to ${tabName} tab`); +} + +// Fast validation using Gemini Flash model +async function validateAccessToken() { + const accessToken = document.getElementById('access-token-input').value; + const projectId = document.getElementById('project-id-input').value; + const location = document.getElementById('location-input')?.value || 'us-central1'; + const statusElement = document.getElementById('access-token-status'); + const validateBtn = document.getElementById('validate-token-btn'); + const authMethod = document.getElementById('auth-method-select').value; + + // For API key mode, just do client-side check + if (authMethod === 'api-key') { + statusElement.textContent = '✅ Using server API key. Ready to generate!'; + statusElement.className = 'text-xs mt-2 h-4 text-blue-600 dark:text-blue-400'; + validateBtn.textContent = '✓ API Key Mode'; + setTimeout(() => { + validateBtn.textContent = 'Validate'; + }, 2000); + return; + } + + // For access token mode, check fields first + if (!projectId || !accessToken) { + statusElement.textContent = 'Project ID and Token are required.'; + statusElement.className = 'text-xs mt-2 h-4 text-red-600 dark:text-red-400'; + return; + } + + // Show loading state (should be quick with Flash) + validateBtn.disabled = true; + validateBtn.textContent = 'Validating...'; + validateBtn.classList.add('loading'); + statusElement.textContent = 'Checking credentials...'; + statusElement.className = 'text-xs mt-2 h-4 text-gray-500 dark:text-gray-400'; + + try { + const response = await fetch('/api/validate-token', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ projectId, accessToken, location }) + }); + + const result = await response.json(); + + if (result.valid) { + statusElement.textContent = '✅ ' + result.message; + statusElement.className = 'text-xs mt-2 h-4 text-green-600 dark:text-green-400'; + validateBtn.textContent = '✓ Validated'; + validateBtn.classList.add('bg-green-600', 'hover:bg-green-700'); + showNotification(`Token validated for ${location}!`, 'success'); + + setTimeout(() => { + validateBtn.textContent = 'Validate'; + validateBtn.classList.remove('bg-green-600', 'hover:bg-green-700'); + }, 3000); + } else { + statusElement.textContent = `❌ ${result.message}`; + statusElement.className = 'text-xs mt-2 h-4 text-red-600 dark:text-red-400'; + validateBtn.textContent = 'Retry'; + showNotification('Validation failed', 'error'); + } + } catch (error) { + console.error('Token validation error:', error); + statusElement.textContent = 'Validation failed. Check server console.'; + statusElement.className = 'text-xs mt-2 h-4 text-red-600 dark:text-red-400'; + showNotification('Network error during validation', 'error'); + } finally { + // Reset button state + validateBtn.disabled = false; + if (validateBtn.textContent === 'Validating...') { + validateBtn.textContent = 'Validate'; + } + validateBtn.classList.remove('loading'); + } +} + +// Enhanced notification system with dark mode support +function showNotification(message, type = 'info', duration = 3000) { + const toast = document.getElementById('notification-toast'); + + if (!toast) { + console.warn('Notification toast element not found'); + return; + } + + // Clear any existing classes and timers + clearTimeout(toast.hideTimer); + toast.className = ''; + + // Set the message + toast.textContent = message; + + // Base classes for the toast + const baseClasses = 'fixed bottom-20 left-1/2 transform -translate-x-1/2 px-6 py-3 rounded-full font-semibold text-sm shadow-lg transition-all duration-300 z-50'; + + // Type-specific styling + let typeClass = ''; + switch (type) { + case 'success': + typeClass = 'bg-green-500 text-white'; + break; + case 'error': + typeClass = 'bg-red-500 text-white'; + break; + case 'warning': + typeClass = 'bg-yellow-500 text-white'; + break; + default: + typeClass = 'bg-blue-500 text-white'; + } + + // Apply classes and show + toast.className = `${baseClasses} ${typeClass} opacity-100 translate-y-0`; + + // Hide after duration + toast.hideTimer = setTimeout(() => { + toast.className = `${baseClasses} ${typeClass} opacity-0 translate-y-2`; + }, duration); + + console.log(`Notification: ${message} (${type})`); +} + +// Utility function to get current theme +function getCurrentTheme() { + return document.documentElement.classList.contains('dark') ? 'dark' : 'light'; +} + +// Utility function to check if user prefers reduced motion +function prefersReducedMotion() { + return window.matchMedia('(prefers-reduced-motion: reduce)').matches; +} + +// Initialize keyboard shortcuts +function initKeyboardShortcuts() { + document.addEventListener('keydown', (e) => { + // Ctrl/Cmd + Shift + D to toggle dark mode + if ((e.ctrlKey || e.metaKey) && e.shiftKey && e.key === 'D') { + e.preventDefault(); + document.getElementById('theme-toggle')?.click(); + } + + // Ctrl/Cmd + 1-7 for tab switching + if ((e.ctrlKey || e.metaKey) && e.key >= '1' && e.key <= '7') { + e.preventDefault(); + const tabNames = Object.keys(tabs); + const tabIndex = parseInt(e.key) - 1; + if (tabNames[tabIndex]) { + showMainTab(tabNames[tabIndex]); + } + } + }); +} + +// Initialize animations based on user preference +function initAnimations() { + if (prefersReducedMotion()) { + document.documentElement.style.setProperty('--animation-duration', '0ms'); + } +} + +// Main initialization function +document.addEventListener('DOMContentLoaded', () => { + console.log('VeoStart application initializing...'); + + // Get DOM elements + const authMethodSelect = document.getElementById('auth-method-select'); + const apiKeySection = document.getElementById('api-key-auth-section'); + const accessTokenSection = document.getElementById('access-token-auth-section'); + const validateTokenBtn = document.getElementById('validate-token-btn'); + const authHeader = document.getElementById('auth-header'); + const authContent = document.getElementById('auth-content'); + const authChevron = document.getElementById('auth-chevron'); + + // Initialize dark mode first + initDarkMode(); + + // Initialize animations + initAnimations(); + + // Initialize keyboard shortcuts + initKeyboardShortcuts(); + + // Collapsible auth section logic + if (authHeader && authContent && authChevron) { + authHeader.addEventListener('click', () => { + const isHidden = authContent.classList.contains('hidden'); + authContent.classList.toggle('hidden'); + authChevron.classList.toggle('rotate-180'); + + // Add smooth animation + if (!prefersReducedMotion()) { + authChevron.style.transform = isHidden ? 'rotate(0deg)' : 'rotate(180deg)'; + } + }); + } + + // Auth method switching + if (authMethodSelect && apiKeySection && accessTokenSection) { + authMethodSelect.addEventListener('change', () => { + const isApiKey = authMethodSelect.value === 'api-key'; + apiKeySection.style.display = isApiKey ? 'block' : 'none'; + accessTokenSection.style.display = isApiKey ? 'none' : 'block'; + }); + } + + // Token validation + if (validateTokenBtn) { + validateTokenBtn.addEventListener('click', validateAccessToken); + } + + // Tab navigation + Object.keys(tabs).forEach(tabKey => { + const tabEl = document.getElementById(`${tabKey}-main-tab`); + if (tabEl) { + tabEl.addEventListener('click', () => showMainTab(tabKey)); + } + }); + + // Show default tab + showMainTab('generator'); + + // Welcome notification + setTimeout(() => { + const currentTheme = getCurrentTheme(); + showNotification(`Welcome to V-Start! Currently in ${currentTheme} mode. Press Ctrl+Shift+D to toggle theme.`, 'info', 5000); + }, 1000); + + console.log('VeoStart application initialized successfully'); +}); + +// Handle page visibility changes +document.addEventListener('visibilitychange', () => { + if (!document.hidden) { + // Refresh theme when page becomes visible again + const savedTheme = localStorage.getItem('theme'); + if (savedTheme) { + const html = document.documentElement; + if (savedTheme === 'dark' && !html.classList.contains('dark')) { + html.classList.add('dark'); + } else if (savedTheme === 'light' && html.classList.contains('dark')) { + html.classList.remove('dark'); + } + } + } +}); + +// Export functions for use in other modules +export { showNotification, getCurrentTheme, showMainTab }; \ No newline at end of file diff --git a/vision/sample-apps/V-Start/src/ui.js b/vision/sample-apps/V-Start/src/ui.js new file mode 100644 index 0000000..6cc414a --- /dev/null +++ b/vision/sample-apps/V-Start/src/ui.js @@ -0,0 +1,48 @@ +/* + * Copyright 2025 Google LLC + * + * 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 + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export function showToast(message, type = 'success') { + const toast = document.getElementById('notification-toast'); + if (!toast) return; + + toast.textContent = message; + toast.className = 'show'; + toast.classList.add(type); + + setTimeout(() => { + toast.classList.remove('show'); + }, 3000); +} + +export async function copyToClipboard(elementId) { + const element = document.getElementById(elementId); + if (!element) return; + + const textToCopy = element.innerText; + + if (!navigator.clipboard) { + showToast('Clipboard API not available.', 'error'); + return; + } + + try { + await navigator.clipboard.writeText(textToCopy); + showToast('Copied to clipboard!', 'success'); + } catch (err) { + console.error('Failed to copy text: ', err); + showToast('Failed to copy.', 'error'); + } +} \ No newline at end of file diff --git a/vision/sample-apps/V-Start/style.css b/vision/sample-apps/V-Start/style.css new file mode 100644 index 0000000..7b1c026 --- /dev/null +++ b/vision/sample-apps/V-Start/style.css @@ -0,0 +1,260 @@ +/* + * Copyright 2025 Google LLC + * + * 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 + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* + ===================== + V-Start Application + ==================== +*/ + +/* --- CSS Variables for Light & Dark Themes --- */ +:root { + /* Light Theme */ + --primary-color: #4f46e5; /* Indigo */ + --secondary-color: #10b981; /* Emerald */ + --background-color: #f1f5f9; /* Slate 100 */ + --text-primary: #1e293b; /* Slate 800 */ + --text-secondary: #64748b; /* Slate 500 */ + --card-background: rgba(255, 255, 255, 0.9); + --card-border: #e2e8f0; /* Slate 200 */ + --input-background: #f8fafc; /* Slate 50 */ + --input-border: #cbd5e1; /* Slate 300 */ + --hover-background: #f1f5f9; /* Slate 100 */ + --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); + --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05); + --shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04); +} + +/* Dark Theme */ +.dark { + --primary-color: #6366f1; /* Indigo 500 */ + --secondary-color: #34d399; /* Emerald 400 */ + --background-color: #0f172a; /* Slate 900 */ + --text-primary: #f1f5f9; /* Slate 100 */ + --text-secondary: #94a3b8; /* Slate 400 */ + --card-background: rgba(30, 41, 59, 0.9); /* Slate 800 with opacity */ + --card-border: #334155; /* Slate 700 */ + --input-background: #1e293b; /* Slate 800 */ + --input-border: #475569; /* Slate 600 */ + --hover-background: #1e293b; /* Slate 800 */ +} + +/* --- Base & Body Styles --- */ +body { + font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; + background-color: var(--background-color); + color: var(--text-primary); + min-height: 100vh; + transition: background-color 0.3s ease, color 0.3s ease; +} + +/* --- Enhanced Header --- */ +#main-header { + background: linear-gradient(135deg, #1f2937 0%, #111827 100%); + border-bottom: 1px solid rgba(255, 255, 255, 0.1); + box-shadow: var(--shadow-lg); +} + +.dark #main-header { + background: linear-gradient(135deg, #111827 0%, #000000 100%); +} + +#main-header h1 { + color: white; + text-shadow: 0 2px 10px rgba(0,0,0,0.3); +} + + +/* --- Main Container --- */ +main { + padding: 2rem 1rem; +} + +.container { + max-width: 1400px; + margin: 0 auto; +} + +/* --- Navigation Tabs --- */ +nav { + background: var(--card-background); + border: 1px solid var(--card-border); + border-radius: 16px; + padding: 1rem; + box-shadow: var(--shadow-lg); + backdrop-filter: blur(10px); + transition: all 0.3s ease; +} + +.main-tab { + transition: all 0.3s ease; + color: var(--text-secondary); + display: flex; + align-items: center; + justify-content: flex-start; + font-weight: 600; + border-radius: 12px; + border: 1px solid transparent; +} + +.main-tab:hover { + background-color: var(--hover-background); + color: var(--primary-color); + transform: translateX(4px); + border-color: var(--primary-color); +} + +.main-tab-active { + background-color: var(--primary-color); + color: white !important; + box-shadow: 0 4px 12px rgba(99, 102, 241, 0.3); + border-color: var(--primary-color); +} + +.main-tab-active:hover { + color: white !important; + transform: translateX(0); + background-color: var(--primary-color); +} + +/* --- Section Headers --- */ +h3, h4 { + position: relative; + color: var(--text-primary); +} + +h3::after { + content: ''; + position: absolute; + bottom: -6px; + left: 0; + width: 40px; + height: 3px; + background: var(--primary-color); + border-radius: 2px; +} + +/* --- General Overrides for Tailwind Classes --- */ +.bg-white { + background: var(--card-background) !important; + backdrop-filter: blur(10px); + border: 1px solid var(--card-border); +} + +.border-gray-200 { border-color: var(--card-border) !important; } +.border-gray-300 { border-color: var(--input-border) !important; } +.text-gray-600 { color: var(--text-secondary) !important; } +.text-gray-700 { color: var(--text-primary) !important; } +.text-gray-800 { color: var(--text-primary) !important; } +.bg-gray-50 { background-color: var(--input-background) !important; } +.bg-gray-100 { background-color: var(--hover-background) !important; } + +/* --- Buttons --- */ +button:active { + transform: scale(0.97); +} + +/* --- Notification Toast --- */ +#notification-toast { + position: fixed; + bottom: 20px; + left: 50%; + transform: translate(-50%, 10px); + padding: 12px 24px; + border-radius: 9999px; + color: white; + font-weight: 600; + z-index: 9999; + opacity: 0; + visibility: hidden; + transition: all 0.4s ease; + box-shadow: var(--shadow-lg); +} + +#notification-toast.show { + opacity: 1; + visibility: visible; + transform: translate(-50%, 0); +} + +#notification-toast.success { background-color: #10b981; } +#notification-toast.error { background-color: #ef4444; } +#notification-toast.info { background-color: #3b82f6; } + +/* --- Scrollbars --- */ +::-webkit-scrollbar { + width: 8px; + height: 8px; +} +::-webkit-scrollbar-track { + background: var(--input-background); + border-radius: 4px; +} +::-webkit-scrollbar-thumb { + background: var(--text-secondary); + border-radius: 4px; +} +::-webkit-scrollbar-thumb:hover { + background: var(--primary-color); +} + +/* --- Accessibility --- */ +*:focus-visible { + outline: 3px solid var(--primary-color); + outline-offset: 2px; + border-radius: 4px; +} + +/* --- Responsive Video Container --- */ +.video-container { + position: relative; + overflow: hidden; + width: 100%; + padding-top: 56.25%; /* 16:9 Aspect Ratio */ +} + +.video-container iframe, +.video-container video { + position: absolute; + top: 0; + left: 0; + bottom: 0; + right: 0; + width: 100%; + height: 100%; + border-radius: 8px; +} + +/* --- Dark Mode Specific Adjustments --- */ +.dark img { + opacity: 0.9; +} +.dark img:hover { + opacity: 1; +} + +/* --- Mobile Responsive Improvements --- */ +@media (max-width: 768px) { + .main-tab { + justify-content: center; + } + .main-tab span { + display: none; + } + .main-tab svg { + margin: 0; + } +} \ No newline at end of file diff --git a/vision/sample-apps/genmedia-live/.gitignore b/vision/sample-apps/genmedia-live/.gitignore new file mode 100644 index 0000000..250f31e --- /dev/null +++ b/vision/sample-apps/genmedia-live/.gitignore @@ -0,0 +1,23 @@ +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +venv/ +env/ +.env + +# IDE +.idea/ +.vscode/ +*.swp +*.swo + +# OS +.DS_Store +Thumbs.db + +# Project +outputs/ +*.log diff --git a/vision/sample-apps/genmedia-live/README.md b/vision/sample-apps/genmedia-live/README.md new file mode 100644 index 0000000..a268958 --- /dev/null +++ b/vision/sample-apps/genmedia-live/README.md @@ -0,0 +1,141 @@ +# GenMedia Live + +Author: [Wafae Bakkali](https://github.com/WafaeBakkali) + +**Real-time multimodal creation enabled by extending the Gemini Live API with image and video generation tools.** + +GenMedia Live enables multimodal AI creation by extending the [Gemini Live API](https://cloud.google.com/vertex-ai/generative-ai/docs/live-api) capabilities to image and video generation using Gemini Pro Image and Veo through function calling. + +You can interact with voice or text to: +- **Generate images** from descriptions or edit existing ones +- **Create videos** from text prompts or animate images +- **Create short movies** by combining multiple video clips +- **Ask questions** about what you show on camera or upload +- **Extract frames** from videos for further editing + +Users can reference previously generated images, or use images from their camera, screen share, or upload images from the UI as reference. + +## Features + +- **Voice Interaction**: Real-time voice conversations with the Gemini Live API +- **Text Input**: Type messages as an alternative to voice +- **Camera**: Share your camera for visual context +- **Screen Share**: Share your screen for visual context +- **Image Upload**: Upload images from your device as reference +- **Image Generation**: Create and edit images using Gemini Pro Image +- **Video Generation**: Generate videos with Veo, including image-to-video +- **Frame Extraction**: Extract frames from videos using ffmpeg +- **Video Combining**: Merge multiple videos into one using ffmpeg +- **Session Management**: 30-minute conversation history with automatic reconnection + +## Prerequisites + +- Python 3.10+ +- Google Cloud project with Vertex AI API enabled +- ffmpeg (for video operations) + +### Installing ffmpeg + +**Linux (Ubuntu/Debian):** +```bash +sudo apt update && sudo apt install ffmpeg +``` + +**macOS:** +```bash +brew install ffmpeg +``` + +**Windows:** +Download from [ffmpeg.org](https://ffmpeg.org/download.html) and add to PATH. + +## Setup + +1. Clone the repository: +```bash +git clone https://github.com/GoogleCloudPlatform/generative-ai.git +cd generative-ai/vision/sample-apps/genmedia-live +``` + +2. Create and activate virtual environment: +```bash +python -m venv venv +source venv/bin/activate # Linux/Mac +# or +venv\Scripts\activate # Windows +``` + +3. Install dependencies: +```bash +pip install -r requirements.txt +``` + +4. Run the application: +```bash +python app.py +``` + +5. Open http://localhost:8080 in your browser. + +## Authentication + +GenMedia Live requires users to authenticate with their own Google Cloud credentials. + +### Steps to Authenticate: + +1. **Enter your Project ID** in the sidebar +2. **Click "Open Cloud Shell"** - this opens Google Cloud Shell in a new tab +3. **Run the command** in Cloud Shell: + ```bash + gcloud auth print-access-token + ``` +4. **Copy the token** and paste it in the "Paste Token" field +5. **Click "Validate & Connect"** + +If validation succeeds, you'll see "Connected!" and can start using the app. + +> **Note**: Access tokens expire after ~1 hour. You'll need to generate a new token if your session expires. + +## Usage + +1. Complete authentication (see above) +2. Click **Voice** to start a voice conversation, or type in the text box +3. Click **Camera** to share your camera as visual context +4. Click **Screen** to share your screen as visual context +5. Use **Upload** to add reference images from your device +6. Ask GenMedia Live to generate images or videos + +## Project Structure + +``` +genmedia-live/ +├── app.py # Flask backend +├── index.html # Main page +├── style.css # Global styles +├── requirements.txt # Python dependencies +├── outputs/ # Generated files +│ ├── images/ +│ └── videos/ +└── src/ + ├── main.js # App initialization + ├── ui.js # UI utilities + └── features/ + ├── genmedia-chat.js # Voice chat functionality + └── templates/ + └── genmedia-chat.html +``` + +## Deployment + +### Cloud Run + +```bash +gcloud run deploy genmedia-live \ + --source . \ + --region us-central1 \ + --allow-unauthenticated +``` + +## License + +Licensed under the [Apache License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0). diff --git a/vision/sample-apps/genmedia-live/app.py b/vision/sample-apps/genmedia-live/app.py new file mode 100644 index 0000000..bd2c5da --- /dev/null +++ b/vision/sample-apps/genmedia-live/app.py @@ -0,0 +1,1590 @@ +# Copyright 2026 Google LLC +# +# 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 +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""GenMedia Live - Real-time multimodal AI creation application. + +This application extends the Gemini Live API with image and video generation +capabilities using Gemini Pro Image and Veo through function calling. +""" + +import asyncio +import base64 +import logging +import os +import re +import threading +import traceback +from pathlib import Path + +import google.auth +from flask import Flask, jsonify, request, send_from_directory +from flask_cors import CORS +from flask_socketio import SocketIO, emit +from google import genai +from google.cloud import storage +from google.genai import types +from google.oauth2.credentials import Credentials + +try: + import io as pil_io + + from PIL import Image + + PIL_AVAILABLE = True +except ImportError: + PIL_AVAILABLE = False + +app = Flask(__name__, static_folder="src", static_url_path="") +CORS(app) +logging.basicConfig( + level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s" +) + +socketio = SocketIO( + app, + cors_allowed_origins="*", + async_mode="threading", + ping_timeout=300, + ping_interval=60, + max_http_buffer_size=50000000, + transports=["polling"], +) + +# Configuration +DEFAULT_PROJECT_ID = os.getenv("GOOGLE_CLOUD_PROJECT") +DEFAULT_LOCATION_ID = "us-central1" + +if not DEFAULT_PROJECT_ID: + try: + _, project = google.auth.default() + if project: + DEFAULT_PROJECT_ID = project + except Exception: + pass + +VEO_MODEL_ID = "veo-3.1-generate-001" +GEMINI_IMAGE_MODEL = "gemini-3-pro-image" +GEMINI_LIVE_MODEL = "gemini-live-2.5-flash-native-audio" + +IMAGE_ASPECT_RATIOS = [ + "1:1", "2:3", "3:2", "3:4", "4:3", "4:5", "5:4", "9:16", "16:9", "21:9" +] +VIDEO_ASPECT_RATIOS = ["16:9", "9:16"] + +# Initialize clients +try: + default_gemini_client = genai.Client( + vertexai=True, project=DEFAULT_PROJECT_ID, location=DEFAULT_LOCATION_ID + ) + default_image_client = genai.Client( + vertexai=True, project=DEFAULT_PROJECT_ID, location="global" + ) + storage_client = storage.Client(project=DEFAULT_PROJECT_ID) + logging.info(f"Initialized with project: {DEFAULT_PROJECT_ID}") +except Exception as e: + logging.error(f"Initialization failed: {e}") + default_gemini_client = None + default_image_client = None + +# Global state +session_credentials = {} +bridges = {} +live_sessions = {} +starting_sessions = set() +starting_session_sids = {} +session_states = {} +file_history = [] +session_handles = {} +user_name = "User" +custom_system_instructions = "" + + +def sanitize_log(text, max_length=200): + """Sanitize text for logging by removing base64 and image data.""" + if not text: + return text + text = str(text) + text = re.sub(r"[A-Za-z0-9+/=]{50,}", "[base64]", text) + text = re.sub(r"data:image/[^;]+;base64,[^\s,)]+", "[img]", text) + return text[:max_length] + "..." if len(text) > max_length else text + + +def get_session_state(session_id): + """Get or create session state for a given session ID.""" + if session_id not in session_states: + session_states[session_id] = { + "last_image": None, + "last_video": None, + "last_seen_frame": None, + "uploaded_images": [], + } + return session_states[session_id] + + +def get_next_id_for_type(file_type, session_id): + """Get the next available ID for a file type in a session.""" + existing = [ + e["id"] + for e in file_history + if e["type"] == file_type and e.get("session_id") == session_id + ] + return 1 if not existing else max(existing) + 1 + + +def get_last_id_for_type(file_type, session_id): + """Get the last ID for a file type in a session.""" + session_files = [ + f + for f in file_history + if f.get("session_id") == session_id and f.get("type") == file_type + ] + return max(f["id"] for f in session_files) if session_files else None + + +def get_active_client(): + """Get the active Gemini client using OAuth credentials.""" + if "oauth" not in session_credentials: + return None + creds_data = session_credentials["oauth"] + creds = Credentials(token=creds_data["access_token"]) + return genai.Client( + vertexai=True, + project=creds_data["project_id"], + location=creds_data["location"], + credentials=creds, + ) + + +def get_active_image_client(): + """Get the active image generation client using OAuth credentials.""" + if "oauth" not in session_credentials: + return None + creds_data = session_credentials["oauth"] + creds = Credentials(token=creds_data["access_token"]) + return genai.Client( + vertexai=True, + project=creds_data["project_id"], + location="global", + credentials=creds, + ) + + +def get_live_system_prompt(is_resumed=False): + """Generate the system prompt for the Gemini Live API session.""" + custom = f"\n{custom_system_instructions}\n" if custom_system_instructions else "" + name_section = ( + f"\nUser's name: {user_name}\n" if user_name and user_name != "User" else "" + ) + resume_note = ( + "\nThis is a continued conversation. Continue naturally from where you left off.\n" + if is_resumed + else "" + ) + + return f"""You are GenMedia Live, a creative AI assistant with vision and media generation capabilities. +{custom}{name_section}{resume_note} + +When the user asks you to SEE or DESCRIBE something (camera, uploaded image), just respond verbally - no tools needed. +When the user explicitly asks to GENERATE, CREATE, MAKE, or EDIT content, use the appropriate tool. + +Available tools: +- generate_image: Create new images OR edit/modify existing ones by using them as reference +- generate_video: Create videos from text or animate images +- extract_frame: Get a frame from a video at a specific timestamp +- combine_videos: Merge multiple videos together +- view_generated_image: Load a previously generated image to see it + +For image generation AND editing: +- To EDIT an existing image: set refers_to_last=true or use image_id to reference the image, then describe the changes in the prompt +- refers_to_camera=true: Use uploaded/camera image as reference +- refers_to_last=true: Use last generated image as reference for editing +- image_id: Reference a specific generated image by ID for editing + +For video generation: +- Default duration is 8 seconds unless specified +- refers_to_last=true: Animate the last generated image +- image_id: Animate a specific image + +Remember ALL conversation context and ALL previously generated content throughout the session. +Keep track of all generated images (image 1, image 2, etc.) and videos (video 1, video 2, etc.) by their IDs. +""" + + +class SessionBridge: + """Bridge for communication between Flask and async Gemini Live session.""" + + def __init__(self, loop): + self.loop = loop + self.queue = asyncio.Queue(maxsize=100) + self.dropped_frames = 0 + + def put_nowait(self, item): + """Put an item in the queue from a non-async context.""" + if not self.loop.is_closed(): + try: + self.loop.call_soon_threadsafe(self._safe_put, item) + except RuntimeError: + pass + + def _safe_put(self, item): + """Safely put an item in the queue, dropping old items if full.""" + try: + self.queue.put_nowait(item) + except asyncio.QueueFull: + self.dropped_frames += 1 + try: + self.queue.get_nowait() + self.queue.put_nowait(item) + except Exception: + pass + + +async def run_live_session(session_id, sid): + """Run the Gemini Live API session.""" + if session_id in live_sessions and live_sessions[session_id].get("active"): + live_sessions[session_id]["sid"] = sid + socketio.emit( + "live_session_started", + {"status": "reconnected", "user_name": user_name}, + room=sid, + ) + return + + max_reconnects = 5 + reconnect_count = 0 + + while reconnect_count < max_reconnects: + resumption_handle = session_handles.get(session_id) + is_resumed = resumption_handle is not None or reconnect_count > 0 + if resumption_handle: + logging.info(f"Session {session_id}: resuming with handle") + input_queue = asyncio.Queue(maxsize=100) + + loop = asyncio.get_event_loop() + bridge = SessionBridge(loop) + bridge.queue = input_queue + bridges[session_id] = bridge + + try: + tools = [ + types.Tool( + function_declarations=[ + types.FunctionDeclaration( + name="generate_image", + description="Generate a new AI image OR edit/modify an existing image. For editing, use refers_to_last=true or image_id to reference the source image, and describe the desired changes in the prompt.", + parameters={ + "type": "object", + "properties": { + "prompt": { + "type": "string", + "description": "Description of the image to generate", + }, + "aspect_ratio": { + "type": "string", + "enum": IMAGE_ASPECT_RATIOS, + }, + "refers_to_camera": { + "type": "boolean", + "description": "Use camera/uploaded image as reference", + }, + "refers_to_last": { + "type": "boolean", + "description": "Use last generated image as reference", + }, + "image_id": { + "type": "integer", + "description": "Specific image ID to reference", + }, + }, + "required": ["prompt"], + }, + ), + types.FunctionDeclaration( + name="generate_video", + description="Generate a video from text or animate an image.", + parameters={ + "type": "object", + "properties": { + "prompt": { + "type": "string", + "description": "Description of video to generate", + }, + "duration": { + "type": "integer", + "description": "Duration in seconds (4, 6, or 8)", + }, + "refers_to_last": { + "type": "boolean", + "description": "Animate the last generated image", + }, + "image_id": { + "type": "integer", + "description": "Specific image ID to animate", + }, + }, + "required": ["prompt"], + }, + ), + types.FunctionDeclaration( + name="extract_frame", + description="Extract a frame from a video at a specific timestamp", + parameters={ + "type": "object", + "properties": { + "video_id": { + "type": "integer", + "description": "Video ID (defaults to last video)", + }, + "timestamp": { + "type": "number", + "description": "Timestamp in seconds", + }, + }, + "required": [], + }, + ), + types.FunctionDeclaration( + name="combine_videos", + description="Combine multiple videos into one. Pass empty array to combine all session videos.", + parameters={ + "type": "object", + "properties": { + "video_ids": { + "type": "array", + "items": {"type": "integer"}, + "description": "Video IDs to combine", + }, + }, + "required": ["video_ids"], + }, + ), + types.FunctionDeclaration( + name="view_generated_image", + description="Load a previously generated image to view it", + parameters={ + "type": "object", + "properties": { + "image_id": { + "type": "integer", + "description": "Image ID to view", + }, + }, + "required": ["image_id"], + }, + ), + ] + ) + ] + + config = types.LiveConnectConfig( + response_modalities=["AUDIO"], + system_instruction=get_live_system_prompt(is_resumed=is_resumed), + media_resolution=types.MediaResolution.MEDIA_RESOLUTION_MEDIUM, + context_window_compression=types.ContextWindowCompressionConfig( + trigger_tokens=100000, + sliding_window=types.SlidingWindow(target_tokens=80000), + ), + session_resumption=types.SessionResumptionConfig( + handle=resumption_handle + ), + speech_config=types.SpeechConfig( + voice_config=types.VoiceConfig( + prebuilt_voice_config=types.PrebuiltVoiceConfig( + voice_name="Aoede" + ) + ) + ), + tools=tools, + ) + + client = get_active_client() + if client is None: + current_sid = starting_session_sids.get(session_id, sid) + socketio.emit( + "live_session_error", + { + "error": "Authentication required. Please enter your Project ID and access token.", + "code": 401, + }, + room=current_sid, + ) + starting_sessions.discard(session_id) + return + + async with client.aio.live.connect( + model=GEMINI_LIVE_MODEL, config=config + ) as session: + current_sid = starting_session_sids.get(session_id, sid) + live_sessions[session_id] = {"active": True, "sid": current_sid} + starting_sessions.discard(session_id) + starting_session_sids.pop(session_id, None) + socketio.emit( + "live_session_started", + {"status": "connected", "user_name": user_name}, + room=current_sid, + ) + + async def sender_loop(): + while live_sessions.get(session_id, {}).get("active"): + try: + item = await asyncio.wait_for( + input_queue.get(), timeout=0.5 + ) + + if item["type"] == "audio": + await session.send_realtime_input(audio=item["data"]) + elif item["type"] == "video": + await session.send_realtime_input(video=item["data"]) + elif item["type"] == "text": + await session.send_client_content( + turns=types.Content( + role="user", + parts=[types.Part(text=item["data"])], + ), + turn_complete=True, + ) + elif item["type"] == "image_with_text": + for img_data in item.get("images", []): + if isinstance(img_data, dict): + blob = types.Blob( + mime_type=img_data.get( + "mime_type", "image/jpeg" + ), + data=img_data["data"], + ) + for _ in range(5): + await session.send_realtime_input( + video=blob + ) + await asyncio.sleep(0.1) + await session.send_client_content( + turns=types.Content( + role="user", + parts=[ + types.Part(text=item.get("text", "")) + ], + ), + turn_complete=True, + ) + elif item["type"] == "image": + for img_data in item["data"]: + if isinstance(img_data, dict): + blob = types.Blob( + mime_type=img_data.get( + "mime_type", "image/jpeg" + ), + data=img_data["data"], + ) + for _ in range(3): + await session.send_realtime_input( + video=blob + ) + await asyncio.sleep(0.1) + elif item["type"] == "tool_response": + await session.send_tool_response( + function_responses=item["data"] + ) + elif item["type"] == "generated_image_feedback": + img_data = item["data"] + blob = types.Blob( + mime_type=img_data.get("mime_type", "image/png"), + data=img_data["data"], + ) + await session.send_client_content( + turns=types.Content( + role="user", + parts=[ + types.Part(inline_data=blob), + types.Part( + text=f"[Generated image {item.get('id', '?')}]" + ), + ], + ), + turn_complete=False, + ) + input_queue.task_done() + except asyncio.TimeoutError: + continue + except Exception as e: + logging.error(f"Send error: {e}") + + async def receiver_loop(): + try: + while live_sessions.get(session_id, {}).get("active"): + async for response in session.receive(): + if not live_sessions.get(session_id, {}).get("active"): + return "ended" + current_sid = live_sessions[session_id]["sid"] + + if response.session_resumption_update: + update = response.session_resumption_update + if update.resumable and update.new_handle: + session_handles[session_id] = update.new_handle + logging.info( + f"Session {session_id}: captured resumption handle" + ) + + if response.tool_call: + for fc in response.tool_call.function_calls: + socketio.emit( + "voice_generation_request", + { + "text": "Generating...", + "function_name": fc.name, + "function_args": dict(fc.args), + "function_call_id": fc.id, + }, + room=current_sid, + ) + + if ( + response.server_content + and response.server_content.model_turn + ): + for ( + part + ) in response.server_content.model_turn.parts: + if part.text: + socketio.emit( + "text_response", + {"text": part.text}, + room=current_sid, + ) + if part.inline_data: + audio_b64 = base64.b64encode( + part.inline_data.data + ).decode("utf-8") + socketio.emit( + "audio_response", + { + "audio": audio_b64, + "mime_type": part.inline_data.mime_type, + }, + room=current_sid, + ) + except asyncio.CancelledError: + return "cancelled" + except Exception as e: + error_msg = str(e) + if ( + "1011" in error_msg + or "Insufficient model resources" in error_msg + ): + current_sid = live_sessions[session_id]["sid"] + socketio.emit( + "live_session_error", + { + "error": "Server overloaded. Please try again.", + "code": 1011, + }, + room=current_sid, + ) + return "capacity_error" + if "1000" in error_msg or "cancelled" in error_msg.lower(): + return "reconnect" + logging.error(f"Receive error: {e}") + return "error" + return "ended" + + sender_task = asyncio.create_task(sender_loop()) + receiver_task = asyncio.create_task(receiver_loop()) + + done, pending = await asyncio.wait( + [sender_task, receiver_task], return_when=asyncio.FIRST_COMPLETED + ) + + session_active = live_sessions.get(session_id, {}).get("active", False) + should_reconnect = False + for task in done: + try: + result = task.result() + if result == "reconnect": + should_reconnect = True + elif result == "ended" and session_active: + should_reconnect = True + except Exception: + pass + + for task in pending: + task.cancel() + try: + await task + except asyncio.CancelledError: + pass + + session_state = get_session_state(session_id) + session_state["should_reconnect"] = should_reconnect + + session_active = live_sessions.get(session_id, {}).get("active", False) + should_reconnect = get_session_state(session_id).get( + "should_reconnect", False + ) + has_handle = session_id in session_handles + + if session_active and (should_reconnect or has_handle): + reconnect_count += 1 + logging.info( + f"Reconnecting session {session_id} (attempt {reconnect_count}, has_handle={has_handle})" + ) + await asyncio.sleep(1) + continue + else: + break + + except Exception as e: + logging.error(f"Session error: {e}") + current_sid = live_sessions.get(session_id, {}).get("sid", sid) + error_msg = str(e) + + if "1011" in error_msg or "Insufficient model resources" in error_msg: + socketio.emit( + "live_session_error", + { + "error": "Server overloaded. Please try again.", + "code": 1011, + }, + room=current_sid, + ) + break + + socketio.emit( + "live_session_error", {"error": str(e)}, room=current_sid + ) + + if ( + "1000" in error_msg or "cancelled" in error_msg.lower() + ) and reconnect_count < max_reconnects: + reconnect_count += 1 + await asyncio.sleep(2) + continue + else: + break + + if session_id in bridges: + del bridges[session_id] + if session_id in live_sessions: + final_sid = live_sessions[session_id]["sid"] + del live_sessions[session_id] + socketio.emit( + "session_ended_reconnect", + {"session_id": session_id, "can_resume": session_id in session_handles}, + room=final_sid, + ) + starting_sessions.discard(session_id) + + +def start_background_loop(session_id, sid): + """Start the async event loop for the Gemini Live session.""" + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + try: + loop.run_until_complete(run_live_session(session_id, sid)) + except Exception as e: + logging.error(f"Loop error: {e}") + finally: + starting_sessions.discard(session_id) + loop.close() + + +# Socket handlers +@socketio.on("connect") +def handle_connect(): + """Handle client connection.""" + logging.info(f"Client connected: {request.sid}") + + +@socketio.on("disconnect") +def handle_disconnect(): + """Handle client disconnection.""" + logging.info(f"Client disconnected: {request.sid}") + + +@socketio.on("start_live_session") +def handle_start(data): + """Handle start live session request.""" + session_id = data.get("session_id", "default") + sid = request.sid + + session_state = get_session_state(session_id) + session_state["uploaded_images"] = [] + session_state["last_seen_frame"] = None + + if session_id in bridges and session_id in live_sessions: + live_sessions[session_id]["sid"] = sid + emit("live_session_started", {"status": "reconnected", "user_name": user_name}) + return + + if session_id in starting_sessions: + starting_session_sids[session_id] = sid + return + + starting_sessions.add(session_id) + starting_session_sids[session_id] = sid + t = threading.Thread( + target=start_background_loop, args=(session_id, sid), daemon=True + ) + t.start() + + +@socketio.on("stop_live_session") +def handle_stop(data): + """Handle stop live session request.""" + session_id = data.get("session_id") + if session_id in live_sessions: + live_sessions[session_id]["active"] = False + emit("live_session_stopped") + + +@socketio.on("check_session_status") +def handle_check_session(data): + """Check if a session is active.""" + session_id = data.get("session_id") + sid = request.sid + + if session_id in bridges and session_id in live_sessions: + live_sessions[session_id]["sid"] = sid + emit("live_session_started", {"status": "reconnected", "user_name": user_name}) + return {"active": True} + elif session_id in starting_sessions: + starting_session_sids[session_id] = sid + return {"active": False, "starting": True} + return {"active": False} + + +@socketio.on("send_audio") +def handle_audio(data): + """Handle incoming audio data.""" + session_id = data.get("session_id") + audio = data.get("audio") + + if not session_id or session_id not in bridges or not audio: + return + + try: + import struct + + if isinstance(audio, list): + b = struct.pack(f"<{len(audio)}h", *audio) + else: + b = base64.b64decode(audio) + audio_blob = types.Blob(mime_type="audio/pcm;rate=16000", data=b) + bridges[session_id].put_nowait({"type": "audio", "data": audio_blob}) + except Exception as e: + logging.error(f"Audio error: {e}") + + +@socketio.on("send_camera_frame") +def handle_video(data): + """Handle incoming camera frame.""" + session_id = data.get("session_id") + frame = data.get("frame") + + if session_id and frame: + get_session_state(session_id)[ + "last_seen_frame" + ] = f"data:image/jpeg;base64,{frame}" + + if session_id in bridges and frame: + try: + frame_bytes = base64.b64decode(frame) + video_blob = types.Blob(mime_type="image/jpeg", data=frame_bytes) + bridges[session_id].put_nowait({"type": "video", "data": video_blob}) + except Exception as e: + logging.error(f"Frame error: {e}") + + +@socketio.on("send_text_message") +def handle_text(data): + """Handle incoming text message.""" + session_id = data.get("session_id") + text = data.get("text") + if session_id in bridges and text: + bridges[session_id].put_nowait({"type": "text", "data": text}) + + +@socketio.on("send_uploaded_images") +def handle_uploaded_images(data): + """Handle uploaded images.""" + session_id = data.get("session_id") + images = data.get("images", []) + + if not session_id or not images: + return {"status": "error", "message": "No session_id or images"} + + session_state = get_session_state(session_id) + session_state["uploaded_images"] = images + if images: + session_state["last_seen_frame"] = images[-1] + + if session_id in bridges and session_id in live_sessions: + try: + processed_images = [] + for img_data_url in images: + if isinstance(img_data_url, str) and "," in img_data_url: + header, b64_data = img_data_url.split(",", 1) + img_bytes = base64.b64decode(b64_data) + + if PIL_AVAILABLE: + img = Image.open(pil_io.BytesIO(img_bytes)) + if img.mode in ("RGBA", "P"): + img = img.convert("RGB") + img.thumbnail((768, 768), Image.Resampling.LANCZOS) + canvas = Image.new("RGB", (768, 768), (128, 128, 128)) + x = (768 - img.width) // 2 + y = (768 - img.height) // 2 + canvas.paste(img, (x, y)) + buffer = pil_io.BytesIO() + canvas.save(buffer, format="JPEG", quality=85) + processed_images.append( + {"mime_type": "image/jpeg", "data": buffer.getvalue()} + ) + else: + mime_type = "image/jpeg" if "jpeg" in header else "image/png" + processed_images.append( + {"mime_type": mime_type, "data": img_bytes} + ) + + if processed_images: + bridges[session_id].put_nowait( + {"type": "image", "data": processed_images} + ) + return {"status": "ok", "queued": len(processed_images)} + except Exception as e: + logging.error(f"Image upload error: {e}") + return {"status": "error", "message": str(e)} + return {"status": "stored"} + + +@socketio.on("send_message_with_images") +def handle_message_with_images(data): + """Handle text message with images.""" + session_id = data.get("session_id") + text = data.get("text", "") + images = data.get("images", []) + + if not session_id or session_id not in bridges: + return {"status": "error", "message": "Session not active"} + + session_state = get_session_state(session_id) + session_state["uploaded_images"] = images + if images: + session_state["last_seen_frame"] = images[-1] + + try: + processed_images = [] + for img_data_url in images: + if isinstance(img_data_url, str) and "," in img_data_url: + header, b64_data = img_data_url.split(",", 1) + img_bytes = base64.b64decode(b64_data) + mime_type = "image/jpeg" + if "image/png" in header: + mime_type = "image/png" + + if PIL_AVAILABLE: + img = Image.open(pil_io.BytesIO(img_bytes)) + if img.mode in ("RGBA", "P"): + img = img.convert("RGB") + img.thumbnail((768, 768), Image.Resampling.LANCZOS) + canvas = Image.new("RGB", (768, 768), (128, 128, 128)) + x = (768 - img.width) // 2 + y = (768 - img.height) // 2 + canvas.paste(img, (x, y)) + buffer = pil_io.BytesIO() + canvas.save(buffer, format="JPEG", quality=85) + processed_images.append( + {"mime_type": "image/jpeg", "data": buffer.getvalue()} + ) + else: + processed_images.append({"mime_type": mime_type, "data": img_bytes}) + + context = f"[User uploaded {len(images)} image(s)]: {text}" if images else text + bridges[session_id].put_nowait( + {"type": "image_with_text", "images": processed_images, "text": context} + ) + return {"status": "ok"} + except Exception as e: + logging.error(f"Message with images error: {e}") + return {"status": "error", "message": str(e)} + + +@socketio.on("generation_completed") +def handle_gen_complete(data): + """Handle generation completed notification.""" + session_id = data.get("session_id") + gen_id = data.get("id") + gen_type = data.get("type") + call_id = data.get("function_call_id") + + if session_id in bridges and call_id: + response = [ + types.FunctionResponse( + id=call_id, + name="generate_content", + response={"status": "completed", "result": f"Generated {gen_type} {gen_id}"}, + ) + ] + bridges[session_id].put_nowait({"type": "tool_response", "data": response}) + + if gen_type == "image": + try: + img_path = Path(f"outputs/images/image_{gen_id}.png") + if img_path.exists(): + img_bytes = img_path.read_bytes() + bridges[session_id].put_nowait( + { + "type": "generated_image_feedback", + "data": {"mime_type": "image/png", "data": img_bytes}, + "id": gen_id, + } + ) + except Exception as e: + logging.error(f"Error sending generated image: {e}") + + +# REST API routes +@app.route("/api/genmedia-chat", methods=["POST"]) +def handle_genmedia_chat(): + """Handle generation requests from the frontend.""" + try: + data = request.json + if not data: + return jsonify({"error": "No data received"}), 400 + + session_id = data.get("session_id", "default") + + tool_name = data.get("function_name") or data.get("tool") or data.get("name") + tool_decision = data.get("tool_decision") or {} + if isinstance(tool_decision, dict): + tool_name = tool_name or tool_decision.get( + "function_name" + ) or tool_decision.get("tool") + + args = {} + if data.get("function_args") and isinstance(data.get("function_args"), dict): + args = data.get("function_args") + elif tool_decision.get("function_args") and isinstance( + tool_decision.get("function_args"), dict + ): + args = tool_decision.get("function_args") + + if not tool_name: + return jsonify({"error": "No tool name found"}), 400 + + if tool_name in ["nano_banana", "generate_image"]: + return handle_image_generation(args, session_id) + elif tool_name in ["veo", "generate_video"]: + return handle_video_generation(args, session_id) + elif tool_name == "extract_frame": + video_id = args.get("video_id") or get_last_id_for_type("video", session_id) + if video_id is None: + return jsonify({"error": "No videos found"}), 404 + return handle_extract_frame( + {"video_id": video_id, "timestamp": args.get("timestamp", 0)}, + session_id, + ) + elif tool_name == "combine_videos": + video_ids = args.get("video_ids", []) + if len(video_ids) < 2: + session_videos = [ + f + for f in file_history + if f.get("session_id") == session_id and f.get("type") == "video" + ] + video_ids = sorted([f["id"] for f in session_videos]) + if len(video_ids) < 2: + return jsonify({"error": "Need at least 2 videos"}), 400 + return handle_combine_videos({"video_ids": video_ids}, session_id) + elif tool_name == "view_generated_image": + image_id = args.get("image_id") + if image_id is None: + return jsonify({"error": "No image_id provided"}), 400 + img_path = Path(f"outputs/images/image_{image_id}.png") + if not img_path.exists(): + return jsonify({"error": f"Image {image_id} not found"}), 404 + if session_id in bridges: + bridges[session_id].put_nowait( + { + "type": "generated_image_feedback", + "data": {"mime_type": "image/png", "data": img_path.read_bytes()}, + "id": image_id, + } + ) + return jsonify({"response": f"Image {image_id} loaded", "image_id": image_id}) + + return jsonify({"response": "Unknown tool", "tool": tool_name}) + except Exception as e: + logging.error(f"API error: {e}") + traceback.print_exc() + return jsonify({"error": str(e)}), 500 + + +def handle_image_generation(args, session_id): + """Handle image generation requests.""" + session_state = get_session_state(session_id) + prompt = args.get("prompt") + aspect_ratio = args.get("aspect_ratio", args.get("aspectRatio", "1:1")) + if aspect_ratio not in IMAGE_ASPECT_RATIOS: + aspect_ratio = "1:1" + + ref_images = args.get("reference_images", []) + refers_to_camera = args.get("refers_to_camera", False) + refers_to_last = args.get("refers_to_last", False) + specific_image_id = args.get("image_id") + if specific_image_id is not None: + specific_image_id = int(specific_image_id) + + if refers_to_camera: + if session_state.get("uploaded_images"): + ref_images.extend(session_state.get("uploaded_images")) + elif session_state.get("last_seen_frame"): + ref_images.append(session_state.get("last_seen_frame")) + + if specific_image_id: + session_images = [ + f + for f in file_history + if f.get("session_id") == session_id and f.get("type") == "image" + ] + matching = [f for f in session_images if f.get("id") == specific_image_id] + if matching: + img_path = Path(matching[0]["path"]) + if img_path.exists(): + with open(img_path, "rb") as f: + img_data = base64.b64encode(f.read()).decode("utf-8") + ref_images.append(f"data:image/png;base64,{img_data}") + + if refers_to_last and not specific_image_id: + last_id = get_last_id_for_type("image", session_id) + if last_id: + img_path = Path(f"outputs/images/image_{last_id}.png") + if img_path.exists(): + with open(img_path, "rb") as f: + img_data = base64.b64encode(f.read()).decode("utf-8") + ref_images.append(f"data:image/png;base64,{img_data}") + + try: + generation_config = types.GenerateContentConfig( + temperature=0.7, + response_modalities=["TEXT", "IMAGE"], + image_config=types.ImageConfig(aspect_ratio=aspect_ratio, image_size="2K"), + ) + + content_parts = [] + for ref_img in ref_images[:4]: + if isinstance(ref_img, str) and ref_img.startswith("data:"): + try: + b = base64.b64decode(ref_img.split(",")[1]) + content_parts.append( + {"inline_data": {"mime_type": "image/png", "data": b}} + ) + except Exception: + pass + + if content_parts: + content_parts.append(f"Based on the reference image(s), create: {prompt}") + else: + content_parts.append(prompt) + + client = get_active_image_client() + if client is None: + return ( + jsonify( + { + "error": "Authentication required. Please enter your Project ID and access token." + } + ), + 401, + ) + response = client.models.generate_content( + model=GEMINI_IMAGE_MODEL, contents=content_parts, config=generation_config + ) + + output_dir = Path("outputs/images") + output_dir.mkdir(parents=True, exist_ok=True) + next_id = get_next_id_for_type("image", session_id) + image_path = output_dir / f"image_{next_id}.png" + + image_saved = False + for candidate in response.candidates: + if candidate.content and candidate.content.parts: + for part in candidate.content.parts: + if part.inline_data: + image_path.write_bytes(part.inline_data.data) + image_saved = True + break + + if not image_saved: + return jsonify({"error": "No image generated"}), 500 + + session_state["last_image"] = str(image_path) + file_history.append( + {"id": next_id, "type": "image", "path": str(image_path), "session_id": session_id} + ) + + return jsonify( + { + "response": f"Generated image {next_id}", + "file": { + "id": next_id, + "type": "image", + "url": f"/outputs/images/image_{next_id}.png", + }, + } + ) + except Exception as e: + logging.error(f"Image generation error: {e}") + traceback.print_exc() + return jsonify({"error": str(e)}), 500 + + +def handle_video_generation(args, session_id): + """Handle video generation requests.""" + import time + + session_state = get_session_state(session_id) + prompt = args.get("prompt", "") + duration = args.get("duration", 8) + refers_to_last = args.get("refers_to_last", False) + image_id = args.get("image_id") + if image_id is not None: + image_id = int(image_id) + + if duration <= 4: + duration = 4 + elif duration <= 6: + duration = 6 + else: + duration = 8 + + try: + reference_image = None + + if image_id: + img_path = Path(f"outputs/images/image_{image_id}.png") + if img_path.exists(): + reference_image = img_path + elif refers_to_last: + last_id = get_last_id_for_type("image", session_id) + if last_id: + img_path = Path(f"outputs/images/image_{last_id}.png") + if img_path.exists(): + reference_image = img_path + + if "oauth" not in session_credentials: + return ( + jsonify( + { + "error": "Authentication required. Please enter your Project ID and access token." + } + ), + 401, + ) + creds_data = session_credentials["oauth"] + creds = Credentials(token=creds_data["access_token"]) + video_client = genai.Client( + vertexai=True, + project=creds_data["project_id"], + location="global", + credentials=creds, + ) + + output_dir = Path("outputs/videos") + output_dir.mkdir(parents=True, exist_ok=True) + next_id = get_next_id_for_type("video", session_id) + video_path = output_dir / f"video_{next_id}.mp4" + + video_config = types.GenerateVideosConfig( + aspect_ratio="16:9", + number_of_videos=1, + duration_seconds=duration, + enhance_prompt=True, + person_generation="allow_adults", + ) + + if reference_image and reference_image.exists(): + operation = video_client.models.generate_videos( + model=VEO_MODEL_ID, + prompt=prompt, + image=types.Image( + image_bytes=reference_image.read_bytes(), mime_type="image/png" + ), + config=video_config, + ) + else: + operation = video_client.models.generate_videos( + model=VEO_MODEL_ID, + prompt=prompt, + config=video_config, + ) + + max_wait = 300 + waited = 0 + while not operation.done and waited < max_wait: + time.sleep(15) + waited += 15 + operation = video_client.operations.get(operation) + + if not operation.done: + return jsonify({"error": "Video generation timed out"}), 500 + + result = operation.result if hasattr(operation, "result") else operation.response + if not result or not result.generated_videos: + return jsonify({"error": "No video generated"}), 500 + + video = result.generated_videos[0].video + + if hasattr(video, "video_bytes") and video.video_bytes: + video_path.write_bytes(video.video_bytes) + elif hasattr(video, "uri") and video.uri: + gcs_match = re.match(r"gs://([^/]+)/(.+)", video.uri) + if gcs_match: + bucket_name, blob_name = gcs_match.groups() + bucket = storage_client.bucket(bucket_name) + blob = bucket.blob(blob_name) + blob.download_to_filename(str(video_path)) + else: + return jsonify({"error": "Invalid video URI"}), 500 + else: + return jsonify({"error": "No video data available"}), 500 + + session_state["last_video"] = str(video_path) + file_history.append( + {"id": next_id, "type": "video", "path": str(video_path), "session_id": session_id} + ) + + return jsonify( + { + "response": f"Generated video {next_id}", + "file": { + "id": next_id, + "type": "video", + "url": f"/outputs/videos/video_{next_id}.mp4", + }, + } + ) + + except Exception as e: + logging.error(f"Video generation error: {e}") + traceback.print_exc() + return jsonify({"error": str(e)}), 500 + + +def handle_extract_frame(args, session_id): + """Handle frame extraction from video.""" + import subprocess + + video_id = args.get("video_id", 1) + if video_id is not None: + video_id = int(video_id) + timestamp = args.get("timestamp", 0) + + try: + video_path = Path(f"outputs/videos/video_{video_id}.mp4") + if not video_path.exists(): + for f in file_history: + if f.get("type") == "video" and f.get("id") == video_id: + video_path = Path(f["path"]) + break + + if not video_path.exists(): + return jsonify({"error": f"Video {video_id} not found"}), 404 + + output_dir = Path("outputs/images") + output_dir.mkdir(parents=True, exist_ok=True) + next_id = get_next_id_for_type("image", session_id) + frame_path = output_dir / f"image_{next_id}.png" + + cmd = [ + "ffmpeg", + "-y", + "-ss", + str(timestamp), + "-i", + str(video_path), + "-vframes", + "1", + "-f", + "image2", + "-vcodec", + "png", + str(frame_path), + ] + result = subprocess.run(cmd, capture_output=True, text=True) + + if result.returncode != 0 or not frame_path.exists(): + return jsonify({"error": "Frame extraction failed"}), 500 + + session_state = get_session_state(session_id) + session_state["last_image"] = str(frame_path) + file_history.append( + {"id": next_id, "type": "image", "path": str(frame_path), "session_id": session_id} + ) + + return jsonify( + { + "response": f"Extracted frame {next_id} from video {video_id}", + "file": { + "id": next_id, + "type": "image", + "url": f"/outputs/images/image_{next_id}.png", + }, + } + ) + + except Exception as e: + logging.error(f"Frame extraction error: {e}") + traceback.print_exc() + return jsonify({"error": str(e)}), 500 + + +def handle_combine_videos(args, session_id): + """Handle combining multiple videos.""" + import subprocess + import tempfile + + video_ids = args.get("video_ids", []) + video_ids = [int(v) for v in video_ids] + + if len(video_ids) < 2: + return jsonify({"error": "Need at least 2 videos"}), 400 + + try: + video_paths = [] + for vid_id in video_ids: + video_path = Path(f"outputs/videos/video_{vid_id}.mp4") + if not video_path.exists(): + for f in file_history: + if f.get("type") == "video" and f.get("id") == vid_id: + video_path = Path(f["path"]) + break + if not video_path.exists(): + return jsonify({"error": f"Video {vid_id} not found"}), 404 + video_paths.append(video_path) + + output_dir = Path("outputs/videos") + output_dir.mkdir(parents=True, exist_ok=True) + next_id = get_next_id_for_type("video", session_id) + output_path = output_dir / f"video_{next_id}.mp4" + + with tempfile.NamedTemporaryFile(mode="w", suffix=".txt", delete=False) as f: + concat_file = f.name + for vp in video_paths: + f.write(f"file '{vp.absolute()}'\n") + + cmd = [ + "ffmpeg", + "-y", + "-f", + "concat", + "-safe", + "0", + "-i", + concat_file, + "-c", + "copy", + str(output_path), + ] + result = subprocess.run(cmd, capture_output=True, text=True) + Path(concat_file).unlink(missing_ok=True) + + if result.returncode != 0: + cmd = [ + "ffmpeg", + "-y", + "-f", + "concat", + "-safe", + "0", + "-i", + concat_file, + "-c:v", + "libx264", + "-c:a", + "aac", + "-preset", + "fast", + str(output_path), + ] + subprocess.run(cmd, capture_output=True, text=True) + + if not output_path.exists(): + return jsonify({"error": "Video combination failed"}), 500 + + session_state = get_session_state(session_id) + session_state["last_video"] = str(output_path) + file_history.append( + {"id": next_id, "type": "video", "path": str(output_path), "session_id": session_id} + ) + + return jsonify( + { + "response": f"Combined {len(video_ids)} videos into video {next_id}", + "file": { + "id": next_id, + "type": "video", + "url": f"/outputs/videos/video_{next_id}.mp4", + }, + } + ) + + except Exception as e: + logging.error(f"Video combination error: {e}") + traceback.print_exc() + return jsonify({"error": str(e)}), 500 + + +@app.route("/api/validate-token", methods=["POST"]) +def validate_token(): + """Validate user's access token.""" + global session_credentials + data = request.json + project_id = data.get("projectId") + location = data.get("location", "us-central1") + access_token = data.get("accessToken") + + if not project_id or not access_token: + return jsonify({"valid": False, "message": "Project ID and token required"}) + + try: + creds = Credentials(token=access_token) + + test_client = genai.Client( + vertexai=True, project=project_id, location=location, credentials=creds + ) + + test_client.models.generate_content( + model="gemini-2.0-flash", contents="Say 'ok' and nothing else" + ) + + session_credentials["oauth"] = { + "credentials": creds, + "project_id": project_id, + "location": location, + "access_token": access_token, + } + + logging.info(f"User authenticated with project: {project_id}") + return jsonify({"valid": True, "project": project_id}) + except Exception as e: + logging.error(f"Token validation failed: {e}") + return jsonify({"valid": False, "message": str(e)}) + + +@app.route("/api/auth-status", methods=["GET"]) +def auth_status(): + """Get current authentication status.""" + if "oauth" in session_credentials: + return jsonify( + {"authenticated": True, "project": session_credentials["oauth"]["project_id"]} + ) + return jsonify( + {"authenticated": False, "project": DEFAULT_PROJECT_ID, "using": "default"} + ) + + +@app.route("/api/logout", methods=["POST"]) +def logout(): + """Log out the current user.""" + global session_credentials + if "oauth" in session_credentials: + del session_credentials["oauth"] + logging.info("User logged out, using default credentials") + return jsonify({"success": True}) + + +@app.route("/api/set-user-name", methods=["POST"]) +def set_user_name_route(): + """Set the user's display name.""" + global user_name + user_name = request.json.get("name", "User") + return jsonify({"success": True, "name": user_name}) + + +@app.route("/api/get-user-name", methods=["GET"]) +def get_user_name_route(): + """Get the user's display name.""" + return jsonify({"name": user_name}) + + +@app.route("/api/get-system-instructions", methods=["GET"]) +def get_system_instructions(): + """Get custom system instructions.""" + return jsonify({"instructions": custom_system_instructions}) + + +@app.route("/api/set-system-instructions", methods=["POST"]) +def set_system_instructions(): + """Set custom system instructions.""" + global custom_system_instructions + custom_system_instructions = request.json.get("instructions", "").strip() + return jsonify({"success": True, "instructions": custom_system_instructions}) + + +@app.route("/api/clear-all", methods=["POST"]) +def clear_all(): + """Clear all session data.""" + global file_history + session_id = request.json.get("session_id", "default") + if session_id in session_states: + del session_states[session_id] + if session_id in session_handles: + del session_handles[session_id] + file_history = [f for f in file_history if f.get("session_id") != session_id] + return jsonify({"success": True}) + + +@app.route("/api/list-files", methods=["GET"]) +def api_list_files(): + """List all files for a session.""" + session_id = request.args.get("session_id", "default") + files = [f for f in file_history if f.get("session_id") == session_id] + return jsonify({"files": files}) + + +@app.route("/") +def serve_home(): + """Serve the main page.""" + return send_from_directory(".", "index.html") + + +@app.route("/outputs/") +def serve_output(filename): + """Serve generated output files.""" + return send_from_directory("outputs", filename) + + +@app.route("/src/") +def serve_src_files(filename): + """Serve source files.""" + return send_from_directory("src", filename) + + +@app.route("/style.css") +def serve_style(): + """Serve the stylesheet.""" + if Path("style.css").exists(): + return send_from_directory(".", "style.css") + return "", 200 + + +if __name__ == "__main__": + import shutil + + print("=" * 50) + print("GenMedia Live") + print(f"Project: {DEFAULT_PROJECT_ID}") + print(f"ffmpeg: {'available' if shutil.which('ffmpeg') else 'NOT FOUND'}") + print("=" * 50) + + socketio.run(app, host="0.0.0.0", port=8080, debug=False) diff --git a/vision/sample-apps/genmedia-live/index.html b/vision/sample-apps/genmedia-live/index.html new file mode 100644 index 0000000..42accb2 --- /dev/null +++ b/vision/sample-apps/genmedia-live/index.html @@ -0,0 +1,237 @@ + + + + + + + GenMedia Live + + + + + + + + + +
    +
    +
    +
    +

    GenMedia Live

    +

    Real-Time Multimodal AI Creation

    +
    + +
    +
    +
    + +
    +
    + + + +
    +
    +
    + +
    +
    +

    Made with ❀ by Wafae Bakkali | wafaeb@google.com

    +
    +
    + +
    + + + + + + diff --git a/vision/sample-apps/genmedia-live/requirements.txt b/vision/sample-apps/genmedia-live/requirements.txt new file mode 100644 index 0000000..5b31c83 --- /dev/null +++ b/vision/sample-apps/genmedia-live/requirements.txt @@ -0,0 +1,8 @@ +flask>=3.0.0 +flask-cors>=4.0.0 +flask-socketio>=5.3.5 +python-socketio>=5.10.0 +google-cloud-aiplatform>=1.71.1 +google-genai>=1.0.0 +google-cloud-storage>=2.14.0 +pillow>=10.4.0 diff --git a/vision/sample-apps/genmedia-live/src/features/genmedia-chat.js b/vision/sample-apps/genmedia-live/src/features/genmedia-chat.js new file mode 100644 index 0000000..a2ed128 --- /dev/null +++ b/vision/sample-apps/genmedia-live/src/features/genmedia-chat.js @@ -0,0 +1,877 @@ +/* + * Copyright 2026 Google LLC + * + * 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 + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { showToast } from '../ui.js'; + +let socket = null; +let audioContext = null; +let audioWorklet = null; +let mediaStream = null; +let sessionId = localStorage.getItem('genmedia_session_id') || `session-${Date.now()}`; +localStorage.setItem('genmedia_session_id', sessionId); + +let isRecording = false; +let isConnected = false; +let isConnecting = false; +let sessionStartPending = false; + +let audioQueue = []; +let isPlayingAudio = false; +let playbackAudioContext = null; +let currentAudioSource = null; + +let cameraStream = null; +let screenStream = null; +let cameraInterval = null; +let screenInterval = null; +let lastCameraFrame = null; +let isProcessingFrame = false; + +let conversationHistory = []; +let lastGeneratedImage = null; +let lastGeneratedVideo = null; +let currentUserName = "User"; +let uploadedImages = []; + +let messagesContainer, userInput, sendButton, voiceButton, cameraButton; +let screenShareButton, cameraVideo, cameraContainer, userNameInput; +let setNameButton, currentUserNameDisplay, clearAllButton, uploadButton; + +function clearSessionState() { + conversationHistory = []; + lastGeneratedImage = null; + lastGeneratedVideo = null; + lastCameraFrame = null; + uploadedImages = []; +} + +function showSpinner(message = 'Generating...') { + let spinner = document.getElementById('generation-spinner'); + if (!spinner) { + spinner = document.createElement('div'); + spinner.id = 'generation-spinner'; + spinner.innerHTML = ` +
    +
    + ${message} +
    + `; + document.body.appendChild(spinner); + } + spinner.querySelector('.spinner-text').textContent = message; + spinner.style.display = 'block'; +} + +function hideSpinner() { + const spinner = document.getElementById('generation-spinner'); + if (spinner) spinner.style.display = 'none'; +} + +export async function getGenMediaContent() { + try { + const response = await fetch('/src/features/templates/genmedia-chat.html'); + if (!response.ok) throw new Error(`Failed to load template: ${response.status}`); + return await response.text(); + } catch (error) { + console.error('Template load error:', error); + return '
    Failed to load chat interface. Please refresh.
    '; + } +} + +export function initGenMediaChat() { + clearSessionState(); + + setTimeout(() => { + messagesContainer = document.getElementById('messages-container'); + userInput = document.getElementById('user-input'); + sendButton = document.getElementById('send-button'); + voiceButton = document.getElementById('voice-button'); + uploadButton = document.getElementById('upload-button'); + cameraButton = document.getElementById('camera-button'); + screenShareButton = document.getElementById('screen-share-button'); + cameraVideo = document.getElementById('camera-video'); + cameraContainer = document.getElementById('camera-container'); + userNameInput = document.getElementById('user-name-input'); + setNameButton = document.getElementById('set-name-btn'); + currentUserNameDisplay = document.getElementById('current-user-name'); + clearAllButton = document.getElementById('clear-all-button'); + + if (!messagesContainer || !userInput || !sendButton) { + console.error('Required UI elements not found'); + return; + } + + initSocket(); + loadCurrentUserName(); + + sendButton.addEventListener('click', sendMessage); + userInput.addEventListener('keypress', (e) => { if (e.key === 'Enter') sendMessage(); }); + voiceButton.addEventListener('click', toggleVoice); + uploadButton.addEventListener('click', () => document.getElementById('image-upload-input').click()); + document.getElementById('image-upload-input').addEventListener('change', handleImageUpload); + document.getElementById('remove-upload-btn')?.addEventListener('click', removeUploadedImage); + cameraButton.addEventListener('click', toggleCamera); + screenShareButton.addEventListener('click', toggleScreenShare); + setNameButton.addEventListener('click', setUserName); + userNameInput.addEventListener('keypress', (e) => { if (e.key === 'Enter') setUserName(); }); + clearAllButton.addEventListener('click', clearAllData); + + document.addEventListener('click', () => { + if (playbackAudioContext?.state === 'suspended') { + playbackAudioContext.resume(); + } + }, { once: true }); + + const imageModal = document.createElement('div'); + imageModal.className = 'image-modal'; + imageModal.innerHTML = 'Full size'; + document.body.appendChild(imageModal); + imageModal.addEventListener('click', () => { imageModal.style.display = 'none'; }); + + window.enlargeImage = (src) => { + const modal = document.querySelector('.image-modal'); + if (modal) { + modal.querySelector('img').src = src; + modal.style.display = 'block'; + } + }; + }, 100); +} + +async function loadCurrentUserName() { + try { + const response = await fetch('/api/get-user-name'); + const data = await response.json(); + if (data.name && currentUserNameDisplay) { + currentUserNameDisplay.textContent = data.name; + currentUserName = data.name; + } + } catch (error) { + console.error('Load user name error:', error); + } +} + +async function setUserName() { + const name = userNameInput.value.trim(); + if (!name) { + showToast('Please enter a name', 'warning'); + return; + } + + try { + const response = await fetch('/api/set-user-name', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ name }) + }); + + const data = await response.json(); + if (data.success && currentUserNameDisplay) { + currentUserNameDisplay.textContent = data.name; + currentUserName = data.name; + showToast(`Hi ${data.name}!`, 'success'); + userNameInput.value = ''; + } + } catch (error) { + console.error('Set name error:', error); + showToast('Failed to set name', 'error'); + } +} + +async function handleImageUpload(event) { + const files = Array.from(event.target.files); + if (!files.length) return; + + if (uploadedImages.length + files.length > 14) { + showToast(`Max 14 images. You have ${uploadedImages.length}.`, 'warning'); + return; + } + + for (const file of files) { + if (!file.type.startsWith('image/')) { + showToast('Please select only image files', 'error'); + return; + } + } + + const newImages = []; + for (const file of files) { + const reader = new FileReader(); + await new Promise((resolve) => { + reader.onload = (e) => { + newImages.push(e.target.result); + resolve(); + }; + reader.readAsDataURL(file); + }); + } + + uploadedImages.push(...newImages); + updateUploadPreview(); + showToast(`${uploadedImages.length} image(s) ready`, 'success'); +} + +function updateUploadPreview() { + const previewContainer = document.getElementById('upload-preview-container'); + const previewGrid = document.getElementById('upload-preview-grid'); + const uploadCount = document.getElementById('upload-count'); + + if (!uploadedImages.length) { + previewContainer.style.display = 'none'; + return; + } + + uploadCount.textContent = uploadedImages.length; + previewGrid.innerHTML = ''; + + uploadedImages.forEach((img, index) => { + const div = document.createElement('div'); + div.className = 'relative group'; + div.innerHTML = ` + Upload ${index + 1} + + `; + previewGrid.appendChild(div); + }); + + previewContainer.style.display = 'block'; +} + +window.clearAllUploadedImages = () => { + uploadedImages = []; + updateUploadPreview(); + document.getElementById('image-upload-input').value = ''; + showToast('All images cleared', 'info'); +}; + +window.removeUploadedImageAt = (index) => { + uploadedImages.splice(index, 1); + updateUploadPreview(); + document.getElementById('image-upload-input').value = ''; +}; + +function removeUploadedImage() { + uploadedImages = []; + updateUploadPreview(); + document.getElementById('image-upload-input').value = ''; + showToast('Images removed', 'info'); +} + +async function clearAllData() { + if (!confirm('Clear all content and reset session?')) return; + + try { + showSpinner('Clearing...'); + + if (isConnected) { + socket.emit('stop_live_session', { session_id: sessionId }); + isConnected = false; + } + + await fetch('/api/clear-all', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ session_id: sessionId }) + }); + + conversationHistory = []; + lastGeneratedImage = null; + lastGeneratedVideo = null; + lastCameraFrame = null; + uploadedImages = []; + removeUploadedImage(); + + localStorage.removeItem('genmedia_session_id'); + sessionId = `session-${Date.now()}`; + localStorage.setItem('genmedia_session_id', sessionId); + + if (messagesContainer) { + messagesContainer.innerHTML = ` +
    +

    All cleared, ${currentUserName}!

    +

    Start fresh - talk or type to begin

    +
    + `; + } + + if (isRecording) stopVoice(); + if (cameraStream) stopCamera(); + if (screenStream) stopScreenShare(); + + updateConnectionStatus('Ready', 'success'); + showToast('All cleared!', 'success'); + } catch (error) { + console.error('Clear error:', error); + showToast('Failed to clear data', 'error'); + } finally { + hideSpinner(); + } +} + +function updateConnectionStatus(status, type) { + const statusEl = document.getElementById('connection-status'); + if (!statusEl) return; + + const dotClass = type === 'success' ? 'status-ready' : + type === 'active' ? 'status-active' : + type === 'warning' ? 'status-connecting' : 'status-error'; + + statusEl.innerHTML = `${status}`; +} + +function initSocket() { + if (socket?.connected) return; + if (socket) socket.close(); + + socket = io({ + transports: ['polling'], + upgrade: false, + reconnection: true, + reconnectionDelay: 1000, + reconnectionDelayMax: 3000, + reconnectionAttempts: Infinity, + timeout: 60000 + }); + + socket.on('connect', () => { + updateConnectionStatus('Ready', 'success'); + socket.emit('check_session_status', { session_id: sessionId }, (response) => { + if (response?.active) { + isConnected = true; + updateConnectionStatus('Live', 'active'); + } + }); + }); + + socket.on('disconnect', (reason) => { + if (reason === 'io server disconnect' || reason === 'io client disconnect') { + updateConnectionStatus('Disconnected', 'error'); + isConnected = false; + } + isConnecting = false; + sessionStartPending = false; + }); + + socket.on('live_session_started', (data) => { + isConnected = true; + isConnecting = false; + sessionStartPending = false; + updateConnectionStatus('Live', 'active'); + if (data.user_name) currentUserName = data.user_name; + }); + + socket.on('live_session_ended', () => { + isConnected = false; + updateConnectionStatus('Disconnected', 'inactive'); + uploadedImages = []; + updateUploadPreview(); + }); + + socket.on('reconnect', () => { + updateConnectionStatus('Ready', 'success'); + socket.emit('check_session_status', { session_id: sessionId }, (response) => { + if (response?.active) { + isConnected = true; + updateConnectionStatus('Live', 'active'); + } + }); + }); + + socket.on('audio_response', playAudioResponse); + + socket.on('voice_generation_request', async (data) => { + const { text, function_name, function_args, function_call_id } = data; + conversationHistory.push({ role: 'user', content: text }); + + const spinnerMsgs = { + generate_image: 'Generating image...', + generate_video: 'Generating video...', + extract_frame: 'Extracting frame...', + combine_videos: 'Combining videos...' + }; + showSpinner(spinnerMsgs[function_name] || 'Processing...'); + + try { + const payload = { + session_id: sessionId, + function_name, + function_args: { ...function_args }, + function_call_id + }; + + if (function_name === 'generate_image') { + const ref_images = []; + if (function_args.refers_to_camera && uploadedImages.length > 0) { + ref_images.push(...uploadedImages); + } + if (function_args.refers_to_camera && lastCameraFrame && uploadedImages.length === 0) { + ref_images.push(lastCameraFrame); + } + if (ref_images.length > 0) { + payload.function_args.reference_images = ref_images; + } + } + + const response = await fetch('/api/genmedia-chat', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(payload) + }); + + if (!response.ok) throw new Error(`HTTP ${response.status}`); + const result = await response.json(); + + if (result.file) { + displayGeneratedContent(result); + if (result.file.type === 'image') { + lastGeneratedImage = result.file.path || result.file.url; + } else if (result.file.type === 'video') { + lastGeneratedVideo = result.file.path || result.file.url; + } + socket.emit('generation_completed', { + session_id: sessionId, + type: result.file.type, + id: result.file.id, + function_call_id, + user_request: text + }); + } + + if (result.response) { + conversationHistory.push({ role: 'assistant', content: result.response }); + } + + if (result.error) { + showToast(result.error, 'error'); + addMessage('error', result.error); + } + } catch (error) { + console.error('Generation error:', error); + showToast('Generation failed', 'error'); + addMessage('error', 'Generation failed: ' + error.message); + } finally { + hideSpinner(); + } + }); + + socket.on('session_ended_reconnect', () => { + isConnected = false; + if (cameraStream || screenStream || isRecording) { + if (!isConnecting) { + isConnecting = true; + updateConnectionStatus('Reconnecting...', 'warning'); + setTimeout(() => { + requestSessionStart(); + isConnecting = false; + }, 500); + } + } else { + updateConnectionStatus('Ready', 'success'); + } + }); + + socket.on('live_session_error', (data) => { + showToast('Session error: ' + data.error, 'error'); + hideSpinner(); + isConnecting = false; + sessionStartPending = false; + }); +} + +async function waitForConnection(timeout = 15000) { + return new Promise((resolve) => { + if (isConnected) { + resolve(true); + return; + } + const startTime = Date.now(); + const checkConnection = setInterval(() => { + if (isConnected) { + clearInterval(checkConnection); + resolve(true); + } else if (Date.now() - startTime > timeout) { + clearInterval(checkConnection); + resolve(false); + } + }, 100); + }); +} + +function requestSessionStart() { + if (sessionStartPending) return false; + sessionStartPending = true; + socket.emit('start_live_session', { session_id: sessionId }); + return true; +} + +async function sendMessage() { + if (!userInput) return; + const message = userInput.value.trim(); + if (!message) return; + + if (isConnecting) { + showToast('Please wait, connecting...', 'warning'); + return; + } + + addMessage('user', message); + userInput.value = ''; + conversationHistory.push({ role: 'user', content: message }); + + if (!isConnected && !isConnecting) { + isConnecting = true; + updateConnectionStatus('Connecting...', 'warning'); + showSpinner('Connecting...'); + requestSessionStart(); + + const connected = await waitForConnection(30000); + hideSpinner(); + isConnecting = false; + + if (!connected) { + sessionStartPending = false; + showToast('Connection timeout. Please try again.', 'error'); + return; + } + await new Promise(resolve => setTimeout(resolve, 500)); + } + + if (isConnected) { + if (uploadedImages.length > 0) { + socket.emit('send_message_with_images', { + session_id: sessionId, + text: message, + images: uploadedImages + }); + } else { + socket.emit('send_text_message', { session_id: sessionId, text: message }); + } + } else { + showToast('Not connected. Please try again.', 'error'); + } +} + +async function toggleVoice() { + if (!isRecording) await startVoice(); + else stopVoice(); +} + +async function startVoice() { + try { + if (isConnecting) return; + + if (!playbackAudioContext) { + playbackAudioContext = new (window.AudioContext || window.webkitAudioContext)({ sampleRate: 24000 }); + if (playbackAudioContext.state === 'suspended') await playbackAudioContext.resume(); + } + + if (!isConnected && !isConnecting) { + isConnecting = true; + updateConnectionStatus('Connecting...', 'warning'); + showSpinner('Connecting...'); + requestSessionStart(); + + const connected = await waitForConnection(30000); + hideSpinner(); + + if (!connected) { + isConnecting = false; + sessionStartPending = false; + showToast('Connection timeout. Please try again.', 'error'); + return; + } + isConnecting = false; + await new Promise(resolve => setTimeout(resolve, 500)); + } + + if (uploadedImages.length > 0 && isConnected && socket?.connected) { + socket.emit('send_message_with_images', { + session_id: sessionId, + text: 'I have uploaded an image. Please look at it.', + images: uploadedImages + }); + await new Promise(resolve => setTimeout(resolve, 3000)); + } + + mediaStream = await navigator.mediaDevices.getUserMedia({ + audio: { channelCount: 1, sampleRate: 16000, echoCancellation: true, noiseSuppression: true, autoGainControl: true } + }); + + audioContext = new (window.AudioContext || window.webkitAudioContext)({ sampleRate: 16000 }); + const source = audioContext.createMediaStreamSource(mediaStream); + const processor = audioContext.createScriptProcessor(4096, 1, 1); + + let audioBuffer = []; + let lastSendTime = Date.now(); + const SEND_INTERVAL_MS = 500; + + processor.onaudioprocess = (e) => { + if (!isRecording || !isConnected) return; + const inputData = e.inputBuffer.getChannelData(0); + const int16Array = new Int16Array(inputData.length); + for (let i = 0; i < inputData.length; i++) { + const s = Math.max(-1, Math.min(1, inputData[i])); + int16Array[i] = s < 0 ? s * 0x8000 : s * 0x7FFF; + } + + audioBuffer.push(...Array.from(int16Array)); + + const now = Date.now(); + if (now - lastSendTime >= SEND_INTERVAL_MS && audioBuffer.length > 0) { + socket.emit('send_audio', { session_id: sessionId, audio: audioBuffer }); + audioBuffer = []; + lastSendTime = now; + } + }; + + source.connect(processor); + processor.connect(audioContext.destination); + audioWorklet = processor; + + isRecording = true; + voiceButton.classList.add('active'); + voiceButton.querySelector('span').textContent = 'Stop'; + showToast(`Listening, ${currentUserName}!`, 'success'); + } catch (error) { + console.error('Voice error:', error); + showToast('Failed to start voice', 'error'); + hideSpinner(); + isConnecting = false; + } +} + +function stopVoice() { + if (audioWorklet) { audioWorklet.disconnect(); audioWorklet = null; } + if (audioContext) { audioContext.close(); audioContext = null; } + if (mediaStream) { mediaStream.getTracks().forEach(track => track.stop()); mediaStream = null; } + + isRecording = false; + voiceButton.classList.remove('active'); + voiceButton.querySelector('span').textContent = 'Voice'; + + updateConnectionStatus(isConnected ? 'Session Active' : 'Ready', isConnected ? 'active' : 'success'); + showToast('Mic off', 'info'); +} + +async function toggleCamera() { + if (!cameraStream) await startCamera(); + else stopCamera(); +} + +async function startCamera() { + try { + if (!isConnected && !isConnecting) { + isConnecting = true; + updateConnectionStatus('Connecting...', 'warning'); + requestSessionStart(); + await waitForConnection(30000); + isConnecting = false; + } + + cameraStream = await navigator.mediaDevices.getUserMedia({ video: { width: 768, height: 768 } }); + cameraVideo.srcObject = cameraStream; + cameraContainer.style.display = 'block'; + cameraButton.classList.add('active'); + cameraInterval = setInterval(sendCameraFrame, 2000); + showToast('Camera on', 'success'); + } catch (error) { + console.error('Camera error:', error); + showToast('Camera failed', 'error'); + } +} + +function stopCamera() { + if (cameraInterval) { clearInterval(cameraInterval); cameraInterval = null; } + if (cameraStream) { cameraStream.getTracks().forEach(track => track.stop()); cameraStream = null; } + cameraVideo.srcObject = null; + cameraContainer.style.display = 'none'; + cameraButton.classList.remove('active'); + showToast('Camera off', 'info'); +} + +function sendCameraFrame() { + if (!cameraVideo || !cameraStream || !isConnected || isProcessingFrame) return; + isProcessingFrame = true; + + const canvas = document.createElement('canvas'); + canvas.width = 768; canvas.height = 768; + const ctx = canvas.getContext('2d'); + const ar = cameraVideo.videoWidth / cameraVideo.videoHeight; + let dw = 768, dh = 768, ox = 0, oy = 0; + if (ar > 1) { dh = 768/ar; oy = (768-dh)/2; } else { dw = 768*ar; ox = (768-dw)/2; } + ctx.drawImage(cameraVideo, ox, oy, dw, dh); + + canvas.toBlob((blob) => { + const reader = new FileReader(); + reader.onloadend = () => { + const base64 = reader.result.split(',')[1]; + lastCameraFrame = `data:image/jpeg;base64,${base64}`; + socket.emit('send_camera_frame', { session_id: sessionId, frame: base64 }); + setTimeout(() => { isProcessingFrame = false; }, 50); + }; + reader.readAsDataURL(blob); + }, 'image/jpeg', 0.6); +} + +async function toggleScreenShare() { + if (!screenStream) await startScreenShare(); + else stopScreenShare(); +} + +async function startScreenShare() { + try { + if (!isConnected && !isConnecting) { + isConnecting = true; + updateConnectionStatus('Connecting...', 'warning'); + requestSessionStart(); + await waitForConnection(30000); + isConnecting = false; + } + + screenStream = await navigator.mediaDevices.getDisplayMedia({ video: { width: 768, height: 768 } }); + const screenVideo = document.createElement('video'); + screenVideo.srcObject = screenStream; + screenVideo.autoplay = true; + screenVideo.style.display = 'none'; + document.body.appendChild(screenVideo); + screenShareButton.classList.add('active'); + screenInterval = setInterval(() => sendScreenFrame(screenVideo), 2000); + screenStream.getVideoTracks()[0].addEventListener('ended', stopScreenShare); + showToast('Screen sharing', 'success'); + } catch (error) { + console.error('Screen error:', error); + showToast('Screen share failed', 'error'); + } +} + +function stopScreenShare() { + if (screenInterval) { clearInterval(screenInterval); screenInterval = null; } + if (screenStream) { screenStream.getTracks().forEach(track => track.stop()); screenStream = null; } + screenShareButton.classList.remove('active'); + showToast('Screen sharing stopped', 'info'); +} + +function sendScreenFrame(screenVideo) { + if (!screenVideo || !screenStream || !isConnected || isProcessingFrame) return; + isProcessingFrame = true; + + const canvas = document.createElement('canvas'); + canvas.width = 768; canvas.height = 768; + const ctx = canvas.getContext('2d'); + const ar = screenVideo.videoWidth / screenVideo.videoHeight; + let dw = 768, dh = 768, ox = 0, oy = 0; + if (ar > 1) { dh = 768/ar; oy = (768-dh)/2; } else { dw = 768*ar; ox = (768-dw)/2; } + ctx.drawImage(screenVideo, ox, oy, dw, dh); + + canvas.toBlob((blob) => { + const reader = new FileReader(); + reader.onloadend = () => { + const base64 = reader.result.split(',')[1]; + lastCameraFrame = `data:image/jpeg;base64,${base64}`; + socket.emit('send_camera_frame', { session_id: sessionId, frame: base64 }); + setTimeout(() => { isProcessingFrame = false; }, 50); + }; + reader.readAsDataURL(blob); + }, 'image/jpeg', 0.6); +} + +function playAudioResponse(data) { + if (!data.audio) return; + audioQueue.push(data); + if (!isPlayingAudio) playNextInQueue(); +} + +async function playNextInQueue() { + if (audioQueue.length === 0) { isPlayingAudio = false; return; } + isPlayingAudio = true; + const data = audioQueue.shift(); + + try { + if (!playbackAudioContext) { + playbackAudioContext = new (window.AudioContext || window.webkitAudioContext)({ sampleRate: 24000 }); + } + if (playbackAudioContext.state === 'suspended') await playbackAudioContext.resume(); + + const bytes = Uint8Array.from(atob(data.audio), c => c.charCodeAt(0)); + const int16 = new Int16Array(bytes.buffer); + const float32 = new Float32Array(int16.length); + for (let i = 0; i < int16.length; i++) { + float32[i] = int16[i] / (int16[i] < 0 ? 32768 : 32767); + } + + const buf = playbackAudioContext.createBuffer(1, float32.length, 24000); + buf.getChannelData(0).set(float32); + const src = playbackAudioContext.createBufferSource(); + src.buffer = buf; + src.connect(playbackAudioContext.destination); + currentAudioSource = src; + src.onended = () => { currentAudioSource = null; playNextInQueue(); }; + src.start(0); + } catch (error) { + console.error('Audio playback error:', error); + playNextInQueue(); + } +} + +function addMessage(role, content) { + if (!messagesContainer) return; + const div = document.createElement('div'); + div.className = `message message-${role} mb-3`; + const bubbleClass = role === 'user' ? 'bg-indigo-100 dark:bg-indigo-900 text-indigo-900 dark:text-indigo-100' : + role === 'error' ? 'bg-red-100 dark:bg-red-900 text-red-900 dark:text-red-100' : + 'bg-gray-100 dark:bg-gray-700 text-gray-900 dark:text-gray-100'; + div.innerHTML = `
    ${escapeHtml(content)}
    `; + messagesContainer.appendChild(div); + messagesContainer.scrollTop = messagesContainer.scrollHeight; +} + +function displayGeneratedContent(result) { + if (!messagesContainer) return; + const div = document.createElement('div'); + div.className = 'message message-assistant mb-4'; + const file = result.file; + const url = file.url || file.path; + if (!url) return; + + if (file.type === 'image') { + div.innerHTML = ` +
    +
    + + Download +
    + Generated +
    `; + } else if (file.type === 'video') { + div.innerHTML = ` +
    +
    + + Download +
    + +
    `; + } + + messagesContainer.appendChild(div); + messagesContainer.scrollTop = messagesContainer.scrollHeight; +} + +function escapeHtml(text) { + const div = document.createElement('div'); + div.textContent = text; + return div.innerHTML; +} diff --git a/vision/sample-apps/genmedia-live/src/features/templates/genmedia-chat.html b/vision/sample-apps/genmedia-live/src/features/templates/genmedia-chat.html new file mode 100644 index 0000000..0c669b1 --- /dev/null +++ b/vision/sample-apps/genmedia-live/src/features/templates/genmedia-chat.html @@ -0,0 +1,331 @@ + + + +
    +
    +

    + Voice, camera, and text-driven AI creation +

    +

    + Gemini Live ‱ Gemini 3 Pro Image ‱ Veo +

    +
    + +
    +
    +
    + + + +
    +
    + Current: + User +
    +
    +
    + + + +
    +
    +

    Welcome to GenMedia Live

    +

    Set your name above, then start talking or typing

    +
    +
    + +
    + + + +
    + + + + + + + + + + + +
    + + Ready +
    +
    + +
    + + +
    +
    +
    diff --git a/vision/sample-apps/genmedia-live/src/main.js b/vision/sample-apps/genmedia-live/src/main.js new file mode 100644 index 0000000..9eb6ecc --- /dev/null +++ b/vision/sample-apps/genmedia-live/src/main.js @@ -0,0 +1,118 @@ +/* + * Copyright 2026 Google LLC + * + * 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 + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { getGenMediaContent, initGenMediaChat } from './features/genmedia-chat.js'; +import { showToast } from './ui.js'; + +function initDarkMode() { + const themeToggleBtn = document.getElementById('theme-toggle'); + const lightIcon = document.getElementById('theme-toggle-light-icon'); + const darkIcon = document.getElementById('theme-toggle-dark-icon'); + + if (!themeToggleBtn || !lightIcon || !darkIcon) return; + + const savedTheme = localStorage.getItem('theme') || 'light'; + applyTheme(savedTheme); + + function applyTheme(theme) { + const html = document.documentElement; + if (theme === 'dark') { + html.classList.add('dark'); + lightIcon.style.display = 'block'; + darkIcon.style.display = 'none'; + } else { + html.classList.remove('dark'); + lightIcon.style.display = 'none'; + darkIcon.style.display = 'block'; + } + } + + themeToggleBtn.addEventListener('click', () => { + const isDark = document.documentElement.classList.contains('dark'); + const newTheme = isDark ? 'light' : 'dark'; + applyTheme(newTheme); + localStorage.setItem('theme', newTheme); + showToast(`Switched to ${newTheme} mode`, 'info', 2000); + }); + + setTimeout(() => { + document.body.classList.add('transition-colors', 'duration-300'); + }, 100); +} + +async function initApp() { + try { + initDarkMode(); + + const mainContent = document.getElementById('main-content'); + if (!mainContent) { + throw new Error('Main content container not found'); + } + + const chatContent = await getGenMediaContent(); + mainContent.innerHTML = chatContent; + mainContent.classList.add('fade-in'); + setTimeout(() => mainContent.classList.remove('fade-in'), 500); + + initGenMediaChat(); + + setTimeout(() => { + const theme = document.documentElement.classList.contains('dark') ? 'dark' : 'light'; + showToast(`Welcome to GenMedia Live! (${theme} mode)`, 'success', 3000); + }, 500); + + } catch (error) { + console.error('Init failed:', error); + showToast('Failed to load. Please refresh.', 'error'); + } +} + +function initKeyboardShortcuts() { + document.addEventListener('keydown', (e) => { + if ((e.ctrlKey || e.metaKey) && e.shiftKey && e.key === 'D') { + e.preventDefault(); + document.getElementById('theme-toggle')?.click(); + } + }); +} + +document.addEventListener('visibilitychange', () => { + if (!document.hidden) { + const savedTheme = localStorage.getItem('theme'); + if (savedTheme) { + const html = document.documentElement; + const isDark = html.classList.contains('dark'); + if (savedTheme === 'dark' && !isDark) { + html.classList.add('dark'); + } else if (savedTheme === 'light' && isDark) { + html.classList.remove('dark'); + } + } + } +}); + +if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', () => { + initApp(); + initKeyboardShortcuts(); + }); +} else { + initApp(); + initKeyboardShortcuts(); +} + +window.GenMediaLive = { version: '1.0.0', name: 'GenMedia Live' }; +export { initApp }; diff --git a/vision/sample-apps/genmedia-live/src/ui.js b/vision/sample-apps/genmedia-live/src/ui.js new file mode 100644 index 0000000..526ffd6 --- /dev/null +++ b/vision/sample-apps/genmedia-live/src/ui.js @@ -0,0 +1,57 @@ +/* + * Copyright 2026 Google LLC + * + * 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 + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export function showToast(message, type = 'success', duration = 3000) { + let toast = document.getElementById('notification-toast'); + if (!toast) { + toast = document.createElement('div'); + toast.id = 'notification-toast'; + document.body.appendChild(toast); + } + + clearTimeout(toast.hideTimer); + toast.textContent = message; + + const baseClasses = 'fixed bottom-20 left-1/2 transform -translate-x-1/2 px-6 py-3 rounded-full font-semibold text-sm shadow-lg transition-all duration-300 z-50'; + const typeClasses = { + success: 'bg-green-500 text-white', + error: 'bg-red-500 text-white', + warning: 'bg-yellow-500 text-white', + info: 'bg-blue-500 text-white' + }; + + toast.className = `${baseClasses} ${typeClasses[type] || typeClasses.info} opacity-100 translate-y-0`; + + toast.hideTimer = setTimeout(() => { + toast.className = `${baseClasses} ${typeClasses[type] || typeClasses.info} opacity-0 translate-y-2`; + }, duration); +} + +export async function copyToClipboard(elementId) { + const element = document.getElementById(elementId); + if (!element || !navigator.clipboard) { + showToast('Clipboard not available', 'error'); + return; + } + + try { + await navigator.clipboard.writeText(element.innerText); + showToast('Copied to clipboard!', 'success'); + } catch (err) { + console.error('Copy failed:', err); + showToast('Failed to copy', 'error'); + } +} diff --git a/vision/sample-apps/genmedia-live/style.css b/vision/sample-apps/genmedia-live/style.css new file mode 100644 index 0000000..5a4087b --- /dev/null +++ b/vision/sample-apps/genmedia-live/style.css @@ -0,0 +1,98 @@ +/* + * Copyright 2026 Google LLC + * + * 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 + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +::-webkit-scrollbar { + width: 8px; + height: 8px; +} + +::-webkit-scrollbar-track { + background: #f1f5f9; +} + +.dark ::-webkit-scrollbar-track { + background: #1e293b; +} + +::-webkit-scrollbar-thumb { + background: #cbd5e1; + border-radius: 4px; +} + +.dark ::-webkit-scrollbar-thumb { + background: #475569; +} + +::-webkit-scrollbar-thumb:hover { + background: #94a3b8; +} + +.dark ::-webkit-scrollbar-thumb:hover { + background: #64748b; +} + +*:focus { + outline: 2px solid #6366f1; + outline-offset: 2px; +} + +.dark *:focus { + outline-color: #818cf8; +} + +@keyframes fadeIn { + from { opacity: 0; transform: translateY(10px); } + to { opacity: 1; transform: translateY(0); } +} + +.fade-in { + animation: fadeIn 0.5s ease-in-out; +} + +@keyframes spin { + to { transform: rotate(360deg); } +} + +.spinner { + animation: spin 1s linear infinite; +} + +#notification-toast { + font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + pointer-events: none; +} + +html.dark { + color-scheme: dark; +} + +html { + color-scheme: light; +} + +body { + min-height: 100vh; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + } +} diff --git a/vision/use-cases/batch_virtual_try_on.ipynb b/vision/use-cases/batch_virtual_try_on.ipynb new file mode 100644 index 0000000..bea2807 --- /dev/null +++ b/vision/use-cases/batch_virtual_try_on.ipynb @@ -0,0 +1,492 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "LxultT3VptKy" + }, + "outputs": [], + "source": [ + "# Copyright 2025 Google LLC\n", + "#\n", + "# Licensed under the Apache License, Version 2.0 (the \"License\");\n", + "# you may not use this file except in compliance with the License.\n", + "# You may obtain a copy of the License at\n", + "#\n", + "# https://www.apache.org/licenses/LICENSE-2.0\n", + "#\n", + "# Unless required by applicable law or agreed to in writing, software\n", + "# distributed under the License is distributed on an \"AS IS\" BASIS,\n", + "# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n", + "# See the License for the specific language governing permissions and\n", + "# limitations under the License." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "FROnodhKp4vU" + }, + "source": [ + "# Virtual Try-On: Batch Generation Pipeline\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + "
    \n", + " \n", + " \"Google
    Open in Colab\n", + "
    \n", + "
    \n", + " \n", + " \"Google
    Open in Colab Enterprise\n", + "
    \n", + "
    \n", + " \n", + " \"Workbench
    Open in Workbench\n", + "
    \n", + "
    \n", + " \n", + " \"GitHub
    View on GitHub\n", + "
    \n", + "
    \n", + "\n", + "
    \n", + "\n", + "

    \n", + "Share to:\n", + "\n", + "\n", + " \"LinkedIn\n", + "\n", + "\n", + "\n", + " \"Bluesky\n", + "\n", + "\n", + "\n", + " \"X\n", + "\n", + "\n", + "\n", + " \"Reddit\n", + "\n", + "\n", + "\n", + " \"Facebook\n", + "\n", + "

    " + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "kVzXSHsWzfcx" + }, + "source": [ + "| Authors |\n", + "| --- |\n", + "| [Wafae Bakkali](https://github.com/WafaeBakkali) |\n", + "| [Katie Nguyen](https://github.com/katiemn) |" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "exrbLwZCzwdm" + }, + "source": [ + "## Overview\n", + "\n", + "### Virtual Try-On\n", + "\n", + "[Virtual Try-On](https://cloud.google.com/vertex-ai/generative-ai/docs/image/generate-virtual-try-on-images) uses Google's advanced image generation models to create high-quality images of people virtually trying on clothing. By providing an image of a human model and a clothing product, you can generate an image of the model wearing that product. This technology is designed for application developers and retailers in the fashion industry.\n", + "\n", + "This tutorial demonstrates how to use the Virtual Try-On API to automate batch processing, i.e., generating multiple results across various combinations of person and apparel images. In this tutorial, you will learn how to use the Google Gen AI SDK for Python to interact with the Virtual Try-On model to:\n", + " - Upload images from local sources, Cloud Storage, or other URL-based inputs\n", + " - Return side-by-side visual outputs to preview the try-on results" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "nd86wufNz0Ze" + }, + "source": [ + "## Get started" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "qUAdq4Iz-hoy" + }, + "source": [ + "### Install Google Gen AI SDK for Python" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "ImzTDW9m-kuD" + }, + "outputs": [], + "source": [ + "%pip install --upgrade --quiet google-genai" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "caZ9JpIlh9V_" + }, + "source": [ + "### Authenticate your notebook environment (Colab only)\n", + "\n", + "If you are running this notebook on Google Colab, run the following cell to authenticate your environment." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": { + "id": "XWCHsLNJREe2" + }, + "outputs": [], + "source": [ + "import sys\n", + "\n", + "if \"google.colab\" in sys.modules:\n", + " from google.colab import auth\n", + "\n", + " auth.authenticate_user()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "9OHJ7O1fALAB" + }, + "source": [ + "### Import libraries" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": { + "id": "wbj-UXJNAN4V" + }, + "outputs": [], + "source": [ + "import io\n", + "import os\n", + "import time\n", + "import zipfile\n", + "\n", + "import matplotlib.image as img\n", + "import matplotlib.pyplot as plt\n", + "import numpy as np\n", + "import requests\n", + "from PIL import Image as PIL_Image\n", + "from google import genai\n", + "from google.colab import files\n", + "from google.genai.types import Image, ProductImage, RecontextImageSource" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "Py2XWxe3RRud" + }, + "source": [ + "### Set Google Cloud project information\n", + "\n", + "To get started using Agent Platform, you must have an existing Google Cloud project and [enable the Agent Platform API](https://console.cloud.google.com/flows/enableapi?apiid=aiplatform.googleapis.com).\n", + "\n", + "Learn more about [setting up a project](https://docs.cloud.google.com/resource-manager/docs/creating-managing-projects) and a [development environment](https://cloud.google.com/docs/authentication/set-up-adc-local-dev-environment)." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": { + "id": "iEhbosxKRPcH" + }, + "outputs": [], + "source": [ + "# fmt: off\n", + "PROJECT_ID = \"[your-project-id]\" # @param {type: \"string\", placeholder: \"[your-project-id]\", isTemplate: true}\n", + "# fmt: on\n", + "if not PROJECT_ID or PROJECT_ID == \"[your-project-id]\":\n", + " PROJECT_ID = str(os.environ.get(\"GOOGLE_CLOUD_PROJECT\"))\n", + "\n", + "LOCATION = os.environ.get(\"GOOGLE_CLOUD_REGION\", \"us-central1\")\n", + "\n", + "client = genai.Client(enterprise=True, project=PROJECT_ID, location=LOCATION)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "rlHjmqj5A3k8" + }, + "source": [ + "### Define helper functions" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": { + "id": "3mrS8px7RdjF" + }, + "outputs": [], + "source": [ + "def display_images(images: list, type: str) -> None:\n", + " fig, axes = plt.subplots(1, len(images), figsize=(12, 6))\n", + " if len(images) == 1:\n", + " axes = np.array([axes])\n", + " for i, ax in enumerate(axes):\n", + " if type == \"local\":\n", + " image = img.imread(images[i])\n", + " elif type == \"url\":\n", + " response = requests.get(images[i])\n", + " image = PIL_Image.open(io.BytesIO(response.content))\n", + " # Display generated images\n", + " else:\n", + " image = images[i]._pil_image\n", + " ax.imshow(image)\n", + " ax.axis(\"off\")\n", + " plt.show()\n", + "\n", + "\n", + "def process_urls(url_string):\n", + " urls = [url.strip() for url in url_string.split(\"\\n\") if url.strip()]\n", + " return urls\n", + "\n", + "\n", + "def download_results_as_zip(results):\n", + " zip_filename = f\"virtual-try-on-results-{int(time.time())}.zip\"\n", + " with zipfile.ZipFile(zip_filename, \"w\", zipfile.ZIP_DEFLATED) as zip_file:\n", + " for i, result in enumerate(results):\n", + " image_data = result.image_bytes\n", + " zip_file.writestr(f\"result_{i + 1}.png\", image_data)\n", + " files.download(zip_filename)\n", + " print(f\"Download started for {zip_filename}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "RLLQstUOCFV1" + }, + "source": [ + "### Load the image model" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": { + "id": "GDp_NibQCbny" + }, + "outputs": [], + "source": [ + "virtual_try_on = \"virtual-try-on-001\"" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "0eFV5m5MC-XK" + }, + "source": [ + "## Batch processing with Virtual Try-On" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "OmhdutmeRkeG" + }, + "source": [ + "### Upload person and apparel images\n", + "\n", + "In this tutorial you can select your images from your local computer or public URLs. Choose only one option per batch processing job.\n", + "\n", + "Supported Clothing:\n", + " - **Tops:** shirts, hoodies, sweaters, tank tops, blouses\n", + " - **Bottoms:** pants, leggings, shorts, skirts\n", + " - **Footwear:** sneakers, boots, sandals, flats, heels, formal shoes" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "giwjbVX1r2ac" + }, + "source": [ + "#### **Option 1:** Use local images\n", + "\n", + "In this section, you'll upload images of people and clothing items to try them on from local files. Simply run the cell and select the images you'd like to use." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "uO9FXGEFTzIk" + }, + "outputs": [], + "source": [ + "print(\"Please upload one or more PERSON images:\")\n", + "person_files = files.upload()\n", + "person_images = [img_name for img_name, img_bytes in person_files.items()]\n", + "display_images(person_images, \"local\")\n", + "\n", + "print(\"\\nPlease upload one or more PRODUCT images:\")\n", + "product_files = files.upload()\n", + "product_images = [img_name for img_name, img_bytes in product_files.items()]\n", + "display_images(product_images, \"local\")\n", + "\n", + "local_images = True" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "NKHro9JSFEdP" + }, + "source": [ + "#### **Option 2:** Use public images\n", + "\n", + "If you uploaded images through Option 1, skip this section. Otherwise, paste each URL on a new line in the appropriate variable below. Do not use comma-separated values or local file paths." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "t75hoe-8ov7d" + }, + "outputs": [], + "source": [ + "# Paste person URLs here, one per line\n", + "person_urls = \"\"\"\n", + "https://storage.googleapis.com/cloud-samples-data/generative-ai/image/man-in-field.png\n", + "https://storage.googleapis.com/cloud-samples-data/generative-ai/image/woman.jpg\n", + "\"\"\"\n", + "\n", + "# Paste product URLs here, one per line\n", + "product_urls = \"\"\"\n", + "https://storage.googleapis.com/cloud-samples-data/generative-ai/image/sweater.jpg\n", + "https://storage.googleapis.com/cloud-samples-data/generative-ai/image/trousers.jpg\n", + "\"\"\"\n", + "\n", + "person_images = process_urls(person_urls)\n", + "product_images = process_urls(product_urls)\n", + "\n", + "display_images(person_images, \"url\")\n", + "display_images(product_images, \"url\")\n", + "\n", + "local_images = False" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "WJGO9d7KSBQ0" + }, + "source": [ + "### Generate and view results\n", + "\n", + "Running the cell below will generate an image for each combination of model and clothing item. The results will be displayed below." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "EYcWev3ASCjL" + }, + "outputs": [], + "source": [ + "generated_results = []\n", + "total_jobs = len(person_images) * len(product_images)\n", + "current_job = 0\n", + "\n", + "for person_img in person_images:\n", + " for product_img in product_images:\n", + " current_job += 1\n", + " print(f\"Running job {current_job}/{total_jobs}...\")\n", + " try:\n", + " if local_images:\n", + " person_image_obj = Image.from_file(location=person_img)\n", + " product_image_obj = Image.from_file(location=product_img)\n", + " else:\n", + " person_response = requests.get(person_img)\n", + " person_response.raise_for_status()\n", + " person_image_obj = Image(image_bytes=person_response.content)\n", + "\n", + " product_response = requests.get(product_img)\n", + " product_response.raise_for_status()\n", + " product_image_obj = Image(image_bytes=product_response.content)\n", + "\n", + " generated_image = client.models.recontext_image(\n", + " model=virtual_try_on,\n", + " source=RecontextImageSource(\n", + " person_image=person_image_obj,\n", + " product_images=[ProductImage(product_image=product_image_obj)],\n", + " ),\n", + " )\n", + " generated_results.append(generated_image.generated_images[0].image)\n", + " except Exception as e:\n", + " print(f\"Skipping job {current_job}/{total_jobs} due to an error: {e}\")\n", + "\n", + " time.sleep(2) # Add a delay to help with API quota limits\n", + "\n", + "# Display results\n", + "if generated_results:\n", + " display_images(generated_results, \"generated\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "GpZmv8zbSYyy" + }, + "source": [ + "### Download results locally in a ZIP file" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "FHHI5vsTShsd" + }, + "outputs": [], + "source": [ + "if generated_results:\n", + " download_results_as_zip(generated_results)" + ] + } + ], + "metadata": { + "colab": { + "name": "batch_virtual_try_on.ipynb", + "toc_visible": true + }, + "kernelspec": { + "display_name": "Python 3", + "name": "python3" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} diff --git a/workshops/README.md b/workshops/README.md new file mode 100644 index 0000000..59d5b65 --- /dev/null +++ b/workshops/README.md @@ -0,0 +1,12 @@ +# Workshops + +This directory contains materials and resources for various workshops related to Google Cloud generative AI. + +The subdirectories include hands-on labs, code examples, and presentations covering different topics and technologies. + +Explore the workshop materials to learn about: + +- AI Agents +- Question Answering Operations (QA-Ops) +- Retrieval Augmented Generation Operations (RAG-Ops) +- And more. diff --git a/workshops/ai-agents/1-prompt-essay.png b/workshops/ai-agents/1-prompt-essay.png new file mode 100644 index 0000000..15d3275 Binary files /dev/null and b/workshops/ai-agents/1-prompt-essay.png differ diff --git a/workshops/ai-agents/2-langchain-essay.png b/workshops/ai-agents/2-langchain-essay.png new file mode 100644 index 0000000..00e49b6 Binary files /dev/null and b/workshops/ai-agents/2-langchain-essay.png differ diff --git a/workshops/ai-agents/3-langgraph-essay.png b/workshops/ai-agents/3-langgraph-essay.png new file mode 100644 index 0000000..8e95e09 Binary files /dev/null and b/workshops/ai-agents/3-langgraph-essay.png differ diff --git a/workshops/ai-agents/ai_agents_for_engineers.ipynb b/workshops/ai-agents/ai_agents_for_engineers.ipynb new file mode 100644 index 0000000..e3e5f38 --- /dev/null +++ b/workshops/ai-agents/ai_agents_for_engineers.ipynb @@ -0,0 +1,1666 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "ur8xi4C7S06n" + }, + "outputs": [], + "source": [ + "# Copyright 2024 Google LLC\n", + "#\n", + "# Licensed under the Apache License, Version 2.0 (the \"License\");\n", + "# you may not use this file except in compliance with the License.\n", + "# You may obtain a copy of the License at\n", + "#\n", + "# https://www.apache.org/licenses/LICENSE-2.0\n", + "#\n", + "# Unless required by applicable law or agreed to in writing, software\n", + "# distributed under the License is distributed on an \"AS IS\" BASIS,\n", + "# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n", + "# See the License for the specific language governing permissions and\n", + "# limitations under the License." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "JAPoU8Sm5E6e" + }, + "source": [ + "# AI Agents for Engineers (Evolution of AI Agents)\n", + "\n", + "\n", + " \"Open\n", + "" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "84f0f73a0f76" + }, + "source": [ + "| | |\n", + "|-|-|\n", + "| Author(s) | [Kristopher Overholt](https://github.com/koverholt) [Holt Skinner](https://github.com/holtskinner)|" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "tvgnzT1CKxrO" + }, + "source": [ + "## Overview\n", + "\n", + "This notebook demonstrates 3 different approaches to generating essays using the [Gemini Developer API](https://ai.google.dev/gemini-api/docs) or [Gemini API in Vertex AI](https://cloud.google.com/vertex-ai/generative-ai/docs). Each method illustrates a distinct paradigm for running AI Agents in differing levels of complexity.\n", + "\n", + "1. Zero-Shot Approach with the Gemini API\n", + "2. Step-by-Step Approach With LangChain\n", + "3. Iterative, AI-Agent Approach with LangGraph" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "61RBz8LLbxCR" + }, + "source": [ + "## Get started" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "No17Cw5hgx12" + }, + "source": [ + "### Install Gemini SDK and other required packages\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "tFy3H3aPgx12" + }, + "outputs": [], + "source": [ + "%pip install --upgrade --quiet \\\n", + " google-genai \\\n", + " langgraph \\\n", + " langchain \\\n", + " langchain-google-genai \\\n", + " langchain-google-vertexai \\\n", + " langchain-community \\\n", + " tavily-python \\\n", + " pydantic" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "R5Xep4W9lq-Z" + }, + "source": [ + "### Restart runtime\n", + "\n", + "To use the newly installed packages in this Jupyter runtime, you must restart the runtime. You can do this by running the cell below, which restarts the current kernel.\n", + "\n", + "The restart might take a minute or longer. After it's restarted, continue to the next step." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "XRvKdaPDTznN" + }, + "outputs": [], + "source": [ + "import IPython\n", + "\n", + "app = IPython.Application.instance()\n", + "app.kernel.do_shutdown(True)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "SbmM4z7FOBpM" + }, + "source": [ + "
    \n", + "⚠ The kernel is going to restart. Wait until it's finished before continuing to the next step. ⚠\n", + "
    \n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "b1824bd79df4" + }, + "source": [ + "### Configure Tavily\n", + "\n", + "Get an API key for [Tavily](https://tavily.com/), a web search API for Generative AI models." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": { + "id": "5df2f86c691f" + }, + "outputs": [], + "source": [ + "import os\n", + "\n", + "os.environ[\"TAVILY_API_KEY\"] = \"YOUR_API_KEY_HERE\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "e00844b64cf7" + }, + "outputs": [], + "source": [ + "# If your API Keys are in Colab Secrets\n", + "import sys\n", + "\n", + "if \"google.colab\" in sys.modules:\n", + " from google.colab import userdata\n", + "\n", + " os.environ[\"TAVILY_API_KEY\"] = userdata.get(\"TAVILY_API_KEY\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "0781fd4c9001" + }, + "source": [ + "### Configure Gemini Developer API\n", + "\n", + "Get API keys from [Google AI Studio](https://ai.google.dev/gemini-api/docs/api-key) and [Tavily](https://tavily.com/)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "f2d2ecd0e96d" + }, + "outputs": [], + "source": [ + "import os\n", + "\n", + "os.environ[\"GOOGLE_API_KEY\"] = \"YOUR_API_KEY_HERE\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "nvuDx1V6buOQ" + }, + "outputs": [], + "source": [ + "# If your API Keys are in Colab Secrets\n", + "import sys\n", + "\n", + "if \"google.colab\" in sys.modules:\n", + " from google.colab import userdata\n", + "\n", + " os.environ[\"GOOGLE_API_KEY\"] = userdata.get(\"GOOGLE_API_KEY\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "5ffb0f859dde" + }, + "source": [ + "### Configure Vertex AI\n", + "\n", + "**Use a Google Cloud Project:** This requires enabling the Vertex AI API in your Google Cloud project.\n", + "\n", + "[Enable the Vertex AI API](https://console.cloud.google.com/flows/enableapi?apiid=aiplatform.googleapis.com)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "885a3c84ddac" + }, + "outputs": [], + "source": [ + "import sys\n", + "\n", + "if \"google.colab\" in sys.modules:\n", + " from google.colab import auth\n", + "\n", + " auth.authenticate_user()" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": { + "id": "c80118166880" + }, + "outputs": [], + "source": [ + "# fmt: off\n", + "PROJECT_ID = \"[your-project-id]\" # @param {type: \"string\", placeholder: \"[your-project-id]\", isTemplate: true}\n", + "# fmt: on\n", + "if not PROJECT_ID or PROJECT_ID == \"[your-project-id]\":\n", + " PROJECT_ID = str(os.environ.get(\"GOOGLE_CLOUD_PROJECT\"))\n", + "\n", + "LOCATION = os.environ.get(\"GOOGLE_CLOUD_REGION\", \"us-central1\")\n", + "\n", + "os.environ[\"GOOGLE_GENAI_USE_ENTERPRISE\"] = \"true\"" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "EdvJRUWRNGHE" + }, + "source": [ + "## Generating Essays Using a Zero-Shot Approach with the Gemini API\n", + "\n", + "With just a single call to the `generate_content` method, users can create detailed, structured essays on any topic by leveraging state-of-the-art language models such as Gemini 3 or Gemini 3.\n", + "\n", + "" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "5303c05f7aa6" + }, + "source": [ + "### Import libraries" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": { + "id": "6fc324893334" + }, + "outputs": [], + "source": [ + "from IPython.display import Markdown, display" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "4631829d00c8" + }, + "source": [ + "### Create Client" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": { + "id": "898c62c59d40" + }, + "outputs": [], + "source": [ + "from google import genai\n", + "\n", + "client = genai.Client()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "4b87f1f593d5" + }, + "source": [ + "Verify which API you are using." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "84c16a5f3f33" + }, + "outputs": [], + "source": [ + "if not client.vertexai:\n", + " print(\"Using Gemini Developer API.\")\n", + "elif client._api_client.project:\n", + " print(\n", + " f\"Using Vertex AI with project: {client._api_client.project} in location: {client._api_client.location}\"\n", + " )\n", + "elif client._api_client.api_key:\n", + " print(\n", + " f\"Using Vertex AI in express mode with API key: {client._api_client.api_key[:5]}...{client._api_client.api_key[-5:]}\"\n", + " )" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "e43229f3ad4f" + }, + "source": [ + "### Load model" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": { + "id": "cf93d5f0ce00" + }, + "outputs": [], + "source": [ + "MODEL_ID = \"gemini-3.5-flash\"" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "bbcd4f57190f" + }, + "source": [ + "### Make an API call to generate the essay" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": { + "id": "3734f520c1b3" + }, + "outputs": [ + { + "data": { + "text/markdown": [ + "Heat transfer is a critical consideration in the design and operation of modern data centers, as the concentration of high-powered computing equipment generates tremendous amounts of heat. Effective heat management is essential for maintaining optimal performance, preventing equipment failure, and ensuring energy efficiency. Various heat transfer mechanisms, including conduction, convection, and radiation, are leveraged in innovative cooling solutions. For instance, cold plates directly attached to CPUs and GPUs conduct heat away, while air and liquid cooling systems rely on convection to dissipate heat from the components to the surrounding environment. The use of phase-change materials and heat pipes further enhances heat transfer efficiency, enabling higher densities and improved reliability in data center operations.\n", + "\n", + "Modern data centers employ a range of sophisticated cooling technologies to manage the generated heat effectively. Air cooling, once the dominant method, is now often augmented with liquid cooling solutions, particularly for high-density deployments. Direct-to-chip liquid cooling offers superior thermal performance compared to air cooling, allowing for higher clock speeds and reduced fan noise. Rear door heat exchangers and immersion cooling systems further enhance heat dissipation by capturing heat directly from the exhaust air or submerging the equipment in a dielectric fluid. Computational fluid dynamics (CFD) simulations are used to model airflow and temperature distribution within the data center, enabling optimized placement of cooling units and servers for efficient heat removal.\n", + "\n", + "The implementation of advanced heat transfer technologies in data centers not only improves equipment reliability and performance but also contributes to significant energy savings and reduced environmental impact. By optimizing cooling systems, data centers can minimize their power consumption, lowering operating costs and decreasing their carbon footprint. Furthermore, waste heat recovery systems can capture and repurpose the excess heat generated by the servers, utilizing it for building heating or other applications, further improving energy efficiency and sustainability. As data centers continue to evolve and house increasingly powerful computing hardware, the effective management of heat transfer will remain a crucial challenge and a key enabler of their continued growth and success.\n" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "prompt = \"Write a 3-paragraph essay about the application of heat transfer in modern data centers\"\n", + "\n", + "response = client.models.generate_content(model=MODEL_ID, contents=prompt)\n", + "display(Markdown(response.text))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "520e23ea4332" + }, + "source": [ + "---\n", + "\n", + "However, what if we ask the model to write an essay about an event that happened more recently and the LLM doesn't inherently know about that event?" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": { + "id": "dcb775b83997" + }, + "outputs": [ + { + "data": { + "text/markdown": [ + "While specific details regarding the impact of hypothetical 2024 hurricanes Helene and Milton are impossible to predict without them actually occurring, we can discuss the *potential* ramifications based on hurricane characteristics and historical precedent. A major impact would undoubtedly be widespread damage to infrastructure, particularly in coastal communities. Strong winds could topple power lines, leaving residents without electricity for extended periods, while torrential rainfall could lead to catastrophic flooding, damaging homes, businesses, and critical transportation networks. Depending on the intensity and track, these storms could trigger evacuations, straining local resources and potentially causing displacement of entire populations. Moreover, the economic repercussions would be significant, affecting tourism, agriculture, and the fishing industry.\n", + "\n", + "Beyond the immediate physical damage, both hurricanes could have devastating effects on human lives and well-being. Injuries and fatalities are always a concern, particularly amongst vulnerable populations with limited access to resources or residing in flood-prone areas. The stress and trauma associated with experiencing a natural disaster can lead to long-term mental health issues, requiring extensive support services. Disruption to healthcare facilities and emergency services could further exacerbate the crisis, hindering rescue and recovery efforts. In the aftermath, access to clean water, food, and shelter would become paramount concerns, demanding a coordinated response from governmental and non-governmental organizations.\n", + "\n", + "Looking at the broader picture, Hurricanes Helene and Milton, even if categorized as less intense storms, could significantly impact the environment. Coastal erosion and beach degradation would be likely, altering shorelines and potentially endangering fragile ecosystems. Runoff from floodwaters could contaminate water supplies and spread pollutants, impacting both human and marine life. The destruction of habitats could lead to displacement of wildlife and disruption of ecological balance. Ultimately, the scale of the impact would depend on a confluence of factors, including the storms' intensity, track, and the preparedness levels of affected communities, highlighting the crucial need for robust disaster preparedness and mitigation strategies.\n" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "prompt = \"Write a 3-paragraph essay about the impacts of Hurricane Helene and Hurricane Milton in 2024.\"\n", + "response = client.models.generate_content(model=MODEL_ID, contents=prompt)\n", + "display(Markdown(response.text))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "764ce71aecd5" + }, + "source": [ + "In this case, the model had no information about these recent events and was unable to write an effective essay." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "16aafc60d80b" + }, + "source": [ + "## Generating Essays Using a Step-by-Step Approach With LangChain\n", + "\n", + "This step demonstrates how to build an essay-writing pipeline using [LangChain](https://www.langchain.com/), the [Gemini API in Google AI Studio](https://ai.google.dev/gemini-api/docs), and [Tavily](https://tavily.com/) for search.\n", + "\n", + "By combining these tools, we create a seamless workflow that plans an essay outline, performs web searches for relevant information, and generates a complete essay draft based on the collected data.\n", + "\n", + "This solution showcases the power of chaining LLM models and external tools to tackle complex tasks with minimal human intervention, providing a robust approach to automated content generation.\n", + "\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "85666976a359" + }, + "source": [ + "### Import libraries" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": { + "id": "29d6e42d27ae" + }, + "outputs": [], + "source": [ + "from IPython.display import Markdown, display\n", + "from langchain import LLMChain\n", + "from langchain.prompts import ChatPromptTemplate\n", + "from langchain_community.tools import TavilySearchResults\n", + "from langchain_core.output_parsers import StrOutputParser\n", + "from langchain_google_genai import ChatGoogleGenerativeAI\n", + "from langchain_google_vertexai import ChatVertexAI" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "43392590b1d8" + }, + "source": [ + "### Initialize Gemini model & search tool" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": { + "id": "6f8b0c205551" + }, + "outputs": [], + "source": [ + "if client.vertexai:\n", + " model = ChatVertexAI(project=PROJECT_ID, model=MODEL_ID, temperature=0)\n", + "else:\n", + " model = ChatGoogleGenerativeAI(model=MODEL_ID, temperature=0)\n", + "tavily_tool = TavilySearchResults(max_results=5)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "7ee8707e1867" + }, + "source": [ + "### Define prompt templates and Runnables" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": { + "id": "a09a6a6d1f36" + }, + "outputs": [], + "source": [ + "# Planning: Create an outline for the essay\n", + "outline_template = ChatPromptTemplate.from_template(\n", + " \"Create a detailed outline for an essay on {topic}\"\n", + ")\n", + "\n", + "\n", + "# Research: Web search\n", + "def research_fn(topic):\n", + " response = tavily_tool.invoke({\"query\": topic})\n", + " return \"\\n\".join([f\"- {result['content']}\" for result in response])\n", + "\n", + "\n", + "# Writing: Write the essay based on outline and research\n", + "writing_template = ChatPromptTemplate.from_template(\n", + " \"Based on the following outline and research, write a 3-paragraph essay on '{topic}':\\n\\nOutline:\\n{outline}\\n\\nResearch:\\n{research}\\n\\nEssay:\"\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "9a18006523f7" + }, + "source": [ + "### Define the Runnable Chain using [LangChain Expression Language (LCEL)](https://python.langchain.com/docs/how_to/#langchain-expression-language-lcel)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "cf48601613fd" + }, + "outputs": [], + "source": [ + "# Define individual chains\n", + "outline_chain = LLMChain(llm=model, prompt=outline_template)\n", + "writing_chain = LLMChain(llm=model, prompt=writing_template)\n", + "\n", + "# Use the pipe operator to combine chains\n", + "chain = (\n", + " outline_chain\n", + " | (\n", + " lambda result: {\n", + " \"topic\": result[\"topic\"],\n", + " \"outline\": result[\"text\"],\n", + " \"research\": research_fn(result[\"topic\"]),\n", + " }\n", + " )\n", + " | writing_chain\n", + " | (lambda result: result[\"text\"]) # Extract the essay text from the final result\n", + " | StrOutputParser()\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "839fc48dd408" + }, + "source": [ + "### Generate the essay" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "metadata": { + "id": "a76f80ceec98" + }, + "outputs": [ + { + "data": { + "text/markdown": [ + "As climate change continues to fuel warmer ocean temperatures, the threat of increasingly powerful and frequent hurricanes looms large. Understanding the potential impacts of these storms is crucial for effective preparation and mitigation efforts. In late September and early October of 2024, Florida was struck by two such storms: Hurricane Helene, a Category 3 storm, and Hurricane Milton, a Category 1 storm. While both hurricanes brought destruction, their differing intensities and paths resulted in distinct impacts, ranging from widespread wastewater contamination to significant infrastructural damage and economic disruption.\n", + "\n", + "Hurricane Helene, the stronger of the two, caused significant environmental damage in Florida. St. Petersburg's infrastructure was overwhelmed, leading to the discharge of approximately 1.5 million gallons of untreated wastewater into the surrounding areas. The Glendale Wastewater Reclamation Facility in Lakeland, operating since 1926, experienced two major spills totaling five million gallons. This contamination posed a serious threat to public health and the delicate ecosystems of the Tampa Bay area. The coastal region experienced increased rainfall, which climate models indicated was increased in both likelihood and intensity.\n", + "\n", + "In contrast, Hurricane Milton, while weaker, inflicted substantial economic and infrastructural damage across Florida. Making landfall near Siesta Key as a Category 3 storm on October 9, 2024, Milton disrupted key industries and essential services. The fishing industry in Apalachicola Bay suffered greatly, with damage to boats, docks, and processing facilities leading to significant income loss for local fishermen. Debris removal and cleanup efforts were extensive, and the combined impact of Helene and Milton contributed to the ten costliest climate disasters of 2024, which caused more than $229 billion in damages globally. World Vision, through its 20 church partnerships, served over 39,600 people in Florida, Georgia, and the Carolinas in response to the hurricanes.\n", + "\n", + "While both hypothetical hurricanes brought destruction, Helene's impact was primarily focused on environmental contamination, while Milton caused significant economic disruption and infrastructure damage. As the threat of increasingly intense hurricanes continues to grow, proactive measures to enhance preparedness, strengthen infrastructure, and foster community resilience are essential to minimize the devastating impacts of these powerful storms. The events of late September and early October 2024 serve as a stark reminder of the urgent need for continued research, investment in resilient infrastructure, and community-based preparedness initiatives to mitigate the impacts of future hurricanes.\n" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "essay = chain.invoke({\"topic\": prompt})\n", + "display(Markdown(essay))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "294d3b7c43b2" + }, + "source": [ + "## Generating Essays Using an Iterative, AI-Agent Approach with LangGraph\n", + "\n", + "This section demonstrates how to build a [LangGraph](https://langchain-ai.github.io/langgraph/)-powered AI agent to generate, revise, and critique essays using large language models such as Google's [Gemini API in Google AI Studio](https://ai.google.dev/gemini-api/docs) or the [Gemini API in Vertex AI](https://cloud.google.com/vertex-ai/generative-ai/docs/multimodal/overview). The LangGraph code was adapted from the awesome DeepLearning.AI course on [AI Agents in LangGraph](https://www.deeplearning.ai/short-courses/ai-agents-in-langgraph/).\n", + "\n", + "By defining a structured state flow with nodes such as \"Planner,\" \"Research Plan,\" \"Generate,\" \"Reflect,\" and \"Research Critique,\" the system iteratively creates an essay on a given topic, incorporates feedback, and provides research-backed insights.\n", + "\n", + "\n", + "\n", + "The workflow enables automated essay generation with revision controls, making it ideal for structured writing tasks or educational use cases. Additionally, the notebook uses external search tools to gather and integrate real-time information into the essay content." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "f8e41763086f" + }, + "source": [ + "### Import libraries" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "52fbe2cb7be7" + }, + "outputs": [], + "source": [ + "from typing import TypedDict\n", + "\n", + "# Common libraries\n", + "from IPython.display import Image, Markdown, display\n", + "\n", + "# LangChain and LangGraph components\n", + "from langchain_core.messages import HumanMessage, SystemMessage\n", + "\n", + "# LangChain integrations for Gemini API in Google AI Studio and Vertex AI\n", + "from langchain_google_genai import ChatGoogleGenerativeAI\n", + "from langchain_google_vertexai import ChatVertexAI\n", + "from langgraph.checkpoint.memory import MemorySaver\n", + "from langgraph.graph import END, StateGraph\n", + "\n", + "# Typing utilities for data validation and schema definitions\n", + "from pydantic.v1 import BaseModel\n", + "\n", + "# Tavily client for performing web searches\n", + "from tavily import TavilyClient" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "fc6ae1fac44f" + }, + "source": [ + "### Initialize agent memory, agent state, and schema for search queries" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "metadata": { + "id": "6b92f7bab46d" + }, + "outputs": [], + "source": [ + "# Initialize agent memory\n", + "memory = MemorySaver()\n", + "\n", + "\n", + "# Define the agent's state\n", + "class AgentState(TypedDict):\n", + " task: str\n", + " plan: str\n", + " draft: str\n", + " critique: str\n", + " content: list[str]\n", + " revision_number: int\n", + " max_revisions: int\n", + "\n", + "\n", + "# Define a schema for search queries\n", + "class Queries(BaseModel):\n", + " \"\"\"Variants of query to search for.\"\"\"\n", + "\n", + " queries: list[str]" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "d9660e58afab" + }, + "source": [ + "### Initialize Gemini model and search tool\n", + "\n", + "Remember to set the environment variables `GOOGLE_API_KEY` and `TAVILY_API_KEY`. And configure credentials for Vertex AI if you switch to it." + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "metadata": { + "id": "ec96b00bb67f" + }, + "outputs": [], + "source": [ + "if client.vertexai:\n", + " model = ChatVertexAI(project=PROJECT_ID, model=MODEL_ID, temperature=0)\n", + "else:\n", + " model = ChatGoogleGenerativeAI(model=MODEL_ID, temperature=0)\n", + "\n", + "# Initialize Tavily client for performing web searches\n", + "tavily = TavilyClient()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "d94dc64d3846" + }, + "source": [ + "### Define prompt templates for each stage" + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "metadata": { + "id": "2cc6f9b05d29" + }, + "outputs": [], + "source": [ + "PLAN_PROMPT = \"\"\"You are an expert writer tasked with writing a high level outline of an essay.\n", + "Write such an outline for the user provided topic. Give an outline of the essay along with any\n", + "relevant notes or instructions for the sections.\"\"\"\n", + "\n", + "WRITER_PROMPT = \"\"\"You are an essay assistant tasked with writing excellent 3-paragraph essays.\n", + "Generate the best essay possible for the user's request and the initial outline.\n", + "If the user provides critique, respond with a revised version of your previous attempts.\n", + "Use Markdown formatting to specify a title and section headers for each paragraph.\n", + "Utilize all of the information below as needed:\n", + "---\n", + "{content}\"\"\"\n", + "\n", + "REFLECTION_PROMPT = \"\"\"You are a teacher grading an essay submission.\n", + "Generate critique and recommendations for the user's submission.\n", + "Provide detailed recommendations, including requests for length, depth, style, etc.\"\"\"\n", + "\n", + "RESEARCH_PLAN_PROMPT = \"\"\"You are a researcher charged with providing information that can\n", + "be used when writing the following essay. Generate a list of search queries that will gather\n", + "any relevant information. Only generate 3 queries max.\"\"\"\n", + "\n", + "RESEARCH_CRITIQUE_PROMPT = \"\"\"You are a researcher charged with providing information that can\n", + "be used when making any requested revisions (as outlined below).\n", + "Generate a list of search queries that will gather any relevant information.\n", + "Only generate 3 queries max.\"\"\"" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "c4f51c668222" + }, + "source": [ + "### Define node functions for each stage" + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "metadata": { + "id": "75c8d7021369" + }, + "outputs": [], + "source": [ + "# Generate an outline for the essay\n", + "\n", + "\n", + "def plan_node(state: AgentState):\n", + " messages = [SystemMessage(content=PLAN_PROMPT), HumanMessage(content=state[\"task\"])]\n", + " response = model.invoke(messages)\n", + " return {\"plan\": response.content}\n", + "\n", + "\n", + "# Conducts research based on the generated plan and web search results\n", + "def research_plan_node(state: AgentState):\n", + " queries = model.with_structured_output(Queries).invoke(\n", + " [\n", + " SystemMessage(content=RESEARCH_PLAN_PROMPT),\n", + " HumanMessage(content=state[\"task\"]),\n", + " ]\n", + " )\n", + " content = state[\"content\"] or []\n", + " for q in queries.queries:\n", + " response = tavily.search(query=q, max_results=2)\n", + " for r in response[\"results\"]:\n", + " content.append(r[\"content\"])\n", + " return {\"content\": content}\n", + "\n", + "\n", + "# Generates a draft based on the content and plan\n", + "def generation_node(state: AgentState):\n", + " content = \"\\n\\n\".join(state[\"content\"] or [])\n", + " user_message = HumanMessage(\n", + " content=f\"{state['task']}\\n\\nHere is my plan:\\n\\n{state['plan']}\"\n", + " )\n", + " messages = [\n", + " SystemMessage(content=WRITER_PROMPT.format(content=content)),\n", + " user_message,\n", + " ]\n", + " response = model.invoke(messages)\n", + " return {\n", + " \"draft\": response.content,\n", + " \"revision_number\": state.get(\"revision_number\", 1) + 1,\n", + " }\n", + "\n", + "\n", + "# Provides feedback or critique on the draft\n", + "def reflection_node(state: AgentState):\n", + " messages = [\n", + " SystemMessage(content=REFLECTION_PROMPT),\n", + " HumanMessage(content=state[\"draft\"]),\n", + " ]\n", + " response = model.invoke(messages)\n", + " return {\"critique\": response.content}\n", + "\n", + "\n", + "# Conducts research based on the critique\n", + "def research_critique_node(state: AgentState):\n", + " queries = model.with_structured_output(Queries).invoke(\n", + " [\n", + " SystemMessage(content=RESEARCH_CRITIQUE_PROMPT),\n", + " HumanMessage(content=state[\"critique\"]),\n", + " ]\n", + " )\n", + " content = state[\"content\"] or []\n", + " for q in queries.queries:\n", + " response = tavily.search(query=q, max_results=2)\n", + " for r in response[\"results\"]:\n", + " content.append(r[\"content\"])\n", + " return {\"content\": content}\n", + "\n", + "\n", + "# Determines whether the critique and research cycle should\n", + "# continue based on the number of revisions\n", + "def should_continue(state):\n", + " if state[\"revision_number\"] > state[\"max_revisions\"]:\n", + " return END\n", + " return \"reflect\"" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "48569416595a" + }, + "source": [ + "### Define and compile the graph" + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "metadata": { + "id": "86567ad87aa2" + }, + "outputs": [], + "source": [ + "# Initialize the state graph\n", + "builder = StateGraph(AgentState)\n", + "\n", + "# Add nodes for each step in the workflow\n", + "builder.add_node(\"planner\", plan_node)\n", + "builder.add_node(\"generate\", generation_node)\n", + "builder.add_node(\"reflect\", reflection_node)\n", + "builder.add_node(\"research_plan\", research_plan_node)\n", + "builder.add_node(\"research_critique\", research_critique_node)\n", + "\n", + "# Set the entry point of the workflow\n", + "builder.set_entry_point(\"planner\")\n", + "\n", + "# Add conditional edges for task continuation or end\n", + "builder.add_conditional_edges(\n", + " \"generate\", should_continue, {END: END, \"reflect\": \"reflect\"}\n", + ")\n", + "\n", + "# Define task sequence edges\n", + "builder.add_edge(\"planner\", \"research_plan\")\n", + "builder.add_edge(\"research_plan\", \"generate\")\n", + "\n", + "builder.add_edge(\"reflect\", \"research_critique\")\n", + "builder.add_edge(\"research_critique\", \"generate\")\n", + "\n", + "# Compile the graph with memory state management\n", + "graph = builder.compile(checkpointer=memory)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "44d87b0a2052" + }, + "source": [ + "### Show the compiled graph" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "9c3170874384" + }, + "outputs": [], + "source": [ + "Image(graph.get_graph().draw_mermaid_png())" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "c95bebc7e74c" + }, + "source": [ + "### Run the agent - write on!" + ] + }, + { + "cell_type": "code", + "execution_count": 30, + "metadata": { + "id": "2a849843b454" + }, + "outputs": [ + { + "data": { + "text/markdown": [ + "# planner" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "Okay, here's a high-level outline for an essay exploring the impacts of Hurricanes Helene and Milton in 2024. This outline focuses on providing a structure for a comprehensive analysis, considering various sectors and potential long-term consequences.\n", + "\n", + "**Essay Title (Example):** Twin Storms, Lasting Scars: A Comparative Analysis of the Impacts of Hurricanes Helene and Milton (2024)\n", + "\n", + "**I. Introduction**\n", + "\n", + "* **Hook:** Start with a compelling statistic or anecdote about the 2024 hurricane season or the general increase in extreme weather events.\n", + "* **Background:** Briefly introduce Hurricane Helene and Hurricane Milton, including their formation, paths, and peak intensities. Mention the geographical areas most affected by each storm.\n", + "* **Thesis Statement:** Clearly state the essay's main argument. For example: \"While both Hurricanes Helene and Milton caused significant damage in 2024, their impacts differed substantially due to variations in intensity, geographical location, and pre-existing vulnerabilities, resulting in distinct challenges for affected communities in terms of infrastructure, economy, and public health.\"\n", + "* **Roadmap:** Briefly outline the key areas the essay will cover (e.g., infrastructure damage, economic consequences, environmental effects, and social impacts).\n", + "\n", + "**II. Hurricane Helene: Impacts and Analysis**\n", + "\n", + "* **A. Geographical Context and Vulnerabilities:**\n", + " * Describe the specific regions affected by Hurricane Helene.\n", + " * Analyze the pre-existing vulnerabilities of these regions (e.g., coastal erosion, poverty, inadequate infrastructure, reliance on specific industries).\n", + " * Discuss any evacuation efforts and their effectiveness.\n", + "* **B. Infrastructure Damage:**\n", + " * Detail the damage to critical infrastructure: power grids, transportation networks (roads, bridges, ports), communication systems, water and sanitation facilities.\n", + " * Provide specific examples and, if available, estimated costs of repair.\n", + " * Discuss the immediate and long-term consequences of infrastructure failure (e.g., power outages, disruptions to supply chains).\n", + "* **C. Economic Consequences:**\n", + " * Analyze the impact on key industries in the affected regions (e.g., tourism, agriculture, fishing).\n", + " * Discuss job losses, business closures, and disruptions to trade.\n", + " * Examine the role of insurance and disaster relief in mitigating economic losses.\n", + "* **D. Environmental Effects:**\n", + " * Assess the environmental damage caused by Helene: coastal erosion, flooding, saltwater intrusion, damage to ecosystems (e.g., wetlands, coral reefs).\n", + " * Discuss the potential long-term ecological consequences.\n", + "* **E. Social Impacts:**\n", + " * Examine the impact on public health: injuries, disease outbreaks, mental health issues.\n", + " * Discuss displacement, homelessness, and food insecurity.\n", + " * Analyze the role of community organizations and government agencies in providing aid and support.\n", + "\n", + "**III. Hurricane Milton: Impacts and Analysis**\n", + "\n", + "* **A. Geographical Context and Vulnerabilities:**\n", + " * Describe the specific regions affected by Hurricane Milton.\n", + " * Analyze the pre-existing vulnerabilities of these regions (e.g., different demographics, building codes, economic activities compared to Helene's affected areas).\n", + " * Discuss any evacuation efforts and their effectiveness.\n", + "* **B. Infrastructure Damage:**\n", + " * Detail the damage to critical infrastructure, noting any differences from the types of damage caused by Helene.\n", + " * Provide specific examples and estimated costs of repair.\n", + " * Discuss the immediate and long-term consequences of infrastructure failure.\n", + "* **C. Economic Consequences:**\n", + " * Analyze the impact on key industries in the affected regions, noting any differences from Helene's affected areas.\n", + " * Discuss job losses, business closures, and disruptions to trade.\n", + " * Examine the role of insurance and disaster relief in mitigating economic losses.\n", + "* **D. Environmental Effects:**\n", + " * Assess the environmental damage caused by Milton, noting any differences from Helene.\n", + " * Discuss the potential long-term ecological consequences.\n", + "* **E. Social Impacts:**\n", + " * Examine the impact on public health, noting any differences from Helene's affected areas.\n", + " * Discuss displacement, homelessness, and food insecurity.\n", + " * Analyze the role of community organizations and government agencies in providing aid and support.\n", + "\n", + "**IV. Comparative Analysis: Helene vs. Milton**\n", + "\n", + "* **A. Key Differences in Impacts:**\n", + " * Directly compare and contrast the impacts of the two hurricanes across the categories discussed above (infrastructure, economy, environment, social).\n", + " * Highlight the factors that contributed to these differences (e.g., storm intensity, geographical characteristics, preparedness levels).\n", + "* **B. Lessons Learned:**\n", + " * Identify key lessons learned from the responses to both hurricanes.\n", + " * Discuss the effectiveness of different mitigation and adaptation strategies.\n", + " * Consider the implications for future hurricane preparedness and disaster management.\n", + "\n", + "**V. Conclusion**\n", + "\n", + "* **Restate Thesis:** Summarize the main argument of the essay, emphasizing the distinct impacts of the two hurricanes.\n", + "* **Broader Implications:** Discuss the broader implications of these events in the context of climate change and increasing hurricane intensity.\n", + "* **Call to Action (Optional):** Suggest potential actions that can be taken to improve hurricane preparedness, resilience, and disaster response in the future.\n", + "* **Concluding Thought:** End with a powerful statement about the challenges posed by hurricanes and the importance of proactive measures to protect vulnerable communities.\n", + "\n", + "**Important Notes:**\n", + "\n", + "* **Sources:** Use credible sources, including government reports (e.g., NOAA, FEMA), academic studies, news articles from reputable sources, and reports from NGOs involved in disaster relief.\n", + "* **Data:** Include specific data and statistics to support your claims (e.g., rainfall amounts, wind speeds, damage estimates, number of people displaced).\n", + "* **Visuals:** Consider including maps, charts, and photographs to enhance the essay.\n", + "* **Objectivity:** Present a balanced and objective analysis, avoiding sensationalism or biased reporting.\n", + "* **Future Projections:** While focusing on the 2024 events, briefly touch upon how climate change might influence future hurricane seasons and the need for long-term adaptation strategies.\n", + "* **Specificity:** The more specific you can be about the locations affected and the types of damage, the stronger your essay will be.\n", + "\n", + "This outline provides a solid framework. Remember to adapt it based on the specific information available about Hurricanes Helene and Milton in 2024. Good luck!\n" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "---\n", + "\n" + ] + }, + { + "data": { + "text/markdown": [ + "# research_plan" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "North Carolina was severely impacted by Hurricane Helene during late September 2024, primarily in its western Appalachian region, causing at least 105 reported deaths and significant destruction of infrastructure and residential areas across several settlements.[1] After making landfall in the Big Bend region of Florida on September 27, the hurricane began to traverse over land across Georgia as a Category 2 hurricane and into the Appalachian mountain range as a tropical storm, depositing record-breaking amounts of rainfall across several settlements in western North Carolina, such as Asheville, Swannanoa, Spruce Pine, Chimney Rock, Montreat, Lake Lure, and several others. Of the 241 people reported victims of Hurricane Helene in the United States, 119 people were in North Carolina, surpassing the state record of 80 deaths recorded in a 1916 flood.[8][35] Of these, 72 residents of Buncombe County were among the deceased." + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "Ask the Chatbot Games & Quizzes History & Society Science & Tech Biographies Animals & Nature Geography & Travel Arts & Culture ProCon Money Videos Hurricane Helene, large, catastrophic tropical cyclone notable for its rapid intensification that ultimately produced a wide swath of damage and loss of life that extended from northwestern Florida, where the storm made landfall on September 26, 2024, to Tennessee, Georgia, and North Carolina. Although Helene’s winds had diminished to tropical-storm strength, the storm delivered a broad and deadly swath of heavy rain across the southeastern U.S. In parts of Florida, Georgia, North and South Carolina, Tennessee, and Virginia, record rainfall caused rivers to overflow and produced flash flooding, road washouts, and even landslides in some areas." + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "An EF-0 tornado impacted Martin and St. Lucie counties on October 9, 2024, as Hurricane Milton's rainbands swept across the Treasure Coast. An EF-1 tornado impacted St. Lucie County on October 9, 2024, as Hurricane Milton's outer rainbands moved across the Treasure Coast. In Fort Pierce, the tornado caused significant structural damage to a metal canopy at the St. Lucie County Sheriff's Office, with peak winds estimated at 90 mph before weakening and lifting near the I-95 and Okeechobee interchange. An EF-1 tornado touched down near the Belcher Canal in Fort Pierce, causing vegetative damage and partial roof loss near St. Lucie Blvd and N 25th St. The tornado continued northwest across Treasure Coast International Airport, where several small planes were tossed and recorded gusts of 60 mph." + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "A tropical storm watch was issued for Extreme Northwest Bahamian Islands on October 8, 2024, upgraded six hours later to a tropical storm warning.[102][103][104] Grand Bahama activated their emergency operations center.[105] The Royal Bahamas Defence Force went on standby with supplies prepared for Hurricane Milton.[106] In-person schools on Grand Bahama, Bimini, Abaco, and Grand Cay were closed.[107] The Anglican Central Education Authority closed two campuses.[108] Bahamian students in Florida were ordered to evacuate; Bahamasair made two flights on October 7 and 8 to Orlando.[109] The Grand Bahama International Airport had closed.[110] Bahamas Power and Light offices in northern islands closed October 9.[111] Banks on Grand Bahama and Abaco closed October 10.[112] A large surge in sales of hurricane-related supplies occurred.[105] ^ Hurricane Milton Spawned Record Number Of Florida Tornadoes, The Weather Channel, October 23, 2024" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "Search for News Email This Subscribe to Newsletter Email to a friend Facebook Tweet LinkedIn Print Article The 2024 hurricane season presented challenges for insurers thanks largely to hurricanes Beryl, Helene, and Milton—the three powerful storms caused more than $138 billion in damage as well as $52 million in insured losses. Hurricane Milton insured losses totaled $25 billion with a total loss of $38 billion. The company analyzed 2 million properties in the area of where Hurricane Helene hit the southeastern area, finding: 131,004 properties were impacted, 43,352 properties suffered severe damage and 3,580 properties were destroyed. Get the latest insurance news Email This Subscribe to Newsletter Email to a friend Facebook Tweet LinkedIn Print Article Email * More News More News Features Claims News Email Newsletters" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "Helene, Milton losses expected to surpass \"truly historic\" $50 billion each - CBS News Monstrous hurricanes Helene and Milton caused so much complex havoc that damages are still being added up, but government and private experts say they will likely join the infamous ranks of Katrina, Sandy and Harvey as super costly $50-billion-plus killers. But even adjusted for inflation, damages from intense storms are skyrocketing because people are building in harm's way, rebuilding costs are rising faster than inflation and human-caused climate change are making storms stronger and wetter, experts in different fields said. Of the top 10 costliest hurricanes as compiled by insurance giant Swiss Re - not including Helene or Milton yet - insured damage is about 44% of total costs." + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "---\n", + "\n" + ] + }, + { + "data": { + "text/markdown": [ + "# generate" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "Okay, I will write a 3-paragraph essay based on the information provided.\n", + "\n", + "**Title: The Destructive Force of Hurricanes Helene and Milton in 2024**\n", + "\n", + "**Paragraph 1: Introduction to the 2024 Hurricane Season**\n", + "\n", + "The 2024 hurricane season proved to be particularly devastating, marked by the significant impacts of Hurricanes Helene and Milton. These storms left a trail of destruction across multiple states, causing billions of dollars in damage and resulting in tragic loss of life. Hurricane Helene, notable for its rapid intensification, made landfall in Florida before traversing through Georgia and into the Appalachian region, unleashing record-breaking rainfall and widespread flooding. Meanwhile, Hurricane Milton spawned a record number of tornadoes in Florida, exacerbating the damage caused by high winds and heavy rain. The combined impact of these storms presented unprecedented challenges for insurers and communities alike, underscoring the increasing vulnerability of coastal and inland regions to extreme weather events.\n", + "\n", + "**Paragraph 2: Contrasting Impacts of Helene and Milton**\n", + "\n", + "While both hurricanes inflicted substantial damage, their impacts varied significantly due to differences in their paths and intensities. Hurricane Helene's slow movement and torrential rainfall led to catastrophic flooding in the Appalachian region, particularly in western North Carolina, where it caused a state-record number of fatalities. The storm's impact extended beyond immediate property damage, disrupting critical infrastructure such as power grids and transportation networks, hindering rescue and recovery efforts. In contrast, Hurricane Milton's primary impact was felt in Florida, where it spawned numerous tornadoes that caused structural damage to buildings and infrastructure. The storm's effects were further compounded by insured losses totaling $25 billion, contributing to a total loss of $38 billion.\n", + "\n", + "**Paragraph 3: Long-Term Consequences and Lessons Learned**\n", + "\n", + "The aftermath of Hurricanes Helene and Milton has prompted a comprehensive reassessment of hurricane preparedness and disaster management strategies. The extensive damage caused by these storms has highlighted the need for improved infrastructure resilience, enhanced early warning systems, and more effective evacuation plans. Moreover, the economic consequences of the hurricanes have underscored the importance of insurance coverage and disaster relief programs in mitigating financial losses for affected communities. As climate change continues to intensify extreme weather events, it is imperative that proactive measures are taken to protect vulnerable populations and build more resilient communities capable of withstanding the destructive force of future hurricanes.\n" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "**Revision Number**: 2" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "---\n", + "\n" + ] + }, + { + "data": { + "text/markdown": [ + "# reflect" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "Okay, here's a critique of your 3-paragraph essay, \"The Destructive Force of Hurricanes Helene and Milton in 2024,\" along with recommendations for improvement.\n", + "\n", + "**Overall Impression:**\n", + "\n", + "The essay provides a decent overview of the hypothetical impact of Hurricanes Helene and Milton in 2024. It touches upon key aspects like the damage caused, contrasting impacts, and long-term consequences. However, it lacks depth, specific examples, and a compelling narrative. The language is somewhat generic and could be more engaging.\n", + "\n", + "**Specific Feedback and Recommendations:**\n", + "\n", + "**Paragraph 1: Introduction**\n", + "\n", + "* **Specificity:** While you mention the storms caused billions in damage and loss of life, these statements lack impact without specific numbers or examples. *Recommendation: Add estimated death tolls (even if hypothetical), specific dollar amounts for damage in each state, and perhaps a brief, impactful anecdote about a family affected by one of the storms.*\n", + "* **Intensification:** You mention Helene's rapid intensification. *Recommendation: Briefly explain what factors contributed to this rapid intensification (e.g., warm water temperatures, favorable atmospheric conditions). This adds a layer of scientific understanding.*\n", + "* **Tornadoes:** You mention Milton spawned a record number of tornadoes. *Recommendation: Quantify this \"record number.\" How many tornadoes? Where were they concentrated? What was the strongest tornado's rating (e.g., EF3)?*\n", + "* **\"Unprecedented challenges for insurers\":** This is a bit vague. *Recommendation: Elaborate. What specific challenges did insurers face? Were there issues with payouts, policy coverage, or reinsurance?*\n", + "* **Length:** This paragraph is a good length, but the added details will likely make it a bit longer, which is fine.\n", + "\n", + "**Paragraph 2: Contrasting Impacts**\n", + "\n", + "* **Depth:** The contrast between the storms is a good idea, but it needs more development. *Recommendation: Instead of just saying Helene caused flooding, describe the flooding. What areas were submerged? How high did the water get? What infrastructure was affected (e.g., bridges washed out, dams breached)? For Milton, describe the types of structural damage caused by the tornadoes (e.g., homes flattened, businesses destroyed).*\n", + "* **Human Element:** The essay is very focused on infrastructure and economic damage. *Recommendation: Include a sentence or two about the human impact of each storm. For example, mention the displacement of residents, the challenges faced by first responders, or the emotional toll on affected communities.*\n", + "* **\"State-record number of fatalities\":** Again, be specific. *Recommendation: Provide the actual number of fatalities.*\n", + "* **Dollar Amounts:** You mention insured losses and total losses. *Recommendation: Clarify the difference between these figures. What accounts for the $13 billion difference between insured and total losses?*\n", + "* **Length:** This paragraph could benefit from being slightly longer to accommodate the added details.\n", + "\n", + "**Paragraph 3: Long-Term Consequences and Lessons Learned**\n", + "\n", + "* **Specificity:** \"Improved infrastructure resilience\" and \"enhanced early warning systems\" are broad terms. *Recommendation: Provide concrete examples. What specific infrastructure improvements are needed (e.g., elevating homes, building stronger levees, upgrading power grids)? How can early warning systems be enhanced (e.g., more weather stations, better communication strategies)?*\n", + "* **Evacuation Plans:** *Recommendation: What made the evacuation plans ineffective? Were there traffic problems, lack of transportation, or communication breakdowns?*\n", + "* **Insurance Coverage:** *Recommendation: Were there specific issues with insurance coverage that were revealed by these storms (e.g., underinsurance, exclusions for flood damage)?*\n", + "* **Climate Change Connection:** While mentioning climate change is good, make the connection more explicit. *Recommendation: Explain how climate change is contributing to the intensification of hurricanes and the increased frequency of extreme weather events. Cite a credible source (even a hypothetical one for 2024) to support this claim.*\n", + "* **Call to Action:** Consider ending with a stronger call to action. *Recommendation: Instead of just saying \"proactive measures are taken,\" suggest specific actions that governments, communities, and individuals can take to prepare for future hurricanes.*\n", + "* **Length:** This paragraph is a good length, but the added details will likely make it a bit longer, which is fine.\n", + "\n", + "**Style and Tone:**\n", + "\n", + "* **Engaging Language:** The writing is somewhat dry and factual. *Recommendation: Use more vivid language to describe the impact of the storms. For example, instead of saying \"widespread flooding,\" you could say \"floodwaters inundated entire neighborhoods, leaving homes submerged to their rooftops.\"*\n", + "* **Active Voice:** Use active voice whenever possible to make the writing more direct and impactful.\n", + "* **Conciseness:** Avoid unnecessary words and phrases.\n", + "\n", + "**Overall Length:**\n", + "\n", + "The essay is currently a good starting length. The added details and examples will naturally increase the length, making it more comprehensive and impactful. Aim for each paragraph to be approximately 7-10 sentences long.\n", + "\n", + "**Revised Structure Suggestion:**\n", + "\n", + "Consider restructuring the essay slightly to create a stronger narrative flow:\n", + "\n", + "1. **Introduction:** (As is, but with added details as suggested above)\n", + "2. **Hurricane Helene: A Tale of Devastating Flooding:** (Focus solely on Helene's impact, with specific examples and human stories)\n", + "3. **Hurricane Milton: A Barrage of Tornadoes:** (Focus solely on Milton's impact, with specific examples and human stories)\n", + "4. **Lessons Learned and the Path Forward:** (Combine the current third paragraph with a stronger call to action)\n", + "\n", + "By dedicating a full paragraph to each storm, you can provide a more detailed and compelling account of their individual impacts.\n", + "\n", + "**In summary, to improve your essay, focus on adding specific details, examples, and a stronger narrative voice. Make the connection to climate change more explicit and end with a compelling call to action.** Good luck!\n" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "---\n", + "\n" + ] + }, + { + "data": { + "text/markdown": [ + "# research_critique" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "North Carolina was severely impacted by Hurricane Helene during late September 2024, primarily in its western Appalachian region, causing at least 105 reported deaths and significant destruction of infrastructure and residential areas across several settlements.[1] After making landfall in the Big Bend region of Florida on September 27, the hurricane began to traverse over land across Georgia as a Category 2 hurricane and into the Appalachian mountain range as a tropical storm, depositing record-breaking amounts of rainfall across several settlements in western North Carolina, such as Asheville, Swannanoa, Spruce Pine, Chimney Rock, Montreat, Lake Lure, and several others. Of the 241 people reported victims of Hurricane Helene in the United States, 119 people were in North Carolina, surpassing the state record of 80 deaths recorded in a 1916 flood.[8][35] Of these, 72 residents of Buncombe County were among the deceased." + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "Ask the Chatbot Games & Quizzes History & Society Science & Tech Biographies Animals & Nature Geography & Travel Arts & Culture ProCon Money Videos Hurricane Helene, large, catastrophic tropical cyclone notable for its rapid intensification that ultimately produced a wide swath of damage and loss of life that extended from northwestern Florida, where the storm made landfall on September 26, 2024, to Tennessee, Georgia, and North Carolina. Although Helene’s winds had diminished to tropical-storm strength, the storm delivered a broad and deadly swath of heavy rain across the southeastern U.S. In parts of Florida, Georgia, North and South Carolina, Tennessee, and Virginia, record rainfall caused rivers to overflow and produced flash flooding, road washouts, and even landslides in some areas." + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "An EF-0 tornado impacted Martin and St. Lucie counties on October 9, 2024, as Hurricane Milton's rainbands swept across the Treasure Coast. An EF-1 tornado impacted St. Lucie County on October 9, 2024, as Hurricane Milton's outer rainbands moved across the Treasure Coast. In Fort Pierce, the tornado caused significant structural damage to a metal canopy at the St. Lucie County Sheriff's Office, with peak winds estimated at 90 mph before weakening and lifting near the I-95 and Okeechobee interchange. An EF-1 tornado touched down near the Belcher Canal in Fort Pierce, causing vegetative damage and partial roof loss near St. Lucie Blvd and N 25th St. The tornado continued northwest across Treasure Coast International Airport, where several small planes were tossed and recorded gusts of 60 mph." + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "A tropical storm watch was issued for Extreme Northwest Bahamian Islands on October 8, 2024, upgraded six hours later to a tropical storm warning.[102][103][104] Grand Bahama activated their emergency operations center.[105] The Royal Bahamas Defence Force went on standby with supplies prepared for Hurricane Milton.[106] In-person schools on Grand Bahama, Bimini, Abaco, and Grand Cay were closed.[107] The Anglican Central Education Authority closed two campuses.[108] Bahamian students in Florida were ordered to evacuate; Bahamasair made two flights on October 7 and 8 to Orlando.[109] The Grand Bahama International Airport had closed.[110] Bahamas Power and Light offices in northern islands closed October 9.[111] Banks on Grand Bahama and Abaco closed October 10.[112] A large surge in sales of hurricane-related supplies occurred.[105] ^ Hurricane Milton Spawned Record Number Of Florida Tornadoes, The Weather Channel, October 23, 2024" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "Search for News Email This Subscribe to Newsletter Email to a friend Facebook Tweet LinkedIn Print Article The 2024 hurricane season presented challenges for insurers thanks largely to hurricanes Beryl, Helene, and Milton—the three powerful storms caused more than $138 billion in damage as well as $52 million in insured losses. Hurricane Milton insured losses totaled $25 billion with a total loss of $38 billion. The company analyzed 2 million properties in the area of where Hurricane Helene hit the southeastern area, finding: 131,004 properties were impacted, 43,352 properties suffered severe damage and 3,580 properties were destroyed. Get the latest insurance news Email This Subscribe to Newsletter Email to a friend Facebook Tweet LinkedIn Print Article Email * More News More News Features Claims News Email Newsletters" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "Helene, Milton losses expected to surpass \"truly historic\" $50 billion each - CBS News Monstrous hurricanes Helene and Milton caused so much complex havoc that damages are still being added up, but government and private experts say they will likely join the infamous ranks of Katrina, Sandy and Harvey as super costly $50-billion-plus killers. But even adjusted for inflation, damages from intense storms are skyrocketing because people are building in harm's way, rebuilding costs are rising faster than inflation and human-caused climate change are making storms stronger and wetter, experts in different fields said. Of the top 10 costliest hurricanes as compiled by insurance giant Swiss Re - not including Helene or Milton yet - insured damage is about 44% of total costs." + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "Hurricane Helene (/ hɛˈliːn / ⓘ heh-LEEN) [1] was a deadly and devastating tropical cyclone that caused widespread catastrophic damage and numerous fatalities across the Southeastern United States in late September 2024." + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "Ask the Chatbot Games & Quizzes History & Society Science & Tech Biographies Animals & Nature Geography & Travel Arts & Culture ProCon Money Videos Hurricane Helene, large, catastrophic tropical cyclone notable for its rapid intensification that ultimately produced a wide swath of damage and loss of life that extended from northwestern Florida, where the storm made landfall on September 26, 2024, to Tennessee, Georgia, and North Carolina. Although Helene’s winds had diminished to tropical-storm strength, the storm delivered a broad and deadly swath of heavy rain across the southeastern U.S. In parts of Florida, Georgia, North and South Carolina, Tennessee, and Virginia, record rainfall caused rivers to overflow and produced flash flooding, road washouts, and even landslides in some areas." + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "An EF-0 tornado impacted Martin and St. Lucie counties on October 9, 2024, as Hurricane Milton's rainbands swept across the Treasure Coast. An EF-1 tornado impacted St. Lucie County on October 9, 2024, as Hurricane Milton's outer rainbands moved across the Treasure Coast. In Fort Pierce, the tornado caused significant structural damage to a metal canopy at the St. Lucie County Sheriff's Office, with peak winds estimated at 90 mph before weakening and lifting near the I-95 and Okeechobee interchange. An EF-1 tornado touched down near the Belcher Canal in Fort Pierce, causing vegetative damage and partial roof loss near St. Lucie Blvd and N 25th St. The tornado continued northwest across Treasure Coast International Airport, where several small planes were tossed and recorded gusts of 60 mph." + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "A destroyed house is seen in Lakewood Park, Florida, after a tornado hit the area and caused severe damage as Hurricane Milton swept through Florida on Oct. 10, 2024. (GIORGIO VIERA/AFP via Getty" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "Climate change caused the maximum wind speeds generated by roughly 80% of Atlantic Basin hurricanes from 2019 and 2023 to intensify by an average of 18 miles per hour, according to a new Climate Central study published in Environmental Research: Climate. Alongside this peer-reviewed study, Climate Central published a report, Climate change increased wind speeds for every 2024 Atlantic hurricane, expanding the framework to the 2024 Atlantic Basin hurricane season. Read Climate Central’s full report: Climate change increased wind speeds for every 2024 Atlantic hurricane. Climate Central’s Climate Shift Index: Ocean measures the influence of climate change on daily ocean temperatures and helps connect human-caused ocean warming to consequences, such as hurricane rapid intensification." + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "All eleven hurricanes in 2024 (as of November 10) intensified by 9-28 mph during the record-breaking ocean warmth of the 2024 hurricane season, strengthening over waters made as much as 2.5°F" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "---\n", + "\n" + ] + }, + { + "data": { + "text/markdown": [ + "# generate" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "Okay, I will use your outline to generate a 3-paragraph essay.\n", + "\n", + "**Title: The Destructive Wake of Hurricanes Helene and Milton in 2024**\n", + "\n", + "**Paragraph 1: Introduction to the 2024 Hurricane Season**\n", + "\n", + "The 2024 hurricane season proved to be a stark reminder of the increasing intensity and devastating potential of tropical cyclones, with Hurricanes Helene and Milton leaving a trail of destruction across the Southeastern United States. Helene, a large and catastrophic storm, made landfall in Florida before cutting a deadly path through Georgia and into the Appalachian region, while Milton impacted the Treasure Coast of Florida. Although both storms brought widespread damage, their impacts differed significantly due to variations in their tracks, intensities, and the vulnerabilities of the affected regions. This essay will explore the distinct consequences of Hurricanes Helene and Milton, highlighting the challenges faced by impacted communities in terms of infrastructure, economic stability, and overall resilience.\n", + "\n", + "**Paragraph 2: Contrasting Impacts of Helene and Milton**\n", + "\n", + "Hurricane Helene's impact was most acutely felt in the Appalachian region of North Carolina, where record-breaking rainfall led to catastrophic flooding and landslides. The storm claimed over 100 lives in the state and caused extensive damage to infrastructure, isolating communities and disrupting essential services. In contrast, Hurricane Milton, while less deadly, spawned a series of tornadoes in Florida, causing significant structural damage and disrupting air travel. The economic consequences also varied, with Helene severely impacting tourism and agriculture in the Appalachian region, while Milton primarily affected coastal communities and infrastructure in Florida. The differing impacts underscore the importance of considering the specific vulnerabilities of each region when preparing for and responding to hurricanes.\n", + "\n", + "**Paragraph 3: Lessons Learned and the Future of Hurricane Preparedness**\n", + "\n", + "The experiences of Hurricanes Helene and Milton in 2024 offer valuable lessons for future hurricane preparedness and disaster management. The need for resilient infrastructure, effective evacuation plans, and coordinated disaster relief efforts was evident in both cases. Furthermore, the increasing intensity of these storms highlights the urgent need to address climate change and its contribution to extreme weather events. By understanding the distinct impacts of these hurricanes and investing in proactive measures, communities can better protect themselves from the devastating consequences of future storms.\n" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "**Revision Number**: 3" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "---\n", + "\n" + ] + } + ], + "source": [ + "# Define the topic of the essay\n", + "ESSAY_TOPIC = \"What were the impacts of Hurricane Helene and Hurricane Milton in 2024?\"\n", + "\n", + "# Define a thread configuration with a unique thread ID\n", + "thread = {\"configurable\": {\"thread_id\": \"1\"}}\n", + "\n", + "# Stream through the graph execution with an initial task and state\n", + "for s in graph.stream(\n", + " {\n", + " \"task\": ESSAY_TOPIC, # Initial task\n", + " \"max_revisions\": 2, # Maximum number of revisions allowed\n", + " \"revision_number\": 1, # Current revision number\n", + " \"content\": [], # Initial empty content list\n", + " },\n", + " thread,\n", + "):\n", + " step = next(iter(s))\n", + " display(Markdown(f\"# {step}\"))\n", + " for key, content in s[step].items():\n", + " if key == \"revision_number\":\n", + " display(Markdown(f\"**Revision Number**: {content}\"))\n", + " elif isinstance(content, list):\n", + " for c in content:\n", + " display(Markdown(c))\n", + " else:\n", + " display(Markdown(content))\n", + " print(\"\\n---\\n\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "4c95d0cd7f6a" + }, + "source": [ + "### Output the final draft of the essay" + ] + }, + { + "cell_type": "code", + "execution_count": 31, + "metadata": { + "id": "e50e674081f2" + }, + "outputs": [ + { + "data": { + "text/markdown": [ + "Okay, I will use your outline to generate a 3-paragraph essay.\n", + "\n", + "**Title: The Destructive Wake of Hurricanes Helene and Milton in 2024**\n", + "\n", + "**Paragraph 1: Introduction to the 2024 Hurricane Season**\n", + "\n", + "The 2024 hurricane season proved to be a stark reminder of the increasing intensity and devastating potential of tropical cyclones, with Hurricanes Helene and Milton leaving a trail of destruction across the Southeastern United States. Helene, a large and catastrophic storm, made landfall in Florida before cutting a deadly path through Georgia and into the Appalachian region, while Milton impacted the Treasure Coast of Florida. Although both storms brought widespread damage, their impacts differed significantly due to variations in their tracks, intensities, and the vulnerabilities of the affected regions. This essay will explore the distinct consequences of Hurricanes Helene and Milton, highlighting the challenges faced by impacted communities in terms of infrastructure, economic stability, and overall resilience.\n", + "\n", + "**Paragraph 2: Contrasting Impacts of Helene and Milton**\n", + "\n", + "Hurricane Helene's impact was most acutely felt in the Appalachian region of North Carolina, where record-breaking rainfall led to catastrophic flooding and landslides. The storm claimed over 100 lives in the state and caused extensive damage to infrastructure, isolating communities and disrupting essential services. In contrast, Hurricane Milton, while less deadly, spawned a series of tornadoes in Florida, causing significant structural damage and disrupting air travel. The economic consequences also varied, with Helene severely impacting tourism and agriculture in the Appalachian region, while Milton primarily affected coastal communities and infrastructure in Florida. The differing impacts underscore the importance of considering the specific vulnerabilities of each region when preparing for and responding to hurricanes.\n", + "\n", + "**Paragraph 3: Lessons Learned and the Future of Hurricane Preparedness**\n", + "\n", + "The experiences of Hurricanes Helene and Milton in 2024 offer valuable lessons for future hurricane preparedness and disaster management. The need for resilient infrastructure, effective evacuation plans, and coordinated disaster relief efforts was evident in both cases. Furthermore, the increasing intensity of these storms highlights the urgent need to address climate change and its contribution to extreme weather events. By understanding the distinct impacts of these hurricanes and investing in proactive measures, communities can better protect themselves from the devastating consequences of future storms.\n" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "display(Markdown(s[\"generate\"][\"draft\"]))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "c01d26c8df5b" + }, + "source": [ + "## Additional Resources\n", + "\n", + "- [Google Cloud Generative AI repository on GitHub](https://github.com/GoogleCloudPlatform/generative-ai/)\n", + "- [Gemini API in Google AI Studio](https://ai.google.dev/gemini-api/docs)\n", + "- [Gemini API in Vertex AI](https://cloud.google.com/vertex-ai/generative-ai/docs/multimodal/overview)\n", + "- [LangGraph tutorials](https://langchain-ai.github.io/langgraph/tutorials/)\n", + "- [DeepLearning.AI course on AI Agents in LangGraph](https://www.deeplearning.ai/short-courses/ai-agents-in-langgraph/)" + ] + } + ], + "metadata": { + "colab": { + "name": "ai_agents_for_engineers.ipynb", + "toc_visible": true + }, + "kernelspec": { + "display_name": "Python 3", + "name": "python3" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +}